cc-mek-scada/supervisor/unit.lua
2022-05-18 13:28:43 -04:00

225 lines
7.8 KiB
Lua

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 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, ---@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 = {
-- reactor
PLCOnline = false,
ReactorTrip = false,
ManualReactorTrip = false,
RCPTrip = false,
RCSFlowLow = false,
ReactorTempHigh = false,
ReactorHighDeltaT = false,
HighStartupRate = false,
-- boiler
BoilerOnline = TRI_FAIL.OK,
HeatingRateLow = false,
BoilRateMismatch = false,
CoolantFeedMismatch = false,
-- turbine
TurbineOnline = TRI_FAIL.OK,
SteamFeedMismatch = false,
SteamDumpOpen = false,
TurbineOverSpeed = false,
TurbineTrip = false
}
}
}
---@class reactor_unit
local public = {}
-- PRIVATE FUNCTIONS --
-- update the annunciator
local _update_annunciator = function ()
-- check PLC status
self.db.annunciator.PLCOnline = (self.plc_s ~= nil) and (self.plc_s.open)
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 --
-- link the PLC
---@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 session
---@param turbine unit_session
public.add_turbine = function (turbine)
table.insert(self.turbines, turbine)
end
-- link a boiler RTU session
---@param boiler unit_session
public.add_boiler = function (boiler)
table.insert(self.boilers, boiler)
end
-- link a redstone RTU capability
public.add_redstone = function (field, accessor)
-- ensure field exists
if self.redstone[field] == nil then
self.redstone[field] = {}
end
-- insert into list
table.insert(self.redstone[field], accessor)
end
-- update (iterate) this session
public.update = function ()
-- unlink PLC if session was closed
if not self.plc_s.open then
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
-- get the annunciator status
public.get_annunciator = function () return self.db.annunciator end
return public
end
return unit