xy_modbus/transport.rs
1//! Modbus-RTU transport trait and error types.
2//!
3//! Implement [`ModbusTransport`] over your platform's UART. The
4//! [`crate::framing`] module gives you the on-wire codec; a typical
5//! implementation is <100 lines of UART-specific timing on top.
6
7use core::fmt;
8
9// ─── ModbusError ─────────────────────────────────────────────────────────────
10
11/// Protocol-layer error: a frame was received but failed validation.
12#[derive(Copy, Clone, Debug, PartialEq, Eq)]
13#[cfg_attr(feature = "defmt", derive(defmt::Format))]
14pub enum ModbusError {
15 /// Response was shorter than the smallest valid frame for the
16 /// expected reply.
17 ShortResponse(usize),
18 /// Slave address byte didn't match the request.
19 BadSlave(u8),
20 /// Function-code, byte-count, address, or quantity field didn't
21 /// match what was expected.
22 BadHeader,
23 /// CRC-16 mismatch.
24 BadCrc,
25 /// Slave returned a Modbus exception. The byte is the exception
26 /// code (`0x01`–`0x0B` per the spec).
27 Exception(u8),
28}
29
30impl fmt::Display for ModbusError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 Self::ShortResponse(n) => write!(f, "short response ({n} bytes)"),
34 Self::BadSlave(a) => write!(f, "wrong slave id 0x{a:02X}"),
35 Self::BadHeader => write!(f, "malformed header"),
36 Self::BadCrc => write!(f, "CRC mismatch"),
37 Self::Exception(c) => write!(f, "modbus exception 0x{c:02X}"),
38 }
39 }
40}
41
42impl core::error::Error for ModbusError {}
43
44// ─── RtuError ────────────────────────────────────────────────────────────────
45
46/// Unified error returned by the device API: either the transport
47/// (UART layer) failed, or the response was a malformed / exception
48/// Modbus frame.
49#[derive(Copy, Clone, Debug, PartialEq, Eq)]
50#[cfg_attr(feature = "defmt", derive(defmt::Format))]
51pub enum RtuError {
52 /// No (or insufficient) bytes received within the response window.
53 Timeout,
54 /// Underlying UART returned an I/O error on read or write.
55 Io,
56 /// Decoded response was invalid or the slave reported a Modbus
57 /// exception.
58 Modbus(ModbusError),
59}
60
61impl fmt::Display for RtuError {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Self::Timeout => f.write_str("UART response timed out"),
65 Self::Io => f.write_str("UART I/O error"),
66 Self::Modbus(e) => fmt::Display::fmt(e, f),
67 }
68 }
69}
70
71impl From<ModbusError> for RtuError {
72 fn from(e: ModbusError) -> Self {
73 Self::Modbus(e)
74 }
75}
76
77impl core::error::Error for RtuError {
78 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
79 match self {
80 Self::Modbus(e) => Some(e),
81 _ => None,
82 }
83 }
84}
85
86// ─── BlockingRead ────────────────────────────────────────────────────────────
87
88/// Read side of the bundled [`crate::uart::UartTransport`].
89///
90/// Wraps a UART driver that already knows how to block efficiently for
91/// incoming bytes — every kernel-backed HAL exposes one
92/// (`esp_idf_hal::uart::UartDriver::read(buf, ticks)`, embassy with
93/// timeout futures, `serialport-rs::SerialPort::read` after
94/// `set_timeout`, …). Implementing this trait is typically 3 lines that
95/// translate `timeout_ms` into the driver's native timeout type.
96///
97/// Avoiding `embedded_io::ReadReady` here is deliberate: the trait is
98/// optional in the embedded-io ecosystem and many HALs (esp-idf-hal
99/// included) don't impl it, and the busy-poll loop a `ReadReady`-based
100/// implementation needs fights kernel-backed drivers that already block
101/// cheaply.
102pub trait BlockingRead {
103 type Error;
104
105 /// Block for up to `timeout_ms` waiting for at least one byte to
106 /// arrive, then return up to `buf.len()` bytes. `Ok(0)` means the
107 /// timeout elapsed without a byte appearing. `timeout_ms == 0` is a
108 /// non-blocking poll: return whatever is already buffered without
109 /// waiting (used for the pre-TX flush).
110 fn read(&mut self, buf: &mut [u8], timeout_ms: u32) -> Result<usize, Self::Error>;
111}
112
113// ─── Transport trait ─────────────────────────────────────────────────────────
114
115/// Modbus-RTU transport: send a request, validate the response, hand
116/// back the payload (for reads) or just `Ok(())` (for writes).
117///
118/// Implementers handle UART framing timing — the inter-frame gap, the
119/// per-device response timeout, and the post-write quiet gap. The
120/// XY-series wants ~50 ms between frames and ~500 ms response window.
121///
122/// All three function codes are required; the device API uses each
123/// (`0x03` for reads, `0x06` for single setpoint writes, `0x10` for
124/// bulk memory-group writes).
125pub trait ModbusTransport {
126 fn read_holding(&mut self, slave: u8, addr: u16, dst: &mut [u16]) -> Result<(), RtuError>;
127
128 fn write_single_holding(&mut self, slave: u8, addr: u16, value: u16) -> Result<(), RtuError>;
129
130 fn write_multiple_holdings(
131 &mut self,
132 slave: u8,
133 addr: u16,
134 values: &[u16],
135 ) -> Result<(), RtuError>;
136}
137
138#[cfg(test)]
139mod tests {
140 extern crate std;
141 use super::*;
142 use std::format;
143
144 #[test]
145 fn modbus_error_display_strings() {
146 assert_eq!(
147 format!("{}", ModbusError::ShortResponse(3)),
148 "short response (3 bytes)"
149 );
150 assert_eq!(
151 format!("{}", ModbusError::BadSlave(0x02)),
152 "wrong slave id 0x02"
153 );
154 assert_eq!(format!("{}", ModbusError::BadHeader), "malformed header");
155 assert_eq!(format!("{}", ModbusError::BadCrc), "CRC mismatch");
156 assert_eq!(
157 format!("{}", ModbusError::Exception(0x03)),
158 "modbus exception 0x03"
159 );
160 }
161
162 #[test]
163 fn rtu_error_display_strings() {
164 assert_eq!(format!("{}", RtuError::Timeout), "UART response timed out");
165 assert_eq!(format!("{}", RtuError::Io), "UART I/O error");
166 // Modbus variant delegates to inner Display.
167 assert_eq!(
168 format!("{}", RtuError::Modbus(ModbusError::BadCrc)),
169 "CRC mismatch"
170 );
171 }
172
173 /// Modbus-variant `RtuError` exposes the underlying error via `Error::source`.
174 #[test]
175 fn rtu_error_source_chain() {
176 use core::error::Error;
177 let e = RtuError::Modbus(ModbusError::BadCrc);
178 assert!(e.source().is_some());
179 assert!(RtuError::Timeout.source().is_none());
180 assert!(RtuError::Io.source().is_none());
181 }
182}