@opendisplay/opendisplay 1.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/index.js ADDED
@@ -0,0 +1,1936 @@
1
+ // src/device.ts
2
+ import { ColorScheme as ColorScheme4, DitherMode as DitherMode2 } from "@opendisplay/epaper-dithering";
3
+
4
+ // src/encoding/images.ts
5
+ import {
6
+ ColorScheme as ColorScheme2,
7
+ DitherMode,
8
+ ditherImage
9
+ } from "@opendisplay/epaper-dithering";
10
+
11
+ // src/exceptions.ts
12
+ var OpenDisplayError = class extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "OpenDisplayError";
16
+ }
17
+ };
18
+ var BLEConnectionError = class extends OpenDisplayError {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = "BLEConnectionError";
22
+ }
23
+ };
24
+ var BLETimeoutError = class extends OpenDisplayError {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = "BLETimeoutError";
28
+ }
29
+ };
30
+ var ProtocolError = class extends OpenDisplayError {
31
+ constructor(message) {
32
+ super(message);
33
+ this.name = "ProtocolError";
34
+ }
35
+ };
36
+ var ConfigParseError = class extends ProtocolError {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = "ConfigParseError";
40
+ }
41
+ };
42
+ var InvalidResponseError = class extends ProtocolError {
43
+ constructor(message) {
44
+ super(message);
45
+ this.name = "InvalidResponseError";
46
+ }
47
+ };
48
+ var ImageEncodingError = class extends OpenDisplayError {
49
+ constructor(message) {
50
+ super(message);
51
+ this.name = "ImageEncodingError";
52
+ }
53
+ };
54
+
55
+ // src/encoding/bitplanes.ts
56
+ import { ColorScheme } from "@opendisplay/epaper-dithering";
57
+ function encodeBitplanes(paletteImage, colorScheme) {
58
+ if (colorScheme !== ColorScheme.BWR && colorScheme !== ColorScheme.BWY) {
59
+ throw new ImageEncodingError(
60
+ `Bitplane encoding only supports BWR/BWY, got ${ColorScheme[colorScheme]}`
61
+ );
62
+ }
63
+ const { width, height, indices: pixels } = paletteImage;
64
+ const bytesPerRow = Math.ceil(width / 8);
65
+ const plane1 = new Uint8Array(bytesPerRow * height);
66
+ const plane2 = new Uint8Array(bytesPerRow * height);
67
+ for (let y = 0; y < height; y++) {
68
+ for (let x = 0; x < width; x++) {
69
+ const byteIdx = y * bytesPerRow + Math.floor(x / 8);
70
+ const bitIdx = 7 - x % 8;
71
+ const paletteIdx = pixels[y * width + x];
72
+ if (paletteIdx === 1) {
73
+ plane1[byteIdx] |= 1 << bitIdx;
74
+ } else if (paletteIdx === 2) {
75
+ plane2[byteIdx] |= 1 << bitIdx;
76
+ }
77
+ }
78
+ }
79
+ return [plane1, plane2];
80
+ }
81
+
82
+ // src/encoding/images.ts
83
+ function encodeImage(paletteImage, colorScheme) {
84
+ switch (colorScheme) {
85
+ case ColorScheme2.MONO:
86
+ return encode1bpp(paletteImage);
87
+ case ColorScheme2.BWR:
88
+ case ColorScheme2.BWY:
89
+ throw new ImageEncodingError(
90
+ `Color scheme ${ColorScheme2[colorScheme]} requires bitplane encoding, use encodeBitplanes() instead`
91
+ );
92
+ case ColorScheme2.BWRY:
93
+ return encode2bpp(paletteImage);
94
+ case ColorScheme2.BWGBRY:
95
+ return encode4bpp(paletteImage, true);
96
+ case ColorScheme2.GRAYSCALE_4:
97
+ return encode2bpp(paletteImage);
98
+ default:
99
+ throw new ImageEncodingError(
100
+ `Unsupported color scheme: ${ColorScheme2[colorScheme]}`
101
+ );
102
+ }
103
+ }
104
+ function encode1bpp(paletteImage) {
105
+ const { width, height, indices: pixels } = paletteImage;
106
+ const bytesPerRow = Math.ceil(width / 8);
107
+ const output = new Uint8Array(bytesPerRow * height);
108
+ for (let y = 0; y < height; y++) {
109
+ for (let x = 0; x < width; x++) {
110
+ const byteIdx = y * bytesPerRow + Math.floor(x / 8);
111
+ const bitIdx = 7 - x % 8;
112
+ if (pixels[y * width + x] > 0) {
113
+ output[byteIdx] |= 1 << bitIdx;
114
+ }
115
+ }
116
+ }
117
+ return output;
118
+ }
119
+ function encode2bpp(paletteImage) {
120
+ const { width, height, indices: pixels } = paletteImage;
121
+ const bytesPerRow = Math.ceil(width / 4);
122
+ const output = new Uint8Array(bytesPerRow * height);
123
+ for (let y = 0; y < height; y++) {
124
+ for (let x = 0; x < width; x++) {
125
+ const byteIdx = y * bytesPerRow + Math.floor(x / 4);
126
+ const pixelInByte = x % 4;
127
+ const bitShift = (3 - pixelInByte) * 2;
128
+ const paletteIdx = pixels[y * width + x] & 3;
129
+ output[byteIdx] |= paletteIdx << bitShift;
130
+ }
131
+ }
132
+ return output;
133
+ }
134
+ function encode4bpp(paletteImage, bwgbryMapping = false) {
135
+ const { width, height, indices: pixels } = paletteImage;
136
+ const BWGBRY_MAP = { 0: 0, 1: 1, 2: 2, 3: 3, 4: 5, 5: 6 };
137
+ const bytesPerRow = Math.ceil(width / 2);
138
+ const output = new Uint8Array(bytesPerRow * height);
139
+ for (let y = 0; y < height; y++) {
140
+ for (let x = 0; x < width; x++) {
141
+ const byteIdx = y * bytesPerRow + Math.floor(x / 2);
142
+ const pixelInByte = x % 2;
143
+ let paletteIdx = pixels[y * width + x] & 15;
144
+ if (bwgbryMapping && paletteIdx in BWGBRY_MAP) {
145
+ paletteIdx = BWGBRY_MAP[paletteIdx];
146
+ }
147
+ const bitShift = (1 - pixelInByte) * 4;
148
+ output[byteIdx] |= paletteIdx << bitShift;
149
+ }
150
+ }
151
+ return output;
152
+ }
153
+ function prepareImageForUpload(imageData, targetWidth, targetHeight, colorScheme, ditherMode = DitherMode.BURKES) {
154
+ let resizedImageData = imageData;
155
+ if (imageData.width !== targetWidth || imageData.height !== targetHeight) {
156
+ console.warn(
157
+ `Resizing image from ${imageData.width}x${imageData.height} to ${targetWidth}x${targetHeight}`
158
+ );
159
+ resizedImageData = resizeImageData(imageData, targetWidth, targetHeight);
160
+ }
161
+ const paletteImage = ditherImage(resizedImageData, colorScheme, ditherMode);
162
+ if (colorScheme === ColorScheme2.BWR || colorScheme === ColorScheme2.BWY) {
163
+ const [plane1, plane2] = encodeBitplanes(paletteImage, colorScheme);
164
+ const result = new Uint8Array(plane1.length + plane2.length);
165
+ result.set(plane1, 0);
166
+ result.set(plane2, plane1.length);
167
+ return result;
168
+ } else {
169
+ return encodeImage(paletteImage, colorScheme);
170
+ }
171
+ }
172
+ function resizeImageData(imageData, targetWidth, targetHeight) {
173
+ const canvas = new OffscreenCanvas(imageData.width, imageData.height);
174
+ const ctx = canvas.getContext("2d");
175
+ if (!ctx) {
176
+ throw new ImageEncodingError("Failed to get canvas context");
177
+ }
178
+ ctx.putImageData(imageData, 0, 0);
179
+ const targetCanvas = new OffscreenCanvas(targetWidth, targetHeight);
180
+ const targetCtx = targetCanvas.getContext("2d");
181
+ if (!targetCtx) {
182
+ throw new ImageEncodingError("Failed to get target canvas context");
183
+ }
184
+ targetCtx.drawImage(canvas, 0, 0, targetWidth, targetHeight);
185
+ return targetCtx.getImageData(0, 0, targetWidth, targetHeight);
186
+ }
187
+
188
+ // src/encoding/compression.ts
189
+ import pako from "pako";
190
+ function compressImageData(data, level = 6) {
191
+ if (level === 0) {
192
+ return data;
193
+ }
194
+ const compressed = pako.deflate(data, { level });
195
+ const ratio = data.length > 0 ? compressed.length / data.length * 100 : 0;
196
+ console.debug(
197
+ `Compressed ${data.length} bytes -> ${compressed.length} bytes (${ratio.toFixed(1)}%)`
198
+ );
199
+ return compressed;
200
+ }
201
+
202
+ // src/models/enums.ts
203
+ var RefreshMode = /* @__PURE__ */ ((RefreshMode2) => {
204
+ RefreshMode2[RefreshMode2["FULL"] = 0] = "FULL";
205
+ RefreshMode2[RefreshMode2["FAST"] = 1] = "FAST";
206
+ RefreshMode2[RefreshMode2["PARTIAL"] = 2] = "PARTIAL";
207
+ RefreshMode2[RefreshMode2["PARTIAL2"] = 3] = "PARTIAL2";
208
+ return RefreshMode2;
209
+ })(RefreshMode || {});
210
+ var ICType = /* @__PURE__ */ ((ICType2) => {
211
+ ICType2[ICType2["NRF52840"] = 1] = "NRF52840";
212
+ ICType2[ICType2["ESP32_S3"] = 2] = "ESP32_S3";
213
+ ICType2[ICType2["ESP32_C3"] = 3] = "ESP32_C3";
214
+ ICType2[ICType2["ESP32_C6"] = 4] = "ESP32_C6";
215
+ return ICType2;
216
+ })(ICType || {});
217
+ var PowerMode = /* @__PURE__ */ ((PowerMode2) => {
218
+ PowerMode2[PowerMode2["BATTERY"] = 1] = "BATTERY";
219
+ PowerMode2[PowerMode2["USB"] = 2] = "USB";
220
+ PowerMode2[PowerMode2["SOLAR"] = 3] = "SOLAR";
221
+ return PowerMode2;
222
+ })(PowerMode || {});
223
+ var BusType = /* @__PURE__ */ ((BusType2) => {
224
+ BusType2[BusType2["I2C"] = 0] = "I2C";
225
+ BusType2[BusType2["SPI"] = 1] = "SPI";
226
+ return BusType2;
227
+ })(BusType || {});
228
+ var Rotation = /* @__PURE__ */ ((Rotation2) => {
229
+ Rotation2[Rotation2["ROTATE_0"] = 0] = "ROTATE_0";
230
+ Rotation2[Rotation2["ROTATE_90"] = 90] = "ROTATE_90";
231
+ Rotation2[Rotation2["ROTATE_180"] = 180] = "ROTATE_180";
232
+ Rotation2[Rotation2["ROTATE_270"] = 270] = "ROTATE_270";
233
+ return Rotation2;
234
+ })(Rotation || {});
235
+
236
+ // src/protocol/constants.ts
237
+ var SERVICE_UUID = "00002446-0000-1000-8000-00805f9b34fb";
238
+ var MANUFACTURER_ID = 9286;
239
+ var RESPONSE_HIGH_BIT_FLAG = 32768;
240
+ var CHUNK_SIZE = 230;
241
+ var CONFIG_CHUNK_SIZE = 200;
242
+ var MAX_COMPRESSED_SIZE = 50 * 1024;
243
+ var MAX_START_PAYLOAD = 200;
244
+ var CommandCode = /* @__PURE__ */ ((CommandCode3) => {
245
+ CommandCode3[CommandCode3["READ_CONFIG"] = 64] = "READ_CONFIG";
246
+ CommandCode3[CommandCode3["WRITE_CONFIG"] = 65] = "WRITE_CONFIG";
247
+ CommandCode3[CommandCode3["WRITE_CONFIG_CHUNK"] = 66] = "WRITE_CONFIG_CHUNK";
248
+ CommandCode3[CommandCode3["READ_FW_VERSION"] = 67] = "READ_FW_VERSION";
249
+ CommandCode3[CommandCode3["REBOOT"] = 15] = "REBOOT";
250
+ CommandCode3[CommandCode3["DIRECT_WRITE_START"] = 112] = "DIRECT_WRITE_START";
251
+ CommandCode3[CommandCode3["DIRECT_WRITE_DATA"] = 113] = "DIRECT_WRITE_DATA";
252
+ CommandCode3[CommandCode3["DIRECT_WRITE_END"] = 114] = "DIRECT_WRITE_END";
253
+ return CommandCode3;
254
+ })(CommandCode || {});
255
+
256
+ // src/protocol/commands.ts
257
+ function buildReadConfigCommand() {
258
+ const buffer = new ArrayBuffer(2);
259
+ const view = new DataView(buffer);
260
+ view.setUint16(0, 64 /* READ_CONFIG */, false);
261
+ return new Uint8Array(buffer);
262
+ }
263
+ function buildReadFwVersionCommand() {
264
+ const buffer = new ArrayBuffer(2);
265
+ const view = new DataView(buffer);
266
+ view.setUint16(0, 67 /* READ_FW_VERSION */, false);
267
+ return new Uint8Array(buffer);
268
+ }
269
+ function buildRebootCommand() {
270
+ const buffer = new ArrayBuffer(2);
271
+ const view = new DataView(buffer);
272
+ view.setUint16(0, 15 /* REBOOT */, false);
273
+ return new Uint8Array(buffer);
274
+ }
275
+ function buildDirectWriteStartCompressed(uncompressedSize, compressedData) {
276
+ const maxDataInStart = MAX_START_PAYLOAD - 6;
277
+ const headerSize = 6;
278
+ const totalSize = compressedData.length <= maxDataInStart ? headerSize + compressedData.length : MAX_START_PAYLOAD;
279
+ const buffer = new ArrayBuffer(totalSize);
280
+ const view = new DataView(buffer);
281
+ view.setUint16(0, 112 /* DIRECT_WRITE_START */, false);
282
+ view.setUint32(2, uncompressedSize, true);
283
+ const startCommand = new Uint8Array(buffer);
284
+ const dataLength = Math.min(compressedData.length, maxDataInStart);
285
+ startCommand.set(compressedData.subarray(0, dataLength), 6);
286
+ const remainingData = compressedData.length <= maxDataInStart ? new Uint8Array(0) : compressedData.subarray(maxDataInStart);
287
+ return [startCommand, remainingData];
288
+ }
289
+ function buildDirectWriteStartUncompressed() {
290
+ const buffer = new ArrayBuffer(2);
291
+ const view = new DataView(buffer);
292
+ view.setUint16(0, 112 /* DIRECT_WRITE_START */, false);
293
+ return new Uint8Array(buffer);
294
+ }
295
+ function buildDirectWriteDataCommand(chunkData) {
296
+ if (chunkData.length > CHUNK_SIZE) {
297
+ throw new Error(
298
+ `Chunk size ${chunkData.length} exceeds maximum ${CHUNK_SIZE}`
299
+ );
300
+ }
301
+ const buffer = new ArrayBuffer(2 + chunkData.length);
302
+ const view = new DataView(buffer);
303
+ view.setUint16(0, 113 /* DIRECT_WRITE_DATA */, false);
304
+ const result = new Uint8Array(buffer);
305
+ result.set(chunkData, 2);
306
+ return result;
307
+ }
308
+ function buildDirectWriteEndCommand(refreshMode = 0) {
309
+ const buffer = new ArrayBuffer(3);
310
+ const view = new DataView(buffer);
311
+ view.setUint16(0, 114 /* DIRECT_WRITE_END */, false);
312
+ view.setUint8(2, refreshMode);
313
+ return new Uint8Array(buffer);
314
+ }
315
+ function buildWriteConfigCommand(configData) {
316
+ const configLen = configData.length;
317
+ if (configLen <= CONFIG_CHUNK_SIZE) {
318
+ const buffer = new ArrayBuffer(2 + configLen);
319
+ const view = new DataView(buffer);
320
+ view.setUint16(0, 65 /* WRITE_CONFIG */, false);
321
+ const result = new Uint8Array(buffer);
322
+ result.set(configData, 2);
323
+ return [result, []];
324
+ }
325
+ const firstChunkDataSize = CONFIG_CHUNK_SIZE - 2;
326
+ const firstBuffer = new ArrayBuffer(2 + 2 + firstChunkDataSize);
327
+ const firstView = new DataView(firstBuffer);
328
+ firstView.setUint16(0, 65 /* WRITE_CONFIG */, false);
329
+ firstView.setUint16(2, configLen, true);
330
+ const firstCommand = new Uint8Array(firstBuffer);
331
+ firstCommand.set(configData.subarray(0, firstChunkDataSize), 4);
332
+ const chunks = [];
333
+ let offset = firstChunkDataSize;
334
+ while (offset < configLen) {
335
+ const chunkSize = Math.min(CONFIG_CHUNK_SIZE, configLen - offset);
336
+ const buffer = new ArrayBuffer(2 + chunkSize);
337
+ const view = new DataView(buffer);
338
+ view.setUint16(0, 66 /* WRITE_CONFIG_CHUNK */, false);
339
+ const chunk = new Uint8Array(buffer);
340
+ chunk.set(configData.subarray(offset, offset + chunkSize), 2);
341
+ chunks.push(chunk);
342
+ offset += chunkSize;
343
+ }
344
+ return [firstCommand, chunks];
345
+ }
346
+
347
+ // src/protocol/responses.ts
348
+ function unpackCommandCode(data, offset = 0) {
349
+ const view = new DataView(data.buffer, data.byteOffset + offset, 2);
350
+ return view.getUint16(0, false);
351
+ }
352
+ function stripCommandEcho(data, expectedCmd) {
353
+ if (data.length >= 2) {
354
+ const echo = unpackCommandCode(data);
355
+ if (echo === expectedCmd || echo === (expectedCmd | RESPONSE_HIGH_BIT_FLAG)) {
356
+ return data.subarray(2);
357
+ }
358
+ }
359
+ return data;
360
+ }
361
+ function checkResponseType(response) {
362
+ const code = unpackCommandCode(response);
363
+ const isAck = Boolean(code & RESPONSE_HIGH_BIT_FLAG);
364
+ const command = code & ~RESPONSE_HIGH_BIT_FLAG;
365
+ return [command, isAck];
366
+ }
367
+ function validateAckResponse(data, expectedCommand) {
368
+ if (data.length < 2) {
369
+ throw new InvalidResponseError(
370
+ `ACK too short: ${data.length} bytes (need at least 2)`
371
+ );
372
+ }
373
+ const responseCode = unpackCommandCode(data);
374
+ const validResponses = /* @__PURE__ */ new Set([
375
+ expectedCommand,
376
+ expectedCommand | RESPONSE_HIGH_BIT_FLAG
377
+ ]);
378
+ if (!validResponses.has(responseCode)) {
379
+ throw new InvalidResponseError(
380
+ `ACK mismatch: expected 0x${expectedCommand.toString(16).padStart(4, "0")}, got 0x${responseCode.toString(16).padStart(4, "0")}`
381
+ );
382
+ }
383
+ }
384
+ function parseFirmwareVersion(data) {
385
+ if (data.length < 5) {
386
+ throw new InvalidResponseError(
387
+ `Firmware version response too short: ${data.length} bytes (need at least 5)`
388
+ );
389
+ }
390
+ const echo = unpackCommandCode(data);
391
+ if (echo !== 67 && echo !== (67 | RESPONSE_HIGH_BIT_FLAG)) {
392
+ throw new InvalidResponseError(
393
+ `Firmware version echo mismatch: expected 0x0043, got 0x${echo.toString(16).padStart(4, "0")}`
394
+ );
395
+ }
396
+ const major = data[2];
397
+ const minor = data[3];
398
+ const shaLength = data[4];
399
+ if (shaLength === 0) {
400
+ throw new InvalidResponseError(
401
+ "Firmware version missing SHA hash (shaLength is 0)"
402
+ );
403
+ }
404
+ const expectedTotalLength = 5 + shaLength;
405
+ if (data.length < expectedTotalLength) {
406
+ throw new InvalidResponseError(
407
+ `Firmware version response incomplete: expected ${expectedTotalLength} bytes (5 header + ${shaLength} SHA), got ${data.length}`
408
+ );
409
+ }
410
+ const shaBytes = data.subarray(5, 5 + shaLength);
411
+ const textDecoder = new TextDecoder("ascii");
412
+ let sha;
413
+ try {
414
+ sha = textDecoder.decode(shaBytes);
415
+ } catch (e) {
416
+ throw new InvalidResponseError(
417
+ `Invalid SHA hash encoding (expected ASCII): ${e}`
418
+ );
419
+ }
420
+ return {
421
+ major,
422
+ minor,
423
+ sha
424
+ };
425
+ }
426
+
427
+ // src/models/config.ts
428
+ import { ColorScheme as ColorScheme3 } from "@opendisplay/epaper-dithering";
429
+ var SystemConfig;
430
+ ((SystemConfig2) => {
431
+ SystemConfig2.SIZE = 22;
432
+ function hasPwrPin(config) {
433
+ return !!(config.deviceFlags & 1);
434
+ }
435
+ SystemConfig2.hasPwrPin = hasPwrPin;
436
+ function needsXiaoinit(config) {
437
+ return !!(config.deviceFlags & 2);
438
+ }
439
+ SystemConfig2.needsXiaoinit = needsXiaoinit;
440
+ function icTypeEnum(config) {
441
+ if (Object.values(ICType).includes(config.icType)) {
442
+ return config.icType;
443
+ }
444
+ return config.icType;
445
+ }
446
+ SystemConfig2.icTypeEnum = icTypeEnum;
447
+ function fromBytes(data) {
448
+ if (data.length < SystemConfig2.SIZE) {
449
+ throw new Error(`Invalid SystemConfig size: ${data.length} < ${SystemConfig2.SIZE}`);
450
+ }
451
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
452
+ return {
453
+ icType: view.getUint16(0, true),
454
+ // little-endian
455
+ communicationModes: view.getUint8(2),
456
+ deviceFlags: view.getUint8(3),
457
+ pwrPin: view.getUint8(4),
458
+ reserved: data.slice(5, 22)
459
+ };
460
+ }
461
+ SystemConfig2.fromBytes = fromBytes;
462
+ })(SystemConfig || (SystemConfig = {}));
463
+ var ManufacturerData;
464
+ ((ManufacturerData2) => {
465
+ ManufacturerData2.SIZE = 22;
466
+ function fromBytes(data) {
467
+ if (data.length < ManufacturerData2.SIZE) {
468
+ throw new Error(`Invalid ManufacturerData size: ${data.length} < ${ManufacturerData2.SIZE}`);
469
+ }
470
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
471
+ return {
472
+ manufacturerId: view.getUint16(0, true),
473
+ // little-endian
474
+ boardType: view.getUint8(2),
475
+ boardRevision: view.getUint8(3),
476
+ reserved: data.slice(4, 22)
477
+ };
478
+ }
479
+ ManufacturerData2.fromBytes = fromBytes;
480
+ })(ManufacturerData || (ManufacturerData = {}));
481
+ var PowerOption;
482
+ ((PowerOption2) => {
483
+ PowerOption2.SIZE = 30;
484
+ function batteryMah(config) {
485
+ return config.batteryCapacityMah;
486
+ }
487
+ PowerOption2.batteryMah = batteryMah;
488
+ function powerModeEnum(config) {
489
+ if (Object.values(PowerMode).includes(config.powerMode)) {
490
+ return config.powerMode;
491
+ }
492
+ return config.powerMode;
493
+ }
494
+ PowerOption2.powerModeEnum = powerModeEnum;
495
+ function fromBytes(data) {
496
+ if (data.length < PowerOption2.SIZE) {
497
+ throw new Error(`Invalid PowerOption size: ${data.length} < ${PowerOption2.SIZE}`);
498
+ }
499
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
500
+ const batteryMah2 = data[1] | data[2] << 8 | data[3] << 16;
501
+ return {
502
+ powerMode: view.getUint8(0),
503
+ batteryCapacityMah: batteryMah2,
504
+ sleepTimeoutMs: view.getUint16(4, true),
505
+ // little-endian
506
+ txPower: view.getInt8(6),
507
+ sleepFlags: view.getUint8(7),
508
+ batterySensePin: view.getUint8(8),
509
+ batterySenseEnablePin: view.getUint8(9),
510
+ batterySenseFlags: view.getUint8(10),
511
+ capacityEstimator: view.getUint8(11),
512
+ voltageScalingFactor: view.getUint16(12, true),
513
+ // little-endian
514
+ deepSleepCurrentUa: view.getUint32(14, true),
515
+ // little-endian
516
+ deepSleepTimeSeconds: view.getUint16(18, true),
517
+ // little-endian
518
+ reserved: data.slice(20, 30)
519
+ };
520
+ }
521
+ PowerOption2.fromBytes = fromBytes;
522
+ })(PowerOption || (PowerOption = {}));
523
+ var DisplayConfig;
524
+ ((DisplayConfig2) => {
525
+ DisplayConfig2.SIZE = 46;
526
+ function supportsRaw(config) {
527
+ return !!(config.transmissionModes & 1);
528
+ }
529
+ DisplayConfig2.supportsRaw = supportsRaw;
530
+ function supportsZip(config) {
531
+ return !!(config.transmissionModes & 2);
532
+ }
533
+ DisplayConfig2.supportsZip = supportsZip;
534
+ function supportsG5(config) {
535
+ return !!(config.transmissionModes & 4);
536
+ }
537
+ DisplayConfig2.supportsG5 = supportsG5;
538
+ function supportsDirectWrite(config) {
539
+ return !!(config.transmissionModes & 8);
540
+ }
541
+ DisplayConfig2.supportsDirectWrite = supportsDirectWrite;
542
+ function clearOnBoot(config) {
543
+ return !!(config.transmissionModes & 128);
544
+ }
545
+ DisplayConfig2.clearOnBoot = clearOnBoot;
546
+ function colorSchemeEnum(config) {
547
+ if (Object.values(ColorScheme3).includes(config.colorScheme)) {
548
+ return config.colorScheme;
549
+ }
550
+ return config.colorScheme;
551
+ }
552
+ DisplayConfig2.colorSchemeEnum = colorSchemeEnum;
553
+ function rotationEnum(config) {
554
+ if (Object.values(Rotation).includes(config.rotation)) {
555
+ return config.rotation;
556
+ }
557
+ return config.rotation;
558
+ }
559
+ DisplayConfig2.rotationEnum = rotationEnum;
560
+ function fromBytes(data) {
561
+ if (data.length < DisplayConfig2.SIZE) {
562
+ throw new Error(`Invalid DisplayConfig size: ${data.length} < ${DisplayConfig2.SIZE}`);
563
+ }
564
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
565
+ return {
566
+ instanceNumber: view.getUint8(0),
567
+ displayTechnology: view.getUint8(1),
568
+ panelIcType: view.getUint16(2, true),
569
+ // little-endian
570
+ pixelWidth: view.getUint16(4, true),
571
+ // little-endian
572
+ pixelHeight: view.getUint16(6, true),
573
+ // little-endian
574
+ activeWidthMm: view.getUint16(8, true),
575
+ // little-endian
576
+ activeHeightMm: view.getUint16(10, true),
577
+ // little-endian
578
+ tagType: view.getUint16(12, true),
579
+ // little-endian
580
+ rotation: view.getUint8(14),
581
+ resetPin: view.getUint8(15),
582
+ busyPin: view.getUint8(16),
583
+ dcPin: view.getUint8(17),
584
+ csPin: view.getUint8(18),
585
+ dataPin: view.getUint8(19),
586
+ partialUpdateSupport: view.getUint8(20),
587
+ colorScheme: view.getUint8(21),
588
+ transmissionModes: view.getUint8(22),
589
+ clkPin: view.getUint8(23),
590
+ reservedPins: data.slice(24, 31),
591
+ // 7 pins
592
+ reserved: data.slice(31, 46)
593
+ // 15 bytes
594
+ };
595
+ }
596
+ DisplayConfig2.fromBytes = fromBytes;
597
+ })(DisplayConfig || (DisplayConfig = {}));
598
+ var LedConfig;
599
+ ((LedConfig2) => {
600
+ LedConfig2.SIZE = 22;
601
+ function fromBytes(data) {
602
+ if (data.length < LedConfig2.SIZE) {
603
+ throw new Error(`Invalid LedConfig size: ${data.length} < ${LedConfig2.SIZE}`);
604
+ }
605
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
606
+ return {
607
+ instanceNumber: view.getUint8(0),
608
+ ledType: view.getUint8(1),
609
+ led1R: view.getUint8(2),
610
+ led2G: view.getUint8(3),
611
+ led3B: view.getUint8(4),
612
+ led4: view.getUint8(5),
613
+ ledFlags: view.getUint8(6),
614
+ reserved: data.slice(7, 22)
615
+ };
616
+ }
617
+ LedConfig2.fromBytes = fromBytes;
618
+ })(LedConfig || (LedConfig = {}));
619
+ var SensorData;
620
+ ((SensorData2) => {
621
+ SensorData2.SIZE = 30;
622
+ function fromBytes(data) {
623
+ if (data.length < SensorData2.SIZE) {
624
+ throw new Error(`Invalid SensorData size: ${data.length} < ${SensorData2.SIZE}`);
625
+ }
626
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
627
+ return {
628
+ instanceNumber: view.getUint8(0),
629
+ sensorType: view.getUint16(1, true),
630
+ // little-endian
631
+ busId: view.getUint8(3),
632
+ reserved: data.slice(4, 30)
633
+ };
634
+ }
635
+ SensorData2.fromBytes = fromBytes;
636
+ })(SensorData || (SensorData = {}));
637
+ var DataBus;
638
+ ((DataBus2) => {
639
+ DataBus2.SIZE = 30;
640
+ function busTypeEnum(config) {
641
+ if (Object.values(BusType).includes(config.busType)) {
642
+ return config.busType;
643
+ }
644
+ return config.busType;
645
+ }
646
+ DataBus2.busTypeEnum = busTypeEnum;
647
+ function fromBytes(data) {
648
+ if (data.length < DataBus2.SIZE) {
649
+ throw new Error(`Invalid DataBus size: ${data.length} < ${DataBus2.SIZE}`);
650
+ }
651
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
652
+ return {
653
+ instanceNumber: view.getUint8(0),
654
+ busType: view.getUint8(1),
655
+ pin1: view.getUint8(2),
656
+ pin2: view.getUint8(3),
657
+ pin3: view.getUint8(4),
658
+ pin4: view.getUint8(5),
659
+ pin5: view.getUint8(6),
660
+ pin6: view.getUint8(7),
661
+ pin7: view.getUint8(8),
662
+ busSpeedHz: view.getUint32(9, true),
663
+ // little-endian
664
+ busFlags: view.getUint8(13),
665
+ pullups: view.getUint8(14),
666
+ pulldowns: view.getUint8(15),
667
+ reserved: data.slice(16, 30)
668
+ };
669
+ }
670
+ DataBus2.fromBytes = fromBytes;
671
+ })(DataBus || (DataBus = {}));
672
+ var BinaryInputs;
673
+ ((BinaryInputs2) => {
674
+ BinaryInputs2.SIZE = 30;
675
+ function fromBytes(data) {
676
+ if (data.length < BinaryInputs2.SIZE) {
677
+ throw new Error(`Invalid BinaryInputs size: ${data.length} < ${BinaryInputs2.SIZE}`);
678
+ }
679
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
680
+ return {
681
+ instanceNumber: view.getUint8(0),
682
+ inputType: view.getUint8(1),
683
+ displayAs: view.getUint8(2),
684
+ reservedPins: data.slice(3, 11),
685
+ // 8 pins
686
+ inputFlags: view.getUint8(11),
687
+ invert: view.getUint8(12),
688
+ pullups: view.getUint8(13),
689
+ pulldowns: view.getUint8(14),
690
+ reserved: data.slice(15, 30)
691
+ };
692
+ }
693
+ BinaryInputs2.fromBytes = fromBytes;
694
+ })(BinaryInputs || (BinaryInputs = {}));
695
+
696
+ // src/protocol/config-parser.ts
697
+ var PACKET_TYPE_SYSTEM = 1;
698
+ var PACKET_TYPE_MANUFACTURER = 2;
699
+ var PACKET_TYPE_POWER = 4;
700
+ var PACKET_TYPE_DISPLAY = 32;
701
+ var PACKET_TYPE_LED = 33;
702
+ var PACKET_TYPE_SENSOR = 35;
703
+ var PACKET_TYPE_DATABUS = 36;
704
+ var PACKET_TYPE_BINARY_INPUT = 37;
705
+ function parseConfigResponse(rawData) {
706
+ if (rawData.length < 5) {
707
+ throw new ConfigParseError(
708
+ `Config data too short: ${rawData.length} bytes (need at least 5)`
709
+ );
710
+ }
711
+ const view = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength);
712
+ const configLength = view.getUint16(0, true);
713
+ const configVersion = view.getUint8(2);
714
+ console.debug(
715
+ `TLV wrapper: length=${configLength} bytes, version=${configVersion}`
716
+ );
717
+ let packetData;
718
+ if (rawData.length > 5) {
719
+ packetData = rawData.slice(3, -2);
720
+ } else {
721
+ packetData = rawData.slice(3);
722
+ }
723
+ console.debug(`Packet data after wrapper strip: ${packetData.length} bytes`);
724
+ return parseTlvConfig(packetData, configVersion);
725
+ }
726
+ function parseTlvConfig(data, version = 1) {
727
+ if (data.length < 2) {
728
+ throw new ConfigParseError(
729
+ `TLV data too short: ${data.length} bytes (need at least 2)`
730
+ );
731
+ }
732
+ console.debug(`Parsing TLV config, ${data.length} bytes`);
733
+ let offset = 0;
734
+ const packets = /* @__PURE__ */ new Map();
735
+ while (offset < data.length - 1) {
736
+ if (offset + 2 > data.length) {
737
+ break;
738
+ }
739
+ const packetNumber = data[offset];
740
+ const packetType = data[offset + 1];
741
+ offset += 2;
742
+ const packetSize = getPacketSize(packetType);
743
+ if (packetSize === null) {
744
+ console.warn(
745
+ `Unknown packet type 0x${packetType.toString(16).padStart(2, "0")} at offset ${offset - 2}, skipping`
746
+ );
747
+ break;
748
+ }
749
+ if (offset + packetSize > data.length) {
750
+ throw new ConfigParseError(
751
+ `Packet type 0x${packetType.toString(16).padStart(2, "0")} truncated: need ${packetSize} bytes, have ${data.length - offset}`
752
+ );
753
+ }
754
+ const packetData = data.slice(offset, offset + packetSize);
755
+ offset += packetSize;
756
+ const key = `${packetType}-${packetNumber}`;
757
+ packets.set(key, packetData);
758
+ console.debug(
759
+ `Parsed packet: type=0x${packetType.toString(16).padStart(2, "0")}, num=${packetNumber}, size=${packetSize}`
760
+ );
761
+ }
762
+ let system;
763
+ let manufacturer;
764
+ let power;
765
+ const displays = [];
766
+ const leds = [];
767
+ const sensors = [];
768
+ const dataBuses = [];
769
+ const binaryInputs = [];
770
+ for (const [key, packetData] of packets) {
771
+ const [packetTypeStr] = key.split("-");
772
+ const packetType = parseInt(packetTypeStr, 10);
773
+ switch (packetType) {
774
+ case PACKET_TYPE_SYSTEM:
775
+ system = parseSystemConfig(packetData);
776
+ break;
777
+ case PACKET_TYPE_MANUFACTURER:
778
+ manufacturer = parseManufacturerData(packetData);
779
+ break;
780
+ case PACKET_TYPE_POWER:
781
+ power = parsePowerOption(packetData);
782
+ break;
783
+ case PACKET_TYPE_DISPLAY:
784
+ displays.push(parseDisplayConfig(packetData));
785
+ break;
786
+ case PACKET_TYPE_LED:
787
+ leds.push(parseLedConfig(packetData));
788
+ break;
789
+ case PACKET_TYPE_SENSOR:
790
+ sensors.push(parseSensorData(packetData));
791
+ break;
792
+ case PACKET_TYPE_DATABUS:
793
+ dataBuses.push(parseDataBus(packetData));
794
+ break;
795
+ case PACKET_TYPE_BINARY_INPUT:
796
+ binaryInputs.push(parseBinaryInputs(packetData));
797
+ break;
798
+ }
799
+ }
800
+ return {
801
+ system,
802
+ manufacturer,
803
+ power,
804
+ displays,
805
+ leds,
806
+ sensors,
807
+ dataBuses,
808
+ binaryInputs,
809
+ version,
810
+ // From firmware wrapper
811
+ minorVersion: 1,
812
+ // Not stored in device (only single version byte exists)
813
+ loaded: true
814
+ };
815
+ }
816
+ function getPacketSize(packetType) {
817
+ const sizes = {
818
+ [PACKET_TYPE_SYSTEM]: 22,
819
+ [PACKET_TYPE_MANUFACTURER]: 22,
820
+ [PACKET_TYPE_POWER]: 30,
821
+ // Fixed: was 32
822
+ [PACKET_TYPE_DISPLAY]: 46,
823
+ // Fixed: was 66
824
+ [PACKET_TYPE_LED]: 22,
825
+ [PACKET_TYPE_SENSOR]: 30,
826
+ [PACKET_TYPE_DATABUS]: 30,
827
+ // Fixed: was 28
828
+ [PACKET_TYPE_BINARY_INPUT]: 30
829
+ // Fixed: was 29
830
+ };
831
+ return sizes[packetType] ?? null;
832
+ }
833
+ function parseSystemConfig(data) {
834
+ if (data.length < 22) {
835
+ throw new ConfigParseError(
836
+ `SystemConfig too short: ${data.length} bytes (need 22)`
837
+ );
838
+ }
839
+ return SystemConfig.fromBytes(data);
840
+ }
841
+ function parseManufacturerData(data) {
842
+ if (data.length < 22) {
843
+ throw new ConfigParseError(
844
+ `ManufacturerData too short: ${data.length} bytes (need 22)`
845
+ );
846
+ }
847
+ return ManufacturerData.fromBytes(data);
848
+ }
849
+ function parsePowerOption(data) {
850
+ if (data.length < 30) {
851
+ throw new ConfigParseError(
852
+ `PowerOption too short: ${data.length} bytes (need 30)`
853
+ );
854
+ }
855
+ return PowerOption.fromBytes(data);
856
+ }
857
+ function parseDisplayConfig(data) {
858
+ if (data.length < 46) {
859
+ throw new ConfigParseError(
860
+ `DisplayConfig too short: ${data.length} bytes (need 46)`
861
+ );
862
+ }
863
+ return DisplayConfig.fromBytes(data);
864
+ }
865
+ function parseLedConfig(data) {
866
+ if (data.length < 22) {
867
+ throw new ConfigParseError(
868
+ `LedConfig too short: ${data.length} bytes (need 22)`
869
+ );
870
+ }
871
+ return LedConfig.fromBytes(data);
872
+ }
873
+ function parseSensorData(data) {
874
+ if (data.length < 30) {
875
+ throw new ConfigParseError(
876
+ `SensorData too short: ${data.length} bytes (need 30)`
877
+ );
878
+ }
879
+ return SensorData.fromBytes(data);
880
+ }
881
+ function parseDataBus(data) {
882
+ if (data.length < 30) {
883
+ throw new ConfigParseError(
884
+ `DataBus too short: ${data.length} bytes (need 30)`
885
+ );
886
+ }
887
+ return DataBus.fromBytes(data);
888
+ }
889
+ function parseBinaryInputs(data) {
890
+ if (data.length < 30) {
891
+ throw new ConfigParseError(
892
+ `BinaryInputs too short: ${data.length} bytes (need 30)`
893
+ );
894
+ }
895
+ return BinaryInputs.fromBytes(data);
896
+ }
897
+
898
+ // src/protocol/config-serializer.ts
899
+ var PACKET_TYPE_SYSTEM2 = 1;
900
+ var PACKET_TYPE_MANUFACTURER2 = 2;
901
+ var PACKET_TYPE_POWER2 = 4;
902
+ var PACKET_TYPE_DISPLAY2 = 32;
903
+ var PACKET_TYPE_LED2 = 33;
904
+ var PACKET_TYPE_SENSOR2 = 35;
905
+ var PACKET_TYPE_DATABUS2 = 36;
906
+ var PACKET_TYPE_BINARY_INPUT2 = 37;
907
+ function calculateConfigCrc(data) {
908
+ let crc = 4294967295;
909
+ for (const byte of data) {
910
+ crc ^= byte;
911
+ for (let i = 0; i < 8; i++) {
912
+ if (crc & 1) {
913
+ crc = crc >>> 1 ^ 3988292384;
914
+ } else {
915
+ crc = crc >>> 1;
916
+ }
917
+ }
918
+ }
919
+ const crc32 = ~crc >>> 0;
920
+ return crc32 & 65535;
921
+ }
922
+ function serializeSystemConfig(config) {
923
+ const buffer = new ArrayBuffer(22);
924
+ const view = new DataView(buffer);
925
+ const result = new Uint8Array(buffer);
926
+ view.setUint16(0, config.icType, true);
927
+ view.setUint8(2, config.communicationModes);
928
+ view.setUint8(3, config.deviceFlags);
929
+ view.setUint8(4, config.pwrPin);
930
+ const reserved = config.reserved || new Uint8Array(17);
931
+ result.set(reserved.subarray(0, 17), 5);
932
+ return result;
933
+ }
934
+ function serializeManufacturerData(config) {
935
+ const buffer = new ArrayBuffer(22);
936
+ const view = new DataView(buffer);
937
+ const result = new Uint8Array(buffer);
938
+ view.setUint16(0, config.manufacturerId, true);
939
+ view.setUint8(2, config.boardType);
940
+ view.setUint8(3, config.boardRevision);
941
+ const reserved = config.reserved || new Uint8Array(18);
942
+ result.set(reserved.subarray(0, 18), 4);
943
+ return result;
944
+ }
945
+ function serializePowerOption(config) {
946
+ const buffer = new ArrayBuffer(30);
947
+ const view = new DataView(buffer);
948
+ const result = new Uint8Array(buffer);
949
+ view.setUint8(0, config.powerMode);
950
+ view.setUint8(1, config.batteryCapacityMah & 255);
951
+ view.setUint8(2, config.batteryCapacityMah >> 8 & 255);
952
+ view.setUint8(3, config.batteryCapacityMah >> 16 & 255);
953
+ view.setUint16(4, config.sleepTimeoutMs, true);
954
+ view.setInt8(6, config.txPower);
955
+ view.setUint8(7, config.sleepFlags);
956
+ view.setUint8(8, config.batterySensePin);
957
+ view.setUint8(9, config.batterySenseEnablePin);
958
+ view.setUint8(10, config.batterySenseFlags);
959
+ view.setUint8(11, config.capacityEstimator);
960
+ view.setUint16(12, config.voltageScalingFactor, true);
961
+ view.setUint32(14, config.deepSleepCurrentUa, true);
962
+ view.setUint16(18, config.deepSleepTimeSeconds, true);
963
+ const reserved = config.reserved || new Uint8Array(10);
964
+ result.set(reserved.subarray(0, 10), 20);
965
+ return result;
966
+ }
967
+ function serializeDisplayConfig(config) {
968
+ const buffer = new ArrayBuffer(46);
969
+ const view = new DataView(buffer);
970
+ const result = new Uint8Array(buffer);
971
+ view.setUint8(0, config.instanceNumber);
972
+ view.setUint8(1, config.displayTechnology);
973
+ view.setUint16(2, config.panelIcType, true);
974
+ view.setUint16(4, config.pixelWidth, true);
975
+ view.setUint16(6, config.pixelHeight, true);
976
+ view.setUint16(8, config.activeWidthMm, true);
977
+ view.setUint16(10, config.activeHeightMm, true);
978
+ view.setUint16(12, config.tagType, true);
979
+ view.setUint8(14, config.rotation);
980
+ view.setUint8(15, config.resetPin);
981
+ view.setUint8(16, config.busyPin);
982
+ view.setUint8(17, config.dcPin);
983
+ view.setUint8(18, config.csPin);
984
+ view.setUint8(19, config.dataPin);
985
+ view.setUint8(20, config.partialUpdateSupport);
986
+ view.setUint8(21, config.colorScheme);
987
+ view.setUint8(22, config.transmissionModes);
988
+ view.setUint8(23, config.clkPin);
989
+ const reservedPins = config.reservedPins || new Uint8Array(7).fill(255);
990
+ result.set(reservedPins.subarray(0, 7), 24);
991
+ const reserved = config.reserved || new Uint8Array(15);
992
+ result.set(reserved.subarray(0, 15), 31);
993
+ return result;
994
+ }
995
+ function serializeLedConfig(config) {
996
+ const buffer = new ArrayBuffer(22);
997
+ const view = new DataView(buffer);
998
+ const result = new Uint8Array(buffer);
999
+ view.setUint8(0, config.instanceNumber);
1000
+ view.setUint8(1, config.ledType);
1001
+ view.setUint8(2, config.led1R);
1002
+ view.setUint8(3, config.led2G);
1003
+ view.setUint8(4, config.led3B);
1004
+ view.setUint8(5, config.led4);
1005
+ view.setUint8(6, config.ledFlags);
1006
+ const reserved = config.reserved || new Uint8Array(15);
1007
+ result.set(reserved.subarray(0, 15), 7);
1008
+ return result;
1009
+ }
1010
+ function serializeSensorData(config) {
1011
+ const buffer = new ArrayBuffer(30);
1012
+ const view = new DataView(buffer);
1013
+ const result = new Uint8Array(buffer);
1014
+ view.setUint8(0, config.instanceNumber);
1015
+ view.setUint16(1, config.sensorType, true);
1016
+ view.setUint8(3, config.busId);
1017
+ const reserved = config.reserved || new Uint8Array(26);
1018
+ result.set(reserved.subarray(0, 26), 4);
1019
+ return result;
1020
+ }
1021
+ function serializeDataBus(config) {
1022
+ const buffer = new ArrayBuffer(30);
1023
+ const view = new DataView(buffer);
1024
+ const result = new Uint8Array(buffer);
1025
+ view.setUint8(0, config.instanceNumber);
1026
+ view.setUint8(1, config.busType);
1027
+ view.setUint8(2, config.pin1);
1028
+ view.setUint8(3, config.pin2);
1029
+ view.setUint8(4, config.pin3);
1030
+ view.setUint8(5, config.pin4);
1031
+ view.setUint8(6, config.pin5);
1032
+ view.setUint8(7, config.pin6);
1033
+ view.setUint8(8, config.pin7);
1034
+ view.setUint32(9, config.busSpeedHz, true);
1035
+ view.setUint8(13, config.busFlags);
1036
+ view.setUint8(14, config.pullups);
1037
+ view.setUint8(15, config.pulldowns);
1038
+ const reserved = config.reserved || new Uint8Array(14);
1039
+ result.set(reserved.subarray(0, 14), 16);
1040
+ return result;
1041
+ }
1042
+ function serializeBinaryInputs(config) {
1043
+ const buffer = new ArrayBuffer(30);
1044
+ const view = new DataView(buffer);
1045
+ const result = new Uint8Array(buffer);
1046
+ view.setUint8(0, config.instanceNumber);
1047
+ view.setUint8(1, config.inputType);
1048
+ view.setUint8(2, config.displayAs);
1049
+ const reservedPins = config.reservedPins || new Uint8Array(8);
1050
+ result.set(reservedPins.subarray(0, 8), 3);
1051
+ view.setUint8(11, config.inputFlags);
1052
+ view.setUint8(12, config.invert);
1053
+ view.setUint8(13, config.pullups);
1054
+ view.setUint8(14, config.pulldowns);
1055
+ const reserved = config.reserved || new Uint8Array(15);
1056
+ result.set(reserved.subarray(0, 15), 15);
1057
+ return result;
1058
+ }
1059
+ function serializeConfig(config) {
1060
+ const chunks = [];
1061
+ const header = new Uint8Array([0, 0, config.version]);
1062
+ chunks.push(header);
1063
+ if (config.system) {
1064
+ chunks.push(new Uint8Array([0, PACKET_TYPE_SYSTEM2]));
1065
+ chunks.push(serializeSystemConfig(config.system));
1066
+ }
1067
+ if (config.manufacturer) {
1068
+ chunks.push(new Uint8Array([0, PACKET_TYPE_MANUFACTURER2]));
1069
+ chunks.push(serializeManufacturerData(config.manufacturer));
1070
+ }
1071
+ if (config.power) {
1072
+ chunks.push(new Uint8Array([0, PACKET_TYPE_POWER2]));
1073
+ chunks.push(serializePowerOption(config.power));
1074
+ }
1075
+ for (let i = 0; i < Math.min(config.displays.length, 4); i++) {
1076
+ chunks.push(new Uint8Array([i, PACKET_TYPE_DISPLAY2]));
1077
+ chunks.push(serializeDisplayConfig(config.displays[i]));
1078
+ }
1079
+ for (let i = 0; i < Math.min(config.leds.length, 4); i++) {
1080
+ chunks.push(new Uint8Array([i, PACKET_TYPE_LED2]));
1081
+ chunks.push(serializeLedConfig(config.leds[i]));
1082
+ }
1083
+ for (let i = 0; i < Math.min(config.sensors.length, 4); i++) {
1084
+ chunks.push(new Uint8Array([i, PACKET_TYPE_SENSOR2]));
1085
+ chunks.push(serializeSensorData(config.sensors[i]));
1086
+ }
1087
+ for (let i = 0; i < Math.min(config.dataBuses.length, 4); i++) {
1088
+ chunks.push(new Uint8Array([i, PACKET_TYPE_DATABUS2]));
1089
+ chunks.push(serializeDataBus(config.dataBuses[i]));
1090
+ }
1091
+ for (let i = 0; i < Math.min(config.binaryInputs.length, 4); i++) {
1092
+ chunks.push(new Uint8Array([i, PACKET_TYPE_BINARY_INPUT2]));
1093
+ chunks.push(serializeBinaryInputs(config.binaryInputs[i]));
1094
+ }
1095
+ const totalDataSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
1096
+ if (totalDataSize + 2 > 4096) {
1097
+ throw new Error(
1098
+ `Config size ${totalDataSize + 2} bytes exceeds maximum 4096 bytes`
1099
+ );
1100
+ }
1101
+ const packetData = new Uint8Array(totalDataSize);
1102
+ let offset = 0;
1103
+ for (const chunk of chunks) {
1104
+ packetData.set(chunk, offset);
1105
+ offset += chunk.length;
1106
+ }
1107
+ const crc16 = calculateConfigCrc(packetData);
1108
+ const result = new Uint8Array(totalDataSize + 2);
1109
+ result.set(packetData, 0);
1110
+ const crcView = new DataView(result.buffer, totalDataSize, 2);
1111
+ crcView.setUint16(0, crc16, true);
1112
+ return result;
1113
+ }
1114
+
1115
+ // src/transport/notification-queue.ts
1116
+ var NotificationQueue = class {
1117
+ queue = [];
1118
+ pendingResolvers = [];
1119
+ /**
1120
+ * Add a notification to the queue.
1121
+ *
1122
+ * If there are pending consumers waiting, immediately resolve the oldest one.
1123
+ * Otherwise, buffer the notification for future consumption.
1124
+ *
1125
+ * @param data - Notification data received from BLE characteristic
1126
+ */
1127
+ enqueue(data) {
1128
+ if (this.pendingResolvers.length > 0) {
1129
+ const pending = this.pendingResolvers.shift();
1130
+ clearTimeout(pending.timeoutId);
1131
+ pending.resolve(data);
1132
+ } else {
1133
+ this.queue.push(data);
1134
+ }
1135
+ }
1136
+ /**
1137
+ * Get the next notification from the queue.
1138
+ *
1139
+ * If a notification is already buffered, return it immediately.
1140
+ * Otherwise, wait for the next notification or timeout.
1141
+ *
1142
+ * @param timeoutMs - Maximum time to wait in milliseconds
1143
+ * @returns Promise that resolves with notification data
1144
+ * @throws {BLETimeoutError} If timeout expires before notification arrives
1145
+ */
1146
+ async dequeue(timeoutMs) {
1147
+ if (this.queue.length > 0) {
1148
+ return this.queue.shift();
1149
+ }
1150
+ return new Promise((resolve, reject) => {
1151
+ const timeoutId = window.setTimeout(() => {
1152
+ const index = this.pendingResolvers.findIndex(
1153
+ (p) => p.resolve === resolve
1154
+ );
1155
+ if (index !== -1) {
1156
+ this.pendingResolvers.splice(index, 1);
1157
+ reject(
1158
+ new BLETimeoutError(
1159
+ `No response received within ${timeoutMs}ms timeout`
1160
+ )
1161
+ );
1162
+ }
1163
+ }, timeoutMs);
1164
+ this.pendingResolvers.push({ resolve, reject, timeoutId });
1165
+ });
1166
+ }
1167
+ /**
1168
+ * Clear the queue and reject all pending requests.
1169
+ *
1170
+ * Called when the connection is closed or reset.
1171
+ *
1172
+ * @param reason - Reason for clearing (default: "Connection closed")
1173
+ */
1174
+ clear(reason = "Connection closed") {
1175
+ this.queue = [];
1176
+ for (const pending of this.pendingResolvers) {
1177
+ clearTimeout(pending.timeoutId);
1178
+ pending.reject(new Error(reason));
1179
+ }
1180
+ this.pendingResolvers = [];
1181
+ }
1182
+ /**
1183
+ * Get the number of buffered notifications.
1184
+ */
1185
+ get size() {
1186
+ return this.queue.length;
1187
+ }
1188
+ /**
1189
+ * Get the number of pending consumers waiting for notifications.
1190
+ */
1191
+ get pendingCount() {
1192
+ return this.pendingResolvers.length;
1193
+ }
1194
+ };
1195
+
1196
+ // src/transport/connection.ts
1197
+ var BLEConnection = class {
1198
+ device = null;
1199
+ gattServer = null;
1200
+ characteristic = null;
1201
+ notificationQueue = new NotificationQueue();
1202
+ disconnectHandler = null;
1203
+ /**
1204
+ * Check if currently connected to a device.
1205
+ */
1206
+ get isConnected() {
1207
+ return this.gattServer?.connected ?? false;
1208
+ }
1209
+ /**
1210
+ * Get the connected device name, if available.
1211
+ */
1212
+ get deviceName() {
1213
+ return this.device?.name;
1214
+ }
1215
+ /**
1216
+ * Connect to an OpenDisplay device.
1217
+ *
1218
+ * @param options - Connection options (device or namePrefix filter)
1219
+ * @throws {BLEConnectionError} If connection fails
1220
+ * @throws {Error} If Web Bluetooth is not supported
1221
+ */
1222
+ async connect(options = {}) {
1223
+ if (!navigator.bluetooth) {
1224
+ throw new Error(
1225
+ "Web Bluetooth API not supported in this browser. Try Chrome, Edge, or Opera on desktop/Android."
1226
+ );
1227
+ }
1228
+ try {
1229
+ if (options.device) {
1230
+ this.device = options.device;
1231
+ } else {
1232
+ const filters = [];
1233
+ if (options.namePrefix) {
1234
+ filters.push({
1235
+ services: [SERVICE_UUID],
1236
+ namePrefix: options.namePrefix
1237
+ });
1238
+ } else {
1239
+ filters.push({
1240
+ services: [SERVICE_UUID]
1241
+ });
1242
+ }
1243
+ filters.push({
1244
+ manufacturerData: [
1245
+ {
1246
+ companyIdentifier: MANUFACTURER_ID
1247
+ }
1248
+ ]
1249
+ });
1250
+ this.device = await navigator.bluetooth.requestDevice({
1251
+ filters,
1252
+ optionalServices: [SERVICE_UUID]
1253
+ });
1254
+ }
1255
+ if (!this.device.gatt) {
1256
+ throw new BLEConnectionError("Device does not support GATT");
1257
+ }
1258
+ this.gattServer = await this.device.gatt.connect();
1259
+ const service = await this.gattServer.getPrimaryService(SERVICE_UUID);
1260
+ const characteristics = await service.getCharacteristics();
1261
+ if (characteristics.length === 0) {
1262
+ throw new BLEConnectionError(
1263
+ "No characteristics found in OpenDisplay service"
1264
+ );
1265
+ }
1266
+ this.characteristic = characteristics[0];
1267
+ await this.characteristic.startNotifications();
1268
+ this.characteristic.addEventListener(
1269
+ "characteristicvaluechanged",
1270
+ this.handleNotification.bind(this)
1271
+ );
1272
+ this.disconnectHandler = this.handleDisconnect.bind(this);
1273
+ this.device.addEventListener(
1274
+ "gattserverdisconnected",
1275
+ this.disconnectHandler
1276
+ );
1277
+ console.log(
1278
+ `Connected to ${this.device.name || "OpenDisplay device"}`
1279
+ );
1280
+ } catch (error) {
1281
+ this.cleanup();
1282
+ if (error instanceof Error) {
1283
+ throw new BLEConnectionError(
1284
+ `Failed to connect: ${error.message}`
1285
+ );
1286
+ }
1287
+ throw new BLEConnectionError("Failed to connect to device");
1288
+ }
1289
+ }
1290
+ /**
1291
+ * Disconnect from the device.
1292
+ */
1293
+ async disconnect() {
1294
+ try {
1295
+ if (this.characteristic) {
1296
+ try {
1297
+ await this.characteristic.stopNotifications();
1298
+ } catch {
1299
+ }
1300
+ }
1301
+ if (this.gattServer?.connected) {
1302
+ this.gattServer.disconnect();
1303
+ }
1304
+ } finally {
1305
+ this.cleanup();
1306
+ }
1307
+ }
1308
+ /**
1309
+ * Write a command to the device.
1310
+ *
1311
+ * @param data - Command data to send
1312
+ * @throws {BLEConnectionError} If not connected or write fails
1313
+ */
1314
+ async writeCommand(data) {
1315
+ if (!this.isConnected || !this.characteristic) {
1316
+ throw new BLEConnectionError("Not connected to device");
1317
+ }
1318
+ try {
1319
+ await this.characteristic.writeValueWithResponse(data);
1320
+ } catch (error) {
1321
+ if (error instanceof Error) {
1322
+ throw new BLEConnectionError(
1323
+ `Failed to write command: ${error.message}`
1324
+ );
1325
+ }
1326
+ throw new BLEConnectionError("Failed to write command");
1327
+ }
1328
+ }
1329
+ /**
1330
+ * Read the next response from the device.
1331
+ *
1332
+ * @param timeoutMs - Maximum time to wait for response
1333
+ * @returns Promise that resolves with response data
1334
+ * @throws {BLETimeoutError} If timeout expires
1335
+ * @throws {BLEConnectionError} If not connected
1336
+ */
1337
+ async readResponse(timeoutMs) {
1338
+ if (!this.isConnected) {
1339
+ throw new BLEConnectionError("Not connected to device");
1340
+ }
1341
+ return this.notificationQueue.dequeue(timeoutMs);
1342
+ }
1343
+ /**
1344
+ * Handle incoming BLE notifications.
1345
+ *
1346
+ * @param event - Characteristic value changed event
1347
+ */
1348
+ handleNotification(event) {
1349
+ const characteristic = event.target;
1350
+ if (!characteristic.value) {
1351
+ return;
1352
+ }
1353
+ const data = new Uint8Array(characteristic.value.buffer);
1354
+ this.notificationQueue.enqueue(data);
1355
+ }
1356
+ /**
1357
+ * Handle device disconnection.
1358
+ */
1359
+ handleDisconnect() {
1360
+ console.log("Device disconnected");
1361
+ this.cleanup();
1362
+ }
1363
+ /**
1364
+ * Clean up resources and reset state.
1365
+ */
1366
+ cleanup() {
1367
+ this.notificationQueue.clear();
1368
+ if (this.characteristic) {
1369
+ this.characteristic.removeEventListener(
1370
+ "characteristicvaluechanged",
1371
+ this.handleNotification.bind(this)
1372
+ );
1373
+ this.characteristic = null;
1374
+ }
1375
+ if (this.device && this.disconnectHandler) {
1376
+ this.device.removeEventListener(
1377
+ "gattserverdisconnected",
1378
+ this.disconnectHandler
1379
+ );
1380
+ this.disconnectHandler = null;
1381
+ }
1382
+ this.gattServer = null;
1383
+ this.device = null;
1384
+ }
1385
+ };
1386
+
1387
+ // src/device.ts
1388
+ var OpenDisplayDevice = class _OpenDisplayDevice {
1389
+ /**
1390
+ * Initialize OpenDisplay device.
1391
+ *
1392
+ * @param options - Device initialization options
1393
+ */
1394
+ constructor(options = {}) {
1395
+ this.options = options;
1396
+ this._config = options.config ?? null;
1397
+ this._capabilities = options.capabilities ?? null;
1398
+ }
1399
+ // BLE operation timeouts (milliseconds)
1400
+ static TIMEOUT_FIRST_CHUNK = 1e4;
1401
+ // First chunk may take longer
1402
+ static TIMEOUT_CHUNK = 2e3;
1403
+ // Subsequent chunks
1404
+ static TIMEOUT_ACK = 5e3;
1405
+ // Command acknowledgments
1406
+ static TIMEOUT_REFRESH = 9e4;
1407
+ // Display refresh (firmware spec: up to 60s)
1408
+ connection = null;
1409
+ _config = null;
1410
+ _capabilities = null;
1411
+ _fwVersion = null;
1412
+ /**
1413
+ * Get full device configuration (if interrogated).
1414
+ */
1415
+ get config() {
1416
+ return this._config;
1417
+ }
1418
+ /**
1419
+ * Get device capabilities (width, height, color scheme, rotation).
1420
+ */
1421
+ get capabilities() {
1422
+ return this._capabilities;
1423
+ }
1424
+ /**
1425
+ * Get display width in pixels.
1426
+ */
1427
+ get width() {
1428
+ return this.ensureCapabilities().width;
1429
+ }
1430
+ /**
1431
+ * Get display height in pixels.
1432
+ */
1433
+ get height() {
1434
+ return this.ensureCapabilities().height;
1435
+ }
1436
+ /**
1437
+ * Get display color scheme.
1438
+ */
1439
+ get colorScheme() {
1440
+ return this.ensureCapabilities().colorScheme;
1441
+ }
1442
+ /**
1443
+ * Get display rotation in degrees.
1444
+ */
1445
+ get rotation() {
1446
+ return this.ensureCapabilities().rotation ?? 0;
1447
+ }
1448
+ /**
1449
+ * Check if currently connected to a device.
1450
+ */
1451
+ get isConnected() {
1452
+ return this.connection?.isConnected ?? false;
1453
+ }
1454
+ /**
1455
+ * Connect to an OpenDisplay device and optionally interrogate.
1456
+ *
1457
+ * @param connectionOptions - Optional connection parameters
1458
+ * @throws {BLEConnectionError} If connection fails
1459
+ */
1460
+ async connect(connectionOptions) {
1461
+ this.connection = new BLEConnection();
1462
+ const mergedOptions = {
1463
+ ...connectionOptions,
1464
+ device: this.options.device ?? connectionOptions?.device,
1465
+ namePrefix: this.options.namePrefix ?? connectionOptions?.namePrefix
1466
+ };
1467
+ await this.connection.connect(mergedOptions);
1468
+ if (!this._config && !this._capabilities) {
1469
+ console.log("No config provided, auto-interrogating device");
1470
+ await this.interrogate();
1471
+ }
1472
+ if (this._config && !this._capabilities) {
1473
+ this._capabilities = this.extractCapabilitiesFromConfig();
1474
+ }
1475
+ }
1476
+ /**
1477
+ * Disconnect from the device.
1478
+ */
1479
+ async disconnect() {
1480
+ if (this.connection) {
1481
+ await this.connection.disconnect();
1482
+ this.connection = null;
1483
+ }
1484
+ }
1485
+ /**
1486
+ * Read device configuration from device.
1487
+ *
1488
+ * @returns GlobalConfig with complete device configuration
1489
+ * @throws {ProtocolError} If interrogation fails
1490
+ */
1491
+ async interrogate() {
1492
+ this.ensureConnected();
1493
+ console.log("Interrogating device");
1494
+ const cmd = buildReadConfigCommand();
1495
+ await this.connection.writeCommand(cmd);
1496
+ const response = await this.connection.readResponse(
1497
+ _OpenDisplayDevice.TIMEOUT_FIRST_CHUNK
1498
+ );
1499
+ const chunkData = stripCommandEcho(response, 64 /* READ_CONFIG */);
1500
+ const view = new DataView(chunkData.buffer, chunkData.byteOffset);
1501
+ const totalLength = view.getUint16(2, true);
1502
+ const tlvData = [chunkData.subarray(4)];
1503
+ let currentLength = chunkData.length - 4;
1504
+ console.debug(`First chunk: ${chunkData.length} bytes, total length: ${totalLength}`);
1505
+ while (currentLength < totalLength) {
1506
+ const nextResponse = await this.connection.readResponse(
1507
+ _OpenDisplayDevice.TIMEOUT_CHUNK
1508
+ );
1509
+ const nextChunkData = stripCommandEcho(nextResponse, 64 /* READ_CONFIG */);
1510
+ tlvData.push(nextChunkData.subarray(2));
1511
+ currentLength += nextChunkData.length - 2;
1512
+ console.debug(`Received chunk, total: ${currentLength}/${totalLength} bytes`);
1513
+ }
1514
+ console.log(`Received complete TLV data: ${currentLength} bytes`);
1515
+ const completeData = new Uint8Array(currentLength);
1516
+ let offset = 0;
1517
+ for (const chunk of tlvData) {
1518
+ completeData.set(chunk, offset);
1519
+ offset += chunk.length;
1520
+ }
1521
+ this._config = parseConfigResponse(completeData);
1522
+ this._capabilities = this.extractCapabilitiesFromConfig();
1523
+ console.log(
1524
+ `Interrogated device: ${this.width}x${this.height}, ${ColorScheme4[this.colorScheme]}, rotation=${this.rotation}\xB0`
1525
+ );
1526
+ return this._config;
1527
+ }
1528
+ /**
1529
+ * Read firmware version from device.
1530
+ *
1531
+ * @returns FirmwareVersion with major, minor, and sha fields
1532
+ */
1533
+ async readFirmwareVersion() {
1534
+ this.ensureConnected();
1535
+ console.log("Reading firmware version");
1536
+ const cmd = buildReadFwVersionCommand();
1537
+ await this.connection.writeCommand(cmd);
1538
+ const response = await this.connection.readResponse(
1539
+ _OpenDisplayDevice.TIMEOUT_ACK
1540
+ );
1541
+ this._fwVersion = parseFirmwareVersion(response);
1542
+ console.log(
1543
+ `Firmware version: ${this._fwVersion.major}.${this._fwVersion.minor} (SHA: ${this._fwVersion.sha.substring(0, 8)}...)`
1544
+ );
1545
+ return this._fwVersion;
1546
+ }
1547
+ /**
1548
+ * Reboot the device.
1549
+ *
1550
+ * Sends a reboot command to the device, which will cause an immediate
1551
+ * system reset. The device will NOT send an ACK response - it simply
1552
+ * resets after a 100ms delay.
1553
+ *
1554
+ * Warning: The BLE connection will be forcibly terminated when the device
1555
+ * resets. This is expected behavior. The device will restart and begin
1556
+ * advertising again after the reset completes (typically within a few seconds).
1557
+ *
1558
+ * @throws {BLEConnectionError} If command cannot be sent
1559
+ */
1560
+ async reboot() {
1561
+ this.ensureConnected();
1562
+ console.log("Sending reboot command to device");
1563
+ const cmd = buildRebootCommand();
1564
+ await this.connection.writeCommand(cmd);
1565
+ console.log("Reboot command sent - device will reset (connection will drop)");
1566
+ }
1567
+ /**
1568
+ * Write configuration to device.
1569
+ *
1570
+ * Serializes the GlobalConfig to TLV binary format and writes it
1571
+ * to the device using the WRITE_CONFIG (0x0041) command with
1572
+ * automatic chunking for large configs.
1573
+ *
1574
+ * @param config - GlobalConfig to write to device
1575
+ * @throws {Error} If config serialization fails or exceeds size limit
1576
+ * @throws {BLEConnectionError} If write fails
1577
+ * @throws {ProtocolError} If device returns error response
1578
+ *
1579
+ * @example
1580
+ * ```typescript
1581
+ * // Read current config
1582
+ * const config = device.config;
1583
+ *
1584
+ * // Modify config
1585
+ * config.displays[0].rotation = 180;
1586
+ *
1587
+ * // Write back to device
1588
+ * await device.writeConfig(config);
1589
+ *
1590
+ * // Reboot to apply changes
1591
+ * await device.reboot();
1592
+ * ```
1593
+ */
1594
+ async writeConfig(config) {
1595
+ this.ensureConnected();
1596
+ console.log("Writing config to device");
1597
+ if (!config.system) {
1598
+ console.warn("Config missing system packet - device may not boot correctly");
1599
+ }
1600
+ if (!config.displays || config.displays.length === 0) {
1601
+ throw new Error("Config must have at least one display");
1602
+ }
1603
+ const missingPackets = [];
1604
+ if (!config.manufacturer) {
1605
+ missingPackets.push("manufacturer");
1606
+ }
1607
+ if (!config.power) {
1608
+ missingPackets.push("power");
1609
+ }
1610
+ if (missingPackets.length > 0) {
1611
+ console.warn(
1612
+ `Config missing optional packets: ${missingPackets.join(", ")}. Device may lose these settings.`
1613
+ );
1614
+ }
1615
+ const configData = serializeConfig(config);
1616
+ console.log(
1617
+ `Serialized config: ${configData.length} bytes (chunking ${configData.length > 200 ? "required" : "not needed"})`
1618
+ );
1619
+ const [firstCmd, chunkCmds] = buildWriteConfigCommand(configData);
1620
+ console.debug(`Sending first config chunk (${firstCmd.length} bytes)`);
1621
+ await this.connection.writeCommand(firstCmd);
1622
+ let response = await this.connection.readResponse(
1623
+ _OpenDisplayDevice.TIMEOUT_ACK
1624
+ );
1625
+ validateAckResponse(response, 65 /* WRITE_CONFIG */);
1626
+ for (let i = 0; i < chunkCmds.length; i++) {
1627
+ const chunkCmd = chunkCmds[i];
1628
+ console.debug(
1629
+ `Sending config chunk ${i + 1}/${chunkCmds.length} (${chunkCmd.length} bytes)`
1630
+ );
1631
+ await this.connection.writeCommand(chunkCmd);
1632
+ response = await this.connection.readResponse(
1633
+ _OpenDisplayDevice.TIMEOUT_ACK
1634
+ );
1635
+ validateAckResponse(response, 66 /* WRITE_CONFIG_CHUNK */);
1636
+ }
1637
+ console.log("Config written successfully");
1638
+ }
1639
+ /**
1640
+ * Upload image to device display.
1641
+ *
1642
+ * Automatically handles:
1643
+ * - Image resizing to display dimensions
1644
+ * - Dithering based on color scheme
1645
+ * - Encoding to device format
1646
+ * - Compression
1647
+ * - Direct write protocol
1648
+ *
1649
+ * @param imageData - Image as ImageData (from canvas or OffscreenCanvas)
1650
+ * @param options - Upload options
1651
+ * @throws {Error} If device not interrogated/configured
1652
+ * @throws {ProtocolError} If upload fails
1653
+ *
1654
+ * @example
1655
+ * ```typescript
1656
+ * const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
1657
+ * const ctx = canvas.getContext('2d')!;
1658
+ * const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
1659
+ *
1660
+ * await device.uploadImage(imageData, {
1661
+ * refreshMode: RefreshMode.FULL,
1662
+ * ditherMode: DitherMode.BURKES,
1663
+ * compress: true
1664
+ * });
1665
+ * ```
1666
+ */
1667
+ async uploadImage(imageData, options = {}) {
1668
+ this.ensureConnected();
1669
+ this.ensureCapabilities();
1670
+ const refreshMode = options.refreshMode ?? 0 /* FULL */;
1671
+ const ditherMode = options.ditherMode ?? DitherMode2.BURKES;
1672
+ const compress = options.compress ?? true;
1673
+ console.log(
1674
+ `Uploading image (${this.width}x${this.height}, ${ColorScheme4[this.colorScheme]})`
1675
+ );
1676
+ const encodedData = prepareImageForUpload(
1677
+ imageData,
1678
+ this.width,
1679
+ this.height,
1680
+ this.colorScheme,
1681
+ ditherMode
1682
+ );
1683
+ let compressedData = null;
1684
+ if (compress) {
1685
+ compressedData = compressImageData(encodedData, 6);
1686
+ if (compressedData.length < MAX_COMPRESSED_SIZE) {
1687
+ console.log(`Using compressed upload protocol (size: ${compressedData.length} bytes)`);
1688
+ await this.executeUpload({
1689
+ imageData: encodedData,
1690
+ refreshMode,
1691
+ useCompression: true,
1692
+ compressedData,
1693
+ uncompressedSize: encodedData.length
1694
+ });
1695
+ } else {
1696
+ console.log(
1697
+ `Compressed size exceeds ${MAX_COMPRESSED_SIZE} bytes, using uncompressed protocol`
1698
+ );
1699
+ await this.executeUpload({ imageData: encodedData, refreshMode });
1700
+ }
1701
+ } else {
1702
+ console.log("Compression disabled, using uncompressed protocol");
1703
+ await this.executeUpload({ imageData: encodedData, refreshMode });
1704
+ }
1705
+ console.log("Image upload complete");
1706
+ }
1707
+ /**
1708
+ * Execute image upload using compressed or uncompressed protocol.
1709
+ */
1710
+ async executeUpload(params) {
1711
+ const {
1712
+ imageData,
1713
+ refreshMode,
1714
+ useCompression = false,
1715
+ compressedData,
1716
+ uncompressedSize
1717
+ } = params;
1718
+ let startCmd;
1719
+ let remainingCompressed = null;
1720
+ if (useCompression && compressedData && uncompressedSize) {
1721
+ [startCmd, remainingCompressed] = buildDirectWriteStartCompressed(
1722
+ uncompressedSize,
1723
+ compressedData
1724
+ );
1725
+ } else {
1726
+ startCmd = buildDirectWriteStartUncompressed();
1727
+ }
1728
+ await this.connection.writeCommand(startCmd);
1729
+ let response = await this.connection.readResponse(
1730
+ _OpenDisplayDevice.TIMEOUT_ACK
1731
+ );
1732
+ validateAckResponse(response, 112 /* DIRECT_WRITE_START */);
1733
+ let autoCompleted = false;
1734
+ if (useCompression && remainingCompressed && remainingCompressed.length > 0) {
1735
+ autoCompleted = await this.sendDataChunks(remainingCompressed);
1736
+ } else if (!useCompression) {
1737
+ autoCompleted = await this.sendDataChunks(imageData);
1738
+ }
1739
+ if (!autoCompleted) {
1740
+ const endCmd = buildDirectWriteEndCommand(refreshMode);
1741
+ await this.connection.writeCommand(endCmd);
1742
+ response = await this.connection.readResponse(
1743
+ _OpenDisplayDevice.TIMEOUT_REFRESH
1744
+ );
1745
+ validateAckResponse(response, 114 /* DIRECT_WRITE_END */);
1746
+ }
1747
+ }
1748
+ /**
1749
+ * Send image data chunks with ACK handling.
1750
+ *
1751
+ * Sends image data in chunks via 0x0071 DATA commands. Handles:
1752
+ * - Timeout recovery when firmware starts display refresh
1753
+ * - Auto-completion detection (firmware sends 0x0072 END early)
1754
+ * - Progress logging
1755
+ *
1756
+ * @param imageData - Data to send in chunks
1757
+ * @returns True if device auto-completed (sent 0x0072 END early), false if all chunks sent normally
1758
+ * @throws {ProtocolError} If unexpected response received
1759
+ * @throws {BLETimeoutError} If no response within timeout
1760
+ */
1761
+ async sendDataChunks(imageData) {
1762
+ let bytesSent = 0;
1763
+ let chunksSent = 0;
1764
+ while (bytesSent < imageData.length) {
1765
+ const chunkStart = bytesSent;
1766
+ const chunkEnd = Math.min(chunkStart + CHUNK_SIZE, imageData.length);
1767
+ const chunkData = imageData.subarray(chunkStart, chunkEnd);
1768
+ const dataCmd = buildDirectWriteDataCommand(chunkData);
1769
+ await this.connection.writeCommand(dataCmd);
1770
+ bytesSent += chunkData.length;
1771
+ chunksSent++;
1772
+ let response;
1773
+ try {
1774
+ response = await this.connection.readResponse(
1775
+ _OpenDisplayDevice.TIMEOUT_ACK
1776
+ );
1777
+ } catch (error) {
1778
+ if (error instanceof BLETimeoutError) {
1779
+ console.log(
1780
+ `No response after chunk ${chunksSent} (${(bytesSent / imageData.length * 100).toFixed(1)}%), waiting for device refresh...`
1781
+ );
1782
+ response = await this.connection.readResponse(
1783
+ _OpenDisplayDevice.TIMEOUT_REFRESH
1784
+ );
1785
+ } else {
1786
+ throw error;
1787
+ }
1788
+ }
1789
+ const [command, isAck] = checkResponseType(response);
1790
+ if (command === 113 /* DIRECT_WRITE_DATA */) {
1791
+ } else if (command === 114 /* DIRECT_WRITE_END */) {
1792
+ console.log(
1793
+ `Received END response after chunk ${chunksSent} - device auto-completed`
1794
+ );
1795
+ return true;
1796
+ } else {
1797
+ throw new ProtocolError(
1798
+ `Unexpected response: ${CommandCode[command]} (0x${command.toString(16).padStart(4, "0")})`
1799
+ );
1800
+ }
1801
+ if (chunksSent % 50 === 0 || bytesSent >= imageData.length) {
1802
+ console.debug(
1803
+ `Sent ${bytesSent}/${imageData.length} bytes (${(bytesSent / imageData.length * 100).toFixed(1)}%)`
1804
+ );
1805
+ }
1806
+ }
1807
+ console.debug(`All data chunks sent (${chunksSent} chunks total)`);
1808
+ return false;
1809
+ }
1810
+ /**
1811
+ * Extract DeviceCapabilities from GlobalConfig.
1812
+ */
1813
+ extractCapabilitiesFromConfig() {
1814
+ if (!this._config) {
1815
+ throw new Error("No config available");
1816
+ }
1817
+ if (!this._config.displays || this._config.displays.length === 0) {
1818
+ throw new Error("Config has no display information");
1819
+ }
1820
+ const display = this._config.displays[0];
1821
+ return {
1822
+ width: display.pixelWidth,
1823
+ height: display.pixelHeight,
1824
+ colorScheme: display.colorScheme,
1825
+ rotation: display.rotation
1826
+ };
1827
+ }
1828
+ /**
1829
+ * Ensure device capabilities are available.
1830
+ */
1831
+ ensureCapabilities() {
1832
+ if (!this._capabilities) {
1833
+ throw new Error(
1834
+ "Device capabilities unknown - interrogate first or provide config/capabilities"
1835
+ );
1836
+ }
1837
+ return this._capabilities;
1838
+ }
1839
+ /**
1840
+ * Ensure device is connected.
1841
+ */
1842
+ ensureConnected() {
1843
+ if (!this.connection || !this.isConnected) {
1844
+ throw new BLEConnectionError("Not connected to device");
1845
+ }
1846
+ }
1847
+ };
1848
+
1849
+ // src/discovery.ts
1850
+ async function discoverDevices(namePrefix) {
1851
+ if (!navigator.bluetooth) {
1852
+ throw new Error(
1853
+ "Web Bluetooth API not supported in this browser. Try Chrome, Edge, or Opera on desktop/Android."
1854
+ );
1855
+ }
1856
+ const filters = [];
1857
+ if (namePrefix) {
1858
+ filters.push({
1859
+ services: [SERVICE_UUID],
1860
+ namePrefix
1861
+ });
1862
+ } else {
1863
+ filters.push({
1864
+ services: [SERVICE_UUID]
1865
+ });
1866
+ }
1867
+ filters.push({
1868
+ manufacturerData: [
1869
+ {
1870
+ companyIdentifier: MANUFACTURER_ID
1871
+ }
1872
+ ]
1873
+ });
1874
+ try {
1875
+ const device = await navigator.bluetooth.requestDevice({
1876
+ filters,
1877
+ optionalServices: [SERVICE_UUID]
1878
+ });
1879
+ console.log(`Selected device: ${device.name || "Unknown"}`);
1880
+ return device;
1881
+ } catch (error) {
1882
+ if (error instanceof Error) {
1883
+ throw new Error(`Device selection failed: ${error.message}`);
1884
+ }
1885
+ throw new Error("Device selection cancelled or failed");
1886
+ }
1887
+ }
1888
+
1889
+ // src/models/advertisement.ts
1890
+ function parseAdvertisement(data) {
1891
+ if (data.length < 11) {
1892
+ throw new Error(
1893
+ `Advertisement data too short: ${data.length} bytes (need 11)`
1894
+ );
1895
+ }
1896
+ const view = new DataView(data.buffer, data.byteOffset);
1897
+ const batteryMv = view.getUint16(7, true);
1898
+ const temperatureC = view.getInt8(9);
1899
+ const loopCounter = data[10];
1900
+ return {
1901
+ batteryMv,
1902
+ temperatureC,
1903
+ loopCounter
1904
+ };
1905
+ }
1906
+
1907
+ // src/index.ts
1908
+ import { ColorScheme as ColorScheme5, DitherMode as DitherMode3 } from "@opendisplay/epaper-dithering";
1909
+ export {
1910
+ BLEConnectionError,
1911
+ BLETimeoutError,
1912
+ BinaryInputs,
1913
+ BusType,
1914
+ ColorScheme5 as ColorScheme,
1915
+ ConfigParseError,
1916
+ DataBus,
1917
+ DisplayConfig,
1918
+ DitherMode3 as DitherMode,
1919
+ ICType,
1920
+ ImageEncodingError,
1921
+ InvalidResponseError,
1922
+ LedConfig,
1923
+ ManufacturerData,
1924
+ OpenDisplayDevice,
1925
+ OpenDisplayError,
1926
+ PowerMode,
1927
+ PowerOption,
1928
+ ProtocolError,
1929
+ RefreshMode,
1930
+ Rotation,
1931
+ SensorData,
1932
+ SystemConfig,
1933
+ discoverDevices,
1934
+ parseAdvertisement
1935
+ };
1936
+ //# sourceMappingURL=index.js.map