#272 basic dynamic tank data in supervisor and coordinator

This commit is contained in:
Mikayla Fischler 2023-07-15 13:16:36 -04:00
parent a164c18a50
commit 47bda73afe
10 changed files with 645 additions and 231 deletions

View File

@ -91,6 +91,9 @@ function iocontrol.init(conf, comms)
sps_ps_tbl = {},
sps_data_tbl = {},
tank_ps_tbl = {},
tank_data_tbl = {},
env_d_ps = psil.create(),
env_d_data = {}
}
@ -181,7 +184,10 @@ function iocontrol.init(conf, comms)
boiler_data_tbl = {},
turbine_ps_tbl = {},
turbine_data_tbl = {}
turbine_data_tbl = {},
tank_ps_tbl = {},
tank_data_tbl = {}
}
-- create boiler tables
@ -208,6 +214,8 @@ function iocontrol.init(conf, comms)
process.init(io, comms)
end
--#region Front Panel PSIL
-- toggle heartbeat indicator
function iocontrol.heartbeat() io.fp.ps.toggle("heartbeat") end
@ -262,6 +270,36 @@ function iocontrol.fp_pkt_rtt(session_id, rtt)
end
end
--#endregion
--#region Builds
-- record and publish multiblock RTU build data
---@param id integer
---@param entry table
---@param data_tbl table
---@param ps_tbl table
---@param create boolean? true to create an entry if non exists, false to fail on missing
---@return boolean ok true if data saved, false if invalid ID
local function _record_multiblock_build(id, entry, data_tbl, ps_tbl, create)
local exists = type(data_tbl[id]) == "table"
if exists or create then
if not exists then
ps_tbl[id] = psil.create()
data_tbl[id] = {}
end
data_tbl[id].formed = entry[1] ---@type boolean
data_tbl[id].build = entry[2] ---@type table
ps_tbl[id].publish("formed", entry[1])
for key, val in pairs(data_tbl[id].build) do ps_tbl[id].publish(key, val) end
end
return exists or (create == true)
end
-- populate facility structure builds
---@param build table
---@return boolean valid
@ -274,16 +312,7 @@ function iocontrol.record_facility_builds(build)
-- 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
if not _record_multiblock_build(id, matrix, fac.induction_data_tbl, fac.induction_ps_tbl) then
log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id))
valid = false
end
@ -293,21 +322,19 @@ function iocontrol.record_facility_builds(build)
-- SPS
if type(build.sps) == "table" then
for id, sps in pairs(build.sps) do
if type(fac.sps_data_tbl[id]) == "table" then
fac.sps_data_tbl[id].formed = sps[1] ---@type boolean
fac.sps_data_tbl[id].build = sps[2] ---@type table
fac.sps_ps_tbl[id].publish("formed", sps[1])
for key, val in pairs(fac.sps_data_tbl[id].build) do
fac.sps_ps_tbl[id].publish(key, val)
end
else
if not _record_multiblock_build(id, sps, fac.sps_data_tbl, fac.sps_ps_tbl) then
log.debug(util.c("iocontrol.record_facility_builds: invalid SPS id ", id))
valid = false
end
end
end
-- dynamic tanks
if type(build.tanks) == "table" then
for id, tank in pairs(build.tanks) do
_record_multiblock_build(id, tank, fac.tank_data_tbl, fac.tank_ps_tbl, true)
end
end
else
log.debug("facility builds not a table")
valid = false
@ -351,16 +378,7 @@ function iocontrol.record_unit_builds(builds)
-- 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
if not _record_multiblock_build(b_id, boiler, unit.boiler_data_tbl, unit.boiler_ps_tbl) then
log.debug(util.c(log_header, "invalid boiler id ", b_id))
valid = false
end
@ -370,27 +388,49 @@ function iocontrol.record_unit_builds(builds)
-- 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
unit.turbine_ps_tbl[t_id].publish("formed", turbine[1])
for key, val in pairs(unit.turbine_data_tbl[t_id].build) do
unit.turbine_ps_tbl[t_id].publish(key, val)
end
else
if not _record_multiblock_build(t_id, turbine, unit.turbine_data_tbl, unit.turbine_ps_tbl) then
log.debug(util.c(log_header, "invalid turbine id ", t_id))
valid = false
end
end
end
-- dynamic tank builds
if type(build.tanks) == "table" then
for d_id, d_tank in pairs(build.tanks) do
_record_multiblock_build(d_id, d_tank, unit.tank_data_tbl, unit.tank_ps_tbl, true)
end
end
end
end
return valid
end
--#endregion
--#region Statuses
-- record and publish multiblock status data
---@param entry any
---@param data imatrix_session_db|sps_session_db|dynamicv_session_db|turbinev_session_db|boilerv_session_db
---@param ps psil
---@return boolean is_faulted
local function _record_multiblock_status(entry, data, ps)
local is_faulted = entry[1] ---@type boolean
data.formed = entry[2] ---@type boolean
data.state = entry[3] ---@type table
data.tanks = entry[4] ---@type table
ps.publish("formed", data.formed)
ps.publish("faulted", is_faulted)
for key, val in pairs(data.state) do ps.publish(key, val) end
for key, val in pairs(data.tanks) do ps.publish(key, val) end
return is_faulted
end
-- update facility status
---@param status table
---@return boolean valid
@ -498,36 +538,23 @@ function iocontrol.update_facility_status(status)
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
local ps = fac.induction_ps_tbl[id] ---@type psil
local data = fac.induction_data_tbl[id] ---@type imatrix_session_db
local rtu_faulted = _record_multiblock_status(matrix, data, ps)
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
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.energy_fill >= 0.99 then
ps.publish("computed_status", 6) -- full
elseif data.tanks.energy_fill <= 0.01 then
fac.induction_ps_tbl[id].publish("computed_status", 5) -- empty
ps.publish("computed_status", 5) -- empty
else
fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line
ps.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)
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid induction matrix id ", id))
@ -549,31 +576,23 @@ function iocontrol.update_facility_status(status)
for id, sps in pairs(rtu_statuses.sps) do
if type(fac.sps_data_tbl[id]) == "table" then
local rtu_faulted = sps[1] ---@type boolean
fac.sps_data_tbl[id].formed = sps[2] ---@type boolean
fac.sps_data_tbl[id].state = sps[3] ---@type table
fac.sps_data_tbl[id].tanks = sps[4] ---@type table
local data = fac.sps_data_tbl[id] ---@type sps_session_db
local ps = fac.sps_ps_tbl[id] ---@type psil
local data = fac.sps_data_tbl[id] ---@type sps_session_db
local rtu_faulted = _record_multiblock_status(sps, data, ps)
fac.sps_ps_tbl[id].publish("formed", data.formed)
fac.sps_ps_tbl[id].publish("faulted", rtu_faulted)
if data.formed then
if rtu_faulted then
fac.sps_ps_tbl[id].publish("computed_status", 3) -- faulted
elseif data.state.process_rate > 0 then
fac.sps_ps_tbl[id].publish("computed_status", 5) -- active
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.state.process_rate > 0 then
ps.publish("computed_status", 5) -- active
else
fac.sps_ps_tbl[id].publish("computed_status", 4) -- idle
ps.publish("computed_status", 4) -- idle
end
else
fac.sps_ps_tbl[id].publish("computed_status", 2) -- not formed
ps.publish("computed_status", 2) -- not formed
end
for key, val in pairs(data.state) do fac.sps_ps_tbl[id].publish(key, val) end
for key, val in pairs(data.tanks) do fac.sps_ps_tbl[id].publish(key, val) end
io.facility.ps.publish("am_rate", data.state.process_rate * 1000)
else
log.debug(util.c(log_header, "invalid sps id ", id))
@ -584,6 +603,44 @@ function iocontrol.update_facility_status(status)
valid = false
end
-- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then
for id = 1, #fac.tank_ps_tbl do
if rtu_statuses.tanks[id] == nil then
-- disconnected
fac.tank_ps_tbl[id].publish("computed_status", 1)
end
end
for id, tank in pairs(rtu_statuses.tanks) do
if type(fac.tank_data_tbl[id]) == "table" then
local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db
local ps = fac.tank_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(tank, data, ps)
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.fill >= 0.99 then
ps.publish("computed_status", 6) -- full
elseif data.tanks.fill < 0.20 then
ps.publish("computed_status", 5) -- low
else
ps.publish("computed_status", 4) -- on-line
end
else
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid dynamic tank id ", id))
end
end
else
log.debug(log_header .. "dyanmic tank list not a table")
valid = false
end
-- environment detector status
if type(rtu_statuses.rad_mon) == "table" then
if #rtu_statuses.rad_mon > 0 then
@ -731,34 +788,21 @@ function iocontrol.update_unit_statuses(statuses)
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
local ps = unit.boiler_ps_tbl[id] ---@type psil
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)
local rtu_faulted = _record_multiblock_status(boiler, data, ps)
if rtu_faulted then
unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.state.boil_rate > 0 then
unit.boiler_ps_tbl[id].publish("computed_status", 5) -- active
ps.publish("computed_status", 5) -- active
else
unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle
ps.publish("computed_status", 4) -- idle
end
else
unit.boiler_ps_tbl[id].publish("computed_status", 2) -- not formed
end
for key, val in pairs(unit.boiler_data_tbl[id].state) do
unit.boiler_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.boiler_data_tbl[id].tanks) do
unit.boiler_ps_tbl[id].publish(key, val)
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid boiler id ", id))
@ -781,36 +825,23 @@ function iocontrol.update_unit_statuses(statuses)
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
local ps = unit.turbine_ps_tbl[id] ---@type psil
unit.turbine_ps_tbl[id].publish("formed", data.formed)
unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted)
local rtu_faulted = _record_multiblock_status(turbine, data, ps)
if rtu_faulted then
unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.energy_fill >= 0.99 then
unit.turbine_ps_tbl[id].publish("computed_status", 6) -- trip
ps.publish("computed_status", 6) -- trip
elseif data.state.flow_rate < 100 then
unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle
ps.publish("computed_status", 4) -- idle
else
unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active
ps.publish("computed_status", 5) -- active
end
else
unit.turbine_ps_tbl[id].publish("computed_status", 2) -- not formed
end
for key, val in pairs(unit.turbine_data_tbl[id].state) do
unit.turbine_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.turbine_data_tbl[id].tanks) do
unit.turbine_ps_tbl[id].publish(key, val)
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid turbine id ", id))
@ -822,6 +853,45 @@ function iocontrol.update_unit_statuses(statuses)
valid = false
end
-- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then
for id = 1, #unit.tank_ps_tbl do
if rtu_statuses.tanks[i] == nil then
-- disconnected
unit.tank_ps_tbl[id].publish("computed_status", 1)
end
end
for id, tank in pairs(rtu_statuses.tanks) do
if type(unit.tank_data_tbl[id]) == "table" then
local data = unit.tank_data_tbl[id] ---@type dynamicv_session_db
local ps = unit.tank_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(tank, data, ps)
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.fill >= 0.99 then
ps.publish("computed_status", 6) -- full
elseif data.tanks.fill < 0.20 then
ps.publish("computed_status", 5) -- low
else
ps.publish("computed_status", 5) -- active
end
else
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid dynamic tank id ", id))
valid = false
end
end
else
log.debug(log_header .. "dynamic tank list not a table")
valid = false
end
-- solar neutron activator status info
if type(rtu_statuses.sna) == "table" then
unit.num_snas = rtu_statuses.sna[1] ---@type integer
@ -951,6 +1021,8 @@ function iocontrol.update_unit_statuses(statuses)
return valid
end
--#endregion
-- get the IO controller database
function iocontrol.get_db() return io end

View File

@ -22,7 +22,7 @@ local sounder = require("coordinator.sounder")
local apisessions = require("coordinator.session.apisessions")
local COORDINATOR_VERSION = "v0.20.3"
local COORDINATOR_VERSION = "v0.21.0"
local println = util.println
local println_ts = util.println_ts

File diff suppressed because one or more lines are too long

View File

@ -62,9 +62,11 @@ function facility.new(num_reactors, cooling_conf)
all_sys_ok = false,
-- rtus
rtu_conn_count = 0,
rtu_list = {},
redstone = {},
induction = {},
sps = {},
tanks = {},
envd = {},
-- redstone I/O control
io_ctl = nil, ---@type rs_controller
@ -120,15 +122,12 @@ function facility.new(num_reactors, cooling_conf)
table.insert(self.group_map, 0)
end
-- list for RTU session management
self.rtu_list = { self.redstone, self.induction, self.sps, self.tanks, self.envd }
-- init redstone RTU I/O controller
self.io_ctl = rsctl.new(self.redstone)
-- unlink disconnected units
---@param sessions table
local function _unlink_disconnected_units(sessions)
util.filter_table(sessions, function (u) return u.is_connected() end)
end
-- check if all auto-controlled units completed ramping
---@nodiscard
local function _all_units_ramped()
@ -215,29 +214,48 @@ function facility.new(num_reactors, cooling_conf)
-- link an induction matrix RTU session
---@param imatrix unit_session
---@return boolean linked induction matrix accepted (max 1)
function public.add_imatrix(imatrix)
table.insert(self.induction, imatrix)
if #self.induction == 0 then
table.insert(self.induction, imatrix)
return true
else return false end
end
-- link an SPS RTU session
---@param sps unit_session
---@return boolean linked SPS accepted (max 1)
function public.add_sps(sps)
table.insert(self.sps, sps)
if #self.sps == 0 then
table.insert(self.sps, sps)
return true
else return false end
end
-- link a dynamic tank RTU session
---@param dynamic_tank unit_session
---@return boolean linked dynamic tank accepted (max 1)
function public.add_tank(dynamic_tank)
if #self.tanks == 0 then
table.insert(self.tanks, dynamic_tank)
return true
else return false end
end
-- link an environment detector RTU session
---@param envd unit_session
---@return boolean linked environment detector accepted (max 1)
function public.add_envd(envd)
table.insert(self.envd, envd)
if #self.envd == 0 then
table.insert(self.envd, envd)
return true
else return false end
end
-- purge devices associated with the given RTU session ID
---@param session integer RTU session ID
function public.purge_rtu_devices(session)
util.filter_table(self.redstone, function (s) return s.get_session_id() ~= session end)
util.filter_table(self.induction, function (s) return s.get_session_id() ~= session end)
util.filter_table(self.sps, function (s) return s.get_session_id() ~= session end)
util.filter_table(self.envd, function (s) return s.get_session_id() ~= session end)
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (s) return s.get_session_id() ~= session end) end
end
-- UPDATE --
@ -251,10 +269,7 @@ function facility.new(num_reactors, cooling_conf)
-- update (iterate) the facility management
function public.update()
-- unlink RTU unit sessions if they are closed
_unlink_disconnected_units(self.redstone)
_unlink_disconnected_units(self.induction)
_unlink_disconnected_units(self.sps)
_unlink_disconnected_units(self.envd)
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
-- current state for process control
local charge_update = 0
@ -814,11 +829,9 @@ function facility.new(num_reactors, cooling_conf)
ready = self.mode_set > 0
if (self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0) then
ready = false
elseif (self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0) then
ready = false
elseif (self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1) then
if (self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0) or
(self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0) or
(self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1) then
ready = false
end
@ -903,6 +916,14 @@ function facility.new(num_reactors, cooling_conf)
end
end
if all or type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
build.tanks = {}
for i = 1, #self.tanks do
local tank = self.tanks[i] ---@type unit_session
build.tanks[tank.get_device_idx()] = { tank.get_db().formed, tank.get_db().build }
end
end
return build
end
@ -948,35 +969,32 @@ function facility.new(num_reactors, cooling_conf)
-- status of induction matricies (including tanks)
status.induction = {}
for i = 1, #self.induction do
local matrix = self.induction[i] ---@type unit_session
status.induction[matrix.get_device_idx()] = {
matrix.is_faulted(),
matrix.get_db().formed,
matrix.get_db().state,
matrix.get_db().tanks
}
local matrix = self.induction[i] ---@type unit_session
local db = matrix.get_db() ---@type imatrix_session_db
status.induction[matrix.get_device_idx()] = { matrix.is_faulted(), db.formed, db.state, db.tanks }
end
-- status of sps
status.sps = {}
for i = 1, #self.sps do
local sps = self.sps[i] ---@type unit_session
status.sps[sps.get_device_idx()] = {
sps.is_faulted(),
sps.get_db().formed,
sps.get_db().state,
sps.get_db().tanks
}
local sps = self.sps[i] ---@type unit_session
local db = sps.get_db() ---@type sps_session_db
status.sps[sps.get_device_idx()] = { sps.is_faulted(), db.formed, db.state, db.tanks }
end
-- status of dynamic tanks
status.tanks = {}
for i = 1, #self.tanks do
local tank = self.tanks[i] ---@type unit_session
local db = tank.get_db() ---@type dynamicv_session_db
status.tanks[tank.get_device_idx()] = { tank.is_faulted(), db.formed, db.state, db.tanks }
end
-- radiation monitors (environment detectors)
status.rad_mon = {}
for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session
status.rad_mon[envd.get_device_idx()] = {
envd.is_faulted(),
envd.get_db().radiation
}
status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation }
end
return status

View File

@ -406,7 +406,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
local builds = {}
local unit = self.units[unit_id] ---@type reactor_unit
builds[unit_id] = unit.get_build(true, false, false)
builds[unit_id] = unit.get_build(-1)
_send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
elseif cmd.key == CRD_S_DATA.RESEND_RTU_BUILD then
@ -420,7 +420,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
local builds = {}
local unit = self.units[unit_id] ---@type reactor_unit
builds[unit_id] = unit.get_build(false, cmd.val.type == RTU_UNIT_TYPE.BOILER_VALVE, cmd.val.type == RTU_UNIT_TYPE.TURBINE_VALVE)
builds[unit_id] = unit.get_build(cmd.val.type)
_send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
else

View File

@ -11,6 +11,7 @@ local svqtypes = require("supervisor.session.svqtypes")
-- supervisor rtu sessions (svrs)
local unit_session = require("supervisor.session.rtu.unit_session")
local svrs_boilerv = require("supervisor.session.rtu.boilerv")
local svrs_dynamicv = require("supervisor.session.rtu.dynamicv")
local svrs_envd = require("supervisor.session.rtu.envd")
local svrs_imatrix = require("supervisor.session.rtu.imatrix")
local svrs_redstone = require("supervisor.session.rtu.redstone")
@ -138,6 +139,10 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
-- turbine
unit = svrs_turbinev.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_turbine(unit) end
elseif u_type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
-- dynamic tank
unit = svrs_dynamicv.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_tank(unit) end
elseif u_type == RTU_UNIT_TYPE.SNA then
-- solar neutron activator
unit = svrs_sna.new(id, i, unit_advert, self.modbus_q)
@ -166,6 +171,10 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
-- super-critical phase shifter
unit = svrs_sps.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then facility.add_sps(unit) end
elseif u_type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
-- dynamic tank
unit = svrs_dynamicv.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then facility.add_tank(unit) end
elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then
-- environment detector
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)

View File

@ -0,0 +1,289 @@
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
local qtypes = require("supervisor.session.rtu.qtypes")
local unit_session = require("supervisor.session.rtu.unit_session")
local dynamicv = {}
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local CONTAINER_MODE = types.CONTAINER_MODE
local MODBUS_FCODE = types.MODBUS_FCODE
local DTV_RTU_S_CMDS = qtypes.DTV_RTU_S_CMDS
local DTV_RTU_S_DATA = qtypes.DTV_RTU_S_DATA
local TXN_TYPES = {
FORMED = 1,
BUILD = 2,
STATE = 3,
TANKS = 4,
INC_CONT = 5,
DEC_CONT = 6,
SET_CONT = 7
}
local TXN_TAGS = {
"dynamicv.formed",
"dynamicv.build",
"dynamicv.state",
"dynamicv.tanks",
"dynamicv.inc_cont_mode",
"dynamicv.dec_cont_mode",
"dynamicv.set_cont_mode"
}
local PERIODICS = {
FORMED = 2000,
BUILD = 1000,
STATE = 1000,
TANKS = 500
}
-- create a new dynamicv rtu session runner
---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
function dynamicv.new(session_id, unit_id, advert, out_queue)
-- type check
if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then
log.error("attempt to instantiate dynamicv RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil
end
local log_tag = "session.rtu(" .. session_id .. ").dynamicv(" .. advert.index .. "): "
local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
has_build = false,
periodics = {
next_formed_req = 0,
next_build_req = 0,
next_state_req = 0,
next_tanks_req = 0
},
---@class dynamicv_session_db
db = {
formed = false,
build = {
last_update = 0,
length = 0,
width = 0,
height = 0,
min_pos = types.new_zero_coordinate(),
max_pos = types.new_zero_coordinate(),
tank_capacity = 0,
chem_tank_capacity = 0
},
state = {
last_update = 0,
container_mode = CONTAINER_MODE.BOTH ---@type container_mode
},
tanks = {
last_update = 0,
stored = types.new_empty_gas(),
fill = 0
}
}
}
local public = self.session.get()
-- PRIVATE FUNCTIONS --
-- increment the container mode
local function _inc_cont_mode()
-- write coil 1 with unused value 0
self.session.send_request(TXN_TYPES.INC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 1, 0 })
end
-- decrement the container mode
local function _dec_cont_mode()
-- write coil 2 with unused value 0
self.session.send_request(TXN_TYPES.DEC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 2, 0 })
end
-- set the container mode
---@param mode container_mode
local function _set_cont_mode(mode)
-- write holding register 1
self.session.send_request(TXN_TYPES.SET_CONT, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode })
end
-- query if the multiblock is formed
local function _request_formed()
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
end
-- query the build of the device
local function _request_build()
-- read input registers 1 through 7 (start = 1, count = 7)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 })
end
-- query the state of the device
local function _request_state()
-- read holding register 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, 1 })
end
-- query the tanks of the device
local function _request_tanks()
-- read input registers 8 through 9 (start = 8, count = 2)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 })
end
-- PUBLIC FUNCTIONS --
-- handle a packet
---@param m_pkt modbus_frame
function public.handle_packet(m_pkt)
local txn_type = self.session.try_resolve(m_pkt)
if txn_type == false then
-- nothing to do
elseif txn_type == TXN_TYPES.FORMED then
-- formed response
-- load in data if correct length
if m_pkt.length == 1 then
self.db.formed = m_pkt.data[1]
if not self.db.formed then self.has_build = false end
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.BUILD then
-- build response
if m_pkt.length == 7 then
self.db.build.last_update = util.time_ms()
self.db.build.length = m_pkt.data[1]
self.db.build.width = m_pkt.data[2]
self.db.build.height = m_pkt.data[3]
self.db.build.min_pos = m_pkt.data[4]
self.db.build.max_pos = m_pkt.data[5]
self.db.build.tank_capacity = m_pkt.data[6]
self.db.build.chem_tank_capacity = m_pkt.data[7]
self.has_build = true
out_queue.push_data(unit_session.RTU_US_DATA.BUILD_CHANGED, { unit = advert.reactor, type = advert.type })
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.STATE then
-- state response
if m_pkt.length == 1 then
self.db.state.last_update = util.time_ms()
self.db.state.container_mode = m_pkt.data[1]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.TANKS then
-- tanks response
if m_pkt.length == 2 then
self.db.tanks.last_update = util.time_ms()
self.db.tanks.stored = m_pkt.data[1]
self.db.tanks.fill = m_pkt.data[2]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.INC_CONT or txn_type == TXN_TYPES.DEC_CONT or txn_type == TXN_TYPES.SET_CONT then
-- successful acknowledgement
elseif txn_type == nil then
log.error(log_tag .. "unknown transaction reply")
else
log.error(log_tag .. "unknown transaction type " .. txn_type)
end
end
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
-- check command queue
while self.session.in_q.ready() do
-- get a new message to process
local msg = self.session.in_q.pop()
if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then
-- instruction
local cmd = msg.message
if cmd == DTV_RTU_S_CMDS.INC_CONT_MODE then
_inc_cont_mode()
elseif cmd == DTV_RTU_S_CMDS.DEC_CONT_MODE then
_dec_cont_mode()
else
log.debug(util.c(log_tag, "unrecognized in-queue command ", cmd))
end
elseif msg.qtype == mqueue.TYPE.DATA then
-- instruction with body
local cmd = msg.message ---@type queue_data
if cmd.key == DTV_RTU_S_DATA.SET_CONT_MODE then
if cmd.val == types.CONTAINER_MODE.BOTH or
cmd.val == types.CONTAINER_MODE.FILL or
cmd.val == types.CONTAINER_MODE.EMPTY then
_set_cont_mode(cmd.val)
else
log.debug(util.c(log_tag, "unrecognized container mode \"", cmd.val, "\""))
end
else
log.debug(util.c(log_tag, "unrecognized in-queue data ", cmd.key))
end
end
end
-- max 100ms spent processing queue
if util.time() - time_now > 100 then
log.warning(log_tag .. "exceeded 100ms queue process limit")
break
end
end
time_now = util.time()
-- handle periodics
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
self.session.post_update()
end
-- invalidate build cache
function public.invalidate_cache()
self.periodics.next_formed_req = 0
self.periodics.next_build_req = 0
self.has_build = false
end
-- get the unit session database
---@nodiscard
function public.get_db() return self.db end
return public
end
return dynamicv

View File

@ -1,16 +1,31 @@
---@class rtu_unit_qtypes
local qtypes = {}
-- turbine valve rtu session commands
local TBV_RTU_S_CMDS = {
INC_DUMP_MODE = 1,
DEC_DUMP_MODE = 2
}
-- turbine valve rtu session commands w/ parameters
local TBV_RTU_S_DATA = {
SET_DUMP_MODE = 1
}
-- dynamic tank valve rtu session commands
local DTV_RTU_S_CMDS = {
INC_CONT_MODE = 1,
DEC_CONT_MODE = 2
}
-- dynamic tank valve rtu session commands w/ parameters
local DTV_RTU_S_DATA = {
SET_CONT_MODE = 1
}
qtypes.TBV_RTU_S_CMDS = TBV_RTU_S_CMDS
qtypes.TBV_RTU_S_DATA = TBV_RTU_S_DATA
qtypes.DTV_RTU_S_CMDS = DTV_RTU_S_CMDS
qtypes.DTV_RTU_S_DATA = DTV_RTU_S_DATA
return qtypes

View File

@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v0.19.4"
local SUPERVISOR_VERSION = "v0.20.0"
local println = util.println
local println_ts = util.println_ts

View File

@ -11,12 +11,13 @@ local rsctl = require("supervisor.session.rsctl")
---@class reactor_control_unit
local unit = {}
local WASTE_MODE = types.WASTE_MODE
local WASTE = types.WASTE_PRODUCT
local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE
local TRI_FAIL = types.TRI_FAIL
local WASTE_MODE = types.WASTE_MODE
local WASTE = types.WASTE_PRODUCT
local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE
local TRI_FAIL = types.TRI_FAIL
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local PLC_S_CMDS = plc.PLC_S_CMDS
@ -69,10 +70,12 @@ function unit.new(reactor_id, num_boilers, num_turbines)
num_turbines = num_turbines,
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE },
-- rtus
rtu_list = {},
redstone = {},
boilers = {},
turbines = {},
sna = {},
tanks = {},
snas = {},
envd = {},
sna_prod_rate = 0,
-- redstone control
@ -230,6 +233,9 @@ function unit.new(reactor_id, num_boilers, num_turbines)
}
}
-- list for RTU session management
self.rtu_list = { self.redstone, self.boilers, self.turbines, self.tanks, self.snas, self.envd }
-- init redstone RTU I/O controller
self.io_ctl = rsctl.new(self.redstone)
@ -373,12 +379,6 @@ function unit.new(reactor_id, num_boilers, num_turbines)
--#endregion
-- unlink disconnected units
---@param sessions table
local function _unlink_disconnected_units(sessions)
util.filter_table(sessions, function (u) return u.is_connected() end)
end
-- PUBLIC FUNCTIONS --
---@class reactor_unit
@ -413,6 +413,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
-- link a turbine RTU session
---@param turbine unit_session
---@return boolean linked turbine accepted to associated device slot
function public.add_turbine(turbine)
if #self.turbines < num_turbines and turbine.get_device_idx() <= num_turbines then
table.insert(self.turbines, turbine)
@ -422,13 +423,12 @@ function unit.new(reactor_id, num_boilers, num_turbines)
_reset_dt(DT_KEYS.TurbinePower .. turbine.get_device_idx())
return true
else
return false
end
else return false end
end
-- link a boiler RTU session
---@param boiler unit_session
---@return boolean linked boiler accepted to associated device slot
function public.add_boiler(boiler)
if #self.boilers < num_boilers and boiler.get_device_idx() <= num_boilers then
table.insert(self.boilers, boiler)
@ -440,31 +440,37 @@ function unit.new(reactor_id, num_boilers, num_turbines)
_reset_dt(DT_KEYS.BoilerHCool .. boiler.get_device_idx())
return true
else
return false
end
else return false end
end
-- link a dynamic tank RTU session
---@param dynamic_tank unit_session
---@return boolean linked dynamic tank accepted (max 1)
function public.add_tank(dynamic_tank)
if #self.tanks == 0 then
table.insert(self.tanks, dynamic_tank)
return true
else return false end
end
-- link a solar neutron activator RTU session
---@param sna unit_session
function public.add_sna(sna)
table.insert(self.sna, sna)
end
function public.add_sna(sna) table.insert(self.snas, sna) end
-- link an environment detector RTU session
---@param envd unit_session
---@return boolean linked environment detector accepted (max 1)
function public.add_envd(envd)
table.insert(self.envd, envd)
if #self.envd == 0 then
table.insert(self.envd, envd)
return true
else return false end
end
-- purge devices associated with the given RTU session ID
---@param session integer RTU session ID
function public.purge_rtu_devices(session)
util.filter_table(self.redstone, function (s) return s.get_session_id() ~= session end)
util.filter_table(self.boilers, function (s) return s.get_session_id() ~= session end)
util.filter_table(self.turbines, function (s) return s.get_session_id() ~= session end)
util.filter_table(self.sna, function (s) return s.get_session_id() ~= session end)
util.filter_table(self.envd, function (s) return s.get_session_id() ~= session end)
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (s) return s.get_session_id() ~= session end) end
end
--#endregion
@ -482,11 +488,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end
-- unlink RTU unit sessions if they are closed
_unlink_disconnected_units(self.redstone)
_unlink_disconnected_units(self.boilers)
_unlink_disconnected_units(self.turbines)
_unlink_disconnected_units(self.sna)
_unlink_disconnected_units(self.envd)
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
-- update degraded state for auto control
self.db.control.degraded = (#self.boilers ~= num_boilers) or (#self.turbines ~= num_turbines) or (self.plc_i == nil)
@ -717,21 +719,25 @@ function unit.new(reactor_id, num_boilers, num_turbines)
return false
end
-- get build properties of all machines
-- get build properties of machines
--
-- filter options
-- - nil to include all builds
-- - -1 to include only PLC build
-- - RTU_UNIT_TYPE to include all builds of machines of that type
---@nodiscard
---@param inc_plc boolean? true/nil to include PLC build, false to exclude
---@param inc_boilers boolean? true/nil to include boiler builds, false to exclude
---@param inc_turbines boolean? true/nil to include turbine builds, false to exclude
function public.get_build(inc_plc, inc_boilers, inc_turbines)
---@param filter -1|RTU_UNIT_TYPE? filter as described above
function public.get_build(filter)
local all = filter == nil
local build = {}
if inc_plc ~= false then
if all or (filter == -1) then
if self.plc_i ~= nil then
build.reactor = self.plc_i.get_struct()
end
end
if inc_boilers ~= false then
if all or (filter == RTU_UNIT_TYPE.BOILER_VALVE) then
build.boilers = {}
for i = 1, #self.boilers do
local boiler = self.boilers[i] ---@type unit_session
@ -739,7 +745,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end
end
if inc_turbines ~= false then
if all or (filter == RTU_UNIT_TYPE.TURBINE_VALVE) then
build.turbines = {}
for i = 1, #self.turbines do
local turbine = self.turbines[i] ---@type unit_session
@ -747,6 +753,14 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end
end
if all or (filter == RTU_UNIT_TYPE.DYNAMIC_VALVE) then
build.tanks = {}
for i = 1, #self.tanks do
local tank = self.tanks[i] ---@type unit_session
build.tanks[tank.get_device_idx()] = { tank.get_db().formed, tank.get_db().build }
end
end
return build
end
@ -777,38 +791,35 @@ function unit.new(reactor_id, num_boilers, num_turbines)
-- status of boilers (including tanks)
status.boilers = {}
for i = 1, #self.boilers do
local boiler = self.boilers[i] ---@type unit_session
status.boilers[boiler.get_device_idx()] = {
boiler.is_faulted(),
boiler.get_db().formed,
boiler.get_db().state,
boiler.get_db().tanks
}
local boiler = self.boilers[i] ---@type unit_session
local db = boiler.get_db() ---@type boilerv_session_db
status.boilers[boiler.get_device_idx()] = { boiler.is_faulted(), db.formed, db.state, db.tanks }
end
-- status of turbines (including tanks)
status.turbines = {}
for i = 1, #self.turbines do
local turbine = self.turbines[i] ---@type unit_session
status.turbines[turbine.get_device_idx()] = {
turbine.is_faulted(),
turbine.get_db().formed,
turbine.get_db().state,
turbine.get_db().tanks
}
local db = turbine.get_db() ---@type turbinev_session_db
status.turbines[turbine.get_device_idx()] = { turbine.is_faulted(), db.formed, db.state, db.tanks }
end
-- basic SNA statistical information, don't send everything, it's not necessary
status.sna = { #self.sna, public.get_sna_rate() }
-- status of dynamic tanks
status.tanks = {}
for i = 1, #self.tanks do
local tank = self.tanks[i] ---@type unit_session
local db = tank.get_db() ---@type dynamicv_session_db
status.turbines[tank.get_device_idx()] = { tank.is_faulted(), db.formed, db.state, db.tanks }
end
-- basic SNA statistical information
status.sna = { #self.snas, public.get_sna_rate() }
-- radiation monitors (environment detectors)
status.rad_mon = {}
for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session
status.rad_mon[envd.get_device_idx()] = {
envd.is_faulted(),
envd.get_db().radiation
}
local envd = self.envd[i] ---@type unit_session
status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation }
end
return status
@ -820,8 +831,8 @@ function unit.new(reactor_id, num_boilers, num_turbines)
function public.get_sna_rate()
local total_avail_rate = 0
for i = 1, #self.sna do
local db = self.sna[i].get_db() ---@type sna_session_db
for i = 1, #self.snas do
local db = self.snas[i].get_db() ---@type sna_session_db
total_avail_rate = total_avail_rate + db.state.production_rate
end