Merge pull request #5 from jwetzell/feat/sdt

Add support for decoding SDT protocol blocks inside RLP
This commit is contained in:
2024-12-08 21:12:43 -06:00
committed by GitHub
11 changed files with 538 additions and 36 deletions
+18
View File
@@ -4,5 +4,23 @@ not sure how far I will take this but messing around with E1.17 ACN protocol
## areas
- rlp
- sdt
- vectors
- [x] JOIN
- [x] JOIN_REFUSE
- [x] JOIN_ACCEPT
- [x] LEAVING
- [x] NAK
- [x] REL_WRAP
- [x] UNREL_WRAP
- [x] GET_SESSIONS
- [ ] SESSIONS
- [x] ACK
- [ ] CHANNEL_PARAMS
- [ ] LEAVE
- [x] CONNECT
- [x] CONNECT_ACCEPT
- [x] CONNECT_REFUSE
- [x] DISCONNECT
- [x] DISCONNECTING
- dmp
- ddl
+45
View File
@@ -0,0 +1,45 @@
export enum Protocol {
SDT = 1,
}
export enum SDTVector {
REL_WRAP = 1,
UNREL_WRAP,
CHANNEL_PARAMS,
JOIN,
JOIN_REFUSE,
JOIN_ACCEPT,
LEAVE,
LEAVING,
CONNECT,
CONNECT_ACCEPT,
CONNECT_REFUSE,
DISCONNECT,
DISCONNECTING,
ACK,
NAK,
GET_SESSIONS,
SESSIONS,
}
export enum TransportLayerAddressType {
SDT_ADDR_NULL = 0,
SDT_ADDR_IPV4,
SDT_ADDR_IPV6,
}
export enum SDTReasonCode {
NONSPECIFIC = 1,
ILLEGAL_PARAMETERS,
LOW_RESOURCES,
ALREADY_MEMBER,
BAD_ADDRESS_TYPE,
NO_RECIPROCAL_CHANNEL,
CHANNEL_EXPIRED,
LOST_SEQUENCE,
SATURATED,
TRANSPORT_ADDRESS_CHANGING,
ASKED_TO_LEAVE,
NO_RECIPIENT,
ONLY_UNICAST_SUPPORTED,
}
+1 -1
View File
@@ -1,2 +1,2 @@
export * from './models';
export * from './types';
export * from './udp';
-21
View File
@@ -1,21 +0,0 @@
export type Protocols = {
SDT: 1;
};
export type UDPPreamble = {
preambleSize: number;
postambleSize: number;
packetIdentifier: string;
};
export type ACNPacket<PreambleType, PDUBlockType, PostambleType> = {
preamble: PreambleType;
pduBlock: PDUBlockType;
postamble: PostambleType;
};
export type RootLayerPDU = {
vector: number;
header: string;
data: Uint8Array;
};
+2
View File
@@ -1,4 +1,6 @@
import rlp from './rlp';
import sdt from './sdt';
export default {
rlp,
sdt,
};
+19 -2
View File
@@ -1,4 +1,6 @@
import { RootLayerPDU } from '../models';
import pdu from '.';
import { Protocol } from '../enums';
import { RootLayerPDU, SessionDataTransportPDU } from '../types';
import { toHex } from '../utils';
function decode(bytes: Uint8Array): RootLayerPDU {
@@ -49,7 +51,22 @@ function decode(bytes: Uint8Array): RootLayerPDU {
const dataOffset = headerOffset + 16;
// NOTE(jwetzell): flags/lengthH + lengthL + lengthX + vector + header
const dataLength = length - (1 + 1 + (lengthFlag ? 1 : 0) + 4 + 16);
const data = bytes.subarray(dataOffset, dataOffset + dataLength);
let data: SessionDataTransportPDU[] | Uint8Array = bytes.subarray(dataOffset, dataOffset + dataLength);
if (vector === Protocol.SDT) {
const blocks: SessionDataTransportPDU[] = [];
let blockDataOffset = 0;
// TODO(jwetzell): this could blow up so might need some attention later?
while (blockDataOffset < dataLength) {
const blockData = pdu.sdt.decode(bytes.subarray(dataOffset + blockDataOffset));
blocks.push(blockData);
blockDataOffset += blockData.length;
}
data = blocks;
} else {
console.error(`unhandled protocol in RLP: ${vector}`);
}
return {
vector,
+275
View File
@@ -0,0 +1,275 @@
import { Protocol, SDTVector } from '../enums';
import {
SDTAckData,
SDTConnectAcceptData,
SDTConnectData,
SDTConnectRefuseData,
SDTDisconnectData,
SDTDisconnectingData,
SDTGetSessionsData,
SDTJoinAcceptData,
SDTJoinData,
SDTJoinRefuseData,
SDTLeavingData,
SDTNakData,
SDTWrapperData,
SessionDataTransportPDU,
TransportLayerAddress,
} from '../types';
import { toHex } from '../utils';
// TODO(jwetzell): work out flag inheritance, will need previous PDU
export function decode(bytes: Uint8Array): SessionDataTransportPDU {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const flags = view.getUint8(0) >> 4;
const lengthFlag = ((flags >> 3) & 0x1) === 1;
const vectorFlag = ((flags >> 2) & 0x1) === 1;
if (!vectorFlag) {
throw new Error('SDT PDU must have a vector');
}
const dataFlag = (flags & 0x1) === 1;
if (!dataFlag) {
// TODO(jwetzell): idk if this is true
throw new Error('SDT PDU must have data');
}
const lengthH = view.getUint8(0) & 0x0f;
let lengthOffset = 1;
const lengthL = view.getUint8(lengthOffset);
let length = (lengthH << 8) + lengthL;
if (lengthFlag) {
lengthOffset += 1;
const lengthX = view.getUint8(lengthOffset);
length = (lengthH << 16) + (lengthL << 8) + lengthX;
}
let vectorOffset = lengthOffset + 1;
if (lengthFlag) {
vectorOffset += 1;
}
const vector = view.getUint8(vectorOffset);
const dataOffset = vectorOffset + 1;
// NOTE(jwetzell): flags/lengthH + lengthL + lengthX + vector
const dataLength = length - (1 + 1 + (lengthFlag ? 1 : 0) + 1);
const data = decodeData(vector, bytes.subarray(dataOffset, dataOffset + dataLength));
return {
vector,
length,
data,
};
}
function decodeClientBlock(bytes: Uint8Array) {
if (bytes.byteLength === 0) {
return undefined;
}
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const flags = view.getUint8(0) >> 4;
const lengthFlag = ((flags >> 3) & 0x1) === 1;
const vectorFlag = ((flags >> 2) & 0x1) === 1;
if (!vectorFlag) {
throw new Error('SDT PDU must have a vector');
}
const dataFlag = (flags & 0x1) === 1;
if (!dataFlag) {
// TODO(jwetzell): idk if this is true
throw new Error('SDT PDU must have data');
}
const lengthH = view.getUint8(0) & 0x0f;
let lengthOffset = 1;
const lengthL = view.getUint8(lengthOffset);
let length = (lengthH << 8) + lengthL;
if (lengthFlag) {
lengthOffset += 1;
const lengthX = view.getUint8(lengthOffset);
length = (lengthH << 16) + (lengthL << 8) + lengthX;
}
let vectorOffset = lengthOffset + 1;
if (lengthFlag) {
vectorOffset += 1;
}
const memberID = view.getUint16(vectorOffset);
const headerOffset = vectorOffset + 2;
const clientProtocol = view.getUint32(headerOffset);
const association = view.getUint16(headerOffset + 4);
const dataOffset = headerOffset + 6;
// NOTE(jwetzell): flags/lengthH + lengthL + lengthX + vector + header
const dataLength = length - (1 + 1 + (lengthFlag ? 1 : 0) + 2 + 6);
let data: SessionDataTransportPDU | Uint8Array = bytes.subarray(dataOffset, dataOffset + dataLength);
if (clientProtocol === Protocol.SDT) {
data = decode(data);
} else {
console.error(`SDT client block contains unknown protocol: ${clientProtocol}`);
}
return {
memberID,
clientProtocol,
association,
data,
};
}
function decodeData(
vector: SDTVector,
bytes: Uint8Array
):
| SDTJoinData
| SDTJoinAcceptData
| SDTJoinRefuseData
| SDTWrapperData
| SDTAckData
| SDTLeavingData
| SDTGetSessionsData
| SDTNakData
| SDTConnectData
| SDTConnectAcceptData
| SDTConnectRefuseData
| SDTDisconnectData
| SDTDisconnectingData
| Uint8Array {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
switch (vector) {
case SDTVector.JOIN: {
const destinationAddress: TransportLayerAddress = {
type: view.getUint8(30),
};
// TODO(jwetzell): handle IPv6, type === 2
if (destinationAddress.type === 1) {
// IPv4
destinationAddress.port = view.getUint16(31);
destinationAddress.address = bytes.subarray(33, 37).join('.');
}
const adhocExpiry = view.getUint8(45);
const joinData = {
componentID: toHex(bytes.subarray(0, 16)),
memberID: view.getUint16(16),
channelNumber: view.getUint16(18),
reciprocalChannel: view.getUint16(20),
totalSequenceNumber: view.getUint32(22),
reliableSequenceNumber: view.getUint32(26),
destinationAddress,
channelParameters: {
expiry: view.getUint8(37),
nakOutboundFlag: view.getUint8(38),
nakHoldoff: view.getUint16(39),
nakModulus: view.getUint16(41),
nakMaxWait: view.getUint16(43),
},
adhocExpiry,
};
return joinData;
}
case SDTVector.JOIN_ACCEPT: {
const joinData = {
leaderComponentID: toHex(bytes.subarray(0, 16)),
channelNumber: view.getUint16(16),
memberID: view.getUint16(18),
reliableSequenceNumber: view.getUint32(20),
reciprocalChannel: view.getUint16(24),
};
return joinData;
}
case SDTVector.JOIN_REFUSE: {
const joinData = {
leaderComponentID: toHex(bytes.subarray(0, 16)),
channelNumber: view.getUint16(16),
memberID: view.getUint16(18),
reliableSequenceNumber: view.getUint32(20),
refuseCode: view.getUint8(24),
};
return joinData;
}
case SDTVector.REL_WRAP:
case SDTVector.UNREL_WRAP: {
const wrapperData: SDTWrapperData = {
channelNumber: view.getUint16(0),
totalSequenceNumber: view.getUint32(2),
reliableSequenceNumber: view.getUint32(6),
oldestAvailableWrapper: view.getUint32(10),
firstMemberToAck: view.getUint16(14),
lastMemberToAck: view.getUint16(16),
makThreshold: view.getUint16(18),
sdtClientBlock: decodeClientBlock(bytes.subarray(20)),
};
return wrapperData;
}
case SDTVector.ACK: {
return {
reliableSequenceNumber: view.getUint32(0),
};
}
case SDTVector.LEAVING: {
return {
leaderComponentID: toHex(bytes.subarray(0, 16)),
channelNumber: view.getUint16(16),
memberID: view.getUint16(18),
reliableSequenceNumber: view.getUint32(20),
reasonCode: view.getUint8(24),
};
}
case SDTVector.GET_SESSIONS: {
return {
componentID: toHex(bytes.subarray(0, 16)),
};
}
case SDTVector.NAK: {
return {
leaderComponentID: toHex(bytes.subarray(0, 16)),
channelNumber: view.getUint16(16),
memberID: view.getUint16(18),
reliableSequenceNumber: view.getUint32(20),
firstMissedSequence: view.getUint32(24),
lastMissedSequence: view.getUint32(28),
};
}
case SDTVector.DISCONNECT:
case SDTVector.CONNECT:
case SDTVector.CONNECT_ACCEPT: {
return {
protocolID: view.getUint32(0),
};
}
case SDTVector.CONNECT_REFUSE: {
return {
protocolID: view.getUint32(0),
refuseCode: view.getUint8(4),
};
}
case SDTVector.DISCONNECTING: {
return {
protocolID: view.getUint32(0),
reasonCode: view.getUint8(4),
};
}
default:
console.error(`unhandled SDT vector: ${vector}`);
return bytes;
}
}
export default {
decode,
};
+141
View File
@@ -0,0 +1,141 @@
import { TransportLayerAddressType, SDTVector, SDTReasonCode } from './enums';
export type UDPPreamble = {
preambleSize: number;
postambleSize: number;
packetIdentifier: string;
};
export type ACNPacket<PreambleType, PDUBlockType, PostambleType> = {
preamble: PreambleType;
pduBlock: PDUBlockType;
postamble: PostambleType;
};
export type RootLayerPDU = {
vector: number;
header: string;
data: SessionDataTransportPDU[] | Uint8Array;
};
export type SDTChannelParams = {
expiry: number;
nakOutboundFlag: number;
nakHoldoff: number;
nakModulus: number;
nakMaxWait: number;
};
export type TransportLayerAddress = {
type: TransportLayerAddressType;
port?: number;
address?: string;
};
export type SDTJoinData = {
componentID: string;
memberID: number;
channelNumber: number;
reciprocalChannel: number;
totalSequenceNumber: number;
reliableSequenceNumber: number;
destinationAddress: TransportLayerAddress;
channelParameters: SDTChannelParams;
adhocExpiry: number;
};
export type SDTJoinAcceptData = {
leaderComponentID: string;
channelNumber: number;
memberID: number;
reliableSequenceNumber: number;
reciprocalChannel: number;
};
export type SDTJoinRefuseData = {
leaderComponentID: string;
channelNumber: number;
memberID: number;
reliableSequenceNumber: number;
refuseCode: SDTReasonCode;
};
export type SDTLeavingData = {
leaderComponentID: string;
channelNumber: number;
memberID: number;
reliableSequenceNumber: number;
reasonCode: SDTReasonCode;
};
export type SDTWrapperData = {
channelNumber: number;
totalSequenceNumber: number;
reliableSequenceNumber: number;
oldestAvailableWrapper: number;
firstMemberToAck: number;
lastMemberToAck: number;
makThreshold: number;
sdtClientBlock?: SDTClientBlock;
};
export type SDTAckData = {
reliableSequenceNumber: number;
};
export type SDTNakData = {
leaderComponenetID: string;
channelNumber: number;
memberID: number;
reliableSequenceNumber: number;
firstMissedSequence: number;
lastMissedSequence: number;
};
export type SDTConnectData = {
protocolID: number;
};
export type SDTConnectAcceptData = SDTConnectData;
export type SDTConnectRefuseData = {
protocolID: number;
refuseCode: SDTReasonCode;
};
export type SDTDisconnectData = SDTConnectData;
export type SDTDisconnectingData = {
protocolID: number;
reasonCode: SDTReasonCode;
};
export type SDTGetSessionsData = {
componentID: string;
};
export type SessionDataTransportPDU = {
vector: SDTVector;
length: number;
// TODO(jwetzell): cleanup these types
data:
| SDTJoinData
| SDTJoinAcceptData
| SDTJoinRefuseData
| SDTWrapperData
| SDTAckData
| SDTLeavingData
| SDTGetSessionsData
| SDTNakData
| SDTConnectData
| SDTConnectAcceptData
| SDTConnectRefuseData
| SDTDisconnectData
| SDTDisconnectingData
| Uint8Array;
};
export type SDTClientBlock = {
memberID: number;
clientProtocol: number;
association: number;
data: SessionDataTransportPDU | Uint8Array;
};
+1 -1
View File
@@ -1,4 +1,4 @@
import { ACNPacket, RootLayerPDU, UDPPreamble } from './models';
import { ACNPacket, RootLayerPDU, UDPPreamble } from './types';
import pdu from './pdu';
// ANSI E1.17 - 2015 (R2020) EPI 17
+7 -7
View File
@@ -1,7 +1,7 @@
export function toHex(bytes: Uint8Array): string{
let hex = '';
bytes.forEach((byte)=>{
hex += byte.toString(16).padStart(2,'0')
})
return hex;
}
export function toHex(bytes: Uint8Array): string {
let hex = '';
bytes.forEach((byte) => {
hex += byte.toString(16).padStart(2, '0');
});
return hex;
}
+29 -4
View File
@@ -20,10 +20,33 @@ const goodTests = [
pduBlock: {
vector: 1,
header: 'b0abd82a20d24240a7cd3e49d999a4e5',
data: new Uint8Array([
112, 50, 4, 73, 136, 131, 52, 255, 255, 74, 69, 176, 66, 102, 183, 33, 235, 216, 155, 0, 1, 244, 136, 0, 0, 0,
0, 244, 136, 0, 0, 244, 136, 1, 67, 216, 239, 192, 220, 207, 5, 0, 0, 2, 0, 10, 0, 20, 1, 44,
]),
data: [
{
vector: 4,
length: 50,
data: {
componentID: '49888334ffff4a45b04266b721ebd89b',
memberID: 1,
channelNumber: 62600,
reciprocalChannel: 0,
totalSequenceNumber: 62600,
reliableSequenceNumber: 62600,
destinationAddress: {
type: 1,
port: 17368,
address: '239.192.220.207'
},
channelParameters: {
expiry: 5,
nakOutboundFlag: 0,
nakHoldoff: 2,
nakModulus: 10,
nakMaxWait: 20
},
adhocExpiry: 1,
}
}
],
},
postamble: undefined,
},
@@ -71,7 +94,9 @@ const badTests = [
describe('ACN Message Decoding Pass', () => {
goodTests.forEach((messageTest) => {
it(messageTest.description, () => {
console.log(JSON.stringify(messageTest.expected))
const decoded = acn.decode(messageTest.bytes);
console.log(JSON.stringify(decoded))
deepEqual(decoded, messageTest.expected);
});
});