Skip to main content

mabi_cli/commands/
protocol.rs

1//! Protocol-specific commands.
2//!
3//! Provides subcommands for each supported protocol.
4
5use 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/// Base trait for protocol-specific commands.
16#[async_trait]
17pub trait ProtocolCommand: Command {
18    /// Get the protocol type.
19    fn protocol(&self) -> Protocol;
20
21    /// Get the default port.
22    fn default_port(&self) -> u16;
23
24    /// Start the protocol server.
25    async fn start_server(&self, ctx: &mut CliContext) -> CliResult<()>;
26
27    /// Stop the protocol server.
28    async fn stop_server(&self, ctx: &mut CliContext) -> CliResult<()>;
29}
30
31// =============================================================================
32// Modbus Command
33// =============================================================================
34
35/// Modbus protocol command.
36pub struct ModbusCommand {
37    /// Binding address.
38    bind_addr: SocketAddr,
39    /// Number of devices to simulate.
40    devices: usize,
41    /// Points per device.
42    points_per_device: usize,
43    /// Use RTU mode instead of TCP.
44    rtu_mode: bool,
45    /// Serial port for RTU mode.
46    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        // Display configuration first
113        {
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        // Start server
133        self.start_server(ctx).await?;
134
135        // Display status table
136        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        // Wait for shutdown
155        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        // TODO: Integrate with actual Modbus server from mabi-modbus
183        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        // TODO: Stop actual server
191        Ok(())
192    }
193}
194
195// =============================================================================
196// OPC UA Command
197// =============================================================================
198
199/// OPC UA protocol command.
200pub 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        // Display configuration first
264        {
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        // TODO: Integrate with actual OPC UA server
309        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
320// =============================================================================
321// BACnet Command
322// =============================================================================
323
324/// BACnet protocol command.
325pub 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        // Display configuration first
389        {
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        // TODO: Integrate with actual BACnet server
435        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
446// =============================================================================
447// KNX Command
448// =============================================================================
449
450/// KNX protocol command.
451pub 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        // Display configuration first
508        {
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        // TODO: Integrate with actual KNX server
551        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}