|
|
@@ -30,12 +30,18 @@ function modbus:new(config, TASK_NAME)
|
|
|
client_password = config.client_password, -- TCP模式下的客户端私钥口令数据
|
|
|
}
|
|
|
|
|
|
+ -- 连接状态
|
|
|
+ obj.is_connected = false
|
|
|
-- 从站请求处理回调函数;
|
|
|
obj.slaveHandler = nil
|
|
|
-- 任务名称
|
|
|
obj.TASK_NAME = TASK_NAME
|
|
|
-- 接收数据缓冲区
|
|
|
obj.recv_buff = nil
|
|
|
+ -- 发送请求队列
|
|
|
+ obj.send_queue = {}
|
|
|
+ -- 当前等待响应的事务ID
|
|
|
+ obj.pending_transaction = nil
|
|
|
|
|
|
-- 设置原表;
|
|
|
setmetatable(obj, modbus)
|
|
|
@@ -43,6 +49,659 @@ function modbus:new(config, TASK_NAME)
|
|
|
return obj
|
|
|
end
|
|
|
|
|
|
+-- 构建 Modbus TCP 请求帧(主站使用)
|
|
|
+local function build_tcp_frame(request_type, config)
|
|
|
+ -- 参数验证
|
|
|
+ if not config or type(config) ~= "table" then
|
|
|
+ log.error("exmodbus", "配置必须是表格类型")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 验证必要参数
|
|
|
+ if not config.slave_id then
|
|
|
+ log.error("exmodbus", "缺少必要参数: slave_id")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+
|
|
|
+ if not config.reg_type then
|
|
|
+ log.error("exmodbus", "缺少必要参数: reg_type")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+
|
|
|
+ if not config.start_addr then
|
|
|
+ log.error("exmodbus", "缺少必要参数: start_addr")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+
|
|
|
+ if not config.reg_count then
|
|
|
+ log.error("exmodbus", "缺少必要参数: reg_count")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+
|
|
|
+ if request_type == "write" then
|
|
|
+ if not config.data then
|
|
|
+ log.error("exmodbus", "缺少写入请求必要参数: data")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 参数范围验证
|
|
|
+ if type(config.slave_id) ~= "number" or config.slave_id < 1 or config.slave_id > 247 then
|
|
|
+ log.error("exmodbus", "从站地址必须在 1-247 范围内")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+
|
|
|
+ if type(config.start_addr) ~= "number" or config.start_addr < 0 or config.start_addr > 65535 then
|
|
|
+ log.error("exmodbus", "起始地址必须在 0-65535 范围内")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+
|
|
|
+ if config.reg_type ~= exmodbus_ref.COIL_STATUS and config.reg_type ~= exmodbus_ref.INPUT_STATUS and
|
|
|
+ config.reg_type ~= exmodbus_ref.HOLDING_REGISTER and config.reg_type ~= exmodbus_ref.INPUT_REGISTER then
|
|
|
+ log.error("exmodbus", "无效的寄存器类型: " .. tostring(config.reg_type))
|
|
|
+ return false
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 根据操作类型和寄存器类型确定功能码
|
|
|
+ local func_code
|
|
|
+ local data = ""
|
|
|
+
|
|
|
+ if request_type == "read" then
|
|
|
+ -- 读请求
|
|
|
+ if config.reg_type == exmodbus_ref.COIL_STATUS then
|
|
|
+ func_code = exmodbus_ref.READ_COILS
|
|
|
+ if config.reg_count < 1 or config.reg_count > 2000 then
|
|
|
+ log.error("exmodbus", "线圈读取数量超出范围: " .. config.reg_count .. " (范围: 1-2000)")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+ elseif config.reg_type == exmodbus_ref.INPUT_STATUS then
|
|
|
+ func_code = exmodbus_ref.READ_DISCRETE_INPUTS
|
|
|
+ if config.reg_count < 1 or config.reg_count > 2000 then
|
|
|
+ log.error("exmodbus", "离散输入读取数量超出范围: " .. config.reg_count .. " (范围: 1-2000)")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+ elseif config.reg_type == exmodbus_ref.HOLDING_REGISTER then
|
|
|
+ func_code = exmodbus_ref.READ_HOLDING_REGISTERS
|
|
|
+ if config.reg_count < 1 or config.reg_count > 125 then
|
|
|
+ log.error("exmodbus", "保持寄存器读取数量超出范围: " .. config.reg_count .. " (范围: 1-125)")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+ elseif config.reg_type == exmodbus_ref.INPUT_REGISTER then
|
|
|
+ func_code = exmodbus_ref.READ_INPUT_REGISTERS
|
|
|
+ if config.reg_count < 1 or config.reg_count > 125 then
|
|
|
+ log.error("exmodbus", "输入寄存器读取数量超出范围: " .. config.reg_count .. " (范围: 1-125)")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ data = string.char(config.slave_id, func_code) ..
|
|
|
+ string.char((config.start_addr >> 8) & 0xFF, config.start_addr & 0xFF) ..
|
|
|
+ string.char((config.reg_count >> 8) & 0xFF, config.reg_count & 0xFF)
|
|
|
+ else
|
|
|
+ -- 写请求
|
|
|
+ -- 校验每一个地址是否有数据,且数据是否为数字类型
|
|
|
+ for i = 0, config.reg_count - 1 do
|
|
|
+ local addr = config.start_addr + i
|
|
|
+ if config.data[addr] == nil then
|
|
|
+ log.error("exmodbus", "缺少寄存器数据", "address:", addr)
|
|
|
+ return false
|
|
|
+ end
|
|
|
+ if type(config.data[addr]) ~= "number" then
|
|
|
+ log.error("exmodbus", "寄存器数据必须是数字类型", "address:", addr)
|
|
|
+ return false
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 判断是否强制使用写多个功能码
|
|
|
+ local use_multiple = config.force_multiple
|
|
|
+
|
|
|
+ if config.reg_type == exmodbus_ref.COIL_STATUS then
|
|
|
+ if config.reg_count == 1 then
|
|
|
+ if not use_multiple then
|
|
|
+ func_code = exmodbus_ref.WRITE_SINGLE_COIL
|
|
|
+ else
|
|
|
+ func_code = exmodbus_ref.WRITE_MULTIPLE_COILS
|
|
|
+ end
|
|
|
+ else
|
|
|
+ func_code = exmodbus_ref.WRITE_MULTIPLE_COILS
|
|
|
+ if config.reg_count < 1 or config.reg_count > 1968 then
|
|
|
+ log.error("exmodbus", "线圈写入数量超出范围: " .. config.reg_count .. " (范围: 1-1968)")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+ end
|
|
|
+ elseif config.reg_type == exmodbus_ref.HOLDING_REGISTER then
|
|
|
+ if config.reg_count == 1 then
|
|
|
+ if not use_multiple then
|
|
|
+ func_code = exmodbus_ref.WRITE_SINGLE_HOLDING_REGISTER
|
|
|
+ else
|
|
|
+ func_code = exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS
|
|
|
+ end
|
|
|
+ else
|
|
|
+ func_code = exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS
|
|
|
+ if config.reg_count < 1 or config.reg_count > 123 then
|
|
|
+ log.error("exmodbus", "寄存器写入数量超出范围: " .. config.reg_count .. " (范围: 1-123)")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+ end
|
|
|
+ else
|
|
|
+ log.error("exmodbus", "不支持的寄存器类型")
|
|
|
+ return nil
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 构建写数据
|
|
|
+ if func_code == exmodbus_ref.WRITE_SINGLE_COIL then
|
|
|
+ -- 写入单个线圈,值必须是 0xFF00 (ON) 或 0x0000 (OFF)
|
|
|
+ local value = config.data[config.start_addr] ~= 0 and 0xFF00 or 0x0000
|
|
|
+ data = string.char(config.slave_id, func_code) ..
|
|
|
+ string.char((config.start_addr >> 8) & 0xFF, config.start_addr & 0xFF) ..
|
|
|
+ string.char((value >> 8) & 0xFF, value & 0xFF)
|
|
|
+
|
|
|
+ elseif func_code == exmodbus_ref.WRITE_SINGLE_HOLDING_REGISTER then
|
|
|
+ -- 写入单个保持寄存器
|
|
|
+ local value = config.data[config.start_addr]
|
|
|
+ if value < 0 or value > 65535 or value ~= math.floor(value) then
|
|
|
+ log.error("exmodbus", "寄存器值必须是 0~65535 范围内的整数,实际值: ", value)
|
|
|
+ return false
|
|
|
+ end
|
|
|
+ data = string.char(config.slave_id, func_code) ..
|
|
|
+ string.char((config.start_addr >> 8) & 0xFF, config.start_addr & 0xFF) ..
|
|
|
+ string.char((value >> 8) & 0xFF, value & 0xFF)
|
|
|
+
|
|
|
+ elseif func_code == exmodbus_ref.WRITE_MULTIPLE_COILS then
|
|
|
+ -- 写入多个线圈
|
|
|
+ local byte_count = math.ceil(config.reg_count / 8)
|
|
|
+ local values_bytes = ""
|
|
|
+
|
|
|
+ -- 构建线圈数据(字节序为大端序)
|
|
|
+ for i = 0, byte_count - 1 do
|
|
|
+ local byte_value = 0
|
|
|
+ -- 遍历当前字节的 8 个位
|
|
|
+ for j = 0, 7 do
|
|
|
+ local bit_index = i * 8 + j
|
|
|
+ -- 检查当前比特是否在有效范围内
|
|
|
+ if bit_index < config.reg_count then
|
|
|
+ local addr = config.start_addr + bit_index
|
|
|
+ local bit_val = config.data[addr]
|
|
|
+ if bit_val ~= nil and bit_val ~= 0 then
|
|
|
+ byte_value = byte_value | (1 << j)
|
|
|
+ end
|
|
|
+ end
|
|
|
+ end
|
|
|
+ values_bytes = values_bytes .. string.char(byte_value)
|
|
|
+ end
|
|
|
+
|
|
|
+ data = string.char(config.slave_id, func_code) ..
|
|
|
+ string.char((config.start_addr >> 8) & 0xFF, config.start_addr & 0xFF) ..
|
|
|
+ string.char((config.reg_count >> 8) & 0xFF, config.reg_count & 0xFF) ..
|
|
|
+ string.char(byte_count) .. values_bytes
|
|
|
+
|
|
|
+ elseif func_code == exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS then
|
|
|
+ -- 写入多个保持寄存器
|
|
|
+ local byte_count = config.reg_count * 2
|
|
|
+ local values_bytes = ""
|
|
|
+
|
|
|
+ -- 构建寄存器数据(字节序为大端序)
|
|
|
+ for i = 0, config.reg_count - 1 do
|
|
|
+ local addr = config.start_addr + i
|
|
|
+ local value = config.data[addr]
|
|
|
+ if value < 0 or value > 65535 or value ~= math.floor(value) then
|
|
|
+ log.error("exmodbus", "寄存器值必须是 0~65535 范围内的整数,地址:", addr, "值:", value)
|
|
|
+ return false
|
|
|
+ end
|
|
|
+ values_bytes = values_bytes .. string.char((value >> 8) & 0xFF, value & 0xFF)
|
|
|
+ end
|
|
|
+
|
|
|
+ data = string.char(config.slave_id, func_code) ..
|
|
|
+ string.char((config.start_addr >> 8) & 0xFF, config.start_addr & 0xFF) ..
|
|
|
+ string.char((config.reg_count >> 8) & 0xFF, config.reg_count & 0xFF) ..
|
|
|
+ string.char(byte_count) .. values_bytes
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 构建完整的 TCP 请求帧
|
|
|
+ local transaction_id = gen_id_func() % 0x10000
|
|
|
+ local length = #data -- 长度包含从站ID
|
|
|
+ local frame = string.pack(">H", transaction_id) .. -- 事务 ID
|
|
|
+ string.pack(">H", 0) .. -- 协议 ID
|
|
|
+ string.pack(">H", length) .. -- 长度
|
|
|
+ data -- 从站ID + PDU数据
|
|
|
+
|
|
|
+ return frame, func_code, transaction_id
|
|
|
+end
|
|
|
+
|
|
|
+-- 解析 Modbus TCP 响应帧(主站使用)
|
|
|
+local function parse_tcp_response(response, config, expected_func_code, expected_transaction_id)
|
|
|
+ -- 定义返回数据结构;
|
|
|
+ local return_data = {
|
|
|
+ status = false,
|
|
|
+ execption_code = nil,
|
|
|
+ data = {},
|
|
|
+ }
|
|
|
+
|
|
|
+ -- 检查响应是否为空
|
|
|
+ if not response or #response == 0 then
|
|
|
+ log.error("exmodbus", "响应报文为空")
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 检查响应帧长度
|
|
|
+ if #response < MODBUS_TCP_HEADER_LEN + 1 then
|
|
|
+ log.error("exmodbus", "响应帧长度不足")
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 解析 MBAP 头
|
|
|
+ local transaction_id = string.unpack(">H", response, 1)
|
|
|
+ local protocol_id = string.unpack(">H", response, 3)
|
|
|
+ local length = string.unpack(">H", response, 5)
|
|
|
+ local slave_id = string.unpack("B", response, 7)
|
|
|
+
|
|
|
+ -- 检查事务 ID 是否匹配
|
|
|
+ if transaction_id ~= expected_transaction_id then
|
|
|
+ log.error("exmodbus", "事务ID不匹配")
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 检查协议 ID
|
|
|
+ if protocol_id ~= 0 then
|
|
|
+ log.error("exmodbus", "无效的协议ID")
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 检查数据长度
|
|
|
+ if #response ~= 6 + length then
|
|
|
+ log.error("exmodbus", "数据长度与实际长度不匹配")
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 检查从站ID是否匹配
|
|
|
+ if slave_id ~= config.slave_id then
|
|
|
+ log.error("exmodbus", "从站地址不匹配,期望:", config.slave_id, "实际:", slave_id)
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 解析功能码
|
|
|
+ local func_code = string.unpack("B", response, 8)
|
|
|
+ local response_length = #response
|
|
|
+
|
|
|
+ -- 检查异常响应
|
|
|
+ if bit.band(func_code, 0x80) ~= 0 then
|
|
|
+ -- 检查异常响应报文长度是否正确
|
|
|
+ if response_length ~= 9 then
|
|
|
+ log.error("exmodbus", "异常响应报文长度不正确,期望: 9 字节,实际:", response_length, "字节")
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 提取异常码
|
|
|
+ local exception_code = string.unpack("B", response, 9)
|
|
|
+ log.error("exmodbus", "接收到 Modbus 异常响应,功能码:", func_code, "异常码:", exception_code)
|
|
|
+
|
|
|
+ return_data.status = exmodbus_ref.STATUS_EXCEPTION
|
|
|
+ return_data.execption_code = exception_code
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 检查功能码是否匹配
|
|
|
+ if func_code ~= expected_func_code then
|
|
|
+ log.error("exmodbus", "功能码不匹配,期望:", expected_func_code, "实际:", func_code)
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+
|
|
|
+ -- 根据不同的功能码解析数据
|
|
|
+ local parsed_data = {}
|
|
|
+
|
|
|
+ -- 功能码 0x01 和 0x02:读取线圈状态和离散输入状态
|
|
|
+ if func_code == exmodbus_ref.READ_COILS or func_code == exmodbus_ref.READ_DISCRETE_INPUTS then
|
|
|
+ -- 提取数据部分
|
|
|
+ local byte_count = string.unpack("B", response, 9)
|
|
|
+ local data_start_pos = 10
|
|
|
+ local data_end_pos = response_length
|
|
|
+
|
|
|
+ -- 验证数据长度是否正确
|
|
|
+ if data_end_pos - data_start_pos + 1 ~= byte_count then
|
|
|
+ log.error("exmodbus", "数据长度不匹配,期望:", byte_count, "实际:", data_end_pos - data_start_pos + 1)
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 验证字节数是否足够表示指定数量的位
|
|
|
+ local expected_bytes = math.ceil(config.reg_count / 8)
|
|
|
+ if byte_count < expected_bytes then
|
|
|
+ log.error("exmodbus", "数据字节数不足,无法表示所有位")
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 解析位数据
|
|
|
+ for i = 0, config.reg_count - 1 do
|
|
|
+ local modbus_addr = config.start_addr + i -- 计算当前位对应的 Modbus 地址
|
|
|
+ local byte_pos = data_start_pos + math.floor(i / 8) -- 计算当前位对应的字节位置
|
|
|
+ local bit_pos = i % 8 -- 计算当前位对应的位位置
|
|
|
+ local byte_value = string.unpack("B", response, byte_pos)
|
|
|
+ parsed_data[modbus_addr] = bit.band(byte_value, bit.lshift(1, bit_pos)) ~= 0 and 1 or 0
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 功能码 0x03 和 0x04:读取保持寄存器和输入寄存器
|
|
|
+ elseif func_code == exmodbus_ref.READ_HOLDING_REGISTERS or func_code == exmodbus_ref.READ_INPUT_REGISTERS then
|
|
|
+ -- 提取数据部分
|
|
|
+ local byte_count = string.unpack("B", response, 9)
|
|
|
+ local data_start_pos = 10
|
|
|
+ local data_end_pos = response_length
|
|
|
+
|
|
|
+ -- 验证数据长度是否正确
|
|
|
+ if data_end_pos - data_start_pos + 1 ~= byte_count then
|
|
|
+ log.error("exmodbus", "数据长度不匹配,期望:", byte_count, "实际:", data_end_pos - data_start_pos + 1)
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 验证字节数是否足够表示指定数量的寄存器
|
|
|
+ local expected_bytes = config.reg_count * 2
|
|
|
+ if byte_count < expected_bytes then
|
|
|
+ log.error("exmodbus", "数据字节数不足,无法表示所有寄存器")
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 解析寄存器数据(大端序)
|
|
|
+ for i = 0, config.reg_count - 1 do
|
|
|
+ local modbus_addr = config.start_addr + i -- 计算当前寄存器对应的 Modbus 地址
|
|
|
+ local reg_pos = data_start_pos + i * 2 -- 计算当前寄存器对应的字节位置
|
|
|
+ parsed_data[modbus_addr] = bit.lshift(string.unpack("B", response, reg_pos), 8) + string.unpack("B", response, reg_pos + 1)
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 功能码 0x05:写入单个线圈
|
|
|
+ elseif func_code == exmodbus_ref.WRITE_SINGLE_COIL then
|
|
|
+ -- 写入单个线圈响应格式:事务ID(2 字节) + 协议ID(2 字节) + 长度(2 字节) + 从站地址(1 字节) + 功能码(1 字节) + 线圈地址(2 字节) + 线圈值(2 字节)
|
|
|
+ if response_length ~= 12 then
|
|
|
+ log.error("exmodbus", "写入单个线圈响应报文长度不正确")
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 解析线圈地址和值
|
|
|
+ local coil_addr = string.unpack(">H", response, 9)
|
|
|
+ local coil_value = string.unpack(">H", response, 11)
|
|
|
+
|
|
|
+ -- 验证地址是否匹配请求
|
|
|
+ if config.start_addr and coil_addr ~= config.start_addr then
|
|
|
+ log.error("exmodbus", "线圈地址不匹配,期望:", config.start_addr, "实际:", coil_addr)
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 线圈值应该是 0x0000(OFF) 或 0xFF00(ON)
|
|
|
+ local normalized_value = (coil_value == 0x0000) and 0 or 1
|
|
|
+ parsed_data[coil_addr] = normalized_value
|
|
|
+
|
|
|
+ -- 功能码 0x06:写入单个保持寄存器
|
|
|
+ elseif func_code == exmodbus_ref.WRITE_SINGLE_HOLDING_REGISTER then
|
|
|
+ -- 写入单个保持寄存器响应格式:事务ID(2 字节) + 协议ID(2 字节) + 长度(2 字节) + 从站地址(1 字节) + 功能码(1 字节) + 寄存器地址(2 字节) + 寄存器值(2 字节)
|
|
|
+ if response_length ~= 12 then
|
|
|
+ log.error("exmodbus", "写入单个保持寄存器响应报文长度不正确")
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 解析寄存器地址和值
|
|
|
+ local reg_addr = string.unpack(">H", response, 9)
|
|
|
+ local reg_value = string.unpack(">H", response, 11)
|
|
|
+
|
|
|
+ -- 验证地址是否匹配请求
|
|
|
+ if config.start_addr and reg_addr ~= config.start_addr then
|
|
|
+ log.error("exmodbus", "单个保持寄存器地址不匹配,期望:", config.start_addr, "实际:", reg_addr)
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ parsed_data[reg_addr] = reg_value
|
|
|
+
|
|
|
+ -- 功能码 0x0F:写入多个线圈
|
|
|
+ elseif func_code == exmodbus_ref.WRITE_MULTIPLE_COILS then
|
|
|
+ -- 写入多个线圈响应格式:事务ID(2 字节) + 协议ID(2 字节) + 长度(2 字节) + 从站地址(1 字节) + 功能码(1 字节) + 起始地址(2 字节) + 线圈数量(2 字节)
|
|
|
+ if response_length ~= 12 then
|
|
|
+ log.error("exmodbus", "写入多个线圈响应报文长度不正确")
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 解析起始地址和线圈数量
|
|
|
+ local start_addr = string.unpack(">H", response, 9)
|
|
|
+ local coil_count = string.unpack(">H", response, 11)
|
|
|
+
|
|
|
+ -- 验证地址和数量是否匹配请求
|
|
|
+ if config.start_addr and start_addr ~= config.start_addr then
|
|
|
+ log.error("exmodbus", "线圈起始地址不匹配,期望:", config.start_addr, "实际:", start_addr)
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ if config.reg_count and coil_count ~= config.reg_count then
|
|
|
+ log.error("exmodbus", "线圈数量不匹配,期望:", config.reg_count, "实际:", coil_count)
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 在返回数据中记录操作成功的起始地址和数量
|
|
|
+ parsed_data.start_addr = start_addr
|
|
|
+ parsed_data.count = coil_count
|
|
|
+
|
|
|
+ -- 功能码 0x10:写入多个保持寄存器
|
|
|
+ elseif func_code == exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS then
|
|
|
+ -- 写入多个保持寄存器响应格式:事务ID(2 字节) + 协议ID(2 字节) + 长度(2 字节) + 从站地址(1 字节) + 功能码(1 字节) + 起始地址(2 字节) + 寄存器数量(2 字节)
|
|
|
+ if response_length ~= 12 then
|
|
|
+ log.error("exmodbus", "写入多个保持寄存器响应报文长度不正确")
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 解析起始地址和寄存器数量
|
|
|
+ local start_addr = string.unpack(">H", response, 9)
|
|
|
+ local reg_count = string.unpack(">H", response, 11)
|
|
|
+
|
|
|
+ -- 验证地址和数量是否匹配请求
|
|
|
+ if config.start_addr and start_addr ~= config.start_addr then
|
|
|
+ log.error("exmodbus", "寄存器起始地址不匹配,期望:", config.start_addr, "实际:", start_addr)
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ if config.reg_count and reg_count ~= config.reg_count then
|
|
|
+ log.error("exmodbus", "寄存器数量不匹配,期望:", config.reg_count, "实际:", reg_count)
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 在返回数据中记录操作成功的起始地址和数量
|
|
|
+ parsed_data.start_addr = start_addr
|
|
|
+ parsed_data.count = reg_count
|
|
|
+
|
|
|
+ -- 未知功能码
|
|
|
+ else
|
|
|
+ log.error("exmodbus", "不支持的功能码解析:", func_code)
|
|
|
+ return_data.status = exmodbus_ref.STATUS_DATA_INVALID
|
|
|
+ return return_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 成功解析响应数据
|
|
|
+ return_data.data = parsed_data
|
|
|
+ return_data.status = exmodbus_ref.STATUS_SUCCESS
|
|
|
+
|
|
|
+ return return_data
|
|
|
+end
|
|
|
+
|
|
|
+-- TCP 主站发送请求并等待响应
|
|
|
+local function sendRequest_waitResponse(instance, request_frame, config)
|
|
|
+ -- 检查连接状态
|
|
|
+ if not instance.is_connected or not instance.socket_client then
|
|
|
+ log.error("exmodbus", "TCP 连接未建立或已断开,无法发送请求")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 从请求帧中提取事务ID和功能码
|
|
|
+ local transaction_id = string.unpack(">H", request_frame, 1)
|
|
|
+ local function_code = string.unpack("B", request_frame, 8)
|
|
|
+
|
|
|
+ -- 创建请求信息并添加到发送队列
|
|
|
+ local request_info = {
|
|
|
+ request_frame = request_frame,
|
|
|
+ function_code = function_code,
|
|
|
+ transaction_id = transaction_id,
|
|
|
+ config = config
|
|
|
+ }
|
|
|
+
|
|
|
+ table.insert(instance.send_queue, request_info)
|
|
|
+
|
|
|
+ -- log.info("exmodbus", "请求已添加到发送队列", transaction_id)
|
|
|
+
|
|
|
+ sys.sendMsg(instance.TASK_NAME, socket.EVENT, 0)
|
|
|
+
|
|
|
+ -- 等待响应
|
|
|
+ local _, response_data = sys.waitUntil("exmodbus/tcp_resp/" .. transaction_id,
|
|
|
+ config.timeout or 5000)
|
|
|
+
|
|
|
+ if not response_data then
|
|
|
+ log.error("exmodbus", "等待响应超时")
|
|
|
+ return false, nil
|
|
|
+ end
|
|
|
+
|
|
|
+ return true, response_data
|
|
|
+end
|
|
|
+
|
|
|
+-- TCP 主站接收数据处理函数
|
|
|
+local function tcp_master_receiver(instance)
|
|
|
+ if instance.recv_buff == nil then
|
|
|
+ instance.recv_buff = zbuff.create(1024)
|
|
|
+ end
|
|
|
+
|
|
|
+ while true do
|
|
|
+ local succ, param = socket.rx(instance.socket_client, instance.recv_buff)
|
|
|
+
|
|
|
+ if not succ then
|
|
|
+ log.info("exmodbus", "读取数据失败")
|
|
|
+ return false
|
|
|
+ end
|
|
|
+
|
|
|
+ if instance.recv_buff:used() > 0 then
|
|
|
+ local data = instance.recv_buff:query()
|
|
|
+ sys.publish("exmodbus/tcp_resp/" .. instance.current_transaction_id, data)
|
|
|
+ -- log.info("exmodbus", "读取数据成功")
|
|
|
+ instance.recv_buff:del()
|
|
|
+ else
|
|
|
+ break
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ return true
|
|
|
+end
|
|
|
+
|
|
|
+-- TCP 主站发送数据处理函数
|
|
|
+local function tcp_master_sender(instance)
|
|
|
+ -- 检查发送队列中是否有请求
|
|
|
+ while #instance.send_queue > 0 do
|
|
|
+ -- 取出队列中的第一个请求
|
|
|
+ local request_info = table.remove(instance.send_queue, 1)
|
|
|
+ local request_frame = request_info.request_frame
|
|
|
+ local function_code = request_info.function_code
|
|
|
+ local transaction_id = request_info.transaction_id
|
|
|
+ local config = request_info.config
|
|
|
+
|
|
|
+ -- 设置当前事务信息
|
|
|
+ instance.current_transaction_id = transaction_id
|
|
|
+
|
|
|
+ -- 发送请求
|
|
|
+ local result, buff_full = libnet.tx(instance.TASK_NAME, 15000, instance.socket_client, request_frame)
|
|
|
+ if not result then
|
|
|
+ log.error("exmodbus", "发送请求失败")
|
|
|
+ return true
|
|
|
+ end
|
|
|
+
|
|
|
+ if buff_full then
|
|
|
+ log.error("exmodbus", "缓冲区已满,将请求重新放回队列队首")
|
|
|
+ -- 将请求重新放回队列队首
|
|
|
+ table.insert(instance.send_queue, 1, request_info)
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ return true
|
|
|
+end
|
|
|
+
|
|
|
+-- TCP 主站主任务函数
|
|
|
+local function tcp_master_main_task_func(instance)
|
|
|
+ local result, param
|
|
|
+
|
|
|
+ while true do
|
|
|
+ -- 创建 socket 客户端
|
|
|
+ instance.socket_client = socket.create(instance.adapter, instance.TASK_NAME)
|
|
|
+ if not instance.socket_client then
|
|
|
+ log.error("exmodbus", "创建 socket 客户端失败")
|
|
|
+ goto EXCEPTION_PROC
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 配置 socket
|
|
|
+ result = socket.config(instance.socket_client)
|
|
|
+ if not result then
|
|
|
+ log.error("exmodbus", "配置 socket 失败")
|
|
|
+ goto EXCEPTION_PROC
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 连接服务器
|
|
|
+ result = libnet.connect(instance.TASK_NAME, 15000, instance.socket_client, instance.ip_address, instance.port)
|
|
|
+ if not result then
|
|
|
+ log.error("exmodbus", "连接服务器失败")
|
|
|
+ goto EXCEPTION_PROC
|
|
|
+ end
|
|
|
+
|
|
|
+ log.info("exmodbus", "连接服务器成功")
|
|
|
+ instance.is_connected = true
|
|
|
+
|
|
|
+ -- 主循环
|
|
|
+ while true do
|
|
|
+ -- 处理接收数据
|
|
|
+ if not tcp_master_receiver(instance) then
|
|
|
+ log.info("exmodbus", "接收数据处理失败")
|
|
|
+ break
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 处理发送数据
|
|
|
+ if not tcp_master_sender(instance) then
|
|
|
+ log.info("exmodbus", "发送数据处理失败")
|
|
|
+ break
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 等待事件
|
|
|
+ result, param = libnet.wait(instance.TASK_NAME, 0, instance.socket_client)
|
|
|
+ if not result then
|
|
|
+ log.info("exmodbus", "连接断开")
|
|
|
+ break
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 异常处理
|
|
|
+ ::EXCEPTION_PROC::
|
|
|
+
|
|
|
+ -- 关闭连接
|
|
|
+ if instance.socket_client then
|
|
|
+ libnet.close(instance.TASK_NAME, 5000, instance.socket_client)
|
|
|
+ socket.release(instance.socket_client)
|
|
|
+ instance.socket_client = nil
|
|
|
+ instance.is_connected = false
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 等待 5 秒后重试
|
|
|
+ sys.wait(5000)
|
|
|
+ end
|
|
|
+end
|
|
|
+
|
|
|
-- 解析 TCP 请求帧(从站使用)
|
|
|
local function parse_tcp_request(data)
|
|
|
-- 检查请求帧长度,至少包含 MBAP 头和功能码
|
|
|
@@ -448,8 +1107,19 @@ local function create(config, exmodbus, gen_request_id)
|
|
|
return false
|
|
|
end
|
|
|
|
|
|
- -- 启动任务
|
|
|
- sys.taskInitEx(tcp_slave_main_task_func, TASK_NAME, nil, instance)
|
|
|
+ -- 根据模式启动不同的任务
|
|
|
+ if config.mode == exmodbus_ref.TCP_MASTER then
|
|
|
+ -- 启动主站任务
|
|
|
+ sys.taskInitEx(tcp_master_main_task_func, TASK_NAME, nil, instance)
|
|
|
+ log.info("exmodbus", "TCP 主站任务已启动")
|
|
|
+ elseif config.mode == exmodbus_ref.TCP_SLAVE then
|
|
|
+ -- 启动从站任务
|
|
|
+ sys.taskInitEx(tcp_slave_main_task_func, TASK_NAME, nil, instance)
|
|
|
+ log.info("exmodbus", "TCP 从站任务已启动")
|
|
|
+ else
|
|
|
+ log.error("exmodbus", "不支持的 TCP 模式")
|
|
|
+ return false
|
|
|
+ end
|
|
|
|
|
|
-- 返回实例;
|
|
|
return instance
|
|
|
@@ -465,6 +1135,104 @@ function modbus:destroy()
|
|
|
end
|
|
|
end
|
|
|
|
|
|
+-- 内部读函数
|
|
|
+function modbus:read_internal(config)
|
|
|
+ -- 处理响应结果;
|
|
|
+ local parsed_data = {}
|
|
|
+
|
|
|
+ -- 检查是否同时指定了 slave_id 和 raw_request
|
|
|
+ if config.slave_id and config.raw_request then
|
|
|
+ log.error("exmodbus", "禁止同时指定 slave_id 和 raw_request")
|
|
|
+
|
|
|
+ parsed_data.status = exmodbus_ref.STATUS_PARAM_INVALID
|
|
|
+ return parsed_data
|
|
|
+ end
|
|
|
+
|
|
|
+ if config.slave_id then
|
|
|
+ local request_frame, function_code, transaction_id = build_tcp_frame("read", config)
|
|
|
+ if not request_frame then
|
|
|
+ log.error("exmodbus", "构建 TCP 读取请求失败")
|
|
|
+ parsed_data.status = exmodbus_ref.STATUS_PARAM_INVALID
|
|
|
+ return parsed_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 发送请求并等待响应;
|
|
|
+ local result, response = sendRequest_waitResponse(self, request_frame, config)
|
|
|
+ if not result then
|
|
|
+ parsed_data.status = exmodbus_ref.STATUS_TIMEOUT
|
|
|
+ else
|
|
|
+ -- 解析响应数据;
|
|
|
+ parsed_data = parse_tcp_response(response, config, function_code, transaction_id)
|
|
|
+ end
|
|
|
+ elseif config.raw_request then
|
|
|
+ -- 发送请求并等待响应;
|
|
|
+ local result, response = sendRequest_waitResponse(self, config.raw_request, config)
|
|
|
+ if not result then
|
|
|
+ parsed_data.status = exmodbus_ref.STATUS_TIMEOUT
|
|
|
+ else
|
|
|
+ -- 直接返回响应结果和原始响应数据;
|
|
|
+ parsed_data.status = exmodbus_ref.STATUS_SUCCESS
|
|
|
+ parsed_data.raw_response = response
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ return parsed_data
|
|
|
+end
|
|
|
+
|
|
|
+-- 主站写入请求的函数;
|
|
|
+function modbus:write_internal(config)
|
|
|
+ -- 处理响应结果;
|
|
|
+ local parsed_data = {}
|
|
|
+
|
|
|
+ -- 检查是否同时指定了 slave_id 和 raw_request
|
|
|
+ if config.slave_id and config.raw_request then
|
|
|
+ log.error("exmodbus", "禁止同时指定 slave_id 和 raw_request")
|
|
|
+
|
|
|
+ parsed_data.status = exmodbus_ref.STATUS_PARAM_INVALID
|
|
|
+ return parsed_data
|
|
|
+ end
|
|
|
+
|
|
|
+ if config.slave_id then
|
|
|
+ local request_frame, function_code, transaction_id = build_tcp_frame("write", config)
|
|
|
+ if not request_frame then
|
|
|
+ log.error("exmodbus", "构建 TCP 写入请求失败")
|
|
|
+ parsed_data.status = exmodbus_ref.STATUS_PARAM_INVALID
|
|
|
+ return parsed_data
|
|
|
+ end
|
|
|
+
|
|
|
+ -- 发送请求并等待响应;
|
|
|
+ local result, response = sendRequest_waitResponse(self, request_frame, config)
|
|
|
+ if not result then
|
|
|
+ parsed_data.status = exmodbus_ref.STATUS_TIMEOUT
|
|
|
+ else
|
|
|
+ -- 解析响应数据;
|
|
|
+ parsed_data = parse_tcp_response(response, config, function_code, transaction_id)
|
|
|
+ end
|
|
|
+ elseif config.raw_request then
|
|
|
+ -- 发送请求并等待响应;
|
|
|
+ local result, response = sendRequest_waitResponse(self, config.raw_request, config)
|
|
|
+ if not result then
|
|
|
+ parsed_data.status = exmodbus_ref.STATUS_TIMEOUT
|
|
|
+ else
|
|
|
+ -- 直接返回响应结果和原始响应数据;
|
|
|
+ parsed_data.status = exmodbus_ref.STATUS_SUCCESS
|
|
|
+ parsed_data.raw_response = response
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ return parsed_data
|
|
|
+end
|
|
|
+
|
|
|
+-- 读函数(主站使用)
|
|
|
+function modbus:read(config)
|
|
|
+ return exmodbus_ref.enqueue_request(self, config, true)
|
|
|
+end
|
|
|
+
|
|
|
+-- 写函数(主站使用)
|
|
|
+function modbus:write(config)
|
|
|
+ return exmodbus_ref.enqueue_request(self, config, false)
|
|
|
+end
|
|
|
+
|
|
|
-- 注册从站请求处理回调函数;
|
|
|
function modbus:on(callback)
|
|
|
if type(callback) ~= "function" then
|