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 environments: EnvironmentsConfig,
32}
33
34#[derive(Debug, Serialize, Deserialize, Default)]
35pub struct NodesConfig {
36 #[serde(default)]
37 pub drivers: Vec<NodeEntry>,
38 #[serde(default)]
39 pub custom: Vec<NodeEntry>,
40}
41
42#[derive(Debug, Serialize, Deserialize, Clone)]
46pub struct BehaviorsConfig {
47 pub active: String,
49
50 #[serde(default = "default_behaviors_templates_dir")]
52 pub templates_dir: String,
53
54 #[serde(default = "default_behaviors_configs_dir")]
56 pub configs_dir: String,
57
58 #[serde(default)]
60 pub executor: BehaviorExecutorConfig,
61}
62
63#[derive(Debug, Serialize, Deserialize, Clone)]
67pub struct BehaviorExecutorConfig {
68 #[serde(default = "default_tick_rate_hz")]
70 pub tick_rate_hz: f32,
71
72 #[serde(default)]
74 pub max_ticks: Option<usize>,
75
76 #[serde(default = "default_log_stats")]
78 pub log_stats: bool,
79}
80
81impl Default for BehaviorExecutorConfig {
82 fn default() -> Self {
83 Self {
84 tick_rate_hz: default_tick_rate_hz(),
85 max_ticks: None,
86 log_stats: default_log_stats(),
87 }
88 }
89}
90
91fn default_behaviors_templates_dir() -> String {
92 "behaviors".to_string()
93}
94
95fn default_behaviors_configs_dir() -> String {
96 "configs".to_string()
97}
98
99fn default_tick_rate_hz() -> f32 {
100 10.0
101}
102
103fn default_log_stats() -> bool {
104 true
105}
106
107#[derive(Debug, Serialize, Deserialize, Default)]
108pub struct ServicesConfig {
109 #[serde(default)]
110 pub http_api: Option<HttpApiServiceConfig>,
111 #[serde(default)]
112 pub database: Option<DatabaseServiceConfig>,
113 #[serde(default)]
114 pub job_processor: Option<JobProcessorServiceConfig>,
115 #[serde(default)]
116 pub scheduler: Option<SchedulerServiceConfig>,
117}
118
119#[derive(Debug, Serialize, Deserialize, Clone)]
120pub struct HttpApiServiceConfig {
121 pub host: String,
122 pub port: u16,
123 #[serde(default = "default_enable_cors")]
124 pub _enable_cors: bool,
125}
126
127#[derive(Debug, Serialize, Deserialize, Clone)]
128pub struct DatabaseServiceConfig {
129 pub url: String,
130 #[serde(default = "default_max_connections")]
131 pub max_connections: u32,
132 #[serde(default = "default_timeout_seconds")]
133 pub timeout_seconds: u64,
134}
135
136#[derive(Debug, Serialize, Deserialize, Clone)]
137pub struct JobProcessorServiceConfig {
138 #[serde(default = "default_worker_count")]
139 pub worker_count: usize,
140 #[serde(default = "default_max_queue_size")]
141 pub max_queue_size: usize,
142}
143
144#[derive(Debug, Serialize, Deserialize, Clone)]
145pub struct SchedulerServiceConfig {
146 pub tasks: Vec<ScheduledTaskConfig>,
147}
148
149#[derive(Debug, Serialize, Deserialize, Clone)]
150#[allow(dead_code)]
151pub struct ScheduledTaskConfig {
152 pub name: String,
153 pub cron: String,
154 pub topic: String,
155 pub payload: String,
156}
157
158#[derive(Debug, Serialize, Deserialize, Clone)]
159#[allow(dead_code)]
160pub struct NodeEntry {
161 pub name: String,
162 pub path: String,
163 #[serde(default)]
164 pub config: Option<serde_json::Value>,
165 #[serde(default)]
166 pub description: Option<String>,
167 #[serde(default)]
168 pub run_target: Option<String>,
169 #[serde(default = "default_enabled")]
170 pub enabled: bool,
171}
172
173#[derive(Debug, Serialize, Deserialize)]
174pub struct RobotConfig {
175 pub id: String,
176 #[serde(default)]
177 pub platform: Option<String>,
178 #[serde(default)]
179 pub description: Option<String>,
180}
181
182#[derive(Debug, Serialize, Deserialize, Clone)]
187pub struct ProjectSimulationConfig {
188 #[serde(default = "default_simulation_enabled")]
190 pub enabled: bool,
191
192 #[serde(default = "default_config_profile")]
194 pub config_profile: String,
195
196 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub scenario: Option<String>,
200
201 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub model: Option<String>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub model_config: Option<String>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub environment: Option<String>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub environment_config: Option<String>,
211}
212
213fn default_simulation_enabled() -> bool {
214 true
215}
216
217fn default_config_profile() -> String {
218 "dev".to_string()
219}
220
221#[derive(Debug, Serialize, Deserialize, Clone)]
226pub struct LifecycleConfig {
227 pub modes: std::collections::HashMap<String, ModeConfig>,
229
230 #[serde(default = "default_lifecycle_mode")]
232 pub default_mode: String,
233}
234
235#[derive(Debug, Serialize, Deserialize, Clone)]
239pub struct ModeConfig {
240 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub description: Option<String>,
243
244 pub nodes: Vec<String>,
246
247 #[serde(default)]
250 pub stop_nodes: Vec<String>,
251}
252
253fn default_lifecycle_mode() -> String {
254 "startup".to_string()
255}
256
257impl LifecycleConfig {
259 #[allow(dead_code)] pub fn validate(&self, available_nodes: &[String]) -> Result<()> {
272 if !self.modes.contains_key(&self.default_mode) {
274 return Err(anyhow::anyhow!(
275 "Default mode '{}' not found in modes. Available modes: {}",
276 self.default_mode,
277 self.modes.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
278 ));
279 }
280
281 for (mode_name, mode_config) in &self.modes {
283 mode_config.validate(mode_name, available_nodes)?;
284 }
285
286 Ok(())
287 }
288}
289
290impl ModeConfig {
291 #[allow(dead_code)] pub fn validate(&self, mode_name: &str, available_nodes: &[String]) -> Result<()> {
303 for node in &self.nodes {
305 if !available_nodes.contains(node) {
306 return Err(anyhow::anyhow!(
307 "Mode '{}': node '{}' not found in project configuration. Available nodes: {}",
308 mode_name,
309 node,
310 available_nodes.join(", ")
311 ));
312 }
313 }
314
315 for node in &self.stop_nodes {
317 if !available_nodes.contains(node) {
318 return Err(anyhow::anyhow!(
319 "Mode '{}': stop_node '{}' not found in project configuration. Available nodes: {}",
320 mode_name,
321 node,
322 available_nodes.join(", ")
323 ));
324 }
325 }
326
327 Ok(())
328 }
329}
330
331#[derive(Debug, Serialize, Deserialize)]
332pub struct RedisConfig {
333 #[serde(default = "default_redis_url")]
334 pub url: String,
335}
336
337#[derive(Debug, Serialize, Deserialize, Default, Clone)]
339pub struct DockerServicesConfig {
340 #[serde(default)]
342 pub robot: Vec<String>,
343
344 #[serde(default)]
346 pub edge: Vec<String>,
347
348 #[serde(default = "default_compose_file")]
350 pub compose_file: String,
351
352 #[serde(default = "default_auto_start")]
354 pub auto_start: bool,
355}
356
357impl Default for RedisConfig {
358 fn default() -> Self {
359 Self {
360 url: default_redis_url(),
361 }
362 }
363}
364
365fn default_redis_url() -> String {
366 "redis://localhost:6379".to_string()
367}
368
369#[derive(Debug, Serialize, Deserialize, Clone)]
374pub struct DashboardConfig {
375 #[serde(default = "default_dashboard_url")]
379 pub url: String,
380}
381
382impl Default for DashboardConfig {
383 fn default() -> Self {
384 Self {
385 url: default_dashboard_url(),
386 }
387 }
388}
389
390impl DashboardConfig {
391 pub fn effective_url(&self) -> String {
398 std::env::var("MECHA10_DASHBOARD_URL").unwrap_or_else(|_| self.url.clone())
399 }
400}
401
402fn default_dashboard_url() -> String {
403 "https://dashboard.mecha.industries".to_string()
404}
405
406#[derive(Debug, Serialize, Deserialize, Clone)]
414pub struct EnvironmentsConfig {
415 #[serde(default = "default_dev_environment")]
417 pub dev: EnvironmentConfig,
418
419 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub staging: Option<EnvironmentConfig>,
422
423 #[serde(default = "default_prod_environment")]
425 pub prod: EnvironmentConfig,
426}
427
428impl Default for EnvironmentsConfig {
429 fn default() -> Self {
430 Self {
431 dev: default_dev_environment(),
432 staging: None,
433 prod: default_prod_environment(),
434 }
435 }
436}
437
438impl EnvironmentsConfig {
439 pub fn get(&self, env: &str) -> Option<&EnvironmentConfig> {
441 match env {
442 "dev" | "development" => Some(&self.dev),
443 "staging" => self.staging.as_ref(),
444 "prod" | "production" => Some(&self.prod),
445 _ => None,
446 }
447 }
448
449 pub fn current(&self) -> &EnvironmentConfig {
451 let env = std::env::var("MECHA10_ENV").unwrap_or_else(|_| "dev".to_string());
452 self.get(&env).unwrap_or(&self.dev)
453 }
454
455 pub fn control_plane_url(&self) -> String {
457 std::env::var("MECHA10_CONTROL_PLANE_URL").unwrap_or_else(|_| self.current().control_plane.clone())
458 }
459}
460
461#[derive(Debug, Serialize, Deserialize, Clone)]
463pub struct EnvironmentConfig {
464 pub control_plane: String,
466}
467
468fn default_dev_environment() -> EnvironmentConfig {
469 EnvironmentConfig {
470 control_plane: "ws://localhost:8000".to_string(),
471 }
472}
473
474fn default_prod_environment() -> EnvironmentConfig {
475 EnvironmentConfig {
476 control_plane: "wss://api.mecha.industries".to_string(),
477 }
478}
479
480fn default_enable_cors() -> bool {
482 true
483}
484
485fn default_max_connections() -> u32 {
486 10
487}
488
489fn default_timeout_seconds() -> u64 {
490 30
491}
492
493fn default_worker_count() -> usize {
494 4
495}
496
497fn default_max_queue_size() -> usize {
498 100
499}
500
501fn default_enabled() -> bool {
502 true
503}
504
505fn default_compose_file() -> String {
506 "docker-compose.yml".to_string()
507}
508
509fn default_auto_start() -> bool {
510 true
511}
512
513#[allow(dead_code)]
515pub async fn load_robot_id(config_path: &PathBuf) -> Result<String> {
516 let content = tokio::fs::read_to_string(config_path)
517 .await
518 .context("Failed to read mecha10.json")?;
519
520 let config: ProjectConfig = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
521
522 Ok(config.robot.id)
523}
524
525pub async fn load_project_config(config_path: &PathBuf) -> Result<ProjectConfig> {
527 let content = tokio::fs::read_to_string(config_path)
528 .await
529 .context("Failed to read mecha10.json")?;
530
531 let config: ProjectConfig = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
532
533 Ok(config)
534}