1mod config;
38mod factory;
39
40pub use config::{ControllerConfig, L298nControllerConfig, L298nMotorPins, MockControllerConfig, MotorConfig};
41pub use factory::{create_motor_controller, BoxedMotorController, DynMotorController};
42
43use mecha10_core::actuator::{MotorStatus, Twist};
44use mecha10_core::prelude::*;
45use mecha10_core::sensor::Odometry;
46use mecha10_core::topics::Topic;
47use std::time::Duration;
48
49pub mod topics {
51 use super::*;
52
53 pub const CMD_VEL: Topic<Twist> = Topic::new("/motor/cmd_vel");
55
56 pub const ODOM: Topic<Odometry> = Topic::new("/odom");
58 pub const MOTOR_STATUS: Topic<MotorStatus> = Topic::new("/motor/status");
59}
60
61pub struct MotorNode {
65 config: MotorConfig,
66 controller: BoxedMotorController,
67 x: f32,
69 y: f32,
70 theta: f32,
71 cmd_linear: f32,
73 cmd_angular: f32,
74 last_cmd_time: u64,
76 timed_out: bool,
77 update_counter: u64,
79}
80
81impl std::fmt::Debug for MotorNode {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("MotorNode")
84 .field("config", &self.config)
85 .field("controller", &self.controller.name())
86 .field("update_counter", &self.update_counter)
87 .finish()
88 }
89}
90
91#[async_trait]
92impl NodeImpl for MotorNode {
93 type Config = MotorConfig;
94
95 async fn init(config: Self::Config) -> Result<Self> {
96 info!("Initializing motor node");
97
98 let controller = factory::create_motor_controller(config.controller.clone())
100 .await
101 .map_err(|e| Mecha10Error::NodeInit(format!("Failed to create controller: {}", e)))?;
102
103 info!("Motor controller created: {}", controller.name());
104 info!(
105 "Odometry rate: {} Hz, Max linear: {} m/s, Max angular: {} rad/s",
106 config.odom_rate_hz, config.max_linear_vel, config.max_angular_vel
107 );
108
109 Ok(Self {
110 config,
111 controller,
112 x: 0.0,
113 y: 0.0,
114 theta: 0.0,
115 cmd_linear: 0.0,
116 cmd_angular: 0.0,
117 last_cmd_time: 0,
118 timed_out: false,
119 update_counter: 0,
120 })
121 }
122
123 async fn run(&mut self, ctx: &Context) -> Result<()> {
124 self.controller
126 .start()
127 .await
128 .map_err(|e| Mecha10Error::NodeInit(format!("Failed to start controller: {}", e)))?;
129
130 let mut cmd_vel_rx = ctx.subscribe(topics::CMD_VEL).await?;
132
133 let odom_interval = Duration::from_secs_f32(1.0 / self.config.odom_rate_hz);
135 let mut odom_timer = tokio::time::interval(odom_interval);
136
137 let status_interval = Duration::from_secs_f32(1.0 / self.config.status_rate_hz);
138 let mut status_timer = tokio::time::interval(status_interval);
139
140 let timeout_interval = Duration::from_millis(100);
142 let mut timeout_timer = tokio::time::interval(timeout_interval);
143
144 info!("Motor node running");
145 info!("Controller: {}", self.controller.name());
146 info!("Odometry rate: {} Hz", self.config.odom_rate_hz);
147 info!("Status rate: {} Hz", self.config.status_rate_hz);
148 if self.config.cmd_timeout_sec > 0.0 {
149 info!("Command timeout: {} sec", self.config.cmd_timeout_sec);
150 } else {
151 info!("Command timeout: disabled");
152 }
153 info!("Listening on: /motor/cmd_vel");
154 info!("Publishing to: /odom, /motor/status");
155
156 loop {
157 tokio::select! {
158 Some(cmd) = cmd_vel_rx.recv() => {
160 self.handle_cmd_vel(cmd).await?;
161 }
162
163 _ = odom_timer.tick() => {
165 self.update_odometry();
166 self.publish_odometry(ctx).await?;
167 self.update_counter += 1;
168
169 if self.update_counter % 1000 == 0 {
170 debug!("Published {} odometry updates", self.update_counter);
171 }
172 }
173
174 _ = status_timer.tick() => {
176 self.publish_status(ctx).await?;
177 }
178
179 _ = timeout_timer.tick() => {
181 self.check_command_timeout().await?;
182 }
183 }
184 }
185 }
186
187 async fn shutdown(&mut self) -> Result<()> {
188 info!("Shutting down motor node");
189
190 if let Err(e) = self.controller.emergency_stop().await {
192 warn!("Emergency stop failed: {}", e);
193 }
194
195 if let Err(e) = self.controller.stop().await {
197 warn!("Controller stop failed: {}", e);
198 }
199
200 info!("Motor node shutdown complete");
201 Ok(())
202 }
203}
204
205impl MotorNode {
206 async fn handle_cmd_vel(&mut self, cmd: Twist) -> Result<()> {
208 self.last_cmd_time = now_micros();
210 self.timed_out = false;
211
212 self.cmd_linear = cmd
214 .linear
215 .clamp(-self.config.max_linear_vel, self.config.max_linear_vel);
216 self.cmd_angular = cmd
217 .angular
218 .clamp(-self.config.max_angular_vel, self.config.max_angular_vel);
219
220 debug!(
221 "Velocity command: linear={:.2} m/s, angular={:.2} rad/s",
222 self.cmd_linear, self.cmd_angular
223 );
224
225 let wheel_base = self.get_wheel_base();
227 let left_vel = self.cmd_linear - (self.cmd_angular * wheel_base / 2.0);
228 let right_vel = self.cmd_linear + (self.cmd_angular * wheel_base / 2.0);
229
230 self.controller
232 .set_velocity(left_vel, right_vel)
233 .await
234 .map_err(|e| Mecha10Error::NodeInit(format!("Failed to set velocity: {}", e)))?;
235
236 Ok(())
237 }
238
239 async fn check_command_timeout(&mut self) -> Result<()> {
241 if self.config.cmd_timeout_sec <= 0.0 {
243 return Ok(());
244 }
245
246 if self.last_cmd_time == 0 {
248 return Ok(());
249 }
250
251 let now = now_micros();
252 let timeout_micros = (self.config.cmd_timeout_sec * 1_000_000.0) as u64;
253 let elapsed = now.saturating_sub(self.last_cmd_time);
254
255 if elapsed > timeout_micros && !self.timed_out {
256 warn!(
258 "Command timeout exceeded ({:.1}s > {:.1}s), stopping motors",
259 elapsed as f32 / 1_000_000.0,
260 self.config.cmd_timeout_sec
261 );
262
263 self.cmd_linear = 0.0;
264 self.cmd_angular = 0.0;
265 self.timed_out = true;
266
267 self.controller
269 .set_velocity(0.0, 0.0)
270 .await
271 .map_err(|e| Mecha10Error::NodeInit(format!("Failed to stop motors: {}", e)))?;
272 }
273
274 Ok(())
275 }
276
277 fn get_wheel_base(&self) -> f32 {
279 match &self.config.controller {
280 ControllerConfig::Mock(cfg) => cfg.wheel_base,
281 ControllerConfig::L298n(cfg) => cfg.wheel_base,
282 }
283 }
284
285 fn get_wheel_radius(&self) -> f32 {
287 match &self.config.controller {
288 ControllerConfig::Mock(cfg) => cfg.wheel_radius,
289 ControllerConfig::L298n(cfg) => cfg.wheel_radius,
290 }
291 }
292
293 fn update_odometry(&mut self) {
295 let dt = 1.0 / self.config.odom_rate_hz;
296
297 let delta_theta = self.cmd_angular * dt;
299 let delta_x = self.cmd_linear * dt * self.theta.cos();
300 let delta_y = self.cmd_linear * dt * self.theta.sin();
301
302 self.x += delta_x;
303 self.y += delta_y;
304 self.theta += delta_theta;
305
306 while self.theta > std::f32::consts::PI {
308 self.theta -= 2.0 * std::f32::consts::PI;
309 }
310 while self.theta < -std::f32::consts::PI {
311 self.theta += 2.0 * std::f32::consts::PI;
312 }
313 }
314
315 async fn publish_odometry(&self, ctx: &Context) -> Result<()> {
317 let odom = Odometry {
318 timestamp: now_micros(),
319 x: self.x,
320 y: self.y,
321 theta: self.theta,
322 linear_vel: self.cmd_linear,
323 angular_vel: self.cmd_angular,
324 };
325
326 ctx.publish_to(topics::ODOM, &odom).await?;
327 Ok(())
328 }
329
330 async fn publish_status(&mut self, ctx: &Context) -> Result<()> {
332 let status = match self.controller.get_status().await {
334 Ok(s) => s,
335 Err(e) => {
336 debug!("Could not get controller status: {}", e);
337 self.calculate_status()
339 }
340 };
341
342 ctx.publish_to(topics::MOTOR_STATUS, &status).await?;
343 Ok(())
344 }
345
346 fn calculate_status(&self) -> MotorStatus {
348 let wheel_base = self.get_wheel_base();
350 let wheel_radius = self.get_wheel_radius();
351 let linear_to_rpm = 60.0 / (2.0 * std::f32::consts::PI * wheel_radius);
352
353 let left_vel = self.cmd_linear - (self.cmd_angular * wheel_base / 2.0);
354 let right_vel = self.cmd_linear + (self.cmd_angular * wheel_base / 2.0);
355
356 MotorStatus {
357 timestamp: now_micros(),
358 left_motor_rpm: left_vel * linear_to_rpm,
359 right_motor_rpm: right_vel * linear_to_rpm,
360 battery_voltage: 0.0, temperature_c: 0.0, }
363 }
364}
365
366pub async fn run() -> Result<()> {
368 let ctx = Context::new("motor").await?;
370
371 let config: MotorConfig = ctx.load_node_config("motor").await?;
373
374 run_node::<MotorNode>(config, ctx, HealthReportingConfig::critical()).await
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_wheel_base_extraction() {
384 let mock_config = MotorConfig {
385 controller: ControllerConfig::Mock(MockControllerConfig {
386 wheel_base: 0.4,
387 ..Default::default()
388 }),
389 ..Default::default()
390 };
391
392 let l298n_config = MotorConfig {
393 controller: ControllerConfig::L298n(L298nControllerConfig {
394 wheel_base: 0.5,
395 ..Default::default()
396 }),
397 ..Default::default()
398 };
399
400 assert!(matches!(mock_config.controller, ControllerConfig::Mock(_)));
403 assert!(matches!(l298n_config.controller, ControllerConfig::L298n(_)));
404 }
405}