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

532 lines
14 KiB
Lua

PROTOCOLS = {
MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol
RPLC = 1, -- reactor PLC protocol
SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc
COORD_DATA = 3, -- data/control packets for coordinators to/from supervisory controllers
COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
}
RPLC_TYPES = {
KEEP_ALIVE = 0, -- keep alive packets
LINK_REQ = 1, -- linking requests
STATUS = 2, -- reactor/system status
MEK_STRUCT = 3, -- mekanism build structure
MEK_SCRAM = 4, -- SCRAM reactor
MEK_ENABLE = 5, -- enable reactor
MEK_BURN_RATE = 6, -- set burn rate
ISS_STATUS = 7, -- ISS status
ISS_ALARM = 8, -- ISS alarm broadcast
ISS_CLEAR = 9 -- clear ISS trip (if in bad state, will trip immediately)
}
RPLC_LINKING = {
ALLOW = 0, -- link approved
DENY = 1, -- link denied
COLLISION = 2 -- link denied due to existing active link
}
SCADA_MGMT_TYPES = {
PING = 0, -- generic ping
SV_HEARTBEAT = 1, -- supervisor heartbeat
REMOTE_LINKED = 2, -- remote device linked
RTU_ADVERT = 3, -- RTU capability advertisement
RTU_HEARTBEAT = 4, -- RTU heartbeat
}
RTU_ADVERT_TYPES = {
BOILER = 0, -- boiler
TURBINE = 1, -- turbine
IMATRIX = 2, -- induction matrix
REDSTONE = 3 -- redstone I/O
}
-- generic SCADA packet object
function scada_packet()
local self = {
modem_msg_in = nil,
valid = false,
raw = nil,
seq_num = nil,
protocol = nil,
length = nil,
payload = nil
}
-- make a SCADA packet
local make = function (seq_num, protocol, payload)
self.valid = true
self.seq_num = seq_num
self.protocol = protocol
self.length = #payload
self.payload = payload
self.raw = { self.seq_num, self.protocol, self.payload }
end
-- parse in a modem message as a SCADA packet
local 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
return self.valid
end
-- public accessors --
local modem_event = function () return self.modem_msg_in end
local raw_sendable = function () return self.raw end
local local_port = function () return self.modem_msg_in.s_port end
local remote_port = function () return self.modem_msg_in.r_port end
local is_valid = function () return self.valid end
local seq_num = function () return self.seq_num end
local protocol = function () return self.protocol end
local length = function () return self.length end
local data = function () return self.payload end
return {
-- construct
make = make,
receive = receive,
-- raw access
modem_event = modem_event,
raw_sendable = raw_sendable,
-- ports
local_port = local_port,
remote_port = remote_port,
-- well-formed
is_valid = is_valid,
-- packet properties
seq_num = seq_num,
protocol = protocol,
length = length,
data = data
}
end
-- MODBUS packet
-- modeled after MODBUS TCP packet
function modbus_packet()
local self = {
frame = nil,
raw = nil,
txn_id = txn_id,
length = length,
unit_id = unit_id,
func_code = func_code,
data = data
}
-- make a MODBUS packet
local make = function (txn_id, unit_id, func_code, data)
self.txn_id = txn_id
self.length = #data
self.unit_id = unit_id
self.func_code = func_code
self.data = data
-- populate raw array
self.raw = { self.txn_id, self.unit_id, self.func_code }
for i = 1, self.length do
table.insert(self.raw, data[i])
end
end
-- decode a MODBUS packet from a SCADA frame
local decode = function (frame)
if frame then
self.frame = frame
if frame.protocol() == PROTOCOLS.MODBUS_TCP then
local size_ok = frame.length() >= 3
if size_ok then
local data = frame.data()
make(data[1], data[2], data[3], { table.unpack(data, 4, #data) })
end
return size_ok
else
log._debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log._debug("nil frame encountered", true)
return false
end
end
-- get raw to send
local raw_sendable = function () return self.raw end
-- get this packet
local get = function ()
return {
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
}
end
return {
-- construct
make = make,
decode = decode,
-- raw access
raw_sendable = raw_sendable,
-- formatted access
get = get
}
end
-- reactor PLC packet
function rplc_packet()
local self = {
frame = nil,
raw = nil,
id = nil,
type = nil,
length = nil,
body = nil
}
-- check that type is known
local _rplc_type_valid = function ()
return self.type == RPLC_TYPES.KEEP_ALIVE or
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_SCRAM or
self.type == RPLC_TYPES.MEK_ENABLE or
self.type == RPLC_TYPES.MEK_BURN_RATE or
self.type == RPLC_TYPES.ISS_ALARM or
self.type == RPLC_TYPES.ISS_STATUS or
self.type == RPLC_TYPES.ISS_CLEAR
end
-- make an RPLC packet
local make = function (id, packet_type, data)
-- packet accessor properties
self.id = id
self.type = packet_type
self.length = #data
self.data = data
-- populate raw array
self.raw = { self.id, self.type }
for i = 1, #data do
table.insert(self.raw, data[i])
end
end
-- decode an RPLC packet from a SCADA frame
local decode = function (frame)
if frame then
self.frame = frame
if frame.protocol() == PROTOCOLS.RPLC then
local ok = frame.length() >= 2
if ok then
local data = frame.data()
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
-- get raw to send
local raw_sendable = function () return self.raw end
-- get this packet
local get = function ()
return {
scada_frame = self.frame,
id = self.id,
type = self.type,
length = self.length,
data = self.data
}
end
return {
-- construct
make = make,
decode = decode,
-- raw access
raw_sendable = raw_sendable,
-- formatted access
get = get
}
end
-- SCADA management packet
function mgmt_packet()
local self = {
frame = nil,
raw = nil,
type = nil,
length = nil,
data = nil
}
-- check that type is known
local _scada_type_valid = function ()
return self.type == SCADA_MGMT_TYPES.PING or
self.type == SCADA_MGMT_TYPES.SV_HEARTBEAT or
self.type == SCADA_MGMT_TYPES.REMOTE_LINKED or
self.type == SCADA_MGMT_TYPES.RTU_ADVERT or
self.type == SCADA_MGMT_TYPES.RTU_HEARTBEAT
end
-- make a SCADA management packet
local make = function (packet_type, data)
-- 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
table.insert(self.raw, data[i])
end
end
-- decode a SCADA management packet from a SCADA frame
local decode = function (frame)
if frame then
self.frame = frame
if frame.protocol() == PROTOCOLS.SCADA_MGMT then
local ok = #data >= 1
if ok then
local data = frame.data()
make(data[1], { table.unpack(data, 2, #data) })
ok = _scada_type_valid()
end
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
-- get raw to send
local raw_sendable = function () return self.raw end
-- get this packet
local get = function ()
return {
scada_frame = self.frame,
type = self.type,
length = self.length,
data = self.data
}
end
return {
-- construct
make = make,
decode = decode,
-- raw access
raw_sendable = raw_sendable,
-- formatted access
get = get
}
end
-- SCADA coordinator packet
-- @todo
function coord_packet()
local self = {
frame = nil,
raw = nil,
type = nil,
length = nil,
data = nil
}
local _coord_type_valid = function ()
-- @todo
return false
end
-- make a coordinator packet
local make = function (packet_type, data)
-- 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
table.insert(self.raw, data[i])
end
end
-- decode a coordinator packet from a SCADA frame
local decode = function (frame)
if frame then
self.frame = frame
if frame.protocol() == PROTOCOLS.COORD_DATA then
local ok = #data >= 1
if ok then
local data = frame.data()
make(data[1], { table.unpack(data, 2, #data) })
ok = _coord_type_valid()
end
return ok
else
log._debug("attempted COORD_DATA parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log._debug("nil frame encountered", true)
return false
end
end
-- get raw to send
local raw_sendable = function () return self.raw end
-- get this packet
local get = function ()
return {
scada_frame = self.frame,
type = self.type,
length = self.length,
data = self.data
}
end
return {
-- construct
make = make,
decode = decode,
-- raw access
raw_sendable = raw_sendable,
-- formatted access
get = get
}
end
-- coordinator API (CAPI) packet
-- @todo
function capi_packet()
local self = {
frame = nil,
raw = nil,
type = nil,
length = nil,
data = nil
}
local _coord_type_valid = function ()
-- @todo
return false
end
-- make a coordinator packet
local make = function (packet_type, data)
-- 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
table.insert(self.raw, data[i])
end
end
-- decode a coordinator packet from a SCADA frame
local decode = function (frame)
if frame then
self.frame = frame
if frame.protocol() == PROTOCOLS.COORD_API then
local ok = #data >= 1
if ok then
local data = frame.data()
make(data[1], { table.unpack(data, 2, #data) })
ok = _coord_type_valid()
end
return ok
else
log._debug("attempted COORD_API parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log._debug("nil frame encountered", true)
return false
end
end
-- get raw to send
local raw_sendable = function () return self.raw end
-- get this packet
local get = function ()
return {
scada_frame = self.frame,
type = self.type,
length = self.length,
data = self.data
}
end
return {
-- construct
make = make,
decode = decode,
-- raw access
raw_sendable = raw_sendable,
-- formatted access
get = get
}
end