diff --git a/coordinator/configure.lua b/coordinator/configure.lua index 54fb854..e763a27 100644 --- a/coordinator/configure.lua +++ b/coordinator/configure.lua @@ -1449,9 +1449,11 @@ function configurator.configure(start_code, message) elseif event == "paste" then display.handle_paste(param1) elseif event == "peripheral_detach" then +---@diagnostic disable-next-line: discard-returns ppm.handle_unmount(param1) tool_ctl.gen_mon_list() elseif event == "peripheral" then +---@diagnostic disable-next-line: discard-returns ppm.mount(param1) tool_ctl.gen_mon_list() elseif event == "monitor_resize" then diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 9aa3dbc..bf73e1f 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -228,6 +228,8 @@ function iocontrol.init(conf, comms, temp_scale) ---@class ioctl_unit local entry = { unit_id = i, + connected = false, + rtu_hw = { boilers = {}, turbines = {} }, num_boilers = 0, num_turbines = 0, @@ -319,12 +321,14 @@ function iocontrol.init(conf, comms, temp_scale) for _ = 1, conf.cooling.r_cool[i].BoilerCount do table.insert(entry.boiler_ps_tbl, psil.create()) table.insert(entry.boiler_data_tbl, {}) + table.insert(entry.rtu_hw.boilers, { connected = false, faulted = false }) end -- create turbine tables for _ = 1, conf.cooling.r_cool[i].TurbineCount do table.insert(entry.turbine_ps_tbl, psil.create()) table.insert(entry.turbine_data_tbl, {}) + table.insert(entry.rtu_hw.turbines, { connected = false, faulted = false }) end -- create tank tables @@ -897,6 +901,7 @@ function iocontrol.update_unit_statuses(statuses) end if #reactor_status == 0 then + unit.connected = false unit.unit_ps.publish("computed_status", 1) -- disconnected elseif #reactor_status == 3 then local mek_status = reactor_status[1] @@ -956,6 +961,8 @@ function iocontrol.update_unit_statuses(statuses) unit.unit_ps.publish(key, val) end end + + unit.connected = true else log.debug(log_header .. "reactor status length mismatch") valid = false @@ -970,7 +977,10 @@ function iocontrol.update_unit_statuses(statuses) local boil_sum = 0 for id = 1, #unit.boiler_ps_tbl do - if rtu_statuses.boilers[id] == nil then + local connected = rtu_statuses.boilers[id] ~= nil + unit.rtu_hw.boilers[id].connected = connected + + if not connected then -- disconnected unit.boiler_ps_tbl[id].publish("computed_status", 1) end @@ -982,6 +992,7 @@ function iocontrol.update_unit_statuses(statuses) local ps = unit.boiler_ps_tbl[id] ---@type psil local rtu_faulted = _record_multiblock_status(boiler, data, ps) + unit.rtu_hw.boilers[id].faulted = rtu_faulted if rtu_faulted then ps.publish("computed_status", 3) -- faulted @@ -1013,7 +1024,10 @@ function iocontrol.update_unit_statuses(statuses) local flow_sum = 0 for id = 1, #unit.turbine_ps_tbl do - if rtu_statuses.turbines[id] == nil then + local connected = rtu_statuses.turbines[id] ~= nil + unit.rtu_hw.turbines[id].connected = connected + + if not connected then -- disconnected unit.turbine_ps_tbl[id].publish("computed_status", 1) end @@ -1025,6 +1039,7 @@ function iocontrol.update_unit_statuses(statuses) local ps = unit.turbine_ps_tbl[id] ---@type psil local rtu_faulted = _record_multiblock_status(turbine, data, ps) + unit.rtu_hw.turbines[id].faulted = rtu_faulted if rtu_faulted then ps.publish("computed_status", 3) -- faulted diff --git a/coordinator/session/pocket.lua b/coordinator/session/pocket.lua index 7101297..09ec15c 100644 --- a/coordinator/session/pocket.lua +++ b/coordinator/session/pocket.lua @@ -138,21 +138,26 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) } _send(CRDN_TYPE.API_GET_FAC, data) - elseif pkt.type == CRDN_TYPE.API_GET_UNITS then - local data = {} + elseif pkt.type == CRDN_TYPE.API_GET_UNIT then + if pkt.length == 1 and type(pkt.data[1]) == "number" then + local u = db.units[pkt.data[1]] ---@type ioctl_unit - 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 - }) + if u then + local data = { + u.unit_id, + u.connected, + u.rtu_hw, + u.alarms, + u.annunciator, + u.reactor_data, + u.boiler_data_tbl, + u.turbine_data_tbl, + u.tank_data_tbl + } + + _send(CRDN_TYPE.API_GET_UNIT, data) + end 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/coordinator/startup.lua b/coordinator/startup.lua index f4de35d..0b8a06c 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") local threads = require("coordinator.threads") -local COORDINATOR_VERSION = "v1.4.5" +local COORDINATOR_VERSION = "v1.4.6" local CHUNK_LOAD_DELAY_S = 30.0 diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index fe36a85..2c26b98 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -183,7 +183,7 @@ local function init(parent, id) local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=ind_yel} local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=ind_red} local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=ind_yel} - local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel} + local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel} local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=ind_red} local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=ind_yel} local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=ind_yel} diff --git a/graphics/core.lua b/graphics/core.lua index b4e1d94..830ca69 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -7,7 +7,7 @@ local flasher = require("graphics.flasher") local core = {} -core.version = "2.2.3" +core.version = "2.2.4" core.flasher = flasher core.events = events diff --git a/graphics/element.lua b/graphics/element.lua index c7cc056..731a22c 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -198,6 +198,9 @@ function element.new(args, child_offset_x, child_offset_y) ---@param offset_y integer y offset for mouse events ---@param next_y integer next line if no y was provided function protected.prepare_template(offset_x, offset_y, next_y) + -- don't auto incrememnt y if inheriting height, that would cause an assertion + next_y = util.trinary(args.height == nil, 1, next_y) + -- record offsets in case there is a reposition self.offset_x = offset_x self.offset_y = offset_y diff --git a/graphics/elements/controls/app.lua b/graphics/elements/controls/app.lua index f610393..4ac936d 100644 --- a/graphics/elements/controls/app.lua +++ b/graphics/elements/controls/app.lua @@ -30,7 +30,7 @@ local function app_button(args) element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field") args.height = 4 - args.width = 5 + args.width = 7 -- create new graphics element base object local e = element.new(args) @@ -46,7 +46,7 @@ local function app_button(args) end -- draw icon - e.w_set_cur(1, 1) + e.w_set_cur(2, 1) e.w_set_fgd(fgd) e.w_set_bkg(bkg) e.w_write("\x9f\x83\x83\x83") @@ -55,16 +55,16 @@ local function app_button(args) e.w_write("\x90") e.w_set_fgd(fgd) e.w_set_bkg(bkg) - e.w_set_cur(1, 2) + e.w_set_cur(2, 2) e.w_write("\x95 ") e.w_set_fgd(bkg) e.w_set_bkg(fgd) e.w_write("\x95") - e.w_set_cur(1, 3) + e.w_set_cur(2, 3) e.w_write("\x82\x8f\x8f\x8f\x81") -- write the icon text - e.w_set_cur(3, 2) + e.w_set_cur(4, 2) e.w_set_fgd(fgd) e.w_set_bkg(bkg) e.w_write(args.text) diff --git a/graphics/elements/controls/sidebar.lua b/graphics/elements/controls/sidebar.lua index fef8a8a..58e8b13 100644 --- a/graphics/elements/controls/sidebar.lua +++ b/graphics/elements/controls/sidebar.lua @@ -8,13 +8,7 @@ local element = require("graphics.element") local MOUSE_CLICK = core.events.MOUSE_CLICK ----@class sidebar_tab ----@field char string character identifier ----@field color cpair tab colors (fg/bg) - ---@class sidebar_args ----@field tabs table sidebar tab options ----@field callback function function to call on tab change ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -27,21 +21,16 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK ---@param args sidebar_args ---@return graphics_element element, element_id id local function sidebar(args) - element.assert(type(args.tabs) == "table", "tabs is a required field") - element.assert(#args.tabs > 0, "at least one tab is required") - element.assert(type(args.callback) == "function", "callback is a required field") - args.width = 3 -- create new graphics element base object local e = element.new(args) - element.assert(e.frame.h >= (#args.tabs * 3), "height insufficent to display all tabs") - -- default to 1st tab e.value = 1 local was_pressed = false + local tabs = {} -- show the button state ---@param pressed? boolean if the currently selected tab should appear as actively pressed @@ -51,10 +40,18 @@ local function sidebar(args) was_pressed = pressed pressed_idx = pressed_idx or e.value - for i = 1, #args.tabs do - local tab = args.tabs[i] ---@type sidebar_tab + -- clear + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) + for y = 1, e.frame.h do + e.w_set_cur(1, y) + e.w_write(" ") + end - local y = ((i - 1) * 3) + 1 + -- draw tabs + for i = 1, #tabs do + local tab = tabs[i] ---@type sidebar_tab + local y = tab.y_start e.w_set_cur(1, y) @@ -66,13 +63,29 @@ local function sidebar(args) e.w_set_bkg(tab.color.bkg) end - e.w_write(" ") - e.w_set_cur(1, y + 1) - if e.value == i then - e.w_write(" " .. tab.char .. "\x10") - else e.w_write(" " .. tab.char .. " ") end - e.w_set_cur(1, y + 2) - e.w_write(" ") + if tab.tall then + e.w_write(" ") + e.w_set_cur(1, y + 1) + end + + e.w_write(tab.label) + + if tab.tall then + e.w_set_cur(1, y + 2) + e.w_write(" ") + end + end + end + + -- determine which tab was pressed + ---@param y integer y coordinate + local function find_tab(y) + for i = 1, #tabs do + local tab = tabs[i] ---@type sidebar_tab + + if y >= tab.y_start and y <= tab.y_end then + return i + end end end @@ -81,23 +94,25 @@ local function sidebar(args) function e.handle_mouse(event) -- determine what was pressed if e.enabled then - local cur_idx = math.ceil(event.current.y / 3) - local ini_idx = math.ceil(event.initial.y / 3) + local cur_idx = find_tab(event.current.y) + local ini_idx = find_tab(event.initial.y) + local tab = tabs[cur_idx] - if args.tabs[cur_idx] ~= nil then + -- handle press if a callback was provided + if tab ~= nil and type(tab.callback) == "function" then if event.type == MOUSE_CLICK.TAP then e.value = cur_idx draw(true) -- show as unpressed in 0.25 seconds tcd.dispatch(0.25, function () draw(false) end) - args.callback(e.value) + tab.callback() elseif event.type == MOUSE_CLICK.DOWN then draw(true, cur_idx) elseif event.type == MOUSE_CLICK.UP then if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then e.value = cur_idx draw(false) - args.callback(e.value) + tab.callback() else draw(false) end end elseif event.type == MOUSE_CLICK.UP then @@ -113,6 +128,35 @@ local function sidebar(args) draw(false) end + -- update the sidebar navigation options + ---@param items table sidebar entries + function e.on_update(items) + local next_y = 1 + + tabs = {} + + for i = 1, #items do + local item = items[i] + local height = util.trinary(item.tall, 3, 1) + + ---@class sidebar_tab + local entry = { + y_start = next_y, ---@type integer + y_end = next_y + height - 1, ---@type integer + tall = item.tall, ---@type boolean + label = item.label, ---@type string + color = item.color, ---@type cpair + callback = item.callback ---@type function|nil + } + + next_y = next_y + height + + tabs[i] = entry + end + + draw() + end + -- element redraw e.redraw = draw diff --git a/graphics/elements/indicators/icon.lua b/graphics/elements/indicators/icon.lua index 5c2fc4e..15aef3a 100644 --- a/graphics/elements/indicators/icon.lua +++ b/graphics/elements/indicators/icon.lua @@ -9,7 +9,7 @@ local element = require("graphics.element") ---@class icon_indicator_args ---@field label string indicator label ---@field states table state color and symbol table ----@field value? integer default state, defaults to 1 +---@field value? integer|boolean default state, defaults to 1 (true = 2, false = 1) ---@field min_label_width? integer label length if omitted ---@field parent graphics_element ---@field id? string element id @@ -33,6 +33,7 @@ local function icon(args) local e = element.new(args) e.value = args.value or 1 + if e.value == true then e.value = 2 end -- state blit strings local state_blit_cmds = {} @@ -47,8 +48,11 @@ local function icon(args) end -- on state change - ---@param new_state integer indicator state + ---@param new_state integer|boolean indicator state function e.on_update(new_state) + new_state = new_state or 1 + if new_state == true then new_state = 2 end + local blit_cmd = state_blit_cmds[new_state] e.value = new_state e.w_set_cur(1, 1) @@ -56,7 +60,7 @@ local function icon(args) end -- set indicator state - ---@param val integer indicator state + ---@param val integer|boolean indicator state function e.set_value(val) e.on_update(val) end -- element redraw diff --git a/pocket/iocontrol.lua b/pocket/iocontrol.lua index 30f4879..f6cbadb 100644 --- a/pocket/iocontrol.lua +++ b/pocket/iocontrol.lua @@ -5,8 +5,10 @@ local log = require("scada-common.log") local psil = require("scada-common.psil") local types = require("scada-common.types") +local util = require("scada-common.util") local ALARM = types.ALARM +local ALARM_STATE = types.ALARM_STATE ---@todo nominal trip time is ping (0ms to 10ms usually) local WARN_TT = 40 @@ -72,6 +74,10 @@ function iocontrol.alloc_nav() self.pane = root_pane end + function io.nav.set_sidebar(sidebar) + self.sidebar = sidebar + end + -- register an app ---@param app_id POCKET_APP_ID app ID ---@param container graphics_element element that contains this app (usually a Div) @@ -79,18 +85,36 @@ function iocontrol.alloc_nav() 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 + loaded = false, + load = nil, cur_page = nil, ---@type nav_tree_page pane = pane, - paned_pages = {} + paned_pages = {}, + sidebar_items = {} } + app.load = function () app.loaded = true end + -- 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 + function app.set_sidebar(items) + app.sidebar_items = items + if self.sidebar then self.sidebar.update(items) end + end + + -- function to run on initial load into memory + ---@param on_load function callback + function app.set_on_load(on_load) + app.load = function () + on_load() + app.loaded = true + end + end + -- if a pane was provided, this will switch between numbered pages ---@param idx integer page index function app.switcher(idx) @@ -107,9 +131,8 @@ function iocontrol.alloc_nav() ---@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 + if parent == nil and app.cur_page == nil then + app.cur_page = page end if type(nav_to) == "number" then @@ -160,9 +183,16 @@ function iocontrol.alloc_nav() -- open a given app ---@param app_id POCKET_APP_ID function io.nav.open_app(app_id) - if self.apps[app_id] then + local app = self.apps[app_id] ---@type pocket_app + if app then + if not app.loaded then app.load() end + self.cur_app = app_id self.pane.set_value(app_id) + + if #app.sidebar_items > 0 then + self.sidebar.update(app.sidebar_items) + end else log.debug("tried to open unknown app") end @@ -227,6 +257,12 @@ function iocontrol.init_core(comms) alarm_buttons = {}, tone_indicators = {} -- indicators to update from supervisor tone states } + + -- API access + ---@class pocket_ioctl_api + io.api = { + get_unit = function (unit) comms.api__get_unit(unit) end + } end -- initialize facility-dependent components of pocket iocontrol @@ -262,6 +298,16 @@ function iocontrol.init_fac(conf, temp_scale) auto_ramping = false, auto_saturated = false, + auto_scram = false, + ---@type ascram_status + ascram_status = { + matrix_dc = false, + matrix_fill = false, + crit_alarm = false, + radiation = false, + gen_fault = false + }, + ---@type WASTE_PRODUCT auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM, auto_pu_fallback_active = false, @@ -282,11 +328,188 @@ function iocontrol.init_fac(conf, temp_scale) env_d_ps = psil.create(), env_d_data = {} } + + -- create induction and SPS tables (currently only 1 of each is supported) + table.insert(io.facility.induction_ps_tbl, psil.create()) + table.insert(io.facility.induction_data_tbl, {}) + table.insert(io.facility.sps_ps_tbl, psil.create()) + table.insert(io.facility.sps_data_tbl, {}) + + -- determine tank information + if io.facility.tank_mode == 0 then + io.facility.tank_defs = {} + -- on facility tank mode 0, setup tank defs to match unit tank option + for i = 1, conf.num_units do + io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0) + end + + io.facility.tank_list = { table.unpack(io.facility.tank_defs) } + else + -- decode the layout of tanks from the connections definitions + local tank_mode = io.facility.tank_mode + local tank_defs = io.facility.tank_defs + local tank_list = { table.unpack(tank_defs) } + + local function calc_fdef(start_idx, end_idx) + local first = 4 + for i = start_idx, end_idx do + if io.facility.tank_defs[i] == 2 then + if i < first then first = i end + end + end + return first + end + + if tank_mode == 1 then + -- (1) 1 total facility tank (A A A A) + local first_fdef = calc_fdef(1, #tank_defs) + for i = 1, #tank_defs do + if i > first_fdef and tank_defs[i] == 2 then + tank_list[i] = 0 + end + end + elseif tank_mode == 2 then + -- (2) 2 total facility tanks (A A A B) + local first_fdef = calc_fdef(1, math.min(3, #tank_defs)) + for i = 1, #tank_defs do + if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then + tank_list[i] = 0 + end + end + elseif tank_mode == 3 then + -- (3) 2 total facility tanks (A A B B) + for _, a in pairs({ 1, 3 }) do + local b = a + 1 + if (tank_defs[a] == 2) and (tank_defs[b] == 2) then + tank_list[b] = 0 + end + end + elseif tank_mode == 4 then + -- (4) 2 total facility tanks (A B B B) + local first_fdef = calc_fdef(2, #tank_defs) + for i = 1, #tank_defs do + if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then + tank_list[i] = 0 + end + end + elseif tank_mode == 5 then + -- (5) 3 total facility tanks (A A B C) + local first_fdef = calc_fdef(1, math.min(2, #tank_defs)) + for i = 1, #tank_defs do + if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then + tank_list[i] = 0 + end + end + elseif tank_mode == 6 then + -- (6) 3 total facility tanks (A B B C) + local first_fdef = calc_fdef(2, math.min(3, #tank_defs)) + for i = 1, #tank_defs do + if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then + tank_list[i] = 0 + end + end + elseif tank_mode == 7 then + -- (7) 3 total facility tanks (A B C C) + local first_fdef = calc_fdef(3, #tank_defs) + for i = 1, #tank_defs do + if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then + tank_list[i] = 0 + end + end + end + + io.facility.tank_list = tank_list + end + + -- create facility tank tables + for i = 1, #io.facility.tank_list do + if io.facility.tank_list[i] == 2 then + table.insert(io.facility.tank_ps_tbl, psil.create()) + table.insert(io.facility.tank_data_tbl, {}) + end + end + + -- create unit data structures + io.units = {} + for i = 1, conf.num_units do + ---@class pioctl_unit + local entry = { + unit_id = i, + connected = false, + rtu_hw = {}, + + num_boilers = 0, + num_turbines = 0, + num_snas = 0, + has_tank = conf.cooling.r_cool[i].TankConnection, + + control_state = false, + burn_rate_cmd = 0.0, + radiation = types.new_zero_radiation_reading(), + + sna_peak_rate = 0.0, + sna_max_rate = 0.0, + sna_out_rate = 0.0, + + waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM, + waste_product = types.WASTE_PRODUCT.PLUTONIUM, + + -- auto control group + a_group = 0, + + ---@type alarms + alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE }, + + annunciator = {}, ---@type annunciator + + unit_ps = psil.create(), + reactor_data = {}, ---@type reactor_db + + boiler_ps_tbl = {}, + boiler_data_tbl = {}, + + turbine_ps_tbl = {}, + turbine_data_tbl = {}, + + tank_ps_tbl = {}, + tank_data_tbl = {} + } + + -- on other facility modes, overwrite unit TANK option with facility tank defs + if io.facility.tank_mode ~= 0 then + entry.has_tank = conf.cooling.fac_tank_defs[i] > 0 + end + + -- create boiler tables + for _ = 1, conf.cooling.r_cool[i].BoilerCount do + table.insert(entry.boiler_ps_tbl, psil.create()) + table.insert(entry.boiler_data_tbl, {}) + end + + -- create turbine tables + for _ = 1, conf.cooling.r_cool[i].TurbineCount do + table.insert(entry.turbine_ps_tbl, psil.create()) + table.insert(entry.turbine_data_tbl, {}) + end + + -- create tank tables + if io.facility.tank_defs[i] == 1 then + table.insert(entry.tank_ps_tbl, psil.create()) + table.insert(entry.tank_data_tbl, {}) + end + + entry.num_boilers = #entry.boiler_data_tbl + entry.num_turbines = #entry.turbine_data_tbl + + table.insert(io.units, entry) + end end -- set network link state ---@param state POCKET_LINK_STATE -function iocontrol.report_link_state(state) +---@param sv_addr integer? supervisor address if linked +---@param api_addr integer? coordinator address if linked +function iocontrol.report_link_state(state, sv_addr, api_addr) io.ps.publish("link_state", state) if state == LINK_STATE.API_LINK_ONLY or state == LINK_STATE.UNLINKED then @@ -296,6 +519,11 @@ function iocontrol.report_link_state(state) if state == LINK_STATE.SV_LINK_ONLY or state == LINK_STATE.UNLINKED then io.ps.publish("crd_conn_quality", 0) end + + if state == LINK_STATE.LINKED then + io.ps.publish("sv_addr", sv_addr) + io.ps.publish("api_addr", api_addr) + end end -- determine supervisor connection quality (trip time) @@ -357,6 +585,188 @@ function iocontrol.record_facility_data(data) return valid end +-- update unit status data from API_GET_UNIT +---@param data table +function iocontrol.record_unit_data(data) + if type(data[1]) == "number" and io.units[data[1]] then + local unit = io.units[data[1]] ---@type pioctl_unit + + unit.connected = data[2] + unit.rtu_hw = data[3] + unit.alarms = data[4] + + --#region Annunciator + + unit.annunciator = data[5] + + local rcs_disconn, rcs_warn, rcs_hazard = false, false, false + + for key, val in pairs(unit.annunciator) do + if key == "BoilerOnline" or key == "TurbineOnline" then + -- split up online arrays + local every = true + for id = 1, #val do + every = every and val[id] + unit.boiler_ps_tbl[id].publish(key, val[id]) + end + + if not every then rcs_disconn = true end + + unit.unit_ps.publish("U_" .. key, every) + elseif key == "HeatingRateLow" or key == "WaterLevelLow" then + -- split up array for all boilers + local any = false + for id = 1, #val do + any = any or val[id] + unit.boiler_ps_tbl[id].publish(key, val[id]) + end + + if key == "HeatingRateLow" and any then + rcs_warn = true + elseif key == "WaterLevelLow" and any then + rcs_hazard = true + end + + unit.unit_ps.publish("U_" .. key, any) + elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then + -- split up array for all turbines + local any = false + for id = 1, #val do + any = any or val[id] + unit.turbine_ps_tbl[id].publish(key, val[id]) + end + + if key == "GeneratorTrip" and any then + rcs_warn = true + elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then + rcs_hazard = true + end + + unit.unit_ps.publish("U_" .. key, any) + else + -- non-table fields + unit.unit_ps.publish(key, val) + end + end + + local anc = unit.annunciator + rcs_hazard = rcs_hazard or anc.RCPTrip + rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or + anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch or anc.MaxWaterReturnFeed + + local rcs_status = 4 + if rcs_hazard then + rcs_status = 2 + elseif rcs_warn then + rcs_status = 3 + elseif rcs_disconn then + rcs_status = 1 + end + + unit.unit_ps.publish("U_RCS", rcs_status) + + --#endregion + + --#region Reactor Data + + unit.reactor_data = data[6] + + local control_status = 1 + local reactor_status = 1 + local rps_status = 1 + + if unit.connected then + -- update RPS status + if unit.reactor_data.rps_tripped then + control_status = 2 + rps_status = util.trinary(unit.reactor_data.rps_trip_cause == "manual", 3, 2) + else rps_status = 4 end + + -- update reactor/control status + if unit.reactor_data.mek_status.status then + reactor_status = 4 + control_status = util.trinary(unit.annunciator.AutoControl, 4, 3) + else + if unit.reactor_data.no_reactor then + reactor_status = 2 + elseif not unit.reactor_data.formed or unit.reactor_data.rps_status.force_dis then + reactor_status = 3 + else + reactor_status = 4 + end + end + + for key, val in pairs(unit.reactor_data) do + if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then + unit.unit_ps.publish(key, val) + end + end + + if type(unit.reactor_data.rps_status) == "table" then + for key, val in pairs(unit.reactor_data.rps_status) do + unit.unit_ps.publish(key, val) + end + end + + if type(unit.reactor_data.mek_status) == "table" then + for key, val in pairs(unit.reactor_data.mek_status) do + unit.unit_ps.publish(key, val) + end + end + end + + unit.unit_ps.publish("U_ControlStatus", control_status) + unit.unit_ps.publish("U_ReactorStatus", reactor_status) + unit.unit_ps.publish("U_RPS", rps_status) + + --#endregion + + unit.boiler_data_tbl = data[7] + + for id = 1, #unit.boiler_data_tbl do + local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db + local ps = unit.boiler_ps_tbl[id] ---@type psil + + local boiler_status = 1 + + if unit.rtu_hw.boilers[id].connected then + if unit.rtu_hw.boilers[id].faulted then + boiler_status = 3 + elseif boiler.formed then + boiler_status = 4 + else + boiler_status = 2 + end + end + + ps.publish("BoilerStatus", boiler_status) + end + + unit.turbine_data_tbl = data[8] + + for id = 1, #unit.turbine_data_tbl do + local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db + local ps = unit.turbine_ps_tbl[id] ---@type psil + + local turbine_status = 1 + + if unit.rtu_hw.turbines[id].connected then + if unit.rtu_hw.turbines[id].faulted then + turbine_status = 3 + elseif turbine.formed then + turbine_status = 4 + else + turbine_status = 2 + end + end + + ps.publish("TurbineStatus", turbine_status) + end + + unit.tank_data_tbl = data[9] + end +end + -- get the IO controller database function iocontrol.get_db() return io end diff --git a/pocket/pocket.lua b/pocket/pocket.lua index 63149e8..0482386 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -119,6 +119,20 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) self.api.seq_num = self.api.seq_num + 1 end + -- send an API packet to the coordinator + ---@param msg_type CRDN_TYPE + ---@param msg table + local function _send_api(msg_type, msg) + local s_pkt = comms.scada_packet() + local pkt = comms.crdn_packet() + + pkt.make(msg_type, msg) + s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_CRDN, pkt.raw_sendable()) + + nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt) + self.api.seq_num = self.api.seq_num + 1 + end + -- attempt supervisor connection establishment local function _send_sv_establish() _send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) @@ -192,7 +206,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) end else -- linked, all good! - iocontrol.report_link_state(LINK_STATE.LINKED) + iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr) end end @@ -215,6 +229,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end end + -- coordinator get unit data + function public.api__get_unit(unit) + if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end + end + -- parse a packet ---@param side string ---@param sender integer @@ -304,7 +323,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) if _check_length(packet, 11) then iocontrol.record_facility_data(packet.data) end - elseif packet.type == CRDN_TYPE.API_GET_UNITS then + elseif packet.type == CRDN_TYPE.API_GET_UNIT then + if _check_length(packet, 9) then + iocontrol.record_unit_data(packet.data) + end else _fail_type(packet) end else log.debug("discarding coordinator SCADA_CRDN packet before linked") @@ -358,7 +380,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) self.api.addr = src_addr if self.sv.linked then - iocontrol.report_link_state(LINK_STATE.LINKED) + iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr) else iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY) end @@ -497,7 +519,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) self.sv.addr = src_addr if self.api.linked then - iocontrol.report_link_state(LINK_STATE.LINKED) + iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr) else iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY) end diff --git a/pocket/startup.lua b/pocket/startup.lua index 67e2f79..34cc9f7 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.8.0-alpha" +local POCKET_VERSION = "v0.9.0-alpha" local println = util.println local println_ts = util.println_ts diff --git a/pocket/ui/apps/dummy_app.lua b/pocket/ui/apps/dummy_app.lua index fe21db3..d7845a7 100644 --- a/pocket/ui/apps/dummy_app.lua +++ b/pocket/ui/apps/dummy_app.lua @@ -19,6 +19,8 @@ local function create_pages(root) 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} + + TextBox{parent=main,text=" pretend something cool is here \x03",x=1,y=10,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors.black)} end return create_pages diff --git a/pocket/ui/apps/sys_apps.lua b/pocket/ui/apps/sys_apps.lua index a48f85f..aa20a8d 100644 --- a/pocket/ui/apps/sys_apps.lua +++ b/pocket/ui/apps/sys_apps.lua @@ -3,10 +3,12 @@ -- local comms = require("scada-common.comms") -local lockbox = require("lockbox") local util = require("scada-common.util") +local lockbox = require("lockbox") + local iocontrol = require("pocket.iocontrol") +local pocket = require("pocket.pocket") local core = require("graphics.core") @@ -35,8 +37,9 @@ local function create_pages(root) 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 nt_page = about_app.new_page(about_page, 2) + local fw_page = about_app.new_page(about_page, 3) + local hw_page = about_app.new_page(about_page, 4) local about = Div{parent=about_root,x=1,y=2} @@ -46,8 +49,42 @@ local function create_pages(root) 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} + PushButton{parent=about,x=2,y=3,text="Network >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=nt_page.nav_to} + PushButton{parent=about,x=2,y=4,text="Firmware >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fw_page.nav_to} + PushButton{parent=about,x=2,y=5,text="Host Details >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=hw_page.nav_to} + + --#region Network Details + + local config = pocket.config + + local nt_div = Div{parent=about_root,x=1,y=2} + TextBox{parent=nt_div,y=1,text="Network Details",height=1,alignment=ALIGN.CENTER} + + PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to} + + TextBox{parent=nt_div,x=2,y=3,text="Pocket Address",height=1,alignment=ALIGN.LEFT,fg_bg=label} +---@diagnostic disable-next-line: undefined-field + TextBox{parent=nt_div,x=2,text=util.c(os.getComputerID(),":",config.PKT_Channel),height=1,alignment=ALIGN.LEFT} + + nt_div.line_break() + TextBox{parent=nt_div,x=2,text="Supervisor Address",height=1,alignment=ALIGN.LEFT,fg_bg=label} + local sv = TextBox{parent=nt_div,x=2,text="",height=1,alignment=ALIGN.LEFT} + + nt_div.line_break() + TextBox{parent=nt_div,x=2,text="Coordinator Address",height=1,alignment=ALIGN.LEFT,fg_bg=label} + local coord = TextBox{parent=nt_div,x=2,text="",height=1,alignment=ALIGN.LEFT} + + sv.register(db.ps, "sv_addr", function (addr) sv.set_value(util.c(addr, ":", config.SVR_Channel)) end) + coord.register(db.ps, "api_addr", function (addr) coord.set_value(util.c(addr, ":", config.CRD_Channel)) end) + + nt_div.line_break() + TextBox{parent=nt_div,x=2,text="Message Authentication",height=1,alignment=ALIGN.LEFT,fg_bg=label} + local auth = util.trinary(type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0, "HMAC-MD5", "None") + TextBox{parent=nt_div,x=2,text=auth,height=1,alignment=ALIGN.LEFT} + + --#endregion + + --#region Firmware Versions 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} @@ -81,6 +118,10 @@ local function create_pages(root) 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} + --#endregion + + --#region Host Versions + 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} @@ -94,7 +135,9 @@ local function create_pages(root) 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}} + --#endregion + + local root_pane = MultiPane{parent=about_root,x=1,y=1,panes={about,nt_div,fw_div,hw_div}} about_app.set_root_pane(root_pane) end diff --git a/pocket/ui/main.lua b/pocket/ui/main.lua index 02e33fd..87960f2 100644 --- a/pocket/ui/main.lua +++ b/pocket/ui/main.lua @@ -38,7 +38,7 @@ local function init(main) local db = iocontrol.get_db() -- window header message - TextBox{parent=main,y=1,text="DEV ALPHA APP S C ",alignment=ALIGN.LEFT,height=1,fg_bg=style.header} + TextBox{parent=main,y=1,text="WIP 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)} @@ -71,10 +71,6 @@ local function init(main) local page_div = Div{parent=main_pane,x=4,y=1} - local sidebar_tabs = { - { char = "#", color = cpair(colors.black, colors.green) } - } - home_page(page_div) unit_page(page_div) @@ -84,13 +80,13 @@ local function init(main) assert(#db.nav.get_containers() == iocontrol.APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered") - 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=db.nav.open_app} + db.nav.set_pane(MultiPane{parent=page_div,x=1,y=1,panes=db.nav.get_containers()}) + db.nav.set_sidebar(Sidebar{parent=main_pane,x=1,y=1,height=18,fg_bg=cpair(colors.white,colors.gray)}) 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} + db.nav.open_app(iocontrol.APP_ID.ROOT) + --#endregion end diff --git a/pocket/ui/pages/home_page.lua b/pocket/ui/pages/home_page.lua index 0ea7c3f..483a881 100644 --- a/pocket/ui/pages/home_page.lua +++ b/pocket/ui/pages/home_page.lua @@ -39,21 +39,26 @@ local function new_view(root) local function open(id) db.nav.open_app(id) end + app.set_sidebar({ + { label = " #\x10", tall = true, color = core.cpair(colors.black, colors.green), callback = function () open(APP_ID.ROOT) 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} + App{parent=apps_1,x=2,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=9,y=2,text="F",title="Facil",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg} + App{parent=apps_1,x=16,y=2,text="\x15",title="Control",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=2,y=7,text="\x17",title="Process",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=9,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=16,y=7,text="\x08",title="Devices",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=2,y=12,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=9,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} + App{parent=apps_2,x=2,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=9,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=16,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/unit_page.lua b/pocket/ui/pages/unit_page.lua index 273ad27..da123e0 100644 --- a/pocket/ui/pages/unit_page.lua +++ b/pocket/ui/pages/unit_page.lua @@ -2,14 +2,56 @@ -- Unit Overview Page -- +local util = require("scada-common.util") +-- local log = require("scada-common.log") + 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 DataIndicator = require("graphics.elements.indicators.data") +local IconIndicator = require("graphics.elements.indicators.icon") +-- local RadIndicator = require("graphics.elements.indicators.rad") +-- local VerticalBar = require("graphics.elements.indicators.vbar") + +local PushButton = require("graphics.elements.controls.push_button") + local ALIGN = core.ALIGN +local cpair = core.cpair + +local basic_states = { + { color = cpair(colors.black, colors.lightGray), symbol = "\x07" }, + { color = cpair(colors.black, colors.red), symbol = "-" }, + { color = cpair(colors.black, colors.yellow), symbol = "\x1e" }, + { color = cpair(colors.black, colors.green), symbol = "+" } +} + +local mode_states = { + { color = cpair(colors.black, colors.lightGray), symbol = "\x07" }, + { color = cpair(colors.black, colors.red), symbol = "-" }, + { color = cpair(colors.black, colors.green), symbol = "+" }, + { color = cpair(colors.black, colors.purple), symbol = "A" } +} + +local emc_ind_s = { + { color = cpair(colors.black, colors.gray), symbol = "-" }, + { color = cpair(colors.black, colors.white), symbol = "\x07" }, + { color = cpair(colors.black, colors.green), symbol = "+" } +} + +local red_ind_s = { + { color = cpair(colors.black, colors.lightGray), symbol = "+" }, + { color = cpair(colors.black, colors.red), symbol = "-" } +} + +local yel_ind_s = { + { color = cpair(colors.black, colors.lightGray), symbol = "+" }, + { color = cpair(colors.black, colors.yellow), symbol = "-" } +} -- new unit page view ---@param root graphics_element parent @@ -19,11 +61,259 @@ local function new_view(root) local main = Div{parent=root,x=1,y=1} 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=2,text="Units App",height=1,alignment=ALIGN.CENTER} - TextBox{parent=main,y=4,text="work in progress",height=1,alignment=ALIGN.CENTER} + TextBox{parent=main,y=4,text="Loading...",height=1,alignment=ALIGN.CENTER} + + local btn_fg_bg = cpair(colors.yellow, colors.black) + local btn_active = cpair(colors.white, colors.black) + -- local label = cpair(colors.lightGray, colors.black) + + local nav_links = {} + + local function set_sidebar(id) + -- local unit = db.units[id] ---@type pioctl_unit + + local list = { + { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(iocontrol.APP_ID.ROOT) end }, + { label = "U-" .. id, color = core.cpair(colors.black, colors.yellow), callback = function () app.switcher(id) end }, + { label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = nav_links[id].alarm }, + { label = "RPS", tall = true, color = core.cpair(colors.black, colors.cyan), callback = nav_links[id].rps }, + -- { label = " R ", color = core.cpair(colors.black, colors.lightGray), callback = function () end }, + { label = "RCS", tall = true, color = core.cpair(colors.black, colors.blue), callback = nav_links[id].rcs }, + } + + -- for i = 1, unit.num_boilers do + -- table.insert(list, { label = "B-" .. i, color = core.cpair(colors.black, colors.lightBlue), callback = function () end }) + -- end + + -- for i = 1, unit.num_turbines do + -- table.insert(list, { label = "T-" .. i, color = core.cpair(colors.black, colors.white), callback = function () end }) + -- end + + app.set_sidebar(list) + end + + local function load() + local page_div = Div{parent=main,x=2,y=2,width=main.get_width()-2} + + local panes = {} + + local active_unit = 1 + + -- create all page divs + for _ = 1, db.facility.num_units do + local div = Div{parent=page_div} + table.insert(panes, div) + table.insert(nav_links, {}) + end + + -- previous unit + local function prev(x) + active_unit = util.trinary(x == 1, db.facility.num_units, x - 1) + app.switcher(active_unit) + set_sidebar(active_unit) + end + + -- next unit + local function next(x) + active_unit = util.trinary(x == db.facility.num_units, 1, x + 1) + app.switcher(active_unit) + set_sidebar(active_unit) + end + + for i = 1, db.facility.num_units do + local u_div = panes[i] ---@type graphics_element + local unit = db.units[i] ---@type pioctl_unit + local u_ps = unit.unit_ps + + -- refresh data callback, every 500ms it will re-send the query + local last_update = 0 + local function update() + if util.time_ms() - last_update >= 500 then + db.api.get_unit(i) + last_update = util.time_ms() + end + end + + --#region Main Unit Overview + + local u_page = app.new_page(nil, i) + u_page.tasks = { update } + + TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,height=1,alignment=ALIGN.CENTER} + PushButton{parent=u_div,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end} + PushButton{parent=u_div,x=21,y=1,text=">",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()next(i)end} + + local type = util.trinary(unit.num_boilers > 0, "Sodium Cooled Reactor", "Boiling Water Reactor") + TextBox{parent=u_div,y=3,text=type,height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)} + + local lu_col = cpair(colors.lightGray, colors.lightGray) + local text_fg = cpair(colors.white, colors._INHERIT) + + local rate = DataIndicator{parent=u_div,y=5,lu_colors=lu_col,label="Rate",unit="mB/t",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg} + local temp = DataIndicator{parent=u_div,lu_colors=lu_col,label="Temp",unit="K",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg} + + local ctrl = IconIndicator{parent=u_div,x=1,y=8,label="Control State",states=mode_states} + + rate.register(u_ps, "act_burn_rate", rate.update) + temp.register(u_ps, "temp", temp.update) + ctrl.register(u_ps, "U_ControlStatus", ctrl.update) + + u_div.line_break() + + local rct = IconIndicator{parent=u_div,x=1,label="Fission Reactor",states=basic_states} + local rps = IconIndicator{parent=u_div,x=1,label="Protection System",states=basic_states} + + rct.register(u_ps, "U_ReactorStatus", rct.update) + rps.register(u_ps, "U_RPS", rps.update) + + u_div.line_break() + + local rcs = IconIndicator{parent=u_div,x=1,label="Coolant System",states=basic_states} + rcs.register(u_ps, "U_RCS", rcs.update) + + for b = 1, unit.num_boilers do + local blr = IconIndicator{parent=u_div,x=1,label="Boiler "..b,states=basic_states} + blr.register(unit.boiler_ps_tbl[b], "BoilerStatus", blr.update) + end + + for t = 1, unit.num_turbines do + local tbn = IconIndicator{parent=u_div,x=1,label="Turbine "..t,states=basic_states} + tbn.register(unit.turbine_ps_tbl[t], "TurbineStatus", tbn.update) + end + + --#endregion + + --#region Alarms Tab + + local alm_div = Div{parent=page_div} + table.insert(panes, alm_div) + + local alm_page = app.new_page(u_page, #panes) + alm_page.tasks = { update } + + nav_links[i].alarm = alm_page.nav_to + + TextBox{parent=alm_div,y=1,text="Unit Alarms",height=1,alignment=ALIGN.CENTER} + + TextBox{parent=alm_div,y=3,text="work in progress",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)} + + --#endregion + + --#region RPS Tab + + local rps_div = Div{parent=page_div} + table.insert(panes, rps_div) + + local rps_page = app.new_page(u_page, #panes) + rps_page.tasks = { update } + + nav_links[i].rps = rps_page.nav_to + + TextBox{parent=rps_div,y=1,text="Protection System",height=1,alignment=ALIGN.CENTER} + + local r_trip = IconIndicator{parent=rps_div,y=3,label="RPS Trip",states=basic_states} + r_trip.register(u_ps, "U_RPS", r_trip.update) + + local r_mscrm = IconIndicator{parent=rps_div,y=5,label="Manual SCRAM",states=red_ind_s} + local r_ascrm = IconIndicator{parent=rps_div,label="Automatic SCRAM",states=red_ind_s} + local rps_tmo = IconIndicator{parent=rps_div,label="Timeout",states=yel_ind_s} + local rps_flt = IconIndicator{parent=rps_div,label="PPM Fault",states=yel_ind_s} + local rps_sfl = IconIndicator{parent=rps_div,label="Not Formed",states=red_ind_s} + + r_mscrm.register(u_ps, "manual", r_mscrm.update) + r_ascrm.register(u_ps, "automatic", r_ascrm.update) + rps_tmo.register(u_ps, "timeout", rps_tmo.update) + rps_flt.register(u_ps, "fault", rps_flt.update) + rps_sfl.register(u_ps, "sys_fail", rps_sfl.update) + + rps_div.line_break() + local rps_dmg = IconIndicator{parent=rps_div,label="Reactor Damage Hi",states=red_ind_s} + local rps_tmp = IconIndicator{parent=rps_div,label="Temp. Critical",states=red_ind_s} + local rps_nof = IconIndicator{parent=rps_div,label="Fuel Level Lo",states=yel_ind_s} + local rps_exw = IconIndicator{parent=rps_div,label="Waste Level Hi",states=yel_ind_s} + local rps_loc = IconIndicator{parent=rps_div,label="Coolant Lo Lo",states=yel_ind_s} + local rps_exh = IconIndicator{parent=rps_div,label="Heated Coolant Hi",states=yel_ind_s} + + rps_dmg.register(u_ps, "high_dmg", rps_dmg.update) + rps_tmp.register(u_ps, "high_temp", rps_tmp.update) + rps_nof.register(u_ps, "no_fuel", rps_nof.update) + rps_exw.register(u_ps, "ex_waste", rps_exw.update) + rps_loc.register(u_ps, "low_cool", rps_loc.update) + rps_exh.register(u_ps, "ex_hcool", rps_exh.update) + + --#endregion + + --#region RCS Tab + + local rcs_div = Div{parent=page_div} + table.insert(panes, rcs_div) + + local rcs_page = app.new_page(u_page, #panes) + rcs_page.tasks = { update } + + nav_links[i].rcs = rcs_page.nav_to + + TextBox{parent=rcs_div,y=1,text="Coolant System",height=1,alignment=ALIGN.CENTER} + + local r_rtrip = IconIndicator{parent=rcs_div,y=3,label="RCP Trip",states=red_ind_s} + local r_cflow = IconIndicator{parent=rcs_div,label="RCS Flow Lo",states=yel_ind_s} + local r_clow = IconIndicator{parent=rcs_div,label="Coolant Level Lo",states=yel_ind_s} + + r_rtrip.register(u_ps, "RCPTrip", r_rtrip.update) + r_cflow.register(u_ps, "RCSFlowLow", r_cflow.update) + r_clow.register(u_ps, "CoolantLevelLow", r_clow.update) + + local c_flt = IconIndicator{parent=rcs_div,label="RCS HW Fault",states=yel_ind_s} + local c_emg = IconIndicator{parent=rcs_div,label="Emergency Coolant",states=emc_ind_s} + local c_mwrf = IconIndicator{parent=rcs_div,label="Max Water Return",states=yel_ind_s} + + c_flt.register(u_ps, "RCSFault", c_flt.update) + c_emg.register(u_ps, "EmergencyCoolant", c_emg.update) + c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update) + + -- rcs_div.line_break() + -- TextBox{parent=rcs_div,text="Mismatches",height=1,alignment=ALIGN.CENTER,fg_bg=label} + local c_cfm = IconIndicator{parent=rcs_div,label="Coolant Feed",states=yel_ind_s} + local c_brm = IconIndicator{parent=rcs_div,label="Boil Rate",states=yel_ind_s} + local c_sfm = IconIndicator{parent=rcs_div,label="Steam Feed",states=yel_ind_s} + + c_cfm.register(u_ps, "CoolantFeedMismatch", c_cfm.update) + c_brm.register(u_ps, "BoilRateMismatch", c_brm.update) + c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update) + + rcs_div.line_break() + -- TextBox{parent=rcs_div,text="Aggregate Checks",height=1,alignment=ALIGN.CENTER,fg_bg=label} + + if unit.num_boilers > 0 then + local wll = IconIndicator{parent=rcs_div,label="Boiler Water Lo",states=red_ind_s} + local hrl = IconIndicator{parent=rcs_div,label="Heating Rate Lo",states=yel_ind_s} + + wll.register(u_ps, "U_WaterLevelLow", wll.update) + hrl.register(u_ps, "U_HeatingRateLow", hrl.update) + end + + local tospd = IconIndicator{parent=rcs_div,label="TRB Over Speed",states=red_ind_s} + local gtrip = IconIndicator{parent=rcs_div,label="Generator Trip",states=yel_ind_s} + local ttrip = IconIndicator{parent=rcs_div,label="Turbine Trip",states=red_ind_s} + + tospd.register(u_ps, "U_TurbineOverSpeed", tospd.update) + gtrip.register(u_ps, "U_GeneratorTrip", gtrip.update) + ttrip.register(u_ps, "U_TurbineTrip", ttrip.update) + + --#endregion + end + + -- setup multipane + local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} + app.set_root_pane(u_pane) + + set_sidebar(active_unit) + end + + app.set_on_load(load) return main end diff --git a/rtu/configure.lua b/rtu/configure.lua index efebdee..9012705 100644 --- a/rtu/configure.lua +++ b/rtu/configure.lua @@ -1669,9 +1669,11 @@ function configurator.configure(ask_config) elseif event == "paste" then display.handle_paste(param1) elseif event == "peripheral_detach" then +---@diagnostic disable-next-line: discard-returns ppm.handle_unmount(param1) tool_ctl.update_peri_list() elseif event == "peripheral" then +---@diagnostic disable-next-line: discard-returns ppm.mount(param1) tool_ctl.update_peri_list() end diff --git a/rtu/startup.lua b/rtu/startup.lua index 027549c..ae2e72b 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "v1.9.5" +local RTU_VERSION = "v1.9.6" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE diff --git a/scada-common/comms.lua b/scada-common/comms.lua index e107d33..5e364e8 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -18,7 +18,7 @@ local comms = {} -- protocol/data versions (protocol/data independent changes tracked by util.lua version) comms.version = "2.5.1" -comms.api_version = "0.0.1" +comms.api_version = "0.0.2" ---@enum PROTOCOL local PROTOCOL = { @@ -67,7 +67,7 @@ local CRDN_TYPE = { UNIT_STATUSES = 5, -- state of each of the reactor units 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 + API_GET_UNIT = 8 -- API: get reactor unit data } ---@enum ESTABLISH_ACK