A framework to build fast and reliable Modbus-powered applications.
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.
no_std
is fully supported!)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:
```rust,ignore use std::io::{Read, Write}; use std::net::TcpListener; use std::thread;
use lazystatic::lazystatic;
use std::sync::RwLock;
use rmodbus::{ server::{context::ModbusContextFull, ModbusFrame}, ModbusFrameBuf, ModbusProto, };
lazy_static! {
pub static ref CONTEXT: RwLock
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).unwrapor(0) == 0 { return; } let mut frame = ModbusFrame::new(unit, &buf, ModbusProto::TcpUdp, &mut response); if frame.parse().iserr() { println!("server error"); return; } if frame.processingrequired { let result = match frame.readonly { true => frame.processread(&CONTEXT.read().unwrap()), false => frame.processwrite(&mut CONTEXT.write().unwrap()), }; if result.iserr() { println!("frame processing error"); return; } } if frame.responserequired { frame.finalizeresponse().unwrap(); println!("{:x?}", response.asslice()); if stream.write(response.asslice()).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:
shell
cargo run --example app
cargo run --example tcpserver
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:
```rust,ignore use std::error::Error; use std::fs::File; use std::io::{Read, Write};
use rmodbus::server::context::ModbusContextFull;
mod srv;
// put 1 to holding register 1500 to save current context to /tmp/plc1.dat // if the file exists, context will be loaded at the next start
fn looping() { println!("Loop started"); loop { // READ WORK MODES ETC let ctx = srv::CONTEXT.read().unwrap(); let param1 = ctx.getholding(1000).unwrap(); let param2 = ctx.getholdingsasf32(1100).unwrap(); // ieee754 f32 let param3 = ctx.getholdingsasu32(1200).unwrap(); // u32 let cmd = ctx.getholding(1500).unwrap(); drop(ctx); if cmd != 0 { println!("got command code {}", cmd); let mut ctx = srv::CONTEXT.write().unwrap(); ctx.setholding(1500, 0).unwrap(); match cmd { 1 => { println!("saving memory context"); let _ = save("/tmp/plc1.dat", &ctx).maperr(|| { eprintln!("unable to save context!"); }); } _ => println!("command not implemented"), } } // ============================================== // DO SOME JOB // .......... // WRITE RESULTS let mut ctx = srv::CONTEXT.write().unwrap(); ctx.setcoil(0, true).unwrap(); ctx.setholdingsbulk(10, &(vec![10, 20])).unwrap(); ctx.setinputsfromf32(20, 935.77).unwrap(); } }
fn save(fname: &str, ctx: &ModbusContextFull) -> Result<(), Box
fn load(fname: &str, ctx: &mut ModbusContextFull) -> Result<(), Box
fn main() { // read context let unitid = 1; { let mut ctx = srv::CONTEXT.write().unwrap(); let _ = load(&"/tmp/plc1.dat", &mut ctx).maperr(|| { eprintln!("warning: no saved context"); }); } use std::thread; thread::spawn(move || { srv::tcpserver(unitid, "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:
toml
rmodbus = { version = "*", default-features = false }
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:
rust
use rmodbus::server::context::ModbusContextSmall;
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:
```rust use rmodbus::server::context::ModbusContext;
let context = ModbusContext::<128, 16, 0, 100>::new(); ```
Some of rmodbus functions use vectors to store result. Different vector types can be used:
std
feature is enabled (default), std::vec::Vec
can be used.fixedvec
feature, fixedvec::FixedVec
can be used.heapless
feature, heapless::Vec
can be used.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:
```rust,ignore use std::io::{Read, Write}; use std::net::TcpStream; use std::time::Duration;
use rmodbus::{client::ModbusRequest, guessresponseframe_len, ModbusProto};
fn main() { 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; // just for test, default tr_id is 1
// 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();
// get coil values back
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();
// check if frame has no Modbus error inside and parse response bools into data vec
mreq.parse_bool(&response, &mut data).unwrap();
for i in 0..data.len() {
println!("{} {}", i, data[i]);
}
} ```