mbus-client 0.5.0

Modbus client stack for embedded and std environments with TCP, RTU, and ASCII transport support for modbus-rs project
Documentation

mbus-client

mbus-client is a helper crate for 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-network
  • 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):

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.connect() to open the transport after construction.
  • 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)
  • traffic (enables raw TX/RX frame callbacks via TrafficNotifier)
  • 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):

[dependencies]
mbus-client = { version = "0.5.0", default-features = false, features = ["coils"] }

Traffic Callbacks (optional traffic feature)

When traffic is enabled, apps can implement TrafficNotifier to observe raw ADU frames:

use mbus_client::app::{TrafficDirection, TrafficNotifier};
use mbus_core::transport::UnitIdOrSlaveAddr;

struct App;

impl TrafficNotifier for App {
  fn on_tx_frame(
    &mut self,
    txn_id: u16,
    unit_id_slave_addr: UnitIdOrSlaveAddr,
    frame_bytes: &[u8],
  ) {
    println!(
      "[{:?}] txn={} unit={} bytes={:02X?}",
      TrafficDirection::Tx,
      txn_id,
      unit_id_slave_addr.get(),
      frame_bytes
    );
  }

  fn on_rx_frame(
    &mut self,
    txn_id: u16,
    unit_id_slave_addr: UnitIdOrSlaveAddr,
    frame_bytes: &[u8],
  ) {
    println!(
      "[{:?}] txn={} unit={} bytes={:02X?}",
      TrafficDirection::Rx,
      txn_id,
      unit_id_slave_addr.get(),
      frame_bytes
    );
  }

  fn on_tx_error(
    &mut self,
    txn_id: u16,
    unit_id_slave_addr: UnitIdOrSlaveAddr,
    error: mbus_core::errors::MbusError,
    frame_bytes: &[u8],
  ) {
    println!(
      "[{:?}] txn={} unit={} error={error:?} bytes={:02X?}",
      TrafficDirection::Tx,
      txn_id,
      unit_id_slave_addr.get(),
      frame_bytes
    );
  }

  fn on_rx_error(
    &mut self,
    txn_id: u16,
    unit_id_slave_addr: UnitIdOrSlaveAddr,
    error: mbus_core::errors::MbusError,
    frame_bytes: &[u8],
  ) {
    println!(
      "[{:?}] txn={} unit={} error={error:?} bytes={:02X?}",
      TrafficDirection::Rx,
      txn_id,
      unit_id_slave_addr.get(),
      frame_bytes
    );
  }
}

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:

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-network, or mbus-serial).
  3. Build a ModbusConfig.
  4. Construct ClientServices.
  5. Issue requests.
  6. Call poll() periodically to process responses and timeouts.

Minimal Example

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)?;
  client.connect()?;

    #[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:

# 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 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: