Skip to main content

mabi_modbus/rtu/
serial.rs

1//! Virtual serial port support for Modbus RTU simulation.
2//!
3//! This module provides virtual serial port creation and configuration,
4//! enabling RTU simulation without physical hardware.
5//!
6//! # Features
7//!
8//! - **PTY-based ports** (Unix): Creates pseudo-terminal pairs
9//! - **Named pipes** (Windows): Creates named pipe pairs
10//! - **Baud rate simulation**: Accurate timing delays
11//! - **Serial configuration**: Parity, stop bits, data bits
12//!
13//! # Unix Implementation
14//!
15//! On Unix systems, this uses PTY (pseudo-terminal) pairs:
16//! - Master: Used by the simulator
17//! - Slave: Exposed as a device file for clients
18//!
19//! # Example
20//!
21//! ```rust,ignore
22//! use mabi_modbus::rtu::{VirtualSerialConfig, VirtualSerial};
23//!
24//! fn main() -> Result<(), Box<dyn std::error::Error>> {
25//!     let config = VirtualSerialConfig::default()
26//!         .with_baud_rate(9600)
27//!         .with_slave_path("/tmp/modbus_slave");
28//!
29//!     #[cfg(unix)]
30//!     let serial = VirtualSerial::create(config)?;
31//!
32//!     // Clients can connect to /tmp/modbus_slave
33//!     #[cfg(unix)]
34//!     println!("Virtual serial ready at: {:?}", serial.slave_path());
35//!
36//!     Ok(())
37//! }
38//! ```
39
40use std::path::{Path, PathBuf};
41use std::time::Duration;
42
43use serde::{Deserialize, Serialize};
44use thiserror::Error;
45
46/// Serial port parity configuration.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
48#[serde(rename_all = "lowercase")]
49pub enum Parity {
50    /// No parity bit.
51    #[default]
52    None,
53    /// Even parity.
54    Even,
55    /// Odd parity.
56    Odd,
57    /// Mark parity (always 1).
58    Mark,
59    /// Space parity (always 0).
60    Space,
61}
62
63impl Parity {
64    /// Get the number of bits added by this parity setting.
65    pub fn bits(&self) -> u32 {
66        match self {
67            Parity::None => 0,
68            _ => 1,
69        }
70    }
71}
72
73/// Serial port stop bits configuration.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
75#[serde(rename_all = "lowercase")]
76pub enum StopBits {
77    /// 1 stop bit.
78    #[default]
79    One,
80    /// 1.5 stop bits.
81    OnePointFive,
82    /// 2 stop bits.
83    Two,
84}
85
86impl StopBits {
87    /// Get the number of stop bits as a value.
88    pub fn bits(&self) -> f32 {
89        match self {
90            StopBits::One => 1.0,
91            StopBits::OnePointFive => 1.5,
92            StopBits::Two => 2.0,
93        }
94    }
95}
96
97/// Serial port data bits configuration.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
99#[serde(rename_all = "lowercase")]
100pub enum DataBits {
101    Five = 5,
102    Six = 6,
103    Seven = 7,
104    Eight = 8,
105}
106
107impl Default for DataBits {
108    fn default() -> Self {
109        Self::Eight
110    }
111}
112
113impl DataBits {
114    /// Get the number of data bits.
115    pub fn bits(&self) -> u32 {
116        *self as u32
117    }
118}
119
120/// Serial port configuration.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct SerialConfig {
123    /// Baud rate (bits per second).
124    pub baud_rate: u32,
125
126    /// Data bits per character.
127    pub data_bits: DataBits,
128
129    /// Parity setting.
130    pub parity: Parity,
131
132    /// Stop bits.
133    pub stop_bits: StopBits,
134
135    /// Enable hardware flow control (RTS/CTS).
136    pub flow_control: bool,
137}
138
139impl Default for SerialConfig {
140    fn default() -> Self {
141        Self {
142            baud_rate: 9600,
143            data_bits: DataBits::Eight,
144            parity: Parity::None,
145            stop_bits: StopBits::One,
146            flow_control: false,
147        }
148    }
149}
150
151impl SerialConfig {
152    /// Create a new serial config with the given baud rate.
153    pub fn new(baud_rate: u32) -> Self {
154        Self {
155            baud_rate,
156            ..Default::default()
157        }
158    }
159
160    /// Common configuration: 9600/8N1.
161    pub fn baud_9600() -> Self {
162        Self::new(9600)
163    }
164
165    /// Common configuration: 19200/8N1.
166    pub fn baud_19200() -> Self {
167        Self::new(19200)
168    }
169
170    /// Common configuration: 38400/8N1.
171    pub fn baud_38400() -> Self {
172        Self::new(38400)
173    }
174
175    /// Common configuration: 115200/8N1.
176    pub fn baud_115200() -> Self {
177        Self::new(115200)
178    }
179
180    /// Set parity.
181    pub fn with_parity(mut self, parity: Parity) -> Self {
182        self.parity = parity;
183        self
184    }
185
186    /// Set stop bits.
187    pub fn with_stop_bits(mut self, stop_bits: StopBits) -> Self {
188        self.stop_bits = stop_bits;
189        self
190    }
191
192    /// Set data bits.
193    pub fn with_data_bits(mut self, data_bits: DataBits) -> Self {
194        self.data_bits = data_bits;
195        self
196    }
197
198    /// Calculate bits per character (start + data + parity + stop).
199    pub fn bits_per_character(&self) -> f32 {
200        1.0 // Start bit
201            + self.data_bits.bits() as f32
202            + self.parity.bits() as f32
203            + self.stop_bits.bits()
204    }
205
206    /// Calculate character transmission time.
207    pub fn char_time(&self) -> Duration {
208        let bits = self.bits_per_character();
209        let seconds = bits / self.baud_rate as f32;
210        Duration::from_secs_f32(seconds)
211    }
212
213    /// Calculate transmission time for a given number of bytes.
214    pub fn transmission_time(&self, bytes: usize) -> Duration {
215        self.char_time() * bytes as u32
216    }
217
218    /// Calculate inter-frame timeout (3.5 character times).
219    pub fn inter_frame_timeout(&self) -> Duration {
220        // For high baud rates (> 19200), use minimum of 1.75ms
221        if self.baud_rate > 19200 {
222            Duration::from_micros(1750)
223        } else {
224            let char_time = self.char_time();
225            Duration::from_secs_f32(char_time.as_secs_f32() * 3.5)
226        }
227    }
228
229    /// Calculate inter-character timeout (1.5 character times).
230    pub fn inter_char_timeout(&self) -> Duration {
231        // For high baud rates (> 19200), use minimum of 0.75ms
232        if self.baud_rate > 19200 {
233            Duration::from_micros(750)
234        } else {
235            let char_time = self.char_time();
236            Duration::from_secs_f32(char_time.as_secs_f32() * 1.5)
237        }
238    }
239}
240
241/// Virtual serial port configuration.
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct VirtualSerialConfig {
244    /// Serial port configuration.
245    #[serde(flatten)]
246    pub serial: SerialConfig,
247
248    /// Path for the slave device (client connection point).
249    pub slave_path: PathBuf,
250
251    /// Optional symbolic link path for the slave device.
252    pub symlink_path: Option<PathBuf>,
253
254    /// Enable transmission delay simulation.
255    pub simulate_delays: bool,
256
257    /// Buffer size for the virtual port.
258    pub buffer_size: usize,
259}
260
261impl Default for VirtualSerialConfig {
262    fn default() -> Self {
263        Self {
264            serial: SerialConfig::default(),
265            slave_path: PathBuf::from("/tmp/modbus_rtu_slave"),
266            symlink_path: None,
267            simulate_delays: true,
268            buffer_size: 4096,
269        }
270    }
271}
272
273impl VirtualSerialConfig {
274    /// Create a new config with the specified slave path.
275    pub fn new<P: AsRef<Path>>(slave_path: P) -> Self {
276        Self {
277            slave_path: slave_path.as_ref().to_path_buf(),
278            ..Default::default()
279        }
280    }
281
282    /// Set the baud rate.
283    pub fn with_baud_rate(mut self, baud_rate: u32) -> Self {
284        self.serial.baud_rate = baud_rate;
285        self
286    }
287
288    /// Set the slave path.
289    pub fn with_slave_path<P: AsRef<Path>>(mut self, path: P) -> Self {
290        self.slave_path = path.as_ref().to_path_buf();
291        self
292    }
293
294    /// Set a symbolic link path for the slave device.
295    pub fn with_symlink<P: AsRef<Path>>(mut self, path: P) -> Self {
296        self.symlink_path = Some(path.as_ref().to_path_buf());
297        self
298    }
299
300    /// Set serial configuration.
301    pub fn with_serial_config(mut self, config: SerialConfig) -> Self {
302        self.serial = config;
303        self
304    }
305
306    /// Enable or disable transmission delay simulation.
307    pub fn with_delay_simulation(mut self, enabled: bool) -> Self {
308        self.simulate_delays = enabled;
309        self
310    }
311
312    /// Set buffer size.
313    pub fn with_buffer_size(mut self, size: usize) -> Self {
314        self.buffer_size = size;
315        self
316    }
317}
318
319/// Virtual serial port errors.
320#[derive(Debug, Error)]
321pub enum VirtualSerialError {
322    /// I/O error.
323    #[error("I/O error: {0}")]
324    Io(#[from] std::io::Error),
325
326    /// PTY creation failed.
327    #[error("Failed to create PTY: {0}")]
328    PtyCreation(String),
329
330    /// Symlink creation failed.
331    #[error("Failed to create symlink: {source}")]
332    Symlink {
333        path: PathBuf,
334        #[source]
335        source: std::io::Error,
336    },
337
338    /// Port not available.
339    #[error("Virtual serial port not available on this platform")]
340    NotAvailable,
341
342    /// Port already in use.
343    #[error("Port already in use: {0}")]
344    InUse(PathBuf),
345}
346
347/// Virtual serial port.
348///
349/// Provides a simulated serial port using platform-specific mechanisms.
350#[derive(Debug)]
351pub struct VirtualSerial {
352    /// Configuration.
353    config: VirtualSerialConfig,
354
355    /// Platform-specific implementation.
356    inner: VirtualSerialInner,
357}
358
359#[cfg(unix)]
360#[derive(Debug)]
361struct VirtualSerialInner {
362    /// Master file descriptor.
363    master_fd: std::os::unix::io::RawFd,
364
365    /// Slave device path (actual PTY path).
366    slave_device_path: PathBuf,
367
368    /// Whether we created a symlink.
369    has_symlink: bool,
370}
371
372#[cfg(not(unix))]
373#[derive(Debug)]
374struct VirtualSerialInner {
375    // Placeholder for non-Unix platforms
376    _marker: std::marker::PhantomData<()>,
377}
378
379impl VirtualSerial {
380    /// Create a new virtual serial port.
381    #[cfg(unix)]
382    pub fn create(config: VirtualSerialConfig) -> Result<Self, VirtualSerialError> {
383        use std::os::unix::io::AsRawFd;
384
385        // Open PTY master
386        let master = nix::pty::posix_openpt(nix::fcntl::OFlag::O_RDWR | nix::fcntl::OFlag::O_NOCTTY)
387            .map_err(|e| VirtualSerialError::PtyCreation(e.to_string()))?;
388
389        // Grant access to slave
390        nix::pty::grantpt(&master)
391            .map_err(|e| VirtualSerialError::PtyCreation(format!("grantpt failed: {}", e)))?;
392
393        // Unlock slave
394        nix::pty::unlockpt(&master)
395            .map_err(|e| VirtualSerialError::PtyCreation(format!("unlockpt failed: {}", e)))?;
396
397        // Get slave name
398        let slave_name = unsafe {
399            nix::pty::ptsname(&master)
400                .map_err(|e| VirtualSerialError::PtyCreation(format!("ptsname failed: {}", e)))?
401        };
402
403        let slave_device_path = PathBuf::from(&slave_name);
404
405        // Create symlink if requested
406        let has_symlink = if let Some(ref symlink_path) = config.symlink_path {
407            // Remove existing symlink if present
408            let _ = std::fs::remove_file(symlink_path);
409
410            std::os::unix::fs::symlink(&slave_device_path, symlink_path).map_err(|e| {
411                VirtualSerialError::Symlink {
412                    path: symlink_path.clone(),
413                    source: e,
414                }
415            })?;
416            true
417        } else if config.slave_path != slave_device_path {
418            // Create symlink at slave_path pointing to actual device
419            let _ = std::fs::remove_file(&config.slave_path);
420
421            std::os::unix::fs::symlink(&slave_device_path, &config.slave_path).map_err(|e| {
422                VirtualSerialError::Symlink {
423                    path: config.slave_path.clone(),
424                    source: e,
425                }
426            })?;
427            true
428        } else {
429            false
430        };
431
432        let inner = VirtualSerialInner {
433            master_fd: master.as_raw_fd(),
434            slave_device_path,
435            has_symlink,
436        };
437
438        // Keep the master fd open by forgetting the PtyMaster
439        std::mem::forget(master);
440
441        Ok(Self { config, inner })
442    }
443
444    /// Create a new virtual serial port (non-Unix stub).
445    #[cfg(not(unix))]
446    pub fn create(_config: VirtualSerialConfig) -> Result<Self, VirtualSerialError> {
447        Err(VirtualSerialError::NotAvailable)
448    }
449
450    /// Get the slave device path (where clients should connect).
451    pub fn slave_path(&self) -> &Path {
452        &self.config.slave_path
453    }
454
455    /// Get the actual PTY device path.
456    #[cfg(unix)]
457    pub fn device_path(&self) -> &Path {
458        &self.inner.slave_device_path
459    }
460
461    /// Get the configuration.
462    pub fn config(&self) -> &VirtualSerialConfig {
463        &self.config
464    }
465
466    /// Get the serial configuration.
467    pub fn serial_config(&self) -> &SerialConfig {
468        &self.config.serial
469    }
470
471    /// Calculate transmission delay for a given number of bytes.
472    pub fn transmission_delay(&self, bytes: usize) -> Duration {
473        if self.config.simulate_delays {
474            self.config.serial.transmission_time(bytes)
475        } else {
476            Duration::ZERO
477        }
478    }
479
480    /// Get the master file descriptor (for async I/O).
481    #[cfg(unix)]
482    pub fn master_fd(&self) -> std::os::unix::io::RawFd {
483        self.inner.master_fd
484    }
485
486    /// Create an async reader/writer for the master side.
487    #[cfg(unix)]
488    pub fn into_async_io(self) -> std::io::Result<tokio::fs::File> {
489        use std::os::unix::io::FromRawFd;
490
491        // Create a std::fs::File from the raw fd
492        let file = unsafe { std::fs::File::from_raw_fd(self.inner.master_fd) };
493
494        // Forget self to prevent double-close
495        std::mem::forget(self);
496
497        // Convert to tokio::fs::File
498        Ok(tokio::fs::File::from_std(file))
499    }
500}
501
502#[cfg(unix)]
503impl Drop for VirtualSerial {
504    fn drop(&mut self) {
505        // Clean up symlink
506        if self.inner.has_symlink {
507            if let Some(ref symlink_path) = self.config.symlink_path {
508                let _ = std::fs::remove_file(symlink_path);
509            } else {
510                let _ = std::fs::remove_file(&self.config.slave_path);
511            }
512        }
513
514        // Close master fd
515        unsafe {
516            libc::close(self.inner.master_fd);
517        }
518    }
519}
520
521/// Serial port pair for testing.
522///
523/// Creates a connected pair of virtual serial ports for testing
524/// without requiring actual hardware or PTY support.
525#[derive(Debug)]
526pub struct SerialPair {
527    /// Server-side buffer.
528    server_tx: tokio::sync::mpsc::Sender<Vec<u8>>,
529    server_rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
530
531    /// Client-side buffer.
532    client_tx: tokio::sync::mpsc::Sender<Vec<u8>>,
533    client_rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
534
535    /// Configuration.
536    config: SerialConfig,
537}
538
539impl SerialPair {
540    /// Create a new connected pair with the given configuration.
541    pub fn new(config: SerialConfig) -> (SerialPairServer, SerialPairClient) {
542        let (server_tx, client_rx) = tokio::sync::mpsc::channel(256);
543        let (client_tx, server_rx) = tokio::sync::mpsc::channel(256);
544
545        let server = SerialPairServer {
546            tx: server_tx,
547            rx: server_rx,
548            config: config.clone(),
549        };
550
551        let client = SerialPairClient {
552            tx: client_tx,
553            rx: client_rx,
554            config,
555        };
556
557        (server, client)
558    }
559}
560
561/// Server side of a serial pair.
562#[derive(Debug)]
563pub struct SerialPairServer {
564    tx: tokio::sync::mpsc::Sender<Vec<u8>>,
565    rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
566    config: SerialConfig,
567}
568
569impl SerialPairServer {
570    /// Send data to the client.
571    pub async fn send(&self, data: &[u8]) -> Result<(), tokio::sync::mpsc::error::SendError<Vec<u8>>> {
572        // Simulate transmission delay
573        if self.config.baud_rate > 0 {
574            let delay = self.config.transmission_time(data.len());
575            tokio::time::sleep(delay).await;
576        }
577
578        self.tx.send(data.to_vec()).await
579    }
580
581    /// Receive data from the client.
582    pub async fn recv(&mut self) -> Option<Vec<u8>> {
583        self.rx.recv().await
584    }
585
586    /// Try to receive without blocking.
587    pub fn try_recv(&mut self) -> Result<Vec<u8>, tokio::sync::mpsc::error::TryRecvError> {
588        self.rx.try_recv()
589    }
590}
591
592/// Client side of a serial pair.
593#[derive(Debug)]
594pub struct SerialPairClient {
595    tx: tokio::sync::mpsc::Sender<Vec<u8>>,
596    rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
597    config: SerialConfig,
598}
599
600impl SerialPairClient {
601    /// Send data to the server.
602    pub async fn send(&self, data: &[u8]) -> Result<(), tokio::sync::mpsc::error::SendError<Vec<u8>>> {
603        // Simulate transmission delay
604        if self.config.baud_rate > 0 {
605            let delay = self.config.transmission_time(data.len());
606            tokio::time::sleep(delay).await;
607        }
608
609        self.tx.send(data.to_vec()).await
610    }
611
612    /// Receive data from the server.
613    pub async fn recv(&mut self) -> Option<Vec<u8>> {
614        self.rx.recv().await
615    }
616
617    /// Try to receive without blocking.
618    pub fn try_recv(&mut self) -> Result<Vec<u8>, tokio::sync::mpsc::error::TryRecvError> {
619        self.rx.try_recv()
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626
627    #[test]
628    fn test_serial_config_defaults() {
629        let config = SerialConfig::default();
630        assert_eq!(config.baud_rate, 9600);
631        assert_eq!(config.data_bits, DataBits::Eight);
632        assert_eq!(config.parity, Parity::None);
633        assert_eq!(config.stop_bits, StopBits::One);
634    }
635
636    #[test]
637    fn test_bits_per_character() {
638        // 8N1: 1 + 8 + 0 + 1 = 10 bits
639        let config = SerialConfig::default();
640        assert_eq!(config.bits_per_character(), 10.0);
641
642        // 8E1: 1 + 8 + 1 + 1 = 11 bits
643        let config = SerialConfig::default().with_parity(Parity::Even);
644        assert_eq!(config.bits_per_character(), 11.0);
645
646        // 8N2: 1 + 8 + 0 + 2 = 11 bits
647        let config = SerialConfig::default().with_stop_bits(StopBits::Two);
648        assert_eq!(config.bits_per_character(), 11.0);
649    }
650
651    #[test]
652    fn test_char_time() {
653        let config = SerialConfig::new(9600);
654        let char_time = config.char_time();
655
656        // 10 bits at 9600 baud ≈ 1.04ms
657        let ms = char_time.as_micros();
658        assert!(ms > 1000 && ms < 1100);
659    }
660
661    #[test]
662    fn test_inter_frame_timeout() {
663        // Low baud rate: 3.5 character times
664        let config = SerialConfig::new(9600);
665        let timeout = config.inter_frame_timeout();
666        let ms = timeout.as_micros();
667        assert!(ms > 3500 && ms < 4000);
668
669        // High baud rate: fixed 1.75ms minimum
670        let config = SerialConfig::new(115200);
671        let timeout = config.inter_frame_timeout();
672        assert_eq!(timeout, Duration::from_micros(1750));
673    }
674
675    #[test]
676    fn test_virtual_serial_config() {
677        let config = VirtualSerialConfig::default()
678            .with_baud_rate(19200)
679            .with_slave_path("/tmp/test_serial")
680            .with_delay_simulation(true);
681
682        assert_eq!(config.serial.baud_rate, 19200);
683        assert_eq!(config.slave_path, PathBuf::from("/tmp/test_serial"));
684        assert!(config.simulate_delays);
685    }
686
687    #[tokio::test]
688    async fn test_serial_pair() {
689        let (server, mut client) = SerialPair::new(SerialConfig::new(115200));
690
691        // Server sends to client
692        let data = vec![0x01, 0x02, 0x03];
693        server.send(&data).await.unwrap();
694
695        let received = client.recv().await.unwrap();
696        assert_eq!(received, data);
697    }
698
699    #[tokio::test]
700    async fn test_serial_pair_bidirectional() {
701        let (mut server, mut client) = SerialPair::new(SerialConfig::new(115200));
702
703        // Client sends to server
704        let client_data = vec![0x01, 0x03, 0x00, 0x00, 0x00, 0x0A];
705        client.send(&client_data).await.unwrap();
706
707        let received = server.recv().await.unwrap();
708        assert_eq!(received, client_data);
709
710        // Server sends response
711        let server_data = vec![0x01, 0x03, 0x14]; // Start of response
712        server.send(&server_data).await.unwrap();
713
714        let received = client.recv().await.unwrap();
715        assert_eq!(received, server_data);
716    }
717}