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)*);
}};
}
#[derive(Debug)]
pub struct StdSerialTransport<const ASCII: bool = false> {
port: Option<Box<dyn SerialPort>>,
timeout: Duration,
baud_rate: u32,
}
pub type StdRtuTransport = StdSerialTransport<false>;
pub type StdAsciiTransport = StdSerialTransport<true>;
impl<const ASCII: bool> Default for StdSerialTransport<ASCII> {
fn default() -> Self {
Self::new()
}
}
impl<const ASCII: bool> StdSerialTransport<ASCII> {
const MODE: SerialMode = if ASCII {
SerialMode::Ascii
} else {
SerialMode::Rtu
};
pub fn new() -> Self {
Self {
port: None,
timeout: Duration::from_secs(1), baud_rate: 9600, }
}
pub fn available_ports() -> Result<std::vec::Vec<serialport::SerialPortInfo>, serialport::Error>
{
serialport::available_ports()
}
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<const ASCII: bool> Transport for StdSerialTransport<ASCII> {
type Error = TransportError;
const SUPPORTS_BROADCAST_WRITES: bool = true;
const TRANSPORT_TYPE: TransportType = TransportType::StdSerial(Self::MODE);
fn connect(&mut self, config: &ModbusConfig) -> Result<(), Self::Error> {
let serial_config = match config {
ModbusConfig::Serial(c) => c,
_ => return Err(TransportError::InvalidConfiguration),
};
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,
};
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);
let builder = serialport::new(serial_config.port_path.as_str(), self.baud_rate)
.parity(parity)
.data_bits(data_bits)
.stop_bits(stop_bits) .flow_control(FlowControl::None)
.timeout(self.timeout);
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
);
#[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)
}
}
}
fn disconnect(&mut self) -> Result<(), Self::Error> {
self.port = None;
Ok(())
}
fn send(&mut self, adu: &[u8]) -> Result<(), Self::Error> {
let port = self.port.as_mut().ok_or(TransportError::ConnectionClosed)?;
if let Err(e) = port.clear(ClearBuffer::All) {
serial_log_warn!("Failed to clear serial buffers before send: {}", e);
}
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) => {
#[cfg(windows)]
if let Some(1) = e.raw_os_error() {
return Ok(());
}
serial_log_error!("Serial flush failed: {}", e);
Err(Self::map_io_error(e))
}
}
}
fn recv(&mut self) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, Self::Error> {
let port = self.port.as_mut().ok_or(TransportError::ConnectionClosed)?;
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);
}
let limit = std::cmp::min(bytes_to_read as usize, buffer.capacity());
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);
}
if buffer.extend_from_slice(&temp_buf[..read_count]).is_err() {
return Err(TransportError::IoError); }
Ok(buffer)
}
fn is_connected(&self) -> bool {
self.port.is_some()
}
}