Skip to main content

plato_engine_block/
protocol.rs

1//! Protocol — text command parser and response formatter.
2
3use crate::engine::PlatoEngine;
4
5/// Handles text protocol commands for a PlatoEngine.
6pub struct ProtocolHandler;
7
8impl ProtocolHandler {
9    /// Parse and execute a text command against the engine.
10    /// Returns the response string.
11    pub fn handle(engine: &mut PlatoEngine, command: &str) -> String {
12        let cmd = command.trim();
13        let parts: Vec<&str> = cmd.splitn(3, ' ').collect();
14
15        match parts.get(0).map(|s| *s) {
16            Some("tick") => {
17                let tick = engine.tick();
18                let mut lines = Vec::new();
19                lines.push(format!("tick {} @ {:.3}s", tick.index, tick.timestamp));
20                for (name, val) in &tick.data {
21                    lines.push(format!("  {} = {:.4}", name, val));
22                }
23                // Check for any alarm fires
24                let alarms = engine.alarm_fires.drain(..).collect::<Vec<_>>();
25                for alarm_name in alarms {
26                    lines.push(format!("! ALARM: {}", alarm_name));
27                }
28                lines.join("\n")
29            }
30            Some("history") => {
31                let n: usize = parts
32                    .get(1)
33                    .and_then(|s| s.parse().ok())
34                    .unwrap_or(10);
35                let ticks = engine.history(n);
36                if ticks.is_empty() {
37                    "no history".to_string()
38                } else {
39                    let mut lines = Vec::new();
40                    for tick in ticks {
41                        let mut data_strs = Vec::new();
42                        for (name, val) in &tick.data {
43                            data_strs.push(format!("{}={:.4}", name, val));
44                        }
45                        lines.push(format!(
46                            "tick {} @ {:.3}s: {}",
47                            tick.index,
48                            tick.timestamp,
49                            data_strs.join(", ")
50                        ));
51                    }
52                    lines.join("\n")
53                }
54            }
55            Some("subscribe") => {
56                engine.subscribe();
57                "subscribed".to_string()
58            }
59            Some("unsubscribe") => {
60                engine.unsubscribe();
61                "unsubscribed".to_string()
62            }
63            Some("alarm") => {
64                match parts.get(1).map(|s| *s) {
65                    Some("list") => {
66                        let alarms = &engine.alarms;
67                        if alarms.is_empty() {
68                            "no alarms".to_string()
69                        } else {
70                            let lines: Vec<String> = alarms
71                                .iter()
72                                .map(|a| {
73                                    format!("{}: {:?} (cooldown={}ticks)", a.name, a.state, a.cooldown_ticks)
74                                })
75                                .collect();
76                            lines.join("\n")
77                        }
78                    }
79                    _ => "usage: alarm list".to_string(),
80                }
81            }
82            Some("help") => {
83                "Commands:\n\
84                 tick              — take one tick, read all sensors\n\
85                 history [N]       — show last N ticks (default 10)\n\
86                 <actuator> <val>  — set actuator to value\n\
87                 alarm list        — list alarm states\n\
88                 subscribe         — subscribe to live updates\n\
89                 unsubscribe       — unsubscribe from updates\n\
90                 help              — show this help"
91                    .to_string()
92            }
93            Some(name) => {
94                // Try as actuator command: <name> <value>
95                if let Some(val_str) = parts.get(1) {
96                    if let Ok(value) = val_str.parse::<f64>() {
97                        match engine.set_actuator(name, value) {
98                            Ok(true) => format!("{} <- {:.4}", name, value),
99                            Ok(false) => format!("error: {} rejected value", name),
100                            Err(e) => format!("error: {}", e),
101                        }
102                    } else {
103                        format!("unknown command: {}", cmd)
104                    }
105                } else {
106                    format!("unknown command: {}", cmd)
107                }
108            }
109            None => "error: empty command".to_string(),
110        }
111    }
112}