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(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 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 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 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
193fn 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
268fn 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}