Skip to main content

mecha10_sim/sim/
profile.rs

1//! Robot profile extraction from mecha10.json
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::HashSet;
6use std::path::Path;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct RobotProfile {
10    pub platform: String,
11    pub sensors: Vec<String>,
12    pub sensor_configs: Vec<SensorConfig>,
13    pub task_nodes: Vec<TaskNode>,
14    pub capabilities: Vec<String>,
15    pub difficulty_preference: String,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct SensorConfig {
20    pub name: String,
21    pub sensor_type: String,
22    pub mount_point: Option<Vec<f32>>,
23    pub orientation: Option<Vec<f32>>,
24    pub rl_config: Option<RLConfig>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct RLConfig {
29    pub observation: Option<bool>,
30    pub num_rays: Option<i32>,
31    pub ray_length: Option<f32>,
32    pub resolution: Option<Vec<i32>>,
33    pub fov: Option<f32>,
34    pub sensor_type: Option<String>,
35    pub update_rate_hz: Option<f32>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct TaskNode {
40    pub name: String,
41    pub description: String,
42    pub tags: Vec<String>,
43}
44
45#[derive(Debug, Deserialize)]
46struct Mecha10Config {
47    #[serde(flatten)]
48    robot: RobotConfig,
49    #[serde(default)]
50    drivers: Vec<DriverConfig>,
51    #[serde(default)]
52    nodes: NodesConfig,
53    #[serde(default)]
54    simulation: Option<SimulationConfig>,
55}
56
57#[derive(Debug, Deserialize)]
58struct RobotConfig {
59    #[serde(rename = "robot_platform", default = "default_platform")]
60    platform: String,
61}
62
63fn default_platform() -> String {
64    "rover".to_string()
65}
66
67#[derive(Debug, Deserialize)]
68struct DriverConfig {
69    #[serde(rename = "type")]
70    driver_type: String,
71    #[serde(default)]
72    name: String,
73    #[serde(default)]
74    physics: Option<PhysicsConfig>,
75    #[serde(default)]
76    rl: Option<RLConfig>,
77}
78
79#[derive(Debug, Deserialize)]
80struct PhysicsConfig {
81    #[serde(default)]
82    mount_point: Option<Vec<f32>>,
83    #[serde(default)]
84    orientation: Option<Vec<f32>>,
85}
86
87#[derive(Debug, Deserialize, Default)]
88#[serde(transparent)]
89struct NodesConfig(Vec<String>);
90
91impl NodesConfig {
92    /// Extract project node names (plain `<name>` identifiers)
93    fn project_node_names(&self) -> Vec<String> {
94        self.0
95            .iter()
96            .filter_map(|id| {
97                let spec = mecha10_cli_core::types::NodeSpec::parse(id).ok()?;
98                if spec.source == mecha10_cli_core::types::NodeSource::Project {
99                    Some(spec.name)
100                } else {
101                    None
102                }
103            })
104            .collect()
105    }
106}
107
108#[derive(Debug, Deserialize)]
109struct SimulationConfig {
110    #[serde(default = "default_difficulty")]
111    difficulty_preference: String,
112}
113
114fn default_difficulty() -> String {
115    "medium".to_string()
116}
117
118impl RobotProfile {
119    /// Extract robot profile from mecha10.json file
120    pub fn from_config_file(path: &Path) -> Result<Self> {
121        let config: Mecha10Config = super::load_json(path, "config file")?;
122
123        Ok(Self::from_config(config))
124    }
125
126    /// Extract robot profile from parsed config
127    fn from_config(config: Mecha10Config) -> Self {
128        let platform = config.robot.platform;
129
130        let sensors: Vec<String> = config
131            .drivers
132            .iter()
133            .map(|d| d.driver_type.clone())
134            .collect::<HashSet<_>>()
135            .into_iter()
136            .collect();
137
138        let sensor_configs: Vec<SensorConfig> = config
139            .drivers
140            .iter()
141            .filter(|d| d.driver_type != "motor")
142            .map(|d| {
143                let mount_point = d.physics.as_ref().and_then(|p| p.mount_point.clone());
144                let orientation = d.physics.as_ref().and_then(|p| p.orientation.clone());
145
146                SensorConfig {
147                    name: if d.name.is_empty() {
148                        format!("{}_{}", d.driver_type, "default")
149                    } else {
150                        d.name.clone()
151                    },
152                    sensor_type: d.driver_type.clone(),
153                    mount_point,
154                    orientation,
155                    rl_config: d.rl.clone(),
156                }
157            })
158            .collect();
159
160        let task_nodes: Vec<TaskNode> = config
161            .nodes
162            .project_node_names()
163            .into_iter()
164            .map(|name| {
165                let tags = extract_tags_from_node(&name, "");
166                TaskNode {
167                    name,
168                    description: String::new(),
169                    tags,
170                }
171            })
172            .collect();
173
174        let capabilities = infer_capabilities(&task_nodes, &sensors);
175
176        let difficulty_preference = config
177            .simulation
178            .as_ref()
179            .map(|s| s.difficulty_preference.clone())
180            .unwrap_or_else(|| "medium".to_string());
181
182        Self {
183            platform,
184            sensors,
185            sensor_configs,
186            task_nodes,
187            capabilities,
188            difficulty_preference,
189        }
190    }
191}
192
193/// Extract tags from node name and description
194fn extract_tags_from_node(name: &str, description: &str) -> Vec<String> {
195    let mut tags = HashSet::new();
196
197    let name_lower = name.to_lowercase();
198    let desc_lower = description.to_lowercase();
199
200    if name_lower.contains("task") || name_lower.contains("planner") {
201        tags.insert("task".to_string());
202        tags.insert("planner".to_string());
203    }
204    if name_lower.contains("delivery") {
205        tags.insert("delivery".to_string());
206        tags.insert("navigation".to_string());
207    }
208    if name_lower.contains("follow") {
209        tags.insert("following".to_string());
210        tags.insert("tracking".to_string());
211        tags.insert("hri".to_string());
212    }
213    if name_lower.contains("object") && name_lower.contains("detector") {
214        tags.insert("object".to_string());
215        tags.insert("detection".to_string());
216        tags.insert("vision".to_string());
217    }
218    if name_lower.contains("patrol") {
219        tags.insert("patrol".to_string());
220        tags.insert("security".to_string());
221        tags.insert("monitoring".to_string());
222    }
223    if name_lower.contains("manipulation") || name_lower.contains("gripper") {
224        tags.insert("manipulation".to_string());
225        tags.insert("gripper".to_string());
226        tags.insert("arm".to_string());
227    }
228    if name_lower.contains("person") || name_lower.contains("human") {
229        tags.insert("hri".to_string());
230        tags.insert("person".to_string());
231    }
232    if name_lower.contains("tray") {
233        tags.insert("delivery".to_string());
234        tags.insert("waiter".to_string());
235    }
236
237    if desc_lower.contains("delivery") || desc_lower.contains("waiter") {
238        tags.insert("delivery".to_string());
239        tags.insert("service".to_string());
240    }
241    if desc_lower.contains("navigation") || desc_lower.contains("navigate") {
242        tags.insert("navigation".to_string());
243    }
244    if desc_lower.contains("avoid") {
245        tags.insert("avoidance".to_string());
246    }
247    if desc_lower.contains("follow") || desc_lower.contains("tracking") {
248        tags.insert("following".to_string());
249        tags.insert("tracking".to_string());
250    }
251    if desc_lower.contains("person") || desc_lower.contains("human") {
252        tags.insert("hri".to_string());
253        tags.insert("person".to_string());
254    }
255    if desc_lower.contains("object") {
256        tags.insert("object".to_string());
257    }
258    if desc_lower.contains("detection") || desc_lower.contains("detect") {
259        tags.insert("detection".to_string());
260    }
261    if desc_lower.contains("workflow") {
262        tags.insert("workflow".to_string());
263    }
264
265    tags.into_iter().collect()
266}
267
268/// Infer robot capabilities from task nodes and sensors
269fn infer_capabilities(task_nodes: &[TaskNode], sensors: &[String]) -> Vec<String> {
270    let mut capabilities = HashSet::new();
271
272    let has_camera = sensors.iter().any(|s| s == "camera");
273    let has_lidar = sensors.iter().any(|s| s == "lidar");
274    let has_gripper = sensors.iter().any(|s| s == "gripper");
275    let has_imu = sensors.iter().any(|s| s == "imu");
276
277    if has_camera && has_lidar {
278        capabilities.insert("vision_navigation".to_string());
279    }
280    if has_camera {
281        capabilities.insert("vision".to_string());
282        capabilities.insert("object_detection".to_string());
283    }
284    if has_lidar {
285        capabilities.insert("navigation".to_string());
286        capabilities.insert("obstacle_avoidance".to_string());
287    }
288    if has_gripper {
289        capabilities.insert("manipulation".to_string());
290    }
291    if has_imu {
292        capabilities.insert("stabilization".to_string());
293    }
294
295    for node in task_nodes {
296        if node.name.contains("planner") {
297            capabilities.insert("planning".to_string());
298        }
299        if node.name.contains("controller") {
300            capabilities.insert("control".to_string());
301        }
302        if node.tags.contains(&"delivery".to_string()) {
303            capabilities.insert("delivery".to_string());
304        }
305        if node.tags.contains(&"following".to_string()) {
306            capabilities.insert("following".to_string());
307        }
308    }
309
310    capabilities.into_iter().collect()
311}