cc-mek-scada/scada-common/comms.lua

610 lines
16 KiB
Lua
Raw Normal View History

--
-- Communications
--
2022-05-10 15:35:52 +00:00
local log = require("scada-common.log")
2022-05-09 13:34:26 +00:00
local types = require("scada-common.types")
2022-05-10 21:06:27 +00:00
---@class comms
local comms = {}
2022-05-09 13:34:26 +00:00
local rtu_t = types.rtu_t
2022-05-07 17:39:12 +00:00
local insert = table.insert
2022-05-10 21:06:27 +00:00
---@alias PROTOCOLS integer
local PROTOCOLS = {
2022-03-15 16:02:31 +00:00
MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol
RPLC = 1, -- reactor PLC protocol
SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc
2022-04-22 19:43:25 +00:00
COORD_DATA = 3, -- data/control packets for coordinators to/from supervisory controllers
COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
}
2022-05-10 21:06:27 +00:00
---@alias RPLC_TYPES integer
local RPLC_TYPES = {
LINK_REQ = 0, -- linking requests
STATUS = 1, -- reactor/system status
MEK_STRUCT = 2, -- mekanism build structure
MEK_BURN_RATE = 3, -- set burn rate
RPS_ENABLE = 4, -- enable reactor
RPS_SCRAM = 5, -- SCRAM reactor
RPS_STATUS = 6, -- RPS status
RPS_ALARM = 7, -- RPS alarm broadcast
RPS_RESET = 8 -- clear RPS trip (if in bad state, will trip immediately)
2022-01-22 19:26:25 +00:00
}
2022-05-10 21:06:27 +00:00
---@alias RPLC_LINKING integer
local RPLC_LINKING = {
2022-03-15 16:02:31 +00:00
ALLOW = 0, -- link approved
DENY = 1, -- link denied
COLLISION = 2 -- link denied due to existing active link
}
2022-05-10 21:06:27 +00:00
---@alias SCADA_MGMT_TYPES integer
local SCADA_MGMT_TYPES = {
KEEP_ALIVE = 0, -- keep alive packet w/ RTT
2022-05-02 15:42:24 +00:00
CLOSE = 1, -- close a connection
RTU_ADVERT = 2, -- RTU capability advertisement
REMOTE_LINKED = 3 -- remote device linked
2022-03-15 16:02:31 +00:00
}
---@alias RTU_UNIT_TYPES integer
local RTU_UNIT_TYPES = {
2022-05-09 13:34:26 +00:00
REDSTONE = 0, -- redstone I/O
BOILER = 1, -- boiler
BOILER_VALVE = 2, -- boiler mekanism 10.1+
TURBINE = 3, -- turbine
TURBINE_VALVE = 4, -- turbine, mekanism 10.1+
EMACHINE = 5, -- energy machine
IMATRIX = 6 -- induction matrix
2022-01-22 19:26:25 +00:00
}
comms.PROTOCOLS = PROTOCOLS
comms.RPLC_TYPES = RPLC_TYPES
comms.RPLC_LINKING = RPLC_LINKING
comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES
comms.RTU_UNIT_TYPES = RTU_UNIT_TYPES
-- generic SCADA packet object
comms.scada_packet = function ()
local self = {
modem_msg_in = nil,
2022-01-22 19:26:25 +00:00
valid = false,
2022-04-22 19:43:25 +00:00
raw = nil,
2022-03-14 18:19:14 +00:00
seq_num = nil,
protocol = nil,
2022-01-22 19:26:25 +00:00
length = nil,
2022-04-22 19:43:25 +00:00
payload = nil
}
2022-05-10 21:06:27 +00:00
---@class scada_packet
local public = {}
2022-04-22 19:43:25 +00:00
-- make a SCADA packet
2022-05-10 21:06:27 +00:00
---@param seq_num integer
---@param protocol PROTOCOLS
---@param payload table
public.make = function (seq_num, protocol, payload)
2022-01-22 19:26:25 +00:00
self.valid = true
2022-03-14 18:19:14 +00:00
self.seq_num = seq_num
2022-01-22 19:26:25 +00:00
self.protocol = protocol
self.length = #payload
2022-04-22 19:43:25 +00:00
self.payload = payload
self.raw = { self.seq_num, self.protocol, self.payload }
2022-01-22 19:26:25 +00:00
end
2022-04-22 19:43:25 +00:00
-- parse in a modem message as a SCADA packet
2022-05-10 21:06:27 +00:00
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any
---@param distance integer
public.receive = function (side, sender, reply_to, message, distance)
self.modem_msg_in = {
iface = side,
s_port = sender,
r_port = reply_to,
msg = message,
dist = distance
}
self.raw = self.modem_msg_in.msg
if type(self.raw) == "table" then
if #self.raw >= 3 then
self.valid = true
self.seq_num = self.raw[1]
self.protocol = self.raw[2]
self.length = #self.raw[3]
self.payload = self.raw[3]
end
end
2022-04-22 19:43:25 +00:00
return self.valid
end
2022-04-22 19:43:25 +00:00
-- public accessors --
2022-05-10 21:06:27 +00:00
public.modem_event = function () return self.modem_msg_in end
public.raw_sendable = function () return self.raw end
public.local_port = function () return self.modem_msg_in.s_port end
public.remote_port = function () return self.modem_msg_in.r_port end
public.is_valid = function () return self.valid end
public.seq_num = function () return self.seq_num end
public.protocol = function () return self.protocol end
public.length = function () return self.length end
public.data = function () return self.payload end
return public
end
2022-05-10 21:06:27 +00:00
-- MODBUS packet
2022-04-23 00:21:28 +00:00
-- modeled after MODBUS TCP packet
comms.modbus_packet = function ()
local self = {
frame = nil,
2022-04-22 19:43:25 +00:00
raw = nil,
2022-05-10 15:35:52 +00:00
txn_id = nil,
length = nil,
unit_id = nil,
func_code = nil,
data = nil
}
2022-05-10 21:06:27 +00:00
---@class modbus_packet
local public = {}
-- make a MODBUS packet
2022-05-10 21:06:27 +00:00
---@param txn_id integer
---@param unit_id integer
---@param func_code MODBUS_FCODE
---@param data table
public.make = function (txn_id, unit_id, func_code, data)
self.txn_id = txn_id
2022-04-22 19:43:25 +00:00
self.length = #data
self.unit_id = unit_id
self.func_code = func_code
self.data = data
2022-04-22 19:43:25 +00:00
-- populate raw array
2022-04-23 00:21:28 +00:00
self.raw = { self.txn_id, self.unit_id, self.func_code }
2022-04-22 19:43:25 +00:00
for i = 1, self.length do
2022-05-07 17:39:12 +00:00
insert(self.raw, data[i])
2022-04-22 19:43:25 +00:00
end
end
-- decode a MODBUS packet from a SCADA frame
2022-05-10 21:06:27 +00:00
---@param frame scada_packet
---@return boolean success
public.decode = function (frame)
if frame then
self.frame = frame
2022-04-22 19:43:25 +00:00
if frame.protocol() == PROTOCOLS.MODBUS_TCP then
2022-04-23 00:21:28 +00:00
local size_ok = frame.length() >= 3
2022-05-10 21:06:27 +00:00
2022-04-22 19:43:25 +00:00
if size_ok then
local data = frame.data()
2022-05-10 21:06:27 +00:00
public.make(data[1], data[2], data[3], { table.unpack(data, 4, #data) })
2022-04-22 19:43:25 +00:00
end
2022-05-10 21:06:27 +00:00
2022-04-22 19:43:25 +00:00
return size_ok
else
log.debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
2022-04-22 19:43:25 +00:00
return false
end
else
log.debug("nil frame encountered", true)
return false
end
end
2022-04-22 19:43:25 +00:00
-- get raw to send
2022-05-10 21:06:27 +00:00
public.raw_sendable = function () return self.raw end
2022-04-22 19:43:25 +00:00
2022-05-26 23:37:19 +00:00
-- get this packet as a frame with an immutable relation to this object
2022-05-10 21:06:27 +00:00
public.get = function ()
2022-05-11 01:51:04 +00:00
---@class modbus_frame
local frame = {
scada_frame = self.frame,
txn_id = self.txn_id,
length = self.length,
unit_id = self.unit_id,
func_code = self.func_code,
data = self.data
}
2022-05-11 01:51:04 +00:00
return frame
end
2022-05-10 21:06:27 +00:00
return public
end
-- reactor PLC packet
comms.rplc_packet = function ()
local self = {
frame = nil,
2022-04-22 19:43:25 +00:00
raw = nil,
id = nil,
type = nil,
length = nil,
body = nil
}
2022-05-10 21:06:27 +00:00
---@class rplc_packet
local public = {}
2022-04-22 19:43:25 +00:00
-- check that type is known
local _rplc_type_valid = function ()
2022-05-10 21:06:27 +00:00
return self.type == RPLC_TYPES.LINK_REQ or
self.type == RPLC_TYPES.STATUS or
self.type == RPLC_TYPES.MEK_STRUCT or
self.type == RPLC_TYPES.MEK_BURN_RATE or
self.type == RPLC_TYPES.RPS_ENABLE or
self.type == RPLC_TYPES.RPS_SCRAM or
2022-05-05 15:55:04 +00:00
self.type == RPLC_TYPES.RPS_ALARM or
self.type == RPLC_TYPES.RPS_STATUS or
self.type == RPLC_TYPES.RPS_RESET
end
-- make an RPLC packet
2022-05-10 21:06:27 +00:00
---@param id integer
---@param packet_type RPLC_TYPES
---@param data table
public.make = function (id, packet_type, data)
2022-04-22 19:43:25 +00:00
-- packet accessor properties
self.id = id
self.type = packet_type
2022-04-22 19:43:25 +00:00
self.length = #data
self.data = data
2022-04-22 19:43:25 +00:00
-- populate raw array
self.raw = { self.id, self.type }
for i = 1, #data do
2022-05-07 17:39:12 +00:00
insert(self.raw, data[i])
2022-04-22 19:43:25 +00:00
end
end
-- decode an RPLC packet from a SCADA frame
2022-05-10 21:06:27 +00:00
---@param frame scada_packet
---@return boolean success
public.decode = function (frame)
if frame then
self.frame = frame
2022-04-22 19:43:25 +00:00
if frame.protocol() == PROTOCOLS.RPLC then
local ok = frame.length() >= 2
if ok then
2022-04-22 19:43:25 +00:00
local data = frame.data()
2022-05-10 21:06:27 +00:00
public.make(data[1], data[2], { table.unpack(data, 3, #data) })
ok = _rplc_type_valid()
end
return ok
else
log.debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log.debug("nil frame encountered", true)
return false
end
end
2022-04-22 19:43:25 +00:00
-- get raw to send
2022-05-10 21:06:27 +00:00
public.raw_sendable = function () return self.raw end
2022-04-22 19:43:25 +00:00
2022-05-26 23:37:19 +00:00
-- get this packet as a frame with an immutable relation to this object
2022-05-10 21:06:27 +00:00
public.get = function ()
2022-05-11 01:51:04 +00:00
---@class rplc_frame
local frame = {
scada_frame = self.frame,
id = self.id,
type = self.type,
length = self.length,
data = self.data
}
2022-05-11 01:51:04 +00:00
return frame
end
2022-05-10 21:06:27 +00:00
return public
end
-- SCADA management packet
comms.mgmt_packet = function ()
local self = {
frame = nil,
2022-04-22 19:43:25 +00:00
raw = nil,
type = nil,
length = nil,
data = nil
}
2022-05-10 21:06:27 +00:00
---@class mgmt_packet
local public = {}
2022-04-22 19:43:25 +00:00
-- check that type is known
local _scada_type_valid = function ()
2022-05-10 21:06:27 +00:00
return self.type == SCADA_MGMT_TYPES.KEEP_ALIVE or
self.type == SCADA_MGMT_TYPES.CLOSE or
self.type == SCADA_MGMT_TYPES.REMOTE_LINKED or
2022-05-10 21:06:27 +00:00
self.type == SCADA_MGMT_TYPES.RTU_ADVERT
end
-- make a SCADA management packet
2022-05-10 21:06:27 +00:00
---@param packet_type SCADA_MGMT_TYPES
---@param data table
public.make = function (packet_type, data)
2022-04-22 19:43:25 +00:00
-- packet accessor properties
self.type = packet_type
2022-04-22 19:43:25 +00:00
self.length = #data
self.data = data
2022-04-22 19:43:25 +00:00
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
2022-05-07 17:39:12 +00:00
insert(self.raw, data[i])
2022-04-22 19:43:25 +00:00
end
end
-- decode a SCADA management packet from a SCADA frame
2022-05-10 21:06:27 +00:00
---@param frame scada_packet
---@return boolean success
public.decode = function (frame)
if frame then
self.frame = frame
2022-04-22 19:43:25 +00:00
if frame.protocol() == PROTOCOLS.SCADA_MGMT then
local ok = frame.length() >= 1
2022-05-10 21:06:27 +00:00
if ok then
2022-04-22 19:43:25 +00:00
local data = frame.data()
2022-05-10 21:06:27 +00:00
public.make(data[1], { table.unpack(data, 2, #data) })
ok = _scada_type_valid()
end
2022-05-10 21:06:27 +00:00
return ok
else
log.debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log.debug("nil frame encountered", true)
return false
end
end
2022-04-22 19:43:25 +00:00
-- get raw to send
2022-05-10 21:06:27 +00:00
public.raw_sendable = function () return self.raw end
2022-04-22 19:43:25 +00:00
2022-05-26 23:37:19 +00:00
-- get this packet as a frame with an immutable relation to this object
2022-05-10 21:06:27 +00:00
public.get = function ()
2022-05-11 01:51:04 +00:00
---@class mgmt_frame
local frame = {
scada_frame = self.frame,
type = self.type,
length = self.length,
data = self.data
}
2022-05-11 01:51:04 +00:00
return frame
end
2022-05-10 21:06:27 +00:00
return public
end
2022-04-22 15:07:59 +00:00
-- SCADA coordinator packet
-- @todo
comms.coord_packet = function ()
2022-04-22 15:07:59 +00:00
local self = {
frame = nil,
2022-04-22 19:43:25 +00:00
raw = nil,
2022-04-22 15:07:59 +00:00
type = nil,
length = nil,
data = nil
}
2022-05-10 21:06:27 +00:00
---@class coord_packet
local public = {}
2022-04-22 15:07:59 +00:00
local _coord_type_valid = function ()
-- @todo
return false
end
-- make a coordinator packet
2022-05-10 21:06:27 +00:00
---@param packet_type any
---@param data table
public.make = function (packet_type, data)
2022-04-22 19:43:25 +00:00
-- packet accessor properties
2022-04-22 15:07:59 +00:00
self.type = packet_type
2022-04-22 19:43:25 +00:00
self.length = #data
2022-04-22 15:07:59 +00:00
self.data = data
2022-04-22 19:43:25 +00:00
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
2022-05-07 17:39:12 +00:00
insert(self.raw, data[i])
2022-04-22 19:43:25 +00:00
end
2022-04-22 15:07:59 +00:00
end
-- decode a coordinator packet from a SCADA frame
2022-05-10 21:06:27 +00:00
---@param frame scada_packet
---@return boolean success
public.decode = function (frame)
2022-04-22 15:07:59 +00:00
if frame then
self.frame = frame
2022-04-22 19:43:25 +00:00
if frame.protocol() == PROTOCOLS.COORD_DATA then
local ok = frame.length() >= 1
2022-04-22 15:07:59 +00:00
if ok then
2022-04-22 19:43:25 +00:00
local data = frame.data()
2022-05-10 21:06:27 +00:00
public.make(data[1], { table.unpack(data, 2, #data) })
2022-04-22 15:07:59 +00:00
ok = _coord_type_valid()
end
return ok
else
log.debug("attempted COORD_DATA parse of incorrect protocol " .. frame.protocol(), true)
2022-04-22 15:07:59 +00:00
return false
end
else
log.debug("nil frame encountered", true)
2022-04-22 15:07:59 +00:00
return false
end
end
2022-04-22 19:43:25 +00:00
-- get raw to send
2022-05-10 21:06:27 +00:00
public.raw_sendable = function () return self.raw end
2022-04-22 19:43:25 +00:00
2022-05-26 23:37:19 +00:00
-- get this packet as a frame with an immutable relation to this object
2022-05-10 21:06:27 +00:00
public.get = function ()
2022-05-11 01:51:04 +00:00
---@class coord_frame
local frame = {
2022-04-22 19:43:25 +00:00
scada_frame = self.frame,
type = self.type,
length = self.length,
data = self.data
}
2022-05-11 01:51:04 +00:00
return frame
2022-04-22 19:43:25 +00:00
end
2022-05-10 21:06:27 +00:00
return public
2022-04-22 19:43:25 +00:00
end
-- coordinator API (CAPI) packet
-- @todo
comms.capi_packet = function ()
2022-04-22 19:43:25 +00:00
local self = {
frame = nil,
raw = nil,
type = nil,
length = nil,
data = nil
}
2022-05-10 21:06:27 +00:00
---@class capi_packet
local public = {}
2022-04-22 19:43:25 +00:00
local _coord_type_valid = function ()
-- @todo
return false
end
2022-05-10 21:06:27 +00:00
-- make a coordinator API packet
---@param packet_type any
---@param data table
public.make = function (packet_type, data)
2022-04-22 19:43:25 +00:00
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
2022-05-07 17:39:12 +00:00
insert(self.raw, data[i])
2022-04-22 19:43:25 +00:00
end
end
2022-05-10 21:06:27 +00:00
-- decode a coordinator API packet from a SCADA frame
---@param frame scada_packet
---@return boolean success
public.decode = function (frame)
2022-04-22 19:43:25 +00:00
if frame then
self.frame = frame
if frame.protocol() == PROTOCOLS.COORD_API then
local ok = frame.length() >= 1
2022-04-22 19:43:25 +00:00
if ok then
local data = frame.data()
2022-05-10 21:06:27 +00:00
public.make(data[1], { table.unpack(data, 2, #data) })
2022-04-22 19:43:25 +00:00
ok = _coord_type_valid()
end
return ok
else
log.debug("attempted COORD_API parse of incorrect protocol " .. frame.protocol(), true)
2022-04-22 19:43:25 +00:00
return false
end
else
log.debug("nil frame encountered", true)
2022-04-22 19:43:25 +00:00
return false
end
end
-- get raw to send
2022-05-10 21:06:27 +00:00
public.raw_sendable = function () return self.raw end
2022-04-22 19:43:25 +00:00
2022-05-26 23:37:19 +00:00
-- get this packet as a frame with an immutable relation to this object
2022-05-10 21:06:27 +00:00
public.get = function ()
2022-05-11 01:51:04 +00:00
---@class capi_frame
local frame = {
2022-04-22 15:07:59 +00:00
scada_frame = self.frame,
type = self.type,
length = self.length,
data = self.data
}
2022-05-11 01:51:04 +00:00
return frame
2022-04-22 15:07:59 +00:00
end
2022-05-10 21:06:27 +00:00
return public
2022-04-22 15:07:59 +00:00
end
2022-05-13 15:08:22 +00:00
-- convert rtu_t to RTU unit type
2022-05-10 21:06:27 +00:00
---@param type rtu_t
---@return RTU_UNIT_TYPES|nil
2022-05-13 15:08:22 +00:00
comms.rtu_t_to_unit_type = function (type)
2022-05-09 13:34:26 +00:00
if type == rtu_t.redstone then
return RTU_UNIT_TYPES.REDSTONE
2022-05-09 13:34:26 +00:00
elseif type == rtu_t.boiler then
return RTU_UNIT_TYPES.BOILER
2022-05-09 13:34:26 +00:00
elseif type == rtu_t.boiler_valve then
return RTU_UNIT_TYPES.BOILER_VALVE
2022-05-09 13:34:26 +00:00
elseif type == rtu_t.turbine then
return RTU_UNIT_TYPES.TURBINE
2022-05-09 13:34:26 +00:00
elseif type == rtu_t.turbine_valve then
return RTU_UNIT_TYPES.TURBINE_VALVE
2022-05-09 13:34:26 +00:00
elseif type == rtu_t.energy_machine then
return RTU_UNIT_TYPES.EMACHINE
2022-05-09 13:34:26 +00:00
elseif type == rtu_t.induction_matrix then
return RTU_UNIT_TYPES.IMATRIX
2022-05-09 13:34:26 +00:00
end
return nil
end
2022-05-13 15:08:22 +00:00
-- convert RTU unit type to rtu_t
---@param utype RTU_UNIT_TYPES
2022-05-10 21:06:27 +00:00
---@return rtu_t|nil
2022-05-13 15:08:22 +00:00
comms.advert_type_to_rtu_t = function (utype)
if utype == RTU_UNIT_TYPES.REDSTONE then
2022-05-09 13:34:26 +00:00
return rtu_t.redstone
2022-05-13 15:08:22 +00:00
elseif utype == RTU_UNIT_TYPES.BOILER then
2022-05-09 13:34:26 +00:00
return rtu_t.boiler
2022-05-13 15:08:22 +00:00
elseif utype == RTU_UNIT_TYPES.BOILER_VALVE then
2022-05-09 13:34:26 +00:00
return rtu_t.boiler_valve
2022-05-13 15:08:22 +00:00
elseif utype == RTU_UNIT_TYPES.TURBINE then
2022-05-09 13:34:26 +00:00
return rtu_t.turbine
2022-05-13 15:08:22 +00:00
elseif utype == RTU_UNIT_TYPES.TURBINE_VALVE then
2022-05-09 13:34:26 +00:00
return rtu_t.turbine_valve
2022-05-13 15:08:22 +00:00
elseif utype == RTU_UNIT_TYPES.EMACHINE then
2022-05-09 13:34:26 +00:00
return rtu_t.energy_machine
2022-05-13 15:08:22 +00:00
elseif utype == RTU_UNIT_TYPES.IMATRIX then
2022-05-09 13:34:26 +00:00
return rtu_t.induction_matrix
end
return nil
end
return comms