diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 339ad63..0a42038 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -172,16 +172,21 @@ function iocontrol.update_statuses(statuses) if #reactor_status == 0 then unit.reactor_ps.publish("computed_status", 1) -- disconnected - else + elseif #reactor_status == 3 then local mek_status = reactor_status[1] local rps_status = reactor_status[2] local gen_status = reactor_status[3] - 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.degraded = gen_status[5] + 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("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 @@ -189,8 +194,10 @@ function iocontrol.update_statuses(statuses) if unit.reactor_data.mek_status.status then unit.reactor_ps.publish("computed_status", 3) -- running else - if unit.reactor_data.degraded then + if unit.reactor_data.no_reactor then unit.reactor_ps.publish("computed_status", 5) -- faulted + elseif not unit.reactor_data.formed then + unit.reactor_ps.publish("computed_status", 6) -- multiblock not formed elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then unit.reactor_ps.publish("computed_status", 4) -- SCRAM else @@ -204,13 +211,19 @@ function iocontrol.update_statuses(statuses) end end - for key, val in pairs(unit.reactor_data.rps_status) do - unit.reactor_ps.publish(key, val) + if type(unit.reactor_data.rps_status) == "table" then + for key, val in pairs(unit.reactor_data.rps_status) do + unit.reactor_ps.publish(key, val) + end end - for key, val in pairs(unit.reactor_data.mek_status) do - unit.reactor_ps.publish(key, val) + if type(unit.reactor_data.mek_status) == "table" then + for key, val in pairs(unit.reactor_data.mek_status) do + unit.reactor_ps.publish(key, val) + end end + else + log.debug("reactor status length mismatch") end -- annunciator @@ -252,65 +265,71 @@ function iocontrol.update_statuses(statuses) local rtu_statuses = status[3] - -- boiler statuses + if type(rtu_statuses) == "table" then + if type(rtu_statuses.boilers) == "table" then + -- boiler statuses - for id = 1, #unit.boiler_data_tbl do - if rtu_statuses.boilers[i] == nil then - -- disconnected - unit.boiler_ps_tbl[id].publish("computed_status", 1) - end - end + for id = 1, #unit.boiler_data_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 - unit.boiler_data_tbl[id].state = boiler[1] ---@type table - unit.boiler_data_tbl[id].tanks = boiler[2] ---@type table + for id, boiler in pairs(rtu_statuses.boilers) do + unit.boiler_data_tbl[id].state = boiler[1] ---@type table + unit.boiler_data_tbl[id].tanks = boiler[2] ---@type table - local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db + local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db - if data.state.boil_rate > 0 then - unit.boiler_ps_tbl[id].publish("computed_status", 3) -- active - else - unit.boiler_ps_tbl[id].publish("computed_status", 2) -- idle + if data.state.boil_rate > 0 then + unit.boiler_ps_tbl[id].publish("computed_status", 3) -- active + else + unit.boiler_ps_tbl[id].publish("computed_status", 2) -- idle + 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 + end end - for key, val in pairs(unit.boiler_data_tbl[id].state) do - unit.boiler_ps_tbl[id].publish(key, val) - end + if type(rtu_statuses.turbines) == "table" then + -- turbine statuses - for key, val in pairs(unit.boiler_data_tbl[id].tanks) do - unit.boiler_ps_tbl[id].publish(key, val) - end - end + 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 - -- turbine statuses + for id, turbine in pairs(rtu_statuses.turbines) do + unit.turbine_data_tbl[id].state = turbine[1] ---@type table + unit.turbine_data_tbl[id].tanks = turbine[2] ---@type table - 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 - unit.turbine_data_tbl[id].state = turbine[1] ---@type table - unit.turbine_data_tbl[id].tanks = turbine[2] ---@type table + if data.tanks.steam_fill >= 0.99 then + unit.turbine_ps_tbl[id].publish("computed_status", 4) -- trip + elseif data.state.flow_rate < 100 then + unit.turbine_ps_tbl[id].publish("computed_status", 2) -- idle + else + unit.turbine_ps_tbl[id].publish("computed_status", 3) -- active + end - local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db + for key, val in pairs(unit.turbine_data_tbl[id].state) do + unit.turbine_ps_tbl[id].publish(key, val) + end - if data.tanks.steam_fill >= 0.99 then - unit.turbine_ps_tbl[id].publish("computed_status", 4) -- trip - elseif data.state.flow_rate < 100 then - unit.turbine_ps_tbl[id].publish("computed_status", 2) -- idle - else - unit.turbine_ps_tbl[id].publish("computed_status", 3) -- active - 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) + for key, val in pairs(unit.turbine_data_tbl[id].tanks) do + unit.turbine_ps_tbl[id].publish(key, val) + end + end end end end diff --git a/coordinator/startup.lua b/coordinator/startup.lua index c612921..e8717c4 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -16,7 +16,7 @@ local config = require("coordinator.config") local coordinator = require("coordinator.coordinator") local renderer = require("coordinator.renderer") -local COORDINATOR_VERSION = "alpha-v0.5.9" +local COORDINATOR_VERSION = "alpha-v0.5.10" local print = util.print local println = util.println diff --git a/coordinator/ui/style.lua b/coordinator/ui/style.lua index ec0d20d..395e6f2 100644 --- a/coordinator/ui/style.lua +++ b/coordinator/ui/style.lua @@ -54,6 +54,10 @@ style.reactor = { { color = cpair(colors.black, colors.orange), text = "PLC FAULT" + }, + { + color = cpair(colors.black, colors.orange), + text = "NOT FORMED" } } } diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 5e04ed1..f8cfc76 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -38,7 +38,9 @@ local MAX_HEATED_COLLANT_FILL = 0.95 --- identifies dangerous states and SCRAMs reactor if warranted --- --- autonomous from main SCADA supervisor/coordinator control -function plc.rps_init(reactor) +---@param reactor table +---@param is_formed boolean +function plc.rps_init(reactor, is_formed) local state_keys = { dmg_crit = 1, high_temp = 2, @@ -48,13 +50,15 @@ function plc.rps_init(reactor) no_fuel = 6, fault = 7, timeout = 8, - manual = 9 + manual = 9, + sys_fail = 10 } local self = { reactor = reactor, - state = { false, false, false, false, false, false, false, false, false }, + state = { false, false, false, false, false, false, false, false, false, false }, reactor_enabled = false, + formed = is_formed, tripped = false, trip_cause = "" ---@type rps_trip_cause } @@ -76,14 +80,25 @@ function plc.rps_init(reactor) self.state[state_keys.fault] = false end + -- check if the reactor is formed + local function _is_formed() + local is_formed = self.reactor.isFormed() + if is_formed == ppm.ACCESS_FAULT then + -- lost the peripheral or terminated, handled later + _set_fault() + elseif not self.state[state_keys.sys_fail] then + self.formed = is_formed + self.state[state_keys.sys_fail] = not is_formed + end + end + -- check for critical damage local function _damage_critical() local damage_percent = self.reactor.getDamagePercent() if damage_percent == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() - self.state[state_keys.dmg_crit] = false - else + elseif not self.state[state_keys.dmg_crit] then self.state[state_keys.dmg_crit] = damage_percent >= MAX_DAMAGE_PERCENT end end @@ -95,8 +110,7 @@ function plc.rps_init(reactor) if temp == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() - self.state[state_keys.high_temp] = false - else + elseif not self.state[state_keys.high_temp] then self.state[state_keys.high_temp] = temp >= MAX_DAMAGE_TEMPERATURE end end @@ -107,8 +121,7 @@ function plc.rps_init(reactor) if coolant_filled == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() - self.state[state_keys.no_coolant] = false - else + elseif not self.state[state_keys.no_coolant] then self.state[state_keys.no_coolant] = coolant_filled < MIN_COOLANT_FILL end end @@ -119,8 +132,7 @@ function plc.rps_init(reactor) if w_filled == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() - self.state[state_keys.ex_waste] = false - else + elseif not self.state[state_keys.ex_waste] then self.state[state_keys.ex_waste] = w_filled > MAX_WASTE_FILL end end @@ -131,8 +143,7 @@ function plc.rps_init(reactor) if hc_filled == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() - self.state[state_keys.ex_hcoolant] = false - else + elseif not self.state[state_keys.ex_hcoolant] then self.state[state_keys.ex_hcoolant] = hc_filled > MAX_HEATED_COLLANT_FILL end end @@ -143,8 +154,7 @@ function plc.rps_init(reactor) if fuel == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later _set_fault() - self.state[state_keys.no_fuel] = false - else + elseif not self.state[state_keys.no_fuel] then self.state[state_keys.no_fuel] = fuel == 0 end end @@ -169,7 +179,13 @@ function plc.rps_init(reactor) -- manually SCRAM the reactor function public.trip_manual() - self.state[state_keys.manual] = true + self.state[state_keys.manual] = true + end + + -- trip for unformed reactor + function public.trip_sys_fail() + self.state[state_keys.fault] = true + self.state[state_keys.sys_fail] = true end -- SCRAM the reactor now @@ -216,6 +232,7 @@ function plc.rps_init(reactor) -- update state parallel.waitForAll( + _is_formed, _damage_critical, _high_temp, _no_coolant, @@ -227,6 +244,9 @@ function plc.rps_init(reactor) -- check system states in order of severity if self.tripped then status = self.trip_cause + elseif self.state[state_keys.sys_fail] then + log.warning("RPS: system failure, reactor not formed") + status = rps_status_t.sys_fail elseif self.state[state_keys.dmg_crit] then log.warning("RPS: damage critical") status = rps_status_t.dmg_crit @@ -273,9 +293,11 @@ function plc.rps_init(reactor) function public.status() return self.state end function public.is_tripped() return self.tripped end function public.is_active() return self.reactor_enabled end + function public.is_formed() return self.formed end -- reset the RPS - function public.reset() + ---@param quiet? boolean true to suppress the info log message + function public.reset(quiet) self.tripped = false self.trip_cause = rps_status_t.ok @@ -283,7 +305,7 @@ function plc.rps_init(reactor) self.state[i] = false end - log.info("RPS: reset") + if not quiet then log.info("RPS: reset") end end return public @@ -473,20 +495,19 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co local mek_data = { false, 0, 0, 0, min_pos, max_pos, 0, 0, 0, 0, 0, 0, 0, 0 } local tasks = { - function () mek_data[1] = self.reactor.isFormed() end, - function () mek_data[2] = self.reactor.getLength() end, - function () mek_data[3] = self.reactor.getWidth() end, - function () mek_data[4] = self.reactor.getHeight() end, - function () mek_data[5] = self.reactor.getMinPos() end, - function () mek_data[6] = self.reactor.getMaxPos() end, - function () mek_data[7] = self.reactor.getHeatCapacity() end, - function () mek_data[8] = self.reactor.getFuelAssemblies() end, - function () mek_data[9] = self.reactor.getFuelSurfaceArea() end, - function () mek_data[10] = self.reactor.getFuelCapacity() end, - function () mek_data[11] = self.reactor.getWasteCapacity() end, - function () mek_data[12] = self.reactor.getCoolantCapacity() end, - function () mek_data[13] = self.reactor.getHeatedCoolantCapacity() end, - function () mek_data[14] = self.reactor.getMaxBurnRate() end + function () mek_data[1] = self.reactor.getLength() end, + function () mek_data[2] = self.reactor.getWidth() end, + function () mek_data[3] = self.reactor.getHeight() end, + function () mek_data[4] = self.reactor.getMinPos() end, + function () mek_data[5] = self.reactor.getMaxPos() end, + function () mek_data[6] = self.reactor.getHeatCapacity() end, + function () mek_data[7] = self.reactor.getFuelAssemblies() end, + function () mek_data[8] = self.reactor.getFuelSurfaceArea() end, + function () mek_data[9] = self.reactor.getFuelCapacity() end, + function () mek_data[10] = self.reactor.getWasteCapacity() end, + function () mek_data[11] = self.reactor.getCoolantCapacity() end, + function () mek_data[12] = self.reactor.getHeatedCoolantCapacity() end, + function () mek_data[13] = self.reactor.getMaxBurnRate() end } parallel.waitForAll(table.unpack(tasks)) @@ -536,29 +557,35 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co end -- send live status information - ---@param degraded boolean - function public.send_status(degraded) + ---@param no_reactor boolean PLC lost reactor connection + ---@param formed boolean reactor formed + function public.send_status(no_reactor, formed) if self.linked then - local mek_data = nil + local mek_data = nil ---@type table + local heating_rate = nil ---@type number - if _update_status_cache() then - mek_data = self.status_cache + if (not no_reactor) and formed then + if _update_status_cache() then + mek_data = self.status_cache + log.debug("sent updated status") + else + log.debug("sent cached status") + end + + heating_rate = self.reactor.getHeatingRate() end local sys_status = { util.time(), -- timestamp (not self.scrammed), -- requested control state rps.is_tripped(), -- rps_tripped - degraded, -- degraded - self.reactor.getHeatingRate(), -- heating rate + no_reactor, -- no reactor peripheral connected + formed, -- reactor formed + heating_rate, -- heating rate mek_data -- mekanism status data } - if not self.reactor.__p_is_faulted() then - _send(RPLC_TYPES.STATUS, sys_status) - else - log.error("failed to send status: PPM fault") - end + _send(RPLC_TYPES.STATUS, sys_status) end end @@ -570,7 +597,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co end -- send reactor protection system alarm - ---@param cause rps_status_t + ---@param cause rps_status_t reactor protection system status function public.send_rps_alarm(cause) if self.linked then local rps_alarm = { @@ -618,9 +645,9 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co end -- handle an RPLC packet - ---@param packet rplc_frame|mgmt_frame - ---@param plc_state plc_state - ---@param setpoints setpoints + ---@param packet rplc_frame|mgmt_frame packet frame + ---@param plc_state plc_state PLC state + ---@param setpoints setpoints setpoint control table function public.handle_packet(packet, plc_state, setpoints) if packet ~= nil and packet.scada_frame.local_port() == self.l_port then -- check sequence number @@ -651,7 +678,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co if link_ack == RPLC_LINKING.ALLOW then self.status_cache = nil _send_struct() - public.send_status(plc_state.degraded) + public.send_status(plc_state.no_reactor, plc_state.reactor_formed) log.debug("re-sent initial status data") elseif link_ack == RPLC_LINKING.DENY then println_ts("received unsolicited link denial, unlinking") @@ -671,7 +698,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co elseif packet.type == RPLC_TYPES.STATUS then -- request of full status, clear cache first self.status_cache = nil - public.send_status(plc_state.degraded) + public.send_status(plc_state.no_reactor, plc_state.reactor_formed) log.debug("sent out status cache again, did supervisor miss it?") elseif packet.type == RPLC_TYPES.MEK_STRUCT then -- request for physical structure @@ -738,8 +765,11 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co self.r_seq_num = nil self.status_cache = nil - _send_struct() - public.send_status(plc_state.degraded) + if plc_state.reactor_formed then + _send_struct() + end + + public.send_status(plc_state.no_reactor, plc_state.reactor_formed) log.debug("sent initial status data") elseif link_ack == RPLC_LINKING.DENY then diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 81338ee..22dd79a 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -13,7 +13,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "beta-v0.8.9" +local R_PLC_VERSION = "beta-v0.9.0" local print = util.print local println = util.println @@ -64,6 +64,7 @@ local __shared_memory = { init_ok = true, shutdown = false, degraded = false, + reactor_formed = true, no_reactor = false, no_modem = false }, @@ -101,7 +102,7 @@ local smem_sys = __shared_memory.plc_sys local plc_state = __shared_memory.plc_state --- we need a reactor and a modem +-- we need a reactor, can at least do some things even if it isn't formed though if smem_dev.reactor == nil then println("boot> fission reactor not found"); log.warning("no reactor on startup") @@ -109,12 +110,21 @@ if smem_dev.reactor == nil then plc_state.init_ok = false plc_state.degraded = true plc_state.no_reactor = true +elseif not smem_dev.reactor.isFormed() then + println("boot> fission reactor not formed"); + log.warning("reactor logic adapter present, but reactor is not formed") + + plc_state.degraded = true + plc_state.reactor_formed = false end + +-- modem is required if networked if __shared_memory.networked and smem_dev.modem == nil then println("boot> wireless modem not found") log.warning("no wireless modem on startup") - if smem_dev.reactor ~= nil and smem_dev.reactor.getStatus() then + -- scram reactor if present and enabled + if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then smem_dev.reactor.scram() end @@ -127,10 +137,12 @@ end local function init() if plc_state.init_ok then -- just booting up, no fission allowed (neutrons stay put thanks) - if smem_dev.reactor.getStatus() then smem_dev.reactor.scram() end + if plc_state.reactor_formed and smem_dev.reactor.getStatus() then + smem_dev.reactor.scram() + end -- init reactor protection system - smem_sys.rps = plc.rps_init(smem_dev.reactor) + smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed) log.debug("init> rps init") if __shared_memory.networked then @@ -147,8 +159,7 @@ local function init() log.debug("init> running without networking") end ----@diagnostic disable-next-line: undefined-field - os.queueEvent("clock_start") + util.push_event("clock_start") println("boot> completed") log.debug("init> boot completed") @@ -182,7 +193,7 @@ if __shared_memory.networked then if plc_state.init_ok then -- send status one last time after RPS shutdown - smem_sys.plc_comms.send_status(plc_state.degraded) + smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed) smem_sys.plc_comms.send_rps_status() -- close connection diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 3b4cf49..b700fbd 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -78,6 +78,51 @@ function threads.thread__main(smem, init) end end end + + -- are we now formed after waiting to be formed? + if not plc_state.reactor_formed and rps.is_formed() then + -- push a connect event and unmount it from the PPM + local iface = ppm.get_iface(plc_dev.reactor) + if iface then + ppm.unmount(plc_dev.reactor) + + local type, device = ppm.mount(iface) + + if type ~= "fissionReactorLogicAdapter" and device ~= nil then + -- reconnect reactor + plc_dev.reactor = device + + smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) + + println_ts("reactor reconnected as formed.") + log.info("reactor reconnected as formed") + + plc_state.reactor_formed = device.isFormed() + + rps.reconnect_reactor(plc_dev.reactor) + if networked then + plc_comms.reconnect_reactor(plc_dev.reactor) + end + + -- determine if we are still in a degraded state + if not networked or not plc_state.no_modem then + plc_state.degraded = false + end + else + -- fully lost the reactor now :( + println_ts("reactor lost (failed reconnect)!") + log.error("reactor lost (failed reconnect!") + + plc_state.no_reactor = true + plc_state.degraded = true + end + else + log.error("failed to get interface of previously connected reactor", true) + end + elseif not rps.is_formed() then + -- reactor no longer formed + plc_state.reactor_formed = false + end elseif event == "modem_message" and networked and plc_state.init_ok and not plc_state.no_modem then -- got a packet local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) @@ -94,16 +139,18 @@ function threads.thread__main(smem, init) local type, device = ppm.handle_unmount(param1) if type ~= nil and device ~= nil then - if type == "fissionReactor" then + if type == "fissionReactorLogicAdapter" then println_ts("reactor disconnected!") log.error("reactor disconnected!") + plc_state.no_reactor = true plc_state.degraded = true elseif networked and type == "modem" then -- we only care if this is our wireless modem if device == plc_dev.modem then - println_ts("wireless modem disconnected!") + println_ts("comms modem disconnected!") log.error("comms modem disconnected!") + plc_state.no_modem = true if plc_state.init_ok then @@ -122,7 +169,7 @@ function threads.thread__main(smem, init) local type, device = ppm.mount(param1) if type ~= nil and device ~= nil then - if type == "fissionReactor" then + if type == "fissionReactorLogicAdapter" then -- reconnected reactor plc_dev.reactor = device @@ -130,7 +177,9 @@ function threads.thread__main(smem, init) println_ts("reactor reconnected.") log.info("reactor reconnected") + plc_state.no_reactor = false + plc_state.reactor_formed = device.isFormed() if plc_state.init_ok then rps.reconnect_reactor(plc_dev.reactor) @@ -140,7 +189,7 @@ function threads.thread__main(smem, init) end -- determine if we are still in a degraded state - if not networked or not plc_state.no_modem then + if (not networked or not plc_state.no_modem) and plc_state.reactor_formed then plc_state.degraded = false end elseif networked and type == "modem" then @@ -202,9 +251,7 @@ function threads.thread__main(smem, init) -- this thread cannot be slept because it will miss events (namely "terminate" otherwise) if not plc_state.shutdown then log.info("main thread restarting now...") - ----@diagnostic disable-next-line: undefined-field - os.queueEvent("clock_start") + util.push_event("clock_start") end end end @@ -261,7 +308,7 @@ function threads.thread__rps(smem) -- if we are in standalone mode, continuously reset RPS -- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable - if not networked then rps.reset() end + if not networked then rps.reset(true) end -- check safety (SCRAM occurs if tripped) if not plc_state.no_reactor then @@ -380,7 +427,7 @@ function threads.thread__comms_tx(smem) -- received a command if msg.message == MQ__COMM_CMD.SEND_STATUS then -- send PLC/RPS status - plc_comms.send_status(plc_state.degraded) + plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed) plc_comms.send_rps_status() end elseif msg.qtype == mqueue.TYPE.DATA then diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 3ca4921..e536ca6 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -10,7 +10,10 @@ local ppm = {} local ACCESS_FAULT = nil ---@type nil +local UNDEFINED_FIELD = "undefined field" + ppm.ACCESS_FAULT = ACCESS_FAULT +ppm.UNDEFINED_FIELD = UNDEFINED_FIELD ---------------------------- -- PRIVATE DATA/FUNCTIONS -- @@ -110,6 +113,40 @@ local function peri_init(iface) self.device.__p_enable_afc = enable_afc self.device.__p_disable_afc = disable_afc + -- add default index function to catch undefined indicies + + local mt = {} + + function mt.__index(_, key) + -- this will continuously be counting calls here as faults + -- unlike other functions, faults here can't be cleared as it is just not defined + if self.fault_counts[key] == nil then + self.fault_counts[key] = 0 + end + + -- function failed + self.faulted = true + self.last_fault = UNDEFINED_FIELD + + _ppm_sys.faulted = true + _ppm_sys.last_fault = UNDEFINED_FIELD + + if not _ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then + local count_str = "" + if self.fault_counts[key] > 0 then + count_str = " [" .. self.fault_counts[key] .. " total calls]" + end + + log.error(util.c("PPM: caught undefined function ", key, "()", count_str)) + end + + self.fault_counts[key] = self.fault_counts[key] + 1 + + return ACCESS_FAULT + end + + setmetatable(self.device, mt) + return { type = self.type, dev = self.device @@ -208,6 +245,20 @@ function ppm.mount(iface) return pm_type, pm_dev end +-- manually unmount a peripheral from the PPM +---@param device table device table +function ppm.unmount(device) + if device then + for side, data in pairs(_ppm_sys.mounts) do + if data.dev == device then + log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", side)) + _ppm_sys.mounts[side] = nil + break + end + end + end +end + -- handle peripheral_detach event ---@param iface string CC peripheral interface ---@return string|nil type, table|nil device @@ -227,6 +278,8 @@ function ppm.handle_unmount(iface) log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface)) end + _ppm_sys.mounts[iface] = nil + return pm_type, pm_dev end @@ -244,6 +297,19 @@ function ppm.list_mounts() return _ppm_sys.mounts end +-- get a mounted peripheral side/interface by device table +---@param device table device table +---@return string|nil iface CC peripheral interface +function ppm.get_iface(device) + if device then + for side, data in pairs(_ppm_sys.mounts) do + if data.dev == device then return side end + end + end + + return nil +end + -- get a mounted peripheral by side/interface ---@param iface string CC peripheral interface ---@return table|nil device function table @@ -298,7 +364,7 @@ end -- get the fission reactor (if multiple, returns the first) ---@return table|nil reactor function table function ppm.get_fission_reactor() - return ppm.get_device("fissionReactor") or ppm.get_device("fissionReactorLogicAdapter") + return ppm.get_device("fissionReactorLogicAdapter") end -- get the wireless modem (if multiple, returns the first) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 514cd60..079d8e9 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -90,20 +90,22 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) sDB = { last_status_update = 0, control_state = false, - degraded = false, + no_reactor = false, + formed = false, rps_tripped = false, rps_trip_cause = "ok", ---@type rps_trip_cause ---@class rps_status rps_status = { dmg_crit = false, - ex_hcool = false, - ex_waste = false, high_temp = false, - no_fuel = false, no_cool = false, + ex_waste = false, + ex_hcool = false, + no_fuel = false, fault = false, timeout = false, - manual = false + manual = false, + sys_fail = false }, ---@class mek_status mek_status = { @@ -159,14 +161,15 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) ---@param rps_status table local function _copy_rps_status(rps_status) self.sDB.rps_status.dmg_crit = rps_status[1] - self.sDB.rps_status.ex_hcool = rps_status[2] - self.sDB.rps_status.ex_waste = rps_status[3] - self.sDB.rps_status.high_temp = rps_status[4] - self.sDB.rps_status.no_fuel = rps_status[5] - self.sDB.rps_status.no_cool = rps_status[6] + self.sDB.rps_status.high_temp = rps_status[2] + self.sDB.rps_status.no_cool = rps_status[3] + self.sDB.rps_status.ex_waste = rps_status[4] + self.sDB.rps_status.ex_hcool = rps_status[5] + self.sDB.rps_status.no_fuel = rps_status[6] self.sDB.rps_status.fault = rps_status[7] self.sDB.rps_status.timeout = rps_status[8] self.sDB.rps_status.manual = rps_status[9] + self.sDB.rps_status.sys_fail = rps_status[10] end -- copy in the reactor status @@ -205,20 +208,19 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) -- copy in the reactor structure ---@param mek_data table local function _copy_struct(mek_data) - self.sDB.mek_struct.formed = mek_data[1] - self.sDB.mek_struct.length = mek_data[2] - self.sDB.mek_struct.width = mek_data[3] - self.sDB.mek_struct.height = mek_data[4] - self.sDB.mek_struct.min_pos = mek_data[5] - self.sDB.mek_struct.max_pos = mek_data[6] - self.sDB.mek_struct.heat_cap = mek_data[7] - self.sDB.mek_struct.fuel_asm = mek_data[8] - self.sDB.mek_struct.fuel_sa = mek_data[9] - self.sDB.mek_struct.fuel_cap = mek_data[10] - self.sDB.mek_struct.waste_cap = mek_data[11] - self.sDB.mek_struct.ccool_cap = mek_data[12] - self.sDB.mek_struct.hcool_cap = mek_data[13] - self.sDB.mek_struct.max_burn = mek_data[14] + self.sDB.mek_struct.length = mek_data[1] + self.sDB.mek_struct.width = mek_data[2] + self.sDB.mek_struct.height = mek_data[3] + self.sDB.mek_struct.min_pos = mek_data[4] + self.sDB.mek_struct.max_pos = mek_data[5] + self.sDB.mek_struct.heat_cap = mek_data[6] + self.sDB.mek_struct.fuel_asm = mek_data[7] + self.sDB.mek_struct.fuel_sa = mek_data[8] + self.sDB.mek_struct.fuel_cap = mek_data[9] + self.sDB.mek_struct.waste_cap = mek_data[10] + self.sDB.mek_struct.ccool_cap = mek_data[11] + self.sDB.mek_struct.hcool_cap = mek_data[12] + self.sDB.mek_struct.max_burn = mek_data[13] end -- mark this PLC session as closed, stop watchdog @@ -298,18 +300,22 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) self.sDB.last_status_update = pkt.data[1] self.sDB.control_state = pkt.data[2] self.sDB.rps_tripped = pkt.data[3] - self.sDB.degraded = pkt.data[4] - self.sDB.mek_status.heating_rate = pkt.data[5] + self.sDB.no_reactor = pkt.data[4] + self.sDB.formed = pkt.data[5] - -- attempt to read mek_data table - if pkt.data[6] ~= nil then - local status = pcall(_copy_status, pkt.data[6]) - if status then - -- copied in status data OK - self.received_status_cache = true - else - -- error copying status data - log.error(log_header .. "failed to parse status packet data") + if not self.sDB.no_reactor and self.sDB.formed then + self.sDB.mek_status.heating_rate = pkt.data[6] + + -- attempt to read mek_data table + if pkt.data[7] ~= nil then + local status = pcall(_copy_status, pkt.data[7]) + if status then + -- copied in status data OK + self.received_status_cache = true + else + -- error copying status data + log.error(log_header .. "failed to parse status packet data") + end end end else @@ -379,7 +385,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) }) elseif pkt.type == RPLC_TYPES.RPS_STATUS then -- RPS status packet received, copy data - if pkt.length == 9 then + if pkt.length == 10 then local status = pcall(_copy_rps_status, pkt.data) if status then -- copied in RPS status data OK @@ -392,7 +398,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) end elseif pkt.type == RPLC_TYPES.RPS_ALARM then -- RPS alarm - if pkt.length == 10 then + if pkt.length == 11 then self.sDB.rps_tripped = true self.sDB.rps_trip_cause = pkt.data[1] local status = pcall(_copy_rps_status, { table.unpack(pkt.data, 2, pkt.length) }) @@ -490,7 +496,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) self.sDB.control_state, self.sDB.rps_tripped, self.sDB.rps_trip_cause, - self.sDB.degraded + self.sDB.no_reactor, + self.sDB.formed } end @@ -609,21 +616,41 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) local rtimes = self.retry_times - -- struct request retry + if (not self.sDB.no_reactor) and self.sDB.formed then + -- struct request retry - if not self.received_struct then - if rtimes.struct_req - util.time() <= 0 then - _send(RPLC_TYPES.MEK_STRUCT, {}) - rtimes.struct_req = util.time() + RETRY_PERIOD + if not self.received_struct then + if rtimes.struct_req - util.time() <= 0 then + _send(RPLC_TYPES.MEK_STRUCT, {}) + rtimes.struct_req = util.time() + RETRY_PERIOD + end end - end - -- status cache request retry + -- status cache request retry - if not self.received_status_cache then - if rtimes.status_req - util.time() <= 0 then - _send(RPLC_TYPES.MEK_STATUS, {}) - rtimes.status_req = util.time() + RETRY_PERIOD + if not self.received_status_cache then + if rtimes.status_req - util.time() <= 0 then + _send(RPLC_TYPES.MEK_STATUS, {}) + rtimes.status_req = util.time() + RETRY_PERIOD + end + end + + -- enable request retry + + if not self.acks.enable then + if rtimes.enable_req - util.time() <= 0 then + _send(RPLC_TYPES.RPS_ENABLE, {}) + rtimes.enable_req = util.time() + RETRY_PERIOD + end + end + + -- burn rate request retry + + if not self.acks.burn_rate then + if rtimes.burn_rate_req - util.time() <= 0 then + _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + rtimes.burn_rate_req = util.time() + RETRY_PERIOD + end end end @@ -636,24 +663,6 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) end end - -- enable request retry - - if not self.acks.enable then - if rtimes.enable_req - util.time() <= 0 then - _send(RPLC_TYPES.RPS_ENABLE, {}) - rtimes.enable_req = util.time() + RETRY_PERIOD - end - end - - -- burn rate request retry - - if not self.acks.burn_rate then - if rtimes.burn_rate_req - util.time() <= 0 then - _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) - rtimes.burn_rate_req = util.time() + RETRY_PERIOD - end - end - -- RPS reset request retry if not self.acks.rps_reset then diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 1e3148b..adf6437 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -13,7 +13,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "beta-v0.6.5" +local SUPERVISOR_VERSION = "beta-v0.6.6" local print = util.print local println = util.println