diff --git a/rtu/config.lua b/rtu/config.lua index 55968e1..7dc1baa 100644 --- a/rtu/config.lua +++ b/rtu/config.lua @@ -31,17 +31,17 @@ config.RTU_REDSTONE = { -- for_reactor = 1, -- io = { -- { - -- channel = rsio.IO.WASTE_PO, + -- port = rsio.IO.WASTE_PO, -- side = "top", -- bundled_color = colors.blue -- }, -- { - -- channel = rsio.IO.WASTE_PU, + -- port = rsio.IO.WASTE_PU, -- side = "top", -- bundled_color = colors.cyan -- }, -- { - -- channel = rsio.IO.WASTE_AM, + -- port = rsio.IO.WASTE_AM, -- side = "top", -- bundled_color = colors.purple -- } diff --git a/rtu/dev/redstone_rtu.lua b/rtu/dev/redstone_rtu.lua index f461fdf..13ca83b 100644 --- a/rtu/dev/redstone_rtu.lua +++ b/rtu/dev/redstone_rtu.lua @@ -4,9 +4,10 @@ local rsio = require("scada-common.rsio") local redstone_rtu = {} +local IO_LVL = rsio.IO_LVL + local digital_read = rsio.digital_read local digital_write = rsio.digital_write -local digital_is_active = rsio.digital_is_active -- create new redstone device function redstone_rtu.new() @@ -47,10 +48,9 @@ function redstone_rtu.new() end -- link digital output - ---@param channel RS_IO ---@param side string ---@param color integer - function public.link_do(channel, side, color) + function public.link_do(side, color) local f_read = nil local f_write = nil @@ -60,15 +60,17 @@ function redstone_rtu.new() end f_write = function (level) - local output = rs.getBundledOutput(side) + if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then + local output = rs.getBundledOutput(side) - if digital_write(channel, level) then - output = colors.combine(output, color) - else - output = colors.subtract(output, color) + if digital_write(level) then + output = colors.combine(output, color) + else + output = colors.subtract(output, color) + end + + rs.setBundledOutput(side, output) end - - rs.setBundledOutput(side, output) end else f_read = function () @@ -76,7 +78,9 @@ function redstone_rtu.new() end f_write = function (level) - rs.setOutput(side, digital_is_active(channel, level)) + if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then + rs.setOutput(side, digital_write(level)) + end end end diff --git a/rtu/modbus.lua b/rtu/modbus.lua index fea8e45..5411f37 100644 --- a/rtu/modbus.lua +++ b/rtu/modbus.lua @@ -366,7 +366,7 @@ function modbus.new(rtu_dev, use_parallel_read) local return_code = true local response = nil - if packet.length == 2 then + if packet.length >= 2 then -- 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]) @@ -381,9 +381,9 @@ function modbus.new(rtu_dev, use_parallel_read) 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]) elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_COILS then - return_code, response = _15_write_multiple_coils(packet.data[1], packet.data[2]) + return_code, response = _15_write_multiple_coils(packet.data[1], { table.unpack(packet.data, 2, packet.length) }) elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_HOLD_REGS then - return_code, response = _16_write_multiple_holding_registers(packet.data[1], packet.data[2]) + return_code, response = _16_write_multiple_holding_registers(packet.data[1], { table.unpack(packet.data, 2, packet.length) }) else -- unknown function return_code = false @@ -392,6 +392,7 @@ function modbus.new(rtu_dev, use_parallel_read) else -- invalid length return_code = false + response = MODBUS_EXCODE.NEG_ACKNOWLEDGE end -- default is to echo back diff --git a/rtu/startup.lua b/rtu/startup.lua index 76944c6..2306bcd 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -25,7 +25,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "beta-v0.9.7" +local RTU_VERSION = "beta-v0.9.8" local rtu_t = types.rtu_t @@ -166,7 +166,7 @@ local function main() local conf = io_table[i] -- verify configuration - if rsio.is_valid_channel(conf.channel) and rsio.is_valid_side(conf.side) then + if rsio.is_valid_port(conf.port) and rsio.is_valid_side(conf.side) then if conf.bundled_color then valid = rsio.is_color(conf.bundled_color) else @@ -182,22 +182,22 @@ local function main() return false else -- link redstone in RTU - local mode = rsio.get_io_mode(conf.channel) + local mode = rsio.get_io_mode(conf.port) if mode == rsio.IO_MODE.DIGITAL_IN then -- can't have duplicate inputs - if util.table_contains(capabilities, conf.channel) then - local message = util.c("configure> skipping duplicate input for channel ", rsio.to_string(conf.channel), " on side ", conf.side) + if util.table_contains(capabilities, conf.port) then + local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(conf.port), " on side ", conf.side) println(message) log.warning(message) else rs_rtu.link_di(conf.side, conf.bundled_color) end elseif mode == rsio.IO_MODE.DIGITAL_OUT then - rs_rtu.link_do(conf.channel, conf.side, conf.bundled_color) + rs_rtu.link_do(conf.side, conf.bundled_color) elseif mode == rsio.IO_MODE.ANALOG_IN then -- can't have duplicate inputs - if util.table_contains(capabilities, conf.channel) then - local message = util.c("configure> skipping duplicate input for channel ", rsio.to_string(conf.channel), " on side ", conf.side) + if util.table_contains(capabilities, conf.port) then + local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(conf.port), " on side ", conf.side) println(message) log.warning(message) else @@ -206,15 +206,15 @@ local function main() elseif mode == rsio.IO_MODE.ANALOG_OUT then rs_rtu.link_ao(conf.side) else - -- should be unreachable code, we already validated channels + -- should be unreachable code, we already validated ports log.error("configure> fell through if chain attempting to identify IO mode", true) println("configure> encountered a software error, check logs") return false end - table.insert(capabilities, conf.channel) + table.insert(capabilities, conf.port) - log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(conf.channel), + log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(conf.port), " (", conf.side, ") for reactor ", io_reactor)) end end @@ -225,7 +225,7 @@ local function main() type = rtu_t.redstone, index = entry_idx, reactor = io_reactor, - device = capabilities, -- use device field for redstone channels + device = capabilities, -- use device field for redstone ports formed = nil, ---@type boolean|nil rtu = rs_rtu, ---@type rtu_device|rtu_rs_device modbus_io = modbus.new(rs_rtu, false), diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index 8598e85..15bb258 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -4,6 +4,7 @@ local util = require("scada-common.util") +---@class rsio local rsio = {} ---------------------- @@ -12,9 +13,10 @@ local rsio = {} ---@alias IO_LVL integer local IO_LVL = { + DISCONNECT = -1, -- use for RTU session to indicate this RTU is not connected to this port LOW = 0, HIGH = 1, - DISCONNECT = -1 -- use for RTU session to indicate this RTU is not connected to this channel + FLOATING = 2 -- use for RTU session to indicate this RTU is connected but not yet read } ---@alias IO_DIR integer @@ -31,8 +33,8 @@ local IO_MODE = { ANALOG_OUT = 3 } ----@alias RS_IO integer -local RS_IO = { +---@alias IO_PORT integer +local IO_PORT = { -- digital inputs -- -- facility @@ -48,45 +50,47 @@ local RS_IO = { F_ALARM = 4, -- active high, facility safety alarm -- waste - WASTE_PO = 5, -- active low, polonium routing - WASTE_PU = 6, -- active low, plutonium routing - WASTE_AM = 7, -- active low, antimatter routing + WASTE_PU = 5, -- active low, waste -> plutonium -> pellets route + WASTE_PO = 6, -- active low, waste -> polonium route + WASTE_POPL = 7, -- active low, polonium -> pellets route + WASTE_AM = 8, -- active low, polonium -> anti-matter route -- reactor - R_ALARM = 8, -- active high, reactor safety alarm - R_SCRAMMED = 9, -- active high, if the reactor is scrammed - R_AUTO_SCRAM = 10, -- active high, if the reactor was automatically scrammed - R_ACTIVE = 11, -- active high, if the reactor is active - R_AUTO_CTRL = 12, -- active high, if the reactor burn rate is automatic - R_DMG_CRIT = 13, -- active high, if the reactor damage is critical - R_HIGH_TEMP = 14, -- active high, if the reactor is at a high temperature - R_NO_COOLANT = 15, -- active high, if the reactor has no coolant - R_EXCESS_HC = 16, -- active high, if the reactor has excess heated coolant - R_EXCESS_WS = 17, -- active high, if the reactor has excess waste - R_INSUFF_FUEL = 18, -- active high, if the reactor has insufficent fuel - R_PLC_FAULT = 19, -- active high, if the reactor PLC reports a device access fault - R_PLC_TIMEOUT = 20 -- active high, if the reactor PLC has not been heard from + R_ALARM = 9, -- active high, reactor safety alarm + R_SCRAMMED = 10, -- active high, if the reactor is scrammed + R_AUTO_SCRAM = 11, -- active high, if the reactor was automatically scrammed + R_ACTIVE = 12, -- active high, if the reactor is active + R_AUTO_CTRL = 13, -- active high, if the reactor burn rate is automatic + R_DMG_CRIT = 14, -- active high, if the reactor damage is critical + R_HIGH_TEMP = 15, -- active high, if the reactor is at a high temperature + R_NO_COOLANT = 16, -- active high, if the reactor has no coolant + R_EXCESS_HC = 17, -- active high, if the reactor has excess heated coolant + R_EXCESS_WS = 18, -- active high, if the reactor has excess waste + R_INSUFF_FUEL = 19, -- active high, if the reactor has insufficent fuel + R_PLC_FAULT = 20, -- active high, if the reactor PLC reports a device access fault + R_PLC_TIMEOUT = 21 -- active high, if the reactor PLC has not been heard from } rsio.IO_LVL = IO_LVL rsio.IO_DIR = IO_DIR rsio.IO_MODE = IO_MODE -rsio.IO = RS_IO +rsio.IO = IO_PORT ----------------------- -- UTILITY FUNCTIONS -- ----------------------- --- channel to string ----@param channel RS_IO -function rsio.to_string(channel) +-- port to string +---@param port IO_PORT +function rsio.to_string(port) local names = { "F_SCRAM", "R_SCRAM", "R_ENABLE", "F_ALARM", - "WASTE_PO", "WASTE_PU", + "WASTE_PO", + "WASTE_POPL", "WASTE_AM", "R_ALARM", "R_SCRAMMED", @@ -103,8 +107,8 @@ function rsio.to_string(channel) "R_PLC_TIMEOUT" } - if util.is_int(channel) and channel > 0 and channel <= #names then - return names[channel] + if util.is_int(port) and port > 0 and port <= #names then + return names[port] else return "" end @@ -112,64 +116,69 @@ end local _B_AND = bit.band -local function _ACTIVE_HIGH(level) return level == IO_LVL.HIGH end -local function _ACTIVE_LOW(level) return level == IO_LVL.LOW end +local function _I_ACTIVE_HIGH(level) return level == IO_LVL.HIGH end +local function _I_ACTIVE_LOW(level) return level == IO_LVL.LOW end +local function _O_ACTIVE_HIGH(active) if active then return IO_LVL.HIGH else return IO_LVL.LOW end end +local function _O_ACTIVE_LOW(active) if active then return IO_LVL.LOW else return IO_LVL.HIGH end end -- I/O mappings to I/O function and I/O mode local RS_DIO_MAP = { -- F_SCRAM - { _f = _ACTIVE_LOW, mode = IO_DIR.IN }, + { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN }, -- R_SCRAM - { _f = _ACTIVE_LOW, mode = IO_DIR.IN }, + { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN }, -- R_ENABLE - { _f = _ACTIVE_HIGH, mode = IO_DIR.IN }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, -- F_ALARM - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- WASTE_PO - { _f = _ACTIVE_LOW, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- WASTE_PU - { _f = _ACTIVE_LOW, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, + -- WASTE_PO + { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, + -- WASTE_POPL + { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, -- WASTE_AM - { _f = _ACTIVE_LOW, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, -- R_ALARM - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_SCRAMMED - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_AUTO_SCRAM - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_ACTIVE - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_AUTO_CTRL - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_DMG_CRIT - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_HIGH_TEMP - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_NO_COOLANT - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_EXCESS_HC - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_EXCESS_WS - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_INSUFF_FUEL - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_PLC_FAULT - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_PLC_TIMEOUT - { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT } + { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT } } --- get the mode of a channel ----@param channel RS_IO +-- get the mode of a port +---@param port IO_PORT ---@return IO_MODE -function rsio.get_io_mode(channel) +function rsio.get_io_mode(port) local modes = { IO_MODE.DIGITAL_IN, -- F_SCRAM IO_MODE.DIGITAL_IN, -- R_SCRAM IO_MODE.DIGITAL_IN, -- R_ENABLE IO_MODE.DIGITAL_OUT, -- F_ALARM - IO_MODE.DIGITAL_OUT, -- WASTE_PO IO_MODE.DIGITAL_OUT, -- WASTE_PU + IO_MODE.DIGITAL_OUT, -- WASTE_PO + IO_MODE.DIGITAL_OUT, -- WASTE_POPL IO_MODE.DIGITAL_OUT, -- WASTE_AM IO_MODE.DIGITAL_OUT, -- R_ALARM IO_MODE.DIGITAL_OUT, -- R_SCRAMMED @@ -186,8 +195,8 @@ function rsio.get_io_mode(channel) IO_MODE.DIGITAL_OUT -- R_PLC_TIMEOUT } - if util.is_int(channel) and channel > 0 and channel <= #modes then - return modes[channel] + if util.is_int(port) and port > 0 and port <= #modes then + return modes[port] else return IO_MODE.ANALOG_IN end @@ -199,11 +208,11 @@ end local RS_SIDES = rs.getSides() --- check if a channel is valid ----@param channel RS_IO +-- check if a port is valid +---@param port IO_PORT ---@return boolean valid -function rsio.is_valid_channel(channel) - return util.is_int(channel) and (channel > 0) and (channel <= RS_IO.R_PLC_TIMEOUT) +function rsio.is_valid_port(port) + return util.is_int(port) and (port > 0) and (port <= IO_PORT.R_PLC_TIMEOUT) end -- check if a side is valid @@ -229,7 +238,7 @@ end -- DIGITAL I/O -- ----------------- --- get digital IO level reading +-- get digital I/O level reading from a redstone boolean input value ---@param rs_value boolean ---@return IO_LVL function rsio.digital_read(rs_value) @@ -240,27 +249,36 @@ function rsio.digital_read(rs_value) end end --- returns the level corresponding to active ----@param channel RS_IO +-- get redstone boolean output value corresponding to a digital I/O level ---@param level IO_LVL ---@return boolean -function rsio.digital_write(channel, level) - if (not util.is_int(channel)) or (channel < RS_IO.F_ALARM) or (channel > RS_IO.R_PLC_TIMEOUT) then +function rsio.digital_write(level) + return level == IO_LVL.HIGH +end + +-- returns the level corresponding to active +---@param port IO_PORT +---@param active boolean +---@return IO_LVL|false +function rsio.digital_write_active(port, active) + if (not util.is_int(port)) or (port < IO_PORT.F_ALARM) or (port > IO_PORT.R_PLC_TIMEOUT) then return false else - return RS_DIO_MAP[channel]._f(level) + return RS_DIO_MAP[port]._out(active) end end -- returns true if the level corresponds to active ----@param channel RS_IO +---@param port IO_PORT ---@param level IO_LVL ----@return boolean -function rsio.digital_is_active(channel, level) - if (not util.is_int(channel)) or (channel > RS_IO.R_ENABLE) then - return false +---@return boolean|nil +function rsio.digital_is_active(port, level) + if (not util.is_int(port)) or (port > IO_PORT.R_ENABLE) then + return nil + elseif level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then + return nil else - return RS_DIO_MAP[channel]._f(level) + return RS_DIO_MAP[port]._in(level) end end diff --git a/scada-common/types.lua b/scada-common/types.lua index eaeced6..2ef80b3 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -35,6 +35,14 @@ types.TRI_FAIL = { FULL = 2 } +---@alias WASTE_MODE integer +types.WASTE_MODE = { + AUTO = 1, + PLUTONIUM = 2, + POLONIUM = 3, + ANTI_MATTER = 4 +} + ---@alias ALARM integer types.ALARM = { ContainmentBreach = 1, diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index 675c481..4dac34a 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -207,7 +207,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) end elseif cmd == CRDN_COMMANDS.SET_WASTE then if pkt.length == 3 then - self.out_q.push_data(SV_Q_DATA.SET_WASTE, data) + unit.set_waste(pkt.data[3]) else log.debug(log_header .. "CRDN command unit set waste missing option") end diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 997e198..922112f 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -27,26 +27,10 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts -local RTU_S_CMDS = { -} - -local RTU_S_DATA = { - RS_COMMAND = 1, - UNIT_COMMAND = 2 -} - -rtu.RTU_S_CMDS = RTU_S_CMDS -rtu.RTU_S_DATA = RTU_S_DATA - local PERIODICS = { KEEP_ALIVE = 2000 } ----@class rs_session_command ----@field reactor integer ----@field channel RS_IO ----@field value integer|boolean - -- create a new RTU session ---@param id integer ---@param in_queue mqueue @@ -74,8 +58,6 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units) last_update = 0, keep_alive = 0 }, - rs_io_q = {}, - turbine_cmd_q = {}, units = {} } @@ -84,8 +66,6 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units) local function _reset_config() self.units = {} - self.rs_io_q = {} - self.turbine_cmd_q = {} end -- parse the recorded advertisement and create unit sub-sessions @@ -141,14 +121,15 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units) if u_type == RTU_UNIT_TYPES.REDSTONE then -- redstone - unit, rs_in_q = svrs_redstone.new(self.id, i, unit_advert, self.modbus_q) + unit = svrs_redstone.new(self.id, i, unit_advert, self.modbus_q) + if type(unit) ~= "nil" then target_unit.add_redstone(unit) end elseif u_type == RTU_UNIT_TYPES.BOILER_VALVE then -- boiler (Mekanism 10.1+) unit = svrs_boilerv.new(self.id, i, unit_advert, self.modbus_q) if type(unit) ~= "nil" then target_unit.add_boiler(unit) end elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then -- turbine (Mekanism 10.1+) - unit, tbv_in_q = svrs_turbinev.new(self.id, i, unit_advert, self.modbus_q) + unit = svrs_turbinev.new(self.id, i, unit_advert, self.modbus_q) if type(unit) ~= "nil" then target_unit.add_turbine(unit) end elseif u_type == RTU_UNIT_TYPES.IMATRIX then -- induction matrix @@ -169,31 +150,6 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units) if unit ~= nil then table.insert(self.units, unit) - - if u_type == RTU_UNIT_TYPES.REDSTONE then - if self.rs_io_q[unit_advert.reactor] == nil then - self.rs_io_q[unit_advert.reactor] = rs_in_q - else - _reset_config() - log.error(log_header .. util.c("bad advertisement: duplicate redstone RTU for reactor " .. unit_advert.reactor)) - break - end - elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then - if self.turbine_cmd_q[unit_advert.reactor] == nil then - self.turbine_cmd_q[unit_advert.reactor] = {} - end - - local queues = self.turbine_cmd_q[unit_advert.reactor] - - if queues[unit_advert.index] == nil then - queues[unit_advert.index] = tbv_in_q - else - _reset_config() - log.error(log_header .. util.c("bad advertisement: duplicate turbine RTU (same index of ", - unit_advert.index, ") for reactor ", unit_advert.reactor)) - break - end - end else _reset_config() if type(u_type) == "number" then @@ -353,25 +309,6 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units) -- handle instruction elseif msg.qtype == mqueue.TYPE.DATA then -- instruction with body - local cmd = msg.message ---@type queue_data - if cmd.key == RTU_S_DATA.RS_COMMAND then - local rs_cmd = cmd.val ---@type rs_session_command - - if rsio.is_valid_channel(rs_cmd.channel) then - cmd.key = svrs_redstone.RS_RTU_S_DATA.RS_COMMAND - if rs_cmd.reactor == nil then - -- for all reactors (facility) - for i = 1, #self.rs_io_q do - local q = self.rs_io.q[i] ---@type mqueue - q.push_data(msg) - end - elseif self.rs_io_q[rs_cmd.reactor] ~= nil then - -- for just one reactor - local q = self.rs_io.q[rs_cmd.reactor] ---@type mqueue - q.push_data(msg) - end - end - end end end diff --git a/supervisor/session/rtu/qtypes.lua b/supervisor/session/rtu/qtypes.lua new file mode 100644 index 0000000..92a927f --- /dev/null +++ b/supervisor/session/rtu/qtypes.lua @@ -0,0 +1,16 @@ +---@class rtu_unit_qtypes +local qtypes = {} + +local TBV_RTU_S_CMDS = { + INC_DUMP_MODE = 1, + DEC_DUMP_MODE = 2 +} + +local TBV_RTU_S_DATA = { + SET_DUMP_MODE = 1 +} + +qtypes.TBV_RTU_S_CMDS = TBV_RTU_S_CMDS +qtypes.TBV_RTU_S_DATA = TBV_RTU_S_DATA + +return qtypes diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua index 90e4b58..a286f9c 100644 --- a/supervisor/session/rtu/redstone.lua +++ b/supervisor/session/rtu/redstone.lua @@ -12,39 +12,40 @@ local redstone = {} local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local MODBUS_FCODE = types.MODBUS_FCODE -local RS_IO = rsio.IO +local IO_PORT = rsio.IO local IO_LVL = rsio.IO_LVL local IO_DIR = rsio.IO_DIR local IO_MODE = rsio.IO_MODE -local RS_RTU_S_CMDS = { -} - -local RS_RTU_S_DATA = { - RS_COMMAND = 1 -} - -redstone.RS_RTU_S_CMDS = RS_RTU_S_CMDS -redstone.RS_RTU_S_DATA = RS_RTU_S_DATA +local TXN_READY = -1 local TXN_TYPES = { DI_READ = 1, COIL_WRITE = 2, - INPUT_REG_READ = 3, - HOLD_REG_WRITE = 4 + COIL_READ = 3, + INPUT_REG_READ = 4, + HOLD_REG_WRITE = 5, + HOLD_REG_READ = 6 } local TXN_TAGS = { "redstone.di_read", "redstone.coil_write", - "redstone.input_reg_write", - "redstone.hold_reg_write" + "redstone.coil_read", + "redstone.input_reg_read", + "redstone.hold_reg_write", + "redstone.hold_reg_read" } local PERIODICS = { - INPUT_READ = 200 + INPUT_READ = 200, + OUTPUT_SYNC = 200 } +---@class phy_entry +---@field phy IO_LVL +---@field req IO_LVL + -- create a new redstone rtu session runner ---@param session_id integer ---@param unit_id integer @@ -62,57 +63,127 @@ function redstone.new(session_id, unit_id, advert, out_queue) local self = { session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), - in_q = mqueue.new(), has_di = false, + has_do = false, has_ai = false, + has_ao = false, periodics = { - next_di_req = 0, - next_ir_req = 0 + next_di_req = 0, + next_cl_sync = 0, + next_ir_req = 0, + next_hr_sync = 0 }, + ---@class rs_io_list io_list = { - digital_in = {}, -- discrete inputs - digital_out = {}, -- coils - analog_in = {}, -- input registers - analog_out = {} -- holding registers + digital_in = {}, -- discrete inputs + digital_out = {}, -- coils + analog_in = {}, -- input registers + analog_out = {} -- holding registers }, - db = {} + 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 = {}, -- discrete inputs + digital_out = {}, -- coils + analog_in = {}, -- input registers + analog_out = {} -- holding registers + }, + ---@class redstone_session_db + db = { + -- read/write functions for connected I/O + io = {} + } } local public = self.session.get() -- INITIALIZE -- - -- create all channels as disconnected - for _ = 1, #RS_IO do + -- 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 channel = advert.rsio[i] + local port = advert.rsio[i] - if rsio.is_valid_channel(channel) then - local mode = rsio.get_io_mode(channel) + if rsio.is_valid_port(port) then + local mode = rsio.get_io_mode(port) if mode == IO_MODE.DIGITAL_IN then self.has_di = true - table.insert(self.io_list.digital_in, channel) + table.insert(self.io_list.digital_in, port) + + self.phy_io.digital_in[port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING } + + ---@class rs_db_dig_io + local io_f = { + read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end, + ---@param active boolean + write = function (active) end + } + + self.db.io[port] = io_f elseif mode == IO_MODE.DIGITAL_OUT then - table.insert(self.io_list.digital_out, channel) + self.has_do = true + table.insert(self.io_list.digital_out, port) + + self.phy_io.digital_out[port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING } + + ---@class rs_db_dig_io + local io_f = { + 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, channel) + 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 = { + ---@return integer + read = function () return self.phy_io.analog_in[port].phy end, + ---@param value integer + write = function (value) end + } + + self.db.io[port] = io_f elseif mode == IO_MODE.ANALOG_OUT then - table.insert(self.io_list.analog_out, channel) + 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 = { + ---@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 + end + end + } + + self.db.io[port] = io_f else - -- should be unreachable code, we already validated channels - log.error(util.c(log_tag, "failed to identify advertisement channel IO mode (", channel, ")"), true) + -- should be unreachable code, we already validated ports + log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", port, ")"), true) return nil end - - self.db[channel] = IO_LVL.LOW else - log.error(util.c(log_tag, "invalid advertisement channel (", channel, ")"), true) + log.error(util.c(log_tag, "invalid advertisement port (", port, ")"), true) return nil end end @@ -129,14 +200,40 @@ function redstone.new(session_id, unit_id, advert, out_queue) self.session.send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_list.analog_in }) end - -- write coil output - local function _write_coil(coil, value) - self.session.send_request(TXN_TYPES.COIL_WRITE, MODBUS_FCODE.WRITE_MUL_COILS, { coil, value }) + -- write all coil outputs + local function _write_coils() + 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) + end + + self.phy_trans.coils = self.session.send_request(TXN_TYPES.COIL_WRITE, MODBUS_FCODE.WRITE_MUL_COILS, params) end - -- write holding register output - local function _write_holding_register(reg, value) - self.session.send_request(TXN_TYPES.HOLD_REG_WRITE, MODBUS_FCODE.WRITE_MUL_HOLD_REGS, { reg, value }) + -- 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 }) + end + + -- write all holding register outputs + local function _write_holding_registers() + 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) + end + + self.phy_trans.hold_regs = self.session.send_request(TXN_TYPES.HOLD_REG_WRITE, MODBUS_FCODE.WRITE_MUL_HOLD_REGS, params) + end + + -- 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 }) end -- PUBLIC FUNCTIONS -- @@ -146,14 +243,23 @@ function redstone.new(session_id, unit_id, advert, out_queue) function public.handle_packet(m_pkt) local txn_type = self.session.try_resolve(m_pkt) if txn_type == false then - -- nothing to do + -- check if this is a failed write request + -- redstone operations are always immediately executed, so this would not be from an ACK or BUSY + if m_pkt.txn_id == self.phy_trans.coils then + self.phy_trans.coils = TXN_READY + log.debug(log_tag .. "failed to write coils, retrying soon") + elseif m_pkt.txn_id == self.phy_trans.hold_regs then + self.phy_trans.hold_regs = TXN_READY + log.debug(log_tag .. "failed to write holding registers, retrying soon") + end elseif txn_type == TXN_TYPES.DI_READ then -- discrete input read response if m_pkt.length == #self.io_list.digital_in then for i = 1, m_pkt.length do - local channel = self.io_list.digital_in[i] + local port = self.io_list.digital_in[i] local value = m_pkt.data[i] - self.db[channel] = value + + self.phy_io.digital_in[port].phy = value end else log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") @@ -162,15 +268,55 @@ function redstone.new(session_id, unit_id, advert, out_queue) -- input register read response if m_pkt.length == #self.io_list.analog_in then for i = 1, m_pkt.length do - local channel = self.io_list.analog_in[i] + local port = self.io_list.analog_in[i] local value = m_pkt.data[i] - self.db[channel] = value + + self.phy_io.analog_in[port].phy = value end else log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") end - elseif txn_type == TXN_TYPES.COIL_WRITE or txn_type == TXN_TYPES.HOLD_REG_WRITE then - -- successful acknowledgement + elseif txn_type == TXN_TYPES.COIL_WRITE then + -- successful acknowledgement, read back + _read_coils() + elseif txn_type == TXN_TYPES.COIL_READ then + -- 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 + for i = 1, m_pkt.length do + local port = self.io_list.digital_out[i] + 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 + end + + self.phy_trans.coils = TXN_READY + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") + end + elseif txn_type == TXN_TYPES.HOLD_REG_WRITE then + -- successful acknowledgement, read back + _read_holding_registers() + elseif txn_type == TXN_TYPES.HOLD_REG_READ then + -- 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 + for i = 1, m_pkt.length do + local port = self.io_list.analog_out[i] + local value = m_pkt.data[i] + + self.phy_io.analog_out[port].phy = value + end + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") + end + + self.phy_trans.hold_regs = TXN_READY elseif txn_type == nil then log.error(log_tag .. "unknown transaction reply") else @@ -181,60 +327,6 @@ function redstone.new(session_id, unit_id, advert, out_queue) -- update this runner ---@param time_now integer milliseconds function public.update(time_now) - -- check command queue - while self.in_q.ready() do - -- get a new message to process - local msg = self.in_q.pop() - - if msg ~= nil then - if msg.qtype == mqueue.TYPE.DATA then - -- instruction with body - local cmd = msg.message ---@type queue_data - if cmd.key == RS_RTU_S_DATA.RS_COMMAND then - local rs_cmd = cmd.val ---@type rs_session_command - - if self.db[rs_cmd.channel] ~= IO_LVL.DISCONNECT then - -- we have this as a connected channel - local mode = rsio.get_io_mode(rs_cmd.channel) - if mode == IO_MODE.DIGITAL_OUT then - -- record the value for retries - self.db[rs_cmd.channel] = rs_cmd.value - - -- find the coil address then write to it - for i = 0, #self.digital_out do - if self.digital_out[i] == rs_cmd.channel then - _write_coil(i, rs_cmd.value) - break - end - end - elseif mode == IO_MODE.ANALOG_OUT then - -- record the value for retries - self.db[rs_cmd.channel] = rs_cmd.value - - -- find the holding register address then write to it - for i = 0, #self.analog_out do - if self.analog_out[i] == rs_cmd.channel then - _write_holding_register(i, rs_cmd.value) - break - end - end - else - log.debug(util.c(log_tag, "attemted write to non D/O or A/O mode ", mode)) - end - end - end - end - end - - -- max 100ms spent processing queue - if util.time() - time_now > 100 then - log.warning(log_tag .. "exceeded 100ms queue process limit") - break - end - end - - time_now = util.time() - -- poll digital inputs if self.has_di then if self.periodics.next_di_req <= time_now then @@ -243,6 +335,20 @@ function redstone.new(session_id, unit_id, advert, out_queue) end end + -- 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 + _write_coils() + break + end + end + + self.periodics.next_cl_sync = time_now + PERIODICS.OUTPUT_SYNC + end + end + -- poll analog inputs if self.has_ai then if self.periodics.next_ir_req <= time_now then @@ -251,6 +357,20 @@ function redstone.new(session_id, unit_id, advert, out_queue) end end + -- 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 + _write_holding_registers() + break + end + end + + self.periodics.next_hr_sync = time_now + PERIODICS.OUTPUT_SYNC + end + end + self.session.post_update() end @@ -262,7 +382,7 @@ function redstone.new(session_id, unit_id, advert, out_queue) -- get the unit session database function public.get_db() return self.db end - return public, self.in_q + return public end return redstone diff --git a/supervisor/session/rtu/turbinev.lua b/supervisor/session/rtu/turbinev.lua index ad3fea7..d1bba6b 100644 --- a/supervisor/session/rtu/turbinev.lua +++ b/supervisor/session/rtu/turbinev.lua @@ -4,6 +4,7 @@ local mqueue = require("scada-common.mqueue") local types = require("scada-common.types") local util = require("scada-common.util") +local qtypes = require("supervisor.session.rtu.qtypes") local unit_session = require("supervisor.session.rtu.unit_session") local turbinev = {} @@ -12,17 +13,8 @@ local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local DUMPING_MODE = types.DUMPING_MODE local MODBUS_FCODE = types.MODBUS_FCODE -local TBV_RTU_S_CMDS = { - INC_DUMP_MODE = 1, - DEC_DUMP_MODE = 2 -} - -local TBV_RTU_S_DATA = { - SET_DUMP_MODE = 1 -} - -turbinev.RS_RTU_S_CMDS = TBV_RTU_S_CMDS -turbinev.RS_RTU_S_DATA = TBV_RTU_S_DATA +local TBV_RTU_S_CMDS = qtypes.TBV_RTU_S_CMDS +local TBV_RTU_S_DATA = qtypes.TBV_RTU_S_DATA local TXN_TYPES = { FORMED = 1, @@ -67,7 +59,6 @@ function turbinev.new(session_id, unit_id, advert, out_queue) local self = { session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), - in_q = mqueue.new(), has_build = false, periodics = { next_formed_req = 0, @@ -240,9 +231,9 @@ function turbinev.new(session_id, unit_id, advert, out_queue) ---@param time_now integer milliseconds function public.update(time_now) -- check command queue - while self.in_q.ready() do + while self.session.in_q.ready() do -- get a new message to process - local msg = self.in_q.pop() + local msg = self.session.in_q.pop() if msg ~= nil then if msg.qtype == mqueue.TYPE.COMMAND then @@ -254,15 +245,21 @@ function turbinev.new(session_id, unit_id, advert, out_queue) elseif cmd == TBV_RTU_S_CMDS.DEC_DUMP_MODE then _dec_dump_mode() else - log.debug(util.c(log_tag, "unrecognized in_q command ", cmd)) + log.debug(util.c(log_tag, "unrecognized in-queue command ", cmd)) end elseif msg.qtype == mqueue.TYPE.DATA then -- instruction with body local cmd = msg.message ---@type queue_data if cmd.key == TBV_RTU_S_DATA.SET_DUMP_MODE then - _set_dump_mode(cmd.val) + if cmd.val == types.DUMPING_MODE.IDLE or + cmd.val == types.DUMPING_MODE.DUMPING_EXCESS or + cmd.val == types.DUMPING_MODE.DUMPING then + _set_dump_mode(cmd.val) + else + log.debug(util.c(log_tag, "unrecognized dumping mode \"", cmd.val, "\"")) + end else - log.debug(util.c(log_tag, "unrecognized in_q data ", cmd.key)) + log.debug(util.c(log_tag, "unrecognized in-queue data ", cmd.key)) end end end @@ -313,7 +310,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue) -- get the unit session database function public.get_db() return self.db end - return public, self.in_q + return public end return turbinev diff --git a/supervisor/session/rtu/unit_session.lua b/supervisor/session/rtu/unit_session.lua index a3f27f2..f6e4297 100644 --- a/supervisor/session/rtu/unit_session.lua +++ b/supervisor/session/rtu/unit_session.lua @@ -1,5 +1,6 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") local types = require("scada-common.types") local util = require("scada-common.util") @@ -42,7 +43,9 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t } ---@class _unit_session - local protected = {} + local protected = { + in_q = mqueue.new() + } ---@class unit_session local public = {} @@ -53,6 +56,7 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t ---@param txn_type integer transaction type ---@param f_code MODBUS_FCODE function code ---@param register_param table register range or register and values + ---@return integer txn_id transaction ID of this transaction function protected.send_request(txn_type, f_code, register_param) local m_pkt = comms.modbus_packet() local txn_id = self.transaction_controller.create(txn_type) @@ -60,11 +64,13 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t m_pkt.make(txn_id, self.unit_id, f_code, register_param) self.out_q.push_packet(m_pkt) + + return txn_id end -- try to resolve a MODBUS transaction ---@param m_pkt modbus_frame MODBUS packet - ---@return integer|false txn_type transaction type or false on error/busy + ---@return integer|false txn_type, integer txn_id transaction type or false on error/busy, transaction ID function protected.try_resolve(m_pkt) if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then if m_pkt.unit_id == self.unit_id then @@ -110,7 +116,7 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t self.device_fail = false -- no error, return the transaction type - return txn_type + return txn_type, m_pkt.txn_id end else log.error(log_tag .. "wrong unit ID: " .. m_pkt.unit_id, true) @@ -120,7 +126,7 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t end -- error or transaction in progress, return false - return false + return false, m_pkt.txn_id end -- post update tasks @@ -141,6 +147,8 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t function public.get_device_idx() return self.device_index end -- get the reactor ID function public.get_reactor() return self.reactor end + -- get the command queue + function public.get_cmd_queue() return protected.in_q end -- close this unit function public.close() self.connected = false end @@ -171,7 +179,10 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t end -- get the unit session database - function public.get_db() return {} end + function public.get_db() + log.debug("template unit_session.get_db() called", true) + return {} + end return protected end diff --git a/supervisor/session/svqtypes.lua b/supervisor/session/svqtypes.lua index 1b8b131..09ef4f1 100644 --- a/supervisor/session/svqtypes.lua +++ b/supervisor/session/svqtypes.lua @@ -9,9 +9,8 @@ local SV_Q_DATA = { SCRAM = 2, RESET_RPS = 3, SET_BURN = 4, - SET_WASTE = 5, - __END_PLC_CMDS__ = 6, - CRDN_ACK = 7 + __END_PLC_CMDS__ = 5, + CRDN_ACK = 6 } ---@class coord_ack diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index c4a6c8f..80ea0d6 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -82,8 +82,6 @@ local function _sv_handle_outq(session) plc_s.in_queue.push_command(PLC_S_CMDS.RPS_RESET) elseif cmd.key == SV_Q_DATA.SET_BURN and type(cmd.val) == "table" and #cmd.val == 2 then plc_s.in_queue.push_data(PLC_S_DATA.BURN_RATE, cmd.val[2]) - elseif cmd.key == SV_Q_DATA.SET_WASTE and type(cmd.val) == "table" and #cmd.val == 2 then - ---@todo set waste else log.debug(util.c("unknown PLC SV queue command ", cmd.key)) end diff --git a/supervisor/session/unit.lua b/supervisor/session/unit.lua index da74b30..fa85adc 100644 --- a/supervisor/session/unit.lua +++ b/supervisor/session/unit.lua @@ -1,9 +1,15 @@ -local types = require("scada-common.types") -local util = require("scada-common.util") -local log = require("scada-common.log") +local log = require("scada-common.log") +local rsio = require("scada-common.rsio") +local types = require("scada-common.types") +local util = require("scada-common.util") +local qtypes = require("supervisor.session.rtu.qtypes") + +---@class reactor_control_unit local unit = {} +local WASTE_MODE = types.WASTE_MODE + local ALARM = types.ALARM local PRIO = types.ALARM_PRIORITY local ALARM_STATE = types.ALARM_STATE @@ -11,6 +17,8 @@ local ALARM_STATE = types.ALARM_STATE local TRI_FAIL = types.TRI_FAIL local DUMPING_MODE = types.DUMPING_MODE +local IO = rsio.IO + local FLOW_STABILITY_DELAY_MS = 15000 local DT_KEYS = { @@ -105,6 +113,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- "It's just a routine turbin' trip!" -Bill Gibson TurbineTrip = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.TurbineTrip, tier = PRIO.URGENT } }, + ---@class unit_db db = { ---@class annunciator annunciator = { @@ -173,6 +182,8 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- PRIVATE FUNCTIONS -- + --#region time derivative utility functions + -- compute a change with respect to time of the given value ---@param key string value key ---@param value number value @@ -204,13 +215,44 @@ function unit.new(for_reactor, num_boilers, num_turbines) ---@param key string value key ---@return number local function _get_dt(key) - if self.deltas[key] then - return self.deltas[key].dt - else - return 0.0 + if self.deltas[key] then return self.deltas[key].dt else return 0.0 end + end + + --#endregion + + --#region redstone I/O + + -- write to a redstone port + local function __rs_w(port, value) + for i = 1, #self.redstone do + local db = self.redstone[i].get_db() ---@type redstone_session_db + local io = db.io[port] ---@type rs_db_dig_io|nil + if io ~= nil then io.write(value) end end end + -- read a redstone port
+ -- this will read from the first one encountered if there are multiple, because there should not be multiple + ---@param port IO_PORT + ---@return boolean|nil + local function __rs_r(port) + for i = 1, #self.redstone do + local db = self.redstone[i].get_db() ---@type redstone_session_db + local io = db.io[port] ---@type rs_db_dig_io|nil + if io ~= nil then return io.read() end + end + end + + -- waste valves + local waste_pu = { open = function () __rs_w(IO.WASTE_PU, true) end, close = function () __rs_w(IO.WASTE_PU, false) end } + local waste_sna = { open = function () __rs_w(IO.WASTE_PO, true) end, close = function () __rs_w(IO.WASTE_PO, false) end } + local waste_po = { open = function () __rs_w(IO.WASTE_POPL, true) end, close = function () __rs_w(IO.WASTE_POPL, false) end } + local waste_sps = { open = function () __rs_w(IO.WASTE_AM, true) end, close = function () __rs_w(IO.WASTE_AM, false) end } + + --#endregion + + --#region task helpers + -- update an alarm state given conditions ---@param tripped boolean if the alarm condition is still active ---@param alarm alarm_def alarm table @@ -335,6 +377,10 @@ function unit.new(for_reactor, num_boilers, num_turbines) end end + --#endregion + + --#region alarms and annunciator + -- update the annunciator local function _update_annunciator() -- update deltas @@ -617,6 +663,8 @@ function unit.new(for_reactor, num_boilers, num_turbines) _update_alarm_state(any_trip, self.alarms.TurbineTrip) end + --#endregion + -- unlink disconnected units ---@param sessions table local function _unlink_disconnected_units(sessions) @@ -642,6 +690,13 @@ function unit.new(for_reactor, num_boilers, num_turbines) _reset_dt(DT_KEYS.ReactorHCool) end + -- link a redstone RTU session + ---@param rs_unit unit_session + function public.add_redstone(rs_unit) + -- insert into list + table.insert(self.redstone, rs_unit) + end + -- link a turbine RTU session ---@param turbine unit_session function public.add_turbine(turbine) @@ -676,17 +731,6 @@ function unit.new(for_reactor, num_boilers, num_turbines) end end - -- link a redstone RTU capability - function public.add_redstone(field, accessor) - -- ensure field exists - if self.redstone[field] == nil then - self.redstone[field] = {} - end - - -- insert into list - table.insert(self.redstone[field], accessor) - end - -- purge devices associated with the given RTU session ID ---@param session integer RTU session ID function public.purge_rtu_devices(session) @@ -716,7 +760,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) _update_alarms() end - -- ACK/RESET ALARMS -- + -- OPERATIONS -- -- acknowledge all alarms (if possible) function public.ack_all() @@ -743,6 +787,32 @@ function unit.new(for_reactor, num_boilers, num_turbines) end end + -- route reactor waste + ---@param mode WASTE_MODE waste handling mode + function public.set_waste(mode) + if mode == WASTE_MODE.AUTO then + ---@todo automatic waste routing + elseif mode == WASTE_MODE.PLUTONIUM then + -- route through plutonium generation + waste_pu.open() + waste_sna.close() + waste_po.close() + waste_sps.close() + elseif mode == WASTE_MODE.POLONIUM then + -- route through polonium generation into pellets + waste_pu.close() + waste_sna.open() + waste_po.open() + waste_sps.close() + elseif mode == WASTE_MODE.ANTI_MATTER then + -- route through polonium generation into SPS + waste_pu.close() + waste_sna.open() + waste_po.close() + waste_sps.open() + end + end + -- READ STATES/PROPERTIES -- -- get build properties of all machines diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 5fa14de..3d27727 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "beta-v0.8.0" +local SUPERVISOR_VERSION = "beta-v0.8.1" local print = util.print local println = util.println diff --git a/test/rstest.lua b/test/rstest.lua index 1ed6827..d195fc6 100644 --- a/test/rstest.lua +++ b/test/rstest.lua @@ -16,9 +16,9 @@ local IO_MODE = rsio.IO_MODE println("starting RSIO tester") println("") -println(">>> checking valid channels:") +println(">>> checking valid ports:") --- channel function tests +-- port function tests local cid = 0 local max_value = 1 for key, value in pairs(IO) do @@ -38,18 +38,18 @@ for key, value in pairs(IO) do elseif io_mode == IO_MODE.ANALOG_OUT then mode = " (ANALOG_OUT)" else - error("unknown mode for channel " .. key) + error("unknown mode for port " .. key) end assert(key == c_name, c_name .. " != " .. key .. ": " .. value .. mode) println(c_name .. ": " .. value .. mode) end -assert(max_value == cid, "RS_IO last IDx out-of-sync with count: " .. max_value .. " (count " .. cid .. ")") +assert(max_value == cid, "IO_PORT last IDx out-of-sync with count: " .. max_value .. " (count " .. cid .. ")") testutils.pause() -println(">>> checking invalid channels:") +println(">>> checking invalid ports:") testutils.test_func("rsio.to_string", rsio.to_string, { -1, 100, false }, "") testutils.test_func_nil("rsio.to_string", rsio.to_string, "") @@ -61,8 +61,8 @@ testutils.pause() println(">>> checking validity checks:") local ivc_t_list = { 0, -1, 100 } -testutils.test_func("rsio.is_valid_channel", rsio.is_valid_channel, ivc_t_list, false) -testutils.test_func_nil("rsio.is_valid_channel", rsio.is_valid_channel, false) +testutils.test_func("rsio.is_valid_port", rsio.is_valid_port, ivc_t_list, false) +testutils.test_func_nil("rsio.is_valid_port", rsio.is_valid_port, false) local ivs_t_list = rs.getSides() testutils.test_func("rsio.is_valid_side", rsio.is_valid_side, ivs_t_list, true) @@ -76,7 +76,7 @@ testutils.test_func_nil("rsio.is_color", rsio.is_color, false) testutils.pause() -println(">>> checking channel-independent I/O wrappers:") +println(">>> checking port-independent I/O wrappers:") testutils.test_func("rsio.digital_read", rsio.digital_read, { true, false }, { IO_LVL.HIGH, IO_LVL.LOW }) @@ -97,11 +97,11 @@ println("PASS") testutils.pause() -println(">>> checking channel I/O:") +println(">>> checking port I/O:") print("rsio.digital_is_active(...): ") --- check input channels +-- check input ports assert(rsio.digital_is_active(IO.F_SCRAM, IO_LVL.LOW) == true, "IO_F_SCRAM_HIGH") assert(rsio.digital_is_active(IO.F_SCRAM, IO_LVL.HIGH) == false, "IO_F_SCRAM_LOW") assert(rsio.digital_is_active(IO.R_SCRAM, IO_LVL.LOW) == true, "IO_R_SCRAM_HIGH") @@ -115,30 +115,32 @@ assert(rsio.digital_is_active(IO.F_ALARM, IO_LVL.HIGH) == false, "IO_OUT_READ_HI println("PASS") --- check output channels +-- check output ports print("rsio.digital_write(...): ") --- check output channels -assert(rsio.digital_write(IO.F_ALARM, IO_LVL.LOW) == false, "IO_F_ALARM_FALSE") -assert(rsio.digital_write(IO.F_ALARM, IO_LVL.HIGH) == true, "IO_F_ALARM_TRUE") -assert(rsio.digital_write(IO.WASTE_PO, IO_LVL.HIGH) == false, "IO_WASTE_PO_FALSE") -assert(rsio.digital_write(IO.WASTE_PO, IO_LVL.LOW) == true, "IO_WASTE_PO_TRUE") -assert(rsio.digital_write(IO.WASTE_PU, IO_LVL.HIGH) == false, "IO_WASTE_PU_FALSE") -assert(rsio.digital_write(IO.WASTE_PU, IO_LVL.LOW) == true, "IO_WASTE_PU_TRUE") -assert(rsio.digital_write(IO.WASTE_AM, IO_LVL.HIGH) == false, "IO_WASTE_AM_FALSE") -assert(rsio.digital_write(IO.WASTE_AM, IO_LVL.LOW) == true, "IO_WASTE_AM_TRUE") +-- check output ports +assert(rsio.digital_write_active(IO.F_ALARM, true) == IO_LVL.LOW, "IO_F_ALARM_LOW") +assert(rsio.digital_write_active(IO.F_ALARM, true) == IO_LVL.HIGH, "IO_F_ALARM_HIGH") +assert(rsio.digital_write_active(IO.WASTE_PU, true) == IO_LVL.HIGH, "IO_WASTE_PU_HIGH") +assert(rsio.digital_write_active(IO.WASTE_PU, true) == IO_LVL.LOW, "IO_WASTE_PU_LOW") +assert(rsio.digital_write_active(IO.WASTE_PO, true) == IO_LVL.HIGH, "IO_WASTE_PO_HIGH") +assert(rsio.digital_write_active(IO.WASTE_PO, true) == IO_LVL.LOW, "IO_WASTE_PO_LOW") +assert(rsio.digital_write_active(IO.WASTE_POPL, true) == IO_LVL.HIGH, "IO_WASTE_POPL_HIGH") +assert(rsio.digital_write_active(IO.WASTE_POPL, true) == IO_LVL.LOW, "IO_WASTE_POPL_LOW") +assert(rsio.digital_write_active(IO.WASTE_AM, true) == IO_LVL.HIGH, "IO_WASTE_AM_HIGH") +assert(rsio.digital_write_active(IO.WASTE_AM, true) == IO_LVL.LOW, "IO_WASTE_AM_LOW") --- check all reactor output channels (all are active high) +-- check all reactor output ports (all are active high) for i = IO.R_ALARM, (IO.R_PLC_TIMEOUT - IO.R_ALARM + 1) do - assert(rsio.to_string(i) ~= "", "REACTOR_IO_BAD_CHANNEL") - assert(rsio.digital_write(i, IO_LVL.LOW) == false, "IO_" .. rsio.to_string(i) .. "_FALSE") - assert(rsio.digital_write(i, IO_LVL.HIGH) == true, "IO_" .. rsio.to_string(i) .. "_TRUE") + assert(rsio.to_string(i) ~= "", "REACTOR_IO_BAD_PORT") + assert(rsio.digital_write_active(i, false) == IO_LVL.LOW, "IO_" .. rsio.to_string(i) .. "_LOW") + assert(rsio.digital_write_active(i, true) == IO_LVL.HIGH, "IO_" .. rsio.to_string(i) .. "_HIGH") end -- non-outputs should always return false -assert(rsio.digital_write(IO.F_SCRAM, IO_LVL.LOW) == false, "IO_IN_WRITE_LOW") -assert(rsio.digital_write(IO.F_SCRAM, IO_LVL.LOW) == false, "IO_IN_WRITE_HIGH") +assert(rsio.digital_write_active(IO.F_SCRAM, false) == IO_LVL.LOW, "IO_IN_WRITE_FALSE") +assert(rsio.digital_write_active(IO.F_SCRAM, true) == IO_LVL.LOW, "IO_IN_WRITE_TRUE") println("PASS")