mecha10_cli/services/dev/
mod.rs1use anyhow::{Context, Result};
7use std::collections::HashMap;
8use std::net::TcpListener;
9use std::path::{Path, PathBuf};
10
11use crate::types::SimulationConfig;
12
13#[derive(Debug, Clone)]
15pub struct SimulationPaths {
16 pub model_path: PathBuf,
18 pub environment_path: PathBuf,
20 pub model_config_path: Option<PathBuf>,
22 pub environment_config_path: Option<PathBuf>,
24 pub headless: bool,
26 pub networking: crate::types::simulation::NetworkingConfig,
28}
29
30pub struct DevService {
57 #[allow(dead_code)]
58 redis_url: String,
59}
60
61impl DevService {
62 pub fn new(redis_url: String) -> Self {
68 Self { redis_url }
69 }
70
71 pub async fn load_project_info(
77 ctx: &mut crate::context::CliContext,
78 ) -> Result<(String, String, crate::types::ProjectConfig)> {
79 let project = ctx.project()?;
80 let name = project.name()?.to_string();
81 let version = project.version()?.to_string();
82 let config = ctx.load_project_config().await?;
83 Ok((name, version, config))
84 }
85
86 pub fn resolve_simulation_paths(
100 ctx: &mut crate::context::CliContext,
101 config: &crate::types::ProjectConfig,
102 ) -> Result<Option<SimulationPaths>> {
103 if let Some(sim_config) = &config.simulation {
104 if !sim_config.enabled {
106 return Ok(None);
107 }
108
109 let sim = ctx.simulation();
110
111 if let Ok(runtime_config) = SimulationConfig::load_with_profile_and_scenario(
113 Some(&sim_config.config_profile),
114 sim_config.scenario.as_deref(),
115 ) {
116 let model_path = sim.resolve_model_path(&runtime_config.model)?;
118 let env_path = sim.resolve_environment_path(&runtime_config.environment)?;
119
120 let model_config_path = sim_config.model_config.as_ref().map(|p| {
122 let path = PathBuf::from(p);
123 if path.is_absolute() {
124 path
125 } else {
126 std::env::current_dir().unwrap_or_default().join(path)
127 }
128 });
129 let env_config_path = sim_config.environment_config.as_ref().map(|p| {
130 let path = PathBuf::from(p);
131 if path.is_absolute() {
132 path
133 } else {
134 std::env::current_dir().unwrap_or_default().join(path)
135 }
136 });
137
138 return Ok(Some(SimulationPaths {
139 model_path,
140 environment_path: env_path,
141 model_config_path,
142 environment_config_path: env_config_path,
143 headless: runtime_config.godot.headless,
144 networking: runtime_config.networking.clone(),
145 }));
146 }
147
148 if let Some(model) = &sim_config.model {
150 let model_path = sim.resolve_model_path(model)?;
151 let env_name = sim_config
152 .environment
153 .as_deref()
154 .unwrap_or("@mecha10/simulation-environments/basic_arena");
155 let env_path = sim.resolve_environment_path(env_name)?;
156
157 let model_config_path = sim_config.model_config.as_ref().map(|p| {
159 let path = PathBuf::from(p);
160 if path.is_absolute() {
161 path
162 } else {
163 std::env::current_dir().unwrap_or_default().join(path)
164 }
165 });
166 let env_config_path = sim_config.environment_config.as_ref().map(|p| {
167 let path = PathBuf::from(p);
168 if path.is_absolute() {
169 path
170 } else {
171 std::env::current_dir().unwrap_or_default().join(path)
172 }
173 });
174
175 return Ok(Some(SimulationPaths {
176 model_path,
177 environment_path: env_path,
178 model_config_path,
179 environment_config_path: env_config_path,
180 headless: false, networking: crate::types::simulation::NetworkingConfig::default(),
182 }));
183 }
184
185 Err(anyhow::anyhow!(
187 "Simulation enabled but no valid configuration found.\n\
188 Either:\n\
189 1. Set config_profile in mecha10.json and create configs/{{profile}}/simulation/config.json\n\
190 2. Use legacy format with model/environment fields in mecha10.json"
191 ))
192 } else {
193 Ok(None)
194 }
195 }
196
197 pub fn is_port_available(&self, port: u16) -> Result<bool> {
207 Ok(TcpListener::bind(("127.0.0.1", port)).is_ok())
208 }
209
210 pub fn is_port_in_use(&self, port: u16) -> Result<bool> {
220 Ok(!self.is_port_available(port)?)
221 }
222
223 pub async fn flush_redis<C>(&self, conn: &mut C) -> Result<()>
233 where
234 C: redis::aio::ConnectionLike,
235 {
236 redis::cmd("FLUSHALL")
237 .query_async::<()>(conn)
238 .await
239 .context("Failed to flush Redis")?;
240 Ok(())
241 }
242
243 #[allow(dead_code)]
252 pub fn get_default_env_vars(&self) -> HashMap<String, String> {
253 let mut env = HashMap::new();
254 env.insert("REDIS_URL".to_string(), self.redis_url.clone());
255 env.insert("RUST_LOG".to_string(), "info".to_string());
256 env
257 }
258
259 #[allow(clippy::too_many_arguments)]
272 pub fn build_godot_args(
273 &self,
274 godot_project_path: &Path,
275 model_path: &Path,
276 env_path: &Path,
277 model_config_path: Option<&PathBuf>,
278 env_config_path: Option<&PathBuf>,
279 headless: bool,
280 networking: Option<&crate::types::simulation::NetworkingConfig>,
281 ) -> Vec<String> {
282 eprintln!("[DEBUG] build_godot_args called with:");
283 eprintln!("[DEBUG] model_path: {}", model_path.display());
284 eprintln!("[DEBUG] env_path: {}", env_path.display());
285 eprintln!("[DEBUG] model_config_path: {:?}", model_config_path);
286 eprintln!("[DEBUG] env_config_path: {:?}", env_config_path);
287 eprintln!("[DEBUG] headless: {}", headless);
288
289 let env_name = env_path.file_name().and_then(|n| n.to_str()).unwrap_or("environment");
291
292 let model_name = model_path.file_name().and_then(|n| n.to_str()).unwrap_or("robot");
294
295 let env_scene_path = format!("res://packages/simulation/environments/{}/{}.tscn", env_name, env_name);
298 let model_scene_path = format!("res://packages/simulation/models/{}/robot.tscn", model_name);
299
300 let mut args = vec!["--path".to_string(), godot_project_path.to_string_lossy().to_string()];
303
304 if headless {
306 args.push("--headless".to_string());
307 }
308
309 args.push("--".to_string());
310 args.push(format!("--env-path={}", env_scene_path));
311 args.push(format!("--model-path={}", model_scene_path));
312
313 if let Some(model_config) = model_config_path {
315 eprintln!("[DEBUG] Using provided model_config: {}", model_config.display());
316 args.push(format!("--model-config={}", model_config.display()));
317 } else {
318 eprintln!("[DEBUG] No model_config provided");
319 }
320
321 if let Some(env_config) = env_config_path {
322 eprintln!("[DEBUG] Using provided env_config: {}", env_config.display());
323 args.push(format!("--env-config={}", env_config.display()));
324 } else {
325 eprintln!("[DEBUG] No env_config provided");
326 }
327
328 if let Some(net) = networking {
330 args.push(format!("--protocol-port={}", net.protocol_port));
331 args.push(format!("--protocol-bind={}", net.protocol_bind));
332 args.push(format!("--camera-port={}", net.camera_port));
333 args.push(format!("--camera-bind={}", net.camera_bind));
334 args.push(format!("--signaling-port={}", net.signaling_port));
335 args.push(format!("--signaling-bind={}", net.signaling_bind));
336 }
337
338 args
339 }
340
341 pub fn get_godot_project_path(&self) -> PathBuf {
353 if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
355 return PathBuf::from(framework_path).join("packages/simulation/godot-project");
356 }
357
358 let cli_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
361 let godot_project_path = cli_manifest_dir
362 .parent() .map(|p| p.join("simulation/godot-project"))
364 .unwrap_or_else(|| PathBuf::from("simulation/godot"));
365
366 if godot_project_path.exists() {
367 return godot_project_path;
368 }
369
370 let assets_service = crate::services::SimulationAssetsService::new();
372 if let Some(godot_path) = assets_service.godot_project_path() {
373 return godot_path;
374 }
375
376 PathBuf::from("simulation/godot")
378 }
379}
380
381impl Default for DevService {
382 fn default() -> Self {
383 Self::new("redis://localhost:6379".to_string())
384 }
385}