mecha10_sim/sim/
profile.rs1use 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 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 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 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
185fn 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
260fn 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}