diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index 71da387..36163eb 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -24,6 +24,7 @@ local LINK_TIMEOUT = 60.0 local coordinator = {} ---@type crd_config +---@diagnostic disable-next-line: missing-fields local config = {} coordinator.config = config diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 7108c01..f7dde3d 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -20,6 +20,13 @@ local ENERGY_UNITS = types.ENERGY_SCALE_UNITS local TEMP_SCALE = types.TEMP_SCALE local TEMP_UNITS = types.TEMP_SCALE_UNITS +local RCT_STATE = types.REACTOR_STATE +local BLR_STATE = types.BOILER_STATE +local TRB_STATE = types.TURBINE_STATE +local TNK_STATE = types.TANK_STATE +local MTX_STATE = types.IMATRIX_STATE +local SPS_STATE = types.SPS_STATE + -- nominal RTT is ping (0ms to 10ms usually) + 500ms for CRD main loop tick local WARN_RTT = 1000 -- 2x as long as expected w/ 0 ping local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping @@ -119,7 +126,6 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) induction_ps_tbl = {}, ---@type psil[] induction_data_tbl = {}, ---@type imatrix_session_db[] - sps_status = 1, sps_ps_tbl = {}, ---@type psil[] sps_data_tbl = {}, ---@type sps_session_db[] @@ -151,10 +157,6 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) local entry = { unit_id = i, connected = false, - rtu_hw = { - boilers = {}, ---@type { connected: boolean, faulted: boolean }[] - turbines = {} ---@type { connected: boolean, faulted: boolean }[] - }, num_boilers = 0, num_turbines = 0, @@ -224,6 +226,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) ALARM_STATE.INACTIVE -- turbine trip }, +---@diagnostic disable-next-line: missing-fields annunciator = {}, ---@type annunciator unit_ps = psil.create(), @@ -248,14 +251,12 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) for _ = 1, conf.cooling.r_cool[i].BoilerCount do table.insert(entry.boiler_ps_tbl, psil.create()) table.insert(entry.boiler_data_tbl, {}) - table.insert(entry.rtu_hw.boilers, { connected = false, faulted = false }) end -- create turbine tables for _ = 1, conf.cooling.r_cool[i].TurbineCount do table.insert(entry.turbine_ps_tbl, psil.create()) table.insert(entry.turbine_data_tbl, {}) - table.insert(entry.rtu_hw.turbines, { connected = false, faulted = false }) end -- create tank tables @@ -366,6 +367,7 @@ local function _record_multiblock_build(id, entry, data_tbl, ps_tbl, create) if exists or create then if not exists then ps_tbl[id] = psil.create() +---@diagnostic disable-next-line: missing-fields data_tbl[id] = {} end @@ -627,10 +629,12 @@ function iocontrol.update_facility_status(status) -- induction matricies statuses if type(rtu_statuses.induction) == "table" then + local matrix_status = MTX_STATE.OFFLINE + for id = 1, #fac.induction_ps_tbl do if rtu_statuses.induction[id] == nil then -- disconnected - fac.induction_ps_tbl[id].publish("computed_status", 1) + fac.induction_ps_tbl[id].publish("computed_status", matrix_status) end end @@ -642,18 +646,20 @@ function iocontrol.update_facility_status(status) local rtu_faulted = _record_multiblock_status(matrix, data, ps) if rtu_faulted then - ps.publish("computed_status", 3) -- faulted + matrix_status = MTX_STATE.FAULT elseif data.formed then if data.tanks.energy_fill >= 0.99 then - ps.publish("computed_status", 6) -- full + matrix_status = MTX_STATE.HIGH_CHARGE elseif data.tanks.energy_fill <= 0.01 then - ps.publish("computed_status", 5) -- empty + matrix_status = MTX_STATE.LOW_CHARGE else - ps.publish("computed_status", 4) -- on-line + matrix_status = MTX_STATE.ONLINE end else - ps.publish("computed_status", 2) -- not formed + matrix_status = MTX_STATE.UNFORMED end + + ps.publish("computed_status", matrix_status) else log.debug(util.c(log_header, "invalid induction matrix id ", id)) end @@ -665,12 +671,12 @@ function iocontrol.update_facility_status(status) -- SPS statuses if type(rtu_statuses.sps) == "table" then - local sps_status = 1 + local sps_status = SPS_STATE.OFFLINE for id = 1, #fac.sps_ps_tbl do if rtu_statuses.sps[id] == nil then -- disconnected - fac.sps_ps_tbl[id].publish("computed_status", 1) + fac.sps_ps_tbl[id].publish("computed_status", sps_status) end end @@ -682,11 +688,11 @@ function iocontrol.update_facility_status(status) local rtu_faulted = _record_multiblock_status(sps, data, ps) if rtu_faulted then - sps_status = 3 -- faulted + sps_status = SPS_STATE.FAULT elseif data.formed then -- active / idle - sps_status = util.trinary(data.state.process_rate > 0, 5, 4) - else sps_status = 2 end -- not formed + sps_status = util.trinary(data.state.process_rate > 0, SPS_STATE.ACTIVE, SPS_STATE.IDLE) + else sps_status = SPS_STATE.UNFORMED end ps.publish("computed_status", sps_status) @@ -695,8 +701,6 @@ function iocontrol.update_facility_status(status) log.debug(util.c(log_header, "invalid sps id ", id)) end end - - io.facility.sps_status = sps_status else log.debug(log_header .. "sps list not a table") valid = false @@ -704,10 +708,12 @@ function iocontrol.update_facility_status(status) -- dynamic tank statuses if type(rtu_statuses.tanks) == "table" then + local tank_status = TNK_STATE.OFFLINE + for id = 1, #fac.tank_ps_tbl do if rtu_statuses.tanks[id] == nil then -- disconnected - fac.tank_ps_tbl[id].publish("computed_status", 1) + fac.tank_ps_tbl[id].publish("computed_status", tank_status) end end @@ -719,18 +725,18 @@ function iocontrol.update_facility_status(status) local rtu_faulted = _record_multiblock_status(tank, data, ps) if rtu_faulted then - ps.publish("computed_status", 3) -- faulted + tank_status = TNK_STATE.FAULT elseif data.formed then if data.tanks.fill >= 0.99 then - ps.publish("computed_status", 6) -- full + tank_status = TNK_STATE.HIGH_FILL elseif data.tanks.fill < 0.20 then - ps.publish("computed_status", 5) -- low + tank_status = TNK_STATE.LOW_FILL else - ps.publish("computed_status", 4) -- on-line + tank_status = TNK_STATE.ONLINE end - else - ps.publish("computed_status", 2) -- not formed - end + else tank_status = TNK_STATE.UNFORMED end + + ps.publish("computed_status", tank_status) else log.debug(util.c(log_header, "invalid dynamic tank id ", id)) end @@ -830,9 +836,11 @@ function iocontrol.update_unit_statuses(statuses) log.debug(log_header .. "reactor status not a table") end + local computed_status = RCT_STATE.OFFLINE + if #reactor_status == 0 then unit.connected = false - unit.unit_ps.publish("computed_status", 1) -- disconnected + unit.unit_ps.publish("computed_status", computed_status) elseif #reactor_status == 3 then local mek_status = reactor_status[1] local rps_status = reactor_status[2] @@ -871,22 +879,23 @@ function iocontrol.update_unit_statuses(statuses) burn_rate_sum = burn_rate_sum + burn_rate if unit.reactor_data.mek_status.status then - unit.unit_ps.publish("computed_status", 5) -- running + computed_status = RCT_STATE.ACTIVE else if unit.reactor_data.no_reactor then - unit.unit_ps.publish("computed_status", 3) -- faulted + computed_status = RCT_STATE.FAULT elseif not unit.reactor_data.formed then - unit.unit_ps.publish("computed_status", 2) -- multiblock not formed + computed_status = RCT_STATE.UNFORMED elseif unit.reactor_data.rps_status.force_dis then - unit.unit_ps.publish("computed_status", 7) -- reactor force disabled + computed_status = RCT_STATE.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 + computed_status = RCT_STATE.SCRAMMED else - unit.unit_ps.publish("computed_status", 4) -- disabled + computed_status = RCT_STATE.DISABLED end end unit.connected = true + unit.unit_ps.publish("computed_status", computed_status) else log.debug(log_header .. "reactor status length mismatch") valid = false @@ -900,13 +909,11 @@ function iocontrol.update_unit_statuses(statuses) if type(rtu_statuses.boilers) == "table" then local boil_sum = 0 - for id = 1, #unit.boiler_ps_tbl do - local connected = rtu_statuses.boilers[id] ~= nil - unit.rtu_hw.boilers[id].connected = connected + computed_status = BLR_STATE.OFFLINE - if not connected then - -- disconnected - unit.boiler_ps_tbl[id].publish("computed_status", 1) + for id = 1, #unit.boiler_ps_tbl do + if rtu_statuses.boilers[id] == nil then + unit.boiler_ps_tbl[id].publish("computed_status", computed_status) end end @@ -916,21 +923,15 @@ function iocontrol.update_unit_statuses(statuses) local ps = unit.boiler_ps_tbl[id] local rtu_faulted = _record_multiblock_status(boiler, data, ps) - unit.rtu_hw.boilers[id].faulted = rtu_faulted if rtu_faulted then - ps.publish("computed_status", 3) -- faulted + computed_status = BLR_STATE.FAULT elseif data.formed then boil_sum = boil_sum + data.state.boil_rate + computed_status = util.trinary(data.state.boil_rate > 0, BLR_STATE.ACTIVE, BLR_STATE.IDLE) + else computed_status = BLR_STATE.UNFORMED end - if data.state.boil_rate > 0 then - ps.publish("computed_status", 5) -- active - else - ps.publish("computed_status", 4) -- idle - end - else - ps.publish("computed_status", 2) -- not formed - end + unit.boiler_ps_tbl[id].publish("computed_status", computed_status) else log.debug(util.c(log_header, "invalid boiler id ", id)) valid = false @@ -947,13 +948,11 @@ function iocontrol.update_unit_statuses(statuses) if type(rtu_statuses.turbines) == "table" then local flow_sum = 0 - for id = 1, #unit.turbine_ps_tbl do - local connected = rtu_statuses.turbines[id] ~= nil - unit.rtu_hw.turbines[id].connected = connected + computed_status = TRB_STATE.OFFLINE - if not connected then - -- disconnected - unit.turbine_ps_tbl[id].publish("computed_status", 1) + for id = 1, #unit.turbine_ps_tbl do + if rtu_statuses.turbines[id] == nil then + unit.turbine_ps_tbl[id].publish("computed_status", computed_status) end end @@ -963,23 +962,22 @@ function iocontrol.update_unit_statuses(statuses) local ps = unit.turbine_ps_tbl[id] local rtu_faulted = _record_multiblock_status(turbine, data, ps) - unit.rtu_hw.turbines[id].faulted = rtu_faulted if rtu_faulted then - ps.publish("computed_status", 3) -- faulted + computed_status = TRB_STATE.FAULT elseif data.formed then flow_sum = flow_sum + data.state.flow_rate if data.tanks.energy_fill >= 0.99 then - ps.publish("computed_status", 6) -- trip + computed_status = TRB_STATE.TRIPPED elseif data.state.flow_rate < 100 then - ps.publish("computed_status", 4) -- idle + computed_status = TRB_STATE.IDLE else - ps.publish("computed_status", 5) -- active + computed_status = TRB_STATE.ACTIVE end - else - ps.publish("computed_status", 2) -- not formed - end + else computed_status = TRB_STATE.UNFORMED end + + unit.turbine_ps_tbl[id].publish("computed_status", computed_status) else log.debug(util.c(log_header, "invalid turbine id ", id)) valid = false @@ -994,10 +992,11 @@ function iocontrol.update_unit_statuses(statuses) -- dynamic tank statuses if type(rtu_statuses.tanks) == "table" then + computed_status = TNK_STATE.OFFLINE + for id = 1, #unit.tank_ps_tbl do if rtu_statuses.tanks[id] == nil then - -- disconnected - unit.tank_ps_tbl[id].publish("computed_status", 1) + unit.tank_ps_tbl[id].publish("computed_status", computed_status) end end @@ -1009,18 +1008,18 @@ function iocontrol.update_unit_statuses(statuses) local rtu_faulted = _record_multiblock_status(tank, data, ps) if rtu_faulted then - ps.publish("computed_status", 3) -- faulted + computed_status = TNK_STATE.FAULT elseif data.formed then if data.tanks.fill >= 0.99 then - ps.publish("computed_status", 6) -- full + computed_status = TNK_STATE.HIGH_FILL elseif data.tanks.fill < 0.20 then - ps.publish("computed_status", 5) -- low + computed_status = TNK_STATE.LOW_FILL else - ps.publish("computed_status", 4) -- on-line + computed_status = TNK_STATE.ONLINE end - else - ps.publish("computed_status", 2) -- not formed - end + else computed_status = TNK_STATE.UNFORMED end + + unit.tank_ps_tbl[id].publish("computed_status", computed_status) else log.debug(util.c(log_header, "invalid dynamic tank id ", id)) valid = false @@ -1085,6 +1084,7 @@ function iocontrol.update_unit_statuses(statuses) unit.annunciator = status[3] if type(unit.annunciator) ~= "table" then +---@diagnostic disable-next-line: missing-fields unit.annunciator = {} log.debug(log_header .. "annunciator state not a table") valid = false diff --git a/coordinator/process.lua b/coordinator/process.lua index 1f2b914..1866686 100644 --- a/coordinator/process.lua +++ b/coordinator/process.lua @@ -286,6 +286,7 @@ function process.create_handle() handle.unit_ack = {} for u = 1, pctl.io.facility.num_units do +---@diagnostic disable-next-line: missing-fields handle.unit_ack[u] = {} ---@class process_unit_ack diff --git a/coordinator/session/pocket.lua b/coordinator/session/pocket.lua index 6d60ab0..c8b77de 100644 --- a/coordinator/session/pocket.lua +++ b/coordinator/session/pocket.lua @@ -269,11 +269,17 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout) if pkt.length == 1 and type(pkt.data[1]) == "number" then local u = db.units[pkt.data[1]] + local statuses = { u.unit_ps.get("computed_status") } + + for i = 1, #u.boiler_ps_tbl do table.insert(statuses, u.boiler_ps_tbl[i].get("computed_status")) end + for i = 1, #u.turbine_ps_tbl do table.insert(statuses, u.turbine_ps_tbl[i].get("computed_status")) end + for i = 1, #u.tank_ps_tbl do table.insert(statuses, u.tank_ps_tbl[i].get("computed_status")) end + if u then local data = { u.unit_id, u.connected, - u.rtu_hw, + statuses, u.a_group, u.alarms, u.annunciator, @@ -375,7 +381,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout) proc.pu_fallback, proc.sps_low_power, fac.waste_stats, - fac.sps_status, + fac.sps_ps_tbl[1].get("computed_status") or types.SPS_STATE.OFFLINE, process_rate } diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 52ce676..8f7f1a6 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") local threads = require("coordinator.threads") -local COORDINATOR_VERSION = "v1.5.16" +local COORDINATOR_VERSION = "v1.5.17" local CHUNK_LOAD_DELAY_S = 30.0 diff --git a/coordinator/ui/style.lua b/coordinator/ui/style.lua index 306fe2e..6f8f795 100644 --- a/coordinator/ui/style.lua +++ b/coordinator/ui/style.lua @@ -147,235 +147,102 @@ style.gray_white = cpair(colors.gray, colors.white) -- UI COMPONENTS -- style.reactor = { - -- reactor states + -- reactor states
+ ---@see REACTOR_STATE states = { - { - color = cpair(colors.black, colors.yellow), - text = "PLC OFF-LINE" - }, - { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - }, - { - color = cpair(colors.black, colors.orange), - text = "PLC FAULT" - }, - { - color = cpair(colors.white, colors.gray), - text = "DISABLED" - }, - { - color = cpair(colors.black, colors.green), - text = "ACTIVE" - }, - { - color = cpair(colors.black, colors.red), - text = "SCRAMMED" - }, - { - color = cpair(colors.black, colors.red), - text = "FORCE DISABLED" - } + { color = cpair(colors.black, colors.yellow), text = "PLC OFF-LINE" }, + { color = cpair(colors.black, colors.orange), text = "NOT FORMED" }, + { color = cpair(colors.black, colors.orange), text = "PLC FAULT" }, + { color = cpair(colors.white, colors.gray), text = "DISABLED" }, + { color = cpair(colors.black, colors.green), text = "ACTIVE" }, + { color = cpair(colors.black, colors.red), text = "SCRAMMED" }, + { color = cpair(colors.black, colors.red), text = "FORCE DISABLED" } } } style.boiler = { - -- boiler states + -- boiler states
+ ---@see BOILER_STATE states = { - { - color = cpair(colors.black, colors.yellow), - text = "OFF-LINE" - }, - { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - }, - { - color = cpair(colors.black, colors.orange), - text = "RTU FAULT" - }, - { - color = cpair(colors.white, colors.gray), - text = "IDLE" - }, - { - color = cpair(colors.black, colors.green), - text = "ACTIVE" - } + { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, + { color = cpair(colors.black, colors.orange), text = "NOT FORMED" }, + { color = cpair(colors.black, colors.orange), text = "RTU FAULT" }, + { color = cpair(colors.white, colors.gray), text = "IDLE" }, + { color = cpair(colors.black, colors.green), text = "ACTIVE" } } } style.turbine = { - -- turbine states + -- turbine states
+ ---@see TURBINE_STATE states = { - { - color = cpair(colors.black, colors.yellow), - text = "OFF-LINE" - }, - { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - }, - { - color = cpair(colors.black, colors.orange), - text = "RTU FAULT" - }, - { - color = cpair(colors.white, colors.gray), - text = "IDLE" - }, - { - color = cpair(colors.black, colors.green), - text = "ACTIVE" - }, - { - color = cpair(colors.black, colors.red), - text = "TRIP" - } - } -} - -style.imatrix = { - -- induction matrix states - states = { - { - color = cpair(colors.black, colors.yellow), - text = "OFF-LINE" - }, - { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - }, - { - color = cpair(colors.black, colors.orange), - text = "RTU FAULT" - }, - { - color = cpair(colors.black, colors.green), - text = "ONLINE" - }, - { - color = cpair(colors.black, colors.yellow), - text = "LOW CHARGE" - }, - { - color = cpair(colors.black, colors.yellow), - text = "HIGH CHARGE" - } - } -} - -style.sps = { - -- SPS states - states = { - { - color = cpair(colors.black, colors.yellow), - text = "OFF-LINE" - }, - { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - }, - { - color = cpair(colors.black, colors.orange), - text = "RTU FAULT" - }, - { - color = cpair(colors.white, colors.gray), - text = "IDLE" - }, - { - color = cpair(colors.black, colors.green), - text = "ACTIVE" - } + { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, + { color = cpair(colors.black, colors.orange), text = "NOT FORMED" }, + { color = cpair(colors.black, colors.orange), text = "RTU FAULT" }, + { color = cpair(colors.white, colors.gray), text = "IDLE" }, + { color = cpair(colors.black, colors.green), text = "ACTIVE" }, + { color = cpair(colors.black, colors.red), text = "TRIP" } } } style.dtank = { - -- dynamic tank states + -- dynamic tank states
+ ---@see TANK_STATE states = { - { - color = cpair(colors.black, colors.yellow), - text = "OFF-LINE" - }, - { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - }, - { - color = cpair(colors.black, colors.orange), - text = "RTU FAULT" - }, - { - color = cpair(colors.black, colors.green), - text = "ONLINE" - }, - { - color = cpair(colors.black, colors.yellow), - text = "LOW FILL" - }, - { - color = cpair(colors.black, colors.green), - text = "FILLED" - }, + { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, + { color = cpair(colors.black, colors.orange), text = "NOT FORMED" }, + { color = cpair(colors.black, colors.orange), text = "RTU FAULT" }, + { color = cpair(colors.black, colors.green), text = "ONLINE" }, + { color = cpair(colors.black, colors.yellow), text = "LOW FILL" }, + { color = cpair(colors.black, colors.green), text = "FILLED" } + } +} + +style.imatrix = { + -- induction matrix states
+ ---@see IMATRIX_STATE + states = { + { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, + { color = cpair(colors.black, colors.orange), text = "NOT FORMED" }, + { color = cpair(colors.black, colors.orange), text = "RTU FAULT" }, + { color = cpair(colors.black, colors.green), text = "ONLINE" }, + { color = cpair(colors.black, colors.yellow), text = "LOW CHARGE" }, + { color = cpair(colors.black, colors.yellow), text = "HIGH CHARGE" } + } +} + +style.sps = { + -- SPS states
+ ---@see SPS_STATE + states = { + { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, + { color = cpair(colors.black, colors.orange), text = "NOT FORMED" }, + { color = cpair(colors.black, colors.orange), text = "RTU FAULT" }, + { color = cpair(colors.white, colors.gray), text = "IDLE" }, + { color = cpair(colors.black, colors.green), text = "ACTIVE" } } } style.waste = { -- auto waste processing states states = { - { - color = cpair(colors.black, colors.green), - text = "PLUTONIUM" - }, - { - color = cpair(colors.black, colors.cyan), - text = "POLONIUM" - }, - { - color = cpair(colors.black, colors.purple), - text = "ANTI MATTER" - } + { color = cpair(colors.black, colors.green), text = "PLUTONIUM" }, + { color = cpair(colors.black, colors.cyan), text = "POLONIUM" }, + { color = cpair(colors.black, colors.purple), text = "ANTI MATTER" } }, states_abbrv = { - { - color = cpair(colors.black, colors.green), - text = "Pu" - }, - { - color = cpair(colors.black, colors.cyan), - text = "Po" - }, - { - color = cpair(colors.black, colors.purple), - text = "AM" - } + { color = cpair(colors.black, colors.green), text = "Pu" }, + { color = cpair(colors.black, colors.cyan), text = "Po" }, + { color = cpair(colors.black, colors.purple), text = "AM" } }, -- process radio button options options = { "Plutonium", "Polonium", "Antimatter" }, -- unit waste selection unit_opts = { - { - text = "Auto", - fg_bg = cpair(colors.black, colors.lightGray), - active_fg_bg = cpair(colors.white, colors.gray) - }, - { - text = "Pu", - fg_bg = cpair(colors.black, colors.lightGray), - active_fg_bg = cpair(colors.black, colors.green) - }, - { - text = "Po", - fg_bg = cpair(colors.black, colors.lightGray), - active_fg_bg = cpair(colors.black, colors.cyan) - }, - { - text = "AM", - fg_bg = cpair(colors.black, colors.lightGray), - active_fg_bg = cpair(colors.black, colors.purple) - } + { text = "Auto", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.white, colors.gray) }, + { text = "Pu", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.green) }, + { text = "Po", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.cyan) }, + { text = "AM", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.purple) } } } diff --git a/pocket/iocontrol.lua b/pocket/iocontrol.lua index 0a3af09..b860aa4 100644 --- a/pocket/iocontrol.lua +++ b/pocket/iocontrol.lua @@ -2,11 +2,11 @@ -- I/O Control for Pocket Integration with Supervisor & Coordinator -- -local const = require("scada-common.constants") local psil = require("scada-common.psil") local types = require("scada-common.types") local util = require("scada-common.util") +local iorx = require("pocket.iorx") local process = require("pocket.process") local ALARM = types.ALARM @@ -50,6 +50,8 @@ function iocontrol.init_core(pkt_comms, nav, cfg) comms = pkt_comms config = cfg + iocontrol.rx = iorx(io) + io.nav = nav ---@class pocket_ioctl_diag @@ -194,8 +196,6 @@ function iocontrol.init_fac(conf) local entry = { unit_id = i, connected = false, - ---@type { boilers: { connected: boolean, faulted: boolean }[], turbines: { connected: boolean, faulted: boolean }[] } - rtu_hw = {}, num_boilers = 0, num_turbines = 0, @@ -239,6 +239,7 @@ function iocontrol.init_fac(conf) ---@type { [ALARM]: ALARM_STATE } alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE }, +---@diagnostic disable-next-line: missing-fields annunciator = {}, ---@type annunciator unit_ps = psil.create(), @@ -347,642 +348,6 @@ function iocontrol.report_crd_tt(trip_time) io.ps.publish("crd_conn_quality", state) end --- populate facility data from API_GET_FAC ----@param data table ----@return boolean valid -function iocontrol.record_facility_data(data) - local valid = true - - local fac = io.facility - - fac.all_sys_ok = data[1] - fac.rtu_count = data[2] - fac.radiation = data[3] - - -- auto control - if type(data[4]) == "table" and #data[4] == 4 then - fac.auto_ready = data[4][1] - fac.auto_active = data[4][2] - fac.auto_ramping = data[4][3] - fac.auto_saturated = data[4][4] - end - - -- waste - if type(data[5]) == "table" and #data[5] == 2 then - fac.auto_current_waste_product = data[5][1] - fac.auto_pu_fallback_active = data[5][2] - end - - fac.num_tanks = data[6] - fac.has_imatrix = data[7] - fac.has_sps = data[8] - - return valid -end - -local function tripped(state) return state == ALARM_STATE.TRIPPED or state == ALARM_STATE.ACKED end - -local function _record_multiblock_status(faulted, data, ps) - ps.publish("formed", data.formed) - ps.publish("faulted", faulted) - - for key, val in pairs(data.state) do ps.publish(key, val) end - for key, val in pairs(data.tanks) do ps.publish(key, val) end -end - --- update unit status data from API_GET_UNIT ----@param data table -function iocontrol.record_unit_data(data) - local unit = io.units[data[1]] - - unit.connected = data[2] - unit.rtu_hw = data[3] - unit.a_group = data[4] - unit.alarms = data[5] - - unit.unit_ps.publish("auto_group_id", unit.a_group) - unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1]) - - --#region Annunciator - - unit.annunciator = data[6] - - local rcs_disconn, rcs_warn, rcs_hazard = false, false, false - - for key, val in pairs(unit.annunciator) do - if key == "BoilerOnline" or key == "TurbineOnline" then - local every = true - - -- split up online arrays - for id = 1, #val do - every = every and val[id] - - if key == "BoilerOnline" then - unit.boiler_ps_tbl[id].publish(key, val[id]) - else - unit.turbine_ps_tbl[id].publish(key, val[id]) - end - end - - if not every then rcs_disconn = true end - - unit.unit_ps.publish("U_" .. key, every) - elseif key == "HeatingRateLow" or key == "WaterLevelLow" then - -- split up array for all boilers - local any = false - for id = 1, #val do - any = any or val[id] - unit.boiler_ps_tbl[id].publish(key, val[id]) - end - - if key == "HeatingRateLow" and any then - rcs_warn = true - elseif key == "WaterLevelLow" and any then - rcs_hazard = true - end - - unit.unit_ps.publish("U_" .. key, any) - elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then - -- split up array for all turbines - local any = false - for id = 1, #val do - any = any or val[id] - unit.turbine_ps_tbl[id].publish(key, val[id]) - end - - if key == "GeneratorTrip" and any then - rcs_warn = true - elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then - rcs_hazard = true - end - - unit.unit_ps.publish("U_" .. key, any) - else - -- non-table fields - unit.unit_ps.publish(key, val) - end - end - - local anc = unit.annunciator - rcs_hazard = rcs_hazard or anc.RCPTrip - rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or - anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch - - local rcs_status = 4 - if rcs_hazard then - rcs_status = 2 - elseif rcs_warn then - rcs_status = 3 - elseif rcs_disconn then - rcs_status = 1 - end - - unit.unit_ps.publish("U_RCS", rcs_status) - - --#endregion - - --#region Reactor Data - - unit.reactor_data = data[7] - - local control_status = 1 - local reactor_status = 1 - local reactor_state = 1 - local rps_status = 1 - - if unit.connected then - -- update RPS status - if unit.reactor_data.rps_tripped then - control_status = 2 - - if unit.reactor_data.rps_trip_cause == "manual" then - reactor_state = 4 -- disabled - rps_status = 3 - else - reactor_state = 6 -- SCRAM - rps_status = 2 - end - else - rps_status = 4 - reactor_state = 4 - end - - -- update reactor/control status - if unit.reactor_data.mek_status.status then - reactor_status = 4 - reactor_state = 5 -- running - control_status = util.trinary(unit.annunciator.AutoControl, 4, 3) - else - if unit.reactor_data.no_reactor then - reactor_status = 2 - reactor_state = 3 -- faulted - elseif not unit.reactor_data.formed then - reactor_status = 3 - reactor_state = 2 -- not formed - elseif unit.reactor_data.rps_status.force_dis then - reactor_status = 3 - reactor_state = 7 -- force disabled - else - reactor_status = 4 - 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 - - for key, val in pairs(unit.reactor_data.rps_status) do - unit.unit_ps.publish(key, val) - end - - for key, val in pairs(unit.reactor_data.mek_struct) do - unit.unit_ps.publish(key, val) - end - - for key, val in pairs(unit.reactor_data.mek_status) do - unit.unit_ps.publish(key, val) - end - end - - unit.unit_ps.publish("U_ControlStatus", control_status) - unit.unit_ps.publish("U_ReactorStatus", reactor_status) - unit.unit_ps.publish("U_ReactorStateStatus", reactor_state) - unit.unit_ps.publish("U_RPS", rps_status) - - --#endregion - - --#region RTU Devices - - unit.boiler_data_tbl = data[8] - - for id = 1, #unit.boiler_data_tbl do - local boiler = unit.boiler_data_tbl[id] - local ps = unit.boiler_ps_tbl[id] - - local boiler_status = 1 - local computed_status = 1 - - if unit.rtu_hw.boilers[id].connected then - if unit.rtu_hw.boilers[id].faulted then - boiler_status = 3 - computed_status = 3 - elseif boiler.formed then - boiler_status = 4 - - if boiler.state.boil_rate > 0 then - computed_status = 5 - else - computed_status = 4 - end - else - boiler_status = 2 - computed_status = 2 - end - - _record_multiblock_status(unit.rtu_hw.boilers[id].faulted, boiler, ps) - end - - ps.publish("BoilerStatus", boiler_status) - ps.publish("BoilerStateStatus", computed_status) - end - - unit.turbine_data_tbl = data[9] - - for id = 1, #unit.turbine_data_tbl do - local turbine = unit.turbine_data_tbl[id] - local ps = unit.turbine_ps_tbl[id] - - local turbine_status = 1 - local computed_status = 1 - - if unit.rtu_hw.turbines[id].connected then - if unit.rtu_hw.turbines[id].faulted then - turbine_status = 3 - computed_status = 3 - elseif turbine.formed then - turbine_status = 4 - - if turbine.tanks.energy_fill >= 0.99 then - computed_status = 6 - elseif turbine.state.flow_rate < 100 then - computed_status = 4 - else - computed_status = 5 - end - else - turbine_status = 2 - computed_status = 2 - end - - _record_multiblock_status(unit.rtu_hw.turbines[id].faulted, turbine, ps) - end - - ps.publish("TurbineStatus", turbine_status) - ps.publish("TurbineStateStatus", computed_status) - end - - unit.tank_data_tbl = data[10] - - unit.last_rate_change_ms = data[11] - unit.turbine_flow_stable = data[12] - - --#endregion - - --#region Status Information Display - - local ecam = {} -- aviation reference :) - - -- local function red(text) return { text = text, color = colors.red } end - local function white(text) return { text = text, color = colors.white } end - local function blue(text) return { text = text, color = colors.blue } end - - -- if unit.reactor_data.rps_status then - -- for k, v in pairs(unit.alarms) do - -- unit.alarms[k] = ALARM_STATE.TRIPPED - -- end - -- end - - if tripped(unit.alarms[ALARM.ContainmentBreach]) then - local items = { white("REACTOR MELTDOWN"), blue("DON HAZMAT SUIT") } - table.insert(ecam, { color = colors.red, text = "CONTAINMENT BREACH", help = "ContainmentBreach", items = items }) - end - - if tripped(unit.alarms[ALARM.ContainmentRadiation]) then - local items = { - white("RADIATION DETECTED"), - blue("DON HAZMAT SUIT"), - blue("RESOLVE LEAK"), - blue("AWAIT SAFE LEVELS") - } - - table.insert(ecam, { color = colors.red, text = "RADIATION LEAK", help = "ContainmentRadiation", items = items }) - end - - if tripped(unit.alarms[ALARM.CriticalDamage]) then - local items = { white("MELTDOWN IMMINENT"), blue("EVACUATE") } - table.insert(ecam, { color = colors.red, text = "RCT DAMAGE CRITICAL", help = "CriticalDamage", items = items }) - end - - if tripped(unit.alarms[ALARM.ReactorLost]) then - local items = { white("REACTOR OFF-LINE"), blue("CHECK PLC") } - table.insert(ecam, { color = colors.red, text = "REACTOR CONN LOST", help = "ReactorLost", items = items }) - end - - if tripped(unit.alarms[ALARM.ReactorDamage]) then - local items = { white("REACTOR DAMAGED"), blue("CHECK RCS"), blue("AWAIT DMG REDUCED") } - table.insert(ecam, { color = colors.red, text = "REACTOR DAMAGE", help = "ReactorDamage", items = items }) - end - - if tripped(unit.alarms[ALARM.ReactorOverTemp]) then - local items = { white("DAMAGING TEMP"), blue("CHECK RCS"), blue("AWAIT COOLDOWN") } - table.insert(ecam, { color = colors.red, text = "REACTOR OVER TEMP", help = "ReactorOverTemp", items = items }) - end - - if tripped(unit.alarms[ALARM.ReactorHighTemp]) then - local items = { white("OVER EXPECTED TEMP"), blue("CHECK RCS") } - table.insert(ecam, { color = colors.yellow, text = "REACTOR HIGH TEMP", help = "ReactorHighTemp", items = items}) - end - - if tripped(unit.alarms[ALARM.ReactorWasteLeak]) then - local items = { white("AT WASTE CAPACITY"), blue("CHECK WASTE OUTPUT"), blue("KEEP RCT DISABLED") } - table.insert(ecam, { color = colors.red, text = "REACTOR WASTE LEAK", help = "ReactorWasteLeak", items = items}) - end - - if tripped(unit.alarms[ALARM.ReactorHighWaste]) then - local items = { blue("CHECK WASTE OUTPUT") } - table.insert(ecam, { color = colors.yellow, text = "REACTOR WASTE HIGH", help = "ReactorHighWaste", items = items}) - end - - if tripped(unit.alarms[ALARM.RPSTransient]) then - local items = {} - local stat = unit.reactor_data.rps_status - - -- for k, _ in pairs(stat) do stat[k] = true end - - local function insert(cond, key, text, color) if cond[key] then table.insert(items, { text = text, help = key, color = color }) end end - - table.insert(items, white("REACTOR SCRAMMED")) - insert(stat, "high_dmg", "HIGH DAMAGE", colors.red) - insert(stat, "high_temp", "HIGH TEMPERATURE", colors.red) - insert(stat, "low_cool", "CRIT LOW COOLANT") - insert(stat, "ex_waste", "EXCESS WASTE") - insert(stat, "ex_hcool", "EXCESS HEATED COOL") - insert(stat, "no_fuel", "NO FUEL") - insert(stat, "fault", "HARDWARE FAULT") - insert(stat, "timeout", "SUPERVISOR DISCONN") - insert(stat, "manual", "MANUAL SCRAM", colors.white) - insert(stat, "automatic", "AUTOMATIC SCRAM") - insert(stat, "sys_fail", "NOT FORMED", colors.red) - insert(stat, "force_dis", "FORCE DISABLED", colors.red) - table.insert(items, blue("RESOLVE PROBLEM")) - table.insert(items, blue("RESET RPS")) - - table.insert(ecam, { color = colors.yellow, text = "RPS TRANSIENT", help = "RPSTransient", items = items}) - end - - if tripped(unit.alarms[ALARM.RCSTransient]) then - local items = {} - local annunc = unit.annunciator - - -- for k, v in pairs(annunc) do - -- if type(v) == "boolean" then annunc[k] = true end - -- if type(v) == "table" then - -- for a, _ in pairs(v) do - -- v[a] = true - -- end - -- end - -- end - - local function insert(cond, key, text, color) - if cond == true or (type(cond) == "table" and cond[key]) then table.insert(items, { text = text, help = key, color = color }) end - end - - table.insert(items, white("COOLANT PROBLEM")) - - insert(annunc, "RCPTrip", "RCP TRIP", colors.red) - insert(annunc, "CoolantLevelLow", "LOW COOLANT") - - if unit.num_boilers == 0 then - if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then - insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH") - end - - if unit.turbine_flow_stable then - insert(annunc, "RCSFlowLow", "RCS FLOW LOW") - insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH") - insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH") - end - else - if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then - insert(annunc, "RCSFlowLow", "RCS FLOW LOW") - insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH") - insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH") - end - - if unit.turbine_flow_stable then - insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH") - end - end - - insert(annunc, "MaxWaterReturnFeed", "MAX WTR RTRN FEED") - - for k, v in ipairs(annunc.WaterLevelLow) do insert(v, "WaterLevelLow", "BOILER " .. k .. " WTR LOW", colors.red) end - for k, v in ipairs(annunc.HeatingRateLow) do insert(v, "HeatingRateLow", "BOILER " .. k .. " HEAT RATE") end - for k, v in ipairs(annunc.TurbineOverSpeed) do insert(v, "TurbineOverSpeed", "TURBINE " .. k .. " OVERSPD", colors.red) end - for k, v in ipairs(annunc.GeneratorTrip) do insert(v, "GeneratorTrip", "TURBINE " .. k .. " GEN TRIP") end - - table.insert(items, blue("CHECK COOLING SYS")) - - table.insert(ecam, { color = colors.yellow, text = "RCS TRANSIENT", help = "RCSTransient", items = items}) - end - - if tripped(unit.alarms[ALARM.TurbineTrip]) then - local items = {} - - for k, v in ipairs(unit.annunciator.TurbineTrip) do - if v then table.insert(items, { text = "TURBINE " .. k .. " TRIP", help = "TurbineTrip" }) end - end - - table.insert(items, blue("CHECK ENERGY OUT")) - table.insert(ecam, { color = colors.red, text = "TURBINE TRIP", help = "TurbineTripAlarm", items = items}) - end - - if not (tripped(unit.alarms[ALARM.ReactorLost]) or unit.connected) then - local items = { blue("CHECK PLC") } - table.insert(ecam, { color = colors.yellow, text = "REACTOR OFF-LINE", items = items }) - end - - for k, v in ipairs(unit.annunciator.BoilerOnline) do - if not v then - local items = { blue("CHECK RTU") } - table.insert(ecam, { color = colors.yellow, text = "BOILER " .. k .. " OFF-LINE", items = items}) - end - end - - for k, v in ipairs(unit.annunciator.TurbineOnline) do - if not v then - local items = { blue("CHECK RTU") } - table.insert(ecam, { color = colors.yellow, text = "TURBINE " .. k .. " OFF-LINE", items = items}) - end - end - - -- if no alarms, put some basic status messages in - if #ecam == 0 then - table.insert(ecam, { color = colors.green, text = "REACTOR " .. util.trinary(unit.reactor_data.mek_status.status, "NOMINAL", "IDLE"), items = {}}) - - local plural = util.trinary(unit.num_turbines > 1, "S", "") - table.insert(ecam, { color = colors.green, text = "TURBINE" .. plural .. util.trinary(unit.turbine_flow_stable, " STABLE", " STABILIZING"), items = {}}) - end - - unit.unit_ps.publish("U_ECAM", textutils.serialize(ecam)) - - --#endregion -end - --- update control app with unit data from API_GET_CTRL ----@param data table -function iocontrol.record_control_data(data) - for u_id = 1, #data do - local unit = io.units[u_id] - local u_data = data[u_id] - - unit.connected = u_data[1] - - unit.reactor_data.rps_tripped = u_data[2] - unit.unit_ps.publish("rps_tripped", u_data[2]) - unit.reactor_data.mek_status.status = u_data[3] - unit.unit_ps.publish("status", u_data[3]) - unit.reactor_data.mek_status.temp = u_data[4] - unit.unit_ps.publish("temp", u_data[4]) - unit.reactor_data.mek_status.burn_rate = u_data[5] - unit.unit_ps.publish("burn_rate", u_data[5]) - unit.reactor_data.mek_status.act_burn_rate = u_data[6] - unit.unit_ps.publish("act_burn_rate", u_data[6]) - unit.reactor_data.mek_struct.max_burn = u_data[7] - unit.unit_ps.publish("max_burn", u_data[7]) - - unit.annunciator.AutoControl = u_data[8] - unit.unit_ps.publish("AutoControl", u_data[8]) - - unit.a_group = u_data[9] - unit.unit_ps.publish("auto_group_id", unit.a_group) - unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1]) - - local control_status = 1 - - if unit.connected then - if unit.reactor_data.rps_tripped then - control_status = 2 - end - - if unit.reactor_data.mek_status.status then - control_status = util.trinary(unit.annunciator.AutoControl, 4, 3) - end - end - - unit.unit_ps.publish("U_ControlStatus", control_status) - end -end - --- update process app with unit data from API_GET_PROC ----@param data table -function iocontrol.record_process_data(data) - -- get unit data - for u_id = 1, #io.units do - local unit = io.units[u_id] - local u_data = data[u_id] - - unit.reactor_data.mek_status.status = u_data[1] - unit.reactor_data.mek_struct.max_burn = u_data[2] - unit.annunciator.AutoControl = u_data[6] - unit.a_group = u_data[7] - - unit.unit_ps.publish("status", u_data[1]) - unit.unit_ps.publish("max_burn", u_data[2]) - unit.unit_ps.publish("burn_limit", u_data[3]) - unit.unit_ps.publish("U_AutoReady", u_data[4]) - unit.unit_ps.publish("U_AutoDegraded", u_data[5]) - unit.unit_ps.publish("AutoControl", u_data[6]) - unit.unit_ps.publish("auto_group_id", unit.a_group) - unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1]) - end - - -- get facility data - local fac = io.facility - local f_data = data[#io.units + 1] - - fac.status_lines = f_data[1] - - fac.auto_ready = f_data[2][1] - fac.auto_active = f_data[2][2] - fac.auto_ramping = f_data[2][3] - fac.auto_saturated = f_data[2][4] - - fac.auto_scram = f_data[3] - fac.ascram_status = f_data[4] - - fac.ps.publish("status_line_1", fac.status_lines[1]) - fac.ps.publish("status_line_2", fac.status_lines[2]) - - fac.ps.publish("auto_ready", fac.auto_ready) - fac.ps.publish("auto_active", fac.auto_active) - fac.ps.publish("auto_ramping", fac.auto_ramping) - fac.ps.publish("auto_saturated", fac.auto_saturated) - - fac.ps.publish("auto_scram", fac.auto_scram) - fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault) - fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill) - fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm) - fac.ps.publish("as_radiation", fac.ascram_status.radiation) - fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault) - - fac.ps.publish("process_mode", f_data[5][1]) - fac.ps.publish("process_burn_target", f_data[5][2]) - fac.ps.publish("process_charge_target", f_data[5][3]) - fac.ps.publish("process_gen_target", f_data[5][4]) -end - --- update waste app with unit data from API_GET_WASTE ----@param data table -function iocontrol.record_waste_data(data) - -- get unit data - for u_id = 1, #io.units do - local unit = io.units[u_id] - local u_data = data[u_id] - - unit.waste_mode = u_data[1] - unit.waste_product = u_data[2] - unit.num_snas = u_data[3] - unit.sna_peak_rate = u_data[4] - unit.sna_max_rate = u_data[5] - unit.sna_out_rate = u_data[6] - unit.waste_stats = u_data[7] - - unit.unit_ps.publish("U_AutoWaste", unit.waste_mode == types.WASTE_MODE.AUTO) - unit.unit_ps.publish("U_WasteMode", unit.waste_mode) - unit.unit_ps.publish("U_WasteProduct", unit.waste_product) - - unit.unit_ps.publish("sna_count", unit.num_snas) - unit.unit_ps.publish("sna_peak_rate", unit.sna_peak_rate) - unit.unit_ps.publish("sna_max_rate", unit.sna_max_rate) - unit.unit_ps.publish("sna_out_rate", unit.sna_out_rate) - - unit.unit_ps.publish("pu_rate", unit.waste_stats[1]) - unit.unit_ps.publish("po_rate", unit.waste_stats[2]) - unit.unit_ps.publish("po_pl_rate", unit.waste_stats[3]) - end - - -- get facility data - local fac = io.facility - local f_data = data[#io.units + 1] - - fac.auto_current_waste_product = f_data[1] - fac.auto_pu_fallback_active = f_data[2] - fac.auto_sps_disabled = f_data[3] - - fac.ps.publish("current_waste_product", fac.auto_current_waste_product) - fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active) - fac.ps.publish("sps_disabled_low_power", fac.auto_sps_disabled) - - fac.ps.publish("process_waste_product", f_data[4]) - fac.ps.publish("process_pu_fallback", f_data[5]) - fac.ps.publish("process_sps_low_power", f_data[6]) - - fac.waste_stats = f_data[7] - - fac.ps.publish("burn_sum", fac.waste_stats[1]) - fac.ps.publish("pu_rate", fac.waste_stats[2]) - fac.ps.publish("po_rate", fac.waste_stats[3]) - fac.ps.publish("po_pl_rate", fac.waste_stats[4]) - fac.ps.publish("po_am_rate", fac.waste_stats[5]) - fac.ps.publish("spent_waste_rate", fac.waste_stats[6]) - - fac.ps.publish("sps_computed_status", f_data[8]) - fac.ps.publish("sps_process_rate", f_data[9]) -end - -- get the IO controller database function iocontrol.get_db() return io end diff --git a/pocket/iorx.lua b/pocket/iorx.lua new file mode 100644 index 0000000..0aac073 --- /dev/null +++ b/pocket/iorx.lua @@ -0,0 +1,657 @@ +-- +-- I/O Control's Data Receive (Rx) Handlers +-- + +local const = require("scada-common.constants") +local types = require("scada-common.types") +local util = require("scada-common.util") + +local ALARM = types.ALARM +local ALARM_STATE = types.ALARM_STATE + +local BLR_STATE = types.BOILER_STATE +local TRB_STATE = types.TURBINE_STATE +local TNK_STATE = types.TANK_STATE + +local io ---@type pocket_ioctl +local iorx = {} ---@class iorx + +-- populate facility data from API_GET_FAC +---@param data table +---@return boolean valid +function iorx.record_facility_data(data) + local valid = true + + local fac = io.facility + + fac.all_sys_ok = data[1] + fac.rtu_count = data[2] + fac.radiation = data[3] + + -- auto control + if type(data[4]) == "table" and #data[4] == 4 then + fac.auto_ready = data[4][1] + fac.auto_active = data[4][2] + fac.auto_ramping = data[4][3] + fac.auto_saturated = data[4][4] + end + + -- waste + if type(data[5]) == "table" and #data[5] == 2 then + fac.auto_current_waste_product = data[5][1] + fac.auto_pu_fallback_active = data[5][2] + end + + fac.num_tanks = data[6] + fac.has_imatrix = data[7] + fac.has_sps = data[8] + + return valid +end + +local function tripped(state) return state == ALARM_STATE.TRIPPED or state == ALARM_STATE.ACKED end + +local function _record_multiblock_status(faulted, data, ps) + ps.publish("formed", data.formed) + ps.publish("faulted", faulted) + + for key, val in pairs(data.state) do ps.publish(key, val) end + for key, val in pairs(data.tanks) do ps.publish(key, val) end +end + +-- update unit status data from API_GET_UNIT +---@param data table +function iorx.record_unit_data(data) + local unit = io.units[data[1]] + + unit.connected = data[2] + local comp_statuses = data[3] + unit.a_group = data[4] + unit.alarms = data[5] + + local next_c_stat = 1 + + unit.unit_ps.publish("auto_group_id", unit.a_group) + unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1]) + + --#region Annunciator + + unit.annunciator = data[6] + + local rcs_disconn, rcs_warn, rcs_hazard = false, false, false + + for key, val in pairs(unit.annunciator) do + if key == "BoilerOnline" or key == "TurbineOnline" then + local every = true + + -- split up online arrays + for id = 1, #val do + every = every and val[id] + + if key == "BoilerOnline" then + unit.boiler_ps_tbl[id].publish(key, val[id]) + else + unit.turbine_ps_tbl[id].publish(key, val[id]) + end + end + + if not every then rcs_disconn = true end + + unit.unit_ps.publish("U_" .. key, every) + elseif key == "HeatingRateLow" or key == "WaterLevelLow" then + -- split up array for all boilers + local any = false + for id = 1, #val do + any = any or val[id] + unit.boiler_ps_tbl[id].publish(key, val[id]) + end + + if key == "HeatingRateLow" and any then + rcs_warn = true + elseif key == "WaterLevelLow" and any then + rcs_hazard = true + end + + unit.unit_ps.publish("U_" .. key, any) + elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then + -- split up array for all turbines + local any = false + for id = 1, #val do + any = any or val[id] + unit.turbine_ps_tbl[id].publish(key, val[id]) + end + + if key == "GeneratorTrip" and any then + rcs_warn = true + elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then + rcs_hazard = true + end + + unit.unit_ps.publish("U_" .. key, any) + else + -- non-table fields + unit.unit_ps.publish(key, val) + end + end + + local anc = unit.annunciator + rcs_hazard = rcs_hazard or anc.RCPTrip + rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or + anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch + + local rcs_status = 4 + if rcs_hazard then + rcs_status = 2 + elseif rcs_warn then + rcs_status = 3 + elseif rcs_disconn then + rcs_status = 1 + end + + unit.unit_ps.publish("U_RCS", rcs_status) + + --#endregion + + --#region Reactor Data + + unit.reactor_data = data[7] + + local control_status = 1 + local reactor_status = 1 + local rps_status = 1 + + if unit.connected then + -- update RPS status + if unit.reactor_data.rps_tripped then + control_status = 2 + rps_status = util.trinary(unit.reactor_data.rps_trip_cause == "manual", 3, 2) + else + rps_status = 4 + end + + reactor_status = 4 -- ok, until proven otherwise + + -- update reactor/control status + if unit.reactor_data.mek_status.status then + control_status = util.trinary(unit.annunciator.AutoControl, 4, 3) + else + if unit.reactor_data.no_reactor then + reactor_status = 2 + elseif (not unit.reactor_data.formed) or unit.reactor_data.rps_status.force_dis then + reactor_status = 3 + 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 + + for key, val in pairs(unit.reactor_data.rps_status) do + unit.unit_ps.publish(key, val) + end + + for key, val in pairs(unit.reactor_data.mek_struct) do + unit.unit_ps.publish(key, val) + end + + for key, val in pairs(unit.reactor_data.mek_status) do + unit.unit_ps.publish(key, val) + end + end + + unit.unit_ps.publish("U_ControlStatus", control_status) + unit.unit_ps.publish("U_ReactorStatus", reactor_status) + unit.unit_ps.publish("U_ReactorStateStatus", comp_statuses[next_c_stat]) + unit.unit_ps.publish("U_RPS", rps_status) + + next_c_stat = next_c_stat + 1 + + --#endregion + + --#region RTU Devices + + unit.boiler_data_tbl = data[8] + + for id = 1, #unit.boiler_data_tbl do + local boiler = unit.boiler_data_tbl[id] + local ps = unit.boiler_ps_tbl[id] + local c_stat = comp_statuses[next_c_stat] + + local boiler_status = 1 + + if c_stat ~= BLR_STATE.OFFLINE then + if c_stat == BLR_STATE.FAULT then + boiler_status = 3 + elseif c_stat ~= BLR_STATE.UNFORMED then + boiler_status = 4 + else + boiler_status = 2 + end + + _record_multiblock_status(c_stat == BLR_STATE.FAULT, boiler, ps) + end + + ps.publish("BoilerStatus", boiler_status) + ps.publish("BoilerStateStatus", c_stat) + + next_c_stat = next_c_stat + 1 + end + + unit.turbine_data_tbl = data[9] + + for id = 1, #unit.turbine_data_tbl do + local turbine = unit.turbine_data_tbl[id] + local ps = unit.turbine_ps_tbl[id] + local c_stat = comp_statuses[next_c_stat] + + local turbine_status = 1 + + if c_stat ~= TRB_STATE.OFFLINE then + if c_stat == TRB_STATE.FAULT then + turbine_status = 3 + elseif turbine.formed then + turbine_status = 4 + else + turbine_status = 2 + end + + _record_multiblock_status(c_stat == TRB_STATE.FAULT, turbine, ps) + end + + ps.publish("TurbineStatus", turbine_status) + ps.publish("TurbineStateStatus", c_stat) + + next_c_stat = next_c_stat + 1 + end + + unit.tank_data_tbl = data[10] + + for id = 1, #unit.tank_data_tbl do + local tank = unit.tank_data_tbl[id] + local ps = unit.tank_ps_tbl[id] + local c_stat = comp_statuses[next_c_stat] + + local tank_status = 1 + + if c_stat ~= TNK_STATE.OFFLINE then + if c_stat == TNK_STATE.FAULT then + tank_status = 3 + elseif tank.formed then + tank_status = 4 + else + tank_status = 2 + end + + _record_multiblock_status(c_stat == TNK_STATE.FAULT, tank, ps) + end + + ps.publish("DynamicTankStatus", tank_status) + ps.publish("DynamicTankStateStatus", c_stat) + + next_c_stat = next_c_stat + 1 + end + + unit.last_rate_change_ms = data[11] + unit.turbine_flow_stable = data[12] + + --#endregion + + --#region Status Information Display + + local ecam = {} -- aviation reference :) + + -- local function red(text) return { text = text, color = colors.red } end + local function white(text) return { text = text, color = colors.white } end + local function blue(text) return { text = text, color = colors.blue } end + + -- if unit.reactor_data.rps_status then + -- for k, v in pairs(unit.alarms) do + -- unit.alarms[k] = ALARM_STATE.TRIPPED + -- end + -- end + + if tripped(unit.alarms[ALARM.ContainmentBreach]) then + local items = { white("REACTOR MELTDOWN"), blue("DON HAZMAT SUIT") } + table.insert(ecam, { color = colors.red, text = "CONTAINMENT BREACH", help = "ContainmentBreach", items = items }) + end + + if tripped(unit.alarms[ALARM.ContainmentRadiation]) then + local items = { + white("RADIATION DETECTED"), + blue("DON HAZMAT SUIT"), + blue("RESOLVE LEAK"), + blue("AWAIT SAFE LEVELS") + } + + table.insert(ecam, { color = colors.red, text = "RADIATION LEAK", help = "ContainmentRadiation", items = items }) + end + + if tripped(unit.alarms[ALARM.CriticalDamage]) then + local items = { white("MELTDOWN IMMINENT"), blue("EVACUATE") } + table.insert(ecam, { color = colors.red, text = "RCT DAMAGE CRITICAL", help = "CriticalDamage", items = items }) + end + + if tripped(unit.alarms[ALARM.ReactorLost]) then + local items = { white("REACTOR OFF-LINE"), blue("CHECK PLC") } + table.insert(ecam, { color = colors.red, text = "REACTOR CONN LOST", help = "ReactorLost", items = items }) + end + + if tripped(unit.alarms[ALARM.ReactorDamage]) then + local items = { white("REACTOR DAMAGED"), blue("CHECK RCS"), blue("AWAIT DMG REDUCED") } + table.insert(ecam, { color = colors.red, text = "REACTOR DAMAGE", help = "ReactorDamage", items = items }) + end + + if tripped(unit.alarms[ALARM.ReactorOverTemp]) then + local items = { white("DAMAGING TEMP"), blue("CHECK RCS"), blue("AWAIT COOLDOWN") } + table.insert(ecam, { color = colors.red, text = "REACTOR OVER TEMP", help = "ReactorOverTemp", items = items }) + end + + if tripped(unit.alarms[ALARM.ReactorHighTemp]) then + local items = { white("OVER EXPECTED TEMP"), blue("CHECK RCS") } + table.insert(ecam, { color = colors.yellow, text = "REACTOR HIGH TEMP", help = "ReactorHighTemp", items = items}) + end + + if tripped(unit.alarms[ALARM.ReactorWasteLeak]) then + local items = { white("AT WASTE CAPACITY"), blue("CHECK WASTE OUTPUT"), blue("KEEP RCT DISABLED") } + table.insert(ecam, { color = colors.red, text = "REACTOR WASTE LEAK", help = "ReactorWasteLeak", items = items}) + end + + if tripped(unit.alarms[ALARM.ReactorHighWaste]) then + local items = { blue("CHECK WASTE OUTPUT") } + table.insert(ecam, { color = colors.yellow, text = "REACTOR WASTE HIGH", help = "ReactorHighWaste", items = items}) + end + + if tripped(unit.alarms[ALARM.RPSTransient]) then + local items = {} + local stat = unit.reactor_data.rps_status + + -- for k, _ in pairs(stat) do stat[k] = true end + + local function insert(cond, key, text, color) if cond[key] then table.insert(items, { text = text, help = key, color = color }) end end + + table.insert(items, white("REACTOR SCRAMMED")) + insert(stat, "high_dmg", "HIGH DAMAGE", colors.red) + insert(stat, "high_temp", "HIGH TEMPERATURE", colors.red) + insert(stat, "low_cool", "CRIT LOW COOLANT") + insert(stat, "ex_waste", "EXCESS WASTE") + insert(stat, "ex_hcool", "EXCESS HEATED COOL") + insert(stat, "no_fuel", "NO FUEL") + insert(stat, "fault", "HARDWARE FAULT") + insert(stat, "timeout", "SUPERVISOR DISCONN") + insert(stat, "manual", "MANUAL SCRAM", colors.white) + insert(stat, "automatic", "AUTOMATIC SCRAM") + insert(stat, "sys_fail", "NOT FORMED", colors.red) + insert(stat, "force_dis", "FORCE DISABLED", colors.red) + table.insert(items, blue("RESOLVE PROBLEM")) + table.insert(items, blue("RESET RPS")) + + table.insert(ecam, { color = colors.yellow, text = "RPS TRANSIENT", help = "RPSTransient", items = items}) + end + + if tripped(unit.alarms[ALARM.RCSTransient]) then + local items = {} + local annunc = unit.annunciator + + -- for k, v in pairs(annunc) do + -- if type(v) == "boolean" then annunc[k] = true end + -- if type(v) == "table" then + -- for a, _ in pairs(v) do + -- v[a] = true + -- end + -- end + -- end + + local function insert(cond, key, text, color) + if cond == true or (type(cond) == "table" and cond[key]) then table.insert(items, { text = text, help = key, color = color }) end + end + + table.insert(items, white("COOLANT PROBLEM")) + + insert(annunc, "RCPTrip", "RCP TRIP", colors.red) + insert(annunc, "CoolantLevelLow", "LOW COOLANT") + + if unit.num_boilers == 0 then + if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then + insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH") + end + + if unit.turbine_flow_stable then + insert(annunc, "RCSFlowLow", "RCS FLOW LOW") + insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH") + insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH") + end + else + if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then + insert(annunc, "RCSFlowLow", "RCS FLOW LOW") + insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH") + insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH") + end + + if unit.turbine_flow_stable then + insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH") + end + end + + insert(annunc, "MaxWaterReturnFeed", "MAX WTR RTRN FEED") + + for k, v in ipairs(annunc.WaterLevelLow) do insert(v, "WaterLevelLow", "BOILER " .. k .. " WTR LOW", colors.red) end + for k, v in ipairs(annunc.HeatingRateLow) do insert(v, "HeatingRateLow", "BOILER " .. k .. " HEAT RATE") end + for k, v in ipairs(annunc.TurbineOverSpeed) do insert(v, "TurbineOverSpeed", "TURBINE " .. k .. " OVERSPD", colors.red) end + for k, v in ipairs(annunc.GeneratorTrip) do insert(v, "GeneratorTrip", "TURBINE " .. k .. " GEN TRIP") end + + table.insert(items, blue("CHECK COOLING SYS")) + + table.insert(ecam, { color = colors.yellow, text = "RCS TRANSIENT", help = "RCSTransient", items = items}) + end + + if tripped(unit.alarms[ALARM.TurbineTrip]) then + local items = {} + + for k, v in ipairs(unit.annunciator.TurbineTrip) do + if v then table.insert(items, { text = "TURBINE " .. k .. " TRIP", help = "TurbineTrip" }) end + end + + table.insert(items, blue("CHECK ENERGY OUT")) + table.insert(ecam, { color = colors.red, text = "TURBINE TRIP", help = "TurbineTripAlarm", items = items}) + end + + if not (tripped(unit.alarms[ALARM.ReactorLost]) or unit.connected) then + local items = { blue("CHECK PLC") } + table.insert(ecam, { color = colors.yellow, text = "REACTOR OFF-LINE", items = items }) + end + + for k, v in ipairs(unit.annunciator.BoilerOnline) do + if not v then + local items = { blue("CHECK RTU") } + table.insert(ecam, { color = colors.yellow, text = "BOILER " .. k .. " OFF-LINE", items = items}) + end + end + + for k, v in ipairs(unit.annunciator.TurbineOnline) do + if not v then + local items = { blue("CHECK RTU") } + table.insert(ecam, { color = colors.yellow, text = "TURBINE " .. k .. " OFF-LINE", items = items}) + end + end + + -- if no alarms, put some basic status messages in + if #ecam == 0 then + table.insert(ecam, { color = colors.green, text = "REACTOR " .. util.trinary(unit.reactor_data.mek_status.status, "NOMINAL", "IDLE"), items = {}}) + + local plural = util.trinary(unit.num_turbines > 1, "S", "") + table.insert(ecam, { color = colors.green, text = "TURBINE" .. plural .. util.trinary(unit.turbine_flow_stable, " STABLE", " STABILIZING"), items = {}}) + end + + unit.unit_ps.publish("U_ECAM", textutils.serialize(ecam)) + + --#endregion +end + +-- update control app with unit data from API_GET_CTRL +---@param data table +function iorx.record_control_data(data) + for u_id = 1, #data do + local unit = io.units[u_id] + local u_data = data[u_id] + + unit.connected = u_data[1] + + unit.reactor_data.rps_tripped = u_data[2] + unit.unit_ps.publish("rps_tripped", u_data[2]) + unit.reactor_data.mek_status.status = u_data[3] + unit.unit_ps.publish("status", u_data[3]) + unit.reactor_data.mek_status.temp = u_data[4] + unit.unit_ps.publish("temp", u_data[4]) + unit.reactor_data.mek_status.burn_rate = u_data[5] + unit.unit_ps.publish("burn_rate", u_data[5]) + unit.reactor_data.mek_status.act_burn_rate = u_data[6] + unit.unit_ps.publish("act_burn_rate", u_data[6]) + unit.reactor_data.mek_struct.max_burn = u_data[7] + unit.unit_ps.publish("max_burn", u_data[7]) + + unit.annunciator.AutoControl = u_data[8] + unit.unit_ps.publish("AutoControl", u_data[8]) + + unit.a_group = u_data[9] + unit.unit_ps.publish("auto_group_id", unit.a_group) + unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1]) + + local control_status = 1 + + if unit.connected then + if unit.reactor_data.rps_tripped then + control_status = 2 + end + + if unit.reactor_data.mek_status.status then + control_status = util.trinary(unit.annunciator.AutoControl, 4, 3) + end + end + + unit.unit_ps.publish("U_ControlStatus", control_status) + end +end + +-- update process app with unit data from API_GET_PROC +---@param data table +function iorx.record_process_data(data) + -- get unit data + for u_id = 1, #io.units do + local unit = io.units[u_id] + local u_data = data[u_id] + + unit.reactor_data.mek_status.status = u_data[1] + unit.reactor_data.mek_struct.max_burn = u_data[2] + unit.annunciator.AutoControl = u_data[6] + unit.a_group = u_data[7] + + unit.unit_ps.publish("status", u_data[1]) + unit.unit_ps.publish("max_burn", u_data[2]) + unit.unit_ps.publish("burn_limit", u_data[3]) + unit.unit_ps.publish("U_AutoReady", u_data[4]) + unit.unit_ps.publish("U_AutoDegraded", u_data[5]) + unit.unit_ps.publish("AutoControl", u_data[6]) + unit.unit_ps.publish("auto_group_id", unit.a_group) + unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1]) + end + + -- get facility data + local fac = io.facility + local f_data = data[#io.units + 1] + + fac.status_lines = f_data[1] + + fac.auto_ready = f_data[2][1] + fac.auto_active = f_data[2][2] + fac.auto_ramping = f_data[2][3] + fac.auto_saturated = f_data[2][4] + + fac.auto_scram = f_data[3] + fac.ascram_status = f_data[4] + + fac.ps.publish("status_line_1", fac.status_lines[1]) + fac.ps.publish("status_line_2", fac.status_lines[2]) + + fac.ps.publish("auto_ready", fac.auto_ready) + fac.ps.publish("auto_active", fac.auto_active) + fac.ps.publish("auto_ramping", fac.auto_ramping) + fac.ps.publish("auto_saturated", fac.auto_saturated) + + fac.ps.publish("auto_scram", fac.auto_scram) + fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault) + fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill) + fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm) + fac.ps.publish("as_radiation", fac.ascram_status.radiation) + fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault) + + fac.ps.publish("process_mode", f_data[5][1]) + fac.ps.publish("process_burn_target", f_data[5][2]) + fac.ps.publish("process_charge_target", f_data[5][3]) + fac.ps.publish("process_gen_target", f_data[5][4]) +end + +-- update waste app with unit data from API_GET_WASTE +---@param data table +function iorx.record_waste_data(data) + -- get unit data + for u_id = 1, #io.units do + local unit = io.units[u_id] + local u_data = data[u_id] + + unit.waste_mode = u_data[1] + unit.waste_product = u_data[2] + unit.num_snas = u_data[3] + unit.sna_peak_rate = u_data[4] + unit.sna_max_rate = u_data[5] + unit.sna_out_rate = u_data[6] + unit.waste_stats = u_data[7] + + unit.unit_ps.publish("U_AutoWaste", unit.waste_mode == types.WASTE_MODE.AUTO) + unit.unit_ps.publish("U_WasteMode", unit.waste_mode) + unit.unit_ps.publish("U_WasteProduct", unit.waste_product) + + unit.unit_ps.publish("sna_count", unit.num_snas) + unit.unit_ps.publish("sna_peak_rate", unit.sna_peak_rate) + unit.unit_ps.publish("sna_max_rate", unit.sna_max_rate) + unit.unit_ps.publish("sna_out_rate", unit.sna_out_rate) + + unit.unit_ps.publish("pu_rate", unit.waste_stats[1]) + unit.unit_ps.publish("po_rate", unit.waste_stats[2]) + unit.unit_ps.publish("po_pl_rate", unit.waste_stats[3]) + end + + -- get facility data + local fac = io.facility + local f_data = data[#io.units + 1] + + fac.auto_current_waste_product = f_data[1] + fac.auto_pu_fallback_active = f_data[2] + fac.auto_sps_disabled = f_data[3] + + fac.ps.publish("current_waste_product", fac.auto_current_waste_product) + fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active) + fac.ps.publish("sps_disabled_low_power", fac.auto_sps_disabled) + + fac.ps.publish("process_waste_product", f_data[4]) + fac.ps.publish("process_pu_fallback", f_data[5]) + fac.ps.publish("process_sps_low_power", f_data[6]) + + fac.waste_stats = f_data[7] + + fac.ps.publish("burn_sum", fac.waste_stats[1]) + fac.ps.publish("pu_rate", fac.waste_stats[2]) + fac.ps.publish("po_rate", fac.waste_stats[3]) + fac.ps.publish("po_pl_rate", fac.waste_stats[4]) + fac.ps.publish("po_am_rate", fac.waste_stats[5]) + fac.ps.publish("spent_waste_rate", fac.waste_stats[6]) + + fac.ps.publish("sps_computed_status", f_data[8]) + fac.ps.publish("sps_process_rate", f_data[9]) +end + +return function (io_obj) + io = io_obj + return iorx +end diff --git a/pocket/pocket.lua b/pocket/pocket.lua index 461f57a..8233c78 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -29,6 +29,7 @@ pocket.MQ__RENDER_CMD = MQ__RENDER_CMD pocket.MQ__RENDER_DATA = MQ__RENDER_DATA ---@type pkt_config +---@diagnostic disable-next-line: missing-fields local config = {} pocket.config = config @@ -726,23 +727,23 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) end elseif packet.type == CRDN_TYPE.API_GET_FAC then if _check_length(packet, 11) then - iocontrol.record_facility_data(packet.data) + iocontrol.rx.record_facility_data(packet.data) end elseif packet.type == CRDN_TYPE.API_GET_UNIT then if _check_length(packet, 12) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then - iocontrol.record_unit_data(packet.data) + iocontrol.rx.record_unit_data(packet.data) end elseif packet.type == CRDN_TYPE.API_GET_CTRL then if _check_length(packet, #iocontrol.get_db().units) then - iocontrol.record_control_data(packet.data) + iocontrol.rx.record_control_data(packet.data) end elseif packet.type == CRDN_TYPE.API_GET_PROC then if _check_length(packet, #iocontrol.get_db().units + 1) then - iocontrol.record_process_data(packet.data) + iocontrol.rx.record_process_data(packet.data) end elseif packet.type == CRDN_TYPE.API_GET_WASTE then if _check_length(packet, #iocontrol.get_db().units + 1) then - iocontrol.record_waste_data(packet.data) + iocontrol.rx.record_waste_data(packet.data) end else _fail_type(packet) end else diff --git a/pocket/startup.lua b/pocket/startup.lua index 0eea7b0..0058904 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -20,7 +20,7 @@ local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") local threads = require("pocket.threads") -local POCKET_VERSION = "v0.12.10-alpha" +local POCKET_VERSION = "v0.12.11-alpha" local println = util.println local println_ts = util.println_ts diff --git a/pocket/ui/apps/unit.lua b/pocket/ui/apps/unit.lua index bc34d5f..8335ff1 100644 --- a/pocket/ui/apps/unit.lua +++ b/pocket/ui/apps/unit.lua @@ -9,6 +9,7 @@ local pocket = require("pocket.pocket") local style = require("pocket.ui.style") +local dyn_tank = require("pocket.ui.pages.dynamic_tank") local boiler = require("pocket.ui.pages.unit_boiler") local reactor = require("pocket.ui.pages.unit_reactor") local turbine = require("pocket.ui.pages.unit_turbine") @@ -92,6 +93,10 @@ local function new_view(root) table.insert(list, { label = "T-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = nav_links[id].turbine[i] }) end + if #unit.tank_data_tbl > 0 then + table.insert(list, { label = "DYN", color = core.cpair(colors.black, colors.lightGray), callback = nav_links[id].d_tank }) + end + app.set_sidebar(list) end @@ -363,6 +368,15 @@ local function new_view(root) --#endregion + --#region Dynamic Tank Tab + + if #unit.tank_data_tbl > 0 then + local tank_pane = Div{parent=page_div} + nav_links[i].d_tank = dyn_tank(app, u_page, panes, tank_pane, unit.tank_ps_tbl[1], update) + end + + --#endregion + util.nop() end diff --git a/pocket/ui/pages/dynamic_tank.lua b/pocket/ui/pages/dynamic_tank.lua new file mode 100644 index 0000000..bfc89de --- /dev/null +++ b/pocket/ui/pages/dynamic_tank.lua @@ -0,0 +1,74 @@ +local types = require("scada-common.types") + +local style = require("pocket.ui.style") + +local core = require("graphics.core") + +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") + +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local HorizontalBar = require("graphics.elements.indicators.HorizontalBar") +local IconIndicator = require("graphics.elements.indicators.IconIndicator") +local StateIndicator = require("graphics.elements.indicators.StateIndicator") + +local CONTAINER_MODE = types.CONTAINER_MODE + +local cpair = core.cpair + +local label = style.label +local lu_col = style.label_unit_pair +local text_fg = style.text_fg + +local mode_ind_s = { + { color = cpair(colors.black, colors.lightGray), symbol = "-" }, + { color = cpair(colors.black, colors.white), symbol = "+" } +} + +-- create a dynamic tank view for the unit or facility app +---@param app pocket_app +---@param page nav_tree_page +---@param panes Div[] +---@param tank_pane Div +---@param ps psil +---@param update function +return function (app, page, panes, tank_pane, ps, update) + local tank_div = Div{parent=tank_pane,x=2,width=tank_pane.get_width()-2} + table.insert(panes, tank_div) + + local tank_page = app.new_page(page, #panes) + tank_page.tasks = { update } + + TextBox{parent=tank_div,y=1,text="Dyn Tank",width=9} + local status = StateIndicator{parent=tank_div,x=10,y=1,states=style.dtank.states,value=1,min_width=12} + status.register(ps, "DynamicTankStateStatus", status.update) + + TextBox{parent=tank_div,y=3,text="Fill",width=10,fg_bg=label} + local tank_pcnt = DataIndicator{parent=tank_div,x=14,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_fg} + local tank_amnt = DataIndicator{parent=tank_div,label="",format="%18d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=21,fg_bg=text_fg} + + TextBox{parent=tank_div,y=6,text="Fluid Level",width=11,fg_bg=label} + local level = HorizontalBar{parent=tank_div,y=7,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=21} + + TextBox{parent=tank_div,y=9,text="Tank Fill Mode",width=14,fg_bg=label} + local can_fill = IconIndicator{parent=tank_div,y=10,label="Fill",states=mode_ind_s} + local can_empty = IconIndicator{parent=tank_div,y=11,label="Empty",states=mode_ind_s} + + local function _can_fill(mode) + can_fill.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.FILL)) + end + + local function _can_empty(mode) + can_empty.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.EMPTY)) + end + + tank_pcnt.register(ps, "fill", function (f) tank_pcnt.update(f * 100) end) + tank_amnt.register(ps, "stored", function (sto) tank_amnt.update(sto.amount) end) + + level.register(ps, "fill", level.update) + + can_fill.register(ps, "container_mode", _can_fill) + can_empty.register(ps, "container_mode", _can_empty) + + return tank_page.nav_to +end diff --git a/pocket/ui/style.lua b/pocket/ui/style.lua index aba5d19..7b35cc4 100644 --- a/pocket/ui/style.lua +++ b/pocket/ui/style.lua @@ -95,180 +95,93 @@ style.icon_states = states -- MAIN LAYOUT -- style.reactor = { - -- reactor states + -- reactor states
+ ---@see REACTOR_STATE states = { - { - color = cpair(colors.black, colors.yellow), - text = "OFF-LINE" - }, - { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - }, - { - color = cpair(colors.black, colors.orange), - text = "PLC FAULT" - }, - { - color = cpair(colors.white, colors.gray), - text = "DISABLED" - }, - { - color = cpair(colors.black, colors.green), - text = "ACTIVE" - }, - { - color = cpair(colors.black, colors.red), - text = "SCRAMMED" - }, - { - color = cpair(colors.black, colors.red), - text = "FORCE DSBL" - } + { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, + { color = cpair(colors.black, colors.orange), text = "NOT FORMED" }, + { color = cpair(colors.black, colors.orange), text = "PLC FAULT" }, + { color = cpair(colors.white, colors.gray), text = "DISABLED" }, + { color = cpair(colors.black, colors.green), text = "ACTIVE" }, + { color = cpair(colors.black, colors.red), text = "SCRAMMED" }, + { color = cpair(colors.black, colors.red), text = "FORCE DSBL" } } } style.boiler = { - -- boiler states + -- boiler states
+ ---@see BOILER_STATE states = { - { - color = cpair(colors.black, colors.yellow), - text = "OFF-LINE" - }, - { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - }, - { - color = cpair(colors.black, colors.orange), - text = "RTU FAULT" - }, - { - color = cpair(colors.white, colors.gray), - text = "IDLE" - }, - { - color = cpair(colors.black, colors.green), - text = "ACTIVE" - } + { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, + { color = cpair(colors.black, colors.orange), text = "NOT FORMED" }, + { color = cpair(colors.black, colors.orange), text = "RTU FAULT" }, + { color = cpair(colors.white, colors.gray), text = "IDLE" }, + { color = cpair(colors.black, colors.green), text = "ACTIVE" } } } style.turbine = { - -- turbine states + -- turbine states
+ ---@see TURBINE_STATE states = { - { - color = cpair(colors.black, colors.yellow), - text = "OFF-LINE" - }, - { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - }, - { - color = cpair(colors.black, colors.orange), - text = "RTU FAULT" - }, - { - color = cpair(colors.white, colors.gray), - text = "IDLE" - }, - { - color = cpair(colors.black, colors.green), - text = "ACTIVE" - }, - { - color = cpair(colors.black, colors.red), - text = "TRIP" - } + { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, + { color = cpair(colors.black, colors.orange), text = "NOT FORMED" }, + { color = cpair(colors.black, colors.orange), text = "RTU FAULT" }, + { color = cpair(colors.white, colors.gray), text = "IDLE" }, + { color = cpair(colors.black, colors.green), text = "ACTIVE" }, + { color = cpair(colors.black, colors.red), text = "TRIP" } + } +} + +style.dtank = { + -- dynamic tank states
+ ---@see TANK_STATE + states = { + { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, + { color = cpair(colors.black, colors.orange), text = "NOT FORMED" }, + { color = cpair(colors.black, colors.orange), text = "RTU FAULT" }, + { color = cpair(colors.black, colors.green), text = "ONLINE" }, + { color = cpair(colors.black, colors.yellow), text = "LOW FILL" }, + { color = cpair(colors.black, colors.green), text = "FILLED" } } } style.imatrix = { - -- induction matrix states + -- induction matrix states
+ ---@see IMATRIX_STATE states = { - { - color = cpair(colors.black, colors.yellow), - text = "OFF-LINE" - }, - { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - }, - { - color = cpair(colors.black, colors.orange), - text = "RTU FAULT" - }, - { - color = cpair(colors.black, colors.green), - text = "ONLINE" - }, - { - color = cpair(colors.black, colors.yellow), - text = "LOW CHARGE" - }, - { - color = cpair(colors.black, colors.yellow), - text = "HIGH CHARGE" - }, + { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, + { color = cpair(colors.black, colors.orange), text = "NOT FORMED" }, + { color = cpair(colors.black, colors.orange), text = "RTU FAULT" }, + { color = cpair(colors.black, colors.green), text = "ONLINE" }, + { color = cpair(colors.black, colors.yellow), text = "LOW CHARGE" }, + { color = cpair(colors.black, colors.yellow), text = "HIGH CHARGE" } } } style.sps = { - -- SPS states + -- SPS states
+ ---@see SPS_STATE states = { - { - color = cpair(colors.black, colors.yellow), - text = "OFF-LINE" - }, - { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - }, - { - color = cpair(colors.black, colors.orange), - text = "RTU FAULT" - }, - { - color = cpair(colors.white, colors.gray), - text = "IDLE" - }, - { - color = cpair(colors.black, colors.green), - text = "ACTIVE" - } + { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, + { color = cpair(colors.black, colors.orange), text = "NOT FORMED" }, + { color = cpair(colors.black, colors.orange), text = "RTU FAULT" }, + { color = cpair(colors.white, colors.gray), text = "IDLE" }, + { color = cpair(colors.black, colors.green), text = "ACTIVE" } } } style.waste = { -- auto waste processing states states = { - { - color = cpair(colors.black, colors.green), - text = "PLUTONIUM" - }, - { - color = cpair(colors.black, colors.cyan), - text = "POLONIUM" - }, - { - color = cpair(colors.black, colors.purple), - text = "ANTI MATTER" - } + { color = cpair(colors.black, colors.green), text = "PLUTONIUM" }, + { color = cpair(colors.black, colors.cyan), text = "POLONIUM" }, + { color = cpair(colors.black, colors.purple), text = "ANTI MATTER" } }, states_abbrv = { - { - color = cpair(colors.black, colors.green), - text = "Pu" - }, - { - color = cpair(colors.black, colors.cyan), - text = "Po" - }, - { - color = cpair(colors.black, colors.purple), - text = "AM" - } + { color = cpair(colors.black, colors.green), text = "Pu" }, + { color = cpair(colors.black, colors.cyan), text = "Po" }, + { color = cpair(colors.black, colors.purple), text = "AM" } }, -- process radio button options options = { "Plutonium", "Polonium", "Antimatter" }, diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 73a1126..a84e55a 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -29,6 +29,7 @@ local PCALL_SCRAM_MSG = "Scram requires the reactor to be active." local PCALL_START_MSG = "Reactor is already active." ---@type plc_config +---@diagnostic disable-next-line: missing-fields local config = {} plc.config = config diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 0ea1779..06178ad 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -19,6 +19,7 @@ local MGMT_TYPE = comms.MGMT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE ---@type rtu_config +---@diagnostic disable-next-line: missing-fields local config = {} rtu.config = config diff --git a/scada-common/psil.lua b/scada-common/psil.lua index f3d810a..82e99f6 100644 --- a/scada-common/psil.lua +++ b/scada-common/psil.lua @@ -74,6 +74,13 @@ function psil.create() end end + -- get the currently stored value for a key, or nil if not set + ---@param key string data key + ---@return any + function public.get(key) + if ic[key] ~= nil then return ic[key].value else return nil end + end + -- clear the contents of the interconnect function public.purge() ic = {} end diff --git a/scada-common/types.lua b/scada-common/types.lua index ca1b885..19392fe 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -253,6 +253,61 @@ types.ENERGY_SCALE_UNITS = { "RF" } +local GENERIC_STATE = { + OFFLINE = 1, + UNFORMED = 2, + FAULT = 3, + IDLE = 4, + ACTIVE = 5 +} + +---@enum REACTOR_STATE +types.REACTOR_STATE = { + OFFLINE = 1, + UNFORMED = 2, + FAULT = 3, + DISABLED = 4, + ACTIVE = 5, + SCRAMMED = 6, + FORCE_DISABLED = 7 +} + +---@enum BOILER_STATE +types.BOILER_STATE = GENERIC_STATE + +---@enum TURBINE_STATE +types.TURBINE_STATE = { + OFFLINE = 1, + UNFORMED = 2, + FAULT = 3, + IDLE = 4, + ACTIVE = 5, + TRIPPED = 6 +} + +---@enum TANK_STATE +types.TANK_STATE = { + OFFLINE = 1, + UNFORMED = 2, + FAULT = 3, + ONLINE = 4, + LOW_FILL = 5, + HIGH_FILL = 6 +} + +---@enum IMATRIX_STATE +types.IMATRIX_STATE = { + OFFLINE = 1, + UNFORMED = 2, + FAULT = 3, + ONLINE = 4, + LOW_CHARGE = 5, + HIGH_CHARGE = 6 +} + +---@enum SPS_STATE +types.SPS_STATE = GENERIC_STATE + ---@enum PANEL_LINK_STATE types.PANEL_LINK_STATE = { LINKED = 1, diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index e9c585d..b011d44 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -255,6 +255,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim elseif cmd == FAC_COMMAND.START then if pkt.length == 6 then ---@type sys_auto_config +---@diagnostic disable-next-line: missing-fields local config = { mode = pkt.data[2], burn_target = pkt.data[3], diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 6090eff..ac216d6 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -46,12 +46,14 @@ local self = { config = nil, ---@type svr_config facility = nil, ---@type facility|nil -- lists of connected sessions +---@diagnostic disable: missing-fields sessions = { rtu = {}, ---@type rtu_session_struct plc = {}, ---@type plc_session_struct crd = {}, ---@type crd_session_struct pdg = {} ---@type pdg_session_struct }, +---@diagnostic enable: missing-fields -- next session IDs next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 }, -- rtu device tracking and invalid assignment detection diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 8b06d49..98cdc78 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -14,6 +14,7 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK local MGMT_TYPE = comms.MGMT_TYPE ---@type svr_config +---@diagnostic disable-next-line: missing-fields local config = {} supervisor.config = config diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 794b3bf..dc59cff 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -90,6 +90,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) envd = {}, ---@type envd_session[] -- redstone control io_ctl = nil, ---@type rs_controller +---@diagnostic disable-next-line: missing-fields valves = {}, ---@type unit_valves emcool_opened = false, -- auto control