diff --git a/coordinator/apisessions.lua b/coordinator/apisessions.lua index 3c14c08..268052e 100644 --- a/coordinator/apisessions.lua +++ b/coordinator/apisessions.lua @@ -4,13 +4,17 @@ local apisessions = {} function apisessions.handle_packet(packet) end -function apisessions.check_all_watchdogs() -end - -function apisessions.close_all() +-- attempt to identify which session's watchdog timer fired +---@param timer_event number +function apisessions.check_all_watchdogs(timer_event) end +-- delete all closed sessions function apisessions.free_all_closed() end +-- close all open connections +function apisessions.close_all() +end + return apisessions diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index 0e23b4f..e4bb283 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -14,17 +14,18 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts -local PROTOCOLS = comms.PROTOCOLS -local DEVICE_TYPES = comms.DEVICE_TYPES +local PROTOCOL = comms.PROTOCOL +local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK -local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES -local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES -local UNIT_COMMANDS = comms.UNIT_COMMANDS -local FAC_COMMANDS = comms.FAC_COMMANDS +local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE +local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE +local UNIT_COMMAND = comms.UNIT_COMMAND +local FAC_COMMAND = comms.FAC_COMMAND local coordinator = {} -- request the user to select a monitor +---@nodiscard ---@param names table available monitors ---@return boolean|string|nil local function ask_monitor(names) @@ -64,9 +65,11 @@ function coordinator.configure_monitors(num_units) end -- we need a certain number of monitors (1 per unit + 1 primary display) - if #names < num_units + 1 then - println("not enough monitors connected (need " .. num_units + 1 .. ")") - log.warning("insufficient monitors present (need " .. num_units + 1 .. ")") + local num_displays_needed = num_units + 1 + if #names < num_displays_needed then + local message = "not enough monitors connected (need " .. num_displays_needed .. ")" + println(message) + log.warning(message) return false end @@ -125,7 +128,6 @@ function coordinator.configure_monitors(num_units) else -- make sure all displays are connected for i = 1, num_units do ----@diagnostic disable-next-line: need-check-nil local display = unit_displays[i] if not util.table_contains(names, display) then @@ -183,14 +185,19 @@ function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end function coordinator.log_boot(message) log_dmesg(message, "BOOT") end function coordinator.log_comms(message) log_dmesg(message, "COMMS") end +-- log a message for communications connecting, providing access to progress indication control functions +---@nodiscard ---@param message string ---@return function update, function done function coordinator.log_comms_connecting(message) ----@diagnostic disable-next-line: return-type-mismatch - return log_dmesg(message, "COMMS", true) + local update, done = log_dmesg(message, "COMMS", true) + ---@cast update function + ---@cast done function + return update, done end -- coordinator communications +---@nodiscard ---@param version string coordinator version ---@param modem table modem device ---@param sv_port integer port of configured supervisor @@ -203,37 +210,33 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range sv_linked = false, sv_seq_num = 0, sv_r_seq_num = nil, - modem = modem, connected = false, last_est_ack = ESTABLISH_ACK.ALLOW } - ---@class coord_comms - local public = {} - comms.set_trusted_range(range) -- PRIVATE FUNCTIONS -- -- configure modem channels local function _conf_channels() - self.modem.closeAll() - self.modem.open(sv_listen) - self.modem.open(api_listen) + modem.closeAll() + modem.open(sv_listen) + modem.open(api_listen) end _conf_channels() -- send a packet to the supervisor - ---@param msg_type SCADA_MGMT_TYPES|SCADA_CRDN_TYPES + ---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE ---@param msg table local function _send_sv(protocol, msg_type, msg) local s_pkt = comms.scada_packet() local pkt = nil ---@type mgmt_packet|crdn_packet - if protocol == PROTOCOLS.SCADA_MGMT then + if protocol == PROTOCOL.SCADA_MGMT then pkt = comms.mgmt_packet() - elseif protocol == PROTOCOLS.SCADA_CRDN then + elseif protocol == PROTOCOL.SCADA_CRDN then pkt = comms.crdn_packet() else return @@ -242,28 +245,30 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range pkt.make(msg_type, msg) s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable()) - self.modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable()) + modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable()) self.sv_seq_num = self.sv_seq_num + 1 end -- attempt connection establishment local function _send_establish() - _send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.ESTABLISH, { comms.version, version, DEVICE_TYPES.CRDN }) + _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN }) end -- keep alive ack ---@param srv_time integer local function _send_keep_alive_ack(srv_time) - _send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() }) + _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) end -- PUBLIC FUNCTIONS -- + ---@class coord_comms + local public = {} + -- reconnect a newly connected modem - ---@param modem table ----@diagnostic disable-next-line: redefined-local - function public.reconnect_modem(modem) - self.modem = modem + ---@param new_modem table + function public.reconnect_modem(new_modem) + modem = new_modem _conf_channels() end @@ -271,10 +276,11 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range function public.close() sv_watchdog.cancel() self.sv_linked = false - _send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.CLOSE, {}) + _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {}) end -- attempt to connect to the subervisor + ---@nodiscard ---@param timeout_s number timeout in seconds ---@param tick_dmesg_waiting function callback to tick dmesg waiting ---@param task_done function callback to show done on dmesg @@ -300,7 +306,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range elseif event == "modem_message" then -- handle message local packet = public.parse_packet(p1, p2, p3, p4, p5) - if packet ~= nil and packet.type == SCADA_MGMT_TYPES.ESTABLISH then + if packet ~= nil and packet.type == SCADA_MGMT_TYPE.ESTABLISH then public.handle_packet(packet) end elseif event == "terminate" then @@ -329,25 +335,25 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range end -- send a facility command - ---@param cmd FAC_COMMANDS command + ---@param cmd FAC_COMMAND command function public.send_fac_command(cmd) - _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, { cmd }) + _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd }) end -- send the auto process control configuration with a start command ---@param config coord_auto_config configuration function public.send_auto_start(config) - _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, { - FAC_COMMANDS.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits + _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { + FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits }) end -- send a unit command - ---@param cmd UNIT_COMMANDS command + ---@param cmd UNIT_COMMAND command ---@param unit integer unit ID ---@param option any? optional option options for the optional options (like burn rate) (does option still look like a word?) function public.send_unit_command(cmd, unit, option) - _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.UNIT_CMD, { cmd, unit, option }) + _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option }) end -- parse a packet @@ -366,19 +372,19 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range if s_pkt.is_valid() then -- get as SCADA management packet - if s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then + if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then local mgmt_pkt = comms.mgmt_packet() if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() end -- get as coordinator packet - elseif s_pkt.protocol() == PROTOCOLS.SCADA_CRDN then + elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then local crdn_pkt = comms.crdn_packet() if crdn_pkt.decode(s_pkt) then pkt = crdn_pkt.get() end -- get as coordinator API packet - elseif s_pkt.protocol() == PROTOCOLS.COORD_API then + elseif s_pkt.protocol() == PROTOCOL.COORD_API then local capi_pkt = comms.capi_packet() if capi_pkt.decode(s_pkt) then pkt = capi_pkt.get() @@ -399,8 +405,8 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range local l_port = packet.scada_frame.local_port() if l_port == api_listen then - if protocol == PROTOCOLS.COORD_API then ----@diagnostic disable-next-line: param-type-mismatch + if protocol == PROTOCOL.COORD_API then + ---@cast packet capi_frame apisessions.handle_packet(packet) else log.debug("illegal packet type " .. protocol .. " on api listening channel", true) @@ -420,9 +426,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range sv_watchdog.feed() -- handle packet - if protocol == PROTOCOLS.SCADA_CRDN then + if protocol == PROTOCOL.SCADA_CRDN then + ---@cast packet crdn_frame if self.sv_linked then - if packet.type == SCADA_CRDN_TYPES.INITIAL_BUILDS then + if packet.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then if packet.length == 2 then -- record builds local fac_builds = iocontrol.record_facility_builds(packet.data[1]) @@ -430,47 +437,47 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range if fac_builds and unit_builds then -- acknowledge receipt of builds - _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.INITIAL_BUILDS, {}) + _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.INITIAL_BUILDS, {}) else - log.error("received invalid INITIAL_BUILDS packet") + log.debug("received invalid INITIAL_BUILDS packet") end else log.debug("INITIAL_BUILDS packet length mismatch") end - elseif packet.type == SCADA_CRDN_TYPES.FAC_BUILDS then + elseif packet.type == SCADA_CRDN_TYPE.FAC_BUILDS then if packet.length == 1 then -- record facility builds if iocontrol.record_facility_builds(packet.data[1]) then -- acknowledge receipt of builds - _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_BUILDS, {}) + _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_BUILDS, {}) else - log.error("received invalid FAC_BUILDS packet") + log.debug("received invalid FAC_BUILDS packet") end else log.debug("FAC_BUILDS packet length mismatch") end - elseif packet.type == SCADA_CRDN_TYPES.FAC_STATUS then + elseif packet.type == SCADA_CRDN_TYPE.FAC_STATUS then -- update facility status if not iocontrol.update_facility_status(packet.data) then - log.error("received invalid FAC_STATUS packet") + log.debug("received invalid FAC_STATUS packet") end - elseif packet.type == SCADA_CRDN_TYPES.FAC_CMD then + elseif packet.type == SCADA_CRDN_TYPE.FAC_CMD then -- facility command acknowledgement if packet.length >= 2 then local cmd = packet.data[1] local ack = packet.data[2] == true - if cmd == FAC_COMMANDS.SCRAM_ALL then + if cmd == FAC_COMMAND.SCRAM_ALL then iocontrol.get_db().facility.scram_ack(ack) - elseif cmd == FAC_COMMANDS.STOP then + elseif cmd == FAC_COMMAND.STOP then iocontrol.get_db().facility.stop_ack(ack) - elseif cmd == FAC_COMMANDS.START then + elseif cmd == FAC_COMMAND.START then if packet.length == 7 then process.start_ack_handle({ table.unpack(packet.data, 2) }) else log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch") end - elseif cmd == FAC_COMMANDS.ACK_ALL_ALARMS then + elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then iocontrol.get_db().facility.ack_alarms_ack(ack) else log.debug(util.c("received facility command ack with unknown command ", cmd)) @@ -478,24 +485,24 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range else log.debug("SCADA_CRDN facility command ack packet length mismatch") end - elseif packet.type == SCADA_CRDN_TYPES.UNIT_BUILDS then + elseif packet.type == SCADA_CRDN_TYPE.UNIT_BUILDS then -- record builds if packet.length == 1 then if iocontrol.record_unit_builds(packet.data[1]) then -- acknowledge receipt of builds - _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.UNIT_BUILDS, {}) + _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_BUILDS, {}) else - log.error("received invalid UNIT_BUILDS packet") + log.debug("received invalid UNIT_BUILDS packet") end else log.debug("UNIT_BUILDS packet length mismatch") end - elseif packet.type == SCADA_CRDN_TYPES.UNIT_STATUSES then + elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then -- update statuses if not iocontrol.update_unit_statuses(packet.data) then log.error("received invalid UNIT_STATUSES packet") end - elseif packet.type == SCADA_CRDN_TYPES.UNIT_CMD then + elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then -- unit command acknowledgement if packet.length == 3 then local cmd = packet.data[1] @@ -505,20 +512,20 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range local unit = iocontrol.get_db().units[unit_id] ---@type ioctl_unit if unit ~= nil then - if cmd == UNIT_COMMANDS.SCRAM then + if cmd == UNIT_COMMAND.SCRAM then unit.scram_ack(ack) - elseif cmd == UNIT_COMMANDS.START then + elseif cmd == UNIT_COMMAND.START then unit.start_ack(ack) - elseif cmd == UNIT_COMMANDS.RESET_RPS then + elseif cmd == UNIT_COMMAND.RESET_RPS then unit.reset_rps_ack(ack) - elseif cmd == UNIT_COMMANDS.SET_BURN then + elseif cmd == UNIT_COMMAND.SET_BURN then unit.set_burn_ack(ack) - elseif cmd == UNIT_COMMANDS.SET_WASTE then + elseif cmd == UNIT_COMMAND.SET_WASTE then unit.set_waste_ack(ack) - elseif cmd == UNIT_COMMANDS.ACK_ALL_ALARMS then + elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then unit.ack_alarms_ack(ack) - elseif cmd == UNIT_COMMANDS.SET_GROUP then - ---@todo how is this going to be handled? + elseif cmd == UNIT_COMMAND.SET_GROUP then + -- UI will be updated to display current group if changed successfully else log.debug(util.c("received unit command ack with unknown command ", cmd)) end @@ -534,8 +541,9 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range else log.debug("discarding SCADA_CRDN packet before linked") end - elseif protocol == PROTOCOLS.SCADA_MGMT then - if packet.type == SCADA_MGMT_TYPES.ESTABLISH then + elseif protocol == PROTOCOL.SCADA_MGMT then + ---@cast packet mgmt_frame + if packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- connection with supervisor established if packet.length == 2 then local est_ack = packet.data[1] @@ -562,10 +570,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range self.sv_linked = true else - log.error("invalid supervisor configuration definitions received, establish failed") + log.debug("invalid supervisor configuration definitions received, establish failed") end else - log.error("invalid supervisor configuration table received, establish failed") + log.debug("invalid supervisor configuration table received, establish failed") end else log.debug("SCADA_MGMT establish packet reply (len = 2) unsupported") @@ -577,11 +585,11 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range if est_ack == ESTABLISH_ACK.DENY then if self.last_est_ack ~= est_ack then - log.debug("supervisor connection denied") + log.info("supervisor connection denied") end elseif est_ack == ESTABLISH_ACK.COLLISION then if self.last_est_ack ~= est_ack then - log.debug("supervisor connection denied due to collision") + log.info("supervisor connection denied due to collision") end elseif est_ack == ESTABLISH_ACK.BAD_VERSION then if self.last_est_ack ~= est_ack then @@ -596,7 +604,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range log.debug("SCADA_MGMT establish packet length mismatch") end elseif self.sv_linked then - if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then + if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back if packet.length == 1 then local timestamp = packet.data[1] @@ -614,14 +622,14 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range else log.debug("SCADA keep alive packet length mismatch") end - elseif packet.type == SCADA_MGMT_TYPES.CLOSE then + elseif packet.type == SCADA_MGMT_TYPE.CLOSE then -- handle session close sv_watchdog.cancel() self.sv_linked = false println_ts("server connection closed by remote host") - log.warning("server connection closed by remote host") + log.info("server connection closed by remote host") else - log.warning("received unknown SCADA_MGMT packet type " .. packet.type) + log.debug("received unknown SCADA_MGMT packet type " .. packet.type) end else log.debug("discarding non-link SCADA_MGMT packet before linked") @@ -636,6 +644,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range end -- check if the coordinator is still linked to the supervisor + ---@nodiscard function public.is_linked() return self.sv_linked end return public diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 30bb6e5..ea243cd 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -1,4 +1,7 @@ -local comms = require("scada-common.comms") +-- +-- I/O Control for Supervisor/Coordinator Integration +-- + local log = require("scada-common.log") local psil = require("scada-common.psil") local types = require("scada-common.types") @@ -7,8 +10,6 @@ local util = require("scada-common.util") local process = require("coordinator.process") local sounder = require("coordinator.sounder") -local UNIT_COMMANDS = comms.UNIT_COMMANDS - local ALARM_STATE = types.ALARM_STATE local iocontrol = {} @@ -19,7 +20,6 @@ local io = {} -- initialize the coordinator IO controller ---@param conf facility_conf configuration ---@param comms coord_comms comms reference ----@diagnostic disable-next-line: redefined-local function iocontrol.init(conf, comms) ---@class ioctl_facility io.facility = { @@ -44,11 +44,11 @@ function iocontrol.init(conf, comms) radiation = types.new_zero_radiation_reading(), - save_cfg_ack = function (success) end, ---@param success boolean - start_ack = function (success) end, ---@param success boolean - stop_ack = function (success) end, ---@param success boolean - scram_ack = function (success) end, ---@param success boolean - ack_alarms_ack = function (success) end, ---@param success boolean + save_cfg_ack = function (success) end, ---@param success boolean + start_ack = function (success) end, ---@param success boolean + stop_ack = function (success) end, ---@param success boolean + scram_ack = function (success) end, ---@param success boolean + ack_alarms_ack = function (success) end, ---@param success boolean ps = psil.create(), @@ -59,7 +59,7 @@ function iocontrol.init(conf, comms) env_d_data = {} } - -- create induction tables (max 1 per unit, preferably 1 total) + -- create induction tables (currently only 1 is supported) for _ = 1, conf.num_units do local data = {} ---@type imatrix_session_db table.insert(io.facility.induction_ps_tbl, psil.create()) @@ -173,6 +173,8 @@ end ---@param build table ---@return boolean valid function iocontrol.record_facility_builds(build) + local valid = true + if type(build) == "table" then local fac = io.facility @@ -190,96 +192,103 @@ function iocontrol.record_facility_builds(build) end else log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id)) + valid = false end end end else - log.error("facility builds not a table") - return false + log.debug("facility builds not a table") + valid = false end - return true + return valid end -- populate unit structure builds ---@param builds table ---@return boolean valid function iocontrol.record_unit_builds(builds) + local valid = true + -- note: if not all units and RTUs are connected, some will be nil for id, build in pairs(builds) do local unit = io.units[id] ---@type ioctl_unit + local log_header = util.c("iocontrol.record_unit_builds[UNIT ", id, "]: ") + if type(build) ~= "table" then - log.error(util.c("corrupted unit builds provided, unit ", id, " not a table")) - return false + log.debug(log_header .. "build not a table") + valid = false elseif type(unit) ~= "table" then - log.error(util.c("corrupted unit builds provided, invalid unit ", id)) - return false - end + log.debug(log_header .. "invalid unit id") + valid = false + else + -- reactor build + if type(build.reactor) == "table" then + unit.reactor_data.mek_struct = build.reactor ---@type mek_struct + for key, val in pairs(unit.reactor_data.mek_struct) do + unit.unit_ps.publish(key, val) + end - local log_header = util.c("iocontrol.record_unit_builds[unit ", id, "]: ") - - -- reactor build - if type(build.reactor) == "table" then - unit.reactor_data.mek_struct = build.reactor ---@type mek_struct - for key, val in pairs(unit.reactor_data.mek_struct) do - unit.unit_ps.publish(key, val) - end - - if (type(unit.reactor_data.mek_struct.length) == "number") and (unit.reactor_data.mek_struct.length ~= 0) and - (type(unit.reactor_data.mek_struct.width) == "number") and (unit.reactor_data.mek_struct.width ~= 0) then - unit.unit_ps.publish("size", { unit.reactor_data.mek_struct.length, unit.reactor_data.mek_struct.width }) - end - end - - -- boiler builds - if type(build.boilers) == "table" then - for b_id, boiler in pairs(build.boilers) do - if type(unit.boiler_data_tbl[b_id]) == "table" then - unit.boiler_data_tbl[b_id].formed = boiler[1] ---@type boolean - unit.boiler_data_tbl[b_id].build = boiler[2] ---@type table - - unit.boiler_ps_tbl[b_id].publish("formed", boiler[1]) - - for key, val in pairs(unit.boiler_data_tbl[b_id].build) do - unit.boiler_ps_tbl[b_id].publish(key, val) - end - else - log.debug(util.c(log_header, "invalid boiler id ", b_id)) + if (type(unit.reactor_data.mek_struct.length) == "number") and (unit.reactor_data.mek_struct.length ~= 0) and + (type(unit.reactor_data.mek_struct.width) == "number") and (unit.reactor_data.mek_struct.width ~= 0) then + unit.unit_ps.publish("size", { unit.reactor_data.mek_struct.length, unit.reactor_data.mek_struct.width }) end end - end - -- turbine builds - if type(build.turbines) == "table" then - for t_id, turbine in pairs(build.turbines) do - if type(unit.turbine_data_tbl[t_id]) == "table" then - unit.turbine_data_tbl[t_id].formed = turbine[1] ---@type boolean - unit.turbine_data_tbl[t_id].build = turbine[2] ---@type table + -- boiler builds + if type(build.boilers) == "table" then + for b_id, boiler in pairs(build.boilers) do + if type(unit.boiler_data_tbl[b_id]) == "table" then + unit.boiler_data_tbl[b_id].formed = boiler[1] ---@type boolean + unit.boiler_data_tbl[b_id].build = boiler[2] ---@type table - unit.turbine_ps_tbl[t_id].publish("formed", turbine[1]) + unit.boiler_ps_tbl[b_id].publish("formed", boiler[1]) - for key, val in pairs(unit.turbine_data_tbl[t_id].build) do - unit.turbine_ps_tbl[t_id].publish(key, val) + for key, val in pairs(unit.boiler_data_tbl[b_id].build) do + unit.boiler_ps_tbl[b_id].publish(key, val) + end + else + log.debug(util.c(log_header, "invalid boiler id ", b_id)) + valid = false + end + end + end + + -- turbine builds + if type(build.turbines) == "table" then + for t_id, turbine in pairs(build.turbines) do + if type(unit.turbine_data_tbl[t_id]) == "table" then + unit.turbine_data_tbl[t_id].formed = turbine[1] ---@type boolean + unit.turbine_data_tbl[t_id].build = turbine[2] ---@type table + + unit.turbine_ps_tbl[t_id].publish("formed", turbine[1]) + + for key, val in pairs(unit.turbine_data_tbl[t_id].build) do + unit.turbine_ps_tbl[t_id].publish(key, val) + end + else + log.debug(util.c(log_header, "invalid turbine id ", t_id)) + valid = false end - else - log.debug(util.c(log_header, "invalid turbine id ", t_id)) end end end end - return true + return valid end -- update facility status ---@param status table ---@return boolean valid function iocontrol.update_facility_status(status) + local valid = true local log_header = util.c("iocontrol.update_facility_status: ") + if type(status) ~= "table" then - log.debug(log_header .. "status not a table") - return false + log.debug(util.c(log_header, "status not a table")) + valid = false else local fac = io.facility @@ -287,10 +296,17 @@ function iocontrol.update_facility_status(status) local ctl_status = status[1] - if type(ctl_status) == "table" and (#ctl_status == 14) then + if type(ctl_status) == "table" and #ctl_status == 14 then fac.all_sys_ok = ctl_status[1] fac.auto_ready = ctl_status[2] - fac.auto_active = ctl_status[3] > 0 + + if type(ctl_status[3]) == "number" then + fac.auto_active = ctl_status[3] > 1 + else + fac.auto_active = false + valid = false + end + fac.auto_ramping = ctl_status[4] fac.auto_saturated = ctl_status[5] @@ -330,6 +346,7 @@ function iocontrol.update_facility_status(status) end else log.debug(log_header .. "control status not a table or length mismatch") + valid = false end -- RTU statuses @@ -337,10 +354,10 @@ function iocontrol.update_facility_status(status) local rtu_statuses = status[2] fac.rtu_count = 0 + if type(rtu_statuses) == "table" then -- connected RTU count fac.rtu_count = rtu_statuses.count - fac.ps.publish("rtu_count", fac.rtu_count) -- power statistics if type(rtu_statuses.power) == "table" then @@ -349,6 +366,7 @@ function iocontrol.update_facility_status(status) fac.induction_ps_tbl[1].publish("avg_outflow", rtu_statuses.power[3]) else log.debug(log_header .. "power statistics list not a table") + valid = false end -- induction matricies statuses @@ -374,16 +392,16 @@ function iocontrol.update_facility_status(status) if data.formed then if rtu_faulted then - fac.induction_ps_tbl[id].publish("computed_status", 3) -- faulted + fac.induction_ps_tbl[id].publish("computed_status", 3) -- faulted elseif data.tanks.energy_fill >= 0.99 then - fac.induction_ps_tbl[id].publish("computed_status", 6) -- full + fac.induction_ps_tbl[id].publish("computed_status", 6) -- full elseif data.tanks.energy_fill <= 0.01 then - fac.induction_ps_tbl[id].publish("computed_status", 5) -- empty + fac.induction_ps_tbl[id].publish("computed_status", 5) -- empty else - fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line + fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line end else - fac.induction_ps_tbl[id].publish("computed_status", 2) -- not formed + fac.induction_ps_tbl[id].publish("computed_status", 2) -- not formed end for key, val in pairs(fac.induction_data_tbl[id].state) do @@ -399,6 +417,7 @@ function iocontrol.update_facility_status(status) end else log.debug(log_header .. "induction matrix list not a table") + valid = false end -- environment detector status @@ -416,313 +435,324 @@ function iocontrol.update_facility_status(status) end else log.debug(log_header .. "radiation monitor list not a table") - return false + valid = false end else log.debug(log_header .. "rtu statuses not a table") + valid = false end + + fac.ps.publish("rtu_count", fac.rtu_count) end - return true + return valid end -- update unit statuses ---@param statuses table ---@return boolean valid function iocontrol.update_unit_statuses(statuses) + local valid = true + if type(statuses) ~= "table" then log.debug("iocontrol.update_unit_statuses: unit statuses not a table") - return false + valid = false elseif #statuses ~= #io.units then log.debug("iocontrol.update_unit_statuses: number of provided unit statuses does not match expected number of units") - return false + valid = false else local burn_rate_sum = 0.0 -- get all unit statuses for i = 1, #statuses do local log_header = util.c("iocontrol.update_unit_statuses[unit ", i, "]: ") + local unit = io.units[i] ---@type ioctl_unit local status = statuses[i] if type(status) ~= "table" or #status ~= 5 then log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)") - return false - end - - -- reactor PLC status - - local reactor_status = status[1] - - if type(reactor_status) ~= "table" then - reactor_status = {} - log.debug(log_header .. "reactor status not a table") - end - - if #reactor_status == 0 then - unit.unit_ps.publish("computed_status", 1) -- disconnected - elseif #reactor_status == 3 then - local mek_status = reactor_status[1] - local rps_status = reactor_status[2] - local gen_status = reactor_status[3] - - if #gen_status == 6 then - unit.reactor_data.last_status_update = gen_status[1] - unit.reactor_data.control_state = gen_status[2] - unit.reactor_data.rps_tripped = gen_status[3] - unit.reactor_data.rps_trip_cause = gen_status[4] - unit.reactor_data.no_reactor = gen_status[5] - unit.reactor_data.formed = gen_status[6] - else - log.debug(log_header .. "reactor general status length mismatch") - end - - unit.reactor_data.rps_status = rps_status ---@type rps_status - unit.reactor_data.mek_status = mek_status ---@type mek_status - - -- if status hasn't been received, mek_status = {} - if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then - burn_rate_sum = burn_rate_sum + unit.reactor_data.mek_status.act_burn_rate - end - - if unit.reactor_data.mek_status.status then - unit.unit_ps.publish("computed_status", 5) -- running - else - if unit.reactor_data.no_reactor then - unit.unit_ps.publish("computed_status", 3) -- faulted - elseif not unit.reactor_data.formed then - unit.unit_ps.publish("computed_status", 2) -- multiblock not formed - elseif unit.reactor_data.rps_status.force_dis then - unit.unit_ps.publish("computed_status", 7) -- reactor force disabled - elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then - unit.unit_ps.publish("computed_status", 6) -- SCRAM - else - unit.unit_ps.publish("computed_status", 4) -- disabled - end - end - - for key, val in pairs(unit.reactor_data) do - if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then - unit.unit_ps.publish(key, val) - end - end - - if type(unit.reactor_data.rps_status) == "table" then - for key, val in pairs(unit.reactor_data.rps_status) do - unit.unit_ps.publish(key, val) - end - end - - if type(unit.reactor_data.mek_status) == "table" then - for key, val in pairs(unit.reactor_data.mek_status) do - unit.unit_ps.publish(key, val) - end - end + valid = false else - log.debug(log_header .. "reactor status length mismatch") - end + -- reactor PLC status + local reactor_status = status[1] - -- RTU statuses + if type(reactor_status) ~= "table" then + reactor_status = {} + log.debug(log_header .. "reactor status not a table") + end - local rtu_statuses = status[2] + if #reactor_status == 0 then + unit.unit_ps.publish("computed_status", 1) -- disconnected + elseif #reactor_status == 3 then + local mek_status = reactor_status[1] + local rps_status = reactor_status[2] + local gen_status = reactor_status[3] - if type(rtu_statuses) == "table" then - -- boiler statuses - if type(rtu_statuses.boilers) == "table" then - for id = 1, #unit.boiler_ps_tbl do - if rtu_statuses.boilers[i] == nil then - -- disconnected - unit.boiler_ps_tbl[id].publish("computed_status", 1) + if #gen_status == 6 then + unit.reactor_data.last_status_update = gen_status[1] + unit.reactor_data.control_state = gen_status[2] + unit.reactor_data.rps_tripped = gen_status[3] + unit.reactor_data.rps_trip_cause = gen_status[4] + unit.reactor_data.no_reactor = gen_status[5] + unit.reactor_data.formed = gen_status[6] + else + log.debug(log_header .. "reactor general status length mismatch") + end + + unit.reactor_data.rps_status = rps_status ---@type rps_status + unit.reactor_data.mek_status = mek_status ---@type mek_status + + -- if status hasn't been received, mek_status = {} + if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then + burn_rate_sum = burn_rate_sum + unit.reactor_data.mek_status.act_burn_rate + end + + if unit.reactor_data.mek_status.status then + unit.unit_ps.publish("computed_status", 5) -- running + else + if unit.reactor_data.no_reactor then + unit.unit_ps.publish("computed_status", 3) -- faulted + elseif not unit.reactor_data.formed then + unit.unit_ps.publish("computed_status", 2) -- multiblock not formed + elseif unit.reactor_data.rps_status.force_dis then + unit.unit_ps.publish("computed_status", 7) -- reactor force disabled + elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then + unit.unit_ps.publish("computed_status", 6) -- SCRAM + else + unit.unit_ps.publish("computed_status", 4) -- disabled end end - for id, boiler in pairs(rtu_statuses.boilers) do - if type(unit.boiler_data_tbl[id]) == "table" then - local rtu_faulted = boiler[1] ---@type boolean - unit.boiler_data_tbl[id].formed = boiler[2] ---@type boolean - unit.boiler_data_tbl[id].state = boiler[3] ---@type table - unit.boiler_data_tbl[id].tanks = boiler[4] ---@type table + for key, val in pairs(unit.reactor_data) do + if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then + unit.unit_ps.publish(key, val) + end + end - local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db + if type(unit.reactor_data.rps_status) == "table" then + for key, val in pairs(unit.reactor_data.rps_status) do + unit.unit_ps.publish(key, val) + end + end - unit.boiler_ps_tbl[id].publish("formed", data.formed) - unit.boiler_ps_tbl[id].publish("faulted", rtu_faulted) + if type(unit.reactor_data.mek_status) == "table" then + for key, val in pairs(unit.reactor_data.mek_status) do + unit.unit_ps.publish(key, val) + end + end + else + log.debug(log_header .. "reactor status length mismatch") + valid = false + end - if rtu_faulted then - unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted - elseif data.formed then - if data.state.boil_rate > 0 then - unit.boiler_ps_tbl[id].publish("computed_status", 5) -- active + -- RTU statuses + local rtu_statuses = status[2] + + if type(rtu_statuses) == "table" then + -- boiler statuses + if type(rtu_statuses.boilers) == "table" then + for id = 1, #unit.boiler_ps_tbl do + if rtu_statuses.boilers[i] == nil then + -- disconnected + unit.boiler_ps_tbl[id].publish("computed_status", 1) + end + end + + for id, boiler in pairs(rtu_statuses.boilers) do + if type(unit.boiler_data_tbl[id]) == "table" then + local rtu_faulted = boiler[1] ---@type boolean + unit.boiler_data_tbl[id].formed = boiler[2] ---@type boolean + unit.boiler_data_tbl[id].state = boiler[3] ---@type table + unit.boiler_data_tbl[id].tanks = boiler[4] ---@type table + + local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db + + unit.boiler_ps_tbl[id].publish("formed", data.formed) + unit.boiler_ps_tbl[id].publish("faulted", rtu_faulted) + + if rtu_faulted then + unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted + elseif data.formed then + if data.state.boil_rate > 0 then + unit.boiler_ps_tbl[id].publish("computed_status", 5) -- active + else + unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle + end else - unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle + unit.boiler_ps_tbl[id].publish("computed_status", 2) -- not formed + end + + for key, val in pairs(unit.boiler_data_tbl[id].state) do + unit.boiler_ps_tbl[id].publish(key, val) + end + + for key, val in pairs(unit.boiler_data_tbl[id].tanks) do + unit.boiler_ps_tbl[id].publish(key, val) end else - unit.boiler_ps_tbl[id].publish("computed_status", 2) -- not formed + log.debug(util.c(log_header, "invalid boiler id ", id)) + valid = false end - - for key, val in pairs(unit.boiler_data_tbl[id].state) do - unit.boiler_ps_tbl[id].publish(key, val) - end - - for key, val in pairs(unit.boiler_data_tbl[id].tanks) do - unit.boiler_ps_tbl[id].publish(key, val) - end - else - log.debug(util.c(log_header, "invalid boiler id ", id)) - end - end - else - log.debug(log_header .. "boiler list not a table") - end - - -- turbine statuses - if type(rtu_statuses.turbines) == "table" then - for id = 1, #unit.turbine_ps_tbl do - if rtu_statuses.turbines[i] == nil then - -- disconnected - unit.turbine_ps_tbl[id].publish("computed_status", 1) end + else + log.debug(log_header .. "boiler list not a table") + valid = false end - for id, turbine in pairs(rtu_statuses.turbines) do - if type(unit.turbine_data_tbl[id]) == "table" then - local rtu_faulted = turbine[1] ---@type boolean - unit.turbine_data_tbl[id].formed = turbine[2] ---@type boolean - unit.turbine_data_tbl[id].state = turbine[3] ---@type table - unit.turbine_data_tbl[id].tanks = turbine[4] ---@type table + -- turbine statuses + if type(rtu_statuses.turbines) == "table" then + for id = 1, #unit.turbine_ps_tbl do + if rtu_statuses.turbines[i] == nil then + -- disconnected + unit.turbine_ps_tbl[id].publish("computed_status", 1) + end + end - local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db + for id, turbine in pairs(rtu_statuses.turbines) do + if type(unit.turbine_data_tbl[id]) == "table" then + local rtu_faulted = turbine[1] ---@type boolean + unit.turbine_data_tbl[id].formed = turbine[2] ---@type boolean + unit.turbine_data_tbl[id].state = turbine[3] ---@type table + unit.turbine_data_tbl[id].tanks = turbine[4] ---@type table - unit.turbine_ps_tbl[id].publish("formed", data.formed) - unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted) + local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db - if rtu_faulted then - unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted - elseif data.formed then - if data.tanks.energy_fill >= 0.99 then - unit.turbine_ps_tbl[id].publish("computed_status", 6) -- trip - elseif data.state.flow_rate < 100 then - unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle + unit.turbine_ps_tbl[id].publish("formed", data.formed) + unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted) + + if rtu_faulted then + unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted + elseif data.formed then + if data.tanks.energy_fill >= 0.99 then + unit.turbine_ps_tbl[id].publish("computed_status", 6) -- trip + elseif data.state.flow_rate < 100 then + unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle + else + unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active + end else - unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active + unit.turbine_ps_tbl[id].publish("computed_status", 2) -- not formed + end + + for key, val in pairs(unit.turbine_data_tbl[id].state) do + unit.turbine_ps_tbl[id].publish(key, val) + end + + for key, val in pairs(unit.turbine_data_tbl[id].tanks) do + unit.turbine_ps_tbl[id].publish(key, val) end else - unit.turbine_ps_tbl[id].publish("computed_status", 2) -- not formed + log.debug(util.c(log_header, "invalid turbine id ", id)) + valid = false end + end + else + log.debug(log_header .. "turbine list not a table") + valid = false + end - for key, val in pairs(unit.turbine_data_tbl[id].state) do - unit.turbine_ps_tbl[id].publish(key, val) - end + -- environment detector status + if type(rtu_statuses.rad_mon) == "table" then + if #rtu_statuses.rad_mon > 0 then + local rad_mon = rtu_statuses.rad_mon[1] + local rtu_faulted = rad_mon[1] ---@type boolean + unit.radiation = rad_mon[2] ---@type number - for key, val in pairs(unit.turbine_data_tbl[id].tanks) do - unit.turbine_ps_tbl[id].publish(key, val) - end + unit.unit_ps.publish("radiation", unit.radiation) else - log.debug(util.c(log_header, "invalid turbine id ", id)) + unit.radiation = types.new_zero_radiation_reading() + end + else + log.debug(log_header .. "radiation monitor list not a table") + valid = false + end + else + log.debug(log_header .. "rtu list not a table") + valid = false + end + + -- annunciator + unit.annunciator = status[3] + + if type(unit.annunciator) ~= "table" then + unit.annunciator = {} + log.debug(log_header .. "annunciator state not a table") + valid = false + end + + for key, val in pairs(unit.annunciator) do + if key == "TurbineTrip" then + -- split up turbine trip table for all turbines and a general OR combination + local trips = val + local any = false + + for id = 1, #trips do + any = any or trips[id] + unit.turbine_ps_tbl[id].publish(key, trips[id]) + end + + unit.unit_ps.publish("TurbineTrip", any) + elseif key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then + -- split up array for all boilers + for id = 1, #val do + unit.boiler_ps_tbl[id].publish(key, val[id]) + end + elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then + -- split up array for all turbines + for id = 1, #val do + unit.turbine_ps_tbl[id].publish(key, val[id]) + end + elseif type(val) == "table" then + -- we missed one of the tables? + log.debug(log_header .. "unrecognized table found in annunciator list, this is a bug") + valid = false + else + -- non-table fields + unit.unit_ps.publish(key, val) + end + end + + -- alarms + local alarm_states = status[4] + + if type(alarm_states) == "table" then + for id = 1, #alarm_states do + local state = alarm_states[id] + + unit.alarms[id] = state + + if state == types.ALARM_STATE.TRIPPED or state == types.ALARM_STATE.ACKED then + unit.unit_ps.publish("Alarm_" .. id, 2) + elseif state == types.ALARM_STATE.RING_BACK then + unit.unit_ps.publish("Alarm_" .. id, 3) + else + unit.unit_ps.publish("Alarm_" .. id, 1) end end else - log.debug(log_header .. "turbine list not a table") - return false + log.debug(log_header .. "alarm states not a table") + valid = false end - -- environment detector status - if type(rtu_statuses.rad_mon) == "table" then - if #rtu_statuses.rad_mon > 0 then - local rad_mon = rtu_statuses.rad_mon[1] - local rtu_faulted = rad_mon[1] ---@type boolean - unit.radiation = rad_mon[2] ---@type number + -- unit state fields + local unit_state = status[5] - unit.unit_ps.publish("radiation", unit.radiation) + if type(unit_state) == "table" then + if #unit_state == 5 then + unit.unit_ps.publish("U_StatusLine1", unit_state[1]) + unit.unit_ps.publish("U_StatusLine2", unit_state[2]) + unit.unit_ps.publish("U_WasteMode", unit_state[3]) + unit.unit_ps.publish("U_AutoReady", unit_state[4]) + unit.unit_ps.publish("U_AutoDegraded", unit_state[5]) else - unit.radiation = types.new_zero_radiation_reading() + log.debug(log_header .. "unit state length mismatch") + valid = false end else - log.debug(log_header .. "radiation monitor list not a table") - return false + log.debug(log_header .. "unit state not a table") + valid = false end - else - log.debug(log_header .. "rtu list not a table") - end - - -- annunciator - - unit.annunciator = status[3] - - if type(unit.annunciator) ~= "table" then - unit.annunciator = {} - log.debug(log_header .. "annunciator state not a table") - end - - for key, val in pairs(unit.annunciator) do - if key == "TurbineTrip" then - -- split up turbine trip table for all turbines and a general OR combination - local trips = val - local any = false - - for id = 1, #trips do - any = any or trips[id] - unit.turbine_ps_tbl[id].publish(key, trips[id]) - end - - unit.unit_ps.publish("TurbineTrip", any) - elseif key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then - -- split up array for all boilers - for id = 1, #val do - unit.boiler_ps_tbl[id].publish(key, val[id]) - end - elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then - -- split up array for all turbines - for id = 1, #val do - unit.turbine_ps_tbl[id].publish(key, val[id]) - end - elseif type(val) == "table" then - -- we missed one of the tables? - log.error(log_header .. "unrecognized table found in annunciator list, this is a bug", true) - else - -- non-table fields - unit.unit_ps.publish(key, val) - end - end - - -- alarms - - local alarm_states = status[4] - - if type(alarm_states) == "table" then - for id = 1, #alarm_states do - local state = alarm_states[id] - - unit.alarms[id] = state - - if state == types.ALARM_STATE.TRIPPED or state == types.ALARM_STATE.ACKED then - unit.unit_ps.publish("Alarm_" .. id, 2) - elseif state == types.ALARM_STATE.RING_BACK then - unit.unit_ps.publish("Alarm_" .. id, 3) - else - unit.unit_ps.publish("Alarm_" .. id, 1) - end - end - else - log.debug(log_header .. "alarm states not a table") - end - - -- unit state fields - - local unit_state = status[5] - - if type(unit_state) == "table" then - if #unit_state == 5 then - unit.unit_ps.publish("U_StatusLine1", unit_state[1]) - unit.unit_ps.publish("U_StatusLine2", unit_state[2]) - unit.unit_ps.publish("U_WasteMode", unit_state[3]) - unit.unit_ps.publish("U_AutoReady", unit_state[4]) - unit.unit_ps.publish("U_AutoDegraded", unit_state[5]) - else - log.debug(log_header .. "unit state length mismatch") - end - else - log.debug(log_header .. "unit state not a table") end end @@ -732,7 +762,7 @@ function iocontrol.update_unit_statuses(statuses) sounder.eval(io.units) end - return true + return valid end -- get the IO controller database diff --git a/coordinator/process.lua b/coordinator/process.lua index b3a6bb3..1e318ed 100644 --- a/coordinator/process.lua +++ b/coordinator/process.lua @@ -1,11 +1,14 @@ +-- +-- Process Control Management +-- local comms = require("scada-common.comms") local log = require("scada-common.log") local types = require("scada-common.types") local util = require("scada-common.util") -local FAC_COMMANDS = comms.FAC_COMMANDS -local UNIT_COMMANDS = comms.UNIT_COMMANDS +local FAC_COMMAND = comms.FAC_COMMAND +local UNIT_COMMAND = comms.UNIT_COMMAND local PROCESS = types.PROCESS @@ -30,11 +33,11 @@ local self = { -------------------------- -- initialize the process controller ----@param iocontrol ioctl ----@diagnostic disable-next-line: redefined-local -function process.init(iocontrol, comms) +---@param iocontrol ioctl iocontrl system +---@param coord_comms coord_comms coordinator communications +function process.init(iocontrol, coord_comms) self.io = iocontrol - self.comms = comms + self.comms = coord_comms for i = 1, self.io.facility.num_units do self.config.limits[i] = 0.1 @@ -71,7 +74,7 @@ function process.init(iocontrol, comms) if type(waste_mode) == "table" then for id, mode in pairs(waste_mode) do - self.comms.send_unit_command(UNIT_COMMANDS.SET_WASTE, id, mode) + self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) end log.info("PROCESS: loaded waste mode settings from coord.settings") @@ -81,7 +84,7 @@ function process.init(iocontrol, comms) if type(prio_groups) == "table" then for id, group in pairs(prio_groups) do - self.comms.send_unit_command(UNIT_COMMANDS.SET_GROUP, id, group) + self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group) end log.info("PROCESS: loaded priority groups settings from coord.settings") @@ -90,45 +93,45 @@ end -- facility SCRAM command function process.fac_scram() - self.comms.send_fac_command(FAC_COMMANDS.SCRAM_ALL) - log.debug("FAC: SCRAM ALL") + self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL) + log.debug("PROCESS: FAC SCRAM ALL") end -- facility alarm acknowledge command function process.fac_ack_alarms() - self.comms.send_fac_command(FAC_COMMANDS.ACK_ALL_ALARMS) - log.debug("FAC: ACK ALL ALARMS") + self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS) + log.debug("PROCESS: FAC ACK ALL ALARMS") end -- start reactor ---@param id integer unit ID function process.start(id) self.io.units[id].control_state = true - self.comms.send_unit_command(UNIT_COMMANDS.START, id) - log.debug(util.c("UNIT[", id, "]: START")) + self.comms.send_unit_command(UNIT_COMMAND.START, id) + log.debug(util.c("PROCESS: UNIT[", id, "] START")) end -- SCRAM reactor ---@param id integer unit ID function process.scram(id) self.io.units[id].control_state = false - self.comms.send_unit_command(UNIT_COMMANDS.SCRAM, id) - log.debug(util.c("UNIT[", id, "]: SCRAM")) + self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id) + log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM")) end -- reset reactor protection system ---@param id integer unit ID function process.reset_rps(id) - self.comms.send_unit_command(UNIT_COMMANDS.RESET_RPS, id) - log.debug(util.c("UNIT[", id, "]: RESET RPS")) + self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id) + log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS")) end -- set burn rate ---@param id integer unit ID ---@param rate number burn rate function process.set_rate(id, rate) - self.comms.send_unit_command(UNIT_COMMANDS.SET_BURN, id, rate) - log.debug(util.c("UNIT[", id, "]: SET BURN = ", rate)) + self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate) + log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate)) end -- set waste mode @@ -138,14 +141,12 @@ function process.set_waste(id, mode) -- publish so that if it fails then it gets reset self.io.units[id].unit_ps.publish("U_WasteMode", mode) - self.comms.send_unit_command(UNIT_COMMANDS.SET_WASTE, id, mode) - log.debug(util.c("UNIT[", id, "]: SET WASTE = ", mode)) + self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) + log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode)) local waste_mode = settings.get("WASTE_MODES") ---@type table|nil - if type(waste_mode) ~= "table" then - waste_mode = {} - end + if type(waste_mode) ~= "table" then waste_mode = {} end waste_mode[id] = mode @@ -159,38 +160,36 @@ end -- acknowledge all alarms ---@param id integer unit ID function process.ack_all_alarms(id) - self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALL_ALARMS, id) - log.debug(util.c("UNIT[", id, "]: ACK ALL ALARMS")) + self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id) + log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS")) end -- acknowledge an alarm ---@param id integer unit ID ---@param alarm integer alarm ID function process.ack_alarm(id, alarm) - self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALARM, id, alarm) - log.debug(util.c("UNIT[", id, "]: ACK ALARM ", alarm)) + self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm) + log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm)) end -- reset an alarm ---@param id integer unit ID ---@param alarm integer alarm ID function process.reset_alarm(id, alarm) - self.comms.send_unit_command(UNIT_COMMANDS.RESET_ALARM, id, alarm) - log.debug(util.c("UNIT[", id, "]: RESET ALARM ", alarm)) + self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm) + log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm)) end -- assign a unit to a group ---@param unit_id integer unit ID ---@param group_id integer|0 group ID or 0 for independent function process.set_group(unit_id, group_id) - self.comms.send_unit_command(UNIT_COMMANDS.SET_GROUP, unit_id, group_id) - log.debug(util.c("UNIT[", unit_id, "]: SET GROUP ", group_id)) + self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id) + log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id)) local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil - if type(prio_groups) ~= "table" then - prio_groups = {} - end + if type(prio_groups) ~= "table" then prio_groups = {} end prio_groups[unit_id] = group_id @@ -207,14 +206,14 @@ end -- stop automatic process control function process.stop_auto() - self.comms.send_fac_command(FAC_COMMANDS.STOP) - log.debug("FAC: STOP AUTO") + self.comms.send_fac_command(FAC_COMMAND.STOP) + log.debug("PROCESS: STOP AUTO CTL") end -- start automatic process control function process.start_auto() self.comms.send_auto_start(self.config) - log.debug("FAC: START AUTO") + log.debug("PROCESS: START AUTO CTL") end -- save process control settings @@ -246,8 +245,6 @@ function process.save(mode, burn_target, charge_target, gen_target, limits) log.warning("process.save(): failed to save coordinator settings file") end - log.debug("saved = " .. util.strval(saved)) - self.io.facility.save_cfg_ack(saved) end @@ -273,18 +270,4 @@ function process.start_ack_handle(response) self.io.facility.start_ack(ack) end --------------------------- --- SUPERVISOR RESPONSES -- --------------------------- - --- acknowledgement from the supervisor to assign a unit to a group -function process.sv_assign(unit_id, group_id) - self.io.units[unit_id].group = group_id -end - --- acknowledgement from the supervisor to assign a unit a burn rate limit -function process.sv_limit(unit_id, limit) - self.io.units[unit_id].limit = limit -end - return process diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index 51816f6..4003d71 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -1,3 +1,7 @@ +-- +-- Graphics Rendering Control +-- + local log = require("scada-common.log") local util = require("scada-common.util") @@ -56,6 +60,7 @@ function renderer.set_displays(monitors) end -- check if the renderer is configured to use a given monitor peripheral +---@nodiscard ---@param periph table peripheral ---@return boolean is_used function renderer.is_monitor_used(periph) @@ -87,6 +92,7 @@ function renderer.reset(recolor) end -- check main display width +---@nodiscard ---@return boolean width_okay function renderer.validate_main_display_width() local w, _ = engine.monitors.primary.getSize() @@ -94,6 +100,7 @@ function renderer.validate_main_display_width() end -- check display sizes +---@nodiscard ---@return boolean valid all unit display dimensions OK function renderer.validate_unit_display_sizes() local valid = true @@ -101,7 +108,7 @@ function renderer.validate_unit_display_sizes() for id, monitor in pairs(engine.monitors.unit_displays) do local w, h = monitor.getSize() if w ~= 79 or h ~= 52 then - log.warning(util.c("unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h)) + log.warning(util.c("RENDERER: unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h)) valid = false end end @@ -171,6 +178,7 @@ function renderer.close_ui() end -- is the UI ready? +---@nodiscard ---@return boolean ready function renderer.ui_ready() return engine.ui_ready end diff --git a/coordinator/sounder.lua b/coordinator/sounder.lua index ff0ec17..373b8f1 100644 --- a/coordinator/sounder.lua +++ b/coordinator/sounder.lua @@ -14,7 +14,7 @@ local sounder = {} local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry local _DRATE = 48000 -- 48kHz audio -local _MAX_VAL = 127/2 -- max signed integer in this 8-bit audio +local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio local _MAX_SAMPLES = 0x20000 -- 128 * 1024 samples local _05s_SAMPLES = 24000 -- half a second worth of samples @@ -26,7 +26,8 @@ local alarm_ctl = { playing = false, num_active = 0, next_block = 1, - quad_buffer = { {}, {}, {}, {} } -- split audio up into 0.5s samples so specific components can be ended quicker + -- split audio up into 0.5s samples so specific components can be ended quicker + quad_buffer = { {}, {}, {}, {} } } -- sounds modeled after https://www.e2s.com/references-and-guidelines/listen-and-download-alarm-tones @@ -52,6 +53,7 @@ local TONES = { } -- calculate how many samples are in the given number of milliseconds +---@nodiscard ---@param ms integer milliseconds ---@return integer samples local function ms_to_samples(ms) return math.floor(ms * 48) end @@ -224,6 +226,7 @@ end --#endregion -- hard audio limiter +---@nodiscard ---@param output number output level ---@return number limited -128.0 to 127.0 local function limit(output) @@ -454,7 +457,7 @@ function sounder.test_power_scale() end end - log.debug("power rescale test took " .. (util.time_ms() - start) .. "ms") + log.debug("SOUNDER: power rescale test took " .. (util.time_ms() - start) .. "ms") end --#endregion diff --git a/coordinator/startup.lua b/coordinator/startup.lua index aebb5dd..8af8567 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local iocontrol = require("coordinator.iocontrol") local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") -local COORDINATOR_VERSION = "beta-v0.10.1" +local COORDINATOR_VERSION = "v0.11.0" local print = util.print local println = util.println @@ -81,7 +81,7 @@ local function main() -- setup monitors local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS) if not configured or monitors == nil then - println("boot> monitor setup failed") + println("startup> monitor setup failed") log.fatal("monitor configuration failed") return end @@ -91,11 +91,11 @@ local function main() renderer.reset(config.RECOLOR) if not renderer.validate_main_display_width() then - println("boot> main display must be 8 blocks wide") + println("startup> main display must be 8 blocks wide") log.fatal("main display not wide enough") return elseif not renderer.validate_unit_display_sizes() then - println("boot> one or more unit display dimensions incorrect; they must be 4x4 blocks") + println("startup> one or more unit display dimensions incorrect; they must be 4x4 blocks") log.fatal("unit display dimensions incorrect") return end @@ -116,7 +116,7 @@ local function main() local speaker = ppm.get_device("speaker") if speaker == nil then log_boot("annunciator alarm speaker not found") - println("boot> speaker not found") + println("startup> speaker not found") log.fatal("no annunciator alarm speaker found") return else @@ -135,7 +135,7 @@ local function main() local modem = ppm.get_wireless_modem() if modem == nil then log_comms("wireless modem not found") - println("boot> wireless modem not found") + println("startup> wireless modem not found") log.fatal("no wireless modem on startup") return else @@ -145,12 +145,12 @@ local function main() -- create connection watchdog local conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) conn_watchdog.cancel() - log.debug("boot> conn watchdog created") + log.debug("startup> conn watchdog created") -- start comms, open all channels local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_LISTEN, config.SCADA_API_LISTEN, config.TRUSTED_RANGE, conn_watchdog) - log.debug("boot> comms init") + log.debug("startup> comms init") log_comms("comms initialized") -- base loop clock (2Hz, 10 ticks) @@ -176,7 +176,7 @@ local function main() end if not init_connect_sv() then - println("boot> failed to connect to supervisor") + println("startup> failed to connect to supervisor") log_sys("system shutdown") return else @@ -199,7 +199,7 @@ local function main() renderer.close_ui() log_graphics(util.c("UI crashed: ", message)) println_ts("UI crashed") - log.fatal(util.c("ui crashed with error ", message)) + log.fatal(util.c("GUI crashed with error ", message)) else log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms") @@ -223,7 +223,7 @@ local function main() if ui_ok then -- start connection watchdog conn_watchdog.feed() - log.debug("boot> conn watchdog started") + log.debug("startup> conn watchdog started") log_sys("system started successfully") end @@ -243,7 +243,6 @@ local function main() no_modem = true log_sys("comms modem disconnected") println_ts("wireless modem disconnected!") - log.error("comms modem disconnected!") -- close out UI renderer.close_ui() @@ -252,20 +251,21 @@ local function main() log_sys("awaiting comms modem reconnect...") else log_sys("non-comms modem disconnected") - log.warning("non-comms modem disconnected") end elseif type == "monitor" then if renderer.is_monitor_used(device) then -- "halt and catch fire" style handling - println_ts("lost a configured monitor, system will now exit") - log_sys("lost a configured monitor, system will now exit") + local msg = "lost a configured monitor, system will now exit" + println_ts(msg) + log_sys(msg) break else log_sys("lost unused monitor, ignoring") end elseif type == "speaker" then - println_ts("lost alarm sounder speaker") - log_sys("lost alarm sounder speaker") + local msg = "lost alarm sounder speaker" + println_ts(msg) + log_sys(msg) end end elseif event == "peripheral" then @@ -291,8 +291,9 @@ local function main() elseif type == "monitor" then -- not supported, system will exit on loss of in-use monitors elseif type == "speaker" then - println_ts("alarm sounder speaker reconnected") - log_sys("alarm sounder speaker reconnected") + local msg = "alarm sounder speaker reconnected" + println_ts(msg) + log_sys(msg) sounder.reconnect(device) end end @@ -301,7 +302,7 @@ local function main() -- main loop tick -- free any closed sessions - --apisessions.free_all_closed() + apisessions.free_all_closed() -- update date and time string for main display iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format)) @@ -326,7 +327,7 @@ local function main() -- a non-clock/main watchdog timer event --check API watchdogs - --apisessions.check_all_watchdogs(param1) + apisessions.check_all_watchdogs(param1) -- notify timer callback dispatcher tcallbackdsp.handle(param1) diff --git a/coordinator/ui/components/processctl.lua b/coordinator/ui/components/processctl.lua index 17b3e66..26944e0 100644 --- a/coordinator/ui/components/processctl.lua +++ b/coordinator/ui/components/processctl.lua @@ -16,11 +16,8 @@ local DataIndicator = require("graphics.elements.indicators.data") local IndicatorLight = require("graphics.elements.indicators.light") local RadIndicator = require("graphics.elements.indicators.rad") local TriIndicatorLight = require("graphics.elements.indicators.trilight") -local VerticalBar = require("graphics.elements.indicators.vbar") local HazardButton = require("graphics.elements.controls.hazard_button") -local MultiButton = require("graphics.elements.controls.multi_button") -local PushButton = require("graphics.elements.controls.push_button") local RadioButton = require("graphics.elements.controls.radio_button") local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") diff --git a/coordinator/ui/components/reactor.lua b/coordinator/ui/components/reactor.lua index 5dc9f23..a17fc75 100644 --- a/coordinator/ui/components/reactor.lua +++ b/coordinator/ui/components/reactor.lua @@ -1,3 +1,5 @@ +local types = require("scada-common.types") + local style = require("coordinator.ui.style") local core = require("graphics.core") @@ -47,7 +49,7 @@ local function new_view(root, x, y, data, ps) local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14} ps.subscribe("ccool_type", function (type) - if type == "mekanism:sodium" then + if type == types.FLUID.SODIUM then ccool.recolor(cpair(colors.lightBlue, colors.gray)) else ccool.recolor(cpair(colors.blue, colors.gray)) @@ -55,7 +57,7 @@ local function new_view(root, x, y, data, ps) end) ps.subscribe("hcool_type", function (type) - if type == "mekanism:superheated_sodium" then + if type == types.FLUID.SUPERHEATED_SODIUM then hcool.recolor(cpair(colors.orange, colors.gray)) else hcool.recolor(cpair(colors.white, colors.gray)) diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index f5716b5..f507682 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -237,13 +237,13 @@ local function init(parent, id) local rcs_annunc = Div{parent=rcs,width=27,height=22,x=2,y=1} local rcs_tags = Div{parent=rcs,width=2,height=13,x=29,y=9} - local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)} - local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.yellow} - local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)} - local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)} - local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)} - local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)} - local c_tbnt = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} + local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)} + local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.yellow} + local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)} + local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)} + local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)} + local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)} + local c_tbnt = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} u_ps.subscribe("RCSFault", c_flt.update) u_ps.subscribe("EmergencyCoolant", c_emg.update) @@ -287,7 +287,7 @@ local function init(parent, id) end local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} - t_ps[1].subscribe("SteamDumpOpen", function (val) t1_sdo.update(val + 1) end) + t_ps[1].subscribe("SteamDumpOpen", t1_sdo.update) TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg} local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)} @@ -300,7 +300,7 @@ local function init(parent, id) if unit.num_turbines > 1 then TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg} local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} - t_ps[2].subscribe("SteamDumpOpen", function (val) t2_sdo.update(val + 1) end) + t_ps[2].subscribe("SteamDumpOpen", t2_sdo.update) TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg} local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)} @@ -314,7 +314,7 @@ local function init(parent, id) if unit.num_turbines > 2 then TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg} local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} - t_ps[3].subscribe("SteamDumpOpen", function (val) t3_sdo.update(val + 1) end) + t_ps[3].subscribe("SteamDumpOpen", t3_sdo.update) TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg} local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)} diff --git a/coordinator/ui/components/unit_overview.lua b/coordinator/ui/components/unit_overview.lua index 672c6ea..e5a07f9 100644 --- a/coordinator/ui/components/unit_overview.lua +++ b/coordinator/ui/components/unit_overview.lua @@ -101,16 +101,16 @@ local function make(parent, x, y, unit) local steam_pipes_b = {} if no_boilers then - table.insert(steam_pipes_b, pipe(0, 1, 3, 1, colors.white)) -- steam to turbine 1 - table.insert(steam_pipes_b, pipe(0, 2, 3, 2, colors.blue)) -- water to turbine 1 + table.insert(steam_pipes_b, pipe(0, 1, 3, 1, colors.white)) -- steam to turbine 1 + table.insert(steam_pipes_b, pipe(0, 2, 3, 2, colors.blue)) -- water to turbine 1 if num_turbines >= 2 then - table.insert(steam_pipes_b, pipe(1, 2, 3, 9, colors.white)) -- steam to turbine 2 - table.insert(steam_pipes_b, pipe(2, 3, 3, 10, colors.blue)) -- water to turbine 2 + table.insert(steam_pipes_b, pipe(1, 2, 3, 9, colors.white)) -- steam to turbine 2 + table.insert(steam_pipes_b, pipe(2, 3, 3, 10, colors.blue)) -- water to turbine 2 end if num_turbines >= 3 then - table.insert(steam_pipes_b, pipe(1, 9, 3, 17, colors.white)) -- steam boiler 1 to turbine 1 junction end + table.insert(steam_pipes_b, pipe(1, 9, 3, 17, colors.white)) -- steam boiler 1 to turbine 1 junction end table.insert(steam_pipes_b, pipe(2, 10, 3, 18, colors.blue)) -- water boiler 1 to turbine 1 junction start end else diff --git a/coordinator/ui/components/unit_waiting.lua b/coordinator/ui/components/unit_waiting.lua index f7fd7ec..3b1a846 100644 --- a/coordinator/ui/components/unit_waiting.lua +++ b/coordinator/ui/components/unit_waiting.lua @@ -1,5 +1,5 @@ -- --- Reactor Unit SCADA Coordinator GUI +-- Reactor Unit Waiting Spinner -- local style = require("coordinator.ui.style") diff --git a/coordinator/ui/dialog.lua b/coordinator/ui/dialog.lua index 4c2a522..676ae2b 100644 --- a/coordinator/ui/dialog.lua +++ b/coordinator/ui/dialog.lua @@ -3,13 +3,11 @@ local completion = require("cc.completion") local util = require("scada-common.util") local print = util.print -local println = util.println -local print_ts = util.print_ts -local println_ts = util.println_ts local dialog = {} -- ask the user yes or no +---@nodiscard ---@param question string ---@param default boolean ---@return boolean|nil @@ -36,6 +34,7 @@ function dialog.ask_y_n(question, default) end -- ask the user for an input within a set of options +---@nodiscard ---@param options table ---@param cancel string ---@return boolean|string|nil diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua index b200f44..94d25f8 100644 --- a/coordinator/ui/layout/main_view.lua +++ b/coordinator/ui/layout/main_view.lua @@ -77,7 +77,7 @@ local function init(monitor) end end - -- command & control + -- command & control cnc_y_start = cnc_y_start @@ -90,7 +90,7 @@ local function init(monitor) cnc_bottom_align_start = cnc_bottom_align_start + 2 - local process = process_ctl(main, 2, cnc_bottom_align_start) + process_ctl(main, 2, cnc_bottom_align_start) -- testing ---@fixme remove test code @@ -123,7 +123,7 @@ local function init(monitor) SwitchButton{parent=audio,x=1,text="RCS TRANSIENT",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_rcs} SwitchButton{parent=audio,x=1,text="TURBINE TRIP",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_turbinet} - local imatrix_1 = imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1]) + imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1]) return main end diff --git a/graphics/core.lua b/graphics/core.lua index 510e31d..98c8ed5 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -16,6 +16,7 @@ local events = {} ---@field y integer -- create a new touch event definition +---@nodiscard ---@param monitor string ---@param x integer ---@param y integer @@ -32,7 +33,7 @@ core.events = events local graphics = {} ----@alias TEXT_ALIGN integer +---@enum TEXT_ALIGN graphics.TEXT_ALIGN = { LEFT = 1, CENTER = 2, @@ -47,6 +48,7 @@ graphics.TEXT_ALIGN = { ---@alias element_id string|integer -- create a new border definition +---@nodiscard ---@param width integer border width ---@param color color border color ---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false @@ -66,6 +68,7 @@ end ---@field h integer -- create a new graphics frame definition +---@nodiscard ---@param x integer ---@param y integer ---@param w integer @@ -91,6 +94,7 @@ end ---@field blit_bkg string -- create a new color pair definition +---@nodiscard ---@param a color ---@param b color ---@return cpair @@ -120,9 +124,9 @@ end ---@field thin boolean true for 1 subpixel, false (default) for 2 ---@field align_tr boolean false to align bottom left (default), true to align top right --- create a new pipe --- +-- create a new pipe
-- note: pipe coordinate origin is (0, 0) +---@nodiscard ---@param x1 integer starting x, origin is 0 ---@param y1 integer starting y, origin is 0 ---@param x2 integer ending x, origin is 0 diff --git a/graphics/element.lua b/graphics/element.lua index 5f32060..8aa3ce9 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -47,6 +47,7 @@ local element = {} ---|tiling_args -- a base graphics element, should not be created on its own +---@nodiscard ---@param args graphics_args arguments function element.new(args) local self = { @@ -172,6 +173,7 @@ function element.new(args) end -- get value + ---@nodiscard function protected.get_value() return protected.value end @@ -218,6 +220,7 @@ function element.new(args) end -- get public interface + ---@nodiscard ---@return graphics_element element, element_id id function protected.get() return public, self.id end @@ -246,11 +249,13 @@ function element.new(args) ---------------------- -- get the window object + ---@nodiscard function public.window() return protected.window end -- CHILD ELEMENTS -- -- add a child element + ---@nodiscard ---@param key string|nil id ---@param child graphics_template ---@return integer|string key @@ -271,6 +276,7 @@ function element.new(args) end -- get a child element + ---@nodiscard ---@return graphics_element function public.get_child(key) return self.children[key] end @@ -279,6 +285,7 @@ function element.new(args) function public.remove(key) self.children[key] = nil end -- attempt to get a child element by ID (does not include this element itself) + ---@nodiscard ---@param id element_id ---@return graphics_element|nil element function public.get_element_by_id(id) @@ -297,39 +304,49 @@ function element.new(args) -- AUTO-PLACEMENT -- -- skip a line for automatically placed elements - function public.line_break() self.next_y = self.next_y + 1 end + function public.line_break() + self.next_y = self.next_y + 1 + end -- PROPERTIES -- -- get the foreground/background colors + ---@nodiscard ---@return cpair fg_bg - function public.get_fg_bg() return protected.fg_bg end + function public.get_fg_bg() + return protected.fg_bg + end -- get element x + ---@nodiscard ---@return integer x function public.get_x() return protected.frame.x end -- get element y + ---@nodiscard ---@return integer y function public.get_y() return protected.frame.y end -- get element width + ---@nodiscard ---@return integer width function public.width() return protected.frame.w end -- get element height + ---@nodiscard ---@return integer height function public.height() return protected.frame.h end -- get the element value + ---@nodiscard ---@return any value function public.get_value() return protected.get_value() diff --git a/graphics/elements/displaybox.lua b/graphics/elements/displaybox.lua index 0d7fd47..c7e5c9f 100644 --- a/graphics/elements/displaybox.lua +++ b/graphics/elements/displaybox.lua @@ -12,6 +12,7 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors -- new root display box +---@nodiscard ---@param args displaybox_args local function displaybox(args) -- create new graphics element base object diff --git a/graphics/elements/div.lua b/graphics/elements/div.lua index 59e3a1e..5eeef71 100644 --- a/graphics/elements/div.lua +++ b/graphics/elements/div.lua @@ -13,6 +13,7 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors -- new div element +---@nodiscard ---@param args div_args ---@return graphics_element element, element_id id local function div(args) diff --git a/graphics/elements/indicators/alight.lua b/graphics/elements/indicators/alight.lua index eea103a..8bb8fa6 100644 --- a/graphics/elements/indicators/alight.lua +++ b/graphics/elements/indicators/alight.lua @@ -20,6 +20,7 @@ local flasher = require("graphics.flasher") ---@field fg_bg? cpair foreground/background colors -- new alarm indicator light +---@nodiscard ---@param args alarm_indicator_light ---@return graphics_element element, element_id id local function alarm_indicator_light(args) diff --git a/graphics/elements/indicators/coremap.lua b/graphics/elements/indicators/coremap.lua index 0ca72e1..c50348b 100644 --- a/graphics/elements/indicators/coremap.lua +++ b/graphics/elements/indicators/coremap.lua @@ -14,6 +14,7 @@ local element = require("graphics.element") ---@field y? integer 1 if omitted -- new core map box +---@nodiscard ---@param args core_map_args ---@return graphics_element element, element_id id local function core_map(args) diff --git a/graphics/elements/indicators/data.lua b/graphics/elements/indicators/data.lua index d19fab0..66d45dc 100644 --- a/graphics/elements/indicators/data.lua +++ b/graphics/elements/indicators/data.lua @@ -19,6 +19,7 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors -- new data indicator +---@nodiscard ---@param args data_indicator_args ---@return graphics_element element, element_id id local function data(args) diff --git a/graphics/elements/indicators/hbar.lua b/graphics/elements/indicators/hbar.lua index a05cdb6..2d9b110 100644 --- a/graphics/elements/indicators/hbar.lua +++ b/graphics/elements/indicators/hbar.lua @@ -17,6 +17,7 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors -- new horizontal bar +---@nodiscard ---@param args hbar_args ---@return graphics_element element, element_id id local function hbar(args) diff --git a/graphics/elements/indicators/icon.lua b/graphics/elements/indicators/icon.lua index 0c71d29..f31479d 100644 --- a/graphics/elements/indicators/icon.lua +++ b/graphics/elements/indicators/icon.lua @@ -20,6 +20,7 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors -- new icon indicator +---@nodiscard ---@param args icon_indicator_args ---@return graphics_element element, element_id id local function icon(args) diff --git a/graphics/elements/indicators/light.lua b/graphics/elements/indicators/light.lua index 3695553..e764ad9 100644 --- a/graphics/elements/indicators/light.lua +++ b/graphics/elements/indicators/light.lua @@ -18,6 +18,7 @@ local flasher = require("graphics.flasher") ---@field fg_bg? cpair foreground/background colors -- new indicator light +---@nodiscard ---@param args indicator_light_args ---@return graphics_element element, element_id id local function indicator_light(args) diff --git a/graphics/elements/indicators/power.lua b/graphics/elements/indicators/power.lua index e76f690..1d727ae 100644 --- a/graphics/elements/indicators/power.lua +++ b/graphics/elements/indicators/power.lua @@ -18,6 +18,7 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors -- new power indicator +---@nodiscard ---@param args power_indicator_args ---@return graphics_element element, element_id id local function power(args) diff --git a/graphics/elements/indicators/rad.lua b/graphics/elements/indicators/rad.lua index 86ec856..2e4ad56 100644 --- a/graphics/elements/indicators/rad.lua +++ b/graphics/elements/indicators/rad.lua @@ -19,6 +19,7 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors -- new radiation indicator +---@nodiscard ---@param args rad_indicator_args ---@return graphics_element element, element_id id local function rad(args) diff --git a/graphics/elements/indicators/state.lua b/graphics/elements/indicators/state.lua index 386910c..10d081b 100644 --- a/graphics/elements/indicators/state.lua +++ b/graphics/elements/indicators/state.lua @@ -20,6 +20,7 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors -- new state indicator +---@nodiscard ---@param args state_indicator_args ---@return graphics_element element, element_id id local function state_indicator(args) diff --git a/graphics/elements/indicators/trilight.lua b/graphics/elements/indicators/trilight.lua index 2c61fb7..543ebf5 100644 --- a/graphics/elements/indicators/trilight.lua +++ b/graphics/elements/indicators/trilight.lua @@ -20,6 +20,7 @@ local flasher = require("graphics.flasher") ---@field fg_bg? cpair foreground/background colors -- new tri-state indicator light +---@nodiscard ---@param args tristate_indicator_light_args ---@return graphics_element element, element_id id local function tristate_indicator_light(args) diff --git a/graphics/elements/indicators/vbar.lua b/graphics/elements/indicators/vbar.lua index be7d9e4..fe7f9bc 100644 --- a/graphics/elements/indicators/vbar.lua +++ b/graphics/elements/indicators/vbar.lua @@ -15,6 +15,7 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors -- new vertical bar +---@nodiscard ---@param args vbar_args ---@return graphics_element element, element_id id local function vbar(args) diff --git a/graphics/elements/rectangle.lua b/graphics/elements/rectangle.lua index 09c20da..6422cbc 100644 --- a/graphics/elements/rectangle.lua +++ b/graphics/elements/rectangle.lua @@ -144,7 +144,7 @@ local function rectangle(args) e.window.blit(spaces, blit_fg, blit_bg_top_bot) end else - if (args.thin == true) then + if args.thin == true then e.window.blit(p_s, blit_fg_sides, blit_bg_sides) else e.window.blit(p_s, blit_fg, blit_bg_sides) diff --git a/graphics/elements/tiling.lua b/graphics/elements/tiling.lua index 86af96d..a97438a 100644 --- a/graphics/elements/tiling.lua +++ b/graphics/elements/tiling.lua @@ -60,7 +60,7 @@ local function tiling(args) -- create pattern for y = start_y, inner_height + (start_y - 1) do e.window.setCursorPos(start_x, y) - for x = 1, inner_width do + for _ = 1, inner_width do if alternator then if even then e.window.blit(" ", "00", fill_a .. fill_a) diff --git a/graphics/flasher.lua b/graphics/flasher.lua index b5eed69..0a3d9ea 100644 --- a/graphics/flasher.lua +++ b/graphics/flasher.lua @@ -21,8 +21,7 @@ local active = false local registry = { {}, {}, {} } -- one registry table per period local callback_counter = 0 --- blink registered indicators --- +-- blink registered indicators
-- this assumes it is called every 250ms, it does no checking of time on its own local function callback_250ms() if active then @@ -55,8 +54,7 @@ function flasher.clear() registry = { {}, {}, {} } end --- register a function to be called on the selected blink period --- +-- register a function to be called on the selected blink period
-- times are not strictly enforced, but all with a given period will be set at the same time ---@param f function function to call each period ---@param period PERIOD time period option (1, 2, or 3) diff --git a/install_manifest.json b/install_manifest.json index f68183c..1f0e40f 100644 --- a/install_manifest.json +++ b/install_manifest.json @@ -2,10 +2,10 @@ "versions": { "bootloader": "0.2", "comms": "1.4.0", - "reactor-plc": "beta-v0.11.1", - "rtu": "beta-v0.11.2", - "supervisor": "beta-v0.12.2", - "coordinator": "beta-v0.10.1", + "reactor-plc": "v0.12.0", + "rtu": "v0.12.1", + "supervisor": "v0.13.1", + "coordinator": "v0.11.0", "pocket": "alpha-v0.0.0" }, "files": { @@ -177,13 +177,13 @@ }, "sizes": { "system": 1982, - "common": 88163, - "graphics": 99360, + "common": 88565, + "graphics": 99858, "lockbox": 100797, - "reactor-plc": 75902, - "rtu": 81679, - "supervisor": 268416, - "coordinator": 181783, + "reactor-plc": 75621, + "rtu": 85496, + "supervisor": 270182, + "coordinator": 183279, "pocket": 335 } } \ No newline at end of file diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index f6bbe5d..973ed66 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,4 +1,5 @@ local comms = require("scada-common.comms") +local const = require("scada-common.constants") local log = require("scada-common.log") local ppm = require("scada-common.ppm") local types = require("scada-common.types") @@ -6,15 +7,17 @@ local util = require("scada-common.util") local plc = {} -local rps_status_t = types.rps_status_t +local RPS_TRIP_CAUSE = types.RPS_TRIP_CAUSE -local PROTOCOLS = comms.PROTOCOLS -local DEVICE_TYPES = comms.DEVICE_TYPES +local PROTOCOL = comms.PROTOCOL +local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK -local RPLC_TYPES = comms.RPLC_TYPES -local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES +local RPLC_TYPE = comms.RPLC_TYPE +local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local AUTO_ACK = comms.PLC_AUTO_ACK +local RPS_LIMITS = const.RPS_LIMITS + local print = util.print local println = util.println local print_ts = util.print_ts @@ -25,21 +28,10 @@ local println_ts = util.println_ts local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active." local PCALL_START_MSG = "pcall: Reactor is already active." --- RPS SAFETY CONSTANTS - -local MAX_DAMAGE_PERCENT = 90 -local MAX_DAMAGE_TEMPERATURE = 1200 -local MIN_COOLANT_FILL = 0.10 -local MAX_WASTE_FILL = 0.8 -local MAX_HEATED_COLLANT_FILL = 0.95 - --- END RPS SAFETY CONSTANTS - ---- RPS: Reactor Protection System ---- ---- identifies dangerous states and SCRAMs reactor if warranted ---- ---- autonomous from main SCADA supervisor/coordinator control +-- RPS: Reactor Protection System
+-- identifies dangerous states and SCRAMs reactor if warranted
+-- autonomous from main SCADA supervisor/coordinator control +---@nodiscard ---@param reactor table ---@param is_formed boolean function plc.rps_init(reactor, is_formed) @@ -59,24 +51,20 @@ function plc.rps_init(reactor, is_formed) } local self = { - reactor = reactor, state = { false, false, false, false, false, false, false, false, false, false, false, false }, reactor_enabled = false, enabled_at = 0, formed = is_formed, force_disabled = false, tripped = false, - trip_cause = "ok" ---@type rps_trip_cause + trip_cause = "ok" ---@type rps_trip_cause } - ---@class rps - local public = {} - -- PRIVATE FUNCTIONS -- -- set reactor access fault flag local function _set_fault() - if self.reactor.__p_last_fault() ~= "Terminated" then + if reactor.__p_last_fault() ~= "Terminated" then self.state[state_keys.fault] = true end end @@ -88,7 +76,7 @@ function plc.rps_init(reactor, is_formed) -- check if the reactor is formed local function _is_formed() - local formed = self.reactor.isFormed() + local formed = reactor.isFormed() if formed == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() @@ -103,7 +91,7 @@ function plc.rps_init(reactor, is_formed) -- check if the reactor is force disabled local function _is_force_disabled() - local disabled = self.reactor.isForceDisabled() + local disabled = reactor.isForceDisabled() if disabled == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() @@ -118,77 +106,80 @@ function plc.rps_init(reactor, is_formed) -- check for critical damage local function _damage_critical() - local damage_percent = self.reactor.getDamagePercent() + local damage_percent = reactor.getDamagePercent() if damage_percent == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() elseif not self.state[state_keys.dmg_crit] then - self.state[state_keys.dmg_crit] = damage_percent >= MAX_DAMAGE_PERCENT + self.state[state_keys.dmg_crit] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT end end -- check if the reactor is at a critically high temperature local function _high_temp() -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 - local temp = self.reactor.getTemperature() + local temp = reactor.getTemperature() if temp == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() elseif not self.state[state_keys.high_temp] then - self.state[state_keys.high_temp] = temp >= MAX_DAMAGE_TEMPERATURE + self.state[state_keys.high_temp] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE end end -- check if there is no coolant (<2% filled) local function _no_coolant() - local coolant_filled = self.reactor.getCoolantFilledPercentage() + local coolant_filled = reactor.getCoolantFilledPercentage() if coolant_filled == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() elseif not self.state[state_keys.no_coolant] then - self.state[state_keys.no_coolant] = coolant_filled < MIN_COOLANT_FILL + self.state[state_keys.no_coolant] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL end end -- check for excess waste (>80% filled) local function _excess_waste() - local w_filled = self.reactor.getWasteFilledPercentage() + local w_filled = reactor.getWasteFilledPercentage() if w_filled == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() elseif not self.state[state_keys.ex_waste] then - self.state[state_keys.ex_waste] = w_filled > MAX_WASTE_FILL + self.state[state_keys.ex_waste] = w_filled > RPS_LIMITS.MAX_WASTE_FILL end end -- check for heated coolant backup (>95% filled) local function _excess_heated_coolant() - local hc_filled = self.reactor.getHeatedCoolantFilledPercentage() + local hc_filled = reactor.getHeatedCoolantFilledPercentage() if hc_filled == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() elseif not self.state[state_keys.ex_hcoolant] then - self.state[state_keys.ex_hcoolant] = hc_filled > MAX_HEATED_COLLANT_FILL + self.state[state_keys.ex_hcoolant] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL end end -- check if there is no fuel local function _insufficient_fuel() - local fuel = self.reactor.getFuel() + local fuel = reactor.getFuelFilledPercentage() if fuel == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() elseif not self.state[state_keys.no_fuel] then - self.state[state_keys.no_fuel] = fuel == 0 + self.state[state_keys.no_fuel] = fuel <= RPS_LIMITS.NO_FUEL_FILL end end -- PUBLIC FUNCTIONS -- + ---@class rps + local public = {} + -- re-link a reactor after a peripheral re-connect ----@diagnostic disable-next-line: redefined-local - function public.reconnect_reactor(reactor) - self.reactor = reactor + ---@param new_reactor table reconnected reactor + function public.reconnect_reactor(new_reactor) + reactor = new_reactor end -- trip for lost peripheral @@ -222,8 +213,8 @@ function plc.rps_init(reactor, is_formed) function public.scram() log.info("RPS: reactor SCRAM") - self.reactor.scram() - if self.reactor.__p_is_faulted() and (self.reactor.__p_last_fault() ~= PCALL_SCRAM_MSG) then + reactor.scram() + if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_SCRAM_MSG) then log.error("RPS: failed reactor SCRAM") return false else @@ -239,8 +230,8 @@ function plc.rps_init(reactor, is_formed) if not self.tripped then log.info("RPS: reactor start") - self.reactor.activate() - if self.reactor.__p_is_faulted() and (self.reactor.__p_last_fault() ~= PCALL_START_MSG) then + reactor.activate() + if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_START_MSG) then log.error("RPS: failed reactor start") else self.reactor_enabled = true @@ -260,7 +251,7 @@ function plc.rps_init(reactor, is_formed) -- clear automatic SCRAM if it was the cause if self.tripped and self.trip_cause == "automatic" then self.state[state_keys.automatic] = true - self.trip_cause = rps_status_t.ok + self.trip_cause = RPS_TRIP_CAUSE.OK self.tripped = false log.debug("RPS: cleared automatic SCRAM for re-activation") @@ -270,9 +261,10 @@ function plc.rps_init(reactor, is_formed) end -- check all safety conditions - ---@return boolean tripped, rps_status_t trip_status, boolean first_trip + ---@nodiscard + ---@return boolean tripped, rps_trip_cause trip_status, boolean first_trip function public.check() - local status = rps_status_t.ok + local status = RPS_TRIP_CAUSE.OK local was_tripped = self.tripped local first_trip = false @@ -298,47 +290,47 @@ function plc.rps_init(reactor, is_formed) status = self.trip_cause elseif self.state[state_keys.sys_fail] then log.warning("RPS: system failure, reactor not formed") - status = rps_status_t.sys_fail + status = RPS_TRIP_CAUSE.SYS_FAIL elseif self.state[state_keys.force_disabled] then log.warning("RPS: reactor was force disabled") - status = rps_status_t.force_disabled + status = RPS_TRIP_CAUSE.FORCE_DISABLED elseif self.state[state_keys.dmg_crit] then log.warning("RPS: damage critical") - status = rps_status_t.dmg_crit + status = RPS_TRIP_CAUSE.DMG_CRIT elseif self.state[state_keys.high_temp] then log.warning("RPS: high temperature") - status = rps_status_t.high_temp + status = RPS_TRIP_CAUSE.HIGH_TEMP elseif self.state[state_keys.no_coolant] then log.warning("RPS: no coolant") - status = rps_status_t.no_coolant + status = RPS_TRIP_CAUSE.NO_COOLANT elseif self.state[state_keys.ex_waste] then log.warning("RPS: full waste") - status = rps_status_t.ex_waste + status = RPS_TRIP_CAUSE.EX_WASTE elseif self.state[state_keys.ex_hcoolant] then log.warning("RPS: heated coolant backup") - status = rps_status_t.ex_hcoolant + status = RPS_TRIP_CAUSE.EX_HCOOLANT elseif self.state[state_keys.no_fuel] then log.warning("RPS: no fuel") - status = rps_status_t.no_fuel + status = RPS_TRIP_CAUSE.NO_FUEL elseif self.state[state_keys.fault] then log.warning("RPS: reactor access fault") - status = rps_status_t.fault + status = RPS_TRIP_CAUSE.FAULT elseif self.state[state_keys.timeout] then log.warning("RPS: supervisor connection timeout") - status = rps_status_t.timeout + status = RPS_TRIP_CAUSE.TIMEOUT elseif self.state[state_keys.manual] then log.warning("RPS: manual SCRAM requested") - status = rps_status_t.manual + status = RPS_TRIP_CAUSE.MANUAL elseif self.state[state_keys.automatic] then log.warning("RPS: automatic SCRAM requested") - status = rps_status_t.automatic + status = RPS_TRIP_CAUSE.AUTOMATIC else self.tripped = false - self.trip_cause = rps_status_t.ok + self.trip_cause = RPS_TRIP_CAUSE.OK end -- if a new trip occured... - if (not was_tripped) and (status ~= rps_status_t.ok) then + if (not was_tripped) and (status ~= RPS_TRIP_CAUSE.OK) then first_trip = true self.tripped = true self.trip_cause = status @@ -359,16 +351,23 @@ function plc.rps_init(reactor, is_formed) return self.tripped, status, first_trip end + ---@nodiscard function public.status() return self.state end + ---@nodiscard function public.is_tripped() return self.tripped end + ---@nodiscard function public.get_trip_cause() return self.trip_cause end + ---@nodiscard function public.is_active() return self.reactor_enabled end + ---@nodiscard function public.is_formed() return self.formed end + ---@nodiscard function public.is_force_disabled() return self.force_disabled end -- get the runtime of the reactor if active, or the last runtime if disabled + ---@nodiscard ---@return integer runtime time since last enable function public.get_runtime() return util.trinary(self.reactor_enabled, util.time_ms() - self.enabled_at, self.last_runtime) end @@ -376,7 +375,7 @@ function plc.rps_init(reactor, is_formed) ---@param quiet? boolean true to suppress the info log message function public.reset(quiet) self.tripped = false - self.trip_cause = rps_status_t.ok + self.trip_cause = RPS_TRIP_CAUSE.OK for i = 1, #self.state do self.state[i] = false @@ -390,8 +389,8 @@ function plc.rps_init(reactor, is_formed) self.state[state_keys.automatic] = false self.state[state_keys.timeout] = false - if self.trip_cause == rps_status_t.automatic or self.trip_cause == rps_status_t.timeout then - self.trip_cause = rps_status_t.ok + if self.trip_cause == RPS_TRIP_CAUSE.AUTOMATIC or self.trip_cause == RPS_TRIP_CAUSE.TIMEOUT then + self.trip_cause = RPS_TRIP_CAUSE.OK self.tripped = false log.info("RPS: auto reset") @@ -402,6 +401,7 @@ function plc.rps_init(reactor, is_formed) end -- Reactor PLC Communications +---@nodiscard ---@param id integer reactor ID ---@param version string PLC version ---@param modem table modem device @@ -415,10 +415,6 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, local self = { seq_num = 0, r_seq_num = nil, - modem = modem, - s_port = server_port, - l_port = local_port, - reactor = reactor, scrammed = false, linked = false, last_est_ack = ESTABLISH_ACK.ALLOW, @@ -428,46 +424,43 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, max_burn_rate = nil } - ---@class plc_comms - local public = {} - comms.set_trusted_range(range) -- PRIVATE FUNCTIONS -- -- configure modem channels local function _conf_channels() - self.modem.closeAll() - self.modem.open(self.l_port) + modem.closeAll() + modem.open(local_port) end _conf_channels() -- send an RPLC packet - ---@param msg_type RPLC_TYPES + ---@param msg_type RPLC_TYPE ---@param msg table local function _send(msg_type, msg) local s_pkt = comms.scada_packet() local r_pkt = comms.rplc_packet() r_pkt.make(id, msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable()) + s_pkt.make(self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) - self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) + modem.transmit(server_port, local_port, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end -- send a SCADA management packet - ---@param msg_type SCADA_MGMT_TYPES + ---@param msg_type SCADA_MGMT_TYPE ---@param msg table local function _send_mgmt(msg_type, msg) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) + modem.transmit(server_port, local_port, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end @@ -500,21 +493,21 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, } local tasks = { - function () data_table[1] = self.reactor.getStatus() end, - function () data_table[2] = self.reactor.getBurnRate() end, - function () data_table[3] = self.reactor.getActualBurnRate() end, - function () data_table[4] = self.reactor.getTemperature() end, - function () data_table[5] = self.reactor.getDamagePercent() end, - function () data_table[6] = self.reactor.getBoilEfficiency() end, - function () data_table[7] = self.reactor.getEnvironmentalLoss() end, - function () fuel = self.reactor.getFuel() end, - function () data_table[9] = self.reactor.getFuelFilledPercentage() end, - function () waste = self.reactor.getWaste() end, - function () data_table[11] = self.reactor.getWasteFilledPercentage() end, - function () coolant = self.reactor.getCoolant() end, - function () data_table[14] = self.reactor.getCoolantFilledPercentage() end, - function () hcoolant = self.reactor.getHeatedCoolant() end, - function () data_table[17] = self.reactor.getHeatedCoolantFilledPercentage() end + function () data_table[1] = reactor.getStatus() end, + function () data_table[2] = reactor.getBurnRate() end, + function () data_table[3] = reactor.getActualBurnRate() end, + function () data_table[4] = reactor.getTemperature() end, + function () data_table[5] = reactor.getDamagePercent() end, + function () data_table[6] = reactor.getBoilEfficiency() end, + function () data_table[7] = reactor.getEnvironmentalLoss() end, + function () fuel = reactor.getFuel() end, + function () data_table[9] = reactor.getFuelFilledPercentage() end, + function () waste = reactor.getWaste() end, + function () data_table[11] = reactor.getWasteFilledPercentage() end, + function () coolant = reactor.getCoolant() end, + function () data_table[14] = reactor.getCoolantFilledPercentage() end, + function () hcoolant = reactor.getHeatedCoolant() end, + function () data_table[17] = reactor.getHeatedCoolantFilledPercentage() end } parallel.waitForAll(table.unpack(tasks)) @@ -537,7 +530,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, data_table[16] = hcoolant.amount end - return data_table, self.reactor.__p_is_faulted() + return data_table, reactor.__p_is_faulted() end -- update the status cache if changed @@ -569,11 +562,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- keep alive ack ---@param srv_time integer local function _send_keep_alive_ack(srv_time) - _send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() }) + _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) end -- general ack - ---@param msg_type RPLC_TYPES + ---@param msg_type RPLC_TYPE ---@param status boolean|integer local function _send_ack(msg_type, status) _send(msg_type, { status }) @@ -587,25 +580,25 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, local mek_data = { false, 0, 0, 0, min_pos, max_pos, 0, 0, 0, 0, 0, 0, 0, 0 } local tasks = { - function () mek_data[1] = self.reactor.getLength() end, - function () mek_data[2] = self.reactor.getWidth() end, - function () mek_data[3] = self.reactor.getHeight() end, - function () mek_data[4] = self.reactor.getMinPos() end, - function () mek_data[5] = self.reactor.getMaxPos() end, - function () mek_data[6] = self.reactor.getHeatCapacity() end, - function () mek_data[7] = self.reactor.getFuelAssemblies() end, - function () mek_data[8] = self.reactor.getFuelSurfaceArea() end, - function () mek_data[9] = self.reactor.getFuelCapacity() end, - function () mek_data[10] = self.reactor.getWasteCapacity() end, - function () mek_data[11] = self.reactor.getCoolantCapacity() end, - function () mek_data[12] = self.reactor.getHeatedCoolantCapacity() end, - function () mek_data[13] = self.reactor.getMaxBurnRate() end + function () mek_data[1] = reactor.getLength() end, + function () mek_data[2] = reactor.getWidth() end, + function () mek_data[3] = reactor.getHeight() end, + function () mek_data[4] = reactor.getMinPos() end, + function () mek_data[5] = reactor.getMaxPos() end, + function () mek_data[6] = reactor.getHeatCapacity() end, + function () mek_data[7] = reactor.getFuelAssemblies() end, + function () mek_data[8] = reactor.getFuelSurfaceArea() end, + function () mek_data[9] = reactor.getFuelCapacity() end, + function () mek_data[10] = reactor.getWasteCapacity() end, + function () mek_data[11] = reactor.getCoolantCapacity() end, + function () mek_data[12] = reactor.getHeatedCoolantCapacity() end, + function () mek_data[13] = reactor.getMaxBurnRate() end } parallel.waitForAll(table.unpack(tasks)) - if not self.reactor.__p_is_faulted() then - _send(RPLC_TYPES.MEK_STRUCT, mek_data) + if not reactor.__p_is_faulted() then + _send(RPLC_TYPE.MEK_STRUCT, mek_data) self.resend_build = false else log.error("failed to send structure: PPM fault") @@ -614,19 +607,20 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- PUBLIC FUNCTIONS -- + ---@class plc_comms + local public = {} + -- reconnect a newly connected modem - ---@param modem table ----@diagnostic disable-next-line: redefined-local - function public.reconnect_modem(modem) - self.modem = modem + ---@param new_modem table + function public.reconnect_modem(new_modem) + modem = new_modem _conf_channels() end -- reconnect a newly connected reactor - ---@param reactor table ----@diagnostic disable-next-line: redefined-local - function public.reconnect_reactor(reactor) - self.reactor = reactor + ---@param new_reactor table + function public.reconnect_reactor(new_reactor) + reactor = new_reactor self.status_cache = nil self.resend_build = true self.max_burn_rate = nil @@ -643,12 +637,12 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, function public.close() conn_watchdog.cancel() public.unlink() - _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) + _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) end -- attempt to establish link with supervisor function public.send_link_req() - _send_mgmt(SCADA_MGMT_TYPES.ESTABLISH, { comms.version, version, DEVICE_TYPES.PLC, id }) + _send_mgmt(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PLC, id }) end -- send live status information @@ -664,7 +658,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, mek_data = self.status_cache end - heating_rate = self.reactor.getHeatingRate() + heating_rate = reactor.getHeatingRate() end local sys_status = { @@ -677,35 +671,29 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, mek_data -- mekanism status data } - _send(RPLC_TYPES.STATUS, sys_status) + _send(RPLC_TYPE.STATUS, sys_status) - if self.resend_build then - _send_struct() - end + if self.resend_build then _send_struct() end end end -- send reactor protection system status function public.send_rps_status() if self.linked then - _send(RPLC_TYPES.RPS_STATUS, { rps.is_tripped(), rps.get_trip_cause(), table.unpack(rps.status()) }) + _send(RPLC_TYPE.RPS_STATUS, { rps.is_tripped(), rps.get_trip_cause(), table.unpack(rps.status()) }) end end -- send reactor protection system alarm - ---@param cause rps_status_t reactor protection system status + ---@param cause rps_trip_cause reactor protection system status function public.send_rps_alarm(cause) if self.linked then - local rps_alarm = { - cause, - table.unpack(rps.status()) - } - - _send(RPLC_TYPES.RPS_ALARM, rps_alarm) + _send(RPLC_TYPE.RPS_ALARM, { cause, table.unpack(rps.status()) }) end end -- parse an RPLC packet + ---@nodiscard ---@param side string ---@param sender integer ---@param reply_to integer @@ -721,13 +709,13 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, if s_pkt.is_valid() then -- get as RPLC packet - if s_pkt.protocol() == PROTOCOLS.RPLC then + if s_pkt.protocol() == PROTOCOL.RPLC then local rplc_pkt = comms.rplc_packet() if rplc_pkt.decode(s_pkt) then pkt = rplc_pkt.get() end -- get as SCADA management packet - elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then + elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then local mgmt_pkt = comms.mgmt_packet() if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() @@ -745,7 +733,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, ---@param plc_state plc_state PLC state ---@param setpoints setpoints setpoint control table function public.handle_packet(packet, plc_state, setpoints) - if packet ~= nil and packet.scada_frame.local_port() == self.l_port then + if packet.scada_frame.local_port() == local_port then -- check sequence number if self.r_seq_num == nil then self.r_seq_num = packet.scada_frame.seq_num() @@ -762,18 +750,19 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, local protocol = packet.scada_frame.protocol() -- handle packet - if protocol == PROTOCOLS.RPLC then + if protocol == PROTOCOL.RPLC then + ---@cast packet rplc_frame if self.linked then - if packet.type == RPLC_TYPES.STATUS then + if packet.type == RPLC_TYPE.STATUS then -- request of full status, clear cache first self.status_cache = nil public.send_status(plc_state.no_reactor, plc_state.reactor_formed) log.debug("sent out status cache again, did supervisor miss it?") - elseif packet.type == RPLC_TYPES.MEK_STRUCT then + elseif packet.type == RPLC_TYPE.MEK_STRUCT then -- request for physical structure _send_struct() log.debug("sent out structure again, did supervisor miss it?") - elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then + elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then -- set the burn rate if (packet.length == 2) and (type(packet.data[1]) == "number") then local success = false @@ -782,7 +771,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- if no known max burn rate, check again if self.max_burn_rate == nil then - self.max_burn_rate = self.reactor.getMaxBurnRate() + self.max_burn_rate = reactor.getMaxBurnRate() end -- if we know our max burn rate, update current burn rate setpoint if in range @@ -793,8 +782,8 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, setpoints.burn_rate = burn_rate success = true else - self.reactor.setBurnRate(burn_rate) - success = not self.reactor.__p_is_faulted() + reactor.setBurnRate(burn_rate) + success = not reactor.__p_is_faulted() end else log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate) @@ -805,29 +794,29 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, else log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate") end - elseif packet.type == RPLC_TYPES.RPS_ENABLE then + elseif packet.type == RPLC_TYPE.RPS_ENABLE then -- enable the reactor self.scrammed = false _send_ack(packet.type, rps.activate()) - elseif packet.type == RPLC_TYPES.RPS_SCRAM then + elseif packet.type == RPLC_TYPE.RPS_SCRAM then -- disable the reactor per manual request self.scrammed = true rps.trip_manual() _send_ack(packet.type, true) - elseif packet.type == RPLC_TYPES.RPS_ASCRAM then + elseif packet.type == RPLC_TYPE.RPS_ASCRAM then -- disable the reactor per automatic request self.scrammed = true rps.trip_auto() _send_ack(packet.type, true) - elseif packet.type == RPLC_TYPES.RPS_RESET then + elseif packet.type == RPLC_TYPE.RPS_RESET then -- reset the RPS status rps.reset() _send_ack(packet.type, true) - elseif packet.type == RPLC_TYPES.RPS_AUTO_RESET then + elseif packet.type == RPLC_TYPE.RPS_AUTO_RESET then -- reset automatic SCRAM and timeout trips rps.auto_reset() _send_ack(packet.type, true) - elseif packet.type == RPLC_TYPES.AUTO_BURN_RATE then + elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then -- automatic control requested a new burn rate if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then local ack = AUTO_ACK.FAIL @@ -837,7 +826,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- if no known max burn rate, check again if self.max_burn_rate == nil then - self.max_burn_rate = self.reactor.getMaxBurnRate() + self.max_burn_rate = reactor.getMaxBurnRate() end -- if we know our max burn rate, update current burn rate setpoint if in range @@ -848,9 +837,8 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, log.debug("AUTO: stopping the reactor to meet 0.0 burn rate") if rps.scram() then ack = AUTO_ACK.ZERO_DIS_OK - self.auto_last_disable = util.time_ms() else - log.debug("AUTO: automatic reactor stop failed") + log.warning("AUTO: automatic reactor stop failed") end else ack = AUTO_ACK.ZERO_DIS_OK @@ -860,12 +848,12 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- activate the reactor log.debug("AUTO: activating the reactor") - self.reactor.setBurnRate(0.01) - if self.reactor.__p_is_faulted() then - log.debug("AUTO: failed to reset burn rate for auto activation") + reactor.setBurnRate(0.01) + if reactor.__p_is_faulted() then + log.warning("AUTO: failed to reset burn rate for auto activation") else if not rps.auto_activate() then - log.debug("AUTO: automatic reactor activation failed") + log.warning("AUTO: automatic reactor activation failed") end end end @@ -879,8 +867,8 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, ack = AUTO_ACK.RAMP_SET_OK else log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate)) - self.reactor.setBurnRate(burn_rate) - ack = util.trinary(self.reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK) + reactor.setBurnRate(burn_rate) + ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK) end end else @@ -898,9 +886,10 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, else log.debug("discarding RPLC packet before linked") end - elseif protocol == PROTOCOLS.SCADA_MGMT then + elseif protocol == PROTOCOL.SCADA_MGMT then + ---@cast packet mgmt_frame if self.linked then - if packet.type == SCADA_MGMT_TYPES.ESTABLISH then + if packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- link request confirmation if packet.length == 1 then log.debug("received unsolicited establish response") @@ -933,7 +922,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, else log.debug("SCADA_MGMT establish packet length mismatch") end - elseif packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then + elseif packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back if packet.length == 1 and type(packet.data[1]) == "number" then local timestamp = packet.data[1] @@ -949,7 +938,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, else log.debug("SCADA_MGMT keep alive packet length/type mismatch") end - elseif packet.type == SCADA_MGMT_TYPES.CLOSE then + elseif packet.type == SCADA_MGMT_TYPE.CLOSE then -- handle session close conn_watchdog.cancel() public.unlink() @@ -958,14 +947,14 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, else log.warning("received unsupported SCADA_MGMT packet type " .. packet.type) end - elseif packet.type == SCADA_MGMT_TYPES.ESTABLISH then + elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- link request confirmation if packet.length == 1 then local est_ack = packet.data[1] if est_ack == ESTABLISH_ACK.ALLOW then println_ts("linked!") - log.debug("supervisor establish request approved") + log.info("supervisor establish request approved, PLC is linked") -- reset remote sequence number and cache self.r_seq_num = nil @@ -978,16 +967,16 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, elseif self.last_est_ack ~= est_ack then if est_ack == ESTABLISH_ACK.DENY then println_ts("link request denied, retrying...") - log.debug("establish request denied") + log.info("supervisor establish request denied, retrying") elseif est_ack == ESTABLISH_ACK.COLLISION then println_ts("reactor PLC ID collision (check config), retrying...") - log.warning("establish request collision") + log.warning("establish request collision, retrying") elseif est_ack == ESTABLISH_ACK.BAD_VERSION then println_ts("supervisor version mismatch (try updating), retrying...") - log.warning("establish request version mismatch") + log.warning("establish request version mismatch, retrying") else println_ts("invalid link response, bad channel? retrying...") - log.error("unknown establish request response") + log.error("unknown establish request response, retrying") end end @@ -1006,7 +995,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, end end + ---@nodiscard function public.is_scrammed() return self.scrammed end + ---@nodiscard function public.is_linked() return self.linked end return public diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 9fefc1d..6eecad0 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -14,7 +14,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "beta-v0.11.1" +local R_PLC_VERSION = "v0.12.1" local print = util.print local println = util.println @@ -116,15 +116,15 @@ local function main() -- we need a reactor, can at least do some things even if it isn't formed though if smem_dev.reactor == nil then - println("boot> fission reactor not found"); - log.warning("no reactor on startup") + println("init> fission reactor not found"); + log.warning("init> no reactor on startup") plc_state.init_ok = false plc_state.degraded = true plc_state.no_reactor = true elseif not smem_dev.reactor.isFormed() then - println("boot> fission reactor not formed"); - log.warning("reactor logic adapter present, but reactor is not formed") + println("init> fission reactor not formed"); + log.warning("init> reactor logic adapter present, but reactor is not formed") plc_state.degraded = true plc_state.reactor_formed = false @@ -132,8 +132,8 @@ local function main() -- modem is required if networked if __shared_memory.networked and smem_dev.modem == nil then - println("boot> wireless modem not found") - log.warning("no wireless modem on startup") + println("init> wireless modem not found") + log.warning("init> no wireless modem on startup") -- scram reactor if present and enabled if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then @@ -145,8 +145,7 @@ local function main() plc_state.no_modem = true end - -- PLC init - --- + -- PLC init
--- EVENT_CONSUMER: this function consumes events local function init() if plc_state.init_ok then @@ -169,18 +168,17 @@ local function main() config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) log.debug("init> comms init") else - println("boot> starting in offline mode") - log.debug("init> running without networking") + println("init> starting in offline mode") + log.info("init> running without networking") end ----@diagnostic disable-next-line: param-type-mismatch util.push_event("clock_start") - println("boot> completed") - log.debug("init> boot completed") + println("init> completed") + log.info("init> startup completed") else - println("boot> system in degraded state, awaiting devices...") - log.warning("init> booted in a degraded state, awaiting peripheral connections...") + println("init> system in degraded state, awaiting devices...") + log.warning("init> started in a degraded state, awaiting peripheral connections...") end end diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 1c3c29f..d2708fd 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -28,10 +28,12 @@ local MQ__COMM_CMD = { } -- main thread +---@nodiscard ---@param smem plc_shared_memory ---@param init function function threads.thread__main(smem, init) - local public = {} ---@class thread + ---@class parallel_thread + local public = {} -- execute thread function public.exec() @@ -44,9 +46,9 @@ function threads.thread__main(smem, init) local loop_clock = util.new_clock(MAIN_CLOCK) -- load in from shared memory - local networked = smem.networked - local plc_state = smem.plc_state - local plc_dev = smem.plc_dev + local networked = smem.networked + local plc_state = smem.plc_state + local plc_dev = smem.plc_dev -- event loop while true do @@ -266,7 +268,6 @@ function threads.thread__main(smem, init) -- this thread cannot be slept because it will miss events (namely "terminate" otherwise) if not plc_state.shutdown then log.info("main thread restarting now...") ----@diagnostic disable-next-line: param-type-mismatch util.push_event("clock_start") end end @@ -276,9 +277,11 @@ function threads.thread__main(smem, init) end -- RPS operation thread +---@nodiscard ---@param smem plc_shared_memory function threads.thread__rps(smem) - local public = {} ---@class thread + ---@class parallel_thread + local public = {} -- execute thread function public.exec() @@ -297,10 +300,10 @@ function threads.thread__rps(smem) -- thread loop while true do -- get plc_sys fields (may have been set late due to degraded boot) - local rps = smem.plc_sys.rps - local plc_comms = smem.plc_sys.plc_comms + local rps = smem.plc_sys.rps + local plc_comms = smem.plc_sys.plc_comms -- get reactor, may have changed do to disconnect/reconnect - local reactor = plc_dev.reactor + local reactor = plc_dev.reactor -- RPS checks if plc_state.init_ok then @@ -415,9 +418,11 @@ function threads.thread__rps(smem) end -- communications sender thread +---@nodiscard ---@param smem plc_shared_memory function threads.thread__comms_tx(smem) - local public = {} ---@class thread + ---@class parallel_thread + local public = {} -- execute thread function public.exec() @@ -489,9 +494,11 @@ function threads.thread__comms_tx(smem) end -- communications handler thread +---@nodiscard ---@param smem plc_shared_memory function threads.thread__comms_rx(smem) - local public = {} ---@class thread + ---@class parallel_thread + local public = {} -- execute thread function public.exec() @@ -562,10 +569,12 @@ function threads.thread__comms_rx(smem) return public end --- apply setpoints +-- ramp control outputs to desired setpoints +---@nodiscard ---@param smem plc_shared_memory function threads.thread__setpoint_control(smem) - local public = {} ---@class thread + ---@class parallel_thread + local public = {} -- execute thread function public.exec() diff --git a/rtu/dev/boilerv_rtu.lua b/rtu/dev/boilerv_rtu.lua index ed7cdb5..b93d412 100644 --- a/rtu/dev/boilerv_rtu.lua +++ b/rtu/dev/boilerv_rtu.lua @@ -3,6 +3,7 @@ local rtu = require("rtu.rtu") local boilerv_rtu = {} -- create new boiler (mek 10.1+) device +---@nodiscard ---@param boiler table function boilerv_rtu.new(boiler) local unit = rtu.init_unit() diff --git a/rtu/dev/envd_rtu.lua b/rtu/dev/envd_rtu.lua index c09ee0c..ba4758a 100644 --- a/rtu/dev/envd_rtu.lua +++ b/rtu/dev/envd_rtu.lua @@ -3,6 +3,7 @@ local rtu = require("rtu.rtu") local envd_rtu = {} -- create new environment detector device +---@nodiscard ---@param envd table function envd_rtu.new(envd) local unit = rtu.init_unit() diff --git a/rtu/dev/imatrix_rtu.lua b/rtu/dev/imatrix_rtu.lua index 6e99453..29405b8 100644 --- a/rtu/dev/imatrix_rtu.lua +++ b/rtu/dev/imatrix_rtu.lua @@ -3,6 +3,7 @@ local rtu = require("rtu.rtu") local imatrix_rtu = {} -- create new induction matrix (mek 10.1+) device +---@nodiscard ---@param imatrix table function imatrix_rtu.new(imatrix) local unit = rtu.init_unit() diff --git a/rtu/dev/redstone_rtu.lua b/rtu/dev/redstone_rtu.lua index 13ca83b..da7db6b 100644 --- a/rtu/dev/redstone_rtu.lua +++ b/rtu/dev/redstone_rtu.lua @@ -1,7 +1,7 @@ -local rtu = require("rtu.rtu") - local rsio = require("scada-common.rsio") +local rtu = require("rtu.rtu") + local redstone_rtu = {} local IO_LVL = rsio.IO_LVL @@ -10,14 +10,15 @@ local digital_read = rsio.digital_read local digital_write = rsio.digital_write -- create new redstone device +---@nodiscard function redstone_rtu.new() local unit = rtu.init_unit() -- get RTU interface local interface = unit.interface() + -- extends rtu_device; fields added manually to please Lua diagnostics ---@class rtu_rs_device - --- extends rtu_device; fields added manually to please Lua diagnostics local public = { io_count = interface.io_count, read_coil = interface.read_coil, diff --git a/rtu/dev/sna_rtu.lua b/rtu/dev/sna_rtu.lua index a4c250f..0339794 100644 --- a/rtu/dev/sna_rtu.lua +++ b/rtu/dev/sna_rtu.lua @@ -2,7 +2,8 @@ local rtu = require("rtu.rtu") local sna_rtu = {} --- create new solar neutron activator (sna) device +-- create new solar neutron activator (SNA) device +---@nodiscard ---@param sna table function sna_rtu.new(sna) local unit = rtu.init_unit() diff --git a/rtu/dev/sps_rtu.lua b/rtu/dev/sps_rtu.lua index 3b7fdf1..ba0a18c 100644 --- a/rtu/dev/sps_rtu.lua +++ b/rtu/dev/sps_rtu.lua @@ -2,7 +2,8 @@ local rtu = require("rtu.rtu") local sps_rtu = {} --- create new super-critical phase shifter (sps) device +-- create new super-critical phase shifter (SPS) device +---@nodiscard ---@param sps table function sps_rtu.new(sps) local unit = rtu.init_unit() diff --git a/rtu/dev/turbinev_rtu.lua b/rtu/dev/turbinev_rtu.lua index 191427d..eba310c 100644 --- a/rtu/dev/turbinev_rtu.lua +++ b/rtu/dev/turbinev_rtu.lua @@ -3,6 +3,7 @@ local rtu = require("rtu.rtu") local turbinev_rtu = {} -- create new turbine (mek 10.1+) device +---@nodiscard ---@param turbine table function turbinev_rtu.new(turbine) local unit = rtu.init_unit() diff --git a/rtu/modbus.lua b/rtu/modbus.lua index 5411f37..20c5939 100644 --- a/rtu/modbus.lua +++ b/rtu/modbus.lua @@ -7,22 +7,15 @@ local MODBUS_FCODE = types.MODBUS_FCODE local MODBUS_EXCODE = types.MODBUS_EXCODE -- new modbus comms handler object +---@nodiscard ---@param rtu_dev rtu_device|rtu_rs_device RTU device ---@param use_parallel_read boolean whether or not to use parallel calls when reading function modbus.new(rtu_dev, use_parallel_read) - local self = { - rtu = rtu_dev, - use_parallel = use_parallel_read - } - - ---@class modbus - local public = {} - local insert = table.insert - -- read a span of coils (digital outputs) - -- + -- read a span of coils (digital outputs)
-- returns a table of readings or a MODBUS_EXCODE error code + ---@nodiscard ---@param c_addr_start integer ---@param count integer ---@return boolean ok, table|MODBUS_EXCODE readings @@ -30,20 +23,20 @@ function modbus.new(rtu_dev, use_parallel_read) local tasks = {} local readings = {} ---@type table|MODBUS_EXCODE local access_fault = false - local _, coils, _, _ = self.rtu.io_count() + local _, coils, _, _ = rtu_dev.io_count() local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0) if return_ok then for i = 1, count do local addr = c_addr_start + i - 1 - if self.use_parallel then + if use_parallel_read then insert(tasks, function () - local reading, fault = self.rtu.read_coil(addr) + local reading, fault = rtu_dev.read_coil(addr) if fault then access_fault = true else readings[i] = reading end end) else - readings[i], access_fault = self.rtu.read_coil(addr) + readings[i], access_fault = rtu_dev.read_coil(addr) if access_fault then return_ok = false @@ -54,7 +47,7 @@ function modbus.new(rtu_dev, use_parallel_read) end -- run parallel tasks if configured - if self.use_parallel then + if use_parallel_read then parallel.waitForAll(table.unpack(tasks)) end @@ -69,9 +62,9 @@ function modbus.new(rtu_dev, use_parallel_read) return return_ok, readings end - -- read a span of discrete inputs (digital inputs) - -- + -- read a span of discrete inputs (digital inputs)
-- returns a table of readings or a MODBUS_EXCODE error code + ---@nodiscard ---@param di_addr_start integer ---@param count integer ---@return boolean ok, table|MODBUS_EXCODE readings @@ -79,20 +72,20 @@ function modbus.new(rtu_dev, use_parallel_read) local tasks = {} local readings = {} ---@type table|MODBUS_EXCODE local access_fault = false - local discrete_inputs, _, _, _ = self.rtu.io_count() + local discrete_inputs, _, _, _ = rtu_dev.io_count() local return_ok = ((di_addr_start + count) <= (discrete_inputs + 1)) and (count > 0) if return_ok then for i = 1, count do local addr = di_addr_start + i - 1 - if self.use_parallel then + if use_parallel_read then insert(tasks, function () - local reading, fault = self.rtu.read_di(addr) + local reading, fault = rtu_dev.read_di(addr) if fault then access_fault = true else readings[i] = reading end end) else - readings[i], access_fault = self.rtu.read_di(addr) + readings[i], access_fault = rtu_dev.read_di(addr) if access_fault then return_ok = false @@ -103,7 +96,7 @@ function modbus.new(rtu_dev, use_parallel_read) end -- run parallel tasks if configured - if self.use_parallel then + if use_parallel_read then parallel.waitForAll(table.unpack(tasks)) end @@ -118,9 +111,9 @@ function modbus.new(rtu_dev, use_parallel_read) return return_ok, readings end - -- read a span of holding registers (analog outputs) - -- + -- read a span of holding registers (analog outputs)
-- returns a table of readings or a MODBUS_EXCODE error code + ---@nodiscard ---@param hr_addr_start integer ---@param count integer ---@return boolean ok, table|MODBUS_EXCODE readings @@ -128,20 +121,20 @@ function modbus.new(rtu_dev, use_parallel_read) local tasks = {} local readings = {} ---@type table|MODBUS_EXCODE local access_fault = false - local _, _, _, hold_regs = self.rtu.io_count() + local _, _, _, hold_regs = rtu_dev.io_count() local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0) if return_ok then for i = 1, count do local addr = hr_addr_start + i - 1 - if self.use_parallel then + if use_parallel_read then insert(tasks, function () - local reading, fault = self.rtu.read_holding_reg(addr) + local reading, fault = rtu_dev.read_holding_reg(addr) if fault then access_fault = true else readings[i] = reading end end) else - readings[i], access_fault = self.rtu.read_holding_reg(addr) + readings[i], access_fault = rtu_dev.read_holding_reg(addr) if access_fault then return_ok = false @@ -152,7 +145,7 @@ function modbus.new(rtu_dev, use_parallel_read) end -- run parallel tasks if configured - if self.use_parallel then + if use_parallel_read then parallel.waitForAll(table.unpack(tasks)) end @@ -167,9 +160,9 @@ function modbus.new(rtu_dev, use_parallel_read) return return_ok, readings end - -- read a span of input registers (analog inputs) - -- + -- read a span of input registers (analog inputs)
-- returns a table of readings or a MODBUS_EXCODE error code + ---@nodiscard ---@param ir_addr_start integer ---@param count integer ---@return boolean ok, table|MODBUS_EXCODE readings @@ -177,20 +170,20 @@ function modbus.new(rtu_dev, use_parallel_read) local tasks = {} local readings = {} ---@type table|MODBUS_EXCODE local access_fault = false - local _, _, input_regs, _ = self.rtu.io_count() + local _, _, input_regs, _ = rtu_dev.io_count() local return_ok = ((ir_addr_start + count) <= (input_regs + 1)) and (count > 0) if return_ok then for i = 1, count do local addr = ir_addr_start + i - 1 - if self.use_parallel then + if use_parallel_read then insert(tasks, function () - local reading, fault = self.rtu.read_input_reg(addr) + local reading, fault = rtu_dev.read_input_reg(addr) if fault then access_fault = true else readings[i] = reading end end) else - readings[i], access_fault = self.rtu.read_input_reg(addr) + readings[i], access_fault = rtu_dev.read_input_reg(addr) if access_fault then return_ok = false @@ -201,7 +194,7 @@ function modbus.new(rtu_dev, use_parallel_read) end -- run parallel tasks if configured - if self.use_parallel then + if use_parallel_read then parallel.waitForAll(table.unpack(tasks)) end @@ -217,16 +210,17 @@ function modbus.new(rtu_dev, use_parallel_read) end -- write a single coil (digital output) + ---@nodiscard ---@param c_addr integer ---@param value any ---@return boolean ok, MODBUS_EXCODE local function _5_write_single_coil(c_addr, value) local response = nil - local _, coils, _, _ = self.rtu.io_count() + local _, coils, _, _ = rtu_dev.io_count() local return_ok = c_addr <= coils if return_ok then - local access_fault = self.rtu.write_coil(c_addr, value) + local access_fault = rtu_dev.write_coil(c_addr, value) if access_fault then return_ok = false @@ -240,16 +234,17 @@ function modbus.new(rtu_dev, use_parallel_read) end -- write a single holding register (analog output) + ---@nodiscard ---@param hr_addr integer ---@param value any ---@return boolean ok, MODBUS_EXCODE local function _6_write_single_holding_register(hr_addr, value) local response = nil - local _, _, _, hold_regs = self.rtu.io_count() + local _, _, _, hold_regs = rtu_dev.io_count() local return_ok = hr_addr <= hold_regs if return_ok then - local access_fault = self.rtu.write_holding_reg(hr_addr, value) + local access_fault = rtu_dev.write_holding_reg(hr_addr, value) if access_fault then return_ok = false @@ -263,19 +258,20 @@ function modbus.new(rtu_dev, use_parallel_read) end -- write multiple coils (digital outputs) + ---@nodiscard ---@param c_addr_start integer ---@param values any ---@return boolean ok, MODBUS_EXCODE local function _15_write_multiple_coils(c_addr_start, values) local response = nil - local _, coils, _, _ = self.rtu.io_count() + local _, coils, _, _ = rtu_dev.io_count() local count = #values local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0) if return_ok then for i = 1, count do local addr = c_addr_start + i - 1 - local access_fault = self.rtu.write_coil(addr, values[i]) + local access_fault = rtu_dev.write_coil(addr, values[i]) if access_fault then return_ok = false @@ -291,19 +287,20 @@ function modbus.new(rtu_dev, use_parallel_read) end -- write multiple holding registers (analog outputs) + ---@nodiscard ---@param hr_addr_start integer ---@param values any ---@return boolean ok, MODBUS_EXCODE local function _16_write_multiple_holding_registers(hr_addr_start, values) local response = nil - local _, _, _, hold_regs = self.rtu.io_count() + local _, _, _, hold_regs = rtu_dev.io_count() local count = #values local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0) if return_ok then for i = 1, count do local addr = hr_addr_start + i - 1 - local access_fault = self.rtu.write_holding_reg(addr, values[i]) + local access_fault = rtu_dev.write_holding_reg(addr, values[i]) if access_fault then return_ok = false @@ -318,7 +315,11 @@ function modbus.new(rtu_dev, use_parallel_read) return return_ok, response end + ---@class modbus + local public = {} + -- validate a request without actually executing it + ---@nodiscard ---@param packet modbus_frame ---@return boolean return_code, modbus_packet reply function public.check_request(packet) @@ -360,6 +361,7 @@ function modbus.new(rtu_dev, use_parallel_read) end -- handle a MODBUS TCP packet and generate a reply + ---@nodiscard ---@param packet modbus_frame ---@return boolean return_code, modbus_packet reply function public.handle_packet(packet) @@ -420,6 +422,7 @@ function modbus.new(rtu_dev, use_parallel_read) end -- return a SERVER_DEVICE_BUSY error reply +---@nodiscard ---@param packet modbus_frame MODBUS packet frame ---@return modbus_packet reply function modbus.reply__srv_device_busy(packet) @@ -432,6 +435,7 @@ function modbus.reply__srv_device_busy(packet) end -- return a NEG_ACKNOWLEDGE error reply +---@nodiscard ---@param packet modbus_frame MODBUS packet frame ---@return modbus_packet reply function modbus.reply__neg_ack(packet) @@ -444,6 +448,7 @@ function modbus.reply__neg_ack(packet) end -- return a GATEWAY_PATH_UNAVAILABLE error reply +---@nodiscard ---@param packet modbus_frame MODBUS packet frame ---@return modbus_packet reply function modbus.reply__gw_unavailable(packet) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index ff81659..1344a3e 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -1,24 +1,26 @@ local comms = require("scada-common.comms") local ppm = require("scada-common.ppm") local log = require("scada-common.log") +local types = require("scada-common.types") local util = require("scada-common.util") local modbus = require("rtu.modbus") local rtu = {} -local PROTOCOLS = comms.PROTOCOLS -local DEVICE_TYPES = comms.DEVICE_TYPES +local PROTOCOL = comms.PROTOCOL +local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK -local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES -local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES +local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local print = util.print local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts --- create a new RTU +-- create a new RTU unit +---@nodiscard function rtu.init_unit() local self = { discrete_inputs = {}, @@ -152,14 +154,13 @@ function rtu.init_unit() -- public RTU device access -- get the public interface to this RTU - function protected.interface() - return public - end + function protected.interface() return public end return protected end -- RTU Communications +---@nodiscard ---@param version string RTU version ---@param modem table modem device ---@param local_port integer local listening port @@ -168,20 +169,12 @@ end ---@param conn_watchdog watchdog watchdog reference function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog) local self = { - version = version, seq_num = 0, r_seq_num = nil, txn_id = 0, - modem = modem, - s_port = server_port, - l_port = local_port, - conn_watchdog = conn_watchdog, last_est_ack = ESTABLISH_ACK.ALLOW } - ---@class rtu_comms - local public = {} - local insert = table.insert comms.set_trusted_range(range) @@ -190,50 +183,46 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog -- configure modem channels local function _conf_channels() - self.modem.closeAll() - self.modem.open(self.l_port) + modem.closeAll() + modem.open(local_port) end _conf_channels() -- send a scada management packet - ---@param msg_type SCADA_MGMT_TYPES + ---@param msg_type SCADA_MGMT_TYPE ---@param msg table local function _send(msg_type, msg) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) + modem.transmit(server_port, local_port, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end -- keep alive ack ---@param srv_time integer local function _send_keep_alive_ack(srv_time) - _send(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() }) + _send(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) end -- generate device advertisement table + ---@nodiscard ---@param units table ---@return table advertisement local function _generate_advertisement(units) local advertisement = {} for i = 1, #units do - local unit = units[i] --@type rtu_unit_registry_entry - local type = comms.rtu_t_to_unit_type(unit.type) + local unit = units[i] ---@type rtu_unit_registry_entry - if type ~= nil then - local advert = { - type, - unit.index, - unit.reactor - } + if unit.type ~= nil then + local advert = { unit.type, unit.index, unit.reactor } - if type == RTU_UNIT_TYPES.REDSTONE then + if unit.type == RTU_UNIT_TYPE.REDSTONE then insert(advert, unit.device) end @@ -246,20 +235,22 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog -- PUBLIC FUNCTIONS -- + ---@class rtu_comms + local public = {} + -- send a MODBUS TCP packet ---@param m_pkt modbus_packet function public.send_modbus(m_pkt) local s_pkt = comms.scada_packet() - s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable()) - self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) + s_pkt.make(self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) + modem.transmit(server_port, local_port, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end -- reconnect a newly connected modem - ---@param modem table ----@diagnostic disable-next-line: redefined-local - function public.reconnect_modem(modem) - self.modem = modem + ---@param new_modem table + function public.reconnect_modem(new_modem) + modem = new_modem _conf_channels() end @@ -273,30 +264,31 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog -- close the connection to the server ---@param rtu_state rtu_state function public.close(rtu_state) - self.conn_watchdog.cancel() + conn_watchdog.cancel() public.unlink(rtu_state) - _send(SCADA_MGMT_TYPES.CLOSE, {}) + _send(SCADA_MGMT_TYPE.CLOSE, {}) end -- send establish request (includes advertisement) ---@param units table function public.send_establish(units) - _send(SCADA_MGMT_TYPES.ESTABLISH, { comms.version, self.version, DEVICE_TYPES.RTU, _generate_advertisement(units) }) + _send(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.RTU, _generate_advertisement(units) }) end -- send capability advertisement ---@param units table function public.send_advertisement(units) - _send(SCADA_MGMT_TYPES.RTU_ADVERT, _generate_advertisement(units)) + _send(SCADA_MGMT_TYPE.RTU_ADVERT, _generate_advertisement(units)) end -- notify that a peripheral was remounted ---@param unit_index integer RTU unit ID function public.send_remounted(unit_index) - _send(SCADA_MGMT_TYPES.RTU_DEV_REMOUNT, { unit_index }) + _send(SCADA_MGMT_TYPE.RTU_DEV_REMOUNT, { unit_index }) end -- parse a MODBUS/SCADA packet + ---@nodiscard ---@param side string ---@param sender integer ---@param reply_to integer @@ -312,13 +304,13 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog if s_pkt.is_valid() then -- get as MODBUS TCP packet - if s_pkt.protocol() == PROTOCOLS.MODBUS_TCP then + if s_pkt.protocol() == PROTOCOL.MODBUS_TCP then local m_pkt = comms.modbus_packet() if m_pkt.decode(s_pkt) then pkt = m_pkt.get() end -- get as SCADA management packet - elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then + elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then local mgmt_pkt = comms.mgmt_packet() if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() @@ -333,10 +325,10 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog -- handle a MODBUS/SCADA packet ---@param packet modbus_frame|mgmt_frame - ---@param units table + ---@param units table RTU units ---@param rtu_state rtu_state function public.handle_packet(packet, units, rtu_state) - if packet ~= nil and packet.scada_frame.local_port() == self.l_port then + if packet.scada_frame.local_port() == local_port then -- check sequence number if self.r_seq_num == nil then self.r_seq_num = packet.scada_frame.seq_num() @@ -348,14 +340,14 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog end -- feed watchdog on valid sequence number - self.conn_watchdog.feed() + conn_watchdog.feed() local protocol = packet.scada_frame.protocol() - if protocol == PROTOCOLS.MODBUS_TCP then + if protocol == PROTOCOL.MODBUS_TCP then + ---@cast packet modbus_frame if rtu_state.linked then local return_code = false ----@diagnostic disable-next-line: param-type-mismatch local reply = modbus.reply__neg_ack(packet) -- handle MODBUS instruction @@ -365,20 +357,17 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog if unit.name == "redstone_io" then -- immediately execute redstone RTU requests ----@diagnostic disable-next-line: param-type-mismatch return_code, reply = unit.modbus_io.handle_packet(packet) if not return_code then log.warning("requested MODBUS operation failed" .. unit_dbg_tag) end else -- check validity then pass off to unit comms thread ----@diagnostic disable-next-line: param-type-mismatch return_code, reply = unit.modbus_io.check_request(packet) if return_code then -- check if there are more than 3 active transactions -- still queue the packet, but this may indicate a problem if unit.pkt_queue.length() > 3 then ----@diagnostic disable-next-line: param-type-mismatch reply = modbus.reply__srv_device_busy(packet) log.debug("queueing new request with " .. unit.pkt_queue.length() .. " transactions already in the queue" .. unit_dbg_tag) @@ -392,7 +381,6 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog end else -- unit ID out of range? ----@diagnostic disable-next-line: param-type-mismatch reply = modbus.reply__gw_unavailable(packet) log.error("received MODBUS packet for non-existent unit") end @@ -401,9 +389,10 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog else log.debug("discarding MODBUS packet before linked") end - elseif protocol == PROTOCOLS.SCADA_MGMT then + elseif protocol == PROTOCOL.SCADA_MGMT then + ---@cast packet mgmt_frame -- SCADA management packet - if packet.type == SCADA_MGMT_TYPES.ESTABLISH then + if packet.type == SCADA_MGMT_TYPE.ESTABLISH then if packet.length == 1 then local est_ack = packet.data[1] @@ -419,10 +408,10 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog if est_ack == ESTABLISH_ACK.BAD_VERSION then -- version mismatch println_ts("supervisor comms version mismatch (try updating), retrying...") - log.warning("supervisor connection denied due to comms version mismatch") + log.warning("supervisor connection denied due to comms version mismatch, retrying") else println_ts("supervisor connection denied, retrying...") - log.warning("supervisor connection denied") + log.warning("supervisor connection denied, retrying") end end @@ -434,7 +423,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog log.debug("SCADA_MGMT establish packet length mismatch") end elseif rtu_state.linked then - if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then + if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back if packet.length == 1 and type(packet.data[1]) == "number" then local timestamp = packet.data[1] @@ -450,15 +439,15 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog else log.debug("SCADA_MGMT keep alive packet length/type mismatch") end - elseif packet.type == SCADA_MGMT_TYPES.CLOSE then + elseif packet.type == SCADA_MGMT_TYPE.CLOSE then -- close connection - self.conn_watchdog.cancel() + conn_watchdog.cancel() public.unlink(rtu_state) println_ts("server connection closed by remote host") log.warning("server connection closed by remote host") - elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then + elseif packet.type == SCADA_MGMT_TYPE.RTU_ADVERT then -- request for capabilities again - public.send_advertisement(units) + public.send_advertisement(units) else -- not supported log.warning("received unsupported SCADA_MGMT message type " .. packet.type) diff --git a/rtu/startup.lua b/rtu/startup.lua index 418921c..aff3a22 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -25,9 +25,9 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "beta-v0.11.2" +local RTU_VERSION = "v0.12.1" -local rtu_t = types.rtu_t +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local print = util.print local println = util.println @@ -132,13 +132,17 @@ local function main() -- CHECK: reactor ID must be >= to 1 if (not util.is_int(io_reactor)) or (io_reactor < 0) then - println(util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 0")) + local message = util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 0") + println(message) + log.fatal(message) return false end -- CHECK: io table exists if type(io_table) ~= "table" then - println(util.c("configure> redstone entry #", entry_idx, " no IO table found")) + local message = util.c("configure> redstone entry #", entry_idx, " no IO table found") + println(message) + log.fatal(message) return false end @@ -148,10 +152,10 @@ local function main() local continue = true - -- check for duplicate entries + -- CHECK: no duplicate entries for i = 1, #units do local unit = units[i] ---@type rtu_unit_registry_entry - if unit.reactor == io_reactor and unit.type == rtu_t.redstone then + if unit.reactor == io_reactor and unit.type == RTU_UNIT_TYPE.REDSTONE then -- duplicate entry local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor, " with already defined redstone I/O") @@ -181,7 +185,7 @@ local function main() local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx, " (for reactor ", io_reactor, ")") println(message) - log.error(message) + log.fatal(message) return false else -- link redstone in RTU @@ -224,23 +228,28 @@ local function main() ---@class rtu_unit_registry_entry local unit = { - uid = 0, - name = "redstone_io", - type = rtu_t.redstone, - index = entry_idx, - reactor = io_reactor, - device = capabilities, -- use device field for redstone ports - is_multiblock = false, - formed = nil, ---@type boolean|nil - rtu = rs_rtu, ---@type rtu_device|rtu_rs_device + uid = 0, ---@type integer + name = "redstone_io", ---@type string + type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE + index = entry_idx, ---@type integer + reactor = io_reactor, ---@type integer + device = capabilities, ---@type table use device field for redstone ports + is_multiblock = false, ---@type boolean + formed = nil, ---@type boolean|nil + rtu = rs_rtu, ---@type rtu_device|rtu_rs_device modbus_io = modbus.new(rs_rtu, false), - pkt_queue = nil, ---@type mqueue|nil - thread = nil + pkt_queue = nil, ---@type mqueue|nil + thread = nil ---@type parallel_thread|nil } table.insert(units, unit) - log.debug(util.c("init> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for reactor ", io_reactor)) + local for_message = "facility" + if io_reactor > 0 then + for_message = util.c("reactor ", io_reactor) + end + + log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message)) unit.uid = #units end @@ -254,27 +263,33 @@ local function main() -- CHECK: name is a string if type(name) ~= "string" then - println(util.c("configure> device entry #", i, ": device ", name, " isn't a string")) + local message = util.c("configure> device entry #", i, ": device ", name, " isn't a string") + println(message) + log.fatal(message) return false end -- CHECK: index is an integer >= 1 if (not util.is_int(index)) or (index <= 0) then - println(util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1")) + local message = util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1") + println(message) + log.fatal(message) return false end -- CHECK: reactor is an integer >= 0 if (not util.is_int(for_reactor)) or (for_reactor < 0) then - println(util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 0")) + local message = util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 0") + println(message) + log.fatal(message) return false end local device = ppm.get_periph(name) - local type = nil + local type = nil ---@type string|nil local rtu_iface = nil ---@type rtu_device - local rtu_type = "" + local rtu_type = nil ---@type RTU_UNIT_TYPE local is_multiblock = false local formed = nil ---@type boolean|nil @@ -291,7 +306,7 @@ local function main() if type == "boilerValve" then -- boiler multiblock - rtu_type = rtu_t.boiler_valve + rtu_type = RTU_UNIT_TYPE.BOILER_VALVE rtu_iface = boilerv_rtu.new(device) is_multiblock = true formed = device.isFormed() @@ -303,7 +318,7 @@ local function main() end elseif type == "turbineValve" then -- turbine multiblock - rtu_type = rtu_t.turbine_valve + rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE rtu_iface = turbinev_rtu.new(device) is_multiblock = true formed = device.isFormed() @@ -315,7 +330,7 @@ local function main() end elseif type == "inductionPort" then -- induction matrix multiblock - rtu_type = rtu_t.induction_matrix + rtu_type = RTU_UNIT_TYPE.IMATRIX rtu_iface = imatrix_rtu.new(device) is_multiblock = true formed = device.isFormed() @@ -327,7 +342,7 @@ local function main() end elseif type == "spsPort" then -- SPS multiblock - rtu_type = rtu_t.sps + rtu_type = RTU_UNIT_TYPE.SPS rtu_iface = sps_rtu.new(device) is_multiblock = true formed = device.isFormed() @@ -339,15 +354,15 @@ local function main() end elseif type == "solarNeutronActivator" then -- SNA - rtu_type = rtu_t.sna + rtu_type = RTU_UNIT_TYPE.SNA rtu_iface = sna_rtu.new(device) elseif type == "environmentDetector" then -- advanced peripherals environment detector - rtu_type = rtu_t.env_detector + rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR rtu_iface = envd_rtu.new(device) elseif type == ppm.VIRTUAL_DEVICE_TYPE then -- placeholder device - rtu_type = "virtual" + rtu_type = RTU_UNIT_TYPE.VIRTUAL rtu_iface = rtu.init_unit().interface() else local message = util.c("configure> device '", name, "' is not a known type (", type, ")") @@ -358,18 +373,18 @@ local function main() ---@class rtu_unit_registry_entry local rtu_unit = { - uid = 0, - name = name, - type = rtu_type, - index = index, - reactor = for_reactor, - device = device, - is_multiblock = is_multiblock, + uid = 0, ---@type integer + name = name, ---@type string + type = rtu_type, ---@type RTU_UNIT_TYPE + index = index, ---@type integer + reactor = for_reactor, ---@type integer + device = device, ---@type table + is_multiblock = is_multiblock, ---@type boolean formed = formed, ---@type boolean|nil rtu = rtu_iface, ---@type rtu_device|rtu_rs_device modbus_io = modbus.new(rtu_iface, true), pkt_queue = mqueue.new(), ---@type mqueue|nil - thread = nil + thread = nil ---@type parallel_thread|nil } rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit) @@ -377,7 +392,7 @@ local function main() table.insert(units, rtu_unit) if is_multiblock and not formed then - log.debug(util.c("configure> device '", name, "' is not formed")) + log.info(util.c("configure> device '", name, "' is not formed")) end local for_message = "facility" @@ -385,7 +400,7 @@ local function main() for_message = util.c("reactor ", for_reactor) end - log.debug(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", rtu_type, ") [", index, "] for ", for_message)) + log.info(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message)) rtu_unit.uid = #units end @@ -403,12 +418,12 @@ local function main() if configure() then -- start connection watchdog smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) - log.debug("boot> conn watchdog started") + log.debug("startup> conn watchdog started") -- setup comms smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, config.TRUSTED_RANGE, smem_sys.conn_watchdog) - log.debug("boot> comms init") + log.debug("startup> comms init") -- init threads local main_thread = threads.thread__main(__shared_memory) @@ -422,6 +437,8 @@ local function main() end end + log.info("startup> completed") + -- run threads parallel.waitForAll(table.unpack(_threads)) else diff --git a/rtu/threads.lua b/rtu/threads.lua index 7af184e..6b06eb0 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -1,21 +1,21 @@ -local log = require("scada-common.log") -local mqueue = require("scada-common.mqueue") -local ppm = require("scada-common.ppm") -local types = require("scada-common.types") -local util = require("scada-common.util") +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local ppm = require("scada-common.ppm") +local types = require("scada-common.types") +local util = require("scada-common.util") -local boilerv_rtu = require("rtu.dev.boilerv_rtu") -local envd_rtu = require("rtu.dev.envd_rtu") -local imatrix_rtu = require("rtu.dev.imatrix_rtu") -local sna_rtu = require("rtu.dev.sna_rtu") -local sps_rtu = require("rtu.dev.sps_rtu") -local turbinev_rtu = require("rtu.dev.turbinev_rtu") +local boilerv_rtu = require("rtu.dev.boilerv_rtu") +local envd_rtu = require("rtu.dev.envd_rtu") +local imatrix_rtu = require("rtu.dev.imatrix_rtu") +local sna_rtu = require("rtu.dev.sna_rtu") +local sps_rtu = require("rtu.dev.sps_rtu") +local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local modbus = require("rtu.modbus") +local modbus = require("rtu.modbus") local threads = {} -local rtu_t = types.rtu_t +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local print = util.print local println = util.println @@ -26,9 +26,11 @@ local MAIN_CLOCK = 2 -- (2Hz, 40 ticks) local COMMS_SLEEP = 100 -- (100ms, 2 ticks) -- main thread +---@nodiscard ---@param smem rtu_shared_memory function threads.thread__main(smem) - local public = {} ---@class thread + ---@class parallel_thread + local public = {} -- execute thread function public.exec() @@ -93,8 +95,9 @@ function threads.thread__main(smem) -- we are going to let the PPM prevent crashes -- return fault flags/codes to MODBUS queries local unit = units[i] - println_ts(util.c("lost the ", unit.type, " on interface ", unit.name)) - log.warning(util.c("lost the ", unit.type, " unit peripheral on interface ", unit.name)) + local type_name = types.rtu_type_to_string(unit.type) + println_ts(util.c("lost the ", type_name, " on interface ", unit.name)) + log.warning(util.c("lost the ", type_name, " unit peripheral on interface ", unit.name)) break end end @@ -112,9 +115,9 @@ function threads.thread__main(smem) rtu_comms.reconnect_modem(rtu_dev.modem) println_ts("wireless modem reconnected.") - log.info("comms modem reconnected.") + log.info("comms modem reconnected") else - log.info("wired modem reconnected.") + log.info("wired modem reconnected") end else -- relink lost peripheral to correct unit entry @@ -129,51 +132,51 @@ function threads.thread__main(smem) -- found, re-link unit.device = device - if unit.type == "virtual" then + if unit.type == RTU_UNIT_TYPE.VIRTUAL then resend_advert = true if type == "boilerValve" then -- boiler multiblock - unit.type = rtu_t.boiler_valve + unit.type = RTU_UNIT_TYPE.BOILER_VALVE elseif type == "turbineValve" then -- turbine multiblock - unit.type = rtu_t.turbine_valve + unit.type = RTU_UNIT_TYPE.TURBINE_VALVE elseif type == "inductionPort" then -- induction matrix multiblock - unit.type = rtu_t.induction_matrix + unit.type = RTU_UNIT_TYPE.IMATRIX elseif type == "spsPort" then -- SPS multiblock - unit.type = rtu_t.sps + unit.type = RTU_UNIT_TYPE.SPS elseif type == "solarNeutronActivator" then -- SNA - unit.type = rtu_t.sna + unit.type = RTU_UNIT_TYPE.SNA elseif type == "environmentDetector" then -- advanced peripherals environment detector - unit.type = rtu_t.env_detector + unit.type = RTU_UNIT_TYPE.ENV_DETECTOR else resend_advert = false log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")")) end end - if unit.type == rtu_t.boiler_valve then + if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then unit.rtu = boilerv_rtu.new(device) -- if not formed, indexing the multiblock functions would have resulted in a PPM fault unit.formed = util.trinary(device.__p_is_faulted(), false, nil) - elseif unit.type == rtu_t.turbine_valve then + elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then unit.rtu = turbinev_rtu.new(device) -- if not formed, indexing the multiblock functions would have resulted in a PPM fault unit.formed = util.trinary(device.__p_is_faulted(), false, nil) - elseif unit.type == rtu_t.induction_matrix then + elseif unit.type == RTU_UNIT_TYPE.IMATRIX then unit.rtu = imatrix_rtu.new(device) -- if not formed, indexing the multiblock functions would have resulted in a PPM fault unit.formed = util.trinary(device.__p_is_faulted(), false, nil) - elseif unit.type == rtu_t.sps then + elseif unit.type == RTU_UNIT_TYPE.SPS then unit.rtu = sps_rtu.new(device) -- if not formed, indexing the multiblock functions would have resulted in a PPM fault unit.formed = util.trinary(device.__p_is_faulted(), false, nil) - elseif unit.type == rtu_t.sna then + elseif unit.type == RTU_UNIT_TYPE.SNA then unit.rtu = sna_rtu.new(device) - elseif unit.type == rtu_t.env_detector then + elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then unit.rtu = envd_rtu.new(device) else log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true) @@ -185,8 +188,10 @@ function threads.thread__main(smem) unit.modbus_io = modbus.new(unit.rtu, true) - println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) - log.info("reconnected the " .. unit.type .. " on interface " .. unit.name) + local type_name = types.rtu_type_to_string(unit.type) + local message = util.c("reconnected the ", type_name, " on interface ", unit.name) + println_ts(message) + log.info(message) if resend_advert then rtu_comms.send_advertisement(units) @@ -229,22 +234,24 @@ function threads.thread__main(smem) end -- communications handler thread +---@nodiscard ---@param smem rtu_shared_memory function threads.thread__comms(smem) - local public = {} ---@class thread + ---@class parallel_thread + local public = {} -- execute thread function public.exec() log.debug("comms thread start") -- load in from shared memory - local rtu_state = smem.rtu_state - local rtu_comms = smem.rtu_sys.rtu_comms - local units = smem.rtu_sys.units + local rtu_state = smem.rtu_state + local rtu_comms = smem.rtu_sys.rtu_comms + local units = smem.rtu_sys.units - local comms_queue = smem.q.mq_comms + local comms_queue = smem.q.mq_comms - local last_update = util.time() + local last_update = util.time() -- thread loop while true do @@ -301,14 +308,16 @@ function threads.thread__comms(smem) end -- per-unit communications handler thread +---@nodiscard ---@param smem rtu_shared_memory ---@param unit rtu_unit_registry_entry function threads.thread__unit_comms(smem, unit) - local public = {} ---@class thread + ---@class parallel_thread + local public = {} -- execute thread function public.exec() - log.debug("rtu unit thread start -> " .. unit.type .. "(" .. unit.name .. ")") + log.debug(util.c("rtu unit thread start -> ", types.rtu_type_to_string(unit.type), "(", unit.name, ")")) -- load in from shared memory local rtu_state = smem.rtu_state @@ -319,8 +328,8 @@ function threads.thread__unit_comms(smem, unit) local last_f_check = 0 - local detail_name = util.c(unit.type, " (", unit.name, ") [", unit.index, "] for reactor ", unit.reactor) - local short_name = util.c(unit.type, " (", unit.name, ")") + local detail_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ") [", unit.index, "] for reactor ", unit.reactor) + local short_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ")") if packet_queue == nil then log.error("rtu unit thread created without a message queue, exiting...", true) @@ -368,25 +377,25 @@ function threads.thread__unit_comms(smem, unit) local type, device = ppm.mount(iface) if device ~= nil then - if type == "boilerValve" and unit.type == rtu_t.boiler_valve then + if type == "boilerValve" and unit.type == RTU_UNIT_TYPE.BOILER_VALVE then -- boiler multiblock unit.device = device unit.rtu = boilerv_rtu.new(device) unit.formed = device.isFormed() unit.modbus_io = modbus.new(unit.rtu, true) - elseif type == "turbineValve" and unit.type == rtu_t.turbine_valve then + elseif type == "turbineValve" and unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then -- turbine multiblock unit.device = device unit.rtu = turbinev_rtu.new(device) unit.formed = device.isFormed() unit.modbus_io = modbus.new(unit.rtu, true) - elseif type == "inductionPort" and unit.type == rtu_t.induction_matrix then + elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then -- induction matrix multiblock unit.device = device unit.rtu = imatrix_rtu.new(device) unit.formed = device.isFormed() unit.modbus_io = modbus.new(unit.rtu, true) - elseif type == "spsPort" and unit.type == rtu_t.sps then + elseif type == "spsPort" and unit.type == RTU_UNIT_TYPE.SPS then -- SPS multiblock unit.device = device unit.rtu = sps_rtu.new(device) @@ -433,7 +442,7 @@ function threads.thread__unit_comms(smem, unit) end if not rtu_state.shutdown then - log.info(util.c("rtu unit thread ", unit.type, "(", unit.name, " restarting in 5 seconds...")) + log.info(util.c("rtu unit thread ", types.rtu_type_to_string(unit.type), "(", unit.name, " restarting in 5 seconds...")) util.psleep(5) end end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index b275622..ad7c0aa 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -3,21 +3,18 @@ -- local log = require("scada-common.log") -local types = require("scada-common.types") ---@class comms local comms = {} -local rtu_t = types.rtu_t - local insert = table.insert local max_distance = nil comms.version = "1.4.0" ----@alias PROTOCOLS integer -local PROTOCOLS = { +---@enum PROTOCOL +local PROTOCOL = { MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol RPLC = 1, -- reactor PLC protocol SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc @@ -25,8 +22,8 @@ local PROTOCOLS = { COORD_API = 4 -- data/control packets for pocket computers to/from coordinators } ----@alias RPLC_TYPES integer -local RPLC_TYPES = { +---@enum RPLC_TYPE +local RPLC_TYPE = { STATUS = 0, -- reactor/system status MEK_STRUCT = 1, -- mekanism build structure MEK_BURN_RATE = 2, -- set burn rate @@ -40,8 +37,8 @@ local RPLC_TYPES = { AUTO_BURN_RATE = 10 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited } ----@alias SCADA_MGMT_TYPES integer -local SCADA_MGMT_TYPES = { +---@enum SCADA_MGMT_TYPE +local SCADA_MGMT_TYPE = { ESTABLISH = 0, -- establish new connection KEEP_ALIVE = 1, -- keep alive packet w/ RTT CLOSE = 2, -- close a connection @@ -49,8 +46,8 @@ local SCADA_MGMT_TYPES = { RTU_DEV_REMOUNT = 4 -- RTU multiblock possbily changed (formed, unformed) due to PPM remount } ----@alias SCADA_CRDN_TYPES integer -local SCADA_CRDN_TYPES = { +---@enum SCADA_CRDN_TYPE +local SCADA_CRDN_TYPE = { INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator FAC_BUILDS = 1, -- facility RTU builds FAC_STATUS = 2, -- state of facility and facility devices @@ -60,12 +57,11 @@ local SCADA_CRDN_TYPES = { UNIT_CMD = 6 -- command a reactor unit } ----@alias CAPI_TYPES integer -local CAPI_TYPES = { - ESTABLISH = 0 -- initial greeting +---@enum CAPI_TYPE +local CAPI_TYPE = { } ----@alias ESTABLISH_ACK integer +---@enum ESTABLISH_ACK local ESTABLISH_ACK = { ALLOW = 0, -- link approved DENY = 1, -- link denied @@ -73,26 +69,15 @@ local ESTABLISH_ACK = { BAD_VERSION = 3 -- link denied due to comms version mismatch } ----@alias DEVICE_TYPES integer -local DEVICE_TYPES = { +---@enum DEVICE_TYPE +local DEVICE_TYPE = { PLC = 0, -- PLC device type for establish RTU = 1, -- RTU device type for establish SV = 2, -- supervisor device type for establish CRDN = 3 -- coordinator device type for establish } ----@alias RTU_UNIT_TYPES integer -local RTU_UNIT_TYPES = { - REDSTONE = 0, -- redstone I/O - BOILER_VALVE = 1, -- boiler mekanism 10.1+ - TURBINE_VALVE = 2, -- turbine, mekanism 10.1+ - IMATRIX = 3, -- induction matrix - SPS = 4, -- SPS - SNA = 5, -- SNA - ENV_DETECTOR = 6 -- environment detector -} - ----@alias PLC_AUTO_ACK integer +---@enum PLC_AUTO_ACK local PLC_AUTO_ACK = { FAIL = 0, -- failed to set burn rate/burn rate invalid DIRECT_SET_OK = 1, -- successfully set burn rate @@ -100,16 +85,16 @@ local PLC_AUTO_ACK = { ZERO_DIS_OK = 3 -- successfully disabled reactor with < 0.01 burn rate } ----@alias FAC_COMMANDS integer -local FAC_COMMANDS = { +---@enum FAC_COMMAND +local FAC_COMMAND = { SCRAM_ALL = 0, -- SCRAM all reactors STOP = 1, -- stop automatic control START = 2, -- start automatic control ACK_ALL_ALARMS = 3 -- acknowledge all alarms on all units } ----@alias UNIT_COMMANDS integer -local UNIT_COMMANDS = { +---@enum UNIT_COMMAND +local UNIT_COMMAND = { SCRAM = 0, -- SCRAM the reactor START = 1, -- start the reactor RESET_RPS = 2, -- reset the RPS @@ -121,26 +106,25 @@ local UNIT_COMMANDS = { SET_GROUP = 8 -- assign this unit to a group } -comms.PROTOCOLS = PROTOCOLS +comms.PROTOCOL = PROTOCOL -comms.RPLC_TYPES = RPLC_TYPES -comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES -comms.SCADA_CRDN_TYPES = SCADA_CRDN_TYPES -comms.CAPI_TYPES = CAPI_TYPES +comms.RPLC_TYPE = RPLC_TYPE +comms.SCADA_MGMT_TYPE = SCADA_MGMT_TYPE +comms.SCADA_CRDN_TYPE = SCADA_CRDN_TYPE +comms.CAPI_TYPE = CAPI_TYPE comms.ESTABLISH_ACK = ESTABLISH_ACK -comms.DEVICE_TYPES = DEVICE_TYPES -comms.RTU_UNIT_TYPES = RTU_UNIT_TYPES +comms.DEVICE_TYPE = DEVICE_TYPE comms.PLC_AUTO_ACK = PLC_AUTO_ACK -comms.UNIT_COMMANDS = UNIT_COMMANDS -comms.FAC_COMMANDS = FAC_COMMANDS +comms.UNIT_COMMAND = UNIT_COMMAND +comms.FAC_COMMAND = FAC_COMMAND ---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet|capi_packet ---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame|capi_frame --- configure the maximum allowable message receive distance
+-- configure the maximum allowable message receive distance
-- packets received with distances greater than this will be silently discarded ---@param distance integer max modem message distance (less than 1 disables the limit) function comms.set_trusted_range(distance) @@ -152,6 +136,7 @@ function comms.set_trusted_range(distance) end -- generic SCADA packet object +---@nodiscard function comms.scada_packet() local self = { modem_msg_in = nil, @@ -168,7 +153,7 @@ function comms.scada_packet() -- make a SCADA packet ---@param seq_num integer - ---@param protocol PROTOCOLS + ---@param protocol PROTOCOL ---@param payload table function public.make(seq_num, protocol, payload) self.valid = true @@ -180,11 +165,12 @@ function comms.scada_packet() end -- parse in a modem message as a SCADA packet - ---@param side string - ---@param sender integer - ---@param reply_to integer - ---@param message any - ---@param distance integer + ---@param side string modem side + ---@param sender integer sender port + ---@param reply_to integer reply port + ---@param message any message body + ---@param distance integer transmission distance + ---@return boolean valid valid message received function public.receive(side, sender, reply_to, message, distance) self.modem_msg_in = { iface = side, @@ -223,24 +209,34 @@ function comms.scada_packet() -- public accessors -- + ---@nodiscard function public.modem_event() return self.modem_msg_in end + ---@nodiscard function public.raw_sendable() return self.raw end + ---@nodiscard function public.local_port() return self.modem_msg_in.s_port end + ---@nodiscard function public.remote_port() return self.modem_msg_in.r_port end + ---@nodiscard function public.is_valid() return self.valid end + ---@nodiscard function public.seq_num() return self.seq_num end + ---@nodiscard function public.protocol() return self.protocol end + ---@nodiscard function public.length() return self.length end + ---@nodiscard function public.data() return self.payload end return public end --- MODBUS packet +-- MODBUS packet
-- modeled after MODBUS TCP packet +---@nodiscard function comms.modbus_packet() local self = { frame = nil, @@ -248,7 +244,7 @@ function comms.modbus_packet() txn_id = -1, length = 0, unit_id = -1, - func_code = 0, + func_code = 0x80, data = {} } @@ -285,7 +281,7 @@ function comms.modbus_packet() if frame then self.frame = frame - if frame.protocol() == PROTOCOLS.MODBUS_TCP then + if frame.protocol() == PROTOCOL.MODBUS_TCP then local size_ok = frame.length() >= 3 if size_ok then @@ -309,9 +305,11 @@ function comms.modbus_packet() end -- get raw to send + ---@nodiscard function public.raw_sendable() return self.raw end -- get this packet as a frame with an immutable relation to this object + ---@nodiscard function public.get() ---@class modbus_frame local frame = { @@ -330,12 +328,13 @@ function comms.modbus_packet() end -- reactor PLC packet +---@nodiscard function comms.rplc_packet() local self = { frame = nil, raw = {}, id = 0, - type = -1, + type = 0, ---@type RPLC_TYPE length = 0, data = {} } @@ -345,22 +344,22 @@ function comms.rplc_packet() -- check that type is known local function _rplc_type_valid() - return self.type == RPLC_TYPES.STATUS or - self.type == RPLC_TYPES.MEK_STRUCT or - self.type == RPLC_TYPES.MEK_BURN_RATE or - self.type == RPLC_TYPES.RPS_ENABLE or - self.type == RPLC_TYPES.RPS_SCRAM or - self.type == RPLC_TYPES.RPS_ASCRAM or - self.type == RPLC_TYPES.RPS_STATUS or - self.type == RPLC_TYPES.RPS_ALARM or - self.type == RPLC_TYPES.RPS_RESET or - self.type == RPLC_TYPES.RPS_AUTO_RESET or - self.type == RPLC_TYPES.AUTO_BURN_RATE + return self.type == RPLC_TYPE.STATUS or + self.type == RPLC_TYPE.MEK_STRUCT or + self.type == RPLC_TYPE.MEK_BURN_RATE or + self.type == RPLC_TYPE.RPS_ENABLE or + self.type == RPLC_TYPE.RPS_SCRAM or + self.type == RPLC_TYPE.RPS_ASCRAM or + self.type == RPLC_TYPE.RPS_STATUS or + self.type == RPLC_TYPE.RPS_ALARM or + self.type == RPLC_TYPE.RPS_RESET or + self.type == RPLC_TYPE.RPS_AUTO_RESET or + self.type == RPLC_TYPE.AUTO_BURN_RATE end -- make an RPLC packet ---@param id integer - ---@param packet_type RPLC_TYPES + ---@param packet_type RPLC_TYPE ---@param data table function public.make(id, packet_type, data) if type(data) == "table" then @@ -387,7 +386,7 @@ function comms.rplc_packet() if frame then self.frame = frame - if frame.protocol() == PROTOCOLS.RPLC then + if frame.protocol() == PROTOCOL.RPLC then local ok = frame.length() >= 2 if ok then @@ -410,9 +409,11 @@ function comms.rplc_packet() end -- get raw to send + ---@nodiscard function public.raw_sendable() return self.raw end -- get this packet as a frame with an immutable relation to this object + ---@nodiscard function public.get() ---@class rplc_frame local frame = { @@ -430,11 +431,12 @@ function comms.rplc_packet() end -- SCADA management packet +---@nodiscard function comms.mgmt_packet() local self = { frame = nil, raw = {}, - type = -1, + type = 0, ---@type SCADA_MGMT_TYPE length = 0, data = {} } @@ -444,16 +446,16 @@ function comms.mgmt_packet() -- check that type is known local function _scada_type_valid() - return self.type == SCADA_MGMT_TYPES.ESTABLISH or - self.type == SCADA_MGMT_TYPES.KEEP_ALIVE or - self.type == SCADA_MGMT_TYPES.CLOSE or - self.type == SCADA_MGMT_TYPES.REMOTE_LINKED or - self.type == SCADA_MGMT_TYPES.RTU_ADVERT or - self.type == SCADA_MGMT_TYPES.RTU_DEV_REMOUNT + return self.type == SCADA_MGMT_TYPE.ESTABLISH or + self.type == SCADA_MGMT_TYPE.KEEP_ALIVE or + self.type == SCADA_MGMT_TYPE.CLOSE or + self.type == SCADA_MGMT_TYPE.REMOTE_LINKED or + self.type == SCADA_MGMT_TYPE.RTU_ADVERT or + self.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT end -- make a SCADA management packet - ---@param packet_type SCADA_MGMT_TYPES + ---@param packet_type SCADA_MGMT_TYPE ---@param data table function public.make(packet_type, data) if type(data) == "table" then @@ -479,7 +481,7 @@ function comms.mgmt_packet() if frame then self.frame = frame - if frame.protocol() == PROTOCOLS.SCADA_MGMT then + if frame.protocol() == PROTOCOL.SCADA_MGMT then local ok = frame.length() >= 1 if ok then @@ -500,9 +502,11 @@ function comms.mgmt_packet() end -- get raw to send + ---@nodiscard function public.raw_sendable() return self.raw end -- get this packet as a frame with an immutable relation to this object + ---@nodiscard function public.get() ---@class mgmt_frame local frame = { @@ -519,11 +523,12 @@ function comms.mgmt_packet() end -- SCADA coordinator packet +---@nodiscard function comms.crdn_packet() local self = { frame = nil, raw = {}, - type = -1, + type = 0, ---@type SCADA_CRDN_TYPE length = 0, data = {} } @@ -532,18 +537,19 @@ function comms.crdn_packet() local public = {} -- check that type is known + ---@nodiscard local function _crdn_type_valid() - return self.type == SCADA_CRDN_TYPES.INITIAL_BUILDS or - self.type == SCADA_CRDN_TYPES.FAC_BUILDS or - self.type == SCADA_CRDN_TYPES.FAC_STATUS or - self.type == SCADA_CRDN_TYPES.FAC_CMD or - self.type == SCADA_CRDN_TYPES.UNIT_BUILDS or - self.type == SCADA_CRDN_TYPES.UNIT_STATUSES or - self.type == SCADA_CRDN_TYPES.UNIT_CMD + return self.type == SCADA_CRDN_TYPE.INITIAL_BUILDS or + self.type == SCADA_CRDN_TYPE.FAC_BUILDS or + self.type == SCADA_CRDN_TYPE.FAC_STATUS or + self.type == SCADA_CRDN_TYPE.FAC_CMD or + self.type == SCADA_CRDN_TYPE.UNIT_BUILDS or + self.type == SCADA_CRDN_TYPE.UNIT_STATUSES or + self.type == SCADA_CRDN_TYPE.UNIT_CMD end -- make a coordinator packet - ---@param packet_type SCADA_CRDN_TYPES + ---@param packet_type SCADA_CRDN_TYPE ---@param data table function public.make(packet_type, data) if type(data) == "table" then @@ -569,7 +575,7 @@ function comms.crdn_packet() if frame then self.frame = frame - if frame.protocol() == PROTOCOLS.SCADA_CRDN then + if frame.protocol() == PROTOCOL.SCADA_CRDN then local ok = frame.length() >= 1 if ok then @@ -590,9 +596,11 @@ function comms.crdn_packet() end -- get raw to send + ---@nodiscard function public.raw_sendable() return self.raw end -- get this packet as a frame with an immutable relation to this object + ---@nodiscard function public.get() ---@class crdn_frame local frame = { @@ -609,12 +617,13 @@ function comms.crdn_packet() end -- coordinator API (CAPI) packet --- @todo +---@todo implement for pocket access, set enum type for self.type +---@nodiscard function comms.capi_packet() local self = { frame = nil, raw = {}, - type = -1, + type = 0, length = 0, data = {} } @@ -623,12 +632,12 @@ function comms.capi_packet() local public = {} local function _capi_type_valid() - -- @todo + ---@todo return false end -- make a coordinator API packet - ---@param packet_type CAPI_TYPES + ---@param packet_type CAPI_TYPE ---@param data table function public.make(packet_type, data) if type(data) == "table" then @@ -654,7 +663,7 @@ function comms.capi_packet() if frame then self.frame = frame - if frame.protocol() == PROTOCOLS.COORD_API then + if frame.protocol() == PROTOCOL.COORD_API then local ok = frame.length() >= 1 if ok then @@ -675,9 +684,11 @@ function comms.capi_packet() end -- get raw to send + ---@nodiscard function public.raw_sendable() return self.raw end -- get this packet as a frame with an immutable relation to this object + ---@nodiscard function public.get() ---@class capi_frame local frame = { @@ -693,50 +704,4 @@ function comms.capi_packet() return public end --- convert rtu_t to RTU unit type ----@param type rtu_t ----@return RTU_UNIT_TYPES|nil -function comms.rtu_t_to_unit_type(type) - if type == rtu_t.redstone then - return RTU_UNIT_TYPES.REDSTONE - elseif type == rtu_t.boiler_valve then - return RTU_UNIT_TYPES.BOILER_VALVE - elseif type == rtu_t.turbine_valve then - return RTU_UNIT_TYPES.TURBINE_VALVE - elseif type == rtu_t.induction_matrix then - return RTU_UNIT_TYPES.IMATRIX - elseif type == rtu_t.sps then - return RTU_UNIT_TYPES.SPS - elseif type == rtu_t.sna then - return RTU_UNIT_TYPES.SNA - elseif type == rtu_t.env_detector then - return RTU_UNIT_TYPES.ENV_DETECTOR - end - - return nil -end - --- convert RTU unit type to rtu_t ----@param utype RTU_UNIT_TYPES ----@return rtu_t|nil -function comms.advert_type_to_rtu_t(utype) - if utype == RTU_UNIT_TYPES.REDSTONE then - return rtu_t.redstone - elseif utype == RTU_UNIT_TYPES.BOILER_VALVE then - return rtu_t.boiler_valve - elseif utype == RTU_UNIT_TYPES.TURBINE_VALVE then - return rtu_t.turbine_valve - elseif utype == RTU_UNIT_TYPES.IMATRIX then - return rtu_t.induction_matrix - elseif utype == RTU_UNIT_TYPES.SPS then - return rtu_t.sps - elseif utype == RTU_UNIT_TYPES.SNA then - return rtu_t.sna - elseif utype == RTU_UNIT_TYPES.ENV_DETECTOR then - return rtu_t.env_detector - end - - return nil -end - return comms diff --git a/scada-common/constants.lua b/scada-common/constants.lua new file mode 100644 index 0000000..c10722d --- /dev/null +++ b/scada-common/constants.lua @@ -0,0 +1,71 @@ +-- +-- System and Safety Constants +-- + +-- Notes on Radiation +-- - background radiation 0.0000001 Sv/h (99.99 nSv/h) +-- - "green tint" radiation 0.00001 Sv/h (10 uSv/h) +-- - damaging radiation 0.00006 Sv/h (60 uSv/h) + +local constants = {} + +--#region Reactor Protection System (on the PLC) Limits + +local rps = {} + +rps.MAX_DAMAGE_PERCENT = 90 -- damage >= 90% +rps.MAX_DAMAGE_TEMPERATURE = 1200 -- temp >= 1200K +rps.MIN_COOLANT_FILL = 0.10 -- fill < 10% +rps.MAX_WASTE_FILL = 0.8 -- fill > 80% +rps.MAX_HEATED_COLLANT_FILL = 0.95 -- fill > 95% +rps.NO_FUEL_FILL = 0.0 -- fill <= 0% + +constants.RPS_LIMITS = rps + +--#endregion + +--#region Annunciator Limits + +local annunc = {} + +annunc.RCSFlowLow = -2.0 -- flow < -2.0 mB/s +annunc.CoolantLevelLow = 0.4 -- fill < 40% +annunc.ReactorTempHigh = 1000 -- temp > 1000K +annunc.ReactorHighDeltaT = 50 -- rate > 50K/s +annunc.FuelLevelLow = 0.05 -- fill <= 5% +annunc.WasteLevelHigh = 0.85 -- fill >= 85% +annunc.SteamFeedMismatch = 10 -- ±10mB difference between total coolant flow and total steam input rate +annunc.RadiationWarning = 0.00001 -- 10 uSv/h + +constants.ANNUNCIATOR_LIMITS = annunc + +--#endregion + +--#region Supervisor Alarm Limits + +local alarms = {} + +-- unit alarms + +alarms.HIGH_TEMP = 1150 -- temp >= 1150K +alarms.HIGH_WASTE = 0.5 -- fill > 50% +alarms.HIGH_RADIATION = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good + +-- facility alarms + +alarms.CHARGE_HIGH = 1.0 -- once at or above 100% charge +alarms.CHARGE_RE_ENABLE = 0.95 -- once below 95% charge +alarms.FAC_HIGH_RAD = 0.00001 -- 10 uSv/h + +constants.ALARM_LIMITS = alarms + +--#endregion + +--#region Supervisor Constants + +-- milliseconds until turbine flow is assumed to be stable enough to enable coolant checks +constants.FLOW_STABILITY_DELAY_MS = 15000 + +--#endregion + +return constants diff --git a/scada-common/crypto.lua b/scada-common/crypto.lua index 6424bcb..a1053bf 100644 --- a/scada-common/crypto.lua +++ b/scada-common/crypto.lua @@ -70,6 +70,7 @@ function crypto.init(password, server_port) end -- encrypt plaintext +---@nodiscard ---@param plaintext string ---@return table initial_value, string ciphertext function crypto.encrypt(plaintext) @@ -113,6 +114,7 @@ function crypto.encrypt(plaintext) end -- decrypt ciphertext +---@nodiscard ---@param iv string CTR initial value ---@param ciphertext string ciphertext hex ---@return string plaintext @@ -135,6 +137,7 @@ function crypto.decrypt(iv, ciphertext) end -- generate HMAC of message +---@nodiscard ---@param message_hex string initial value concatenated with ciphertext function crypto.hmac(message_hex) local start = util.time() @@ -201,11 +204,12 @@ function crypto.secure_modem(modem) end -- parse in a modem message as a network packet - ---@param side string - ---@param sender integer - ---@param reply_to integer + ---@nodiscard + ---@param side string modem side + ---@param sender integer sender port + ---@param reply_to integer reply port ---@param message any encrypted packet sent with secure_modem.transmit - ---@param distance integer + ---@param distance integer transmission distance ---@return string side, integer sender, integer reply_to, any plaintext_message, integer distance function public.receive(side, sender, reply_to, message, distance) local body = "" diff --git a/scada-common/log.lua b/scada-common/log.lua index cc6dd0a..30f785d 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -18,7 +18,7 @@ log.MODE = MODE -- whether to log debug messages or not local LOG_DEBUG = true -local _log_sys = { +local log_sys = { path = "/log.txt", mode = MODE.APPEND, file = nil, @@ -33,27 +33,25 @@ local free_space = fs.getFreeSpace ---@param write_mode MODE ---@param dmesg_redirect? table terminal/window to direct dmesg to function log.init(path, write_mode, dmesg_redirect) - _log_sys.path = path - _log_sys.mode = write_mode + log_sys.path = path + log_sys.mode = write_mode - if _log_sys.mode == MODE.APPEND then - _log_sys.file = fs.open(path, "a") + if log_sys.mode == MODE.APPEND then + log_sys.file = fs.open(path, "a") else - _log_sys.file = fs.open(path, "w") + log_sys.file = fs.open(path, "w") end if dmesg_redirect then - _log_sys.dmesg_out = dmesg_redirect + log_sys.dmesg_out = dmesg_redirect else - _log_sys.dmesg_out = term.current() + log_sys.dmesg_out = term.current() end end -- direct dmesg output to a monitor/window ---@param window table window or terminal reference -function log.direct_dmesg(window) - _log_sys.dmesg_out = window -end +function log.direct_dmesg(window) log_sys.dmesg_out = window end -- private log write function ---@param msg string @@ -64,8 +62,8 @@ local function _log(msg) -- attempt to write log local status, result = pcall(function () - _log_sys.file.writeLine(stamped) - _log_sys.file.flush() + log_sys.file.writeLine(stamped) + log_sys.file.flush() end) -- if we don't have space, we need to create a new log file @@ -80,18 +78,18 @@ local function _log(msg) end end - if out_of_space or (free_space(_log_sys.path) < 100) then + if out_of_space or (free_space(log_sys.path) < 100) then -- delete the old log file before opening a new one - _log_sys.file.close() - fs.delete(_log_sys.path) + log_sys.file.close() + fs.delete(log_sys.path) -- re-init logger and pass dmesg_out so that it doesn't change - log.init(_log_sys.path, _log_sys.mode, _log_sys.dmesg_out) + log.init(log_sys.path, log_sys.mode, log_sys.dmesg_out) -- leave a message - _log_sys.file.writeLine(time_stamp .. "recycled log file") - _log_sys.file.writeLine(stamped) - _log_sys.file.flush() + log_sys.file.writeLine(time_stamp .. "recycled log file") + log_sys.file.writeLine(stamped) + log_sys.file.flush() end end @@ -109,7 +107,7 @@ function log.dmesg(msg, tag, tag_color) tag = util.strval(tag) local t_stamp = string.format("%12.2f", os.clock()) - local out = _log_sys.dmesg_out + local out = log_sys.dmesg_out if out ~= nil then local out_w, out_h = out.getSize() @@ -197,6 +195,7 @@ function log.dmesg(msg, tag, tag_color) end -- print a dmesg message, but then show remaining seconds instead of timestamp +---@nodiscard ---@param msg string message ---@param tag? string log tag ---@param tag_color? integer log tag color @@ -204,7 +203,7 @@ end function log.dmesg_working(msg, tag, tag_color) local ts_coord = log.dmesg(msg, tag, tag_color) - local out = _log_sys.dmesg_out + local out = log_sys.dmesg_out local width = (ts_coord.x2 - ts_coord.x1) + 1 if out ~= nil then diff --git a/scada-common/mqueue.lua b/scada-common/mqueue.lua index 22bae5d..b48e4ad 100644 --- a/scada-common/mqueue.lua +++ b/scada-common/mqueue.lua @@ -4,7 +4,7 @@ local mqueue = {} ----@alias MQ_TYPE integer +---@enum MQ_TYPE local TYPE = { COMMAND = 0, DATA = 1, @@ -14,6 +14,7 @@ local TYPE = { mqueue.TYPE = TYPE -- create a new message queue +---@nodiscard function mqueue.new() local queue = {} @@ -35,10 +36,13 @@ function mqueue.new() function public.length() return #queue end -- check if queue is empty + ---@nodiscard ---@return boolean is_empty function public.empty() return #queue == 0 end -- check if queue has contents + ---@nodiscard + ---@return boolean has_contents function public.ready() return #queue ~= 0 end -- push a new item onto the queue @@ -68,6 +72,7 @@ function mqueue.new() end -- get an item off the queue + ---@nodiscard ---@return queue_item|nil function public.pop() if #queue > 0 then diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index f69de1e..fe9e026 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -24,7 +24,7 @@ ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE local REPORT_FREQUENCY = 20 -- log every 20 faults per function -local _ppm_sys = { +local ppm_sys = { mounts = {}, next_vid = 0, auto_cf = false, @@ -34,11 +34,9 @@ local _ppm_sys = { mute = false } --- wrap peripheral calls with lua protected call as we don't want a disconnect to crash a program ---- ----also provides peripheral-specific fault checks (auto-clear fault defaults to true) ---- ----assumes iface is a valid peripheral +-- wrap peripheral calls with lua protected call as we don't want a disconnect to crash a program
+-- also provides peripheral-specific fault checks (auto-clear fault defaults to true)
+-- assumes iface is a valid peripheral ---@param iface string CC peripheral interface local function peri_init(iface) local self = { @@ -68,7 +66,7 @@ local function peri_init(iface) if status then -- auto fault clear if self.auto_cf then self.faulted = false end - if _ppm_sys.auto_cf then _ppm_sys.faulted = false end + if ppm_sys.auto_cf then ppm_sys.faulted = false end self.fault_counts[key] = 0 @@ -80,10 +78,10 @@ local function peri_init(iface) self.faulted = true self.last_fault = result - _ppm_sys.faulted = true - _ppm_sys.last_fault = result + ppm_sys.faulted = true + ppm_sys.last_fault = result - if not _ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then + if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then local count_str = "" if self.fault_counts[key] > 0 then count_str = " [" .. self.fault_counts[key] .. " total faults]" @@ -95,7 +93,7 @@ local function peri_init(iface) self.fault_counts[key] = self.fault_counts[key] + 1 if result == "Terminated" then - _ppm_sys.terminate = true + ppm_sys.terminate = true end return ACCESS_FAULT @@ -136,10 +134,10 @@ local function peri_init(iface) self.faulted = true self.last_fault = UNDEFINED_FIELD - _ppm_sys.faulted = true - _ppm_sys.last_fault = UNDEFINED_FIELD + ppm_sys.faulted = true + ppm_sys.last_fault = UNDEFINED_FIELD - if not _ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then + if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then local count_str = "" if self.fault_counts[key] > 0 then count_str = " [" .. self.fault_counts[key] .. " total calls]" @@ -169,48 +167,35 @@ end -- REPORTING -- -- silence error prints -function ppm.disable_reporting() - _ppm_sys.mute = true -end +function ppm.disable_reporting() ppm_sys.mute = true end -- allow error prints -function ppm.enable_reporting() - _ppm_sys.mute = false -end +function ppm.enable_reporting() ppm_sys.mute = false end -- FAULT MEMORY -- -- enable automatically clearing fault flag -function ppm.enable_afc() - _ppm_sys.auto_cf = true -end +function ppm.enable_afc() ppm_sys.auto_cf = true end -- disable automatically clearing fault flag -function ppm.disable_afc() - _ppm_sys.auto_cf = false -end +function ppm.disable_afc() ppm_sys.auto_cf = false end -- clear fault flag -function ppm.clear_fault() - _ppm_sys.faulted = false -end +function ppm.clear_fault() ppm_sys.faulted = false end -- check fault flag -function ppm.is_faulted() - return _ppm_sys.faulted -end +---@nodiscard +function ppm.is_faulted() return ppm_sys.faulted end -- get the last fault message -function ppm.get_last_fault() - return _ppm_sys.last_fault -end +---@nodiscard +function ppm.get_last_fault() return ppm_sys.last_fault end -- TERMINATION -- -- if a caught error was a termination request -function ppm.should_terminate() - return _ppm_sys.terminate -end +---@nodiscard +function ppm.should_terminate() return ppm_sys.terminate end -- MOUNTING -- @@ -218,12 +203,12 @@ end function ppm.mount_all() local ifaces = peripheral.getNames() - _ppm_sys.mounts = {} + ppm_sys.mounts = {} for i = 1, #ifaces do - _ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i]) + ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i]) - log.info(util.c("PPM: found a ", _ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")")) + log.info(util.c("PPM: found a ", ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")")) end if #ifaces == 0 then @@ -232,6 +217,7 @@ function ppm.mount_all() end -- mount a particular device +---@nodiscard ---@param iface string CC peripheral interface ---@return string|nil type, table|nil device function ppm.mount(iface) @@ -241,10 +227,10 @@ function ppm.mount(iface) for i = 1, #ifaces do if iface == ifaces[i] then - _ppm_sys.mounts[iface] = peri_init(iface) + ppm_sys.mounts[iface] = peri_init(iface) - pm_type = _ppm_sys.mounts[iface].type - pm_dev = _ppm_sys.mounts[iface].dev + pm_type = ppm_sys.mounts[iface].type + pm_dev = ppm_sys.mounts[iface].dev log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type)) break @@ -255,26 +241,27 @@ function ppm.mount(iface) end -- mount a virtual, placeholder device (specifically designed for RTU startup with missing devices) +---@nodiscard ---@return string type, table device function ppm.mount_virtual() - local iface = "ppm_vdev_" .. _ppm_sys.next_vid + local iface = "ppm_vdev_" .. ppm_sys.next_vid - _ppm_sys.mounts[iface] = peri_init("__virtual__") - _ppm_sys.next_vid = _ppm_sys.next_vid + 1 + ppm_sys.mounts[iface] = peri_init("__virtual__") + ppm_sys.next_vid = ppm_sys.next_vid + 1 log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface)) - return _ppm_sys.mounts[iface].type, _ppm_sys.mounts[iface].dev + return ppm_sys.mounts[iface].type, ppm_sys.mounts[iface].dev end -- manually unmount a peripheral from the PPM ---@param device table device table function ppm.unmount(device) if device then - for side, data in pairs(_ppm_sys.mounts) do + for side, data in pairs(ppm_sys.mounts) do if data.dev == device then log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", side)) - _ppm_sys.mounts[side] = nil + ppm_sys.mounts[side] = nil break end end @@ -282,6 +269,7 @@ function ppm.unmount(device) end -- handle peripheral_detach event +---@nodiscard ---@param iface string CC peripheral interface ---@return string|nil type, table|nil device function ppm.handle_unmount(iface) @@ -289,7 +277,7 @@ function ppm.handle_unmount(iface) local pm_type = nil -- what got disconnected? - local lost_dev = _ppm_sys.mounts[iface] + local lost_dev = ppm_sys.mounts[iface] if lost_dev then pm_type = lost_dev.type @@ -300,7 +288,7 @@ function ppm.handle_unmount(iface) log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface)) end - _ppm_sys.mounts[iface] = nil + ppm_sys.mounts[iface] = nil return pm_type, pm_dev end @@ -308,23 +296,26 @@ end -- GENERAL ACCESSORS -- -- list all available peripherals +---@nodiscard ---@return table names function ppm.list_avail() return peripheral.getNames() end -- list mounted peripherals +---@nodiscard ---@return table mounts function ppm.list_mounts() - return _ppm_sys.mounts + return ppm_sys.mounts end -- get a mounted peripheral side/interface by device table +---@nodiscard ---@param device table device table ---@return string|nil iface CC peripheral interface function ppm.get_iface(device) if device then - for side, data in pairs(_ppm_sys.mounts) do + for side, data in pairs(ppm_sys.mounts) do if data.dev == device then return side end end end @@ -333,30 +324,33 @@ function ppm.get_iface(device) end -- get a mounted peripheral by side/interface +---@nodiscard ---@param iface string CC peripheral interface ---@return table|nil device function table function ppm.get_periph(iface) - if _ppm_sys.mounts[iface] then - return _ppm_sys.mounts[iface].dev + if ppm_sys.mounts[iface] then + return ppm_sys.mounts[iface].dev else return nil end end -- get a mounted peripheral type by side/interface +---@nodiscard ---@param iface string CC peripheral interface ---@return string|nil type function ppm.get_type(iface) - if _ppm_sys.mounts[iface] then - return _ppm_sys.mounts[iface].type + if ppm_sys.mounts[iface] then + return ppm_sys.mounts[iface].type else return nil end end -- get all mounted peripherals by type +---@nodiscard ---@param name string type name ---@return table devices device function tables function ppm.get_all_devices(name) local devices = {} - for _, data in pairs(_ppm_sys.mounts) do + for _, data in pairs(ppm_sys.mounts) do if data.type == name then table.insert(devices, data.dev) end @@ -366,12 +360,13 @@ function ppm.get_all_devices(name) end -- get a mounted peripheral by type (if multiple, returns the first) +---@nodiscard ---@param name string type name ---@return table|nil device function table function ppm.get_device(name) local device = nil - for side, data in pairs(_ppm_sys.mounts) do + for _, data in pairs(ppm_sys.mounts) do if data.type == name then device = data.dev break @@ -384,20 +379,21 @@ end -- SPECIFIC DEVICE ACCESSORS -- -- get the fission reactor (if multiple, returns the first) +---@nodiscard ---@return table|nil reactor function table function ppm.get_fission_reactor() return ppm.get_device("fissionReactorLogicAdapter") end --- get the wireless modem (if multiple, returns the first) --- +-- get the wireless modem (if multiple, returns the first)
-- if this is in a CraftOS emulated environment, wired modems will be used instead +---@nodiscard ---@return table|nil modem function table function ppm.get_wireless_modem() local w_modem = nil local emulated_env = periphemu ~= nil - for _, device in pairs(_ppm_sys.mounts) do + for _, device in pairs(ppm_sys.mounts) do if device.type == "modem" and (emulated_env or device.dev.isWireless()) then w_modem = device.dev break @@ -408,11 +404,12 @@ function ppm.get_wireless_modem() end -- list all connected monitors +---@nodiscard ---@return table monitors function ppm.get_monitor_list() local list = {} - for iface, device in pairs(_ppm_sys.mounts) do + for iface, device in pairs(ppm_sys.mounts) do if device.type == "monitor" then list[iface] = device end diff --git a/scada-common/psil.lua b/scada-common/psil.lua index ddadf36..c21b2cf 100644 --- a/scada-common/psil.lua +++ b/scada-common/psil.lua @@ -5,6 +5,7 @@ local psil = {} -- instantiate a new PSI layer +---@nodiscard function psil.create() local self = { ic = {} @@ -19,8 +20,7 @@ function psil.create() ---@class psil local public = {} - -- subscribe to a data object in the interconnect - -- + -- subscribe to a data object in the interconnect
-- will call func() right away if a value is already avaliable ---@param key string data key ---@param func function function to call on change diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index 79ced10..6b33a24 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -89,6 +89,7 @@ rsio.IO = IO_PORT ----------------------- -- port to string +---@nodiscard ---@param port IO_PORT function rsio.to_string(port) local names = { @@ -194,6 +195,7 @@ local RS_DIO_MAP = { } -- get the mode of a port +---@nodiscard ---@param port IO_PORT ---@return IO_MODE function rsio.get_io_mode(port) @@ -239,6 +241,7 @@ end local RS_SIDES = rs.getSides() -- check if a port is valid +---@nodiscard ---@param port IO_PORT ---@return boolean valid function rsio.is_valid_port(port) @@ -246,6 +249,7 @@ function rsio.is_valid_port(port) end -- check if a side is valid +---@nodiscard ---@param side string ---@return boolean valid function rsio.is_valid_side(side) @@ -258,6 +262,7 @@ function rsio.is_valid_side(side) end -- check if a color is a valid single color +---@nodiscard ---@param color integer ---@return boolean valid function rsio.is_color(color) @@ -269,22 +274,25 @@ end ----------------- -- get digital I/O level reading from a redstone boolean input value ----@param rs_value boolean +---@nodiscard +---@param rs_value boolean raw value from redstone ---@return IO_LVL function rsio.digital_read(rs_value) if rs_value then return IO_LVL.HIGH else return IO_LVL.LOW end end -- get redstone boolean output value corresponding to a digital I/O level ----@param level IO_LVL +---@nodiscard +---@param level IO_LVL logic level ---@return boolean function rsio.digital_write(level) return level == IO_LVL.HIGH end -- returns the level corresponding to active ----@param port IO_PORT ----@param active boolean +---@nodiscard +---@param port IO_PORT port (to determine active high/low) +---@param active boolean state to convert to logic level ---@return IO_LVL|false function rsio.digital_write_active(port, active) if (not util.is_int(port)) or (port < IO_PORT.F_ALARM) or (port > IO_PORT.U_EMER_COOL) then @@ -295,9 +303,10 @@ function rsio.digital_write_active(port, active) end -- returns true if the level corresponds to active ----@param port IO_PORT ----@param level IO_LVL ----@return boolean|nil +---@nodiscard +---@param port IO_PORT port (to determine active low/high) +---@param level IO_LVL logic level +---@return boolean|nil state true for active, false for inactive, or nil if invalid port or level provided function rsio.digital_is_active(port, level) if not util.is_int(port) then return nil @@ -313,6 +322,7 @@ end ---------------- -- read an analog value scaled from min to max +---@nodiscard ---@param rs_value number redstone reading (0 to 15) ---@param min number minimum of range ---@param max number maximum of range @@ -323,6 +333,7 @@ function rsio.analog_read(rs_value, min, max) end -- write an analog value from the provided scale range +---@nodiscard ---@param value number value to write (from min to max range) ---@param min number minimum of range ---@param max number maximum of range diff --git a/scada-common/tcallbackdsp.lua b/scada-common/tcallbackdsp.lua index 52f55da..3f8f07a 100644 --- a/scada-common/tcallbackdsp.lua +++ b/scada-common/tcallbackdsp.lua @@ -19,8 +19,6 @@ function tcallbackdsp.dispatch(time, f) duration = time, expiry = time + util.time_s() } - - -- log.debug(util.c("TCD: queued callback for ", f, " [timer: ", timer, "]")) end -- request a function to be called after the specified time, aborting any registered instances of that function reference @@ -45,8 +43,6 @@ function tcallbackdsp.dispatch_unique(time, f) duration = time, expiry = time + util.time_s() } - - -- log.debug(util.c("TCD: queued callback for ", f, " [timer: ", timer, "]")) end -- abort a requested callback @@ -72,8 +68,7 @@ function tcallbackdsp.handle(event) end end --- identify any overdo callbacks --- +-- identify any overdo callbacks
-- prints to log debug output function tcallbackdsp.diagnostics() for timer, entry in pairs(registry) do diff --git a/scada-common/types.lua b/scada-common/types.lua index 23a4006..7ad0eb8 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -12,12 +12,14 @@ local types = {} ---@field amount integer -- create a new tank fluid +---@nodiscard ---@param n string name ---@param a integer amount ---@return radiation_reading function types.new_tank_fluid(n, a) return { name = n, amount = a } end -- create a new empty tank fluid +---@nodiscard ---@return tank_fluid function types.new_empty_gas() return { type = "mekanism:empty_gas", amount = 0 } end @@ -26,12 +28,14 @@ function types.new_empty_gas() return { type = "mekanism:empty_gas", amount = 0 ---@field unit string -- create a new radiation reading +---@nodiscard ---@param r number radiaiton level ---@param u string radiation unit ---@return radiation_reading function types.new_radiation_reading(r, u) return { radiation = r, unit = u } end -- create a new zeroed radiation reading +---@nodiscard ---@return radiation_reading function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv" } end @@ -41,6 +45,7 @@ function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv" ---@field z integer -- create a new coordinate +---@nodiscard ---@param x integer ---@param y integer ---@param z integer @@ -48,11 +53,12 @@ function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv" function types.new_coordinate(x, y, z) return { x = x, y = y, z = z } end -- create a new zero coordinate +---@nodiscard ---@return coordinate function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end ---@class rtu_advertisement ----@field type integer +---@field type RTU_UNIT_TYPE ---@field index integer ---@field reactor integer ---@field rsio table|nil @@ -62,15 +68,58 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end ---@alias color integer -- ENUMERATION TYPES -- +--#region ----@alias TRI_FAIL integer -types.TRI_FAIL = { - OK = 0, - PARTIAL = 1, - FULL = 2 +---@enum RTU_UNIT_TYPE +types.RTU_UNIT_TYPE = { + VIRTUAL = 0, -- virtual device + REDSTONE = 1, -- redstone I/O + BOILER_VALVE = 2, -- boiler mekanism 10.1+ + TURBINE_VALVE = 3, -- turbine, mekanism 10.1+ + IMATRIX = 4, -- induction matrix + SPS = 5, -- SPS + SNA = 6, -- SNA + ENV_DETECTOR = 7 -- environment detector } ----@alias PROCESS integer +types.RTU_UNIT_NAMES = { + "redstone", + "boiler_valve", + "turbine_valve", + "induction_matrix", + "sps", + "sna", + "environment_detector" +} + +-- safe conversion of RTU UNIT TYPE to string +---@nodiscard +---@param utype RTU_UNIT_TYPE +---@return string +function types.rtu_type_to_string(utype) + if utype == types.RTU_UNIT_TYPE.VIRTUAL then + return "virtual" + elseif utype == types.RTU_UNIT_TYPE.REDSTONE or + utype == types.RTU_UNIT_TYPE.BOILER_VALVE or + utype == types.RTU_UNIT_TYPE.TURBINE_VALVE or + utype == types.RTU_UNIT_TYPE.IMATRIX or + utype == types.RTU_UNIT_TYPE.SPS or + utype == types.RTU_UNIT_TYPE.SNA or + utype == types.RTU_UNIT_TYPE.ENV_DETECTOR then + return types.RTU_UNIT_NAMES[utype] + else + return "" + end +end + +---@enum TRI_FAIL +types.TRI_FAIL = { + OK = 1, + PARTIAL = 2, + FULL = 3 +} + +---@enum PROCESS types.PROCESS = { INACTIVE = 0, MAX_BURN = 1, @@ -93,7 +142,7 @@ types.PROCESS_NAMES = { "GEN_RATE_FAULT_IDLE" } ----@alias WASTE_MODE integer +---@enum WASTE_MODE types.WASTE_MODE = { AUTO = 1, PLUTONIUM = 2, @@ -101,7 +150,14 @@ types.WASTE_MODE = { ANTI_MATTER = 4 } ----@alias ALARM integer +types.WASTE_MODE_NAMES = { + "AUTO", + "PLUTONIUM", + "POLONIUM", + "ANTI_MATTER" +} + +---@enum ALARM types.ALARM = { ContainmentBreach = 1, ContainmentRadiation = 2, @@ -117,7 +173,7 @@ types.ALARM = { TurbineTrip = 12 } -types.alarm_string = { +types.ALARM_NAMES = { "ContainmentBreach", "ContainmentRadiation", "ReactorLost", @@ -132,46 +188,40 @@ types.alarm_string = { "TurbineTrip" } ----@alias ALARM_PRIORITY integer +---@enum ALARM_PRIORITY types.ALARM_PRIORITY = { - CRITICAL = 0, - EMERGENCY = 1, - URGENT = 2, - TIMELY = 3 + CRITICAL = 1, + EMERGENCY = 2, + URGENT = 3, + TIMELY = 4 } -types.alarm_prio_string = { +types.ALARM_PRIORITY_NAMES = { "CRITICAL", "EMERGENCY", "URGENT", "TIMELY" } --- map alarms to alarm priority -types.ALARM_PRIO_MAP = { - types.ALARM_PRIORITY.CRITICAL, - types.ALARM_PRIORITY.CRITICAL, - types.ALARM_PRIORITY.URGENT, - types.ALARM_PRIORITY.CRITICAL, - types.ALARM_PRIORITY.EMERGENCY, - types.ALARM_PRIORITY.EMERGENCY, - types.ALARM_PRIORITY.TIMELY, - types.ALARM_PRIORITY.EMERGENCY, - types.ALARM_PRIORITY.TIMELY, - types.ALARM_PRIORITY.URGENT, - types.ALARM_PRIORITY.TIMELY, - types.ALARM_PRIORITY.URGENT +---@enum ALARM_STATE +types.ALARM_STATE = { + INACTIVE = 1, + TRIPPED = 2, + ACKED = 3, + RING_BACK = 4 } ----@alias ALARM_STATE integer -types.ALARM_STATE = { - INACTIVE = 0, - TRIPPED = 1, - ACKED = 2, - RING_BACK = 3 +types.ALARM_STATE_NAMES = { + "INACTIVE", + "TRIPPED", + "ACKED", + "RING_BACK" } +--#endregion + -- STRING TYPES -- +--#region ---@alias os_event ---| "alarm" @@ -206,14 +256,28 @@ types.ALARM_STATE = { ---| "websocket_failure" ---| "websocket_message" ---| "websocket_success" +---| "clock_start" custom, added for reactor PLC + +---@alias fluid +---| "mekanism:empty_gas" +---| "minecraft:water" +---| "mekanism:sodium" +---| "mekanism:superheated_sodium" + +types.FLUID = { + EMPTY_GAS = "mekanism:empty_gas", + WATER = "minecraft:water", + SODIUM = "mekanism:sodium", + SUPERHEATED_SODIUM = "mekanism:superheated_sodium" +} ---@alias rps_trip_cause ---| "ok" ---| "dmg_crit" ---| "high_temp" ---| "no_coolant" ----| "full_waste" ----| "heated_coolant_backup" +---| "ex_waste" +---| "ex_heated_coolant" ---| "no_fuel" ---| "fault" ---| "timeout" @@ -222,59 +286,40 @@ types.ALARM_STATE = { ---| "sys_fail" ---| "force_disabled" ----@alias fluid ----| "mekanism:empty_gas" ----| "minecraft:water" ----| "mekanism:sodium" ----| "mekanism:superheated_sodium" - -types.fluid = { - empty_gas = "mekanism:empty_gas", - water = "minecraft:water", - sodium = "mekanism:sodium", - superheated_sodium = "mekanism:superheated_sodium" +types.RPS_TRIP_CAUSE = { + OK = "ok", + DMG_CRIT = "dmg_crit", + HIGH_TEMP = "high_temp", + NO_COOLANT = "no_coolant", + EX_WASTE = "ex_waste", + EX_HCOOLANT = "ex_heated_coolant", + NO_FUEL = "no_fuel", + FAULT = "fault", + TIMEOUT = "timeout", + MANUAL = "manual", + AUTOMATIC = "automatic", + SYS_FAIL = "sys_fail", + FORCE_DISABLED = "force_disabled" } ----@alias rtu_t string -types.rtu_t = { - redstone = "redstone", - boiler_valve = "boiler_valve", - turbine_valve = "turbine_valve", - induction_matrix = "induction_matrix", - sps = "sps", - sna = "sna", - env_detector = "environment_detector" -} +---@alias dumping_mode +---| "IDLE" +---| "DUMPING" +---| "DUMPING_EXCESS" ----@alias rps_status_t rps_trip_cause -types.rps_status_t = { - ok = "ok", - dmg_crit = "dmg_crit", - high_temp = "high_temp", - no_coolant = "no_coolant", - ex_waste = "full_waste", - ex_hcoolant = "heated_coolant_backup", - no_fuel = "no_fuel", - fault = "fault", - timeout = "timeout", - manual = "manual", - automatic = "automatic", - sys_fail = "sys_fail", - force_disabled = "force_disabled" -} - --- turbine steam dumping modes ----@alias DUMPING_MODE string types.DUMPING_MODE = { IDLE = "IDLE", DUMPING = "DUMPING", DUMPING_EXCESS = "DUMPING_EXCESS" } --- MODBUS +--#endregion --- modbus function codes ----@alias MODBUS_FCODE integer +-- MODBUS -- +--#region + +-- MODBUS function codes +---@enum MODBUS_FCODE types.MODBUS_FCODE = { READ_COILS = 0x01, READ_DISCRETE_INPUTS = 0x02, @@ -287,8 +332,8 @@ types.MODBUS_FCODE = { ERROR_FLAG = 0x80 } --- modbus exception codes ----@alias MODBUS_EXCODE integer +-- MODBUS exception codes +---@enum MODBUS_EXCODE types.MODBUS_EXCODE = { ILLEGAL_FUNCTION = 0x01, ILLEGAL_DATA_ADDR = 0x02, @@ -302,4 +347,6 @@ types.MODBUS_EXCODE = { GATEWAY_TARGET_TIMEOUT = 0x0B } +--#endregion + return types diff --git a/scada-common/util.lua b/scada-common/util.lua index afe3c59..2913e9f 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -14,6 +14,7 @@ util.TICK_TIME_MS = 50 --#region -- trinary operator +---@nodiscard ---@param cond boolean|nil condition ---@param a any return if true ---@param b any return if false @@ -57,6 +58,7 @@ end --#region -- get a value as a string +---@nodiscard ---@param val any ---@return string function util.strval(val) @@ -69,6 +71,7 @@ function util.strval(val) end -- repeat a string n times +---@nodiscard ---@param str string ---@param n integer ---@return string @@ -81,6 +84,7 @@ function util.strrep(str, n) end -- repeat a space n times +---@nodiscard ---@param n integer ---@return string function util.spaces(n) @@ -88,6 +92,7 @@ function util.spaces(n) end -- pad text to a minimum width +---@nodiscard ---@param str string text ---@param n integer minimum width ---@return string @@ -100,6 +105,7 @@ function util.pad(str, n) end -- wrap a string into a table of lines, supporting single dash splits +---@nodiscard ---@param str string ---@param limit integer line limit ---@return table lines @@ -147,13 +153,12 @@ function util.strwrap(str, limit) end -- concatenation with built-in to string +---@nodiscard ---@vararg any ---@return string function util.concat(...) local str = "" - for _, v in ipairs(arg) do - str = str .. util.strval(v) - end + for _, v in ipairs(arg) do str = str .. util.strval(v) end return str end @@ -161,15 +166,16 @@ end util.c = util.concat -- sprintf implementation +---@nodiscard ---@param format string ---@vararg any function util.sprintf(format, ...) return string.format(format, table.unpack(arg)) end --- format a number string with commas as the thousands separator --- +-- format a number string with commas as the thousands separator
-- subtracts from spaces at the start if present for each comma used +---@nodiscard ---@param num string number string ---@return string function util.comma_format(num) @@ -196,6 +202,7 @@ end --#region -- is a value an integer +---@nodiscard ---@param x any value ---@return boolean is_integer if the number is an integer function util.is_int(x) @@ -203,6 +210,7 @@ function util.is_int(x) end -- get the sign of a number +---@nodiscard ---@param x number value ---@return integer sign (-1 for < 0, 1 otherwise) function util.sign(x) @@ -210,12 +218,14 @@ function util.sign(x) end -- round a number to an integer +---@nodiscard ---@return integer rounded function util.round(x) return math.floor(x + 0.5) end -- get a new moving average object +---@nodiscard ---@param length integer history length ---@param default number value to fill history with for first call to compute() function util.mov_avg(length, default) @@ -249,6 +259,7 @@ function util.mov_avg(length, default) end -- compute the moving average + ---@nodiscard ---@return number average function public.compute() local sum = 0 @@ -264,6 +275,7 @@ end -- TIME -- -- current time +---@nodiscard ---@return integer milliseconds function util.time_ms() ---@diagnostic disable-next-line: undefined-field @@ -271,6 +283,7 @@ function util.time_ms() end -- current time +---@nodiscard ---@return number seconds function util.time_s() ---@diagnostic disable-next-line: undefined-field @@ -278,10 +291,9 @@ function util.time_s() end -- current time +---@nodiscard ---@return integer milliseconds -function util.time() - return util.time_ms() -end +function util.time() return util.time_ms() end --#endregion @@ -289,6 +301,7 @@ end --#region -- OS pull event raw wrapper with types +---@nodiscard ---@param target_event? string event to wait for ---@return os_event event, any param1, any param2, any param3, any param4, any param5 function util.pull_event(target_event) @@ -309,6 +322,7 @@ function util.push_event(event, param1, param2, param3, param4, param5) end -- start an OS timer +---@nodiscard ---@param t number timer duration in seconds ---@return integer timer ID function util.start_timer(t) @@ -336,14 +350,12 @@ function util.psleep(t) pcall(os.sleep, t) end --- no-op to provide a brief pause (1 tick) to yield ---- +-- no-op to provide a brief pause (1 tick) to yield
--- EVENT_CONSUMER: this function consumes events -function util.nop() - util.psleep(0.05) -end +function util.nop() util.psleep(0.05) end -- attempt to maintain a minimum loop timing (duration of execution) +---@nodiscard ---@param target_timing integer minimum amount of milliseconds to wait for ---@param last_update integer millisecond time of last update ---@return integer time_now @@ -351,9 +363,7 @@ end function util.adaptive_delay(target_timing, last_update) local sleep_for = target_timing - (util.time() - last_update) -- only if >50ms since worker loops already yield 0.05s - if sleep_for >= 50 then - util.psleep(sleep_for / 1000.0) - end + if sleep_for >= 50 then util.psleep(sleep_for / 1000.0) end return util.time() end @@ -362,8 +372,7 @@ end -- TABLE UTILITIES -- --#region --- delete elements from a table if the passed function returns false when passed a table element --- +-- delete elements from a table if the passed function returns false when passed a table element
-- put briefly: deletes elements that return false, keeps elements that return true ---@param t table table to remove elements from ---@param f function should return false to delete an element when passed the element: f(elem) = true|false @@ -388,6 +397,7 @@ function util.filter_table(t, f, on_delete) end -- check if a table contains the provided element +---@nodiscard ---@param t table table to check ---@param element any element to check for function util.table_contains(t, element) @@ -404,11 +414,13 @@ end --#region -- convert Joules to FE +---@nodiscard ---@param J number Joules ---@return number FE Forge Energy function util.joules_to_fe(J) return (J * 0.4) end -- convert FE to Joules +---@nodiscard ---@param FE number Forge Energy ---@return number J Joules function util.fe_to_joules(FE) return (FE * 2.5) end @@ -418,10 +430,11 @@ local function MFE(fe) return fe / 1000000.0 end local function GFE(fe) return fe / 1000000000.0 end local function TFE(fe) return fe / 1000000000000.0 end local function PFE(fe) return fe / 1000000000000000.0 end -local function EFE(fe) return fe / 1000000000000000000.0 end -- if you accomplish this please touch grass -local function ZFE(fe) return fe / 1000000000000000000000.0 end -- please stop +local function EFE(fe) return fe / 1000000000000000000.0 end -- if you accomplish this please touch grass +local function ZFE(fe) return fe / 1000000000000000000000.0 end -- please stop -- format a power value into XXX.XX UNIT format (FE, kFE, MFE, GFE, TFE, PFE, EFE, ZFE) +---@nodiscard ---@param fe number forge energy value ---@param combine_label? boolean if a label should be included in the string itself ---@param format? string format override @@ -430,9 +443,7 @@ function util.power_format(fe, combine_label, format) local unit local value - if type(format) ~= "string" then - format = "%.2f" - end + if type(format) ~= "string" then format = "%.2f" end if fe < 1000.0 then unit = "FE" @@ -474,10 +485,10 @@ end -- WATCHDOG -- --- ComputerCraft OS Timer based Watchdog +-- OS timer based watchdog
+-- triggers a timer event if not fed within 'timeout' seconds +---@nodiscard ---@param timeout number timeout duration ---- ---- triggers a timer event if not fed within 'timeout' seconds function util.new_watchdog(timeout) local self = { timeout = timeout, @@ -487,10 +498,10 @@ function util.new_watchdog(timeout) ---@class watchdog local public = {} + -- check if a timer is this watchdog + ---@nodiscard ---@param timer number timer event timer ID - function public.is_timer(timer) - return self.wd_timer == timer - end + function public.is_timer(timer) return self.wd_timer == timer end -- satiate the beast function public.feed() @@ -512,10 +523,10 @@ end -- LOOP CLOCK -- --- ComputerCraft OS Timer based Loop Clock +-- OS timer based loop clock
+-- fires a timer event at the specified period, does not start at construct time +---@nodiscard ---@param period number clock period ---- ---- fires a timer event at the specified period, does not start at construct time function util.new_clock(period) local self = { period = period, @@ -525,24 +536,22 @@ function util.new_clock(period) ---@class clock local public = {} + -- check if a timer is this clock + ---@nodiscard ---@param timer number timer event timer ID - function public.is_clock(timer) - return self.timer == timer - end + function public.is_clock(timer) return self.timer == timer end -- start the clock - function public.start() - self.timer = util.start_timer(self.period) - end + function public.start() self.timer = util.start_timer(self.period) end return public end -- FIELD VALIDATOR -- --- create a new type validator --- +-- create a new type validator
-- can execute sequential checks and check valid() to see if it is still valid +---@nodiscard function util.new_validator() local valid = true @@ -565,6 +574,8 @@ function util.new_validator() function public.assert_port(port) valid = valid and type(port) == "number" and port >= 0 and port <= 65535 end + -- check if all assertions passed successfully + ---@nodiscard function public.valid() return valid end return public diff --git a/supervisor/facility.lua b/supervisor/facility.lua index fe9db20..106c4d7 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -1,3 +1,4 @@ +local const = require("scada-common.constants") local log = require("scada-common.log") local rsio = require("scada-common.rsio") local types = require("scada-common.types") @@ -12,19 +13,13 @@ local PROCESS_NAMES = types.PROCESS_NAMES local IO = rsio.IO --- 7.14 kJ per blade for 1 mB of fissile fuel
+-- 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(7140) -local FLOW_STABILITY_DELAY_S = unit.FLOW_STABILITY_DELAY_MS / 1000 +local FLOW_STABILITY_DELAY_S = const.FLOW_STABILITY_DELAY_MS / 1000 --- background radiation 0.0000001 Sv/h (99.99 nSv/h) --- "green tint" radiation 0.00001 Sv/h (10 uSv/h) --- damaging radiation 0.00006 Sv/h (60 uSv/h) -local RADIATION_ALARM_LEVEL = 0.00001 - -local HIGH_CHARGE = 1.0 -local RE_ENABLE_CHARGE = 0.95 +local ALARM_LIMS = const.ALARM_LIMITS local AUTO_SCRAM = { NONE = 0, @@ -53,6 +48,7 @@ local rate_Kd = -1.0 local facility = {} -- create a new facility management object +---@nodiscard ---@param num_reactors integer number of reactor units ---@param cooling_conf table cooling configurations of reactor units function facility.new(num_reactors, cooling_conf) @@ -124,6 +120,7 @@ function facility.new(num_reactors, cooling_conf) end -- check if all auto-controlled units completed ramping + ---@nodiscard local function _all_units_ramped() local all_ramped = true @@ -185,10 +182,7 @@ function facility.new(num_reactors, cooling_conf) unallocated = math.max(0, unallocated - ctl.br100) - if last ~= ctl.br100 then - log.debug("unit " .. u.get_id() .. ": set to " .. ctl.br100 .. " (was " .. last .. ")") - u.a_commit_br100(ramp) - end + if last ~= ctl.br100 then u.a_commit_br100(ramp) end end end end @@ -426,7 +420,7 @@ function facility.new(num_reactors, cooling_conf) self.accumulator = self.accumulator + (error * (now - self.last_time)) end - local runtime = now - self.time_start + -- local runtime = now - self.time_start local integral = self.accumulator local derivative = (error - self.last_error) / (now - self.last_time) @@ -441,8 +435,8 @@ function facility.new(num_reactors, cooling_conf) self.saturated = output ~= out_c - log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%d] }", - runtime, avg_charge, error, integral, output, out_c, P, I, D)) + -- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%d] }", + -- runtime, avg_charge, error, integral, output, out_c, P, I, D)) _allocate_burn_rate(out_c, true) @@ -495,7 +489,7 @@ function facility.new(num_reactors, cooling_conf) self.accumulator = self.accumulator + (error * (now - self.last_time)) end - local runtime = now - self.time_start + -- local runtime = now - self.time_start local integral = self.accumulator local derivative = (error - self.last_error) / (now - self.last_time) @@ -513,8 +507,8 @@ function facility.new(num_reactors, cooling_conf) 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)) + -- 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) @@ -564,10 +558,10 @@ function facility.new(num_reactors, cooling_conf) -- check matrix fill too high local was_fill = astatus.matrix_fill - astatus.matrix_fill = (db.tanks.energy_fill >= HIGH_CHARGE) or (astatus.matrix_fill and db.tanks.energy_fill > RE_ENABLE_CHARGE) + 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("FAC: charge state of induction matrix entered acceptable range <= " .. (RE_ENABLE_CHARGE * 100) .. "%") + log.info("FAC: charge state of induction matrix entered acceptable range <= " .. (ALARM_LIMS.CHARGE_RE_ENABLE * 100) .. "%") end -- check for critical unit alarms @@ -586,7 +580,7 @@ function facility.new(num_reactors, cooling_conf) local envd = self.envd[1] ---@type unit_session local e_db = envd.get_db() ---@type envd_session_db - astatus.radiation = e_db.radiation_raw > RADIATION_ALARM_LEVEL + astatus.radiation = e_db.radiation_raw > 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 @@ -814,6 +808,7 @@ function facility.new(num_reactors, cooling_conf) -- READ STATES/PROPERTIES -- -- get build properties of all machines + ---@nodiscard ---@param inc_imatrix boolean? true/nil to include induction matrix build, false to exclude function public.get_build(inc_imatrix) local build = {} @@ -830,6 +825,7 @@ function facility.new(num_reactors, cooling_conf) end -- get automatic process control status + ---@nodiscard function public.get_control_status() local astat = self.ascram_status return { @@ -851,6 +847,7 @@ function facility.new(num_reactors, cooling_conf) end -- get RTU statuses + ---@nodiscard function public.get_rtu_statuses() local status = {} @@ -889,9 +886,9 @@ function facility.new(num_reactors, cooling_conf) return status end - function public.get_units() - return self.units - end + -- get the units in this facility + ---@nodiscard + function public.get_units() return self.units end return public end diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index 55bbcc1..1402993 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -1,18 +1,20 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") +local types = require("scada-common.types") local util = require("scada-common.util") local svqtypes = require("supervisor.session.svqtypes") local coordinator = {} -local PROTOCOLS = comms.PROTOCOLS -local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES -local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES -local UNIT_COMMANDS = comms.UNIT_COMMANDS -local FAC_COMMANDS = comms.FAC_COMMANDS -local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES +local PROTOCOL = comms.PROTOCOL +local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE +local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE +local UNIT_COMMAND = comms.UNIT_COMMAND +local FAC_COMMAND = comms.FAC_COMMAND + +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local SV_Q_CMDS = svqtypes.SV_Q_CMDS local SV_Q_DATA = svqtypes.SV_Q_DATA @@ -45,6 +47,7 @@ local PERIODICS = { } -- coordinator supervisor session +---@nodiscard ---@param id integer session ID ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue @@ -54,8 +57,6 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) local log_header = "crdn_session(" .. id .. "): " local self = { - in_q = in_queue, - out_q = out_queue, units = facility.get_units(), -- connection properties seq_num = 0, @@ -90,30 +91,30 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) end -- send a CRDN packet - ---@param msg_type SCADA_CRDN_TYPES + ---@param msg_type SCADA_CRDN_TYPE ---@param msg table local function _send(msg_type, msg) local s_pkt = comms.scada_packet() local c_pkt = comms.crdn_packet() c_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.SCADA_CRDN, c_pkt.raw_sendable()) + s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable()) - self.out_q.push_packet(s_pkt) + out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 end -- send a SCADA management packet - ---@param msg_type SCADA_MGMT_TYPES + ---@param msg_type SCADA_MGMT_TYPE ---@param msg table local function _send_mgmt(msg_type, msg) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - self.out_q.push_packet(s_pkt) + out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 end @@ -126,12 +127,12 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) unit_builds[unit.get_id()] = unit.get_build() end - _send(SCADA_CRDN_TYPES.INITIAL_BUILDS, { facility.get_build(), unit_builds }) + _send(SCADA_CRDN_TYPE.INITIAL_BUILDS, { facility.get_build(), unit_builds }) end -- send facility builds local function _send_fac_builds() - _send(SCADA_CRDN_TYPES.FAC_BUILDS, { facility.get_build() }) + _send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build() }) end -- send unit builds @@ -143,7 +144,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) builds[unit.get_id()] = unit.get_build() end - _send(SCADA_CRDN_TYPES.UNIT_BUILDS, { builds }) + _send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds }) end -- send facility status @@ -153,7 +154,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) facility.get_rtu_statuses() } - _send(SCADA_CRDN_TYPES.FAC_STATUS, status) + _send(SCADA_CRDN_TYPE.FAC_STATUS, status) end -- send unit statuses @@ -172,7 +173,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) } end - _send(SCADA_CRDN_TYPES.UNIT_STATUSES, status) + _send(SCADA_CRDN_TYPE.UNIT_STATUSES, status) end -- handle a packet @@ -192,8 +193,8 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) self.conn_watchdog.feed() -- process packet - if pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then - if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then + if pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then + if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then -- keep alive reply if pkt.length == 2 then local srv_start = pkt.data[1] @@ -210,30 +211,30 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) else log.debug(log_header .. "SCADA keep alive packet length mismatch") end - elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then + elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then -- close the session _close() else log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) end - elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_CRDN then - if pkt.type == SCADA_CRDN_TYPES.INITIAL_BUILDS then + elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then + if pkt.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then -- acknowledgement to coordinator receiving builds self.acks.builds = true - elseif pkt.type == SCADA_CRDN_TYPES.FAC_BUILDS then + elseif pkt.type == SCADA_CRDN_TYPE.FAC_BUILDS then -- acknowledgement to coordinator receiving builds self.acks.fac_builds = true - elseif pkt.type == SCADA_CRDN_TYPES.FAC_CMD then + elseif pkt.type == SCADA_CRDN_TYPE.FAC_CMD then if pkt.length >= 1 then local cmd = pkt.data[1] - if cmd == FAC_COMMANDS.SCRAM_ALL then + if cmd == FAC_COMMAND.SCRAM_ALL then facility.scram_all() - _send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true }) - elseif cmd == FAC_COMMANDS.STOP then + _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true }) + elseif cmd == FAC_COMMAND.STOP then facility.auto_stop() - _send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true }) - elseif cmd == FAC_COMMANDS.START then + _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true }) + elseif cmd == FAC_COMMAND.START then if pkt.length == 6 then ---@type coord_auto_config local config = { @@ -244,23 +245,23 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) limits = pkt.data[6] } - _send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) }) + _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) }) else log.debug(log_header .. "CRDN auto start (with configuration) packet length mismatch") end - elseif cmd == FAC_COMMANDS.ACK_ALL_ALARMS then + elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then facility.ack_all() - _send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true }) + _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true }) else log.debug(log_header .. "CRDN facility command unknown") end else log.debug(log_header .. "CRDN facility command packet length mismatch") end - elseif pkt.type == SCADA_CRDN_TYPES.UNIT_BUILDS then + elseif pkt.type == SCADA_CRDN_TYPE.UNIT_BUILDS then -- acknowledgement to coordinator receiving builds self.acks.unit_builds = true - elseif pkt.type == SCADA_CRDN_TYPES.UNIT_CMD then + elseif pkt.type == SCADA_CRDN_TYPE.UNIT_CMD then if pkt.length >= 2 then -- get command and unit id local cmd = pkt.data[1] @@ -273,43 +274,43 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) if util.is_int(uid) and uid > 0 and uid <= #self.units then local unit = self.units[uid] ---@type reactor_unit - if cmd == UNIT_COMMANDS.START then - self.out_q.push_data(SV_Q_DATA.START, data) - elseif cmd == UNIT_COMMANDS.SCRAM then - self.out_q.push_data(SV_Q_DATA.SCRAM, data) - elseif cmd == UNIT_COMMANDS.RESET_RPS then - self.out_q.push_data(SV_Q_DATA.RESET_RPS, data) - elseif cmd == UNIT_COMMANDS.SET_BURN then + if cmd == UNIT_COMMAND.START then + out_queue.push_data(SV_Q_DATA.START, data) + elseif cmd == UNIT_COMMAND.SCRAM then + out_queue.push_data(SV_Q_DATA.SCRAM, data) + elseif cmd == UNIT_COMMAND.RESET_RPS then + out_queue.push_data(SV_Q_DATA.RESET_RPS, data) + elseif cmd == UNIT_COMMAND.SET_BURN then if pkt.length == 3 then - self.out_q.push_data(SV_Q_DATA.SET_BURN, data) + out_queue.push_data(SV_Q_DATA.SET_BURN, data) else log.debug(log_header .. "CRDN unit command burn rate missing option") end - elseif cmd == UNIT_COMMANDS.SET_WASTE then + elseif cmd == UNIT_COMMAND.SET_WASTE then if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] > 0) and (pkt.data[3] <= 4) then unit.set_waste(pkt.data[3]) else log.debug(log_header .. "CRDN unit command set waste missing option") end - elseif cmd == UNIT_COMMANDS.ACK_ALL_ALARMS then + elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then unit.ack_all() - _send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, true }) - elseif cmd == UNIT_COMMANDS.ACK_ALARM then + _send(SCADA_CRDN_TYPE.UNIT_CMD, { cmd, uid, true }) + elseif cmd == UNIT_COMMAND.ACK_ALARM then if pkt.length == 3 then unit.ack_alarm(pkt.data[3]) else log.debug(log_header .. "CRDN unit command ack alarm missing alarm id") end - elseif cmd == UNIT_COMMANDS.RESET_ALARM then + elseif cmd == UNIT_COMMAND.RESET_ALARM then if pkt.length == 3 then unit.reset_alarm(pkt.data[3]) else log.debug(log_header .. "CRDN unit command reset alarm missing alarm id") end - elseif cmd == UNIT_COMMANDS.SET_GROUP then + elseif cmd == UNIT_COMMAND.SET_GROUP then if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] >= 0) and (pkt.data[3] <= 4) then facility.set_group(unit.get_id(), pkt.data[3]) - _send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, pkt.data[3] }) + _send(SCADA_CRDN_TYPE.UNIT_CMD, { cmd, uid, pkt.data[3] }) else log.debug(log_header .. "CRDN unit command set group missing group id") end @@ -332,9 +333,11 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) local public = {} -- get the session ID + ---@nodiscard function public.get_id() return id end -- check if a timer matches this session's watchdog + ---@nodiscard function public.check_wd(timer) return self.conn_watchdog.is_timer(timer) and self.connected end @@ -342,12 +345,13 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) -- close the connection function public.close() _close() - _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) + _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) println("connection to coordinator " .. id .. " closed by server") log.info(log_header .. "session closed by server") end -- iterate the session + ---@nodiscard ---@return boolean connected function public.iterate() if self.connected then @@ -357,9 +361,9 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) local handle_start = util.time() - while self.in_q.ready() and self.connected do + while in_queue.ready() and self.connected do -- get a new message to process - local message = self.in_q.pop() + local message = in_queue.pop() if message ~= nil then if message.qtype == mqueue.TYPE.PACKET then @@ -373,7 +377,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) if cmd.key == CRD_S_DATA.CMD_ACK then local ack = cmd.val ---@type coord_ack - _send(SCADA_CRDN_TYPES.UNIT_CMD, { ack.cmd, ack.unit, ack.ack }) + _send(SCADA_CRDN_TYPE.UNIT_CMD, { ack.cmd, ack.unit, ack.ack }) elseif cmd.key == CRD_S_DATA.RESEND_PLC_BUILD then -- re-send PLC build -- retry logic will be kept as-is, so as long as no retry is needed, this will be a small update @@ -386,7 +390,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) local unit = self.units[unit_id] ---@type reactor_unit builds[unit_id] = unit.get_build(true, false, false) - _send(SCADA_CRDN_TYPES.UNIT_BUILDS, { builds }) + _send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds }) elseif cmd.key == CRD_S_DATA.RESEND_RTU_BUILD then local unit_id = cmd.val.unit if unit_id > 0 then @@ -398,16 +402,16 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) local builds = {} local unit = self.units[unit_id] ---@type reactor_unit - builds[unit_id] = unit.get_build(false, cmd.val.type == RTU_UNIT_TYPES.BOILER_VALVE, cmd.val.type == RTU_UNIT_TYPES.TURBINE_VALVE) + builds[unit_id] = unit.get_build(false, cmd.val.type == RTU_UNIT_TYPE.BOILER_VALVE, cmd.val.type == RTU_UNIT_TYPE.TURBINE_VALVE) - _send(SCADA_CRDN_TYPES.UNIT_BUILDS, { builds }) + _send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds }) else -- re-send facility RTU builds -- retry logic will be kept as-is, so as long as no retry is needed, this will be a small update self.retry_times.f_builds_packet = util.time() + PARTIAL_RETRY_PERIOD self.acks.fac_builds = false - _send(SCADA_CRDN_TYPES.FAC_BUILDS, { facility.get_build(cmd.val.type == RTU_UNIT_TYPES.IMATRIX) }) + _send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type == RTU_UNIT_TYPE.IMATRIX) }) end else log.warning(log_header .. "unsupported data command received in in_queue (this is a bug)") @@ -441,7 +445,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) periodics.keep_alive = periodics.keep_alive + elapsed if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then - _send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() }) + _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() }) periodics.keep_alive = 0 end diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 3d3270b..676263c 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -8,12 +8,11 @@ local svqtypes = require("supervisor.session.svqtypes") local plc = {} -local PROTOCOLS = comms.PROTOCOLS -local RPLC_TYPES = comms.RPLC_TYPES -local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES +local PROTOCOL = comms.PROTOCOL +local RPLC_TYPE = comms.RPLC_TYPE +local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local PLC_AUTO_ACK = comms.PLC_AUTO_ACK - -local UNIT_COMMANDS = comms.UNIT_COMMANDS +local UNIT_COMMAND = comms.UNIT_COMMAND local print = util.print local println = util.println @@ -47,18 +46,16 @@ local PERIODICS = { } -- PLC supervisor session +---@nodiscard ---@param id integer session ID ----@param for_reactor integer reactor ID +---@param reactor_id integer reactor ID ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue ---@param timeout number communications timeout -function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) +function plc.new_session(id, reactor_id, in_queue, out_queue, timeout) local log_header = "plc_session(" .. id .. "): " local self = { - for_reactor = for_reactor, - in_q = in_queue, - out_q = out_queue, commanded_state = false, commanded_burn_rate = 0.0, auto_cmd_token = 0, @@ -244,34 +241,35 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) end -- send an RPLC packet - ---@param msg_type RPLC_TYPES + ---@param msg_type RPLC_TYPE ---@param msg table local function _send(msg_type, msg) local s_pkt = comms.scada_packet() local r_pkt = comms.rplc_packet() - r_pkt.make(for_reactor, msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable()) + r_pkt.make(reactor_id, msg_type, msg) + s_pkt.make(self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) - self.out_q.push_packet(s_pkt) + out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 end -- send a SCADA management packet - ---@param msg_type SCADA_MGMT_TYPES + ---@param msg_type SCADA_MGMT_TYPE ---@param msg table local function _send_mgmt(msg_type, msg) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - self.out_q.push_packet(s_pkt) + out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 end -- get an ACK status + ---@nodiscard ---@param pkt rplc_frame ---@return boolean|nil ack local function _get_ack(pkt) @@ -297,10 +295,10 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) end -- process packet - if pkt.scada_frame.protocol() == PROTOCOLS.RPLC then + if pkt.scada_frame.protocol() == PROTOCOL.RPLC then -- check reactor ID - if pkt.id ~= for_reactor then - log.warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. self.for_reactor .. " != " .. pkt.id) + if pkt.id ~= reactor_id then + log.warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. reactor_id .. " != " .. pkt.id) return end @@ -308,7 +306,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) self.plc_conn_watchdog.feed() -- handle packet by type - if pkt.type == RPLC_TYPES.STATUS then + if pkt.type == RPLC_TYPE.STATUS then -- status packet received, update data if pkt.length >= 5 then self.sDB.last_status_update = pkt.data[1] @@ -335,14 +333,14 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) else log.debug(log_header .. "RPLC status packet length mismatch") end - elseif pkt.type == RPLC_TYPES.MEK_STRUCT then + elseif pkt.type == RPLC_TYPE.MEK_STRUCT then -- received reactor structure, record it if pkt.length == 14 then local status = pcall(_copy_struct, pkt.data) if status then -- copied in structure data OK self.received_struct = true - self.out_q.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, for_reactor) + out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id) else -- error copying structure data log.error(log_header .. "failed to parse struct packet data") @@ -350,7 +348,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) else log.debug(log_header .. "RPLC struct packet length mismatch") end - elseif pkt.type == RPLC_TYPES.MEK_BURN_RATE then + elseif pkt.type == RPLC_TYPE.MEK_BURN_RATE then -- burn rate acknowledgement local ack = _get_ack(pkt) if ack then @@ -360,12 +358,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) end -- send acknowledgement to coordinator - self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { - unit = self.for_reactor, - cmd = UNIT_COMMANDS.SET_BURN, + out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { + unit = reactor_id, + cmd = UNIT_COMMAND.SET_BURN, ack = ack }) - elseif pkt.type == RPLC_TYPES.RPS_ENABLE then + elseif pkt.type == RPLC_TYPE.RPS_ENABLE then -- enable acknowledgement local ack = _get_ack(pkt) if ack then @@ -375,12 +373,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) end -- send acknowledgement to coordinator - self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { - unit = self.for_reactor, - cmd = UNIT_COMMANDS.START, + out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { + unit = reactor_id, + cmd = UNIT_COMMAND.START, ack = ack }) - elseif pkt.type == RPLC_TYPES.RPS_SCRAM then + elseif pkt.type == RPLC_TYPE.RPS_SCRAM then -- manual SCRAM acknowledgement local ack = _get_ack(pkt) if ack then @@ -391,12 +389,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) end -- send acknowledgement to coordinator - self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { - unit = self.for_reactor, - cmd = UNIT_COMMANDS.SCRAM, + out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { + unit = reactor_id, + cmd = UNIT_COMMAND.SCRAM, ack = ack }) - elseif pkt.type == RPLC_TYPES.RPS_ASCRAM then + elseif pkt.type == RPLC_TYPE.RPS_ASCRAM then -- automatic SCRAM acknowledgement local ack = _get_ack(pkt) if ack then @@ -405,7 +403,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) elseif ack == false then log.debug(log_header .. " automatic SCRAM failed!") end - elseif pkt.type == RPLC_TYPES.RPS_STATUS then + elseif pkt.type == RPLC_TYPE.RPS_STATUS then -- RPS status packet received, copy data if pkt.length == 14 then local status = pcall(_copy_rps_status, pkt.data) @@ -418,7 +416,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) else log.debug(log_header .. "RPLC RPS status packet length mismatch") end - elseif pkt.type == RPLC_TYPES.RPS_ALARM then + elseif pkt.type == RPLC_TYPE.RPS_ALARM then -- RPS alarm if pkt.length == 13 then local status = pcall(_copy_rps_status, { true, table.unpack(pkt.data) }) @@ -431,7 +429,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) else log.debug(log_header .. "RPLC RPS alarm packet length mismatch") end - elseif pkt.type == RPLC_TYPES.RPS_RESET then + elseif pkt.type == RPLC_TYPE.RPS_RESET then -- RPS reset acknowledgement local ack = _get_ack(pkt) if ack then @@ -443,18 +441,18 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) end -- send acknowledgement to coordinator - self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { - unit = self.for_reactor, - cmd = UNIT_COMMANDS.RESET_RPS, + out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { + unit = reactor_id, + cmd = UNIT_COMMAND.RESET_RPS, ack = ack }) - elseif pkt.type == RPLC_TYPES.RPS_AUTO_RESET then + elseif pkt.type == RPLC_TYPE.RPS_AUTO_RESET then -- RPS auto control reset acknowledgement local ack = _get_ack(pkt) if not ack then log.debug(log_header .. "RPS auto reset failed") end - elseif pkt.type == RPLC_TYPES.AUTO_BURN_RATE then + elseif pkt.type == RPLC_TYPE.AUTO_BURN_RATE then if pkt.length == 1 then local ack = pkt.data[1] @@ -473,8 +471,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) else log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type) end - elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then - if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then + elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then + if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then -- keep alive reply if pkt.length == 2 then local srv_start = pkt.data[1] @@ -491,7 +489,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) else log.debug(log_header .. "SCADA keep alive packet length mismatch") end - elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then + elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then -- close the session _close() else @@ -503,17 +501,22 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) -- PUBLIC FUNCTIONS -- -- get the session ID + ---@nodiscard function public.get_id() return id end -- get the session database + ---@nodiscard function public.get_db() return self.sDB end -- check if ramping is completed by first verifying auto command token ack + ---@nodiscard function public.is_ramp_complete() return (self.sDB.auto_ack_token == self.auto_cmd_token) and (self.commanded_burn_rate == self.sDB.mek_status.act_burn_rate) end -- get the reactor structure + ---@nodiscard + ---@return mek_struct|table struct struct or empty table function public.get_struct() if self.received_struct then return self.sDB.mek_struct @@ -523,6 +526,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) end -- get the reactor status + ---@nodiscard + ---@return mek_status|table struct status or empty table function public.get_status() if self.received_status_cache then return self.sDB.mek_status @@ -532,11 +537,13 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) end -- get the reactor RPS status + ---@nodiscard function public.get_rps() return self.sDB.rps_status end -- get the general status information + ---@nodiscard function public.get_general_status() return { self.sDB.last_status_update, @@ -564,10 +571,11 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) ---@param ramp boolean true to ramp, false to not function public.auto_set_burn(rate, ramp) self.ramping_rate = ramp - self.in_q.push_data(PLC_S_DATA.AUTO_BURN_RATE, rate) + in_queue.push_data(PLC_S_DATA.AUTO_BURN_RATE, rate) end -- check if a timer matches this session's watchdog + ---@nodiscard function public.check_wd(timer) return self.plc_conn_watchdog.is_timer(timer) and self.connected end @@ -575,12 +583,13 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) -- close the connection function public.close() _close() - _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) - println("connection to reactor " .. self.for_reactor .. " PLC closed by server") + _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) + println("connection to reactor " .. reactor_id .. " PLC closed by server") log.info(log_header .. "session closed by server") end -- iterate the session + ---@nodiscard ---@return boolean connected function public.iterate() if self.connected then @@ -590,9 +599,9 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) local handle_start = util.time() - while self.in_q.ready() and self.connected do + while in_queue.ready() and self.connected do -- get a new message to process - local message = self.in_q.pop() + local message = in_queue.pop() if message ~= nil then if message.qtype == mqueue.TYPE.PACKET then @@ -604,27 +613,27 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) if cmd == PLC_S_CMDS.ENABLE then -- enable reactor if not self.auto_lock then - _send(RPLC_TYPES.RPS_ENABLE, {}) + _send(RPLC_TYPE.RPS_ENABLE, {}) end elseif cmd == PLC_S_CMDS.SCRAM then -- SCRAM reactor self.acks.scram = false self.retry_times.scram_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.RPS_SCRAM, {}) + _send(RPLC_TYPE.RPS_SCRAM, {}) elseif cmd == PLC_S_CMDS.ASCRAM then -- SCRAM reactor self.acks.ascram = false self.retry_times.ascram_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.RPS_ASCRAM, {}) + _send(RPLC_TYPE.RPS_ASCRAM, {}) elseif cmd == PLC_S_CMDS.RPS_RESET then -- reset RPS self.acks.ascram = true self.acks.rps_reset = false self.retry_times.rps_reset_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.RPS_RESET, {}) + _send(RPLC_TYPE.RPS_RESET, {}) elseif cmd == PLC_S_CMDS.RPS_AUTO_RESET then if self.sDB.rps_status.automatic or self.sDB.rps_status.timeout then - _send(RPLC_TYPES.RPS_AUTO_RESET, {}) + _send(RPLC_TYPE.RPS_AUTO_RESET, {}) end else log.warning(log_header .. "unsupported command received in in_queue (this is a bug)") @@ -642,7 +651,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) self.ramping_rate = false self.acks.burn_rate = false self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + _send(RPLC_TYPE.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) end end elseif cmd.key == PLC_S_DATA.RAMP_BURN_RATE then @@ -655,7 +664,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) self.ramping_rate = true self.acks.burn_rate = false self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + _send(RPLC_TYPE.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) end end elseif cmd.key == PLC_S_DATA.AUTO_BURN_RATE then @@ -670,7 +679,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) self.acks.burn_rate = not self.ramping_rate self.retry_times.burn_rate_req = util.time() + INITIAL_AUTO_WAIT - _send(RPLC_TYPES.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token }) + _send(RPLC_TYPE.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token }) end end else @@ -688,7 +697,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) -- exit if connection was closed if not self.connected then - println("connection to reactor " .. self.for_reactor .. " PLC closed by remote host") + println("connection to reactor " .. reactor_id .. " PLC closed by remote host") log.info(log_header .. "session closed by remote host") return self.connected end @@ -705,7 +714,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) periodics.keep_alive = periodics.keep_alive + elapsed if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then - _send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() }) + _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() }) periodics.keep_alive = 0 end @@ -722,7 +731,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) if not self.received_struct then if rtimes.struct_req - util.time() <= 0 then - _send(RPLC_TYPES.MEK_STRUCT, {}) + _send(RPLC_TYPE.MEK_STRUCT, {}) rtimes.struct_req = util.time() + RETRY_PERIOD end end @@ -731,7 +740,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) if not self.received_status_cache then if rtimes.status_req - util.time() <= 0 then - _send(RPLC_TYPES.MEK_STATUS, {}) + _send(RPLC_TYPE.MEK_STATUS, {}) rtimes.status_req = util.time() + RETRY_PERIOD end end @@ -742,13 +751,13 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) if rtimes.burn_rate_req - util.time() <= 0 then if self.auto_cmd_token > 0 then if self.auto_lock then - _send(RPLC_TYPES.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token }) + _send(RPLC_TYPE.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token }) else -- would have been an auto command, but disengaged, so stop retrying self.acks.burn_rate = true end elseif not self.auto_lock then - _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + _send(RPLC_TYPE.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) else -- shouldn't be in this state, just pretend it was acknowledged self.acks.burn_rate = true @@ -763,7 +772,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) if not self.acks.scram then if rtimes.scram_req - util.time() <= 0 then - _send(RPLC_TYPES.RPS_SCRAM, {}) + _send(RPLC_TYPE.RPS_SCRAM, {}) rtimes.scram_req = util.time() + RETRY_PERIOD end end @@ -772,7 +781,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) if not self.acks.ascram then if rtimes.ascram_req - util.time() <= 0 then - _send(RPLC_TYPES.RPS_ASCRAM, {}) + _send(RPLC_TYPE.RPS_ASCRAM, {}) rtimes.ascram_req = util.time() + RETRY_PERIOD end end @@ -781,7 +790,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout) if not self.acks.rps_reset then if rtimes.rps_reset_req - util.time() <= 0 then - _send(RPLC_TYPES.RPS_RESET, {}) + _send(RPLC_TYPE.RPS_RESET, {}) rtimes.rps_reset_req = util.time() + RETRY_PERIOD end end diff --git a/supervisor/session/rsctl.lua b/supervisor/session/rsctl.lua index 1544cc3..fb17efe 100644 --- a/supervisor/session/rsctl.lua +++ b/supervisor/session/rsctl.lua @@ -5,6 +5,7 @@ local rsctl = {} -- create a new redstone RTU I/O controller +---@nodiscard ---@param redstone_rtus table redstone RTU sessions function rsctl.new(redstone_rtus) ---@class rs_controller diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 1db89cb..9d178fe 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -1,7 +1,7 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") -local rsio = require("scada-common.rsio") +local types = require("scada-common.types") local util = require("scada-common.util") local svqtypes = require("supervisor.session.svqtypes") @@ -18,9 +18,9 @@ local svrs_turbinev = require("supervisor.session.rtu.turbinev") local rtu = {} -local PROTOCOLS = comms.PROTOCOLS -local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES -local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES +local PROTOCOL = comms.PROTOCOL +local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local print = util.print local println = util.println @@ -32,6 +32,7 @@ local PERIODICS = { } -- create a new RTU session +---@nodiscard ---@param id integer session ID ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue @@ -42,8 +43,6 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili local log_header = "rtu_session(" .. id .. "): " local self = { - in_q = in_queue, - out_q = out_queue, modbus_q = mqueue.new(), advert = advertisement, fac_units = facility.get_units(), @@ -99,7 +98,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili advert_validator.assert_type_int(unit_advert.index) advert_validator.assert_type_int(unit_advert.reactor) - if u_type == RTU_UNIT_TYPES.REDSTONE then + if u_type == RTU_UNIT_TYPE.REDSTONE then advert_validator.assert_type_table(unit_advert.rsio) end @@ -113,7 +112,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili end local type_string = util.strval(u_type) - if type(u_type) == "number" then type_string = util.strval(comms.advert_type_to_rtu_t(u_type)) end + if type(u_type) == "number" then type_string = types.rtu_type_to_string(u_type) end -- create unit by type @@ -124,19 +123,19 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili if unit_advert.reactor > 0 then local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit - if u_type == RTU_UNIT_TYPES.REDSTONE then + if u_type == RTU_UNIT_TYPE.REDSTONE then -- redstone unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q) if type(unit) ~= "nil" then target_unit.add_redstone(unit) end - elseif u_type == RTU_UNIT_TYPES.BOILER_VALVE then + elseif u_type == RTU_UNIT_TYPE.BOILER_VALVE then -- boiler (Mekanism 10.1+) unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q) if type(unit) ~= "nil" then target_unit.add_boiler(unit) end - elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then + elseif u_type == RTU_UNIT_TYPE.TURBINE_VALVE then -- turbine (Mekanism 10.1+) unit = svrs_turbinev.new(id, i, unit_advert, self.modbus_q) if type(unit) ~= "nil" then target_unit.add_turbine(unit) end - elseif u_type == RTU_UNIT_TYPES.ENV_DETECTOR then + elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then -- environment detector unit = svrs_envd.new(id, i, unit_advert, self.modbus_q) if type(unit) ~= "nil" then target_unit.add_envd(unit) end @@ -144,21 +143,21 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili log.error(util.c(log_header, "bad advertisement: encountered unsupported reactor-specific RTU type ", type_string)) end else - if u_type == RTU_UNIT_TYPES.REDSTONE then + if u_type == RTU_UNIT_TYPE.REDSTONE then -- redstone unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q) if type(unit) ~= "nil" then facility.add_redstone(unit) end - elseif u_type == RTU_UNIT_TYPES.IMATRIX then + elseif u_type == RTU_UNIT_TYPE.IMATRIX then -- induction matrix unit = svrs_imatrix.new(id, i, unit_advert, self.modbus_q) if type(unit) ~= "nil" then facility.add_imatrix(unit) end - elseif u_type == RTU_UNIT_TYPES.SPS then + elseif u_type == RTU_UNIT_TYPE.SPS then -- super-critical phase shifter unit = svrs_sps.new(id, i, unit_advert, self.modbus_q) - elseif u_type == RTU_UNIT_TYPES.SNA then + elseif u_type == RTU_UNIT_TYPE.SNA then -- solar neutron activator unit = svrs_sna.new(id, i, unit_advert, self.modbus_q) - elseif u_type == RTU_UNIT_TYPES.ENV_DETECTOR then + elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then -- environment detector unit = svrs_envd.new(id, i, unit_advert, self.modbus_q) if type(unit) ~= "nil" then facility.add_envd(unit) end @@ -194,23 +193,23 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili local function _send_modbus(m_pkt) local s_pkt = comms.scada_packet() - s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable()) + s_pkt.make(self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) - self.out_q.push_packet(s_pkt) + out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 end -- send a SCADA management packet - ---@param msg_type SCADA_MGMT_TYPES + ---@param msg_type SCADA_MGMT_TYPE ---@param msg table local function _send_mgmt(msg_type, msg) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - self.out_q.push_packet(s_pkt) + out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 end @@ -231,15 +230,15 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili self.rtu_conn_watchdog.feed() -- process packet - if pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then + if pkt.scada_frame.protocol() == PROTOCOL.MODBUS_TCP then if self.units[pkt.unit_id] ~= nil then local unit = self.units[pkt.unit_id] ---@type unit_session ---@diagnostic disable-next-line: param-type-mismatch unit.handle_packet(pkt) end - elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then + elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then -- handle management packet - if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then + if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then -- keep alive reply if pkt.length == 2 then local srv_start = pkt.data[1] @@ -256,20 +255,17 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili else log.debug(log_header .. "SCADA keep alive packet length mismatch") end - elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then + elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then -- close the session _close() - elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then + elseif pkt.type == SCADA_MGMT_TYPE.RTU_ADVERT then -- RTU unit advertisement log.debug(log_header .. "received updated advertisement") - - -- copy advertisement and remove version tag self.advert = pkt.data - table.remove(self.advert, 1) -- handle advertisement; this will re-create all unit sub-sessions _handle_advertisement() - elseif pkt.type == SCADA_MGMT_TYPES.RTU_DEV_REMOUNT then + elseif pkt.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT then if pkt.length == 1 then local unit_id = pkt.data[1] if self.units[unit_id] ~= nil then @@ -291,6 +287,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili function public.get_id() return id end -- check if a timer matches this session's watchdog + ---@nodiscard ---@param timer number function public.check_wd(timer) return self.rtu_conn_watchdog.is_timer(timer) and self.connected @@ -299,12 +296,13 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili -- close the connection function public.close() _close() - _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) + _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) println(log_header .. "connection to RTU closed by server") log.info(log_header .. "session closed by server") end -- iterate the session + ---@nodiscard ---@return boolean connected function public.iterate() if self.connected then @@ -314,9 +312,9 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili local handle_start = util.time() - while self.in_q.ready() and self.connected do + while in_queue.ready() and self.connected do -- get a new message to process - local msg = self.in_q.pop() + local msg = in_queue.pop() if msg ~= nil then if msg.qtype == mqueue.TYPE.PACKET then @@ -365,7 +363,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili periodics.keep_alive = periodics.keep_alive + elapsed if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then - _send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() }) + _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() }) periodics.keep_alive = 0 end @@ -389,7 +387,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili -- instruction with body local cmd = msg.message ---@type queue_data if cmd.key == unit_session.RTU_US_DATA.BUILD_CHANGED then - self.out_q.push_data(svqtypes.SV_Q_DATA.RTU_BUILD_CHANGED, cmd.val) + out_queue.push_data(svqtypes.SV_Q_DATA.RTU_BUILD_CHANGED, cmd.val) end end end diff --git a/supervisor/session/rtu/boilerv.lua b/supervisor/session/rtu/boilerv.lua index 0cc4945..2f3c231 100644 --- a/supervisor/session/rtu/boilerv.lua +++ b/supervisor/session/rtu/boilerv.lua @@ -1,4 +1,3 @@ -local comms = require("scada-common.comms") local log = require("scada-common.log") local types = require("scada-common.types") local util = require("scada-common.util") @@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session") local boilerv = {} -local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local MODBUS_FCODE = types.MODBUS_FCODE local TXN_TYPES = { @@ -32,14 +31,15 @@ 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 advert rtu_advertisement RTU advertisement table ---@param out_queue mqueue RTU unit message out queue function boilerv.new(session_id, unit_id, advert, out_queue) -- type check - if advert.type ~= RTU_UNIT_TYPES.BOILER_VALVE then - log.error("attempt to instantiate boilerv RTU for type '" .. advert.type .. "'. this is a bug.") + if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then + log.error("attempt to instantiate boilerv RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") return nil end @@ -239,6 +239,7 @@ function boilerv.new(session_id, unit_id, advert, out_queue) end -- get the unit session database + ---@nodiscard function public.get_db() return self.db end return public diff --git a/supervisor/session/rtu/envd.lua b/supervisor/session/rtu/envd.lua index bc65476..3b4b666 100644 --- a/supervisor/session/rtu/envd.lua +++ b/supervisor/session/rtu/envd.lua @@ -1,4 +1,3 @@ -local comms = require("scada-common.comms") local log = require("scada-common.log") local types = require("scada-common.types") local util = require("scada-common.util") @@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session") local envd = {} -local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local MODBUS_FCODE = types.MODBUS_FCODE local TXN_TYPES = { @@ -23,14 +22,15 @@ 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 function envd.new(session_id, unit_id, advert, out_queue) -- type check - if advert.type ~= RTU_UNIT_TYPES.ENV_DETECTOR then - log.error("attempt to instantiate envd RTU for type '" .. advert.type .. "'. this is a bug.") + if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then + log.error("attempt to instantiate envd RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") return nil end @@ -100,6 +100,7 @@ function envd.new(session_id, unit_id, advert, out_queue) end -- get the unit session database + ---@nodiscard function public.get_db() return self.db end return public diff --git a/supervisor/session/rtu/imatrix.lua b/supervisor/session/rtu/imatrix.lua index 607c22d..0b120b4 100644 --- a/supervisor/session/rtu/imatrix.lua +++ b/supervisor/session/rtu/imatrix.lua @@ -1,4 +1,3 @@ -local comms = require("scada-common.comms") local log = require("scada-common.log") local types = require("scada-common.types") local util = require("scada-common.util") @@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session") local imatrix = {} -local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local MODBUS_FCODE = types.MODBUS_FCODE local TXN_TYPES = { @@ -32,14 +31,15 @@ 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 advert rtu_advertisement RTU advertisement table ---@param out_queue mqueue RTU unit message out queue function imatrix.new(session_id, unit_id, advert, out_queue) -- type check - if advert.type ~= RTU_UNIT_TYPES.IMATRIX then - log.error("attempt to instantiate imatrix RTU for type '" .. advert.type .. "'. this is a bug.") + if advert.type ~= RTU_UNIT_TYPE.IMATRIX then + log.error("attempt to instantiate imatrix RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") return nil end @@ -213,6 +213,7 @@ function imatrix.new(session_id, unit_id, advert, out_queue) end -- get the unit session database + ---@nodiscard function public.get_db() return self.db end return public diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua index a286f9c..7c813a2 100644 --- a/supervisor/session/rtu/redstone.lua +++ b/supervisor/session/rtu/redstone.lua @@ -1,6 +1,4 @@ -local comms = require("scada-common.comms") local log = require("scada-common.log") -local mqueue = require("scada-common.mqueue") local rsio = require("scada-common.rsio") local types = require("scada-common.types") local util = require("scada-common.util") @@ -9,7 +7,7 @@ local unit_session = require("supervisor.session.rtu.unit_session") local redstone = {} -local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local MODBUS_FCODE = types.MODBUS_FCODE local IO_PORT = rsio.IO @@ -47,14 +45,15 @@ local PERIODICS = { ---@field req IO_LVL -- create a new redstone rtu session runner +---@nodiscard ---@param session_id integer ---@param unit_id integer ---@param advert rtu_advertisement ---@param out_queue mqueue function redstone.new(session_id, unit_id, advert, out_queue) -- type check - if advert.type ~= RTU_UNIT_TYPES.REDSTONE then - log.error("attempt to instantiate redstone RTU for type '" .. advert.type .. "'. this is a bug.") + if advert.type ~= RTU_UNIT_TYPE.REDSTONE then + log.error("attempt to instantiate redstone RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") return nil end @@ -120,6 +119,7 @@ function redstone.new(session_id, unit_id, advert, out_queue) ---@class rs_db_dig_io local io_f = { + ---@nodiscard read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end, ---@param active boolean write = function (active) end @@ -134,6 +134,7 @@ function redstone.new(session_id, unit_id, advert, out_queue) ---@class rs_db_dig_io local io_f = { + ---@nodiscard read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[port].phy) end, ---@param active boolean write = function (active) @@ -151,6 +152,7 @@ function redstone.new(session_id, unit_id, advert, out_queue) ---@class rs_db_ana_io local io_f = { + ---@nodiscard ---@return integer read = function () return self.phy_io.analog_in[port].phy end, ---@param value integer @@ -166,6 +168,7 @@ function redstone.new(session_id, unit_id, advert, out_queue) ---@class rs_db_ana_io local io_f = { + ---@nodiscard ---@return integer read = function () return self.phy_io.analog_out[port].phy end, ---@param value integer @@ -380,6 +383,7 @@ function redstone.new(session_id, unit_id, advert, out_queue) end -- get the unit session database + ---@nodiscard function public.get_db() return self.db end return public diff --git a/supervisor/session/rtu/sna.lua b/supervisor/session/rtu/sna.lua index e2a667e..006222b 100644 --- a/supervisor/session/rtu/sna.lua +++ b/supervisor/session/rtu/sna.lua @@ -1,4 +1,3 @@ -local comms = require("scada-common.comms") local log = require("scada-common.log") local types = require("scada-common.types") local util = require("scada-common.util") @@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session") local sna = {} -local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local MODBUS_FCODE = types.MODBUS_FCODE local TXN_TYPES = { @@ -29,14 +28,15 @@ 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 advert rtu_advertisement RTU advertisement table ---@param out_queue mqueue RTU unit message out queue function sna.new(session_id, unit_id, advert, out_queue) -- type check - if advert.type ~= RTU_UNIT_TYPES.SNA then - log.error("attempt to instantiate sna RTU for type '" .. advert.type .. "'. this is a bug.") + if advert.type ~= RTU_UNIT_TYPE.SNA then + log.error("attempt to instantiate sna RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") return nil end @@ -176,6 +176,7 @@ function sna.new(session_id, unit_id, advert, out_queue) end -- get the unit session database + ---@nodiscard function public.get_db() return self.db end return public diff --git a/supervisor/session/rtu/sps.lua b/supervisor/session/rtu/sps.lua index 9b07f3e..da036cd 100644 --- a/supervisor/session/rtu/sps.lua +++ b/supervisor/session/rtu/sps.lua @@ -1,4 +1,3 @@ -local comms = require("scada-common.comms") local log = require("scada-common.log") local types = require("scada-common.types") local util = require("scada-common.util") @@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session") local sps = {} -local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local MODBUS_FCODE = types.MODBUS_FCODE local TXN_TYPES = { @@ -32,14 +31,15 @@ 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 advert rtu_advertisement RTU advertisement table ---@param out_queue mqueue RTU unit message out queue function sps.new(session_id, unit_id, advert, out_queue) -- type check - if advert.type ~= RTU_UNIT_TYPES.SPS then - log.error("attempt to instantiate sps RTU for type '" .. advert.type .. "'. this is a bug.") + if advert.type ~= RTU_UNIT_TYPE.SPS then + log.error("attempt to instantiate sps RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") return nil end @@ -113,7 +113,7 @@ function sps.new(session_id, unit_id, advert, out_queue) -- query the tanks of the device local function _request_tanks() -- read input registers 11 through 19 (start = 11, count = 9) - self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 10, 12 }) + self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 11, 9 }) end -- PUBLIC FUNCTIONS -- @@ -223,6 +223,7 @@ function sps.new(session_id, unit_id, advert, out_queue) end -- get the unit session database + ---@nodiscard function public.get_db() return self.db end return public diff --git a/supervisor/session/rtu/turbinev.lua b/supervisor/session/rtu/turbinev.lua index 3f8357f..4cf32c4 100644 --- a/supervisor/session/rtu/turbinev.lua +++ b/supervisor/session/rtu/turbinev.lua @@ -1,4 +1,3 @@ -local comms = require("scada-common.comms") local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local types = require("scada-common.types") @@ -9,7 +8,7 @@ local unit_session = require("supervisor.session.rtu.unit_session") local turbinev = {} -local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local DUMPING_MODE = types.DUMPING_MODE local MODBUS_FCODE = types.MODBUS_FCODE @@ -44,14 +43,15 @@ 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 advert rtu_advertisement RTU advertisement table ---@param out_queue mqueue RTU unit message out queue function turbinev.new(session_id, unit_id, advert, out_queue) -- type check - if advert.type ~= RTU_UNIT_TYPES.TURBINE_VALVE then - log.error("attempt to instantiate turbinev RTU for type '" .. advert.type .. "'. this is a bug.") + if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then + log.error("attempt to instantiate turbinev RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") return nil end @@ -92,7 +92,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue) flow_rate = 0, prod_rate = 0, steam_input_rate = 0, - dumping_mode = DUMPING_MODE.IDLE ---@type DUMPING_MODE + dumping_mode = DUMPING_MODE.IDLE ---@type dumping_mode }, tanks = { last_update = 0, @@ -123,7 +123,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue) end -- set the dumping mode - ---@param mode DUMPING_MODE + ---@param mode dumping_mode local function _set_dump_mode(mode) -- write holding register 1 self.session.send_request(TXN_TYPES.SET_DUMP, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode }) @@ -310,6 +310,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue) end -- get the unit session database + ---@nodiscard function public.get_db() return self.db end return public diff --git a/supervisor/session/rtu/txnctrl.lua b/supervisor/session/rtu/txnctrl.lua index a1f3b4b..9766bab 100644 --- a/supervisor/session/rtu/txnctrl.lua +++ b/supervisor/session/rtu/txnctrl.lua @@ -6,9 +6,10 @@ local util = require("scada-common.util") local txnctrl = {} -local TIMEOUT = 2000 -- 2000ms max wait +local TIMEOUT = 2000 -- 2000ms max wait -- create a new transaction controller +---@nodiscard function txnctrl.new() local self = { list = {}, @@ -22,16 +23,19 @@ function txnctrl.new() local remove = table.remove -- get the length of the transaction list + ---@nodiscard function public.length() return #self.list end -- check if there are no active transactions + ---@nodiscard function public.empty() return #self.list == 0 end -- create a new transaction of the given type + ---@nodiscard ---@param txn_type integer ---@return integer txn_id function public.create(txn_type) @@ -49,6 +53,7 @@ function txnctrl.new() end -- mark a transaction as resolved to get its transaction type + ---@nodiscard ---@param txn_id integer ---@return integer txn_type function public.resolve(txn_id) diff --git a/supervisor/session/rtu/unit_session.lua b/supervisor/session/rtu/unit_session.lua index 27b21c0..700f9b1 100644 --- a/supervisor/session/rtu/unit_session.lua +++ b/supervisor/session/rtu/unit_session.lua @@ -8,7 +8,7 @@ local txnctrl = require("supervisor.session.rtu.txnctrl") local unit_session = {} -local PROTOCOLS = comms.PROTOCOLS +local PROTOCOL = comms.PROTOCOL local MODBUS_FCODE = types.MODBUS_FCODE local MODBUS_EXCODE = types.MODBUS_EXCODE @@ -23,6 +23,7 @@ unit_session.RTU_US_CMDS = RTU_US_CMDS unit_session.RTU_US_DATA = RTU_US_DATA -- create a new unit session runner +---@nodiscard ---@param session_id integer RTU session ID ---@param unit_id integer MODBUS unit ID ---@param advert rtu_advertisement RTU advertisement for this unit @@ -31,12 +32,8 @@ unit_session.RTU_US_DATA = RTU_US_DATA ---@param txn_tags table transaction log tags function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_tags) local self = { - log_tag = log_tag, - txn_tags = txn_tags, - unit_id = unit_id, device_index = advert.index, reactor = advert.reactor, - out_q = out_queue, transaction_controller = txnctrl.new(), connected = true, device_fail = false @@ -61,21 +58,22 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t local m_pkt = comms.modbus_packet() local txn_id = self.transaction_controller.create(txn_type) - m_pkt.make(txn_id, self.unit_id, f_code, register_param) + m_pkt.make(txn_id, unit_id, f_code, register_param) - self.out_q.push_packet(m_pkt) + out_queue.push_packet(m_pkt) return txn_id end -- try to resolve a MODBUS transaction + ---@nodiscard ---@param m_pkt modbus_frame MODBUS packet ---@return integer|false txn_type, integer txn_id transaction type or false on error/busy, transaction ID function protected.try_resolve(m_pkt) - if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then - if m_pkt.unit_id == self.unit_id then + if m_pkt.scada_frame.protocol() == PROTOCOL.MODBUS_TCP then + if m_pkt.unit_id == unit_id then local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) - local txn_tag = " (" .. util.strval(self.txn_tags[txn_type]) .. ")" + local txn_tag = " (" .. util.strval(txn_tags[txn_type]) .. ")" if bit.band(m_pkt.func_code, MODBUS_FCODE.ERROR_FLAG) ~= 0 then -- transaction incomplete or failed @@ -135,26 +133,35 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t end -- get the public interface + ---@nodiscard function protected.get() return public end -- PUBLIC FUNCTIONS -- -- get the unit ID + ---@nodiscard function public.get_session_id() return session_id end -- get the unit ID - function public.get_unit_id() return self.unit_id end + ---@nodiscard + function public.get_unit_id() return unit_id end -- get the device index + ---@nodiscard function public.get_device_idx() return self.device_index end -- get the reactor ID + ---@nodiscard function public.get_reactor() return self.reactor end -- get the command queue + ---@nodiscard function public.get_cmd_queue() return protected.in_q end -- close this unit + ---@nodiscard function public.close() self.connected = false end -- check if this unit is connected + ---@nodiscard function public.is_connected() return self.connected end -- check if this unit is faulted + ---@nodiscard function public.is_faulted() return self.device_fail end -- PUBLIC TEMPLATE FUNCTIONS -- @@ -179,6 +186,7 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t end -- get the unit session database + ---@nodiscard function public.get_db() log.debug("template unit_session.get_db() called", true) return {} diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index b7c8ef5..76fb6d1 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -183,9 +183,10 @@ local function _free_closed(sessions) end -- find a session by remote port +---@nodiscard ---@param list table ---@param port integer ----@return plc_session_struct|rtu_session_struct|nil +---@return plc_session_struct|rtu_session_struct|coord_session_struct|nil local function _find_session(list, port) for i = 1, #list do if list[i].r_port == port then return list[i] end @@ -212,54 +213,63 @@ function svsessions.relink_modem(modem) end -- find an RTU session by the remote port +---@nodiscard ---@param remote_port integer ---@return rtu_session_struct|nil function svsessions.find_rtu_session(remote_port) -- check RTU sessions ----@diagnostic disable-next-line: return-type-mismatch - return _find_session(self.rtu_sessions, remote_port) + local session = _find_session(self.rtu_sessions, remote_port) + ---@cast session rtu_session_struct + return session end -- find a PLC session by the remote port +---@nodiscard ---@param remote_port integer ---@return plc_session_struct|nil function svsessions.find_plc_session(remote_port) -- check PLC sessions ----@diagnostic disable-next-line: return-type-mismatch - return _find_session(self.plc_sessions, remote_port) + local session = _find_session(self.plc_sessions, remote_port) + ---@cast session plc_session_struct + return session end -- find a PLC/RTU session by the remote port +---@nodiscard ---@param remote_port integer ---@return plc_session_struct|rtu_session_struct|nil function svsessions.find_device_session(remote_port) -- check RTU sessions - local s = _find_session(self.rtu_sessions, remote_port) + local session = _find_session(self.rtu_sessions, remote_port) -- check PLC sessions - if s == nil then s = _find_session(self.plc_sessions, remote_port) end + if session == nil then session = _find_session(self.plc_sessions, remote_port) end + ---@cast session plc_session_struct|rtu_session_struct|nil - return s + return session end --- find a coordinator session by the remote port --- +-- find a coordinator session by the remote port
-- only one coordinator is allowed, but this is kept to be consistent with all other session tables +---@nodiscard ---@param remote_port integer ----@return nil +---@return coord_session_struct|nil function svsessions.find_coord_session(remote_port) -- check coordinator sessions ----@diagnostic disable-next-line: return-type-mismatch - return _find_session(self.coord_sessions, remote_port) + local session = _find_session(self.coord_sessions, remote_port) + ---@cast session coord_session_struct + return session end -- get the a coordinator session if exists +---@nodiscard ---@return coord_session_struct|nil function svsessions.get_coord_session() return self.coord_sessions[1] end -- get a session by reactor ID +---@nodiscard ---@param reactor integer ---@return plc_session_struct|nil session function svsessions.get_reactor_session(reactor) @@ -275,6 +285,7 @@ function svsessions.get_reactor_session(reactor) end -- establish a new PLC session +---@nodiscard ---@param local_port integer ---@param remote_port integer ---@param for_reactor integer @@ -314,6 +325,7 @@ function svsessions.establish_plc_session(local_port, remote_port, for_reactor, end -- establish a new RTU session +---@nodiscard ---@param local_port integer ---@param remote_port integer ---@param advertisement table @@ -344,6 +356,7 @@ function svsessions.establish_rtu_session(local_port, remote_port, advertisement end -- establish a new coordinator session +---@nodiscard ---@param local_port integer ---@param remote_port integer ---@param version string diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 5cba7f7..0682994 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "beta-v0.12.2" +local SUPERVISOR_VERSION = "v0.13.1" local print = util.print local println = util.println @@ -81,7 +81,7 @@ local function main() local modem = ppm.get_wireless_modem() if modem == nil then - println("boot> wireless modem not found") + println("startup> wireless modem not found") log.fatal("no wireless modem on startup") return end @@ -110,7 +110,7 @@ local function main() -- we only care if this is our wireless modem if device == modem then println_ts("wireless modem disconnected!") - log.error("comms modem disconnected!") + log.warning("comms modem disconnected") else log.warning("non-comms modem disconnected") end @@ -127,9 +127,9 @@ local function main() superv_comms.reconnect_modem(modem) println_ts("wireless modem reconnected.") - log.info("comms modem reconnected.") + log.info("comms modem reconnected") else - log.info("wired modem reconnected.") + log.info("wired modem reconnected") end end end diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 5cadad4..848dfc3 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -6,10 +6,10 @@ local svsessions = require("supervisor.session.svsessions") local supervisor = {} -local PROTOCOLS = comms.PROTOCOLS -local DEVICE_TYPES = comms.DEVICE_TYPES +local PROTOCOL = comms.PROTOCOL +local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK -local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES +local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local print = util.print local println = util.println @@ -17,6 +17,7 @@ local print_ts = util.print_ts local println_ts = util.println_ts -- supervisory controller communications +---@nodiscard ---@param version string supervisor version ---@param num_reactors integer number of reactors ---@param cooling_conf table cooling configuration table @@ -26,32 +27,24 @@ local println_ts = util.println_ts ---@param range integer trusted device connection range function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen, coord_listen, range) local self = { - version = version, - num_reactors = num_reactors, - modem = modem, - dev_listen = dev_listen, - coord_listen = coord_listen, - reactor_struct_cache = nil + last_est_acks = {} } - ---@class superv_comms - local public = {} - comms.set_trusted_range(range) -- PRIVATE FUNCTIONS -- -- configure modem channels local function _conf_channels() - self.modem.closeAll() - self.modem.open(self.dev_listen) - self.modem.open(self.coord_listen) + modem.closeAll() + modem.open(dev_listen) + modem.open(coord_listen) end _conf_channels() -- link modem to svsessions - svsessions.init(self.modem, num_reactors, cooling_conf) + svsessions.init(modem, num_reactors, cooling_conf) -- send an establish request response to a PLC/RTU ---@param dest integer @@ -60,10 +53,10 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() - m_pkt.make(SCADA_MGMT_TYPES.ESTABLISH, msg) - s_pkt.make(seq_id, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) + m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg) + s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - self.modem.transmit(dest, self.dev_listen, s_pkt.raw_sendable()) + modem.transmit(dest, dev_listen, s_pkt.raw_sendable()) end -- send coordinator connection establish response @@ -74,24 +67,27 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen local s_pkt = comms.scada_packet() local c_pkt = comms.mgmt_packet() - c_pkt.make(SCADA_MGMT_TYPES.ESTABLISH, msg) - s_pkt.make(seq_id, PROTOCOLS.SCADA_MGMT, c_pkt.raw_sendable()) + c_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg) + s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, c_pkt.raw_sendable()) - self.modem.transmit(dest, self.coord_listen, s_pkt.raw_sendable()) + modem.transmit(dest, coord_listen, s_pkt.raw_sendable()) end -- PUBLIC FUNCTIONS -- + ---@class superv_comms + local public = {} + -- reconnect a newly connected modem - ---@param modem table ----@diagnostic disable-next-line: redefined-local - function public.reconnect_modem(modem) - self.modem = modem - svsessions.relink_modem(self.modem) + ---@param new_modem table + function public.reconnect_modem(new_modem) + modem = new_modem + svsessions.relink_modem(new_modem) _conf_channels() end -- parse a packet + ---@nodiscard ---@param side string ---@param sender integer ---@param reply_to integer @@ -107,25 +103,25 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen if s_pkt.is_valid() then -- get as MODBUS TCP packet - if s_pkt.protocol() == PROTOCOLS.MODBUS_TCP then + if s_pkt.protocol() == PROTOCOL.MODBUS_TCP then local m_pkt = comms.modbus_packet() if m_pkt.decode(s_pkt) then pkt = m_pkt.get() end -- get as RPLC packet - elseif s_pkt.protocol() == PROTOCOLS.RPLC then + elseif s_pkt.protocol() == PROTOCOL.RPLC then local rplc_pkt = comms.rplc_packet() if rplc_pkt.decode(s_pkt) then pkt = rplc_pkt.get() end -- get as SCADA management packet - elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then + elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then local mgmt_pkt = comms.mgmt_packet() if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() end -- get as coordinator packet - elseif s_pkt.protocol() == PROTOCOLS.SCADA_CRDN then + elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then local crdn_pkt = comms.crdn_packet() if crdn_pkt.decode(s_pkt) then pkt = crdn_pkt.get() @@ -147,8 +143,9 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen local protocol = packet.scada_frame.protocol() -- device (RTU/PLC) listening channel - if l_port == self.dev_listen then - if protocol == PROTOCOLS.MODBUS_TCP then + if l_port == dev_listen then + if protocol == PROTOCOL.MODBUS_TCP then + ---@cast packet modbus_frame -- look for an associated session local session = svsessions.find_rtu_session(r_port) @@ -160,7 +157,8 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen -- any other packet should be session related, discard it log.debug("discarding MODBUS_TCP packet without a known session") end - elseif protocol == PROTOCOLS.RPLC then + elseif protocol == PROTOCOL.RPLC then + ---@cast packet rplc_frame -- look for an associated session local session = svsessions.find_plc_session(r_port) @@ -173,7 +171,8 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink") _send_dev_establish(packet.scada_frame.seq_num() + 1, r_port, { ESTABLISH_ACK.DENY }) end - elseif protocol == PROTOCOLS.SCADA_MGMT then + elseif protocol == PROTOCOL.SCADA_MGMT then + ---@cast packet mgmt_frame -- look for an associated session local session = svsessions.find_device_session(r_port) @@ -181,7 +180,7 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) - elseif packet.type == SCADA_MGMT_TYPES.ESTABLISH then + elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- establish a new session local next_seq_id = packet.scada_frame.seq_num() + 1 @@ -192,13 +191,13 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen local dev_type = packet.data[3] if comms_v ~= comms.version then - log.debug(util.c("dropping establish packet with incorrect comms version v", comms_v, - " (expected v", comms.version, ")")) - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION }) - return - end + if self.last_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then + log.info(util.c("dropping device establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) + self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION + end - if dev_type == DEVICE_TYPES.PLC then + _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION }) + elseif dev_type == DEVICE_TYPE.PLC then -- PLC linking request if packet.length == 4 and type(packet.data[4]) == "number" then local reactor_id = packet.data[4] @@ -206,19 +205,25 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen if plc_id == false then -- reactor already has a PLC assigned - log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id)) + if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then + log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id)) + self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION + end + _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION }) else -- got an ID; assigned to a reactor successfully println(util.c("PLC (", firmware_v, ") [:", r_port, "] \xbb reactor ", reactor_id, " connected")) log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [:", r_port, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id)) + _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW }) + self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW end else log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type") _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) end - elseif dev_type == DEVICE_TYPES.RTU then + elseif dev_type == DEVICE_TYPE.RTU then if packet.length == 4 then -- this is an RTU advertisement for a new session local rtu_advert = packet.data[4] @@ -226,6 +231,7 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen println(util.c("RTU (", firmware_v, ") [:", r_port, "] \xbb connected")) log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id)) + _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW }) else log.debug("RTU_ESTABLISH: packet length mismatch") @@ -247,16 +253,17 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen log.debug("illegal packet type " .. protocol .. " on device listening channel") end -- coordinator listening channel - elseif l_port == self.coord_listen then + elseif l_port == coord_listen then -- look for an associated session local session = svsessions.find_coord_session(r_port) - if protocol == PROTOCOLS.SCADA_MGMT then + if protocol == PROTOCOL.SCADA_MGMT then + ---@cast packet mgmt_frame -- SCADA management packet if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) - elseif packet.type == SCADA_MGMT_TYPES.ESTABLISH then + elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- establish a new session local next_seq_id = packet.scada_frame.seq_num() + 1 @@ -267,32 +274,39 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen local dev_type = packet.data[3] if comms_v ~= comms.version then - log.debug(util.c("dropping establish packet with incorrect comms version v", comms_v, - " (expected v", comms.version, ")")) - _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION }) - return - elseif dev_type ~= DEVICE_TYPES.CRDN then - log.debug(util.c("illegal establish packet for device ", dev_type, " on CRDN listening channel")) - _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) - return - end - - -- this is an attempt to establish a new session - local s_id = svsessions.establish_coord_session(l_port, r_port, firmware_v) - - if s_id ~= false then - local config = { self.num_reactors } - for i = 1, #cooling_conf do - table.insert(config, cooling_conf[i].BOILERS) - table.insert(config, cooling_conf[i].TURBINES) + if self.last_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then + log.info(util.c("dropping coordinator establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) + self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION end - println(util.c("CRD (",firmware_v, ") [:", r_port, "] \xbb connected")) - log.info(util.c("CRDN_ESTABLISH: coordinator (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id)) - _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW, config }) + _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION }) + elseif dev_type ~= DEVICE_TYPE.CRDN then + log.debug(util.c("illegal establish packet for device ", dev_type, " on CRDN listening channel")) + _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) else - log.debug("CRDN_ESTABLISH: denied new coordinator due to already being connected to another coordinator") - _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION }) + -- this is an attempt to establish a new session + local s_id = svsessions.establish_coord_session(l_port, r_port, firmware_v) + + if s_id ~= false then + local config = { num_reactors } + for i = 1, #cooling_conf do + table.insert(config, cooling_conf[i].BOILERS) + table.insert(config, cooling_conf[i].TURBINES) + end + + println(util.c("CRD (",firmware_v, ") [:", r_port, "] \xbb connected")) + log.info(util.c("CRDN_ESTABLISH: coordinator (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id)) + + _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW, config }) + self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW + else + if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then + log.info("CRDN_ESTABLISH: denied new coordinator due to already being connected to another coordinator") + self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION + end + + _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION }) + end end else log.debug("CRDN_ESTABLISH: establish packet length mismatch") @@ -302,7 +316,8 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen -- any other packet should be session related, discard it log.debug(r_port .. "->" .. l_port .. ": discarding SCADA_MGMT packet without a known session") end - elseif protocol == PROTOCOLS.SCADA_CRDN then + elseif protocol == PROTOCOL.SCADA_CRDN then + ---@cast packet crdn_frame -- coordinator packet if session ~= nil then -- pass the packet onto the session handler diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 9b0849c..c3f9482 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -11,21 +11,16 @@ local rsctl = require("supervisor.session.rsctl") ---@class reactor_control_unit local unit = {} -local WASTE_MODE = types.WASTE_MODE - -local ALARM = types.ALARM -local PRIO = types.ALARM_PRIORITY -local ALARM_STATE = types.ALARM_STATE - -local TRI_FAIL = types.TRI_FAIL -local DUMPING_MODE = types.DUMPING_MODE +local WASTE_MODE = types.WASTE_MODE +local ALARM = types.ALARM +local PRIO = types.ALARM_PRIORITY +local ALARM_STATE = types.ALARM_STATE +local TRI_FAIL = types.TRI_FAIL local PLC_S_CMDS = plc.PLC_S_CMDS local IO = rsio.IO -local FLOW_STABILITY_DELAY_MS = 15000 - local DT_KEYS = { ReactorBurnR = "RBR", ReactorTemp = "RTP", @@ -41,18 +36,16 @@ local DT_KEYS = { TurbinePower = "TPR" } ----@alias ALARM_INT_STATE integer +---@enum ALARM_INT_STATE local AISTATE = { - INACTIVE = 0, - TRIPPING = 1, - TRIPPED = 2, - ACKED = 3, - RING_BACK = 4, - RING_BACK_TRIPPING = 5 + INACTIVE = 1, + TRIPPING = 2, + TRIPPED = 3, + ACKED = 4, + RING_BACK = 5, + RING_BACK_TRIPPING = 6 } -unit.FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS - ---@class alarm_def ---@field state ALARM_INT_STATE internal alarm state ---@field trip_time integer time (ms) when first tripped @@ -61,19 +54,19 @@ unit.FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS ---@field tier integer alarm urgency tier (0 = highest) -- create a new reactor unit ----@param for_reactor integer reactor unit number +---@nodiscard +---@param reactor_id integer reactor unit number ---@param num_boilers integer number of boilers expected ---@param num_turbines integer number of turbines expected -function unit.new(for_reactor, num_boilers, num_turbines) +function unit.new(reactor_id, num_boilers, num_turbines) ---@class _unit_self local self = { - r_id = for_reactor, + r_id = reactor_id, plc_s = nil, ---@class plc_session_struct plc_i = nil, ---@class plc_session num_boilers = num_boilers, num_turbines = num_turbines, types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE }, - defs = { FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS }, -- rtus redstone = {}, boilers = {}, @@ -278,6 +271,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) local function _reset_dt(key) self.deltas[key] = nil end -- get the delta t of a value + ---@nodiscard ---@param key string value key ---@return number value value or 0 if not known function self._get_dt(key) if self.deltas[key] then return self.deltas[key].dt else return 0.0 end end @@ -326,7 +320,6 @@ function unit.new(for_reactor, num_boilers, num_turbines) --#region redstone I/O local __rs_w = self.io_ctl.digital_write - local __rs_r = self.io_ctl.digital_read -- valves local waste_pu = { open = function () __rs_w(IO.WASTE_PU, true) end, close = function () __rs_w(IO.WASTE_PU, false) end } @@ -525,9 +518,9 @@ function unit.new(for_reactor, num_boilers, num_turbines) end end - -- get the actual limit of this unit - -- + -- get the actual limit of this unit
-- if it is degraded or not ready, the limit will be 0 + ---@nodiscard ---@return integer lim_br100 function public.a_get_effective_limit() if not self.db.control.ready or self.db.control.degraded or self.plc_cache.rps_trip then @@ -551,6 +544,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) end -- check if ramping is complete (burn rate is same as target) + ---@nodiscard ---@return boolean complete function public.a_ramp_complete() if self.plc_i ~= nil then @@ -610,7 +604,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- acknowledge an alarm (if possible) ---@param id ALARM alarm ID function public.ack_alarm(id) - if (type(id) == "number") and (self.db.alarm_states[id] == ALARM_STATE.TRIPPED) then + if type(id) == "number" and self.db.alarm_states[id] == ALARM_STATE.TRIPPED then self.db.alarm_states[id] = ALARM_STATE.ACKED end end @@ -618,7 +612,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- reset an alarm (if possible) ---@param id ALARM alarm ID function public.reset_alarm(id) - if (type(id) == "number") and (self.db.alarm_states[id] == ALARM_STATE.RING_BACK) then + if type(id) == "number" and self.db.alarm_states[id] == ALARM_STATE.RING_BACK then self.db.alarm_states[id] = ALARM_STATE.INACTIVE end end @@ -675,6 +669,8 @@ function unit.new(for_reactor, num_boilers, num_turbines) --#region -- check if a critical alarm is tripped + ---@nodiscard + ---@return boolean tripped function public.has_critical_alarm() for _, alarm in pairs(self.alarms) do if alarm.tier == PRIO.CRITICAL and (alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED) then @@ -686,6 +682,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) end -- get build properties of all machines + ---@nodiscard ---@param inc_plc boolean? true/nil to include PLC build, false to exclude ---@param inc_boilers boolean? true/nil to include boiler builds, false to exclude ---@param inc_turbines boolean? true/nil to include turbine builds, false to exclude @@ -718,6 +715,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) end -- get reactor status + ---@nodiscard function public.get_reactor_status() local status = {} if self.plc_i ~= nil then @@ -728,6 +726,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) end -- get RTU statuses + ---@nodiscard function public.get_rtu_statuses() local status = {} @@ -769,20 +768,25 @@ function unit.new(for_reactor, num_boilers, num_turbines) end -- get the annunciator status + ---@nodiscard function public.get_annunciator() return self.db.annunciator end -- get the alarm states + ---@nodiscard function public.get_alarms() return self.db.alarm_states end -- get information required for automatic reactor control + ---@nodiscard function public.get_control_inf() return self.db.control end -- get unit state + ---@nodiscard function public.get_state() return { self.status_text[1], self.status_text[2], self.waste_mode, self.db.control.ready, self.db.control.degraded } end -- get the reactor ID + ---@nodiscard function public.get_id() return self.r_id end --#endregion diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua index 836dad2..3a55c82 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unitlogic.lua @@ -1,3 +1,4 @@ +local const = require("scada-common.constants") local log = require("scada-common.log") local rsio = require("scada-common.rsio") local types = require("scada-common.types") @@ -5,17 +6,16 @@ local util = require("scada-common.util") local plc = require("supervisor.session.plc") -local PRIO = types.ALARM_PRIORITY -local ALARM_STATE = types.ALARM_STATE - -local TRI_FAIL = types.TRI_FAIL +local TRI_FAIL = types.TRI_FAIL local DUMPING_MODE = types.DUMPING_MODE +local PRIO = types.ALARM_PRIORITY +local ALARM_STATE = types.ALARM_STATE local IO = rsio.IO local PLC_S_CMDS = plc.PLC_S_CMDS -local aistate_string = { +local AISTATE_NAMES = { "INACTIVE", "TRIPPING", "TRIPPED", @@ -24,11 +24,10 @@ local aistate_string = { "RING_BACK_TRIPPING" } --- background radiation 0.0000001 Sv/h (99.99 nSv/h) --- "green tint" radiation 0.00001 Sv/h (10 uSv/h) --- damaging radiation 0.00006 Sv/h (60 uSv/h) -local RADIATION_ALERT_LEVEL = 0.00001 -- 10 uSv/h -local RADIATION_ALARM_LEVEL = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good +local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS + +local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS +local ALARM_LIMS = const.ALARM_LIMITS ---@class unit_logic_extension local logic = {} @@ -108,15 +107,15 @@ function logic.update_annunciator(self) -- update other annunciator fields self.db.annunciator.ReactorSCRAM = plc_db.rps_tripped - self.db.annunciator.ManualReactorSCRAM = plc_db.rps_trip_cause == types.rps_status_t.manual - self.db.annunciator.AutoReactorSCRAM = plc_db.rps_trip_cause == types.rps_status_t.automatic + self.db.annunciator.ManualReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.MANUAL + self.db.annunciator.AutoReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.AUTOMATIC self.db.annunciator.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.no_cool) - self.db.annunciator.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < -2.0 - self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < 0.4 - self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > 1000 - self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > 100 - self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= 0.01 - self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= 0.85 + self.db.annunciator.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < ANNUNC_LIMS.RCSFlowLow + self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow + self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh + self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT + self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow + self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh -- this warning applies when no coolant is buffered (which we can't easily determine without running) --[[ @@ -129,7 +128,7 @@ function logic.update_annunciator(self) such as when a burn rate consumes half the coolant in the tank, meaning that: 50% at some point will be in the boiler, and 50% in a tube, so that leaves 0% in the reactor ]]-- - local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.fluid.sodium, 200000, 20000) + local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, 200000, 20000) local high_rate = (plc_db.mek_status.ccool_amnt / (plc_db.mek_status.burn_rate * heating_rate_conv)) < 4 self.db.annunciator.HighStartupRate = not plc_db.mek_status.status and high_rate @@ -150,7 +149,7 @@ function logic.update_annunciator(self) for i = 1, #self.envd do local envd = self.envd[i] ---@type unit_session self.db.annunciator.RadiationMonitor = util.trinary(envd.is_faulted(), 2, 3) - self.db.annunciator.RadiationWarning = envd.get_db().radiation_raw > RADIATION_ALERT_LEVEL + self.db.annunciator.RadiationWarning = envd.get_db().radiation_raw > ANNUNC_LIMS.RadiationWarning break end @@ -299,7 +298,7 @@ function logic.update_annunciator(self) self.db.annunciator.BoilRateMismatch = math.abs(total_boil_rate - total_input_rate) > (0.04 * total_boil_rate) -- check for steam feed mismatch and max return rate - local sfmismatch = math.abs(total_flow_rate - total_input_rate) > 10 + local sfmismatch = math.abs(total_flow_rate - total_input_rate) > ANNUNC_LIMS.SteamFeedMismatch sfmismatch = sfmismatch or boiler_steam_dt_sum > 2.0 or boiler_water_dt_sum < -2.0 self.db.annunciator.SteamFeedMismatch = sfmismatch self.db.annunciator.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0 @@ -367,8 +366,8 @@ local function _update_alarm_state(self, tripped, alarm) else alarm.state = AISTATE.TRIPPED self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED - log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.alarm_string[alarm.id], "): TRIPPED [PRIORITY ", - types.alarm_prio_string[alarm.tier + 1],"]")) + log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ", + types.ALARM_PRIORITY_NAMES[alarm.tier],"]")) end else alarm.trip_time = util.time_ms() @@ -381,8 +380,8 @@ local function _update_alarm_state(self, tripped, alarm) if elapsed > (alarm.hold_time * 1000) then alarm.state = AISTATE.TRIPPED self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED - log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.alarm_string[alarm.id], "): TRIPPED [PRIORITY ", - types.alarm_prio_string[alarm.tier + 1],"]")) + log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ", + types.ALARM_PRIORITY_NAMES[alarm.tier],"]")) end elseif int_state == AISTATE.RING_BACK_TRIPPING then alarm.trip_time = 0 @@ -431,8 +430,8 @@ local function _update_alarm_state(self, tripped, alarm) -- check for state change if alarm.state ~= int_state then - local change_str = util.c(aistate_string[int_state + 1], " -> ", aistate_string[alarm.state + 1]) - log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.alarm_string[alarm.id], "): ", change_str)) + local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state]) + log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str)) end end @@ -449,7 +448,7 @@ function logic.update_alarms(self) -- Containment Radiation local rad_alarm = false for i = 1, #self.envd do - rad_alarm = self.envd[i].get_db().radiation_raw > RADIATION_ALARM_LEVEL + rad_alarm = self.envd[i].get_db().radiation_raw > ALARM_LIMS.HIGH_RADIATION break end _update_alarm_state(self, rad_alarm, self.alarms.ContainmentRadiation) @@ -469,14 +468,14 @@ function logic.update_alarms(self) _update_alarm_state(self, (plc_cache.temp >= 1200) or rps_high_temp, self.alarms.ReactorOverTemp) -- High Temperature - _update_alarm_state(self, plc_cache.temp > 1150, self.alarms.ReactorHighTemp) + _update_alarm_state(self, plc_cache.temp >= ALARM_LIMS.HIGH_TEMP, self.alarms.ReactorHighTemp) -- Waste Leak - _update_alarm_state(self, plc_cache.waste >= 0.99, self.alarms.ReactorWasteLeak) + _update_alarm_state(self, plc_cache.waste >= 1.0, self.alarms.ReactorWasteLeak) -- High Waste local rps_high_waste = plc_cache.rps_status.ex_waste and not self.last_rps_trips.ex_waste - _update_alarm_state(self, (plc_cache.waste > 0.50) or rps_high_waste, self.alarms.ReactorHighWaste) + _update_alarm_state(self, (plc_cache.waste > ALARM_LIMS.HIGH_WASTE) or rps_high_waste, self.alarms.ReactorHighWaste) -- RPS Transient (excludes timeouts and manual trips) local rps_alarm = false @@ -501,7 +500,7 @@ function logic.update_alarms(self) -- annunciator indicators for these states may not indicate a real issue when: -- > flow is ramping up right after reactor start -- > flow is ramping down after reactor shutdown - if ((util.time_ms() - self.last_rate_change_ms) > self.defs.FLOW_STABILITY_DELAY_MS) and plc_cache.active then + if ((util.time_ms() - self.last_rate_change_ms) > FLOW_STABILITY_DELAY_MS) and plc_cache.active then rcs_trans = rcs_trans or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch end @@ -530,8 +529,8 @@ function logic.update_auto_safety(public, self) for _, alarm in pairs(self.alarms) do if alarm.tier <= PRIO.URGENT and (alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED) then if not self.auto_was_alarmed then - log.info(util.c("UNIT ", self.r_id, " AUTO SCRAM due to ALARM ", alarm.id, " (", types.alarm_string[alarm.id], ") [PRIORITY ", - types.alarm_prio_string[alarm.tier + 1],"]")) + log.info(util.c("UNIT ", self.r_id, " AUTO SCRAM due to ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], ") [PRIORITY ", + types.ALARM_PRIORITY_NAMES[alarm.tier],"]")) end alarmed = true @@ -555,6 +554,7 @@ function logic.update_status_text(self) local AISTATE = self.types.AISTATE -- check if an alarm is active (tripped or ack'd) + ---@nodiscard ---@param alarm table alarm entry ---@return boolean active local function is_active(alarm) @@ -620,7 +620,7 @@ function logic.update_status_text(self) self.status_text[2] = "insufficient fuel input rate" elseif self.db.annunciator.WasteLineOcclusion then self.status_text[2] = "insufficient waste output rate" - elseif (util.time_ms() - self.last_rate_change_ms) <= self.defs.FLOW_STABILITY_DELAY_MS then + elseif (util.time_ms() - self.last_rate_change_ms) <= FLOW_STABILITY_DELAY_MS then self.status_text[2] = "awaiting flow stability" else self.status_text[2] = "system nominal" @@ -636,9 +636,9 @@ function logic.update_status_text(self) cause = "core temperature high" elseif plc_db.rps_trip_cause == "no_coolant" then cause = "insufficient coolant" - elseif plc_db.rps_trip_cause == "full_waste" then + elseif plc_db.rps_trip_cause == "ex_waste" then cause = "excess waste" - elseif plc_db.rps_trip_cause == "heated_coolant_backup" then + elseif plc_db.rps_trip_cause == "ex_heated_coolant" then cause = "excess heated coolant" elseif plc_db.rps_trip_cause == "no_fuel" then cause = "insufficient fuel" @@ -670,7 +670,7 @@ function logic.update_status_text(self) end end else - self.status_text = { "Reactor Off-line", "awaiting connection..." } + self.status_text = { "REACTOR OFF-LINE", "awaiting connection..." } end end @@ -680,6 +680,7 @@ function logic.handle_redstone(self) local AISTATE = self.types.AISTATE -- check if an alarm is active (tripped or ack'd) + ---@nodiscard ---@param alarm table alarm entry ---@return boolean active local function is_active(alarm)