feat(hosts): trusted wall time from the system clock or a peer; set observed_at

This commit is contained in:
2026-10-22 08:44:09 -07:00
parent 9f028743b5
commit 7c8d8b74e9
9 changed files with 633 additions and 251 deletions
+5 -1
View File
@@ -17,12 +17,16 @@ categories = ["science::robotics", "network-programming"]
# and are tested without a ROS2 toolchain. Enable `--features ros2` to compile
# the rclrs-backed `TinesNode`.
default = []
ros2 = ["dep:rclrs", "dep:ros-env"]
ros2 = ["dep:rclrs", "dep:ros-env", "dep:tines"]
[dependencies]
tines-core = { path = "../tines-core", version = "0.3.0" }
tines-store = { path = "../tines-store", version = "0.3.0" }
tines-bt = { path = "../tines-bt", version = "0.3.0" }
# The host's wall-clock abstraction (`HostClock`) and the trusted-time state
# machine it drives. Only needed behind the `ros2` feature — `TinesNode` is the
# only consumer and that is compiled only with `ros2` — so keep it optional.
tines = { path = "../tines", version = "0.3.0", optional = true }
rclrs = { version = "0.8", optional = true }
# ros_env provides the ROS 2 message types (std_msgs, including
# UInt8MultiArray) by include!()ing the generated crates from
+48 -1
View File
@@ -44,6 +44,7 @@ use std::sync::mpsc::channel;
use rclrs::{Context, CreateBasicExecutor, Node as RclNode, Publisher, RclrsError, Subscription};
use ros_env::std_msgs::msg::{MultiArrayLayout, UInt8MultiArray};
use tines::HostClock;
use tines_bt::{BtTransport, Radio};
use tines_core::{
Node as TinesNodeCore, NodeEvent, NodeId, Observation, ObservationId, Store, Tick,
@@ -116,6 +117,11 @@ pub struct TinesNode {
inbound_tx: std::sync::mpsc::Sender<InboundMsg>,
/// Upper bound on the number of managed ROS2 topics.
topic_cap: usize,
/// The host's wall clock, anchored once the host establishes a trusted wall
/// clock (`HostClock`). Observations made once trusted carry their wall time
/// as `observed_at` (see [`Self::drain_inbound`]); before that they carry
/// `None`.
clock: HostClock,
}
impl TinesNode {
@@ -132,6 +138,12 @@ impl TinesNode {
/// [`rclrs::Context::default_from_env`]; the store's anchor tick is `0`;
/// ages then advance under the `Tick` values passed to
/// [`TinesNode::poll`].
///
/// Wall time starts untrusted: the store quarantines the held observations
/// it loaded until the host establishes a trusted wall clock. That happens
/// in order (see [`TinesNode::apply_trusted_time`]) — a peer's observation
/// (the youngest stored observation carrying an `observed_at`) — after which
/// observations made by this node carry that wall time as `observed_at`.
pub fn new(
me: NodeId,
log_path: &std::path::Path,
@@ -162,6 +174,7 @@ impl TinesNode {
inbound_rx,
inbound_tx,
topic_cap,
clock: HostClock::start(),
})
}
@@ -187,6 +200,7 @@ impl TinesNode {
/// Returns the number of *new* observations stored this round (from
/// [`TinesNodeCore::poll`]).
pub fn poll(&mut self, now: Tick) -> Result<usize, TinesNodeError> {
self.apply_trusted_time(now);
self.drain_inbound(now);
let stored = self.tines.poll(now);
self.publish_live_topics(now)?;
@@ -194,16 +208,49 @@ impl TinesNode {
Ok(stored)
}
/// Trusted wall-time state machine, run at the head of each [`poll`].
///
/// If the host is not yet trusted, derive a wall-clock estimate from a
/// peer's observation: scan the observations stored since this open (the
/// store's held data is quarantined, hence invisible to the scan) for one
/// carrying an `observed_at`, taking the youngest (smallest `age_ms`) as
/// the best estimate, which is never later than true time. Anchor the
/// clock once on the first trusted time; thereafter the clock returns
/// `Some` and observations made here carry an `observed_at`. If no peer
/// time is available, the host stays quarantined — observations keep
/// carrying `None` — the robot's own fail-safe is outside tines.
fn apply_trusted_time(&mut self, now: Tick) {
if self.clock.is_trusted() {
return;
}
let Some(est) = self.tines.store_mut().peer_time_estimate(now) else {
return;
};
let report = self.tines.store_mut().set_wall_clock(est, now);
self.clock.anchor_wall(est, now);
match report.downtime_ms {
Some(ms) => println!("tines: trusted wall clock from peer (folded {ms} ms downtime)"),
None => {
println!("tines: trusted wall clock from peer, downtime unknown");
}
}
}
/// Consume inbound ROS2 observations into the tines node, skipping anything
/// oversized for tines to reject.
///
/// Each observation carries the host's current wall time as `observed_at`
/// once the clock is trusted (see [`Self::apply_trusted_time`]); `None`
/// before then (R4).
fn drain_inbound(&mut self, now: Tick) {
let observed_at = self.clock.wall_ms(now);
while let Ok((topic, payload)) = self.inbound_rx.try_recv() {
if payload.len() > tines_core::consts::MAX_PAYLOAD
|| topic.len() > tines_core::consts::MAX_TOPIC
{
continue; // oversized; tines would reject it anyway
}
let _ = self.tines.observe(&topic, &payload, None, now);
let _ = self.tines.observe(&topic, &payload, observed_at, now);
}
}