1use crate::context::CliContext;
6use crate::error::{CliError, CliResult};
7use crate::output::{StatusType, TableBuilder};
8use crate::runner::{Command, CommandOutput};
9use async_trait::async_trait;
10use mabi_core::prelude::*;
11use std::path::PathBuf;
12use std::time::Duration;
13
14pub struct RunCommand {
16 scenario_path: PathBuf,
18 time_scale: f64,
20 duration: Option<Duration>,
22 dry_run: bool,
24}
25
26impl RunCommand {
27 pub fn new(scenario_path: PathBuf) -> Self {
29 Self {
30 scenario_path,
31 time_scale: 1.0,
32 duration: None,
33 dry_run: false,
34 }
35 }
36
37 pub fn with_time_scale(mut self, scale: f64) -> Self {
39 self.time_scale = scale;
40 self
41 }
42
43 pub fn with_duration(mut self, duration: Duration) -> Self {
45 self.duration = Some(duration);
46 self
47 }
48
49 pub fn with_dry_run(mut self, dry_run: bool) -> Self {
51 self.dry_run = dry_run;
52 self
53 }
54
55 async fn load_scenario(&self, ctx: &CliContext) -> CliResult<ScenarioConfig> {
57 let path = ctx.resolve_path(&self.scenario_path);
58
59 if !path.exists() {
60 return Err(CliError::ScenarioNotFound { path });
61 }
62
63 ctx.vprintln(format!("Loading scenario from: {}", path.display()));
64
65 let content = tokio::fs::read_to_string(&path).await?;
66 let config: ScenarioConfig = serde_yaml::from_str(&content)?;
67
68 self.validate_scenario(&config)?;
70
71 Ok(config)
72 }
73
74 fn validate_scenario(&self, config: &ScenarioConfig) -> CliResult<()> {
76 let mut errors: Vec<String> = Vec::new();
77
78 if config.name.is_empty() {
79 errors.push("Scenario name is required".to_string());
80 }
81
82 if config.devices.is_empty() {
83 errors.push("At least one device is required".to_string());
84 }
85
86 for (idx, device) in config.devices.iter().enumerate() {
87 if device.id.is_empty() {
88 errors.push(format!("Device {} has empty ID", idx));
89 }
90 }
91
92 if errors.is_empty() {
93 Ok(())
94 } else {
95 Err(CliError::validation_failed(errors))
96 }
97 }
98
99 async fn run_scenario(
101 &self,
102 ctx: &mut CliContext,
103 config: ScenarioConfig,
104 ) -> CliResult<CommandOutput> {
105 let output = ctx.output();
106
107 output.header("Scenario Configuration");
109 output.kv("Name", &config.name);
110 output.kv("Devices", config.devices.len());
111 output.kv("Time Scale", format!("{}x", self.time_scale));
112 if let Some(duration) = self.duration {
113 output.kv("Duration", format!("{:?}", duration));
114 }
115
116 if self.dry_run {
117 output.success("Dry run completed - scenario is valid");
118 return Ok(CommandOutput::quiet_success());
119 }
120
121 let engine_guard = ctx.engine().await?;
123 let mut engine_lock = engine_guard.write().await;
124 let engine = engine_lock.as_mut().ok_or_else(|| CliError::ExecutionFailed {
125 message: "Engine not initialized".into(),
126 })?;
127
128 let spinner = output.spinner("Creating devices...");
130 for device_config in &config.devices {
131 ctx.dprintln(format!("Creating device: {}", device_config.id));
133 }
134 spinner.finish_with_message("Devices created");
135
136 let spinner = output.spinner("Starting simulator...");
138 engine.start().await?;
139 spinner.finish_with_message("Simulator started");
140
141 output.header("Simulator Status");
143
144 let table = TableBuilder::new(output.colors_enabled())
145 .header(["Protocol", "Devices", "Points", "Status"])
146 .status_row(["Modbus TCP", "0", "0", "Ready"], StatusType::Success)
147 .status_row(["OPC UA", "0", "0", "Ready"], StatusType::Success)
148 .status_row(["BACnet/IP", "0", "0", "Ready"], StatusType::Success)
149 .status_row(["KNXnet/IP", "0", "0", "Ready"], StatusType::Success);
150 table.print();
151
152 output.info("Press Ctrl+C to stop");
153
154 let shutdown = ctx.shutdown_signal();
156 if let Some(duration) = self.duration {
157 tokio::select! {
158 _ = tokio::time::sleep(duration) => {
159 output.info("Duration reached, shutting down...");
160 }
161 _ = shutdown.notified() => {
162 output.info("Shutdown signal received");
163 }
164 }
165 } else {
166 shutdown.notified().await;
167 }
168
169 engine.stop().await?;
171 output.success("Simulator stopped");
172
173 Ok(CommandOutput::quiet_success())
174 }
175}
176
177#[async_trait]
178impl Command for RunCommand {
179 fn name(&self) -> &str {
180 "run"
181 }
182
183 fn description(&self) -> &str {
184 "Run a simulation scenario"
185 }
186
187 fn requires_engine(&self) -> bool {
188 true
189 }
190
191 fn supports_shutdown(&self) -> bool {
192 true
193 }
194
195 fn validate(&self) -> CliResult<()> {
196 if self.time_scale <= 0.0 {
197 return Err(CliError::InvalidConfig {
198 message: "Time scale must be positive".into(),
199 });
200 }
201 Ok(())
202 }
203
204 async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
205 let config = self.load_scenario(ctx).await?;
206 self.run_scenario(ctx, config).await
207 }
208}
209
210#[derive(Debug, Clone, serde::Deserialize)]
212pub struct ScenarioConfig {
213 pub name: String,
214 #[serde(default)]
215 pub description: String,
216 #[serde(default)]
217 pub devices: Vec<ScenarioDeviceConfig>,
218 #[serde(default)]
219 pub events: Vec<ScenarioEventConfig>,
220}
221
222#[derive(Debug, Clone, serde::Deserialize)]
223pub struct ScenarioDeviceConfig {
224 pub id: String,
225 pub protocol: String,
226 #[serde(default)]
227 pub name: String,
228 #[serde(default)]
229 pub points: Vec<ScenarioPointConfig>,
230}
231
232#[derive(Debug, Clone, serde::Deserialize)]
233pub struct ScenarioPointConfig {
234 pub id: String,
235 #[serde(default)]
236 pub data_type: String,
237 #[serde(default)]
238 pub initial_value: serde_yaml::Value,
239 #[serde(default)]
240 pub pattern: Option<String>,
241}
242
243#[derive(Debug, Clone, serde::Deserialize)]
244pub struct ScenarioEventConfig {
245 pub name: String,
246 pub trigger: serde_yaml::Value,
247 pub actions: Vec<serde_yaml::Value>,
248}