mirror of
https://github.com/MikaylaFischler/cc-mek-scada.git
synced 2024-08-30 18:22:34 +00:00
579 lines
18 KiB
Lua
579 lines
18 KiB
Lua
--
|
|
-- I/O Control for Pocket Integration with Supervisor & Coordinator
|
|
--
|
|
|
|
local log = require("scada-common.log")
|
|
local psil = require("scada-common.psil")
|
|
local types = require("scada-common.types")
|
|
local util = require("scada-common.util")
|
|
|
|
local ALARM = types.ALARM
|
|
local ALARM_STATE = types.ALARM_STATE
|
|
|
|
---@todo nominal trip time is ping (0ms to 10ms usually)
|
|
local WARN_TT = 40
|
|
local HIGH_TT = 80
|
|
|
|
local iocontrol = {}
|
|
|
|
---@enum POCKET_LINK_STATE
|
|
local LINK_STATE = {
|
|
UNLINKED = 0,
|
|
SV_LINK_ONLY = 1,
|
|
API_LINK_ONLY = 2,
|
|
LINKED = 3
|
|
}
|
|
|
|
iocontrol.LINK_STATE = LINK_STATE
|
|
|
|
---@enum POCKET_APP_ID
|
|
local APP_ID = {
|
|
ROOT = 1,
|
|
-- main app page
|
|
UNITS = 2,
|
|
ABOUT = 3,
|
|
-- diag app page
|
|
ALARMS = 4,
|
|
-- other
|
|
DUMMY = 5,
|
|
NUM_APPS = 5
|
|
}
|
|
|
|
iocontrol.APP_ID = APP_ID
|
|
|
|
---@class pocket_ioctl
|
|
local io = {
|
|
version = "unknown",
|
|
ps = psil.create()
|
|
}
|
|
|
|
---@class nav_tree_page
|
|
---@field _p nav_tree_page|nil page's parent
|
|
---@field _c table page's children
|
|
---@field nav_to function function to navigate to this page
|
|
---@field switcher function|nil function to switch between children
|
|
---@field tasks table tasks to run while viewing this page
|
|
|
|
-- allocate the page navigation system
|
|
function iocontrol.alloc_nav()
|
|
local self = {
|
|
pane = nil, ---@type graphics_element
|
|
apps = {},
|
|
containers = {},
|
|
cur_app = APP_ID.ROOT
|
|
}
|
|
|
|
self.cur_page = self.root
|
|
|
|
---@class pocket_nav
|
|
io.nav = {}
|
|
|
|
-- set the root pane element to switch between apps with
|
|
---@param root_pane graphics_element
|
|
function io.nav.set_pane(root_pane)
|
|
self.pane = root_pane
|
|
end
|
|
|
|
function io.nav.set_sidebar(sidebar)
|
|
self.sidebar = sidebar
|
|
end
|
|
|
|
-- register an app
|
|
---@param app_id POCKET_APP_ID app ID
|
|
---@param container graphics_element element that contains this app (usually a Div)
|
|
---@param pane graphics_element? multipane if this is a simple paned app, then nav_to must be a number
|
|
function io.nav.register_app(app_id, container, pane)
|
|
---@class pocket_app
|
|
local app = {
|
|
loaded = false,
|
|
load = nil,
|
|
root = { _p = nil, _c = {}, nav_to = function () end, tasks = {} }, ---@type nav_tree_page
|
|
cur_page = nil, ---@type nav_tree_page
|
|
pane = pane,
|
|
paned_pages = {},
|
|
sidebar_items = {}
|
|
}
|
|
|
|
app.load = function () app.loaded = true end
|
|
|
|
-- delayed set of the pane if it wasn't ready at the start
|
|
---@param root_pane graphics_element multipane
|
|
function app.set_root_pane(root_pane)
|
|
app.pane = root_pane
|
|
end
|
|
|
|
function app.set_sidebar(items)
|
|
app.sidebar_items = items
|
|
if self.sidebar then self.sidebar.update(items) end
|
|
end
|
|
|
|
-- function to run on initial load into memory
|
|
---@param on_load function callback
|
|
function app.set_on_load(on_load)
|
|
app.load = function ()
|
|
on_load()
|
|
app.loaded = true
|
|
end
|
|
end
|
|
|
|
-- if a pane was provided, this will switch between numbered pages
|
|
---@param idx integer page index
|
|
function app.switcher(idx)
|
|
if app.paned_pages[idx] then
|
|
app.paned_pages[idx].nav_to()
|
|
end
|
|
end
|
|
|
|
-- create a new page entry in the app's page navigation tree
|
|
---@param parent nav_tree_page? a parent page or nil to set this as the root
|
|
---@param nav_to function|integer function to navigate to this page or pane index
|
|
---@return nav_tree_page new_page this new page
|
|
function app.new_page(parent, nav_to)
|
|
---@type nav_tree_page
|
|
local page = { _p = parent, _c = {}, nav_to = function () end, switcher = function () end, tasks = {} }
|
|
|
|
if parent == nil then
|
|
app.root = page
|
|
if app.cur_page == nil then app.cur_page = page end
|
|
end
|
|
|
|
if type(nav_to) == "number" then
|
|
app.paned_pages[nav_to] = page
|
|
|
|
function page.nav_to()
|
|
app.cur_page = page
|
|
if app.pane then app.pane.set_value(nav_to) end
|
|
end
|
|
else
|
|
function page.nav_to()
|
|
app.cur_page = page
|
|
nav_to()
|
|
end
|
|
end
|
|
|
|
-- switch between children
|
|
---@param id integer child ID
|
|
function page.switcher(id) if page._c[id] then page._c[id].nav_to() end end
|
|
|
|
if parent ~= nil then
|
|
table.insert(page._p._c, page)
|
|
end
|
|
|
|
return page
|
|
end
|
|
|
|
-- get the currently active page
|
|
function app.get_current_page() return app.cur_page end
|
|
|
|
-- attempt to navigate up the tree
|
|
---@return boolean success true if successfully navigated up
|
|
function app.nav_up()
|
|
local parent = app.cur_page._p
|
|
if parent then parent.nav_to() end
|
|
return parent ~= nil
|
|
end
|
|
|
|
self.apps[app_id] = app
|
|
self.containers[app_id] = container
|
|
|
|
return app
|
|
end
|
|
|
|
-- get a list of the app containers (usually Div elements)
|
|
function io.nav.get_containers() return self.containers end
|
|
|
|
-- open a given app
|
|
---@param app_id POCKET_APP_ID
|
|
function io.nav.open_app(app_id)
|
|
local app = self.apps[app_id] ---@type pocket_app
|
|
if app then
|
|
if not app.loaded then app.load() end
|
|
|
|
self.cur_app = app_id
|
|
self.pane.set_value(app_id)
|
|
|
|
if #app.sidebar_items > 0 then
|
|
self.sidebar.update(app.sidebar_items)
|
|
end
|
|
else
|
|
log.debug("tried to open unknown app")
|
|
end
|
|
end
|
|
|
|
-- get the currently active page
|
|
---@return nav_tree_page
|
|
function io.nav.get_current_page()
|
|
return self.apps[self.cur_app].get_current_page()
|
|
end
|
|
|
|
-- attempt to navigate up
|
|
function io.nav.nav_up()
|
|
local app = self.apps[self.cur_app] ---@type pocket_app
|
|
log.debug("attempting app nav up for app " .. self.cur_app)
|
|
|
|
if not app.nav_up() then
|
|
log.debug("internal app nav up failed, going to home screen")
|
|
io.nav.open_app(APP_ID.ROOT)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- initialize facility-independent components of pocket iocontrol
|
|
---@param comms pocket_comms
|
|
function iocontrol.init_core(comms)
|
|
iocontrol.alloc_nav()
|
|
|
|
---@class pocket_ioctl_diag
|
|
io.diag = {}
|
|
|
|
-- alarm testing
|
|
io.diag.tone_test = {
|
|
test_1 = function (state) comms.diag__set_alarm_tone(1, state) end,
|
|
test_2 = function (state) comms.diag__set_alarm_tone(2, state) end,
|
|
test_3 = function (state) comms.diag__set_alarm_tone(3, state) end,
|
|
test_4 = function (state) comms.diag__set_alarm_tone(4, state) end,
|
|
test_5 = function (state) comms.diag__set_alarm_tone(5, state) end,
|
|
test_6 = function (state) comms.diag__set_alarm_tone(6, state) end,
|
|
test_7 = function (state) comms.diag__set_alarm_tone(7, state) end,
|
|
test_8 = function (state) comms.diag__set_alarm_tone(8, state) end,
|
|
stop_tones = function () comms.diag__set_alarm_tone(0, false) end,
|
|
|
|
test_breach = function (state) comms.diag__set_alarm(ALARM.ContainmentBreach, state) end,
|
|
test_rad = function (state) comms.diag__set_alarm(ALARM.ContainmentRadiation, state) end,
|
|
test_lost = function (state) comms.diag__set_alarm(ALARM.ReactorLost, state) end,
|
|
test_crit = function (state) comms.diag__set_alarm(ALARM.CriticalDamage, state) end,
|
|
test_dmg = function (state) comms.diag__set_alarm(ALARM.ReactorDamage, state) end,
|
|
test_overtemp = function (state) comms.diag__set_alarm(ALARM.ReactorOverTemp, state) end,
|
|
test_hightemp = function (state) comms.diag__set_alarm(ALARM.ReactorHighTemp, state) end,
|
|
test_wasteleak = function (state) comms.diag__set_alarm(ALARM.ReactorWasteLeak, state) end,
|
|
test_highwaste = function (state) comms.diag__set_alarm(ALARM.ReactorHighWaste, state) end,
|
|
test_rps = function (state) comms.diag__set_alarm(ALARM.RPSTransient, state) end,
|
|
test_rcs = function (state) comms.diag__set_alarm(ALARM.RCSTransient, state) end,
|
|
test_turbinet = function (state) comms.diag__set_alarm(ALARM.TurbineTrip, state) end,
|
|
stop_alarms = function () comms.diag__set_alarm(0, false) end,
|
|
|
|
get_tone_states = function () comms.diag__get_alarm_tones() end,
|
|
|
|
ready_warn = nil, ---@type graphics_element
|
|
tone_buttons = {},
|
|
alarm_buttons = {},
|
|
tone_indicators = {} -- indicators to update from supervisor tone states
|
|
}
|
|
end
|
|
|
|
-- initialize facility-dependent components of pocket iocontrol
|
|
---@param conf facility_conf configuration
|
|
---@param temp_scale 1|2|3|4 temperature unit (1 = K, 2 = C, 3 = F, 4 = R)
|
|
function iocontrol.init_fac(conf, temp_scale)
|
|
-- temperature unit label and conversion function (from Kelvin)
|
|
if temp_scale == 2 then
|
|
io.temp_label = "\xb0C"
|
|
io.temp_convert = function (t) return t - 273.15 end
|
|
elseif temp_scale == 3 then
|
|
io.temp_label = "\xb0F"
|
|
io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end
|
|
elseif temp_scale == 4 then
|
|
io.temp_label = "\xb0R"
|
|
io.temp_convert = function (t) return 1.8 * t end
|
|
else
|
|
io.temp_label = "K"
|
|
io.temp_convert = function (t) return t end
|
|
end
|
|
|
|
-- facility data structure
|
|
---@class pioctl_facility
|
|
io.facility = {
|
|
num_units = conf.num_units,
|
|
tank_mode = conf.cooling.fac_tank_mode,
|
|
tank_defs = conf.cooling.fac_tank_defs,
|
|
all_sys_ok = false,
|
|
rtu_count = 0,
|
|
|
|
auto_ready = false,
|
|
auto_active = false,
|
|
auto_ramping = false,
|
|
auto_saturated = false,
|
|
|
|
auto_scram = false,
|
|
---@type ascram_status
|
|
ascram_status = {
|
|
matrix_dc = false,
|
|
matrix_fill = false,
|
|
crit_alarm = false,
|
|
radiation = false,
|
|
gen_fault = false
|
|
},
|
|
|
|
---@type WASTE_PRODUCT
|
|
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
|
|
auto_pu_fallback_active = false,
|
|
|
|
radiation = types.new_zero_radiation_reading(),
|
|
|
|
ps = psil.create(),
|
|
|
|
induction_ps_tbl = {},
|
|
induction_data_tbl = {},
|
|
|
|
sps_ps_tbl = {},
|
|
sps_data_tbl = {},
|
|
|
|
tank_ps_tbl = {},
|
|
tank_data_tbl = {},
|
|
|
|
env_d_ps = psil.create(),
|
|
env_d_data = {}
|
|
}
|
|
|
|
-- create induction and SPS tables (currently only 1 of each is supported)
|
|
table.insert(io.facility.induction_ps_tbl, psil.create())
|
|
table.insert(io.facility.induction_data_tbl, {})
|
|
table.insert(io.facility.sps_ps_tbl, psil.create())
|
|
table.insert(io.facility.sps_data_tbl, {})
|
|
|
|
-- determine tank information
|
|
if io.facility.tank_mode == 0 then
|
|
io.facility.tank_defs = {}
|
|
-- on facility tank mode 0, setup tank defs to match unit tank option
|
|
for i = 1, conf.num_units do
|
|
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
|
|
end
|
|
|
|
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
|
|
else
|
|
-- decode the layout of tanks from the connections definitions
|
|
local tank_mode = io.facility.tank_mode
|
|
local tank_defs = io.facility.tank_defs
|
|
local tank_list = { table.unpack(tank_defs) }
|
|
|
|
local function calc_fdef(start_idx, end_idx)
|
|
local first = 4
|
|
for i = start_idx, end_idx do
|
|
if io.facility.tank_defs[i] == 2 then
|
|
if i < first then first = i end
|
|
end
|
|
end
|
|
return first
|
|
end
|
|
|
|
if tank_mode == 1 then
|
|
-- (1) 1 total facility tank (A A A A)
|
|
local first_fdef = calc_fdef(1, #tank_defs)
|
|
for i = 1, #tank_defs do
|
|
if i > first_fdef and tank_defs[i] == 2 then
|
|
tank_list[i] = 0
|
|
end
|
|
end
|
|
elseif tank_mode == 2 then
|
|
-- (2) 2 total facility tanks (A A A B)
|
|
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
|
|
for i = 1, #tank_defs do
|
|
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
|
|
tank_list[i] = 0
|
|
end
|
|
end
|
|
elseif tank_mode == 3 then
|
|
-- (3) 2 total facility tanks (A A B B)
|
|
for _, a in pairs({ 1, 3 }) do
|
|
local b = a + 1
|
|
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
|
|
tank_list[b] = 0
|
|
end
|
|
end
|
|
elseif tank_mode == 4 then
|
|
-- (4) 2 total facility tanks (A B B B)
|
|
local first_fdef = calc_fdef(2, #tank_defs)
|
|
for i = 1, #tank_defs do
|
|
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
|
|
tank_list[i] = 0
|
|
end
|
|
end
|
|
elseif tank_mode == 5 then
|
|
-- (5) 3 total facility tanks (A A B C)
|
|
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
|
|
for i = 1, #tank_defs do
|
|
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
|
|
tank_list[i] = 0
|
|
end
|
|
end
|
|
elseif tank_mode == 6 then
|
|
-- (6) 3 total facility tanks (A B B C)
|
|
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
|
|
for i = 1, #tank_defs do
|
|
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
|
|
tank_list[i] = 0
|
|
end
|
|
end
|
|
elseif tank_mode == 7 then
|
|
-- (7) 3 total facility tanks (A B C C)
|
|
local first_fdef = calc_fdef(3, #tank_defs)
|
|
for i = 1, #tank_defs do
|
|
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
|
|
tank_list[i] = 0
|
|
end
|
|
end
|
|
end
|
|
|
|
io.facility.tank_list = tank_list
|
|
end
|
|
|
|
-- create facility tank tables
|
|
for i = 1, #io.facility.tank_list do
|
|
if io.facility.tank_list[i] == 2 then
|
|
table.insert(io.facility.tank_ps_tbl, psil.create())
|
|
table.insert(io.facility.tank_data_tbl, {})
|
|
end
|
|
end
|
|
|
|
-- create unit data structures
|
|
io.units = {}
|
|
for i = 1, conf.num_units do
|
|
---@class pioctl_unit
|
|
local entry = {
|
|
unit_id = i,
|
|
|
|
num_boilers = 0,
|
|
num_turbines = 0,
|
|
num_snas = 0,
|
|
has_tank = conf.cooling.r_cool[i].TankConnection,
|
|
|
|
control_state = false,
|
|
burn_rate_cmd = 0.0,
|
|
radiation = types.new_zero_radiation_reading(),
|
|
|
|
sna_peak_rate = 0.0,
|
|
sna_max_rate = 0.0,
|
|
sna_out_rate = 0.0,
|
|
|
|
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
|
|
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
|
|
|
|
-- auto control group
|
|
a_group = 0,
|
|
|
|
---@type alarms
|
|
alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE },
|
|
|
|
annunciator = {}, ---@type annunciator
|
|
|
|
unit_ps = psil.create(),
|
|
reactor_data = {}, ---@type reactor_db
|
|
|
|
boiler_ps_tbl = {},
|
|
boiler_data_tbl = {},
|
|
|
|
turbine_ps_tbl = {},
|
|
turbine_data_tbl = {},
|
|
|
|
tank_ps_tbl = {},
|
|
tank_data_tbl = {}
|
|
}
|
|
|
|
-- on other facility modes, overwrite unit TANK option with facility tank defs
|
|
if io.facility.tank_mode ~= 0 then
|
|
entry.has_tank = conf.cooling.fac_tank_defs[i] > 0
|
|
end
|
|
|
|
-- create boiler tables
|
|
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
|
|
table.insert(entry.boiler_ps_tbl, psil.create())
|
|
table.insert(entry.boiler_data_tbl, {})
|
|
end
|
|
|
|
-- create turbine tables
|
|
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
|
|
table.insert(entry.turbine_ps_tbl, psil.create())
|
|
table.insert(entry.turbine_data_tbl, {})
|
|
end
|
|
|
|
-- create tank tables
|
|
if io.facility.tank_defs[i] == 1 then
|
|
table.insert(entry.tank_ps_tbl, psil.create())
|
|
table.insert(entry.tank_data_tbl, {})
|
|
end
|
|
|
|
entry.num_boilers = #entry.boiler_data_tbl
|
|
entry.num_turbines = #entry.turbine_data_tbl
|
|
|
|
table.insert(io.units, entry)
|
|
end
|
|
end
|
|
|
|
-- set network link state
|
|
---@param state POCKET_LINK_STATE
|
|
function iocontrol.report_link_state(state)
|
|
io.ps.publish("link_state", state)
|
|
|
|
if state == LINK_STATE.API_LINK_ONLY or state == LINK_STATE.UNLINKED then
|
|
io.ps.publish("svr_conn_quality", 0)
|
|
end
|
|
|
|
if state == LINK_STATE.SV_LINK_ONLY or state == LINK_STATE.UNLINKED then
|
|
io.ps.publish("crd_conn_quality", 0)
|
|
end
|
|
end
|
|
|
|
-- determine supervisor connection quality (trip time)
|
|
---@param trip_time integer
|
|
function iocontrol.report_svr_tt(trip_time)
|
|
local state = 3
|
|
if trip_time > HIGH_TT then
|
|
state = 1
|
|
elseif trip_time > WARN_TT then
|
|
state = 2
|
|
end
|
|
|
|
io.ps.publish("svr_conn_quality", state)
|
|
end
|
|
|
|
-- determine coordinator connection quality (trip time)
|
|
---@param trip_time integer
|
|
function iocontrol.report_crd_tt(trip_time)
|
|
local state = 3
|
|
if trip_time > HIGH_TT then
|
|
state = 1
|
|
elseif trip_time > WARN_TT then
|
|
state = 2
|
|
end
|
|
|
|
io.ps.publish("crd_conn_quality", state)
|
|
end
|
|
|
|
-- populate facility data from API_GET_FAC
|
|
---@param data table
|
|
---@return boolean valid
|
|
function iocontrol.record_facility_data(data)
|
|
local valid = true
|
|
|
|
local fac = io.facility
|
|
|
|
fac.all_sys_ok = data[1]
|
|
fac.rtu_count = data[2]
|
|
fac.radiation = data[3]
|
|
|
|
-- auto control
|
|
if type(data[4]) == "table" and #data[4] == 4 then
|
|
fac.auto_ready = data[4][1]
|
|
fac.auto_active = data[4][2]
|
|
fac.auto_ramping = data[4][3]
|
|
fac.auto_saturated = data[4][4]
|
|
end
|
|
|
|
-- waste
|
|
if type(data[5]) == "table" and #data[5] == 2 then
|
|
fac.auto_current_waste_product = data[5][1]
|
|
fac.auto_pu_fallback_active = data[5][2]
|
|
end
|
|
|
|
fac.num_tanks = data[6]
|
|
fac.has_imatrix = data[7]
|
|
fac.has_sps = data[8]
|
|
|
|
return valid
|
|
end
|
|
|
|
-- get the IO controller database
|
|
function iocontrol.get_db() return io end
|
|
|
|
return iocontrol
|