cc-mek-scada/coordinator/coordinator.lua

763 lines
34 KiB
Lua
Raw Normal View History

local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
2023-07-10 03:31:56 +00:00
local types = require("scada-common.types")
2024-03-24 16:56:51 +00:00
local themes = require("graphics.themes")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local apisessions = require("coordinator.session.apisessions")
2023-02-21 16:05:57 +00:00
local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
2023-08-30 20:45:48 +00:00
local MGMT_TYPE = comms.MGMT_TYPE
local CRDN_TYPE = comms.CRDN_TYPE
2023-02-21 16:05:57 +00:00
local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND
2022-05-10 21:09:02 +00:00
local LINK_TIMEOUT = 60.0
local coordinator = {}
---@type crd_config
local config = {}
coordinator.config = config
-- load the coordinator configuration<br>
-- status of 0 is OK, 1 is bad config, 2 is bad monitor config
2024-02-18 21:49:39 +00:00
---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message)
function coordinator.load_config()
if not settings.load("/coordinator.settings") then return 1 end
config.UnitCount = settings.get("UnitCount")
config.SpeakerVolume = settings.get("SpeakerVolume")
config.Time24Hour = settings.get("Time24Hour")
config.TempScale = settings.get("TempScale")
config.DisableFlowView = settings.get("DisableFlowView")
config.MainDisplay = settings.get("MainDisplay")
config.FlowDisplay = settings.get("FlowDisplay")
config.UnitDisplays = settings.get("UnitDisplays")
config.SVR_Channel = settings.get("SVR_Channel")
config.CRD_Channel = settings.get("CRD_Channel")
config.PKT_Channel = settings.get("PKT_Channel")
config.SVR_Timeout = settings.get("SVR_Timeout")
config.API_Timeout = settings.get("API_Timeout")
config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug")
config.MainTheme = settings.get("MainTheme")
config.FrontPanelTheme = settings.get("FrontPanelTheme")
config.ColorMode = settings.get("ColorMode")
local cfv = util.new_validator()
cfv.assert_type_int(config.UnitCount)
cfv.assert_range(config.UnitCount, 1, 4)
cfv.assert_type_bool(config.Time24Hour)
cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_bool(config.DisableFlowView)
cfv.assert_type_table(config.UnitDisplays)
cfv.assert_type_num(config.SpeakerVolume)
2024-02-18 21:49:39 +00:00
cfv.assert_range(config.SpeakerVolume, 0, 3)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.CRD_Channel)
cfv.assert_channel(config.PKT_Channel)
cfv.assert_type_num(config.SVR_Timeout)
cfv.assert_min(config.SVR_Timeout, 2)
cfv.assert_type_num(config.API_Timeout)
cfv.assert_min(config.API_Timeout, 2)
cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0)
cfv.assert_type_str(config.AuthKey)
if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
2024-04-12 00:30:54 +00:00
cfv.assert(len == 0 or len >= 8)
end
cfv.assert_type_int(config.LogMode)
cfv.assert_range(config.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath)
2024-03-11 16:35:06 +00:00
cfv.assert_type_bool(config.LogDebug)
cfv.assert_type_int(config.MainTheme)
cfv.assert_range(config.MainTheme, 1, 2)
cfv.assert_type_int(config.FrontPanelTheme)
cfv.assert_range(config.FrontPanelTheme, 1, 2)
cfv.assert_type_int(config.ColorMode)
2024-03-24 16:56:51 +00:00
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
-- Monitor Setup
---@class monitors_struct
local monitors = {
2024-03-06 02:24:17 +00:00
main = nil, ---@type table|nil
main_name = "",
flow = nil, ---@type table|nil
2023-08-10 03:26:06 +00:00
flow_name = "",
unit_displays = {},
unit_name_map = {}
}
local mon_cfv = util.new_validator()
-- get all interface names
local names = {}
for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end
local function setup_monitors()
mon_cfv.assert_type_str(config.MainDisplay)
if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
if mon_cfv.valid() then
2024-02-18 20:30:18 +00:00
local w, h, _
2024-02-18 21:49:39 +00:00
if not util.table_contains(names, config.MainDisplay) then
return 2, "Main monitor is not connected."
end
2024-03-06 02:24:17 +00:00
monitors.main = ppm.get_periph(config.MainDisplay)
monitors.main_name = config.MainDisplay
2023-08-10 03:26:06 +00:00
2024-03-06 02:24:17 +00:00
monitors.main.setTextScale(0.5)
w, _ = ppm.monitor_block_size(monitors.main.getSize())
if w ~= 8 then
return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
end
2023-08-10 03:26:06 +00:00
if not config.DisableFlowView then
2024-02-18 21:49:39 +00:00
if not util.table_contains(names, config.FlowDisplay) then
return 2, "Flow monitor is not connected."
end
2023-08-10 03:26:06 +00:00
monitors.flow = ppm.get_periph(config.FlowDisplay)
monitors.flow_name = config.FlowDisplay
2023-08-10 03:26:06 +00:00
monitors.flow.setTextScale(0.5)
2024-02-18 20:30:18 +00:00
w, _ = ppm.monitor_block_size(monitors.flow.getSize())
if w ~= 8 then
return 2, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
end
end
for i = 1, config.UnitCount do
local display = config.UnitDisplays[i]
2024-02-18 21:49:39 +00:00
if type(display) ~= "string" or not util.table_contains(names, display) then
return 2, "Unit " .. i .. " monitor is not connected."
end
monitors.unit_displays[i] = ppm.get_periph(display)
monitors.unit_name_map[i] = display
monitors.unit_displays[i].setTextScale(0.5)
w, h = ppm.monitor_block_size(monitors.unit_displays[i].getSize())
if w ~= 4 or h ~= 4 then
return 2, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
end
end
else return 2, "Monitor configuration invalid." end
end
if cfv.valid() then
local ok, result, message = pcall(setup_monitors)
assert(ok, util.c("fatal error while trying to verify monitors: ", result))
if result == 2 then return 2, message end
else return 1 end
return 0, monitors
end
2022-07-05 15:18:26 +00:00
-- dmesg print wrapper
---@param message string message
---@param dmesg_tag string tag
2022-07-06 03:48:01 +00:00
---@param working? boolean to use dmesg_working
---@return function? update, function? done
local function log_dmesg(message, dmesg_tag, working)
2022-07-05 15:18:26 +00:00
local colors = {
2024-04-08 00:47:31 +00:00
RENDER = colors.green,
2022-07-05 15:18:26 +00:00
SYSTEM = colors.cyan,
BOOT = colors.blue,
2023-06-25 18:00:18 +00:00
COMMS = colors.purple,
CRYPTO = colors.yellow
2022-07-05 15:18:26 +00:00
}
2022-07-06 03:48:01 +00:00
if working then
return log.dmesg_working(message, dmesg_tag, colors[dmesg_tag])
else
log.dmesg(message, dmesg_tag, colors[dmesg_tag])
end
2022-07-05 15:18:26 +00:00
end
2024-04-08 00:47:31 +00:00
function coordinator.log_render(message) log_dmesg(message, "RENDER") end
2022-07-05 15:18:26 +00:00
function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
2023-06-25 18:00:18 +00:00
function coordinator.log_crypto(message) log_dmesg(message, "CRYPTO") end
2022-07-05 15:18:26 +00:00
2023-02-23 04:09:47 +00:00
-- log a message for communications connecting, providing access to progress indication control functions
---@nodiscard
2022-07-06 03:48:01 +00:00
---@param message string
---@return function update, function done
function coordinator.log_comms_connecting(message)
2023-02-23 04:09:47 +00:00
local update, done = log_dmesg(message, "COMMS", true)
---@cast update function
---@cast done function
return update, done
end
2022-07-06 03:48:01 +00:00
-- coordinator communications
2023-02-23 04:09:47 +00:00
---@nodiscard
---@param version string coordinator version
2023-06-21 23:04:39 +00:00
---@param nic nic network interface device
---@param sv_watchdog watchdog
function coordinator.comms(version, nic, sv_watchdog)
local self = {
2022-07-06 03:48:01 +00:00
sv_linked = false,
2023-06-06 23:41:09 +00:00
sv_addr = comms.BROADCAST,
sv_seq_num = 0,
sv_r_seq_num = nil,
sv_config_err = false,
last_est_ack = ESTABLISH_ACK.ALLOW,
last_api_est_acks = {},
est_start = 0,
est_last = 0,
est_tick_waiting = nil,
est_task_done = nil
}
2022-06-15 19:35:34 +00:00
comms.set_trusted_range(config.TrustedRange)
2023-06-21 23:04:39 +00:00
-- configure network channels
nic.closeAll()
nic.open(config.CRD_Channel)
2022-06-15 19:35:34 +00:00
-- pass config to apisessions
2024-02-19 02:34:25 +00:00
apisessions.init(nic, config)
2024-01-31 19:10:03 +00:00
-- PRIVATE FUNCTIONS --
-- send a packet to the supervisor
2023-08-30 20:45:48 +00:00
---@param msg_type MGMT_TYPE|CRDN_TYPE
---@param msg table
local function _send_sv(protocol, msg_type, msg)
2022-06-15 19:35:34 +00:00
local s_pkt = comms.scada_packet()
local pkt ---@type mgmt_packet|crdn_packet
2023-02-21 16:05:57 +00:00
if protocol == PROTOCOL.SCADA_MGMT then
pkt = comms.mgmt_packet()
2023-02-21 16:05:57 +00:00
elseif protocol == PROTOCOL.SCADA_CRDN then
pkt = comms.crdn_packet()
else
return
end
2022-06-15 19:35:34 +00:00
pkt.make(msg_type, msg)
2023-06-06 23:41:09 +00:00
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
2022-06-15 19:35:34 +00:00
nic.transmit(config.SVR_Channel, config.CRD_Channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1
2022-06-15 19:35:34 +00:00
end
-- send an API establish request response
2023-06-06 23:41:09 +00:00
---@param packet scada_packet
---@param ack ESTABLISH_ACK
local function _send_api_establish_ack(packet, ack)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
2023-08-30 20:45:48 +00:00
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack })
2023-06-06 23:41:09 +00:00
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
2023-06-06 23:41:09 +00:00
self.last_api_est_acks[packet.src_addr()] = ack
end
-- attempt connection establishment
local function _send_establish()
2023-08-30 21:15:42 +00:00
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD })
end
-- keep alive ack
---@param srv_time integer
local function _send_keep_alive_ack(srv_time)
2023-08-30 20:45:48 +00:00
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end
2022-06-15 19:35:34 +00:00
-- PUBLIC FUNCTIONS --
2023-02-23 04:09:47 +00:00
---@class coord_comms
local public = {}
-- try to connect to the supervisor if not already linked
---@param abort boolean? true to print out cancel info if not linked (use on program terminate)
---@return boolean ok, boolean start_ui
function public.try_connect(abort)
local ok = true
local start_ui = false
if not self.sv_linked then
if self.est_tick_waiting == nil then
self.est_start = os.clock()
self.est_last = self.est_start
self.est_tick_waiting, self.est_task_done =
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_Channel)
_send_establish()
else
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (os.clock() - self.est_start)))
end
if abort or (os.clock() - self.est_start) >= LINK_TIMEOUT then
self.est_task_done(false)
if abort then
coordinator.log_comms("supervisor connection attempt cancelled by user")
elseif self.sv_config_err then
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
elseif not self.sv_linked then
if self.last_est_ack == ESTABLISH_ACK.DENY then
coordinator.log_comms("supervisor connection attempt denied")
elseif self.last_est_ack == ESTABLISH_ACK.COLLISION then
coordinator.log_comms("supervisor connection failed due to collision")
elseif self.last_est_ack == ESTABLISH_ACK.BAD_VERSION then
coordinator.log_comms("supervisor connection failed due to version mismatch")
else
coordinator.log_comms("supervisor connection failed with no valid response")
end
end
ok = false
elseif self.sv_config_err then
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
ok = false
elseif (os.clock() - self.est_last) > 1.0 then
_send_establish()
self.est_last = os.clock()
end
elseif self.est_tick_waiting ~= nil then
self.est_task_done(true)
self.est_tick_waiting = nil
self.est_task_done = nil
start_ui = true
end
return ok, start_ui
end
-- close the connection to the server
function public.close()
sv_watchdog.cancel()
2023-06-06 23:41:09 +00:00
self.sv_addr = comms.BROADCAST
self.sv_linked = false
2023-06-06 23:41:09 +00:00
self.sv_r_seq_num = nil
2023-07-10 03:31:56 +00:00
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
2023-08-30 20:45:48 +00:00
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
end
-- send a facility command
2023-02-21 16:05:57 +00:00
---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode)
function public.send_fac_command(cmd, option)
2023-08-30 20:45:48 +00:00
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { cmd, option })
end
-- send the auto process control configuration with a start command
2024-02-18 20:31:45 +00:00
---@param auto_cfg coord_auto_config configuration
function public.send_auto_start(auto_cfg)
2023-08-30 20:45:48 +00:00
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
2024-02-18 20:31:45 +00:00
FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits
})
end
-- send a unit command
2023-02-21 16:05:57 +00:00
---@param cmd UNIT_COMMAND command
---@param unit integer unit ID
---@param option any? optional option options for the optional options (like burn rate)
function public.send_unit_command(cmd, unit, option)
2023-08-30 20:45:48 +00:00
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
end
2022-06-15 19:35:34 +00:00
-- parse a packet
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any
---@param distance integer
2023-08-30 20:45:48 +00:00
---@return mgmt_frame|crdn_frame|nil packet
2022-06-15 19:35:34 +00:00
function public.parse_packet(side, sender, reply_to, message, distance)
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
2022-06-15 19:35:34 +00:00
local pkt = nil
if s_pkt then
2022-06-15 19:35:34 +00:00
-- get as SCADA management packet
2023-02-21 16:05:57 +00:00
if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
2022-06-15 19:35:34 +00:00
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get()
end
-- get as coordinator packet
2023-02-21 16:05:57 +00:00
elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
local crdn_pkt = comms.crdn_packet()
if crdn_pkt.decode(s_pkt) then
pkt = crdn_pkt.get()
2022-06-15 19:35:34 +00:00
end
else
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
end
end
return pkt
end
-- handle a packet
2023-08-30 20:45:48 +00:00
---@param packet mgmt_frame|crdn_frame|nil
---@return boolean close_ui
2022-06-15 19:35:34 +00:00
function public.handle_packet(packet)
local was_linked = self.sv_linked
2022-06-15 19:35:34 +00:00
if packet ~= nil then
2023-06-06 23:41:09 +00:00
local l_chan = packet.scada_frame.local_channel()
local r_chan = packet.scada_frame.remote_channel()
local src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol()
2022-06-15 19:35:34 +00:00
if l_chan ~= config.CRD_Channel then
2023-06-06 23:41:09 +00:00
log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == config.PKT_Channel then
if not self.sv_linked then
log.debug("discarding pocket API packet before linked to supervisor")
2023-08-30 20:45:48 +00:00
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- look for an associated session
2023-06-06 23:41:09 +00:00
local session = apisessions.find_session(src_addr)
2023-08-30 20:45:48 +00:00
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
2023-08-30 20:45:48 +00:00
log.debug("discarding SCADA_CRDN packet without a known session")
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- look for an associated session
2023-06-06 23:41:09 +00:00
local session = apisessions.find_session(src_addr)
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
2023-08-30 20:45:48 +00:00
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
-- validate packet and continue
if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
2023-06-06 23:41:09 +00:00
if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping API establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
2023-06-06 23:41:09 +00:00
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then
-- pocket linking request
2023-06-06 23:41:09 +00:00
local id = apisessions.establish_session(src_addr, firmware_v)
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
2023-06-06 23:41:09 +00:00
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
2023-06-06 23:41:09 +00:00
log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel"))
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug("invalid establish packet (on API listening channel)")
2023-06-06 23:41:09 +00:00
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
2023-06-06 23:41:09 +00:00
log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr))
end
else
2023-06-06 23:41:09 +00:00
log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
end
elseif r_chan == config.SVR_Channel then
-- check sequence number
if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num()
elseif self.sv_linked and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return false
2023-06-06 23:41:09 +00:00
elseif self.sv_linked and src_addr ~= self.sv_addr then
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
return false
2022-06-15 19:35:34 +00:00
else
self.sv_r_seq_num = packet.scada_frame.seq_num()
2022-06-15 19:35:34 +00:00
end
-- feed watchdog on valid sequence number
sv_watchdog.feed()
-- handle packet
2023-02-21 16:05:57 +00:00
if protocol == PROTOCOL.SCADA_CRDN then
2023-02-23 04:09:47 +00:00
---@cast packet crdn_frame
if self.sv_linked then
2023-08-30 20:45:48 +00:00
if packet.type == CRDN_TYPE.INITIAL_BUILDS then
if packet.length == 2 then
-- record builds
local fac_builds = iocontrol.record_facility_builds(packet.data[1])
local unit_builds = iocontrol.record_unit_builds(packet.data[2])
if fac_builds and unit_builds then
-- acknowledge receipt of builds
2023-08-30 20:45:48 +00:00
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.INITIAL_BUILDS, {})
else
2023-02-23 04:09:47 +00:00
log.debug("received invalid INITIAL_BUILDS packet")
end
else
log.debug("INITIAL_BUILDS packet length mismatch")
end
2023-08-30 20:45:48 +00:00
elseif packet.type == CRDN_TYPE.FAC_BUILDS then
2022-12-10 20:44:11 +00:00
if packet.length == 1 then
-- record facility builds
if iocontrol.record_facility_builds(packet.data[1]) then
-- acknowledge receipt of builds
2023-08-30 20:45:48 +00:00
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_BUILDS, {})
2022-12-10 20:44:11 +00:00
else
2023-02-23 04:09:47 +00:00
log.debug("received invalid FAC_BUILDS packet")
2022-12-10 20:44:11 +00:00
end
else
2022-12-10 20:44:11 +00:00
log.debug("FAC_BUILDS packet length mismatch")
end
2023-08-30 20:45:48 +00:00
elseif packet.type == CRDN_TYPE.FAC_STATUS then
-- update facility status
if not iocontrol.update_facility_status(packet.data) then
2023-02-23 04:09:47 +00:00
log.debug("received invalid FAC_STATUS packet")
end
2023-08-30 20:45:48 +00:00
elseif packet.type == CRDN_TYPE.FAC_CMD then
-- facility command acknowledgement
if packet.length >= 2 then
local cmd = packet.data[1]
local ack = packet.data[2] == true
2023-02-21 16:05:57 +00:00
if cmd == FAC_COMMAND.SCRAM_ALL then
iocontrol.get_db().facility.scram_ack(ack)
2023-02-21 16:05:57 +00:00
elseif cmd == FAC_COMMAND.STOP then
iocontrol.get_db().facility.stop_ack(ack)
2023-02-21 16:05:57 +00:00
elseif cmd == FAC_COMMAND.START then
if packet.length == 7 then
process.start_ack_handle({ table.unpack(packet.data, 2) })
else
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
end
2023-02-21 16:05:57 +00:00
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
2023-02-07 23:44:34 +00:00
iocontrol.get_db().facility.ack_alarms_ack(ack)
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
process.waste_ack_handle(packet.data[2])
elseif cmd == FAC_COMMAND.SET_PU_FB then
process.pu_fb_ack_handle(packet.data[2])
else
log.debug(util.c("received facility command ack with unknown command ", cmd))
end
else
log.debug("SCADA_CRDN facility command ack packet length mismatch")
end
2023-08-30 20:45:48 +00:00
elseif packet.type == CRDN_TYPE.UNIT_BUILDS then
-- record builds
2023-02-20 05:49:37 +00:00
if packet.length == 1 then
if iocontrol.record_unit_builds(packet.data[1]) then
-- acknowledge receipt of builds
2023-08-30 20:45:48 +00:00
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_BUILDS, {})
2023-02-20 05:49:37 +00:00
else
2023-02-23 04:09:47 +00:00
log.debug("received invalid UNIT_BUILDS packet")
2023-02-20 05:49:37 +00:00
end
2022-07-06 03:48:01 +00:00
else
2023-02-20 05:49:37 +00:00
log.debug("UNIT_BUILDS packet length mismatch")
2022-07-06 03:48:01 +00:00
end
2023-08-30 20:45:48 +00:00
elseif packet.type == CRDN_TYPE.UNIT_STATUSES then
-- update statuses
if not iocontrol.update_unit_statuses(packet.data) then
2023-04-19 13:30:17 +00:00
log.debug("received invalid UNIT_STATUSES packet")
end
2023-08-30 20:45:48 +00:00
elseif packet.type == CRDN_TYPE.UNIT_CMD then
-- unit command acknowledgement
if packet.length == 3 then
local cmd = packet.data[1]
local unit_id = packet.data[2]
local ack = packet.data[3] == true
local unit = iocontrol.get_db().units[unit_id] ---@type ioctl_unit
if unit ~= nil then
2023-02-21 16:05:57 +00:00
if cmd == UNIT_COMMAND.SCRAM then
unit.scram_ack(ack)
2023-02-21 16:05:57 +00:00
elseif cmd == UNIT_COMMAND.START then
unit.start_ack(ack)
2023-02-21 16:05:57 +00:00
elseif cmd == UNIT_COMMAND.RESET_RPS then
unit.reset_rps_ack(ack)
2023-02-21 16:05:57 +00:00
elseif cmd == UNIT_COMMAND.SET_BURN then
unit.set_burn_ack(ack)
2023-02-21 16:05:57 +00:00
elseif cmd == UNIT_COMMAND.SET_WASTE then
unit.set_waste_ack(ack)
2023-02-21 16:05:57 +00:00
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_alarms_ack(ack)
2023-02-21 16:05:57 +00:00
elseif cmd == UNIT_COMMAND.SET_GROUP then
2023-02-23 04:09:47 +00:00
-- UI will be updated to display current group if changed successfully
else
log.debug(util.c("received unit command ack with unknown command ", cmd))
end
else
log.debug(util.c("received unit command ack with unknown unit ", unit_id))
end
else
log.debug("SCADA_CRDN unit command ack packet length mismatch")
end
else
2023-04-19 13:30:17 +00:00
log.debug("received unknown SCADA_CRDN packet type " .. packet.type)
end
else
log.debug("discarding SCADA_CRDN packet before linked")
end
2023-02-21 16:05:57 +00:00
elseif protocol == PROTOCOL.SCADA_MGMT then
2023-02-23 04:09:47 +00:00
---@cast packet mgmt_frame
if self.sv_linked then
2023-08-30 20:45:48 +00:00
if packet.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
local timestamp = packet.data[1]
local trip_time = util.time() - timestamp
if trip_time > 750 then
log.warning("coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end
-- log.debug("coordinator RTT = " .. trip_time .. "ms")
iocontrol.get_db().facility.ps.publish("sv_ping", trip_time)
_send_keep_alive_ack(timestamp)
else
log.debug("SCADA keep alive packet length mismatch")
end
2023-08-30 20:45:48 +00:00
elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close
sv_watchdog.cancel()
self.sv_addr = comms.BROADCAST
self.sv_linked = false
self.sv_r_seq_num = nil
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
log.info("server connection closed by remote host")
else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
end
2023-08-30 20:45:48 +00:00
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with supervisor established
if packet.length == 2 then
local est_ack = packet.data[1]
local sv_config = packet.data[2]
if est_ack == ESTABLISH_ACK.ALLOW then
2023-07-10 03:31:56 +00:00
-- reset to disconnected before validating
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
if type(sv_config) == "table" and #sv_config == 2 then
-- get configuration
---@class facility_conf
local conf = {
num_units = sv_config[1], ---@type integer
cooling = sv_config[2] ---@type sv_cooling_conf
}
if conf.num_units == config.UnitCount then
-- init io controller
iocontrol.init(conf, public, config.TempScale)
2023-06-06 23:41:09 +00:00
self.sv_addr = src_addr
self.sv_linked = true
self.sv_r_seq_num = nil
self.sv_config_err = false
2023-07-10 03:31:56 +00:00
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)
else
self.sv_config_err = true
log.warning("supervisor config's number of units don't match coordinator's config, establish failed")
end
else
2023-02-23 04:09:47 +00:00
log.debug("invalid supervisor configuration table received, establish failed")
end
else
log.debug("SCADA_MGMT establish packet reply (len = 2) unsupported")
end
self.last_est_ack = est_ack
elseif packet.length == 1 then
local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.DENY then
if self.last_est_ack ~= est_ack then
2023-07-10 03:31:56 +00:00
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DENIED)
2023-02-23 04:09:47 +00:00
log.info("supervisor connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.last_est_ack ~= est_ack then
2023-07-10 03:31:56 +00:00
iocontrol.fp_link_state(types.PANEL_LINK_STATE.COLLISION)
2023-04-19 13:30:17 +00:00
log.warning("supervisor connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.last_est_ack ~= est_ack then
2023-07-10 03:31:56 +00:00
iocontrol.fp_link_state(types.PANEL_LINK_STATE.BAD_VERSION)
2023-04-19 13:30:17 +00:00
log.warning("supervisor comms version mismatch")
end
else
log.debug("SCADA_MGMT establish packet reply (len = 1) unsupported")
end
self.last_est_ack = est_ack
else
log.debug("SCADA_MGMT establish packet length mismatch")
end
else
log.debug("discarding non-link SCADA_MGMT packet before linked")
end
2022-06-15 19:35:34 +00:00
else
log.debug("illegal packet type " .. protocol .. " on supervisor listening channel", true)
2022-06-15 19:35:34 +00:00
end
else
2023-06-06 23:41:09 +00:00
log.debug("received packet for unknown channel " .. r_chan, true)
2022-06-15 19:35:34 +00:00
end
end
return was_linked and not self.sv_linked
2022-06-15 19:35:34 +00:00
end
-- check if the coordinator is still linked to the supervisor
2023-02-23 04:09:47 +00:00
---@nodiscard
function public.is_linked() return self.sv_linked end
2022-06-15 19:35:34 +00:00
return public
end
2022-05-10 21:09:02 +00:00
return coordinator