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