Skip to content

Raghava-Ch/modbus-rs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

193 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

modbus-rs

A cross-platform, low-footprint Modbus client and server library for Rust.

  • no_std compatible β€” runs on embedded MCUs and standard OS targets
  • All transports β€” TCP, Serial RTU, Serial ASCII
  • Sync and async β€” poll-driven sync core; native async/await via Tokio
  • Feature-gated β€” enable only what you need for minimal binary size
  • Multi-language bindings β€” native C/C++, .NET (C#), Python, Go, and Node.js integration via mbus-ffi
  • Gateway β€” Modbus TCP ↔ RTU/ASCII gateway with sync (no_std) and async modes

Quick Start

[dependencies]
modbus-rs = "0.9.0"
use modbus_rs::{ClientServices, ModbusConfig, ModbusTcpConfig, StdTcpTransport};

let config = ModbusConfig::Tcp(ModbusTcpConfig::new("192.168.1.10", 502)?);
let mut client = ClientServices::<_, _, 4>::new(StdTcpTransport::new(), app, config)?;
client.connect()?;
client.coils().read_coils(1, unit_id, 0, 16)?;
loop { client.poll(); }

Minimal Install Profiles

Use default-features = false and opt into only the features you need.

Minimal TCP Client

[dependencies]
modbus-rs = { version = "0.9.0", default-features = false, features = ["client", "network-tcp", "coils"] }

Minimal Embedded / no_std Client

[dependencies]
modbus-rs = { version = "0.9.0", default-features = false, features = ["client", "coils", "registers"] }

Core-only (protocol + models)

[dependencies]
mbus-core = { version = "0.9.0", default-features = false, features = ["coils", "registers"] }

πŸ“– Full Documentation β†’


Documentation

Section Quick Links
Client Quick Start Β· Examples Β· Building Apps Β· Sync Β· Async Β· Policies
Server Quick Start Β· Examples Β· Building Apps Β· Sync Β· Async Β· Macros Β· Write Hooks Β· Function Codes
Gateway Quick Start Β· Architecture Β· Routing Β· WebSocket Gateway Β· Feature Flags
Bindings C/FFI Β· WASM Β· Python Β· .NET / C# Β· Go Β· Node.js
Reference Client Feature Flags Β· Server Feature Flags Β· Migration Guide

Workspace Crates

Crate Purpose
modbus-rs Top-level convenience crate β€” start here
mbus-client Client state machine and request services
mbus-server Server runtime with derive macros
mbus-client-async Role-focused async client facade crate
mbus-server-async Role-focused async server facade crate
mbus-core Shared protocol types and transport trait
mbus-async Combined async client+server crate β€” prefer mbus-client-async / mbus-server-async for new projects
mbus-macros Proc macros: #[modbus_app], #[derive(CoilsModel)], etc.
mbus-network TCP transport implementation
mbus-serial Serial RTU/ASCII transport implementation
mbus-gateway Modbus gateway β€” TCP ↔ RTU/ASCII routing (sync + async)
mbus-ffi Native C, WASM, Python, .NET (C#), Go, and Node.js bindings β€” client, server, and gateway

Direct Async Crate Selection

  • Use mbus-client-async for async client-only dependencies.
  • Use mbus-server-async for async server-only dependencies.
  • mbus-async provides a combined async client+server crate and is still supported, but prefer mbus-client-async and mbus-server-async for new projects β€” mbus-async will be consolidated into those two focused crates in a future release.

Feature Flags

Flag Description
client Client state machine (default)
server Server runtime and macros
network-tcp Modbus TCP transport (default)
serial-rtu Serial RTU transport (default)
serial-ascii Serial ASCII transport
async Native async runtime via Tokio for client and server APIs (default)
coils FC01, FC05, FC0F (default)
registers FC03, FC04, FC06, FC10 (default)
discrete-inputs FC02 (default)
fifo FC18 FIFO queue read (default)
file-record FC14, FC15 file record read/write (default)
diagnostics FC07, FC08, FC2B, etc. (default)
gateway Modbus gateway (TCP ↔ RTU/ASCII, sync + async)
diagnostics-stats Per-counter diagnostics statistics
traffic Raw TX/RX frame callbacks
logging log facade integration

See Feature Flags Reference for complete details.


Examples

TCP Client (sync)

use modbus_rs::{ClientServices, ModbusConfig, ModbusTcpConfig, StdTcpTransport};

let config = ModbusConfig::Tcp(ModbusTcpConfig::new("192.168.1.10", 502)?);
let mut client = ClientServices::<_, _, 4>::new(StdTcpTransport::new(), app, config)?;
client.connect()?;
client.coils().read_coils(1, unit_id, 0, 16)?;
loop { client.poll(); }

Async TCP Client

use modbus_rs::mbus_async::AsyncTcpClient;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = AsyncTcpClient::new("192.168.1.10", 502)?;
    client.connect().await?;

    let coils = client.read_multiple_coils(1, 0, 8).await?;
    for addr in coils.from_address()..coils.from_address() + coils.quantity() {
        println!("coil[{}] = {}", addr, coils.value(addr)?);
    }

    let holding = client.read_holding_registers(1, 0, 4).await?;
    for addr in holding.from_address()..holding.from_address() + holding.quantity() {
        println!("reg[{}] = {}", addr, holding.value(addr)?);
    }

    client.write_single_coil(1, 0, true).await?;
    Ok(())
}
cargo run -p modbus-rs --example modbus_rs_client_async_tcp --no-default-features --features async,client,network-tcp,coils,registers,discrete-inputs

C Client (via mbus-ffi)

#include "modbus_rs_client.h"

/* Required locking hooks β€” provide real mutexes in production */
void mbus_pool_lock(void)        { /* pthread_mutex_lock(&g_pool_mutex); */ }
void mbus_pool_unlock(void)      { /* pthread_mutex_unlock(&g_pool_mutex); */ }
void mbus_client_lock(MbusClientId id)   { (void)id; }
void mbus_client_unlock(MbusClientId id) { (void)id; }

/* Transport callbacks β€” wire these to your socket/UART layer */
static MbusStatusCode on_connect(void *ud)    { return tcp_open(ud);  }
static MbusStatusCode on_disconnect(void *ud) { return tcp_close(ud); }
static MbusStatusCode on_send(const uint8_t *buf, uint16_t len, void *ud)
    { return tcp_write(ud, buf, len); }
static MbusStatusCode on_recv(uint8_t *buf, uint16_t cap, uint16_t *out, void *ud)
    { return tcp_read(ud, buf, cap, out); }
static uint8_t on_is_connected(void *ud) { return tcp_is_open(ud); }

/* Response callback */
static void on_read_coils(const MbusReadCoilsCtx *ctx) {
    for (uint16_t i = 0; i < mbus_coils_quantity(ctx->coils); i++) {
        bool val; mbus_coils_value_at_index(ctx->coils, i, &val);
        printf("coil[%u] = %d\n", i, val);
    }
}

int main(void) {
    struct MyTcpCtx io = { .fd = -1, .host = "192.168.1.10", .port = 502 };

    MbusTransportCallbacks transport = {
        .userdata = &io, .on_connect = on_connect, .on_disconnect = on_disconnect,
        .on_send = on_send, .on_recv = on_recv, .on_is_connected = on_is_connected,
    };
    MbusTcpConfig cfg = { .host = "192.168.1.10", .port = 502,
                          .response_timeout_ms = 2000, .retries = 1 };
    MbusCallbacks app = { .on_read_coils = on_read_coils };

    MbusClientId id = mbus_tcp_client_new(&cfg, &transport, &app);
    mbus_tcp_connect(id);
    mbus_tcp_read_coils(id, /*unit*/1, /*txn*/42, /*addr*/0, /*qty*/10);

    while (mbus_tcp_has_pending_requests(id))
        mbus_tcp_poll(id);

    mbus_tcp_disconnect(id);
    mbus_tcp_client_free(id);
}

See mbus-ffi/ for the full C binding reference, build instructions, and server demo.

WASM Browser Bindings (via mbus-ffi)

Build the WASM package and serve locally:

cd mbus-ffi
wasm-pack build --target web --features wasm,full
python3 -m http.server 8089

Run canonical browser E2E tests:

bash mbus-ffi/scripts/run_wasm_browser_tests.sh

Open the runnable smoke examples in a Chromium-based browser:

Example What it exercises
examples/network_smoke.html WebSocket client (TCP proxy)
examples/serial_smoke.html Web Serial client (RTU/ASCII)
examples/network_server_smoke.html WASM TCP server lifecycle + dispatch
examples/serial_server_smoke.html WASM Serial server lifecycle + dispatch

See mbus-ffi/README.md for the full WASM API reference and server binding architecture.

.NET / C# Client (via mbus-ffi)

# 1. Build the native library (Debug configuration β€” do this once per Rust change)
cargo build -p mbus-ffi --features dotnet,full

# 2. Open the solution in Visual Studio 2022 and press F5, or run from the CLI:
dotnet run --project mbus-ffi/dotnet/examples/ModbusRsClientExample
using ModbusRs;

using var client = new ModbusTcpClient("192.168.1.10", 502);
client.SetRequestTimeout(TimeSpan.FromSeconds(2));
await client.ConnectAsync();

ushort[] regs = await client.ReadHoldingRegistersAsync(unitId: 1, address: 0, quantity: 4);
await client.WriteSingleRegisterAsync(unitId: 1, address: 5, value: 0xBEEF);
bool[] coils  = await client.ReadCoilsAsync(unitId: 1, address: 0, quantity: 8);

await client.DisconnectAsync();

DllNotFoundException? Run cargo build -p mbus-ffi --features dotnet,full first β€” the native library is not committed to the repository. Visual Studio automatically copies mbus_ffi.dll from target\debug\ to the output folder on every build (see .NET binding documentation).

πŸ“– Full .NET Binding Documentation β†’

Go bindings

Idiomatic Go async client / server / gateway, built on cgo over the same async Rust crates as the .NET binding.

# 1. Build the native static library + header for your host platform
./mbus-ffi/go/scripts/build_native.sh
# 2. Use the module
cd mbus-ffi/go && go test ./...
import "github.com/Raghava-Ch/modbus-rs/mbus-ffi/go/client/tcp"

c, _ := tcp.NewClient("127.0.0.1", 1502, tcp.WithTimeout(2*time.Second))
defer c.Close()
_ = c.Connect(ctx)
regs, _ := c.ReadHoldingRegisters(ctx, 1, 0, 4)

πŸ“– Full Go Binding Documentation β†’

Node.js bindings (via mbus-ffi)

Idiomatic Promise-based JavaScript and TypeScript API, built on napi-rs over the same async Rust crates as the .NET and Go bindings. Prebuilt binaries mean end users do not need a Rust toolchain.

# Install from npm (prebuilt binary downloaded automatically):
npm install modbus-rs

# Or build locally:
cd mbus-ffi/nodejs && npm install && npm run build
import { AsyncTcpModbusClient } from 'modbus-rs';

const client = await AsyncTcpModbusClient.connect({
  host: '192.168.1.10',
  port: 502,
  unitId: 1,
  timeoutMs: 2000,
});

const regs = await client.readHoldingRegisters({ address: 0, quantity: 4 });
await client.writeMultipleRegisters({ address: 10, values: [1, 2, 3, 4] });
await client.close();

Server handler dispatch note (v0.8): AsyncTcpModbusServer.bind() accepts a handlers object but the JS callback invocation is not yet wired up β€” reads return IllegalFunction and writes are echoed. Full dispatch is planned for v0.9. The client API is fully functional.

πŸ“– Full Node.js Binding Documentation β†’

Gateway (sync TCP β†’ RTU)

use modbus_rs::{gateway::GatewayServices, gateway::UnitRouteTable, gateway::NoopEventHandler,
                gateway::DownstreamChannel};

fn run_gateway(upstream_transport: impl std::any::Any, downstream_rtu_transport: impl std::any::Any) {
    // Route unit IDs 1–10 to channel 0 (RTU downstream)
    let mut routes = UnitRouteTable::new();
    routes.add(1, 10, 0).unwrap();

    let mut gw = GatewayServices::<_, _, 1, 8>::new(
        upstream_transport, [(downstream_rtu_transport, 0)],
        routes, NoopEventHandler,
    );

    loop { gw.poll(); }
}
# Sync TCP β†’ RTU gateway
cargo run -p modbus-rs --example modbus_rs_gateway_sync_tcp_to_rtu \
  --no-default-features --features gateway,network-tcp,serial-rtu

# Async TCP β†’ TCP gateway
cargo run -p modbus-rs --example modbus_rs_gateway_async_tcp_to_tcp \
  --no-default-features --features gateway,async,network-tcp

πŸ“– Gateway Documentation β†’


Run examples

# Sync TCP client
cargo run -p modbus-rs --example modbus_rs_client_tcp_coils --no-default-features --features client,network-tcp,coils -- 192.168.1.10 502 1

# Serial RTU client
cargo run -p modbus-rs --example modbus_rs_client_serial_rtu_coils --no-default-features --features client,serial-rtu,coils -- /dev/ttyUSB0 1

# TCP server
cargo run -p modbus-rs --example modbus_rs_server_tcp_demo --features server,network-tcp,coils,holding-registers,input-registers

πŸ“– All Examples β†’


Contributing

See CONTRIBUTING.md for development setup and contribution workflow.

License

This project is licensed under the GNU General Public License v3.0 (GPLv3) β€” see LICENSE.

This crate is licensed under GPLv3. Commercial licenses are also available for proprietary use; contact ch.raghava44@gmail.com.


Repository: github.com/Raghava-Ch/modbus-rs

About

Low-footprint, feature-gated Modbus stack for MCU to enterprise: TCP/RTU/ASCII, sync+async, no_std/std, with C and WASM bindings.

Resources

License

Contributing

Stars

Watchers

Forks

Sponsor this project

 

Packages

 
 
 

Contributors