Rust v0.2.0 MIT OR Apache-2.0

OMS Modbus

High-performance, transport-generic Modbus library in pure Rust. TCP, RTU, ASCII — one API. Passive bus monitoring with microsecond timestamps. 440+ tests, zero panics. MIT/Apache-2.0 licensed.

⚠️ 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

CategoryDetails
ProtocolsTCP (MBAP), RTU (CRC-16), ASCII (LRC). Full client + server for each.
Generic TransportRTU/ASCII work over any AsyncRead + AsyncWrite — serial ports, TCP streams, Unix sockets, in-memory channels. One RtuClient for all.
Bus MonitorPassive WireTap — microsecond ISO 8601 timestamps. Memory ring buffer, channel dispatch, file recorder, custom backends.
Auto-ReconnectBuilt-in for all transports. Fixed-interval (industrial-predictable, no exponential backoff). USB serial resilience.
Bus TimingEnforces ≥3.5T silence between RTU/ASCII frames. Per Modbus Serial Spec V1.02 §1.4.
11 Function CodesFC01–06, FC08, FC15–16, FC22–23. Diagnostic sub-functions 0x00, 0x0A–0x0E.
Error HandlingStructured ModbusError — short Display, full detail(), machine-readable label().
Zero PanicsZero .unwrap() in production code. Poison-safe mutexes. Checked arithmetic.
Lean Dependencies8 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.

ArticleWhat You'll Learn
Modbus Protocol GuideThe 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–06The 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, 23The 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 PitfallsThe 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 NanosecondsThe 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 RuleWhy 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 TransportHow 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 RustThe story behind oms-modbus — the four hard requirements that ruled out every existing crate, and what we learned building it from scratch.
WireTap Quick StartAttach a passive bus monitor to Modbus RTU/TCP in 15 minutes. Self-contained demo — no RS-485 hardware needed.
Passive Bus MonitoringHow passive bus observation works at the I/O boundary, why microsecond timestamps matter for diagnostics, and what the captures reveal.
Decoding Modbus Frames by HandByte-by-byte breakdown of RTU, TCP, and ASCII frames. Function code identification, CRC verification, error frame detection.
Field Troubleshooting with WireTapThree real-world RS-485 failure scenarios — CRC errors, silent slaves, timing jitter — with a diagnostic analyzer.
oms-modbus vs libmodbus vs tokio-modbusAn objective comparison across protocols, async, bus monitoring, licensing, and maturity — with the same read task written all three ways.
oms-modbus FAQConcise answers on protocols, function codes, WireTap, licensing, dependencies, and getting started.
Building a Modbus Device ScannerSweep 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 EngineeringThe four disciplines that make a Modbus client survive in production: auto-reconnect, retryable-error classification, bounded concurrency, and passive observability.
Building a Modbus GatewayBridge 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: MBAPThe Modbus TCP header decoded byte by byte — transaction IDs, the Length field, unit IDs, and the TcpConfig options for non-standard gateways.
Modbus Data EncodingHow 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 ModeThe human-readable transmission mode — colon/CRLF framing, hex encoding, the LRC checksum, and how it compares to RTU's CRC and timing.
Modbus Register AddressingThe 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 IdentificationHow 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 CodesEvery 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 ServerThe 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):

BenchmarkTime
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

ChannelLink
🌐 Websiteorangehorsetech.com
💼 LinkedInOrangeHorseTech
📧 Emailgithub@orangehorsetech.com
📦 crates.iooms-modbus
📚 Documentationdocs.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.

Technical Articles

oms-modbus FAQ: Frequently Asked Questions

Concise answers to the most common questions about oms-modbus — protocols, function codes, WireTap, licensing, dependencies, and how to get started.

3 min read

Seven Modbus Pitfalls That Break Integrations (and How to Avoid Them)

The seven most common Modbus integration mistakes — 0-based vs 1-based addressing, multi-register float word order, signed vs unsigned values, wire byte order, integer scaling, quantity limits, and blind retries — with concrete frame examples and runnable Rust code.

10 min read

Why We Built a Modbus Library in Rust

The story behind oms-modbus: why OrangeHorse built a new Modbus library from scratch, the four hard requirements that ruled out every existing crate, and what we learned along the way.

5 min read

Want to contribute?

Issues, pull requests, and feature suggestions are welcome. Check the contributing guide or open an issue on GitHub.

View on GitHub