From df67795239657cf1bed903d343bd5b67ff0e57d4 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 29 Jul 2023 18:16:59 -0400 Subject: [PATCH] #290 pocket page management and alarm test tool, supervisor pocket diagnostics system --- graphics/core.lua | 2 +- graphics/elements/controls/app.lua | 130 +++++++++++++++ pocket/coreio.lua | 35 ---- pocket/iocontrol.lua | 106 ++++++++++++ pocket/pocket.lua | 249 +++++++++++++++++------------ pocket/startup.lua | 39 +++-- pocket/ui/main.lua | 52 ++++-- pocket/ui/pages/diag_page.lua | 149 +++++++++++++++++ pocket/ui/pages/home_page.lua | 17 +- scada-common/comms.lua | 130 ++++++++------- scada-common/network.lua | 1 + supervisor/facility.lua | 124 ++++++++++++-- supervisor/session/pocket.lua | 52 +++++- supervisor/session/svsessions.lua | 3 +- supervisor/startup.lua | 2 +- supervisor/unit.lua | 17 ++ 16 files changed, 858 insertions(+), 250 deletions(-) create mode 100644 graphics/elements/controls/app.lua delete mode 100644 pocket/coreio.lua create mode 100644 pocket/iocontrol.lua create mode 100644 pocket/ui/pages/diag_page.lua diff --git a/graphics/core.lua b/graphics/core.lua index 5abc28d..4355b76 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -7,7 +7,7 @@ local flasher = require("graphics.flasher") local core = {} -core.version = "1.0.3" +core.version = "1.1.0" core.flasher = flasher core.events = events diff --git a/graphics/elements/controls/app.lua b/graphics/elements/controls/app.lua new file mode 100644 index 0000000..ae831da --- /dev/null +++ b/graphics/elements/controls/app.lua @@ -0,0 +1,130 @@ +-- Button Graphics Element + +local tcd = require("scada-common.tcd") + +local core = require("graphics.core") +local element = require("graphics.element") + +local CLICK_TYPE = core.events.CLICK_TYPE + +---@class app_button_args +---@field text string app icon text +---@field title string app title text +---@field callback function function to call on touch +---@field app_fg_bg cpair app icon foreground/background colors +---@field active_fg_bg? cpair foreground/background colors when pressed +---@field parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer auto incremented if omitted +---@field fg_bg? cpair foreground/background colors +---@field hidden? boolean true to hide on initial draw + +-- new app button +---@param args app_button_args +---@return graphics_element element, element_id id +local function app_button(args) + assert(type(args.text) == "string", "graphics.elements.controls.app: text is a required field") + assert(type(args.title) == "string", "graphics.elements.controls.app: title is a required field") + assert(type(args.callback) == "function", "graphics.elements.controls.app: callback is a required field") + assert(type(args.app_fg_bg) == "table", "graphics.elements.controls.app: app_fg_bg is a required field") + + args.height = 4 + args.width = 5 + + -- create new graphics element base object + local e = element.new(args) + + -- write app title, centered + e.window.setCursorPos(1, 4) + e.window.setCursorPos(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4) + e.window.write(args.title) + + -- draw the button + local function draw() + local fgd = args.app_fg_bg.fgd + local bkg = args.app_fg_bg.bkg + + if e.value then + fgd = args.active_fg_bg.fgd + bkg = args.active_fg_bg.bkg + end + + -- draw icon + e.window.setCursorPos(1, 1) + e.window.setTextColor(fgd) + e.window.setBackgroundColor(bkg) + e.window.write("\x9f\x83\x83\x83") + e.window.setTextColor(bkg) + e.window.setBackgroundColor(fgd) + e.window.write("\x90") + e.window.setTextColor(fgd) + e.window.setBackgroundColor(bkg) + e.window.setCursorPos(1, 2) + e.window.write("\x95 ") + e.window.setTextColor(bkg) + e.window.setBackgroundColor(fgd) + e.window.write("\x95") + e.window.setCursorPos(1, 3) + e.window.write("\x82\x8f\x8f\x8f\x81") + + -- write the icon text + e.window.setCursorPos(3, 2) + e.window.setTextColor(fgd) + e.window.setBackgroundColor(bkg) + e.window.write(args.text) + end + + -- draw the button as pressed (if active_fg_bg set) + local function show_pressed() + if e.enabled and args.active_fg_bg ~= nil then + e.value = true + e.window.setTextColor(args.active_fg_bg.fgd) + e.window.setBackgroundColor(args.active_fg_bg.bkg) + draw() + end + end + + -- draw the button as unpressed (if active_fg_bg set) + local function show_unpressed() + if e.enabled and args.active_fg_bg ~= nil then + e.value = false + e.window.setTextColor(e.fg_bg.fgd) + e.window.setBackgroundColor(e.fg_bg.bkg) + draw() + end + end + + -- handle mouse interaction + ---@param event mouse_interaction mouse event + function e.handle_mouse(event) + if e.enabled then + if event.type == CLICK_TYPE.TAP then + show_pressed() + -- show as unpressed in 0.25 seconds + if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end + args.callback() + elseif event.type == CLICK_TYPE.DOWN then + show_pressed() + elseif event.type == CLICK_TYPE.UP then + show_unpressed() + if e.in_frame_bounds(event.current.x, event.current.y) then + args.callback() + end + end + end + end + + -- set the value (true simulates pressing the button) + ---@param val boolean new value + function e.set_value(val) + if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end + end + + -- initial draw + draw() + + return e.complete() +end + +return app_button diff --git a/pocket/coreio.lua b/pocket/coreio.lua deleted file mode 100644 index 6f43dfd..0000000 --- a/pocket/coreio.lua +++ /dev/null @@ -1,35 +0,0 @@ --- --- Core I/O - Pocket Central I/O Management --- - -local psil = require("scada-common.psil") - -local coreio = {} - ----@class pocket_core_io -local io = { - ps = psil.create() -} - ----@enum POCKET_LINK_STATE -local LINK_STATE = { - UNLINKED = 0, - SV_LINK_ONLY = 1, - API_LINK_ONLY = 2, - LINKED = 3 -} - -coreio.LINK_STATE = LINK_STATE - --- get the core PSIL -function coreio.core_ps() - return io.ps -end - --- set network link state ----@param state POCKET_LINK_STATE -function coreio.report_link_state(state) - io.ps.publish("link_state", state) -end - -return coreio diff --git a/pocket/iocontrol.lua b/pocket/iocontrol.lua new file mode 100644 index 0000000..1bf3046 --- /dev/null +++ b/pocket/iocontrol.lua @@ -0,0 +1,106 @@ +-- +-- I/O Control for Pocket Integration with Supervisor & Coordinator +-- + +local psil = require("scada-common.psil") + +local types = require("scada-common.types") + +local ALARM = types.ALARM + +local iocontrol = {} + +---@class pocket_ioctl +local io = { + ps = psil.create() +} + +---@enum POCKET_LINK_STATE +local LINK_STATE = { + UNLINKED = 0, + SV_LINK_ONLY = 1, + API_LINK_ONLY = 2, + LINKED = 3 +} + +---@enum NAV_PAGE +local NAV_PAGE = { + HOME = 1, + UNITS = 2, + REACTORS = 3, + BOILERS = 4, + TURBINES = 5, + DIAG = 6, + D_ALARMS = 7 +} + +iocontrol.LINK_STATE = LINK_STATE +iocontrol.NAV_PAGE = NAV_PAGE + +-- initialize facility-independent components of pocket iocontrol +---@param comms pocket_comms +function iocontrol.init_core(comms) + ---@class pocket_ioctl_diag + io.diag = {} + + -- alarm testing + io.diag.tone_test = { + test_1 = function (state) comms.diag__set_alarm_tone(1, state) end, + test_2 = function (state) comms.diag__set_alarm_tone(2, state) end, + test_3 = function (state) comms.diag__set_alarm_tone(3, state) end, + test_4 = function (state) comms.diag__set_alarm_tone(4, state) end, + test_5 = function (state) comms.diag__set_alarm_tone(5, state) end, + test_6 = function (state) comms.diag__set_alarm_tone(6, state) end, + test_7 = function (state) comms.diag__set_alarm_tone(7, state) end, + test_8 = function (state) comms.diag__set_alarm_tone(8, state) end, + stop_tones = function () comms.diag__set_alarm_tone(0, false) end, + + test_breach = function (state) comms.diag__set_alarm(ALARM.ContainmentBreach, state) end, + test_rad = function (state) comms.diag__set_alarm(ALARM.ContainmentRadiation, state) end, + test_lost = function (state) comms.diag__set_alarm(ALARM.ReactorLost, state) end, + test_crit = function (state) comms.diag__set_alarm(ALARM.CriticalDamage, state) end, + test_dmg = function (state) comms.diag__set_alarm(ALARM.ReactorDamage, state) end, + test_overtemp = function (state) comms.diag__set_alarm(ALARM.ReactorOverTemp, state) end, + test_hightemp = function (state) comms.diag__set_alarm(ALARM.ReactorHighTemp, state) end, + test_wasteleak = function (state) comms.diag__set_alarm(ALARM.ReactorWasteLeak, state) end, + test_highwaste = function (state) comms.diag__set_alarm(ALARM.ReactorHighWaste, state) end, + test_rps = function (state) comms.diag__set_alarm(ALARM.RPSTransient, state) end, + test_rcs = function (state) comms.diag__set_alarm(ALARM.RCSTransient, state) end, + test_turbinet = function (state) comms.diag__set_alarm(ALARM.TurbineTrip, state) end, + stop_alarms = function () comms.diag__set_alarm(0, false) end, + + get_tone_states = function () comms.diag__get_alarm_tones() end, + + ready_warn = nil, ---@type graphics_element + tone_buttons = {}, + alarm_buttons = {}, + tone_indicators = {} -- indicators to update from supervisor tone states + } + + ---@class pocket_nav + io.nav = { + page = NAV_PAGE.HOME, ---@type NAV_PAGE + sub_pages = { NAV_PAGE.HOME, NAV_PAGE.UNITS, NAV_PAGE.REACTORS, NAV_PAGE.BOILERS, NAV_PAGE.TURBINES, NAV_PAGE.DIAG }, + tasks = {} + } + + -- add a task to be performed periodically while on a given page + ---@param page NAV_PAGE page to add task to + ---@param task function function to execute + function io.nav.register_task(page, task) + if io.nav.tasks[page] == nil then io.nav.tasks[page] = {} end + table.insert(io.nav.tasks[page], task) + end +end + +-- initialize facility-dependent components of pocket iocontrol +function iocontrol.init_fac() end + +-- set network link state +---@param state POCKET_LINK_STATE +function iocontrol.report_link_state(state) io.ps.publish("link_state", state) end + +-- get the IO controller database +function iocontrol.get_db() return io end + +return iocontrol diff --git a/pocket/pocket.lua b/pocket/pocket.lua index b0432cf..950f8f7 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -1,16 +1,15 @@ -local comms = require("scada-common.comms") -local log = require("scada-common.log") -local util = require("scada-common.util") +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local util = require("scada-common.util") -local coreio = require("pocket.coreio") +local iocontrol = require("pocket.iocontrol") local PROTOCOL = comms.PROTOCOL local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE --- local CAPI_TYPE = comms.CAPI_TYPE -local LINK_STATE = coreio.LINK_STATE +local LINK_STATE = iocontrol.LINK_STATE local pocket = {} @@ -79,20 +78,6 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range self.api.seq_num = self.api.seq_num + 1 end - -- send a packet to the coordinator API - -----@param msg_type CAPI_TYPE - -----@param msg table - -- local function _send_api(msg_type, msg) - -- local s_pkt = comms.scada_packet() - -- local pkt = comms.capi_packet() - - -- pkt.make(msg_type, msg) - -- s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.COORD_API, pkt.raw_sendable()) - - -- nic.transmit(crd_channel, pkt_channel, s_pkt) - -- self.api.seq_num = self.api.seq_num + 1 - -- end - -- attempt supervisor connection establishment local function _send_sv_establish() _send_sv(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) @@ -147,7 +132,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range -- attempt to re-link if any of the dependent links aren't active function public.link_update() if not self.sv.linked then - coreio.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED)) + iocontrol.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED)) if self.establish_delay_counter <= 0 then _send_sv_establish() @@ -156,7 +141,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range self.establish_delay_counter = self.establish_delay_counter - 1 end elseif not self.api.linked then - coreio.report_link_state(LINK_STATE.SV_LINK_ONLY) + iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY) if self.establish_delay_counter <= 0 then _send_api_establish() @@ -166,10 +151,29 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range end else -- linked, all good! - coreio.report_link_state(LINK_STATE.LINKED) + iocontrol.report_link_state(LINK_STATE.LINKED) end end + -- supervisor test alarm tones by tone + ---@param id tone_id|0 tone ID, or 0 to stop all + ---@param state boolean tone state + function public.diag__set_alarm_tone(id, state) + if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_TONE_SET, { id, state }) end + end + + -- supervisor get active alarm tones + function public.diag__get_alarm_tones() + if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_TONE_GET, {}) end + end + + -- supervisor test alarm tones by alarm + ---@param id ALARM|0 alarm ID, 0 to stop all + ---@param state boolean alarm state + function public.diag__set_alarm(id, state) + if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end + end + -- parse a packet ---@param side string ---@param sender integer @@ -205,6 +209,8 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range -- handle a packet ---@param packet mgmt_frame|capi_frame|nil function public.handle_packet(packet) + local diag = iocontrol.get_db().diag + if packet ~= nil then local l_chan = packet.scada_frame.local_channel() local r_chan = packet.scada_frame.remote_channel() @@ -231,47 +237,9 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range -- feed watchdog on valid sequence number api_watchdog.feed() - if protocol == PROTOCOL.COORD_API then - ---@cast packet capi_frame - elseif protocol == PROTOCOL.SCADA_MGMT then + if protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame - if packet.type == SCADA_MGMT_TYPE.ESTABLISH then - -- connection with coordinator established - if packet.length == 1 then - local est_ack = packet.data[1] - - if est_ack == ESTABLISH_ACK.ALLOW then - log.info("coordinator connection established") - self.establish_delay_counter = 0 - self.api.linked = true - self.api.addr = src_addr - - if self.sv.linked then - coreio.report_link_state(LINK_STATE.LINKED) - else - coreio.report_link_state(LINK_STATE.API_LINK_ONLY) - end - elseif est_ack == ESTABLISH_ACK.DENY then - if self.api.last_est_ack ~= est_ack then - log.info("coordinator connection denied") - end - elseif est_ack == ESTABLISH_ACK.COLLISION then - if self.api.last_est_ack ~= est_ack then - log.info("coordinator connection denied due to collision") - end - elseif est_ack == ESTABLISH_ACK.BAD_VERSION then - if self.api.last_est_ack ~= est_ack then - log.info("coordinator comms version mismatch") - end - else - log.debug("coordinator SCADA_MGMT establish packet reply unsupported") - end - - self.api.last_est_ack = est_ack - else - log.debug("coordinator SCADA_MGMT establish packet length mismatch") - end - elseif self.api.linked then + if self.api.linked then if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back if packet.length == 1 then @@ -298,6 +266,42 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range else log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator") end + elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + -- connection with coordinator established + if packet.length == 1 then + local est_ack = packet.data[1] + + if est_ack == ESTABLISH_ACK.ALLOW then + log.info("coordinator connection established") + self.establish_delay_counter = 0 + self.api.linked = true + self.api.addr = src_addr + + if self.sv.linked then + iocontrol.report_link_state(LINK_STATE.LINKED) + else + iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY) + end + elseif est_ack == ESTABLISH_ACK.DENY then + if self.api.last_est_ack ~= est_ack then + log.info("coordinator connection denied") + end + elseif est_ack == ESTABLISH_ACK.COLLISION then + if self.api.last_est_ack ~= est_ack then + log.info("coordinator connection denied due to collision") + end + elseif est_ack == ESTABLISH_ACK.BAD_VERSION then + if self.api.last_est_ack ~= est_ack then + log.info("coordinator comms version mismatch") + end + else + log.debug("coordinator SCADA_MGMT establish packet reply unsupported") + end + + self.api.last_est_ack = est_ack + else + log.debug("coordinator SCADA_MGMT establish packet length mismatch") + end else log.debug("discarding coordinator non-link SCADA_MGMT packet before linked") end @@ -325,43 +329,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range -- handle packet if protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame - if packet.type == SCADA_MGMT_TYPE.ESTABLISH then - -- connection with supervisor established - if packet.length == 1 then - local est_ack = packet.data[1] - - if est_ack == ESTABLISH_ACK.ALLOW then - log.info("supervisor connection established") - self.establish_delay_counter = 0 - self.sv.linked = true - self.sv.addr = src_addr - - if self.api.linked then - coreio.report_link_state(LINK_STATE.LINKED) - else - coreio.report_link_state(LINK_STATE.SV_LINK_ONLY) - end - elseif est_ack == ESTABLISH_ACK.DENY then - if self.sv.last_est_ack ~= est_ack then - log.info("supervisor connection denied") - end - elseif est_ack == ESTABLISH_ACK.COLLISION then - if self.sv.last_est_ack ~= est_ack then - log.info("supervisor connection denied due to collision") - end - elseif est_ack == ESTABLISH_ACK.BAD_VERSION then - if self.sv.last_est_ack ~= est_ack then - log.info("supervisor comms version mismatch") - end - else - log.debug("supervisor SCADA_MGMT establish packet reply unsupported") - end - - self.sv.last_est_ack = est_ack - else - log.debug("supervisor SCADA_MGMT establish packet length mismatch") - end - elseif self.sv.linked then + if self.sv.linked then if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back if packet.length == 1 then @@ -385,9 +353,90 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range self.sv.r_seq_num = nil self.sv.addr = comms.BROADCAST log.info("supervisor server connection closed by remote host") + elseif packet.type == SCADA_MGMT_TYPE.DIAG_TONE_GET then + if packet.length == 8 then + for i = 1, #packet.data do + diag.tone_test.tone_indicators[i].update(packet.data[i] == true) + end + else + log.debug("supervisor SCADA diag alarm states packet length mismatch") + end + elseif packet.type == SCADA_MGMT_TYPE.DIAG_TONE_SET then + if packet.length == 1 and packet.data[1] == false then + diag.tone_test.ready_warn.set_value("testing denied") + log.debug("supervisor SCADA diag tone set failed") + elseif packet.length == 2 and type(packet.data[2]) == "table" then + local ready = packet.data[1] + local states = packet.data[2] + + diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready")) + + for i = 1, #states do + if diag.tone_test.tone_buttons[i] ~= nil then + diag.tone_test.tone_buttons[i].set_value(states[i] == true) + diag.tone_test.tone_indicators[i].update(states[i] == true) + end + end + else + log.debug("supervisor SCADA diag tone set packet length/type mismatch") + end + elseif packet.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET then + if packet.length == 1 and packet.data[1] == false then + diag.tone_test.ready_warn.set_value("testing denied") + log.debug("supervisor SCADA diag alarm set failed") + elseif packet.length == 2 and type(packet.data[2]) == "table" then + local ready = packet.data[1] + local states = packet.data[2] + + diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready")) + + for i = 1, #states do + if diag.tone_test.alarm_buttons[i] ~= nil then + diag.tone_test.alarm_buttons[i].set_value(states[i] == true) + end + end + else + log.debug("supervisor SCADA diag alarm set packet length/type mismatch") + end else log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor") end + elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + -- connection with supervisor established + if packet.length == 1 then + local est_ack = packet.data[1] + + if est_ack == ESTABLISH_ACK.ALLOW then + log.info("supervisor connection established") + self.establish_delay_counter = 0 + self.sv.linked = true + self.sv.addr = src_addr + + if self.api.linked then + iocontrol.report_link_state(LINK_STATE.LINKED) + else + iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY) + end + elseif est_ack == ESTABLISH_ACK.DENY then + if self.sv.last_est_ack ~= est_ack then + log.info("supervisor connection denied") + end + elseif est_ack == ESTABLISH_ACK.COLLISION then + if self.sv.last_est_ack ~= est_ack then + log.info("supervisor connection denied due to collision") + end + elseif est_ack == ESTABLISH_ACK.BAD_VERSION then + if self.sv.last_est_ack ~= est_ack then + log.info("supervisor comms version mismatch") + end + else + log.debug("supervisor SCADA_MGMT establish packet reply unsupported") + end + + self.sv.last_est_ack = est_ack + else + log.debug("supervisor SCADA_MGMT establish packet length mismatch") + end else log.debug("discarding supervisor non-link SCADA_MGMT packet before linked") end diff --git a/pocket/startup.lua b/pocket/startup.lua index f8403c1..14ece9e 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -4,21 +4,21 @@ require("/initenv").init_env() -local crash = require("scada-common.crash") -local log = require("scada-common.log") -local network = require("scada-common.network") -local ppm = require("scada-common.ppm") -local tcd = require("scada-common.tcd") -local util = require("scada-common.util") +local crash = require("scada-common.crash") +local log = require("scada-common.log") +local network = require("scada-common.network") +local ppm = require("scada-common.ppm") +local tcd = require("scada-common.tcd") +local util = require("scada-common.util") -local core = require("graphics.core") +local core = require("graphics.core") -local config = require("pocket.config") -local coreio = require("pocket.coreio") -local pocket = require("pocket.pocket") -local renderer = require("pocket.renderer") +local config = require("pocket.config") +local iocontrol = require("pocket.iocontrol") +local pocket = require("pocket.pocket") +local renderer = require("pocket.renderer") -local POCKET_VERSION = "alpha-v0.5.2" +local POCKET_VERSION = "v0.6.0-alpha" local println = util.println local println_ts = util.println_ts @@ -73,7 +73,7 @@ local function main() network.init_mac(config.AUTH_KEY) end - coreio.report_link_state(coreio.LINK_STATE.UNLINKED) + iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED) -- get the communications modem local modem = ppm.get_wireless_modem() @@ -104,6 +104,9 @@ local function main() local MAIN_CLOCK = 0.5 local loop_clock = util.new_clock(MAIN_CLOCK) + -- init I/O control + iocontrol.init_core(pocket_comms) + ---------------------------------------- -- start the UI ---------------------------------------- @@ -128,6 +131,9 @@ local function main() conn_wd.api.feed() log.debug("startup> conn watchdog started") + local io_db = iocontrol.get_db() + local nav = io_db.nav + -- main event loop while true do local event, param1, param2, param3, param4, param5 = util.pull_event() @@ -140,6 +146,13 @@ local function main() -- relink if necessary pocket_comms.link_update() + -- update any tasks for the active page + if (type(nav.tasks[nav.page]) == "table") then + for i = 1, #nav.tasks[nav.page] do + nav.tasks[nav.page][i]() + end + end + loop_clock.start() elseif conn_wd.sv.is_timer(param1) then -- supervisor watchdog timeout diff --git a/pocket/ui/main.lua b/pocket/ui/main.lua index 11331e4..212b5bc 100644 --- a/pocket/ui/main.lua +++ b/pocket/ui/main.lua @@ -2,17 +2,18 @@ -- Pocket GUI Root -- -local coreio = require("pocket.coreio") +local iocontrol = require("pocket.iocontrol") local style = require("pocket.ui.style") local conn_waiting = require("pocket.ui.components.conn_waiting") -local home_page = require("pocket.ui.pages.home_page") -local unit_page = require("pocket.ui.pages.unit_page") -local reactor_page = require("pocket.ui.pages.reactor_page") local boiler_page = require("pocket.ui.pages.boiler_page") +local diag_page = require("pocket.ui.pages.diag_page") +local home_page = require("pocket.ui.pages.home_page") +local reactor_page = require("pocket.ui.pages.reactor_page") local turbine_page = require("pocket.ui.pages.turbine_page") +local unit_page = require("pocket.ui.pages.unit_page") local core = require("graphics.core") @@ -22,6 +23,9 @@ local TextBox = require("graphics.elements.textbox") local Sidebar = require("graphics.elements.controls.sidebar") +local LINK_STATE = iocontrol.LINK_STATE +local NAV_PAGE = iocontrol.NAV_PAGE + local TEXT_ALIGN = core.TEXT_ALIGN local cpair = core.cpair @@ -29,6 +33,9 @@ local cpair = core.cpair -- create new main view ---@param main graphics_element main displaybox local function init(main) + local nav = iocontrol.get_db().nav + local ps = iocontrol.get_db().ps + -- window header message TextBox{parent=main,y=1,text="",alignment=TEXT_ALIGN.LEFT,height=1,fg_bg=style.header} @@ -45,10 +52,10 @@ local function init(main) local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes=root_panes} - root_pane.register(coreio.core_ps(), "link_state", function (state) - if state == coreio.LINK_STATE.UNLINKED or state == coreio.LINK_STATE.API_LINK_ONLY then + root_pane.register(ps, "link_state", function (state) + if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then root_pane.set_value(1) - elseif state == coreio.LINK_STATE.SV_LINK_ONLY then + elseif state == LINK_STATE.SV_LINK_ONLY then root_pane.set_value(2) else root_pane.set_value(3) @@ -81,19 +88,36 @@ local function init(main) { char = "T", color = cpair(colors.black,colors.white) + }, + { + char = "D", + color = cpair(colors.black,colors.orange) } } - local pane_1 = home_page(page_div) - local pane_2 = unit_page(page_div) - local pane_3 = reactor_page(page_div) - local pane_4 = boiler_page(page_div) - local pane_5 = turbine_page(page_div) - local panes = { pane_1, pane_2, pane_3, pane_4, pane_5 } + local panes = { home_page(page_div), unit_page(page_div), reactor_page(page_div), boiler_page(page_div), turbine_page(page_div), diag_page(page_div) } local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} - Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=page_pane.set_value} + local function navigate_sidebar(page) + if page == 1 then + nav.page = nav.sub_pages[NAV_PAGE.HOME] + elseif page == 2 then + nav.page = nav.sub_pages[NAV_PAGE.UNITS] + elseif page == 3 then + nav.page = nav.sub_pages[NAV_PAGE.REACTORS] + elseif page == 4 then + nav.page = nav.sub_pages[NAV_PAGE.BOILERS] + elseif page == 5 then + nav.page = nav.sub_pages[NAV_PAGE.TURBINES] + elseif page == 6 then + nav.page = nav.sub_pages[NAV_PAGE.DIAG] + end + + page_pane.set_value(page) + end + + Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=navigate_sidebar} end return init diff --git a/pocket/ui/pages/diag_page.lua b/pocket/ui/pages/diag_page.lua new file mode 100644 index 0000000..9820a6a --- /dev/null +++ b/pocket/ui/pages/diag_page.lua @@ -0,0 +1,149 @@ +-- local style = require("pocket.ui.style") + +local iocontrol = require("pocket.iocontrol") + +local core = require("graphics.core") + +local Div = require("graphics.elements.div") +local MultiPane = require("graphics.elements.multipane") +local TextBox = require("graphics.elements.textbox") + +local IndicatorLight = require("graphics.elements.indicators.light") + +local App = require("graphics.elements.controls.app") +local Checkbox = require("graphics.elements.controls.checkbox") +local PushButton = require("graphics.elements.controls.push_button") +local SwitchButton = require("graphics.elements.controls.switch_button") + +local cpair = core.cpair + +local NAV_PAGE = iocontrol.NAV_PAGE + +local TEXT_ALIGN = core.TEXT_ALIGN + +-- new diagnostics page view +---@param root graphics_element parent +local function new_view(root) + local db = iocontrol.get_db() + + local main = Div{parent=root,x=1,y=1} + + local diag_home = Div{parent=main,x=1,y=1} + + TextBox{parent=diag_home,text="Diagnostic Apps",x=1,y=2,height=1,alignment=TEXT_ALIGN.CENTER} + + local alarm_test = Div{parent=main,x=1,y=1} + + local panes = { diag_home, alarm_test } + + local page_pane = MultiPane{parent=main,x=1,y=1,panes=panes} + + local function navigate_diag() + page_pane.set_value(1) + db.nav.page = NAV_PAGE.DIAG + db.nav.sub_pages[NAV_PAGE.DIAG] = NAV_PAGE.DIAG + end + + local function navigate_alarm() + page_pane.set_value(2) + db.nav.page = NAV_PAGE.D_ALARMS + db.nav.sub_pages[NAV_PAGE.DIAG] = NAV_PAGE.D_ALARMS + end + + ------------------------ + -- Alarm Testing Page -- + ------------------------ + + db.nav.register_task(NAV_PAGE.D_ALARMS, db.diag.tone_test.get_tone_states) + + local ttest = db.diag.tone_test + + local c_wht_gray = cpair(colors.white, colors.gray) + local c_red_gray = cpair(colors.red, colors.gray) + local c_yel_gray = cpair(colors.yellow, colors.gray) + local c_blue_gray = cpair(colors.blue, colors.gray) + + local audio = Div{parent=alarm_test,x=1,y=1} + + TextBox{parent=audio,y=1,text="Alarm Sounder Tests",height=1,alignment=TEXT_ALIGN.CENTER} + + ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)} + + PushButton{parent=audio,x=13,y=18,text="\x11 BACK",min_width=8,fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=c_wht_gray,callback=navigate_diag} + + local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)} + + TextBox{parent=tones,text="Tones",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=audio.get_fg_bg()} + + local test_btns = {} + test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1} + test_btns[2] = SwitchButton{parent=tones,text="TEST 2",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_2} + test_btns[3] = SwitchButton{parent=tones,text="TEST 3",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_3} + test_btns[4] = SwitchButton{parent=tones,text="TEST 4",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_4} + test_btns[5] = SwitchButton{parent=tones,text="TEST 5",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_5} + test_btns[6] = SwitchButton{parent=tones,text="TEST 6",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_6} + test_btns[7] = SwitchButton{parent=tones,text="TEST 7",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_7} + test_btns[8] = SwitchButton{parent=tones,text="TEST 8",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_8} + + ttest.tone_buttons = test_btns + + local function stop_all_tones() + for i = 1, #test_btns do test_btns[i].set_value(false) end + ttest.stop_tones() + end + + PushButton{parent=tones,text="STOP",min_width=8,active_fg_bg=c_wht_gray,fg_bg=cpair(colors.black,colors.red),callback=stop_all_tones} + + local alarms = Div{parent=audio,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)} + + TextBox{parent=alarms,text="Alarms (\x13)",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=audio.get_fg_bg()} + + local alarm_btns = {} + alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach} + alarm_btns[2] = Checkbox{parent=alarms,label="RADIATION",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_rad} + alarm_btns[3] = Checkbox{parent=alarms,label="RCT LOST",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_lost} + alarm_btns[4] = Checkbox{parent=alarms,label="CRIT DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_crit} + alarm_btns[5] = Checkbox{parent=alarms,label="DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_dmg} + alarm_btns[6] = Checkbox{parent=alarms,label="OVER TEMP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_overtemp} + alarm_btns[7] = Checkbox{parent=alarms,label="HIGH TEMP",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_hightemp} + alarm_btns[8] = Checkbox{parent=alarms,label="WASTE LEAK",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_wasteleak} + alarm_btns[9] = Checkbox{parent=alarms,label="WASTE HIGH",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_highwaste} + alarm_btns[10] = Checkbox{parent=alarms,label="RPS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rps} + alarm_btns[11] = Checkbox{parent=alarms,label="RCS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rcs} + alarm_btns[12] = Checkbox{parent=alarms,label="TURBINE TRP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_turbinet} + + ttest.alarm_buttons = alarm_btns + + local function stop_all_alarms() + for i = 1, #alarm_btns do alarm_btns[i].set_value(false) end + ttest.stop_alarms() + end + + PushButton{parent=alarms,x=3,y=15,text="STOP \x13",min_width=8,fg_bg=cpair(colors.black,colors.red),active_fg_bg=c_wht_gray,callback=stop_all_alarms} + + local states = Div{parent=audio,x=2,y=14,height=5,width=8} + + TextBox{parent=states,text="States",height=1,alignment=TEXT_ALIGN.CENTER} + local t_1 = IndicatorLight{parent=states,label="1",colors=c_blue_gray} + local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray} + local t_3 = IndicatorLight{parent=states,label="3",colors=c_blue_gray} + local t_4 = IndicatorLight{parent=states,label="4",colors=c_blue_gray} + local t_5 = IndicatorLight{parent=states,x=6,y=2,label="5",colors=c_blue_gray} + local t_6 = IndicatorLight{parent=states,x=6,label="6",colors=c_blue_gray} + local t_7 = IndicatorLight{parent=states,x=6,label="7",colors=c_blue_gray} + local t_8 = IndicatorLight{parent=states,x=6,label="8",colors=c_blue_gray} + + ttest.tone_indicators = { t_1, t_2, t_3, t_4, t_5, t_6, t_7, t_8 } + + -------------- + -- App List -- + -------------- + + App{parent=diag_home,x=3,y=4,text="\x0f",title="Alarm",callback=navigate_alarm,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + App{parent=diag_home,x=10,y=4,text="\x1e",title="LoopT",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)} + App{parent=diag_home,x=17,y=4,text="@",title="Comps",callback=function()end,app_fg_bg=cpair(colors.black,colors.orange)} + + return main +end + +return new_view diff --git a/pocket/ui/pages/home_page.lua b/pocket/ui/pages/home_page.lua index a31cae8..d192796 100644 --- a/pocket/ui/pages/home_page.lua +++ b/pocket/ui/pages/home_page.lua @@ -1,20 +1,21 @@ --- local style = require("pocket.ui.style") +local core = require("graphics.core") -local core = require("graphics.core") +local Div = require("graphics.elements.div") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local App = require("graphics.elements.controls.app") --- local cpair = core.cpair - -local TEXT_ALIGN = core.TEXT_ALIGN +local cpair = core.cpair -- new home page view ---@param root graphics_element parent local function new_view(root) local main = Div{parent=root,x=1,y=1} - TextBox{parent=main,text="HOME",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER} + App{parent=main,x=3,y=2,text="\x17",title="PRC",callback=function()end,app_fg_bg=cpair(colors.black,colors.purple)} + App{parent=main,x=10,y=2,text="\x15",title="CTL",callback=function()end,app_fg_bg=cpair(colors.black,colors.green)} + App{parent=main,x=17,y=2,text="\x08",title="DEV",callback=function()end,app_fg_bg=cpair(colors.black,colors.lightGray)} + App{parent=main,x=3,y=7,text="\x7f",title="Waste",callback=function()end,app_fg_bg=cpair(colors.black,colors.brown)} + App{parent=main,x=10,y=7,text="\xb6",title="Guide",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)} return main end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 34f0137..0823748 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -14,51 +14,54 @@ local max_distance = nil ---@type number|nil maximum acceptable t ---@class comms local comms = {} -comms.version = "2.2.0" +comms.version = "2.2.1" ---@enum PROTOCOL local PROTOCOL = { - MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol - RPLC = 1, -- reactor PLC protocol - SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc - SCADA_CRDN = 3, -- data/control packets for coordinators to/from supervisory controllers - COORD_API = 4 -- data/control packets for pocket computers to/from coordinators + MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol + RPLC = 1, -- reactor PLC protocol + SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc + SCADA_CRDN = 3, -- data/control packets for coordinators to/from supervisory controllers + COORD_API = 4 -- data/control packets for pocket computers to/from coordinators } ---@enum RPLC_TYPE local RPLC_TYPE = { - STATUS = 0, -- reactor/system status - MEK_STRUCT = 1, -- mekanism build structure - 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_STATUS = 6, -- RPS status - RPS_ALARM = 7, -- RPS alarm broadcast - RPS_RESET = 8, -- clear RPS trip (if in bad state, will trip immediately) - RPS_AUTO_RESET = 9, -- clear RPS trip if it is just a timeout or auto scram - AUTO_BURN_RATE = 10 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited + STATUS = 0, -- reactor/system status + MEK_STRUCT = 1, -- mekanism build structure + 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_STATUS = 6, -- RPS status + RPS_ALARM = 7, -- RPS alarm broadcast + RPS_RESET = 8, -- clear RPS trip (if in bad state, will trip immediately) + RPS_AUTO_RESET = 9, -- clear RPS trip if it is just a timeout or auto scram + AUTO_BURN_RATE = 10 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited } ---@enum SCADA_MGMT_TYPE local SCADA_MGMT_TYPE = { - ESTABLISH = 0, -- establish new connection - KEEP_ALIVE = 1, -- keep alive packet w/ RTT - CLOSE = 2, -- close a connection - RTU_ADVERT = 3, -- RTU capability advertisement - RTU_DEV_REMOUNT = 4,-- RTU multiblock possbily changed (formed, unformed) due to PPM remount - RTU_TONE_ALARM = 5 -- instruct RTUs to play specified alarm tones + ESTABLISH = 0, -- establish new connection + KEEP_ALIVE = 1, -- keep alive packet w/ RTT + CLOSE = 2, -- close a connection + RTU_ADVERT = 3, -- RTU capability advertisement + RTU_DEV_REMOUNT = 4, -- RTU multiblock possbily changed (formed, unformed) due to PPM remount + RTU_TONE_ALARM = 5, -- instruct RTUs to play specified alarm tones + DIAG_TONE_GET = 6, -- diagnostic: get alarm tones + DIAG_TONE_SET = 7, -- diagnostic: set alarm tones + DIAG_ALARM_SET = 8 -- diagnostic: set alarm to simulate audio for } ---@enum SCADA_CRDN_TYPE local SCADA_CRDN_TYPE = { - INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator - FAC_BUILDS = 1, -- facility RTU builds - FAC_STATUS = 2, -- state of facility and facility devices - FAC_CMD = 3, -- faility command - UNIT_BUILDS = 4, -- build of each reactor unit (reactor + RTUs) - UNIT_STATUSES = 5, -- state of each of the reactor units - UNIT_CMD = 6 -- command a reactor unit + INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator + FAC_BUILDS = 1, -- facility RTU builds + FAC_STATUS = 2, -- state of facility and facility devices + FAC_CMD = 3, -- faility command + UNIT_BUILDS = 4, -- build of each reactor unit (reactor + RTUs) + UNIT_STATUSES = 5, -- state of each of the reactor units + UNIT_CMD = 6 -- command a reactor unit } ---@enum CAPI_TYPE @@ -67,50 +70,50 @@ local CAPI_TYPE = { ---@enum ESTABLISH_ACK local ESTABLISH_ACK = { - ALLOW = 0, -- link approved - DENY = 1, -- link denied - COLLISION = 2, -- link denied due to existing active link - BAD_VERSION = 3 -- link denied due to comms version mismatch + ALLOW = 0, -- link approved + DENY = 1, -- link denied + COLLISION = 2, -- link denied due to existing active link + BAD_VERSION = 3 -- link denied due to comms version mismatch } ---@enum DEVICE_TYPE local DEVICE_TYPE = { - PLC = 0, -- PLC device type for establish - RTU = 1, -- RTU device type for establish - SV = 2, -- supervisor device type for establish - CRDN = 3, -- coordinator device type for establish - PKT = 4 -- pocket device type for establish + PLC = 0, -- PLC device type for establish + RTU = 1, -- RTU device type for establish + SV = 2, -- supervisor device type for establish + CRDN = 3, -- coordinator device type for establish + PKT = 4 -- pocket device type for establish } ---@enum PLC_AUTO_ACK local PLC_AUTO_ACK = { - FAIL = 0, -- failed to set burn rate/burn rate invalid - DIRECT_SET_OK = 1, -- successfully set burn rate - RAMP_SET_OK = 2, -- successfully started burn rate ramping - ZERO_DIS_OK = 3 -- successfully disabled reactor with < 0.01 burn rate + FAIL = 0, -- failed to set burn rate/burn rate invalid + DIRECT_SET_OK = 1, -- successfully set burn rate + RAMP_SET_OK = 2, -- successfully started burn rate ramping + ZERO_DIS_OK = 3 -- successfully disabled reactor with < 0.01 burn rate } ---@enum FAC_COMMAND local FAC_COMMAND = { - SCRAM_ALL = 0, -- SCRAM all reactors - STOP = 1, -- stop automatic process control - START = 2, -- start automatic process control - ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units - SET_WASTE_MODE = 4, -- set automatic waste processing mode - SET_PU_FB = 5 -- set plutonium fallback mode + SCRAM_ALL = 0, -- SCRAM all reactors + STOP = 1, -- stop automatic process control + START = 2, -- start automatic process control + ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units + SET_WASTE_MODE = 4, -- set automatic waste processing mode + SET_PU_FB = 5 -- set plutonium fallback mode } ---@enum UNIT_COMMAND local UNIT_COMMAND = { - SCRAM = 0, -- SCRAM the reactor - START = 1, -- start the reactor - RESET_RPS = 2, -- reset the RPS - SET_BURN = 3, -- set the burn rate - SET_WASTE = 4, -- set the waste processing mode - ACK_ALL_ALARMS = 5, -- ack all active alarms - ACK_ALARM = 6, -- ack a particular alarm - RESET_ALARM = 7, -- reset a particular alarm - SET_GROUP = 8 -- assign this unit to a group + SCRAM = 0, -- SCRAM the reactor + START = 1, -- start the reactor + RESET_RPS = 2, -- reset the RPS + SET_BURN = 3, -- set the burn rate + SET_WASTE = 4, -- set the waste processing mode + ACK_ALL_ALARMS = 5, -- ack all active alarms + ACK_ALARM = 6, -- ack a particular alarm + RESET_ALARM = 7, -- reset a particular alarm + SET_GROUP = 8 -- assign this unit to a group } comms.PROTOCOL = PROTOCOL @@ -147,6 +150,7 @@ function comms.scada_packet() local self = { modem_msg_in = nil, ---@type modem_message|nil valid = false, + authenticated = false, raw = {}, src_addr = comms.BROADCAST, dest_addr = comms.BROADCAST, @@ -235,6 +239,9 @@ function comms.scada_packet() return self.valid end + -- report that this packet has been authenticated (was received with a valid HMAC) + function public.stamp_authenticated() self.authenticated = true end + -- public accessors -- ---@nodiscard @@ -249,6 +256,8 @@ function comms.scada_packet() ---@nodiscard function public.is_valid() return self.valid end + ---@nodiscard + function public.is_authenticated() return self.authenticated end ---@nodiscard function public.src_addr() return self.src_addr end @@ -590,7 +599,10 @@ function comms.mgmt_packet() self.type == SCADA_MGMT_TYPE.REMOTE_LINKED or self.type == SCADA_MGMT_TYPE.RTU_ADVERT or self.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT or - self.type == SCADA_MGMT_TYPE.RTU_TONE_ALARM + self.type == SCADA_MGMT_TYPE.RTU_TONE_ALARM or + self.type == SCADA_MGMT_TYPE.DIAG_TONE_GET or + self.type == SCADA_MGMT_TYPE.DIAG_TONE_SET or + self.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET end -- make a SCADA management packet diff --git a/scada-common/network.lua b/scada-common/network.lua index 491faef..f8275bc 100644 --- a/scada-common/network.lua +++ b/scada-common/network.lua @@ -212,6 +212,7 @@ function network.nic(modem) if packet_hmac == computed_hmac then -- log.debug("crypto.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms") s_packet.receive(side, sender, reply_to, textutils.unserialize(msg), distance) + s_packet.stamp_authenticated() else -- log.debug("crypto.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms") end diff --git a/supervisor/facility.lua b/supervisor/facility.lua index eb8d1af..1564ae9 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -9,16 +9,16 @@ local unit = require("supervisor.unit") local rsctl = require("supervisor.session.rsctl") -local TONES = audio.TONES +local TONES = audio.TONES +local ALARM = types.ALARM +local PRIO = types.ALARM_PRIORITY +local ALARM_STATE = types.ALARM_STATE local PROCESS = types.PROCESS local PROCESS_NAMES = types.PROCESS_NAMES -local PRIO = types.ALARM_PRIORITY -local ALARM = types.ALARM -local ALARM_STATE = types.ALARM_STATE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE -local WASTE = types.WASTE_PRODUCT local WASTE_MODE = types.WASTE_MODE +local WASTE = types.WASTE_PRODUCT local IO = rsio.IO @@ -65,6 +65,7 @@ function facility.new(num_reactors, cooling_conf) units = {}, status_text = { "START UP", "initializing..." }, all_sys_ok = false, + allow_testing = false, -- rtus rtu_conn_count = 0, rtu_list = {}, @@ -115,7 +116,11 @@ function facility.new(num_reactors, cooling_conf) current_waste_product = WASTE.PLUTONIUM, pu_fallback = false, -- alarm tones - tone_states = { false, false, false, false, false, false, false, false }, + tone_states = {}, + test_tone_set = false, + test_tone_reset = false, + test_tone_states = {}, + test_alarm_states = {}, -- statistics im_stat_init = false, avg_charge = util.mov_avg(3, 0.0), @@ -135,6 +140,13 @@ function facility.new(num_reactors, cooling_conf) -- init redstone RTU I/O controller self.io_ctl = rsctl.new(self.redstone) + -- fill blank alarm/tone states + for _ = 1, 12 do table.insert(self.test_alarm_states, false) end + for _ = 1, 8 do + table.insert(self.tone_states, false) + table.insert(self.test_tone_states, false) + end + -- check if all auto-controlled units completed ramping ---@nodiscard local function _all_units_ramped() @@ -269,15 +281,20 @@ function facility.new(num_reactors, cooling_conf) -- supervisor sessions reporting the list of active RTU sessions ---@param rtu_sessions table session list of all connected RTUs - function public.report_rtus(rtu_sessions) - self.rtu_conn_count = #rtu_sessions - end + function public.report_rtus(rtu_sessions) self.rtu_conn_count = #rtu_sessions end -- update (iterate) the facility management function public.update() -- unlink RTU unit sessions if they are closed for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end + -- check if test routines are allowed right now + self.allow_testing = true + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + self.allow_testing = self.allow_testing and u.is_safe_idle() + end + -- current state for process control local charge_update = 0 local rate_update = 0 @@ -762,17 +779,43 @@ function facility.new(num_reactors, cooling_conf) -- Update Alarm Tones -- ------------------------ - local alarms = { false, false, false, false, false, false, false, false, false, false, false, false } - self.tone_states = { false, false, false, false, false, false, false, false} + local allow_test = self.allow_testing and self.test_tone_set - -- check all alarms for all units - for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit - for id, alarm in pairs(u.get_alarms()) do - alarms[id] = alarms[id] or (alarm == ALARM_STATE.TRIPPED) - end + local alarms = { false, false, false, false, false, false, false, false, false, false, false, false } + + for i = 1, #self.tone_states do + -- reset tone states before re-evaluting + self.tone_states[i] = false + + -- clear testing tones if we aren't using them + if (not allow_test) and (not self.test_tone_reset) then self.test_tone_states[i] = false end end + if allow_test then + alarms = self.test_alarm_states + else + -- check all alarms for all units + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + for id, alarm in pairs(u.get_alarms()) do + alarms[id] = alarms[id] or (alarm == ALARM_STATE.TRIPPED) + + -- clear testing alarms if we aren't using them + if not self.test_tone_reset then self.test_alarm_states[id] = false end + end + end + + self.test_tone_reset = true + end + + -- flag that tones were reset to notify diagnostic accessor + if not allow_test then + self.test_tone_set = false + self.test_tone_reset = true + end + + -- Evaluate Alarms -- + -- containment breach is worst case CRITICAL alarm, this takes priority if alarms[ALARM.ContainmentBreach] then self.tone_states[TONES.T_1800Hz_Int_4Hz] = true @@ -815,6 +858,13 @@ function facility.new(num_reactors, cooling_conf) self.tone_states[TONES.T_800Hz_Int] = false self.tone_states[TONES.T_1000Hz_Int] = false end + + -- add to tone states if testing is active + if allow_test then + for i = 1, #self.tone_states do + self.tone_states[i] = self.tone_states[i] or self.test_tone_states[i] + end + end end -- call the update function of all units in the facility
@@ -956,6 +1006,46 @@ function facility.new(num_reactors, cooling_conf) return self.pu_fallback end + -- DIAGNOSTIC TESTING -- + + -- attempt to set a test tone state + ---@param id tone_id|0 tone ID or 0 to disable all + ---@param state boolean state + ---@return boolean allow_testing, table test_tone_states + function public.diag_set_test_tone(id, state) + if self.allow_testing then + self.test_tone_set = true + self.test_tone_reset = false + + if id == 0 then + for i = 1, #self.test_tone_states do self.test_tone_states[i] = false end + else + self.test_tone_states[id] = state + end + end + + return self.allow_testing, self.test_tone_states + end + + -- attempt to set a test alarm state + ---@param id ALARM|0 alarm ID or 0 to disable all + ---@param state boolean state + ---@return boolean allow_testing, table test_alarm_states + function public.diag_set_test_alarm(id, state) + if self.allow_testing then + self.test_tone_set = true + self.test_tone_reset = false + + if id == 0 then + for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end + else + self.test_alarm_states[id] = state + end + end + + return self.allow_testing, self.test_alarm_states + end + -- READ STATES/PROPERTIES -- -- get current alarm tone on/off states diff --git a/supervisor/session/pocket.lua b/supervisor/session/pocket.lua index 9de55ab..7637cb5 100644 --- a/supervisor/session/pocket.lua +++ b/supervisor/session/pocket.lua @@ -33,8 +33,9 @@ local PERIODICS = { ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue ---@param timeout number communications timeout +---@param facility facility facility data table ---@param fp_ok boolean if the front panel UI is running -function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, fp_ok) +function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, fp_ok) -- print a log message to the terminal as long as the UI isn't running local function println(message) if not fp_ok then util.println_ts(message) end end @@ -129,6 +130,55 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, fp_ok) elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then -- close the session _close() + elseif pkt.type == SCADA_MGMT_TYPE.DIAG_TONE_GET then + -- get the state of alarm tones + _send_mgmt(SCADA_MGMT_TYPE.DIAG_TONE_GET, facility.get_alarm_tones()) + elseif pkt.type == SCADA_MGMT_TYPE.DIAG_TONE_SET then + local valid = false + + -- attempt to set a tone state + if pkt.scada_frame.is_authenticated() then + if pkt.length == 2 then + if type(pkt.data[1]) == "number" and type(pkt.data[2]) == "boolean" then + valid = true + + -- try to set tone states, then send back if testing is allowed + local allow_testing, test_tone_states = facility.diag_set_test_tone(pkt.data[1], pkt.data[2]) + _send_mgmt(SCADA_MGMT_TYPE.DIAG_TONE_SET, { allow_testing, test_tone_states }) + else + log.debug(log_header .. "SCADA diag tone set packet data type mismatch") + end + else + log.debug(log_header .. "SCADA diag tone set packet length mismatch") + end + else + log.debug(log_header .. "DIAG_TONE_SET is blocked without HMAC for security") + end + + if not valid then _send_mgmt(SCADA_MGMT_TYPE.DIAG_TONE_SET, { false }) end + elseif pkt.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET then + local valid = false + + -- attempt to set an alarm state + if pkt.scada_frame.is_authenticated() then + if pkt.length == 2 then + if type(pkt.data[1]) == "number" and type(pkt.data[2]) == "boolean" then + valid = true + + -- try to set alarm states, then send back if testing is allowed + local allow_testing, test_alarm_states = facility.diag_set_test_alarm(pkt.data[1], pkt.data[2]) + _send_mgmt(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { allow_testing, test_alarm_states }) + else + log.debug(log_header .. "SCADA diag alarm set packet data type mismatch") + end + else + log.debug(log_header .. "SCADA diag alarm set packet length mismatch") + end + else + log.debug(log_header .. "DIAG_ALARM_SET is blocked without HMAC for security") + end + + if not valid then _send_mgmt(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { false }) end else log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) end diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 075f090..a485441 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -430,7 +430,8 @@ function svsessions.establish_pdg_session(source_addr, version) local id = self.next_ids.pdg - pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.fp_ok) + pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.facility, + self.fp_ok) table.insert(self.sessions.pdg, pdg_s) local mt = { diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 8f6a6fb..24cab86 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.21.0" +local SUPERVISOR_VERSION = "v0.22.0" local println = util.println local println_ts = util.println_ts diff --git a/supervisor/unit.lua b/supervisor/unit.lua index c233631..cae423b 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -719,6 +719,23 @@ function unit.new(reactor_id, num_boilers, num_turbines) return false end + -- check if the reactor is connected, is stopped, the RPS is not tripped, and no alarms are active + ---@nodiscard + function public.is_safe_idle() + -- can't be disconnected + if self.plc_i == nil then return false end + + -- alarms must be inactive and not tripping + for _, alarm in pairs(self.alarms) do + if not (alarm.state == AISTATE.INACTIVE or alarm.state == AISTATE.RING_BACK) then return false end + end + + -- reactor must be stopped and RPS can't be tripped + if self.plc_i.get_status().status or self.plc_i.get_db().rps_tripped then return false end + + return true + end + -- get build properties of machines -- -- filter options