1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
//! Portable `no_std` Modbus RTU/TCP bridge — async and blocking.
//!
//! Accepts Modbus TCP connections and transparently forwards each request to a
//! Modbus RTU device over a serial port, then returns the response to the TCP
//! client. No heap allocation is required: all internal buffers use
//! fixed-capacity [`heapless`] collections.
//!
//! # When to use this crate
//!
//! Use this crate when you need to:
//!
//! - Bridge legacy RS-485/Modbus RTU sensors or PLCs onto a Wi-Fi or Ethernet
//! network.
//! - Act as a Modbus TCP gateway (port 502) for a home-automation hub, SCADA
//! system, or any Modbus TCP client.
//! - Run on a microcontroller such as an ESP32, STM32, or RP2040 without an
//! operating system.
//!
//! # Adding to your project
//!
//! ```toml
//! [dependencies]
//! # Async — Embassy, smoltcp, and other async runtimes (enabled by default)
//! modbus-bridge = { version = "0.2", features = ["async", "defmt"] }
//!
//! # Blocking — esp-idf-hal, FreeRTOS tasks, bare-metal loops
//! modbus-bridge = { version = "0.2", default-features = false, features = ["sync", "log"] }
//! ```
//!
//! `async` and `sync` are mutually exclusive — enable exactly one.
//!
//! # Quick start — Embassy + embassy-net
//!
//! This example shows a complete Modbus TCP gateway task for any Embassy target
//! (ESP32, STM32, RP2040, …). The UART and TCP socket are represented by the
//! `embedded_io_async` traits, so the code is portable across HALs.
//!
//! ```rust,ignore
//! use modbus_bridge::{Bridge, BridgeError, BridgeEvent};
//!
//! #[embassy_executor::task]
//! async fn modbus_gateway(
//! stack: embassy_net::Stack<'static>,
//! // Any UART implementing embedded_io_async, e.g. from esp-hal or embassy-stm32.
//! uart: impl embedded_io_async::Read + embedded_io_async::Write + 'static,
//! // RS-485 direction-control pin. Pass `modbus_bridge::NoPin` if not needed.
//! tx_en: impl embedded_hal::digital::OutputPin + 'static,
//! ) {
//! let mut bridge = Bridge::builder()
//! .rtu(uart, tx_en)
//! .build();
//!
//! // Allocate the TCP socket using the exported buffer-size constants.
//! let mut rx_buf = [0u8; modbus_bridge::TCP_SOCKET_RX_BUF];
//! let mut tx_buf = [0u8; modbus_bridge::TCP_SOCKET_TX_BUF];
//! let mut socket = embassy_net::tcp::TcpSocket::new(stack, &mut rx_buf, &mut tx_buf);
//!
//! loop {
//! // Wait for a Modbus TCP client to connect on the standard port 502.
//! if socket.accept(502).await.is_err() {
//! socket.abort();
//! continue;
//! }
//!
//! // `accept` borrows `bridge` for the lifetime of the connection and
//! // takes ownership of the socket.
//! let mut conn = bridge.accept(socket);
//!
//! loop {
//! match conn.next().await {
//! // A complete request/response cycle finished successfully.
//! Ok(BridgeEvent::Transaction(t)) => defmt::info!("modbus: {}", t),
//! // Non-fatal anomaly (e.g. transaction ID mismatch) — still running.
//! Ok(BridgeEvent::Warning(w)) => defmt::warn!("modbus: {}", w),
//! // TCP client disconnected cleanly — break and accept next client.
//! Err(BridgeError::TcpClosed) => break,
//! // Hard error — log it and terminate the connection.
//! Err(e) => {
//! defmt::error!("modbus error: {}", e);
//! break;
//! }
//! }
//! }
//!
//! // Recover the socket so it can accept the next client.
//! socket = conn.into_stream();
//! socket.close();
//! }
//! }
//! ```
//!
//! ## Hardware without an RS-485 TX-enable pin
//!
//! Many USB-to-RS-485 adapters and UART peripherals with automatic direction
//! control do not need an explicit TX-enable signal. Use
//! [`BridgeBuilder::rtu_no_pin`] as a shorthand, or pass [`NoPin`] explicitly:
//!
//! ```rust,ignore
//! // Shorthand
//! let mut bridge = Bridge::builder().rtu_no_pin(uart).build();
//!
//! // Equivalent explicit form
//! let mut bridge = Bridge::builder().rtu(uart, modbus_bridge::NoPin).build();
//! ```
//!
//! # Blocking (sync) usage
//!
//! Compile with `default-features = false, features = ["sync"]`. The API is
//! identical: every `.next().await` becomes `.next()` and there is no executor
//! or async runtime required.
//!
//! ```rust,ignore
//! use modbus_bridge::{Bridge, BridgeError, BridgeEvent};
//!
//! let mut bridge = Bridge::builder().rtu(uart, tx_en).build();
//!
//! loop {
//! // Accept a connection from your blocking TCP stack.
//! let stream = tcp_listener.accept().unwrap();
//! let mut conn = bridge.accept(stream);
//!
//! loop {
//! match conn.next() {
//! Ok(BridgeEvent::Transaction(t)) => log::info!("modbus: {t}"),
//! Ok(BridgeEvent::Warning(w)) => log::warn!("modbus: {w}"),
//! Err(BridgeError::TcpClosed) => break,
//! Err(e) => { log::error!("modbus error: {e}"); break; }
//! }
//! }
//! }
//! ```
//!
//! # Feature flags
//!
//! | Feature | Default | Description |
//! |---------|---------|-------------|
//! | `async` | yes | Async transport via [`embedded_io_async`]. Mutually exclusive with `sync`. |
//! | `sync` | no | Blocking transport via [`embedded_io`]. Mutually exclusive with `async`. |
//! | `defmt` | no | Structured logging via [`defmt`] over RTT. Recommended for bare-metal targets. |
//! | `log` | no | Logging via the [`log`] facade. Suitable for Linux, esp-idf, and RTOS targets. |
//!
//! # Logging
//!
//! Enable `defmt` (bare-metal / probe-rs RTT) or `log` (standard logger) to
//! receive `info`-level messages for each RTU and TCP frame, and `error`-level
//! messages on I/O failures. Without either feature the crate produces no
//! output at all.
//!
//! # TCP socket buffer sizing
//!
//! When allocating a TCP socket for `embassy-net` or `smoltcp`, pass
//! [`TCP_SOCKET_RX_BUF`] and [`TCP_SOCKET_TX_BUF`] (512 bytes each) as the
//! socket's internal buffer sizes. They are sized to hold one maximum-length
//! Modbus TCP frame (261 bytes) with headroom for TCP ACK latency and a
//! pipelined follow-on request.
//!
//! For computing Modbus *frame* sizes at compile time, see the [`capacity`]
//! module.
// ── Feature guards ────────────────────────────────────────────────────────────
compile_error!;
compile_error!;
// ── Private modules ───────────────────────────────────────────────────────────
// ── Public modules ────────────────────────────────────────────────────────────
// ── Top-level re-exports ──────────────────────────────────────────────────────
pub use Bridge;
pub use BridgeBuilder;
pub use Client;
pub use ClientBuilder;
pub use ClientSession;
pub use Connection;
pub use ;
// ── No-op TX-enable pin ───────────────────────────────────────────────────────
/// No-op TX-enable pin for hardware that does not need RS-485 direction control.
///
/// Pass this to [`BridgeBuilder::rtu`] when your RS-485 transceiver handles bus
/// direction automatically (e.g. auto-direction-control adapters, full-duplex
/// wiring, or RS-232 connections). [`BridgeBuilder::rtu_no_pin`] is a
/// convenience shorthand that inserts `NoPin` for you.
///
/// # Examples
///
/// ```rust,ignore
/// use modbus_bridge::{Bridge, NoPin};
///
/// let mut bridge = Bridge::builder()
/// .rtu(uart, NoPin)
/// .build();
/// ```
;
// ── No-op delay provider ──────────────────────────────────────────────────────
/// No-op delay provider — the default when no timeout is configured.
///
/// Pass `NoDelay` (or omit the third generic) when you do not need RTU or TCP
/// timeouts. `NoDelay` does **not** implement any delay trait; this is
/// intentional — it enables disjoint `impl` blocks in `Connection` and
/// `ClientSession` without requiring language specialization.
///
/// To enable timeouts, call `.delay(my_delay)` on the builder and set
/// `.rtu_timeout(ms)` and/or `.tcp_timeout(ms)`.
;
// ── TCP socket buffer sizing constants ───────────────────────────────────────
/// Recommended receive-buffer size for the underlying TCP socket (512 bytes).
///
/// Sized to hold one maximum-length Modbus TCP frame (261 bytes: 255-byte RTU
/// PDU + 6-byte MBAP header), rounded to the next power of two with headroom
/// for TCP ACK latency and a pipelined follow-on request.
///
/// Pass this constant when constructing your `TcpSocket` in `embassy-net` or
/// `smoltcp`:
///
/// ```rust,ignore
/// use modbus_bridge::{TCP_SOCKET_RX_BUF, TCP_SOCKET_TX_BUF};
///
/// let mut rx_buf = [0u8; TCP_SOCKET_RX_BUF];
/// let mut tx_buf = [0u8; TCP_SOCKET_TX_BUF];
/// let socket = embassy_net::tcp::TcpSocket::new(stack, &mut rx_buf, &mut tx_buf);
/// ```
pub const TCP_SOCKET_RX_BUF: usize = 512;
/// Recommended transmit-buffer size for the underlying TCP socket (512 bytes).
///
/// See [`TCP_SOCKET_RX_BUF`] for sizing rationale and usage.
pub const TCP_SOCKET_TX_BUF: usize = 512;
// ── Internal logging ──────────────────────────────────────────────────────────
pub use mb_error;
pub use mb_info;
// ── Fuzzing surface (hidden from public docs) ─────────────────────────────────
/// Internal module exposing frame primitives for fuzz targets.
///
/// Not part of the public API — stability not guaranteed.