diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index de92ef0..e72cccf 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -279,11 +279,12 @@ function coordinator.comms(version, nic, sv_watchdog) -- send an API establish request response ---@param packet scada_packet ---@param ack ESTABLISH_ACK - local function _send_api_establish_ack(packet, ack) + ---@param data any? + local function _send_api_establish_ack(packet, ack, data) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() - m_pkt.make(MGMT_TYPE.ESTABLISH, { ack }) + m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data }) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt) @@ -470,10 +471,11 @@ function coordinator.comms(version, nic, sv_watchdog) elseif packet.type == MGMT_TYPE.ESTABLISH then -- establish a new session -- validate packet and continue - if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then - local comms_v = packet.data[1] - local firmware_v = packet.data[2] + if packet.length == 4 then + local comms_v = util.strval(packet.data[1]) + local firmware_v = util.strval(packet.data[2]) local dev_type = packet.data[3] + local api_v = util.strval(packet.data[4]) if comms_v ~= comms.version then if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then @@ -481,12 +483,19 @@ function coordinator.comms(version, nic, sv_watchdog) end _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) + elseif api_v ~= comms.api_version then + if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_API_VERSION then + log.info(util.c("dropping API establish packet with incorrect api version v", api_v, " (expected v", comms.api_version, ")")) + end + + _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_API_VERSION) elseif dev_type == DEVICE_TYPE.PKT then -- pocket linking request local id = apisessions.establish_session(src_addr, firmware_v) 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) + local conf = iocontrol.get_db().facility.conf + _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW, { conf.num_units, conf.cooling }) else log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel")) _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY) diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 9be448a..1de337f 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -67,6 +67,7 @@ function iocontrol.init(conf, comms, temp_scale) -- facility data structure ---@class ioctl_facility io.facility = { + conf = conf, num_units = conf.num_units, tank_mode = conf.cooling.fac_tank_mode, tank_defs = conf.cooling.fac_tank_defs, @@ -279,18 +280,18 @@ function iocontrol.init(conf, comms, temp_scale) ---@type alarms alarms = { - ALARM_STATE.INACTIVE, -- containment breach - ALARM_STATE.INACTIVE, -- containment radiation - ALARM_STATE.INACTIVE, -- reactor lost - ALARM_STATE.INACTIVE, -- damage critical - ALARM_STATE.INACTIVE, -- reactor taking damage - ALARM_STATE.INACTIVE, -- reactor over temperature - ALARM_STATE.INACTIVE, -- reactor high temperature - ALARM_STATE.INACTIVE, -- waste leak - ALARM_STATE.INACTIVE, -- waste level high - ALARM_STATE.INACTIVE, -- RPS transient - ALARM_STATE.INACTIVE, -- RCS transient - ALARM_STATE.INACTIVE -- turbine trip + ALARM_STATE.INACTIVE, -- containment breach + ALARM_STATE.INACTIVE, -- containment radiation + ALARM_STATE.INACTIVE, -- reactor lost + ALARM_STATE.INACTIVE, -- damage critical + ALARM_STATE.INACTIVE, -- reactor taking damage + ALARM_STATE.INACTIVE, -- reactor over temperature + ALARM_STATE.INACTIVE, -- reactor high temperature + ALARM_STATE.INACTIVE, -- waste leak + ALARM_STATE.INACTIVE, -- waste level high + ALARM_STATE.INACTIVE, -- RPS transient + ALARM_STATE.INACTIVE, -- RCS transient + ALARM_STATE.INACTIVE -- turbine trip }, annunciator = {}, ---@type annunciator diff --git a/coordinator/session/pocket.lua b/coordinator/session/pocket.lua index a19f7c3..7101297 100644 --- a/coordinator/session/pocket.lua +++ b/coordinator/session/pocket.lua @@ -8,7 +8,7 @@ local iocontrol = require("coordinator.iocontrol") local pocket = {} local PROTOCOL = comms.PROTOCOL --- local CRDN_TYPE = comms.CRDN_TYPE +local CRDN_TYPE = comms.CRDN_TYPE local MGMT_TYPE = comms.MGMT_TYPE -- retry time constants in ms @@ -73,18 +73,18 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) end -- send a CRDN packet - -----@param msg_type CRDN_TYPE - -----@param msg table - -- local function _send(msg_type, msg) - -- local s_pkt = comms.scada_packet() - -- local c_pkt = comms.crdn_packet() + ---@param msg_type CRDN_TYPE + ---@param msg table + local function _send(msg_type, msg) + local s_pkt = comms.scada_packet() + local c_pkt = comms.crdn_packet() - -- c_pkt.make(msg_type, msg) - -- s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable()) + c_pkt.make(msg_type, msg) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable()) - -- out_queue.push_packet(s_pkt) - -- self.seq_num = self.seq_num + 1 - -- end + out_queue.push_packet(s_pkt) + self.seq_num = self.seq_num + 1 + end -- send a SCADA management packet ---@param msg_type MGMT_TYPE @@ -120,8 +120,39 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then ---@cast pkt crdn_frame + local db = iocontrol.get_db() + -- handle packet by type - if pkt.type == nil then + if pkt.type == CRDN_TYPE.API_GET_FAC then + local fac = db.facility + + local data = { + fac.all_sys_ok, + fac.rtu_count, + fac.radiation, + { fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated }, + { fac.auto_current_waste_product, fac.auto_pu_fallback_active }, + util.table_len(fac.tank_data_tbl), + fac.induction_data_tbl[1] ~= nil, + fac.sps_data_tbl[1] ~= nil, + } + + _send(CRDN_TYPE.API_GET_FAC, data) + elseif pkt.type == CRDN_TYPE.API_GET_UNITS then + local data = {} + + for i = 1, #db.units do + local u = db.units[i] ---@type ioctl_unit + table.insert(data, { + u.unit_id, + u.num_boilers, + u.num_turbines, + u.num_snas, + u.has_tank + }) + end + + _send(CRDN_TYPE.API_GET_UNITS, data) else log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type) end diff --git a/graphics/core.lua b/graphics/core.lua index b931a3e..b4e1d94 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -7,7 +7,7 @@ local flasher = require("graphics.flasher") local core = {} -core.version = "2.2.2" +core.version = "2.2.3" core.flasher = flasher core.events = events diff --git a/graphics/element.lua b/graphics/element.lua index 5b6d70d..c7cc056 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -49,9 +49,11 @@ local element = {} ---|indicator_light_args ---|power_indicator_args ---|rad_indicator_args +---|signal_bar_args ---|state_indicator_args ---|tristate_indicator_light_args ---|vbar_args +---|app_multipane_args ---|colormap_args ---|displaybox_args ---|div_args diff --git a/graphics/elements/appmultipane.lua b/graphics/elements/appmultipane.lua new file mode 100644 index 0000000..5973182 --- /dev/null +++ b/graphics/elements/appmultipane.lua @@ -0,0 +1,109 @@ +-- App Page Multi-Pane Display Graphics Element + +local util = require("scada-common.util") + +local core = require("graphics.core") +local element = require("graphics.element") +local events = require("graphics.events") + +local MOUSE_CLICK = core.events.MOUSE_CLICK + +---@class app_multipane_args +---@field panes table panes to swap between +---@field nav_colors cpair on/off colors (a/b respectively) for page navigator +---@field scroll_nav boolean? true to allow scrolling to change the active pane +---@field drag_nav boolean? true to allow mouse dragging to change the active pane (on mouse up) +---@field callback function? function to call when pane is changed by mouse interaction +---@field parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer auto incremented if omitted +---@field width? integer parent width if omitted +---@field height? integer parent height if omitted +---@field gframe? graphics_frame frame instead of x/y/width/height +---@field fg_bg? cpair foreground/background colors +---@field hidden? boolean true to hide on initial draw + +-- new app multipane element +---@nodiscard +---@param args app_multipane_args +---@return graphics_element element, element_id id +local function multipane(args) + element.assert(type(args.panes) == "table", "panes is a required field") + + -- create new graphics element base object + local e = element.new(args) + + e.value = 1 + + local nav_x_start = math.floor((e.frame.w / 2) - (#args.panes / 2)) + 1 + local nav_x_end = math.floor((e.frame.w / 2) - (#args.panes / 2)) + #args.panes + + -- show the selected pane + function e.redraw() + for i = 1, #args.panes do args.panes[i].hide() end + args.panes[e.value].show() + + -- draw page indicator dots + for i = 1, #args.panes do + e.w_set_cur(nav_x_start + (i - 1), e.frame.h) + e.w_set_fgd(util.trinary(i == e.value, args.nav_colors.color_a, args.nav_colors.color_b)) + e.w_write("\x07") + end + end + + -- handle mouse interaction + ---@param event mouse_interaction mouse event + function e.handle_mouse(event) + local initial = e.value + + if e.enabled then + if event.current.y == e.frame.h and event.current.x >= nav_x_start and event.current.x <= nav_x_end then + local id = event.current.x - nav_x_start + 1 + + if event.type == MOUSE_CLICK.TAP then + e.set_value(id) + elseif event.type == MOUSE_CLICK.UP then + e.set_value(id) + end + end + end + + if args.scroll_nav then + if event.type == events.MOUSE_CLICK.SCROLL_DOWN then + e.set_value(e.value + 1) + elseif event.type == events.MOUSE_CLICK.SCROLL_UP then + e.set_value(e.value - 1) + end + end + + if args.drag_nav then + local x1, x2 = event.initial.x, event.current.x + if event.type == events.MOUSE_CLICK.UP and e.in_frame_bounds(x1, event.initial.y) and e.in_frame_bounds(x1, event.current.y) then + if x2 > x1 then + e.set_value(e.value - 1) + elseif x2 < x1 then + e.set_value(e.value + 1) + end + end + end + + if e.value ~= initial and type(args.callback) == "function" then args.callback(e.value) end + end + + -- select which pane is shown + ---@param value integer pane to show + function e.set_value(value) + if (e.value ~= value) and (value > 0) and (value <= #args.panes) then + e.value = value + e.redraw() + end + end + + -- initial draw + e.redraw() + + return e.complete() +end + +return multipane diff --git a/graphics/elements/controls/switch_button.lua b/graphics/elements/controls/switch_button.lua index ac44ca6..8abf8a7 100644 --- a/graphics/elements/controls/switch_button.lua +++ b/graphics/elements/controls/switch_button.lua @@ -65,7 +65,7 @@ local function switch_button(args) end end - -- set the value + -- set the value (does not call the callback) ---@param val boolean new value function e.set_value(val) e.value = val diff --git a/graphics/elements/indicators/signal.lua b/graphics/elements/indicators/signal.lua new file mode 100644 index 0000000..7e9c1b8 --- /dev/null +++ b/graphics/elements/indicators/signal.lua @@ -0,0 +1,85 @@ +-- Signal Bars Graphics Element + +local util = require("scada-common.util") + +local element = require("graphics.element") + +---@class signal_bar_args +---@field compact? boolean true to use a single character (works better against edges that extend out colors) +---@field colors_low_med? cpair color a for low signal quality, color b for medium signal quality +---@field disconnect_color? color color for the 'x' on disconnect +---@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 (foreground is used for high signal quality) +---@field hidden? boolean true to hide on initial draw + +-- new signal bar +---@nodiscard +---@param args signal_bar_args +---@return graphics_element element, element_id id +local function signal_bar(args) + args.height = 1 + args.width = util.trinary(args.compact, 1, 2) + + -- create new graphics element base object + local e = element.new(args) + + e.value = 0 + + local blit_bkg = args.fg_bg.blit_bkg + local blit_0, blit_1, blit_2, blit_3 = args.fg_bg.blit_fgd, args.fg_bg.blit_fgd, args.fg_bg.blit_fgd, args.fg_bg.blit_fgd + + if type(args.colors_low_med) == "table" then + blit_1 = args.colors_low_med.blit_a or blit_1 + blit_2 = args.colors_low_med.blit_b or blit_2 + end + + if util.is_int(args.disconnect_color) then blit_0 = colors.toBlit(args.disconnect_color) end + + -- on state change (0 = offline, 1 through 3 = low to high signal) + ---@param new_state integer signal state + function e.on_update(new_state) + e.value = new_state + e.redraw() + end + + -- set signal state (0 = offline, 1 through 3 = low to high signal) + ---@param val integer signal state + function e.set_value(val) e.on_update(val) end + + -- draw label and signal bar + function e.redraw() + e.w_set_cur(1, 1) + + if args.compact then + if e.value == 1 then + e.w_blit("\x90", blit_1, blit_bkg) + elseif e.value == 2 then + e.w_blit("\x94", blit_2, blit_bkg) + elseif e.value == 3 then + e.w_blit("\x95", blit_3, blit_bkg) + else + e.w_blit("x", blit_0, blit_bkg) + end + else + if e.value == 1 then + e.w_blit("\x9f ", blit_bkg .. blit_bkg, blit_1 .. blit_bkg) + elseif e.value == 2 then + e.w_blit("\x9f\x94", blit_bkg .. blit_2, blit_2 .. blit_bkg) + elseif e.value == 3 then + e.w_blit("\x9f\x81", blit_bkg .. blit_bkg, blit_3 .. blit_3) + else + e.w_blit(" x", blit_0 .. blit_0, blit_bkg .. blit_bkg) + end + end + end + + -- initial draw + e.redraw() + + return e.complete() +end + +return signal_bar diff --git a/pocket/iocontrol.lua b/pocket/iocontrol.lua index 30645ae..30f4879 100644 --- a/pocket/iocontrol.lua +++ b/pocket/iocontrol.lua @@ -2,18 +2,17 @@ -- I/O Control for Pocket Integration with Supervisor & Coordinator -- -local psil = require("scada-common.psil") - +local log = require("scada-common.log") +local psil = require("scada-common.psil") local types = require("scada-common.types") local ALARM = types.ALARM -local iocontrol = {} +---@todo nominal trip time is ping (0ms to 10ms usually) +local WARN_TT = 40 +local HIGH_TT = 80 ----@class pocket_ioctl -local io = { - ps = psil.create() -} +local iocontrol = {} ---@enum POCKET_LINK_STATE local LINK_STATE = { @@ -23,23 +22,175 @@ local LINK_STATE = { LINKED = 3 } ----@enum NAV_PAGE -local NAV_PAGE = { - HOME = 1, +iocontrol.LINK_STATE = LINK_STATE + +---@enum POCKET_APP_ID +local APP_ID = { + ROOT = 1, + -- main app page UNITS = 2, - REACTORS = 3, - BOILERS = 4, - TURBINES = 5, - DIAG = 6, - D_ALARMS = 7 + ABOUT = 3, + -- diag app page + ALARMS = 4, + -- other + DUMMY = 5, + NUM_APPS = 5 } -iocontrol.LINK_STATE = LINK_STATE -iocontrol.NAV_PAGE = NAV_PAGE +iocontrol.APP_ID = APP_ID + +---@class pocket_ioctl +local io = { + version = "unknown", + ps = psil.create() +} + +---@class nav_tree_page +---@field _p nav_tree_page|nil page's parent +---@field _c table page's children +---@field nav_to function function to navigate to this page +---@field switcher function|nil function to switch between children +---@field tasks table tasks to run while viewing this page + +-- allocate the page navigation system +function iocontrol.alloc_nav() + local self = { + pane = nil, ---@type graphics_element + apps = {}, + containers = {}, + cur_app = APP_ID.ROOT + } + + self.cur_page = self.root + + ---@class pocket_nav + io.nav = {} + + -- set the root pane element to switch between apps with + ---@param root_pane graphics_element + function io.nav.set_pane(root_pane) + self.pane = root_pane + end + + -- register an app + ---@param app_id POCKET_APP_ID app ID + ---@param container graphics_element element that contains this app (usually a Div) + ---@param pane graphics_element? multipane if this is a simple paned app, then nav_to must be a number + function io.nav.register_app(app_id, container, pane) + ---@class pocket_app + local app = { + root = { _p = nil, _c = {}, nav_to = function () end, tasks = {} }, ---@type nav_tree_page + cur_page = nil, ---@type nav_tree_page + pane = pane, + paned_pages = {} + } + + -- delayed set of the pane if it wasn't ready at the start + ---@param root_pane graphics_element multipane + function app.set_root_pane(root_pane) + app.pane = root_pane + end + + -- if a pane was provided, this will switch between numbered pages + ---@param idx integer page index + function app.switcher(idx) + if app.paned_pages[idx] then + app.paned_pages[idx].nav_to() + end + end + + -- create a new page entry in the app's page navigation tree + ---@param parent nav_tree_page? a parent page or nil to set this as the root + ---@param nav_to function|integer function to navigate to this page or pane index + ---@return nav_tree_page new_page this new page + function app.new_page(parent, nav_to) + ---@type nav_tree_page + local page = { _p = parent, _c = {}, nav_to = function () end, switcher = function () end, tasks = {} } + + if parent == nil then + app.root = page + if app.cur_page == nil then app.cur_page = page end + end + + if type(nav_to) == "number" then + app.paned_pages[nav_to] = page + + function page.nav_to() + app.cur_page = page + if app.pane then app.pane.set_value(nav_to) end + end + else + function page.nav_to() + app.cur_page = page + nav_to() + end + end + + -- switch between children + ---@param id integer child ID + function page.switcher(id) if page._c[id] then page._c[id].nav_to() end end + + if parent ~= nil then + table.insert(page._p._c, page) + end + + return page + end + + -- get the currently active page + function app.get_current_page() return app.cur_page end + + -- attempt to navigate up the tree + ---@return boolean success true if successfully navigated up + function app.nav_up() + local parent = app.cur_page._p + if parent then parent.nav_to() end + return parent ~= nil + end + + self.apps[app_id] = app + self.containers[app_id] = container + + return app + end + + -- get a list of the app containers (usually Div elements) + function io.nav.get_containers() return self.containers end + + -- open a given app + ---@param app_id POCKET_APP_ID + function io.nav.open_app(app_id) + if self.apps[app_id] then + self.cur_app = app_id + self.pane.set_value(app_id) + else + log.debug("tried to open unknown app") + end + end + + -- get the currently active page + ---@return nav_tree_page + function io.nav.get_current_page() + return self.apps[self.cur_app].get_current_page() + end + + -- attempt to navigate up + function io.nav.nav_up() + local app = self.apps[self.cur_app] ---@type pocket_app + log.debug("attempting app nav up for app " .. self.cur_app) + + if not app.nav_up() then + log.debug("internal app nav up failed, going to home screen") + io.nav.open_app(APP_ID.ROOT) + end + end +end -- initialize facility-independent components of pocket iocontrol ---@param comms pocket_comms function iocontrol.init_core(comms) + iocontrol.alloc_nav() + ---@class pocket_ioctl_diag io.diag = {} @@ -76,29 +227,135 @@ function iocontrol.init_core(comms) 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 +---@param conf facility_conf configuration +---@param temp_scale 1|2|3|4 temperature unit (1 = K, 2 = C, 3 = F, 4 = R) +function iocontrol.init_fac(conf, temp_scale) + -- temperature unit label and conversion function (from Kelvin) + if temp_scale == 2 then + io.temp_label = "\xb0C" + io.temp_convert = function (t) return t - 273.15 end + elseif temp_scale == 3 then + io.temp_label = "\xb0F" + io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end + elseif temp_scale == 4 then + io.temp_label = "\xb0R" + io.temp_convert = function (t) return 1.8 * t end + else + io.temp_label = "K" + io.temp_convert = function (t) return t end + end + + -- facility data structure + ---@class pioctl_facility + io.facility = { + num_units = conf.num_units, + tank_mode = conf.cooling.fac_tank_mode, + tank_defs = conf.cooling.fac_tank_defs, + all_sys_ok = false, + rtu_count = 0, + + auto_ready = false, + auto_active = false, + auto_ramping = false, + auto_saturated = false, + + ---@type WASTE_PRODUCT + auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM, + auto_pu_fallback_active = false, + + radiation = types.new_zero_radiation_reading(), + + ps = psil.create(), + + induction_ps_tbl = {}, + induction_data_tbl = {}, + + sps_ps_tbl = {}, + sps_data_tbl = {}, + + tank_ps_tbl = {}, + tank_data_tbl = {}, + + env_d_ps = psil.create(), + env_d_data = {} + } +end -- set network link state ---@param state POCKET_LINK_STATE -function iocontrol.report_link_state(state) io.ps.publish("link_state", state) end +function iocontrol.report_link_state(state) + io.ps.publish("link_state", state) + + if state == LINK_STATE.API_LINK_ONLY or state == LINK_STATE.UNLINKED then + io.ps.publish("svr_conn_quality", 0) + end + + if state == LINK_STATE.SV_LINK_ONLY or state == LINK_STATE.UNLINKED then + io.ps.publish("crd_conn_quality", 0) + end +end + +-- determine supervisor connection quality (trip time) +---@param trip_time integer +function iocontrol.report_svr_tt(trip_time) + local state = 3 + if trip_time > HIGH_TT then + state = 1 + elseif trip_time > WARN_TT then + state = 2 + end + + io.ps.publish("svr_conn_quality", state) +end + +-- determine coordinator connection quality (trip time) +---@param trip_time integer +function iocontrol.report_crd_tt(trip_time) + local state = 3 + if trip_time > HIGH_TT then + state = 1 + elseif trip_time > WARN_TT then + state = 2 + end + + io.ps.publish("crd_conn_quality", state) +end + +-- populate facility data from API_GET_FAC +---@param data table +---@return boolean valid +function iocontrol.record_facility_data(data) + local valid = true + + local fac = io.facility + + fac.all_sys_ok = data[1] + fac.rtu_count = data[2] + fac.radiation = data[3] + + -- auto control + if type(data[4]) == "table" and #data[4] == 4 then + fac.auto_ready = data[4][1] + fac.auto_active = data[4][2] + fac.auto_ramping = data[4][3] + fac.auto_saturated = data[4][4] + end + + -- waste + if type(data[5]) == "table" and #data[5] == 2 then + fac.auto_current_waste_product = data[5][1] + fac.auto_pu_fallback_active = data[5][2] + end + + fac.num_tanks = data[6] + fac.has_imatrix = data[7] + fac.has_sps = data[8] + + return valid +end -- get the IO controller database function iocontrol.get_db() return io end diff --git a/pocket/pocket.lua b/pocket/pocket.lua index 0bec6ba..63149e8 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -8,6 +8,7 @@ local PROTOCOL = comms.PROTOCOL local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK local MGMT_TYPE = comms.MGMT_TYPE +local CRDN_TYPE = comms.CRDN_TYPE local LINK_STATE = iocontrol.LINK_STATE @@ -125,7 +126,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) -- attempt coordinator API connection establishment local function _send_api_establish() - _send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) + _send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT, comms.api_version }) end -- keep alive ack to supervisor @@ -246,6 +247,25 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) return pkt end + ---@param packet mgmt_frame|crdn_frame + ---@param length integer + ---@param max integer? + ---@return boolean + local function _check_length(packet, length, max) + local ok = util.trinary(max == nil, packet.length == length, packet.length >= length and packet.length <= (max or 0)) + if not ok then + local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: packet length mismatch -> expect %d != actual %d" + log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type)) + end + return ok + end + + ---@param packet mgmt_frame|crdn_frame + local function _fail_type(packet) + local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: unrecognized packet type" + log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type)) + end + -- handle a packet ---@param packet mgmt_frame|crdn_frame|nil function public.handle_packet(packet) @@ -268,7 +288,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) return elseif self.api.linked and (src_addr ~= self.api.addr) then log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr .. - "); channel in use by another system?") + "); channel in use by another system?") return else self.api.r_seq_num = packet.scada_frame.seq_num() @@ -277,12 +297,24 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) -- feed watchdog on valid sequence number api_watchdog.feed() - if protocol == PROTOCOL.SCADA_MGMT then + if protocol == PROTOCOL.SCADA_CRDN then + ---@cast packet crdn_frame + if self.api.linked then + if packet.type == CRDN_TYPE.API_GET_FAC then + if _check_length(packet, 11) then + iocontrol.record_facility_data(packet.data) + end + elseif packet.type == CRDN_TYPE.API_GET_UNITS then + else _fail_type(packet) end + else + log.debug("discarding coordinator SCADA_CRDN packet before linked") + end + elseif protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame if self.api.linked then if packet.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back - if packet.length == 1 then + if _check_length(packet, 1) then local timestamp = packet.data[1] local trip_time = util.time() - timestamp @@ -290,11 +322,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) log.warning("pocket coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)") end - -- log.debug("pocket coordinator RTT = " .. trip_time .. "ms") + -- log.debug("pocket coordinator TT = " .. trip_time .. "ms") _send_api_keep_alive_ack(timestamp) - else - log.debug("coordinator SCADA keep alive packet length mismatch") + + iocontrol.report_crd_tt(trip_time) end elseif packet.type == MGMT_TYPE.CLOSE then -- handle session close @@ -303,24 +335,38 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) self.api.r_seq_num = nil self.api.addr = comms.BROADCAST log.info("coordinator server connection closed by remote host") - else - log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator") - end + else _fail_type(packet) end elseif packet.type == MGMT_TYPE.ESTABLISH then -- connection with coordinator established - if packet.length == 1 then + if _check_length(packet, 1, 2) 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 packet.length == 2 then + local fac_config = packet.data[2] - if self.sv.linked then - iocontrol.report_link_state(LINK_STATE.LINKED) + if type(fac_config) == "table" and #fac_config == 2 then + -- get configuration + local conf = { num_units = fac_config[1], cooling = fac_config[2] } + + ---@todo unit options + iocontrol.init_fac(conf, 1) + + 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 + else + log.debug("invalid facility configuration table received from coordinator, establish failed") + end else - iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY) + log.debug("received coordinator establish allow without facility configuration") end elseif est_ack == ESTABLISH_ACK.DENY then if self.api.last_est_ack ~= est_ack then @@ -334,13 +380,15 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) if self.api.last_est_ack ~= est_ack then log.info("coordinator comms version mismatch") end + elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then + if self.api.last_est_ack ~= est_ack then + log.info("coordinator api 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") @@ -372,7 +420,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) if self.sv.linked then if packet.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back - if packet.length == 1 then + if _check_length(packet, 1) then local timestamp = packet.data[1] local trip_time = util.time() - timestamp @@ -380,11 +428,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) log.warning("pocket supervisor KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)") end - -- log.debug("pocket supervisor RTT = " .. trip_time .. "ms") + -- log.debug("pocket supervisor TT = " .. trip_time .. "ms") _send_sv_keep_alive_ack(timestamp) - else - log.debug("supervisor SCADA keep alive packet length mismatch") + + iocontrol.report_svr_tt(trip_time) end elseif packet.type == MGMT_TYPE.CLOSE then -- handle session close @@ -394,12 +442,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) self.sv.addr = comms.BROADCAST log.info("supervisor server connection closed by remote host") elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then - if packet.length == 8 then + if _check_length(packet, 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 == MGMT_TYPE.DIAG_TONE_SET then if packet.length == 1 and packet.data[1] == false then @@ -438,12 +484,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) 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 + else _fail_type(packet) end elseif packet.type == MGMT_TYPE.ESTABLISH then -- connection with supervisor established - if packet.length == 1 then + if _check_length(packet, 1) then local est_ack = packet.data[1] if est_ack == ESTABLISH_ACK.ALLOW then @@ -474,15 +518,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) 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 - else - log.debug("illegal packet type " .. protocol .. " from supervisor", true) - end + else _fail_type(packet) end else log.debug("received packet from unconfigured channel " .. r_chan, true) end @@ -500,5 +540,4 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) return public end - return pocket diff --git a/pocket/startup.lua b/pocket/startup.lua index 5b7c91f..67e2f79 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -18,7 +18,7 @@ local iocontrol = require("pocket.iocontrol") local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") -local POCKET_VERSION = "v0.7.3-alpha" +local POCKET_VERSION = "v0.8.0-alpha" local println = util.println local println_ts = util.println_ts @@ -68,6 +68,9 @@ local function main() -- mount connected devices ppm.mount_all() + -- record version for GUI + iocontrol.get_db().version = POCKET_VERSION + ---------------------------------------- -- setup communications & clocks ---------------------------------------- @@ -131,7 +134,7 @@ local function main() -- start connection watchdogs conn_wd.sv.feed() conn_wd.api.feed() - log.debug("startup> conn watchdog started") + log.debug("startup> conn watchdogs started") local io_db = iocontrol.get_db() local nav = io_db.nav @@ -149,11 +152,8 @@ local function main() 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 + local page_tasks = nav.get_current_page().tasks + for i = 1, #page_tasks do page_tasks[i]() end loop_clock.start() elseif conn_wd.sv.is_timer(param1) then diff --git a/pocket/ui/pages/diag_page.lua b/pocket/ui/apps/diag_apps.lua similarity index 77% rename from pocket/ui/pages/diag_page.lua rename to pocket/ui/apps/diag_apps.lua index c28aba9..ec8b5e6 100644 --- a/pocket/ui/pages/diag_page.lua +++ b/pocket/ui/apps/diag_apps.lua @@ -1,58 +1,39 @@ +-- +-- Diagnostic Apps +-- + 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 ALIGN = core.ALIGN --- new diagnostics page view +-- create diagnostic app pages ---@param root graphics_element parent -local function new_view(root) +local function create_pages(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=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 alarm_test = Div{parent=root,x=1,y=1} + + local alarm_app = db.nav.register_app(iocontrol.APP_ID.ALARMS, alarm_test) + + local page = alarm_app.new_page(nil, function () end) + page.tasks = { db.diag.tone_test.get_tone_states } local ttest = db.diag.tone_test @@ -67,8 +48,6 @@ local function new_view(root) ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=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=ALIGN.CENTER,fg_bg=audio.get_fg_bg()} @@ -132,16 +111,6 @@ local function new_view(root) 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 +return create_pages diff --git a/pocket/ui/apps/dummy_app.lua b/pocket/ui/apps/dummy_app.lua new file mode 100644 index 0000000..fe21db3 --- /dev/null +++ b/pocket/ui/apps/dummy_app.lua @@ -0,0 +1,24 @@ +-- +-- Placeholder App +-- + +local iocontrol = require("pocket.iocontrol") + +local core = require("graphics.core") + +local Div = require("graphics.elements.div") +local TextBox = require("graphics.elements.textbox") + +-- create placeholder app page +---@param root graphics_element parent +local function create_pages(root) + local db = iocontrol.get_db() + + local main = Div{parent=root,x=1,y=1} + + db.nav.register_app(iocontrol.APP_ID.DUMMY, main).new_page(nil, function () end) + + TextBox{parent=main,text="This app is not implemented yet.",x=1,y=2,alignment=core.ALIGN.CENTER} +end + +return create_pages diff --git a/pocket/ui/apps/sys_apps.lua b/pocket/ui/apps/sys_apps.lua new file mode 100644 index 0000000..a48f85f --- /dev/null +++ b/pocket/ui/apps/sys_apps.lua @@ -0,0 +1,102 @@ +-- +-- System Apps +-- + +local comms = require("scada-common.comms") +local lockbox = require("lockbox") +local util = require("scada-common.util") + +local iocontrol = require("pocket.iocontrol") + +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 PushButton = require("graphics.elements.controls.push_button") + +local cpair = core.cpair + +local ALIGN = core.ALIGN + +-- create system app pages +---@param root graphics_element parent +local function create_pages(root) + local db = iocontrol.get_db() + + ---------------- + -- About Page -- + ---------------- + + local about_root = Div{parent=root,x=1,y=1} + + local about_app = db.nav.register_app(iocontrol.APP_ID.ABOUT, about_root) + + local about_page = about_app.new_page(nil, 1) + local fw_page = about_app.new_page(about_page, 2) + local hw_page = about_app.new_page(about_page, 3) + + local about = Div{parent=about_root,x=1,y=2} + + TextBox{parent=about,y=1,text="System Information",height=1,alignment=ALIGN.CENTER} + + local btn_fg_bg = cpair(colors.lightBlue, colors.black) + local btn_active = cpair(colors.white, colors.black) + local label = cpair(colors.lightGray, colors.black) + + PushButton{parent=about,x=2,y=3,text="Firmware >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fw_page.nav_to} + PushButton{parent=about,x=2,y=4,text="Host Details >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=hw_page.nav_to} + + local fw_div = Div{parent=about_root,x=1,y=2} + TextBox{parent=fw_div,y=1,text="Firmware Versions",height=1,alignment=ALIGN.CENTER} + + PushButton{parent=fw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to} + + local fw_list_box = ListBox{parent=fw_div,x=1,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)} + + local fw_list = Div{parent=fw_list_box,x=1,y=2,height=18} + + TextBox{parent=fw_list,x=2,text="Pocket Version",height=1,alignment=ALIGN.LEFT,fg_bg=label} + TextBox{parent=fw_list,x=2,text=db.version,height=1,alignment=ALIGN.LEFT} + + fw_list.line_break() + TextBox{parent=fw_list,x=2,text="Comms Version",height=1,alignment=ALIGN.LEFT,fg_bg=label} + TextBox{parent=fw_list,x=2,text=comms.version,height=1,alignment=ALIGN.LEFT} + + fw_list.line_break() + TextBox{parent=fw_list,x=2,text="API Version",height=1,alignment=ALIGN.LEFT,fg_bg=label} + TextBox{parent=fw_list,x=2,text=comms.api_version,height=1,alignment=ALIGN.LEFT} + + fw_list.line_break() + TextBox{parent=fw_list,x=2,text="Common Lib Version",height=1,alignment=ALIGN.LEFT,fg_bg=label} + TextBox{parent=fw_list,x=2,text=util.version,height=1,alignment=ALIGN.LEFT} + + fw_list.line_break() + TextBox{parent=fw_list,x=2,text="Graphics Version",height=1,alignment=ALIGN.LEFT,fg_bg=label} + TextBox{parent=fw_list,x=2,text=core.version,height=1,alignment=ALIGN.LEFT} + + fw_list.line_break() + TextBox{parent=fw_list,x=2,text="Lockbox Version",height=1,alignment=ALIGN.LEFT,fg_bg=label} + TextBox{parent=fw_list,x=2,text=lockbox.version,height=1,alignment=ALIGN.LEFT} + + local hw_div = Div{parent=about_root,x=1,y=2} + TextBox{parent=hw_div,y=1,text="Host Versions",height=1,alignment=ALIGN.CENTER} + + PushButton{parent=hw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to} + + hw_div.line_break() + TextBox{parent=hw_div,x=2,text="Lua Version",height=1,alignment=ALIGN.LEFT,fg_bg=label} + TextBox{parent=hw_div,x=2,text=_VERSION,height=1,alignment=ALIGN.LEFT} + + hw_div.line_break() + TextBox{parent=hw_div,x=2,text="Environment",height=1,alignment=ALIGN.LEFT,fg_bg=label} + TextBox{parent=hw_div,x=2,text=_HOST,height=6,alignment=ALIGN.LEFT} + + local root_pane = MultiPane{parent=about_root,x=1,y=1,panes={about,fw_div,hw_div}} + + about_app.set_root_pane(root_pane) +end + +return create_pages diff --git a/pocket/ui/main.lua b/pocket/ui/main.lua index 4cd70c6..02e33fd 100644 --- a/pocket/ui/main.lua +++ b/pocket/ui/main.lua @@ -4,27 +4,29 @@ local iocontrol = require("pocket.iocontrol") -local style = require("pocket.ui.style") +local diag_apps = require("pocket.ui.apps.diag_apps") +local dummy_app = require("pocket.ui.apps.dummy_app") +local sys_apps = require("pocket.ui.apps.sys_apps") local conn_waiting = require("pocket.ui.components.conn_waiting") -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 style = require("pocket.ui.style") + local core = require("graphics.core") local Div = require("graphics.elements.div") local MultiPane = require("graphics.elements.multipane") local TextBox = require("graphics.elements.textbox") +local PushButton = require("graphics.elements.controls.push_button") local Sidebar = require("graphics.elements.controls.sidebar") +local SignalBar = require("graphics.elements.indicators.signal") + local LINK_STATE = iocontrol.LINK_STATE -local NAV_PAGE = iocontrol.NAV_PAGE local ALIGN = core.ALIGN @@ -33,26 +35,27 @@ 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 + local db = iocontrol.get_db() -- window header message - TextBox{parent=main,y=1,text="",alignment=ALIGN.LEFT,height=1,fg_bg=style.header} + TextBox{parent=main,y=1,text="DEV ALPHA APP S C ",alignment=ALIGN.LEFT,height=1,fg_bg=style.header} + local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)} + local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)} - -- - -- root panel panes (connection screens + main screen) - -- + db.ps.subscribe("svr_conn_quality", svr_conn.set_value) + db.ps.subscribe("crd_conn_quality", crd_conn.set_value) + + --#region root panel panes (connection screens + main screen) local root_pane_div = Div{parent=main,x=1,y=2} local conn_sv_wait = conn_waiting(root_pane_div, 6, false) local conn_api_wait = conn_waiting(root_pane_div, 6, true) local main_pane = Div{parent=main,x=1,y=2} - local root_panes = { conn_sv_wait, conn_api_wait, main_pane } - local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes=root_panes} + local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}} - root_pane.register(ps, "link_state", function (state) + root_pane.register(db.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 == LINK_STATE.SV_LINK_ONLY then @@ -62,62 +65,33 @@ local function init(main) end end) - -- - -- main page panel panes & sidebar - -- + --#endregion + + --#region main page panel panes & sidebar local page_div = Div{parent=main_pane,x=4,y=1} local sidebar_tabs = { - { - char = "#", - color = cpair(colors.black,colors.green) - }, - { - char = "U", - color = cpair(colors.black,colors.yellow) - }, - { - char = "R", - color = cpair(colors.black,colors.cyan) - }, - { - char = "B", - color = cpair(colors.black,colors.lightGray) - }, - { - char = "T", - color = cpair(colors.black,colors.white) - }, - { - char = "D", - color = cpair(colors.black,colors.orange) - } + { char = "#", color = cpair(colors.black, colors.green) } } - 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) } + home_page(page_div) + unit_page(page_div) - local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} + diag_apps(page_div) + sys_apps(page_div) + dummy_app(page_div) - 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 + assert(#db.nav.get_containers() == iocontrol.APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered") - page_pane.set_value(page) - end + local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=db.nav.get_containers()} + db.nav.set_pane(page_pane) - Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=navigate_sidebar} + Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=db.nav.open_app} + + PushButton{parent=main_pane,x=1,y=19,text="\x1b",min_width=3,fg_bg=cpair(colors.white,colors.gray),active_fg_bg=cpair(colors.gray,colors.black),callback=db.nav.nav_up} + + --#endregion end return init diff --git a/pocket/ui/pages/boiler_page.lua b/pocket/ui/pages/boiler_page.lua deleted file mode 100644 index ff92b05..0000000 --- a/pocket/ui/pages/boiler_page.lua +++ /dev/null @@ -1,22 +0,0 @@ --- local style = require("pocket.ui.style") - -local core = require("graphics.core") - -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") - --- local cpair = core.cpair - -local ALIGN = core.ALIGN - --- new boiler 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="BOILERS",x=1,y=1,height=1,alignment=ALIGN.CENTER} - - return main -end - -return new_view diff --git a/pocket/ui/pages/home_page.lua b/pocket/ui/pages/home_page.lua index d192796..0ea7c3f 100644 --- a/pocket/ui/pages/home_page.lua +++ b/pocket/ui/pages/home_page.lua @@ -1,21 +1,59 @@ -local core = require("graphics.core") +-- +-- Main Home Page +-- -local Div = require("graphics.elements.div") +local iocontrol = require("pocket.iocontrol") -local App = require("graphics.elements.controls.app") +local core = require("graphics.core") + +local AppMultiPane = require("graphics.elements.appmultipane") +local Div = require("graphics.elements.div") +local TextBox = require("graphics.elements.textbox") + +local App = require("graphics.elements.controls.app") local cpair = core.cpair +local APP_ID = iocontrol.APP_ID + +local ALIGN = core.ALIGN + -- new home page view ---@param root graphics_element parent local function new_view(root) - local main = Div{parent=root,x=1,y=1} + local db = iocontrol.get_db() - 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)} + local main = Div{parent=root,x=1,y=1,height=19} + + local app = db.nav.register_app(iocontrol.APP_ID.ROOT, main) + + local apps_1 = Div{parent=main,x=1,y=1,height=15} + local apps_2 = Div{parent=main,x=1,y=1,height=15} + + local panes = { apps_1, apps_2 } + + local app_pane = AppMultiPane{parent=main,x=1,y=1,height=18,panes=panes,active_color=colors.lightGray,nav_colors=cpair(colors.lightGray,colors.gray),scroll_nav=true,drag_nav=true,callback=app.switcher} + + app.set_root_pane(app_pane) + app.new_page(app.new_page(nil, 1), 2) + + local function open(id) db.nav.open_app(id) end + + local active_fg_bg = cpair(colors.white,colors.gray) + + App{parent=apps_1,x=3,y=2,text="U",title="Units",callback=function()open(APP_ID.UNITS)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg} + App{parent=apps_1,x=10,y=2,text="\x17",title="PRC",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg} + App{parent=apps_1,x=17,y=2,text="\x15",title="CTL",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg} + App{parent=apps_1,x=3,y=7,text="\x08",title="DEV",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=active_fg_bg} + App{parent=apps_1,x=10,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg} + App{parent=apps_1,x=17,y=7,text="\xb6",title="Guide",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg} + App{parent=apps_1,x=3,y=12,text="?",title="About",callback=function()open(APP_ID.ABOUT)end,app_fg_bg=cpair(colors.black,colors.white),active_fg_bg=active_fg_bg} + + TextBox{parent=apps_2,text="Diagnostic Apps",x=1,y=2,height=1,alignment=ALIGN.CENTER} + + App{parent=apps_2,x=3,y=4,text="\x0f",title="Alarm",callback=function()open(APP_ID.ALARMS)end,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=active_fg_bg} + App{parent=apps_2,x=10,y=4,text="\x1e",title="LoopT",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg} + App{parent=apps_2,x=17,y=4,text="@",title="Comps",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg} return main end diff --git a/pocket/ui/pages/reactor_page.lua b/pocket/ui/pages/reactor_page.lua deleted file mode 100644 index ae11436..0000000 --- a/pocket/ui/pages/reactor_page.lua +++ /dev/null @@ -1,22 +0,0 @@ --- local style = require("pocket.ui.style") - -local core = require("graphics.core") - -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") - --- local cpair = core.cpair - -local ALIGN = core.ALIGN - --- new reactor 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="REACTOR",x=1,y=1,height=1,alignment=ALIGN.CENTER} - - return main -end - -return new_view diff --git a/pocket/ui/pages/turbine_page.lua b/pocket/ui/pages/turbine_page.lua deleted file mode 100644 index 41d656c..0000000 --- a/pocket/ui/pages/turbine_page.lua +++ /dev/null @@ -1,22 +0,0 @@ --- local style = require("pocket.ui.style") - -local core = require("graphics.core") - -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") - --- local cpair = core.cpair - -local ALIGN = core.ALIGN - --- new turbine 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="TURBINES",x=1,y=1,height=1,alignment=ALIGN.CENTER} - - return main -end - -return new_view diff --git a/pocket/ui/pages/unit_page.lua b/pocket/ui/pages/unit_page.lua index de61e81..273ad27 100644 --- a/pocket/ui/pages/unit_page.lua +++ b/pocket/ui/pages/unit_page.lua @@ -1,20 +1,29 @@ --- local style = require("pocket.ui.style") +-- +-- Unit Overview Page +-- -local core = require("graphics.core") +local iocontrol = require("pocket.iocontrol") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local core = require("graphics.core") --- local cpair = core.cpair +local Div = require("graphics.elements.div") +local TextBox = require("graphics.elements.textbox") local ALIGN = core.ALIGN -- new unit 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} - TextBox{parent=main,text="UNITS",x=1,y=1,height=1,alignment=ALIGN.CENTER} + local app = db.nav.register_app(iocontrol.APP_ID.UNITS, main) + app.new_page(nil, function () end) + + TextBox{parent=main,y=2,text="UNITS",height=1,alignment=ALIGN.CENTER} + + TextBox{parent=main,y=4,text="work in progress",height=1,alignment=ALIGN.CENTER} return main end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 180e9c8..fffaaee 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -16,8 +16,9 @@ local max_distance = nil ---@class comms local comms = {} --- protocol/data version (protocol/data independent changes tracked by util.lua version) -comms.version = "2.4.5" +-- protocol/data versions (protocol/data independent changes tracked by util.lua version) +comms.version = "2.5.0" +comms.api_version = "0.0.1" ---@enum PROTOCOL local PROTOCOL = { @@ -64,7 +65,9 @@ local CRDN_TYPE = { 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 + UNIT_CMD = 6, -- command a reactor unit + API_GET_FAC = 7, -- API: get all the facility data + API_GET_UNITS = 8 -- API: get all the reactor unit data } ---@enum ESTABLISH_ACK @@ -72,7 +75,8 @@ 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 + BAD_VERSION = 3, -- link denied due to comms version mismatch + BAD_API_VERSION = 4 -- link denied due to api version mismatch } ---@enum DEVICE_TYPE device types for establish messages