1use serde::{Deserialize, Serialize};
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
48pub struct ModeRequest {
49 pub mode: String,
51
52 pub requested_by: String,
54
55 pub timestamp: u64,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub struct ModeChanged {
65 pub previous_mode: String,
67
68 pub current_mode: String,
70
71 pub nodes_started: Vec<String>,
73
74 pub nodes_stopped: Vec<String>,
76
77 pub timestamp: u64,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85pub struct ModeError {
86 pub mode: String,
88
89 pub error: String,
91
92 pub current_mode: String,
94
95 pub timestamp: u64,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106pub struct NodeLifecycleEvent {
107 pub node: String,
109
110 pub action: NodeLifecycleAction,
112
113 pub reason: String,
115
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub target_mode: Option<String>,
119
120 #[serde(default = "default_graceful_timeout")]
122 pub graceful_timeout_ms: u64,
123
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub config: Option<serde_json::Value>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131#[serde(rename_all = "lowercase")]
132pub enum NodeLifecycleAction {
133 Start,
135
136 Stop,
138
139 Kill,
141
142 Restart,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
150pub struct NodeStatusEvent {
151 pub node: String,
153
154 pub status: NodeLifecycleStatus,
156
157 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub pid: Option<u32>,
160
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub error: Option<String>,
164
165 pub timestamp: u64,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
174#[serde(rename_all = "lowercase")]
175pub enum NodeLifecycleStatus {
176 Starting,
178
179 Started,
181
182 Running,
184
185 Stopping,
187
188 Stopped,
190
191 Failed,
193}
194
195pub const TOPIC_MODE_REQUEST: &str = "system/lifecycle/mode/request";
203
204pub const TOPIC_MODE_CHANGED: &str = "system/lifecycle/mode/changed";
210
211pub const TOPIC_MODE_ERROR: &str = "system/lifecycle/mode/error";
217
218pub fn topic_node_lifecycle(node: &str) -> String {
226 format!("system/node/{}/lifecycle", node)
227}
228
229pub fn topic_node_status(node: &str) -> String {
235 format!("system/node/{}/status", node)
236}
237
238fn default_graceful_timeout() -> u64 {
242 5000
243}
244
245pub fn get_timestamp_ms() -> u64 {
247 std::time::SystemTime::now()
248 .duration_since(std::time::UNIX_EPOCH)
249 .unwrap_or_default()
250 .as_millis() as u64
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_mode_request_serialization() {
259 let request = ModeRequest {
260 mode: "simulation".to_string(),
261 requested_by: "cli".to_string(),
262 timestamp: 1234567890,
263 };
264
265 let json = serde_json::to_string(&request).unwrap();
266 let deserialized: ModeRequest = serde_json::from_str(&json).unwrap();
267
268 assert_eq!(request, deserialized);
269 }
270
271 #[test]
272 fn test_mode_changed_serialization() {
273 let changed = ModeChanged {
274 previous_mode: "startup".to_string(),
275 current_mode: "simulation".to_string(),
276 nodes_started: vec!["sim-bridge".to_string(), "object-detector".to_string()],
277 nodes_stopped: vec!["imu".to_string(), "motor".to_string()],
278 timestamp: 1234567890,
279 };
280
281 let json = serde_json::to_string(&changed).unwrap();
282 let deserialized: ModeChanged = serde_json::from_str(&json).unwrap();
283
284 assert_eq!(changed, deserialized);
285 }
286
287 #[test]
288 fn test_node_lifecycle_action_serialization() {
289 let actions = vec![
290 NodeLifecycleAction::Start,
291 NodeLifecycleAction::Stop,
292 NodeLifecycleAction::Kill,
293 NodeLifecycleAction::Restart,
294 ];
295
296 for action in actions {
297 let json = serde_json::to_string(&action).unwrap();
298 let deserialized: NodeLifecycleAction = serde_json::from_str(&json).unwrap();
299 assert_eq!(action, deserialized);
300 }
301 }
302
303 #[test]
304 fn test_node_lifecycle_status_serialization() {
305 let statuses = vec![
306 NodeLifecycleStatus::Starting,
307 NodeLifecycleStatus::Started,
308 NodeLifecycleStatus::Running,
309 NodeLifecycleStatus::Stopping,
310 NodeLifecycleStatus::Stopped,
311 NodeLifecycleStatus::Failed,
312 ];
313
314 for status in statuses {
315 let json = serde_json::to_string(&status).unwrap();
316 let deserialized: NodeLifecycleStatus = serde_json::from_str(&json).unwrap();
317 assert_eq!(status, deserialized);
318 }
319 }
320
321 #[test]
322 fn test_topic_helpers() {
323 assert_eq!(topic_node_lifecycle("test-node"), "system/node/test-node/lifecycle");
324 assert_eq!(topic_node_status("test-node"), "system/node/test-node/status");
325 }
326
327 #[test]
328 fn test_default_graceful_timeout() {
329 let event = NodeLifecycleEvent {
330 node: "test".to_string(),
331 action: NodeLifecycleAction::Stop,
332 reason: "test".to_string(),
333 target_mode: None,
334 graceful_timeout_ms: default_graceful_timeout(),
335 config: None,
336 };
337
338 assert_eq!(event.graceful_timeout_ms, 5000);
339 }
340
341 #[test]
342 fn test_get_timestamp_ms() {
343 let ts1 = get_timestamp_ms();
344 std::thread::sleep(std::time::Duration::from_millis(10));
345 let ts2 = get_timestamp_ms();
346
347 assert!(ts2 > ts1);
348 assert!(ts2 - ts1 >= 10);
349 }
350
351 #[test]
352 fn test_node_status_event_optional_fields() {
353 let event1 = NodeStatusEvent {
355 node: "test".to_string(),
356 status: NodeLifecycleStatus::Starting,
357 pid: None,
358 error: None,
359 timestamp: 1234567890,
360 };
361
362 let json1 = serde_json::to_string(&event1).unwrap();
363 assert!(!json1.contains("\"pid\""));
364 assert!(!json1.contains("\"error\""));
365
366 let event2 = NodeStatusEvent {
368 node: "test".to_string(),
369 status: NodeLifecycleStatus::Running,
370 pid: Some(12345),
371 error: Some("test error".to_string()),
372 timestamp: 1234567890,
373 };
374
375 let json2 = serde_json::to_string(&event2).unwrap();
376 assert!(json2.contains("\"pid\""));
377 assert!(json2.contains("\"error\""));
378 }
379}