分别使用netty和apache.plc4x测试读取modbus协议的设备信号

记录一下常见的工业协议数据读取方法

目录

  • 前言
  • Modbus协议说明
  • Netty 读取测试
  • 使用plc4x 读取测试
  • 结束语

前言

Modbus 是一种通讯协议,用于在工业控制系统中进行数据通信和控制。Modbus 协议主要分为两种常用的变体:Modbus RTU 和 Modbus TCP/IP

  • Modbus RTU:Modbus RTU 是一种基于串行通信的协议。

  • Modbus TCP/IP:Modbus TCP/IP 是一种基于 TCP/IP 网络的协议。
    本次使用TCP协议,一般常见使用这种协议。

Modbus 协议一般工业设备例如光电信号,各类传感器和执行器等。
一些电力设备(如变压器、开关设备、仪表等)

Modbus协议说明

如果要使用netty读取modbus协议数据必须了解一下协议报文格式。

参考: https://neugates.io/docs/zh/latest/appendix/protocol/modbus_tcp.html

如果设备数量< 30个可以尝试使用 Neuro 产品读取,里面包含配置监控SDK等。

Modbus=MBAP(报文头)+PDU(帧结构)

Netty 读取测试

假设有一个光电IO模块对接了8个激光设备,激光扫描到障碍物为1 没有扫到位0,先通过厂家自带的web端管理界面查看目前的实际信号情况:
在这里插入图片描述

这边测试的Netty代码如下:


package org.example.modbus;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

public class ModbusClient {

    private final String host;
    private final int port;

    public ModbusClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new ChannelInitializer<Channel>() {
                        @Override
                        protected void initChannel(Channel ch) {
                            ch.pipeline()
                                    .addLast(new ModbusClientHandler());
                        }
                    });

            ChannelFuture f = b.connect(host, port).sync();
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

    private static class ModbusClientHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            // writeShort 一次写2个字节   writeByte 一次写1个字节
            ByteBuf buffer = Unpooled.buffer();

            // >>>>>>>>>>>>构造 M B A P Header(报文头)<<<<<<<<<<<<<
            // 事务标识符 占用2个字节
            // 可以解释为报文的序列号,例如测试使用的 Modbus Poll 客户端一直发送数据,
            // 所以每发送一次数据标识符就加一。服务器接收时会把这个数据原封返回。
            buffer.writeShort(1);

            // 协议类型 占用2个字节, 十六进制格式"00 00" 表示Modbus TCP 协议
            buffer.writeShort(0);

            // 长度 占用2个字节, 6 表示报文长度(后面有6个字节),包括 M B A P Header 和 PDU
            // 表示从单元标识符开始后面数据的长度。如:00 06 表示后面有 0X06 个字节长度的数据。
            buffer.writeShort(6);

            // 单元标识符 占用1个字节, 17 表示设备存储单元编号
            buffer.writeByte(17);

            // >>>>>>>>>>>>构造 PDU PDU=功能码+数据<<<<<<<<<<<<<
            // 功能码 占用1个字节, 02 表示读离散量输入
            buffer.writeByte(2);

            // 开始读的数据的地址。从 00 32 开始读数据。
            buffer.writeShort(32);

            // 读取的寄存器数量。从开始位置读 00 08 个寄存器数据。
            buffer.writeShort(8);
            ctx.writeAndFlush(buffer);
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            ByteBuf buffer = (ByteBuf) msg;
            byte[] response = new byte[buffer.readableBytes()];
            buffer.readBytes(response);
            System.out.println("Response: " + bytesToHex(response));
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            ctx.close();
        }
    }

    private static String bytesToHex(byte[] bytes) {
        // 这个 bytesToHex 方法用于将一个 byte 数组转换为十六进制格式的字符串。
        // 每个字节被转换为两个十六进制字符,并用空格分隔,
        // 最终返回一个表示十六进制表示形式的字符串
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b));
        }
        return sb.toString();
    }

    public static void main(String[] args) throws InterruptedException {
        String host = "192.168.50.41";
        int port = 28899;
        new ModbusClient(host, port).start();
    }
}

光电IO设备模块IP地址为 192.168.50.41 端口使用 28899,上面代码是使用netty 向IO模块发送读取 8个光电的离散量信号报文,地址从32开始,然后获得modbus协议的结果报文,最终获得的结果报文解析成十六进制的字符串形式如下:

Response: 00 01 00 00 00 04 11 02 01 CD 

还是建议参考上面推荐的文档,这里截取主要信息:
在这里插入图片描述
根据上面的样例说明,我们其实想要得到的结果是最后2位16进制数据 DD,占据1个字节,因为我们读取的是离散值(类似true或fase 一般是1或者0),因此我们将 CD转换为二进制数据:

1 1 0 1 1 1 0 1

从低位开始(从右 至 左) 对应厂家web管理界面中的 DI-1 DI-2 …
绿色=1 灰色=0
在这里插入图片描述
可以发现netty读取到的信号和厂家web管理界面显示的数据一致。

使用plc4x 读取测试

apache旗下工业协议适配工具库,具体文档查看官网:
链接: https://plc4x.apache.org/users/protocols/modbus.html

pom.xml文件引入maven依赖包:

    <properties>
        <plc4x.version>0.12.0</plc4x.version>
    </properties>
    
	 <dependency>
		<groupId>org.apache.plc4x</groupId>
		<artifactId>plc4j-api</artifactId>
		<version>${plc4x.version}</version>
	</dependency>

	<dependency>
		<groupId>org.apache.plc4x</groupId>
		<artifactId>plc4j-driver-modbus</artifactId>
		<version>${plc4x.version}</version>
	</dependency>

代码如下:

package cn.guzt.modbustest;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.plc4x.java.api.PlcConnection;
import org.apache.plc4x.java.api.PlcDriverManager;
import org.apache.plc4x.java.api.messages.PlcReadRequest;
import org.apache.plc4x.java.api.messages.PlcReadResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * apache PLX modbus测试用例
 *
 * @author guzt
 */
public class ModbusExample {

    protected static final Logger logger = LoggerFactory.getLogger(ModbusExample.class);

    public static void main(String[] args) {
        String ip = "192.168.50.41";
        String port = "28899";
        // 单元标识符:相当于设备的地址
        String unitIdentifier = "17";
        String timeout = "5000";
        String urlFormat = "modbus-tcp:tcp://{}:{}?unit-identifier={}&request-timeout={}";

        // try里面会自动关闭连接
        try (PlcConnection plcConnection = PlcDriverManager
                .getDefault()
                .getConnectionManager()
                .getConnection(StrUtil.format(urlFormat, ip, port, unitIdentifier, timeout))) {
            // Check if this connection support reading of data.
            if (!plcConnection.getMetadata().isReadSupported()) {
                logger.info(">>>>>>>>>>>>>>This connection doesn't support reading.");
                return;
            }
            // Check if this connection support writing of data.
            if (!plcConnection.getMetadata().isWriteSupported()) {
                logger.info(">>>>>>>>>>>>>>This connection doesn't support writing.");
                return;
            }

            if (plcConnection.isConnected()) {
                logger.info(">>>>>>>>>>>>>>Modbus已经连上..............");
            }
            // Create a new read request:
            // You will need to pass the reference you are asking for
            PlcReadRequest.Builder builder = plcConnection.readRequestBuilder();
            // 一次性读取几个寄存器里面的内容
            int count = 8;
            // 这里面的起始地址为实际为 32,传递参数时候加1
            int startAddress = 33;
            for (int i = 0; i < count; i++) {
                // 功能码 (tagAddress) Modbus 的操作对象有四种:线圈、离散输入、输入寄存器、保持寄存器。
                // 1. 线圈:相当于开关,在 Modbus 中可读可写,数据只有 00 和 01。
                // 2. 离散量:输入位,开关量,在 Modbus 中只读。
                // 3. 输入寄存器:只能从模拟量输入端改变的寄存器,在 Modbus 中只读。
                // 4. 保持寄存器:用于输出模拟量信号的寄存器,在 Modbus 中可读可写。
                // 查看参考:https://neugates.io/docs/zh/latest/appendix/protocol/modbus_tcp.html
                // 不同功能码对应不同的地址格式:参看 org.apache.plc4x.java.modbus.base.tag.ModbusTagHandler
                builder.addTagAddress("第" + (i + 1) + "个光电信号:", "discrete-input:" + (startAddress + i));
            }

            // 这种方式一次性读取8个:builder.addTagAddress("DI-count8N", "discrete-input:33:BOOL[8]")
            PlcReadRequest readRequest = builder.build();
            logger.info(">>>>>>>>>>>>>>开始读取");

            // Execute the request
            PlcReadResponse response = readRequest.execute().get();
            // Handle the response
            // 创建了一个写请求,尝试将地址1的线圈设置为true
            for (String fieldName : response.getTagNames()) {
                if (response.getObject(fieldName) instanceof Boolean) {
                    logger.info(">>>>>>>>>>>>>>Boolean[" + fieldName + "]: " + response.getBoolean(fieldName));
                } else if (ArrayUtil.isArray(response.getObject(fieldName))) {
                    logger.info(">>>>>>>>>>>>>>Array[" + fieldName + "]: " + response.getObject(fieldName));
                } else {
                    logger.info(">>>>>>>>>>>>>>Object[" + fieldName + "]: " + response.getObject(fieldName));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行打印结果如下:

...
16:20:25.378 [main] INFO cn.guzt.modbustest.ModbusExample - >>>>>>>>>>>>>>Boolean[第1个光电信号:]: true
16:20:25.378 [main] INFO cn.guzt.modbustest.ModbusExample - >>>>>>>>>>>>>>Boolean[第2个光电信号:]: false
16:20:25.378 [main] INFO cn.guzt.modbustest.ModbusExample - >>>>>>>>>>>>>>Boolean[第3个光电信号:]: true
16:20:25.378 [main] INFO cn.guzt.modbustest.ModbusExample - >>>>>>>>>>>>>>Boolean[第4个光电信号:]: true
16:20:25.378 [main] INFO cn.guzt.modbustest.ModbusExample - >>>>>>>>>>>>>>Boolean[第5个光电信号:]: true
16:20:25.378 [main] INFO cn.guzt.modbustest.ModbusExample - >>>>>>>>>>>>>>Boolean[第6个光电信号:]: false
16:20:25.378 [main] INFO cn.guzt.modbustest.ModbusExample - >>>>>>>>>>>>>>Boolean[第7个光电信号:]: true
16:20:25.378 [main] INFO cn.guzt.modbustest.ModbusExample - >>>>>>>>>>>>>>Boolean[第8个光电信号:]: true

...

从第1行至第8行记录值 对应厂家web管理界面中的 DI-1 DI-2 …
绿色=true 灰色=fase
在这里插入图片描述
可以发现plc4x读取到的信号和厂家web管理界面显示的数据一致。

结束语

读取Modbus的开源库有很多,这里列举常见的使用库,尤其是 Plc4x 这个适配了主流的工业协议,值得我们去研究。

使用netty的话对基本功要求比较高,如果对modbus工业协议包括TCP/IP协议一知半解估计应该是写不出成功案例。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/750580.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于51单片机太阳能风能风光互补路灯控制器

一.硬件方案 本设计由STC89C52单片机电路太阳能电池板电路风机发电电路锂电池充电保护电路升压电路稳压电路光敏电阻电路4位高亮LED灯电路2档拨动开关电路电源电路设计而成。 二.设计功能 &#xff08;1&#xff09;采用风机和太阳能电池板给锂电池充电&#xff0c;具有充电…

微服务开发 —— 项目环境搭建篇

环境搭建 Linux 环境搭建 Linux 环境搭建大家可以使用虚拟机 VMware、VirtualBox 等应用创建虚拟机&#xff0c;使用Vagrant也可以快捷搭建虚拟环境&#xff1b;Windows 中有 WSL2&#xff0c;Windows 中的 Docker 也对 WSL 进行了支持&#xff0c;也是一个不错的选择。或者可…

麒麟系统安装Redis

一、背景 如前文&#xff08;《麒麟系统安装MySQL》&#xff09;所述。 二、下载Redis源码 官方未提供麒麟系统的Redis软件&#xff0c;须下载源码编译。 下载地址&#xff1a;https://redis.io/downloads 6.2.14版本源码下载地址&#xff1a;https://download.redis.io/re…

构建LangChain应用程序的示例代码:46、使用 Meta-Prompt 构建自我改进代理的 LangChain 实现

Meta-Prompt 实现 摘要&#xff1a; 本文介绍了 Noah Goodman 提出的 Meta-Prompt 方法的 LangChain 实现&#xff0c;该方法用于构建能够自我反思和改进的智能代理。 核心思想&#xff1a; Meta-Prompt 的核心思想是促使代理反思自己的性能&#xff0c;并修改自己的指令。…

降低IT运营成本,提升客户体验 |LinkSLA亮相第十届CDIE

6月25-26日&#xff0c;中国数字化创新博览会&#xff08;CDIE 2024&#xff09;在上海张江科学会堂举行。本届展览主题为“AI创新&#xff0c;引领商业增长新格局”&#xff0c;旨在交流企业在数字化时代&#xff0c;如何以科技为驱动&#xff0c;在转型中如何把握机遇&#x…

文本编辑命令和正则表达式

一、 编辑文本的命令 正则表达式匹配的是文本内容&#xff0c;Linux的文本三剑客&#xff0c;都是针对文本内容。 文本三剑客 grep&#xff1a;过滤文本内容 sed&#xff1a;针对文本内容进行增删改查 &#xff08;本文不相关&#xff09; awk&#xff1a;按行取列 &#x…

Web服务器与Apache(虚拟主机基于ip、域名和端口号)

一、Web基础 1.HTML概述 HTML&#xff08;Hypertext Markup Language&#xff09;是一种标记语音,用于创建和组织Web页面的结构和内容&#xff0c;HTML是构建Web页面的基础&#xff0c;定义了页面的结构和内容&#xff0c;通过标记和元素来实现 2.HTML文件结构 <html>…

Transformer教程之什么是Transformer

在过去的几年里&#xff0c;Transformer 模型已经成为了自然语言处理&#xff08;NLP&#xff09;领域的主流技术。无论是机器翻译、文本生成还是语音识别&#xff0c;Transformer 都表现出了非凡的性能。那么&#xff0c;什么是 Transformer&#xff1f;它是如何工作的&#x…

LeetCode 剑指 Offer 40

// void help(int[] a,int l,int r,int k){ // if(k0) return; // if(r-l1 < k){ // for(int il;i<r;i){ // ans[cnt] a[i]; // } // return; // } // // 快排的基准值 // int base a[l]; // int i l, j r; // while(i<j){ // while(i<j &&…

1961 Springboot自习室预约系统idea开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 springboot 自习室预约管理系统是一套完善的信息系统&#xff0c;结合springboot框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用springboot框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数据库…

鸿蒙开发系统基础能力:【Timer (定时器)】

定时器 setTimeout setTimeout(handler[,delay[,…args]]): number 设置一个定时器&#xff0c;该定时器在定时器到期后执行一个函数。 参数 参数名类型必填说明handlerFunction是定时器到期后执行函数。delaynumber否延迟的毫秒数&#xff0c;函数的调用会在该延迟之后发生…

谷歌SEO在外贸推广中的应用效果如何?

谷歌SEO在外贸推广中非常有效。通过优化网站&#xff0c;可以提高在搜索结果中的排名&#xff0c;这意味着更多的潜在客户会看到你的产品和服务。 一个高排名的网站能带来更多自然流量&#xff0c;不需要花费广告费用。这种流量通常质量较高&#xff0c;因为用户是主动搜索相关…

Java 流式编程的7个技巧,必学!

作为Java开发者&#xff0c;我们还没有完全掌握Java Streams这个多功能工具的威力。在这里&#xff0c;你将发现一些有价值的技巧&#xff0c;可以作为参考并应用到你的下一个项目中。 Java Streams在很多年前就被引入了&#xff0c;但作为Java开发者&#xff0c;我们还没有完…

2.4G特技翻斗车方案定制

遥控翻斗车不仅能够提供基本的前进、后退、左转和右转功能&#xff0c;还设计有多种特技动作和互动模式&#xff0c;以增加娱乐性和互动性。 1、无线遥控&#xff1a;玩具翻斗车一般通过2.4G无线遥控器进行控制&#xff0c;允许操作者在一定距离内远程操控车辆。 2、炫彩灯光…

安装VEX外部编辑器

Houdini20配置VEX外部编辑器方法_哔哩哔哩_bilibili 下载并安装Visual Studio Code软件&#xff1a;Download Visual Studio Code - Mac, Linux, Windows 在Visual Studio Code软件内&#xff0c;安装相关插件&#xff0c;如&#xff1a; 中文汉化插件vex插件 安装Houdini Expr…

JavaScript中的Date对象,以及常用格式化日期的方法封装

一、Date对象 二、操作Date对象 1、创建Date对象 &#xff08;1&#xff09;常用方法 &#xff08;2&#xff09;使用示例 2、获取日期 &#xff08;1&#xff09;常用方法 &#xff08;2&#xff09;使用示例 3、设置日期 &#xff08;1&#xff09;常用方法 &…

LSTM时间序列基础学习

时间序列 时间序列可以是一维&#xff0c;二维&#xff0c;三维甚至更高维度的数据&#xff0c;在深度学习的世界中常见的是三维时间序列&#xff0c;这三个维度分别是&#xff08;batch_size,time_step,input_dimensions&#xff09;。 其中time_step是时间步&#xff0c;它…

智慧校园-就业管理系统总体概述

在当代教育与信息技术深度融合的背景下&#xff0c;智慧校园就业管理系统成为了连接学生、高校与企业的重要纽带&#xff0c;它以创新的服务理念和技术手段&#xff0c;重塑了职业规划与就业服务的传统模式。这一系统致力于为即将步入社会的学生们提供全面、个性化的支持&#…

C++系统编程篇——Linux第一个小程序--进度条

&#xff08;1&#xff09;先引入一个概念&#xff1a;行缓冲区 \r和\n \r表示回车 \n表示回车并换行 ①代码一 #include<stdio.h> #include<unistd.h> int main()…

MSA 助力实验室测量更稳定、更准确

在汽车制造、石油化工、电子制造等行业,产品的质量和性能需要通过准确的测量来保证。但是由于测量设备的误差、操作人员的主观影响以及环境条件的干扰等因素会导致测量系统出现各种问题,且这些问题会导致测量结果不准确,从而影响产品质量。 随着工业信息化的迅速发展, 各行业对…