diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index c8a5f09..3e32d6d 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -110,9 +110,9 @@ function iocontrol.init(conf, comms) -- determine tank information if io.facility.tank_mode == 0 then io.facility.tank_defs = {} - -- on facility tank mode 0, setup tank defs to match unit TANK option + -- on facility tank mode 0, setup tank defs to match unit tank option for i = 1, conf.num_units do - io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TANK, 1, 0) + io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0) end io.facility.tank_list = { table.unpack(io.facility.tank_defs) } @@ -214,7 +214,7 @@ function iocontrol.init(conf, comms) num_boilers = 0, num_turbines = 0, num_snas = 0, - has_tank = conf.cooling.r_cool[i].TANK, + has_tank = conf.cooling.r_cool[i].TankConnection, control_state = false, burn_rate_cmd = 0.0, @@ -295,13 +295,13 @@ function iocontrol.init(conf, comms) end -- create boiler tables - for _ = 1, conf.cooling.r_cool[i].BOILERS do + for _ = 1, conf.cooling.r_cool[i].BoilerCount do table.insert(entry.boiler_ps_tbl, psil.create()) table.insert(entry.boiler_data_tbl, {}) end -- create turbine tables - for _ = 1, conf.cooling.r_cool[i].TURBINES do + for _ = 1, conf.cooling.r_cool[i].TurbineCount do table.insert(entry.turbine_ps_tbl, psil.create()) table.insert(entry.turbine_data_tbl, {}) end diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 3df5ba2..3a52029 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -22,7 +22,7 @@ local sounder = require("coordinator.sounder") local apisessions = require("coordinator.session.apisessions") -local COORDINATOR_VERSION = "v1.0.18" +local COORDINATOR_VERSION = "v1.1.0" local println = util.println local println_ts = util.println_ts diff --git a/supervisor/configure.lua b/supervisor/configure.lua index 5639660..5a48610 100644 --- a/supervisor/configure.lua +++ b/supervisor/configure.lua @@ -96,17 +96,17 @@ local tmp_cfg = { CoolingConfig = {}, FacilityTankMode = 0, FacilityTankDefs = {}, - SVR_Channel = nil, - PLC_Channel = nil, - RTU_Channel = nil, - CRD_Channel = nil, - PKT_Channel = nil, - PLC_Timeout = nil, - RTU_Timeout = nil, - CRD_Timeout = nil, - PKT_Timeout = nil, - TrustedRange = nil, - AuthKey = nil, + SVR_Channel = nil, ---@type integer + PLC_Channel = nil, ---@type integer + RTU_Channel = nil, ---@type integer + CRD_Channel = nil, ---@type integer + PKT_Channel = nil, ---@type integer + PLC_Timeout = nil, ---@type number + RTU_Timeout = nil, ---@type number + CRD_Timeout = nil, ---@type number + PKT_Timeout = nil, ---@type number + TrustedRange = nil, ---@type number + AuthKey = nil, ---@type string|nil LogMode = 0, LogPath = "", LogDebug = false, diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 81fd531..ce98d1f 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -135,7 +135,7 @@ function facility.new(num_reactors, cooling_conf) -- create units for i = 1, num_reactors do - table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BOILERS, cooling_conf.r_cool[i].TURBINES)) + table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BoilerCount, cooling_conf.r_cool[i].TurbineCount)) table.insert(self.group_map, 0) end diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 166a067..68d2587 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -2,16 +2,14 @@ local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local util = require("scada-common.util") -local config = require("supervisor.config") local databus = require("supervisor.databus") local facility = require("supervisor.facility") -local svqtypes = require("supervisor.session.svqtypes") - local coordinator = require("supervisor.session.coordinator") local plc = require("supervisor.session.plc") local pocket = require("supervisor.session.pocket") local rtu = require("supervisor.session.rtu") +local svqtypes = require("supervisor.session.svqtypes") -- Supervisor Sessions Handler @@ -36,6 +34,7 @@ svsessions.SESSION_TYPE = SESSION_TYPE local self = { nic = nil, ---@type nic|nil fp_ok = false, + config = nil, ---@type svr_config num_reactors = 0, facility = nil, ---@type facility|nil sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} }, @@ -60,7 +59,7 @@ local function _sv_handle_outq(session) if msg ~= nil then if msg.qtype == mqueue.TYPE.PACKET then -- handle a packet to be sent - self.nic.transmit(session.r_chan, config.SVR_CHANNEL, msg.message) + self.nic.transmit(session.r_chan, self.config.SVR_Channel, msg.message) elseif msg.qtype == mqueue.TYPE.COMMAND then -- handle instruction/notification elseif msg.qtype == mqueue.TYPE.DATA then @@ -135,7 +134,7 @@ local function _shutdown(session) while session.out_queue.ready() do local msg = session.out_queue.pop() if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then - self.nic.transmit(session.r_chan, config.SVR_CHANNEL, msg.message) + self.nic.transmit(session.r_chan, self.config.SVR_Channel, msg.message) end end @@ -197,13 +196,13 @@ end -- initialize svsessions ---@param nic nic network interface device ---@param fp_ok boolean front panel active ----@param num_reactors integer number of reactors +---@param config svr_config supervisor configuration ---@param cooling_conf sv_cooling_conf cooling configuration definition -function svsessions.init(nic, fp_ok, num_reactors, cooling_conf) +function svsessions.init(nic, fp_ok, config, cooling_conf) self.nic = nic self.fp_ok = fp_ok - self.num_reactors = num_reactors - self.facility = facility.new(num_reactors, cooling_conf) + self.config = config + self.facility = facility.new(config.UnitCount, cooling_conf) end -- find an RTU session by the computer ID @@ -287,7 +286,7 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version) open = true, reactor = for_reactor, version = version, - r_chan = config.PLC_CHANNEL, + r_chan = self.config.PLC_Channel, s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), @@ -296,8 +295,7 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version) local id = self.next_ids.plc - plc_s.instance = plc.new_session(id, source_addr, for_reactor, plc_s.in_queue, plc_s.out_queue, - config.PLC_TIMEOUT, self.fp_ok) + plc_s.instance = plc.new_session(id, source_addr, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.fp_ok) table.insert(self.sessions.plc, plc_s) local units = self.facility.get_units() @@ -305,8 +303,7 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version) local mt = { ---@param s plc_session_struct - __tostring = function (s) return util.c("PLC [", s.instance.get_id(), "] for reactor #", s.reactor, - " (@", s.s_addr, ")") end + __tostring = function (s) return util.c("PLC [", s.instance.get_id(), "] for reactor #", s.reactor, " (@", s.s_addr, ")") end } setmetatable(plc_s, mt) @@ -336,7 +333,7 @@ function svsessions.establish_rtu_session(source_addr, advertisement, version) s_type = "rtu", open = true, version = version, - r_chan = config.RTU_CHANNEL, + r_chan = self.config.RTU_Channel, s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), @@ -345,8 +342,7 @@ function svsessions.establish_rtu_session(source_addr, advertisement, version) local id = self.next_ids.rtu - rtu_s.instance = rtu.new_session(id, source_addr, rtu_s.in_queue, rtu_s.out_queue, config.RTU_TIMEOUT, - advertisement, self.facility, self.fp_ok) + rtu_s.instance = rtu.new_session(id, source_addr, rtu_s.in_queue, rtu_s.out_queue, self.config.RTU_Timeout, advertisement, self.facility, self.fp_ok) table.insert(self.sessions.rtu, rtu_s) local mt = { @@ -377,7 +373,7 @@ function svsessions.establish_crd_session(source_addr, version) s_type = "crd", open = true, version = version, - r_chan = config.CRD_CHANNEL, + r_chan = self.config.CRD_Channel, s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), @@ -386,8 +382,7 @@ function svsessions.establish_crd_session(source_addr, version) local id = self.next_ids.crd - crd_s.instance = coordinator.new_session(id, source_addr, crd_s.in_queue, crd_s.out_queue, config.CRD_TIMEOUT, - self.facility, self.fp_ok) + crd_s.instance = coordinator.new_session(id, source_addr, crd_s.in_queue, crd_s.out_queue, self.config.CRD_Timeout, self.facility, self.fp_ok) table.insert(self.sessions.crd, crd_s) local mt = { @@ -421,7 +416,7 @@ function svsessions.establish_pdg_session(source_addr, version) s_type = "pkt", open = true, version = version, - r_chan = config.PKT_CHANNEL, + r_chan = self.config.PKT_Channel, s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), @@ -430,8 +425,7 @@ function svsessions.establish_pdg_session(source_addr, version) local id = self.next_ids.pdg - pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.facility, - self.fp_ok) + pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, self.config.PKT_Timeout, self.facility, self.fp_ok) table.insert(self.sessions.pdg, pdg_s) local mt = { diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 2ff69ad..b88cd4f 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,70 +14,66 @@ local util = require("scada-common.util") local core = require("graphics.core") -local config = require("supervisor.config") +local configure = require("supervisor.configure") local databus = require("supervisor.databus") local renderer = require("supervisor.renderer") local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.1.0" +local SUPERVISOR_VERSION = "v1.2.0" local println = util.println local println_ts = util.println_ts ---------------------------------------- --- config validation +-- get configuration ---------------------------------------- +if not supervisor.load_config() then + -- try to reconfigure (user action) + local success, error = configure.configure(true) + if success then + assert(supervisor.load_config(), "failed to load valid supervisor configuration") + else + assert(success, "supervisor configuration error: " .. error) + end +end + +local config = supervisor.config + local cfv = util.new_validator() -cfv.assert_channel(config.SVR_CHANNEL) -cfv.assert_channel(config.PLC_CHANNEL) -cfv.assert_channel(config.RTU_CHANNEL) -cfv.assert_channel(config.CRD_CHANNEL) -cfv.assert_channel(config.PKT_CHANNEL) -cfv.assert_type_int(config.TRUSTED_RANGE) -cfv.assert_type_num(config.PLC_TIMEOUT) -cfv.assert_min(config.PLC_TIMEOUT, 2) -cfv.assert_type_num(config.RTU_TIMEOUT) -cfv.assert_min(config.RTU_TIMEOUT, 2) -cfv.assert_type_num(config.CRD_TIMEOUT) -cfv.assert_min(config.CRD_TIMEOUT, 2) -cfv.assert_type_num(config.PKT_TIMEOUT) -cfv.assert_min(config.PKT_TIMEOUT, 2) -cfv.assert_type_int(config.NUM_REACTORS) -cfv.assert_type_table(config.REACTOR_COOLING) -cfv.assert_type_int(config.FAC_TANK_MODE) -cfv.assert_type_table(config.FAC_TANK_DEFS) -cfv.assert_type_str(config.LOG_PATH) -cfv.assert_type_int(config.LOG_MODE) +assert((config.FacilityTankMode == 0) or (config.UnitCount == #config.FacilityTankDefs), + "startup> FacilityTankDefs length not equal to UnitCount") -assert(cfv.valid(), "bad config file: missing/invalid fields") +for i = 1, config.UnitCount do + local def = config.FacilityTankDefs[i] + cfv.assert_type_int(def) + cfv.assert_range(def, 0, 2) + assert(cfv.valid(), "startup> invalid facility tank definition for reactor unit " .. i) +end -assert((config.FAC_TANK_MODE == 0) or (config.NUM_REACTORS == #config.FAC_TANK_DEFS), - "bad config file: FAC_TANK_DEFS length not equal to NUM_REACTORS") +cfv.assert_eq(#config.CoolingConfig, config.UnitCount) +assert(cfv.valid(), "startup> the number of reactor cooling configurations is different than the number of units") -cfv.assert_eq(#config.REACTOR_COOLING, config.NUM_REACTORS) -assert(cfv.valid(), "config: number of cooling configs different than number of units") - -for i = 1, config.NUM_REACTORS do - cfv.assert_type_table(config.REACTOR_COOLING[i]) - assert(cfv.valid(), "config: missing cooling entry for reactor " .. i) - cfv.assert_type_int(config.REACTOR_COOLING[i].BOILERS) - cfv.assert_type_int(config.REACTOR_COOLING[i].TURBINES) - cfv.assert_type_bool(config.REACTOR_COOLING[i].TANK) - assert(cfv.valid(), "config: missing boilers/turbines for reactor " .. i) - cfv.assert_min(config.REACTOR_COOLING[i].BOILERS, 0) - cfv.assert_min(config.REACTOR_COOLING[i].TURBINES, 1) - assert(cfv.valid(), "config: bad number of boilers/turbines for reactor " .. i) +for i = 1, config.UnitCount do + cfv.assert_type_table(config.CoolingConfig[i]) + assert(cfv.valid(), "startup> missing cooling entry for reactor unit " .. i) + cfv.assert_type_int(config.CoolingConfig[i].BoilerCount) + cfv.assert_type_int(config.CoolingConfig[i].TurbineCount) + cfv.assert_type_bool(config.CoolingConfig[i].TankConnection) + assert(cfv.valid(), "startup> missing boiler/turbine/tank fields for reactor unit " .. i) + cfv.assert_range(config.CoolingConfig[i].BoilerCount, 0, 2) + cfv.assert_range(config.CoolingConfig[i].TurbineCount, 1, 3) + assert(cfv.valid(), "startup> out-of-range number of boilers and/or turbines provided for reactor unit " .. i) end ---------------------------------------- -- log init ---------------------------------------- -log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true) +log.init(config.LogPath, config.LogMode, config.LogDebug == true) log.info("========================================") log.info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION) @@ -102,8 +98,8 @@ local function main() ppm.mount_all() -- message authentication init - if type(config.AUTH_KEY) == "string" then - network.init_mac(config.AUTH_KEY) + if type(config.AuthKey) == "string" then + network.init_mac(config.AuthKey) end -- get modem diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 498a51d..755893f 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -2,8 +2,6 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local util = require("scada-common.util") -local config = require("supervisor.config") - local svsessions = require("supervisor.session.svsessions") local supervisor = {} @@ -13,6 +11,77 @@ local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK local MGMT_TYPE = comms.MGMT_TYPE +---@type svr_config +local config = {} + +supervisor.config = config + +-- load the supervisor configuration +function supervisor.load_config() + if not settings.load("/supervisor.settings") then return false end + + config.UnitCount = settings.get("UnitCount") + config.CoolingConfig = settings.get("CoolingConfig") + config.FacilityTankMode = settings.get("FacilityTankMode") + config.FacilityTankDefs = settings.get("FacilityTankDefs") + + config.SVR_Channel = settings.get("SVR_Channel") + config.PLC_Channel = settings.get("PLC_Channel") + config.RTU_Channel = settings.get("RTU_Channel") + config.CRD_Channel = settings.get("CRD_Channel") + config.PKT_Channel = settings.get("PKT_Channel") + + config.PLC_Timeout = settings.get("PLC_Timeout") + config.RTU_Timeout = settings.get("RTU_Timeout") + config.CRD_Timeout = settings.get("CRD_Timeout") + config.PKT_Timeout = settings.get("PKT_Timeout") + + config.TrustedRange = settings.get("TrustedRange") + config.AuthKey = settings.get("AuthKey") + + config.LogMode = settings.get("LogMode") + config.LogPath = settings.get("LogPath") + config.LogDebug = settings.get("LogDebug") + + local cfv = util.new_validator() + + cfv.assert_type_int(config.UnitCount) + cfv.assert_range(config.UnitCount, 1, 4) + + cfv.assert_type_table(config.CoolingConfig) + cfv.assert_type_table(config.FacilityTankDefs) + cfv.assert_type_int(config.FacilityTankMode) + cfv.assert_range(config.FacilityTankMode, 0, 8) + + cfv.assert_channel(config.SVR_Channel) + cfv.assert_channel(config.PLC_Channel) + cfv.assert_channel(config.RTU_Channel) + cfv.assert_channel(config.CRD_Channel) + cfv.assert_channel(config.PKT_Channel) + + cfv.assert_type_num(config.PLC_Timeout) + cfv.assert_min(config.PLC_Timeout, 2) + cfv.assert_type_num(config.RTU_Timeout) + cfv.assert_min(config.RTU_Timeout, 2) + cfv.assert_type_num(config.CRD_Timeout) + cfv.assert_min(config.CRD_Timeout, 2) + cfv.assert_type_num(config.PKT_Timeout) + cfv.assert_min(config.PKT_Timeout, 2) + + cfv.assert_type_num(config.TrustedRange) + + if type(config.AuthKey) == "string" then + local len = string.len(config.AuthKey) + cfv.assert_eq(len == 0 or len >= 8, true) + end + + cfv.assert_type_int(config.LogMode) + cfv.assert_type_str(config.LogPath) + cfv.assert_type_bool(config.LogDebug) + + return cfv.valid() +end + -- supervisory controller communications ---@nodiscard ---@param _version string supervisor version @@ -23,32 +92,23 @@ function supervisor.comms(_version, nic, fp_ok) -- print a log message to the terminal as long as the UI isn't running local function println(message) if not fp_ok then util.println_ts(message) end end - -- channel list from config - local svr_channel = config.SVR_CHANNEL - local plc_channel = config.PLC_CHANNEL - local rtu_channel = config.RTU_CHANNEL - local crd_channel = config.CRD_CHANNEL - local pkt_channel = config.PKT_CHANNEL - - -- configuration data - local num_reactors = config.NUM_REACTORS ---@class sv_cooling_conf - local cooling_conf = { r_cool = config.REACTOR_COOLING, fac_tank_mode = config.FAC_TANK_MODE, fac_tank_defs = config.FAC_TANK_DEFS } + local cooling_conf = { r_cool = config.CoolingConfig, fac_tank_mode = config.FacilityTankMode, fac_tank_defs = config.FacilityTankDefs } local self = { last_est_acks = {} } - comms.set_trusted_range(config.TRUSTED_RANGE) + comms.set_trusted_range(config.TrustedRange) -- PRIVATE FUNCTIONS -- -- configure modem channels nic.closeAll() - nic.open(svr_channel) + nic.open(config.SVR_Channel) -- pass modem, status, and config data to svsessions - svsessions.init(nic, fp_ok, num_reactors, cooling_conf) + svsessions.init(nic, fp_ok, config, cooling_conf) -- send an establish request response ---@param packet scada_packet @@ -61,7 +121,7 @@ function supervisor.comms(_version, nic, fp_ok) m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data }) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - nic.transmit(packet.remote_channel(), svr_channel, s_pkt) + nic.transmit(packet.remote_channel(), config.SVR_Channel, s_pkt) self.last_est_acks[packet.src_addr()] = ack end @@ -124,9 +184,9 @@ function supervisor.comms(_version, nic, fp_ok) local src_addr = packet.scada_frame.src_addr() local protocol = packet.scada_frame.protocol() - if l_chan ~= svr_channel then + if l_chan ~= config.SVR_Channel then log.debug("received packet on unconfigured channel " .. l_chan, true) - elseif r_chan == plc_channel then + elseif r_chan == config.PLC_Channel then -- look for an associated session local session = svsessions.find_plc_session(src_addr) @@ -201,7 +261,7 @@ function supervisor.comms(_version, nic, fp_ok) else log.debug(util.c("illegal packet type ", protocol, " on PLC channel")) end - elseif r_chan == rtu_channel then + elseif r_chan == config.RTU_Channel then -- look for an associated session local session = svsessions.find_rtu_session(src_addr) @@ -265,7 +325,7 @@ function supervisor.comms(_version, nic, fp_ok) else log.debug(util.c("illegal packet type ", protocol, " on RTU channel")) end - elseif r_chan == crd_channel then + elseif r_chan == config.CRD_Channel then -- look for an associated session local session = svsessions.find_crd_session(src_addr) @@ -299,7 +359,7 @@ function supervisor.comms(_version, nic, fp_ok) println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected")) log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id)) - _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { num_reactors, cooling_conf }) + _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, cooling_conf }) else if last_ack ~= ESTABLISH_ACK.COLLISION then log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator") @@ -332,7 +392,7 @@ function supervisor.comms(_version, nic, fp_ok) else log.debug(util.c("illegal packet type ", protocol, " on coordinator channel")) end - elseif r_chan == pkt_channel then + elseif r_chan == config.PKT_Channel then -- look for an associated session local session = svsessions.find_pdg_session(src_addr)