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 d67a6c9..e4bb283 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -25,6 +25,7 @@ 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,23 +210,19 @@ 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() @@ -242,7 +245,7 @@ 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 @@ -259,11 +262,13 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range -- 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 @@ -275,6 +280,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range 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 @@ -400,7 +406,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range if l_port == api_listen then if protocol == PROTOCOL.COORD_API then ----@diagnostic disable-next-line: param-type-mismatch + ---@cast packet capi_frame apisessions.handle_packet(packet) else log.debug("illegal packet type " .. protocol .. " on api listening channel", true) @@ -421,6 +427,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range -- handle packet if protocol == PROTOCOL.SCADA_CRDN then + ---@cast packet crdn_frame if self.sv_linked then if packet.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then if packet.length == 2 then @@ -432,7 +439,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range -- acknowledge receipt of 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") @@ -444,7 +451,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range -- acknowledge receipt of 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") @@ -452,7 +459,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range 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_TYPE.FAC_CMD then -- facility command acknowledgement @@ -485,7 +492,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range -- acknowledge receipt of 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") @@ -518,7 +525,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then unit.ack_alarms_ack(ack) elseif cmd == UNIT_COMMAND.SET_GROUP then - ---@todo how is this going to be handled? + -- 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 @@ -535,6 +542,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range log.debug("discarding SCADA_CRDN packet before linked") end 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 @@ -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 @@ -619,9 +627,9 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range 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 b66ea80..ea243cd 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -1,3 +1,7 @@ +-- +-- 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") @@ -16,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 = { @@ -41,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(), @@ -56,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()) @@ -170,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 @@ -187,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 @@ -284,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] @@ -327,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 @@ -334,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 @@ -346,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 @@ -371,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 @@ -396,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 @@ -413,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 @@ -729,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 e3604bf..1e318ed 100644 --- a/coordinator/process.lua +++ b/coordinator/process.lua @@ -1,3 +1,6 @@ +-- +-- Process Control Management +-- local comms = require("scada-common.comms") local log = require("scada-common.log") @@ -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 @@ -91,13 +94,13 @@ end -- facility SCRAM command function process.fac_scram() self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL) - log.debug("FAC: SCRAM ALL") + log.debug("PROCESS: FAC SCRAM ALL") end -- facility alarm acknowledge command function process.fac_ack_alarms() self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS) - log.debug("FAC: ACK ALL ALARMS") + log.debug("PROCESS: FAC ACK ALL ALARMS") end -- start reactor @@ -105,7 +108,7 @@ end function process.start(id) self.io.units[id].control_state = true self.comms.send_unit_command(UNIT_COMMAND.START, id) - log.debug(util.c("UNIT[", id, "]: START")) + log.debug(util.c("PROCESS: UNIT[", id, "] START")) end -- SCRAM reactor @@ -113,14 +116,14 @@ end function process.scram(id) self.io.units[id].control_state = false self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id) - log.debug(util.c("UNIT[", id, "]: SCRAM")) + 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_COMMAND.RESET_RPS, id) - log.debug(util.c("UNIT[", id, "]: RESET RPS")) + log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS")) end -- set burn rate @@ -128,7 +131,7 @@ end ---@param rate number burn rate function process.set_rate(id, rate) self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate) - log.debug(util.c("UNIT[", id, "]: SET BURN = ", rate)) + log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate)) end -- set waste mode @@ -139,13 +142,11 @@ function process.set_waste(id, mode) self.io.units[id].unit_ps.publish("U_WasteMode", mode) self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) - log.debug(util.c("UNIT[", id, "]: SET WASTE = ", 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 @@ -160,7 +161,7 @@ end ---@param id integer unit ID function process.ack_all_alarms(id) self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id) - log.debug(util.c("UNIT[", id, "]: ACK ALL ALARMS")) + log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS")) end -- acknowledge an alarm @@ -168,7 +169,7 @@ end ---@param alarm integer alarm ID function process.ack_alarm(id, alarm) self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm) - log.debug(util.c("UNIT[", id, "]: ACK ALARM ", alarm)) + log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm)) end -- reset an alarm @@ -176,7 +177,7 @@ end ---@param alarm integer alarm ID function process.reset_alarm(id, alarm) self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm) - log.debug(util.c("UNIT[", id, "]: RESET ALARM ", alarm)) + log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm)) end -- assign a unit to a group @@ -184,13 +185,11 @@ end ---@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_COMMAND.SET_GROUP, unit_id, group_id) - log.debug(util.c("UNIT[", unit_id, "]: SET GROUP ", 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 @@ -208,13 +207,13 @@ end -- stop automatic process control function process.stop_auto() self.comms.send_fac_command(FAC_COMMAND.STOP) - log.debug("FAC: STOP AUTO") + 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/boiler.lua b/coordinator/ui/components/boiler.lua index c4a433b..cc4e93b 100644 --- a/coordinator/ui/components/boiler.lua +++ b/coordinator/ui/components/boiler.lua @@ -13,6 +13,7 @@ local cpair = core.graphics.cpair local border = core.graphics.border -- new boiler view +---@nodiscard ---@param root graphics_element parent ---@param x integer top left x ---@param y integer top left y diff --git a/coordinator/ui/components/imatrix.lua b/coordinator/ui/components/imatrix.lua index 2910fbb..e60a126 100644 --- a/coordinator/ui/components/imatrix.lua +++ b/coordinator/ui/components/imatrix.lua @@ -19,6 +19,7 @@ local border = core.graphics.border local TEXT_ALIGN = core.graphics.TEXT_ALIGN -- new induction matrix view +---@nodiscard ---@param root graphics_element parent ---@param x integer top left x ---@param y integer top left y diff --git a/coordinator/ui/components/processctl.lua b/coordinator/ui/components/processctl.lua index 17b3e66..82ce614 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") @@ -32,6 +29,7 @@ local border = core.graphics.border local period = core.flasher.PERIOD -- new process control view +---@nodiscard ---@param root graphics_element parent ---@param x integer top left x ---@param y integer top left y diff --git a/coordinator/ui/components/reactor.lua b/coordinator/ui/components/reactor.lua index 5dc9f23..139a8d3 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") @@ -13,6 +15,7 @@ local cpair = core.graphics.cpair local border = core.graphics.border -- create new reactor view +---@nodiscard ---@param root graphics_element parent ---@param x integer top left x ---@param y integer top left y @@ -47,7 +50,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 +58,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/turbine.lua b/coordinator/ui/components/turbine.lua index ef09cee..c6caec9 100644 --- a/coordinator/ui/components/turbine.lua +++ b/coordinator/ui/components/turbine.lua @@ -15,6 +15,7 @@ local cpair = core.graphics.cpair local border = core.graphics.border -- new turbine view +---@nodiscard ---@param root graphics_element parent ---@param x integer top left x ---@param y integer top left y diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index 8fb7c38..82b01ec 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -57,6 +57,7 @@ local waste_opts = { } -- create a unit view +---@nodiscard ---@param parent graphics_element parent ---@param id integer local function init(parent, id) @@ -237,13 +238,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) diff --git a/coordinator/ui/components/unit_overview.lua b/coordinator/ui/components/unit_overview.lua index 672c6ea..5062116 100644 --- a/coordinator/ui/components/unit_overview.lua +++ b/coordinator/ui/components/unit_overview.lua @@ -19,6 +19,7 @@ local TEXT_ALIGN = core.graphics.TEXT_ALIGN local pipe = core.graphics.pipe -- make a new unit overview window +---@nodiscard ---@param parent graphics_element parent ---@param x integer top left x ---@param y integer top left y @@ -51,7 +52,7 @@ local function make(parent, x, y, unit) -- REACTOR -- ------------- - reactor_view(root, 1, 3, unit.reactor_data, unit.unit_ps) + local _ = reactor_view(root, 1, 3, unit.reactor_data, unit.unit_ps) if num_boilers > 0 then local coolant_pipes = {} @@ -101,16 +102,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..8c29f1a 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") @@ -16,6 +16,7 @@ local TEXT_ALIGN = core.graphics.TEXT_ALIGN local cpair = core.graphics.cpair -- create a unit waiting view +---@nodiscard ---@param parent graphics_element parent ---@param y integer y offset local function init(parent, y) 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..d1397d8 100644 --- a/coordinator/ui/layout/main_view.lua +++ b/coordinator/ui/layout/main_view.lua @@ -30,6 +30,7 @@ local TEXT_ALIGN = core.graphics.TEXT_ALIGN local cpair = core.graphics.cpair -- create new main view +---@nodiscard ---@param monitor table main viewscreen local function init(monitor) local facility = iocontrol.get_db().facility @@ -77,7 +78,7 @@ local function init(monitor) end end - -- command & control + -- command & control cnc_y_start = cnc_y_start @@ -90,7 +91,7 @@ local function init(monitor) cnc_bottom_align_start = cnc_bottom_align_start + 2 - local process = process_ctl(main, 2, cnc_bottom_align_start) + local _ = process_ctl(main, 2, cnc_bottom_align_start) -- testing ---@fixme remove test code @@ -123,7 +124,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]) + local _ = imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1]) return main end diff --git a/coordinator/ui/layout/unit_view.lua b/coordinator/ui/layout/unit_view.lua index 1c5fddf..c6ca828 100644 --- a/coordinator/ui/layout/unit_view.lua +++ b/coordinator/ui/layout/unit_view.lua @@ -9,12 +9,13 @@ local unit_detail = require("coordinator.ui.components.unit_detail") local DisplayBox = require("graphics.elements.displaybox") -- create a unit view +---@nodiscard ---@param monitor table ---@param id integer local function init(monitor, id) local main = DisplayBox{window=monitor,fg_bg=style.root} - unit_detail(main, id) + local _ = unit_detail(main, id) return main end