diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index 78748c8..71da387 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -387,10 +387,14 @@ function coordinator.comms(version, nic, sv_watchdog) end -- send the auto process control configuration with a start command - ---@param auto_cfg sys_auto_config configuration - function public.send_auto_start(auto_cfg) + ---@param mode PROCESS process control mode + ---@param burn_target number burn rate target + ---@param charge_target number charge level target + ---@param gen_target number generation rate target + ---@param limits number[] unit burn rate limits + function public.send_auto_start(mode, burn_target, charge_target, gen_target, limits) _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { - FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits + FAC_COMMAND.START, mode, burn_target, charge_target, gen_target, limits }) end @@ -578,7 +582,7 @@ function coordinator.comms(version, nic, sv_watchdog) if cmd == FAC_COMMAND.SCRAM_ALL then process.fac_ack(cmd, ack) elseif cmd == FAC_COMMAND.STOP then - iocontrol.get_db().facility.stop_ack(ack) + process.fac_ack(cmd, ack) elseif cmd == FAC_COMMAND.START then if packet.length == 7 then process.start_ack_handle({ table.unpack(packet.data, 2) }) diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index aa436a6..8d57bed 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -84,6 +84,8 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) all_sys_ok = false, rtu_count = 0, + status_lines = { "", "" }, + auto_ready = false, auto_active = false, auto_ramping = false, @@ -107,8 +109,6 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) radiation = types.new_zero_radiation_reading(), save_cfg_ack = nil, ---@type fun(success: boolean) - start_ack = nil, ---@type fun(success: boolean) - stop_ack = nil, ---@type fun(success: boolean) ---@type { [TONE]: boolean } alarm_tones = { false, false, false, false, false, false, false, false }, @@ -159,6 +159,11 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) num_snas = 0, has_tank = conf.cooling.r_cool[i].TankConnection, + status_lines = { "", "" }, + + auto_ready = false, + auto_degraded = false, + control_state = false, burn_rate_cmd = 0.0, radiation = types.new_zero_radiation_reading(), @@ -541,8 +546,8 @@ function iocontrol.update_facility_status(status) fac.ascram_status.radiation = ctl_status[10] fac.ascram_status.gen_fault = ctl_status[11] - fac.status_line_1 = ctl_status[12] - fac.status_line_2 = ctl_status[13] + fac.status_lines[1] = ctl_status[12] + fac.status_lines[2] = ctl_status[13] fac.ps.publish("all_sys_ok", fac.all_sys_ok) fac.ps.publish("auto_ready", fac.auto_ready) @@ -555,8 +560,8 @@ function iocontrol.update_facility_status(status) fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm) fac.ps.publish("as_radiation", fac.ascram_status.radiation) fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault) - fac.ps.publish("status_line_1", fac.status_line_1) - fac.ps.publish("status_line_2", fac.status_line_2) + fac.ps.publish("status_line_1", fac.status_lines[1]) + fac.ps.publish("status_line_2", fac.status_lines[2]) local group_map = ctl_status[14] @@ -1130,15 +1135,19 @@ function iocontrol.update_unit_statuses(statuses) if type(unit_state) == "table" then if #unit_state == 8 then + unit.status_lines[1] = unit_state[1] + unit.status_lines[2] = unit_state[2] + unit.auto_ready = unit_state[3] + unit.auto_degraded = unit_state[4] 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]) - unit.unit_ps.publish("U_AutoReady", unit_state[3]) - unit.unit_ps.publish("U_AutoDegraded", unit_state[4]) + unit.unit_ps.publish("U_StatusLine1", unit.status_lines[1]) + unit.unit_ps.publish("U_StatusLine2", unit.status_lines[2]) + unit.unit_ps.publish("U_AutoReady", unit.auto_ready) + unit.unit_ps.publish("U_AutoDegraded", unit.auto_degraded) unit.unit_ps.publish("U_AutoWaste", unit.waste_mode == types.WASTE_MODE.AUTO) unit.unit_ps.publish("U_WasteMode", unit.waste_mode) unit.unit_ps.publish("U_WasteProduct", unit.waste_product) diff --git a/coordinator/process.lua b/coordinator/process.lua index f96b3e7..78be172 100644 --- a/coordinator/process.lua +++ b/coordinator/process.lua @@ -25,12 +25,12 @@ local pctl = { control_states = { ---@class sys_auto_config process = { - mode = PROCESS.INACTIVE, + mode = PROCESS.INACTIVE, ---@type PROCESS burn_target = 0.0, charge_target = 0.0, gen_target = 0.0, - limits = {}, ---@type number[] - waste_product = PRODUCT.PLUTONIUM, + limits = {}, ---@type number[] + waste_product = PRODUCT.PLUTONIUM, ---@type WASTE_PRODUCT pu_fallback = false, sps_low_power = false }, @@ -49,6 +49,7 @@ local pctl = { ---@field requestors function[] list of callbacks from the requestors -- write auto process control to config file +---@return boolean saved local function _write_auto_config() -- save config settings.set("ControlStates", pctl.control_states) @@ -60,6 +61,8 @@ local function _write_auto_config() return saved end +--#region Core + -- initialize the process controller ---@param iocontrol ioctl iocontrl system ---@param coord_comms coord_comms coordinator communications @@ -180,6 +183,36 @@ function process.create_handle() end end + -- start automatic process control with current settings + function handle.process_start() + if f_request(F_CMD.START, handle.fac_ack.on_start) then + local p = pctl.control_states.process + pctl.comms.send_auto_start(p.mode, p.burn_target, p.charge_target, p.gen_target, p.limits) + log.debug("PROCESS: START AUTO CTRL") + end + end + + -- start automatic process control with remote settings that haven't been set on the coordinator + ---@param mode PROCESS process control mode + ---@param burn_target number burn rate target + ---@param charge_target number charge level target + ---@param gen_target number generation rate target + ---@param limits number[] unit burn rate limits + function handle.process_start_remote(mode, burn_target, charge_target, gen_target, limits) + if f_request(F_CMD.START, handle.fac_ack.on_start) then + pctl.comms.send_auto_start(mode, burn_target, charge_target, gen_target, limits) + log.debug("PROCESS: START AUTO CTRL") + end + end + + -- stop process control + function handle.process_stop() + if f_request(F_CMD.STOP, handle.fac_ack.on_stop) then + pctl.comms.send_fac_command(F_CMD.STOP) + log.debug("PROCESS: STOP AUTO CTRL") + end + end + handle.fac_ack = {} -- luacheck: no unused args @@ -194,6 +227,16 @@ function process.create_handle() ---@diagnostic disable-next-line: unused-local function handle.fac_ack.on_ack_alarms(success) end + -- facility auto control start ack, override to implement + ---@param success boolean + ---@diagnostic disable-next-line: unused-local + function handle.fac_ack.on_start(success) end + + -- facility auto control stop ack, override to implement + ---@param success boolean + ---@diagnostic disable-next-line: unused-local + function handle.fac_ack.on_stop(success) end + -- luacheck: unused args --#endregion @@ -294,6 +337,14 @@ function process.clear_timed_out() end end +-- get the control states table +---@nodiscard +function process.get_control_states() return pctl.control_states end + +--#endregion + +--#region Command Handling + -- handle a command acknowledgement ---@param cmd_state process_command_state ---@param success boolean if the command was successful @@ -335,6 +386,21 @@ function process.set_rate(id, rate) log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate)) end +-- assign a unit to a group +---@param unit_id integer unit ID +---@param group_id integer|0 group ID or 0 for independent +function process.set_group(unit_id, group_id) + pctl.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id) + log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id)) + + pctl.control_states.priority_groups[unit_id] = group_id + settings.set("ControlStates", pctl.control_states) + + if not settings.save("/coordinator.settings") then + log.error("process.set_group(): failed to save coordinator settings file") + end +end + -- set waste mode ---@param id integer unit ID ---@param mode integer waste mode @@ -369,39 +435,12 @@ function process.reset_alarm(id, alarm) log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm)) end --- assign a unit to a group ----@param unit_id integer unit ID ----@param group_id integer|0 group ID or 0 for independent -function process.set_group(unit_id, group_id) - pctl.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id) - log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id)) - - pctl.control_states.priority_groups[unit_id] = group_id - settings.set("ControlStates", pctl.control_states) - - if not settings.save("/coordinator.settings") then - log.error("process.set_group(): failed to save coordinator settings file") - end -end - --#endregion -------------------------- -- AUTO PROCESS CONTROL -- -------------------------- --- start automatic process control -function process.start_auto() - pctl.comms.send_auto_start(pctl.control_states.process) - log.debug("PROCESS: START AUTO CTL") -end - --- stop automatic process control -function process.stop_auto() - pctl.comms.send_fac_command(F_CMD.STOP) - log.debug("PROCESS: STOP AUTO CTL") -end - -- set automatic process control waste mode ---@param product WASTE_PRODUCT waste product for auto control function process.set_process_waste(product) @@ -439,9 +478,9 @@ function process.set_sps_low_power(enabled) end -- save process control settings ----@param mode PROCESS control mode +---@param mode PROCESS process control mode ---@param burn_target number burn rate target ----@param charge_target number charge target +---@param charge_target number charge level target ---@param gen_target number generation rate target ---@param limits number[] unit burn rate limits function process.save(mode, burn_target, charge_target, gen_target, limits) @@ -472,9 +511,7 @@ function process.start_ack_handle(response) for i = 1, math.min(#response[6], pctl.io.facility.num_units) do ctl_proc.limits[i] = response[6][i] - - local unit = pctl.io.units[i] - unit.unit_ps.publish("burn_limit", ctl_proc.limits[i]) + pctl.io.units[i].unit_ps.publish("burn_limit", ctl_proc.limits[i]) end pctl.io.facility.ps.publish("process_mode", ctl_proc.mode) @@ -482,7 +519,9 @@ function process.start_ack_handle(response) pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target)) pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target)) - pctl.io.facility.start_ack(ack) + _write_auto_config() + + process.fac_ack(F_CMD.START, ack) end -- record waste product settting after attempting to change it @@ -506,4 +545,6 @@ function process.sps_lp_ack_handle(response) pctl.io.facility.ps.publish("process_sps_low_power", response) end +--#endregion + return process diff --git a/coordinator/session/pocket.lua b/coordinator/session/pocket.lua index 25dd2da..cb38a61 100644 --- a/coordinator/session/pocket.lua +++ b/coordinator/session/pocket.lua @@ -108,14 +108,20 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout) -- link callback transmissions - self.proc_handle.fac_ack.on_scram = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.SCRAM_ALL, success }) end - self.proc_handle.fac_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.ACK_ALL_ALARMS, success }) end + local f_ack = self.proc_handle.fac_ack + + f_ack.on_scram = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.SCRAM_ALL, success }) end + f_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.ACK_ALL_ALARMS, success }) end + + f_ack.on_start = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, success }) end + f_ack.on_stop = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.STOP, success }) end for u = 1, iocontrol.get_db().facility.num_units do - self.proc_handle.unit_ack[u].on_start = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.START, u, success }) end - self.proc_handle.unit_ack[u].on_scram = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.SCRAM, u, success }) end - self.proc_handle.unit_ack[u].on_rps_reset = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.RESET_RPS, u, success }) end - self.proc_handle.unit_ack[u].on_ack_alarms = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.ACK_ALL_ALARMS, u, success }) end + local u_ack = self.proc_handle.unit_ack[u] + u_ack.on_start = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.START, u, success }) end + u_ack.on_scram = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.SCRAM, u, success }) end + u_ack.on_rps_reset = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.RESET_RPS, u, success }) end + u_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.ACK_ALL_ALARMS, u, success }) end end -- handle a packet @@ -147,7 +153,15 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout) log.info(log_tag .. "FAC SCRAM ALL") self.proc_handle.fac_scram() elseif cmd == FAC_COMMAND.STOP then + log.info(log_tag .. "STOP PROCESS CTRL") + self.proc_handle.process_stop() elseif cmd == FAC_COMMAND.START then + if pkt.length == 6 then + log.info(log_tag .. "START PROCESS CTRL") + self.proc_handle.process_start_remote(pkt.data[2], pkt.data[3], pkt.data[4], pkt.data[5], pkt.data[6]) + else + log.debug(log_tag .. "CRDN auto start (with configuration) packet length mismatch") + end elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then log.info(log_tag .. "FAC ACK ALL ALARMS") self.proc_handle.fac_ack_alarms() @@ -191,6 +205,12 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout) elseif cmd == UNIT_COMMAND.ACK_ALARM then elseif cmd == UNIT_COMMAND.RESET_ALARM then elseif cmd == UNIT_COMMAND.SET_GROUP then + if pkt.length == 3 then + log.info(util.c(log_tag, "UNIT[", uid, "] SET GROUP ", pkt.data[3])) + process.set_group(uid, pkt.data[3]) + else + log.debug(log_tag .. "CRDN unit set group missing option") + end else log.debug(log_tag .. "CRDN unit command unknown") end @@ -259,6 +279,37 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout) end _send(CRDN_TYPE.API_GET_CTRL, data) + elseif pkt.type == CRDN_TYPE.API_GET_PROC then + local data = {} + + local fac = db.facility + local proc = process.get_control_states().process + + -- unit data + for i = 1, #db.units do + local u = db.units[i] + + data[i] = { + u.reactor_data.mek_status.status, + u.reactor_data.mek_struct.max_burn, + proc.limits[i], + u.auto_ready, + u.auto_degraded, + u.annunciator.AutoControl, + u.a_group + } + end + + -- facility data + data[#db.units + 1] = { + fac.status_lines, + { fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated }, + fac.auto_scram, + fac.ascram_status, + { proc.mode, proc.burn_target, proc.charge_target, proc.gen_target } + } + + _send(CRDN_TYPE.API_GET_PROC, data) else log.debug(log_tag .. "handler received unsupported CRDN packet type " .. pkt.type) end diff --git a/coordinator/ui/components/process_ctl.lua b/coordinator/ui/components/process_ctl.lua index 44c9ceb..a70bcd6 100644 --- a/coordinator/ui/components/process_ctl.lua +++ b/coordinator/ui/components/process_ctl.lua @@ -264,24 +264,22 @@ local function new_view(root, x, y) local limits = {} for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end - process.save(mode.get_value(), b_target.get_value(), - db.energy_convert_to_fe(c_target.get_value()), - db.energy_convert_to_fe(g_target.get_value()), - limits) + process.save(mode.get_value(), b_target.get_value(), db.energy_convert_to_fe(c_target.get_value()), + db.energy_convert_to_fe(g_target.get_value()), limits) end -- start automatic control after saving process control settings local function _start_auto() _save_cfg() - process.start_auto() + db.process.process_start() end local save = HazardButton{parent=auto_controls,x=2,y=2,text="SAVE",accent=colors.purple,dis_colors=dis_colors,callback=_save_cfg,fg_bg=hzd_fg_bg} local start = HazardButton{parent=auto_controls,x=13,y=2,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=_start_auto,fg_bg=hzd_fg_bg} - local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.red,dis_colors=dis_colors,callback=process.stop_auto,fg_bg=hzd_fg_bg} + local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.red,dis_colors=dis_colors,callback=db.process.process_stop,fg_bg=hzd_fg_bg} - facility.start_ack = start.on_response - facility.stop_ack = stop.on_response + db.process.fac_ack.on_start = start.on_response + db.process.fac_ack.on_stop = stop.on_response function facility.save_cfg_ack(ack) tcd.dispatch(0.2, function () save.on_response(ack) end) diff --git a/graphics/core.lua b/graphics/core.lua index cbedccf..5439603 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -7,7 +7,7 @@ local flasher = require("graphics.flasher") local core = {} -core.version = "2.4.3" +core.version = "2.4.5" core.flasher = flasher core.events = events diff --git a/graphics/elements/controls/RadioButton.lua b/graphics/elements/controls/RadioButton.lua index a2acffc..5c94316 100644 --- a/graphics/elements/controls/RadioButton.lua +++ b/graphics/elements/controls/RadioButton.lua @@ -11,6 +11,7 @@ local KEY_CLICK = core.events.KEY_CLICK ---@field options table button options ---@field radio_colors cpair radio button colors (inner & outer) ---@field select_color color color for radio button border when selected +---@field dis_fg_bg? cpair foreground/background colors when disabled ---@field default? integer default state, defaults to options[1] ---@field min_width? integer text length + 2 if omitted ---@field callback? function function to call on touch @@ -64,6 +65,10 @@ return function (args) local inner_color = util.trinary(e.value == i, args.radio_colors.color_b, args.radio_colors.color_a) local outer_color = util.trinary(e.value == i, args.select_color, args.radio_colors.color_b) + if e.value == i and args.dis_fg_bg and not e.enabled then + outer_color = args.radio_colors.color_a + end + e.w_set_cur(1, i) e.w_set_fgd(inner_color) @@ -75,9 +80,14 @@ return function (args) e.w_write("\x95") -- write button text - if i == focused_opt and e.is_focused() and e.enabled then - e.w_set_fgd(e.fg_bg.bkg) - e.w_set_bkg(e.fg_bg.fgd) + if args.dis_fg_bg and not e.enabled then + e.w_set_fgd(args.dis_fg_bg.fgd) + e.w_set_bkg(args.dis_fg_bg.bkg) + elseif i == focused_opt and e.is_focused() then + if e.enabled then + e.w_set_fgd(e.fg_bg.bkg) + e.w_set_bkg(e.fg_bg.fgd) + end else e.w_set_fgd(e.fg_bg.fgd) e.w_set_bkg(e.fg_bg.bkg) diff --git a/graphics/elements/form/NumberField.lua b/graphics/elements/form/NumberField.lua index 2653069..d582f5b 100644 --- a/graphics/elements/form/NumberField.lua +++ b/graphics/elements/form/NumberField.lua @@ -44,8 +44,47 @@ return function (args) args.max_chars = args.max_chars or e.frame.w + -- determine the format to convert the number to a string + local format = "%d" + if args.allow_decimal then + if args.max_frac_digits then + format = "%."..args.max_frac_digits.."f" + else format = "%f" end + end + + -- set the value to a formatted numeric string
+ -- trims trailing zeros from floating point numbers + ---@param num number + local function _set_value(num) + local str = util.sprintf(format, num) + + if args.allow_decimal then + local found_nonzero = false + local str_table = {} + + for i = #str, 1, -1 do + local c = string.sub(str, i, i) + + if found_nonzero then + str_table[i] = c + else + if c == "." then + found_nonzero = true + elseif c ~= "0" then + str_table[i] = c + found_nonzero = true + end + end + end + + e.value = table.concat(str_table) + else + e.value = str + end + end + -- set initial value - e.value = "" .. (args.default or 0) + _set_value(args.default or 0) -- make an interactive field manager local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg, args.align_right) @@ -107,7 +146,17 @@ return function (args) -- set the value (must be a number) ---@param val number number to show function e.set_value(val) - if tonumber(val) then ifield.set_value("" .. tonumber(val)) end + local num, max, min = tonumber(val), tonumber(args.max), tonumber(args.min) + + if max and num > max then + _set_value(max) + elseif min and num < min then + _set_value(min) + elseif num then + _set_value(num) + end + + ifield.set_value(e.value) end -- set minimum input value @@ -136,11 +185,9 @@ return function (args) -- handle unfocused function e.on_unfocused() - local val = tonumber(e.value) - local max = tonumber(args.max) - local min = tonumber(args.min) + local val, max, min = tonumber(e.value), tonumber(args.max), tonumber(args.min) - if type(val) == "number" then + if val then if args.max_int_digits or args.max_frac_digits then local str = e.value local ceil = false @@ -169,17 +216,17 @@ return function (args) if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end - val = tonumber((parts[1] or "") .. parts[2]) + val = tonumber((parts[1] or "") .. parts[2]) or 0 end - if type(args.max) == "number" and val > max then - e.value = "" .. max + if max and val > max then + _set_value(max) ifield.nav_start() - elseif type(args.min) == "number" and val < min then - e.value = "" .. min + elseif min and val < min then + _set_value(min) ifield.nav_start() else - e.value = "" .. val + _set_value(val) ifield.nav_end() end else @@ -198,5 +245,11 @@ return function (args) ---@class NumberField:graphics_element local NumberField, id = e.complete(true) + -- get the numeric value of this field + ---@return number value the value, or 0 if not a valid number + function NumberField.get_numeric() + return tonumber(e.value) or 0 + end + return NumberField, id end diff --git a/pocket/iocontrol.lua b/pocket/iocontrol.lua index 36b5fd6..e72a573 100644 --- a/pocket/iocontrol.lua +++ b/pocket/iocontrol.lua @@ -6,7 +6,6 @@ local const = require("scada-common.constants") local psil = require("scada-common.psil") local types = require("scada-common.types") local util = require("scada-common.util") -local log = require("scada-common.log") local process = require("pocket.process") @@ -94,7 +93,8 @@ function iocontrol.init_core(pkt_comms, nav, cfg) ---@class pocket_ioctl_api io.api = { get_unit = function (unit) comms.api__get_unit(unit) end, - get_ctrl = function () comms.api__get_control() end + get_ctrl = function () comms.api__get_control() end, + get_proc = function () comms.api__get_process() end } end @@ -138,6 +138,8 @@ function iocontrol.init_fac(conf) all_sys_ok = false, rtu_count = 0, + status_lines = { "", "" }, + auto_ready = false, auto_active = false, auto_ramping = false, @@ -197,6 +199,11 @@ function iocontrol.init_fac(conf) num_snas = 0, has_tank = conf.cooling.r_cool[i].TankConnection, + status_lines = { "", "" }, + + auto_ready = false, + auto_degraded = false, + control_state = false, burn_rate_cmd = 0.0, radiation = types.new_zero_radiation_reading(), @@ -817,48 +824,102 @@ function iocontrol.record_control_data(data) local unit = io.units[u_id] local u_data = data[u_id] - if type(u_data) ~= "table" then - log.debug(util.c("iocontrol.record_control_data: unit ", u_id, " data invalid")) - else - unit.connected = u_data[1] + unit.connected = u_data[1] - unit.reactor_data.rps_tripped = u_data[2] - unit.unit_ps.publish("rps_tripped", u_data[2]) - unit.reactor_data.mek_status.status = u_data[3] - unit.unit_ps.publish("status", u_data[3]) - unit.reactor_data.mek_status.temp = u_data[4] - unit.unit_ps.publish("temp", u_data[4]) - unit.reactor_data.mek_status.burn_rate = u_data[5] - unit.unit_ps.publish("burn_rate", u_data[5]) - unit.reactor_data.mek_status.act_burn_rate = u_data[6] - unit.unit_ps.publish("act_burn_rate", u_data[6]) - unit.reactor_data.mek_struct.max_burn = u_data[7] - unit.unit_ps.publish("max_burn", u_data[7]) + unit.reactor_data.rps_tripped = u_data[2] + unit.unit_ps.publish("rps_tripped", u_data[2]) + unit.reactor_data.mek_status.status = u_data[3] + unit.unit_ps.publish("status", u_data[3]) + unit.reactor_data.mek_status.temp = u_data[4] + unit.unit_ps.publish("temp", u_data[4]) + unit.reactor_data.mek_status.burn_rate = u_data[5] + unit.unit_ps.publish("burn_rate", u_data[5]) + unit.reactor_data.mek_status.act_burn_rate = u_data[6] + unit.unit_ps.publish("act_burn_rate", u_data[6]) + unit.reactor_data.mek_struct.max_burn = u_data[7] + unit.unit_ps.publish("max_burn", u_data[7]) - unit.annunciator.AutoControl = u_data[8] - unit.unit_ps.publish("AutoControl", u_data[8]) + unit.annunciator.AutoControl = u_data[8] + unit.unit_ps.publish("AutoControl", u_data[8]) - unit.a_group = u_data[9] - unit.unit_ps.publish("auto_group_id", unit.a_group) - unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1]) + unit.a_group = u_data[9] + unit.unit_ps.publish("auto_group_id", unit.a_group) + unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1]) - local control_status = 1 + local control_status = 1 - if unit.connected then - if unit.reactor_data.rps_tripped then - control_status = 2 - end - - if unit.reactor_data.mek_status.status then - control_status = util.trinary(unit.annunciator.AutoControl, 4, 3) - end + if unit.connected then + if unit.reactor_data.rps_tripped then + control_status = 2 end - unit.unit_ps.publish("U_ControlStatus", control_status) + if unit.reactor_data.mek_status.status then + control_status = util.trinary(unit.annunciator.AutoControl, 4, 3) + end end + + unit.unit_ps.publish("U_ControlStatus", control_status) end end +-- update process app with unit data from API_GET_PROC +---@param data table +function iocontrol.record_process_data(data) + -- get unit data + for u_id = 1, #io.units do + local unit = io.units[u_id] + local u_data = data[u_id] + + unit.reactor_data.mek_status.status = u_data[1] + unit.reactor_data.mek_struct.max_burn = u_data[2] + unit.annunciator.AutoControl = u_data[6] + unit.a_group = u_data[7] + + unit.unit_ps.publish("status", u_data[1]) + unit.unit_ps.publish("max_burn", u_data[2]) + unit.unit_ps.publish("burn_limit", u_data[3]) + unit.unit_ps.publish("U_AutoReady", u_data[4]) + unit.unit_ps.publish("U_AutoDegraded", u_data[5]) + unit.unit_ps.publish("AutoControl", u_data[6]) + unit.unit_ps.publish("auto_group_id", unit.a_group) + unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1]) + end + + -- get facility data + local fac = io.facility + local f_data = data[#io.units + 1] + + fac.status_lines = f_data[1] + + fac.auto_ready = f_data[2][1] + fac.auto_active = f_data[2][2] + fac.auto_ramping = f_data[2][3] + fac.auto_saturated = f_data[2][4] + + fac.auto_scram = f_data[3] + fac.ascram_status = f_data[4] + + fac.ps.publish("status_line_1", fac.status_lines[1]) + fac.ps.publish("status_line_2", fac.status_lines[2]) + + fac.ps.publish("auto_ready", fac.auto_ready) + fac.ps.publish("auto_active", fac.auto_active) + fac.ps.publish("auto_ramping", fac.auto_ramping) + fac.ps.publish("auto_saturated", fac.auto_saturated) + + fac.ps.publish("auto_scram", fac.auto_scram) + fac.ps.publish("as_matrix_dc", fac.ascram_status.matrix_dc) + fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill) + fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm) + fac.ps.publish("as_radiation", fac.ascram_status.radiation) + fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault) + + fac.ps.publish("process_mode", f_data[5][1]) + fac.ps.publish("process_burn_target", f_data[5][2]) + fac.ps.publish("process_charge_target", f_data[5][3]) + fac.ps.publish("process_gen_target", f_data[5][4]) +end + -- get the IO controller database function iocontrol.get_db() return io end diff --git a/pocket/pocket.lua b/pocket/pocket.lua index a625907..de6e6b2 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -82,18 +82,20 @@ end ---@enum POCKET_APP_ID local APP_ID = { + -- core UI ROOT = 1, LOADER = 2, -- main app pages UNITS = 3, CONTROL = 4, - GUIDE = 5, - ABOUT = 6, - -- diag app page - ALARMS = 7, + PROCESS = 5, + GUIDE = 6, + ABOUT = 7, + -- diagnostic app pages + ALARMS = 8, -- other - DUMMY = 8, - NUM_APPS = 8 + DUMMY = 9, + NUM_APPS = 9 } pocket.APP_ID = APP_ID @@ -167,9 +169,9 @@ function pocket.init_nav(smem) -- configure the sidebar ---@param items sidebar_entry[] function app.set_sidebar(items) + app.sidebar_items = items -- only modify the sidebar if this app is still open if self.cur_app == app_id then - app.sidebar_items = items if self.sidebar then self.sidebar.update(items) end end end @@ -178,8 +180,8 @@ function pocket.init_nav(smem) ---@param on_load function callback function app.set_load(on_load) app.load = function () + app.loaded = true -- must flag first so it can't be repeatedly attempted on_load() - app.loaded = true end end @@ -187,8 +189,8 @@ function pocket.init_nav(smem) ---@param on_unload function callback function app.set_unload(on_unload) app.unload = function () - on_unload() app.loaded = false + on_unload() end end @@ -288,6 +290,9 @@ function pocket.init_nav(smem) end end + -- go home (open the home screen app) + function nav.go_home() nav.open_app(APP_ID.ROOT) end + -- open the app that was blocked on connecting function nav.on_loader_connected() if self.loader_return then @@ -555,6 +560,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) if self.api.linked then _send_api(CRDN_TYPE.API_GET_CTRL, {}) end end + -- coordinator get process app data + function public.api__get_process() + if self.api.linked then _send_api(CRDN_TYPE.API_GET_PROC, {}) end + end + -- send a facility command ---@param cmd FAC_COMMAND command ---@param option any? optional option options for the optional options (like waste mode) @@ -562,6 +572,12 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) _send_api(CRDN_TYPE.FAC_CMD, { cmd, option }) end + -- send the auto process control configuration with a start command + ---@param auto_cfg [ PROCESS, number, number, number, number[] ] + function public.send_auto_start(auto_cfg) + _send_api(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, table.unpack(auto_cfg) }) + end + -- send a unit command ---@param cmd UNIT_COMMAND command ---@param unit integer unit ID @@ -664,7 +680,9 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) if cmd == FAC_COMMAND.SCRAM_ALL then iocontrol.get_db().facility.scram_ack(ack) elseif cmd == FAC_COMMAND.STOP then + iocontrol.get_db().facility.stop_ack(ack) elseif cmd == FAC_COMMAND.START then + iocontrol.get_db().facility.start_ack(ack) elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then iocontrol.get_db().facility.ack_alarms_ack(ack) elseif cmd == FAC_COMMAND.SET_WASTE_MODE then @@ -711,6 +729,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) if _check_length(packet, #iocontrol.get_db().units) then iocontrol.record_control_data(packet.data) end + elseif packet.type == CRDN_TYPE.API_GET_PROC then + if _check_length(packet, #iocontrol.get_db().units + 1) then + iocontrol.record_process_data(packet.data) + end else _fail_type(packet) end else log.debug("discarding coordinator SCADA_CRDN packet before linked") diff --git a/pocket/process.lua b/pocket/process.lua index 454caff..d0a3241 100644 --- a/pocket/process.lua +++ b/pocket/process.lua @@ -6,8 +6,8 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local util = require("scada-common.util") -local FAC_COMMAND = comms.FAC_COMMAND -local UNIT_COMMAND = comms.UNIT_COMMAND +local F_CMD = comms.FAC_COMMAND +local U_CMD = comms.UNIT_COMMAND ---@class pocket_process_controller local process = {} @@ -25,23 +25,32 @@ function process.init(iocontrol, pocket_comms) self.comms = pocket_comms end +------------------------------ +--#region FACILITY COMMANDS -- + -- facility SCRAM command function process.fac_scram() - self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL) + self.comms.send_fac_command(F_CMD.SCRAM_ALL) log.debug("PROCESS: FAC SCRAM ALL") end -- facility alarm acknowledge command function process.fac_ack_alarms() - self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS) + self.comms.send_fac_command(F_CMD.ACK_ALL_ALARMS) log.debug("PROCESS: FAC ACK ALL ALARMS") end +--#endregion +------------------------------ + +-------------------------- +--#region UNIT COMMANDS -- + -- start reactor ---@param id integer unit ID function process.start(id) self.io.units[id].control_state = true - self.comms.send_unit_command(UNIT_COMMAND.START, id) + self.comms.send_unit_command(U_CMD.START, id) log.debug(util.c("PROCESS: UNIT[", id, "] START")) end @@ -49,14 +58,14 @@ end ---@param id integer unit ID function process.scram(id) self.io.units[id].control_state = false - self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id) + self.comms.send_unit_command(U_CMD.SCRAM, id) log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM")) end -- reset reactor protection system ---@param id integer unit ID function process.reset_rps(id) - self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id) + self.comms.send_unit_command(U_CMD.RESET_RPS, id) log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS")) end @@ -64,14 +73,22 @@ end ---@param id integer unit ID ---@param rate number burn rate function process.set_rate(id, rate) - self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate) + self.comms.send_unit_command(U_CMD.SET_BURN, id, rate) log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate)) end +-- assign a unit to a group +---@param unit_id integer unit ID +---@param group_id integer|0 group ID or 0 for independent +function process.set_group(unit_id, group_id) + self.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id) + log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id)) +end + -- acknowledge all alarms ---@param id integer unit ID function process.ack_all_alarms(id) - self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id) + self.comms.send_unit_command(U_CMD.ACK_ALL_ALARMS, id) log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS")) end @@ -79,7 +96,7 @@ end ---@param id integer unit ID ---@param alarm integer alarm ID function process.ack_alarm(id, alarm) - self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm) + self.comms.send_unit_command(U_CMD.ACK_ALARM, id, alarm) log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm)) end @@ -87,8 +104,34 @@ end ---@param id integer unit ID ---@param alarm integer alarm ID function process.reset_alarm(id, alarm) - self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm) + self.comms.send_unit_command(U_CMD.RESET_ALARM, id, alarm) log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm)) end +-- #endregion +-------------------------- + +--------------------------------- +--#region AUTO PROCESS CONTROL -- + +-- process start command +---@param mode PROCESS process control mode +---@param burn_target number burn rate target +---@param charge_target number charge level target +---@param gen_target number generation rate target +---@param limits number[] unit burn rate limits +function process.process_start(mode, burn_target, charge_target, gen_target, limits) + self.comms.send_auto_start({ mode, burn_target, charge_target, gen_target, limits }) + log.debug("PROCESS: START AUTO CTRL") +end + +-- process stop command +function process.process_stop() + self.comms.send_fac_command(F_CMD.STOP) + log.debug("PROCESS: STOP AUTO CTRL") +end + +-- #endregion +--------------------------------- + return process diff --git a/pocket/startup.lua b/pocket/startup.lua index 074da8b..0f6fe40 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -20,7 +20,7 @@ local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") local threads = require("pocket.threads") -local POCKET_VERSION = "v0.12.5-alpha" +local POCKET_VERSION = "v0.12.7-alpha" local println = util.println local println_ts = util.println_ts diff --git a/pocket/ui/apps/control.lua b/pocket/ui/apps/control.lua index a74866b..57cabc8 100644 --- a/pocket/ui/apps/control.lua +++ b/pocket/ui/apps/control.lua @@ -34,16 +34,21 @@ local cpair = core.cpair local APP_ID = pocket.APP_ID -local lu_col = style.label_unit_pair -local text_fg = style.text_fg -local mode_states = style.icon_states.mode_states +local label_fg_bg = style.label +local lu_col = style.label_unit_pair +local text_fg = style.text_fg -local hzd_fg_bg = cpair(colors.white, colors.gray) -local dis_colors = cpair(colors.white, colors.lightGray) +local mode_states = style.icon_states.mode_states + +local btn_active = cpair(colors.white, colors.black) +local hzd_fg_bg = style.hzd_fg_bg +local hzd_dis_colors = style.hzd_dis_colors -- new unit control page view ---@param root Container parent local function new_view(root) + local btn_fg_bg = cpair(colors.green, colors.black) + local db = iocontrol.get_db() local frame = Div{parent=root,x=1,y=1} @@ -58,17 +63,14 @@ local function new_view(root) 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.green, colors.black) - local btn_active = cpair(colors.white, colors.black) + app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } }) local page_div = nil ---@type Div|nil -- set sidebar to display unit-specific fields based on a specified unit local function set_sidebar() local list = { - { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end }, + { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home }, { label = "FAC", color = core.cpair(colors.black, colors.orange), callback = function () app.switcher(db.facility.num_units + 1) end } } @@ -138,12 +140,12 @@ local function new_view(root) u_div.line_break() - TextBox{parent=u_div,y=8,text="CMD",width=4,fg_bg=cpair(colors.lightGray,colors.black)} - TextBox{parent=u_div,x=14,y=8,text="mB/t",width=4,fg_bg=cpair(colors.lightGray,colors.black)} - local burn_cmd = NumberField{parent=u_div,x=5,y=8,width=8,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=cpair(colors.gray,colors.lightGray)} + TextBox{parent=u_div,y=8,text="CMD",width=4,fg_bg=label_fg_bg} + TextBox{parent=u_div,x=14,y=8,text="mB/t",width=4,fg_bg=label_fg_bg} + local burn_cmd = NumberField{parent=u_div,x=5,y=8,width=8,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=style.field,dis_fg_bg=style.field_disable} - local set_burn = function () unit.set_burn(burn_cmd.get_value()) end - local set_burn_btn = PushButton{parent=u_div,x=19,y=8,text="SET",min_width=5,fg_bg=cpair(colors.green,colors.black),active_fg_bg=cpair(colors.white,colors.black),dis_fg_bg=cpair(colors.gray,colors.black),callback=set_burn} + local set_burn = function () unit.set_burn(burn_cmd.get_numeric()) end + local set_burn_btn = PushButton{parent=u_div,x=19,y=8,text="SET",min_width=5,fg_bg=cpair(colors.green,colors.black),active_fg_bg=cpair(colors.white,colors.black),dis_fg_bg=style.btn_disable,callback=set_burn} -- enable/disable controls based on group assignment (start button is separate) burn_cmd.register(u_ps, "auto_group_id", function (gid) @@ -156,10 +158,10 @@ local function new_view(root) burn_cmd.register(u_ps, "burn_rate", burn_cmd.set_value) burn_cmd.register(u_ps, "max_burn", burn_cmd.set_max) - local start = HazardButton{parent=u_div,x=2,y=11,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,timeout=3,fg_bg=hzd_fg_bg} - local ack_a = HazardButton{parent=u_div,x=12,y=11,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,timeout=3,fg_bg=hzd_fg_bg} - local scram = HazardButton{parent=u_div,x=2,y=15,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,timeout=3,fg_bg=hzd_fg_bg} - local reset = HazardButton{parent=u_div,x=12,y=15,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,timeout=3,fg_bg=hzd_fg_bg} + local start = HazardButton{parent=u_div,x=2,y=11,text="START",accent=colors.lightBlue,callback=unit.start,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors} + local ack_a = HazardButton{parent=u_div,x=12,y=11,text="ACK \x13",accent=colors.orange,callback=unit.ack_alarms,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors} + local scram = HazardButton{parent=u_div,x=2,y=15,text="SCRAM",accent=colors.yellow,callback=unit.scram,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors} + local reset = HazardButton{parent=u_div,x=12,y=15,text="RESET",accent=colors.red,callback=unit.reset_rps,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors} unit.start_ack = start.on_response unit.ack_alarms_ack = ack_a.on_response @@ -192,8 +194,8 @@ local function new_view(root) TextBox{parent=f_div,y=1,text="Facility Commands",alignment=ALIGN.CENTER} - local scram = HazardButton{parent=f_div,x=5,y=6,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,timeout=3,fg_bg=hzd_fg_bg} - local ack_a = HazardButton{parent=f_div,x=7,y=11,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,timeout=3,fg_bg=hzd_fg_bg} + local scram = HazardButton{parent=f_div,x=5,y=6,text="FAC SCRAM",accent=colors.yellow,dis_colors=hzd_dis_colors,callback=process.fac_scram,timeout=3,fg_bg=hzd_fg_bg} + local ack_a = HazardButton{parent=f_div,x=7,y=11,text="ACK \x13",accent=colors.orange,dis_colors=hzd_dis_colors,callback=process.fac_ack_alarms,timeout=3,fg_bg=hzd_fg_bg} db.facility.scram_ack = scram.on_response db.facility.ack_alarms_ack = ack_a.on_response @@ -215,7 +217,7 @@ local function new_view(root) 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.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } }) app.delete_pages() -- show loading screen diff --git a/pocket/ui/apps/guide.lua b/pocket/ui/apps/guide.lua index 5e4ae31..2efb862 100644 --- a/pocket/ui/apps/guide.lua +++ b/pocket/ui/apps/guide.lua @@ -56,14 +56,14 @@ local function new_view(root) 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 }}) + app.set_sidebar({{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home }}) local page_div = nil ---@type Div|nil -- 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 = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home }, { 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 } } @@ -263,7 +263,7 @@ local function new_view(root) 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.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } }) app.delete_pages() -- show loading screen diff --git a/pocket/ui/apps/process.lua b/pocket/ui/apps/process.lua new file mode 100644 index 0000000..deb7b1f --- /dev/null +++ b/pocket/ui/apps/process.lua @@ -0,0 +1,337 @@ +-- +-- Process Control Page +-- + +local types = require("scada-common.types") +local util = require("scada-common.util") + +local iocontrol = require("pocket.iocontrol") +local pocket = require("pocket.pocket") +local process = require("pocket.process") + +local style = require("pocket.ui.style") + +local core = require("graphics.core") + +local Div = require("graphics.elements.Div") +local MultiPane = require("graphics.elements.MultiPane") +local Rectangle = require("graphics.elements.Rectangle") +local TextBox = require("graphics.elements.TextBox") + +local WaitingAnim = require("graphics.elements.animations.Waiting") + +local HazardButton = require("graphics.elements.controls.HazardButton") +local RadioButton = require("graphics.elements.controls.RadioButton") + +local NumberField = require("graphics.elements.form.NumberField") + +local IconIndicator = require("graphics.elements.indicators.IconIndicator") + +local ALIGN = core.ALIGN +local cpair = core.cpair +local border = core.border + +local APP_ID = pocket.APP_ID + +local label_fg_bg = style.label +local text_fg = style.text_fg + +local field_fg_bg = style.field +local field_dis_fg_bg = style.field_disable + +local red_ind_s = style.icon_states.red_ind_s +local yel_ind_s = style.icon_states.yel_ind_s +local grn_ind_s = style.icon_states.grn_ind_s +local wht_ind_s = style.icon_states.wht_ind_s + +local hzd_fg_bg = style.hzd_fg_bg +local dis_colors = cpair(colors.white, colors.lightGray) + +-- new process control page view +---@param root Container 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.PROCESS, frame, nil, false, true) + + 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...",alignment=ALIGN.CENTER} + WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.purple,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 = db.nav.go_home } }) + + local page_div = nil ---@type Div|nil + + -- load the app (create the elements) + local function load() + local f_ps = db.facility.ps + + page_div = Div{parent=main,y=2,width=main.get_width()} + + local panes = {} ---@type Div[] + + -- create all page divs + for _ = 1, db.facility.num_units + 3 do + local div = Div{parent=page_div} + table.insert(panes, div) + end + + local last_update = 0 + -- refresh data callback, every 500ms it will re-send the query + local function update() + if util.time_ms() - last_update >= 500 then + db.api.get_proc() + last_update = util.time_ms() + end + end + + --#region unit settings/status + + local rate_limits = {} ---@type NumberField[] + + for i = 1, db.facility.num_units do + local u_pane = panes[i] + local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2} + local unit = db.units[i] + local u_ps = unit.unit_ps + + local u_page = app.new_page(nil, i) + u_page.tasks = { update } + + TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,alignment=ALIGN.CENTER} + + TextBox{parent=u_div,y=3,text="Auto Rate Limit",fg_bg=label_fg_bg} + rate_limits[i] = NumberField{parent=u_div,x=1,y=4,width=16,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg} + TextBox{parent=u_div,x=18,y=4,text="mB/t",width=4,fg_bg=label_fg_bg} + + rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max) + rate_limits[i].register(unit.unit_ps, "burn_limit", rate_limits[i].set_value) + + local ready = IconIndicator{parent=u_div,y=6,label="Auto Ready",states=grn_ind_s} + local a_stb = IconIndicator{parent=u_div,label="Auto Standby",states=wht_ind_s} + local degraded = IconIndicator{parent=u_div,label="Unit Degraded",states=red_ind_s} + + ready.register(u_ps, "U_AutoReady", ready.update) + degraded.register(u_ps, "U_AutoDegraded", degraded.update) + + -- update standby indicator + a_stb.register(u_ps, "status", function (active) + a_stb.update(unit.annunciator.AutoControl and (not active)) + end) + a_stb.register(u_ps, "AutoControl", function (auto_active) + if auto_active then + a_stb.update(unit.reactor_data.mek_status.status == false) + else a_stb.update(false) end + end) + + local function _set_group(value) process.set_group(i, value - 1) end + + local group = RadioButton{parent=u_div,y=10,options=types.AUTO_GROUP_NAMES,callback=_set_group,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.purple,dis_fg_bg=style.btn_disable} + + -- can't change group if auto is engaged regardless of if this unit is part of auto control + group.register(f_ps, "auto_active", function (auto_active) + if auto_active then group.disable() else group.enable() end + end) + + group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end) + + TextBox{parent=u_div,y=16,text="Assigned Group",fg_bg=style.label} + local auto_grp = TextBox{parent=u_div,text="Manual",width=11,fg_bg=text_fg} + + auto_grp.register(u_ps, "auto_group", auto_grp.set_value) + + util.nop() + end + + --#endregion + + --#region process control options page + + local o_pane = panes[db.facility.num_units + 2] + local o_div = Div{parent=o_pane,x=2,width=main.get_width()-2} + + local opt_page = app.new_page(nil, db.facility.num_units + 2) + opt_page.tasks = { update } + + TextBox{parent=o_div,y=1,text="Process Options",alignment=ALIGN.CENTER} + + local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" } + local mode = RadioButton{parent=o_div,x=1,y=3,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.purple,dis_fg_bg=style.btn_disable} + + mode.register(f_ps, "process_mode", mode.set_value) + + TextBox{parent=o_div,y=9,text="Burn Rate Target",fg_bg=label_fg_bg} + local b_target = NumberField{parent=o_div,x=1,y=10,width=15,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg} + TextBox{parent=o_div,x=17,y=10,text="mB/t",fg_bg=label_fg_bg} + + TextBox{parent=o_div,y=12,text="Charge Level Target",fg_bg=label_fg_bg} + local c_target = NumberField{parent=o_div,x=1,y=13,width=15,default=0,min=0,max_chars=16,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg} + TextBox{parent=o_div,x=17,y=13,text="M"..db.energy_label,fg_bg=label_fg_bg} + + TextBox{parent=o_div,y=15,text="Generation Target",fg_bg=label_fg_bg} + local g_target = NumberField{parent=o_div,x=1,y=16,width=15,default=0,min=0,max_chars=16,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg} + TextBox{parent=o_div,x=17,y=16,text="k"..db.energy_label.."/t",fg_bg=label_fg_bg} + + b_target.register(f_ps, "process_burn_target", b_target.set_value) + c_target.register(f_ps, "process_charge_target", c_target.set_value) + g_target.register(f_ps, "process_gen_target", g_target.set_value) + + --#endregion + + --#region process control page + + local c_pane = panes[db.facility.num_units + 1] + local c_div = Div{parent=c_pane,x=2,width=main.get_width()-2} + + local proc_ctrl = app.new_page(nil, db.facility.num_units + 1) + proc_ctrl.tasks = { update } + + TextBox{parent=c_div,y=1,text="Process Control",alignment=ALIGN.CENTER} + + local u_stat = Rectangle{parent=c_div,border=border(1,colors.gray,true),thin=true,width=21,height=5,x=1,y=3,fg_bg=cpair(colors.black,colors.lightGray)} + local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",alignment=ALIGN.CENTER} + local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",height=2,alignment=ALIGN.CENTER,trim_whitespace=true,fg_bg=cpair(colors.gray,colors.lightGray)} + + stat_line_1.register(f_ps, "status_line_1", stat_line_1.set_value) + stat_line_2.register(f_ps, "status_line_2", stat_line_2.set_value) + + local function _start_auto() + local limits = {} + for i = 1, #rate_limits do limits[i] = rate_limits[i].get_numeric() end + + process.process_start(mode.get_value(), b_target.get_numeric(), db.energy_convert_to_fe(c_target.get_numeric()), + db.energy_convert_to_fe(g_target.get_numeric()), limits) + end + + local start = HazardButton{parent=c_div,x=2,y=9,text="START",accent=colors.lightBlue,callback=_start_auto,timeout=3,fg_bg=hzd_fg_bg,dis_colors=dis_colors} + local stop = HazardButton{parent=c_div,x=13,y=9,text="STOP",accent=colors.red,callback=process.process_stop,timeout=3,fg_bg=hzd_fg_bg,dis_colors=dis_colors} + + db.facility.start_ack = start.on_response + db.facility.stop_ack = stop.on_response + + start.register(f_ps, "auto_ready", function (ready) + if ready and (not db.facility.auto_active) then start.enable() else start.disable() end + end) + + local auto_ready = IconIndicator{parent=c_div,y=14,label="Units Ready",states=grn_ind_s} + local auto_act = IconIndicator{parent=c_div,label="Process Active",states=grn_ind_s} + local auto_ramp = IconIndicator{parent=c_div,label="Process Ramping",states=wht_ind_s} + local auto_sat = IconIndicator{parent=c_div,label="Min/Max Burn Rate",states=yel_ind_s} + + auto_ready.register(f_ps, "auto_ready", auto_ready.update) + auto_act.register(f_ps, "auto_active", auto_act.update) + auto_ramp.register(f_ps, "auto_ramping", auto_ramp.update) + auto_sat.register(f_ps, "auto_saturated", auto_sat.update) + + -- REGISTER_NOTE: for optimization/brevity, due to not deleting anything but the whole element tree + -- when it comes to unloading the process app, child elements will not directly be registered here + -- (preventing garbage collection until the parent 'page_div' is deleted) + page_div.register(f_ps, "auto_active", function (active) + if active then + b_target.disable() + c_target.disable() + g_target.disable() + + mode.disable() + start.disable() + + for i = 1, #rate_limits do rate_limits[i].disable() end + else + b_target.enable() + c_target.enable() + g_target.enable() + + mode.enable() + if db.facility.auto_ready then start.enable() end + + for i = 1, #rate_limits do rate_limits[i].enable() end + end + end) + + --#endregion + + --#region auto-SCRAM annunciator page + + local a_pane = panes[db.facility.num_units + 3] + local a_div = Div{parent=a_pane,x=2,width=main.get_width()-2} + + local annunc_page = app.new_page(nil, db.facility.num_units + 3) + annunc_page.tasks = { update } + + TextBox{parent=a_div,y=1,text="Automatic SCRAM",alignment=ALIGN.CENTER} + + local auto_scram = IconIndicator{parent=a_div,y=3,label="Automatic SCRAM",states=red_ind_s} + + TextBox{parent=a_div,y=5,text="Induction Matrix",fg_bg=label_fg_bg} + local matrix_dc = IconIndicator{parent=a_div,label="Disconnected",states=yel_ind_s} + local matrix_fill = IconIndicator{parent=a_div,label="Charge High",states=red_ind_s} + + TextBox{parent=a_div,y=9,text="Assigned Units",fg_bg=label_fg_bg} + local unit_crit = IconIndicator{parent=a_div,label="Critical Alarm",states=red_ind_s} + + TextBox{parent=a_div,y=12,text="Facility",fg_bg=label_fg_bg} + local fac_rad_h = IconIndicator{parent=a_div,label="Radiation High",states=red_ind_s} + + TextBox{parent=a_div,y=15,text="Generation Rate Mode",fg_bg=label_fg_bg} + local gen_fault = IconIndicator{parent=a_div,label="Control Fault",states=yel_ind_s} + + auto_scram.register(f_ps, "auto_scram", auto_scram.update) + matrix_dc.register(f_ps, "as_matrix_dc", matrix_dc.update) + matrix_fill.register(f_ps, "as_matrix_fill", matrix_fill.update) + unit_crit.register(f_ps, "as_crit_alarm", unit_crit.update) + fac_rad_h.register(f_ps, "as_radiation", fac_rad_h.update) + gen_fault.register(f_ps, "as_gen_fault", gen_fault.update) + + --#endregion + + -- setup multipane + local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} + app.set_root_pane(u_pane) + + -- setup sidebar + + local list = { + { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home }, + { label = " \x17 ", color = core.cpair(colors.black, colors.purple), callback = proc_ctrl.nav_to }, + { label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = annunc_page.nav_to }, + { label = "OPT", color = core.cpair(colors.black, colors.yellow), callback = opt_page.nav_to } + } + + for i = 1, db.facility.num_units do + table.insert(list, { label = "U-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(i) end }) + end + + app.set_sidebar(list) + + -- done, show the app + proc_ctrl.nav_to() + 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 = db.nav.go_home } }) + 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/unit.lua b/pocket/ui/apps/unit.lua index 2f97bf9..bc34d5f 100644 --- a/pocket/ui/apps/unit.lua +++ b/pocket/ui/apps/unit.lua @@ -63,7 +63,7 @@ local function new_view(root) 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 } }) + app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } }) local btn_fg_bg = cpair(colors.yellow, colors.black) local btn_active = cpair(colors.white, colors.black) @@ -76,7 +76,7 @@ local function new_view(root) local unit = db.units[id] local list = { - { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end }, + { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home }, { 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 }, @@ -383,7 +383,7 @@ local function new_view(root) 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.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } }) app.delete_pages() -- show loading screen diff --git a/pocket/ui/main.lua b/pocket/ui/main.lua index eb9c7f1..1596485 100644 --- a/pocket/ui/main.lua +++ b/pocket/ui/main.lua @@ -12,6 +12,7 @@ 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 loader_app = require("pocket.ui.apps.loader") +local process_app = require("pocket.ui.apps.process") local sys_apps = require("pocket.ui.apps.sys_apps") local unit_app = require("pocket.ui.apps.unit") @@ -64,6 +65,7 @@ local function init(main) home_page(page_div) unit_app(page_div) control_app(page_div) + process_app(page_div) guide_app(page_div) loader_app(page_div) sys_apps(page_div) @@ -78,7 +80,7 @@ local function init(main) 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(APP_ID.ROOT) + db.nav.go_home() -- done with initial render, lets go! root_pane.set_value(2) diff --git a/pocket/ui/pages/home_page.lua b/pocket/ui/pages/home_page.lua index 80c7a24..1e478dc 100644 --- a/pocket/ui/pages/home_page.lua +++ b/pocket/ui/pages/home_page.lua @@ -48,7 +48,7 @@ local function new_view(root) App{parent=apps_1,x=2,y=2,text="U",title="Units",callback=function()open(APP_ID.UNITS)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg} App{parent=apps_1,x=9,y=2,text="F",title="Facil",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg} App{parent=apps_1,x=16,y=2,text="\x15",title="Control",callback=function()open(APP_ID.CONTROL)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg} - App{parent=apps_1,x=2,y=7,text="\x17",title="Process",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg} + App{parent=apps_1,x=2,y=7,text="\x17",title="Process",callback=function()open(APP_ID.PROCESS)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.GUIDE)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg} diff --git a/pocket/ui/style.lua b/pocket/ui/style.lua index dc26755..ff7fc9b 100644 --- a/pocket/ui/style.lua +++ b/pocket/ui/style.lua @@ -10,12 +10,19 @@ local cpair = core.cpair -- GLOBAL -- -style.root = cpair(colors.white, colors.black) -style.header = cpair(colors.white, colors.gray) -style.text_fg = cpair(colors.white, colors._INHERIT) -style.label = cpair(colors.lightGray, colors.black) +style.root = cpair(colors.white, colors.black) +style.header = cpair(colors.white, colors.gray) +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.field = cpair(colors.white, colors.gray) +style.field_disable = cpair(colors.gray, colors.lightGray) +style.btn_disable = cpair(colors.gray, colors.black) +style.hzd_fg_bg = cpair(colors.white, colors.gray) +style.hzd_dis_colors = cpair(colors.white, colors.lightGray) + style.colors = { { c = colors.red, hex = 0xdf4949 }, { c = colors.orange, hex = 0xffb659 }, @@ -73,6 +80,16 @@ states.yel_ind_s = { { color = cpair(colors.black, colors.yellow), symbol = "-" } } +states.grn_ind_s = { + { color = cpair(colors.black, colors.lightGray), symbol = "\x07" }, + { color = cpair(colors.black, colors.green), symbol = "+" } +} + +states.wht_ind_s = { + { color = cpair(colors.black, colors.lightGray), symbol = "\x07" }, + { color = cpair(colors.black, colors.white), symbol = "+" } +} + style.icon_states = states -- MAIN LAYOUT -- diff --git a/scada-common/comms.lua b/scada-common/comms.lua index a1281ce..037ce5e 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -18,7 +18,7 @@ local comms = {} -- protocol/data versions (protocol/data independent changes tracked by util.lua version) comms.version = "3.0.0" -comms.api_version = "0.0.5" +comms.api_version = "0.0.6" ---@enum PROTOCOL local PROTOCOL = { @@ -68,7 +68,8 @@ local CRDN_TYPE = { UNIT_CMD = 6, -- command a reactor unit API_GET_FAC = 7, -- API: get all the facility data API_GET_UNIT = 8, -- API: get reactor unit data - API_GET_CTRL = 9 -- API: get data used for the control app + API_GET_CTRL = 9, -- API: get data used for the control app + API_GET_PROC = 10 -- API: get data used for the process app } ---@enum ESTABLISH_ACK diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 3ec2b82..4b2b38c 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -425,9 +425,9 @@ function facility.new(config) ready = self.mode_set > 0 - if (self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0) or - (self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0) or - (self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1) then + if ((self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0)) or + ((self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0)) or + ((self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1)) then ready = false end @@ -436,6 +436,8 @@ function facility.new(config) if ready then self.mode = self.mode_set end end + log.debug(util.c("FAC: process start ", util.trinary(ready, "accepted", "rejected"))) + return { ready, self.mode_set, diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 388ab51..bf702b7 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -22,7 +22,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.5.8" +local SUPERVISOR_VERSION = "v1.5.9" local println = util.println local println_ts = util.println_ts