initial commit

This commit is contained in:
2024-10-29 11:55:53 -05:00
commit bcf508e88f
27 changed files with 4394 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
dist
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@jwetzell/free-d",
"version": "0.1.0",
"description": "libray for encoding/decoding free-d messages",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts"
}
},
"scripts": {
"prebuild": "rimraf dist",
"build": "npm run build:cjs && npm run build:esm && npm run build:types",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json",
"build:types": "tsc -p tsconfig.types.json",
"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/free-d-js.git"
},
"license": "MIT",
"devDependencies": {
"@types/node": "22.8.2",
"rimraf": "6.0.1",
"typescript": "5.6.3"
}
}
+55
View File
@@ -0,0 +1,55 @@
import { FreeDPosition, FreeDMessageType } from './types';
import { checksum, freeDUnitsToPosition, freeDUnitsToRotation } from './utils';
function bytesToPositionMessage(bytes: Uint8Array): FreeDPosition {
const id = bytes[1];
const pan = freeDUnitsToRotation(bytes[2], bytes[3], bytes[4]);
const tilt = freeDUnitsToRotation(bytes[5], bytes[6], bytes[7]);
const roll = freeDUnitsToRotation(bytes[8], bytes[9], bytes[10]);
const posX = freeDUnitsToPosition(bytes[11], bytes[12], bytes[13]);
const posY = freeDUnitsToPosition(bytes[14], bytes[15], bytes[16]);
const posZ = freeDUnitsToPosition(bytes[17], bytes[18], bytes[19]);
const zoom = bytes[20] * 65536 + bytes[21] * 256 + bytes[22];
const focus = bytes[23] * 65536 + bytes[24] * 256 + bytes[25];
const spare = bytes[26] * 256 + bytes[27];
return {
type: FreeDMessageType.Position,
id,
pan,
tilt,
roll,
posX,
posY,
posZ,
zoom,
focus,
spare,
};
}
export function decode(bytes: Uint8Array): FreeDPosition | undefined {
if (bytes.length === 0) {
throw new Error('cannot decode 0 bytes');
}
const messageType = bytes[0];
switch (messageType) {
case FreeDMessageType.Position:
if (bytes.length !== 29) {
throw new Error('freeD position message should be 29 bytes long');
}
if (checksum(bytes.subarray(0, bytes.length - 1)) !== bytes[bytes.length - 1]) {
throw new Error('freeD message failed checksum');
}
return bytesToPositionMessage(bytes);
default:
throw new Error('unsupported freeD message type');
}
}
+41
View File
@@ -0,0 +1,41 @@
import { FreeDPosition, FreeDMessageType } from './types';
import { checksum, positionToFreeDUnits, rotationToFreeDUnits } from './utils';
function positionMessageToBytes(message: FreeDPosition): Uint8Array {
const bytes = new Uint8Array(29);
bytes[0] = FreeDMessageType.Position;
bytes[1] = message.id;
bytes.set(rotationToFreeDUnits(message.pan), 2);
bytes.set(rotationToFreeDUnits(message.tilt), 5);
bytes.set(rotationToFreeDUnits(message.roll), 8);
bytes.set(positionToFreeDUnits(message.posX), 11);
bytes.set(positionToFreeDUnits(message.posY), 14);
bytes.set(positionToFreeDUnits(message.posZ), 17);
bytes[20] = Math.trunc(message.zoom / 65536);
bytes[21] = Math.trunc((message.zoom / 256) % 256);
bytes[22] = message.zoom % 256;
bytes[23] = Math.trunc(message.focus / 65536);
bytes[24] = Math.trunc((message.focus / 256) % 256);
bytes[25] = message.focus % 256;
if (message.spare !== undefined) {
bytes[26] = message.spare >> 8;
bytes[27] = message.spare % 256;
}
bytes[28] = checksum(bytes.subarray(0, 28));
return bytes;
}
export function encode(message: FreeDPosition): Uint8Array {
switch (message.type) {
case FreeDMessageType.Position:
return positionMessageToBytes(message);
default:
throw new Error('unsupported freeD message type');
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from './decode';
export * from './encode';
export * from './types';
+17
View File
@@ -0,0 +1,17 @@
export enum FreeDMessageType {
Position = 0xd1
}
export type FreeDPosition = {
type: FreeDMessageType.Position;
id: number;
pan: number;
tilt: number;
roll: number;
posZ: number;
posX: number;
posY: number;
zoom: number;
focus: number;
spare?: number;
};
+37
View File
@@ -0,0 +1,37 @@
export function checksum(bytes: Uint8Array): number {
let checksum = 0x40;
bytes.forEach((byte) => {
checksum -= byte;
if (checksum < 0) {
checksum += 256;
}
});
return checksum;
}
export function rotationToFreeDUnits(rotation: number): Uint8Array {
const view = new DataView(new ArrayBuffer(4));
const units = rotation * 32768 * 256;
view.setInt32(0, units);
return new Uint8Array(view.buffer, 0, 3);
}
export function positionToFreeDUnits(position: number): Uint8Array {
const view = new DataView(new ArrayBuffer(4));
const units = position * 64 * 256;
view.setInt32(0, units);
return new Uint8Array(view.buffer, 0, 3);
}
export function freeDUnitsToRotation(upper: number, middle: number, lower: number): number {
const padded = new Uint8Array([upper, middle, lower, 0]);
const view = new DataView(padded.buffer, padded.byteOffset, padded.byteLength);
return view.getInt32(0) / 256 / 32768;
}
export function freeDUnitsToPosition(upper: number, middle: number, lower: number): number {
const padded = new Uint8Array([upper, middle, lower, 0]);
const view = new DataView(padded.buffer, padded.byteOffset, padded.byteLength);
return view.getInt32(0) / 256 / 64;
}
+74
View File
@@ -0,0 +1,74 @@
const { deepEqual, throws } = require('assert');
const { describe, it } = require('node:test');
const { decode } = require('../dist/cjs');
const goodTests = [
{
description: 'simple position message',
bytes: new Uint8Array([
0xd1, 0x01, 0x5a, 0x00, 0x00, 0x2d, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x7f, 0xff, 0x40, 0x7f, 0xff, 0x80, 0x7f, 0xff,
0xc0, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x00, 50,
]),
expected: {
type: 0xd1,
id: 1,
pan: 180,
tilt: 90,
roll: -180,
posX: 131069,
posY: 131070,
posZ: 131071,
zoom: 66051,
focus: 263430,
spare: 0,
},
},
];
const badTests = [
{
description: 'position - bad checksum',
bytes: new Uint8Array([
0xd1, 0x01, 0x5a, 0x00, 0x00, 0x2d, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x7f, 0xff, 0x40, 0x7f, 0xff, 0x80, 0x7f, 0xff,
0xc0, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x00, 100,
]),
throwsMessage: { name: /^Error$/, message: /failed checksum/ },
},
{
description: 'position - incomplete',
bytes: new Uint8Array([
0xd1, 0x01, 0x5a, 0x00, 0x00, 0x2d, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x7f, 0xff, 0x40, 0x7f, 0xff, 0x80, 0x7f, 0xff,
0xc0, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x00,
]),
throwsMessage: { name: /^Error$/, message: /should be 29 bytes/ },
},
{
description: 'empty bytes',
bytes: new Uint8Array([]),
throwsMessage: { name: /^Error$/, message: /cannot decode 0 bytes/ },
},
{
description: 'bad message type',
bytes: new Uint8Array([0xff]),
throwsMessage: { name: /^Error$/, message: /unsupported freeD/ },
},
];
describe('FreeD Bytes Decoding', () => {
goodTests.forEach((bytesTest) => {
it(bytesTest.description, () => {
const decoded = decode(bytesTest.bytes);
deepEqual(decoded, bytesTest.expected);
});
});
});
describe('FreeD Bytes Decoding Throws', () => {
badTests.forEach((bytesTest) => {
it(bytesTest.description, () => {
throws(() => {
decode(bytesTest.bytes);
}, bytesTest.throwsMessage);
});
});
});
+55
View File
@@ -0,0 +1,55 @@
const { deepEqual, throws } = require('assert');
const { describe, it } = require('node:test');
const { encode } = require('../dist/cjs');
const goodTests = [
{
description: 'simple position message',
message: {
type: 0xd1,
id: 1,
pan: 180,
tilt: 90,
roll: -180,
posX: 131069,
posY: 131070,
posZ: 131071,
zoom: 66051,
focus: 263430,
spare: 0,
},
expected: new Uint8Array([
0xd1, 0x01, 0x5a, 0x00, 0x00, 0x2d, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x7f, 0xff, 0x40, 0x7f, 0xff, 0x80, 0x7f, 0xff,
0xc0, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x00, 50,
]),
},
];
const badTests = [
{
description: 'unsupported message',
message: {
type: 0xff,
},
throwsMessage: { name: /^Error$/, message: /unsupported freeD message type/ },
},
];
describe('FreeD Message Encoding', () => {
goodTests.forEach((messageTest) => {
it(messageTest.description, () => {
const decoded = encode(messageTest.message);
deepEqual(decoded, messageTest.expected);
});
});
});
describe('FreeD Message Encoding Throws', () => {
badTests.forEach((messageTest) => {
it(messageTest.description, () => {
throws(() => {
encode(messageTest.message);
}, messageTest.throwsMessage);
});
});
});
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"declaration": false,
"outDir": "./dist/cjs"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"declaration": false,
"outDir": "./dist/esm"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"moduleResolution": "Node",
"lib": ["ESNext"],
"target": "ESNext",
"esModuleInterop": true,
"isolatedModules": true,
"rootDir": "src",
"strict": true
},
"include": ["src"]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": true,
"declaration": true,
"outDir": "./dist/types"
}
}