⚠️ 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;
use oms_modbus::*;
// Start a TCP server
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
let server = tcp::TcpServer::bind(addr).await.unwrap();
let server_addr = server.local_addr().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_addr, 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. |
Tutorials
Most articles include runnable Rust code — compile and run with cargo check, zero hardware required. The FAQ and comparison are reference material.
| Article | What You'll Learn |
|---|---|
| Modbus Protocol Guide | The ground-up guide: four data tables, 0-based vs 1-based addressing, PDU vs ADU, byte ordering, and frame formats — with a real soil-sensor worked example. |
| Modbus Function Codes 01–06 | The six workhorse function codes — Read Coils through Write Single Register — with request/response byte layouts and verified frame examples. |
| Modbus Function Codes 15, 16, 22, 23 | The advanced write codes — Write Multiple Coils/Registers (15/16), Mask Write (22), Read/Write Multiple (23), and Diagnostic (08) — with the complete exception-code table. |
| Seven Modbus Pitfalls | The mistakes that break integrations: 0-based vs 1-based addressing, multi-register float word order, signed vs unsigned, wire byte order — with a runnable demo. |
| CRC-16 in 668 Nanoseconds | The Modbus CRC-16 algorithm, from bit-by-bit to a compile-time lookup table — and how oms-modbus validates a 256-byte frame in under a microsecond. |
| The 3.5T Rule | Why the 3.5-character silent interval is the most-violated timing rule on RS-485 buses — and how oms-modbus enforces it at the protocol level. |
| One Modbus Client, Any Transport | How one RTU/ASCII client works over serial ports, TCP streams, Unix sockets, and in-memory channels through a single ModbusClient trait. |
| Why We Built a Modbus Library in Rust | The story behind oms-modbus — the four hard requirements that ruled out every existing crate, and what we learned building it from scratch. |
| WireTap Quick Start | Attach a passive bus monitor to Modbus RTU/TCP in 15 minutes. Self-contained demo — no RS-485 hardware needed. |
| Passive Bus Monitoring | How passive bus observation works at the I/O boundary, why microsecond timestamps matter for diagnostics, and what the captures reveal. |
| Decoding Modbus Frames by Hand | Byte-by-byte breakdown of RTU, TCP, and ASCII frames. Function code identification, CRC verification, error frame detection. |
| Field Troubleshooting with WireTap | Three real-world RS-485 failure scenarios — CRC errors, silent slaves, timing jitter — with a diagnostic analyzer. |
| oms-modbus vs libmodbus vs tokio-modbus | An objective comparison across protocols, async, bus monitoring, licensing, and maturity — with the same read task written all three ways. |
| oms-modbus FAQ | Concise answers on protocols, function codes, WireTap, licensing, dependencies, and getting started. |
| Building a Modbus Device Scanner | Sweep slave addresses 1–247 to find live devices — the three-way classification of responses, exception code 11, bounded concurrency, and a runnable scanner. |
| Rust Modbus Production Engineering | The four disciplines that make a Modbus client survive in production: auto-reconnect, retryable-error classification, bounded concurrency, and passive observability. |
| Building a Modbus Gateway | Bridge Modbus TCP and RTU by implementing the Service trait — recovering the slave address, forwarding to a downstream client, and translating transport failures into gateway exceptions 10 and 11. |
| Modbus TCP Framing: MBAP | The Modbus TCP header decoded byte by byte — transaction IDs, the Length field, unit IDs, and the TcpConfig options for non-standard gateways. |
| Modbus Data Encoding | How 32-bit values and floats are split across two 16-bit registers — the four byte orders (ABCD/CDAB/BADC/DCBA), signed vs unsigned, and scale factors. |
| Modbus ASCII Mode | The human-readable transmission mode — colon/CRLF framing, hex encoding, the LRC checksum, and how it compares to RTU's CRC and timing. |
| Modbus Register Addressing | The 40001 puzzle — Modicon addresses vs 0-based PDU offsets, the four data-table prefixes, and the off-by-one trap that breaks integrations. |
| Function Code 43: Read Device Identification | How a master asks a slave "what are you?" — the FC43 PDU, the four Read Device ID access modes, conformity levels, and the standard identification objects byte by byte. |
| Modbus Exception Codes | Every exception code from 1 to 11, the exception PDU format, why an exception is not a timeout, and the two ways oms-modbus surfaces them. |
| Building a Modbus Server | The slave side of Modbus — the one-line SlaveStore path for static data, and the Service trait for live readings, write side effects, and validation. |
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.