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