mecha10_behavior_runtime/
actions.rs1use crate::{BehaviorNode, BoxedBehavior, Context, NodeRegistry, NodeStatus};
7use async_trait::async_trait;
8use mecha10_core::actuator::Twist;
9use mecha10_core::topics::Topic;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::time::{Duration, Instant};
13
14pub fn register_builtin_actions(registry: &mut NodeRegistry) {
37 registry.register("wander", |config: Value| -> anyhow::Result<BoxedBehavior> {
38 Ok(Box::new(WanderNode::from_json(config)?))
39 });
40
41 registry.register("move", |config: Value| -> anyhow::Result<BoxedBehavior> {
42 Ok(Box::new(MoveNode::from_json(config)?))
43 });
44
45 registry.register("sensor_check", |config: Value| -> anyhow::Result<BoxedBehavior> {
46 Ok(Box::new(SensorCheckNode::from_json(config)?))
47 });
48
49 registry.register("timer", |config: Value| -> anyhow::Result<BoxedBehavior> {
50 Ok(Box::new(TimerNode::from_json(config)?))
51 });
52
53 registry.register("idle", |config: Value| -> anyhow::Result<BoxedBehavior> {
54 Ok(Box::new(IdleNode::from_json(config)?))
55 });
56
57 registry.register("pause", |config: Value| -> anyhow::Result<BoxedBehavior> {
58 Ok(Box::new(PauseNode::from_json(config)?))
59 });
60
61 registry.register("stop", |config: Value| -> anyhow::Result<BoxedBehavior> {
62 Ok(Box::new(StopNode::from_json(config)?))
63 });
64
65 registry.register("plan_path", |config: Value| -> anyhow::Result<BoxedBehavior> {
66 Ok(Box::new(PlanPathNode::from_json(config)?))
67 });
68
69 registry.register("follow_path", |config: Value| -> anyhow::Result<BoxedBehavior> {
70 Ok(Box::new(FollowPathNode::from_json(config)?))
71 });
72
73 registry.register("check_arrival", |config: Value| -> anyhow::Result<BoxedBehavior> {
74 Ok(Box::new(CheckArrivalNode::from_json(config)?))
75 });
76}
77
78#[derive(Debug)]
97pub struct WanderNode {
98 topic: String,
99 linear_speed: f64,
100 angular_speed: f64,
101 change_interval: Duration,
102 last_change: Option<Instant>,
103 current_angular: f64,
104}
105
106#[derive(Debug, Serialize, Deserialize)]
107struct WanderConfig {
108 topic: String,
109 #[serde(default = "default_linear_speed")]
110 linear_speed: f64,
111 #[serde(default = "default_angular_speed")]
112 angular_speed: f64,
113 #[serde(default = "default_change_interval")]
114 change_interval_secs: f64,
115}
116
117fn default_linear_speed() -> f64 {
118 0.3
119}
120
121fn default_angular_speed() -> f64 {
122 0.5
123}
124
125fn default_change_interval() -> f64 {
126 3.0
127}
128
129impl WanderNode {
130 pub fn from_json(config: Value) -> anyhow::Result<Self> {
132 let cfg: WanderConfig = serde_json::from_value(config)?;
133 Ok(Self {
134 topic: cfg.topic,
135 linear_speed: cfg.linear_speed,
136 angular_speed: cfg.angular_speed,
137 change_interval: Duration::from_secs_f64(cfg.change_interval_secs),
138 last_change: None,
139 current_angular: 0.0,
140 })
141 }
142}
143
144#[async_trait]
145impl BehaviorNode for WanderNode {
146 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
147 let now = Instant::now();
148
149 if self.last_change.is_none() || now.duration_since(self.last_change.unwrap()) >= self.change_interval {
151 self.current_angular = (rand::random::<f64>() * 2.0 - 1.0) * self.angular_speed;
153 self.last_change = Some(now);
154 }
155
156 let twist = Twist {
158 linear: self.linear_speed as f32,
159 angular: self.current_angular as f32,
160 };
161
162 let topic_static: &'static str = Box::leak(self.topic.clone().into_boxed_str());
164 let topic = Topic::<Twist>::new(topic_static);
165
166 ctx.publish_to(topic, &twist).await?;
167 Ok(NodeStatus::Running)
168 }
169
170 fn name(&self) -> &str {
171 "WanderNode"
172 }
173}
174
175#[derive(Debug)]
193pub struct MoveNode {
194 topic: String,
195 linear: f64,
196 angular: f64,
197 duration: Option<Duration>,
198 start_time: Option<Instant>,
199}
200
201#[derive(Debug, Serialize, Deserialize)]
202struct MoveConfig {
203 topic: String,
204 linear: f64,
205 angular: f64,
206 duration_secs: Option<f64>,
207}
208
209impl MoveNode {
210 pub fn from_json(config: Value) -> anyhow::Result<Self> {
212 let cfg: MoveConfig = serde_json::from_value(config)?;
213 Ok(Self {
214 topic: cfg.topic,
215 linear: cfg.linear,
216 angular: cfg.angular,
217 duration: cfg.duration_secs.map(Duration::from_secs_f64),
218 start_time: None,
219 })
220 }
221}
222
223#[async_trait]
224impl BehaviorNode for MoveNode {
225 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
226 let now = Instant::now();
227
228 if self.start_time.is_none() {
230 self.start_time = Some(now);
231 }
232
233 if let Some(duration) = self.duration {
235 if now.duration_since(self.start_time.unwrap()) >= duration {
236 return Ok(NodeStatus::Success);
237 }
238 }
239
240 let twist = Twist {
242 linear: self.linear as f32,
243 angular: self.angular as f32,
244 };
245
246 let topic_static: &'static str = Box::leak(self.topic.clone().into_boxed_str());
248 let topic = Topic::<Twist>::new(topic_static);
249
250 ctx.publish_to(topic, &twist).await?;
251
252 Ok(NodeStatus::Running)
253 }
254
255 async fn reset(&mut self) -> anyhow::Result<()> {
256 self.start_time = None;
257 Ok(())
258 }
259
260 fn name(&self) -> &str {
261 "MoveNode"
262 }
263}
264
265#[derive(Debug)]
283pub struct SensorCheckNode {
284 #[allow(dead_code)] topic: String,
286 field: String,
287 operator: ComparisonOperator,
288 threshold: f64,
289}
290
291#[derive(Debug, Serialize, Deserialize)]
292#[serde(rename_all = "snake_case")]
293enum ComparisonOperator {
294 LessThan,
295 GreaterThan,
296 Equal,
297}
298
299#[derive(Debug, Serialize, Deserialize)]
300struct SensorCheckConfig {
301 topic: String,
302 field: String,
303 operator: ComparisonOperator,
304 threshold: f64,
305}
306
307impl SensorCheckNode {
308 pub fn from_json(config: Value) -> anyhow::Result<Self> {
310 let cfg: SensorCheckConfig = serde_json::from_value(config)?;
311 Ok(Self {
312 topic: cfg.topic,
313 field: cfg.field,
314 operator: cfg.operator,
315 threshold: cfg.threshold,
316 })
317 }
318}
319
320#[async_trait]
321impl BehaviorNode for SensorCheckNode {
322 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
323 tracing::debug!(
326 "SensorCheckNode: checking {} {} {}",
327 self.field,
328 match self.operator {
329 ComparisonOperator::LessThan => "<",
330 ComparisonOperator::GreaterThan => ">",
331 ComparisonOperator::Equal => "==",
332 },
333 self.threshold
334 );
335 Ok(NodeStatus::Success)
336 }
337
338 fn name(&self) -> &str {
339 "SensorCheckNode"
340 }
341}
342
343#[derive(Debug)]
358pub struct TimerNode {
359 duration: Duration,
360 start_time: Option<Instant>,
361}
362
363#[derive(Debug, Serialize, Deserialize)]
364struct TimerConfig {
365 duration_secs: f64,
366}
367
368impl TimerNode {
369 pub fn from_json(config: Value) -> anyhow::Result<Self> {
371 let cfg: TimerConfig = serde_json::from_value(config)?;
372 Ok(Self {
373 duration: Duration::from_secs_f64(cfg.duration_secs),
374 start_time: None,
375 })
376 }
377}
378
379#[async_trait]
380impl BehaviorNode for TimerNode {
381 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
382 let now = Instant::now();
383
384 if self.start_time.is_none() {
386 self.start_time = Some(now);
387 }
388
389 if now.duration_since(self.start_time.unwrap()) >= self.duration {
391 Ok(NodeStatus::Success)
392 } else {
393 Ok(NodeStatus::Running)
394 }
395 }
396
397 async fn reset(&mut self) -> anyhow::Result<()> {
398 self.start_time = None;
399 Ok(())
400 }
401
402 fn name(&self) -> &str {
403 "TimerNode"
404 }
405}
406
407#[derive(Debug)]
421pub struct IdleNode;
422
423impl IdleNode {
424 pub fn from_json(_config: Value) -> anyhow::Result<Self> {
426 Ok(Self)
427 }
428}
429
430#[async_trait]
431impl BehaviorNode for IdleNode {
432 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
433 Ok(NodeStatus::Success)
434 }
435
436 fn name(&self) -> &str {
437 "IdleNode"
438 }
439}
440
441#[derive(Debug)]
457pub struct PauseNode {
458 duration: Duration,
459 start_time: Option<Instant>,
460}
461
462#[derive(Debug, Serialize, Deserialize)]
463struct PauseConfig {
464 duration_secs: f64,
465}
466
467impl PauseNode {
468 pub fn from_json(config: Value) -> anyhow::Result<Self> {
470 let cfg: PauseConfig = serde_json::from_value(config)?;
471 Ok(Self {
472 duration: Duration::from_secs_f64(cfg.duration_secs),
473 start_time: None,
474 })
475 }
476}
477
478#[async_trait]
479impl BehaviorNode for PauseNode {
480 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
481 let now = Instant::now();
482
483 if self.start_time.is_none() {
485 self.start_time = Some(now);
486 tracing::debug!("PauseNode: starting pause for {:?}", self.duration);
487 }
488
489 if now.duration_since(self.start_time.unwrap()) >= self.duration {
491 tracing::debug!("PauseNode: pause complete");
492 Ok(NodeStatus::Success)
493 } else {
494 Ok(NodeStatus::Running)
495 }
496 }
497
498 async fn reset(&mut self) -> anyhow::Result<()> {
499 self.start_time = None;
500 Ok(())
501 }
502
503 fn name(&self) -> &str {
504 "PauseNode"
505 }
506}
507
508#[derive(Debug)]
524pub struct StopNode {
525 topic: String,
526}
527
528#[derive(Debug, Serialize, Deserialize)]
529struct StopConfig {
530 topic: String,
531}
532
533impl StopNode {
534 pub fn from_json(config: Value) -> anyhow::Result<Self> {
536 let cfg: StopConfig = serde_json::from_value(config)?;
537 Ok(Self { topic: cfg.topic })
538 }
539}
540
541#[async_trait]
542impl BehaviorNode for StopNode {
543 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
544 let twist = Twist {
546 linear: 0.0,
547 angular: 0.0,
548 };
549
550 let topic_static: &'static str = Box::leak(self.topic.clone().into_boxed_str());
552 let topic = Topic::<Twist>::new(topic_static);
553
554 ctx.publish_to(topic, &twist).await?;
555 tracing::debug!("StopNode: published stop command");
556 Ok(NodeStatus::Success)
557 }
558
559 fn name(&self) -> &str {
560 "StopNode"
561 }
562}
563
564#[derive(Debug)]
582pub struct PlanPathNode {
583 #[allow(dead_code)]
584 target_position: Vec<f64>,
585 #[allow(dead_code)]
586 algorithm: String,
587}
588
589#[derive(Debug, Serialize, Deserialize)]
590struct PlanPathConfig {
591 target_position: Vec<f64>,
592 #[serde(default = "default_algorithm")]
593 path_planning_algorithm: String,
594}
595
596fn default_algorithm() -> String {
597 "a_star".to_string()
598}
599
600impl PlanPathNode {
601 pub fn from_json(config: Value) -> anyhow::Result<Self> {
603 let cfg: PlanPathConfig = serde_json::from_value(config)?;
604 Ok(Self {
605 target_position: cfg.target_position,
606 algorithm: cfg.path_planning_algorithm,
607 })
608 }
609}
610
611#[async_trait]
612impl BehaviorNode for PlanPathNode {
613 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
614 tracing::debug!("PlanPathNode: planning path to {:?} (stub)", self.target_position);
617 Ok(NodeStatus::Success)
618 }
619
620 fn name(&self) -> &str {
621 "PlanPathNode"
622 }
623}
624
625#[derive(Debug)]
643pub struct FollowPathNode {
644 #[allow(dead_code)] topic: String,
646 #[allow(dead_code)]
647 max_speed: f64,
648 #[allow(dead_code)]
649 turn_rate: f64,
650}
651
652#[derive(Debug, Serialize, Deserialize)]
653struct FollowPathConfig {
654 #[serde(default = "default_motor_topic")]
655 topic: String,
656 #[serde(default = "default_max_speed")]
657 max_speed: f64,
658 #[serde(default = "default_turn_rate")]
659 turn_rate: f64,
660}
661
662fn default_motor_topic() -> String {
663 "/motor/cmd_vel".to_string()
664}
665
666fn default_max_speed() -> f64 {
667 1.0
668}
669
670fn default_turn_rate() -> f64 {
671 0.5
672}
673
674impl FollowPathNode {
675 pub fn from_json(config: Value) -> anyhow::Result<Self> {
677 let cfg: FollowPathConfig = serde_json::from_value(config)?;
678 Ok(Self {
679 topic: cfg.topic,
680 max_speed: cfg.max_speed,
681 turn_rate: cfg.turn_rate,
682 })
683 }
684}
685
686#[async_trait]
687impl BehaviorNode for FollowPathNode {
688 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
689 tracing::debug!("FollowPathNode: following path (stub)");
692 Ok(NodeStatus::Success)
693 }
694
695 fn name(&self) -> &str {
696 "FollowPathNode"
697 }
698}
699
700#[derive(Debug)]
717pub struct CheckArrivalNode {
718 #[allow(dead_code)]
719 position_tolerance: f64,
720 #[allow(dead_code)]
721 angular_tolerance: f64,
722}
723
724#[derive(Debug, Serialize, Deserialize)]
725struct CheckArrivalConfig {
726 position_tolerance: f64,
727 angular_tolerance: f64,
728}
729
730impl CheckArrivalNode {
731 pub fn from_json(config: Value) -> anyhow::Result<Self> {
733 let cfg: CheckArrivalConfig = serde_json::from_value(config)?;
734 Ok(Self {
735 position_tolerance: cfg.position_tolerance,
736 angular_tolerance: cfg.angular_tolerance,
737 })
738 }
739}
740
741#[async_trait]
742impl BehaviorNode for CheckArrivalNode {
743 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
744 tracing::debug!("CheckArrivalNode: checking arrival (stub)");
747 Ok(NodeStatus::Success)
748 }
749
750 fn name(&self) -> &str {
751 "CheckArrivalNode"
752 }
753}