exmodbus_tcp.lua 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. -- 定义类结构;
  2. local modbus = {} -- 定义 modbus 实例的元表;
  3. modbus.__index = modbus -- 定义 modbus 实例的索引元方法,用于访问实例的属性;
  4. modbus.__metatable = "instance is protected" -- 定义 modbus 实例的元表,防止外部修改;
  5. -- 模块级变量:依赖注入的引用;
  6. local exmodbus_ref -- 主模块引用,用于访问enqueue_request等核心功能;
  7. local gen_id_func -- ID生成函数引用,用于生成唯一请求ID;
  8. -- Modbus TCP 协议头长度
  9. local MODBUS_TCP_HEADER_LEN = 7
  10. local libnet = require "libnet"
  11. -- 创建 modbus 实例的构造函数;
  12. function modbus:new(config, TASK_NAME)
  13. local obj = {
  14. mode = config.mode, -- 通信模式
  15. adapter = config.adapter, -- 网络适配器
  16. ip_address = config.ip_address, -- IP 地址
  17. port = config.port, -- 端口号
  18. is_udp = config.is_udp, -- 是否使用 UDP 协议
  19. is_tls = config.is_tls, -- 是否使用 TLS 加密
  20. keep_idle = config.keep_idle, -- 连接空闲多长时间后,开始发送第一个 keepalive 探针报文(秒)
  21. keep_interval = config.keep_interval, -- 发送第一个探针后,如果没收到 ACK 回复,间隔多久再发送下一个探针(秒)
  22. keep_cnt = config.keep_cnt, -- 总共发送多少次探针后,如果依然没有回复,则判定连接已断开
  23. server_cert = config.server_cert, -- TCP模式下的服务器ca证书数据,UDP模式下的PSK
  24. client_cert = config.client_cert, -- TCP模式下的客户端证书数据,UDP模式下的PSK-ID
  25. client_key = config.client_key, -- TCP模式下的客户端私钥加密数据
  26. client_password = config.client_password, -- TCP模式下的客户端私钥口令数据
  27. }
  28. -- 从站请求处理回调函数;
  29. obj.slaveHandler = nil
  30. -- 任务名称
  31. obj.TASK_NAME = TASK_NAME
  32. -- 接收数据缓冲区
  33. obj.recv_buff = nil
  34. -- 设置原表;
  35. setmetatable(obj, modbus)
  36. -- 返回实例;
  37. return obj
  38. end
  39. -- 解析 TCP 请求帧(从站使用)
  40. local function parse_tcp_request(data)
  41. -- 检查请求帧长度,至少包含 MBAP 头和功能码
  42. if #data < MODBUS_TCP_HEADER_LEN + 1 then
  43. log.error("exmodbus", "请求帧长度不足")
  44. return nil, "请求帧长度不足"
  45. end
  46. -- 解析 MBAP 头(事务标识符(2)、协议标识符(2)、数据长度(2)、从站地址(1))
  47. local transaction_id = string.unpack(">H", data, 1)
  48. local protocol_id = string.unpack(">H", data, 3)
  49. local length = string.unpack(">H", data, 5)
  50. local slave_id = string.unpack("B", data, 7)
  51. -- 检查数据长度是否与实际长度匹配
  52. if #data ~= 6 + length then
  53. log.error("exmodbus", "数据长度与实际长度不匹配")
  54. return nil, "数据长度与实际长度不匹配"
  55. end
  56. -- 检查协议 ID(Modbus TCP 协议 ID 必须为 0)
  57. if protocol_id ~= 0 then
  58. log.error("exmodbus", "无效的协议 ID")
  59. return nil, "无效的协议 ID"
  60. end
  61. -- 解析功能码
  62. local func_code = string.unpack("B", data, 8)
  63. local request = {
  64. transaction_id = transaction_id,
  65. protocol_id = protocol_id,
  66. length = length,
  67. slave_id = slave_id,
  68. func_code = func_code,
  69. reg_type = nil,
  70. start_addr = nil,
  71. reg_count = nil,
  72. data = {},
  73. }
  74. -- 根据功能码解析请求内容
  75. if func_code == exmodbus_ref.READ_COILS or func_code == exmodbus_ref.READ_DISCRETE_INPUTS then
  76. -- 读线圈或离散输入
  77. request.reg_type = func_code == exmodbus_ref.READ_COILS and exmodbus_ref.COIL_STATUS or exmodbus_ref.DISCRETE_INPUT_STATUS
  78. request.start_addr = string.unpack(">H", data, 9)
  79. request.reg_count = string.unpack(">H", data, 11)
  80. elseif func_code == exmodbus_ref.READ_HOLDING_REGISTERS or func_code == exmodbus_ref.READ_INPUT_REGISTERS then
  81. -- 读保持寄存器或输入寄存器
  82. request.reg_type = func_code == exmodbus_ref.READ_HOLDING_REGISTERS and exmodbus_ref.HOLDING_REGISTER or exmodbus_ref.INPUT_REGISTER
  83. request.start_addr = string.unpack(">H", data, 9)
  84. request.reg_count = string.unpack(">H", data, 11)
  85. elseif func_code == exmodbus_ref.WRITE_SINGLE_COIL then
  86. -- 写单个线圈
  87. request.reg_type = exmodbus_ref.COIL_STATUS
  88. request.start_addr = string.unpack(">H", data, 9)
  89. request.reg_count = 1
  90. local value = string.unpack(">H", data, 11)
  91. request.data = { [request.start_addr] = value == 0xFF00 and 1 or 0 }
  92. elseif func_code == exmodbus_ref.WRITE_SINGLE_HOLDING_REGISTER then
  93. -- 写单个寄存器
  94. request.reg_type = exmodbus_ref.HOLDING_REGISTER
  95. request.start_addr = string.unpack(">H", data, 9)
  96. request.reg_count = 1
  97. local value = string.unpack(">H", data, 11)
  98. request.data = { [request.start_addr] = value }
  99. elseif func_code == exmodbus_ref.WRITE_MULTIPLE_COILS then
  100. -- 写多个线圈
  101. request.reg_type = exmodbus_ref.COIL_STATUS
  102. request.start_addr = string.unpack(">H", data, 9)
  103. request.reg_count = string.unpack(">H", data, 11)
  104. -- local byte_count = string.unpack("B", data, 13)
  105. request.data = {}
  106. for i = 0, request.reg_count - 1 do
  107. local byte_pos = 13 + 1 + math.floor(i / 8)
  108. local bit_pos = i % 8
  109. local byte_value = string.unpack("B", data, byte_pos)
  110. local bit_value = bit.band(byte_value, bit.lshift(1, bit_pos)) > 0 and 1 or 0
  111. request.data[request.start_addr + i] = bit_value
  112. end
  113. elseif func_code == exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS then
  114. -- 写多个寄存器
  115. request.reg_type = exmodbus_ref.HOLDING_REGISTER
  116. request.start_addr = string.unpack(">H", data, 9)
  117. request.reg_count = string.unpack(">H", data, 11)
  118. -- local byte_count = string.unpack("B", data, 13)
  119. request.data = {}
  120. for i = 0, request.reg_count - 1 do
  121. local value = string.unpack(">H", data, 13 + 1 + i * 2)
  122. request.data[request.start_addr + i] = value
  123. end
  124. else
  125. log.error("exmodbus", "不支持的功能码:", func_code)
  126. end
  127. return request
  128. end
  129. -- 构建 Modbus TCP 响应帧(从站使用);
  130. local function build_tcp_response(request, user_return)
  131. local slave_id = request.slave_id
  132. local func_code = request.func_code
  133. -- 用户返回异常码 -> 异常响应;
  134. if type(user_return) == "number" then
  135. local exception_code = user_return
  136. local response_payload = string.char(slave_id, bit.bor(func_code, 0x80), exception_code)
  137. -- 构建完整的 TCP 响应帧
  138. local length = #response_payload
  139. local response = string.pack(">H", request.transaction_id) .. -- 事务 ID
  140. string.pack(">H", 0) .. -- 协议 ID
  141. string.pack(">H", length) .. -- 长度
  142. response_payload
  143. return response
  144. end
  145. -- 用户返回表 -> 正常响应;
  146. if type(user_return) ~= "table" then
  147. log.error("exmodbus", "从站回调必须返回 table 或 number,实际类型: ", type(user_return))
  148. return nil
  149. end
  150. local response_payload = ""
  151. -- 处理读线圈和读离散输入响应;
  152. if func_code == exmodbus_ref.READ_COILS or func_code == exmodbus_ref.READ_DISCRETE_INPUTS then
  153. local reg_count = request.reg_count
  154. -- 校验 reg_count 是否有效;
  155. if not reg_count or reg_count <= 0 then
  156. log.error("exmodbus", "请求中 reg_count 无效")
  157. return nil
  158. end
  159. local byte_count = math.ceil(reg_count / 8)
  160. local values = {}
  161. for i = 0, reg_count - 1 do
  162. local addr = request.start_addr + i
  163. local bit_val = user_return[addr]
  164. if bit_val == nil then
  165. log.error("exmodbus", "读线圈/离散输入回调未返回地址 ", addr, " 的数据")
  166. return nil
  167. end
  168. if bit_val ~= 0 and bit_val ~= 1 then
  169. log.error("exmodbus", "地址 ", addr, " 的值必须为 0 或 1,实际: ", bit_val)
  170. return nil
  171. end
  172. local byte_idx = math.floor(i / 8)
  173. if not values[byte_idx] then values[byte_idx] = 0 end
  174. if bit_val == 1 then
  175. values[byte_idx] = bit.bor(values[byte_idx], bit.lshift(1, i % 8))
  176. end
  177. end
  178. response_payload = string.char(slave_id, func_code, byte_count)
  179. for i = 0, byte_count - 1 do
  180. response_payload = response_payload .. string.char(values[i] or 0)
  181. end
  182. -- 处理读保持寄存器和读输入寄存器响应;
  183. elseif func_code == exmodbus_ref.READ_HOLDING_REGISTERS or func_code == exmodbus_ref.READ_INPUT_REGISTERS then
  184. local reg_count = request.reg_count
  185. -- 校验 reg_count 是否有效;
  186. if not reg_count or reg_count <= 0 then
  187. log.error("exmodbus", "请求中 reg_count 无效")
  188. return nil
  189. end
  190. local values = ""
  191. for i = 0, reg_count - 1 do
  192. local addr = request.start_addr + i
  193. local val = user_return[addr]
  194. if val == nil then
  195. log.error("exmodbus", "读保持寄存器/输入寄存器回调未返回地址 ", addr, " 的数据")
  196. return nil
  197. end
  198. if type(val) ~= "number" or val ~= math.floor(val) or val < 0 or val > 65535 then
  199. log.error("exmodbus", "地址 ", addr, " 的值必须为 0~65535 的整数,实际: ", val)
  200. return nil
  201. end
  202. values = values .. string.char((val >> 8) & 0xFF, val & 0xFF)
  203. end
  204. response_payload = string.char(slave_id, func_code, #values) .. values
  205. -- 处理写单个线圈响应;
  206. elseif func_code == exmodbus_ref.WRITE_SINGLE_COIL then
  207. local addr = request.start_addr
  208. -- 校验 start_addr 是否有效;
  209. if addr == nil then
  210. log.error("exmodbus", "请求中 start_addr 无效")
  211. return nil
  212. end
  213. local coil_val = (request.data and request.data[addr]) or 0
  214. local resp_val = (coil_val ~= 0) and 0xFF00 or 0x0000
  215. response_payload = string.char(slave_id, func_code) ..
  216. string.char((addr >> 8) & 0xFF, addr & 0xFF,
  217. (resp_val >> 8) & 0xFF, resp_val & 0xFF)
  218. -- 处理写单个保持寄存器响应;
  219. elseif func_code == exmodbus_ref.WRITE_SINGLE_HOLDING_REGISTER then
  220. local addr = request.start_addr
  221. -- 校验 start_addr 是否有效;
  222. if addr == nil then
  223. log.error("exmodbus", "请求中 start_addr 无效")
  224. return nil
  225. end
  226. local reg_val = (request.data and request.data[addr]) or 0
  227. -- 校验 reg_val 是否有效;
  228. if type(reg_val) ~= "number" or reg_val ~= math.floor(reg_val) or reg_val < 0 or reg_val > 65535 then
  229. log.error("exmodbus", "地址 ", addr, " 的值必须为 0~65535 的整数,实际: ", reg_val)
  230. return nil
  231. end
  232. response_payload = string.char(slave_id, func_code) ..
  233. string.char((addr >> 8) & 0xFF, addr & 0xFF,
  234. (reg_val >> 8) & 0xFF, reg_val & 0xFF)
  235. -- 处理写多个线圈/保持寄存器响应;
  236. elseif func_code == exmodbus_ref.WRITE_MULTIPLE_COILS or func_code == exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS then
  237. local start_addr = request.start_addr
  238. local reg_count = request.reg_count
  239. -- 校验 start_addr 和 reg_count 是否有效;
  240. if not start_addr or not reg_count or reg_count <= 0 then
  241. log.error("exmodbus", "请求中 start_addr 或 reg_count 无效")
  242. return nil
  243. end
  244. response_payload = string.char(slave_id, func_code) ..
  245. string.char((start_addr >> 8) & 0xFF, start_addr & 0xFF,
  246. (reg_count >> 8) & 0xFF, reg_count & 0xFF)
  247. -- 处理未知功能码,视为错误;
  248. else
  249. log.error("exmodbus", "不支持的功能码,且未返回异常码: ", func_code)
  250. return nil
  251. end
  252. -- 构建完整的 TCP 响应帧
  253. local length = #response_payload -- 长度包含从站ID
  254. local response = string.pack(">H", request.transaction_id) .. -- 事务 ID
  255. string.pack(">H", 0) .. -- 协议 ID
  256. string.pack(">H", length) .. -- 长度(包含从站ID)
  257. response_payload -- 从站ID + PDU数据
  258. return response
  259. end
  260. -- TCP 从站接收数据处理函数;
  261. local function tcp_receiver(netc, instance)
  262. -- 如果数据接收缓冲区还没有申请过空间,则先申请内存空间
  263. if instance.recv_buff == nil then
  264. instance.recv_buff = zbuff.create(1024)
  265. end
  266. -- 循环从内核的缓冲区读取接收到的数据
  267. while true do
  268. -- 从内核的缓冲区中读取数据到 instance.recv_buff 中
  269. local succ, param = socket.rx(netc, instance.recv_buff)
  270. -- 读取数据失败
  271. if not succ then
  272. log.info("exmodbus", "读取数据失败,已接收数据长度", param)
  273. return false
  274. end
  275. -- 如果读取到了数据
  276. if instance.recv_buff:used() > 0 then
  277. -- log.info("exmodbus", "已接收数据长度", instance.recv_buff:used())
  278. -- 读取数据
  279. local data = instance.recv_buff:query()
  280. -- 解析 TCP 请求帧
  281. local request, err = parse_tcp_request(data)
  282. if request then
  283. -- 广播地址(0)不响应;
  284. if request.slave_id == 0 then
  285. -- 调用回调以允许用户记录或处理广播命令(如写寄存器);
  286. if instance.slaveHandler then
  287. instance.slaveHandler(request)
  288. -- 注意:即使回调返回数据,也不发送响应;
  289. end
  290. -- 广播请求处理完毕,清除对应的报文数据
  291. local expected_len = request.length + MODBUS_TCP_HEADER_LEN - 1
  292. instance.recv_buff:del(0, expected_len)
  293. -- log.info("exmodbus", "广播请求处理完毕,清除报文长度:", expected_len)
  294. -- 广播请求处理完毕,不回复;
  295. break
  296. end
  297. if instance.slaveHandler then
  298. local user_return = instance.slaveHandler(request)
  299. local response = build_tcp_response(request, user_return)
  300. if response then
  301. libnet.tx(instance.TASK_NAME, 0, netc, response)
  302. sys.sendMsg(instance.TASK_NAME, socket.EVENT, 0)
  303. else
  304. log.error("exmodbus", "构建响应帧失败,从站地址:", request.slave_id)
  305. end
  306. -- 清除当前请求数据
  307. local expected_len = request.length + MODBUS_TCP_HEADER_LEN - 1
  308. instance.recv_buff:del(0, expected_len)
  309. -- log.info("exmodbus", "请求处理完毕,清除报文长度:", expected_len)
  310. else
  311. log.warn("exmodbus", "收到主站请求,但未注册回调函数")
  312. -- 清除当前请求数据
  313. local expected_len = request.length + MODBUS_TCP_HEADER_LEN - 1
  314. instance.recv_buff:del(0, expected_len)
  315. log.info("exmodbus", "清除报文长度:", expected_len)
  316. end
  317. else
  318. if err == "请求帧长度不足" then
  319. -- 请求帧长度不足,等待更多数据
  320. -- log.info("exmodbus", "请求帧长度不足,等待更多数据")
  321. break
  322. elseif err == "数据长度与实际长度不匹配" then
  323. -- 数据长度与实际长度不匹配,清空缓冲区
  324. -- log.warn("exmodbus", "数据长度与实际长度不匹配,清空缓冲区")
  325. instance.recv_buff:del()
  326. break
  327. elseif err == "协议 ID 错误" then
  328. -- 协议 ID 错误,清空缓冲区
  329. -- log.warn("exmodbus", "协议 ID 错误,清空缓冲区")
  330. instance.recv_buff:del()
  331. break
  332. end
  333. end
  334. else
  335. -- 没有数据可读
  336. break
  337. end
  338. end
  339. return true
  340. end
  341. local function tcp_slave_main_task_func(instance)
  342. local netc = nil
  343. local result, param
  344. while true do
  345. -- 创建 TCP 服务器
  346. netc = socket.create(instance.adapter, instance.TASK_NAME)
  347. if not netc then
  348. log.error("exmodbus", "创建 TCP 服务器失败")
  349. goto EXCEPTION_PROC
  350. end
  351. -- 配置服务器
  352. result = socket.config(netc, instance.port)
  353. if not result then
  354. log.error("exmodbus", "配置 TCP 服务器失败")
  355. goto EXCEPTION_PROC
  356. end
  357. -- 监听端口
  358. result = libnet.listen(instance.TASK_NAME, 0, netc)
  359. if not result then
  360. log.error("exmodbus", "监听端口失败")
  361. goto EXCEPTION_PROC
  362. end
  363. log.info("exmodbus", "TCP 从站已启动,监听端口:", instance.port)
  364. -- 处理连接和数据
  365. while true do
  366. -- 处理接收数据
  367. if not tcp_receiver(netc, instance) then
  368. log.info("exmodbus", "接收数据处理失败")
  369. break
  370. end
  371. -- 等待事件
  372. result, param = libnet.wait(instance.TASK_NAME, 0, netc)
  373. if not result then
  374. log.info("exmodbus", "客户端断开连接")
  375. break
  376. end
  377. end
  378. -- 异常处理
  379. ::EXCEPTION_PROC::
  380. -- 关闭连接
  381. if netc then
  382. libnet.close(instance.TASK_NAME, 5000, netc)
  383. socket.release(netc)
  384. netc = nil
  385. end
  386. -- 等待 5 秒后重试
  387. sys.wait(5000)
  388. end
  389. end
  390. -- 创建一个新的实例;
  391. local function create(config, exmodbus, gen_request_id)
  392. exmodbus_ref = exmodbus
  393. gen_id_func = gen_request_id
  394. local TASK_NAME = "exmodbus_tcp_task_"..gen_id_func()
  395. -- 创建一个新的实例;
  396. local instance = modbus:new(config, TASK_NAME)
  397. -- 检查实例是否创建成功;
  398. if not instance then
  399. log.error("exmodbus", "创建 Modbus 实例失败")
  400. return false
  401. end
  402. -- 启动任务
  403. sys.taskInitEx(tcp_slave_main_task_func, TASK_NAME, nil, instance)
  404. -- 返回实例;
  405. return instance
  406. end
  407. function modbus:destroy()
  408. -- 停止任务
  409. sys.taskDel(self.TASK_NAME)
  410. -- 释放缓冲区
  411. if self.recv_buff then
  412. self.recv_buff:free()
  413. self.recv_buff = nil
  414. end
  415. end
  416. -- 注册从站请求处理回调函数;
  417. function modbus:on(callback)
  418. if type(callback) ~= "function" then
  419. log.error("exmodbus", "on(callback) 的参数必须是一个函数")
  420. return false
  421. end
  422. self.slaveHandler = callback
  423. log.info("exmodbus", "已注册从站请求处理回调函数")
  424. return true
  425. end
  426. return { create = create }