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;

// 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

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.

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

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