diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index 11287e7..26009ff 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -2,6 +2,7 @@ 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 types = require("scada-common.types") local iocontrol = require("coordinator.iocontrol") local process = require("coordinator.process") @@ -12,7 +13,6 @@ local dialog = require("coordinator.ui.dialog") local print = util.print local println = util.println -local println_ts = util.println_ts local PROTOCOL = comms.PROTOCOL local DEVICE_TYPE = comms.DEVICE_TYPE @@ -301,6 +301,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, self.sv_addr = comms.BROADCAST self.sv_linked = false self.sv_r_seq_num = nil + iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED) _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {}) end @@ -474,7 +475,6 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, elseif dev_type == DEVICE_TYPE.PKT then -- pocket linking request local id = apisessions.establish_session(src_addr, firmware_v) - println(util.c("[API] pocket (", firmware_v, ") [@", src_addr, "] \xbb connected")) coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id)) _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW) @@ -639,6 +639,9 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, local config = packet.data[2] if est_ack == ESTABLISH_ACK.ALLOW then + -- reset to disconnected before validating + iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED) + if type(config) == "table" and #config > 1 then -- get configuration @@ -660,6 +663,8 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, self.sv_addr = src_addr self.sv_linked = true self.sv_config_err = false + + iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED) else self.sv_config_err = true log.warning("invalid supervisor configuration definitions received, establish failed") @@ -677,14 +682,17 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, if est_ack == ESTABLISH_ACK.DENY then if self.last_est_ack ~= est_ack then + iocontrol.fp_link_state(types.PANEL_LINK_STATE.DENIED) log.info("supervisor connection denied") end elseif est_ack == ESTABLISH_ACK.COLLISION then if self.last_est_ack ~= est_ack then + iocontrol.fp_link_state(types.PANEL_LINK_STATE.COLLISION) log.warning("supervisor connection denied due to collision") end elseif est_ack == ESTABLISH_ACK.BAD_VERSION then if self.last_est_ack ~= est_ack then + iocontrol.fp_link_state(types.PANEL_LINK_STATE.BAD_VERSION) log.warning("supervisor comms version mismatch") end else @@ -720,7 +728,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, self.sv_addr = comms.BROADCAST self.sv_linked = false self.sv_r_seq_num = nil - println_ts("server connection closed by remote host") + iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED) log.info("server connection closed by remote host") else log.debug("received unknown SCADA_MGMT packet type " .. packet.type) diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 2eeb5e2..921c626 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -10,9 +10,15 @@ local util = require("scada-common.util") local process = require("coordinator.process") local sounder = require("coordinator.sounder") +local pgi = require("coordinator.ui.pgi") + local ALARM_STATE = types.ALARM_STATE local PROCESS = types.PROCESS +-- nominal RTT is ping (0ms to 10ms usually) + 500ms for CRD main loop tick +local WARN_RTT = 1000 -- 2x as long as expected w/ 0 ping +local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping + local iocontrol = {} ---@class ioctl @@ -27,6 +33,19 @@ local function __generic_ack(success) end -- luacheck: unused args +-- initialize front panel PSIL +---@param firmware_v string coordinator version +---@param comms_v string comms version +function iocontrol.init_fp(firmware_v, comms_v) + ---@class ioctl_front_panel + io.fp = { + ps = psil.create() + } + + io.fp.ps.publish("version", firmware_v) + io.fp.ps.publish("comms_version", comms_v) +end + -- initialize the coordinator IO controller ---@param conf facility_conf configuration ---@param comms coord_comms comms reference @@ -189,6 +208,52 @@ function iocontrol.init(conf, comms) process.init(io, comms) end +-- toggle heartbeat indicator +function iocontrol.heartbeat() io.fp.ps.toggle("heartbeat") end + +-- report presence of the wireless modem +---@param has_modem boolean +function iocontrol.fp_has_modem(has_modem) io.fp.ps.publish("has_modem", has_modem) end + +-- report presence of the speaker +---@param has_speaker boolean +function iocontrol.fp_has_speaker(has_speaker) io.fp.ps.publish("has_speaker", has_speaker) end + +-- report supervisor link state +---@param state integer +function iocontrol.fp_link_state(state) io.fp.ps.publish("link_state", state) end + +-- report PKT firmware version and PKT session connection state +---@param session_id integer PKT session +---@param fw string firmware version +---@param s_addr integer PKT computer ID +function iocontrol.fp_pkt_connected(session_id, fw, s_addr) + io.fp.ps.publish("pkt_" .. session_id .. "_fw", fw) + io.fp.ps.publish("pkt_" .. session_id .. "_addr", util.sprintf("@ C% 3d", s_addr)) + pgi.create_pkt_entry(session_id) +end + +-- report PKT session disconnected +---@param session_id integer PKT session +function iocontrol.fp_pkt_disconnected(session_id) + pgi.delete_pkt_entry(session_id) +end + +-- transmit PKT session RTT +---@param session_id integer PKT session +---@param rtt integer round trip time +function iocontrol.fp_pkt_rtt(session_id, rtt) + io.fp.ps.publish("pkt_" .. session_id .. "_rtt", rtt) + + if rtt > HIGH_RTT then + io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.red) + elseif rtt > WARN_RTT then + io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.yellow_hc) + else + io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.green) + end +end + -- populate facility structure builds ---@param build table ---@return boolean valid diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index fec6629..16ea6a9 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -6,7 +6,9 @@ local log = require("scada-common.log") local util = require("scada-common.util") local style = require("coordinator.ui.style") +local pgi = require("coordinator.ui.pgi") +local panel_view = require("coordinator.ui.layout.front_panel") local main_view = require("coordinator.ui.layout.main_view") local unit_view = require("coordinator.ui.layout.unit_view") @@ -21,7 +23,9 @@ local engine = { monitors = nil, ---@type monitors_struct|nil dmesg_window = nil, ---@type table|nil ui_ready = false, + fp_ready = false, ui = { + front_panel = nil, ---@type graphics_element|nil main_display = nil, ---@type graphics_element|nil unit_displays = {} } @@ -44,9 +48,7 @@ end -- link to the monitor peripherals ---@param monitors monitors_struct -function renderer.set_displays(monitors) - engine.monitors = monitors -end +function renderer.set_displays(monitors) engine.monitors = monitors end -- check if the renderer is configured to use a given monitor peripheral ---@nodiscard @@ -75,6 +77,17 @@ function renderer.init_displays() for _, monitor in ipairs(engine.monitors.unit_displays) do _init_display(monitor) end + + -- init terminal + term.setTextColor(colors.white) + term.setBackgroundColor(colors.black) + term.clear() + term.setCursorPos(1, 1) + + -- set overridden colors + for i = 1, #style.fp.colors do + term.setPaletteColor(style.fp.colors[i].c, style.fp.colors[i].hex) + end end -- check main display width @@ -109,6 +122,21 @@ function renderer.init_dmesg() log.direct_dmesg(engine.dmesg_window) end +-- start the coordinator front panel +function renderer.start_fp() + if not engine.fp_ready then + -- show front panel view on terminal + engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root} + panel_view(engine.ui.front_panel) + + -- start flasher callback task + flasher.run() + + -- report front panel as ready + engine.fp_ready = true + end +end + -- start the coordinator GUI function renderer.start_ui() if not engine.ui_ready then @@ -133,10 +161,42 @@ function renderer.start_ui() end end +-- close out the front panel +function renderer.close_fp() + if engine.fp_ready then + if not engine.ui_ready then + -- stop blinking indicators + flasher.clear() + end + + -- disable PGI + pgi.unlink() + + -- hide to stop animation callbacks and clear root UI elements + engine.ui.front_panel.hide() + engine.ui.front_panel = nil + engine.fp_ready = false + + -- 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 + -- close out the UI function renderer.close_ui() - -- stop blinking indicators - flasher.clear() + if not engine.fp_ready then + -- stop blinking indicators + flasher.clear() + end -- delete element trees if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end @@ -157,6 +217,11 @@ function renderer.close_ui() engine.dmesg_window.redraw() end +-- is the front panel ready? +---@nodiscard +---@return boolean ready +function renderer.fp_ready() return engine.fp_ready end + -- is the UI ready? ---@nodiscard ---@return boolean ready @@ -165,14 +230,19 @@ function renderer.ui_ready() return engine.ui_ready end -- handle a touch event ---@param event mouse_interaction|nil function renderer.handle_mouse(event) - if engine.ui_ready and event ~= nil then - if event.monitor == engine.monitors.primary_name then - engine.ui.main_display.handle_mouse(event) - else - for id, monitor in ipairs(engine.monitors.unit_name_map) do - if event.monitor == monitor then - local layout = engine.ui.unit_displays[id] ---@type graphics_element - layout.handle_mouse(event) + if event ~= nil then + if engine.fp_ready and event.monitor == "terminal" then + engine.ui.front_panel.handle_mouse(event) + elseif engine.ui_ready then + if event.monitor == engine.monitors.primary_name then + engine.ui.main_display.handle_mouse(event) + else + for id, monitor in ipairs(engine.monitors.unit_name_map) do + if event.monitor == monitor then + local layout = engine.ui.unit_displays[id] ---@type graphics_element + layout.handle_mouse(event) + break + end end end end diff --git a/coordinator/session/apisessions.lua b/coordinator/session/apisessions.lua index 1ea1beb..c1f1d4e 100644 --- a/coordinator/session/apisessions.lua +++ b/coordinator/session/apisessions.lua @@ -1,11 +1,12 @@ -local log = require("scada-common.log") -local mqueue = require("scada-common.mqueue") -local util = require("scada-common.util") +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local util = require("scada-common.util") -local config = require("coordinator.config") +local config = require("coordinator.config") +local iocontrol = require("coordinator.iocontrol") -local pocket = require("coordinator.session.pocket") +local pocket = require("coordinator.session.pocket") local apisessions = {} @@ -112,6 +113,7 @@ function apisessions.establish_session(source_addr, version) setmetatable(pkt_s, mt) + iocontrol.fp_pkt_connected(id, version, source_addr) log.debug(util.c("[API] established new session: ", pkt_s)) self.next_id = id + 1 diff --git a/coordinator/session/pocket.lua b/coordinator/session/pocket.lua index ddabdda..f5211a7 100644 --- a/coordinator/session/pocket.lua +++ b/coordinator/session/pocket.lua @@ -1,7 +1,9 @@ -local comms = require("scada-common.comms") -local log = require("scada-common.log") -local mqueue = require("scada-common.mqueue") -local util = require("scada-common.util") +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local util = require("scada-common.util") + +local iocontrol = require("coordinator.iocontrol") local pocket = {} @@ -9,8 +11,6 @@ local PROTOCOL = comms.PROTOCOL -- local CAPI_TYPE = comms.CAPI_TYPE local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE -local println = util.println - -- retry time constants in ms -- local INITIAL_WAIT = 1500 -- local RETRY_PERIOD = 1000 @@ -69,6 +69,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) local function _close() self.conn_watchdog.cancel() self.connected = false + iocontrol.fp_pkt_disconnected(id) end -- send a CAPI packet @@ -140,6 +141,8 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) -- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms") -- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms") + + iocontrol.fp_pkt_rtt(id, self.last_rtt) else log.debug(log_header .. "SCADA keep alive packet length mismatch") end @@ -172,7 +175,6 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) function public.close() _close() _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) - println("connection to pocket session " .. id .. " closed by server") log.info(log_header .. "session closed by server") end @@ -211,7 +213,6 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) -- exit if connection was closed if not self.connected then - println("connection to pocket session " .. id .. " closed by remote host") log.info(log_header .. "session closed by remote host") return self.connected end diff --git a/coordinator/startup.lua b/coordinator/startup.lua index f58642d..6a97896 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -4,6 +4,7 @@ require("/initenv").init_env() +local comms = require("scada-common.comms") local crash = require("scada-common.crash") local log = require("scada-common.log") local network = require("scada-common.network") @@ -21,7 +22,7 @@ local sounder = require("coordinator.sounder") local apisessions = require("coordinator.session.apisessions") -local COORDINATOR_VERSION = "v0.18.0" +local COORDINATOR_VERSION = "v0.19.0" local println = util.println local println_ts = util.println_ts @@ -80,6 +81,9 @@ local function main() -- mount connected devices ppm.mount_all() + -- report versions/init fp PSIL + iocontrol.init_fp(COORDINATOR_VERSION, comms.version) + -- setup monitors local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS) if not configured or monitors == nil then @@ -127,6 +131,7 @@ local function main() sounder.init(speaker, config.SOUNDER_VOLUME) log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms") log_sys("annunciator alarm configured") + iocontrol.fp_has_speaker(true) end ---------------------------------------- @@ -148,6 +153,7 @@ local function main() return else log_comms("wireless modem connected") + iocontrol.fp_has_modem(true) end -- create connection watchdog @@ -166,6 +172,21 @@ local function main() local MAIN_CLOCK = 0.5 local loop_clock = util.new_clock(MAIN_CLOCK) + ---------------------------------------- + -- start front panel + ---------------------------------------- + + log_graphics("starting front panel UI...") + + local fp_ok, fp_message = pcall(renderer.start_fp) + if not fp_ok then + renderer.close_fp() + log_graphics(util.c("front panel UI error: ", fp_message)) + println_ts("front panel UI creation failed") + log.fatal(util.c("front panel GUI render failed with error ", fp_message)) + return + else log_graphics("front panel ready") end + ---------------------------------------- -- connect to the supervisor ---------------------------------------- @@ -199,18 +220,18 @@ local function main() -- start up the UI ---@return boolean ui_ok started ok local function init_start_ui() - log_graphics("starting UI...") + log_graphics("starting main UI...") local draw_start = util.time_ms() - local ui_ok, message = pcall(renderer.start_ui) + local ui_ok, ui_message = pcall(renderer.start_ui) if not ui_ok then renderer.close_ui() - log_graphics(util.c("UI crashed: ", message)) - println_ts("UI crashed") - log.fatal(util.c("GUI crashed with error ", message)) + log_graphics(util.c("main UI error: ", ui_message)) + println_ts("main UI creation failed") + log.fatal(util.c("main GUI render failed with error ", ui_message)) else - log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms") + log_graphics("first main UI draw took " .. (util.time_ms() - draw_start) .. "ms") -- start clock loop_clock.start() @@ -257,6 +278,8 @@ local function main() -- alert user to status log_sys("awaiting comms modem reconnect...") + + iocontrol.fp_has_modem(false) else log_sys("non-comms modem disconnected") end @@ -275,6 +298,8 @@ local function main() local msg = "lost alarm sounder speaker" println_ts(msg) log_sys(msg) + + iocontrol.fp_has_speaker(false) end end elseif event == "peripheral" then @@ -292,6 +317,8 @@ local function main() -- re-init system if not init_connect_sv() then break end ui_ok = init_start_ui() + + iocontrol.fp_has_modem(true) else log_sys("wired modem reconnected") end @@ -302,12 +329,15 @@ local function main() local msg = "alarm sounder speaker reconnected" println_ts(msg) log_sys(msg) + sounder.reconnect(device) + iocontrol.fp_has_speaker(true) end end elseif event == "timer" then if loop_clock.is_clock(param1) then -- main loop tick + iocontrol.heartbeat() -- iterate sessions apisessions.iterate_all() @@ -364,8 +394,9 @@ local function main() ui_ok = init_start_ui() end end - elseif event == "monitor_touch" then - -- handle a monitor touch event + elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or + event == "mouse_drag" or event == "mouse_scroll" then + -- handle a mouse event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) elseif event == "speaker_audio_empty" then -- handle speaker buffer emptied @@ -386,6 +417,7 @@ local function main() end renderer.close_ui() + renderer.close_fp() sounder.stop() log_sys("system shutdown") @@ -395,6 +427,7 @@ end if not xpcall(main, crash.handler) then pcall(renderer.close_ui) + pcall(renderer.close_fp) pcall(sounder.stop) crash.exit() else diff --git a/coordinator/ui/components/pkt_entry.lua b/coordinator/ui/components/pkt_entry.lua new file mode 100644 index 0000000..8ba805c --- /dev/null +++ b/coordinator/ui/components/pkt_entry.lua @@ -0,0 +1,48 @@ +-- +-- Pocket Connection Entry +-- + +local iocontrol = require("coordinator.iocontrol") + +local core = require("graphics.core") + +local Div = require("graphics.elements.div") +local TextBox = require("graphics.elements.textbox") + +local DataIndicator = require("graphics.elements.indicators.data") + +local TEXT_ALIGN = core.TEXT_ALIGN + +local cpair = core.cpair + +-- create a pocket list entry +---@param parent graphics_element parent +---@param id integer PKT session ID +local function init(parent, id) + local ps = iocontrol.get_db().fp.ps + + -- root div + local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} + local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)} + + local ps_prefix = "pkt_" .. id .. "_" + + TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} + local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)} + TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} + pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value) + + TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1} + local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)} + pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value) + + TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1} + local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)} + TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)} + pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update) + pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor) + + return root +end + +return init diff --git a/coordinator/ui/layout/front_panel.lua b/coordinator/ui/layout/front_panel.lua new file mode 100644 index 0000000..176299e --- /dev/null +++ b/coordinator/ui/layout/front_panel.lua @@ -0,0 +1,108 @@ +-- +-- Coordinator Front Panel GUI +-- + +local types = require("scada-common.types") +local util = require("scada-common.util") + +local iocontrol = require("coordinator.iocontrol") + +local pgi = require("coordinator.ui.pgi") +local style = require("coordinator.ui.style") + +local pkt_entry = require("coordinator.ui.components.pkt_entry") + +local core = require("graphics.core") + +local Div = require("graphics.elements.div") +local ListBox = require("graphics.elements.listbox") +local MultiPane = require("graphics.elements.multipane") +local TextBox = require("graphics.elements.textbox") + +local TabBar = require("graphics.elements.controls.tabbar") + +local LED = require("graphics.elements.indicators.led") +local RGBLED = require("graphics.elements.indicators.ledrgb") + +local TEXT_ALIGN = core.TEXT_ALIGN + +local cpair = core.cpair + +-- create new front panel view +---@param panel graphics_element main displaybox +local function init(panel) + local ps = iocontrol.get_db().fp.ps + + TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.fp.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 status = 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)} + status.update(true) + system.line_break() + + heartbeat.register(ps, "heartbeat", heartbeat.update) + + local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)} + local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} + network.update(types.PANEL_LINK_STATE.DISCONNECTED) + system.line_break() + + modem.register(ps, "has_modem", modem.update) + network.register(ps, "link_state", network.update) + + local speaker = LED{parent=system,label="SPEAKER",colors=cpair(colors.green,colors.green_off)} + speaker.register(ps, "has_speaker", speaker.update) + +---@diagnostic disable-next-line: undefined-field + local comp_id = util.sprintf("(%d)", os.getComputerID()) + TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} + + -- + -- 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} + + fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) + comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) + + -- + -- page handling + -- + + -- API page + + local api_page = Div{parent=page_div,x=1,y=1,hidden=true} + local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} + local _ = Div{parent=api_list,height=1,hidden=true} -- padding + + -- assemble page panes + + local panes = { main_page, api_page } + + local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} + + local tabs = { + { name = "CRD", color = cpair(colors.black, colors.ivory) }, + { name = "API", color = cpair(colors.black, colors.ivory) }, + } + + TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)} + + -- link pocket API list management to PGI + pgi.link_elements(api_list, pkt_entry) +end + +return init diff --git a/coordinator/ui/pgi.lua b/coordinator/ui/pgi.lua new file mode 100644 index 0000000..5e4077f --- /dev/null +++ b/coordinator/ui/pgi.lua @@ -0,0 +1,58 @@ +-- +-- Protected Graphics Interface +-- + +local log = require("scada-common.log") +local util = require("scada-common.util") + +local pgi = {} + +local data = { + pkt_list = nil, ---@type nil|graphics_element + pkt_entry = nil, ---@type function + -- session entries + s_entries = { pkt = {} } +} + +-- link list boxes +---@param pkt_list graphics_element pocket list element +---@param pkt_entry function pocket entry constructor +function pgi.link_elements(pkt_list, pkt_entry) + data.pkt_list = pkt_list + data.pkt_entry = pkt_entry +end + +-- unlink all fields, disabling the PGI +function pgi.unlink() + data.pkt_list = nil + data.pkt_entry = nil +end + +-- add a PKT entry to the PKT list +---@param session_id integer pocket session +function pgi.create_pkt_entry(session_id) + if data.pkt_list ~= nil and data.pkt_entry ~= nil then + local success, result = pcall(data.pkt_entry, data.pkt_list, session_id) + + if success then + data.s_entries.pkt[session_id] = result + else + log.error(util.c("PGI: failed to create PKT entry (", result, ")"), true) + end + end +end + +-- delete a PKT entry from the PKT list +---@param session_id integer pocket session +function pgi.delete_pkt_entry(session_id) + if data.s_entries.pkt[session_id] ~= nil then + local success, result = pcall(data.s_entries.pkt[session_id].delete) + data.s_entries.pkt[session_id] = nil + + if not success then + log.error(util.c("PGI: failed to delete PKT entry (", result, ")"), true) + end + end +end + +return pgi diff --git a/coordinator/ui/style.lua b/coordinator/ui/style.lua index 46faf4a..1fd569f 100644 --- a/coordinator/ui/style.lua +++ b/coordinator/ui/style.lua @@ -10,6 +10,40 @@ local cpair = core.cpair -- GLOBAL -- +-- add color mappings for front panel +colors.ivory = colors.pink +colors.red_off = colors.brown +colors.yellow_off = colors.magenta +colors.green_off = colors.lime + +-- front panel styling + +style.fp = {} + +style.fp.root = cpair(colors.black, colors.ivory) +style.fp.header = cpair(colors.black, colors.lightGray) + +style.fp.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 +} + +-- main GUI styling + style.root = cpair(colors.black, colors.lightGray) style.header = cpair(colors.white, colors.gray) style.label = cpair(colors.gray, colors.lightGray)