exmodbus.lua 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. --[[
  2. @module exmodbus
  3. @summary exmodbus 控制Modbus RTU/ASCII/TCP主站/从站通信
  4. @version 1.0
  5. @date 2025.
  6. @author 马梦阳
  7. @usage
  8. 本文件的对外接口有 5 个:
  9. 1、exmodbus.create(config):创建 modbus 主站/从站,支持 RTU、ASCII、TCP 三种通信模式
  10. 2、modbus:read(config):主站向从站发起读取请求(仅适用于 RTU、ASCII、TCP 主站模式)
  11. 3、modbus:write(config):主站向从站发起写入请求(仅适用于 RTU、ASCII、TCP 主站模式)
  12. 4、modbus:destroy():销毁 modbus 主站/从站实例对象
  13. 5、modbus:on(callback):从站注册回调接口,用于处理主站发起的请求(仅适用于 RTU、ASCII、TCP 从站模式)
  14. ]]
  15. local exmodbus = {}
  16. -- 定义通信模式常量
  17. exmodbus.RTU_MASTER = 0 -- RTU 主站模式
  18. exmodbus.RTU_SLAVE = 1 -- RTU 从站模式
  19. exmodbus.ASCII_MASTER = 2 -- ASCII 主站模式
  20. exmodbus.ASCII_SLAVE = 3 -- ASCII 从站模式
  21. exmodbus.TCP_MASTER = 4 -- TCP 主站模式
  22. exmodbus.TCP_SLAVE = 5 -- TCP 从站模式
  23. -- 定义数据类型常量
  24. exmodbus.COIL_STATUS = 0 -- 线圈状态
  25. exmodbus.INPUT_STATUS = 1 -- 离散输入状态
  26. exmodbus.HOLDING_REGISTER = 4 -- 保持寄存器
  27. exmodbus.INPUT_REGISTER = 3 -- 输入寄存器
  28. -- 定义操作类型常量
  29. exmodbus.READ_COILS = 0x01 -- 读线圈状态
  30. exmodbus.READ_DISCRETE_INPUTS = 0x02 -- 读离散输入状态
  31. exmodbus.READ_HOLDING_REGISTERS = 0x03 -- 读保持寄存器
  32. exmodbus.READ_INPUT_REGISTERS = 0x04 -- 读输入寄存器
  33. exmodbus.WRITE_SINGLE_COIL = 0x05 -- 写单个线圈状态
  34. exmodbus.WRITE_SINGLE_HOLDING_REGISTER = 0x06 -- 写单个保持寄存器
  35. exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS = 0x10 -- 写多个保持寄存器
  36. exmodbus.WRITE_MULTIPLE_COILS = 0x0F -- 写多个线圈状态
  37. -- 定义响应结果常量
  38. exmodbus.STATUS_SUCCESS = 0 -- 收到响应数据且数据有效
  39. exmodbus.STATUS_DATA_INVALID = 1 -- 收到响应数据但数据损坏/校验失败
  40. exmodbus.STATUS_EXCEPTION = 2 -- 收到标准异常响应码
  41. exmodbus.STATUS_TIMEOUT = 3 -- 超时未收到响应
  42. exmodbus.STATUS_PARAM_INVALID = 4 -- 请求参数不正确
  43. -- 异常响应码常量
  44. exmodbus.ILLEGAL_FUNCTION = 0x01 -- 不支持请求的功能码
  45. exmodbus.ILLEGAL_DATA_ADDRESS = 0x02 -- 请求的数据地址无效或超出范围
  46. exmodbus.ILLEGAL_DATA_VALUE = 0x03 -- 请求的数据值无效
  47. exmodbus.SLAVE_DEVICE_FAILURE = 0x04 -- 从站在执行操作时发生内部错误
  48. exmodbus.ACKNOWLEDGE = 0x05 -- 请求已接受,但需要长时间处理
  49. exmodbus.SLAVE_DEVICE_BUSY = 0x06 -- 从站正忙,无法处理请求
  50. exmodbus.NEGATIVE_ACKNOWLEDGE = 0x07 -- 无法执行编程功能
  51. exmodbus.MEMORY_PARITY_ERROR = 0x08 -- 内存奇偶校验错误
  52. exmodbus.GATEWAY_PATH_UNAVAILABLE = 0x0A -- 网关路径不可用
  53. exmodbus.GATEWAY_TARGET_NO_RESPONSE = 0x0B -- 网关目标设备无响应
  54. -- 全局队列与调度器;
  55. local request_queue = {}
  56. local next_request_id = 1
  57. local scheduler_started = false
  58. -- 生成唯一请求 ID;
  59. local function gen_request_id()
  60. local id = next_request_id
  61. next_request_id = next_request_id + 1
  62. -- 确保请求 ID 在 32 位有符号整数范围内;
  63. if next_request_id == 0x7FFFFFFF then next_request_id = 1 end
  64. return id
  65. end
  66. -- 处理队列中的请求;
  67. local function process_request_queue()
  68. while true do
  69. if #request_queue > 0 then
  70. local req = table.remove(request_queue, 1)
  71. local instance = req.instance
  72. local config = req.config
  73. local is_read = req.is_read
  74. local req_id = req.request_id
  75. local result
  76. if is_read then
  77. result = instance:read_internal(config)
  78. else
  79. result = instance:write_internal(config)
  80. end
  81. sys.publish("exmodbus/resp/" .. req_id, result)
  82. else
  83. sys.waitUntil("start_scheduler")
  84. end
  85. end
  86. end
  87. -- 启动调度器;
  88. local function start_scheduler()
  89. if scheduler_started then return end
  90. scheduler_started = true
  91. sys.taskInit(process_request_queue)
  92. end
  93. -- 入队请求并等待响应;(内部使用)
  94. function exmodbus.enqueue_request(instance, config, is_read)
  95. -- 生成唯一请求 ID;
  96. local req_id = gen_request_id()
  97. -- 检查队列是否为空;
  98. -- 如果为空,先入队,然后发布主题告知调度器开始处理;
  99. -- 如果不为空,则直接入队,不用告知调度器;
  100. if #request_queue == 0 then
  101. -- 入队请求;
  102. table.insert(request_queue, {
  103. instance = instance,
  104. config = config,
  105. is_read = is_read,
  106. request_id = req_id
  107. })
  108. sys.publish("start_scheduler")
  109. else
  110. -- 入队请求;
  111. table.insert(request_queue, {
  112. instance = instance,
  113. config = config,
  114. is_read = is_read,
  115. request_id = req_id
  116. })
  117. end
  118. -- 启动调度器;
  119. start_scheduler()
  120. local ok, result = sys.waitUntil("exmodbus/resp/" .. req_id)
  121. return result
  122. end
  123. --[[
  124. 创建一个新的实例;
  125. @api exmodbus.create(config)
  126. @param config table 配置参数表,包含以下字段:
  127. mode number 通信模式,必须是 exmodbus 模块定义的常量(如 exmodbus.RTU_MASTER)
  128. uart_id number 串口 ID,uart0 写 0,uart1 写 1,以此类推
  129. baud_rate number 波特率
  130. data_bits number 数据位
  131. stop_bits number 停止位
  132. parity_bits number 校验位
  133. byte_order number 字节顺序
  134. rs485_dir_gpio number RS485 方向转换 GPIO 引脚
  135. rs485_dir_rx_level number RS485 接收方向电平
  136. adapter number 网卡 ID
  137. ip_address string 服务器 IP 地址
  138. port number 服务器端口号
  139. is_udp boolean 是否使用 UDP 协议
  140. is_tls boolean 是否使用加密传输
  141. keep_idle number 连接空闲多长时间后,开始发送第一个 keepalive 探针报文,单位:秒
  142. keep_interval number 发送第一个探针后,如果没收到 ACK 回复,间隔多久再发送下一个探针,单位:秒
  143. keep_cnt number 总共发送多少次探针后,如果依然没有回复,则判断连接已断开
  144. server_cert string TCP 模式下的服务器 CA 证书数据,UDP 模式下的 PSK
  145. client_cert string TCP 模式下的客户端证书数据,UDP 模式下的 PSK-ID
  146. client_key string TCP 模式下的客户端私钥加密数据
  147. client_password string TCP 模式下的客户端私钥口令数据
  148. @return table/nil 成功时返回实例对象,失败时返回 nil
  149. @usage
  150. RTU/ASCII 通信模式:
  151. local config = {
  152. mode = exmodbus.RTU_MASTER, -- 通信模式:RTU 主站
  153. uart_id = 1, -- 串口 ID:uart1
  154. baud_rate = 115200, -- 波特率:115200
  155. data_bits = 8, -- 数据位:8
  156. stop_bits = 1, -- 停止位:1
  157. parity_bits = uart.None, -- 校验位:无校验
  158. byte_order = uart.LSB, -- 字节顺序:小端序
  159. rs485_dir_gpio = 23, -- RS485 方向转换 GPIO 引脚
  160. rs485_dir_rx_level = 0 -- RS485 接收方向电平:0 为低电平,1 为高电平
  161. }
  162. local rtu_master = exmodbus.create(config)
  163. TCP 通信模式:
  164. local config = {
  165. mode = exmodbus.TCP_MASTER, -- 通信模式:TCP 主站
  166. adapter = socket.LWIP_ETH, -- 网卡 ID:LwIP 协议栈的以太网卡
  167. ip_address = "192.168.1.100", -- 服务器 IP 地址:192.168.1.100(主站:服务器 IP;从站:本地 IP,从站可以不用填此参数)
  168. port = 502, -- 服务器端口号:502(主站:服务器端口;从站:本地端口)
  169. is_udp = false, -- 是否使用 UDP 协议:不使用 UDP 协议,false/nil 表示使用 TCP 协议
  170. is_tls = false, -- 是否使用加密传输:不使用加密传输,false/nil 表示不使用加密
  171. keep_idle = 300, -- 连接空闲多长时间后,开始发送第一个 keepalive 探针报文:300 秒
  172. keep_interval = 10, -- 发送第一个探针后,如果没收到 ACK 回复,间隔多久再发送下一个探针:10 秒
  173. keep_cnt = 3, -- 总共发送多少次探针后,如果依然没有回复,则判断连接已断开:3 次
  174. server_cert = nil, -- TCP 模式下的服务器 CA 证书数据,UDP 模式下的 PSK:如果客户端不需要验证服务器证书,则设为 nil 或空着
  175. client_cert = nil, -- TCP 模式下的客户端证书数据,UDP 模式下的 PSK-ID:如果服务器不需要验证客户端证书,则设为 nil 或空着
  176. client_key = nil, -- TCP 模式下的客户端私钥加密数据:如果服务器不需要验证客户端私钥,则设为 nil 或空着
  177. client_password = nil -- TCP 模式下的客户端私钥口令数据:如果服务器不需要验证客户端私钥口令,则设为 nil 或空着
  178. }
  179. local tcp_master = exmodbus.create(config)
  180. --]]
  181. function exmodbus.create(config)
  182. -- 检查配置参数是否有效;
  183. if not config or type(config) ~= "table" then
  184. log.error("exmodbus", "配置必须是表格类型")
  185. return false
  186. end
  187. -- 根据通信模式加载对应的模块;
  188. if config.mode == exmodbus.RTU_MASTER or config.mode == exmodbus.RTU_SLAVE or
  189. config.mode == exmodbus.ASCII_MASTER or config.mode == exmodbus.ASCII_SLAVE then
  190. local result, mod = pcall(require, "exmodbus_rtu_ascii")
  191. if not result then
  192. log.error("exmodbus", "加载 RTU/ASCII 模块失败")
  193. return false
  194. end
  195. return mod.create(config, exmodbus, gen_request_id)
  196. elseif config.mode == exmodbus.TCP_MASTER or config.mode == exmodbus.TCP_SLAVE then
  197. local result, mod = pcall(require, "exmodbus_tcp")
  198. if not result then
  199. log.error("exmodbus", "加载 TCP 模块失败")
  200. return false
  201. end
  202. return mod.create(config, exmodbus, gen_request_id)
  203. else
  204. log.error("exmodbus", "通信模式不支持")
  205. return false
  206. end
  207. end
  208. --[[
  209. 主站向从站发送读取请求(仅适用于 RTU、ASCII、TCP 主站模式)
  210. @api modbus:read(config)
  211. @param config table 配置参数表,包含以下字段:
  212. slave_id number 从站 ID
  213. reg_type number 寄存器类型
  214. start_addr number 寄存器起始地址
  215. reg_count number 寄存器数量
  216. raw_request string 原始请求帧
  217. timeout number 超时时间,单位:毫秒
  218. @return table 包含以下字段:
  219. status number 响应结果状态码,参考 exmodbus 模块定义的常量(如 exmodbus.STATUS_SUCCESS)
  220. execption_code number 异常码,仅在 status 为 exmodbus.STATUS_EXCEPTION 时有效
  221. data table 寄存器数值,仅在 status 为 exmodbus.STATUS_SUCCESS 时有效,包含以下字段
  222. [start_addr] number 寄存器数值,索引为寄存器地址,值为寄存器数值
  223. ...
  224. raw_response string 原始响应帧
  225. @usage
  226. 用户在传入 config 参数时,有 原始帧 和 字段参数 两种方式
  227. 1. 原始帧方式
  228. local read_config = {
  229. raw_request = "010300000002C40B", -- 原始请求帧:01 03 00 00 00 02 C4 0B(读取保持寄存器 0x0000 开始的 2 个寄存器)
  230. timeout = 1000 -- 超时时间:1000 毫秒
  231. }
  232. local result = modbus:read(read_config)
  233. if result.status == exmodbus.STATUS_SUCCESS then
  234. log.info("exmodbus_test", "读取成功,原始响应帧: ", table.concat(result.raw_response, ", "))
  235. elseif result.status == exmodbus.STATUS_TIMEOUT then
  236. log.error("exmodbus_test", "读取请求超时")
  237. else
  238. log.error("exmodbus_test", "读取失败")
  239. end
  240. 2. 字段参数方式
  241. local read_config = {
  242. slave_id = 1, -- 从站 ID:1
  243. reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
  244. start_addr = 0x0000, -- 寄存器起始地址:0
  245. reg_count = 0x0002, -- 寄存器数量:2
  246. timeout = 1000 -- 超时时间:1000 毫秒
  247. }
  248. local result = modbus:read(read_config)
  249. -- 根据返回状态处理结果
  250. if result.status == exmodbus.STATUS_SUCCESS then
  251. -- 数据解析:
  252. log.info("exmodbus_test", "成功读取到从站 1 保持寄存器 0-2 的值,寄存器 0 数值:", result.data[result.start_addr],
  253. ",寄存器 1 数值:", result.data[result.start_addr + 1])
  254. elseif result.status == exmodbus.STATUS_DATA_INVALID then
  255. log.info("exmodbus_test", "收到从站 1 的响应数据但数据损坏/校验失败")
  256. elseif result.status == exmodbus.STATUS_EXCEPTION then
  257. log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", result.execption_code)
  258. elseif result.status == exmodbus.STATUS_TIMEOUT then
  259. log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
  260. end
  261. --]]
  262. -- 该接口在各个子文件中,此处仅用作注释
  263. -- function modbus:read(config) end
  264. --[[
  265. 主站向从站发送写入请求(仅适用于 RTU、ASCII、TCP 主站模式)
  266. @api modbus:write(config)
  267. @param config table 配置参数表,包含以下字段:
  268. slave_id number 从站 ID
  269. reg_type number 寄存器类型
  270. start_addr number 寄存器起始地址
  271. reg_count number 寄存器数量
  272. data table 寄存器数值,包含以下字段:
  273. [start_addr] number 寄存器数值,索引为寄存器地址,值为寄存器数值
  274. ...
  275. force_multiple boolean 是否强制使用写多个功能码进行写入单个寄存器操作
  276. raw_request string 原始请求帧
  277. timeout number 超时时间,单位:毫秒
  278. @return table 包含以下字段:
  279. status number 响应结果状态码,参考 exmodbus 模块定义的常量(如 exmodbus.STATUS_SUCCESS)
  280. execption_code number 异常码,仅在 status 为 exmodbus.STATUS_EXCEPTION 时有效
  281. raw_response string 原始响应帧
  282. @usage
  283. 用户在传入 config 参数时,有 原始帧 和 字段参数 两种方式
  284. 1. 原始帧方式
  285. local write_config = {
  286. raw_request = "011000000002007B01592471", -- 原始请求帧:01 10 00 00 00 02 00 7B 01 59 24 71(写入保持寄存器 0x0000 开始的 2 个寄存器,值为 0x007B 和 0x0159)
  287. timeout = 1000 -- 超时时间:1000 毫秒
  288. }
  289. local result = modbus:write(write_config)
  290. if result.status == exmodbus.STATUS_SUCCESS then
  291. log.info("exmodbus_test", "写入成功,原始响应帧: ", table.concat(result.raw_response, ", "))
  292. elseif result.status == exmodbus.STATUS_TIMEOUT then
  293. log.error("exmodbus_test", "写入请求超时")
  294. else
  295. log.error("exmodbus_test", "写入失败")
  296. end
  297. 2. 字段参数方式
  298. local write_config = {
  299. slave_id = 1, -- 从站 ID:1
  300. reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
  301. start_addr = 0x0000, -- 寄存器起始地址:0
  302. reg_count = 0x0002, -- 寄存器数量:2
  303. data = {
  304. [0x0000] = 0x007B, -- 寄存器 0 数值:0x007B
  305. [0x0001] = 0x0159, -- 寄存器 1 数值:0x0159
  306. },
  307. timeout = 1000 -- 超时时间:1000 毫秒
  308. }
  309. local result = modbus:write(write_config)
  310. -- 根据返回状态处理结果
  311. if result.status == exmodbus.STATUS_SUCCESS then
  312. log.info("exmodbus_test", "成功写入从站 1 保持寄存器 0-2 的值")
  313. elseif result.status == exmodbus.STATUS_DATA_INVALID then
  314. log.info("exmodbus_test", "收到从站 1 的响应数据但数据损坏/校验失败")
  315. elseif result.status == exmodbus.STATUS_EXCEPTION then
  316. log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", result.execption_code)
  317. elseif result.status == exmodbus.STATUS_TIMEOUT then
  318. log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
  319. end
  320. --]]
  321. -- 该接口在各个子文件中,此处仅用作注释
  322. -- function modbus:write(config) end
  323. --[[
  324. 销毁 modbus 主站/从站实例对象
  325. @api modbus:destroy()
  326. @return nil
  327. @usage
  328. modbus:destroy()
  329. --]]
  330. -- 该接口在各个子文件中,此处仅用作注释
  331. -- function modbus:destroy() end
  332. --[[
  333. 从站注册回调接口,用于处理主站发起的请求(仅适用于 RTU、ASCII、TCP 从站模式)
  334. @api modbus:on(callback)
  335. @param callback function 回调函数,格式为:
  336. function callback(request)
  337. -- 用户代码
  338. end
  339. 该回调函数接收 requset 一个参数,该参数为 table 类型,包含以下字段:
  340. slave_id number 从站 ID
  341. func_code number 功能码
  342. reg_type number 寄存器类型
  343. start_addr number 寄存器起始地址
  344. reg_count number 寄存器数量
  345. data table 寄存器数值,包含以下字段:
  346. [start_addr] number 寄存器数值,索引为寄存器地址,值为寄存器数值
  347. ...
  348. @return nil
  349. @usage
  350. function callback(request)
  351. -- 用户处理代码
  352. end
  353. --]]
  354. -- 该接口在各个子文件中,此处仅用作注释
  355. -- modbus:on(callback)
  356. return exmodbus