mirror of
https://github.com/jwetzell/free-d-js.git
synced 2026-08-20 22:38:58 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
dist
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './decode';
|
||||
export * from './encode';
|
||||
export * from './types';
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"declaration": false,
|
||||
"outDir": "./dist/cjs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"declaration": false,
|
||||
"outDir": "./dist/esm"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "Node",
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"rootDir": "src",
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist/types"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user