Skip to main content

Crate modbus_bridge

Crate modbus_bridge 

Source
Expand description

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

[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.

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:

// 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.

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

FeatureDefaultDescription
asyncyesAsync transport via embedded_io_async. Mutually exclusive with sync.
syncnoBlocking transport via [embedded_io]. Mutually exclusive with async.
defmtnoStructured logging via [defmt] over RTT. Recommended for bare-metal targets.
lognoLogging 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.

Re-exports§

pub use bridge::Bridge;
pub use builder::BridgeBuilder;
pub use client::Client;
pub use client_builder::ClientBuilder;
pub use client_session::ClientSession;
pub use connection::Connection;
pub use event::BridgeError;
pub use event::BridgeEvent;
pub use event::FunctionCode;
pub use event::Transaction;
pub use event::Warning;

Modules§

bridge
The Bridge type — owns the RTU serial port and creates Connections.
builder
Typestate builder for Bridge.
capacity
Frame buffer sizing constants and helpers for Modbus RTU and TCP frames.
client
Client — owns the RTU serial port and creates ClientSessions.
client_builder
Typestate builder for Client.
client_session
ClientSession — a live TCP session bound to a Client.
connection
Connection — a live TCP session bound to a Bridge.
event
Public event and error types for the Bridge API.

Structs§

NoDelay
No-op delay provider — the default when no timeout is configured.
NoPin
No-op TX-enable pin for hardware that does not need RS-485 direction control.

Constants§

TCP_SOCKET_RX_BUF
Recommended receive-buffer size for the underlying TCP socket (512 bytes).
TCP_SOCKET_TX_BUF
Recommended transmit-buffer size for the underlying TCP socket (512 bytes).