Skip to main content

mecha10_behavior_runtime/actions/
movement.rs

1//! Action nodes that publish velocity commands to move the robot.
2
3use crate::{BehaviorNode, Context, NodeStatus};
4use async_trait::async_trait;
5use mecha10_core::actuator::Twist;
6use mecha10_core::topics::Topic;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::time::{Duration, Instant};
10
11/// WanderNode - Generates random movement commands
12///
13/// This node publishes random velocity commands to make a robot wander around.
14/// It periodically changes direction to create natural wandering behavior.
15///
16/// # Configuration
17///
18/// ```json
19/// {
20///   "type": "wander",
21///   "config": {
22///     "topic": "/motor/cmd_vel",
23///     "linear_speed": 0.3,
24///     "angular_speed": 0.5,
25///     "change_interval_secs": 3.0
26///   }
27/// }
28/// ```
29#[derive(Debug)]
30pub struct WanderNode {
31    topic: String,
32    linear_speed: f64,
33    angular_speed: f64,
34    change_interval: Duration,
35    last_change: Option<Instant>,
36    current_angular: f64,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40struct WanderConfig {
41    topic: String,
42    #[serde(default = "default_linear_speed")]
43    linear_speed: f64,
44    #[serde(default = "default_angular_speed")]
45    angular_speed: f64,
46    #[serde(default = "default_change_interval")]
47    change_interval_secs: f64,
48}
49
50fn default_linear_speed() -> f64 {
51    0.3
52}
53
54fn default_angular_speed() -> f64 {
55    0.5
56}
57
58fn default_change_interval() -> f64 {
59    3.0
60}
61
62impl WanderNode {
63    /// Create a new WanderNode from JSON configuration
64    pub fn from_json(config: Value) -> anyhow::Result<Self> {
65        let cfg: WanderConfig = serde_json::from_value(config)?;
66        Ok(Self {
67            topic: cfg.topic,
68            linear_speed: cfg.linear_speed,
69            angular_speed: cfg.angular_speed,
70            change_interval: Duration::from_secs_f64(cfg.change_interval_secs),
71            last_change: None,
72            current_angular: 0.0,
73        })
74    }
75}
76
77#[async_trait]
78impl BehaviorNode for WanderNode {
79    async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
80        let now = Instant::now();
81
82        // Change direction periodically
83        if self.last_change.is_none() || now.duration_since(self.last_change.unwrap()) >= self.change_interval {
84            // Generate random angular velocity between -angular_speed and +angular_speed
85            self.current_angular = (rand::random::<f64>() * 2.0 - 1.0) * self.angular_speed;
86            self.last_change = Some(now);
87        }
88
89        // Publish velocity command
90        let twist = Twist {
91            linear: self.linear_speed as f32,
92            angular: self.current_angular as f32,
93        };
94
95        // Leak topic path for 'static lifetime
96        let topic_static: &'static str = Box::leak(self.topic.clone().into_boxed_str());
97        let topic = Topic::<Twist>::new(topic_static);
98
99        ctx.publish_to(topic, &twist).await?;
100        Ok(NodeStatus::Running)
101    }
102
103    fn name(&self) -> &str {
104        "WanderNode"
105    }
106}
107
108/// MoveNode - Executes a velocity command
109///
110/// Publishes a fixed velocity command to control robot movement.
111///
112/// # Configuration
113///
114/// ```json
115/// {
116///   "type": "move",
117///   "config": {
118///     "topic": "/motor/cmd_vel",
119///     "linear": 0.5,
120///     "angular": 0.0,
121///     "duration_secs": 2.0
122///   }
123/// }
124/// ```
125#[derive(Debug)]
126pub struct MoveNode {
127    topic: String,
128    linear: f64,
129    angular: f64,
130    duration: Option<Duration>,
131    start_time: Option<Instant>,
132}
133
134#[derive(Debug, Serialize, Deserialize)]
135struct MoveConfig {
136    topic: String,
137    linear: f64,
138    angular: f64,
139    duration_secs: Option<f64>,
140}
141
142impl MoveNode {
143    /// Create a new MoveNode from JSON configuration
144    pub fn from_json(config: Value) -> anyhow::Result<Self> {
145        let cfg: MoveConfig = serde_json::from_value(config)?;
146        Ok(Self {
147            topic: cfg.topic,
148            linear: cfg.linear,
149            angular: cfg.angular,
150            duration: cfg.duration_secs.map(Duration::from_secs_f64),
151            start_time: None,
152        })
153    }
154}
155
156#[async_trait]
157impl BehaviorNode for MoveNode {
158    async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
159        let now = Instant::now();
160
161        // Initialize start time on first tick
162        if self.start_time.is_none() {
163            self.start_time = Some(now);
164        }
165
166        // Check if duration has elapsed
167        if let Some(duration) = self.duration {
168            if now.duration_since(self.start_time.unwrap()) >= duration {
169                return Ok(NodeStatus::Success);
170            }
171        }
172
173        // Publish velocity command
174        let twist = Twist {
175            linear: self.linear as f32,
176            angular: self.angular as f32,
177        };
178
179        // Leak topic path for 'static lifetime
180        let topic_static: &'static str = Box::leak(self.topic.clone().into_boxed_str());
181        let topic = Topic::<Twist>::new(topic_static);
182
183        ctx.publish_to(topic, &twist).await?;
184
185        Ok(NodeStatus::Running)
186    }
187
188    async fn reset(&mut self) -> anyhow::Result<()> {
189        self.start_time = None;
190        Ok(())
191    }
192
193    fn name(&self) -> &str {
194        "MoveNode"
195    }
196}
197
198/// StopNode - Publishes zero velocity to stop the robot
199///
200/// This node publishes a zero velocity command to stop the robot immediately.
201/// Returns Success after publishing the stop command.
202///
203/// # Configuration
204///
205/// ```json
206/// {
207///   "type": "stop",
208///   "config": {
209///     "topic": "/motor/cmd_vel"
210///   }
211/// }
212/// ```
213#[derive(Debug)]
214pub struct StopNode {
215    topic: String,
216}
217
218#[derive(Debug, Serialize, Deserialize)]
219struct StopConfig {
220    topic: String,
221}
222
223impl StopNode {
224    /// Create a new StopNode from JSON configuration
225    pub fn from_json(config: Value) -> anyhow::Result<Self> {
226        let cfg: StopConfig = serde_json::from_value(config)?;
227        Ok(Self { topic: cfg.topic })
228    }
229}
230
231#[async_trait]
232impl BehaviorNode for StopNode {
233    async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
234        // Publish zero velocity
235        let twist = Twist {
236            linear: 0.0,
237            angular: 0.0,
238        };
239
240        // Leak topic path for 'static lifetime
241        let topic_static: &'static str = Box::leak(self.topic.clone().into_boxed_str());
242        let topic = Topic::<Twist>::new(topic_static);
243
244        ctx.publish_to(topic, &twist).await?;
245        tracing::debug!("StopNode: published stop command");
246        Ok(NodeStatus::Success)
247    }
248
249    fn name(&self) -> &str {
250        "StopNode"
251    }
252}