# modbus-rtu
[](https://github.com/im-jababa/rust-modbus-rtu)
[](https://crates.io/crates/modbus-rtu)
[](https://docs.rs/modbus-rtu)
[](https://github.com/im-jababa/rust-modbus-rtu/actions?query=branch%3Amain)
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)
```rust
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.
```toml
[dependencies]
modbus-rtu = "1.2"
tokio = { version = "1.38", features = ["rt", "macros"] }
```
```rust
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.
---
## 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`:
```toml
[dependencies]
modbus-rtu = { version = "1.2", default-features = false }
```
Then opt into what you need:
- Blocking master only (drops Tokio/async deps):
```toml
modbus-rtu = { version = "1.2", default-features = false, features = ["master"] }
```
- Both masters (default behavior):
```toml
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`.
```rust
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.
```rust
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.
```rust
...
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.
```rust
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`.