A framework to build fast and reliable Modbus-powered applications.
What is rmodbus
rmodbus is not a yet another Modbus client/server. rmodbus is a set of tools to
quickly build Modbus-powered applications. Consider rmodbus is a
request/response codec, plus context manager.
rmodbus is a part of EVA ICS v4 industrial
automation stack.
Why yet another Modbus lib?
- rmodbus is transport- and protocol-independent
- rmodbus is platform independent (
no_std is fully supported!)
- can be easily used in blocking and async (non-blocking) applications
- tuned for speed and reliability
- provides a set of tools to easily work with Modbus context
- supports client/server frame processing for Modbus TCP/UDP, RTU and ASCII
- server context can be easily managed, imported and exported
So no server is included?
Yes, there is no server included. You build the server by your own. You choose
the transport protocol, technology and everything else. rmodbus just process
frames and works with Modbus context.
Here is an example of a simple TCP blocking server:
use std::io::{Read, Write};
use std::net::TcpListener;
use std::thread;
use lazy_static::lazy_static;
use std::sync::RwLock;
use rmodbus::{
server::{context::ModbusContextFull, ModbusFrame},
ModbusFrameBuf, ModbusProto,
};
lazy_static! {
pub static ref CONTEXT: RwLock<ModbusContextFull> = RwLock::new(ModbusContextFull::new());
}
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(); 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 = match frame.readonly {
true => frame.process_read(&CONTEXT.read().unwrap()),
false => 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;
}
}
}
});
}
}
There are also examples for Serial-RTU, Serial-ASCII and UDP in examples
folder (if you're reading this text somewhere else, visit rmodbus project
repository.
Launch the examples as:
cargo run --example app
cargo run --example tcpserver
Modbus context
The rule is simple: one standard Modbus context per application. 10k+10k 16-bit
registers and 10k+10k coils are usually more than enough. This takes about
59Kbytes of RAM.
rmodbus server context is thread-safe, easy to use and has a lot of functions.
The context must be protected with a mutex/rwlock and every time Modbus context
is accessed, a context mutex must be locked. This slows down performance, but
guarantees that the context always has valid data after bulk-sets and after
writes of long data types. So make sure your application locks context only
when required and only for a short period time.
A simple PLC example:
use std::error::Error;
use std::fs::File;
use std::io::{Read, Write};
use rmodbus::server::context::ModbusContextFull;
#[path = "servers/tcp.rs"]
mod srv;
fn looping() {
println!("Loop started");
loop {
let ctx = srv::CONTEXT.read().unwrap();
let _param1 = ctx.get_holding(1000).unwrap();
let _param2 = ctx.get_holdings_as_f32(1100).unwrap(); let _param3 = ctx.get_holdings_as_u32(1200).unwrap(); let cmd = ctx.get_holding(1500).unwrap();
drop(ctx);
if cmd != 0 {
println!("got command code {}", cmd);
let mut ctx = srv::CONTEXT.write().unwrap();
ctx.set_holding(1500, 0).unwrap();
match cmd {
1 => {
println!("saving memory context");
let _ = save("/tmp/plc1.dat", &ctx).map_err(|_| {
eprintln!("unable to save context!");
});
}
_ => println!("command not implemented"),
}
}
let mut ctx = srv::CONTEXT.write().unwrap();
ctx.set_coil(0, true).unwrap();
ctx.set_holdings_bulk(10, &(vec![10, 20])).unwrap();
ctx.set_inputs_from_f32(20, 935.77).unwrap();
}
}
fn save(fname: &str, ctx: &ModbusContextFull) -> Result<(), Box<dyn Error>> {
let config = bincode::config::standard();
let mut file = File::create(fname)?;
file.write(&bincode::encode_to_vec(ctx, config)?)?;
file.sync_all()?;
Ok(())
}
fn load(fname: &str, ctx: &mut ModbusContextFull) -> Result<(), Box<dyn Error>> {
let config = bincode::config::standard();
let mut file = File::open(fname)?;
let mut data: Vec<u8> = Vec::new();
file.read_to_end(&mut data)?;
(*ctx, _) = bincode::decode_from_slice(&data, config)?;
Ok(())
}
fn main() {
let unit_id = 1;
{
let mut ctx = srv::CONTEXT.write().unwrap();
let _ = load(&"/tmp/plc1.dat", &mut ctx).map_err(|_| {
eprintln!("warning: no saved context");
});
}
use std::thread;
thread::spawn(move || {
srv::tcpserver(unit_id, "localhost:5502");
});
looping();
}
To let the above program communicate with outer world, Modbus server must be up
and running in the separate thread, asynchronously or whatever is preferred.
no_std
rmodbus supports no_std mode. Most of the library code is written the way to
support both std and no_std.
For no_std, set the dependency as:
rmodbus = { version = "*", default-features = false }
Small context
The full Modbus context has 10000 registers of each type, which requires 60000
bytes total. For systems with small RAM amount there is a pre-defined small
context with 1000 registers:
use rmodbus::server::context::ModbusContextSmall;
Custom-sized context
Starting from the version 0.7 it is allowed to define context of any size using
generic constants. The generic constants order is: coils, discretes, inputs,
holdings.
E.g. let us define a context for 128 coils, 16 discretes, 0 inputs and 100
holdings:
use rmodbus::server::context::ModbusContext;
let context = ModbusContext::<128, 16, 0, 100>::new();
Vectors
Some of rmodbus functions use vectors to store result. Different vector types can be used:
- When the
std feature is enabled (default), std::vec::Vec can be used.
- With the
fixedvec feature, fixedvec::FixedVec can be used.
- With the
heapless feature, heapless::Vec can be used.
Modbus client
Modbus client is designed with the same principles as the server: the crate
gives frame generator / processor, while the frames can be read / written with
any source and with any required way.
TCP client Example:
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
use rmodbus::{client::ModbusRequest, guess_response_frame_len, ModbusProto};
fn main() {
let timeout = Duration::from_secs(1);
let mut stream = TcpStream::connect("localhost:5502").unwrap();
stream.set_read_timeout(Some(timeout)).unwrap();
stream.set_write_timeout(Some(timeout)).unwrap();
let mut mreq = ModbusRequest::new(1, ModbusProto::TcpUdp);
mreq.tr_id = 2;
let mut request = Vec::new();
mreq.generate_set_coils_bulk(0, &[true, true], &mut request)
.unwrap();
stream.write(&request).unwrap();
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();
if len > 6 {
let mut rest = vec![0u8; (len - 6) as usize];
stream.read_exact(&mut rest).unwrap();
response.extend(rest);
}
mreq.parse_ok(&response).unwrap();
mreq.generate_get_coils(0, 2, &mut request).unwrap();
stream.write(&request).unwrap();
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();
if len > 6 {
let mut rest = vec![0u8; (len - 6) as usize];
stream.read_exact(&mut rest).unwrap();
response.extend(rest);
}
let mut data = Vec::new();
mreq.parse_bool(&response, &mut data).unwrap();
for i in 0..data.len() {
println!("{} {}", i, data[i]);
}
}