ultimatedarktower 5.0.0 → 5.0.1
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 +27 -0
- package/dist/esm/index.mjs +127 -96
- package/dist/src/UltimateDarkTower.d.ts +5 -13
- package/dist/src/UltimateDarkTower.js +33 -44
- package/dist/src/UltimateDarkTower.js.map +1 -1
- package/dist/src/adapters/NodeBluetoothAdapter.js +7 -9
- package/dist/src/adapters/NodeBluetoothAdapter.js.map +1 -1
- package/dist/src/adapters/WebBluetoothAdapter.js +1 -0
- package/dist/src/adapters/WebBluetoothAdapter.js.map +1 -1
- package/dist/src/seed/udtSystemRandom.d.ts +6 -0
- package/dist/src/seed/udtSystemRandom.js +21 -2
- package/dist/src/seed/udtSystemRandom.js.map +1 -1
- package/dist/src/udtBleConnection.js +3 -2
- package/dist/src/udtBleConnection.js.map +1 -1
- package/dist/src/udtCommandFactory.d.ts +1 -1
- package/dist/src/udtCommandFactory.js +4 -2
- package/dist/src/udtCommandFactory.js.map +1 -1
- package/dist/src/udtDiagnostics.js +2 -1
- package/dist/src/udtDiagnostics.js.map +1 -1
- package/dist/src/udtHelpers.js +1 -4
- package/dist/src/udtHelpers.js.map +1 -1
- package/dist/src/udtLogger.js +11 -6
- package/dist/src/udtLogger.js.map +1 -1
- package/dist/src/udtTowerCommands.d.ts +10 -0
- package/dist/src/udtTowerCommands.js +66 -37
- package/dist/src/udtTowerCommands.js.map +1 -1
- package/dist/src/udtTowerResponse.d.ts +9 -0
- package/dist/src/udtTowerResponse.js +12 -1
- package/dist/src/udtTowerResponse.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [5.0.1] - 2026-07-03
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
Fixes from a full-source code review, ordered by severity:
|
|
14
|
+
|
|
15
|
+
- **Node adapter: a failed `connect()` no longer permanently breaks retries.** `NodeBluetoothAdapter.cleanup()` was nulling the `onCharacteristicValueChanged`/`onDisconnect`/`onBluetoothAvailabilityChanged` callbacks on any connection failure, but `UdtBleConnection` only wires those callbacks once. A failed connect (e.g. a scan timeout) silently broke all future notifications on the next successful `connect()` — no battery heartbeat, no command responses, forcing a disconnect ~3s later. `cleanup()` no longer clears these caller-owned callback fields, matching `WebBluetoothAdapter`'s existing behavior.
|
|
16
|
+
- **Tower state aliasing fixed: `onTowerStateUpdate` now receives distinct old/new objects.** `getCurrentTowerState()` (both the internal command-builder dependency and the public method) previously returned the live state object (or a shallow copy sharing nested `layer`/`drum` objects), so commands that mutated it in place before calling `setTowerState` produced `oldState === newState` — consumers couldn't diff, and `logDetail` always logged "No changes detected". Both now return a full deep copy (`UdtCommandFactory.deepCopyTowerState`, now public). `createTransientAudioCommandWithModifications` also switched from a shallow spread to a deep copy, since its `Object.assign` calls on nested drum/layer/beam objects were mutating the caller's original state.
|
|
17
|
+
- **Drum-position tracking after rotation.** As a consequence of the above, `rotate()` and `rotateWithState()` — which mutated the (now-copied) state directly — needed explicit fixes: `rotate()` now calls `setTowerState` after updating drum positions so the change is actually recorded and notified; `rotateWithState()`'s `finally` block no longer force-writes all three drum positions unconditionally, since that masked partial failures (a drum that never rotated could be recorded as if it had) and was redundant on success (each `rotateDrumStateful()` call already records its own drum).
|
|
18
|
+
- **Send failures are no longer swallowed.** `sendTowerCommandDirect` returned normally instead of throwing when not connected or after retries were exhausted, so the command queue only ever learned of the failure via its 30s timeout — any command issued while disconnected silently hung for 30 seconds before rejecting with a misleading "Command timeout". It now throws immediately in both cases.
|
|
19
|
+
- **Calibration flags are now set before the command is sent**, not after `await`, so a fast calibration-complete response can't arrive before `performingCalibration` is set (which would suppress heartbeat disconnect detection and never fire `onCalibrationComplete`). The flags are cleared again if the send fails.
|
|
20
|
+
- **`logDetail` no longer discards the command queue.** Setting `tower.logDetail` used to reconstruct the whole `UdtTowerCommands`/`CommandQueue`, orphaning any in-flight/queued commands (they could then only resolve via their own 30s timeout, since responses routed to the new queue). It now updates the existing instance in place. `retrySendCommandMax` is likewise now a live setter instead of a value copied once at construction (previously it only "refreshed" as an accidental side effect of toggling `logDetail`).
|
|
21
|
+
- **`breakSeal` now syncs volume whenever it actually changes**, not just when the requested volume is truthy. Requesting volume `0` (Loud) while the tower's tracked volume was non-zero previously skipped the sync entirely, so the seal sound played at the wrong volume.
|
|
22
|
+
- **Removed duplicate console logging** — `Logger`'s constructor already installs a default `ConsoleOutput`; `UltimateDarkTower` was adding a second one, duplicating every default log line.
|
|
23
|
+
- **Unsolicited tower notifications no longer resolve the wrong queued command.** Spontaneous mechanical-sensor notifications (jiggle detection, unexpected trigger, differential readings) can arrive at any time and were being treated like a command ack, prematurely completing whatever command was in flight. These are now excluded from queue resolution while still reaching the public raw `onTowerResponse` passthrough.
|
|
24
|
+
- **`DOMOutput` no longer goes silent on pages without `logLevel-*` checkboxes** — it now defaults to showing all levels when none of the expected checkbox elements exist, instead of filtering out all output.
|
|
25
|
+
- Diagnostics `LIBRARY_VERSION` corrected from a stale `3.0.0` to `5.0.0`.
|
|
26
|
+
- **NodeBluetoothAdapter** no longer leaks a noble `stateChange` listener across repeated connect/disconnect cycles (it's now removed in `disconnect()`, not just `cleanup()`).
|
|
27
|
+
- **WebBluetoothAdapter** now cleans up partial connection state (GATT connection, listeners) when `connect()` fails partway through, matching the Node adapter's existing behavior.
|
|
28
|
+
- **Sound index validation tightened.** `playSound`/`playSoundStateful` were checking `soundIndex === null`, which let `undefined`/`NaN` silently through as a "valid" index; they now use `Number.isFinite`. `rotate()`'s optional `soundIndex` is now bounds-checked against the audio library (previously written unvalidated).
|
|
29
|
+
- `UdtBleConnection.disconnect()` no longer fires `onTowerDisconnect` (and clears the command queue) when called on a connection that was never established.
|
|
30
|
+
- Battery log line now correctly labels its already-volts value as `v` instead of `mv`.
|
|
31
|
+
- **`SystemRandom.nextRange`'s large-range branch (> `Int32.MaxValue`) now faithfully ports .NET's `GetSampleForLargeRange()`** instead of a rougher approximation, so seed-derived sequences match the reference implementation for wide ranges too.
|
|
32
|
+
- Disconnect detection in `sendTowerCommandDirect` now checks `instanceof BluetoothConnectionError` first, falling back to message-substring checks only for untyped native errors; removed a dead substring check that could never match.
|
|
33
|
+
- Removed the deprecated, uncalled `updateGlyphPositionsForRotation` method.
|
|
34
|
+
- `commandToPacketString` now zero-pads each byte to 2 hex digits (e.g. `[0f,03]` instead of `[f,3]`), removing ambiguity in packet dumps/logs.
|
|
35
|
+
|
|
9
36
|
## [5.0.0] - 2026-06-20
|
|
10
37
|
|
|
11
38
|
### Changed
|
package/dist/esm/index.mjs
CHANGED
|
@@ -473,6 +473,7 @@ var init_WebBluetoothAdapter = __esm({
|
|
|
473
473
|
this.boundOnCharacteristicValueChanged
|
|
474
474
|
);
|
|
475
475
|
} catch (error) {
|
|
476
|
+
await this.cleanup();
|
|
476
477
|
if (error instanceof BluetoothDeviceNotFoundError || error instanceof BluetoothUserCancelledError || error instanceof BluetoothConnectionError) {
|
|
477
478
|
throw error;
|
|
478
479
|
}
|
|
@@ -677,6 +678,10 @@ var init_NodeBluetoothAdapter = __esm({
|
|
|
677
678
|
}
|
|
678
679
|
}
|
|
679
680
|
async disconnect() {
|
|
681
|
+
if (noble && this.boundStateChangeHandler) {
|
|
682
|
+
noble.removeListener("stateChange", this.boundStateChangeHandler);
|
|
683
|
+
this.boundStateChangeHandler = void 0;
|
|
684
|
+
}
|
|
680
685
|
if (!this.peripheral) return;
|
|
681
686
|
try {
|
|
682
687
|
if (this.rxCharacteristic) {
|
|
@@ -798,14 +803,6 @@ var init_NodeBluetoothAdapter = __esm({
|
|
|
798
803
|
return info;
|
|
799
804
|
}
|
|
800
805
|
async cleanup() {
|
|
801
|
-
if (noble) {
|
|
802
|
-
if (this.boundStateChangeHandler) {
|
|
803
|
-
noble.removeListener(
|
|
804
|
-
"stateChange",
|
|
805
|
-
this.boundStateChangeHandler
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
806
|
if (this.peripheral && this.boundDisconnectHandler) {
|
|
810
807
|
this.peripheral.removeListener(
|
|
811
808
|
"disconnect",
|
|
@@ -813,9 +810,6 @@ var init_NodeBluetoothAdapter = __esm({
|
|
|
813
810
|
);
|
|
814
811
|
}
|
|
815
812
|
await this.disconnect();
|
|
816
|
-
this.characteristicCallback = void 0;
|
|
817
|
-
this.disconnectCallback = void 0;
|
|
818
|
-
this.availabilityCallback = void 0;
|
|
819
813
|
}
|
|
820
814
|
/**
|
|
821
815
|
* Scans for a BLE device by name using Noble's event-driven discovery
|
|
@@ -1075,10 +1069,7 @@ function commandToPacketString(command) {
|
|
|
1075
1069
|
if (command.length === 0) {
|
|
1076
1070
|
return "[]";
|
|
1077
1071
|
}
|
|
1078
|
-
|
|
1079
|
-
command.forEach((n) => cmdStr += n.toString(16) + ",");
|
|
1080
|
-
cmdStr = cmdStr.slice(0, -1) + "]";
|
|
1081
|
-
return cmdStr;
|
|
1072
|
+
return `[${Array.from(command).map((n) => n.toString(16).padStart(2, "0")).join(",")}]`;
|
|
1082
1073
|
}
|
|
1083
1074
|
function parseDifferentialReadings(response) {
|
|
1084
1075
|
if (response.length < 5 || response[0] !== 6) {
|
|
@@ -1222,15 +1213,18 @@ var DOMOutput = class {
|
|
|
1222
1213
|
this.updateBufferSizeDisplay();
|
|
1223
1214
|
}
|
|
1224
1215
|
getEnabledLevelsFromCheckboxes() {
|
|
1225
|
-
const
|
|
1216
|
+
const levelNames = ["debug", "info", "warn", "error"];
|
|
1226
1217
|
if (typeof document === "undefined") {
|
|
1227
|
-
return
|
|
1218
|
+
return new Set(levelNames);
|
|
1219
|
+
}
|
|
1220
|
+
const checkboxes = levelNames.map((level) => document.getElementById(`logLevel-${level}`));
|
|
1221
|
+
if (checkboxes.every((checkbox) => !checkbox)) {
|
|
1222
|
+
return new Set(levelNames);
|
|
1228
1223
|
}
|
|
1229
|
-
const
|
|
1230
|
-
checkboxes.forEach((
|
|
1231
|
-
const checkbox = document.getElementById(`logLevel-${level}`);
|
|
1224
|
+
const enabledLevels = /* @__PURE__ */ new Set();
|
|
1225
|
+
checkboxes.forEach((checkbox, index) => {
|
|
1232
1226
|
if (checkbox && checkbox.checked) {
|
|
1233
|
-
enabledLevels.add(
|
|
1227
|
+
enabledLevels.add(levelNames[index]);
|
|
1234
1228
|
}
|
|
1235
1229
|
});
|
|
1236
1230
|
return enabledLevels;
|
|
@@ -1508,7 +1502,7 @@ var TowerResponseProcessor = class {
|
|
|
1508
1502
|
return [towerCommand.name, commandToPacketString(command)];
|
|
1509
1503
|
case TC.BATTERY: {
|
|
1510
1504
|
const millivolts = getMilliVoltsFromTowerResponse(command);
|
|
1511
|
-
const retval = [towerCommand.name, `${milliVoltsToPercentage(millivolts)} (${(millivolts / 1e3).toFixed(2)}
|
|
1505
|
+
const retval = [towerCommand.name, `${milliVoltsToPercentage(millivolts)} (${(millivolts / 1e3).toFixed(2)}v)`];
|
|
1512
1506
|
if (this.logDetail) {
|
|
1513
1507
|
retval.push(commandToPacketString(command));
|
|
1514
1508
|
}
|
|
@@ -1547,6 +1541,17 @@ var TowerResponseProcessor = class {
|
|
|
1547
1541
|
isTowerStateResponse(cmdKey) {
|
|
1548
1542
|
return cmdKey === TC.STATE;
|
|
1549
1543
|
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Checks if a command is a spontaneous mechanical-sensor notification that
|
|
1546
|
+
* isn't tied to any specific in-flight command (jiggle detection,
|
|
1547
|
+
* unexpected trigger, differential sensor readings). These can arrive at
|
|
1548
|
+
* any time and should not be treated as the ack for a queued command.
|
|
1549
|
+
* @param {string} cmdKey - Command key from tower message
|
|
1550
|
+
* @returns {boolean} True if this is an unsolicited notification
|
|
1551
|
+
*/
|
|
1552
|
+
isUnsolicitedResponse(cmdKey) {
|
|
1553
|
+
return cmdKey === TC.JIGGLE || cmdKey === TC.UNEXPECTED || cmdKey === TC.DIFFERENTIAL;
|
|
1554
|
+
}
|
|
1550
1555
|
};
|
|
1551
1556
|
|
|
1552
1557
|
// src/udtBluetoothAdapterFactory.ts
|
|
@@ -1758,9 +1763,10 @@ var UdtBleConnection = class {
|
|
|
1758
1763
|
}
|
|
1759
1764
|
async disconnect() {
|
|
1760
1765
|
this.stopConnectionMonitoring();
|
|
1761
|
-
if (this.isConnected) {
|
|
1762
|
-
|
|
1766
|
+
if (!this.isConnected) {
|
|
1767
|
+
return;
|
|
1763
1768
|
}
|
|
1769
|
+
this.recordIncident("user_initiated");
|
|
1764
1770
|
const adapter = this.bluetoothAdapter;
|
|
1765
1771
|
if (adapter?.isConnected()) {
|
|
1766
1772
|
await adapter.disconnect();
|
|
@@ -2161,7 +2167,7 @@ var UdtCommandFactory = class {
|
|
|
2161
2167
|
audio: audioMods
|
|
2162
2168
|
};
|
|
2163
2169
|
const command = this.createStatefulCommand(currentState, modifications);
|
|
2164
|
-
const stateWithoutAudio = currentState ?
|
|
2170
|
+
const stateWithoutAudio = currentState ? this.deepCopyTowerState(currentState) : this.createEmptyTowerState();
|
|
2165
2171
|
if (otherModifications.drum) {
|
|
2166
2172
|
otherModifications.drum.forEach((drum, index) => {
|
|
2167
2173
|
if (drum && stateWithoutAudio.drum[index]) {
|
|
@@ -2273,6 +2279,7 @@ var UdtCommandFactory = class {
|
|
|
2273
2279
|
|
|
2274
2280
|
// src/udtTowerCommands.ts
|
|
2275
2281
|
init_udtConstants();
|
|
2282
|
+
init_udtBluetoothAdapter();
|
|
2276
2283
|
|
|
2277
2284
|
// src/udtCommandQueue.ts
|
|
2278
2285
|
var CommandQueue = class {
|
|
@@ -2446,13 +2453,13 @@ var UdtTowerCommands = class {
|
|
|
2446
2453
|
* @returns Promise that resolves when command is sent successfully
|
|
2447
2454
|
*/
|
|
2448
2455
|
async sendTowerCommandDirect(command) {
|
|
2456
|
+
const cmdStr = commandToPacketString(command);
|
|
2457
|
+
this.deps.logDetail && this.deps.logger.debug(`${cmdStr}`, "[UDT][CMD]");
|
|
2458
|
+
if (!this.deps.bleConnection.isConnected) {
|
|
2459
|
+
this.deps.logger.warn("Tower is not connected", "[UDT][CMD]");
|
|
2460
|
+
throw new Error("Cannot send command: tower is not connected");
|
|
2461
|
+
}
|
|
2449
2462
|
try {
|
|
2450
|
-
const cmdStr = commandToPacketString(command);
|
|
2451
|
-
this.deps.logDetail && this.deps.logger.debug(`${cmdStr}`, "[UDT][CMD]");
|
|
2452
|
-
if (!this.deps.bleConnection.isConnected) {
|
|
2453
|
-
this.deps.logger.warn("Tower is not connected", "[UDT][CMD]");
|
|
2454
|
-
return;
|
|
2455
|
-
}
|
|
2456
2463
|
await this.deps.bleConnection.writeCommand(command);
|
|
2457
2464
|
this.deps.retrySendCommandCount.value = 0;
|
|
2458
2465
|
this.deps.bleConnection.lastSuccessfulCommand = Date.now();
|
|
@@ -2461,11 +2468,12 @@ var UdtTowerCommands = class {
|
|
|
2461
2468
|
const errorMsg = error?.message ?? String(error);
|
|
2462
2469
|
const wasCancelled = errorMsg.includes("User cancelled");
|
|
2463
2470
|
const maxRetriesReached = this.deps.retrySendCommandCount.value >= this.deps.retrySendCommandMax;
|
|
2464
|
-
const
|
|
2471
|
+
const sendError = error instanceof Error ? error : new Error(errorMsg);
|
|
2472
|
+
const isDisconnected = error instanceof BluetoothConnectionError || errorMsg.includes("Cannot read properties of null") || errorMsg.includes("GATT Server is disconnected") || errorMsg.includes("Device is not connected") || !this.deps.bleConnection.isConnected;
|
|
2465
2473
|
if (isDisconnected) {
|
|
2466
2474
|
this.deps.logger.warn("Disconnect detected during command send", "[UDT][CMD]");
|
|
2467
2475
|
await this.deps.bleConnection.disconnect();
|
|
2468
|
-
|
|
2476
|
+
throw sendError;
|
|
2469
2477
|
}
|
|
2470
2478
|
if (!maxRetriesReached && this.deps.bleConnection.isConnected && !wasCancelled) {
|
|
2471
2479
|
this.deps.logger.info(`retrying tower command attempt ${this.deps.retrySendCommandCount.value + 1}`, "[UDT][CMD]");
|
|
@@ -2473,9 +2481,9 @@ var UdtTowerCommands = class {
|
|
|
2473
2481
|
const delay = 250 * this.deps.retrySendCommandCount.value;
|
|
2474
2482
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2475
2483
|
return await this.sendTowerCommandDirect(command);
|
|
2476
|
-
} else {
|
|
2477
|
-
this.deps.retrySendCommandCount.value = 0;
|
|
2478
2484
|
}
|
|
2485
|
+
this.deps.retrySendCommandCount.value = 0;
|
|
2486
|
+
throw sendError;
|
|
2479
2487
|
}
|
|
2480
2488
|
}
|
|
2481
2489
|
/**
|
|
@@ -2484,16 +2492,21 @@ var UdtTowerCommands = class {
|
|
|
2484
2492
|
* @returns Promise that resolves when calibration command is sent
|
|
2485
2493
|
*/
|
|
2486
2494
|
async calibrate() {
|
|
2487
|
-
if (
|
|
2488
|
-
this.deps.logger.
|
|
2489
|
-
this.deps.recorder?.recordEvent("calibration_started");
|
|
2490
|
-
await this.sendTowerCommand(new Uint8Array([TOWER_COMMANDS.calibration]), "calibrate");
|
|
2491
|
-
this.deps.bleConnection.performingCalibration = true;
|
|
2492
|
-
this.deps.bleConnection.performingLongCommand = true;
|
|
2495
|
+
if (this.deps.bleConnection.performingCalibration) {
|
|
2496
|
+
this.deps.logger.warn("Tower calibration requested when tower is already performing calibration", "[UDT][CMD]");
|
|
2493
2497
|
return;
|
|
2494
2498
|
}
|
|
2495
|
-
this.deps.logger.
|
|
2496
|
-
|
|
2499
|
+
this.deps.logger.info("Performing Tower Calibration", "[UDT][CMD]");
|
|
2500
|
+
this.deps.recorder?.recordEvent("calibration_started");
|
|
2501
|
+
this.deps.bleConnection.performingCalibration = true;
|
|
2502
|
+
this.deps.bleConnection.performingLongCommand = true;
|
|
2503
|
+
try {
|
|
2504
|
+
await this.sendTowerCommand(new Uint8Array([TOWER_COMMANDS.calibration]), "calibrate");
|
|
2505
|
+
} catch (error) {
|
|
2506
|
+
this.deps.bleConnection.performingCalibration = false;
|
|
2507
|
+
this.deps.bleConnection.performingLongCommand = false;
|
|
2508
|
+
throw error;
|
|
2509
|
+
}
|
|
2497
2510
|
}
|
|
2498
2511
|
/**
|
|
2499
2512
|
* Plays a sound from the tower's audio library using stateful commands that preserve existing tower state.
|
|
@@ -2502,7 +2515,7 @@ var UdtTowerCommands = class {
|
|
|
2502
2515
|
* @returns Promise that resolves when sound command is sent
|
|
2503
2516
|
*/
|
|
2504
2517
|
async playSound(soundIndex) {
|
|
2505
|
-
const invalidIndex = soundIndex
|
|
2518
|
+
const invalidIndex = !Number.isFinite(soundIndex) || soundIndex > Object.keys(TOWER_AUDIO_LIBRARY).length || soundIndex <= 0;
|
|
2506
2519
|
if (invalidIndex) {
|
|
2507
2520
|
this.deps.logger.error(`attempt to play invalid sound index ${soundIndex}`, "[UDT][CMD]");
|
|
2508
2521
|
return;
|
|
@@ -2691,8 +2704,13 @@ var UdtTowerCommands = class {
|
|
|
2691
2704
|
async rotate(top, middle, bottom, soundIndex) {
|
|
2692
2705
|
this.deps.logDetail && this.deps.logger.debug(`Rotate Parameter TMB[${JSON.stringify(top)}|${middle}|${bottom}] S[${soundIndex}]`, "[UDT][CMD]");
|
|
2693
2706
|
const rotateCommand = this.deps.commandFactory.createRotateCommand(top, middle, bottom);
|
|
2694
|
-
if (soundIndex) {
|
|
2695
|
-
|
|
2707
|
+
if (soundIndex !== void 0) {
|
|
2708
|
+
const validSoundIndex = Number.isFinite(soundIndex) && soundIndex > 0 && soundIndex <= Object.keys(TOWER_AUDIO_LIBRARY).length;
|
|
2709
|
+
if (validSoundIndex) {
|
|
2710
|
+
rotateCommand[AUDIO_COMMAND_POS] = soundIndex;
|
|
2711
|
+
} else {
|
|
2712
|
+
this.deps.logger.error(`attempt to play invalid sound index ${soundIndex} during rotate`, "[UDT][CMD]");
|
|
2713
|
+
}
|
|
2696
2714
|
}
|
|
2697
2715
|
this.deps.logger.info("Sending rotate command" + (soundIndex ? " with sound" : ""), "[UDT]");
|
|
2698
2716
|
this.deps.bleConnection.performingLongCommand = true;
|
|
@@ -2711,6 +2729,7 @@ var UdtTowerCommands = class {
|
|
|
2711
2729
|
towerState.drum[0].position = topPosition;
|
|
2712
2730
|
towerState.drum[1].position = middlePosition;
|
|
2713
2731
|
towerState.drum[2].position = bottomPosition;
|
|
2732
|
+
this.deps.setTowerState(towerState, "rotate");
|
|
2714
2733
|
}
|
|
2715
2734
|
}
|
|
2716
2735
|
/**
|
|
@@ -2743,12 +2762,6 @@ var UdtTowerCommands = class {
|
|
|
2743
2762
|
this.deps.bleConnection.performingLongCommand = false;
|
|
2744
2763
|
this.deps.bleConnection.lastBatteryHeartbeat = Date.now();
|
|
2745
2764
|
}, this.deps.bleConnection.longTowerCommandTimeout);
|
|
2746
|
-
const towerState = this.deps.getCurrentTowerState();
|
|
2747
|
-
if (towerState) {
|
|
2748
|
-
towerState.drum[0].position = positionMap[top];
|
|
2749
|
-
towerState.drum[1].position = positionMap[middle];
|
|
2750
|
-
towerState.drum[2].position = positionMap[bottom];
|
|
2751
|
-
}
|
|
2752
2765
|
}
|
|
2753
2766
|
}
|
|
2754
2767
|
/**
|
|
@@ -2773,9 +2786,9 @@ var UdtTowerCommands = class {
|
|
|
2773
2786
|
* @returns Promise that resolves when seal break sequence is complete
|
|
2774
2787
|
*/
|
|
2775
2788
|
async breakSeal(seal, volume) {
|
|
2776
|
-
const
|
|
2777
|
-
|
|
2778
|
-
|
|
2789
|
+
const currentState = this.deps.getCurrentTowerState();
|
|
2790
|
+
const actualVolume = volume !== void 0 ? volume : currentState.audio.volume;
|
|
2791
|
+
if (actualVolume !== currentState.audio.volume) {
|
|
2779
2792
|
const stateWithVolume = { ...currentState };
|
|
2780
2793
|
stateWithVolume.audio = { sample: 0, loop: false, volume: actualVolume };
|
|
2781
2794
|
await this.sendTowerStateStateful(stateWithVolume);
|
|
@@ -2906,7 +2919,7 @@ var UdtTowerCommands = class {
|
|
|
2906
2919
|
* @returns Promise that resolves when command is sent
|
|
2907
2920
|
*/
|
|
2908
2921
|
async playSoundStateful(soundIndex, loop = false, volume) {
|
|
2909
|
-
const invalidIndex = soundIndex
|
|
2922
|
+
const invalidIndex = !Number.isFinite(soundIndex) || soundIndex > Object.keys(TOWER_AUDIO_LIBRARY).length || soundIndex <= 0;
|
|
2910
2923
|
if (invalidIndex) {
|
|
2911
2924
|
this.deps.logger.error(`attempt to play invalid sound index ${soundIndex}`, "[UDT][CMD]");
|
|
2912
2925
|
return;
|
|
@@ -2956,6 +2969,20 @@ var UdtTowerCommands = class {
|
|
|
2956
2969
|
await this.sendTowerCommand(command, "sendTowerStateStateful");
|
|
2957
2970
|
}
|
|
2958
2971
|
//#endregion
|
|
2972
|
+
/**
|
|
2973
|
+
* Updates the detailed-logging flag without discarding the command queue.
|
|
2974
|
+
* @param value - Whether detailed logging is enabled
|
|
2975
|
+
*/
|
|
2976
|
+
updateLogDetail(value) {
|
|
2977
|
+
this.deps.logDetail = value;
|
|
2978
|
+
}
|
|
2979
|
+
/**
|
|
2980
|
+
* Updates the max retry count without discarding the command queue.
|
|
2981
|
+
* @param value - New max retry count
|
|
2982
|
+
*/
|
|
2983
|
+
updateRetrySendCommandMax(value) {
|
|
2984
|
+
this.deps.retrySendCommandMax = value;
|
|
2985
|
+
}
|
|
2959
2986
|
/**
|
|
2960
2987
|
* Public access to sendTowerCommandDirect for testing purposes.
|
|
2961
2988
|
* This bypasses the command queue and sends commands directly.
|
|
@@ -2991,7 +3018,7 @@ var RING_BUFFER_SIZE = 500;
|
|
|
2991
3018
|
var RING_BUFFER_DRAIN = 50;
|
|
2992
3019
|
var BATTERY_HISTORY_SIZE = 60;
|
|
2993
3020
|
var PAYLOAD_MAX_BYTES = 32;
|
|
2994
|
-
var LIBRARY_VERSION = "
|
|
3021
|
+
var LIBRARY_VERSION = "5.0.0";
|
|
2995
3022
|
function detectPlatform() {
|
|
2996
3023
|
if (typeof window !== "undefined" && typeof window.navigator !== "undefined") {
|
|
2997
3024
|
return "web";
|
|
@@ -3183,7 +3210,7 @@ var UltimateDarkTower = class {
|
|
|
3183
3210
|
this.beforeUnloadHandler = null;
|
|
3184
3211
|
// tower configuration
|
|
3185
3212
|
this.retrySendCommandCountRef = { value: 0 };
|
|
3186
|
-
this.
|
|
3213
|
+
this._retrySendCommandMax = DEFAULT_RETRY_SEND_COMMAND_MAX;
|
|
3187
3214
|
// tower state
|
|
3188
3215
|
this.currentBatteryValue = 0;
|
|
3189
3216
|
this.previousBatteryValue = 0;
|
|
@@ -3241,7 +3268,6 @@ var UltimateDarkTower = class {
|
|
|
3241
3268
|
*/
|
|
3242
3269
|
initializeLogger() {
|
|
3243
3270
|
this.logger = new Logger();
|
|
3244
|
-
this.logger.addOutput(new ConsoleOutput());
|
|
3245
3271
|
}
|
|
3246
3272
|
/**
|
|
3247
3273
|
* Initialize the diagnostics recorder. Always constructed; `enabled` defaults
|
|
@@ -3307,20 +3333,20 @@ var UltimateDarkTower = class {
|
|
|
3307
3333
|
*/
|
|
3308
3334
|
setupTowerResponseCallback() {
|
|
3309
3335
|
this.towerEventCallbacks.onTowerResponse = (response) => {
|
|
3310
|
-
this.
|
|
3311
|
-
if (
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3336
|
+
const { cmdKey } = this.responseProcessor.getTowerCommand(response[0]);
|
|
3337
|
+
if (!this.responseProcessor.isUnsolicitedResponse(cmdKey)) {
|
|
3338
|
+
this.towerCommands.onTowerResponse();
|
|
3339
|
+
}
|
|
3340
|
+
if (response.length >= TOWER_STATE_RESPONSE_MIN_LENGTH && this.responseProcessor.isTowerStateResponse(cmdKey)) {
|
|
3341
|
+
const stateData = response.slice(TOWER_STATE_DATA_OFFSET, TOWER_STATE_RESPONSE_MIN_LENGTH);
|
|
3342
|
+
this.updateTowerStateFromResponse(stateData);
|
|
3317
3343
|
}
|
|
3318
3344
|
this.onTowerResponse(response);
|
|
3319
3345
|
};
|
|
3320
3346
|
}
|
|
3321
3347
|
/**
|
|
3322
|
-
|
|
3323
|
-
|
|
3348
|
+
* Create tower event callbacks for BLE connection
|
|
3349
|
+
*/
|
|
3324
3350
|
createTowerEventCallbacks() {
|
|
3325
3351
|
return {
|
|
3326
3352
|
onTowerConnect: () => this.onTowerConnect(),
|
|
@@ -3355,8 +3381,12 @@ var UltimateDarkTower = class {
|
|
|
3355
3381
|
responseProcessor: this.responseProcessor,
|
|
3356
3382
|
logDetail: this.logDetail,
|
|
3357
3383
|
retrySendCommandCount: this.retrySendCommandCountRef,
|
|
3358
|
-
retrySendCommandMax: this.
|
|
3359
|
-
|
|
3384
|
+
retrySendCommandMax: this._retrySendCommandMax,
|
|
3385
|
+
// Return a deep copy so command builders can mutate it in place before
|
|
3386
|
+
// handing it to setTowerState() without aliasing the live state (the
|
|
3387
|
+
// old/new state passed to onTowerStateUpdate would otherwise be the
|
|
3388
|
+
// same object).
|
|
3389
|
+
getCurrentTowerState: () => this.commandFactory.deepCopyTowerState(this.currentTowerState),
|
|
3360
3390
|
setTowerState: (newState, source) => this.setTowerState(newState, source),
|
|
3361
3391
|
recorder: this.diagnosticsRecorder
|
|
3362
3392
|
};
|
|
@@ -3377,15 +3407,17 @@ var UltimateDarkTower = class {
|
|
|
3377
3407
|
this._logDetail = value;
|
|
3378
3408
|
this.responseProcessor.setDetailedLogging(value);
|
|
3379
3409
|
if (this.towerCommands) {
|
|
3380
|
-
this.
|
|
3410
|
+
this.towerCommands.updateLogDetail(value);
|
|
3381
3411
|
}
|
|
3382
3412
|
}
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
this.towerCommands
|
|
3413
|
+
get retrySendCommandMax() {
|
|
3414
|
+
return this._retrySendCommandMax;
|
|
3415
|
+
}
|
|
3416
|
+
set retrySendCommandMax(value) {
|
|
3417
|
+
this._retrySendCommandMax = value;
|
|
3418
|
+
if (this.towerCommands) {
|
|
3419
|
+
this.towerCommands.updateRetrySendCommandMax(value);
|
|
3420
|
+
}
|
|
3389
3421
|
}
|
|
3390
3422
|
// Getter methods for connection state
|
|
3391
3423
|
get isConnected() {
|
|
@@ -3608,7 +3640,7 @@ var UltimateDarkTower = class {
|
|
|
3608
3640
|
* @returns The current tower state object
|
|
3609
3641
|
*/
|
|
3610
3642
|
getCurrentTowerState() {
|
|
3611
|
-
return
|
|
3643
|
+
return this.commandFactory.deepCopyTowerState(this.currentTowerState);
|
|
3612
3644
|
}
|
|
3613
3645
|
/**
|
|
3614
3646
|
* Sends a complete tower state to the tower, preserving existing state.
|
|
@@ -3775,23 +3807,6 @@ var UltimateDarkTower = class {
|
|
|
3775
3807
|
this.updateGlyphPositionsAfterRotation(level, rotationSteps);
|
|
3776
3808
|
}
|
|
3777
3809
|
}
|
|
3778
|
-
/**
|
|
3779
|
-
* Updates glyph positions for a specific level rotation.
|
|
3780
|
-
* @param level - The drum level that was rotated
|
|
3781
|
-
* @param newPosition - The new position the drum was rotated to
|
|
3782
|
-
* @deprecated Use calculateAndUpdateGlyphPositions instead
|
|
3783
|
-
*/
|
|
3784
|
-
updateGlyphPositionsForRotation(level, newPosition) {
|
|
3785
|
-
const currentPosition = this.getCurrentDrumPosition(level);
|
|
3786
|
-
const sides = ["north", "east", "south", "west"];
|
|
3787
|
-
const currentIndex = sides.indexOf(currentPosition);
|
|
3788
|
-
const newIndex = sides.indexOf(newPosition);
|
|
3789
|
-
let rotationSteps = newIndex - currentIndex;
|
|
3790
|
-
if (rotationSteps < 0) {
|
|
3791
|
-
rotationSteps += TOWER_SIDES_COUNT;
|
|
3792
|
-
}
|
|
3793
|
-
this.updateGlyphPositionsAfterRotation(level, rotationSteps);
|
|
3794
|
-
}
|
|
3795
3810
|
/**
|
|
3796
3811
|
* Checks if a specific seal is broken.
|
|
3797
3812
|
* @param seal - The seal identifier to check
|
|
@@ -6436,6 +6451,22 @@ var SystemRandom = class {
|
|
|
6436
6451
|
sample() {
|
|
6437
6452
|
return this.internalSample() * (1 / INT32_MAX);
|
|
6438
6453
|
}
|
|
6454
|
+
/**
|
|
6455
|
+
* Sample for ranges wider than Int32.MaxValue.
|
|
6456
|
+
* Matches C#'s GetSampleForLargeRange(): draws two internal samples (the
|
|
6457
|
+
* second decides sign) and normalizes to [0.0, 1.0).
|
|
6458
|
+
*/
|
|
6459
|
+
getSampleForLargeRange() {
|
|
6460
|
+
let result = this.internalSample();
|
|
6461
|
+
const negative = this.internalSample() % 2 === 0;
|
|
6462
|
+
if (negative) {
|
|
6463
|
+
result = -result;
|
|
6464
|
+
}
|
|
6465
|
+
let d = result;
|
|
6466
|
+
d += INT32_MAX - 1;
|
|
6467
|
+
d /= 2 * INT32_MAX - 1;
|
|
6468
|
+
return d;
|
|
6469
|
+
}
|
|
6439
6470
|
/**
|
|
6440
6471
|
* Returns a non-negative random integer less than Int32.MaxValue.
|
|
6441
6472
|
* Matches C# `Random.Next()`.
|
|
@@ -6465,7 +6496,7 @@ var SystemRandom = class {
|
|
|
6465
6496
|
if (range <= INT32_MAX) {
|
|
6466
6497
|
return toInt32(this.sample() * range) + minValue;
|
|
6467
6498
|
}
|
|
6468
|
-
return
|
|
6499
|
+
return Math.floor(this.getSampleForLargeRange() * range) + minValue;
|
|
6469
6500
|
}
|
|
6470
6501
|
/**
|
|
6471
6502
|
* Returns a random double in range [0.0, 1.0).
|
|
@@ -51,7 +51,7 @@ declare class UltimateDarkTower {
|
|
|
51
51
|
private commandFactory;
|
|
52
52
|
private towerCommands;
|
|
53
53
|
private retrySendCommandCountRef;
|
|
54
|
-
|
|
54
|
+
private _retrySendCommandMax;
|
|
55
55
|
currentBatteryValue: number;
|
|
56
56
|
previousBatteryValue: number;
|
|
57
57
|
currentBatteryPercentage: number;
|
|
@@ -96,7 +96,8 @@ declare class UltimateDarkTower {
|
|
|
96
96
|
/**
|
|
97
97
|
* Set up the tower response callback after all components are initialized
|
|
98
98
|
*/
|
|
99
|
-
private setupTowerResponseCallback;
|
|
99
|
+
private setupTowerResponseCallback;
|
|
100
|
+
/**
|
|
100
101
|
* Create tower event callbacks for BLE connection
|
|
101
102
|
*/
|
|
102
103
|
private createTowerEventCallbacks;
|
|
@@ -111,10 +112,8 @@ declare class UltimateDarkTower {
|
|
|
111
112
|
private _logDetail;
|
|
112
113
|
get logDetail(): boolean;
|
|
113
114
|
set logDetail(value: boolean);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*/
|
|
117
|
-
private updateTowerCommandDependencies;
|
|
115
|
+
get retrySendCommandMax(): number;
|
|
116
|
+
set retrySendCommandMax(value: number);
|
|
118
117
|
get isConnected(): boolean;
|
|
119
118
|
get isCalibrated(): boolean;
|
|
120
119
|
get performingCalibration(): boolean;
|
|
@@ -316,13 +315,6 @@ declare class UltimateDarkTower {
|
|
|
316
315
|
* @param newPosition - The position after rotation
|
|
317
316
|
*/
|
|
318
317
|
private calculateAndUpdateGlyphPositions;
|
|
319
|
-
/**
|
|
320
|
-
* Updates glyph positions for a specific level rotation.
|
|
321
|
-
* @param level - The drum level that was rotated
|
|
322
|
-
* @param newPosition - The new position the drum was rotated to
|
|
323
|
-
* @deprecated Use calculateAndUpdateGlyphPositions instead
|
|
324
|
-
*/
|
|
325
|
-
private updateGlyphPositionsForRotation;
|
|
326
318
|
/**
|
|
327
319
|
* Checks if a specific seal is broken.
|
|
328
320
|
* @param seal - The seal identifier to check
|