From 6ea530635fa38d479ed16835fd6275b5d5f69f57 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 6 Apr 2023 22:10:33 -0400 Subject: [PATCH] #182 WIP PLC front panel --- graphics/element.lua | 15 ++- graphics/elements/indicators/led.lua | 98 ++++++++++++++++++++ graphics/elements/indicators/ledpair.lua | 111 +++++++++++++++++++++++ imgen.py | 2 +- reactor-plc/panel/front_panel.lua | 85 +++++++++++++++++ reactor-plc/panel/style.lua | 41 +++++++++ reactor-plc/renderer.lua | 62 +++++++++++++ reactor-plc/startup.lua | 16 +++- scada-common/log.lua | 2 +- 9 files changed, 426 insertions(+), 6 deletions(-) create mode 100644 graphics/elements/indicators/led.lua create mode 100644 graphics/elements/indicators/ledpair.lua create mode 100644 reactor-plc/panel/front_panel.lua create mode 100644 reactor-plc/panel/style.lua create mode 100644 reactor-plc/renderer.lua diff --git a/graphics/element.lua b/graphics/element.lua index 8aa3ce9..91810ae 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -32,6 +32,8 @@ local element = {} ---|data_indicator_args ---|hbar_args ---|icon_indicator_args +---|indicator_led_args +---|indicator_led_pair_args ---|indicator_light_args ---|power_indicator_args ---|rad_indicator_args @@ -100,7 +102,13 @@ function element.new(args) else local w, h = self.p_window.getSize() protected.frame.x = args.x or 1 - protected.frame.y = args.y or next_y + + if args.parent ~= nil then + protected.frame.y = args.y or (next_y - offset_y) + else + protected.frame.y = args.y or next_y + end + protected.frame.w = args.width or w protected.frame.h = args.height or h end @@ -260,6 +268,11 @@ function element.new(args) ---@param child graphics_template ---@return integer|string key function public.__add_child(key, child) + -- offset first automatic placement + if self.next_y <= self.child_offset.y then + self.next_y = self.child_offset.y + 1 + end + child.prepare_template(self.child_offset.x, self.child_offset.y, self.next_y) self.next_y = child.frame.y + child.frame.h diff --git a/graphics/elements/indicators/led.lua b/graphics/elements/indicators/led.lua new file mode 100644 index 0000000..7905848 --- /dev/null +++ b/graphics/elements/indicators/led.lua @@ -0,0 +1,98 @@ +-- Indicator "LED" Graphics Element + +local util = require("scada-common.util") + +local element = require("graphics.element") +local flasher = require("graphics.flasher") + +---@class indicator_led_args +---@field label string indicator label +---@field colors cpair on/off colors (a/b respectively) +---@field min_label_width? integer label length if omitted +---@field flash? boolean whether to flash on true rather than stay on +---@field period? PERIOD flash period +---@field parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer 1 if omitted +---@field fg_bg? cpair foreground/background colors + +-- new indicator LED +---@nodiscard +---@param args indicator_led_args +---@return graphics_element element, element_id id +local function indicator_led(args) + assert(type(args.label) == "string", "graphics.elements.indicators.led: label is a required field") + assert(type(args.colors) == "table", "graphics.elements.indicators.led: colors is a required field") + + if args.flash then + assert(util.is_int(args.period), "graphics.elements.indicators.led: period is a required field if flash is enabled") + end + + -- single line + args.height = 1 + + -- determine width + args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 + + -- flasher state + local flash_on = true + + -- create new graphics element base object + local e = element.new(args) + + -- called by flasher when enabled + local function flash_callback() + e.window.setCursorPos(1, 1) + + if flash_on then + e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg) + else + e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg) + end + + flash_on = not flash_on + end + + -- enable light or start flashing + local function enable() + if args.flash then + flash_on = true + flasher.start(flash_callback, args.period) + else + e.window.setCursorPos(1, 1) + e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg) + end + end + + -- disable light or stop flashing + local function disable() + if args.flash then + flash_on = false + flasher.stop(flash_callback) + end + + e.window.setCursorPos(1, 1) + e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg) + end + + -- on state change + ---@param new_state boolean indicator state + function e.on_update(new_state) + e.value = new_state + if new_state then enable() else disable() end + end + + -- set indicator state + ---@param val boolean indicator state + function e.set_value(val) e.on_update(val) end + + -- write label and initial indicator light + e.on_update(false) + e.window.setCursorPos(3, 1) + e.window.write(args.label) + + return e.get() +end + +return indicator_led diff --git a/graphics/elements/indicators/ledpair.lua b/graphics/elements/indicators/ledpair.lua new file mode 100644 index 0000000..c56fafd --- /dev/null +++ b/graphics/elements/indicators/ledpair.lua @@ -0,0 +1,111 @@ +-- Indicator LED Pair Graphics Element (two LEDs provide: off, color_a, color_b) + +local util = require("scada-common.util") + +local element = require("graphics.element") +local flasher = require("graphics.flasher") + +---@class indicator_led_pair_args +---@field label string indicator label +---@field off color color for off +---@field c1 color color for #1 on +---@field c2 color color for #2 on +---@field min_label_width? integer label length if omitted +---@field flash? boolean whether to flash when on rather than stay on +---@field period? PERIOD flash period +---@field parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer 1 if omitted +---@field fg_bg? cpair foreground/background colors + +-- new tri-state indicator light +---@nodiscard +---@param args indicator_led_pair_args +---@return graphics_element element, element_id id +local function indicator_led_pair(args) + assert(type(args.label) == "string", "graphics.elements.indicators.ledpair: label is a required field") + assert(type(args.off) == "number", "graphics.elements.indicators.ledpair: off is a required field") + assert(type(args.c1) == "number", "graphics.elements.indicators.ledpair: c1 is a required field") + assert(type(args.c2) == "number", "graphics.elements.indicators.ledpair: c2 is a required field") + + if args.flash then + assert(util.is_int(args.period), "graphics.elements.indicators.ledpair: period is a required field if flash is enabled") + end + + -- single line + args.height = 1 + + -- determine width + args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 + + -- flasher state + local flash_on = true + + -- blit translations + local co = colors.toBlit(args.off) + local c1 = colors.toBlit(args.c1) + local c2 = colors.toBlit(args.c2) + + -- create new graphics element base object + local e = element.new(args) + + -- init value for initial check in on_update + e.value = 1 + + -- called by flasher when enabled + local function flash_callback() + e.window.setCursorPos(1, 1) + + if flash_on then + if e.value == 2 then + e.window.blit("\x8c", c1, e.fg_bg.blit_bkg) + elseif e.value == 3 then + e.window.blit("\x8c", c2, e.fg_bg.blit_bkg) + end + else + e.window.blit("\x8c", co, e.fg_bg.blit_bkg) + end + + flash_on = not flash_on + end + + -- on state change + ---@param new_state integer indicator state + function e.on_update(new_state) + local was_off = e.value <= 1 + + e.value = new_state + e.window.setCursorPos(1, 1) + + if args.flash then + if was_off and (new_state > 1) then + flash_on = true + flasher.start(flash_callback, args.period) + elseif new_state <= 1 then + flash_on = false + flasher.stop(flash_callback) + + e.window.blit("\x8c", co, e.fg_bg.blit_bkg) + end + elseif new_state == 2 then + e.window.blit("\x8c", c1, e.fg_bg.blit_bkg) + elseif new_state == 3 then + e.window.blit("\x8c", c2, e.fg_bg.blit_bkg) + else + e.window.blit("\x8c", co, e.fg_bg.blit_bkg) + end + end + + -- set indicator state + ---@param val integer indicator state + function e.set_value(val) e.on_update(val) end + + -- write label and initial indicator light + e.on_update(1) + e.window.write(args.label) + + return e.get() +end + +return indicator_led_pair diff --git a/imgen.py b/imgen.py index faab46a..1ef46f2 100644 --- a/imgen.py +++ b/imgen.py @@ -68,7 +68,7 @@ def make_manifest(size): "pocket" : list_files("./pocket"), }, "depends" : { - "reactor-plc" : [ "system", "common" ], + "reactor-plc" : [ "system", "common", "graphics" ], "rtu" : [ "system", "common" ], "supervisor" : [ "system", "common" ], "coordinator" : [ "system", "common", "graphics" ], diff --git a/reactor-plc/panel/front_panel.lua b/reactor-plc/panel/front_panel.lua new file mode 100644 index 0000000..455dd15 --- /dev/null +++ b/reactor-plc/panel/front_panel.lua @@ -0,0 +1,85 @@ +-- +-- Main SCADA Coordinator GUI +-- + +local util = require("scada-common.util") + +local style = require("reactor-plc.panel.style") + +local core = require("graphics.core") + +local DisplayBox = require("graphics.elements.displaybox") +local Div = require("graphics.elements.div") +local Rectangle = require("graphics.elements.rectangle") +local TextBox = require("graphics.elements.textbox") +local ColorMap = require("graphics.elements.colormap") + +local PushButton = require("graphics.elements.controls.push_button") +local SwitchButton = require("graphics.elements.controls.switch_button") + +local DataIndicator = require("graphics.elements.indicators.data") +local LED = require("graphics.elements.indicators.led") + +local TEXT_ALIGN = core.graphics.TEXT_ALIGN + +local cpair = core.graphics.cpair +local border = core.graphics.border + +-- create new main view +---@param monitor table main viewscreen +local function init(monitor) + local panel = DisplayBox{window=monitor,fg_bg=style.root} + + -- window header message + local header = TextBox{parent=panel,y=1,text="REACTOR PLC",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} + + local system = Div{parent=panel,width=14,height=18,x=2,y=3} + + local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} + local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)} + system.line_break() + local reactor = LED{parent=system,label="REACTOR",colors=cpair(colors.green,colors.red)} + local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.gray)} + local network = LED{parent=system,label="NETWORK",colors=cpair(colors.green,colors.gray)} + system.line_break() + local _ = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.gray)} + local _ = LED{parent=system,label="RT RPS",colors=cpair(colors.green,colors.gray)} + local _ = LED{parent=system,label="RT COMMS TX",colors=cpair(colors.green,colors.gray)} + local _ = LED{parent=system,label="RT COMMS RX",colors=cpair(colors.green,colors.gray)} + local _ = LED{parent=system,label="RT SPCTL",colors=cpair(colors.green,colors.gray)} + system.line_break() + local active = LED{parent=system,label="RCT ACTIVE",colors=cpair(colors.green,colors.green_off)} + local scram = LED{parent=system,label="RPS TRIP",colors=cpair(colors.red,colors.red_off)} + system.line_break() + + local about = Rectangle{parent=panel,width=16,height=4,x=18,y=15,border=border(1,colors.white),thin=true,fg_bg=cpair(colors.black,colors.white)} + local _ = TextBox{parent=about,text="FW: v1.0.0",alignment=TEXT_ALIGN.LEFT,height=1} + local _ = TextBox{parent=about,text="NT: v1.4.0",alignment=TEXT_ALIGN.LEFT,height=1} + -- about.line_break() + -- local _ = TextBox{parent=about,text="SVTT: 10ms",alignment=TEXT_ALIGN.LEFT,height=1} + + local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,colors.lightGray),thin=true,fg_bg=cpair(colors.black,colors.lightGray)} + local _ = LED{parent=rps,label="MANUAL",colors=cpair(colors.red,colors.red_off)} + local _ = LED{parent=rps,label="AUTOMATIC",colors=cpair(colors.red,colors.red_off)} + local _ = LED{parent=rps,label="TIMEOUT",colors=cpair(colors.red,colors.red_off)} + local _ = LED{parent=rps,label="PLC FAULT",colors=cpair(colors.red,colors.red_off)} + local _ = LED{parent=rps,label="RCT FAULT",colors=cpair(colors.red,colors.red_off)} + rps.line_break() + local _ = LED{parent=rps,label="HI DAMAGE",colors=cpair(colors.red,colors.red_off)} + local _ = LED{parent=rps,label="HI TEMP",colors=cpair(colors.red,colors.red_off)} + rps.line_break() + local _ = LED{parent=rps,label="LO FUEL",colors=cpair(colors.red,colors.red_off)} + local _ = LED{parent=rps,label="HI WASTE",colors=cpair(colors.red,colors.red_off)} + rps.line_break() + local _ = LED{parent=rps,label="LO CCOOLANT",colors=cpair(colors.red,colors.red_off)} + local _ = LED{parent=rps,label="HI HCOOLANT",colors=cpair(colors.red,colors.red_off)} + + + ColorMap{parent=panel,x=1,y=19} + -- facility.ps.subscribe("sv_ping", ping.update) + -- facility.ps.subscribe("date_time", datetime.set_value) + + return panel +end + +return init diff --git a/reactor-plc/panel/style.lua b/reactor-plc/panel/style.lua new file mode 100644 index 0000000..0b55ec3 --- /dev/null +++ b/reactor-plc/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.green_off = colors.lime + +style.root = cpair(colors.black, colors.ivory) +style.header = cpair(colors.black, colors.lightGray) +style.label = cpair(colors.gray, colors.lightGray) + +style.colors = { + { c = colors.red, hex = 0xdf4949 }, -- RED ON + { c = colors.orange, hex = 0xffb659 }, + { c = colors.yellow, hex = 0xe5e552 }, + { 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 = 0xf9488a }, + -- { c = colors.white, hex = 0xdcd9ca }, + { c = colors.lightGray, hex = 0x999f9b }, + { c = colors.gray, hex = 0x575757 }, + -- { c = colors.black, hex = 0x191919 }, + { c = colors.brown, hex = 0x672223 } -- RED OFF +} + +return style diff --git a/reactor-plc/renderer.lua b/reactor-plc/renderer.lua new file mode 100644 index 0000000..cdda240 --- /dev/null +++ b/reactor-plc/renderer.lua @@ -0,0 +1,62 @@ +-- +-- Graphics Rendering Control +-- + +local style = require("reactor-plc.panel.style") +local panel_view = require("reactor-plc.panel.front_panel") + +local renderer = {} + +local ui = { + view = nil +} + +-- start the UI +function renderer.start_ui() + if ui.view == nil then + 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.view = panel_view(term.current()) + end +end + +-- close out the UI +function renderer.close_ui() + if ui.view ~= nil then + -- hide to stop animation callbacks + ui.view.hide() + end + + -- clear root UI elements + ui.view = 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 + + term.clear() +end + +-- is the UI ready? +---@nodiscard +---@return boolean ready +function renderer.ui_ready() return ui.view ~= nil end + +-- handle a touch event +---@param event monitor_touch +function renderer.handle_touch(event) + ui.view.handle_touch(event) +end + +return renderer diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 5ad5ca4..b833136 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -8,13 +8,15 @@ local crash = require("scada-common.crash") local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local ppm = require("scada-common.ppm") +local psil = require("scada-common.psil") local util = require("scada-common.util") local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") +local renderer = require("reactor-plc.renderer") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "v1.0.0" +local R_PLC_VERSION = "v1.1.0" local print = util.print local println = util.println @@ -106,7 +108,10 @@ local function main() mq_rps = mqueue.new(), mq_comms_tx = mqueue.new(), mq_comms_rx = mqueue.new() - } + }, + + -- publisher/subscriber interface for front panel + fp_ps = psil.create() } local smem_dev = __shared_memory.plc_dev @@ -148,6 +153,9 @@ local function main() -- PLC init
--- EVENT_CONSUMER: this function consumes events local function init() + -- front panel time! + renderer.start_ui() + if plc_state.init_ok then -- just booting up, no fission allowed (neutrons stay put thanks) if plc_state.reactor_formed and smem_dev.reactor.getStatus() then @@ -177,7 +185,7 @@ local function main() println("init> completed") log.info("init> startup completed") else - println("init> system in degraded state, awaiting devices...") + -- println("init> system in degraded state, awaiting devices...") log.warning("init> started in a degraded state, awaiting peripheral connections...") end end @@ -217,6 +225,8 @@ local function main() parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec) end + renderer.close_ui() + println_ts("exited") log.info("exited") end diff --git a/scada-common/log.lua b/scada-common/log.lua index 424bf55..30f785d 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -16,7 +16,7 @@ local MODE = { log.MODE = MODE -- whether to log debug messages or not -local LOG_DEBUG = false +local LOG_DEBUG = true local log_sys = { path = "/log.txt",