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
Important
Active Development & Breaking Changes
This project is currently undergoing active development. While the core APIs are highly robust, heavily tested, and mostly stable, they are not yet finalized. You may expect occasional breaking changes as we refine internal structures, align feature gates, and polish macro interfaces.
[dependencies]
modbus-rs = "0.12.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.12.0", default-features = false, features = ["client", "network-tcp", "coils"] }[dependencies]
modbus-rs = { version = "0.12.0", default-features = false, features = ["client", "coils", "registers"] }[dependencies]
mbus-core = { version = "0.12.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