⚠️ Preview Release — v0.2.0 is a preview. The API may change based on user feedback. We welcome issues, PRs, and suggestions.
OMS Modbus is a high-performance, transport-generic Modbus library in pure Rust — built to support a next-generation industrial diagnostic IDE. Complete Master (client) and Slave (server) for TCP, RTU, and ASCII, all through a single unified API.
Why We Built It
Most Modbus libraries target one protocol (usually TCP) and one platform. When we started building an industrial diagnostic IDE at OrangeHorse, we needed:
- All three protocols — TCP, RTU, and ASCII — in one API, because real RS-485 networks mix them
- Passive bus monitoring — the ability to spy on traffic without participating, like a hardware logic analyzer
- Spec-compliant bus timing — the Modbus Serial Spec mandates a fixed 1.75 ms silent interval above 19200 baud. Most libraries ignore this, and it causes frame collisions on loaded buses.
- Panic-free production paths — zero
.unwrap()in library code, because a crashed diagnostic tool in the field is unacceptable
No existing Rust Modbus crate checked all four boxes. So we built one.
Quick Start
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
// Start a TCP server
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
let server = tcp::TcpServer::bind(addr).await.unwrap();
let store = Arc::new(SlaveStore::with_holding_registers(&[(0, 1234)]));
tokio::spawn(async move { server.serve_forever(store).await.ok(); });
// Connect and read registers
let client = tcp::TcpClient::connect_with_timeout(
server.local_addr().unwrap(), Duration::from_secs(3),
).await.unwrap();
let regs = client.read_holding_registers(1, 0, 1).await.unwrap(); // [1234]
Key Features
| Category | Details |
|---|---|
| Protocols | TCP (MBAP), RTU (CRC-16), ASCII (LRC). Full client + server for each. |
| Generic Transport | RTU/ASCII work over any AsyncRead + AsyncWrite — serial ports, TCP streams, Unix sockets, in-memory channels. One RtuClient for all. |
| Bus Monitor | Passive WireTap — microsecond ISO 8601 timestamps. Memory ring buffer, channel dispatch, file recorder, custom backends. |
| Auto-Reconnect | Built-in for all transports. Fixed-interval (industrial-predictable, no exponential backoff). USB serial resilience. |
| Bus Timing | Enforces ≥3.5T silence between RTU/ASCII frames. Per Modbus Serial Spec V1.02 §1.4. |
| 11 Function Codes | FC01–06, FC08, FC15–16, FC22–23. Diagnostic sub-functions 0x00, 0x0A–0x0E. |
| Error Handling | Structured ModbusError — short Display, full detail(), machine-readable label(). |
| Zero Panics | Zero .unwrap() in production code. Poison-safe mutexes. Checked arithmetic. |
| Lean Dependencies | 8 runtime deps: tokio + tokio-serial + tokio-util + bytes + futures-util + async-trait + thiserror + log. No transient dependencies. |
Performance
Benchmarks from a developer workstation (Intel Core i7-6800K @ 3.40 GHz, Windows 10, DDR4 32 GB, Rust 1.97):
| Benchmark | Time |
|---|---|
| CRC-16 (6-byte RTU frame) | 16 ns |
| CRC-16 (256 bytes) | 668 ns |
| PDU decode (ReadHolding) | 39 ns |
| PDU encode (ReadHolding) | 241 ns |
| RTU codec encode (6 bytes) | 112 ns |
| ASCII codec decode (19-byte frame) | 446 ns |
Installation
Add to your Cargo.toml:
[dependencies]
oms-modbus = "0.2"
No feature flags — everything included. TCP, RTU, ASCII, server, capture, monitoring, and timing all available by default.
Architecture
src/
├── frame.rs PDU encode/decode (Request, Response, Exception)
├── error.rs ModbusError — structured, label + detail + Display
├── client.rs ModbusClient trait — single required method: call()
├── server.rs Service trait + ServerHook + HookedService
├── slave_store.rs In-memory register/coil store (RwLock<Vec<u16>>)
├── bus_timing.rs Minimum frame spacing for RS-485 buses
├── capture.rs BusCapture — WireTap impl with recording + stats
├── wire_tap.rs WireTap trait — passive bus observer (3 hooks)
├── reconnect.rs ReconnectConfig + is_retryable()
├── codec.rs CRC-16 + LRC + frame encoding
├── intercept.rs PacketRecord, PacketData, TrafficStats
├── transport/
│ ├── tcp.rs TcpClient + TcpServer + TcpConfig (MBAP)
│ ├── rtu.rs RtuClient + RtuServer (generic transport)
│ ├── ascii.rs AsciiClient + AsciiServer (generic transport)
│ ├── send_recv.rs Shared drain/reconnect/process helpers
│ └── sniff_io.rs SniffIo<T> — WireTap attachment + BusTiming
└── monitor/
├── channel.rs ChannelRecorder + RecordSink trait
├── ring_buffer.rs RingBufferCapture
└── file_recorder.rs FileRecorder (ISO 8601, non-blocking)
The core principle: WireTap attaches at the physical transport boundary. Neither client nor server participates in capture — it is a pure passive observer, like a hardware logic analyzer clipped onto an RS-485 bus.
Connect
| Channel | Link |
|---|---|
| 🌐 Website | orangehorsetech.com |
| OrangeHorseTech | |
| github@orangehorsetech.com | |
| 📦 crates.io | oms-modbus |
| 📚 Documentation | docs.rs/oms-modbus |
License
Licensed under either of MIT License or Apache License 2.0, at your option.
OMS Modbus is used internally at OrangeHorse to build diagnostic tools for our Modbus-enabled sensors. It is not a side project — it is production infrastructure we depend on every day.