mecha10_sim/sim/
profile.rs1use 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#[derive(Debug, Deserialize, Default)]
92#[serde(transparent)]
93struct NodesConfig(Vec<String>);
94
95impl NodesConfig {
96 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 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 fn from_config(config: Mecha10Config) -> Self {
134 let platform = config.robot.platform;
135
136 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 let sensor_configs: Vec<SensorConfig> = config
147 .drivers
148 .iter()
149 .filter(|d| d.driver_type != "motor") .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 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(), tags,
179 }
180 })
181 .collect();
182
183 let capabilities = infer_capabilities(&task_nodes, &sensors);
185
186 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
204fn 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 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 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
281fn infer_capabilities(task_nodes: &[TaskNode], sensors: &[String]) -> Vec<String> {
283 let mut capabilities = HashSet::new();
284
285 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 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}