cc-mek-scada/rtu/modbus.lua

462 lines
16 KiB
Lua
Raw Normal View History

local comms = require("scada-common.comms")
local types = require("scada-common.types")
local modbus = {}
local MODBUS_FCODE = types.MODBUS_FCODE
local MODBUS_EXCODE = types.MODBUS_EXCODE
2022-01-14 17:42:11 +00:00
-- new modbus comms handler object
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-12 19:36:27 +00:00
---@param rtu_dev rtu_device|rtu_rs_device RTU device
2022-05-11 16:03:15 +00:00
---@param use_parallel_read boolean whether or not to use parallel calls when reading
function modbus.new(rtu_dev, use_parallel_read)
2022-05-07 17:39:12 +00:00
local insert = table.insert
2023-02-22 04:50:43 +00:00
-- read a span of coils (digital outputs)<br>
-- returns a table of readings or a MODBUS_EXCODE error code
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param c_addr_start integer
---@param count integer
---@return boolean ok, table|MODBUS_EXCODE readings
local function _1_read_coils(c_addr_start, count)
2022-05-03 15:39:03 +00:00
local tasks = {}
local readings = {} ---@type table|MODBUS_EXCODE
local access_fault = false
2023-02-22 04:50:43 +00:00
local _, coils, _, _ = rtu_dev.io_count()
local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0)
2022-01-14 17:42:11 +00:00
if return_ok then
for i = 1, count do
local addr = c_addr_start + i - 1
2022-05-03 15:39:03 +00:00
2023-02-22 04:50:43 +00:00
if use_parallel_read then
2022-05-07 17:39:12 +00:00
insert(tasks, function ()
2023-02-22 04:50:43 +00:00
local reading, fault = rtu_dev.read_coil(addr)
2022-05-03 15:39:03 +00:00
if fault then access_fault = true else readings[i] = reading end
end)
else
2023-02-22 04:50:43 +00:00
readings[i], access_fault = rtu_dev.read_coil(addr)
2022-05-03 15:39:03 +00:00
if access_fault then
return_ok = false
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
break
end
end
end
-- run parallel tasks if configured
2023-02-22 04:50:43 +00:00
if use_parallel_read then
2022-05-03 15:39:03 +00:00
parallel.waitForAll(table.unpack(tasks))
2022-11-12 04:49:45 +00:00
end
2022-11-12 04:49:45 +00:00
if access_fault or #readings ~= count then
return_ok = false
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
2022-01-14 17:42:11 +00:00
end
else
readings = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
2022-01-14 17:42:11 +00:00
end
return return_ok, readings
end
2023-02-22 04:50:43 +00:00
-- read a span of discrete inputs (digital inputs)<br>
-- returns a table of readings or a MODBUS_EXCODE error code
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param di_addr_start integer
---@param count integer
---@return boolean ok, table|MODBUS_EXCODE readings
local function _2_read_discrete_inputs(di_addr_start, count)
2022-05-03 15:39:03 +00:00
local tasks = {}
local readings = {} ---@type table|MODBUS_EXCODE
local access_fault = false
2023-02-22 04:50:43 +00:00
local discrete_inputs, _, _, _ = rtu_dev.io_count()
local return_ok = ((di_addr_start + count) <= (discrete_inputs + 1)) and (count > 0)
2022-05-10 16:01:56 +00:00
2022-01-14 17:42:11 +00:00
if return_ok then
for i = 1, count do
local addr = di_addr_start + i - 1
2022-05-03 15:39:03 +00:00
2023-02-22 04:50:43 +00:00
if use_parallel_read then
2022-05-07 17:39:12 +00:00
insert(tasks, function ()
2023-02-22 04:50:43 +00:00
local reading, fault = rtu_dev.read_di(addr)
2022-05-03 15:39:03 +00:00
if fault then access_fault = true else readings[i] = reading end
end)
else
2023-02-22 04:50:43 +00:00
readings[i], access_fault = rtu_dev.read_di(addr)
2022-05-03 15:39:03 +00:00
if access_fault then
return_ok = false
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
break
end
end
end
-- run parallel tasks if configured
2023-02-22 04:50:43 +00:00
if use_parallel_read then
2022-05-03 15:39:03 +00:00
parallel.waitForAll(table.unpack(tasks))
2022-11-12 04:49:45 +00:00
end
2022-11-12 04:49:45 +00:00
if access_fault or #readings ~= count then
return_ok = false
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
2022-01-14 17:42:11 +00:00
end
else
readings = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
2022-01-14 17:42:11 +00:00
end
return return_ok, readings
end
2023-02-22 04:50:43 +00:00
-- read a span of holding registers (analog outputs)<br>
-- returns a table of readings or a MODBUS_EXCODE error code
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param hr_addr_start integer
---@param count integer
---@return boolean ok, table|MODBUS_EXCODE readings
local function _3_read_multiple_holding_registers(hr_addr_start, count)
2022-05-03 15:39:03 +00:00
local tasks = {}
local readings = {} ---@type table|MODBUS_EXCODE
local access_fault = false
2023-02-22 04:50:43 +00:00
local _, _, _, hold_regs = rtu_dev.io_count()
local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0)
2022-01-14 17:42:11 +00:00
if return_ok then
for i = 1, count do
local addr = hr_addr_start + i - 1
2022-05-03 15:39:03 +00:00
2023-02-22 04:50:43 +00:00
if use_parallel_read then
2022-05-07 17:39:12 +00:00
insert(tasks, function ()
2023-02-22 04:50:43 +00:00
local reading, fault = rtu_dev.read_holding_reg(addr)
2022-05-03 15:39:03 +00:00
if fault then access_fault = true else readings[i] = reading end
end)
else
2023-02-22 04:50:43 +00:00
readings[i], access_fault = rtu_dev.read_holding_reg(addr)
2022-05-03 15:39:03 +00:00
if access_fault then
return_ok = false
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
break
end
end
end
-- run parallel tasks if configured
2023-02-22 04:50:43 +00:00
if use_parallel_read then
2022-05-03 15:39:03 +00:00
parallel.waitForAll(table.unpack(tasks))
2022-11-12 04:49:45 +00:00
end
2022-11-12 04:49:45 +00:00
if access_fault or #readings ~= count then
return_ok = false
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
2022-01-14 17:42:11 +00:00
end
else
readings = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
2022-01-14 17:42:11 +00:00
end
return return_ok, readings
end
2023-02-22 04:50:43 +00:00
-- read a span of input registers (analog inputs)<br>
-- returns a table of readings or a MODBUS_EXCODE error code
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param ir_addr_start integer
---@param count integer
---@return boolean ok, table|MODBUS_EXCODE readings
local function _4_read_input_registers(ir_addr_start, count)
2022-05-03 15:39:03 +00:00
local tasks = {}
local readings = {} ---@type table|MODBUS_EXCODE
local access_fault = false
2023-02-22 04:50:43 +00:00
local _, _, input_regs, _ = rtu_dev.io_count()
local return_ok = ((ir_addr_start + count) <= (input_regs + 1)) and (count > 0)
2022-01-14 17:42:11 +00:00
if return_ok then
for i = 1, count do
local addr = ir_addr_start + i - 1
2022-05-03 15:39:03 +00:00
2023-02-22 04:50:43 +00:00
if use_parallel_read then
2022-05-07 17:39:12 +00:00
insert(tasks, function ()
2023-02-22 04:50:43 +00:00
local reading, fault = rtu_dev.read_input_reg(addr)
2022-05-03 15:39:03 +00:00
if fault then access_fault = true else readings[i] = reading end
end)
else
2023-02-22 04:50:43 +00:00
readings[i], access_fault = rtu_dev.read_input_reg(addr)
2022-05-03 15:39:03 +00:00
if access_fault then
return_ok = false
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
break
end
end
end
-- run parallel tasks if configured
2023-02-22 04:50:43 +00:00
if use_parallel_read then
2022-05-03 15:39:03 +00:00
parallel.waitForAll(table.unpack(tasks))
2022-11-12 04:49:45 +00:00
end
2022-11-12 04:49:45 +00:00
if access_fault or #readings ~= count then
return_ok = false
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
2022-01-14 17:42:11 +00:00
end
else
readings = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
2022-01-14 17:42:11 +00:00
end
return return_ok, readings
end
-- write a single coil (digital output)
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param c_addr integer
---@param value any
---@return boolean ok, MODBUS_EXCODE
local function _5_write_single_coil(c_addr, value)
2023-09-03 21:54:39 +00:00
local response = MODBUS_EXCODE.OK
2023-02-22 04:50:43 +00:00
local _, coils, _, _ = rtu_dev.io_count()
2022-01-14 17:42:11 +00:00
local return_ok = c_addr <= coils
2022-01-14 17:42:11 +00:00
if return_ok then
2023-02-22 04:50:43 +00:00
local access_fault = rtu_dev.write_coil(c_addr, value)
if access_fault then
return_ok = false
2022-05-10 16:01:56 +00:00
response = MODBUS_EXCODE.SERVER_DEVICE_FAIL
end
else
response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
2022-01-14 17:42:11 +00:00
end
return return_ok, response
2022-01-14 17:42:11 +00:00
end
-- write a single holding register (analog output)
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param hr_addr integer
---@param value any
---@return boolean ok, MODBUS_EXCODE
local function _6_write_single_holding_register(hr_addr, value)
2023-09-03 21:54:39 +00:00
local response = MODBUS_EXCODE.OK
2023-02-22 04:50:43 +00:00
local _, _, _, hold_regs = rtu_dev.io_count()
2022-01-14 17:42:11 +00:00
local return_ok = hr_addr <= hold_regs
2022-05-10 16:01:56 +00:00
2022-01-14 17:42:11 +00:00
if return_ok then
2023-02-22 04:50:43 +00:00
local access_fault = rtu_dev.write_holding_reg(hr_addr, value)
if access_fault then
return_ok = false
2022-05-10 16:01:56 +00:00
response = MODBUS_EXCODE.SERVER_DEVICE_FAIL
end
else
response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
2022-01-14 17:42:11 +00:00
end
2022-05-11 16:03:15 +00:00
return return_ok, response
2022-01-13 15:12:44 +00:00
end
-- write multiple coils (digital outputs)
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param c_addr_start integer
---@param values any
---@return boolean ok, MODBUS_EXCODE
local function _15_write_multiple_coils(c_addr_start, values)
2023-09-03 21:54:39 +00:00
local response = MODBUS_EXCODE.OK
2023-02-22 04:50:43 +00:00
local _, coils, _, _ = rtu_dev.io_count()
2022-01-14 17:42:11 +00:00
local count = #values
local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0)
2022-01-14 17:42:11 +00:00
if return_ok then
for i = 1, count do
local addr = c_addr_start + i - 1
2023-02-22 04:50:43 +00:00
local access_fault = rtu_dev.write_coil(addr, values[i])
if access_fault then
return_ok = false
2022-05-10 16:01:56 +00:00
response = MODBUS_EXCODE.SERVER_DEVICE_FAIL
break
end
2022-01-14 17:42:11 +00:00
end
else
response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
2022-01-14 17:42:11 +00:00
end
return return_ok, response
2022-01-14 17:42:11 +00:00
end
-- write multiple holding registers (analog outputs)
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param hr_addr_start integer
---@param values any
---@return boolean ok, MODBUS_EXCODE
local function _16_write_multiple_holding_registers(hr_addr_start, values)
2023-09-03 21:54:39 +00:00
local response = MODBUS_EXCODE.OK
2023-02-22 04:50:43 +00:00
local _, _, _, hold_regs = rtu_dev.io_count()
2022-01-14 17:42:11 +00:00
local count = #values
local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0)
2022-01-14 17:42:11 +00:00
if return_ok then
for i = 1, count do
local addr = hr_addr_start + i - 1
2023-02-22 04:50:43 +00:00
local access_fault = rtu_dev.write_holding_reg(addr, values[i])
if access_fault then
return_ok = false
2022-05-10 16:01:56 +00:00
response = MODBUS_EXCODE.SERVER_DEVICE_FAIL
break
end
2022-01-14 17:42:11 +00:00
end
else
response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
2022-01-14 17:42:11 +00:00
end
return return_ok, response
2022-01-14 17:42:11 +00:00
end
2023-02-22 04:50:43 +00:00
---@class modbus
local public = {}
2022-04-29 17:19:01 +00:00
-- validate a request without actually executing it
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param packet modbus_frame
---@return boolean return_code, modbus_packet reply
function public.check_request(packet)
2022-04-29 17:19:01 +00:00
local return_code = true
local response = { MODBUS_EXCODE.ACKNOWLEDGE }
if packet.length == 2 then
2022-04-29 17:19:01 +00:00
-- handle by function code
if packet.func_code == MODBUS_FCODE.READ_COILS then
elseif packet.func_code == MODBUS_FCODE.READ_DISCRETE_INPUTS then
elseif packet.func_code == MODBUS_FCODE.READ_MUL_HOLD_REGS then
elseif packet.func_code == MODBUS_FCODE.READ_INPUT_REGS then
2022-04-29 17:19:01 +00:00
elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_COIL then
elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_HOLD_REG then
elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_COILS then
elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_HOLD_REGS then
else
-- unknown function
return_code = false
response = { MODBUS_EXCODE.ILLEGAL_FUNCTION }
end
else
-- invalid length
return_code = false
response = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
end
-- default is to echo back<br>
-- but here we echo back with error flag, on success the "error" will be acknowledgement
local func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
2022-04-29 17:19:01 +00:00
-- create reply
local reply = comms.modbus_packet()
reply.make(packet.txn_id, packet.unit_id, func_code, response)
return return_code, reply
end
2022-04-23 00:21:28 +00:00
-- handle a MODBUS TCP packet and generate a reply
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param packet modbus_frame
---@return boolean return_code, modbus_packet reply
function public.handle_packet(packet)
local return_code ---@type boolean
local response ---@type table|MODBUS_EXCODE
2022-01-14 17:42:11 +00:00
if packet.length >= 2 then
2022-01-14 17:42:11 +00:00
-- handle by function code
if packet.func_code == MODBUS_FCODE.READ_COILS then
return_code, response = _1_read_coils(packet.data[1], packet.data[2])
2022-01-14 17:42:11 +00:00
elseif packet.func_code == MODBUS_FCODE.READ_DISCRETE_INPUTS then
return_code, response = _2_read_discrete_inputs(packet.data[1], packet.data[2])
2022-01-14 17:42:11 +00:00
elseif packet.func_code == MODBUS_FCODE.READ_MUL_HOLD_REGS then
return_code, response = _3_read_multiple_holding_registers(packet.data[1], packet.data[2])
elseif packet.func_code == MODBUS_FCODE.READ_INPUT_REGS then
return_code, response = _4_read_input_registers(packet.data[1], packet.data[2])
2022-01-14 17:42:11 +00:00
elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_COIL then
return_code, response = _5_write_single_coil(packet.data[1], packet.data[2])
2022-01-14 17:42:11 +00:00
elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_HOLD_REG then
return_code, response = _6_write_single_holding_register(packet.data[1], packet.data[2])
2022-01-14 17:42:11 +00:00
elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_COILS then
return_code, response = _15_write_multiple_coils(packet.data[1], { table.unpack(packet.data, 2, packet.length) })
2022-01-14 17:42:11 +00:00
elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_HOLD_REGS then
return_code, response = _16_write_multiple_holding_registers(packet.data[1], { table.unpack(packet.data, 2, packet.length) })
2022-01-14 17:42:11 +00:00
else
-- unknown function
return_code = false
response = MODBUS_EXCODE.ILLEGAL_FUNCTION
2022-01-14 17:42:11 +00:00
end
else
-- invalid length
return_code = false
response = MODBUS_EXCODE.NEG_ACKNOWLEDGE
2022-01-14 17:42:11 +00:00
end
2022-04-23 00:21:28 +00:00
-- default is to echo back
local func_code = packet.func_code
if not return_code then
2022-01-14 17:42:11 +00:00
-- echo back with error flag
2022-04-23 00:21:28 +00:00
func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
end
if type(response) == "table" then
2023-09-03 21:54:39 +00:00
elseif response == MODBUS_EXCODE.OK then
response = {}
else
response = { response }
2022-01-14 17:42:11 +00:00
end
2022-04-23 00:21:28 +00:00
-- create reply
local reply = comms.modbus_packet()
reply.make(packet.txn_id, packet.unit_id, func_code, response)
return return_code, reply
2022-01-13 15:12:44 +00:00
end
return public
end
2022-04-29 17:19:01 +00:00
-- return a SERVER_DEVICE_BUSY error reply
2023-02-22 04:50:43 +00:00
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__srv_device_busy(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.SERVER_DEVICE_BUSY }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
2022-04-23 00:21:28 +00:00
-- return a NEG_ACKNOWLEDGE error reply
2023-02-22 04:50:43 +00:00
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__neg_ack(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
2022-04-23 00:21:28 +00:00
-- return a GATEWAY_PATH_UNAVAILABLE error reply
2023-02-22 04:50:43 +00:00
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__gw_unavailable(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
2022-01-14 17:42:11 +00:00
end
return modbus