What is rmodbus
masterstd and no_std environments and can be used in blocking or async applications.repository·master·Indexed 41 days ago
https://github.com/alttch/rmodbusA fast and platform-independent Modbus server framework for Rust. It provides a request/response codec and a context manager for building Modbus-powered applications across TCP/UDP, RTU, and ASCII protocols. Supports both std and no_std environments, with flexible storage options (Full, Small, or Custom-sized) and support for various vector implementations like fixedvec and heapless. Version 0.12.2.
std and no_std environments and can be used in blocking or async applications.The ModbusContext is the central data store for a Modbus server. It manages coils, discrete inputs, input registers, and holding registers.
Best Practices:
Mutex or RwLock in your application.Common Operations:
get_holding(address): Read a holding register.get_holdings_as_f32(address): Read an IEEE754 f32 from registers.set_coil(address, value): Write a coil.set_holdings_bulk(address, data): Write multiple registers at once.Depending on your target environment and enabled features, rmodbus supports different vector implementations:
std (default): Uses std::vec::Vec.fixedvec feature: Uses fixedvec::FixedVec.heapless feature: Uses heapless::Vec.alloc feature: Enables alloc::vec::Vec for no_std environments with an allocator.To use the client, use ModbusRequest to generate frames and guess_response_frame_len to handle variable-length responses.
Workflow:
TcpStream).ModbusRequest object with the appropriate unit ID and protocol.generate_set_coils_bulk or generate_get_coils to populate a request buffer.guess_response_frame_len to determine how many more bytes to read from the stream.parse_ok or parse_bool to validate the response and extract data.use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
use rmodbus::{client::ModbusRequest, guess_response_frame_len, ModbusProto};
let timeout = Duration::from_secs(1);
// open TCP connection
let mut stream = TcpStream::connect("localhost:5502").unwrap();
stream.set_read_timeout(Some(timeout)).unwrap();
stream.set_write_timeout(Some(timeout)).unwrap();
// create request object
let mut mreq = ModbusRequest::new(1, ModbusProto::TcpUdp);
mreq.tr_id = 2;
// set 2 coils
let mut request = Vec::new();
mreq.generate_set_coils_bulk(0, &[true, true], &mut request).unwrap();
// write request to stream
stream.write(&request).unwrap();
// read first 6 bytes of response frame
let mut buf = [0u8; 6];
stream.read_exact(&mut buf).unwrap();
let mut response = Vec::new();
response.extend_from_slice(&buf);
let len = guess_response_frame_len(&buf, ModbusProto::TcpUdp).unwrap();
// read rest of response frame
if len > 6 {
let mut rest = vec![0u8; (len - 6) as usize];
stream.read_exact(&mut rest).unwrap();
response.extend(rest);
}
// check if frame has no Modbus error inside
mreq.parse_ok(&response).unwrap();To build a server, you must provide your own transport layer (e.g., TcpListener). rmodbus is used to parse incoming frames from the stream and process them against a ModbusContext.
Key steps in the loop:
ModbusFrameBuf.ModbusFrame specifying the unit ID and protocol (e.g., ModbusProto::TcpUdp).frame.parse() to decode the frame.frame.processing_required is true, call frame.process_read() or frame.process_write() using your application's ModbusContext.frame.response_required is true, call frame.finalize_response() and write the resulting buffer back to the stream.use std::io::{Read, Write};
use std::net::TcpListener;
use std::thread;
use std::sync::RwLock;
use once_cell::sync::Lazy;
use rmodbus::{
server::{storage::ModbusStorageFull, context::ModbusContext, ModbusFrame},
ModbusFrameBuf, ModbusProto,
};
static CONTEXT: Lazy<RwLock<ModbusStorageFull>> = Lazy::new(<_>::default);
pub fn tcpserver(unit: u8, listen: &str) {
let listener = TcpListener::bind(listen).unwrap();
println!("listening started, ready to accept");
for stream in listener.incoming() {
thread::spawn(move || {
println!("client connected");
let mut stream = stream.unwrap();
loop {
let mut buf: ModbusFrameBuf = [0; 256];
let mut response = Vec::new(); // for nostd use FixedVec with alloc [u8;256]
if stream.read(&mut buf).unwrap_or(0) == 0 {
return;
}
let mut frame = ModbusFrame::new(unit, &buf, ModbusProto::TcpUdp, &mut response);
if frame.parse().is_err() {
println!("server error");
return;
}
if frame.processing_required {
let result = if frame.readonly {
frame.process_read(&*CONTEXT.read().unwrap())
} else {
frame.process_write(&mut *CONTEXT.write().unwrap())
};
if result.is_err() {
println!("frame processing error");
return;
}
}
if frame.response_required {
frame.finalize_response().unwrap();
println!("{:x?}", response.as_slice());
if stream.write(response.as_slice()).is_err() {
return;
}
}
}
});
}
}To use rmodbus in a no_std environment, disable the default features in your Cargo.toml:
rmodbus = { version = "*", default-features = false }Note that when using no_std, you may need to enable specific features like alloc to use alloc::vec::Vec or use fixedvec/heapless for vector-like structures.
Depending on your RAM constraints, you can choose different storage implementations:
ModbusStorageFull provides 10,000 registers of each type (approx. 60KB RAM).ModbusStorageSmall provides 1,000 registers of each type.ModbusStorage to define exact sizes for coils, discretes, inputs, and holdings using generic constants.Generic Constant Order: ModbusStorage<COILS, DISCRETES, INPUTS, HOLDINGS>
// Small storage
use rmodbus::server::{storage::ModbusStorageSmall, context::ModbusContext};
// Custom-sized storage: 128 coils, 16 discretes, 0 inputs, 100 holdings
use rmodbus::server::{storage::ModbusStorage, context::ModbusContext};
let context = ModbusStorage::<128, 16, 0, 100>::new();To build a custom Modbus server, you must implement the ModbusContext trait. This trait defines how the server interacts with your underlying data storage for all four Modbus data types: Coils, Discretes, Input Registers, and Holding Registers.
Methods in this trait allow for:
u32, u64, f32, or custom types implementing RegisterRepresentable.u8 sequences, with specific support for bit-packed coils/discretes or byte-aligned representations.The rmodbus::server::representations module provides pre-defined types for storing multi-register values. These types handle the mapping between large integers and u16 register sequences using different endianness patterns.
Note on Endianness: In this context, 'Endianness' refers to the order of 16-bit words (registers). The bytes within each individual 16-bit word are always big-endian.
For example, a u32 like 0x1122_3344 would be represented as 3344 1122 in Little Endian (the low-order word comes first).
// Example of using representations with a Modbus context
let mut ctx = ModbusStorageFull::default();
let to_be_stored: u32 = 123u32;
// Store as little endian at address 1
ctx.set_inputs_from_representable(1, &U32LittleEndian(to_be_stored)).unwrap();
// Read it back
let read: U32LittleEndian = ctx.get_inputs_as_representable(1).unwrap();
assert_eq!(U32LittleEndian(to_be_stored), read);The ModbusStorage struct implements the ModbusContext trait, providing a complete set of methods to interact with Modbus registers. This allows you to use ModbusStorage as the data backend for a Modbus server.
Key capabilities include:
get_coil, get_discrete, get_input, get_holding, set_coil, etc.get_holdings_bulk, set_coils_bulk, etc.u32, u64, or f32 (using IEEE 754 representation).get_holdings_as_u8 and set_holdings_from_u8 for raw byte manipulation.To implement a Modbus server, use the ModbusFrame struct to process incoming buffers. The typical workflow involves:
ModbusFrame with a unit ID, the input buffer, the protocol (ModbusProto), and a mutable response buffer..parse() to validate the frame and extract function codes, registers, and counts..processing_required to determine if the request needs to be applied to your data storage..process_read(&ctx) or .process_write(&mut ctx) to interact with a ModbusContext..response_required and calling .finalize_response() to prepare the response buffer for transmission.Note: If you are managing your own data storage instead of using ModbusContext, use .get_external_read()/.get_external_write() followed by .process_external_read()/.process_external_write().
// Example server loop logic
let mut ctx = ModbusStorageFull::new();
let unit_id = 1;
loop {
let framebuf: ModbusFrameBuf = [0; 256];
// ... read frame into framebuf ...
let mut response = FixedVec::new(&mut mem);
let mut frame = ModbusFrame::new(unit_id, &framebuf, ModbusProto::TcpUdp, &mut response);
if frame.parse().is_ok() {
if frame.processing_required {
let result = match frame.readonly {
true => frame.process_read(&ctx),
false => frame.process_write(&mut ctx)
};
if result.is_err() {
continue;
}
}
if frame.response_required {
frame.finalize_response();
// ... send response ...
}
}
}If you do not want to use the provided ModbusContext trait, you can manually handle reads and writes by extracting the request details and then notifying the frame of the result. This is useful for integrating with custom hardware or existing databases.
For Writes:
get_external_write() to get a Write enum (containing WriteBits or WriteWords).process_external_write(result) with the outcome of your operation.For Reads:
get_external_read() to get a Read enum (containing ReadBits or ReadWords).process_external_read(result) to finalize the frame state.// Example of external write handling
match frame.get_external_write()? {
Write::Bits(bits) => {
// Apply bits to your custom system
let result = my_custom_system.write_bits(bits.address, bits.count, bits.data);
frame.process_external_write(result)?;
}
Write::Words(words) => {
// Apply words to your custom system
let result = my_custom_system.write_words(words.address, words.count, words.data);
frame.process_external_write(result)?;
}
}