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/awaitvia 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
[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(); }Use default-features = false and opt into only the features you need.
[dependencies]
modbus-rs = { version = "0.9.0", default-features = false, features = ["client", "network-tcp", "coils"] }[dependencies]
modbus-rs = { version = "0.9.0", default-features = false, features = ["client", "coils", "registers"] }[dependencies]
mbus-core = { version = "0.9.0", default-features = false, features = ["coils", "registers"] }| 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 |
| 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 |
- Use
mbus-client-asyncfor async client-only dependencies. - Use
mbus-server-asyncfor async server-only dependencies. mbus-asyncprovides a combined async client+server crate and is still supported, but prefermbus-client-asyncandmbus-server-asyncfor new projects βmbus-asyncwill be consolidated into those two focused crates in a future release.
| 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.
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(); }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#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.
Build the WASM package and serve locally:
cd mbus-ffi
wasm-pack build --target web --features wasm,full
python3 -m http.server 8089Run canonical browser E2E tests:
bash mbus-ffi/scripts/run_wasm_browser_tests.shOpen 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.
# 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/ModbusRsClientExampleusing 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,fullfirst β the native library is not committed to the repository. Visual Studio automatically copiesmbus_ffi.dllfromtarget\debug\to the output folder on every build (see .NET binding documentation).
π Full .NET Binding Documentation β
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 β
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 buildimport { 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 returnIllegalFunctionand writes are echoed. Full dispatch is planned for v0.9. The client API is fully functional.
π Full Node.js Binding Documentation β
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 β
# 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 β
See CONTRIBUTING.md for development setup and contribution workflow.
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