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