modbus-rtu

Crates.iomodbus-rtu
lib.rsmodbus-rtu
version1.3.0
created_at2024-09-12 07:42:37.385639+00
updated_at2025-11-25 12:41:37.405262+00
descriptionStandard Modbus RTU protocols
homepage
repository
max_upload_size
id1372556
size93,835
Jababa (im-jababa)

documentation

README

modbus-rtu

Github Crates.io Docs.rs Build

This crate provides helpers for building and decoding standard Modbus RTU request and response packets. It now ships with a synchronous Master that can talk to a serial port directly, while still exposing the lower-level building blocks for applications that prefer to manage framing themselves.


Usage

High-level master (auto write/read)

use modbus_rtu::{Function, Master, Request};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut master = Master::new_rs485("/dev/ttyUSB0", 19_200)?;

    let func = Function::ReadHoldingRegisters { starting_address: 0x0000, quantity: 2 };
    let request = Request::new(0x01, &func, std::time::Duration::from_millis(200));

    let response = master.send(&request)?;
    println!("response: {response:?}");
    Ok(())
}

The master enforces the Modbus RTU silent interval (T3.5) before/after each transmission, flushes the TX buffer, reads until the slave stops talking, and automatically decodes the reply.


Async master (Tokio)

AsyncMaster ships enabled by default (feature async). You only need to wire up a Tokio runtime. Disable async in Cargo.toml if you prefer the smaller blocking-only build.

[dependencies]
modbus-rtu = "1.2"
tokio = { version = "1.38", features = ["rt", "macros"] }
use modbus_rtu::{Function, AsyncMaster, Request};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut master = AsyncMaster::new_rs485("/dev/ttyUSB0", 19_200)?;

    let func = Function::ReadHoldingRegisters { starting_address: 0x0000, quantity: 2 };
    let request = Request::new(0x01, &func, std::time::Duration::from_millis(200));

    let response = master.send(&request).await?;
    println!("response: {response:?}");
    Ok(())
}

The async master mirrors the synchronous behavior but uses async sleeps and I/O to maintain the Modbus RTU silent interval between frames.


Queued async master (shared worker)

QueuedMaster wraps a single async worker task and an MPSC queue so multiple async callers can share one serial link. The buffer argument is the channel depth; requests are buffered up to that limit and passing 0 will panic inside tokio::sync::mpsc::channel.

use std::time::Duration;
use modbus_rtu::{Function, QueuedMaster, Request};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // buffer=4 means up to four queued requests before senders await.
    let master = QueuedMaster::new_rs485("/dev/ttyUSB0", 38_400, 4).await?;

    let func = Function::ReadInputRegisters { starting_address: 0x0001, quantity: 12 };
    let req = Request::new(1, &func, Duration::from_millis(100));

    let response = master.send(&req, 38_400).await?;
    println!("response: {response:?}");
    Ok(())
}

You can clone the returned Arc<QueuedMaster> and call send from many tasks; the worker enforces Modbus idle timing between frames and switches baud rate per request if needed.


Opting out of the master to shrink binaries

The synchronous and async masters (and their serial dependencies) are enabled by default. If you only need the packet-building utilities, disable default features in your Cargo.toml:

[dependencies]
modbus-rtu = { version = "1.2", default-features = false }

Then opt into what you need:

  • Blocking master only (drops Tokio/async deps):

    modbus-rtu = { version = "1.2", default-features = false, features = ["master"] }
    
  • Both masters (default behavior):

    modbus-rtu = { version = "1.2", default-features = false, features = ["master", "async"] }
    

Manual packet construction

First, construct the function you want to issue. The following example reads four input registers starting at address 0x1234.

use modbus_rtu::Function;

let starting_address: u16 = 0x1234;
let quantity: usize = 4;
let function = Function::ReadInputRegisters { starting_address, quantity };

Next, build the request with the target device information and timeout.

use modbus_rtu::{Function, Request};

...

let modbus_id: u8 = 1;
let timeout: std::time::Duration = std::time::Duration::from_millis(100);
let request = Request::new(1, &function, timeout);

Finally, convert the request into a Modbus RTU frame.

...

let packet: Box<[u8]> = request.to_bytes().expect("Failed to build request packet");

You can now write packet through any transport of your choice (UART, TCP tunnel, etc.).


Receiving

With the original request available, attempt to decode the response bytes as shown below.

use modbus_rtu::Response;

...

let bytes: &[u8] = ... ; // user-implemented receive logic

let response = Response::from_bytes(&request, bytes).expect("Failed to analyze response packet");

match response {
    Response::Value(value) => {
        let _ = value[0];   // value at address 0x1234
        let _ = value[1];   // value at address 0x1235
        let _ = value[2];   // value at address 0x1236
        let _ = value[3];   // value at address 0x1237
    },
    Response::Exception(e) => {
        eprintln!("device responded with exception: {e}");
    },
    _ => unreachable!(),
}

If you disable default features, re-enable both master and async to access AsyncMaster.

Commit count: 0

cargo fmt