feat(ros2): local trusted time and decision J's peer-time window
- S4: TinesNode::trust_system_clock(now) and trust_wall_clock(wall, now) give a ROS2-only fleet a local trusted-time source, with the same plausibility bounds as the service (tines::trust). - Decision J: poll collects peer estimates for a window (default 10 s, set_peer_time_window_ms) from the first plausible one and adopts the latest, once; implausible estimates (B3) are never adopted. - S5: adoption goes through tines::trust::adopt_wall_clock, which flushes the store right after set_wall_clock. - has_trusted_time() reports whether a wall time was adopted. The `ros2` feature is outside the default gate; the shared logic is tested in `tines`, and these methods were type-checked against it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+98
-21
@@ -44,6 +44,7 @@ use std::sync::mpsc::channel;
|
|||||||
use rclrs::{Context, CreateBasicExecutor, Node as RclNode, Publisher, RclrsError, Subscription};
|
use rclrs::{Context, CreateBasicExecutor, Node as RclNode, Publisher, RclrsError, Subscription};
|
||||||
use ros_env::std_msgs::msg::{MultiArrayLayout, UInt8MultiArray};
|
use ros_env::std_msgs::msg::{MultiArrayLayout, UInt8MultiArray};
|
||||||
|
|
||||||
|
use tines::trust::{adopt_wall_clock, system_wall_ms, PeerTimeWindow};
|
||||||
use tines::HostClock;
|
use tines::HostClock;
|
||||||
use tines_bt::{BtTransport, Radio};
|
use tines_bt::{BtTransport, Radio};
|
||||||
use tines_core::{
|
use tines_core::{
|
||||||
@@ -122,6 +123,9 @@ pub struct TinesNode {
|
|||||||
/// as `observed_at` (see [`Self::drain_inbound`]); before that they carry
|
/// as `observed_at` (see [`Self::drain_inbound`]); before that they carry
|
||||||
/// `None`.
|
/// `None`.
|
||||||
clock: HostClock,
|
clock: HostClock,
|
||||||
|
/// Decision J's peer-time window: collects peer estimates from the first
|
||||||
|
/// plausible one and yields the latest, once.
|
||||||
|
window: PeerTimeWindow,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TinesNode {
|
impl TinesNode {
|
||||||
@@ -140,10 +144,19 @@ impl TinesNode {
|
|||||||
/// [`TinesNode::poll`].
|
/// [`TinesNode::poll`].
|
||||||
///
|
///
|
||||||
/// Wall time starts untrusted: the store quarantines the held observations
|
/// Wall time starts untrusted: the store quarantines the held observations
|
||||||
/// it loaded until the host establishes a trusted wall clock. That happens
|
/// it loaded until the host establishes a trusted wall clock. It comes, in
|
||||||
/// in order (see [`TinesNode::apply_trusted_time`]) — a peer's observation
|
/// order, from:
|
||||||
/// (the youngest stored observation carrying an `observed_at`) — after which
|
///
|
||||||
/// observations made by this node carry that wall time as `observed_at`.
|
/// 1. a local source the application trusts: call
|
||||||
|
/// [`TinesNode::trust_system_clock`] (or [`TinesNode::trust_wall_clock`]
|
||||||
|
/// with an RTC or GPS reading) right after construction;
|
||||||
|
/// 2. otherwise peers: [`TinesNode::poll`] collects their estimates for a
|
||||||
|
/// window (default 10 s, see [`TinesNode::set_peer_time_window_ms`])
|
||||||
|
/// and adopts the latest (spec decision J).
|
||||||
|
///
|
||||||
|
/// Either way the time is adopted once per run, and only if it's plausible
|
||||||
|
/// ([`tines::trust::is_plausible_wall_ms`]). After that, observations made
|
||||||
|
/// by this node carry the wall time as `observed_at`.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
me: NodeId,
|
me: NodeId,
|
||||||
log_path: &std::path::Path,
|
log_path: &std::path::Path,
|
||||||
@@ -175,9 +188,67 @@ impl TinesNode {
|
|||||||
inbound_tx,
|
inbound_tx,
|
||||||
topic_cap,
|
topic_cap,
|
||||||
clock: HostClock::start(),
|
clock: HostClock::start(),
|
||||||
|
window: PeerTimeWindow::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the peer-time window: how long, from the first plausible peer
|
||||||
|
/// estimate, [`TinesNode::poll`] keeps collecting estimates before
|
||||||
|
/// adopting the latest (spec decision J). Defaults to
|
||||||
|
/// [`tines::trust::DEFAULT_PEER_TIME_WINDOW_MS`]. Call it before the first
|
||||||
|
/// poll; a call restarts any window already open.
|
||||||
|
pub fn set_peer_time_window_ms(&mut self, window_ms: u64) {
|
||||||
|
self.window = PeerTimeWindow::new(window_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trust the system clock: adopt its current reading as the wall time at
|
||||||
|
/// tick `now` (the same monotonic tick passed to [`TinesNode::poll`]).
|
||||||
|
///
|
||||||
|
/// The ROS2 counterpart of the service's `trusted_clock = "system"`. Call
|
||||||
|
/// it right after construction when the machine's clock is reliable (an
|
||||||
|
/// RTC, or an NTP- or GPS-disciplined clock), ideally once the clock has
|
||||||
|
/// synced. Returns `false`, changing nothing, if the system clock is
|
||||||
|
/// implausible (a board that booted before NTP synced, reading 1970) —
|
||||||
|
/// logged to stderr; the node then takes its time from peers — or if the
|
||||||
|
/// node already has a trusted wall time.
|
||||||
|
pub fn trust_system_clock(&mut self, now: Tick) -> bool {
|
||||||
|
match system_wall_ms() {
|
||||||
|
Some(wall) => self.trust_wall_clock(wall, now),
|
||||||
|
None => {
|
||||||
|
eprintln!(
|
||||||
|
"tines: WARNING: the system clock is not plausible (before 2026 or \
|
||||||
|
after 2126); NOT trusting it. Falling back to peer time."
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adopt `wall_ms` (ms since the Unix epoch) from a local trusted source,
|
||||||
|
/// such as an RTC or a GPS fix, as the wall time at tick `now`.
|
||||||
|
///
|
||||||
|
/// Runs the store's fold, anchors the host clock, and flushes the store.
|
||||||
|
/// Returns `false`, changing nothing, if `wall_ms` is implausible
|
||||||
|
/// ([`tines::trust::is_plausible_wall_ms`]) or the node already has a
|
||||||
|
/// trusted wall time (adopted once per run).
|
||||||
|
pub fn trust_wall_clock(&mut self, wall_ms: u64, now: Tick) -> bool {
|
||||||
|
match adopt_wall_clock(&mut self.tines, &mut self.clock, wall_ms, now) {
|
||||||
|
Some(report) => {
|
||||||
|
match report.downtime_ms {
|
||||||
|
Some(ms) => println!("tines: trusted local clock, folded {ms} ms downtime"),
|
||||||
|
None => println!("tines: trusted local clock, downtime unknown"),
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the node has adopted a trusted wall time.
|
||||||
|
pub fn has_trusted_time(&self) -> bool {
|
||||||
|
self.clock.is_trusted()
|
||||||
|
}
|
||||||
|
|
||||||
/// The local tines node id this node was built with.
|
/// The local tines node id this node was built with.
|
||||||
pub fn id(&self) -> NodeId {
|
pub fn id(&self) -> NodeId {
|
||||||
self.tines.id()
|
self.tines.id()
|
||||||
@@ -210,29 +281,35 @@ impl TinesNode {
|
|||||||
|
|
||||||
/// Trusted wall-time state machine, run at the head of each [`poll`].
|
/// 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
|
/// While the node has no trusted wall time, re-query the store's peer
|
||||||
/// peer's observation: scan the observations stored since this open (the
|
/// estimate (drawn only from observations stored since this open; held
|
||||||
/// store's held data is quarantined, hence invisible to the scan) for one
|
/// data is quarantined, hence invisible) and feed it to the peer-time
|
||||||
/// carrying an `observed_at`, taking the youngest (smallest `age_ms`) as
|
/// window (spec decision J). The first plausible estimate opens the
|
||||||
/// the best estimate, which is never later than true time. Anchor the
|
/// window; when it closes, the latest estimate is adopted once: the store
|
||||||
/// clock once on the first trusted time; thereafter the clock returns
|
/// folds, the clock is anchored, and the store is flushed. With no peer
|
||||||
/// `Some` and observations made here carry an `observed_at`. If no peer
|
/// time the host stays quarantined — observations keep carrying `None` —
|
||||||
/// time is available, the host stays quarantined — observations keep
|
/// and the robot's own fail-safe is outside tines.
|
||||||
/// carrying `None` — the robot's own fail-safe is outside tines.
|
///
|
||||||
|
/// The estimate scans the store, O(n) per poll; it stops for good once
|
||||||
|
/// the node is trusted.
|
||||||
|
///
|
||||||
|
/// [`poll`]: TinesNode::poll
|
||||||
fn apply_trusted_time(&mut self, now: Tick) {
|
fn apply_trusted_time(&mut self, now: Tick) {
|
||||||
if self.clock.is_trusted() {
|
if self.clock.is_trusted() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(est) = self.tines.peer_time_estimate(now) else {
|
let estimate = self.tines.peer_time_estimate(now);
|
||||||
|
let Some(est) = self.window.offer(estimate, now) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let report = self.tines.set_wall_clock(est, now);
|
match adopt_wall_clock(&mut self.tines, &mut self.clock, est, now) {
|
||||||
self.clock.anchor_wall(est, now);
|
Some(report) => match report.downtime_ms {
|
||||||
match report.downtime_ms {
|
Some(ms) => {
|
||||||
Some(ms) => println!("tines: trusted wall clock from peer (folded {ms} ms downtime)"),
|
println!("tines: trusted wall clock from peer (folded {ms} ms downtime)")
|
||||||
None => {
|
}
|
||||||
println!("tines: trusted wall clock from peer, downtime unknown");
|
None => println!("tines: trusted wall clock from peer, downtime unknown"),
|
||||||
}
|
},
|
||||||
|
None => eprintln!("tines: peer wall time {est} ms rejected"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user