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.
- package/CHANGELOG.md +61 -1
- package/README.md +84 -71
- package/dist/esm/index.mjs +1270 -268
- package/dist/src/UltimateDarkTower.d.ts +48 -6
- package/dist/src/UltimateDarkTower.js +115 -53
- package/dist/src/UltimateDarkTower.js.map +1 -1
- package/dist/src/adapters/NodeBluetoothAdapter.js +9 -5
- package/dist/src/adapters/NodeBluetoothAdapter.js.map +1 -1
- package/dist/src/adapters/WebBluetoothAdapter.js +11 -8
- package/dist/src/adapters/WebBluetoothAdapter.js.map +1 -1
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +34 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/sinks/IndexedDBSink.d.ts +26 -0
- package/dist/src/sinks/IndexedDBSink.js +165 -0
- package/dist/src/sinks/IndexedDBSink.js.map +1 -0
- package/dist/src/udtBleConnection.d.ts +28 -5
- package/dist/src/udtBleConnection.js +80 -13
- package/dist/src/udtBleConnection.js.map +1 -1
- package/dist/src/udtBluetoothAdapter.d.ts +6 -6
- package/dist/src/udtBluetoothAdapter.js.map +1 -1
- package/dist/src/udtCommandFactory.d.ts +6 -0
- package/dist/src/udtCommandFactory.js +26 -22
- package/dist/src/udtCommandFactory.js.map +1 -1
- package/dist/src/udtCommandQueue.d.ts +6 -3
- package/dist/src/udtCommandQueue.js +28 -5
- package/dist/src/udtCommandQueue.js.map +1 -1
- package/dist/src/udtConstants.d.ts +3 -8
- package/dist/src/udtConstants.js +2 -0
- package/dist/src/udtConstants.js.map +1 -1
- package/dist/src/udtDiagnostics.d.ts +122 -0
- package/dist/src/udtDiagnostics.js +228 -0
- package/dist/src/udtDiagnostics.js.map +1 -0
- package/dist/src/udtGameBoard.d.ts +38 -0
- package/dist/src/udtGameBoard.js +86 -0
- package/dist/src/udtGameBoard.js.map +1 -0
- package/dist/src/udtLogger.d.ts +16 -0
- package/dist/src/udtLogger.js +41 -2
- package/dist/src/udtLogger.js.map +1 -1
- package/dist/src/udtSeedParser.d.ts +124 -0
- package/dist/src/udtSeedParser.js +369 -0
- package/dist/src/udtSeedParser.js.map +1 -0
- package/dist/src/udtSystemRandom.d.ts +58 -0
- package/dist/src/udtSystemRandom.js +154 -0
- package/dist/src/udtSystemRandom.js.map +1 -0
- package/dist/src/udtTowerCommands.d.ts +4 -2
- package/dist/src/udtTowerCommands.js +24 -43
- package/dist/src/udtTowerCommands.js.map +1 -1
- package/dist/src/udtTowerResponse.d.ts +9 -5
- package/dist/src/udtTowerResponse.js +5 -6
- package/dist/src/udtTowerResponse.js.map +1 -1
- package/package.json +5 -1
package/dist/esm/index.mjs
CHANGED
|
@@ -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
|
|
603
|
-
|
|
604
|
-
|
|
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
|
|
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") ||
|
|
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
|
|
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
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
1479
|
+
return true;
|
|
1453
1480
|
}
|
|
1454
|
-
|
|
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.
|
|
1582
|
+
this.lastBatteryLog = 0;
|
|
1553
1583
|
this.lastBatteryPercentage = "";
|
|
1554
|
-
this.
|
|
1555
|
-
this.
|
|
1556
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1651
|
-
const
|
|
1652
|
-
if (
|
|
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.
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
1933
|
-
|
|
1934
|
-
|
|
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
|
|
2037
|
-
|
|
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
|
-
|
|
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.
|
|
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 ??
|
|
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
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
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
|
-
|
|
2320
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
2576
|
-
await this.
|
|
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
|
|
2989
|
-
return this.bleConnection.
|
|
3313
|
+
get batteryLogFrequency() {
|
|
3314
|
+
return this.bleConnection.batteryLogFrequency;
|
|
2990
3315
|
}
|
|
2991
|
-
set
|
|
2992
|
-
this.bleConnection.
|
|
3316
|
+
set batteryLogFrequency(value) {
|
|
3317
|
+
this.bleConnection.batteryLogFrequency = value;
|
|
2993
3318
|
}
|
|
2994
|
-
get
|
|
2995
|
-
return this.bleConnection.
|
|
3319
|
+
get batteryLogOnChangeOnly() {
|
|
3320
|
+
return this.bleConnection.batteryLogOnChangeOnly;
|
|
2996
3321
|
}
|
|
2997
|
-
set
|
|
2998
|
-
this.bleConnection.
|
|
3322
|
+
set batteryLogOnChangeOnly(value) {
|
|
3323
|
+
this.bleConnection.batteryLogOnChangeOnly = value;
|
|
2999
3324
|
}
|
|
3000
|
-
get
|
|
3001
|
-
return this.bleConnection.
|
|
3325
|
+
get batteryLogEnabled() {
|
|
3326
|
+
return this.bleConnection.batteryLogEnabled;
|
|
3002
3327
|
}
|
|
3003
|
-
set
|
|
3004
|
-
this.bleConnection.
|
|
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 =
|
|
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
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
};
|