1use crate::context::CliContext;
6use crate::error::CliResult;
7use crate::output::{OutputFormat, PaginatedTable, StatusType, TableBuilder};
8use crate::runner::{Command, CommandOutput};
9use async_trait::async_trait;
10use mabi_core::prelude::*;
11use mabi_core::tags::Tags;
12use serde::Serialize;
13use std::net::SocketAddr;
14use std::sync::Arc;
15use std::time::Duration;
16use tokio::sync::Mutex;
17use tokio::task::JoinHandle;
18
19use mabi_bacnet::prelude::{
21 default_object_descriptors, BACnetServer, ObjectRegistry, ServerConfig as BacnetServerConfig,
22};
23use mabi_knx::{
24 DptId, GroupAddress, GroupObjectTable, IndividualAddress, KnxServer, KnxServerConfig,
25};
26use mabi_modbus::{tcp::ServerConfigV2, ModbusDevice, ModbusDeviceConfig, ModbusTcpServerV2};
27use mabi_opcua::{OpcUaServer, OpcUaServerConfig};
28
29async fn check_port_availability(addr: SocketAddr) {
34 use tokio::io::{AsyncReadExt, AsyncWriteExt};
35 use tokio::net::TcpStream;
36
37 let connect_result =
39 tokio::time::timeout(Duration::from_millis(500), TcpStream::connect(addr)).await;
40
41 match connect_result {
42 Ok(Ok(_first_stream)) => {
43 drop(_first_stream);
45
46 let probe_result = tokio::time::timeout(Duration::from_secs(1), async {
47 let mut stream = TcpStream::connect(addr).await?;
48 let request: [u8; 12] = [
50 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x00, 0x00, 0x01,
51 ];
52 stream.write_all(&request).await?;
53 let mut response = [0u8; 32];
54 let n = stream.read(&mut response).await?;
55 Ok::<_, std::io::Error>(n)
56 })
57 .await;
58
59 match probe_result {
60 Ok(Ok(n)) if n >= 7 => {
61 tracing::warn!(
62 port = addr.port(),
63 "Port {} is already in use by a responding Modbus server. \
64 The new server will fail to bind.",
65 addr.port()
66 );
67 }
68 _ => {
69 tracing::warn!(
71 port = addr.port(),
72 "Port {} is in use: TCP connects but no Modbus response. \
73 This may be a suspended (zombie) process holding the port.\n \
74 Diagnostic: lsof -i :{} | grep LISTEN\n \
75 To kill: kill $(lsof -ti :{} -sTCP:LISTEN)",
76 addr.port(),
77 addr.port(),
78 addr.port()
79 );
80 }
81 }
82 }
83 Ok(Err(_)) | Err(_) => {
84 tracing::debug!(port = addr.port(), "Port {} is available", addr.port());
86 }
87 }
88}
89
90#[async_trait]
92pub trait ProtocolCommand: Command {
93 fn protocol(&self) -> Protocol;
95
96 fn default_port(&self) -> u16;
98
99 async fn start_server(&self, ctx: &mut CliContext) -> CliResult<()>;
101
102 async fn stop_server(&self, ctx: &mut CliContext) -> CliResult<()>;
104}
105
106pub struct ModbusCommand {
112 bind_addr: SocketAddr,
114 devices: usize,
116 points_per_device: usize,
118 rtu_mode: bool,
120 serial_port: Option<String>,
122 tags: Tags,
124 server: Arc<Mutex<Option<Arc<ModbusTcpServerV2>>>>,
126 server_task: Arc<Mutex<Option<JoinHandle<()>>>>,
128}
129
130impl ModbusCommand {
131 pub fn new() -> Self {
132 Self {
133 bind_addr: "0.0.0.0:502".parse().unwrap(),
134 devices: 1,
135 points_per_device: 100,
136 rtu_mode: false,
137 serial_port: None,
138 tags: Tags::new(),
139 server: Arc::new(Mutex::new(None)),
140 server_task: Arc::new(Mutex::new(None)),
141 }
142 }
143
144 pub fn with_bind_addr(mut self, addr: SocketAddr) -> Self {
145 self.bind_addr = addr;
146 self
147 }
148
149 pub fn with_port(mut self, port: u16) -> Self {
150 self.bind_addr.set_port(port);
151 self
152 }
153
154 pub fn with_devices(mut self, devices: usize) -> Self {
155 self.devices = devices;
156 self
157 }
158
159 pub fn with_points(mut self, points: usize) -> Self {
160 self.points_per_device = points;
161 self
162 }
163
164 pub fn with_rtu_mode(mut self, serial_port: impl Into<String>) -> Self {
165 self.rtu_mode = true;
166 self.serial_port = Some(serial_port.into());
167 self
168 }
169
170 pub fn with_tags(mut self, tags: Tags) -> Self {
171 self.tags = tags;
172 self
173 }
174}
175
176impl Default for ModbusCommand {
177 fn default() -> Self {
178 Self::new()
179 }
180}
181
182#[async_trait]
183impl Command for ModbusCommand {
184 fn name(&self) -> &str {
185 "modbus"
186 }
187
188 fn description(&self) -> &str {
189 "Start a Modbus TCP/RTU simulator"
190 }
191
192 fn requires_engine(&self) -> bool {
193 true
194 }
195
196 fn supports_shutdown(&self) -> bool {
197 true
198 }
199
200 async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
201 let format = ctx.output().format();
202 let is_quiet = ctx.is_quiet();
203 let is_verbose = ctx.is_verbose();
204 let is_debug = ctx.is_debug();
205
206 if !is_quiet && matches!(format, OutputFormat::Table) {
207 let output = ctx.output();
208 if self.rtu_mode {
209 output.header("Modbus RTU Simulator");
210 output.kv("Serial Port", self.serial_port.as_deref().unwrap_or("N/A"));
211 } else {
212 output.header("Modbus TCP Simulator");
213 output.kv("Bind Address", self.bind_addr);
214 }
215 output.kv("Devices", self.devices);
216 output.kv("Points per Device", self.points_per_device);
217 output.kv("Total Points", self.devices * self.points_per_device);
218 }
219
220 if is_verbose {
222 ctx.vprintln(format!(
223 " Protocol Mode: {}",
224 if self.rtu_mode { "RTU" } else { "TCP" }
225 ));
226 ctx.vprintln(format!(
227 " Points Distribution: {} per register type",
228 self.points_per_device / 4
229 ));
230 }
231
232 if is_debug {
234 ctx.dprintln(format!("Bind address: {}", self.bind_addr));
235 ctx.dprintln(format!(
236 "RTU mode: {}, Serial: {:?}",
237 self.rtu_mode, self.serial_port
238 ));
239 ctx.dprintln(format!(
240 "Devices: {}, Points/device: {}",
241 self.devices, self.points_per_device
242 ));
243 }
244
245 self.start_server(ctx).await?;
246
247 let points_per_type = self.points_per_device / 4;
248
249 if !is_quiet {
250 match format {
251 OutputFormat::Table => {
252 let colors_enabled = ctx.colors_enabled();
253 let builder = TableBuilder::new(colors_enabled).header([
254 "Unit ID",
255 "Holding Regs",
256 "Input Regs",
257 "Coils",
258 "Discrete",
259 "Status",
260 ]);
261
262 let devices = self.devices;
263 let pts = points_per_type.to_string();
264 let table = PaginatedTable::default().render(builder, devices, 6, |i| {
265 let unit_id = (i + 1).to_string();
266 (
267 vec![
268 unit_id,
269 pts.clone(),
270 pts.clone(),
271 pts.clone(),
272 pts.clone(),
273 "Online".into(),
274 ],
275 StatusType::Success,
276 )
277 });
278 table.print();
279 }
280 _ => {
281 #[derive(Serialize)]
282 struct ModbusServerInfo {
283 protocol: String,
284 bind_address: String,
285 devices: usize,
286 points_per_device: usize,
287 total_points: usize,
288 rtu_mode: bool,
289 serial_port: Option<String>,
290 device_list: Vec<ModbusDeviceInfo>,
291 status: String,
292 }
293 #[derive(Serialize)]
294 struct ModbusDeviceInfo {
295 unit_id: usize,
296 holding_registers: usize,
297 input_registers: usize,
298 coils: usize,
299 discrete_inputs: usize,
300 status: String,
301 }
302 let device_list: Vec<ModbusDeviceInfo> = (0..self.devices)
303 .map(|i| ModbusDeviceInfo {
304 unit_id: i + 1,
305 holding_registers: points_per_type,
306 input_registers: points_per_type,
307 coils: points_per_type,
308 discrete_inputs: points_per_type,
309 status: "Online".into(),
310 })
311 .collect();
312 let info = ModbusServerInfo {
313 protocol: if self.rtu_mode {
314 "Modbus RTU".into()
315 } else {
316 "Modbus TCP".into()
317 },
318 bind_address: self.bind_addr.to_string(),
319 devices: self.devices,
320 points_per_device: self.points_per_device,
321 total_points: self.devices * self.points_per_device,
322 rtu_mode: self.rtu_mode,
323 serial_port: self.serial_port.clone(),
324 device_list,
325 status: "Online".into(),
326 };
327 let _ = ctx.output().write(&info);
328 }
329 }
330 }
331
332 if !is_quiet {
333 ctx.output().info("Press Ctrl+C to stop");
334 }
335 ctx.shutdown_signal().notified().await;
336
337 self.stop_server(ctx).await?;
338 if !is_quiet {
339 ctx.output().success("Modbus simulator stopped");
340 }
341
342 Ok(CommandOutput::quiet_success())
343 }
344}
345
346#[async_trait]
347impl ProtocolCommand for ModbusCommand {
348 fn protocol(&self) -> Protocol {
349 if self.rtu_mode {
350 Protocol::ModbusRtu
351 } else {
352 Protocol::ModbusTcp
353 }
354 }
355
356 fn default_port(&self) -> u16 {
357 502
358 }
359
360 async fn start_server(&self, ctx: &mut CliContext) -> CliResult<()> {
361 let output = ctx.output();
362
363 check_port_availability(self.bind_addr).await;
365
366 let spinner = output.spinner("Starting Modbus server...");
367
368 let config = ServerConfigV2 {
369 bind_address: self.bind_addr,
370 ..Default::default()
371 };
372
373 let server = Arc::new(ModbusTcpServerV2::new(config));
374
375 for i in 0..self.devices {
376 let unit_id = (i + 1) as u8;
377 let points = (self.points_per_device / 4) as u16;
378 let device_config = ModbusDeviceConfig {
379 unit_id,
380 name: format!("Device-{}", unit_id),
381 holding_registers: points,
382 input_registers: points,
383 coils: points,
384 discrete_inputs: points,
385 response_delay_ms: 0,
386 tags: self.tags.clone(),
387 };
388 let device = ModbusDevice::new(device_config);
389 server.add_device(device);
390 }
391
392 {
393 let mut server_guard = self.server.lock().await;
394 *server_guard = Some(server.clone());
395 }
396
397 let server_clone = server.clone();
398 let task = tokio::spawn(async move {
399 if let Err(e) = server_clone.run().await {
400 tracing::error!("Modbus server error: {}", e);
401 }
402 });
403
404 {
405 let mut task_guard = self.server_task.lock().await;
406 *task_guard = Some(task);
407 }
408
409 tokio::time::sleep(Duration::from_millis(100)).await;
410
411 {
413 let task_guard = self.server_task.lock().await;
414 if let Some(task) = task_guard.as_ref() {
415 if task.is_finished() {
416 spinner.finish_with_message("Failed to start server");
417 return Err(crate::error::CliError::PortInUse {
418 port: self.bind_addr.port(),
419 });
420 }
421 }
422 }
423
424 spinner.finish_with_message(format!("Modbus server started on {}", self.bind_addr));
425 Ok(())
426 }
427
428 async fn stop_server(&self, _ctx: &mut CliContext) -> CliResult<()> {
429 if let Some(server) = self.server.lock().await.as_ref() {
430 server.shutdown();
431 }
432
433 if let Some(task) = self.server_task.lock().await.take() {
434 let _ = tokio::time::timeout(Duration::from_secs(5), task).await;
435 }
436
437 Ok(())
438 }
439}
440
441pub struct OpcuaCommand {
447 bind_addr: SocketAddr,
448 endpoint_path: String,
449 nodes: usize,
450 security_mode: String,
451 tags: Tags,
453 server: Arc<Mutex<Option<Arc<OpcUaServer>>>>,
455 server_task: Arc<Mutex<Option<JoinHandle<()>>>>,
457}
458
459impl OpcuaCommand {
460 pub fn new() -> Self {
461 Self {
462 bind_addr: "0.0.0.0:4840".parse().unwrap(),
463 endpoint_path: "/".into(),
464 nodes: 1000,
465 security_mode: "None".into(),
466 tags: Tags::new(),
467 server: Arc::new(Mutex::new(None)),
468 server_task: Arc::new(Mutex::new(None)),
469 }
470 }
471
472 pub fn with_port(mut self, port: u16) -> Self {
473 self.bind_addr.set_port(port);
474 self
475 }
476
477 pub fn with_endpoint(mut self, path: impl Into<String>) -> Self {
478 self.endpoint_path = path.into();
479 self
480 }
481
482 pub fn with_nodes(mut self, nodes: usize) -> Self {
483 self.nodes = nodes;
484 self
485 }
486
487 pub fn with_security(mut self, mode: impl Into<String>) -> Self {
488 self.security_mode = mode.into();
489 self
490 }
491
492 pub fn with_tags(mut self, tags: Tags) -> Self {
493 self.tags = tags;
494 self
495 }
496}
497
498impl Default for OpcuaCommand {
499 fn default() -> Self {
500 Self::new()
501 }
502}
503
504#[async_trait]
505impl Command for OpcuaCommand {
506 fn name(&self) -> &str {
507 "opcua"
508 }
509
510 fn description(&self) -> &str {
511 "Start an OPC UA server simulator"
512 }
513
514 fn requires_engine(&self) -> bool {
515 true
516 }
517
518 fn supports_shutdown(&self) -> bool {
519 true
520 }
521
522 async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
523 let format = ctx.output().format();
524 let is_quiet = ctx.is_quiet();
525 let is_verbose = ctx.is_verbose();
526 let is_debug = ctx.is_debug();
527
528 if !is_quiet && matches!(format, OutputFormat::Table) {
529 let output = ctx.output();
530 output.header("OPC UA Simulator");
531 output.kv(
532 "Endpoint",
533 format!("opc.tcp://{}{}", self.bind_addr, self.endpoint_path),
534 );
535 output.kv("Nodes", self.nodes);
536 output.kv("Security Mode", &self.security_mode);
537 }
538
539 if is_verbose {
541 ctx.vprintln(format!(" Bind Address: {}", self.bind_addr));
542 ctx.vprintln(format!(" Endpoint Path: {}", self.endpoint_path));
543 ctx.vprintln(" Max Subscriptions: 1000");
544 ctx.vprintln(" Max Monitored Items: 10000");
545 }
546
547 if is_debug {
549 ctx.dprintln(format!(
550 "Full endpoint URL: opc.tcp://{}{}",
551 self.bind_addr, self.endpoint_path
552 ));
553 ctx.dprintln(format!("Node count: {}", self.nodes));
554 ctx.dprintln(format!("Security mode: {}", self.security_mode));
555 ctx.dprintln(format!("Sample nodes created: {}", self.nodes.min(100)));
556 }
557
558 self.start_server(ctx).await?;
559
560 if !is_quiet {
561 match format {
562 OutputFormat::Table => {
563 let colors_enabled = ctx.colors_enabled();
564 let table = TableBuilder::new(colors_enabled)
565 .header(["Namespace", "Nodes", "Subscriptions", "Status"])
566 .status_row(["0", "Standard", "0", "Ready"], StatusType::Info)
567 .status_row(
568 ["1", &self.nodes.to_string(), "0", "Online"],
569 StatusType::Success,
570 );
571 table.print();
572 }
573 _ => {
574 #[derive(Serialize)]
575 struct OpcuaServerInfo {
576 protocol: String,
577 endpoint: String,
578 nodes: usize,
579 security_mode: String,
580 namespaces: Vec<NamespaceInfo>,
581 status: String,
582 }
583 #[derive(Serialize)]
584 struct NamespaceInfo {
585 index: u32,
586 nodes: String,
587 subscriptions: u32,
588 status: String,
589 }
590 let info = OpcuaServerInfo {
591 protocol: "OPC UA".into(),
592 endpoint: format!("opc.tcp://{}{}", self.bind_addr, self.endpoint_path),
593 nodes: self.nodes,
594 security_mode: self.security_mode.clone(),
595 namespaces: vec![
596 NamespaceInfo {
597 index: 0,
598 nodes: "Standard".into(),
599 subscriptions: 0,
600 status: "Ready".into(),
601 },
602 NamespaceInfo {
603 index: 1,
604 nodes: self.nodes.to_string(),
605 subscriptions: 0,
606 status: "Online".into(),
607 },
608 ],
609 status: "Online".into(),
610 };
611 let _ = ctx.output().write(&info);
612 }
613 }
614 }
615
616 if !is_quiet {
617 ctx.output().info("Press Ctrl+C to stop");
618 }
619 ctx.shutdown_signal().notified().await;
620
621 self.stop_server(ctx).await?;
622 if !is_quiet {
623 ctx.output().success("OPC UA simulator stopped");
624 }
625
626 Ok(CommandOutput::quiet_success())
627 }
628}
629
630#[async_trait]
631impl ProtocolCommand for OpcuaCommand {
632 fn protocol(&self) -> Protocol {
633 Protocol::OpcUa
634 }
635
636 fn default_port(&self) -> u16 {
637 4840
638 }
639
640 async fn start_server(&self, ctx: &mut CliContext) -> CliResult<()> {
641 let output = ctx.output();
642 let spinner = output.spinner("Starting OPC UA server...");
643
644 let config = OpcUaServerConfig {
645 endpoint_url: format!("opc.tcp://{}{}", self.bind_addr, self.endpoint_path),
646 server_name: "Mabinogion OPC UA Simulator".to_string(),
647 max_subscriptions: 1000,
648 max_monitored_items: 10000,
649 ..Default::default()
650 };
651
652 let server = Arc::new(OpcUaServer::new(config).map_err(|e| {
653 crate::error::CliError::ExecutionFailed {
654 message: format!("Failed to create OPC UA server: {}", e),
655 }
656 })?);
657
658 let node_count = self.nodes.min(100);
661 for i in 0..node_count {
662 let node_id = format!("ns=2;i={}", 1000 + i);
663 let name = format!("Variable_{}", i);
664 let value = (i as f64) * 0.1;
665
666 if i % 2 == 0 {
667 let _ = server.add_writable_variable(node_id, name, value);
668 } else {
669 let _ = server.add_variable(node_id, name, value);
670 }
671 }
672
673 {
674 let mut server_guard = self.server.lock().await;
675 *server_guard = Some(server.clone());
676 }
677
678 let server_clone = server.clone();
679 let task = tokio::spawn(async move {
680 if let Err(e) = server_clone.start().await {
681 tracing::error!("OPC UA server error: {}", e);
682 }
683 });
684
685 {
686 let mut task_guard = self.server_task.lock().await;
687 *task_guard = Some(task);
688 }
689
690 tokio::time::sleep(Duration::from_millis(100)).await;
691
692 spinner.finish_with_message(format!("OPC UA server started on {}", self.bind_addr));
693 Ok(())
694 }
695
696 async fn stop_server(&self, _ctx: &mut CliContext) -> CliResult<()> {
697 if let Some(server) = self.server.lock().await.as_ref() {
698 let _ = server.stop().await;
699 }
700
701 if let Some(task) = self.server_task.lock().await.take() {
702 let _ = tokio::time::timeout(Duration::from_secs(5), task).await;
703 }
704
705 Ok(())
706 }
707}
708
709pub struct BacnetCommand {
715 bind_addr: SocketAddr,
716 device_instance: u32,
717 objects: usize,
718 bbmd_enabled: bool,
719 tags: Tags,
721 server: Arc<Mutex<Option<Arc<BACnetServer>>>>,
723 server_task: Arc<Mutex<Option<JoinHandle<()>>>>,
725}
726
727impl BacnetCommand {
728 pub fn new() -> Self {
729 Self {
730 bind_addr: "0.0.0.0:47808".parse().unwrap(),
731 device_instance: 1234,
732 objects: 0,
733 bbmd_enabled: false,
734 tags: Tags::new(),
735 server: Arc::new(Mutex::new(None)),
736 server_task: Arc::new(Mutex::new(None)),
737 }
738 }
739
740 pub fn with_port(mut self, port: u16) -> Self {
741 self.bind_addr.set_port(port);
742 self
743 }
744
745 pub fn with_device_instance(mut self, instance: u32) -> Self {
746 self.device_instance = instance;
747 self
748 }
749
750 pub fn with_objects(mut self, objects: usize) -> Self {
751 self.objects = objects;
752 self
753 }
754
755 pub fn with_bbmd(mut self, enabled: bool) -> Self {
756 self.bbmd_enabled = enabled;
757 self
758 }
759
760 pub fn with_tags(mut self, tags: Tags) -> Self {
761 self.tags = tags;
762 self
763 }
764}
765
766impl Default for BacnetCommand {
767 fn default() -> Self {
768 Self::new()
769 }
770}
771
772#[async_trait]
773impl Command for BacnetCommand {
774 fn name(&self) -> &str {
775 "bacnet"
776 }
777
778 fn description(&self) -> &str {
779 "Start a BACnet/IP simulator"
780 }
781
782 fn requires_engine(&self) -> bool {
783 true
784 }
785
786 fn supports_shutdown(&self) -> bool {
787 true
788 }
789
790 async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
791 let format = ctx.output().format();
792 let is_quiet = ctx.is_quiet();
793 let is_verbose = ctx.is_verbose();
794 let is_debug = ctx.is_debug();
795
796 if !is_quiet && matches!(format, OutputFormat::Table) {
797 let output = ctx.output();
798 output.header("BACnet/IP Simulator");
799 output.kv("Bind Address", self.bind_addr);
800 output.kv("Device Instance", self.device_instance);
801 output.kv("Objects", self.objects);
802 output.kv(
803 "BBMD",
804 if self.bbmd_enabled {
805 "Enabled"
806 } else {
807 "Disabled"
808 },
809 );
810 }
811
812 if is_verbose {
814 let per_type = if self.objects > 0 {
815 std::cmp::max(1, self.objects / 4)
816 } else {
817 0
818 };
819 if per_type == 0 {
820 ctx.vprintln(" Demo Objects: disabled (Device object only)");
821 } else {
822 ctx.vprintln(format!(
823 " Objects per Type: {} (AI: {}, AO: {}, BI: {}, BO: {})",
824 per_type, per_type, per_type, per_type, per_type
825 ));
826 }
827 ctx.vprintln(" Device Name: Mabinogion BACnet Simulator");
828 }
829
830 if is_debug {
832 ctx.dprintln(format!("Bind address: {}", self.bind_addr));
833 ctx.dprintln(format!("Device instance: {}", self.device_instance));
834 ctx.dprintln(format!(
835 "Total objects: {}, BBMD: {}",
836 self.objects, self.bbmd_enabled
837 ));
838 }
839
840 self.start_server(ctx).await?;
841
842 let per_type = if self.objects > 0 {
843 std::cmp::max(1, self.objects / 4)
844 } else {
845 0
846 };
847
848 if !is_quiet {
849 match format {
850 OutputFormat::Table => {
851 let colors_enabled = ctx.colors_enabled();
852 let demo_status = if per_type == 0 {
853 "Not created"
854 } else {
855 "Active"
856 };
857 let table = TableBuilder::new(colors_enabled)
858 .header(["Object Type", "Count", "Status"])
859 .status_row(["Device", "1", "Online"], StatusType::Success)
860 .status_row(
861 ["Analog Input", &per_type.to_string(), demo_status],
862 StatusType::Success,
863 )
864 .status_row(
865 ["Analog Output", &per_type.to_string(), demo_status],
866 StatusType::Success,
867 )
868 .status_row(
869 ["Binary Input", &per_type.to_string(), demo_status],
870 StatusType::Success,
871 )
872 .status_row(
873 ["Binary Output", &per_type.to_string(), demo_status],
874 StatusType::Success,
875 );
876 table.print();
877 }
878 _ => {
879 #[derive(Serialize)]
880 struct BacnetServerInfo {
881 protocol: String,
882 bind_address: String,
883 device_instance: u32,
884 objects: usize,
885 bbmd_enabled: bool,
886 object_types: Vec<ObjectTypeInfo>,
887 status: String,
888 }
889 #[derive(Serialize)]
890 struct ObjectTypeInfo {
891 object_type: String,
892 count: usize,
893 status: String,
894 }
895 let info = BacnetServerInfo {
896 protocol: "BACnet/IP".into(),
897 bind_address: self.bind_addr.to_string(),
898 device_instance: self.device_instance,
899 objects: self.objects,
900 bbmd_enabled: self.bbmd_enabled,
901 object_types: vec![
902 ObjectTypeInfo {
903 object_type: "Device".into(),
904 count: 1,
905 status: "Online".into(),
906 },
907 ObjectTypeInfo {
908 object_type: "Analog Input".into(),
909 count: per_type,
910 status: if per_type == 0 {
911 "Not created".into()
912 } else {
913 "Active".into()
914 },
915 },
916 ObjectTypeInfo {
917 object_type: "Analog Output".into(),
918 count: per_type,
919 status: if per_type == 0 {
920 "Not created".into()
921 } else {
922 "Active".into()
923 },
924 },
925 ObjectTypeInfo {
926 object_type: "Binary Input".into(),
927 count: per_type,
928 status: if per_type == 0 {
929 "Not created".into()
930 } else {
931 "Active".into()
932 },
933 },
934 ObjectTypeInfo {
935 object_type: "Binary Output".into(),
936 count: per_type,
937 status: if per_type == 0 {
938 "Not created".into()
939 } else {
940 "Active".into()
941 },
942 },
943 ],
944 status: "Online".into(),
945 };
946 let _ = ctx.output().write(&info);
947 }
948 }
949 }
950
951 if !is_quiet {
952 ctx.output().info("Press Ctrl+C to stop");
953 }
954 ctx.shutdown_signal().notified().await;
955
956 self.stop_server(ctx).await?;
957 if !is_quiet {
958 ctx.output().success("BACnet simulator stopped");
959 }
960
961 Ok(CommandOutput::quiet_success())
962 }
963}
964
965#[async_trait]
966impl ProtocolCommand for BacnetCommand {
967 fn protocol(&self) -> Protocol {
968 Protocol::BacnetIp
969 }
970
971 fn default_port(&self) -> u16 {
972 47808
973 }
974
975 async fn start_server(&self, ctx: &mut CliContext) -> CliResult<()> {
976 let output = ctx.output();
977 let spinner = output.spinner("Starting BACnet server...");
978
979 let config = BacnetServerConfig::new(self.device_instance)
980 .with_bind_addr(self.bind_addr)
981 .with_device_name("Mabinogion BACnet Simulator");
982
983 let registry = ObjectRegistry::new();
984
985 if self.objects > 0 {
986 let descriptors = default_object_descriptors();
987 let objects_per_type = std::cmp::max(1, self.objects / descriptors.len());
988 registry.populate_standard_objects(&descriptors, objects_per_type);
989 }
990
991 let server = Arc::new(BACnetServer::new(config, registry));
992
993 {
994 let mut server_guard = self.server.lock().await;
995 *server_guard = Some(server.clone());
996 }
997
998 let server_clone = server.clone();
999 let task = tokio::spawn(async move {
1000 if let Err(e) = server_clone.run().await {
1001 tracing::error!("BACnet server error: {}", e);
1002 }
1003 });
1004
1005 {
1006 let mut task_guard = self.server_task.lock().await;
1007 *task_guard = Some(task);
1008 }
1009
1010 tokio::time::sleep(Duration::from_millis(100)).await;
1011
1012 spinner.finish_with_message(format!("BACnet server started on {}", self.bind_addr));
1013 Ok(())
1014 }
1015
1016 async fn stop_server(&self, _ctx: &mut CliContext) -> CliResult<()> {
1017 if let Some(server) = self.server.lock().await.as_ref() {
1018 server.shutdown();
1019 }
1020
1021 if let Some(task) = self.server_task.lock().await.take() {
1022 let _ = tokio::time::timeout(Duration::from_secs(5), task).await;
1023 }
1024
1025 Ok(())
1026 }
1027}
1028
1029pub struct KnxCommand {
1035 bind_addr: SocketAddr,
1036 individual_address: String,
1037 group_objects: usize,
1038 tags: Tags,
1040 server: Arc<Mutex<Option<Arc<KnxServer>>>>,
1042 server_task: Arc<Mutex<Option<JoinHandle<()>>>>,
1044}
1045
1046impl KnxCommand {
1047 pub fn new() -> Self {
1048 Self {
1049 bind_addr: "0.0.0.0:3671".parse().unwrap(),
1050 individual_address: "1.1.1".into(),
1051 group_objects: 100,
1052 tags: Tags::new(),
1053 server: Arc::new(Mutex::new(None)),
1054 server_task: Arc::new(Mutex::new(None)),
1055 }
1056 }
1057
1058 pub fn with_port(mut self, port: u16) -> Self {
1059 self.bind_addr.set_port(port);
1060 self
1061 }
1062
1063 pub fn with_individual_address(mut self, addr: impl Into<String>) -> Self {
1064 self.individual_address = addr.into();
1065 self
1066 }
1067
1068 pub fn with_group_objects(mut self, count: usize) -> Self {
1069 self.group_objects = count;
1070 self
1071 }
1072
1073 pub fn with_tags(mut self, tags: Tags) -> Self {
1074 self.tags = tags;
1075 self
1076 }
1077}
1078
1079impl Default for KnxCommand {
1080 fn default() -> Self {
1081 Self::new()
1082 }
1083}
1084
1085#[async_trait]
1086impl Command for KnxCommand {
1087 fn name(&self) -> &str {
1088 "knx"
1089 }
1090
1091 fn description(&self) -> &str {
1092 "Start a KNXnet/IP simulator"
1093 }
1094
1095 fn requires_engine(&self) -> bool {
1096 true
1097 }
1098
1099 fn supports_shutdown(&self) -> bool {
1100 true
1101 }
1102
1103 async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
1104 let format = ctx.output().format();
1105 let is_quiet = ctx.is_quiet();
1106 let is_verbose = ctx.is_verbose();
1107 let is_debug = ctx.is_debug();
1108
1109 if !is_quiet && matches!(format, OutputFormat::Table) {
1110 let output = ctx.output();
1111 output.header("KNXnet/IP Simulator");
1112 output.kv("Bind Address", self.bind_addr);
1113 output.kv("Individual Address", &self.individual_address);
1114 output.kv("Group Objects", self.group_objects);
1115 }
1116
1117 if is_verbose {
1119 ctx.vprintln(" Max Connections: 10");
1120 ctx.vprintln(" Services: Core, Device Management, Tunneling");
1121 }
1122
1123 if is_debug {
1125 ctx.dprintln(format!("Bind address: {}", self.bind_addr));
1126 ctx.dprintln(format!("Individual address: {}", self.individual_address));
1127 ctx.dprintln(format!("Group objects: {}", self.group_objects));
1128 }
1129
1130 self.start_server(ctx).await?;
1131
1132 if !is_quiet {
1133 match format {
1134 OutputFormat::Table => {
1135 let colors_enabled = ctx.colors_enabled();
1136 let table = TableBuilder::new(colors_enabled)
1137 .header(["Service", "Status"])
1138 .status_row(["Core", "Ready"], StatusType::Success)
1139 .status_row(["Device Management", "Ready"], StatusType::Success)
1140 .status_row(["Tunneling", "Ready"], StatusType::Success);
1141 table.print();
1142 }
1143 _ => {
1144 #[derive(Serialize)]
1145 struct KnxServerInfo {
1146 protocol: String,
1147 bind_address: String,
1148 individual_address: String,
1149 group_objects: usize,
1150 services: Vec<ServiceInfo>,
1151 status: String,
1152 }
1153 #[derive(Serialize)]
1154 struct ServiceInfo {
1155 service: String,
1156 status: String,
1157 }
1158 let info = KnxServerInfo {
1159 protocol: "KNXnet/IP".into(),
1160 bind_address: self.bind_addr.to_string(),
1161 individual_address: self.individual_address.clone(),
1162 group_objects: self.group_objects,
1163 services: vec![
1164 ServiceInfo {
1165 service: "Core".into(),
1166 status: "Ready".into(),
1167 },
1168 ServiceInfo {
1169 service: "Device Management".into(),
1170 status: "Ready".into(),
1171 },
1172 ServiceInfo {
1173 service: "Tunneling".into(),
1174 status: "Ready".into(),
1175 },
1176 ],
1177 status: "Online".into(),
1178 };
1179 let _ = ctx.output().write(&info);
1180 }
1181 }
1182 }
1183
1184 if !is_quiet {
1185 ctx.output().info("Press Ctrl+C to stop");
1186 }
1187 ctx.shutdown_signal().notified().await;
1188
1189 self.stop_server(ctx).await?;
1190 if !is_quiet {
1191 ctx.output().success("KNX simulator stopped");
1192 }
1193
1194 Ok(CommandOutput::quiet_success())
1195 }
1196}
1197
1198#[async_trait]
1199impl ProtocolCommand for KnxCommand {
1200 fn protocol(&self) -> Protocol {
1201 Protocol::KnxIp
1202 }
1203
1204 fn default_port(&self) -> u16 {
1205 3671
1206 }
1207
1208 async fn start_server(&self, ctx: &mut CliContext) -> CliResult<()> {
1209 let output = ctx.output();
1210 let spinner = output.spinner("Starting KNX server...");
1211
1212 let individual_address: IndividualAddress =
1214 self.individual_address.parse().map_err(|_| {
1215 crate::error::CliError::ExecutionFailed {
1216 message: format!("Invalid individual address: {}", self.individual_address),
1217 }
1218 })?;
1219
1220 let config = KnxServerConfig {
1221 bind_addr: self.bind_addr,
1222 individual_address,
1223 max_connections: 256,
1224 ..Default::default()
1225 };
1226
1227 let group_table = Arc::new(GroupObjectTable::new());
1229 let dpt_types = [
1230 DptId::new(1, 1), DptId::new(5, 1), DptId::new(9, 1), DptId::new(9, 4), DptId::new(9, 7), DptId::new(12, 1), DptId::new(13, 1), DptId::new(14, 56), ];
1239 let dpt_names = [
1240 "Switch",
1241 "Scaling",
1242 "Temperature",
1243 "Lux",
1244 "Humidity",
1245 "Counter",
1246 "SignedCounter",
1247 "Float",
1248 ];
1249
1250 for i in 0..self.group_objects {
1251 let main = ((i / 256) + 1) as u8;
1252 let middle = ((i / 8) % 8) as u8;
1253 let sub = (i % 256) as u8;
1254 let addr = GroupAddress::three_level(main, middle, sub);
1255 let dpt_idx = i % dpt_types.len();
1256 let name = format!("{}_{}", dpt_names[dpt_idx], i);
1257 if let Err(e) = group_table.create(addr, &name, &dpt_types[dpt_idx]) {
1258 tracing::warn!("Failed to create group object {}: {}", i, e);
1259 }
1260 }
1261
1262 let server = Arc::new(KnxServer::new(config).with_group_objects(group_table));
1263
1264 {
1265 let mut server_guard = self.server.lock().await;
1266 *server_guard = Some(server.clone());
1267 }
1268
1269 let server_clone = server.clone();
1270 let task = tokio::spawn(async move {
1271 if let Err(e) = server_clone.start().await {
1272 tracing::error!("KNX server error: {}", e);
1273 }
1274 });
1275
1276 {
1277 let mut task_guard = self.server_task.lock().await;
1278 *task_guard = Some(task);
1279 }
1280
1281 tokio::time::sleep(Duration::from_millis(100)).await;
1282
1283 spinner.finish_with_message(format!("KNX server started on {}", self.bind_addr));
1284 Ok(())
1285 }
1286
1287 async fn stop_server(&self, _ctx: &mut CliContext) -> CliResult<()> {
1288 let server_opt = self.server.lock().await.take();
1290 if let Some(server) = server_opt {
1291 let _ = tokio::task::spawn_blocking(move || {
1293 let rt = tokio::runtime::Handle::current();
1294 rt.block_on(async {
1295 let _ = server.stop().await;
1296 })
1297 })
1298 .await;
1299 }
1300
1301 if let Some(task) = self.server_task.lock().await.take() {
1302 let _ = tokio::time::timeout(Duration::from_secs(5), task).await;
1303 }
1304
1305 Ok(())
1306 }
1307}