node-toypad 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ var _a, _b, _c, _d;
2
+ import charactersJson from "../data/minifigs.json" with { type: "json" };
3
+ import vehiclesJson from "../data/vehicles.json" with { type: "json" };
4
+ import upgradesJson from "../data/upgrades.json" with { type: "json" };
5
+ import { VEHICLE_MAP_BY_ID } from "./vehicle-map.js";
6
+ const characters = (_a = charactersJson) !== null && _a !== void 0 ? _a : [];
7
+ const vehicles = (_b = vehiclesJson) !== null && _b !== void 0 ? _b : [];
8
+ const upgrades = (_c = upgradesJson) !== null && _c !== void 0 ? _c : [];
9
+ const characterMap = new Map(characters.map((character) => [character.id, character]));
10
+ const vehicleMap = new Map(vehicles.map((vehicle) => [vehicle.id, vehicle]));
11
+ const vehicleVariants = new Map();
12
+ const upgradeLabelMap = new Map(upgrades.map((entry) => [entry.id, entry.label]));
13
+ for (const vehicle of vehicles) {
14
+ const parentId = (vehicle.parentId || vehicle.id);
15
+ const variants = (_d = vehicleVariants.get(parentId)) !== null && _d !== void 0 ? _d : [];
16
+ variants.push(vehicle);
17
+ vehicleVariants.set(parentId, variants);
18
+ }
19
+ for (const [parentId, variants] of vehicleVariants.entries()) {
20
+ variants.sort((a, b) => {
21
+ const aBase = a.id === parentId ? 0 : 1;
22
+ const bBase = b.id === parentId ? 0 : 1;
23
+ if (aBase !== bBase) {
24
+ return aBase - bBase;
25
+ }
26
+ return a.id - b.id;
27
+ });
28
+ }
29
+ export function getCharacterById(id) {
30
+ return characterMap.get(id);
31
+ }
32
+ export function getVehicleById(id) {
33
+ return vehicleMap.get(id);
34
+ }
35
+ export function getVehicleVariant(id, step) {
36
+ if (typeof step === "number") {
37
+ const variants = vehicleVariants.get(id);
38
+ if (!variants) {
39
+ return undefined;
40
+ }
41
+ if (step < 0 || step >= variants.length) {
42
+ return undefined;
43
+ }
44
+ const variant = variants[step];
45
+ return { ...variant, step };
46
+ }
47
+ const vehicle = vehicleMap.get(id);
48
+ if (!vehicle) {
49
+ return undefined;
50
+ }
51
+ const variants = vehicleVariants.get(vehicle.parentId || vehicle.id);
52
+ const stepIndex = variants ? variants.findIndex((entry) => entry.id === vehicle.id) : -1;
53
+ return stepIndex >= 0 ? { ...vehicle, step: stepIndex } : vehicle;
54
+ }
55
+ export function listCharacters() {
56
+ return characters.slice();
57
+ }
58
+ export function listVehicles() {
59
+ return vehicles.slice();
60
+ }
61
+ export function getUpgradeLabel(id) {
62
+ return upgradeLabelMap.get(id);
63
+ }
64
+ export function listUpgradeLabels() {
65
+ return upgrades.slice();
66
+ }
67
+ export function getVehicleMap(id) {
68
+ var _a;
69
+ return (_a = VEHICLE_MAP_BY_ID[id]) !== null && _a !== void 0 ? _a : 0;
70
+ }
@@ -0,0 +1,35 @@
1
+ import { ActionType, RequestType, ToyPadPanel } from "./constants.js";
2
+ export interface ToyPadCommand {
3
+ id: RequestType | number;
4
+ params: number[];
5
+ }
6
+ export interface ToyPadResponseMessage {
7
+ kind: "response";
8
+ requestId: number;
9
+ payload: Buffer;
10
+ }
11
+ export interface ToyPadTagEvent {
12
+ kind: "event";
13
+ panel: ToyPadPanel;
14
+ action: ActionType;
15
+ index: number;
16
+ tagType: number;
17
+ signature: string;
18
+ raw: Buffer;
19
+ }
20
+ export type ToyPadIncomingMessage = ToyPadResponseMessage | ToyPadTagEvent;
21
+ export interface FlashOptions {
22
+ onTicks?: number;
23
+ offTicks?: number;
24
+ }
25
+ export declare function encodeCommand(command: ToyPadCommand, requestId: number): Buffer;
26
+ export declare function decodeMessage(data: Buffer): ToyPadIncomingMessage | undefined;
27
+ export declare function createSetColorCommand(panel: ToyPadPanel, color: number): ToyPadCommand;
28
+ export declare function createGetColorCommand(panel: ToyPadPanel): ToyPadCommand;
29
+ export declare function createFadeCommand(panel: ToyPadPanel, speed: number, cycles: number, color: number): ToyPadCommand;
30
+ export declare function createFlashCommand(panel: ToyPadPanel, color: number, count: number, options?: FlashOptions): ToyPadCommand;
31
+ export declare function createReadTagCommand(index: number, page: number): ToyPadCommand;
32
+ export declare function createWriteTagCommand(index: number, page: number, data: Buffer | number[]): ToyPadCommand;
33
+ export declare function decodeColor(payload: Buffer): number;
34
+ export declare function normalizePanel(value: number): ToyPadPanel;
35
+ export declare function normalizeAction(value: number): ActionType;
@@ -0,0 +1,177 @@
1
+ import { ActionType, CommandType, MessageType, PACKET_LENGTH, RequestType, ToyPadPanel } from "./constants.js";
2
+ export function encodeCommand(command, requestId) {
3
+ var _a;
4
+ const params = (_a = command.params) !== null && _a !== void 0 ? _a : [];
5
+ const payload = [
6
+ MessageType.Response,
7
+ (params.length + 2) & 0xff,
8
+ command.id & 0xff,
9
+ requestId & 0xff,
10
+ ...params.map((value) => value & 0xff)
11
+ ];
12
+ const withChecksum = appendChecksum(payload);
13
+ return padMessage(withChecksum);
14
+ }
15
+ export function decodeMessage(data) {
16
+ const normalized = normalizePacket(data);
17
+ if (!normalized.length) {
18
+ return undefined;
19
+ }
20
+ const type = normalized[0];
21
+ if (type === MessageType.Event) {
22
+ return decodeActionEvent(normalized);
23
+ }
24
+ if (type === MessageType.Response) {
25
+ return decodeResponse(normalized);
26
+ }
27
+ return undefined;
28
+ }
29
+ export function createSetColorCommand(panel, color) {
30
+ const rgb = normalizeColor(color);
31
+ return {
32
+ id: RequestType.SetColor,
33
+ params: [panel & 0xff, rgb.red, rgb.green, rgb.blue]
34
+ };
35
+ }
36
+ export function createGetColorCommand(panel) {
37
+ return {
38
+ id: RequestType.GetColor,
39
+ params: [((panel - 1) & 0xff) >>> 0]
40
+ };
41
+ }
42
+ export function createFadeCommand(panel, speed, cycles, color) {
43
+ const rgb = normalizeColor(color);
44
+ return {
45
+ id: RequestType.Fade,
46
+ params: [panel & 0xff, speed & 0xff, cycles & 0xff, rgb.red, rgb.green, rgb.blue]
47
+ };
48
+ }
49
+ export function createFlashCommand(panel, color, count, options = {}) {
50
+ var _a, _b;
51
+ const rgb = normalizeColor(color);
52
+ const offTicks = ((_a = options.offTicks) !== null && _a !== void 0 ? _a : 10) & 0xff;
53
+ const onTicks = ((_b = options.onTicks) !== null && _b !== void 0 ? _b : 10) & 0xff;
54
+ return {
55
+ id: RequestType.Flash,
56
+ params: [panel & 0xff, offTicks, onTicks, count & 0xff, rgb.red, rgb.green, rgb.blue]
57
+ };
58
+ }
59
+ export function createReadTagCommand(index, page) {
60
+ return {
61
+ id: RequestType.ReadTag,
62
+ params: [index & 0xff, page & 0xff]
63
+ };
64
+ }
65
+ export function createWriteTagCommand(index, page, data) {
66
+ const payload = Array.isArray(data) ? data.slice() : Array.from(data.values());
67
+ if (payload.length !== 16) {
68
+ throw new Error(`ToyPad write requires 16 bytes of data, received ${payload.length}.`);
69
+ }
70
+ return {
71
+ id: RequestType.WriteTag,
72
+ params: [index & 0xff, page & 0xff, ...payload.map((value) => value & 0xff)]
73
+ };
74
+ }
75
+ export function decodeColor(payload) {
76
+ if (payload.length < 3) {
77
+ return 0;
78
+ }
79
+ return ((payload[0] & 0xff) << 16) | ((payload[1] & 0xff) << 8) | (payload[2] & 0xff);
80
+ }
81
+ export function normalizePanel(value) {
82
+ switch (value) {
83
+ case ToyPadPanel.Center:
84
+ case ToyPadPanel.Left:
85
+ case ToyPadPanel.Right:
86
+ return value;
87
+ default:
88
+ return ToyPadPanel.All;
89
+ }
90
+ }
91
+ export function normalizeAction(value) {
92
+ return value === ActionType.Remove ? ActionType.Remove : ActionType.Add;
93
+ }
94
+ function decodeActionEvent(data) {
95
+ if (data.length < 14) {
96
+ return undefined;
97
+ }
98
+ const command = data[1];
99
+ if (command !== CommandType.Action) {
100
+ return undefined;
101
+ }
102
+ const panel = normalizePanel(data[2]);
103
+ const tagType = data[3] & 0xff;
104
+ const index = data[4] & 0xff;
105
+ const action = normalizeAction(data[5]);
106
+ const signatureBytes = data.subarray(6, 13);
107
+ return {
108
+ kind: "event",
109
+ panel,
110
+ action,
111
+ index,
112
+ tagType,
113
+ signature: formatSignature(signatureBytes),
114
+ raw: Buffer.from(signatureBytes)
115
+ };
116
+ }
117
+ function decodeResponse(data) {
118
+ if (data.length < 3) {
119
+ return undefined;
120
+ }
121
+ const length = data[1];
122
+ const payloadStart = 2;
123
+ const payloadEnd = Math.min(data.length - 1, payloadStart + length);
124
+ if (payloadEnd <= payloadStart) {
125
+ return undefined;
126
+ }
127
+ const payloadWithRequestId = data.subarray(payloadStart, payloadEnd);
128
+ if (!payloadWithRequestId.length) {
129
+ return undefined;
130
+ }
131
+ const [requestId] = payloadWithRequestId;
132
+ const payload = payloadWithRequestId.subarray(1);
133
+ return {
134
+ kind: "response",
135
+ requestId,
136
+ payload
137
+ };
138
+ }
139
+ function appendChecksum(values) {
140
+ let checksum = 0;
141
+ for (const value of values) {
142
+ checksum = (checksum + (value & 0xff)) & 0xff;
143
+ }
144
+ return [...values, checksum];
145
+ }
146
+ function padMessage(values) {
147
+ if (values.length > PACKET_LENGTH) {
148
+ return Buffer.from(values.slice(0, PACKET_LENGTH));
149
+ }
150
+ const padded = values.slice();
151
+ while (padded.length < PACKET_LENGTH) {
152
+ padded.push(0x00);
153
+ }
154
+ return Buffer.from(padded);
155
+ }
156
+ function normalizePacket(data) {
157
+ if (!data.length) {
158
+ return data;
159
+ }
160
+ if (data[0] === 0x00 && data.length > 1) {
161
+ return data.subarray(1);
162
+ }
163
+ return data;
164
+ }
165
+ function formatSignature(buffer) {
166
+ return Array.from(buffer)
167
+ .map((value) => value.toString(16).padStart(2, "0"))
168
+ .join(" ");
169
+ }
170
+ function normalizeColor(color) {
171
+ const clamped = color & 0xffffff;
172
+ return {
173
+ red: (clamped >> 16) & 0xff,
174
+ green: (clamped >> 8) & 0xff,
175
+ blue: clamped & 0xff
176
+ };
177
+ }
package/dist/tag.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { CharacterId, VehicleId } from "./ids.js";
2
+ export declare enum TagType {
3
+ Unknown = "unknown",
4
+ Character = "character",
5
+ Vehicle = "vehicle"
6
+ }
7
+ export declare function isVehicle(data: Buffer | Uint8Array): boolean;
8
+ export declare function getVehicleId(data: Buffer | Uint8Array): VehicleId;
9
+ export declare function getCharacterId(uid: Buffer | Uint8Array, encrypted: Buffer | Uint8Array): CharacterId;
10
+ export declare function detectTagType(block26: Buffer | Uint8Array): TagType;
package/dist/tag.js ADDED
@@ -0,0 +1,96 @@
1
+ import { CharacterId } from "./ids.js";
2
+ const VEHICLE_MARKER = Buffer.from([0x00, 0x01, 0x00, 0x00]);
3
+ const TEA_DELTA = 0x9e3779b9;
4
+ const TEA_SUM_INIT = 0xc6ef3720;
5
+ export var TagType;
6
+ (function (TagType) {
7
+ TagType["Unknown"] = "unknown";
8
+ TagType["Character"] = "character";
9
+ TagType["Vehicle"] = "vehicle";
10
+ })(TagType || (TagType = {}));
11
+ export function isVehicle(data) {
12
+ var _a;
13
+ if (data.length < VEHICLE_MARKER.length) {
14
+ return false;
15
+ }
16
+ for (let i = 0; i < VEHICLE_MARKER.length; i++) {
17
+ if (((_a = data[i]) !== null && _a !== void 0 ? _a : 0) !== VEHICLE_MARKER[i]) {
18
+ return false;
19
+ }
20
+ }
21
+ return true;
22
+ }
23
+ export function getVehicleId(data) {
24
+ if (data.length < 2) {
25
+ throw new Error("Vehicle data must contain at least 2 bytes.");
26
+ }
27
+ return (((data[1] & 0xff) << 8) | (data[0] & 0xff));
28
+ }
29
+ export function getCharacterId(uid, encrypted) {
30
+ if (uid.length !== 7) {
31
+ throw new Error("UID must be exactly 7 bytes long.");
32
+ }
33
+ if (encrypted.length < 8) {
34
+ throw new Error("Encrypted character data must be at least 8 bytes.");
35
+ }
36
+ const key = generateKeys(uid);
37
+ const values = [readUInt32LE(encrypted, 0), readUInt32LE(encrypted, 4)];
38
+ const [v0, v1] = teaDecrypt(values, key);
39
+ if (v0 !== v1) {
40
+ return CharacterId.Unknown;
41
+ }
42
+ return (v0 & 0xffff);
43
+ }
44
+ export function detectTagType(block26) {
45
+ if (isVehicle(block26)) {
46
+ return TagType.Vehicle;
47
+ }
48
+ return TagType.Character;
49
+ }
50
+ function readUInt32LE(buffer, offset) {
51
+ return ((buffer[offset + 3] << 24) |
52
+ (buffer[offset + 2] << 16) |
53
+ (buffer[offset + 1] << 8) |
54
+ buffer[offset]) >>> 0;
55
+ }
56
+ function rotateRight(value, count) {
57
+ const normalized = count & 31;
58
+ return ((value >>> normalized) | (value << (32 - normalized))) >>> 0;
59
+ }
60
+ function scramble(uid, count) {
61
+ const base = Buffer.from([
62
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb7, 0xd5, 0xd7, 0xe6, 0xe7,
63
+ 0xba, 0x3c, 0xa8, 0xd8, 0x75, 0x47, 0x68, 0xcf, 0x23, 0xe9, 0xfe, 0xaa
64
+ ]);
65
+ Buffer.from(uid).copy(base, 0, 0, Math.min(uid.length, 7));
66
+ base[count * 4 - 1] = 0xaa;
67
+ let v2 = 0;
68
+ for (let i = 0; i < count; i++) {
69
+ const b = base.readUInt32LE(i * 4);
70
+ v2 = (b + rotateRight(v2, 25) + rotateRight(v2, 10) - v2) >>> 0;
71
+ }
72
+ return v2 >>> 0;
73
+ }
74
+ function generateKeys(uid) {
75
+ return [
76
+ scramble(uid, 3),
77
+ scramble(uid, 4),
78
+ scramble(uid, 5),
79
+ scramble(uid, 6)
80
+ ];
81
+ }
82
+ function teaDecrypt(values, key) {
83
+ let v0 = values[0] >>> 0;
84
+ let v1 = values[1] >>> 0;
85
+ let sum = TEA_SUM_INIT >>> 0;
86
+ const k0 = key[0] >>> 0;
87
+ const k1 = key[1] >>> 0;
88
+ const k2 = key[2] >>> 0;
89
+ const k3 = key[3] >>> 0;
90
+ for (let i = 0; i < 32; i++) {
91
+ v1 = (v1 - ((((v0 << 4) >>> 0) + k2) ^ (v0 + sum) ^ (((v0 >>> 5) + k3) >>> 0))) >>> 0;
92
+ v0 = (v0 - ((((v1 << 4) >>> 0) + k0) ^ (v1 + sum) ^ (((v1 >>> 5) + k1) >>> 0))) >>> 0;
93
+ sum = (sum - TEA_DELTA) >>> 0;
94
+ }
95
+ return [v0 >>> 0, v1 >>> 0];
96
+ }
@@ -0,0 +1,81 @@
1
+ import { EventEmitter } from "events";
2
+ import { ToyPadPanel } from "./constants.js";
3
+ import { FlashOptions, ToyPadTagEvent } from "./protocol.js";
4
+ import { TagType } from "./tag.js";
5
+ import { CharacterId, VehicleId, UpgradeId } from "./ids.js";
6
+ import { getCharacterById, getVehicleById, listCharacters, listVehicles, getUpgradeLabel, listUpgradeLabels } from "./metadata.js";
7
+ import { UpgradeOverrides, UpgradeValue } from "./upgrades.js";
8
+ export interface ToyPadEvents {
9
+ connect: () => void;
10
+ disconnect: () => void;
11
+ error: (error: Error) => void;
12
+ event: (event: ToyPadTagEvent) => void;
13
+ add: (event: ToyPadTagEvent) => void;
14
+ remove: (event: ToyPadTagEvent) => void;
15
+ }
16
+ type BasicTagInfo = {
17
+ id: CharacterId;
18
+ type: TagType.Character;
19
+ } | {
20
+ id: VehicleId;
21
+ type: TagType.Vehicle;
22
+ };
23
+ export type ToyPadTagInfo = BasicTagInfo & {
24
+ signature: string;
25
+ };
26
+ export interface WriteVehicleOptions {
27
+ signature?: string;
28
+ step?: number;
29
+ }
30
+ export interface WriteVehicleUpgradesOptions {
31
+ signature?: string;
32
+ upgrades: UpgradeOverrides;
33
+ }
34
+ export interface ReadVehicleUpgradesOptions {
35
+ signature?: string;
36
+ }
37
+ export declare class ToyPad extends EventEmitter {
38
+ static Panel: typeof ToyPadPanel;
39
+ static metadata: {
40
+ getCharacterById: typeof getCharacterById;
41
+ getVehicleById: typeof getVehicleById;
42
+ listCharacters: typeof listCharacters;
43
+ listVehicles: typeof listVehicles;
44
+ getUpgradeLabel: typeof getUpgradeLabel;
45
+ listUpgradeLabels: typeof listUpgradeLabels;
46
+ listVehicleUpgrades(vehicleId: VehicleId): UpgradeId[];
47
+ };
48
+ private connection?;
49
+ private readonly activeTags;
50
+ connect(): Promise<void>;
51
+ disconnect(): void;
52
+ setColor(panel: ToyPadPanel, color: number): Promise<void>;
53
+ getColor(panel: ToyPadPanel): Promise<number>;
54
+ fade(panel: ToyPadPanel, speed: number, cycles: number, color: number): Promise<void>;
55
+ flash(panel: ToyPadPanel, color: number, count: number, options?: FlashOptions): Promise<void>;
56
+ readTag(panel: ToyPadPanel, signature?: string): Promise<ToyPadTagInfo>;
57
+ writeVehicle(panel: ToyPadPanel, vehicleId: VehicleId, options?: WriteVehicleOptions): Promise<void>;
58
+ writeVehicleUpgrades(panel: ToyPadPanel, options: WriteVehicleUpgradesOptions): Promise<void>;
59
+ readVehicleUpgrades(panel: ToyPadPanel, options?: ReadVehicleUpgradesOptions): Promise<UpgradeValue[]>;
60
+ private ensureConnection;
61
+ private forwardEvent;
62
+ private normalizeSignature;
63
+ private resolveActiveTag;
64
+ private readTagData;
65
+ private resolveVehicleVariant;
66
+ private createVehiclePayload;
67
+ private writeUpgradePayload;
68
+ private readUpgradePayload;
69
+ private readUpgradeBlock;
70
+ private extractUpgradePayload;
71
+ private delay;
72
+ private readBlock;
73
+ private resolveUpgradeFlag;
74
+ }
75
+ export interface ToyPad {
76
+ on<U extends keyof ToyPadEvents>(event: U, listener: ToyPadEvents[U]): this;
77
+ once<U extends keyof ToyPadEvents>(event: U, listener: ToyPadEvents[U]): this;
78
+ off<U extends keyof ToyPadEvents>(event: U, listener: ToyPadEvents[U]): this;
79
+ emit<U extends keyof ToyPadEvents>(event: U, ...args: Parameters<ToyPadEvents[U]>): boolean;
80
+ }
81
+ export type { ToyPadTagEvent };