diff --git a/supervisor/config.lua b/supervisor/config.lua index 734e820..b98ceb2 100644 --- a/supervisor/config.lua +++ b/supervisor/config.lua @@ -6,6 +6,13 @@ config.SCADA_DEV_LISTEN = 16000 config.SCADA_SV_LISTEN = 16100 -- expected number of reactors config.NUM_REACTORS = 4 +-- expected number of boilers/turbines for each reactor +config.REACTOR_COOLING = { + { BOILERS = 1, TURBINES = 1 }, -- reactor unit 1 + { BOILERS = 1, TURBINES = 1 }, -- reactor unit 2 + { BOILERS = 1, TURBINES = 1 }, -- reactor unit 3 + { BOILERS = 1, TURBINES = 1 } -- reactor unit 4 +} -- log path config.LOG_PATH = "/log.txt" -- log mode diff --git a/supervisor/unit.lua b/supervisor/unit.lua index a246ebc..0afd7e5 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -1,19 +1,35 @@ +local types = require "scada-common.types" +local util = require "scada-common.util" + local unit = {} +---@alias TRI_FAIL integer +local TRI_FAIL = { + OK = 0, + PARTIAL = 1, + FULL = 2 +} + -- create a new reactor unit ----@param for_reactor integer -unit.new = function (for_reactor) +---@param for_reactor integer reactor unit number +---@param num_boilers integer number of boilers expected +---@param num_turbines integer number of turbines expected +unit.new = function (for_reactor, num_boilers, num_turbines) local self = { r_id = for_reactor, - plc_s = nil, + plc_s = nil, ---@class plc_session + counts = { boilers = num_boilers, turbines = num_turbines }, turbines = {}, boilers = {}, energy_storage = {}, redstone = {}, + deltas = { + last_reactor_temp = nil, + last_reactor_temp_time = 0 + }, db = { ---@class annunciator annunciator = { - -- RPS -- reactor PLCOnline = false, ReactorTrip = false, @@ -22,18 +38,18 @@ unit.new = function (for_reactor) RCSFlowLow = false, ReactorTempHigh = false, ReactorHighDeltaT = false, - ReactorOverPower = false, HighStartupRate = false, -- boiler - BoilerOnline = false, + BoilerOnline = TRI_FAIL.OK, HeatingRateLow = false, + BoilRateMismatch = false, CoolantFeedMismatch = false, -- turbine - TurbineOnline = false, + TurbineOnline = TRI_FAIL.OK, SteamFeedMismatch = false, SteamDumpOpen = false, - TurbineTrip = false, - TurbineOverUnderSpeed = false + TurbineOverSpeed = false, + TurbineTrip = false } } } @@ -45,8 +61,110 @@ unit.new = function (for_reactor) -- update the annunciator local _update_annunciator = function () + -- check PLC status self.db.annunciator.PLCOnline = (self.plc_s ~= nil) and (self.plc_s.open) - self.db.annunciator.ReactorTrip = false + + if self.plc_s ~= nil then + ------------- + -- REACTOR -- + ------------- + + local plc_db = self.plc_s.get_db() + + -- compute deltas + local reactor_delta_t = 0 + if self.deltas.last_reactor_temp ~= nil then + reactor_delta_t = (plc_db.mek_status.temp - self.deltas.last_reactor_temp) / (util.time_s() - self.deltas.last_reactor_temp_time) + else + self.deltas.last_reactor_temp = plc_db.mek_status.temp + self.deltas.last_reactor_temp_time = util.time_s() + end + + -- update annunciator + self.db.annunciator.ReactorTrip = plc_db.rps_tripped + self.db.annunciator.ManualReactorTrip = plc_db.rps_trip_cause == types.rps_status_t.manual + self.db.annunciator.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.no_cool) + self.db.annunciator.RCSFlowLow = plc_db.mek_status.ccool_fill < 0.75 or plc_db.mek_status.hcool_fill > 0.25 + self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > 1000 + self.db.annunciator.ReactorHighDeltaT = reactor_delta_t > 100 + -- @todo this is dependent on setup, i.e. how much coolant is buffered and the turbine setup + self.db.annunciator.HighStartupRate = not plc_db.control_state and plc_db.mek_status.burn_rate > 40 + end + + ------------- + -- BOILERS -- + ------------- + + -- check boiler online status + local connected_boilers = #self.boilers + if connected_boilers == 0 and self.num_boilers > 0 then + self.db.annunciator.BoilerOnline = TRI_FAIL.FULL + elseif connected_boilers > 0 and connected_boilers ~= self.num_boilers then + self.db.annunciator.BoilerOnline = TRI_FAIL.PARTIAL + else + self.db.annunciator.BoilerOnline = TRI_FAIL.OK + end + + local total_boil_rate = 0.0 + local no_boil_count = 0 + for i = 1, #self.boilers do + local boiler = self.boilers[i].get_db() ---@type boiler_session_db + local boil_rate = boiler.state.boil_rate + if boil_rate == 0 then + no_boil_count = no_boil_count + 1 + else + total_boil_rate = total_boil_rate + boiler.state.boil_rate + end + end + + if no_boil_count == 0 and self.num_boilers > 0 then + self.db.annunciator.HeatingRateLow = TRI_FAIL.FULL + elseif no_boil_count > 0 and no_boil_count ~= self.num_boilers then + self.db.annunciator.HeatingRateLow = TRI_FAIL.PARTIAL + else + self.db.annunciator.HeatingRateLow = TRI_FAIL.OK + end + + if self.plc_s ~= nil then + local expected_boil_rate = self.plc_s.get_db().mek_status.heating_rate / 10.0 + self.db.annunciator.BoilRateMismatch = math.abs(expected_boil_rate - total_boil_rate) > 25.0 + else + self.db.annunciator.BoilRateMismatch = false + end + + -------------- + -- TURBINES -- + -------------- + + -- check turbine online status + local connected_turbines = #self.turbines + if connected_turbines == 0 and self.num_turbines > 0 then + self.db.annunciator.TurbineOnline = TRI_FAIL.FULL + elseif connected_turbines > 0 and connected_turbines ~= self.num_turbines then + self.db.annunciator.TurbineOnline = TRI_FAIL.PARTIAL + else + self.db.annunciator.TurbineOnline = TRI_FAIL.OK + end + + --[[ + Turbine Under/Over Speed + ]]-- + + --[[ + Turbine Trip + a turbine trip is when the turbine stops, which means we are no longer receiving water and lose the ability to cool + this can be identified by these conditions: + - the current flow rate is 0 mB/t and it should not be + - it should not be if the boiler or reactor has a non-zero heating rate + - can initially catch this by detecting a 0 flow rate with a non-zero input rate, but eventually the steam will fill up + - can later identified by presence of steam in tank with a 0 flow rate + ]]-- + end + + -- unlink disconnected units + ---@param sessions table + local _unlink_disconnected_units = function (sessions) + util.filter_table(sessions, function (u) return u.is_connected() end) end -- PUBLIC FUNCTIONS -- @@ -55,14 +173,18 @@ unit.new = function (for_reactor) ---@param plc_session plc_session_struct public.link_plc_session = function (plc_session) self.plc_s = plc_session + self.deltas.last_reactor_temp = self.plc_s.get_db().mek_status.temp + self.deltas.last_reactor_temp_time = util.time_s() end - -- link a turbine RTU + -- link a turbine RTU session + ---@param turbine unit_session public.add_turbine = function (turbine) table.insert(self.turbines, turbine) end - -- link a boiler RTU + -- link a boiler RTU session + ---@param boiler unit_session public.add_boiler = function (boiler) table.insert(self.boilers, boiler) end @@ -85,6 +207,10 @@ unit.new = function (for_reactor) self.plc_s = nil end + -- unlink RTU unit sessions if they are closed + _unlink_disconnected_units(self.boilers) + _unlink_disconnected_units(self.turbines) + -- update annunciator logic _update_annunciator() end