Skip to main content

mecha10_sim/sim/
profile.rs

1//! Robot profile extraction from mecha10.json
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashSet;
6use std::fs;
7use std::path::Path;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RobotProfile {
11    pub platform: String,
12    pub sensors: Vec<String>,
13    pub sensor_configs: Vec<SensorConfig>,
14    pub task_nodes: Vec<TaskNode>,
15    pub capabilities: Vec<String>,
16    pub difficulty_preference: String,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct SensorConfig {
21    pub name: String,
22    pub sensor_type: String,
23    pub mount_point: Option<Vec<f32>>,
24    pub orientation: Option<Vec<f32>>,
25    pub rl_config: Option<RLConfig>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct RLConfig {
30    pub observation: Option<bool>,
31    pub num_rays: Option<i32>,
32    pub ray_length: Option<f32>,
33    pub resolution: Option<Vec<i32>>,
34    pub fov: Option<f32>,
35    pub sensor_type: Option<String>,
36    pub update_rate_hz: Option<f32>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct TaskNode {
41    pub name: String,
42    pub description: String,
43    pub tags: Vec<String>,
44}
45
46#[derive(Debug, Deserialize)]
47struct Mecha10Config {
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(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/// Nodes are now a simple list of identifiers:
88/// - "@mecha10/<name>" - Framework nodes
89/// - "@local/<name>" - Project nodes
90/// - "@<org>/<name>" - Registry nodes (future)
91#[derive(Debug, Deserialize, Default)]
92#[serde(transparent)]
93struct NodesConfig(Vec<String>);
94
95impl NodesConfig {
96    /// Extract project node names (@local/<name> format)
97    fn project_node_names(&self) -> Vec<String> {
98        self.0
99            .iter()
100            .filter_map(|id| {
101                if id.starts_with("@local/") {
102                    id.strip_prefix("@local/").map(|s| s.to_string())
103                } else {
104                    None
105                }
106            })
107            .collect()
108    }
109}
110
111#[derive(Debug, Deserialize)]
112struct SimulationConfig {
113    #[serde(default = "default_difficulty")]
114    difficulty_preference: String,
115}
116
117fn default_difficulty() -> String {
118    "medium".to_string()
119}
120
121impl RobotProfile {
122    /// Extract robot profile from mecha10.json file
123    pub fn from_config_file(path: &Path) -> Result<Self> {
124        let content =
125            fs::read_to_string(path).with_context(|| format!("Failed to read config file: {}", path.display()))?;
126
127        let config: Mecha10Config = serde_json::from_str(&content).with_context(|| "Failed to parse mecha10.json")?;
128
129        Ok(Self::from_config(config))
130    }
131
132    /// Extract robot profile from parsed config
133    fn from_config(config: Mecha10Config) -> Self {
134        let platform = config.robot.platform;
135
136        // Extract sensor types (unique)
137        let sensors: Vec<String> = config
138            .drivers
139            .iter()
140            .map(|d| d.driver_type.clone())
141            .collect::<HashSet<_>>()
142            .into_iter()
143            .collect();
144
145        // Extract detailed sensor configurations (exclude motors - they're actuators, not sensors)
146        let sensor_configs: Vec<SensorConfig> = config
147            .drivers
148            .iter()
149            .filter(|d| d.driver_type != "motor") // Filter out motors
150            .map(|d| {
151                let mount_point = d.physics.as_ref().and_then(|p| p.mount_point.clone());
152                let orientation = d.physics.as_ref().and_then(|p| p.orientation.clone());
153
154                SensorConfig {
155                    name: if d.name.is_empty() {
156                        format!("{}_{}", d.driver_type, "default")
157                    } else {
158                        d.name.clone()
159                    },
160                    sensor_type: d.driver_type.clone(),
161                    mount_point,
162                    orientation,
163                    rl_config: d.rl.clone(),
164                }
165            })
166            .collect();
167
168        // Extract task nodes with tags (from project nodes: nodes/<name>)
169        let task_nodes: Vec<TaskNode> = config
170            .nodes
171            .project_node_names()
172            .into_iter()
173            .map(|name| {
174                let tags = extract_tags_from_node(&name, "");
175                TaskNode {
176                    name,
177                    description: String::new(), // Description no longer in config
178                    tags,
179                }
180            })
181            .collect();
182
183        // Infer capabilities
184        let capabilities = infer_capabilities(&task_nodes, &sensors);
185
186        // Get difficulty preference
187        let difficulty_preference = config
188            .simulation
189            .as_ref()
190            .map(|s| s.difficulty_preference.clone())
191            .unwrap_or_else(|| "medium".to_string());
192
193        Self {
194            platform,
195            sensors,
196            sensor_configs,
197            task_nodes,
198            capabilities,
199            difficulty_preference,
200        }
201    }
202}
203
204/// Extract tags from node name and description
205fn extract_tags_from_node(name: &str, description: &str) -> Vec<String> {
206    let mut tags = HashSet::new();
207
208    let name_lower = name.to_lowercase();
209    let desc_lower = description.to_lowercase();
210
211    // Extract from node name
212    if name_lower.contains("task") || name_lower.contains("planner") {
213        tags.insert("task".to_string());
214        tags.insert("planner".to_string());
215    }
216    if name_lower.contains("delivery") {
217        tags.insert("delivery".to_string());
218        tags.insert("navigation".to_string());
219    }
220    if name_lower.contains("follow") {
221        tags.insert("following".to_string());
222        tags.insert("tracking".to_string());
223        tags.insert("hri".to_string());
224    }
225    if name_lower.contains("object") && name_lower.contains("detector") {
226        tags.insert("object".to_string());
227        tags.insert("detection".to_string());
228        tags.insert("vision".to_string());
229    }
230    if name_lower.contains("patrol") {
231        tags.insert("patrol".to_string());
232        tags.insert("security".to_string());
233        tags.insert("monitoring".to_string());
234    }
235    if name_lower.contains("manipulation") || name_lower.contains("gripper") {
236        tags.insert("manipulation".to_string());
237        tags.insert("gripper".to_string());
238        tags.insert("arm".to_string());
239    }
240    if name_lower.contains("person") || name_lower.contains("human") {
241        tags.insert("hri".to_string());
242        tags.insert("person".to_string());
243    }
244    if name_lower.contains("tray") {
245        tags.insert("delivery".to_string());
246        tags.insert("waiter".to_string());
247    }
248
249    // Extract from description
250    if desc_lower.contains("delivery") || desc_lower.contains("waiter") {
251        tags.insert("delivery".to_string());
252        tags.insert("service".to_string());
253    }
254    if desc_lower.contains("navigation") || desc_lower.contains("navigate") {
255        tags.insert("navigation".to_string());
256    }
257    if desc_lower.contains("avoid") {
258        tags.insert("avoidance".to_string());
259    }
260    if desc_lower.contains("follow") || desc_lower.contains("tracking") {
261        tags.insert("following".to_string());
262        tags.insert("tracking".to_string());
263    }
264    if desc_lower.contains("person") || desc_lower.contains("human") {
265        tags.insert("hri".to_string());
266        tags.insert("person".to_string());
267    }
268    if desc_lower.contains("object") {
269        tags.insert("object".to_string());
270    }
271    if desc_lower.contains("detection") || desc_lower.contains("detect") {
272        tags.insert("detection".to_string());
273    }
274    if desc_lower.contains("workflow") {
275        tags.insert("workflow".to_string());
276    }
277
278    tags.into_iter().collect()
279}
280
281/// Infer robot capabilities from task nodes and sensors
282fn infer_capabilities(task_nodes: &[TaskNode], sensors: &[String]) -> Vec<String> {
283    let mut capabilities = HashSet::new();
284
285    // Infer from sensor combinations
286    let has_camera = sensors.iter().any(|s| s == "camera");
287    let has_lidar = sensors.iter().any(|s| s == "lidar");
288    let has_gripper = sensors.iter().any(|s| s == "gripper");
289    let has_imu = sensors.iter().any(|s| s == "imu");
290
291    if has_camera && has_lidar {
292        capabilities.insert("vision_navigation".to_string());
293    }
294    if has_camera {
295        capabilities.insert("vision".to_string());
296        capabilities.insert("object_detection".to_string());
297    }
298    if has_lidar {
299        capabilities.insert("navigation".to_string());
300        capabilities.insert("obstacle_avoidance".to_string());
301    }
302    if has_gripper {
303        capabilities.insert("manipulation".to_string());
304    }
305    if has_imu {
306        capabilities.insert("stabilization".to_string());
307    }
308
309    // Infer from task nodes
310    for node in task_nodes {
311        if node.name.contains("planner") {
312            capabilities.insert("planning".to_string());
313        }
314        if node.name.contains("controller") {
315            capabilities.insert("control".to_string());
316        }
317        if node.tags.contains(&"delivery".to_string()) {
318            capabilities.insert("delivery".to_string());
319        }
320        if node.tags.contains(&"following".to_string()) {
321            capabilities.insert("following".to_string());
322        }
323    }
324
325    capabilities.into_iter().collect()
326}