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}
29
30#[derive(Debug, Serialize, Deserialize, Default)]
31pub struct NodesConfig {
32 #[serde(default)]
33 pub drivers: Vec<NodeEntry>,
34 #[serde(default)]
35 pub custom: Vec<NodeEntry>,
36}
37
38#[derive(Debug, Serialize, Deserialize, Clone)]
42pub struct BehaviorsConfig {
43 pub active: String,
45
46 #[serde(default = "default_behaviors_templates_dir")]
48 pub templates_dir: String,
49
50 #[serde(default = "default_behaviors_configs_dir")]
52 pub configs_dir: String,
53
54 #[serde(default)]
56 pub executor: BehaviorExecutorConfig,
57}
58
59#[derive(Debug, Serialize, Deserialize, Clone)]
63pub struct BehaviorExecutorConfig {
64 #[serde(default = "default_tick_rate_hz")]
66 pub tick_rate_hz: f32,
67
68 #[serde(default)]
70 pub max_ticks: Option<usize>,
71
72 #[serde(default = "default_log_stats")]
74 pub log_stats: bool,
75}
76
77impl Default for BehaviorExecutorConfig {
78 fn default() -> Self {
79 Self {
80 tick_rate_hz: default_tick_rate_hz(),
81 max_ticks: None,
82 log_stats: default_log_stats(),
83 }
84 }
85}
86
87fn default_behaviors_templates_dir() -> String {
88 "behaviors".to_string()
89}
90
91fn default_behaviors_configs_dir() -> String {
92 "configs".to_string()
93}
94
95fn default_tick_rate_hz() -> f32 {
96 10.0
97}
98
99fn default_log_stats() -> bool {
100 true
101}
102
103#[derive(Debug, Serialize, Deserialize, Default)]
104pub struct ServicesConfig {
105 #[serde(default)]
106 pub http_api: Option<HttpApiServiceConfig>,
107 #[serde(default)]
108 pub database: Option<DatabaseServiceConfig>,
109 #[serde(default)]
110 pub job_processor: Option<JobProcessorServiceConfig>,
111 #[serde(default)]
112 pub scheduler: Option<SchedulerServiceConfig>,
113}
114
115#[derive(Debug, Serialize, Deserialize, Clone)]
116pub struct HttpApiServiceConfig {
117 pub host: String,
118 pub port: u16,
119 #[serde(default = "default_enable_cors")]
120 pub _enable_cors: bool,
121}
122
123#[derive(Debug, Serialize, Deserialize, Clone)]
124pub struct DatabaseServiceConfig {
125 pub url: String,
126 #[serde(default = "default_max_connections")]
127 pub max_connections: u32,
128 #[serde(default = "default_timeout_seconds")]
129 pub timeout_seconds: u64,
130}
131
132#[derive(Debug, Serialize, Deserialize, Clone)]
133pub struct JobProcessorServiceConfig {
134 #[serde(default = "default_worker_count")]
135 pub worker_count: usize,
136 #[serde(default = "default_max_queue_size")]
137 pub max_queue_size: usize,
138}
139
140#[derive(Debug, Serialize, Deserialize, Clone)]
141pub struct SchedulerServiceConfig {
142 pub tasks: Vec<ScheduledTaskConfig>,
143}
144
145#[derive(Debug, Serialize, Deserialize, Clone)]
146#[allow(dead_code)]
147pub struct ScheduledTaskConfig {
148 pub name: String,
149 pub cron: String,
150 pub topic: String,
151 pub payload: String,
152}
153
154#[derive(Debug, Serialize, Deserialize, Clone)]
155#[allow(dead_code)]
156pub struct NodeEntry {
157 pub name: String,
158 pub path: String,
159 #[serde(default)]
160 pub config: Option<serde_json::Value>,
161 #[serde(default)]
162 pub description: Option<String>,
163 #[serde(default)]
164 pub run_target: Option<String>,
165 #[serde(default = "default_enabled")]
166 pub enabled: bool,
167}
168
169#[derive(Debug, Serialize, Deserialize)]
170pub struct RobotConfig {
171 pub id: String,
172 #[serde(default)]
173 pub platform: Option<String>,
174 #[serde(default)]
175 pub description: Option<String>,
176}
177
178#[derive(Debug, Serialize, Deserialize, Clone)]
183pub struct ProjectSimulationConfig {
184 #[serde(default = "default_simulation_enabled")]
186 pub enabled: bool,
187
188 #[serde(default = "default_config_profile")]
190 pub config_profile: String,
191
192 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub scenario: Option<String>,
196
197 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub model: Option<String>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub model_config: Option<String>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub environment: Option<String>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub environment_config: Option<String>,
207}
208
209fn default_simulation_enabled() -> bool {
210 true
211}
212
213fn default_config_profile() -> String {
214 "dev".to_string()
215}
216
217#[derive(Debug, Serialize, Deserialize, Clone)]
222pub struct LifecycleConfig {
223 pub modes: std::collections::HashMap<String, ModeConfig>,
225
226 #[serde(default = "default_lifecycle_mode")]
228 pub default_mode: String,
229}
230
231#[derive(Debug, Serialize, Deserialize, Clone)]
235pub struct ModeConfig {
236 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub description: Option<String>,
239
240 pub nodes: Vec<String>,
242
243 #[serde(default)]
246 pub stop_nodes: Vec<String>,
247}
248
249fn default_lifecycle_mode() -> String {
250 "startup".to_string()
251}
252
253impl LifecycleConfig {
255 #[allow(dead_code)] pub fn validate(&self, available_nodes: &[String]) -> Result<()> {
268 if !self.modes.contains_key(&self.default_mode) {
270 return Err(anyhow::anyhow!(
271 "Default mode '{}' not found in modes. Available modes: {}",
272 self.default_mode,
273 self.modes.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
274 ));
275 }
276
277 for (mode_name, mode_config) in &self.modes {
279 mode_config.validate(mode_name, available_nodes)?;
280 }
281
282 Ok(())
283 }
284}
285
286impl ModeConfig {
287 #[allow(dead_code)] pub fn validate(&self, mode_name: &str, available_nodes: &[String]) -> Result<()> {
299 for node in &self.nodes {
301 if !available_nodes.contains(node) {
302 return Err(anyhow::anyhow!(
303 "Mode '{}': node '{}' not found in project configuration. Available nodes: {}",
304 mode_name,
305 node,
306 available_nodes.join(", ")
307 ));
308 }
309 }
310
311 for node in &self.stop_nodes {
313 if !available_nodes.contains(node) {
314 return Err(anyhow::anyhow!(
315 "Mode '{}': stop_node '{}' not found in project configuration. Available nodes: {}",
316 mode_name,
317 node,
318 available_nodes.join(", ")
319 ));
320 }
321 }
322
323 Ok(())
324 }
325}
326
327#[derive(Debug, Serialize, Deserialize)]
328pub struct RedisConfig {
329 #[serde(default = "default_redis_url")]
330 pub url: String,
331}
332
333#[derive(Debug, Serialize, Deserialize, Default, Clone)]
335pub struct DockerServicesConfig {
336 #[serde(default)]
338 pub robot: Vec<String>,
339
340 #[serde(default)]
342 pub edge: Vec<String>,
343
344 #[serde(default = "default_compose_file")]
346 pub compose_file: String,
347
348 #[serde(default = "default_auto_start")]
350 pub auto_start: bool,
351}
352
353impl Default for RedisConfig {
354 fn default() -> Self {
355 Self {
356 url: default_redis_url(),
357 }
358 }
359}
360
361fn default_redis_url() -> String {
362 "redis://localhost:6379".to_string()
363}
364
365fn default_enable_cors() -> bool {
367 true
368}
369
370fn default_max_connections() -> u32 {
371 10
372}
373
374fn default_timeout_seconds() -> u64 {
375 30
376}
377
378fn default_worker_count() -> usize {
379 4
380}
381
382fn default_max_queue_size() -> usize {
383 100
384}
385
386fn default_enabled() -> bool {
387 true
388}
389
390fn default_compose_file() -> String {
391 "docker-compose.yml".to_string()
392}
393
394fn default_auto_start() -> bool {
395 true
396}
397
398#[allow(dead_code)]
400pub async fn load_robot_id(config_path: &PathBuf) -> Result<String> {
401 let content = tokio::fs::read_to_string(config_path)
402 .await
403 .context("Failed to read mecha10.json")?;
404
405 let config: ProjectConfig = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
406
407 Ok(config.robot.id)
408}
409
410pub async fn load_project_config(config_path: &PathBuf) -> Result<ProjectConfig> {
412 let content = tokio::fs::read_to_string(config_path)
413 .await
414 .context("Failed to read mecha10.json")?;
415
416 let config: ProjectConfig = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
417
418 Ok(config)
419}