mbus_serial/management/
std_serial.rs1use 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#[derive(Debug)]
48pub struct StdSerialTransport<const ASCII: bool = false> {
49 port: Option<Box<dyn SerialPort>>,
50 timeout: Duration,
52 baud_rate: u32,
54}
55
56pub type StdRtuTransport = StdSerialTransport<false>;
58pub 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 const MODE: SerialMode = if ASCII {
70 SerialMode::Ascii
71 } else {
72 SerialMode::Rtu
73 };
74
75 pub fn new() -> Self {
77 Self {
78 port: None,
79 timeout: Duration::from_secs(1), baud_rate: 9600, }
82 }
83
84 pub fn available_ports() -> Result<std::vec::Vec<serialport::SerialPortInfo>, serialport::Error>
87 {
88 serialport::available_ports()
89 }
90
91 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 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 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 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 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) .flow_control(FlowControl::None)
163 .timeout(self.timeout);
164
165 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 #[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 fn disconnect(&mut self) -> Result<(), Self::Error> {
212 self.port = None;
214 Ok(())
215 }
216
217 fn send(&mut self, adu: &[u8]) -> Result<(), Self::Error> {
225 let port = self.port.as_mut().ok_or(TransportError::ConnectionClosed)?;
226
227 if let Err(e) = port.clear(ClearBuffer::All) {
232 serial_log_warn!("Failed to clear serial buffers before send: {}", e);
233 }
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 #[cfg(windows)]
247 if let Some(1) = e.raw_os_error() {
248 return Ok(());
250 }
251 serial_log_error!("Serial flush failed: {}", e);
252 Err(Self::map_io_error(e))
253 }
254 }
255 }
256
257 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 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 let limit = std::cmp::min(bytes_to_read as usize, buffer.capacity());
282
283 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 if buffer.extend_from_slice(&temp_buf[..read_count]).is_err() {
298 return Err(TransportError::IoError); }
300
301 Ok(buffer)
302 }
303
304 fn is_connected(&self) -> bool {
306 self.port.is_some()
307 }
308}