diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 6aa2caa..76e10cc 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -39,7 +39,7 @@ function iocontrol.init(conf, comms) ---@class ioctl_entry local entry = { - unit_id = i, ---@type integer + unit_id = i, ---@type integer initialized = false, num_boilers = 0, @@ -53,15 +53,15 @@ function iocontrol.init(conf, comms) scram = function () end, reset_rps = function () end, ack_alarms = function () end, - set_burn = function (rate) end, ---@param rate number - set_waste = function (mode) end, ---@param mode integer + set_burn = function (rate) end, ---@param rate number + set_waste = function (mode) end, ---@param mode integer - start_ack = function (success) end, ---@param success boolean - scram_ack = function (success) end, ---@param success boolean - reset_rps_ack = function (success) end, ---@param success boolean - ack_alarms_ack = function (success) end,---@param success boolean - set_burn_ack = function (success) end, ---@param success boolean - set_waste_ack = function (success) end, ---@param success boolean + start_ack = function (success) end, ---@param success boolean + scram_ack = function (success) end, ---@param success boolean + reset_rps_ack = function (success) end, ---@param success boolean + ack_alarms_ack = function (success) end, ---@param success boolean + set_burn_ack = function (success) end, ---@param success boolean + set_waste_ack = function (success) end, ---@param success boolean alarm_callbacks = { c_breach = { ack = function () ack(1) end, reset = function () reset(1) end }, @@ -95,7 +95,7 @@ function iocontrol.init(conf, comms) }, reactor_ps = psil.create(), - reactor_data = {}, ---@type reactor_db + reactor_data = {}, ---@type reactor_db boiler_ps_tbl = {}, boiler_data_tbl = {}, @@ -221,18 +221,19 @@ end ---@return boolean valid function iocontrol.update_statuses(statuses) if type(statuses) ~= "table" then - log.debug("unit statuses not a table") + log.debug("iocontrol.update_statuses: unit statuses not a table") return false elseif #statuses ~= #io.units then - log.debug("number of provided unit statuses does not match expected number of units") + log.debug("iocontrol.update_statuses: number of provided unit statuses does not match expected number of units") return false else for i = 1, #statuses do + local log_header = util.c("iocontrol.update_statuses[unit ", i, "]: ") local unit = io.units[i] ---@type ioctl_entry local status = statuses[i] - if type(status) ~= "table" or #status ~= 4 then - log.debug("invalid status entry in unit statuses (not a table or invalid length)") + if type(status) ~= "table" or #status ~= 5 then + log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)") return false end @@ -255,7 +256,7 @@ function iocontrol.update_statuses(statuses) unit.reactor_data.no_reactor = gen_status[5] unit.reactor_data.formed = gen_status[6] else - log.debug("reactor general status length mismatch") + log.debug(log_header .. "reactor general status length mismatch") end unit.reactor_data.rps_status = rps_status ---@type rps_status @@ -295,75 +296,16 @@ function iocontrol.update_statuses(statuses) end end else - log.debug("reactor status length mismatch") - end - - -- annunciator - - local annunciator = status[2] ---@type annunciator - - for key, val in pairs(annunciator) do - if key == "TurbineTrip" then - -- split up turbine trip table for all turbines and a general OR combination - local trips = val - local any = false - - for id = 1, #trips do - any = any or trips[id] - unit.turbine_ps_tbl[id].publish(key, trips[id]) - end - - unit.reactor_ps.publish("TurbineTrip", any) - elseif key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then - -- split up array for all boilers - for id = 1, #val do - unit.boiler_ps_tbl[id].publish(key, val[id]) - end - elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then - -- split up array for all turbines - for id = 1, #val do - unit.turbine_ps_tbl[id].publish(key, val[id]) - end - elseif type(val) == "table" then - -- we missed one of the tables? - log.error("unrecognized table found in annunciator list, this is a bug", true) - else - -- non-table fields - unit.reactor_ps.publish(key, val) - end - end - - -- alarms - - local alarm_states = status[3] - - if type(alarm_states) == "table" then - for id = 1, #alarm_states do - local state = alarm_states[id] - - unit.alarms[id] = state - - if state == types.ALARM_STATE.TRIPPED or state == types.ALARM_STATE.ACKED then - unit.reactor_ps.publish("ALM" .. id, 2) - elseif state == types.ALARM_STATE.RING_BACK then - unit.reactor_ps.publish("ALM" .. id, 3) - else - unit.reactor_ps.publish("ALM" .. id, 1) - end - end - else - log.debug("alarm states not a table") - return false + log.debug(log_header .. "reactor status length mismatch") end -- RTU statuses - local rtu_statuses = status[4] + local rtu_statuses = status[2] if type(rtu_statuses) == "table" then + -- boiler statuses 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 @@ -403,12 +345,11 @@ function iocontrol.update_statuses(statuses) end end else - log.debug("boiler list not a table") + log.debug(log_header .. "boiler list not a table") end + -- turbine statuses if type(rtu_statuses.turbines) == "table" then - -- turbine statuses - for id = 1, #unit.turbine_ps_tbl do if rtu_statuses.turbines[i] == nil then -- disconnected @@ -450,11 +391,84 @@ function iocontrol.update_statuses(statuses) end end else - log.debug("turbine list not a table") + log.debug(log_header .. "turbine list not a table") return false end else - log.debug("rtu list not a table") + log.debug(log_header .. "rtu list not a table") + end + + -- annunciator + + local annunciator = status[3] ---@type annunciator + + for key, val in pairs(annunciator) do + if key == "TurbineTrip" then + -- split up turbine trip table for all turbines and a general OR combination + local trips = val + local any = false + + for id = 1, #trips do + any = any or trips[id] + unit.turbine_ps_tbl[id].publish(key, trips[id]) + end + + unit.reactor_ps.publish("TurbineTrip", any) + elseif key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then + -- split up array for all boilers + for id = 1, #val do + unit.boiler_ps_tbl[id].publish(key, val[id]) + end + elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then + -- split up array for all turbines + for id = 1, #val do + unit.turbine_ps_tbl[id].publish(key, val[id]) + end + elseif type(val) == "table" then + -- we missed one of the tables? + log.error(log_header .. "unrecognized table found in annunciator list, this is a bug", true) + else + -- non-table fields + unit.reactor_ps.publish(key, val) + end + end + + -- alarms + + local alarm_states = status[4] + + if type(alarm_states) == "table" then + for id = 1, #alarm_states do + local state = alarm_states[id] + + unit.alarms[id] = state + + if state == types.ALARM_STATE.TRIPPED or state == types.ALARM_STATE.ACKED then + unit.reactor_ps.publish("Alarm_" .. id, 2) + elseif state == types.ALARM_STATE.RING_BACK then + unit.reactor_ps.publish("Alarm_" .. id, 3) + else + unit.reactor_ps.publish("Alarm_" .. id, 1) + end + end + else + log.debug(log_header .. "alarm states not a table") + end + + -- unit state fields + + local unit_state = status[5] + + if type(unit_state) == "table" then + if #unit_state == 3 then + unit.reactor_ps.publish("U_StatusLine1", unit_state[1]) + unit.reactor_ps.publish("U_StatusLine2", unit_state[2]) + unit.reactor_ps.publish("U_WasteMode", unit_state[3]) + else + log.debug(log_header .. "unit state length mismatch") + end + else + log.debug(log_header .. "unit state not a table") end end diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 14cf63d..db8c426 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -18,7 +18,7 @@ local coordinator = require("coordinator.coordinator") local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") -local COORDINATOR_VERSION = "beta-v0.7.4" +local COORDINATOR_VERSION = "beta-v0.7.5" 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 c2ebf16..39bfeb4 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -308,8 +308,8 @@ local function init(parent, id) local set_burn = function () unit.set_burn(burn_rate.get_value()) end PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=set_burn} - r_ps.subscribe("burn_rate", function (v) burn_rate.set_value(v) end) - r_ps.subscribe("max_burn", function (v) burn_rate.set_max(v) end) + r_ps.subscribe("burn_rate", burn_rate.set_value) + r_ps.subscribe("max_burn", burn_rate.set_max) local dis_colors = cpair(colors.white, colors.lightGray) @@ -334,18 +334,24 @@ 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) - TextBox{parent=main,x=2,y=30,text="Idle",width=29,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)} + 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)} - 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)} + 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)} TextBox{parent=waste_sel,text="Waste Processing",alignment=TEXT_ALIGN.CENTER,x=1,y=1,height=1} + r_ps.subscribe("U_WasteMode", waste_mode.set_value) + ---------------------- -- alarm management -- ---------------------- - local alarm_panel = Div{parent=main,x=2,y=32,width=29,height=16,fg_bg=cpair(colors.black,colors.white)} + local alarm_panel = Div{parent=main,x=2,y=33,width=29,height=16,fg_bg=cpair(colors.black,colors.white)} local a_brc = AlarmLight{parent=alarm_panel,x=6,y=2,label="Containment Breach",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS} local a_rad = AlarmLight{parent=alarm_panel,x=6,label="Containment Radiation",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS} @@ -362,20 +368,20 @@ local function init(parent, id) local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS} local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS} - r_ps.subscribe("ALM1", a_brc.update) - r_ps.subscribe("ALM2", a_rad.update) - r_ps.subscribe("ALM4", a_dmg.update) + r_ps.subscribe("Alarm_1", a_brc.update) + r_ps.subscribe("Alarm_2", a_rad.update) + r_ps.subscribe("Alarm_4", a_dmg.update) - r_ps.subscribe("ALM3", a_rcl.update) - r_ps.subscribe("ALM5", a_rcd.update) - r_ps.subscribe("ALM6", a_rot.update) - r_ps.subscribe("ALM7", a_rht.update) - r_ps.subscribe("ALM8", a_rwl.update) - r_ps.subscribe("ALM9", a_rwh.update) + r_ps.subscribe("Alarm_3", a_rcl.update) + r_ps.subscribe("Alarm_5", a_rcd.update) + r_ps.subscribe("Alarm_6", a_rot.update) + r_ps.subscribe("Alarm_7", a_rht.update) + r_ps.subscribe("Alarm_8", a_rwl.update) + r_ps.subscribe("Alarm_9", a_rwh.update) - r_ps.subscribe("ALM10", a_rps.update) - r_ps.subscribe("ALM11", a_clt.update) - r_ps.subscribe("ALM12", a_tbt.update) + r_ps.subscribe("Alarm_10", a_rps.update) + r_ps.subscribe("Alarm_11", a_clt.update) + r_ps.subscribe("Alarm_12", a_tbt.update) -- ack's and resets diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua index 0a70971..b5fd744 100644 --- a/coordinator/ui/layout/main_view.lua +++ b/coordinator/ui/layout/main_view.lua @@ -4,6 +4,7 @@ local iocontrol = require("coordinator.iocontrol") local sounder = require("coordinator.sounder") +local util = require("scada-common.util") local style = require("coordinator.ui.style") @@ -28,32 +29,46 @@ local function init(monitor) local main = DisplayBox{window=monitor,fg_bg=style.root} -- window header message - TextBox{parent=main,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} + local header = TextBox{parent=main,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} local db = iocontrol.get_db() local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element + local cnc_y_start = 3 + -- unit overviews - if db.facility.num_units >= 1 then uo_1 = unit_overview(main, 2, 3, db.units[1]) end - if db.facility.num_units >= 2 then uo_2 = unit_overview(main, 84, 3, db.units[2]) end + if db.facility.num_units >= 1 then + uo_1 = unit_overview(main, 2, 3, db.units[1]) + cnc_y_start = cnc_y_start + uo_1.height() + 1 + end + + if db.facility.num_units >= 2 then + uo_2 = unit_overview(main, 84, 3, db.units[2]) + end if db.facility.num_units >= 3 then -- base offset 3, spacing 1, max height of units 1 and 2 local row_2_offset = 3 + 1 + math.max(uo_1.height(), uo_2.height()) uo_3 = unit_overview(main, 2, row_2_offset, db.units[3]) - if db.facility.num_units == 4 then uo_4 = unit_overview(main, 84, row_2_offset, db.units[4]) end + cnc_y_start = cnc_y_start + uo_3.height() + 1 + + if db.facility.num_units == 4 then + uo_4 = unit_overview(main, 84, row_2_offset, db.units[4]) + end end -- command & control + TextBox{parent=main,y=cnc_y_start,text=util.strrep("\x8c", header.width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} + -- testing ---@fixme remove test code ColorMap{parent=main,x=2,y=(main.height()-1)} - PushButton{parent=main,x=2,y=(main.height()-20),text="TEST 1",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_1} + PushButton{parent=main,x=2,y=(cnc_y_start+2),text="TEST 1",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_1} PushButton{parent=main,x=2,text="TEST 2",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_2} PushButton{parent=main,x=2,text="TEST 3",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_3} PushButton{parent=main,x=2,text="TEST 4",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_4} @@ -64,7 +79,7 @@ local function init(monitor) PushButton{parent=main,x=2,text="STOP",min_width=8,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.stop} PushButton{parent=main,x=2,text="PSCALE",min_width=8,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_power_scale} - SwitchButton{parent=main,x=12,y=(main.height()-20),text="CONTAINMENT BREACH",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_breach} + SwitchButton{parent=main,x=12,y=(cnc_y_start+2),text="CONTAINMENT BREACH",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_breach} SwitchButton{parent=main,x=12,text="CONTAINMENT RADIATION",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_rad} SwitchButton{parent=main,x=12,text="REACTOR LOST",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_lost} SwitchButton{parent=main,x=12,text="CRITICAL DAMAGE",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_crit} diff --git a/coordinator/ui/layout/unit_view.lua b/coordinator/ui/layout/unit_view.lua index bdf0537..1c5fddf 100644 --- a/coordinator/ui/layout/unit_view.lua +++ b/coordinator/ui/layout/unit_view.lua @@ -2,15 +2,11 @@ -- Reactor Unit SCADA Coordinator GUI -- -local tcallbackdsp = require("scada-common.tcallbackdsp") +local style = require("coordinator.ui.style") -local iocontrol = require("coordinator.iocontrol") +local unit_detail = require("coordinator.ui.components.unit_detail") -local style = require("coordinator.ui.style") - -local unit_detail = require("coordinator.ui.components.unit_detail") - -local DisplayBox = require("graphics.elements.displaybox") +local DisplayBox = require("graphics.elements.displaybox") -- create a unit view ---@param monitor table diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua index 7369866..51edfb8 100644 --- a/graphics/elements/controls/hazard_button.lua +++ b/graphics/elements/controls/hazard_button.lua @@ -174,7 +174,7 @@ local function hazard_button(args) end end - -- set the value + -- set the value (true simulates pressing the button) ---@param val boolean new value function e.set_value(val) if val then e.handle_touch(core.events.touch("", 1, 1)) end diff --git a/graphics/elements/controls/multi_button.lua b/graphics/elements/controls/multi_button.lua index 4948b33..24614c8 100644 --- a/graphics/elements/controls/multi_button.lua +++ b/graphics/elements/controls/multi_button.lua @@ -109,7 +109,6 @@ local function multi_button(args) function e.set_value(val) e.value = val draw() - args.callback(e.value) end -- initial draw diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua index 397a367..018b0e9 100644 --- a/graphics/elements/controls/push_button.lua +++ b/graphics/elements/controls/push_button.lua @@ -72,7 +72,7 @@ local function push_button(args) end end - -- set the value + -- set the value (true simulates pressing the button) ---@param val boolean new value function e.set_value(val) if val then e.handle_touch(core.events.touch("", 1, 1)) end diff --git a/graphics/elements/controls/switch_button.lua b/graphics/elements/controls/switch_button.lua index 46364b5..bf138f2 100644 --- a/graphics/elements/controls/switch_button.lua +++ b/graphics/elements/controls/switch_button.lua @@ -82,9 +82,6 @@ local function switch_button(args) -- set state e.value = val draw_state() - - -- call the touch callback with state - args.callback(e.value) end return e.get() diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index 1aaf384..9a5f10f 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -129,7 +129,13 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) for i = 1, #self.units do local unit = self.units[i] ---@type reactor_unit - status[unit.get_id()] = { unit.get_reactor_status(), unit.get_annunciator(), unit.get_alarms(), unit.get_rtu_statuses() } + status[unit.get_id()] = { + unit.get_reactor_status(), + unit.get_rtu_statuses(), + unit.get_annunciator(), + unit.get_alarms(), + unit.get_state() + } end _send(SCADA_CRDN_TYPES.UNIT_STATUSES, status) diff --git a/supervisor/session/unit.lua b/supervisor/session/unit.lua index fa85adc..4cf3ac1 100644 --- a/supervisor/session/unit.lua +++ b/supervisor/session/unit.lua @@ -54,6 +54,13 @@ local aistate_string = { "RING_BACK_TRIPPING" } +-- check if an alarm is active (tripped or ack'd) +---@param alarm table alarm entry +---@return boolean active +local function is_active(alarm) + return alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED +end + ---@class alarm_def ---@field state ALARM_INT_STATE internal alarm state ---@field trip_time integer time (ms) when first tripped @@ -73,11 +80,16 @@ function unit.new(for_reactor, num_boilers, num_turbines) turbines = {}, boilers = {}, redstone = {}, + -- state tracking deltas = {}, last_heartbeat = 0, + damage_initial = 0, + damage_start = 0, + waste_mode = WASTE_MODE.AUTO, + status_text = { "Unknown", "Awaiting Connection..." }, -- logic for alarms had_reactor = false, - start_time = 0, + start_ms = 0, plc_cache = { ok = false, rps_trip = false, @@ -110,7 +122,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) RPSTransient = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.RPSTransient, tier = PRIO.URGENT }, -- BoilRateMismatch, CoolantFeedMismatch, SteamFeedMismatch, MaxWaterReturnFeed RCSTransient = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 5, id = ALARM.RCSTransient, tier = PRIO.TIMELY }, - -- "It's just a routine turbin' trip!" -Bill Gibson + -- "It's just a routine turbin' trip!" -Bill Gibson, "The China Syndrome" TurbineTrip = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.TurbineTrip, tier = PRIO.URGENT } }, ---@class unit_db @@ -244,10 +256,10 @@ function unit.new(for_reactor, num_boilers, num_turbines) end -- waste valves - local waste_pu = { open = function () __rs_w(IO.WASTE_PU, true) end, close = function () __rs_w(IO.WASTE_PU, false) end } - local waste_sna = { open = function () __rs_w(IO.WASTE_PO, true) end, close = function () __rs_w(IO.WASTE_PO, false) end } + local waste_pu = { open = function () __rs_w(IO.WASTE_PU, true) end, close = function () __rs_w(IO.WASTE_PU, false) end } + local waste_sna = { open = function () __rs_w(IO.WASTE_PO, true) end, close = function () __rs_w(IO.WASTE_PO, false) end } local waste_po = { open = function () __rs_w(IO.WASTE_POPL, true) end, close = function () __rs_w(IO.WASTE_POPL, false) end } - local waste_sps = { open = function () __rs_w(IO.WASTE_AM, true) end, close = function () __rs_w(IO.WASTE_AM, false) end } + local waste_sps = { open = function () __rs_w(IO.WASTE_AM, true) end, close = function () __rs_w(IO.WASTE_AM, false) end } --#endregion @@ -396,14 +408,14 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- check PLC status self.db.annunciator.PLCOnline = (self.plc_s ~= nil) and (self.plc_s.open) - if self.plc_s ~= nil then + if self.plc_i ~= nil then local plc_db = self.plc_i.get_db() -- record reactor start time (some alarms are delayed during reactor heatup) - if self.start_time == 0 and plc_db.mek_status.status then - self.start_time = util.time_ms() + if self.start_ms == 0 and plc_db.mek_status.status then + self.start_ms = util.time_ms() elseif not plc_db.mek_status.status then - self.start_time = 0 + self.start_ms = 0 end -- record reactor stats @@ -414,6 +426,15 @@ function unit.new(for_reactor, num_boilers, num_turbines) self.plc_cache.temp = plc_db.mek_status.temp 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 + else + self.damage_initial = 0 + self.damage_start = 0 + end + -- heartbeat blink about every second if self.last_heartbeat + 1000 < plc_db.last_status_update then self.db.annunciator.PLCHeartbeat = not self.db.annunciator.PLCHeartbeat @@ -633,9 +654,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) if plc_cache.rps_status.manual ~= nil then if plc_cache.rps_trip then for key, val in pairs(plc_cache.rps_status) do - if key ~= "manual" and key ~= "timeout" then - rps_alarm = rps_alarm or val - end + if key ~= "manual" and key ~= "timeout" then rps_alarm = rps_alarm or val end end end end @@ -651,7 +670,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) 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_time > FLOW_STABILITY_DELAY_MS then + if util.time_ms() - self.start_ms > FLOW_STABILITY_DELAY_MS then rcs_trans = rcs_trans or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch end @@ -758,6 +777,42 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- update alarm status _update_alarms() + + -- update status text (what the reactor doin?) + if is_active(self.alarms.ContainmentBreach) then + -- boom? or was boom disabled + if self.plc_i ~= nil and self.plc_i.get_rps().force_dis then + self.status_text = { "REACTOR FORCE DISABLED", "meltdown would have occured" } + else + self.status_text = { "CORE MELTDOWN", "reactor destroyed" } + end + elseif is_active(self.alarms.CriticalDamage) then + -- so much for it being a "routine turbin' trip"... + 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" + 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 + + self.status_text[2] = util.c("damage critical in ", remaining_s, "s") + else + self.status_text[2] = "estimating time to critical..." + end + -- 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" } + else + self.status_text = { "Idle", "" } + end + else + self.status_text = { "Reactor Off-line", "awaiting connection..." } + end end -- OPERATIONS -- @@ -792,24 +847,30 @@ function unit.new(for_reactor, num_boilers, num_turbines) function public.set_waste(mode) if mode == WASTE_MODE.AUTO then ---@todo automatic waste routing + self.waste_mode = mode elseif mode == WASTE_MODE.PLUTONIUM then -- route through plutonium generation + self.waste_mode = mode waste_pu.open() waste_sna.close() waste_po.close() waste_sps.close() elseif mode == WASTE_MODE.POLONIUM then -- route through polonium generation into pellets + self.waste_mode = mode waste_pu.close() waste_sna.open() waste_po.open() waste_sps.close() elseif mode == WASTE_MODE.ANTI_MATTER then -- route through polonium generation into SPS + self.waste_mode = mode waste_pu.close() waste_sna.open() waste_po.close() waste_sps.open() + else + log.debug(util.c("invalid waste mode setting ", mode)) end end @@ -889,6 +950,11 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- get the alarm states function public.get_alarms() return self.db.alarm_states end + -- get unit state (currently only waste mode) + function public.get_state() + return { self.status_text[1], self.status_text[2], self.waste_mode } + end + -- get the reactor ID function public.get_id() return self.r_id end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 6257679..e3f0415 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.2" +local SUPERVISOR_VERSION = "beta-v0.8.3" local print = util.print local println = util.println