diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index bbbd90e..94071d4 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -1,4 +1,5 @@ local log = require("scada-common.log") +local flasher = require("graphics.flasher") local iocontrol = require("coordinator.iocontrol") @@ -107,6 +108,9 @@ function renderer.start_ui() table.insert(ui.unit_layouts, unit_view(monitor, id)) end + -- start flasher callback task + flasher.init() + -- report ui as ready engine.ui_ready = true end @@ -114,25 +118,33 @@ end -- close out the UI function renderer.close_ui() - if engine.ui_ready then - -- report ui as not ready - engine.ui_ready = false + -- report ui as not ready + engine.ui_ready = false + -- stop blinking indicators + flasher.clear() + + if engine.ui_ready then -- hide to stop animation callbacks ui.main_layout.hide() for i = 1, #ui.unit_layouts do ui.unit_layouts[i].hide() engine.monitors.unit_displays[i].clear() end - - -- clear root UI elements - ui.main_layout = nil - ui.unit_layouts = {} - - -- re-draw dmesg - engine.dmesg_window.setVisible(true) - engine.dmesg_window.redraw() + else + -- clear unit displays + for i = 1, #ui.unit_layouts do + engine.monitors.unit_displays[i].clear() + end end + + -- clear root UI elements + ui.main_layout = nil + ui.unit_layouts = {} + + -- re-draw dmesg + engine.dmesg_window.setVisible(true) + engine.dmesg_window.redraw() end -- is the UI ready? diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 766b7ff..aed8ac0 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -16,7 +16,7 @@ local config = require("coordinator.config") local coordinator = require("coordinator.coordinator") local renderer = require("coordinator.renderer") -local COORDINATOR_VERSION = "alpha-v0.5.3" +local COORDINATOR_VERSION = "alpha-v0.5.4" local print = util.print local println = util.println @@ -174,11 +174,13 @@ local ui_ok = init_start_ui() local no_modem = false --- start connection watchdog -conn_watchdog.feed() -log.debug("boot> conn watchdog started") +if ui_ok then + -- start connection watchdog + conn_watchdog.feed() + log.debug("boot> conn watchdog started") -log_sys("system started successfully") + log_sys("system started successfully") +end -- event loop -- ui_ok will never change in this loop, same as while true or exit if UI start failed diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index 899d50e..df79a00 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -28,6 +28,8 @@ local TEXT_ALIGN = core.graphics.TEXT_ALIGN local cpair = core.graphics.cpair +local period = core.flasher.PERIOD + -- create a unit view ---@param parent graphics_element parent ---@param id integer @@ -130,15 +132,15 @@ local function init(parent, id) annunciator.line_break() -- RPS - local rps_trp = IndicatorLight{parent=annunciator,label="RPS Trip",colors=cpair(colors.red,colors.gray)} - local rps_dmg = IndicatorLight{parent=annunciator,label="Damage Critical",colors=cpair(colors.yellow,colors.gray)} + local rps_trp = IndicatorLight{parent=annunciator,label="RPS Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} + local rps_dmg = IndicatorLight{parent=annunciator,label="Damage Critical",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local rps_exh = IndicatorLight{parent=annunciator,label="Excess Heated Coolant",colors=cpair(colors.yellow,colors.gray)} local rps_exw = IndicatorLight{parent=annunciator,label="Excess Waste",colors=cpair(colors.yellow,colors.gray)} - local rps_tmp = IndicatorLight{parent=annunciator,label="High Core Temp",colors=cpair(colors.yellow,colors.gray)} + local rps_tmp = IndicatorLight{parent=annunciator,label="High Core Temp",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local rps_nof = IndicatorLight{parent=annunciator,label="No Fuel",colors=cpair(colors.yellow,colors.gray)} local rps_noc = IndicatorLight{parent=annunciator,label="No Coolant",colors=cpair(colors.yellow,colors.gray)} - local rps_flt = IndicatorLight{parent=annunciator,label="PPM Fault",colors=cpair(colors.yellow,colors.gray)} - local rps_tmo = IndicatorLight{parent=annunciator,label="Timeout",colors=cpair(colors.yellow,colors.gray)} + local rps_flt = IndicatorLight{parent=annunciator,label="PPM Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS} + local rps_tmo = IndicatorLight{parent=annunciator,label="Timeout",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS} r_ps.subscribe("rps_tripped", rps_trp.update) r_ps.subscribe("dmg_crit", rps_dmg.update) @@ -157,7 +159,7 @@ local function init(parent, id) local c_cfm = IndicatorLight{parent=annunciator,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)} local c_sfm = IndicatorLight{parent=annunciator,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)} local c_mwrf = IndicatorLight{parent=annunciator,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)} - local c_tbnt = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)} + local c_tbnt = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} r_ps.subscribe("BoilRateMismatch", c_brm.update) r_ps.subscribe("CoolantFeedMismatch", c_cfm.update) @@ -193,7 +195,7 @@ local function init(parent, id) t_ps[1].subscribe("TurbineOverSpeed", t1_tos.update) TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} - local t1_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)} + local t1_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} t_ps[1].subscribe("TurbineTrip", t1_trp.update) main.line_break() @@ -209,7 +211,7 @@ local function init(parent, id) t_ps[2].subscribe("TurbineOverSpeed", t2_tos.update) TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} - local t2_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)} + local t2_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} t_ps[2].subscribe("TurbineTrip", t2_trp.update) main.line_break() @@ -226,7 +228,7 @@ local function init(parent, id) t_ps[3].subscribe("TurbineOverSpeed", t3_tos.update) TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} - local t3_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)} + local t3_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} t_ps[3].subscribe("TurbineTrip", t3_trp.update) annunciator.line_break() @@ -234,7 +236,7 @@ local function init(parent, id) ---@todo radiation monitor IndicatorLight{parent=annunciator,label="Radiation Monitor",colors=cpair(colors.green,colors.gray)} - IndicatorLight{parent=annunciator,label="Radiation Alarm",colors=cpair(colors.red,colors.gray)} + IndicatorLight{parent=annunciator,label="Radiation Alarm",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} DataIndicator{parent=main,x=34,y=51,label="",format="%10.1f",value=0,unit="mSv/h",lu_colors=lu_cpair,width=18,fg_bg=stat_fg_bg} -- reactor controls -- diff --git a/graphics/core.lua b/graphics/core.lua index f4d86fd..510e31d 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -4,6 +4,10 @@ local core = {} +local flasher = require("graphics.flasher") + +core.flasher = flasher + local events = {} ---@class monitor_touch diff --git a/graphics/elements/indicators/light.lua b/graphics/elements/indicators/light.lua index 5d38a7c..6106ebe 100644 --- a/graphics/elements/indicators/light.lua +++ b/graphics/elements/indicators/light.lua @@ -1,11 +1,15 @@ -- Indicator Light Graphics Element local element = require("graphics.element") +local flasher = require("graphics.flasher") +local util = require("scada-common.util") ---@class indicator_light_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 @@ -19,25 +23,62 @@ local function indicator_light(args) assert(type(args.label) == "string", "graphics.elements.indicators.light: label is a required field") assert(type(args.colors) == "table", "graphics.elements.indicators.light: colors is a required field") + if args.flash then + assert(util.is_int(args.period), "graphics.elements.indicators.light: 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(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg) + else + e.window.blit(" \x95", "0" .. args.colors.blit_b, 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(" \x95", "0" .. args.colors.blit_a, 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(" \x95", "0" .. args.colors.blit_b, 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 - e.window.setCursorPos(1, 1) - if new_state then - e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg) - else - e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg) - end + if new_state then enable() else disable() end end -- set indicator state @@ -46,6 +87,7 @@ local function indicator_light(args) -- write label and initial indicator light e.on_update(false) + e.window.setCursorPos(3, 1) e.window.write(args.label) return e.get() diff --git a/graphics/flasher.lua b/graphics/flasher.lua new file mode 100644 index 0000000..689fab6 --- /dev/null +++ b/graphics/flasher.lua @@ -0,0 +1,82 @@ +-- +-- Indicator Light Flasher +-- + +local tcd = require("scada-common.tcallbackdsp") + +local flasher = {} + +-- note: no additional call needs to be made in a main loop as this class automatically uses the TCD to operate + +---@alias PERIOD integer +local PERIOD = { + BLINK_250_MS = 1, + BLINK_500_MS = 2, + BLINK_1000_MS = 3 +} + +flasher.PERIOD = PERIOD + +local active = false +local registry = { {}, {}, {} } -- one registry table per period +local callback_counter = 0 + +-- start the flasher task +function flasher.init() + active = true + registry = { {}, {}, {} } + flasher.callback_250ms() +end + +-- clear all blinking indicators and stop the flasher task +function flasher.clear() + active = false + registry = { {}, {}, {} } +end + +-- register a function to be called on the selected blink period +-- +-- times are not strictly enforced, but all with a given period will be set at the same time +---@param f function function to call each period +---@param period PERIOD time period option (1, 2, or 3) +function flasher.start(f, period) + if type(registry[period]) == "table" then + table.insert(registry[period], f) + end +end + +-- stop a function from being called at the blink period +---@param f function function callback registered +function flasher.stop(f) + for i = 1, #registry do + for j = 1, #registry[i] do + if registry[i][j] == f then + registry[i][j] = nil + break + end + end + end +end + +-- blink registered indicators +-- +-- this assumes it is called every 250ms, it does no checking of time on its own +function flasher.callback_250ms() + if active then + for _, f in pairs(registry[PERIOD.BLINK_250_MS]) do f() end + + if callback_counter % 2 == 0 then + for _, f in pairs(registry[PERIOD.BLINK_500_MS]) do f() end + end + + if callback_counter % 4 == 0 then + for _, f in pairs(registry[PERIOD.BLINK_1000_MS]) do f() end + end + + callback_counter = callback_counter + 1 + + tcd.dispatch(0.25, flasher.callback_250ms) + end +end + +return flasher