#industrial-automation #modbus #server #server-framework #hardware #no-std

no-std rmodbus

快速且平台无关的 Modbus 服务器框架

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硬件支持

Download history 40/week @ 2024-04-29 38/week @ 2024-05-06 49/week @ 2024-05-13 90/week @ 2024-05-20 162/week @ 2024-05-27 447/week @ 2024-06-03 290/week @ 2024-06-10 143/week @ 2024-06-17 119/week @ 2024-06-24 165/week @ 2024-07-01 171/week @ 2024-07-08 140/week @ 2024-07-15 105/week @ 2024-07-22 40/week @ 2024-07-29 30/week @ 2024-08-05 18/week @ 2024-08-12

每月 214 次下载
用于 5 个包

Apache-2.0

165KB
3.5K SLoC

rmodbus - Rust 的 Modbus crates.io 页面 docs.rs 页面 GitHub Actions CI

构建快速且可靠的 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模式。大多数库代码都是按照支持stdno_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::Veccargo 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