1#![forbid(unsafe_code)]
16
17use async_trait::async_trait;
18
19use serde_json::{Value, json};
20use std::sync::{Arc, Mutex};
21use wm_cognitive::{EventType, GanYingBus};
22use wm_core::{Context, EffectRow, Gana, Resource, Tool, ToolStats};
23use wm_substrate::sensorimotor::{
24 ActuatorCommand, ActuatorKind, ReflexLoop, ReflexRule, SensorimotorBus,
25};
26
27pub struct SensorListTool {
31 bus: Arc<Mutex<SensorimotorBus>>,
32 stats: ToolStats,
33 effects: EffectRow,
34}
35
36impl SensorListTool {
37 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
38 Self {
39 bus,
40 stats: ToolStats::default(),
41 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
42 }
43 }
44}
45
46#[async_trait]
47impl Tool for SensorListTool {
48 fn name(&self) -> &str {
49 "sensor.list"
50 }
51 fn gana(&self) -> Gana {
52 Gana::Dipper
53 }
54 fn effects(&self) -> &EffectRow {
55 &self.effects
56 }
57 fn description(&self) -> &str {
58 "List all registered hardware sensors"
59 }
60 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
61 let Ok(bus) = self.bus.lock() else {
62 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
63 };
64 Ok(json!({
65 "sensor_count": bus.sensor_count(),
66 "sensors": bus.sensor_ids(),
67 "readings_collected": bus.readings_collected(),
68 }))
69 }
70 fn stats(&self) -> &ToolStats {
71 &self.stats
72 }
73}
74
75pub struct SensorReadTool {
77 bus: Arc<Mutex<SensorimotorBus>>,
78 stats: ToolStats,
79 effects: EffectRow,
80}
81
82impl SensorReadTool {
83 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
84 Self {
85 bus,
86 stats: ToolStats::default(),
87 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
88 }
89 }
90}
91
92#[async_trait]
93impl Tool for SensorReadTool {
94 fn name(&self) -> &str {
95 "sensor.read"
96 }
97 fn gana(&self) -> Gana {
98 Gana::Dipper
99 }
100 fn effects(&self) -> &EffectRow {
101 &self.effects
102 }
103 fn description(&self) -> &str {
104 "Read current value from a specific sensor by ID (args: sensor_id)"
105 }
106 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
107 let sensor_id = args.get("sensor_id").and_then(Value::as_str).unwrap_or("");
108 if sensor_id.is_empty() {
109 return Ok(json!({
110 "status": "error",
111 "message": "Missing required parameter: sensor_id",
112 }));
113 }
114
115 let Ok(bus) = self.bus.lock() else {
116 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
117 };
118 match bus.read_sensor(sensor_id) {
119 Some(reading) => Ok(json!({
120 "sensor_id": reading.sensor_id,
121 "kind": reading.kind.as_str(),
122 "value": reading.value,
123 "extra": reading.extra,
124 "timestamp": reading.timestamp,
125 "confidence": reading.confidence,
126 })),
127 None => Ok(json!({
128 "status": "error",
129 "message": format!("Sensor '{sensor_id}' not found or unavailable"),
130 })),
131 }
132 }
133 fn stats(&self) -> &ToolStats {
134 &self.stats
135 }
136}
137
138pub struct SensorPollTool {
140 bus: Arc<Mutex<SensorimotorBus>>,
141 gan_ying: Option<Arc<Mutex<GanYingBus>>>,
142 stats: ToolStats,
143 effects: EffectRow,
144}
145
146impl SensorPollTool {
147 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
148 Self {
149 bus,
150 gan_ying: None,
151 stats: ToolStats::default(),
152 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
153 }
154 }
155
156 pub fn with_gan_ying(
157 bus: Arc<Mutex<SensorimotorBus>>,
158 gan_ying: Arc<Mutex<GanYingBus>>,
159 ) -> Self {
160 Self {
161 bus,
162 gan_ying: Some(gan_ying),
163 stats: ToolStats::default(),
164 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
165 }
166 }
167}
168
169#[async_trait]
170impl Tool for SensorPollTool {
171 fn name(&self) -> &str {
172 "sensor.poll"
173 }
174 fn gana(&self) -> Gana {
175 Gana::Dipper
176 }
177 fn effects(&self) -> &EffectRow {
178 &self.effects
179 }
180 fn description(&self) -> &str {
181 "Poll all registered sensors and return current readings"
182 }
183 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
184 let Ok(mut bus) = self.bus.lock() else {
185 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
186 };
187 let readings = bus.poll_all();
188 let readings_json: Vec<Value> = readings
189 .iter()
190 .map(|r| {
191 json!({
192 "sensor_id": r.sensor_id,
193 "kind": r.kind.as_str(),
194 "value": r.value,
195 "extra": r.extra,
196 "timestamp": r.timestamp,
197 "confidence": r.confidence,
198 })
199 })
200 .collect();
201
202 if let Some(gan_ying) = &self.gan_ying {
204 if let Ok(mut bus) = gan_ying.lock() {
205 bus.emit(
206 EventType::SensorFrameReceived,
207 "sensorimotor",
208 json!({
209 "sensor_count": readings.len(),
210 "sensors": readings.iter().map(|r| {
211 json!({
212 "id": r.sensor_id,
213 "kind": r.kind.as_str(),
214 "value": r.value,
215 })
216 }).collect::<Vec<_>>(),
217 }),
218 );
219 }
220 }
221
222 Ok(json!({
223 "count": readings.len(),
224 "readings": readings_json,
225 }))
226 }
227 fn stats(&self) -> &ToolStats {
228 &self.stats
229 }
230}
231
232pub struct SensorHistoryTool {
234 bus: Arc<Mutex<SensorimotorBus>>,
235 stats: ToolStats,
236 effects: EffectRow,
237}
238
239impl SensorHistoryTool {
240 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
241 Self {
242 bus,
243 stats: ToolStats::default(),
244 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
245 }
246 }
247}
248
249#[async_trait]
250impl Tool for SensorHistoryTool {
251 fn name(&self) -> &str {
252 "sensor.history"
253 }
254 fn gana(&self) -> Gana {
255 Gana::Dipper
256 }
257 fn effects(&self) -> &EffectRow {
258 &self.effects
259 }
260 fn description(&self) -> &str {
261 "Get recent sensor reading history (optional args: limit)"
262 }
263 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
264 let limit = args.get("limit").and_then(Value::as_u64).unwrap_or(50) as usize;
265
266 let Ok(bus) = self.bus.lock() else {
267 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
268 };
269 let history = bus.history();
270 let limited: Vec<Value> = history
271 .iter()
272 .rev()
273 .take(limit)
274 .map(|r| {
275 json!({
276 "sensor_id": r.sensor_id,
277 "kind": r.kind.as_str(),
278 "value": r.value,
279 "timestamp": r.timestamp,
280 })
281 })
282 .collect();
283 Ok(json!({
284 "count": limited.len(),
285 "total_collected": bus.readings_collected(),
286 "readings": limited,
287 }))
288 }
289 fn stats(&self) -> &ToolStats {
290 &self.stats
291 }
292}
293
294pub struct ActuatorListTool {
298 bus: Arc<Mutex<SensorimotorBus>>,
299 stats: ToolStats,
300 effects: EffectRow,
301}
302
303impl ActuatorListTool {
304 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
305 Self {
306 bus,
307 stats: ToolStats::default(),
308 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
309 }
310 }
311}
312
313#[async_trait]
314impl Tool for ActuatorListTool {
315 fn name(&self) -> &str {
316 "actuator.list"
317 }
318 fn gana(&self) -> Gana {
319 Gana::Dipper
320 }
321 fn effects(&self) -> &EffectRow {
322 &self.effects
323 }
324 fn description(&self) -> &str {
325 "List all registered actuators"
326 }
327 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
328 let Ok(bus) = self.bus.lock() else {
329 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
330 };
331 Ok(json!({
332 "actuator_count": bus.actuator_count(),
333 "actuators": bus.actuator_ids(),
334 "commands_sent": bus.commands_sent(),
335 }))
336 }
337 fn stats(&self) -> &ToolStats {
338 &self.stats
339 }
340}
341
342pub struct ActuatorCommandTool {
344 bus: Arc<Mutex<SensorimotorBus>>,
345 gan_ying: Option<Arc<Mutex<GanYingBus>>>,
346 stats: ToolStats,
347 effects: EffectRow,
348}
349
350impl ActuatorCommandTool {
351 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
352 Self {
353 bus,
354 gan_ying: None,
355 stats: ToolStats::default(),
356 effects: EffectRow {
357 writes: vec![Resource::Galaxy("substrate".into())],
358 ..Default::default()
359 },
360 }
361 }
362
363 pub fn with_gan_ying(
364 bus: Arc<Mutex<SensorimotorBus>>,
365 gan_ying: Arc<Mutex<GanYingBus>>,
366 ) -> Self {
367 Self {
368 bus,
369 gan_ying: Some(gan_ying),
370 stats: ToolStats::default(),
371 effects: EffectRow {
372 writes: vec![Resource::Galaxy("substrate".into())],
373 ..Default::default()
374 },
375 }
376 }
377}
378
379#[async_trait]
380impl Tool for ActuatorCommandTool {
381 fn name(&self) -> &str {
382 "actuator.command"
383 }
384 fn gana(&self) -> Gana {
385 Gana::Dipper
386 }
387 fn effects(&self) -> &EffectRow {
388 &self.effects
389 }
390 fn description(&self) -> &str {
391 "Send a command to an actuator (args: actuator_id, value, optional: kind, params)"
392 }
393 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
394 let actuator_id = args
395 .get("actuator_id")
396 .and_then(Value::as_str)
397 .unwrap_or("");
398 if actuator_id.is_empty() {
399 return Ok(json!({
400 "status": "error",
401 "message": "Missing required parameter: actuator_id",
402 }));
403 }
404
405 let value = args.get("value").and_then(Value::as_f64).unwrap_or(0.0);
406
407 let kind_str = args.get("kind").and_then(Value::as_str).unwrap_or("custom");
408 let kind = parse_actuator_kind(kind_str);
409
410 let params: Vec<f64> = args
411 .get("params")
412 .and_then(Value::as_array)
413 .map(|arr| arr.iter().filter_map(Value::as_f64).collect())
414 .unwrap_or_default();
415
416 let cmd = ActuatorCommand::new(actuator_id, kind, value).with_params(params);
417
418 let Ok(mut bus) = self.bus.lock() else {
419 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
420 };
421 match bus.send_command(&cmd) {
422 Ok(()) => {
423 if let Some(gan_ying) = &self.gan_ying {
425 if let Ok(mut gy) = gan_ying.lock() {
426 gy.emit(
427 EventType::ActuatorCommandSent,
428 "sensorimotor",
429 json!({
430 "actuator_id": actuator_id,
431 "value": value,
432 "kind": kind.as_str(),
433 }),
434 );
435 }
436 }
437 Ok(json!({
438 "status": "ok",
439 "actuator_id": actuator_id,
440 "value": value,
441 "commands_sent": bus.commands_sent(),
442 }))
443 }
444 Err(e) => Ok(json!({
445 "status": "error",
446 "message": e,
447 })),
448 }
449 }
450 fn stats(&self) -> &ToolStats {
451 &self.stats
452 }
453}
454
455pub struct ActuatorEStopTool {
457 bus: Arc<Mutex<SensorimotorBus>>,
458 gan_ying: Option<Arc<Mutex<GanYingBus>>>,
459 stats: ToolStats,
460 effects: EffectRow,
461}
462
463impl ActuatorEStopTool {
464 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
465 Self {
466 bus,
467 gan_ying: None,
468 stats: ToolStats::default(),
469 effects: EffectRow {
470 writes: vec![Resource::Galaxy("substrate".into())],
471 ..Default::default()
472 },
473 }
474 }
475
476 pub fn with_gan_ying(
477 bus: Arc<Mutex<SensorimotorBus>>,
478 gan_ying: Arc<Mutex<GanYingBus>>,
479 ) -> Self {
480 Self {
481 bus,
482 gan_ying: Some(gan_ying),
483 stats: ToolStats::default(),
484 effects: EffectRow {
485 writes: vec![Resource::Galaxy("substrate".into())],
486 ..Default::default()
487 },
488 }
489 }
490}
491
492#[async_trait]
493impl Tool for ActuatorEStopTool {
494 fn name(&self) -> &str {
495 "actuator.estop"
496 }
497 fn gana(&self) -> Gana {
498 Gana::Dipper
499 }
500 fn effects(&self) -> &EffectRow {
501 &self.effects
502 }
503 fn description(&self) -> &str {
504 "Emergency stop all actuators"
505 }
506 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
507 let Ok(bus) = self.bus.lock() else {
508 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
509 };
510 let errors = bus.e_stop_all();
511
512 if let Some(gan_ying) = &self.gan_ying {
514 if let Ok(mut gy) = gan_ying.lock() {
515 gy.emit_with(
516 EventType::ReflexEmergencyStop,
517 "sensorimotor",
518 json!({"errors": errors.len()}),
519 0.9,
520 true,
521 );
522 }
523 }
524
525 if errors.is_empty() {
526 Ok(json!({"status": "ok", "message": "All actuators stopped"}))
527 } else {
528 Ok(json!({
529 "status": "partial",
530 "errors": errors,
531 }))
532 }
533 }
534 fn stats(&self) -> &ToolStats {
535 &self.stats
536 }
537}
538
539pub struct ReflexListTool {
543 reflex: Arc<Mutex<ReflexLoop>>,
544 stats: ToolStats,
545 effects: EffectRow,
546}
547
548impl ReflexListTool {
549 pub fn new(reflex: Arc<Mutex<ReflexLoop>>) -> Self {
550 Self {
551 reflex,
552 stats: ToolStats::default(),
553 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
554 }
555 }
556}
557
558#[async_trait]
559impl Tool for ReflexListTool {
560 fn name(&self) -> &str {
561 "reflex.list"
562 }
563 fn gana(&self) -> Gana {
564 Gana::Dipper
565 }
566 fn effects(&self) -> &EffectRow {
567 &self.effects
568 }
569 fn description(&self) -> &str {
570 "List all registered reflex rules"
571 }
572 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
573 let Ok(reflex) = self.reflex.lock() else {
574 return Ok(json!({"status": "error", "message": "reflex mutex poisoned"}));
575 };
576 Ok(json!({
577 "rule_count": reflex.rule_count(),
578 }))
579 }
580 fn stats(&self) -> &ToolStats {
581 &self.stats
582 }
583}
584
585pub struct ReflexAddTool {
587 reflex: Arc<Mutex<ReflexLoop>>,
588 stats: ToolStats,
589 effects: EffectRow,
590}
591
592impl ReflexAddTool {
593 pub fn new(reflex: Arc<Mutex<ReflexLoop>>) -> Self {
594 Self {
595 reflex,
596 stats: ToolStats::default(),
597 effects: EffectRow {
598 writes: vec![Resource::Galaxy("substrate".into())],
599 ..Default::default()
600 },
601 }
602 }
603}
604
605#[async_trait]
606impl Tool for ReflexAddTool {
607 fn name(&self) -> &str {
608 "reflex.add"
609 }
610 fn gana(&self) -> Gana {
611 Gana::Dipper
612 }
613 fn effects(&self) -> &EffectRow {
614 &self.effects
615 }
616 fn description(&self) -> &str {
617 "Add a reflex rule (args: sensor_id, actuator_id, actuator_kind, threshold, command_value, trigger_above, cooldown_secs)"
618 }
619 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
620 let sensor_id = args.get("sensor_id").and_then(Value::as_str).unwrap_or("");
621 let actuator_id = args
622 .get("actuator_id")
623 .and_then(Value::as_str)
624 .unwrap_or("");
625 let actuator_kind = parse_actuator_kind(
626 args.get("actuator_kind")
627 .and_then(Value::as_str)
628 .unwrap_or("custom"),
629 );
630 let threshold = args.get("threshold").and_then(Value::as_f64).unwrap_or(1.0);
631 let command_value = args
632 .get("command_value")
633 .and_then(Value::as_f64)
634 .unwrap_or(0.0);
635 let trigger_above = args
636 .get("trigger_above")
637 .and_then(Value::as_bool)
638 .unwrap_or(true);
639 let cooldown_secs = args
640 .get("cooldown_secs")
641 .and_then(Value::as_f64)
642 .unwrap_or(1.0);
643
644 if sensor_id.is_empty() || actuator_id.is_empty() {
645 return Ok(json!({
646 "status": "error",
647 "message": "Missing required parameters: sensor_id and actuator_id",
648 }));
649 }
650
651 let rule = if trigger_above {
652 ReflexRule::above(
653 sensor_id,
654 actuator_id,
655 actuator_kind,
656 threshold,
657 command_value,
658 cooldown_secs,
659 )
660 } else {
661 ReflexRule::below(
662 sensor_id,
663 actuator_id,
664 actuator_kind,
665 threshold,
666 command_value,
667 cooldown_secs,
668 )
669 };
670
671 let Ok(mut reflex) = self.reflex.lock() else {
672 return Ok(json!({"status": "error", "message": "reflex mutex poisoned"}));
673 };
674 reflex.add_rule(rule);
675 Ok(json!({
676 "status": "ok",
677 "rule_count": reflex.rule_count(),
678 }))
679 }
680 fn stats(&self) -> &ToolStats {
681 &self.stats
682 }
683}
684
685pub struct ReflexEvaluateTool {
687 bus: Arc<Mutex<SensorimotorBus>>,
688 reflex: Arc<Mutex<ReflexLoop>>,
689 gan_ying: Option<Arc<Mutex<GanYingBus>>>,
690 stats: ToolStats,
691 effects: EffectRow,
692}
693
694impl ReflexEvaluateTool {
695 pub fn new(bus: Arc<Mutex<SensorimotorBus>>, reflex: Arc<Mutex<ReflexLoop>>) -> Self {
696 Self {
697 bus,
698 reflex,
699 gan_ying: None,
700 stats: ToolStats::default(),
701 effects: EffectRow {
702 writes: vec![Resource::Galaxy("substrate".into())],
703 ..Default::default()
704 },
705 }
706 }
707
708 pub fn with_gan_ying(
709 bus: Arc<Mutex<SensorimotorBus>>,
710 reflex: Arc<Mutex<ReflexLoop>>,
711 gan_ying: Arc<Mutex<GanYingBus>>,
712 ) -> Self {
713 Self {
714 bus,
715 reflex,
716 gan_ying: Some(gan_ying),
717 stats: ToolStats::default(),
718 effects: EffectRow {
719 writes: vec![Resource::Galaxy("substrate".into())],
720 ..Default::default()
721 },
722 }
723 }
724}
725
726#[async_trait]
727impl Tool for ReflexEvaluateTool {
728 fn name(&self) -> &str {
729 "reflex.evaluate"
730 }
731 fn gana(&self) -> Gana {
732 Gana::Dipper
733 }
734 fn effects(&self) -> &EffectRow {
735 &self.effects
736 }
737 fn description(&self) -> &str {
738 "Poll sensors, evaluate reflex rules, and send any triggered actuator commands"
739 }
740 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
741 let readings = {
742 let Ok(mut bus) = self.bus.lock() else {
743 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
744 };
745 bus.poll_all()
746 };
747
748 let commands = {
749 let Ok(mut reflex) = self.reflex.lock() else {
750 return Ok(json!({"status": "error", "message": "reflex mutex poisoned"}));
751 };
752 reflex.evaluate(&readings)
753 };
754
755 let mut executed = 0usize;
756 let mut errors = Vec::new();
757
758 if !commands.is_empty() {
759 if let Some(gan_ying) = &self.gan_ying {
761 if let Ok(mut gy) = gan_ying.lock() {
762 gy.emit_with(
763 EventType::ReflexFired,
764 "sensorimotor",
765 json!({
766 "sensors_polled": readings.len(),
767 "commands_triggered": commands.len(),
768 }),
769 0.7,
770 true,
771 );
772 }
773 }
774
775 let Ok(mut bus) = self.bus.lock() else {
776 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
777 };
778 for cmd in &commands {
779 match bus.send_command(cmd) {
780 Ok(()) => executed += 1,
781 Err(e) => errors.push(e),
782 }
783 }
784 }
785
786 Ok(json!({
787 "sensors_polled": readings.len(),
788 "commands_triggered": commands.len(),
789 "commands_executed": executed,
790 "errors": errors,
791 }))
792 }
793 fn stats(&self) -> &ToolStats {
794 &self.stats
795 }
796}
797
798fn parse_actuator_kind(s: &str) -> ActuatorKind {
802 match s {
803 "motor" => ActuatorKind::Motor,
804 "relay" => ActuatorKind::Relay,
805 "display" => ActuatorKind::Display,
806 "speaker" => ActuatorKind::Speaker,
807 "valve" => ActuatorKind::Valve,
808 "thermal" => ActuatorKind::Thermal,
809 _ => ActuatorKind::Custom,
810 }
811}
812
813pub fn register_sensorimotor(
818 registry: &wm_dispatch::ToolRegistry,
819 bus: Arc<Mutex<SensorimotorBus>>,
820 reflex: Arc<Mutex<ReflexLoop>>,
821 gan_ying: Option<&Arc<Mutex<GanYingBus>>>,
822) -> wm_dispatch::ToolRegistry {
823 let poll = match gan_ying {
824 Some(gy) => SensorPollTool::with_gan_ying(bus.clone(), Arc::clone(gy)),
825 None => SensorPollTool::new(bus.clone()),
826 };
827 let cmd = match gan_ying {
828 Some(gy) => ActuatorCommandTool::with_gan_ying(bus.clone(), Arc::clone(gy)),
829 None => ActuatorCommandTool::new(bus.clone()),
830 };
831 let estop = match &gan_ying {
832 Some(gy) => ActuatorEStopTool::with_gan_ying(bus.clone(), Arc::clone(gy)),
833 None => ActuatorEStopTool::new(bus.clone()),
834 };
835 let eval = match &gan_ying {
836 Some(gy) => ReflexEvaluateTool::with_gan_ying(bus.clone(), reflex.clone(), Arc::clone(gy)),
837 None => ReflexEvaluateTool::new(bus.clone(), reflex.clone()),
838 };
839 registry
840 .register(Arc::new(SensorListTool::new(bus.clone())))
841 .register(Arc::new(SensorReadTool::new(bus.clone())))
842 .register(Arc::new(poll))
843 .register(Arc::new(SensorHistoryTool::new(bus.clone())))
844 .register(Arc::new(ActuatorListTool::new(bus)))
845 .register(Arc::new(cmd))
846 .register(Arc::new(estop))
847 .register(Arc::new(ReflexListTool::new(reflex.clone())))
848 .register(Arc::new(ReflexAddTool::new(reflex)))
849 .register(Arc::new(eval))
850}
851
852#[cfg(test)]
855mod tests {
856 use super::*;
857 use wm_substrate::sensorimotor::{SensorKind, StubSensor};
858
859 fn make_bus() -> Arc<Mutex<SensorimotorBus>> {
860 let mut bus = SensorimotorBus::new(64);
861 bus.register_sensor(Box::new(StubSensor::new(
862 "temp0",
863 SensorKind::Temperature,
864 55.0,
865 )));
866 bus.register_sensor(Box::new(StubSensor::new("load0", SensorKind::Custom, 0.3)));
867 Arc::new(Mutex::new(bus))
868 }
869
870 fn make_reflex() -> Arc<Mutex<ReflexLoop>> {
871 Arc::new(Mutex::new(ReflexLoop::new()))
872 }
873
874 #[tokio::test]
875 async fn sensor_list_tool() {
876 let tool = SensorListTool::new(make_bus());
877 let mut ctx = Context::default();
878 let result = tool.call(&mut ctx, json!({})).await.unwrap();
879 assert_eq!(result["sensor_count"], 2);
880 assert!(
881 result["sensors"]
882 .as_array()
883 .unwrap()
884 .contains(&json!("temp0"))
885 );
886 }
887
888 #[tokio::test]
889 async fn sensor_read_tool() {
890 let tool = SensorReadTool::new(make_bus());
891 let mut ctx = Context::default();
892 let result = tool
893 .call(&mut ctx, json!({"sensor_id": "temp0"}))
894 .await
895 .unwrap();
896 assert_eq!(result["sensor_id"], "temp0");
897 assert!((result["value"].as_f64().unwrap() - 55.0).abs() < 0.01);
898 }
899
900 #[tokio::test]
901 async fn sensor_read_missing_id() {
902 let tool = SensorReadTool::new(make_bus());
903 let mut ctx = Context::default();
904 let result = tool.call(&mut ctx, json!({})).await.unwrap();
905 assert_eq!(result["status"], "error");
906 }
907
908 #[tokio::test]
909 async fn sensor_read_nonexistent() {
910 let tool = SensorReadTool::new(make_bus());
911 let mut ctx = Context::default();
912 let result = tool
913 .call(&mut ctx, json!({"sensor_id": "nonexistent"}))
914 .await
915 .unwrap();
916 assert_eq!(result["status"], "error");
917 }
918
919 #[tokio::test]
920 async fn sensor_poll_tool() {
921 let tool = SensorPollTool::new(make_bus());
922 let mut ctx = Context::default();
923 let result = tool.call(&mut ctx, json!({})).await.unwrap();
924 assert_eq!(result["count"], 2);
925 assert!(result["readings"].is_array());
926 }
927
928 #[tokio::test]
929 async fn sensor_history_tool() {
930 let bus = make_bus();
931 {
932 let mut b = bus.lock().unwrap();
933 let _ = b.poll_all();
934 let _ = b.poll_all();
935 }
936 let tool = SensorHistoryTool::new(bus);
937 let mut ctx = Context::default();
938 let result = tool.call(&mut ctx, json!({"limit": 5})).await.unwrap();
939 assert!(result["count"].as_u64().unwrap() > 0);
940 }
941
942 #[tokio::test]
943 async fn actuator_list_tool() {
944 let tool = ActuatorListTool::new(make_bus());
945 let mut ctx = Context::default();
946 let result = tool.call(&mut ctx, json!({})).await.unwrap();
947 assert_eq!(result["actuator_count"], 0);
948 }
949
950 #[tokio::test]
951 async fn actuator_command_missing_id() {
952 let tool = ActuatorCommandTool::new(make_bus());
953 let mut ctx = Context::default();
954 let result = tool.call(&mut ctx, json!({})).await.unwrap();
955 assert_eq!(result["status"], "error");
956 }
957
958 #[tokio::test]
959 async fn actuator_command_nonexistent() {
960 let tool = ActuatorCommandTool::new(make_bus());
961 let mut ctx = Context::default();
962 let result = tool
963 .call(
964 &mut ctx,
965 json!({"actuator_id": "nonexistent", "value": 0.5}),
966 )
967 .await
968 .unwrap();
969 assert_eq!(result["status"], "error");
970 }
971
972 #[tokio::test]
973 async fn actuator_estop_tool() {
974 let tool = ActuatorEStopTool::new(make_bus());
975 let mut ctx = Context::default();
976 let result = tool.call(&mut ctx, json!({})).await.unwrap();
977 assert_eq!(result["status"], "ok");
978 }
979
980 #[tokio::test]
981 async fn reflex_list_tool() {
982 let tool = ReflexListTool::new(make_reflex());
983 let mut ctx = Context::default();
984 let result = tool.call(&mut ctx, json!({})).await.unwrap();
985 assert_eq!(result["rule_count"], 0);
986 }
987
988 #[tokio::test]
989 async fn reflex_add_tool() {
990 let reflex = make_reflex();
991 let tool = ReflexAddTool::new(reflex);
992 let mut ctx = Context::default();
993 let result = tool
994 .call(
995 &mut ctx,
996 json!({
997 "sensor_id": "temp0",
998 "actuator_id": "fan0",
999 "actuator_kind": "motor",
1000 "threshold": 70.0,
1001 "command_value": 1.0,
1002 "trigger_above": true,
1003 "cooldown_secs": 5.0,
1004 }),
1005 )
1006 .await
1007 .unwrap();
1008 assert_eq!(result["status"], "ok");
1009 assert_eq!(result["rule_count"], 1);
1010 }
1011
1012 #[tokio::test]
1013 async fn reflex_add_missing_params() {
1014 let tool = ReflexAddTool::new(make_reflex());
1015 let mut ctx = Context::default();
1016 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1017 assert_eq!(result["status"], "error");
1018 }
1019
1020 #[tokio::test]
1021 async fn reflex_evaluate_tool() {
1022 let bus = make_bus();
1023 let reflex = make_reflex();
1024
1025 {
1027 let mut r = reflex.lock().unwrap();
1028 r.add_rule(ReflexRule::above(
1029 "temp0",
1030 "fan0",
1031 ActuatorKind::Motor,
1032 50.0,
1033 1.0,
1034 0.0,
1035 ));
1036 }
1037
1038 let tool = ReflexEvaluateTool::new(bus, reflex);
1040 let mut ctx = Context::default();
1041 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1042 assert_eq!(result["sensors_polled"], 2);
1043 assert!(result["commands_triggered"].as_u64().unwrap() > 0);
1045 }
1046
1047 #[tokio::test]
1048 async fn parse_actuator_kind_all_variants() {
1049 assert_eq!(parse_actuator_kind("motor"), ActuatorKind::Motor);
1050 assert_eq!(parse_actuator_kind("relay"), ActuatorKind::Relay);
1051 assert_eq!(parse_actuator_kind("display"), ActuatorKind::Display);
1052 assert_eq!(parse_actuator_kind("speaker"), ActuatorKind::Speaker);
1053 assert_eq!(parse_actuator_kind("valve"), ActuatorKind::Valve);
1054 assert_eq!(parse_actuator_kind("thermal"), ActuatorKind::Thermal);
1055 assert_eq!(parse_actuator_kind("unknown"), ActuatorKind::Custom);
1056 }
1057
1058 #[tokio::test]
1059 async fn register_sensorimotor_returns_registry() {
1060 let registry = wm_dispatch::ToolRegistry::new();
1061 let bus = make_bus();
1062 let reflex = make_reflex();
1063 let registered = register_sensorimotor(®istry, bus, reflex, None);
1064 assert!(registered.len() >= 10);
1065 }
1066}