Skip to main content

piper_driver/
heartbeat.rs

1//! Connection Monitor - Monitors incoming feedback to detect connection aliveness
2//!
3//! **Purpose**: Detect if the robot is still responding (powered on, CAN cable connected).
4//!
5//! **App Start Relative Time Pattern**:
6//! - Uses monotonic time anchored to application start
7//! - Unaffected by system clock changes (NTP, manual adjustments)
8//! - Safe to store in AtomicU64 for lock-free access
9
10use std::sync::OnceLock;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::time::{Duration, Instant};
13
14/// Global anchor point for monotonic time
15/// Set once on first access, never changes
16static APP_START: OnceLock<Instant> = OnceLock::new();
17
18/// Get monotonic time as microseconds since app start
19///
20/// This is guaranteed to be:
21/// - Monotonic (always increases)
22/// - Unaffected by system clock changes
23/// - Safe to store in AtomicU64
24fn get_monotonic_micros() -> u64 {
25    let start = APP_START.get_or_init(Instant::now);
26    start.elapsed().as_micros() as u64
27}
28
29/// Connection health monitor
30///
31/// Tracks the time since last feedback was received from the robot.
32pub struct ConnectionMonitor {
33    last_feedback: AtomicU64,
34    timeout: Duration,
35}
36
37impl ConnectionMonitor {
38    /// Create a new connection monitor
39    ///
40    /// # Parameters
41    /// - `timeout`: Maximum duration without feedback before considering connection lost
42    ///
43    /// # Example
44    /// ```
45    /// # use piper_driver::heartbeat::ConnectionMonitor;
46    /// # use std::time::Duration;
47    /// let monitor = ConnectionMonitor::new(Duration::from_secs(1));
48    /// ```
49    pub fn new(timeout: Duration) -> Self {
50        // Initialize with current time (app start relative)
51        let now = get_monotonic_micros();
52        Self {
53            last_feedback: AtomicU64::new(now),
54            timeout,
55        }
56    }
57
58    /// Check if connection is still alive
59    ///
60    /// Returns true if feedback received within timeout window
61    pub fn check_connection(&self) -> bool {
62        let last_us = self.last_feedback.load(Ordering::Relaxed);
63        let now_us = get_monotonic_micros();
64
65        // Safe subtraction: now_us is always >= last_us (monotonic)
66        let elapsed_us = now_us.saturating_sub(last_us);
67        let elapsed = Duration::from_micros(elapsed_us);
68
69        elapsed < self.timeout
70    }
71
72    /// Register that we received feedback from the robot
73    ///
74    /// Call this after processing each CAN frame to update the last feedback time.
75    pub fn register_feedback(&self) {
76        let now = get_monotonic_micros();
77        self.last_feedback.store(now, Ordering::Relaxed);
78    }
79
80    /// Get time since last feedback
81    pub fn time_since_last_feedback(&self) -> Duration {
82        let last_us = self.last_feedback.load(Ordering::Relaxed);
83        let now_us = get_monotonic_micros();
84        Duration::from_micros(now_us.saturating_sub(last_us))
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use std::thread;
92
93    #[test]
94    fn test_monotonic_time_always_increases() {
95        let t1 = get_monotonic_micros();
96        thread::sleep(Duration::from_millis(10));
97        let t2 = get_monotonic_micros();
98
99        assert!(t2 > t1, "Monotonic time should always increase");
100    }
101
102    #[test]
103    fn test_connection_monitor_initially_alive() {
104        let monitor = ConnectionMonitor::new(Duration::from_secs(1));
105        assert!(
106            monitor.check_connection(),
107            "Connection should be alive initially"
108        );
109    }
110
111    #[test]
112    fn test_connection_monitor_timeout_after_delay() {
113        let monitor = ConnectionMonitor::new(Duration::from_millis(50));
114
115        // Initially alive
116        assert!(monitor.check_connection());
117
118        // Wait for timeout
119        thread::sleep(Duration::from_millis(100));
120
121        // Should be timed out
122        assert!(
123            !monitor.check_connection(),
124            "Connection should timeout after delay"
125        );
126    }
127
128    #[test]
129    fn test_connection_monitor_feedback_resets_timer() {
130        // Use 200ms timeout so CI (macOS etc.) timer variance doesn't flake
131        let monitor = ConnectionMonitor::new(Duration::from_millis(200));
132
133        // Wait part of timeout
134        thread::sleep(Duration::from_millis(50));
135
136        // Register feedback (resets timer)
137        monitor.register_feedback();
138
139        // Wait less than timeout; even if CI oversleeps (e.g. 80ms), still under 200ms
140        thread::sleep(Duration::from_millis(60));
141
142        // Should still be alive because timer was reset at register_feedback
143        assert!(
144            monitor.check_connection(),
145            "Feedback should reset timeout timer"
146        );
147    }
148
149    #[test]
150    fn test_time_since_last_feedback() {
151        let monitor = ConnectionMonitor::new(Duration::from_secs(1));
152
153        thread::sleep(Duration::from_millis(10));
154        let elapsed = monitor.time_since_last_feedback();
155
156        assert!(elapsed >= Duration::from_millis(10));
157        // 在 CI 环境中,系统负载可能导致实际睡眠时间更长,增加容差
158        assert!(elapsed < Duration::from_millis(200)); // Should be close to 10ms, but allow for CI delays
159    }
160
161    #[test]
162    fn test_monotonic_micros_no_panic_on_system_clock_change() {
163        // This test verifies that get_monotonic_micros doesn't panic
164        // and continues to work correctly (monotonically increasing)
165        // even if system clock changes (we can't actually test NTP changes,
166        // but we verify the function doesn't panic and returns increasing values)
167
168        let mut last = get_monotonic_micros();
169
170        for _ in 0..100 {
171            thread::sleep(Duration::from_micros(100));
172            let current = get_monotonic_micros();
173            assert!(
174                current >= last,
175                "Monotonic time should never decrease (current={}, last={})",
176                current,
177                last
178            );
179            last = current;
180        }
181    }
182}