ultimatedarktower 2.2.0 → 2.5.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 +92 -45
- package/README.md +29 -0
- package/dist/esm/index.mjs +80 -34
- package/dist/src/UltimateDarkTower.d.ts +29 -1
- package/dist/src/UltimateDarkTower.js +66 -8
- package/dist/src/UltimateDarkTower.js.map +1 -1
- package/dist/src/adapters/NodeBluetoothAdapter.js.map +1 -1
- package/dist/src/adapters/WebBluetoothAdapter.js.map +1 -1
- package/dist/src/udtBleConnection.js +1 -1
- package/dist/src/udtBleConnection.js.map +1 -1
- package/dist/src/udtBluetoothAdapterFactory.js.map +1 -1
- package/dist/src/udtCommandFactory.d.ts +3 -3
- package/dist/src/udtCommandFactory.js +3 -3
- package/dist/src/udtCommandFactory.js.map +1 -1
- package/dist/src/udtCommandQueue.js.map +1 -1
- package/dist/src/udtConstants.js +6 -6
- package/dist/src/udtConstants.js.map +1 -1
- package/dist/src/udtHelpers.js +8 -9
- package/dist/src/udtHelpers.js.map +1 -1
- package/dist/src/udtLogger.js.map +1 -1
- package/dist/src/udtTowerCommands.d.ts +1 -1
- package/dist/src/udtTowerCommands.js +8 -4
- package/dist/src/udtTowerCommands.js.map +1 -1
- package/dist/src/udtTowerResponse.js.map +1 -1
- package/dist/src/udtTowerState.js +5 -4
- package/dist/src/udtTowerState.js.map +1 -1
- package/package.json +19 -15
package/CHANGELOG.md
CHANGED
|
@@ -6,100 +6,147 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.5.0] - 2026-03-23
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`markSealBroken()` method** — Marks a seal as broken in software tracking without sending hardware commands. Enables restoring game state (e.g., resuming a saved game).
|
|
14
|
+
- **`markSealRestored()` method** — Marks a seal as unbroken in software tracking without sending hardware commands. Enables undoing a seal break or restoring individual seals.
|
|
15
|
+
- **`brokenSeals` config option** — Accepts an array of `SealIdentifier` in `UltimateDarkTowerConfig` to initialize seal state at construction time.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Improved seal management documentation** — Reference.md now explains that seals are physical plastic covers on the tower (12 total), that seal state is tracked purely in software (not by firmware), and documents all new seal state management APIs.
|
|
20
|
+
|
|
21
|
+
## [2.4.0] - 2026-03-19
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **Migrated project baseline to Node.js 18+** — Updated `engines.node` to `>=18.0.0`, aligned CI matrix validation to Node 18 and 20, and refreshed contributing guidance to reflect active runtime support.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Cleared development-tooling security advisories without permanent overrides** — Upgraded direct dev dependencies (`ts-jest`, `@typescript-eslint/parser`, `@typescript-eslint/eslint-plugin`, and `esbuild`) so the lockfile now resolves patched transitive versions for `minimatch`, `ajv`, `js-yaml`, and `flatted` without retaining temporary npm `overrides`. Full `npm audit` now reports zero vulnerabilities while preserving the existing Jest, ESLint, and build configuration.
|
|
30
|
+
- **Stabilised `ts-jest` coverage resolution after dependency cleanup** — Added an explicit `jest-util` devDependency so `ts-jest` can resolve its runtime helper consistently during Jest coverage runs.
|
|
31
|
+
- **Adjusted BLE device-info fallback for newer lint rules** — Removed an unused catch binding in `readDeviceInformation()` so the code remains compatible with the stricter `@typescript-eslint` rules introduced by the dependency upgrades.
|
|
32
|
+
- **Started staged modernization dependency refresh** — Updated core dev tooling to current non-breaking lines (`typescript` to `^5.9.3`, `prettier` to `3.8.1`, `@types/node` to `^24.12.0`, `@types/jest` to `^30.0.0`, and `@stoprocent/noble` to `^2.3.17`) and revalidated with full CI and `npm audit`.
|
|
33
|
+
- **Consolidated duplicate ESLint configuration files** — Unified lint configuration into `.eslintrc.js` and removed `.eslintrc.json` to reduce rule drift and prepare cleanly for future ESLint major migration work.
|
|
34
|
+
- **Updated Node matrix CI for active support policy** — CI matrix now validates Node 18 and 20 only, matching the new runtime baseline.
|
|
35
|
+
- **Added ESLint flat-config preview path for staged migration** — Added `eslint.config.mjs` and preview scripts (`lint:flat:preview`) so ESLint 9 compatibility can be tested incrementally while the current CI lint path remains unchanged.
|
|
36
|
+
- **Achieved ESLint rule parity between legacy and flat-config paths** — Updated `eslint.config.mjs` to include `js.configs.recommended` (base ESLint rules) and explicitly disable `no-unused-vars` in favour of `@typescript-eslint/no-unused-vars` with argument patterns, ensuring both `npm run lint` and `npm run lint:flat:preview` produce identical output (86 warnings). Both lint paths now feature full parity for future seamless migration to ESLint 9.
|
|
37
|
+
- **Upgraded Jest toolchain toward current major** — Upgraded `jest` and `jest-util` to the 30.x line and aligned Jest type definitions to `@types/jest` 30.x while keeping `ts-jest` on latest available stable 29.x until a compatible 30.x release is published.
|
|
38
|
+
|
|
39
|
+
## [2.3.1] - 2026-03-09
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **Troubleshooting modal in TowerController example** — A "Troubleshooting" button now appears in the TowerController web app button bar. Clicking it opens a modal overlay displaying the full Restoration Games troubleshooting guide (tower jams, disconnects, firmware errors 133/257, and battery specifications). The modal can be dismissed via the close button, clicking the backdrop, or pressing Escape. The button is visually de-emphasised (reduced opacity, smaller text, extra left margin) to indicate it is secondary to Connect/Disconnect/Calibrate.
|
|
44
|
+
|
|
45
|
+
## [2.3.0] - 2026-02-23
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- **`allLightsOn(effect?)` and `allLightsOff()` convenience methods** — Turns all 24 tower LEDs on or off with a single command packet. `allLightsOn` accepts an optional `effect` parameter (default: `LIGHT_EFFECTS.on`); `allLightsOff` is a convenience wrapper around `allLightsOn(LIGHT_EFFECTS.off)`. Both preserve existing drum, beam, and audio state.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- **Public audio volume API now clamps inputs to 0–3** — The `volume` parameter accepted by `playSoundStateful`, `breakSeal`, and related methods is now clamped to the range 0–3 (0=loudest, 1=medium, 2=quiet, 3=softest/mute) before being sent to the tower. The tower's 4-bit device field accepts 0–15, but the firmware only defines behaviour for 0–3; out-of-range inputs now silently clamp rather than producing undefined tower behaviour. If you were passing values outside 0–3, update them to the equivalent in-range value.
|
|
54
|
+
|
|
9
55
|
## [2.2.0] - 2026-02-20
|
|
10
56
|
|
|
11
57
|
### Fixed
|
|
12
58
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
59
|
+
- **`setLEDStateful` stale-state accumulation** — `setLEDStateful` never called `setTowerState`, so `onTowerStateUpdate` callbacks were never fired for LED changes and any code calling it in a loop (including `lights()`) risked reading stale state if `this.currentTowerState` was replaced between iterations. State is now updated explicitly before the command is sent.
|
|
60
|
+
- **`cleanup()` reconnect hazard** — `cleanup()` called `disconnect()` which fired `onTowerDisconnect`, meaning a reconnect-on-disconnect handler could call `connect()` on an instance mid-teardown. `isDisposed` is now set before any disconnect logic runs so the callback cannot re-enter `connect()`.
|
|
61
|
+
- **`cleanup()` not idempotent** — Calling `cleanup()` more than once would re-run the full teardown sequence. It now returns early if the instance is already disposed.
|
|
62
|
+
- **`MockBluetoothAdapter.cleanup()` leaving callbacks registered** — The mock adapter's `cleanup()` now clears all three event callbacks, matching the behaviour of `NodeBluetoothAdapter`.
|
|
17
63
|
|
|
18
64
|
### Changed
|
|
19
65
|
|
|
20
|
-
-
|
|
66
|
+
- **`connect()` throws after disposal** — Calling `connect()` on a `UdtBleConnection` instance after `cleanup()` now throws `Error: UdtBleConnection instance has been disposed and cannot reconnect`. Use `disconnect()` for reversible disconnection.
|
|
21
67
|
|
|
22
68
|
## [2.1.3] - 2026-02-19
|
|
23
69
|
|
|
24
70
|
### Fixed
|
|
25
71
|
|
|
26
|
-
-
|
|
72
|
+
- **`@stoprocent/noble` not loading in ESM build** — In Node.js ESM contexts, `require` is not defined, causing esbuild's `__require` shim to silently fail and leave `noble` as `undefined`. The ESM bundle now injects `import{createRequire}from'module';const require=createRequire(import.meta.url);` as a banner so `@stoprocent/noble` loads correctly via CJS `require` within the ESM module.
|
|
27
73
|
|
|
28
74
|
## [2.1.2] - 2026-02-19
|
|
29
75
|
|
|
30
76
|
### Fixed
|
|
31
77
|
|
|
32
|
-
-
|
|
78
|
+
- **ESM named imports broken** — `import { UltimateDarkTower } from 'ultimatedarktower'` previously threw `SyntaxError: The requested module does not provide an export named 'UltimateDarkTower'` in Node.js ESM projects because the `"import"` export condition pointed to the CommonJS build. The package now ships a true ES Module bundle so named imports work correctly.
|
|
33
79
|
|
|
34
80
|
### Added
|
|
35
81
|
|
|
36
|
-
-
|
|
82
|
+
- **ESM build** (`dist/esm/index.mjs`) — a native ES Module bundle produced by esbuild, included in the published package alongside the existing CommonJS build
|
|
37
83
|
|
|
38
84
|
### Changed
|
|
39
85
|
|
|
40
|
-
-
|
|
41
|
-
-
|
|
86
|
+
- `package.json` `exports["import"]` condition now points to `dist/esm/index.mjs` instead of the CommonJS output; `exports["require"]` is unchanged
|
|
87
|
+
- `package.json` `files` now includes `dist/esm/**/*`
|
|
42
88
|
|
|
43
89
|
## [2.1.1] - 2026-02-19
|
|
44
90
|
|
|
45
91
|
### Added
|
|
46
92
|
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
93
|
+
- Integration test for tower calibration using Node.js Bluetooth adapter, located in `tests/integration/calibration.integration.ts`
|
|
94
|
+
- `npm run test:integration` script to run integration tests requiring real hardware
|
|
95
|
+
- Integration tests are now organized under `tests/integration/` and are not run by default with unit tests or during publish
|
|
50
96
|
|
|
51
97
|
## [2.1.0] - 2026-02-19
|
|
52
98
|
|
|
53
99
|
### Added
|
|
54
100
|
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
101
|
+
- **Public Tower State Types** — Exported `TowerState`, `Light`, `Layer`, `Drum`, `Audio`, and `Beam` type interfaces for direct tower state manipulation
|
|
102
|
+
- **Tower State Utilities** — Exported `rtdt_unpack_state`, `rtdt_pack_state`, `isCalibrated`, and `createDefaultTowerState` for converting between `TowerState` objects and binary tower data
|
|
103
|
+
- **Differential Readings** — Exported `parseDifferentialReadings` function and `ParsedDifferentialReadings` type for parsing tower sensor data
|
|
104
|
+
- **`TowerResponseConfig` Type** — Exported interface for controlling which tower responses are logged via `logTowerResponseConfig`
|
|
59
105
|
|
|
60
106
|
### Changed
|
|
61
107
|
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
108
|
+
- **`TowerResponseConfig`** — Moved from private interface in `UltimateDarkTower.ts` to exported interface in `udtTowerResponse.ts`
|
|
109
|
+
- **`shouldLogResponse`** — Updated parameter type from `any` to `TowerResponseConfig` for type safety
|
|
110
|
+
- **Controller Example** — Updated imports to use the package index instead of internal module paths
|
|
65
111
|
|
|
66
112
|
## [2.0.0] - 2025-02-18
|
|
67
113
|
|
|
68
114
|
### Added
|
|
69
115
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
116
|
+
- **Node.js Support** — `NodeBluetoothAdapter` using `@stoprocent/noble` for BLE communication in Node.js environments (macOS, Linux, Windows)
|
|
117
|
+
- **Platform Auto-Detection** — `BluetoothAdapterFactory` automatically selects the correct adapter based on the runtime environment (browser vs Node.js vs Electron)
|
|
118
|
+
- **`BluetoothPlatform` Enum** — Explicit platform selection via `BluetoothPlatform.WEB`, `BluetoothPlatform.NODE`, or `BluetoothPlatform.AUTO`
|
|
119
|
+
- **`IBluetoothAdapter` Interface** — Public adapter interface for implementing custom Bluetooth adapters (React Native, Cordova, etc.)
|
|
120
|
+
- **Platform-Agnostic Error Types** — `BluetoothConnectionError`, `BluetoothDeviceNotFoundError`, `BluetoothNotAvailableError`, `BluetoothCharacteristicError`, `BluetoothAdapterError` for consistent error handling across platforms
|
|
121
|
+
- **Node.js CLI Example** — Interactive command-line example application (`examples/node/`)
|
|
122
|
+
- **Adapter Layer Tests** — Unit tests for `NodeBluetoothAdapter`, `WebBluetoothAdapter`, `BluetoothAdapterFactory`, `UdtBleConnection`, and error types
|
|
77
123
|
|
|
78
124
|
### Changed
|
|
79
125
|
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
126
|
+
- **`udtBleConnection`** — Refactored to use `IBluetoothAdapter` interface instead of direct Web Bluetooth API calls, enabling multi-platform support
|
|
127
|
+
- **`UltimateDarkTower` Constructor** — Now accepts `UltimateDarkTowerConfig` with optional `platform` or `adapter` properties for platform selection
|
|
128
|
+
- **Peer Dependency** — Updated `@stoprocent/noble` peer dependency from `^1.15.0` to `^2.0.0`
|
|
83
129
|
|
|
84
130
|
## [1.0.0] - 2025-08-18
|
|
85
131
|
|
|
86
132
|
### Added
|
|
87
133
|
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
|
|
134
|
+
- Initial release
|
|
135
|
+
- Web Bluetooth support for Chrome, Edge, and Samsung Internet
|
|
136
|
+
- Tower control API (lights, sounds, drum rotation)
|
|
137
|
+
- Glyph position tracking with automatic updates on drum rotation
|
|
138
|
+
- Seal management for game mechanics
|
|
139
|
+
- Tower state management and validation
|
|
140
|
+
- Multi-layered disconnect detection (heartbeat, GATT events, command timeout)
|
|
141
|
+
- Callback-based event system for tower events
|
|
142
|
+
- Comprehensive logging system with multiple outputs
|
|
143
|
+
- Battery monitoring with low battery warnings
|
|
144
|
+
- TypeScript definitions and type safety
|
|
145
|
+
- Tower Controller example web app
|
|
146
|
+
- Tower Game ("The Tower's Challenge") example web app
|
|
147
|
+
- Complete API reference documentation
|
|
148
|
+
|
|
149
|
+
[2.3.0]: https://github.com/ChessMess/UltimateDarkTower/compare/v2.2.0...v2.3.0
|
|
103
150
|
[2.2.0]: https://github.com/ChessMess/UltimateDarkTower/compare/v2.1.3...v2.2.0
|
|
104
151
|
[2.1.3]: https://github.com/ChessMess/UltimateDarkTower/compare/v2.1.2...v2.1.3
|
|
105
152
|
[2.1.2]: https://github.com/ChessMess/UltimateDarkTower/compare/v2.1.1...v2.1.2
|
package/README.md
CHANGED
|
@@ -158,6 +158,35 @@ npm run test:integration
|
|
|
158
158
|
- The test will fail if the tower is not available or calibration does not complete within 60 seconds.
|
|
159
159
|
- Integration tests are not included in automated test runs or npm publish.
|
|
160
160
|
|
|
161
|
+
### Lights Integration Test
|
|
162
|
+
|
|
163
|
+
The lights integration test validates the `allLightsOn` and `allLightsOff` API methods using real tower hardware.
|
|
164
|
+
|
|
165
|
+
**Test steps:**
|
|
166
|
+
|
|
167
|
+
- Turns all 24 LEDs on (solid effect) for 2 seconds
|
|
168
|
+
- Turns all 24 LEDs on (breathe effect) for 3 seconds
|
|
169
|
+
- Turns all 24 LEDs off
|
|
170
|
+
|
|
171
|
+
**How to run:**
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
npm run test:integration:lights
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Prerequisites:**
|
|
178
|
+
|
|
179
|
+
- Tower must be powered on and in Bluetooth range
|
|
180
|
+
- `@stoprocent/noble` must be installed
|
|
181
|
+
|
|
182
|
+
**Visual verification:**
|
|
183
|
+
|
|
184
|
+
- All lights on (solid) for 2 seconds
|
|
185
|
+
- All lights breathe effect for 3 seconds
|
|
186
|
+
- All lights off
|
|
187
|
+
|
|
188
|
+
See [Reference.md](Reference.md) for API details on `allLightsOn` and `allLightsOff`.
|
|
189
|
+
|
|
161
190
|
**Prerequisites:**
|
|
162
191
|
|
|
163
192
|
- Tower must be powered on and in Bluetooth range
|
package/dist/esm/index.mjs
CHANGED
|
@@ -6,8 +6,7 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
|
6
6
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
7
7
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
8
8
|
}) : x)(function(x) {
|
|
9
|
-
if (typeof require !== "undefined")
|
|
10
|
-
return require.apply(this, arguments);
|
|
9
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
10
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
11
|
});
|
|
13
12
|
var __esm = (fn, res) => function __init() {
|
|
@@ -813,8 +812,7 @@ var init_NodeBluetoothAdapter = __esm({
|
|
|
813
812
|
}
|
|
814
813
|
}
|
|
815
814
|
async disconnect() {
|
|
816
|
-
if (!this.peripheral)
|
|
817
|
-
return;
|
|
815
|
+
if (!this.peripheral) return;
|
|
818
816
|
try {
|
|
819
817
|
if (this.rxCharacteristic) {
|
|
820
818
|
if (this.boundDataHandler) {
|
|
@@ -916,8 +914,7 @@ var init_NodeBluetoothAdapter = __esm({
|
|
|
916
914
|
return cUuid === normalizedUuid || cUuid === shortUuid;
|
|
917
915
|
}
|
|
918
916
|
);
|
|
919
|
-
if (!char)
|
|
920
|
-
continue;
|
|
917
|
+
if (!char) continue;
|
|
921
918
|
try {
|
|
922
919
|
const buffer = await char.readAsync();
|
|
923
920
|
if (binary) {
|
|
@@ -1135,8 +1132,7 @@ var DOMOutput = class {
|
|
|
1135
1132
|
this.maxLines = maxLines;
|
|
1136
1133
|
}
|
|
1137
1134
|
write(level, message, timestamp) {
|
|
1138
|
-
if (!this.container)
|
|
1139
|
-
return;
|
|
1135
|
+
if (!this.container) return;
|
|
1140
1136
|
this.allEntries.push({ level, message, timestamp });
|
|
1141
1137
|
while (this.allEntries.length > this.maxLines) {
|
|
1142
1138
|
this.allEntries.shift();
|
|
@@ -1144,8 +1140,7 @@ var DOMOutput = class {
|
|
|
1144
1140
|
this.refreshDisplay();
|
|
1145
1141
|
}
|
|
1146
1142
|
refreshDisplay() {
|
|
1147
|
-
if (!this.container)
|
|
1148
|
-
return;
|
|
1143
|
+
if (!this.container) return;
|
|
1149
1144
|
this.container.innerHTML = "";
|
|
1150
1145
|
const enabledLevels = this.getEnabledLevelsFromCheckboxes();
|
|
1151
1146
|
const textFilter = this.getTextFilter();
|
|
@@ -1258,12 +1253,9 @@ var Logger = class _Logger {
|
|
|
1258
1253
|
return Array.from(this.enabledLevels);
|
|
1259
1254
|
}
|
|
1260
1255
|
shouldLog(level) {
|
|
1261
|
-
if (this.enabledLevels.has("all"))
|
|
1262
|
-
|
|
1263
|
-
if (level
|
|
1264
|
-
return true;
|
|
1265
|
-
if (this.enabledLevels.has(level))
|
|
1266
|
-
return true;
|
|
1256
|
+
if (this.enabledLevels.has("all")) return true;
|
|
1257
|
+
if (level === "all") return true;
|
|
1258
|
+
if (this.enabledLevels.has(level)) return true;
|
|
1267
1259
|
if (this.enabledLevels.size === 1) {
|
|
1268
1260
|
const singleLevel = Array.from(this.enabledLevels)[0];
|
|
1269
1261
|
if (singleLevel !== "all") {
|
|
@@ -1276,8 +1268,7 @@ var Logger = class _Logger {
|
|
|
1276
1268
|
return false;
|
|
1277
1269
|
}
|
|
1278
1270
|
log(level, message, context) {
|
|
1279
|
-
if (!this.shouldLog(level))
|
|
1280
|
-
return;
|
|
1271
|
+
if (!this.shouldLog(level)) return;
|
|
1281
1272
|
const contextPrefix = context ? `${context} ` : "";
|
|
1282
1273
|
const finalMessage = `${contextPrefix}${message}`;
|
|
1283
1274
|
const timestamp = /* @__PURE__ */ new Date();
|
|
@@ -1835,13 +1826,12 @@ var UdtBleConnection = class {
|
|
|
1835
1826
|
this.logger.info(`Device ${key}: ${value}`, "[UDT][BLE]");
|
|
1836
1827
|
}
|
|
1837
1828
|
}
|
|
1838
|
-
} catch
|
|
1829
|
+
} catch {
|
|
1839
1830
|
this.logger.debug("Device Information Service not available", "[UDT][BLE]");
|
|
1840
1831
|
}
|
|
1841
1832
|
}
|
|
1842
1833
|
async cleanup() {
|
|
1843
|
-
if (this.isDisposed)
|
|
1844
|
-
return;
|
|
1834
|
+
if (this.isDisposed) return;
|
|
1845
1835
|
this.isDisposed = true;
|
|
1846
1836
|
this.logger.info("Cleaning up UdtBleConnection instance", "[UDT][BLE]");
|
|
1847
1837
|
this.stopConnectionMonitoring();
|
|
@@ -1958,7 +1948,7 @@ var UdtCommandFactory = class {
|
|
|
1958
1948
|
* @param currentState - The current complete tower state
|
|
1959
1949
|
* @param sample - Audio sample index to play (0-127)
|
|
1960
1950
|
* @param loop - Whether to loop the audio
|
|
1961
|
-
* @param volume - Audio volume (0-
|
|
1951
|
+
* @param volume - Audio volume (0-3, 0=loudest, 3=softest). Public API clamps inputs to this range before reaching here.
|
|
1962
1952
|
* @returns 20-byte command packet
|
|
1963
1953
|
*/
|
|
1964
1954
|
createStatefulAudioCommand(currentState, sample, loop = false, volume) {
|
|
@@ -1971,10 +1961,10 @@ var UdtCommandFactory = class {
|
|
|
1971
1961
|
/**
|
|
1972
1962
|
* Creates a transient audio command that includes current tower state but doesn't persist audio state.
|
|
1973
1963
|
* This prevents audio from being included in subsequent commands.
|
|
1974
|
-
* @param currentState - The current complete tower state
|
|
1964
|
+
* @param currentState - The current complete tower state
|
|
1975
1965
|
* @param sample - Audio sample index to play
|
|
1976
1966
|
* @param loop - Whether to loop the audio
|
|
1977
|
-
* @param volume - Audio volume (0-
|
|
1967
|
+
* @param volume - Audio volume (0-3, 0=loudest, 3=softest). Public API clamps inputs to this range before reaching here.
|
|
1978
1968
|
* @returns Object containing the command packet and the state without audio for local tracking
|
|
1979
1969
|
*/
|
|
1980
1970
|
createTransientAudioCommand(currentState, sample, loop = false, volume) {
|
|
@@ -1988,12 +1978,12 @@ var UdtCommandFactory = class {
|
|
|
1988
1978
|
return { command, stateWithoutAudio };
|
|
1989
1979
|
}
|
|
1990
1980
|
/**
|
|
1991
|
-
* Creates a transient audio command with additional modifications that includes current tower state
|
|
1981
|
+
* Creates a transient audio command with additional modifications that includes current tower state
|
|
1992
1982
|
* but doesn't persist audio state. This prevents audio from being included in subsequent commands.
|
|
1993
|
-
* @param currentState - The current complete tower state
|
|
1983
|
+
* @param currentState - The current complete tower state
|
|
1994
1984
|
* @param sample - Audio sample index to play
|
|
1995
1985
|
* @param loop - Whether to loop the audio
|
|
1996
|
-
* @param volume - Audio volume (0-
|
|
1986
|
+
* @param volume - Audio volume (0-3, 0=loudest, 3=softest). Public API clamps inputs to this range before reaching here.
|
|
1997
1987
|
* @param otherModifications - Other tower state modifications to include
|
|
1998
1988
|
* @returns Object containing the command packet and the state with modifications but without audio
|
|
1999
1989
|
*/
|
|
@@ -2725,7 +2715,7 @@ var UdtTowerCommands = class {
|
|
|
2725
2715
|
* Audio state is not persisted to prevent sounds from replaying on subsequent commands.
|
|
2726
2716
|
* @param soundIndex - Index of the sound to play (1-based)
|
|
2727
2717
|
* @param loop - Whether to loop the audio
|
|
2728
|
-
* @param volume - Audio volume (0-
|
|
2718
|
+
* @param volume - Audio volume (0-3, 0=loudest, 3=softest), optional. Out-of-range values are clamped.
|
|
2729
2719
|
* @returns Promise that resolves when command is sent
|
|
2730
2720
|
*/
|
|
2731
2721
|
async playSoundStateful(soundIndex, loop = false, volume) {
|
|
@@ -2734,10 +2724,11 @@ var UdtTowerCommands = class {
|
|
|
2734
2724
|
this.deps.logger.error(`attempt to play invalid sound index ${soundIndex}`, "[UDT][CMD]");
|
|
2735
2725
|
return;
|
|
2736
2726
|
}
|
|
2727
|
+
const clampedVolume = volume === void 0 ? void 0 : Math.min(3, Math.max(0, Math.round(volume)));
|
|
2737
2728
|
const currentState = this.deps.getCurrentTowerState();
|
|
2738
|
-
const { command } = this.deps.commandFactory.createTransientAudioCommand(currentState, soundIndex, loop,
|
|
2739
|
-
this.deps.logger.info(`Playing sound ${soundIndex}${loop ? " (looped)" : ""}${
|
|
2740
|
-
await this.sendTowerCommand(command, `playSoundStateful(${soundIndex}, ${loop}${
|
|
2729
|
+
const { command } = this.deps.commandFactory.createTransientAudioCommand(currentState, soundIndex, loop, clampedVolume);
|
|
2730
|
+
this.deps.logger.info(`Playing sound ${soundIndex}${loop ? " (looped)" : ""}${clampedVolume !== void 0 ? ` at volume ${clampedVolume}` : ""}`, "[UDT][CMD]");
|
|
2731
|
+
await this.sendTowerCommand(command, `playSoundStateful(${soundIndex}, ${loop}${clampedVolume !== void 0 ? `, ${clampedVolume}` : ""})`);
|
|
2741
2732
|
}
|
|
2742
2733
|
/**
|
|
2743
2734
|
* Rotates a single drum using stateful commands that preserve existing tower state.
|
|
@@ -2835,10 +2826,15 @@ var UltimateDarkTower = class {
|
|
|
2835
2826
|
this.onCalibrationComplete = () => {
|
|
2836
2827
|
};
|
|
2837
2828
|
this.onSkullDrop = (towerSkullCount) => {
|
|
2829
|
+
void towerSkullCount;
|
|
2838
2830
|
};
|
|
2839
2831
|
this.onBatteryLevelNotify = (millivolts) => {
|
|
2832
|
+
void millivolts;
|
|
2840
2833
|
};
|
|
2841
2834
|
this.onTowerStateUpdate = (newState, oldState, source) => {
|
|
2835
|
+
void newState;
|
|
2836
|
+
void oldState;
|
|
2837
|
+
void source;
|
|
2842
2838
|
};
|
|
2843
2839
|
// utility
|
|
2844
2840
|
this._logDetail = false;
|
|
@@ -2869,6 +2865,12 @@ var UltimateDarkTower = class {
|
|
|
2869
2865
|
this.commandFactory = new UdtCommandFactory();
|
|
2870
2866
|
const commandDependencies = this.createCommandDependencies();
|
|
2871
2867
|
this.towerCommands = new UdtTowerCommands(commandDependencies);
|
|
2868
|
+
if (config?.brokenSeals) {
|
|
2869
|
+
for (const seal of config.brokenSeals) {
|
|
2870
|
+
const sealKey = `${seal.level}-${seal.side}`;
|
|
2871
|
+
this.brokenSeals.add(sealKey);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2872
2874
|
}
|
|
2873
2875
|
/**
|
|
2874
2876
|
* Set up the tower response callback after all components are initialized
|
|
@@ -3107,7 +3109,7 @@ var UltimateDarkTower = class {
|
|
|
3107
3109
|
* Plays a sound using stateful commands that preserve existing tower state.
|
|
3108
3110
|
* @param soundIndex - Index of the sound to play (1-based)
|
|
3109
3111
|
* @param loop - Whether to loop the audio
|
|
3110
|
-
* @param volume - Audio volume (0-
|
|
3112
|
+
* @param volume - Audio volume (0-3, 0=loudest, 3=softest), optional. Out-of-range values are clamped.
|
|
3111
3113
|
* @returns Promise that resolves when command is sent
|
|
3112
3114
|
*/
|
|
3113
3115
|
async playSoundStateful(soundIndex, loop = false, volume) {
|
|
@@ -3142,6 +3144,31 @@ var UltimateDarkTower = class {
|
|
|
3142
3144
|
this.calculateAndUpdateGlyphPositions("bottom", oldBottomPosition, bottom);
|
|
3143
3145
|
return result;
|
|
3144
3146
|
}
|
|
3147
|
+
/**
|
|
3148
|
+
* Turns all tower LEDs on with the specified light effect, sending a single command packet.
|
|
3149
|
+
* Preserves current drum, beam, and audio state while overriding all 6 layers of lights.
|
|
3150
|
+
* @param effect - Light effect to apply (default: LIGHT_EFFECTS.on). Use LIGHT_EFFECTS constants for named values.
|
|
3151
|
+
* @returns Promise that resolves when the command is sent
|
|
3152
|
+
*/
|
|
3153
|
+
async allLightsOn(effect = LIGHT_EFFECTS.on) {
|
|
3154
|
+
const currentState = this.getCurrentTowerState();
|
|
3155
|
+
const loop = effect !== LIGHT_EFFECTS.off;
|
|
3156
|
+
const newState = {
|
|
3157
|
+
...currentState,
|
|
3158
|
+
layer: currentState.layer.map((layer) => ({
|
|
3159
|
+
light: layer.light.map(() => ({ effect, loop }))
|
|
3160
|
+
}))
|
|
3161
|
+
};
|
|
3162
|
+
return this.sendTowerState(newState);
|
|
3163
|
+
}
|
|
3164
|
+
/**
|
|
3165
|
+
* Turns all tower LEDs off, sending a single command packet.
|
|
3166
|
+
* Convenience wrapper around allLightsOn(LIGHT_EFFECTS.off).
|
|
3167
|
+
* @returns Promise that resolves when the command is sent
|
|
3168
|
+
*/
|
|
3169
|
+
async allLightsOff() {
|
|
3170
|
+
return this.allLightsOn(LIGHT_EFFECTS.off);
|
|
3171
|
+
}
|
|
3145
3172
|
//#endregion
|
|
3146
3173
|
//#region Tower State Management
|
|
3147
3174
|
/**
|
|
@@ -3354,6 +3381,25 @@ var UltimateDarkTower = class {
|
|
|
3354
3381
|
return { level, side };
|
|
3355
3382
|
});
|
|
3356
3383
|
}
|
|
3384
|
+
/**
|
|
3385
|
+
* Marks a seal as broken in software tracking without sending any commands to the tower.
|
|
3386
|
+
* Use this to restore game state (e.g., resuming a game where seals were already broken).
|
|
3387
|
+
* Unlike breakSeal(), this does NOT trigger sound or light effects on the tower.
|
|
3388
|
+
* @param seal - Seal identifier to mark as broken
|
|
3389
|
+
*/
|
|
3390
|
+
markSealBroken(seal) {
|
|
3391
|
+
const sealKey = `${seal.level}-${seal.side}`;
|
|
3392
|
+
this.brokenSeals.add(sealKey);
|
|
3393
|
+
}
|
|
3394
|
+
/**
|
|
3395
|
+
* Marks a seal as unbroken in software tracking without sending any commands to the tower.
|
|
3396
|
+
* Use this to undo a seal break or restore individual seals for game state management.
|
|
3397
|
+
* @param seal - Seal identifier to mark as unbroken
|
|
3398
|
+
*/
|
|
3399
|
+
markSealRestored(seal) {
|
|
3400
|
+
const sealKey = `${seal.level}-${seal.side}`;
|
|
3401
|
+
this.brokenSeals.delete(sealKey);
|
|
3402
|
+
}
|
|
3357
3403
|
/**
|
|
3358
3404
|
* Resets the broken seals tracking (clears all broken seals).
|
|
3359
3405
|
*/
|
|
@@ -3502,7 +3548,7 @@ var UltimateDarkTower_default = UltimateDarkTower;
|
|
|
3502
3548
|
init_udtConstants();
|
|
3503
3549
|
init_udtBluetoothAdapter();
|
|
3504
3550
|
init_udtTowerState();
|
|
3505
|
-
var
|
|
3551
|
+
var index_default = UltimateDarkTower_default;
|
|
3506
3552
|
export {
|
|
3507
3553
|
AUDIO_COMMAND_POS,
|
|
3508
3554
|
BATTERY_STATUS_FREQUENCY,
|
|
@@ -3563,7 +3609,7 @@ export {
|
|
|
3563
3609
|
VOLUME_DESCRIPTIONS,
|
|
3564
3610
|
VOLUME_ICONS,
|
|
3565
3611
|
createDefaultTowerState,
|
|
3566
|
-
|
|
3612
|
+
index_default as default,
|
|
3567
3613
|
drumPositionCmds,
|
|
3568
3614
|
isCalibrated,
|
|
3569
3615
|
logger,
|
|
@@ -14,6 +14,8 @@ export interface UltimateDarkTowerConfig {
|
|
|
14
14
|
platform?: BluetoothPlatform;
|
|
15
15
|
/** Custom Bluetooth adapter (for testing or custom platforms like React Native) */
|
|
16
16
|
adapter?: IBluetoothAdapter;
|
|
17
|
+
/** Initial broken seals to restore game state (software-only, no hardware effects) */
|
|
18
|
+
brokenSeals?: SealIdentifier[];
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* @title UltimateDarkTower
|
|
@@ -174,7 +176,7 @@ declare class UltimateDarkTower {
|
|
|
174
176
|
* Plays a sound using stateful commands that preserve existing tower state.
|
|
175
177
|
* @param soundIndex - Index of the sound to play (1-based)
|
|
176
178
|
* @param loop - Whether to loop the audio
|
|
177
|
-
* @param volume - Audio volume (0-
|
|
179
|
+
* @param volume - Audio volume (0-3, 0=loudest, 3=softest), optional. Out-of-range values are clamped.
|
|
178
180
|
* @returns Promise that resolves when command is sent
|
|
179
181
|
*/
|
|
180
182
|
playSoundStateful(soundIndex: number, loop?: boolean, volume?: number): Promise<void>;
|
|
@@ -196,6 +198,19 @@ declare class UltimateDarkTower {
|
|
|
196
198
|
* @returns Promise that resolves when rotate command is sent
|
|
197
199
|
*/
|
|
198
200
|
rotateWithState(top: TowerSide, middle: TowerSide, bottom: TowerSide, soundIndex?: number): Promise<void>;
|
|
201
|
+
/**
|
|
202
|
+
* Turns all tower LEDs on with the specified light effect, sending a single command packet.
|
|
203
|
+
* Preserves current drum, beam, and audio state while overriding all 6 layers of lights.
|
|
204
|
+
* @param effect - Light effect to apply (default: LIGHT_EFFECTS.on). Use LIGHT_EFFECTS constants for named values.
|
|
205
|
+
* @returns Promise that resolves when the command is sent
|
|
206
|
+
*/
|
|
207
|
+
allLightsOn(effect?: number): Promise<void>;
|
|
208
|
+
/**
|
|
209
|
+
* Turns all tower LEDs off, sending a single command packet.
|
|
210
|
+
* Convenience wrapper around allLightsOn(LIGHT_EFFECTS.off).
|
|
211
|
+
* @returns Promise that resolves when the command is sent
|
|
212
|
+
*/
|
|
213
|
+
allLightsOff(): Promise<void>;
|
|
199
214
|
/**
|
|
200
215
|
* Gets the current complete tower state if available.
|
|
201
216
|
* @returns The current tower state object
|
|
@@ -295,6 +310,19 @@ declare class UltimateDarkTower {
|
|
|
295
310
|
* @returns Array of SealIdentifier objects representing all broken seals
|
|
296
311
|
*/
|
|
297
312
|
getBrokenSeals(): SealIdentifier[];
|
|
313
|
+
/**
|
|
314
|
+
* Marks a seal as broken in software tracking without sending any commands to the tower.
|
|
315
|
+
* Use this to restore game state (e.g., resuming a game where seals were already broken).
|
|
316
|
+
* Unlike breakSeal(), this does NOT trigger sound or light effects on the tower.
|
|
317
|
+
* @param seal - Seal identifier to mark as broken
|
|
318
|
+
*/
|
|
319
|
+
markSealBroken(seal: SealIdentifier): void;
|
|
320
|
+
/**
|
|
321
|
+
* Marks a seal as unbroken in software tracking without sending any commands to the tower.
|
|
322
|
+
* Use this to undo a seal break or restore individual seals for game state management.
|
|
323
|
+
* @param seal - Seal identifier to mark as unbroken
|
|
324
|
+
*/
|
|
325
|
+
markSealRestored(seal: SealIdentifier): void;
|
|
298
326
|
/**
|
|
299
327
|
* Resets the broken seals tracking (clears all broken seals).
|
|
300
328
|
*/
|