modbus_bridge/event.rs
1//! Public event and error types for the [`Bridge`](crate::Bridge) API.
2
3use core::fmt;
4
5// ── Function code ─────────────────────────────────────────────────────────────
6
7/// Modbus function code extracted from a request frame.
8///
9/// Covers the most common read/write operations. Unknown function codes are
10/// wrapped in [`Other`](FunctionCode::Other) and forwarded to the RTU device
11/// without modification, so vendor-specific extensions work transparently.
12///
13/// # Examples
14///
15/// ```rust,ignore
16/// if let BridgeEvent::Transaction(t) = event {
17/// match t.function_code {
18/// FunctionCode::ReadHoldingRegisters => { /* … */ }
19/// FunctionCode::WriteMultipleRegisters => { /* … */ }
20/// other => defmt::warn!("unexpected FC: {}", other),
21/// }
22/// }
23/// ```
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[cfg_attr(feature = "defmt", derive(defmt::Format))]
26pub enum FunctionCode {
27 /// FC 0x01 — reads one or more output coils (digital outputs).
28 ReadCoils,
29 /// FC 0x02 — reads one or more discrete inputs (digital inputs).
30 ReadDiscreteInputs,
31 /// FC 0x03 — reads one or more holding registers (read/write 16-bit words).
32 ReadHoldingRegisters,
33 /// FC 0x04 — reads one or more input registers (read-only 16-bit words).
34 ReadInputRegisters,
35 /// FC 0x05 — writes a single output coil.
36 WriteSingleCoil,
37 /// FC 0x06 — writes a single holding register.
38 WriteSingleRegister,
39 /// FC 0x0F — writes multiple output coils in a single request.
40 WriteMultipleCoils,
41 /// FC 0x10 — writes multiple holding registers in a single request.
42 WriteMultipleRegisters,
43 /// Any function code not listed above — passed through to the RTU device transparently.
44 Other(u8),
45}
46
47impl From<u8> for FunctionCode {
48 fn from(v: u8) -> Self {
49 match v {
50 0x01 => Self::ReadCoils,
51 0x02 => Self::ReadDiscreteInputs,
52 0x03 => Self::ReadHoldingRegisters,
53 0x04 => Self::ReadInputRegisters,
54 0x05 => Self::WriteSingleCoil,
55 0x06 => Self::WriteSingleRegister,
56 0x0F => Self::WriteMultipleCoils,
57 0x10 => Self::WriteMultipleRegisters,
58 other => Self::Other(other),
59 }
60 }
61}
62
63impl fmt::Display for FunctionCode {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::ReadCoils => write!(f, "ReadCoils"),
67 Self::ReadDiscreteInputs => write!(f, "ReadDiscreteInputs"),
68 Self::ReadHoldingRegisters => write!(f, "ReadHoldingRegisters"),
69 Self::ReadInputRegisters => write!(f, "ReadInputRegisters"),
70 Self::WriteSingleCoil => write!(f, "WriteSingleCoil"),
71 Self::WriteSingleRegister => write!(f, "WriteSingleRegister"),
72 Self::WriteMultipleCoils => write!(f, "WriteMultipleCoils"),
73 Self::WriteMultipleRegisters => write!(f, "WriteMultipleRegisters"),
74 Self::Other(n) => write!(f, "FC({:#04x})", n),
75 }
76 }
77}
78
79// ── Transaction ───────────────────────────────────────────────────────────────
80
81/// A successfully completed Modbus request/response cycle.
82///
83/// Returned inside [`BridgeEvent::Transaction`] after
84/// [`Connection::next`](crate::Connection::next) successfully forwards a request
85/// to the RTU device and relays the response back to the TCP client.
86///
87/// # Examples
88///
89/// ```rust,ignore
90/// if let BridgeEvent::Transaction(t) = conn.next().await? {
91/// defmt::info!(
92/// "unit={} fc={} addr={} count={}",
93/// t.unit_id, t.function_code, t.start_address, t.register_count,
94/// );
95/// }
96/// ```
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98#[cfg_attr(feature = "defmt", derive(defmt::Format))]
99pub struct Transaction {
100 /// Modbus unit (slave) address from the request, in the range 1–247.
101 pub unit_id: u8,
102 /// Function code identifying the type of operation requested.
103 pub function_code: FunctionCode,
104 /// Starting register or coil address (zero-based) from the request.
105 pub start_address: u16,
106 /// Number of registers or coils requested, or the output value for
107 /// single-write function codes (FC 0x05, FC 0x06).
108 pub register_count: u16,
109}
110
111impl fmt::Display for Transaction {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 write!(
114 f,
115 "unit={} fc={} addr={} count={}",
116 self.unit_id, self.function_code, self.start_address, self.register_count
117 )
118 }
119}
120
121// ── Warning ───────────────────────────────────────────────────────────────────
122
123/// Non-fatal protocol anomaly detected during a request/response cycle.
124///
125/// Returned inside [`BridgeEvent::Warning`] by
126/// [`Connection::next`](crate::Connection::next). The connection remains open
127/// after a warning — the response was still forwarded to the TCP client.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129#[cfg_attr(feature = "defmt", derive(defmt::Format))]
130pub enum Warning {
131 /// The transaction ID in the TCP response did not match the one sent in the
132 /// request. The response was forwarded using the server's actual transaction
133 /// ID as a fallback.
134 ///
135 /// This can occur with upstream servers or RTU devices that echo back stale
136 /// or incorrect transaction IDs. It is safe to continue after this warning.
137 TransactionIdMismatch { expected: u16, got: u16 },
138}
139
140impl fmt::Display for Warning {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 match self {
143 Self::TransactionIdMismatch { expected, got } => {
144 write!(f, "transaction ID mismatch: expected {expected}, got {got}")
145 }
146 }
147 }
148}
149
150// ── BridgeEvent ───────────────────────────────────────────────────────────────
151
152/// Successful outcome returned by [`Connection::next`](crate::Connection::next).
153///
154/// Inspect the variant to decide how to log or react:
155///
156/// - [`Transaction`](BridgeEvent::Transaction) — normal operation; one full
157/// request/response cycle completed.
158/// - [`Warning`](BridgeEvent::Warning) — a non-fatal anomaly was detected and
159/// the connection is still running. Log the warning and keep calling `next`.
160///
161/// # Examples
162///
163/// ```rust,ignore
164/// match conn.next().await? {
165/// BridgeEvent::Transaction(t) => defmt::info!("ok: {}", t),
166/// BridgeEvent::Warning(w) => defmt::warn!("warn: {}", w),
167/// }
168/// ```
169#[derive(Debug)]
170#[cfg_attr(feature = "defmt", derive(defmt::Format))]
171pub enum BridgeEvent {
172 /// One complete Modbus request/response cycle completed successfully.
173 Transaction(Transaction),
174 /// A non-fatal protocol anomaly was detected; the connection continues.
175 ///
176 /// See [`Warning`] for details on individual anomaly kinds.
177 Warning(Warning),
178}
179
180impl fmt::Display for BridgeEvent {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 match self {
183 Self::Transaction(t) => fmt::Display::fmt(t, f),
184 Self::Warning(w) => fmt::Display::fmt(w, f),
185 }
186 }
187}
188
189// ── BridgeError ───────────────────────────────────────────────────────────────
190
191/// Hard error returned by [`Connection::next`](crate::Connection::next).
192///
193/// On any `BridgeError` the caller should exit the connection loop, close the
194/// TCP stream, and call [`Bridge::accept`](crate::Bridge::accept) for the next
195/// client:
196///
197/// ```rust,ignore
198/// loop {
199/// match conn.next().await {
200/// Ok(event) => { /* handle */ }
201/// Err(BridgeError::TcpClosed) => break, // normal disconnect
202/// Err(e) => { defmt::error!("{}", e); break; } // hard error
203/// }
204/// }
205/// conn.into_stream().close();
206/// ```
207///
208/// `SE` is the serial-port error type and `TE` is the TCP-stream error type.
209/// Both come from the [`embedded_io_async::ErrorType`] (or [`embedded_io::ErrorType`])
210/// implementations of the serial and TCP types passed to
211/// [`BridgeBuilder`](crate::BridgeBuilder).
212#[derive(Debug)]
213#[cfg_attr(feature = "defmt", derive(defmt::Format))]
214pub enum BridgeError<SE, TE> {
215 /// TCP client closed the connection cleanly (EOF / zero-byte read).
216 ///
217 /// This is the normal exit condition and is not an error in itself. Break
218 /// the connection loop and accept the next client.
219 TcpClosed,
220 /// RTU master closed the serial connection cleanly (EOF / zero-byte read).
221 ///
222 /// Normal exit condition in client mode — equivalent to `TcpClosed` in bridge mode.
223 RtuClosed,
224 /// TCP I/O error from the underlying stream.
225 TcpIo(TE),
226 /// Serial (RTU) I/O error from the underlying serial port.
227 RtuIo(SE),
228 /// RTU device response failed CRC-16 verification.
229 ///
230 /// This usually indicates a wiring problem, an incorrect baud rate, or
231 /// electrical noise on the RS-485 bus.
232 RtuCrcMismatch,
233 /// A Modbus frame was larger than the internal frame buffer can hold.
234 ///
235 /// The internal buffers support the full Modbus specification maximum
236 /// (255-byte RTU / 261-byte TCP). This error indicates a malformed or
237 /// non-Modbus frame.
238 BufferOverflow,
239 /// An RTU or TCP I/O operation did not complete within the configured timeout.
240 Timeout,
241}
242
243impl<SE: fmt::Debug, TE: fmt::Debug> fmt::Display for BridgeError<SE, TE> {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 match self {
246 Self::TcpClosed => write!(f, "TCP connection closed"),
247 Self::RtuClosed => write!(f, "RTU connection closed"),
248 Self::TcpIo(e) => write!(f, "TCP I/O error: {:?}", e),
249 Self::RtuIo(e) => write!(f, "RTU I/O error: {:?}", e),
250 Self::RtuCrcMismatch => write!(f, "RTU response CRC mismatch"),
251 Self::BufferOverflow => write!(f, "frame buffer overflow — increase BUF capacity"),
252 Self::Timeout => write!(f, "I/O timeout"),
253 }
254 }
255}