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.
package/dist/toypad.js ADDED
@@ -0,0 +1,318 @@
1
+ import debug from "debug";
2
+ import { EventEmitter } from "events";
3
+ import { ToyPadConnection } from "./connection.js";
4
+ import { ActionType, ToyPadPanel } from "./constants.js";
5
+ import { createFadeCommand, createFlashCommand, createGetColorCommand, createReadTagCommand, createSetColorCommand, createWriteTagCommand, decodeColor } from "./protocol.js";
6
+ import { TagType, detectTagType, getCharacterId, getVehicleId } from "./tag.js";
7
+ import { CharacterId } from "./ids.js";
8
+ import { getCharacterById, getVehicleById, getVehicleVariant, listCharacters, listVehicles, getUpgradeLabel, listUpgradeLabels, getVehicleMap } from "./metadata.js";
9
+ import { decodeUpgradePayload, encodePresetPayload, encodeUpgradePayload, listUpgradeSlots } from "./upgrades.js";
10
+ const metadataLog = debug("node-toypad:metadata");
11
+ const UPGRADE_READ_RETRY_COUNT = 3;
12
+ const UPGRADE_READ_RETRY_DELAY_MS = 25;
13
+ export class ToyPad extends EventEmitter {
14
+ constructor() {
15
+ super(...arguments);
16
+ this.activeTags = new Map();
17
+ }
18
+ async connect() {
19
+ if (this.connection) {
20
+ return;
21
+ }
22
+ const connection = new ToyPadConnection();
23
+ this.connection = connection;
24
+ connection.on("event", (event) => this.forwardEvent(event));
25
+ connection.on("connect", () => this.emit("connect"));
26
+ connection.on("error", (error) => this.emit("error", error));
27
+ await connection.open();
28
+ }
29
+ disconnect() {
30
+ if (!this.connection) {
31
+ return;
32
+ }
33
+ this.connection.close();
34
+ this.connection.removeAllListeners();
35
+ this.connection = undefined;
36
+ this.activeTags.clear();
37
+ this.emit("disconnect");
38
+ }
39
+ async setColor(panel, color) {
40
+ const connection = this.ensureConnection();
41
+ await connection.request(createSetColorCommand(panel, color));
42
+ }
43
+ async getColor(panel) {
44
+ const connection = this.ensureConnection();
45
+ const response = await connection.request(createGetColorCommand(panel));
46
+ return decodeColor(response);
47
+ }
48
+ async fade(panel, speed, cycles, color) {
49
+ const connection = this.ensureConnection();
50
+ await connection.request(createFadeCommand(panel, speed, cycles, color));
51
+ }
52
+ async flash(panel, color, count, options) {
53
+ const connection = this.ensureConnection();
54
+ await connection.request(createFlashCommand(panel, color, count, options));
55
+ }
56
+ async readTag(panel, signature) {
57
+ const tag = this.resolveActiveTag(panel, signature);
58
+ const info = await this.readTagData(tag);
59
+ return { ...info, signature: tag.signature };
60
+ }
61
+ async writeVehicle(panel, vehicleId, options = {}) {
62
+ var _a, _b, _c;
63
+ const { signature, step } = options;
64
+ const tag = this.resolveActiveTag(panel, signature);
65
+ const info = await this.readTagData(tag);
66
+ if (info.type !== TagType.Vehicle) {
67
+ throw new Error(`Tag on panel ${panel} is not a vehicle tag.`);
68
+ }
69
+ const variant = this.resolveVehicleVariant(vehicleId, step);
70
+ const mapId = getVehicleMap(variant.id);
71
+ const presetPayload = encodePresetPayload(mapId, (_a = variant.step) !== null && _a !== void 0 ? _a : 0);
72
+ if (!presetPayload) {
73
+ throw new Error(`Vehicle preset data is missing for map ${mapId} step ${(_b = variant.step) !== null && _b !== void 0 ? _b : 0}.`);
74
+ }
75
+ await this.writeUpgradePayload(tag, presetPayload);
76
+ const payload = this.createVehiclePayload(variant.id, (_c = variant.step) !== null && _c !== void 0 ? _c : 0);
77
+ const connection = this.ensureConnection();
78
+ await connection.request(createWriteTagCommand(tag.index, 0x24, payload));
79
+ }
80
+ async writeVehicleUpgrades(panel, options) {
81
+ const { signature, upgrades } = options;
82
+ if (!upgrades || !upgrades.length) {
83
+ throw new Error("No upgrades specified.");
84
+ }
85
+ const tag = this.resolveActiveTag(panel, signature);
86
+ const info = await this.readTagData(tag);
87
+ if (info.type !== TagType.Vehicle) {
88
+ throw new Error(`Tag on panel ${panel} is not a vehicle tag.`);
89
+ }
90
+ const vehicle = getVehicleById(info.id);
91
+ if (!vehicle) {
92
+ throw new Error(`Vehicle metadata for id ${info.id} is not available.`);
93
+ }
94
+ const mapId = getVehicleMap(vehicle.id);
95
+ if (mapId === 0) {
96
+ throw new Error("Cannot write upgrades to an empty vehicle tag. Program a vehicle first.");
97
+ }
98
+ const upgradeBlock = await this.readUpgradeBlock(tag);
99
+ const currentPayload = this.extractUpgradePayload(upgradeBlock);
100
+ const payload = encodeUpgradePayload(mapId, upgrades, currentPayload);
101
+ await this.writeUpgradePayload(tag, payload, upgradeBlock);
102
+ }
103
+ async readVehicleUpgrades(panel, options = {}) {
104
+ const { signature } = options;
105
+ const tag = this.resolveActiveTag(panel, signature);
106
+ const info = await this.readTagData(tag);
107
+ if (info.type !== TagType.Vehicle) {
108
+ throw new Error(`Tag on panel ${panel} is not a vehicle tag.`);
109
+ }
110
+ const vehicle = getVehicleById(info.id);
111
+ if (!vehicle) {
112
+ throw new Error(`Vehicle metadata for id ${info.id} is not available.`);
113
+ }
114
+ const mapId = getVehicleMap(vehicle.id);
115
+ if (mapId === 0) {
116
+ return [];
117
+ }
118
+ const payload = await this.readUpgradePayload(tag);
119
+ const states = decodeUpgradePayload(mapId, payload);
120
+ return states.map((slot) => ({
121
+ id: slot.id,
122
+ value: slot.max <= 1 ? slot.value > 0 : slot.value
123
+ }));
124
+ }
125
+ ensureConnection() {
126
+ if (!this.connection) {
127
+ throw new Error("ToyPad is not connected");
128
+ }
129
+ return this.connection;
130
+ }
131
+ forwardEvent(event) {
132
+ var _a;
133
+ this.emit("event", event);
134
+ if (event.action === ActionType.Add) {
135
+ if (event.panel !== ToyPadPanel.All) {
136
+ const tagKey = this.normalizeSignature(event.signature);
137
+ const panelTags = (_a = this.activeTags.get(event.panel)) !== null && _a !== void 0 ? _a : new Map();
138
+ const existing = panelTags.get(tagKey);
139
+ panelTags.set(tagKey, { index: event.index, uid: event.raw, signature: event.signature });
140
+ this.activeTags.set(event.panel, panelTags);
141
+ if (existing && existing.index === event.index) {
142
+ return;
143
+ }
144
+ }
145
+ this.emit("add", event);
146
+ }
147
+ else {
148
+ if (event.panel !== ToyPadPanel.All) {
149
+ const tagKey = this.normalizeSignature(event.signature);
150
+ const panelTags = this.activeTags.get(event.panel);
151
+ if (panelTags) {
152
+ panelTags.delete(tagKey);
153
+ if (!panelTags.size) {
154
+ this.activeTags.delete(event.panel);
155
+ }
156
+ }
157
+ }
158
+ this.emit("remove", event);
159
+ }
160
+ }
161
+ normalizeSignature(signature) {
162
+ return signature.trim().toLowerCase();
163
+ }
164
+ resolveActiveTag(panel, signature) {
165
+ const panelTags = this.activeTags.get(panel);
166
+ if (!panelTags || panelTags.size === 0) {
167
+ throw new Error(`No tag present on panel ${panel}.`);
168
+ }
169
+ if (signature) {
170
+ const tag = panelTags.get(this.normalizeSignature(signature));
171
+ if (!tag) {
172
+ throw new Error(`No tag with signature ${signature} on panel ${panel}.`);
173
+ }
174
+ return tag;
175
+ }
176
+ if (panelTags.size === 1) {
177
+ const iterator = panelTags.values().next();
178
+ if (!iterator.done && iterator.value) {
179
+ return iterator.value;
180
+ }
181
+ throw new Error("Unable to determine which tag to read.");
182
+ }
183
+ const available = Array.from(panelTags.values())
184
+ .map((value) => value.signature)
185
+ .join(", ");
186
+ throw new Error(`Multiple tags present on panel ${panel}. Specify signature (${available}).`);
187
+ }
188
+ async readTagData(tag) {
189
+ const connection = this.ensureConnection();
190
+ const cardData = await this.readBlock(tag, 0x24, connection);
191
+ const payloadView = cardData.subarray(8, 12);
192
+ const type = detectTagType(payloadView);
193
+ if (type === TagType.Vehicle) {
194
+ const id = getVehicleId(cardData);
195
+ return { id, type };
196
+ }
197
+ if (type === TagType.Character) {
198
+ const id = getCharacterId(tag.uid, cardData.subarray(0, 8));
199
+ if (id === CharacterId.Unknown) {
200
+ throw new Error("Unable to decrypt character id from tag.");
201
+ }
202
+ return { id, type };
203
+ }
204
+ throw new Error("Unsupported tag type detected.");
205
+ }
206
+ resolveVehicleVariant(vehicleId, step) {
207
+ const variant = getVehicleVariant(vehicleId, step);
208
+ if (!variant) {
209
+ if (typeof step === "number") {
210
+ throw new Error(`Vehicle ${vehicleId} does not have rebuild step ${step}.`);
211
+ }
212
+ throw new Error(`Unknown vehicle id ${vehicleId}.`);
213
+ }
214
+ return variant;
215
+ }
216
+ createVehiclePayload(vehicleId, step) {
217
+ const payload = Buffer.alloc(16, 0);
218
+ payload.writeUInt32LE(vehicleId, 0);
219
+ payload[9] = 0x01;
220
+ payload[12] = this.resolveUpgradeFlag(step);
221
+ return payload;
222
+ }
223
+ async writeUpgradePayload(tag, payload, baseBlock) {
224
+ if (payload.length !== 8) {
225
+ throw new Error(`Upgrade payload must be 8 bytes, received ${payload.length}.`);
226
+ }
227
+ const connection = this.ensureConnection();
228
+ const block = baseBlock ? Buffer.from(baseBlock) : await this.readUpgradeBlock(tag, connection);
229
+ payload.subarray(0, 4).copy(block, 0);
230
+ payload.subarray(4, 8).copy(block, 8);
231
+ await connection.request(createWriteTagCommand(tag.index, 0x23, block));
232
+ }
233
+ async readUpgradePayload(tag) {
234
+ const block = await this.readUpgradeBlock(tag);
235
+ return this.extractUpgradePayload(block);
236
+ }
237
+ async readUpgradeBlock(tag, connection) {
238
+ const activeConnection = connection !== null && connection !== void 0 ? connection : this.ensureConnection();
239
+ let attempt = 0;
240
+ let lastError;
241
+ while (attempt < UPGRADE_READ_RETRY_COUNT) {
242
+ try {
243
+ const block = await this.readBlock(tag, 0x23, activeConnection);
244
+ if (block.length < 12) {
245
+ throw new Error("ToyPad returned an invalid upgrade block.");
246
+ }
247
+ return block;
248
+ }
249
+ catch (error) {
250
+ lastError = error;
251
+ attempt++;
252
+ if (attempt >= UPGRADE_READ_RETRY_COUNT) {
253
+ throw error instanceof Error ? error : new Error(String(error));
254
+ }
255
+ await this.delay(UPGRADE_READ_RETRY_DELAY_MS * attempt);
256
+ }
257
+ }
258
+ throw lastError instanceof Error ? lastError : new Error("Unable to read upgrade block.");
259
+ }
260
+ extractUpgradePayload(block) {
261
+ const payload = Buffer.alloc(8);
262
+ block.subarray(0, 4).copy(payload, 0);
263
+ block.subarray(8, 12).copy(payload, 4);
264
+ return payload;
265
+ }
266
+ async delay(ms) {
267
+ await new Promise((resolve) => setTimeout(resolve, ms));
268
+ }
269
+ async readBlock(tag, page, connection) {
270
+ const activeConnection = connection !== null && connection !== void 0 ? connection : this.ensureConnection();
271
+ const payload = await activeConnection.request(createReadTagCommand(tag.index, page));
272
+ if (payload.length < 17) {
273
+ throw new Error("ToyPad returned an invalid read response.");
274
+ }
275
+ const errorCode = payload[0];
276
+ if (errorCode !== 0) {
277
+ throw new Error(`ToyPad read failed with error code 0x${errorCode.toString(16).padStart(2, "0")}.`);
278
+ }
279
+ const cardData = payload.subarray(1);
280
+ if (cardData.length < 16) {
281
+ throw new Error("ToyPad read response did not include enough data.");
282
+ }
283
+ return cardData.subarray(0, 16);
284
+ }
285
+ resolveUpgradeFlag(step) {
286
+ switch (step) {
287
+ case 1:
288
+ return 0x04;
289
+ case 2:
290
+ return 0x08;
291
+ default:
292
+ return 0x00;
293
+ }
294
+ }
295
+ }
296
+ ToyPad.Panel = ToyPadPanel;
297
+ ToyPad.metadata = {
298
+ getCharacterById,
299
+ getVehicleById,
300
+ listCharacters,
301
+ listVehicles,
302
+ getUpgradeLabel,
303
+ listUpgradeLabels,
304
+ listVehicleUpgrades(vehicleId) {
305
+ const vehicle = getVehicleById(vehicleId);
306
+ const mapId = vehicle ? getVehicleMap(vehicle.id) : 0;
307
+ if (!vehicle || mapId === 0) {
308
+ return [];
309
+ }
310
+ try {
311
+ return listUpgradeSlots(mapId).map((slot) => slot.id);
312
+ }
313
+ catch (error) {
314
+ metadataLog(`Unable to list upgrades for vehicle ${vehicleId}: ${error instanceof Error ? error.message : error}`);
315
+ return [];
316
+ }
317
+ }
318
+ };
@@ -0,0 +1,2 @@
1
+ export declare const UPGRADE_DIGITS: readonly [readonly [], readonly [5, 9, 2, 9, 9, 2, 9, 9, 2, 4, 2, 4, 9, 5, 2, 2, 2, 2, 2, 2, 2, 8, 9, 2, 0], readonly [5, 9, 2, 9, 9, 2, 9, 9, 2, 4, 2, 4, 9, 5, 2, 2, 2, 2, 2, 2, 2, 7, 9, 2, 0], readonly [5, 9, 2, 9, 9, 2, 9, 9, 2, 4, 2, 4, 9, 5, 2, 2, 2, 2, 2, 2, 2, 8, 2, 8, 2], readonly [5, 9, 2, 9, 9, 2, 9, 2, 4, 2, 4, 9, 5, 2, 2, 2, 2, 2, 2, 2, 7, 9, 2, 0, 0], readonly [5, 9, 2, 9, 9, 2, 9, 9, 2, 4, 2, 4, 9, 5, 2, 2, 2, 2, 2, 2, 2, 6, 9, 2, 0], readonly [4, 9, 9, 3, 5, 2, 9, 2, 9, 2, 2, 2, 7, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], readonly [9, 9, 1, 5, 2, 9, 2, 9, 2, 2, 2, 7, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]];
2
+ export declare function getUpgradeDigits(mapId: number): readonly number[];
@@ -0,0 +1,199 @@
1
+ export const UPGRADE_DIGITS = [
2
+ [],
3
+ [
4
+ 5,
5
+ 9,
6
+ 2,
7
+ 9,
8
+ 9,
9
+ 2,
10
+ 9,
11
+ 9,
12
+ 2,
13
+ 4,
14
+ 2,
15
+ 4,
16
+ 9,
17
+ 5,
18
+ 2,
19
+ 2,
20
+ 2,
21
+ 2,
22
+ 2,
23
+ 2,
24
+ 2,
25
+ 8,
26
+ 9,
27
+ 2,
28
+ 0
29
+ ],
30
+ [
31
+ 5,
32
+ 9,
33
+ 2,
34
+ 9,
35
+ 9,
36
+ 2,
37
+ 9,
38
+ 9,
39
+ 2,
40
+ 4,
41
+ 2,
42
+ 4,
43
+ 9,
44
+ 5,
45
+ 2,
46
+ 2,
47
+ 2,
48
+ 2,
49
+ 2,
50
+ 2,
51
+ 2,
52
+ 7,
53
+ 9,
54
+ 2,
55
+ 0
56
+ ],
57
+ [
58
+ 5,
59
+ 9,
60
+ 2,
61
+ 9,
62
+ 9,
63
+ 2,
64
+ 9,
65
+ 9,
66
+ 2,
67
+ 4,
68
+ 2,
69
+ 4,
70
+ 9,
71
+ 5,
72
+ 2,
73
+ 2,
74
+ 2,
75
+ 2,
76
+ 2,
77
+ 2,
78
+ 2,
79
+ 8,
80
+ 2,
81
+ 8,
82
+ 2
83
+ ],
84
+ [
85
+ 5,
86
+ 9,
87
+ 2,
88
+ 9,
89
+ 9,
90
+ 2,
91
+ 9,
92
+ 2,
93
+ 4,
94
+ 2,
95
+ 4,
96
+ 9,
97
+ 5,
98
+ 2,
99
+ 2,
100
+ 2,
101
+ 2,
102
+ 2,
103
+ 2,
104
+ 2,
105
+ 7,
106
+ 9,
107
+ 2,
108
+ 0,
109
+ 0
110
+ ],
111
+ [
112
+ 5,
113
+ 9,
114
+ 2,
115
+ 9,
116
+ 9,
117
+ 2,
118
+ 9,
119
+ 9,
120
+ 2,
121
+ 4,
122
+ 2,
123
+ 4,
124
+ 9,
125
+ 5,
126
+ 2,
127
+ 2,
128
+ 2,
129
+ 2,
130
+ 2,
131
+ 2,
132
+ 2,
133
+ 6,
134
+ 9,
135
+ 2,
136
+ 0
137
+ ],
138
+ [
139
+ 4,
140
+ 9,
141
+ 9,
142
+ 3,
143
+ 5,
144
+ 2,
145
+ 9,
146
+ 2,
147
+ 9,
148
+ 2,
149
+ 2,
150
+ 2,
151
+ 7,
152
+ 2,
153
+ 0,
154
+ 0,
155
+ 0,
156
+ 0,
157
+ 0,
158
+ 0,
159
+ 0,
160
+ 0,
161
+ 0,
162
+ 0,
163
+ 0
164
+ ],
165
+ [
166
+ 9,
167
+ 9,
168
+ 1,
169
+ 5,
170
+ 2,
171
+ 9,
172
+ 2,
173
+ 9,
174
+ 2,
175
+ 2,
176
+ 2,
177
+ 7,
178
+ 2,
179
+ 0,
180
+ 0,
181
+ 0,
182
+ 0,
183
+ 0,
184
+ 0,
185
+ 0,
186
+ 0,
187
+ 0,
188
+ 0,
189
+ 0,
190
+ 0
191
+ ]
192
+ ];
193
+ export function getUpgradeDigits(mapId) {
194
+ const digits = UPGRADE_DIGITS[mapId];
195
+ if (!digits) {
196
+ throw new Error('Unknown upgrade map ' + mapId);
197
+ }
198
+ return digits;
199
+ }