From 03f0216d515eee644bff1ac8b68325d26e373f10 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 10 Dec 2022 13:58:17 -0500 Subject: [PATCH] #130 facility data object, some code cleanup, comms protocol changed from 1.0.1 to 1.1.0 --- coordinator/coordinator.lua | 51 +++-- coordinator/iocontrol.lua | 344 ++++++++++++++++++++--------- coordinator/startup.lua | 8 +- coordinator/ui/style.lua | 68 ++++-- reactor-plc/startup.lua | 2 +- rtu/startup.lua | 2 +- scada-common/comms.lua | 28 ++- supervisor/session/coordinator.lua | 111 ++++++---- supervisor/session/facility.lua | 100 +++++++++ supervisor/session/plc.lua | 10 +- supervisor/session/rsctl.lua | 39 ++++ supervisor/session/rtu.lua | 89 ++++---- supervisor/session/svsessions.lua | 21 +- supervisor/session/unit.lua | 43 ++-- supervisor/startup.lua | 2 +- 15 files changed, 635 insertions(+), 283 deletions(-) create mode 100644 supervisor/session/facility.lua create mode 100644 supervisor/session/rsctl.lua diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index 9513b55..a6597c4 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -18,7 +18,7 @@ local DEVICE_TYPES = comms.DEVICE_TYPES local ESTABLISH_ACK = comms.ESTABLISH_ACK local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES -local CRDN_COMMANDS = comms.CRDN_COMMANDS +local UNIT_COMMANDS = comms.UNIT_COMMANDS local coordinator = {} @@ -309,11 +309,11 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa end -- send a unit command - ---@param cmd CRDN_COMMANDS command + ---@param cmd UNIT_COMMANDS command ---@param unit integer unit ID ---@param option any? optional options (like burn rate) function public.send_command(cmd, unit, option) - _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.COMMAND_UNIT, { cmd, unit, option }) + _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.UNIT_CMD, { cmd, unit, option }) end -- parse a packet @@ -388,20 +388,35 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa -- handle packet if protocol == PROTOCOLS.SCADA_CRDN then if self.sv_linked then - if packet.type == SCADA_CRDN_TYPES.STRUCT_BUILDS then - -- record builds - if iocontrol.record_builds(packet.data) then + if packet.type == SCADA_CRDN_TYPES.FAC_BUILDS then + -- record facility builds + if iocontrol.record_facility_builds(packet.data) then -- acknowledge receipt of builds - _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.STRUCT_BUILDS, {}) + _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_BUILDS, {}) else - log.error("received invalid SCADA_CRDN build packet") + log.error("received invalid FAC_BUILDS packet") + end + elseif packet.type == SCADA_CRDN_TYPES.FAC_STATUS then + -- update facility status + if not iocontrol.update_facility_status(packet.data) then + log.error("received invalid FAC_STATUS packet") + end + elseif packet.type == SCADA_CRDN_TYPES.FAC_CMD then + -- facility command acknowledgement + elseif packet.type == SCADA_CRDN_TYPES.UNIT_BUILDS then + -- record builds + if iocontrol.record_unit_builds(packet.data) then + -- acknowledge receipt of builds + _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.UNIT_BUILDS, {}) + else + log.error("received invalid UNIT_BUILDS packet") end elseif packet.type == SCADA_CRDN_TYPES.UNIT_STATUSES then -- update statuses - if not iocontrol.update_statuses(packet.data) then - log.error("received invalid SCADA_CRDN unit statuses packet") + if not iocontrol.update_unit_statuses(packet.data) then + log.error("received invalid UNIT_STATUSES packet") end - elseif packet.type == SCADA_CRDN_TYPES.COMMAND_UNIT then + elseif packet.type == SCADA_CRDN_TYPES.UNIT_CMD then -- unit command acknowledgement if packet.length == 3 then local cmd = packet.data[1] @@ -411,17 +426,17 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa local unit = iocontrol.get_db().units[unit_id] ---@type ioctl_entry if unit ~= nil then - if cmd == CRDN_COMMANDS.SCRAM then + if cmd == UNIT_COMMANDS.SCRAM then unit.scram_ack(ack) - elseif cmd == CRDN_COMMANDS.START then + elseif cmd == UNIT_COMMANDS.START then unit.start_ack(ack) - elseif cmd == CRDN_COMMANDS.RESET_RPS then + elseif cmd == UNIT_COMMANDS.RESET_RPS then unit.reset_rps_ack(ack) - elseif cmd == CRDN_COMMANDS.SET_BURN then + elseif cmd == UNIT_COMMANDS.SET_BURN then unit.set_burn_ack(ack) - elseif cmd == CRDN_COMMANDS.SET_WASTE then + elseif cmd == UNIT_COMMANDS.SET_WASTE then unit.set_waste_ack(ack) - elseif cmd == CRDN_COMMANDS.ACK_ALL_ALARMS then + elseif cmd == UNIT_COMMANDS.ACK_ALL_ALARMS then unit.ack_alarms_ack(ack) else log.debug(util.c("received command ack with unknown command ", cmd)) @@ -432,8 +447,6 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa else log.debug("SCADA_CRDN unit command ack packet length mismatch") end - elseif packet.type == SCADA_CRDN_TYPES.ALARM then - ---@todo alarm/architecture handling else log.warning("received unknown SCADA_CRDN packet type " .. packet.type) end diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 08adbc2..775cfb9 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -6,7 +6,7 @@ local util = require("scada-common.util") local sounder = require("coordinator.sounder") -local CRDN_COMMANDS = comms.CRDN_COMMANDS +local UNIT_COMMANDS = comms.UNIT_COMMANDS local ALARM_STATE = types.ALARM_STATE @@ -22,19 +22,29 @@ local io = {} function iocontrol.init(conf, comms) io.facility = { scram = false, - num_units = conf.num_units, - ps = psil.create() + num_units = conf.num_units, ---@type integer + ps = psil.create(), + + induction_ps_tbl = {}, + induction_data_tbl = {} } + -- create induction tables (max 1 per unit, preferably 1 total) + for _ = 1, conf.num_units do + local data = {} ---@type imatrix_session_db + table.insert(io.facility.induction_ps_tbl, psil.create()) + table.insert(io.facility.induction_data_tbl, data) + end + io.units = {} for i = 1, conf.num_units do local function ack(alarm) - comms.send_command(CRDN_COMMANDS.ACK_ALARM, i, alarm) + comms.send_command(UNIT_COMMANDS.ACK_ALARM, i, alarm) log.debug(util.c("UNIT[", i, "]: ACK ALARM ", alarm)) end local function reset(alarm) - comms.send_command(CRDN_COMMANDS.RESET_ALARM, i, alarm) + comms.send_command(UNIT_COMMANDS.RESET_ALARM, i, alarm) log.debug(util.c("UNIT[", i, "]: RESET ALARM ", alarm)) end @@ -107,33 +117,33 @@ function iocontrol.init(conf, comms) function entry.start() entry.control_state = true - comms.send_command(CRDN_COMMANDS.START, i) + comms.send_command(UNIT_COMMANDS.START, i) log.debug(util.c("UNIT[", i, "]: START")) end function entry.scram() entry.control_state = false - comms.send_command(CRDN_COMMANDS.SCRAM, i) + comms.send_command(UNIT_COMMANDS.SCRAM, i) log.debug(util.c("UNIT[", i, "]: SCRAM")) end function entry.reset_rps() - comms.send_command(CRDN_COMMANDS.RESET_RPS, i) + comms.send_command(UNIT_COMMANDS.RESET_RPS, i) log.debug(util.c("UNIT[", i, "]: RESET_RPS")) end function entry.ack_alarms() - comms.send_command(CRDN_COMMANDS.ACK_ALL_ALARMS, i) + comms.send_command(UNIT_COMMANDS.ACK_ALL_ALARMS, i) log.debug(util.c("UNIT[", i, "]: ACK_ALL_ALARMS")) end function entry.set_burn(rate) - comms.send_command(CRDN_COMMANDS.SET_BURN, i, rate) + comms.send_command(UNIT_COMMANDS.SET_BURN, i, rate) log.debug(util.c("UNIT[", i, "]: SET_BURN = ", rate)) end function entry.set_waste(mode) - comms.send_command(CRDN_COMMANDS.SET_WASTE, i, mode) + comms.send_command(UNIT_COMMANDS.SET_WASTE, i, mode) log.debug(util.c("UNIT[", i, "]: SET_WASTE = ", mode)) end @@ -158,58 +168,173 @@ function iocontrol.init(conf, comms) end end --- populate structure builds +-- populate facility structure builds +---@param build table +---@return boolean valid +function iocontrol.record_facility_builds(build) + if type(build) == "table" then + local fac = io.facility + + -- induction matricies + if type(build.induction) == "table" then + for id, matrix in pairs(build.induction) do + if type(fac.induction_data_tbl[id]) == "table" then + fac.induction_data_tbl[id].formed = matrix[1] ---@type boolean + fac.induction_data_tbl[id].build = matrix[2] ---@type table + + fac.induction_ps_tbl[id].publish("formed", matrix[1]) + + for key, val in pairs(fac.induction_data_tbl[id].build) do + fac.induction_ps_tbl[id].publish(key, val) + end + else + log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id)) + end + end + end + else + log.error("facility builds not a table") + return false + end + + return true +end + +-- populate unit structure builds ---@param builds table ---@return boolean valid -function iocontrol.record_builds(builds) - if #builds ~= #io.units then - log.error("number of provided unit builds does not match expected number of units") +function iocontrol.record_unit_builds(builds) + -- note: if not all units and RTUs are connected, some will be nil + for id, build in pairs(builds) do + local unit = io.units[id] ---@type ioctl_entry + + if type(build) ~= "table" then + log.error(util.c("corrupted unit builds provided, unit ", id, " not a table")) + return false + elseif type(unit) ~= "table" then + log.error(util.c("corrupted unit builds provided, invalid unit ", id)) + return false + end + + local log_header = util.c("iocontrol.record_unit_builds[unit ", id, "]: ") + + -- reactor build + if type(build.reactor) == "table" then + unit.reactor_data.mek_struct = build.reactor + for key, val in pairs(unit.reactor_data.mek_struct) do + unit.reactor_ps.publish(key, val) + end + + if (type(unit.reactor_data.mek_struct.length) == "number") and (unit.reactor_data.mek_struct.length ~= 0) and + (type(unit.reactor_data.mek_struct.width) == "number") and (unit.reactor_data.mek_struct.width ~= 0) then + unit.reactor_ps.publish("size", { unit.reactor_data.mek_struct.length, unit.reactor_data.mek_struct.width }) + end + end + + -- boiler builds + if type(build.boilers) == "table" then + for b_id, boiler in pairs(build.boilers) do + if type(unit.boiler_data_tbl[b_id]) == "table" then + unit.boiler_data_tbl[b_id].formed = boiler[1] ---@type boolean + unit.boiler_data_tbl[b_id].build = boiler[2] ---@type table + + unit.boiler_ps_tbl[b_id].publish("formed", boiler[1]) + + for key, val in pairs(unit.boiler_data_tbl[b_id].build) do + unit.boiler_ps_tbl[b_id].publish(key, val) + end + else + log.debug(util.c(log_header, "invalid boiler id ", b_id)) + end + end + end + + -- turbine builds + if type(build.turbines) == "table" then + for t_id, turbine in pairs(build.turbines) do + if type(unit.turbine_data_tbl[t_id]) == "table" then + unit.turbine_data_tbl[t_id].formed = turbine[1] ---@type boolean + unit.turbine_data_tbl[t_id].build = turbine[2] ---@type table + + 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 + log.debug(util.c(log_header, "invalid turbine id ", t_id)) + end + end + end + end + + return true +end + +-- update facility status +---@param status table +---@return boolean valid +function iocontrol.update_facility_status(status) + local log_header = util.c("iocontrol.update_facility_status: ") + if type(status) ~= "table" then + log.debug(log_header .. "status not a table") return false else - -- note: if not all units and RTUs are connected, some will be nil - for i = 1, #builds do - local unit = io.units[i] ---@type ioctl_entry - local build = builds[i] + local fac = io.facility - -- reactor build - if type(build.reactor) == "table" then - unit.reactor_data.mek_struct = build.reactor - for key, val in pairs(unit.reactor_data.mek_struct) do - unit.reactor_ps.publish(key, val) - end + -- RTU statuses - if (type(unit.reactor_data.mek_struct.length) == "number") and (unit.reactor_data.mek_struct.length ~= 0) and - (type(unit.reactor_data.mek_struct.width) == "number") and (unit.reactor_data.mek_struct.width ~= 0) then - unit.reactor_ps.publish("size", { unit.reactor_data.mek_struct.length, unit.reactor_data.mek_struct.width }) - end - end + local rtu_statuses = status[1] - -- boiler builds - if type(build.boilers) == "table" then - for id, boiler in pairs(build.boilers) do - unit.boiler_data_tbl[id].formed = boiler[1] ---@type boolean - unit.boiler_data_tbl[id].build = boiler[2] ---@type table - - unit.boiler_ps_tbl[id].publish("formed", boiler[1]) - - for key, val in pairs(unit.boiler_data_tbl[id].build) do - unit.boiler_ps_tbl[id].publish(key, val) + if type(rtu_statuses) == "table" then + -- induction matricies statuses + if type(rtu_statuses.induction) == "table" then + for id = 1, #fac.induction_ps_tbl do + if rtu_statuses.induction[id] == nil then + -- disconnected + fac.induction_ps_tbl[id].publish("computed_status", 1) end end - end - -- turbine builds - if type(build.turbines) == "table" then - for id, turbine in pairs(build.turbines) do - unit.turbine_data_tbl[id].formed = turbine[1] ---@type boolean - unit.turbine_data_tbl[id].build = turbine[2] ---@type table + 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 - unit.turbine_ps_tbl[id].publish("formed", turbine[1]) + local data = fac.induction_data_tbl[id] ---@type imatrix_session_db - for key, val in pairs(unit.turbine_data_tbl[id].build) do - unit.turbine_ps_tbl[id].publish(key, val) + fac.induction_ps_tbl[id].publish("formed", data.formed) + fac.induction_ps_tbl[id].publish("faulted", rtu_faulted) + + if data.formed then + if rtu_faulted then + fac.induction_ps_tbl[id].publish("computed_status", 3) -- faulted + elseif data.tanks.energy_fill >= 0.99 then + fac.induction_ps_tbl[id].publish("computed_status", 6) -- full + elseif data.tanks.energy_fill <= 0.01 then + fac.induction_ps_tbl[id].publish("computed_status", 5) -- empty + else + fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line + end + else + fac.induction_ps_tbl[id].publish("computed_status", 2) -- not formed + end + + for key, val in pairs(fac.induction_data_tbl[id].state) do + fac.induction_ps_tbl[id].publish(key, val) + end + + for key, val in pairs(fac.induction_data_tbl[id].tanks) do + fac.induction_ps_tbl[id].publish(key, val) + end + else + log.debug(util.c(log_header, "invalid induction matrix id ", id)) end end + else + log.debug(log_header .. "induction matrix list not a table") end end end @@ -220,16 +345,17 @@ end -- update unit statuses ---@param statuses table ---@return boolean valid -function iocontrol.update_statuses(statuses) +function iocontrol.update_unit_statuses(statuses) if type(statuses) ~= "table" then - log.debug("iocontrol.update_statuses: unit statuses not a table") + log.debug("iocontrol.update_unit_statuses: unit statuses not a table") return false elseif #statuses ~= #io.units then - log.debug("iocontrol.update_statuses: number of provided unit statuses does not match expected number of units") + log.debug("iocontrol.update_unit_statuses: number of provided unit statuses does not match expected number of units") return false else + -- get all unit statuses for i = 1, #statuses do - local log_header = util.c("iocontrol.update_statuses[unit ", i, "]: ") + local log_header = util.c("iocontrol.update_unit_statuses[unit ", i, "]: ") local unit = io.units[i] ---@type ioctl_entry local status = statuses[i] @@ -264,18 +390,18 @@ function iocontrol.update_statuses(statuses) unit.reactor_data.mek_status = mek_status ---@type mek_status if unit.reactor_data.mek_status.status then - unit.reactor_ps.publish("computed_status", 3) -- running + unit.reactor_ps.publish("computed_status", 5) -- running else if unit.reactor_data.no_reactor then - unit.reactor_ps.publish("computed_status", 5) -- faulted + unit.reactor_ps.publish("computed_status", 3) -- faulted elseif not unit.reactor_data.formed then - unit.reactor_ps.publish("computed_status", 6) -- multiblock not formed + unit.reactor_ps.publish("computed_status", 2) -- multiblock not formed elseif unit.reactor_data.rps_status.force_dis then unit.reactor_ps.publish("computed_status", 7) -- reactor force disabled elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then - unit.reactor_ps.publish("computed_status", 4) -- SCRAM + unit.reactor_ps.publish("computed_status", 6) -- SCRAM else - unit.reactor_ps.publish("computed_status", 2) -- disabled + unit.reactor_ps.publish("computed_status", 4) -- disabled end end @@ -307,7 +433,7 @@ function iocontrol.update_statuses(statuses) if type(rtu_statuses) == "table" then -- boiler statuses if type(rtu_statuses.boilers) == "table" then - for id = 1, #unit.boiler_data_tbl do + for id = 1, #unit.boiler_ps_tbl do if rtu_statuses.boilers[i] == nil then -- disconnected unit.boiler_ps_tbl[id].publish("computed_status", 1) @@ -315,34 +441,38 @@ function iocontrol.update_statuses(statuses) end for id, boiler in pairs(rtu_statuses.boilers) do - 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 + 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 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) + unit.boiler_ps_tbl[id].publish("formed", data.formed) + unit.boiler_ps_tbl[id].publish("faulted", rtu_faulted) - if data.formed then - if rtu_faulted then - unit.boiler_ps_tbl[id].publish("computed_status", 4) -- faulted - elseif data.state.boil_rate > 0 then - unit.boiler_ps_tbl[id].publish("computed_status", 3) -- active + if data.formed then + if rtu_faulted then + unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted + elseif data.state.boil_rate > 0 then + unit.boiler_ps_tbl[id].publish("computed_status", 5) -- active + else + unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle + end else - unit.boiler_ps_tbl[id].publish("computed_status", 2) -- idle + 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) end else - unit.boiler_ps_tbl[id].publish("computed_status", 5) -- 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) + log.debug(util.c(log_header, "invalid boiler id ", id)) end end else @@ -359,36 +489,40 @@ function iocontrol.update_statuses(statuses) end for id, turbine in pairs(rtu_statuses.turbines) do - 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 + 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 data = unit.turbine_data_tbl[id] ---@type turbinev_session_db - unit.turbine_ps_tbl[id].publish("formed", data.formed) - unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted) + unit.turbine_ps_tbl[id].publish("formed", data.formed) + unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted) - if data.formed then - if data.tanks.energy_fill >= 0.99 then - unit.turbine_ps_tbl[id].publish("computed_status", 4) -- trip - elseif rtu_faulted then - unit.turbine_ps_tbl[id].publish("computed_status", 5) -- faulted - elseif data.state.flow_rate < 100 then - unit.turbine_ps_tbl[id].publish("computed_status", 2) -- idle + if data.formed then + if data.tanks.energy_fill >= 0.99 then + unit.turbine_ps_tbl[id].publish("computed_status", 6) -- trip + elseif rtu_faulted then + unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted + elseif data.state.flow_rate < 100 then + unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle + else + unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active + end else - unit.turbine_ps_tbl[id].publish("computed_status", 3) -- active + 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) end else - unit.turbine_ps_tbl[id].publish("computed_status", 6) -- 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) + log.debug(util.c(log_header, "invalid turbine id ", id)) end end else diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 7b63302..a95b861 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local iocontrol = require("coordinator.iocontrol") local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") -local COORDINATOR_VERSION = "beta-v0.7.7" +local COORDINATOR_VERSION = "beta-v0.8.0" local print = util.print local println = util.println @@ -365,4 +365,8 @@ local function main() log.info("exited") end -if not xpcall(main, crash.handler) then crash.exit() end +if not xpcall(main, crash.handler) then + pcall(renderer.close_ui) + pcall(sounder.stop) + crash.exit() +end diff --git a/coordinator/ui/style.lua b/coordinator/ui/style.lua index 509e87a..6fb8f88 100644 --- a/coordinator/ui/style.lua +++ b/coordinator/ui/style.lua @@ -39,6 +39,14 @@ style.reactor = { color = cpair(colors.black, colors.yellow), text = "PLC OFF-LINE" }, + { + color = cpair(colors.black, colors.orange), + text = "NOT FORMED" + }, + { + color = cpair(colors.black, colors.orange), + text = "PLC FAULT" + }, { color = cpair(colors.white, colors.gray), text = "DISABLED" @@ -51,14 +59,6 @@ style.reactor = { color = cpair(colors.black, colors.red), text = "SCRAMMED" }, - { - color = cpair(colors.black, colors.orange), - text = "PLC FAULT" - }, - { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - }, { color = cpair(colors.black, colors.red), text = "FORCE DISABLED" @@ -73,14 +73,6 @@ style.boiler = { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, - { - color = cpair(colors.white, colors.gray), - text = "IDLE" - }, - { - color = cpair(colors.black, colors.green), - text = "ACTIVE" - }, { color = cpair(colors.black, colors.orange), text = "RTU FAULT" @@ -88,6 +80,14 @@ style.boiler = { { color = cpair(colors.black, colors.orange), text = "NOT FORMED" + }, + { + color = cpair(colors.white, colors.gray), + text = "IDLE" + }, + { + color = cpair(colors.black, colors.green), + text = "ACTIVE" } } } @@ -99,6 +99,14 @@ style.turbine = { color = cpair(colors.black, colors.yellow), text = "OFF-LINE" }, + { + color = cpair(colors.black, colors.orange), + text = "NOT FORMED" + }, + { + color = cpair(colors.black, colors.orange), + text = "RTU FAULT" + }, { color = cpair(colors.white, colors.gray), text = "IDLE" @@ -110,15 +118,37 @@ style.turbine = { { color = cpair(colors.black, colors.red), text = "TRIP" + } + } +} + +style.imatrix = { + -- induction matrix states + states = { + { + color = cpair(colors.black, colors.yellow), + text = "OFF-LINE" + }, + { + color = cpair(colors.black, colors.orange), + text = "NOT FORMED" }, { color = cpair(colors.black, colors.orange), text = "RTU FAULT" }, { - color = cpair(colors.black, colors.orange), - text = "NOT FORMED" - } + color = cpair(colors.white, colors.green), + text = "ONLINE" + }, + { + color = cpair(colors.black, colors.yellow), + text = "LOW CHARGE" + }, + { + color = cpair(colors.black, colors.red), + text = "HIGH CHARGE" + }, } } diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 583fd39..5e0dca5 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -14,7 +14,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "beta-v0.9.9" +local R_PLC_VERSION = "beta-v0.9.10" local print = util.print local println = util.println diff --git a/rtu/startup.lua b/rtu/startup.lua index d87b0ff..ec5fc0a 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -25,7 +25,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "beta-v0.9.9" +local RTU_VERSION = "beta-v0.9.10" local rtu_t = types.rtu_t diff --git a/scada-common/comms.lua b/scada-common/comms.lua index f25df22..025a5c9 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -12,7 +12,7 @@ local rtu_t = types.rtu_t local insert = table.insert -comms.version = "1.0.1" +comms.version = "1.1.0" ---@alias PROTOCOLS integer local PROTOCOLS = { @@ -38,7 +38,7 @@ local RPLC_TYPES = { MEK_BURN_RATE = 2, -- set burn rate RPS_ENABLE = 3, -- enable reactor RPS_SCRAM = 4, -- SCRAM reactor (manual request) - RPS_ASCRAM = 5, -- SCRAM reactor (automatic request) + RPS_ASCRAM = 5, -- SCRAM reactor (automatic request) RPS_STATUS = 6, -- RPS status RPS_ALARM = 7, -- RPS alarm broadcast RPS_RESET = 8 -- clear RPS trip (if in bad state, will trip immediately) @@ -62,14 +62,16 @@ local ESTABLISH_ACK = { ---@alias SCADA_CRDN_TYPES integer local SCADA_CRDN_TYPES = { - STRUCT_BUILDS = 0, -- mekanism structure builds - UNIT_STATUSES = 1, -- state of reactor units - COMMAND_UNIT = 2, -- command a reactor unit - ALARM = 3 -- alarm signaling + FAC_BUILDS = 0, -- facility RTU builds + FAC_STATUS = 1, -- state of facility and facility devices + FAC_CMD = 2, -- faility command + UNIT_BUILDS = 3, -- build of each reactor unit (reactor + RTUs) + UNIT_STATUSES = 4, -- state of each of the reactor units + UNIT_CMD = 5 -- command a reactor unit } ----@alias CRDN_COMMANDS integer -local CRDN_COMMANDS = { +---@alias UNIT_COMMANDS integer +local UNIT_COMMANDS = { SCRAM = 0, -- SCRAM the reactor START = 1, -- start the reactor RESET_RPS = 2, -- reset the RPS @@ -102,7 +104,7 @@ comms.RPLC_TYPES = RPLC_TYPES comms.ESTABLISH_ACK = ESTABLISH_ACK comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES comms.SCADA_CRDN_TYPES = SCADA_CRDN_TYPES -comms.CRDN_COMMANDS = CRDN_COMMANDS +comms.UNIT_COMMANDS = UNIT_COMMANDS comms.CAPI_TYPES = CAPI_TYPES comms.RTU_UNIT_TYPES = RTU_UNIT_TYPES @@ -484,10 +486,12 @@ function comms.crdn_packet() -- check that type is known local function _crdn_type_valid() - return self.type == SCADA_CRDN_TYPES.STRUCT_BUILDS or + return self.type == SCADA_CRDN_TYPES.FAC_BUILDS or + self.type == SCADA_CRDN_TYPES.FAC_STATUS or + self.type == SCADA_CRDN_TYPES.FAC_CMD or + self.type == SCADA_CRDN_TYPES.UNIT_BUILDS or self.type == SCADA_CRDN_TYPES.UNIT_STATUSES or - self.type == SCADA_CRDN_TYPES.COMMAND_UNIT or - self.type == SCADA_CRDN_TYPES.ALARM + self.type == SCADA_CRDN_TYPES.UNIT_STATUSES end -- make a coordinator packet diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index 9a5f10f..2ce66df 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -10,7 +10,7 @@ local coordinator = {} local PROTOCOLS = comms.PROTOCOLS local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES -local CRDN_COMMANDS = comms.CRDN_COMMANDS +local UNIT_COMMANDS = comms.UNIT_COMMANDS local SV_Q_CMDS = svqtypes.SV_Q_CMDS local SV_Q_DATA = svqtypes.SV_Q_DATA @@ -44,15 +44,14 @@ local PERIODICS = { ---@param id integer ---@param in_queue mqueue ---@param out_queue mqueue ----@param facility_units table -function coordinator.new_session(id, in_queue, out_queue, facility_units) +---@param facility facility +---@param units table +function coordinator.new_session(id, in_queue, out_queue, facility, units) local log_header = "crdn_session(" .. id .. "): " local self = { - id = id, in_q = in_queue, out_q = out_queue, - units = facility_units, -- connection properties seq_num = 0, r_seq_num = nil, @@ -67,11 +66,13 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) }, -- when to next retry one of these messages retry_times = { - builds_packet = 0 + f_builds_packet = 0, + u_builds_packet = 0 }, -- message acknowledgements acks = { - builds = false + fac_builds = false, + unit_builds = false } } @@ -109,26 +110,41 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) self.seq_num = self.seq_num + 1 end + -- send facility builds + local function _send_fac_builds() + self.acks.fac_builds = false + _send(SCADA_CRDN_TYPES.FAC_BUILDS, facility.get_build()) + end + -- send unit builds - local function _send_builds() - self.acks.builds = false + local function _send_unit_builds() + self.acks.unit_builds = false local builds = {} - for i = 1, #self.units do - local unit = self.units[i] ---@type reactor_unit + for i = 1, #units do + local unit = units[i] ---@type reactor_unit builds[unit.get_id()] = unit.get_build() end - _send(SCADA_CRDN_TYPES.STRUCT_BUILDS, builds) + _send(SCADA_CRDN_TYPES.UNIT_BUILDS, builds) + end + + -- send facility status + local function _send_fac_status() + local status = { + facility.get_rtu_statuses() + } + + _send(SCADA_CRDN_TYPES.FAC_STATUS, status) end -- send unit statuses - local function _send_status() + local function _send_unit_statuses() local status = {} - for i = 1, #self.units do - local unit = self.units[i] ---@type reactor_unit + for i = 1, #units do + local unit = units[i] ---@type reactor_unit status[unit.get_id()] = { unit.get_reactor_status(), unit.get_rtu_statuses(), @@ -183,10 +199,13 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) end elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_CRDN then - if pkt.type == SCADA_CRDN_TYPES.STRUCT_BUILDS then + if pkt.type == SCADA_CRDN_TYPES.FAC_BUILDS then -- acknowledgement to coordinator receiving builds - self.acks.builds = true - elseif pkt.type == SCADA_CRDN_TYPES.COMMAND_UNIT then + self.acks.fac_builds = true + elseif pkt.type == SCADA_CRDN_TYPES.UNIT_BUILDS then + -- acknowledgement to coordinator receiving builds + self.acks.unit_builds = true + elseif pkt.type == SCADA_CRDN_TYPES.UNIT_CMD then if pkt.length >= 2 then -- get command and unit id local cmd = pkt.data[1] @@ -196,37 +215,37 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) local data = { uid, pkt.data[3] } -- continue if valid unit id - if util.is_int(uid) and uid > 0 and uid <= #self.units then - local unit = self.units[uid] ---@type reactor_unit + if util.is_int(uid) and uid > 0 and uid <= #units then + local unit = units[uid] ---@type reactor_unit - if cmd == CRDN_COMMANDS.START then + if cmd == UNIT_COMMANDS.START then self.out_q.push_data(SV_Q_DATA.START, data) - elseif cmd == CRDN_COMMANDS.SCRAM then + elseif cmd == UNIT_COMMANDS.SCRAM then self.out_q.push_data(SV_Q_DATA.SCRAM, data) - elseif cmd == CRDN_COMMANDS.RESET_RPS then + elseif cmd == UNIT_COMMANDS.RESET_RPS then self.out_q.push_data(SV_Q_DATA.RESET_RPS, data) - elseif cmd == CRDN_COMMANDS.SET_BURN then + elseif cmd == UNIT_COMMANDS.SET_BURN then if pkt.length == 3 then self.out_q.push_data(SV_Q_DATA.SET_BURN, data) else log.debug(log_header .. "CRDN command unit burn rate missing option") end - elseif cmd == CRDN_COMMANDS.SET_WASTE then + elseif cmd == UNIT_COMMANDS.SET_WASTE then if pkt.length == 3 then unit.set_waste(pkt.data[3]) else log.debug(log_header .. "CRDN command unit set waste missing option") end - elseif cmd == CRDN_COMMANDS.ACK_ALL_ALARMS then + elseif cmd == UNIT_COMMANDS.ACK_ALL_ALARMS then unit.ack_all() - _send(SCADA_CRDN_TYPES.COMMAND_UNIT, { cmd, uid, true }) - elseif cmd == CRDN_COMMANDS.ACK_ALARM then + _send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, true }) + elseif cmd == UNIT_COMMANDS.ACK_ALARM then if pkt.length == 3 then unit.ack_alarm(pkt.data[3]) else log.debug(log_header .. "CRDN command unit ack alarm missing id") end - elseif cmd == CRDN_COMMANDS.RESET_ALARM then + elseif cmd == UNIT_COMMANDS.RESET_ALARM then if pkt.length == 3 then unit.reset_alarm(pkt.data[3]) else @@ -251,7 +270,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) local public = {} -- get the session ID - function public.get_id() return self.id end + function public.get_id() return id end -- check if a timer matches this session's watchdog function public.check_wd(timer) @@ -262,7 +281,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) function public.close() _close() _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) - println("connection to coordinator " .. self.id .. " closed by server") + println("connection to coordinator " .. id .. " closed by server") log.info(log_header .. "session closed by server") end @@ -289,9 +308,9 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) local cmd = message.message if cmd == CRD_S_CMDS.RESEND_BUILDS then -- re-send builds - self.acks.builds = false self.retry_times.builds_packet = util.time() + RETRY_PERIOD - _send_builds() + _send_fac_builds() + _send_unit_builds() end elseif message.qtype == mqueue.TYPE.DATA then -- instruction with body @@ -299,7 +318,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) if cmd.key == CRD_S_DATA.CMD_ACK then local ack = cmd.val ---@type coord_ack - _send(SCADA_CRDN_TYPES.COMMAND_UNIT, { ack.cmd, ack.unit, ack.ack }) + _send(SCADA_CRDN_TYPES.UNIT_CMD, { ack.cmd, ack.unit, ack.ack }) end end end @@ -313,7 +332,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) -- exit if connection was closed if not self.connected then - println("connection to coordinator " .. self.id .. " closed by remote host") + println("connection to coordinator " .. id .. " closed by remote host") log.info(log_header .. "session closed by remote host") return self.connected end @@ -334,11 +353,12 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) periodics.keep_alive = 0 end - -- unit statuses to coordinator + -- statuses to coordinator periodics.status_packet = periodics.status_packet + elapsed if periodics.status_packet >= PERIODICS.STATUS then - _send_status() + _send_fac_status() + _send_unit_statuses() periodics.status_packet = 0 end @@ -350,12 +370,19 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) local rtimes = self.retry_times - -- builds packet retry + -- builds packet retries - if not self.acks.builds then - if rtimes.builds_packet - util.time() <= 0 then - _send_builds() - rtimes.builds_packet = util.time() + RETRY_PERIOD + if not self.acks.fac_builds then + if rtimes.f_builds_packet - util.time() <= 0 then + _send_fac_builds() + rtimes.f_builds_packet = util.time() + RETRY_PERIOD + end + end + + if not self.acks.unit_builds then + if rtimes.u_builds_packet - util.time() <= 0 then + _send_unit_builds() + rtimes.u_builds_packet = util.time() + RETRY_PERIOD end end end diff --git a/supervisor/session/facility.lua b/supervisor/session/facility.lua new file mode 100644 index 0000000..454606c --- /dev/null +++ b/supervisor/session/facility.lua @@ -0,0 +1,100 @@ +local log = require("scada-common.log") +local rsio = require("scada-common.rsio") +local util = require("scada-common.util") + +local rsctl = require("supervisor.session.rsctl") + +---@class facility_management +local facility = {} + +-- create a new facility management object +function facility.new() + local self = { + induction = {}, + redstone = {} + } + + -- init redstone RTU I/O controller + local rs_rtu_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 + + -- PUBLIC FUNCTIONS -- + + ---@class facility + local public = {} + + -- ADD/LINK DEVICES -- + + -- link a redstone RTU session + ---@param rs_unit unit_session + function public.add_redstone(rs_unit) + table.insert(self.redstone, rs_unit) + end + + -- link an imatrix RTU session + ---@param imatrix unit_session + function public.add_imatrix(imatrix) + table.insert(self.induction, imatrix) + 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) + end + + -- UPDATE -- + + -- update (iterate) the facility management + function public.update() + -- unlink RTU unit sessions if they are closed + _unlink_disconnected_units(self.induction) + _unlink_disconnected_units(self.redstone) + end + + -- READ STATES/PROPERTIES -- + + -- get build properties of all machines + function public.get_build() + local build = {} + + build.induction = {} + for i = 1, #self.induction do + local matrix = self.induction[i] ---@type unit_session + build.induction[matrix.get_device_idx()] = { matrix.get_db().formed, matrix.get_db().build } + end + + return build + end + + -- get RTU statuses + function public.get_rtu_statuses() + local status = {} + + -- 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 + } + end + + ---@todo other RTU statuses + + return status + end + + return public +end + +return facility diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index ec821ff..316a584 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -11,7 +11,7 @@ local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES -local CRDN_COMMANDS = comms.CRDN_COMMANDS +local UNIT_COMMANDS = comms.UNIT_COMMANDS local print = util.print local println = util.println @@ -352,7 +352,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) -- send acknowledgement to coordinator self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { unit = self.for_reactor, - cmd = CRDN_COMMANDS.SET_BURN, + cmd = UNIT_COMMANDS.SET_BURN, ack = ack }) elseif pkt.type == RPLC_TYPES.RPS_ENABLE then @@ -367,7 +367,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) -- send acknowledgement to coordinator self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { unit = self.for_reactor, - cmd = CRDN_COMMANDS.START, + cmd = UNIT_COMMANDS.START, ack = ack }) elseif pkt.type == RPLC_TYPES.RPS_SCRAM then @@ -383,7 +383,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) -- send acknowledgement to coordinator self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { unit = self.for_reactor, - cmd = CRDN_COMMANDS.SCRAM, + cmd = UNIT_COMMANDS.SCRAM, ack = ack }) elseif pkt.type == RPLC_TYPES.RPS_ASCRAM then @@ -435,7 +435,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) -- send acknowledgement to coordinator self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { unit = self.for_reactor, - cmd = CRDN_COMMANDS.RESET_RPS, + cmd = UNIT_COMMANDS.RESET_RPS, ack = ack }) else diff --git a/supervisor/session/rsctl.lua b/supervisor/session/rsctl.lua new file mode 100644 index 0000000..1544cc3 --- /dev/null +++ b/supervisor/session/rsctl.lua @@ -0,0 +1,39 @@ +-- +-- Redstone RTU Session I/O Controller +-- + +local rsctl = {} + +-- create a new redstone RTU I/O controller +---@param redstone_rtus table redstone RTU sessions +function rsctl.new(redstone_rtus) + ---@class rs_controller + local public = {} + + -- write to a digital redstone port (applies to all RTUs) + ---@param port IO_PORT + ---@param value boolean + function public.digital_write(port, value) + for i = 1, #redstone_rtus do + local db = redstone_rtus[i].get_db() ---@type redstone_session_db + local io = db.io[port] ---@type rs_db_dig_io|nil + if io ~= nil then io.write(value) end + end + end + + -- read a digital redstone port
+ -- this will read from the first one encountered if there are multiple, because there should not be multiple + ---@param port IO_PORT + ---@return boolean|nil + function public.digital_read(port) + for i = 1, #redstone_rtus do + local db = redstone_rtus[i].get_db() ---@type redstone_session_db + local io = db.io[port] ---@type rs_db_dig_io|nil + if io ~= nil then return io.read() end + end + end + + return public +end + +return rsctl diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 922112f..0d5bc1d 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -36,16 +36,15 @@ local PERIODICS = { ---@param in_queue mqueue ---@param out_queue mqueue ---@param advertisement table +---@param facility facility ---@param facility_units table -function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units) +function rtu.new_session(id, in_queue, out_queue, advertisement, facility, facility_units) local log_header = "rtu_session(" .. id .. "): " local self = { - id = id, in_q = in_queue, out_q = out_queue, modbus_q = mqueue.new(), - f_units = facility_units, advert = advertisement, -- connection properties seq_num = 0, @@ -72,9 +71,10 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units) local function _handle_advertisement() _reset_config() - for i = 1, #self.f_units do - local unit = self.f_units[i] ---@type reactor_unit - unit.purge_rtu_devices(self.id) + for i = 1, #facility_units do + local unit = facility_units[i] ---@type reactor_unit + unit.purge_rtu_devices(id) + facility.purge_rtu_devices(id) end for i = 1, #self.advert do @@ -104,47 +104,61 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units) if advert_validator.valid() then advert_validator.assert_min(unit_advert.index, 1) - advert_validator.assert_min(unit_advert.reactor, 1) - advert_validator.assert_max(unit_advert.reactor, #self.f_units) + advert_validator.assert_min(unit_advert.reactor, 0) + advert_validator.assert_max(unit_advert.reactor, #facility_units) if not advert_validator.valid() then u_type = false end else u_type = false end + local type_string = util.strval(u_type) + if type(u_type) == "number" then type_string = util.strval(comms.advert_type_to_rtu_t(u_type)) end + -- create unit by type if u_type == false then -- validation fail log.debug(log_header .. "advertisement unit validation failure") else - local target_unit = self.f_units[unit_advert.reactor] ---@type reactor_unit + if unit_advert.reactor > 0 then + local target_unit = facility_units[unit_advert.reactor] ---@type reactor_unit - if u_type == RTU_UNIT_TYPES.REDSTONE then - -- redstone - unit = svrs_redstone.new(self.id, i, unit_advert, self.modbus_q) - if type(unit) ~= "nil" then target_unit.add_redstone(unit) end - elseif u_type == RTU_UNIT_TYPES.BOILER_VALVE then - -- boiler (Mekanism 10.1+) - unit = svrs_boilerv.new(self.id, i, unit_advert, self.modbus_q) - if type(unit) ~= "nil" then target_unit.add_boiler(unit) end - elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then - -- turbine (Mekanism 10.1+) - unit = svrs_turbinev.new(self.id, i, unit_advert, self.modbus_q) - if type(unit) ~= "nil" then target_unit.add_turbine(unit) end - elseif u_type == RTU_UNIT_TYPES.IMATRIX then - -- induction matrix - unit = svrs_imatrix.new(self.id, i, unit_advert, self.modbus_q) - elseif u_type == RTU_UNIT_TYPES.SPS then - -- super-critical phase shifter - unit = svrs_sps.new(self.id, i, unit_advert, self.modbus_q) - elseif u_type == RTU_UNIT_TYPES.SNA then - -- solar neutron activator - unit = svrs_sna.new(self.id, i, unit_advert, self.modbus_q) - elseif u_type == RTU_UNIT_TYPES.ENV_DETECTOR then - -- environment detector - unit = svrs_envd.new(self.id, i, unit_advert, self.modbus_q) + if u_type == RTU_UNIT_TYPES.REDSTONE then + -- redstone + unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q) + if type(unit) ~= "nil" then target_unit.add_redstone(unit) end + elseif u_type == RTU_UNIT_TYPES.BOILER_VALVE then + -- boiler (Mekanism 10.1+) + unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q) + if type(unit) ~= "nil" then target_unit.add_boiler(unit) end + elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then + -- turbine (Mekanism 10.1+) + unit = svrs_turbinev.new(id, i, unit_advert, self.modbus_q) + if type(unit) ~= "nil" then target_unit.add_turbine(unit) end + else + log.error(util.c(log_header, "bad advertisement: encountered unsupported reactor-specific RTU type ", type_string)) + end else - log.error(log_header .. "bad advertisement: encountered unsupported RTU type") + if u_type == RTU_UNIT_TYPES.REDSTONE then + -- redstone + unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q) + if type(unit) ~= "nil" then facility.add_redstone(unit) end + elseif u_type == RTU_UNIT_TYPES.IMATRIX then + -- induction matrix + unit = svrs_imatrix.new(id, i, unit_advert, self.modbus_q) + if type(unit) ~= "nil" then facility.add_imatrix(unit) end + elseif u_type == RTU_UNIT_TYPES.SPS then + -- super-critical phase shifter + unit = svrs_sps.new(id, i, unit_advert, self.modbus_q) + elseif u_type == RTU_UNIT_TYPES.SNA then + -- solar neutron activator + unit = svrs_sna.new(id, i, unit_advert, self.modbus_q) + elseif u_type == RTU_UNIT_TYPES.ENV_DETECTOR then + -- environment detector + unit = svrs_envd.new(id, i, unit_advert, self.modbus_q) + else + log.error(util.c(log_header, "bad advertisement: encountered unsupported reactor-independent RTU type ", type_string)) + end end end @@ -152,10 +166,7 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units) table.insert(self.units, unit) else _reset_config() - if type(u_type) == "number" then - local type_string = util.strval(comms.advert_type_to_rtu_t(u_type)) - log.error(log_header .. "bad advertisement: error occured while creating a unit (type is " .. type_string .. ")") - end + log.error(util.c(log_header, "bad advertisement: error occured while creating a unit (type is ", type_string, ")")) break end end @@ -271,7 +282,7 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units) -- PUBLIC FUNCTIONS -- -- get the session ID - function public.get_id() return self.id end + function public.get_id() return id end -- check if a timer matches this session's watchdog ---@param timer number diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 80ea0d6..91b4873 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -2,6 +2,7 @@ local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local util = require("scada-common.util") +local facility = require("supervisor.session.facility") local svqtypes = require("supervisor.session.svqtypes") local unit = require("supervisor.session.unit") @@ -32,7 +33,8 @@ svsessions.SESSION_TYPE = SESSION_TYPE local self = { modem = nil, num_reactors = 0, - facility_units = {}, + facility = facility.new(), + units = {}, rtu_sessions = {}, plc_sessions = {}, coord_sessions = {}, @@ -197,10 +199,10 @@ end function svsessions.init(modem, num_reactors, cooling_conf) self.modem = modem self.num_reactors = num_reactors - self.facility_units = {} + self.units = {} for i = 1, self.num_reactors do - table.insert(self.facility_units, unit.new(i, cooling_conf[i].BOILERS, cooling_conf[i].TURBINES)) + table.insert(self.units, unit.new(i, cooling_conf[i].BOILERS, cooling_conf[i].TURBINES)) end end @@ -297,7 +299,7 @@ function svsessions.establish_plc_session(local_port, remote_port, for_reactor, plc_s.instance = plc.new_session(self.next_plc_id, for_reactor, plc_s.in_queue, plc_s.out_queue) table.insert(self.plc_sessions, plc_s) - self.facility_units[for_reactor].link_plc_session(plc_s) + self.units[for_reactor].link_plc_session(plc_s) log.debug("established new PLC session to " .. remote_port .. " with ID " .. self.next_plc_id) @@ -330,7 +332,7 @@ function svsessions.establish_rtu_session(local_port, remote_port, advertisement instance = nil ---@type rtu_session } - rtu_s.instance = rtu.new_session(self.next_rtu_id, rtu_s.in_queue, rtu_s.out_queue, advertisement, self.facility_units) + rtu_s.instance = rtu.new_session(self.next_rtu_id, rtu_s.in_queue, rtu_s.out_queue, advertisement, self.facility, self.units) table.insert(self.rtu_sessions, rtu_s) log.debug("established new RTU session to " .. remote_port .. " with ID " .. self.next_rtu_id) @@ -360,7 +362,7 @@ function svsessions.establish_coord_session(local_port, remote_port, version) instance = nil ---@type coord_session } - coord_s.instance = coordinator.new_session(self.next_coord_id, coord_s.in_queue, coord_s.out_queue, self.facility_units) + coord_s.instance = coordinator.new_session(self.next_coord_id, coord_s.in_queue, coord_s.out_queue, self.facility, self.units) table.insert(self.coord_sessions, coord_s) log.debug("established new coordinator session to " .. remote_port .. " with ID " .. self.next_coord_id) @@ -399,9 +401,12 @@ function svsessions.iterate_all() -- iterate coordinator sessions _iterate(self.coord_sessions) + -- iterate facility + self.facility.update() + -- iterate units - for i = 1, #self.facility_units do - local u = self.facility_units[i] ---@type reactor_unit + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit u.update() end end diff --git a/supervisor/session/unit.lua b/supervisor/session/unit.lua index b0d0fea..3c378e6 100644 --- a/supervisor/session/unit.lua +++ b/supervisor/session/unit.lua @@ -1,9 +1,9 @@ -local log = require("scada-common.log") -local rsio = require("scada-common.rsio") -local types = require("scada-common.types") -local util = require("scada-common.util") +local log = require("scada-common.log") +local rsio = require("scada-common.rsio") +local types = require("scada-common.types") +local util = require("scada-common.util") -local qtypes = require("supervisor.session.rtu.qtypes") +local rsctl = require("supervisor.session.rsctl") ---@class reactor_control_unit local unit = {} @@ -178,6 +178,9 @@ function unit.new(for_reactor, num_boilers, num_turbines) } } + -- init redstone RTU I/O controller + local rs_rtu_io_ctl = rsctl.new(self.redstone) + -- init boiler table fields for _ = 1, num_boilers do table.insert(self.db.annunciator.BoilerOnline, false) @@ -192,9 +195,6 @@ function unit.new(for_reactor, num_boilers, num_turbines) table.insert(self.db.annunciator.TurbineTrip, false) end - ---@class reactor_unit - local public = {} - -- PRIVATE FUNCTIONS -- --#region time derivative utility functions @@ -237,26 +237,8 @@ function unit.new(for_reactor, num_boilers, num_turbines) --#region redstone I/O - -- write to a redstone port - local function __rs_w(port, value) - for i = 1, #self.redstone do - local db = self.redstone[i].get_db() ---@type redstone_session_db - local io = db.io[port] ---@type rs_db_dig_io|nil - if io ~= nil then io.write(value) end - end - end - - -- read a redstone port
- -- this will read from the first one encountered if there are multiple, because there should not be multiple - ---@param port IO_PORT - ---@return boolean|nil - local function __rs_r(port) - for i = 1, #self.redstone do - local db = self.redstone[i].get_db() ---@type redstone_session_db - local io = db.io[port] ---@type rs_db_dig_io|nil - if io ~= nil then return io.read() end - end - end + local __rs_w = rs_rtu_io_ctl.digital_write + local __rs_r = rs_rtu_io_ctl.digital_read -- waste valves local waste_pu = { open = function () __rs_w(IO.WASTE_PU, true) end, close = function () __rs_w(IO.WASTE_PU, false) end } @@ -702,6 +684,9 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- PUBLIC FUNCTIONS -- + ---@class reactor_unit + local public = {} + -- ADD/LINK DEVICES -- -- link the PLC @@ -722,7 +707,6 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- link a redstone RTU session ---@param rs_unit unit_session function public.add_redstone(rs_unit) - -- insert into list table.insert(self.redstone, rs_unit) end @@ -781,6 +765,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- unlink RTU unit sessions if they are closed _unlink_disconnected_units(self.boilers) _unlink_disconnected_units(self.turbines) + _unlink_disconnected_units(self.redstone) -- update annunciator logic _update_annunciator() diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 96ee10e..4467a3b 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "beta-v0.8.5" +local SUPERVISOR_VERSION = "beta-v0.9.0" local print = util.print local println = util.println