mbus-async
An async facade for the modbus-rs client stack.
mbus-async wraps the existing poll-driven mbus-client state machine in a Tokio-compatible
.await API. You get familiar async/await ergonomics without replacing the battle-tested
synchronous protocol core.
TCP Quick Start
Add to Cargo.toml:
[]
= { = "0.4", = ["async"] }
= { = "1", = ["full"] }
use AsyncTcpClient;
async
Serial Quick Start
[]
= { = "0.4", = false, = [
"async", "serial-rtu", "coils", "registers"
] }
= { = "1", = ["full"] }
use AsyncSerialClient;
use ;
use FromStr;
async
Design
┌─────────────────────────────────────────────────────────────────┐
│ Your async code │
│ client.read_holding_registers(1, 0, 10).await? │
└─────────────────────────┬───────────────────────────────────────┘
│ WorkerCommand (mpsc)
▼
┌─────────────────────────────────────────────────────────────────┐
│ Worker thread (std::thread) │
│ - receives WorkerCommand │
│ - calls ClientServices::<_, _, N> sync API │
│ - polls state machine in a tight loop │
│ - fires oneshot channel when response arrives │
└─────────────────────────┬───────────────────────────────────────┘
│ Tokio oneshot resolved
▼
┌─────────────────────────────────────────────────────────────────┐
│ Your async code resumes with the typed result │
└─────────────────────────────────────────────────────────────────┘
Each call gets a unique transaction id. Multiple concurrent calls can be in flight simultaneously.
TCP uses a compile-time pipeline depth const generic on AsyncTcpClient<const N: usize = 9>.
The default is 9 via AsyncTcpClient::new(...), and you can override it at compile time via
AsyncTcpClient::<N>::new_with_pipeline(...).
Serial remains request/reply oriented and defaults to 1 in-flight request.
Features
| Feature | Default | Enables |
|---|---|---|
tcp |
✓ | AsyncTcpClient via mbus-network |
serial-rtu |
✓ | AsyncSerialClient with RTU framing |
serial-ascii |
✓ | AsyncSerialClient with ASCII framing |
coils |
✓ | Coil read/write methods |
registers |
✓ | Register read/write/mask methods |
discrete-inputs |
✓ | Discrete input read methods |
fifo |
✓ | FIFO queue read methods |
file-record |
✓ | File record read/write methods |
diagnostics |
✓ | Device identification, diagnostics, event log, etc. |
traffic |
Dedicated-thread raw TX/RX frame callback API |
Available Methods
AsyncTcpClient and AsyncSerialClient
Constructors are side-effect free. Build the client first, then call
client.connect().await? before issuing Modbus requests.
Both clients expose an identical async API:
| Method | FC | Feature |
|---|---|---|
read_multiple_coils(unit, address, quantity) |
01 | coils |
write_single_coil(unit, address, value) |
05 | coils |
write_multiple_coils(unit, address, &coils) |
15 | coils |
read_discrete_inputs(unit, address, quantity) |
02 | discrete-inputs |
read_holding_registers(unit, address, quantity) |
03 | registers |
read_input_registers(unit, address, quantity) |
04 | registers |
write_single_register(unit, address, value) |
06 | registers |
write_multiple_registers(unit, address, &values) |
16 | registers |
read_write_multiple_registers(unit, ra, rq, wa, &wv) |
23 | registers |
mask_write_register(unit, address, and_mask, or_mask) |
22 | registers |
read_fifo_queue(unit, address) |
24 | fifo |
read_file_record(unit, &sub_request) |
20 | file-record |
write_file_record(unit, &sub_request) |
21 | file-record |
read_device_identification(unit, code, object_id) |
43/14 | diagnostics |
encapsulated_interface_transport(unit, mei, &data) |
43 | diagnostics |
read_exception_status(unit) |
07 | diagnostics |
diagnostics(unit, sub_fn, &data) |
08 | diagnostics |
get_comm_event_counter(unit) |
11 | diagnostics |
get_comm_event_log(unit) |
12 | diagnostics |
report_server_id(unit) |
17 | diagnostics |
Serial-specific constructors
| Constructor | Mode |
|---|---|
AsyncSerialClient::new_rtu(config) |
RTU |
AsyncSerialClient::new_rtu_with_poll_interval(config, interval) |
RTU |
AsyncSerialClient::new_ascii(config) |
ASCII |
AsyncSerialClient::new_ascii_with_poll_interval(config, interval) |
ASCII |
Each constructor validates that ModbusSerialConfig::mode matches the constructor's expected mode, returning AsyncError::Mbus(MbusError::InvalidConfiguration) on mismatch.
TCP-specific constructors
Default pipeline (AsyncTcpClient<9>) constructors:
| Constructor | Pipeline | Poll Interval |
|---|---|---|
AsyncTcpClient::new(host, port) |
9 |
default (20ms) |
AsyncTcpClient::new_with_poll_interval(host, port, interval) |
9 |
custom |
AsyncTcpClient::new_with_config(tcp_config, interval) |
9 |
custom |
Custom pipeline (AsyncTcpClient<N>) constructors:
| Constructor | Pipeline | Poll Interval |
|---|---|---|
AsyncTcpClient::<N>::new_with_pipeline(host, port) |
N |
default (20ms) |
AsyncTcpClient::<N>::new_with_pipeline_and_poll_interval(host, port, interval) |
N |
custom |
AsyncTcpClient::<N>::new_with_config_and_pipeline(tcp_config, interval) |
N |
custom |
Error Handling
use AsyncError;
match client.read_holding_registers.await
Concurrency
Multiple concurrent .await calls are supported. Each call gets an independent
transaction id and Tokio oneshot channel. Responses are routed back to the correct
caller by transaction id when the worker's AsyncApp callback fires.
Under TCP the underlying sync client pipelines up to N simultaneous requests
(default: 9).
Under serial, only one request can be outstanding at a time (Modbus serial is
inherently request/reply).
Example with custom TCP pipeline depth:
use AsyncTcpClient;
let client = new_with_pipeline?;
client.connect.await?;
Traffic Callback (optional traffic feature)
Enable traffic when you need raw frame observability in async apps:
[]
= { = "0.4", = false, = [
"async", "traffic", "tcp", "coils"
] }
= { = "1", = ["full"] }
use AsyncTcpClient;
async
License
This crate is licensed under GPL-3.0-only — see the repository root LICENSE.
If you require a commercial license to use this crate in a proprietary project, please contact ch.raghava44@gmail.com to purchase a license.