diff --git a/coordinator/configure.lua b/coordinator/configure.lua index 7abb267..7c3fe58 100644 --- a/coordinator/configure.lua +++ b/coordinator/configure.lua @@ -39,7 +39,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 = { + {"v1.2.4", { "Added temperature scale options" } } +} ---@class crd_configurator local configurator = {} @@ -119,6 +121,7 @@ local tmp_cfg = { UnitCount = 1, SpeakerVolume = 1.0, Time24Hour = true, + TempScale = 1, DisableFlowView = false, MainDisplay = nil, ---@type string FlowDisplay = nil, ---@type string @@ -148,6 +151,7 @@ local fields = { { "UnitDisplays", "Unit Monitors", {} }, { "SpeakerVolume", "Speaker Volume", 1.0 }, { "Time24Hour", "Use 24-hour Time Format", true }, + { "TempScale", "Temperature Scale", 1 }, { "DisableFlowView", "Disable Flow Monitor (legacy, discouraged)", false }, { "SVR_Channel", "SVR Channel", 16240 }, { "CRD_Channel", "CRD Channel", 16243 }, @@ -465,7 +469,7 @@ local function config_view(display) key_err.hide(true) -- init mac for supervisor connection - if string.len(v) >= 8 then network.init_mac(tmp_cfg.AuthKey) end + if string.len(v) >= 8 then network.init_mac(tmp_cfg.AuthKey) else network.deinit_mac() end main_pane.set_value(3) @@ -739,8 +743,12 @@ local function config_view(display) TextBox{parent=crd_c_1,x=1,y=4,height=1,text="Clock Time Format"} 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 function submit_ui_opts() tmp_cfg.Time24Hour = clock_fmt.get_value() == 1 + tmp_cfg.TempScale = temp_scale.get_value() main_pane.set_value(7) end @@ -1186,6 +1194,8 @@ 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" else val = "Rankine" end elseif f[1] == "UnitDisplays" and type(cfg.UnitDisplays) == "table" then val = "" for idx = 1, #cfg.UnitDisplays do diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index ae2ab77..bc97c99 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -35,6 +35,7 @@ function coordinator.load_config() config.UnitCount = settings.get("UnitCount") config.SpeakerVolume = settings.get("SpeakerVolume") config.Time24Hour = settings.get("Time24Hour") + config.TempScale = settings.get("TempScale") config.DisableFlowView = settings.get("DisableFlowView") config.MainDisplay = settings.get("MainDisplay") @@ -58,6 +59,8 @@ function coordinator.load_config() cfv.assert_type_int(config.UnitCount) cfv.assert_range(config.UnitCount, 1, 4) cfv.assert_type_bool(config.Time24Hour) + cfv.assert_type_int(config.TempScale) + cfv.assert_range(config.TempScale, 1, 4) cfv.assert_type_bool(config.DisableFlowView) cfv.assert_type_table(config.UnitDisplays) @@ -92,9 +95,9 @@ function coordinator.load_config() ---@class monitors_struct local monitors = { - primary = nil, ---@type table|nil - primary_name = "", - flow = nil, ---@type table|nil + main = nil, ---@type table|nil + main_name = "", + flow = nil, ---@type table|nil flow_name = "", unit_displays = {}, unit_name_map = {} @@ -118,11 +121,11 @@ function coordinator.load_config() return 2, "Main monitor is not connected." end - monitors.primary = ppm.get_periph(config.MainDisplay) - monitors.primary_name = config.MainDisplay + monitors.main = ppm.get_periph(config.MainDisplay) + monitors.main_name = config.MainDisplay - monitors.primary.setTextScale(0.5) - w, _ = ppm.monitor_block_size(monitors.primary.getSize()) + monitors.main.setTextScale(0.5) + w, _ = ppm.monitor_block_size(monitors.main.getSize()) if w ~= 8 then return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).") end @@ -299,7 +302,7 @@ function coordinator.comms(version, nic, sv_watchdog) if not self.sv_linked then if self.est_tick_waiting == nil then - self.est_start = util.time_s() + self.est_start = os.clock() self.est_last = self.est_start self.est_tick_waiting, self.est_task_done = @@ -307,10 +310,10 @@ function coordinator.comms(version, nic, sv_watchdog) _send_establish() else - self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (util.time_s() - self.est_start))) + self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (os.clock() - self.est_start))) end - if abort or (util.time_s() - self.est_start) >= LINK_TIMEOUT then + if abort or (os.clock() - self.est_start) >= LINK_TIMEOUT then self.est_task_done(false) if abort then @@ -333,9 +336,9 @@ function coordinator.comms(version, nic, sv_watchdog) elseif self.sv_config_err then coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs") ok = false - elseif (util.time_s() - self.est_last) > 1.0 then + elseif (os.clock() - self.est_last) > 1.0 then _send_establish() - self.est_last = util.time_s() + self.est_last = os.clock() end elseif self.est_tick_waiting ~= nil then self.est_task_done(true) @@ -676,7 +679,7 @@ function coordinator.comms(version, nic, sv_watchdog) if conf.num_units == config.UnitCount then -- init io controller - iocontrol.init(conf, public) + iocontrol.init(conf, public, config.TempScale) self.sv_addr = src_addr self.sv_linked = true diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 3e32d6d..312d194 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -47,7 +47,23 @@ end -- initialize the coordinator IO controller ---@param conf facility_conf configuration ---@param comms coord_comms comms reference -function iocontrol.init(conf, comms) +---@param temp_scale integer temperature unit (1 = K, 2 = C, 3 = F, 4 = R) +function iocontrol.init(conf, comms, temp_scale) + -- temperature unit label and conversion function (from Kelvin) + if temp_scale == 2 then + io.temp_label = "\xb0C" + io.temp_convert = function (t) return t - 273.15 end + elseif temp_scale == 3 then + io.temp_label = "\xb0F" + io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end + elseif temp_scale == 4 then + io.temp_label = "\xb0R" + io.temp_convert = function (t) return 1.8 * t end + else + io.temp_label = "K" + io.temp_convert = function (t) return t end + end + -- facility data structure ---@class ioctl_facility io.facility = { @@ -219,7 +235,10 @@ function iocontrol.init(conf, comms) control_state = false, burn_rate_cmd = 0.0, radiation = types.new_zero_radiation_reading(), - sna_prod_rate = 0.0, + + sna_peak_rate = 0.0, + sna_max_rate = 0.0, + sna_out_rate = 0.0, waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM, waste_product = types.WASTE_PRODUCT.PLUTONIUM, @@ -923,7 +942,7 @@ function iocontrol.update_unit_statuses(statuses) local boil_sum = 0 for id = 1, #unit.boiler_ps_tbl do - if rtu_statuses.boilers[i] == nil then + if rtu_statuses.boilers[id] == nil then -- disconnected unit.boiler_ps_tbl[id].publish("computed_status", 1) end @@ -966,7 +985,7 @@ function iocontrol.update_unit_statuses(statuses) local flow_sum = 0 for id = 1, #unit.turbine_ps_tbl do - if rtu_statuses.turbines[i] == nil then + if rtu_statuses.turbines[id] == nil then -- disconnected unit.turbine_ps_tbl[id].publish("computed_status", 1) end @@ -1009,7 +1028,7 @@ function iocontrol.update_unit_statuses(statuses) -- dynamic tank statuses if type(rtu_statuses.tanks) == "table" then for id = 1, #unit.tank_ps_tbl do - if rtu_statuses.tanks[i] == nil then + if rtu_statuses.tanks[id] == nil then -- disconnected unit.tank_ps_tbl[id].publish("computed_status", 1) end @@ -1048,12 +1067,14 @@ function iocontrol.update_unit_statuses(statuses) -- solar neutron activator status info if type(rtu_statuses.sna) == "table" then unit.num_snas = rtu_statuses.sna[1] ---@type integer - unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number - unit.sna_peak_rate = rtu_statuses.sna[3] ---@type number + unit.sna_peak_rate = rtu_statuses.sna[2] ---@type number + unit.sna_max_rate = rtu_statuses.sna[3] ---@type number + unit.sna_out_rate = rtu_statuses.sna[4] ---@type number unit.unit_ps.publish("sna_count", unit.num_snas) - unit.unit_ps.publish("sna_prod_rate", unit.sna_prod_rate) unit.unit_ps.publish("sna_peak_rate", unit.sna_peak_rate) + unit.unit_ps.publish("sna_max_rate", unit.sna_max_rate) + unit.unit_ps.publish("sna_out_rate", unit.sna_out_rate) sna_count_sum = sna_count_sum + unit.num_snas else @@ -1201,7 +1222,7 @@ function iocontrol.update_unit_statuses(statuses) local u_spent_rate = waste_rate local u_pu_rate = util.trinary(is_pu, waste_rate, 0.0) - local u_po_rate = util.trinary(not is_pu, math.min(waste_rate, unit.sna_prod_rate), 0.0) + local u_po_rate = unit.sna_out_rate unit.unit_ps.publish("pu_rate", u_pu_rate) unit.unit_ps.publish("po_rate", u_po_rate) @@ -1209,14 +1230,15 @@ function iocontrol.update_unit_statuses(statuses) unit.unit_ps.publish("sna_in", util.trinary(is_pu, 0, burn_rate)) if unit.waste_product == types.WASTE_PRODUCT.POLONIUM then + u_spent_rate = u_po_rate unit.unit_ps.publish("po_pl_rate", u_po_rate) unit.unit_ps.publish("po_am_rate", 0) po_pl_rate = po_pl_rate + u_po_rate elseif unit.waste_product == types.WASTE_PRODUCT.ANTI_MATTER then + u_spent_rate = 0 unit.unit_ps.publish("po_pl_rate", 0) unit.unit_ps.publish("po_am_rate", u_po_rate) po_am_rate = po_am_rate + u_po_rate - u_spent_rate = 0 else unit.unit_ps.publish("po_pl_rate", 0) unit.unit_ps.publish("po_am_rate", 0) diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index 07282c4..541b1a7 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -52,6 +52,16 @@ local function _init_display(monitor) end end +-- print out that the monitor is too small +---@param monitor table monitor +local function _print_too_small(monitor) + monitor.setCursorPos(1, 1) + monitor.setBackgroundColor(colors.black) + monitor.setTextColor(colors.red) + monitor.clear() + monitor.write("monitor too small") +end + -- disable the flow view ---@param disable boolean function renderer.legacy_disable_flow_view(disable) @@ -64,15 +74,15 @@ function renderer.set_displays(monitors) engine.monitors = monitors -- report to front panel as connected - iocontrol.fp_monitor_state("main", engine.monitors.primary ~= nil) + iocontrol.fp_monitor_state("main", engine.monitors.main ~= nil) iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil) for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end end -- init all displays in use by the renderer function renderer.init_displays() - -- init primary and flow monitors - _init_display(engine.monitors.primary) + -- init main and flow monitors + _init_display(engine.monitors.main) if not engine.disable_flow_view then _init_display(engine.monitors.flow) end -- init unit displays @@ -94,8 +104,8 @@ end -- initialize the dmesg output window function renderer.init_dmesg() - local disp_x, disp_y = engine.monitors.primary.getSize() - engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y) + local disp_w, disp_h = engine.monitors.main.getSize() + engine.dmesg_window = window.create(engine.monitors.main, 1, 1, disp_w, disp_h) log.direct_dmesg(engine.dmesg_window) end @@ -166,8 +176,8 @@ function renderer.try_start_ui() status, msg = pcall(function () -- show main view on main monitor - if engine.monitors.primary ~= nil then - engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root} + if engine.monitors.main ~= nil then + engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root} main_view(engine.ui.main_display) end @@ -242,43 +252,43 @@ function renderer.ui_ready() return engine.ui_ready end function renderer.handle_disconnect(device) local is_used = false - if engine.monitors ~= nil then - if engine.monitors.primary == device then - if engine.ui.main_display ~= nil then - -- delete element tree and clear root UI elements - engine.ui.main_display.delete() - end + if not engine.monitors then return false end - is_used = true - engine.monitors.primary = nil - engine.ui.main_display = nil + if engine.monitors.main == device then + if engine.ui.main_display ~= nil then + -- delete element tree and clear root UI elements + engine.ui.main_display.delete() + end - iocontrol.fp_monitor_state("main", false) - elseif engine.monitors.flow == device then - if engine.ui.flow_display ~= nil then - -- delete element tree and clear root UI elements - engine.ui.flow_display.delete() - end + is_used = true + engine.monitors.main = nil + engine.ui.main_display = nil - is_used = true - engine.monitors.flow = nil - engine.ui.flow_display = nil + iocontrol.fp_monitor_state("main", false) + elseif engine.monitors.flow == device then + if engine.ui.flow_display ~= nil then + -- delete element tree and clear root UI elements + engine.ui.flow_display.delete() + end - iocontrol.fp_monitor_state("flow", false) - else - for idx, monitor in pairs(engine.monitors.unit_displays) do - if monitor == device then - if engine.ui.unit_displays[idx] ~= nil then - engine.ui.unit_displays[idx].delete() - end + is_used = true + engine.monitors.flow = nil + engine.ui.flow_display = nil - is_used = true - engine.monitors.unit_displays[idx] = nil - engine.ui.unit_displays[idx] = nil - - iocontrol.fp_monitor_state(idx, false) - break + iocontrol.fp_monitor_state("flow", false) + else + for idx, monitor in pairs(engine.monitors.unit_displays) do + if monitor == device then + if engine.ui.unit_displays[idx] ~= nil then + engine.ui.unit_displays[idx].delete() end + + is_used = true + engine.monitors.unit_displays[idx] = nil + engine.ui.unit_displays[idx] = nil + + iocontrol.fp_monitor_state(idx, false) + break end end end @@ -293,52 +303,29 @@ end function renderer.handle_reconnect(name, device) local is_used = false - if engine.monitors ~= nil then - if engine.monitors.primary_name == name then - is_used = true - _init_display(device) - engine.monitors.primary = device + if not engine.monitors then return false end - local disp_x, disp_y = engine.monitors.primary.getSize() - engine.dmesg_window.reposition(1, 1, disp_x, disp_y, engine.monitors.primary) + -- note: handle_resize is a more adaptive way of re-initializing a connected monitor + -- since it can handle a monitor being reconnected that isn't the right size - if engine.ui_ready and (engine.ui.main_display == nil) then - engine.dmesg_window.setVisible(false) + if engine.monitors.main_name == name then + is_used = true + engine.monitors.main = device - engine.ui.main_display = DisplayBox{window=device,fg_bg=style.root} - main_view(engine.ui.main_display) - else - engine.dmesg_window.setVisible(true) - engine.dmesg_window.redraw() - end + renderer.handle_resize(name) + elseif engine.monitors.flow_name == name then + is_used = true + engine.monitors.flow = device - iocontrol.fp_monitor_state("main", true) - elseif engine.monitors.flow_name == name then - is_used = true - _init_display(device) - engine.monitors.flow = device + renderer.handle_resize(name) + else + for idx, monitor in ipairs(engine.monitors.unit_name_map) do + if monitor == name then + is_used = true + engine.monitors.unit_displays[idx] = device - if engine.ui_ready and (engine.ui.flow_display == nil) then - engine.ui.flow_display = DisplayBox{window=device,fg_bg=style.root} - flow_view(engine.ui.flow_display) - end - - iocontrol.fp_monitor_state("flow", true) - else - for idx, monitor in ipairs(engine.monitors.unit_name_map) do - if monitor == name then - is_used = true - _init_display(device) - engine.monitors.unit_displays[idx] = device - - if engine.ui_ready and (engine.ui.unit_displays[idx] == nil) then - engine.ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root} - unit_view(engine.ui.unit_displays[idx], idx) - end - - iocontrol.fp_monitor_state(idx, true) - break - end + renderer.handle_resize(name) + break end end end @@ -346,6 +333,137 @@ function renderer.handle_reconnect(name, device) return is_used end +-- handle a monitor being resized
+-- returns if this monitor is assigned + if the assigned screen still fits +---@param name string monitor name +---@return boolean is_used, boolean is_ok +function renderer.handle_resize(name) + local is_used = false + local is_ok = true + local ui = engine.ui + + if not engine.monitors then return false, false end + + if engine.monitors.main_name == name and engine.monitors.main then + local device = engine.monitors.main ---@type table + + -- this is necessary if the bottom left block was broken and on reconnect + _init_display(device) + + is_used = true + + -- resize dmesg window if needed, but don't make it thinner + local disp_w, disp_h = engine.monitors.main.getSize() + local dmsg_w, _ = engine.dmesg_window.getSize() + engine.dmesg_window.reposition(1, 1, math.max(disp_w, dmsg_w), disp_h, engine.monitors.main) + + if ui.main_display then + ui.main_display.delete() + ui.main_display = nil + end + + iocontrol.fp_monitor_state("main", true) + + engine.dmesg_window.setVisible(not engine.ui_ready) + + if engine.ui_ready then + local ok = pcall(function () + ui.main_display = DisplayBox{window=device,fg_bg=style.root} + main_view(ui.main_display) + end) + + if not ok then + if ui.main_display then + ui.main_display.delete() + ui.main_display = nil + end + + _print_too_small(device) + + iocontrol.fp_monitor_state("main", false) + is_ok = false + end + else engine.dmesg_window.redraw() end + elseif engine.monitors.flow_name == name and engine.monitors.flow then + local device = engine.monitors.flow ---@type table + + -- this is necessary if the bottom left block was broken and on reconnect + _init_display(device) + + is_used = true + + if ui.flow_display then + ui.flow_display.delete() + ui.flow_display = nil + end + + iocontrol.fp_monitor_state("flow", true) + + if engine.ui_ready then + engine.dmesg_window.setVisible(false) + + local ok = pcall(function () + ui.flow_display = DisplayBox{window=device,fg_bg=style.root} + flow_view(ui.flow_display) + end) + + if not ok then + if ui.flow_display then + ui.flow_display.delete() + ui.flow_display = nil + end + + _print_too_small(device) + + iocontrol.fp_monitor_state("flow", false) + is_ok = false + end + end + else + for idx, monitor in ipairs(engine.monitors.unit_name_map) do + local device = engine.monitors.unit_displays[idx] + + if monitor == name and device then + -- this is necessary if the bottom left block was broken and on reconnect + _init_display(device) + + is_used = true + + if ui.unit_displays[idx] then + ui.unit_displays[idx].delete() + ui.unit_displays[idx] = nil + end + + iocontrol.fp_monitor_state(idx, true) + + if engine.ui_ready then + engine.dmesg_window.setVisible(false) + + local ok = pcall(function () + ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root} + unit_view(ui.unit_displays[idx], idx) + end) + + if not ok then + if ui.unit_displays[idx] then + ui.unit_displays[idx].delete() + ui.unit_displays[idx] = nil + end + + _print_too_small(device) + + iocontrol.fp_monitor_state(idx, false) + is_ok = false + end + end + + break + end + end + end + + return is_used, is_ok +end -- handle a touch event ---@param event mouse_interaction|nil @@ -354,16 +472,15 @@ function renderer.handle_mouse(event) if engine.fp_ready and event.monitor == "terminal" then engine.ui.front_panel.handle_mouse(event) elseif engine.ui_ready then - if event.monitor == engine.monitors.primary_name then - engine.ui.main_display.handle_mouse(event) + if event.monitor == engine.monitors.main_name then + if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end elseif event.monitor == engine.monitors.flow_name then - engine.ui.flow_display.handle_mouse(event) + if engine.ui.flow_display then engine.ui.flow_display.handle_mouse(event) end else for id, monitor in ipairs(engine.monitors.unit_name_map) do - if event.monitor == monitor then - local layout = engine.ui.unit_displays[id] ---@type graphics_element - layout.handle_mouse(event) - break + local display = engine.ui.unit_displays[id] + if event.monitor == monitor and display then + if display then display.handle_mouse(event) end end end end diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 5831b8a..10294f8 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -22,7 +22,9 @@ local sounder = require("coordinator.sounder") local apisessions = require("coordinator.session.apisessions") -local COORDINATOR_VERSION = "v1.2.2" +local COORDINATOR_VERSION = "v1.2.11" + +local CHUNK_LOAD_DELAY_S = 30.0 local println = util.println local println_ts = util.println_ts @@ -40,15 +42,47 @@ local log_crypto = coordinator.log_crypto -- mount connected devices (required for monitor setup) ppm.mount_all() +local wait_on_load = true local loaded, monitors = coordinator.load_config() + +-- if the computer just started, its chunk may have just loaded (...or the user rebooted) +-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying +while wait_on_load and loaded == 2 and os.clock() < CHUNK_LOAD_DELAY_S do + term.clear() + term.setCursorPos(1, 1) + println("There was a monitor configuration problem at boot.\n") + println("Startup will keep trying every 2s in case of chunk load delays.\n") + println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock()))) + println("(click to skip to the configurator)") + + local timer_id = util.start_timer(2) + + while true do + local event, param1 = util.pull_event() + if event == "timer" and param1 == timer_id then + -- remount and re-attempt + ppm.mount_all() + loaded, monitors = coordinator.load_config() + break + elseif event == "mouse_click" or event == "terminate" then + wait_on_load = false + break + end + end +end + if loaded ~= 0 then -- try to reconfigure (user action) local success, error = configure.configure(loaded, monitors) if success then loaded, monitors = coordinator.load_config() - assert(loaded == 0, util.trinary(loaded == 1, "failed to load valid configuration", "monitor configuration invalid")) + if loaded ~= 0 then + println(util.trinary(loaded == 2, "monitor configuration invalid", "failed to load a valid configuration") .. ", please reconfigure") + return + end else - assert(success, "coordinator configuration error: " .. error) + println("configuration error: " .. error) + return end end @@ -79,8 +113,8 @@ local function main() -- system startup ---------------------------------------- - -- re-mount devices now that logging is ready - ppm.mount_all() + -- log mounts now since mounting was done before logging was ready + ppm.log_mounts() -- report versions/init fp PSIL iocontrol.init_fp(COORDINATOR_VERSION, comms.version) @@ -269,6 +303,11 @@ local function main() iocontrol.fp_has_speaker(true) end end + elseif event == "monitor_resize" then + local is_used, is_ok = renderer.handle_resize(param1) + if is_used then + log_sys(util.c("configured monitor ", param1, " resized, ", util.trinary(is_ok, "display still fits", "display no longer fits"))) + end elseif event == "timer" then if loop_clock.is_clock(param1) then -- main loop tick diff --git a/coordinator/ui/components/boiler.lua b/coordinator/ui/components/boiler.lua index 0d651d9..ce2f1bc 100644 --- a/coordinator/ui/components/boiler.lua +++ b/coordinator/ui/components/boiler.lua @@ -1,5 +1,7 @@ local style = require("coordinator.ui.style") +local iocontrol = require("coordinator.iocontrol") + local core = require("graphics.core") local Rectangle = require("graphics.elements.rectangle") @@ -13,6 +15,7 @@ local cpair = core.cpair local border = core.border local text_fg_bg = style.text_colors +local lu_col = style.lu_colors -- new boiler view ---@param root graphics_element parent @@ -20,14 +23,16 @@ local text_fg_bg = style.text_colors ---@param y integer top left y ---@param ps psil ps interface local function new_view(root, x, y, ps) + local db = iocontrol.get_db() + local boiler = Rectangle{parent=root,border=border(1,colors.gray,true),width=31,height=7,x=x,y=y} local status = StateIndicator{parent=boiler,x=9,y=1,states=style.boiler.states,value=1,min_width=12} - local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=style.lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg} - local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=style.lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg} + local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=22,fg_bg=text_fg_bg} + local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg} status.register(ps, "computed_status", status.update) - temp.register(ps, "temperature", temp.update) + temp.register(ps, "temperature", function (t) temp.update(db.temp_convert(t)) end) boil_r.register(ps, "boil_rate", boil_r.update) TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg} diff --git a/coordinator/ui/components/reactor.lua b/coordinator/ui/components/reactor.lua index c74ab0a..27b033e 100644 --- a/coordinator/ui/components/reactor.lua +++ b/coordinator/ui/components/reactor.lua @@ -1,5 +1,7 @@ local types = require("scada-common.types") +local iocontrol = require("coordinator.iocontrol") + local style = require("coordinator.ui.style") local core = require("graphics.core") @@ -23,15 +25,17 @@ local lu_col = style.lu_colors ---@param y integer top left y ---@param ps psil ps interface local function new_view(root, x, y, ps) + local db = iocontrol.get_db() + local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y} local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16} - local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg} + local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg_bg} local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg} local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg_bg} status.register(ps, "computed_status", status.update) - core_temp.register(ps, "temp", core_temp.update) + core_temp.register(ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end) burn_r.register(ps, "act_burn_rate", burn_r.update) heating_r.register(ps, "heating_rate", heating_r.update) diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index 52b633a..64fc1e0 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -3,6 +3,7 @@ -- local types = require("scada-common.types") +local util = require("scada-common.util") local iocontrol = require("coordinator.iocontrol") @@ -51,8 +52,9 @@ local period = core.flasher.PERIOD ---@param parent graphics_element parent ---@param id integer local function init(parent, id) - local unit = iocontrol.get_db().units[id] ---@type ioctl_unit - local f_ps = iocontrol.get_db().facility.ps + local db = iocontrol.get_db() + local unit = db.units[id] ---@type ioctl_unit + local f_ps = db.facility.ps local main = Div{parent=parent,x=1,y=1} @@ -114,8 +116,9 @@ local function init(parent, id) end) TextBox{parent=main,x=32,y=22,text="Core Temp",height=1,width=9,fg_bg=style.label} - local core_temp = DataIndicator{parent=main,x=32,label="",format="%11.2f",value=0,unit="K",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg} - core_temp.register(u_ps, "temp", core_temp.update) + local fmt = util.trinary(string.len(db.temp_label) == 2, "%10.2f", "%11.2f") + local core_temp = DataIndicator{parent=main,x=32,label="",format=fmt,value=0,commas=true,unit=db.temp_label,lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg} + core_temp.register(u_ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end) TextBox{parent=main,x=32,y=25,text="Burn Rate",height=1,width=9,fg_bg=style.label} local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg} diff --git a/coordinator/ui/components/unit_flow.lua b/coordinator/ui/components/unit_flow.lua index f3103e5..0cb64c2 100644 --- a/coordinator/ui/components/unit_flow.lua +++ b/coordinator/ui/components/unit_flow.lua @@ -181,10 +181,10 @@ local function make(parent, x, y, wide, unit) local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg} local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} - local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} - local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} - local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} - local spent_rate = DataIndicator{parent=waste,x=_wide(117,99),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} + local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg} + local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg} + local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg} + local spent_rate = DataIndicator{parent=waste,x=_wide(117,98),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%8.3f",value=0,width=13,fg_bg=bw_fg_bg} waste_rate.register(unit.unit_ps, "act_burn_rate", waste_rate.update) pu_rate.register(unit.unit_ps, "pu_rate", pu_rate.update) @@ -214,7 +214,7 @@ local function make(parent, x, y, wide, unit) sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end) sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update) sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update) - sna_max.register(unit.unit_ps, "sna_prod_rate", sna_max.update) + sna_max.register(unit.unit_ps, "sna_max_rate", sna_max.update) sna_in.register(unit.unit_ps, "sna_in", sna_in.update) return root diff --git a/coordinator/ui/layout/flow_view.lua b/coordinator/ui/layout/flow_view.lua index f19dda2..1040a8c 100644 --- a/coordinator/ui/layout/flow_view.lua +++ b/coordinator/ui/layout/flow_view.lua @@ -348,7 +348,7 @@ local function init(main) status.register(facility.sps_ps_tbl[1], "computed_status", status.update) TextBox{parent=sps_box,x=2,y=3,text="Input Rate",height=1,width=10,fg_bg=style.label} - local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.3f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg} + local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.2f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg} sps_in.register(facility.ps, "po_am_rate", sps_in.update) @@ -370,8 +370,8 @@ local function init(main) TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg} local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17} - local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.3f",value=0,width=17} - local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.3f",value=0,width=17} + local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.2f",value=0,width=17} + local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.2f",value=0,width=17} pu.register(facility.ps, "pu_rate", pu.update) po.register(facility.ps, "po_rate", po.update) diff --git a/pocket/startup.lua b/pocket/startup.lua index 13ac7fd..bbd8587 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -18,7 +18,7 @@ local iocontrol = require("pocket.iocontrol") local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") -local POCKET_VERSION = "v0.7.0-alpha" +local POCKET_VERSION = "v0.7.1-alpha" local println = util.println local println_ts = util.println_ts @@ -31,9 +31,13 @@ if not pocket.load_config() then -- try to reconfigure (user action) local success, error = configure.configure(true) if success then - assert(pocket.load_config(), "failed to load valid configuration") + if not pocket.load_config() then + println("failed to load a valid configuration, please reconfigure") + return + end else - assert(success, "pocket configuration error: " .. error) + println("configuration error: " .. error) + return end end diff --git a/reactor-plc/databus.lua b/reactor-plc/databus.lua index ba7a68e..7436dbb 100644 --- a/reactor-plc/databus.lua +++ b/reactor-plc/databus.lua @@ -70,9 +70,9 @@ function databus.tx_link_state(state) end -- transmit reactor enable state across the bus ----@param active boolean reactor active +---@param active any reactor active function databus.tx_reactor_state(active) - databus.ps.publish("reactor_active", active) + databus.ps.publish("reactor_active", active == true) end -- transmit RPS data across the bus diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index c5e6be3..eec1e4d 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -129,6 +129,21 @@ function plc.rps_init(reactor, is_formed) end end + -- check if the result of a peripheral call was OK, handle the failure if not + ---@nodiscard + ---@param result any PPM function call result + ---@return boolean succeeded if the result is OK, false if it was a PPM failure + local function _check_and_handle_ppm_call(result) + if result == ppm.ACCESS_FAULT then + _set_fault() + elseif result == ppm.UNDEFINED_FIELD then + _set_fault() + self.formed = false + else return true end + + return false + end + -- set emergency coolant control (if configured) ---@param state boolean true to enable emergency coolant, false to disable local function _set_emer_cool(state) @@ -167,25 +182,20 @@ function plc.rps_init(reactor, is_formed) -- check if the reactor is formed local function _is_formed() local formed = reactor.isFormed() - if formed == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - else + if _check_and_handle_ppm_call(formed) then self.formed = formed + end - if not self.state[state_keys.sys_fail] then - self.state[state_keys.sys_fail] = not formed - end + -- always update, since some ppm failures constitute not being formed + if not self.state[state_keys.sys_fail] then + self.state[state_keys.sys_fail] = not self.formed end end -- check if the reactor is force disabled local function _is_force_disabled() local disabled = reactor.isForceDisabled() - if disabled == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - else + if _check_and_handle_ppm_call(disabled) then self.force_disabled = disabled if not self.state[state_keys.force_disabled] then @@ -197,22 +207,16 @@ function plc.rps_init(reactor, is_formed) -- check for high damage local function _high_damage() local damage_percent = reactor.getDamagePercent() - if damage_percent == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - elseif not self.state[state_keys.high_dmg] then + if _check_and_handle_ppm_call(damage_percent) and not self.state[state_keys.high_dmg] then self.state[state_keys.high_dmg] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT end end -- check if the reactor is at a critically high temperature local function _high_temp() - -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 + -- mekanism: MAX_DAMAGE_TEMPERATURE = 1200K local temp = reactor.getTemperature() - if temp == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - elseif not self.state[state_keys.high_temp] then + if _check_and_handle_ppm_call(temp) and not self.state[state_keys.high_temp] then self.state[state_keys.high_temp] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE end end @@ -220,10 +224,7 @@ function plc.rps_init(reactor, is_formed) -- check if there is very low coolant local function _low_coolant() local coolant_filled = reactor.getCoolantFilledPercentage() - if coolant_filled == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - elseif not self.state[state_keys.low_coolant] then + if _check_and_handle_ppm_call(coolant_filled) and not self.state[state_keys.low_coolant] then self.state[state_keys.low_coolant] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL end end @@ -231,10 +232,7 @@ function plc.rps_init(reactor, is_formed) -- check for excess waste (>80% filled) local function _excess_waste() local w_filled = reactor.getWasteFilledPercentage() - if w_filled == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - elseif not self.state[state_keys.ex_waste] then + if _check_and_handle_ppm_call(w_filled) and not self.state[state_keys.ex_waste] then self.state[state_keys.ex_waste] = w_filled > RPS_LIMITS.MAX_WASTE_FILL end end @@ -242,10 +240,7 @@ function plc.rps_init(reactor, is_formed) -- check for heated coolant backup (>95% filled) local function _excess_heated_coolant() local hc_filled = reactor.getHeatedCoolantFilledPercentage() - if hc_filled == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - elseif not self.state[state_keys.ex_hcoolant] then + if _check_and_handle_ppm_call(hc_filled) and not self.state[state_keys.ex_hcoolant] then self.state[state_keys.ex_hcoolant] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL end end @@ -253,10 +248,7 @@ function plc.rps_init(reactor, is_formed) -- check if there is no fuel local function _insufficient_fuel() local fuel = reactor.getFuelFilledPercentage() - if fuel == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - elseif not self.state[state_keys.no_fuel] then + if _check_and_handle_ppm_call(fuel) and not self.state[state_keys.no_fuel] then self.state[state_keys.no_fuel] = fuel <= RPS_LIMITS.NO_FUEL_FILL end end @@ -477,13 +469,22 @@ function plc.rps_init(reactor, is_formed) self.tripped = false self.trip_cause = RPS_TRIP_CAUSE.OK - for i = 1, #self.state do - self.state[i] = false - end + for i = 1, #self.state do self.state[i] = false end if not quiet then log.info("RPS: reset") end end + -- partial RPS reset that only clears fault and sys_fail + function public.reset_formed() + self.tripped = false + self.trip_cause = RPS_TRIP_CAUSE.OK + + self.state[state_keys.fault] = false + self.state[state_keys.sys_fail] = false + + log.info("RPS: partial reset on formed") + end + -- reset the automatic and timeout trip flags, then clear trip if that was the trip cause function public.auto_reset() self.state[state_keys.automatic] = false diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index e667e46..9c3d382 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc") local renderer = require("reactor-plc.renderer") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "v1.6.11" +local R_PLC_VERSION = "v1.6.14" local println = util.println local println_ts = util.println_ts @@ -31,9 +31,13 @@ if not plc.load_config() then -- try to reconfigure (user action) local success, error = configure.configure(true) if success then - assert(plc.load_config(), "failed to load valid configuration") + if not plc.load_config() then + println("failed to load a valid configuration, please reconfigure") + return + end else - assert(success, "reactor PLC configuration error: " .. error) + println("configuration error: " .. error) + return end end @@ -131,15 +135,22 @@ local function main() -- we need a reactor, can at least do some things even if it isn't formed though if plc_state.no_reactor then - println("init> fission reactor not found"); + println("init> fission reactor not found") log.warning("init> no reactor on startup") plc_state.init_ok = false plc_state.degraded = true elseif not smem_dev.reactor.isFormed() then - println("init> fission reactor not formed"); + println("init> fission reactor is not formed") log.warning("init> reactor logic adapter present, but reactor is not formed") + plc_state.degraded = true + plc_state.reactor_formed = false + elseif smem_dev.reactor.getStatus() == ppm.UNDEFINED_FIELD then + -- reactor formed after ppm.mount_all was called + println("init> fission reactor was not formed") + log.warning("init> reactor reported formed, but multiblock functions are not available") + plc_state.degraded = true plc_state.reactor_formed = false end diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 85e2a86..afe6acb 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -125,9 +125,8 @@ function threads.thread__main(smem, init) plc_comms.reconnect_reactor(plc_dev.reactor) end - -- reset RPS for newly connected reactor - -- without this, is_formed will be out of date and cause it to think its no longer formed again - rps.reset() + -- partial reset of RPS, specific to becoming formed + rps.reset_formed() else -- fully lost the reactor now :( println_ts("reactor lost (failed reconnect)!") @@ -231,9 +230,8 @@ function threads.thread__main(smem, init) plc_comms.reconnect_reactor(plc_dev.reactor) end - -- reset RPS for newly connected reactor - -- without this, is_formed will be out of date and cause it to think its no longer formed again - rps.reset() + -- partial reset of RPS, specific to becoming formed + rps.reset_formed() end elseif networked and type == "modem" then -- note, check init_ok first since nic will be nil if it is false diff --git a/rtu/startup.lua b/rtu/startup.lua index d58639c..1c5a4a2 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "v1.7.13" +local RTU_VERSION = "v1.7.14" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE @@ -47,9 +47,13 @@ if not rtu.load_config() then -- try to reconfigure (user action) local success, error = configure.configure(true) if success then - assert(rtu.load_config(), "failed to load valid configuration") + if not rtu.load_config() then + println("failed to load a valid configuration, please reconfigure") + return + end else - assert(success, "RTU configuration error: " .. error) + println("configuration error: " .. error) + return end end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index eaa7ec4..180e9c8 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -17,7 +17,7 @@ local max_distance = nil local comms = {} -- protocol/data version (protocol/data independent changes tracked by util.lua version) -comms.version = "2.4.4" +comms.version = "2.4.5" ---@enum PROTOCOL local PROTOCOL = { diff --git a/scada-common/network.lua b/scada-common/network.lua index 742f418..bdaf5c3 100644 --- a/scada-common/network.lua +++ b/scada-common/network.lua @@ -53,6 +53,11 @@ function network.init_mac(passkey) return init_time end +-- de-initialize message authentication system +function network.deinit_mac() + c_eng.key, c_eng.hmac = nil, nil +end + -- generate HMAC of message ---@nodiscard ---@param message string initial value concatenated with ciphertext diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index df64b68..fb98161 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -9,7 +9,7 @@ local util = require("scada-common.util") local ppm = {} local ACCESS_FAULT = nil ---@type nil -local UNDEFINED_FIELD = "undefined field" +local UNDEFINED_FIELD = "__PPM_UNDEF_FIELD__" local VIRTUAL_DEVICE_TYPE = "ppm_vdev" ppm.ACCESS_FAULT = ACCESS_FAULT @@ -155,7 +155,7 @@ local function peri_init(iface) self.fault_counts[key] = self.fault_counts[key] + 1 - return (function () return ACCESS_FAULT end) + return (function () return UNDEFINED_FIELD end) end } @@ -300,6 +300,17 @@ function ppm.handle_unmount(iface) return pm_type, pm_dev end +-- log all mounts, to be used if `ppm.mount_all` is called before logging is ready +function ppm.log_mounts() + for iface, mount in pairs(ppm_sys.mounts) do + log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")")) + end + + if util.table_len(ppm_sys.mounts) == 0 then + log.warning("PPM: no devices had been found") + end +end + -- GENERAL ACCESSORS -- -- list all available peripherals diff --git a/scada-common/util.lua b/scada-common/util.lua index 10b4202..c72d62e 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -22,7 +22,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.1.14" +util.version = "1.1.18" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 @@ -284,11 +284,13 @@ function util.cancel_timer(timer) os.cancelTimer(timer) end --#region PARALLELIZATION --- protected sleep call so we still are in charge of catching termination ----@param t integer seconds +-- protected sleep call so we still are in charge of catching termination
+-- returns the result of pcall +---@param t number seconds +---@return boolean success, any result, any ... --- EVENT_CONSUMER: this function consumes events ---@diagnostic disable-next-line: undefined-field -function util.psleep(t) pcall(os.sleep, t) end +function util.psleep(t) return pcall(os.sleep, t) end -- no-op to provide a brief pause (1 tick) to yield
--- EVENT_CONSUMER: this function consumes events diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 94f93b3..43c6cc4 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -152,6 +152,8 @@ function facility.new(num_reactors, cooling_conf) table.insert(self.test_tone_states, false) end + -- PRIVATE FUNCTIONS -- + -- check if all auto-controlled units completed ramping ---@nodiscard local function _all_units_ramped() @@ -228,7 +230,7 @@ function facility.new(num_reactors, cooling_conf) ---@class facility local public = {} - -- ADD/LINK DEVICES -- + --#region Add/Link Devices -- link a redstone RTU session ---@param rs_unit unit_session @@ -268,11 +270,9 @@ function facility.new(num_reactors, cooling_conf) for _, v in pairs(self.rtu_list) do util.filter_table(v, function (s) return s.get_session_id() ~= session end) end end - -- UPDATE -- + --#endregion - -- supervisor sessions reporting the list of active RTU sessions - ---@param rtu_sessions table session list of all connected RTUs - function public.report_rtus(rtu_sessions) self.rtu_conn_count = #rtu_sessions end + --#region Update -- update (iterate) the facility management function public.update() @@ -323,7 +323,7 @@ function facility.new(num_reactors, cooling_conf) -- Run Process Control -- ------------------------- - --#region Process Control + --#region local avg_charge = self.avg_charge.compute() local avg_inflow = self.avg_inflow.compute() @@ -597,7 +597,7 @@ function facility.new(num_reactors, cooling_conf) -- Evaluate Automatic SCRAM -- ------------------------------ - --#region Automatic SCRAM + --#region local astatus = self.ascram_status @@ -727,6 +727,8 @@ function facility.new(num_reactors, cooling_conf) -- Handle Redstone I/O -- ------------------------- + --#region + if #self.redstone > 0 then -- handle facility SCRAM if self.io_ctl.digital_read(IO.F_SCRAM) then @@ -756,10 +758,14 @@ function facility.new(num_reactors, cooling_conf) self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm) end + --#endregion + ---------------- -- Unit Tasks -- ---------------- + --#region + local insufficent_po_rate = false local need_emcool = false @@ -798,10 +804,14 @@ function facility.new(num_reactors, cooling_conf) end end + --#endregion + ------------------------ -- Update Alarm Tones -- ------------------------ + --#region + local allow_test = self.allow_testing and self.test_tone_set local alarms = { false, false, false, false, false, false, false, false, false, false, false, false } @@ -888,6 +898,8 @@ function facility.new(num_reactors, cooling_conf) self.test_tone_set = false self.test_tone_reset = true end + + --#endregion end -- call the update function of all units in the facility
@@ -900,7 +912,9 @@ function facility.new(num_reactors, cooling_conf) end end - -- COMMANDS -- + --#endregion + + --#region Commands -- SCRAM all reactor units function public.scram_all() @@ -988,7 +1002,9 @@ function facility.new(num_reactors, cooling_conf) } end - -- SETTINGS -- + --#endregion + + --#region Settings -- set the automatic control group of a unit ---@param unit_id integer unit ID @@ -1029,7 +1045,9 @@ function facility.new(num_reactors, cooling_conf) return self.pu_fallback end - -- DIAGNOSTIC TESTING -- + --#endregion + + --#region Diagnostic Testing -- attempt to set a test tone state ---@param id TONE|0 tone ID or 0 to disable all @@ -1069,7 +1087,9 @@ function facility.new(num_reactors, cooling_conf) return self.allow_testing, self.test_alarm_states end - -- READ STATES/PROPERTIES -- + --#endregion + + --#region Read States/Properties -- get current alarm tone on/off states ---@nodiscard @@ -1183,6 +1203,12 @@ function facility.new(num_reactors, cooling_conf) return status end + --#endregion + + -- supervisor sessions reporting the list of active RTU sessions + ---@param rtu_sessions table session list of all connected RTUs + function public.report_rtus(rtu_sessions) self.rtu_conn_count = #rtu_sessions end + -- get the units in this facility ---@nodiscard function public.get_units() return self.units end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 69633fa..72c0c97 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.2.8" +local SUPERVISOR_VERSION = "v1.2.11" local println = util.println local println_ts = util.println_ts @@ -34,9 +34,13 @@ if not supervisor.load_config() then -- try to reconfigure (user action) local success, error = configure.configure(true) if success then - assert(supervisor.load_config(), "failed to load valid configuration") + if not supervisor.load_config() then + println("failed to load a valid configuration, please reconfigure") + return + end else - assert(success, "supervisor configuration error: " .. error) + println("configuration error: " .. error) + return end end diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 8d38222..afdf6f3 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -77,7 +77,6 @@ function unit.new(reactor_id, num_boilers, num_turbines) tanks = {}, snas = {}, envd = {}, - sna_prod_rate = 0, -- redstone control io_ctl = nil, ---@type rs_controller valves = {}, ---@type unit_valves @@ -256,7 +255,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) -- PRIVATE FUNCTIONS -- - --#region time derivative utility functions + --#region Time Derivative Utility Functions -- compute a change with respect to time of the given value ---@param key string value key @@ -331,7 +330,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) --#endregion - --#region redstone I/O + --#region Redstone I/O -- create a generic valve interface ---@nodiscard @@ -398,8 +397,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) ---@class reactor_unit local public = {} - -- ADD/LINK DEVICES -- - --#region + --#region Add/Link Devices -- link the PLC ---@param plc_session plc_session_struct @@ -489,7 +487,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) --#endregion - -- UPDATE SESSION -- + --#region Update Session -- update (iterate) this unit function public.update() @@ -557,8 +555,9 @@ function unit.new(reactor_id, num_boilers, num_turbines) end end - -- AUTO CONTROL OPERATIONS -- - --#region + --#endregion + + --#region Auto Control Operations -- engage automatic control function public.auto_engage() @@ -645,8 +644,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) --#endregion - -- OPERATIONS -- - --#region + --#region Operations -- queue a command to disable the reactor function public.disable() @@ -726,8 +724,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) --#endregion - -- READ STATES/PROPERTIES -- - --#region + --#region Read States/Properties -- check if an alarm of at least a certain priority level is tripped ---@nodiscard @@ -857,13 +854,15 @@ function unit.new(reactor_id, num_boilers, num_turbines) status.tanks[tank.get_device_idx()] = { tank.is_faulted(), db.formed, db.state, db.tanks } end - -- basic SNA statistical information - local total_peak = 0 + -- SNA statistical information + local total_peak, total_avail, total_out = 0, 0, 0 for i = 1, #self.snas do local db = self.snas[i].get_db() ---@type sna_session_db total_peak = total_peak + db.state.peak_production + total_avail = total_avail + db.state.production_rate + total_out = total_out + math.min(db.tanks.input.amount / 10, db.state.production_rate) end - status.sna = { #self.snas, public.get_sna_rate(), total_peak } + status.sna = { #self.snas, total_peak, total_avail, total_out } -- radiation monitors (environment detectors) status.envds = {} @@ -876,7 +875,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) return status end - -- get the current total [max] production rate is + -- get the current total max production rate ---@nodiscard ---@return number total_avail_rate function public.get_sna_rate() diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua index e207c44..f43ff03 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unitlogic.lua @@ -54,9 +54,7 @@ function logic.update_annunciator(self) -- variables for boiler, or reactor if no boilers used local total_boil_rate = 0.0 - ------------- - -- REACTOR -- - ------------- + --#region Reactor annunc.AutoControl = self.auto_engaged @@ -143,9 +141,9 @@ function logic.update_annunciator(self) self.plc_cache.ok = false end - --------------- - -- MISC RTUs -- - --------------- + --#endregion + + --#region Misc RTUs local max_rad, any_faulted = 0, false @@ -170,9 +168,9 @@ function logic.update_annunciator(self) end end - ------------- - -- BOILERS -- - ------------- + --#endregion + + --#region Boilers local boilers_ready = num_boilers == #self.boilers @@ -230,9 +228,9 @@ function logic.update_annunciator(self) boiler_water_dt_sum = _get_dt(DT_KEYS.ReactorCCool) end - --------------------------- - -- COOLANT FEED MISMATCH -- - --------------------------- + --#endregion + + --#region Coolant Feed Mismatch -- check coolant feed mismatch if using boilers, otherwise calculate with reactor local cfmismatch = false @@ -263,9 +261,9 @@ function logic.update_annunciator(self) annunc.CoolantFeedMismatch = cfmismatch - -------------- - -- TURBINES -- - -------------- + --#endregion + + --#region Turbines local turbines_ready = num_turbines == #self.turbines @@ -340,6 +338,8 @@ function logic.update_annunciator(self) annunc.TurbineTrip[idx] = has_steam and db.state.flow_rate == 0 end + --#endregion + -- update auto control ready state for this unit self.db.control.ready = plc_ready and boilers_ready and turbines_ready end diff --git a/test/watch_psil_allocs.lua b/test/watch_psil_allocs.lua new file mode 100644 index 0000000..a45685e --- /dev/null +++ b/test/watch_psil_allocs.lua @@ -0,0 +1,56 @@ +-- add this to psil: + +--[[ + -- count the number of subscribers in this PSIL instance + ---@return integer count + function public.count() + local c = 0 + for _, val in pairs(ic) do + for _ = 1, #val.subscribers do c = c + 1 end + end + return c + end +]]-- + + +-- add this to coordinator iocontrol front panel heartbeat function: + +--[[ +if io.facility then + local count = io.facility.ps.count() + + count = count + io.facility.env_d_ps.count() + + for x = 1, #io.facility.induction_ps_tbl do + count = count + io.facility.induction_ps_tbl[x].count() + end + + for x = 1, #io.facility.sps_ps_tbl do + count = count + io.facility.sps_ps_tbl[x].count() + end + + for x = 1, #io.facility.tank_ps_tbl do + count = count + io.facility.tank_ps_tbl[x].count() + end + + for i = 1, #io.units do + local entry = io.units[i] ---@type ioctl_unit + + count = count + entry.unit_ps.count() + + for x = 1, #entry.boiler_ps_tbl do + count = count + entry.boiler_ps_tbl[x].count() + end + + for x = 1, #entry.turbine_ps_tbl do + count = count + entry.turbine_ps_tbl[x].count() + end + + for x = 1, #entry.tank_ps_tbl do + count = count + entry.tank_ps_tbl[x].count() + end + end + + log.debug(count) +end +]]--