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