Skip to main content

mbus_serial/management/
std_serial.rs

1use std::io::{self, Read, Write};
2use std::time::Duration;
3
4use heapless::Vec;
5use mbus_core::data_unit::common::MAX_ADU_FRAME_LEN;
6use mbus_core::transport::{
7    BaudRate, DataBits as ConfigDataBits, ModbusConfig, Parity, SerialMode, Transport,
8    TransportError, TransportType,
9};
10use serialport::{ClearBuffer, DataBits as SerialPortDataBits, FlowControl, SerialPort, StopBits};
11
12#[cfg(feature = "logging")]
13macro_rules! serial_log_error {
14    ($($arg:tt)*) => {
15        log::error!($($arg)*)
16    };
17}
18
19#[cfg(not(feature = "logging"))]
20macro_rules! serial_log_error {
21    ($($arg:tt)*) => {{
22        let _ = core::format_args!($($arg)*);
23    }};
24}
25
26#[cfg(feature = "logging")]
27macro_rules! serial_log_warn {
28    ($($arg:tt)*) => {
29        log::warn!($($arg)*)
30    };
31}
32
33#[cfg(not(feature = "logging"))]
34macro_rules! serial_log_warn {
35    ($($arg:tt)*) => {{
36        let _ = core::format_args!($($arg)*);
37    }};
38}
39
40/// A concrete implementation of `Transport` for Serial communication using `serialport` crate.
41/// Supports both RTU and ASCII modes.
42#[derive(Debug)]
43pub struct StdSerialTransport {
44    port: Option<Box<dyn SerialPort>>,
45    mode: SerialMode, // The serial mode (RTU or ASCII).
46    // Store the configured timeout to restore it after dynamic adjustments in recv
47    timeout: Duration,
48    // Store the baud rate to calculate inter-frame delays dynamically.
49    baud_rate: u32,
50}
51
52impl StdSerialTransport {
53    /// Creates a new `StdSerialTransport` instance.
54    pub fn new(mode: SerialMode) -> Self {
55        Self {
56            port: None,
57            mode,
58            timeout: Duration::from_secs(1), // Default safe value, overwritten in connect
59            baud_rate: 9600,                 // Default, overwritten in connect.
60        }
61    }
62
63    /// Returns a list of available serial ports on the system.
64    /// This can be useful for allowing a user to select a port.
65    pub fn available_ports() -> Result<std::vec::Vec<serialport::SerialPortInfo>, serialport::Error>
66    {
67        serialport::available_ports()
68    }
69
70    /// Helper function to convert `std::io::Error` to `TransportError`.
71    ///
72    /// This maps common I/O error kinds to specific Modbus transport errors.
73    fn map_io_error(err: io::Error) -> TransportError {
74        match err.kind() {
75            io::ErrorKind::TimedOut => TransportError::Timeout,
76            io::ErrorKind::BrokenPipe
77            | io::ErrorKind::ConnectionReset
78            | io::ErrorKind::UnexpectedEof => TransportError::ConnectionClosed,
79            _ => TransportError::IoError,
80        }
81    }
82}
83
84impl Transport for StdSerialTransport {
85    type Error = TransportError;
86
87    /// Establishes a connection to the specified serial port.
88    ///
89    /// # Arguments
90    /// * `config` - The `ModbusConfig` containing the serial port configuration.
91    ///   This must be the `ModbusConfig::Serial` variant.
92    ///
93    /// # Returns
94    /// `Ok(())` if the connection is successfully established, or an error otherwise.
95    fn connect(&mut self, config: &ModbusConfig) -> Result<(), Self::Error> {
96        let serial_config = match config {
97            ModbusConfig::Serial(c) => c,
98            _ => return Err(TransportError::InvalidConfiguration),
99        };
100
101        // Ensure the mode from the configuration matches the mode this transport was initialized with.
102        if serial_config.mode != self.mode {
103            return Err(TransportError::InvalidConfiguration);
104        }
105
106        self.baud_rate = match serial_config.baud_rate {
107            BaudRate::Baud9600 => 9600,
108            BaudRate::Baud19200 => 19200,
109            BaudRate::Custom(rate) => rate,
110        };
111
112        let parity = match serial_config.parity {
113            Parity::None => serialport::Parity::None,
114            Parity::Even => serialport::Parity::Even,
115            Parity::Odd => serialport::Parity::Odd,
116        };
117
118        let data_bits = match serial_config.data_bits {
119            ConfigDataBits::Five => SerialPortDataBits::Five,
120            ConfigDataBits::Six => SerialPortDataBits::Six,
121            ConfigDataBits::Seven => SerialPortDataBits::Seven,
122            ConfigDataBits::Eight => SerialPortDataBits::Eight,
123        };
124
125        // Convert the numeric stop_bits from config to the serialport enum.
126        let stop_bits = match serial_config.stop_bits {
127            1 => StopBits::One,
128            2 => StopBits::Two,
129            _ => return Err(TransportError::InvalidConfiguration),
130        };
131
132        self.timeout = Duration::from_millis(serial_config.response_timeout_ms as u64);
133
134        // Build the serial port configuration.
135        let builder = serialport::new(serial_config.port_path.as_str(), self.baud_rate)
136            .parity(parity)
137            .data_bits(data_bits)
138            .stop_bits(stop_bits) // Use stop_bits from config.
139            .flow_control(FlowControl::None)
140            .timeout(self.timeout);
141
142        // Attempt to open the port.
143        match builder.open() {
144            Ok(port) => {
145                if let Err(e) = port.clear(ClearBuffer::All) {
146                    serial_log_warn!("Failed to clear serial buffers on connect: {}", e);
147                }
148                self.port = Some(port);
149                Ok(())
150            }
151            Err(e) => {
152                serial_log_error!(
153                    "Failed to open serial port '{}': {}",
154                    serial_config.port_path.as_str(),
155                    e
156                );
157                // Provide platform-specific hints for common serial port errors.
158                #[cfg(windows)]
159                {
160                    let error_string = e.to_string().to_lowercase();
161                    if error_string.contains("access is denied") {
162                        serial_log_error!(
163                            "Hint: 'Access is denied' on Windows usually means the port is already in use by another application."
164                        );
165                    }
166                    if error_string.contains("the system cannot find the file specified") {
167                        serial_log_error!(
168                            "Hint: 'The system cannot find the file specified' on Windows means the port does not exist. Check available ports."
169                        );
170                    }
171                }
172                if e.to_string().contains("Not a typewriter") {
173                    serial_log_error!(
174                        "Hint: This error often occurs on macOS when using a pseudo-terminal (pty) created by tools like socat."
175                    );
176                    serial_log_error!(
177                        "PTYs may not support setting serial parameters like baud rate. Consider using a physical serial port or a different virtual setup."
178                    );
179                }
180                Err(TransportError::ConnectionFailed)
181            }
182        }
183    }
184
185    /// Closes the active serial port connection.
186    ///
187    /// If no connection is active, this operation does nothing and returns `Ok(())`.
188    fn disconnect(&mut self) -> Result<(), Self::Error> {
189        // Dropping the `port` will automatically close the serial connection.
190        self.port = None;
191        Ok(())
192    }
193
194    /// Sends a Modbus Application Data Unit (ADU) over the serial port.
195    ///
196    /// # Arguments
197    /// * `adu` - The byte slice representing the ADU to send.
198    ///
199    /// # Returns
200    /// `Ok(())` if the ADU is successfully sent, or an error otherwise.
201    fn send(&mut self, adu: &[u8]) -> Result<(), Self::Error> {
202        let port = self.port.as_mut().ok_or(TransportError::ConnectionClosed)?;
203
204        // Before sending a new request, it's crucial to clear any data
205        // that may have been left in the buffers from a previous, possibly incomplete,
206        // transaction. This prevents stale data from being misinterpreted as a response
207        // to the new request.
208        if let Err(e) = port.clear(ClearBuffer::All) {
209            serial_log_warn!("Failed to clear serial buffers before send: {}", e);
210            // This is often not a fatal error, so we log it and continue.
211        }
212
213        port.write_all(adu).map_err(|e| {
214            serial_log_error!("Serial write_all failed: {}", e);
215            Self::map_io_error(e)
216        })?;
217
218        match port.flush() {
219            Ok(_) => Ok(()),
220            Err(e) => {
221                // On Windows, some drivers (e.g. some USB-to-Serial) return "Incorrect function" (OS error 1)
222                // when FlushFileBuffers is called. Since write_all succeeded, we can often ignore this.
223                #[cfg(windows)]
224                if let Some(1) = e.raw_os_error() {
225                    // Ignoring this specific error is a workaround for buggy drivers.
226                    return Ok(());
227                }
228                serial_log_error!("Serial flush failed: {}", e);
229                Err(Self::map_io_error(e))
230            }
231        }
232    }
233
234    /// Receives a Modbus Application Data Unit (ADU) from the serial port.
235    ///
236    /// This implementation is non-blocking: it checks the serial port's input buffer
237    /// and reads only the bytes currently available. If no bytes are available,
238    /// it returns `TransportError::Timeout`.
239    ///
240    /// # Returns
241    /// `Ok(Vec<u8, MAX_ADU_FRAME_LEN>)` containing the received ADU, or an error otherwise.
242    fn recv(&mut self) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, Self::Error> {
243        let port = self.port.as_mut().ok_or(TransportError::ConnectionClosed)?;
244
245        // Check how many bytes are available in the RX buffer to ensure non-blocking behavior.
246        let bytes_to_read = port.bytes_to_read().map_err(|e| {
247            serial_log_error!("Failed to check available bytes: {}", e);
248            TransportError::IoError
249        })?;
250
251        let mut buffer = Vec::new();
252
253        if bytes_to_read == 0 {
254            return Err(TransportError::Timeout);
255        }
256
257        // Limit the read to the capacity of our heapless::Vec.
258        let limit = std::cmp::min(bytes_to_read as usize, buffer.capacity());
259
260        // Create a temporary slice to read into.
261        let mut temp_buf = [0u8; MAX_ADU_FRAME_LEN];
262        let read_count = port.read(&mut temp_buf[..limit]).map_err(|e| {
263            if e.kind() == io::ErrorKind::WouldBlock {
264                return TransportError::Timeout;
265            }
266            Self::map_io_error(e)
267        })?;
268
269        if read_count == 0 {
270            return Err(TransportError::Timeout);
271        }
272
273        // Extend the heapless Vec with the bytes actually read.
274        if buffer.extend_from_slice(&temp_buf[..read_count]).is_err() {
275            return Err(TransportError::IoError); // Should not happen given the limit check.
276        }
277
278        Ok(buffer)
279    }
280
281    /// Checks if the transport is currently connected to a remote host.
282    fn is_connected(&self) -> bool {
283        self.port.is_some()
284    }
285
286    /// Returns the type of transport.
287    fn transport_type(&self) -> TransportType {
288        let mode = self.mode; // SerialMode implements Copy, so no need to clone
289        TransportType::StdSerial(mode)
290    }
291}