piper_client/
heartbeat.rs1use std::sync::Arc;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::thread;
24use std::time::Duration;
25
26use crate::types::{Result, RobotError};
27use piper_driver::Piper as RobotPiper;
28
29#[derive(Debug, Clone)]
31pub struct HeartbeatConfig {
32 pub interval_ms: u64,
34 pub enabled: bool,
36}
37
38impl Default for HeartbeatConfig {
39 fn default() -> Self {
40 HeartbeatConfig {
41 interval_ms: 20, enabled: false, }
44 }
45}
46
47pub struct HeartbeatManager {
51 handle: Option<thread::JoinHandle<()>>,
52 shutdown: Arc<AtomicBool>,
53}
54
55impl HeartbeatManager {
56 pub fn start(robot: Arc<RobotPiper>, config: HeartbeatConfig) -> Self {
76 if !config.enabled {
77 return HeartbeatManager {
79 handle: None,
80 shutdown: Arc::new(AtomicBool::new(true)),
81 };
82 }
83
84 let shutdown = Arc::new(AtomicBool::new(false));
85 let shutdown_clone = shutdown.clone();
86
87 let handle = thread::spawn(move || {
88 Self::heartbeat_loop(robot, config, shutdown_clone);
89 });
90
91 HeartbeatManager {
92 handle: Some(handle),
93 shutdown,
94 }
95 }
96
97 fn heartbeat_loop(robot: Arc<RobotPiper>, config: HeartbeatConfig, shutdown: Arc<AtomicBool>) {
99 let interval = Duration::from_millis(config.interval_ms);
100
101 while !shutdown.load(Ordering::Relaxed) {
102 if let Err(_e) = Self::send_heartbeat(&robot) {
104 tracing::warn!("Heartbeat failed: {}", _e);
107 }
108
109 thread::sleep(interval);
110 }
111 }
112
113 fn send_heartbeat(_robot: &Arc<RobotPiper>) -> Result<()> {
125 Err(RobotError::ConfigError(
127 "Heartbeat is not supported: robot does not have a watchdog mechanism".to_string(),
128 ))
129 }
130
131 pub fn shutdown(mut self) {
135 self.shutdown.store(true, Ordering::Relaxed);
136 if let Some(handle) = self.handle.take() {
137 let _ = handle.join();
138 }
139 }
140
141 pub fn is_running(&self) -> bool {
143 !self.shutdown.load(Ordering::Relaxed)
144 }
145}
146
147impl Drop for HeartbeatManager {
148 fn drop(&mut self) {
149 self.shutdown.store(true, Ordering::Relaxed);
150 if let Some(handle) = self.handle.take() {
151 let _ = handle.join();
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 #[test]
163 fn test_heartbeat_config_default() {
164 let config = HeartbeatConfig::default();
165 assert!(!config.enabled);
167 assert_eq!(config.interval_ms, 20);
168 }
169
170 #[test]
171 fn test_heartbeat_config_disabled() {
172 let config = HeartbeatConfig {
173 enabled: false,
174 interval_ms: 10,
175 };
176 assert!(!config.enabled);
177 }
178}