cc-mek-scada/coordinator/coordinator.lua

743 lines
32 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")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local apisessions = require("coordinator.session.apisessions")
local dialog = require("coordinator.ui.dialog")
2022-06-15 19:35:34 +00:00
local print = util.print
local println = util.println
local println_ts = util.println_ts
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-02-21 16:05:57 +00:00
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND
2022-05-10 21:09:02 +00:00
local coordinator = {}
2022-06-15 19:35:34 +00:00
-- request the user to select a monitor
2023-02-23 04:09:47 +00:00
---@nodiscard
2022-06-15 19:35:34 +00:00
---@param names table available monitors
---@return boolean|string|nil
local function ask_monitor(names)
println("available monitors:")
for i = 1, #names do
print(" " .. names[i])
end
println("")
println("select a monitor or type c to cancel")
local iface = dialog.ask_options(names, "c")
if iface ~= false and iface ~= nil then
util.filter_table(names, function (x) return x ~= iface end)
end
return iface
end
2022-06-15 19:35:34 +00:00
-- configure monitor layout
---@param num_units integer number of units expected
---@return boolean success, monitors_struct? monitors
function coordinator.configure_monitors(num_units)
---@class monitors_struct
local monitors = {
primary = nil,
primary_name = "",
unit_displays = {},
unit_name_map = {}
}
local monitors_avail = ppm.get_monitor_list()
local names = {}
local available = {}
-- get all interface names
for iface, _ in pairs(monitors_avail) do
table.insert(names, iface)
table.insert(available, iface)
end
-- we need a certain number of monitors (1 per unit + 1 primary display)
2023-02-23 04:09:47 +00:00
local num_displays_needed = num_units + 1
if #names < num_displays_needed then
local message = "not enough monitors connected (need " .. num_displays_needed .. ")"
println(message)
log.warning(message)
return false
end
-- attempt to load settings
if not settings.load("/coord.settings") then
log.warning("configure_monitors(): failed to load coordinator settings file (may not exist yet)")
else
local _primary = settings.get("PRIMARY_DISPLAY")
local _unitd = settings.get("UNIT_DISPLAYS")
-- filter out already assigned monitors
util.filter_table(available, function (x) return x ~= _primary end)
if type(_unitd) == "table" then
util.filter_table(available, function (x) return not util.table_contains(_unitd, x) end)
end
end
---------------------
-- PRIMARY DISPLAY --
---------------------
local iface_primary_display = settings.get("PRIMARY_DISPLAY") ---@type boolean|string|nil
if not util.table_contains(names, iface_primary_display) then
println("primary display is not connected")
local response = dialog.ask_y_n("would you like to change it", true)
if response == false then return false end
iface_primary_display = nil
end
while iface_primary_display == nil and #available > 0 do
-- lets get a monitor
iface_primary_display = ask_monitor(available)
end
if type(iface_primary_display) ~= "string" then return false end
settings.set("PRIMARY_DISPLAY", iface_primary_display)
util.filter_table(available, function (x) return x ~= iface_primary_display end)
monitors.primary = ppm.get_periph(iface_primary_display)
monitors.primary_name = iface_primary_display
-------------------
-- UNIT DISPLAYS --
-------------------
local unit_displays = settings.get("UNIT_DISPLAYS")
if unit_displays == nil then
unit_displays = {}
for i = 1, num_units do
local display = nil
while display == nil and #available > 0 do
-- lets get a monitor
println("please select monitor for unit #" .. i)
display = ask_monitor(available)
end
if display == false then return false end
unit_displays[i] = display
end
else
-- make sure all displays are connected
for i = 1, num_units do
local display = unit_displays[i]
if not util.table_contains(names, display) then
println("unit #" .. i .. " display is not connected")
local response = dialog.ask_y_n("would you like to change it", true)
if response == false then return false end
display = nil
end
while display == nil and #available > 0 do
-- lets get a monitor
display = ask_monitor(available)
end
if display == false then return false end
unit_displays[i] = display
end
end
settings.set("UNIT_DISPLAYS", unit_displays)
if not settings.save("/coord.settings") then
log.warning("configure_monitors(): failed to save coordinator settings file")
end
for i = 1, #unit_displays do
monitors.unit_displays[i] = ppm.get_periph(unit_displays[i])
monitors.unit_name_map[i] = unit_displays[i]
end
return true, 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 = {
GRAPHICS = colors.green,
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
function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") end
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
2023-06-06 23:41:09 +00:00
---@param crd_channel integer port of configured supervisor
---@param svr_channel integer listening port for supervisor replys
---@param pkt_channel integer listening port for pocket API
---@param range integer trusted device connection range
---@param sv_watchdog watchdog
2023-06-21 23:04:39 +00:00
function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, range, 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,
connected = false,
last_est_ack = ESTABLISH_ACK.ALLOW,
last_api_est_acks = {}
}
2022-06-15 19:35:34 +00:00
comms.set_trusted_range(range)
2022-06-15 19:35:34 +00:00
-- PRIVATE FUNCTIONS --
2023-06-21 23:04:39 +00:00
-- configure network channels
nic.closeAll()
nic.open(crd_channel)
2022-06-15 19:35:34 +00:00
2023-06-21 23:04:39 +00:00
-- link nic to apisessions
apisessions.init(nic)
-- send a packet to the supervisor
2023-02-21 16:05:57 +00:00
---@param msg_type SCADA_MGMT_TYPE|SCADA_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
2023-06-21 23:04:39 +00:00
nic.transmit(svr_channel, 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-06-06 23:41:09 +00:00
m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack })
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
2023-06-21 23:04:39 +00:00
nic.transmit(pkt_channel, 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-02-21 16:05:57 +00:00
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN })
end
-- keep alive ack
---@param srv_time integer
local function _send_keep_alive_ack(srv_time)
2023-02-21 16:05:57 +00:00
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_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 = {}
-- 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-02-21 16:05:57 +00:00
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
end
2022-07-06 03:48:01 +00:00
-- attempt to connect to the subervisor
2023-02-23 04:09:47 +00:00
---@nodiscard
2022-07-06 03:48:01 +00:00
---@param timeout_s number timeout in seconds
---@param tick_dmesg_waiting function callback to tick dmesg waiting
---@param task_done function callback to show done on dmesg
---@return boolean sv_linked true if connected, false otherwise
--- EVENT_CONSUMER: this function consumes events
function public.sv_connect(timeout_s, tick_dmesg_waiting, task_done)
local clock = util.new_clock(1)
local start = util.time_s()
local terminated = false
_send_establish()
2022-07-06 03:48:01 +00:00
clock.start()
while (util.time_s() - start) < timeout_s and (not self.sv_linked) and (not self.sv_config_err) do
local event, p1, p2, p3, p4, p5 = util.pull_event()
2022-07-06 03:48:01 +00:00
if event == "timer" and clock.is_clock(p1) then
-- timed out attempt, try again
tick_dmesg_waiting(math.max(0, timeout_s - (util.time_s() - start)))
_send_establish()
2022-07-06 03:48:01 +00:00
clock.start()
elseif event == "timer" then
-- keep checking watchdog timers
apisessions.check_all_watchdogs(p1)
2022-07-06 03:48:01 +00:00
elseif event == "modem_message" then
-- handle message
local packet = public.parse_packet(p1, p2, p3, p4, p5)
public.handle_packet(packet)
2022-07-06 03:48:01 +00:00
elseif event == "terminate" then
terminated = true
break
end
end
2022-07-06 03:49:48 +00:00
task_done(self.sv_linked)
if terminated then
coordinator.log_comms("supervisor connection attempt cancelled by user")
elseif self.sv_config_err then
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
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
2022-07-06 03:48:01 +00:00
end
return self.sv_linked
end
-- send a facility command
2023-02-21 16:05:57 +00:00
---@param cmd FAC_COMMAND command
function public.send_fac_command(cmd)
2023-02-21 16:05:57 +00:00
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd })
end
-- send the auto process control configuration with a start command
---@param config coord_auto_config configuration
function public.send_auto_start(config)
2023-02-21 16:05:57 +00:00
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, {
FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.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) (does option still look like a word?)
function public.send_unit_command(cmd, unit, option)
2023-02-21 16:05:57 +00:00
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_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
---@return mgmt_frame|crdn_frame|capi_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
-- get as coordinator API packet
2023-02-21 16:05:57 +00:00
elseif s_pkt.protocol() == PROTOCOL.COORD_API then
2022-06-15 19:35:34 +00:00
local capi_pkt = comms.capi_packet()
if capi_pkt.decode(s_pkt) then
pkt = capi_pkt.get()
end
else
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
end
end
return pkt
end
-- handle a packet
---@param packet mgmt_frame|crdn_frame|capi_frame|nil
2022-06-15 19:35:34 +00:00
function public.handle_packet(packet)
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
2023-06-06 23:41:09 +00:00
if l_chan ~= crd_channel then
log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == pkt_channel then
2023-02-21 16:05:57 +00:00
if protocol == PROTOCOL.COORD_API then
2023-02-23 04:09:47 +00:00
---@cast packet capi_frame
-- look for an associated session
2023-06-06 23:41:09 +00:00
local session = apisessions.find_session(src_addr)
-- API 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
log.debug("discarding COORD_API 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)
elseif packet.type == SCADA_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)
println(util.c("[API] pocket (", firmware_v, ") [@", src_addr, "] \xbb connected"))
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
2023-06-06 23:41:09 +00:00
elseif r_chan == svr_channel then
-- check sequence number
if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num()
2023-04-21 21:10:15 +00:00
elseif self.connected 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
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
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-02-21 16:05:57 +00:00
if packet.type == SCADA_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-02-21 16:05:57 +00:00
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_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-02-21 16:05:57 +00:00
elseif packet.type == SCADA_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-02-21 16:05:57 +00:00
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_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-02-21 16:05:57 +00:00
elseif packet.type == SCADA_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-02-21 16:05:57 +00:00
elseif packet.type == SCADA_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)
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-02-21 16:05:57 +00:00
elseif packet.type == SCADA_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-02-21 16:05:57 +00:00
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_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-02-21 16:05:57 +00:00
elseif packet.type == SCADA_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-02-21 16:05:57 +00:00
elseif packet.type == SCADA_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
2023-02-21 16:05:57 +00:00
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- connection with supervisor established
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 > 1 then
-- get configuration
---@class facility_conf
local conf = {
num_units = config[1], ---@type integer
defs = {} -- boilers and turbines
}
if (#config - 1) == (conf.num_units * 2) then
-- record sequence of pairs of [#boilers, #turbines] per unit
for i = 2, #config do
table.insert(conf.defs, config[i])
end
-- init io controller
iocontrol.init(conf, public)
2023-06-06 23:41:09 +00:00
self.sv_addr = src_addr
self.sv_linked = true
self.sv_config_err = false
else
self.sv_config_err = true
log.warning("invalid supervisor configuration definitions received, 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-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-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-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
elseif self.sv_linked then
2023-02-21 16:05:57 +00:00
if packet.type == SCADA_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
2023-02-26 19:49:16 +00:00
if trip_time > 750 then
2023-06-06 23:41:09 +00:00
log.warning("coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
end
2023-06-06 23:41:09 +00:00
-- 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-02-21 16:05:57 +00:00
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- handle session 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
println_ts("server connection closed by remote host")
2023-02-23 04:09:47 +00:00
log.info("server connection closed by remote host")
else
2023-02-23 04:09:47 +00:00
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
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
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