如何通过Lua Sockets实现Ethernet/IP上的显式消息通信?
使用Lua Sockets实现Ethernet/IP Class 3显式消息通信(Lua 5.1)
核心前提:明确Ethernet/IP显式消息结构
Ethernet/IP的Class 3显式消息基于TCP协议,核心是按照CIP(通用工业协议)规范打包请求/响应帧,主要包含:
- TCP传输层(Lua Sockets自动处理)
- CIP封装层头部
- CIP路由路径
- CIP服务请求码
- 业务数据段
第一步:搭建Lua Sockets基础环境
先确保Lua 5.1环境已安装Lua Sockets模块,脚本中引入并建立TCP连接(Ethernet/IP默认端口为44818):
local socket = require("socket") -- 建立TCP连接到伺服驱动器 local tcp = socket.tcp() tcp:settimeout(1000) -- 设置1秒超时,按需调整 local ok, err = tcp:connect("192.168.1.100", 44818) -- 替换为你的设备IP if not ok then print("连接失败:" .. err) return end
第二步:构造CIP封装层头部
封装层是Ethernet/IP的外层包装,必须严格遵循规范,核心字段包括命令码、数据长度、会话句柄等:
-- 构造Send RR Data(请求响应数据)类型的封装头部 local encaps_header = string.char(0x00, 0x00) -- 命令码:Send RR Data encaps_header = encaps_header .. string.char(0x00, 0x00) -- 数据长度(后续计算后替换) encaps_header = encaps_header .. string.char(0x00, 0x00, 0x00, 0x00) -- 会话句柄(初始设为0) encaps_header = encaps_header .. string.char(0x00, 0x00, 0x00, 0x00) -- 状态码(请求时设为0) encaps_header = encaps_header .. string.char(0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00) -- Sender Context(自定义,默认0) encaps_header = encaps_header .. string.char(0x00, 0x00) -- 选项(默认0)
第三步:构造CIP请求数据
这部分是核心,必须根据伺服驱动器的官方手册确定以下参数:
- 服务码:比如读取单个属性用
0x0E(Get Attribute Single) - 类ID:伺服驱动器的目标数据类(比如驱动器状态类可能是
0x20) - 实例ID:类的实例编号(一般为
0x01) - 属性ID:要读取的具体数据项(比如位置反馈可能是
0x47) - 路由路径:设备的CIP访问路径(示例为
0x20,0x04,0x24,0x01,需按设备手册调整)
将这些参数打包为二进制字符串:
local service_code = string.char(0x0E) -- Get Attribute Single服务 local class_id = string.char(0x20, 0x00) -- 替换为目标类ID local instance_id = string.char(0x01, 0x00) -- 替换为实例ID local attribute_id = string.char(0x47, 0x00) -- 替换为属性ID local route_path = string.char(0x20, 0x04, 0x24, 0x01) -- 替换为设备路由路径 -- 拼接完整CIP请求数据 local cip_request = service_code .. class_id .. instance_id .. attribute_id .. route_path
第四步:发送完整请求帧
更新封装头部的长度字段,拼接完整帧后通过TCP发送:
-- 更新封装头部的长度字段 local cip_len = #cip_request encaps_header = string.sub(encaps_header, 1, 2) .. string.char(math.floor(cip_len/256), cip_len%256) .. string.sub(encaps_header, 5) -- 拼接完整请求帧并发送 local request_frame = encaps_header .. cip_request local bytes_sent, err = tcp:send(request_frame) if not bytes_sent then print("发送失败:" .. err) tcp:close() return end
第五步:接收并解析响应
接收响应后先校验封装层状态,再解析CIP业务数据:
-- 接收封装层头部(固定24字节) local resp_header, err = tcp:receive(24) if not resp_header then print("接收头部失败:" .. err) tcp:close() return end -- 校验封装层状态码 local status = string.byte(resp_header, 8) + string.byte(resp_header,9)*256 + string.byte(resp_header,10)*65536 + string.byte(resp_header,11)*16777216 if status ~= 0 then print("请求错误,状态码:" .. string.format("0x%08X", status)) tcp:close() return end -- 接收CIP响应数据 local resp_data_len = string.byte(resp_header,3)*256 + string.byte(resp_header,4) local resp_data, err = tcp:receive(resp_data_len) if not resp_data then print("接收数据失败:" .. err) tcp:close() return end -- 解析业务数据(示例:4字节整数位置反馈) local resp_service = string.byte(resp_data,1) if resp_service == 0x0E + 0x80 then -- 服务响应码为请求码+0x80表示成功 local position = string.byte(resp_data,5) + string.byte(resp_data,6)*256 + string.byte(resp_data,7)*65536 + string.byte(resp_data,8)*16777216 print("当前位置反馈:" .. position) else print("服务响应错误:" .. string.format("0x%02X", resp_service)) end
第六步:实现轮询逻辑
将发送-接收逻辑放入循环,设置轮询间隔(注意保持TCP长连接以提升效率):
local poll_interval = 100 -- 100毫秒轮询一次 while true do -- 复用之前的TCP连接,重复执行发送、接收、解析逻辑 -- ... 省略重复代码 ... socket.sleep(poll_interval/1000) -- sleep函数接收秒为单位 end
关键注意事项
- 设备手册是核心:不同品牌伺服驱动器的类ID、属性ID等参数差异极大,必须严格参考设备的Ethernet/IP或CIP文档。
- 二进制字节序:Ethernet/IP多数采用小端字节序,处理多字节数据时需注意顺序,可借助
struct模块(Lua 5.1需额外安装)简化打包解包。 - 错误处理:必须处理连接超时、发送失败、接收超时等异常,避免脚本崩溃。
- 长连接优化:Class 3显式消息支持TCP长连接,不要每次轮询都重建连接。
内容的提问来源于stack exchange,提问作者Mike Cardoso
相关产品推荐
相关产品推荐

