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