diff --git a/coordinator/startup.lua b/coordinator/startup.lua index efc900e..7b63302 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local iocontrol = require("coordinator.iocontrol") local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") -local COORDINATOR_VERSION = "beta-v0.7.6" +local COORDINATOR_VERSION = "beta-v0.7.7" local print = util.print local println = util.println diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index 39bfeb4..d92f5e7 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -107,6 +107,12 @@ local function init(parent, id) DataIndicator{parent=main,x=21,label="",format="%6.2f",value=0,unit="mSv/h",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg} main.line_break() + local stat_line_1 = TextBox{parent=main,x=2,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.black, colors.white)} + local stat_line_2 = TextBox{parent=main,x=2,text="awaiting data",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)} + + r_ps.subscribe("U_StatusLine1", stat_line_1.set_value) + r_ps.subscribe("U_StatusLine2", stat_line_2.set_value) + ----------------- -- annunciator -- ----------------- @@ -301,7 +307,7 @@ local function init(parent, id) -- reactor controls -- ---------------------- - local burn_control = Div{parent=main,x=2,y=22,width=19,height=3,fg_bg=cpair(colors.gray,colors.white)} + local burn_control = Div{parent=main,x=2,y=25,width=19,height=3,fg_bg=cpair(colors.gray,colors.white)} local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=cpair(colors.black,colors.white)} TextBox{parent=burn_control,x=9,y=2,text="mB/t"} @@ -313,10 +319,10 @@ local function init(parent, id) local dis_colors = cpair(colors.white, colors.lightGray) - local start = HazardButton{parent=main,x=22,y=22,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,fg_bg=hzd_fg_bg} - local ack_a = HazardButton{parent=main,x=12,y=26,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,fg_bg=hzd_fg_bg} - local scram = HazardButton{parent=main,x=2,y=26,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,fg_bg=hzd_fg_bg} - local reset = HazardButton{parent=main,x=22,y=26,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,fg_bg=hzd_fg_bg} + local start = HazardButton{parent=main,x=22,y=25,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,fg_bg=hzd_fg_bg} + local ack_a = HazardButton{parent=main,x=12,y=29,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,fg_bg=hzd_fg_bg} + local scram = HazardButton{parent=main,x=2,y=29,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,fg_bg=hzd_fg_bg} + local reset = HazardButton{parent=main,x=22,y=29,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,fg_bg=hzd_fg_bg} unit.start_ack = start.on_response unit.scram_ack = scram.on_response @@ -334,12 +340,6 @@ local function init(parent, id) r_ps.subscribe("rps_tripped", start_button_en_check) r_ps.subscribe("rps_tripped", function (active) if active then reset.enable() else reset.disable() end end) - local stat_line_1 = DataIndicator{parent=main,x=2,y=30,label="",format="%s",value="",width=29,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)} - local stat_line_2 = DataIndicator{parent=main,x=2,y=31,label="",format="%s",value="",width=29,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)} - - r_ps.subscribe("U_StatusLine1", stat_line_1.update) - r_ps.subscribe("U_StatusLine2", stat_line_2.update) - local waste_sel = Div{parent=main,x=2,y=50,width=29,height=2,fg_bg=cpair(colors.black, colors.white)} local waste_mode = MultiButton{parent=waste_sel,x=1,y=1,options=waste_opts,callback=unit.set_waste,min_width=6,fg_bg=cpair(colors.black, colors.white)} diff --git a/scada-common/types.lua b/scada-common/types.lua index abc7561..cfdfcad 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -175,7 +175,7 @@ types.rtu_t = { env_detector = "environment_detector" } ----@alias rps_status_t string +---@alias rps_status_t rps_trip_cause types.rps_status_t = { ok = "ok", dmg_crit = "dmg_crit", diff --git a/supervisor/session/unit.lua b/supervisor/session/unit.lua index 4cf3ac1..b0d0fea 100644 --- a/supervisor/session/unit.lua +++ b/supervisor/session/unit.lua @@ -85,12 +85,15 @@ function unit.new(for_reactor, num_boilers, num_turbines) last_heartbeat = 0, damage_initial = 0, damage_start = 0, + damage_last = 0, + damage_est_last = 0, waste_mode = WASTE_MODE.AUTO, status_text = { "Unknown", "Awaiting Connection..." }, -- logic for alarms had_reactor = false, start_ms = 0, plc_cache = { + active = false, ok = false, rps_trip = false, rps_status = {}, ---@type rps_status @@ -419,6 +422,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) end -- record reactor stats + self.plc_cache.active = plc_db.mek_status.status self.plc_cache.ok = not (plc_db.rps_status.fault or plc_db.rps_status.sys_fail or plc_db.rps_status.force_dis) self.plc_cache.rps_trip = plc_db.rps_tripped self.plc_cache.rps_status = plc_db.rps_status @@ -427,12 +431,16 @@ function unit.new(for_reactor, num_boilers, num_turbines) self.plc_cache.waste = plc_db.mek_status.waste_fill -- track damage - if (self.damage_initial == 0) and (plc_db.mek_status.damage > 0) then - self.damage_start = util.time_s() - self.damage_initial = plc_db.mek_status.damage + if plc_db.mek_status.damage > 0 then + if self.damage_start == 0 then + self.damage_start = util.time_s() + self.damage_initial = plc_db.mek_status.damage + end else - self.damage_initial = 0 self.damage_start = 0 + self.damage_initial = 0 + self.damage_last = 0 + self.damage_est_last = 0 end -- heartbeat blink about every second @@ -662,15 +670,17 @@ function unit.new(for_reactor, num_boilers, num_turbines) _update_alarm_state(rps_alarm, self.alarms.RPSTransient) -- RCS Transient - local any_low = false + local any_low = annunc.CoolantLevelLow local any_over = false for i = 1, #annunc.WaterLevelLow do any_low = any_low or annunc.WaterLevelLow[i] end for i = 1, #annunc.TurbineOverSpeed do any_over = any_over or annunc.TurbineOverSpeed[i] end local rcs_trans = any_low or any_over or annunc.RCPTrip or annunc.RCSFlowLow or annunc.MaxWaterReturnFeed - -- flow is ramping up right after reactor start, annunciator indicators for these states may not indicate a real issue - if util.time_ms() - self.start_ms > FLOW_STABILITY_DELAY_MS then + -- annunciator indicators for these states may not indicate a real issue when: + -- > flow is ramping up right after reactor start + -- > flow is ramping down after reactor shutdown + if (util.time_ms() - self.start_ms > FLOW_STABILITY_DELAY_MS) and plc_cache.active then rcs_trans = rcs_trans or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch end @@ -791,24 +801,102 @@ function unit.new(for_reactor, num_boilers, num_turbines) self.status_text = { "MELTDOWN IMMINENT", "evacuate facility immediately" } elseif is_active(self.alarms.ReactorDamage) then -- attempt to determine when a chance of a meltdown will occur - self.status_text[1] = "Containment Taking Damage" + self.status_text[1] = "CONTAINMENT TAKING DAMAGE" if self.plc_cache.damage >= 100 then self.status_text[2] = "damage critical" elseif (self.plc_cache.damage - self.damage_initial) > 0 then - local rate = (self.plc_cache.damage - self.damage_initial) / (util.time_s() - self.damage_start) - local remaining_s = (100 - self.plc_cache.damage) * rate + if self.plc_cache.damage > self.damage_last then + self.damage_last = self.plc_cache.damage + local rate = (self.plc_cache.damage - self.damage_initial) / (util.time_s() - self.damage_start) + self.damage_est_last = (100 - self.plc_cache.damage) / rate + end - self.status_text[2] = util.c("damage critical in ", remaining_s, "s") + self.status_text[2] = util.c("damage critical in ", util.sprintf("%.1f", self.damage_est_last), "s") else self.status_text[2] = "estimating time to critical..." end + elseif is_active(self.alarms.ContainmentRadiation) then + self.status_text = { "RADIATION DETECTED", "radiation levels above normal" } + -- elseif is_active(self.alarms.RPSTransient) then + -- RPS status handled when checking reactor status + elseif is_active(self.alarms.RCSTransient) then + self.status_text = { "RCS TRANSIENT", "check coolant system" } + elseif is_active(self.alarms.ReactorOverTemp) then + self.status_text = { "CORE OVER TEMP", "reactor core temperature >=1200K" } + elseif is_active(self.alarms.ReactorWasteLeak) then + self.status_text = { "WASTE LEAK", "radioactive waste leak detected" } + elseif is_active(self.alarms.ReactorHighTemp) then + self.status_text = { "CORE TEMP HIGH", "reactor core temperature >1150K" } + elseif is_active(self.alarms.ReactorHighWaste) then + self.status_text = { "WASTE LEVEL HIGH", "waste accumulating in reactor" } + elseif is_active(self.alarms.TurbineTrip) then + self.status_text = { "TURBINE TRIP", "turbine stall occured" } -- connection dependent states elseif self.plc_i ~= nil then local plc_db = self.plc_i.get_db() if plc_db.mek_status.status then - self.status_text = { "Active", "reactor nominal" } + self.status_text[1] = "ACTIVE" + + if self.db.annunciator.ReactorHighDeltaT then + self.status_text[2] = "core temperature rising" + elseif self.db.annunciator.ReactorTempHigh then + self.status_text[2] = "core temp high, system nominal" + elseif self.db.annunciator.FuelInputRateLow then + self.status_text[2] = "insufficient fuel input rate" + elseif self.db.annunciator.WasteLineOcclusion then + self.status_text[2] = "insufficient waste output rate" + elseif (util.time_ms() - self.start_ms) <= FLOW_STABILITY_DELAY_MS then + if num_turbines > 1 then + self.status_text[2] = "turbines spinning up" + else + self.status_text[2] = "turbine spinning up" + end + else + self.status_text[2] = "system nominal" + end + elseif plc_db.rps_tripped then + local cause = "unknown" + + if plc_db.rps_trip_cause == "ok" then + -- hmm... + elseif plc_db.rps_trip_cause == "dmg_crit" then + cause = "core damage critical" + elseif plc_db.rps_trip_cause == "high_temp" then + cause = "core temperature high" + elseif plc_db.rps_trip_cause == "no_coolant" then + cause = "insufficient coolant" + elseif plc_db.rps_trip_cause == "full_waste" then + cause = "excess waste" + elseif plc_db.rps_trip_cause == "heated_coolant_backup" then + cause = "excess heated coolant" + elseif plc_db.rps_trip_cause == "no_fuel" then + cause = "insufficient fuel" + elseif plc_db.rps_trip_cause == "fault" then + cause = "hardware fault" + elseif plc_db.rps_trip_cause == "timeout" then + cause = "connection timed out" + elseif plc_db.rps_trip_cause == "manual" then + cause = "manual operator SCRAM" + elseif plc_db.rps_trip_cause == "automatic" then + cause = "automated system SCRAM" + elseif plc_db.rps_trip_cause == "sys_fail" then + cause = "PLC system failure" + elseif plc_db.rps_trip_cause == "force_disabled" then + cause = "reactor force disabled" + end + + self.status_text = { "RPS SCRAM", cause } else - self.status_text = { "Idle", "" } + self.status_text[1] = "IDLE" + + local temp = plc_db.mek_status.temp + if temp < 350 then + self.status_text[2] = "core cold" + elseif temp < 600 then + self.status_text[2] = "core warm" + else + self.status_text[2] = "core hot" + end end else self.status_text = { "Reactor Off-line", "awaiting connection..." } diff --git a/supervisor/startup.lua b/supervisor/startup.lua index e3f0415..f54330a 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "beta-v0.8.3" +local SUPERVISOR_VERSION = "beta-v0.8.4" local print = util.print local println = util.println