diff --git a/build/bundle.py b/build/bundle.py index 38940448..16c37962 100644 --- a/build/bundle.py +++ b/build/bundle.py @@ -2,7 +2,6 @@ import json import os import subprocess -import sys path_prefix = "./_minified/" diff --git a/ccmsi.lua b/ccmsi.lua index a7f7eb82..e1ec80c5 100644 --- a/ccmsi.lua +++ b/ccmsi.lua @@ -15,7 +15,7 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]]-- -local CCMSI_VERSION = "v1.17" +local CCMSI_VERSION = "v1.19" local install_dir = "/.install-cache" local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/" @@ -169,6 +169,7 @@ local function write_install_manifest(manifest, dependencies) end -- try at most 3 times to download a file from the repository and write into w_path base directory +---@return 0|1|2|3 success 0: ok, 1: download fail, 2: file open fail, 3: out of space local function http_get_file(file, w_path) local dl, err for i = 1, 3 do @@ -176,17 +177,28 @@ local function http_get_file(file, w_path) if dl then if i > 1 then green();println("success!");lgray() end local f = fs.open(w_path..file, "w") - f.write(dl.readAll()) + if not f then return 2 end + local ok, msg = pcall(function() f.write(dl.readAll()) end) f.close() + if not ok then + if string.find(msg or "", "Out of space") ~= nil then + red();println("[out of space]");lgray() + return 3 + else return 2 end + end break else red();println("HTTP Error: "..err) - if i < 3 then lgray();print("> retrying...") end ----@diagnostic disable-next-line: undefined-field - os.sleep(i/3.0) + if i < 3 then + lgray();print("> retrying...") + ---@diagnostic disable-next-line: undefined-field + os.sleep(i/3.0) + else + return 1 + end end end - return dl ~= nil + return 0 end -- recursively build a tree out of the file manifest @@ -407,7 +419,7 @@ elseif mode == "install" or mode == "update" then if manifest.versions.installer ~= CCMSI_VERSION then if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end - if update_installer or ask_y_n("Would you like to update now") then + if update_installer or ask_y_n("Would you like to update now", true) then lgray();println("GET ccmsi.lua") local dl, err = http.get(repo_path.."ccmsi.lua") @@ -492,19 +504,119 @@ elseif mode == "install" or mode == "update" then -- check space constraints if space_available < space_required then single_file_mode = true - yellow();println("NOTICE: Insufficient space available for a full cached download!");white() - lgray();println("Files can instead be downloaded one by one. If you are replacing a current install this may corrupt your install ONLY if it fails (such as a sudden network issue). If that occurs, you can still try again.") - if mode == "update" then println("If installation still fails, delete this device's log file and/or any unrelated files you have on this computer then try again.") end - white(); - if not ask_y_n("Do you wish to continue", false) then - println("Operation cancelled.") - return - end end local success = true - if not single_file_mode then + -- delete a file if the capitalization changes so that things work on Windows + ---@param path string + local function mitigate_case(path) + local dir, file = fs.getDir(path), fs.getName(path) + if not fs.isDir(dir) then return end + for _, p in ipairs(fs.list(dir)) do + if string.lower(p) == string.lower(file) then + if p ~= file then fs.delete(path) end + return + end + end + end + + ---@param dl_stat 1|2|3 download status + ---@param file string file name + ---@param attempt integer recursive attempt # + ---@param sf_install function installer function for recursion + local function handle_dl_fail(dl_stat, file, attempt, sf_install) + red() + if dl_stat == 1 then + println("failed to download "..file) + elseif dl_stat > 1 then + if dl_stat == 2 then println("filesystem error with "..file) else println("no space for "..file) end + if attempt == 1 then + orange();println("re-attempting operation...");white() + sf_install(2) + elseif attempt == 2 then + yellow() + if dl_stat == 2 then println("There was an error writing to a file.") else println("Insufficient space available.") end + lgray() + if dl_stat == 2 then + println("This may be due to insufficent space available or file permission issues. The installer can now attempt to delete files not used by the SCADA system.") + else + println("The installer can now attempt to delete files not used by the SCADA system.") + end + white() + if not ask_y_n("Continue", false) then + success = false + return + end + clean(manifest) + sf_install(3) + elseif attempt == 3 then + yellow() + if dl_stat == 2 then println("There again was an error writing to a file.") else println("Insufficient space available.") end + lgray() + if dl_stat == 2 then + println("This may be due to insufficent space available or file permission issues. Please delete any unused files you have on this computer then try again. Do not delete the "..app..".settings file unless you want to re-configure.") + else + println("Please delete any unused files you have on this computer then try again. Do not delete the "..app..".settings file unless you want to re-configure.") + end + white() + success = false + end + end + end + + -- single file update routine: go through all files and replace one by one + ---@param attempt integer recursive attempt # + local function sf_install(attempt) +---@diagnostic disable-next-line: undefined-field + if attempt > 1 then os.sleep(2.0) end + + local abort_attempt = false + success = true + + for _, dependency in pairs(dependencies) do + if mode == "update" and unchanged(dependency) then + pkg_message("skipping install of unchanged package", dependency) + else + pkg_message("installing package", dependency) + lgray() + + -- beginning on the second try, delete the directory before starting + if attempt >= 2 then + if dependency == "system" then + elseif dependency == "common" then + if fs.exists("/scada-common") then + fs.delete("/scada-common") + println("deleted /scada-common") + end + else + if fs.exists("/"..dependency) then + fs.delete("/"..dependency) + println("deleted /"..dependency) + end + end + end + + local files = file_list[dependency] + for _, file in pairs(files) do + println("GET "..file) + mitigate_case(file) + local dl_stat = http_get_file(file, "/") + if dl_stat ~= 0 then + abort_attempt = true +---@diagnostic disable-next-line: param-type-mismatch + handle_dl_fail(dl_stat, file, attempt, sf_install) + break + end + end + end + if abort_attempt or not success then break end + end + end + + -- handle update/install + if single_file_mode then sf_install(1) + else if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end -- download all dependencies @@ -518,9 +630,17 @@ elseif mode == "install" or mode == "update" then local files = file_list[dependency] for _, file in pairs(files) do println("GET "..file) - if not http_get_file(file, install_dir.."/") then + local dl_stat = http_get_file(file, install_dir.."/") + success = dl_stat == 0 + if dl_stat == 1 then red();println("failed to download "..file) - success = false + break + elseif dl_stat == 2 then + red();println("filesystem error with "..file) + break + elseif dl_stat == 3 then + -- this shouldn't occur in this mode + red();println("no space for "..file) break end end @@ -548,57 +668,27 @@ elseif mode == "install" or mode == "update" then end fs.delete(install_dir) + end - if success then - write_install_manifest(manifest, dependencies) - green() - if mode == "install" then - println("Installation completed successfully.") - else println("Update completed successfully.") end - white();println("Ready to clean up unused files, press any key to continue...") - any_key();clean(manifest) - white();println("Done.") - else - if mode == "install" then - red();println("Installation failed.") - else orange();println("Update failed, existing files unmodified.") end - end + if success then + write_install_manifest(manifest, dependencies) + green() + if mode == "install" then + println("Installation completed successfully.") + else println("Update completed successfully.") end + white();println("Ready to clean up unused files, press any key to continue...") + any_key();clean(manifest) + white();println("Done.") else - -- go through all files and replace one by one - for _, dependency in pairs(dependencies) do - if mode == "update" and unchanged(dependency) then - pkg_message("skipping install of unchanged package", dependency) - else - pkg_message("installing package", dependency) - lgray() - - local files = file_list[dependency] - for _, file in pairs(files) do - println("GET "..file) - if not http_get_file(file, "/") then - red();println("failed to download "..file) - success = false - break - end - end - end - if not success then break end - end - - if success then - write_install_manifest(manifest, dependencies) - green() - if mode == "install" then - println("Installation completed successfully.") - else println("Update completed successfully.") end - white();println("Ready to clean up unused files, press any key to continue...") - any_key();clean(manifest) - white();println("Done.") - else - red() + red() + if single_file_mode then if mode == "install" then println("Installation failed, files may have been skipped.") else println("Update failed, files may have been skipped.") end + else + if mode == "install" then + println("Installation failed.") + else orange();println("Update failed, existing files unmodified.") end end end elseif mode == "uninstall" then diff --git a/coordinator/config/facility.lua b/coordinator/config/facility.lua new file mode 100644 index 00000000..00f8c997 --- /dev/null +++ b/coordinator/config/facility.lua @@ -0,0 +1,318 @@ +local comms = require("scada-common.comms") +local network = require("scada-common.network") +local ppm = require("scada-common.ppm") +local tcd = require("scada-common.tcd") +local util = require("scada-common.util") + +local core = require("graphics.core") + +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") + +local PushButton = require("graphics.elements.controls.PushButton") + +local NumberField = require("graphics.elements.form.NumberField") + +local tri = util.trinary + +local cpair = core.cpair + +local PROTOCOL = comms.PROTOCOL +local DEVICE_TYPE = comms.DEVICE_TYPE +local ESTABLISH_ACK = comms.ESTABLISH_ACK +local MGMT_TYPE = comms.MGMT_TYPE + +local self = { + nic = nil, ---@type nic + net_listen = false, + sv_addr = comms.BROADCAST, + sv_seq_num = util.time_ms() * 10, + show_sv_cfg = nil, ---@type function + + sv_conn_button = nil, ---@type PushButton + sv_conn_status = nil, ---@type TextBox + sv_conn_detail = nil, ---@type TextBox + sv_next = nil, ---@type PushButton + sv_skip = nil, ---@type PushButton + + tool_ctl = nil, ---@type _crd_cfg_tool_ctl + tmp_cfg = nil ---@type crd_config +} + +-- check if a value is an integer within a range (inclusive) +---@param x any +---@param min integer +---@param max integer +local function is_int_min_max(x, min, max) return util.is_int(x) and x >= min and x <= max end + +-- send a management packet to the supervisor +---@param msg_type MGMT_TYPE +---@param msg table +local function send_sv(msg_type, msg) + local s_pkt = comms.scada_packet() + local pkt = comms.mgmt_packet() + + pkt.make(msg_type, msg) + s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) + + self.nic.transmit(self.tmp_cfg.SVR_Channel, self.tmp_cfg.CRD_Channel, s_pkt) + self.sv_seq_num = self.sv_seq_num + 1 +end + +-- handle an establish message from the supervisor +---@param packet mgmt_frame +local function handle_packet(packet) + local error_msg = nil + + if packet.scada_frame.local_channel() ~= self.tmp_cfg.CRD_Channel then + error_msg = "Error: unknown receive channel." + elseif packet.scada_frame.remote_channel() == self.tmp_cfg.SVR_Channel and packet.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then + if packet.type == MGMT_TYPE.ESTABLISH then + if packet.length == 2 then + local est_ack = packet.data[1] + local config = packet.data[2] + + if est_ack == ESTABLISH_ACK.ALLOW then + if type(config) == "table" and #config == 2 then + local count_ok = is_int_min_max(config[1], 1, 4) + local cool_ok = type(config[2]) == "table" and type(config[2].r_cool) == "table" and #config[2].r_cool == config[1] + + if count_ok and cool_ok then + self.tmp_cfg.UnitCount = config[1] + self.tool_ctl.sv_cool_conf = {} + + for i = 1, self.tmp_cfg.UnitCount do + local num_b = config[2].r_cool[i].BoilerCount + local num_t = config[2].r_cool[i].TurbineCount + self.tool_ctl.sv_cool_conf[i] = { num_b, num_t } + cool_ok = cool_ok and is_int_min_max(num_b, 0, 2) and is_int_min_max(num_t, 1, 3) + end + end + + if not count_ok then + error_msg = "Error: supervisor unit count out of range." + elseif not cool_ok then + error_msg = "Error: supervisor cooling configuration malformed." + self.tool_ctl.sv_cool_conf = nil + end + + self.sv_addr = packet.scada_frame.src_addr() + send_sv(MGMT_TYPE.CLOSE, {}) + else + error_msg = "Error: invalid cooling configuration supervisor." + end + else + error_msg = "Error: invalid allow reply length from supervisor." + end + elseif packet.length == 1 then + local est_ack = packet.data[1] + + if est_ack == ESTABLISH_ACK.DENY then + error_msg = "Error: supervisor connection denied." + elseif est_ack == ESTABLISH_ACK.COLLISION then + error_msg = "Error: a coordinator is already/still connected. Please try again." + elseif est_ack == ESTABLISH_ACK.BAD_VERSION then + error_msg = "Error: coordinator comms version does not match supervisor comms version." + else + error_msg = "Error: invalid reply from supervisor." + end + else + error_msg = "Error: invalid reply length from supervisor." + end + else + error_msg = "Error: didn't get an establish reply from supervisor." + end + end + + self.net_listen = false + + if error_msg then + self.sv_conn_status.set_value("") + self.sv_conn_detail.set_value(error_msg) + self.sv_conn_button.enable() + else + self.sv_conn_status.set_value("Connected!") + self.sv_conn_detail.set_value("Data received successfully, press 'Next' to continue.") + self.sv_skip.hide() + self.sv_next.show() + end +end + +-- handle supervisor connection failure +local function handle_timeout() + self.net_listen = false + self.sv_conn_button.enable() + self.sv_conn_status.set_value("Timed out.") + self.sv_conn_detail.set_value("Supervisor did not reply. Ensure startup app is running on the supervisor.") +end + +-- attempt a connection to the supervisor to get cooling info +local function sv_connect() + self.sv_conn_button.disable() + self.sv_conn_detail.set_value("") + + local modem = ppm.get_wireless_modem() + if modem == nil then + self.sv_conn_status.set_value("Please connect an ender/wireless modem.") + else + self.sv_conn_status.set_value("Modem found, connecting...") + if self.nic == nil then self.nic = network.nic(modem) end + + self.nic.closeAll() + self.nic.open(self.tmp_cfg.CRD_Channel) + + self.sv_addr = comms.BROADCAST + self.net_listen = true + + send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.CRD }) + + tcd.dispatch_unique(8, handle_timeout) + end +end + +local facility = {} + +-- create the facility configuration view +---@param tool_ctl _crd_cfg_tool_ctl +---@param main_pane MultiPane +---@param cfg_sys [ crd_config, crd_config, crd_config, { [1]: string, [2]: string, [3]: any }[], function ] +---@param fac_cfg Div +---@param style { [string]: cpair } +---@return MultiPane fac_pane +function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style) + local _, ini_cfg, tmp_cfg, _, _ = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5] + + self.tmp_cfg = tmp_cfg + self.tool_ctl = tool_ctl + + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + + --#region Facility + + local fac_c_1 = Div{parent=fac_cfg,x=2,y=4,width=49} + local fac_c_2 = Div{parent=fac_cfg,x=2,y=4,width=49} + local fac_c_3 = Div{parent=fac_cfg,x=2,y=4,width=49} + + local fac_pane = MultiPane{parent=fac_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3}} + + TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)} + + TextBox{parent=fac_c_1,x=1,y=1,height=4,text="This tool can attempt to connect to your supervisor computer. This would load facility information in order to get the unit count and aid monitor setup."} + TextBox{parent=fac_c_1,x=1,y=6,height=2,text="The supervisor startup app must be running and fully configured on your supervisor computer."} + + self.sv_conn_status = TextBox{parent=fac_c_1,x=11,y=9,text=""} + self.sv_conn_detail = TextBox{parent=fac_c_1,x=1,y=11,height=2,text=""} + + self.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()sv_connect()end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + + local function sv_skip() + tcd.abort(handle_timeout) + tool_ctl.sv_cool_conf = nil + self.net_listen = false + fac_pane.set_value(2) + end + + local function sv_next() + self.show_sv_cfg() + tool_ctl.update_mon_reqs() + fac_pane.set_value(3) + end + + PushButton{parent=fac_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + self.sv_skip = PushButton{parent=fac_c_1,x=44,y=14,text="Skip \x1a",callback=sv_skip,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + self.sv_next = PushButton{parent=fac_c_1,x=44,y=14,text="Next \x1a",callback=sv_next,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,hidden=true} + + TextBox{parent=fac_c_2,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."} + tool_ctl.num_units = NumberField{parent=fac_c_2,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg} + TextBox{parent=fac_c_2,x=7,y=5,text="reactors"} + TextBox{parent=fac_c_2,x=1,y=7,height=3,text="This will decide how many monitors you need. If this does not match the supervisor's number of reactor units, the coordinator will not connect.",fg_bg=g_lg_fg_bg} + TextBox{parent=fac_c_2,x=1,y=10,height=3,text="Since you skipped supervisor sync, the main monitor minimum height can't be determined precisely. It is marked with * on the next page.",fg_bg=g_lg_fg_bg} + + local nu_error = TextBox{parent=fac_c_2,x=8,y=14,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_num_units() + local count = tonumber(tool_ctl.num_units.get_value()) + if count ~= nil and count > 0 and count < 5 then + nu_error.hide(true) + tmp_cfg.UnitCount = count + tool_ctl.update_mon_reqs() + main_pane.set_value(4) + else nu_error.show() end + end + + PushButton{parent=fac_c_2,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=fac_c_2,x=44,y=14,text="Next \x1a",callback=submit_num_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=fac_c_3,x=1,y=1,height=2,text="The following facility configuration was fetched from your supervisor computer."} + + local fac_config_list = ListBox{parent=fac_c_3,x=1,y=4,height=9,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + PushButton{parent=fac_c_3,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=fac_c_3,x=44,y=14,text="Next \x1a",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Tool and Helper Functions + + tool_ctl.is_int_min_max = is_int_min_max + + -- reset the connection display for a new attempt + function tool_ctl.init_sv_connect_ui() + self.sv_next.hide() + self.sv_skip.disable() + self.sv_skip.show() + self.sv_conn_button.enable() + self.sv_conn_status.set_value("") + self.sv_conn_detail.set_value("") + + -- the user needs to wait a few seconds, encouraging the to connect + tcd.dispatch_unique(2, function () self.sv_skip.enable() end) + end + + -- show the facility's unit count and cooling configuration data + function self.show_sv_cfg() + local conf = tool_ctl.sv_cool_conf + fac_config_list.remove_all() + + local str = util.sprintf("Facility has %d reactor unit%s:", #conf, tri(#conf==1,"","s")) + TextBox{parent=fac_config_list,text=str,fg_bg=cpair(colors.gray,colors.white)} + + for i = 1, #conf do + local num_b, num_t = conf[i][1], conf[i][2] + str = util.sprintf("\x07 Unit %d has %d boiler%s and %d turbine%s", i, num_b, tri(num_b == 1, "", "s"), num_t, tri(num_t == 1, "", "s")) + TextBox{parent=fac_config_list,text=str,fg_bg=cpair(colors.gray,colors.white)} + end + end + + --#endregion + + return fac_pane +end + +-- handle incoming modem messages +---@param side string +---@param sender integer +---@param reply_to integer +---@param message any +---@param distance integer +function facility.receive_sv(side, sender, reply_to, message, distance) + if self.nic ~= nil and self.net_listen then + local s_pkt = self.nic.receive(side, sender, reply_to, message, distance) + + if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then + local mgmt_pkt = comms.mgmt_packet() + if mgmt_pkt.decode(s_pkt) then + tcd.abort(handle_timeout) + handle_packet(mgmt_pkt.get()) + end + end + end +end + +return facility diff --git a/coordinator/config/hmi.lua b/coordinator/config/hmi.lua new file mode 100644 index 00000000..c913da15 --- /dev/null +++ b/coordinator/config/hmi.lua @@ -0,0 +1,450 @@ +local ppm = require("scada-common.ppm") +local types = require("scada-common.types") +local util = require("scada-common.util") + +local core = require("graphics.core") + +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") + +local Checkbox = require("graphics.elements.controls.Checkbox") +local PushButton = require("graphics.elements.controls.PushButton") +local RadioButton = require("graphics.elements.controls.RadioButton") + +local NumberField = require("graphics.elements.form.NumberField") + +local cpair = core.cpair + +local self = { + apply_mon = nil, ---@type PushButton + + edit_monitor = nil, ---@type function + + mon_iface = "", + mon_expect = {} ---@type integer[] +} + +local hmi = {} + +-- create the HMI (human machine interface) configuration view +---@param tool_ctl _crd_cfg_tool_ctl +---@param main_pane MultiPane +---@param cfg_sys [ crd_config, crd_config, crd_config, { [1]: string, [2]: string, [3]: any }[], function ] +---@param divs Div[] +---@param style { [string]: cpair } +---@return MultiPane mon_pane +function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style) + local _, ini_cfg, tmp_cfg, _, _ = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5] + local mon_cfg, spkr_cfg, crd_cfg = divs[1], divs[2], divs[3] + + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + + --#region Monitors + + local mon_c_1 = Div{parent=mon_cfg,x=2,y=4,width=49} + local mon_c_2 = Div{parent=mon_cfg,x=2,y=4,width=49} + local mon_c_3 = Div{parent=mon_cfg,x=2,y=4,width=49} + local mon_c_4 = Div{parent=mon_cfg,x=2,y=4,width=49} + + local mon_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={mon_c_1,mon_c_2,mon_c_3,mon_c_4}} + + TextBox{parent=mon_cfg,x=1,y=2,text=" Monitor Configuration",fg_bg=cpair(colors.black,colors.blue)} + + TextBox{parent=mon_c_1,x=1,y=1,height=5,text="Your configuration requires the following monitors. The main and flow monitors' heights are dependent on your unit count and cooling setup. If you manually entered the unit count, a * will be shown on potentially inaccurate calculations."} + local mon_reqs = ListBox{parent=mon_c_1,x=1,y=7,height=6,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local function next_from_reqs() + -- unassign unit monitors above the unit count + for i = tmp_cfg.UnitCount + 1, 4 do tmp_cfg.UnitDisplays[i] = nil end + + tool_ctl.gen_mon_list() + mon_pane.set_value(2) + end + + PushButton{parent=mon_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=mon_c_1,x=8,y=14,text="Legacy Options",min_width=16,callback=function()mon_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=mon_c_1,x=44,y=14,text="Next \x1a",callback=next_from_reqs,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=mon_c_2,x=1,y=1,height=5,text="Please configure your monitors below. You can go back to the prior page without losing progress to double check what you need. All of those monitors must be assigned before you can proceed."} + + local mon_list = ListBox{parent=mon_c_2,x=1,y=6,height=7,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local assign_err = TextBox{parent=mon_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_monitors() + if tmp_cfg.MainDisplay == nil then + assign_err.set_value("Please assign the main monitor.") + elseif tmp_cfg.FlowDisplay == nil and not tmp_cfg.DisableFlowView then + assign_err.set_value("Please assign the flow monitor.") + elseif util.table_len(tmp_cfg.UnitDisplays) ~= tmp_cfg.UnitCount then + for i = 1, tmp_cfg.UnitCount do + if tmp_cfg.UnitDisplays[i] == nil then + assign_err.set_value("Please assign the unit " .. i .. " monitor.") + break + end + end + else + assign_err.hide(true) + main_pane.set_value(5) + return + end + + assign_err.show() + end + + PushButton{parent=mon_c_2,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=mon_c_2,x=44,y=14,text="Next \x1a",callback=submit_monitors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + local mon_desc = TextBox{parent=mon_c_3,x=1,y=1,height=4,text=""} + + local mon_unit_l, mon_unit = nil, nil ---@type TextBox, NumberField + + local mon_warn = TextBox{parent=mon_c_3,x=1,y=11,height=2,text="",fg_bg=cpair(colors.red,colors.lightGray)} + + ---@param val integer assignment type + local function on_assign_mon(val) + if val == 2 and tmp_cfg.DisableFlowView then + self.apply_mon.disable() + mon_warn.set_value("You disabled having a flow view monitor. It can't be set unless you go back and enable it.") + mon_warn.show() + elseif not util.table_contains(self.mon_expect, val) then + self.apply_mon.disable() + mon_warn.set_value("That assignment doesn't fit monitor dimensions. You'll need to resize the monitor for it to work.") + mon_warn.show() + else + self.apply_mon.enable() + mon_warn.hide(true) + end + + if val == 3 then + mon_unit_l.show() + mon_unit.show() + else + mon_unit_l.hide(true) + mon_unit.hide(true) + end + + local value = mon_unit.get_value() + mon_unit.set_max(tmp_cfg.UnitCount) + if value == "0" or value == nil then mon_unit.set_value(0) end + end + + TextBox{parent=mon_c_3,x=1,y=6,width=10,text="Assignment"} + local mon_assign = RadioButton{parent=mon_c_3,x=1,y=7,default=1,options={"Main Monitor","Flow Monitor","Unit Monitor"},callback=on_assign_mon,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.blue} + + mon_unit_l = TextBox{parent=mon_c_3,x=18,y=6,width=7,text="Unit ID"} + mon_unit = NumberField{parent=mon_c_3,x=18,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg} + + local mon_u_err = TextBox{parent=mon_c_3,x=8,y=14,width=35,text="Please provide a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + -- purge all assignments for a given monitor + ---@param iface string + local function purge_assignments(iface) + if tmp_cfg.MainDisplay == iface then + tmp_cfg.MainDisplay = nil + elseif tmp_cfg.FlowDisplay == iface then + tmp_cfg.FlowDisplay = nil + else + for i = 1, tmp_cfg.UnitCount do + if tmp_cfg.UnitDisplays[i] == iface then tmp_cfg.UnitDisplays[i] = nil end + end + end + end + + local function apply_monitor() + local iface = self.mon_iface + local type = mon_assign.get_value() + local u_id = tonumber(mon_unit.get_value()) + + if type == 1 then + purge_assignments(iface) + tmp_cfg.MainDisplay = iface + elseif type == 2 then + purge_assignments(iface) + tmp_cfg.FlowDisplay = iface + elseif u_id and u_id > 0 then + purge_assignments(iface) + tmp_cfg.UnitDisplays[u_id] = iface + else + mon_u_err.show() + return + end + + tool_ctl.gen_mon_list() + mon_u_err.hide(true) + mon_pane.set_value(2) + end + + PushButton{parent=mon_c_3,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + self.apply_mon = PushButton{parent=mon_c_3,x=43,y=14,min_width=7,text="Apply",callback=apply_monitor,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + + TextBox{parent=mon_c_4,x=1,y=1,height=3,text="For legacy compatibility with facilities built without space for a flow monitor, you can disable the flow monitor requirement here."} + TextBox{parent=mon_c_4,x=1,y=5,height=3,text="Please be aware that THIS OPTION WILL BE REMOVED ON RELEASE. Disabling it will only be available for the remainder of the beta."} + + tool_ctl.dis_flow_view = Checkbox{parent=mon_c_4,x=1,y=9,default=ini_cfg.DisableFlowView,label="Disable Flow View Monitor",box_fg_bg=cpair(colors.blue,colors.black)} + + local function back_from_legacy() + tmp_cfg.DisableFlowView = tool_ctl.dis_flow_view.get_value() + tool_ctl.update_mon_reqs() + mon_pane.set_value(1) + end + + PushButton{parent=mon_c_4,x=44,y=14,min_width=6,text="Done",callback=back_from_legacy,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Speaker + + local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49} + + TextBox{parent=spkr_cfg,x=1,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)} + + TextBox{parent=spkr_c,x=1,y=1,height=2,text="The coordinator uses a speaker to play alarm sounds."} + TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."} + + tool_ctl.s_vol = NumberField{parent=spkr_c,x=1,y=8,width=9,max_chars=7,allow_decimal=true,default=ini_cfg.SpeakerVolume,min=0,max=3,fg_bg=bw_fg_bg} + + TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg} + + local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_vol() + local vol = tonumber(tool_ctl.s_vol.get_value()) + if vol ~= nil then + s_vol_err.hide(true) + tmp_cfg.SpeakerVolume = vol + main_pane.set_value(6) + else s_vol_err.show() end + end + + PushButton{parent=spkr_c,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=spkr_c,x=44,y=14,text="Next \x1a",callback=submit_vol,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Coordinator UI + + local crd_c_1 = Div{parent=crd_cfg,x=2,y=4,width=49} + + TextBox{parent=crd_cfg,x=1,y=2,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)} + + TextBox{parent=crd_c_1,x=1,y=1,height=3,text="Configure the UI interface options below if you wish to customize formats."} + + TextBox{parent=crd_c_1,x=1,y=4,text="Clock Time Format"} + tool_ctl.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,text="Temperature Scale"} + tool_ctl.temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} + + TextBox{parent=crd_c_1,x=24,y=8,text="Energy Scale"} + tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=24,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} + + local function submit_ui_opts() + tmp_cfg.Time24Hour = tool_ctl.clock_fmt.get_value() == 1 + tmp_cfg.TempScale = tool_ctl.temp_scale.get_value() + tmp_cfg.EnergyScale = tool_ctl.energy_scale.get_value() + main_pane.set_value(7) + end + + PushButton{parent=crd_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=crd_c_1,x=44,y=14,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Tool and Helper Functions + + -- update list of monitor requirements + function tool_ctl.update_mon_reqs() + local plural = tmp_cfg.UnitCount > 1 + + if tool_ctl.sv_cool_conf ~= nil then + local cnf = tool_ctl.sv_cool_conf + + local row1_tall = cnf[1][1] > 1 or cnf[1][2] > 2 or (cnf[2] and (cnf[2][1] > 1 or cnf[2][2] > 2)) + local row1_short = (cnf[1][1] == 0 and cnf[1][2] == 1) and (cnf[2] == nil or (cnf[2][1] == 0 and cnf[2][2] == 1)) + local row2_tall = (cnf[3] and (cnf[3][1] > 1 or cnf[3][2] > 2)) or (cnf[4] and (cnf[4][1] > 1 or cnf[4][2] > 2)) + local row2_short = (cnf[3] == nil or (cnf[3][1] == 0 and cnf[3][2] == 1)) and (cnf[4] == nil or (cnf[4][1] == 0 and cnf[4][2] == 1)) + + if tmp_cfg.UnitCount <= 2 then + tool_ctl.main_mon_h = util.trinary(row1_tall, 5, 4) + else + -- is only one tall and the other short, or are both tall? -> 5 or 6; are neither tall? -> 5 + if row1_tall or row2_tall then + tool_ctl.main_mon_h = util.trinary((row1_short and row2_tall) or (row1_tall and row2_short), 5, 6) + else tool_ctl.main_mon_h = 5 end + end + else + tool_ctl.main_mon_h = util.trinary(tmp_cfg.UnitCount <= 2, 4, 5) + end + + tool_ctl.flow_mon_h = 2 + tmp_cfg.UnitCount + + local asterisk = util.trinary(tool_ctl.sv_cool_conf == nil, "*", "") + local m_at_least = util.trinary(tool_ctl.main_mon_h < 6, "at least ", "") + local f_at_least = util.trinary(tool_ctl.flow_mon_h < 6, "at least ", "") + + mon_reqs.remove_all() + + TextBox{parent=mon_reqs,x=1,y=1,text="\x1a "..tmp_cfg.UnitCount.." Unit View Monitor"..util.trinary(plural,"s","")} + TextBox{parent=mon_reqs,x=1,y=1,text=" "..util.trinary(plural,"each ","").."must be 4 blocks wide by 4 tall",fg_bg=cpair(colors.gray,colors.white)} + TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Main View Monitor"} + TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..m_at_least..tool_ctl.main_mon_h..asterisk.." tall",fg_bg=cpair(colors.gray,colors.white)} + if not tmp_cfg.DisableFlowView then + TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Flow View Monitor"} + TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..f_at_least..tool_ctl.flow_mon_h.." tall",fg_bg=cpair(colors.gray,colors.white)} + end + end + + -- set/edit a monitor's assignment + ---@param iface string + ---@param device ppm_entry + function self.edit_monitor(iface, device) + self.mon_iface = iface + + local dev = device.dev + local w, h = ppm.monitor_block_size(dev.getSize()) + + local msg = "This size doesn't match a required screen. Please go back and resize it, or configure below at the risk of it not working." + + self.mon_expect = {} + mon_assign.set_value(1) + mon_unit.set_value(0) + + if w == 4 and h == 4 then + msg = "This could work as a unit display. Please configure below." + self.mon_expect = { 3 } + mon_assign.set_value(3) + elseif w == 8 then + if h >= tool_ctl.main_mon_h and h >= tool_ctl.flow_mon_h then + msg = "This could work as either your main monitor or flow monitor. Please configure below." + self.mon_expect = { 1, 2 } + if tmp_cfg.MainDisplay then mon_assign.set_value(2) end + elseif h >= tool_ctl.main_mon_h then + msg = "This could work as your main monitor. Please configure below." + self.mon_expect = { 1 } + elseif h >= tool_ctl.flow_mon_h then + msg = "This could work as your flow monitor. Please configure below." + self.mon_expect = { 2 } + mon_assign.set_value(2) + end + end + + -- override if a config exists + if tmp_cfg.MainDisplay == iface then + mon_assign.set_value(1) + elseif tmp_cfg.FlowDisplay == iface then + mon_assign.set_value(2) + else + for i = 1, tmp_cfg.UnitCount do + if tmp_cfg.UnitDisplays[i] == iface then + mon_assign.set_value(3) + mon_unit.set_value(i) + break + end + end + end + + on_assign_mon(mon_assign.get_value()) + + mon_desc.set_value(util.c("You have selected '", iface, "', which has a block size of ", w, " wide by ", h, " tall. ", msg)) + mon_pane.set_value(3) + end + + -- generate the list of available monitors + function tool_ctl.gen_mon_list() + mon_list.remove_all() + + local missing = { main = tmp_cfg.MainDisplay ~= nil, flow = tmp_cfg.FlowDisplay ~= nil, unit = {} } + for i = 1, tmp_cfg.UnitCount do missing.unit[i] = tmp_cfg.UnitDisplays[i] ~= nil end + + -- list connected monitors + local monitors = ppm.get_monitor_list() + for iface, device in pairs(monitors) do + local dev = device.dev ---@type Monitor + + dev.setTextScale(0.5) + dev.setTextColor(colors.white) + dev.setBackgroundColor(colors.black) + dev.clear() + dev.setCursorPos(1, 1) + dev.setTextColor(colors.magenta) + dev.write("This is monitor") + dev.setCursorPos(1, 2) + dev.setTextColor(colors.white) + dev.write(iface) + + local assignment = "Unused" + + if tmp_cfg.MainDisplay == iface then + assignment = "Main" + missing.main = false + elseif tmp_cfg.FlowDisplay == iface then + assignment = "Flow" + missing.flow = false + else + for i = 1, tmp_cfg.UnitCount do + if tmp_cfg.UnitDisplays[i] == iface then + missing.unit[i] = false + assignment = "Unit " .. i + break + end + end + end + + local line = Div{parent=mon_list,x=1,y=1,height=1} + + TextBox{parent=line,x=1,y=1,width=6,text=assignment,fg_bg=cpair(util.trinary(assignment=="Unused",colors.red,colors.blue),colors.white)} + TextBox{parent=line,x=8,y=1,text=iface} + + local w, h = ppm.monitor_block_size(dev.getSize()) + + local function unset_mon() + purge_assignments(iface) + tool_ctl.gen_mon_list() + end + + TextBox{parent=line,x=33,y=1,width=4,text=w.."x"..h,fg_bg=cpair(colors.black,colors.white)} + PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()self.edit_monitor(iface,device)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} + local unset = PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)} + + if assignment == "Unused" then unset.disable() end + end + + local dc_list = {} -- disconnected monitor list + + if missing.main then table.insert(dc_list, { "Main", tmp_cfg.MainDisplay }) end + if missing.flow then table.insert(dc_list, { "Flow", tmp_cfg.FlowDisplay }) end + for i = 1, tmp_cfg.UnitCount do + if missing.unit[i] then table.insert(dc_list, { "Unit " .. i, tmp_cfg.UnitDisplays[i] }) end + end + + -- add monitors that are assigned but not connected + for i = 1, #dc_list do + local line = Div{parent=mon_list,x=1,y=1,height=1} + + TextBox{parent=line,x=1,y=1,width=6,text=dc_list[i][1],fg_bg=cpair(colors.blue,colors.white)} + TextBox{parent=line,x=8,y=1,text="disconnected",fg_bg=cpair(colors.red,colors.white)} + + local function unset_mon() + purge_assignments(dc_list[i][2]) + tool_ctl.gen_mon_list() + end + + TextBox{parent=line,x=33,y=1,width=4,text="?x?",fg_bg=cpair(colors.black,colors.white)} + PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()end,dis_fg_bg=cpair(colors.black,colors.gray)}.disable() + PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)} + end + end + + --#endregion + + return mon_pane +end + +return hmi diff --git a/coordinator/config/system.lua b/coordinator/config/system.lua new file mode 100644 index 00000000..3925114b --- /dev/null +++ b/coordinator/config/system.lua @@ -0,0 +1,577 @@ + +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local network = require("scada-common.network") +local types = require("scada-common.types") +local util = require("scada-common.util") + +local core = require("graphics.core") +local themes = require("graphics.themes") + +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") + +local Checkbox = require("graphics.elements.controls.Checkbox") +local PushButton = require("graphics.elements.controls.PushButton") +local RadioButton = require("graphics.elements.controls.RadioButton") + +local NumberField = require("graphics.elements.form.NumberField") +local TextField = require("graphics.elements.form.TextField") + +local IndLight = require("graphics.elements.indicators.IndicatorLight") + +local tri = util.trinary + +local cpair = core.cpair + +local RIGHT = core.ALIGN.RIGHT + +local self = { + importing_legacy = false, + + show_auth_key = nil, ---@type function + show_key_btn = nil, ---@type PushButton + auth_key_textbox = nil, ---@type TextBox + auth_key_value = "" +} + +local system = {} + +-- create the system configuration view +---@param tool_ctl _crd_cfg_tool_ctl +---@param main_pane MultiPane +---@param cfg_sys [ crd_config, crd_config, crd_config, { [1]: string, [2]: string, [3]: any }[], function ] +---@param divs Div[] +---@param ext [ MultiPane, MultiPane, function, function ] +---@param style { [string]: cpair } +function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style) + local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5] + local net_cfg, log_cfg, clr_cfg, summary = divs[1], divs[2], divs[3], divs[4] + local fac_pane, mon_pane, preset_monitor_fields, exit = ext[1], ext[2], ext[3], ext[4] + + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + + --#region Network + + local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49} + local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49} + local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49} + local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49} + + local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}} + + TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} + + TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."} + TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"} + local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"} + local crd_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"} + local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg} + + local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_channels() + local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value()) + if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then + tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c + net_pane.set_value(2) + chan_err.hide(true) + else chan_err.show() end + end + + PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_2,x=1,y=1,text="Please set the connection timeouts below."} + TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_2,x=1,y=8,width=19,text="Supervisor Timeout"} + local svr_timeout = NumberField{parent=net_c_2,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} + + TextBox{parent=net_c_2,x=1,y=10,width=14,text="Pocket Timeout"} + local api_timeout = NumberField{parent=net_c_2,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} + + TextBox{parent=net_c_2,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg} + + local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_timeouts() + local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(api_timeout.get_value()) + if svr_cto ~= nil and api_cto ~= nil then + tmp_cfg.SVR_Timeout, tmp_cfg.API_Timeout = svr_cto, api_cto + net_pane.set_value(3) + ct_err.hide(true) + else ct_err.show() end + end + + PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."} + TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg} + TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg} + + local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg} + + local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_tr() + local range_val = tonumber(range.get_value()) + if range_val ~= nil then + tmp_cfg.TrustedRange = range_val + comms.set_trusted_range(range_val) + net_pane.set_value(4) + tr_err.hide(true) + else tr_err.show() end + end + + PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} + TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_4,x=1,y=11,text="Facility Auth Key"} + local key, _ = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg} + + local function censor_key(enable) key.censor(tri(enable, "*", nil)) end + + local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} + + hide_key.set_value(true) + censor_key(true) + + local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_auth() + local v = key.get_value() + if string.len(v) == 0 or string.len(v) >= 8 then + tmp_cfg.AuthKey = key.get_value() + key_err.hide(true) + + -- init mac for supervisor connection + if string.len(v) >= 8 then network.init_mac(tmp_cfg.AuthKey) else network.deinit_mac() end + + -- prep supervisor connection screen + tool_ctl.init_sv_connect_ui() + + main_pane.set_value(3) + else key_err.show() end + end + + PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Logging + + local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} + + TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} + + TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."} + + TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} + local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} + + TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"} + local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg} + + local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} + TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg} + + local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_log() + if path.get_value() ~= "" then + path_err.hide(true) + tmp_cfg.LogMode = mode.get_value() - 1 + tmp_cfg.LogPath = path.get_value() + tmp_cfg.LogDebug = en_dbg.get_value() + tool_ctl.color_apply.hide(true) + tool_ctl.color_next.show() + main_pane.set_value(8) + else path_err.show() end + end + + PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Color Options + + local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49} + local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49} + local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49} + local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49} + + local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}} + + TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)} + + TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color themes for the different UI displays."} + TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg} + + TextBox{parent=clr_c_1,x=1,y=7,text="Main UI Theme"} + local main_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} + + TextBox{parent=clr_c_1,x=18,y=7,text="Front Panel Theme"} + local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} + + TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will usually be split up."} + + TextBox{parent=clr_c_2,x=21,y=7,text="Preview"} + local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)} + _ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)} + _ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)} + local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true} + local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true} + + local function recolor(value) + local c = themes.smooth_stone.color_modes[value] + + if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then + b_off.hide() + g_off.show() + else + g_off.hide() + b_off.show() + end + + if #c == 0 then + for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end + else + term.setPaletteColor(colors.green, c[1].hex) + term.setPaletteColor(colors.yellow, c[2].hex) + term.setPaletteColor(colors.red, c[3].hex) + end + end + + TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"} + local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} + + TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg} + + PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + local function back_from_colors() + main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 7)) + tool_ctl.jumped_to_color = false + recolor(1) + end + + local function show_access() + clr_pane.set_value(2) + recolor(c_mode.get_value()) + end + + local function submit_colors() + tmp_cfg.MainTheme = main_theme.get_value() + tmp_cfg.FrontPanelTheme = fp_theme.get_value() + tmp_cfg.ColorMode = c_mode.get_value() + + if tool_ctl.jumped_to_color then + settings.set("MainTheme", tmp_cfg.MainTheme) + settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme) + settings.set("ColorMode", tmp_cfg.ColorMode) + + if settings.save("/coordinator.settings") then + load_settings(settings_cfg, true) + load_settings(ini_cfg) + clr_pane.set_value(3) + else + clr_pane.set_value(4) + end + else + tool_ctl.gen_summary(tmp_cfg) + tool_ctl.viewing_config = false + self.importing_legacy = false + tool_ctl.settings_apply.show() + main_pane.set_value(9) + end + end + + PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + + tool_ctl.color_apply.hide(true) + + local function c_go_home() + main_pane.set_value(1) + clr_pane.set_value(1) + end + + TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"} + PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} + PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Summary and Saving + + local sum_c_1 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_2 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_3 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_4 = Div{parent=summary,x=2,y=4,width=49} + + local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}} + + TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)} + + local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local function back_from_summary() + if tool_ctl.viewing_config or self.importing_legacy then + main_pane.set_value(1) + tool_ctl.viewing_config = false + self.importing_legacy = false + tool_ctl.settings_apply.show() + else + main_pane.set_value(8) + end + end + + ---@param element graphics_element + ---@param data any + local function try_set(element, data) + if data ~= nil then element.set_value(data) end + end + + local function save_and_continue() + for _, field in ipairs(fields) do + local k, v = field[1], tmp_cfg[field[1]] + if v == nil then settings.unset(k) else settings.set(k, v) end + end + + if settings.save("/coordinator.settings") then + load_settings(settings_cfg, true) + load_settings(ini_cfg) + + try_set(svr_chan, ini_cfg.SVR_Channel) + try_set(crd_chan, ini_cfg.CRD_Channel) + try_set(pkt_chan, ini_cfg.PKT_Channel) + try_set(svr_timeout, ini_cfg.SVR_Timeout) + try_set(api_timeout, ini_cfg.API_Timeout) + try_set(range, ini_cfg.TrustedRange) + try_set(key, ini_cfg.AuthKey) + try_set(tool_ctl.num_units, ini_cfg.UnitCount) + try_set(tool_ctl.dis_flow_view, ini_cfg.DisableFlowView) + try_set(tool_ctl.s_vol, ini_cfg.SpeakerVolume) + try_set(tool_ctl.clock_fmt, tri(ini_cfg.Time24Hour, 1, 2)) + try_set(tool_ctl.temp_scale, ini_cfg.TempScale) + try_set(tool_ctl.energy_scale, ini_cfg.EnergyScale) + try_set(mode, ini_cfg.LogMode) + try_set(path, ini_cfg.LogPath) + try_set(en_dbg, ini_cfg.LogDebug) + try_set(main_theme, ini_cfg.MainTheme) + try_set(fp_theme, ini_cfg.FrontPanelTheme) + try_set(c_mode, ini_cfg.ColorMode) + + preset_monitor_fields() + + tool_ctl.gen_mon_list() + + tool_ctl.view_cfg.enable() + tool_ctl.color_cfg.enable() + + if self.importing_legacy then + self.importing_legacy = false + sum_pane.set_value(3) + else + sum_pane.set_value(2) + end + else + sum_pane.set_value(4) + end + end + + PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + self.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + + TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"} + + local function go_home() + main_pane.set_value(1) + net_pane.set_value(1) + fac_pane.set_value(1) + mon_pane.set_value(1) + clr_pane.set_value(1) + sum_pane.set_value(1) + end + + PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + + TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua and coord.settings files will now be deleted, then the configurator will exit."} + + local function delete_legacy() + fs.delete("/coordinator/config.lua") + fs.delete("/coord.settings") + exit() + end + + PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)} + + TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} + PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + + --#endregion + + --#region Tool Functions + + -- load a legacy config file + function tool_ctl.load_legacy() + local config = require("coordinator.config") + + tmp_cfg.SVR_Channel = config.SVR_CHANNEL + tmp_cfg.CRD_Channel = config.CRD_CHANNEL + tmp_cfg.PKT_Channel = config.PKT_CHANNEL + tmp_cfg.SVR_Timeout = config.SV_TIMEOUT + tmp_cfg.API_Timeout = config.API_TIMEOUT + tmp_cfg.TrustedRange = config.TRUSTED_RANGE + tmp_cfg.AuthKey = config.AUTH_KEY or "" + + tmp_cfg.UnitCount = config.NUM_UNITS + tmp_cfg.DisableFlowView = config.DISABLE_FLOW_VIEW + tmp_cfg.SpeakerVolume = config.SOUNDER_VOLUME + tmp_cfg.Time24Hour = config.TIME_24_HOUR + + tmp_cfg.LogMode = config.LOG_MODE + tmp_cfg.LogPath = config.LOG_PATH + tmp_cfg.LogDebug = config.LOG_DEBUG or false + + settings.load("/coord.settings") + + tmp_cfg.MainDisplay = settings.get("PRIMARY_DISPLAY") + tmp_cfg.FlowDisplay = settings.get("FLOW_DISPLAY") + tmp_cfg.UnitDisplays = settings.get("UNIT_DISPLAYS", {}) + + -- if there are extra monitor entries, delete them now + -- not doing so will cause the app to fail to start + if tool_ctl.is_int_min_max(tmp_cfg.UnitCount, 1, 4) then + for i = tmp_cfg.UnitCount + 1, 4 do tmp_cfg.UnitDisplays[i] = nil end + end + + if settings.get("ControlStates") == nil then + local ctrl_states = { + process = settings.get("PROCESS"), + waste_modes = settings.get("WASTE_MODES"), + priority_groups = settings.get("PRIORITY_GROUPS"), + } + + settings.set("ControlStates", ctrl_states) + end + + settings.unset("PRIMARY_DISPLAY") + settings.unset("FLOW_DISPLAY") + settings.unset("UNIT_DISPLAYS") + settings.unset("PROCESS") + settings.unset("WASTE_MODES") + settings.unset("PRIORITY_GROUPS") + + tool_ctl.gen_summary(tmp_cfg) + sum_pane.set_value(1) + main_pane.set_value(9) + self.importing_legacy = true + end + + -- expose the auth key on the summary page + function self.show_auth_key() + self.show_key_btn.disable() + self.auth_key_textbox.set_value(self.auth_key_value) + end + + -- generate the summary list + ---@param cfg crd_config + function tool_ctl.gen_summary(cfg) + setting_list.remove_all() + + local alternate = false + local inner_width = setting_list.get_width() - 1 + + self.show_key_btn.enable() + self.auth_key_value = cfg.AuthKey or "" -- to show auth key + + for i = 1, #fields do + local f = fields[i] + local height = 1 + local label_w = string.len(f[2]) + local val_max_w = (inner_width - label_w) + 1 + local raw = cfg[f[1]] + local val = util.strval(raw) + + if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) + elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") + elseif f[1] == "TempScale" then + val = util.strval(types.TEMP_SCALE_NAMES[raw]) + elseif f[1] == "EnergyScale" then + val = util.strval(types.ENERGY_SCALE_NAMES[raw]) + elseif f[1] == "MainTheme" then + val = util.strval(themes.ui_theme_name(raw)) + elseif f[1] == "FrontPanelTheme" then + val = util.strval(themes.fp_theme_name(raw)) + elseif f[1] == "ColorMode" then + val = util.strval(themes.color_mode_name(raw)) + elseif f[1] == "UnitDisplays" and type(cfg.UnitDisplays) == "table" then + val = "" + for idx = 1, #cfg.UnitDisplays do + val = val .. util.trinary(idx == 1, "", "\n") .. util.sprintf(" \x07 Unit %d - %s", idx, cfg.UnitDisplays[idx]) + end + end + + if val == "nil" then val = "" end + + local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) + alternate = not alternate + + if string.len(val) > val_max_w then + local lines = util.strwrap(val, inner_width) + height = #lines + 1 + end + + if (f[1] == "UnitDisplays") and (height == 1) and (val ~= "") then height = 2 end + + local line = Div{parent=setting_list,height=height,fg_bg=c} + TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} + + local textbox + if height > 1 then + textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1} + else + textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT} + end + + if f[1] == "AuthKey" then self.auth_key_textbox = textbox end + end + end + + --#endregion +end + +return system diff --git a/coordinator/configure.lua b/coordinator/configure.lua index 31d359c9..3c9bfe79 100644 --- a/coordinator/configure.lua +++ b/coordinator/configure.lua @@ -2,44 +2,33 @@ -- Configuration GUI -- -local comms = require("scada-common.comms") -local log = require("scada-common.log") -local network = require("scada-common.network") -local ppm = require("scada-common.ppm") -local tcd = require("scada-common.tcd") -local types = require("scada-common.types") -local util = require("scada-common.util") +local log = require("scada-common.log") +local ppm = require("scada-common.ppm") +local tcd = require("scada-common.tcd") +local types = require("scada-common.types") +local util = require("scada-common.util") -local core = require("graphics.core") -local themes = require("graphics.themes") +local facility = require("coordinator.config.facility") +local hmi = require("coordinator.config.hmi") +local system = require("coordinator.config.system") -local DisplayBox = require("graphics.elements.displaybox") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") +local core = require("graphics.core") +local themes = require("graphics.themes") -local CheckBox = require("graphics.elements.controls.checkbox") -local PushButton = require("graphics.elements.controls.push_button") -local RadioButton = require("graphics.elements.controls.radio_button") +local DisplayBox = require("graphics.elements.DisplayBox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local NumberField = require("graphics.elements.form.number_field") -local TextField = require("graphics.elements.form.text_field") - -local IndLight = require("graphics.elements.indicators.light") +local PushButton = require("graphics.elements.controls.PushButton") local println = util.println local tri = util.trinary -local PROTOCOL = comms.PROTOCOL -local DEVICE_TYPE = comms.DEVICE_TYPE -local ESTABLISH_ACK = comms.ESTABLISH_ACK -local MGMT_TYPE = comms.MGMT_TYPE - local cpair = core.cpair local CENTER = core.ALIGN.CENTER -local RIGHT = core.ALIGN.RIGHT -- changes to the config data/format to let the user know local changes = { @@ -54,63 +43,50 @@ local configurator = {} local style = {} -style.root = cpair(colors.black, colors.lightGray) -style.header = cpair(colors.white, colors.gray) +style.root = cpair(colors.black, colors.lightGray) +style.header = cpair(colors.white, colors.gray) -style.colors = themes.smooth_stone.colors +style.colors = themes.smooth_stone.colors -local bw_fg_bg = cpair(colors.black, colors.white) -local g_lg_fg_bg = cpair(colors.gray, colors.lightGray) -local nav_fg_bg = bw_fg_bg -local btn_act_fg_bg = cpair(colors.white, colors.gray) -local dis_fg_bg = cpair(colors.lightGray,colors.white) +style.bw_fg_bg = cpair(colors.black, colors.white) +style.g_lg_fg_bg = cpair(colors.gray, colors.lightGray) +style.nav_fg_bg = style.bw_fg_bg +style.btn_act_fg_bg = cpair(colors.white, colors.gray) +style.btn_dis_fg_bg = cpair(colors.lightGray,colors.white) ---@class _crd_cfg_tool_ctl local tool_ctl = { - nic = nil, ---@type nic - net_listen = false, - sv_addr = comms.BROADCAST, - sv_seq_num = util.time_ms() * 10, - sv_cool_conf = nil, ---@type table list of boiler & turbine counts - show_sv_cfg = nil, ---@type function + sv_cool_conf = nil, ---@type [ integer, integer ][] list of boiler & turbine counts start_fail = 0, fail_message = "", has_config = false, viewing_config = false, - importing_legacy = false, jumped_to_color = false, - view_cfg = nil, ---@type graphics_element - color_cfg = nil, ---@type graphics_element - color_next = nil, ---@type graphics_element - color_apply = nil, ---@type graphics_element - settings_apply = nil, ---@type graphics_element - - gen_summary = nil, ---@type function - show_current_cfg = nil, ---@type function - load_legacy = nil, ---@type function - - show_auth_key = nil, ---@type function - show_key_btn = nil, ---@type graphics_element - auth_key_textbox = nil, ---@type graphics_element - auth_key_value = "", - - sv_connect = nil, ---@type function - sv_conn_button = nil, ---@type graphics_element - sv_conn_status = nil, ---@type graphics_element - sv_conn_detail = nil, ---@type graphics_element - sv_skip = nil, ---@type graphics_element - sv_next = nil, ---@type graphics_element - - apply_mon = nil, ---@type graphics_element - - update_mon_reqs = nil, ---@type function - gen_mon_list = function () end, - edit_monitor = nil, ---@type function - - mon_iface = "", - mon_expect = {} + view_cfg = nil, ---@type PushButton + color_cfg = nil, ---@type PushButton + color_next = nil, ---@type PushButton + color_apply = nil, ---@type PushButton + settings_apply = nil, ---@type PushButton + + gen_summary = nil, ---@type function + load_legacy = nil, ---@type function + + -- settings elements from hmi + dis_flow_view = nil, ---@type Checkbox + s_vol = nil, ---@type NumberField + clock_fmt = nil, ---@type RadioButton + temp_scale = nil, ---@type RadioButton + energy_scale = nil, ---@type RadioButton + + -- settings elements and functions from facility + num_units = nil, ---@type NumberField + init_sv_connect_ui = nil, ---@type function + is_int_min_max = nil, ---@type function + + update_mon_reqs = nil, ---@type function + gen_mon_list = function () end } ---@class crd_config @@ -118,25 +94,25 @@ local tmp_cfg = { UnitCount = 1, SpeakerVolume = 1.0, Time24Hour = true, - TempScale = 1, - EnergyScale = 1, + TempScale = 1, ---@type TEMP_SCALE + EnergyScale = 1, ---@type ENERGY_SCALE DisableFlowView = false, - MainDisplay = nil, ---@type string - FlowDisplay = nil, ---@type string - UnitDisplays = {}, - SVR_Channel = nil, ---@type integer - CRD_Channel = nil, ---@type integer - PKT_Channel = nil, ---@type integer - SVR_Timeout = nil, ---@type number - API_Timeout = nil, ---@type number - TrustedRange = nil, ---@type number - AuthKey = nil, ---@type string|nil - LogMode = 0, + MainDisplay = nil, ---@type string + FlowDisplay = nil, ---@type string + UnitDisplays = {}, ---@type string[] + SVR_Channel = nil, ---@type integer + CRD_Channel = nil, ---@type integer + PKT_Channel = nil, ---@type integer + SVR_Timeout = nil, ---@type number + API_Timeout = nil, ---@type number + TrustedRange = nil, ---@type number + AuthKey = nil, ---@type string|nil + LogMode = 0, ---@type LOG_MODE LogPath = "", LogDebug = false, - MainTheme = 1, - FrontPanelTheme = 1, - ColorMode = 1 + MainTheme = 1, ---@type UI_THEME + FrontPanelTheme = 1, ---@type FP_THEME + ColorMode = 1 ---@type COLOR_MODE } ---@class crd_config @@ -170,113 +146,6 @@ local fields = { { "ColorMode", "Color Mode", themes.COLOR_MODE.STANDARD } } --- check if a value is an integer within a range (inclusive) ----@param x integer ----@param min integer ----@param max integer -local function is_int_min_max(x, min, max) return util.is_int(x) and x >= min and x <= max end - --- send a management packet to the supervisor ----@param msg_type MGMT_TYPE ----@param msg table -local function send_sv(msg_type, msg) - local s_pkt = comms.scada_packet() - local pkt = comms.mgmt_packet() - - pkt.make(msg_type, msg) - s_pkt.make(tool_ctl.sv_addr, tool_ctl.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) - - tool_ctl.nic.transmit(tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, s_pkt) - tool_ctl.sv_seq_num = tool_ctl.sv_seq_num + 1 -end - --- handle an establish message from the supervisor ----@param packet mgmt_frame -local function handle_packet(packet) - local error_msg = nil - - if packet.scada_frame.local_channel() ~= tmp_cfg.CRD_Channel then - error_msg = "Error: unknown receive channel." - elseif packet.scada_frame.remote_channel() == tmp_cfg.SVR_Channel and packet.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then - if packet.type == MGMT_TYPE.ESTABLISH then - if packet.length == 2 then - local est_ack = packet.data[1] - local config = packet.data[2] - - if est_ack == ESTABLISH_ACK.ALLOW then - if type(config) == "table" and #config == 2 then - local count_ok = is_int_min_max(config[1], 1, 4) - local cool_ok = type(config[2]) == "table" and type(config[2].r_cool) == "table" and #config[2].r_cool == config[1] - - if count_ok and cool_ok then - tmp_cfg.UnitCount = config[1] - tool_ctl.sv_cool_conf = {} - - for i = 1, tmp_cfg.UnitCount do - local num_b = config[2].r_cool[i].BoilerCount - local num_t = config[2].r_cool[i].TurbineCount - tool_ctl.sv_cool_conf[i] = { num_b, num_t } - cool_ok = cool_ok and is_int_min_max(num_b, 0, 2) and is_int_min_max(num_t, 1, 3) - end - end - - if not count_ok then - error_msg = "Error: supervisor unit count out of range." - elseif not cool_ok then - error_msg = "Error: supervisor cooling configuration malformed." - tool_ctl.sv_cool_conf = nil - end - - tool_ctl.sv_addr = packet.scada_frame.src_addr() - send_sv(MGMT_TYPE.CLOSE, {}) - else - error_msg = "Error: invalid cooling configuration supervisor." - end - else - error_msg = "Error: invalid allow reply length from supervisor." - end - elseif packet.length == 1 then - local est_ack = packet.data[1] - - if est_ack == ESTABLISH_ACK.DENY then - error_msg = "Error: supervisor connection denied." - elseif est_ack == ESTABLISH_ACK.COLLISION then - error_msg = "Error: a coordinator is already/still connected. Please try again." - elseif est_ack == ESTABLISH_ACK.BAD_VERSION then - error_msg = "Error: coordinator comms version does not match supervisor comms version." - else - error_msg = "Error: invalid reply from supervisor." - end - else - error_msg = "Error: invalid reply length from supervisor." - end - else - error_msg = "Error: didn't get an establish reply from supervisor." - end - end - - tool_ctl.net_listen = false - - if error_msg then - tool_ctl.sv_conn_status.set_value("") - tool_ctl.sv_conn_detail.set_value(error_msg) - tool_ctl.sv_conn_button.enable() - else - tool_ctl.sv_conn_status.set_value("Connected!") - tool_ctl.sv_conn_detail.set_value("Data received successfully, press 'Next' to continue.") - tool_ctl.sv_skip.hide() - tool_ctl.sv_next.show() - end -end - --- handle supervisor connection failure -local function handle_timeout() - tool_ctl.net_listen = false - tool_ctl.sv_conn_button.enable() - tool_ctl.sv_conn_status.set_value("Timed out.") - tool_ctl.sv_conn_detail.set_value("Supervisor did not reply. Ensure startup app is running on the supervisor.") -end - -- load tmp_cfg fields from ini_cfg fields for displays local function preset_monitor_fields() tmp_cfg.DisableFlowView = ini_cfg.DisableFlowView @@ -302,8 +171,14 @@ local function load_settings(target, raw) end -- create the config view ----@param display graphics_element +---@param display DisplayBox local function config_view(display) + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + ---@diagnostic disable-next-line: undefined-field local function exit() os.queueEvent("terminate") end @@ -324,7 +199,7 @@ local function config_view(display) local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,fac_cfg,mon_cfg,spkr_cfg,crd_cfg,log_cfg,clr_cfg,summary,changelog}} - -- Main Page + --#region Main Page local y_start = 5 @@ -352,7 +227,7 @@ local function config_view(display) end PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg} + tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} local function jump_color() tool_ctl.jumped_to_color = true @@ -370,677 +245,32 @@ local function config_view(display) tool_ctl.color_cfg.disable() end - --#region Network - - local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49} - local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49} - local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49} - local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49} - - local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}} - - TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} - - TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."} - TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"} - local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"} - local crd_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"} - local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg} - - local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_channels() - local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value()) - if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then - tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c - net_pane.set_value(2) - chan_err.hide(true) - else chan_err.show() end - end - - PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=net_c_2,x=1,y=1,text="Please set the connection timeouts below."} - TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_2,x=1,y=8,width=19,text="Supervisor Timeout"} - local svr_timeout = NumberField{parent=net_c_2,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} - - TextBox{parent=net_c_2,x=1,y=10,width=14,text="Pocket Timeout"} - local api_timeout = NumberField{parent=net_c_2,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} - - TextBox{parent=net_c_2,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg} - - local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_timeouts() - local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(api_timeout.get_value()) - if svr_cto ~= nil and api_cto ~= nil then - tmp_cfg.SVR_Timeout, tmp_cfg.API_Timeout = svr_cto, api_cto - net_pane.set_value(3) - ct_err.hide(true) - else ct_err.show() end - end - - PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."} - TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg} - TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg} - - local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg} - - local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_tr() - local range_val = tonumber(range.get_value()) - if range_val ~= nil then - tmp_cfg.TrustedRange = range_val - comms.set_trusted_range(range_val) - net_pane.set_value(4) - tr_err.hide(true) - else tr_err.show() end - end - - PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} - TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_4,x=1,y=11,text="Facility Auth Key"} - local key, _, censor = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg} - - local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end - - local hide_key = CheckBox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} - - hide_key.set_value(true) - censor_key(true) - - local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_auth() - local v = key.get_value() - if string.len(v) == 0 or string.len(v) >= 8 then - -- prep supervisor connection screen - tool_ctl.sv_next.hide() - tool_ctl.sv_skip.disable() - tool_ctl.sv_skip.show() - tool_ctl.sv_conn_button.enable() - tool_ctl.sv_conn_status.set_value("") - tool_ctl.sv_conn_detail.set_value("") - - tmp_cfg.AuthKey = key.get_value() - key_err.hide(true) - - -- init mac for supervisor connection - if string.len(v) >= 8 then network.init_mac(tmp_cfg.AuthKey) else network.deinit_mac() end - - main_pane.set_value(3) - - tcd.dispatch_unique(2, function () tool_ctl.sv_skip.enable() end) - else key_err.show() end - end - - PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - --#endregion - - --#region Facility - - local fac_c_1 = Div{parent=fac_cfg,x=2,y=4,width=49} - local fac_c_2 = Div{parent=fac_cfg,x=2,y=4,width=49} - local fac_c_3 = Div{parent=fac_cfg,x=2,y=4,width=49} - - local fac_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3}} - - TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)} - - TextBox{parent=fac_c_1,x=1,y=1,height=4,text="This tool can attempt to connect to your supervisor computer. This would load facility information in order to get the unit count and aid monitor setup."} - TextBox{parent=fac_c_1,x=1,y=6,height=2,text="The supervisor startup app must be running and fully configured on your supervisor computer."} - - tool_ctl.sv_conn_status = TextBox{parent=fac_c_1,x=11,y=9,text=""} - tool_ctl.sv_conn_detail = TextBox{parent=fac_c_1,x=1,y=11,height=2,text=""} - - tool_ctl.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()tool_ctl.sv_connect()end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg} - - local function sv_skip() - tcd.abort(handle_timeout) - tool_ctl.sv_cool_conf = nil - tool_ctl.net_listen = false - fac_pane.set_value(2) - end - - local function sv_next() - tool_ctl.show_sv_cfg() - tool_ctl.update_mon_reqs() - fac_pane.set_value(3) - end - - PushButton{parent=fac_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - tool_ctl.sv_skip = PushButton{parent=fac_c_1,x=44,y=14,text="Skip \x1a",callback=sv_skip,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg} - tool_ctl.sv_next = PushButton{parent=fac_c_1,x=44,y=14,text="Next \x1a",callback=sv_next,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,hidden=true} - - TextBox{parent=fac_c_2,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."} - local num_units = NumberField{parent=fac_c_2,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg} - TextBox{parent=fac_c_2,x=7,y=5,text="reactors"} - TextBox{parent=fac_c_2,x=1,y=7,height=3,text="This will decide how many monitors you need. If this does not match the supervisor's number of reactor units, the coordinator will not connect.",fg_bg=g_lg_fg_bg} - TextBox{parent=fac_c_2,x=1,y=10,height=3,text="Since you skipped supervisor sync, the main monitor minimum height can't be determined precisely. It is marked with * on the next page.",fg_bg=g_lg_fg_bg} - - local nu_error = TextBox{parent=fac_c_2,x=8,y=14,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_num_units() - local count = tonumber(num_units.get_value()) - if count ~= nil and count > 0 and count < 5 then - nu_error.hide(true) - tmp_cfg.UnitCount = count - tool_ctl.update_mon_reqs() - main_pane.set_value(4) - else nu_error.show() end - end - - PushButton{parent=fac_c_2,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=fac_c_2,x=44,y=14,text="Next \x1a",callback=submit_num_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=fac_c_3,x=1,y=1,height=2,text="The following facility configuration was fetched from your supervisor computer."} - - local fac_config_list = ListBox{parent=fac_c_3,x=1,y=4,height=9,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - PushButton{parent=fac_c_3,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=fac_c_3,x=44,y=14,text="Next \x1a",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - --#endregion - - --#region Monitors - - local mon_c_1 = Div{parent=mon_cfg,x=2,y=4,width=49} - local mon_c_2 = Div{parent=mon_cfg,x=2,y=4,width=49} - local mon_c_3 = Div{parent=mon_cfg,x=2,y=4,width=49} - local mon_c_4 = Div{parent=mon_cfg,x=2,y=4,width=49} - - local mon_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={mon_c_1,mon_c_2,mon_c_3,mon_c_4}} - - TextBox{parent=mon_cfg,x=1,y=2,text=" Monitor Configuration",fg_bg=cpair(colors.black,colors.blue)} - - TextBox{parent=mon_c_1,x=1,y=1,height=5,text="Your configuration requires the following monitors. The main and flow monitors' heights are dependent on your unit count and cooling setup. If you manually entered the unit count, a * will be shown on potentially inaccurate calculations."} - local mon_reqs = ListBox{parent=mon_c_1,x=1,y=7,height=6,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - local function next_from_reqs() - -- unassign unit monitors above the unit count - for i = tmp_cfg.UnitCount + 1, 4 do tmp_cfg.UnitDisplays[i] = nil end - - tool_ctl.gen_mon_list() - mon_pane.set_value(2) - end - - PushButton{parent=mon_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=mon_c_1,x=8,y=14,text="Legacy Options",min_width=16,callback=function()mon_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=mon_c_1,x=44,y=14,text="Next \x1a",callback=next_from_reqs,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=mon_c_2,x=1,y=1,height=5,text="Please configure your monitors below. You can go back to the prior page without losing progress to double check what you need. All of those monitors must be assigned before you can proceed."} - - local mon_list = ListBox{parent=mon_c_2,x=1,y=6,height=7,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - local assign_err = TextBox{parent=mon_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_monitors() - if tmp_cfg.MainDisplay == nil then - assign_err.set_value("Please assign the main monitor.") - elseif tmp_cfg.FlowDisplay == nil and not tmp_cfg.DisableFlowView then - assign_err.set_value("Please assign the flow monitor.") - elseif util.table_len(tmp_cfg.UnitDisplays) ~= tmp_cfg.UnitCount then - for i = 1, tmp_cfg.UnitCount do - if tmp_cfg.UnitDisplays[i] == nil then - assign_err.set_value("Please assign the unit " .. i .. " monitor.") - break - end - end - else - assign_err.hide(true) - main_pane.set_value(5) - return - end - - assign_err.show() - end - - PushButton{parent=mon_c_2,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=mon_c_2,x=44,y=14,text="Next \x1a",callback=submit_monitors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - local mon_desc = TextBox{parent=mon_c_3,x=1,y=1,height=4,text=""} - - local mon_unit_l, mon_unit = nil, nil ---@type graphics_element, graphics_element - - local mon_warn = TextBox{parent=mon_c_3,x=1,y=11,height=2,text="",fg_bg=cpair(colors.red,colors.lightGray)} - - ---@param val integer assignment type - local function on_assign_mon(val) - if val == 2 and tmp_cfg.DisableFlowView then - tool_ctl.apply_mon.disable() - mon_warn.set_value("You disabled having a flow view monitor. It can't be set unless you go back and enable it.") - mon_warn.show() - elseif not util.table_contains(tool_ctl.mon_expect, val) then - tool_ctl.apply_mon.disable() - mon_warn.set_value("That assignment doesn't fit monitor dimensions. You'll need to resize the monitor for it to work.") - mon_warn.show() - else - tool_ctl.apply_mon.enable() - mon_warn.hide(true) - end - - if val == 3 then - mon_unit_l.show() - mon_unit.show() - else - mon_unit_l.hide(true) - mon_unit.hide(true) - end - - local value = mon_unit.get_value() - mon_unit.set_max(tmp_cfg.UnitCount) - if value == "0" or value == nil then mon_unit.set_value(0) end - end - - TextBox{parent=mon_c_3,x=1,y=6,width=10,text="Assignment"} - local mon_assign = RadioButton{parent=mon_c_3,x=1,y=7,default=1,options={"Main Monitor","Flow Monitor","Unit Monitor"},callback=on_assign_mon,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.blue} - - mon_unit_l = TextBox{parent=mon_c_3,x=18,y=6,width=7,text="Unit ID"} - mon_unit = NumberField{parent=mon_c_3,x=18,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg} - - local mon_u_err = TextBox{parent=mon_c_3,x=8,y=14,width=35,text="Please provide a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - -- purge all assignments for a given monitor - ---@param iface string - local function purge_assignments(iface) - if tmp_cfg.MainDisplay == iface then - tmp_cfg.MainDisplay = nil - elseif tmp_cfg.FlowDisplay == iface then - tmp_cfg.FlowDisplay = nil - else - for i = 1, tmp_cfg.UnitCount do - if tmp_cfg.UnitDisplays[i] == iface then tmp_cfg.UnitDisplays[i] = nil end - end - end - end - - local function apply_monitor() - local iface = tool_ctl.mon_iface - local type = mon_assign.get_value() - local u_id = tonumber(mon_unit.get_value()) - - if type == 1 then - purge_assignments(iface) - tmp_cfg.MainDisplay = iface - elseif type == 2 then - purge_assignments(iface) - tmp_cfg.FlowDisplay = iface - elseif u_id and u_id > 0 then - purge_assignments(iface) - tmp_cfg.UnitDisplays[u_id] = iface - else - mon_u_err.show() - return - end - - tool_ctl.gen_mon_list() - mon_u_err.hide(true) - mon_pane.set_value(2) - end - - PushButton{parent=mon_c_3,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - tool_ctl.apply_mon = PushButton{parent=mon_c_3,x=43,y=14,min_width=7,text="Apply",callback=apply_monitor,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg} - - TextBox{parent=mon_c_4,x=1,y=1,height=3,text="For legacy compatibility with facilities built without space for a flow monitor, you can disable the flow monitor requirement here."} - TextBox{parent=mon_c_4,x=1,y=5,height=3,text="Please be aware that THIS OPTION WILL BE REMOVED ON RELEASE. Disabling it will only be available for the remainder of the beta."} - - local dis_flow_view = CheckBox{parent=mon_c_4,x=1,y=9,default=ini_cfg.DisableFlowView,label="Disable Flow View Monitor",box_fg_bg=cpair(colors.blue,colors.black)} - - local function back_from_legacy() - tmp_cfg.DisableFlowView = dis_flow_view.get_value() - tool_ctl.update_mon_reqs() - mon_pane.set_value(1) - end - - PushButton{parent=mon_c_4,x=44,y=14,min_width=6,text="Done",callback=back_from_legacy,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - --#endregion - - --#region Speaker - - local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49} - - TextBox{parent=spkr_cfg,x=1,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)} - - TextBox{parent=spkr_c,x=1,y=1,height=2,text="The coordinator uses a speaker to play alarm sounds."} - TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."} - - local s_vol = NumberField{parent=spkr_c,x=1,y=8,width=9,max_chars=7,allow_decimal=true,default=ini_cfg.SpeakerVolume,min=0,max=3,fg_bg=bw_fg_bg} - - TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg} - - local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_vol() - local vol = tonumber(s_vol.get_value()) - if vol ~= nil then - s_vol_err.hide(true) - tmp_cfg.SpeakerVolume = vol - main_pane.set_value(6) - else s_vol_err.show() end - end - - PushButton{parent=spkr_c,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=spkr_c,x=44,y=14,text="Next \x1a",callback=submit_vol,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - --#endregion - - --#region Coordinator UI - - local crd_c_1 = Div{parent=crd_cfg,x=2,y=4,width=49} - - TextBox{parent=crd_cfg,x=1,y=2,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)} - - TextBox{parent=crd_c_1,x=1,y=1,height=3,text="Configure the UI interface options below if you wish to customize formats."} - - TextBox{parent=crd_c_1,x=1,y=4,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,text="Temperature Scale"} - local temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} - - TextBox{parent=crd_c_1,x=24,y=8,text="Energy Scale"} - local energy_scale = RadioButton{parent=crd_c_1,x=24,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} - - local function submit_ui_opts() - tmp_cfg.Time24Hour = clock_fmt.get_value() == 1 - tmp_cfg.TempScale = temp_scale.get_value() - tmp_cfg.EnergyScale = energy_scale.get_value() - main_pane.set_value(7) - end - - PushButton{parent=crd_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=crd_c_1,x=44,y=14,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - --#endregion - --#region Logging - - local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} + local settings = { settings_cfg, ini_cfg, tmp_cfg, fields, load_settings } - TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} + --#region Facility Configuration - TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."} - - TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} - local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} - - TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"} - local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg} - - local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} - TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg} - - local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_log() - if path.get_value() ~= "" then - path_err.hide(true) - tmp_cfg.LogMode = mode.get_value() - 1 - tmp_cfg.LogPath = path.get_value() - tmp_cfg.LogDebug = en_dbg.get_value() - tool_ctl.color_apply.hide(true) - tool_ctl.color_next.show() - main_pane.set_value(8) - else path_err.show() end - end - - PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + local fac_pane = facility.create(tool_ctl, main_pane, settings, fac_cfg, style) --#endregion - --#region Color Options - - local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49} - local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49} - local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49} - local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49} - - local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}} - - TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)} - - TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color themes for the different UI displays."} - TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg} - - TextBox{parent=clr_c_1,x=1,y=7,text="Main UI Theme"} - local main_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} - - TextBox{parent=clr_c_1,x=18,y=7,text="Front Panel Theme"} - local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} - - TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."} - - TextBox{parent=clr_c_2,x=21,y=7,text="Preview"} - local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)} - _ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)} - _ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)} - local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true} - local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true} - - local function recolor(value) - local c = themes.smooth_stone.color_modes[value] - - if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then - b_off.hide() - g_off.show() - else - g_off.hide() - b_off.show() - end - - if #c == 0 then - for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end - else - term.setPaletteColor(colors.green, c[1].hex) - term.setPaletteColor(colors.yellow, c[2].hex) - term.setPaletteColor(colors.red, c[3].hex) - end - end - - TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"} - local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} - - TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg} - - PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - local function back_from_colors() - main_pane.set_value(util.trinary(tool_ctl.jumped_to_color, 1, 7)) - tool_ctl.jumped_to_color = false - recolor(1) - end - - local function show_access() - clr_pane.set_value(2) - recolor(c_mode.get_value()) - end - - local function submit_colors() - tmp_cfg.MainTheme = main_theme.get_value() - tmp_cfg.FrontPanelTheme = fp_theme.get_value() - tmp_cfg.ColorMode = c_mode.get_value() - - if tool_ctl.jumped_to_color then - settings.set("MainTheme", tmp_cfg.MainTheme) - settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme) - settings.set("ColorMode", tmp_cfg.ColorMode) - - if settings.save("/coordinator.settings") then - load_settings(settings_cfg, true) - load_settings(ini_cfg) - clr_pane.set_value(3) - else - clr_pane.set_value(4) - end - else - tool_ctl.gen_summary(tmp_cfg) - tool_ctl.viewing_config = false - tool_ctl.importing_legacy = false - tool_ctl.settings_apply.show() - main_pane.set_value(9) - end - end - - PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + --#region HMI Configuration - tool_ctl.color_apply.hide(true) - - local function c_go_home() - main_pane.set_value(1) - clr_pane.set_value(1) - end - - TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"} - PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} - PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + local mon_pane = hmi.create(tool_ctl, main_pane, settings, { mon_cfg, spkr_cfg, crd_cfg }, style) --#endregion - --#region Summary and Saving - - local sum_c_1 = Div{parent=summary,x=2,y=4,width=49} - local sum_c_2 = Div{parent=summary,x=2,y=4,width=49} - local sum_c_3 = Div{parent=summary,x=2,y=4,width=49} - local sum_c_4 = Div{parent=summary,x=2,y=4,width=49} - - local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}} - - TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)} - - local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - local function back_from_summary() - if tool_ctl.viewing_config or tool_ctl.importing_legacy then - main_pane.set_value(1) - tool_ctl.viewing_config = false - tool_ctl.importing_legacy = false - tool_ctl.settings_apply.show() - else - main_pane.set_value(8) - end - end - - ---@param element graphics_element - ---@param data any - local function try_set(element, data) - if data ~= nil then element.set_value(data) end - end - - local function save_and_continue() - for _, field in ipairs(fields) do - local k, v = field[1], tmp_cfg[field[1]] - if v == nil then settings.unset(k) else settings.set(k, v) end - end - - if settings.save("/coordinator.settings") then - load_settings(settings_cfg, true) - load_settings(ini_cfg) - - try_set(svr_chan, ini_cfg.SVR_Channel) - try_set(crd_chan, ini_cfg.CRD_Channel) - try_set(pkt_chan, ini_cfg.PKT_Channel) - try_set(svr_timeout, ini_cfg.SVR_Timeout) - try_set(api_timeout, ini_cfg.API_Timeout) - try_set(range, ini_cfg.TrustedRange) - try_set(key, ini_cfg.AuthKey) - try_set(num_units, ini_cfg.UnitCount) - try_set(dis_flow_view, ini_cfg.DisableFlowView) - try_set(s_vol, ini_cfg.SpeakerVolume) - try_set(clock_fmt, util.trinary(ini_cfg.Time24Hour, 1, 2)) - try_set(temp_scale, ini_cfg.TempScale) - try_set(energy_scale, ini_cfg.EnergyScale) - try_set(mode, ini_cfg.LogMode) - try_set(path, ini_cfg.LogPath) - try_set(en_dbg, ini_cfg.LogDebug) - try_set(main_theme, ini_cfg.MainTheme) - try_set(fp_theme, ini_cfg.FrontPanelTheme) - try_set(c_mode, ini_cfg.ColorMode) - - preset_monitor_fields() - - tool_ctl.gen_mon_list() - - tool_ctl.view_cfg.enable() - tool_ctl.color_cfg.enable() - - if tool_ctl.importing_legacy then - tool_ctl.importing_legacy = false - sum_pane.set_value(3) - else - sum_pane.set_value(2) - end - else - sum_pane.set_value(4) - end - end + --#region System Configuration - PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg} - tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + local divs = { net_cfg, log_cfg, clr_cfg, summary } + local ext = { fac_pane, mon_pane, preset_monitor_fields, exit } - TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"} - - local function go_home() - main_pane.set_value(1) - net_pane.set_value(1) - fac_pane.set_value(1) - mon_pane.set_value(1) - clr_pane.set_value(1) - sum_pane.set_value(1) - end - - PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - - TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua and coord.settings files will now be deleted, then the configurator will exit."} - - local function delete_legacy() - fs.delete("/coordinator/config.lua") - fs.delete("/coord.settings") - exit() - end - - PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)} - - TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} - PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + system.create(tool_ctl, main_pane, settings, divs, ext, style) --#endregion - -- Config Change Log + --#region Config Change Log local cl = Div{parent=changelog,x=2,y=4,width=49} @@ -1059,355 +289,7 @@ local function config_view(display) PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - -- set tool functions now that we have the elements - - -- load a legacy config file - function tool_ctl.load_legacy() - local config = require("coordinator.config") - - tmp_cfg.SVR_Channel = config.SVR_CHANNEL - tmp_cfg.CRD_Channel = config.CRD_CHANNEL - tmp_cfg.PKT_Channel = config.PKT_CHANNEL - tmp_cfg.SVR_Timeout = config.SV_TIMEOUT - tmp_cfg.API_Timeout = config.API_TIMEOUT - tmp_cfg.TrustedRange = config.TRUSTED_RANGE - tmp_cfg.AuthKey = config.AUTH_KEY or "" - - tmp_cfg.UnitCount = config.NUM_UNITS - tmp_cfg.DisableFlowView = config.DISABLE_FLOW_VIEW - tmp_cfg.SpeakerVolume = config.SOUNDER_VOLUME - tmp_cfg.Time24Hour = config.TIME_24_HOUR - - tmp_cfg.LogMode = config.LOG_MODE - tmp_cfg.LogPath = config.LOG_PATH - tmp_cfg.LogDebug = config.LOG_DEBUG or false - - settings.load("/coord.settings") - - tmp_cfg.MainDisplay = settings.get("PRIMARY_DISPLAY") - tmp_cfg.FlowDisplay = settings.get("FLOW_DISPLAY") - tmp_cfg.UnitDisplays = settings.get("UNIT_DISPLAYS", {}) - - -- if there are extra monitor entries, delete them now - -- not doing so will cause the app to fail to start - if is_int_min_max(tmp_cfg.UnitCount, 1, 4) then - for i = tmp_cfg.UnitCount + 1, 4 do tmp_cfg.UnitDisplays[i] = nil end - end - - if settings.get("ControlStates") == nil then - local ctrl_states = { - process = settings.get("PROCESS"), - waste_modes = settings.get("WASTE_MODES"), - priority_groups = settings.get("PRIORITY_GROUPS"), - } - - settings.set("ControlStates", ctrl_states) - end - - settings.unset("PRIMARY_DISPLAY") - settings.unset("FLOW_DISPLAY") - settings.unset("UNIT_DISPLAYS") - settings.unset("PROCESS") - settings.unset("WASTE_MODES") - settings.unset("PRIORITY_GROUPS") - - tool_ctl.gen_summary(tmp_cfg) - sum_pane.set_value(1) - main_pane.set_value(9) - tool_ctl.importing_legacy = true - end - - -- attempt a connection to the supervisor to get cooling info - function tool_ctl.sv_connect() - tool_ctl.sv_conn_button.disable() - tool_ctl.sv_conn_detail.set_value("") - - local modem = ppm.get_wireless_modem() - if modem == nil then - tool_ctl.sv_conn_status.set_value("Please connect an ender/wireless modem.") - else - tool_ctl.sv_conn_status.set_value("Modem found, connecting...") - if tool_ctl.nic == nil then tool_ctl.nic = network.nic(modem) end - - tool_ctl.nic.closeAll() - tool_ctl.nic.open(tmp_cfg.CRD_Channel) - - tool_ctl.sv_addr = comms.BROADCAST - tool_ctl.net_listen = true - - send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.CRD }) - - tcd.dispatch_unique(8, handle_timeout) - end - end - - -- show the facility's unit count and cooling configuration data - function tool_ctl.show_sv_cfg() - local conf = tool_ctl.sv_cool_conf - fac_config_list.remove_all() - - local str = util.sprintf("Facility has %d reactor unit%s:", #conf, util.trinary(#conf==1,"","s")) - TextBox{parent=fac_config_list,text=str,fg_bg=cpair(colors.gray,colors.white)} - - for i = 1, #conf do - local num_b, num_t = conf[i][1], conf[i][2] - str = util.sprintf("\x07 Unit %d has %d boiler%s and %d turbine%s", i, num_b, util.trinary(num_b == 1, "", "s"), num_t, util.trinary(num_t == 1, "", "s")) - TextBox{parent=fac_config_list,text=str,fg_bg=cpair(colors.gray,colors.white)} - end - end - - -- update list of monitor requirements - function tool_ctl.update_mon_reqs() - local plural = tmp_cfg.UnitCount > 1 - - if tool_ctl.sv_cool_conf ~= nil then - local cnf = tool_ctl.sv_cool_conf - - local row1_tall = cnf[1][1] > 1 or cnf[1][2] > 2 or (cnf[2] and (cnf[2][1] > 1 or cnf[2][2] > 2)) - local row1_short = (cnf[1][1] == 0 and cnf[1][2] == 1) and (cnf[2] == nil or (cnf[2][1] == 0 and cnf[2][2] == 1)) - local row2_tall = (cnf[3] and (cnf[3][1] > 1 or cnf[3][2] > 2)) or (cnf[4] and (cnf[4][1] > 1 or cnf[4][2] > 2)) - local row2_short = (cnf[3] == nil or (cnf[3][1] == 0 and cnf[3][2] == 1)) and (cnf[4] == nil or (cnf[4][1] == 0 and cnf[4][2] == 1)) - - if tmp_cfg.UnitCount <= 2 then - tool_ctl.main_mon_h = util.trinary(row1_tall, 5, 4) - else - -- is only one tall and the other short, or are both tall? -> 5 or 6; are neither tall? -> 5 - if row1_tall or row2_tall then - tool_ctl.main_mon_h = util.trinary((row1_short and row2_tall) or (row1_tall and row2_short), 5, 6) - else tool_ctl.main_mon_h = 5 end - end - else - tool_ctl.main_mon_h = util.trinary(tmp_cfg.UnitCount <= 2, 4, 5) - end - - tool_ctl.flow_mon_h = 2 + tmp_cfg.UnitCount - - local asterisk = util.trinary(tool_ctl.sv_cool_conf == nil, "*", "") - local m_at_least = util.trinary(tool_ctl.main_mon_h < 6, "at least ", "") - local f_at_least = util.trinary(tool_ctl.flow_mon_h < 6, "at least ", "") - - mon_reqs.remove_all() - - TextBox{parent=mon_reqs,x=1,y=1,text="\x1a "..tmp_cfg.UnitCount.." Unit View Monitor"..util.trinary(plural,"s","")} - TextBox{parent=mon_reqs,x=1,y=1,text=" "..util.trinary(plural,"each ","").."must be 4 blocks wide by 4 tall",fg_bg=cpair(colors.gray,colors.white)} - TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Main View Monitor"} - TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..m_at_least..tool_ctl.main_mon_h..asterisk.." tall",fg_bg=cpair(colors.gray,colors.white)} - if not tmp_cfg.DisableFlowView then - TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Flow View Monitor"} - TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..f_at_least..tool_ctl.flow_mon_h.." tall",fg_bg=cpair(colors.gray,colors.white)} - end - end - - -- set/edit a monitor's assignment - ---@param iface string - ---@param device ppm_entry - function tool_ctl.edit_monitor(iface, device) - tool_ctl.mon_iface = iface - - local dev = device.dev - local w, h = ppm.monitor_block_size(dev.getSize()) - - local msg = "This size doesn't match a required screen. Please go back and resize it, or configure below at the risk of it not working." - - tool_ctl.mon_expect = {} - mon_assign.set_value(1) - mon_unit.set_value(0) - - if w == 4 and h == 4 then - msg = "This could work as a unit display. Please configure below." - tool_ctl.mon_expect = { 3 } - mon_assign.set_value(3) - elseif w == 8 then - if h >= tool_ctl.main_mon_h and h >= tool_ctl.flow_mon_h then - msg = "This could work as either your main monitor or flow monitor. Please configure below." - tool_ctl.mon_expect = { 1, 2 } - if tmp_cfg.MainDisplay then mon_assign.set_value(2) end - elseif h >= tool_ctl.main_mon_h then - msg = "This could work as your main monitor. Please configure below." - tool_ctl.mon_expect = { 1 } - elseif h >= tool_ctl.flow_mon_h then - msg = "This could work as your flow monitor. Please configure below." - tool_ctl.mon_expect = { 2 } - mon_assign.set_value(2) - end - end - - -- override if a config exists - if tmp_cfg.MainDisplay == iface then - mon_assign.set_value(1) - elseif tmp_cfg.FlowDisplay == iface then - mon_assign.set_value(2) - else - for i = 1, tmp_cfg.UnitCount do - if tmp_cfg.UnitDisplays[i] == iface then - mon_assign.set_value(3) - mon_unit.set_value(i) - break - end - end - end - - on_assign_mon(mon_assign.get_value()) - - mon_desc.set_value(util.c("You have selected '", iface, "', which has a block size of ", w, " wide by ", h, " tall. ", msg)) - mon_pane.set_value(3) - end - - -- generate the list of available monitors - function tool_ctl.gen_mon_list() - mon_list.remove_all() - - local missing = { main = tmp_cfg.MainDisplay ~= nil, flow = tmp_cfg.FlowDisplay ~= nil, unit = {} } - for i = 1, tmp_cfg.UnitCount do missing.unit[i] = tmp_cfg.UnitDisplays[i] ~= nil end - - -- list connected monitors - local monitors = ppm.get_monitor_list() - for iface, device in pairs(monitors) do - local dev = device.dev - - dev.setTextScale(0.5) - dev.setTextColor(colors.white) - dev.setBackgroundColor(colors.black) - dev.clear() - dev.setCursorPos(1, 1) - dev.setTextColor(colors.magenta) - dev.write("This is monitor") - dev.setCursorPos(1, 2) - dev.setTextColor(colors.white) - dev.write(iface) - - local assignment = "Unused" - - if tmp_cfg.MainDisplay == iface then - assignment = "Main" - missing.main = false - elseif tmp_cfg.FlowDisplay == iface then - assignment = "Flow" - missing.flow = false - else - for i = 1, tmp_cfg.UnitCount do - if tmp_cfg.UnitDisplays[i] == iface then - missing.unit[i] = false - assignment = "Unit " .. i - break - end - end - end - - local line = Div{parent=mon_list,x=1,y=1,height=1} - - TextBox{parent=line,x=1,y=1,width=6,text=assignment,fg_bg=cpair(util.trinary(assignment=="Unused",colors.red,colors.blue),colors.white)} - TextBox{parent=line,x=8,y=1,text=iface} - - local w, h = ppm.monitor_block_size(dev.getSize()) - - local function unset_mon() - purge_assignments(iface) - tool_ctl.gen_mon_list() - end - - TextBox{parent=line,x=33,y=1,width=4,text=w.."x"..h,fg_bg=cpair(colors.black,colors.white)} - PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()tool_ctl.edit_monitor(iface,device)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - local unset = PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)} - - if assignment == "Unused" then unset.disable() end - end - - local dc_list = {} -- disconnected monitor list - - if missing.main then table.insert(dc_list, { "Main", tmp_cfg.MainDisplay }) end - if missing.flow then table.insert(dc_list, { "Flow", tmp_cfg.FlowDisplay }) end - for i = 1, tmp_cfg.UnitCount do - if missing.unit[i] then table.insert(dc_list, { "Unit " .. i, tmp_cfg.UnitDisplays[i] }) end - end - - -- add monitors that are assigned but not connected - for i = 1, #dc_list do - local line = Div{parent=mon_list,x=1,y=1,height=1} - - TextBox{parent=line,x=1,y=1,width=6,text=dc_list[i][1],fg_bg=cpair(colors.blue,colors.white)} - TextBox{parent=line,x=8,y=1,text="disconnected",fg_bg=cpair(colors.red,colors.white)} - - local function unset_mon() - purge_assignments(dc_list[i][2]) - tool_ctl.gen_mon_list() - end - - TextBox{parent=line,x=33,y=1,width=4,text="?x?",fg_bg=cpair(colors.black,colors.white)} - PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()end,dis_fg_bg=cpair(colors.black,colors.gray)}.disable() - PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)} - end - end - - -- expose the auth key on the summary page - function tool_ctl.show_auth_key() - tool_ctl.show_key_btn.disable() - tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value) - end - - -- generate the summary list - ---@param cfg crd_config - function tool_ctl.gen_summary(cfg) - setting_list.remove_all() - - local alternate = false - local inner_width = setting_list.get_width() - 1 - - tool_ctl.show_key_btn.enable() - tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key - - for i = 1, #fields do - local f = fields[i] - local height = 1 - local label_w = string.len(f[2]) - local val_max_w = (inner_width - label_w) + 1 - local raw = cfg[f[1]] - local val = util.strval(raw) - - if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) - elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") - elseif f[1] == "TempScale" then - val = util.strval(types.TEMP_SCALE_NAMES[raw]) - elseif f[1] == "EnergyScale" then - val = util.strval(types.ENERGY_SCALE_NAMES[raw]) - elseif f[1] == "MainTheme" then - val = util.strval(themes.ui_theme_name(raw)) - elseif f[1] == "FrontPanelTheme" then - val = util.strval(themes.fp_theme_name(raw)) - elseif f[1] == "ColorMode" then - val = util.strval(themes.color_mode_name(raw)) - elseif f[1] == "UnitDisplays" and type(cfg.UnitDisplays) == "table" then - val = "" - for idx = 1, #cfg.UnitDisplays do - val = val .. util.trinary(idx == 1, "", "\n") .. util.sprintf(" \x07 Unit %d - %s", idx, cfg.UnitDisplays[idx]) - end - end - - if val == "nil" then val = "" end - - local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) - alternate = not alternate - - if string.len(val) > val_max_w then - local lines = util.strwrap(val, inner_width) - height = #lines + 1 - end - - if (f[1] == "UnitDisplays") and (height == 1) and (val ~= "") then height = 2 end - - local line = Div{parent=setting_list,height=height,fg_bg=c} - TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} - - local textbox - if height > 1 then - textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1} - else - textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT} - end - - if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end - end - end + --#endregion end -- reset terminal screen @@ -1471,16 +353,8 @@ function configurator.configure(start_code, message) tool_ctl.gen_mon_list() elseif event == "monitor_resize" then tool_ctl.gen_mon_list() - elseif event == "modem_message" and tool_ctl.nic ~= nil and tool_ctl.net_listen then - local s_pkt = tool_ctl.nic.receive(param1, param2, param3, param4, param5) - - if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then - local mgmt_pkt = comms.mgmt_packet() - if mgmt_pkt.decode(s_pkt) then - tcd.abort(handle_timeout) - handle_packet(mgmt_pkt.get()) - end - end + elseif event == "modem_message" then + facility.receive_sv(param1, param2, param3, param4, param5) end if event == "terminate" then return end diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index bb8bcee1..71da3877 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -111,12 +111,12 @@ function coordinator.load_config() ---@class monitors_struct local monitors = { - main = nil, ---@type table|nil + main = nil, ---@type Monitor|nil main_name = "", - flow = nil, ---@type table|nil + flow = nil, ---@type Monitor|nil flow_name = "", - unit_displays = {}, - unit_name_map = {} + unit_displays = {}, ---@type Monitor[] + unit_name_map = {} ---@type string[] } local mon_cfv = util.new_validator() @@ -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) }) @@ -623,7 +627,7 @@ function coordinator.comms(version, nic, sv_watchdog) local unit_id = packet.data[2] local ack = packet.data[3] == true - local unit = iocontrol.get_db().units[unit_id] ---@type ioctl_unit + local unit = iocontrol.get_db().units[unit_id] if unit ~= nil then if cmd == UNIT_COMMAND.SCRAM then diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 37230b3f..8d57beda 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -29,15 +29,6 @@ local iocontrol = {} ---@class ioctl local io = {} --- luacheck: no unused args - --- placeholder acknowledge function for type hinting ----@param success boolean ----@diagnostic disable-next-line: unused-local -local function __generic_ack(success) end - --- luacheck: unused args - -- initialize front panel PSIL ---@param firmware_v string coordinator version ---@param comms_v string comms version @@ -93,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, @@ -115,25 +108,21 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) radiation = types.new_zero_radiation_reading(), - save_cfg_ack = __generic_ack, - start_ack = __generic_ack, - stop_ack = __generic_ack, + save_cfg_ack = nil, ---@type fun(success: boolean) + ---@type { [TONE]: boolean } alarm_tones = { false, false, false, false, false, false, false, false }, ps = psil.create(), - induction_ps_tbl = {}, - induction_data_tbl = {}, - - sps_ps_tbl = {}, - sps_data_tbl = {}, + induction_ps_tbl = {}, ---@type psil[] + induction_data_tbl = {}, ---@type imatrix_session_db[] - tank_ps_tbl = {}, - tank_data_tbl = {}, + sps_ps_tbl = {}, ---@type psil[] + sps_data_tbl = {}, ---@type sps_session_db[] - env_d_ps = psil.create(), - env_d_data = {} + tank_ps_tbl = {}, ---@type psil[] + tank_data_tbl = {} ---@type dynamicv_session_db[] } -- create induction and SPS tables (currently only 1 of each is supported) @@ -151,7 +140,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) end -- create unit data structures - io.units = {} + io.units = {} ---@type ioctl_unit[] for i = 1, conf.num_units do local function ack(alarm) process.ack_alarm(i, alarm) end local function reset(alarm) process.reset_alarm(i, alarm) end @@ -160,13 +149,21 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) local entry = { unit_id = i, connected = false, - rtu_hw = { boilers = {}, turbines = {} }, + rtu_hw = { + boilers = {}, ---@type { connected: boolean, faulted: boolean }[] + turbines = {} ---@type { connected: boolean, faulted: boolean }[] + }, num_boilers = 0, num_turbines = 0, 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(), @@ -208,7 +205,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) t_trip = { ack = function () ack(12) end, reset = function () reset(12) end } }, - ---@type alarms + ---@type { [ALARM]: ALARM_STATE } alarms = { ALARM_STATE.INACTIVE, -- containment breach ALARM_STATE.INACTIVE, -- containment radiation @@ -224,19 +221,19 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale) ALARM_STATE.INACTIVE -- turbine trip }, - annunciator = {}, ---@type annunciator + annunciator = {}, ---@type annunciator unit_ps = psil.create(), - reactor_data = {}, ---@type reactor_db + reactor_data = types.new_reactor_db(), - boiler_ps_tbl = {}, - boiler_data_tbl = {}, + boiler_ps_tbl = {}, ---@type psil[] + boiler_data_tbl = {}, ---@type boilerv_session_db[] - turbine_ps_tbl = {}, - turbine_data_tbl = {}, + turbine_ps_tbl = {}, ---@type psil[] + turbine_data_tbl = {}, ---@type turbinev_session_db[] - tank_ps_tbl = {}, - tank_data_tbl = {} + tank_ps_tbl = {}, ---@type psil[] + tank_data_tbl = {} ---@type dynamicv_session_db[] } -- on other facility modes, overwrite unit TANK option with facility tank defs @@ -357,8 +354,8 @@ end -- record and publish multiblock RTU build data ---@param id integer ---@param entry table ----@param data_tbl table ----@param ps_tbl table +---@param data_tbl (imatrix_session_db|sps_session_db|dynamicv_session_db|turbinev_session_db|boilerv_session_db)[] +---@param ps_tbl psil[] ---@param create boolean? true to create an entry if non exists, false to fail on missing ---@return boolean ok true if data saved, false if invalid ID local function _record_multiblock_build(id, entry, data_tbl, ps_tbl, create) @@ -369,8 +366,8 @@ local function _record_multiblock_build(id, entry, data_tbl, ps_tbl, create) data_tbl[id] = {} end - data_tbl[id].formed = entry[1] ---@type boolean - data_tbl[id].build = entry[2] ---@type table + data_tbl[id].formed = entry[1] + data_tbl[id].build = entry[2] ps_tbl[id].publish("formed", entry[1]) @@ -444,7 +441,7 @@ function iocontrol.record_unit_builds(builds) else -- reactor build if type(build.reactor) == "table" then - unit.reactor_data.mek_struct = build.reactor ---@type mek_struct + unit.reactor_data.mek_struct = build.reactor for key, val in pairs(unit.reactor_data.mek_struct) do unit.unit_ps.publish(key, val) end @@ -497,10 +494,10 @@ end ---@param ps psil ---@return boolean is_faulted local function _record_multiblock_status(entry, data, ps) - local is_faulted = entry[1] ---@type boolean - data.formed = entry[2] ---@type boolean - data.state = entry[3] ---@type table - data.tanks = entry[4] ---@type table + local is_faulted = entry[1] + data.formed = entry[2] + data.state = entry[3] + data.tanks = entry[4] ps.publish("formed", data.formed) ps.publish("faulted", is_faulted) @@ -549,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) @@ -563,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] @@ -600,8 +597,8 @@ function iocontrol.update_facility_status(status) -- power statistics if type(rtu_statuses.power) == "table" and #rtu_statuses.power == 4 then - local data = fac.induction_data_tbl[1] ---@type imatrix_session_db - local ps = fac.induction_ps_tbl[1] ---@type psil + local data = fac.induction_data_tbl[1] + local ps = fac.induction_ps_tbl[1] local chg = tonumber(rtu_statuses.power[1]) local in_f = tonumber(rtu_statuses.power[2]) @@ -636,23 +633,23 @@ function iocontrol.update_facility_status(status) for id, matrix in pairs(rtu_statuses.induction) do if type(fac.induction_data_tbl[id]) == "table" then - local data = fac.induction_data_tbl[id] ---@type imatrix_session_db - local ps = fac.induction_ps_tbl[id] ---@type psil + local data = fac.induction_data_tbl[id] + local ps = fac.induction_ps_tbl[id] local rtu_faulted = _record_multiblock_status(matrix, data, ps) if rtu_faulted then - ps.publish("computed_status", 3) -- faulted + ps.publish("computed_status", 3) -- faulted elseif data.formed then if data.tanks.energy_fill >= 0.99 then - ps.publish("computed_status", 6) -- full + ps.publish("computed_status", 6) -- full elseif data.tanks.energy_fill <= 0.01 then - ps.publish("computed_status", 5) -- empty + ps.publish("computed_status", 5) -- empty else - ps.publish("computed_status", 4) -- on-line + ps.publish("computed_status", 4) -- on-line end else - ps.publish("computed_status", 2) -- not formed + ps.publish("computed_status", 2) -- not formed end else log.debug(util.c(log_header, "invalid induction matrix id ", id)) @@ -674,21 +671,21 @@ function iocontrol.update_facility_status(status) for id, sps in pairs(rtu_statuses.sps) do if type(fac.sps_data_tbl[id]) == "table" then - local data = fac.sps_data_tbl[id] ---@type sps_session_db - local ps = fac.sps_ps_tbl[id] ---@type psil + local data = fac.sps_data_tbl[id] + local ps = fac.sps_ps_tbl[id] local rtu_faulted = _record_multiblock_status(sps, data, ps) if rtu_faulted then - ps.publish("computed_status", 3) -- faulted + ps.publish("computed_status", 3) -- faulted elseif data.formed then if data.state.process_rate > 0 then - ps.publish("computed_status", 5) -- active + ps.publish("computed_status", 5) -- active else - ps.publish("computed_status", 4) -- idle + ps.publish("computed_status", 4) -- idle end else - ps.publish("computed_status", 2) -- not formed + ps.publish("computed_status", 2) -- not formed end io.facility.ps.publish("am_rate", data.state.process_rate * 1000) @@ -712,23 +709,23 @@ function iocontrol.update_facility_status(status) for id, tank in pairs(rtu_statuses.tanks) do if type(fac.tank_data_tbl[id]) == "table" then - local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db - local ps = fac.tank_ps_tbl[id] ---@type psil + local data = fac.tank_data_tbl[id] + local ps = fac.tank_ps_tbl[id] local rtu_faulted = _record_multiblock_status(tank, data, ps) if rtu_faulted then - ps.publish("computed_status", 3) -- faulted + ps.publish("computed_status", 3) -- faulted elseif data.formed then if data.tanks.fill >= 0.99 then - ps.publish("computed_status", 6) -- full + ps.publish("computed_status", 6) -- full elseif data.tanks.fill < 0.20 then - ps.publish("computed_status", 5) -- low + ps.publish("computed_status", 5) -- low else - ps.publish("computed_status", 4) -- on-line + ps.publish("computed_status", 4) -- on-line end else - ps.publish("computed_status", 2) -- not formed + ps.publish("computed_status", 2) -- not formed end else log.debug(util.c(log_header, "invalid dynamic tank id ", id)) @@ -812,7 +809,7 @@ function iocontrol.update_unit_statuses(statuses) for i = 1, #statuses do local log_header = util.c("iocontrol.update_unit_statuses[unit ", i, "]: ") - local unit = io.units[i] ---@type ioctl_unit + local unit = io.units[i] local status = statuses[i] local burn_rate = 0.0 @@ -848,46 +845,40 @@ function iocontrol.update_unit_statuses(statuses) log.debug(log_header .. "reactor general status length mismatch") end - unit.reactor_data.rps_status = rps_status ---@type rps_status - unit.reactor_data.mek_status = mek_status ---@type mek_status - - -- if status hasn't been received, mek_status = {} - if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then - burn_rate = unit.reactor_data.mek_status.act_burn_rate - burn_rate_sum = burn_rate_sum + burn_rate - end - - if unit.reactor_data.mek_status.status then - unit.unit_ps.publish("computed_status", 5) -- running - else - if unit.reactor_data.no_reactor then - unit.unit_ps.publish("computed_status", 3) -- faulted - elseif not unit.reactor_data.formed then - unit.unit_ps.publish("computed_status", 2) -- multiblock not formed - elseif unit.reactor_data.rps_status.force_dis then - unit.unit_ps.publish("computed_status", 7) -- reactor force disabled - elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then - unit.unit_ps.publish("computed_status", 6) -- SCRAM - else - unit.unit_ps.publish("computed_status", 4) -- disabled - end - end - for key, val in pairs(unit.reactor_data) do if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then unit.unit_ps.publish(key, val) end end - if type(unit.reactor_data.rps_status) == "table" then - for key, val in pairs(unit.reactor_data.rps_status) do + unit.reactor_data.rps_status = rps_status + for key, val in pairs(rps_status) do + unit.unit_ps.publish(key, val) + end + + if next(mek_status) then + unit.reactor_data.mek_status = mek_status + for key, val in pairs(mek_status) do unit.unit_ps.publish(key, val) end end - if type(unit.reactor_data.mek_status) == "table" then - for key, val in pairs(unit.reactor_data.mek_status) do - unit.unit_ps.publish(key, val) + burn_rate = unit.reactor_data.mek_status.act_burn_rate + burn_rate_sum = burn_rate_sum + burn_rate + + if unit.reactor_data.mek_status.status then + unit.unit_ps.publish("computed_status", 5) -- running + else + if unit.reactor_data.no_reactor then + unit.unit_ps.publish("computed_status", 3) -- faulted + elseif not unit.reactor_data.formed then + unit.unit_ps.publish("computed_status", 2) -- multiblock not formed + elseif unit.reactor_data.rps_status.force_dis then + unit.unit_ps.publish("computed_status", 7) -- reactor force disabled + elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then + unit.unit_ps.publish("computed_status", 6) -- SCRAM + else + unit.unit_ps.publish("computed_status", 4) -- disabled end end @@ -917,24 +908,24 @@ function iocontrol.update_unit_statuses(statuses) for id, boiler in pairs(rtu_statuses.boilers) do if type(unit.boiler_data_tbl[id]) == "table" then - local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db - local ps = unit.boiler_ps_tbl[id] ---@type psil + local data = unit.boiler_data_tbl[id] + local ps = unit.boiler_ps_tbl[id] local rtu_faulted = _record_multiblock_status(boiler, data, ps) unit.rtu_hw.boilers[id].faulted = rtu_faulted if rtu_faulted then - ps.publish("computed_status", 3) -- faulted + ps.publish("computed_status", 3) -- faulted elseif data.formed then boil_sum = boil_sum + data.state.boil_rate if data.state.boil_rate > 0 then - ps.publish("computed_status", 5) -- active + ps.publish("computed_status", 5) -- active else - ps.publish("computed_status", 4) -- idle + ps.publish("computed_status", 4) -- idle end else - ps.publish("computed_status", 2) -- not formed + ps.publish("computed_status", 2) -- not formed end else log.debug(util.c(log_header, "invalid boiler id ", id)) @@ -964,26 +955,26 @@ function iocontrol.update_unit_statuses(statuses) for id, turbine in pairs(rtu_statuses.turbines) do if type(unit.turbine_data_tbl[id]) == "table" then - local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db - local ps = unit.turbine_ps_tbl[id] ---@type psil + local data = unit.turbine_data_tbl[id] + local ps = unit.turbine_ps_tbl[id] local rtu_faulted = _record_multiblock_status(turbine, data, ps) unit.rtu_hw.turbines[id].faulted = rtu_faulted if rtu_faulted then - ps.publish("computed_status", 3) -- faulted + ps.publish("computed_status", 3) -- faulted elseif data.formed then flow_sum = flow_sum + data.state.flow_rate if data.tanks.energy_fill >= 0.99 then - ps.publish("computed_status", 6) -- trip + ps.publish("computed_status", 6) -- trip elseif data.state.flow_rate < 100 then - ps.publish("computed_status", 4) -- idle + ps.publish("computed_status", 4) -- idle else - ps.publish("computed_status", 5) -- active + ps.publish("computed_status", 5) -- active end else - ps.publish("computed_status", 2) -- not formed + ps.publish("computed_status", 2) -- not formed end else log.debug(util.c(log_header, "invalid turbine id ", id)) @@ -1008,23 +999,23 @@ function iocontrol.update_unit_statuses(statuses) for id, tank in pairs(rtu_statuses.tanks) do if type(unit.tank_data_tbl[id]) == "table" then - local data = unit.tank_data_tbl[id] ---@type dynamicv_session_db - local ps = unit.tank_ps_tbl[id] ---@type psil + local data = unit.tank_data_tbl[id] + local ps = unit.tank_ps_tbl[id] local rtu_faulted = _record_multiblock_status(tank, data, ps) if rtu_faulted then - ps.publish("computed_status", 3) -- faulted + ps.publish("computed_status", 3) -- faulted elseif data.formed then if data.tanks.fill >= 0.99 then - ps.publish("computed_status", 6) -- full + ps.publish("computed_status", 6) -- full elseif data.tanks.fill < 0.20 then - ps.publish("computed_status", 5) -- low + ps.publish("computed_status", 5) -- low else - ps.publish("computed_status", 4) -- on-line + ps.publish("computed_status", 4) -- on-line end else - ps.publish("computed_status", 2) -- not formed + ps.publish("computed_status", 2) -- not formed end else log.debug(util.c(log_header, "invalid dynamic tank id ", id)) @@ -1144,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 5f7f0102..78be1729 100644 --- a/coordinator/process.lua +++ b/coordinator/process.lua @@ -25,17 +25,17 @@ 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 = {}, - waste_product = PRODUCT.PLUTONIUM, + limits = {}, ---@type number[] + waste_product = PRODUCT.PLUTONIUM, ---@type WASTE_PRODUCT pu_fallback = false, sps_low_power = false }, - waste_modes = {}, - priority_groups = {} + waste_modes = {}, ---@type WASTE_MODE[] + priority_groups = {} ---@type AUTO_GROUP[] }, commands = { unit = {}, ---@type process_command_state[][] @@ -46,9 +46,10 @@ local pctl = { ---@class process_command_state ---@field active boolean if this command is live ---@field timeout integer expiration time of this command request ----@field requestors table list of callbacks from the requestors +---@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 @@ -80,8 +83,8 @@ function process.init(iocontrol, coord_comms) ctl_proc.limits[i] = 0.1 end - local ctrl_states = settings.get("ControlStates", {}) - local config = ctrl_states.process ---@type sys_auto_config + local ctrl_states = settings.get("ControlStates", {}) ---@type sys_control_states + local config = ctrl_states.process -- facility auto control configuration if type(config) == "table" then @@ -103,7 +106,7 @@ function process.init(iocontrol, coord_comms) pctl.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power) for id = 1, math.min(#ctl_proc.limits, pctl.io.facility.num_units) do - local unit = pctl.io.units[id] ---@type ioctl_unit + local unit = pctl.io.units[id] unit.unit_ps.publish("burn_limit", ctl_proc.limits[id]) end @@ -116,7 +119,7 @@ function process.init(iocontrol, coord_comms) end -- unit waste states - local waste_modes = ctrl_states.waste_modes ---@type table|nil + local waste_modes = ctrl_states.waste_modes if type(waste_modes) == "table" then for id, mode in pairs(waste_modes) do pctl.control_states.waste_modes[id] = mode @@ -127,7 +130,7 @@ function process.init(iocontrol, coord_comms) end -- unit priority groups - local prio_groups = ctrl_states.priority_groups ---@type table|nil + local prio_groups = ctrl_states.priority_groups if type(prio_groups) == "table" then for id, group in pairs(prio_groups) do pctl.control_states.priority_groups[id] = group @@ -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,11 +478,11 @@ 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 table unit burn rate limits +---@param limits number[] unit burn rate limits function process.save(mode, burn_target, charge_target, gen_target, limits) log.debug("PROCESS: SAVE") @@ -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] ---@type ioctl_unit - 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/renderer.lua b/coordinator/renderer.lua index f2d2418e..cfa50131 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -19,7 +19,7 @@ local unit_view = require("coordinator.ui.layout.unit_view") local core = require("graphics.core") local flasher = require("graphics.flasher") -local DisplayBox = require("graphics.elements.displaybox") +local DisplayBox = require("graphics.elements.DisplayBox") local log_render = coordinator.log_render @@ -30,20 +30,20 @@ local renderer = {} local engine = { color_mode = 1, ---@type COLOR_MODE monitors = nil, ---@type monitors_struct|nil - dmesg_window = nil, ---@type table|nil + dmesg_window = nil, ---@type Window|nil ui_ready = false, fp_ready = false, ui = { - front_panel = nil, ---@type graphics_element|nil - main_display = nil, ---@type graphics_element|nil - flow_display = nil, ---@type graphics_element|nil - unit_displays = {} + front_panel = nil, ---@type DisplayBox|nil + main_display = nil, ---@type DisplayBox|nil + flow_display = nil, ---@type DisplayBox|nil + unit_displays = {} ---@type (DisplayBox|nil)[] }, disable_flow_view = false } -- init a display to the "default", but set text scale to 0.5 ----@param monitor table monitor +---@param monitor Monitor monitor local function _init_display(monitor) monitor.setTextScale(0.5) monitor.setTextColor(colors.white) @@ -64,7 +64,7 @@ local function _init_display(monitor) end -- print out that the monitor is too small ----@param monitor table monitor +---@param monitor Monitor monitor local function _print_too_small(monitor) monitor.setCursorPos(1, 1) monitor.setBackgroundColor(colors.black) @@ -275,7 +275,7 @@ function renderer.fp_ready() return engine.fp_ready end function renderer.ui_ready() return engine.ui_ready end -- handle a monitor peripheral being disconnected ----@param device table monitor +---@param device Monitor monitor ---@return boolean is_used if the monitor is one of the configured monitors function renderer.handle_disconnect(device) local is_used = false @@ -326,7 +326,7 @@ end -- handle a monitor peripheral being reconnected ---@param name string monitor name ----@param device table monitor +---@param device Monitor monitor ---@return boolean is_used if the monitor is one of the configured monitors function renderer.handle_reconnect(name, device) local is_used = false @@ -373,7 +373,7 @@ function renderer.handle_resize(name) 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 + local device = engine.monitors.main ---@type Monitor -- this is necessary if the bottom left block was broken and on reconnect _init_display(device) @@ -416,7 +416,7 @@ function renderer.handle_resize(name) 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 + local device = engine.monitors.flow ---@type Monitor -- this is necessary if the bottom left block was broken and on reconnect _init_display(device) diff --git a/coordinator/session/apisessions.lua b/coordinator/session/apisessions.lua index 5c4a38a7..30fee091 100644 --- a/coordinator/session/apisessions.lua +++ b/coordinator/session/apisessions.lua @@ -13,7 +13,7 @@ local self = { nic = nil, ---@type nic config = nil, ---@type crd_config next_id = 0, - sessions = {} + sessions = {} ---@type pkt_session_struct[] } -- PRIVATE FUNCTIONS -- @@ -129,7 +129,7 @@ end ---@param timer_event number function apisessions.check_all_watchdogs(timer_event) for i = 1, #self.sessions do - local session = self.sessions[i] ---@type pkt_session_struct + local session = self.sessions[i] if session.open then local triggered = session.instance.check_wd(timer_event) if triggered then @@ -143,7 +143,7 @@ end -- iterate all the API sessions function apisessions.iterate_all() for i = 1, #self.sessions do - local session = self.sessions[i] ---@type pkt_session_struct + local session = self.sessions[i] if session.open and session.instance.iterate() then _api_handle_outq(session) @@ -168,7 +168,7 @@ end -- close all open connections function apisessions.close_all() for i = 1, #self.sessions do - local session = self.sessions[i] ---@type pkt_session_struct + local session = self.sessions[i] if session.open then _shutdown(session) end end diff --git a/coordinator/session/pocket.lua b/coordinator/session/pocket.lua index 02053978..cb38a619 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 @@ -217,7 +237,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout) _send(CRDN_TYPE.API_GET_FAC, data) elseif pkt.type == CRDN_TYPE.API_GET_UNIT then if pkt.length == 1 and type(pkt.data[1]) == "number" then - local u = db.units[pkt.data[1]] ---@type ioctl_unit + local u = db.units[pkt.data[1]] if u then local data = { @@ -238,6 +258,58 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout) _send(CRDN_TYPE.API_GET_UNIT, data) end end + elseif pkt.type == CRDN_TYPE.API_GET_CTRL then + local data = {} + + for i = 1, #db.units do + local u = db.units[i] + + data[i] = { + u.connected, + u.reactor_data.rps_tripped, + u.reactor_data.mek_status.status, + u.reactor_data.mek_status.temp, + u.reactor_data.mek_status.burn_rate, + u.reactor_data.mek_status.act_burn_rate, + u.reactor_data.mek_struct.max_burn, + u.annunciator.AutoControl, + u.a_group + } + + 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/sounder.lua b/coordinator/sounder.lua index e7a02ac0..7ec48c52 100644 --- a/coordinator/sounder.lua +++ b/coordinator/sounder.lua @@ -9,7 +9,7 @@ local log = require("scada-common.log") local sounder = {} local alarm_ctl = { - speaker = nil, + speaker = nil, ---@type Speaker volume = 0.5, stream = audio.new_stream() } @@ -24,7 +24,7 @@ local function play() end -- initialize the annunciator alarm system ----@param speaker table speaker peripheral +---@param speaker Speaker speaker peripheral ---@param volume number speaker volume function sounder.init(speaker, volume) alarm_ctl.speaker = speaker @@ -36,7 +36,7 @@ function sounder.init(speaker, volume) end -- reconnect the speaker peripheral ----@param speaker table speaker peripheral +---@param speaker Speaker speaker peripheral function sounder.reconnect(speaker) alarm_ctl.speaker = speaker alarm_ctl.playing = false @@ -44,7 +44,7 @@ function sounder.reconnect(speaker) end -- set alarm tones ----@param states table alarm tone commands from supervisor +---@param states { [TONE]: boolean } alarm tone commands from supervisor function sounder.set(states) -- set tone states for id = 1, #states do alarm_ctl.stream.set_active(id, states[id]) end diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 52ed4a67..772b8b66 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") local threads = require("coordinator.threads") -local COORDINATOR_VERSION = "v1.5.8" +local COORDINATOR_VERSION = "v1.5.13" local CHUNK_LOAD_DELAY_S = 30.0 @@ -152,7 +152,7 @@ local function main() -- core coordinator devices crd_dev = { modem = ppm.get_wireless_modem(), - speaker = ppm.get_device("speaker") + speaker = ppm.get_device("speaker") ---@type Speaker|nil }, -- system objects diff --git a/coordinator/threads.lua b/coordinator/threads.lua index cefe705e..03921784 100644 --- a/coordinator/threads.lua +++ b/coordinator/threads.lua @@ -68,6 +68,7 @@ function threads.thread__main(smem) if type ~= nil and device ~= nil then if type == "modem" then + ---@cast device Modem -- we only really care if this is our wireless modem -- if it is another modem, handle other peripheral losses separately if nic.is_modem(device) then @@ -91,8 +92,10 @@ function threads.thread__main(smem) log_sys("non-comms modem disconnected") end elseif type == "monitor" then + ---@cast device Monitor smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device) elseif type == "speaker" then + ---@cast device Speaker log_sys("lost alarm sounder speaker") iocontrol.fp_has_speaker(false) end @@ -102,6 +105,7 @@ function threads.thread__main(smem) if type ~= nil and device ~= nil then if type == "modem" then + ---@cast device Modem if device.isWireless() and not nic.is_connected() then -- reconnected modem log_sys("comms modem reconnected") @@ -113,8 +117,10 @@ function threads.thread__main(smem) log_sys("wired modem reconnected") end elseif type == "monitor" then + ---@cast device Monitor smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device }) elseif type == "speaker" then + ---@cast device Speaker log_sys("alarm sounder speaker reconnected") sounder.reconnect(device) iocontrol.fp_has_speaker(true) diff --git a/coordinator/ui/components/boiler.lua b/coordinator/ui/components/boiler.lua index 86866f5a..38bd05c8 100644 --- a/coordinator/ui/components/boiler.lua +++ b/coordinator/ui/components/boiler.lua @@ -4,18 +4,18 @@ local style = require("coordinator.ui.style") local core = require("graphics.core") -local Rectangle = require("graphics.elements.rectangle") -local TextBox = require("graphics.elements.textbox") +local Rectangle = require("graphics.elements.Rectangle") +local TextBox = require("graphics.elements.TextBox") -local DataIndicator = require("graphics.elements.indicators.data") -local StateIndicator = require("graphics.elements.indicators.state") -local VerticalBar = require("graphics.elements.indicators.vbar") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local StateIndicator = require("graphics.elements.indicators.StateIndicator") +local VerticalBar = require("graphics.elements.indicators.VerticalBar") local cpair = core.cpair local border = core.border -- new boiler view ----@param root graphics_element parent +---@param root Container parent ---@param x integer top left x ---@param y integer top left y ---@param ps psil ps interface diff --git a/coordinator/ui/components/imatrix.lua b/coordinator/ui/components/imatrix.lua index cfec34dd..186f7bda 100644 --- a/coordinator/ui/components/imatrix.lua +++ b/coordinator/ui/components/imatrix.lua @@ -6,15 +6,15 @@ local style = require("coordinator.ui.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local Rectangle = require("graphics.elements.rectangle") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local Rectangle = require("graphics.elements.Rectangle") +local TextBox = require("graphics.elements.TextBox") -local DataIndicator = require("graphics.elements.indicators.data") -local IndicatorLight = require("graphics.elements.indicators.light") -local PowerIndicator = require("graphics.elements.indicators.power") -local StateIndicator = require("graphics.elements.indicators.state") -local VerticalBar = require("graphics.elements.indicators.vbar") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local IndicatorLight = require("graphics.elements.indicators.IndicatorLight") +local PowerIndicator = require("graphics.elements.indicators.PowerIndicator") +local StateIndicator = require("graphics.elements.indicators.StateIndicator") +local VerticalBar = require("graphics.elements.indicators.VerticalBar") local cpair = core.cpair local border = core.border @@ -22,7 +22,7 @@ local border = core.border local ALIGN = core.ALIGN -- new induction matrix view ----@param root graphics_element parent +---@param root Container parent ---@param x integer top left x ---@param y integer top left y ---@param data imatrix_session_db matrix data diff --git a/coordinator/ui/components/pkt_entry.lua b/coordinator/ui/components/pkt_entry.lua index e377e2e4..78084f9c 100644 --- a/coordinator/ui/components/pkt_entry.lua +++ b/coordinator/ui/components/pkt_entry.lua @@ -8,17 +8,17 @@ local style = require("coordinator.ui.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") -local DataIndicator = require("graphics.elements.indicators.data") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") local ALIGN = core.ALIGN local cpair = core.cpair -- create a pocket list entry ----@param parent graphics_element parent +---@param parent ListBox parent ---@param id integer PKT session ID local function init(parent, id) local s_hi_box = style.fp_theme.highlight_box @@ -29,7 +29,7 @@ local function init(parent, id) local ps = iocontrol.get_db().fp.ps -- root div - local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} + local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=s_hi_bright} local ps_prefix = "pkt_" .. id .. "_" diff --git a/coordinator/ui/components/process_ctl.lua b/coordinator/ui/components/process_ctl.lua index 85e04365..a70bcd6d 100644 --- a/coordinator/ui/components/process_ctl.lua +++ b/coordinator/ui/components/process_ctl.lua @@ -8,20 +8,20 @@ local style = require("coordinator.ui.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local Rectangle = require("graphics.elements.rectangle") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local Rectangle = require("graphics.elements.Rectangle") +local TextBox = require("graphics.elements.TextBox") -local DataIndicator = require("graphics.elements.indicators.data") -local IndicatorLight = require("graphics.elements.indicators.light") -local RadIndicator = require("graphics.elements.indicators.rad") -local StateIndicator = require("graphics.elements.indicators.state") -local TriIndicatorLight = require("graphics.elements.indicators.trilight") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local IndicatorLight = require("graphics.elements.indicators.IndicatorLight") +local RadIndicator = require("graphics.elements.indicators.RadIndicator") +local StateIndicator = require("graphics.elements.indicators.StateIndicator") +local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight") -local Checkbox = require("graphics.elements.controls.checkbox") -local HazardButton = require("graphics.elements.controls.hazard_button") -local RadioButton = require("graphics.elements.controls.radio_button") -local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") +local Checkbox = require("graphics.elements.controls.Checkbox") +local HazardButton = require("graphics.elements.controls.HazardButton") +local NumericSpinbox = require("graphics.elements.controls.NumericSpinbox") +local RadioButton = require("graphics.elements.controls.RadioButton") local ALIGN = core.ALIGN @@ -33,7 +33,7 @@ local bw_fg_bg = style.bw_fg_bg local period = core.flasher.PERIOD -- new process control view ----@param root graphics_element parent +---@param root Container parent ---@param x integer top left x ---@param y integer top left y local function new_view(root, x, y) @@ -131,7 +131,7 @@ local function new_view(root, x, y) TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2} local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=s_hi_box} - local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} + local b_target = NumericSpinbox{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} TextBox{parent=burn_target,x=18,y=2,text="mB/t",fg_bg=style.theme.label_fg} local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn} @@ -142,7 +142,7 @@ local function new_view(root, x, y) TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2} local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box} - local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} + local c_target = NumericSpinbox{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} TextBox{parent=chg_target,x=18,y=2,text="M"..db.energy_label,fg_bg=style.theme.label_fg} local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="M"..db.energy_label,commas=true,lu_colors=black,width=23,fg_bg=blk_brn} @@ -153,7 +153,7 @@ local function new_view(root, x, y) TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2} local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box} - local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} + local g_target = NumericSpinbox{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} TextBox{parent=gen_target,x=18,y=2,text="k"..db.energy_label.."/t",fg_bg=style.theme.label_fg} local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="k"..db.energy_label.."/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn} @@ -177,7 +177,7 @@ local function new_view(root, x, y) local cur_lu = style.theme.disabled if i <= facility.num_units then - unit = units[i] ---@type ioctl_unit + unit = units[i] tag_fg_bg = cpair(colors.black, colors.lightBlue) lim_fg_bg = s_hi_box label_fg = style.theme.label_fg @@ -191,7 +191,7 @@ local function new_view(root, x, y) TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2} local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=s_hi_box} - local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled,fg_bg=lim_fg_bg} + local lim = NumericSpinbox{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled,fg_bg=lim_fg_bg} TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,fg_bg=label_fg} local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg} @@ -234,7 +234,7 @@ local function new_view(root, x, y) local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(ind_red.fgd,ind_off),flash=true,period=period.BLINK_250_MS} if i <= facility.num_units then - local unit = units[i] ---@type ioctl_unit + local unit = units[i] ready.register(unit.unit_ps, "U_AutoReady", ready.update) degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update) @@ -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) @@ -323,7 +321,7 @@ local function new_view(root, x, y) local waste_status = Div{parent=proc,width=24,height=4,x=57,y=1,} for i = 1, facility.num_units do - local unit = units[i] ---@type ioctl_unit + local unit = units[i] TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8} local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht} diff --git a/coordinator/ui/components/reactor.lua b/coordinator/ui/components/reactor.lua index 2319f762..f1cec6d9 100644 --- a/coordinator/ui/components/reactor.lua +++ b/coordinator/ui/components/reactor.lua @@ -6,18 +6,18 @@ local style = require("coordinator.ui.style") local core = require("graphics.core") -local Rectangle = require("graphics.elements.rectangle") -local TextBox = require("graphics.elements.textbox") +local Rectangle = require("graphics.elements.Rectangle") +local TextBox = require("graphics.elements.TextBox") -local DataIndicator = require("graphics.elements.indicators.data") -local HorizontalBar = require("graphics.elements.indicators.hbar") -local StateIndicator = require("graphics.elements.indicators.state") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local HorizontalBar = require("graphics.elements.indicators.HorizontalBar") +local StateIndicator = require("graphics.elements.indicators.StateIndicator") local cpair = core.cpair local border = core.border -- create new reactor view ----@param root graphics_element parent +---@param root Container parent ---@param x integer top left x ---@param y integer top left y ---@param ps psil ps interface diff --git a/coordinator/ui/components/turbine.lua b/coordinator/ui/components/turbine.lua index fd6049a0..6ce3d117 100644 --- a/coordinator/ui/components/turbine.lua +++ b/coordinator/ui/components/turbine.lua @@ -4,19 +4,19 @@ local style = require("coordinator.ui.style") local core = require("graphics.core") -local Rectangle = require("graphics.elements.rectangle") -local TextBox = require("graphics.elements.textbox") +local Rectangle = require("graphics.elements.Rectangle") +local TextBox = require("graphics.elements.TextBox") -local DataIndicator = require("graphics.elements.indicators.data") -local PowerIndicator = require("graphics.elements.indicators.power") -local StateIndicator = require("graphics.elements.indicators.state") -local VerticalBar = require("graphics.elements.indicators.vbar") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local PowerIndicator = require("graphics.elements.indicators.PowerIndicator") +local StateIndicator = require("graphics.elements.indicators.StateIndicator") +local VerticalBar = require("graphics.elements.indicators.VerticalBar") local cpair = core.cpair local border = core.border -- new turbine view ----@param root graphics_element parent +---@param root Container parent ---@param x integer top left x ---@param y integer top left y ---@param ps psil ps interface diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index fcfd0622..8c9ce7a8 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -11,23 +11,23 @@ local style = require("coordinator.ui.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local Rectangle = require("graphics.elements.rectangle") -local TextBox = require("graphics.elements.textbox") - -local AlarmLight = require("graphics.elements.indicators.alight") -local CoreMap = require("graphics.elements.indicators.coremap") -local DataIndicator = require("graphics.elements.indicators.data") -local IndicatorLight = require("graphics.elements.indicators.light") -local RadIndicator = require("graphics.elements.indicators.rad") -local TriIndicatorLight = require("graphics.elements.indicators.trilight") -local VerticalBar = require("graphics.elements.indicators.vbar") - -local HazardButton = require("graphics.elements.controls.hazard_button") -local MultiButton = require("graphics.elements.controls.multi_button") -local PushButton = require("graphics.elements.controls.push_button") -local RadioButton = require("graphics.elements.controls.radio_button") -local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") +local Div = require("graphics.elements.Div") +local Rectangle = require("graphics.elements.Rectangle") +local TextBox = require("graphics.elements.TextBox") + +local AlarmLight = require("graphics.elements.indicators.AlarmLight") +local CoreMap = require("graphics.elements.indicators.CoreMap") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local IndicatorLight = require("graphics.elements.indicators.IndicatorLight") +local RadIndicator = require("graphics.elements.indicators.RadIndicator") +local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight") +local VerticalBar = require("graphics.elements.indicators.VerticalBar") + +local HazardButton = require("graphics.elements.controls.HazardButton") +local MultiButton = require("graphics.elements.controls.MultiButton") +local NumericSpinbox = require("graphics.elements.controls.NumericSpinbox") +local PushButton = require("graphics.elements.controls.PushButton") +local RadioButton = require("graphics.elements.controls.RadioButton") local AUTO_GROUP = types.AUTO_GROUP @@ -42,7 +42,7 @@ local gry_wht = style.gray_white local period = core.flasher.PERIOD -- create a unit view ----@param parent graphics_element parent +---@param parent Container parent ---@param id integer local function init(parent, id) local s_hi_box = style.theme.highlight_box @@ -62,7 +62,7 @@ local function init(parent, id) local ind_wht = style.ind_wht local db = iocontrol.get_db() - local unit = db.units[id] ---@type ioctl_unit + local unit = db.units[id] local f_ps = db.facility.ps local main = Div{parent=parent,x=1,y=1} @@ -361,7 +361,7 @@ local function init(parent, id) ---------------------- local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=s_hi_box} - local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} + local burn_rate = NumericSpinbox{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled} TextBox{parent=burn_control,x=9,y=2,text="mB/t",fg_bg=style.theme.label_fg} local set_burn = function () unit.set_burn(burn_rate.get_value()) end @@ -381,12 +381,10 @@ local function init(parent, id) db.process.unit_ack[id].on_ack_alarms = ack_a.on_response local function start_button_en_check() - if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then - local can_start = (not unit.reactor_data.mek_status.status) and - (not unit.reactor_data.rps_tripped) and - (unit.a_group == AUTO_GROUP.MANUAL) - if can_start then start.enable() else start.disable() end - end + local can_start = (not unit.reactor_data.mek_status.status) and + (not unit.reactor_data.rps_tripped) and + (unit.a_group == AUTO_GROUP.MANUAL) + if can_start then start.enable() else start.disable() end end start.register(u_ps, "status", start_button_en_check) diff --git a/coordinator/ui/components/unit_flow.lua b/coordinator/ui/components/unit_flow.lua index 9ba29bef..27e4d0d3 100644 --- a/coordinator/ui/components/unit_flow.lua +++ b/coordinator/ui/components/unit_flow.lua @@ -8,16 +8,16 @@ local style = require("coordinator.ui.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local PipeNetwork = require("graphics.elements.pipenet") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local PipeNetwork = require("graphics.elements.PipeNetwork") +local TextBox = require("graphics.elements.TextBox") -local Rectangle = require("graphics.elements.rectangle") +local Rectangle = require("graphics.elements.Rectangle") -local DataIndicator = require("graphics.elements.indicators.data") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") -local IndicatorLight = require("graphics.elements.indicators.light") -local TriIndicatorLight = require("graphics.elements.indicators.trilight") +local IndicatorLight = require("graphics.elements.indicators.IndicatorLight") +local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight") local ALIGN = core.ALIGN @@ -31,7 +31,7 @@ local wh_gray = style.wh_gray local lg_gray = style.lg_gray -- make a new unit flow window ----@param parent graphics_element parent +---@param parent Container parent ---@param x integer top left x ---@param y integer top left y ---@param wide boolean whether to render wide version diff --git a/coordinator/ui/components/unit_overview.lua b/coordinator/ui/components/unit_overview.lua index fc4ac4ee..a3f41c12 100644 --- a/coordinator/ui/components/unit_overview.lua +++ b/coordinator/ui/components/unit_overview.lua @@ -10,16 +10,16 @@ local reactor_view = require("coordinator.ui.components.reactor") local boiler_view = require("coordinator.ui.components.boiler") local turbine_view = require("coordinator.ui.components.turbine") -local Div = require("graphics.elements.div") -local PipeNetwork = require("graphics.elements.pipenet") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local PipeNetwork = require("graphics.elements.PipeNetwork") +local TextBox = require("graphics.elements.TextBox") local ALIGN = core.ALIGN local pipe = core.pipe -- make a new unit overview window ----@param parent graphics_element parent +---@param parent Container parent ---@param x integer top left x ---@param y integer top left y ---@param unit ioctl_unit unit database entry diff --git a/coordinator/ui/layout/flow_view.lua b/coordinator/ui/layout/flow_view.lua index 7a7f57d3..b002f243 100644 --- a/coordinator/ui/layout/flow_view.lua +++ b/coordinator/ui/layout/flow_view.lua @@ -13,15 +13,15 @@ local unit_flow = require("coordinator.ui.components.unit_flow") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local PipeNetwork = require("graphics.elements.pipenet") -local Rectangle = require("graphics.elements.rectangle") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local PipeNetwork = require("graphics.elements.PipeNetwork") +local Rectangle = require("graphics.elements.Rectangle") +local TextBox = require("graphics.elements.TextBox") -local DataIndicator = require("graphics.elements.indicators.data") -local HorizontalBar = require("graphics.elements.indicators.hbar") -local IndicatorLight = require("graphics.elements.indicators.light") -local StateIndicator = require("graphics.elements.indicators.state") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local HorizontalBar = require("graphics.elements.indicators.HorizontalBar") +local IndicatorLight = require("graphics.elements.indicators.IndicatorLight") +local StateIndicator = require("graphics.elements.indicators.StateIndicator") local CONTAINER_MODE = types.CONTAINER_MODE @@ -34,7 +34,7 @@ local pipe = core.pipe local wh_gray = style.wh_gray -- create new flow view ----@param main graphics_element main displaybox +---@param main DisplayBox main displaybox local function init(main) local s_hi_bright = style.theme.highlight_box_bright local s_field = style.theme.field_box @@ -84,8 +84,7 @@ local function init(main) table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true)) table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true)) - local u = units[i] ---@type ioctl_unit - local x = util.trinary(u.num_boilers == 0, 45, 84) + local x = util.trinary(units[i].num_boilers == 0, 45, 84) table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true)) end end @@ -102,8 +101,7 @@ local function init(main) table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true)) end - local u = units[i] ---@type ioctl_unit - local x = util.trinary(u.num_boilers == 0, 45, 84) + local x = util.trinary(units[i].num_boilers == 0, 45, 84) table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true)) end end diff --git a/coordinator/ui/layout/front_panel.lua b/coordinator/ui/layout/front_panel.lua index 3573fb20..46d506b3 100644 --- a/coordinator/ui/layout/front_panel.lua +++ b/coordinator/ui/layout/front_panel.lua @@ -14,16 +14,16 @@ local pkt_entry = require("coordinator.ui.components.pkt_entry") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local TabBar = require("graphics.elements.controls.tabbar") +local TabBar = require("graphics.elements.controls.TabBar") -local LED = require("graphics.elements.indicators.led") -local LEDPair = require("graphics.elements.indicators.ledpair") -local RGBLED = require("graphics.elements.indicators.ledrgb") +local LED = require("graphics.elements.indicators.LED") +local LEDPair = require("graphics.elements.indicators.LEDPair") +local RGBLED = require("graphics.elements.indicators.RGBLED") local LINK_STATE = types.PANEL_LINK_STATE @@ -34,7 +34,7 @@ local cpair = core.cpair local led_grn = style.led_grn -- create new front panel view ----@param panel graphics_element main displaybox +---@param panel DisplayBox main displaybox ---@param num_units integer number of units (number of unit monitors) local function init(panel, num_units) local ps = iocontrol.get_db().fp.ps @@ -146,7 +146,7 @@ local function init(panel, num_units) local api_page = Div{parent=page_div,x=1,y=1,hidden=true} local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} - local _ = Div{parent=api_list,height=1,hidden=true} -- padding + local _ = Div{parent=api_list,height=1} -- padding -- assemble page panes diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua index 33ed43cc..ebc9cc35 100644 --- a/coordinator/ui/layout/main_view.lua +++ b/coordinator/ui/layout/main_view.lua @@ -14,14 +14,14 @@ local unit_overview = require("coordinator.ui.components.unit_overview") local core = require("graphics.core") -local TextBox = require("graphics.elements.textbox") +local TextBox = require("graphics.elements.TextBox") -local DataIndicator = require("graphics.elements.indicators.data") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") local ALIGN = core.ALIGN -- create new main view ----@param main graphics_element main displaybox +---@param main DisplayBox main displaybox local function init(main) local s_header = style.theme.header @@ -37,7 +37,8 @@ local function init(main) ping.register(facility.ps, "sv_ping", ping.update) datetime.register(facility.ps, "date_time", datetime.set_value) - local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element + ---@type Div, Div, Div, Div + local uo_1, uo_2, uo_3, uo_4 local cnc_y_start = 3 local row_1_height = 0 diff --git a/coordinator/ui/layout/unit_view.lua b/coordinator/ui/layout/unit_view.lua index 7ac1b7c6..7a8ec26d 100644 --- a/coordinator/ui/layout/unit_view.lua +++ b/coordinator/ui/layout/unit_view.lua @@ -5,7 +5,7 @@ local unit_detail = require("coordinator.ui.components.unit_detail") -- create a unit view ----@param main graphics_element main displaybox +---@param main DisplayBox main displaybox ---@param id integer local function init(main, id) unit_detail(main, id) diff --git a/coordinator/ui/pgi.lua b/coordinator/ui/pgi.lua index b6034652..70ca0b88 100644 --- a/coordinator/ui/pgi.lua +++ b/coordinator/ui/pgi.lua @@ -8,15 +8,17 @@ local util = require("scada-common.util") local pgi = {} local data = { - pkt_list = nil, ---@type nil|graphics_element - pkt_entry = nil, ---@type function + pkt_list = nil, ---@type ListBox|nil + pkt_entry = nil, ---@type function -- session entries - s_entries = { pkt = {} } + s_entries = { + pkt = {} ---@type Div[] + } } -- link list boxes ----@param pkt_list graphics_element pocket list element ----@param pkt_entry function pocket entry constructor +---@param pkt_list ListBox pocket list element +---@param pkt_entry fun(parent: ListBox, id: integer) : Div pocket entry constructor function pgi.link_elements(pkt_list, pkt_entry) data.pkt_list = pkt_list data.pkt_entry = pkt_entry diff --git a/graphics/core.lua b/graphics/core.lua index fe3b291c..54396039 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -7,7 +7,7 @@ local flasher = require("graphics.flasher") local core = {} -core.version = "2.3.4" +core.version = "2.4.5" core.flasher = flasher core.events = events @@ -17,6 +17,8 @@ core.events = events ---@enum ALIGN core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 } +---@alias Container DisplayBox|Div|ListBox|MultiPane|AppMultiPane|Rectangle + ---@class graphics_border ---@field width integer ---@field color color @@ -124,10 +126,10 @@ end -- Interactive Field Manager ---@param e graphics_base element ----@param max_len any max value length ----@param fg_bg any enabled fg/bg ----@param dis_fg_bg any disabled fg/bg ----@param align_right any true to align content right while unfocused +---@param max_len integer max value length +---@param fg_bg cpair enabled fg/bg +---@param dis_fg_bg? cpair disabled fg/bg +---@param align_right? boolean true to align content right while unfocused function core.new_ifield(e, max_len, fg_bg, dis_fg_bg, align_right) local self = { frame_start = 1, diff --git a/graphics/element.lua b/graphics/element.lua index 1e94edd1..cde4fe78 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -11,9 +11,9 @@ local events = core.events local element = {} ----@class graphics_args_generic ----@field window? table ----@field parent? graphics_element +---@class graphics_args +---@field window? Window base window to use, only root elements should use this +---@field parent? graphics_element parent element, if not a root element ---@field id? string element id ---@field x? integer 1 if omitted ---@field y? integer next line if omitted @@ -24,47 +24,6 @@ local element = {} ---@field hidden? boolean true to hide on initial draw ---@field can_focus? boolean true if this element can be focused, false by default ----@alias graphics_args graphics_args_generic ----|waiting_args ----|app_button_args ----|checkbox_args ----|hazard_button_args ----|multi_button_args ----|push_button_args ----|radio_2d_args ----|radio_button_args ----|sidebar_args ----|spinbox_args ----|switch_button_args ----|tabbar_args ----|number_field_args ----|text_field_args ----|alarm_indicator_light ----|core_map_args ----|data_indicator_args ----|hbar_args ----|icon_indicator_args ----|indicator_led_args ----|indicator_led_pair_args ----|indicator_led_rgb_args ----|indicator_light_args ----|power_indicator_args ----|rad_indicator_args ----|signal_bar_args ----|state_indicator_args ----|tristate_indicator_light_args ----|vbar_args ----|app_multipane_args ----|colormap_args ----|displaybox_args ----|div_args ----|listbox_args ----|multipane_args ----|pipenet_args ----|rectangle_args ----|textbox_args ----|tiling_args - ---@class element_subscription ---@field ps psil ps used ---@field key string data key @@ -92,14 +51,14 @@ function element.new(args, constraint, child_offset_x, child_offset_y) is_root = args.parent == nil, elem_type = debug.getinfo(2).name, define_completed = false, - p_window = nil, ---@type table + p_window = nil, ---@type Window position = events.new_coord_2d(1, 1), bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds offset_x = 0, offset_y = 0, next_y = 1, -- next child y coordinate - next_id = 0, -- next child ID - subscriptions = {}, + next_id = 1, -- next child ID + subscriptions = {}, ---@type { ps: psil, key: string, func: function }[] button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) }, focused = false, mt = {} @@ -109,13 +68,13 @@ function element.new(args, constraint, child_offset_x, child_offset_y) local protected = { enabled = true, value = nil, ---@type any - window = nil, ---@type table - content_window = nil, ---@type table|nil + window = nil, ---@type Window + content_window = nil, ---@type Window|nil mouse_window_shift = { x = 0, y = 0 }, fg_bg = core.cpair(colors.white, colors.black), frame = core.gframe(1, 1, 1, 1), - children = {}, - child_id_map = {} + children = {}, ---@type graphics_base[] + child_id_map = {} ---@type { [element_id]: integer } } -- element as string @@ -128,9 +87,9 @@ function element.new(args, constraint, child_offset_x, child_offset_y) setmetatable(public, self.mt) - ----------------------- - -- PRIVATE FUNCTIONS -- - ----------------------- + ------------------------------ + --#region PRIVATE FUNCTIONS -- + ------------------------------ -- use tab to jump to the next focusable field ---@param reverse boolean @@ -168,10 +127,10 @@ function element.new(args, constraint, child_offset_x, child_offset_y) end end - ---@param children table + ---@param children graphics_base[] local function traverse(children) for i = 1, #children do - local child = children[i] ---@type graphics_base + local child = children[i] handle_element(child.get()) if child.get().is_visible() then traverse(child.children) end end @@ -191,9 +150,11 @@ function element.new(args, constraint, child_offset_x, child_offset_y) end end - ------------------------- - -- PROTECTED FUNCTIONS -- - ------------------------- + --#endregion + + -------------------------------- + --#region PROTECTED FUNCTIONS -- + -------------------------------- -- prepare the template ---@param offset_x integer x offset for mouse events @@ -286,24 +247,29 @@ function element.new(args, constraint, child_offset_x, child_offset_y) -- alias functions - -- window set cursor position + -- window set cursor position
+ ---@see Window.setCursorPos ---@param x integer ---@param y integer function protected.w_set_cur(x, y) protected.window.setCursorPos(x, y) end - -- set background color + -- set background color
+ ---@see Window.setBackgroundColor ---@param c color function protected.w_set_bkg(c) protected.window.setBackgroundColor(c) end - -- set foreground (text) color + -- set foreground (text) color
+ ---@see Window.setTextColor ---@param c color function protected.w_set_fgd(c) protected.window.setTextColor(c) end - -- write text + -- write text
+ ---@see Window.write ---@param str string function protected.w_write(str) protected.window.write(str) end - -- blit text + -- blit text
+ ---@see Window.blit ---@param str string ---@param fg string ---@param bg string @@ -335,8 +301,10 @@ function element.new(args, constraint, child_offset_x, child_offset_y) -- report completion of element instantiation and get the public interface ---@nodiscard + ---@param redraw? boolean true to call redraw as part of completing this element ---@return graphics_element element, element_id id - function protected.complete() + function protected.complete(redraw) + if redraw then protected.redraw() end if args.parent ~= nil then args.parent.__child_ready(self.id, public) end return public, self.id end @@ -352,7 +320,7 @@ function element.new(args, constraint, child_offset_x, child_offset_y) -- focus this element and take away focus from all other elements function protected.take_focus() args.parent.__focus_child(public) end - -- action handlers -- + --#region Action Handlers -- luacheck: push ignore ---@diagnostic disable: unused-local, unused-vararg @@ -401,14 +369,12 @@ function element.new(args, constraint, child_offset_x, child_offset_y) function protected.handle_paste(text) end -- handle data value changes - ---@vararg any value(s) + ---@param ... any value(s) function protected.on_update(...) end - -- callback on control press responses - ---@param result any - function protected.response_callback(result) end + --#endregion - -- accessors and control -- + --#region Accessors and Control -- get value ---@nodiscard @@ -427,11 +393,11 @@ function element.new(args, constraint, child_offset_x, child_offset_y) function protected.set_max(max) end -- custom recolor command, varies by element if implemented - ---@vararg cpair|color color(s) + ---@param ... cpair|color color(s) function protected.recolor(...) end -- custom resize command, varies by element if implemented - ---@vararg integer sizing + ---@param ... integer sizing function protected.resize(...) end -- luacheck: pop @@ -446,9 +412,13 @@ function element.new(args, constraint, child_offset_x, child_offset_y) -- stop animations function protected.stop_anim() end - ----------- - -- SETUP -- - ----------- + --#endregion + + --#endregion + + ------------------ + --#region SETUP -- + ------------------ -- get the parent window self.p_window = args.window @@ -467,9 +437,11 @@ function element.new(args, constraint, child_offset_x, child_offset_y) self.id = args.parent.__add_child(args.id, protected) end - ---------------------- - -- PUBLIC FUNCTIONS -- - ---------------------- + --#endregion + + ----------------------------- + --#region PUBLIC FUNCTIONS -- + ----------------------------- -- get the window object ---@nodiscard @@ -511,9 +483,10 @@ function element.new(args, constraint, child_offset_x, child_offset_y) end end - -- ELEMENT TREE -- + --#region ELEMENT TREE -- add a child element + ---@package ---@nodiscard ---@param key string|nil id ---@param child graphics_base @@ -523,7 +496,7 @@ function element.new(args, constraint, child_offset_x, child_offset_y) self.next_y = child.frame.y + child.frame.h - local id = key ---@type string|integer|nil + local id = key ---@type element_id|nil if id == nil then id = self.next_id self.next_id = self.next_id + 1 @@ -537,6 +510,7 @@ function element.new(args, constraint, child_offset_x, child_offset_y) end -- remove a child element + ---@package ---@param id element_id id function public.__remove_child(id) local index = protected.child_id_map[id] @@ -548,11 +522,13 @@ function element.new(args, constraint, child_offset_x, child_offset_y) end -- actions to take upon a child element becoming ready (initial draw/construction completed) + ---@package ---@param key element_id id ---@param child graphics_element function public.__child_ready(key, child) protected.on_added(key, child) end -- focus solely on this child + ---@package ---@param child graphics_element function public.__focus_child(child) if self.is_root then @@ -562,6 +538,7 @@ function element.new(args, constraint, child_offset_x, child_offset_y) end -- a child was focused, used to make sure it is actually visible to the user in the content frame + ---@package ---@param child graphics_element function public.__child_focused(child) protected.on_child_focused(child) @@ -571,8 +548,8 @@ function element.new(args, constraint, child_offset_x, child_offset_y) -- get a child element ---@nodiscard ---@param id element_id - ---@return graphics_element - function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end + ---@return graphics_element element + function public.get_child(id) return ({ protected.children[protected.child_id_map[id]].get() })[1] end -- get all children ---@nodiscard @@ -598,7 +575,7 @@ function element.new(args, constraint, child_offset_x, child_offset_y) -- remove all child elements and reset next y function public.remove_all() for i = 1, #protected.children do - local child = protected.children[i].get() ---@type graphics_element + local child = protected.children[i].get() ---@type graphics_element child.delete() protected.on_removed(child.get_id()) end @@ -619,29 +596,33 @@ function element.new(args, constraint, child_offset_x, child_offset_y) local elem = child.get().get_element_by_id(id) if elem ~= nil then return elem end end - else return protected.children[index].get() end + else return ({ protected.children[index].get() })[1] end end - -- AUTO-PLACEMENT -- + --#endregion + + --#region AUTO-PLACEMENT -- skip a line for automatically placed elements function public.line_break() self.next_y = self.next_y + 1 end - -- PROPERTIES -- + --#endregion - -- get element id + --#region PROPERTIES + + -- get element ID ---@nodiscard ---@return element_id function public.get_id() return self.id end - -- get element x + -- get element relative x position ---@nodiscard ---@return integer x function public.get_x() return protected.frame.x end - -- get element y + -- get element relative y position ---@nodiscard ---@return integer y function public.get_y() return protected.frame.y end @@ -661,12 +642,12 @@ function element.new(args, constraint, child_offset_x, child_offset_y) ---@return cpair fg_bg function public.get_fg_bg() return protected.fg_bg end - -- get the element value + -- get the element's value ---@nodiscard ---@return any value function public.get_value() return protected.get_value() end - -- set the element value + -- set the element's value ---@param value any new value function public.set_value(value) protected.set_value(value) end @@ -728,11 +709,11 @@ function element.new(args, constraint, child_offset_x, child_offset_y) end -- custom recolor command, varies by element if implemented - ---@vararg cpair|color color(s) + ---@param ... cpair|color color(s) function public.recolor(...) protected.recolor(...) end -- resize attributes of the element value if supported - ---@vararg number dimensions (element specific) + ---@param ... number dimensions (element specific) function public.resize(...) protected.resize(...) end -- reposition the element window
@@ -756,7 +737,9 @@ function element.new(args, constraint, child_offset_x, child_offset_y) self.bounds.y2 = self.position.y + protected.frame.h - 1 end - -- FUNCTION CALLBACKS -- + --#endregion + + --#region FUNCTION CALLBACKS -- handle a monitor touch or mouse click if this element is visible ---@param event mouse_interaction mouse interaction event @@ -818,13 +801,9 @@ function element.new(args, constraint, child_offset_x, child_offset_y) end -- draw the element given new data - ---@vararg any new data + ---@param ... any new data function public.update(...) protected.on_update(...) end - -- on a control request response - ---@param result any - function public.on_response(result) protected.response_callback(result) end - -- register a callback with a PSIL, allowing for automatic unregister on delete
-- do not use graphics elements directly with PSIL subscribe() ---@param ps psil PSIL to subscribe to @@ -835,7 +814,9 @@ function element.new(args, constraint, child_offset_x, child_offset_y) ps.subscribe(key, func) end - -- VISIBILITY & ANIMATIONS -- + --#endregion + + --#region VISIBILITY & ANIMATIONS -- check if this element is visible function public.is_visible() return protected.window.isVisible() end @@ -849,6 +830,7 @@ function element.new(args, constraint, child_offset_x, child_offset_y) -- hide the element and disables animations
-- this alone does not cause an element to be fully hidden, it only prevents updates from being shown
+ ---@see Window.redraw ---@see graphics_element.redraw ---@see graphics_element.content_redraw ---@param clear? boolean true to visibly hide this element (redraws the parent) @@ -900,6 +882,10 @@ function element.new(args, constraint, child_offset_x, child_offset_y) end end + --#endregion + + --#endregion + return protected end diff --git a/graphics/elements/appmultipane.lua b/graphics/elements/AppMultiPane.lua similarity index 92% rename from graphics/elements/appmultipane.lua rename to graphics/elements/AppMultiPane.lua index 59731822..948cb323 100644 --- a/graphics/elements/appmultipane.lua +++ b/graphics/elements/AppMultiPane.lua @@ -24,15 +24,15 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new app multipane element +-- Create a new app multipane container element. ---@nodiscard ---@param args app_multipane_args ----@return graphics_element element, element_id id -local function multipane(args) +---@return AppMultiPane element, element_id id +return function (args) element.assert(type(args.panes) == "table", "panes is a required field") -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = 1 @@ -100,10 +100,8 @@ local function multipane(args) end end - -- initial draw - e.redraw() + ---@class AppMultiPane:graphics_element + local AppMultiPane, id = e.complete(true) - return e.complete() + return AppMultiPane, id end - -return multipane diff --git a/graphics/elements/colormap.lua b/graphics/elements/ColorMap.lua similarity index 66% rename from graphics/elements/colormap.lua rename to graphics/elements/ColorMap.lua index 7e3554f0..1a4b0a79 100644 --- a/graphics/elements/colormap.lua +++ b/graphics/elements/ColorMap.lua @@ -9,10 +9,10 @@ local element = require("graphics.element") ---@field y? integer auto incremented if omitted ---@field hidden? boolean true to hide on initial draw --- new color map +-- Create a horizontal reference color map. Primarily used for tuning custom colors. ---@param args colormap_args ----@return graphics_element element, element_id id -local function colormap(args) +---@return ColorMap element, element_id id +return function (args) local bkg = "008877FFCCEE114455DD9933BBAA2266" local spaces = string.rep(" ", 32) @@ -20,7 +20,7 @@ local function colormap(args) args.height = 1 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) -- draw color map function e.redraw() @@ -28,10 +28,8 @@ local function colormap(args) e.w_blit(spaces, bkg, bkg) end - -- initial draw - e.redraw() + ---@class ColorMap:graphics_element + local ColorMap, id = e.complete(true) - return e.complete() + return ColorMap, id end - -return colormap diff --git a/graphics/elements/displaybox.lua b/graphics/elements/DisplayBox.lua similarity index 69% rename from graphics/elements/displaybox.lua rename to graphics/elements/DisplayBox.lua index 3578a637..ecfe03a1 100644 --- a/graphics/elements/displaybox.lua +++ b/graphics/elements/DisplayBox.lua @@ -13,13 +13,16 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new root display box +-- Create a root display box. ---@nodiscard ---@param args displaybox_args ----@return graphics_element element, element_id id -local function displaybox(args) +---@return DisplayBox element, element_id id +return function (args) -- create new graphics element base object - return element.new(args).complete() -end + local e = element.new(args --[[@as graphics_args]]) + + ---@class DisplayBox:graphics_element + local DisplayBox, id = e.complete() -return displaybox + return DisplayBox, id +end diff --git a/graphics/elements/div.lua b/graphics/elements/Div.lua similarity index 72% rename from graphics/elements/div.lua rename to graphics/elements/Div.lua index 0af3259b..2be440b3 100644 --- a/graphics/elements/div.lua +++ b/graphics/elements/Div.lua @@ -13,13 +13,16 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new div element +-- Create a new div container element. ---@nodiscard ---@param args div_args ----@return graphics_element element, element_id id -local function div(args) +---@return Div element, element_id id +return function (args) -- create new graphics element base object - return element.new(args).complete() -end + local e = element.new(args --[[@as graphics_args]]) + + ---@class Div:graphics_element + local Div, id = e.complete() -return div + return Div, id +end diff --git a/graphics/elements/listbox.lua b/graphics/elements/ListBox.lua similarity index 97% rename from graphics/elements/listbox.lua rename to graphics/elements/ListBox.lua index 86cacd46..e1c8decf 100644 --- a/graphics/elements/listbox.lua +++ b/graphics/elements/ListBox.lua @@ -30,15 +30,15 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK ---@field y integer y position ---@field h integer element height --- new listbox element +-- Create a new scrollable listbox container element. ---@nodiscard ---@param args listbox_args ----@return graphics_element element, element_id id -local function listbox(args) +---@return ListBox element, element_id id +return function (args) args.can_focus = true -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) -- create content window for child elements local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false) @@ -339,10 +339,8 @@ local function listbox(args) draw_bar() end - -- initial draw - e.redraw() + ---@class ListBox:graphics_element + local ListBox, id = e.complete(true) - return e.complete() + return ListBox, id end - -return listbox diff --git a/graphics/elements/multipane.lua b/graphics/elements/MultiPane.lua similarity index 81% rename from graphics/elements/multipane.lua rename to graphics/elements/MultiPane.lua index a283ed8c..aae22ac9 100644 --- a/graphics/elements/multipane.lua +++ b/graphics/elements/MultiPane.lua @@ -14,15 +14,15 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new multipane element +-- Create a new multipane container element. ---@nodiscard ---@param args multipane_args ----@return graphics_element element, element_id id -local function multipane(args) +---@return MultiPane element, element_id id +return function (args) element.assert(type(args.panes) == "table", "panes is a required field") -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = 1 @@ -41,10 +41,8 @@ local function multipane(args) end end - -- initial draw - e.redraw() + ---@class MultiPane:graphics_element + local MultiPane, id = e.complete(true) - return e.complete() + return MultiPane, id end - -return multipane diff --git a/graphics/elements/pipenet.lua b/graphics/elements/PipeNetwork.lua similarity index 97% rename from graphics/elements/pipenet.lua rename to graphics/elements/PipeNetwork.lua index 625a70de..7045bd02 100644 --- a/graphics/elements/pipenet.lua +++ b/graphics/elements/PipeNetwork.lua @@ -20,10 +20,10 @@ local element = require("graphics.element") ---@field fg string foreground blit ---@field bg string background blit --- new pipe network +-- Create a pipe network diagram. ---@param args pipenet_args ----@return graphics_element element, element_id id -local function pipenet(args) +---@return PipeNetwork element, element_id id +return function (args) element.assert(type(args.pipes) == "table", "pipes is a required field") args.width = 0 @@ -47,7 +47,7 @@ local function pipenet(args) end -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) -- determine if there are any thin pipes involved local any_thin = false @@ -322,10 +322,8 @@ local function pipenet(args) if any_thin then map_draw() else vector_draw() end end - -- initial draw - e.redraw() + ---@class PipeNetwork:graphics_element + local PipeNetwork, id = e.complete(true) - return e.complete() + return PipeNetwork, id end - -return pipenet diff --git a/graphics/elements/rectangle.lua b/graphics/elements/Rectangle.lua similarity index 96% rename from graphics/elements/rectangle.lua rename to graphics/elements/Rectangle.lua index eceb9bd7..ce5fbba1 100644 --- a/graphics/elements/rectangle.lua +++ b/graphics/elements/Rectangle.lua @@ -18,10 +18,10 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new rectangle +-- Create a new rectangle container element. ---@param args rectangle_args ----@return graphics_element element, element_id id -local function rectangle(args) +---@return Rectangle element, element_id id +return function (args) element.assert(args.border ~= nil or args.thin ~= true, "thin requires border to be provided") -- if thin, then width will always need to be 1 @@ -45,7 +45,7 @@ local function rectangle(args) end -- create new graphics element base object - local e = element.new(args, nil, offset_x, offset_y) + local e = element.new(args --[[@as graphics_args]], nil, offset_x, offset_y) -- create content window for child elements e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y)) @@ -191,7 +191,8 @@ local function rectangle(args) e.redraw() end - return e.complete() -end + ---@class Rectangle:graphics_element + local Rectangle, id = e.complete() -return rectangle + return Rectangle, id +end diff --git a/graphics/elements/textbox.lua b/graphics/elements/TextBox.lua similarity index 78% rename from graphics/elements/textbox.lua rename to graphics/elements/TextBox.lua index a52d5282..c91746de 100644 --- a/graphics/elements/textbox.lua +++ b/graphics/elements/TextBox.lua @@ -10,6 +10,7 @@ local ALIGN = core.ALIGN ---@class textbox_args ---@field text string text to show ---@field alignment? ALIGN text alignment, left by default +---@field trim_whitespace? boolean true to trim whitespace before/after lines of text ---@field anchor? boolean true to use this as an anchor, making it focusable ---@field parent graphics_element ---@field id? string element id @@ -21,10 +22,10 @@ local ALIGN = core.ALIGN ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new text box +-- Create a new text box element. ---@param args textbox_args ----@return graphics_element element, element_id id -local function textbox(args) +---@return TextBox element, element_id id +return function (args) element.assert(type(args.text) == "string", "text is a required field") if args.anchor == true then args.can_focus = true end @@ -42,7 +43,7 @@ local function textbox(args) end -- create new graphics element base object - local e = element.new(args, constrain) + local e = element.new(args --[[@as graphics_args]], constrain) e.value = args.text @@ -57,8 +58,11 @@ local function textbox(args) for i = 1, #lines do if i > e.frame.h then break end - -- trim leading/trailing whitespace - lines[i] = util.trim(lines[i]) + -- trim leading/trailing whitespace, except on the first line + -- leading whitespace on the first line is usually intentional + if args.trim_whitespace == true then + lines[i] = util.trim(lines[i]) + end local len = string.len(lines[i]) @@ -82,10 +86,8 @@ local function textbox(args) e.redraw() end - -- initial draw - e.redraw() + ---@class TextBox:graphics_element + local TextBox, id = e.complete(true) - return e.complete() + return TextBox, id end - -return textbox diff --git a/graphics/elements/tiling.lua b/graphics/elements/Tiling.lua similarity index 90% rename from graphics/elements/tiling.lua rename to graphics/elements/Tiling.lua index 02e26052..d4b48d20 100644 --- a/graphics/elements/tiling.lua +++ b/graphics/elements/Tiling.lua @@ -18,14 +18,14 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new tiling box +-- Create a new tiling box element. ---@param args tiling_args ----@return graphics_element element, element_id id -local function tiling(args) +---@return Tiling element, element_id id +return function (args) element.assert(type(args.fill_c) == "table", "fill_c is a required field") -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) local fill_a = args.fill_c.blit_a local fill_b = args.fill_c.blit_b @@ -52,7 +52,7 @@ local function tiling(args) element.assert(start_x <= inner_width, "start_x > inner_width") element.assert(start_y <= inner_height, "start_y > inner_height") - -- draw tiling box + -- draw the tiling box function e.redraw() local alternator = true @@ -86,10 +86,8 @@ local function tiling(args) end end - -- initial draw - e.redraw() + ---@class Tiling:graphics_element + local Tiling, id = e.complete(true) - return e.complete() + return Tiling, id end - -return tiling diff --git a/graphics/elements/animations/waiting.lua b/graphics/elements/animations/Waiting.lua similarity index 91% rename from graphics/elements/animations/waiting.lua rename to graphics/elements/animations/Waiting.lua index 36aa432b..0456ef39 100644 --- a/graphics/elements/animations/waiting.lua +++ b/graphics/elements/animations/Waiting.lua @@ -12,10 +12,10 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new waiting animation element +-- Create a new waiting animation element. ---@param args waiting_args ----@return graphics_element element, element_id id -local function waiting(args) +---@return Waiting element, element_id id +return function (args) local state = 0 local run_animation = false @@ -23,7 +23,7 @@ local function waiting(args) args.height = 3 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) local blit_fg = e.fg_bg.blit_fgd local blit_bg = e.fg_bg.blit_bkg @@ -103,7 +103,8 @@ local function waiting(args) e.start_anim() - return e.complete() -end + ---@class Waiting:graphics_element + local Waiting, id = e.complete() -return waiting + return Waiting, id +end diff --git a/graphics/elements/controls/app.lua b/graphics/elements/controls/App.lua similarity index 92% rename from graphics/elements/controls/app.lua rename to graphics/elements/controls/App.lua index 4ac936db..2d6ca01a 100644 --- a/graphics/elements/controls/app.lua +++ b/graphics/elements/controls/App.lua @@ -20,10 +20,10 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new app button +-- Create a new app icon style button control element, like on a mobile device. ---@param args app_button_args ----@return graphics_element element, element_id id -local function app_button(args) +---@return App element, element_id id +return function (args) element.assert(type(args.text) == "string", "text is a required field") element.assert(type(args.title) == "string", "title is a required field") element.assert(type(args.callback) == "function", "callback is a required field") @@ -33,7 +33,7 @@ local function app_button(args) args.width = 7 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) -- draw the app button local function draw() @@ -123,10 +123,8 @@ local function app_button(args) draw() end - -- initial draw - e.redraw() + ---@class App:graphics_element + local App, id = e.complete(true) - return e.complete() + return App, id end - -return app_button diff --git a/graphics/elements/controls/checkbox.lua b/graphics/elements/controls/Checkbox.lua similarity index 92% rename from graphics/elements/controls/checkbox.lua rename to graphics/elements/controls/Checkbox.lua index d63ca69e..26d4faf3 100644 --- a/graphics/elements/controls/checkbox.lua +++ b/graphics/elements/controls/Checkbox.lua @@ -15,10 +15,10 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new checkbox control +-- Create a new checkbox control element. ---@param args checkbox_args ----@return graphics_element element, element_id id -local function checkbox(args) +---@return Checkbox element, element_id id +return function (args) element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.box_fg_bg) == "table", "box_fg_bg is a required field") @@ -27,7 +27,7 @@ local function checkbox(args) args.width = 2 + string.len(args.label) -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = args.default == true @@ -112,10 +112,8 @@ local function checkbox(args) draw_label() end - -- initial draw - e.redraw() + ---@class Checkbox:graphics_element + local Checkbox, id = e.complete(true) - return e.complete() + return Checkbox, id end - -return checkbox diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/HazardButton.lua similarity index 92% rename from graphics/elements/controls/hazard_button.lua rename to graphics/elements/controls/HazardButton.lua index 5a3d37f3..7815ba3d 100644 --- a/graphics/elements/controls/hazard_button.lua +++ b/graphics/elements/controls/HazardButton.lua @@ -18,10 +18,10 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new hazard button +-- Create a new hazard button control element. ---@param args hazard_button_args ----@return graphics_element element, element_id id -local function hazard_button(args) +---@return HazardButton element, element_id id +return function (args) element.assert(type(args.text) == "string", "text is a required field") element.assert(type(args.accent) == "number", "accent is a required field") element.assert(type(args.callback) == "function", "callback is a required field") @@ -32,7 +32,7 @@ local function hazard_button(args) local timeout = args.timeout or 1.5 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) -- draw border ---@param accent color accent color @@ -159,13 +159,6 @@ local function hazard_button(args) end end - -- callback on request response - ---@param result boolean true for success, false for failure - function e.response_callback(result) - tcd.abort(on_timeout) - if result then on_success() else on_failure(0) end - end - -- set the value (true simulates pressing the button) ---@param val boolean new value function e.set_value(val) @@ -198,10 +191,15 @@ local function hazard_button(args) draw_border(args.accent) end - -- initial draw - e.redraw() + ---@class HazardButton:graphics_element + local HazardButton, id = e.complete(true) - return e.complete() -end + -- callback for request response + ---@param success boolean + function HazardButton.on_response(success) + tcd.abort(on_timeout) + if success then on_success() else on_failure(0) end + end -return hazard_button + return HazardButton, id +end diff --git a/graphics/elements/controls/multi_button.lua b/graphics/elements/controls/MultiButton.lua similarity index 92% rename from graphics/elements/controls/multi_button.lua rename to graphics/elements/controls/MultiButton.lua index d686b9d3..4e44ebe7 100644 --- a/graphics/elements/controls/multi_button.lua +++ b/graphics/elements/controls/MultiButton.lua @@ -25,10 +25,10 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new multi button (latch selection, exclusively one button at a time) +-- Create a new multi button control element (latch selection, exclusively one button at a time). ---@param args multi_button_args ----@return graphics_element element, element_id id -local function multi_button(args) +---@return MultiButton element, element_id id +return function (args) element.assert(type(args.options) == "table", "options is a required field") element.assert(#args.options > 0, "at least one option is required") element.assert(type(args.callback) == "function", "callback is a required field") @@ -52,7 +52,7 @@ local function multi_button(args) args.width = (button_width * #args.options) + #args.options + 1 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) -- button state (convert nil to 1 if missing) e.value = args.default or 1 @@ -126,10 +126,8 @@ local function multi_button(args) e.redraw() end - -- initial draw - e.redraw() + ---@class MultiButton:graphics_element + local MultiButton, id = e.complete(true) - return e.complete() + return MultiButton, id end - -return multi_button diff --git a/graphics/elements/controls/spinbox_numeric.lua b/graphics/elements/controls/NumericSpinbox.lua similarity index 94% rename from graphics/elements/controls/spinbox_numeric.lua rename to graphics/elements/controls/NumericSpinbox.lua index e114c6aa..0b530f43 100644 --- a/graphics/elements/controls/spinbox_numeric.lua +++ b/graphics/elements/controls/NumericSpinbox.lua @@ -20,10 +20,10 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new spinbox control (minimum value is 0) +-- Create a new spinbox control element (minimum value is 0). ---@param args spinbox_args ----@return graphics_element element, element_id id -local function spinbox(args) +---@return NumericSpinbox element, element_id id +return function (args) -- properties local digits = {} local wn_prec = args.whole_num_precision @@ -51,7 +51,7 @@ local function spinbox(args) args.height = 3 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) -- set initial value e.value = args.default or 0 @@ -179,10 +179,8 @@ local function spinbox(args) draw_arrows(util.trinary(e.enabled, args.arrow_fg_bg.fgd, args.arrow_disable or colors.lightGray)) end - -- initial draw - e.redraw() + ---@class NumericSpinbox:graphics_element + local NumericSpinbox, id = e.complete(true) - return e.complete() + return NumericSpinbox, id end - -return spinbox diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/PushButton.lua similarity index 94% rename from graphics/elements/controls/push_button.lua rename to graphics/elements/controls/PushButton.lua index f0609016..bc8b7ddc 100644 --- a/graphics/elements/controls/push_button.lua +++ b/graphics/elements/controls/PushButton.lua @@ -25,10 +25,10 @@ local KEY_CLICK = core.events.KEY_CLICK ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new push button +-- Create a new push button control element. ---@param args push_button_args ----@return graphics_element element, element_id id -local function push_button(args) +---@return PushButton element, element_id id +return function (args) element.assert(type(args.text) == "string", "text is a required field") element.assert(type(args.callback) == "function", "callback is a required field") element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0") @@ -48,7 +48,7 @@ local function push_button(args) end -- create new graphics element base object - local e = element.new(args, constrain) + local e = element.new(args --[[@as graphics_args]], constrain) local text_lines = util.strwrap(args.text, e.frame.w) @@ -157,10 +157,8 @@ local function push_button(args) e.on_focused = show_pressed e.on_unfocused = show_unpressed - -- initial draw - e.redraw() + ---@class PushButton:graphics_element + local PushButton, id = e.complete(true) - return e.complete() + return PushButton, id end - -return push_button diff --git a/graphics/elements/controls/radio_2d.lua b/graphics/elements/controls/Radio2D.lua similarity index 95% rename from graphics/elements/controls/radio_2d.lua rename to graphics/elements/controls/Radio2D.lua index 65d4c096..cf2da34b 100644 --- a/graphics/elements/controls/radio_2d.lua +++ b/graphics/elements/controls/Radio2D.lua @@ -23,10 +23,10 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new 2D radio button list (latch selection, exclusively one color at a time) +-- Create a new 2-dimensional (rows and columns of options) radio button list control element (latch selection, exclusively one color at a time). ---@param args radio_2d_args ----@return graphics_element element, element_id id -local function radio_2d_button(args) +---@return Radio2D element, element_id id +return function (args) element.assert(type(args.options) == "table" and #args.options > 0, "options should be a table with length >= 1") element.assert(util.is_int(args.rows) and util.is_int(args.columns), "rows/columns must be integers") element.assert((args.rows * args.columns) >= #args.options, "rows x columns size insufficient for provided number of options") @@ -70,7 +70,7 @@ local function radio_2d_button(args) args.height = max_rows -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) -- selected option (convert nil to 1 if missing) e.value = args.default or 1 @@ -194,10 +194,8 @@ local function radio_2d_button(args) e.on_enabled = e.redraw e.on_disabled = e.redraw - -- initial draw - e.redraw() + ---@class Radio2D:graphics_element + local Radio2D, id = e.complete(true) - return e.complete() + return Radio2D, id end - -return radio_2d_button diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/RadioButton.lua similarity index 84% rename from graphics/elements/controls/radio_button.lua rename to graphics/elements/controls/RadioButton.lua index be9b1ee8..5c943169 100644 --- a/graphics/elements/controls/radio_button.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 @@ -21,10 +22,10 @@ local KEY_CLICK = core.events.KEY_CLICK ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new radio button list (latch selection, exclusively one button at a time) +-- Create a new radio button list control element (latch selection, exclusively one button at a time). ---@param args radio_button_args ----@return graphics_element element, element_id id -local function radio_button(args) +---@return RadioButton element, element_id id +return function (args) element.assert(type(args.options) == "table", "options is a required field") element.assert(#args.options > 0, "at least one option is required") element.assert(type(args.radio_colors) == "table", "radio_colors is a required field") @@ -49,7 +50,7 @@ local function radio_button(args) args.height = #args.options -- one line per option -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) local focused_opt = 1 @@ -64,6 +65,10 @@ local function radio_button(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 @@ local function radio_button(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) @@ -139,10 +149,8 @@ local function radio_button(args) e.on_enabled = e.redraw e.on_disabled = e.redraw - -- initial draw - e.redraw() + ---@class RadioButton:graphics_element + local RadioButton, id = e.complete(true) - return e.complete() + return RadioButton, id end - -return radio_button diff --git a/graphics/elements/controls/sidebar.lua b/graphics/elements/controls/Sidebar.lua similarity index 90% rename from graphics/elements/controls/sidebar.lua rename to graphics/elements/controls/Sidebar.lua index 58e8b13e..7c3eac69 100644 --- a/graphics/elements/controls/sidebar.lua +++ b/graphics/elements/controls/Sidebar.lua @@ -17,14 +17,14 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new sidebar tab selector +-- Create a new sidebar tab selector control element. ---@param args sidebar_args ----@return graphics_element element, element_id id -local function sidebar(args) +---@return Sidebar element, element_id id +return function (args) args.width = 3 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) -- default to 1st tab e.value = 1 @@ -129,8 +129,14 @@ local function sidebar(args) end -- update the sidebar navigation options - ---@param items table sidebar entries + ---@param items sidebar_entry[] sidebar entries function e.on_update(items) + ---@class sidebar_entry + ---@field label string + ---@field tall boolean + ---@field color cpair + ---@field callback function|nil + local next_y = 1 tabs = {} @@ -160,9 +166,8 @@ local function sidebar(args) -- element redraw e.redraw = draw - e.redraw() + ---@class Sidebar:graphics_element + local Sidebar, id = e.complete(true) - return e.complete() + return Sidebar, id end - -return sidebar diff --git a/graphics/elements/controls/switch_button.lua b/graphics/elements/controls/SwitchButton.lua similarity index 88% rename from graphics/elements/controls/switch_button.lua rename to graphics/elements/controls/SwitchButton.lua index 8abf8a7d..e23db039 100644 --- a/graphics/elements/controls/switch_button.lua +++ b/graphics/elements/controls/SwitchButton.lua @@ -17,10 +17,10 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new switch button (latch high/low) +-- Create a new latching switch button control element. ---@param args switch_button_args ----@return graphics_element element, element_id id -local function switch_button(args) +---@return SwitchButton element, element_id id +return function (args) element.assert(type(args.text) == "string", "text is a required field") element.assert(type(args.callback) == "function", "callback is a required field") element.assert(type(args.active_fg_bg) == "table", "active_fg_bg is a required field") @@ -33,7 +33,7 @@ local function switch_button(args) args.width = math.max(text_width, args.min_width) -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = args.default or false @@ -72,10 +72,8 @@ local function switch_button(args) e.redraw() end - -- initial draw - e.redraw() + ---@class SwitchButton:graphics_element + local SwitchButton, id = e.complete(true) - return e.complete() + return SwitchButton, id end - -return switch_button diff --git a/graphics/elements/controls/tabbar.lua b/graphics/elements/controls/TabBar.lua similarity index 93% rename from graphics/elements/controls/tabbar.lua rename to graphics/elements/controls/TabBar.lua index c76781b5..f6aa13c3 100644 --- a/graphics/elements/controls/tabbar.lua +++ b/graphics/elements/controls/TabBar.lua @@ -23,10 +23,10 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new tab selector +-- Create a new tab selector control element. ---@param args tabbar_args ----@return graphics_element element, element_id id -local function tabbar(args) +---@return TabBar element, element_id id +return function (args) element.assert(type(args.tabs) == "table", "tabs is a required field") element.assert(#args.tabs > 0, "at least one tab is required") element.assert(type(args.callback) == "function", "callback is a required field") @@ -46,7 +46,7 @@ local function tabbar(args) local button_width = math.max(max_width, args.min_width or 0) -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs") @@ -120,10 +120,8 @@ local function tabbar(args) e.redraw() end - -- initial draw - e.redraw() + ---@class TabBar:graphics_element + local TabBar, id = e.complete(true) - return e.complete() + return TabBar, id end - -return tabbar diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/NumberField.lua similarity index 75% rename from graphics/elements/form/number_field.lua rename to graphics/elements/form/NumberField.lua index 01a4dad9..d582f5bd 100644 --- a/graphics/elements/form/number_field.lua +++ b/graphics/elements/form/NumberField.lua @@ -27,10 +27,10 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new numeric entry field +-- Create a new numeric entry field. ---@param args number_field_args ----@return graphics_element element, element_id id -local function number_field(args) +---@return NumberField element, element_id id +return function (args) element.assert(args.max_int_digits == nil or (util.is_int(args.max_int_digits) and args.max_int_digits > 0), "max_int_digits must be an integer greater than zero if supplied") element.assert(args.max_frac_digits == nil or (util.is_int(args.max_frac_digits) and args.max_frac_digits > 0), "max_frac_digits must be an integer greater than zero if supplied") @@ -38,14 +38,53 @@ local function number_field(args) args.can_focus = true -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) local has_decimal = false 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 @@ local function number_field(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 @@ local function number_field(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 @@ local function number_field(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 @@ -195,10 +242,14 @@ local function number_field(args) e.on_disabled = ifield.show e.redraw = ifield.show - -- initial draw - e.redraw() + ---@class NumberField:graphics_element + local NumberField, id = e.complete(true) - return e.complete() -end + -- 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 number_field + return NumberField, id +end diff --git a/graphics/elements/form/text_field.lua b/graphics/elements/form/TextField.lua similarity index 91% rename from graphics/elements/form/text_field.lua rename to graphics/elements/form/TextField.lua index f912b9e0..1382a11b 100644 --- a/graphics/elements/form/text_field.lua +++ b/graphics/elements/form/TextField.lua @@ -19,15 +19,15 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new text entry field +-- Create a new text entry field. ---@param args text_field_args ----@return graphics_element element, element_id id, function censor_ctl -local function text_field(args) +---@return TextField element, element_id id +return function (args) args.height = 1 args.can_focus = true -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) -- set initial value e.value = args.value or "" @@ -95,11 +95,10 @@ local function text_field(args) e.on_disabled = ifield.show e.redraw = ifield.show - -- initial draw - e.redraw() + ---@class TextField:graphics_element + local TextField, id = e.complete(true) - local elem, id = e.complete() - return elem, id, ifield.censor -end + TextField.censor = ifield.censor -return text_field + return TextField, id +end diff --git a/graphics/elements/indicators/alight.lua b/graphics/elements/indicators/AlarmLight.lua similarity index 92% rename from graphics/elements/indicators/alight.lua rename to graphics/elements/indicators/AlarmLight.lua index e05569c4..7350462d 100644 --- a/graphics/elements/indicators/alight.lua +++ b/graphics/elements/indicators/AlarmLight.lua @@ -20,11 +20,11 @@ local flasher = require("graphics.flasher") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new alarm indicator light +-- Create a new alarm indicator light element. ---@nodiscard ---@param args alarm_indicator_light ----@return graphics_element element, element_id id -local function alarm_indicator_light(args) +---@return AlarmLight element, element_id id +return function (args) element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.c1) == "number", "c1 is a required field") element.assert(type(args.c2) == "number", "c2 is a required field") @@ -49,7 +49,7 @@ local function alarm_indicator_light(args) local c3 = colors.toBlit(args.c3) -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = 1 @@ -113,10 +113,8 @@ local function alarm_indicator_light(args) e.w_write(args.label) end - -- initial draw - e.redraw() + ---@class AlarmLight:graphics_element + local AlarmLight, id = e.complete(true) - return e.complete() + return AlarmLight, id end - -return alarm_indicator_light diff --git a/graphics/elements/indicators/coremap.lua b/graphics/elements/indicators/CoreMap.lua similarity index 94% rename from graphics/elements/indicators/coremap.lua rename to graphics/elements/indicators/CoreMap.lua index 9084b99c..071fdc06 100644 --- a/graphics/elements/indicators/coremap.lua +++ b/graphics/elements/indicators/CoreMap.lua @@ -13,11 +13,11 @@ local element = require("graphics.element") ---@field x? integer 1 if omitted ---@field y? integer auto incremented if omitted --- new core map box +-- Create a new core map diagram indicator element. ---@nodiscard ---@param args core_map_args ----@return graphics_element element, element_id id -local function core_map(args) +---@return CoreMap element, element_id id +return function (args) element.assert(util.is_int(args.reactor_l), "reactor_l is a required field") element.assert(util.is_int(args.reactor_w), "reactor_w is a required field") @@ -29,7 +29,7 @@ local function core_map(args) args.fg_bg = core.cpair(args.parent.get_fg_bg().fgd, colors.gray) -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = 0 @@ -165,10 +165,8 @@ local function core_map(args) draw_core(e.value) end - -- initial draw - e.redraw() + ---@class CoreMap:graphics_element + local CoreMap, id = e.complete(true) - return e.complete() + return CoreMap, id end - -return core_map diff --git a/graphics/elements/indicators/data.lua b/graphics/elements/indicators/DataIndicator.lua similarity index 90% rename from graphics/elements/indicators/data.lua rename to graphics/elements/indicators/DataIndicator.lua index 2304807d..38a253af 100644 --- a/graphics/elements/indicators/data.lua +++ b/graphics/elements/indicators/DataIndicator.lua @@ -19,11 +19,11 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new data indicator +-- Create new data indicator element. ---@nodiscard ---@param args data_indicator_args ----@return graphics_element element, element_id id -local function data(args) +---@return DataIndicator element, element_id id +return function (args) element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.format) == "string", "format is a required field") element.assert(args.value ~= nil, "value is a required field") @@ -32,7 +32,7 @@ local function data(args) args.height = 1 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = args.value @@ -94,10 +94,8 @@ local function data(args) e.on_update(e.value) end - -- initial draw - e.redraw() + ---@class DataIndicator:graphics_element + local DataIndicator, id = e.complete(true) - return e.complete() + return DataIndicator, id end - -return data diff --git a/graphics/elements/indicators/hbar.lua b/graphics/elements/indicators/HorizontalBar.lua similarity index 93% rename from graphics/elements/indicators/hbar.lua rename to graphics/elements/indicators/HorizontalBar.lua index eb4607a3..892cac73 100644 --- a/graphics/elements/indicators/hbar.lua +++ b/graphics/elements/indicators/HorizontalBar.lua @@ -17,13 +17,13 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new horizontal bar +-- Create a new horizontal fill bar indicator element. ---@nodiscard ---@param args hbar_args ---@return graphics_element element, element_id id -local function hbar(args) +return function (args) -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = 0.0 @@ -119,10 +119,8 @@ local function hbar(args) e.on_update(e.value) end - -- initial draw - e.redraw() + ---@class HorizontalBar:graphics_element + local HorizontalBar, id = e.complete(true) - return e.complete() + return HorizontalBar, id end - -return hbar diff --git a/graphics/elements/indicators/icon.lua b/graphics/elements/indicators/IconIndicator.lua similarity index 88% rename from graphics/elements/indicators/icon.lua rename to graphics/elements/indicators/IconIndicator.lua index 15aef3af..377c4470 100644 --- a/graphics/elements/indicators/icon.lua +++ b/graphics/elements/indicators/IconIndicator.lua @@ -18,11 +18,11 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new icon indicator +-- Create a new icon indicator element. ---@nodiscard ---@param args icon_indicator_args ----@return graphics_element element, element_id id -local function icon(args) +---@return IconIndicator element, element_id id +return function (args) element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.states) == "table", "states is a required field") @@ -30,7 +30,7 @@ local function icon(args) args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = args.value or 1 if e.value == true then e.value = 2 end @@ -71,10 +71,8 @@ local function icon(args) e.on_update(e.value) end - -- initial draw - e.redraw() + ---@class IconIndicator:graphics_element + local IconIndicator, id = e.complete(true) - return e.complete() + return IconIndicator, id end - -return icon diff --git a/graphics/elements/indicators/light.lua b/graphics/elements/indicators/IndicatorLight.lua similarity index 90% rename from graphics/elements/indicators/light.lua rename to graphics/elements/indicators/IndicatorLight.lua index 290118cb..15e3ddd1 100644 --- a/graphics/elements/indicators/light.lua +++ b/graphics/elements/indicators/IndicatorLight.lua @@ -18,11 +18,11 @@ local flasher = require("graphics.flasher") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new indicator light +-- Create a new indicator light element. ---@nodiscard ---@param args indicator_light_args ----@return graphics_element element, element_id id -local function indicator_light(args) +---@return IndicatorLight element, element_id id +return function (args) element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.colors) == "table", "colors is a required field") @@ -36,7 +36,7 @@ local function indicator_light(args) local flash_on = true -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = false @@ -93,10 +93,8 @@ local function indicator_light(args) e.w_write(args.label) end - -- initial draw - e.redraw() + ---@class IndicatorLight:graphics_element + local IndicatorLight, id = e.complete(true) - return e.complete() + return IndicatorLight, id end - -return indicator_light diff --git a/graphics/elements/indicators/led.lua b/graphics/elements/indicators/LED.lua similarity index 91% rename from graphics/elements/indicators/led.lua rename to graphics/elements/indicators/LED.lua index 011ee62d..9cdb072b 100644 --- a/graphics/elements/indicators/led.lua +++ b/graphics/elements/indicators/LED.lua @@ -18,11 +18,11 @@ local flasher = require("graphics.flasher") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new indicator LED +-- Create a new indicator LED element. ---@nodiscard ---@param args indicator_led_args ----@return graphics_element element, element_id id -local function indicator_led(args) +---@return LED element, element_id id +return function (args) element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.colors) == "table", "colors is a required field") @@ -36,7 +36,7 @@ local function indicator_led(args) local flash_on = true -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = false @@ -95,10 +95,8 @@ local function indicator_led(args) end end - -- initial draw - e.redraw() + ---@class LED:graphics_element + local LED, id = e.complete(true) - return e.complete() + return LED, id end - -return indicator_led diff --git a/graphics/elements/indicators/ledpair.lua b/graphics/elements/indicators/LEDPair.lua similarity index 89% rename from graphics/elements/indicators/ledpair.lua rename to graphics/elements/indicators/LEDPair.lua index 1a81854f..46e55c63 100644 --- a/graphics/elements/indicators/ledpair.lua +++ b/graphics/elements/indicators/LEDPair.lua @@ -20,11 +20,12 @@ local flasher = require("graphics.flasher") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new dual LED indicator light +-- Create a new three-state LED indicator light. Two "active" states (colors c1 and c2) and an inactive state (off).
+-- Values: 1 = off, 2 = c1, 3 = c2 ---@nodiscard ---@param args indicator_led_pair_args ----@return graphics_element element, element_id id -local function indicator_led_pair(args) +---@return LEDPair element, element_id id +return function (args) element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.off) == "number", "off is a required field") element.assert(type(args.c1) == "number", "c1 is a required field") @@ -44,7 +45,7 @@ local function indicator_led_pair(args) local c2 = colors.toBlit(args.c2) -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = 1 @@ -104,10 +105,8 @@ local function indicator_led_pair(args) end end - -- initial draw - e.redraw() + ---@class LEDPair:graphics_element + local LEDPair, id = e.complete(true) - return e.complete() + return LEDPair, id end - -return indicator_led_pair diff --git a/graphics/elements/indicators/power.lua b/graphics/elements/indicators/PowerIndicator.lua similarity index 87% rename from graphics/elements/indicators/power.lua rename to graphics/elements/indicators/PowerIndicator.lua index 7a09f957..53c18aa1 100644 --- a/graphics/elements/indicators/power.lua +++ b/graphics/elements/indicators/PowerIndicator.lua @@ -19,11 +19,11 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new power indicator +-- Create a new power indicator. Variant of a data indicator with dynamic energy units. ---@nodiscard ---@param args power_indicator_args ----@return graphics_element element, element_id id -local function power(args) +---@return PowerIndicator element, element_id id +return function (args) element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.unit) == "string", "unit is a required field") element.assert(type(args.value) == "number", "value is a required field") @@ -32,7 +32,7 @@ local function power(args) args.height = 1 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = args.value @@ -82,10 +82,8 @@ local function power(args) e.on_update(e.value) end - -- initial draw - e.redraw() + ---@class PowerIndicator:graphics_element + local PowerIndicator, id = e.complete(true) - return e.complete() + return PowerIndicator, id end - -return power diff --git a/graphics/elements/indicators/ledrgb.lua b/graphics/elements/indicators/RGBLED.lua similarity index 85% rename from graphics/elements/indicators/ledrgb.lua rename to graphics/elements/indicators/RGBLED.lua index 406e0ccc..aacd4845 100644 --- a/graphics/elements/indicators/ledrgb.lua +++ b/graphics/elements/indicators/RGBLED.lua @@ -13,11 +13,11 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new RGB LED indicator light +-- Create a new RGB LED indicator light element. ---@nodiscard ---@param args indicator_led_rgb_args ----@return graphics_element element, element_id id -local function indicator_led_rgb(args) +---@return RGBLED element, element_id id +return function (args) element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.colors) == "table", "colors is a required field") @@ -25,7 +25,7 @@ local function indicator_led_rgb(args) args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = 1 @@ -52,10 +52,8 @@ local function indicator_led_rgb(args) end end - -- initial draw - e.redraw() + ---@class RGBLED:graphics_element + local RGBLED, id = e.complete(true) - return e.complete() + return RGBLED, id end - -return indicator_led_rgb diff --git a/graphics/elements/indicators/rad.lua b/graphics/elements/indicators/RadIndicator.lua similarity index 87% rename from graphics/elements/indicators/rad.lua rename to graphics/elements/indicators/RadIndicator.lua index 545ea419..73fc9b3a 100644 --- a/graphics/elements/indicators/rad.lua +++ b/graphics/elements/indicators/RadIndicator.lua @@ -19,11 +19,11 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new radiation indicator +-- Create a new radiation indicator element. Variant of a data indicator using dynamic Sievert unit precision. ---@nodiscard ---@param args rad_indicator_args ----@return graphics_element element, element_id id -local function rad(args) +---@return RadIndicator element, element_id id +return function (args) element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.format) == "string", "format is a required field") element.assert(util.is_int(args.width), "width is a required field") @@ -31,7 +31,7 @@ local function rad(args) args.height = 1 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = args.value or types.new_zero_radiation_reading() @@ -83,10 +83,8 @@ local function rad(args) e.on_update(e.value) end - -- initial draw - e.redraw() + ---@class RadIndicator:graphics_element + local RadIndicator, id = e.complete(true) - return e.complete() + return RadIndicator, id end - -return rad diff --git a/graphics/elements/indicators/signal.lua b/graphics/elements/indicators/SignalBar.lua similarity index 90% rename from graphics/elements/indicators/signal.lua rename to graphics/elements/indicators/SignalBar.lua index 7e9c1b85..37d13597 100644 --- a/graphics/elements/indicators/signal.lua +++ b/graphics/elements/indicators/SignalBar.lua @@ -15,16 +15,16 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors (foreground is used for high signal quality) ---@field hidden? boolean true to hide on initial draw --- new signal bar +-- Create a new signal bar indicator element. ---@nodiscard ---@param args signal_bar_args ----@return graphics_element element, element_id id -local function signal_bar(args) +---@return SignalBar element, element_id id +return function (args) args.height = 1 args.width = util.trinary(args.compact, 1, 2) -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = 0 @@ -76,10 +76,8 @@ local function signal_bar(args) end end - -- initial draw - e.redraw() + ---@class SignalBar:graphics_element + local SignalBar, id = e.complete(true) - return e.complete() + return SignalBar, id end - -return signal_bar diff --git a/graphics/elements/indicators/state.lua b/graphics/elements/indicators/StateIndicator.lua similarity index 88% rename from graphics/elements/indicators/state.lua rename to graphics/elements/indicators/StateIndicator.lua index f2dc1349..8ee2cdc9 100644 --- a/graphics/elements/indicators/state.lua +++ b/graphics/elements/indicators/StateIndicator.lua @@ -20,11 +20,11 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new state indicator +-- Create a new state indicator element. ---@nodiscard ---@param args state_indicator_args ----@return graphics_element element, element_id id -local function state_indicator(args) +---@return StateIndicator element, element_id id +return function (args) element.assert(type(args.states) == "table", "states is a required field") if util.is_int(args.height) then @@ -52,7 +52,7 @@ local function state_indicator(args) end -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = args.value or 1 @@ -74,10 +74,8 @@ local function state_indicator(args) ---@param val integer indicator state function e.set_value(val) e.on_update(val) end - -- initial draw - e.redraw() + ---@class StateIndicator:graphics_element + local StateIndicator, id = e.complete(true) - return e.complete() + return StateIndicator, id end - -return state_indicator diff --git a/graphics/elements/indicators/trilight.lua b/graphics/elements/indicators/TriIndicatorLight.lua similarity index 91% rename from graphics/elements/indicators/trilight.lua rename to graphics/elements/indicators/TriIndicatorLight.lua index f5d441c2..e01d12dd 100644 --- a/graphics/elements/indicators/trilight.lua +++ b/graphics/elements/indicators/TriIndicatorLight.lua @@ -20,11 +20,11 @@ local flasher = require("graphics.flasher") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new tri-state indicator light +-- Create a new tri-state indicator light element. ---@nodiscard ---@param args tristate_indicator_light_args ----@return graphics_element element, element_id id -local function tristate_indicator_light(args) +---@return TriIndicatorLight element, element_id id +return function (args) element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.c1) == "number", "c1 is a required field") element.assert(type(args.c2) == "number", "c2 is a required field") @@ -38,7 +38,7 @@ local function tristate_indicator_light(args) args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = 1 @@ -102,10 +102,8 @@ local function tristate_indicator_light(args) e.w_write(args.label) end - -- initial draw - e.redraw() + ---@class TriIndicatorLight:graphics_element + local TriIndicatorLight, id = e.complete(true) - return e.complete() + return TriIndicatorLight, id end - -return tristate_indicator_light diff --git a/graphics/elements/indicators/vbar.lua b/graphics/elements/indicators/VerticalBar.lua similarity index 90% rename from graphics/elements/indicators/vbar.lua rename to graphics/elements/indicators/VerticalBar.lua index afe56fc6..d04c2868 100644 --- a/graphics/elements/indicators/vbar.lua +++ b/graphics/elements/indicators/VerticalBar.lua @@ -15,13 +15,13 @@ local element = require("graphics.element") ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new vertical bar +-- Create a new vertical fill bar indicator element. ---@nodiscard ---@param args vbar_args ----@return graphics_element element, element_id id -local function vbar(args) +---@return VerticalBar element, element_id id +return function (args) -- create new graphics element base object - local e = element.new(args) + local e = element.new(args --[[@as graphics_args]]) e.value = 0.0 @@ -98,10 +98,8 @@ local function vbar(args) e.redraw() end - -- initial draw - e.redraw() + ---@class VerticalBar:graphics_element + local VerticalBar, id = e.complete(true) - return e.complete() + return VerticalBar, id end - -return vbar diff --git a/graphics/flasher.lua b/graphics/flasher.lua index 9412ac74..624822b0 100644 --- a/graphics/flasher.lua +++ b/graphics/flasher.lua @@ -18,7 +18,7 @@ local PERIOD = { flasher.PERIOD = PERIOD local active = false -local registry = { {}, {}, {} } -- one registry table per period +local registry = { {}, {}, {} } ---@type [ function[], function[], function [] ] one registry table per period local callback_counter = 0 -- blink registered indicators
diff --git a/pocket/config/system.lua b/pocket/config/system.lua new file mode 100644 index 00000000..4c195ff6 --- /dev/null +++ b/pocket/config/system.lua @@ -0,0 +1,410 @@ +local log = require("scada-common.log") +local types = require("scada-common.types") +local util = require("scada-common.util") + +local core = require("graphics.core") + +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") + +local Checkbox = require("graphics.elements.controls.Checkbox") +local PushButton = require("graphics.elements.controls.PushButton") +local RadioButton = require("graphics.elements.controls.RadioButton") + +local NumberField = require("graphics.elements.form.NumberField") +local TextField = require("graphics.elements.form.TextField") + +local tri = util.trinary + +local cpair = core.cpair + +local RIGHT = core.ALIGN.RIGHT + +local self = { + importing_legacy = false, + + show_auth_key = nil, ---@type function + show_key_btn = nil, ---@type PushButton + auth_key_textbox = nil, ---@type TextBox + auth_key_value = "" +} + +local system = {} + +-- create the system configuration view +---@param tool_ctl _pkt_cfg_tool_ctl +---@param main_pane MultiPane +---@param cfg_sys [ pkt_config, pkt_config, pkt_config, { [1]: string, [2]: string, [3]: any }[], function ] +---@param divs Div[] +---@param style { [string]: cpair } +---@param exit function +function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit) + local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5] + local ui_cfg, net_cfg, log_cfg, summary = divs[1], divs[2], divs[3], divs[4] + + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + + --#region Pocket UI + + local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24} + + TextBox{parent=ui_cfg,x=1,y=2,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)} + + TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize units below."} + + TextBox{parent=ui_c_1,x=1,y=4,text="Temperature Scale"} + local temp_scale = RadioButton{parent=ui_c_1,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} + + TextBox{parent=ui_c_1,x=1,y=10,text="Energy Scale"} + local energy_scale = RadioButton{parent=ui_c_1,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} + + local function submit_ui_opts() + tmp_cfg.TempScale = temp_scale.get_value() + tmp_cfg.EnergyScale = energy_scale.get_value() + main_pane.set_value(3) + end + + PushButton{parent=ui_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Network + + local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=24} + local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=24} + local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=24} + local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=24} + + local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}} + + TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} + + TextBox{parent=net_c_1,x=1,y=1,text="Set network channels."} + TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the named channels must be the same within a particular SCADA network.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"} + local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"} + local crd_chan = NumberField{parent=net_c_1,x=1,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=9,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"} + local pkt_chan = NumberField{parent=net_c_1,x=1,y=13,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=9,y=13,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg} + + local chan_err = TextBox{parent=net_c_1,x=1,y=14,width=24,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_channels() + local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value()) + if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then + tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c + net_pane.set_value(2) + chan_err.hide(true) + else chan_err.show() end + end + + PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_1,x=19,y=15,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_2,x=1,y=1,text="Set connection timeout."} + TextBox{parent=net_c_2,x=1,y=3,height=7,text="You generally should not need to modify this. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_2,x=1,y=11,width=19,text="Connection Timeout"} + local timeout = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} + + TextBox{parent=net_c_2,x=9,y=12,height=2,text="seconds\n(default 5)",fg_bg=g_lg_fg_bg} + + local ct_err = TextBox{parent=net_c_2,x=1,y=14,width=24,text="Please set timeout.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_timeouts() + local timeout_val = tonumber(timeout.get_value()) + if timeout_val ~= nil then + tmp_cfg.ConnTimeout = timeout_val + net_pane.set_value(3) + ct_err.hide(true) + else ct_err.show() end + end + + PushButton{parent=net_c_2,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_2,x=19,y=15,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_3,x=1,y=1,text="Set the trusted range."} + TextBox{parent=net_c_3,x=1,y=3,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many blocks away.",fg_bg=g_lg_fg_bg} + TextBox{parent=net_c_3,x=1,y=8,height=4,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg} + + local range = NumberField{parent=net_c_3,x=1,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg} + + local tr_err = TextBox{parent=net_c_3,x=1,y=14,width=24,text="Set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_tr() + local range_val = tonumber(range.get_value()) + if range_val ~= nil then + tmp_cfg.TrustedRange = range_val + net_pane.set_value(4) + tr_err.hide(true) + else tr_err.show() end + end + + PushButton{parent=net_c_3,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_3,x=19,y=15,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_4,x=1,y=1,height=4,text="Optionally, set the facility authentication key. Do NOT use one of your passwords."} + TextBox{parent=net_c_4,x=1,y=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_4,x=1,y=12,text="Facility Auth Key"} + local key, _ = TextField{parent=net_c_4,x=1,y=13,max_len=64,value=ini_cfg.AuthKey,width=24,height=1,fg_bg=bw_fg_bg} + + local function censor_key(enable) key.censor(tri(enable, "*", nil)) end + + -- declare back first so tabbing makes sense visually + PushButton{parent=net_c_4,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + local hide_key = Checkbox{parent=net_c_4,x=8,y=15,label="Hide Key",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} + + hide_key.set_value(true) + censor_key(true) + + local key_err = TextBox{parent=net_c_4,x=1,y=14,width=24,text="Length must be > 7.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_auth() + local v = key.get_value() + if string.len(v) == 0 or string.len(v) >= 8 then + tmp_cfg.AuthKey = key.get_value() + main_pane.set_value(4) + key_err.hide(true) + else key_err.show() end + end + + PushButton{parent=net_c_4,x=19,y=15,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Logging + + local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24} + + TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} + + TextBox{parent=log_c_1,x=1,y=1,text="Configure logging below."} + + TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} + local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} + + TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"} + local path = TextField{parent=log_c_1,x=1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg} + + local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} + TextBox{parent=log_c_1,x=3,y=11,height=4,text="This results in much larger log files. Use only as needed.",fg_bg=g_lg_fg_bg} + + local path_err = TextBox{parent=log_c_1,x=1,y=14,width=24,text="Provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_log() + if path.get_value() ~= "" then + path_err.hide(true) + tmp_cfg.LogMode = mode.get_value() - 1 + tmp_cfg.LogPath = path.get_value() + tmp_cfg.LogDebug = en_dbg.get_value() + tool_ctl.gen_summary(tmp_cfg) + tool_ctl.viewing_config = false + self.importing_legacy = false + tool_ctl.settings_apply.show() + main_pane.set_value(5) + else path_err.show() end + end + + PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=log_c_1,x=19,y=15,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Summary and Saving + + local sum_c_1 = Div{parent=summary,x=2,y=4,width=24} + local sum_c_2 = Div{parent=summary,x=2,y=4,width=24} + local sum_c_3 = Div{parent=summary,x=2,y=4,width=24} + local sum_c_4 = Div{parent=summary,x=2,y=4,width=24} + + local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}} + + TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)} + + local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=11,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local function back_from_summary() + if tool_ctl.viewing_config or self.importing_legacy then + main_pane.set_value(1) + tool_ctl.viewing_config = false + self.importing_legacy = false + tool_ctl.settings_apply.show() + else + main_pane.set_value(4) + end + end + + ---@param element graphics_element + ---@param data any + local function try_set(element, data) + if data ~= nil then element.set_value(data) end + end + + local function save_and_continue() + for _, field in ipairs(fields) do + local k, v = field[1], tmp_cfg[field[1]] + if v == nil then settings.unset(k) else settings.set(k, v) end + end + + if settings.save("/pocket.settings") then + load_settings(settings_cfg, true) + load_settings(ini_cfg) + + try_set(temp_scale, ini_cfg.TempScale) + try_set(energy_scale, ini_cfg.EnergyScale) + try_set(svr_chan, ini_cfg.SVR_Channel) + try_set(crd_chan, ini_cfg.CRD_Channel) + try_set(pkt_chan, ini_cfg.PKT_Channel) + try_set(timeout, ini_cfg.ConnTimeout) + try_set(range, ini_cfg.TrustedRange) + try_set(key, ini_cfg.AuthKey) + try_set(mode, ini_cfg.LogMode) + try_set(path, ini_cfg.LogPath) + try_set(en_dbg, ini_cfg.LogDebug) + + tool_ctl.view_cfg.enable() + + if self.importing_legacy then + self.importing_legacy = false + sum_pane.set_value(3) + else + sum_pane.set_value(2) + end + else + sum_pane.set_value(4) + end + end + + PushButton{parent=sum_c_1,x=1,y=15,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + self.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=18,y=15,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + + TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"} + + local function go_home() + main_pane.set_value(1) + net_pane.set_value(1) + sum_pane.set_value(1) + end + + PushButton{parent=sum_c_2,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_2,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + + TextBox{parent=sum_c_3,x=1,y=1,height=4,text="The old config.lua file will now be deleted, then the configurator will exit."} + + local function delete_legacy() + fs.delete("/pocket/config.lua") + exit() + end + + PushButton{parent=sum_c_3,x=1,y=15,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_3,x=19,y=15,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)} + + TextBox{parent=sum_c_4,x=1,y=1,height=8,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} + PushButton{parent=sum_c_4,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_4,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + + --#endregion + + --#region Tool Functions + + -- load a legacy config file + function tool_ctl.load_legacy() + local config = require("pocket.config") + + tmp_cfg.SVR_Channel = config.SVR_CHANNEL + tmp_cfg.CRD_Channel = config.CRD_CHANNEL + tmp_cfg.PKT_Channel = config.PKT_CHANNEL + tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT + tmp_cfg.TrustedRange = config.TRUSTED_RANGE + tmp_cfg.AuthKey = config.AUTH_KEY or "" + + tmp_cfg.LogMode = config.LOG_MODE + tmp_cfg.LogPath = config.LOG_PATH + tmp_cfg.LogDebug = config.LOG_DEBUG or false + + tool_ctl.gen_summary(tmp_cfg) + sum_pane.set_value(1) + main_pane.set_value(5) + self.importing_legacy = true + end + + -- expose the auth key on the summary page + function self.show_auth_key() + self.show_key_btn.disable() + self.auth_key_textbox.set_value(self.auth_key_value) + end + + -- generate the summary list + ---@param cfg pkt_config + function tool_ctl.gen_summary(cfg) + setting_list.remove_all() + + local alternate = false + local inner_width = setting_list.get_width() - 1 + + self.show_key_btn.enable() + self.auth_key_value = cfg.AuthKey or "" -- to show auth key + + for i = 1, #fields do + local f = fields[i] + local height = 1 + local label_w = string.len(f[2]) + local val_max_w = (inner_width - label_w) - 1 + local raw = cfg[f[1]] + local val = util.strval(raw) + + if f[1] == "AuthKey" then + val = string.rep("*", string.len(val)) + elseif f[1] == "LogMode" then + val = tri(raw == log.MODE.APPEND, "append", "replace") + elseif f[1] == "TempScale" then + val = util.strval(types.TEMP_SCALE_NAMES[raw]) + elseif f[1] == "EnergyScale" then + val = util.strval(types.ENERGY_SCALE_NAMES[raw]) + end + + if val == "nil" then val = "" end + + local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) + alternate = not alternate + + if string.len(val) > val_max_w then + local lines = util.strwrap(val, inner_width) + height = #lines + 1 + end + + local line = Div{parent=setting_list,height=height,fg_bg=c} + TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} + + local textbox + if height > 1 then + textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1} + else + textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT} + end + + if f[1] == "AuthKey" then self.auth_key_textbox = textbox end + end + end + + --#endregion +end + +return system diff --git a/pocket/configure.lua b/pocket/configure.lua index 0e91caaa..69a99c75 100644 --- a/pocket/configure.lua +++ b/pocket/configure.lua @@ -6,21 +6,18 @@ local log = require("scada-common.log") local types = require("scada-common.types") local util = require("scada-common.util") +local system = require("pocket.config.system") + local core = require("graphics.core") local themes = require("graphics.themes") -local DisplayBox = require("graphics.elements.displaybox") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") - -local CheckBox = require("graphics.elements.controls.checkbox") -local PushButton = require("graphics.elements.controls.push_button") -local RadioButton = require("graphics.elements.controls.radio_button") +local DisplayBox = require("graphics.elements.DisplayBox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local NumberField = require("graphics.elements.form.number_field") -local TextField = require("graphics.elements.form.text_field") +local PushButton = require("graphics.elements.controls.PushButton") local println = util.println local tri = util.trinary @@ -28,7 +25,6 @@ local tri = util.trinary local cpair = core.cpair local CENTER = core.ALIGN.CENTER -local RIGHT = core.ALIGN.RIGHT -- changes to the config data/format to let the user know local changes = { @@ -41,49 +37,41 @@ local configurator = {} local style = {} -style.root = cpair(colors.black, colors.lightGray) -style.header = cpair(colors.white, colors.gray) +style.root = cpair(colors.black, colors.lightGray) +style.header = cpair(colors.white, colors.gray) -style.colors = themes.smooth_stone.colors +style.colors = themes.smooth_stone.colors -local bw_fg_bg = cpair(colors.black, colors.white) -local g_lg_fg_bg = cpair(colors.gray, colors.lightGray) -local nav_fg_bg = bw_fg_bg -local btn_act_fg_bg = cpair(colors.white, colors.gray) -local dis_fg_bg = cpair(colors.lightGray,colors.white) +style.bw_fg_bg = cpair(colors.black, colors.white) +style.g_lg_fg_bg = cpair(colors.gray, colors.lightGray) +style.nav_fg_bg = style.bw_fg_bg +style.btn_act_fg_bg = cpair(colors.white, colors.gray) +style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white) +---@class _pkt_cfg_tool_ctl local tool_ctl = { ask_config = false, has_config = false, viewing_config = false, - importing_legacy = false, - view_cfg = nil, ---@type graphics_element - settings_apply = nil, ---@type graphics_element + view_cfg = nil, ---@type PushButton + settings_apply = nil, ---@type PushButton - set_networked = nil, ---@type function - bundled_emcool = nil, ---@type function gen_summary = nil, ---@type function - show_current_cfg = nil, ---@type function - load_legacy = nil, ---@type function - - show_auth_key = nil, ---@type function - show_key_btn = nil, ---@type graphics_element - auth_key_textbox = nil, ---@type graphics_element - auth_key_value = "" + load_legacy = nil ---@type function } ---@class pkt_config local tmp_cfg = { - TempScale = 1, - EnergyScale = 1, + TempScale = 1, ---@type TEMP_SCALE + EnergyScale = 1, ---@type ENERGY_SCALE SVR_Channel = nil, ---@type integer CRD_Channel = nil, ---@type integer PKT_Channel = nil, ---@type integer ConnTimeout = nil, ---@type number TrustedRange = nil, ---@type number AuthKey = nil, ---@type string|nil - LogMode = 0, + LogMode = 0, ---@type LOG_MODE LogPath = "", LogDebug = false, } @@ -122,8 +110,14 @@ local function load_settings(target, raw) end -- create the config view ----@param display graphics_element +---@param display DisplayBox local function config_view(display) + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + ---@diagnostic disable-next-line: undefined-field local function exit() os.queueEvent("terminate") end @@ -140,7 +134,7 @@ local function config_view(display) local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,ui_cfg,net_cfg,log_cfg,summary,changelog}} - -- Main Page + --#region Main Page local y_start = 7 @@ -164,286 +158,25 @@ local function config_view(display) end PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure Device",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg} + tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end PushButton{parent=main_page,x=2,y=18,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} PushButton{parent=main_page,x=14,y=18,min_width=12,text="Change Log",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - --#region Pocket UI - - local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24} - - TextBox{parent=ui_cfg,x=1,y=2,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)} - - TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize units below."} - - TextBox{parent=ui_c_1,x=1,y=4,text="Temperature Scale"} - local temp_scale = RadioButton{parent=ui_c_1,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} - - TextBox{parent=ui_c_1,x=1,y=10,text="Energy Scale"} - local energy_scale = RadioButton{parent=ui_c_1,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} - - local function submit_ui_opts() - tmp_cfg.TempScale = temp_scale.get_value() - tmp_cfg.EnergyScale = energy_scale.get_value() - main_pane.set_value(3) - end - - PushButton{parent=ui_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - --#endregion - --#region Network - - local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=24} - local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=24} - local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=24} - local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=24} - - local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}} - - TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} - - TextBox{parent=net_c_1,x=1,y=1,text="Set network channels."} - TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the named channels must be the same within a particular SCADA network.",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"} - local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"} - local crd_chan = NumberField{parent=net_c_1,x=1,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=9,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"} - local pkt_chan = NumberField{parent=net_c_1,x=1,y=13,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=9,y=13,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg} - - local chan_err = TextBox{parent=net_c_1,x=1,y=14,width=24,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_channels() - local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value()) - if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then - tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c - net_pane.set_value(2) - chan_err.hide(true) - else chan_err.show() end - end - - PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_1,x=19,y=15,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=net_c_2,x=1,y=1,text="Set connection timeout."} - TextBox{parent=net_c_2,x=1,y=3,height=7,text="You generally should not need to modify this. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_2,x=1,y=11,width=19,text="Connection Timeout"} - local timeout = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} - - TextBox{parent=net_c_2,x=9,y=12,height=2,text="seconds\n(default 5)",fg_bg=g_lg_fg_bg} + --#region System Configuration - local ct_err = TextBox{parent=net_c_2,x=1,y=14,width=24,text="Please set timeout.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + local settings = { settings_cfg, ini_cfg, tmp_cfg, fields, load_settings } + local divs = { ui_cfg, net_cfg, log_cfg, summary } - local function submit_timeouts() - local timeout_val = tonumber(timeout.get_value()) - if timeout_val ~= nil then - tmp_cfg.ConnTimeout = timeout_val - net_pane.set_value(3) - ct_err.hide(true) - else ct_err.show() end - end - - PushButton{parent=net_c_2,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_2,x=19,y=15,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=net_c_3,x=1,y=1,text="Set the trusted range."} - TextBox{parent=net_c_3,x=1,y=3,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many blocks away.",fg_bg=g_lg_fg_bg} - TextBox{parent=net_c_3,x=1,y=8,height=4,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg} - - local range = NumberField{parent=net_c_3,x=1,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg} - - local tr_err = TextBox{parent=net_c_3,x=1,y=14,width=24,text="Set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_tr() - local range_val = tonumber(range.get_value()) - if range_val ~= nil then - tmp_cfg.TrustedRange = range_val - net_pane.set_value(4) - tr_err.hide(true) - else tr_err.show() end - end - - PushButton{parent=net_c_3,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_3,x=19,y=15,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=net_c_4,x=1,y=1,height=4,text="Optionally, set the facility authentication key. Do NOT use one of your passwords."} - TextBox{parent=net_c_4,x=1,y=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_4,x=1,y=12,text="Facility Auth Key"} - local key, _, censor = TextField{parent=net_c_4,x=1,y=13,max_len=64,value=ini_cfg.AuthKey,width=24,height=1,fg_bg=bw_fg_bg} - - local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end - - -- declare back first so tabbing makes sense visually - PushButton{parent=net_c_4,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - local hide_key = CheckBox{parent=net_c_4,x=8,y=15,label="Hide Key",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} - - hide_key.set_value(true) - censor_key(true) - - local key_err = TextBox{parent=net_c_4,x=1,y=14,width=24,text="Length must be > 7.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_auth() - local v = key.get_value() - if string.len(v) == 0 or string.len(v) >= 8 then - tmp_cfg.AuthKey = key.get_value() - main_pane.set_value(4) - key_err.hide(true) - else key_err.show() end - end - - PushButton{parent=net_c_4,x=19,y=15,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + system.create(tool_ctl, main_pane, settings, divs, style, exit) --#endregion - --#region Logging - - local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24} - - TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} - - TextBox{parent=log_c_1,x=1,y=1,text="Configure logging below."} - - TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} - local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} - - TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"} - local path = TextField{parent=log_c_1,x=1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg} - - local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} - TextBox{parent=log_c_1,x=3,y=11,height=4,text="This results in much larger log files. Use only as needed.",fg_bg=g_lg_fg_bg} - - local path_err = TextBox{parent=log_c_1,x=1,y=14,width=24,text="Provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_log() - if path.get_value() ~= "" then - path_err.hide(true) - tmp_cfg.LogMode = mode.get_value() - 1 - tmp_cfg.LogPath = path.get_value() - tmp_cfg.LogDebug = en_dbg.get_value() - tool_ctl.gen_summary(tmp_cfg) - tool_ctl.viewing_config = false - tool_ctl.importing_legacy = false - tool_ctl.settings_apply.show() - main_pane.set_value(5) - else path_err.show() end - end - - PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=log_c_1,x=19,y=15,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - --#endregion - - --#region Summary and Saving - - local sum_c_1 = Div{parent=summary,x=2,y=4,width=24} - local sum_c_2 = Div{parent=summary,x=2,y=4,width=24} - local sum_c_3 = Div{parent=summary,x=2,y=4,width=24} - local sum_c_4 = Div{parent=summary,x=2,y=4,width=24} - - local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}} - - TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)} - - local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=11,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - local function back_from_summary() - if tool_ctl.viewing_config or tool_ctl.importing_legacy then - main_pane.set_value(1) - tool_ctl.viewing_config = false - tool_ctl.importing_legacy = false - tool_ctl.settings_apply.show() - else - main_pane.set_value(4) - end - end - - ---@param element graphics_element - ---@param data any - local function try_set(element, data) - if data ~= nil then element.set_value(data) end - end - - local function save_and_continue() - for _, field in ipairs(fields) do - local k, v = field[1], tmp_cfg[field[1]] - if v == nil then settings.unset(k) else settings.set(k, v) end - end - - if settings.save("/pocket.settings") then - load_settings(settings_cfg, true) - load_settings(ini_cfg) - - try_set(temp_scale, ini_cfg.TempScale) - try_set(energy_scale, ini_cfg.EnergyScale) - try_set(svr_chan, ini_cfg.SVR_Channel) - try_set(crd_chan, ini_cfg.CRD_Channel) - try_set(pkt_chan, ini_cfg.PKT_Channel) - try_set(timeout, ini_cfg.ConnTimeout) - try_set(range, ini_cfg.TrustedRange) - try_set(key, ini_cfg.AuthKey) - try_set(mode, ini_cfg.LogMode) - try_set(path, ini_cfg.LogPath) - try_set(en_dbg, ini_cfg.LogDebug) - - tool_ctl.view_cfg.enable() - - if tool_ctl.importing_legacy then - tool_ctl.importing_legacy = false - sum_pane.set_value(3) - else - sum_pane.set_value(2) - end - else - sum_pane.set_value(4) - end - end - - PushButton{parent=sum_c_1,x=1,y=15,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg} - tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=18,y=15,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} - - TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"} - - local function go_home() - main_pane.set_value(1) - net_pane.set_value(1) - sum_pane.set_value(1) - end - - PushButton{parent=sum_c_2,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_2,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - - TextBox{parent=sum_c_3,x=1,y=1,height=4,text="The old config.lua file will now be deleted, then the configurator will exit."} - - local function delete_legacy() - fs.delete("/pocket/config.lua") - exit() - end - - PushButton{parent=sum_c_3,x=1,y=15,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_3,x=19,y=15,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)} - - TextBox{parent=sum_c_4,x=1,y=1,height=8,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} - PushButton{parent=sum_c_4,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_4,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - - --#endregion - - -- Config Change Log + --#region Config Change Log local cl = Div{parent=changelog,x=2,y=4,width=24} @@ -462,87 +195,7 @@ local function config_view(display) PushButton{parent=cl,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - -- set tool functions now that we have the elements - - -- load a legacy config file - function tool_ctl.load_legacy() - local config = require("pocket.config") - - tmp_cfg.SVR_Channel = config.SVR_CHANNEL - tmp_cfg.CRD_Channel = config.CRD_CHANNEL - tmp_cfg.PKT_Channel = config.PKT_CHANNEL - tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT - tmp_cfg.TrustedRange = config.TRUSTED_RANGE - tmp_cfg.AuthKey = config.AUTH_KEY or "" - - tmp_cfg.LogMode = config.LOG_MODE - tmp_cfg.LogPath = config.LOG_PATH - tmp_cfg.LogDebug = config.LOG_DEBUG or false - - tool_ctl.gen_summary(tmp_cfg) - sum_pane.set_value(1) - main_pane.set_value(5) - tool_ctl.importing_legacy = true - end - - -- expose the auth key on the summary page - function tool_ctl.show_auth_key() - tool_ctl.show_key_btn.disable() - tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value) - end - - -- generate the summary list - ---@param cfg pkt_config - function tool_ctl.gen_summary(cfg) - setting_list.remove_all() - - local alternate = false - local inner_width = setting_list.get_width() - 1 - - tool_ctl.show_key_btn.enable() - tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key - - for i = 1, #fields do - local f = fields[i] - local height = 1 - local label_w = string.len(f[2]) - local val_max_w = (inner_width - label_w) - 1 - local raw = cfg[f[1]] - local val = util.strval(raw) - - if f[1] == "AuthKey" then - val = string.rep("*", string.len(val)) - elseif f[1] == "LogMode" then - val = util.trinary(raw == log.MODE.APPEND, "append", "replace") - elseif f[1] == "TempScale" then - val = util.strval(types.TEMP_SCALE_NAMES[raw]) - elseif f[1] == "EnergyScale" then - val = util.strval(types.ENERGY_SCALE_NAMES[raw]) - end - - if val == "nil" then val = "" end - - local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) - alternate = not alternate - - if string.len(val) > val_max_w then - local lines = util.strwrap(val, inner_width) - height = #lines + 1 - end - - local line = Div{parent=setting_list,height=height,fg_bg=c} - TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} - - local textbox - if height > 1 then - textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1} - else - textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT} - end - - if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end - end - end + --#endregion end -- reset terminal screen diff --git a/pocket/iocontrol.lua b/pocket/iocontrol.lua index b04f64d9..e72a5736 100644 --- a/pocket/iocontrol.lua +++ b/pocket/iocontrol.lua @@ -39,15 +39,6 @@ local io = { ps = psil.create() } --- luacheck: no unused args - --- placeholder acknowledge function for type hinting ----@param success boolean ----@diagnostic disable-next-line: unused-local -local function __generic_ack(success) end - --- luacheck: unused args - local config = nil ---@type pkt_config local comms = nil ---@type pocket_comms @@ -92,16 +83,18 @@ function iocontrol.init_core(pkt_comms, nav, cfg) get_tone_states = function () comms.diag__get_alarm_tones() end, - ready_warn = nil, ---@type graphics_element - tone_buttons = {}, - alarm_buttons = {}, - tone_indicators = {} -- indicators to update from supervisor tone states + ready_warn = nil, ---@type TextBox + tone_buttons = {}, ---@type SwitchButton[] + alarm_buttons = {}, ---@type Checkbox[] + tone_indicators = {} ---@type IndicatorLight[] indicators to update from supervisor tone states } -- API access ---@class pocket_ioctl_api io.api = { - get_unit = function (unit) comms.api__get_unit(unit) end + get_unit = function (unit) comms.api__get_unit(unit) end, + get_ctrl = function () comms.api__get_control() end, + get_proc = function () comms.api__get_process() end } end @@ -145,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, @@ -166,24 +161,21 @@ function iocontrol.init_fac(conf) radiation = types.new_zero_radiation_reading(), - start_ack = __generic_ack, - stop_ack = __generic_ack, - scram_ack = __generic_ack, - ack_alarms_ack = __generic_ack, + start_ack = nil, ---@type fun(success: boolean) + stop_ack = nil, ---@type fun(success: boolean) + scram_ack = nil, ---@type fun(success: boolean) + ack_alarms_ack = nil, ---@type fun(success: boolean) ps = psil.create(), - induction_ps_tbl = {}, - induction_data_tbl = {}, - - sps_ps_tbl = {}, - sps_data_tbl = {}, + induction_ps_tbl = {}, ---@type psil[] + induction_data_tbl = {}, ---@type imatrix_session_db[] - tank_ps_tbl = {}, - tank_data_tbl = {}, + sps_ps_tbl = {}, ---@type psil[] + sps_data_tbl = {}, ---@type sps_session_db[] - env_d_ps = psil.create(), - env_d_data = {} + tank_ps_tbl = {}, ---@type psil[] + tank_data_tbl = {} ---@type dynamicv_session_db[] } -- create induction and SPS tables (currently only 1 of each is supported) @@ -192,107 +184,14 @@ function iocontrol.init_fac(conf) table.insert(io.facility.sps_ps_tbl, psil.create()) table.insert(io.facility.sps_data_tbl, {}) - -- determine tank information - if io.facility.tank_mode == 0 then - io.facility.tank_defs = {} - -- on facility tank mode 0, setup tank defs to match unit tank option - for i = 1, conf.num_units do - io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0) - end - - io.facility.tank_list = { table.unpack(io.facility.tank_defs) } - else - -- decode the layout of tanks from the connections definitions - local tank_mode = io.facility.tank_mode - local tank_defs = io.facility.tank_defs - local tank_list = { table.unpack(tank_defs) } - - local function calc_fdef(start_idx, end_idx) - local first = 4 - for i = start_idx, end_idx do - if io.facility.tank_defs[i] == 2 then - if i < first then first = i end - end - end - return first - end - - if tank_mode == 1 then - -- (1) 1 total facility tank (A A A A) - local first_fdef = calc_fdef(1, #tank_defs) - for i = 1, #tank_defs do - if i > first_fdef and tank_defs[i] == 2 then - tank_list[i] = 0 - end - end - elseif tank_mode == 2 then - -- (2) 2 total facility tanks (A A A B) - local first_fdef = calc_fdef(1, math.min(3, #tank_defs)) - for i = 1, #tank_defs do - if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then - tank_list[i] = 0 - end - end - elseif tank_mode == 3 then - -- (3) 2 total facility tanks (A A B B) - for _, a in pairs({ 1, 3 }) do - local b = a + 1 - if (tank_defs[a] == 2) and (tank_defs[b] == 2) then - tank_list[b] = 0 - end - end - elseif tank_mode == 4 then - -- (4) 2 total facility tanks (A B B B) - local first_fdef = calc_fdef(2, #tank_defs) - for i = 1, #tank_defs do - if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then - tank_list[i] = 0 - end - end - elseif tank_mode == 5 then - -- (5) 3 total facility tanks (A A B C) - local first_fdef = calc_fdef(1, math.min(2, #tank_defs)) - for i = 1, #tank_defs do - if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then - tank_list[i] = 0 - end - end - elseif tank_mode == 6 then - -- (6) 3 total facility tanks (A B B C) - local first_fdef = calc_fdef(2, math.min(3, #tank_defs)) - for i = 1, #tank_defs do - if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then - tank_list[i] = 0 - end - end - elseif tank_mode == 7 then - -- (7) 3 total facility tanks (A B C C) - local first_fdef = calc_fdef(3, #tank_defs) - for i = 1, #tank_defs do - if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then - tank_list[i] = 0 - end - end - end - - io.facility.tank_list = tank_list - end - - -- create facility tank tables - for i = 1, #io.facility.tank_list do - if io.facility.tank_list[i] == 2 then - table.insert(io.facility.tank_ps_tbl, psil.create()) - table.insert(io.facility.tank_data_tbl, {}) - end - end - -- create unit data structures - io.units = {} + io.units = {} ---@type pioctl_unit[] for i = 1, conf.num_units do ---@class pioctl_unit local entry = { unit_id = i, connected = false, + ---@type { boilers: { connected: boolean, faulted: boolean }[], turbines: { connected: boolean, faulted: boolean }[] } rtu_hw = {}, num_boilers = 0, @@ -300,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(), @@ -323,27 +227,27 @@ function iocontrol.init_fac(conf) ack_alarms = function () process.ack_all_alarms(i) end, set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate - start_ack = __generic_ack, - scram_ack = __generic_ack, - reset_rps_ack = __generic_ack, - ack_alarms_ack = __generic_ack, + start_ack = nil, ---@type fun(success: boolean) + scram_ack = nil, ---@type fun(success: boolean) + reset_rps_ack = nil, ---@type fun(success: boolean) + ack_alarms_ack = nil, ---@type fun(success: boolean) - ---@type alarms + ---@type { [ALARM]: ALARM_STATE } alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE }, - annunciator = {}, ---@type annunciator + annunciator = {}, ---@type annunciator unit_ps = psil.create(), - reactor_data = {}, ---@type reactor_db + reactor_data = types.new_reactor_db(), - boiler_ps_tbl = {}, - boiler_data_tbl = {}, + boiler_ps_tbl = {}, ---@type psil[] + boiler_data_tbl = {}, ---@type boilerv_session_db[] - turbine_ps_tbl = {}, - turbine_data_tbl = {}, + turbine_ps_tbl = {}, ---@type psil[] + turbine_data_tbl = {}, ---@type turbinev_session_db[] - tank_ps_tbl = {}, - tank_data_tbl = {} + tank_ps_tbl = {}, ---@type psil[] + tank_data_tbl = {} ---@type dynamicv_session_db[] } -- on other facility modes, overwrite unit TANK option with facility tank defs @@ -485,7 +389,7 @@ end -- update unit status data from API_GET_UNIT ---@param data table function iocontrol.record_unit_data(data) - local unit = io.units[data[1]] ---@type pioctl_unit + local unit = io.units[data[1]] unit.connected = data[2] unit.rtu_hw = data[3] @@ -625,16 +529,16 @@ function iocontrol.record_unit_data(data) end end - if type(unit.reactor_data.rps_status) == "table" then - for key, val in pairs(unit.reactor_data.rps_status) do - unit.unit_ps.publish(key, val) - end + for key, val in pairs(unit.reactor_data.rps_status) do + unit.unit_ps.publish(key, val) end - if type(unit.reactor_data.mek_status) == "table" then - for key, val in pairs(unit.reactor_data.mek_status) do - unit.unit_ps.publish(key, val) - end + for key, val in pairs(unit.reactor_data.mek_struct) do + unit.unit_ps.publish(key, val) + end + + for key, val in pairs(unit.reactor_data.mek_status) do + unit.unit_ps.publish(key, val) end end @@ -650,8 +554,8 @@ function iocontrol.record_unit_data(data) unit.boiler_data_tbl = data[8] for id = 1, #unit.boiler_data_tbl do - local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db - local ps = unit.boiler_ps_tbl[id] ---@type psil + local boiler = unit.boiler_data_tbl[id] + local ps = unit.boiler_ps_tbl[id] local boiler_status = 1 local computed_status = 1 @@ -683,8 +587,8 @@ function iocontrol.record_unit_data(data) unit.turbine_data_tbl = data[9] for id = 1, #unit.turbine_data_tbl do - local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db - local ps = unit.turbine_ps_tbl[id] ---@type psil + local turbine = unit.turbine_data_tbl[id] + local ps = unit.turbine_ps_tbl[id] local turbine_status = 1 local computed_status = 1 @@ -730,21 +634,6 @@ function iocontrol.record_unit_data(data) local function white(text) return { text = text, color = colors.white } end local function blue(text) return { text = text, color = colors.blue } end - -- unit.reactor_data.rps_status = { - -- high_dmg = false, - -- high_temp = false, - -- low_cool = false, - -- ex_waste = false, - -- ex_hcool = false, - -- no_fuel = false, - -- fault = false, - -- timeout = false, - -- manual = false, - -- automatic = false, - -- sys_fail = false, - -- force_dis = false - -- } - -- if unit.reactor_data.rps_status then -- for k, v in pairs(unit.alarms) do -- unit.alarms[k] = ALARM_STATE.TRIPPED @@ -928,6 +817,109 @@ function iocontrol.record_unit_data(data) --#endregion end +-- update control app with unit data from API_GET_CTRL +---@param data table +function iocontrol.record_control_data(data) + for u_id = 1, #data do + local unit = io.units[u_id] + local u_data = data[u_id] + + 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.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]) + + 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 + 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 80a2cb59..de6e6b26 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -82,40 +82,42 @@ 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 ---@class nav_tree_page ---@field _p nav_tree_page|nil page's parent ----@field _c table page's children +---@field _c nav_tree_page[] page's children ---@field nav_to function function to navigate to this page ---@field switcher function|nil function to switch between children ----@field tasks table tasks to run while viewing this page +---@field tasks function[] tasks to run while viewing this page -- initialize the page navigation system ---@param smem pkt_shared_memory function pocket.init_nav(smem) local self = { - pane = nil, ---@type graphics_element - sidebar = nil, ---@type graphics_element - apps = {}, - containers = {}, - help_map = {}, - help_return = nil, - loader_return = nil, + pane = nil, ---@type AppMultiPane|MultiPane|nil + sidebar = nil, ---@type Sidebar|nil + apps = {}, ---@type pocket_app[] + containers = {}, ---@type Container[] + help_map = {}, ---@type { [string]: function } + help_return = nil, ---@type POCKET_APP_ID|nil + loader_return = nil, ---@type POCKET_APP_ID|nil cur_app = APP_ID.ROOT } @@ -125,27 +127,27 @@ function pocket.init_nav(smem) local nav = {} -- set the root pane element to switch between apps with - ---@param root_pane graphics_element + ---@param root_pane MultiPane function nav.set_pane(root_pane) self.pane = root_pane end -- link sidebar element - ---@param sidebar graphics_element + ---@param sidebar Sidebar function nav.set_sidebar(sidebar) self.sidebar = sidebar end -- register an app ---@param app_id POCKET_APP_ID app ID - ---@param container graphics_element element that contains this app (usually a Div) - ---@param pane? graphics_element multipane if this is a simple paned app, then nav_to must be a number + ---@param container Container element that contains this app (usually a Div) + ---@param pane? AppMultiPane|MultiPane multipane if this is a simple paned app, then nav_to must be a number ---@param require_sv? boolean true to specifiy if this app should be unloaded when the supervisor connection is lost ---@param require_api? boolean true to specifiy if this app should be unloaded when the api connection is lost function nav.register_app(app_id, container, pane, require_sv, require_api) ---@class pocket_app local app = { loaded = false, - cur_page = nil, ---@type nav_tree_page + cur_page = nil, ---@type nav_tree_page pane = pane, - paned_pages = {}, - sidebar_items = {} + paned_pages = {}, ---@type nav_tree_page[] + sidebar_items = {} ---@type sidebar_entry[] } app.load = function () app.loaded = true end @@ -159,24 +161,27 @@ function pocket.init_nav(smem) function app.requires_conn() return require_sv or require_api or false end -- delayed set of the pane if it wasn't ready at the start - ---@param root_pane graphics_element multipane + ---@param root_pane AppMultiPane|MultiPane multipane function app.set_root_pane(root_pane) app.pane = root_pane end -- configure the sidebar - ---@param items table + ---@param items sidebar_entry[] function app.set_sidebar(items) app.sidebar_items = items - if self.sidebar then self.sidebar.update(items) end + -- only modify the sidebar if this app is still open + if self.cur_app == app_id then + if self.sidebar then self.sidebar.update(items) end + end end -- function to run on initial load into memory ---@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 @@ -184,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 @@ -263,7 +268,7 @@ function pocket.init_nav(smem) -- reset help return on navigating out of an app if app_id == APP_ID.ROOT then self.help_return = nil end - local app = self.apps[app_id] ---@type pocket_app + local app = self.apps[app_id] if app then if app.requires_conn() and not smem.pkt_sys.pocket_comms.is_linked() then -- bring up the app loader @@ -285,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 @@ -339,7 +347,7 @@ function pocket.init_nav(smem) return end - local app = self.apps[self.cur_app] ---@type pocket_app + local app = self.apps[self.cur_app] log.debug("attempting app nav up for app " .. self.cur_app) if not app.nav_up() then @@ -359,6 +367,7 @@ function pocket.init_nav(smem) end -- link the help map from the guide app + ---@param map { [string]: function } function nav.link_help(map) self.help_map = map end return nav @@ -546,6 +555,16 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end end + -- coordinator get control app data + function public.api__get_control() + 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) @@ -553,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 @@ -655,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 @@ -698,6 +725,14 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav) if _check_length(packet, 12) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then iocontrol.record_unit_data(packet.data) end + elseif packet.type == CRDN_TYPE.API_GET_CTRL then + 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 454caff3..d0a32416 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/renderer.lua b/pocket/renderer.lua index bc16037f..727a74b9 100644 --- a/pocket/renderer.lua +++ b/pocket/renderer.lua @@ -8,7 +8,7 @@ local style = require("pocket.ui.style") local core = require("graphics.core") local flasher = require("graphics.flasher") -local DisplayBox = require("graphics.elements.displaybox") +local DisplayBox = require("graphics.elements.DisplayBox") ---@class pocket_renderer local renderer = {} diff --git a/pocket/startup.lua b/pocket/startup.lua index 28b37d93..0f6fe408 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.1-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 4066603f..57cabc87 100644 --- a/pocket/ui/apps/control.lua +++ b/pocket/ui/apps/control.lua @@ -13,19 +13,19 @@ local style = require("pocket.ui.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local WaitingAnim = require("graphics.elements.animations.waiting") +local WaitingAnim = require("graphics.elements.animations.Waiting") -local HazardButton = require("graphics.elements.controls.hazard_button") -local PushButton = require("graphics.elements.controls.push_button") +local HazardButton = require("graphics.elements.controls.HazardButton") +local PushButton = require("graphics.elements.controls.PushButton") -local NumberField = require("graphics.elements.form.number_field") +local NumberField = require("graphics.elements.form.NumberField") -local DataIndicator = require("graphics.elements.indicators.data") -local IconIndicator = require("graphics.elements.indicators.icon") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local IconIndicator = require("graphics.elements.indicators.IconIndicator") local AUTO_GROUP = types.AUTO_GROUP @@ -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 graphics_element parent +---@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 nil|graphics_element + 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 } } @@ -83,7 +85,7 @@ local function new_view(root) local function load() page_div = Div{parent=main,y=2,width=main.get_width()} - local panes = {} + local panes = {} ---@type Div[] local active_unit = 1 @@ -105,21 +107,21 @@ local function new_view(root) app.switcher(active_unit) 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_ctrl() + last_update = util.time_ms() + end + end + 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] ---@type pioctl_unit + local unit = db.units[i] local u_ps = unit.unit_ps - -- refresh data callback, every 500ms it will re-send the query - local last_update = 0 - local function update() - if util.time_ms() - last_update >= 500 then - db.api.get_unit(i) - last_update = util.time_ms() - end - end - local u_page = app.new_page(nil, i) u_page.tasks = { update } @@ -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 @@ -167,12 +169,10 @@ local function new_view(root) unit.reset_rps_ack = reset.on_response local function start_button_en_check() - if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then - local can_start = (not unit.reactor_data.mek_status.status) and - (not unit.reactor_data.rps_tripped) and - (unit.a_group == AUTO_GROUP.MANUAL) - if can_start then start.enable() else start.disable() end - end + local can_start = (not unit.reactor_data.mek_status.status) and + (not unit.reactor_data.rps_tripped) and + (unit.a_group == AUTO_GROUP.MANUAL) + if can_start then start.enable() else start.disable() end end start.register(u_ps, "status", start_button_en_check) @@ -194,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 @@ -217,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/diag_apps.lua b/pocket/ui/apps/diag_apps.lua index 79b3c127..ba2bed31 100644 --- a/pocket/ui/apps/diag_apps.lua +++ b/pocket/ui/apps/diag_apps.lua @@ -7,14 +7,14 @@ local pocket = require("pocket.pocket") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") -local IndicatorLight = require("graphics.elements.indicators.light") +local IndicatorLight = require("graphics.elements.indicators.IndicatorLight") -local Checkbox = require("graphics.elements.controls.checkbox") -local PushButton = require("graphics.elements.controls.push_button") -local SwitchButton = require("graphics.elements.controls.switch_button") +local Checkbox = require("graphics.elements.controls.Checkbox") +local PushButton = require("graphics.elements.controls.PushButton") +local SwitchButton = require("graphics.elements.controls.SwitchButton") local ALIGN = core.ALIGN local cpair = core.cpair @@ -22,7 +22,7 @@ local cpair = core.cpair local APP_ID = pocket.APP_ID -- create diagnostic app pages ----@param root graphics_element parent +---@param root Container parent local function create_pages(root) local db = iocontrol.get_db() diff --git a/pocket/ui/apps/dummy_app.lua b/pocket/ui/apps/dummy_app.lua index 6e924938..a5647614 100644 --- a/pocket/ui/apps/dummy_app.lua +++ b/pocket/ui/apps/dummy_app.lua @@ -7,13 +7,13 @@ local pocket = require("pocket.pocket") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") local APP_ID = pocket.APP_ID -- create placeholder app page ----@param root graphics_element parent +---@param root Container parent local function create_pages(root) local db = iocontrol.get_db() diff --git a/pocket/ui/apps/guide.lua b/pocket/ui/apps/guide.lua index 768824a1..2efb8624 100644 --- a/pocket/ui/apps/guide.lua +++ b/pocket/ui/apps/guide.lua @@ -15,16 +15,16 @@ local guide_section = require("pocket.ui.pages.guide_section") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local WaitingAnim = require("graphics.elements.animations.waiting") +local WaitingAnim = require("graphics.elements.animations.Waiting") -local PushButton = require("graphics.elements.controls.push_button") +local PushButton = require("graphics.elements.controls.PushButton") -local TextField = require("graphics.elements.form.text_field") +local TextField = require("graphics.elements.form.TextField") local ALIGN = core.ALIGN local cpair = core.cpair @@ -36,7 +36,7 @@ local APP_ID = pocket.APP_ID -- local text_fg = style.text_fg -- new system guide view ----@param root graphics_element parent +---@param root Container parent local function new_view(root) local db = iocontrol.get_db() @@ -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 nil|graphics_element + 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 } } @@ -88,12 +88,11 @@ local function new_view(root) local fps = Div{parent=page_div,x=2,width=p_width} local gls = Div{parent=page_div,x=2,width=p_width} local lnk = Div{parent=page_div,x=2,width=p_width} - local panes = { home, search, use, uis, fps, gls, lnk } + local panes = { home, search, use, uis, fps, gls, lnk } ---@type Div[] - local doc_map = {} - local search_db = {} + local doc_map = {} ---@type { [string]: function } + local search_db = {} ---@type [ string, string, string, function ][] - ---@class _guide_section_constructor_data local sect_construct_data = { app, page_div, panes, doc_map, search_db, btn_fg_bg, btn_active } TextBox{parent=home,y=1,text="cc-mek-scada Guide",alignment=ALIGN.CENTER} @@ -117,7 +116,7 @@ local function new_view(root) function func_ref.run_search() local query = string.lower(query_field.get_value()) - local s_results = { {}, {}, {}, {} } + local s_results = { {}, {}, {}, {} } ---@type [ string, string, string, function ][][] search_results.remove_all() @@ -155,7 +154,7 @@ local function new_view(root) for idx = 1, #s_results[tier] do local entry = s_results[tier][idx] TextBox{parent=search_results,text=entry[3].." >",fg_bg=cpair(colors.gray,colors.black)} - PushButton{parent=search_results,text=entry[2],fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=entry[4]} + PushButton{parent=search_results,text=entry[2],alignment=ALIGN.LEFT,fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=entry[4]} empty = false end @@ -264,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/loader.lua b/pocket/ui/apps/loader.lua index 8ea72f06..7a45a7ab 100644 --- a/pocket/ui/apps/loader.lua +++ b/pocket/ui/apps/loader.lua @@ -9,16 +9,16 @@ local conn_waiting = require("pocket.ui.components.conn_waiting") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") local APP_ID = pocket.APP_ID local LINK_STATE = iocontrol.LINK_STATE -- create the connecting to SV & API page ----@param root graphics_element parent +---@param root Container parent local function create_pages(root) local db = iocontrol.get_db() diff --git a/pocket/ui/apps/process.lua b/pocket/ui/apps/process.lua new file mode 100644 index 00000000..deb7b1f4 --- /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/sys_apps.lua b/pocket/ui/apps/sys_apps.lua index 197a4f66..d797caf8 100644 --- a/pocket/ui/apps/sys_apps.lua +++ b/pocket/ui/apps/sys_apps.lua @@ -12,12 +12,12 @@ local pocket = require("pocket.pocket") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local PushButton = require("graphics.elements.controls.push_button") +local PushButton = require("graphics.elements.controls.PushButton") local ALIGN = core.ALIGN local cpair = core.cpair @@ -25,7 +25,7 @@ local cpair = core.cpair local APP_ID = pocket.APP_ID -- create system app pages ----@param root graphics_element parent +---@param root Container parent local function create_pages(root) local db = iocontrol.get_db() diff --git a/pocket/ui/apps/unit.lua b/pocket/ui/apps/unit.lua index 94f33417..bc34d5fc 100644 --- a/pocket/ui/apps/unit.lua +++ b/pocket/ui/apps/unit.lua @@ -15,17 +15,17 @@ local turbine = require("pocket.ui.pages.unit_turbine") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local WaitingAnim = require("graphics.elements.animations.waiting") +local WaitingAnim = require("graphics.elements.animations.Waiting") -local PushButton = require("graphics.elements.controls.push_button") +local PushButton = require("graphics.elements.controls.PushButton") -local DataIndicator = require("graphics.elements.indicators.data") -local IconIndicator = require("graphics.elements.indicators.icon") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local IconIndicator = require("graphics.elements.indicators.IconIndicator") local ALIGN = core.ALIGN local cpair = core.cpair @@ -47,7 +47,7 @@ local emc_ind_s = { } -- new unit page view ----@param root graphics_element parent +---@param root Container parent local function new_view(root) local db = iocontrol.get_db() @@ -63,20 +63,20 @@ 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) local nav_links = {} - local page_div = nil ---@type nil|graphics_element + local page_div = nil ---@type Div|nil -- set sidebar to display unit-specific fields based on a specified unit local function set_sidebar(id) - local unit = db.units[id] ---@type pioctl_unit + local unit = db.units[id] 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 }, @@ -99,7 +99,7 @@ local function new_view(root) local function load() page_div = Div{parent=main,y=2,width=main.get_width()} - local panes = {} + local panes = {} ---@type Div[] local active_unit = 1 @@ -127,7 +127,7 @@ local function new_view(root) 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] ---@type pioctl_unit + local unit = db.units[i] local u_ps = unit.unit_ps -- refresh data callback, every 500ms it will re-send the query @@ -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/components/conn_waiting.lua b/pocket/ui/components/conn_waiting.lua index b55f0670..fd2fb80e 100644 --- a/pocket/ui/components/conn_waiting.lua +++ b/pocket/ui/components/conn_waiting.lua @@ -8,17 +8,17 @@ local style = require("pocket.ui.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") -local WaitingAnim = require("graphics.elements.animations.waiting") +local WaitingAnim = require("graphics.elements.animations.Waiting") local ALIGN = core.ALIGN local cpair = core.cpair -- create a waiting view ----@param parent graphics_element parent +---@param parent Container parent ---@param y integer y offset local function init(parent, y, is_api) -- root div @@ -29,15 +29,15 @@ local function init(parent, y, is_api) local waiting_x = math.floor(parent.get_width() / 2) - 1 - local msg = TextBox{parent=box,x=3,y=11,width=box.get_width()-4,height=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.red,style.root.bkg)} + local msg = TextBox{parent=box,x=3,y=11,width=box.get_width()-4,height=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.red,style.root.bkg),trim_whitespace=true} if is_api then WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)} - TextBox{parent=box,y=5,text="Connecting to API",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg)} + TextBox{parent=box,y=5,text="Connecting to API",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg),trim_whitespace=true} msg.register(iocontrol.get_db().ps, "api_link_msg", msg.set_value) else WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)} - TextBox{parent=box,y=5,text="Connecting to Supervisor",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg)} + TextBox{parent=box,y=5,text="Connecting to Supervisor",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg),trim_whitespace=true} msg.register(iocontrol.get_db().ps, "svr_link_msg", msg.set_value) end diff --git a/pocket/ui/main.lua b/pocket/ui/main.lua index bf66f9bc..15964858 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") @@ -21,16 +22,16 @@ local style = require("pocket.ui.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local WaitingAnim = require("graphics.elements.animations.waiting") +local WaitingAnim = require("graphics.elements.animations.Waiting") -local PushButton = require("graphics.elements.controls.push_button") -local Sidebar = require("graphics.elements.controls.sidebar") +local PushButton = require("graphics.elements.controls.PushButton") +local Sidebar = require("graphics.elements.controls.Sidebar") -local SignalBar = require("graphics.elements.indicators.signal") +local SignalBar = require("graphics.elements.indicators.SignalBar") local ALIGN = core.ALIGN local cpair = core.cpair @@ -38,7 +39,7 @@ local cpair = core.cpair local APP_ID = pocket.APP_ID -- create new main view ----@param main graphics_element main displaybox +---@param main DisplayBox main displaybox local function init(main) local db = iocontrol.get_db() @@ -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/guide_section.lua b/pocket/ui/pages/guide_section.lua index c14a6ab0..76ceaf6c 100644 --- a/pocket/ui/pages/guide_section.lua +++ b/pocket/ui/pages/guide_section.lua @@ -5,14 +5,14 @@ local docs = require("pocket.ui.docs") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local TextBox = require("graphics.elements.TextBox") -local PushButton = require("graphics.elements.controls.push_button") +local PushButton = require("graphics.elements.controls.PushButton") -local IndicatorLight = require("graphics.elements.indicators.light") -local LED = require("graphics.elements.indicators.led") +local IndicatorLight = require("graphics.elements.indicators.IndicatorLight") +local LED = require("graphics.elements.indicators.LED") local ALIGN = core.ALIGN local cpair = core.cpair @@ -21,14 +21,14 @@ local DOC_TYPE = docs.DOC_ITEM_TYPE local LIST_TYPE = docs.DOC_LIST_TYPE -- new guide documentation section ----@param data _guide_section_constructor_data +---@param data { [1]: pocket_app, [2]: Div, [3]: Div[], [4]: { [string]: function }, [5]: [ string, string, string, function ][], [6]: cpair, [7]: cpair } ---@param base_page nav_tree_page ---@param title string ---@param items table ---@param scroll_height integer ---@return nav_tree_page return function (data, base_page, title, items, scroll_height) - local app, page_div, panes, doc_map, search_db, btn_fg_bg, btn_active = table.unpack(data) + local app, page_div, panes, doc_map, search_db, btn_fg_bg, btn_active = data[1], data[2], data[3], data[4], data[5], data[6], data[7] local section_page = app.new_page(base_page, #panes + 1) local section_div = Div{parent=page_div,x=2} diff --git a/pocket/ui/pages/home_page.lua b/pocket/ui/pages/home_page.lua index 38385be8..1e478dc2 100644 --- a/pocket/ui/pages/home_page.lua +++ b/pocket/ui/pages/home_page.lua @@ -7,11 +7,11 @@ local pocket = require("pocket.pocket") local core = require("graphics.core") -local AppMultiPane = require("graphics.elements.appmultipane") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local AppMultiPane = require("graphics.elements.AppMultiPane") +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") -local App = require("graphics.elements.controls.app") +local App = require("graphics.elements.controls.App") local ALIGN = core.ALIGN local cpair = core.cpair @@ -19,7 +19,7 @@ local cpair = core.cpair local APP_ID = pocket.APP_ID -- new home page view ----@param root graphics_element parent +---@param root Container parent local function new_view(root) local db = iocontrol.get_db() @@ -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/pages/unit_boiler.lua b/pocket/ui/pages/unit_boiler.lua index 4ac39934..17902689 100644 --- a/pocket/ui/pages/unit_boiler.lua +++ b/pocket/ui/pages/unit_boiler.lua @@ -7,15 +7,15 @@ local style = require("pocket.ui.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") -local PushButton = require("graphics.elements.controls.push_button") +local PushButton = require("graphics.elements.controls.PushButton") -local DataIndicator = require("graphics.elements.indicators.data") -local StateIndicator = require("graphics.elements.indicators.state") -local IconIndicator = require("graphics.elements.indicators.icon") -local VerticalBar = require("graphics.elements.indicators.vbar") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local StateIndicator = require("graphics.elements.indicators.StateIndicator") +local IconIndicator = require("graphics.elements.indicators.IconIndicator") +local VerticalBar = require("graphics.elements.indicators.VerticalBar") local ALIGN = core.ALIGN local cpair = core.cpair @@ -29,8 +29,8 @@ local yel_ind_s = style.icon_states.yel_ind_s -- create a boiler view in the unit app ---@param app pocket_app ---@param u_page nav_tree_page ----@param panes table ----@param blr_pane graphics_element +---@param panes Div[] +---@param blr_pane Div ---@param b_id integer boiler ID ---@param ps psil ---@param update function diff --git a/pocket/ui/pages/unit_reactor.lua b/pocket/ui/pages/unit_reactor.lua index 56b03780..a3333f5e 100644 --- a/pocket/ui/pages/unit_reactor.lua +++ b/pocket/ui/pages/unit_reactor.lua @@ -7,15 +7,15 @@ local style = require("pocket.ui.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") -local PushButton = require("graphics.elements.controls.push_button") +local PushButton = require("graphics.elements.controls.PushButton") -local DataIndicator = require("graphics.elements.indicators.data") -local StateIndicator = require("graphics.elements.indicators.state") -local IconIndicator = require("graphics.elements.indicators.icon") -local VerticalBar = require("graphics.elements.indicators.vbar") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local StateIndicator = require("graphics.elements.indicators.StateIndicator") +local IconIndicator = require("graphics.elements.indicators.IconIndicator") +local VerticalBar = require("graphics.elements.indicators.VerticalBar") local ALIGN = core.ALIGN local cpair = core.cpair @@ -29,8 +29,8 @@ local yel_ind_s = style.icon_states.yel_ind_s -- create a reactor view in the unit app ---@param app pocket_app ---@param u_page nav_tree_page ----@param panes table ----@param page_div graphics_element +---@param panes Div[] +---@param page_div Div ---@param u_ps psil ---@param update function return function (app, u_page, panes, page_div, u_ps, update) diff --git a/pocket/ui/pages/unit_turbine.lua b/pocket/ui/pages/unit_turbine.lua index df90061d..94541add 100644 --- a/pocket/ui/pages/unit_turbine.lua +++ b/pocket/ui/pages/unit_turbine.lua @@ -6,16 +6,16 @@ local style = require("pocket.ui.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") -local PushButton = require("graphics.elements.controls.push_button") +local PushButton = require("graphics.elements.controls.PushButton") -local DataIndicator = require("graphics.elements.indicators.data") -local IconIndicator = require("graphics.elements.indicators.icon") -local PowerIndicator = require("graphics.elements.indicators.power") -local StateIndicator = require("graphics.elements.indicators.state") -local VerticalBar = require("graphics.elements.indicators.vbar") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local IconIndicator = require("graphics.elements.indicators.IconIndicator") +local PowerIndicator = require("graphics.elements.indicators.PowerIndicator") +local StateIndicator = require("graphics.elements.indicators.StateIndicator") +local VerticalBar = require("graphics.elements.indicators.VerticalBar") local ALIGN = core.ALIGN local cpair = core.cpair @@ -30,8 +30,8 @@ local yel_ind_s = style.icon_states.yel_ind_s -- create a turbine view in the unit app ---@param app pocket_app ---@param u_page nav_tree_page ----@param panes table ----@param tbn_pane graphics_element +---@param panes Div[] +---@param tbn_pane Div ---@param u_id integer unit ID ---@param t_id integer turbine ID ---@param ps psil diff --git a/pocket/ui/style.lua b/pocket/ui/style.lua index dc267555..ff7fc9be 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/reactor-plc/config/check.lua b/reactor-plc/config/check.lua index 106a51d2..7a964feb 100644 --- a/reactor-plc/config/check.lua +++ b/reactor-plc/config/check.lua @@ -8,11 +8,11 @@ local plc = require("reactor-plc.plc") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local TextBox = require("graphics.elements.TextBox") -local PushButton = require("graphics.elements.controls.push_button") +local PushButton = require("graphics.elements.controls.PushButton") local tri = util.trinary @@ -33,8 +33,8 @@ local self = { settings = nil, ---@type plc_config - run_test_btn = nil, ---@type graphics_element - sc_log = nil, ---@type graphics_element + run_test_btn = nil, ---@type PushButton + sc_log = nil, ---@type ListBox self_check_msg = nil ---@type function } @@ -160,7 +160,7 @@ local function self_check() end -- exit self check back home ----@param main_pane graphics_element +---@param main_pane MultiPane local function exit_self_check(main_pane) tcd.abort(handle_timeout) self.net_listen = false @@ -172,10 +172,10 @@ end local check = {} -- create the self-check view ----@param main_pane graphics_element +---@param main_pane MultiPane ---@param settings_cfg plc_config ----@param check_sys graphics_element ----@param style table +---@param check_sys Div +---@param style { [string]: cpair } function check.create(main_pane, settings_cfg, check_sys, style) local bw_fg_bg = style.bw_fg_bg local g_lg_fg_bg = style.g_lg_fg_bg diff --git a/reactor-plc/config/system.lua b/reactor-plc/config/system.lua index f612f9ec..d6bd7345 100644 --- a/reactor-plc/config/system.lua +++ b/reactor-plc/config/system.lua @@ -5,20 +5,20 @@ local util = require("scada-common.util") local core = require("graphics.core") local themes = require("graphics.themes") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local CheckBox = require("graphics.elements.controls.checkbox") -local PushButton = require("graphics.elements.controls.push_button") -local Radio2D = require("graphics.elements.controls.radio_2d") -local RadioButton = require("graphics.elements.controls.radio_button") +local Checkbox = require("graphics.elements.controls.Checkbox") +local PushButton = require("graphics.elements.controls.PushButton") +local Radio2D = require("graphics.elements.controls.Radio2D") +local RadioButton = require("graphics.elements.controls.RadioButton") -local NumberField = require("graphics.elements.form.number_field") -local TextField = require("graphics.elements.form.text_field") +local NumberField = require("graphics.elements.form.NumberField") +local TextField = require("graphics.elements.form.TextField") -local IndLight = require("graphics.elements.indicators.light") +local IndLight = require("graphics.elements.indicators.IndicatorLight") local cpair = core.cpair @@ -31,8 +31,8 @@ local self = { bundled_emcool = nil, ---@type function show_auth_key = nil, ---@type function - show_key_btn = nil, ---@type graphics_element - auth_key_textbox = nil, ---@type graphics_element + show_key_btn = nil, ---@type PushButton + auth_key_textbox = nil, ---@type TextBox auth_key_value = "" } @@ -61,17 +61,14 @@ local system = {} -- create the system configuration view ---@param tool_ctl _plc_cfg_tool_ctl ----@param main_pane graphics_element ----@param cfg_sys table ----@param divs table ----@param style table +---@param main_pane MultiPane +---@param cfg_sys [ plc_config, plc_config, plc_config, { [1]: string, [2]: string, [3]: any }[], function ] +---@param divs Div[] +---@param style { [string]: cpair } ---@param exit function function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit) - ---@type plc_config, plc_config, plc_config, table, function - local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = table.unpack(cfg_sys) - - ---@type graphics_element, graphics_element, graphics_element, graphics_element, graphics_element - local plc_cfg, net_cfg, log_cfg, clr_cfg, summary = table.unpack(divs) + local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5] + local plc_cfg, net_cfg, log_cfg, clr_cfg, summary = divs[1], divs[2], divs[3], divs[4], divs[5] local bw_fg_bg = style.bw_fg_bg local g_lg_fg_bg = style.g_lg_fg_bg @@ -93,7 +90,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit) TextBox{parent=plc_c_1,x=1,y=1,text="Would you like to set this PLC as networked?"} TextBox{parent=plc_c_1,x=1,y=3,height=4,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=g_lg_fg_bg} - local networked = CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)} + local networked = Checkbox{parent=plc_c_1,x=1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)} local function submit_networked() self.set_networked(networked.get_value()) @@ -131,7 +128,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit) TextBox{parent=plc_c_3,x=1,y=1,height=4,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC."} TextBox{parent=plc_c_3,x=1,y=6,height=5,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=g_lg_fg_bg} - local en_em_cool = CheckBox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",default=ini_cfg.EmerCoolEnable,box_fg_bg=cpair(colors.orange,colors.black)} + local en_em_cool = Checkbox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",default=ini_cfg.EmerCoolEnable,box_fg_bg=cpair(colors.orange,colors.black)} local function next_from_plc() if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(4) end @@ -149,7 +146,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit) local side = Radio2D{parent=plc_c_4,x=1,y=2,rows=2,columns=3,default=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange} TextBox{parent=plc_c_4,x=1,y=5,text="Bundled Redstone Configuration"} - local bundled = CheckBox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",default=ini_cfg.EmerCoolColor~=nil,box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)self.bundled_emcool(v)end} + local bundled = Checkbox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",default=ini_cfg.EmerCoolColor~=nil,box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)self.bundled_emcool(v)end} local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,default=color_to_idx(ini_cfg.EmerCoolColor),options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg} if ini_cfg.EmerCoolColor == nil then color.disable() end @@ -240,14 +237,14 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit) PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} TextBox{parent=net_c_3,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} - TextBox{parent=net_c_3,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=g_lg_fg_bg} + TextBox{parent=net_c_3,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_3,x=1,y=11,text="Facility Auth Key"} - local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg} + local key, _ = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg} - local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end + local function censor_key(enable) key.censor(util.trinary(enable, "*", nil)) end - local hide_key = CheckBox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} + local hide_key = Checkbox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} hide_key.set_value(true) censor_key(true) @@ -282,7 +279,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit) TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"} local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg} - local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} + local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg} local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} diff --git a/reactor-plc/configure.lua b/reactor-plc/configure.lua index 81933f93..828c2e69 100644 --- a/reactor-plc/configure.lua +++ b/reactor-plc/configure.lua @@ -12,13 +12,13 @@ local system = require("reactor-plc.config.system") local core = require("graphics.core") local themes = require("graphics.themes") -local DisplayBox = require("graphics.elements.displaybox") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") +local DisplayBox = require("graphics.elements.DisplayBox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local PushButton = require("graphics.elements.controls.push_button") +local PushButton = require("graphics.elements.controls.PushButton") local println = util.println local tri = util.trinary @@ -58,11 +58,11 @@ local tool_ctl = { viewing_config = false, jumped_to_color = false, - view_cfg = nil, ---@type graphics_element - color_cfg = nil, ---@type graphics_element - color_next = nil, ---@type graphics_element - color_apply = nil, ---@type graphics_element - settings_apply = nil, ---@type graphics_element + view_cfg = nil, ---@type PushButton + color_cfg = nil, ---@type PushButton + color_next = nil, ---@type PushButton + color_apply = nil, ---@type PushButton + settings_apply = nil, ---@type PushButton gen_summary = nil, ---@type function load_legacy = nil, ---@type function @@ -80,11 +80,11 @@ local tmp_cfg = { ConnTimeout = nil, ---@type number TrustedRange = nil, ---@type number AuthKey = nil, ---@type string|nil - LogMode = 0, + LogMode = 0, ---@type LOG_MODE LogPath = "", LogDebug = false, - FrontPanelTheme = 1, - ColorMode = 1 + FrontPanelTheme = 1, ---@type FP_THEME + ColorMode = 1 ---@type COLOR_MODE } ---@class plc_config @@ -125,7 +125,7 @@ local function load_settings(target, raw) end -- create the config view ----@param display graphics_element +---@param display DisplayBox local function config_view(display) local bw_fg_bg = style.bw_fg_bg local g_lg_fg_bg = style.g_lg_fg_bg diff --git a/reactor-plc/databus.lua b/reactor-plc/databus.lua index 7436dbb5..b2bd1c22 100644 --- a/reactor-plc/databus.lua +++ b/reactor-plc/databus.lua @@ -77,7 +77,7 @@ end -- transmit RPS data across the bus ---@param tripped boolean RPS tripped ----@param status table RPS status +---@param status boolean[] RPS status ---@param emer_cool_active boolean RPS activated the emergency coolant function databus.tx_rps(tripped, status, emer_cool_active) databus.ps.publish("rps_scram", tripped) diff --git a/reactor-plc/panel/front_panel.lua b/reactor-plc/panel/front_panel.lua index d07395fb..8bb6a0f4 100644 --- a/reactor-plc/panel/front_panel.lua +++ b/reactor-plc/panel/front_panel.lua @@ -13,15 +13,15 @@ local style = require("reactor-plc.panel.style") local core = require("graphics.core") local flasher = require("graphics.flasher") -local Div = require("graphics.elements.div") -local Rectangle = require("graphics.elements.rectangle") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local Rectangle = require("graphics.elements.Rectangle") +local TextBox = require("graphics.elements.TextBox") -local PushButton = require("graphics.elements.controls.push_button") +local PushButton = require("graphics.elements.controls.PushButton") -local LED = require("graphics.elements.indicators.led") -local LEDPair = require("graphics.elements.indicators.ledpair") -local RGBLED = require("graphics.elements.indicators.ledrgb") +local LED = require("graphics.elements.indicators.LED") +local LEDPair = require("graphics.elements.indicators.LEDPair") +local RGBLED = require("graphics.elements.indicators.RGBLED") local LINK_STATE = types.PANEL_LINK_STATE @@ -34,7 +34,7 @@ local ind_grn = style.ind_grn local ind_red = style.ind_red -- create new front panel view ----@param panel graphics_element main displaybox +---@param panel DisplayBox main displaybox local function init(panel) local s_hi_box = style.theme.highlight_box diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index eee2bc68..73a11261 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -110,22 +110,8 @@ end ---@param reactor table ---@param is_formed boolean function plc.rps_init(reactor, is_formed) - local state_keys = { - high_dmg = 1, - high_temp = 2, - low_coolant = 3, - ex_waste = 4, - ex_hcoolant = 5, - no_fuel = 6, - fault = 7, - timeout = 8, - manual = 9, - automatic = 10, - sys_fail = 11, - force_disabled = 12 - } - local self = { + ---@type boolean[] check states state = { false, false, false, false, false, false, false, false, false, false, false, false }, reactor_enabled = false, enabled_at = 0, @@ -136,12 +122,27 @@ function plc.rps_init(reactor, is_formed) trip_cause = "ok" ---@type rps_trip_cause } + local CHK = { + HIGH_DMG = 1, + HIGH_TEMP = 2, + LOW_COOLANT = 3, + EX_WASTE = 4, + EX_HCOOLANT = 5, + NO_FUEL = 6, + FAULT = 7, + TIMEOUT = 8, + MANUAL = 9, + AUTOMATIC = 10, + SYS_FAIL = 11, + FORCE_DISABLED = 12 + } + -- PRIVATE FUNCTIONS -- -- set reactor access fault flag local function _set_fault() if reactor.__p_last_fault() ~= "Terminated" then - self.state[state_keys.fault] = true + self.state[CHK.FAULT] = true end end @@ -203,8 +204,8 @@ function plc.rps_init(reactor, is_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 + if not self.state[CHK.SYS_FAIL] then + self.state[CHK.SYS_FAIL] = not self.formed end end @@ -214,8 +215,8 @@ function plc.rps_init(reactor, is_formed) if _check_and_handle_ppm_call(disabled) then self.force_disabled = disabled - if not self.state[state_keys.force_disabled] then - self.state[state_keys.force_disabled] = disabled + if not self.state[CHK.FORCE_DISABLED] then + self.state[CHK.FORCE_DISABLED] = disabled end end end @@ -223,8 +224,8 @@ function plc.rps_init(reactor, is_formed) -- check for high damage local function _high_damage() local damage_percent = reactor.getDamagePercent() - 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 + if _check_and_handle_ppm_call(damage_percent) and not self.state[CHK.HIGH_DMG] then + self.state[CHK.HIGH_DMG] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT end end @@ -232,40 +233,40 @@ function plc.rps_init(reactor, is_formed) local function _high_temp() -- mekanism: MAX_DAMAGE_TEMPERATURE = 1200K local temp = reactor.getTemperature() - 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 + if _check_and_handle_ppm_call(temp) and not self.state[CHK.HIGH_TEMP] then + self.state[CHK.HIGH_TEMP] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE end end -- check if there is very low coolant local function _low_coolant() local coolant_filled = reactor.getCoolantFilledPercentage() - 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 + if _check_and_handle_ppm_call(coolant_filled) and not self.state[CHK.LOW_COOLANT] then + self.state[CHK.LOW_COOLANT] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL end end -- check for excess waste (>80% filled) local function _excess_waste() local w_filled = reactor.getWasteFilledPercentage() - 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 + if _check_and_handle_ppm_call(w_filled) and not self.state[CHK.EX_WASTE] then + self.state[CHK.EX_WASTE] = w_filled > RPS_LIMITS.MAX_WASTE_FILL end end -- check for heated coolant backup (>95% filled) local function _excess_heated_coolant() local hc_filled = reactor.getHeatedCoolantFilledPercentage() - 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 + if _check_and_handle_ppm_call(hc_filled) and not self.state[CHK.EX_HCOOLANT] then + self.state[CHK.EX_HCOOLANT] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL end end -- check if there is no fuel local function _insufficient_fuel() local fuel = reactor.getFuelFilledPercentage() - 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 + if _check_and_handle_ppm_call(fuel) and not self.state[CHK.NO_FUEL] then + self.state[CHK.NO_FUEL] = fuel <= RPS_LIMITS.NO_FUEL_FILL end end @@ -287,23 +288,23 @@ function plc.rps_init(reactor, is_formed) -- trip for a PLC comms timeout function public.trip_timeout() - self.state[state_keys.timeout] = true + self.state[CHK.TIMEOUT] = true end -- manually SCRAM the reactor function public.trip_manual() - self.state[state_keys.manual] = true + self.state[CHK.MANUAL] = true end -- automatic SCRAM commanded by supervisor function public.trip_auto() - self.state[state_keys.automatic] = true + self.state[CHK.AUTOMATIC] = true end -- trip for unformed reactor function public.trip_sys_fail() - self.state[state_keys.fault] = true - self.state[state_keys.sys_fail] = true + self.state[CHK.FAULT] = true + self.state[CHK.SYS_FAIL] = true end -- SCRAM the reactor now
@@ -350,7 +351,7 @@ function plc.rps_init(reactor, is_formed) function public.auto_activate() -- clear automatic SCRAM if it was the cause if self.tripped and self.trip_cause == "automatic" then - self.state[state_keys.automatic] = true + self.state[CHK.AUTOMATIC] = true self.trip_cause = RPS_TRIP_CAUSE.OK self.tripped = false @@ -388,40 +389,40 @@ function plc.rps_init(reactor, is_formed) -- check system states in order of severity if self.tripped then status = self.trip_cause - elseif self.state[state_keys.sys_fail] then + elseif self.state[CHK.SYS_FAIL] then log.warning("RPS: system failure, reactor not formed") status = RPS_TRIP_CAUSE.SYS_FAIL - elseif self.state[state_keys.force_disabled] then + elseif self.state[CHK.FORCE_DISABLED] then log.warning("RPS: reactor was force disabled") status = RPS_TRIP_CAUSE.FORCE_DISABLED - elseif self.state[state_keys.high_dmg] then + elseif self.state[CHK.HIGH_DMG] then log.warning("RPS: high damage") status = RPS_TRIP_CAUSE.HIGH_DMG - elseif self.state[state_keys.high_temp] then + elseif self.state[CHK.HIGH_TEMP] then log.warning("RPS: high temperature") status = RPS_TRIP_CAUSE.HIGH_TEMP - elseif self.state[state_keys.low_coolant] then + elseif self.state[CHK.LOW_COOLANT] then log.warning("RPS: low coolant") status = RPS_TRIP_CAUSE.LOW_COOLANT - elseif self.state[state_keys.ex_waste] then + elseif self.state[CHK.EX_WASTE] then log.warning("RPS: full waste") status = RPS_TRIP_CAUSE.EX_WASTE - elseif self.state[state_keys.ex_hcoolant] then + elseif self.state[CHK.EX_HCOOLANT] then log.warning("RPS: heated coolant backup") status = RPS_TRIP_CAUSE.EX_HCOOLANT - elseif self.state[state_keys.no_fuel] then + elseif self.state[CHK.NO_FUEL] then log.warning("RPS: no fuel") status = RPS_TRIP_CAUSE.NO_FUEL - elseif self.state[state_keys.fault] then + elseif self.state[CHK.FAULT] then log.warning("RPS: reactor access fault") status = RPS_TRIP_CAUSE.FAULT - elseif self.state[state_keys.timeout] then + elseif self.state[CHK.TIMEOUT] then log.warning("RPS: supervisor connection timeout") status = RPS_TRIP_CAUSE.TIMEOUT - elseif self.state[state_keys.manual] then + elseif self.state[CHK.MANUAL] then log.warning("RPS: manual SCRAM requested") status = RPS_TRIP_CAUSE.MANUAL - elseif self.state[state_keys.automatic] then + elseif self.state[CHK.AUTOMATIC] then log.warning("RPS: automatic SCRAM requested") status = RPS_TRIP_CAUSE.AUTOMATIC else @@ -449,7 +450,7 @@ function plc.rps_init(reactor, is_formed) end -- update emergency coolant control if configured - _set_emer_cool(self.state[state_keys.low_coolant]) + _set_emer_cool(self.state[CHK.LOW_COOLANT]) -- report RPS status databus.tx_rps(self.tripped, self.state, self.emer_cool_active) @@ -465,7 +466,7 @@ function plc.rps_init(reactor, is_formed) ---@nodiscard function public.get_trip_cause() return self.trip_cause end ---@nodiscard - function public.is_low_coolant() return self.states[state_keys.low_coolant] end + function public.is_low_coolant() return self.states[CHK.LOW_COOLANT] end ---@nodiscard function public.is_active() return self.reactor_enabled end @@ -495,16 +496,16 @@ function plc.rps_init(reactor, is_formed) self.tripped = false self.trip_cause = RPS_TRIP_CAUSE.OK - self.state[state_keys.fault] = false - self.state[state_keys.sys_fail] = false + self.state[CHK.FAULT] = false + self.state[CHK.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 - self.state[state_keys.timeout] = false + self.state[CHK.AUTOMATIC] = false + self.state[CHK.TIMEOUT] = false if self.trip_cause == RPS_TRIP_CAUSE.AUTOMATIC or self.trip_cause == RPS_TRIP_CAUSE.TIMEOUT then self.trip_cause = RPS_TRIP_CAUSE.OK diff --git a/reactor-plc/renderer.lua b/reactor-plc/renderer.lua index aa62e631..f8a80443 100644 --- a/reactor-plc/renderer.lua +++ b/reactor-plc/renderer.lua @@ -8,7 +8,7 @@ local style = require("reactor-plc.panel.style") local core = require("graphics.core") local flasher = require("graphics.flasher") -local DisplayBox = require("graphics.elements.displaybox") +local DisplayBox = require("graphics.elements.DisplayBox") ---@class reactor_plc_renderer local renderer = {} diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index cb40ead3..67dc290b 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.8.8" +local R_PLC_VERSION = "v1.8.12" local println = util.println local println_ts = util.println_ts diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index b3a02516..b56ccc72 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -144,6 +144,7 @@ function threads.thread__main(smem, init) plc_state.no_reactor = true plc_state.degraded = true elseif networked and type == "modem" then + ---@cast device Modem -- we only care if this is our wireless modem -- note, check init_ok first since nic will be nil if it is false if plc_state.init_ok and nic.is_modem(device) then @@ -208,6 +209,7 @@ function threads.thread__main(smem, init) rps.reset_formed() end elseif networked and type == "modem" then + ---@cast device Modem -- note, check init_ok first since nic will be nil if it is false if device.isWireless() and not (plc_state.init_ok and nic.is_connected()) then -- reconnected modem @@ -628,9 +630,10 @@ function threads.thread__setpoint_control(smem) local reactor = plc_dev.reactor if plc_state.init_ok and (not plc_state.no_reactor) then + ---@cast reactor table won't be nil + -- check if we should start ramping if setpoints.burn_rate_en and (setpoints.burn_rate ~= last_burn_sp) then ----@diagnostic disable-next-line: need-check-nil local cur_burn_rate = reactor.getBurnRate() if (type(cur_burn_rate) == "number") and (setpoints.burn_rate ~= cur_burn_rate) and rps.is_active() then @@ -644,7 +647,6 @@ function threads.thread__setpoint_control(smem) log.debug(util.c("SPCTL: starting burn rate ramp from ", cur_burn_rate, " mB/t to ", setpoints.burn_rate, " mB/t")) else log.debug(util.c("SPCTL: setting burn rate directly to ", setpoints.burn_rate, " mB/t")) ----@diagnostic disable-next-line: need-check-nil reactor.setBurnRate(setpoints.burn_rate) end end @@ -658,7 +660,6 @@ function threads.thread__setpoint_control(smem) -- adjust burn rate (setpoints.burn_rate) if setpoints.burn_rate_en then if rps.is_active() then ----@diagnostic disable-next-line: need-check-nil local current_burn_rate = reactor.getBurnRate() -- we yielded, check enable again @@ -679,7 +680,6 @@ function threads.thread__setpoint_control(smem) running = running or (new_burn_rate ~= setpoints.burn_rate) -- set the burn rate ----@diagnostic disable-next-line: need-check-nil reactor.setBurnRate(new_burn_rate) end else diff --git a/rtu/config/peripherals.lua b/rtu/config/peripherals.lua new file mode 100644 index 00000000..51c4b567 --- /dev/null +++ b/rtu/config/peripherals.lua @@ -0,0 +1,434 @@ +local ppm = require("scada-common.ppm") +local util = require("scada-common.util") + +local core = require("graphics.core") + +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") + +local PushButton = require("graphics.elements.controls.PushButton") +local Radio2D = require("graphics.elements.controls.Radio2D") +local RadioButton = require("graphics.elements.controls.RadioButton") + +local NumberField = require("graphics.elements.form.NumberField") +local TextField = require("graphics.elements.form.TextField") + +---@class rtu_peri_definition +---@field unit integer|nil +---@field index integer|nil +---@field name string + +local tri = util.trinary + +local cpair = core.cpair + +local LEFT = core.ALIGN.LEFT + +local self = { + peri_cfg_editing = false, ---@type integer|false + + p_assign = nil, ---@type function + + ppm_devs = nil, ---@type ListBox + p_name_msg = nil, ---@type TextBox + p_prompt = nil, ---@type TextBox + p_idx = nil, ---@type NumberField + p_unit = nil, ---@type NumberField + p_desc = nil, ---@type TextBox + p_desc_ext = nil, ---@type TextBox + p_err = nil ---@type TextBox +} + +local peripherals = {} + +local RTU_DEV_TYPES = { "boilerValve", "turbineValve", "dynamicValve", "inductionPort", "spsPort", "solarNeutronActivator", "environmentDetector" } +local NEEDS_UNIT = { "boilerValve", "turbineValve", "dynamicValve", "solarNeutronActivator", "environmentDetector" } + +-- create the peripherals configuration view +---@param tool_ctl _rtu_cfg_tool_ctl +---@param main_pane MultiPane +---@param cfg_sys [ rtu_config, rtu_config, rtu_config, table, function ] +---@param peri_cfg Div +---@param style { [string]: cpair } +---@return MultiPane peri_pane, string[] NEEDS_UNIT +function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style) + local settings_cfg, ini_cfg, tmp_cfg, _, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5] + + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + + --#region Peripherals + + local peri_c_1 = Div{parent=peri_cfg,x=2,y=4,width=49} + local peri_c_2 = Div{parent=peri_cfg,x=2,y=4,width=49} + local peri_c_3 = Div{parent=peri_cfg,x=2,y=4,width=49} + local peri_c_4 = Div{parent=peri_cfg,x=2,y=4,width=49} + local peri_c_5 = Div{parent=peri_cfg,x=2,y=4,width=49} + local peri_c_6 = Div{parent=peri_cfg,x=2,y=4,width=49} + local peri_c_7 = Div{parent=peri_cfg,x=2,y=4,width=49} + + local peri_pane = MultiPane{parent=peri_cfg,x=1,y=4,panes={peri_c_1,peri_c_2,peri_c_3,peri_c_4,peri_c_5,peri_c_6,peri_c_7}} + + TextBox{parent=peri_cfg,x=1,y=2,text=" Peripheral Connections",fg_bg=cpair(colors.black,colors.purple)} + + local peri_list = ListBox{parent=peri_c_1,x=1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local function peri_revert() + tmp_cfg.Peripherals = tool_ctl.deep_copy_peri(ini_cfg.Peripherals) + tool_ctl.gen_peri_summary() + end + + local function peri_apply() + settings.set("Peripherals", tmp_cfg.Peripherals) + + if settings.save("/rtu.settings") then + load_settings(settings_cfg, true) + load_settings(ini_cfg) + peri_pane.set_value(5) + else + peri_pane.set_value(6) + end + end + + PushButton{parent=peri_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + local peri_revert_btn = PushButton{parent=peri_c_1,x=8,y=14,min_width=16,text="Revert Changes",callback=peri_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + PushButton{parent=peri_c_1,x=35,y=14,min_width=7,text="Add +",callback=function()peri_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} + local peri_apply_btn = PushButton{parent=peri_c_1,x=43,y=14,min_width=7,text="Apply",callback=peri_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + + TextBox{parent=peri_c_2,x=1,y=1,text="Select one of the below devices to use."} + + self.ppm_devs = ListBox{parent=peri_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + PushButton{parent=peri_c_2,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=peri_c_2,x=8,y=14,min_width=10,text="Manual +",callback=function()peri_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.orange),active_fg_bg=btn_act_fg_bg} + PushButton{parent=peri_c_2,x=26,y=14,min_width=24,text="I don't see my device!",callback=function()peri_pane.set_value(7)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg} + + TextBox{parent=peri_c_7,x=1,y=1,height=10,text="Make sure your device is either touching the RTU or connected via wired modems. There should be a wired modem on a side of the RTU then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."} + TextBox{parent=peri_c_7,x=1,y=9,height=4,text="If it still does not show, it may not be compatible. Currently only Boilers, Turbines, Dynamic Tanks, SNAs, SPSs, Induction Matricies, and Environment Detectors are supported."} + PushButton{parent=peri_c_7,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + local new_peri_attrs = { "", "" } + local function new_peri(name, type) + new_peri_attrs = { name, type } + self.peri_cfg_editing = false + + self.p_err.hide(true) + self.p_name_msg.set_value("Configuring peripheral on '" .. name .. "':") + self.p_desc_ext.set_value("") + + local function reposition(prompt, idx_x, idx_max, unit_x, unit_y, desc_y) + self.p_prompt.set_value(prompt) + self.p_idx.reposition(idx_x, 4) + self.p_idx.enable() + self.p_idx.set_max(idx_max) + self.p_idx.show() + self.p_unit.reposition(unit_x, unit_y) + self.p_unit.enable() + self.p_unit.show() + self.p_desc.reposition(1, desc_y) + end + + if type == "boilerValve" then + reposition("This is reactor unit # 's # boiler.", 31, 2, 23, 4, 7) + self.p_assign_btn.hide(true) + self.p_desc.set_value("Each unit can have at most 2 boilers. Boiler #1 shows up first on the main display, followed by boiler #2 below it. The numberings are per unit (unit 1 and unit 2 would both have a boiler #1 if each had one boiler) and can be split amongst multiple RTUs (one has #1, another has #2).") + elseif type == "turbineValve" then + reposition("This is reactor unit # 's # turbine.", 31, 3, 23, 4, 7) + self.p_assign_btn.hide(true) + self.p_desc.set_value("Each unit can have at most 3 turbines. Turbine #1 shows up first on the main display, followed by #2 then #3 below it. The numberings are per unit (unit 1 and unit 2 would both have a turbine #1) and can be split amongst multiple RTUs (one has #1, another has #2).") + elseif type == "solarNeutronActivator" then + reposition("This SNA is for reactor unit # .", 46, 1, 31, 4, 7) + self.p_idx.hide() + self.p_assign_btn.hide(true) + self.p_desc_ext.set_value("Before adding lots of SNAs: multiply the \"PEAK\" rate on the flow monitor (after connecting at least 1 SNA) by 10 to get the mB/t of waste that they can process. Enough SNAs to provide 2x to 3x of your max burn rate should be a good margin to catch up after night or cloudy weather. Too many devices (such as SNAs) on one RTU can cause lag.") + elseif type == "dynamicValve" then + reposition("This is the below system's # dynamic tank.", 29, 4, 17, 6, 8) + self.p_assign_btn.show() + self.p_assign_btn.redraw() + + if self.p_assign_btn.get_value() == 1 then + self.p_idx.enable() + self.p_unit.disable() + else + self.p_idx.set_value(1) + self.p_idx.disable() + self.p_unit.enable() + end + + self.p_desc.set_value("Each reactor unit can have at most 1 tank and the facility can have at most 4. Each facility tank must have a unique # 1 through 4, regardless of where it is connected. Only a total of 4 tanks can be displayed on the flow monitor.") + elseif type == "environmentDetector" then + reposition("This is the below system's # env. detector.", 29, 99, 17, 6, 8) + self.p_assign_btn.show() + self.p_assign_btn.redraw() + if self.p_assign_btn.get_value() == 1 then self.p_unit.disable() else self.p_unit.enable() end + self.p_desc.set_value("You can connect more than one environment detector for a particular unit or the facility. In that case, the maximum radiation reading from those assigned to that particular unit or the facility will be used for alarms and display.") + elseif type == "inductionPort" or type == "spsPort" then + local dev = tri(type == "inductionPort", "induction matrix", "SPS") + self.p_idx.hide(true) + self.p_unit.hide(true) + self.p_prompt.set_value("This is the " .. dev .. " for the facility.") + self.p_assign_btn.hide(true) + self.p_desc.reposition(1, 7) + self.p_desc.set_value("There can only be one of these devices per SCADA network, so it will be assigned as the sole " .. dev .. " for the facility. There must only be one of these across all the RTUs you have.") + else + assert(false, "invalid peripheral type after type validation") + end + + peri_pane.set_value(4) + end + + -- update peripherals list + function tool_ctl.update_peri_list() + local alternate = true + local mounts = ppm.list_mounts() + + -- filter out in-use peripherals + for _, v in ipairs(tmp_cfg.Peripherals) do mounts[v.name] = nil end + + self.ppm_devs.remove_all() + for name, entry in pairs(mounts) do + if util.table_contains(RTU_DEV_TYPES, entry.type) then + local bkg = tri(alternate, colors.white, colors.lightGray) + + ---@cast entry ppm_entry + local line = Div{parent=self.ppm_devs,height=2,fg_bg=cpair(colors.black,bkg)} + PushButton{parent=line,x=1,y=1,min_width=9,alignment=LEFT,height=1,text="> SELECT",callback=function()new_peri(name,entry.type)end,fg_bg=cpair(colors.black,colors.purple),active_fg_bg=cpair(colors.white,colors.black)} + TextBox{parent=line,x=11,y=1,text=name,fg_bg=cpair(colors.black,bkg)} + TextBox{parent=line,x=11,y=2,text=entry.type,fg_bg=cpair(colors.gray,bkg)} + + alternate = not alternate + end + end + end + + tool_ctl.update_peri_list() + + TextBox{parent=peri_c_3,x=1,y=1,height=4,text="This feature is intended for advanced users. If you are clicking this just because your device is not shown, follow the connection instructions in 'I don't see my device!'."} + TextBox{parent=peri_c_3,x=1,y=6,height=4,text="Peripheral Name"} + local p_name = TextField{parent=peri_c_3,x=1,y=7,width=49,height=1,max_len=128,fg_bg=bw_fg_bg} + local p_type = Radio2D{parent=peri_c_3,x=1,y=9,rows=4,columns=2,default=1,options=RTU_DEV_TYPES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple} + local man_p_err = TextBox{parent=peri_c_3,x=8,y=14,width=35,text="Please enter a peripheral name.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + man_p_err.hide(true) + + local function submit_manual_peri() + local name = p_name.get_value() + if string.len(name) > 0 then + tool_ctl.entering_manual = true + man_p_err.hide(true) + new_peri(name, RTU_DEV_TYPES[p_type.get_value()]) + else man_p_err.show() end + end + + PushButton{parent=peri_c_3,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=peri_c_3,x=44,y=14,text="Next \x1a",callback=submit_manual_peri,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + self.p_name_msg = TextBox{parent=peri_c_4,x=1,y=1,height=2,text=""} + self.p_prompt = TextBox{parent=peri_c_4,x=1,y=4,height=2,text=""} + self.p_idx = NumberField{parent=peri_c_4,x=31,y=4,width=4,max_chars=2,min=1,max=2,default=1,fg_bg=bw_fg_bg,dis_fg_bg=btn_dis_fg_bg} + self.p_assign_btn = RadioButton{parent=peri_c_4,x=1,y=5,default=1,options={"the facility","reactor unit #"},callback=function(v)self.p_assign(v)end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple} + + self.p_unit = NumberField{parent=peri_c_4,x=23,y=4,width=4,max_chars=2,min=1,max=4,default=1,fg_bg=bw_fg_bg,dis_fg_bg=btn_dis_fg_bg} + self.p_unit.disable() + + function self.p_assign(opt) + if opt == 1 then + self.p_unit.disable() + if new_peri_attrs[2] == "dynamicValve" then self.p_idx.enable() end + else + self.p_unit.enable() + if new_peri_attrs[2] == "dynamicValve" then + self.p_idx.set_value(1) + self.p_idx.disable() + end + end + end + + self.p_desc = TextBox{parent=peri_c_4,x=1,y=7,height=6,text="",fg_bg=g_lg_fg_bg} + self.p_desc_ext = TextBox{parent=peri_c_4,x=1,y=6,height=7,text="",fg_bg=g_lg_fg_bg} + + self.p_err = TextBox{parent=peri_c_4,x=8,y=14,width=32,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + self.p_err.hide(true) + + local function back_from_peri_opts() + if self.peri_cfg_editing ~= false then + peri_pane.set_value(1) + elseif tool_ctl.entering_manual then + peri_pane.set_value(3) + else + peri_pane.set_value(2) + end + + tool_ctl.entering_manual = false + end + + local function save_peri_entry() + local peri_name = new_peri_attrs[1] + local peri_type = new_peri_attrs[2] + + local unit, index = nil, nil + + local for_facility = self.p_assign_btn.get_value() == 1 + local u = tonumber(self.p_unit.get_value()) + local idx = tonumber(self.p_idx.get_value()) + + if util.table_contains(NEEDS_UNIT, peri_type) then + if (peri_type == "dynamicValve" or peri_type == "environmentDetector") and for_facility then + -- skip + elseif not (util.is_int(u) and u > 0 and u < 5) then + self.p_err.set_value("Unit ID must be within 1 to 4.") + self.p_err.show() + return + else unit = u end + end + + if peri_type == "boilerValve" then + if not (idx == 1 or idx == 2) then + self.p_err.set_value("Index must be 1 or 2.") + self.p_err.show() + return + else index = idx end + elseif peri_type == "turbineValve" then + if not (idx == 1 or idx == 2 or idx == 3) then + self.p_err.set_value("Index must be 1, 2, or 3.") + self.p_err.show() + return + else index = idx end + elseif peri_type == "dynamicValve" and for_facility then + if not (util.is_int(idx) and idx > 0 and idx < 5) then + self.p_err.set_value("Index must be within 1 to 4.") + self.p_err.show() + return + else index = idx end + elseif peri_type == "dynamicValve" then + index = 1 + elseif peri_type == "environmentDetector" then + if not (util.is_int(idx) and idx > 0) then + self.p_err.set_value("Index must be greater than 0.") + self.p_err.show() + return + else index = idx end + end + + self.p_err.hide(true) + + ---@type rtu_peri_definition + local def = { name = peri_name, unit = unit, index = index } + + if self.peri_cfg_editing == false then + table.insert(tmp_cfg.Peripherals, def) + else + def.name = tmp_cfg.Peripherals[self.peri_cfg_editing].name + tmp_cfg.Peripherals[self.peri_cfg_editing] = def + end + + peri_pane.set_value(1) + tool_ctl.gen_peri_summary() + tool_ctl.update_peri_list() + + self.p_idx.set_value(1) + end + + PushButton{parent=peri_c_4,x=1,y=14,text="\x1b Back",callback=back_from_peri_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=peri_c_4,x=41,y=14,min_width=9,text="Confirm",callback=save_peri_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} + + TextBox{parent=peri_c_5,x=1,y=1,text="Settings saved!"} + PushButton{parent=peri_c_5,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=peri_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=peri_c_6,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} + PushButton{parent=peri_c_6,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=peri_c_6,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Tool Functions + + ---@param def rtu_peri_definition + ---@param idx integer + ---@param type string + local function edit_peri_entry(idx, def, type) + -- set inputs BEFORE calling new_peri() + if def.index ~= nil then self.p_idx.set_value(def.index) end + if def.unit == nil then + self.p_assign_btn.set_value(1) + else + self.p_unit.set_value(def.unit) + self.p_assign_btn.set_value(2) + end + + new_peri(def.name, type) + + -- set editing mode AFTER new_peri() + self.peri_cfg_editing = idx + end + + local function delete_peri_entry(idx) + table.remove(tmp_cfg.Peripherals, idx) + tool_ctl.gen_peri_summary() + tool_ctl.update_peri_list() + end + + -- generate the peripherals summary list + function tool_ctl.gen_peri_summary() + peri_list.remove_all() + + local modified = #ini_cfg.Peripherals ~= #tmp_cfg.Peripherals + + for i = 1, #tmp_cfg.Peripherals do + local def = tmp_cfg.Peripherals[i] + + local t = ppm.get_type(def.name) + local t_str = " (connect to edit)" + local disconnected = t == nil + + if not disconnected then t_str = "[" .. t .. "]" end + + local desc = " \x1a " + + if type(def.index) == "number" then + desc = desc .. "#" .. def.index .. " " + end + + if type(def.unit) == "number" then + desc = desc .. "for unit " .. def.unit + else + desc = desc .. "for the facility" + end + + local entry = Div{parent=peri_list,height=3} + TextBox{parent=entry,x=1,y=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)} + TextBox{parent=entry,x=1,y=2,text=" \x1a "..t_str,fg_bg=cpair(colors.gray,colors.white)} + TextBox{parent=entry,x=1,y=3,text=desc,fg_bg=cpair(colors.gray,colors.white)} + local edit_btn = PushButton{parent=entry,x=41,y=2,min_width=8,height=1,text="EDIT",callback=function()edit_peri_entry(i,def,t or "")end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + PushButton{parent=entry,x=41,y=3,min_width=8,height=1,text="DELETE",callback=function()delete_peri_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} + + if disconnected then edit_btn.disable() end + + if not modified then + local a = ini_cfg.Peripherals[i] + local b = tmp_cfg.Peripherals[i] + + modified = (a.unit ~= b.unit) or (a.index ~= b.index) or (a.name ~= b.name) + end + end + + if modified then + peri_revert_btn.enable() + peri_apply_btn.enable() + else + peri_revert_btn.disable() + peri_apply_btn.disable() + end + end + + --#endregion + + return peri_pane, NEEDS_UNIT +end + +return peripherals diff --git a/rtu/config/redstone.lua b/rtu/config/redstone.lua new file mode 100644 index 00000000..d21dcb7f --- /dev/null +++ b/rtu/config/redstone.lua @@ -0,0 +1,426 @@ +local constants = require("scada-common.constants") +local rsio = require("scada-common.rsio") +local util = require("scada-common.util") + +local core = require("graphics.core") + +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") + +local Checkbox = require("graphics.elements.controls.Checkbox") +local PushButton = require("graphics.elements.controls.PushButton") +local Radio2D = require("graphics.elements.controls.Radio2D") + +local NumberField = require("graphics.elements.form.NumberField") + +---@class rtu_rs_definition +---@field unit integer|nil +---@field port IO_PORT +---@field side side +---@field color color|nil + +local tri = util.trinary + +local cpair = core.cpair + +local IO = rsio.IO +local IO_LVL = rsio.IO_LVL +local IO_MODE = rsio.IO_MODE + +local LEFT = core.ALIGN.LEFT + +local self = { + rs_cfg_port = 1, ---@type IO_PORT + rs_cfg_editing = false, ---@type integer|false + + rs_cfg_selection = nil, ---@type TextBox + rs_cfg_unit_l = nil, ---@type TextBox + rs_cfg_unit = nil, ---@type NumberField + rs_cfg_side_l = nil, ---@type TextBox + rs_cfg_color = nil, ---@type Radio2D + rs_cfg_shortcut = nil ---@type TextBox +} + +-- rsio port descriptions +local PORT_DESC_MAP = { + { IO.F_SCRAM, "Facility SCRAM" }, + { IO.F_ACK, "Facility Acknowledge" }, + { IO.R_SCRAM, "Reactor SCRAM" }, + { IO.R_RESET, "Reactor RPS Reset" }, + { IO.R_ENABLE, "Reactor Enable" }, + { IO.U_ACK, "Unit Acknowledge" }, + { IO.F_ALARM, "Facility Alarm (high prio)" }, + { IO.F_ALARM_ANY, "Facility Alarm (any)" }, + { IO.F_MATRIX_LOW, "Induction Matrix < " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) .. "%" }, + { IO.F_MATRIX_HIGH, "Induction Matrix > " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) .. "%" }, + { IO.F_MATRIX_CHG, "Induction Matrix Charge %" }, + { IO.WASTE_PU, "Waste Plutonium Valve" }, + { IO.WASTE_PO, "Waste Polonium Valve" }, + { IO.WASTE_POPL, "Waste Po Pellets Valve" }, + { IO.WASTE_AM, "Waste Antimatter Valve" }, + { IO.R_ACTIVE, "Reactor Active" }, + { IO.R_AUTO_CTRL, "Reactor in Auto Control" }, + { IO.R_SCRAMMED, "RPS Tripped" }, + { IO.R_AUTO_SCRAM, "RPS Auto SCRAM" }, + { IO.R_HIGH_DMG, "RPS High Damage" }, + { IO.R_HIGH_TEMP, "RPS High Temperature" }, + { IO.R_LOW_COOLANT, "RPS Low Coolant" }, + { IO.R_EXCESS_HC, "RPS Excess Heated Coolant" }, + { IO.R_EXCESS_WS, "RPS Excess Waste" }, + { IO.R_INSUFF_FUEL, "RPS Insufficient Fuel" }, + { IO.R_PLC_FAULT, "RPS PLC Fault" }, + { IO.R_PLC_TIMEOUT, "RPS Supervisor Timeout" }, + { IO.U_ALARM, "Unit Alarm" }, + { IO.U_EMER_COOL, "Unit Emergency Cool. Valve" } +} + +-- designation (0 = facility, 1 = unit) +local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 } + +assert(#PORT_DESC_MAP == rsio.NUM_PORTS) +assert(#PORT_DSGN == rsio.NUM_PORTS) + +local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" } +local side_options_map = { "top", "bottom", "left", "right", "front", "back" } +local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" } +local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown } + +-- convert text representation to index +---@param side string +local function side_to_idx(side) + for k, v in ipairs(side_options_map) do + if v == side then return k end + end +end + +-- convert color to index +---@param color color +local function color_to_idx(color) + for k, v in ipairs(color_options_map) do + if v == color then return k end + end +end + +local redstone = {} + +-- create the redstone configuration view +---@param tool_ctl _rtu_cfg_tool_ctl +---@param main_pane MultiPane +---@param cfg_sys [ rtu_config, rtu_config, rtu_config, table, function ] +---@param rs_cfg Div +---@param style { [string]: cpair } +---@return MultiPane rs_pane +function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) + local settings_cfg, ini_cfg, tmp_cfg, _, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5] + + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + + --#region Redstone + + local rs_c_1 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_2 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_3 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49} + + local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7}} + + TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)} + + TextBox{parent=rs_c_1,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg} + local rs_list = ListBox{parent=rs_c_1,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local function rs_revert() + tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone) + tool_ctl.gen_rs_summary() + end + + local function rs_apply() + settings.set("Redstone", tmp_cfg.Redstone) + + if settings.save("/rtu.settings") then + load_settings(settings_cfg, true) + load_settings(ini_cfg) + rs_pane.set_value(4) + else + rs_pane.set_value(5) + end + end + + PushButton{parent=rs_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + local rs_revert_btn = PushButton{parent=rs_c_1,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + PushButton{parent=rs_c_1,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} + local rs_apply_btn = PushButton{parent=rs_c_1,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + + TextBox{parent=rs_c_6,x=1,y=1,height=5,text="You already configured this input. There can only be one entry for each input.\n\nPlease select a different port."} + PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=rs_c_2,x=1,y=1,text="Select one of the below ports to use."} + + local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local function new_rs(port) + if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then + for i = 1, #tmp_cfg.Redstone do + if tmp_cfg.Redstone[i].port == port then + rs_pane.set_value(6) + return + end + end + end + + self.rs_cfg_editing = false + + local text + + if port == -1 then + self.rs_cfg_color.hide(true) + self.rs_cfg_shortcut.show() + self.rs_cfg_side_l.set_value("Output Side") + text = "You selected the ALL_WASTE shortcut." + else + self.rs_cfg_shortcut.hide(true) + self.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(port) == rsio.IO_DIR.IN, "Input Side", "Output Side")) + self.rs_cfg_color.show() + + local io_type = "analog input " + local io_mode = rsio.get_io_mode(port) + local inv = tri(rsio.digital_is_active(port, IO_LVL.LOW) == true, "inverted ", "") + + if io_mode == IO_MODE.DIGITAL_IN then + io_type = inv .. "digital input " + elseif io_mode == IO_MODE.DIGITAL_OUT then + io_type = inv .. "digital output " + elseif io_mode == IO_MODE.ANALOG_OUT then + io_type = "analog output " + end + + text = "You selected the " .. io_type .. rsio.to_string(port) .. " (for " + + if PORT_DSGN[port] == 1 then + text = text .. "a unit)." + self.rs_cfg_unit_l.show() + self.rs_cfg_unit.show() + else + self.rs_cfg_unit_l.hide(true) + self.rs_cfg_unit.hide(true) + text = text .. "the facility)." + end + end + + self.rs_cfg_selection.set_value(text) + self.rs_cfg_port = port + rs_pane.set_value(3) + end + + -- add entries to redstone option list + local all_w_macro = Div{parent=rs_ports,height=1} + PushButton{parent=all_w_macro,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">ALL_WASTE",callback=function()new_rs(-1)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.black)} + TextBox{parent=all_w_macro,x=16,y=1,width=5,text="[n/a]",fg_bg=cpair(colors.lightGray,colors.white)} + TextBox{parent=all_w_macro,x=22,y=1,text="Create all 4 waste entries",fg_bg=cpair(colors.gray,colors.white)} + + for i = 1, rsio.NUM_PORTS do + local p = PORT_DESC_MAP[i][1] + local name = rsio.to_string(p) + local io_dir = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, "[in]", "[out]") + local btn_color = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue) + + local entry = Div{parent=rs_ports,height=1} + PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(p)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)} + TextBox{parent=entry,x=16,y=1,width=5,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} + TextBox{parent=entry,x=22,y=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)} + end + + PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + self.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=2,text=""} + + PushButton{parent=rs_c_3,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=rs_c_7,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"} + TextBox{parent=rs_c_7,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"} + TextBox{parent=rs_c_7,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"} + PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + self.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=4,width=11,text="Output Side"} + local side = Radio2D{parent=rs_c_3,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red} + + self.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=25,y=7,width=7,text="Unit ID"} + self.rs_cfg_unit = NumberField{parent=rs_c_3,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg} + + local function set_bundled(bundled) + if bundled then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end + end + + self.rs_cfg_shortcut = TextBox{parent=rs_c_3,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."} + self.rs_cfg_shortcut.hide(true) + + local bundled = Checkbox{parent=rs_c_3,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled} + self.rs_cfg_color = Radio2D{parent=rs_c_3,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg} + self.rs_cfg_color.disable() + + local rs_err = TextBox{parent=rs_c_3,x=8,y=14,width=30,text="Unit ID must be within 1 to 4.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + rs_err.hide(true) + + local function back_from_rs_opts() + rs_err.hide(true) + if self.rs_cfg_editing ~= false then rs_pane.set_value(1) else rs_pane.set_value(2) end + end + + local function save_rs_entry() + local port = self.rs_cfg_port + local u = tonumber(self.rs_cfg_unit.get_value()) + + if PORT_DSGN[port] == 0 or (util.is_int(u) and u > 0 and u < 5) then + rs_err.hide(true) + + if port >= 0 then + ---@type rtu_rs_definition + local def = { + unit = tri(PORT_DSGN[port] == 1, u, nil), + port = port, + side = side_options_map[side.get_value()], + color = tri(bundled.get_value(), color_options_map[self.rs_cfg_color.get_value()], nil) + } + + if self.rs_cfg_editing == false then + table.insert(tmp_cfg.Redstone, def) + else + def.port = tmp_cfg.Redstone[self.rs_cfg_editing].port + tmp_cfg.Redstone[self.rs_cfg_editing] = def + end + elseif port == -1 then + local default_sides = { "left", "back", "right", "front" } + local default_colors = { colors.red, colors.orange, colors.yellow, colors.lime } + for i = 0, 3 do + table.insert(tmp_cfg.Redstone, { + unit = tri(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil), + port = IO.WASTE_PU + i, + side = tri(bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]), + color = tri(bundled.get_value(), default_colors[i + 1], nil) + }) + end + end + + rs_pane.set_value(1) + tool_ctl.gen_rs_summary() + + side.set_value(1) + bundled.set_value(false) + self.rs_cfg_color.set_value(1) + self.rs_cfg_color.disable() + else rs_err.show() end + end + + PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=rs_c_3,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} + + TextBox{parent=rs_c_4,x=1,y=1,text="Settings saved!"} + PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=rs_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=rs_c_5,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} + PushButton{parent=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=rs_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Tool Functions + + local function edit_rs_entry(idx) + local def = tmp_cfg.Redstone[idx] + + self.rs_cfg_shortcut.hide(true) + self.rs_cfg_color.show() + + self.rs_cfg_port = def.port + self.rs_cfg_editing = idx + + local text = "Editing " .. rsio.to_string(def.port) .. " (for " + if PORT_DSGN[def.port] == 1 then + text = text .. "a unit)." + self.rs_cfg_unit_l.show() + self.rs_cfg_unit.show() + self.rs_cfg_unit.set_value(def.unit or 1) + else + self.rs_cfg_unit_l.hide(true) + self.rs_cfg_unit.hide(true) + text = text .. "the facility)." + end + + local value = 1 + if def.color ~= nil then + value = color_to_idx(def.color) + self.rs_cfg_color.enable() + else + self.rs_cfg_color.disable() + end + + self.rs_cfg_selection.set_value(text) + self.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "Input Side", "Output Side")) + side.set_value(side_to_idx(def.side)) + bundled.set_value(def.color ~= nil) + self.rs_cfg_color.set_value(value) + rs_pane.set_value(3) + end + + local function delete_rs_entry(idx) + table.remove(tmp_cfg.Redstone, idx) + tool_ctl.gen_rs_summary() + end + + -- generate the redstone summary list + function tool_ctl.gen_rs_summary() + rs_list.remove_all() + + local modified = #ini_cfg.Redstone ~= #tmp_cfg.Redstone + + for i = 1, #tmp_cfg.Redstone do + local def = tmp_cfg.Redstone[i] + + local name = rsio.to_string(def.port) + local io_dir = tri(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") + local conn = def.side + local unit = util.strval(def.unit or "F") + + if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end + + local entry = Div{parent=rs_list,height=1} + TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} + TextBox{parent=entry,x=2,y=1,width=14,text=name} + TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)} + TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)} + PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} + PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} + + if not modified then + local a = ini_cfg.Redstone[i] + local b = tmp_cfg.Redstone[i] + + modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.side ~= b.side) or (a.color ~= b.color) + end + end + + if modified then + rs_revert_btn.enable() + rs_apply_btn.enable() + else + rs_revert_btn.disable() + rs_apply_btn.disable() + end + end + + --#endregion + + return rs_pane +end + +return redstone diff --git a/rtu/config/system.lua b/rtu/config/system.lua new file mode 100644 index 00000000..5b572e4f --- /dev/null +++ b/rtu/config/system.lua @@ -0,0 +1,671 @@ +local log = require("scada-common.log") +local ppm = require("scada-common.ppm") +local rsio = require("scada-common.rsio") +local util = require("scada-common.util") + +local core = require("graphics.core") +local themes = require("graphics.themes") + +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") + +local Checkbox = require("graphics.elements.controls.Checkbox") +local PushButton = require("graphics.elements.controls.PushButton") +local RadioButton = require("graphics.elements.controls.RadioButton") + +local NumberField = require("graphics.elements.form.NumberField") +local TextField = require("graphics.elements.form.TextField") + +local IndLight = require("graphics.elements.indicators.IndicatorLight") + +local tri = util.trinary + +local cpair = core.cpair + +local RIGHT = core.ALIGN.RIGHT + +local self = { + importing_legacy = false, + importing_any_dc = false, + + show_auth_key = nil, ---@type function + show_key_btn = nil, ---@type PushButton + auth_key_textbox = nil, ---@type TextBox + auth_key_value = "" +} + +local system = {} + +-- create the system configuration view +---@param tool_ctl _rtu_cfg_tool_ctl +---@param main_pane MultiPane +---@param cfg_sys [ rtu_config, rtu_config, rtu_config, { [1]: string, [2]: string, [3]: any }[], function ] +---@param divs Div[] +---@param ext [ MultiPane, MultiPane, string[], function, function, function ] +---@param style { [string]: cpair } +function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style) + local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5] + local spkr_cfg, net_cfg, log_cfg, clr_cfg, summary = divs[1], divs[2], divs[3], divs[4], divs[5] + local peri_pane, rs_pane, NEEDS_UNIT, show_peri_conns, show_rs_conns, exit = ext[1], ext[2], ext[3], ext[4], ext[5], ext[6] + + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + + --#region Speakers + + local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49} + + TextBox{parent=spkr_cfg,x=1,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)} + + TextBox{parent=spkr_c,x=1,y=1,height=2,text="Speakers can be connected to this RTU gateway without RTU unit configuration entries."} + TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."} + + local s_vol = NumberField{parent=spkr_c,x=1,y=8,width=9,max_chars=7,allow_decimal=true,default=ini_cfg.SpeakerVolume,min=0,max=3,fg_bg=bw_fg_bg} + + TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg} + + local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_vol() + local vol = tonumber(s_vol.get_value()) + if vol ~= nil then + s_vol_err.hide(true) + tmp_cfg.SpeakerVolume = vol + main_pane.set_value(3) + else s_vol_err.show() end + end + + PushButton{parent=spkr_c,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=spkr_c,x=44,y=14,text="Next \x1a",callback=submit_vol,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Network + + local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49} + local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49} + local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49} + + local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}} + + TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} + + TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."} + TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=8,text="Supervisor Channel"} + local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} + TextBox{parent=net_c_1,x=1,y=11,text="RTU Channel"} + local rtu_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=9,y=12,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg} + + local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_channels() + local svr_c = tonumber(svr_chan.get_value()) + local rtu_c = tonumber(rtu_chan.get_value()) + if svr_c ~= nil and rtu_c ~= nil then + tmp_cfg.SVR_Channel = svr_c + tmp_cfg.RTU_Channel = rtu_c + net_pane.set_value(2) + chan_err.hide(true) + elseif svr_c == nil then + chan_err.set_value("Please set the supervisor channel.") + chan_err.show() + else + chan_err.set_value("Please set the RTU channel.") + chan_err.show() + end + end + + PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_2,x=1,y=1,text="Connection Timeout"} + local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} + TextBox{parent=net_c_2,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg} + TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_2,x=1,y=8,text="Trusted Range"} + local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg} + TextBox{parent=net_c_2,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg} + + local p2_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_ct_tr() + local timeout_val = tonumber(timeout.get_value()) + local range_val = tonumber(range.get_value()) + if timeout_val ~= nil and range_val ~= nil then + tmp_cfg.ConnTimeout = timeout_val + tmp_cfg.TrustedRange = range_val + net_pane.set_value(3) + p2_err.hide(true) + elseif timeout_val == nil then + p2_err.set_value("Please set the connection timeout.") + p2_err.show() + else + p2_err.set_value("Please set the trusted range.") + p2_err.show() + end + end + + PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_3,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} + TextBox{parent=net_c_3,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_3,x=1,y=11,text="Facility Auth Key"} + local key, _ = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg} + + local function censor_key(enable) key.censor(tri(enable, "*", nil)) end + + local hide_key = Checkbox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} + + hide_key.set_value(true) + censor_key(true) + + local key_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_auth() + local v = key.get_value() + if string.len(v) == 0 or string.len(v) >= 8 then + tmp_cfg.AuthKey = key.get_value() + main_pane.set_value(4) + key_err.hide(true) + else key_err.show() end + end + + PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Logging + + local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} + + TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} + + TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."} + + TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} + local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} + + TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"} + local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg} + + local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} + TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg} + + local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_log() + if path.get_value() ~= "" then + path_err.hide(true) + tmp_cfg.LogMode = mode.get_value() - 1 + tmp_cfg.LogPath = path.get_value() + tmp_cfg.LogDebug = en_dbg.get_value() + tool_ctl.color_apply.hide(true) + tool_ctl.color_next.show() + main_pane.set_value(5) + else path_err.show() end + end + + PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Color Options + + local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49} + local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49} + local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49} + local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49} + + local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}} + + TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)} + + TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color theme for the front panel."} + TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg} + + TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"} + local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} + + TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."} + + TextBox{parent=clr_c_2,x=21,y=7,text="Preview"} + local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)} + _ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)} + _ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)} + local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true} + local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true} + + local function recolor(value) + local c = themes.smooth_stone.color_modes[value] + + if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then + b_off.hide() + g_off.show() + else + g_off.hide() + b_off.show() + end + + if #c == 0 then + for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end + else + term.setPaletteColor(colors.green, c[1].hex) + term.setPaletteColor(colors.yellow, c[2].hex) + term.setPaletteColor(colors.red, c[3].hex) + end + end + + TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"} + local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} + + TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg} + + PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + local function back_from_colors() + main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 4)) + tool_ctl.jumped_to_color = false + recolor(1) + end + + local function show_access() + clr_pane.set_value(2) + recolor(c_mode.get_value()) + end + + local function submit_colors() + tmp_cfg.FrontPanelTheme = fp_theme.get_value() + tmp_cfg.ColorMode = c_mode.get_value() + + if tool_ctl.jumped_to_color then + settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme) + settings.set("ColorMode", tmp_cfg.ColorMode) + + if settings.save("/rtu.settings") then + load_settings(settings_cfg, true) + load_settings(ini_cfg) + clr_pane.set_value(3) + else + clr_pane.set_value(4) + end + else + tool_ctl.gen_summary(tmp_cfg) + tool_ctl.viewing_config = false + self.importing_legacy = false + tool_ctl.settings_apply.show() + tool_ctl.settings_confirm.hide(true) + main_pane.set_value(6) + end + end + + PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + + tool_ctl.color_apply.hide(true) + + TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"} + PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} + PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Summary and Saving + + local sum_c_1 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_2 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_3 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_4 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_5 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_6 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_7 = Div{parent=summary,x=2,y=4,width=49} + + local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4,sum_c_5,sum_c_6,sum_c_7}} + + TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)} + + local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local function back_from_settings() + if tool_ctl.viewing_config or self.importing_legacy then + if self.importing_legacy and self.importing_any_dc then + sum_pane.set_value(7) + else + self.importing_legacy = false + tool_ctl.go_home() + end + + tool_ctl.viewing_config = false + else main_pane.set_value(5) end + end + + ---@param element graphics_element + ---@param data any + local function try_set(element, data) + if data ~= nil then element.set_value(data) end + end + + ---@param exclude_conns boolean? true to exclude saving peripheral/redstone connections + local function save_and_continue(exclude_conns) + for _, field in ipairs(fields) do + local k, v = field[1], tmp_cfg[field[1]] + if not (exclude_conns and (k == "Peripherals" or k == "Redstone")) then + if v == nil then settings.unset(k) else settings.set(k, v) end + end + end + + -- always set these if missing + if settings.get("Peripherals") == nil then settings.set("Peripherals", {}) end + if settings.get("Redstone") == nil then settings.set("Redstone", {}) end + + if settings.save("/rtu.settings") then + load_settings(settings_cfg, true) + load_settings(ini_cfg) + + try_set(s_vol, ini_cfg.SpeakerVolume) + try_set(svr_chan, ini_cfg.SVR_Channel) + try_set(rtu_chan, ini_cfg.RTU_Channel) + try_set(timeout, ini_cfg.ConnTimeout) + try_set(range, ini_cfg.TrustedRange) + try_set(key, ini_cfg.AuthKey) + try_set(mode, ini_cfg.LogMode) + try_set(path, ini_cfg.LogPath) + try_set(en_dbg, ini_cfg.LogDebug) + try_set(fp_theme, ini_cfg.FrontPanelTheme) + try_set(c_mode, ini_cfg.ColorMode) + + if not exclude_conns then + tmp_cfg.Peripherals = tool_ctl.deep_copy_peri(ini_cfg.Peripherals) + tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone) + + tool_ctl.update_peri_list() + end + + tool_ctl.dev_cfg.enable() + tool_ctl.rs_cfg.enable() + tool_ctl.view_gw_cfg.enable() + + if self.importing_legacy then + self.importing_legacy = false + sum_pane.set_value(5) + else sum_pane.set_value(4) end + else sum_pane.set_value(6) end + end + + PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + self.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=function()save_and_continue(true)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + tool_ctl.settings_confirm = PushButton{parent=sum_c_1,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + tool_ctl.settings_confirm.hide() + + TextBox{parent=sum_c_2,x=1,y=1,text="The following peripherals will be imported:"} + local peri_import_list = ListBox{parent=sum_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + PushButton{parent=sum_c_2,x=1,y=14,text="\x1b Back",callback=function()sum_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_2,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + + TextBox{parent=sum_c_3,x=1,y=1,text="The following redstone entries will be imported:"} + local rs_import_list = ListBox{parent=sum_c_3,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + PushButton{parent=sum_c_3,x=1,y=14,text="\x1b Back",callback=function()sum_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_3,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + + local function jump_peri_conns() + tool_ctl.go_home() + show_peri_conns() + end + + local function jump_rs_conns() + tool_ctl.go_home() + show_rs_conns() + end + + TextBox{parent=sum_c_4,x=1,y=1,text="Settings saved!"} + TextBox{parent=sum_c_4,x=1,y=3,height=4,text="Remember to configure any peripherals or redstone that you have connected to this RTU gateway if you have not already done so, or if you have added, removed, or modified any of them."} + PushButton{parent=sum_c_4,x=1,y=8,min_width=24,text="Peripheral Connections",callback=jump_peri_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_4,x=1,y=10,min_width=22,text="Redstone Connections",callback=jump_rs_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=sum_c_5,x=1,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."} + + local function delete_legacy() + fs.delete("/rtu/config.lua") + exit() + end + + PushButton{parent=sum_c_5,x=1,y=14,min_width=8,text="Cancel",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_5,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)} + + TextBox{parent=sum_c_6,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} + PushButton{parent=sum_c_6,x=1,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_6,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + + TextBox{parent=sum_c_7,x=1,y=1,height=8,text="Warning!\n\nSome of the devices in your old config file aren't currently connected. If the device isn't connected, the options can't be properly validated. Please either connect your devices and try again or complete the import without validation on those entry's settings."} + TextBox{parent=sum_c_7,x=1,y=10,height=3,text="Afterwards, either (a) edit then save entries for currently disconnected devices to properly configure or (b) delete those entries."} + PushButton{parent=sum_c_7,x=1,y=14,text="\x1b Back",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_7,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(1)end,fg_bg=cpair(colors.black,colors.orange),active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Tool Functions + + -- load a legacy config file + function tool_ctl.load_legacy() + local config = require("rtu.config") + + self.importing_any_dc = false + + tmp_cfg.SpeakerVolume = config.SOUNDER_VOLUME or 1 + tmp_cfg.SVR_Channel = config.SVR_CHANNEL + tmp_cfg.RTU_Channel = config.RTU_CHANNEL + tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT + tmp_cfg.TrustedRange = config.TRUSTED_RANGE + tmp_cfg.AuthKey = config.AUTH_KEY or "" + tmp_cfg.LogMode = config.LOG_MODE + tmp_cfg.LogPath = config.LOG_PATH + tmp_cfg.LogDebug = config.LOG_DEBUG or false + tmp_cfg.Peripherals = {} + tmp_cfg.Redstone = {} + + local mounts = ppm.list_mounts() + + peri_import_list.remove_all() + for _, entry in ipairs(config.RTU_DEVICES) do + local for_facility = entry.for_reactor == 0 + local ini_unit = tri(for_facility, nil, entry.for_reactor) + + local def = { name = entry.name, unit = ini_unit, index = entry.index } + local mount = mounts[def.name] + + local status = " \x13 not connected, please re-config later" + local color = colors.orange + + if mount ~= nil then + -- lets make sure things are valid + local unit, index, err = nil, nil, false + local u, idx = def.unit, def.index + + if util.table_contains(NEEDS_UNIT, mount.type) then + if (mount.type == "dynamicValve" or mount.type == "environmentDetector") and for_facility then + -- skip + elseif not (util.is_int(u) and u > 0 and u < 5) then + err = true + else unit = u end + end + + if mount.type == "boilerValve" then + if not (idx == 1 or idx == 2) then + err = true + else index = idx end + elseif mount.type == "turbineValve" then + if not (idx == 1 or idx == 2 or idx == 3) then + err = true + else index = idx end + elseif mount.type == "dynamicValve" and for_facility then + if not (util.is_int(idx) and idx > 0 and idx < 5) then + err = true + else index = idx end + elseif mount.type == "dynamicValve" then + index = 1 + elseif mount.type == "environmentDetector" then + if not (util.is_int(idx) and idx > 0) then + err = true + else index = idx end + end + + if err then + status = " \x13 invalid, please re-config later" + else + def.index = index + def.unit = unit + status = " \x04 validated" + color = colors.green + end + else self.importing_any_dc = true end + + table.insert(tmp_cfg.Peripherals, def) + + local desc = " \x1a " + + if type(def.index) == "number" then + desc = desc .. "#" .. def.index .. " " + end + + if type(def.unit) == "number" then + desc = desc .. "for unit " .. def.unit + else + desc = desc .. "for the facility" + end + + local line = Div{parent=peri_import_list,height=3} + TextBox{parent=line,x=1,y=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)} + TextBox{parent=line,x=1,y=2,text=status,fg_bg=cpair(color,colors.white)} + TextBox{parent=line,x=1,y=3,text=desc,fg_bg=cpair(colors.gray,colors.white)} + end + + rs_import_list.remove_all() + for _, entry in ipairs(config.RTU_REDSTONE) do + if entry.for_reactor == 0 then entry.for_reactor = nil end + for _, io_entry in ipairs(entry.io) do + local def = { unit = entry.for_reactor, port = io_entry.port, side = io_entry.side, color = io_entry.bundled_color } + table.insert(tmp_cfg.Redstone, def) + + local name = rsio.to_string(def.port) + local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") + local conn = def.side + local unit = "facility" + + if def.unit then unit = "unit " .. def.unit end + if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end + + local line = Div{parent=rs_import_list,height=1} + TextBox{parent=line,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} + TextBox{parent=line,x=2,y=1,width=14,text=name} + TextBox{parent=line,x=18,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)} + TextBox{parent=line,x=40,y=1,text=unit,fg_bg=cpair(colors.gray,colors.white)} + end + end + + tool_ctl.gen_summary(tmp_cfg) + if self.importing_any_dc then sum_pane.set_value(7) else sum_pane.set_value(1) end + main_pane.set_value(6) + tool_ctl.settings_apply.hide(true) + tool_ctl.settings_confirm.show() + self.importing_legacy = true + end + + -- go back to the home page + function tool_ctl.go_home() + tool_ctl.viewing_config = false + self.importing_legacy = false + self.importing_any_dc = false + + main_pane.set_value(1) + net_pane.set_value(1) + clr_pane.set_value(1) + sum_pane.set_value(1) + peri_pane.set_value(1) + rs_pane.set_value(1) + end + + -- expose the auth key on the summary page + function self.show_auth_key() + self.show_key_btn.disable() + self.auth_key_textbox.set_value(self.auth_key_value) + end + + -- generate the summary list + ---@param cfg rtu_config + function tool_ctl.gen_summary(cfg) + setting_list.remove_all() + + local alternate = false + local inner_width = setting_list.get_width() - 1 + + self.show_key_btn.enable() + self.auth_key_value = cfg.AuthKey or "" -- to show auth key + + for i = 1, #fields do + local f = fields[i] + local height = 1 + local label_w = string.len(f[2]) + local val_max_w = (inner_width - label_w) + 1 + local raw = cfg[f[1]] + local val = util.strval(raw) + + if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) + elseif f[1] == "LogMode" then val = tri(raw == log.MODE.APPEND, "append", "replace") + elseif f[1] == "FrontPanelTheme" then + val = util.strval(themes.fp_theme_name(raw)) + elseif f[1] == "ColorMode" then + val = util.strval(themes.color_mode_name(raw)) + end + + if val == "nil" then val = "" end + + local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) + alternate = not alternate + + if string.len(val) > val_max_w then + local lines = util.strwrap(val, inner_width) + height = #lines + 1 + end + + local line = Div{parent=setting_list,height=height,fg_bg=c} + TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} + + local textbox + if height > 1 then + textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1} + else + textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT} + end + + if f[1] == "AuthKey" then self.auth_key_textbox = textbox end + end + end + + --#endregion +end + +return system diff --git a/rtu/configure.lua b/rtu/configure.lua index 0c571deb..d4453d1c 100644 --- a/rtu/configure.lua +++ b/rtu/configure.lua @@ -2,83 +2,32 @@ -- Configuration GUI -- -local constants = require("scada-common.constants") local log = require("scada-common.log") local ppm = require("scada-common.ppm") -local rsio = require("scada-common.rsio") local tcd = require("scada-common.tcd") local util = require("scada-common.util") +local peripherals = require("rtu.config.peripherals") +local redstone = require("rtu.config.redstone") +local system = require("rtu.config.system") + local core = require("graphics.core") local themes = require("graphics.themes") -local DisplayBox = require("graphics.elements.displaybox") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") - -local CheckBox = require("graphics.elements.controls.checkbox") -local PushButton = require("graphics.elements.controls.push_button") -local Radio2D = require("graphics.elements.controls.radio_2d") -local RadioButton = require("graphics.elements.controls.radio_button") - -local NumberField = require("graphics.elements.form.number_field") -local TextField = require("graphics.elements.form.text_field") +local DisplayBox = require("graphics.elements.DisplayBox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local IndLight = require("graphics.elements.indicators.light") +local PushButton = require("graphics.elements.controls.PushButton") local println = util.println local tri = util.trinary local cpair = core.cpair -local IO = rsio.IO -local IO_LVL = rsio.IO_LVL -local IO_MODE = rsio.IO_MODE - -local LEFT = core.ALIGN.LEFT local CENTER = core.ALIGN.CENTER -local RIGHT = core.ALIGN.RIGHT - --- rsio port descriptions -local PORT_DESC_MAP = { - { IO.F_SCRAM, "Facility SCRAM" }, - { IO.F_ACK, "Facility Acknowledge" }, - { IO.R_SCRAM, "Reactor SCRAM" }, - { IO.R_RESET, "Reactor RPS Reset" }, - { IO.R_ENABLE, "Reactor Enable" }, - { IO.U_ACK, "Unit Acknowledge" }, - { IO.F_ALARM, "Facility Alarm (high prio)" }, - { IO.F_ALARM_ANY, "Facility Alarm (any)" }, - { IO.F_MATRIX_LOW, "Induction Matrix < " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) .. "%" }, - { IO.F_MATRIX_HIGH, "Induction Matrix > " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) .. "%" }, - { IO.F_MATRIX_CHG, "Induction Matrix Charge %" }, - { IO.WASTE_PU, "Waste Plutonium Valve" }, - { IO.WASTE_PO, "Waste Polonium Valve" }, - { IO.WASTE_POPL, "Waste Po Pellets Valve" }, - { IO.WASTE_AM, "Waste Antimatter Valve" }, - { IO.R_ACTIVE, "Reactor Active" }, - { IO.R_AUTO_CTRL, "Reactor in Auto Control" }, - { IO.R_SCRAMMED, "RPS Tripped" }, - { IO.R_AUTO_SCRAM, "RPS Auto SCRAM" }, - { IO.R_HIGH_DMG, "RPS High Damage" }, - { IO.R_HIGH_TEMP, "RPS High Temperature" }, - { IO.R_LOW_COOLANT, "RPS Low Coolant" }, - { IO.R_EXCESS_HC, "RPS Excess Heated Coolant" }, - { IO.R_EXCESS_WS, "RPS Excess Waste" }, - { IO.R_INSUFF_FUEL, "RPS Insufficient Fuel" }, - { IO.R_PLC_FAULT, "RPS PLC Fault" }, - { IO.R_PLC_TIMEOUT, "RPS Supervisor Timeout" }, - { IO.U_ALARM, "Unit Alarm" }, - { IO.U_EMER_COOL, "Unit Emergency Cool. Valve" } -} - --- designation (0 = facility, 1 = unit) -local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 } - -assert(#PORT_DESC_MAP == rsio.NUM_PORTS) -assert(#PORT_DSGN == rsio.NUM_PORTS) -- changes to the config data/format to let the user know local changes = { @@ -88,104 +37,61 @@ local changes = { { "v1.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } } } ----@class rtu_rs_definition ----@field unit integer|nil ----@field port IO_PORT ----@field side side ----@field color color|nil - ----@class rtu_peri_definition ----@field unit integer|nil ----@field index integer|nil ----@field name string - -local RTU_DEV_TYPES = { "boilerValve", "turbineValve", "dynamicValve", "inductionPort", "spsPort", "solarNeutronActivator", "environmentDetector" } -local NEEDS_UNIT = { "boilerValve", "turbineValve", "dynamicValve", "solarNeutronActivator", "environmentDetector" } - ---@class rtu_configurator local configurator = {} local style = {} -style.root = cpair(colors.black, colors.lightGray) -style.header = cpair(colors.white, colors.gray) +style.root = cpair(colors.black, colors.lightGray) +style.header = cpair(colors.white, colors.gray) -style.colors = themes.smooth_stone.colors +style.colors = themes.smooth_stone.colors -local bw_fg_bg = cpair(colors.black, colors.white) -local g_lg_fg_bg = cpair(colors.gray, colors.lightGray) -local nav_fg_bg = bw_fg_bg -local btn_act_fg_bg = cpair(colors.white, colors.gray) +style.bw_fg_bg = cpair(colors.black, colors.white) +style.g_lg_fg_bg = cpair(colors.gray, colors.lightGray) +style.nav_fg_bg = style.bw_fg_bg +style.btn_act_fg_bg = cpair(colors.white, colors.gray) +style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white) ---@class _rtu_cfg_tool_ctl local tool_ctl = { ask_config = false, has_config = false, viewing_config = false, - importing_legacy = false, - importing_any_dc = false, jumped_to_color = false, - peri_cfg_editing = false, ---@type integer|false - peri_cfg_manual = false, - rs_cfg_port = IO.F_SCRAM, ---@type IO_PORT - rs_cfg_editing = false, ---@type integer|false - - view_gw_cfg = nil, ---@type graphics_element - dev_cfg = nil, ---@type graphics_element - rs_cfg = nil, ---@type graphics_element - color_cfg = nil, ---@type graphics_element - color_next = nil, ---@type graphics_element - color_apply = nil, ---@type graphics_element - settings_apply = nil, ---@type graphics_element - settings_confirm = nil, ---@type graphics_element + + view_gw_cfg = nil, ---@type PushButton + dev_cfg = nil, ---@type PushButton + rs_cfg = nil, ---@type PushButton + color_cfg = nil, ---@type PushButton + color_next = nil, ---@type PushButton + color_apply = nil, ---@type PushButton + settings_apply = nil, ---@type PushButton + settings_confirm = nil, ---@type PushButton go_home = nil, ---@type function gen_summary = nil, ---@type function - show_current_cfg = nil, ---@type function load_legacy = nil, ---@type function - p_assign = nil, ---@type function update_peri_list = nil, ---@type function gen_peri_summary = nil, ---@type function gen_rs_summary = nil, ---@type function - - show_auth_key = nil, ---@type function - show_key_btn = nil, ---@type graphics_element - auth_key_textbox = nil, ---@type graphics_element - auth_key_value = "", - - ppm_devs = nil, ---@type graphics_element - p_name_msg = nil, ---@type graphics_element - p_prompt = nil, ---@type graphics_element - p_idx = nil, ---@type graphics_element - p_unit = nil, ---@type graphics_element - p_assign_btn = nil, ---@type graphics_element - p_desc = nil, ---@type graphics_element - p_desc_ext = nil, ---@type graphics_element - p_err = nil, ---@type graphics_element - - rs_cfg_selection = nil, ---@type graphics_element - rs_cfg_unit_l = nil, ---@type graphics_element - rs_cfg_unit = nil, ---@type graphics_element - rs_cfg_side_l = nil, ---@type graphics_element - rs_cfg_color = nil, ---@type graphics_element - rs_cfg_shortcut = nil ---@type graphics_element } ---@class rtu_config local tmp_cfg = { SpeakerVolume = 1.0, - Peripherals = {}, - Redstone = {}, - SVR_Channel = nil, ---@type integer - RTU_Channel = nil, ---@type integer - ConnTimeout = nil, ---@type number - TrustedRange = nil, ---@type number - AuthKey = nil, ---@type string|nil - LogMode = 0, + Peripherals = {}, ---@type rtu_peri_definition[] + Redstone = {}, ---@type rtu_rs_definition[] + SVR_Channel = nil, ---@type integer + RTU_Channel = nil, ---@type integer + ConnTimeout = nil, ---@type number + TrustedRange = nil, ---@type number + AuthKey = nil, ---@type string|nil + LogMode = 0, ---@type LOG_MODE LogPath = "", LogDebug = false, - FrontPanelTheme = 1, - ColorMode = 1 + FrontPanelTheme = 1, ---@type FP_THEME + ColorMode = 1 ---@type COLOR_MODE } ---@class rtu_config @@ -207,36 +113,15 @@ local fields = { { "ColorMode", "Color Mode", themes.COLOR_MODE.STANDARD } } -local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" } -local side_options_map = { "top", "bottom", "left", "right", "front", "back" } -local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" } -local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown } - --- convert text representation to index ----@param side string -local function side_to_idx(side) - for k, v in ipairs(side_options_map) do - if v == side then return k end - end -end - --- convert color to index ----@param color color -local function color_to_idx(color) - for k, v in ipairs(color_options_map) do - if v == color then return k end - end -end - -- deep copy peripherals defs -local function deep_copy_peri(data) +function tool_ctl.deep_copy_peri(data) local array = {} for _, d in ipairs(data) do table.insert(array, { unit = d.unit, index = d.index, name = d.name }) end return array end -- deep copy redstone defs -local function deep_copy_rs(data) +function tool_ctl.deep_copy_rs(data) local array = {} for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, side = d.side, color = d.color }) end return array @@ -259,8 +144,14 @@ local function load_settings(target, raw) end -- create the config view ----@param display graphics_element +---@param display DisplayBox local function config_view(display) + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + ---@diagnostic disable-next-line: undefined-field local function exit() os.queueEvent("terminate") end @@ -306,19 +197,19 @@ local function config_view(display) end local function show_peri_conns() - tool_ctl.gen_peri_summary(ini_cfg) + tool_ctl.gen_peri_summary() main_pane.set_value(8) end local function show_rs_conns() - tool_ctl.gen_rs_summary(ini_cfg) + tool_ctl.gen_rs_summary() main_pane.set_value(9) end PushButton{parent=main_page,x=2,y=y_start,min_width=19,text="Configure Gateway",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - tool_ctl.view_gw_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=28,text="View Gateway Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} - tool_ctl.dev_cfg = PushButton{parent=main_page,x=2,y=y_start+4,min_width=24,text="Peripheral Connections",callback=show_peri_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} - tool_ctl.rs_cfg = PushButton{parent=main_page,x=2,y=y_start+6,min_width=22,text="Redstone Connections",callback=show_rs_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} + tool_ctl.view_gw_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=28,text="View Gateway Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + tool_ctl.dev_cfg = PushButton{parent=main_page,x=2,y=y_start+4,min_width=24,text="Peripheral Connections",callback=show_peri_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + tool_ctl.rs_cfg = PushButton{parent=main_page,x=2,y=y_start+6,min_width=22,text="Redstone Connections",callback=show_rs_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} local function jump_color() tool_ctl.jumped_to_color = true @@ -328,7 +219,7 @@ local function config_view(display) end PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} - tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} + tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} if not tool_ctl.has_config then @@ -340,414 +231,26 @@ local function config_view(display) --#endregion - --#region Speakers - - local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49} - - TextBox{parent=spkr_cfg,x=1,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)} - - TextBox{parent=spkr_c,x=1,y=1,height=2,text="Speakers can be connected to this RTU gateway without RTU unit configuration entries."} - TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."} - - local s_vol = NumberField{parent=spkr_c,x=1,y=8,width=9,max_chars=7,allow_decimal=true,default=ini_cfg.SpeakerVolume,min=0,max=3,fg_bg=bw_fg_bg} + local settings = { settings_cfg, ini_cfg, tmp_cfg, fields, load_settings } - TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg} + --#region Peripherals Configuration - local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_vol() - local vol = tonumber(s_vol.get_value()) - if vol ~= nil then - s_vol_err.hide(true) - tmp_cfg.SpeakerVolume = vol - main_pane.set_value(3) - else s_vol_err.show() end - end - - PushButton{parent=spkr_c,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=spkr_c,x=44,y=14,text="Next \x1a",callback=submit_vol,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + local peri_pane, NEEDS_UNIT = peripherals.create(tool_ctl, main_pane, settings, peri_cfg, style) --#endregion - --#region Network - - local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49} - local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49} - local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49} - - local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}} - - TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} - - TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."} - TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_1,x=1,y=8,text="Supervisor Channel"} - local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} - TextBox{parent=net_c_1,x=1,y=11,text="RTU Channel"} - local rtu_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=9,y=12,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg} - - local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_channels() - local svr_c = tonumber(svr_chan.get_value()) - local rtu_c = tonumber(rtu_chan.get_value()) - if svr_c ~= nil and rtu_c ~= nil then - tmp_cfg.SVR_Channel = svr_c - tmp_cfg.RTU_Channel = rtu_c - net_pane.set_value(2) - chan_err.hide(true) - elseif svr_c == nil then - chan_err.set_value("Please set the supervisor channel.") - chan_err.show() - else - chan_err.set_value("Please set the RTU channel.") - chan_err.show() - end - end - - PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=net_c_2,x=1,y=1,text="Connection Timeout"} - local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} - TextBox{parent=net_c_2,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg} - TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_2,x=1,y=8,text="Trusted Range"} - local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg} - TextBox{parent=net_c_2,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg} - - local p2_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_ct_tr() - local timeout_val = tonumber(timeout.get_value()) - local range_val = tonumber(range.get_value()) - if timeout_val ~= nil and range_val ~= nil then - tmp_cfg.ConnTimeout = timeout_val - tmp_cfg.TrustedRange = range_val - net_pane.set_value(3) - p2_err.hide(true) - elseif timeout_val == nil then - p2_err.set_value("Please set the connection timeout.") - p2_err.show() - else - p2_err.set_value("Please set the trusted range.") - p2_err.show() - end - end - - PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=net_c_3,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} - TextBox{parent=net_c_3,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_3,x=1,y=11,text="Facility Auth Key"} - local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg} - - local function censor_key(enable) censor(tri(enable, "*", nil)) end - - local hide_key = CheckBox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} + --#region Redstone Configuration - hide_key.set_value(true) - censor_key(true) - - local key_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_auth() - local v = key.get_value() - if string.len(v) == 0 or string.len(v) >= 8 then - tmp_cfg.AuthKey = key.get_value() - main_pane.set_value(4) - key_err.hide(true) - else key_err.show() end - end - - PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + local rs_pane = redstone.create(tool_ctl, main_pane, settings, rs_cfg, style) --#endregion - --#region Logging - - local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} + --#region System Configuration - TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} + local divs = { spkr_cfg, net_cfg, log_cfg, clr_cfg, summary } + local ext = { peri_pane, rs_pane, NEEDS_UNIT, show_peri_conns, show_rs_conns, exit } - TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."} - - TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} - local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} - - TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"} - local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg} - - local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} - TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg} - - local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_log() - if path.get_value() ~= "" then - path_err.hide(true) - tmp_cfg.LogMode = mode.get_value() - 1 - tmp_cfg.LogPath = path.get_value() - tmp_cfg.LogDebug = en_dbg.get_value() - tool_ctl.color_apply.hide(true) - tool_ctl.color_next.show() - main_pane.set_value(5) - else path_err.show() end - end - - PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - --#endregion - - --#region Color Options - - local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49} - local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49} - local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49} - local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49} - - local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}} - - TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)} - - TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color theme for the front panel."} - TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg} - - TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"} - local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} - - TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."} - - TextBox{parent=clr_c_2,x=21,y=7,text="Preview"} - local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)} - _ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)} - _ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)} - local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true} - local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true} - - local function recolor(value) - local c = themes.smooth_stone.color_modes[value] - - if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then - b_off.hide() - g_off.show() - else - g_off.hide() - b_off.show() - end - - if #c == 0 then - for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end - else - term.setPaletteColor(colors.green, c[1].hex) - term.setPaletteColor(colors.yellow, c[2].hex) - term.setPaletteColor(colors.red, c[3].hex) - end - end - - TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"} - local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} - - TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg} - - PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - local function back_from_colors() - main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 4)) - tool_ctl.jumped_to_color = false - recolor(1) - end - - local function show_access() - clr_pane.set_value(2) - recolor(c_mode.get_value()) - end - - local function submit_colors() - tmp_cfg.FrontPanelTheme = fp_theme.get_value() - tmp_cfg.ColorMode = c_mode.get_value() - - if tool_ctl.jumped_to_color then - settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme) - settings.set("ColorMode", tmp_cfg.ColorMode) - - if settings.save("/rtu.settings") then - load_settings(settings_cfg, true) - load_settings(ini_cfg) - clr_pane.set_value(3) - else - clr_pane.set_value(4) - end - else - tool_ctl.gen_summary(tmp_cfg) - tool_ctl.viewing_config = false - tool_ctl.importing_legacy = false - tool_ctl.settings_apply.show() - tool_ctl.settings_confirm.hide(true) - main_pane.set_value(6) - end - end - - PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} - - tool_ctl.color_apply.hide(true) - - TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"} - PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} - PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - --#endregion - - --#region Summary and Saving - - local sum_c_1 = Div{parent=summary,x=2,y=4,width=49} - local sum_c_2 = Div{parent=summary,x=2,y=4,width=49} - local sum_c_3 = Div{parent=summary,x=2,y=4,width=49} - local sum_c_4 = Div{parent=summary,x=2,y=4,width=49} - local sum_c_5 = Div{parent=summary,x=2,y=4,width=49} - local sum_c_6 = Div{parent=summary,x=2,y=4,width=49} - local sum_c_7 = Div{parent=summary,x=2,y=4,width=49} - - local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4,sum_c_5,sum_c_6,sum_c_7}} - - TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)} - - local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - local function back_from_settings() - if tool_ctl.viewing_config or tool_ctl.importing_legacy then - if tool_ctl.importing_legacy and tool_ctl.importing_any_dc then - sum_pane.set_value(7) - else - tool_ctl.importing_legacy = false - tool_ctl.go_home() - end - - tool_ctl.viewing_config = false - else main_pane.set_value(5) end - end - - ---@param element graphics_element - ---@param data any - local function try_set(element, data) - if data ~= nil then element.set_value(data) end - end - - ---@param exclude_conns boolean? true to exclude saving peripheral/redstone connections - local function save_and_continue(exclude_conns) - for _, field in ipairs(fields) do - local k, v = field[1], tmp_cfg[field[1]] - if not (exclude_conns and (k == "Peripherals" or k == "Redstone")) then - if v == nil then settings.unset(k) else settings.set(k, v) end - end - end - - -- always set these if missing - if settings.get("Peripherals") == nil then settings.set("Peripherals", {}) end - if settings.get("Redstone") == nil then settings.set("Redstone", {}) end - - if settings.save("/rtu.settings") then - load_settings(settings_cfg, true) - load_settings(ini_cfg) - - try_set(s_vol, ini_cfg.SpeakerVolume) - try_set(svr_chan, ini_cfg.SVR_Channel) - try_set(rtu_chan, ini_cfg.RTU_Channel) - try_set(timeout, ini_cfg.ConnTimeout) - try_set(range, ini_cfg.TrustedRange) - try_set(key, ini_cfg.AuthKey) - try_set(mode, ini_cfg.LogMode) - try_set(path, ini_cfg.LogPath) - try_set(en_dbg, ini_cfg.LogDebug) - try_set(fp_theme, ini_cfg.FrontPanelTheme) - try_set(c_mode, ini_cfg.ColorMode) - - if not exclude_conns then - tmp_cfg.Peripherals = deep_copy_peri(ini_cfg.Peripherals) - tmp_cfg.Redstone = deep_copy_rs(ini_cfg.Redstone) - - tool_ctl.update_peri_list() - end - - tool_ctl.dev_cfg.enable() - tool_ctl.rs_cfg.enable() - tool_ctl.view_gw_cfg.enable() - - if tool_ctl.importing_legacy then - tool_ctl.importing_legacy = false - sum_pane.set_value(5) - else sum_pane.set_value(4) end - else sum_pane.set_value(6) end - end - - PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} - tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=function()save_and_continue(true)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} - tool_ctl.settings_confirm = PushButton{parent=sum_c_1,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} - tool_ctl.settings_confirm.hide() - - TextBox{parent=sum_c_2,x=1,y=1,text="The following peripherals will be imported:"} - local peri_import_list = ListBox{parent=sum_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - PushButton{parent=sum_c_2,x=1,y=14,text="\x1b Back",callback=function()sum_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_2,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} - - TextBox{parent=sum_c_3,x=1,y=1,text="The following redstone entries will be imported:"} - local rs_import_list = ListBox{parent=sum_c_3,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - PushButton{parent=sum_c_3,x=1,y=14,text="\x1b Back",callback=function()sum_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_3,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} - - local function jump_peri_conns() - tool_ctl.go_home() - show_peri_conns() - end - - local function jump_rs_conns() - tool_ctl.go_home() - show_rs_conns() - end - - TextBox{parent=sum_c_4,x=1,y=1,text="Settings saved!"} - TextBox{parent=sum_c_4,x=1,y=3,height=4,text="Remember to configure any peripherals or redstone that you have connected to this RTU gateway if you have not already done so, or if you have added, removed, or modified any of them."} - PushButton{parent=sum_c_4,x=1,y=8,min_width=24,text="Peripheral Connections",callback=jump_peri_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_4,x=1,y=10,min_width=22,text="Redstone Connections",callback=jump_rs_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=sum_c_5,x=1,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."} - - local function delete_legacy() - fs.delete("/rtu/config.lua") - exit() - end - - PushButton{parent=sum_c_5,x=1,y=14,min_width=8,text="Cancel",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_5,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)} - - TextBox{parent=sum_c_6,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} - PushButton{parent=sum_c_6,x=1,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_6,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - - TextBox{parent=sum_c_7,x=1,y=1,height=8,text="Warning!\n\nSome of the devices in your old config file aren't currently connected. If the device isn't connected, the options can't be properly validated. Please either connect your devices and try again or complete the import without validation on those entry's settings."} - TextBox{parent=sum_c_7,x=1,y=10,height=3,text="Afterwards, either (a) edit then save entries for currently disconnected devices to properly configure or (b) delete those entries."} - PushButton{parent=sum_c_7,x=1,y=14,text="\x1b Back",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_7,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(1)end,fg_bg=cpair(colors.black,colors.orange),active_fg_bg=btn_act_fg_bg} + system.create(tool_ctl, main_pane, settings, divs, ext, style) --#endregion @@ -771,829 +274,6 @@ local function config_view(display) PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} --#endregion - - --#region Peripherals - - local peri_c_1 = Div{parent=peri_cfg,x=2,y=4,width=49} - local peri_c_2 = Div{parent=peri_cfg,x=2,y=4,width=49} - local peri_c_3 = Div{parent=peri_cfg,x=2,y=4,width=49} - local peri_c_4 = Div{parent=peri_cfg,x=2,y=4,width=49} - local peri_c_5 = Div{parent=peri_cfg,x=2,y=4,width=49} - local peri_c_6 = Div{parent=peri_cfg,x=2,y=4,width=49} - local peri_c_7 = Div{parent=peri_cfg,x=2,y=4,width=49} - - local peri_pane = MultiPane{parent=peri_cfg,x=1,y=4,panes={peri_c_1,peri_c_2,peri_c_3,peri_c_4,peri_c_5,peri_c_6,peri_c_7}} - - TextBox{parent=peri_cfg,x=1,y=2,text=" Peripheral Connections",fg_bg=cpair(colors.black,colors.purple)} - - local peri_list = ListBox{parent=peri_c_1,x=1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - local function peri_revert() - tmp_cfg.Peripherals = deep_copy_peri(ini_cfg.Peripherals) - tool_ctl.gen_peri_summary(tmp_cfg) - end - - local function peri_apply() - settings.set("Peripherals", tmp_cfg.Peripherals) - - if settings.save("/rtu.settings") then - load_settings(settings_cfg, true) - load_settings(ini_cfg) - peri_pane.set_value(5) - else - peri_pane.set_value(6) - end - end - - PushButton{parent=peri_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=peri_c_1,x=8,y=14,min_width=16,text="Revert Changes",callback=peri_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg} - PushButton{parent=peri_c_1,x=35,y=14,min_width=7,text="Add +",callback=function()peri_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - PushButton{parent=peri_c_1,x=43,y=14,min_width=7,text="Apply",callback=peri_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} - - TextBox{parent=peri_c_2,x=1,y=1,text="Select one of the below devices to use."} - - tool_ctl.ppm_devs = ListBox{parent=peri_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - PushButton{parent=peri_c_2,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=peri_c_2,x=8,y=14,min_width=10,text="Manual +",callback=function()peri_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.orange),active_fg_bg=btn_act_fg_bg} - PushButton{parent=peri_c_2,x=26,y=14,min_width=24,text="I don't see my device!",callback=function()peri_pane.set_value(7)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg} - - TextBox{parent=peri_c_7,x=1,y=1,height=10,text="Make sure your device is either touching the RTU or connected via wired modems. There should be a wired modem on a side of the RTU then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."} - TextBox{parent=peri_c_7,x=1,y=9,height=4,text="If it still does not show, it may not be compatible. Currently only Boilers, Turbines, Dynamic Tanks, SNAs, SPSs, Induction Matricies, and Environment Detectors are supported."} - PushButton{parent=peri_c_7,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - local new_peri_attrs = { "", "" } - local function new_peri(name, type) - new_peri_attrs = { name, type } - tool_ctl.peri_cfg_editing = false - - tool_ctl.p_err.hide(true) - tool_ctl.p_name_msg.set_value("Configuring peripheral on '" .. name .. "':") - tool_ctl.p_desc_ext.set_value("") - - local function reposition(prompt, idx_x, idx_max, unit_x, unit_y, desc_y) - tool_ctl.p_prompt.set_value(prompt) - tool_ctl.p_idx.reposition(idx_x, 4) - tool_ctl.p_idx.enable() - tool_ctl.p_idx.set_max(idx_max) - tool_ctl.p_idx.show() - tool_ctl.p_unit.reposition(unit_x, unit_y) - tool_ctl.p_unit.enable() - tool_ctl.p_unit.show() - tool_ctl.p_desc.reposition(1, desc_y) - end - - if type == "boilerValve" then - reposition("This is reactor unit # 's # boiler.", 31, 2, 23, 4, 7) - tool_ctl.p_assign_btn.hide(true) - tool_ctl.p_desc.set_value("Each unit can have at most 2 boilers. Boiler #1 shows up first on the main display, followed by boiler #2 below it. The numberings are per unit (unit 1 and unit 2 would both have a boiler #1 if each had one boiler) and can be split amongst multiple RTUs (one has #1, another has #2).") - elseif type == "turbineValve" then - reposition("This is reactor unit # 's # turbine.", 31, 3, 23, 4, 7) - tool_ctl.p_assign_btn.hide(true) - tool_ctl.p_desc.set_value("Each unit can have at most 3 turbines. Turbine #1 shows up first on the main display, followed by #2 then #3 below it. The numberings are per unit (unit 1 and unit 2 would both have a turbine #1) and can be split amongst multiple RTUs (one has #1, another has #2).") - elseif type == "solarNeutronActivator" then - reposition("This SNA is for reactor unit # .", 46, 1, 31, 4, 7) - tool_ctl.p_idx.hide() - tool_ctl.p_assign_btn.hide(true) - tool_ctl.p_desc_ext.set_value("Before adding lots of SNAs: multiply the \"PEAK\" rate on the flow monitor (after connecting at least 1 SNA) by 10 to get the mB/t of waste that they can process. Enough SNAs to provide 2x to 3x of your max burn rate should be a good margin to catch up after night or cloudy weather. Too many devices (such as SNAs) on one RTU can cause lag.") - elseif type == "dynamicValve" then - reposition("This is the below system's # dynamic tank.", 29, 4, 17, 6, 8) - tool_ctl.p_assign_btn.show() - tool_ctl.p_assign_btn.redraw() - - if tool_ctl.p_assign_btn.get_value() == 1 then - tool_ctl.p_idx.enable() - tool_ctl.p_unit.disable() - else - tool_ctl.p_idx.set_value(1) - tool_ctl.p_idx.disable() - tool_ctl.p_unit.enable() - end - - tool_ctl.p_desc.set_value("Each reactor unit can have at most 1 tank and the facility can have at most 4. Each facility tank must have a unique # 1 through 4, regardless of where it is connected. Only a total of 4 tanks can be displayed on the flow monitor.") - elseif type == "environmentDetector" then - reposition("This is the below system's # env. detector.", 29, 99, 17, 6, 8) - tool_ctl.p_assign_btn.show() - tool_ctl.p_assign_btn.redraw() - if tool_ctl.p_assign_btn.get_value() == 1 then tool_ctl.p_unit.disable() else tool_ctl.p_unit.enable() end - tool_ctl.p_desc.set_value("You can connect more than one environment detector for a particular unit or the facility. In that case, the maximum radiation reading from those assigned to that particular unit or the facility will be used for alarms and display.") - elseif type == "inductionPort" or type == "spsPort" then - local dev = tri(type == "inductionPort", "induction matrix", "SPS") - tool_ctl.p_idx.hide(true) - tool_ctl.p_unit.hide(true) - tool_ctl.p_prompt.set_value("This is the " .. dev .. " for the facility.") - tool_ctl.p_assign_btn.hide(true) - tool_ctl.p_desc.reposition(1, 7) - tool_ctl.p_desc.set_value("There can only be one of these devices per SCADA network, so it will be assigned as the sole " .. dev .. " for the facility. There must only be one of these across all the RTUs you have.") - else - assert(false, "invalid peripheral type after type validation") - end - - peri_pane.set_value(4) - end - - -- update peripherals list - function tool_ctl.update_peri_list() - local alternate = true - local mounts = ppm.list_mounts() - - -- filter out in-use peripherals - for _, v in ipairs(tmp_cfg.Peripherals) do mounts[v.name] = nil end - - tool_ctl.ppm_devs.remove_all() - for name, entry in pairs(mounts) do - if util.table_contains(RTU_DEV_TYPES, entry.type) then - local bkg = tri(alternate, colors.white, colors.lightGray) - - ---@cast entry ppm_entry - local line = Div{parent=tool_ctl.ppm_devs,height=2,fg_bg=cpair(colors.black,bkg)} - PushButton{parent=line,x=1,y=1,min_width=9,alignment=LEFT,height=1,text="> SELECT",callback=function()tool_ctl.peri_cfg_manual=false;new_peri(name,entry.type)end,fg_bg=cpair(colors.black,colors.purple),active_fg_bg=cpair(colors.white,colors.black)} - TextBox{parent=line,x=11,y=1,text=name,fg_bg=cpair(colors.black,bkg)} - TextBox{parent=line,x=11,y=2,text=entry.type,fg_bg=cpair(colors.gray,bkg)} - - alternate = not alternate - end - end - end - - tool_ctl.update_peri_list() - - TextBox{parent=peri_c_3,x=1,y=1,height=4,text="This feature is intended for advanced users. If you are clicking this just because your device is not shown, follow the connection instructions in 'I don't see my device!'."} - TextBox{parent=peri_c_3,x=1,y=6,height=4,text="Peripheral Name"} - local p_name = TextField{parent=peri_c_3,x=1,y=7,width=49,height=1,max_len=128,fg_bg=bw_fg_bg} - local p_type = Radio2D{parent=peri_c_3,x=1,y=9,rows=4,columns=2,default=1,options=RTU_DEV_TYPES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple} - local man_p_err = TextBox{parent=peri_c_3,x=8,y=14,width=35,text="Please enter a peripheral name.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - man_p_err.hide(true) - - local function submit_manual_peri() - local name = p_name.get_value() - if string.len(name) > 0 then - tool_ctl.entering_manual = true - man_p_err.hide(true) - new_peri(name, RTU_DEV_TYPES[p_type.get_value()]) - else man_p_err.show() end - end - - PushButton{parent=peri_c_3,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=peri_c_3,x=44,y=14,text="Next \x1a",callback=submit_manual_peri,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - tool_ctl.p_name_msg = TextBox{parent=peri_c_4,x=1,y=1,height=2,text=""} - tool_ctl.p_prompt = TextBox{parent=peri_c_4,x=1,y=4,height=2,text=""} - tool_ctl.p_idx = NumberField{parent=peri_c_4,x=31,y=4,width=4,max_chars=2,min=1,max=2,default=1,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} - tool_ctl.p_assign_btn = RadioButton{parent=peri_c_4,x=1,y=5,default=1,options={"the facility","reactor unit #"},callback=function(v)tool_ctl.p_assign(v)end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple} - - tool_ctl.p_unit = NumberField{parent=peri_c_4,x=23,y=4,width=4,max_chars=2,min=1,max=4,default=1,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} - tool_ctl.p_unit.disable() - - function tool_ctl.p_assign(opt) - if opt == 1 then - tool_ctl.p_unit.disable() - if new_peri_attrs[2] == "dynamicValve" then tool_ctl.p_idx.enable() end - else - tool_ctl.p_unit.enable() - if new_peri_attrs[2] == "dynamicValve" then - tool_ctl.p_idx.set_value(1) - tool_ctl.p_idx.disable() - end - end - end - - tool_ctl.p_desc = TextBox{parent=peri_c_4,x=1,y=7,height=6,text="",fg_bg=g_lg_fg_bg} - tool_ctl.p_desc_ext = TextBox{parent=peri_c_4,x=1,y=6,height=7,text="",fg_bg=g_lg_fg_bg} - - tool_ctl.p_err = TextBox{parent=peri_c_4,x=8,y=14,width=32,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - tool_ctl.p_err.hide(true) - - local function back_from_peri_opts() - if tool_ctl.peri_cfg_editing ~= false then - peri_pane.set_value(1) - elseif tool_ctl.entering_manual then - peri_pane.set_value(3) - else - peri_pane.set_value(2) - end - - tool_ctl.entering_manual = false - end - - local function save_peri_entry() - local peri_name = new_peri_attrs[1] - local peri_type = new_peri_attrs[2] - - local unit, index = nil, nil - - local for_facility = tool_ctl.p_assign_btn.get_value() == 1 - local u = tonumber(tool_ctl.p_unit.get_value()) - local idx = tonumber(tool_ctl.p_idx.get_value()) - - if util.table_contains(NEEDS_UNIT, peri_type) then - if (peri_type == "dynamicValve" or peri_type == "environmentDetector") and for_facility then - -- skip - elseif not (util.is_int(u) and u > 0 and u < 5) then - tool_ctl.p_err.set_value("Unit ID must be within 1 to 4.") - tool_ctl.p_err.show() - return - else unit = u end - end - - if peri_type == "boilerValve" then - if not (idx == 1 or idx == 2) then - tool_ctl.p_err.set_value("Index must be 1 or 2.") - tool_ctl.p_err.show() - return - else index = idx end - elseif peri_type == "turbineValve" then - if not (idx == 1 or idx == 2 or idx == 3) then - tool_ctl.p_err.set_value("Index must be 1, 2, or 3.") - tool_ctl.p_err.show() - return - else index = idx end - elseif peri_type == "dynamicValve" and for_facility then - if not (util.is_int(idx) and idx > 0 and idx < 5) then - tool_ctl.p_err.set_value("Index must be within 1 to 4.") - tool_ctl.p_err.show() - return - else index = idx end - elseif peri_type == "dynamicValve" then - index = 1 - elseif peri_type == "environmentDetector" then - if not (util.is_int(idx) and idx > 0) then - tool_ctl.p_err.set_value("Index must be greater than 0.") - tool_ctl.p_err.show() - return - else index = idx end - end - - tool_ctl.p_err.hide(true) - - ---@type rtu_peri_definition - local def = { name = peri_name, unit = unit, index = index } - - if tool_ctl.peri_cfg_editing == false then - table.insert(tmp_cfg.Peripherals, def) - else - def.name = tmp_cfg.Peripherals[tool_ctl.peri_cfg_editing].name - tmp_cfg.Peripherals[tool_ctl.peri_cfg_editing] = def - end - - peri_pane.set_value(1) - tool_ctl.gen_peri_summary(tmp_cfg) - tool_ctl.update_peri_list() - - tool_ctl.p_idx.set_value(1) - end - - PushButton{parent=peri_c_4,x=1,y=14,text="\x1b Back",callback=back_from_peri_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=peri_c_4,x=41,y=14,min_width=9,text="Confirm",callback=save_peri_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - - TextBox{parent=peri_c_5,x=1,y=1,text="Settings saved!"} - PushButton{parent=peri_c_5,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=peri_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=peri_c_6,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} - PushButton{parent=peri_c_6,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=peri_c_6,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - --#endregion - - --#region Redstone - - local rs_c_1 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_2 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_3 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49} - - local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7}} - - TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)} - - TextBox{parent=rs_c_1,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg} - local rs_list = ListBox{parent=rs_c_1,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - local function rs_revert() - tmp_cfg.Redstone = deep_copy_rs(ini_cfg.Redstone) - tool_ctl.gen_rs_summary(tmp_cfg) - end - - local function rs_apply() - settings.set("Redstone", tmp_cfg.Redstone) - - if settings.save("/rtu.settings") then - load_settings(settings_cfg, true) - load_settings(ini_cfg) - rs_pane.set_value(4) - else - rs_pane.set_value(5) - end - end - - PushButton{parent=rs_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=rs_c_1,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg} - PushButton{parent=rs_c_1,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - PushButton{parent=rs_c_1,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} - - TextBox{parent=rs_c_6,x=1,y=1,height=5,text="You already configured this input. There can only be one entry for each input.\n\nPlease select a different port."} - PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=rs_c_2,x=1,y=1,text="Select one of the below ports to use."} - - local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - local function new_rs(port) - if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then - for i = 1, #tmp_cfg.Redstone do - if tmp_cfg.Redstone[i].port == port then - rs_pane.set_value(6) - return - end - end - end - - tool_ctl.rs_cfg_editing = false - - local text - - if port == -1 then - tool_ctl.rs_cfg_color.hide(true) - tool_ctl.rs_cfg_shortcut.show() - tool_ctl.rs_cfg_side_l.set_value("Output Side") - text = "You selected the ALL_WASTE shortcut." - else - tool_ctl.rs_cfg_shortcut.hide(true) - tool_ctl.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(port) == rsio.IO_DIR.IN, "Input Side", "Output Side")) - tool_ctl.rs_cfg_color.show() - - local io_type = "analog input " - local io_mode = rsio.get_io_mode(port) - local inv = tri(rsio.digital_is_active(port, IO_LVL.LOW) == true, "inverted ", "") - - if io_mode == IO_MODE.DIGITAL_IN then - io_type = inv .. "digital input " - elseif io_mode == IO_MODE.DIGITAL_OUT then - io_type = inv .. "digital output " - elseif io_mode == IO_MODE.ANALOG_OUT then - io_type = "analog output " - end - - text = "You selected the " .. io_type .. rsio.to_string(port) .. " (for " - - if PORT_DSGN[port] == 1 then - text = text .. "a unit)." - tool_ctl.rs_cfg_unit_l.show() - tool_ctl.rs_cfg_unit.show() - else - tool_ctl.rs_cfg_unit_l.hide(true) - tool_ctl.rs_cfg_unit.hide(true) - text = text .. "the facility)." - end - end - - tool_ctl.rs_cfg_selection.set_value(text) - tool_ctl.rs_cfg_port = port - rs_pane.set_value(3) - end - - -- add entries to redstone option list - local all_w_macro = Div{parent=rs_ports,height=1} - PushButton{parent=all_w_macro,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">ALL_WASTE",callback=function()new_rs(-1)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.black)} - TextBox{parent=all_w_macro,x=16,y=1,width=5,text="[n/a]",fg_bg=cpair(colors.lightGray,colors.white)} - TextBox{parent=all_w_macro,x=22,y=1,text="Create all 4 waste entries",fg_bg=cpair(colors.gray,colors.white)} - - for i = 1, rsio.NUM_PORTS do - local p = PORT_DESC_MAP[i][1] - local name = rsio.to_string(p) - local io_dir = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, "[in]", "[out]") - local btn_color = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue) - - local entry = Div{parent=rs_ports,height=1} - PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(p)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)} - TextBox{parent=entry,x=16,y=1,width=5,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} - TextBox{parent=entry,x=22,y=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)} - end - - PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - tool_ctl.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=2,text=""} - - PushButton{parent=rs_c_3,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=rs_c_7,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"} - TextBox{parent=rs_c_7,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"} - TextBox{parent=rs_c_7,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"} - PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - tool_ctl.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=4,width=11,text="Output Side"} - local side = Radio2D{parent=rs_c_3,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red} - - tool_ctl.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=25,y=7,width=7,text="Unit ID"} - tool_ctl.rs_cfg_unit = NumberField{parent=rs_c_3,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg} - - local function set_bundled(bundled) - if bundled then tool_ctl.rs_cfg_color.enable() else tool_ctl.rs_cfg_color.disable() end - end - - tool_ctl.rs_cfg_shortcut = TextBox{parent=rs_c_3,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."} - tool_ctl.rs_cfg_shortcut.hide(true) - - local bundled = CheckBox{parent=rs_c_3,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled} - tool_ctl.rs_cfg_color = Radio2D{parent=rs_c_3,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg} - tool_ctl.rs_cfg_color.disable() - - local rs_err = TextBox{parent=rs_c_3,x=8,y=14,width=30,text="Unit ID must be within 1 to 4.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - rs_err.hide(true) - - local function back_from_rs_opts() - rs_err.hide(true) - if tool_ctl.rs_cfg_editing ~= false then rs_pane.set_value(1) else rs_pane.set_value(2) end - end - - local function save_rs_entry() - local port = tool_ctl.rs_cfg_port - local u = tonumber(tool_ctl.rs_cfg_unit.get_value()) - - if PORT_DSGN[port] == 0 or (util.is_int(u) and u > 0 and u < 5) then - rs_err.hide(true) - - if port >= 0 then - ---@type rtu_rs_definition - local def = { - unit = tri(PORT_DSGN[port] == 1, u, nil), - port = port, - side = side_options_map[side.get_value()], - color = tri(bundled.get_value(), color_options_map[tool_ctl.rs_cfg_color.get_value()], nil) - } - - if tool_ctl.rs_cfg_editing == false then - table.insert(tmp_cfg.Redstone, def) - else - def.port = tmp_cfg.Redstone[tool_ctl.rs_cfg_editing].port - tmp_cfg.Redstone[tool_ctl.rs_cfg_editing] = def - end - elseif port == -1 then - local default_sides = { "left", "back", "right", "front" } - local default_colors = { colors.red, colors.orange, colors.yellow, colors.lime } - for i = 0, 3 do - table.insert(tmp_cfg.Redstone, { - unit = tri(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil), - port = IO.WASTE_PU + i, - side = tri(bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]), - color = tri(bundled.get_value(), default_colors[i + 1], nil) - }) - end - end - - rs_pane.set_value(1) - tool_ctl.gen_rs_summary(tmp_cfg) - - side.set_value(1) - bundled.set_value(false) - tool_ctl.rs_cfg_color.set_value(1) - tool_ctl.rs_cfg_color.disable() - else rs_err.show() end - end - - PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=rs_c_3,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - - TextBox{parent=rs_c_4,x=1,y=1,text="Settings saved!"} - PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=rs_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=rs_c_5,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} - PushButton{parent=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=rs_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - --#endregion - - -- set tool functions now that we have the elements - - -- load a legacy config file - function tool_ctl.load_legacy() - local config = require("rtu.config") - - tool_ctl.importing_any_dc = false - - tmp_cfg.SpeakerVolume = config.SOUNDER_VOLUME or 1 - tmp_cfg.SVR_Channel = config.SVR_CHANNEL - tmp_cfg.RTU_Channel = config.RTU_CHANNEL - tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT - tmp_cfg.TrustedRange = config.TRUSTED_RANGE - tmp_cfg.AuthKey = config.AUTH_KEY or "" - tmp_cfg.LogMode = config.LOG_MODE - tmp_cfg.LogPath = config.LOG_PATH - tmp_cfg.LogDebug = config.LOG_DEBUG or false - tmp_cfg.Peripherals = {} - tmp_cfg.Redstone = {} - - local mounts = ppm.list_mounts() - - peri_import_list.remove_all() - for _, entry in ipairs(config.RTU_DEVICES) do - local for_facility = entry.for_reactor == 0 - local ini_unit = tri(for_facility, nil, entry.for_reactor) - - local def = { name = entry.name, unit = ini_unit, index = entry.index } - local mount = mounts[def.name] ---@type ppm_entry|nil - - local status = " \x13 not connected, please re-config later" - local color = colors.orange - - if mount ~= nil then - -- lets make sure things are valid - local unit, index, err = nil, nil, false - local u, idx = def.unit, def.index - - if util.table_contains(NEEDS_UNIT, mount.type) then - if (mount.type == "dynamicValve" or mount.type == "environmentDetector") and for_facility then - -- skip - elseif not (util.is_int(u) and u > 0 and u < 5) then - err = true - else unit = u end - end - - if mount.type == "boilerValve" then - if not (idx == 1 or idx == 2) then - err = true - else index = idx end - elseif mount.type == "turbineValve" then - if not (idx == 1 or idx == 2 or idx == 3) then - err = true - else index = idx end - elseif mount.type == "dynamicValve" and for_facility then - if not (util.is_int(idx) and idx > 0 and idx < 5) then - err = true - else index = idx end - elseif mount.type == "dynamicValve" then - index = 1 - elseif mount.type == "environmentDetector" then - if not (util.is_int(idx) and idx > 0) then - err = true - else index = idx end - end - - if err then - status = " \x13 invalid, please re-config later" - else - def.index = index - def.unit = unit - status = " \x04 validated" - color = colors.green - end - else tool_ctl.importing_any_dc = true end - - table.insert(tmp_cfg.Peripherals, def) - - local desc = " \x1a " - - if type(def.index) == "number" then - desc = desc .. "#" .. def.index .. " " - end - - if type(def.unit) == "number" then - desc = desc .. "for unit " .. def.unit - else - desc = desc .. "for the facility" - end - - local line = Div{parent=peri_import_list,height=3} - TextBox{parent=line,x=1,y=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)} - TextBox{parent=line,x=1,y=2,text=status,fg_bg=cpair(color,colors.white)} - TextBox{parent=line,x=1,y=3,text=desc,fg_bg=cpair(colors.gray,colors.white)} - end - - rs_import_list.remove_all() - for _, entry in ipairs(config.RTU_REDSTONE) do - if entry.for_reactor == 0 then entry.for_reactor = nil end - for _, io_entry in ipairs(entry.io) do - local def = { unit = entry.for_reactor, port = io_entry.port, side = io_entry.side, color = io_entry.bundled_color } - table.insert(tmp_cfg.Redstone, def) - - local name = rsio.to_string(def.port) - local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") - local conn = def.side - local unit = "facility" - - if def.unit then unit = "unit " .. def.unit end - if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end - - local line = Div{parent=rs_import_list,height=1} - TextBox{parent=line,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} - TextBox{parent=line,x=2,y=1,width=14,text=name} - TextBox{parent=line,x=18,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)} - TextBox{parent=line,x=40,y=1,text=unit,fg_bg=cpair(colors.gray,colors.white)} - end - end - - tool_ctl.gen_summary(tmp_cfg) - if tool_ctl.importing_any_dc then sum_pane.set_value(7) else sum_pane.set_value(1) end - main_pane.set_value(6) - tool_ctl.settings_apply.hide(true) - tool_ctl.settings_confirm.show() - tool_ctl.importing_legacy = true - end - - -- go back to the home page - function tool_ctl.go_home() - tool_ctl.viewing_config = false - tool_ctl.importing_legacy = false - tool_ctl.importing_any_dc = false - - main_pane.set_value(1) - net_pane.set_value(1) - clr_pane.set_value(1) - sum_pane.set_value(1) - peri_pane.set_value(1) - rs_pane.set_value(1) - end - - -- expose the auth key on the summary page - function tool_ctl.show_auth_key() - tool_ctl.show_key_btn.disable() - tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value) - end - - -- generate the summary list - ---@param cfg rtu_config - function tool_ctl.gen_summary(cfg) - setting_list.remove_all() - - local alternate = false - local inner_width = setting_list.get_width() - 1 - - tool_ctl.show_key_btn.enable() - tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key - - for i = 1, #fields do - local f = fields[i] - local height = 1 - local label_w = string.len(f[2]) - local val_max_w = (inner_width - label_w) + 1 - local raw = cfg[f[1]] - local val = util.strval(raw) - - if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) - elseif f[1] == "LogMode" then val = tri(raw == log.MODE.APPEND, "append", "replace") - elseif f[1] == "FrontPanelTheme" then - val = util.strval(themes.fp_theme_name(raw)) - elseif f[1] == "ColorMode" then - val = util.strval(themes.color_mode_name(raw)) - end - - if val == "nil" then val = "" end - - local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) - alternate = not alternate - - if string.len(val) > val_max_w then - local lines = util.strwrap(val, inner_width) - height = #lines + 1 - end - - local line = Div{parent=setting_list,height=height,fg_bg=c} - TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} - - local textbox - if height > 1 then - textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1} - else - textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT} - end - - if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end - end - end - - ---@param def rtu_peri_definition - ---@param idx integer - ---@param type string - local function edit_peri_entry(idx, def, type) - -- set inputs BEFORE calling new_peri() - if def.index ~= nil then tool_ctl.p_idx.set_value(def.index) end - if def.unit == nil then - tool_ctl.p_assign_btn.set_value(1) - else - tool_ctl.p_unit.set_value(def.unit) - tool_ctl.p_assign_btn.set_value(2) - end - - new_peri(def.name, type) - - -- set editing mode AFTER new_peri() - tool_ctl.peri_cfg_editing = idx - end - - local function delete_peri_entry(idx) - table.remove(tmp_cfg.Peripherals, idx) - tool_ctl.gen_peri_summary(tmp_cfg) - tool_ctl.update_peri_list() - end - - -- generate the peripherals summary list - ---@param cfg rtu_config - function tool_ctl.gen_peri_summary(cfg) - peri_list.remove_all() - - for i = 1, #cfg.Peripherals do - local def = cfg.Peripherals[i] ---@type rtu_peri_definition - - local t = ppm.get_type(def.name) - local t_str = " (connect to edit)" - local disconnected = t == nil - - if not disconnected then t_str = "[" .. t .. "]" end - - local desc = " \x1a " - - if type(def.index) == "number" then - desc = desc .. "#" .. def.index .. " " - end - - if type(def.unit) == "number" then - desc = desc .. "for unit " .. def.unit - else - desc = desc .. "for the facility" - end - - local entry = Div{parent=peri_list,height=3} - TextBox{parent=entry,x=1,y=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)} - TextBox{parent=entry,x=1,y=2,text=" \x1a "..t_str,fg_bg=cpair(colors.gray,colors.white)} - TextBox{parent=entry,x=1,y=3,text=desc,fg_bg=cpair(colors.gray,colors.white)} - local edit_btn = PushButton{parent=entry,x=41,y=2,min_width=8,height=1,text="EDIT",callback=function()edit_peri_entry(i,def,t or "")end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} - PushButton{parent=entry,x=41,y=3,min_width=8,height=1,text="DELETE",callback=function()delete_peri_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} - - if disconnected then edit_btn.disable() end - end - end - - local function edit_rs_entry(idx) - local def = tmp_cfg.Redstone[idx] ---@type rtu_rs_definition - - tool_ctl.rs_cfg_shortcut.hide(true) - tool_ctl.rs_cfg_color.show() - - tool_ctl.rs_cfg_port = def.port - tool_ctl.rs_cfg_editing = idx - - local text = "Editing " .. rsio.to_string(def.port) .. " (for " - if PORT_DSGN[def.port] == 1 then - text = text .. "a unit)." - tool_ctl.rs_cfg_unit_l.show() - tool_ctl.rs_cfg_unit.show() - tool_ctl.rs_cfg_unit.set_value(def.unit or 1) - else - tool_ctl.rs_cfg_unit_l.hide(true) - tool_ctl.rs_cfg_unit.hide(true) - text = text .. "the facility)." - end - - local value = 1 - if def.color ~= nil then - value = color_to_idx(def.color) - tool_ctl.rs_cfg_color.enable() - else - tool_ctl.rs_cfg_color.disable() - end - - tool_ctl.rs_cfg_selection.set_value(text) - tool_ctl.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "Input Side", "Output Side")) - side.set_value(side_to_idx(def.side)) - bundled.set_value(def.color ~= nil) - tool_ctl.rs_cfg_color.set_value(value) - rs_pane.set_value(3) - end - - local function delete_rs_entry(idx) - table.remove(tmp_cfg.Redstone, idx) - tool_ctl.gen_rs_summary(tmp_cfg) - end - - -- generate the redstone summary list - ---@param cfg rtu_config - function tool_ctl.gen_rs_summary(cfg) - rs_list.remove_all() - - for i = 1, #cfg.Redstone do - local def = cfg.Redstone[i] ---@type rtu_rs_definition - - local name = rsio.to_string(def.port) - local io_dir = tri(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") - local conn = def.side - local unit = util.strval(def.unit or "F") - - if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end - - local entry = Div{parent=rs_list,height=1} - TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} - TextBox{parent=entry,x=2,y=1,width=14,text=name} - TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)} - TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)} - PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} - end - end end -- reset terminal screen @@ -1611,8 +291,8 @@ function configurator.configure(ask_config) load_settings(settings_cfg, true) tool_ctl.has_config = load_settings(ini_cfg) - tmp_cfg.Peripherals = deep_copy_peri(ini_cfg.Peripherals) - tmp_cfg.Redstone = deep_copy_rs(ini_cfg.Redstone) + tmp_cfg.Peripherals = tool_ctl.deep_copy_peri(ini_cfg.Peripherals) + tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone) reset_term() diff --git a/rtu/databus.lua b/rtu/databus.lua index 4fe183ab..0d086f46 100644 --- a/rtu/databus.lua +++ b/rtu/databus.lua @@ -10,15 +10,15 @@ local databus = {} -- databus PSIL databus.ps = psil.create() ----@enum RTU_UNIT_HW_STATE -local RTU_UNIT_HW_STATE = { +---@enum RTU_HW_STATE +local RTU_HW_STATE = { OFFLINE = 1, FAULTED = 2, UNFORMED = 3, OK = 4 } -databus.RTU_UNIT_HW_STATE = RTU_UNIT_HW_STATE +databus.RTU_HW_STATE = RTU_HW_STATE -- call to toggle heartbeat signal function databus.heartbeat() databus.ps.toggle("heartbeat") end @@ -52,7 +52,7 @@ end -- transmit unit hardware status across the bus ---@param uid integer unit ID ----@param status RTU_UNIT_HW_STATE +---@param status RTU_HW_STATE function databus.tx_unit_hw_status(uid, status) databus.ps.publish("unit_hw_" .. uid, status) end diff --git a/rtu/panel/front_panel.lua b/rtu/panel/front_panel.lua index b5b773af..6fc40e59 100644 --- a/rtu/panel/front_panel.lua +++ b/rtu/panel/front_panel.lua @@ -11,13 +11,13 @@ local style = require("rtu.panel.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") -local DataIndicator = require("graphics.elements.indicators.data") -local LED = require("graphics.elements.indicators.led") -local LEDPair = require("graphics.elements.indicators.ledpair") -local RGBLED = require("graphics.elements.indicators.ledrgb") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") +local LED = require("graphics.elements.indicators.LED") +local LEDPair = require("graphics.elements.indicators.LEDPair") +local RGBLED = require("graphics.elements.indicators.RGBLED") local LINK_STATE = types.PANEL_LINK_STATE @@ -30,8 +30,8 @@ local ind_grn = style.ind_grn local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC TANK", "IND MATRIX", "SPS", "SNA", "ENV DETECTOR" } -- create new front panel view ----@param panel graphics_element main displaybox ----@param units table unit list +---@param panel DisplayBox main displaybox +---@param units rtu_registry_entry[] unit list local function init(panel, units) local disabled_fg = style.fp.disabled_fg @@ -135,7 +135,7 @@ local function init(panel, units) -- show hardware statuses for i = 1, list_length do - local unit = units[i] ---@type rtu_unit_registry_entry + local unit = units[i] -- hardware status local unit_hw = RGBLED{parent=unit_hw_statuses,y=i,label="",colors={colors.red,colors.orange,colors.yellow,colors.green}} diff --git a/rtu/renderer.lua b/rtu/renderer.lua index bdbc25bb..dd86a062 100644 --- a/rtu/renderer.lua +++ b/rtu/renderer.lua @@ -8,7 +8,7 @@ local style = require("rtu.panel.style") local core = require("graphics.core") local flasher = require("graphics.flasher") -local DisplayBox = require("graphics.elements.displaybox") +local DisplayBox = require("graphics.elements.DisplayBox") ---@class rtu_renderer local renderer = {} @@ -18,7 +18,7 @@ local ui = { } -- try to start the UI ----@param units table RTU units +---@param units rtu_registry_entry[] RTU entries ---@param theme FP_THEME front panel theme ---@param color_mode COLOR_MODE color mode ---@return boolean success, any error_msg diff --git a/rtu/rtu.lua b/rtu/rtu.lua index dc398b2d..0ea17798 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -235,7 +235,7 @@ function rtu.init_unit(device) end -- create an alarm speaker sounder ----@param speaker table device peripheral +---@param speaker Speaker device peripheral function rtu.init_sounder(speaker) ---@class rtu_speaker_sounder local spkr_ctl = { @@ -322,13 +322,13 @@ function rtu.comms(version, nic, conn_watchdog) -- generate device advertisement table ---@nodiscard - ---@param units table + ---@param units rtu_registry_entry[] ---@return table advertisement local function _generate_advertisement(units) local advertisement = {} for i = 1, #units do - local unit = units[i] ---@type rtu_unit_registry_entry + local unit = units[i] if unit.type ~= nil then local advert = { unit.type, unit.index, unit.reactor } @@ -429,9 +429,9 @@ function rtu.comms(version, nic, conn_watchdog) -- handle a MODBUS/SCADA packet ---@param packet modbus_frame|mgmt_frame - ---@param units table RTU units + ---@param units rtu_registry_entry[] RTU entries ---@param rtu_state rtu_state - ---@param sounders table speaker alarm sounders + ---@param sounders rtu_speaker_sounder[] speaker alarm sounders function public.handle_packet(packet, units, rtu_state, sounders) -- print a log message to the terminal as long as the UI isn't running local function println_ts(message) if not rtu_state.fp_ok then util.println_ts(message) end end @@ -467,7 +467,7 @@ function rtu.comms(version, nic, conn_watchdog) -- handle MODBUS instruction if packet.unit_id <= #units then - local unit = units[packet.unit_id] ---@type rtu_unit_registry_entry + local unit = units[packet.unit_id] local unit_dbg_tag = " (unit " .. packet.unit_id .. ")" if unit.name == "redstone_io" then @@ -538,11 +538,9 @@ function rtu.comms(version, nic, conn_watchdog) if (packet.length == 1) and type(packet.data[1] == "table") and (#packet.data[1] == 8) then local states = packet.data[1] + -- set tone states for i = 1, #sounders do - local s = sounders[i] ---@type rtu_speaker_sounder - - -- set tone states - for id = 1, #states do s.stream.set_active(id, states[id] == true) end + for id = 1, #states do sounders[i].stream.set_active(id, states[id] == true) end end end else diff --git a/rtu/startup.lua b/rtu/startup.lua index cb0d4ea6..f5ec27fc 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,10 +31,10 @@ 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.10.8" +local RTU_VERSION = "v1.10.14" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE -local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE +local RTU_HW_STATE = databus.RTU_HW_STATE local println = util.println local println_ts = util.println_ts @@ -109,7 +109,7 @@ local function main() -- RTU gateway devices (not RTU units) rtu_dev = { modem = ppm.get_wireless_modem(), - sounders = {} + sounders = {} ---@type rtu_speaker_sounder[] }, -- system objects @@ -117,7 +117,7 @@ local function main() nic = nil, ---@type nic rtu_comms = nil, ---@type rtu_comms conn_watchdog = nil, ---@type watchdog - units = {} + units = {} ---@type rtu_registry_entry[] }, -- message queues @@ -143,11 +143,11 @@ local function main() -- configure RTU gateway based on settings file definitions local function sys_config() -- redstone interfaces - local rs_rtus = {} + local rs_rtus = {} ---@type { rtu: rtu_rs_device, capabilities: IO_PORT[] }[] -- go through redstone definitions list for entry_idx = 1, #rtu_redstone do - local entry = rtu_redstone[entry_idx] ---@type rtu_rs_definition + local entry = rtu_redstone[entry_idx] local assignment local for_reactor = entry.unit local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side) @@ -227,21 +227,21 @@ local function main() -- create unit entries for redstone RTUs for for_reactor, def in pairs(rs_rtus) do - ---@class rtu_unit_registry_entry + ---@class rtu_registry_entry local unit = { - uid = 0, ---@type integer - name = "redstone_io", ---@type string - type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE - index = false, ---@type integer|false - reactor = for_reactor, ---@type integer - device = def.capabilities, ---@type table use device field for redstone ports - is_multiblock = false, ---@type boolean - formed = nil, ---@type boolean|nil - hw_state = RTU_UNIT_HW_STATE.OK, ---@type RTU_UNIT_HW_STATE - rtu = def.rtu, ---@type rtu_device|rtu_rs_device + uid = 0, ---@type integer + name = "redstone_io", ---@type string + type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE + index = false, ---@type integer|false + reactor = for_reactor, ---@type integer + device = def.capabilities, ---@type IO_PORT[] use device field for redstone ports + is_multiblock = false, ---@type boolean + formed = nil, ---@type boolean|nil + hw_state = RTU_HW_STATE.OK, ---@type RTU_HW_STATE + rtu = def.rtu, ---@type rtu_device|rtu_rs_device modbus_io = modbus.new(def.rtu, false), - pkt_queue = nil, ---@type mqueue|nil - thread = nil ---@type parallel_thread|nil + pkt_queue = nil, ---@type mqueue|nil + thread = nil ---@type parallel_thread|nil } table.insert(units, unit) @@ -440,21 +440,21 @@ local function main() end end - ---@class rtu_unit_registry_entry + ---@class rtu_registry_entry local rtu_unit = { - uid = 0, ---@type integer - name = name, ---@type string - type = rtu_type, ---@type RTU_UNIT_TYPE - index = index or false, ---@type integer|false - reactor = for_reactor, ---@type integer - device = device, ---@type table - is_multiblock = is_multiblock, ---@type boolean - formed = formed, ---@type boolean|nil - hw_state = RTU_UNIT_HW_STATE.OFFLINE, ---@type RTU_UNIT_HW_STATE - rtu = rtu_iface, ---@type rtu_device|rtu_rs_device + uid = 0, ---@type integer + name = name, ---@type string + type = rtu_type, ---@type RTU_UNIT_TYPE + index = index or false, ---@type integer|false + reactor = for_reactor, ---@type integer + device = device, ---@type table peripheral reference + is_multiblock = is_multiblock, ---@type boolean + formed = formed, ---@type boolean|nil + hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE + rtu = rtu_iface, ---@type rtu_device|rtu_rs_device modbus_io = modbus.new(rtu_iface, true), - pkt_queue = mqueue.new(), ---@type mqueue|nil - thread = nil ---@type parallel_thread|nil + pkt_queue = mqueue.new(), ---@type mqueue|nil + thread = nil ---@type parallel_thread|nil } rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit) @@ -473,14 +473,14 @@ local function main() -- determine hardware status if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then - rtu_unit.hw_state = RTU_UNIT_HW_STATE.OFFLINE + rtu_unit.hw_state = RTU_HW_STATE.OFFLINE else if rtu_unit.is_multiblock then - rtu_unit.hw_state = util.trinary(rtu_unit.formed == true, RTU_UNIT_HW_STATE.OK, RTU_UNIT_HW_STATE.UNFORMED) + rtu_unit.hw_state = util.trinary(rtu_unit.formed == true, RTU_HW_STATE.OK, RTU_HW_STATE.UNFORMED) elseif faulted then - rtu_unit.hw_state = RTU_UNIT_HW_STATE.FAULTED + rtu_unit.hw_state = RTU_HW_STATE.FAULTED else - rtu_unit.hw_state = RTU_UNIT_HW_STATE.OK + rtu_unit.hw_state = RTU_HW_STATE.OK end end diff --git a/rtu/threads.lua b/rtu/threads.lua index 011b6175..4ad5fcb9 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -23,7 +23,7 @@ local core = require("graphics.core") local threads = {} local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE -local UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE +local RTU_HW_STATE = databus.RTU_HW_STATE local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) local COMMS_SLEEP = 100 -- (100ms, 2 ticks) @@ -33,7 +33,7 @@ local COMMS_SLEEP = 100 -- (100ms, 2 ticks) ---@param iface string ---@param type string ---@param device table ----@param unit rtu_unit_registry_entry +---@param unit rtu_registry_entry local function handle_unit_mount(smem, println_ts, iface, type, device, unit) local sys = smem.rtu_sys @@ -106,7 +106,7 @@ local function handle_unit_mount(smem, println_ts, iface, type, device, unit) -- if disconnected on startup, config wouldn't have been validated -- checking now that it has connected; the config isn't valid, so don't connect it if invalid then - unit.hw_state = UNIT_HW_STATE.OFFLINE + unit.hw_state = RTU_HW_STATE.OFFLINE databus.tx_unit_hw_status(unit.uid, unit.hw_state) return end @@ -138,16 +138,16 @@ local function handle_unit_mount(smem, println_ts, iface, type, device, unit) end if unit.is_multiblock then - unit.hw_state = UNIT_HW_STATE.UNFORMED + unit.hw_state = RTU_HW_STATE.UNFORMED if unit.formed == false then log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing")) end elseif faulted then - unit.hw_state = UNIT_HW_STATE.FAULTED + unit.hw_state = RTU_HW_STATE.FAULTED elseif not unknown then - unit.hw_state = UNIT_HW_STATE.OK + unit.hw_state = RTU_HW_STATE.OK else - unit.hw_state = UNIT_HW_STATE.OFFLINE + unit.hw_state = RTU_HW_STATE.OFFLINE end databus.tx_unit_hw_status(unit.uid, unit.hw_state) @@ -245,6 +245,7 @@ function threads.thread__main(smem) if type ~= nil and device ~= nil then if type == "modem" then + ---@cast device Modem -- we only care if this is our wireless modem if nic.is_modem(device) then nic.disconnect() @@ -263,6 +264,7 @@ function threads.thread__main(smem) log.warning("non-comms modem disconnected") end elseif type == "speaker" then + ---@cast device Speaker for i = 1, #sounders do if sounders[i].speaker == device then table.remove(sounders, i) @@ -279,13 +281,13 @@ function threads.thread__main(smem) -- find disconnected device if units[i].device == device then -- will let the PPM prevent crashes, which will indicate failures in MODBUS queries - local unit = units[i] ---@type rtu_unit_registry_entry + local unit = units[i] local type_name = types.rtu_type_to_string(unit.type) println_ts(util.c("lost the ", type_name, " on interface ", unit.name)) log.warning(util.c("lost the ", type_name, " unit peripheral on interface ", unit.name)) - unit.hw_state = UNIT_HW_STATE.OFFLINE + unit.hw_state = RTU_HW_STATE.OFFLINE databus.tx_unit_hw_status(unit.uid, unit.hw_state) break end @@ -298,6 +300,7 @@ function threads.thread__main(smem) if type ~= nil and device ~= nil then if type == "modem" then + ---@cast device Modem if device.isWireless() and not nic.is_connected() then -- reconnected modem nic.connect(device) @@ -312,6 +315,7 @@ function threads.thread__main(smem) log.info("wired modem reconnected") end elseif type == "speaker" then + ---@cast device Speaker table.insert(sounders, rtu.init_sounder(device)) println_ts("speaker connected") @@ -332,7 +336,7 @@ function threads.thread__main(smem) elseif event == "speaker_audio_empty" then -- handle empty speaker audio buffer for i = 1, #sounders do - local sounder = sounders[i] ---@type rtu_speaker_sounder + local sounder = sounders[i] if sounder.name == param1 then sounder.continue() break @@ -460,7 +464,7 @@ end -- per-unit communications handler thread ---@nodiscard ---@param smem rtu_shared_memory ----@param unit rtu_unit_registry_entry +---@param unit rtu_registry_entry function threads.thread__unit_comms(smem, unit) ---@class parallel_thread local public = {} @@ -523,13 +527,13 @@ function threads.thread__unit_comms(smem, unit) if unit.formed == nil then unit.formed = is_formed - if is_formed then unit.hw_state = UNIT_HW_STATE.OK end + if is_formed then unit.hw_state = RTU_HW_STATE.OK end elseif not unit.formed then - unit.hw_state = UNIT_HW_STATE.UNFORMED + unit.hw_state = RTU_HW_STATE.UNFORMED end if (is_formed == true) and not unit.formed then - unit.hw_state = UNIT_HW_STATE.OK + unit.hw_state = RTU_HW_STATE.OK log.info(util.c(detail_name, " is now formed")) rtu_comms.send_remounted(unit.uid) elseif (is_formed == false) and unit.formed then @@ -541,9 +545,9 @@ function threads.thread__unit_comms(smem, unit) -- check hardware status if unit.device.__p_is_healthy() then - if unit.hw_state == UNIT_HW_STATE.FAULTED then unit.hw_state = UNIT_HW_STATE.OK end + if unit.hw_state == RTU_HW_STATE.FAULTED then unit.hw_state = RTU_HW_STATE.OK end else - if unit.hw_state == UNIT_HW_STATE.OK then unit.hw_state = UNIT_HW_STATE.FAULTED end + if unit.hw_state == RTU_HW_STATE.OK then unit.hw_state = RTU_HW_STATE.FAULTED end end -- update hw status diff --git a/scada-common/audio.lua b/scada-common/audio.lua index 019b5105..e1a69585 100644 --- a/scada-common/audio.lua +++ b/scada-common/audio.lua @@ -6,10 +6,10 @@ -- note: max samples = 0x20000 (128 * 1024 samples) -local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry -local _DRATE = 48000 -- 48kHz audio -local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio -local _05s_SAMPLES = 24000 -- half a second worth of samples +local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry +local _DRATE = 48000 -- 48kHz audio data rate +local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio +local _05s_SAMPLES = 24000 -- half a second worth of samples ---@class audio local audio = {} @@ -28,6 +28,7 @@ local TONE = { audio.TONE = TONE +---@type integer[][][] local tone_data = { { {}, {}, {}, {} }, -- 340Hz @ 2Hz Intermittent { {}, {}, {}, {} }, -- 544Hz 100mS / 440Hz 400mS Alternating @@ -214,7 +215,14 @@ end -- generate all 8 tone sequences function audio.generate_tones() - gen_tone_1(); gen_tone_2(); gen_tone_3(); gen_tone_4(); gen_tone_5(); gen_tone_6(); gen_tone_7(); gen_tone_8() + gen_tone_1() + gen_tone_2() + gen_tone_3() + gen_tone_4() + gen_tone_5() + gen_tone_6() + gen_tone_7() + gen_tone_8() end -- hard audio limiter @@ -226,7 +234,7 @@ local function limit(output) end -- clear output buffer ----@param buffer table quad buffer +---@param buffer integer[][] quad buffer local function clear(buffer) for i = 1, 4 do for s = 1, _05s_SAMPLES do buffer[i][s] = 0 end @@ -239,7 +247,7 @@ function audio.new_stream() any_active = false, need_recompute = false, next_block = 1, - -- split audio up into 0.5s samples, so specific components can be ended quicker + ---@type integer[][] split audio up into 0.5s samples, so specific components can be ended quicker quad_buffer = { {}, {}, {}, {} }, -- all tone enable states tone_active = { false, false, false, false, false, false, false, false } @@ -263,14 +271,17 @@ function audio.new_stream() -- check if a tone is active ---@param index TONE tone index function public.is_active(index) - if self.tone_active[index] then return self.tone_active[index] end - return false + return self.tone_active[index] or false end -- set all tones inactive, reset next block, and clear output buffer function public.stop() - for i = 1, #self.tone_active do self.tone_active[i] = false end + for i = 1, #self.tone_active do + self.tone_active[i] = false + end + self.next_block = 1 + clear(self.quad_buffer) end @@ -287,9 +298,11 @@ function audio.new_stream() for id = 1, #tone_data do if self.tone_active[id] then self.any_active = true + for i = 1, 4 do local buffer = self.quad_buffer[i] local values = tone_data[id][i] + for s = 1, _05s_SAMPLES do self.quad_buffer[i][s] = limit(buffer[s] + values[s]) end end end @@ -305,8 +318,13 @@ function audio.new_stream() -- get the next audio block function public.get_next_block() local block = self.quad_buffer[self.next_block] + self.next_block = self.next_block + 1 - if self.next_block > 4 then self.next_block = 1 end + + if self.next_block > 4 then + self.next_block = 1 + end + return block end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 6a822285..17752487 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -17,8 +17,8 @@ local max_distance = nil local comms = {} -- protocol/data versions (protocol/data independent changes tracked by util.lua version) -comms.version = "3.0.0" -comms.api_version = "0.0.4" +comms.version = "3.0.1" +comms.api_version = "0.0.6" ---@enum PROTOCOL local PROTOCOL = { @@ -67,7 +67,9 @@ local CRDN_TYPE = { UNIT_STATUSES = 5, -- state of each of the reactor units UNIT_CMD = 6, -- command a reactor unit API_GET_FAC = 7, -- API: get all the facility data - API_GET_UNIT = 8 -- API: get reactor unit data + API_GET_UNIT = 8, -- API: get reactor unit data + 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 @@ -405,7 +407,7 @@ function comms.modbus_packet() self.raw = { self.txn_id, self.unit_id, self.func_code } for i = 1, self.length do insert(self.raw, data[i]) end else - log.error("comms.modbus_packet.make(): data not table") + log.error("comms.modbus_packet.make(): data not a table") end end @@ -491,7 +493,7 @@ function comms.rplc_packet() self.raw = { self.id, self.type } for i = 1, #data do insert(self.raw, data[i]) end else - log.error("comms.rplc_packet.make(): data not table") + log.error("comms.rplc_packet.make(): data not a table") end end @@ -573,7 +575,7 @@ function comms.mgmt_packet() self.raw = { self.type } for i = 1, #data do insert(self.raw, data[i]) end else - log.error("comms.mgmt_packet.make(): data not table") + log.error("comms.mgmt_packet.make(): data not a table") end end @@ -652,7 +654,7 @@ function comms.crdn_packet() self.raw = { self.type } for i = 1, #data do insert(self.raw, data[i]) end else - log.error("comms.crdn_packet.make(): data not table") + log.error("comms.crdn_packet.make(): data not a table") end end diff --git a/scada-common/log.lua b/scada-common/log.lua index f876eaa6..49f4c5e3 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -4,10 +4,14 @@ local util = require("scada-common.util") +-- constant strings for speed +local DBG_TAG, INF_TAG, WRN_TAG, ERR_TAG, FTL_TAG = "[DBG] ", "[INF] ", "[WRN] ", "[ERR] ", "[FTL] " +local COLON, FUNC, ARROW = ":", "():", " > " + ---@class logger local log = {} ----@alias MODE integer +---@enum LOG_MODE local MODE = { APPEND = 0, NEW = 1 } log.MODE = MODE @@ -18,7 +22,7 @@ local logger = { mode = MODE.APPEND, debug = false, file = nil, ---@type table|nil - dmesg_out = nil, + dmesg_out = nil, ---@type Redirect|nil dmesg_restore_coord = { 1, 1 }, dmesg_scroll_count = 0 } @@ -31,13 +35,13 @@ local free_space = fs.getFreeSpace ----------------------- -- private log write function ----@param msg string -local function _log(msg) +---@param msg_bits any[] +local function _log(msg_bits) if logger.not_ready then return end local out_of_space = false - local time_stamp = os.date("[%c] ") - local stamped = time_stamp .. util.strval(msg) + local time_stamp = os.date("[%c] ") + local stamped = util.c(time_stamp, table.unpack(msg_bits)) -- attempt to write log local status, result = pcall(function () @@ -78,9 +82,9 @@ end -- initialize logger ---@param path string file path ----@param write_mode MODE file write mode +---@param write_mode LOG_MODE file write mode ---@param include_debug boolean whether or not to include debug logs ----@param dmesg_redirect? table terminal/window to direct dmesg to +---@param dmesg_redirect? Redirect terminal/window to direct dmesg to function log.init(path, write_mode, include_debug, dmesg_redirect) logger.path = path logger.mode = write_mode @@ -102,16 +106,14 @@ function log.init(path, write_mode, include_debug, dmesg_redirect) end -- close the log file handle -function log.close() - logger.file.close() -end +function log.close() logger.file.close() end -- direct dmesg output to a monitor/window ----@param window table window or terminal reference +---@param window Window window or terminal reference function log.direct_dmesg(window) logger.dmesg_out = window end -- dmesg style logging for boot because I like linux-y things ----@param msg string message +---@param msg any message ---@param tag? string log tag ---@param tag_color? integer log tag color ---@return dmesg_ts_coord coordinates line area to place working indicator @@ -120,8 +122,7 @@ function log.dmesg(msg, tag, tag_color) local ts_coord = { x1 = 2, x2 = 3, y = 1 } msg = util.strval(msg) - tag = tag or "" - tag = util.strval(tag) + tag = util.strval(tag or "") local t_stamp = string.format("%12.2f", os.clock()) local out = logger.dmesg_out @@ -209,7 +210,7 @@ function log.dmesg(msg, tag, tag_color) logger.dmesg_restore_coord = { out.getCursorPos() } - _log(util.c("[", t_stamp, "] [", tag, "] ", msg)) + _log{"[", t_stamp, "] [", tag, "] ", msg} end return ts_coord @@ -291,63 +292,51 @@ function log.dmesg_working(msg, tag, tag_color) end -- log debug messages ----@param msg string message +---@param msg any message ---@param trace? boolean include file trace function log.debug(msg, trace) if logger.debug then - local dbg_info = "" - if trace then local info = debug.getinfo(2) - local name = "" if info.name ~= nil then - name = ":" .. info.name .. "():" + _log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg} + else + _log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg} end - - dbg_info = info.short_src .. ":" .. name .. info.currentline .. " > " + else + _log{DBG_TAG, msg} end - - _log("[DBG] " .. dbg_info .. util.strval(msg)) end end -- log info messages ----@param msg string message -function log.info(msg) - _log("[INF] " .. util.strval(msg)) -end +---@param msg any message +function log.info(msg) _log{INF_TAG, msg} end -- log warning messages ----@param msg string message -function log.warning(msg) - _log("[WRN] " .. util.strval(msg)) -end +---@param msg any message +function log.warning(msg) _log{WRN_TAG, msg} end -- log error messages ----@param msg string message +---@param msg any message ---@param trace? boolean include file trace function log.error(msg, trace) - local dbg_info = "" - if trace then local info = debug.getinfo(2) - local name = "" if info.name ~= nil then - name = ":" .. info.name .. "():" + _log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg} + else + _log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg} end - - dbg_info = info.short_src .. ":" .. name .. info.currentline .. " > " + else + _log{ERR_TAG, msg} end - - _log("[ERR] " .. dbg_info .. util.strval(msg)) end -- log fatal errors ----@param msg string message -function log.fatal(msg) - _log("[FTL] " .. util.strval(msg)) -end +---@param msg any message +function log.fatal(msg) _log{FTL_TAG, msg} end return log diff --git a/scada-common/mqueue.lua b/scada-common/mqueue.lua index fc60a1e5..db09509f 100644 --- a/scada-common/mqueue.lua +++ b/scada-common/mqueue.lua @@ -27,6 +27,7 @@ local remove = table.remove -- create a new message queue ---@nodiscard function mqueue.new() + ---@type queue_item[] local queue = {} ---@class mqueue diff --git a/scada-common/network.lua b/scada-common/network.lua index c34a1d50..7eccff7d 100644 --- a/scada-common/network.lua +++ b/scada-common/network.lua @@ -77,26 +77,14 @@ end -- NIC: Network Interface Controller
-- utilizes HMAC-MD5 for message authentication, if enabled ----@param modem table modem to use +---@param modem Modem modem to use function network.nic(modem) local self = { connected = true, -- used to avoid costly MAC calculations if modem isn't even present channels = {} } - ---@class nic - ---@field open function - ---@field isOpen function - ---@field close function - ---@field closeAll function - ---@field isWireless function - ---@field getNameLocal function - ---@field getNamesRemote function - ---@field isPresentRemote function - ---@field getTypeRemote function - ---@field hasTypeRemote function - ---@field getMethodsRemote function - ---@field callRemote function + ---@class nic:Modem local public = {} -- check if this NIC has a connected modem @@ -104,7 +92,7 @@ function network.nic(modem) function public.is_connected() return self.connected end -- connect to a modem peripheral - ---@param reconnected_modem table + ---@param reconnected_modem Modem function public.connect(reconnected_modem) modem = reconnected_modem self.connected = true diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index e6a95e8d..7d6071ed 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -23,7 +23,7 @@ ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE local REPORT_FREQUENCY = 20 -- log every 20 faults per function local ppm_sys = { - mounts = {}, + mounts = {}, ---@type { [string]: ppm_entry } next_vid = 0, auto_cf = false, faulted = false, @@ -40,10 +40,10 @@ local function peri_init(iface) local self = { faulted = false, last_fault = "", - fault_counts = {}, + fault_counts = {}, ---@type { [string]: integer } auto_cf = true, - type = VIRTUAL_DEVICE_TYPE, - device = {} + type = VIRTUAL_DEVICE_TYPE, ---@type string + device = {} ---@type { [string]: function } } if iface ~= "__virtual__" then @@ -181,7 +181,7 @@ local function peri_init(iface) setmetatable(self.device, mt) ---@class ppm_entry - local entry = { type = self.type, dev = self.device } + local entry = { type = self.type, dev = self.device } return entry end @@ -284,10 +284,10 @@ end ---@param device table device table function ppm.unmount(device) if device then - for side, data in pairs(ppm_sys.mounts) do + for iface, data in pairs(ppm_sys.mounts) do if data.dev == device then - log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", side)) - ppm_sys.mounts[side] = nil + log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", iface)) + ppm_sys.mounts[iface] = nil break end end @@ -334,12 +334,12 @@ end -- list all available peripherals ---@nodiscard ----@return table names +---@return string[] names function ppm.list_avail() return peripheral.getNames() end -- list mounted peripherals ---@nodiscard ----@return table mounts +---@return { [string]: ppm_entry } mounts function ppm.list_mounts() local list = {} for k, v in pairs(ppm_sys.mounts) do list[k] = v end @@ -352,8 +352,8 @@ end ---@return string|nil iface CC peripheral interface function ppm.get_iface(device) if device then - for side, data in pairs(ppm_sys.mounts) do - if data.dev == device then return side end + for iface, data in pairs(ppm_sys.mounts) do + if data.dev == device then return iface end end end @@ -363,7 +363,7 @@ end -- get a mounted peripheral by side/interface ---@nodiscard ---@param iface string CC peripheral interface ----@return table|nil device function table +---@return { [string]: function }|nil device function table function ppm.get_periph(iface) if ppm_sys.mounts[iface] then return ppm_sys.mounts[iface].dev @@ -423,7 +423,7 @@ function ppm.get_fission_reactor() return ppm.get_device("fissionReactorLogicAda -- get the wireless modem (if multiple, returns the first)
-- if this is in a CraftOS emulated environment, wired modems will be used instead ---@nodiscard ----@return table|nil modem function table +---@return Modem|nil modem function table function ppm.get_wireless_modem() local w_modem = nil local emulated_env = periphemu ~= nil @@ -440,7 +440,7 @@ end -- list all connected monitors ---@nodiscard ----@return table monitors +---@return { [string]: ppm_entry } monitors function ppm.get_monitor_list() local list = {} diff --git a/scada-common/psil.lua b/scada-common/psil.lua index 09686bf5..f3d810ad 100644 --- a/scada-common/psil.lua +++ b/scada-common/psil.lua @@ -6,9 +6,10 @@ local util = require("scada-common.util") local psil = {} --- instantiate a new PSI layer +-- instantiate a new interconnect layer ---@nodiscard function psil.create() + ---@type { [string]: { subscribers: { notify: fun(param: any) }[], value: any } } interconnect table local ic = {} -- allocate a new interconnect field diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index fb3a50a2..b88180ad 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -113,10 +113,13 @@ assert(#dup_chk == rsio.NUM_PORTS, "port list malformed") local IO = IO_PORT -- list of all port names +---@type string[] local PORT_NAMES = {} + for k, v in pairs(IO) do PORT_NAMES[v] = k end -- list of all port I/O modes +---@type { [IO_PORT]: IO_MODE } local MODES = { [IO.F_SCRAM] = IO_MODE.DIGITAL_IN, [IO.F_ACK] = IO_MODE.DIGITAL_IN, @@ -233,6 +236,7 @@ end --#region Generic Checks +---@type string[] local RS_SIDES = rs.getSides() -- check if a port is valid diff --git a/scada-common/tcd.lua b/scada-common/tcd.lua index a3c920f1..f11c2d89 100644 --- a/scada-common/tcd.lua +++ b/scada-common/tcd.lua @@ -7,6 +7,7 @@ local util = require("scada-common.util") local tcd = {} +---@type { callback: function, duration: number, expiry: number }[] local registry = {} -- request a function to be called after the specified time diff --git a/scada-common/types.lua b/scada-common/types.lua index 2bf6ae13..ca1b8857 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -5,6 +5,66 @@ ---@class types local types = {} +--#region CC: TWEAKED CLASSES https://tweaked.cc + +---@class Redirect +---@field write fun(text: string) Write text at the current cursor position, moving the cursor to the end of the text. +---@field scroll fun(y: integer) Move all positions up (or down) by y pixels. +---@field getCursorPos fun() : x: integer, y: integer Get the position of the cursor. +---@field setCursorPos fun(x: integer, y: integer) Set the position of the cursor. +---@field getCursorBlink fun() : boolean Checks if the cursor is currently blinking. +---@field setCursorBlink fun(blink: boolean) Sets whether the cursor should be visible (and blinking) at the current cursor position. +---@field getSize fun() : width: integer, height: integer Get the size of the terminal. +---@field clear fun() Clears the terminal, filling it with the current background color. +---@field clearLine fun() Clears the line the cursor is currently on, filling it with the current background color. +---@field getTextColor fun() : color Return the color that new text will be written as. +---@field setTextColor fun(color: color) Set the colour that new text will be written as. +---@field getBackgroundColor fun() : color Return the current background color. +---@field setBackgroundColor fun(color: color) set the current background color. +---@field isColor fun() Determine if this terminal supports color. +---@field blit fun(text: string, textColor: string, backgroundColor: string) Writes text to the terminal with the specific foreground and background colors. +---@diagnostic disable-next-line: duplicate-doc-field +---@field setPaletteColor fun(index: color, color: integer) Set the palette for a specific color. +---@diagnostic disable-next-line: duplicate-doc-field +---@field setPaletteColor fun(index: color, r: number, g: number, b:number) Set the palette for a specific color. R/G/B are 0 to 1. +---@field getPaletteColor fun(color: color) : r: number, g: number, b:number Get the current palette for a specific color. + +---@class Window:Redirect +---@field getLine fun(y: integer) : content: string, fg: string, bg: string Get the buffered contents of a line in this window. +---@field setVisible fun(visible: boolean) Set whether this window is visible. Invisible windows will not be drawn to the screen until they are made visible again. +---@field isVisible fun() : visible: boolean Get whether this window is visible. Invisible windows will not be drawn to the screen until they are made visible again. +---@field redraw fun() Draw this window. This does nothing if the window is not visible. +---@field restoreCursor fun() Set the current terminal's cursor to where this window's cursor is. This does nothing if the window is not visible. +---@field getPosition fun() : x: integer, y: integer Get the position of the top left corner of this window. +---@field reposition fun(new_x: integer, new_y: integer, new_width?: integer, new_height?: integer, new_parent?: Redirect) Reposition or resize the given window. + +---@class Monitor:Redirect +---@field setTextScale fun(scale: number) Set the scale of this monitor. +---@field getTextScale fun() : number Get the monitor's current text scale. + +---@class Modem +---@field open fun(channel: integer) Open a channel on a modem. +---@field isOpen fun(channel: integer) : boolean Check if a channel is open. +---@field close fun(channel: integer) Close an open channel, meaning it will no longer receive messages. +---@field closeAll fun() Close all open channels. +---@field transmit fun(channel: integer, replyChannel: integer, payload: any) Sends a modem message on a certain channel. +---@field isWireless fun() : boolean Determine if this is a wired or wireless modem. +---@field getNamesRemote fun() : string[] List all remote peripherals on the wired network. +---@field isPresentRemote fun(name: string) : boolean Determine if a peripheral is available on this wired network. +---@field getTypeRemote fun(name: string) : string|nil Get the type of a peripheral is available on this wired network. +---@field hasTypeRemote fun(name: string, type: string) : boolean|nil Check a peripheral is of a particular . +---@field getMethodsRemote fun(name: string) : string[] Get all available methods for the remote peripheral with the given name. +---@field callRemote fun(remoteName: string, method: string, ...) : table Call a method on a peripheral on this wired network. +---@field getNameLocal fun() : string|nil Returns the network name of the current computer, if the modem is on. + +---@class Speaker +---@field playNote fun(instrument: string, volume?: number, pitch?: number) : success: boolean Plays a note block note through the speaker. +---@field playSound fun(name: string, volume?: number, pitch?: number) : success: boolean Plays a Minecraft sound through the speaker. +---@field playAudio fun(audio: number[], volume?: number) : success: boolean Attempt to stream some audio data to the speaker. +---@field stop fun() Stop all audio being played by this speaker. + +--#endregion + --#region CLASSES ---@class tank_fluid @@ -65,7 +125,84 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end ---@field type RTU_UNIT_TYPE ---@field index integer|false ---@field reactor integer ----@field rsio table|nil +---@field rsio IO_PORT[]|nil + +-- create a new reactor database +---@nodiscard +function types.new_reactor_db() + ---@class reactor_db + local db = { + auto_ack_token = 0, + last_status_update = 0, + control_state = false, + no_reactor = false, + formed = false, + rps_tripped = false, + rps_trip_cause = "ok", ---@type rps_trip_cause + max_op_temp_H2O = 1200, + max_op_temp_Na = 1200, + ---@class rps_status + rps_status = { + high_dmg = false, + high_temp = false, + low_cool = false, + ex_waste = false, + ex_hcool = false, + no_fuel = false, + fault = false, + timeout = false, + manual = false, + automatic = false, + sys_fail = false, + force_dis = false + }, + ---@class mek_status + mek_status = { + heating_rate = 0.0, + + status = false, + burn_rate = 0.0, + act_burn_rate = 0.0, + temp = 0.0, + damage = 0.0, + boil_eff = 0.0, + env_loss = 0.0, + + fuel = 0, + fuel_need = 0, + fuel_fill = 0.0, + waste = 0, + waste_need = 0, + waste_fill = 0.0, + ccool_type = types.FLUID.EMPTY_GAS, ---@type fluid + ccool_amnt = 0, + ccool_need = 0, + ccool_fill = 0.0, + hcool_type = types.FLUID.EMPTY_GAS, ---@type fluid + hcool_amnt = 0, + hcool_need = 0, + hcool_fill = 0.0 + }, + ---@class mek_struct + mek_struct = { + length = 0, + width = 0, + height = 0, + min_pos = types.new_zero_coordinate(), + max_pos = types.new_zero_coordinate(), + heat_cap = 0, + fuel_asm = 0, + fuel_sa = 0, + fuel_cap = 0, + waste_cap = 0, + ccool_cap = 0, + hcool_cap = 0, + max_burn = 0.0 + } + } + + return db +end --#endregion diff --git a/scada-common/util.lua b/scada-common/util.lua index 36a73fc9..86645f8e 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -24,7 +24,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.4.4" +util.version = "1.4.6" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 @@ -33,9 +33,9 @@ util.TICK_TIME_MS = 50 -- trinary operator ---@nodiscard ----@param cond boolean|nil condition ----@param a any return if true ----@param b any return if false +---@param cond any condition +---@param a any return if evaluated as true +---@param b any return if false or nil ---@return any value function util.trinary(cond, a, b) if cond then return a else return b end @@ -84,7 +84,7 @@ end -- does not behave exactly like C's strtok ---@param str string string to tokenize ---@param sep string separator to tokenize by ----@return table token_list +---@return string[] token_list function util.strtok(str, sep) local list = {} for part in string.gmatch(str, "([^" .. sep .. "]+)") do t_insert(list, part) end @@ -123,7 +123,7 @@ end ---@nodiscard ---@param str string ---@param limit integer line limit, must be greater than 0 ----@return table lines +---@return string[] lines function util.strwrap(str, limit) assert(limit > 0, "util.strwrap() limit not greater than 0") return cc_strings.wrap(str, limit) @@ -138,7 +138,7 @@ function util.strminw(str, width) return cc_strings.ensure_width(str, width) end -- concatenation with built-in to string ---@nodiscard ----@vararg any +---@param ... any ---@return string function util.concat(...) local args, strings = t_pack(...), {} @@ -152,7 +152,7 @@ util.c = util.concat -- sprintf implementation ---@nodiscard ---@param format string ----@vararg any +---@param ... any function util.sprintf(format, ...) return string.format(format, ...) end -- format a number string with commas as the thousands separator
@@ -343,8 +343,9 @@ end -- delete elements from a table if the passed function returns false when passed a table element
-- put briefly: deletes elements that return false, keeps elements that return true ----@param t table table to remove elements from ----@param f function should return false to delete an element when passed the element: f(elem) = true|false +---@generic Type +---@param t Type[] table to remove elements from +---@param f fun(t_elem: Type) : boolean should return false to delete an element when passed the element ---@param on_delete? function optional function to execute on deletion, passed the table element to be deleted as the parameter function util.filter_table(t, f, on_delete) local move_to = 1 @@ -366,9 +367,10 @@ function util.filter_table(t, f, on_delete) end -- check if a table contains the provided element +---@generic Type ---@nodiscard ----@param t table table to check ----@param element any element to check for +---@param t Type[] table to check +---@param element Type element to check for function util.table_contains(t, element) for i = 1, #t do if t[i] == element then return true end diff --git a/supervisor/config/facility.lua b/supervisor/config/facility.lua new file mode 100644 index 00000000..84c9c515 --- /dev/null +++ b/supervisor/config/facility.lua @@ -0,0 +1,435 @@ +local util = require("scada-common.util") + +local core = require("graphics.core") + +local Div = require("graphics.elements.Div") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") + +local Checkbox = require("graphics.elements.controls.Checkbox") +local PushButton = require("graphics.elements.controls.PushButton") +local Radio2D = require("graphics.elements.controls.Radio2D") +local RadioButton = require("graphics.elements.controls.RadioButton") + +local NumberField = require("graphics.elements.form.NumberField") + +local tri = util.trinary + +local cpair = core.cpair + +local self = { + vis_ftanks = {}, ---@type { line: Div, pipe_conn?: TextBox, pipe_chain?: TextBox, pipe_direct?: TextBox, label?: TextBox }[] + vis_utanks = {} ---@type { line: Div, label: TextBox }[] +} + +local facility = {} + +-- create the facility configuration view +---@param tool_ctl _svr_cfg_tool_ctl +---@param main_pane MultiPane +---@param cfg_sys [ svr_config, svr_config, svr_config, table, function ] +---@param fac_cfg Div +---@param style { [string]: cpair } +---@return MultiPane fac_pane +function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style) + local _, ini_cfg, tmp_cfg, _, _ = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5] + + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + + --#region Facility + + local fac_c_1 = Div{parent=fac_cfg,x=2,y=4,width=49} + local fac_c_2 = Div{parent=fac_cfg,x=2,y=4,width=49} + local fac_c_3 = Div{parent=fac_cfg,x=2,y=4,width=49} + local fac_c_4 = Div{parent=fac_cfg,x=2,y=4,width=49} + local fac_c_5 = Div{parent=fac_cfg,x=2,y=4,width=49} + local fac_c_6 = Div{parent=fac_cfg,x=2,y=4,width=49} + local fac_c_7 = Div{parent=fac_cfg,x=2,y=4,width=49} + + local fac_pane = MultiPane{parent=fac_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3,fac_c_4,fac_c_5,fac_c_6,fac_c_7}} + + TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)} + + TextBox{parent=fac_c_1,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."} + tool_ctl.num_units = NumberField{parent=fac_c_1,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg} + TextBox{parent=fac_c_1,x=7,y=5,text="reactors"} + + local nu_error = TextBox{parent=fac_c_1,x=8,y=14,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_num_units() + local count = tonumber(tool_ctl.num_units.get_value()) + if count ~= nil and count > 0 and count < 5 then + nu_error.hide(true) + tmp_cfg.UnitCount = count + + local confs = tool_ctl.cooling_elems + if count >= 2 then confs[2].line.show() else confs[2].line.hide(true) end + if count >= 3 then confs[3].line.show() else confs[3].line.hide(true) end + if count == 4 then confs[4].line.show() else confs[4].line.hide(true) end + + fac_pane.set_value(2) + else nu_error.show() end + end + + PushButton{parent=fac_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=fac_c_1,x=44,y=14,text="Next \x1a",callback=submit_num_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=fac_c_2,x=1,y=1,height=4,text="Please provide the reactor cooling configuration below. This includes the number of turbines, boilers, and if that reactor has a connection to a dynamic tank for emergency coolant."} + TextBox{parent=fac_c_2,x=1,y=6,text="UNIT TURBINES BOILERS HAS TANK CONNECTION?",fg_bg=g_lg_fg_bg} + + for i = 1, 4 do + local num_t, num_b, has_t = 1, 0, false + + if ini_cfg.CoolingConfig[i] then + local conf = ini_cfg.CoolingConfig[i] + if util.is_int(conf.TurbineCount) then num_t = math.min(3, math.max(1, conf.TurbineCount or 1)) end + if util.is_int(conf.BoilerCount) then num_b = math.min(2, math.max(0, conf.BoilerCount or 0)) end + has_t = conf.TankConnection == true + end + + local line = Div{parent=fac_c_2,x=1,y=7+i,height=1} + + TextBox{parent=line,text="Unit "..i,width=6} + local turbines = NumberField{parent=line,x=9,y=1,width=5,max_chars=2,default=num_t,min=1,max=3,fg_bg=bw_fg_bg} + local boilers = NumberField{parent=line,x=20,y=1,width=5,max_chars=2,default=num_b,min=0,max=2,fg_bg=bw_fg_bg} + local tank = Checkbox{parent=line,x=30,y=1,label="Is Connected",default=has_t,box_fg_bg=cpair(colors.yellow,colors.black)} + + tool_ctl.cooling_elems[i] = { line = line, turbines = turbines, boilers = boilers, tank = tank } + end + + local cool_err = TextBox{parent=fac_c_2,x=8,y=14,width=33,text="Please fill out all fields.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_cooling() + local any_missing = false + for i = 1, tmp_cfg.UnitCount do + local conf = tool_ctl.cooling_elems[i] + any_missing = any_missing or (tonumber(conf.turbines.get_value()) == nil) + any_missing = any_missing or (tonumber(conf.boilers.get_value()) == nil) + end + + if any_missing then + cool_err.show() + else + local any_has_tank = false + + tmp_cfg.CoolingConfig = {} + for i = 1, tmp_cfg.UnitCount do + local conf = tool_ctl.cooling_elems[i] + -- already verified fields are numbers + tmp_cfg.CoolingConfig[i] = { + TurbineCount = tonumber(conf.turbines.get_value()) --[[@as number]], + BoilerCount = tonumber(conf.boilers.get_value()) --[[@as number]], + TankConnection = conf.tank.get_value() + } + + if conf.tank.get_value() then any_has_tank = true end + end + + for i = 1, 4 do + local elem = tool_ctl.tank_elems[i] + if i <= tmp_cfg.UnitCount then + elem.div.show() + if tmp_cfg.CoolingConfig[i].TankConnection then + elem.no_tank.hide() + elem.tank_opt.show() + else + elem.tank_opt.hide(true) + elem.no_tank.show() + end + else elem.div.hide(true) end + end + + if any_has_tank then fac_pane.set_value(3) else main_pane.set_value(3) end + end + end + + PushButton{parent=fac_c_2,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=fac_c_2,x=44,y=14,text="Next \x1a",callback=submit_cooling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=fac_c_3,x=1,y=1,height=6,text="You have set one or more of your units to use dynamic tanks for emergency coolant. You have two paths for configuration. The first is to assign dynamic tanks to reactor units; one tank per reactor, only connected to that reactor. RTU configurations must also assign it as such."} + TextBox{parent=fac_c_3,x=1,y=8,height=3,text="Alternatively, you can configure them as facility tanks to connect to multiple reactor units. These can intermingle with unit-specific tanks."} + + tool_ctl.en_fac_tanks = Checkbox{parent=fac_c_3,x=1,y=12,label="Use Facility Dynamic Tanks",default=ini_cfg.FacilityTankMode>0,box_fg_bg=cpair(colors.yellow,colors.black)} + + local function submit_en_fac_tank() + if tool_ctl.en_fac_tanks.get_value() then + fac_pane.set_value(4) + tmp_cfg.FacilityTankMode = tri(tmp_cfg.FacilityTankMode == 0, 1, math.min(8, math.max(1, ini_cfg.FacilityTankMode))) + else + tmp_cfg.FacilityTankMode = 0 + tmp_cfg.FacilityTankDefs = {} + fac_pane.set_value(7) + end + end + + PushButton{parent=fac_c_3,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=fac_c_3,x=44,y=14,text="Next \x1a",callback=submit_en_fac_tank,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=fac_c_4,x=1,y=1,height=4,text="Please set unit connections to dynamic tanks, selecting at least one facility tank. The layout for facility tanks will be configured next."} + + for i = 1, 4 do + local val = math.max(1, ini_cfg.FacilityTankDefs[i] or 2) + local div = Div{parent=fac_c_4,x=1,y=3+(2*i),height=2} + + TextBox{parent=div,x=1,y=1,width=33,text="Unit "..i.." will be connected to..."} + TextBox{parent=div,x=6,y=2,width=3,text="..."} + local tank_opt = Radio2D{parent=div,x=9,y=2,rows=1,columns=2,default=val,options={"its own Unit Tank","a Facility Tank"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg} + local no_tank = TextBox{parent=div,x=9,y=2,width=34,text="no tank (as you set two steps ago)",fg_bg=cpair(colors.gray,colors.lightGray),hidden=true} + + tool_ctl.tank_elems[i] = { div = div, tank_opt = tank_opt, no_tank = no_tank } + end + + local tank_err = TextBox{parent=fac_c_4,x=8,y=14,width=33,text="You selected no facility tanks.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function hide_fconn(i) + if i > 1 then self.vis_ftanks[i].pipe_conn.hide(true) + else self.vis_ftanks[i].line.hide(true) end + end + + local function submit_tank_defs() + local any_fac = false + + tmp_cfg.FacilityTankDefs = {} + for i = 1, tmp_cfg.UnitCount do + local def + + if tmp_cfg.CoolingConfig[i].TankConnection then + def = tool_ctl.tank_elems[i].tank_opt.get_value() + any_fac = any_fac or (def == 2) + else def = 0 end + + if def == 1 then + self.vis_utanks[i].line.show() + self.vis_utanks[i].label.set_value("Tank U" .. i) + hide_fconn(i) + else + if def == 2 then + if i > 1 then self.vis_ftanks[i].pipe_conn.show() + else self.vis_ftanks[i].line.show() end + else hide_fconn(i) end + self.vis_utanks[i].line.hide(true) + end + + tmp_cfg.FacilityTankDefs[i] = def + end + + for i = tmp_cfg.UnitCount + 1, 4 do + self.vis_utanks[i].line.hide(true) + end + + tool_ctl.vis_draw(tmp_cfg.FacilityTankMode) + + if any_fac then + tank_err.hide(true) + fac_pane.set_value(5) + else tank_err.show() end + end + + PushButton{parent=fac_c_4,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=fac_c_4,x=44,y=14,text="Next \x1a",callback=submit_tank_defs,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=fac_c_5,x=1,y=1,text="Please select your dynamic tank layout."} + TextBox{parent=fac_c_5,x=12,y=3,text="Facility Tanks Unit Tanks",fg_bg=g_lg_fg_bg} + + --#region Tank Layout Visualizer + + local pipe_cpair = cpair(colors.blue,colors.lightGray) + + local vis = Div{parent=fac_c_5,x=14,y=5,height=7} + + local vis_unit_list = TextBox{parent=vis,x=15,y=1,width=6,height=7,text="Unit 1\n\nUnit 2\n\nUnit 3\n\nUnit 4"} + + -- draw unit tanks and their pipes + for i = 1, 4 do + local line = Div{parent=vis,x=22,y=(i*2)-1,width=13,height=1} + TextBox{parent=line,width=5,text=string.rep("\x8c",5),fg_bg=pipe_cpair} + local label = TextBox{parent=line,x=7,y=1,width=7,text="Tank ?"} + self.vis_utanks[i] = { line = line, label = label } + end + + -- draw facility tank connections + + local ftank_1 = Div{parent=vis,x=1,y=1,width=13,height=1} + TextBox{parent=ftank_1,width=7,text="Tank F1"} + self.vis_ftanks[1] = { + line = ftank_1, pipe_direct = TextBox{parent=ftank_1,x=9,y=1,width=5,text=string.rep("\x8c",5),fg_bg=pipe_cpair} + } + + for i = 2, 4 do + local line = Div{parent=vis,x=1,y=(i-1)*2,width=13,height=2} + local pipe_conn = TextBox{parent=line,x=13,y=2,width=1,text="\x8c",fg_bg=pipe_cpair} + local pipe_chain = TextBox{parent=line,x=12,y=1,width=1,height=2,text="\x95\n\x8d",fg_bg=pipe_cpair} + local pipe_direct = TextBox{parent=line,x=9,y=2,width=4,text="\x8c\x8c\x8c\x8c",fg_bg=pipe_cpair} + local label = TextBox{parent=line,x=1,y=2,width=7,text=""} + self.vis_ftanks[i] = { line = line, pipe_conn = pipe_conn, pipe_chain = pipe_chain, pipe_direct = pipe_direct, label = label } + end + + -- draw the pipe visualization + ---@param mode integer pipe mode + function tool_ctl.vis_draw(mode) + -- is a facility tank connected to this unit + ---@param i integer unit 1 - 4 + ---@return boolean connected + local function is_ft(i) return tmp_cfg.FacilityTankDefs[i] == 2 end + + local u_text = "" + for i = 1, tmp_cfg.UnitCount do + u_text = u_text .. "Unit " .. i .. "\n\n" + end + + vis_unit_list.set_value(u_text) + + local vis_ftanks = self.vis_ftanks + local next_idx = 1 + + if is_ft(1) then + next_idx = 2 + + if (mode == 1 and (is_ft(2) or is_ft(3) or is_ft(4))) or (mode == 2 and (is_ft(2) or is_ft(3))) or ((mode == 3 or mode == 5) and is_ft(2)) then + vis_ftanks[1].pipe_direct.set_value("\x8c\x8c\x8c\x9c\x8c") + else + vis_ftanks[1].pipe_direct.set_value(string.rep("\x8c",5)) + end + end + + local _2_12_need_passt = (mode == 1 and (is_ft(3) or is_ft(4))) or (mode == 2 and is_ft(3)) + local _2_46_need_chain = (mode == 4 and (is_ft(3) or is_ft(4))) or (mode == 6 and is_ft(3)) + + if is_ft(2) then + vis_ftanks[2].label.set_value("Tank F" .. next_idx) + + if (mode < 4 or mode == 5) and is_ft(1) then + vis_ftanks[2].label.hide(true) + vis_ftanks[2].pipe_direct.hide(true) + if _2_12_need_passt then + vis_ftanks[2].pipe_chain.set_value("\x95\n\x9d") + else + vis_ftanks[2].pipe_chain.set_value("\x95\n\x8d") + end + vis_ftanks[2].pipe_chain.show() + else + vis_ftanks[2].label.show() + next_idx = next_idx + 1 + + vis_ftanks[2].pipe_chain.hide(true) + if _2_12_need_passt or _2_46_need_chain then + vis_ftanks[2].pipe_direct.set_value("\x8c\x8c\x8c\x9c") + else + vis_ftanks[2].pipe_direct.set_value("\x8c\x8c\x8c\x8c") + end + vis_ftanks[2].pipe_direct.show() + end + + vis_ftanks[2].line.show() + elseif is_ft(1) and _2_12_need_passt then + vis_ftanks[2].label.hide(true) + vis_ftanks[2].pipe_direct.hide(true) + vis_ftanks[2].pipe_chain.set_value("\x95\n\x95") + vis_ftanks[2].pipe_chain.show() + vis_ftanks[2].line.show() + else + vis_ftanks[2].line.hide(true) + end + + if is_ft(3) then + vis_ftanks[3].label.set_value("Tank F" .. next_idx) + + if (mode < 3 and (is_ft(1) or is_ft(2))) or ((mode == 4 or mode == 6) and is_ft(2)) then + vis_ftanks[3].label.hide(true) + vis_ftanks[3].pipe_direct.hide(true) + if (mode == 1 or mode == 4) and is_ft(4) then + vis_ftanks[3].pipe_chain.set_value("\x95\n\x9d") + else + vis_ftanks[3].pipe_chain.set_value("\x95\n\x8d") + end + vis_ftanks[3].pipe_chain.show() + else + vis_ftanks[3].label.show() + next_idx = next_idx + 1 + + vis_ftanks[3].pipe_chain.hide(true) + if (mode == 1 or mode == 3 or mode == 4 or mode == 7) and is_ft(4) then + vis_ftanks[3].pipe_direct.set_value("\x8c\x8c\x8c\x9c") + else + vis_ftanks[3].pipe_direct.set_value("\x8c\x8c\x8c\x8c") + end + vis_ftanks[3].pipe_direct.show() + end + + vis_ftanks[3].line.show() + elseif (mode == 1 and is_ft(4) and (is_ft(1) or is_ft(2))) or (mode == 4 and is_ft(2) and is_ft(4)) then + vis_ftanks[3].label.hide(true) + vis_ftanks[3].pipe_direct.hide(true) + vis_ftanks[3].pipe_chain.set_value("\x95\n\x95") + vis_ftanks[3].pipe_chain.show() + vis_ftanks[3].line.show() + else + vis_ftanks[3].line.hide(true) + end + + if is_ft(4) then + vis_ftanks[4].label.set_value("Tank F" .. next_idx) + + if (mode == 1 and (is_ft(1) or is_ft(2) or is_ft(3))) or ((mode == 3 or mode == 7) and is_ft(3)) or (mode == 4 and (is_ft(2) or is_ft(3))) then + vis_ftanks[4].label.hide(true) + vis_ftanks[4].pipe_direct.hide(true) + vis_ftanks[4].pipe_chain.show() + else + vis_ftanks[4].label.show() + vis_ftanks[4].pipe_chain.hide(true) + vis_ftanks[4].pipe_direct.show() + end + + vis_ftanks[4].line.show() + else + vis_ftanks[4].line.hide(true) + end + end + + local function change_mode(mode) + tmp_cfg.FacilityTankMode = mode + tool_ctl.vis_draw(mode) + end + + local tank_modes = { "Mode 1", "Mode 2", "Mode 3", "Mode 4", "Mode 5", "Mode 6", "Mode 7", "Mode 8" } + tool_ctl.tank_mode = RadioButton{parent=fac_c_5,x=1,y=4,callback=change_mode,default=math.max(1,ini_cfg.FacilityTankMode),options=tank_modes,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow} + + --#endregion + + PushButton{parent=fac_c_5,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=fac_c_5,x=44,y=14,text="Next \x1a",callback=function()fac_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + PushButton{parent=fac_c_5,x=8,y=14,min_width=7,text="About",callback=function()fac_pane.set_value(6)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg} + + TextBox{parent=fac_c_6,height=3,text="This visualization tool shows the pipe connections required for a particular dynamic tank configuration you have selected."} + TextBox{parent=fac_c_6,y=5,height=4,text="Examples: A U2 tank should be configured on an RTU as the dynamic tank for unit #2. An F3 tank should be configured on an RTU as the #3 dynamic tank for the facility."} + TextBox{parent=fac_c_6,y=10,height=3,text="Some modes may look the same if you are not using 4 total reactor units. The wiki has details. Modes that look the same will function the same.",fg_bg=g_lg_fg_bg} + + PushButton{parent=fac_c_6,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=fac_c_7,height=6,text="Charge control provides automatic control to maintain an induction matrix charge level. In order to have smoother control, reactors that were activated will be held on at 0.01 mB/t for a short period before allowing them to turn off. This minimizes overshooting the charge target."} + TextBox{parent=fac_c_7,y=8,height=3,text="You can extend this to a full minute to minimize reactors flickering on/off, but there may be more overshoot of the target."} + + local ext_idling = Checkbox{parent=fac_c_7,x=1,y=12,label="Enable Extended Idling",default=ini_cfg.ExtChargeIdling,box_fg_bg=cpair(colors.yellow,colors.black)} + + local function back_from_idling() + fac_pane.set_value(tri(tmp_cfg.FacilityTankMode == 0, 3, 5)) + end + + local function submit_idling() + tmp_cfg.ExtChargeIdling = ext_idling.get_value() + main_pane.set_value(3) + end + + PushButton{parent=fac_c_7,x=1,y=14,text="\x1b Back",callback=back_from_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=fac_c_7,x=44,y=14,text="Next \x1a",callback=submit_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + return fac_pane +end + +return facility diff --git a/supervisor/config/system.lua b/supervisor/config/system.lua new file mode 100644 index 00000000..9e0b0c73 --- /dev/null +++ b/supervisor/config/system.lua @@ -0,0 +1,625 @@ +local log = require("scada-common.log") +local util = require("scada-common.util") + +local core = require("graphics.core") +local themes = require("graphics.themes") + +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") + +local Checkbox = require("graphics.elements.controls.Checkbox") +local PushButton = require("graphics.elements.controls.PushButton") +local RadioButton = require("graphics.elements.controls.RadioButton") + +local NumberField = require("graphics.elements.form.NumberField") +local TextField = require("graphics.elements.form.TextField") + +local IndLight = require("graphics.elements.indicators.IndicatorLight") + +local tri = util.trinary + +local cpair = core.cpair + +local RIGHT = core.ALIGN.RIGHT + +local self = { + importing_legacy = false, + + show_auth_key = nil, ---@type function + show_key_btn = nil, ---@type PushButton + auth_key_textbox = nil, ---@type TextBox + auth_key_value = "" +} + +local system = {} + +-- create the system configuration view +---@param tool_ctl _svr_cfg_tool_ctl +---@param main_pane MultiPane +---@param cfg_sys [ svr_config, svr_config, svr_config, { [1]: string, [2]: string, [3]: any }[], function ] +---@param divs Div[] +---@param fac_pane MultiPane +---@param style { [string]: cpair } +---@param exit function +function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit) + local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5] + local net_cfg, log_cfg, clr_cfg, summary, import_err = divs[1], divs[2], divs[3], divs[4], divs[5] + + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + + --#region Network + + local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49} + local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49} + local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49} + local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49} + + local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}} + + TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} + + TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."} + TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"} + local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=9,width=11,text="PLC Channel"} + local plc_chan = NumberField{parent=net_c_1,x=21,y=9,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=29,y=9,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=10,width=19,text="RTU Gateway Channel"} + local rtu_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=29,y=10,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=11,width=19,text="Coordinator Channel"} + local crd_chan = NumberField{parent=net_c_1,x=21,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=29,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"} + local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg} + + local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_channels() + local svr_c, plc_c, rtu_c = tonumber(svr_chan.get_value()), tonumber(plc_chan.get_value()), tonumber(rtu_chan.get_value()) + local crd_c, pkt_c = tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value()) + if svr_c ~= nil and plc_c ~= nil and rtu_c ~= nil and crd_c ~= nil and pkt_c ~= nil then + tmp_cfg.SVR_Channel, tmp_cfg.PLC_Channel, tmp_cfg.RTU_Channel = svr_c, plc_c, rtu_c + tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = crd_c, pkt_c + net_pane.set_value(2) + chan_err.hide(true) + else chan_err.show() end + end + + PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_2,x=1,y=1,text="Please set the connection timeouts below."} + TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_2,x=1,y=8,width=11,text="PLC Timeout"} + local plc_timeout = NumberField{parent=net_c_2,x=21,y=8,width=7,default=ini_cfg.PLC_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} + + TextBox{parent=net_c_2,x=1,y=9,width=19,text="RTU Gateway Timeout"} + local rtu_timeout = NumberField{parent=net_c_2,x=21,y=9,width=7,default=ini_cfg.RTU_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} + + TextBox{parent=net_c_2,x=1,y=10,width=19,text="Coordinator Timeout"} + local crd_timeout = NumberField{parent=net_c_2,x=21,y=10,width=7,default=ini_cfg.CRD_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} + + TextBox{parent=net_c_2,x=1,y=11,width=14,text="Pocket Timeout"} + local pkt_timeout = NumberField{parent=net_c_2,x=21,y=11,width=7,default=ini_cfg.PKT_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} + + TextBox{parent=net_c_2,x=29,y=8,height=4,width=7,text="seconds\nseconds\nseconds\nseconds",fg_bg=g_lg_fg_bg} + + local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_timeouts() + local plc_cto, rtu_cto, crd_cto, pkt_cto = tonumber(plc_timeout.get_value()), tonumber(rtu_timeout.get_value()), tonumber(crd_timeout.get_value()), tonumber(pkt_timeout.get_value()) + if plc_cto ~= nil and rtu_cto ~= nil and crd_cto ~= nil and pkt_cto ~= nil then + tmp_cfg.PLC_Timeout, tmp_cfg.RTU_Timeout, tmp_cfg.CRD_Timeout, tmp_cfg.PKT_Timeout = plc_cto, rtu_cto, crd_cto, pkt_cto + net_pane.set_value(3) + ct_err.hide(true) + else ct_err.show() end + end + + PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."} + TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg} + TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg} + + local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg} + + local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_tr() + local range_val = tonumber(range.get_value()) + if range_val ~= nil then + tmp_cfg.TrustedRange = range_val + net_pane.set_value(4) + tr_err.hide(true) + else tr_err.show() end + end + + PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} + TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_4,x=1,y=11,text="Facility Auth Key"} + local key, _ = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg} + + local function censor_key(enable) key.censor(tri(enable, "*", nil)) end + + local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} + + hide_key.set_value(true) + censor_key(true) + + local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_auth() + local v = key.get_value() + if string.len(v) == 0 or string.len(v) >= 8 then + tmp_cfg.AuthKey = key.get_value() + main_pane.set_value(4) + key_err.hide(true) + else key_err.show() end + end + + PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Logging + + local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} + + TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} + + TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."} + + TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} + local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} + + TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"} + local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg} + + local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} + TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg} + + local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_log() + if path.get_value() ~= "" then + path_err.hide(true) + tmp_cfg.LogMode = mode.get_value() - 1 + tmp_cfg.LogPath = path.get_value() + tmp_cfg.LogDebug = en_dbg.get_value() + tool_ctl.color_apply.hide(true) + tool_ctl.color_next.show() + main_pane.set_value(5) + else path_err.show() end + end + + PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Color Options + + local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49} + local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49} + local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49} + local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49} + + local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}} + + TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)} + + TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color theme for the front panel."} + TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg} + + TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"} + local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} + + TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."} + + TextBox{parent=clr_c_2,x=21,y=7,text="Preview"} + local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)} + _ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)} + _ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)} + local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true} + local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true} + + local function recolor(value) + local c = themes.smooth_stone.color_modes[value] + + if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then + b_off.hide() + g_off.show() + else + g_off.hide() + b_off.show() + end + + if #c == 0 then + for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end + else + term.setPaletteColor(colors.green, c[1].hex) + term.setPaletteColor(colors.yellow, c[2].hex) + term.setPaletteColor(colors.red, c[3].hex) + end + end + + TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"} + local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} + + TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg} + + PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + local function back_from_colors() + main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 4)) + tool_ctl.jumped_to_color = false + recolor(1) + end + + local function show_access() + clr_pane.set_value(2) + recolor(c_mode.get_value()) + end + + local function submit_colors() + tmp_cfg.FrontPanelTheme = fp_theme.get_value() + tmp_cfg.ColorMode = c_mode.get_value() + + if tool_ctl.jumped_to_color then + settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme) + settings.set("ColorMode", tmp_cfg.ColorMode) + + if settings.save("/supervisor.settings") then + load_settings(settings_cfg, true) + load_settings(ini_cfg) + clr_pane.set_value(3) + else + clr_pane.set_value(4) + end + else + tool_ctl.gen_summary(tmp_cfg) + tool_ctl.viewing_config = false + self.importing_legacy = false + tool_ctl.settings_apply.show() + main_pane.set_value(6) + end + end + + PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + + tool_ctl.color_apply.hide(true) + + local function c_go_home() + main_pane.set_value(1) + clr_pane.set_value(1) + end + + TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"} + PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} + PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Summary and Saving + + local sum_c_1 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_2 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_3 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_4 = Div{parent=summary,x=2,y=4,width=49} + + local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}} + + TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)} + + local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local function back_from_settings() + if tool_ctl.viewing_config or self.importing_legacy then + main_pane.set_value(1) + tool_ctl.viewing_config = false + self.importing_legacy = false + tool_ctl.settings_apply.show() + else + main_pane.set_value(5) + end + end + + ---@param element graphics_element + ---@param data any + local function try_set(element, data) + if data ~= nil then element.set_value(data) end + end + + local function save_and_continue() + for _, field in ipairs(fields) do + local k, v = field[1], tmp_cfg[field[1]] + if v == nil then settings.unset(k) else settings.set(k, v) end + end + + if settings.save("/supervisor.settings") then + load_settings(settings_cfg, true) + load_settings(ini_cfg) + + try_set(tool_ctl.num_units, ini_cfg.UnitCount) + try_set(tool_ctl.tank_mode, ini_cfg.FacilityTankMode) + try_set(svr_chan, ini_cfg.SVR_Channel) + try_set(plc_chan, ini_cfg.PLC_Channel) + try_set(rtu_chan, ini_cfg.RTU_Channel) + try_set(crd_chan, ini_cfg.CRD_Channel) + try_set(pkt_chan, ini_cfg.PKT_Channel) + try_set(plc_timeout, ini_cfg.PLC_Timeout) + try_set(rtu_timeout, ini_cfg.RTU_Timeout) + try_set(crd_timeout, ini_cfg.CRD_Timeout) + try_set(pkt_timeout, ini_cfg.PKT_Timeout) + try_set(range, ini_cfg.TrustedRange) + try_set(key, ini_cfg.AuthKey) + try_set(mode, ini_cfg.LogMode) + try_set(path, ini_cfg.LogPath) + try_set(en_dbg, ini_cfg.LogDebug) + try_set(fp_theme, ini_cfg.FrontPanelTheme) + try_set(c_mode, ini_cfg.ColorMode) + + for i = 1, #ini_cfg.CoolingConfig do + local cfg, elems = ini_cfg.CoolingConfig[i], tool_ctl.cooling_elems[i] + try_set(elems.boilers, cfg.BoilerCount) + try_set(elems.turbines, cfg.TurbineCount) + try_set(elems.tank, cfg.TankConnection) + end + + for i = 1, #ini_cfg.FacilityTankDefs do + try_set(tool_ctl.tank_elems[i].tank_opt, ini_cfg.FacilityTankDefs[i]) + end + + tool_ctl.en_fac_tanks.set_value(ini_cfg.FacilityTankMode > 0) + + tool_ctl.view_cfg.enable() + + if self.importing_legacy then + self.importing_legacy = false + sum_pane.set_value(3) + else + sum_pane.set_value(2) + end + else + sum_pane.set_value(4) + end + end + + PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + self.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + + TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"} + + local function go_home() + main_pane.set_value(1) + fac_pane.set_value(1) + net_pane.set_value(1) + clr_pane.set_value(1) + sum_pane.set_value(1) + end + + PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + + TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."} + + local function delete_legacy() + fs.delete("/supervisor/config.lua") + exit() + end + + PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)} + + TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} + PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + + --#endregion + + --#region Import Error + + local i_err = Div{parent=import_err,x=2,y=4,width=49} + + TextBox{parent=import_err,x=1,y=2,text=" Import Error",fg_bg=cpair(colors.black,colors.red)} + TextBox{parent=i_err,x=1,y=1,text="There is a problem with your config.lua file:"} + + local import_err_msg = TextBox{parent=i_err,x=1,y=3,height=6,text=""} + + PushButton{parent=i_err,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=i_err,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + + --#endregion + + --#region Tool Functions + + -- load a legacy config file + function tool_ctl.load_legacy() + local config = require("supervisor.config") + + tmp_cfg.UnitCount = config.NUM_REACTORS + + if config.REACTOR_COOLING == nil or tmp_cfg.UnitCount ~= #config.REACTOR_COOLING then + import_err_msg.set_value("Cooling configuration table length must match the number of units.") + main_pane.set_value(8) + return + end + + for i = 1, tmp_cfg.UnitCount do + local cfg = config.REACTOR_COOLING[i] + + if type(cfg) ~= "table" then + import_err_msg.set_value("Cooling configuration for unit " .. i .. " must be a table.") + main_pane.set_value(8) + return + end + + tmp_cfg.CoolingConfig[i] = { BoilerCount = cfg.BOILERS or 0, TurbineCount = cfg.TURBINES or 1, TankConnection = cfg.TANK or false } + end + + tmp_cfg.FacilityTankMode = config.FAC_TANK_MODE + + if not (util.is_int(tmp_cfg.FacilityTankMode) and tmp_cfg.FacilityTankMode >= 0 and tmp_cfg.FacilityTankMode <= 8) then + import_err_msg.set_value("Invalid tank mode present in config. FAC_TANK_MODE must be a number 0 through 8.") + main_pane.set_value(8) + return + end + + if config.FAC_TANK_MODE > 0 then + if config.FAC_TANK_DEFS == nil or tmp_cfg.UnitCount ~= #config.FAC_TANK_DEFS then + import_err_msg.set_value("Facility tank definitions table length must match the number of units when using facility tanks.") + main_pane.set_value(8) + return + end + + for i = 1, tmp_cfg.UnitCount do + tmp_cfg.FacilityTankDefs[i] = config.FAC_TANK_DEFS[i] + end + else + tmp_cfg.FacilityTankMode = 0 + tmp_cfg.FacilityTankDefs = {} + end + + tmp_cfg.SVR_Channel = config.SVR_CHANNEL + tmp_cfg.PLC_Channel = config.PLC_CHANNEL + tmp_cfg.RTU_Channel = config.RTU_CHANNEL + tmp_cfg.CRD_Channel = config.CRD_CHANNEL + tmp_cfg.PKT_Channel = config.PKT_CHANNEL + + tmp_cfg.PLC_Timeout = config.PLC_TIMEOUT + tmp_cfg.RTU_Timeout = config.RTU_TIMEOUT + tmp_cfg.CRD_Timeout = config.CRD_TIMEOUT + tmp_cfg.PKT_Timeout = config.PKT_TIMEOUT + + tmp_cfg.TrustedRange = config.TRUSTED_RANGE + tmp_cfg.AuthKey = config.AUTH_KEY or "" + tmp_cfg.LogMode = config.LOG_MODE + tmp_cfg.LogPath = config.LOG_PATH + tmp_cfg.LogDebug = config.LOG_DEBUG or false + + tool_ctl.gen_summary(tmp_cfg) + sum_pane.set_value(1) + main_pane.set_value(6) + self.importing_legacy = true + end + + -- expose the auth key on the summary page + function self.show_auth_key() + self.show_key_btn.disable() + self.auth_key_textbox.set_value(self.auth_key_value) + end + + -- generate the summary list + ---@param cfg svr_config + function tool_ctl.gen_summary(cfg) + setting_list.remove_all() + + local alternate = false + local inner_width = setting_list.get_width() - 1 + + self.show_key_btn.enable() + self.auth_key_value = cfg.AuthKey or "" -- to show auth key + + for i = 1, #fields do + local f = fields[i] + local height = 1 + local label_w = string.len(f[2]) + local val_max_w = (inner_width - label_w) + 1 + local raw = cfg[f[1]] + local val = util.strval(raw) + + if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) + elseif f[1] == "LogMode" then val = tri(raw == log.MODE.APPEND, "append", "replace") + elseif f[1] == "FrontPanelTheme" then + val = util.strval(themes.fp_theme_name(raw)) + elseif f[1] == "ColorMode" then + val = util.strval(themes.color_mode_name(raw)) + elseif f[1] == "CoolingConfig" and type(cfg.CoolingConfig) == "table" then + val = "" + + for idx = 1, #cfg.CoolingConfig do + local ccfg = cfg.CoolingConfig[idx] + local b_plural = tri(ccfg.BoilerCount == 1, "", "s") + local t_plural = tri(ccfg.TurbineCount == 1, "", "s") + local tank = tri(ccfg.TankConnection, "has tank conn", "no tank conn") + val = val .. tri(idx == 1, "", "\n") .. + util.sprintf(" \x07 unit %d - %d boiler%s, %d turbine%s, %s", idx, ccfg.BoilerCount, b_plural, ccfg.TurbineCount, t_plural, tank) + end + + if val == "" then val = "no facility tanks" end + elseif f[1] == "FacilityTankMode" and raw == 0 then val = "0 (n/a, unit mode)" + elseif f[1] == "FacilityTankDefs" and type(cfg.FacilityTankDefs) == "table" then + val = "" + + for idx = 1, #cfg.FacilityTankDefs do + local t_mode = "not connected to a tank" + if cfg.FacilityTankDefs[idx] == 1 then + t_mode = "connected to its unit tank" + elseif cfg.FacilityTankDefs[idx] == 2 then + t_mode = "connected to a facility tank" + end + + val = val .. tri(idx == 1, "", "\n") .. util.sprintf(" \x07 unit %d - %s", idx, t_mode) + end + + if val == "" then val = "no facility tanks" end + end + + if val == "nil" then val = "" end + + local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) + alternate = not alternate + + if string.len(val) > val_max_w then + local lines = util.strwrap(val, inner_width) + height = #lines + 1 + end + + local line = Div{parent=setting_list,height=height,fg_bg=c} + TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} + + local textbox + if height > 1 then + textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1} + else + textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT} + end + + if f[1] == "AuthKey" then self.auth_key_textbox = textbox end + end + end + + --#endregion +end + +return system diff --git a/supervisor/configure.lua b/supervisor/configure.lua index 0c90538c..aaab65e2 100644 --- a/supervisor/configure.lua +++ b/supervisor/configure.lua @@ -6,24 +6,19 @@ local log = require("scada-common.log") local tcd = require("scada-common.tcd") local util = require("scada-common.util") +local facility = require("supervisor.config.facility") +local system = require("supervisor.config.system") + local core = require("graphics.core") local themes = require("graphics.themes") -local DisplayBox = require("graphics.elements.displaybox") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") - -local CheckBox = require("graphics.elements.controls.checkbox") -local PushButton = require("graphics.elements.controls.push_button") -local Radio2D = require("graphics.elements.controls.radio_2d") -local RadioButton = require("graphics.elements.controls.radio_button") - -local NumberField = require("graphics.elements.form.number_field") -local TextField = require("graphics.elements.form.text_field") +local DisplayBox = require("graphics.elements.DisplayBox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local IndLight = require("graphics.elements.indicators.light") +local PushButton = require("graphics.elements.controls.PushButton") local println = util.println local tri = util.trinary @@ -31,7 +26,6 @@ local tri = util.trinary local cpair = core.cpair local CENTER = core.ALIGN.CENTER -local RIGHT = core.ALIGN.RIGHT -- changes to the config data/format to let the user know local changes = { @@ -44,69 +38,64 @@ local configurator = {} local style = {} -style.root = cpair(colors.black, colors.lightGray) -style.header = cpair(colors.white, colors.gray) +style.root = cpair(colors.black, colors.lightGray) +style.header = cpair(colors.white, colors.gray) -style.colors = themes.smooth_stone.colors +style.colors = themes.smooth_stone.colors -local bw_fg_bg = cpair(colors.black, colors.white) -local g_lg_fg_bg = cpair(colors.gray, colors.lightGray) -local nav_fg_bg = bw_fg_bg -local btn_act_fg_bg = cpair(colors.white, colors.gray) +style.bw_fg_bg = cpair(colors.black, colors.white) +style.g_lg_fg_bg = cpair(colors.gray, colors.lightGray) +style.nav_fg_bg = style.bw_fg_bg +style.btn_act_fg_bg = cpair(colors.white, colors.gray) +style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white) ---@class _svr_cfg_tool_ctl local tool_ctl = { ask_config = false, has_config = false, viewing_config = false, - importing_legacy = false, jumped_to_color = false, - view_cfg = nil, ---@type graphics_element - color_cfg = nil, ---@type graphics_element - color_next = nil, ---@type graphics_element - color_apply = nil, ---@type graphics_element - settings_apply = nil, ---@type graphics_element + view_cfg = nil, ---@type PushButton + color_cfg = nil, ---@type PushButton + color_next = nil, ---@type PushButton + color_apply = nil, ---@type PushButton + settings_apply = nil, ---@type PushButton - gen_summary = nil, ---@type function - show_current_cfg = nil, ---@type function - load_legacy = nil, ---@type function + num_units = nil, ---@type NumberField + en_fac_tanks = nil, ---@type Checkbox + tank_mode = nil, ---@type RadioButton - show_auth_key = nil, ---@type function - show_key_btn = nil, ---@type graphics_element - auth_key_textbox = nil, ---@type graphics_element - auth_key_value = "", + gen_summary = nil, ---@type function + load_legacy = nil, ---@type function - cooling_elems = {}, - tank_elems = {}, - - vis_ftanks = {}, - vis_utanks = {} + cooling_elems = {}, ---@type { line: Div, turbines: NumberField, boilers: NumberField, tank: Checkbox }[] + tank_elems = {} ---@type { div: Div, tank_opt: Radio2D, no_tank: TextBox }[] } ---@class svr_config local tmp_cfg = { UnitCount = 1, - CoolingConfig = {}, + CoolingConfig = {}, ---@type { TurbineCount: integer, BoilerCount: integer, TankConnection: boolean }[] FacilityTankMode = 0, - FacilityTankDefs = {}, + FacilityTankDefs = {}, ---@type integer[] ExtChargeIdling = false, - SVR_Channel = nil, ---@type integer - PLC_Channel = nil, ---@type integer - RTU_Channel = nil, ---@type integer - CRD_Channel = nil, ---@type integer - PKT_Channel = nil, ---@type integer - PLC_Timeout = nil, ---@type number - RTU_Timeout = nil, ---@type number - CRD_Timeout = nil, ---@type number - PKT_Timeout = nil, ---@type number - TrustedRange = nil, ---@type number - AuthKey = nil, ---@type string|nil - LogMode = 0, + SVR_Channel = nil, ---@type integer + PLC_Channel = nil, ---@type integer + RTU_Channel = nil, ---@type integer + CRD_Channel = nil, ---@type integer + PKT_Channel = nil, ---@type integer + PLC_Timeout = nil, ---@type number + RTU_Timeout = nil, ---@type number + CRD_Timeout = nil, ---@type number + PKT_Timeout = nil, ---@type number + TrustedRange = nil, ---@type number + AuthKey = nil, ---@type string|nil + LogMode = 0, ---@type LOG_MODE LogPath = "", LogDebug = false, - FrontPanelTheme = 1, - ColorMode = 1 + FrontPanelTheme = 1, ---@type FP_THEME + ColorMode = 1 ---@type COLOR_MODE } ---@class svr_config @@ -153,8 +142,14 @@ local function load_settings(target, raw) end -- create the config view ----@param display graphics_element +---@param display DisplayBox local function config_view(display) + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + ---@diagnostic disable-next-line: undefined-field local function exit() os.queueEvent("terminate") end @@ -163,7 +158,7 @@ local function config_view(display) local root_pane_div = Div{parent=display,x=1,y=2} local main_page = Div{parent=root_pane_div,x=1,y=1} - local svr_cfg = Div{parent=root_pane_div,x=1,y=1} + local fac_cfg = Div{parent=root_pane_div,x=1,y=1} local net_cfg = Div{parent=root_pane_div,x=1,y=1} local log_cfg = Div{parent=root_pane_div,x=1,y=1} local clr_cfg = Div{parent=root_pane_div,x=1,y=1} @@ -171,9 +166,9 @@ local function config_view(display) local changelog = Div{parent=root_pane_div,x=1,y=1} local import_err = Div{parent=root_pane_div,x=1,y=1} - local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,svr_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,import_err}} + local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,fac_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,import_err}} - -- Main Page + --#region Main Page local y_start = 5 @@ -197,7 +192,7 @@ local function config_view(display) end PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} + tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} local function jump_color() tool_ctl.jumped_to_color = true @@ -207,7 +202,7 @@ local function config_view(display) end PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} - tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} + tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} if not tool_ctl.has_config then @@ -215,785 +210,25 @@ local function config_view(display) tool_ctl.color_cfg.disable() end - --#region Facility - - local svr_c_1 = Div{parent=svr_cfg,x=2,y=4,width=49} - local svr_c_2 = Div{parent=svr_cfg,x=2,y=4,width=49} - local svr_c_3 = Div{parent=svr_cfg,x=2,y=4,width=49} - local svr_c_4 = Div{parent=svr_cfg,x=2,y=4,width=49} - local svr_c_5 = Div{parent=svr_cfg,x=2,y=4,width=49} - local svr_c_6 = Div{parent=svr_cfg,x=2,y=4,width=49} - local svr_c_7 = Div{parent=svr_cfg,x=2,y=4,width=49} - - local svr_pane = MultiPane{parent=svr_cfg,x=1,y=4,panes={svr_c_1,svr_c_2,svr_c_3,svr_c_4,svr_c_5,svr_c_6,svr_c_7}} - - TextBox{parent=svr_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)} - - TextBox{parent=svr_c_1,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."} - local num_units = NumberField{parent=svr_c_1,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg} - TextBox{parent=svr_c_1,x=7,y=5,text="reactors"} - - local nu_error = TextBox{parent=svr_c_1,x=8,y=14,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_num_units() - local count = tonumber(num_units.get_value()) - if count ~= nil and count > 0 and count < 5 then - nu_error.hide(true) - tmp_cfg.UnitCount = count - - local confs = tool_ctl.cooling_elems - if count >= 2 then confs[2].line.show() else confs[2].line.hide(true) end - if count >= 3 then confs[3].line.show() else confs[3].line.hide(true) end - if count == 4 then confs[4].line.show() else confs[4].line.hide(true) end - - svr_pane.set_value(2) - else nu_error.show() end - end - - PushButton{parent=svr_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=svr_c_1,x=44,y=14,text="Next \x1a",callback=submit_num_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=svr_c_2,x=1,y=1,height=4,text="Please provide the reactor cooling configuration below. This includes the number of turbines, boilers, and if that reactor has a connection to a dynamic tank for emergency coolant."} - TextBox{parent=svr_c_2,x=1,y=6,text="UNIT TURBINES BOILERS HAS TANK CONNECTION?",fg_bg=g_lg_fg_bg} - - for i = 1, 4 do - local num_t, num_b, has_t = 1, 0, false - - if ini_cfg.CoolingConfig[i] then - local conf = ini_cfg.CoolingConfig[i] - if util.is_int(conf.TurbineCount) then num_t = math.min(3, math.max(1, conf.TurbineCount or 1)) end - if util.is_int(conf.BoilerCount) then num_b = math.min(2, math.max(0, conf.BoilerCount or 0)) end - has_t = conf.TankConnection == true - end - - local line = Div{parent=svr_c_2,x=1,y=7+i,height=1} - - TextBox{parent=line,text="Unit "..i,width=6} - local turbines = NumberField{parent=line,x=9,y=1,width=5,max_chars=2,default=num_t,min=1,max=3,fg_bg=bw_fg_bg} - local boilers = NumberField{parent=line,x=20,y=1,width=5,max_chars=2,default=num_b,min=0,max=2,fg_bg=bw_fg_bg} - local tank = CheckBox{parent=line,x=30,y=1,label="Is Connected",default=has_t,box_fg_bg=cpair(colors.yellow,colors.black)} - - tool_ctl.cooling_elems[i] = { line = line, turbines = turbines, boilers = boilers, tank = tank } - end - - local cool_err = TextBox{parent=svr_c_2,x=8,y=14,width=33,text="Please fill out all fields.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_cooling() - local any_missing = false - for i = 1, tmp_cfg.UnitCount do - local conf = tool_ctl.cooling_elems[i] - any_missing = any_missing or (tonumber(conf.turbines.get_value()) == nil) - any_missing = any_missing or (tonumber(conf.boilers.get_value()) == nil) - end - - if any_missing then - cool_err.show() - else - local any_has_tank = false - - tmp_cfg.CoolingConfig = {} - for i = 1, tmp_cfg.UnitCount do - local conf = tool_ctl.cooling_elems[i] - tmp_cfg.CoolingConfig[i] = { TurbineCount = tonumber(conf.turbines.get_value()), BoilerCount = tonumber(conf.boilers.get_value()), TankConnection = conf.tank.get_value() } - if conf.tank.get_value() then any_has_tank = true end - end - - for i = 1, 4 do - local elem = tool_ctl.tank_elems[i] - if i <= tmp_cfg.UnitCount then - elem.div.show() - if tmp_cfg.CoolingConfig[i].TankConnection then - elem.no_tank.hide() - elem.tank_opt.show() - else - elem.tank_opt.hide(true) - elem.no_tank.show() - end - else elem.div.hide(true) end - end - - if any_has_tank then svr_pane.set_value(3) else main_pane.set_value(3) end - end - end - - PushButton{parent=svr_c_2,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=svr_c_2,x=44,y=14,text="Next \x1a",callback=submit_cooling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=svr_c_3,x=1,y=1,height=6,text="You have set one or more of your units to use dynamic tanks for emergency coolant. You have two paths for configuration. The first is to assign dynamic tanks to reactor units; one tank per reactor, only connected to that reactor. RTU configurations must also assign it as such."} - TextBox{parent=svr_c_3,x=1,y=8,height=3,text="Alternatively, you can configure them as facility tanks to connect to multiple reactor units. These can intermingle with unit-specific tanks."} - - local en_fac_tanks = CheckBox{parent=svr_c_3,x=1,y=12,label="Use Facility Dynamic Tanks",default=ini_cfg.FacilityTankMode>0,box_fg_bg=cpair(colors.yellow,colors.black)} - - local function submit_en_fac_tank() - if en_fac_tanks.get_value() then - svr_pane.set_value(4) - tmp_cfg.FacilityTankMode = util.trinary(tmp_cfg.FacilityTankMode == 0, 1, math.min(8, math.max(1, ini_cfg.FacilityTankMode))) - else - tmp_cfg.FacilityTankMode = 0 - tmp_cfg.FacilityTankDefs = {} - svr_pane.set_value(7) - end - end - - PushButton{parent=svr_c_3,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=svr_c_3,x=44,y=14,text="Next \x1a",callback=submit_en_fac_tank,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=svr_c_4,x=1,y=1,height=4,text="Please set unit connections to dynamic tanks, selecting at least one facility tank. The layout for facility tanks will be configured next."} - - for i = 1, 4 do - local val = math.max(1, ini_cfg.FacilityTankDefs[i] or 2) - local div = Div{parent=svr_c_4,x=1,y=3+(2*i),height=2} - - TextBox{parent=div,x=1,y=1,width=33,text="Unit "..i.." will be connected to..."} - TextBox{parent=div,x=6,y=2,width=3,text="..."} - local tank_opt = Radio2D{parent=div,x=9,y=2,rows=1,columns=2,default=val,options={"its own Unit Tank","a Facility Tank"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg} - local no_tank = TextBox{parent=div,x=9,y=2,width=34,text="no tank (as you set two steps ago)",fg_bg=cpair(colors.gray,colors.lightGray),hidden=true} - - tool_ctl.tank_elems[i] = { div = div, tank_opt = tank_opt, no_tank = no_tank } - end - - local tank_err = TextBox{parent=svr_c_4,x=8,y=14,width=33,text="You selected no facility tanks.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function hide_fconn(i) - if i > 1 then tool_ctl.vis_ftanks[i].pipe_conn.hide(true) - else tool_ctl.vis_ftanks[i].line.hide(true) end - end - - local function submit_tank_defs() - local any_fac = false - - tmp_cfg.FacilityTankDefs = {} - for i = 1, tmp_cfg.UnitCount do - local def - - if tmp_cfg.CoolingConfig[i].TankConnection then - def = tool_ctl.tank_elems[i].tank_opt.get_value() - any_fac = any_fac or (def == 2) - else def = 0 end - - if def == 1 then - tool_ctl.vis_utanks[i].line.show() - tool_ctl.vis_utanks[i].label.set_value("Tank U" .. i) - hide_fconn(i) - else - if def == 2 then - if i > 1 then tool_ctl.vis_ftanks[i].pipe_conn.show() - else tool_ctl.vis_ftanks[i].line.show() end - else hide_fconn(i) end - tool_ctl.vis_utanks[i].line.hide(true) - end - - tmp_cfg.FacilityTankDefs[i] = def - end - - for i = tmp_cfg.UnitCount + 1, 4 do - tool_ctl.vis_utanks[i].line.hide(true) - end - - tool_ctl.vis_draw(tmp_cfg.FacilityTankMode) - - if any_fac then - tank_err.hide(true) - svr_pane.set_value(5) - else tank_err.show() end - end - - PushButton{parent=svr_c_4,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=svr_c_4,x=44,y=14,text="Next \x1a",callback=submit_tank_defs,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=svr_c_5,x=1,y=1,text="Please select your dynamic tank layout."} - TextBox{parent=svr_c_5,x=12,y=3,text="Facility Tanks Unit Tanks",fg_bg=g_lg_fg_bg} - - --#region Tank Layout Visualizer - - local pipe_cpair = cpair(colors.blue,colors.lightGray) - - local vis = Div{parent=svr_c_5,x=14,y=5,height=7} - - local vis_unit_list = TextBox{parent=vis,x=15,y=1,width=6,height=7,text="Unit 1\n\nUnit 2\n\nUnit 3\n\nUnit 4"} - - -- draw unit tanks and their pipes - for i = 1, 4 do - local line = Div{parent=vis,x=22,y=(i*2)-1,width=13,height=1} - TextBox{parent=line,width=5,text=string.rep("\x8c",5),fg_bg=pipe_cpair} - local label = TextBox{parent=line,x=7,y=1,width=7,text="Tank ?"} - tool_ctl.vis_utanks[i] = { line = line, label = label } - end - - -- draw facility tank connections - - local ftank_1 = Div{parent=vis,x=1,y=1,width=13,height=1} - TextBox{parent=ftank_1,width=7,text="Tank F1"} - tool_ctl.vis_ftanks[1] = { - line = ftank_1, pipe_direct = TextBox{parent=ftank_1,x=9,y=1,width=5,text=string.rep("\x8c",5),fg_bg=pipe_cpair} - } - - for i = 2, 4 do - local line = Div{parent=vis,x=1,y=(i-1)*2,width=13,height=2} - local pipe_conn = TextBox{parent=line,x=13,y=2,width=1,text="\x8c",fg_bg=pipe_cpair} - local pipe_chain = TextBox{parent=line,x=12,y=1,width=1,height=2,text="\x95\n\x8d",fg_bg=pipe_cpair} - local pipe_direct = TextBox{parent=line,x=9,y=2,width=4,text="\x8c\x8c\x8c\x8c",fg_bg=pipe_cpair} - local label = TextBox{parent=line,x=1,y=2,width=7,text=""} - tool_ctl.vis_ftanks[i] = { line = line, pipe_conn = pipe_conn, pipe_chain = pipe_chain, pipe_direct = pipe_direct, label = label } - end - - -- draw the pipe visualization - ---@param mode integer pipe mode - function tool_ctl.vis_draw(mode) - -- is a facility tank connected to this unit - ---@param i integer unit 1 - 4 - ---@return boolean connected - local function is_ft(i) return tmp_cfg.FacilityTankDefs[i] == 2 end - - local u_text = "" - for i = 1, tmp_cfg.UnitCount do - u_text = u_text .. "Unit " .. i .. "\n\n" - end - - vis_unit_list.set_value(u_text) - - local vis_ftanks = tool_ctl.vis_ftanks - local next_idx = 1 - - if is_ft(1) then - next_idx = 2 - - if (mode == 1 and (is_ft(2) or is_ft(3) or is_ft(4))) or (mode == 2 and (is_ft(2) or is_ft(3))) or ((mode == 3 or mode == 5) and is_ft(2)) then - vis_ftanks[1].pipe_direct.set_value("\x8c\x8c\x8c\x9c\x8c") - else - vis_ftanks[1].pipe_direct.set_value(string.rep("\x8c",5)) - end - end - - local _2_12_need_passt = (mode == 1 and (is_ft(3) or is_ft(4))) or (mode == 2 and is_ft(3)) - local _2_46_need_chain = (mode == 4 and (is_ft(3) or is_ft(4))) or (mode == 6 and is_ft(3)) - - if is_ft(2) then - vis_ftanks[2].label.set_value("Tank F" .. next_idx) - - if (mode < 4 or mode == 5) and is_ft(1) then - vis_ftanks[2].label.hide(true) - vis_ftanks[2].pipe_direct.hide(true) - if _2_12_need_passt then - vis_ftanks[2].pipe_chain.set_value("\x95\n\x9d") - else - vis_ftanks[2].pipe_chain.set_value("\x95\n\x8d") - end - vis_ftanks[2].pipe_chain.show() - else - vis_ftanks[2].label.show() - next_idx = next_idx + 1 - - vis_ftanks[2].pipe_chain.hide(true) - if _2_12_need_passt or _2_46_need_chain then - vis_ftanks[2].pipe_direct.set_value("\x8c\x8c\x8c\x9c") - else - vis_ftanks[2].pipe_direct.set_value("\x8c\x8c\x8c\x8c") - end - vis_ftanks[2].pipe_direct.show() - end - - vis_ftanks[2].line.show() - elseif is_ft(1) and _2_12_need_passt then - vis_ftanks[2].label.hide(true) - vis_ftanks[2].pipe_direct.hide(true) - vis_ftanks[2].pipe_chain.set_value("\x95\n\x95") - vis_ftanks[2].pipe_chain.show() - vis_ftanks[2].line.show() - else - vis_ftanks[2].line.hide(true) - end - - if is_ft(3) then - vis_ftanks[3].label.set_value("Tank F" .. next_idx) - - if (mode < 3 and (is_ft(1) or is_ft(2))) or ((mode == 4 or mode == 6) and is_ft(2)) then - vis_ftanks[3].label.hide(true) - vis_ftanks[3].pipe_direct.hide(true) - if (mode == 1 or mode == 4) and is_ft(4) then - vis_ftanks[3].pipe_chain.set_value("\x95\n\x9d") - else - vis_ftanks[3].pipe_chain.set_value("\x95\n\x8d") - end - vis_ftanks[3].pipe_chain.show() - else - vis_ftanks[3].label.show() - next_idx = next_idx + 1 - - vis_ftanks[3].pipe_chain.hide(true) - if (mode == 1 or mode == 3 or mode == 4 or mode == 7) and is_ft(4) then - vis_ftanks[3].pipe_direct.set_value("\x8c\x8c\x8c\x9c") - else - vis_ftanks[3].pipe_direct.set_value("\x8c\x8c\x8c\x8c") - end - vis_ftanks[3].pipe_direct.show() - end - - vis_ftanks[3].line.show() - elseif (mode == 1 and is_ft(4) and (is_ft(1) or is_ft(2))) or (mode == 4 and is_ft(2) and is_ft(4)) then - vis_ftanks[3].label.hide(true) - vis_ftanks[3].pipe_direct.hide(true) - vis_ftanks[3].pipe_chain.set_value("\x95\n\x95") - vis_ftanks[3].pipe_chain.show() - vis_ftanks[3].line.show() - else - vis_ftanks[3].line.hide(true) - end - - if is_ft(4) then - vis_ftanks[4].label.set_value("Tank F" .. next_idx) - - if (mode == 1 and (is_ft(1) or is_ft(2) or is_ft(3))) or ((mode == 3 or mode == 7) and is_ft(3)) or (mode == 4 and (is_ft(2) or is_ft(3))) then - vis_ftanks[4].label.hide(true) - vis_ftanks[4].pipe_direct.hide(true) - vis_ftanks[4].pipe_chain.show() - else - vis_ftanks[4].label.show() - vis_ftanks[4].pipe_chain.hide(true) - vis_ftanks[4].pipe_direct.show() - end - - vis_ftanks[4].line.show() - else - vis_ftanks[4].line.hide(true) - end - end - - local function change_mode(mode) - tmp_cfg.FacilityTankMode = mode - tool_ctl.vis_draw(mode) - end - - local tank_modes = { "Mode 1", "Mode 2", "Mode 3", "Mode 4", "Mode 5", "Mode 6", "Mode 7", "Mode 8" } - local tank_mode = RadioButton{parent=svr_c_5,x=1,y=4,callback=change_mode,default=math.max(1,ini_cfg.FacilityTankMode),options=tank_modes,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow} - --#endregion - PushButton{parent=svr_c_5,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=svr_c_5,x=44,y=14,text="Next \x1a",callback=function()svr_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - PushButton{parent=svr_c_5,x=8,y=14,min_width=7,text="About",callback=function()svr_pane.set_value(6)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg} + local settings = { settings_cfg, ini_cfg, tmp_cfg, fields, load_settings } - TextBox{parent=svr_c_6,height=3,text="This visualization tool shows the pipe connections required for a particular dynamic tank configuration you have selected."} - TextBox{parent=svr_c_6,y=5,height=4,text="Examples: A U2 tank should be configured on an RTU as the dynamic tank for unit #2. An F3 tank should be configured on an RTU as the #3 dynamic tank for the facility."} - TextBox{parent=svr_c_6,y=10,height=3,text="Some modes may look the same if you are not using 4 total reactor units. The wiki has details. Modes that look the same will function the same.",fg_bg=g_lg_fg_bg} + --#region Facility Configuration - PushButton{parent=svr_c_6,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=svr_c_7,height=6,text="Charge control provides automatic control to maintain an induction matrix charge level. In order to have smoother control, reactors that were activated will be held on at 0.01 mB/t for a short period before allowing them to turn off. This minimizes overshooting the charge target."} - TextBox{parent=svr_c_7,y=8,height=3,text="You can extend this to a full minute to minimize reactors flickering on/off, but there may be more overshoot of the target."} - - local ext_idling = CheckBox{parent=svr_c_7,x=1,y=12,label="Enable Extended Idling",default=ini_cfg.ExtChargeIdling,box_fg_bg=cpair(colors.yellow,colors.black)} - - local function back_from_idling() - svr_pane.set_value(util.trinary(tmp_cfg.FacilityTankMode == 0, 3, 5)) - end - - local function submit_idling() - tmp_cfg.ExtChargeIdling = ext_idling.get_value() - main_pane.set_value(3) - end - - PushButton{parent=svr_c_7,x=1,y=14,text="\x1b Back",callback=back_from_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=svr_c_7,x=44,y=14,text="Next \x1a",callback=submit_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + local fac_pane = facility.create(tool_ctl, main_pane, settings, fac_cfg, style) --#endregion - --#region Network - - local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49} - local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49} - local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49} - local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49} - - local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}} - - TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} - - TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."} - TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"} - local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_1,x=1,y=9,width=11,text="PLC Channel"} - local plc_chan = NumberField{parent=net_c_1,x=21,y=9,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=29,y=9,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_1,x=1,y=10,width=19,text="RTU Gateway Channel"} - local rtu_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=29,y=10,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_1,x=1,y=11,width=19,text="Coordinator Channel"} - local crd_chan = NumberField{parent=net_c_1,x=21,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=29,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"} - local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg} - TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg} - - local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_channels() - local svr_c, plc_c, rtu_c = tonumber(svr_chan.get_value()), tonumber(plc_chan.get_value()), tonumber(rtu_chan.get_value()) - local crd_c, pkt_c = tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value()) - if svr_c ~= nil and plc_c ~= nil and rtu_c ~= nil and crd_c ~= nil and pkt_c ~= nil then - tmp_cfg.SVR_Channel, tmp_cfg.PLC_Channel, tmp_cfg.RTU_Channel = svr_c, plc_c, rtu_c - tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = crd_c, pkt_c - net_pane.set_value(2) - chan_err.hide(true) - else chan_err.show() end - end - - PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=net_c_2,x=1,y=1,text="Please set the connection timeouts below."} - TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg} - - TextBox{parent=net_c_2,x=1,y=8,width=11,text="PLC Timeout"} - local plc_timeout = NumberField{parent=net_c_2,x=21,y=8,width=7,default=ini_cfg.PLC_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} - - TextBox{parent=net_c_2,x=1,y=9,width=19,text="RTU Gateway Timeout"} - local rtu_timeout = NumberField{parent=net_c_2,x=21,y=9,width=7,default=ini_cfg.RTU_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} - - TextBox{parent=net_c_2,x=1,y=10,width=19,text="Coordinator Timeout"} - local crd_timeout = NumberField{parent=net_c_2,x=21,y=10,width=7,default=ini_cfg.CRD_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} - - TextBox{parent=net_c_2,x=1,y=11,width=14,text="Pocket Timeout"} - local pkt_timeout = NumberField{parent=net_c_2,x=21,y=11,width=7,default=ini_cfg.PKT_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} - - TextBox{parent=net_c_2,x=29,y=8,height=4,width=7,text="seconds\nseconds\nseconds\nseconds",fg_bg=g_lg_fg_bg} - - local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_timeouts() - local plc_cto, rtu_cto, crd_cto, pkt_cto = tonumber(plc_timeout.get_value()), tonumber(rtu_timeout.get_value()), tonumber(crd_timeout.get_value()), tonumber(pkt_timeout.get_value()) - if plc_cto ~= nil and rtu_cto ~= nil and crd_cto ~= nil and pkt_cto ~= nil then - tmp_cfg.PLC_Timeout, tmp_cfg.RTU_Timeout, tmp_cfg.CRD_Timeout, tmp_cfg.PKT_Timeout = plc_cto, rtu_cto, crd_cto, pkt_cto - net_pane.set_value(3) - ct_err.hide(true) - else ct_err.show() end - end - - PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."} - TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg} - TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg} - - local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg} - - local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_tr() - local range_val = tonumber(range.get_value()) - if range_val ~= nil then - tmp_cfg.TrustedRange = range_val - net_pane.set_value(4) - tr_err.hide(true) - else tr_err.show() end - end - - PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + --#region System Configuration - TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} - TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=g_lg_fg_bg} + local divs = { net_cfg, log_cfg, clr_cfg, summary, import_err } - TextBox{parent=net_c_4,x=1,y=11,text="Facility Auth Key"} - local key, _, censor = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg} - - local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end - - local hide_key = CheckBox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} - - hide_key.set_value(true) - censor_key(true) - - local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_auth() - local v = key.get_value() - if string.len(v) == 0 or string.len(v) >= 8 then - tmp_cfg.AuthKey = key.get_value() - main_pane.set_value(4) - key_err.hide(true) - else key_err.show() end - end - - PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + system.create(tool_ctl, main_pane, settings, divs, fac_pane, style, exit) --#endregion - --#region Logging - - local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} - - TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} - - TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."} - - TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} - local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} - - TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"} - local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg} - - local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} - TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg} - - local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} - - local function submit_log() - if path.get_value() ~= "" then - path_err.hide(true) - tmp_cfg.LogMode = mode.get_value() - 1 - tmp_cfg.LogPath = path.get_value() - tmp_cfg.LogDebug = en_dbg.get_value() - tool_ctl.color_apply.hide(true) - tool_ctl.color_next.show() - main_pane.set_value(5) - else path_err.show() end - end - - PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - --#endregion - - --#region Color Options - - local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49} - local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49} - local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49} - local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49} - - local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}} - - TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)} - - TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color theme for the front panel."} - TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg} - - TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"} - local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} - - TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."} - - TextBox{parent=clr_c_2,x=21,y=7,text="Preview"} - local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)} - _ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)} - _ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)} - local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true} - local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true} - - local function recolor(value) - local c = themes.smooth_stone.color_modes[value] - - if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then - b_off.hide() - g_off.show() - else - g_off.hide() - b_off.show() - end - - if #c == 0 then - for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end - else - term.setPaletteColor(colors.green, c[1].hex) - term.setPaletteColor(colors.yellow, c[2].hex) - term.setPaletteColor(colors.red, c[3].hex) - end - end - - TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"} - local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta} - - TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg} - - PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - local function back_from_colors() - main_pane.set_value(util.trinary(tool_ctl.jumped_to_color, 1, 4)) - tool_ctl.jumped_to_color = false - recolor(1) - end - - local function show_access() - clr_pane.set_value(2) - recolor(c_mode.get_value()) - end - - local function submit_colors() - tmp_cfg.FrontPanelTheme = fp_theme.get_value() - tmp_cfg.ColorMode = c_mode.get_value() - - if tool_ctl.jumped_to_color then - settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme) - settings.set("ColorMode", tmp_cfg.ColorMode) - - if settings.save("/supervisor.settings") then - load_settings(settings_cfg, true) - load_settings(ini_cfg) - clr_pane.set_value(3) - else - clr_pane.set_value(4) - end - else - tool_ctl.gen_summary(tmp_cfg) - tool_ctl.viewing_config = false - tool_ctl.importing_legacy = false - tool_ctl.settings_apply.show() - main_pane.set_value(6) - end - end - - PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} - - tool_ctl.color_apply.hide(true) - - local function c_go_home() - main_pane.set_value(1) - clr_pane.set_value(1) - end - - TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"} - PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} - PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - - --#endregion - - --#region Summary and Saving - - local sum_c_1 = Div{parent=summary,x=2,y=4,width=49} - local sum_c_2 = Div{parent=summary,x=2,y=4,width=49} - local sum_c_3 = Div{parent=summary,x=2,y=4,width=49} - local sum_c_4 = Div{parent=summary,x=2,y=4,width=49} - - local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}} - - TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)} - - local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} - - local function back_from_settings() - if tool_ctl.viewing_config or tool_ctl.importing_legacy then - main_pane.set_value(1) - tool_ctl.viewing_config = false - tool_ctl.importing_legacy = false - tool_ctl.settings_apply.show() - else - main_pane.set_value(5) - end - end - - ---@param element graphics_element - ---@param data any - local function try_set(element, data) - if data ~= nil then element.set_value(data) end - end - - local function save_and_continue() - for _, field in ipairs(fields) do - local k, v = field[1], tmp_cfg[field[1]] - if v == nil then settings.unset(k) else settings.set(k, v) end - end - - if settings.save("/supervisor.settings") then - load_settings(settings_cfg, true) - load_settings(ini_cfg) - - try_set(num_units, ini_cfg.UnitCount) - try_set(tank_mode, ini_cfg.FacilityTankMode) - try_set(svr_chan, ini_cfg.SVR_Channel) - try_set(plc_chan, ini_cfg.PLC_Channel) - try_set(rtu_chan, ini_cfg.RTU_Channel) - try_set(crd_chan, ini_cfg.CRD_Channel) - try_set(pkt_chan, ini_cfg.PKT_Channel) - try_set(plc_timeout, ini_cfg.PLC_Timeout) - try_set(rtu_timeout, ini_cfg.RTU_Timeout) - try_set(crd_timeout, ini_cfg.CRD_Timeout) - try_set(pkt_timeout, ini_cfg.PKT_Timeout) - try_set(range, ini_cfg.TrustedRange) - try_set(key, ini_cfg.AuthKey) - try_set(mode, ini_cfg.LogMode) - try_set(path, ini_cfg.LogPath) - try_set(en_dbg, ini_cfg.LogDebug) - try_set(fp_theme, ini_cfg.FrontPanelTheme) - try_set(c_mode, ini_cfg.ColorMode) - - for i = 1, #ini_cfg.CoolingConfig do - local cfg, elems = ini_cfg.CoolingConfig[i], tool_ctl.cooling_elems[i] - try_set(elems.boilers, cfg.BoilerCount) - try_set(elems.turbines, cfg.TurbineCount) - try_set(elems.tank, cfg.TankConnection) - end - - for i = 1, #ini_cfg.FacilityTankDefs do - try_set(tool_ctl.tank_elems[i].tank_opt, ini_cfg.FacilityTankDefs[i]) - end - - en_fac_tanks.set_value(ini_cfg.FacilityTankMode > 0) - - tool_ctl.view_cfg.enable() - - if tool_ctl.importing_legacy then - tool_ctl.importing_legacy = false - sum_pane.set_value(3) - else - sum_pane.set_value(2) - end - else - sum_pane.set_value(4) - end - end - - PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} - tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} - - TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"} - - local function go_home() - main_pane.set_value(1) - svr_pane.set_value(1) - net_pane.set_value(1) - clr_pane.set_value(1) - sum_pane.set_value(1) - end - - PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - - TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."} - - local function delete_legacy() - fs.delete("/supervisor/config.lua") - exit() - end - - PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)} - - TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} - PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - - --#endregion - - -- Config Change Log + --#region Config Change Log local cl = Div{parent=changelog,x=2,y=4,width=49} @@ -1012,175 +247,7 @@ local function config_view(display) PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - -- Import Error - - local i_err = Div{parent=import_err,x=2,y=4,width=49} - - TextBox{parent=import_err,x=1,y=2,text=" Import Error",fg_bg=cpair(colors.black,colors.red)} - TextBox{parent=i_err,x=1,y=1,text="There is a problem with your config.lua file:"} - - local import_err_msg = TextBox{parent=i_err,x=1,y=3,height=6,text=""} - - PushButton{parent=i_err,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=i_err,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - - -- set tool functions now that we have the elements - - -- load a legacy config file - function tool_ctl.load_legacy() - local config = require("supervisor.config") - - tmp_cfg.UnitCount = config.NUM_REACTORS - - if config.REACTOR_COOLING == nil or tmp_cfg.UnitCount ~= #config.REACTOR_COOLING then - import_err_msg.set_value("Cooling configuration table length must match the number of units.") - main_pane.set_value(8) - return - end - - for i = 1, tmp_cfg.UnitCount do - local cfg = config.REACTOR_COOLING[i] - - if type(cfg) ~= "table" then - import_err_msg.set_value("Cooling configuration for unit " .. i .. " must be a table.") - main_pane.set_value(8) - return - end - - tmp_cfg.CoolingConfig[i] = { BoilerCount = cfg.BOILERS or 0, TurbineCount = cfg.TURBINES or 1, TankConnection = cfg.TANK or false } - end - - tmp_cfg.FacilityTankMode = config.FAC_TANK_MODE - - if not (util.is_int(tmp_cfg.FacilityTankMode) and tmp_cfg.FacilityTankMode >= 0 and tmp_cfg.FacilityTankMode <= 8) then - import_err_msg.set_value("Invalid tank mode present in config. FAC_TANK_MODE must be a number 0 through 8.") - main_pane.set_value(8) - return - end - - if config.FAC_TANK_MODE > 0 then - if config.FAC_TANK_DEFS == nil or tmp_cfg.UnitCount ~= #config.FAC_TANK_DEFS then - import_err_msg.set_value("Facility tank definitions table length must match the number of units when using facility tanks.") - main_pane.set_value(8) - return - end - - for i = 1, tmp_cfg.UnitCount do - tmp_cfg.FacilityTankDefs[i] = config.FAC_TANK_DEFS[i] - end - else - tmp_cfg.FacilityTankMode = 0 - tmp_cfg.FacilityTankDefs = {} - end - - tmp_cfg.SVR_Channel = config.SVR_CHANNEL - tmp_cfg.PLC_Channel = config.PLC_CHANNEL - tmp_cfg.RTU_Channel = config.RTU_CHANNEL - tmp_cfg.CRD_Channel = config.CRD_CHANNEL - tmp_cfg.PKT_Channel = config.PKT_CHANNEL - - tmp_cfg.PLC_Timeout = config.PLC_TIMEOUT - tmp_cfg.RTU_Timeout = config.RTU_TIMEOUT - tmp_cfg.CRD_Timeout = config.CRD_TIMEOUT - tmp_cfg.PKT_Timeout = config.PKT_TIMEOUT - - tmp_cfg.TrustedRange = config.TRUSTED_RANGE - tmp_cfg.AuthKey = config.AUTH_KEY or "" - tmp_cfg.LogMode = config.LOG_MODE - tmp_cfg.LogPath = config.LOG_PATH - tmp_cfg.LogDebug = config.LOG_DEBUG or false - - tool_ctl.gen_summary(tmp_cfg) - sum_pane.set_value(1) - main_pane.set_value(6) - tool_ctl.importing_legacy = true - end - - -- expose the auth key on the summary page - function tool_ctl.show_auth_key() - tool_ctl.show_key_btn.disable() - tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value) - end - - -- generate the summary list - ---@param cfg svr_config - function tool_ctl.gen_summary(cfg) - setting_list.remove_all() - - local alternate = false - local inner_width = setting_list.get_width() - 1 - - tool_ctl.show_key_btn.enable() - tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key - - for i = 1, #fields do - local f = fields[i] - local height = 1 - local label_w = string.len(f[2]) - local val_max_w = (inner_width - label_w) + 1 - local raw = cfg[f[1]] - local val = util.strval(raw) - - if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) - elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") - elseif f[1] == "FrontPanelTheme" then - val = util.strval(themes.fp_theme_name(raw)) - elseif f[1] == "ColorMode" then - val = util.strval(themes.color_mode_name(raw)) - elseif f[1] == "CoolingConfig" and type(cfg.CoolingConfig) == "table" then - val = "" - - for idx = 1, #cfg.CoolingConfig do - local ccfg = cfg.CoolingConfig[idx] - local b_plural = util.trinary(ccfg.BoilerCount == 1, "", "s") - local t_plural = util.trinary(ccfg.TurbineCount == 1, "", "s") - local tank = util.trinary(ccfg.TankConnection, "has tank conn", "no tank conn") - val = val .. util.trinary(idx == 1, "", "\n") .. - util.sprintf(" \x07 unit %d - %d boiler%s, %d turbine%s, %s", idx, ccfg.BoilerCount, b_plural, ccfg.TurbineCount, t_plural, tank) - end - - if val == "" then val = "no facility tanks" end - elseif f[1] == "FacilityTankMode" and raw == 0 then val = "0 (n/a, unit mode)" - elseif f[1] == "FacilityTankDefs" and type(cfg.FacilityTankDefs) == "table" then - val = "" - - for idx = 1, #cfg.FacilityTankDefs do - local t_mode = "not connected to a tank" - if cfg.FacilityTankDefs[idx] == 1 then - t_mode = "connected to its unit tank" - elseif cfg.FacilityTankDefs[idx] == 2 then - t_mode = "connected to a facility tank" - end - - val = val .. util.trinary(idx == 1, "", "\n") .. util.sprintf(" \x07 unit %d - %s", idx, t_mode) - end - - if val == "" then val = "no facility tanks" end - end - - if val == "nil" then val = "" end - - local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) - alternate = not alternate - - if string.len(val) > val_max_w then - local lines = util.strwrap(val, inner_width) - height = #lines + 1 - end - - local line = Div{parent=setting_list,height=height,fg_bg=c} - TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} - - local textbox - if height > 1 then - textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1} - else - textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT} - end - - if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end - end - end + --#endregion end -- reset terminal screen diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 7b786a28..4b2b38c0 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -40,7 +40,7 @@ local facility = {} function facility.new(config) ---@class _facility_self local self = { - units = {}, + units = {}, ---@type reactor_unit[] types = { AUTO_SCRAM = AUTO_SCRAM, START_STATUS = START_STATUS }, status_text = { "START UP", "initializing..." }, all_sys_ok = false, @@ -51,16 +51,16 @@ function facility.new(config) r_cool = config.CoolingConfig, fac_tank_mode = config.FacilityTankMode, fac_tank_defs = config.FacilityTankDefs, - fac_tank_list = {} + fac_tank_list = {} ---@type integer[] }, -- rtus - rtu_conn_count = 0, - rtu_list = {}, - redstone = {}, - induction = {}, - sps = {}, - tanks = {}, - envd = {}, + rtu_gw_conn_count = 0, + rtu_list = {}, ---@type unit_session[][] + redstone = {}, ---@type redstone_session[] + induction = {}, ---@type imatrix_session[] + sps = {}, ---@type sps_session[] + tanks = {}, ---@type dynamicv_session[] + envd = {}, ---@type envd_session[] -- redstone I/O control io_ctl = nil, ---@type rs_controller -- process control @@ -105,11 +105,11 @@ function facility.new(config) sps_low_power = false, disabled_sps = false, -- alarm tones - tone_states = {}, + tone_states = {}, ---@type { [TONE]: boolean } test_tone_set = false, test_tone_reset = false, - test_tone_states = {}, - test_alarm_states = {}, + test_tone_states = {}, ---@type { [TONE]: boolean } + test_alarm_states = {}, ---@type { [ALARM]: boolean } -- statistics im_stat_init = false, avg_charge = util.mov_avg(3), -- 3 seconds @@ -350,7 +350,7 @@ function facility.new(config) -- additionally sets the requested auto waste mode if applicable function public.update_units() for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit + local u = self.units[i] u.auto_set_waste(self.current_waste_product) u.update() end @@ -363,16 +363,14 @@ function facility.new(config) -- SCRAM all reactor units function public.scram_all() for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit - u.scram() + self.units[i].scram() end end -- ack all alarms on all reactor units function public.ack_all() for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit - u.ack_all() + self.units[i].ack_all() end end @@ -393,8 +391,7 @@ function facility.new(config) -- load up current limits local limits = {} for i = 1, config.UnitCount do - local u = self.units[i] ---@type reactor_unit - limits[i] = u.get_control_inf().lim_br100 * 100 + limits[i] = self.units[i].get_control_inf().lim_br100 * 100 end -- only allow changes if not running @@ -428,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 @@ -439,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, @@ -512,7 +511,7 @@ function facility.new(config) -- attempt to set a test tone state ---@param id TONE|0 tone ID or 0 to disable all ---@param state boolean state - ---@return boolean allow_testing, table test_tone_states + ---@return boolean allow_testing, { [TONE]: boolean } test_tone_states function public.diag_set_test_tone(id, state) if self.allow_testing then self.test_tone_set = true @@ -531,7 +530,7 @@ function facility.new(config) -- attempt to set a test alarm state ---@param id ALARM|0 alarm ID or 0 to disable all ---@param state boolean state - ---@return boolean allow_testing, table test_alarm_states + ---@return boolean allow_testing, { [ALARM]: boolean } test_alarm_states function public.diag_set_test_alarm(id, state) if self.allow_testing then self.test_tone_set = true @@ -565,7 +564,7 @@ function facility.new(config) if all or type == RTU_UNIT_TYPE.IMATRIX then build.induction = {} for i = 1, #self.induction do - local matrix = self.induction[i] ---@type unit_session + local matrix = self.induction[i] build.induction[i] = { matrix.get_db().formed, matrix.get_db().build } end end @@ -573,7 +572,7 @@ function facility.new(config) if all or type == RTU_UNIT_TYPE.SPS then build.sps = {} for i = 1, #self.sps do - local sps = self.sps[i] ---@type unit_session + local sps = self.sps[i] build.sps[i] = { sps.get_db().formed, sps.get_db().build } end end @@ -581,7 +580,7 @@ function facility.new(config) if all or type == RTU_UNIT_TYPE.DYNAMIC_VALVE then build.tanks = {} for i = 1, #self.tanks do - local tank = self.tanks[i] ---@type unit_session + local tank = self.tanks[i] build.tanks[tank.get_device_idx()] = { tank.get_db().formed, tank.get_db().build } end end @@ -636,7 +635,7 @@ function facility.new(config) local status = {} -- total count of all connected RTUs in the facility - status.count = self.rtu_conn_count + status.count = self.rtu_gw_conn_count -- power averages from induction matricies status.power = { @@ -649,8 +648,8 @@ function facility.new(config) -- status of induction matricies (including tanks) status.induction = {} for i = 1, #self.induction do - local matrix = self.induction[i] ---@type unit_session - local db = matrix.get_db() ---@type imatrix_session_db + local matrix = self.induction[i] + local db = matrix.get_db() status.induction[i] = { matrix.is_faulted(), db.formed, db.state, db.tanks } @@ -662,24 +661,24 @@ function facility.new(config) -- status of sps status.sps = {} for i = 1, #self.sps do - local sps = self.sps[i] ---@type unit_session - local db = sps.get_db() ---@type sps_session_db + local sps = self.sps[i] + local db = sps.get_db() status.sps[i] = { sps.is_faulted(), db.formed, db.state, db.tanks } end -- status of dynamic tanks status.tanks = {} for i = 1, #self.tanks do - local tank = self.tanks[i] ---@type unit_session - local db = tank.get_db() ---@type dynamicv_session_db + local tank = self.tanks[i] + local db = tank.get_db() status.tanks[tank.get_device_idx()] = { tank.is_faulted(), db.formed, db.state, db.tanks } end -- radiation monitors (environment detectors) status.envds = {} for i = 1, #self.envd do - local envd = self.envd[i] ---@type unit_session - local db = envd.get_db() ---@type envd_session_db + local envd = self.envd[i] + local db = envd.get_db() status.envds[envd.get_device_idx()] = { envd.is_faulted(), db.radiation, db.radiation_raw } end @@ -688,9 +687,9 @@ function facility.new(config) --#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 + -- supervisor sessions reporting the list of active RTU gateway sessions + ---@param sessions rtu_session_struct[] session list of all connected RTU gateways + function public.report_rtu_gateways(sessions) self.rtu_gw_conn_count = #sessions end -- get the facility cooling configuration function public.get_cooling_conf() return self.cooling_conf end diff --git a/supervisor/facility_update.lua b/supervisor/facility_update.lua index 43fe86be..6375077e 100644 --- a/supervisor/facility_update.lua +++ b/supervisor/facility_update.lua @@ -84,7 +84,7 @@ local function allocate_burn_rate(burn_rate, ramp, abort_on_fault) -- go through all reactor units in this group for id = 1, #units do - local u = units[id] ---@type reactor_unit + local u = units[id] local ctl = u.get_control_inf() local lim_br100 = u.auto_get_effective_limit() @@ -139,7 +139,7 @@ function update.pre_auto() -- check if test routines are allowed right now self.allow_testing = true for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit + local u = self.units[i] self.allow_testing = self.allow_testing and u.is_safe_idle() end @@ -149,8 +149,8 @@ function update.pre_auto() -- calculate moving averages for induction matrix if self.induction[1] ~= nil then - local matrix = self.induction[1] ---@type unit_session - local db = matrix.get_db() ---@type imatrix_session_db + local matrix = self.induction[1] + local db = matrix.get_db() local build_update = db.build.last_update rate_update = db.state.last_update @@ -512,7 +512,7 @@ function update.auto_safety() local astatus = self.ascram_status if self.induction[1] ~= nil then - local db = self.induction[1].get_db() ---@type imatrix_session_db + local db = self.induction[1].get_db() -- clear matrix disconnected if astatus.matrix_dc then @@ -531,7 +531,7 @@ function update.auto_safety() -- check for critical unit alarms astatus.crit_alarm = false for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit + local u = self.units[i] if u.has_alarm_min_prio(PRIO.CRITICAL) then astatus.crit_alarm = true @@ -544,8 +544,8 @@ function update.auto_safety() local max_rad = 0 for i = 1, #self.envd do - local envd = self.envd[i] ---@type unit_session - local e_db = envd.get_db() ---@type envd_session_db + local envd = self.envd[i] + local e_db = envd.get_db() if e_db.radiation_raw > max_rad then max_rad = e_db.radiation_raw end end @@ -620,7 +620,7 @@ function update.auto_safety() -- reset PLC RPS trips if we should for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit + local u = self.units[i] u.auto_cond_rps_reset() end end @@ -647,7 +647,7 @@ function update.alarm_audio() else -- check all alarms for all units for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit + local u = self.units[i] for id, alarm in pairs(u.get_alarms()) do alarms[id] = alarms[id] or (alarm == ALARM_STATE.TRIPPED) end @@ -730,7 +730,7 @@ function update.redstone(ack_all) -- handle facility SCRAM if self.io_ctl.digital_read(IO.F_SCRAM) then for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit + local u = self.units[i] u.cond_scram() end end @@ -741,7 +741,7 @@ function update.redstone(ack_all) -- update facility alarm outputs local has_prio_alarm, has_any_alarm = false, false for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit + local u = self.units[i] if u.has_alarm_min_prio(PRIO.EMERGENCY) then has_prio_alarm, has_any_alarm = true, true @@ -756,7 +756,7 @@ function update.redstone(ack_all) -- update induction matrix related outputs if self.induction[1] ~= nil then - local db = self.induction[1].get_db() ---@type imatrix_session_db + local db = self.induction[1].get_db() self.io_ctl.digital_write(IO.F_MATRIX_LOW, db.tanks.energy_fill < const.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) self.io_ctl.digital_write(IO.F_MATRIX_HIGH, db.tanks.energy_fill > const.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) @@ -771,7 +771,7 @@ function update.unit_mgmt() local need_emcool = false for i = 1, #self.units do - local u = self.units[i] ---@type reactor_unit + local u = self.units[i] -- update auto waste processing if u.get_control_inf().waste_mode == WASTE_MODE.AUTO then @@ -791,7 +791,7 @@ function update.unit_mgmt() self.current_waste_product = self.waste_product if (not self.sps_low_power) and (self.waste_product == WASTE.ANTI_MATTER) and (self.induction[1] ~= nil) then - local db = self.induction[1].get_db() ---@type imatrix_session_db + local db = self.induction[1].get_db() if db.tanks.energy_fill >= 0.15 then self.disabled_sps = false @@ -812,8 +812,8 @@ function update.unit_mgmt() -- there should be no need for any to be in fill only mode if need_emcool then for i = 1, #self.tanks do - local session = self.tanks[i] ---@type unit_session - local tank = session.get_db() ---@type dynamicv_session_db + local session = self.tanks[i] + local tank = session.get_db() if tank.state.container_mode == CONTAINER_MODE.FILL then session.get_cmd_queue().push_data(DTV_RTU_S_DATA.SET_CONT_MODE, CONTAINER_MODE.BOTH) diff --git a/supervisor/panel/components/chk_entry.lua b/supervisor/panel/components/chk_entry.lua index ff4a24b3..ec86bac5 100644 --- a/supervisor/panel/components/chk_entry.lua +++ b/supervisor/panel/components/chk_entry.lua @@ -8,20 +8,20 @@ local style = require("supervisor.panel.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") local ALIGN = core.ALIGN local cpair = core.cpair -- create an ID check list entry ----@param parent graphics_element parent +---@param parent ListBox parent ---@param msg string message ---@param fail_code integer failure code local function init(parent, msg, fail_code) -- root div - local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} + local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright} local fg_bg = cpair(colors.black,colors.yellow) diff --git a/supervisor/panel/components/pdg_entry.lua b/supervisor/panel/components/pdg_entry.lua index de68b8cf..368f4bcf 100644 --- a/supervisor/panel/components/pdg_entry.lua +++ b/supervisor/panel/components/pdg_entry.lua @@ -8,17 +8,17 @@ local style = require("supervisor.panel.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") -local DataIndicator = require("graphics.elements.indicators.data") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") local ALIGN = core.ALIGN local cpair = core.cpair -- create a pocket diagnostics list entry ----@param parent graphics_element parent +---@param parent ListBox parent ---@param id integer PDG session ID local function init(parent, id) local s_hi_box = style.theme.highlight_box @@ -26,7 +26,7 @@ local function init(parent, id) local label_fg = style.fp.label_fg -- root div - local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} + local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright} local ps_prefix = "pdg_" .. id .. "_" diff --git a/supervisor/panel/components/rtu_entry.lua b/supervisor/panel/components/rtu_entry.lua index edb9b3ea..2de0c0fd 100644 --- a/supervisor/panel/components/rtu_entry.lua +++ b/supervisor/panel/components/rtu_entry.lua @@ -8,17 +8,17 @@ local style = require("supervisor.panel.style") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local TextBox = require("graphics.elements.TextBox") -local DataIndicator = require("graphics.elements.indicators.data") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") local ALIGN = core.ALIGN local cpair = core.cpair -- create an RTU list entry ----@param parent graphics_element parent +---@param parent ListBox parent ---@param id integer RTU session ID local function init(parent, id) local s_hi_box = style.theme.highlight_box @@ -26,7 +26,7 @@ local function init(parent, id) local label_fg = style.fp.label_fg -- root div - local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} + local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright} local ps_prefix = "rtu_" .. id .. "_" diff --git a/supervisor/panel/front_panel.lua b/supervisor/panel/front_panel.lua index 245122e0..e31d8bb8 100644 --- a/supervisor/panel/front_panel.lua +++ b/supervisor/panel/front_panel.lua @@ -16,15 +16,15 @@ local rtu_entry = require("supervisor.panel.components.rtu_entry") local core = require("graphics.core") -local Div = require("graphics.elements.div") -local ListBox = require("graphics.elements.listbox") -local MultiPane = require("graphics.elements.multipane") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local MultiPane = require("graphics.elements.MultiPane") +local TextBox = require("graphics.elements.TextBox") -local TabBar = require("graphics.elements.controls.tabbar") +local TabBar = require("graphics.elements.controls.TabBar") -local LED = require("graphics.elements.indicators.led") -local DataIndicator = require("graphics.elements.indicators.data") +local LED = require("graphics.elements.indicators.LED") +local DataIndicator = require("graphics.elements.indicators.DataIndicator") local ALIGN = core.ALIGN @@ -33,7 +33,7 @@ local cpair = core.cpair local ind_grn = style.ind_grn -- create new front panel view ----@param panel graphics_element main displaybox +---@param panel DisplayBox main displaybox local function init(panel) local s_hi_box = style.theme.highlight_box local s_hi_bright = style.theme.highlight_box_bright @@ -120,7 +120,7 @@ local function init(panel) local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true} local rtu_list = ListBox{parent=rtu_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} - local _ = Div{parent=rtu_list,height=1,hidden=true} -- padding + local _ = Div{parent=rtu_list,height=1} -- padding -- coordinator session page @@ -148,13 +148,13 @@ local function init(panel) local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true} local pdg_list = ListBox{parent=pkt_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} - local _ = Div{parent=pdg_list,height=1,hidden=true} -- padding + local _ = Div{parent=pdg_list,height=1} -- padding -- RTU device ID check/diagnostics page local chk_page = Div{parent=page_div,x=1,y=1,hidden=true} local chk_list = ListBox{parent=chk_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} - local _ = Div{parent=chk_list,height=1,hidden=true} -- padding + local _ = Div{parent=chk_list,height=1} -- padding -- info page diff --git a/supervisor/panel/pgi.lua b/supervisor/panel/pgi.lua index 2d8ee93b..5eb35c66 100644 --- a/supervisor/panel/pgi.lua +++ b/supervisor/panel/pgi.lua @@ -8,23 +8,28 @@ local util = require("scada-common.util") local pgi = {} local data = { - rtu_list = nil, ---@type nil|graphics_element - pdg_list = nil, ---@type nil|graphics_element - chk_list = nil, ---@type nil|graphics_element - rtu_entry = nil, ---@type function - pdg_entry = nil, ---@type function - chk_entry = nil, ---@type function + rtu_list = nil, ---@type ListBox|nil + pdg_list = nil, ---@type ListBox|nil + chk_list = nil, ---@type ListBox|nil + rtu_entry = nil, ---@type function + pdg_entry = nil, ---@type function + chk_entry = nil, ---@type function -- list entries - entries = { rtu = {}, pdg = {}, chk = {}, missing = {} } + entries = { + rtu = {}, ---@type Div[] + pdg = {}, ---@type Div[] + chk = {}, ---@type Div[][] + missing = {} ---@type Div[] + } } -- link list boxes ----@param rtu_list graphics_element RTU list element ----@param rtu_entry function RTU entry constructor ----@param pdg_list graphics_element pocket diagnostics list element ----@param pdg_entry function pocket diagnostics entry constructor ----@param chk_list graphics_element CHK list element ----@param chk_entry function CHK entry constructor +---@param rtu_list ListBox RTU list element +---@param rtu_entry fun(parent: ListBox, id: integer) : Div RTU entry constructor +---@param pdg_list ListBox pocket diagnostics list element +---@param pdg_entry fun(parent: ListBox, id: integer) : Div pocket diagnostics entry constructor +---@param chk_list ListBox CHK list element +---@param chk_entry fun(parent: ListBox, msg: string, fail_code: integer) : Div CHK entry constructor function pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry, chk_list, chk_entry) data.rtu_list = rtu_list data.pdg_list = pdg_list @@ -130,8 +135,8 @@ function pgi.create_chk_entry(unit, fail_code, msg) end end --- delete a device ID check failure entry from the CHK list ----@note this assumes only one type of failure can occur per each RTU gateway session's RTU, which is the case +-- delete a device ID check failure entry from the CHK list
+-- this assumes only one type of failure can occur per each RTU gateway session's RTU, which is the case ---@param unit unit_session RTU session function pgi.delete_chk_entry(unit) local gw_session = unit.get_session_id() diff --git a/supervisor/renderer.lua b/supervisor/renderer.lua index fde7fc3f..57c6b4b0 100644 --- a/supervisor/renderer.lua +++ b/supervisor/renderer.lua @@ -9,7 +9,7 @@ local style = require("supervisor.panel.style") local core = require("graphics.core") local flasher = require("graphics.flasher") -local DisplayBox = require("graphics.elements.displaybox") +local DisplayBox = require("graphics.elements.DisplayBox") ---@class supervisor_renderer local renderer = {} diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index 8b9c0d9a..771e210a 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -128,7 +128,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim local unit_builds = {} for i = 1, #self.units do - local unit = self.units[i] ---@type reactor_unit + local unit = self.units[i] unit_builds[unit.get_id()] = unit.get_build() end @@ -145,7 +145,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim local builds = {} for i = 1, #self.units do - local unit = self.units[i] ---@type reactor_unit + local unit = self.units[i] builds[unit.get_id()] = unit.get_build() end @@ -168,7 +168,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim local status = {} for i = 1, #self.units do - local unit = self.units[i] ---@type reactor_unit + local unit = self.units[i] status[unit.get_id()] = { unit.get_reactor_status(), @@ -308,7 +308,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim -- continue if valid unit id if util.is_int(uid) and uid > 0 and uid <= #self.units then - local unit = self.units[uid] ---@type reactor_unit + local unit = self.units[uid] local manual = facility.get_group(uid) == AUTO_GROUP.MANUAL if cmd == UNIT_COMMAND.SCRAM then @@ -432,8 +432,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim local unit_id = cmd.val local builds = {} - local unit = self.units[unit_id] ---@type reactor_unit - builds[unit_id] = unit.get_build(-1) + builds[unit_id] = self.units[unit_id].get_build(-1) _send(CRDN_TYPE.UNIT_BUILDS, { builds }) elseif cmd.key == CRD_S_DATA.RESEND_RTU_BUILD then @@ -446,8 +445,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim local builds = {} - local unit = self.units[unit_id] ---@type reactor_unit - builds[unit_id] = unit.get_build(cmd.val.type) + builds[unit_id] = self.units[unit_id].get_build(cmd.val.type) _send(CRDN_TYPE.UNIT_BUILDS, { builds }) else diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 8850a168..bedbd8be 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -98,76 +98,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue, rps_reset = true }, -- session database - ---@class reactor_db - sDB = { - auto_ack_token = 0, - last_status_update = 0, - control_state = false, - no_reactor = false, - formed = false, - rps_tripped = false, - rps_trip_cause = "ok", ---@type rps_trip_cause - max_op_temp_H2O = 1200, - max_op_temp_Na = 1200, - ---@class rps_status - rps_status = { - high_dmg = false, - high_temp = false, - low_cool = false, - ex_waste = false, - ex_hcool = false, - no_fuel = false, - fault = false, - timeout = false, - manual = false, - automatic = false, - sys_fail = false, - force_dis = false - }, - ---@class mek_status - mek_status = { - heating_rate = 0.0, - - status = false, - burn_rate = 0.0, - act_burn_rate = 0.0, - temp = 0.0, - damage = 0.0, - boil_eff = 0.0, - env_loss = 0.0, - - fuel = 0, - fuel_need = 0, - fuel_fill = 0.0, - waste = 0, - waste_need = 0, - waste_fill = 0.0, - ccool_type = types.FLUID.EMPTY_GAS, ---@type fluid - ccool_amnt = 0, - ccool_need = 0, - ccool_fill = 0.0, - hcool_type = types.FLUID.EMPTY_GAS, ---@type fluid - hcool_amnt = 0, - hcool_need = 0, - hcool_fill = 0.0 - }, - ---@class mek_struct - mek_struct = { - length = 0, - width = 0, - height = 0, - min_pos = types.new_zero_coordinate(), - max_pos = types.new_zero_coordinate(), - heat_cap = 0, - fuel_asm = 0, - fuel_sa = 0, - fuel_cap = 0, - waste_cap = 0, - ccool_cap = 0, - hcool_cap = 0, - max_burn = 0.0 - } - } + sDB = types.new_reactor_db() } ---@class plc_session diff --git a/supervisor/session/rsctl.lua b/supervisor/session/rsctl.lua index a3699375..b2702678 100644 --- a/supervisor/session/rsctl.lua +++ b/supervisor/session/rsctl.lua @@ -8,7 +8,7 @@ local rsctl = {} -- create a new redstone RTU I/O controller ---@nodiscard ----@param redstone_rtus table redstone RTU sessions +---@param redstone_rtus redstone_session[] redstone RTU sessions function rsctl.new(redstone_rtus) ---@class rs_controller local public = {} @@ -18,8 +18,7 @@ function rsctl.new(redstone_rtus) ---@return boolean function public.is_connected(port) for i = 1, #redstone_rtus do - local db = redstone_rtus[i].get_db() ---@type redstone_session_db - if db.io[port] ~= nil then return true end + if redstone_rtus[i].get_db().io[port] ~= nil then return true end end return false @@ -30,8 +29,7 @@ function rsctl.new(redstone_rtus) ---@param value boolean function public.digital_write(port, value) for i = 1, #redstone_rtus do - local db = redstone_rtus[i].get_db() ---@type redstone_session_db - local io = db.io[port] ---@type rs_db_dig_io|nil + local io = redstone_rtus[i].get_db().io[port] if io ~= nil then io.write(value) end end end @@ -42,9 +40,8 @@ function rsctl.new(redstone_rtus) ---@return boolean|nil function public.digital_read(port) for i = 1, #redstone_rtus do - local db = redstone_rtus[i].get_db() ---@type redstone_session_db - local io = db.io[port] ---@type rs_db_dig_io|nil - if io ~= nil then return io.read() end + local io = redstone_rtus[i].get_db().io[port] + if io ~= nil then return io.read() --[[@as boolean|nil]] end end end @@ -55,8 +52,7 @@ function rsctl.new(redstone_rtus) ---@param max number maximum value for scaling 0 to 15 function public.analog_write(port, value, min, max) for i = 1, #redstone_rtus do - local db = redstone_rtus[i].get_db() ---@type redstone_session_db - local io = db.io[port] ---@type rs_db_ana_io|nil + local io = redstone_rtus[i].get_db().io[port] if io ~= nil then io.write(rsio.analog_write(value, min, max)) end end end diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 7d2494a2..a38559c2 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -63,7 +63,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad keep_alive = 0, alarm_tones = 0 }, - units = {} + units = {} ---@type unit_session[] } ---@class rtu_session @@ -80,13 +80,13 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad _reset_config() for i = 1, #self.fac_units do - local unit = self.fac_units[i] ---@type reactor_unit + local unit = self.fac_units[i] unit.purge_rtu_devices(id) facility.purge_rtu_devices(id) end for i = 1, #self.advert do - local unit = nil ---@type unit_session|nil + local unit = nil ---@type rtu_advertisement local unit_advert = { @@ -96,7 +96,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad rsio = self.advert[i][4] } - local u_type = unit_advert.type ---@type integer|boolean + local u_type = unit_advert.type ---@type RTU_UNIT_TYPE|boolean -- validate unit advertisement @@ -127,7 +127,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad log.debug(log_tag .. "_handle_advertisement(): advertisement unit validation failure") else if unit_advert.reactor > 0 then - local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit + local target_unit = self.fac_units[unit_advert.reactor] -- unit RTUs if u_type == RTU_UNIT_TYPE.REDSTONE then @@ -255,8 +255,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad if pkt.scada_frame.protocol() == PROTOCOL.MODBUS_TCP then ---@cast pkt modbus_frame if self.units[pkt.unit_id] ~= nil then - local unit = self.units[pkt.unit_id] ---@type unit_session - unit.handle_packet(pkt) + self.units[pkt.unit_id].handle_packet(pkt) end elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then ---@cast pkt mgmt_frame @@ -298,8 +297,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad if pkt.length == 1 then local unit_id = pkt.data[1] if self.units[unit_id] ~= nil then - local unit = self.units[unit_id] ---@type unit_session - unit.invalidate_cache() + self.units[unit_id].invalidate_cache() end else log.debug(log_tag .. "SCADA RTU GW device re-mount packet length mismatch") diff --git a/supervisor/session/rtu/boilerv.lua b/supervisor/session/rtu/boilerv.lua index 26a8f2d6..a1a1a999 100644 --- a/supervisor/session/rtu/boilerv.lua +++ b/supervisor/session/rtu/boilerv.lua @@ -99,6 +99,7 @@ function boilerv.new(session_id, unit_id, advert, out_queue) } } + ---@class boilerv_session:unit_session local public = self.session.get() -- PRIVATE FUNCTIONS -- diff --git a/supervisor/session/rtu/dynamicv.lua b/supervisor/session/rtu/dynamicv.lua index 13239a78..0c06d7ba 100644 --- a/supervisor/session/rtu/dynamicv.lua +++ b/supervisor/session/rtu/dynamicv.lua @@ -84,7 +84,7 @@ function dynamicv.new(session_id, unit_id, advert, out_queue) }, state = { last_update = 0, - container_mode = CONTAINER_MODE.BOTH ---@type container_mode + container_mode = CONTAINER_MODE.BOTH ---@type container_mode }, tanks = { last_update = 0, @@ -94,6 +94,7 @@ function dynamicv.new(session_id, unit_id, advert, out_queue) } } + ---@class dynamicv_session:unit_session local public = self.session.get() -- PRIVATE FUNCTIONS -- @@ -224,7 +225,7 @@ function dynamicv.new(session_id, unit_id, advert, out_queue) end elseif msg.qtype == mqueue.TYPE.DATA then -- instruction with body - local cmd = msg.message ---@type queue_data + local cmd = msg.message ---@type queue_data if cmd.key == DTV_RTU_S_DATA.SET_CONT_MODE then if cmd.val == types.CONTAINER_MODE.BOTH or cmd.val == types.CONTAINER_MODE.FILL or diff --git a/supervisor/session/rtu/envd.lua b/supervisor/session/rtu/envd.lua index 046ef0af..269975a7 100644 --- a/supervisor/session/rtu/envd.lua +++ b/supervisor/session/rtu/envd.lua @@ -52,6 +52,7 @@ function envd.new(session_id, unit_id, advert, out_queue) } } + ---@class envd_session:unit_session local public = self.session.get() -- PRIVATE FUNCTIONS -- diff --git a/supervisor/session/rtu/imatrix.lua b/supervisor/session/rtu/imatrix.lua index 84bfc1e5..aa7a9845 100644 --- a/supervisor/session/rtu/imatrix.lua +++ b/supervisor/session/rtu/imatrix.lua @@ -83,6 +83,7 @@ function imatrix.new(session_id, unit_id, advert, out_queue) } } + ---@class imatrix_session:unit_session local public = self.session.get() -- PRIVATE FUNCTIONS -- diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua index b99c0d99..ce9d6c4c 100644 --- a/supervisor/session/rtu/redstone.lua +++ b/supervisor/session/rtu/redstone.lua @@ -39,9 +39,13 @@ local PERIODICS = { OUTPUT_SYNC = 200 } ----@class phy_entry ----@field phy IO_LVL ----@field req IO_LVL +---@class dig_phy_entry +---@field phy IO_LVL actual value +---@field req IO_LVL commanded value + +---@class ana_phy_entry +---@field phy number actual value +---@field req number commanded value -- create a new redstone rtu session runner ---@nodiscard @@ -72,27 +76,29 @@ function redstone.new(session_id, unit_id, advert, out_queue) }, ---@class rs_io_list io_list = { - digital_in = {}, -- discrete inputs - digital_out = {}, -- coils - analog_in = {}, -- input registers - analog_out = {} -- holding registers + digital_in = {}, ---@type IO_PORT[] discrete inputs + digital_out = {}, ---@type IO_PORT[] coils + analog_in = {}, ---@type IO_PORT[] input registers + analog_out = {} ---@type IO_PORT[] holding registers }, phy_trans = { coils = -1, hold_regs = -1 }, -- last set/read ports (reflecting the current state of the RTU) ---@class rs_io_states phy_io = { - digital_in = {}, -- discrete inputs - digital_out = {}, -- coils - analog_in = {}, -- input registers - analog_out = {} -- holding registers + digital_in = {}, ---@type dig_phy_entry[] discrete inputs + digital_out = {}, ---@type dig_phy_entry[] coils + analog_in = {}, ---@type ana_phy_entry[] input registers + analog_out = {} ---@type ana_phy_entry[] holding registers }, ---@class redstone_session_db db = { -- read/write functions for connected I/O + ---@type (rs_db_dig_io|rs_db_ana_io)[] io = {} } } + ---@class redstone_session:unit_session local public = self.session.get() -- INITIALIZE -- diff --git a/supervisor/session/rtu/sna.lua b/supervisor/session/rtu/sna.lua index a75e1858..7e0fc340 100644 --- a/supervisor/session/rtu/sna.lua +++ b/supervisor/session/rtu/sna.lua @@ -74,6 +74,7 @@ function sna.new(session_id, unit_id, advert, out_queue) } } + ---@class sna_session:unit_session local public = self.session.get() -- PRIVATE FUNCTIONS -- diff --git a/supervisor/session/rtu/sps.lua b/supervisor/session/rtu/sps.lua index a631e584..1dacd61e 100644 --- a/supervisor/session/rtu/sps.lua +++ b/supervisor/session/rtu/sps.lua @@ -88,6 +88,7 @@ function sps.new(session_id, unit_id, advert, out_queue) } } + ---@class sps_session:unit_session local public = self.session.get() -- PRIVATE FUNCTIONS -- diff --git a/supervisor/session/rtu/turbinev.lua b/supervisor/session/rtu/turbinev.lua index 4541e566..3581884b 100644 --- a/supervisor/session/rtu/turbinev.lua +++ b/supervisor/session/rtu/turbinev.lua @@ -95,7 +95,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue) flow_rate = 0, prod_rate = 0, steam_input_rate = 0, - dumping_mode = DUMPING_MODE.IDLE ---@type dumping_mode + dumping_mode = DUMPING_MODE.IDLE ---@type dumping_mode }, tanks = { last_update = 0, @@ -109,6 +109,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue) } } + ---@class turbinev_session:unit_session local public = self.session.get() -- PRIVATE FUNCTIONS -- @@ -254,7 +255,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue) end elseif msg.qtype == mqueue.TYPE.DATA then -- instruction with body - local cmd = msg.message ---@type queue_data + local cmd = msg.message ---@type queue_data if cmd.key == TBV_RTU_S_DATA.SET_DUMP_MODE then if cmd.val == types.DUMPING_MODE.IDLE or cmd.val == types.DUMPING_MODE.DUMPING_EXCESS or diff --git a/supervisor/session/rtu/txnctrl.lua b/supervisor/session/rtu/txnctrl.lua index 25ab3edd..cb8de472 100644 --- a/supervisor/session/rtu/txnctrl.lua +++ b/supervisor/session/rtu/txnctrl.lua @@ -6,7 +6,8 @@ local util = require("scada-common.util") local txnctrl = {} -local TIMEOUT = 2000 -- 2000ms max wait +-- 2000ms max wait +local TIMEOUT = 2000 -- create a new transaction controller ---@nodiscard diff --git a/supervisor/session/rtu/unit_session.lua b/supervisor/session/rtu/unit_session.lua index 4f516c8b..632890b1 100644 --- a/supervisor/session/rtu/unit_session.lua +++ b/supervisor/session/rtu/unit_session.lua @@ -29,7 +29,7 @@ unit_session.RTU_US_DATA = RTU_US_DATA ---@param advert rtu_advertisement RTU advertisement for this unit ---@param out_queue mqueue send queue ---@param log_tag string logging tag ----@param txn_tags table transaction log tags +---@param txn_tags string[] transaction log tags function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_tags) local self = { device_index = advert.index, @@ -52,7 +52,7 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t -- send a MODBUS message, creating a transaction in the process ---@param txn_type integer transaction type ---@param f_code MODBUS_FCODE function code - ---@param register_param table register range or register and values + ---@param register_param (number|string)[] register range or register and values ---@return integer txn_id transaction ID of this transaction function protected.send_request(txn_type, f_code, register_param) local m_pkt = comms.modbus_packet() @@ -164,7 +164,6 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t function public.get_cmd_queue() return protected.in_q end -- close this unit - ---@nodiscard function public.close() self.connected = false end -- check if this unit is connected ---@nodiscard diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 30ef729b..6090eff0 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -40,13 +40,26 @@ local SESSION_TYPE = { svsessions.SESSION_TYPE = SESSION_TYPE local self = { - nic = nil, ---@type nic|nil + -- references to supervisor state and other data + nic = nil, ---@type nic|nil fp_ok = false, - config = nil, ---@type svr_config - facility = nil, ---@type facility|nil - sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} }, + config = nil, ---@type svr_config + facility = nil, ---@type facility|nil + -- lists of connected sessions + sessions = { + rtu = {}, ---@type rtu_session_struct + plc = {}, ---@type plc_session_struct + crd = {}, ---@type crd_session_struct + pdg = {} ---@type pdg_session_struct + }, + -- next session IDs next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 }, - dev_dbg = { duplicate = {}, out_of_range = {}, connected = {} } + -- rtu device tracking and invalid assignment detection + dev_dbg = { + duplicate = {}, ---@type unit_session[] + out_of_range = {}, ---@type unit_session[] + connected = {} ---@type { induction: boolean, sps: boolean, tanks: boolean[], units: unit_connections[] } + } } ---@alias sv_session_structs plc_session_struct|rtu_session_struct|crd_session_struct|pdg_session_struct @@ -119,10 +132,10 @@ local function _sv_handle_outq(session) end -- iterate all the given sessions ----@param sessions table +---@param sessions sv_session_structs[] local function _iterate(sessions) for i = 1, #sessions do - local session = sessions[i] ---@type sv_session_structs + local session = sessions[i] if session.open and session.instance.iterate() then _sv_handle_outq(session) @@ -150,20 +163,20 @@ local function _shutdown(session) end -- close connections ----@param sessions table +---@param sessions sv_session_structs[] local function _close(sessions) for i = 1, #sessions do - local session = sessions[i] ---@type sv_session_structs + local session = sessions[i] if session.open then _shutdown(session) end end end -- check if a watchdog timer event matches that of one of the provided sessions ----@param sessions table +---@param sessions sv_session_structs[] ---@param timer_event number local function _check_watchdogs(sessions, timer_event) for i = 1, #sessions do - local session = sessions[i] ---@type sv_session_structs + local session = sessions[i] if session.open then local triggered = session.instance.check_wd(timer_event) if triggered then @@ -175,8 +188,9 @@ local function _check_watchdogs(sessions, timer_event) end -- delete any closed sessions ----@param sessions table +---@param sessions sv_session_structs[] local function _free_closed(sessions) + ---@param session sv_session_structs local f = function (session) return session.open end ---@param session sv_session_structs @@ -189,7 +203,7 @@ end -- find a session by computer ID ---@nodiscard ----@param list table +---@param list sv_session_structs[] ---@param s_addr integer ---@return sv_session_structs|nil local function _find_session(list, s_addr) @@ -273,7 +287,7 @@ end -- on attempted link of an RTU to a facility or unit object, verify its ID and report a problem if it can't be accepted ---@param unit unit_session RTU session ----@param list table table of RTU sessions +---@param list unit_session[] table of RTU sessions ---@param max integer max of this type of RTU ---@return RTU_ID_FAIL fail_code, string fail_str function svsessions.check_rtu_id(unit, list, max) @@ -366,7 +380,7 @@ function svsessions.init(nic, fp_ok, config, facility) for i = 1, config.UnitCount do local r_cool = cool_conf.r_cool[i] - local conns = { boilers = {}, turbines = {}, tanks = {} } + local conns = { boilers = {}, turbines = {}, tanks = {} } ---@type unit_connections for b = 1, r_cool.BoilerCount do conns.boilers[b] = true end for t = 1, r_cool.TurbineCount do conns.turbines[t] = true end @@ -633,8 +647,8 @@ function svsessions.iterate_all() -- iterate sessions for _, list in pairs(self.sessions) do _iterate(list) end - -- report RTU sessions to facility - self.facility.report_rtus(self.sessions.rtu) + -- report RTU gateway sessions to facility + self.facility.report_rtu_gateways(self.sessions.rtu) -- iterate facility self.facility.update() diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 4f6726c8..537b263d 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.3" +local SUPERVISOR_VERSION = "v1.5.10" local println = util.println local println_ts = util.println_ts @@ -157,6 +157,7 @@ local function main() if type ~= nil and device ~= nil then if type == "modem" then + ---@cast device Modem -- we only care if this is our wireless modem if nic.is_modem(device) then nic.disconnect() @@ -181,6 +182,7 @@ local function main() if type ~= nil and device ~= nil then if type == "modem" then + ---@cast device Modem if device.isWireless() and not nic.is_connected() then -- reconnected modem nic.connect(device) diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 3290af0f..16f065a1 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -81,13 +81,13 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) num_turbines = num_turbines, types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE }, -- rtus - rtu_list = {}, - redstone = {}, - boilers = {}, - turbines = {}, - tanks = {}, - snas = {}, - envd = {}, + rtu_list = {}, ---@type unit_session[][] + redstone = {}, ---@type redstone_session[] + boilers = {}, ---@type boilerv_session[] + turbines = {}, ---@type turbinev_session[] + tanks = {}, ---@type dynamicv_session[] + snas = {}, ---@type sna_session[] + envd = {}, ---@type envd_session[] -- redstone control io_ctl = nil, ---@type rs_controller valves = {}, ---@type unit_valves @@ -100,7 +100,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) auto_was_alarmed = false, ramp_target_br100 = 0, -- state tracking - deltas = {}, + deltas = {}, ---@type { last_t: number, last_v: number, dt: number }[] last_heartbeat = 0, last_radiation = 0, damage_decreasing = false, @@ -108,12 +108,12 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) damage_start = 0, damage_last = 0, damage_est_last = 0, - waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT + waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT status_text = { "UNKNOWN", "awaiting connection..." }, -- logic for alarms had_reactor = false, turbine_flow_stable = false, - turbine_stability_data = {}, + turbine_stability_data = {}, ---@type { time_state: integer, time_tanks: integer, rotation: number, input_rate: integer }[] last_rate_change_ms = 0, ---@type rps_status last_rps_trips = { @@ -154,7 +154,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) waste = 0, high_temp_lim = 1150 }, - ---@class alarm_monitors + ---@type { [string]: alarm_def } alarms = { -- reactor lost under the condition of meltdown imminent ContainmentBreach = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ContainmentBreach, tier = PRIO.CRITICAL }, @@ -211,17 +211,17 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) SteamFeedMismatch = false, MaxWaterReturnFeed = false, -- boilers - BoilerOnline = {}, - HeatingRateLow = {}, - WaterLevelLow = {}, + BoilerOnline = {}, ---@type boolean[] + HeatingRateLow = {}, ---@type boolean[] + WaterLevelLow = {}, ---@type boolean[] -- turbines - TurbineOnline = {}, - SteamDumpOpen = {}, - TurbineOverSpeed = {}, - GeneratorTrip = {}, - TurbineTrip = {} + TurbineOnline = {}, ---@type boolean[] + SteamDumpOpen = {}, ---@type integer[] + TurbineOverSpeed = {}, ---@type boolean[] + GeneratorTrip = {}, ---@type boolean[] + TurbineTrip = {} ---@type boolean[] }, - ---@class alarms + ---@type { [ALARM]: ALARM_STATE } alarm_states = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, @@ -244,7 +244,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) blade_count = 0, br100 = 0, lim_br100 = 0, - waste_mode = WASTE_MODE.AUTO ---@type WASTE_MODE + waste_mode = WASTE_MODE.AUTO ---@type WASTE_MODE } } } @@ -324,8 +324,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) end for i = 1, #self.boilers do - local boiler = self.boilers[i] ---@type unit_session - local db = boiler.get_db() ---@type boilerv_session_db + local boiler = self.boilers[i] + local db = boiler.get_db() local last_update_s = db.tanks.last_update / 1000.0 @@ -336,8 +336,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) end for i = 1, #self.turbines do - local turbine = self.turbines[i] ---@type unit_session - local db = turbine.get_db() ---@type turbinev_session_db + local turbine = self.turbines[i] + local db = turbine.get_db() local last_update_s = db.tanks.last_update / 1000.0 @@ -553,8 +553,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) -- check boilers formed/faulted for i = 1, #self.boilers do - local sess = self.boilers[i] ---@type unit_session - local boiler = sess.get_db() ---@type boilerv_session_db + local sess = self.boilers[i] + local boiler = sess.get_db() if sess.is_faulted() or not boiler.formed then self.db.control.degraded = true end @@ -562,8 +562,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) -- check turbines formed/faulted for i = 1, #self.turbines do - local sess = self.turbines[i] ---@type unit_session - local turbine = sess.get_db() ---@type turbinev_session_db + local sess = self.turbines[i] + local turbine = sess.get_db() if sess.is_faulted() or not turbine.formed then self.db.control.degraded = true end @@ -881,7 +881,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) if all or (filter == RTU_UNIT_TYPE.BOILER_VALVE) then build.boilers = {} for i = 1, #self.boilers do - local boiler = self.boilers[i] ---@type unit_session + local boiler = self.boilers[i] build.boilers[boiler.get_device_idx()] = { boiler.get_db().formed, boiler.get_db().build } end end @@ -889,7 +889,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) if all or (filter == RTU_UNIT_TYPE.TURBINE_VALVE) then build.turbines = {} for i = 1, #self.turbines do - local turbine = self.turbines[i] ---@type unit_session + local turbine = self.turbines[i] build.turbines[turbine.get_device_idx()] = { turbine.get_db().formed, turbine.get_db().build } end end @@ -897,7 +897,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) if all or (filter == RTU_UNIT_TYPE.DYNAMIC_VALVE) then build.tanks = {} for i = 1, #self.tanks do - local tank = self.tanks[i] ---@type unit_session + local tank = self.tanks[i] build.tanks[tank.get_device_idx()] = { tank.get_db().formed, tank.get_db().build } end end @@ -927,6 +927,10 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) -- check which RTUs are connected ---@nodiscard function public.check_rtu_conns() + ---@class unit_connections + ---@field boilers boolean[] + ---@field turbines boolean[] + ---@field tanks boolean[] local conns = {} conns.boilers = {} @@ -955,31 +959,31 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) -- status of boilers (including tanks) status.boilers = {} for i = 1, #self.boilers do - local boiler = self.boilers[i] ---@type unit_session - local db = boiler.get_db() ---@type boilerv_session_db + local boiler = self.boilers[i] + local db = boiler.get_db() status.boilers[boiler.get_device_idx()] = { boiler.is_faulted(), db.formed, db.state, db.tanks } end -- status of turbines (including tanks) status.turbines = {} for i = 1, #self.turbines do - local turbine = self.turbines[i] ---@type unit_session - local db = turbine.get_db() ---@type turbinev_session_db + local turbine = self.turbines[i] + local db = turbine.get_db() status.turbines[turbine.get_device_idx()] = { turbine.is_faulted(), db.formed, db.state, db.tanks } end -- status of dynamic tanks status.tanks = {} for i = 1, #self.tanks do - local tank = self.tanks[i] ---@type unit_session - local db = tank.get_db() ---@type dynamicv_session_db + local tank = self.tanks[i] + local db = tank.get_db() status.tanks[tank.get_device_idx()] = { tank.is_faulted(), db.formed, db.state, db.tanks } end -- 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 + local db = self.snas[i].get_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) @@ -989,8 +993,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) -- radiation monitors (environment detectors) status.envds = {} for i = 1, #self.envd do - local envd = self.envd[i] ---@type unit_session - local db = envd.get_db() ---@type envd_session_db + local envd = self.envd[i] + local db = envd.get_db() status.envds[envd.get_device_idx()] = { envd.is_faulted(), db.radiation, db.radiation_raw } end @@ -1004,7 +1008,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) local total_avail_rate = 0 for i = 1, #self.snas do - local db = self.snas[i].get_db() ---@type sna_session_db + local db = self.snas[i].get_db() total_avail_rate = total_avail_rate + db.state.production_rate end diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua index b1e1fc89..f0e4c332 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unitlogic.lua @@ -161,8 +161,8 @@ function logic.update_annunciator(self) local max_rad, any_faulted = 0, false for i = 1, #self.envd do - local envd = self.envd[i] ---@type unit_session - local db = envd.get_db() ---@type envd_session_db + local envd = self.envd[i] + local db = envd.get_db() any_faulted = any_faulted or envd.is_faulted() if db.radiation_raw > max_rad then max_rad = db.radiation_raw end end @@ -173,8 +173,7 @@ function logic.update_annunciator(self) annunc.EmergencyCoolant = 1 for i = 1, #self.redstone do - local db = self.redstone[i].get_db() ---@type redstone_session_db - local io = db.io[IO.U_EMER_COOL] ---@type rs_db_dig_io|nil + local io = self.redstone[i].get_db().io[IO.U_EMER_COOL] if io ~= nil then annunc.EmergencyCoolant = util.trinary(io.read(), 3, 2) break @@ -197,8 +196,8 @@ function logic.update_annunciator(self) if num_boilers > 0 then -- go through boilers for stats and online for i = 1, #self.boilers do - local session = self.boilers[i] ---@type unit_session - local boiler = session.get_db() ---@type boilerv_session_db + local session = self.boilers[i] + local boiler = session.get_db() local idx = session.get_device_idx() annunc.RCSFault = annunc.RCSFault or (not boiler.formed) or session.is_faulted() @@ -225,9 +224,9 @@ function logic.update_annunciator(self) -- check for inactive boilers while reactor is active for i = 1, #self.boilers do - local boiler = self.boilers[i] ---@type unit_session + local boiler = self.boilers[i] local idx = boiler.get_device_idx() - local db = boiler.get_db() ---@type boilerv_session_db + local db = boiler.get_db() if r_db.mek_status.status then annunc.HeatingRateLow[idx] = db.state.boil_rate == 0 @@ -250,9 +249,9 @@ function logic.update_annunciator(self) if num_boilers > 0 then for i = 1, #self.boilers do - local boiler = self.boilers[i] ---@type unit_session + local boiler = self.boilers[i] local idx = boiler.get_device_idx() - local db = boiler.get_db() ---@type boilerv_session_db + local db = boiler.get_db() local gaining_hc = _get_dt(DT_KEYS.BoilerHCool .. idx) > 10.0 or db.tanks.hcool_fill == 1 @@ -294,8 +293,8 @@ function logic.update_annunciator(self) -- go through turbines for stats and online for i = 1, #self.turbines do - local session = self.turbines[i] ---@type unit_session - local turbine = session.get_db() ---@type turbinev_session_db + local session = self.turbines[i] + local turbine = session.get_db() local idx = session.get_device_idx() annunc.RCSFault = annunc.RCSFault or (not turbine.formed) or session.is_faulted() @@ -380,8 +379,8 @@ function logic.update_annunciator(self) -- turbine safety checks for i = 1, #self.turbines do - local turbine = self.turbines[i] ---@type unit_session - local db = turbine.get_db() ---@type turbinev_session_db + local turbine = self.turbines[i] + local db = turbine.get_db() local idx = turbine.get_device_idx() -- check if steam dumps are open @@ -652,7 +651,7 @@ function logic.update_status_text(self) -- check if an alarm is active (tripped or ack'd) ---@nodiscard - ---@param alarm table alarm entry + ---@param alarm alarm_def alarm entry ---@return boolean active local function is_active(alarm) return alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED @@ -818,7 +817,7 @@ function logic.handle_redstone(self) -- check if an alarm is active (tripped or ack'd) ---@nodiscard - ---@param alarm table alarm entry + ---@param alarm alarm_def alarm entry ---@return boolean active local function is_active(alarm) return alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED @@ -904,8 +903,8 @@ function logic.handle_redstone(self) if not cache.rps_trip then -- set turbines to not dump steam for i = 1, #self.turbines do - local session = self.turbines[i] ---@type unit_session - local turbine = session.get_db() ---@type turbinev_session_db + local session = self.turbines[i] + local turbine = session.get_db() if turbine.state.dumping_mode ~= DUMPING_MODE.IDLE then session.get_cmd_queue().push_data(TBV_RTU_S_DATA.SET_DUMP_MODE, DUMPING_MODE.IDLE) @@ -921,8 +920,8 @@ function logic.handle_redstone(self) elseif enable_emer_cool or self.emcool_opened then -- set turbines to dump excess steam for i = 1, #self.turbines do - local session = self.turbines[i] ---@type unit_session - local turbine = session.get_db() ---@type turbinev_session_db + local session = self.turbines[i] + local turbine = session.get_db() if turbine.state.dumping_mode ~= DUMPING_MODE.DUMPING_EXCESS then session.get_cmd_queue().push_data(TBV_RTU_S_DATA.SET_DUMP_MODE, DUMPING_MODE.DUMPING_EXCESS) @@ -931,8 +930,8 @@ function logic.handle_redstone(self) -- make sure dynamic tanks are allowing outflow for i = 1, #self.tanks do - local session = self.tanks[i] ---@type unit_session - local tank = session.get_db() ---@type dynamicv_session_db + local session = self.tanks[i] + local tank = session.get_db() if tank.state.container_mode == CONTAINER_MODE.FILL then session.get_cmd_queue().push_data(DTV_RTU_S_DATA.SET_CONT_MODE, CONTAINER_MODE.BOTH)