#604 reworked supervisor redstone RTU interface
This commit is contained in:
@@ -39,6 +39,9 @@ local PERIODICS = {
|
||||
OUTPUT_SYNC = 200
|
||||
}
|
||||
|
||||
-- create a new block of IO banks (facility, then each unit)
|
||||
local function new_io_block() return { [0] = {}, {}, {}, {}, {} } end
|
||||
|
||||
---@class dig_phy_entry
|
||||
---@field phy IO_LVL actual value
|
||||
---@field req IO_LVL commanded value
|
||||
@@ -74,27 +77,27 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
next_ir_req = 0,
|
||||
next_hr_sync = 0
|
||||
},
|
||||
---@class rs_io_list
|
||||
io_list = {
|
||||
digital_in = {}, ---@type IO_PORT[] discrete inputs
|
||||
digital_out = {}, ---@type IO_PORT[] coils
|
||||
analog_in = {}, ---@type IO_PORT[] input registers
|
||||
analog_out = {} ---@type IO_PORT[] holding registers
|
||||
---@class rs_io_map
|
||||
io_map = {
|
||||
digital_in = {}, ---@type { bank: integer, port: IO_PORT }[] discrete inputs
|
||||
digital_out = {}, ---@type { bank: integer, port: IO_PORT }[] coils
|
||||
analog_in = {}, ---@type { bank: integer, port: IO_PORT }[] input registers
|
||||
analog_out = {} ---@type { bank: integer, port: IO_PORT }[] holding registers
|
||||
},
|
||||
phy_trans = { coils = -1, hold_regs = -1 },
|
||||
-- last set/read ports (reflecting the current state of the RTU)
|
||||
---@class rs_io_states
|
||||
phy_io = {
|
||||
digital_in = {}, ---@type dig_phy_entry[] discrete inputs
|
||||
digital_out = {}, ---@type dig_phy_entry[] coils
|
||||
analog_in = {}, ---@type ana_phy_entry[] input registers
|
||||
analog_out = {} ---@type ana_phy_entry[] holding registers
|
||||
digital_in = new_io_block(), ---@type dig_phy_entry[][] discrete inputs
|
||||
digital_out = new_io_block(), ---@type dig_phy_entry[][] coils
|
||||
analog_in = new_io_block(), ---@type ana_phy_entry[][] input registers
|
||||
analog_out = new_io_block() ---@type ana_phy_entry[][] holding registers
|
||||
},
|
||||
---@class redstone_session_db
|
||||
db = {
|
||||
-- read/write functions for connected I/O
|
||||
---@type (rs_db_dig_io|rs_db_ana_io)[]
|
||||
io = {}
|
||||
---@type (rs_db_dig_io|rs_db_ana_io)[][]
|
||||
io = new_io_block()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,93 +106,91 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
|
||||
-- INITIALIZE --
|
||||
|
||||
-- create all ports as disconnected
|
||||
for _ = 1, #IO_PORT do
|
||||
table.insert(self.db, IO_LVL.DISCONNECT)
|
||||
end
|
||||
|
||||
-- setup I/O
|
||||
for i = 1, #advert.rsio do
|
||||
local port = advert.rsio[i]
|
||||
for bank = 0, 4 do
|
||||
for i = 1, #advert.rs_conns[bank] do
|
||||
local port = advert.rs_conns[bank][i]
|
||||
|
||||
if rsio.is_valid_port(port) then
|
||||
local mode = rsio.get_io_mode(port)
|
||||
if rsio.is_valid_port(port) then
|
||||
local mode = rsio.get_io_mode(port)
|
||||
local io_entry = { bank = bank, port = port }
|
||||
|
||||
if mode == IO_MODE.DIGITAL_IN then
|
||||
self.has_di = true
|
||||
table.insert(self.io_list.digital_in, port)
|
||||
if mode == IO_MODE.DIGITAL_IN then
|
||||
self.has_di = true
|
||||
table.insert(self.io_map.digital_in, io_entry)
|
||||
|
||||
self.phy_io.digital_in[port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING }
|
||||
self.phy_io.digital_in[bank][port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING }
|
||||
|
||||
---@class rs_db_dig_io
|
||||
local io_f = {
|
||||
---@nodiscard
|
||||
read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end,
|
||||
write = function () end
|
||||
}
|
||||
---@class rs_db_dig_io
|
||||
local io_f = {
|
||||
---@nodiscard
|
||||
read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[bank][port].phy) end,
|
||||
write = function () end
|
||||
}
|
||||
|
||||
self.db.io[port] = io_f
|
||||
elseif mode == IO_MODE.DIGITAL_OUT then
|
||||
self.has_do = true
|
||||
table.insert(self.io_list.digital_out, port)
|
||||
self.db.io[port] = io_f
|
||||
elseif mode == IO_MODE.DIGITAL_OUT then
|
||||
self.has_do = true
|
||||
table.insert(self.io_map.digital_out, io_entry)
|
||||
|
||||
self.phy_io.digital_out[port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING }
|
||||
self.phy_io.digital_out[bank][port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING }
|
||||
|
||||
---@class rs_db_dig_io
|
||||
local io_f = {
|
||||
---@nodiscard
|
||||
read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[port].phy) end,
|
||||
---@param active boolean
|
||||
write = function (active)
|
||||
local level = rsio.digital_write_active(port, active)
|
||||
if level ~= nil then self.phy_io.digital_out[port].req = level end
|
||||
end
|
||||
}
|
||||
|
||||
self.db.io[port] = io_f
|
||||
elseif mode == IO_MODE.ANALOG_IN then
|
||||
self.has_ai = true
|
||||
table.insert(self.io_list.analog_in, port)
|
||||
|
||||
self.phy_io.analog_in[port] = { phy = 0, req = 0 }
|
||||
|
||||
---@class rs_db_ana_io
|
||||
local io_f = {
|
||||
---@nodiscard
|
||||
---@return integer
|
||||
read = function () return self.phy_io.analog_in[port].phy end,
|
||||
write = function () end
|
||||
}
|
||||
|
||||
self.db.io[port] = io_f
|
||||
elseif mode == IO_MODE.ANALOG_OUT then
|
||||
self.has_ao = true
|
||||
table.insert(self.io_list.analog_out, port)
|
||||
|
||||
self.phy_io.analog_out[port] = { phy = 0, req = 0 }
|
||||
|
||||
---@class rs_db_ana_io
|
||||
local io_f = {
|
||||
---@nodiscard
|
||||
---@return integer
|
||||
read = function () return self.phy_io.analog_out[port].phy end,
|
||||
---@param value integer
|
||||
write = function (value)
|
||||
if value >= 0 and value <= 15 then
|
||||
self.phy_io.analog_out[port].req = value
|
||||
---@class rs_db_dig_io
|
||||
local io_f = {
|
||||
---@nodiscard
|
||||
read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[bank][port].phy) end,
|
||||
---@param active boolean
|
||||
write = function (active)
|
||||
local level = rsio.digital_write_active(port, active)
|
||||
if level ~= nil then self.phy_io.digital_out[bank][port].req = level end
|
||||
end
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
self.db.io[port] = io_f
|
||||
self.db.io[port] = io_f
|
||||
elseif mode == IO_MODE.ANALOG_IN then
|
||||
self.has_ai = true
|
||||
table.insert(self.io_map.analog_in, io_entry)
|
||||
|
||||
self.phy_io.analog_in[bank][port] = { phy = 0, req = 0 }
|
||||
|
||||
---@class rs_db_ana_io
|
||||
local io_f = {
|
||||
---@nodiscard
|
||||
---@return integer
|
||||
read = function () return self.phy_io.analog_in[bank][port].phy end,
|
||||
write = function () end
|
||||
}
|
||||
|
||||
self.db.io[port] = io_f
|
||||
elseif mode == IO_MODE.ANALOG_OUT then
|
||||
self.has_ao = true
|
||||
table.insert(self.io_map.analog_out, io_entry)
|
||||
|
||||
self.phy_io.analog_out[bank][port] = { phy = 0, req = 0 }
|
||||
|
||||
---@class rs_db_ana_io
|
||||
local io_f = {
|
||||
---@nodiscard
|
||||
---@return integer
|
||||
read = function () return self.phy_io.analog_out[bank][port].phy end,
|
||||
---@param value integer
|
||||
write = function (value)
|
||||
if value >= 0 and value <= 15 then
|
||||
self.phy_io.analog_out[bank][port].req = value
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
self.db.io[port] = io_f
|
||||
else
|
||||
-- should be unreachable code, we already validated ports
|
||||
log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", bank, ":", port, ")"), true)
|
||||
return nil
|
||||
end
|
||||
else
|
||||
-- should be unreachable code, we already validated ports
|
||||
log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", port, ")"), true)
|
||||
log.error(util.c(log_tag, "invalid advertisement port (", bank, ":", port, ")"), true)
|
||||
return nil
|
||||
end
|
||||
else
|
||||
log.error(util.c(log_tag, "invalid advertisement port (", port, ")"), true)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -197,12 +198,12 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
|
||||
-- query discrete inputs
|
||||
local function _request_discrete_inputs()
|
||||
self.session.send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_list.digital_in })
|
||||
self.session.send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_map.digital_in })
|
||||
end
|
||||
|
||||
-- query input registers
|
||||
local function _request_input_registers()
|
||||
self.session.send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_list.analog_in })
|
||||
self.session.send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_map.analog_in })
|
||||
end
|
||||
|
||||
-- write all coil outputs
|
||||
@@ -210,9 +211,9 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
local params = { 1 }
|
||||
|
||||
local outputs = self.phy_io.digital_out
|
||||
for i = 1, #self.io_list.digital_out do
|
||||
local port = self.io_list.digital_out[i]
|
||||
table.insert(params, outputs[port].req)
|
||||
for i = 1, #self.io_map.digital_out do
|
||||
local entry = self.io_map.digital_out[i]
|
||||
table.insert(params, outputs[entry.bank][entry.port].req)
|
||||
end
|
||||
|
||||
self.phy_trans.coils = self.session.send_request(TXN_TYPES.COIL_WRITE, MODBUS_FCODE.WRITE_MUL_COILS, params)
|
||||
@@ -220,7 +221,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
|
||||
-- read all coil outputs
|
||||
local function _read_coils()
|
||||
self.session.send_request(TXN_TYPES.COIL_READ, MODBUS_FCODE.READ_COILS, { 1, #self.io_list.digital_out })
|
||||
self.session.send_request(TXN_TYPES.COIL_READ, MODBUS_FCODE.READ_COILS, { 1, #self.io_map.digital_out })
|
||||
end
|
||||
|
||||
-- write all holding register outputs
|
||||
@@ -228,9 +229,9 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
local params = { 1 }
|
||||
|
||||
local outputs = self.phy_io.analog_out
|
||||
for i = 1, #self.io_list.analog_out do
|
||||
local port = self.io_list.analog_out[i]
|
||||
table.insert(params, outputs[port].req)
|
||||
for i = 1, #self.io_map.analog_out do
|
||||
local entry = self.io_map.analog_out[i]
|
||||
table.insert(params, outputs[entry.bank][entry.port].req)
|
||||
end
|
||||
|
||||
self.phy_trans.hold_regs = self.session.send_request(TXN_TYPES.HOLD_REG_WRITE, MODBUS_FCODE.WRITE_MUL_HOLD_REGS, params)
|
||||
@@ -238,7 +239,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
|
||||
-- read all holding register outputs
|
||||
local function _read_holding_registers()
|
||||
self.session.send_request(TXN_TYPES.HOLD_REG_READ, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, #self.io_list.analog_out })
|
||||
self.session.send_request(TXN_TYPES.HOLD_REG_READ, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, #self.io_map.analog_out })
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
@@ -259,24 +260,24 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
end
|
||||
elseif txn_type == TXN_TYPES.DI_READ then
|
||||
-- discrete input read response
|
||||
if m_pkt.length == #self.io_list.digital_in then
|
||||
if m_pkt.length == #self.io_map.digital_in then
|
||||
for i = 1, m_pkt.length do
|
||||
local port = self.io_list.digital_in[i]
|
||||
local entry = self.io_map.digital_in[i]
|
||||
local value = m_pkt.data[i]
|
||||
|
||||
self.phy_io.digital_in[port].phy = value
|
||||
self.phy_io.digital_in[entry.bank][entry.port].phy = value
|
||||
end
|
||||
else
|
||||
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
|
||||
end
|
||||
elseif txn_type == TXN_TYPES.INPUT_REG_READ then
|
||||
-- input register read response
|
||||
if m_pkt.length == #self.io_list.analog_in then
|
||||
if m_pkt.length == #self.io_map.analog_in then
|
||||
for i = 1, m_pkt.length do
|
||||
local port = self.io_list.analog_in[i]
|
||||
local entry = self.io_map.analog_in[i]
|
||||
local value = m_pkt.data[i]
|
||||
|
||||
self.phy_io.analog_in[port].phy = value
|
||||
self.phy_io.analog_in[entry.bank][entry.port].phy = value
|
||||
end
|
||||
else
|
||||
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
|
||||
@@ -288,15 +289,14 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
-- update phy I/O table
|
||||
-- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical)
|
||||
-- given these are redstone outputs, if one worked they all should have, so no additional verification will be done
|
||||
if m_pkt.length == #self.io_list.digital_out then
|
||||
if m_pkt.length == #self.io_map.digital_out then
|
||||
for i = 1, m_pkt.length do
|
||||
local port = self.io_list.digital_out[i]
|
||||
local entry = self.io_map.digital_out[i]
|
||||
local state = self.phy_io.digital_out[entry.bank][entry.port]
|
||||
local value = m_pkt.data[i]
|
||||
|
||||
self.phy_io.digital_out[port].phy = value
|
||||
if self.phy_io.digital_out[port].req == IO_LVL.FLOATING then
|
||||
self.phy_io.digital_out[port].req = value
|
||||
end
|
||||
state.phy = value
|
||||
if state.req == IO_LVL.FLOATING then state.req = value end
|
||||
end
|
||||
|
||||
self.phy_trans.coils = TXN_READY
|
||||
@@ -310,12 +310,12 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
-- update phy I/O table
|
||||
-- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical)
|
||||
-- given these are redstone outputs, if one worked they all should have, so no additional verification will be done
|
||||
if m_pkt.length == #self.io_list.analog_out then
|
||||
if m_pkt.length == #self.io_map.analog_out then
|
||||
for i = 1, m_pkt.length do
|
||||
local port = self.io_list.analog_out[i]
|
||||
local entry = self.io_map.analog_out[i]
|
||||
local value = m_pkt.data[i]
|
||||
|
||||
self.phy_io.analog_out[port].phy = value
|
||||
self.phy_io.analog_out[entry.bank][entry.port].phy = value
|
||||
end
|
||||
else
|
||||
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
|
||||
@@ -343,8 +343,17 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
-- sync digital outputs
|
||||
if self.has_do then
|
||||
if (self.periodics.next_cl_sync <= time_now) and (self.phy_trans.coils == TXN_READY) then
|
||||
for _, entry in pairs(self.phy_io.digital_out) do
|
||||
if entry.phy ~= entry.req then
|
||||
for bank = 0, 4 do
|
||||
local changed = false
|
||||
|
||||
for _, entry in pairs(self.phy_io.digital_out[bank]) do
|
||||
if entry.phy ~= entry.req then
|
||||
changed = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if changed then
|
||||
_write_coils()
|
||||
break
|
||||
end
|
||||
@@ -365,8 +374,17 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
-- sync analog outputs
|
||||
if self.has_ao then
|
||||
if (self.periodics.next_hr_sync <= time_now) and (self.phy_trans.hold_regs == TXN_READY) then
|
||||
for _, entry in pairs(self.phy_io.analog_out) do
|
||||
if entry.phy ~= entry.req then
|
||||
for bank = 0, 4 do
|
||||
local changed = false
|
||||
|
||||
for _, entry in pairs(self.phy_io.analog_out[bank]) do
|
||||
if entry.phy ~= entry.req then
|
||||
changed = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if changed then
|
||||
_write_holding_registers()
|
||||
break
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user