319 lines
13 KiB
Lua
319 lines
13 KiB
Lua
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
|