cc-mek-scada/coordinator/iocontrol.lua

620 lines
26 KiB
Lua
Raw Normal View History

2022-12-04 18:59:10 +00:00
local comms = require("scada-common.comms")
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 process = require("coordinator.process")
2022-12-04 18:59:10 +00:00
local sounder = require("coordinator.sounder")
local UNIT_COMMANDS = comms.UNIT_COMMANDS
2022-07-06 03:48:01 +00:00
2022-12-04 18:59:10 +00:00
local ALARM_STATE = types.ALARM_STATE
local iocontrol = {}
2022-07-06 03:48:01 +00:00
---@class ioctl
local io = {}
2022-07-06 03:48:01 +00:00
-- initialize the coordinator IO controller
2022-07-06 03:48:01 +00:00
---@param conf facility_conf configuration
---@param comms coord_comms comms reference
---@diagnostic disable-next-line: redefined-local
function iocontrol.init(conf, comms)
---@class ioctl_facility
io.facility = {
auto_active = false,
2022-07-06 03:48:01 +00:00
scram = false,
num_units = conf.num_units, ---@type integer
ps = psil.create(),
induction_ps_tbl = {},
induction_data_tbl = {},
env_d_ps = psil.create(),
env_d_data = {}
2022-07-06 03:48:01 +00:00
}
-- create induction tables (max 1 per unit, preferably 1 total)
for _ = 1, conf.num_units do
local data = {} ---@type imatrix_session_db
table.insert(io.facility.induction_ps_tbl, psil.create())
table.insert(io.facility.induction_data_tbl, data)
end
io.units = {}
2022-07-06 03:48:01 +00:00
for i = 1, conf.num_units do
local function ack(alarm) process.ack_alarm(i, alarm) end
local function reset(alarm) process.reset_alarm(i, alarm) end
---@class ioctl_unit
2022-07-06 03:48:01 +00:00
local entry = {
unit_id = i, ---@type integer
2022-07-06 03:48:01 +00:00
num_boilers = 0,
num_turbines = 0,
control_state = false,
burn_rate_cmd = 0.0,
waste_control = 0,
a_group = 0, -- auto control group
start = function () process.start(i) end,
scram = function () process.scram(i) end,
reset_rps = function () process.reset_rps(i) end,
ack_alarms = function () process.ack_all_alarms(i) end,
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
set_waste = function (mode) process.set_waste(i, mode) end, ---@param mode integer waste processing mode
2023-01-13 19:03:47 +00:00
set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0
set_limit = function (lim) process.set_limit(i, lim) end, ---@param lim number burn rate limit
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 },
radiation = { ack = function () ack(2) end, reset = function () reset(2) end },
r_lost = { ack = function () ack(3) end, reset = function () reset(3) end },
dmg_crit = { ack = function () ack(4) end, reset = function () reset(4) end },
damage = { ack = function () ack(5) end, reset = function () reset(5) end },
over_temp = { ack = function () ack(6) end, reset = function () reset(6) end },
high_temp = { ack = function () ack(7) end, reset = function () reset(7) end },
waste_leak = { ack = function () ack(8) end, reset = function () reset(8) end },
waste_high = { ack = function () ack(9) end, reset = function () reset(9) end },
rps_trans = { ack = function () ack(10) end, reset = function () reset(10) end },
rcs_trans = { ack = function () ack(11) end, reset = function () reset(11) end },
t_trip = { ack = function () ack(12) end, reset = function () reset(12) end }
},
2022-12-04 18:59:10 +00:00
---@type alarms
alarms = {
ALARM_STATE.INACTIVE, -- containment breach
ALARM_STATE.INACTIVE, -- containment radiation
ALARM_STATE.INACTIVE, -- reactor lost
ALARM_STATE.INACTIVE, -- damage critical
ALARM_STATE.INACTIVE, -- reactor taking damage
ALARM_STATE.INACTIVE, -- reactor over temperature
ALARM_STATE.INACTIVE, -- reactor high temperature
ALARM_STATE.INACTIVE, -- waste leak
ALARM_STATE.INACTIVE, -- waste level high
ALARM_STATE.INACTIVE, -- RPS transient
ALARM_STATE.INACTIVE, -- RCS transient
ALARM_STATE.INACTIVE -- turbine trip
2022-12-04 18:59:10 +00:00
},
2022-07-06 03:48:01 +00:00
reactor_ps = psil.create(),
reactor_data = {}, ---@type reactor_db
2022-07-06 03:48:01 +00:00
boiler_ps_tbl = {},
boiler_data_tbl = {},
turbine_ps_tbl = {},
turbine_data_tbl = {}
}
-- create boiler tables
2022-07-06 03:48:01 +00:00
for _ = 1, conf.defs[(i * 2) - 1] do
local data = {} ---@type boilerv_session_db
2022-07-06 03:48:01 +00:00
table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, data)
end
-- create turbine tables
2022-07-06 03:48:01 +00:00
for _ = 1, conf.defs[i * 2] do
local data = {} ---@type turbinev_session_db
2022-07-06 03:48:01 +00:00
table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, data)
end
entry.num_boilers = #entry.boiler_data_tbl
entry.num_turbines = #entry.turbine_data_tbl
table.insert(io.units, entry)
2022-07-06 03:48:01 +00:00
end
-- pass IO control here since it can't be require'd due to a require loop
process.init(io, comms)
2022-07-06 03:48:01 +00:00
end
-- populate facility structure builds
---@param build table
---@return boolean valid
function iocontrol.record_facility_builds(build)
if type(build) == "table" then
local fac = io.facility
-- induction matricies
if type(build.induction) == "table" then
for id, matrix in pairs(build.induction) do
if type(fac.induction_data_tbl[id]) == "table" then
fac.induction_data_tbl[id].formed = matrix[1] ---@type boolean
fac.induction_data_tbl[id].build = matrix[2] ---@type table
fac.induction_ps_tbl[id].publish("formed", matrix[1])
for key, val in pairs(fac.induction_data_tbl[id].build) do
fac.induction_ps_tbl[id].publish(key, val)
end
else
log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id))
2022-10-03 01:17:13 +00:00
end
end
end
else
log.error("facility builds not a table")
return false
end
return true
end
-- populate unit structure builds
---@param builds table
---@return boolean valid
function iocontrol.record_unit_builds(builds)
-- note: if not all units and RTUs are connected, some will be nil
for id, build in pairs(builds) do
local unit = io.units[id] ---@type ioctl_unit
if type(build) ~= "table" then
log.error(util.c("corrupted unit builds provided, unit ", id, " not a table"))
return false
elseif type(unit) ~= "table" then
log.error(util.c("corrupted unit builds provided, invalid unit ", id))
return false
end
local log_header = util.c("iocontrol.record_unit_builds[unit ", id, "]: ")
2022-10-03 01:17:13 +00:00
-- reactor build
if type(build.reactor) == "table" then
unit.reactor_data.mek_struct = build.reactor
for key, val in pairs(unit.reactor_data.mek_struct) do
unit.reactor_ps.publish(key, val)
end
if (type(unit.reactor_data.mek_struct.length) == "number") and (unit.reactor_data.mek_struct.length ~= 0) and
(type(unit.reactor_data.mek_struct.width) == "number") and (unit.reactor_data.mek_struct.width ~= 0) then
unit.reactor_ps.publish("size", { unit.reactor_data.mek_struct.length, unit.reactor_data.mek_struct.width })
end
end
-- boiler builds
if type(build.boilers) == "table" then
for b_id, boiler in pairs(build.boilers) do
if type(unit.boiler_data_tbl[b_id]) == "table" then
unit.boiler_data_tbl[b_id].formed = boiler[1] ---@type boolean
unit.boiler_data_tbl[b_id].build = boiler[2] ---@type table
unit.boiler_ps_tbl[b_id].publish("formed", boiler[1])
for key, val in pairs(unit.boiler_data_tbl[b_id].build) do
unit.boiler_ps_tbl[b_id].publish(key, val)
end
else
log.debug(util.c(log_header, "invalid boiler id ", b_id))
2022-10-03 01:17:13 +00:00
end
end
end
-- turbine builds
if type(build.turbines) == "table" then
for t_id, turbine in pairs(build.turbines) do
if type(unit.turbine_data_tbl[t_id]) == "table" then
unit.turbine_data_tbl[t_id].formed = turbine[1] ---@type boolean
unit.turbine_data_tbl[t_id].build = turbine[2] ---@type table
2022-10-03 01:17:13 +00:00
unit.turbine_ps_tbl[t_id].publish("formed", turbine[1])
2022-10-03 01:17:13 +00:00
for key, val in pairs(unit.turbine_data_tbl[t_id].build) do
unit.turbine_ps_tbl[t_id].publish(key, val)
2022-10-03 01:17:13 +00:00
end
else
log.debug(util.c(log_header, "invalid turbine id ", t_id))
end
end
end
end
return true
end
2022-10-03 01:17:13 +00:00
-- update facility status
---@param status table
---@return boolean valid
function iocontrol.update_facility_status(status)
local log_header = util.c("iocontrol.update_facility_status: ")
if type(status) ~= "table" then
log.debug(log_header .. "status not a table")
return false
else
local fac = io.facility
2022-10-03 01:17:13 +00:00
-- RTU statuses
local rtu_statuses = status[1]
if type(rtu_statuses) == "table" then
-- induction matricies statuses
if type(rtu_statuses.induction) == "table" then
for id = 1, #fac.induction_ps_tbl do
if rtu_statuses.induction[id] == nil then
-- disconnected
fac.induction_ps_tbl[id].publish("computed_status", 1)
2022-10-03 01:17:13 +00:00
end
end
for id, matrix in pairs(rtu_statuses.induction) do
if type(fac.induction_data_tbl[id]) == "table" then
local rtu_faulted = matrix[1] ---@type boolean
fac.induction_data_tbl[id].formed = matrix[2] ---@type boolean
fac.induction_data_tbl[id].state = matrix[3] ---@type table
fac.induction_data_tbl[id].tanks = matrix[4] ---@type table
local data = fac.induction_data_tbl[id] ---@type imatrix_session_db
fac.induction_ps_tbl[id].publish("formed", data.formed)
fac.induction_ps_tbl[id].publish("faulted", rtu_faulted)
if data.formed then
if rtu_faulted then
fac.induction_ps_tbl[id].publish("computed_status", 3) -- faulted
elseif data.tanks.energy_fill >= 0.99 then
fac.induction_ps_tbl[id].publish("computed_status", 6) -- full
elseif data.tanks.energy_fill <= 0.01 then
fac.induction_ps_tbl[id].publish("computed_status", 5) -- empty
else
fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line
end
else
fac.induction_ps_tbl[id].publish("computed_status", 2) -- not formed
end
for key, val in pairs(fac.induction_data_tbl[id].state) do
fac.induction_ps_tbl[id].publish(key, val)
end
for key, val in pairs(fac.induction_data_tbl[id].tanks) do
fac.induction_ps_tbl[id].publish(key, val)
end
else
log.debug(util.c(log_header, "invalid induction matrix id ", id))
end
end
else
log.debug(log_header .. "induction matrix list not a table")
end
end
end
return true
end
-- update unit statuses
---@param statuses table
---@return boolean valid
function iocontrol.update_unit_statuses(statuses)
if type(statuses) ~= "table" then
log.debug("iocontrol.update_unit_statuses: unit statuses not a table")
return false
elseif #statuses ~= #io.units then
log.debug("iocontrol.update_unit_statuses: number of provided unit statuses does not match expected number of units")
return false
else
-- get all unit statuses
for i = 1, #statuses do
local log_header = util.c("iocontrol.update_unit_statuses[unit ", i, "]: ")
local unit = io.units[i] ---@type ioctl_unit
local status = statuses[i]
if type(status) ~= "table" or #status ~= 6 then
log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)")
return false
end
2022-09-03 17:10:51 +00:00
-- reactor PLC status
local reactor_status = status[1]
2022-09-03 17:10:51 +00:00
if type(reactor_status) ~= "table" then
reactor_status = {}
log.debug(log_header .. "reactor status not a table")
end
2022-09-03 17:10:51 +00:00
if #reactor_status == 0 then
unit.reactor_ps.publish("computed_status", 1) -- disconnected
elseif #reactor_status == 3 then
2022-09-03 17:10:51 +00:00
local mek_status = reactor_status[1]
local rps_status = reactor_status[2]
local gen_status = reactor_status[3]
if #gen_status == 6 then
unit.reactor_data.last_status_update = gen_status[1]
unit.reactor_data.control_state = gen_status[2]
unit.reactor_data.rps_tripped = gen_status[3]
unit.reactor_data.rps_trip_cause = gen_status[4]
unit.reactor_data.no_reactor = gen_status[5]
unit.reactor_data.formed = gen_status[6]
else
log.debug(log_header .. "reactor general status length mismatch")
end
2022-09-03 17:10:51 +00:00
unit.reactor_data.rps_status = rps_status ---@type rps_status
unit.reactor_data.mek_status = mek_status ---@type mek_status
if unit.reactor_data.mek_status.status then
unit.reactor_ps.publish("computed_status", 5) -- running
2022-09-03 17:10:51 +00:00
else
if unit.reactor_data.no_reactor then
unit.reactor_ps.publish("computed_status", 3) -- faulted
elseif not unit.reactor_data.formed then
unit.reactor_ps.publish("computed_status", 2) -- multiblock not formed
2022-11-11 20:45:46 +00:00
elseif unit.reactor_data.rps_status.force_dis then
unit.reactor_ps.publish("computed_status", 7) -- reactor force disabled
2022-09-03 17:10:51 +00:00
elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then
unit.reactor_ps.publish("computed_status", 6) -- SCRAM
2022-09-03 17:10:51 +00:00
else
unit.reactor_ps.publish("computed_status", 4) -- disabled
2022-09-03 17:10:51 +00:00
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
2022-09-03 17:10:51 +00:00
unit.reactor_ps.publish(key, val)
end
end
if type(unit.reactor_data.rps_status) == "table" then
for key, val in pairs(unit.reactor_data.rps_status) do
unit.reactor_ps.publish(key, val)
end
end
if type(unit.reactor_data.mek_status) == "table" then
for key, val in pairs(unit.reactor_data.mek_status) do
unit.reactor_ps.publish(key, val)
end
end
else
log.debug(log_header .. "reactor status length mismatch")
end
2022-09-03 17:10:51 +00:00
-- RTU statuses
local rtu_statuses = status[2]
2022-09-03 17:10:51 +00:00
if type(rtu_statuses) == "table" then
-- boiler statuses
if type(rtu_statuses.boilers) == "table" then
for id = 1, #unit.boiler_ps_tbl do
if rtu_statuses.boilers[i] == nil then
-- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1)
end
end
2022-09-03 17:10:51 +00:00
for id, boiler in pairs(rtu_statuses.boilers) do
if type(unit.boiler_data_tbl[id]) == "table" then
local rtu_faulted = boiler[1] ---@type boolean
unit.boiler_data_tbl[id].formed = boiler[2] ---@type boolean
unit.boiler_data_tbl[id].state = boiler[3] ---@type table
unit.boiler_data_tbl[id].tanks = boiler[4] ---@type table
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
unit.boiler_ps_tbl[id].publish("formed", data.formed)
unit.boiler_ps_tbl[id].publish("faulted", rtu_faulted)
if data.formed then
if rtu_faulted then
unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted
elseif data.state.boil_rate > 0 then
unit.boiler_ps_tbl[id].publish("computed_status", 5) -- active
else
unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle
end
else
unit.boiler_ps_tbl[id].publish("computed_status", 2) -- not formed
end
2022-09-03 17:10:51 +00:00
for key, val in pairs(unit.boiler_data_tbl[id].state) do
unit.boiler_ps_tbl[id].publish(key, val)
end
2022-09-03 17:10:51 +00:00
for key, val in pairs(unit.boiler_data_tbl[id].tanks) do
unit.boiler_ps_tbl[id].publish(key, val)
end
else
log.debug(util.c(log_header, "invalid boiler id ", id))
end
end
2022-12-04 18:59:10 +00:00
else
log.debug(log_header .. "boiler list not a table")
2022-09-03 17:10:51 +00:00
end
-- turbine statuses
if type(rtu_statuses.turbines) == "table" then
for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[i] == nil then
-- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1)
end
end
2022-09-03 17:10:51 +00:00
for id, turbine in pairs(rtu_statuses.turbines) do
if type(unit.turbine_data_tbl[id]) == "table" then
local rtu_faulted = turbine[1] ---@type boolean
unit.turbine_data_tbl[id].formed = turbine[2] ---@type boolean
unit.turbine_data_tbl[id].state = turbine[3] ---@type table
unit.turbine_data_tbl[id].tanks = turbine[4] ---@type table
local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
unit.turbine_ps_tbl[id].publish("formed", data.formed)
unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted)
if data.formed then
if data.tanks.energy_fill >= 0.99 then
unit.turbine_ps_tbl[id].publish("computed_status", 6) -- trip
elseif rtu_faulted then
unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted
elseif data.state.flow_rate < 100 then
unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle
else
unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active
end
else
unit.turbine_ps_tbl[id].publish("computed_status", 2) -- not formed
end
2022-09-03 17:10:51 +00:00
for key, val in pairs(unit.turbine_data_tbl[id].state) do
unit.turbine_ps_tbl[id].publish(key, val)
end
2022-09-03 17:10:51 +00:00
for key, val in pairs(unit.turbine_data_tbl[id].tanks) do
unit.turbine_ps_tbl[id].publish(key, val)
end
else
log.debug(util.c(log_header, "invalid turbine id ", id))
end
end
2022-12-04 18:59:10 +00:00
else
log.debug(log_header .. "turbine list not a table")
2022-12-04 18:59:10 +00:00
return false
2022-09-03 17:10:51 +00:00
end
2022-12-04 18:59:10 +00:00
else
log.debug(log_header .. "rtu list not a table")
end
-- annunciator
local annunciator = status[3] ---@type annunciator
if type(annunciator) ~= "table" then
annunciator = {}
log.debug(log_header .. "annunciator state not a table")
end
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")
2022-09-03 17:10:51 +00:00
end
-- auto control state fields
local auto_ctl_state = status[6]
if type(auto_ctl_state) == "table" then
if #auto_ctl_state == 1 then
unit.reactor_ps.publish("burn_limit", auto_ctl_state[1])
else
log.debug(log_header .. "auto control state length mismatch")
end
else
log.debug(log_header .. "auto control state not a table")
end
end
2022-12-04 18:59:10 +00:00
-- update alarm sounder
sounder.eval(io.units)
end
return true
end
-- get the IO controller database
function iocontrol.get_db() return io end
return iocontrol