ultimatedarktower 2.5.0 → 4.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/README.md +84 -71
  3. package/dist/esm/index.mjs +1270 -268
  4. package/dist/src/UltimateDarkTower.d.ts +48 -6
  5. package/dist/src/UltimateDarkTower.js +115 -53
  6. package/dist/src/UltimateDarkTower.js.map +1 -1
  7. package/dist/src/adapters/NodeBluetoothAdapter.js +9 -5
  8. package/dist/src/adapters/NodeBluetoothAdapter.js.map +1 -1
  9. package/dist/src/adapters/WebBluetoothAdapter.js +11 -8
  10. package/dist/src/adapters/WebBluetoothAdapter.js.map +1 -1
  11. package/dist/src/index.d.ts +8 -0
  12. package/dist/src/index.js +34 -1
  13. package/dist/src/index.js.map +1 -1
  14. package/dist/src/sinks/IndexedDBSink.d.ts +26 -0
  15. package/dist/src/sinks/IndexedDBSink.js +165 -0
  16. package/dist/src/sinks/IndexedDBSink.js.map +1 -0
  17. package/dist/src/udtBleConnection.d.ts +28 -5
  18. package/dist/src/udtBleConnection.js +80 -13
  19. package/dist/src/udtBleConnection.js.map +1 -1
  20. package/dist/src/udtBluetoothAdapter.d.ts +6 -6
  21. package/dist/src/udtBluetoothAdapter.js.map +1 -1
  22. package/dist/src/udtCommandFactory.d.ts +6 -0
  23. package/dist/src/udtCommandFactory.js +26 -22
  24. package/dist/src/udtCommandFactory.js.map +1 -1
  25. package/dist/src/udtCommandQueue.d.ts +6 -3
  26. package/dist/src/udtCommandQueue.js +28 -5
  27. package/dist/src/udtCommandQueue.js.map +1 -1
  28. package/dist/src/udtConstants.d.ts +3 -8
  29. package/dist/src/udtConstants.js +2 -0
  30. package/dist/src/udtConstants.js.map +1 -1
  31. package/dist/src/udtDiagnostics.d.ts +122 -0
  32. package/dist/src/udtDiagnostics.js +228 -0
  33. package/dist/src/udtDiagnostics.js.map +1 -0
  34. package/dist/src/udtGameBoard.d.ts +38 -0
  35. package/dist/src/udtGameBoard.js +86 -0
  36. package/dist/src/udtGameBoard.js.map +1 -0
  37. package/dist/src/udtLogger.d.ts +16 -0
  38. package/dist/src/udtLogger.js +41 -2
  39. package/dist/src/udtLogger.js.map +1 -1
  40. package/dist/src/udtSeedParser.d.ts +124 -0
  41. package/dist/src/udtSeedParser.js +369 -0
  42. package/dist/src/udtSeedParser.js.map +1 -0
  43. package/dist/src/udtSystemRandom.d.ts +58 -0
  44. package/dist/src/udtSystemRandom.js +154 -0
  45. package/dist/src/udtSystemRandom.js.map +1 -0
  46. package/dist/src/udtTowerCommands.d.ts +4 -2
  47. package/dist/src/udtTowerCommands.js +24 -43
  48. package/dist/src/udtTowerCommands.js.map +1 -1
  49. package/dist/src/udtTowerResponse.d.ts +9 -5
  50. package/dist/src/udtTowerResponse.js +5 -6
  51. package/dist/src/udtTowerResponse.js.map +1 -1
  52. package/package.json +5 -1
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  var UART_SERVICE_UUID, UART_TX_CHARACTERISTIC_UUID, UART_RX_CHARACTERISTIC_UUID, TOWER_DEVICE_NAME, DIS_SERVICE_UUID, DIS_MANUFACTURER_NAME_UUID, DIS_MODEL_NUMBER_UUID, DIS_SERIAL_NUMBER_UUID, DIS_HARDWARE_REVISION_UUID, DIS_FIRMWARE_REVISION_UUID, DIS_SOFTWARE_REVISION_UUID, DIS_SYSTEM_ID_UUID, DIS_IEEE_REGULATORY_UUID, DIS_PNP_ID_UUID, TOWER_COMMAND_PACKET_SIZE, TOWER_STATE_DATA_SIZE, TOWER_COMMAND_HEADER_SIZE, TOWER_STATE_RESPONSE_MIN_LENGTH, TOWER_STATE_DATA_OFFSET, TOWER_COMMAND_TYPE_TOWER_STATE, DEFAULT_CONNECTION_MONITORING_FREQUENCY, DEFAULT_CONNECTION_MONITORING_TIMEOUT, DEFAULT_BATTERY_HEARTBEAT_TIMEOUT, BATTERY_STATUS_FREQUENCY, DEFAULT_RETRY_SEND_COMMAND_MAX, TOWER_SIDES_COUNT, TOWER_COMMANDS, TC, DRUM_PACKETS, GLYPHS, AUDIO_COMMAND_POS, SKULL_DROP_COUNT_POS, drumPositionCmds, LIGHT_EFFECTS, TOWER_LIGHT_SEQUENCES, TOWER_MESSAGES, VOLTAGE_LEVELS, TOWER_LAYERS, RING_LIGHT_POSITIONS, LEDGE_BASE_LIGHT_POSITIONS, LED_CHANNEL_LOOKUP, LAYER_TO_POSITION, LIGHT_INDEX_TO_DIRECTION, STATE_DATA_LENGTH, TOWER_AUDIO_LIBRARY, VOLUME_DESCRIPTIONS, VOLUME_ICONS;
31
31
  var init_udtConstants = __esm({
32
32
  "src/udtConstants.ts"() {
33
+ "use strict";
33
34
  UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
34
35
  UART_TX_CHARACTERISTIC_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
35
36
  UART_RX_CHARACTERISTIC_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
@@ -124,7 +125,9 @@ var init_udtConstants = __esm({
124
125
  rotationDrumTop: 16,
125
126
  rotationDrumMiddle: 17,
126
127
  rotationDrumBottom: 18,
127
- monthStarted: 19
128
+ monthStarted: 19,
129
+ wholeTowerBreathing: 20,
130
+ slowFlareThenFade: 21
128
131
  };
129
132
  TOWER_MESSAGES = {
130
133
  TOWER_STATE: { name: "Tower State", value: 0, critical: false },
@@ -361,160 +364,11 @@ var init_udtConstants = __esm({
361
364
  }
362
365
  });
363
366
 
364
- // src/udtTowerState.ts
365
- var udtTowerState_exports = {};
366
- __export(udtTowerState_exports, {
367
- LAYER_TO_POSITION: () => LAYER_TO_POSITION,
368
- LEDGE_BASE_LIGHT_POSITIONS: () => LEDGE_BASE_LIGHT_POSITIONS,
369
- LED_CHANNEL_LOOKUP: () => LED_CHANNEL_LOOKUP,
370
- LIGHT_INDEX_TO_DIRECTION: () => LIGHT_INDEX_TO_DIRECTION,
371
- RING_LIGHT_POSITIONS: () => RING_LIGHT_POSITIONS,
372
- STATE_DATA_LENGTH: () => STATE_DATA_LENGTH,
373
- TOWER_LAYERS: () => TOWER_LAYERS,
374
- isCalibrated: () => isCalibrated,
375
- rtdt_pack_state: () => rtdt_pack_state,
376
- rtdt_unpack_state: () => rtdt_unpack_state
377
- });
378
- function rtdt_unpack_state(data) {
379
- const state = {
380
- drum: [
381
- { jammed: false, calibrated: false, position: 0, playSound: false, reverse: false },
382
- { jammed: false, calibrated: false, position: 0, playSound: false, reverse: false },
383
- { jammed: false, calibrated: false, position: 0, playSound: false, reverse: false }
384
- ],
385
- layer: [
386
- { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
387
- { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
388
- { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
389
- { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
390
- { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
391
- { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] }
392
- ],
393
- audio: { sample: 0, loop: false, volume: 0 },
394
- beam: { count: 0, fault: false },
395
- led_sequence: 0
396
- };
397
- state.drum[0].jammed = !!(data[0] & 8);
398
- state.drum[0].calibrated = !!(data[0] & 16);
399
- state.drum[1].jammed = !!(data[1] & 1);
400
- state.drum[1].calibrated = !!(data[1] & 2);
401
- state.drum[2].jammed = !!(data[1] & 32);
402
- state.drum[2].calibrated = !!(data[1] & 64);
403
- state.drum[0].position = (data[0] & 6) >> 1;
404
- state.drum[1].position = (data[0] & 192) >> 6;
405
- state.drum[2].position = (data[1] & 24) >> 3;
406
- state.drum[0].playSound = !!(data[0] & 1);
407
- state.drum[1].playSound = !!(data[0] & 32);
408
- state.drum[2].playSound = !!(data[1] & 4);
409
- state.layer[0].light[0].effect = (data[2] & 224) >> 5;
410
- state.layer[0].light[0].loop = !!(data[2] & 16);
411
- state.layer[0].light[1].effect = (data[2] & 14) >> 1;
412
- state.layer[0].light[1].loop = !!(data[2] & 1);
413
- state.layer[0].light[2].effect = (data[3] & 224) >> 5;
414
- state.layer[0].light[2].loop = !!(data[3] & 16);
415
- state.layer[0].light[3].effect = (data[3] & 14) >> 1;
416
- state.layer[0].light[3].loop = !!(data[3] & 1);
417
- state.layer[1].light[0].effect = (data[4] & 224) >> 5;
418
- state.layer[1].light[0].loop = !!(data[4] & 16);
419
- state.layer[1].light[1].effect = (data[4] & 14) >> 1;
420
- state.layer[1].light[1].loop = !!(data[4] & 1);
421
- state.layer[1].light[2].effect = (data[5] & 224) >> 5;
422
- state.layer[1].light[2].loop = !!(data[5] & 16);
423
- state.layer[1].light[3].effect = (data[5] & 14) >> 1;
424
- state.layer[1].light[3].loop = !!(data[5] & 1);
425
- state.layer[2].light[0].effect = (data[6] & 224) >> 5;
426
- state.layer[2].light[0].loop = !!(data[6] & 16);
427
- state.layer[2].light[1].effect = (data[6] & 14) >> 1;
428
- state.layer[2].light[1].loop = !!(data[6] & 1);
429
- state.layer[2].light[2].effect = (data[7] & 224) >> 5;
430
- state.layer[2].light[2].loop = !!(data[7] & 16);
431
- state.layer[2].light[3].effect = (data[7] & 14) >> 1;
432
- state.layer[2].light[3].loop = !!(data[7] & 1);
433
- state.layer[3].light[0].effect = (data[8] & 224) >> 5;
434
- state.layer[3].light[0].loop = !!(data[8] & 16);
435
- state.layer[3].light[1].effect = (data[8] & 14) >> 1;
436
- state.layer[3].light[1].loop = !!(data[8] & 1);
437
- state.layer[3].light[2].effect = (data[9] & 224) >> 5;
438
- state.layer[3].light[2].loop = !!(data[9] & 16);
439
- state.layer[3].light[3].effect = (data[9] & 14) >> 1;
440
- state.layer[3].light[3].loop = !!(data[9] & 1);
441
- state.layer[4].light[0].effect = (data[10] & 224) >> 5;
442
- state.layer[4].light[0].loop = !!(data[10] & 16);
443
- state.layer[4].light[1].effect = (data[10] & 14) >> 1;
444
- state.layer[4].light[1].loop = !!(data[10] & 1);
445
- state.layer[4].light[2].effect = (data[11] & 224) >> 5;
446
- state.layer[4].light[2].loop = !!(data[11] & 16);
447
- state.layer[4].light[3].effect = (data[11] & 14) >> 1;
448
- state.layer[4].light[3].loop = !!(data[11] & 1);
449
- state.layer[5].light[0].effect = (data[12] & 224) >> 5;
450
- state.layer[5].light[0].loop = !!(data[12] & 16);
451
- state.layer[5].light[1].effect = (data[12] & 14) >> 1;
452
- state.layer[5].light[1].loop = !!(data[12] & 1);
453
- state.layer[5].light[2].effect = (data[13] & 224) >> 5;
454
- state.layer[5].light[2].loop = !!(data[13] & 16);
455
- state.layer[5].light[3].effect = (data[13] & 14) >> 1;
456
- state.layer[5].light[3].loop = !!(data[13] & 1);
457
- state.audio.sample = data[14] & 127;
458
- state.audio.loop = !!(data[14] & 128);
459
- state.beam.count = data[15] << 8 | data[16];
460
- state.beam.fault = !!(data[17] & 1);
461
- state.drum[0].reverse = !!(data[17] & 2);
462
- state.drum[1].reverse = !!(data[17] & 4);
463
- state.drum[2].reverse = !!(data[17] & 8);
464
- state.audio.volume = (data[17] & 240) >> 4;
465
- state.led_sequence = data[18];
466
- return state;
467
- }
468
- function rtdt_pack_state(data, len, state) {
469
- if (!data || len < STATE_DATA_LENGTH)
470
- return false;
471
- data.fill(0, 0, STATE_DATA_LENGTH);
472
- data[0] |= (state.drum[0].playSound ? 1 : 0) | (state.drum[0].position & 3) << 1 | (state.drum[0].jammed ? 1 : 0) << 3 | (state.drum[0].calibrated ? 1 : 0) << 4 | (state.drum[1].playSound ? 1 : 0) << 5 | (state.drum[1].position & 3) << 6;
473
- data[1] |= (state.drum[1].jammed ? 1 : 0) | (state.drum[1].calibrated ? 1 : 0) << 1 | (state.drum[2].playSound ? 1 : 0) << 2 | (state.drum[2].position & 3) << 3 | (state.drum[2].jammed ? 1 : 0) << 5 | (state.drum[2].calibrated ? 1 : 0) << 6;
474
- data[2] |= state.layer[0].light[0].effect << 5 | (state.layer[0].light[0].loop ? 1 : 0) << 4;
475
- data[2] |= state.layer[0].light[1].effect << 1 | (state.layer[0].light[1].loop ? 1 : 0);
476
- data[3] |= state.layer[0].light[2].effect << 5 | (state.layer[0].light[2].loop ? 1 : 0) << 4;
477
- data[3] |= state.layer[0].light[3].effect << 1 | (state.layer[0].light[3].loop ? 1 : 0);
478
- data[4] |= state.layer[1].light[0].effect << 5 | (state.layer[1].light[0].loop ? 1 : 0) << 4;
479
- data[4] |= state.layer[1].light[1].effect << 1 | (state.layer[1].light[1].loop ? 1 : 0);
480
- data[5] |= state.layer[1].light[2].effect << 5 | (state.layer[1].light[2].loop ? 1 : 0) << 4;
481
- data[5] |= state.layer[1].light[3].effect << 1 | (state.layer[1].light[3].loop ? 1 : 0);
482
- data[6] |= state.layer[2].light[0].effect << 5 | (state.layer[2].light[0].loop ? 1 : 0) << 4;
483
- data[6] |= state.layer[2].light[1].effect << 1 | (state.layer[2].light[1].loop ? 1 : 0);
484
- data[7] |= state.layer[2].light[2].effect << 5 | (state.layer[2].light[2].loop ? 1 : 0) << 4;
485
- data[7] |= state.layer[2].light[3].effect << 1 | (state.layer[2].light[3].loop ? 1 : 0);
486
- data[8] |= state.layer[3].light[0].effect << 5 | (state.layer[3].light[0].loop ? 1 : 0) << 4;
487
- data[8] |= state.layer[3].light[1].effect << 1 | (state.layer[3].light[1].loop ? 1 : 0);
488
- data[9] |= state.layer[3].light[2].effect << 5 | (state.layer[3].light[2].loop ? 1 : 0) << 4;
489
- data[9] |= state.layer[3].light[3].effect << 1 | (state.layer[3].light[3].loop ? 1 : 0);
490
- data[10] |= state.layer[4].light[0].effect << 5 | (state.layer[4].light[0].loop ? 1 : 0) << 4;
491
- data[10] |= state.layer[4].light[1].effect << 1 | (state.layer[4].light[1].loop ? 1 : 0);
492
- data[11] |= state.layer[4].light[2].effect << 5 | (state.layer[4].light[2].loop ? 1 : 0) << 4;
493
- data[11] |= state.layer[4].light[3].effect << 1 | (state.layer[4].light[3].loop ? 1 : 0);
494
- data[12] |= state.layer[5].light[0].effect << 5 | (state.layer[5].light[0].loop ? 1 : 0) << 4;
495
- data[12] |= state.layer[5].light[1].effect << 1 | (state.layer[5].light[1].loop ? 1 : 0);
496
- data[13] |= state.layer[5].light[2].effect << 5 | (state.layer[5].light[2].loop ? 1 : 0) << 4;
497
- data[13] |= state.layer[5].light[3].effect << 1 | (state.layer[5].light[3].loop ? 1 : 0);
498
- data[14] = state.audio.sample | (state.audio.loop ? 1 : 0) << 7;
499
- data[15] = state.beam.count >> 8;
500
- data[16] = state.beam.count & 255;
501
- data[17] = state.audio.volume << 4 | (state.beam.fault ? 1 : 0) | (state.drum[0].reverse ? 1 : 0) << 1 | (state.drum[1].reverse ? 1 : 0) << 2 | (state.drum[2].reverse ? 1 : 0) << 3;
502
- data[18] = state.led_sequence;
503
- return true;
504
- }
505
- function isCalibrated(state) {
506
- return state.drum.every((drum) => drum.calibrated);
507
- }
508
- var init_udtTowerState = __esm({
509
- "src/udtTowerState.ts"() {
510
- init_udtConstants();
511
- }
512
- });
513
-
514
367
  // src/udtBluetoothAdapter.ts
515
368
  var BluetoothError, BluetoothConnectionError, BluetoothDeviceNotFoundError, BluetoothUserCancelledError, BluetoothTimeoutError;
516
369
  var init_udtBluetoothAdapter = __esm({
517
370
  "src/udtBluetoothAdapter.ts"() {
371
+ "use strict";
518
372
  BluetoothError = class extends Error {
519
373
  constructor(message, originalError) {
520
374
  super(message);
@@ -557,6 +411,7 @@ __export(WebBluetoothAdapter_exports, {
557
411
  var WebBluetoothAdapter;
558
412
  var init_WebBluetoothAdapter = __esm({
559
413
  "src/adapters/WebBluetoothAdapter.ts"() {
414
+ "use strict";
560
415
  init_udtConstants();
561
416
  init_udtBluetoothAdapter();
562
417
  WebBluetoothAdapter = class {
@@ -599,9 +454,10 @@ var init_WebBluetoothAdapter = __esm({
599
454
  await this.rxCharacteristic.startNotifications();
600
455
  this.boundOnCharacteristicValueChanged = (event) => {
601
456
  const target = event.target;
602
- const receivedData = new Uint8Array(target.value.byteLength);
603
- for (let i = 0; i < target.value.byteLength; i++) {
604
- receivedData[i] = target.value.getUint8(i);
457
+ const dataView = target.value;
458
+ const receivedData = new Uint8Array(dataView.byteLength);
459
+ for (let i = 0; i < dataView.byteLength; i++) {
460
+ receivedData[i] = dataView.getUint8(i);
605
461
  }
606
462
  if (this.characteristicCallback) {
607
463
  this.characteristicCallback(receivedData);
@@ -615,11 +471,12 @@ var init_WebBluetoothAdapter = __esm({
615
471
  if (error instanceof BluetoothDeviceNotFoundError || error instanceof BluetoothUserCancelledError || error instanceof BluetoothConnectionError) {
616
472
  throw error;
617
473
  }
618
- const errorMsg = error?.message ?? String(error);
474
+ const errorMsg = error instanceof Error ? error.message : String(error);
475
+ const errorName = error instanceof Error ? error.name : "";
619
476
  if (errorMsg.includes("User cancelled")) {
620
477
  throw new BluetoothUserCancelledError("User cancelled device selection", error);
621
478
  }
622
- if (errorMsg.includes("not found") || error?.name === "NotFoundError") {
479
+ if (errorMsg.includes("not found") || errorName === "NotFoundError") {
623
480
  throw new BluetoothDeviceNotFoundError("Device not found", error);
624
481
  }
625
482
  throw new BluetoothConnectionError(`Failed to connect: ${errorMsg}`, error);
@@ -629,11 +486,11 @@ var init_WebBluetoothAdapter = __esm({
629
486
  if (!this.device) {
630
487
  return;
631
488
  }
632
- if (this.device.gatt.connected) {
489
+ if (this.device.gatt?.connected) {
633
490
  if (this.boundOnDeviceDisconnected) {
634
491
  this.device.removeEventListener("gattserverdisconnected", this.boundOnDeviceDisconnected);
635
492
  }
636
- await this.device.gatt.disconnect();
493
+ await this.device.gatt?.disconnect();
637
494
  }
638
495
  this.device = null;
639
496
  this.txCharacteristic = null;
@@ -719,6 +576,7 @@ __export(NodeBluetoothAdapter_exports, {
719
576
  var noble, NodeBluetoothAdapter;
720
577
  var init_NodeBluetoothAdapter = __esm({
721
578
  "src/adapters/NodeBluetoothAdapter.ts"() {
579
+ "use strict";
722
580
  init_udtBluetoothAdapter();
723
581
  init_udtConstants();
724
582
  try {
@@ -748,8 +606,9 @@ var init_NodeBluetoothAdapter = __esm({
748
606
  try {
749
607
  await noble.waitForPoweredOnAsync();
750
608
  } catch (error) {
609
+ const msg = error instanceof Error ? error.message : String(error);
751
610
  throw new BluetoothConnectionError(
752
- `Bluetooth adapter not ready: ${error.message}`,
611
+ `Bluetooth adapter not ready: ${msg}`,
753
612
  error
754
613
  );
755
614
  }
@@ -784,10 +643,10 @@ var init_NodeBluetoothAdapter = __esm({
784
643
  this.allCharacteristics = characteristics;
785
644
  this.txCharacteristic = characteristics.find(
786
645
  (c) => this.normalizeUuid(c.uuid) === txUuid
787
- );
646
+ ) ?? null;
788
647
  this.rxCharacteristic = characteristics.find(
789
648
  (c) => this.normalizeUuid(c.uuid) === rxUuid
790
- );
649
+ ) ?? null;
791
650
  if (!this.txCharacteristic || !this.rxCharacteristic) {
792
651
  throw new BluetoothConnectionError(
793
652
  "TX or RX characteristic not found on device"
@@ -805,8 +664,9 @@ var init_NodeBluetoothAdapter = __esm({
805
664
  if (error instanceof BluetoothDeviceNotFoundError || error instanceof BluetoothConnectionError || error instanceof BluetoothTimeoutError) {
806
665
  throw error;
807
666
  }
667
+ const msg = error instanceof Error ? error.message : String(error);
808
668
  throw new BluetoothConnectionError(
809
- `Connection failed: ${error.message}`,
669
+ `Connection failed: ${msg}`,
810
670
  error
811
671
  );
812
672
  }
@@ -844,8 +704,9 @@ var init_NodeBluetoothAdapter = __esm({
844
704
  const buffer = Buffer.from(data);
845
705
  await this.txCharacteristic.writeAsync(buffer, false);
846
706
  } catch (error) {
707
+ const msg = error instanceof Error ? error.message : String(error);
847
708
  throw new BluetoothConnectionError(
848
- `Write failed: ${error.message}`,
709
+ `Write failed: ${msg}`,
849
710
  error
850
711
  );
851
712
  }
@@ -1003,7 +864,139 @@ var init_NodeBluetoothAdapter = __esm({
1003
864
 
1004
865
  // src/UltimateDarkTower.ts
1005
866
  init_udtConstants();
1006
- init_udtTowerState();
867
+
868
+ // src/udtTowerState.ts
869
+ init_udtConstants();
870
+ function rtdt_unpack_state(data) {
871
+ const state = {
872
+ drum: [
873
+ { jammed: false, calibrated: false, position: 0, playSound: false, reverse: false },
874
+ { jammed: false, calibrated: false, position: 0, playSound: false, reverse: false },
875
+ { jammed: false, calibrated: false, position: 0, playSound: false, reverse: false }
876
+ ],
877
+ layer: [
878
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
879
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
880
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
881
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
882
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
883
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] }
884
+ ],
885
+ audio: { sample: 0, loop: false, volume: 0 },
886
+ beam: { count: 0, fault: false },
887
+ led_sequence: 0
888
+ };
889
+ state.drum[0].jammed = !!(data[0] & 8);
890
+ state.drum[0].calibrated = !!(data[0] & 16);
891
+ state.drum[1].jammed = !!(data[1] & 1);
892
+ state.drum[1].calibrated = !!(data[1] & 2);
893
+ state.drum[2].jammed = !!(data[1] & 32);
894
+ state.drum[2].calibrated = !!(data[1] & 64);
895
+ state.drum[0].position = (data[0] & 6) >> 1;
896
+ state.drum[1].position = (data[0] & 192) >> 6;
897
+ state.drum[2].position = (data[1] & 24) >> 3;
898
+ state.drum[0].playSound = !!(data[0] & 1);
899
+ state.drum[1].playSound = !!(data[0] & 32);
900
+ state.drum[2].playSound = !!(data[1] & 4);
901
+ state.layer[0].light[0].effect = (data[2] & 224) >> 5;
902
+ state.layer[0].light[0].loop = !!(data[2] & 16);
903
+ state.layer[0].light[1].effect = (data[2] & 14) >> 1;
904
+ state.layer[0].light[1].loop = !!(data[2] & 1);
905
+ state.layer[0].light[2].effect = (data[3] & 224) >> 5;
906
+ state.layer[0].light[2].loop = !!(data[3] & 16);
907
+ state.layer[0].light[3].effect = (data[3] & 14) >> 1;
908
+ state.layer[0].light[3].loop = !!(data[3] & 1);
909
+ state.layer[1].light[0].effect = (data[4] & 224) >> 5;
910
+ state.layer[1].light[0].loop = !!(data[4] & 16);
911
+ state.layer[1].light[1].effect = (data[4] & 14) >> 1;
912
+ state.layer[1].light[1].loop = !!(data[4] & 1);
913
+ state.layer[1].light[2].effect = (data[5] & 224) >> 5;
914
+ state.layer[1].light[2].loop = !!(data[5] & 16);
915
+ state.layer[1].light[3].effect = (data[5] & 14) >> 1;
916
+ state.layer[1].light[3].loop = !!(data[5] & 1);
917
+ state.layer[2].light[0].effect = (data[6] & 224) >> 5;
918
+ state.layer[2].light[0].loop = !!(data[6] & 16);
919
+ state.layer[2].light[1].effect = (data[6] & 14) >> 1;
920
+ state.layer[2].light[1].loop = !!(data[6] & 1);
921
+ state.layer[2].light[2].effect = (data[7] & 224) >> 5;
922
+ state.layer[2].light[2].loop = !!(data[7] & 16);
923
+ state.layer[2].light[3].effect = (data[7] & 14) >> 1;
924
+ state.layer[2].light[3].loop = !!(data[7] & 1);
925
+ state.layer[3].light[0].effect = (data[8] & 224) >> 5;
926
+ state.layer[3].light[0].loop = !!(data[8] & 16);
927
+ state.layer[3].light[1].effect = (data[8] & 14) >> 1;
928
+ state.layer[3].light[1].loop = !!(data[8] & 1);
929
+ state.layer[3].light[2].effect = (data[9] & 224) >> 5;
930
+ state.layer[3].light[2].loop = !!(data[9] & 16);
931
+ state.layer[3].light[3].effect = (data[9] & 14) >> 1;
932
+ state.layer[3].light[3].loop = !!(data[9] & 1);
933
+ state.layer[4].light[0].effect = (data[10] & 224) >> 5;
934
+ state.layer[4].light[0].loop = !!(data[10] & 16);
935
+ state.layer[4].light[1].effect = (data[10] & 14) >> 1;
936
+ state.layer[4].light[1].loop = !!(data[10] & 1);
937
+ state.layer[4].light[2].effect = (data[11] & 224) >> 5;
938
+ state.layer[4].light[2].loop = !!(data[11] & 16);
939
+ state.layer[4].light[3].effect = (data[11] & 14) >> 1;
940
+ state.layer[4].light[3].loop = !!(data[11] & 1);
941
+ state.layer[5].light[0].effect = (data[12] & 224) >> 5;
942
+ state.layer[5].light[0].loop = !!(data[12] & 16);
943
+ state.layer[5].light[1].effect = (data[12] & 14) >> 1;
944
+ state.layer[5].light[1].loop = !!(data[12] & 1);
945
+ state.layer[5].light[2].effect = (data[13] & 224) >> 5;
946
+ state.layer[5].light[2].loop = !!(data[13] & 16);
947
+ state.layer[5].light[3].effect = (data[13] & 14) >> 1;
948
+ state.layer[5].light[3].loop = !!(data[13] & 1);
949
+ state.audio.sample = data[14] & 127;
950
+ state.audio.loop = !!(data[14] & 128);
951
+ state.beam.count = data[15] << 8 | data[16];
952
+ state.beam.fault = !!(data[17] & 1);
953
+ state.drum[0].reverse = !!(data[17] & 2);
954
+ state.drum[1].reverse = !!(data[17] & 4);
955
+ state.drum[2].reverse = !!(data[17] & 8);
956
+ state.audio.volume = (data[17] & 240) >> 4;
957
+ state.led_sequence = data[18];
958
+ return state;
959
+ }
960
+ function rtdt_pack_state(data, len, state) {
961
+ if (!data || len < STATE_DATA_LENGTH)
962
+ return false;
963
+ data.fill(0, 0, STATE_DATA_LENGTH);
964
+ data[0] |= (state.drum[0].playSound ? 1 : 0) | (state.drum[0].position & 3) << 1 | (state.drum[0].jammed ? 1 : 0) << 3 | (state.drum[0].calibrated ? 1 : 0) << 4 | (state.drum[1].playSound ? 1 : 0) << 5 | (state.drum[1].position & 3) << 6;
965
+ data[1] |= (state.drum[1].jammed ? 1 : 0) | (state.drum[1].calibrated ? 1 : 0) << 1 | (state.drum[2].playSound ? 1 : 0) << 2 | (state.drum[2].position & 3) << 3 | (state.drum[2].jammed ? 1 : 0) << 5 | (state.drum[2].calibrated ? 1 : 0) << 6;
966
+ data[2] |= state.layer[0].light[0].effect << 5 | (state.layer[0].light[0].loop ? 1 : 0) << 4;
967
+ data[2] |= state.layer[0].light[1].effect << 1 | (state.layer[0].light[1].loop ? 1 : 0);
968
+ data[3] |= state.layer[0].light[2].effect << 5 | (state.layer[0].light[2].loop ? 1 : 0) << 4;
969
+ data[3] |= state.layer[0].light[3].effect << 1 | (state.layer[0].light[3].loop ? 1 : 0);
970
+ data[4] |= state.layer[1].light[0].effect << 5 | (state.layer[1].light[0].loop ? 1 : 0) << 4;
971
+ data[4] |= state.layer[1].light[1].effect << 1 | (state.layer[1].light[1].loop ? 1 : 0);
972
+ data[5] |= state.layer[1].light[2].effect << 5 | (state.layer[1].light[2].loop ? 1 : 0) << 4;
973
+ data[5] |= state.layer[1].light[3].effect << 1 | (state.layer[1].light[3].loop ? 1 : 0);
974
+ data[6] |= state.layer[2].light[0].effect << 5 | (state.layer[2].light[0].loop ? 1 : 0) << 4;
975
+ data[6] |= state.layer[2].light[1].effect << 1 | (state.layer[2].light[1].loop ? 1 : 0);
976
+ data[7] |= state.layer[2].light[2].effect << 5 | (state.layer[2].light[2].loop ? 1 : 0) << 4;
977
+ data[7] |= state.layer[2].light[3].effect << 1 | (state.layer[2].light[3].loop ? 1 : 0);
978
+ data[8] |= state.layer[3].light[0].effect << 5 | (state.layer[3].light[0].loop ? 1 : 0) << 4;
979
+ data[8] |= state.layer[3].light[1].effect << 1 | (state.layer[3].light[1].loop ? 1 : 0);
980
+ data[9] |= state.layer[3].light[2].effect << 5 | (state.layer[3].light[2].loop ? 1 : 0) << 4;
981
+ data[9] |= state.layer[3].light[3].effect << 1 | (state.layer[3].light[3].loop ? 1 : 0);
982
+ data[10] |= state.layer[4].light[0].effect << 5 | (state.layer[4].light[0].loop ? 1 : 0) << 4;
983
+ data[10] |= state.layer[4].light[1].effect << 1 | (state.layer[4].light[1].loop ? 1 : 0);
984
+ data[11] |= state.layer[4].light[2].effect << 5 | (state.layer[4].light[2].loop ? 1 : 0) << 4;
985
+ data[11] |= state.layer[4].light[3].effect << 1 | (state.layer[4].light[3].loop ? 1 : 0);
986
+ data[12] |= state.layer[5].light[0].effect << 5 | (state.layer[5].light[0].loop ? 1 : 0) << 4;
987
+ data[12] |= state.layer[5].light[1].effect << 1 | (state.layer[5].light[1].loop ? 1 : 0);
988
+ data[13] |= state.layer[5].light[2].effect << 5 | (state.layer[5].light[2].loop ? 1 : 0) << 4;
989
+ data[13] |= state.layer[5].light[3].effect << 1 | (state.layer[5].light[3].loop ? 1 : 0);
990
+ data[14] = state.audio.sample | (state.audio.loop ? 1 : 0) << 7;
991
+ data[15] = state.beam.count >> 8;
992
+ data[16] = state.beam.count & 255;
993
+ data[17] = state.audio.volume << 4 | (state.beam.fault ? 1 : 0) | (state.drum[0].reverse ? 1 : 0) << 1 | (state.drum[1].reverse ? 1 : 0) << 2 | (state.drum[2].reverse ? 1 : 0) << 3;
994
+ data[18] = state.led_sequence;
995
+ return true;
996
+ }
997
+ function isCalibrated(state) {
998
+ return state.drum.every((drum) => drum.calibrated);
999
+ }
1007
1000
 
1008
1001
  // src/udtHelpers.ts
1009
1002
  init_udtConstants();
@@ -1134,10 +1127,28 @@ var DOMOutput = class {
1134
1127
  write(level, message, timestamp) {
1135
1128
  if (!this.container) return;
1136
1129
  this.allEntries.push({ level, message, timestamp });
1130
+ let removedEntries = false;
1137
1131
  while (this.allEntries.length > this.maxLines) {
1138
1132
  this.allEntries.shift();
1133
+ removedEntries = true;
1134
+ }
1135
+ if (removedEntries) {
1136
+ this.refreshDisplay();
1137
+ return;
1138
+ }
1139
+ const enabledLevels = this.getEnabledLevelsFromCheckboxes();
1140
+ if (enabledLevels.has(level)) {
1141
+ const textFilter = this.getTextFilter();
1142
+ if (!textFilter || message.toLowerCase().includes(textFilter.toLowerCase())) {
1143
+ const timeStr = timestamp.toLocaleTimeString();
1144
+ const logLine = document.createElement("div");
1145
+ logLine.className = `log-line log-${level}`;
1146
+ logLine.textContent = `[${timeStr}] ${message}`;
1147
+ this.container.appendChild(logLine);
1148
+ this.container.scrollTop = this.container.scrollHeight;
1149
+ this.updateBufferSizeDisplay();
1150
+ }
1139
1151
  }
1140
- this.refreshDisplay();
1141
1152
  }
1142
1153
  refreshDisplay() {
1143
1154
  if (!this.container) return;
@@ -1223,11 +1234,19 @@ var Logger = class _Logger {
1223
1234
  constructor() {
1224
1235
  this.outputs = [];
1225
1236
  this.enabledLevels = /* @__PURE__ */ new Set(["all"]);
1237
+ this.diagnosticsTarget = null;
1226
1238
  this.outputs.push(new ConsoleOutput());
1227
1239
  }
1228
1240
  static {
1229
1241
  this.instance = null;
1230
1242
  }
1243
+ /**
1244
+ * Bridge warn/error log lines into a diagnostics recorder so they appear
1245
+ * in the disconnect incident ring buffer in correct chronological order.
1246
+ */
1247
+ setDiagnosticsTarget(target) {
1248
+ this.diagnosticsTarget = target;
1249
+ }
1231
1250
  static getInstance() {
1232
1251
  if (!_Logger.instance) {
1233
1252
  _Logger.instance = new _Logger();
@@ -1237,6 +1256,9 @@ var Logger = class _Logger {
1237
1256
  addOutput(output) {
1238
1257
  this.outputs.push(output);
1239
1258
  }
1259
+ clearOutputs() {
1260
+ this.outputs = [];
1261
+ }
1240
1262
  setMinLevel(level) {
1241
1263
  this.enabledLevels = /* @__PURE__ */ new Set([level]);
1242
1264
  }
@@ -1279,6 +1301,13 @@ var Logger = class _Logger {
1279
1301
  console.error("Logger output error:", error);
1280
1302
  }
1281
1303
  });
1304
+ if ((level === "warn" || level === "error") && this.diagnosticsTarget?.enabled) {
1305
+ try {
1306
+ this.diagnosticsTarget.recordLog(level, message, context);
1307
+ } catch (error) {
1308
+ console.error("Diagnostics log bridge error:", error);
1309
+ }
1310
+ }
1282
1311
  }
1283
1312
  debug(message, context) {
1284
1313
  this.log("debug", message, context);
@@ -1403,7 +1432,7 @@ var TowerResponseProcessor = class {
1403
1432
  const cmdKey = cmdKeys.find((key) => TOWER_MESSAGES[key].value === cmdValue);
1404
1433
  if (!cmdKey) {
1405
1434
  logger.warn(`Unknown command received from tower: ${cmdValue} (0x${cmdValue.toString(16)})`, "TowerResponseProcessor");
1406
- return { cmdKey: void 0, command: { name: "Unknown Command", value: cmdValue } };
1435
+ return { cmdKey: void 0, command: { name: "Unknown Command", value: cmdValue, critical: false } };
1407
1436
  }
1408
1437
  const command = TOWER_MESSAGES[cmdKey];
1409
1438
  return { cmdKey, command };
@@ -1446,12 +1475,11 @@ var TowerResponseProcessor = class {
1446
1475
  * @returns {boolean} Whether this response should be logged
1447
1476
  */
1448
1477
  shouldLogResponse(cmdKey, logConfig) {
1449
- const logAll = logConfig["LOG_ALL"];
1450
- let canLogThisResponse = logConfig[cmdKey] || logAll;
1451
1478
  if (!cmdKey) {
1452
- canLogThisResponse = true;
1479
+ return true;
1453
1480
  }
1454
- return canLogThisResponse;
1481
+ const logAll = logConfig["LOG_ALL"];
1482
+ return logConfig[cmdKey] || logAll;
1455
1483
  }
1456
1484
  /**
1457
1485
  * Checks if a command is a battery response type.
@@ -1471,9 +1499,6 @@ var TowerResponseProcessor = class {
1471
1499
  }
1472
1500
  };
1473
1501
 
1474
- // src/udtBleConnection.ts
1475
- init_udtTowerState();
1476
-
1477
1502
  // src/udtBluetoothAdapterFactory.ts
1478
1503
  var BluetoothPlatform = /* @__PURE__ */ ((BluetoothPlatform3) => {
1479
1504
  BluetoothPlatform3["WEB"] = "web";
@@ -1528,7 +1553,12 @@ var BluetoothAdapterFactory = class {
1528
1553
 
1529
1554
  // src/udtBleConnection.ts
1530
1555
  var UdtBleConnection = class {
1531
- constructor(logger2, callbacks, adapter) {
1556
+ constructor(logger2, callbacks, adapter, recorder) {
1557
+ this.recorder = null;
1558
+ // Snapshot providers wired by UltimateDarkTower so the recorder can capture
1559
+ // higher-level state (command queue, tower state, broken seals) at the
1560
+ // moment a disconnect cause fires.
1561
+ this.snapshotProviders = null;
1532
1562
  // Connection state
1533
1563
  this.isConnected = false;
1534
1564
  this.isDisposed = false;
@@ -1549,11 +1579,11 @@ var UdtBleConnection = class {
1549
1579
  // When true, verifies connection before triggering disconnection on heartbeat timeout
1550
1580
  // Tower state
1551
1581
  this.towerSkullDropCount = -1;
1552
- this.lastBatteryNotification = 0;
1582
+ this.lastBatteryLog = 0;
1553
1583
  this.lastBatteryPercentage = "";
1554
- this.batteryNotifyFrequency = 15 * 1e3;
1555
- this.batteryNotifyOnValueChangeOnly = false;
1556
- this.batteryNotifyEnabled = true;
1584
+ this.batteryLogFrequency = 15 * 1e3;
1585
+ this.batteryLogOnChangeOnly = false;
1586
+ this.batteryLogEnabled = true;
1557
1587
  // Device information
1558
1588
  this.deviceInformation = {};
1559
1589
  // Logging configuration
@@ -1573,6 +1603,7 @@ var UdtBleConnection = class {
1573
1603
  this.logger = logger2;
1574
1604
  this.callbacks = callbacks;
1575
1605
  this.responseProcessor = new TowerResponseProcessor();
1606
+ this.recorder = recorder ?? null;
1576
1607
  this.bluetoothAdapter = adapter || BluetoothAdapterFactory.create("auto" /* AUTO */);
1577
1608
  this.bluetoothAdapter.onCharacteristicValueChanged((data) => {
1578
1609
  this.onRxData(data);
@@ -1584,6 +1615,35 @@ var UdtBleConnection = class {
1584
1615
  this.bleAvailabilityChange(available);
1585
1616
  });
1586
1617
  }
1618
+ setDiagnosticsSnapshotProviders(providers) {
1619
+ this.snapshotProviders = providers;
1620
+ }
1621
+ /**
1622
+ * Record a disconnect incident with the recorder. Public so higher layers
1623
+ * (e.g. UltimateDarkTower's beforeunload handler) can synthesize causes
1624
+ * like 'page_unload' that aren't tied to a specific BLE detection path.
1625
+ */
1626
+ recordIncidentPublic(cause) {
1627
+ return this.recordIncident(cause);
1628
+ }
1629
+ recordIncident(cause) {
1630
+ if (!this.recorder || !this.recorder.enabled) return null;
1631
+ const queueSnapshot = this.snapshotProviders?.commandQueue() ?? {
1632
+ queueLength: 0,
1633
+ isProcessing: false,
1634
+ currentCommand: null
1635
+ };
1636
+ const towerState = this.snapshotProviders?.towerState() ?? null;
1637
+ const brokenSeals = this.snapshotProviders?.brokenSeals() ?? [];
1638
+ return this.recorder.recordIncident({
1639
+ cause,
1640
+ connectionStatus: this.getConnectionStatus(),
1641
+ deviceInformation: this.getDeviceInformation(),
1642
+ commandQueue: queueSnapshot,
1643
+ towerState,
1644
+ brokenSeals
1645
+ });
1646
+ }
1587
1647
  async connect() {
1588
1648
  if (this.isDisposed) {
1589
1649
  throw new Error("UdtBleConnection instance has been disposed and cannot reconnect");
@@ -1598,6 +1658,7 @@ var UdtBleConnection = class {
1598
1658
  this.isConnected = true;
1599
1659
  this.lastSuccessfulCommand = Date.now();
1600
1660
  this.lastBatteryHeartbeat = Date.now();
1661
+ this.recorder?.beginSession();
1601
1662
  await this.readDeviceInformation();
1602
1663
  if (this.enableConnectionMonitoring) {
1603
1664
  this.startConnectionMonitoring();
@@ -1606,11 +1667,14 @@ var UdtBleConnection = class {
1606
1667
  } catch (error) {
1607
1668
  this.logger.error(`Tower Connection Error: ${error}`, "[UDT][BLE]");
1608
1669
  this.isConnected = false;
1609
- this.callbacks.onTowerDisconnect();
1670
+ throw error;
1610
1671
  }
1611
1672
  }
1612
1673
  async disconnect() {
1613
1674
  this.stopConnectionMonitoring();
1675
+ if (this.isConnected) {
1676
+ this.recordIncident("user_initiated");
1677
+ }
1614
1678
  if (this.bluetoothAdapter.isConnected()) {
1615
1679
  await this.bluetoothAdapter.disconnect();
1616
1680
  this.logger.info("Tower disconnected", "[UDT]");
@@ -1622,6 +1686,7 @@ var UdtBleConnection = class {
1622
1686
  * Used by UdtTowerCommands instead of direct characteristic access.
1623
1687
  */
1624
1688
  async writeCommand(command) {
1689
+ this.recorder?.recordCommandPayload("cmd_sent", command, { len: command.length });
1625
1690
  return await this.bluetoothAdapter.writeCharacteristic(command);
1626
1691
  }
1627
1692
  /**
@@ -1632,7 +1697,10 @@ var UdtBleConnection = class {
1632
1697
  this.lastSuccessfulCommand = Date.now();
1633
1698
  const { cmdKey } = this.responseProcessor.getTowerCommand(receivedData[0]);
1634
1699
  const isBattery = this.responseProcessor.isBatteryResponse(cmdKey);
1635
- const shouldLogCommand = this.logTowerResponses && this.responseProcessor.shouldLogResponse(cmdKey, this.logTowerResponseConfig) && (!isBattery || this.batteryNotifyEnabled);
1700
+ if (this.recorder?.enabled && !isBattery) {
1701
+ this.recorder.recordCommandPayload("cmd_response", receivedData, { cmdKey, len: receivedData.length });
1702
+ }
1703
+ const shouldLogCommand = this.logTowerResponses && this.responseProcessor.shouldLogResponse(cmdKey, this.logTowerResponseConfig) && !isBattery;
1636
1704
  if (shouldLogCommand) {
1637
1705
  this.logger.info(`${cmdKey}`, "[UDT][BLE][RCVD]");
1638
1706
  }
@@ -1646,15 +1714,16 @@ var UdtBleConnection = class {
1646
1714
  this.lastBatteryHeartbeat = Date.now();
1647
1715
  const millivolts = getMilliVoltsFromTowerResponse(receivedData);
1648
1716
  const batteryPercentage = milliVoltsToPercentage(millivolts);
1717
+ this.recorder?.recordBattery(millivolts, milliVoltsToPercentageNumber(millivolts));
1649
1718
  const didBatteryLevelChange = this.lastBatteryPercentage !== "" && this.lastBatteryPercentage !== batteryPercentage;
1650
- const batteryNotifyFrequencyPassed = Date.now() - this.lastBatteryNotification >= this.batteryNotifyFrequency;
1651
- const shouldNotify = this.batteryNotifyEnabled && (this.batteryNotifyOnValueChangeOnly ? didBatteryLevelChange || this.lastBatteryPercentage === "" : batteryNotifyFrequencyPassed);
1652
- if (shouldNotify) {
1719
+ const batteryLogFrequencyPassed = Date.now() - this.lastBatteryLog >= this.batteryLogFrequency;
1720
+ const shouldLog = this.batteryLogEnabled && (this.batteryLogOnChangeOnly ? didBatteryLevelChange || this.lastBatteryPercentage === "" : batteryLogFrequencyPassed);
1721
+ if (shouldLog) {
1653
1722
  this.logger.info(`${this.responseProcessor.commandToString(receivedData).join(" ")}`, "[UDT][BLE]");
1654
- this.lastBatteryNotification = Date.now();
1723
+ this.lastBatteryLog = Date.now();
1655
1724
  this.lastBatteryPercentage = batteryPercentage;
1656
- this.callbacks.onBatteryLevelNotify(millivolts);
1657
1725
  }
1726
+ this.callbacks.onBatteryLevelNotify(millivolts);
1658
1727
  } else {
1659
1728
  if (this.callbacks.onTowerResponse) {
1660
1729
  this.callbacks.onTowerResponse(receivedData);
@@ -1665,17 +1734,20 @@ var UdtBleConnection = class {
1665
1734
  const dataSkullDropCount = receivedData[SKULL_DROP_COUNT_POS];
1666
1735
  const state = rtdt_unpack_state(receivedData);
1667
1736
  this.logger.debug(`Tower State: ${JSON.stringify(state)} `, "[UDT][BLE]");
1737
+ this.recorder?.recordEvent("tower_state_response");
1668
1738
  if (this.performingCalibration) {
1669
1739
  this.performingCalibration = false;
1670
1740
  this.performingLongCommand = false;
1671
1741
  this.lastBatteryHeartbeat = Date.now();
1672
1742
  this.callbacks.onCalibrationComplete();
1673
1743
  this.logger.info("Tower calibration complete", "[UDT]");
1744
+ this.recorder?.recordEvent("calibration_complete");
1674
1745
  }
1675
1746
  if (dataSkullDropCount !== this.towerSkullDropCount) {
1676
1747
  if (dataSkullDropCount) {
1677
1748
  this.callbacks.onSkullDrop(dataSkullDropCount);
1678
1749
  this.logger.info(`Skull drop detected: app:${this.towerSkullDropCount < 0 ? "empty" : this.towerSkullDropCount} tower:${dataSkullDropCount}`, "[UDT]");
1750
+ this.recorder?.recordEvent("skull_drop", { count: dataSkullDropCount });
1679
1751
  } else {
1680
1752
  this.logger.info(`Skull count reset to ${dataSkullDropCount}`, "[UDT]");
1681
1753
  }
@@ -1701,11 +1773,15 @@ var UdtBleConnection = class {
1701
1773
  this.logger.info("Bluetooth availability changed", "[UDT][BLE]");
1702
1774
  if (!available && this.isConnected) {
1703
1775
  this.logger.warn("Bluetooth became unavailable - handling disconnection", "[UDT][BLE]");
1776
+ this.recordIncident("bt_unavailable");
1704
1777
  this.handleDisconnection();
1705
1778
  }
1706
1779
  }
1707
1780
  onTowerDeviceDisconnected() {
1708
1781
  this.logger.warn("Tower device disconnected unexpectedly", "[UDT][BLE]");
1782
+ if (this.isConnected) {
1783
+ this.recordIncident("adapter_event");
1784
+ }
1709
1785
  this.handleDisconnection();
1710
1786
  }
1711
1787
  handleDisconnection() {
@@ -1738,6 +1814,7 @@ var UdtBleConnection = class {
1738
1814
  }
1739
1815
  if (!this.bluetoothAdapter.isGattConnected()) {
1740
1816
  this.logger.warn("GATT connection lost detected during health check", "[UDT][BLE]");
1817
+ this.recordIncident("gatt_health_check");
1741
1818
  this.handleDisconnection();
1742
1819
  return;
1743
1820
  }
@@ -1755,12 +1832,17 @@ var UdtBleConnection = class {
1755
1832
  this.logger.info("Verifying tower connection status before triggering disconnection...", "[UDT][BLE]");
1756
1833
  if (this.bluetoothAdapter.isGattConnected()) {
1757
1834
  this.logger.info("GATT connection still available - heartbeat timeout may be temporary", "[UDT][BLE]");
1835
+ this.recorder?.recordEvent("heartbeat_late", {
1836
+ sinceMs: timeSinceLastBatteryHeartbeat,
1837
+ threshold: timeoutThreshold
1838
+ });
1758
1839
  this.lastBatteryHeartbeat = Date.now();
1759
1840
  this.logger.info("Reset battery heartbeat timer - will monitor for another timeout period", "[UDT][BLE]");
1760
1841
  return;
1761
1842
  }
1762
1843
  }
1763
1844
  this.logger.warn("Tower possibly disconnected due to battery depletion or power loss", "[UDT][BLE]");
1845
+ this.recordIncident("heartbeat_timeout");
1764
1846
  this.handleDisconnection();
1765
1847
  return;
1766
1848
  }
@@ -1768,6 +1850,7 @@ var UdtBleConnection = class {
1768
1850
  const timeSinceLastResponse = Date.now() - this.lastSuccessfulCommand;
1769
1851
  if (timeSinceLastResponse > this.connectionTimeoutThreshold) {
1770
1852
  this.logger.warn("General connection timeout detected - no responses received", "[UDT][BLE]");
1853
+ this.recordIncident("response_timeout");
1771
1854
  this.handleDisconnection();
1772
1855
  }
1773
1856
  }
@@ -1844,7 +1927,6 @@ var UdtBleConnection = class {
1844
1927
 
1845
1928
  // src/udtCommandFactory.ts
1846
1929
  init_udtConstants();
1847
- init_udtTowerState();
1848
1930
  var UdtCommandFactory = class {
1849
1931
  /**
1850
1932
  * Creates a rotation command packet for positioning tower drums.
@@ -1866,8 +1948,7 @@ var UdtCommandFactory = class {
1866
1948
  */
1867
1949
  createSoundCommand(soundIndex) {
1868
1950
  const soundCommand = new Uint8Array(TOWER_COMMAND_PACKET_SIZE);
1869
- const sound = Number("0x" + Number(soundIndex).toString(16).padStart(2, "0"));
1870
- soundCommand[AUDIO_COMMAND_POS] = sound;
1951
+ soundCommand[AUDIO_COMMAND_POS] = soundIndex & 255;
1871
1952
  return soundCommand;
1872
1953
  }
1873
1954
  /**
@@ -1887,7 +1968,7 @@ var UdtCommandFactory = class {
1887
1968
  * @returns 20-byte command packet (command type + 19-byte state data)
1888
1969
  */
1889
1970
  createStatefulCommand(currentState, modifications) {
1890
- const newState = currentState ? { ...currentState } : this.createEmptyTowerState();
1971
+ const newState = currentState ? this.deepCopyTowerState(currentState) : this.createEmptyTowerState();
1891
1972
  if (modifications.drum) {
1892
1973
  modifications.drum.forEach((drum, index) => {
1893
1974
  if (drum && newState.drum[index]) {
@@ -1929,17 +2010,10 @@ var UdtCommandFactory = class {
1929
2010
  * @returns 20-byte command packet
1930
2011
  */
1931
2012
  createStatefulLEDCommand(currentState, layerIndex, lightIndex, effect, loop = false) {
1932
- const modifications = {};
1933
- if (!modifications.layer) {
1934
- modifications.layer = [];
1935
- }
1936
- if (!modifications.layer[layerIndex]) {
1937
- modifications.layer[layerIndex] = { light: [] };
1938
- }
1939
- if (!modifications.layer[layerIndex].light) {
1940
- modifications.layer[layerIndex].light = [];
1941
- }
1942
- modifications.layer[layerIndex].light[lightIndex] = { effect, loop };
2013
+ const layer = [];
2014
+ layer[layerIndex] = { light: [] };
2015
+ layer[layerIndex].light[lightIndex] = { effect, loop };
2016
+ const modifications = { layer };
1943
2017
  modifications.audio = { sample: 0, loop: false, volume: 0 };
1944
2018
  return this.createStatefulCommand(currentState, modifications);
1945
2019
  }
@@ -2033,17 +2107,15 @@ var UdtCommandFactory = class {
2033
2107
  * @returns 20-byte command packet
2034
2108
  */
2035
2109
  createStatefulDrumCommand(currentState, drumIndex, position, playSound = false) {
2036
- const modifications = {};
2037
- if (!modifications.drum) {
2038
- modifications.drum = [];
2039
- }
2040
- modifications.drum[drumIndex] = {
2110
+ const drum = [];
2111
+ drum[drumIndex] = {
2041
2112
  jammed: false,
2042
2113
  calibrated: true,
2043
2114
  position,
2044
2115
  playSound,
2045
2116
  reverse: false
2046
2117
  };
2118
+ const modifications = { drum };
2047
2119
  modifications.audio = { sample: 0, loop: false, volume: 0 };
2048
2120
  return this.createStatefulCommand(currentState, modifications);
2049
2121
  }
@@ -2087,6 +2159,22 @@ var UdtCommandFactory = class {
2087
2159
  led_sequence: 0
2088
2160
  };
2089
2161
  }
2162
+ /**
2163
+ * Creates a deep copy of a TowerState to avoid mutating the original.
2164
+ * @param state - The tower state to copy
2165
+ * @returns A new TowerState with all nested objects copied
2166
+ */
2167
+ deepCopyTowerState(state) {
2168
+ return {
2169
+ drum: state.drum.map((d) => ({ ...d })),
2170
+ layer: state.layer.map((l) => ({
2171
+ light: l.light.map((lt) => ({ ...lt }))
2172
+ })),
2173
+ audio: { ...state.audio },
2174
+ beam: { ...state.beam },
2175
+ led_sequence: state.led_sequence
2176
+ };
2177
+ }
2090
2178
  //#endregion
2091
2179
  };
2092
2180
 
@@ -2095,8 +2183,7 @@ init_udtConstants();
2095
2183
 
2096
2184
  // src/udtCommandQueue.ts
2097
2185
  var CommandQueue = class {
2098
- // 30 seconds
2099
- constructor(logger2, sendCommandFn) {
2186
+ constructor(logger2, sendCommandFn, recorder) {
2100
2187
  this.logger = logger2;
2101
2188
  this.sendCommandFn = sendCommandFn;
2102
2189
  this.queue = [];
@@ -2104,6 +2191,12 @@ var CommandQueue = class {
2104
2191
  this.timeoutHandle = null;
2105
2192
  this.isProcessing = false;
2106
2193
  this.timeoutMs = 3e4;
2194
+ // 30 seconds
2195
+ this.recorder = null;
2196
+ this.recorder = recorder ?? null;
2197
+ }
2198
+ setRecorder(recorder) {
2199
+ this.recorder = recorder;
2107
2200
  }
2108
2201
  /**
2109
2202
  * Enqueue a command for processing
@@ -2120,6 +2213,11 @@ var CommandQueue = class {
2120
2213
  };
2121
2214
  this.queue.push(queuedCommand);
2122
2215
  this.logger.debug(`Command queued: ${description || "unnamed"} (queue size: ${this.queue.length})`, "[UDT]");
2216
+ this.recorder?.recordEvent("cmd_enqueued", {
2217
+ id: queuedCommand.id,
2218
+ description,
2219
+ queueDepth: this.queue.length
2220
+ });
2123
2221
  if (!this.isProcessing) {
2124
2222
  this.processNext();
2125
2223
  }
@@ -2143,6 +2241,11 @@ var CommandQueue = class {
2143
2241
  await this.sendCommandFn(command);
2144
2242
  } catch (error) {
2145
2243
  this.clearTimeout();
2244
+ this.recorder?.recordEvent("cmd_failed", {
2245
+ id,
2246
+ description,
2247
+ error: error?.message ?? String(error)
2248
+ });
2146
2249
  this.currentCommand = null;
2147
2250
  this.isProcessing = false;
2148
2251
  reject(error);
@@ -2168,11 +2271,18 @@ var CommandQueue = class {
2168
2271
  */
2169
2272
  onTimeout() {
2170
2273
  if (this.currentCommand) {
2171
- const { description, id } = this.currentCommand;
2274
+ const { description, id, timestamp } = this.currentCommand;
2172
2275
  this.logger.warn(`Command timeout after ${this.timeoutMs}ms: ${description || id}`, "[UDT]");
2173
- this.currentCommand.resolve();
2276
+ this.recorder?.recordEvent("cmd_timeout", {
2277
+ id,
2278
+ description,
2279
+ ageMs: Date.now() - timestamp,
2280
+ queueDepth: this.queue.length
2281
+ });
2282
+ const reject = this.currentCommand.reject;
2174
2283
  this.currentCommand = null;
2175
2284
  this.isProcessing = false;
2285
+ reject(new Error(`Command timeout after ${this.timeoutMs}ms: ${description || id}`));
2176
2286
  this.processNext();
2177
2287
  }
2178
2288
  }
@@ -2223,7 +2333,8 @@ var UdtTowerCommands = class {
2223
2333
  this.deps = dependencies;
2224
2334
  this.commandQueue = new CommandQueue(
2225
2335
  this.deps.logger,
2226
- (command) => this.sendTowerCommandDirect(command)
2336
+ (command) => this.sendTowerCommandDirect(command),
2337
+ this.deps.recorder
2227
2338
  );
2228
2339
  }
2229
2340
  /**
@@ -2254,7 +2365,7 @@ var UdtTowerCommands = class {
2254
2365
  this.deps.bleConnection.lastSuccessfulCommand = Date.now();
2255
2366
  } catch (error) {
2256
2367
  this.deps.logger.error(`command send error: ${error}`, "[UDT][CMD]");
2257
- const errorMsg = error?.message ?? new String(error);
2368
+ const errorMsg = error?.message ?? String(error);
2258
2369
  const wasCancelled = errorMsg.includes("User cancelled");
2259
2370
  const maxRetriesReached = this.deps.retrySendCommandCount.value >= this.deps.retrySendCommandMax;
2260
2371
  const isDisconnected = errorMsg.includes("Cannot read properties of null") || errorMsg.includes("GATT Server is disconnected") || errorMsg.includes("Device is not connected") || errorMsg.includes("BluetoothConnectionError") || !this.deps.bleConnection.isConnected;
@@ -2266,9 +2377,9 @@ var UdtTowerCommands = class {
2266
2377
  if (!maxRetriesReached && this.deps.bleConnection.isConnected && !wasCancelled) {
2267
2378
  this.deps.logger.info(`retrying tower command attempt ${this.deps.retrySendCommandCount.value + 1}`, "[UDT][CMD]");
2268
2379
  this.deps.retrySendCommandCount.value++;
2269
- setTimeout(() => {
2270
- this.sendTowerCommandDirect(command);
2271
- }, 250 * this.deps.retrySendCommandCount.value);
2380
+ const delay = 250 * this.deps.retrySendCommandCount.value;
2381
+ await new Promise((resolve) => setTimeout(resolve, delay));
2382
+ return await this.sendTowerCommandDirect(command);
2272
2383
  } else {
2273
2384
  this.deps.retrySendCommandCount.value = 0;
2274
2385
  }
@@ -2282,6 +2393,7 @@ var UdtTowerCommands = class {
2282
2393
  async calibrate() {
2283
2394
  if (!this.deps.bleConnection.performingCalibration) {
2284
2395
  this.deps.logger.info("Performing Tower Calibration", "[UDT][CMD]");
2396
+ this.deps.recorder?.recordEvent("calibration_started");
2285
2397
  await this.sendTowerCommand(new Uint8Array([TOWER_COMMANDS.calibration]), "calibrate");
2286
2398
  this.deps.bleConnection.performingCalibration = true;
2287
2399
  this.deps.bleConnection.performingLongCommand = true;
@@ -2316,9 +2428,13 @@ var UdtTowerCommands = class {
2316
2428
  this.deps.logDetail && this.deps.logger.debug(`Light Parameter ${JSON.stringify(lights)}`, "[UDT][CMD]");
2317
2429
  this.deps.logger.info("Sending light commands", "[UDT][CMD]");
2318
2430
  const layerCommands = this.mapLightsToLayerCommands(lights);
2319
- for (const { layerIndex, lightIndex, effect } of layerCommands) {
2320
- await this.setLEDStateful(layerIndex, lightIndex, effect);
2431
+ const currentState = this.deps.getCurrentTowerState();
2432
+ for (const { layerIndex, lightIndex, effect, loop } of layerCommands) {
2433
+ currentState.layer[layerIndex].light[lightIndex] = { effect, loop };
2321
2434
  }
2435
+ const command = this.deps.commandFactory.createStatefulCommand(currentState, {});
2436
+ this.deps.setTowerState(currentState, "lights");
2437
+ await this.sendTowerCommand(command, "lights");
2322
2438
  }
2323
2439
  /**
2324
2440
  * Maps the Lights object to layer/light index commands for setLEDStateful.
@@ -2332,7 +2448,7 @@ var UdtTowerCommands = class {
2332
2448
  const layerIndex = this.getTowerLayerForLevel(doorwayLight.level);
2333
2449
  const lightIndex = this.getLightIndexForSide(doorwayLight.position);
2334
2450
  const effect = LIGHT_EFFECTS[doorwayLight.style] || LIGHT_EFFECTS.off;
2335
- commands.push({ layerIndex, lightIndex, effect, loop: true });
2451
+ commands.push({ layerIndex, lightIndex, effect, loop: effect !== LIGHT_EFFECTS.off });
2336
2452
  }
2337
2453
  }
2338
2454
  if (lights.ledge) {
@@ -2340,7 +2456,7 @@ var UdtTowerCommands = class {
2340
2456
  const layerIndex = TOWER_LAYERS.LEDGE;
2341
2457
  const lightIndex = this.getLedgeLightIndexForSide(ledgeLight.position);
2342
2458
  const effect = LIGHT_EFFECTS[ledgeLight.style] || LIGHT_EFFECTS.off;
2343
- commands.push({ layerIndex, lightIndex, effect, loop: false });
2459
+ commands.push({ layerIndex, lightIndex, effect, loop: effect !== LIGHT_EFFECTS.off });
2344
2460
  }
2345
2461
  }
2346
2462
  if (lights.base) {
@@ -2348,7 +2464,7 @@ var UdtTowerCommands = class {
2348
2464
  const layerIndex = baseLight.position.level === "top" || baseLight.position.level === "b" ? TOWER_LAYERS.BASE2 : TOWER_LAYERS.BASE1;
2349
2465
  const lightIndex = this.getBaseLightIndexForSide(baseLight.position.side);
2350
2466
  const effect = LIGHT_EFFECTS[baseLight.style] || LIGHT_EFFECTS.off;
2351
- commands.push({ layerIndex, lightIndex, effect, loop: false });
2467
+ commands.push({ layerIndex, lightIndex, effect, loop: effect !== LIGHT_EFFECTS.off });
2352
2468
  }
2353
2469
  }
2354
2470
  return commands;
@@ -2554,8 +2670,7 @@ var UdtTowerCommands = class {
2554
2670
  };
2555
2671
  const command = this.deps.commandFactory.createStatefulCommand(currentState, modifications);
2556
2672
  await this.sendTowerCommand(command, "resetTowerSkullCount");
2557
- const updatedState = { ...currentState };
2558
- updatedState.beam.count = 0;
2673
+ const updatedState = { ...currentState, beam: { ...currentState.beam, count: 0 } };
2559
2674
  this.deps.setTowerState(updatedState, "resetTowerSkullCount");
2560
2675
  }
2561
2676
  /**
@@ -2572,29 +2687,8 @@ var UdtTowerCommands = class {
2572
2687
  stateWithVolume.audio = { sample: 0, loop: false, volume: actualVolume };
2573
2688
  await this.sendTowerStateStateful(stateWithVolume);
2574
2689
  }
2575
- this.deps.logger.info("Playing tower seal sound", "[UDT]");
2576
- await this.playSoundStateful(TOWER_AUDIO_LIBRARY.TowerSeal.value, false, actualVolume);
2577
- const sideCorners = {
2578
- north: ["northeast", "northwest"],
2579
- east: ["northeast", "southeast"],
2580
- south: ["southeast", "southwest"],
2581
- west: ["southwest", "northwest"]
2582
- };
2583
- const ledgeLights = sideCorners[seal.side].map((corner) => ({
2584
- position: corner,
2585
- style: "on"
2586
- }));
2587
- const doorwayLights = [{
2588
- level: seal.level,
2589
- position: seal.side,
2590
- style: "breatheFast"
2591
- }];
2592
- const lights = {
2593
- ledge: ledgeLights,
2594
- doorway: doorwayLights
2595
- };
2596
- this.deps.logger.info(`Breaking seal ${seal.level}-${seal.side} - lighting ledges and doorways with breath effect`, "[UDT]");
2597
- await this.lights(lights);
2690
+ this.deps.logger.info(`Breaking seal ${seal.level}-${seal.side} - triggering firmware sealReveal animation`, "[UDT]");
2691
+ await this.lightOverrides(TOWER_LIGHT_SEQUENCES.sealReveal, TOWER_AUDIO_LIBRARY.TowerSeal.value);
2598
2692
  }
2599
2693
  /**
2600
2694
  * Randomly rotates specified tower levels to random positions.
@@ -2795,9 +2889,201 @@ var UdtTowerCommands = class {
2795
2889
  }
2796
2890
  };
2797
2891
 
2892
+ // src/udtDiagnostics.ts
2893
+ var RING_BUFFER_SIZE = 500;
2894
+ var RING_BUFFER_DRAIN = 50;
2895
+ var BATTERY_HISTORY_SIZE = 60;
2896
+ var PAYLOAD_MAX_BYTES = 32;
2897
+ var LIBRARY_VERSION = "3.0.0";
2898
+ function detectPlatform() {
2899
+ if (typeof window !== "undefined" && typeof window.navigator !== "undefined") {
2900
+ return "web";
2901
+ }
2902
+ if (typeof process !== "undefined" && process.versions?.node) {
2903
+ return "node";
2904
+ }
2905
+ return "custom";
2906
+ }
2907
+ function makeId() {
2908
+ const g = globalThis;
2909
+ if (g.crypto && typeof g.crypto.randomUUID === "function") {
2910
+ try {
2911
+ return g.crypto.randomUUID();
2912
+ } catch {
2913
+ }
2914
+ }
2915
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
2916
+ }
2917
+ function bytesToHex(data, maxBytes = PAYLOAD_MAX_BYTES) {
2918
+ const slice = data.length > maxBytes ? data.subarray(0, maxBytes) : data;
2919
+ let out = "";
2920
+ for (let i = 0; i < slice.length; i++) {
2921
+ out += slice[i].toString(16).padStart(2, "0");
2922
+ }
2923
+ if (data.length > maxBytes) {
2924
+ out += `..(+${data.length - maxBytes})`;
2925
+ }
2926
+ return out;
2927
+ }
2928
+ var InMemorySink = class {
2929
+ constructor(maxIncidents = 50) {
2930
+ this.incidents = [];
2931
+ this.maxIncidents = maxIncidents;
2932
+ }
2933
+ onIncident(report) {
2934
+ this.incidents.push(report);
2935
+ if (this.incidents.length > this.maxIncidents) {
2936
+ this.incidents.splice(0, this.incidents.length - this.maxIncidents);
2937
+ }
2938
+ }
2939
+ list() {
2940
+ return [...this.incidents];
2941
+ }
2942
+ get(incidentId) {
2943
+ return this.incidents.find((r) => r.incidentId === incidentId);
2944
+ }
2945
+ clear() {
2946
+ this.incidents = [];
2947
+ }
2948
+ };
2949
+ var UdtDiagnosticsRecorder = class {
2950
+ constructor(config) {
2951
+ this.events = [];
2952
+ this.batteryHistory = [];
2953
+ this.sessionId = "";
2954
+ this.connectedAt = null;
2955
+ this.lastIncident = null;
2956
+ this.enabled = config.enabled;
2957
+ this.capturePayloads = config.capturePayloads ?? false;
2958
+ this.sinks = config.sinks ?? [];
2959
+ }
2960
+ setSinks(sinks) {
2961
+ this.sinks = sinks;
2962
+ }
2963
+ getSinks() {
2964
+ return [...this.sinks];
2965
+ }
2966
+ addSink(sink) {
2967
+ this.sinks.push(sink);
2968
+ }
2969
+ /** Mark the start of a connected session. Called from BLE connect path. */
2970
+ beginSession() {
2971
+ if (!this.enabled) return;
2972
+ this.sessionId = makeId();
2973
+ this.connectedAt = Date.now();
2974
+ this.events = [];
2975
+ this.batteryHistory = [];
2976
+ this.recordEvent("connect");
2977
+ }
2978
+ recordEvent(kind, data) {
2979
+ if (!this.enabled) return;
2980
+ const event = { t: Date.now(), kind };
2981
+ if (data) event.data = data;
2982
+ this.events.push(event);
2983
+ if (this.events.length > RING_BUFFER_SIZE) {
2984
+ this.events.splice(0, RING_BUFFER_DRAIN);
2985
+ }
2986
+ for (const sink of this.sinks) {
2987
+ if (sink.onEvent) {
2988
+ try {
2989
+ sink.onEvent(event);
2990
+ } catch (e) {
2991
+ console.error("Diagnostics sink onEvent error:", e);
2992
+ }
2993
+ }
2994
+ }
2995
+ }
2996
+ recordCommandPayload(kind, data, extra) {
2997
+ if (!this.enabled) return;
2998
+ const payload = { ...extra };
2999
+ if (this.capturePayloads) {
3000
+ payload.payloadHex = bytesToHex(data);
3001
+ payload.payloadLen = data.length;
3002
+ }
3003
+ this.recordEvent(kind, payload);
3004
+ }
3005
+ recordBattery(mv, pct) {
3006
+ if (!this.enabled) return;
3007
+ this.batteryHistory.push({ t: Date.now(), mv, pct });
3008
+ if (this.batteryHistory.length > BATTERY_HISTORY_SIZE) {
3009
+ this.batteryHistory.splice(0, this.batteryHistory.length - BATTERY_HISTORY_SIZE);
3010
+ }
3011
+ }
3012
+ /** Forwards a log line into the events ring (called by Logger when bridged). */
3013
+ recordLog(level, message, context) {
3014
+ if (!this.enabled) return;
3015
+ this.recordEvent("log", { level, message, context });
3016
+ }
3017
+ /**
3018
+ * Capture an incident snapshot and dispatch to sinks.
3019
+ * Must be called BEFORE the BLE layer clears state.
3020
+ */
3021
+ recordIncident(inputs) {
3022
+ if (!this.enabled) return null;
3023
+ const triggeredAt = Date.now();
3024
+ const inFlightCommandAgeMs = inputs.commandQueue.currentCommand ? triggeredAt - inputs.commandQueue.currentCommand.timestamp : null;
3025
+ const report = {
3026
+ schemaVersion: 1,
3027
+ incidentId: makeId(),
3028
+ sessionId: this.sessionId || makeId(),
3029
+ cause: inputs.cause,
3030
+ triggeredAt,
3031
+ connectedAt: this.connectedAt,
3032
+ sessionDurationMs: this.connectedAt ? triggeredAt - this.connectedAt : 0,
3033
+ connectionStatus: { ...inputs.connectionStatus },
3034
+ deviceInformation: { ...inputs.deviceInformation },
3035
+ commandQueue: {
3036
+ queueLength: inputs.commandQueue.queueLength,
3037
+ isProcessing: inputs.commandQueue.isProcessing,
3038
+ currentCommand: inputs.commandQueue.currentCommand ? { ...inputs.commandQueue.currentCommand } : null
3039
+ },
3040
+ inFlightCommandAgeMs,
3041
+ towerState: inputs.towerState ? JSON.parse(JSON.stringify(inputs.towerState)) : null,
3042
+ brokenSeals: [...inputs.brokenSeals],
3043
+ recentEvents: [...this.events],
3044
+ batteryHistory: [...this.batteryHistory],
3045
+ library: { version: LIBRARY_VERSION, platform: detectPlatform() },
3046
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0
3047
+ };
3048
+ this.lastIncident = report;
3049
+ this.recordEvent("disconnect", { cause: inputs.cause });
3050
+ for (const sink of this.sinks) {
3051
+ try {
3052
+ const result = sink.onIncident(report);
3053
+ if (result && typeof result.then === "function") {
3054
+ result.catch((e) => console.error("Diagnostics sink onIncident error:", e));
3055
+ }
3056
+ } catch (e) {
3057
+ console.error("Diagnostics sink onIncident error:", e);
3058
+ }
3059
+ }
3060
+ return report;
3061
+ }
3062
+ getRingBuffer() {
3063
+ return [...this.events];
3064
+ }
3065
+ getBatteryHistory() {
3066
+ return [...this.batteryHistory];
3067
+ }
3068
+ getSessionId() {
3069
+ return this.sessionId;
3070
+ }
3071
+ getConnectedAt() {
3072
+ return this.connectedAt;
3073
+ }
3074
+ getLastIncident() {
3075
+ return this.lastIncident;
3076
+ }
3077
+ clearRingBuffer() {
3078
+ this.events = [];
3079
+ this.batteryHistory = [];
3080
+ }
3081
+ };
3082
+
2798
3083
  // src/UltimateDarkTower.ts
2799
3084
  var UltimateDarkTower = class {
2800
3085
  constructor(config) {
3086
+ this.beforeUnloadHandler = null;
2801
3087
  // tower configuration
2802
3088
  this.retrySendCommandCountRef = { value: 0 };
2803
3089
  this.retrySendCommandMax = DEFAULT_RETRY_SEND_COMMAND_MAX;
@@ -2839,8 +3125,10 @@ var UltimateDarkTower = class {
2839
3125
  // utility
2840
3126
  this._logDetail = false;
2841
3127
  this.initializeLogger();
3128
+ this.initializeDiagnostics(config?.diagnostics);
2842
3129
  this.initializeComponents(config);
2843
3130
  this.setupTowerResponseCallback();
3131
+ this.installBeforeUnloadHandler();
2844
3132
  }
2845
3133
  /**
2846
3134
  * Initialize the logger with default console output
@@ -2849,6 +3137,20 @@ var UltimateDarkTower = class {
2849
3137
  this.logger = new Logger();
2850
3138
  this.logger.addOutput(new ConsoleOutput());
2851
3139
  }
3140
+ /**
3141
+ * Initialize the diagnostics recorder. Always constructed; `enabled` defaults
3142
+ * to false, so when no config is supplied the recorder is a no-op aside from
3143
+ * a single boolean check at each hook site.
3144
+ */
3145
+ initializeDiagnostics(config) {
3146
+ const sinks = config?.sinks ?? (config?.enabled ? [new InMemorySink()] : []);
3147
+ this.diagnosticsRecorder = new UdtDiagnosticsRecorder({
3148
+ enabled: config?.enabled ?? false,
3149
+ capturePayloads: config?.capturePayloads,
3150
+ sinks
3151
+ });
3152
+ this.logger.setDiagnosticsTarget(this.diagnosticsRecorder);
3153
+ }
2852
3154
  /**
2853
3155
  * Initialize all tower components and their dependencies
2854
3156
  */
@@ -2860,11 +3162,16 @@ var UltimateDarkTower = class {
2860
3162
  adapter = BluetoothAdapterFactory.create(config.platform);
2861
3163
  }
2862
3164
  this.towerEventCallbacks = this.createTowerEventCallbacks();
2863
- this.bleConnection = new UdtBleConnection(this.logger, this.towerEventCallbacks, adapter);
3165
+ this.bleConnection = new UdtBleConnection(this.logger, this.towerEventCallbacks, adapter, this.diagnosticsRecorder);
2864
3166
  this.responseProcessor = new TowerResponseProcessor(this.logDetail);
2865
3167
  this.commandFactory = new UdtCommandFactory();
2866
3168
  const commandDependencies = this.createCommandDependencies();
2867
3169
  this.towerCommands = new UdtTowerCommands(commandDependencies);
3170
+ this.bleConnection.setDiagnosticsSnapshotProviders({
3171
+ commandQueue: () => this.towerCommands.getQueueStatus(),
3172
+ towerState: () => this.currentTowerState,
3173
+ brokenSeals: () => Array.from(this.brokenSeals)
3174
+ });
2868
3175
  if (config?.brokenSeals) {
2869
3176
  for (const seal of config.brokenSeals) {
2870
3177
  const sealKey = `${seal.level}-${seal.side}`;
@@ -2872,6 +3179,23 @@ var UltimateDarkTower = class {
2872
3179
  }
2873
3180
  }
2874
3181
  }
3182
+ /**
3183
+ * Browser-only: synthesize a `page_unload` incident if the page closes while
3184
+ * connected. Without this, refreshing the page during a hang loses the
3185
+ * lead-up context. IndexedDB writes during unload are best-effort.
3186
+ */
3187
+ installBeforeUnloadHandler() {
3188
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
3189
+ this.beforeUnloadHandler = () => {
3190
+ if (this.diagnosticsRecorder.enabled && this.bleConnection?.isConnected) {
3191
+ try {
3192
+ this.bleConnection.recordIncidentPublic("page_unload");
3193
+ } catch {
3194
+ }
3195
+ }
3196
+ };
3197
+ window.addEventListener("beforeunload", this.beforeUnloadHandler);
3198
+ }
2875
3199
  /**
2876
3200
  * Set up the tower response callback after all components are initialized
2877
3201
  */
@@ -2926,7 +3250,8 @@ var UltimateDarkTower = class {
2926
3250
  retrySendCommandCount: this.retrySendCommandCountRef,
2927
3251
  retrySendCommandMax: this.retrySendCommandMax,
2928
3252
  getCurrentTowerState: () => this.currentTowerState,
2929
- setTowerState: (newState, source) => this.setTowerState(newState, source)
3253
+ setTowerState: (newState, source) => this.setTowerState(newState, source),
3254
+ recorder: this.diagnosticsRecorder
2930
3255
  };
2931
3256
  }
2932
3257
  /**
@@ -2985,23 +3310,23 @@ var UltimateDarkTower = class {
2985
3310
  return this.previousBatteryPercentage;
2986
3311
  }
2987
3312
  // Getter/setter methods for connection configuration
2988
- get batteryNotifyFrequency() {
2989
- return this.bleConnection.batteryNotifyFrequency;
3313
+ get batteryLogFrequency() {
3314
+ return this.bleConnection.batteryLogFrequency;
2990
3315
  }
2991
- set batteryNotifyFrequency(value) {
2992
- this.bleConnection.batteryNotifyFrequency = value;
3316
+ set batteryLogFrequency(value) {
3317
+ this.bleConnection.batteryLogFrequency = value;
2993
3318
  }
2994
- get batteryNotifyOnValueChangeOnly() {
2995
- return this.bleConnection.batteryNotifyOnValueChangeOnly;
3319
+ get batteryLogOnChangeOnly() {
3320
+ return this.bleConnection.batteryLogOnChangeOnly;
2996
3321
  }
2997
- set batteryNotifyOnValueChangeOnly(value) {
2998
- this.bleConnection.batteryNotifyOnValueChangeOnly = value;
3322
+ set batteryLogOnChangeOnly(value) {
3323
+ this.bleConnection.batteryLogOnChangeOnly = value;
2999
3324
  }
3000
- get batteryNotifyEnabled() {
3001
- return this.bleConnection.batteryNotifyEnabled;
3325
+ get batteryLogEnabled() {
3326
+ return this.bleConnection.batteryLogEnabled;
3002
3327
  }
3003
- set batteryNotifyEnabled(value) {
3004
- this.bleConnection.batteryNotifyEnabled = value;
3328
+ set batteryLogEnabled(value) {
3329
+ this.bleConnection.batteryLogEnabled = value;
3005
3330
  }
3006
3331
  get logTowerResponses() {
3007
3332
  return this.bleConnection.logTowerResponses;
@@ -3185,11 +3510,10 @@ var UltimateDarkTower = class {
3185
3510
  * @returns Promise that resolves when the command is sent
3186
3511
  */
3187
3512
  async sendTowerState(towerState) {
3188
- const { rtdt_pack_state: rtdt_pack_state2 } = await Promise.resolve().then(() => (init_udtTowerState(), udtTowerState_exports));
3189
3513
  const stateToSend = { ...towerState };
3190
3514
  stateToSend.audio = { sample: 0, loop: false, volume: 0 };
3191
3515
  const stateData = new Uint8Array(TOWER_STATE_DATA_SIZE);
3192
- const success = rtdt_pack_state2(stateData, TOWER_STATE_DATA_SIZE, stateToSend);
3516
+ const success = rtdt_pack_state(stateData, TOWER_STATE_DATA_SIZE, stateToSend);
3193
3517
  if (!success) {
3194
3518
  throw new Error("Failed to pack tower state data");
3195
3519
  }
@@ -3217,11 +3541,10 @@ var UltimateDarkTower = class {
3217
3541
  * @param stateData - The 19-byte state data from tower response
3218
3542
  */
3219
3543
  updateTowerStateFromResponse(stateData) {
3220
- Promise.resolve().then(() => (init_udtTowerState(), udtTowerState_exports)).then(({ rtdt_unpack_state: rtdt_unpack_state2 }) => {
3221
- const newState = rtdt_unpack_state2(stateData);
3222
- newState.audio = { sample: 0, loop: false, volume: this.currentTowerState.audio.volume };
3223
- this.setTowerState(newState, "tower response");
3224
- });
3544
+ const newState = rtdt_unpack_state(stateData);
3545
+ newState.audio = { sample: 0, loop: false, volume: this.currentTowerState.audio.volume };
3546
+ newState.led_sequence = 0;
3547
+ this.setTowerState(newState, "tower response");
3225
3548
  }
3226
3549
  //#endregion
3227
3550
  /**
@@ -3449,7 +3772,7 @@ var UltimateDarkTower = class {
3449
3772
  * @param {LogOutput[]} outputs - Array of log outputs to use (e.g., ConsoleOutput, DOMOutput)
3450
3773
  */
3451
3774
  setLoggerOutputs(outputs) {
3452
- this.logger.outputs = [];
3775
+ this.logger.clearOutputs();
3453
3776
  outputs.forEach((output) => this.logger.addOutput(output));
3454
3777
  }
3455
3778
  /**
@@ -3538,20 +3861,680 @@ var UltimateDarkTower = class {
3538
3861
  async cleanup() {
3539
3862
  this.logger.info("Cleaning up UltimateDarkTower instance", "[UDT]");
3540
3863
  this.towerCommands.clearQueue();
3864
+ if (this.beforeUnloadHandler && typeof window !== "undefined") {
3865
+ window.removeEventListener("beforeunload", this.beforeUnloadHandler);
3866
+ this.beforeUnloadHandler = null;
3867
+ }
3868
+ this.logger.setDiagnosticsTarget(null);
3541
3869
  await this.bleConnection.cleanup();
3542
3870
  }
3543
3871
  //#endregion
3872
+ //#region Diagnostics (BLE flight recorder)
3873
+ /**
3874
+ * Get the diagnostics recorder for direct access (live ring buffer, sinks,
3875
+ * runtime enable/disable). Always returns a recorder; check `.enabled` to
3876
+ * see whether capture is active.
3877
+ */
3878
+ getDiagnosticsRecorder() {
3879
+ return this.diagnosticsRecorder;
3880
+ }
3881
+ /**
3882
+ * Toggle diagnostics capture at runtime without reconstructing the tower.
3883
+ * When enabled mid-session, the next BLE event begins populating the buffer.
3884
+ */
3885
+ setDiagnosticsEnabled(enabled) {
3886
+ this.diagnosticsRecorder.enabled = enabled;
3887
+ }
3888
+ /**
3889
+ * Whether diagnostics capture is currently active.
3890
+ */
3891
+ isDiagnosticsEnabled() {
3892
+ return this.diagnosticsRecorder.enabled;
3893
+ }
3894
+ /**
3895
+ * Get the most recent disconnect incident report, or null if none captured
3896
+ * since this instance was created.
3897
+ */
3898
+ getLastIncident() {
3899
+ return this.diagnosticsRecorder.getLastIncident();
3900
+ }
3901
+ /**
3902
+ * Export current ring buffer + last incident as JSON for sharing/analysis.
3903
+ * Useful as a one-liner in a "copy diagnostic info" button.
3904
+ */
3905
+ exportDiagnosticsJSON() {
3906
+ return JSON.stringify({
3907
+ schemaVersion: 1,
3908
+ capturedAt: Date.now(),
3909
+ sessionId: this.diagnosticsRecorder.getSessionId(),
3910
+ ringBuffer: this.diagnosticsRecorder.getRingBuffer(),
3911
+ batteryHistory: this.diagnosticsRecorder.getBatteryHistory(),
3912
+ lastIncident: this.diagnosticsRecorder.getLastIncident()
3913
+ }, null, 2);
3914
+ }
3915
+ //#endregion
3544
3916
  };
3545
3917
  var UltimateDarkTower_default = UltimateDarkTower;
3546
3918
 
3547
3919
  // src/index.ts
3548
3920
  init_udtConstants();
3549
3921
  init_udtBluetoothAdapter();
3550
- init_udtTowerState();
3922
+
3923
+ // src/sinks/IndexedDBSink.ts
3924
+ var DB_NAME = "udt-diagnostics";
3925
+ var DB_VERSION = 1;
3926
+ var STORE_NAME = "incidents";
3927
+ function indexedDBAvailable() {
3928
+ try {
3929
+ return typeof indexedDB !== "undefined";
3930
+ } catch {
3931
+ return false;
3932
+ }
3933
+ }
3934
+ function openDb() {
3935
+ return new Promise((resolve, reject) => {
3936
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
3937
+ req.onupgradeneeded = () => {
3938
+ const db = req.result;
3939
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
3940
+ const store = db.createObjectStore(STORE_NAME, { keyPath: "incidentId" });
3941
+ store.createIndex("triggeredAt", "triggeredAt", { unique: false });
3942
+ }
3943
+ };
3944
+ req.onsuccess = () => resolve(req.result);
3945
+ req.onerror = () => reject(req.error);
3946
+ });
3947
+ }
3948
+ var IndexedDBSink = class {
3949
+ constructor(maxIncidents = 50) {
3950
+ this.dbPromise = null;
3951
+ this.maxIncidents = maxIncidents;
3952
+ this.available = indexedDBAvailable();
3953
+ }
3954
+ async getDb() {
3955
+ if (!this.available) return null;
3956
+ if (!this.dbPromise) {
3957
+ this.dbPromise = openDb().catch((err) => {
3958
+ this.available = false;
3959
+ this.dbPromise = null;
3960
+ throw err;
3961
+ });
3962
+ }
3963
+ try {
3964
+ return await this.dbPromise;
3965
+ } catch {
3966
+ return null;
3967
+ }
3968
+ }
3969
+ async onIncident(report) {
3970
+ const db = await this.getDb();
3971
+ if (!db) return;
3972
+ await new Promise((resolve, reject) => {
3973
+ const tx = db.transaction(STORE_NAME, "readwrite");
3974
+ const store = tx.objectStore(STORE_NAME);
3975
+ store.put(report);
3976
+ tx.oncomplete = () => resolve();
3977
+ tx.onerror = () => reject(tx.error);
3978
+ tx.onabort = () => reject(tx.error);
3979
+ }).catch((e) => console.error("IndexedDBSink put failed:", e));
3980
+ await this.evictOld();
3981
+ }
3982
+ async list() {
3983
+ const db = await this.getDb();
3984
+ if (!db) return [];
3985
+ return new Promise((resolve, reject) => {
3986
+ const tx = db.transaction(STORE_NAME, "readonly");
3987
+ const store = tx.objectStore(STORE_NAME);
3988
+ const req = store.getAll();
3989
+ req.onsuccess = () => {
3990
+ const all = req.result.slice();
3991
+ all.sort((a, b) => b.triggeredAt - a.triggeredAt);
3992
+ resolve(all);
3993
+ };
3994
+ req.onerror = () => reject(req.error);
3995
+ });
3996
+ }
3997
+ async get(incidentId) {
3998
+ const db = await this.getDb();
3999
+ if (!db) return void 0;
4000
+ return new Promise((resolve, reject) => {
4001
+ const tx = db.transaction(STORE_NAME, "readonly");
4002
+ const store = tx.objectStore(STORE_NAME);
4003
+ const req = store.get(incidentId);
4004
+ req.onsuccess = () => resolve(req.result);
4005
+ req.onerror = () => reject(req.error);
4006
+ });
4007
+ }
4008
+ async delete(incidentId) {
4009
+ const db = await this.getDb();
4010
+ if (!db) return;
4011
+ await new Promise((resolve, reject) => {
4012
+ const tx = db.transaction(STORE_NAME, "readwrite");
4013
+ tx.objectStore(STORE_NAME).delete(incidentId);
4014
+ tx.oncomplete = () => resolve();
4015
+ tx.onerror = () => reject(tx.error);
4016
+ }).catch((e) => console.error("IndexedDBSink delete failed:", e));
4017
+ }
4018
+ async clear() {
4019
+ const db = await this.getDb();
4020
+ if (!db) return;
4021
+ await new Promise((resolve, reject) => {
4022
+ const tx = db.transaction(STORE_NAME, "readwrite");
4023
+ tx.objectStore(STORE_NAME).clear();
4024
+ tx.oncomplete = () => resolve();
4025
+ tx.onerror = () => reject(tx.error);
4026
+ }).catch((e) => console.error("IndexedDBSink clear failed:", e));
4027
+ }
4028
+ /** Insert an externally-supplied report (e.g. from a JSON import). */
4029
+ async put(report) {
4030
+ return this.onIncident(report);
4031
+ }
4032
+ async evictOld() {
4033
+ const db = await this.getDb();
4034
+ if (!db) return;
4035
+ await new Promise((resolve) => {
4036
+ const tx = db.transaction(STORE_NAME, "readwrite");
4037
+ const store = tx.objectStore(STORE_NAME);
4038
+ const countReq = store.count();
4039
+ countReq.onsuccess = () => {
4040
+ const total = countReq.result;
4041
+ if (total <= this.maxIncidents) {
4042
+ resolve();
4043
+ return;
4044
+ }
4045
+ const toRemove = total - this.maxIncidents;
4046
+ const idx = store.index("triggeredAt");
4047
+ const cursorReq = idx.openCursor();
4048
+ let removed = 0;
4049
+ cursorReq.onsuccess = () => {
4050
+ const cursor = cursorReq.result;
4051
+ if (!cursor || removed >= toRemove) {
4052
+ resolve();
4053
+ return;
4054
+ }
4055
+ cursor.delete();
4056
+ removed++;
4057
+ cursor.continue();
4058
+ };
4059
+ cursorReq.onerror = () => resolve();
4060
+ };
4061
+ countReq.onerror = () => resolve();
4062
+ });
4063
+ }
4064
+ };
4065
+
4066
+ // src/udtSeedParser.ts
4067
+ var ALPHABET = "a123456789bcdefghijklmnpqrstuvwxyz";
4068
+ var BASE = 34;
4069
+ var SETUP_LENGTH = 6;
4070
+ var RNG_SEED_LENGTH = 6;
4071
+ var SEED_LENGTH = SETUP_LENGTH + RNG_SEED_LENGTH;
4072
+ var CHAR_TO_VALUE = /* @__PURE__ */ new Map();
4073
+ var VALUE_TO_CHAR = /* @__PURE__ */ new Map();
4074
+ for (let i = 0; i < ALPHABET.length; i++) {
4075
+ CHAR_TO_VALUE.set(ALPHABET[i], i);
4076
+ VALUE_TO_CHAR.set(i, ALPHABET[i]);
4077
+ }
4078
+ var TIER1_FOES = ["Brigands", "Oreks", "Shadow Wolves", "Spine Fiends"];
4079
+ var TIER2_FOES = ["Frost Trolls", "Clan of Neuri", "Lemures", "Widowmade Spiders"];
4080
+ var TIER3_FOES = ["Dragons", "Mormos", "Striga", "Titans"];
4081
+ var ADVERSARIES = [
4082
+ "Ashstrider",
4083
+ "Bane of Omens",
4084
+ "Empress of Shades",
4085
+ "Gaze Eternal",
4086
+ "Gravemaw",
4087
+ "Isa the Exile",
4088
+ "Lingering Rot",
4089
+ "Utuk'Ku"
4090
+ ];
4091
+ var ALLIES = [
4092
+ "Gleb",
4093
+ "Grigor",
4094
+ "Hakan",
4095
+ "Letha",
4096
+ "Miras",
4097
+ "Nimet",
4098
+ "Tomas",
4099
+ "Vasa",
4100
+ "Yana",
4101
+ "Zaida"
4102
+ ];
4103
+ var DIFFICULTIES = ["Heroic", "Gritty"];
4104
+ var GAME_SOURCES = ["Core", "Competitive"];
4105
+ function charToValue(c) {
4106
+ const v = CHAR_TO_VALUE.get(c.toLowerCase());
4107
+ if (v === void 0) {
4108
+ throw new Error(`Invalid seed character: '${c}'`);
4109
+ }
4110
+ return v;
4111
+ }
4112
+ function valueToChar(v) {
4113
+ const c = VALUE_TO_CHAR.get(v);
4114
+ if (c === void 0) {
4115
+ throw new Error(`Invalid seed value: ${v} (must be 0\u2013${BASE - 1})`);
4116
+ }
4117
+ return c;
4118
+ }
4119
+ function validateSeed(seed) {
4120
+ const stripped = seed.replace(/[-\s]/g, "").toLowerCase();
4121
+ if (stripped.length !== SEED_LENGTH) {
4122
+ throw new Error(`Invalid seed length: expected ${SEED_LENGTH} characters, got ${stripped.length}`);
4123
+ }
4124
+ for (const c of stripped) {
4125
+ if (!CHAR_TO_VALUE.has(c)) {
4126
+ throw new Error(`Invalid seed character: '${c}'`);
4127
+ }
4128
+ }
4129
+ const upper = stripped.toUpperCase();
4130
+ return `${upper.slice(0, 4)}-${upper.slice(4, 8)}-${upper.slice(8, 12)}`;
4131
+ }
4132
+ function decodeSeed(seed) {
4133
+ const normalized = validateSeed(seed);
4134
+ const stripped = normalized.replace(/-/g, "").toLowerCase();
4135
+ const setup = [];
4136
+ for (let i = 0; i < SETUP_LENGTH; i++) {
4137
+ setup.push(charToValue(stripped[i]));
4138
+ }
4139
+ let rngSeed = 0;
4140
+ for (let i = 0; i < RNG_SEED_LENGTH; i++) {
4141
+ const value = charToValue(stripped[SETUP_LENGTH + i]);
4142
+ rngSeed += value * Math.round(Math.pow(BASE, i));
4143
+ }
4144
+ const foeByteA = setup[0];
4145
+ const tier1 = foeByteA & 3;
4146
+ const tier2 = (foeByteA & 12) >> 2;
4147
+ const foeByteB = setup[1];
4148
+ const tier3 = (foeByteA & 16) >> 4 | (foeByteB & 16) >> 3;
4149
+ const adversaryIndex = foeByteB & 15;
4150
+ const allyIndex = setup[2];
4151
+ const extra = setup[3];
4152
+ const difficultyIndex = extra & 1;
4153
+ const expansionBits = (extra & 6) >> 1;
4154
+ const sourceBits = (extra & 8) >> 2;
4155
+ const playerCount = (setup[5] & 3) + 1;
4156
+ const expansions = [];
4157
+ if (expansionBits & 1) expansions.push("Monuments");
4158
+ if (expansionBits & 2) expansions.push("Alliances");
4159
+ let source;
4160
+ switch (sourceBits) {
4161
+ case 2:
4162
+ source = "Competitive";
4163
+ break;
4164
+ default:
4165
+ source = "Core";
4166
+ break;
4167
+ }
4168
+ const seedBank = {
4169
+ initializationSeed: rngSeed,
4170
+ questSeed: rngSeed - 1,
4171
+ seedString: normalized
4172
+ };
4173
+ return {
4174
+ seed: normalized,
4175
+ tier1Foe: TIER1_FOES[tier1],
4176
+ tier2Foe: TIER2_FOES[tier2],
4177
+ tier3Foe: TIER3_FOES[tier3],
4178
+ adversary: ADVERSARIES[adversaryIndex],
4179
+ ally: ALLIES[allyIndex],
4180
+ difficulty: DIFFICULTIES[difficultyIndex],
4181
+ source,
4182
+ expansions,
4183
+ playerCount,
4184
+ rngSeed,
4185
+ seedBank,
4186
+ setup
4187
+ };
4188
+ }
4189
+ function decodeRngSeed(seed) {
4190
+ const normalized = validateSeed(seed);
4191
+ const stripped = normalized.replace(/-/g, "").toLowerCase();
4192
+ let rngSeed = 0;
4193
+ for (let i = 0; i < RNG_SEED_LENGTH; i++) {
4194
+ const value = charToValue(stripped[SETUP_LENGTH + i]);
4195
+ rngSeed += value * Math.round(Math.pow(BASE, i));
4196
+ }
4197
+ return rngSeed;
4198
+ }
4199
+ function createSeed(config) {
4200
+ let foeByteA = 0;
4201
+ let foeByteB = 0;
4202
+ const tier1Index = TIER1_FOES.indexOf(config.foes[0]);
4203
+ const tier2Index = TIER2_FOES.indexOf(config.foes[1]);
4204
+ const tier3Index = TIER3_FOES.indexOf(config.foes[2]);
4205
+ if (tier1Index < 0) throw new Error(`Invalid Tier 1 foe: ${config.foes[0]}`);
4206
+ if (tier2Index < 0) throw new Error(`Invalid Tier 2 foe: ${config.foes[1]}`);
4207
+ if (tier3Index < 0) throw new Error(`Invalid Tier 3 foe: ${config.foes[2]}`);
4208
+ foeByteA = tier1Index & 3;
4209
+ foeByteA |= (tier2Index & 3) << 2;
4210
+ foeByteA |= (tier3Index & 1) << 4;
4211
+ foeByteB |= (tier3Index >> 1 & 1) << 4;
4212
+ const adversaryIndex = ADVERSARIES.indexOf(config.adversary);
4213
+ if (adversaryIndex < 0) throw new Error(`Invalid adversary: ${config.adversary}`);
4214
+ foeByteB |= adversaryIndex & 15;
4215
+ const allyIndex = ALLIES.indexOf(config.ally);
4216
+ if (allyIndex < 0) throw new Error(`Invalid ally: ${config.ally}`);
4217
+ let extraByte = 0;
4218
+ if (config.difficulty === "Gritty") extraByte |= 1;
4219
+ for (const expansion of config.expansions) {
4220
+ switch (expansion) {
4221
+ case "Monuments":
4222
+ extraByte |= 2;
4223
+ break;
4224
+ case "Alliances":
4225
+ extraByte |= 4;
4226
+ break;
4227
+ }
4228
+ }
4229
+ if (config.source === "Competitive") extraByte |= 8;
4230
+ const versionByte = 0;
4231
+ const playerCountByte = Math.max(0, Math.min(3, config.playerCount - 1));
4232
+ let seedStr = valueToChar(foeByteA) + valueToChar(foeByteB) + valueToChar(allyIndex) + valueToChar(extraByte) + valueToChar(versionByte) + valueToChar(playerCountByte);
4233
+ let rngValue = 0;
4234
+ for (let i = 0; i < RNG_SEED_LENGTH; i++) {
4235
+ const value = Math.floor(Math.random() * BASE);
4236
+ seedStr += valueToChar(value);
4237
+ rngValue += value * Math.round(Math.pow(BASE, i));
4238
+ }
4239
+ const upper = seedStr.toUpperCase();
4240
+ const formatted = `${upper.slice(0, 4)}-${upper.slice(4, 8)}-${upper.slice(8, 12)}`;
4241
+ return { seed: formatted, rngValue };
4242
+ }
4243
+ function encodeSeed(config, rngValue) {
4244
+ let foeByteA = 0;
4245
+ let foeByteB = 0;
4246
+ const tier1Index = TIER1_FOES.indexOf(config.foes[0]);
4247
+ const tier2Index = TIER2_FOES.indexOf(config.foes[1]);
4248
+ const tier3Index = TIER3_FOES.indexOf(config.foes[2]);
4249
+ if (tier1Index < 0) throw new Error(`Invalid Tier 1 foe: ${config.foes[0]}`);
4250
+ if (tier2Index < 0) throw new Error(`Invalid Tier 2 foe: ${config.foes[1]}`);
4251
+ if (tier3Index < 0) throw new Error(`Invalid Tier 3 foe: ${config.foes[2]}`);
4252
+ foeByteA = tier1Index & 3;
4253
+ foeByteA |= (tier2Index & 3) << 2;
4254
+ foeByteA |= (tier3Index & 1) << 4;
4255
+ foeByteB |= (tier3Index >> 1 & 1) << 4;
4256
+ const adversaryIndex = ADVERSARIES.indexOf(config.adversary);
4257
+ if (adversaryIndex < 0) throw new Error(`Invalid adversary: ${config.adversary}`);
4258
+ foeByteB |= adversaryIndex & 15;
4259
+ const allyIndex = ALLIES.indexOf(config.ally);
4260
+ if (allyIndex < 0) throw new Error(`Invalid ally: ${config.ally}`);
4261
+ let extraByte = 0;
4262
+ if (config.difficulty === "Gritty") extraByte |= 1;
4263
+ for (const expansion of config.expansions) {
4264
+ switch (expansion) {
4265
+ case "Monuments":
4266
+ extraByte |= 2;
4267
+ break;
4268
+ case "Alliances":
4269
+ extraByte |= 4;
4270
+ break;
4271
+ }
4272
+ }
4273
+ if (config.source === "Competitive") extraByte |= 8;
4274
+ const versionByte = 0;
4275
+ const playerCountByte = Math.max(0, Math.min(3, config.playerCount - 1));
4276
+ let seedStr = valueToChar(foeByteA) + valueToChar(foeByteB) + valueToChar(allyIndex) + valueToChar(extraByte) + valueToChar(versionByte) + valueToChar(playerCountByte);
4277
+ let remaining = rngValue;
4278
+ for (let i = 0; i < RNG_SEED_LENGTH; i++) {
4279
+ const digit = remaining % BASE;
4280
+ seedStr += valueToChar(digit);
4281
+ remaining = Math.floor(remaining / BASE);
4282
+ }
4283
+ const upper = seedStr.toUpperCase();
4284
+ return `${upper.slice(0, 4)}-${upper.slice(4, 8)}-${upper.slice(8, 12)}`;
4285
+ }
4286
+ function compareSeedsRaw(seed1, seed2) {
4287
+ const n1 = validateSeed(seed1);
4288
+ const n2 = validateSeed(seed2);
4289
+ const s1 = n1.replace(/-/g, "").toLowerCase();
4290
+ const s2 = n2.replace(/-/g, "").toLowerCase();
4291
+ const diffs = [];
4292
+ for (let i = 0; i < SEED_LENGTH; i++) {
4293
+ const v1 = charToValue(s1[i]);
4294
+ const v2 = charToValue(s2[i]);
4295
+ if (v1 !== v2) {
4296
+ diffs.push({
4297
+ charIndex: i,
4298
+ value1: v1,
4299
+ value2: v2,
4300
+ char1: s1[i],
4301
+ char2: s2[i]
4302
+ });
4303
+ }
4304
+ }
4305
+ return {
4306
+ seed1: n1,
4307
+ seed2: n2,
4308
+ diffs,
4309
+ setupDiffs: diffs.filter((d) => d.charIndex < SETUP_LENGTH),
4310
+ rngDiffs: diffs.filter((d) => d.charIndex >= SETUP_LENGTH)
4311
+ };
4312
+ }
4313
+ var SETUP_FIELD_LABELS = {
4314
+ 0: "Tier1/Tier2/Tier3lo",
4315
+ 1: "Adversary/Tier3hi",
4316
+ 2: "Ally",
4317
+ 3: "Difficulty/Expansions/Source",
4318
+ 4: "Version",
4319
+ 5: "PlayerCount"
4320
+ };
4321
+ function dumpSeedChars(seed) {
4322
+ const normalized = validateSeed(seed);
4323
+ const stripped = normalized.replace(/-/g, "").toLowerCase();
4324
+ const chars = [];
4325
+ for (let i = 0; i < SEED_LENGTH; i++) {
4326
+ const isSetup = i < SETUP_LENGTH;
4327
+ chars.push({
4328
+ index: i,
4329
+ char: stripped[i],
4330
+ value: charToValue(stripped[i]),
4331
+ section: isSetup ? "setup" : "rng",
4332
+ field: isSetup ? SETUP_FIELD_LABELS[i] : void 0
4333
+ });
4334
+ }
4335
+ return { seed: normalized, chars };
4336
+ }
4337
+
4338
+ // src/udtSystemRandom.ts
4339
+ var INT32_MAX = 2147483647;
4340
+ var MSEED = 161803398;
4341
+ function toInt32(n) {
4342
+ return n | 0;
4343
+ }
4344
+ var SystemRandom = class {
4345
+ /**
4346
+ * Create a new PRNG instance with the given seed.
4347
+ * Matches C# `new System.Random(seed)` exactly.
4348
+ */
4349
+ constructor(seed) {
4350
+ this.seedArray = new Array(56).fill(0);
4351
+ this.inext = 0;
4352
+ this.inextp = 0;
4353
+ this.initialize(seed);
4354
+ }
4355
+ /**
4356
+ * Replicate .NET's System.Random constructor seeding algorithm.
4357
+ */
4358
+ initialize(seed) {
4359
+ let subtraction;
4360
+ if (seed === -2147483648) {
4361
+ subtraction = INT32_MAX;
4362
+ } else {
4363
+ subtraction = Math.abs(seed);
4364
+ }
4365
+ let mj = toInt32(MSEED - subtraction);
4366
+ this.seedArray[55] = mj;
4367
+ let mk = 1;
4368
+ for (let i = 1; i < 55; i++) {
4369
+ const ii = 21 * i % 55;
4370
+ this.seedArray[ii] = mk;
4371
+ mk = toInt32(mj - mk);
4372
+ if (mk < 0) mk = toInt32(mk + INT32_MAX);
4373
+ mj = this.seedArray[ii];
4374
+ }
4375
+ for (let k = 1; k < 5; k++) {
4376
+ for (let i = 1; i < 56; i++) {
4377
+ this.seedArray[i] = toInt32(this.seedArray[i] - this.seedArray[1 + (i + 30) % 55]);
4378
+ if (this.seedArray[i] < 0) {
4379
+ this.seedArray[i] = toInt32(this.seedArray[i] + INT32_MAX);
4380
+ }
4381
+ }
4382
+ }
4383
+ this.inext = 0;
4384
+ this.inextp = 21;
4385
+ }
4386
+ /**
4387
+ * Internal sample — returns value in range [0, Int32.MaxValue).
4388
+ * Matches C#'s InternalSample().
4389
+ */
4390
+ internalSample() {
4391
+ let retVal;
4392
+ let locINext = this.inext;
4393
+ let locINextp = this.inextp;
4394
+ if (++locINext >= 56) locINext = 1;
4395
+ if (++locINextp >= 56) locINextp = 1;
4396
+ retVal = toInt32(this.seedArray[locINext] - this.seedArray[locINextp]);
4397
+ if (retVal === INT32_MAX) retVal--;
4398
+ if (retVal < 0) retVal = toInt32(retVal + INT32_MAX);
4399
+ this.seedArray[locINext] = retVal;
4400
+ this.inext = locINext;
4401
+ this.inextp = locINextp;
4402
+ return retVal;
4403
+ }
4404
+ /**
4405
+ * Sample — returns a double in range [0.0, 1.0).
4406
+ * Matches C#'s Sample().
4407
+ */
4408
+ sample() {
4409
+ return this.internalSample() * (1 / INT32_MAX);
4410
+ }
4411
+ /**
4412
+ * Returns a non-negative random integer less than Int32.MaxValue.
4413
+ * Matches C# `Random.Next()`.
4414
+ */
4415
+ next() {
4416
+ return this.internalSample();
4417
+ }
4418
+ /**
4419
+ * Returns a non-negative random integer less than maxValue.
4420
+ * Matches C# `Random.Next(maxValue)`.
4421
+ */
4422
+ nextMax(maxValue) {
4423
+ if (maxValue < 0) {
4424
+ throw new Error("maxValue must be non-negative");
4425
+ }
4426
+ return toInt32(this.sample() * maxValue);
4427
+ }
4428
+ /**
4429
+ * Returns a random integer in range [minValue, maxValue).
4430
+ * Matches C# `Random.Next(minValue, maxValue)`.
4431
+ */
4432
+ nextRange(minValue, maxValue) {
4433
+ if (minValue > maxValue) {
4434
+ throw new Error("minValue must be less than or equal to maxValue");
4435
+ }
4436
+ const range = maxValue - minValue;
4437
+ if (range <= INT32_MAX) {
4438
+ return toInt32(this.sample() * range) + minValue;
4439
+ }
4440
+ return toInt32(this.internalSample() * (1 / INT32_MAX) * range) + minValue;
4441
+ }
4442
+ /**
4443
+ * Returns a random double in range [0.0, 1.0).
4444
+ * Matches C# `Random.NextDouble()`.
4445
+ */
4446
+ nextDouble() {
4447
+ return this.sample();
4448
+ }
4449
+ };
4450
+
4451
+ // src/udtGameBoard.ts
4452
+ var BOARD_GROUPINGS = {
4453
+ /** Dayside and Fivepint (North kingdom lakes). */
4454
+ LONG_WATER: "Long Water",
4455
+ /** Delmsmire, Arkartus, and Yellowpike (West kingdom forests). */
4456
+ THE_GREAT_WOODS: "The Great Woods",
4457
+ /** The Throne, The Cloister, and Archmont (South kingdom grasslands). */
4458
+ REGAL_RUN: "Regal Run"
4459
+ };
4460
+ var BOARD_LOCATIONS = [
4461
+ // ── North ───────────────────────────────────────────────────────────────
4462
+ { name: "Broken Lands", terrain: "Hills", kingdom: "north" },
4463
+ { name: "Dayside", terrain: "Lake", building: "Bazaar", kingdom: "north", grouping: BOARD_GROUPINGS.LONG_WATER },
4464
+ { name: "Egan's End", terrain: "Grasslands", building: "Village", kingdom: "north" },
4465
+ { name: "Fivepint", terrain: "Lake", kingdom: "north", grouping: BOARD_GROUPINGS.LONG_WATER },
4466
+ { name: "Green Bridge", terrain: "Grasslands", kingdom: "north" },
4467
+ { name: "Lodestone Mountains", terrain: "Mountains", kingdom: "north" },
4468
+ { name: "Lower Ice Fangs", terrain: "Mountains", kingdom: "north" },
4469
+ { name: "Muted Forest", terrain: "Forest", kingdom: "north" },
4470
+ { name: "Peaks of the Djinn", terrain: "Mountains", kingdom: "north" },
4471
+ { name: "Pearl of the North", terrain: "Grasslands", kingdom: "north" },
4472
+ { name: "Radiant Mountains", terrain: "Mountains", building: "Citadel", kingdom: "north" },
4473
+ { name: "Rimeweald", terrain: "Forest", kingdom: "north" },
4474
+ { name: "The Tundra", terrain: "Desert", kingdom: "north" },
4475
+ { name: "Tower Scar Desert", terrain: "Desert", kingdom: "north" },
4476
+ { name: "Upper Ice Fangs", terrain: "Mountains", building: "Sanctuary", kingdom: "north" },
4477
+ // ── East ────────────────────────────────────────────────────────────────
4478
+ { name: "Big Sister", terrain: "Mountains", kingdom: "east" },
4479
+ { name: "Bleak Wastes", terrain: "Desert", kingdom: "east" },
4480
+ { name: "Copper Grove", terrain: "Forest", kingdom: "east" },
4481
+ { name: "Dragontooth Lake", terrain: "Lake", kingdom: "east" },
4482
+ { name: "Duwani", terrain: "Grasslands", building: "Village", kingdom: "east" },
4483
+ { name: "Forest of Shades", terrain: "Forest", kingdom: "east" },
4484
+ { name: "Greater Tombstones", terrain: "Hills", building: "Sanctuary", kingdom: "east" },
4485
+ { name: "Inner Kinghills", terrain: "Hills", building: "Citadel", kingdom: "east" },
4486
+ { name: "Jewel Hills", terrain: "Hills", kingdom: "east" },
4487
+ { name: "Lake of Songs", terrain: "Lake", kingdom: "east" },
4488
+ { name: "Lesser Tombstones", terrain: "Hills", kingdom: "east" },
4489
+ { name: "Outer Kinghills", terrain: "Hills", kingdom: "east" },
4490
+ { name: "The Decaying Wilds", terrain: "Grasslands", kingdom: "east" },
4491
+ { name: "Three Rivers", terrain: "Grasslands", building: "Bazaar", kingdom: "east" },
4492
+ { name: "Utar's Barrows", terrain: "Desert", kingdom: "east" },
4493
+ // ── West ────────────────────────────────────────────────────────────────
4494
+ { name: "Anza", terrain: "Grasslands", building: "Village", kingdom: "west" },
4495
+ { name: "Arkartus", terrain: "Forest", building: "Sanctuary", kingdom: "west", grouping: BOARD_GROUPINGS.THE_GREAT_WOODS },
4496
+ { name: "Ash Hills", terrain: "Hills", kingdom: "west" },
4497
+ { name: "Cloudhold", terrain: "Mountains", kingdom: "west" },
4498
+ { name: "Delmsmire", terrain: "Forest", kingdom: "west", grouping: BOARD_GROUPINGS.THE_GREAT_WOODS },
4499
+ { name: "Hissing Groves", terrain: "Forest", building: "Citadel", kingdom: "west" },
4500
+ { name: "Idran Forest", terrain: "Forest", kingdom: "west" },
4501
+ { name: "Lonelight Hills", terrain: "Hills", kingdom: "west" },
4502
+ { name: "Lost Lands", terrain: "Desert", kingdom: "west" },
4503
+ { name: "Plains of Plovo", terrain: "Grasslands", building: "Bazaar", kingdom: "west" },
4504
+ { name: "Plains of Woldra", terrain: "Grasslands", kingdom: "west" },
4505
+ { name: "The Empty Glade", terrain: "Grasslands", kingdom: "west" },
4506
+ { name: "The Grass Sea", terrain: "Grasslands", kingdom: "west" },
4507
+ { name: "Weeping Waters", terrain: "Lake", kingdom: "west" },
4508
+ { name: "Yellowpike", terrain: "Forest", kingdom: "west", grouping: BOARD_GROUPINGS.THE_GREAT_WOODS },
4509
+ // ── South ───────────────────────────────────────────────────────────────
4510
+ { name: "Archmont", terrain: "Grasslands", kingdom: "south", grouping: BOARD_GROUPINGS.REGAL_RUN },
4511
+ { name: "Azkol's Bane", terrain: "Desert", kingdom: "south" },
4512
+ { name: "Bone Hills", terrain: "Hills", kingdom: "south" },
4513
+ { name: "Howling Desert", terrain: "Desert", building: "Citadel", kingdom: "south" },
4514
+ { name: "Irontops", terrain: "Mountains", kingdom: "south" },
4515
+ { name: "Little Sister", terrain: "Mountains", kingdom: "south" },
4516
+ { name: "Middle Sister", terrain: "Mountains", kingdom: "south" },
4517
+ { name: "Mountains of the Watchers", terrain: "Mountains", kingdom: "south" },
4518
+ { name: "Pine Barrens", terrain: "Forest", kingdom: "south" },
4519
+ { name: "Sands of Madness", terrain: "Desert", building: "Sanctuary", kingdom: "south" },
4520
+ { name: "Southern Wastes", terrain: "Desert", building: "Village", kingdom: "south" },
4521
+ { name: "The Cloister", terrain: "Grasslands", kingdom: "south", grouping: BOARD_GROUPINGS.REGAL_RUN },
4522
+ { name: "The Emerald Expanse", terrain: "Grasslands", building: "Bazaar", kingdom: "south" },
4523
+ { name: "The Throne", terrain: "Grasslands", kingdom: "south", grouping: BOARD_GROUPINGS.REGAL_RUN },
4524
+ { name: "Ulamel's Hollow", terrain: "Grasslands", kingdom: "south" }
4525
+ ];
4526
+ var BOARD_LOCATION_BY_NAME = Object.fromEntries(BOARD_LOCATIONS.map((loc) => [loc.name, loc]));
4527
+
4528
+ // src/index.ts
3551
4529
  var index_default = UltimateDarkTower_default;
3552
4530
  export {
4531
+ ADVERSARIES,
4532
+ ALLIES,
3553
4533
  AUDIO_COMMAND_POS,
3554
4534
  BATTERY_STATUS_FREQUENCY,
4535
+ BOARD_GROUPINGS,
4536
+ BOARD_LOCATIONS,
4537
+ BOARD_LOCATION_BY_NAME,
3555
4538
  BluetoothAdapterFactory,
3556
4539
  BluetoothConnectionError,
3557
4540
  BluetoothDeviceNotFoundError,
@@ -3565,6 +4548,7 @@ export {
3565
4548
  DEFAULT_CONNECTION_MONITORING_FREQUENCY,
3566
4549
  DEFAULT_CONNECTION_MONITORING_TIMEOUT,
3567
4550
  DEFAULT_RETRY_SEND_COMMAND_MAX,
4551
+ DIFFICULTIES,
3568
4552
  DIS_FIRMWARE_REVISION_UUID,
3569
4553
  DIS_HARDWARE_REVISION_UUID,
3570
4554
  DIS_IEEE_REGULATORY_UUID,
@@ -3577,7 +4561,10 @@ export {
3577
4561
  DIS_SYSTEM_ID_UUID,
3578
4562
  DOMOutput,
3579
4563
  DRUM_PACKETS,
4564
+ GAME_SOURCES,
3580
4565
  GLYPHS,
4566
+ InMemorySink,
4567
+ IndexedDBSink,
3581
4568
  LAYER_TO_POSITION,
3582
4569
  LEDGE_BASE_LIGHT_POSITIONS,
3583
4570
  LED_CHANNEL_LOOKUP,
@@ -3587,7 +4574,11 @@ export {
3587
4574
  RING_LIGHT_POSITIONS,
3588
4575
  SKULL_DROP_COUNT_POS,
3589
4576
  STATE_DATA_LENGTH,
4577
+ SystemRandom,
3590
4578
  TC,
4579
+ TIER1_FOES,
4580
+ TIER2_FOES,
4581
+ TIER3_FOES,
3591
4582
  TOWER_AUDIO_LIBRARY,
3592
4583
  TOWER_COMMANDS,
3593
4584
  TOWER_COMMAND_HEADER_SIZE,
@@ -3604,18 +4595,29 @@ export {
3604
4595
  UART_RX_CHARACTERISTIC_UUID,
3605
4596
  UART_SERVICE_UUID,
3606
4597
  UART_TX_CHARACTERISTIC_UUID,
4598
+ UdtDiagnosticsRecorder,
3607
4599
  UltimateDarkTower_default as UltimateDarkTower,
3608
4600
  VOLTAGE_LEVELS,
3609
4601
  VOLUME_DESCRIPTIONS,
3610
4602
  VOLUME_ICONS,
4603
+ bytesToHex,
4604
+ charToValue,
4605
+ compareSeedsRaw,
3611
4606
  createDefaultTowerState,
4607
+ createSeed,
4608
+ decodeRngSeed,
4609
+ decodeSeed,
3612
4610
  index_default as default,
3613
4611
  drumPositionCmds,
4612
+ dumpSeedChars,
4613
+ encodeSeed,
3614
4614
  isCalibrated,
3615
4615
  logger,
3616
4616
  milliVoltsToPercentage,
3617
4617
  milliVoltsToPercentageNumber,
3618
4618
  parseDifferentialReadings,
3619
4619
  rtdt_pack_state,
3620
- rtdt_unpack_state
4620
+ rtdt_unpack_state,
4621
+ validateSeed,
4622
+ valueToChar
3621
4623
  };