From ab97f8935d0b6054d5a5edb3a079731e440ddddc Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 16 Aug 2024 18:08:53 +0000 Subject: [PATCH 01/19] #367 reject and record bad or duplicate RTU IDs --- supervisor/facility.lua | 29 ++++++++----- supervisor/session/svsessions.lua | 50 ++++++++++++++++++++++- supervisor/startup.lua | 2 +- supervisor/unit.lua | 67 ++++++++++++++++++++----------- 4 files changed, 113 insertions(+), 35 deletions(-) diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 825186f..17011ae 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -65,7 +65,8 @@ local facility = {} ---@nodiscard ---@param config svr_config supervisor configuration ---@param cooling_conf sv_cooling_conf cooling configurations of reactor units -function facility.new(config, cooling_conf) +---@param check_rtu_id function ID checking function for RTUs attempting to be linked +function facility.new(config, cooling_conf, check_rtu_id) local self = { units = {}, status_text = { "START UP", "initializing..." }, @@ -144,7 +145,7 @@ function facility.new(config, cooling_conf) -- create units for i = 1, config.UnitCount do - table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BoilerCount, cooling_conf.r_cool[i].TurbineCount, config.ExtChargeIdling)) + table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BoilerCount, cooling_conf.r_cool[i].TurbineCount, check_rtu_id, config.ExtChargeIdling)) table.insert(self.group_map, 0) end @@ -257,20 +258,30 @@ function facility.new(config, cooling_conf) ---@param imatrix unit_session ---@return boolean linked induction matrix accepted (max 1) function public.add_imatrix(imatrix) - if #self.induction == 0 then + local fail_code, fail_str = check_rtu_id(imatrix, self.induction, 1) + + if fail_code == 0 then table.insert(self.induction, imatrix) - return true - else return false end + else + log.warning(util.c("FAC: rejected induction matrix linking due to failure code ", fail_code, " (", fail_str, ")")) + end + + return fail_code == 0 end -- link an SPS RTU session ---@param sps unit_session ---@return boolean linked SPS accepted (max 1) function public.add_sps(sps) - if #self.sps == 0 then + local fail_code, fail_str = check_rtu_id(sps, self.sps, 1) + + if fail_code == 0 then table.insert(self.sps, sps) - return true - else return false end + else + log.warning(util.c("FAC: rejected SPS linking due to failure code ", fail_code, " (", fail_str, ")")) + end + + return fail_code == 0 end -- link a dynamic tank RTU session @@ -293,7 +304,7 @@ function facility.new(config, cooling_conf) -- update (iterate) the facility management function public.update() - -- unlink RTU unit sessions if they are closed + -- unlink RTU sessions if they are closed for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end -- check if test routines are allowed right now diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 002dd56..f404291 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -37,7 +37,8 @@ local self = { config = nil, ---@type svr_config facility = nil, ---@type facility|nil sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} }, - next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 } + next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 }, + dev_dbg = { duplicate = {}, out_of_range = {} } } ---@alias sv_session_structs plc_session_struct|rtu_session_struct|crd_session_struct|pdg_session_struct @@ -190,6 +191,49 @@ local function _find_session(list, s_addr) return nil end +local function _update_dev_dbg() + local f = function (unit) return unit.is_connected() end + + ---@param unit unit_session + local on_delete = function (unit) + end + + util.filter_table(self.dev_dbg.duplicate, f, on_delete) + util.filter_table(self.dev_dbg.out_of_range, f, on_delete) +end + +-- SHARED FUNCTIONS -- + +---@param unit unit_session RTU session +---@param list table table of RTU sessions +---@param max integer max of this type of RTU +---@return 0|1|2|3 fail_code, string fail_str 0 = success, 1 = out-of-range, 2 = duplicate, 3 = exceeded table max +local function check_rtu_id(unit, list, max) + local fail_code, fail_str = 0, "OK" + + if (unit.get_device_idx() < 1 and max ~= 1) or unit.get_device_idx() > max then + -- out-of-range + fail_code, fail_str = 1, "index out of range" + table.insert(self.dev_dbg.out_of_range, unit) + else + for _, u in ipairs(list) do + if u.get_device_idx() == unit.get_device_idx() then + -- duplicate + fail_code, fail_str = 2, "duplicate index" + table.insert(self.dev_dbg.duplicate, unit) + break + end + end + end + + -- make sure this won't exceed the maximum allowable devices + if fail_code == 0 and #list >= max then + fail_code, fail_str = 3, "too many of this type" + end + + return fail_code, fail_str +end + -- PUBLIC FUNCTIONS -- -- initialize svsessions @@ -201,7 +245,7 @@ function svsessions.init(nic, fp_ok, config, cooling_conf) self.nic = nic self.fp_ok = fp_ok self.config = config - self.facility = facility.new(config, cooling_conf) + self.facility = facility.new(config, cooling_conf, check_rtu_id) end -- find an RTU session by the computer ID @@ -466,6 +510,8 @@ function svsessions.iterate_all() -- iterate units self.facility.update_units() + + _update_dev_dbg() end -- delete all closed sessions diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 5ea8f6a..a89a777 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.4.3" +local SUPERVISOR_VERSION = "v1.5.0" local println = util.println local println_ts = util.println_ts diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 0fd5f28..31e24d3 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -63,11 +63,14 @@ local unit = {} ---@param reactor_id integer reactor unit number ---@param num_boilers integer number of boilers expected ---@param num_turbines integer number of turbines expected +---@param check_rtu_id function ID checking function for RTUs attempting to be linked ---@param ext_idle boolean extended idling mode -function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) +function unit.new(reactor_id, num_boilers, num_turbines, check_rtu_id, ext_idle) -- time (ms) to idle for auto idling local IDLE_TIME = util.trinary(ext_idle, 60000, 10000) + local log_tag = "UNIT " .. reactor_id .. ": " + ---@class _unit_self local self = { r_id = reactor_id, @@ -441,22 +444,28 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) ---@param turbine unit_session ---@return boolean linked turbine accepted to associated device slot function public.add_turbine(turbine) - if #self.turbines < num_turbines and turbine.get_device_idx() <= num_turbines then + local fail_code, fail_str = check_rtu_id(turbine, self.turbines, num_turbines) + + if fail_code == 0 then table.insert(self.turbines, turbine) -- reset deltas _reset_dt(DT_KEYS.TurbineSteam .. turbine.get_device_idx()) _reset_dt(DT_KEYS.TurbinePower .. turbine.get_device_idx()) + else + log.warning(util.c(log_tag, "rejected turbine linking due to failure code ", fail_code, " (", fail_str, ")")) + end - return true - else return false end + return fail_code == 0 end -- link a boiler RTU session ---@param boiler unit_session ---@return boolean linked boiler accepted to associated device slot function public.add_boiler(boiler) - if #self.boilers < num_boilers and boiler.get_device_idx() <= num_boilers then + local fail_code, fail_str = check_rtu_id(boiler, self.boilers, num_boilers) + + if fail_code == 0 then table.insert(self.boilers, boiler) -- reset deltas @@ -464,19 +473,26 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) _reset_dt(DT_KEYS.BoilerSteam .. boiler.get_device_idx()) _reset_dt(DT_KEYS.BoilerCCool .. boiler.get_device_idx()) _reset_dt(DT_KEYS.BoilerHCool .. boiler.get_device_idx()) + else + log.warning(util.c(log_tag, "rejected boiler linking due to failure code ", fail_code, " (", fail_str, ")")) + end - return true - else return false end + return fail_code == 0 end -- link a dynamic tank RTU session ---@param dynamic_tank unit_session ---@return boolean linked dynamic tank accepted (max 1) function public.add_tank(dynamic_tank) - if #self.tanks == 0 then + local fail_code, fail_str = check_rtu_id(dynamic_tank, self.tanks, 1) + + if fail_code == 0 then table.insert(self.tanks, dynamic_tank) - return true - else return false end + else + log.warning(util.c(log_tag, "rejected dynamic tank linking due to failure code ", fail_code, " (", fail_str, ")")) + end + + return fail_code == 0 end -- link a solar neutron activator RTU session @@ -487,10 +503,15 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) ---@param envd unit_session ---@return boolean linked environment detector accepted (max 1) function public.add_envd(envd) - if #self.envd == 0 then + local fail_code, fail_str = check_rtu_id(envd, self.envd, 99) + + if fail_code == 0 then table.insert(self.envd, envd) - return true - else return false end + else + log.warning(util.c(log_tag, "rejected environment detector linking due to failure code ", fail_code, " (", fail_str, ")")) + end + + return fail_code == 0 end -- purge devices associated with the given RTU session ID @@ -512,7 +533,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) self.db.control.br100 = 0 end - -- unlink RTU unit sessions if they are closed + -- unlink RTU sessions if they are closed for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end -- update degraded state for auto control @@ -547,7 +568,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) -- stop idling when completed if self.auto_idling and (((util.time_ms() - self.auto_idle_start) > IDLE_TIME) or not self.auto_idle) then - log.info(util.c("UNIT ", self.r_id, ": completed idling period")) + log.info(util.c(log_tag, "completed idling period")) self.auto_idling = false self.plc_i.auto_set_burn(0, false) end @@ -584,7 +605,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) function public.auto_engage() self.auto_engaged = true if self.plc_i ~= nil then - log.debug(util.c("UNIT ", self.r_id, ": engaged auto control")) + log.debug(util.c(log_tag, "engaged auto control")) self.plc_i.auto_lock(true) end end @@ -593,7 +614,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) function public.auto_disengage() self.auto_engaged = false if self.plc_i ~= nil then - log.debug(util.c("UNIT ", self.r_id, ": disengaged auto control")) + log.debug(util.c(log_tag, "disengaged auto control")) self.plc_i.auto_lock(false) self.db.control.br100 = 0 end @@ -610,7 +631,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) end if idle ~= self.auto_idle then - log.debug(util.c("UNIT ", self.r_id, ": idling mode changed to ", idle)) + log.debug(util.c(log_tag, "idling mode changed to ", idle)) end self.auto_idle = idle @@ -623,7 +644,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) function public.auto_get_effective_limit() local ctrl = self.db.control if (not ctrl.ready) or ctrl.degraded or self.plc_cache.rps_trip then - -- log.debug(util.c("UNIT ", self.r_id, ": effective limit is zero! ready[", ctrl.ready, "] degraded[", ctrl.degraded, "] rps_trip[", self.plc_cache.rps_trip, "]")) + -- log.debug(util.c(log_tag, "effective limit is zero! ready[", ctrl.ready, "] degraded[", ctrl.degraded, "] rps_trip[", self.plc_cache.rps_trip, "]")) ctrl.br100 = 0 return 0 else return ctrl.lim_br100 end @@ -634,7 +655,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) function public.auto_commit_br100(ramp) if self.auto_engaged then if self.plc_i ~= nil then - log.debug(util.c("UNIT ", self.r_id, ": commit br100 of ", self.db.control.br100, " with ramp set to ", ramp)) + log.debug(util.c(log_tag, "commit br100 of ", self.db.control.br100, " with ramp set to ", ramp)) local rate = self.db.control.br100 / 100 @@ -643,16 +664,16 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) if self.auto_idle_start == 0 then self.auto_idling = true self.auto_idle_start = util.time_ms() - log.info(util.c("UNIT ", self.r_id, ": started idling at ", IDLE_RATE, " mB/t")) + log.info(util.c(log_tag, "started idling at ", IDLE_RATE, " mB/t")) rate = IDLE_RATE elseif (util.time_ms() - self.auto_idle_start) > IDLE_TIME then if self.auto_idling then self.auto_idling = false - log.info(util.c("UNIT ", self.r_id, ": completed idling period")) + log.info(util.c(log_tag, "completed idling period")) end else - log.debug(util.c("UNIT ", self.r_id, ": continuing idle at ", IDLE_RATE, " mB/t")) + log.debug(util.c(log_tag, "continuing idle at ", IDLE_RATE, " mB/t")) rate = IDLE_RATE end From 0f4a8b6dfca403534cd7e3fec43e0d64635e1470 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 16 Aug 2024 18:17:03 +0000 Subject: [PATCH 02/19] refactoring and RTU gateway terminology cleanup --- supervisor/session/rtu.lua | 52 +++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 756ee01..2a9a339 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -30,7 +30,7 @@ local PERIODICS = { ALARM_TONES = 500 } --- create a new RTU session +-- create a new RTU gateway session ---@nodiscard ---@param id integer session ID ---@param s_addr integer device source address @@ -38,14 +38,14 @@ local PERIODICS = { ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue ---@param timeout number communications timeout ----@param advertisement table RTU device advertisement +---@param advertisement table RTU gateway device advertisement ---@param facility facility facility data table ---@param fp_ok boolean if the front panel UI is running function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, advertisement, facility, 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 - local log_header = "rtu_session(" .. id .. "): " + local log_tag = "rtu_gw_session(" .. id .. "): " local self = { modbus_q = mqueue.new(), @@ -124,7 +124,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad if u_type == false then -- validation fail - log.debug(log_header .. "_handle_advertisement(): advertisement unit validation failure") + log.debug(log_tag .. "_handle_advertisement(): advertisement unit validation failure") else if unit_advert.reactor > 0 then local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit @@ -156,9 +156,9 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad if type(unit) ~= "nil" then target_unit.add_envd(unit) end elseif u_type == RTU_UNIT_TYPE.VIRTUAL then -- skip virtual units - log.debug(util.c(log_header, "skipping virtual RTU unit #", i)) + log.debug(util.c(log_tag, "skipping virtual RTU #", i)) else - log.warning(util.c(log_header, "_handle_advertisement(): encountered unsupported reactor-specific RTU type ", type_string)) + log.warning(util.c(log_tag, "_handle_advertisement(): encountered unsupported reactor-specific RTU type ", type_string)) end else -- facility RTUs @@ -184,9 +184,9 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad if type(unit) ~= "nil" then facility.add_envd(unit) end elseif u_type == RTU_UNIT_TYPE.VIRTUAL then -- skip virtual units - log.debug(util.c(log_header, "skipping virtual RTU unit #", i)) + log.debug(util.c(log_tag, "skipping virtual RTU #", i)) else - log.warning(util.c(log_header, "_handle_advertisement(): encountered unsupported facility RTU type ", type_string)) + log.warning(util.c(log_tag, "_handle_advertisement(): encountered unsupported facility RTU type ", type_string)) end end end @@ -195,20 +195,20 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad self.units[i] = unit unit_count = unit_count + 1 elseif u_type ~= RTU_UNIT_TYPE.VIRTUAL then - log.warning(util.c(log_header, "_handle_advertisement(): problem occured while creating a unit (type is ", type_string, ")")) + log.warning(util.c(log_tag, "_handle_advertisement(): problem occured while creating a unit (type is ", type_string, ")")) end end databus.tx_rtu_units(id, unit_count) end - -- mark this RTU session as closed, stop watchdog + -- mark this RTU gateway session as closed, stop watchdog local function _close() self.conn_watchdog.cancel() self.connected = false databus.tx_rtu_disconnected(id) - -- mark all RTU unit sessions as closed so the reactor unit knows + -- mark all RTU sessions as closed so the reactor unit knows for _, unit in pairs(self.units) do unit.close() end end @@ -242,7 +242,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad local function _handle_packet(pkt) -- check sequence number if self.r_seq_num ~= pkt.scada_frame.seq_num() then - log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) + log.warning(log_tag .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) return else self.r_seq_num = pkt.scada_frame.seq_num() + 1 @@ -265,27 +265,27 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad -- keep alive reply if pkt.length == 2 then local srv_start = pkt.data[1] - -- local rtu_send = pkt.data[2] + -- local rtu_gw_send = pkt.data[2] local srv_now = util.time() self.last_rtt = srv_now - srv_start if self.last_rtt > 750 then - log.warning(log_header .. "RTU KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") + log.warning(log_tag .. "RTU GW KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") end - -- log.debug(log_header .. "RTU RTT = " .. self.last_rtt .. "ms") - -- log.debug(log_header .. "RTU TT = " .. (srv_now - rtu_send) .. "ms") + -- log.debug(log_tag .. "RTU GW RTT = " .. self.last_rtt .. "ms") + -- log.debug(log_tag .. "RTU GW TT = " .. (srv_now - rtu_gw_send) .. "ms") databus.tx_rtu_rtt(id, self.last_rtt) else - log.debug(log_header .. "SCADA keep alive packet length mismatch") + log.debug(log_tag .. "SCADA keep alive packet length mismatch") end elseif pkt.type == MGMT_TYPE.CLOSE then -- close the session _close() elseif pkt.type == MGMT_TYPE.RTU_ADVERT then - -- RTU unit advertisement - log.debug(log_header .. "received updated advertisement") + -- RTU advertisement + log.debug(log_tag .. "received updated advertisement") self.advert = pkt.data -- handle advertisement; this will re-create all unit sub-sessions @@ -298,17 +298,17 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad unit.invalidate_cache() end else - log.debug(log_header .. "SCADA RTU device re-mount packet length mismatch") + log.debug(log_tag .. "SCADA RTU GW device re-mount packet length mismatch") end else - log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) + log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) end end end -- PUBLIC FUNCTIONS -- - -- get the session ID + -- get the gateway session ID function public.get_id() return id end -- check if a timer matches this session's watchdog @@ -322,8 +322,8 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad function public.close() _close() _send_mgmt(MGMT_TYPE.CLOSE, {}) - println(log_header .. "connection to RTU closed by server") - log.info(log_header .. "session closed by server") + println(log_tag .. "connection to RTU GW closed by server") + log.info(log_tag .. "session closed by server") end -- iterate the session @@ -354,7 +354,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad -- max 100ms spent processing queue if util.time() - handle_start > 100 then - log.warning(log_header .. "exceeded 100ms queue process limit") + log.warning(log_tag .. "exceeded 100ms queue process limit") break end end @@ -362,7 +362,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad -- exit if connection was closed if not self.connected then println("RTU connection " .. id .. " closed by remote host") - log.info(log_header .. "session closed by remote host") + log.info(log_tag .. "session closed by remote host") return self.connected end From 5597ea2097aee7bee8e87f5bedd4d33da8f75c89 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 16 Aug 2024 19:53:43 +0000 Subject: [PATCH 03/19] comment updates for clarity around RTU gateway vs RTU --- supervisor/session/rtu/boilerv.lua | 6 +++--- supervisor/session/rtu/dynamicv.lua | 6 +++--- supervisor/session/rtu/envd.lua | 8 ++++---- supervisor/session/rtu/imatrix.lua | 6 +++--- supervisor/session/rtu/redstone.lua | 8 ++++---- supervisor/session/rtu/sna.lua | 6 +++--- supervisor/session/rtu/sps.lua | 6 +++--- supervisor/session/rtu/turbinev.lua | 6 +++--- supervisor/session/rtu/unit_session.lua | 4 ++-- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/supervisor/session/rtu/boilerv.lua b/supervisor/session/rtu/boilerv.lua index 33759f2..26a8f2d 100644 --- a/supervisor/session/rtu/boilerv.lua +++ b/supervisor/session/rtu/boilerv.lua @@ -32,10 +32,10 @@ local PERIODICS = { -- create a new boilerv rtu session runner ---@nodiscard ----@param session_id integer RTU session ID ----@param unit_id integer RTU unit ID +---@param session_id integer RTU gateway session ID +---@param unit_id integer RTU ID ---@param advert rtu_advertisement RTU advertisement table ----@param out_queue mqueue RTU unit message out queue +---@param out_queue mqueue RTU message out queue function boilerv.new(session_id, unit_id, advert, out_queue) -- checks if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then diff --git a/supervisor/session/rtu/dynamicv.lua b/supervisor/session/rtu/dynamicv.lua index b1e5b4a..13239a7 100644 --- a/supervisor/session/rtu/dynamicv.lua +++ b/supervisor/session/rtu/dynamicv.lua @@ -44,10 +44,10 @@ local PERIODICS = { -- create a new dynamicv rtu session runner ---@nodiscard ----@param session_id integer RTU session ID ----@param unit_id integer RTU unit ID +---@param session_id integer RTU gateway session ID +---@param unit_id integer RTU ID ---@param advert rtu_advertisement RTU advertisement table ----@param out_queue mqueue RTU unit message out queue +---@param out_queue mqueue RTU message out queue function dynamicv.new(session_id, unit_id, advert, out_queue) -- checks if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then diff --git a/supervisor/session/rtu/envd.lua b/supervisor/session/rtu/envd.lua index 8eacf1d..046ef0a 100644 --- a/supervisor/session/rtu/envd.lua +++ b/supervisor/session/rtu/envd.lua @@ -23,10 +23,10 @@ local PERIODICS = { -- create a new environment detector rtu session runner ---@nodiscard ----@param session_id integer ----@param unit_id integer ----@param advert rtu_advertisement ----@param out_queue mqueue +---@param session_id integer RTU gateway session ID +---@param unit_id integer RTU ID +---@param advert rtu_advertisement RTU advertisement table +---@param out_queue mqueue RTU message out queue function envd.new(session_id, unit_id, advert, out_queue) -- checks if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then diff --git a/supervisor/session/rtu/imatrix.lua b/supervisor/session/rtu/imatrix.lua index 5a6880e..84bfc1e 100644 --- a/supervisor/session/rtu/imatrix.lua +++ b/supervisor/session/rtu/imatrix.lua @@ -32,10 +32,10 @@ local PERIODICS = { -- create a new imatrix rtu session runner ---@nodiscard ----@param session_id integer RTU session ID ----@param unit_id integer RTU unit ID +---@param session_id integer RTU gateway session ID +---@param unit_id integer RTU ID ---@param advert rtu_advertisement RTU advertisement table ----@param out_queue mqueue RTU unit message out queue +---@param out_queue mqueue RTU message out queue function imatrix.new(session_id, unit_id, advert, out_queue) -- checks if advert.type ~= RTU_UNIT_TYPE.IMATRIX then diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua index b248902..b99c0d9 100644 --- a/supervisor/session/rtu/redstone.lua +++ b/supervisor/session/rtu/redstone.lua @@ -45,10 +45,10 @@ local PERIODICS = { -- create a new redstone rtu session runner ---@nodiscard ----@param session_id integer ----@param unit_id integer ----@param advert rtu_advertisement ----@param out_queue mqueue +---@param session_id integer RTU gateway session ID +---@param unit_id integer RTU ID +---@param advert rtu_advertisement RTU advertisement table +---@param out_queue mqueue RTU message out queue function redstone.new(session_id, unit_id, advert, out_queue) -- type check if advert.type ~= RTU_UNIT_TYPE.REDSTONE then diff --git a/supervisor/session/rtu/sna.lua b/supervisor/session/rtu/sna.lua index 39ab1d0..a75e185 100644 --- a/supervisor/session/rtu/sna.lua +++ b/supervisor/session/rtu/sna.lua @@ -29,10 +29,10 @@ local PERIODICS = { -- create a new sna rtu session runner ---@nodiscard ----@param session_id integer RTU session ID ----@param unit_id integer RTU unit ID +---@param session_id integer RTU gateway session ID +---@param unit_id integer RTU ID ---@param advert rtu_advertisement RTU advertisement table ----@param out_queue mqueue RTU unit message out queue +---@param out_queue mqueue RTU message out queue function sna.new(session_id, unit_id, advert, out_queue) -- type check if advert.type ~= RTU_UNIT_TYPE.SNA then diff --git a/supervisor/session/rtu/sps.lua b/supervisor/session/rtu/sps.lua index 3143658..a631e58 100644 --- a/supervisor/session/rtu/sps.lua +++ b/supervisor/session/rtu/sps.lua @@ -32,10 +32,10 @@ local PERIODICS = { -- create a new sps rtu session runner ---@nodiscard ----@param session_id integer RTU session ID ----@param unit_id integer RTU unit ID +---@param session_id integer RTU gateway session ID +---@param unit_id integer RTU ID ---@param advert rtu_advertisement RTU advertisement table ----@param out_queue mqueue RTU unit message out queue +---@param out_queue mqueue RTU message out queue function sps.new(session_id, unit_id, advert, out_queue) -- type check if advert.type ~= RTU_UNIT_TYPE.SPS then diff --git a/supervisor/session/rtu/turbinev.lua b/supervisor/session/rtu/turbinev.lua index e6c08f5..4541e56 100644 --- a/supervisor/session/rtu/turbinev.lua +++ b/supervisor/session/rtu/turbinev.lua @@ -44,10 +44,10 @@ local PERIODICS = { -- create a new turbinev rtu session runner ---@nodiscard ----@param session_id integer RTU session ID ----@param unit_id integer RTU unit ID +---@param session_id integer RTU gateway session ID +---@param unit_id integer RTU ID ---@param advert rtu_advertisement RTU advertisement table ----@param out_queue mqueue RTU unit message out queue +---@param out_queue mqueue RTU message out queue function turbinev.new(session_id, unit_id, advert, out_queue) -- checks if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then diff --git a/supervisor/session/rtu/unit_session.lua b/supervisor/session/rtu/unit_session.lua index 0a2964a..18ac1c9 100644 --- a/supervisor/session/rtu/unit_session.lua +++ b/supervisor/session/rtu/unit_session.lua @@ -24,7 +24,7 @@ unit_session.RTU_US_DATA = RTU_US_DATA -- create a new unit session runner ---@nodiscard ----@param session_id integer RTU session ID +---@param session_id integer RTU gateway session ID ---@param unit_id integer MODBUS unit ID ---@param advert rtu_advertisement RTU advertisement for this unit ---@param out_queue mqueue send queue @@ -144,7 +144,7 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t -- PUBLIC FUNCTIONS -- - -- get the unit ID + -- get the RTU gateway session ID ---@nodiscard function public.get_session_id() return session_id end -- get the unit ID From affe2d6c6dffa31855c7942e16984c5b7474b869 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 16 Aug 2024 21:17:36 +0000 Subject: [PATCH 04/19] listbox debugging --- graphics/element.lua | 4 ++++ graphics/elements/listbox.lua | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/graphics/element.lua b/graphics/element.lua index 7475dc1..8de720a 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -3,6 +3,7 @@ -- local util = require("scada-common.util") +local log = require("scada-common.log") local core = require("graphics.core") @@ -503,7 +504,10 @@ function element.new(args, constraint, child_offset_x, child_offset_y) if args.parent ~= nil then -- remove self from parent + log.debug("removing " .. self.id .. " from parent") args.parent.__remove_child(self.id) + else + log.debug("no parent for " .. self.id .. " on delete attempt") end end diff --git a/graphics/elements/listbox.lua b/graphics/elements/listbox.lua index 3da9ac6..409b6db 100644 --- a/graphics/elements/listbox.lua +++ b/graphics/elements/listbox.lua @@ -1,6 +1,7 @@ -- Scroll-able List Box Display Graphics Element local tcd = require("scada-common.tcd") +local log = require("scada-common.log") local core = require("graphics.core") local element = require("graphics.element") @@ -152,6 +153,7 @@ local function listbox(args) next_y = next_y + item.h + item_pad item.e.reposition(1, item.y) item.e.show() + log.debug("iterated " .. item.e.get_id()) end content_height = next_y @@ -210,6 +212,7 @@ local function listbox(args) ---@param child graphics_element child element function e.on_added(id, child) table.insert(list, { id = id, e = child, y = 0, h = child.get_height() }) + log.debug("added child " .. id .. " into slot " .. #list) update_positions() end @@ -219,10 +222,12 @@ local function listbox(args) for idx, elem in ipairs(list) do if elem.id == id then table.remove(list, idx) + log.debug("removed child " .. id .. " from slot " .. idx) update_positions() return end end + log.debug("failed to remove child " .. id) end -- handle focus From f34747372fc170d41accc0bb63140da916ab1fcb Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 16 Aug 2024 21:19:25 +0000 Subject: [PATCH 05/19] #367 work on device ID check failure list --- supervisor/panel/components/chk_entry.lua | 41 +++++++++++ supervisor/panel/front_panel.lua | 24 ++++--- supervisor/panel/pgi.lua | 84 +++++++++++++++++++---- supervisor/session/svsessions.lua | 23 +++++-- 4 files changed, 143 insertions(+), 29 deletions(-) create mode 100644 supervisor/panel/components/chk_entry.lua diff --git a/supervisor/panel/components/chk_entry.lua b/supervisor/panel/components/chk_entry.lua new file mode 100644 index 0000000..5fa6d3b --- /dev/null +++ b/supervisor/panel/components/chk_entry.lua @@ -0,0 +1,41 @@ +-- +-- RTU ID Check Failure Entry +-- + +local databus = require("supervisor.databus") + +local style = require("supervisor.panel.style") + +local core = require("graphics.core") + +local Div = require("graphics.elements.div") +local TextBox = require("graphics.elements.textbox") + +local ALIGN = core.ALIGN + +local cpair = core.cpair + +-- create an ID check list entry +---@param parent graphics_element parent +---@param unit unit_session RTU session +---@param fail_code integer failure code +local function init(parent, unit, fail_code) + local s_hi_box = style.theme.highlight_box + + local label_fg = style.fp.label_fg + + -- root div + local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} + local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright} + + TextBox{parent=entry,x=1,y=1,text="",width=8,fg_bg=s_hi_box} + local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)} + TextBox{parent=entry,x=1,y=3,text="",width=8,fg_bg=s_hi_box} + + TextBox{parent=entry,x=21,y=2,text="FW:",width=3} + local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,fg_bg=label_fg} + + return root +end + +return init diff --git a/supervisor/panel/front_panel.lua b/supervisor/panel/front_panel.lua index 5f042b0..d3428aa 100644 --- a/supervisor/panel/front_panel.lua +++ b/supervisor/panel/front_panel.lua @@ -10,6 +10,7 @@ local supervisor = require("supervisor.supervisor") local pgi = require("supervisor.panel.pgi") local style = require("supervisor.panel.style") +local chk_entry = require("supervisor.panel.components.chk_entry") local pdg_entry = require("supervisor.panel.components.pdg_entry") local rtu_entry = require("supervisor.panel.components.rtu_entry") @@ -83,7 +84,7 @@ local function init(panel) -- page handling -- - -- plc page + -- plc sessions page local plc_page = Div{parent=page_div,x=1,y=1,hidden=true} local plc_list = Div{parent=plc_page,x=2,y=2,width=49} @@ -115,13 +116,13 @@ local function init(panel) plc_list.line_break() end - -- rtu page + -- rtu sessions page local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true} local rtu_list = ListBox{parent=rtu_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} local _ = Div{parent=rtu_list,height=1,hidden=true} -- padding - -- coordinator page + -- coordinator session page local crd_page = Div{parent=page_div,x=1,y=1,hidden=true} local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=s_hi_bright} @@ -143,15 +144,21 @@ local function init(panel) crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update) crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor) - -- pocket diagnostics page + -- pocket sessions page local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true} local pdg_list = ListBox{parent=pkt_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} local _ = Div{parent=pdg_list,height=1,hidden=true} -- padding + -- RTU device ID check/diagnostics page + + local chk_page = Div{parent=page_div,x=1,y=1,hidden=true} + local chk_list = ListBox{parent=chk_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} + local _ = Div{parent=chk_list,height=1,hidden=true} -- padding + -- assemble page panes - local panes = { main_page, plc_page, rtu_page, crd_page, pkt_page } + local panes = { main_page, plc_page, rtu_page, crd_page, pkt_page, chk_page } local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} @@ -161,12 +168,13 @@ local function init(panel) { name = "RTU", color = style.fp.text }, { name = "CRD", color = style.fp.text }, { name = "PKT", color = style.fp.text }, + { name = "CHECK", color = style.fp.text } } - TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.theme.highlight_box_bright} + TabBar{parent=panel,y=2,tabs=tabs,min_width=7,callback=page_pane.set_value,fg_bg=style.theme.highlight_box_bright} - -- link RTU/PDG list management to PGI - pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry) + -- link RTU/PDG/CHK list management to PGI + pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry, chk_list, chk_entry) end return init diff --git a/supervisor/panel/pgi.lua b/supervisor/panel/pgi.lua index dd8b202..1e46fee 100644 --- a/supervisor/panel/pgi.lua +++ b/supervisor/panel/pgi.lua @@ -10,10 +10,12 @@ local pgi = {} local data = { rtu_list = nil, ---@type nil|graphics_element pdg_list = nil, ---@type nil|graphics_element + chk_list = nil, ---@type nil|graphics_element rtu_entry = nil, ---@type function pdg_entry = nil, ---@type function - -- session entries - s_entries = { rtu = {}, pdg = {} } + chk_entry = nil, ---@type function + -- list entries + entries = { rtu = {}, pdg = {}, chk = {} } } -- link list boxes @@ -21,19 +23,25 @@ local data = { ---@param rtu_entry function RTU entry constructor ---@param pdg_list graphics_element pocket diagnostics list element ---@param pdg_entry function pocket diagnostics entry constructor -function pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry) +---@param chk_list graphics_element CHK list element +---@param chk_entry function CHK entry constructor +function pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry, chk_list, chk_entry) data.rtu_list = rtu_list data.pdg_list = pdg_list + data.chk_list = chk_list data.rtu_entry = rtu_entry data.pdg_entry = pdg_entry + data.chk_entry = chk_entry end -- unlink all fields, disabling the PGI function pgi.unlink() data.rtu_list = nil data.pdg_list = nil + data.chk_list = nil data.rtu_entry = nil data.pdg_entry = nil + data.chk_entry = nil end -- add an RTU entry to the RTU list @@ -43,7 +51,8 @@ function pgi.create_rtu_entry(session_id) local success, result = pcall(data.rtu_entry, data.rtu_list, session_id) if success then - data.s_entries.rtu[session_id] = result + data.entries.rtu[session_id] = result + log.debug(util.c("PGI: created RTU entry (", session_id, ")")) else log.error(util.c("PGI: failed to create RTU entry (", result, ")"), true) end @@ -53,15 +62,17 @@ end -- delete an RTU entry from the RTU list ---@param session_id integer RTU session function pgi.delete_rtu_entry(session_id) - if data.s_entries.rtu[session_id] ~= nil then - local success, result = pcall(data.s_entries.rtu[session_id].delete) - data.s_entries.rtu[session_id] = nil + if data.entries.rtu[session_id] ~= nil then + local success, result = pcall(data.entries.rtu[session_id].delete) + data.entries.rtu[session_id] = nil - if not success then + if success then + log.debug(util.c("PGI: deleted RTU entry (", session_id, ")")) + else log.error(util.c("PGI: failed to delete RTU entry (", result, ")"), true) end else - log.debug(util.c("PGI: tried to delete unknown RTU entry ", session_id)) + log.warning(util.c("PGI: tried to delete unknown RTU entry ", session_id)) end end @@ -72,7 +83,8 @@ function pgi.create_pdg_entry(session_id) local success, result = pcall(data.pdg_entry, data.pdg_list, session_id) if success then - data.s_entries.pdg[session_id] = result + data.entries.pdg[session_id] = result + log.debug(util.c("PGI: created PDG entry (", session_id, ")")) else log.error(util.c("PGI: failed to create PDG entry (", result, ")"), true) end @@ -82,15 +94,57 @@ end -- delete a PDG entry from the PDG list ---@param session_id integer pocket diagnostics session function pgi.delete_pdg_entry(session_id) - if data.s_entries.pdg[session_id] ~= nil then - local success, result = pcall(data.s_entries.pdg[session_id].delete) - data.s_entries.pdg[session_id] = nil + if data.entries.pdg[session_id] ~= nil then + local success, result = pcall(data.entries.pdg[session_id].delete) + data.entries.pdg[session_id] = nil - if not success then + if success then + log.debug(util.c("PGI: deleted PDG entry (", session_id, ")")) + else log.error(util.c("PGI: failed to delete PDG entry (", result, ")"), true) end else - log.debug(util.c("PGI: tried to delete unknown PDG entry ", session_id)) + log.warning(util.c("PGI: tried to delete unknown PDG entry ", session_id)) + end +end + +-- add a device ID check failure entry to the CHK list +---@param unit unit_session RTU session +---@param fail_code integer failure code +function pgi.create_chk_entry(unit, fail_code) + local gw_session = unit.get_session_id() + + if data.chk_list ~= nil and data.chk_entry ~= nil then + if not data.entries.chk[gw_session] then data.entries.chk[gw_session] = {} end + + local success, result = pcall(data.chk_entry, data.chk_list, unit, fail_code) + + if success then + data.entries.chk[gw_session][unit.get_unit_id()] = result + log.debug(util.c("PGI: created CHK entry (", gw_session, ":", unit.get_unit_id(), ")")) + else + log.error(util.c("PGI: failed to create CHK entry (", result, ")"), true) + end + end +end + +-- delete a device ID check failure entry from the CHK list +---@param unit unit_session RTU session +function pgi.delete_chk_entry(unit) + local gw_session = unit.get_session_id() + local ent_chk = data.entries.chk + + if ent_chk[gw_session] ~= nil and ent_chk[gw_session][unit.get_unit_id()] ~= nil then + local success, result = pcall(ent_chk[gw_session][unit.get_unit_id()].delete) + ent_chk[gw_session][unit.get_unit_id()] = nil + + if success then + log.debug(util.c("PGI: deleted CHK entry ", gw_session, ":", unit.get_unit_id())) + else + log.error(util.c("PGI: failed to delete CHK entry (", result, ")"), true) + end + else + log.warning(util.c("PGI: tried to delete unknown CHK entry with session of ", gw_session, " and unit ID of ", unit.get_unit_id())) end end diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index f404291..0f3fece 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -4,6 +4,7 @@ local util = require("scada-common.util") local databus = require("supervisor.databus") local facility = require("supervisor.facility") +local pgi = require("supervisor.pgi") local coordinator = require("supervisor.session.coordinator") local plc = require("supervisor.session.plc") @@ -194,12 +195,8 @@ end local function _update_dev_dbg() local f = function (unit) return unit.is_connected() end - ---@param unit unit_session - local on_delete = function (unit) - end - - util.filter_table(self.dev_dbg.duplicate, f, on_delete) - util.filter_table(self.dev_dbg.out_of_range, f, on_delete) + util.filter_table(self.dev_dbg.duplicate, f, pgi.delete_chk_entry) + util.filter_table(self.dev_dbg.out_of_range, f, pgi.delete_chk_entry) end -- SHARED FUNCTIONS -- @@ -231,6 +228,20 @@ local function check_rtu_id(unit, list, max) fail_code, fail_str = 3, "too many of this type" end + -- add to the list for the user + if fail_code > 0 then + local cmp_id + + for i = 1, #self.sessions.rtu do + if self.sessions.rtu[i].instance.get_id() == unit.get_session_id() then + cmp_id = self.sessions.rtu[i].s_addr + break + end + end + + pgi.create_chk_entry(unit, fail_code, cmp_id) + end + return fail_code, fail_str end From e076e327d8982ef948435703c4637af5e3afafbb Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 18 Aug 2024 19:10:43 -0400 Subject: [PATCH 06/19] split up facility logic into two files --- supervisor/facility.lua | 848 ++------------------------------- supervisor/facility_update.lua | 831 ++++++++++++++++++++++++++++++++ 2 files changed, 858 insertions(+), 821 deletions(-) create mode 100644 supervisor/facility_update.lua diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 17011ae..3ef7eb5 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -1,40 +1,18 @@ -local audio = require("scada-common.audio") -local const = require("scada-common.constants") -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 log = require("scada-common.log") +local types = require("scada-common.types") +local util = require("scada-common.util") -local unit = require("supervisor.unit") +local fac_update = require("supervisor.facility_update") -local qtypes = require("supervisor.session.rtu.qtypes") +local unit = require("supervisor.unit") -local rsctl = require("supervisor.session.rsctl") +local rsctl = require("supervisor.session.rsctl") -local TONE = audio.TONE - -local ALARM = types.ALARM -local PRIO = types.ALARM_PRIORITY -local ALARM_STATE = types.ALARM_STATE -local CONTAINER_MODE = types.CONTAINER_MODE -local PROCESS = types.PROCESS -local PROCESS_NAMES = types.PROCESS_NAMES -local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE -local WASTE_MODE = types.WASTE_MODE -local WASTE = types.WASTE_PRODUCT - -local IO = rsio.IO - -local DTV_RTU_S_DATA = qtypes.DTV_RTU_S_DATA - --- 7.14 kJ per blade for 1 mB of fissile fuel
--- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum) -local POWER_PER_BLADE = util.joules_to_fe_rf(7140) - -local FLOW_STABILITY_DELAY_S = const.FLOW_STABILITY_DELAY_MS / 1000 - -local ALARM_LIMS = const.ALARM_LIMITS +local PROCESS = types.PROCESS +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE +local WASTE = types.WASTE_PRODUCT +---@enum AUTO_SCRAM local AUTO_SCRAM = { NONE = 0, MATRIX_DC = 1, @@ -44,20 +22,13 @@ local AUTO_SCRAM = { GEN_FAULT = 5 } +---@enum START_STATUS local START_STATUS = { OK = 0, NO_UNITS = 1, BLADE_MISMATCH = 2 } -local charge_Kp = 0.15 -local charge_Ki = 0.0 -local charge_Kd = 0.6 - -local rate_Kp = 2.45 -local rate_Ki = 0.4825 -local rate_Kd = -1.0 - ---@class facility_management local facility = {} @@ -67,8 +38,10 @@ local facility = {} ---@param cooling_conf sv_cooling_conf cooling configurations of reactor units ---@param check_rtu_id function ID checking function for RTUs attempting to be linked function facility.new(config, cooling_conf, check_rtu_id) + ---@class _facility_self local self = { units = {}, + types = { AUTO_SCRAM = AUTO_SCRAM, START_STATUS = START_STATUS }, status_text = { "START UP", "initializing..." }, all_sys_ok = false, allow_testing = false, @@ -143,6 +116,9 @@ function facility.new(config, cooling_conf, check_rtu_id) imtx_faulted_times = { 0, 0, 0 } } + -- provide self to facility update functions + local f_update = fac_update(self) + -- create units for i = 1, config.UnitCount do table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BoilerCount, cooling_conf.r_cool[i].TurbineCount, check_rtu_id, config.ExtChargeIdling)) @@ -162,87 +138,6 @@ function facility.new(config, cooling_conf, check_rtu_id) table.insert(self.test_tone_states, false) end - -- PRIVATE FUNCTIONS -- - - -- check if all auto-controlled units completed ramping - ---@nodiscard - local function _all_units_ramped() - local all_ramped = true - - for i = 1, #self.prio_defs do - local units = self.prio_defs[i] - for u = 1, #units do - all_ramped = all_ramped and units[u].auto_ramp_complete() - end - end - - return all_ramped - end - - -- split a burn rate among the reactors - ---@param burn_rate number burn rate assignment - ---@param ramp boolean true to ramp, false to set right away - ---@param abort_on_fault boolean? true to exit if one device has an effective burn rate different than its limit - ---@return integer unallocated_br100, boolean? aborted - local function _allocate_burn_rate(burn_rate, ramp, abort_on_fault) - local unallocated = math.floor(burn_rate * 100) - - -- go through all priority groups - for i = 1, #self.prio_defs do - local units = self.prio_defs[i] - - if #units > 0 then - local split = math.floor(unallocated / #units) - - local splits = {} - for u = 1, #units do splits[u] = split end - splits[#units] = splits[#units] + (unallocated % #units) - - -- go through all reactor units in this group - for id = 1, #units do - local u = units[id] ---@type reactor_unit - - local ctl = u.get_control_inf() - local lim_br100 = u.auto_get_effective_limit() - - if abort_on_fault and (lim_br100 ~= ctl.lim_br100) then - -- effective limit differs from set limit, unit is degraded - return unallocated, true - end - - local last = ctl.br100 - - if splits[id] <= lim_br100 then - ctl.br100 = splits[id] - else - ctl.br100 = lim_br100 - - if id < #units then - local remaining = #units - id - split = math.floor(unallocated / remaining) - for x = (id + 1), #units do splits[x] = split end - splits[#units] = splits[#units] + (unallocated % remaining) - end - end - - unallocated = math.max(0, unallocated - ctl.br100) - - if last ~= ctl.br100 then u.auto_commit_br100(ramp) end - end - end - end - - return unallocated, false - end - - -- set idle state of all assigned reactors - ---@param idle boolean idle state - local function _set_idling(idle) - for i = 1, #self.prio_defs do - for _, u in pairs(self.prio_defs[i]) do u.auto_set_idle(idle) end - end - end - -- PUBLIC FUNCTIONS -- ---@class facility @@ -304,709 +199,20 @@ function facility.new(config, cooling_conf, check_rtu_id) -- update (iterate) the facility management function public.update() - -- unlink RTU sessions if they are closed - for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end + -- run process control and evaluate automatic SCRAM + f_update.pre_auto() + f_update.auto_control(config.ExtChargeIdling) + f_update.auto_safety() + f_update.post_auto() - -- check if test routines are allowed right now - self.allow_testing = true - for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit - self.allow_testing = self.allow_testing and u.is_safe_idle() - end + -- handle redstone I/O + f_update.redstone(public.ack_all) - -- current state for process control - local charge_update = 0 - local rate_update = 0 + -- unit tasks + f_update.unit_mgmt(cooling_conf) - -- calculate moving averages for induction matrix - if self.induction[1] ~= nil then - local matrix = self.induction[1] ---@type unit_session - local db = matrix.get_db() ---@type imatrix_session_db - - local build_update = db.build.last_update - rate_update = db.state.last_update - charge_update = db.tanks.last_update - - local has_data = build_update > 0 and rate_update > 0 and charge_update > 0 - - if matrix.is_faulted() then - -- a fault occured, cannot reliably update stats - has_data = false - self.im_stat_init = false - self.imtx_faulted_times = { build_update, rate_update, charge_update } - elseif not self.im_stat_init then - -- prevent operation with partially invalid data - -- all fields must have updated since the last fault - has_data = self.imtx_faulted_times[1] < build_update and - self.imtx_faulted_times[2] < rate_update and - self.imtx_faulted_times[3] < charge_update - end - - if has_data then - local energy = util.joules_to_fe_rf(db.tanks.energy) - local input = util.joules_to_fe_rf(db.state.last_input) - local output = util.joules_to_fe_rf(db.state.last_output) - - if self.im_stat_init then - self.avg_charge.record(energy, charge_update) - self.avg_inflow.record(input, rate_update) - self.avg_outflow.record(output, rate_update) - - if charge_update ~= self.imtx_last_charge_t then - local delta = (energy - self.imtx_last_charge) / (charge_update - self.imtx_last_charge_t) - - self.imtx_last_charge = energy - self.imtx_last_charge_t = charge_update - - -- if the capacity changed, toss out existing data - if db.build.max_energy ~= self.imtx_last_capacity then - self.imtx_last_capacity = db.build.max_energy - self.avg_net.reset() - else - self.avg_net.record(delta, charge_update) - end - end - else - self.im_stat_init = true - - self.avg_charge.reset(energy) - self.avg_inflow.reset(input) - self.avg_outflow.reset(output) - self.avg_net.reset() - - self.imtx_last_capacity = db.build.max_energy - self.imtx_last_charge = energy - self.imtx_last_charge_t = charge_update - end - else - -- prevent use by control systems - rate_update = 0 - charge_update = 0 - end - else - self.im_stat_init = false - end - - self.all_sys_ok = true - for i = 1, #self.units do - self.all_sys_ok = self.all_sys_ok and not self.units[i].get_control_inf().degraded - end - - ------------------------- - -- Run Process Control -- - ------------------------- - - --#region - - local avg_charge = self.avg_charge.compute() - local avg_inflow = self.avg_inflow.compute() - local avg_outflow = self.avg_outflow.compute() - - local now = os.clock() - - local state_changed = self.mode ~= self.last_mode - local next_mode = self.mode - - -- once auto control is started, sort the priority sublists by limits - if state_changed then - self.saturated = false - - log.debug(util.c("FAC: state changed from ", PROCESS_NAMES[self.last_mode + 1], " to ", PROCESS_NAMES[self.mode + 1])) - - if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then - self.start_fail = START_STATUS.OK - - if (self.mode ~= PROCESS.MATRIX_FAULT_IDLE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then - -- auto clear ASCRAM - self.ascram = false - self.ascram_reason = AUTO_SCRAM.NONE - end - - local blade_count = nil - self.max_burn_combined = 0.0 - - for i = 1, #self.prio_defs do - table.sort(self.prio_defs[i], - ---@param a reactor_unit - ---@param b reactor_unit - function (a, b) return a.get_control_inf().lim_br100 < b.get_control_inf().lim_br100 end - ) - - for _, u in pairs(self.prio_defs[i]) do - local u_blade_count = u.get_control_inf().blade_count - - if blade_count == nil then - blade_count = u_blade_count - elseif (u_blade_count ~= blade_count) and (self.mode == PROCESS.GEN_RATE) then - log.warning("FAC: cannot start GEN_RATE process with inconsistent unit blade counts") - next_mode = PROCESS.INACTIVE - self.start_fail = START_STATUS.BLADE_MISMATCH - end - - if self.start_fail == START_STATUS.OK then u.auto_engage() end - - self.max_burn_combined = self.max_burn_combined + (u.get_control_inf().lim_br100 / 100.0) - end - end - - log.debug(util.c("FAC: computed a max combined burn rate of ", self.max_burn_combined, "mB/t")) - - if blade_count == nil then - -- no units - log.warning("FAC: cannot start process control with 0 units assigned") - next_mode = PROCESS.INACTIVE - self.start_fail = START_STATUS.NO_UNITS - else - self.charge_conversion = blade_count * POWER_PER_BLADE - end - elseif self.mode == PROCESS.INACTIVE then - for i = 1, #self.prio_defs do - -- disable reactors and disengage auto control - for _, u in pairs(self.prio_defs[i]) do - u.disable() - u.auto_set_idle(false) - u.auto_disengage() - end - end - - log.info("FAC: disengaging auto control (now inactive)") - end - - self.initial_ramp = true - self.waiting_on_ramp = false - self.waiting_on_stable = false - else - self.initial_ramp = false - end - - -- update unit ready state - local assign_count = 0 - self.units_ready = true - for i = 1, #self.prio_defs do - for _, u in pairs(self.prio_defs[i]) do - assign_count = assign_count + 1 - self.units_ready = self.units_ready and u.get_control_inf().ready - end - end - - -- perform mode-specific operations - if self.mode == PROCESS.INACTIVE then - if not self.units_ready then - self.status_text = { "NOT READY", "assigned units not ready" } - else - -- clear ASCRAM once ready - self.ascram = false - self.ascram_reason = AUTO_SCRAM.NONE - - if self.start_fail == START_STATUS.NO_UNITS and assign_count == 0 then - self.status_text = { "START FAILED", "no units were assigned" } - elseif self.start_fail == START_STATUS.BLADE_MISMATCH then - self.status_text = { "START FAILED", "turbine blade count mismatch" } - else - self.status_text = { "IDLE", "control disengaged" } - end - end - elseif self.mode == PROCESS.MAX_BURN then - -- run units at their limits - if state_changed then - self.time_start = now - self.saturated = true - - self.status_text = { "MONITORED MODE", "running reactors at limit" } - log.info("FAC: MAX_BURN process mode started") - end - - _allocate_burn_rate(self.max_burn_combined, true) - elseif self.mode == PROCESS.BURN_RATE then - -- a total aggregate burn rate - if state_changed then - self.time_start = now - self.status_text = { "BURN RATE MODE", "running" } - log.info("FAC: BURN_RATE process mode started") - end - - local unallocated = _allocate_burn_rate(self.burn_target, true) - self.saturated = self.burn_target == self.max_burn_combined or unallocated > 0 - elseif self.mode == PROCESS.CHARGE then - -- target a level of charge - if state_changed then - self.time_start = now - self.last_time = now - self.last_error = 0 - self.accumulator = 0 - - -- enabling idling on all assigned units - _set_idling(true) - - self.status_text = { "CHARGE MODE", "running control loop" } - log.info("FAC: CHARGE mode starting PID control") - elseif self.last_update < charge_update then - -- convert to kFE to make constants not microscopic - local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000 - - -- stop accumulator when saturated to avoid windup - if not self.saturated then - self.accumulator = self.accumulator + (error * (now - self.last_time)) - end - - -- local runtime = now - self.time_start - local integral = self.accumulator - local derivative = (error - self.last_error) / (now - self.last_time) - - local P = charge_Kp * error - local I = charge_Ki * integral - local D = charge_Kd * derivative - - local output = P + I + D - - -- clamp at range -> output clamped (out_c) - local out_c = math.max(0, math.min(output, self.max_burn_combined)) - - self.saturated = output ~= out_c - - if not config.ExtChargeIdling then - -- stop idling early if the output is zero, we are at or above the setpoint, and are not losing charge - _set_idling(not ((out_c == 0) and (error <= 0) and (avg_outflow <= 0))) - end - - -- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }", - -- runtime, avg_charge, error, integral, output, out_c, P, I, D)) - - _allocate_burn_rate(out_c, true) - - self.last_time = now - self.last_error = error - end - - self.last_update = charge_update - elseif self.mode == PROCESS.GEN_RATE then - -- target a rate of generation - if state_changed then - -- estimate an initial output - local output = self.gen_rate_setpoint / self.charge_conversion - - local unallocated = _allocate_burn_rate(output, true) - - self.saturated = output >= self.max_burn_combined or unallocated > 0 - self.waiting_on_ramp = true - - self.status_text = { "GENERATION MODE", "starting up" } - log.info(util.c("FAC: GEN_RATE process mode initial ramp started (initial target is ", output, " mB/t)")) - elseif self.waiting_on_ramp then - if _all_units_ramped() then - self.waiting_on_ramp = false - self.waiting_on_stable = true - - self.time_start = now - - self.status_text = { "GENERATION MODE", "holding ramped rate" } - log.info("FAC: GEN_RATE process mode initial ramp completed, holding for stablization time") - end - elseif self.waiting_on_stable then - if (now - self.time_start) > FLOW_STABILITY_DELAY_S then - self.waiting_on_stable = false - - self.time_start = now - self.last_time = now - self.last_error = 0 - self.accumulator = 0 - - self.status_text = { "GENERATION MODE", "running control loop" } - log.info("FAC: GEN_RATE process mode initial hold completed, starting PID control") - end - elseif self.last_update < rate_update then - -- convert to MFE (in rounded kFE) to make constants not microscopic - local error = util.round((self.gen_rate_setpoint - avg_inflow) / 1000) / 1000 - - -- stop accumulator when saturated to avoid windup - if not self.saturated then - self.accumulator = self.accumulator + (error * (now - self.last_time)) - end - - -- local runtime = now - self.time_start - local integral = self.accumulator - local derivative = (error - self.last_error) / (now - self.last_time) - - local P = rate_Kp * error - local I = rate_Ki * integral - local D = rate_Kd * derivative - - -- velocity (rate) (derivative of charge level => rate) feed forward - local FF = self.gen_rate_setpoint / self.charge_conversion - - local output = P + I + D + FF - - -- clamp at range -> output clamped (sp_c) - local out_c = math.max(0, math.min(output, self.max_burn_combined)) - - self.saturated = output ~= out_c - - -- log.debug(util.sprintf("GEN_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }", - -- runtime, avg_inflow, error, integral, output, out_c, P, I, D)) - - _allocate_burn_rate(out_c, false) - - self.last_time = now - self.last_error = error - end - - self.last_update = rate_update - elseif self.mode == PROCESS.MATRIX_FAULT_IDLE then - -- exceeded charge, wait until condition clears - if self.ascram_reason == AUTO_SCRAM.NONE then - next_mode = self.return_mode - log.info("FAC: exiting matrix fault idle state due to fault resolution") - elseif self.ascram_reason == AUTO_SCRAM.CRIT_ALARM then - next_mode = PROCESS.SYSTEM_ALARM_IDLE - log.info("FAC: exiting matrix fault idle state due to critical unit alarm") - end - elseif self.mode == PROCESS.SYSTEM_ALARM_IDLE then - -- do nothing, wait for user to confirm (stop and reset) - elseif self.mode == PROCESS.GEN_RATE_FAULT_IDLE then - -- system faulted (degraded/not ready) while running generation rate mode - -- mode will need to be fully restarted once everything is OK to re-ramp to feed-forward - if self.units_ready then - log.info("FAC: system ready after faulting out of GEN_RATE process mode, switching back...") - next_mode = PROCESS.GEN_RATE - end - elseif self.mode ~= PROCESS.INACTIVE then - log.error(util.c("FAC: unsupported process mode ", self.mode, ", switching to inactive")) - next_mode = PROCESS.INACTIVE - end - - --#endregion - - ------------------------------ - -- Evaluate Automatic SCRAM -- - ------------------------------ - - --#region - - local astatus = self.ascram_status - - if self.induction[1] ~= nil then - local db = self.induction[1].get_db() ---@type imatrix_session_db - - -- clear matrix disconnected - if astatus.matrix_dc then - astatus.matrix_dc = false - log.info("FAC: induction matrix reconnected, clearing ASCRAM condition") - end - - -- check matrix fill too high - local was_fill = astatus.matrix_fill - astatus.matrix_fill = (db.tanks.energy_fill >= ALARM_LIMS.CHARGE_HIGH) or (astatus.matrix_fill and db.tanks.energy_fill > ALARM_LIMS.CHARGE_RE_ENABLE) - - if was_fill and not astatus.matrix_fill then - log.info(util.c("FAC: charge state of induction matrix entered acceptable range <= ", ALARM_LIMS.CHARGE_RE_ENABLE * 100, "%")) - end - - -- check for critical unit alarms - astatus.crit_alarm = false - for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit - - if u.has_alarm_min_prio(PRIO.CRITICAL) then - astatus.crit_alarm = true - break - end - end - - -- check for facility radiation - if #self.envd > 0 then - local max_rad = 0 - - for i = 1, #self.envd do - local envd = self.envd[i] ---@type unit_session - local e_db = envd.get_db() ---@type envd_session_db - if e_db.radiation_raw > max_rad then max_rad = e_db.radiation_raw end - end - - astatus.radiation = max_rad >= ALARM_LIMS.FAC_HIGH_RAD - else - -- don't clear, if it is true then we lost it with high radiation, so just keep alarming - -- operator can restart the system or hit the stop/reset button - end - - -- system not ready, will need to restart GEN_RATE mode - -- clears when we enter the fault waiting state - astatus.gen_fault = self.mode == PROCESS.GEN_RATE and not self.units_ready - else - astatus.matrix_dc = true - end - - if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then - local scram = astatus.matrix_dc or astatus.matrix_fill or astatus.crit_alarm or astatus.gen_fault - - if scram and not self.ascram then - -- SCRAM all units - for i = 1, #self.prio_defs do - for _, u in pairs(self.prio_defs[i]) do - u.auto_scram() - end - end - - if astatus.crit_alarm then - -- highest priority alarm - next_mode = PROCESS.SYSTEM_ALARM_IDLE - self.ascram_reason = AUTO_SCRAM.CRIT_ALARM - self.status_text = { "AUTOMATIC SCRAM", "critical unit alarm tripped" } - - log.info("FAC: automatic SCRAM due to critical unit alarm") - log.warning("FAC: emergency exit of process control due to critical unit alarm") - elseif astatus.radiation then - next_mode = PROCESS.SYSTEM_ALARM_IDLE - self.ascram_reason = AUTO_SCRAM.RADIATION - self.status_text = { "AUTOMATIC SCRAM", "facility radiation high" } - - log.info("FAC: automatic SCRAM due to high facility radiation") - elseif astatus.matrix_dc then - next_mode = PROCESS.MATRIX_FAULT_IDLE - self.ascram_reason = AUTO_SCRAM.MATRIX_DC - self.status_text = { "AUTOMATIC SCRAM", "induction matrix disconnected" } - - if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then self.return_mode = self.mode end - - log.info("FAC: automatic SCRAM due to induction matrix disconnection") - elseif astatus.matrix_fill then - next_mode = PROCESS.MATRIX_FAULT_IDLE - self.ascram_reason = AUTO_SCRAM.MATRIX_FILL - self.status_text = { "AUTOMATIC SCRAM", "induction matrix fill high" } - - if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then self.return_mode = self.mode end - - log.info("FAC: automatic SCRAM due to induction matrix high charge") - elseif astatus.gen_fault then - -- lowest priority alarm - next_mode = PROCESS.GEN_RATE_FAULT_IDLE - self.ascram_reason = AUTO_SCRAM.GEN_FAULT - self.status_text = { "GENERATION MODE IDLE", "paused: system not ready" } - - log.info("FAC: automatic SCRAM due to unit problem while in GEN_RATE mode, will resume once all units are ready") - end - end - - self.ascram = scram - - if not self.ascram then - self.ascram_reason = AUTO_SCRAM.NONE - - -- reset PLC RPS trips if we should - for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit - u.auto_cond_rps_reset() - end - end - end - - --#endregion - - -- update last mode and set next mode - self.last_mode = self.mode - self.mode = next_mode - - ------------------------- - -- Handle Redstone I/O -- - ------------------------- - - --#region - - if #self.redstone > 0 then - -- handle facility SCRAM - if self.io_ctl.digital_read(IO.F_SCRAM) then - for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit - u.cond_scram() - end - end - - -- handle facility ack - if self.io_ctl.digital_read(IO.F_ACK) then public.ack_all() end - - -- update facility alarm outputs - local has_prio_alarm, has_any_alarm = false, false - for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit - - if u.has_alarm_min_prio(PRIO.EMERGENCY) then - has_prio_alarm, has_any_alarm = true, true - break - elseif u.has_alarm_min_prio(PRIO.TIMELY) then - has_any_alarm = true - end - end - - self.io_ctl.digital_write(IO.F_ALARM, has_prio_alarm) - self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm) - - -- update induction matrix related outputs - if self.induction[1] ~= nil then - local db = self.induction[1].get_db() ---@type imatrix_session_db - - self.io_ctl.digital_write(IO.F_MATRIX_LOW, db.tanks.energy_fill < const.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) - self.io_ctl.digital_write(IO.F_MATRIX_HIGH, db.tanks.energy_fill > const.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) - self.io_ctl.analog_write(IO.F_MATRIX_CHG, db.tanks.energy_fill, 0, 1) - end - end - - --#endregion - - ---------------- - -- Unit Tasks -- - ---------------- - - --#region - - local insufficent_po_rate = false - local need_emcool = false - - for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit - - -- update auto waste processing - if u.get_control_inf().waste_mode == WASTE_MODE.AUTO then - if (u.get_sna_rate() * 10.0) < u.get_burn_rate() then - insufficent_po_rate = true - end - end - - -- check if unit activated emergency coolant & uses facility tanks - if (cooling_conf.fac_tank_mode > 0) and u.is_emer_cool_tripped() and (cooling_conf.fac_tank_defs[i] == 2) then - need_emcool = true - end - end - - -- update waste product - - self.current_waste_product = self.waste_product - - if (not self.sps_low_power) and (self.waste_product == WASTE.ANTI_MATTER) and (self.induction[1] ~= nil) then - local db = self.induction[1].get_db() ---@type imatrix_session_db - - if db.tanks.energy_fill >= 0.15 then - self.disabled_sps = false - elseif self.disabled_sps or ((db.tanks.last_update > 0) and (db.tanks.energy_fill < 0.1)) then - self.disabled_sps = true - self.current_waste_product = WASTE.POLONIUM - end - else - self.disabled_sps = false - end - - if self.pu_fallback and insufficent_po_rate then - self.current_waste_product = WASTE.PLUTONIUM - end - - -- make sure dynamic tanks are allowing outflow if required - -- set all, rather than trying to determine which is for which (simpler & safer) - -- there should be no need for any to be in fill only mode - if need_emcool then - for i = 1, #self.tanks do - local session = self.tanks[i] ---@type unit_session - local tank = session.get_db() ---@type dynamicv_session_db - - if tank.state.container_mode == CONTAINER_MODE.FILL then - session.get_cmd_queue().push_data(DTV_RTU_S_DATA.SET_CONT_MODE, CONTAINER_MODE.BOTH) - end - end - end - - --#endregion - - ------------------------ - -- Update Alarm Tones -- - ------------------------ - - --#region - - local allow_test = self.allow_testing and self.test_tone_set - - local alarms = { false, false, false, false, false, false, false, false, false, false, false, false } - - -- reset tone states before re-evaluting - for i = 1, #self.tone_states do self.tone_states[i] = false end - - if allow_test then - alarms = self.test_alarm_states - else - -- check all alarms for all units - for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit - for id, alarm in pairs(u.get_alarms()) do - alarms[id] = alarms[id] or (alarm == ALARM_STATE.TRIPPED) - end - end - - if not self.test_tone_reset then - -- clear testing alarms if we aren't using them - for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end - end - end - - -- Evaluate Alarms -- - - -- containment breach is worst case CRITICAL alarm, this takes priority - if alarms[ALARM.ContainmentBreach] then - self.tone_states[TONE.T_1800Hz_Int_4Hz] = true - else - -- critical damage is highest priority CRITICAL level alarm - if alarms[ALARM.CriticalDamage] then - self.tone_states[TONE.T_660Hz_Int_125ms] = true - else - -- EMERGENCY level alarms + URGENT over temp - if alarms[ALARM.ReactorDamage] or alarms[ALARM.ReactorOverTemp] or alarms[ALARM.ReactorWasteLeak] then - self.tone_states[TONE.T_544Hz_440Hz_Alt] = true - -- URGENT level turbine trip - elseif alarms[ALARM.TurbineTrip] then - self.tone_states[TONE.T_745Hz_Int_1Hz] = true - -- URGENT level reactor lost - elseif alarms[ALARM.ReactorLost] then - self.tone_states[TONE.T_340Hz_Int_2Hz] = true - -- TIMELY level alarms - elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then - self.tone_states[TONE.T_800Hz_Int] = true - end - end - - -- check RPS transient URGENT level alarm - if alarms[ALARM.RPSTransient] then - self.tone_states[TONE.T_1000Hz_Int] = true - -- disable really painful audio combination - self.tone_states[TONE.T_340Hz_Int_2Hz] = false - end - end - - -- radiation is a big concern, always play this CRITICAL level alarm if active - if alarms[ALARM.ContainmentRadiation] then - self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true - -- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled - -- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one - if self.tone_states[TONE.T_1000Hz_Int] and alarms[ALARM.ReactorLost] then self.tone_states[TONE.T_340Hz_Int_2Hz] = true end - -- it sounds *really* bad if this is in conjunction with these other tones, so disable them - self.tone_states[TONE.T_745Hz_Int_1Hz] = false - self.tone_states[TONE.T_800Hz_Int] = false - self.tone_states[TONE.T_1000Hz_Int] = false - end - - -- add to tone states if testing is active - if allow_test then - for i = 1, #self.tone_states do - self.tone_states[i] = self.tone_states[i] or self.test_tone_states[i] - end - - self.test_tone_reset = false - else - if not self.test_tone_reset then - -- clear testing tones if we aren't using them - for i = 1, #self.test_tone_states do self.test_tone_states[i] = false end - end - - -- flag that tones were reset - self.test_tone_set = false - self.test_tone_reset = true - end - - --#endregion + -- update alarm tones + f_update.update_alarms() end -- call the update function of all units in the facility
diff --git a/supervisor/facility_update.lua b/supervisor/facility_update.lua new file mode 100644 index 0000000..94255b2 --- /dev/null +++ b/supervisor/facility_update.lua @@ -0,0 +1,831 @@ +local audio = require("scada-common.audio") +local const = require("scada-common.constants") +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") + +local TONE = audio.TONE + +local ALARM = types.ALARM +local PRIO = types.ALARM_PRIORITY +local ALARM_STATE = types.ALARM_STATE +local CONTAINER_MODE = types.CONTAINER_MODE +local PROCESS = types.PROCESS +local PROCESS_NAMES = types.PROCESS_NAMES +local WASTE_MODE = types.WASTE_MODE +local WASTE = types.WASTE_PRODUCT + +local IO = rsio.IO + +local ALARM_LIMS = const.ALARM_LIMITS + +local DTV_RTU_S_DATA = qtypes.DTV_RTU_S_DATA + +-- 7.14 kJ per blade for 1 mB of fissile fuel
+-- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum) +local POWER_PER_BLADE = util.joules_to_fe_rf(7140) + +local FLOW_STABILITY_DELAY_S = const.FLOW_STABILITY_DELAY_MS / 1000 + +local CHARGE_Kp = 0.15 +local CHARGE_Ki = 0.0 +local CHARGE_Kd = 0.6 + +local RATE_Kp = 2.45 +local RATE_Ki = 0.4825 +local RATE_Kd = -1.0 + +local self = nil ---@type _facility_self +local next_mode = 0 +local charge_update = 0 +local rate_update = 0 + +local update = {} + +--#region PRIVATE FUNCTIONS + +-- check if all auto-controlled units completed ramping +---@nodiscard +local function all_units_ramped() + local all_ramped = true + + for i = 1, #self.prio_defs do + local units = self.prio_defs[i] + for u = 1, #units do + all_ramped = all_ramped and units[u].auto_ramp_complete() + end + end + + return all_ramped +end + +-- split a burn rate among the reactors +---@param burn_rate number burn rate assignment +---@param ramp boolean true to ramp, false to set right away +---@param abort_on_fault boolean? true to exit if one device has an effective burn rate different than its limit +---@return integer unallocated_br100, boolean? aborted +local function allocate_burn_rate(burn_rate, ramp, abort_on_fault) + local unallocated = math.floor(burn_rate * 100) + + -- go through all priority groups + for i = 1, #self.prio_defs do + local units = self.prio_defs[i] + + if #units > 0 then + local split = math.floor(unallocated / #units) + + local splits = {} + for u = 1, #units do splits[u] = split end + splits[#units] = splits[#units] + (unallocated % #units) + + -- go through all reactor units in this group + for id = 1, #units do + local u = units[id] ---@type reactor_unit + + local ctl = u.get_control_inf() + local lim_br100 = u.auto_get_effective_limit() + + if abort_on_fault and (lim_br100 ~= ctl.lim_br100) then + -- effective limit differs from set limit, unit is degraded + return unallocated, true + end + + local last = ctl.br100 + + if splits[id] <= lim_br100 then + ctl.br100 = splits[id] + else + ctl.br100 = lim_br100 + + if id < #units then + local remaining = #units - id + split = math.floor(unallocated / remaining) + for x = (id + 1), #units do splits[x] = split end + splits[#units] = splits[#units] + (unallocated % remaining) + end + end + + unallocated = math.max(0, unallocated - ctl.br100) + + if last ~= ctl.br100 then u.auto_commit_br100(ramp) end + end + end + end + + return unallocated, false +end + +-- set idle state of all assigned reactors +---@param idle boolean idle state +local function set_idling(idle) + for i = 1, #self.prio_defs do + for _, u in pairs(self.prio_defs[i]) do u.auto_set_idle(idle) end + end +end + +--#endregion + +--#region PUBLIC FUNCTIONS + +-- automatic control pre-update logic +function update.pre_auto() + -- unlink RTU sessions if they are closed + for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end + + -- check if test routines are allowed right now + self.allow_testing = true + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + self.allow_testing = self.allow_testing and u.is_safe_idle() + end + + -- current state for process control + charge_update = 0 + rate_update = 0 + + -- calculate moving averages for induction matrix + if self.induction[1] ~= nil then + local matrix = self.induction[1] ---@type unit_session + local db = matrix.get_db() ---@type imatrix_session_db + + local build_update = db.build.last_update + rate_update = db.state.last_update + charge_update = db.tanks.last_update + + local has_data = build_update > 0 and rate_update > 0 and charge_update > 0 + + if matrix.is_faulted() then + -- a fault occured, cannot reliably update stats + has_data = false + self.im_stat_init = false + self.imtx_faulted_times = { build_update, rate_update, charge_update } + elseif not self.im_stat_init then + -- prevent operation with partially invalid data + -- all fields must have updated since the last fault + has_data = self.imtx_faulted_times[1] < build_update and + self.imtx_faulted_times[2] < rate_update and + self.imtx_faulted_times[3] < charge_update + end + + if has_data then + local energy = util.joules_to_fe_rf(db.tanks.energy) + local input = util.joules_to_fe_rf(db.state.last_input) + local output = util.joules_to_fe_rf(db.state.last_output) + + if self.im_stat_init then + self.avg_charge.record(energy, charge_update) + self.avg_inflow.record(input, rate_update) + self.avg_outflow.record(output, rate_update) + + if charge_update ~= self.imtx_last_charge_t then + local delta = (energy - self.imtx_last_charge) / (charge_update - self.imtx_last_charge_t) + + self.imtx_last_charge = energy + self.imtx_last_charge_t = charge_update + + -- if the capacity changed, toss out existing data + if db.build.max_energy ~= self.imtx_last_capacity then + self.imtx_last_capacity = db.build.max_energy + self.avg_net.reset() + else + self.avg_net.record(delta, charge_update) + end + end + else + self.im_stat_init = true + + self.avg_charge.reset(energy) + self.avg_inflow.reset(input) + self.avg_outflow.reset(output) + self.avg_net.reset() + + self.imtx_last_capacity = db.build.max_energy + self.imtx_last_charge = energy + self.imtx_last_charge_t = charge_update + end + else + -- prevent use by control systems + rate_update = 0 + charge_update = 0 + end + else + self.im_stat_init = false + end + + self.all_sys_ok = true + for i = 1, #self.units do + self.all_sys_ok = self.all_sys_ok and not self.units[i].get_control_inf().degraded + end +end + +-- run auto control +---@param ExtChargeIdling boolean ExtChargeIdling config field +function update.auto_control(ExtChargeIdling) + local AUTO_SCRAM = self.types.AUTO_SCRAM + local START_STATUS = self.types.START_STATUS + + local avg_charge = self.avg_charge.compute() + local avg_inflow = self.avg_inflow.compute() + local avg_outflow = self.avg_outflow.compute() + + local now = os.clock() + + local state_changed = self.mode ~= self.last_mode + next_mode = self.mode + + -- once auto control is started, sort the priority sublists by limits + if state_changed then + self.saturated = false + + log.debug(util.c("FAC: state changed from ", PROCESS_NAMES[self.last_mode + 1], " to ", PROCESS_NAMES[self.mode + 1])) + + if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then + self.start_fail = START_STATUS.OK + + if (self.mode ~= PROCESS.MATRIX_FAULT_IDLE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then + -- auto clear ASCRAM + self.ascram = false + self.ascram_reason = AUTO_SCRAM.NONE + end + + local blade_count = nil + self.max_burn_combined = 0.0 + + for i = 1, #self.prio_defs do + table.sort(self.prio_defs[i], + ---@param a reactor_unit + ---@param b reactor_unit + function (a, b) return a.get_control_inf().lim_br100 < b.get_control_inf().lim_br100 end + ) + + for _, u in pairs(self.prio_defs[i]) do + local u_blade_count = u.get_control_inf().blade_count + + if blade_count == nil then + blade_count = u_blade_count + elseif (u_blade_count ~= blade_count) and (self.mode == PROCESS.GEN_RATE) then + log.warning("FAC: cannot start GEN_RATE process with inconsistent unit blade counts") + next_mode = PROCESS.INACTIVE + self.start_fail = START_STATUS.BLADE_MISMATCH + end + + if self.start_fail == START_STATUS.OK then u.auto_engage() end + + self.max_burn_combined = self.max_burn_combined + (u.get_control_inf().lim_br100 / 100.0) + end + end + + log.debug(util.c("FAC: computed a max combined burn rate of ", self.max_burn_combined, "mB/t")) + + if blade_count == nil then + -- no units + log.warning("FAC: cannot start process control with 0 units assigned") + next_mode = PROCESS.INACTIVE + self.start_fail = START_STATUS.NO_UNITS + else + self.charge_conversion = blade_count * POWER_PER_BLADE + end + elseif self.mode == PROCESS.INACTIVE then + for i = 1, #self.prio_defs do + -- disable reactors and disengage auto control + for _, u in pairs(self.prio_defs[i]) do + u.disable() + u.auto_set_idle(false) + u.auto_disengage() + end + end + + log.info("FAC: disengaging auto control (now inactive)") + end + + self.initial_ramp = true + self.waiting_on_ramp = false + self.waiting_on_stable = false + else + self.initial_ramp = false + end + + -- update unit ready state + local assign_count = 0 + self.units_ready = true + for i = 1, #self.prio_defs do + for _, u in pairs(self.prio_defs[i]) do + assign_count = assign_count + 1 + self.units_ready = self.units_ready and u.get_control_inf().ready + end + end + + -- perform mode-specific operations + if self.mode == PROCESS.INACTIVE then + if not self.units_ready then + self.status_text = { "NOT READY", "assigned units not ready" } + else + -- clear ASCRAM once ready + self.ascram = false + self.ascram_reason = AUTO_SCRAM.NONE + + if self.start_fail == START_STATUS.NO_UNITS and assign_count == 0 then + self.status_text = { "START FAILED", "no units were assigned" } + elseif self.start_fail == START_STATUS.BLADE_MISMATCH then + self.status_text = { "START FAILED", "turbine blade count mismatch" } + else + self.status_text = { "IDLE", "control disengaged" } + end + end + elseif self.mode == PROCESS.MAX_BURN then + -- run units at their limits + if state_changed then + self.time_start = now + self.saturated = true + + self.status_text = { "MONITORED MODE", "running reactors at limit" } + log.info("FAC: MAX_BURN process mode started") + end + + allocate_burn_rate(self.max_burn_combined, true) + elseif self.mode == PROCESS.BURN_RATE then + -- a total aggregate burn rate + if state_changed then + self.time_start = now + self.status_text = { "BURN RATE MODE", "running" } + log.info("FAC: BURN_RATE process mode started") + end + + local unallocated = allocate_burn_rate(self.burn_target, true) + self.saturated = self.burn_target == self.max_burn_combined or unallocated > 0 + elseif self.mode == PROCESS.CHARGE then + -- target a level of charge + if state_changed then + self.time_start = now + self.last_time = now + self.last_error = 0 + self.accumulator = 0 + + -- enabling idling on all assigned units + set_idling(true) + + self.status_text = { "CHARGE MODE", "running control loop" } + log.info("FAC: CHARGE mode starting PID control") + elseif self.last_update < charge_update then + -- convert to kFE to make constants not microscopic + local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000 + + -- stop accumulator when saturated to avoid windup + if not self.saturated then + self.accumulator = self.accumulator + (error * (now - self.last_time)) + end + + -- local runtime = now - self.time_start + local integral = self.accumulator + local derivative = (error - self.last_error) / (now - self.last_time) + + local P = CHARGE_Kp * error + local I = CHARGE_Ki * integral + local D = CHARGE_Kd * derivative + + local output = P + I + D + + -- clamp at range -> output clamped (out_c) + local out_c = math.max(0, math.min(output, self.max_burn_combined)) + + self.saturated = output ~= out_c + + if not ExtChargeIdling then + -- stop idling early if the output is zero, we are at or above the setpoint, and are not losing charge + set_idling(not ((out_c == 0) and (error <= 0) and (avg_outflow <= 0))) + end + + -- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }", + -- runtime, avg_charge, error, integral, output, out_c, P, I, D)) + + allocate_burn_rate(out_c, true) + + self.last_time = now + self.last_error = error + end + + self.last_update = charge_update + elseif self.mode == PROCESS.GEN_RATE then + -- target a rate of generation + if state_changed then + -- estimate an initial output + local output = self.gen_rate_setpoint / self.charge_conversion + + local unallocated = allocate_burn_rate(output, true) + + self.saturated = output >= self.max_burn_combined or unallocated > 0 + self.waiting_on_ramp = true + + self.status_text = { "GENERATION MODE", "starting up" } + log.info(util.c("FAC: GEN_RATE process mode initial ramp started (initial target is ", output, " mB/t)")) + elseif self.waiting_on_ramp then + if all_units_ramped() then + self.waiting_on_ramp = false + self.waiting_on_stable = true + + self.time_start = now + + self.status_text = { "GENERATION MODE", "holding ramped rate" } + log.info("FAC: GEN_RATE process mode initial ramp completed, holding for stablization time") + end + elseif self.waiting_on_stable then + if (now - self.time_start) > FLOW_STABILITY_DELAY_S then + self.waiting_on_stable = false + + self.time_start = now + self.last_time = now + self.last_error = 0 + self.accumulator = 0 + + self.status_text = { "GENERATION MODE", "running control loop" } + log.info("FAC: GEN_RATE process mode initial hold completed, starting PID control") + end + elseif self.last_update < rate_update then + -- convert to MFE (in rounded kFE) to make constants not microscopic + local error = util.round((self.gen_rate_setpoint - avg_inflow) / 1000) / 1000 + + -- stop accumulator when saturated to avoid windup + if not self.saturated then + self.accumulator = self.accumulator + (error * (now - self.last_time)) + end + + -- local runtime = now - self.time_start + local integral = self.accumulator + local derivative = (error - self.last_error) / (now - self.last_time) + + local P = RATE_Kp * error + local I = RATE_Ki * integral + local D = RATE_Kd * derivative + + -- velocity (rate) (derivative of charge level => rate) feed forward + local FF = self.gen_rate_setpoint / self.charge_conversion + + local output = P + I + D + FF + + -- clamp at range -> output clamped (sp_c) + local out_c = math.max(0, math.min(output, self.max_burn_combined)) + + self.saturated = output ~= out_c + + -- log.debug(util.sprintf("GEN_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }", + -- runtime, avg_inflow, error, integral, output, out_c, P, I, D)) + + allocate_burn_rate(out_c, false) + + self.last_time = now + self.last_error = error + end + + self.last_update = rate_update + elseif self.mode == PROCESS.MATRIX_FAULT_IDLE then + -- exceeded charge, wait until condition clears + if self.ascram_reason == AUTO_SCRAM.NONE then + next_mode = self.return_mode + log.info("FAC: exiting matrix fault idle state due to fault resolution") + elseif self.ascram_reason == AUTO_SCRAM.CRIT_ALARM then + next_mode = PROCESS.SYSTEM_ALARM_IDLE + log.info("FAC: exiting matrix fault idle state due to critical unit alarm") + end + elseif self.mode == PROCESS.SYSTEM_ALARM_IDLE then + -- do nothing, wait for user to confirm (stop and reset) + elseif self.mode == PROCESS.GEN_RATE_FAULT_IDLE then + -- system faulted (degraded/not ready) while running generation rate mode + -- mode will need to be fully restarted once everything is OK to re-ramp to feed-forward + if self.units_ready then + log.info("FAC: system ready after faulting out of GEN_RATE process mode, switching back...") + next_mode = PROCESS.GEN_RATE + end + elseif self.mode ~= PROCESS.INACTIVE then + log.error(util.c("FAC: unsupported process mode ", self.mode, ", switching to inactive")) + next_mode = PROCESS.INACTIVE + end +end + +-- update automatic safety logic +function update.auto_safety() + local AUTO_SCRAM = self.types.AUTO_SCRAM + + local astatus = self.ascram_status + + if self.induction[1] ~= nil then + local db = self.induction[1].get_db() ---@type imatrix_session_db + + -- clear matrix disconnected + if astatus.matrix_dc then + astatus.matrix_dc = false + log.info("FAC: induction matrix reconnected, clearing ASCRAM condition") + end + + -- check matrix fill too high + local was_fill = astatus.matrix_fill + astatus.matrix_fill = (db.tanks.energy_fill >= ALARM_LIMS.CHARGE_HIGH) or (astatus.matrix_fill and db.tanks.energy_fill > ALARM_LIMS.CHARGE_RE_ENABLE) + + if was_fill and not astatus.matrix_fill then + log.info(util.c("FAC: charge state of induction matrix entered acceptable range <= ", ALARM_LIMS.CHARGE_RE_ENABLE * 100, "%")) + end + + -- check for critical unit alarms + astatus.crit_alarm = false + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + + if u.has_alarm_min_prio(PRIO.CRITICAL) then + astatus.crit_alarm = true + break + end + end + + -- check for facility radiation + if #self.envd > 0 then + local max_rad = 0 + + for i = 1, #self.envd do + local envd = self.envd[i] ---@type unit_session + local e_db = envd.get_db() ---@type envd_session_db + if e_db.radiation_raw > max_rad then max_rad = e_db.radiation_raw end + end + + astatus.radiation = max_rad >= ALARM_LIMS.FAC_HIGH_RAD + else + -- don't clear, if it is true then we lost it with high radiation, so just keep alarming + -- operator can restart the system or hit the stop/reset button + end + + -- system not ready, will need to restart GEN_RATE mode + -- clears when we enter the fault waiting state + astatus.gen_fault = self.mode == PROCESS.GEN_RATE and not self.units_ready + else + astatus.matrix_dc = true + end + + if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then + local scram = astatus.matrix_dc or astatus.matrix_fill or astatus.crit_alarm or astatus.gen_fault + + if scram and not self.ascram then + -- SCRAM all units + for i = 1, #self.prio_defs do + for _, u in pairs(self.prio_defs[i]) do + u.auto_scram() + end + end + + if astatus.crit_alarm then + -- highest priority alarm + next_mode = PROCESS.SYSTEM_ALARM_IDLE + self.ascram_reason = AUTO_SCRAM.CRIT_ALARM + self.status_text = { "AUTOMATIC SCRAM", "critical unit alarm tripped" } + + log.info("FAC: automatic SCRAM due to critical unit alarm") + log.warning("FAC: emergency exit of process control due to critical unit alarm") + elseif astatus.radiation then + next_mode = PROCESS.SYSTEM_ALARM_IDLE + self.ascram_reason = AUTO_SCRAM.RADIATION + self.status_text = { "AUTOMATIC SCRAM", "facility radiation high" } + + log.info("FAC: automatic SCRAM due to high facility radiation") + elseif astatus.matrix_dc then + next_mode = PROCESS.MATRIX_FAULT_IDLE + self.ascram_reason = AUTO_SCRAM.MATRIX_DC + self.status_text = { "AUTOMATIC SCRAM", "induction matrix disconnected" } + + if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then self.return_mode = self.mode end + + log.info("FAC: automatic SCRAM due to induction matrix disconnection") + elseif astatus.matrix_fill then + next_mode = PROCESS.MATRIX_FAULT_IDLE + self.ascram_reason = AUTO_SCRAM.MATRIX_FILL + self.status_text = { "AUTOMATIC SCRAM", "induction matrix fill high" } + + if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then self.return_mode = self.mode end + + log.info("FAC: automatic SCRAM due to induction matrix high charge") + elseif astatus.gen_fault then + -- lowest priority alarm + next_mode = PROCESS.GEN_RATE_FAULT_IDLE + self.ascram_reason = AUTO_SCRAM.GEN_FAULT + self.status_text = { "GENERATION MODE IDLE", "paused: system not ready" } + + log.info("FAC: automatic SCRAM due to unit problem while in GEN_RATE mode, will resume once all units are ready") + end + end + + self.ascram = scram + + if not self.ascram then + self.ascram_reason = AUTO_SCRAM.NONE + + -- reset PLC RPS trips if we should + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + u.auto_cond_rps_reset() + end + end + end +end + +-- update last mode and set next mode +function update.post_auto() + self.last_mode = self.mode + self.mode = next_mode +end + +-- update alarm audio control +function update.alarm_audio() + local allow_test = self.allow_testing and self.test_tone_set + + local alarms = { false, false, false, false, false, false, false, false, false, false, false, false } + + -- reset tone states before re-evaluting + for i = 1, #self.tone_states do self.tone_states[i] = false end + + if allow_test then + alarms = self.test_alarm_states + else + -- check all alarms for all units + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + for id, alarm in pairs(u.get_alarms()) do + alarms[id] = alarms[id] or (alarm == ALARM_STATE.TRIPPED) + end + end + + if not self.test_tone_reset then + -- clear testing alarms if we aren't using them + for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end + end + end + + -- Evaluate Alarms -- + + -- containment breach is worst case CRITICAL alarm, this takes priority + if alarms[ALARM.ContainmentBreach] then + self.tone_states[TONE.T_1800Hz_Int_4Hz] = true + else + -- critical damage is highest priority CRITICAL level alarm + if alarms[ALARM.CriticalDamage] then + self.tone_states[TONE.T_660Hz_Int_125ms] = true + else + -- EMERGENCY level alarms + URGENT over temp + if alarms[ALARM.ReactorDamage] or alarms[ALARM.ReactorOverTemp] or alarms[ALARM.ReactorWasteLeak] then + self.tone_states[TONE.T_544Hz_440Hz_Alt] = true + -- URGENT level turbine trip + elseif alarms[ALARM.TurbineTrip] then + self.tone_states[TONE.T_745Hz_Int_1Hz] = true + -- URGENT level reactor lost + elseif alarms[ALARM.ReactorLost] then + self.tone_states[TONE.T_340Hz_Int_2Hz] = true + -- TIMELY level alarms + elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then + self.tone_states[TONE.T_800Hz_Int] = true + end + end + + -- check RPS transient URGENT level alarm + if alarms[ALARM.RPSTransient] then + self.tone_states[TONE.T_1000Hz_Int] = true + -- disable really painful audio combination + self.tone_states[TONE.T_340Hz_Int_2Hz] = false + end + end + + -- radiation is a big concern, always play this CRITICAL level alarm if active + if alarms[ALARM.ContainmentRadiation] then + self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true + -- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled + -- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one + if self.tone_states[TONE.T_1000Hz_Int] and alarms[ALARM.ReactorLost] then self.tone_states[TONE.T_340Hz_Int_2Hz] = true end + -- it sounds *really* bad if this is in conjunction with these other tones, so disable them + self.tone_states[TONE.T_745Hz_Int_1Hz] = false + self.tone_states[TONE.T_800Hz_Int] = false + self.tone_states[TONE.T_1000Hz_Int] = false + end + + -- add to tone states if testing is active + if allow_test then + for i = 1, #self.tone_states do + self.tone_states[i] = self.tone_states[i] or self.test_tone_states[i] + end + + self.test_tone_reset = false + else + if not self.test_tone_reset then + -- clear testing tones if we aren't using them + for i = 1, #self.test_tone_states do self.test_tone_states[i] = false end + end + + -- flag that tones were reset + self.test_tone_set = false + self.test_tone_reset = true + end +end + +-- update facility redstone +---@param ack_all function acknowledge all alarms +function update.redstone(ack_all) + if #self.redstone > 0 then + -- handle facility SCRAM + if self.io_ctl.digital_read(IO.F_SCRAM) then + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + u.cond_scram() + end + end + + -- handle facility ack + if self.io_ctl.digital_read(IO.F_ACK) then ack_all() end + + -- update facility alarm outputs + local has_prio_alarm, has_any_alarm = false, false + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + + if u.has_alarm_min_prio(PRIO.EMERGENCY) then + has_prio_alarm, has_any_alarm = true, true + break + elseif u.has_alarm_min_prio(PRIO.TIMELY) then + has_any_alarm = true + end + end + + self.io_ctl.digital_write(IO.F_ALARM, has_prio_alarm) + self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm) + + -- update induction matrix related outputs + if self.induction[1] ~= nil then + local db = self.induction[1].get_db() ---@type imatrix_session_db + + self.io_ctl.digital_write(IO.F_MATRIX_LOW, db.tanks.energy_fill < const.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) + self.io_ctl.digital_write(IO.F_MATRIX_HIGH, db.tanks.energy_fill > const.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) + self.io_ctl.analog_write(IO.F_MATRIX_CHG, db.tanks.energy_fill, 0, 1) + end + end +end + +-- update unit tasks +---@param cooling_conf sv_cooling_conf cooling configuration +function update.unit_mgmt(cooling_conf) + local insufficent_po_rate = false + local need_emcool = false + + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + + -- update auto waste processing + if u.get_control_inf().waste_mode == WASTE_MODE.AUTO then + if (u.get_sna_rate() * 10.0) < u.get_burn_rate() then + insufficent_po_rate = true + end + end + + -- check if unit activated emergency coolant & uses facility tanks + if (cooling_conf.fac_tank_mode > 0) and u.is_emer_cool_tripped() and (cooling_conf.fac_tank_defs[i] == 2) then + need_emcool = true + end + end + + -- update waste product + + self.current_waste_product = self.waste_product + + if (not self.sps_low_power) and (self.waste_product == WASTE.ANTI_MATTER) and (self.induction[1] ~= nil) then + local db = self.induction[1].get_db() ---@type imatrix_session_db + + if db.tanks.energy_fill >= 0.15 then + self.disabled_sps = false + elseif self.disabled_sps or ((db.tanks.last_update > 0) and (db.tanks.energy_fill < 0.1)) then + self.disabled_sps = true + self.current_waste_product = WASTE.POLONIUM + end + else + self.disabled_sps = false + end + + if self.pu_fallback and insufficent_po_rate then + self.current_waste_product = WASTE.PLUTONIUM + end + + -- make sure dynamic tanks are allowing outflow if required + -- set all, rather than trying to determine which is for which (simpler & safer) + -- there should be no need for any to be in fill only mode + if need_emcool then + for i = 1, #self.tanks do + local session = self.tanks[i] ---@type unit_session + local tank = session.get_db() ---@type dynamicv_session_db + + if tank.state.container_mode == CONTAINER_MODE.FILL then + session.get_cmd_queue().push_data(DTV_RTU_S_DATA.SET_CONT_MODE, CONTAINER_MODE.BOTH) + end + end + end +end + +--#endregion + +---@param _self _facility_self +return function (_self) + self = _self + return update +end From f259f85a9981f30afa43c6948509a0e300c9d5ae Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 18 Aug 2024 19:12:13 -0400 Subject: [PATCH 07/19] fixed wrong function name --- supervisor/facility.lua | 2 +- supervisor/facility_update.lua | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 3ef7eb5..f418c69 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -212,7 +212,7 @@ function facility.new(config, cooling_conf, check_rtu_id) f_update.unit_mgmt(cooling_conf) -- update alarm tones - f_update.update_alarms() + f_update.alarm_audio() end -- call the update function of all units in the facility
diff --git a/supervisor/facility_update.lua b/supervisor/facility_update.lua index 94255b2..b26c722 100644 --- a/supervisor/facility_update.lua +++ b/supervisor/facility_update.lua @@ -43,6 +43,7 @@ local next_mode = 0 local charge_update = 0 local rate_update = 0 +---@class facility_update_extension local update = {} --#region PRIVATE FUNCTIONS From 072613959cddb096437d5e3d93be4affe4aa90d2 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 18 Aug 2024 23:04:07 -0400 Subject: [PATCH 08/19] facility tank list generation on supervisor --- supervisor/facility.lua | 94 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/supervisor/facility.lua b/supervisor/facility.lua index f418c69..65c6b09 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -45,6 +45,9 @@ function facility.new(config, cooling_conf, check_rtu_id) status_text = { "START UP", "initializing..." }, all_sys_ok = false, allow_testing = false, + -- facility tanks + tank_defs = cooling_conf.fac_tank_defs, + tank_list = {}, -- rtus rtu_conn_count = 0, rtu_list = {}, @@ -138,6 +141,97 @@ function facility.new(config, cooling_conf, check_rtu_id) table.insert(self.test_tone_states, false) end + --#region decode tank configuration + + -- determine tank information + if cooling_conf.fac_tank_mode == 0 then + self.tank_defs = {} + + -- on facility tank mode 0, setup tank defs to match unit tank option + for i = 1, config.UnitCount do + self.tank_defs[i] = util.trinary(cooling_conf.r_cool[i].TankConnection, 1, 0) + end + + self.tank_list = { table.unpack(self.tank_defs) } + else + -- decode the layout of tanks from the connections definitions + local tank_mode = cooling_conf.fac_tank_mode + local tank_defs = self.tank_defs + local tank_list = { table.unpack(tank_defs) } + + local function calc_fdef(start_idx, end_idx) + local first = 4 + for i = start_idx, end_idx do + if self.tank_defs[i] == 2 then + if i < first then first = i end + end + end + return first + end + + if tank_mode == 1 then + -- (1) 1 total facility tank (A A A A) + local first_fdef = calc_fdef(1, #tank_defs) + for i = 1, #tank_defs do + if i > first_fdef and tank_defs[i] == 2 then + tank_list[i] = 0 + end + end + elseif tank_mode == 2 then + -- (2) 2 total facility tanks (A A A B) + local first_fdef = calc_fdef(1, math.min(3, #tank_defs)) + for i = 1, #tank_defs do + if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then + tank_list[i] = 0 + end + end + elseif tank_mode == 3 then + -- (3) 2 total facility tanks (A A B B) + for _, a in pairs({ 1, 3 }) do + local b = a + 1 + if (tank_defs[a] == 2) and (tank_defs[b] == 2) then + tank_list[b] = 0 + end + end + elseif tank_mode == 4 then + -- (4) 2 total facility tanks (A B B B) + local first_fdef = calc_fdef(2, #tank_defs) + for i = 1, #tank_defs do + if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then + tank_list[i] = 0 + end + end + elseif tank_mode == 5 then + -- (5) 3 total facility tanks (A A B C) + local first_fdef = calc_fdef(1, math.min(2, #tank_defs)) + for i = 1, #tank_defs do + if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then + tank_list[i] = 0 + end + end + elseif tank_mode == 6 then + -- (6) 3 total facility tanks (A B B C) + local first_fdef = calc_fdef(2, math.min(3, #tank_defs)) + for i = 1, #tank_defs do + if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then + tank_list[i] = 0 + end + end + elseif tank_mode == 7 then + -- (7) 3 total facility tanks (A B C C) + local first_fdef = calc_fdef(3, #tank_defs) + for i = 1, #tank_defs do + if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then + tank_list[i] = 0 + end + end + end + + self.tank_list = tank_list + end + + --#endregion + -- PUBLIC FUNCTIONS -- ---@class facility From 47756392453309cdbcdd3ac9df49cfdba58f992e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 18 Aug 2024 23:04:44 -0400 Subject: [PATCH 09/19] #367 WIP listing ID check failures and missing devices --- supervisor/panel/components/chk_entry.lua | 26 +++++++++++++++++++---- supervisor/panel/pgi.lua | 5 +++-- supervisor/session/svsessions.lua | 21 +++++++++++++++--- supervisor/unit.lua | 23 ++++++++++++++++++++ 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/supervisor/panel/components/chk_entry.lua b/supervisor/panel/components/chk_entry.lua index 5fa6d3b..0b893a5 100644 --- a/supervisor/panel/components/chk_entry.lua +++ b/supervisor/panel/components/chk_entry.lua @@ -19,7 +19,7 @@ local cpair = core.cpair ---@param parent graphics_element parent ---@param unit unit_session RTU session ---@param fail_code integer failure code -local function init(parent, unit, fail_code) +local function init(parent, unit, fail_code, cmp_id) local s_hi_box = style.theme.highlight_box local label_fg = style.fp.label_fg @@ -28,9 +28,27 @@ local function init(parent, unit, fail_code) local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright} - TextBox{parent=entry,x=1,y=1,text="",width=8,fg_bg=s_hi_box} - local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)} - TextBox{parent=entry,x=1,y=3,text="",width=8,fg_bg=s_hi_box} + if fail_code == 1 then + TextBox{parent=entry,y=1,text="",width=11,fg_bg=cpair(colors.black,colors.orange)} + TextBox{parent=entry,text="BAD INDEX",alignment=ALIGN.CENTER,width=11,nav_active=cpair(colors.black,colors.orange)} + TextBox{parent=entry,text="",width=11,fg_bg=cpair(colors.black,colors.orange)} + elseif fail_code == 2 then + TextBox{parent=entry,y=1,text="",width=11,fg_bg=cpair(colors.black,colors.red)} + TextBox{parent=entry,text="DUPLICATE",alignment=ALIGN.CENTER,width=11,nav_active=cpair(colors.black,colors.red)} + TextBox{parent=entry,text="",width=11,fg_bg=cpair(colors.black,colors.red)} + elseif fail_code == 4 then + TextBox{parent=entry,y=1,text="",width=11,fg_bg=cpair(colors.black,colors.yellow)} + TextBox{parent=entry,text="MISSING",alignment=ALIGN.CENTER,width=11,nav_active=cpair(colors.black,colors.yellow)} + TextBox{parent=entry,text="",width=11,fg_bg=cpair(colors.black,colors.yellow)} + end + + if fail_code ~= 4 and cmp_id then + local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)} + end + + if fail_code ~= 4 and cmp_id then + local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)} + end TextBox{parent=entry,x=21,y=2,text="FW:",width=3} local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,fg_bg=label_fg} diff --git a/supervisor/panel/pgi.lua b/supervisor/panel/pgi.lua index 1e46fee..c9776b5 100644 --- a/supervisor/panel/pgi.lua +++ b/supervisor/panel/pgi.lua @@ -111,13 +111,14 @@ end -- add a device ID check failure entry to the CHK list ---@param unit unit_session RTU session ---@param fail_code integer failure code -function pgi.create_chk_entry(unit, fail_code) +---@param cmp_id integer|nil computer ID if this isn't a 'missing' entry +function pgi.create_chk_entry(unit, fail_code, cmp_id) local gw_session = unit.get_session_id() if data.chk_list ~= nil and data.chk_entry ~= nil then if not data.entries.chk[gw_session] then data.entries.chk[gw_session] = {} end - local success, result = pcall(data.chk_entry, data.chk_list, unit, fail_code) + local success, result = pcall(data.chk_entry, data.chk_list, unit, fail_code, cmd_id) if success then data.entries.chk[gw_session][unit.get_unit_id()] = result diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 0f3fece..3368451 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -4,7 +4,8 @@ local util = require("scada-common.util") local databus = require("supervisor.databus") local facility = require("supervisor.facility") -local pgi = require("supervisor.pgi") + +local pgi = require("supervisor.panel.pgi") local coordinator = require("supervisor.session.coordinator") local plc = require("supervisor.session.plc") @@ -39,7 +40,7 @@ local self = { facility = nil, ---@type facility|nil sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} }, next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 }, - dev_dbg = { duplicate = {}, out_of_range = {} } + dev_dbg = { duplicate = {}, out_of_range = {}, connected = {} } } ---@alias sv_session_structs plc_session_struct|rtu_session_struct|crd_session_struct|pdg_session_struct @@ -197,6 +198,14 @@ local function _update_dev_dbg() util.filter_table(self.dev_dbg.duplicate, f, pgi.delete_chk_entry) util.filter_table(self.dev_dbg.out_of_range, f, pgi.delete_chk_entry) + + local conns = self.dev_dbg.connected + local units = self.facility.get_units() + for i = 1, #units do + local unit = units[i] ---@type reactor_unit + local rtus = unit.check_rtu_conns() + + end end -- SHARED FUNCTIONS -- @@ -229,7 +238,7 @@ local function check_rtu_id(unit, list, max) end -- add to the list for the user - if fail_code > 0 then + if fail_code > 0 and fail_code ~= 3 then local cmp_id for i = 1, #self.sessions.rtu do @@ -257,6 +266,12 @@ function svsessions.init(nic, fp_ok, config, cooling_conf) self.fp_ok = fp_ok self.config = config self.facility = facility.new(config, cooling_conf, check_rtu_id) + + -- initialize connection tracking table + self.dev_dbg.connected = { imatrix = nil, sps = nil, tanks = {}, units = {} } + for i = 1, config.UnitCount do + self.dev_dbg.connected.units[i] = { boilers = {}, turbines = {}, tanks = {} } + end end -- find an RTU session by the computer ID diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 31e24d3..6f39cb6 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -912,6 +912,29 @@ function unit.new(reactor_id, num_boilers, num_turbines, check_rtu_id, ext_idle) return rate or 0 end + -- check which RTUs are connected + ---@nodiscard + function public.check_rtu_conns() + local conns = {} + + conns.boilers = {} + for i = 1, #self.boilers do + conns.boilers[self.boilers[i].get_device_idx()] = true + end + + conns.turbines = {} + for i = 1, #self.turbines do + conns.turbines[self.turbines[i].get_device_idx()] = true + end + + conns.tanks = {} + for i = 1, #self.tanks do + conns.tanks[self.tanks[i].get_device_idx()] = true + end + + return conns + end + -- get RTU statuses ---@nodiscard function public.get_rtu_statuses() From fc7441b2f6f2cccc463df14b3a8c84e61b30ea03 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 20 Aug 2024 21:32:54 -0400 Subject: [PATCH 10/19] #367 reworked ownership of tank data and facility instance to make more sense --- supervisor/facility.lua | 47 ++++++++++++++++++------------- supervisor/facility_update.lua | 5 ++-- supervisor/session/svsessions.lua | 19 +++++++------ supervisor/startup.lua | 6 +++- supervisor/supervisor.lua | 12 ++++---- supervisor/unit.lua | 26 ++++++++--------- 6 files changed, 63 insertions(+), 52 deletions(-) diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 65c6b09..a205dfc 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -2,11 +2,11 @@ local log = require("scada-common.log") local types = require("scada-common.types") local util = require("scada-common.util") +local unit = require("supervisor.unit") local fac_update = require("supervisor.facility_update") -local unit = require("supervisor.unit") - local rsctl = require("supervisor.session.rsctl") +local svsessions = require("supervisor.session.svsessions") local PROCESS = types.PROCESS local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE @@ -35,9 +35,7 @@ local facility = {} -- create a new facility management object ---@nodiscard ---@param config svr_config supervisor configuration ----@param cooling_conf sv_cooling_conf cooling configurations of reactor units ----@param check_rtu_id function ID checking function for RTUs attempting to be linked -function facility.new(config, cooling_conf, check_rtu_id) +function facility.new(config) ---@class _facility_self local self = { units = {}, @@ -46,8 +44,13 @@ function facility.new(config, cooling_conf, check_rtu_id) all_sys_ok = false, allow_testing = false, -- facility tanks - tank_defs = cooling_conf.fac_tank_defs, - tank_list = {}, + ---@class sv_cooling_conf + cooling_conf = { + r_cool = config.CoolingConfig, + fac_tank_mode = config.FacilityTankMode, + fac_tank_defs = config.FacilityTankDefs, + fac_tank_list = {} + }, -- rtus rtu_conn_count = 0, rtu_list = {}, @@ -124,7 +127,8 @@ function facility.new(config, cooling_conf, check_rtu_id) -- create units for i = 1, config.UnitCount do - table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BoilerCount, cooling_conf.r_cool[i].TurbineCount, check_rtu_id, config.ExtChargeIdling)) + table.insert(self.units, + unit.new(i, self.cooling_conf.r_cool[i].BoilerCount, self.cooling_conf.r_cool[i].TurbineCount, config.ExtChargeIdling)) table.insert(self.group_map, 0) end @@ -143,26 +147,28 @@ function facility.new(config, cooling_conf, check_rtu_id) --#region decode tank configuration + local cool_conf = self.cooling_conf + -- determine tank information - if cooling_conf.fac_tank_mode == 0 then - self.tank_defs = {} + if cool_conf.fac_tank_mode == 0 then + cool_conf.tank_defs = {} -- on facility tank mode 0, setup tank defs to match unit tank option for i = 1, config.UnitCount do - self.tank_defs[i] = util.trinary(cooling_conf.r_cool[i].TankConnection, 1, 0) + cool_conf.tank_defs[i] = util.trinary(cool_conf.r_cool[i].TankConnection, 1, 0) end - self.tank_list = { table.unpack(self.tank_defs) } + cool_conf.tank_list = { table.unpack(cool_conf.tank_defs) } else -- decode the layout of tanks from the connections definitions - local tank_mode = cooling_conf.fac_tank_mode - local tank_defs = self.tank_defs + local tank_mode = cool_conf.fac_tank_mode + local tank_defs = cool_conf.fac_tank_defs local tank_list = { table.unpack(tank_defs) } local function calc_fdef(start_idx, end_idx) local first = 4 for i = start_idx, end_idx do - if self.tank_defs[i] == 2 then + if tank_defs[i] == 2 then if i < first then first = i end end end @@ -227,7 +233,7 @@ function facility.new(config, cooling_conf, check_rtu_id) end end - self.tank_list = tank_list + cool_conf.fac_tank_list = tank_list end --#endregion @@ -247,7 +253,7 @@ function facility.new(config, cooling_conf, check_rtu_id) ---@param imatrix unit_session ---@return boolean linked induction matrix accepted (max 1) function public.add_imatrix(imatrix) - local fail_code, fail_str = check_rtu_id(imatrix, self.induction, 1) + local fail_code, fail_str = svsessions.check_rtu_id(imatrix, self.induction, 1) if fail_code == 0 then table.insert(self.induction, imatrix) @@ -262,7 +268,7 @@ function facility.new(config, cooling_conf, check_rtu_id) ---@param sps unit_session ---@return boolean linked SPS accepted (max 1) function public.add_sps(sps) - local fail_code, fail_str = check_rtu_id(sps, self.sps, 1) + local fail_code, fail_str = svsessions.check_rtu_id(sps, self.sps, 1) if fail_code == 0 then table.insert(self.sps, sps) @@ -303,7 +309,7 @@ function facility.new(config, cooling_conf, check_rtu_id) f_update.redstone(public.ack_all) -- unit tasks - f_update.unit_mgmt(cooling_conf) + f_update.unit_mgmt() -- update alarm tones f_update.alarm_audio() @@ -631,6 +637,9 @@ function facility.new(config, cooling_conf, check_rtu_id) ---@param rtu_sessions table session list of all connected RTUs function public.report_rtus(rtu_sessions) self.rtu_conn_count = #rtu_sessions end + -- get the facility cooling configuration + function public.get_cooling_conf() return self.cooling_conf end + -- get the units in this facility ---@nodiscard function public.get_units() return self.units end diff --git a/supervisor/facility_update.lua b/supervisor/facility_update.lua index b26c722..2927023 100644 --- a/supervisor/facility_update.lua +++ b/supervisor/facility_update.lua @@ -766,8 +766,7 @@ function update.redstone(ack_all) end -- update unit tasks ----@param cooling_conf sv_cooling_conf cooling configuration -function update.unit_mgmt(cooling_conf) +function update.unit_mgmt() local insufficent_po_rate = false local need_emcool = false @@ -782,7 +781,7 @@ function update.unit_mgmt(cooling_conf) end -- check if unit activated emergency coolant & uses facility tanks - if (cooling_conf.fac_tank_mode > 0) and u.is_emer_cool_tripped() and (cooling_conf.fac_tank_defs[i] == 2) then + if (self.cooling_conf.fac_tank_mode > 0) and u.is_emer_cool_tripped() and (self.cooling_conf.fac_tank_defs[i] == 2) then need_emcool = true end end diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 3368451..904a29a 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -3,7 +3,6 @@ local mqueue = require("scada-common.mqueue") local util = require("scada-common.util") local databus = require("supervisor.databus") -local facility = require("supervisor.facility") local pgi = require("supervisor.panel.pgi") @@ -45,7 +44,7 @@ local self = { ---@alias sv_session_structs plc_session_struct|rtu_session_struct|crd_session_struct|pdg_session_struct --- PRIVATE FUNCTIONS -- +--#region PRIVATE FUNCTIONS -- handle a session output queue ---@param session sv_session_structs @@ -208,13 +207,15 @@ local function _update_dev_dbg() end end --- SHARED FUNCTIONS -- +--#endregion + +--#region PUBLIC FUNCTIONS ---@param unit unit_session RTU session ---@param list table table of RTU sessions ---@param max integer max of this type of RTU ---@return 0|1|2|3 fail_code, string fail_str 0 = success, 1 = out-of-range, 2 = duplicate, 3 = exceeded table max -local function check_rtu_id(unit, list, max) +function svsessions.check_rtu_id(unit, list, max) local fail_code, fail_str = 0, "OK" if (unit.get_device_idx() < 1 and max ~= 1) or unit.get_device_idx() > max then @@ -254,18 +255,16 @@ local function check_rtu_id(unit, list, max) return fail_code, fail_str end --- PUBLIC FUNCTIONS -- - -- initialize svsessions ---@param nic nic network interface device ---@param fp_ok boolean front panel active ---@param config svr_config supervisor configuration ----@param cooling_conf sv_cooling_conf cooling configuration definition -function svsessions.init(nic, fp_ok, config, cooling_conf) +---@param facility facility +function svsessions.init(nic, fp_ok, config, facility) self.nic = nic self.fp_ok = fp_ok self.config = config - self.facility = facility.new(config, cooling_conf, check_rtu_id) + self.facility = facility -- initialize connection tracking table self.dev_dbg.connected = { imatrix = nil, sps = nil, tanks = {}, units = {} } @@ -554,4 +553,6 @@ function svsessions.close_all() svsessions.free_all_closed() end +--#endregion + return svsessions diff --git a/supervisor/startup.lua b/supervisor/startup.lua index a89a777..9a567ac 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -16,6 +16,7 @@ local core = require("graphics.core") local configure = require("supervisor.configure") local databus = require("supervisor.databus") +local facility = require("supervisor.facility") local renderer = require("supervisor.renderer") local supervisor = require("supervisor.supervisor") @@ -129,9 +130,12 @@ local function main() println_ts = function (_) end end + -- create facility and unit objects + local sv_facility = facility.new(config) + -- create network interface then setup comms local nic = network.nic(modem) - local superv_comms = supervisor.comms(SUPERVISOR_VERSION, nic, fp_ok) + local superv_comms = supervisor.comms(SUPERVISOR_VERSION, nic, fp_ok, sv_facility) -- base loop clock (6.67Hz, 3 ticks) local MAIN_CLOCK = 0.15 diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 69a98f5..8b06d49 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -102,14 +102,12 @@ end ---@param _version string supervisor version ---@param nic nic network interface device ---@param fp_ok boolean if the front panel UI is running +---@param facility facility facility instance ---@diagnostic disable-next-line: unused-local -function supervisor.comms(_version, nic, fp_ok) +function supervisor.comms(_version, nic, fp_ok, facility) -- 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 - ---@class sv_cooling_conf - local cooling_conf = { r_cool = config.CoolingConfig, fac_tank_mode = config.FacilityTankMode, fac_tank_defs = config.FacilityTankDefs } - local self = { last_est_acks = {} } @@ -122,8 +120,8 @@ function supervisor.comms(_version, nic, fp_ok) nic.closeAll() nic.open(config.SVR_Channel) - -- pass modem, status, and config data to svsessions - svsessions.init(nic, fp_ok, config, cooling_conf) + -- pass system data and objects to svsessions + svsessions.init(nic, fp_ok, config, facility) -- send an establish request response ---@param packet scada_packet @@ -373,7 +371,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, { config.UnitCount, cooling_conf }) + _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, facility.get_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") diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 6f39cb6..3578926 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -1,12 +1,13 @@ -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 log = require("scada-common.log") +local rsio = require("scada-common.rsio") +local types = require("scada-common.types") +local util = require("scada-common.util") -local logic = require("supervisor.unitlogic") +local logic = require("supervisor.unitlogic") -local plc = require("supervisor.session.plc") -local rsctl = require("supervisor.session.rsctl") +local plc = require("supervisor.session.plc") +local rsctl = require("supervisor.session.rsctl") +local svsessions = require("supervisor.session.svsessions") local WASTE_MODE = types.WASTE_MODE local WASTE = types.WASTE_PRODUCT @@ -63,9 +64,8 @@ local unit = {} ---@param reactor_id integer reactor unit number ---@param num_boilers integer number of boilers expected ---@param num_turbines integer number of turbines expected ----@param check_rtu_id function ID checking function for RTUs attempting to be linked ---@param ext_idle boolean extended idling mode -function unit.new(reactor_id, num_boilers, num_turbines, check_rtu_id, ext_idle) +function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) -- time (ms) to idle for auto idling local IDLE_TIME = util.trinary(ext_idle, 60000, 10000) @@ -444,7 +444,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, check_rtu_id, ext_idle) ---@param turbine unit_session ---@return boolean linked turbine accepted to associated device slot function public.add_turbine(turbine) - local fail_code, fail_str = check_rtu_id(turbine, self.turbines, num_turbines) + local fail_code, fail_str = svsessions.check_rtu_id(turbine, self.turbines, num_turbines) if fail_code == 0 then table.insert(self.turbines, turbine) @@ -463,7 +463,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, check_rtu_id, ext_idle) ---@param boiler unit_session ---@return boolean linked boiler accepted to associated device slot function public.add_boiler(boiler) - local fail_code, fail_str = check_rtu_id(boiler, self.boilers, num_boilers) + local fail_code, fail_str = svsessions.check_rtu_id(boiler, self.boilers, num_boilers) if fail_code == 0 then table.insert(self.boilers, boiler) @@ -484,7 +484,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, check_rtu_id, ext_idle) ---@param dynamic_tank unit_session ---@return boolean linked dynamic tank accepted (max 1) function public.add_tank(dynamic_tank) - local fail_code, fail_str = check_rtu_id(dynamic_tank, self.tanks, 1) + local fail_code, fail_str = svsessions.check_rtu_id(dynamic_tank, self.tanks, 1) if fail_code == 0 then table.insert(self.tanks, dynamic_tank) @@ -503,7 +503,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, check_rtu_id, ext_idle) ---@param envd unit_session ---@return boolean linked environment detector accepted (max 1) function public.add_envd(envd) - local fail_code, fail_str = check_rtu_id(envd, self.envd, 99) + local fail_code, fail_str = svsessions.check_rtu_id(envd, self.envd, 99) if fail_code == 0 then table.insert(self.envd, envd) From 45d4b4e653b175e424b80dfe890efb6639d24ed1 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 20 Aug 2024 21:35:05 -0400 Subject: [PATCH 11/19] fixed PLC status retry packet type --- supervisor/session/plc.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 4aad6d3..301e9b3 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -802,7 +802,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue, if not self.received_status_cache then if rtimes.status_req - util.time() <= 0 then - _send(RPLC_TYPE.MEK_STATUS, {}) + _send(RPLC_TYPE.STATUS, {}) rtimes.status_req = util.time() + RETRY_PERIOD end end From 465875b28726dca4fb79e8cb8d514822e5f45143 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 20 Aug 2024 22:28:41 -0400 Subject: [PATCH 12/19] coordinator receives tank list from supervisor --- coordinator/iocontrol.lua | 87 +-------------------------------------- coordinator/startup.lua | 2 +- supervisor/facility.lua | 6 +-- 3 files changed, 5 insertions(+), 90 deletions(-) diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index e8102a4..7444ef7 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -89,6 +89,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) num_units = conf.num_units, tank_mode = conf.cooling.fac_tank_mode, tank_defs = conf.cooling.fac_tank_defs, + tank_list = conf.cooling.fac_tank_list, all_sys_ok = false, rtu_count = 0, @@ -143,92 +144,6 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) table.insert(io.facility.sps_ps_tbl, psil.create()) table.insert(io.facility.sps_data_tbl, {}) - -- 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 - for i = 1, conf.num_units do - 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) } - else - -- decode the layout of tanks from the connections definitions - local tank_mode = io.facility.tank_mode - local tank_defs = io.facility.tank_defs - local tank_list = { table.unpack(tank_defs) } - - local function calc_fdef(start_idx, end_idx) - local first = 4 - for i = start_idx, end_idx do - if io.facility.tank_defs[i] == 2 then - if i < first then first = i end - end - end - return first - end - - if tank_mode == 1 then - -- (1) 1 total facility tank (A A A A) - local first_fdef = calc_fdef(1, #tank_defs) - for i = 1, #tank_defs do - if i > first_fdef and tank_defs[i] == 2 then - tank_list[i] = 0 - end - end - elseif tank_mode == 2 then - -- (2) 2 total facility tanks (A A A B) - local first_fdef = calc_fdef(1, math.min(3, #tank_defs)) - for i = 1, #tank_defs do - if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then - tank_list[i] = 0 - end - end - elseif tank_mode == 3 then - -- (3) 2 total facility tanks (A A B B) - for _, a in pairs({ 1, 3 }) do - local b = a + 1 - if (tank_defs[a] == 2) and (tank_defs[b] == 2) then - tank_list[b] = 0 - end - end - elseif tank_mode == 4 then - -- (4) 2 total facility tanks (A B B B) - local first_fdef = calc_fdef(2, #tank_defs) - for i = 1, #tank_defs do - if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then - tank_list[i] = 0 - end - end - elseif tank_mode == 5 then - -- (5) 3 total facility tanks (A A B C) - local first_fdef = calc_fdef(1, math.min(2, #tank_defs)) - for i = 1, #tank_defs do - if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then - tank_list[i] = 0 - end - end - elseif tank_mode == 6 then - -- (6) 3 total facility tanks (A B B C) - local first_fdef = calc_fdef(2, math.min(3, #tank_defs)) - for i = 1, #tank_defs do - if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then - tank_list[i] = 0 - end - end - elseif tank_mode == 7 then - -- (7) 3 total facility tanks (A B C C) - local first_fdef = calc_fdef(3, #tank_defs) - for i = 1, #tank_defs do - if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then - tank_list[i] = 0 - end - end - end - - io.facility.tank_list = tank_list - end - -- create facility tank tables for i = 1, #io.facility.tank_list do if io.facility.tank_list[i] == 2 then diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 15a9999..62d1511 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") local threads = require("coordinator.threads") -local COORDINATOR_VERSION = "v1.5.2" +local COORDINATOR_VERSION = "v1.5.3" local CHUNK_LOAD_DELAY_S = 30.0 diff --git a/supervisor/facility.lua b/supervisor/facility.lua index a205dfc..87d403d 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -151,14 +151,14 @@ function facility.new(config) -- determine tank information if cool_conf.fac_tank_mode == 0 then - cool_conf.tank_defs = {} + cool_conf.fac_tank_defs = {} -- on facility tank mode 0, setup tank defs to match unit tank option for i = 1, config.UnitCount do - cool_conf.tank_defs[i] = util.trinary(cool_conf.r_cool[i].TankConnection, 1, 0) + cool_conf.fac_tank_defs[i] = util.trinary(cool_conf.r_cool[i].TankConnection, 1, 0) end - cool_conf.tank_list = { table.unpack(cool_conf.tank_defs) } + cool_conf.fac_tank_list = { table.unpack(cool_conf.fac_tank_defs) } else -- decode the layout of tanks from the connections definitions local tank_mode = cool_conf.fac_tank_mode From 12f187f59655ecdba05fe90ce5e5fb3c6ccde21d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 21 Aug 2024 21:23:16 +0000 Subject: [PATCH 13/19] #367 logic for missing device detection and user-friendly messages --- supervisor/facility.lua | 29 ++++- supervisor/panel/components/chk_entry.lua | 28 ++--- supervisor/panel/pgi.lua | 41 ++++++- supervisor/session/rtu/unit_session.lua | 3 + supervisor/session/svsessions.lua | 138 ++++++++++++++++++++-- supervisor/unit.lua | 2 +- 6 files changed, 206 insertions(+), 35 deletions(-) diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 87d403d..503d454 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -285,7 +285,18 @@ function facility.new(config) -- link an environment detector RTU session ---@param envd unit_session - function public.add_envd(envd) table.insert(self.envd, envd) end + ---@return boolean linked environment detector accepted + function public.add_envd(envd) + local fail_code, fail_str = svsessions.check_rtu_id(envd, self.envd, 99) + + if fail_code == 0 then + table.insert(self.envd, envd) + else + log.warning(util.c("FAC: rejected environment detector linking due to failure code ", fail_code, " (", fail_str, ")")) + end + + return fail_code == 0 + end -- purge devices associated with the given RTU session ID ---@param session integer RTU session ID @@ -575,6 +586,22 @@ function facility.new(config) } end + -- check which RTUs are connected + ---@nodiscard + function public.check_rtu_conns() + local conns = {} + + conns.induction = #self.induction > 0 + conns.sps = #self.sps > 0 + + conns.tanks = {} + for i = 1, #self.tanks do + conns.tanks[self.tanks[i].get_device_idx()] = true + end + + return conns + end + -- get RTU statuses ---@nodiscard function public.get_rtu_statuses() diff --git a/supervisor/panel/components/chk_entry.lua b/supervisor/panel/components/chk_entry.lua index 0b893a5..63b4f40 100644 --- a/supervisor/panel/components/chk_entry.lua +++ b/supervisor/panel/components/chk_entry.lua @@ -2,14 +2,12 @@ -- RTU ID Check Failure Entry -- -local databus = require("supervisor.databus") +local style = require("supervisor.panel.style") -local style = require("supervisor.panel.style") +local core = require("graphics.core") -local core = require("graphics.core") - -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.div") +local TextBox = require("graphics.elements.textbox") local ALIGN = core.ALIGN @@ -17,9 +15,9 @@ local cpair = core.cpair -- create an ID check list entry ---@param parent graphics_element parent ----@param unit unit_session RTU session +---@param msg string message ---@param fail_code integer failure code -local function init(parent, unit, fail_code, cmp_id) +local function init(parent, msg, fail_code, cmp_id) local s_hi_box = style.theme.highlight_box local label_fg = style.fp.label_fg @@ -42,17 +40,13 @@ local function init(parent, unit, fail_code, cmp_id) TextBox{parent=entry,text="",width=11,fg_bg=cpair(colors.black,colors.yellow)} end - if fail_code ~= 4 and cmp_id then - local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)} + if fail_code == 4 then + TextBox{parent=entry,x=13,y=2,text=msg} + else + TextBox{parent=entry,x=13,y=2,text="@ C "..cmp_id,alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)} + TextBox{parent=entry,x=21,y=2,text=msg} end - if fail_code ~= 4 and cmp_id then - local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)} - end - - TextBox{parent=entry,x=21,y=2,text="FW:",width=3} - local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,fg_bg=label_fg} - return root end diff --git a/supervisor/panel/pgi.lua b/supervisor/panel/pgi.lua index c9776b5..15cfd1c 100644 --- a/supervisor/panel/pgi.lua +++ b/supervisor/panel/pgi.lua @@ -15,7 +15,7 @@ local data = { pdg_entry = nil, ---@type function chk_entry = nil, ---@type function -- list entries - entries = { rtu = {}, pdg = {}, chk = {} } + entries = { rtu = {}, pdg = {}, chk = {}, missing = {} } } -- link list boxes @@ -111,14 +111,15 @@ end -- add a device ID check failure entry to the CHK list ---@param unit unit_session RTU session ---@param fail_code integer failure code ----@param cmp_id integer|nil computer ID if this isn't a 'missing' entry -function pgi.create_chk_entry(unit, fail_code, cmp_id) +---@param cmp_id integer computer ID +---@param msg string description to show the user +function pgi.create_chk_entry(unit, fail_code, cmp_id, msg) local gw_session = unit.get_session_id() if data.chk_list ~= nil and data.chk_entry ~= nil then if not data.entries.chk[gw_session] then data.entries.chk[gw_session] = {} end - local success, result = pcall(data.chk_entry, data.chk_list, unit, fail_code, cmd_id) + local success, result = pcall(data.chk_entry, data.chk_list, msg, fail_code, cmp_id) if success then data.entries.chk[gw_session][unit.get_unit_id()] = result @@ -149,4 +150,36 @@ function pgi.delete_chk_entry(unit) end end +-- add a device ID missing entry to the CHK list +---@param message string missing device message +function pgi.create_missing_entry(message) + if data.chk_list ~= nil and data.chk_entry ~= nil then + local success, result = pcall(data.chk_entry, data.chk_list, message, 4, -1) + + if success then + data.entries.missing[message] = result + log.debug(util.c("PGI: created missing CHK entry (", message, ")")) + else + log.error(util.c("PGI: failed to create missing CHK entry (", result, ")"), true) + end + end +end + +-- delete a device ID missing entry from the CHK list +---@param message string missing device message +function pgi.delete_missing_entry(message) + if data.entries.missing[message] ~= nil then + local success, result = pcall(data.entries.missing[message].delete) + data.entries.missing[message] = nil + + if success then + log.debug(util.c("PGI: deleted missing CHK entry \"", message, "\"")) + else + log.error(util.c("PGI: failed to delete missing CHK entry (", result, ")"), true) + end + else + log.warning(util.c("PGI: tried to delete unknown missing CHK entry \"", message, "\"")) + end +end + return pgi diff --git a/supervisor/session/rtu/unit_session.lua b/supervisor/session/rtu/unit_session.lua index 18ac1c9..4f516c8 100644 --- a/supervisor/session/rtu/unit_session.lua +++ b/supervisor/session/rtu/unit_session.lua @@ -150,6 +150,9 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t -- get the unit ID ---@nodiscard function public.get_unit_id() return unit_id end + -- get the RTU type + ---@nodiscard + function public.get_unit_type() return advert.type end -- get the device index ---@nodiscard function public.get_device_idx() return self.device_index or 0 end diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 904a29a..caed09f 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -1,5 +1,10 @@ +-- +-- Supervisor Sessions Handler +-- + local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") +local types = require("scada-common.types") local util = require("scada-common.util") local databus = require("supervisor.databus") @@ -12,12 +17,13 @@ local pocket = require("supervisor.session.pocket") local rtu = require("supervisor.session.rtu") local svqtypes = require("supervisor.session.svqtypes") --- Supervisor Sessions Handler +local RTU_TYPES = types.RTU_UNIT_TYPE -local SV_Q_DATA = svqtypes.SV_Q_DATA +local SV_Q_DATA = svqtypes.SV_Q_DATA local PLC_S_CMDS = plc.PLC_S_CMDS local PLC_S_DATA = plc.PLC_S_DATA + local CRD_S_DATA = coordinator.CRD_S_DATA local svsessions = {} @@ -192,18 +198,71 @@ local function _find_session(list, s_addr) return nil end +-- periodically remove disconnected RTU gateway's RTU ID warnings and update the missing device list local function _update_dev_dbg() + -- remove disconnected units from check failures lists + local f = function (unit) return unit.is_connected() end util.filter_table(self.dev_dbg.duplicate, f, pgi.delete_chk_entry) util.filter_table(self.dev_dbg.out_of_range, f, pgi.delete_chk_entry) - local conns = self.dev_dbg.connected - local units = self.facility.get_units() - for i = 1, #units do - local unit = units[i] ---@type reactor_unit - local rtus = unit.check_rtu_conns() + -- update missing list + local conns = self.dev_dbg.connected + local units = self.facility.get_units() + local rtu_conns = self.facility.check_rtu_conns() + + local function report(disconnected, msg) + if disconnected then pgi.create_missing_entry(msg) else pgi.delete_missing_entry(msg) end + end + + -- look for disconnected facility RTUs + + if rtu_conns.induction ~= conns.induction then + report(conns.induction, util.c("the facility's induction matrix")) + conns.induction = rtu_conns.induction + end + + if rtu_conns.sps ~= conns.sps then + report(conns.sps, util.c("the facility's SPS")) + conns.sps = rtu_conns.sps + end + + for i = 1, #conns.tanks do + if (rtu_conns.tanks[i] or false) ~= conns.tanks[i] then + report(conns.tanks[i], util.c("the facility's #", i, " dynamic tank")) + conns.tanks[i] = rtu_conns.tanks[i] or false + end + end + + -- look for disconnected unit RTUs + + for u = 1, #units do + local u_conns = conns.units[u] + + rtu_conns = units[u].check_rtu_conns() + + for i = 1, #u_conns.boilers do + if (rtu_conns.boilers[i] or false) ~= u_conns.boilers[i] then + report(u_conns.boilers[i], util.c("unit ", u, "'s #", i, " boiler")) + u_conns.boilers[i] = rtu_conns.boilers[i] or false + end + end + + for i = 1, #u_conns.turbines do + if (rtu_conns.turbines[i] or false) ~= u_conns.turbines[i] then + report(u_conns.turbines[i], util.c("unit ", u, "'s #", i, " turbine")) + u_conns.turbines[i] = rtu_conns.turbines[i] or false + end + end + + for i = 1, #u_conns.tanks do + if (rtu_conns.tanks[i] or false) ~= u_conns.tanks[i] then + report(u_conns.tanks[i], util.c("unit ", u, "'s dynamic tank")) + u_conns.tanks[i] = rtu_conns.tanks[i] or false + end + end end end @@ -211,6 +270,7 @@ end --#region PUBLIC FUNCTIONS +-- on attempted link of an RTU to a facility or unit object, verify its ID and report a problem if it can't be accepted ---@param unit unit_session RTU session ---@param list table table of RTU sessions ---@param max integer max of this type of RTU @@ -240,7 +300,7 @@ function svsessions.check_rtu_id(unit, list, max) -- add to the list for the user if fail_code > 0 and fail_code ~= 3 then - local cmp_id + local cmp_id = -1 for i = 1, #self.sessions.rtu do if self.sessions.rtu[i].instance.get_id() == unit.get_session_id() then @@ -249,7 +309,42 @@ function svsessions.check_rtu_id(unit, list, max) end end - pgi.create_chk_entry(unit, fail_code, cmp_id) + local r_id = unit.get_reactor() + local idx = unit.get_device_idx() + local type = unit.get_unit_type() + local msg = "? (error)" + + if r_id == 0 then + msg = "the facility's " + + if type == RTU_TYPES.IMATRIX then + msg = msg .. "induction matrix" + elseif type == RTU_TYPES.SPS then + msg = msg .. "SPS" + elseif type == RTU_TYPES.DYNAMIC_VALVE then + msg = util.c(msg, "#", idx, " dynamic tank") + elseif type == RTU_TYPES.ENV_DETECTOR then + msg = util.c(msg, "#", idx, " environment detector") + else + msg = msg .. " ? (error)" + end + else + msg = util.c("unit ", r_id, "'s ") + + if type == RTU_TYPES.BOILER_VALVE then + msg = util.c(msg, "#", idx, " boiler") + elseif type == RTU_TYPES.TURBINE_VALVE then + msg = util.c(msg, "#", idx, " turbine") + elseif type == RTU_TYPES.DYNAMIC_VALVE then + msg = msg .. "dynamic tank" + elseif type == RTU_TYPES.ENV_DETECTOR then + msg = util.c(msg, "#", idx, " environment detector") + else + msg = msg .. " ? (error)" + end + end + + pgi.create_chk_entry(unit, fail_code, cmp_id, msg) end return fail_code, fail_str @@ -266,10 +361,29 @@ function svsessions.init(nic, fp_ok, config, facility) self.config = config self.facility = facility - -- initialize connection tracking table - self.dev_dbg.connected = { imatrix = nil, sps = nil, tanks = {}, units = {} } + -- initialize connection tracking table by setting all expected devices to true + -- if connections are missing, missing entries will then be created on the next update + + self.dev_dbg.connected = { induction = true, sps = true, tanks = {}, units = {} } + + local cool_conf = facility.get_cooling_conf() + + for i = 1, #cool_conf.fac_tank_list do + self.dev_dbg.connected.tanks[i] = true + end + for i = 1, config.UnitCount do - self.dev_dbg.connected.units[i] = { boilers = {}, turbines = {}, tanks = {} } + local r_cool = cool_conf.r_cool[i] + local conns = { boilers = {}, turbines = {}, tanks = {} } + + for b = 1, r_cool.BoilerCount do conns.boilers[b] = true end + for t = 1, r_cool.TurbineCount do conns.boilers[t] = true end + + if r_cool.TankConnection and cool_conf.fac_tank_defs[i] == 1 then + conns.tanks[1] = true + end + + self.dev_dbg.connected.units[i] = conns end end diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 3578926..9ee6706 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -501,7 +501,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) -- link an environment detector RTU session ---@param envd unit_session - ---@return boolean linked environment detector accepted (max 1) + ---@return boolean linked environment detector accepted function public.add_envd(envd) local fail_code, fail_str = svsessions.check_rtu_id(envd, self.envd, 99) From 8a5c468606cca129f3b64f53fdb2e69294ba5bf7 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 21 Aug 2024 18:53:52 -0400 Subject: [PATCH 14/19] #367 fixes and removed computer ID display --- supervisor/panel/components/chk_entry.lua | 19 +++++-------------- supervisor/panel/pgi.lua | 7 +++---- supervisor/session/svsessions.lua | 21 +++++++-------------- 3 files changed, 15 insertions(+), 32 deletions(-) diff --git a/supervisor/panel/components/chk_entry.lua b/supervisor/panel/components/chk_entry.lua index 63b4f40..b816a75 100644 --- a/supervisor/panel/components/chk_entry.lua +++ b/supervisor/panel/components/chk_entry.lua @@ -17,35 +17,26 @@ local cpair = core.cpair ---@param parent graphics_element parent ---@param msg string message ---@param fail_code integer failure code -local function init(parent, msg, fail_code, cmp_id) - local s_hi_box = style.theme.highlight_box - - local label_fg = style.fp.label_fg - +local function init(parent, msg, fail_code) -- root div local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright} if fail_code == 1 then TextBox{parent=entry,y=1,text="",width=11,fg_bg=cpair(colors.black,colors.orange)} - TextBox{parent=entry,text="BAD INDEX",alignment=ALIGN.CENTER,width=11,nav_active=cpair(colors.black,colors.orange)} + TextBox{parent=entry,text="BAD INDEX",alignment=ALIGN.CENTER,width=11,fg_bg=cpair(colors.black,colors.orange)} TextBox{parent=entry,text="",width=11,fg_bg=cpair(colors.black,colors.orange)} elseif fail_code == 2 then TextBox{parent=entry,y=1,text="",width=11,fg_bg=cpair(colors.black,colors.red)} - TextBox{parent=entry,text="DUPLICATE",alignment=ALIGN.CENTER,width=11,nav_active=cpair(colors.black,colors.red)} + TextBox{parent=entry,text="DUPLICATE",alignment=ALIGN.CENTER,width=11,fg_bg=cpair(colors.black,colors.red)} TextBox{parent=entry,text="",width=11,fg_bg=cpair(colors.black,colors.red)} elseif fail_code == 4 then TextBox{parent=entry,y=1,text="",width=11,fg_bg=cpair(colors.black,colors.yellow)} - TextBox{parent=entry,text="MISSING",alignment=ALIGN.CENTER,width=11,nav_active=cpair(colors.black,colors.yellow)} + TextBox{parent=entry,text="MISSING",alignment=ALIGN.CENTER,width=11,fg_bg=cpair(colors.black,colors.yellow)} TextBox{parent=entry,text="",width=11,fg_bg=cpair(colors.black,colors.yellow)} end - if fail_code == 4 then - TextBox{parent=entry,x=13,y=2,text=msg} - else - TextBox{parent=entry,x=13,y=2,text="@ C "..cmp_id,alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)} - TextBox{parent=entry,x=21,y=2,text=msg} - end + TextBox{parent=entry,x=13,y=2,text=msg} return root end diff --git a/supervisor/panel/pgi.lua b/supervisor/panel/pgi.lua index 15cfd1c..cb3721e 100644 --- a/supervisor/panel/pgi.lua +++ b/supervisor/panel/pgi.lua @@ -111,15 +111,14 @@ end -- add a device ID check failure entry to the CHK list ---@param unit unit_session RTU session ---@param fail_code integer failure code ----@param cmp_id integer computer ID ---@param msg string description to show the user -function pgi.create_chk_entry(unit, fail_code, cmp_id, msg) +function pgi.create_chk_entry(unit, fail_code, msg) local gw_session = unit.get_session_id() if data.chk_list ~= nil and data.chk_entry ~= nil then if not data.entries.chk[gw_session] then data.entries.chk[gw_session] = {} end - local success, result = pcall(data.chk_entry, data.chk_list, msg, fail_code, cmp_id) + local success, result = pcall(data.chk_entry, data.chk_list, msg, fail_code) if success then data.entries.chk[gw_session][unit.get_unit_id()] = result @@ -154,7 +153,7 @@ end ---@param message string missing device message function pgi.create_missing_entry(message) if data.chk_list ~= nil and data.chk_entry ~= nil then - local success, result = pcall(data.chk_entry, data.chk_list, message, 4, -1) + local success, result = pcall(data.chk_entry, data.chk_list, message, 4) if success then data.entries.missing[message] = result diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index caed09f..e4fd99e 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -300,15 +300,6 @@ function svsessions.check_rtu_id(unit, list, max) -- add to the list for the user if fail_code > 0 and fail_code ~= 3 then - local cmp_id = -1 - - for i = 1, #self.sessions.rtu do - if self.sessions.rtu[i].instance.get_id() == unit.get_session_id() then - cmp_id = self.sessions.rtu[i].s_addr - break - end - end - local r_id = unit.get_reactor() local idx = unit.get_device_idx() local type = unit.get_unit_type() @@ -324,7 +315,7 @@ function svsessions.check_rtu_id(unit, list, max) elseif type == RTU_TYPES.DYNAMIC_VALVE then msg = util.c(msg, "#", idx, " dynamic tank") elseif type == RTU_TYPES.ENV_DETECTOR then - msg = util.c(msg, "#", idx, " environment detector") + msg = util.c(msg, "#", idx, " env. detector") else msg = msg .. " ? (error)" end @@ -338,13 +329,13 @@ function svsessions.check_rtu_id(unit, list, max) elseif type == RTU_TYPES.DYNAMIC_VALVE then msg = msg .. "dynamic tank" elseif type == RTU_TYPES.ENV_DETECTOR then - msg = util.c(msg, "#", idx, " environment detector") + msg = util.c(msg, "#", idx, " env. detector") else msg = msg .. " ? (error)" end end - pgi.create_chk_entry(unit, fail_code, cmp_id, msg) + pgi.create_chk_entry(unit, fail_code, msg) end return fail_code, fail_str @@ -369,7 +360,9 @@ function svsessions.init(nic, fp_ok, config, facility) local cool_conf = facility.get_cooling_conf() for i = 1, #cool_conf.fac_tank_list do - self.dev_dbg.connected.tanks[i] = true + if cool_conf.fac_tank_list[i] == 2 then + table.insert(self.dev_dbg.connected.tanks, true) + end end for i = 1, config.UnitCount do @@ -377,7 +370,7 @@ function svsessions.init(nic, fp_ok, config, facility) local conns = { boilers = {}, turbines = {}, tanks = {} } for b = 1, r_cool.BoilerCount do conns.boilers[b] = true end - for t = 1, r_cool.TurbineCount do conns.boilers[t] = true end + for t = 1, r_cool.TurbineCount do conns.turbines[t] = true end if r_cool.TankConnection and cool_conf.fac_tank_defs[i] == 1 then conns.tanks[1] = true From 8c6b264f6bd79a9e64ec4acdbca8a3fd7c6d02ad Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 21 Aug 2024 19:15:12 -0400 Subject: [PATCH 15/19] #367 simplified chk_entry --- supervisor/panel/components/chk_entry.lua | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/supervisor/panel/components/chk_entry.lua b/supervisor/panel/components/chk_entry.lua index b816a75..02ecc21 100644 --- a/supervisor/panel/components/chk_entry.lua +++ b/supervisor/panel/components/chk_entry.lua @@ -22,20 +22,21 @@ local function init(parent, msg, fail_code) local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright} + local fg_bg = cpair(colors.black,colors.yellow) + local tag = "MISSING" + if fail_code == 1 then - TextBox{parent=entry,y=1,text="",width=11,fg_bg=cpair(colors.black,colors.orange)} - TextBox{parent=entry,text="BAD INDEX",alignment=ALIGN.CENTER,width=11,fg_bg=cpair(colors.black,colors.orange)} - TextBox{parent=entry,text="",width=11,fg_bg=cpair(colors.black,colors.orange)} + fg_bg = cpair(colors.black,colors.orange) + tag = "BAD INDEX" elseif fail_code == 2 then - TextBox{parent=entry,y=1,text="",width=11,fg_bg=cpair(colors.black,colors.red)} - TextBox{parent=entry,text="DUPLICATE",alignment=ALIGN.CENTER,width=11,fg_bg=cpair(colors.black,colors.red)} - TextBox{parent=entry,text="",width=11,fg_bg=cpair(colors.black,colors.red)} - elseif fail_code == 4 then - TextBox{parent=entry,y=1,text="",width=11,fg_bg=cpair(colors.black,colors.yellow)} - TextBox{parent=entry,text="MISSING",alignment=ALIGN.CENTER,width=11,fg_bg=cpair(colors.black,colors.yellow)} - TextBox{parent=entry,text="",width=11,fg_bg=cpair(colors.black,colors.yellow)} + fg_bg = cpair(colors.black,colors.red) + tag = "DUPLICATE" end + TextBox{parent=entry,y=1,text="",width=11,fg_bg=fg_bg} + TextBox{parent=entry,text=tag,alignment=ALIGN.CENTER,width=11,fg_bg=fg_bg} + TextBox{parent=entry,text="",width=11,fg_bg=fg_bg} + TextBox{parent=entry,x=13,y=2,text=msg} return root From a1b6ff4bcc673e07a49add7337b4f20dcee68f58 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 21 Aug 2024 19:18:55 -0400 Subject: [PATCH 16/19] luacheck fixes --- supervisor/facility_update.lua | 7 ++++--- supervisor/session/svsessions.lua | 6 ++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/supervisor/facility_update.lua b/supervisor/facility_update.lua index 2927023..43fe86b 100644 --- a/supervisor/facility_update.lua +++ b/supervisor/facility_update.lua @@ -824,8 +824,9 @@ end --#endregion ----@param _self _facility_self -return function (_self) - self = _self +-- link the self instance and return the update interface +---@param fac_self _facility_self +return function (fac_self) + self = fac_self return update end diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index e4fd99e..ff17bb9 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -300,10 +300,8 @@ function svsessions.check_rtu_id(unit, list, max) -- add to the list for the user if fail_code > 0 and fail_code ~= 3 then - local r_id = unit.get_reactor() - local idx = unit.get_device_idx() - local type = unit.get_unit_type() - local msg = "? (error)" + local r_id, idx, type = unit.get_reactor(), unit.get_device_idx(), unit.get_unit_type() + local msg if r_id == 0 then msg = "the facility's " From a087eda0eed3aebf48e66c56193cd375a3420f96 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 22 Aug 2024 16:42:57 +0000 Subject: [PATCH 17/19] #367 RTU fail enum and logging messages --- scada-common/types.lua | 22 ++++++++++++------ supervisor/facility.lua | 24 +++++++++++++------ supervisor/panel/components/chk_entry.lua | 6 +++-- supervisor/panel/pgi.lua | 2 ++ supervisor/session/svsessions.lua | 26 +++++++++++---------- supervisor/unit.lua | 28 ++++++++++++++++------- 6 files changed, 72 insertions(+), 36 deletions(-) diff --git a/scada-common/types.lua b/scada-common/types.lua index e392f53..c27d2d7 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -5,7 +5,7 @@ ---@class types local types = {} --- CLASSES -- +--#region CLASSES ---@class tank_fluid ---@field name fluid @@ -67,12 +67,13 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end ---@field reactor integer ---@field rsio table|nil +--#endregion + -- ALIASES -- ---@alias color integer --- ENUMERATION TYPES -- ---#region +--#region ENUMERATION TYPES ---@enum TEMP_SCALE types.TEMP_SCALE = { @@ -169,6 +170,15 @@ function types.rtu_type_to_string(utype) end end +---@enum RTU_ID_FAIL +types.RTU_ID_FAIL = { + OK = 0, + OUT_OF_RANGE = 1, + DUPLICATE = 2, + MAX_DEVICES = 3, + MISSING = 4 +} + ---@enum TRI_FAIL types.TRI_FAIL = { OK = 1, @@ -290,8 +300,7 @@ types.ALARM_STATE_NAMES = { --#endregion --- STRING TYPES -- ---#region +--#region STRING TYPES ---@alias side ---|"top" @@ -405,8 +414,7 @@ types.DUMPING_MODE = { --#endregion --- MODBUS -- ---#region +--#region MODBUS -- MODBUS function codes ---@enum MODBUS_FCODE diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 503d454..dcc3171 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -9,6 +9,7 @@ local rsctl = require("supervisor.session.rsctl") local svsessions = require("supervisor.session.svsessions") local PROCESS = types.PROCESS +local RTU_ID_FAIL = types.RTU_ID_FAIL local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local WASTE = types.WASTE_PRODUCT @@ -254,14 +255,16 @@ function facility.new(config) ---@return boolean linked induction matrix accepted (max 1) function public.add_imatrix(imatrix) local fail_code, fail_str = svsessions.check_rtu_id(imatrix, self.induction, 1) + local ok = fail_code == RTU_ID_FAIL.OK - if fail_code == 0 then + if ok then table.insert(self.induction, imatrix) + log.debug(util.c("FAC: linked induction matrix [", imatrix.get_unit_id(), "@", imatrix.get_session_id(), "]")) else log.warning(util.c("FAC: rejected induction matrix linking due to failure code ", fail_code, " (", fail_str, ")")) end - return fail_code == 0 + return ok end -- link an SPS RTU session @@ -269,33 +272,40 @@ function facility.new(config) ---@return boolean linked SPS accepted (max 1) function public.add_sps(sps) local fail_code, fail_str = svsessions.check_rtu_id(sps, self.sps, 1) + local ok = fail_code == RTU_ID_FAIL.OK - if fail_code == 0 then + if ok then table.insert(self.sps, sps) + log.debug(util.c("FAC: linked SPS [", sps.get_unit_id(), "@", sps.get_session_id(), "]")) else log.warning(util.c("FAC: rejected SPS linking due to failure code ", fail_code, " (", fail_str, ")")) end - return fail_code == 0 + return ok end -- link a dynamic tank RTU session ---@param dynamic_tank unit_session - function public.add_tank(dynamic_tank) table.insert(self.tanks, dynamic_tank) end + function public.add_tank(dynamic_tank) + table.insert(self.tanks, dynamic_tank) + log.debug(util.c("FAC: linked dynamic tank #", dynamic_tank.get_device_idx(), " [", dynamic_tank.get_unit_id(), "@", dynamic_tank.get_session_id(), "]")) + end -- link an environment detector RTU session ---@param envd unit_session ---@return boolean linked environment detector accepted function public.add_envd(envd) local fail_code, fail_str = svsessions.check_rtu_id(envd, self.envd, 99) + local ok = fail_code == RTU_ID_FAIL.OK - if fail_code == 0 then + if ok then table.insert(self.envd, envd) + log.debug(util.c("FAC: linked environment detector #", envd.get_device_idx(), " [", envd.get_unit_id(), "@", envd.get_session_id(), "]")) else log.warning(util.c("FAC: rejected environment detector linking due to failure code ", fail_code, " (", fail_str, ")")) end - return fail_code == 0 + return ok end -- purge devices associated with the given RTU session ID diff --git a/supervisor/panel/components/chk_entry.lua b/supervisor/panel/components/chk_entry.lua index 02ecc21..ff4a24b 100644 --- a/supervisor/panel/components/chk_entry.lua +++ b/supervisor/panel/components/chk_entry.lua @@ -2,6 +2,8 @@ -- RTU ID Check Failure Entry -- +local types = require("scada-common.types") + local style = require("supervisor.panel.style") local core = require("graphics.core") @@ -25,10 +27,10 @@ local function init(parent, msg, fail_code) local fg_bg = cpair(colors.black,colors.yellow) local tag = "MISSING" - if fail_code == 1 then + if fail_code == types.RTU_ID_FAIL.OUT_OF_RANGE then fg_bg = cpair(colors.black,colors.orange) tag = "BAD INDEX" - elseif fail_code == 2 then + elseif fail_code == types.RTU_ID_FAIL.DUPLICATE then fg_bg = cpair(colors.black,colors.red) tag = "DUPLICATE" end diff --git a/supervisor/panel/pgi.lua b/supervisor/panel/pgi.lua index cb3721e..2d8ee93 100644 --- a/supervisor/panel/pgi.lua +++ b/supervisor/panel/pgi.lua @@ -109,6 +109,7 @@ function pgi.delete_pdg_entry(session_id) end -- add a device ID check failure entry to the CHK list +---@note this assumes only one type of failure can occur per each RTU gateway session's RTU, which is the case ---@param unit unit_session RTU session ---@param fail_code integer failure code ---@param msg string description to show the user @@ -130,6 +131,7 @@ function pgi.create_chk_entry(unit, fail_code, msg) end -- delete a device ID check failure entry from the CHK list +---@note this assumes only one type of failure can occur per each RTU gateway session's RTU, which is the case ---@param unit unit_session RTU session function pgi.delete_chk_entry(unit) local gw_session = unit.get_session_id() diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index ff17bb9..30ef729 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -17,14 +17,15 @@ local pocket = require("supervisor.session.pocket") local rtu = require("supervisor.session.rtu") local svqtypes = require("supervisor.session.svqtypes") -local RTU_TYPES = types.RTU_UNIT_TYPE +local RTU_ID_FAIL = types.RTU_ID_FAIL +local RTU_TYPES = types.RTU_UNIT_TYPE -local SV_Q_DATA = svqtypes.SV_Q_DATA +local SV_Q_DATA = svqtypes.SV_Q_DATA -local PLC_S_CMDS = plc.PLC_S_CMDS -local PLC_S_DATA = plc.PLC_S_DATA +local PLC_S_CMDS = plc.PLC_S_CMDS +local PLC_S_DATA = plc.PLC_S_DATA -local CRD_S_DATA = coordinator.CRD_S_DATA +local CRD_S_DATA = coordinator.CRD_S_DATA local svsessions = {} @@ -274,19 +275,19 @@ end ---@param unit unit_session RTU session ---@param list table table of RTU sessions ---@param max integer max of this type of RTU ----@return 0|1|2|3 fail_code, string fail_str 0 = success, 1 = out-of-range, 2 = duplicate, 3 = exceeded table max +---@return RTU_ID_FAIL fail_code, string fail_str function svsessions.check_rtu_id(unit, list, max) - local fail_code, fail_str = 0, "OK" + local fail_code, fail_str = RTU_ID_FAIL.OK, "OK" if (unit.get_device_idx() < 1 and max ~= 1) or unit.get_device_idx() > max then -- out-of-range - fail_code, fail_str = 1, "index out of range" + fail_code, fail_str = RTU_ID_FAIL.OUT_OF_RANGE, "index out of range" table.insert(self.dev_dbg.out_of_range, unit) else for _, u in ipairs(list) do if u.get_device_idx() == unit.get_device_idx() then -- duplicate - fail_code, fail_str = 2, "duplicate index" + fail_code, fail_str = RTU_ID_FAIL.DUPLICATE, "duplicate index" table.insert(self.dev_dbg.duplicate, unit) break end @@ -294,12 +295,12 @@ function svsessions.check_rtu_id(unit, list, max) end -- make sure this won't exceed the maximum allowable devices - if fail_code == 0 and #list >= max then - fail_code, fail_str = 3, "too many of this type" + if fail_code == RTU_ID_FAIL.OK and #list >= max then + fail_code, fail_str = RTU_ID_FAIL.MAX_DEVICES, "too many of this type" end -- add to the list for the user - if fail_code > 0 and fail_code ~= 3 then + if fail_code ~= RTU_ID_FAIL.OK and fail_code ~= RTU_ID_FAIL.MAX_DEVICES then local r_id, idx, type = unit.get_reactor(), unit.get_device_idx(), unit.get_unit_type() local msg @@ -641,6 +642,7 @@ function svsessions.iterate_all() -- iterate units self.facility.update_units() + -- update tracking of bad RTU IDs and missing devices _update_dev_dbg() end diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 9ee6706..3290af0 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -15,6 +15,7 @@ local ALARM = types.ALARM local PRIO = types.ALARM_PRIORITY local ALARM_STATE = types.ALARM_STATE local TRI_FAIL = types.TRI_FAIL +local RTU_ID_FAIL = types.RTU_ID_FAIL local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local PLC_S_CMDS = plc.PLC_S_CMDS @@ -423,6 +424,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) self.plc_s = plc_session self.plc_i = plc_session.instance + log.debug(util.c(log_tag, "linked PLC [", plc_session.s_addr, ":", plc_session.r_chan, "]")) + -- reset deltas _reset_dt(DT_KEYS.ReactorTemp) _reset_dt(DT_KEYS.ReactorFuel) @@ -435,6 +438,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) ---@param rs_unit unit_session function public.add_redstone(rs_unit) table.insert(self.redstone, rs_unit) + log.debug(util.c(log_tag, "linked redstone [", rs_unit.get_unit_id(), "@", rs_unit.get_session_id(), "]")) -- send or re-send waste settings _set_waste_valves(self.waste_product) @@ -445,9 +449,11 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) ---@return boolean linked turbine accepted to associated device slot function public.add_turbine(turbine) local fail_code, fail_str = svsessions.check_rtu_id(turbine, self.turbines, num_turbines) + local ok = fail_code == RTU_ID_FAIL.OK - if fail_code == 0 then + if ok then table.insert(self.turbines, turbine) + log.debug(util.c(log_tag, "linked turbine #", turbine.get_device_idx(), " [", turbine.get_unit_id(), "@", turbine.get_session_id(), "]")) -- reset deltas _reset_dt(DT_KEYS.TurbineSteam .. turbine.get_device_idx()) @@ -456,7 +462,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) log.warning(util.c(log_tag, "rejected turbine linking due to failure code ", fail_code, " (", fail_str, ")")) end - return fail_code == 0 + return ok end -- link a boiler RTU session @@ -464,9 +470,11 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) ---@return boolean linked boiler accepted to associated device slot function public.add_boiler(boiler) local fail_code, fail_str = svsessions.check_rtu_id(boiler, self.boilers, num_boilers) + local ok = fail_code == RTU_ID_FAIL.OK - if fail_code == 0 then + if ok then table.insert(self.boilers, boiler) + log.debug(util.c(log_tag, "linked boiler #", boiler.get_device_idx(), " [", boiler.get_unit_id(), "@", boiler.get_session_id(), "]")) -- reset deltas _reset_dt(DT_KEYS.BoilerWater .. boiler.get_device_idx()) @@ -477,7 +485,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) log.warning(util.c(log_tag, "rejected boiler linking due to failure code ", fail_code, " (", fail_str, ")")) end - return fail_code == 0 + return ok end -- link a dynamic tank RTU session @@ -485,14 +493,16 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) ---@return boolean linked dynamic tank accepted (max 1) function public.add_tank(dynamic_tank) local fail_code, fail_str = svsessions.check_rtu_id(dynamic_tank, self.tanks, 1) + local ok = fail_code == RTU_ID_FAIL.OK - if fail_code == 0 then + if ok then table.insert(self.tanks, dynamic_tank) + log.debug(util.c(log_tag, "linked dynamic tank [", dynamic_tank.get_unit_id(), "@", dynamic_tank.get_session_id(), "]")) else log.warning(util.c(log_tag, "rejected dynamic tank linking due to failure code ", fail_code, " (", fail_str, ")")) end - return fail_code == 0 + return ok end -- link a solar neutron activator RTU session @@ -504,14 +514,16 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) ---@return boolean linked environment detector accepted function public.add_envd(envd) local fail_code, fail_str = svsessions.check_rtu_id(envd, self.envd, 99) + local ok = fail_code == RTU_ID_FAIL.OK - if fail_code == 0 then + if ok then table.insert(self.envd, envd) + log.debug(util.c(log_tag, "linked environment detector #", envd.get_device_idx(), " [", envd.get_unit_id(), "@", envd.get_session_id(), "]")) else log.warning(util.c(log_tag, "rejected environment detector linking due to failure code ", fail_code, " (", fail_str, ")")) end - return fail_code == 0 + return ok end -- purge devices associated with the given RTU session ID From 6f63092d4bc2901aae6f95a4b25f0a28f9691118 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 22 Aug 2024 16:45:36 +0000 Subject: [PATCH 18/19] #367 check facility dynamic tank linking --- supervisor/facility.lua | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/supervisor/facility.lua b/supervisor/facility.lua index dcc3171..e25c8c8 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -287,8 +287,17 @@ function facility.new(config) -- link a dynamic tank RTU session ---@param dynamic_tank unit_session function public.add_tank(dynamic_tank) - table.insert(self.tanks, dynamic_tank) - log.debug(util.c("FAC: linked dynamic tank #", dynamic_tank.get_device_idx(), " [", dynamic_tank.get_unit_id(), "@", dynamic_tank.get_session_id(), "]")) + local fail_code, fail_str = svsessions.check_rtu_id(dynamic_tank, self.tanks, #self.cooling_conf.fac_tank_list) + local ok = fail_code == RTU_ID_FAIL.OK + + if ok then + table.insert(self.tanks, dynamic_tank) + log.debug(util.c("FAC: linked dynamic tank #", dynamic_tank.get_device_idx(), " [", dynamic_tank.get_unit_id(), "@", dynamic_tank.get_session_id(), "]")) + else + log.warning(util.c("FAC: rejected dynamic tank linking due to failure code ", fail_code, " (", fail_str, ")")) + end + + return ok end -- link an environment detector RTU session From 183af8a5ca26bcd51d1cb03e68fc7bc728606e1d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 22 Aug 2024 18:18:13 +0000 Subject: [PATCH 19/19] #539 logging for investigations --- graphics/core.lua | 2 +- graphics/element.lua | 2 +- graphics/elements/listbox.lua | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/graphics/core.lua b/graphics/core.lua index 4bb0298..26251f8 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -7,7 +7,7 @@ local flasher = require("graphics.flasher") local core = {} -core.version = "2.3.1" +core.version = "2.3.2" core.flasher = flasher core.events = events diff --git a/graphics/element.lua b/graphics/element.lua index 8de720a..9c22a38 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -2,8 +2,8 @@ -- Generic Graphics Element -- -local util = require("scada-common.util") local log = require("scada-common.log") +local util = require("scada-common.util") local core = require("graphics.core") diff --git a/graphics/elements/listbox.lua b/graphics/elements/listbox.lua index 409b6db..f82d469 100644 --- a/graphics/elements/listbox.lua +++ b/graphics/elements/listbox.lua @@ -1,7 +1,7 @@ -- Scroll-able List Box Display Graphics Element -local tcd = require("scada-common.tcd") local log = require("scada-common.log") +local tcd = require("scada-common.tcd") local core = require("graphics.core") local element = require("graphics.element")