diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index d199e0c..ce6d667 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -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 diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 52904f2..0a922b7 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -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 diff --git a/install_manifest.json b/install_manifest.json index 8982f19..e0db13b 100644 --- a/install_manifest.json +++ b/install_manifest.json @@ -1 +1 @@ -{"versions": {"installer": "v1.5a", "bootloader": "0.2", "comms": "2.1.2", "graphics": "1.0.1", "lockbox": "1.0", "reactor-plc": "v1.5.5", "rtu": "v1.5.3", "supervisor": "v0.19.4", "coordinator": "v0.20.3", "pocket": "alpha-v0.5.2"}, "files": {"system": ["initenv.lua", "startup.lua"], "common": ["scada-common/ppm.lua", "scada-common/comms.lua", "scada-common/psil.lua", "scada-common/rsio.lua", "scada-common/constants.lua", "scada-common/mqueue.lua", "scada-common/tcd.lua", "scada-common/crash.lua", "scada-common/log.lua", "scada-common/types.lua", "scada-common/util.lua", "scada-common/network.lua"], "graphics": ["graphics/element.lua", "graphics/events.lua", "graphics/flasher.lua", "graphics/core.lua", "graphics/elements/listbox.lua", "graphics/elements/textbox.lua", "graphics/elements/displaybox.lua", "graphics/elements/pipenet.lua", "graphics/elements/rectangle.lua", "graphics/elements/div.lua", "graphics/elements/multipane.lua", "graphics/elements/tiling.lua", "graphics/elements/colormap.lua", "graphics/elements/indicators/alight.lua", "graphics/elements/indicators/icon.lua", "graphics/elements/indicators/power.lua", "graphics/elements/indicators/rad.lua", "graphics/elements/indicators/state.lua", "graphics/elements/indicators/light.lua", "graphics/elements/indicators/vbar.lua", "graphics/elements/indicators/led.lua", "graphics/elements/indicators/coremap.lua", "graphics/elements/indicators/data.lua", "graphics/elements/indicators/ledpair.lua", "graphics/elements/indicators/hbar.lua", "graphics/elements/indicators/trilight.lua", "graphics/elements/indicators/ledrgb.lua", "graphics/elements/controls/switch_button.lua", "graphics/elements/controls/spinbox_numeric.lua", "graphics/elements/controls/hazard_button.lua", "graphics/elements/controls/push_button.lua", "graphics/elements/controls/radio_button.lua", "graphics/elements/controls/multi_button.lua", "graphics/elements/controls/tabbar.lua", "graphics/elements/controls/checkbox.lua", "graphics/elements/controls/sidebar.lua", "graphics/elements/animations/waiting.lua"], "lockbox": ["lockbox/init.lua", "lockbox/LICENSE", "lockbox/kdf/pbkdf2.lua", "lockbox/util/bit.lua", "lockbox/util/array.lua", "lockbox/util/stream.lua", "lockbox/util/queue.lua", "lockbox/digest/sha2_224.lua", "lockbox/digest/sha1.lua", "lockbox/digest/sha2_256.lua", "lockbox/digest/md5.lua", "lockbox/mac/hmac.lua"], "reactor-plc": ["reactor-plc/renderer.lua", "reactor-plc/threads.lua", "reactor-plc/databus.lua", "reactor-plc/plc.lua", "reactor-plc/config.lua", "reactor-plc/startup.lua", "reactor-plc/panel/front_panel.lua", "reactor-plc/panel/style.lua"], "rtu": ["rtu/renderer.lua", "rtu/threads.lua", "rtu/rtu.lua", "rtu/databus.lua", "rtu/modbus.lua", "rtu/config.lua", "rtu/startup.lua", "rtu/panel/front_panel.lua", "rtu/panel/style.lua", "rtu/dev/sps_rtu.lua", "rtu/dev/envd_rtu.lua", "rtu/dev/boilerv_rtu.lua", "rtu/dev/redstone_rtu.lua", "rtu/dev/sna_rtu.lua", "rtu/dev/imatrix_rtu.lua", "rtu/dev/dynamicv_rtu.lua", "rtu/dev/turbinev_rtu.lua"], "supervisor": ["supervisor/renderer.lua", "supervisor/databus.lua", "supervisor/supervisor.lua", "supervisor/unit.lua", "supervisor/config.lua", "supervisor/startup.lua", "supervisor/unitlogic.lua", "supervisor/facility.lua", "supervisor/panel/pgi.lua", "supervisor/panel/front_panel.lua", "supervisor/panel/style.lua", "supervisor/panel/components/rtu_entry.lua", "supervisor/panel/components/pdg_entry.lua", "supervisor/session/coordinator.lua", "supervisor/session/svqtypes.lua", "supervisor/session/pocket.lua", "supervisor/session/svsessions.lua", "supervisor/session/rtu.lua", "supervisor/session/plc.lua", "supervisor/session/rsctl.lua", "supervisor/session/rtu/boilerv.lua", "supervisor/session/rtu/txnctrl.lua", "supervisor/session/rtu/unit_session.lua", "supervisor/session/rtu/turbinev.lua", "supervisor/session/rtu/envd.lua", "supervisor/session/rtu/imatrix.lua", "supervisor/session/rtu/sps.lua", "supervisor/session/rtu/qtypes.lua", "supervisor/session/rtu/sna.lua", "supervisor/session/rtu/redstone.lua"], "coordinator": ["coordinator/coordinator.lua", "coordinator/renderer.lua", "coordinator/iocontrol.lua", "coordinator/sounder.lua", "coordinator/config.lua", "coordinator/startup.lua", "coordinator/process.lua", "coordinator/ui/dialog.lua", "coordinator/ui/pgi.lua", "coordinator/ui/style.lua", "coordinator/ui/layout/main_view.lua", "coordinator/ui/layout/unit_view.lua", "coordinator/ui/layout/front_panel.lua", "coordinator/ui/components/reactor.lua", "coordinator/ui/components/pkt_entry.lua", "coordinator/ui/components/process_ctl.lua", "coordinator/ui/components/unit_overview.lua", "coordinator/ui/components/boiler.lua", "coordinator/ui/components/unit_detail.lua", "coordinator/ui/components/imatrix.lua", "coordinator/ui/components/turbine.lua", "coordinator/session/pocket.lua", "coordinator/session/apisessions.lua"], "pocket": ["pocket/pocket.lua", "pocket/renderer.lua", "pocket/config.lua", "pocket/coreio.lua", "pocket/startup.lua", "pocket/ui/main.lua", "pocket/ui/style.lua", "pocket/ui/components/conn_waiting.lua", "pocket/ui/pages/turbine_page.lua", "pocket/ui/pages/reactor_page.lua", "pocket/ui/pages/home_page.lua", "pocket/ui/pages/unit_page.lua", "pocket/ui/pages/boiler_page.lua"]}, "depends": {"reactor-plc": ["system", "common", "graphics", "lockbox"], "rtu": ["system", "common", "graphics", "lockbox"], "supervisor": ["system", "common", "graphics", "lockbox"], "coordinator": ["system", "common", "graphics", "lockbox"], "pocket": ["system", "common", "graphics", "lockbox"]}, "sizes": {"manifest": 5750, "system": 1991, "common": 98474, "graphics": 147714, "lockbox": 34900, "reactor-plc": 95833, "rtu": 105607, "supervisor": 322676, "coordinator": 230367, "pocket": 37639}} \ No newline at end of file +{"versions": {"installer": "v1.5a", "bootloader": "0.2", "comms": "2.1.2", "graphics": "1.0.1", "lockbox": "1.0", "reactor-plc": "v1.5.5", "rtu": "v1.5.4", "supervisor": "v0.20.0", "coordinator": "v0.21.0", "pocket": "alpha-v0.5.2"}, "files": {"system": ["initenv.lua", "startup.lua"], "common": ["scada-common/ppm.lua", "scada-common/comms.lua", "scada-common/psil.lua", "scada-common/rsio.lua", "scada-common/constants.lua", "scada-common/mqueue.lua", "scada-common/tcd.lua", "scada-common/crash.lua", "scada-common/log.lua", "scada-common/types.lua", "scada-common/util.lua", "scada-common/network.lua"], "graphics": ["graphics/element.lua", "graphics/events.lua", "graphics/flasher.lua", "graphics/core.lua", "graphics/elements/listbox.lua", "graphics/elements/textbox.lua", "graphics/elements/displaybox.lua", "graphics/elements/pipenet.lua", "graphics/elements/rectangle.lua", "graphics/elements/div.lua", "graphics/elements/multipane.lua", "graphics/elements/tiling.lua", "graphics/elements/colormap.lua", "graphics/elements/indicators/alight.lua", "graphics/elements/indicators/icon.lua", "graphics/elements/indicators/power.lua", "graphics/elements/indicators/rad.lua", "graphics/elements/indicators/state.lua", "graphics/elements/indicators/light.lua", "graphics/elements/indicators/vbar.lua", "graphics/elements/indicators/led.lua", "graphics/elements/indicators/coremap.lua", "graphics/elements/indicators/data.lua", "graphics/elements/indicators/ledpair.lua", "graphics/elements/indicators/hbar.lua", "graphics/elements/indicators/trilight.lua", "graphics/elements/indicators/ledrgb.lua", "graphics/elements/controls/switch_button.lua", "graphics/elements/controls/spinbox_numeric.lua", "graphics/elements/controls/hazard_button.lua", "graphics/elements/controls/push_button.lua", "graphics/elements/controls/radio_button.lua", "graphics/elements/controls/multi_button.lua", "graphics/elements/controls/tabbar.lua", "graphics/elements/controls/checkbox.lua", "graphics/elements/controls/sidebar.lua", "graphics/elements/animations/waiting.lua"], "lockbox": ["lockbox/init.lua", "lockbox/LICENSE", "lockbox/kdf/pbkdf2.lua", "lockbox/util/bit.lua", "lockbox/util/array.lua", "lockbox/util/stream.lua", "lockbox/util/queue.lua", "lockbox/digest/sha2_224.lua", "lockbox/digest/sha1.lua", "lockbox/digest/sha2_256.lua", "lockbox/digest/md5.lua", "lockbox/mac/hmac.lua"], "reactor-plc": ["reactor-plc/renderer.lua", "reactor-plc/threads.lua", "reactor-plc/databus.lua", "reactor-plc/plc.lua", "reactor-plc/config.lua", "reactor-plc/startup.lua", "reactor-plc/panel/front_panel.lua", "reactor-plc/panel/style.lua"], "rtu": ["rtu/renderer.lua", "rtu/threads.lua", "rtu/rtu.lua", "rtu/databus.lua", "rtu/modbus.lua", "rtu/config.lua", "rtu/startup.lua", "rtu/panel/front_panel.lua", "rtu/panel/style.lua", "rtu/dev/sps_rtu.lua", "rtu/dev/envd_rtu.lua", "rtu/dev/boilerv_rtu.lua", "rtu/dev/redstone_rtu.lua", "rtu/dev/sna_rtu.lua", "rtu/dev/imatrix_rtu.lua", "rtu/dev/dynamicv_rtu.lua", "rtu/dev/turbinev_rtu.lua"], "supervisor": ["supervisor/renderer.lua", "supervisor/databus.lua", "supervisor/supervisor.lua", "supervisor/unit.lua", "supervisor/config.lua", "supervisor/startup.lua", "supervisor/unitlogic.lua", "supervisor/facility.lua", "supervisor/panel/pgi.lua", "supervisor/panel/front_panel.lua", "supervisor/panel/style.lua", "supervisor/panel/components/rtu_entry.lua", "supervisor/panel/components/pdg_entry.lua", "supervisor/session/coordinator.lua", "supervisor/session/svqtypes.lua", "supervisor/session/pocket.lua", "supervisor/session/svsessions.lua", "supervisor/session/rtu.lua", "supervisor/session/plc.lua", "supervisor/session/rsctl.lua", "supervisor/session/rtu/boilerv.lua", "supervisor/session/rtu/dynamicv.lua", "supervisor/session/rtu/txnctrl.lua", "supervisor/session/rtu/unit_session.lua", "supervisor/session/rtu/turbinev.lua", "supervisor/session/rtu/envd.lua", "supervisor/session/rtu/imatrix.lua", "supervisor/session/rtu/sps.lua", "supervisor/session/rtu/qtypes.lua", "supervisor/session/rtu/sna.lua", "supervisor/session/rtu/redstone.lua"], "coordinator": ["coordinator/coordinator.lua", "coordinator/renderer.lua", "coordinator/iocontrol.lua", "coordinator/sounder.lua", "coordinator/config.lua", "coordinator/startup.lua", "coordinator/process.lua", "coordinator/ui/dialog.lua", "coordinator/ui/pgi.lua", "coordinator/ui/style.lua", "coordinator/ui/layout/main_view.lua", "coordinator/ui/layout/unit_view.lua", "coordinator/ui/layout/front_panel.lua", "coordinator/ui/components/reactor.lua", "coordinator/ui/components/pkt_entry.lua", "coordinator/ui/components/process_ctl.lua", "coordinator/ui/components/unit_overview.lua", "coordinator/ui/components/boiler.lua", "coordinator/ui/components/unit_detail.lua", "coordinator/ui/components/imatrix.lua", "coordinator/ui/components/turbine.lua", "coordinator/session/pocket.lua", "coordinator/session/apisessions.lua"], "pocket": ["pocket/pocket.lua", "pocket/renderer.lua", "pocket/config.lua", "pocket/coreio.lua", "pocket/startup.lua", "pocket/ui/main.lua", "pocket/ui/style.lua", "pocket/ui/components/conn_waiting.lua", "pocket/ui/pages/turbine_page.lua", "pocket/ui/pages/reactor_page.lua", "pocket/ui/pages/home_page.lua", "pocket/ui/pages/unit_page.lua", "pocket/ui/pages/boiler_page.lua"]}, "depends": {"reactor-plc": ["system", "common", "graphics", "lockbox"], "rtu": ["system", "common", "graphics", "lockbox"], "supervisor": ["system", "common", "graphics", "lockbox"], "coordinator": ["system", "common", "graphics", "lockbox"], "pocket": ["system", "common", "graphics", "lockbox"]}, "sizes": {"manifest": 5789, "system": 1991, "common": 98474, "graphics": 147714, "lockbox": 34900, "reactor-plc": 95833, "rtu": 105477, "supervisor": 335422, "coordinator": 231949, "pocket": 37639}} \ No newline at end of file diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 52870fa..77aa676 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -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 diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index eb8935f..e6ddc20 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -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 diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index ce5e08d..c33f11a 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -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) diff --git a/supervisor/session/rtu/dynamicv.lua b/supervisor/session/rtu/dynamicv.lua new file mode 100644 index 0000000..d019da4 --- /dev/null +++ b/supervisor/session/rtu/dynamicv.lua @@ -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 diff --git a/supervisor/session/rtu/qtypes.lua b/supervisor/session/rtu/qtypes.lua index 92a927f..0dff62d 100644 --- a/supervisor/session/rtu/qtypes.lua +++ b/supervisor/session/rtu/qtypes.lua @@ -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 diff --git a/supervisor/startup.lua b/supervisor/startup.lua index ba71270..ce7beb6 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -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 diff --git a/supervisor/unit.lua b/supervisor/unit.lua index beaf495..2b1425c 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -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