diff --git a/supervisor/databus.lua b/supervisor/databus.lua new file mode 100644 index 0000000..e6bbee7 --- /dev/null +++ b/supervisor/databus.lua @@ -0,0 +1,44 @@ +-- +-- Data Bus - Central Communication Linking for Supervisor Front Panel +-- + +local psil = require("scada-common.psil") + +local databus = {} + +local dbus_iface = { + ps = psil.create(), + session_entries = { rtu = {}, plc = {}, coord = {}, diag = {} } +} + +-- call to toggle heartbeat signal +function databus.heartbeat() dbus_iface.ps.toggle("heartbeat") end + +-- transmit firmware versions across the bus +---@param plc_v string supervisor version +---@param comms_v string comms version +function databus.tx_versions(plc_v, comms_v) + dbus_iface.ps.publish("version", plc_v) + dbus_iface.ps.publish("comms_version", comms_v) +end + +-- transmit hardware status for modem connection state +---@param has_modem boolean +function databus.tx_hw_modem(has_modem) + dbus_iface.ps.publish("has_modem", has_modem) +end + +function databus.tx_svs_connection(type, data) +end + +function databus.tx_svs_disconnection(type, data) +end + +-- link a function to receive data from the bus +---@param field string field name +---@param func function function to link +function databus.rx_field(field, func) + dbus_iface.ps.subscribe(field, func) +end + +return databus diff --git a/supervisor/panel/components/plc_entry.lua b/supervisor/panel/components/plc_entry.lua new file mode 100644 index 0000000..e69de29 diff --git a/supervisor/panel/front_panel.lua b/supervisor/panel/front_panel.lua new file mode 100644 index 0000000..0098a6e --- /dev/null +++ b/supervisor/panel/front_panel.lua @@ -0,0 +1,91 @@ +-- +-- Main SCADA Coordinator GUI +-- + +local util = require("scada-common.util") + +local databus = require("supervisor.databus") + +local style = require("supervisor.panel.style") + +local core = require("graphics.core") + +local Div = require("graphics.elements.div") +local MultiPane = require("graphics.elements.multipane") +local Rectangle = require("graphics.elements.rectangle") +local TextBox = require("graphics.elements.textbox") + +local PushButton = require("graphics.elements.controls.push_button") +local TabBar = require("graphics.elements.controls.tabbar") + +local LED = require("graphics.elements.indicators.led") +local LEDPair = require("graphics.elements.indicators.ledpair") +local RGBLED = require("graphics.elements.indicators.ledrgb") + +local TEXT_ALIGN = core.graphics.TEXT_ALIGN + +local cpair = core.graphics.cpair +local border = core.graphics.border + +-- create new main view +---@param panel graphics_element main displaybox +local function init(panel) + TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} + + local page_div = Div{parent=panel,x=1,y=3} + + -- + -- system indicators + -- + + local main_page = Div{parent=page_div,x=1,y=1} + + local system = Div{parent=main_page,width=14,height=17,x=2,y=2} + + local on = LED{parent=system,label="POWER",colors=cpair(colors.green,colors.red)} + local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)} + on.update(true) + system.line_break() + + databus.rx_field("heartbeat", heartbeat.update) + + local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)} + system.line_break() + + databus.rx_field("has_modem", modem.update) + + -- + -- about footer + -- + + local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)} + local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} + local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} + + databus.rx_field("version", function (version) fw_v.set_value(util.c("FW: ", version)) end) + databus.rx_field("comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) + + -- + -- page handling + -- + + local plc_list = Div{parent=page_div,x=1,y=1} + + TextBox{parent=plc_list,x=2,y=2,text="v1.1.17 - PLC - UNIT 4 - :15004",alignment=TEXT_ALIGN.LEFT,height=1} + + local panes = { main_page, plc_list, main_page, main_page, main_page } + + local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} + + local tabs = { + { name = "Main", color = cpair(colors.black, colors.ivory) }, + { name = "PLCs", color = cpair(colors.black, colors.ivory) }, + { name = "RTUs", color = cpair(colors.black, colors.ivory) }, + { name = "CRDs", color = cpair(colors.black, colors.ivory) }, + { name = "PKTs", color = cpair(colors.black, colors.ivory) }, + } + + TabBar{parent=panel,y=2,tabs=tabs,min_width=10,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)} +end + +return init diff --git a/supervisor/panel/style.lua b/supervisor/panel/style.lua new file mode 100644 index 0000000..31039d4 --- /dev/null +++ b/supervisor/panel/style.lua @@ -0,0 +1,41 @@ +-- +-- Graphics Style Options +-- + +local core = require("graphics.core") + +local style = {} + +local cpair = core.graphics.cpair + +-- GLOBAL -- + +-- remap global colors +colors.ivory = colors.pink +colors.red_off = colors.brown +colors.yellow_off = colors.magenta +colors.green_off = colors.lime + +style.root = cpair(colors.black, colors.ivory) +style.header = cpair(colors.black, colors.lightGray) + +style.colors = { + { c = colors.red, hex = 0xdf4949 }, -- RED ON + { c = colors.orange, hex = 0xffb659 }, + { c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON + { c = colors.lime, hex = 0x16665a }, -- GREEN OFF + { c = colors.green, hex = 0x6be551 }, -- GREEN ON + { c = colors.cyan, hex = 0x34bac8 }, + { c = colors.lightBlue, hex = 0x6cc0f2 }, + { c = colors.blue, hex = 0x0096ff }, + { c = colors.purple, hex = 0xb156ee }, + { c = colors.pink, hex = 0xdcd9ca }, -- IVORY + { c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF + -- { c = colors.white, hex = 0xdcd9ca }, + { c = colors.lightGray, hex = 0xb1b8b3 }, + { c = colors.gray, hex = 0x575757 }, + -- { c = colors.black, hex = 0x191919 }, + { c = colors.brown, hex = 0x672223 } -- RED OFF +} + +return style diff --git a/supervisor/renderer.lua b/supervisor/renderer.lua new file mode 100644 index 0000000..ca49946 --- /dev/null +++ b/supervisor/renderer.lua @@ -0,0 +1,80 @@ +-- +-- Graphics Rendering Control +-- + +local panel_view = require("supervisor.panel.front_panel") +local style = require("supervisor.panel.style") + +local flasher = require("graphics.flasher") + +local DisplayBox = require("graphics.elements.displaybox") + +local renderer = {} + +local ui = { + display = nil +} + +-- start the UI +function renderer.start_ui() + if ui.display == nil then + -- reset terminal + term.setTextColor(colors.white) + term.setBackgroundColor(colors.black) + term.clear() + term.setCursorPos(1, 1) + + -- set overridden colors + for i = 1, #style.colors do + term.setPaletteColor(style.colors[i].c, style.colors[i].hex) + end + + -- init front panel view + ui.display = DisplayBox{window=term.current(),fg_bg=style.root} + panel_view(ui.display) + + -- start flasher callback task + flasher.run() + end +end + +-- close out the UI +function renderer.close_ui() + if ui.display ~= nil then + -- stop blinking indicators + flasher.clear() + + -- hide to stop animation callbacks + ui.display.hide() + + -- clear root UI elements + ui.display = nil + + -- restore colors + for i = 1, #style.colors do + local r, g, b = term.nativePaletteColor(style.colors[i].c) + term.setPaletteColor(style.colors[i].c, r, g, b) + end + + -- reset terminal + term.setTextColor(colors.white) + term.setBackgroundColor(colors.black) + term.clear() + term.setCursorPos(1, 1) + end +end + +-- is the UI ready? +---@nodiscard +---@return boolean ready +function renderer.ui_ready() return ui.display ~= nil end + +-- handle a mouse event +---@param event mouse_interaction +function renderer.handle_mouse(event) + if ui.display ~= nil then + ui.display.handle_mouse(event) + end +end + +return renderer diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index b093d87..b68d815 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -18,7 +18,8 @@ local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local SV_Q_DATA = svqtypes.SV_Q_DATA -local println = util.println +-- local println = util.println +local println = function (str) end -- retry time constants in ms -- local INITIAL_WAIT = 1500 diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index cca8a26..8c61221 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -14,7 +14,8 @@ local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local PLC_AUTO_ACK = comms.PLC_AUTO_ACK local UNIT_COMMAND = comms.UNIT_COMMAND -local println = util.println +-- local println = util.println +local println = function (str) end -- retry time constants in ms local INITIAL_WAIT = 1500 diff --git a/supervisor/session/pocket.lua b/supervisor/session/pocket.lua index 9cf0a5d..5693e02 100644 --- a/supervisor/session/pocket.lua +++ b/supervisor/session/pocket.lua @@ -8,7 +8,8 @@ local pocket = {} local PROTOCOL = comms.PROTOCOL local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE -local println = util.println +-- local println = util.println +local println = function (str) end -- retry time constants in ms -- local INITIAL_WAIT = 1500 diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 96309a0..5d0e5c2 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -22,7 +22,8 @@ local PROTOCOL = comms.PROTOCOL local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE -local println = util.println +-- local println = util.println +local println = function (str) end local PERIODICS = { KEEP_ALIVE = 2000 diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 8ad3366..93b35b9 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -22,6 +22,7 @@ local CRD_S_DATA = coordinator.CRD_S_DATA local svsessions = {} +---@enum SESSION_TYPE local SESSION_TYPE = { RTU_SESSION = 0, -- RTU gateway PLC_SESSION = 1, -- reactor PLC diff --git a/supervisor/startup.lua b/supervisor/startup.lua index bf27e2e..c104e1b 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -5,16 +5,21 @@ require("/initenv").init_env() local crash = require("scada-common.crash") +local comms = require("scada-common.comms") local log = require("scada-common.log") local ppm = require("scada-common.ppm") local util = require("scada-common.util") +local core = require("graphics.core") + local config = require("supervisor.config") +local databus = require("supervisor.databus") +local renderer = require("supervisor.renderer") local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v0.15.5" +local SUPERVISOR_VERSION = "v0.16.0" local println = util.println local println_ts = util.println_ts @@ -79,6 +84,9 @@ local function main() -- startup ---------------------------------------- + -- record firmware versions and ID + databus.tx_versions(SUPERVISOR_VERSION, comms.version) + -- mount connected devices ppm.mount_all() @@ -89,84 +97,113 @@ local function main() return end - -- start comms, open all channels - local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, config.REACTOR_COOLING, modem, - config.SCADA_DEV_LISTEN, config.SCADA_SV_CTL_LISTEN, config.TRUSTED_RANGE) + databus.tx_hw_modem(true) - -- base loop clock (6.67Hz, 3 ticks) - local MAIN_CLOCK = 0.15 - local loop_clock = util.new_clock(MAIN_CLOCK) + -- start UI + local fp_ok, message = pcall(renderer.start_ui) - -- start clock - loop_clock.start() + if not fp_ok then + renderer.close_ui() + println_ts(util.c("UI error: ", message)) + log.error(util.c("GUI crashed with error ", message)) + else + -- start comms, open all channels + local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, config.REACTOR_COOLING, modem, + config.SCADA_DEV_LISTEN, config.SCADA_SV_CTL_LISTEN, config.TRUSTED_RANGE) - -- event loop - while true do - local event, param1, param2, param3, param4, param5 = util.pull_event() + -- base loop clock (6.67Hz, 3 ticks) + local MAIN_CLOCK = 0.15 + local loop_clock = util.new_clock(MAIN_CLOCK) - -- handle event - if event == "peripheral_detach" then - local type, device = ppm.handle_unmount(param1) + -- start clock + loop_clock.start() - if type ~= nil and device ~= nil then - if type == "modem" then - -- we only care if this is our wireless modem - if device == modem then - println_ts("wireless modem disconnected!") - log.warning("comms modem disconnected") - else - log.warning("non-comms modem disconnected") + -- event loop + while true do + local event, param1, param2, param3, param4, param5 = util.pull_event() + + -- handle event + if event == "peripheral_detach" then + local type, device = ppm.handle_unmount(param1) + + if type ~= nil and device ~= nil then + if type == "modem" then + -- we only care if this is our wireless modem + if device == modem then + log.warning("comms modem disconnected") + databus.tx_hw_modem(false) + else + log.warning("non-comms modem disconnected") + end end end - end - elseif event == "peripheral" then - local type, device = ppm.mount(param1) + elseif event == "peripheral" then + local type, device = ppm.mount(param1) - if type ~= nil and device ~= nil then - if type == "modem" then - if device.isWireless() then - -- reconnected modem - modem = device - superv_comms.reconnect_modem(modem) + if type ~= nil and device ~= nil then + if type == "modem" then + if device.isWireless() then + -- reconnected modem + modem = device + superv_comms.reconnect_modem(modem) - println_ts("wireless modem reconnected.") - log.info("comms modem reconnected") - else - log.info("wired modem reconnected") + log.info("comms modem reconnected") + + databus.tx_hw_modem(true) + else + log.info("wired modem reconnected") + end end end + elseif event == "timer" and loop_clock.is_clock(param1) then + -- main loop tick + databus.heartbeat() + + -- iterate sessions + svsessions.iterate_all() + + -- free any closed sessions + svsessions.free_all_closed() + + loop_clock.start() + elseif event == "timer" then + -- a non-clock timer event, check watchdogs + svsessions.check_all_watchdogs(param1) + elseif event == "modem_message" then + -- got a packet + local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5) + superv_comms.handle_packet(packet) + elseif event == "mouse_click" then + -- handle a monitor touch event + renderer.handle_mouse(core.events.touch(param1, param2, param3)) + log.debug(util.sprintf("mouse_click: %s [%d, %d]", param1, param2, param3)) + elseif event == "mouse_drag" then + log.debug(util.sprintf("mouse_drag: %s [%d, %d]", param1, param2, param3)) + elseif event == "mouse_scroll" then + log.debug(util.sprintf("mouse_scroll: %s [%d, %d]", param1, param2, param3)) + elseif event == "mouse_up" then + log.debug(util.sprintf("mouse_up: %s [%d, %d]", param1, param2, param3)) end - elseif event == "timer" and loop_clock.is_clock(param1) then - -- main loop tick - -- iterate sessions - svsessions.iterate_all() - - -- free any closed sessions - svsessions.free_all_closed() - - loop_clock.start() - elseif event == "timer" then - -- a non-clock timer event, check watchdogs - svsessions.check_all_watchdogs(param1) - elseif event == "modem_message" then - -- got a packet - local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5) - superv_comms.handle_packet(packet) + -- check for termination request + if event == "terminate" or ppm.should_terminate() then + log.info("terminate requested, closing sessions...") + svsessions.close_all() + log.info("sessions closed") + break + end end - -- check for termination request - if event == "terminate" or ppm.should_terminate() then - println_ts("closing sessions...") - log.info("terminate requested, closing sessions...") - svsessions.close_all() - log.info("sessions closed") - break - end + renderer.close_ui() end println_ts("exited") log.info("exited") end -if not xpcall(main, crash.handler) then crash.exit() else log.close() end +if not xpcall(main, crash.handler) then + pcall(renderer.close_ui) + crash.exit() +else + log.close() +end diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 49cf482..076f0ac 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -11,7 +11,8 @@ local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE -local println = util.println +-- local println = util.println +local println = function (str) end -- supervisory controller communications ---@nodiscard