diff --git a/coordinator/configure.lua b/coordinator/configure.lua index e763a271..f57f11e0 100644 --- a/coordinator/configure.lua +++ b/coordinator/configure.lua @@ -7,6 +7,7 @@ local log = require("scada-common.log") local network = require("scada-common.network") local ppm = require("scada-common.ppm") local tcd = require("scada-common.tcd") +local types = require("scada-common.types") local util = require("scada-common.util") local themes = require("graphics.themes") @@ -756,7 +757,7 @@ local function config_view(display) local clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} TextBox{parent=crd_c_1,x=1,y=8,height=1,text="Temperature Scale"} - local temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options={"Kelvin","Celsius","Fahrenheit","Rankine"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} + local temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} local function submit_ui_opts() tmp_cfg.Time24Hour = clock_fmt.get_value() == 1 @@ -1356,7 +1357,7 @@ local function config_view(display) if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") elseif f[1] == "TempScale" then - if raw == 1 then val = "Kelvin" elseif raw == 2 then val = "Celsius" elseif raw == 3 then val = "Fahrenheit" elseif raw == 4 then val = "Rankine" end + val = types.TEMP_SCALE_NAMES[raw] elseif f[1] == "MainTheme" then val = util.strval(themes.ui_theme_name(raw)) elseif f[1] == "FrontPanelTheme" then diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index bf73e1fa..5d8428fe 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -14,6 +14,8 @@ local pgi = require("coordinator.ui.pgi") local ALARM_STATE = types.ALARM_STATE local PROCESS = types.PROCESS +local TEMP_SCALE = types.TEMP_SCALE +local TEMP_UNITS = types.TEMP_SCALE_UNITS -- nominal RTT is ping (0ms to 10ms usually) + 500ms for CRD main loop tick local WARN_RTT = 1000 -- 2x as long as expected w/ 0 ping @@ -47,17 +49,16 @@ end -- initialize the coordinator IO controller ---@param conf facility_conf configuration ---@param comms coord_comms comms reference ----@param temp_scale integer temperature unit (1 = K, 2 = C, 3 = F, 4 = R) +---@param temp_scale TEMP_SCALE temperature unit function iocontrol.init(conf, comms, temp_scale) + io.temp_label = TEMP_UNITS[temp_scale] + -- temperature unit label and conversion function (from Kelvin) - if temp_scale == 2 then - io.temp_label = "\xb0C" + if temp_scale == TEMP_SCALE.CELSIUS then io.temp_convert = function (t) return t - 273.15 end - elseif temp_scale == 3 then - io.temp_label = "\xb0F" + elseif temp_scale == TEMP_SCALE.FAHRENHEIT then io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end - elseif temp_scale == 4 then - io.temp_label = "\xb0R" + elseif temp_scale == TEMP_SCALE.RANKINE then io.temp_convert = function (t) return 1.8 * t end else io.temp_label = "K" @@ -247,6 +248,9 @@ function iocontrol.init(conf, comms, temp_scale) waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM, waste_product = types.WASTE_PRODUCT.PLUTONIUM, + last_rate_change_ms = 0, + turbine_flow_stable = false, + -- auto control group a_group = 0, @@ -1214,9 +1218,11 @@ function iocontrol.update_unit_statuses(statuses) local unit_state = status[5] if type(unit_state) == "table" then - if #unit_state == 6 then + if #unit_state == 8 then unit.waste_mode = unit_state[5] unit.waste_product = unit_state[6] + unit.last_rate_change_ms = unit_state[7] + unit.turbine_flow_stable = unit_state[8] unit.unit_ps.publish("U_StatusLine1", unit_state[1]) unit.unit_ps.publish("U_StatusLine2", unit_state[2]) diff --git a/coordinator/session/pocket.lua b/coordinator/session/pocket.lua index 09ec15cf..cd03fc11 100644 --- a/coordinator/session/pocket.lua +++ b/coordinator/session/pocket.lua @@ -152,7 +152,9 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) u.reactor_data, u.boiler_data_tbl, u.turbine_data_tbl, - u.tank_data_tbl + u.tank_data_tbl, + u.last_rate_change_ms, + u.turbine_flow_stable } _send(CRDN_TYPE.API_GET_UNIT, data) diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 0b8a06ca..f152bc86 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.6" +local COORDINATOR_VERSION = "v1.4.7" local CHUNK_LOAD_DELAY_S = 30.0 diff --git a/coordinator/threads.lua b/coordinator/threads.lua index e7c2d8cf..20ba7aea 100644 --- a/coordinator/threads.lua +++ b/coordinator/threads.lua @@ -244,7 +244,7 @@ function threads.thread__main(smem) return public end --- coordinator renderer thread, tasked with long duration re-draws +-- coordinator renderer thread, tasked with long duration draws ---@nodiscard ---@param smem crd_shared_memory function threads.thread__render(smem) diff --git a/graphics/core.lua b/graphics/core.lua index 830ca690..f6a647a7 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -7,7 +7,7 @@ local flasher = require("graphics.flasher") local core = {} -core.version = "2.2.4" +core.version = "2.3.0" core.flasher = flasher core.events = events diff --git a/graphics/element.lua b/graphics/element.lua index 731a22cb..7475dc10 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -82,9 +82,10 @@ end -- a base graphics element, should not be created on its own ---@nodiscard ---@param args graphics_args arguments +---@param constraint? function apply a dimensional constraint based on proposed dimensions function(frame) -> width, height ---@param child_offset_x? integer mouse event offset x ---@param child_offset_y? integer mouse event offset y -function element.new(args, child_offset_x, child_offset_y) +function element.new(args, constraint, child_offset_x, child_offset_y) local self = { id = nil, ---@type element_id|nil is_root = args.parent == nil, @@ -199,7 +200,7 @@ function element.new(args, child_offset_x, child_offset_y) ---@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) + next_y = util.trinary(args.height == nil and constraint == nil, 1, next_y) -- record offsets in case there is a reposition self.offset_x = offset_x @@ -226,6 +227,13 @@ function element.new(args, child_offset_x, child_offset_y) local w, h = self.p_window.getSize() f.w = math.min(f.w, w - (f.x - 1)) f.h = math.min(f.h, h - (f.y - 1)) + + if type(constraint) == "function" then + -- constrain per provided constraint function (can only get smaller than available space) + w, h = constraint(f) + f.w = math.min(f.w, w) + f.h = math.min(f.h, h) + end end -- check frame diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua index 88c8a5db..f0609016 100644 --- a/graphics/elements/controls/push_button.lua +++ b/graphics/elements/controls/push_button.lua @@ -1,6 +1,7 @@ -- Button Graphics Element local tcd = require("scada-common.tcd") +local util = require("scada-common.util") local core = require("graphics.core") local element = require("graphics.element") @@ -21,7 +22,6 @@ local KEY_CLICK = core.events.KEY_CLICK ---@field id? string element id ---@field x? integer 1 if omitted ---@field y? integer auto incremented if omitted ----@field height? integer parent height if omitted ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw @@ -38,29 +38,40 @@ local function push_button(args) -- set automatic settings args.can_focus = true - args.height = 1 args.min_width = args.min_width or 0 args.width = math.max(text_width, args.min_width) + -- provide a constraint condition to element creation to prefer a single line button + ---@param frame graphics_frame + local function constrain(frame) + return frame.w, math.max(1, #util.strwrap(args.text, frame.w)) + end + -- create new graphics element base object - local e = element.new(args) + local e = element.new(args, constrain) - local h_pad = 1 - local v_pad = math.floor(e.frame.h / 2) + 1 - - if alignment == ALIGN.CENTER then - h_pad = math.floor((e.frame.w - text_width) / 2) + 1 - elseif alignment == ALIGN.RIGHT then - h_pad = (e.frame.w - text_width) + 1 - end + local text_lines = util.strwrap(args.text, e.frame.w) -- draw the button function e.redraw() e.window.clear() - -- write the button text - e.w_set_cur(h_pad, v_pad) - e.w_write(args.text) + for i = 1, #text_lines do + if i > e.frame.h then break end + + local len = string.len(text_lines[i]) + + -- use cursor position to align this line + if alignment == ALIGN.CENTER then + e.w_set_cur(math.floor((e.frame.w - len) / 2) + 1, i) + elseif alignment == ALIGN.RIGHT then + e.w_set_cur((e.frame.w - len) + 1, i) + else + e.w_set_cur(1, i) + end + + e.w_write(text_lines[i]) + end end -- draw the button as pressed (if active_fg_bg set) @@ -109,7 +120,9 @@ local function push_button(args) if event.type == KEY_CLICK.DOWN then if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then args.callback() - e.defocus() + -- visualize click without unfocusing + show_unpressed() + if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_pressed) end end end end diff --git a/graphics/elements/rectangle.lua b/graphics/elements/rectangle.lua index 252790ba..eceb9bd7 100644 --- a/graphics/elements/rectangle.lua +++ b/graphics/elements/rectangle.lua @@ -45,7 +45,7 @@ local function rectangle(args) end -- create new graphics element base object - local e = element.new(args, offset_x, offset_y) + local e = element.new(args, nil, offset_x, offset_y) -- create content window for child elements e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y)) diff --git a/graphics/elements/textbox.lua b/graphics/elements/textbox.lua index 07a87367..2a61860f 100644 --- a/graphics/elements/textbox.lua +++ b/graphics/elements/textbox.lua @@ -10,12 +10,13 @@ local ALIGN = core.ALIGN ---@class textbox_args ---@field text string text to show ---@field alignment? ALIGN text alignment, left by default +---@field anchor? boolean true to use this as an anchor, making it focusable ---@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 height? integer minimum necessary height for wrapped text 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 @@ -26,8 +27,22 @@ local ALIGN = core.ALIGN local function textbox(args) element.assert(type(args.text) == "string", "text is a required field") + if args.anchor == true then args.can_focus = true end + + -- provide a constraint condition to element creation to prevent an pointlessly tall text box + ---@param frame graphics_frame + local function constrain(frame) + local new_height = math.max(1, #util.strwrap(args.text, frame.w)) + + if args.height then + new_height = math.max(frame.h, new_height) + end + + return frame.w, new_height + end + -- create new graphics element base object - local e = element.new(args) + local e = element.new(args, constrain) e.value = args.text diff --git a/pocket/configure.lua b/pocket/configure.lua index 80f91bcc..d16309c3 100644 --- a/pocket/configure.lua +++ b/pocket/configure.lua @@ -3,7 +3,7 @@ -- local log = require("scada-common.log") -local tcd = require("scada-common.tcd") +local types = require("scada-common.types") local util = require("scada-common.util") local core = require("graphics.core") @@ -32,7 +32,9 @@ local CENTER = core.ALIGN.CENTER local RIGHT = core.ALIGN.RIGHT -- changes to the config data/format to let the user know -local changes = {} +local changes = { + { "v0.9.2", { "Added temperature scale options" } } +} ---@class pkt_configurator local configurator = {} @@ -73,6 +75,7 @@ local tool_ctl = { ---@class pkt_config local tmp_cfg = { + TempScale = 1, SVR_Channel = nil, ---@type integer CRD_Channel = nil, ---@type integer PKT_Channel = nil, ---@type integer @@ -91,6 +94,7 @@ local settings_cfg = {} -- all settings fields, their nice names, and their default values local fields = { + { "TempScale", "Temperature Scale", 1 }, { "SVR_Channel", "SVR Channel", 16240 }, { "CRD_Channel", "CRD Channel", 16243 }, { "PKT_Channel", "PKT Channel", 16244 }, @@ -126,12 +130,13 @@ local function config_view(display) local root_pane_div = Div{parent=display,x=1,y=2} local main_page = Div{parent=root_pane_div,x=1,y=1} + local ui_cfg = Div{parent=root_pane_div,x=1,y=1} local net_cfg = Div{parent=root_pane_div,x=1,y=1} local log_cfg = Div{parent=root_pane_div,x=1,y=1} local summary = Div{parent=root_pane_div,x=1,y=1} local changelog = Div{parent=root_pane_div,x=1,y=1} - local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,log_cfg,summary,changelog}} + local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,ui_cfg,net_cfg,log_cfg,summary,changelog}} -- Main Page @@ -148,7 +153,7 @@ local function config_view(display) tool_ctl.viewing_config = true tool_ctl.gen_summary(settings_cfg) tool_ctl.settings_apply.hide(true) - main_pane.set_value(4) + main_pane.set_value(5) end if fs.exists("/pocket/config.lua") then @@ -162,7 +167,28 @@ local function config_view(display) if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end PushButton{parent=main_page,x=2,y=18,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} - PushButton{parent=main_page,x=14,y=18,min_width=12,text="Change Log",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=main_page,x=14,y=18,min_width=12,text="Change Log",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#region Pocket UI + + local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24} + + TextBox{parent=ui_cfg,x=1,y=2,height=1,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)} + + TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may use the options below to customize formats."} + + TextBox{parent=ui_c_1,x=1,y=5,height=1,text="Temperature Scale"} + local temp_scale = RadioButton{parent=ui_c_1,x=1,y=6,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} + + local function submit_ui_opts() + tmp_cfg.TempScale = temp_scale.get_value() + main_pane.set_value(3) + end + + PushButton{parent=ui_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion --#region Network @@ -201,7 +227,7 @@ local function config_view(display) else chan_err.show() end end - PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_1,x=19,y=15,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} TextBox{parent=net_c_2,x=1,y=1,height=1,text="Set connection timeout."} @@ -268,7 +294,7 @@ local function config_view(display) local v = key.get_value() if string.len(v) == 0 or string.len(v) >= 8 then tmp_cfg.AuthKey = key.get_value() - main_pane.set_value(3) + main_pane.set_value(4) key_err.hide(true) else key_err.show() end end @@ -306,11 +332,11 @@ local function config_view(display) tool_ctl.viewing_config = false tool_ctl.importing_legacy = false tool_ctl.settings_apply.show() - main_pane.set_value(4) + main_pane.set_value(5) else path_err.show() end end - PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=log_c_1,x=19,y=15,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} --#endregion @@ -335,7 +361,7 @@ local function config_view(display) tool_ctl.importing_legacy = false tool_ctl.settings_apply.show() else - main_pane.set_value(3) + main_pane.set_value(4) end end @@ -444,7 +470,7 @@ local function config_view(display) tool_ctl.gen_summary(tmp_cfg) sum_pane.set_value(1) - main_pane.set_value(4) + main_pane.set_value(5) tool_ctl.importing_legacy = true end @@ -473,8 +499,13 @@ local function config_view(display) local raw = cfg[f[1]] local val = util.strval(raw) - if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) - elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end + if f[1] == "AuthKey" then + val = string.rep("*", string.len(val)) + elseif f[1] == "LogMode" then + val = util.trinary(raw == log.MODE.APPEND, "append", "replace") + elseif f[1] == "TempScale" then + val = types.TEMP_SCALE_NAMES[raw] + end if val == "nil" then val = "" end @@ -532,9 +563,7 @@ function configurator.configure(ask_config) local event, param1, param2, param3 = util.pull_event() -- handle event - if event == "timer" then - tcd.handle(param1) - elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then + if event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then local m_e = core.events.new_mouse_event(event, param1, param2, param3) if m_e then display.handle_mouse(m_e) end elseif event == "char" or event == "key" or event == "key_up" then diff --git a/pocket/iocontrol.lua b/pocket/iocontrol.lua index ba283a08..da9c0d7b 100644 --- a/pocket/iocontrol.lua +++ b/pocket/iocontrol.lua @@ -2,13 +2,16 @@ -- I/O Control for Pocket Integration with Supervisor & Coordinator -- -local log = require("scada-common.log") +local const = require("scada-common.constants") +-- 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 +local TEMP_SCALE = types.TEMP_SCALE +local TEMP_UNITS = types.TEMP_SCALE_UNITS ---@todo nominal trip time is ping (0ms to 10ms usually) local WARN_TT = 40 @@ -26,200 +29,17 @@ local LINK_STATE = { iocontrol.LINK_STATE = LINK_STATE ----@enum POCKET_APP_ID -local APP_ID = { - ROOT = 1, - -- main app page - UNITS = 2, - ABOUT = 3, - -- diag app page - ALARMS = 4, - -- other - DUMMY = 5, - NUM_APPS = 5 -} - -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 - - 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) - ---@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 = { - loaded = false, - load = nil, - cur_page = nil, ---@type nav_tree_page - pane = pane, - 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) - 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 and app.cur_page == nil then - app.cur_page = page - 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) - 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 - 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() +---@param nav pocket_nav +function iocontrol.init_core(comms, nav) + io.nav = nav ---@class pocket_ioctl_diag io.diag = {} @@ -267,17 +87,16 @@ end -- initialize facility-dependent components of pocket iocontrol ---@param conf facility_conf configuration ----@param temp_scale 1|2|3|4 temperature unit (1 = K, 2 = C, 3 = F, 4 = R) +---@param temp_scale TEMP_SCALE temperature unit function iocontrol.init_fac(conf, temp_scale) + io.temp_label = TEMP_UNITS[temp_scale] + -- temperature unit label and conversion function (from Kelvin) - if temp_scale == 2 then - io.temp_label = "\xb0C" + if temp_scale == TEMP_SCALE.CELSIUS then io.temp_convert = function (t) return t - 273.15 end - elseif temp_scale == 3 then - io.temp_label = "\xb0F" + elseif temp_scale == TEMP_SCALE.FAHRENHEIT then io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end - elseif temp_scale == 4 then - io.temp_label = "\xb0R" + elseif temp_scale == TEMP_SCALE.RANKINE then io.temp_convert = function (t) return 1.8 * t end else io.temp_label = "K" @@ -454,6 +273,9 @@ function iocontrol.init_fac(conf, temp_scale) waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM, waste_product = types.WASTE_PRODUCT.PLUTONIUM, + last_rate_change_ms = 0, + turbine_flow_stable = false, + -- auto control group a_group = 0, @@ -585,192 +407,453 @@ function iocontrol.record_facility_data(data) return valid end +local function tripped(state) return state == ALARM_STATE.TRIPPED or state == ALARM_STATE.ACKED end + +local function _record_multiblock_status(faulted, data, ps) + ps.publish("formed", data.formed) + ps.publish("faulted", faulted) + + for key, val in pairs(data.state) do ps.publish(key, val) end + for key, val in pairs(data.tanks) do ps.publish(key, val) end +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 + local unit = io.units[data[1]] ---@type pioctl_unit - unit.annunciator = data[5] + unit.connected = data[2] + unit.rtu_hw = data[3] + unit.alarms = data[4] - local rcs_disconn, rcs_warn, rcs_hazard = false, false, false + --#region Annunciator - for key, val in pairs(unit.annunciator) do - if key == "BoilerOnline" or key == "TurbineOnline" then - local every = true + unit.annunciator = data[5] - -- split up online arrays - for id = 1, #val do - every = every and val[id] + local rcs_disconn, rcs_warn, rcs_hazard = false, false, false - if key == "BoilerOnline" then - unit.boiler_ps_tbl[id].publish(key, val[id]) - else - unit.turbine_ps_tbl[id].publish(key, val[id]) - end - end + for key, val in pairs(unit.annunciator) do + if key == "BoilerOnline" or key == "TurbineOnline" then + local every = true - if not every then rcs_disconn = true end + -- split up online arrays + for id = 1, #val do + every = every and val[id] - 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] + if key == "BoilerOnline" then unit.boiler_ps_tbl[id].publish(key, val[id]) + else + unit.turbine_ps_tbl[id].publish(key, val[id]) end + end - if key == "HeatingRateLow" and any then - rcs_warn = true - elseif key == "WaterLevelLow" and any then - rcs_hazard = true - end + if not every then rcs_disconn = 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 + 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 == "GeneratorTrip" and any then - rcs_warn = true - elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then - rcs_hazard = true - 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) - else - -- non-table fields - unit.unit_ps.publish(key, val) + 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 - 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 + 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 - unit.unit_ps.publish("U_RCS", rcs_status) + 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 + + 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 - --#endregion + unit.unit_ps.publish("U_RCS", rcs_status) - --#region Reactor Data + --#endregion - unit.reactor_data = data[6] + --#region Reactor Data - local control_status = 1 - local reactor_status = 1 - local rps_status = 1 + unit.reactor_data = data[6] - 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 + local control_status = 1 + local reactor_status = 1 + local reactor_state = 1 + local rps_status = 1 - -- update reactor/control status - if unit.reactor_data.mek_status.status then - reactor_status = 4 - control_status = util.trinary(unit.annunciator.AutoControl, 4, 3) + if unit.connected then + -- update RPS status + if unit.reactor_data.rps_tripped then + control_status = 2 + + if unit.reactor_data.rps_trip_cause == "manual" then + reactor_state = 4 -- disabled + rps_status = 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 + reactor_state = 6 -- SCRAM + rps_status = 2 end + else rps_status = 4 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 + -- update reactor/control status + if unit.reactor_data.mek_status.status then + reactor_status = 4 + reactor_state = 5 -- running + control_status = util.trinary(unit.annunciator.AutoControl, 4, 3) + else + if unit.reactor_data.no_reactor then + reactor_status = 2 + reactor_state = 3 -- faulted + elseif not unit.reactor_data.formed then + reactor_status = 3 + reactor_state = 2 -- not formed + elseif unit.reactor_data.rps_status.force_dis then + reactor_status = 3 + reactor_state = 7 -- force disabled + else + reactor_status = 4 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 + 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.mek_status) == "table" then - for key, val in pairs(unit.reactor_data.mek_status) do - unit.unit_ps.publish(key, val) - 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_ReactorStateStatus", reactor_state) + unit.unit_ps.publish("U_RPS", rps_status) + + --#endregion - unit.unit_ps.publish("U_ControlStatus", control_status) - unit.unit_ps.publish("U_ReactorStatus", reactor_status) - unit.unit_ps.publish("U_RPS", rps_status) + --#region RTU Devices - --#endregion + unit.boiler_data_tbl = data[7] - 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 - 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 + local computed_status = 1 - local boiler_status = 1 + if unit.rtu_hw.boilers[id].connected then + if unit.rtu_hw.boilers[id].faulted then + boiler_status = 3 + computed_status = 3 + elseif boiler.formed then + boiler_status = 4 - 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 + if boiler.state.boil_rate > 0 then + computed_status = 5 else - boiler_status = 2 + computed_status = 4 end + else + boiler_status = 2 + computed_status = 2 end - ps.publish("BoilerStatus", boiler_status) + _record_multiblock_status(unit.rtu_hw.boilers[id].faulted, boiler, ps) end - unit.turbine_data_tbl = data[8] + ps.publish("BoilerStatus", boiler_status) + ps.publish("BoilerStateStatus", computed_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 + 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 + local turbine_status = 1 + local computed_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 + if unit.rtu_hw.turbines[id].connected then + if unit.rtu_hw.turbines[id].faulted then + turbine_status = 3 + computed_status = 3 + elseif turbine.formed then + turbine_status = 4 + + if turbine.tanks.energy_fill >= 0.99 then + computed_status = 6 + elseif turbine.state.flow_rate < 100 then + computed_status = 4 else - turbine_status = 2 + computed_status = 5 end + else + turbine_status = 2 + computed_status = 2 + end + + _record_multiblock_status(unit.rtu_hw.turbines[id].faulted, turbine, ps) + end + + ps.publish("TurbineStatus", turbine_status) + ps.publish("TurbineStateStatus", computed_status) + end + + unit.tank_data_tbl = data[9] + + unit.last_rate_change_ms = data[10] + unit.turbine_flow_stable = data[11] + + --#endregion + + --#region Status Information Display + + local ecam = {} -- aviation reference :) back to VATSIM I go... + + -- local function red(text) return { text = text, color = colors.red } end + local function white(text) return { text = text, color = colors.white } end + local function blue(text) return { text = text, color = colors.blue } end + + -- unit.reactor_data.rps_status = { + -- high_dmg = false, + -- high_temp = false, + -- low_cool = false, + -- ex_waste = false, + -- ex_hcool = false, + -- no_fuel = false, + -- fault = false, + -- timeout = false, + -- manual = false, + -- automatic = false, + -- sys_fail = false, + -- force_dis = false + -- } + + -- if unit.reactor_data.rps_status then + -- for k, v in pairs(unit.alarms) do + -- unit.alarms[k] = ALARM_STATE.TRIPPED + -- end + -- end + + if tripped(unit.alarms[ALARM.ContainmentBreach]) then + local items = { white("REACTOR MELTDOWN"), blue("DON HAZMAT SUIT") } + table.insert(ecam, { color = colors.red, text = "CONTAINMENT BREACH", help = "ContainmentBreach", items = items }) + end + + if tripped(unit.alarms[ALARM.ContainmentRadiation]) then + local items = { + white("RADIATION DETECTED"), + blue("DON HAZMAT SUIT"), + blue("RESOLVE LEAK"), + blue("AWAIT SAFE LEVELS") + } + + table.insert(ecam, { color = colors.red, text = "RADIATION LEAK", help = "ContainmentRadiation", items = items }) + end + + if tripped(unit.alarms[ALARM.CriticalDamage]) then + local items = { white("MELTDOWN IMMINENT"), blue("EVACUATE") } + table.insert(ecam, { color = colors.red, text = "RCT DAMAGE CRITICAL", help = "CriticalDamage", items = items }) + end + + if tripped(unit.alarms[ALARM.ReactorLost]) then + local items = { white("REACTOR OFF-LINE"), blue("CHECK PLC") } + table.insert(ecam, { color = colors.red, text = "REACTOR CONN LOST", help = "ReactorLost", items = items }) + end + + if tripped(unit.alarms[ALARM.ReactorDamage]) then + local items = { white("REACTOR DAMAGED"), blue("CHECK RCS"), blue("AWAIT DMG REDUCED") } + table.insert(ecam, { color = colors.red, text = "REACTOR DAMAGE", help = "ReactorDamage", items = items }) + end + + if tripped(unit.alarms[ALARM.ReactorOverTemp]) then + local items = { white("DAMAGING TEMP"), blue("CHECK RCS"), blue("AWAIT COOLDOWN") } + table.insert(ecam, { color = colors.red, text = "REACTOR OVER TEMP", help = "ReactorOverTemp", items = items }) + end + + if tripped(unit.alarms[ALARM.ReactorHighTemp]) then + local items = { white("OVER EXPECTED TEMP"), blue("CHECK RCS") } + table.insert(ecam, { color = colors.yellow, text = "REACTOR HIGH TEMP", help = "ReactorHighTemp", items = items}) + end + + if tripped(unit.alarms[ALARM.ReactorWasteLeak]) then + local items = { white("AT WASTE CAPACITY"), blue("CHECK WASTE OUTPUT"), blue("KEEP RCT DISABLED") } + table.insert(ecam, { color = colors.red, text = "REACTOR WASTE LEAK", help = "ReactorWasteLeak", items = items}) + end + + if tripped(unit.alarms[ALARM.ReactorHighWaste]) then + local items = { blue("CHECK WASTE OUTPUT") } + table.insert(ecam, { color = colors.yellow, text = "REACTOR WASTE HIGH", help = "ReactorHighWaste", items = items}) + end + + if tripped(unit.alarms[ALARM.RPSTransient]) then + local items = {} + local stat = unit.reactor_data.rps_status + + -- for k, _ in pairs(stat) do stat[k] = true end + + local function insert(cond, key, text, color) if cond[key] then table.insert(items, { text = text, help = key, color = color }) end end + + table.insert(items, white("REACTOR SCRAMMED")) + insert(stat, "high_dmg", "HIGH DAMAGE", colors.red) + insert(stat, "high_temp", "HIGH TEMPERATURE", colors.red) + insert(stat, "low_cool", "CRIT LOW COOLANT") + insert(stat, "ex_waste", "EXCESS WASTE") + insert(stat, "ex_hcool", "EXCESS HEATED COOL") + insert(stat, "no_fuel", "NO FUEL") + insert(stat, "fault", "HARDWARE FAULT") + insert(stat, "timeout", "SUPERVISOR DISCONN") + insert(stat, "manual", "MANUAL SCRAM", colors.white) + insert(stat, "automatic", "AUTOMATIC SCRAM") + insert(stat, "sys_fail", "NOT FORMED", colors.red) + insert(stat, "force_dis", "FORCE DISABLED", colors.red) + table.insert(items, blue("RESOLVE PROBLEM")) + table.insert(items, blue("RESET RPS")) + + table.insert(ecam, { color = colors.yellow, text = "RPS TRANSIENT", help = "RPSTransient", items = items}) + end + + if tripped(unit.alarms[ALARM.RCSTransient]) then + local items = {} + local annunc = unit.annunciator + + -- for k, v in pairs(annunc) do + -- if type(v) == "boolean" then annunc[k] = true end + -- if type(v) == "table" then + -- for a, _ in pairs(v) do + -- v[a] = true + -- end + -- end + -- end + + local function insert(cond, key, text, color) + if cond == true or (type(cond) == "table" and cond[key]) then table.insert(items, { text = text, help = key, color = color }) end + end + + table.insert(items, white("COOLANT PROBLEM")) + + insert(annunc, "RCPTrip", "RCP TRIP", colors.red) + insert(annunc, "CoolantLevelLow", "LOW COOLANT") + + if unit.num_boilers == 0 then + if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then + insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH") + end + + if unit.turbine_flow_stable then + insert(annunc, "RCSFlowLow", "RCS FLOW LOW") + insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH") + insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH") + end + else + if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then + insert(annunc, "RCSFlowLow", "RCS FLOW LOW") + insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH") + insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH") + end + + if unit.turbine_flow_stable then + insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH") end + end + + insert(annunc, "MaxWaterReturnFeed", "MAX WTR RTRN FEED") - ps.publish("TurbineStatus", turbine_status) + for k, v in ipairs(annunc.WaterLevelLow) do insert(v, "WaterLevelLow", "BOILER " .. k .. " WTR LOW", colors.red) end + for k, v in ipairs(annunc.HeatingRateLow) do insert(v, "HeatingRateLow", "BOILER " .. k .. " HEAT RATE") end + for k, v in ipairs(annunc.TurbineOverSpeed) do insert(v, "TurbineOverSpeed", "TURBINE " .. k .. " OVERSPD", colors.red) end + for k, v in ipairs(annunc.GeneratorTrip) do insert(v, "GeneratorTrip", "TURBINE " .. k .. " GEN TRIP") end + + table.insert(items, blue("CHECK COOLING SYS")) + + table.insert(ecam, { color = colors.yellow, text = "RCS TRANSIENT", help = "RCSTransient", items = items}) + end + + if tripped(unit.alarms[ALARM.TurbineTrip]) then + local items = {} + + for k, v in ipairs(unit.annunciator.TurbineTrip) do + if v then table.insert(items, { text = "TURBINE " .. k .. " TRIP", help = "TurbineTrip" }) end end - unit.tank_data_tbl = data[9] + table.insert(items, blue("CHECK ENERGY OUT")) + table.insert(ecam, { color = colors.red, text = "TURBINE TRIP", help = "TurbineTripAlarm", items = items}) end + + if not (tripped(unit.alarms[ALARM.ReactorLost]) or unit.connected) then + local items = { blue("CHECK PLC") } + table.insert(ecam, { color = colors.yellow, text = "REACTOR OFF-LINE", items = items }) + end + + for k, v in ipairs(unit.annunciator.BoilerOnline) do + if not v then + local items = { blue("CHECK RTU") } + table.insert(ecam, { color = colors.yellow, text = "BOILER " .. k .. " OFF-LINE", items = items}) + end + end + + for k, v in ipairs(unit.annunciator.TurbineOnline) do + if not v then + local items = { blue("CHECK RTU") } + table.insert(ecam, { color = colors.yellow, text = "TURBINE " .. k .. " OFF-LINE", items = items}) + end + end + + -- if no alarms, put some basic status messages in + if #ecam == 0 then + table.insert(ecam, { color = colors.green, text = "REACTOR " .. util.trinary(unit.reactor_data.mek_status.status, "NOMINAL", "IDLE"), items = {}}) + + local plural = util.trinary(unit.num_turbines > 1, "S", "") + table.insert(ecam, { color = colors.green, text = "TURBINE" .. plural .. util.trinary(unit.turbine_flow_stable, " STABLE", " STABILIZING"), items = {}}) + end + + unit.unit_ps.publish("U_ECAM", textutils.serialize(ecam)) + + --#endregion end -- get the IO controller database diff --git a/pocket/pocket.lua b/pocket/pocket.lua index 04823863..0afc0683 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -14,6 +14,18 @@ local LINK_STATE = iocontrol.LINK_STATE local pocket = {} +local MQ__RENDER_CMD = { + UNLOAD_SV_APPS = 1, + UNLOAD_API_APPS = 2 +} + +local MQ__RENDER_DATA = { + LOAD_APP = 1 +} + +pocket.MQ__RENDER_CMD = MQ__RENDER_CMD +pocket.MQ__RENDER_DATA = MQ__RENDER_DATA + ---@type pkt_config local config = {} @@ -23,6 +35,8 @@ pocket.config = config function pocket.load_config() if not settings.load("/pocket.settings") then return false end + config.TempScale = settings.get("TempScale") + config.SVR_Channel = settings.get("SVR_Channel") config.CRD_Channel = settings.get("CRD_Channel") config.PKT_Channel = settings.get("PKT_Channel") @@ -36,6 +50,9 @@ function pocket.load_config() local cfv = util.new_validator() + cfv.assert_type_int(config.TempScale) + cfv.assert_range(config.TempScale, 1, 4) + cfv.assert_channel(config.SVR_Channel) cfv.assert_channel(config.CRD_Channel) cfv.assert_channel(config.PKT_Channel) @@ -58,13 +75,278 @@ function pocket.load_config() return cfv.valid() end +---@enum POCKET_APP_ID +local APP_ID = { + ROOT = 1, + -- main app pages + UNITS = 2, + GUIDE = 3, + ABOUT = 4, + -- diag app page + ALARMS = 5, + -- other + DUMMY = 6, + NUM_APPS = 6 +} + +pocket.APP_ID = APP_ID + +---@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 +---@param render_queue mqueue +function pocket.init_nav(render_queue) + local self = { + pane = nil, ---@type graphics_element + sidebar = nil, ---@type graphics_element + apps = {}, + containers = {}, + help_map = {}, + help_return = nil, + cur_app = APP_ID.ROOT + } + + self.cur_page = self.root + + ---@class pocket_nav + local nav = {} + + -- set the root pane element to switch between apps with + ---@param root_pane graphics_element + function nav.set_pane(root_pane) self.pane = root_pane end + + -- link sidebar element + ---@param sidebar graphics_element + function 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) + ---@param pane? graphics_element multipane if this is a simple paned app, then nav_to must be a number + ---@param require_sv? boolean true to specifiy if this app should be unloaded when the supervisor connection is lost + ---@param require_api? boolean true to specifiy if this app should be unloaded when the api connection is lost + function nav.register_app(app_id, container, pane, require_sv, require_api) + ---@class pocket_app + local app = { + loaded = false, + cur_page = nil, ---@type nav_tree_page + pane = pane, + paned_pages = {}, + sidebar_items = {} + } + + app.load = function () app.loaded = true end + app.unload = function () app.loaded = false end + + -- check which connections this requires + ---@return boolean requires_sv, boolean requires_api + function app.check_requires() return require_sv or false, require_api or false 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 + + -- configure the sidebar + ---@param items table + 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_load(on_load) + app.load = function () + on_load() + app.loaded = true + end + end + + -- function to run to close out the app + ---@param on_unload function callback + function app.set_unload(on_unload) + app.unload = function () + on_unload() + app.loaded = false + end + 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|nil 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 and app.cur_page == nil then + app.cur_page = page + 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 + + -- delete paned pages and clear the current page + function app.delete_pages() + app.paned_pages = {} + app.cur_page = nil + 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 + + -- open an app + ---@param app_id POCKET_APP_ID + function nav.open_app(app_id) + -- reset help return on navigating out of an app + if app_id == APP_ID.ROOT then self.help_return = nil end + + local app = self.apps[app_id] ---@type pocket_app + if app then + if not app.loaded then render_queue.push_data(MQ__RENDER_DATA.LOAD_APP, app_id) 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 + end + + -- load a given app + ---@param app_id POCKET_APP_ID + function nav.load_app(app_id) + self.apps[app_id].load() + end + + -- unload api-dependent apps + function nav.unload_api() + for id, app in pairs(self.apps) do + local _, api = app.check_requires() + if app.loaded and api then + if id == self.cur_app then nav.open_app(APP_ID.ROOT) end + app.unload() + end + end + end + + -- unload supervisor-dependent apps + function nav.unload_sv() + for id, app in pairs(self.apps) do + local sv, _ = app.check_requires() + if app.loaded and sv then + if id == self.cur_app then nav.open_app(APP_ID.ROOT) end + app.unload() + end + end + end + + -- get a list of the app containers (usually Div elements) + function nav.get_containers() return self.containers end + + -- get the currently active page + ---@return nav_tree_page + function nav.get_current_page() + return self.apps[self.cur_app].get_current_page() + end + + -- attempt to navigate up within the active app, otherwise open home page
+ -- except, this will go back to a prior app if leaving the help app after open_help was used + function nav.nav_up() + -- return out of help if opened with open_help + if self.help_return then + nav.open_app(self.help_return) + self.help_return = nil + return + end + + 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") + nav.open_app(APP_ID.ROOT) + end + end + + -- open the help app, to show the reference for a key + function nav.open_help(key) + self.help_return = self.cur_app + + nav.open_app(APP_ID.GUIDE) + + local load = self.help_map[key] + if load then load() end + end + + -- link the help map from the guide app + function nav.link_help(map) self.help_map = map end + + return nav +end + -- pocket coordinator + supervisor communications ---@nodiscard ---@param version string pocket version ---@param nic nic network interface device ---@param sv_watchdog watchdog ---@param api_watchdog watchdog -function pocket.comms(version, nic, sv_watchdog, api_watchdog) +---@param nav pocket_nav +function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) local self = { sv = { linked = false, @@ -163,6 +445,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) -- close connection to the supervisor function public.close_sv() sv_watchdog.cancel() + nav.unload_sv() self.sv.linked = false self.sv.r_seq_num = nil self.sv.addr = comms.BROADCAST @@ -172,6 +455,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) -- close connection to coordinator API server function public.close_api() api_watchdog.cancel() + nav.unload_api() self.api.linked = false self.api.r_seq_num = nil self.api.addr = comms.BROADCAST @@ -274,7 +558,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) 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)) + log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type, length, packet.scada_frame.length())) end return ok end @@ -324,7 +608,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) iocontrol.record_facility_data(packet.data) end elseif packet.type == CRDN_TYPE.API_GET_UNIT then - if _check_length(packet, 9) then + if _check_length(packet, 11) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then iocontrol.record_unit_data(packet.data) end else _fail_type(packet) end @@ -353,6 +637,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) elseif packet.type == MGMT_TYPE.CLOSE then -- handle session close api_watchdog.cancel() + nav.unload_api() self.api.linked = false self.api.r_seq_num = nil self.api.addr = comms.BROADCAST @@ -371,8 +656,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) -- get configuration local conf = { num_units = fac_config[1], cooling = fac_config[2] } - ---@todo unit options - iocontrol.init_fac(conf, 1) + iocontrol.init_fac(conf, config.TempScale) log.info("coordinator connection established") self.establish_delay_counter = 0 @@ -459,6 +743,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) elseif packet.type == MGMT_TYPE.CLOSE then -- handle session close sv_watchdog.cancel() + nav.unload_sv() self.sv.linked = false self.sv.r_seq_num = nil self.sv.addr = comms.BROADCAST diff --git a/pocket/renderer.lua b/pocket/renderer.lua index 892fc923..bc16037f 100644 --- a/pocket/renderer.lua +++ b/pocket/renderer.lua @@ -92,4 +92,20 @@ function renderer.handle_mouse(event) end end +-- handle a keyboard event +---@param event key_interaction|nil +function renderer.handle_key(event) + if ui.display ~= nil and event ~= nil then + ui.display.handle_key(event) + end +end + +-- handle a paste event +---@param text string +function renderer.handle_paste(text) + if ui.display ~= nil then + ui.display.handle_paste(text) + end +end + return renderer diff --git a/pocket/startup.lua b/pocket/startup.lua index 8ba8d3e1..b56da4fb 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -2,27 +2,35 @@ -- SCADA System Access on a Pocket Computer -- +---@diagnostic disable-next-line: undefined-global +local _is_pocket_env = pocket or periphemu -- luacheck: ignore pocket + require("/initenv").init_env() local crash = require("scada-common.crash") local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") local network = require("scada-common.network") local ppm = require("scada-common.ppm") -local tcd = require("scada-common.tcd") local util = require("scada-common.util") -local core = require("graphics.core") - local configure = require("pocket.configure") local iocontrol = require("pocket.iocontrol") local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") +local threads = require("pocket.threads") -local POCKET_VERSION = "v0.9.1-alpha" +local POCKET_VERSION = "v0.10.0-alpha" local println = util.println local println_ts = util.println_ts +-- check environment (allows Pocket or CraftOS-PC) +if not _is_pocket_env then + println("You can only use this application on a pocket computer.") + return +end + ---------------------------------------- -- get configuration ---------------------------------------- @@ -72,9 +80,51 @@ local function main() iocontrol.get_db().version = POCKET_VERSION ---------------------------------------- - -- setup communications & clocks + -- memory allocation + ---------------------------------------- + + -- shared memory across threads + ---@class pkt_shared_memory + local __shared_memory = { + -- pocket system state flags + ---@class pkt_state + pkt_state = { + ui_ok = false, + ui_error = nil, + shutdown = false + }, + + -- core pocket devices + pkt_dev = { + modem = ppm.get_wireless_modem() + }, + + -- system objects + pkt_sys = { + nic = nil, ---@type nic + pocket_comms = nil, ---@type pocket_comms + sv_wd = nil, ---@type watchdog + api_wd = nil, ---@type watchdog + nav = nil ---@type pocket_nav + }, + + -- message queues + q = { + mq_render = mqueue.new() + } + } + + local smem_dev = __shared_memory.pkt_dev + local smem_sys = __shared_memory.pkt_sys + + local pkt_state = __shared_memory.pkt_state + + ---------------------------------------- + -- setup system ---------------------------------------- + smem_sys.nav = pocket.init_nav(__shared_memory.q.mq_render) + -- message authentication init if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then network.init_mac(config.AuthKey) @@ -83,112 +133,59 @@ local function main() iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED) -- get the communications modem - local modem = ppm.get_wireless_modem() - if modem == nil then + if smem_dev.modem == nil then println("startup> wireless modem not found: please craft the pocket computer with a wireless modem") log.fatal("startup> no wireless modem on startup") return end -- create connection watchdogs - local conn_wd = { - sv = util.new_watchdog(config.ConnTimeout), - api = util.new_watchdog(config.ConnTimeout) - } - - conn_wd.sv.cancel() - conn_wd.api.cancel() - + smem_sys.sv_wd = util.new_watchdog(config.ConnTimeout) + smem_sys.sv_wd.cancel() + smem_sys.api_wd = util.new_watchdog(config.ConnTimeout) + smem_sys.api_wd.cancel() log.debug("startup> conn watchdogs created") -- create network interface then setup comms - local nic = network.nic(modem) - local pocket_comms = pocket.comms(POCKET_VERSION, nic, conn_wd.sv, conn_wd.api) + smem_sys.nic = network.nic(smem_dev.modem) + smem_sys.pocket_comms = pocket.comms(POCKET_VERSION, smem_sys.nic, smem_sys.sv_wd, smem_sys.api_wd, smem_sys.nav) log.debug("startup> comms init") - -- base loop clock (2Hz, 10 ticks) - local MAIN_CLOCK = 0.5 - local loop_clock = util.new_clock(MAIN_CLOCK) - -- init I/O control - iocontrol.init_core(pocket_comms) + iocontrol.init_core(smem_sys.pocket_comms, smem_sys.nav) ---------------------------------------- -- start the UI ---------------------------------------- - local ui_ok, message = renderer.try_start_ui() - if not ui_ok then - println(util.c("UI error: ", message)) - log.error(util.c("startup> GUI render failed with error ", message)) - else - -- start clock - loop_clock.start() + local ui_message + pkt_state.ui_ok, ui_message = renderer.try_start_ui() + if not pkt_state.ui_ok then + println(util.c("UI error: ", ui_message)) + log.error(util.c("startup> GUI render failed with error ", ui_message)) end ---------------------------------------- - -- main event loop + -- start system ---------------------------------------- - if ui_ok then - -- start connection watchdogs - conn_wd.sv.feed() - conn_wd.api.feed() - log.debug("startup> conn watchdogs started") - - local io_db = iocontrol.get_db() - local nav = io_db.nav - - -- main event loop - while true do - local event, param1, param2, param3, param4, param5 = util.pull_event() - - -- handle event - if event == "timer" then - if loop_clock.is_clock(param1) then - -- main loop tick - - -- relink if necessary - pocket_comms.link_update() - - -- update any tasks for the active page - 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 - -- supervisor watchdog timeout - log.info("supervisor server timeout") - pocket_comms.close_sv() - elseif conn_wd.api.is_timer(param1) then - -- coordinator watchdog timeout - log.info("coordinator api server timeout") - pocket_comms.close_api() - else - -- a non-clock/main watchdog timer event - -- notify timer callback dispatcher - tcd.handle(param1) - end - elseif event == "modem_message" then - -- got a packet - local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5) - pocket_comms.handle_packet(packet) - elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or - event == "double_click" then - -- handle a monitor touch event - renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) - end - - -- check for termination request - if event == "terminate" or ppm.should_terminate() then - log.info("terminate requested, closing server connections...") - pocket_comms.close() - log.info("connections closed") - break - end - end + if pkt_state.ui_ok then + -- init threads + local main_thread = threads.thread__main(__shared_memory) + local render_thread = threads.thread__render(__shared_memory) + + log.info("startup> completed") + + -- run threads + parallel.waitForAll(main_thread.p_exec, render_thread.p_exec) renderer.close_ui() + + if not pkt_state.ui_ok then + println(util.c("UI crashed with error: ", pkt_state.ui_error)) + end + else + println_ts("UI creation failed") end println_ts("exited") diff --git a/pocket/threads.lua b/pocket/threads.lua new file mode 100644 index 00000000..32120b22 --- /dev/null +++ b/pocket/threads.lua @@ -0,0 +1,219 @@ +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local ppm = require("scada-common.ppm") +local tcd = require("scada-common.tcd") +local util = require("scada-common.util") + +local pocket = require("pocket.pocket") +local renderer = require("pocket.renderer") + +local core = require("graphics.core") + +local threads = {} + +local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) +local RENDER_SLEEP = 100 -- (100ms, 2 ticks) + +local MQ__RENDER_CMD = pocket.MQ__RENDER_CMD +local MQ__RENDER_DATA = pocket.MQ__RENDER_DATA + +-- main thread +---@nodiscard +---@param smem pkt_shared_memory +function threads.thread__main(smem) + ---@class parallel_thread + local public = {} + + -- execute thread + function public.exec() + log.debug("main thread start") + + local loop_clock = util.new_clock(MAIN_CLOCK) + + -- start clock + loop_clock.start() + + -- load in from shared memory + local pkt_state = smem.pkt_state + local pocket_comms = smem.pkt_sys.pocket_comms + local sv_wd = smem.pkt_sys.sv_wd + local api_wd = smem.pkt_sys.api_wd + local nav = smem.pkt_sys.nav + + -- start connection watchdogs + sv_wd.feed() + api_wd.feed() + log.debug("startup> conn watchdogs started") + + -- event loop + while true do + local event, param1, param2, param3, param4, param5 = util.pull_event() + + -- handle event + if event == "timer" then + if loop_clock.is_clock(param1) then + -- main loop tick + + -- relink if necessary + pocket_comms.link_update() + + -- update any tasks for the active page + local page_tasks = nav.get_current_page().tasks + for i = 1, #page_tasks do page_tasks[i]() end + + loop_clock.start() + elseif sv_wd.is_timer(param1) then + -- supervisor watchdog timeout + log.info("supervisor server timeout") + pocket_comms.close_sv() + elseif api_wd.is_timer(param1) then + -- coordinator watchdog timeout + log.info("coordinator api server timeout") + pocket_comms.close_api() + else + -- a non-clock/main watchdog timer event + -- notify timer callback dispatcher + tcd.handle(param1) + end + elseif event == "modem_message" then + -- got a packet + local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5) + pocket_comms.handle_packet(packet) + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or + event == "double_click" then + -- handle a mouse event + renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) + elseif event == "char" or event == "key" or event == "key_up" then + -- handle a keyboard event + renderer.handle_key(core.events.new_key_event(event, param1, param2)) + elseif event == "paste" then + -- handle a paste event + renderer.handle_paste(param1) + end + + -- check for termination request or UI crash + if event == "terminate" or ppm.should_terminate() then + log.info("terminate requested, main thread exiting") + pkt_state.shutdown = true + elseif not pkt_state.ui_ok then + pkt_state.shutdown = true + log.info("terminating due to fatal UI error") + end + + if pkt_state.shutdown then + log.info("closing server connections...") + pocket_comms.close() + log.info("connections closed") + break + end + end + end + + -- execute the thread in a protected mode, retrying it on return if not shutting down + function public.p_exec() + local pkt_state = smem.pkt_state + + while not pkt_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(util.strval(result)) + end + + -- if status is true, then we are probably exiting, so this won't matter + -- this thread cannot be slept because it will miss events (namely "terminate") + if not pkt_state.shutdown then + log.info("main thread restarting now...") + end + end + end + + return public +end + +-- pocket renderer thread, tasked with long duration draws +---@nodiscard +---@param smem pkt_shared_memory +function threads.thread__render(smem) + ---@class parallel_thread + local public = {} + + -- execute thread + function public.exec() + log.debug("render thread start") + + -- load in from shared memory + local pkt_state = smem.pkt_state + local nav = smem.pkt_sys.nav + local render_queue = smem.q.mq_render + + local last_update = util.time() + + -- thread loop + while true do + -- check for messages in the message queue + while render_queue.ready() and not pkt_state.shutdown do + local msg = render_queue.pop() + + if msg ~= nil then + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + if msg.message == MQ__RENDER_CMD.UNLOAD_SV_APPS then + elseif msg.message == MQ__RENDER_CMD.UNLOAD_API_APPS then + end + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + local cmd = msg.message ---@type queue_data + + if cmd.key == MQ__RENDER_DATA.LOAD_APP then + log.debug("RENDER: load app " .. cmd.val) + + local draw_start = util.time_ms() + + pkt_state.ui_ok, pkt_state.ui_error = pcall(function () nav.load_app(cmd.val) end) + if not pkt_state.ui_ok then + log.fatal(util.c("RENDER: app load failed with error ", pkt_state.ui_error)) + else + log.debug("RENDER: app loaded in " .. (util.time_ms() - draw_start) .. "ms") + end + end + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet + end + end + + -- quick yield + util.nop() + end + + -- check for termination request + if pkt_state.shutdown then + log.info("render thread exiting") + break + end + + -- delay before next check + last_update = util.adaptive_delay(RENDER_SLEEP, last_update) + end + end + + -- execute the thread in a protected mode, retrying it on return if not shutting down + function public.p_exec() + local pkt_state = smem.pkt_state + + while not pkt_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(util.strval(result)) + end + + if not pkt_state.shutdown then + log.info("render thread restarting in 5 seconds...") + util.psleep(5) + end + end + end + + return public +end + +return threads diff --git a/pocket/ui/apps/diag_apps.lua b/pocket/ui/apps/diag_apps.lua index ec8b5e61..be63e952 100644 --- a/pocket/ui/apps/diag_apps.lua +++ b/pocket/ui/apps/diag_apps.lua @@ -3,6 +3,7 @@ -- local iocontrol = require("pocket.iocontrol") +local pocket = require("pocket.pocket") local core = require("graphics.core") @@ -15,9 +16,10 @@ local Checkbox = require("graphics.elements.controls.checkbox") local PushButton = require("graphics.elements.controls.push_button") local SwitchButton = require("graphics.elements.controls.switch_button") +local ALIGN = core.ALIGN local cpair = core.cpair -local ALIGN = core.ALIGN +local APP_ID = pocket.APP_ID -- create diagnostic app pages ---@param root graphics_element parent @@ -30,7 +32,7 @@ local function create_pages(root) local alarm_test = Div{parent=root,x=1,y=1} - local alarm_app = db.nav.register_app(iocontrol.APP_ID.ALARMS, alarm_test) + local alarm_app = db.nav.register_app(APP_ID.ALARMS, alarm_test) local page = alarm_app.new_page(nil, function () end) page.tasks = { db.diag.tone_test.get_tone_states } diff --git a/pocket/ui/apps/dummy_app.lua b/pocket/ui/apps/dummy_app.lua index d7845a76..6e924938 100644 --- a/pocket/ui/apps/dummy_app.lua +++ b/pocket/ui/apps/dummy_app.lua @@ -3,12 +3,15 @@ -- local iocontrol = require("pocket.iocontrol") +local pocket = require("pocket.pocket") local core = require("graphics.core") local Div = require("graphics.elements.div") local TextBox = require("graphics.elements.textbox") +local APP_ID = pocket.APP_ID + -- create placeholder app page ---@param root graphics_element parent local function create_pages(root) @@ -16,7 +19,7 @@ local function create_pages(root) local main = Div{parent=root,x=1,y=1} - db.nav.register_app(iocontrol.APP_ID.DUMMY, main).new_page(nil, function () end) + db.nav.register_app(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} diff --git a/pocket/ui/apps/guide.lua b/pocket/ui/apps/guide.lua new file mode 100644 index 00000000..b1748058 --- /dev/null +++ b/pocket/ui/apps/guide.lua @@ -0,0 +1,247 @@ +-- +-- System Guide +-- + +local util = require("scada-common.util") + +local iocontrol = require("pocket.iocontrol") +local pocket = require("pocket.pocket") + +local docs = require("pocket.ui.docs") +-- local style = require("pocket.ui.style") + +local guide_section = require("pocket.ui.pages.guide_section") + +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 WaitingAnim = require("graphics.elements.animations.waiting") + +local PushButton = require("graphics.elements.controls.push_button") + +local TextField = require("graphics.elements.form.text_field") + +local ALIGN = core.ALIGN +local cpair = core.cpair + +local APP_ID = pocket.APP_ID + +-- local label = style.label +-- local lu_col = style.label_unit_pair +-- local text_fg = style.text_fg + +-- new system guide view +---@param root graphics_element parent +local function new_view(root) + local db = iocontrol.get_db() + + local frame = Div{parent=root,x=1,y=1} + + local app = db.nav.register_app(APP_ID.GUIDE, frame) + + local load_div = Div{parent=frame,x=1,y=1} + local main = Div{parent=frame,x=1,y=1} + + TextBox{parent=load_div,y=12,text="Loading...",height=1,alignment=ALIGN.CENTER} + WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.cyan,colors._INHERIT)} + + local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}} + + local btn_fg_bg = cpair(colors.cyan, colors.black) + local btn_active = cpair(colors.white, colors.black) + local btn_disable = cpair(colors.gray, colors.black) + + app.set_sidebar({{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end }}) + + local page_div = nil ---@type nil|graphics_element + + -- load the app (create the elements) + local function load() + local list = { + { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end }, + { label = " \x14 ", color = core.cpair(colors.black, colors.cyan), callback = function () app.switcher(1) end }, + { label = "__?", color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(2) end } + } + + app.set_sidebar(list) + + page_div = Div{parent=main,y=2} + local p_width = page_div.get_width() - 2 + + local main_page = app.new_page(nil, 1) + local search_page = app.new_page(main_page, 2) + local use_page = app.new_page(main_page, 3) + local uis_page = app.new_page(main_page, 4) + local fps_page = app.new_page(main_page, 5) + local gls_page = app.new_page(main_page, 6) + + local home = Div{parent=page_div,x=2} + local search = Div{parent=page_div,x=2} + local use = Div{parent=page_div,x=2,width=p_width} + local uis = Div{parent=page_div,x=2,width=p_width} + local fps = Div{parent=page_div,x=2,width=p_width} + local gls = Div{parent=page_div,x=2,width=p_width} + local panes = { home, search, use, uis, fps, gls } + + local doc_map = {} + local search_db = {} + + ---@class _guide_section_constructor_data + local sect_construct_data = { app, page_div, panes, doc_map, search_db, btn_fg_bg, btn_active } + + TextBox{parent=home,y=1,text="cc-mek-scada Guide",height=1,alignment=ALIGN.CENTER} + + PushButton{parent=home,y=3,text="Search >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=search_page.nav_to} + PushButton{parent=home,y=5,text="System Usage >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=use_page.nav_to} + PushButton{parent=home,text="Operator UIs >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to} + PushButton{parent=home,text="Front Panels >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fps_page.nav_to} + PushButton{parent=home,text="Glossary >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_page.nav_to} + + TextBox{parent=search,y=1,text="Search",height=1,alignment=ALIGN.CENTER} + + local query_field = TextField{parent=search,x=1,y=3,width=18,fg_bg=cpair(colors.white,colors.gray)} + + local func_ref = {} + + PushButton{parent=search,x=20,y=3,text="GO",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()func_ref.run_search()end} + + local search_results = ListBox{parent=search,x=1,y=5,scroll_height=200,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)} + + function func_ref.run_search() + local query = string.lower(query_field.get_value()) + local s_results = { {}, {}, {} } + + search_results.remove_all() + + if string.len(query) < 3 then + TextBox{parent=search_results,text="Search requires at least 3 characters."} + return + end + + for _, entry in ipairs(search_db) do + local s_start, _ = string.find(entry[1], query, 1, true) + + if s_start == nil then + elseif s_start == 1 then + -- best match, start of key + table.insert(s_results[1], entry) + elseif string.sub(query, s_start - 1, s_start) == " " then + -- start of word, good match + table.insert(s_results[2], entry) + else + -- basic match in content + table.insert(s_results[3], entry) + end + end + + local empty = true + + for tier = 1, 3 do + for idx = 1, #s_results[tier] do + local entry = s_results[tier][idx] + TextBox{parent=search_results,text=entry[3].." >",fg_bg=cpair(colors.gray,colors.black)} + PushButton{parent=search_results,text=entry[2],alignment=ALIGN.LEFT,fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=entry[4]} + + empty = false + end + end + + if empty then + TextBox{parent=search_results,text="No results found."} + end + end + + TextBox{parent=search_results,text="Click 'GO' to search..."} + + util.nop() + + TextBox{parent=use,y=1,text="System Usage",height=1,alignment=ALIGN.CENTER} + PushButton{parent=use,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to} + + PushButton{parent=use,y=3,text="Configuring Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + PushButton{parent=use,text="Connecting Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + PushButton{parent=use,text="Manual Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + PushButton{parent=use,text="Automatic Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + PushButton{parent=use,text="Waste Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + + TextBox{parent=uis,y=1,text="Operator UIs",height=1,alignment=ALIGN.CENTER} + PushButton{parent=uis,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to} + + local annunc_page = app.new_page(uis_page, #panes + 1) + local annunc_div = Div{parent=page_div,x=2} + table.insert(panes, annunc_div) + + local alarms_page = guide_section(sect_construct_data, uis_page, "Alarms", docs.alarms, 100) + + PushButton{parent=uis,y=3,text="Alarms >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=alarms_page.nav_to} + PushButton{parent=uis,text="Annunciators >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=annunc_page.nav_to} + PushButton{parent=uis,text="Pocket UI >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + PushButton{parent=uis,text="Coordinator UI >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + + TextBox{parent=annunc_div,y=1,text="Annunciators",height=1,alignment=ALIGN.CENTER} + PushButton{parent=annunc_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to} + + local unit_gen_page = guide_section(sect_construct_data, annunc_page, "Unit General", docs.annunc.unit.main_section, 170) + local unit_rps_page = guide_section(sect_construct_data, annunc_page, "Unit RPS", docs.annunc.unit.rps_section, 100) + local unit_rcs_page = guide_section(sect_construct_data, annunc_page, "Unit RCS", docs.annunc.unit.rcs_section, 170) + + PushButton{parent=annunc_div,y=3,text="Unit General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_gen_page.nav_to} + PushButton{parent=annunc_div,text="Unit RPS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rps_page.nav_to} + PushButton{parent=annunc_div,text="Unit RCS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rcs_page.nav_to} + PushButton{parent=annunc_div,text="Facility General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + PushButton{parent=annunc_div,text="Waste & Valves >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + + TextBox{parent=fps,y=1,text="Front Panels",height=1,alignment=ALIGN.CENTER} + PushButton{parent=fps,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to} + + PushButton{parent=fps,y=3,text="Common Items >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + PushButton{parent=fps,text="Reactor PLC >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + PushButton{parent=fps,text="RTU Gateway >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + PushButton{parent=fps,text="Supervisor >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + PushButton{parent=fps,text="Coordinator >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() + + TextBox{parent=gls,y=1,text="Glossary",height=1,alignment=ALIGN.CENTER} + PushButton{parent=gls,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to} + + local gls_abbv_page = guide_section(sect_construct_data, gls_page, "Abbreviations", docs.glossary.abbvs, 120) + local gls_term_page = guide_section(sect_construct_data, gls_page, "Terminology", docs.glossary.terms, 100) + + PushButton{parent=gls,y=3,text="Abbreviations >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_abbv_page.nav_to} + PushButton{parent=gls,text="Terminology >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_term_page.nav_to} + + -- setup multipane + local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} + app.set_root_pane(u_pane) + + -- link help resources + db.nav.link_help(doc_map) + + -- done, show the app + load_pane.set_value(2) + end + + -- delete the elements and switch back to the loading screen + local function unload() + if page_div then + page_div.delete() + page_div = nil + end + + app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } }) + app.delete_pages() + + -- show loading screen + load_pane.set_value(1) + end + + app.set_load(load) + app.set_unload(unload) + + return main +end + +return new_view diff --git a/pocket/ui/apps/sys_apps.lua b/pocket/ui/apps/sys_apps.lua index aa20a8df..85f8ee0f 100644 --- a/pocket/ui/apps/sys_apps.lua +++ b/pocket/ui/apps/sys_apps.lua @@ -19,9 +19,10 @@ local TextBox = require("graphics.elements.textbox") local PushButton = require("graphics.elements.controls.push_button") +local ALIGN = core.ALIGN local cpair = core.cpair -local ALIGN = core.ALIGN +local APP_ID = pocket.APP_ID -- create system app pages ---@param root graphics_element parent @@ -34,7 +35,7 @@ local function create_pages(root) 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_app = db.nav.register_app(APP_ID.ABOUT, about_root) local about_page = about_app.new_page(nil, 1) local nt_page = about_app.new_page(about_page, 2) diff --git a/pocket/ui/pages/unit_page.lua b/pocket/ui/apps/unit.lua similarity index 61% rename from pocket/ui/pages/unit_page.lua rename to pocket/ui/apps/unit.lua index da123e05..1481a2b3 100644 --- a/pocket/ui/pages/unit_page.lua +++ b/pocket/ui/apps/unit.lua @@ -2,40 +2,43 @@ -- Unit Overview Page -- -local util = require("scada-common.util") --- local log = require("scada-common.log") +local util = require("scada-common.util") -local iocontrol = require("pocket.iocontrol") +local iocontrol = require("pocket.iocontrol") +local pocket = require("pocket.pocket") -local core = require("graphics.core") +local style = require("pocket.ui.style") -local Div = require("graphics.elements.div") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") +local boiler = require("pocket.ui.pages.unit_boiler") +local reactor = require("pocket.ui.pages.unit_reactor") +local turbine = require("pocket.ui.pages.unit_turbine") -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 core = require("graphics.core") -local PushButton = require("graphics.elements.controls.push_button") +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 WaitingAnim = require("graphics.elements.animations.waiting") + +local PushButton = require("graphics.elements.controls.push_button") + +local DataIndicator = require("graphics.elements.indicators.data") +local IconIndicator = require("graphics.elements.indicators.icon") 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 APP_ID = pocket.APP_ID -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 label = style.label +local lu_col = style.label_unit_pair +local text_fg = style.text_fg +local basic_states = style.icon_states.basic_states +local mode_states = style.icon_states.mode_states +local red_ind_s = style.icon_states.red_ind_s +local yel_ind_s = style.icon_states.yel_ind_s local emc_ind_s = { { color = cpair(colors.black, colors.gray), symbol = "-" }, @@ -43,60 +46,58 @@ local emc_ind_s = { { 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 local function new_view(root) local db = iocontrol.get_db() - local main = Div{parent=root,x=1,y=1} + local frame = Div{parent=root,x=1,y=1} - local app = db.nav.register_app(iocontrol.APP_ID.UNITS, main) + local app = db.nav.register_app(APP_ID.UNITS, frame, nil, false, true) - TextBox{parent=main,y=2,text="Units App",height=1,alignment=ALIGN.CENTER} + local load_div = Div{parent=frame,x=1,y=1} + local main = Div{parent=frame,x=1,y=1} - TextBox{parent=main,y=4,text="Loading...",height=1,alignment=ALIGN.CENTER} + TextBox{parent=load_div,y=12,text="Loading...",height=1,alignment=ALIGN.CENTER} + WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.yellow,colors._INHERIT)} + + local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}} + + app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } }) 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 page_div = nil ---@type nil|graphics_element + -- set sidebar to display unit-specific fields based on a specified unit local function set_sidebar(id) - -- local unit = db.units[id] ---@type pioctl_unit + 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 = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(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 = " R ", color = core.cpair(colors.black, colors.lightGray), callback = nav_links[id].reactor }, { 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_boilers do + table.insert(list, { label = "B-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = nav_links[id].boiler[i] }) + 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 + for i = 1, unit.num_turbines do + table.insert(list, { label = "T-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = nav_links[id].turbine[i] }) + end app.set_sidebar(list) end + -- load the app (create the elements) local function load() - local page_div = Div{parent=main,x=2,y=2,width=main.get_width()-2} + page_div = Div{parent=main,y=2,width=main.get_width()} local panes = {} @@ -124,7 +125,8 @@ local function new_view(root) end for i = 1, db.facility.num_units do - local u_div = panes[i] ---@type graphics_element + local u_pane = panes[i] + local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2} local unit = db.units[i] ---@type pioctl_unit local u_ps = unit.unit_ps @@ -149,16 +151,13 @@ local function new_view(root) 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 rate = DataIndicator{parent=u_div,y=5,lu_colors=lu_col,label="Burn",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=db.temp_label,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) + temp.register(u_ps, "temp", function (t) temp.update(db.temp_convert(t)) end) ctrl.register(u_ps, "U_ControlStatus", ctrl.update) u_div.line_break() @@ -186,6 +185,8 @@ local function new_view(root) --#endregion + util.nop() + --#region Alarms Tab local alm_div = Div{parent=page_div} @@ -193,23 +194,49 @@ local function new_view(root) 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=1,text="Status Info Display",height=1,alignment=ALIGN.CENTER} + + local ecam_disp = ListBox{parent=alm_div,x=2,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)} + + ecam_disp.register(u_ps, "U_ECAM", function (data) + local ecam = textutils.unserialize(data) - TextBox{parent=alm_div,y=3,text="work in progress",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)} + ecam_disp.remove_all() + for _, entry in ipairs(ecam) do + local div = Div{parent=ecam_disp,height=1+#entry.items,fg_bg=cpair(entry.color,colors.black)} + local text = TextBox{parent=div,height=1,text=entry.text} + + if entry.help then + PushButton{parent=div,x=21,y=text.get_y(),text="?",callback=function()db.nav.open_help(entry.help)end,fg_bg=cpair(colors.gray,colors.black)} + end + + for _, item in ipairs(entry.items) do + local fg_bg = nil + if item.color then fg_bg = cpair(item.color, colors.black) end + + text = TextBox{parent=div,x=3,height=1,text=item.text,fg_bg=fg_bg} + + if item.help then + PushButton{parent=div,x=21,y=text.get_y(),text="?",callback=function()db.nav.open_help(item.help)end,fg_bg=cpair(colors.gray,colors.black)} + end + end + + ecam_disp.line_break() + end + end) --#endregion --#region RPS Tab - local rps_div = Div{parent=page_div} + local rps_pane = Div{parent=page_div} + local rps_div = Div{parent=rps_pane,x=2,width=main.get_width()-2} 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} @@ -246,10 +273,17 @@ local function new_view(root) --#endregion + --#region Reactor Tab + + nav_links[i].reactor = reactor(app, u_page, panes, page_div, u_ps, update) + + --#endregion + --#region RCS Tab - local rcs_div = Div{parent=page_div} - table.insert(panes, rcs_div) + local rcs_pane = Div{parent=page_div} + local rcs_div = Div{parent=rcs_pane,x=2,width=main.get_width()-2} + table.insert(panes, rcs_pane) local rcs_page = app.new_page(u_page, #panes) rcs_page.tasks = { update } @@ -304,6 +338,32 @@ local function new_view(root) ttrip.register(u_ps, "U_TurbineTrip", ttrip.update) --#endregion + + --#region Boiler Tabs + + local blr_pane = Div{parent=page_div} + nav_links[i].boiler = {} + + for b_id = 1, unit.num_boilers do + local ps = unit.boiler_ps_tbl[b_id] + nav_links[i].boiler[b_id] = boiler(app, u_page, panes, blr_pane, b_id, ps, update) + end + + --#endregion + + --#region Turbine Tabs + + local tbn_pane = Div{parent=page_div} + nav_links[i].turbine = {} + + for t_id = 1, unit.num_turbines do + local ps = unit.turbine_ps_tbl[t_id] + nav_links[i].turbine[t_id] = turbine(app, u_page, panes, tbn_pane, i, t_id, ps, update) + end + + --#endregion + + util.nop() end -- setup multipane @@ -311,9 +371,27 @@ local function new_view(root) app.set_root_pane(u_pane) set_sidebar(active_unit) + + -- done, show the app + load_pane.set_value(2) + end + + -- delete the elements and switch back to the loading screen + local function unload() + if page_div then + page_div.delete() + page_div = nil + end + + app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } }) + app.delete_pages() + + -- show loading screen + load_pane.set_value(1) end - app.set_on_load(load) + app.set_load(load) + app.set_unload(unload) return main end diff --git a/pocket/ui/docs.lua b/pocket/ui/docs.lua new file mode 100644 index 00000000..40507a1f --- /dev/null +++ b/pocket/ui/docs.lua @@ -0,0 +1,113 @@ +local docs = {} + +local target + +local function doc(key, name, desc) + ---@class pocket_doc_item + local item = { key = key, name = name, desc = desc } + table.insert(target, item) +end + +-- important to note in the future: The PLC should always be in a chunk with the reactor to ensure it can protect it on chunk load if you do not keep it all chunk loaded + +docs.alarms = {} + +target = docs.alarms +doc("ContainmentBreach", "Containment Breach", "Reactor disconnected or indicated unformed while being at or above 100% damage; explosion assumed.") +doc("ContainmentRadiation", "Containment Radiation", "Environment detector(s) assigned to the unit have observed high levels of radiation.") +doc("ReactorLost", "Reactor Lost", "Reactor PLC has stopped communicating with the supervisor.") +doc("CriticalDamage", "Damage Critical", "Reactor damage has reached or exceeded 100%, so it will explode at any moment.") +doc("ReactorDamage", "Reactor Damage", "Reactor temperature causing increasing damage to the reactor casing.") +doc("ReactorOverTemp", "Reactor Over Temp", "Reactor temperature is at or above maximum safe temperature, so it is now taking damage.") +doc("ReactorHighTemp", "Reactor High Temp", "Reactor temperature is above expected operating levels and may exceed maximum safe temperature soon.") +doc("ReactorWasteLeak", "Reactor Waste Leak", "The reactor is full of spent waste so it will now emit radiation if additional waste is generated.") +doc("ReactorHighWaste", "Reactor High Waste", "Reactor waste levels are high and may leak soon.") +doc("RPSTransient", "RPS Transient", "Reactor protection system was activated.") +doc("RCSTransient", "RCS Transient", "Something is wrong with the reactor coolant system, check RCS indicators for details.") +doc("TurbineTripAlarm", "Turbine Trip", "A turbine stopped rotating, likely due to having full energy storage. This will prevent cooling, so it needs to be resolved before using that unit.") + +docs.annunc = { + unit = { + main_section = {}, rps_section = {}, rcs_section = {} + } +} + +target = docs.annunc.unit.main_section +doc("PLCOnline", "PLC Online", "Indicates if the fission reactor PLC is connected. If it isn't, check that your PLC is on and configured properly.") +doc("PLCHeartbeat", "PLC Heartbeat", "An indicator of status data being live. As status messages are received from the PLC, this light will turn on and off. If it gets stuck, the supervisor has stopped receiving data or a screen has frozen.") +doc("RadiationMonitor", "Radiation Monitor", "On if at least one environment detector is connected and assigned to this unit.") +doc("AutoControl", "Automatic Control", "On if the reactor is under the control of one of the automatic control modes.") +doc("ReactorSCRAM", "Reactor SCRAM", "On if the reactor protection system is holding the reactor SCRAM'd.") +doc("ManualReactorSCRAM", "Manual Reactor SCRAM", "On if the operator (you) initiated a SCRAM.") +doc("AutoReactorSCRAM", "Auto Reactor SCRAM", "On if the automatic control system initiated a SCRAM. The main view screen annunciator will have an indication as to why.") +doc("RadiationWarning", "Radiation Warning", "On if radiation levels are above normal. There is likely a leak somewhere, so that should be identified and fixed. Hazmat suit recommended.") +doc("RCPTrip", "RCP Trip", "Reactor coolant pump tripped. This is a technical concept not directly mapping to Mekansim. Here, it indicates if there is either high heated coolant or low cooled coolant that caused an RPS trip. Check the coolant system if this occurs.") +doc("RCSFlowLow", "RCS Flow Low", "Indicates if the reactor coolant system flow is low. This is observed when the cooled coolant level in the reactor is dropping. This can occur while a turbine spins up, but if it persists, check that the cooling system is operating properly. This can occur with smaller boilers or when using pipes and not having enough.") +doc("CoolantLevelLow", "Coolant Level Low", "On if the reactor coolant level is lower than it should be. Check the coolant system.") +doc("ReactorTempHigh", "Reactor Temp. High", "On if the reactor temperature is above expected maximum operating temperature. This is not yet damaging, but should be attended to. Check coolant system.") +doc("ReactorHighDeltaT", "Reactor High Delta T", "On if the reactor temperature is climbing rapidly. This can occur when a reactor is starting up, but it is a concern if it happens while the burn rate is not increasing.") +doc("FuelInputRateLow", "Fuel Input Rate Low", "On if the fissile fuel levels in the reactor are dropping or very low. Ensure a steady supply of fuel is entering the reactor.") +doc("WasteLineOcclusion", "Waste Line Occlusion", "Waste levels in the reactor are increasing. Ensure your waste processing system is operating at a sufficient rate for your burn rate.") +doc("HighStartupRate", "Startup Rate High", "This is a rough calculation of if your burn rate is high enough to cause a loss of coolant on startup. A burn rate above this is likely to cause that, but it could occur at even higher or even lower rates depending on your setup (such as pipes, water supplies, and boiler tanks).") + +target = docs.annunc.unit.rps_section +doc("rps_tripped", "RPS Trip", "Indicates if the reactor protection system has caused a SCRAM.") +doc("manual", "Manual Reactor SCRAM", "Indicates if the operator (you) tripped the RPS by pressing SCRAM.") +doc("automatic", "Auto Reactor SCRAM", "Indicates if the automatic control system tripped the RPS.") +doc("high_dmg", "Damage Level High", "Indicates if the RPS tripped due to significant reactor damage. Await damage levels to lower.") +doc("ex_waste", "Excess Waste", "Indicates if the RPS tripped due to very high waste levels. Ensure waste processing system is keeping up.") +doc("ex_hcool", "Excess Heated Coolant", "Indicates if the RPS tripped due to very high heated coolant levels. Check that the cooling system is able to keep up with heated coolant flow.") +doc("high_temp", "Temperature High", "Indicates if the RPS tripped due to reaching damaging temperatures. Await damage levels to lower.") +doc("low_cool", "Coolant Level Low Low", "Indicates if the RPS tripped due to very low coolant levels that result in the temperature uncontrollably rising. Ensure that the cooling system can provide sufficient cooled coolant flow.") +doc("no_fuel", "No Fuel", "Indicates if the RPS tripped due to no fuel being available. Check fuel input.") +doc("fault", "PPM Fault", "Indicates if the RPS tripped due to a peripheral access fault. Something went wrong interfacing with the reactor, try restarting the PLC.") +doc("timeout", "Connection Timeout", "Indicates if the RPS tripped due to losing connection with the supervisory computer. Check that your PLC and supervisor remain chunk loaded.") +doc("sys_fail", "System Failure", "Indicates if the RPS tripped due to the reactor not being formed. Ensure that the multi-block is formed.") + +target = docs.annunc.unit.rcs_section +doc("RCSFault", "RCS Hardware Fault", "Indicates if one or more of the RCS devices have a peripheral fault. Check that your machines are formed. If this persists, try rebooting affected RTUs.") +doc("EmergencyCoolant", "Emergency Coolant", "Off if no emergency coolant redstone is configured, white when it is configured but not in use, and green/blue when it is activated. This is based on an RTU having a redstone emergency coolant output configured for this unit.") +doc("CoolantFeedMismatch", "Coolant Feed Mismatch", "The coolant system is accumulating heated coolant or losing cooled coolant, likely due to one of the machines not keeping up with the needs of the reactor. The flow monitor can help figure out where the problem is.") +doc("BoilRateMismatch", "Boil Rate Mismatch", "The total heating rate of the reactor exceed the tolerance from the steam input rate of the turbines OR for sodium setups, the boiler boil rates exceed the tolerance from the steam input rate of the turbines. The flow monitor can help figure out where the problem is.") +doc("SteamFeedMismatch", "Steam Feed Mismatch", "There is an above tolerance difference between turbine flow and steam input rates or the reactor/boilers are gaining steam or losing water. The flow monitor can help figure out where the problem is.") +doc("MaxWaterReturnFeed", "Max Water Return Feed", "The turbines are condensing the max rate of water that they can per the structure build. If water return is insufficient, add more saturating condensers to your turbine(s).") +doc("WaterLevelLow", "Water Level Low", "The water level in the boiler is low. A larger boiler water tank may help, or you can feed additional water into the boiler from elsewhere.") +doc("HeatingRateLow", "Heating Rate Low", "The boiler is not hot enough to boil water, but it is receiving heated coolant. This is almost never a safety concern.") +doc("SteamDumpOpen", "Steam Relief Valve Open", "This turns yellow if the turbine is set to dumping excess and red if it is set to dumping [all]. 'Relief Valve' in this case is that setting allowing the venting of steam. You should never have this set to dumping [all]. Emergency coolant activation from the supervisor will automatically set it to dumping excess to ensure there is no backup of steam as water is added.") +doc("TurbineOverSpeed", "Turbine Over Speed", "The turbine is at steam capacity, but not tripped. You may need more turbines if they can't keep up.") +doc("GeneratorTrip", "Generator Trip", "The turbine is no longer outputting power due to it having nowhere to go. Likely due to full power storage. This will lead to a Turbine Trip if not addressed.") +doc("TurbineTrip", "Turbine Trip", "The turbine has reached its maximum power charge and has stopped rotating, and as a result stopped cooling steam to water. Ensure the turbine has somewhere to output power, as this is the most common cause of reactor meltdowns. However, the likelihood of a meltdown with this system in place is much lower, especially with emergency coolant helping during turbine trips.") + +docs.glossary = { + abbvs = {}, terms = {} +} + +target = docs.glossary.abbvs +doc("G_ACK", "ACK", "Alarm ACKnowledge. Pressing this acknowledges that you understand an alarm occurred and would like to stop the audio tone(s).") +doc("G_CRD", "CRD", "Coordinator. Abbreviation for the coordinator computer.") +doc("G_DBG", "DBG", "Debug. Abbreviation for the debugging sessions from pocket computers found on the supervisor's front panel.") +doc("G_FP", "FP", "Front Panel. See Terminology.") +doc("G_PKT", "PKT", "Pocket. Abbreviation for the pocket computer.") +doc("G_PLC", "PLC", "Programmable Logic Controller. A device that not only reports data and controls outputs, but can also make decisions on its own.") +doc("G_PPM", "PPM", "Protected Peripheral Manager. This is an abstraction layer created for this project that prevents peripheral calls from crashing applications.") +doc("G_RCP", "RCP", "Reactor Coolant Pump. This is from real-world terminology with water-cooled (boiling water and pressurized water) reactors, but in this system it just reflects to the functioning of reactor coolant flow. See the annunciator page on it for more information.") +doc("G_RCS", "RCS", "Reactor Cooling System. The combination of all machines used to cool the reactor (turbines, boilers, dynamic tanks).") +doc("G_RPS", "RPS", "Reactor Protection System. A component of the reactor PLC responsible for keeping the reactor safe.") +doc("G_RTU", "RTU", "Remote Terminal Unit. Provides monitoring to and basic output from a SCADA system, interfacing with various types of devices/interfaces.") +doc("G_SCADA", "SCADA", "Supervisory Control and Data Acquisition. A control systems architecture used in a wide variety process control applications.") +doc("G_SVR", "SVR", "Supervisor. Abbreviation for the supervisory computer.") +doc("G_UI", "UI", "User Interface.") + +target = docs.glossary.terms +doc("G_Fault", "Fault", "Something has gone wrong and/or failed to function.") +doc("G_FrontPanel", "Front Panel", "A basic interface on the front of a device for viewing and sometimes modifying its state. This is what you see when looking at a computer running one of the SCADA applications.") +doc("G_Nominal", "Nominal", "Normal operation. Everything operating as intended.") +doc("G_Ringback", "Ringback", "An indication that an alarm had gone off but is no longer having its trip condition(s) met. This is to make you are aware that it occurred.") +doc("G_SCRAM", "SCRAM", "[Emergency] shut-down of a reactor by stopping the fission. In Mekanism and here, it isn't always for an emergency.") +doc("G_Transient", "Transient", "A temporary change in state from normal operation. Coolant levels dropping or core temperature rising above nominal values are examples of transients.") +doc("G_Trip", "Trip", "A checked condition had occurred, see 'Tripped'.") +doc("G_Tripped", "Tripped", "An alarm condition has been met, and is still met.") +doc("G_Tripping", "Tripping", "Alarm condition(s) is/are met, but has/have not reached the minimum time before the condition(s) is/are deemed a problem.") +doc("G_TurbineTrip", "Turbine Trip", "The turbine stopped, which prevents heated coolant from being cooled. In Mekanism, this would occur when a turbine cannot generate any more energy due to filling its buffer and having no output with any remaining energy capacity.") + +return docs diff --git a/pocket/ui/main.lua b/pocket/ui/main.lua index 87960f2c..40b11043 100644 --- a/pocket/ui/main.lua +++ b/pocket/ui/main.lua @@ -2,16 +2,20 @@ -- Pocket GUI Root -- +local util = require("scada-common.util") + local iocontrol = require("pocket.iocontrol") +local pocket = require("pocket.pocket") local diag_apps = require("pocket.ui.apps.diag_apps") local dummy_app = require("pocket.ui.apps.dummy_app") +local guide_app = require("pocket.ui.apps.guide") local sys_apps = require("pocket.ui.apps.sys_apps") +local unit_app = require("pocket.ui.apps.unit") local conn_waiting = require("pocket.ui.components.conn_waiting") local home_page = require("pocket.ui.pages.home_page") -local unit_page = require("pocket.ui.pages.unit_page") local style = require("pocket.ui.style") @@ -26,19 +30,20 @@ local Sidebar = require("graphics.elements.controls.sidebar") local SignalBar = require("graphics.elements.indicators.signal") -local LINK_STATE = iocontrol.LINK_STATE - local ALIGN = core.ALIGN - local cpair = core.cpair +local LINK_STATE = iocontrol.LINK_STATE + +local APP_ID = pocket.APP_ID + -- create new main view ---@param main graphics_element main displaybox local function init(main) local db = iocontrol.get_db() -- window header message - TextBox{parent=main,y=1,text="WIP ALPHA APP S C ",alignment=ALIGN.LEFT,height=1,fg_bg=style.header} + TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA 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)} @@ -72,20 +77,21 @@ local function init(main) local page_div = Div{parent=main_pane,x=4,y=1} home_page(page_div) - unit_page(page_div) - diag_apps(page_div) + unit_app(page_div) + guide_app(page_div) sys_apps(page_div) + diag_apps(page_div) dummy_app(page_div) - assert(#db.nav.get_containers() == iocontrol.APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered") + assert(util.table_len(db.nav.get_containers()) == APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered") 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) + db.nav.open_app(APP_ID.ROOT) --#endregion end diff --git a/pocket/ui/pages/guide_section.lua b/pocket/ui/pages/guide_section.lua new file mode 100644 index 00000000..e73567b6 --- /dev/null +++ b/pocket/ui/pages/guide_section.lua @@ -0,0 +1,68 @@ +local log = require("scada-common.log") +local util = require("scada-common.util") + +local core = require("graphics.core") + +local Div = require("graphics.elements.div") +local ListBox = require("graphics.elements.listbox") +local TextBox = require("graphics.elements.textbox") + +local PushButton = require("graphics.elements.controls.push_button") + +local ALIGN = core.ALIGN +local cpair = core.cpair + +-- new guide documentation section +---@param data _guide_section_constructor_data +---@param base_page nav_tree_page +---@param title string +---@param items table +---@param scroll_height integer +---@return nav_tree_page +return function (data, base_page, title, items, scroll_height) + local app, page_div, panes, doc_map, search_db, btn_fg_bg, btn_active = table.unpack(data) + + local section_page = app.new_page(base_page, #panes + 1) + local section_div = Div{parent=page_div,x=2} + table.insert(panes, section_div) + TextBox{parent=section_div,y=1,text=title,height=1,alignment=ALIGN.CENTER} + PushButton{parent=section_div,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=base_page.nav_to} + + local view_page = app.new_page(section_page, #panes + 1) + local section_view_div = Div{parent=page_div,x=2} + table.insert(panes, section_view_div) + TextBox{parent=section_view_div,y=1,text=title,height=1,alignment=ALIGN.CENTER} + PushButton{parent=section_view_div,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=section_page.nav_to} + + local name_list = ListBox{parent=section_div,x=1,y=3,scroll_height=30,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)} + local def_list = ListBox{parent=section_view_div,x=1,y=3,scroll_height=scroll_height,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)} + + local _end + + for i = 1, #items do + local item = items[i] ---@type pocket_doc_item + + local anchor = TextBox{parent=def_list,text=item.name,anchor=true,fg_bg=cpair(colors.blue,colors.black)} + TextBox{parent=def_list,text=item.desc} + _end = Div{parent=def_list,height=1,can_focus=true} + + local function view() + _end.focus() + view_page.nav_to() + anchor.focus() + end + + doc_map[item.key] = view + table.insert(search_db, { string.lower(item.name), item.name, title, view }) + + PushButton{parent=name_list,text=item.name,alignment=ALIGN.LEFT,fg_bg=cpair(colors.blue,colors.black),active_fg_bg=btn_active,callback=view} + + if i % 12 == 0 then util.nop() end + end + + log.debug("guide section " .. title .. " generated with final height ".. _end.get_y()) + + util.nop() + + return section_page +end diff --git a/pocket/ui/pages/home_page.lua b/pocket/ui/pages/home_page.lua index 483a8813..4b7fded1 100644 --- a/pocket/ui/pages/home_page.lua +++ b/pocket/ui/pages/home_page.lua @@ -3,6 +3,7 @@ -- local iocontrol = require("pocket.iocontrol") +local pocket = require("pocket.pocket") local core = require("graphics.core") @@ -12,11 +13,10 @@ local TextBox = require("graphics.elements.textbox") local App = require("graphics.elements.controls.app") +local ALIGN = core.ALIGN local cpair = core.cpair -local APP_ID = iocontrol.APP_ID - -local ALIGN = core.ALIGN +local APP_ID = pocket.APP_ID -- new home page view ---@param root graphics_element parent @@ -25,7 +25,7 @@ local function new_view(root) local main = Div{parent=root,x=1,y=1,height=19} - local app = db.nav.register_app(iocontrol.APP_ID.ROOT, main) + local app = db.nav.register_app(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} @@ -51,7 +51,7 @@ local function new_view(root) 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=2,y=12,text="\xb6",title="Guide",callback=function()open(APP_ID.GUIDE)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} diff --git a/pocket/ui/pages/unit_boiler.lua b/pocket/ui/pages/unit_boiler.lua new file mode 100644 index 00000000..86b963ae --- /dev/null +++ b/pocket/ui/pages/unit_boiler.lua @@ -0,0 +1,131 @@ +local types = require("scada-common.types") +local util = require("scada-common.util") + +local iocontrol = require("pocket.iocontrol") + +local style = require("pocket.ui.style") + +local core = require("graphics.core") + +local Div = require("graphics.elements.div") +local TextBox = require("graphics.elements.textbox") + +local PushButton = require("graphics.elements.controls.push_button") + +local DataIndicator = require("graphics.elements.indicators.data") +local StateIndicator = require("graphics.elements.indicators.state") +local IconIndicator = require("graphics.elements.indicators.icon") +local VerticalBar = require("graphics.elements.indicators.vbar") + +local ALIGN = core.ALIGN +local cpair = core.cpair + +local label = style.label +local lu_col = style.label_unit_pair +local text_fg = style.text_fg +local red_ind_s = style.icon_states.red_ind_s +local yel_ind_s = style.icon_states.yel_ind_s + +-- create a boiler view in the unit app +---@param app pocket_app +---@param u_page nav_tree_page +---@param panes table +---@param blr_pane graphics_element +---@param b_id integer boiler ID +---@param ps psil +---@param update function +return function (app, u_page, panes, blr_pane, b_id, ps, update) + local db = iocontrol.get_db() + + local blr_div = Div{parent=blr_pane,x=2,width=blr_pane.get_width()-2} + table.insert(panes, blr_div) + + local blr_page = app.new_page(u_page, #panes) + blr_page.tasks = { update } + + TextBox{parent=blr_div,y=1,text="BLR #"..b_id,width=8,height=1} + local status = StateIndicator{parent=blr_div,x=10,y=1,states=style.boiler.states,value=1,min_width=12} + status.register(ps, "BoilerStateStatus", status.update) + + local hcool = VerticalBar{parent=blr_div,x=1,y=4,fg_bg=cpair(colors.orange,colors.gray),height=5,width=1} + local water = VerticalBar{parent=blr_div,x=3,y=4,fg_bg=cpair(colors.blue,colors.gray),height=5,width=1} + local steam = VerticalBar{parent=blr_div,x=19,y=4,fg_bg=cpair(colors.white,colors.gray),height=5,width=1} + local ccool = VerticalBar{parent=blr_div,x=21,y=4,fg_bg=cpair(colors.lightBlue,colors.gray),height=5,width=1} + + TextBox{parent=blr_div,text="H",x=1,y=3,width=1,height=1,fg_bg=label} + TextBox{parent=blr_div,text="W",x=3,y=3,width=1,height=1,fg_bg=label} + TextBox{parent=blr_div,text="S",x=19,y=3,width=1,height=1,fg_bg=label} + TextBox{parent=blr_div,text="C",x=21,y=3,width=1,height=1,fg_bg=label} + + hcool.register(ps, "hcool_fill", hcool.update) + water.register(ps, "water_fill", water.update) + steam.register(ps, "steam_fill", steam.update) + ccool.register(ps, "ccool_fill", ccool.update) + + TextBox{parent=blr_div,text="Temperature",x=5,y=5,width=13,height=1,fg_bg=label} + local t_prec = util.trinary(db.temp_label == types.TEMP_SCALE_UNITS[types.TEMP_SCALE.KELVIN], 11, 10) + local temp = DataIndicator{parent=blr_div,x=5,y=6,lu_colors=lu_col,label="",unit=db.temp_label,format="%"..t_prec..".2f",value=0,commas=true,width=13,fg_bg=text_fg} + + temp.register(ps, "temperature", function (t) temp.update(db.temp_convert(t)) end) + + local b_wll = IconIndicator{parent=blr_div,y=10,label="Water Level Lo",states=red_ind_s} + local b_hr = IconIndicator{parent=blr_div,label="Heating Rate Lo",states=yel_ind_s} + + b_wll.register(ps, "WaterLevelLow", b_wll.update) + b_hr.register(ps, "HeatingRateLow", b_hr.update) + + TextBox{parent=blr_div,text="Boil Rate",x=1,y=13,width=12,height=1,fg_bg=label} + local boil_r = DataIndicator{parent=blr_div,x=6,y=14,lu_colors=lu_col,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=text_fg} + + boil_r.register(ps, "boil_rate", boil_r.update) + + local blr_ext_div = Div{parent=blr_pane,x=2,width=blr_pane.get_width()-2} + table.insert(panes, blr_ext_div) + + local blr_ext_page = app.new_page(blr_page, #panes) + blr_ext_page.tasks = { update } + + PushButton{parent=blr_div,x=9,y=18,text="MORE",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=blr_ext_page.nav_to} + PushButton{parent=blr_ext_div,x=9,y=18,text="BACK",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=blr_page.nav_to} + + TextBox{parent=blr_ext_div,y=1,text="More Boiler Info",height=1,alignment=ALIGN.CENTER} + + local function update_amount(indicator) + return function (x) indicator.update(x.amount) end + end + + TextBox{parent=blr_ext_div,text="Hot Coolant",x=1,y=3,width=12,height=1,fg_bg=label} + local heated_p = DataIndicator{parent=blr_ext_div,x=14,y=3,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg} + local hcool_amnt = DataIndicator{parent=blr_ext_div,x=1,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg} + + heated_p.register(ps, "hcool_fill", function (x) heated_p.update(x * 100) end) + hcool_amnt.register(ps, "hcool", update_amount(hcool_amnt)) + + TextBox{parent=blr_ext_div,text="Water Tank",x=1,y=6,width=9,height=1,fg_bg=label} + local fuel_p = DataIndicator{parent=blr_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg} + local fuel_amnt = DataIndicator{parent=blr_ext_div,x=1,y=7,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg} + + fuel_p.register(ps, "water_fill", function (x) fuel_p.update(x * 100) end) + fuel_amnt.register(ps, "water", update_amount(fuel_amnt)) + + TextBox{parent=blr_ext_div,text="Steam Tank",x=1,y=9,width=10,height=1,fg_bg=label} + local steam_p = DataIndicator{parent=blr_ext_div,x=14,y=9,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg} + local steam_amnt = DataIndicator{parent=blr_ext_div,x=1,y=10,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg} + + steam_p.register(ps, "steam_fill", function (x) steam_p.update(x * 100) end) + steam_amnt.register(ps, "steam", update_amount(steam_amnt)) + + TextBox{parent=blr_ext_div,text="Cool Coolant",x=1,y=12,width=12,height=1,fg_bg=label} + local cooled_p = DataIndicator{parent=blr_ext_div,x=14,y=12,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg} + local ccool_amnt = DataIndicator{parent=blr_ext_div,x=1,y=13,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg} + + cooled_p.register(ps, "ccool_fill", function (x) cooled_p.update(x * 100) end) + ccool_amnt.register(ps, "ccool", update_amount(ccool_amnt)) + + TextBox{parent=blr_ext_div,text="Env. Loss",x=1,y=15,width=9,height=1,fg_bg=label} + local env_loss = DataIndicator{parent=blr_ext_div,x=11,y=15,lu_colors=lu_col,label="",unit="",format="%11.8f",value=0,width=11,fg_bg=text_fg} + + env_loss.register(ps, "env_loss", env_loss.update) + + return blr_page.nav_to +end diff --git a/pocket/ui/pages/unit_reactor.lua b/pocket/ui/pages/unit_reactor.lua new file mode 100644 index 00000000..b7fb23fb --- /dev/null +++ b/pocket/ui/pages/unit_reactor.lua @@ -0,0 +1,158 @@ +local types = require("scada-common.types") +local util = require("scada-common.util") + +local iocontrol = require("pocket.iocontrol") + +local style = require("pocket.ui.style") + +local core = require("graphics.core") + +local Div = require("graphics.elements.div") +local TextBox = require("graphics.elements.textbox") + +local PushButton = require("graphics.elements.controls.push_button") + +local DataIndicator = require("graphics.elements.indicators.data") +local StateIndicator = require("graphics.elements.indicators.state") +local IconIndicator = require("graphics.elements.indicators.icon") +local VerticalBar = require("graphics.elements.indicators.vbar") + +local ALIGN = core.ALIGN +local cpair = core.cpair + +local label = style.label +local lu_col = style.label_unit_pair +local text_fg = style.text_fg +local red_ind_s = style.icon_states.red_ind_s +local yel_ind_s = style.icon_states.yel_ind_s + +-- create a reactor view in the unit app +---@param app pocket_app +---@param u_page nav_tree_page +---@param panes table +---@param page_div graphics_element +---@param u_ps psil +---@param update function +return function (app, u_page, panes, page_div, u_ps, update) + local db = iocontrol.get_db() + + local rct_pane = Div{parent=page_div} + local rct_div = Div{parent=rct_pane,x=2,width=page_div.get_width()-2} + table.insert(panes, rct_div) + + local rct_page = app.new_page(u_page, #panes) + rct_page.tasks = { update } + + TextBox{parent=rct_div,y=1,text="Reactor",width=8,height=1} + local status = StateIndicator{parent=rct_div,x=10,y=1,states=style.reactor.states,value=1,min_width=12} + status.register(u_ps, "U_ReactorStateStatus", status.update) + + local fuel = VerticalBar{parent=rct_div,x=1,y=4,fg_bg=cpair(colors.lightGray,colors.gray),height=5,width=1} + local ccool = VerticalBar{parent=rct_div,x=3,y=4,fg_bg=cpair(colors.blue,colors.gray),height=5,width=1} + local hcool = VerticalBar{parent=rct_div,x=19,y=4,fg_bg=cpair(colors.white,colors.gray),height=5,width=1} + local waste = VerticalBar{parent=rct_div,x=21,y=4,fg_bg=cpair(colors.brown,colors.gray),height=5,width=1} + + TextBox{parent=rct_div,text="F",x=1,y=3,width=1,height=1,fg_bg=label} + TextBox{parent=rct_div,text="C",x=3,y=3,width=1,height=1,fg_bg=label} + TextBox{parent=rct_div,text="H",x=19,y=3,width=1,height=1,fg_bg=label} + TextBox{parent=rct_div,text="W",x=21,y=3,width=1,height=1,fg_bg=label} + + fuel.register(u_ps, "fuel_fill", fuel.update) + ccool.register(u_ps, "ccool_fill", ccool.update) + hcool.register(u_ps, "hcool_fill", hcool.update) + waste.register(u_ps, "waste_fill", waste.update) + + ccool.register(u_ps, "ccool_type", function (type) + if type == types.FLUID.SODIUM then + ccool.recolor(cpair(colors.lightBlue, colors.gray)) + else + ccool.recolor(cpair(colors.blue, colors.gray)) + end + end) + + hcool.register(u_ps, "hcool_type", function (type) + if type == types.FLUID.SUPERHEATED_SODIUM then + hcool.recolor(cpair(colors.orange, colors.gray)) + else + hcool.recolor(cpair(colors.white, colors.gray)) + end + end) + + TextBox{parent=rct_div,text="Burn Rate",x=5,y=4,width=13,height=1,fg_bg=label} + local burn_rate = DataIndicator{parent=rct_div,x=5,y=5,lu_colors=lu_col,label="",unit="mB/t",format="%8.2f",value=0,commas=true,width=13,fg_bg=text_fg} + TextBox{parent=rct_div,text="Temperature",x=5,y=6,width=13,height=1,fg_bg=label} + local t_prec = util.trinary(db.temp_label == types.TEMP_SCALE_UNITS[types.TEMP_SCALE.KELVIN], 11, 10) + local core_temp = DataIndicator{parent=rct_div,x=5,y=7,lu_colors=lu_col,label="",unit=db.temp_label,format="%"..t_prec..".2f",value=0,commas=true,width=13,fg_bg=text_fg} + + burn_rate.register(u_ps, "act_burn_rate", burn_rate.update) + core_temp.register(u_ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end) + + local r_temp = IconIndicator{parent=rct_div,y=10,label="Reactor Temp. Hi",states=red_ind_s} + local r_rhdt = IconIndicator{parent=rct_div,label="Hi Delta Temp.",states=yel_ind_s} + local r_firl = IconIndicator{parent=rct_div,label="Fuel Rate Lo",states=yel_ind_s} + local r_wloc = IconIndicator{parent=rct_div,label="Waste Line Occl.",states=yel_ind_s} + local r_hsrt = IconIndicator{parent=rct_div,label="Hi Startup Rate",states=yel_ind_s} + + r_temp.register(u_ps, "ReactorTempHigh", r_temp.update) + r_rhdt.register(u_ps, "ReactorHighDeltaT", r_rhdt.update) + r_firl.register(u_ps, "FuelInputRateLow", r_firl.update) + r_wloc.register(u_ps, "WasteLineOcclusion", r_wloc.update) + r_hsrt.register(u_ps, "HighStartupRate", r_hsrt.update) + + TextBox{parent=rct_div,text="HR",x=1,y=16,width=4,height=1,fg_bg=label} + local heating_r = DataIndicator{parent=rct_div,x=6,y=16,lu_colors=lu_col,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=text_fg} + TextBox{parent=rct_div,text="DMG",x=1,y=17,width=4,height=1,fg_bg=label} + local damage_p = DataIndicator{parent=rct_div,x=6,y=17,lu_colors=lu_col,label="",unit="%",format="%11.2f",value=0,width=16,fg_bg=text_fg} + + heating_r.register(u_ps, "heating_rate", heating_r.update) + damage_p.register(u_ps, "damage", damage_p.update) + + local rct_ext_div = Div{parent=rct_pane,x=2,width=page_div.get_width()-2} + table.insert(panes, rct_ext_div) + + local rct_ext_page = app.new_page(rct_page, #panes) + rct_ext_page.tasks = { update } + + PushButton{parent=rct_div,x=9,y=18,text="MORE",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=rct_ext_page.nav_to} + PushButton{parent=rct_ext_div,x=9,y=18,text="BACK",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=rct_page.nav_to} + + TextBox{parent=rct_ext_div,y=1,text="More Reactor Info",height=1,alignment=ALIGN.CENTER} + + TextBox{parent=rct_ext_div,text="Fuel Tank",x=1,y=3,width=9,height=1,fg_bg=label} + local fuel_p = DataIndicator{parent=rct_ext_div,x=14,y=3,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg} + local fuel_amnt = DataIndicator{parent=rct_ext_div,x=1,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg} + + fuel_p.register(u_ps, "fuel_fill", function (x) fuel_p.update(x * 100) end) + fuel_amnt.register(u_ps, "fuel", fuel_amnt.update) + + TextBox{parent=rct_ext_div,text="Cool Coolant",x=1,y=6,width=12,height=1,fg_bg=label} + local cooled_p = DataIndicator{parent=rct_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg} + local ccool_amnt = DataIndicator{parent=rct_ext_div,x=1,y=7,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg} + + cooled_p.register(u_ps, "ccool_fill", function (x) cooled_p.update(x * 100) end) + ccool_amnt.register(u_ps, "ccool_amnt", ccool_amnt.update) + + TextBox{parent=rct_ext_div,text="Hot Coolant",x=1,y=9,width=12,height=1,fg_bg=label} + local heated_p = DataIndicator{parent=rct_ext_div,x=14,y=9,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg} + local hcool_amnt = DataIndicator{parent=rct_ext_div,x=1,y=10,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg} + + heated_p.register(u_ps, "hcool_fill", function (x) heated_p.update(x * 100) end) + hcool_amnt.register(u_ps, "hcool_amnt", hcool_amnt.update) + + TextBox{parent=rct_ext_div,text="Waste Tank",x=1,y=12,width=10,height=1,fg_bg=label} + local waste_p = DataIndicator{parent=rct_ext_div,x=14,y=12,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg} + local waste_amnt = DataIndicator{parent=rct_ext_div,x=1,y=13,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg} + + waste_p.register(u_ps, "waste_fill", function (x) waste_p.update(x * 100) end) + waste_amnt.register(u_ps, "waste", waste_amnt.update) + + TextBox{parent=rct_ext_div,text="Boil Eff.",x=1,y=15,width=9,height=1,fg_bg=label} + TextBox{parent=rct_ext_div,text="Env. Loss",x=1,y=16,width=9,height=1,fg_bg=label} + local boil_eff = DataIndicator{parent=rct_ext_div,x=11,y=15,lu_colors=lu_col,label="",unit="%",format="%9.2f",value=0,width=11,fg_bg=text_fg} + local env_loss = DataIndicator{parent=rct_ext_div,x=11,y=16,lu_colors=lu_col,label="",unit="",format="%11.8f",value=0,width=11,fg_bg=text_fg} + + boil_eff.register(u_ps, "boil_eff", function (x) boil_eff.update(x * 100) end) + env_loss.register(u_ps, "env_loss", env_loss.update) + + return rct_page.nav_to +end diff --git a/pocket/ui/pages/unit_turbine.lua b/pocket/ui/pages/unit_turbine.lua new file mode 100644 index 00000000..03cd8653 --- /dev/null +++ b/pocket/ui/pages/unit_turbine.lua @@ -0,0 +1,116 @@ +local util = require("scada-common.util") + +local iocontrol = require("pocket.iocontrol") + +local style = require("pocket.ui.style") + +local core = require("graphics.core") + +local Div = require("graphics.elements.div") +local TextBox = require("graphics.elements.textbox") + +local PushButton = require("graphics.elements.controls.push_button") + +local DataIndicator = require("graphics.elements.indicators.data") +local IconIndicator = require("graphics.elements.indicators.icon") +local PowerIndicator = require("graphics.elements.indicators.power") +local StateIndicator = require("graphics.elements.indicators.state") +local VerticalBar = require("graphics.elements.indicators.vbar") + +local ALIGN = core.ALIGN +local cpair = core.cpair + +local label = style.label +local lu_col = style.label_unit_pair +local text_fg = style.text_fg +local tri_ind_s = style.icon_states.tri_ind_s +local red_ind_s = style.icon_states.red_ind_s +local yel_ind_s = style.icon_states.yel_ind_s + +-- create a turbine view in the unit app +---@param app pocket_app +---@param u_page nav_tree_page +---@param panes table +---@param tbn_pane graphics_element +---@param u_id integer unit ID +---@param t_id integer turbine ID +---@param ps psil +---@param update function +return function (app, u_page, panes, tbn_pane, u_id, t_id, ps, update) + local db = iocontrol.get_db() + + local tbn_div = Div{parent=tbn_pane,x=2,width=tbn_pane.get_width()-2} + table.insert(panes, tbn_div) + + local tbn_page = app.new_page(u_page, #panes) + tbn_page.tasks = { update } + + TextBox{parent=tbn_div,y=1,text="TRBN #"..t_id,width=8,height=1} + local status = StateIndicator{parent=tbn_div,x=10,y=1,states=style.turbine.states,value=1,min_width=12} + status.register(ps, "TurbineStateStatus", status.update) + + local steam = VerticalBar{parent=tbn_div,x=1,y=4,fg_bg=cpair(colors.white,colors.gray),height=5,width=1} + local ccool = VerticalBar{parent=tbn_div,x=21,y=4,fg_bg=cpair(colors.green,colors.gray),height=5,width=1} + + TextBox{parent=tbn_div,text="S",x=1,y=3,width=1,height=1,fg_bg=label} + TextBox{parent=tbn_div,text="E",x=21,y=3,width=1,height=1,fg_bg=label} + + steam.register(ps, "steam_fill", steam.update) + ccool.register(ps, "energy_fill", ccool.update) + + TextBox{parent=tbn_div,text="Production",x=3,y=3,width=17,height=1,fg_bg=label} + local prod_rate = PowerIndicator{parent=tbn_div,x=3,y=4,lu_colors=lu_col,label="",format="%11.2f",value=0,rate=true,width=17,fg_bg=text_fg} + TextBox{parent=tbn_div,text="Flow Rate",x=3,y=5,width=17,height=1,fg_bg=label} + local flow_rate = DataIndicator{parent=tbn_div,x=3,y=6,lu_colors=lu_col,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=17,fg_bg=text_fg} + TextBox{parent=tbn_div,text="Steam Input Rate",x=3,y=7,width=17,height=1,fg_bg=label} + local input_rate = DataIndicator{parent=tbn_div,x=3,y=8,lu_colors=lu_col,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=17,fg_bg=text_fg} + + prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end) + flow_rate.register(ps, "flow_rate", flow_rate.update) + input_rate.register(ps, "steam_input_rate", input_rate.update) + + local t_sdo = IconIndicator{parent=tbn_div,y=10,label="Steam Dumping",states=tri_ind_s} + local t_tos = IconIndicator{parent=tbn_div,label="Over Speed",states=red_ind_s} + local t_gtrp = IconIndicator{parent=tbn_div,label="Generator Trip",states=yel_ind_s} + local t_trp = IconIndicator{parent=tbn_div,label="Turbine Trip",states=red_ind_s} + + t_sdo.register(ps, "SteamDumpOpen", t_sdo.update) + t_tos.register(ps, "TurbineOverSpeed", t_tos.update) + t_gtrp.register(ps, "GeneratorTrip", t_gtrp.update) + t_trp.register(ps, "TurbineTrip", t_trp.update) + + local tbn_ext_div = Div{parent=tbn_pane,x=2,width=tbn_pane.get_width()-2} + table.insert(panes, tbn_ext_div) + + local tbn_ext_page = app.new_page(tbn_page, #panes) + tbn_ext_page.tasks = { update } + + PushButton{parent=tbn_div,x=9,y=18,text="MORE",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=tbn_ext_page.nav_to} + PushButton{parent=tbn_ext_div,x=9,y=18,text="BACK",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=tbn_page.nav_to} + + TextBox{parent=tbn_ext_div,y=1,text="More Turbine Info",height=1,alignment=ALIGN.CENTER} + + TextBox{parent=tbn_ext_div,text="Steam Tank",x=1,y=3,width=10,height=1,fg_bg=label} + local steam_p = DataIndicator{parent=tbn_ext_div,x=14,y=3,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg} + local steam_amnt = DataIndicator{parent=tbn_ext_div,x=1,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg} + + steam_p.register(ps, "steam_fill", function (x) steam_p.update(x * 100) end) + steam_amnt.register(ps, "steam", function (x) steam_amnt.update(x.amount) end) + + TextBox{parent=tbn_ext_div,text="Energy Fill",x=1,y=6,width=12,height=1,fg_bg=label} + local charge_p = DataIndicator{parent=tbn_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg} + local charge_amnt = PowerIndicator{parent=tbn_ext_div,x=1,y=7,lu_colors=lu_col,label="",format="%17.4f",value=0,width=21,fg_bg=text_fg} + + charge_p.register(ps, "energy_fill", function (x) charge_p.update(x * 100) end) + charge_amnt.register(ps, "energy", charge_amnt.update) + + TextBox{parent=tbn_ext_div,text="Rotation Rate",x=1,y=9,width=13,height=1,fg_bg=label} + local rotation = DataIndicator{parent=tbn_ext_div,x=1,y=10,lu_colors=lu_col,label="",unit="",format="%21.12f",value=0,width=21,fg_bg=text_fg} + + rotation.register(ps, "steam", function () + local ok, result = pcall(function () return util.turbine_rotation(db.units[u_id].turbine_data_tbl[t_id]) end) + if ok then rotation.update(result) end + end) + + return tbn_page.nav_to +end diff --git a/pocket/ui/style.lua b/pocket/ui/style.lua index 2fb7526d..dc267555 100644 --- a/pocket/ui/style.lua +++ b/pocket/ui/style.lua @@ -12,7 +12,9 @@ local cpair = core.cpair style.root = cpair(colors.white, colors.black) style.header = cpair(colors.white, colors.gray) -style.label = cpair(colors.gray, colors.lightGray) +style.text_fg = cpair(colors.white, colors._INHERIT) +style.label = cpair(colors.lightGray, colors.black) +style.label_unit_pair = cpair(colors.lightGray, colors.lightGray) style.colors = { { c = colors.red, hex = 0xdf4949 }, @@ -33,6 +35,46 @@ style.colors = { -- { c = colors.brown, hex = 0x7f664c } } +local states = {} + +states.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 = "+" } +} + +states.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" } +} + +states.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 = "+" } +} + +states.tri_ind_s = { + { color = cpair(colors.black, colors.lightGray), symbol = "+" }, + { color = cpair(colors.black, colors.yellow), symbol = "\x1e" }, + { color = cpair(colors.black, colors.red), symbol = "-" } +} + +states.red_ind_s = { + { color = cpair(colors.black, colors.lightGray), symbol = "+" }, + { color = cpair(colors.black, colors.red), symbol = "-" } +} + +states.yel_ind_s = { + { color = cpair(colors.black, colors.lightGray), symbol = "+" }, + { color = cpair(colors.black, colors.yellow), symbol = "-" } +} + +style.icon_states = states + -- MAIN LAYOUT -- style.reactor = { @@ -40,7 +82,7 @@ style.reactor = { states = { { color = cpair(colors.black, colors.yellow), - text = "PLC OFF-LINE" + text = "OFF-LINE" }, { color = cpair(colors.black, colors.orange), @@ -64,7 +106,7 @@ style.reactor = { }, { color = cpair(colors.black, colors.red), - text = "FORCE DISABLED" + text = "FORCE DSBL" } } } diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 5e364e80..3e65ed61 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -17,8 +17,8 @@ local max_distance = nil local comms = {} -- protocol/data versions (protocol/data independent changes tracked by util.lua version) -comms.version = "2.5.1" -comms.api_version = "0.0.2" +comms.version = "2.5.2" +comms.api_version = "0.0.3" ---@enum PROTOCOL local PROTOCOL = { diff --git a/scada-common/types.lua b/scada-common/types.lua index d6fc7afb..aeeca160 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -74,6 +74,28 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end -- ENUMERATION TYPES -- --#region +---@enum TEMP_SCALE +types.TEMP_SCALE = { + KELVIN = 1, + CELSIUS = 2, + FAHRENHEIT = 3, + RANKINE = 4 +} + +types.TEMP_SCALE_NAMES = { + "Kelvin", + "Celsius", + "Fahrenheit", + "Rankine" +} + +types.TEMP_SCALE_UNITS = { + "K", + "\xb0C", + "\xb0F", + "\xb0R" +} + ---@enum PANEL_LINK_STATE types.PANEL_LINK_STATE = { LINKED = 1, diff --git a/scada-common/util.lua b/scada-common/util.lua index 0fe636d7..344ffd8d 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -4,6 +4,8 @@ local cc_strings = require("cc.strings") +local const = require("scada-common.constants") + local math = math local string = string local table = table @@ -22,7 +24,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.3.0" +util.version = "1.3.1" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 @@ -368,7 +370,7 @@ end --#endregion ---#region MEKANISM POWER +--#region MEKANISM MATH -- convert Joules to FE ---@nodiscard @@ -434,6 +436,22 @@ function util.power_format(fe, combine_label, format) end end +-- compute Mekanism's rotation rate for a turbine +---@nodiscard +---@param turbine turbinev_session_db turbine data +function util.turbine_rotation(turbine) + local build = turbine.build + + local inner_vol = build.steam_cap / const.mek.TURBINE_GAS_PER_TANK + local disp_rate = (build.dispersers * const.mek.TURBINE_DISPERSER_FLOW) * inner_vol + local vent_rate = build.vents * const.mek.TURBINE_VENT_FLOW + + local max_rate = math.min(disp_rate, vent_rate) + local flow = math.min(max_rate, turbine.tanks.steam.amount) + + return (flow * (turbine.tanks.steam.amount / build.steam_cap)) / max_rate +end + --#endregion --#region UTILITY CLASSES diff --git a/supervisor/startup.lua b/supervisor/startup.lua index f83e973f..89c38d8a 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.3.11" +local SUPERVISOR_VERSION = "v1.3.12" local println = util.println local println_ts = util.println_ts diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 8747b61c..bf3e8b15 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -164,7 +164,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) ReactorDamage = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ReactorDamage, tier = PRIO.EMERGENCY }, -- reactor >1200K ReactorOverTemp = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ReactorOverTemp, tier = PRIO.URGENT }, - -- reactor >=1150K + -- reactor >= computed high temp limit ReactorHighTemp = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 1, id = ALARM.ReactorHighTemp, tier = PRIO.TIMELY }, -- waste = 100% ReactorWasteLeak = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ReactorWasteLeak, tier = PRIO.EMERGENCY }, @@ -976,7 +976,9 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) self.db.control.ready, self.db.control.degraded, self.db.control.waste_mode, - self.waste_product + self.waste_product, + self.last_rate_change_ms, + self.turbine_flow_stable } end diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua index 3fe2ebcd..20b00ff9 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unitlogic.lua @@ -39,21 +39,6 @@ local ALARM_LIMS = const.ALARM_LIMITS ---@class unit_logic_extension local logic = {} --- compute Mekanism's rotation rate for a turbine ----@param turbine turbinev_session_db -local function turbine_rotation(turbine) - local build = turbine.build - - local inner_vol = build.steam_cap / const.mek.TURBINE_GAS_PER_TANK - local disp_rate = (build.dispersers * const.mek.TURBINE_DISPERSER_FLOW) * inner_vol - local vent_rate = build.vents * const.mek.TURBINE_VENT_FLOW - - local max_rate = math.min(disp_rate, vent_rate) - local flow = math.min(max_rate, turbine.tanks.steam.amount) - - return (flow * (turbine.tanks.steam.amount / build.steam_cap)) / max_rate -end - -- update the annunciator ---@param self _unit_self function logic.update_annunciator(self) @@ -333,7 +318,7 @@ function logic.update_annunciator(self) local last = self.turbine_stability_data[i] if (not self.turbine_flow_stable) and (turbine.state.steam_input_rate > 0) then - local rotation = turbine_rotation(turbine) + local rotation = util.turbine_rotation(turbine) local rotation_stable = false -- see if data updated, and if so, check rotation speed change