plato_engine_block/
lib.rs1#![cfg_attr(not(feature = "std"), no_std)]
16
17#[cfg(not(feature = "std"))]
18extern crate alloc;
19
20#[cfg(not(feature = "std"))]
21use alloc::{string::String, string::ToString, vec::Vec, format, boxed::Box, collections::BTreeMap};
22
23
24
25pub mod engine;
26pub mod tick;
27pub mod sensor;
28pub mod actuator;
29pub mod alarm;
30pub mod history;
31pub mod protocol;
32
33#[cfg(feature = "server")]
34pub mod server;
35
36pub use engine::{PlatoEngine, PlatoEngineBuilder};
37pub use tick::Tick;
38pub use sensor::{Sensor, SensorFn, SensorSpec};
39pub use actuator::{Actuator, ActuatorFn, ActuatorSpec};
40pub use alarm::{AlarmRule, AlarmState, AlarmCondition};
41pub use history::HistoryBuffer;
42pub use protocol::ProtocolHandler;
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47 use std::sync::atomic::{AtomicU64, Ordering};
48 use std::sync::Arc;
49
50 fn make_engine() -> PlatoEngine {
51 PlatoEngine::builder()
52 .sensor("temp", Box::new(|| 22.5))
53 .sensor("humidity", Box::new(|| 45.0))
54 .actuator("heater", Box::new(|v| v > 0.0 && v < 100.0))
55 .actuator("fan", Box::new(|_| true))
56 .tick_hz(10.0)
57 .history_capacity(50)
58 .build()
59 }
60
61 #[test]
62 fn test_engine_creation_with_builder() {
63 let engine = make_engine();
64 assert_eq!(engine.sensors.len(), 2);
65 assert_eq!(engine.actuators.len(), 2);
66 assert_eq!(engine.tick_hz, 10.0);
67 assert_eq!(engine.history.capacity(), 50);
68 }
69
70 #[test]
71 fn test_sensor_reading_and_tick() {
72 let mut engine = make_engine();
73 let tick = engine.tick();
74 assert_eq!(tick.index, 0);
75 assert_eq!(tick.data.len(), 2);
76 assert_eq!(tick.get("temp"), Some(22.5));
77 assert_eq!(tick.get("humidity"), Some(45.0));
78 }
79
80 #[test]
81 fn test_history_buffer_overflow() {
82 let mut engine = PlatoEngine::builder()
83 .sensor("x", Box::new(|| 1.0))
84 .tick_hz(1.0)
85 .history_capacity(3)
86 .build();
87 engine.tick(); engine.tick(); engine.tick(); engine.tick(); assert_eq!(engine.history.len(), 3);
92 let ticks = engine.history(3);
93 assert_eq!(ticks[0].index, 1);
94 assert_eq!(ticks[2].index, 3);
95 }
96
97 #[test]
98 fn test_actuator_command_parsing() {
99 let mut engine = make_engine();
100 let resp = engine.handle_command("heater 50.0");
101 assert!(resp.contains("heater"));
102 assert!(resp.contains("50"));
103 }
104
105 #[test]
106 fn test_actuator_execution() {
107 let mut engine = make_engine();
108 assert_eq!(engine.set_actuator("heater", 50.0), Ok(true));
109 assert_eq!(engine.set_actuator("heater", -1.0), Ok(false));
110 assert!(engine.set_actuator("nonexistent", 1.0).is_err());
111 }
112
113 #[test]
114 fn test_alarm_triggering() {
115 let mut engine = PlatoEngine::builder()
116 .sensor("temp", Box::new(|| 100.0))
117 .alarm(
118 "overheat",
119 Box::new(|data| {
120 data.iter().any(|(n, v)| n == "temp" && *v > 80.0)
121 }),
122 5,
123 )
124 .tick_hz(1.0)
125 .history_capacity(10)
126 .build();
127 let _ = engine.tick();
128 assert_eq!(engine.alarm_fires.len(), 1);
129 assert_eq!(engine.alarm_fires[0], "overheat");
130 }
131
132 #[test]
133 fn test_alarm_cooldown() {
134 let mut engine = PlatoEngine::builder()
135 .sensor("temp", Box::new(|| 100.0))
136 .alarm(
137 "overheat",
138 Box::new(|data| {
139 data.iter().any(|(n, v)| n == "temp" && *v > 80.0)
140 }),
141 5,
142 )
143 .tick_hz(1.0)
144 .history_capacity(10)
145 .build();
146 let _ = engine.tick();
148 assert_eq!(engine.alarm_fires.len(), 1);
149 engine.alarm_fires.clear();
150 let _ = engine.tick();
152 assert_eq!(engine.alarm_fires.len(), 0);
153 }
154
155 #[test]
156 fn test_alarm_not_retriggering_in_cooldown() {
157 let mut engine = PlatoEngine::builder()
158 .sensor("temp", Box::new(|| 100.0))
159 .alarm(
160 "overheat",
161 Box::new(|_| true),
162 10,
163 )
164 .tick_hz(1.0)
165 .history_capacity(100)
166 .build();
167 let _ = engine.tick(); assert_eq!(engine.alarm_fires.len(), 1);
169 engine.alarm_fires.clear();
170
171 assert_eq!(engine.alarms[0].state, crate::alarm::AlarmState::Active { since_tick: 0 });
175 }
176
177 #[test]
178 fn test_protocol_tick() {
179 let mut engine = make_engine();
180 let resp = engine.handle_command("tick");
181 assert!(resp.contains("tick 0"));
182 assert!(resp.contains("temp = 22.5"));
183 }
184
185 #[test]
186 fn test_protocol_history() {
187 let mut engine = make_engine();
188 engine.handle_command("tick");
189 engine.handle_command("tick");
190 let resp = engine.handle_command("history 2");
191 assert!(resp.contains("tick 0"));
192 assert!(resp.contains("tick 1"));
193 }
194
195 #[test]
196 fn test_protocol_history_with_limit() {
197 let mut engine = make_engine();
198 for _ in 0..5 {
199 engine.tick();
200 }
201 let resp = engine.handle_command("history 3");
202 assert!(resp.contains("tick 2"));
204 assert!(resp.contains("tick 4"));
205 assert!(!resp.contains("tick 0"));
206 assert!(!resp.contains("tick 1"));
207 }
208
209 #[test]
210 fn test_protocol_actuator() {
211 let mut engine = make_engine();
212 let resp = engine.handle_command("fan 75.0");
213 assert!(resp.contains("fan"));
214 assert!(resp.contains("75"));
215 }
216
217 #[test]
218 fn test_protocol_subscribe() {
219 let mut engine = make_engine();
220 let resp = engine.handle_command("subscribe");
221 assert_eq!(resp, "subscribed");
222 assert!(engine.streaming);
223 }
224
225 #[test]
226 fn test_protocol_unsubscribe() {
227 let mut engine = make_engine();
228 engine.subscribe();
229 let resp = engine.handle_command("unsubscribe");
230 assert_eq!(resp, "unsubscribed");
231 assert!(!engine.streaming);
232 }
233
234 #[test]
235 fn test_protocol_help() {
236 let mut engine = make_engine();
237 let resp = engine.handle_command("help");
238 assert!(resp.contains("tick"));
239 assert!(resp.contains("history"));
240 assert!(resp.contains("subscribe"));
241 }
242
243 #[test]
244 fn test_protocol_unknown() {
245 let mut engine = make_engine();
246 let resp = engine.handle_command("foobar");
247 assert!(resp.contains("unknown"));
248 }
249
250 #[test]
251 fn test_subscribe_unsubscribe_state() {
252 let mut engine = make_engine();
253 assert!(!engine.streaming);
254 engine.subscribe();
255 assert!(engine.streaming);
256 engine.unsubscribe();
257 assert!(!engine.streaming);
258 }
259
260 #[test]
261 fn test_multiple_ticks_in_history() {
262 let mut engine = make_engine();
263 for _ in 0..10 {
264 engine.tick();
265 }
266 assert_eq!(engine.history.len(), 10);
267 let ticks = engine.history(5);
268 assert_eq!(ticks.len(), 5);
269 }
270
271 #[test]
272 fn test_tick_data_contains_all_sensors() {
273 let mut engine = make_engine();
274 let tick = engine.tick();
275 assert_eq!(tick.data.len(), 2);
276 let names: Vec<&str> = tick.data.iter().map(|(n, _)| n.as_str()).collect();
277 assert!(names.contains(&"temp"));
278 assert!(names.contains(&"humidity"));
279 }
280
281 #[test]
282 fn test_history_no_data_returns_empty() {
283 let engine = make_engine();
284 let ticks = engine.history(10);
285 assert!(ticks.is_empty());
286 }
287
288 #[test]
289 fn test_builder_defaults() {
290 let engine = PlatoEngine::builder().build();
291 assert_eq!(engine.tick_hz, 1.0);
292 assert_eq!(engine.history.capacity(), 100);
293 }
294
295 #[test]
296 fn test_integration_full() {
297 let counter = Arc::new(AtomicU64::new(0));
298 let counter_clone = counter.clone();
299 let mut engine = PlatoEngine::builder()
300 .sensor(
301 "counter",
302 Box::new(move || {
303 let v = counter_clone.fetch_add(1, Ordering::SeqCst);
304 v as f64
305 }),
306 )
307 .actuator("reset", Box::new(|v| v == 0.0))
308 .alarm(
309 "high",
310 Box::new(|data| data.iter().any(|(_, v)| *v > 5.0)),
311 3,
312 )
313 .tick_hz(10.0)
314 .history_capacity(100)
315 .build();
316
317 for _ in 0..7 {
319 engine.tick();
320 }
321
322 assert_eq!(engine.history.len(), 7);
324
325 let h = engine.history(3);
327 assert_eq!(h.len(), 3);
328
329 let latest = engine.latest().unwrap();
331 assert_eq!(latest.index, 6);
332
333 assert_ne!(engine.alarms[0].state, crate::alarm::AlarmState::Idle);
337
338 let resp = engine.handle_command("history 3");
340 assert!(resp.contains("tick 4"));
341
342 let resp = engine.handle_command("reset 0.0");
344 assert!(resp.contains("reset"));
345
346 let resp = engine.handle_command("reset 1.0");
348 assert!(resp.contains("rejected"));
349 }
350}