1#![forbid(unsafe_code)]
16
17use async_trait::async_trait;
18
19use serde_json::{Value, json};
20use std::sync::{Arc, Mutex};
21use wm_cognitive::reflex::safety::{SAFETY_MASK_ENV, SafetyBit, is_allowed};
22use wm_cognitive::{EventType, GanYingBus, ReflexDispatchTable};
23use wm_core::{Context, EffectRow, Gana, Resource, Tool, ToolStats};
24use wm_substrate::sensorimotor::{
25 ActuatorCommand, ActuatorKind, ReflexLoop, ReflexRule, SensorimotorBus,
26};
27
28fn actuation_refusal(table: Option<&Arc<Mutex<ReflexDispatchTable>>>) -> Option<String> {
37 let Some(table) = table else {
38 return Some(
39 "actuation denied: no safety table attached (fail-closed) — \
40 the server wires the reflex table at init"
41 .to_string(),
42 );
43 };
44 let Ok(table) = table.lock() else {
45 return Some("actuation denied: safety table mutex poisoned".to_string());
46 };
47 if is_allowed(SafetyBit::ActuatorControl.mask(), table.safety_mask()) {
48 None
49 } else {
50 Some(format!(
51 "actuation denied by safety mask {:#010x} — actuator control requires the \
52 ActuatorControl bit (opt in with {SAFETY_MASK_ENV}=0x…)",
53 table.safety_mask()
54 ))
55 }
56}
57
58pub struct SensorListTool {
62 bus: Arc<Mutex<SensorimotorBus>>,
63 stats: ToolStats,
64 effects: EffectRow,
65}
66
67impl SensorListTool {
68 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
69 Self {
70 bus,
71 stats: ToolStats::default(),
72 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
73 }
74 }
75}
76
77#[async_trait]
78impl Tool for SensorListTool {
79 fn name(&self) -> &str {
80 "sensor.list"
81 }
82 fn gana(&self) -> Gana {
83 Gana::Dipper
84 }
85 fn effects(&self) -> &EffectRow {
86 &self.effects
87 }
88 fn description(&self) -> &str {
89 "List all registered hardware sensors"
90 }
91 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
92 let Ok(bus) = self.bus.lock() else {
93 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
94 };
95 Ok(json!({
96 "sensor_count": bus.sensor_count(),
97 "sensors": bus.sensor_ids(),
98 "readings_collected": bus.readings_collected(),
99 }))
100 }
101 fn stats(&self) -> &ToolStats {
102 &self.stats
103 }
104}
105
106pub struct SensorReadTool {
108 bus: Arc<Mutex<SensorimotorBus>>,
109 stats: ToolStats,
110 effects: EffectRow,
111}
112
113impl SensorReadTool {
114 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
115 Self {
116 bus,
117 stats: ToolStats::default(),
118 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
119 }
120 }
121}
122
123#[async_trait]
124impl Tool for SensorReadTool {
125 fn name(&self) -> &str {
126 "sensor.read"
127 }
128 fn gana(&self) -> Gana {
129 Gana::Dipper
130 }
131 fn effects(&self) -> &EffectRow {
132 &self.effects
133 }
134 fn description(&self) -> &str {
135 "Read current value from a specific sensor by ID (args: sensor_id)"
136 }
137 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
138 let sensor_id = args.get("sensor_id").and_then(Value::as_str).unwrap_or("");
139 if sensor_id.is_empty() {
140 return Ok(json!({
141 "status": "error",
142 "message": "Missing required parameter: sensor_id",
143 }));
144 }
145
146 let Ok(bus) = self.bus.lock() else {
147 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
148 };
149 match bus.read_sensor(sensor_id) {
150 Some(reading) => Ok(json!({
151 "sensor_id": reading.sensor_id,
152 "kind": reading.kind.as_str(),
153 "value": reading.value,
154 "extra": reading.extra,
155 "timestamp": reading.timestamp,
156 "confidence": reading.confidence,
157 })),
158 None => Ok(json!({
159 "status": "error",
160 "message": format!("Sensor '{sensor_id}' not found or unavailable"),
161 })),
162 }
163 }
164 fn stats(&self) -> &ToolStats {
165 &self.stats
166 }
167}
168
169pub struct SensorPollTool {
171 bus: Arc<Mutex<SensorimotorBus>>,
172 gan_ying: Option<Arc<Mutex<GanYingBus>>>,
173 stats: ToolStats,
174 effects: EffectRow,
175}
176
177impl SensorPollTool {
178 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
179 Self {
180 bus,
181 gan_ying: None,
182 stats: ToolStats::default(),
183 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
184 }
185 }
186
187 pub fn with_gan_ying(
188 bus: Arc<Mutex<SensorimotorBus>>,
189 gan_ying: Arc<Mutex<GanYingBus>>,
190 ) -> Self {
191 Self {
192 bus,
193 gan_ying: Some(gan_ying),
194 stats: ToolStats::default(),
195 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
196 }
197 }
198}
199
200#[async_trait]
201impl Tool for SensorPollTool {
202 fn name(&self) -> &str {
203 "sensor.poll"
204 }
205 fn gana(&self) -> Gana {
206 Gana::Dipper
207 }
208 fn effects(&self) -> &EffectRow {
209 &self.effects
210 }
211 fn description(&self) -> &str {
212 "Poll all registered sensors and return current readings"
213 }
214 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
215 let Ok(mut bus) = self.bus.lock() else {
216 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
217 };
218 let readings = bus.poll_all();
219 let readings_json: Vec<Value> = readings
220 .iter()
221 .map(|r| {
222 json!({
223 "sensor_id": r.sensor_id,
224 "kind": r.kind.as_str(),
225 "value": r.value,
226 "extra": r.extra,
227 "timestamp": r.timestamp,
228 "confidence": r.confidence,
229 })
230 })
231 .collect();
232
233 if let Some(gan_ying) = &self.gan_ying {
235 if let Ok(mut bus) = gan_ying.lock() {
236 bus.emit(
237 EventType::SensorFrameReceived,
238 "sensorimotor",
239 json!({
240 "sensor_count": readings.len(),
241 "sensors": readings.iter().map(|r| {
242 json!({
243 "id": r.sensor_id,
244 "kind": r.kind.as_str(),
245 "value": r.value,
246 })
247 }).collect::<Vec<_>>(),
248 }),
249 );
250 }
251 }
252
253 Ok(json!({
254 "count": readings.len(),
255 "readings": readings_json,
256 }))
257 }
258 fn stats(&self) -> &ToolStats {
259 &self.stats
260 }
261}
262
263pub struct SensorHistoryTool {
265 bus: Arc<Mutex<SensorimotorBus>>,
266 stats: ToolStats,
267 effects: EffectRow,
268}
269
270impl SensorHistoryTool {
271 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
272 Self {
273 bus,
274 stats: ToolStats::default(),
275 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
276 }
277 }
278}
279
280#[async_trait]
281impl Tool for SensorHistoryTool {
282 fn name(&self) -> &str {
283 "sensor.history"
284 }
285 fn gana(&self) -> Gana {
286 Gana::Dipper
287 }
288 fn effects(&self) -> &EffectRow {
289 &self.effects
290 }
291 fn description(&self) -> &str {
292 "Get recent sensor reading history (optional args: limit)"
293 }
294 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
295 let limit = args.get("limit").and_then(Value::as_u64).unwrap_or(50) as usize;
296
297 let Ok(bus) = self.bus.lock() else {
298 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
299 };
300 let history = bus.history();
301 let limited: Vec<Value> = history
302 .iter()
303 .rev()
304 .take(limit)
305 .map(|r| {
306 json!({
307 "sensor_id": r.sensor_id,
308 "kind": r.kind.as_str(),
309 "value": r.value,
310 "timestamp": r.timestamp,
311 })
312 })
313 .collect();
314 Ok(json!({
315 "count": limited.len(),
316 "total_collected": bus.readings_collected(),
317 "readings": limited,
318 }))
319 }
320 fn stats(&self) -> &ToolStats {
321 &self.stats
322 }
323}
324
325pub struct ActuatorListTool {
329 bus: Arc<Mutex<SensorimotorBus>>,
330 stats: ToolStats,
331 effects: EffectRow,
332}
333
334impl ActuatorListTool {
335 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
336 Self {
337 bus,
338 stats: ToolStats::default(),
339 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
340 }
341 }
342}
343
344#[async_trait]
345impl Tool for ActuatorListTool {
346 fn name(&self) -> &str {
347 "actuator.list"
348 }
349 fn gana(&self) -> Gana {
350 Gana::Dipper
351 }
352 fn effects(&self) -> &EffectRow {
353 &self.effects
354 }
355 fn description(&self) -> &str {
356 "List all registered actuators"
357 }
358 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
359 let Ok(bus) = self.bus.lock() else {
360 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
361 };
362 Ok(json!({
363 "actuator_count": bus.actuator_count(),
364 "actuators": bus.actuator_ids(),
365 "commands_sent": bus.commands_sent(),
366 }))
367 }
368 fn stats(&self) -> &ToolStats {
369 &self.stats
370 }
371}
372
373pub struct ActuatorCommandTool {
375 bus: Arc<Mutex<SensorimotorBus>>,
376 gan_ying: Option<Arc<Mutex<GanYingBus>>>,
377 safety: Option<Arc<Mutex<ReflexDispatchTable>>>,
378 stats: ToolStats,
379 effects: EffectRow,
380}
381
382impl ActuatorCommandTool {
383 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
384 Self {
385 bus,
386 gan_ying: None,
387 safety: None,
388 stats: ToolStats::default(),
389 effects: EffectRow {
390 writes: vec![Resource::Galaxy("substrate".into())],
391 ..Default::default()
392 },
393 }
394 }
395
396 pub fn with_gan_ying(
397 bus: Arc<Mutex<SensorimotorBus>>,
398 gan_ying: Arc<Mutex<GanYingBus>>,
399 ) -> Self {
400 Self {
401 bus,
402 gan_ying: Some(gan_ying),
403 safety: None,
404 stats: ToolStats::default(),
405 effects: EffectRow {
406 writes: vec![Resource::Galaxy("substrate".into())],
407 ..Default::default()
408 },
409 }
410 }
411
412 #[must_use]
415 pub fn with_safety(mut self, table: Arc<Mutex<ReflexDispatchTable>>) -> Self {
416 self.safety = Some(table);
417 self
418 }
419}
420
421#[async_trait]
422impl Tool for ActuatorCommandTool {
423 fn name(&self) -> &str {
424 "actuator.command"
425 }
426 fn gana(&self) -> Gana {
427 Gana::Dipper
428 }
429 fn effects(&self) -> &EffectRow {
430 &self.effects
431 }
432 fn description(&self) -> &str {
433 "Send a command to an actuator (args: actuator_id, value, optional: kind, params). \
434 Safely gated: requires the ActuatorControl bit in the reflex safety mask."
435 }
436 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
437 let actuator_id = args
438 .get("actuator_id")
439 .and_then(Value::as_str)
440 .unwrap_or("");
441 if actuator_id.is_empty() {
442 return Ok(json!({
443 "status": "error",
444 "message": "Missing required parameter: actuator_id",
445 }));
446 }
447
448 let value = args.get("value").and_then(Value::as_f64).unwrap_or(0.0);
449
450 let kind_str = args.get("kind").and_then(Value::as_str).unwrap_or("custom");
451 let kind = parse_actuator_kind(kind_str);
452
453 let params: Vec<f64> = args
454 .get("params")
455 .and_then(Value::as_array)
456 .map(|arr| arr.iter().filter_map(Value::as_f64).collect())
457 .unwrap_or_default();
458
459 let cmd = ActuatorCommand::new(actuator_id, kind, value).with_params(params);
460
461 if let Some(reason) = actuation_refusal(self.safety.as_ref()) {
462 return Ok(json!({
463 "status": "error",
464 "blocked_by": "safety-mask",
465 "message": reason,
466 }));
467 }
468
469 let Ok(mut bus) = self.bus.lock() else {
470 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
471 };
472 match bus.send_command(&cmd) {
473 Ok(()) => {
474 if let Some(gan_ying) = &self.gan_ying {
476 if let Ok(mut gy) = gan_ying.lock() {
477 gy.emit(
478 EventType::ActuatorCommandSent,
479 "sensorimotor",
480 json!({
481 "actuator_id": actuator_id,
482 "value": value,
483 "kind": kind.as_str(),
484 }),
485 );
486 }
487 }
488 Ok(json!({
489 "status": "ok",
490 "actuator_id": actuator_id,
491 "value": value,
492 "commands_sent": bus.commands_sent(),
493 }))
494 }
495 Err(e) => Ok(json!({
496 "status": "error",
497 "message": e,
498 })),
499 }
500 }
501 fn stats(&self) -> &ToolStats {
502 &self.stats
503 }
504}
505
506pub struct ActuatorEStopTool {
508 bus: Arc<Mutex<SensorimotorBus>>,
509 gan_ying: Option<Arc<Mutex<GanYingBus>>>,
510 stats: ToolStats,
511 effects: EffectRow,
512}
513
514impl ActuatorEStopTool {
515 pub fn new(bus: Arc<Mutex<SensorimotorBus>>) -> Self {
516 Self {
517 bus,
518 gan_ying: None,
519 stats: ToolStats::default(),
520 effects: EffectRow {
521 writes: vec![Resource::Galaxy("substrate".into())],
522 ..Default::default()
523 },
524 }
525 }
526
527 pub fn with_gan_ying(
528 bus: Arc<Mutex<SensorimotorBus>>,
529 gan_ying: Arc<Mutex<GanYingBus>>,
530 ) -> Self {
531 Self {
532 bus,
533 gan_ying: Some(gan_ying),
534 stats: ToolStats::default(),
535 effects: EffectRow {
536 writes: vec![Resource::Galaxy("substrate".into())],
537 ..Default::default()
538 },
539 }
540 }
541}
542
543#[async_trait]
544impl Tool for ActuatorEStopTool {
545 fn name(&self) -> &str {
546 "actuator.estop"
547 }
548 fn gana(&self) -> Gana {
549 Gana::Dipper
550 }
551 fn effects(&self) -> &EffectRow {
552 &self.effects
553 }
554 fn description(&self) -> &str {
555 "Emergency stop all actuators"
556 }
557 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
558 let Ok(bus) = self.bus.lock() else {
559 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
560 };
561 let errors = bus.e_stop_all();
562
563 if let Some(gan_ying) = &self.gan_ying {
565 if let Ok(mut gy) = gan_ying.lock() {
566 gy.emit_with(
567 EventType::ReflexEmergencyStop,
568 "sensorimotor",
569 json!({"errors": errors.len()}),
570 0.9,
571 true,
572 );
573 }
574 }
575
576 if errors.is_empty() {
577 Ok(json!({"status": "ok", "message": "All actuators stopped"}))
578 } else {
579 Ok(json!({
580 "status": "partial",
581 "errors": errors,
582 }))
583 }
584 }
585 fn stats(&self) -> &ToolStats {
586 &self.stats
587 }
588}
589
590pub struct ReflexListTool {
594 reflex: Arc<Mutex<ReflexLoop>>,
595 stats: ToolStats,
596 effects: EffectRow,
597}
598
599impl ReflexListTool {
600 pub fn new(reflex: Arc<Mutex<ReflexLoop>>) -> Self {
601 Self {
602 reflex,
603 stats: ToolStats::default(),
604 effects: EffectRow::read_only(vec![Resource::Galaxy("substrate".into())]),
605 }
606 }
607}
608
609#[async_trait]
610impl Tool for ReflexListTool {
611 fn name(&self) -> &str {
612 "reflex.list"
613 }
614 fn gana(&self) -> Gana {
615 Gana::Dipper
616 }
617 fn effects(&self) -> &EffectRow {
618 &self.effects
619 }
620 fn description(&self) -> &str {
621 "List all registered reflex rules"
622 }
623 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
624 let Ok(reflex) = self.reflex.lock() else {
625 return Ok(json!({"status": "error", "message": "reflex mutex poisoned"}));
626 };
627 Ok(json!({
628 "rule_count": reflex.rule_count(),
629 }))
630 }
631 fn stats(&self) -> &ToolStats {
632 &self.stats
633 }
634}
635
636pub struct ReflexAddTool {
638 reflex: Arc<Mutex<ReflexLoop>>,
639 stats: ToolStats,
640 effects: EffectRow,
641}
642
643impl ReflexAddTool {
644 pub fn new(reflex: Arc<Mutex<ReflexLoop>>) -> Self {
645 Self {
646 reflex,
647 stats: ToolStats::default(),
648 effects: EffectRow {
649 writes: vec![Resource::Galaxy("substrate".into())],
650 ..Default::default()
651 },
652 }
653 }
654}
655
656#[async_trait]
657impl Tool for ReflexAddTool {
658 fn name(&self) -> &str {
659 "reflex.add"
660 }
661 fn gana(&self) -> Gana {
662 Gana::Dipper
663 }
664 fn effects(&self) -> &EffectRow {
665 &self.effects
666 }
667 fn description(&self) -> &str {
668 "Add a reflex rule (args: sensor_id, actuator_id, actuator_kind, threshold, command_value, trigger_above, cooldown_secs)"
669 }
670 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
671 let sensor_id = args.get("sensor_id").and_then(Value::as_str).unwrap_or("");
672 let actuator_id = args
673 .get("actuator_id")
674 .and_then(Value::as_str)
675 .unwrap_or("");
676 let actuator_kind = parse_actuator_kind(
677 args.get("actuator_kind")
678 .and_then(Value::as_str)
679 .unwrap_or("custom"),
680 );
681 let threshold = args.get("threshold").and_then(Value::as_f64).unwrap_or(1.0);
682 let command_value = args
683 .get("command_value")
684 .and_then(Value::as_f64)
685 .unwrap_or(0.0);
686 let trigger_above = args
687 .get("trigger_above")
688 .and_then(Value::as_bool)
689 .unwrap_or(true);
690 let cooldown_secs = args
691 .get("cooldown_secs")
692 .and_then(Value::as_f64)
693 .unwrap_or(1.0);
694
695 if sensor_id.is_empty() || actuator_id.is_empty() {
696 return Ok(json!({
697 "status": "error",
698 "message": "Missing required parameters: sensor_id and actuator_id",
699 }));
700 }
701
702 let rule = if trigger_above {
703 ReflexRule::above(
704 sensor_id,
705 actuator_id,
706 actuator_kind,
707 threshold,
708 command_value,
709 cooldown_secs,
710 )
711 } else {
712 ReflexRule::below(
713 sensor_id,
714 actuator_id,
715 actuator_kind,
716 threshold,
717 command_value,
718 cooldown_secs,
719 )
720 };
721
722 let Ok(mut reflex) = self.reflex.lock() else {
723 return Ok(json!({"status": "error", "message": "reflex mutex poisoned"}));
724 };
725 reflex.add_rule(rule);
726 Ok(json!({
727 "status": "ok",
728 "rule_count": reflex.rule_count(),
729 }))
730 }
731 fn stats(&self) -> &ToolStats {
732 &self.stats
733 }
734}
735
736pub struct ReflexEvaluateTool {
738 bus: Arc<Mutex<SensorimotorBus>>,
739 reflex: Arc<Mutex<ReflexLoop>>,
740 gan_ying: Option<Arc<Mutex<GanYingBus>>>,
741 safety: Option<Arc<Mutex<ReflexDispatchTable>>>,
742 stats: ToolStats,
743 effects: EffectRow,
744}
745
746impl ReflexEvaluateTool {
747 pub fn new(bus: Arc<Mutex<SensorimotorBus>>, reflex: Arc<Mutex<ReflexLoop>>) -> Self {
748 Self {
749 bus,
750 reflex,
751 gan_ying: None,
752 safety: None,
753 stats: ToolStats::default(),
754 effects: EffectRow {
755 writes: vec![Resource::Galaxy("substrate".into())],
756 ..Default::default()
757 },
758 }
759 }
760
761 pub fn with_gan_ying(
762 bus: Arc<Mutex<SensorimotorBus>>,
763 reflex: Arc<Mutex<ReflexLoop>>,
764 gan_ying: Arc<Mutex<GanYingBus>>,
765 ) -> Self {
766 Self {
767 bus,
768 reflex,
769 gan_ying: Some(gan_ying),
770 safety: None,
771 stats: ToolStats::default(),
772 effects: EffectRow {
773 writes: vec![Resource::Galaxy("substrate".into())],
774 ..Default::default()
775 },
776 }
777 }
778
779 #[must_use]
782 pub fn with_safety(mut self, table: Arc<Mutex<ReflexDispatchTable>>) -> Self {
783 self.safety = Some(table);
784 self
785 }
786}
787
788#[async_trait]
789impl Tool for ReflexEvaluateTool {
790 fn name(&self) -> &str {
791 "reflex.evaluate"
792 }
793 fn gana(&self) -> Gana {
794 Gana::Dipper
795 }
796 fn effects(&self) -> &EffectRow {
797 &self.effects
798 }
799 fn description(&self) -> &str {
800 "Poll sensors, evaluate reflex rules, and send any triggered actuator commands \
801 (execution is gated by the ActuatorControl bit in the reflex safety mask)"
802 }
803 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
804 let readings = {
805 let Ok(mut bus) = self.bus.lock() else {
806 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
807 };
808 bus.poll_all()
809 };
810
811 let commands = {
812 let Ok(mut reflex) = self.reflex.lock() else {
813 return Ok(json!({"status": "error", "message": "reflex mutex poisoned"}));
814 };
815 reflex.evaluate(&readings)
816 };
817
818 let mut executed = 0usize;
819 let mut blocked = 0usize;
820 let mut errors = Vec::new();
821
822 if !commands.is_empty() {
823 if let Some(gan_ying) = &self.gan_ying {
825 if let Ok(mut gy) = gan_ying.lock() {
826 gy.emit_with(
827 EventType::ReflexFired,
828 "sensorimotor",
829 json!({
830 "sensors_polled": readings.len(),
831 "commands_triggered": commands.len(),
832 }),
833 0.7,
834 true,
835 );
836 }
837 }
838
839 if let Some(reason) = actuation_refusal(self.safety.as_ref()) {
840 blocked = commands.len();
841 errors.push(reason);
842 } else {
843 let Ok(mut bus) = self.bus.lock() else {
844 return Ok(json!({"status": "error", "message": "bus mutex poisoned"}));
845 };
846 for cmd in &commands {
847 match bus.send_command(cmd) {
848 Ok(()) => executed += 1,
849 Err(e) => errors.push(e),
850 }
851 }
852 }
853 }
854
855 Ok(json!({
856 "sensors_polled": readings.len(),
857 "commands_triggered": commands.len(),
858 "commands_executed": executed,
859 "commands_blocked": blocked,
860 "errors": errors,
861 }))
862 }
863 fn stats(&self) -> &ToolStats {
864 &self.stats
865 }
866}
867
868fn parse_actuator_kind(s: &str) -> ActuatorKind {
872 match s {
873 "motor" => ActuatorKind::Motor,
874 "relay" => ActuatorKind::Relay,
875 "display" => ActuatorKind::Display,
876 "speaker" => ActuatorKind::Speaker,
877 "valve" => ActuatorKind::Valve,
878 "thermal" => ActuatorKind::Thermal,
879 _ => ActuatorKind::Custom,
880 }
881}
882
883pub fn register_sensorimotor(
891 registry: &wm_dispatch::ToolRegistry,
892 bus: Arc<Mutex<SensorimotorBus>>,
893 reflex: Arc<Mutex<ReflexLoop>>,
894 gan_ying: Option<&Arc<Mutex<GanYingBus>>>,
895 safety: Option<&Arc<Mutex<ReflexDispatchTable>>>,
896) -> wm_dispatch::ToolRegistry {
897 let poll = match gan_ying {
898 Some(gy) => SensorPollTool::with_gan_ying(bus.clone(), Arc::clone(gy)),
899 None => SensorPollTool::new(bus.clone()),
900 };
901 let cmd = match gan_ying {
902 Some(gy) => ActuatorCommandTool::with_gan_ying(bus.clone(), Arc::clone(gy)),
903 None => ActuatorCommandTool::new(bus.clone()),
904 };
905 let cmd = match safety {
906 Some(table) => cmd.with_safety(Arc::clone(table)),
907 None => cmd,
908 };
909 let estop = match &gan_ying {
910 Some(gy) => ActuatorEStopTool::with_gan_ying(bus.clone(), Arc::clone(gy)),
911 None => ActuatorEStopTool::new(bus.clone()),
912 };
913 let eval = match &gan_ying {
914 Some(gy) => ReflexEvaluateTool::with_gan_ying(bus.clone(), reflex.clone(), Arc::clone(gy)),
915 None => ReflexEvaluateTool::new(bus.clone(), reflex.clone()),
916 };
917 let eval = match safety {
918 Some(table) => eval.with_safety(Arc::clone(table)),
919 None => eval,
920 };
921 registry
922 .register(Arc::new(SensorListTool::new(bus.clone())))
923 .register(Arc::new(SensorReadTool::new(bus.clone())))
924 .register(Arc::new(poll))
925 .register(Arc::new(SensorHistoryTool::new(bus.clone())))
926 .register(Arc::new(ActuatorListTool::new(bus)))
927 .register(Arc::new(cmd))
928 .register(Arc::new(estop))
929 .register(Arc::new(ReflexListTool::new(reflex.clone())))
930 .register(Arc::new(ReflexAddTool::new(reflex)))
931 .register(Arc::new(eval))
932}
933
934#[cfg(test)]
937mod tests {
938 use super::*;
939 use wm_cognitive::reflex::safety::SAFETY_DEFAULT;
940 use wm_substrate::sensorimotor::{SensorKind, StubActuator, StubSensor};
941
942 fn make_bus() -> Arc<Mutex<SensorimotorBus>> {
943 let mut bus = SensorimotorBus::new(64);
944 bus.register_sensor(Box::new(StubSensor::new(
945 "temp0",
946 SensorKind::Temperature,
947 55.0,
948 )));
949 bus.register_sensor(Box::new(StubSensor::new("load0", SensorKind::Custom, 0.3)));
950 Arc::new(Mutex::new(bus))
951 }
952
953 fn make_actuator_bus() -> Arc<Mutex<SensorimotorBus>> {
954 let mut bus = SensorimotorBus::new(64);
955 bus.register_sensor(Box::new(StubSensor::new(
956 "temp0",
957 SensorKind::Temperature,
958 55.0,
959 )));
960 bus.register_actuator(Box::new(StubActuator::new("fan0", ActuatorKind::Motor)));
961 Arc::new(Mutex::new(bus))
962 }
963
964 fn safety_table(mask: u32) -> Arc<Mutex<ReflexDispatchTable>> {
965 Arc::new(Mutex::new(ReflexDispatchTable::new(mask)))
966 }
967
968 fn make_reflex() -> Arc<Mutex<ReflexLoop>> {
969 Arc::new(Mutex::new(ReflexLoop::new()))
970 }
971
972 #[tokio::test]
973 async fn sensor_list_tool() {
974 let tool = SensorListTool::new(make_bus());
975 let mut ctx = Context::default();
976 let result = tool.call(&mut ctx, json!({})).await.unwrap();
977 assert_eq!(result["sensor_count"], 2);
978 assert!(
979 result["sensors"]
980 .as_array()
981 .unwrap()
982 .contains(&json!("temp0"))
983 );
984 }
985
986 #[tokio::test]
987 async fn sensor_read_tool() {
988 let tool = SensorReadTool::new(make_bus());
989 let mut ctx = Context::default();
990 let result = tool
991 .call(&mut ctx, json!({"sensor_id": "temp0"}))
992 .await
993 .unwrap();
994 assert_eq!(result["sensor_id"], "temp0");
995 assert!((result["value"].as_f64().unwrap() - 55.0).abs() < 0.01);
996 }
997
998 #[tokio::test]
999 async fn sensor_read_missing_id() {
1000 let tool = SensorReadTool::new(make_bus());
1001 let mut ctx = Context::default();
1002 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1003 assert_eq!(result["status"], "error");
1004 }
1005
1006 #[tokio::test]
1007 async fn sensor_read_nonexistent() {
1008 let tool = SensorReadTool::new(make_bus());
1009 let mut ctx = Context::default();
1010 let result = tool
1011 .call(&mut ctx, json!({"sensor_id": "nonexistent"}))
1012 .await
1013 .unwrap();
1014 assert_eq!(result["status"], "error");
1015 }
1016
1017 #[tokio::test]
1018 async fn sensor_poll_tool() {
1019 let tool = SensorPollTool::new(make_bus());
1020 let mut ctx = Context::default();
1021 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1022 assert_eq!(result["count"], 2);
1023 assert!(result["readings"].is_array());
1024 }
1025
1026 #[tokio::test]
1027 async fn sensor_history_tool() {
1028 let bus = make_bus();
1029 {
1030 let mut b = bus.lock().unwrap();
1031 let _ = b.poll_all();
1032 let _ = b.poll_all();
1033 }
1034 let tool = SensorHistoryTool::new(bus);
1035 let mut ctx = Context::default();
1036 let result = tool.call(&mut ctx, json!({"limit": 5})).await.unwrap();
1037 assert!(result["count"].as_u64().unwrap() > 0);
1038 }
1039
1040 #[tokio::test]
1041 async fn actuator_list_tool() {
1042 let tool = ActuatorListTool::new(make_bus());
1043 let mut ctx = Context::default();
1044 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1045 assert_eq!(result["actuator_count"], 0);
1046 }
1047
1048 #[tokio::test]
1049 async fn actuator_command_missing_id() {
1050 let tool = ActuatorCommandTool::new(make_bus());
1051 let mut ctx = Context::default();
1052 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1053 assert_eq!(result["status"], "error");
1054 }
1055
1056 #[tokio::test]
1057 async fn actuator_command_nonexistent() {
1058 let tool = ActuatorCommandTool::new(make_bus());
1059 let mut ctx = Context::default();
1060 let result = tool
1061 .call(
1062 &mut ctx,
1063 json!({"actuator_id": "nonexistent", "value": 0.5}),
1064 )
1065 .await
1066 .unwrap();
1067 assert_eq!(result["status"], "error");
1068 }
1069
1070 #[tokio::test]
1071 async fn actuator_estop_tool() {
1072 let tool = ActuatorEStopTool::new(make_bus());
1073 let mut ctx = Context::default();
1074 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1075 assert_eq!(result["status"], "ok");
1076 }
1077
1078 #[tokio::test]
1079 async fn actuator_command_fails_closed_without_safety_table() {
1080 let tool = ActuatorCommandTool::new(make_actuator_bus());
1081 let mut ctx = Context::default();
1082 let result = tool
1083 .call(&mut ctx, json!({"actuator_id": "fan0", "value": 0.5}))
1084 .await
1085 .unwrap();
1086 assert_eq!(result["status"], "error");
1087 assert_eq!(result["blocked_by"], "safety-mask");
1088 }
1089
1090 #[tokio::test]
1091 async fn actuator_command_denied_by_default_mask() {
1092 let tool =
1093 ActuatorCommandTool::new(make_actuator_bus()).with_safety(safety_table(SAFETY_DEFAULT));
1094 let mut ctx = Context::default();
1095 let result = tool
1096 .call(&mut ctx, json!({"actuator_id": "fan0", "value": 0.5}))
1097 .await
1098 .unwrap();
1099 assert_eq!(result["status"], "error");
1100 assert_eq!(result["blocked_by"], "safety-mask");
1101 }
1102
1103 #[tokio::test]
1104 async fn actuator_command_allowed_with_actuator_bit() {
1105 let tool = ActuatorCommandTool::new(make_actuator_bus())
1106 .with_safety(safety_table(SafetyBit::ActuatorControl.mask()));
1107 let mut ctx = Context::default();
1108 let result = tool
1109 .call(&mut ctx, json!({"actuator_id": "fan0", "value": 0.5}))
1110 .await
1111 .unwrap();
1112 assert_eq!(result["status"], "ok");
1113 assert_eq!(result["actuator_id"], "fan0");
1114 }
1115
1116 #[tokio::test]
1117 async fn reflex_list_tool() {
1118 let tool = ReflexListTool::new(make_reflex());
1119 let mut ctx = Context::default();
1120 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1121 assert_eq!(result["rule_count"], 0);
1122 }
1123
1124 #[tokio::test]
1125 async fn reflex_add_tool() {
1126 let reflex = make_reflex();
1127 let tool = ReflexAddTool::new(reflex);
1128 let mut ctx = Context::default();
1129 let result = tool
1130 .call(
1131 &mut ctx,
1132 json!({
1133 "sensor_id": "temp0",
1134 "actuator_id": "fan0",
1135 "actuator_kind": "motor",
1136 "threshold": 70.0,
1137 "command_value": 1.0,
1138 "trigger_above": true,
1139 "cooldown_secs": 5.0,
1140 }),
1141 )
1142 .await
1143 .unwrap();
1144 assert_eq!(result["status"], "ok");
1145 assert_eq!(result["rule_count"], 1);
1146 }
1147
1148 #[tokio::test]
1149 async fn reflex_add_missing_params() {
1150 let tool = ReflexAddTool::new(make_reflex());
1151 let mut ctx = Context::default();
1152 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1153 assert_eq!(result["status"], "error");
1154 }
1155
1156 #[tokio::test]
1157 async fn reflex_evaluate_tool() {
1158 let bus = make_bus();
1159 let reflex = make_reflex();
1160
1161 {
1163 let mut r = reflex.lock().unwrap();
1164 r.add_rule(ReflexRule::above(
1165 "temp0",
1166 "fan0",
1167 ActuatorKind::Motor,
1168 50.0,
1169 1.0,
1170 0.0,
1171 ));
1172 }
1173
1174 let tool = ReflexEvaluateTool::new(bus, reflex);
1176 let mut ctx = Context::default();
1177 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1178 assert_eq!(result["sensors_polled"], 2);
1179 assert!(result["commands_triggered"].as_u64().unwrap() > 0);
1181 }
1182
1183 #[tokio::test]
1184 async fn reflex_evaluate_blocks_commands_without_safety_table() {
1185 let bus = make_actuator_bus();
1186 let reflex = make_reflex();
1187 {
1188 let mut r = reflex.lock().unwrap();
1189 r.add_rule(ReflexRule::above(
1190 "temp0",
1191 "fan0",
1192 ActuatorKind::Motor,
1193 50.0,
1194 1.0,
1195 0.0,
1196 ));
1197 }
1198
1199 let tool = ReflexEvaluateTool::new(bus, reflex);
1200 let mut ctx = Context::default();
1201 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1202 let triggered = result["commands_triggered"].as_u64().unwrap();
1203 assert!(triggered > 0);
1204 assert_eq!(result["commands_executed"], 0);
1205 assert_eq!(result["commands_blocked"].as_u64().unwrap(), triggered);
1206 }
1207
1208 #[tokio::test]
1209 async fn reflex_evaluate_executes_with_actuator_bit() {
1210 let bus = make_actuator_bus();
1211 let reflex = make_reflex();
1212 {
1213 let mut r = reflex.lock().unwrap();
1214 r.add_rule(ReflexRule::above(
1215 "temp0",
1216 "fan0",
1217 ActuatorKind::Motor,
1218 50.0,
1219 1.0,
1220 0.0,
1221 ));
1222 }
1223
1224 let tool = ReflexEvaluateTool::new(bus, reflex)
1225 .with_safety(safety_table(SafetyBit::ActuatorControl.mask()));
1226 let mut ctx = Context::default();
1227 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1228 assert!(result["commands_triggered"].as_u64().unwrap() > 0);
1229 assert_eq!(result["commands_executed"], 1);
1230 assert_eq!(result["commands_blocked"], 0);
1231 }
1232
1233 #[tokio::test]
1234 async fn parse_actuator_kind_all_variants() {
1235 assert_eq!(parse_actuator_kind("motor"), ActuatorKind::Motor);
1236 assert_eq!(parse_actuator_kind("relay"), ActuatorKind::Relay);
1237 assert_eq!(parse_actuator_kind("display"), ActuatorKind::Display);
1238 assert_eq!(parse_actuator_kind("speaker"), ActuatorKind::Speaker);
1239 assert_eq!(parse_actuator_kind("valve"), ActuatorKind::Valve);
1240 assert_eq!(parse_actuator_kind("thermal"), ActuatorKind::Thermal);
1241 assert_eq!(parse_actuator_kind("unknown"), ActuatorKind::Custom);
1242 }
1243
1244 #[tokio::test]
1245 async fn register_sensorimotor_returns_registry() {
1246 let registry = wm_dispatch::ToolRegistry::new();
1247 let bus = make_bus();
1248 let reflex = make_reflex();
1249 let registered = register_sensorimotor(®istry, bus, reflex, None, None);
1250 assert!(registered.len() >= 10);
1251 }
1252
1253 #[tokio::test]
1254 async fn register_sensorimotor_with_safety_table_gates_actuation() {
1255 let registry = wm_dispatch::ToolRegistry::new();
1256 let bus = make_actuator_bus();
1257 let reflex = make_reflex();
1258 let table = safety_table(SAFETY_DEFAULT);
1259 let registered = register_sensorimotor(®istry, bus, reflex, None, Some(&table));
1260
1261 let tool = registered.get("actuator.command").expect("registered");
1262 let mut ctx = Context::default();
1263 let result = tool
1264 .call(&mut ctx, json!({"actuator_id": "fan0", "value": 1.0}))
1265 .await
1266 .unwrap();
1267 assert_eq!(result["blocked_by"], "safety-mask");
1268 }
1269}