mirror of
https://github.com/jwetzell/acn-js.git
synced 2026-07-26 10:28:41 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
commonjs: true,
|
||||||
|
es2021: true,
|
||||||
|
},
|
||||||
|
extends: ['airbnb', 'prettier'],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
},
|
||||||
|
ignorePatterns: ['**/node_modules', '**/dist'],
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
*.md
|
||||||
|
*.min.*
|
||||||
|
dist
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
trailingComma: 'es5',
|
||||||
|
tabWidth: 2,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
bracketSameLine: true,
|
||||||
|
printWidth: 120,
|
||||||
|
};
|
||||||
Generated
+3381
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "acn",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"author": {
|
||||||
|
"name": "Joel Wetzell",
|
||||||
|
"email": "me@jwetzell.com",
|
||||||
|
"url": "https://jwetzell.com"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/jwetzell/acn-js.git"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint:check": "eslint ./",
|
||||||
|
"format:check": "prettier ./ --check",
|
||||||
|
"format:write": "prettier ./ --write"
|
||||||
|
},
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "3.3.3",
|
||||||
|
"eslint": "^8.48.0",
|
||||||
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
|
"eslint-config-prettier": "^9.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
dist
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "@jwetzell/acn",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"prebuild": "rimraf dist",
|
||||||
|
"build": "tsc",
|
||||||
|
"prepublishOnly": "npm run build",
|
||||||
|
"pretest": "npm run build",
|
||||||
|
"test": "node --test --experimental-test-coverage"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"author": {
|
||||||
|
"name": "Joel Wetzell",
|
||||||
|
"email": "me@jwetzell.com",
|
||||||
|
"url": "https://jwetzell.com"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/jwetzell/acn-js.git"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "22.7.5",
|
||||||
|
"rimraf": "6.0.1",
|
||||||
|
"typescript": "5.6.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './models';
|
||||||
|
export * from './udp';
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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: Uint8Array;
|
||||||
|
data: Uint8Array;
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import rlp from './rlp';
|
||||||
|
export default {
|
||||||
|
rlp,
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { RootLayerPDU } from '../models';
|
||||||
|
|
||||||
|
function decode(bytes: Uint8Array): RootLayerPDU {
|
||||||
|
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('Root Layer PDU must have a vector');
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerFlag = ((flags >> 1) & 0x1) === 1;
|
||||||
|
|
||||||
|
if (!headerFlag) {
|
||||||
|
throw new Error('Root Layer PDU must have a header');
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataFlag = (flags & 0x1) === 1;
|
||||||
|
|
||||||
|
if (!dataFlag) {
|
||||||
|
// TODO(jwetzell): idk know if this is true
|
||||||
|
throw new Error('Root Layer 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.getUint32(vectorOffset);
|
||||||
|
const headerOffset = vectorOffset + 4;
|
||||||
|
|
||||||
|
const header = bytes.subarray(headerOffset, headerOffset + 16);
|
||||||
|
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);
|
||||||
|
|
||||||
|
return {
|
||||||
|
vector,
|
||||||
|
header,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
decode,
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { ACNPacket, RootLayerPDU, UDPPreamble } from './models';
|
||||||
|
import pdu from './pdu';
|
||||||
|
|
||||||
|
// ANSI E1.17 - 2015 (R2020) EPI 17
|
||||||
|
export function decode(bytes: Uint8Array): ACNPacket<UDPPreamble, RootLayerPDU, undefined> {
|
||||||
|
if (bytes.byteLength < 16) {
|
||||||
|
throw new Error('ACN UDP packet must be at least 16 bytes long');
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||||
|
|
||||||
|
const preambleSize = view.getUint16(0, false);
|
||||||
|
if (preambleSize != 16) {
|
||||||
|
throw new Error('ACN UDP preamble size should 16');
|
||||||
|
}
|
||||||
|
const postambleSize = view.getUint16(2, false);
|
||||||
|
if (postambleSize > 0) {
|
||||||
|
throw new Error('ACN UDP postamble size should be 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
const packetIdentifier = new TextDecoder().decode(bytes.subarray(4, preambleSize));
|
||||||
|
|
||||||
|
if (packetIdentifier !== 'ASC-E1.17\0\0\0') {
|
||||||
|
throw new Error('ACN Packet Identifier is incorrect');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pduBlock = pdu.rlp.decode(bytes.subarray(preambleSize));
|
||||||
|
|
||||||
|
return {
|
||||||
|
preamble: {
|
||||||
|
preambleSize,
|
||||||
|
postambleSize,
|
||||||
|
packetIdentifier,
|
||||||
|
},
|
||||||
|
pduBlock,
|
||||||
|
postamble: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
const { deepEqual, throws } = require('assert');
|
||||||
|
const { describe, it } = require('node:test');
|
||||||
|
const acn = require('../dist/index');
|
||||||
|
|
||||||
|
const goodTests = [
|
||||||
|
{
|
||||||
|
description: 'simple udp pdu join',
|
||||||
|
bytes: new Uint8Array([
|
||||||
|
0, 16, 0, 0, 65, 83, 67, 45, 69, 49, 46, 49, 55, 0, 0, 0, 0x70, 72, 0, 0, 0, 1, 176, 171, 216, 42, 32, 210, 66,
|
||||||
|
64, 167, 205, 62, 73, 217, 153, 164, 229, 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,
|
||||||
|
]),
|
||||||
|
expected: {
|
||||||
|
preamble: {
|
||||||
|
preambleSize: 16,
|
||||||
|
postambleSize: 0,
|
||||||
|
packetIdentifier: 'ASC-E1.17\0\0\0',
|
||||||
|
},
|
||||||
|
pduBlock: {
|
||||||
|
vector: 1,
|
||||||
|
header: new Uint8Array([176, 171, 216, 42, 32, 210, 66, 64, 167, 205, 62, 73, 217, 153, 164, 229]),
|
||||||
|
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,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
postamble: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const badTests = [
|
||||||
|
{
|
||||||
|
description: 'bad UDP packet size',
|
||||||
|
bytes: new Uint8Array([0, 16]),
|
||||||
|
throwsMessage: { name: /^Error$/, message: /ACN UDP packet must be at least 16 bytes long/ },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'bad UDP preamble size',
|
||||||
|
bytes: new Uint8Array([
|
||||||
|
0, 20, 0, 0, 65, 83, 67, 45, 69, 49, 46, 49, 55, 0, 0, 0, 112, 72, 0, 0, 0, 1, 176, 171, 216, 42, 32, 210, 66, 64,
|
||||||
|
167, 205, 62, 73, 217, 153, 164, 229, 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,
|
||||||
|
]),
|
||||||
|
throwsMessage: { name: /^Error$/, message: /ACN UDP preamble size should 16/ },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'bad UDP postamble size',
|
||||||
|
bytes: new Uint8Array([
|
||||||
|
0, 16, 0, 2, 65, 83, 67, 45, 69, 49, 46, 49, 55, 0, 0, 0, 112, 72, 0, 0, 0, 1, 176, 171, 216, 42, 32, 210, 66, 64,
|
||||||
|
167, 205, 62, 73, 217, 153, 164, 229, 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,
|
||||||
|
]),
|
||||||
|
throwsMessage: { name: /^Error$/, message: /ACN UDP postamble size should be 0/ },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'bad ACN Packet Identifier',
|
||||||
|
bytes: new Uint8Array([
|
||||||
|
0, 16, 0, 0, 63, 83, 67, 45, 69, 49, 46, 49, 55, 0, 0, 0, 112, 72, 0, 0, 0, 1, 176, 171, 216, 42, 32, 210, 66, 64,
|
||||||
|
167, 205, 62, 73, 217, 153, 164, 229, 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,
|
||||||
|
]),
|
||||||
|
throwsMessage: { name: /^Error$/, message: /ACN Packet Identifier is incorrect/ },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('ACN Message Decoding Pass', () => {
|
||||||
|
goodTests.forEach((messageTest) => {
|
||||||
|
it(messageTest.description, () => {
|
||||||
|
const decoded = acn.decode(messageTest.bytes);
|
||||||
|
deepEqual(decoded, messageTest.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ACN Message Decoding Throws', () => {
|
||||||
|
badTests.forEach((messageTest) => {
|
||||||
|
it(messageTest.description, () => {
|
||||||
|
throws(() => {
|
||||||
|
acn.decode(messageTest.bytes);
|
||||||
|
}, messageTest.throwsMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "ES2020",
|
||||||
|
"declaration": true,
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist", "examples"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user