mirror of
https://github.com/MikaylaFischler/cc-mek-scada.git
synced 2024-08-30 18:22:34 +00:00
459 lines
16 KiB
Lua
459 lines
16 KiB
Lua
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 apisessions = require("coordinator.apisessions")
|
|
local database = require("coordinator.database")
|
|
|
|
local dialog = require("coordinator.ui.dialog")
|
|
|
|
local coordinator = {}
|
|
|
|
local print = util.print
|
|
local println = util.println
|
|
local print_ts = util.print_ts
|
|
local println_ts = util.println_ts
|
|
|
|
local PROTOCOLS = comms.PROTOCOLS
|
|
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
|
|
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
|
|
|
|
-- request the user to select a monitor
|
|
---@param names table available monitors
|
|
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
|
|
|
|
-- configure monitor layout
|
|
---@param num_units integer number of units expected
|
|
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 = {}
|
|
|
|
-- get all interface names
|
|
for iface, _ in pairs(monitors_avail) do
|
|
table.insert(names, iface)
|
|
end
|
|
|
|
-- we need a certain number of monitors (1 per unit + 1 primary display)
|
|
if #names ~= num_units + 1 then
|
|
println("not enough monitors connected (need " .. num_units + 1 .. ")")
|
|
log.warning("insufficient monitors present (need " .. num_units + 1 .. ")")
|
|
return false
|
|
end
|
|
|
|
-- attempt to load settings
|
|
settings.load("/coord.settings")
|
|
|
|
---------------------
|
|
-- PRIMARY DISPLAY --
|
|
---------------------
|
|
|
|
local iface_primary_display = settings.get("PRIMARY_DISPLAY")
|
|
|
|
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 #names > 0 do
|
|
-- lets get a monitor
|
|
iface_primary_display = ask_monitor(names)
|
|
end
|
|
|
|
if iface_primary_display == false then return false end
|
|
|
|
settings.set("PRIMARY_DISPLAY", iface_primary_display)
|
|
util.filter_table(names, 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 #names > 0 do
|
|
-- lets get a monitor
|
|
println("please select monitor for unit " .. i)
|
|
display = ask_monitor(names)
|
|
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
|
|
---@diagnostic disable-next-line: need-check-nil
|
|
local display = unit_displays[i]
|
|
|
|
if not util.table_contains(names, display) then
|
|
local response = dialog.ask_y_n("unit display " .. i .. " is not connected, would you like to change it?", true)
|
|
if response == false then return false end
|
|
display = nil
|
|
end
|
|
|
|
while display == nil and #names > 0 do
|
|
-- lets get a monitor
|
|
display = ask_monitor(names)
|
|
end
|
|
|
|
if display == false then return false end
|
|
|
|
unit_displays[i] = display
|
|
end
|
|
end
|
|
|
|
settings.set("UNIT_DISPLAYS", unit_displays)
|
|
settings.save("/coord.settings")
|
|
|
|
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
|
|
|
|
-- dmesg print wrapper
|
|
---@param message string message
|
|
---@param dmesg_tag string tag
|
|
---@param working? boolean to use dmesg_working
|
|
---@return function? update, function? done
|
|
local function log_dmesg(message, dmesg_tag, working)
|
|
local colors = {
|
|
GRAPHICS = colors.green,
|
|
SYSTEM = colors.cyan,
|
|
BOOT = colors.blue,
|
|
COMMS = colors.purple
|
|
}
|
|
|
|
if working then
|
|
return log.dmesg_working(message, dmesg_tag, colors[dmesg_tag])
|
|
else
|
|
log.dmesg(message, dmesg_tag, colors[dmesg_tag])
|
|
end
|
|
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
|
|
|
|
---@param message string
|
|
---@return function update, function done
|
|
function coordinator.log_comms_connecting(message) return log_dmesg(message, "COMMS", true) end
|
|
|
|
-- coordinator communications
|
|
---@param version string
|
|
---@param modem table
|
|
---@param sv_port integer
|
|
---@param sv_listen integer
|
|
---@param api_listen integer
|
|
---@param sv_watchdog watchdog
|
|
function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_watchdog)
|
|
local self = {
|
|
sv_linked = false,
|
|
sv_seq_num = 0,
|
|
sv_r_seq_num = nil,
|
|
modem = modem,
|
|
connected = false
|
|
}
|
|
|
|
---@class coord_comms
|
|
local public = {}
|
|
|
|
-- PRIVATE FUNCTIONS --
|
|
|
|
-- open all channels
|
|
local function _open_channels()
|
|
if not self.modem.isOpen(sv_listen) then
|
|
self.modem.open(sv_listen)
|
|
end
|
|
|
|
if not self.modem.isOpen(api_listen) then
|
|
self.modem.open(api_listen)
|
|
end
|
|
end
|
|
|
|
-- open at construct time
|
|
_open_channels()
|
|
|
|
-- send a packet to the supervisor
|
|
---@param msg_type SCADA_MGMT_TYPES|SCADA_CRDN_TYPES
|
|
---@param msg table
|
|
local function _send_sv(protocol, msg_type, msg)
|
|
local s_pkt = comms.scada_packet()
|
|
local pkt = nil ---@type mgmt_packet|crdn_packet
|
|
|
|
if protocol == PROTOCOLS.SCADA_MGMT then
|
|
pkt = comms.mgmt_packet()
|
|
elseif protocol == PROTOCOLS.SCADA_CRDN then
|
|
pkt = comms.crdn_packet()
|
|
else
|
|
return
|
|
end
|
|
|
|
pkt.make(msg_type, msg)
|
|
s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable())
|
|
|
|
self.modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable())
|
|
self.sv_seq_num = self.sv_seq_num + 1
|
|
end
|
|
|
|
-- attempt connection establishment
|
|
local function _send_establish()
|
|
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.ESTABLISH, { version })
|
|
end
|
|
|
|
-- keep alive ack
|
|
---@param srv_time integer
|
|
local function _send_keep_alive_ack(srv_time)
|
|
_send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
|
|
end
|
|
|
|
-- PUBLIC FUNCTIONS --
|
|
|
|
-- reconnect a newly connected modem
|
|
---@param modem table
|
|
---@diagnostic disable-next-line: redefined-local
|
|
function public.reconnect_modem(modem)
|
|
self.modem = modem
|
|
_open_channels()
|
|
end
|
|
|
|
-- close the connection to the server
|
|
function public.close()
|
|
sv_watchdog.cancel()
|
|
_send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.CLOSE, {})
|
|
end
|
|
|
|
-- attempt to connect to the subervisor
|
|
---@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()
|
|
|
|
clock.start()
|
|
|
|
while (util.time_s() - start) < timeout_s and not self.sv_linked do
|
|
local event, p1, p2, p3, p4, p5 = util.pull_event()
|
|
|
|
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()
|
|
clock.start()
|
|
elseif event == "modem_message" then
|
|
-- handle message
|
|
local packet = public.parse_packet(p1, p2, p3, p4, p5)
|
|
if packet ~= nil and packet.type == SCADA_CRDN_TYPES.ESTABLISH then
|
|
public.handle_packet(packet)
|
|
end
|
|
elseif event == "terminate" then
|
|
terminated = true
|
|
break
|
|
end
|
|
end
|
|
|
|
task_done(self.sv_linked)
|
|
|
|
if terminated then
|
|
coordinator.log_comms("supervisor connection attempt cancelled by user")
|
|
end
|
|
|
|
return self.sv_linked
|
|
end
|
|
|
|
-- 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
|
|
function public.parse_packet(side, sender, reply_to, message, distance)
|
|
local pkt = nil
|
|
local s_pkt = comms.scada_packet()
|
|
|
|
-- parse packet as generic SCADA packet
|
|
s_pkt.receive(side, sender, reply_to, message, distance)
|
|
|
|
if s_pkt.is_valid() then
|
|
-- get as SCADA management packet
|
|
if s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
|
|
local mgmt_pkt = comms.mgmt_packet()
|
|
if mgmt_pkt.decode(s_pkt) then
|
|
pkt = mgmt_pkt.get()
|
|
end
|
|
-- get as coordinator packet
|
|
elseif s_pkt.protocol() == PROTOCOLS.SCADA_CRDN then
|
|
local crdn_pkt = comms.crdn_packet()
|
|
if crdn_pkt.decode(s_pkt) then
|
|
pkt = crdn_pkt.get()
|
|
end
|
|
-- get as coordinator API packet
|
|
elseif s_pkt.protocol() == PROTOCOLS.COORD_API then
|
|
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
|
|
function public.handle_packet(packet)
|
|
if packet ~= nil then
|
|
local protocol = packet.scada_frame.protocol()
|
|
|
|
if protocol == PROTOCOLS.COORD_API then
|
|
apisessions.handle_packet(packet)
|
|
else
|
|
-- check sequence number
|
|
if self.sv_r_seq_num == nil then
|
|
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
|
elseif self.connected and self.sv_r_seq_num >= 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
|
|
else
|
|
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
|
end
|
|
|
|
-- feed watchdog on valid sequence number
|
|
sv_watchdog.feed()
|
|
|
|
-- handle packet
|
|
if protocol == PROTOCOLS.SCADA_CRDN then
|
|
if packet.type == SCADA_CRDN_TYPES.ESTABLISH then
|
|
-- connection with supervisor established
|
|
if packet.length > 1 then
|
|
-- get configuration
|
|
|
|
---@class facility_conf
|
|
local conf = {
|
|
num_units = packet.data[1],
|
|
defs = {} -- boilers and turbines
|
|
}
|
|
|
|
if (packet.length - 1) == (conf.num_units * 2) then
|
|
-- record sequence of pairs of [#boilers, #turbines] per unit
|
|
for i = 2, packet.length do
|
|
table.insert(conf.defs, packet.data[i])
|
|
end
|
|
|
|
-- init database structure
|
|
database.init(conf)
|
|
|
|
self.sv_linked = true
|
|
else
|
|
log.debug("supervisor conn establish packet length mismatch")
|
|
end
|
|
else
|
|
log.debug("supervisor conn establish packet length mismatch")
|
|
end
|
|
elseif packet.type == SCADA_CRDN_TYPES.STRUCT_BUILDS then
|
|
-- record builds
|
|
if database.populate_builds(packet.data) then
|
|
-- acknowledge receipt of builds
|
|
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.STRUCT_BUILDS, {})
|
|
else
|
|
log.error("received invalid build packet")
|
|
end
|
|
elseif packet.type == SCADA_CRDN_TYPES.UNIT_STATUSES then
|
|
-- update statuses
|
|
if not database.update_statuses(packet.data) then
|
|
log.error("received invalid unit statuses packet")
|
|
end
|
|
elseif packet.type == SCADA_CRDN_TYPES.COMMAND_UNIT then
|
|
elseif packet.type == SCADA_CRDN_TYPES.ALARM then
|
|
else
|
|
log.warning("received unknown SCADA_CRDN packet type " .. packet.type)
|
|
end
|
|
elseif protocol == PROTOCOLS.SCADA_MGMT then
|
|
if packet.type == SCADA_MGMT_TYPES.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 > 500 then
|
|
log.warning("coord KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)")
|
|
end
|
|
|
|
-- log.debug("coord RTT = " .. trip_time .. "ms")
|
|
|
|
_send_keep_alive_ack(timestamp)
|
|
else
|
|
log.debug("SCADA keep alive packet length mismatch")
|
|
end
|
|
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
|
|
-- handle session close
|
|
sv_watchdog.cancel()
|
|
self.sv_linked = false
|
|
println_ts("server connection closed by remote host")
|
|
log.warning("server connection closed by remote host")
|
|
else
|
|
log.warning("received unknown SCADA_MGMT packet type " .. packet.type)
|
|
end
|
|
else
|
|
-- should be unreachable assuming packet is from parse_packet()
|
|
log.error("illegal packet type " .. protocol, true)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- check if the coordinator is still linked to the supervisor
|
|
function public.is_linked() return self.sv_linked end
|
|
|
|
return public
|
|
end
|
|
|
|
return coordinator
|