# mbus-client
`mbus-client` is a helper crate for [modbus-rs](https://crates.io/crates/modbus-rs).
It provides the client-side Modbus request/response engine, built on top of shared
protocol and transport abstractions from `mbus-core`.
If you want a single top-level entry point, use `modbus-rs`.
If you want direct access to client orchestration and callbacks, use `mbus-client`.
## Helper Crate Role
`mbus-client` is responsible for client workflow, not transport implementation:
- Builds Modbus requests and tracks outstanding transactions.
- Polls transport for responses and dispatches parsed callbacks.
- Handles retries and timeout-based failure paths.
- Exposes feature-gated service modules by function group.
Transport implementations are provided by helper crates such as:
- `mbus-tcp`
- `mbus-serial`
## What Is Included
- `services::ClientServices`: the central client orchestrator.
- `services::SerialClientServices`: convenience alias for serial clients (`N = 1`).
- Feature-gated service modules:
- `services::coil`
- `services::register`
- `services::discrete_input`
- `services::fifo_queue`
- `services::file_record`
- `services::diagnostic`
- `app` callback traits:
- `RequestErrorNotifier`
- response traits for each function group
## Retry Backoff and Jitter
Retries are poll-driven and timestamp-scheduled. The client never sleeps or blocks.
- Timeout detection happens inside `ClientServices::poll()`.
- Retries are scheduled using `BackoffStrategy` from `modbus-rs`.
- Optional jitter is applied using `JitterStrategy`.
- Randomness for jitter is application-provided via `retry_random_fn` on config.
Example (TCP with exponential backoff + percentage jitter):
```rust
use modbus_rs::{BackoffStrategy, JitterStrategy, ModbusTcpConfig};
fn app_random_u32() -> u32 {
// Replace with your MCU/OS RNG source.
42
}
let mut tcp = ModbusTcpConfig::new("127.0.0.1", 502)?;
tcp.retry_attempts = 3;
tcp.retry_backoff_strategy = BackoffStrategy::Exponential {
base_delay_ms: 100,
max_delay_ms: 2000,
};
tcp.retry_jitter_strategy = JitterStrategy::Percentage { percent: 20 };
tcp.retry_random_fn = Some(app_random_u32);
```
If `retry_random_fn` is `None`, jitter strategies gracefully fall back to non-jittered delays.
## Reconnect and Connection State
`ClientServices` now exposes explicit connection management helpers:
- `client.is_connected()` to query transport connection state.
- `client.reconnect()` to re-establish transport using the current config.
Reconnect behavior:
- Pending in-flight requests are failed immediately with `MbusError::ConnectionLost`.
- Internal receive buffers and timeout checkpoints are cleared.
- `disconnect()` is attempted, then `connect(&config)` is called.
- Requests are not auto re-sent; the application should requeue explicitly.
This behavior is suitable for long-running daemons and embedded systems that must
recover from temporary link loss.
## App Handler Access
`ClientServices` keeps the application callback handler encapsulated.
- Use `client.app()` for immutable inspection.
- There is no public replacement/mutable handler API.
This preserves callback identity for in-flight requests and avoids accidental
handler swaps during active transactions.
## Serial Queue Constraint
For serial transports, Modbus is half-duplex and only one request may be in flight.
- Runtime-safe path: `ClientServices::new(...)` validates serial `N == 1`.
- Compile-time-safe path: `ClientServices::new_serial(...)` enforces `N == 1`.
- Recommended type alias: `SerialClientServices<TRANSPORT, APP>`.
## Feature Flags
This crate uses selective compilation so you only build required protocol services.
Available features:
- `coils`
- `registers`
- `discrete-inputs`
- `fifo`
- `file-record`
- `diagnostics`
- `serial-ascii` (forwards to `mbus-core/serial-ascii` to enable ASCII-sized ADU buffers)
- `logging` (enables low-priority internal state-machine diagnostics via the `log` facade)
Default behavior:
- `default` enables all service features above.
Feature forwarding:
- Each feature forwards to the equivalent model feature in `mbus-core`.
Example (minimal feature set):
```toml
[dependencies]
mbus-client = { version = "0.2.0", default-features = false, features = ["coils"] }
```
## Logging
`mbus-client` can emit low-priority internal diagnostics through the `log` facade when the
`logging` feature is enabled.
These logs are intentionally limited to `debug` and `trace` so applications can filter them
without treating normal control-flow events as warnings or errors.
Examples of logged events:
- frame parse/resynchronization
- response dispatch matching
- timeout scans and retry scheduling
- retry send failures
- pending-request flush during connection loss or reconnect
Typical filtering example:
```bash
RUST_LOG=mbus_client=trace cargo run -p modbus-rs --example logging_example --no-default-features --features tcp,client,logging
```
## Usage Pattern
Typical flow:
1. Implement required callback traits in your app type.
2. Provide a `Transport` implementation (custom, `mbus-tcp`, or `mbus-serial`).
3. Build a `ModbusConfig`.
4. Construct `ClientServices`.
5. Issue requests.
6. Call `poll()` periodically to process responses and timeouts.
## Minimal Example
```rust
use modbus_rs::{
ClientServices, MAX_ADU_FRAME_LEN, MbusError, ModbusConfig, ModbusTcpConfig,
RequestErrorNotifier, TimeKeeper, Transport, TransportType, UnitIdOrSlaveAddr,
};
#[cfg(feature = "coils")]
use modbus_rs::{CoilResponse, Coils};
use heapless::Vec;
struct MockTransport;
impl Transport for MockTransport {
type Error = MbusError;
fn connect(&mut self, _: &ModbusConfig) -> Result<(), Self::Error> { Ok(()) }
fn disconnect(&mut self) -> Result<(), Self::Error> { Ok(()) }
fn send(&mut self, _: &[u8]) -> Result<(), Self::Error> { Ok(()) }
fn recv(&mut self) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, Self::Error> { Ok(Vec::new()) }
fn is_connected(&self) -> bool { true }
fn transport_type(&self) -> TransportType { TransportType::StdTcp }
}
struct App;
impl RequestErrorNotifier for App {
fn request_failed(&self, _: u16, _: UnitIdOrSlaveAddr, _: MbusError) {}
}
#[cfg(feature = "coils")]
impl CoilResponse for App {
fn read_coils_response(&self, _: u16, _: UnitIdOrSlaveAddr, _: &Coils) {}
fn read_single_coil_response(&self, _: u16, _: UnitIdOrSlaveAddr, _: u16, _: bool) {}
fn write_single_coil_response(&self, _: u16, _: UnitIdOrSlaveAddr, _: u16, _: bool) {}
fn write_multiple_coils_response(&self, _: u16, _: UnitIdOrSlaveAddr, _: u16, _: u16) {}
}
impl TimeKeeper for App {
fn current_millis(&self) -> u64 { 0 }
}
fn main() -> Result<(), MbusError> {
let transport = MockTransport;
let app = App;
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502)?);
let mut client = ClientServices::<_, _, 4>::new(transport, app, config)?;
#[cfg(feature = "coils")]
client.coils().read_multiple_coils(1, UnitIdOrSlaveAddr::new(1)?, 0, 8)?;
#[cfg(feature = "coils")]
client.with_coils(|coils| {
coils.read_single_coil(2, UnitIdOrSlaveAddr::new(1)?, 0)?;
coils.write_single_coil(3, UnitIdOrSlaveAddr::new(1)?, 0, true)?;
Ok::<(), MbusError>(())
})?;
client.poll();
Ok(())
}
```
## Feature-Scoped Access Style
`ClientServices` now supports feature facades so request APIs can be grouped by domain:
- `client.coils()`
- `client.registers()`
- `client.discrete_inputs()`
- `client.diagnostic()`
- `client.fifo()`
- `client.file_records()`
For grouped request submission in a single scoped borrow, use batch helpers:
- `client.with_coils(...)`
- `client.with_registers(...)`
- `client.with_discrete_inputs(...)`
- `client.with_diagnostic(...)`
- `client.with_fifo(...)`
- `client.with_file_records(...)`
## Build Examples
From workspace root:
```bash
# default services
cargo check -p mbus-client
# only coils service
cargo check -p mbus-client --no-default-features --features coils
# registers + discrete inputs only
cargo check -p mbus-client --no-default-features --features registers,discrete-inputs
```
## Notes
- This crate is `no_std` friendly and uses `heapless` internally.
- Service and callback traits are conditionally compiled by feature flags.
- Use exact feature names with hyphens:
- `discrete-inputs`
- `file-record`
## License
Copyright (C) 2025 Raghava Challari
This project is currently licensed under GNU GPL v3.0.
See [LICENSE](./LICENSE) for details.
## Disclaimer
This is an independent Rust implementation of the Modbus specification and is not
affiliated with the Modbus Organization.
## Contact
For questions or support:
- Name: Raghava Ch
- Email: [ch.raghava44@gmail.com](mailto:ch.raghava44@gmail.com)