mecha10_behavior_runtime/actions/navigation.rs
1//! Stub navigation action nodes for path planning, following, and arrival checks.
2
3use crate::{BehaviorNode, Context, NodeStatus};
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8/// Body of the `tick` override for a stub node that has nothing to do yet: log a fixed debug
9/// message and immediately report `Success`. `FollowPathNode`/`CheckArrivalNode` had
10/// byte-for-byte identical `tick` bodies here (only the log message differed) - cargo-dupes
11/// flagged them as an exact-duplicate trait-impl pair. This stays an expression-position macro
12/// (rather than generating the whole `async fn tick`) so `#[async_trait]` still sees a literal
13/// `async fn` signature to rewrite on each impl.
14macro_rules! stub_tick_success {
15 ($msg:literal) => {{
16 tracing::debug!($msg);
17 Ok(NodeStatus::Success)
18 }};
19}
20
21/// PlanPathNode - Plans a path to a target position (STUB)
22///
23/// This is a stub implementation for path planning.
24/// In a full implementation, this would calculate a collision-free path.
25///
26/// # Configuration
27///
28/// ```json
29/// {
30/// "type": "plan_path",
31/// "config": {
32/// "target_position": [5.0, 3.0, 0.0],
33/// "path_planning_algorithm": "a_star",
34/// "max_speed": 1.0
35/// }
36/// }
37/// ```
38#[derive(Debug)]
39pub struct PlanPathNode {
40 #[allow(dead_code)]
41 target_position: Vec<f64>,
42 #[allow(dead_code)]
43 algorithm: String,
44}
45
46#[derive(Debug, Serialize, Deserialize)]
47struct PlanPathConfig {
48 target_position: Vec<f64>,
49 #[serde(default = "default_algorithm")]
50 path_planning_algorithm: String,
51}
52
53fn default_algorithm() -> String {
54 "a_star".to_string()
55}
56
57impl PlanPathNode {
58 // duplicate: false positive — matches `StopNode::from_json`
59 // (packages/behavior-runtime/src/actions/movement.rs) and `CheckArrivalNode::from_json`
60 // below on AST shape only: parse a `serde_json::Value` into a node-specific `*Config`
61 // struct, then map its fields into `Self`. See the note on `StopNode::from_json` for why
62 // that's a coincidental structural match rather than real duplication (each maps a
63 // different field set/count) and isn't unified the way `TimerNode`/`PauseNode` were.
64 /// Create a new PlanPathNode from JSON configuration
65 pub fn from_json(config: Value) -> anyhow::Result<Self> {
66 let cfg: PlanPathConfig = serde_json::from_value(config)?;
67 Ok(Self {
68 target_position: cfg.target_position,
69 algorithm: cfg.path_planning_algorithm,
70 })
71 }
72}
73
74#[async_trait]
75impl BehaviorNode for PlanPathNode {
76 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
77 // TODO: Implement actual path planning
78 tracing::debug!("PlanPathNode: planning path to {:?} (stub)", self.target_position);
79 Ok(NodeStatus::Success)
80 }
81
82 fn name(&self) -> &str {
83 "PlanPathNode"
84 }
85}
86
87/// FollowPathNode - Follows a planned path (STUB)
88///
89/// This is a stub implementation for path following.
90/// In a full implementation, this would execute the planned path.
91///
92/// # Configuration
93///
94/// ```json
95/// {
96/// "type": "follow_path",
97/// "config": {
98/// "max_speed": 1.0,
99/// "turn_rate": 0.5,
100/// "topic": "/motor/cmd_vel"
101/// }
102/// }
103/// ```
104#[derive(Debug)]
105pub struct FollowPathNode {
106 #[allow(dead_code)] // TODO: Use this when implementing path following
107 topic: String,
108 #[allow(dead_code)]
109 max_speed: f64,
110 #[allow(dead_code)]
111 turn_rate: f64,
112}
113
114#[derive(Debug, Serialize, Deserialize)]
115struct FollowPathConfig {
116 #[serde(default = "default_motor_topic")]
117 topic: String,
118 #[serde(default = "default_max_speed")]
119 max_speed: f64,
120 #[serde(default = "default_turn_rate")]
121 turn_rate: f64,
122}
123
124fn default_motor_topic() -> String {
125 "/motor/cmd_vel".to_string()
126}
127
128fn default_max_speed() -> f64 {
129 1.0
130}
131
132fn default_turn_rate() -> f64 {
133 0.5
134}
135
136impl FollowPathNode {
137 /// Create a new FollowPathNode from JSON configuration
138 pub fn from_json(config: Value) -> anyhow::Result<Self> {
139 let cfg: FollowPathConfig = serde_json::from_value(config)?;
140 Ok(Self {
141 topic: cfg.topic,
142 max_speed: cfg.max_speed,
143 turn_rate: cfg.turn_rate,
144 })
145 }
146}
147
148#[async_trait]
149impl BehaviorNode for FollowPathNode {
150 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
151 // TODO: Implement actual path following
152 stub_tick_success!("FollowPathNode: following path (stub)")
153 }
154
155 fn name(&self) -> &str {
156 "FollowPathNode"
157 }
158}
159
160/// CheckArrivalNode - Checks if the robot has arrived at the target (STUB)
161///
162/// This is a stub implementation for arrival checking.
163/// In a full implementation, this would check the robot's position against the target.
164///
165/// # Configuration
166///
167/// ```json
168/// {
169/// "type": "check_arrival",
170/// "config": {
171/// "position_tolerance": 0.1,
172/// "angular_tolerance": 0.05
173/// }
174/// }
175/// ```
176#[derive(Debug)]
177pub struct CheckArrivalNode {
178 #[allow(dead_code)]
179 position_tolerance: f64,
180 #[allow(dead_code)]
181 angular_tolerance: f64,
182}
183
184#[derive(Debug, Serialize, Deserialize)]
185struct CheckArrivalConfig {
186 position_tolerance: f64,
187 angular_tolerance: f64,
188}
189
190impl CheckArrivalNode {
191 // duplicate: false positive — matches `StopNode::from_json`
192 // (packages/behavior-runtime/src/actions/movement.rs) and `PlanPathNode::from_json` above
193 // on AST shape only; see the note on `StopNode::from_json` for why.
194 /// Create a new CheckArrivalNode from JSON configuration
195 pub fn from_json(config: Value) -> anyhow::Result<Self> {
196 let cfg: CheckArrivalConfig = serde_json::from_value(config)?;
197 Ok(Self {
198 position_tolerance: cfg.position_tolerance,
199 angular_tolerance: cfg.angular_tolerance,
200 })
201 }
202}
203
204#[async_trait]
205impl BehaviorNode for CheckArrivalNode {
206 async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
207 // TODO: Implement actual arrival checking
208 stub_tick_success!("CheckArrivalNode: checking arrival (stub)")
209 }
210
211 fn name(&self) -> &str {
212 "CheckArrivalNode"
213 }
214}