1use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9#[derive(Debug, Serialize, Deserialize)]
10pub struct ProjectConfig {
11 pub name: String,
12 pub version: String,
13 pub robot: RobotConfig,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub simulation: Option<ProjectSimulationConfig>,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub lifecycle: Option<LifecycleConfig>,
18 #[serde(default)]
19 pub nodes: NodesConfig,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub behaviors: Option<BehaviorsConfig>,
22 #[serde(default)]
23 pub services: ServicesConfig,
24 #[serde(default)]
25 pub docker: DockerServicesConfig,
26 #[serde(default)]
27 pub redis: RedisConfig,
28 #[serde(default)]
29 pub dashboard: DashboardConfig,
30 #[serde(default)]
31 pub networking: NetworkingConfig,
32 #[serde(default)]
33 pub environments: EnvironmentsConfig,
34}
35
36#[derive(Debug, Serialize, Deserialize, Default)]
37pub struct NodesConfig {
38 #[serde(default)]
39 pub drivers: Vec<NodeEntry>,
40 #[serde(default)]
41 pub custom: Vec<NodeEntry>,
42}
43
44#[derive(Debug, Serialize, Deserialize, Clone)]
48pub struct BehaviorsConfig {
49 pub active: String,
51
52 #[serde(default = "default_behaviors_templates_dir")]
54 pub templates_dir: String,
55
56 #[serde(default = "default_behaviors_configs_dir")]
58 pub configs_dir: String,
59
60 #[serde(default)]
62 pub executor: BehaviorExecutorConfig,
63}
64
65#[derive(Debug, Serialize, Deserialize, Clone)]
69pub struct BehaviorExecutorConfig {
70 #[serde(default = "default_tick_rate_hz")]
72 pub tick_rate_hz: f32,
73
74 #[serde(default)]
76 pub max_ticks: Option<usize>,
77
78 #[serde(default = "default_log_stats")]
80 pub log_stats: bool,
81}
82
83impl Default for BehaviorExecutorConfig {
84 fn default() -> Self {
85 Self {
86 tick_rate_hz: default_tick_rate_hz(),
87 max_ticks: None,
88 log_stats: default_log_stats(),
89 }
90 }
91}
92
93fn default_behaviors_templates_dir() -> String {
94 "behaviors".to_string()
95}
96
97fn default_behaviors_configs_dir() -> String {
98 "configs".to_string()
99}
100
101fn default_tick_rate_hz() -> f32 {
102 10.0
103}
104
105fn default_log_stats() -> bool {
106 true
107}
108
109#[derive(Debug, Serialize, Deserialize, Default)]
110pub struct ServicesConfig {
111 #[serde(default)]
112 pub http_api: Option<HttpApiServiceConfig>,
113 #[serde(default)]
114 pub database: Option<DatabaseServiceConfig>,
115 #[serde(default)]
116 pub job_processor: Option<JobProcessorServiceConfig>,
117 #[serde(default)]
118 pub scheduler: Option<SchedulerServiceConfig>,
119}
120
121#[derive(Debug, Serialize, Deserialize, Clone)]
122pub struct HttpApiServiceConfig {
123 pub host: String,
124 pub port: u16,
125 #[serde(default = "default_enable_cors")]
126 pub _enable_cors: bool,
127}
128
129#[derive(Debug, Serialize, Deserialize, Clone)]
130pub struct DatabaseServiceConfig {
131 pub url: String,
132 #[serde(default = "default_max_connections")]
133 pub max_connections: u32,
134 #[serde(default = "default_timeout_seconds")]
135 pub timeout_seconds: u64,
136}
137
138#[derive(Debug, Serialize, Deserialize, Clone)]
139pub struct JobProcessorServiceConfig {
140 #[serde(default = "default_worker_count")]
141 pub worker_count: usize,
142 #[serde(default = "default_max_queue_size")]
143 pub max_queue_size: usize,
144}
145
146#[derive(Debug, Serialize, Deserialize, Clone)]
147pub struct SchedulerServiceConfig {
148 pub tasks: Vec<ScheduledTaskConfig>,
149}
150
151#[derive(Debug, Serialize, Deserialize, Clone)]
152#[allow(dead_code)]
153pub struct ScheduledTaskConfig {
154 pub name: String,
155 pub cron: String,
156 pub topic: String,
157 pub payload: String,
158}
159
160#[derive(Debug, Serialize, Deserialize, Clone)]
161#[allow(dead_code)]
162pub struct NodeEntry {
163 pub name: String,
164 pub path: String,
165 #[serde(default)]
166 pub config: Option<serde_json::Value>,
167 #[serde(default)]
168 pub description: Option<String>,
169 #[serde(default)]
170 pub run_target: Option<String>,
171 #[serde(default = "default_enabled")]
172 pub enabled: bool,
173}
174
175#[derive(Debug, Serialize, Deserialize)]
176pub struct RobotConfig {
177 pub id: String,
178 #[serde(default)]
179 pub platform: Option<String>,
180 #[serde(default)]
181 pub description: Option<String>,
182}
183
184#[derive(Debug, Serialize, Deserialize, Clone)]
189pub struct ProjectSimulationConfig {
190 #[serde(default = "default_simulation_enabled")]
192 pub enabled: bool,
193
194 #[serde(default = "default_config_profile")]
196 pub config_profile: String,
197
198 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub scenario: Option<String>,
202
203 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub model: Option<String>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub model_config: Option<String>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub environment: Option<String>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub environment_config: Option<String>,
213}
214
215fn default_simulation_enabled() -> bool {
216 true
217}
218
219fn default_config_profile() -> String {
220 "dev".to_string()
221}
222
223#[derive(Debug, Serialize, Deserialize, Clone)]
228pub struct LifecycleConfig {
229 pub modes: std::collections::HashMap<String, ModeConfig>,
231
232 #[serde(default = "default_lifecycle_mode")]
234 pub default_mode: String,
235}
236
237#[derive(Debug, Serialize, Deserialize, Clone)]
241pub struct ModeConfig {
242 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub description: Option<String>,
245
246 pub nodes: Vec<String>,
248
249 #[serde(default)]
252 pub stop_nodes: Vec<String>,
253}
254
255fn default_lifecycle_mode() -> String {
256 "startup".to_string()
257}
258
259impl LifecycleConfig {
261 #[allow(dead_code)] pub fn validate(&self, available_nodes: &[String]) -> Result<()> {
274 if !self.modes.contains_key(&self.default_mode) {
276 return Err(anyhow::anyhow!(
277 "Default mode '{}' not found in modes. Available modes: {}",
278 self.default_mode,
279 self.modes.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
280 ));
281 }
282
283 for (mode_name, mode_config) in &self.modes {
285 mode_config.validate(mode_name, available_nodes)?;
286 }
287
288 Ok(())
289 }
290}
291
292impl ModeConfig {
293 #[allow(dead_code)] pub fn validate(&self, mode_name: &str, available_nodes: &[String]) -> Result<()> {
305 for node in &self.nodes {
307 if !available_nodes.contains(node) {
308 return Err(anyhow::anyhow!(
309 "Mode '{}': node '{}' not found in project configuration. Available nodes: {}",
310 mode_name,
311 node,
312 available_nodes.join(", ")
313 ));
314 }
315 }
316
317 for node in &self.stop_nodes {
319 if !available_nodes.contains(node) {
320 return Err(anyhow::anyhow!(
321 "Mode '{}': stop_node '{}' not found in project configuration. Available nodes: {}",
322 mode_name,
323 node,
324 available_nodes.join(", ")
325 ));
326 }
327 }
328
329 Ok(())
330 }
331}
332
333#[derive(Debug, Serialize, Deserialize)]
334pub struct RedisConfig {
335 #[serde(default = "default_redis_url")]
336 pub url: String,
337}
338
339#[derive(Debug, Serialize, Deserialize, Default, Clone)]
341pub struct DockerServicesConfig {
342 #[serde(default)]
344 pub robot: Vec<String>,
345
346 #[serde(default)]
348 pub edge: Vec<String>,
349
350 #[serde(default = "default_compose_file")]
352 pub compose_file: String,
353
354 #[serde(default = "default_auto_start")]
356 pub auto_start: bool,
357}
358
359impl Default for RedisConfig {
360 fn default() -> Self {
361 Self {
362 url: default_redis_url(),
363 }
364 }
365}
366
367fn default_redis_url() -> String {
368 "redis://localhost:6379".to_string()
369}
370
371#[derive(Debug, Serialize, Deserialize, Clone)]
376pub struct DashboardConfig {
377 #[serde(default = "default_dashboard_url")]
381 pub url: String,
382}
383
384impl Default for DashboardConfig {
385 fn default() -> Self {
386 Self {
387 url: default_dashboard_url(),
388 }
389 }
390}
391
392impl DashboardConfig {
393 pub fn effective_url(&self) -> String {
400 std::env::var("MECHA10_DASHBOARD_URL").unwrap_or_else(|_| self.url.clone())
401 }
402}
403
404fn default_dashboard_url() -> String {
405 "https://dashboard.mecha.industries".to_string()
406}
407
408#[derive(Debug, Serialize, Deserialize, Clone, Default)]
412pub struct NetworkingConfig {
413 #[serde(default)]
415 pub relay: RelayConfig,
416}
417
418#[derive(Debug, Serialize, Deserialize, Clone)]
423pub struct RelayConfig {
424 #[serde(default = "default_relay_url")]
428 pub url: String,
429}
430
431impl Default for RelayConfig {
432 fn default() -> Self {
433 Self {
434 url: default_relay_url(),
435 }
436 }
437}
438
439impl RelayConfig {
440 pub fn effective_url(&self) -> String {
447 std::env::var("WEBRTC_RELAY_URL").unwrap_or_else(|_| self.url.clone())
448 }
449}
450
451fn default_relay_url() -> String {
452 "wss://api.mecha.industries/webrtc-relay".to_string()
453}
454
455#[derive(Debug, Serialize, Deserialize, Clone)]
463pub struct EnvironmentsConfig {
464 #[serde(default = "default_dev_environment")]
466 pub dev: EnvironmentConfig,
467
468 #[serde(default, skip_serializing_if = "Option::is_none")]
470 pub staging: Option<EnvironmentConfig>,
471
472 #[serde(default = "default_prod_environment")]
474 pub prod: EnvironmentConfig,
475}
476
477impl Default for EnvironmentsConfig {
478 fn default() -> Self {
479 Self {
480 dev: default_dev_environment(),
481 staging: None,
482 prod: default_prod_environment(),
483 }
484 }
485}
486
487impl EnvironmentsConfig {
488 pub fn get(&self, env: &str) -> Option<&EnvironmentConfig> {
490 match env {
491 "dev" | "development" => Some(&self.dev),
492 "staging" => self.staging.as_ref(),
493 "prod" | "production" => Some(&self.prod),
494 _ => None,
495 }
496 }
497
498 pub fn current(&self) -> &EnvironmentConfig {
500 let env = std::env::var("MECHA10_ENV").unwrap_or_else(|_| "dev".to_string());
501 self.get(&env).unwrap_or(&self.dev)
502 }
503
504 pub fn control_plane_url(&self) -> String {
506 std::env::var("MECHA10_CONTROL_PLANE_URL").unwrap_or_else(|_| self.current().control_plane.clone())
507 }
508}
509
510#[derive(Debug, Serialize, Deserialize, Clone)]
512pub struct EnvironmentConfig {
513 pub control_plane: String,
515}
516
517fn default_dev_environment() -> EnvironmentConfig {
518 EnvironmentConfig {
519 control_plane: "ws://localhost:8000".to_string(),
520 }
521}
522
523fn default_prod_environment() -> EnvironmentConfig {
524 EnvironmentConfig {
525 control_plane: "wss://api.mecha.industries".to_string(),
526 }
527}
528
529fn default_enable_cors() -> bool {
531 true
532}
533
534fn default_max_connections() -> u32 {
535 10
536}
537
538fn default_timeout_seconds() -> u64 {
539 30
540}
541
542fn default_worker_count() -> usize {
543 4
544}
545
546fn default_max_queue_size() -> usize {
547 100
548}
549
550fn default_enabled() -> bool {
551 true
552}
553
554fn default_compose_file() -> String {
555 "docker-compose.yml".to_string()
556}
557
558fn default_auto_start() -> bool {
559 true
560}
561
562#[allow(dead_code)]
564pub async fn load_robot_id(config_path: &PathBuf) -> Result<String> {
565 let content = tokio::fs::read_to_string(config_path)
566 .await
567 .context("Failed to read mecha10.json")?;
568
569 let config: ProjectConfig = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
570
571 Ok(config.robot.id)
572}
573
574pub async fn load_project_config(config_path: &PathBuf) -> Result<ProjectConfig> {
576 let content = tokio::fs::read_to_string(config_path)
577 .await
578 .context("Failed to read mecha10.json")?;
579
580 let config: ProjectConfig = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
581
582 Ok(config)
583}