48 个版本
0.9.8 | 2024 年 7 月 20 日 |
---|---|
0.9.6 | 2024 年 6 月 8 日 |
0.9.4 | 2024 年 3 月 29 日 |
0.8.0 | 2023 年 12 月 8 日 |
0.5.2 | 2020 年 11 月 26 日 |
#31 在 硬件支持 中
每月 214 次下载
用于 5 个包
165KB
3.5K SLoC
rmodbus - Rust 的 Modbus
构建快速且可靠的 Modbus 应用程序的框架。
什么是 rmodbus
rmodbus 不是一个普通的 Modbus 客户端/服务器。rmodbus 是一组用于快速构建 Modbus 应用程序的工具。可以将 rmodbus 视为一个请求/响应编解码器,以及上下文管理器。
rmodbus 是 EVA ICS v4 工业自动化堆栈和 RoboPLC I/O 的一部分。
为什么还需要另一个 Modbus 库?
- rmodbus 传输和协议无关
- rmodbus 平台无关(
no_std
完全支持!) - 可以轻松用于阻塞和异步(非阻塞)应用程序
- 针对速度和可靠性进行了优化
- 提供了一套易于使用 Modbus 上下文的工具
- 支持 Modbus TCP/UDP、RTU 和 ASCII 的客户端/服务器帧处理
- 可以轻松管理、导入和导出服务器上下文
所以没有包含服务器?
是的,没有包含服务器。您需要自行构建服务器。您可以选择传输协议、技术以及所有其他内容。rmodbus 只处理帧并与 Modbus 上下文交互。
对于同步服务器和客户端(std),我们建议使用 RoboPLC Modbus I/O 模块。
以下是一个简单的 TCP 阻塞服务器的示例
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;
}
}
}
});
}
}
在examples文件夹中也有Serial-RTU、Serial-ASCII和UDP的示例(如果您在其他地方阅读此文本,请访问rmodbus项目仓库)。
以以下方式启动示例
cargo run --example app
cargo run --example tcpserver
Modbus上下文
规则很简单:每个应用程序一个标准Modbus上下文。10k+10k的16位寄存器和10k+10k的线圈通常足够使用。这需要大约59K字节的RAM。
rmodbus服务器上下文是线程安全的,易于使用,并且具有很多功能。
上下文必须使用互斥锁/rw锁进行保护,每次访问Modbus上下文时,都必须锁定上下文互斥锁。这会降低性能,但保证了在批量设置和写入长数据类型之后,上下文总是有有效数据。因此,请确保您的应用程序仅在需要时且仅短时间锁定上下文。
一个简单的PLC示例
use std::error::Error;
use std::fs::File;
use std::io::{Read, Write};
use rmodbus::server::{storage::ModbusStorageFull, context::ModbusContext};
#[path = "../examples/servers/tcp.rs"]
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.get_holding(1000).unwrap();
let _param2 = ctx.get_holdings_as_f32(1100).unwrap(); // ieee754 f32
let _param3 = ctx.get_holdings_as_u32(1200).unwrap(); // u32
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"),
}
}
// ==============================================
// DO SOME JOB
// ..........
// WRITE RESULTS
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: &ModbusStorageFull) -> 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 ModbusStorageFull) -> 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() {
// read context
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();
}
为了让上述程序与外部世界通信,Modbus服务器必须在单独的线程中启动并运行,异步或任何其他首选方式。
无标准
rmodbus支持no_std
模式。大多数库代码都是按照支持std
和no_std
的方式编写的。
对于no_std
,将依赖项设置为
rmodbus = { version = "*", default-features = false }
小存储
完整的Modbus存储具有每种类型10000个寄存器,总共需要60000字节。对于内存较小的系统,有一个预定义的小存储,具有1000个寄存器。
use rmodbus::server::{storage::ModbusStorageSmall, context::ModbusContext};
自定义大小存储
从版本0.7开始,可以使用泛型常量定义任何大小的存储。泛型常量的顺序是:线圈、离散、输入、保持。
例如,让我们定义一个具有128个线圈、16个离散、0个输入和100个保持的上下文。
use rmodbus::server::{storage::ModbusStorage, context::ModbusContext};
let context = ModbusStorage::<128, 16, 0, 100>::new();
自定义服务器实现
从版本0.9开始,可以通过实现自定义结构体上的use rmodbus::server::context::ModbusContext
来提供自定义服务器实现。有关示例实现,请参阅src/server/storage.rs
。
向量
一些rmodbus函数使用向量来存储结果。可以使用不同的向量类型。
-
当启用
std
特性(默认)时,可以使用std::vec::Vec
。 -
使用
fixedvec
特性时,可以使用fixedvec::FixedVec
。 -
使用
heapless
特性时,可以使用heapless::Vec
。 -
当启用
alloc
特性时,在no-std模式下可以使用Rust核心分配alloc::vec::Vec
。例如,使用以下命令构建no-std模式,并支持使用核心分配alloc::vec::Vec
:cargo build --no-default-features --features alloc
。当启用std
特性时,忽略alloc
特性。
Modbus客户端
Modbus客户端的设计原理与服务器相同:容器提供帧生成器/处理器,而帧可以通过任何来源和任何需要的方式读取/写入。
TCP客户端示例
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);
// 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]);
}
}
关于作者
波希米亚自动化 / Altertech 是一家拥有 15 年以上企业自动化和工业物联网经验的公司集团。我们的系统包括发电厂、工厂和城市基础设施。其中最大的系统拥有 1M+ 个传感器和受控设备,而且这个标准每天都在提高。
依赖项
~74–470KB