1use crate::context::CliContext;
6use crate::error::{CliError, CliResult};
7use crate::output::{OutputFormat, StatusType, TableBuilder};
8use crate::runner::{Command, CommandOutput};
9use async_trait::async_trait;
10use mabi_core::prelude::*;
11use serde::Serialize;
12use std::net::SocketAddr;
13use std::time::Duration;
14
15#[async_trait]
17pub trait ProtocolCommand: Command {
18 fn protocol(&self) -> Protocol;
20
21 fn default_port(&self) -> u16;
23
24 async fn start_server(&self, ctx: &mut CliContext) -> CliResult<()>;
26
27 async fn stop_server(&self, ctx: &mut CliContext) -> CliResult<()>;
29}
30
31pub struct ModbusCommand {
37 bind_addr: SocketAddr,
39 devices: usize,
41 points_per_device: usize,
43 rtu_mode: bool,
45 serial_port: Option<String>,
47}
48
49impl ModbusCommand {
50 pub fn new() -> Self {
51 Self {
52 bind_addr: "0.0.0.0:502".parse().unwrap(),
53 devices: 1,
54 points_per_device: 100,
55 rtu_mode: false,
56 serial_port: None,
57 }
58 }
59
60 pub fn with_bind_addr(mut self, addr: SocketAddr) -> Self {
61 self.bind_addr = addr;
62 self
63 }
64
65 pub fn with_port(mut self, port: u16) -> Self {
66 self.bind_addr.set_port(port);
67 self
68 }
69
70 pub fn with_devices(mut self, devices: usize) -> Self {
71 self.devices = devices;
72 self
73 }
74
75 pub fn with_points(mut self, points: usize) -> Self {
76 self.points_per_device = points;
77 self
78 }
79
80 pub fn with_rtu_mode(mut self, serial_port: impl Into<String>) -> Self {
81 self.rtu_mode = true;
82 self.serial_port = Some(serial_port.into());
83 self
84 }
85}
86
87impl Default for ModbusCommand {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93#[async_trait]
94impl Command for ModbusCommand {
95 fn name(&self) -> &str {
96 "modbus"
97 }
98
99 fn description(&self) -> &str {
100 "Start a Modbus TCP/RTU simulator"
101 }
102
103 fn requires_engine(&self) -> bool {
104 true
105 }
106
107 fn supports_shutdown(&self) -> bool {
108 true
109 }
110
111 async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
112 {
114 let output = ctx.output();
115 if self.rtu_mode {
116 output.header("Modbus RTU Simulator");
117 output.kv(
118 "Serial Port",
119 self.serial_port.as_deref().unwrap_or("N/A"),
120 );
121 } else {
122 output.header("Modbus TCP Simulator");
123 output.kv("Bind Address", self.bind_addr);
124 }
125 output.kv("Devices", self.devices);
126 output.kv("Points per Device", self.points_per_device);
127
128 let total_points = self.devices * self.points_per_device;
129 output.kv("Total Points", total_points);
130 }
131
132 self.start_server(ctx).await?;
134
135 let colors_enabled = ctx.colors_enabled();
137 let table = TableBuilder::new(colors_enabled)
138 .header(["Unit ID", "Holding Regs", "Input Regs", "Coils", "Discrete", "Status"])
139 .status_row(
140 [
141 "1",
142 &(self.points_per_device / 4).to_string(),
143 &(self.points_per_device / 4).to_string(),
144 &(self.points_per_device / 4).to_string(),
145 &(self.points_per_device / 4).to_string(),
146 "Online",
147 ],
148 StatusType::Success,
149 );
150 table.print();
151
152 ctx.output().info("Press Ctrl+C to stop");
153
154 ctx.shutdown_signal().notified().await;
156
157 self.stop_server(ctx).await?;
158 ctx.output().success("Modbus simulator stopped");
159
160 Ok(CommandOutput::quiet_success())
161 }
162}
163
164#[async_trait]
165impl ProtocolCommand for ModbusCommand {
166 fn protocol(&self) -> Protocol {
167 if self.rtu_mode {
168 Protocol::ModbusRtu
169 } else {
170 Protocol::ModbusTcp
171 }
172 }
173
174 fn default_port(&self) -> u16 {
175 502
176 }
177
178 async fn start_server(&self, ctx: &mut CliContext) -> CliResult<()> {
179 let output = ctx.output();
180 let spinner = output.spinner("Starting Modbus server...");
181
182 tokio::time::sleep(Duration::from_millis(100)).await;
184
185 spinner.finish_with_message(format!("Modbus server started on {}", self.bind_addr));
186 Ok(())
187 }
188
189 async fn stop_server(&self, _ctx: &mut CliContext) -> CliResult<()> {
190 Ok(())
192 }
193}
194
195pub struct OpcuaCommand {
201 bind_addr: SocketAddr,
202 endpoint_path: String,
203 nodes: usize,
204 security_mode: String,
205}
206
207impl OpcuaCommand {
208 pub fn new() -> Self {
209 Self {
210 bind_addr: "0.0.0.0:4840".parse().unwrap(),
211 endpoint_path: "/".into(),
212 nodes: 1000,
213 security_mode: "None".into(),
214 }
215 }
216
217 pub fn with_port(mut self, port: u16) -> Self {
218 self.bind_addr.set_port(port);
219 self
220 }
221
222 pub fn with_endpoint(mut self, path: impl Into<String>) -> Self {
223 self.endpoint_path = path.into();
224 self
225 }
226
227 pub fn with_nodes(mut self, nodes: usize) -> Self {
228 self.nodes = nodes;
229 self
230 }
231
232 pub fn with_security(mut self, mode: impl Into<String>) -> Self {
233 self.security_mode = mode.into();
234 self
235 }
236}
237
238impl Default for OpcuaCommand {
239 fn default() -> Self {
240 Self::new()
241 }
242}
243
244#[async_trait]
245impl Command for OpcuaCommand {
246 fn name(&self) -> &str {
247 "opcua"
248 }
249
250 fn description(&self) -> &str {
251 "Start an OPC UA server simulator"
252 }
253
254 fn requires_engine(&self) -> bool {
255 true
256 }
257
258 fn supports_shutdown(&self) -> bool {
259 true
260 }
261
262 async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
263 {
265 let output = ctx.output();
266 output.header("OPC UA Simulator");
267 output.kv("Endpoint", format!("opc.tcp://{}{}", self.bind_addr, self.endpoint_path));
268 output.kv("Nodes", self.nodes);
269 output.kv("Security Mode", &self.security_mode);
270 }
271
272 self.start_server(ctx).await?;
273
274 let colors_enabled = ctx.colors_enabled();
275 let table = TableBuilder::new(colors_enabled)
276 .header(["Namespace", "Nodes", "Subscriptions", "Status"])
277 .status_row(["0", "Standard", "0", "Ready"], StatusType::Info)
278 .status_row(
279 ["1", &self.nodes.to_string(), "0", "Online"],
280 StatusType::Success,
281 );
282 table.print();
283
284 ctx.output().info("Press Ctrl+C to stop");
285 ctx.shutdown_signal().notified().await;
286
287 self.stop_server(ctx).await?;
288 ctx.output().success("OPC UA simulator stopped");
289
290 Ok(CommandOutput::quiet_success())
291 }
292}
293
294#[async_trait]
295impl ProtocolCommand for OpcuaCommand {
296 fn protocol(&self) -> Protocol {
297 Protocol::OpcUa
298 }
299
300 fn default_port(&self) -> u16 {
301 4840
302 }
303
304 async fn start_server(&self, ctx: &mut CliContext) -> CliResult<()> {
305 let output = ctx.output();
306 let spinner = output.spinner("Starting OPC UA server...");
307
308 tokio::time::sleep(Duration::from_millis(100)).await;
310
311 spinner.finish_with_message("OPC UA server started");
312 Ok(())
313 }
314
315 async fn stop_server(&self, _ctx: &mut CliContext) -> CliResult<()> {
316 Ok(())
317 }
318}
319
320pub struct BacnetCommand {
326 bind_addr: SocketAddr,
327 device_instance: u32,
328 objects: usize,
329 bbmd_enabled: bool,
330}
331
332impl BacnetCommand {
333 pub fn new() -> Self {
334 Self {
335 bind_addr: "0.0.0.0:47808".parse().unwrap(),
336 device_instance: 1234,
337 objects: 100,
338 bbmd_enabled: false,
339 }
340 }
341
342 pub fn with_port(mut self, port: u16) -> Self {
343 self.bind_addr.set_port(port);
344 self
345 }
346
347 pub fn with_device_instance(mut self, instance: u32) -> Self {
348 self.device_instance = instance;
349 self
350 }
351
352 pub fn with_objects(mut self, objects: usize) -> Self {
353 self.objects = objects;
354 self
355 }
356
357 pub fn with_bbmd(mut self, enabled: bool) -> Self {
358 self.bbmd_enabled = enabled;
359 self
360 }
361}
362
363impl Default for BacnetCommand {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369#[async_trait]
370impl Command for BacnetCommand {
371 fn name(&self) -> &str {
372 "bacnet"
373 }
374
375 fn description(&self) -> &str {
376 "Start a BACnet/IP simulator"
377 }
378
379 fn requires_engine(&self) -> bool {
380 true
381 }
382
383 fn supports_shutdown(&self) -> bool {
384 true
385 }
386
387 async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
388 {
390 let output = ctx.output();
391 output.header("BACnet/IP Simulator");
392 output.kv("Bind Address", self.bind_addr);
393 output.kv("Device Instance", self.device_instance);
394 output.kv("Objects", self.objects);
395 output.kv("BBMD", if self.bbmd_enabled { "Enabled" } else { "Disabled" });
396 }
397
398 self.start_server(ctx).await?;
399
400 let colors_enabled = ctx.colors_enabled();
401 let table = TableBuilder::new(colors_enabled)
402 .header(["Object Type", "Count", "Status"])
403 .status_row(["Device", "1", "Online"], StatusType::Success)
404 .status_row(["Analog Input", &(self.objects / 4).to_string(), "Active"], StatusType::Success)
405 .status_row(["Analog Output", &(self.objects / 4).to_string(), "Active"], StatusType::Success)
406 .status_row(["Binary Input", &(self.objects / 4).to_string(), "Active"], StatusType::Success)
407 .status_row(["Binary Output", &(self.objects / 4).to_string(), "Active"], StatusType::Success);
408 table.print();
409
410 ctx.output().info("Press Ctrl+C to stop");
411 ctx.shutdown_signal().notified().await;
412
413 self.stop_server(ctx).await?;
414 ctx.output().success("BACnet simulator stopped");
415
416 Ok(CommandOutput::quiet_success())
417 }
418}
419
420#[async_trait]
421impl ProtocolCommand for BacnetCommand {
422 fn protocol(&self) -> Protocol {
423 Protocol::BacnetIp
424 }
425
426 fn default_port(&self) -> u16 {
427 47808
428 }
429
430 async fn start_server(&self, ctx: &mut CliContext) -> CliResult<()> {
431 let output = ctx.output();
432 let spinner = output.spinner("Starting BACnet server...");
433
434 tokio::time::sleep(Duration::from_millis(100)).await;
436
437 spinner.finish_with_message("BACnet server started");
438 Ok(())
439 }
440
441 async fn stop_server(&self, _ctx: &mut CliContext) -> CliResult<()> {
442 Ok(())
443 }
444}
445
446pub struct KnxCommand {
452 bind_addr: SocketAddr,
453 individual_address: String,
454 group_objects: usize,
455}
456
457impl KnxCommand {
458 pub fn new() -> Self {
459 Self {
460 bind_addr: "0.0.0.0:3671".parse().unwrap(),
461 individual_address: "1.1.1".into(),
462 group_objects: 100,
463 }
464 }
465
466 pub fn with_port(mut self, port: u16) -> Self {
467 self.bind_addr.set_port(port);
468 self
469 }
470
471 pub fn with_individual_address(mut self, addr: impl Into<String>) -> Self {
472 self.individual_address = addr.into();
473 self
474 }
475
476 pub fn with_group_objects(mut self, count: usize) -> Self {
477 self.group_objects = count;
478 self
479 }
480}
481
482impl Default for KnxCommand {
483 fn default() -> Self {
484 Self::new()
485 }
486}
487
488#[async_trait]
489impl Command for KnxCommand {
490 fn name(&self) -> &str {
491 "knx"
492 }
493
494 fn description(&self) -> &str {
495 "Start a KNXnet/IP simulator"
496 }
497
498 fn requires_engine(&self) -> bool {
499 true
500 }
501
502 fn supports_shutdown(&self) -> bool {
503 true
504 }
505
506 async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
507 {
509 let output = ctx.output();
510 output.header("KNXnet/IP Simulator");
511 output.kv("Bind Address", self.bind_addr);
512 output.kv("Individual Address", &self.individual_address);
513 output.kv("Group Objects", self.group_objects);
514 }
515
516 self.start_server(ctx).await?;
517
518 let colors_enabled = ctx.colors_enabled();
519 let table = TableBuilder::new(colors_enabled)
520 .header(["Service", "Status"])
521 .status_row(["Core", "Ready"], StatusType::Success)
522 .status_row(["Device Management", "Ready"], StatusType::Success)
523 .status_row(["Tunneling", "Ready"], StatusType::Success);
524 table.print();
525
526 ctx.output().info("Press Ctrl+C to stop");
527 ctx.shutdown_signal().notified().await;
528
529 self.stop_server(ctx).await?;
530 ctx.output().success("KNX simulator stopped");
531
532 Ok(CommandOutput::quiet_success())
533 }
534}
535
536#[async_trait]
537impl ProtocolCommand for KnxCommand {
538 fn protocol(&self) -> Protocol {
539 Protocol::KnxIp
540 }
541
542 fn default_port(&self) -> u16 {
543 3671
544 }
545
546 async fn start_server(&self, ctx: &mut CliContext) -> CliResult<()> {
547 let output = ctx.output();
548 let spinner = output.spinner("Starting KNX server...");
549
550 tokio::time::sleep(Duration::from_millis(100)).await;
552
553 spinner.finish_with_message("KNX server started");
554 Ok(())
555 }
556
557 async fn stop_server(&self, _ctx: &mut CliContext) -> CliResult<()> {
558 Ok(())
559 }
560}