Skip to main content

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}