ultimatedarktower 2.1.0 → 2.1.2

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.
@@ -0,0 +1,3558 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
6
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
7
+ }) : x)(function(x) {
8
+ if (typeof require !== "undefined")
9
+ return require.apply(this, arguments);
10
+ throw Error('Dynamic require of "' + x + '" is not supported');
11
+ });
12
+ var __esm = (fn, res) => function __init() {
13
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
14
+ };
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/udtConstants.ts
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
+ var init_udtConstants = __esm({
32
+ "src/udtConstants.ts"() {
33
+ UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
34
+ UART_TX_CHARACTERISTIC_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
35
+ UART_RX_CHARACTERISTIC_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
36
+ TOWER_DEVICE_NAME = "ReturnToDarkTower";
37
+ DIS_SERVICE_UUID = "0000180a-0000-1000-8000-00805f9b34fb";
38
+ DIS_MANUFACTURER_NAME_UUID = "00002a29-0000-1000-8000-00805f9b34fb";
39
+ DIS_MODEL_NUMBER_UUID = "00002a24-0000-1000-8000-00805f9b34fb";
40
+ DIS_SERIAL_NUMBER_UUID = "00002a25-0000-1000-8000-00805f9b34fb";
41
+ DIS_HARDWARE_REVISION_UUID = "00002a27-0000-1000-8000-00805f9b34fb";
42
+ DIS_FIRMWARE_REVISION_UUID = "00002a26-0000-1000-8000-00805f9b34fb";
43
+ DIS_SOFTWARE_REVISION_UUID = "00002a28-0000-1000-8000-00805f9b34fb";
44
+ DIS_SYSTEM_ID_UUID = "00002a23-0000-1000-8000-00805f9b34fb";
45
+ DIS_IEEE_REGULATORY_UUID = "00002a2a-0000-1000-8000-00805f9b34fb";
46
+ DIS_PNP_ID_UUID = "00002a50-0000-1000-8000-00805f9b34fb";
47
+ TOWER_COMMAND_PACKET_SIZE = 20;
48
+ TOWER_STATE_DATA_SIZE = 19;
49
+ TOWER_COMMAND_HEADER_SIZE = 1;
50
+ TOWER_STATE_RESPONSE_MIN_LENGTH = 20;
51
+ TOWER_STATE_DATA_OFFSET = 1;
52
+ TOWER_COMMAND_TYPE_TOWER_STATE = 0;
53
+ DEFAULT_CONNECTION_MONITORING_FREQUENCY = 2e3;
54
+ DEFAULT_CONNECTION_MONITORING_TIMEOUT = 3e4;
55
+ DEFAULT_BATTERY_HEARTBEAT_TIMEOUT = 3e3;
56
+ BATTERY_STATUS_FREQUENCY = 200;
57
+ DEFAULT_RETRY_SEND_COMMAND_MAX = 5;
58
+ TOWER_SIDES_COUNT = 4;
59
+ TOWER_COMMANDS = {
60
+ towerState: 0,
61
+ // not a sendable command
62
+ doorReset: 1,
63
+ unjamDrums: 2,
64
+ resetCounter: 3,
65
+ calibration: 4,
66
+ overwriteDrumStates: 5
67
+ // go no further!
68
+ };
69
+ TC = {
70
+ STATE: "TOWER_STATE",
71
+ INVALID_STATE: "INVALID_STATE",
72
+ FAILURE: "HARDWARE_FAILURE",
73
+ JIGGLE: "MECH_JIGGLE_TRIGGERED",
74
+ UNEXPECTED: "MECH_UNEXPECTED_TRIGGER",
75
+ DURATION: "MECH_DURATION",
76
+ DIFFERENTIAL: "DIFFERENTIAL_READINGS",
77
+ CALIBRATION: "CALIBRATION_FINISHED",
78
+ BATTERY: "BATTERY_READING"
79
+ };
80
+ DRUM_PACKETS = {
81
+ topMiddle: 1,
82
+ bottom: 2
83
+ };
84
+ GLYPHS = {
85
+ cleanse: { name: "Cleanse", level: "top", side: "north" },
86
+ quest: { name: "Quest", level: "top", side: "south" },
87
+ battle: { name: "Battle", level: "middle", side: "north" },
88
+ banner: { name: "Banner", level: "bottom", side: "north" },
89
+ reinforce: { name: "Reinforce", level: "bottom", side: "south" }
90
+ };
91
+ AUDIO_COMMAND_POS = 15;
92
+ SKULL_DROP_COUNT_POS = 17;
93
+ drumPositionCmds = {
94
+ top: { north: 16, east: 2, south: 20, west: 22 },
95
+ // bits 1-8
96
+ middle: { north: 16, east: 64, south: 144, west: 208 },
97
+ // bits 1-4
98
+ bottom: { north: 66, east: 74, south: 82, west: 90 }
99
+ };
100
+ LIGHT_EFFECTS = {
101
+ off: 0,
102
+ on: 1,
103
+ breathe: 2,
104
+ breatheFast: 3,
105
+ breathe50percent: 4,
106
+ flicker: 5
107
+ };
108
+ TOWER_LIGHT_SEQUENCES = {
109
+ twinkle: 1,
110
+ flareThenFade: 2,
111
+ flareThenFadeBase: 3,
112
+ flareThenFlicker: 4,
113
+ angryStrobe01: 5,
114
+ angryStrobe02: 6,
115
+ angryStrobe03: 7,
116
+ gloat01: 8,
117
+ gloat02: 9,
118
+ gloat03: 10,
119
+ defeat: 11,
120
+ victory: 12,
121
+ dungeonIdle: 13,
122
+ sealReveal: 14,
123
+ rotationAllDrums: 15,
124
+ rotationDrumTop: 16,
125
+ rotationDrumMiddle: 17,
126
+ rotationDrumBottom: 18,
127
+ monthStarted: 19
128
+ };
129
+ TOWER_MESSAGES = {
130
+ TOWER_STATE: { name: "Tower State", value: 0, critical: false },
131
+ INVALID_STATE: { name: "Invalid State", value: 1, critical: true },
132
+ HARDWARE_FAILURE: { name: "Hardware Failure", value: 2, critical: true },
133
+ MECH_JIGGLE_TRIGGERED: { name: "Unjam Jiggle Triggered", value: 3, critical: true },
134
+ MECH_DURATION: { name: "Rotation Duration", value: 4, critical: false },
135
+ MECH_UNEXPECTED_TRIGGER: { name: "Unexpected Trigger", value: 5, critical: true },
136
+ DIFFERENTIAL_READINGS: { name: "Diff Voltage Readings", value: 6, critical: false },
137
+ BATTERY_READING: { name: "Battery Level", value: 7, critical: false },
138
+ CALIBRATION_FINISHED: { name: "Calibration Finished", value: 8, critical: false }
139
+ };
140
+ VOLTAGE_LEVELS = [
141
+ 1500,
142
+ 1390,
143
+ 1350,
144
+ 1320,
145
+ 1295,
146
+ 1270,
147
+ 1245,
148
+ 1225,
149
+ 1205,
150
+ 1180,
151
+ 1175,
152
+ 1166,
153
+ 1150,
154
+ 1133,
155
+ 1125,
156
+ 1107,
157
+ 1095,
158
+ 1066,
159
+ 1033,
160
+ 980
161
+ // There's an additional 5% until 800mV is reached
162
+ ];
163
+ TOWER_LAYERS = {
164
+ TOP_RING: 0,
165
+ MIDDLE_RING: 1,
166
+ BOTTOM_RING: 2,
167
+ LEDGE: 3,
168
+ BASE1: 4,
169
+ BASE2: 5
170
+ };
171
+ RING_LIGHT_POSITIONS = {
172
+ NORTH: 0,
173
+ EAST: 1,
174
+ SOUTH: 2,
175
+ WEST: 3
176
+ };
177
+ LEDGE_BASE_LIGHT_POSITIONS = {
178
+ NORTH_EAST: 0,
179
+ SOUTH_EAST: 1,
180
+ SOUTH_WEST: 2,
181
+ NORTH_WEST: 3
182
+ };
183
+ LED_CHANNEL_LOOKUP = [
184
+ // Layer 0: Top Ring (C0 R0, C0 R3, C0 R2, C0 R1)
185
+ 0,
186
+ 3,
187
+ 2,
188
+ 1,
189
+ // Layer 1: Middle Ring (C1 R3, C1 R2, C1 R1, C1 R0)
190
+ 7,
191
+ 6,
192
+ 5,
193
+ 4,
194
+ // Layer 2: Bottom Ring (C2 R2, C2 R1, C2 R0, C2 R3)
195
+ 10,
196
+ 9,
197
+ 8,
198
+ 11,
199
+ // Layer 3: Ledge (LEDGE R4, LEDGE R5, LEDGE R6, LEDGE R7)
200
+ 12,
201
+ 13,
202
+ 14,
203
+ 15,
204
+ // Layer 4: Base1 (BASE1 R4, BASE1 R5, BASE1 R6, BASE1 R7)
205
+ 16,
206
+ 17,
207
+ 18,
208
+ 19,
209
+ // Layer 5: Base2 (BASE2 R4, BASE2 R5, BASE2 R6, BASE2 R7)
210
+ 20,
211
+ 21,
212
+ 22,
213
+ 23
214
+ ];
215
+ LAYER_TO_POSITION = {
216
+ [TOWER_LAYERS.TOP_RING]: "TOP_RING",
217
+ [TOWER_LAYERS.MIDDLE_RING]: "MIDDLE_RING",
218
+ [TOWER_LAYERS.BOTTOM_RING]: "BOTTOM_RING",
219
+ [TOWER_LAYERS.LEDGE]: "LEDGE",
220
+ [TOWER_LAYERS.BASE1]: "BASE1",
221
+ [TOWER_LAYERS.BASE2]: "BASE2"
222
+ };
223
+ LIGHT_INDEX_TO_DIRECTION = {
224
+ [RING_LIGHT_POSITIONS.NORTH]: "NORTH",
225
+ [RING_LIGHT_POSITIONS.EAST]: "EAST",
226
+ [RING_LIGHT_POSITIONS.SOUTH]: "SOUTH",
227
+ [RING_LIGHT_POSITIONS.WEST]: "WEST"
228
+ };
229
+ STATE_DATA_LENGTH = 19;
230
+ TOWER_AUDIO_LIBRARY = {
231
+ Ashstrider: { name: "Ashstrider", value: 1, category: "Adversary" },
232
+ BaneofOmens: { name: "Bane of Omens", value: 2, category: "Adversary" },
233
+ EmpressofShades: { name: "Empress of Shades", value: 3, category: "Adversary" },
234
+ GazeEternal: { name: "Gaze Eternal", value: 4, category: "Adversary" },
235
+ Gravemaw: { name: "Gravemaw", value: 5, category: "Adversary" },
236
+ IsatheHollow: { name: "Isa the Hollow", value: 6, category: "Adversary" },
237
+ LingeringRot: { name: "Lingering Rot", value: 7, category: "Adversary" },
238
+ UtukKu: { name: "Utuk'Ku", value: 8, category: "Adversary" },
239
+ Gleb: { name: "Gleb", value: 9, category: "Ally" },
240
+ Grigor: { name: "Grigor", value: 10, category: "Ally" },
241
+ Hakan: { name: "Hakan", value: 11, category: "Ally" },
242
+ Letha: { name: "Letha", value: 12, category: "Ally" },
243
+ Miras: { name: "Miras", value: 13, category: "Ally" },
244
+ Nimet: { name: "Nimet", value: 14, category: "Ally" },
245
+ Tomas: { name: "Tomas", value: 15, category: "Ally" },
246
+ Vasa: { name: "Vasa", value: 16, category: "Ally" },
247
+ Yana: { name: "Yana", value: 17, category: "Ally" },
248
+ Zaida: { name: "Zaida", value: 18, category: "Ally" },
249
+ ApplyAdvantage01: { name: "Apply Advantage 01", value: 19, category: "Battle" },
250
+ ApplyAdvantage02: { name: "Apply Advantage 02", value: 20, category: "Battle" },
251
+ ApplyAdvantage03: { name: "Apply Advantage 03", value: 21, category: "Battle" },
252
+ ApplyAdvantage04: { name: "Apply Advantage 04", value: 22, category: "Battle" },
253
+ ApplyAdvantage05: { name: "Apply Advantage 05", value: 23, category: "Battle" },
254
+ MaxAdvantages: { name: "Max Advantages", value: 24, category: "Battle" },
255
+ NoAdvantages: { name: "No Advantages", value: 25, category: "Battle" },
256
+ AdversaryEscaped: { name: "Adversary Escaped", value: 26, category: "Battle" },
257
+ BattleButton: { name: "Battle Button", value: 27, category: "Battle" },
258
+ CardFlip01: { name: "Card Flip 01", value: 28, category: "Battle" },
259
+ CardFlip02: { name: "Card Flip 02", value: 29, category: "Battle" },
260
+ CardFlip03: { name: "Card Flip 03", value: 30, category: "Battle" },
261
+ CardFlipPaper01: { name: "Card Flip Paper 01", value: 31, category: "Battle" },
262
+ CardFlipPaper02: { name: "Card Flip Paper 02", value: 32, category: "Battle" },
263
+ CardFlipPaper03: { name: "Card Flip Paper 03", value: 33, category: "Battle" },
264
+ CardSelect01: { name: "Card Select 01", value: 34, category: "Battle" },
265
+ CardSelect02: { name: "Card Select 02", value: 35, category: "Battle" },
266
+ CardSelect03: { name: "Card Select 03", value: 36, category: "Battle" },
267
+ BattleStart: { name: "Battle Start", value: 37, category: "Battle" },
268
+ BattleVictory: { name: "Battle Victory", value: 38, category: "Battle" },
269
+ ButtonHoldPressCombo: { name: "Button Hold Press Combo", value: 39, category: "Battle" },
270
+ ButtonHold: { name: "Button Hold", value: 40, category: "Battle" },
271
+ ButtonPress: { name: "Button Press", value: 41, category: "Battle" },
272
+ ClassicAdvantageApplied: { name: "8-bit Advantage", value: 42, category: "Classic" },
273
+ ClassicAttackTower: { name: "8-bit Attack Tower", value: 43, category: "Classic" },
274
+ ClassicBazaar: { name: "8-bit Bazaar", value: 44, category: "Classic" },
275
+ ClassicConfirmation: { name: "8-bit Confirmation", value: 45, category: "Classic" },
276
+ ClassicDragons: { name: "8-bit Dragons", value: 46, category: "Classic" },
277
+ ClassicQuestFailed: { name: "8-bit Quest Failed", value: 47, category: "Classic" },
278
+ ClassicRetreat: { name: "8-bit Retreat", value: 48, category: "Classic" },
279
+ ClassicStartMonth: { name: "8-bit Start Month", value: 49, category: "Classic" },
280
+ ClassicStartDungeon: { name: "8-bit Start Dungeon", value: 50, category: "Classic" },
281
+ ClassicTowerLost: { name: "8-bit Tower Lost", value: 51, category: "Classic" },
282
+ ClassicUnsure: { name: "8-bit Unsure", value: 52, category: "Classic" },
283
+ DungeonAdvantage01: { name: "Dungeon Advantage 01", value: 53, category: "Dungeon" },
284
+ DungeonAdvantage02: { name: "Dungeon Advantage 02", value: 54, category: "Dungeon" },
285
+ DungeonButton: { name: "Dungeon Button", value: 55, category: "Dungeon" },
286
+ DungeonFootsteps: { name: "Dungeon Footsteps", value: 56, category: "Dungeon" },
287
+ DungeonCaves: { name: "Dungeon Caves", value: 57, category: "Dungeon" },
288
+ DungeonComplete: { name: "Dungeon Complete", value: 58, category: "Dungeon" },
289
+ DungeonEncampment: { name: "Dungeon Encampment", value: 59, category: "Dungeon" },
290
+ DungeonEscape: { name: "Dungeon Escape", value: 60, category: "Dungeon" },
291
+ DungeonFortress: { name: "Dungeon Fortress", value: 61, category: "Dungeon" },
292
+ DungeonRuins: { name: "Dungeon Ruins", value: 62, category: "Dungeon" },
293
+ DungeonShrine: { name: "Dungeon Shrine", value: 63, category: "Dungeon" },
294
+ DungeonTomb: { name: "Dungeon Tomb", value: 64, category: "Dungeon" },
295
+ FoeEvent: { name: "Foe Event", value: 65, category: "Foe" },
296
+ FoeSpawn: { name: "Foe Spawn", value: 66, category: "Foe" },
297
+ Brigands: { name: "Brigands", value: 67, category: "Foe" },
298
+ ClanofNeuri: { name: "Clan of Neuri", value: 68, category: "Foe" },
299
+ Dragons: { name: "Dragons", value: 69, category: "Foe" },
300
+ Lemures: { name: "Lemures", value: 70, category: "Foe" },
301
+ LeveledUp: { name: "Leveled Up", value: 71, category: "Foe" },
302
+ Mormos: { name: "Mormos", value: 72, category: "Foe" },
303
+ Oreks: { name: "Oreks", value: 73, category: "Foe" },
304
+ ShadowWolves: { name: "Shadow Wolves", value: 74, category: "Foe" },
305
+ SpineFiends: { name: "Spine Fiends", value: 75, category: "Foe" },
306
+ Strigas: { name: "Strigas", value: 76, category: "Foe" },
307
+ Titans: { name: "Titans", value: 77, category: "Foe" },
308
+ FrostTrolls: { name: "Frost Trolls", value: 78, category: "Foe" },
309
+ WidowmadeSpiders: { name: "Widowmade Spiders", value: 79, category: "Foe" },
310
+ AshstriderSpawn: { name: "Ashstrider Spawn", value: 80, category: "Spawn" },
311
+ BaneofOmensSpawn: { name: "Bane of Omens Spawn", value: 81, category: "Spawn" },
312
+ EmpressofShadesSpawn: { name: "Empress of Shades Spawn", value: 82, category: "Spawn" },
313
+ GazeEternalSpawn: { name: "Gaze Eternal Spawn", value: 83, category: "Spawn" },
314
+ GravemawSpawn: { name: "Gravemaw Spawn", value: 84, category: "Spawn" },
315
+ IsatheHollowSpawn: { name: "Isa the Hollow Spawn", value: 85, category: "Spawn" },
316
+ LingeringRotSpawn: { name: "Lingering Rot Spawn", value: 86, category: "Spawn" },
317
+ UtukKuSpawn: { name: "Utuk'Ku Spawn", value: 87, category: "Spawn" },
318
+ QuestComplete: { name: "Quest Complete", value: 88, category: "Quest" },
319
+ TowerAllGlyphs: { name: "Tower All Glyphs", value: 89, category: "Glyph" },
320
+ TowerAngry1: { name: "Tower Angry 1", value: 90, category: "Glyph" },
321
+ TowerAngry2: { name: "Tower Angry 2", value: 91, category: "Glyph" },
322
+ TowerAngry3: { name: "Tower Angry 3", value: 92, category: "Glyph" },
323
+ TowerAngry4: { name: "Tower Angry 4", value: 93, category: "Glyph" },
324
+ TowerConnected: { name: "Tower Connected", value: 94, category: "State" },
325
+ GameStart: { name: "Game Start", value: 95, category: "State" },
326
+ TowerGloat1: { name: "Tower Gloat 1", value: 96, category: "State" },
327
+ TowerGloat2: { name: "Tower Gloat 2", value: 97, category: "State" },
328
+ TowerGloat3: { name: "Tower Gloat 3", value: 98, category: "State" },
329
+ TowerGlyph: { name: "Tower Glyph", value: 99, category: "State" },
330
+ TowerIdle1: { name: "Tower Idle 1", value: 100, category: "State" },
331
+ TowerIdle2: { name: "Tower Idle 2", value: 101, category: "State" },
332
+ TowerIdle3: { name: "Tower Idle 3", value: 102, category: "State" },
333
+ TowerIdle4: { name: "Tower Idle 4", value: 103, category: "State" },
334
+ TowerIdle5: { name: "Tower Idle 5", value: 104, category: "Unlisted" },
335
+ TowerDisconnected: { name: "Tower Disconnect", value: 105, category: "State" },
336
+ MonthEnded: { name: "Month Ended", value: 106, category: "State" },
337
+ MonthStarted: { name: "Month Started", value: 107, category: "State" },
338
+ QuestFailed: { name: "Quest Failed", value: 108, category: "Quest" },
339
+ RotateExit: { name: "Rotate Exit", value: 109, category: "Seals" },
340
+ RotateLoop: { name: "Rotate Loop", value: 110, category: "Seals" },
341
+ RotateStart: { name: "Rotate Start", value: 111, category: "Seals" },
342
+ TowerSeal: { name: "Tower Seal", value: 112, category: "Seals" },
343
+ TowerSkullDropped: { name: "Tower Skull Dropped", value: 113, category: "State" }
344
+ };
345
+ VOLUME_DESCRIPTIONS = {
346
+ 0: "Loud",
347
+ 1: "Medium",
348
+ 2: "Quiet",
349
+ 3: "Mute"
350
+ };
351
+ VOLUME_ICONS = {
352
+ 0: "\u{1F50A}",
353
+ // Loud - biggest speaker
354
+ 1: "\u{1F509}",
355
+ // Medium - medium speaker
356
+ 2: "\u{1F508}",
357
+ // Quiet - small speaker
358
+ 3: "\u{1F507}"
359
+ // Mute - muted speaker
360
+ };
361
+ }
362
+ });
363
+
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
+ // src/udtBluetoothAdapter.ts
515
+ var BluetoothError, BluetoothConnectionError, BluetoothDeviceNotFoundError, BluetoothUserCancelledError, BluetoothTimeoutError;
516
+ var init_udtBluetoothAdapter = __esm({
517
+ "src/udtBluetoothAdapter.ts"() {
518
+ BluetoothError = class extends Error {
519
+ constructor(message, originalError) {
520
+ super(message);
521
+ this.originalError = originalError;
522
+ this.name = "BluetoothError";
523
+ }
524
+ };
525
+ BluetoothConnectionError = class extends BluetoothError {
526
+ constructor(message, originalError) {
527
+ super(message, originalError);
528
+ this.name = "BluetoothConnectionError";
529
+ }
530
+ };
531
+ BluetoothDeviceNotFoundError = class extends BluetoothError {
532
+ constructor(message, originalError) {
533
+ super(message, originalError);
534
+ this.name = "BluetoothDeviceNotFoundError";
535
+ }
536
+ };
537
+ BluetoothUserCancelledError = class extends BluetoothError {
538
+ constructor(message, originalError) {
539
+ super(message, originalError);
540
+ this.name = "BluetoothUserCancelledError";
541
+ }
542
+ };
543
+ BluetoothTimeoutError = class extends BluetoothError {
544
+ constructor(message, originalError) {
545
+ super(message, originalError);
546
+ this.name = "BluetoothTimeoutError";
547
+ }
548
+ };
549
+ }
550
+ });
551
+
552
+ // src/adapters/WebBluetoothAdapter.ts
553
+ var WebBluetoothAdapter_exports = {};
554
+ __export(WebBluetoothAdapter_exports, {
555
+ WebBluetoothAdapter: () => WebBluetoothAdapter
556
+ });
557
+ var WebBluetoothAdapter;
558
+ var init_WebBluetoothAdapter = __esm({
559
+ "src/adapters/WebBluetoothAdapter.ts"() {
560
+ init_udtConstants();
561
+ init_udtBluetoothAdapter();
562
+ WebBluetoothAdapter = class {
563
+ constructor() {
564
+ this.device = null;
565
+ this.txCharacteristic = null;
566
+ this.rxCharacteristic = null;
567
+ // Bound event handlers for cleanup
568
+ this.boundOnCharacteristicValueChanged = null;
569
+ this.boundOnDeviceDisconnected = null;
570
+ this.boundOnAvailabilityChanged = null;
571
+ }
572
+ async connect(deviceName, serviceUuids) {
573
+ try {
574
+ this.device = await navigator.bluetooth.requestDevice({
575
+ filters: [{ namePrefix: deviceName }],
576
+ optionalServices: serviceUuids
577
+ });
578
+ if (this.device === null) {
579
+ throw new BluetoothDeviceNotFoundError("Tower not found");
580
+ }
581
+ this.boundOnDeviceDisconnected = () => {
582
+ if (this.disconnectCallback) {
583
+ this.disconnectCallback();
584
+ }
585
+ };
586
+ this.device.addEventListener("gattserverdisconnected", this.boundOnDeviceDisconnected);
587
+ this.boundOnAvailabilityChanged = (event) => {
588
+ if (this.availabilityCallback) {
589
+ this.availabilityCallback(event.value);
590
+ }
591
+ };
592
+ if (navigator.bluetooth) {
593
+ navigator.bluetooth.addEventListener("availabilitychanged", this.boundOnAvailabilityChanged);
594
+ }
595
+ const server = await this.device.gatt.connect();
596
+ const service = await server.getPrimaryService(UART_SERVICE_UUID);
597
+ this.txCharacteristic = await service.getCharacteristic(UART_TX_CHARACTERISTIC_UUID);
598
+ this.rxCharacteristic = await service.getCharacteristic(UART_RX_CHARACTERISTIC_UUID);
599
+ await this.rxCharacteristic.startNotifications();
600
+ this.boundOnCharacteristicValueChanged = (event) => {
601
+ const target = event.target;
602
+ const receivedData = new Uint8Array(target.value.byteLength);
603
+ for (let i = 0; i < target.value.byteLength; i++) {
604
+ receivedData[i] = target.value.getUint8(i);
605
+ }
606
+ if (this.characteristicCallback) {
607
+ this.characteristicCallback(receivedData);
608
+ }
609
+ };
610
+ await this.rxCharacteristic.addEventListener(
611
+ "characteristicvaluechanged",
612
+ this.boundOnCharacteristicValueChanged
613
+ );
614
+ } catch (error) {
615
+ if (error instanceof BluetoothDeviceNotFoundError || error instanceof BluetoothUserCancelledError || error instanceof BluetoothConnectionError) {
616
+ throw error;
617
+ }
618
+ const errorMsg = error?.message ?? String(error);
619
+ if (errorMsg.includes("User cancelled")) {
620
+ throw new BluetoothUserCancelledError("User cancelled device selection", error);
621
+ }
622
+ if (errorMsg.includes("not found") || error?.name === "NotFoundError") {
623
+ throw new BluetoothDeviceNotFoundError("Device not found", error);
624
+ }
625
+ throw new BluetoothConnectionError(`Failed to connect: ${errorMsg}`, error);
626
+ }
627
+ }
628
+ async disconnect() {
629
+ if (!this.device) {
630
+ return;
631
+ }
632
+ if (this.device.gatt.connected) {
633
+ if (this.boundOnDeviceDisconnected) {
634
+ this.device.removeEventListener("gattserverdisconnected", this.boundOnDeviceDisconnected);
635
+ }
636
+ await this.device.gatt.disconnect();
637
+ }
638
+ this.device = null;
639
+ this.txCharacteristic = null;
640
+ this.rxCharacteristic = null;
641
+ }
642
+ isConnected() {
643
+ return !!this.device;
644
+ }
645
+ isGattConnected() {
646
+ return this.device?.gatt?.connected ?? false;
647
+ }
648
+ async writeCharacteristic(data) {
649
+ if (!this.txCharacteristic) {
650
+ throw new BluetoothConnectionError("TX characteristic not available");
651
+ }
652
+ await this.txCharacteristic.writeValue(data);
653
+ }
654
+ onCharacteristicValueChanged(callback) {
655
+ this.characteristicCallback = callback;
656
+ }
657
+ onDisconnect(callback) {
658
+ this.disconnectCallback = callback;
659
+ }
660
+ onBluetoothAvailabilityChanged(callback) {
661
+ this.availabilityCallback = callback;
662
+ }
663
+ async readDeviceInformation() {
664
+ const info = {};
665
+ if (!this.device?.gatt?.connected) {
666
+ return info;
667
+ }
668
+ try {
669
+ const disService = await this.device.gatt.getPrimaryService(DIS_SERVICE_UUID);
670
+ const characteristicMap = [
671
+ { uuid: DIS_MANUFACTURER_NAME_UUID, key: "manufacturerName", binary: false },
672
+ { uuid: DIS_MODEL_NUMBER_UUID, key: "modelNumber", binary: false },
673
+ { uuid: DIS_SERIAL_NUMBER_UUID, key: "serialNumber", binary: false },
674
+ { uuid: DIS_HARDWARE_REVISION_UUID, key: "hardwareRevision", binary: false },
675
+ { uuid: DIS_FIRMWARE_REVISION_UUID, key: "firmwareRevision", binary: false },
676
+ { uuid: DIS_SOFTWARE_REVISION_UUID, key: "softwareRevision", binary: false },
677
+ { uuid: DIS_SYSTEM_ID_UUID, key: "systemId", binary: true },
678
+ { uuid: DIS_IEEE_REGULATORY_UUID, key: "ieeeRegulatory", binary: false },
679
+ { uuid: DIS_PNP_ID_UUID, key: "pnpId", binary: true }
680
+ ];
681
+ for (const { uuid, key, binary } of characteristicMap) {
682
+ try {
683
+ const characteristic = await disService.getCharacteristic(uuid);
684
+ const value = await characteristic.readValue();
685
+ if (binary) {
686
+ const hexValue = Array.from(new Uint8Array(value.buffer)).map((b) => b.toString(16).padStart(2, "0")).join(":");
687
+ info[key] = hexValue;
688
+ } else {
689
+ info[key] = new TextDecoder().decode(value);
690
+ }
691
+ } catch {
692
+ }
693
+ }
694
+ info.lastUpdated = /* @__PURE__ */ new Date();
695
+ } catch {
696
+ }
697
+ return info;
698
+ }
699
+ async cleanup() {
700
+ if (navigator.bluetooth && this.boundOnAvailabilityChanged) {
701
+ navigator.bluetooth.removeEventListener("availabilitychanged", this.boundOnAvailabilityChanged);
702
+ }
703
+ if (this.device && this.boundOnDeviceDisconnected) {
704
+ this.device.removeEventListener("gattserverdisconnected", this.boundOnDeviceDisconnected);
705
+ }
706
+ if (this.isConnected()) {
707
+ await this.disconnect();
708
+ }
709
+ }
710
+ };
711
+ }
712
+ });
713
+
714
+ // src/adapters/NodeBluetoothAdapter.ts
715
+ var NodeBluetoothAdapter_exports = {};
716
+ __export(NodeBluetoothAdapter_exports, {
717
+ NodeBluetoothAdapter: () => NodeBluetoothAdapter
718
+ });
719
+ var noble, NodeBluetoothAdapter;
720
+ var init_NodeBluetoothAdapter = __esm({
721
+ "src/adapters/NodeBluetoothAdapter.ts"() {
722
+ init_udtBluetoothAdapter();
723
+ init_udtConstants();
724
+ try {
725
+ if (typeof process !== "undefined" && process.versions?.node) {
726
+ noble = __require("@stoprocent/noble");
727
+ }
728
+ } catch {
729
+ }
730
+ NodeBluetoothAdapter = class {
731
+ constructor() {
732
+ this.peripheral = null;
733
+ this.txCharacteristic = null;
734
+ this.rxCharacteristic = null;
735
+ this.allCharacteristics = [];
736
+ this.isConnectedFlag = false;
737
+ }
738
+ /**
739
+ * Waits for Noble's BLE adapter to reach 'poweredOn' state.
740
+ * Uses @stoprocent/noble's built-in waitForPoweredOnAsync().
741
+ */
742
+ async ensureNobleReady() {
743
+ if (!noble) {
744
+ throw new BluetoothConnectionError(
745
+ "@stoprocent/noble not found. Install with: npm install @stoprocent/noble"
746
+ );
747
+ }
748
+ try {
749
+ await noble.waitForPoweredOnAsync();
750
+ } catch (error) {
751
+ throw new BluetoothConnectionError(
752
+ `Bluetooth adapter not ready: ${error.message}`,
753
+ error
754
+ );
755
+ }
756
+ }
757
+ async connect(deviceName, serviceUuids) {
758
+ try {
759
+ await this.ensureNobleReady();
760
+ if (this.availabilityCallback) {
761
+ this.availabilityCallback(true);
762
+ this.boundStateChangeHandler = (state) => {
763
+ if (this.availabilityCallback) {
764
+ this.availabilityCallback(state === "poweredOn");
765
+ }
766
+ };
767
+ noble.on("stateChange", this.boundStateChangeHandler);
768
+ }
769
+ const normalizedUuids = serviceUuids.map((u) => this.normalizeUuid(u));
770
+ const peripheral = await this.scanForDevice(deviceName, normalizedUuids, 1e4);
771
+ this.peripheral = peripheral;
772
+ this.boundDisconnectHandler = () => {
773
+ this.isConnectedFlag = false;
774
+ if (this.disconnectCallback) {
775
+ this.disconnectCallback();
776
+ }
777
+ };
778
+ this.peripheral.once("disconnect", this.boundDisconnectHandler);
779
+ await this.peripheral.connectAsync();
780
+ this.isConnectedFlag = true;
781
+ const txUuid = this.normalizeUuid(UART_TX_CHARACTERISTIC_UUID);
782
+ const rxUuid = this.normalizeUuid(UART_RX_CHARACTERISTIC_UUID);
783
+ const { characteristics } = await this.peripheral.discoverAllServicesAndCharacteristicsAsync();
784
+ this.allCharacteristics = characteristics;
785
+ this.txCharacteristic = characteristics.find(
786
+ (c) => this.normalizeUuid(c.uuid) === txUuid
787
+ );
788
+ this.rxCharacteristic = characteristics.find(
789
+ (c) => this.normalizeUuid(c.uuid) === rxUuid
790
+ );
791
+ if (!this.txCharacteristic || !this.rxCharacteristic) {
792
+ throw new BluetoothConnectionError(
793
+ "TX or RX characteristic not found on device"
794
+ );
795
+ }
796
+ await this.rxCharacteristic.subscribeAsync();
797
+ this.boundDataHandler = (data) => {
798
+ if (this.characteristicCallback) {
799
+ this.characteristicCallback(new Uint8Array(data));
800
+ }
801
+ };
802
+ this.rxCharacteristic.on("data", this.boundDataHandler);
803
+ } catch (error) {
804
+ await this.cleanup();
805
+ if (error instanceof BluetoothDeviceNotFoundError || error instanceof BluetoothConnectionError || error instanceof BluetoothTimeoutError) {
806
+ throw error;
807
+ }
808
+ throw new BluetoothConnectionError(
809
+ `Connection failed: ${error.message}`,
810
+ error
811
+ );
812
+ }
813
+ }
814
+ async disconnect() {
815
+ if (!this.peripheral)
816
+ return;
817
+ try {
818
+ if (this.rxCharacteristic) {
819
+ if (this.boundDataHandler) {
820
+ this.rxCharacteristic.removeListener("data", this.boundDataHandler);
821
+ }
822
+ await this.rxCharacteristic.unsubscribeAsync();
823
+ }
824
+ await this.peripheral.disconnectAsync();
825
+ } catch {
826
+ } finally {
827
+ this.peripheral = null;
828
+ this.txCharacteristic = null;
829
+ this.rxCharacteristic = null;
830
+ this.allCharacteristics = [];
831
+ this.isConnectedFlag = false;
832
+ }
833
+ }
834
+ isConnected() {
835
+ return this.isConnectedFlag && !!this.peripheral;
836
+ }
837
+ isGattConnected() {
838
+ return this.isConnectedFlag && !!this.peripheral && this.peripheral.state === "connected";
839
+ }
840
+ async writeCharacteristic(data) {
841
+ if (!this.txCharacteristic) {
842
+ throw new BluetoothConnectionError("TX characteristic not available");
843
+ }
844
+ try {
845
+ const buffer = Buffer.from(data);
846
+ await this.txCharacteristic.writeAsync(buffer, false);
847
+ } catch (error) {
848
+ throw new BluetoothConnectionError(
849
+ `Write failed: ${error.message}`,
850
+ error
851
+ );
852
+ }
853
+ }
854
+ onCharacteristicValueChanged(callback) {
855
+ this.characteristicCallback = callback;
856
+ }
857
+ onDisconnect(callback) {
858
+ this.disconnectCallback = callback;
859
+ }
860
+ onBluetoothAvailabilityChanged(callback) {
861
+ this.availabilityCallback = callback;
862
+ }
863
+ async readDeviceInformation() {
864
+ const info = {};
865
+ if (!this.peripheral || !this.isConnectedFlag) {
866
+ return info;
867
+ }
868
+ try {
869
+ const characteristics = this.allCharacteristics;
870
+ const characteristicMap = [
871
+ {
872
+ uuid: DIS_MANUFACTURER_NAME_UUID,
873
+ key: "manufacturerName",
874
+ binary: false
875
+ },
876
+ {
877
+ uuid: DIS_MODEL_NUMBER_UUID,
878
+ key: "modelNumber",
879
+ binary: false
880
+ },
881
+ {
882
+ uuid: DIS_SERIAL_NUMBER_UUID,
883
+ key: "serialNumber",
884
+ binary: false
885
+ },
886
+ {
887
+ uuid: DIS_HARDWARE_REVISION_UUID,
888
+ key: "hardwareRevision",
889
+ binary: false
890
+ },
891
+ {
892
+ uuid: DIS_FIRMWARE_REVISION_UUID,
893
+ key: "firmwareRevision",
894
+ binary: false
895
+ },
896
+ {
897
+ uuid: DIS_SOFTWARE_REVISION_UUID,
898
+ key: "softwareRevision",
899
+ binary: false
900
+ },
901
+ { uuid: DIS_SYSTEM_ID_UUID, key: "systemId", binary: true },
902
+ {
903
+ uuid: DIS_IEEE_REGULATORY_UUID,
904
+ key: "ieeeRegulatory",
905
+ binary: false
906
+ },
907
+ { uuid: DIS_PNP_ID_UUID, key: "pnpId", binary: true }
908
+ ];
909
+ for (const { uuid, key, binary } of characteristicMap) {
910
+ const normalizedUuid = this.normalizeUuid(uuid);
911
+ const shortUuid = this.toShortUuid(uuid);
912
+ const char = characteristics.find(
913
+ (c) => {
914
+ const cUuid = this.normalizeUuid(c.uuid);
915
+ return cUuid === normalizedUuid || cUuid === shortUuid;
916
+ }
917
+ );
918
+ if (!char)
919
+ continue;
920
+ try {
921
+ const buffer = await char.readAsync();
922
+ if (binary) {
923
+ const hexValue = Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join(":");
924
+ info[key] = hexValue;
925
+ } else {
926
+ info[key] = buffer.toString("utf-8");
927
+ }
928
+ } catch {
929
+ }
930
+ }
931
+ info.lastUpdated = /* @__PURE__ */ new Date();
932
+ } catch {
933
+ }
934
+ return info;
935
+ }
936
+ async cleanup() {
937
+ if (noble) {
938
+ if (this.boundStateChangeHandler) {
939
+ noble.removeListener(
940
+ "stateChange",
941
+ this.boundStateChangeHandler
942
+ );
943
+ }
944
+ }
945
+ if (this.peripheral && this.boundDisconnectHandler) {
946
+ this.peripheral.removeListener(
947
+ "disconnect",
948
+ this.boundDisconnectHandler
949
+ );
950
+ }
951
+ await this.disconnect();
952
+ this.characteristicCallback = void 0;
953
+ this.disconnectCallback = void 0;
954
+ this.availabilityCallback = void 0;
955
+ }
956
+ /**
957
+ * Scans for a BLE device by name using Noble's event-driven discovery
958
+ */
959
+ async scanForDevice(deviceName, serviceUuids, timeoutMs) {
960
+ return new Promise((resolve, reject) => {
961
+ const timeout = setTimeout(() => {
962
+ noble.stopScanning();
963
+ noble.removeListener("discover", onDiscover);
964
+ reject(
965
+ new BluetoothTimeoutError(
966
+ `Device scan timeout after ${timeoutMs}ms`
967
+ )
968
+ );
969
+ }, timeoutMs);
970
+ const onDiscover = (peripheral) => {
971
+ const name = peripheral.advertisement?.localName;
972
+ if (name && name.startsWith(deviceName)) {
973
+ clearTimeout(timeout);
974
+ noble.stopScanning();
975
+ noble.removeListener("discover", onDiscover);
976
+ resolve(peripheral);
977
+ }
978
+ };
979
+ noble.on("discover", onDiscover);
980
+ noble.startScanning(serviceUuids, false);
981
+ });
982
+ }
983
+ /**
984
+ * Normalizes UUID to Noble's format (lowercase, no dashes)
985
+ */
986
+ normalizeUuid(uuid) {
987
+ return uuid.toLowerCase().replace(/-/g, "");
988
+ }
989
+ /**
990
+ * Extracts the short 4-character UUID from a standard 128-bit BLE UUID.
991
+ * Standard BLE UUIDs follow the pattern 0000XXXX-0000-1000-8000-00805f9b34fb
992
+ * where XXXX is the short UUID. Noble uses this short form for standard characteristics.
993
+ */
994
+ toShortUuid(uuid) {
995
+ const normalized = this.normalizeUuid(uuid);
996
+ const baseSuffix = "00001000800000805f9b34fb";
997
+ if (normalized.startsWith("0000") && normalized.endsWith(baseSuffix)) {
998
+ return normalized.substring(4, 8);
999
+ }
1000
+ return normalized;
1001
+ }
1002
+ };
1003
+ }
1004
+ });
1005
+
1006
+ // src/UltimateDarkTower.ts
1007
+ init_udtConstants();
1008
+ init_udtTowerState();
1009
+
1010
+ // src/udtHelpers.ts
1011
+ init_udtConstants();
1012
+ function calculateBatteryPercentage(mv) {
1013
+ const batLevel = mv ? mv / 3 : 0;
1014
+ const levels = VOLTAGE_LEVELS.filter((v) => batLevel >= v);
1015
+ return levels.length * 5;
1016
+ }
1017
+ function milliVoltsToPercentageNumber(mv) {
1018
+ return calculateBatteryPercentage(mv);
1019
+ }
1020
+ function milliVoltsToPercentage(mv) {
1021
+ return `${calculateBatteryPercentage(mv)}%`;
1022
+ }
1023
+ function getMilliVoltsFromTowerResponse(command) {
1024
+ const mv = new Uint8Array(4);
1025
+ mv[0] = command[4];
1026
+ mv[1] = command[3];
1027
+ mv[2] = 0;
1028
+ mv[3] = 0;
1029
+ const view = new DataView(mv.buffer, 0);
1030
+ return view.getUint32(0, true);
1031
+ }
1032
+ function commandToPacketString(command) {
1033
+ if (command.length === 0) {
1034
+ return "[]";
1035
+ }
1036
+ let cmdStr = "[";
1037
+ command.forEach((n) => cmdStr += n.toString(16) + ",");
1038
+ cmdStr = cmdStr.slice(0, -1) + "]";
1039
+ return cmdStr;
1040
+ }
1041
+ function parseDifferentialReadings(response) {
1042
+ if (response.length < 5 || response[0] !== 6) {
1043
+ return null;
1044
+ }
1045
+ const drum1 = response[2];
1046
+ const drum2 = response[3];
1047
+ const drum3 = response[4];
1048
+ const irBeam = response[1];
1049
+ return {
1050
+ irBeam,
1051
+ drum1,
1052
+ drum2,
1053
+ drum3,
1054
+ timestamp: Date.now(),
1055
+ rawData: new Uint8Array(response)
1056
+ };
1057
+ }
1058
+ function createDefaultTowerState() {
1059
+ return {
1060
+ drum: [
1061
+ { jammed: false, calibrated: false, position: 0, playSound: false, reverse: false },
1062
+ { jammed: false, calibrated: false, position: 0, playSound: false, reverse: false },
1063
+ { jammed: false, calibrated: false, position: 0, playSound: false, reverse: false }
1064
+ ],
1065
+ layer: [
1066
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
1067
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
1068
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
1069
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
1070
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
1071
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] }
1072
+ ],
1073
+ audio: { sample: 0, loop: false, volume: 0 },
1074
+ beam: { count: 0, fault: false },
1075
+ led_sequence: 0
1076
+ };
1077
+ }
1078
+
1079
+ // src/udtLogger.ts
1080
+ var ConsoleOutput = class {
1081
+ write(level, message) {
1082
+ switch (level) {
1083
+ case "debug":
1084
+ console.debug(message);
1085
+ break;
1086
+ case "info":
1087
+ console.info(message);
1088
+ break;
1089
+ case "warn":
1090
+ console.warn(message);
1091
+ break;
1092
+ case "error":
1093
+ console.error(message);
1094
+ break;
1095
+ }
1096
+ }
1097
+ };
1098
+ var BufferOutput = class {
1099
+ constructor(maxEntries = 1e3, clearCount = 100) {
1100
+ this.buffer = [];
1101
+ this.maxEntries = 1e3;
1102
+ this.clearCount = 100;
1103
+ this.maxEntries = maxEntries;
1104
+ this.clearCount = clearCount;
1105
+ }
1106
+ write(level, message, timestamp) {
1107
+ this.buffer.push({ level, message, timestamp });
1108
+ if (this.buffer.length > this.maxEntries) {
1109
+ this.buffer.splice(0, this.clearCount);
1110
+ }
1111
+ }
1112
+ getBuffer() {
1113
+ return [...this.buffer];
1114
+ }
1115
+ getBufferSize() {
1116
+ return this.buffer.length;
1117
+ }
1118
+ clearBuffer() {
1119
+ this.buffer = [];
1120
+ }
1121
+ getEntriesByLevel(level) {
1122
+ return this.buffer.filter((entry) => entry.level === level);
1123
+ }
1124
+ getEntriesSince(timestamp) {
1125
+ return this.buffer.filter((entry) => entry.timestamp >= timestamp);
1126
+ }
1127
+ };
1128
+ var DOMOutput = class {
1129
+ constructor(containerId, maxLines = 100) {
1130
+ this.container = null;
1131
+ this.maxLines = 100;
1132
+ this.allEntries = [];
1133
+ this.container = typeof document !== "undefined" ? document.getElementById(containerId) : null;
1134
+ this.maxLines = maxLines;
1135
+ }
1136
+ write(level, message, timestamp) {
1137
+ if (!this.container)
1138
+ return;
1139
+ this.allEntries.push({ level, message, timestamp });
1140
+ while (this.allEntries.length > this.maxLines) {
1141
+ this.allEntries.shift();
1142
+ }
1143
+ this.refreshDisplay();
1144
+ }
1145
+ refreshDisplay() {
1146
+ if (!this.container)
1147
+ return;
1148
+ this.container.innerHTML = "";
1149
+ const enabledLevels = this.getEnabledLevelsFromCheckboxes();
1150
+ const textFilter = this.getTextFilter();
1151
+ this.allEntries.forEach((entry) => {
1152
+ if (enabledLevels.has(entry.level)) {
1153
+ if (textFilter && !entry.message.toLowerCase().includes(textFilter.toLowerCase())) {
1154
+ return;
1155
+ }
1156
+ const timeStr = entry.timestamp.toLocaleTimeString();
1157
+ const logLine = document.createElement("div");
1158
+ logLine.className = `log-line log-${entry.level}`;
1159
+ logLine.textContent = `[${timeStr}] ${entry.message}`;
1160
+ this.container.appendChild(logLine);
1161
+ }
1162
+ });
1163
+ this.container.scrollTop = this.container.scrollHeight;
1164
+ this.updateBufferSizeDisplay();
1165
+ }
1166
+ getEnabledLevelsFromCheckboxes() {
1167
+ const enabledLevels = /* @__PURE__ */ new Set();
1168
+ if (typeof document === "undefined") {
1169
+ return enabledLevels;
1170
+ }
1171
+ const checkboxes = ["debug", "info", "warn", "error"];
1172
+ checkboxes.forEach((level) => {
1173
+ const checkbox = document.getElementById(`logLevel-${level}`);
1174
+ if (checkbox && checkbox.checked) {
1175
+ enabledLevels.add(level);
1176
+ }
1177
+ });
1178
+ return enabledLevels;
1179
+ }
1180
+ getTextFilter() {
1181
+ if (typeof document === "undefined") {
1182
+ return "";
1183
+ }
1184
+ const textFilterInput = document.getElementById("logTextFilter");
1185
+ return textFilterInput?.value?.trim() || "";
1186
+ }
1187
+ updateBufferSizeDisplay() {
1188
+ if (typeof document === "undefined") {
1189
+ return;
1190
+ }
1191
+ const bufferSizeElement = document.getElementById("logBufferSize");
1192
+ if (!bufferSizeElement) {
1193
+ return;
1194
+ }
1195
+ const displayedCount = this.container?.children?.length || 0;
1196
+ const totalCount = this.allEntries.length;
1197
+ bufferSizeElement.textContent = `${displayedCount} / ${totalCount}`;
1198
+ }
1199
+ // Public method to refresh display when filter checkboxes change
1200
+ refreshFilter() {
1201
+ this.refreshDisplay();
1202
+ }
1203
+ // Public method to clear all entries
1204
+ clearAll() {
1205
+ this.allEntries = [];
1206
+ if (this.container) {
1207
+ this.container.innerHTML = "";
1208
+ }
1209
+ this.updateBufferSizeDisplay();
1210
+ }
1211
+ // Debug methods to help diagnose filtering issues
1212
+ getEntryCount() {
1213
+ return this.allEntries.length;
1214
+ }
1215
+ getEnabledLevels() {
1216
+ return Array.from(this.getEnabledLevelsFromCheckboxes());
1217
+ }
1218
+ debugEntries() {
1219
+ console.log("DOMOutput Debug:");
1220
+ console.log("- Container exists:", !!this.container);
1221
+ console.log("- Entry count:", this.allEntries.length);
1222
+ console.log("- Enabled levels:", this.getEnabledLevels());
1223
+ console.log("- Entries:", this.allEntries);
1224
+ }
1225
+ };
1226
+ var Logger = class _Logger {
1227
+ constructor() {
1228
+ this.outputs = [];
1229
+ this.enabledLevels = /* @__PURE__ */ new Set(["all"]);
1230
+ this.outputs.push(new ConsoleOutput());
1231
+ }
1232
+ static {
1233
+ this.instance = null;
1234
+ }
1235
+ static getInstance() {
1236
+ if (!_Logger.instance) {
1237
+ _Logger.instance = new _Logger();
1238
+ }
1239
+ return _Logger.instance;
1240
+ }
1241
+ addOutput(output) {
1242
+ this.outputs.push(output);
1243
+ }
1244
+ setMinLevel(level) {
1245
+ this.enabledLevels = /* @__PURE__ */ new Set([level]);
1246
+ }
1247
+ setEnabledLevels(levels) {
1248
+ this.enabledLevels = new Set(levels);
1249
+ }
1250
+ enableLevel(level) {
1251
+ this.enabledLevels.add(level);
1252
+ }
1253
+ disableLevel(level) {
1254
+ this.enabledLevels.delete(level);
1255
+ }
1256
+ getEnabledLevels() {
1257
+ return Array.from(this.enabledLevels);
1258
+ }
1259
+ shouldLog(level) {
1260
+ if (this.enabledLevels.has("all"))
1261
+ return true;
1262
+ if (level === "all")
1263
+ return true;
1264
+ if (this.enabledLevels.has(level))
1265
+ return true;
1266
+ if (this.enabledLevels.size === 1) {
1267
+ const singleLevel = Array.from(this.enabledLevels)[0];
1268
+ if (singleLevel !== "all") {
1269
+ const levels = ["debug", "info", "warn", "error"];
1270
+ const minIndex = levels.indexOf(singleLevel);
1271
+ const currentIndex = levels.indexOf(level);
1272
+ return currentIndex >= minIndex;
1273
+ }
1274
+ }
1275
+ return false;
1276
+ }
1277
+ log(level, message, context) {
1278
+ if (!this.shouldLog(level))
1279
+ return;
1280
+ const contextPrefix = context ? `${context} ` : "";
1281
+ const finalMessage = `${contextPrefix}${message}`;
1282
+ const timestamp = /* @__PURE__ */ new Date();
1283
+ this.outputs.forEach((output) => {
1284
+ try {
1285
+ output.write(level, finalMessage, timestamp);
1286
+ } catch (error) {
1287
+ console.error("Logger output error:", error);
1288
+ }
1289
+ });
1290
+ }
1291
+ debug(message, context) {
1292
+ this.log("debug", message, context);
1293
+ }
1294
+ info(message, context) {
1295
+ this.log("info", message, context);
1296
+ }
1297
+ warn(message, context) {
1298
+ this.log("warn", message, context);
1299
+ }
1300
+ error(message, context) {
1301
+ this.log("error", message, context);
1302
+ }
1303
+ /**
1304
+ * Logs tower state changes with detailed information about what changed.
1305
+ * @param oldState - The previous tower state
1306
+ * @param newState - The new tower state
1307
+ * @param source - Source identifier for the update (e.g., "sendTowerState", "tower response")
1308
+ * @param enableDetailedLogging - Whether to include detailed change descriptions
1309
+ */
1310
+ logTowerStateChange(oldState, newState, source, enableDetailedLogging = false) {
1311
+ this.info(`Tower state updated from ${source}`, "[UDT]");
1312
+ if (enableDetailedLogging) {
1313
+ const changes = this.computeStateChanges(oldState, newState);
1314
+ if (changes.length > 0) {
1315
+ this.info(`State changes: ${changes.join(", ")}`, "[UDT]");
1316
+ } else {
1317
+ this.info("No changes detected in state update", "[UDT]");
1318
+ }
1319
+ }
1320
+ }
1321
+ /**
1322
+ * Computes the differences between two tower states for logging purposes.
1323
+ * @param oldState - The previous tower state
1324
+ * @param newState - The new tower state
1325
+ * @returns Array of human-readable change descriptions
1326
+ */
1327
+ computeStateChanges(oldState, newState) {
1328
+ const changes = [];
1329
+ for (let i = 0; i < 3; i++) {
1330
+ const drumNames = ["top", "middle", "bottom"];
1331
+ const oldDrum = oldState.drum[i];
1332
+ const newDrum = newState.drum[i];
1333
+ if (oldDrum.position !== newDrum.position) {
1334
+ const positions = ["north", "east", "south", "west"];
1335
+ changes.push(`${drumNames[i]} drum: ${positions[oldDrum.position]} \u2192 ${positions[newDrum.position]}`);
1336
+ }
1337
+ if (oldDrum.calibrated !== newDrum.calibrated) {
1338
+ changes.push(`${drumNames[i]} drum calibrated: ${oldDrum.calibrated} \u2192 ${newDrum.calibrated}`);
1339
+ }
1340
+ if (oldDrum.jammed !== newDrum.jammed) {
1341
+ changes.push(`${drumNames[i]} drum jammed: ${oldDrum.jammed} \u2192 ${newDrum.jammed}`);
1342
+ }
1343
+ if (oldDrum.playSound !== newDrum.playSound) {
1344
+ changes.push(`${drumNames[i]} drum playSound: ${oldDrum.playSound} \u2192 ${newDrum.playSound}`);
1345
+ }
1346
+ }
1347
+ const layerNames = ["top ring", "middle ring", "bottom ring", "ledge", "base1", "base2"];
1348
+ for (let layerIndex = 0; layerIndex < 6; layerIndex++) {
1349
+ for (let lightIndex = 0; lightIndex < 4; lightIndex++) {
1350
+ const oldLight = oldState.layer[layerIndex].light[lightIndex];
1351
+ const newLight = newState.layer[layerIndex].light[lightIndex];
1352
+ const lightChanges = [];
1353
+ if (oldLight.effect !== newLight.effect) {
1354
+ lightChanges.push(`effect ${oldLight.effect} \u2192 ${newLight.effect}`);
1355
+ }
1356
+ if (oldLight.loop !== newLight.loop) {
1357
+ lightChanges.push(`loop ${oldLight.loop} \u2192 ${newLight.loop}`);
1358
+ }
1359
+ if (lightChanges.length > 0) {
1360
+ changes.push(`${layerNames[layerIndex]} light ${lightIndex}: ${lightChanges.join(", ")}`);
1361
+ }
1362
+ }
1363
+ }
1364
+ if (oldState.audio.sample !== newState.audio.sample) {
1365
+ changes.push(`audio sample: ${oldState.audio.sample} \u2192 ${newState.audio.sample}`);
1366
+ }
1367
+ if (oldState.audio.loop !== newState.audio.loop) {
1368
+ changes.push(`audio loop: ${oldState.audio.loop} \u2192 ${newState.audio.loop}`);
1369
+ }
1370
+ if (oldState.audio.volume !== newState.audio.volume) {
1371
+ changes.push(`audio volume: ${oldState.audio.volume} \u2192 ${newState.audio.volume}`);
1372
+ }
1373
+ if (oldState.beam.count !== newState.beam.count) {
1374
+ changes.push(`beam count: ${oldState.beam.count} \u2192 ${newState.beam.count}`);
1375
+ }
1376
+ if (oldState.beam.fault !== newState.beam.fault) {
1377
+ changes.push(`beam fault: ${oldState.beam.fault} \u2192 ${newState.beam.fault}`);
1378
+ }
1379
+ if (oldState.led_sequence !== newState.led_sequence) {
1380
+ changes.push(`LED sequence: ${oldState.led_sequence} \u2192 ${newState.led_sequence}`);
1381
+ }
1382
+ return changes;
1383
+ }
1384
+ };
1385
+ var logger = Logger.getInstance();
1386
+
1387
+ // src/udtBleConnection.ts
1388
+ init_udtConstants();
1389
+
1390
+ // src/udtTowerResponse.ts
1391
+ init_udtConstants();
1392
+ var TowerResponseProcessor = class {
1393
+ constructor(logDetail = false) {
1394
+ this.logDetail = false;
1395
+ this.logDetail = logDetail;
1396
+ }
1397
+ /**
1398
+ * Sets whether to include detailed information in command string conversion
1399
+ * @param {boolean} enabled - Whether to enable detailed logging
1400
+ */
1401
+ setDetailedLogging(enabled) {
1402
+ this.logDetail = enabled;
1403
+ }
1404
+ /**
1405
+ * Maps a command value to its corresponding tower message definition.
1406
+ * @param {number} cmdValue - Command value received from tower
1407
+ * @returns {Object} Object containing command key and command definition
1408
+ */
1409
+ getTowerCommand(cmdValue) {
1410
+ const cmdKeys = Object.keys(TOWER_MESSAGES);
1411
+ const cmdKey = cmdKeys.find((key) => TOWER_MESSAGES[key].value === cmdValue);
1412
+ if (!cmdKey) {
1413
+ logger.warn(`Unknown command received from tower: ${cmdValue} (0x${cmdValue.toString(16)})`, "TowerResponseProcessor");
1414
+ return { cmdKey: void 0, command: { name: "Unknown Command", value: cmdValue } };
1415
+ }
1416
+ const command = TOWER_MESSAGES[cmdKey];
1417
+ return { cmdKey, command };
1418
+ }
1419
+ /**
1420
+ * Converts a command packet to a human-readable string array for logging.
1421
+ * @param {Uint8Array} command - Command packet to convert
1422
+ * @returns {Array<string>} Human-readable representation of the command
1423
+ */
1424
+ commandToString(command) {
1425
+ const cmdValue = command[0];
1426
+ const { cmdKey, command: towerCommand } = this.getTowerCommand(cmdValue);
1427
+ switch (cmdKey) {
1428
+ case TC.STATE:
1429
+ case TC.INVALID_STATE:
1430
+ case TC.FAILURE:
1431
+ case TC.JIGGLE:
1432
+ case TC.UNEXPECTED:
1433
+ case TC.DURATION:
1434
+ case TC.DIFFERENTIAL:
1435
+ case TC.CALIBRATION:
1436
+ return [towerCommand.name, commandToPacketString(command)];
1437
+ case TC.BATTERY: {
1438
+ const millivolts = getMilliVoltsFromTowerResponse(command);
1439
+ const retval = [towerCommand.name, milliVoltsToPercentage(millivolts)];
1440
+ if (this.logDetail) {
1441
+ retval.push(`${millivolts}mv`);
1442
+ retval.push(commandToPacketString(command));
1443
+ }
1444
+ return retval;
1445
+ }
1446
+ default:
1447
+ return ["Unmapped Response!", commandToPacketString(command)];
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Determines if a response should be logged based on command type and configuration.
1452
+ * @param {string} cmdKey - Command key from tower message
1453
+ * @param {any} logConfig - Logging configuration object
1454
+ * @returns {boolean} Whether this response should be logged
1455
+ */
1456
+ shouldLogResponse(cmdKey, logConfig) {
1457
+ const logAll = logConfig["LOG_ALL"];
1458
+ let canLogThisResponse = logConfig[cmdKey] || logAll;
1459
+ if (!cmdKey) {
1460
+ canLogThisResponse = true;
1461
+ }
1462
+ return canLogThisResponse;
1463
+ }
1464
+ /**
1465
+ * Checks if a command is a battery response type.
1466
+ * @param {string} cmdKey - Command key from tower message
1467
+ * @returns {boolean} True if this is a battery response
1468
+ */
1469
+ isBatteryResponse(cmdKey) {
1470
+ return cmdKey === TC.BATTERY;
1471
+ }
1472
+ /**
1473
+ * Checks if a command is a tower state response type.
1474
+ * @param {string} cmdKey - Command key from tower message
1475
+ * @returns {boolean} True if this is a tower state response
1476
+ */
1477
+ isTowerStateResponse(cmdKey) {
1478
+ return cmdKey === TC.STATE;
1479
+ }
1480
+ };
1481
+
1482
+ // src/udtBleConnection.ts
1483
+ init_udtTowerState();
1484
+
1485
+ // src/udtBluetoothAdapterFactory.ts
1486
+ var BluetoothPlatform = /* @__PURE__ */ ((BluetoothPlatform3) => {
1487
+ BluetoothPlatform3["WEB"] = "web";
1488
+ BluetoothPlatform3["NODE"] = "node";
1489
+ BluetoothPlatform3["AUTO"] = "auto";
1490
+ return BluetoothPlatform3;
1491
+ })(BluetoothPlatform || {});
1492
+ var BluetoothAdapterFactory = class {
1493
+ /**
1494
+ * Creates a Bluetooth adapter for the specified platform
1495
+ * @param platform - Target platform (web, node, or auto-detect)
1496
+ * @returns Platform-specific Bluetooth adapter instance
1497
+ */
1498
+ static create(platform = "auto" /* AUTO */) {
1499
+ const detectedPlatform = platform === "auto" /* AUTO */ ? this.detectPlatform() : platform;
1500
+ switch (detectedPlatform) {
1501
+ case "web" /* WEB */: {
1502
+ const { WebBluetoothAdapter: WebBluetoothAdapter2 } = (init_WebBluetoothAdapter(), __toCommonJS(WebBluetoothAdapter_exports));
1503
+ return new WebBluetoothAdapter2();
1504
+ }
1505
+ case "node" /* NODE */: {
1506
+ const { NodeBluetoothAdapter: NodeBluetoothAdapter2 } = (init_NodeBluetoothAdapter(), __toCommonJS(NodeBluetoothAdapter_exports));
1507
+ return new NodeBluetoothAdapter2();
1508
+ }
1509
+ default:
1510
+ throw new Error(`Unsupported Bluetooth platform: ${detectedPlatform}`);
1511
+ }
1512
+ }
1513
+ /**
1514
+ * Detects the current runtime environment
1515
+ * @returns Detected platform (web or node)
1516
+ */
1517
+ static detectPlatform() {
1518
+ if (typeof navigator !== "undefined" && navigator.userAgent?.includes("React Native")) {
1519
+ throw new Error(
1520
+ "React Native detected. Auto-detection is not supported. Please provide a custom adapter implementing IBluetoothAdapter. See documentation for react-native-ble-plx adapter example."
1521
+ );
1522
+ }
1523
+ if (typeof window !== "undefined" && typeof navigator !== "undefined") {
1524
+ if ("bluetooth" in navigator) {
1525
+ return "web" /* WEB */;
1526
+ }
1527
+ }
1528
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
1529
+ return "node" /* NODE */;
1530
+ }
1531
+ throw new Error(
1532
+ "Unable to detect Bluetooth platform. Environment is neither browser with Web Bluetooth nor Node.js. Please explicitly specify platform or provide a custom adapter."
1533
+ );
1534
+ }
1535
+ };
1536
+
1537
+ // src/udtBleConnection.ts
1538
+ var UdtBleConnection = class {
1539
+ constructor(logger2, callbacks, adapter) {
1540
+ // Connection state
1541
+ this.isConnected = false;
1542
+ this.performingCalibration = false;
1543
+ this.performingLongCommand = false;
1544
+ // Connection monitoring
1545
+ this.connectionMonitorInterval = null;
1546
+ this.connectionMonitorFrequency = 2 * 1e3;
1547
+ this.lastSuccessfulCommand = 0;
1548
+ this.connectionTimeoutThreshold = 30 * 1e3;
1549
+ this.enableConnectionMonitoring = true;
1550
+ // Battery heartbeat monitoring
1551
+ this.lastBatteryHeartbeat = 0;
1552
+ this.batteryHeartbeatTimeout = 3 * 1e3;
1553
+ this.longTowerCommandTimeout = 30 * 1e3;
1554
+ this.enableBatteryHeartbeatMonitoring = true;
1555
+ this.batteryHeartbeatVerifyConnection = true;
1556
+ // When true, verifies connection before triggering disconnection on heartbeat timeout
1557
+ // Tower state
1558
+ this.towerSkullDropCount = -1;
1559
+ this.lastBatteryNotification = 0;
1560
+ this.lastBatteryPercentage = "";
1561
+ this.batteryNotifyFrequency = 15 * 1e3;
1562
+ this.batteryNotifyOnValueChangeOnly = false;
1563
+ this.batteryNotifyEnabled = true;
1564
+ // Device information
1565
+ this.deviceInformation = {};
1566
+ // Logging configuration
1567
+ this.logTowerResponses = true;
1568
+ this.logTowerResponseConfig = {
1569
+ TOWER_STATE: true,
1570
+ INVALID_STATE: true,
1571
+ HARDWARE_FAILURE: true,
1572
+ MECH_JIGGLE_TRIGGERED: true,
1573
+ MECH_UNEXPECTED_TRIGGER: true,
1574
+ MECH_DURATION: true,
1575
+ DIFFERENTIAL_READINGS: false,
1576
+ BATTERY_READING: true,
1577
+ CALIBRATION_FINISHED: true,
1578
+ LOG_ALL: false
1579
+ };
1580
+ this.logger = logger2;
1581
+ this.callbacks = callbacks;
1582
+ this.responseProcessor = new TowerResponseProcessor();
1583
+ this.bluetoothAdapter = adapter || BluetoothAdapterFactory.create("auto" /* AUTO */);
1584
+ this.bluetoothAdapter.onCharacteristicValueChanged((data) => {
1585
+ this.onRxData(data);
1586
+ });
1587
+ this.bluetoothAdapter.onDisconnect(() => {
1588
+ this.onTowerDeviceDisconnected();
1589
+ });
1590
+ this.bluetoothAdapter.onBluetoothAvailabilityChanged((available) => {
1591
+ this.bleAvailabilityChange(available);
1592
+ });
1593
+ }
1594
+ async connect() {
1595
+ this.logger.info("Looking for Tower...", "[UDT]");
1596
+ try {
1597
+ await this.bluetoothAdapter.connect(
1598
+ TOWER_DEVICE_NAME,
1599
+ [UART_SERVICE_UUID, DIS_SERVICE_UUID]
1600
+ );
1601
+ this.logger.info("Tower connection complete", "[UDT][BLE]");
1602
+ this.isConnected = true;
1603
+ this.lastSuccessfulCommand = Date.now();
1604
+ this.lastBatteryHeartbeat = Date.now();
1605
+ await this.readDeviceInformation();
1606
+ if (this.enableConnectionMonitoring) {
1607
+ this.startConnectionMonitoring();
1608
+ }
1609
+ this.callbacks.onTowerConnect();
1610
+ } catch (error) {
1611
+ this.logger.error(`Tower Connection Error: ${error}`, "[UDT][BLE]");
1612
+ this.isConnected = false;
1613
+ this.callbacks.onTowerDisconnect();
1614
+ }
1615
+ }
1616
+ async disconnect() {
1617
+ this.stopConnectionMonitoring();
1618
+ if (this.bluetoothAdapter.isConnected()) {
1619
+ await this.bluetoothAdapter.disconnect();
1620
+ this.logger.info("Tower disconnected", "[UDT]");
1621
+ }
1622
+ this.handleDisconnection();
1623
+ }
1624
+ /**
1625
+ * Writes a command to the tower via the Bluetooth adapter.
1626
+ * Used by UdtTowerCommands instead of direct characteristic access.
1627
+ */
1628
+ async writeCommand(command) {
1629
+ return await this.bluetoothAdapter.writeCharacteristic(command);
1630
+ }
1631
+ /**
1632
+ * Processes received data from the RX characteristic (platform-agnostic).
1633
+ * Called by the adapter's onCharacteristicValueChanged callback.
1634
+ */
1635
+ onRxData(receivedData) {
1636
+ this.lastSuccessfulCommand = Date.now();
1637
+ const { cmdKey } = this.responseProcessor.getTowerCommand(receivedData[0]);
1638
+ const isBattery = this.responseProcessor.isBatteryResponse(cmdKey);
1639
+ const shouldLogCommand = this.logTowerResponses && this.responseProcessor.shouldLogResponse(cmdKey, this.logTowerResponseConfig) && (!isBattery || this.batteryNotifyEnabled);
1640
+ if (shouldLogCommand) {
1641
+ this.logger.info(`${cmdKey}`, "[UDT][BLE][RCVD]");
1642
+ }
1643
+ if (this.logTowerResponses) {
1644
+ this.logTowerResponse(receivedData);
1645
+ }
1646
+ if (this.responseProcessor.isTowerStateResponse(cmdKey)) {
1647
+ this.handleTowerStateResponse(receivedData);
1648
+ }
1649
+ if (isBattery) {
1650
+ this.lastBatteryHeartbeat = Date.now();
1651
+ const millivolts = getMilliVoltsFromTowerResponse(receivedData);
1652
+ const batteryPercentage = milliVoltsToPercentage(millivolts);
1653
+ const didBatteryLevelChange = this.lastBatteryPercentage !== "" && this.lastBatteryPercentage !== batteryPercentage;
1654
+ const batteryNotifyFrequencyPassed = Date.now() - this.lastBatteryNotification >= this.batteryNotifyFrequency;
1655
+ const shouldNotify = this.batteryNotifyEnabled && (this.batteryNotifyOnValueChangeOnly ? didBatteryLevelChange || this.lastBatteryPercentage === "" : batteryNotifyFrequencyPassed);
1656
+ if (shouldNotify) {
1657
+ this.logger.info(`${this.responseProcessor.commandToString(receivedData).join(" ")}`, "[UDT][BLE]");
1658
+ this.lastBatteryNotification = Date.now();
1659
+ this.lastBatteryPercentage = batteryPercentage;
1660
+ this.callbacks.onBatteryLevelNotify(millivolts);
1661
+ }
1662
+ } else {
1663
+ if (this.callbacks.onTowerResponse) {
1664
+ this.callbacks.onTowerResponse(receivedData);
1665
+ }
1666
+ }
1667
+ }
1668
+ handleTowerStateResponse(receivedData) {
1669
+ const dataSkullDropCount = receivedData[SKULL_DROP_COUNT_POS];
1670
+ const state = rtdt_unpack_state(receivedData);
1671
+ this.logger.debug(`Tower State: ${JSON.stringify(state)} `, "[UDT][BLE]");
1672
+ if (this.performingCalibration) {
1673
+ this.performingCalibration = false;
1674
+ this.performingLongCommand = false;
1675
+ this.lastBatteryHeartbeat = Date.now();
1676
+ this.callbacks.onCalibrationComplete();
1677
+ this.logger.info("Tower calibration complete", "[UDT]");
1678
+ }
1679
+ if (dataSkullDropCount !== this.towerSkullDropCount) {
1680
+ if (dataSkullDropCount) {
1681
+ this.callbacks.onSkullDrop(dataSkullDropCount);
1682
+ this.logger.info(`Skull drop detected: app:${this.towerSkullDropCount < 0 ? "empty" : this.towerSkullDropCount} tower:${dataSkullDropCount}`, "[UDT]");
1683
+ } else {
1684
+ this.logger.info(`Skull count reset to ${dataSkullDropCount}`, "[UDT]");
1685
+ }
1686
+ this.towerSkullDropCount = dataSkullDropCount;
1687
+ }
1688
+ }
1689
+ logTowerResponse(receivedData) {
1690
+ const { cmdKey, command } = this.responseProcessor.getTowerCommand(receivedData[0]);
1691
+ if (!this.responseProcessor.shouldLogResponse(cmdKey, this.logTowerResponseConfig)) {
1692
+ return;
1693
+ }
1694
+ if (this.responseProcessor.isBatteryResponse(cmdKey)) {
1695
+ return;
1696
+ }
1697
+ const logMessage = `${this.responseProcessor.commandToString(receivedData).join(" ")}`;
1698
+ if (command.critical) {
1699
+ this.logger.error(logMessage, "[UDT][BLE]");
1700
+ } else {
1701
+ this.logger.info(logMessage, "[UDT][BLE]");
1702
+ }
1703
+ }
1704
+ bleAvailabilityChange(available) {
1705
+ this.logger.info("Bluetooth availability changed", "[UDT][BLE]");
1706
+ if (!available && this.isConnected) {
1707
+ this.logger.warn("Bluetooth became unavailable - handling disconnection", "[UDT][BLE]");
1708
+ this.handleDisconnection();
1709
+ }
1710
+ }
1711
+ onTowerDeviceDisconnected() {
1712
+ this.logger.warn("Tower device disconnected unexpectedly", "[UDT][BLE]");
1713
+ this.handleDisconnection();
1714
+ }
1715
+ handleDisconnection() {
1716
+ this.isConnected = false;
1717
+ this.performingCalibration = false;
1718
+ this.performingLongCommand = false;
1719
+ this.stopConnectionMonitoring();
1720
+ this.lastBatteryHeartbeat = 0;
1721
+ this.lastSuccessfulCommand = 0;
1722
+ this.deviceInformation = {};
1723
+ this.callbacks.onTowerDisconnect();
1724
+ }
1725
+ startConnectionMonitoring() {
1726
+ if (this.connectionMonitorInterval) {
1727
+ clearInterval(this.connectionMonitorInterval);
1728
+ }
1729
+ this.connectionMonitorInterval = setInterval(() => {
1730
+ this.checkConnectionHealth();
1731
+ }, this.connectionMonitorFrequency);
1732
+ }
1733
+ stopConnectionMonitoring() {
1734
+ if (this.connectionMonitorInterval) {
1735
+ clearInterval(this.connectionMonitorInterval);
1736
+ this.connectionMonitorInterval = null;
1737
+ }
1738
+ }
1739
+ checkConnectionHealth() {
1740
+ if (!this.isConnected) {
1741
+ return;
1742
+ }
1743
+ if (!this.bluetoothAdapter.isGattConnected()) {
1744
+ this.logger.warn("GATT connection lost detected during health check", "[UDT][BLE]");
1745
+ this.handleDisconnection();
1746
+ return;
1747
+ }
1748
+ if (this.enableBatteryHeartbeatMonitoring) {
1749
+ const timeSinceLastBatteryHeartbeat = Date.now() - this.lastBatteryHeartbeat;
1750
+ const timeoutThreshold = this.performingLongCommand ? this.longTowerCommandTimeout : this.batteryHeartbeatTimeout;
1751
+ if (timeSinceLastBatteryHeartbeat > timeoutThreshold) {
1752
+ const operationContext = this.performingLongCommand ? " during long command operation" : "";
1753
+ this.logger.warn(`Battery heartbeat timeout detected${operationContext} - no battery status received in ${timeSinceLastBatteryHeartbeat}ms (expected every ~200ms)`, "[UDT][BLE]");
1754
+ if (this.performingLongCommand) {
1755
+ this.logger.info("Ignoring battery heartbeat timeout during long command - this is expected behavior", "[UDT][BLE]");
1756
+ return;
1757
+ }
1758
+ if (this.batteryHeartbeatVerifyConnection) {
1759
+ this.logger.info("Verifying tower connection status before triggering disconnection...", "[UDT][BLE]");
1760
+ if (this.bluetoothAdapter.isGattConnected()) {
1761
+ this.logger.info("GATT connection still available - heartbeat timeout may be temporary", "[UDT][BLE]");
1762
+ this.lastBatteryHeartbeat = Date.now();
1763
+ this.logger.info("Reset battery heartbeat timer - will monitor for another timeout period", "[UDT][BLE]");
1764
+ return;
1765
+ }
1766
+ }
1767
+ this.logger.warn("Tower possibly disconnected due to battery depletion or power loss", "[UDT][BLE]");
1768
+ this.handleDisconnection();
1769
+ return;
1770
+ }
1771
+ }
1772
+ const timeSinceLastResponse = Date.now() - this.lastSuccessfulCommand;
1773
+ if (timeSinceLastResponse > this.connectionTimeoutThreshold) {
1774
+ this.logger.warn("General connection timeout detected - no responses received", "[UDT][BLE]");
1775
+ this.handleDisconnection();
1776
+ }
1777
+ }
1778
+ setConnectionMonitoring(enabled) {
1779
+ this.enableConnectionMonitoring = enabled;
1780
+ if (enabled && this.isConnected) {
1781
+ this.startConnectionMonitoring();
1782
+ } else {
1783
+ this.stopConnectionMonitoring();
1784
+ }
1785
+ }
1786
+ configureConnectionMonitoring(frequency = 2e3, timeout = 3e4) {
1787
+ this.connectionMonitorFrequency = frequency;
1788
+ this.connectionTimeoutThreshold = timeout;
1789
+ if (this.enableConnectionMonitoring && this.isConnected) {
1790
+ this.startConnectionMonitoring();
1791
+ }
1792
+ }
1793
+ configureBatteryHeartbeatMonitoring(enabled = true, timeout = 3e3, verifyConnection = true) {
1794
+ this.enableBatteryHeartbeatMonitoring = enabled;
1795
+ this.batteryHeartbeatTimeout = timeout;
1796
+ this.batteryHeartbeatVerifyConnection = verifyConnection;
1797
+ }
1798
+ async isConnectedAndResponsive() {
1799
+ if (!this.isConnected) {
1800
+ return false;
1801
+ }
1802
+ return this.bluetoothAdapter.isGattConnected();
1803
+ }
1804
+ getConnectionStatus() {
1805
+ const now = Date.now();
1806
+ const timeSinceLastBattery = this.lastBatteryHeartbeat ? now - this.lastBatteryHeartbeat : -1;
1807
+ const timeSinceLastCommand = this.lastSuccessfulCommand ? now - this.lastSuccessfulCommand : -1;
1808
+ return {
1809
+ isConnected: this.isConnected,
1810
+ isGattConnected: this.bluetoothAdapter.isGattConnected(),
1811
+ lastBatteryHeartbeatMs: timeSinceLastBattery,
1812
+ lastCommandResponseMs: timeSinceLastCommand,
1813
+ batteryHeartbeatHealthy: timeSinceLastBattery >= 0 && timeSinceLastBattery < this.batteryHeartbeatTimeout,
1814
+ connectionMonitoringEnabled: this.enableConnectionMonitoring,
1815
+ batteryHeartbeatMonitoringEnabled: this.enableBatteryHeartbeatMonitoring,
1816
+ batteryHeartbeatTimeoutMs: this.batteryHeartbeatTimeout,
1817
+ batteryHeartbeatVerifyConnection: this.batteryHeartbeatVerifyConnection,
1818
+ connectionTimeoutMs: this.connectionTimeoutThreshold
1819
+ };
1820
+ }
1821
+ getDeviceInformation() {
1822
+ return { ...this.deviceInformation };
1823
+ }
1824
+ async readDeviceInformation() {
1825
+ try {
1826
+ this.logger.info("Reading device information service...", "[UDT][BLE]");
1827
+ this.deviceInformation = await this.bluetoothAdapter.readDeviceInformation();
1828
+ for (const [key, value] of Object.entries(this.deviceInformation)) {
1829
+ if (key !== "lastUpdated" && value) {
1830
+ this.logger.info(`Device ${key}: ${value}`, "[UDT][BLE]");
1831
+ }
1832
+ }
1833
+ } catch (error) {
1834
+ this.logger.debug("Device Information Service not available", "[UDT][BLE]");
1835
+ }
1836
+ }
1837
+ async cleanup() {
1838
+ this.logger.info("Cleaning up UdtBleConnection instance", "[UDT][BLE]");
1839
+ this.stopConnectionMonitoring();
1840
+ if (this.isConnected) {
1841
+ await this.disconnect();
1842
+ }
1843
+ await this.bluetoothAdapter.cleanup();
1844
+ }
1845
+ };
1846
+
1847
+ // src/udtCommandFactory.ts
1848
+ init_udtConstants();
1849
+ init_udtTowerState();
1850
+ var UdtCommandFactory = class {
1851
+ /**
1852
+ * Creates a rotation command packet for positioning tower drums.
1853
+ * @param top - Target position for top drum
1854
+ * @param middle - Target position for middle drum
1855
+ * @param bottom - Target position for bottom drum
1856
+ * @returns Command packet for rotating tower drums
1857
+ */
1858
+ createRotateCommand(top, middle, bottom) {
1859
+ const rotateCmd = new Uint8Array(TOWER_COMMAND_PACKET_SIZE);
1860
+ rotateCmd[DRUM_PACKETS.topMiddle] = drumPositionCmds.top[top] | drumPositionCmds.middle[middle];
1861
+ rotateCmd[DRUM_PACKETS.bottom] = drumPositionCmds.bottom[bottom];
1862
+ return rotateCmd;
1863
+ }
1864
+ /**
1865
+ * Creates a sound command packet for playing tower audio.
1866
+ * @param soundIndex - Index of the sound to play from the audio library
1867
+ * @returns Command packet for playing sound
1868
+ */
1869
+ createSoundCommand(soundIndex) {
1870
+ const soundCommand = new Uint8Array(TOWER_COMMAND_PACKET_SIZE);
1871
+ const sound = Number("0x" + Number(soundIndex).toString(16).padStart(2, "0"));
1872
+ soundCommand[AUDIO_COMMAND_POS] = sound;
1873
+ return soundCommand;
1874
+ }
1875
+ /**
1876
+ * Creates a basic tower command packet with the specified command value.
1877
+ * @param commandValue - The command value to send
1878
+ * @returns Basic command packet
1879
+ */
1880
+ createBasicCommand(commandValue) {
1881
+ return new Uint8Array([commandValue]);
1882
+ }
1883
+ //#region Stateful Command Methods
1884
+ /**
1885
+ * Creates a stateful tower command by modifying only specific fields while preserving the rest.
1886
+ * This is the proper way to send commands that only change certain aspects of the tower state.
1887
+ * @param currentState - The current complete tower state (or null to create default state)
1888
+ * @param modifications - Partial tower state with only the fields to modify
1889
+ * @returns 20-byte command packet (command type + 19-byte state data)
1890
+ */
1891
+ createStatefulCommand(currentState, modifications) {
1892
+ const newState = currentState ? { ...currentState } : this.createEmptyTowerState();
1893
+ if (modifications.drum) {
1894
+ modifications.drum.forEach((drum, index) => {
1895
+ if (drum && newState.drum[index]) {
1896
+ Object.assign(newState.drum[index], drum);
1897
+ }
1898
+ });
1899
+ }
1900
+ if (modifications.layer) {
1901
+ modifications.layer.forEach((layer, layerIndex) => {
1902
+ if (layer && newState.layer[layerIndex]) {
1903
+ if (layer.light) {
1904
+ layer.light.forEach((light, lightIndex) => {
1905
+ if (light && newState.layer[layerIndex].light[lightIndex]) {
1906
+ Object.assign(newState.layer[layerIndex].light[lightIndex], light);
1907
+ }
1908
+ });
1909
+ }
1910
+ }
1911
+ });
1912
+ }
1913
+ if (modifications.audio) {
1914
+ Object.assign(newState.audio, modifications.audio);
1915
+ }
1916
+ if (modifications.beam) {
1917
+ Object.assign(newState.beam, modifications.beam);
1918
+ }
1919
+ if (modifications.led_sequence !== void 0) {
1920
+ newState.led_sequence = modifications.led_sequence;
1921
+ }
1922
+ return this.packTowerStateCommand(newState);
1923
+ }
1924
+ /**
1925
+ * Creates a stateful LED command that only changes specific LEDs while preserving all other state.
1926
+ * @param currentState - The current complete tower state
1927
+ * @param layerIndex - Layer index (0-5)
1928
+ * @param lightIndex - Light index within layer (0-3)
1929
+ * @param effect - Light effect (0=off, 1=on, 2=slow pulse, etc.)
1930
+ * @param loop - Whether to loop the effect
1931
+ * @returns 20-byte command packet
1932
+ */
1933
+ createStatefulLEDCommand(currentState, layerIndex, lightIndex, effect, loop = false) {
1934
+ const modifications = {};
1935
+ if (!modifications.layer) {
1936
+ modifications.layer = [];
1937
+ }
1938
+ if (!modifications.layer[layerIndex]) {
1939
+ modifications.layer[layerIndex] = { light: [] };
1940
+ }
1941
+ if (!modifications.layer[layerIndex].light) {
1942
+ modifications.layer[layerIndex].light = [];
1943
+ }
1944
+ modifications.layer[layerIndex].light[lightIndex] = { effect, loop };
1945
+ modifications.audio = { sample: 0, loop: false, volume: 0 };
1946
+ return this.createStatefulCommand(currentState, modifications);
1947
+ }
1948
+ /**
1949
+ * Creates a stateful audio command that preserves all current tower state while adding audio.
1950
+ * @param currentState - The current complete tower state
1951
+ * @param sample - Audio sample index to play (0-127)
1952
+ * @param loop - Whether to loop the audio
1953
+ * @param volume - Audio volume (0-15), optional
1954
+ * @returns 20-byte command packet
1955
+ */
1956
+ createStatefulAudioCommand(currentState, sample, loop = false, volume) {
1957
+ const audioMods = { sample, loop, volume: volume ?? 0 };
1958
+ const modifications = {
1959
+ audio: audioMods
1960
+ };
1961
+ return this.createStatefulCommand(currentState, modifications);
1962
+ }
1963
+ /**
1964
+ * Creates a transient audio command that includes current tower state but doesn't persist audio state.
1965
+ * This prevents audio from being included in subsequent commands.
1966
+ * @param currentState - The current complete tower state
1967
+ * @param sample - Audio sample index to play
1968
+ * @param loop - Whether to loop the audio
1969
+ * @param volume - Audio volume (0-15), optional
1970
+ * @returns Object containing the command packet and the state without audio for local tracking
1971
+ */
1972
+ createTransientAudioCommand(currentState, sample, loop = false, volume) {
1973
+ const audioMods = { sample, loop, volume: volume ?? 0 };
1974
+ const modifications = {
1975
+ audio: audioMods
1976
+ };
1977
+ const command = this.createStatefulCommand(currentState, modifications);
1978
+ const stateWithoutAudio = currentState ? { ...currentState } : this.createEmptyTowerState();
1979
+ stateWithoutAudio.audio = { sample: 0, loop: false, volume: 0 };
1980
+ return { command, stateWithoutAudio };
1981
+ }
1982
+ /**
1983
+ * Creates a transient audio command with additional modifications that includes current tower state
1984
+ * but doesn't persist audio state. This prevents audio from being included in subsequent commands.
1985
+ * @param currentState - The current complete tower state
1986
+ * @param sample - Audio sample index to play
1987
+ * @param loop - Whether to loop the audio
1988
+ * @param volume - Audio volume (0-15), optional
1989
+ * @param otherModifications - Other tower state modifications to include
1990
+ * @returns Object containing the command packet and the state with modifications but without audio
1991
+ */
1992
+ createTransientAudioCommandWithModifications(currentState, sample, loop = false, volume = void 0, otherModifications = {}) {
1993
+ const audioMods = { sample, loop, volume: volume ?? 0 };
1994
+ const modifications = {
1995
+ ...otherModifications,
1996
+ audio: audioMods
1997
+ };
1998
+ const command = this.createStatefulCommand(currentState, modifications);
1999
+ const stateWithoutAudio = currentState ? { ...currentState } : this.createEmptyTowerState();
2000
+ if (otherModifications.drum) {
2001
+ otherModifications.drum.forEach((drum, index) => {
2002
+ if (drum && stateWithoutAudio.drum[index]) {
2003
+ Object.assign(stateWithoutAudio.drum[index], drum);
2004
+ }
2005
+ });
2006
+ }
2007
+ if (otherModifications.layer) {
2008
+ otherModifications.layer.forEach((layer, layerIndex) => {
2009
+ if (layer && stateWithoutAudio.layer[layerIndex]) {
2010
+ if (layer.light) {
2011
+ layer.light.forEach((light, lightIndex) => {
2012
+ if (light && stateWithoutAudio.layer[layerIndex].light[lightIndex]) {
2013
+ Object.assign(stateWithoutAudio.layer[layerIndex].light[lightIndex], light);
2014
+ }
2015
+ });
2016
+ }
2017
+ }
2018
+ });
2019
+ }
2020
+ if (otherModifications.beam) {
2021
+ Object.assign(stateWithoutAudio.beam, otherModifications.beam);
2022
+ }
2023
+ if (otherModifications.led_sequence !== void 0) {
2024
+ stateWithoutAudio.led_sequence = otherModifications.led_sequence;
2025
+ }
2026
+ stateWithoutAudio.audio = { sample: 0, loop: false, volume: 0 };
2027
+ return { command, stateWithoutAudio };
2028
+ }
2029
+ /**
2030
+ * Creates a stateful drum rotation command that only changes drum positions while preserving all other state.
2031
+ * @param currentState - The current complete tower state
2032
+ * @param drumIndex - Drum index (0=top, 1=middle, 2=bottom)
2033
+ * @param position - Target position (0=north, 1=east, 2=south, 3=west)
2034
+ * @param playSound - Whether to play sound during rotation
2035
+ * @returns 20-byte command packet
2036
+ */
2037
+ createStatefulDrumCommand(currentState, drumIndex, position, playSound = false) {
2038
+ const modifications = {};
2039
+ if (!modifications.drum) {
2040
+ modifications.drum = [];
2041
+ }
2042
+ modifications.drum[drumIndex] = {
2043
+ jammed: false,
2044
+ calibrated: true,
2045
+ position,
2046
+ playSound,
2047
+ reverse: false
2048
+ };
2049
+ modifications.audio = { sample: 0, loop: false, volume: 0 };
2050
+ return this.createStatefulCommand(currentState, modifications);
2051
+ }
2052
+ /**
2053
+ * Packs a complete tower state into a 20-byte command packet.
2054
+ * @param state - Complete tower state to pack
2055
+ * @returns 20-byte command packet (0x00 + 19 bytes state data)
2056
+ */
2057
+ packTowerStateCommand(state) {
2058
+ const stateData = new Uint8Array(TOWER_STATE_DATA_SIZE);
2059
+ const success = rtdt_pack_state(stateData, TOWER_STATE_DATA_SIZE, state);
2060
+ if (!success) {
2061
+ throw new Error("Failed to pack tower state data");
2062
+ }
2063
+ const command = new Uint8Array(TOWER_COMMAND_PACKET_SIZE);
2064
+ command[0] = TOWER_COMMAND_TYPE_TOWER_STATE;
2065
+ command.set(stateData, TOWER_STATE_DATA_OFFSET);
2066
+ return command;
2067
+ }
2068
+ /**
2069
+ * Creates a default tower state with all systems off/neutral.
2070
+ * @returns Default TowerState object
2071
+ */
2072
+ createEmptyTowerState() {
2073
+ return {
2074
+ drum: [
2075
+ { jammed: false, calibrated: false, position: 0, playSound: false, reverse: false },
2076
+ { jammed: false, calibrated: false, position: 0, playSound: false, reverse: false },
2077
+ { jammed: false, calibrated: false, position: 0, playSound: false, reverse: false }
2078
+ ],
2079
+ layer: [
2080
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
2081
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
2082
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
2083
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
2084
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] },
2085
+ { light: [{ effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }, { effect: 0, loop: false }] }
2086
+ ],
2087
+ audio: { sample: 0, loop: false, volume: 0 },
2088
+ beam: { count: 0, fault: false },
2089
+ led_sequence: 0
2090
+ };
2091
+ }
2092
+ //#endregion
2093
+ };
2094
+
2095
+ // src/udtTowerCommands.ts
2096
+ init_udtConstants();
2097
+
2098
+ // src/udtCommandQueue.ts
2099
+ var CommandQueue = class {
2100
+ // 30 seconds
2101
+ constructor(logger2, sendCommandFn) {
2102
+ this.logger = logger2;
2103
+ this.sendCommandFn = sendCommandFn;
2104
+ this.queue = [];
2105
+ this.currentCommand = null;
2106
+ this.timeoutHandle = null;
2107
+ this.isProcessing = false;
2108
+ this.timeoutMs = 3e4;
2109
+ }
2110
+ /**
2111
+ * Enqueue a command for processing
2112
+ */
2113
+ async enqueue(command, description) {
2114
+ return new Promise((resolve, reject) => {
2115
+ const queuedCommand = {
2116
+ id: `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
2117
+ command,
2118
+ timestamp: Date.now(),
2119
+ resolve,
2120
+ reject,
2121
+ description
2122
+ };
2123
+ this.queue.push(queuedCommand);
2124
+ this.logger.debug(`Command queued: ${description || "unnamed"} (queue size: ${this.queue.length})`, "[UDT]");
2125
+ if (!this.isProcessing) {
2126
+ this.processNext();
2127
+ }
2128
+ });
2129
+ }
2130
+ /**
2131
+ * Process the next command in the queue
2132
+ */
2133
+ async processNext() {
2134
+ if (this.isProcessing || this.queue.length === 0) {
2135
+ return;
2136
+ }
2137
+ this.isProcessing = true;
2138
+ this.currentCommand = this.queue.shift();
2139
+ const { id, command, description, reject } = this.currentCommand;
2140
+ this.logger.debug(`Processing command: ${description || id}`, "[UDT]");
2141
+ try {
2142
+ this.timeoutHandle = setTimeout(() => {
2143
+ this.onTimeout();
2144
+ }, this.timeoutMs);
2145
+ await this.sendCommandFn(command);
2146
+ } catch (error) {
2147
+ this.clearTimeout();
2148
+ this.currentCommand = null;
2149
+ this.isProcessing = false;
2150
+ reject(error);
2151
+ this.processNext();
2152
+ }
2153
+ }
2154
+ /**
2155
+ * Called when a tower response is received
2156
+ */
2157
+ onResponse() {
2158
+ if (this.currentCommand) {
2159
+ this.clearTimeout();
2160
+ const { resolve, description, id } = this.currentCommand;
2161
+ this.logger.debug(`Command completed: ${description || id}`, "[UDT]");
2162
+ this.currentCommand = null;
2163
+ this.isProcessing = false;
2164
+ resolve();
2165
+ this.processNext();
2166
+ }
2167
+ }
2168
+ /**
2169
+ * Handle command timeout
2170
+ */
2171
+ onTimeout() {
2172
+ if (this.currentCommand) {
2173
+ const { description, id } = this.currentCommand;
2174
+ this.logger.warn(`Command timeout after ${this.timeoutMs}ms: ${description || id}`, "[UDT]");
2175
+ this.currentCommand.resolve();
2176
+ this.currentCommand = null;
2177
+ this.isProcessing = false;
2178
+ this.processNext();
2179
+ }
2180
+ }
2181
+ /**
2182
+ * Clear the current timeout
2183
+ */
2184
+ clearTimeout() {
2185
+ if (this.timeoutHandle) {
2186
+ clearTimeout(this.timeoutHandle);
2187
+ this.timeoutHandle = null;
2188
+ }
2189
+ }
2190
+ /**
2191
+ * Clear all pending commands
2192
+ */
2193
+ clear() {
2194
+ this.clearTimeout();
2195
+ this.queue.forEach((cmd) => {
2196
+ cmd.reject(new Error("Command queue cleared"));
2197
+ });
2198
+ this.queue = [];
2199
+ if (this.currentCommand) {
2200
+ this.currentCommand.reject(new Error("Command queue cleared"));
2201
+ }
2202
+ this.currentCommand = null;
2203
+ this.isProcessing = false;
2204
+ this.logger.debug("Command queue cleared", "[UDT]");
2205
+ }
2206
+ /**
2207
+ * Get queue status for debugging
2208
+ */
2209
+ getStatus() {
2210
+ return {
2211
+ queueLength: this.queue.length,
2212
+ isProcessing: this.isProcessing,
2213
+ currentCommand: this.currentCommand ? {
2214
+ id: this.currentCommand.id,
2215
+ description: this.currentCommand.description,
2216
+ timestamp: this.currentCommand.timestamp
2217
+ } : null
2218
+ };
2219
+ }
2220
+ };
2221
+
2222
+ // src/udtTowerCommands.ts
2223
+ var UdtTowerCommands = class {
2224
+ constructor(dependencies) {
2225
+ this.deps = dependencies;
2226
+ this.commandQueue = new CommandQueue(
2227
+ this.deps.logger,
2228
+ (command) => this.sendTowerCommandDirect(command)
2229
+ );
2230
+ }
2231
+ /**
2232
+ * Sends a command packet to the tower via the command queue
2233
+ * @param command - The command packet to send to the tower
2234
+ * @param description - Optional description for logging
2235
+ * @returns Promise that resolves when command is completed
2236
+ */
2237
+ async sendTowerCommand(command, description) {
2238
+ return await this.commandQueue.enqueue(command, description);
2239
+ }
2240
+ /**
2241
+ * Directly sends a command packet to the tower via Bluetooth with error handling and retry logic.
2242
+ * This method is used internally by the command queue.
2243
+ * @param command - The command packet to send to the tower
2244
+ * @returns Promise that resolves when command is sent successfully
2245
+ */
2246
+ async sendTowerCommandDirect(command) {
2247
+ try {
2248
+ const cmdStr = commandToPacketString(command);
2249
+ this.deps.logDetail && this.deps.logger.debug(`${cmdStr}`, "[UDT][CMD]");
2250
+ if (!this.deps.bleConnection.isConnected) {
2251
+ this.deps.logger.warn("Tower is not connected", "[UDT][CMD]");
2252
+ return;
2253
+ }
2254
+ await this.deps.bleConnection.writeCommand(command);
2255
+ this.deps.retrySendCommandCount.value = 0;
2256
+ this.deps.bleConnection.lastSuccessfulCommand = Date.now();
2257
+ } catch (error) {
2258
+ this.deps.logger.error(`command send error: ${error}`, "[UDT][CMD]");
2259
+ const errorMsg = error?.message ?? new String(error);
2260
+ const wasCancelled = errorMsg.includes("User cancelled");
2261
+ const maxRetriesReached = this.deps.retrySendCommandCount.value >= this.deps.retrySendCommandMax;
2262
+ 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;
2263
+ if (isDisconnected) {
2264
+ this.deps.logger.warn("Disconnect detected during command send", "[UDT][CMD]");
2265
+ await this.deps.bleConnection.disconnect();
2266
+ return;
2267
+ }
2268
+ if (!maxRetriesReached && this.deps.bleConnection.isConnected && !wasCancelled) {
2269
+ this.deps.logger.info(`retrying tower command attempt ${this.deps.retrySendCommandCount.value + 1}`, "[UDT][CMD]");
2270
+ this.deps.retrySendCommandCount.value++;
2271
+ setTimeout(() => {
2272
+ this.sendTowerCommandDirect(command);
2273
+ }, 250 * this.deps.retrySendCommandCount.value);
2274
+ } else {
2275
+ this.deps.retrySendCommandCount.value = 0;
2276
+ }
2277
+ }
2278
+ }
2279
+ /**
2280
+ * Initiates tower calibration to determine the current position of all tower drums.
2281
+ * This must be performed after connection before other tower operations.
2282
+ * @returns Promise that resolves when calibration command is sent
2283
+ */
2284
+ async calibrate() {
2285
+ if (!this.deps.bleConnection.performingCalibration) {
2286
+ this.deps.logger.info("Performing Tower Calibration", "[UDT][CMD]");
2287
+ await this.sendTowerCommand(new Uint8Array([TOWER_COMMANDS.calibration]), "calibrate");
2288
+ this.deps.bleConnection.performingCalibration = true;
2289
+ this.deps.bleConnection.performingLongCommand = true;
2290
+ return;
2291
+ }
2292
+ this.deps.logger.warn("Tower calibration requested when tower is already performing calibration", "[UDT][CMD]");
2293
+ return;
2294
+ }
2295
+ /**
2296
+ * Plays a sound from the tower's audio library using stateful commands that preserve existing tower state.
2297
+ * Audio state is not persisted to prevent sounds from replaying on subsequent commands.
2298
+ * @param soundIndex - Index of the sound to play (1-based, must be valid in TOWER_AUDIO_LIBRARY)
2299
+ * @returns Promise that resolves when sound command is sent
2300
+ */
2301
+ async playSound(soundIndex) {
2302
+ const invalidIndex = soundIndex === null || soundIndex > Object.keys(TOWER_AUDIO_LIBRARY).length || soundIndex <= 0;
2303
+ if (invalidIndex) {
2304
+ this.deps.logger.error(`attempt to play invalid sound index ${soundIndex}`, "[UDT][CMD]");
2305
+ return;
2306
+ }
2307
+ const currentState = this.deps.getCurrentTowerState();
2308
+ const { command } = this.deps.commandFactory.createTransientAudioCommand(currentState, soundIndex, false);
2309
+ this.deps.logger.info("Sending sound command (stateful)", "[UDT][CMD]");
2310
+ await this.sendTowerCommand(command, `playSound(${soundIndex})`);
2311
+ }
2312
+ /**
2313
+ * Controls the tower's LED lights including doorway, ledge, and base lights.
2314
+ * @param lights - Light configuration object specifying which lights to control and their effects
2315
+ * @returns Promise that resolves when light command is sent
2316
+ */
2317
+ async lights(lights) {
2318
+ this.deps.logDetail && this.deps.logger.debug(`Light Parameter ${JSON.stringify(lights)}`, "[UDT][CMD]");
2319
+ this.deps.logger.info("Sending light commands", "[UDT][CMD]");
2320
+ const layerCommands = this.mapLightsToLayerCommands(lights);
2321
+ for (const { layerIndex, lightIndex, effect } of layerCommands) {
2322
+ await this.setLEDStateful(layerIndex, lightIndex, effect);
2323
+ }
2324
+ }
2325
+ /**
2326
+ * Maps the Lights object to layer/light index commands for setLEDStateful.
2327
+ * @param lights - Light configuration object
2328
+ * @returns Array of layer commands
2329
+ */
2330
+ mapLightsToLayerCommands(lights) {
2331
+ const commands = [];
2332
+ if (lights.doorway) {
2333
+ for (const doorwayLight of lights.doorway) {
2334
+ const layerIndex = this.getTowerLayerForLevel(doorwayLight.level);
2335
+ const lightIndex = this.getLightIndexForSide(doorwayLight.position);
2336
+ const effect = LIGHT_EFFECTS[doorwayLight.style] || LIGHT_EFFECTS.off;
2337
+ commands.push({ layerIndex, lightIndex, effect, loop: true });
2338
+ }
2339
+ }
2340
+ if (lights.ledge) {
2341
+ for (const ledgeLight of lights.ledge) {
2342
+ const layerIndex = TOWER_LAYERS.LEDGE;
2343
+ const lightIndex = this.getLedgeLightIndexForSide(ledgeLight.position);
2344
+ const effect = LIGHT_EFFECTS[ledgeLight.style] || LIGHT_EFFECTS.off;
2345
+ commands.push({ layerIndex, lightIndex, effect, loop: false });
2346
+ }
2347
+ }
2348
+ if (lights.base) {
2349
+ for (const baseLight of lights.base) {
2350
+ const layerIndex = baseLight.position.level === "top" || baseLight.position.level === "b" ? TOWER_LAYERS.BASE2 : TOWER_LAYERS.BASE1;
2351
+ const lightIndex = this.getBaseLightIndexForSide(baseLight.position.side);
2352
+ const effect = LIGHT_EFFECTS[baseLight.style] || LIGHT_EFFECTS.off;
2353
+ commands.push({ layerIndex, lightIndex, effect, loop: false });
2354
+ }
2355
+ }
2356
+ return commands;
2357
+ }
2358
+ /**
2359
+ * Gets the tower layer index for a doorway light level.
2360
+ * @param level - Tower level (top, middle, bottom)
2361
+ * @returns Layer index
2362
+ */
2363
+ getTowerLayerForLevel(level) {
2364
+ switch (level) {
2365
+ case "top":
2366
+ return TOWER_LAYERS.TOP_RING;
2367
+ case "middle":
2368
+ return TOWER_LAYERS.MIDDLE_RING;
2369
+ case "bottom":
2370
+ return TOWER_LAYERS.BOTTOM_RING;
2371
+ default:
2372
+ return TOWER_LAYERS.TOP_RING;
2373
+ }
2374
+ }
2375
+ /**
2376
+ * Gets the light index for a cardinal direction (ring lights).
2377
+ * @param side - Tower side (north, east, south, west)
2378
+ * @returns Light index
2379
+ */
2380
+ getLightIndexForSide(side) {
2381
+ switch (side) {
2382
+ case "north":
2383
+ return RING_LIGHT_POSITIONS.NORTH;
2384
+ case "east":
2385
+ return RING_LIGHT_POSITIONS.EAST;
2386
+ case "south":
2387
+ return RING_LIGHT_POSITIONS.SOUTH;
2388
+ case "west":
2389
+ return RING_LIGHT_POSITIONS.WEST;
2390
+ default:
2391
+ return RING_LIGHT_POSITIONS.NORTH;
2392
+ }
2393
+ }
2394
+ /**
2395
+ * Maps cardinal directions to their closest corner positions for ledge lights.
2396
+ * @param side - Tower side (north, east, south, west)
2397
+ * @returns Tower corner (northeast, southeast, southwest, northwest)
2398
+ */
2399
+ mapSideToCorner(side) {
2400
+ switch (side) {
2401
+ case "north":
2402
+ return "northeast";
2403
+ case "east":
2404
+ return "southeast";
2405
+ case "south":
2406
+ return "southwest";
2407
+ case "west":
2408
+ return "northwest";
2409
+ default:
2410
+ return "northeast";
2411
+ }
2412
+ }
2413
+ /**
2414
+ * Gets the light index for ledge lights (ordinal directions).
2415
+ * @param corner - Tower corner (northeast, southeast, southwest, northwest)
2416
+ * @returns Light index
2417
+ */
2418
+ getLedgeLightIndexForSide(corner) {
2419
+ switch (corner) {
2420
+ case "northeast":
2421
+ return LEDGE_BASE_LIGHT_POSITIONS.NORTH_EAST;
2422
+ case "southeast":
2423
+ return LEDGE_BASE_LIGHT_POSITIONS.SOUTH_EAST;
2424
+ case "southwest":
2425
+ return LEDGE_BASE_LIGHT_POSITIONS.SOUTH_WEST;
2426
+ case "northwest":
2427
+ return LEDGE_BASE_LIGHT_POSITIONS.NORTH_WEST;
2428
+ default:
2429
+ return LEDGE_BASE_LIGHT_POSITIONS.NORTH_EAST;
2430
+ }
2431
+ }
2432
+ /**
2433
+ * Gets the light index for base lights (ordinal directions).
2434
+ * @param side - Tower side (north, east, south, west)
2435
+ * @returns Light index
2436
+ */
2437
+ getBaseLightIndexForSide(side) {
2438
+ return this.getLedgeLightIndexForSide(this.mapSideToCorner(side));
2439
+ }
2440
+ /**
2441
+ * Sends a light override command to control specific light patterns using stateful commands.
2442
+ * @param light - Light override value to send
2443
+ * @param soundIndex - Optional sound to play with the light override
2444
+ * @returns Promise that resolves when light override command is sent
2445
+ */
2446
+ async lightOverrides(light, soundIndex) {
2447
+ if (typeof light !== "number" || isNaN(light)) {
2448
+ this.deps.logger.error(`Invalid light parameter: ${light}. Must be a valid number.`, "[UDT][CMD]");
2449
+ return;
2450
+ }
2451
+ if (soundIndex !== void 0 && (typeof soundIndex !== "number" || isNaN(soundIndex) || soundIndex <= 0)) {
2452
+ this.deps.logger.error(`Invalid soundIndex parameter: ${soundIndex}. Must be a valid positive number.`, "[UDT][CMD]");
2453
+ return;
2454
+ }
2455
+ const currentState = this.deps.getCurrentTowerState();
2456
+ if (soundIndex) {
2457
+ const { command, stateWithoutAudio } = this.deps.commandFactory.createTransientAudioCommandWithModifications(
2458
+ currentState,
2459
+ soundIndex,
2460
+ false,
2461
+ void 0,
2462
+ { led_sequence: light }
2463
+ );
2464
+ this.deps.logger.info("Sending stateful light override with sound", "[UDT][CMD]");
2465
+ this.deps.setTowerState(stateWithoutAudio, "lightOverrides");
2466
+ await this.sendTowerCommand(command, `lightOverrides(${light}, ${soundIndex})`);
2467
+ } else {
2468
+ const modifications = {
2469
+ led_sequence: light
2470
+ };
2471
+ const command = this.deps.commandFactory.createStatefulCommand(currentState, modifications);
2472
+ this.deps.logger.info("Sending stateful light override", "[UDT][CMD]");
2473
+ await this.sendTowerCommand(command, `lightOverrides(${light})`);
2474
+ }
2475
+ }
2476
+ /**
2477
+ * Rotates tower drums to specified positions.
2478
+ * @param top - Position for the top drum ('north', 'east', 'south', 'west')
2479
+ * @param middle - Position for the middle drum
2480
+ * @param bottom - Position for the bottom drum
2481
+ * @param soundIndex - Optional sound to play during rotation
2482
+ * @returns Promise that resolves when rotate command is sent
2483
+ */
2484
+ async rotate(top, middle, bottom, soundIndex) {
2485
+ this.deps.logDetail && this.deps.logger.debug(`Rotate Parameter TMB[${JSON.stringify(top)}|${middle}|${bottom}] S[${soundIndex}]`, "[UDT][CMD]");
2486
+ const rotateCommand = this.deps.commandFactory.createRotateCommand(top, middle, bottom);
2487
+ if (soundIndex) {
2488
+ rotateCommand[AUDIO_COMMAND_POS] = soundIndex;
2489
+ }
2490
+ this.deps.logger.info("Sending rotate command" + (soundIndex ? " with sound" : ""), "[UDT]");
2491
+ this.deps.bleConnection.performingLongCommand = true;
2492
+ await this.sendTowerCommand(rotateCommand, `rotate(${top}, ${middle}, ${bottom}${soundIndex ? `, ${soundIndex}` : ""})`);
2493
+ setTimeout(() => {
2494
+ this.deps.bleConnection.performingLongCommand = false;
2495
+ this.deps.bleConnection.lastBatteryHeartbeat = Date.now();
2496
+ }, this.deps.bleConnection.longTowerCommandTimeout);
2497
+ const towerState = this.deps.getCurrentTowerState();
2498
+ if (towerState) {
2499
+ const topMiddleRaw = rotateCommand[DRUM_PACKETS.topMiddle];
2500
+ const bottomRaw = rotateCommand[DRUM_PACKETS.bottom];
2501
+ const topPosition = this.decodeDrumPositionFromRaw("top", topMiddleRaw);
2502
+ const middlePosition = this.decodeDrumPositionFromRaw("middle", topMiddleRaw);
2503
+ const bottomPosition = this.decodeDrumPositionFromRaw("bottom", bottomRaw);
2504
+ towerState.drum[0].position = topPosition;
2505
+ towerState.drum[1].position = middlePosition;
2506
+ towerState.drum[2].position = bottomPosition;
2507
+ }
2508
+ }
2509
+ /**
2510
+ * Rotates tower drums to specified positions.
2511
+ * @param top - Position for the top drum ('north', 'east', 'south', 'west')
2512
+ * @param middle - Position for the middle drum
2513
+ * @param bottom - Position for the bottom drum
2514
+ * @param soundIndex - Optional sound to play during rotation
2515
+ * @returns Promise that resolves when rotate command is sent
2516
+ */
2517
+ async rotateWithState(top, middle, bottom, soundIndex) {
2518
+ this.deps.logDetail && this.deps.logger.debug(`Rotate Parameter TMB[${JSON.stringify(top)}|${middle}|${bottom}] S[${soundIndex}]`, "[UDT][CMD]");
2519
+ const positionMap = {
2520
+ "north": 0,
2521
+ "east": 1,
2522
+ "south": 2,
2523
+ "west": 3
2524
+ };
2525
+ this.deps.logger.info("Sending stateful rotate commands" + (soundIndex ? " with sound" : ""), "[UDT][CMD]");
2526
+ this.deps.bleConnection.performingLongCommand = true;
2527
+ try {
2528
+ await this.rotateDrumStateful(0, positionMap[top], false);
2529
+ await this.rotateDrumStateful(1, positionMap[middle], false);
2530
+ await this.rotateDrumStateful(2, positionMap[bottom], false);
2531
+ if (soundIndex) {
2532
+ await this.playSound(soundIndex);
2533
+ }
2534
+ } finally {
2535
+ setTimeout(() => {
2536
+ this.deps.bleConnection.performingLongCommand = false;
2537
+ this.deps.bleConnection.lastBatteryHeartbeat = Date.now();
2538
+ }, this.deps.bleConnection.longTowerCommandTimeout);
2539
+ const towerState = this.deps.getCurrentTowerState();
2540
+ if (towerState) {
2541
+ towerState.drum[0].position = positionMap[top];
2542
+ towerState.drum[1].position = positionMap[middle];
2543
+ towerState.drum[2].position = positionMap[bottom];
2544
+ }
2545
+ }
2546
+ }
2547
+ /**
2548
+ * Resets the tower's internal skull drop counter to zero using stateful commands.
2549
+ * @returns Promise that resolves when reset command is sent
2550
+ */
2551
+ async resetTowerSkullCount() {
2552
+ this.deps.logger.info("Tower skull count reset requested", "[UDT][CMD]");
2553
+ const currentState = this.deps.getCurrentTowerState();
2554
+ const modifications = {
2555
+ beam: { count: 0, fault: false }
2556
+ };
2557
+ const command = this.deps.commandFactory.createStatefulCommand(currentState, modifications);
2558
+ await this.sendTowerCommand(command, "resetTowerSkullCount");
2559
+ const updatedState = { ...currentState };
2560
+ updatedState.beam.count = 0;
2561
+ this.deps.setTowerState(updatedState, "resetTowerSkullCount");
2562
+ }
2563
+ /**
2564
+ * Breaks a single seal on the tower, playing appropriate sound and lighting effects.
2565
+ * @param seal - Seal identifier to break (e.g., {side: 'north', level: 'middle'})
2566
+ * @param volume - Optional volume override (0=loud, 1=medium, 2=quiet, 3=mute). Uses current tower state if not provided.
2567
+ * @returns Promise that resolves when seal break sequence is complete
2568
+ */
2569
+ async breakSeal(seal, volume) {
2570
+ const actualVolume = volume !== void 0 ? volume : this.deps.getCurrentTowerState().audio.volume;
2571
+ if (actualVolume > 0) {
2572
+ const currentState = this.deps.getCurrentTowerState();
2573
+ const stateWithVolume = { ...currentState };
2574
+ stateWithVolume.audio = { sample: 0, loop: false, volume: actualVolume };
2575
+ await this.sendTowerStateStateful(stateWithVolume);
2576
+ }
2577
+ this.deps.logger.info("Playing tower seal sound", "[UDT]");
2578
+ await this.playSoundStateful(TOWER_AUDIO_LIBRARY.TowerSeal.value, false, actualVolume);
2579
+ const sideCorners = {
2580
+ north: ["northeast", "northwest"],
2581
+ east: ["northeast", "southeast"],
2582
+ south: ["southeast", "southwest"],
2583
+ west: ["southwest", "northwest"]
2584
+ };
2585
+ const ledgeLights = sideCorners[seal.side].map((corner) => ({
2586
+ position: corner,
2587
+ style: "on"
2588
+ }));
2589
+ const doorwayLights = [{
2590
+ level: seal.level,
2591
+ position: seal.side,
2592
+ style: "breatheFast"
2593
+ }];
2594
+ const lights = {
2595
+ ledge: ledgeLights,
2596
+ doorway: doorwayLights
2597
+ };
2598
+ this.deps.logger.info(`Breaking seal ${seal.level}-${seal.side} - lighting ledges and doorways with breath effect`, "[UDT]");
2599
+ await this.lights(lights);
2600
+ }
2601
+ /**
2602
+ * Randomly rotates specified tower levels to random positions.
2603
+ * @param level - Level configuration: 0=all, 1=top, 2=middle, 3=bottom, 4=top&middle, 5=top&bottom, 6=middle&bottom
2604
+ * @returns Promise that resolves when rotation command is sent
2605
+ */
2606
+ async randomRotateLevels(level = 0) {
2607
+ const sides = ["north", "east", "south", "west"];
2608
+ const getRandomSide = () => sides[Math.floor(Math.random() * sides.length)];
2609
+ const currentTop = this.getCurrentDrumPosition("top");
2610
+ const currentMiddle = this.getCurrentDrumPosition("middle");
2611
+ const currentBottom = this.getCurrentDrumPosition("bottom");
2612
+ let topSide, middleSide, bottomSide;
2613
+ switch (level) {
2614
+ case 0:
2615
+ topSide = getRandomSide();
2616
+ middleSide = getRandomSide();
2617
+ bottomSide = getRandomSide();
2618
+ break;
2619
+ case 1:
2620
+ topSide = getRandomSide();
2621
+ middleSide = currentMiddle;
2622
+ bottomSide = currentBottom;
2623
+ break;
2624
+ case 2:
2625
+ topSide = currentTop;
2626
+ middleSide = getRandomSide();
2627
+ bottomSide = currentBottom;
2628
+ break;
2629
+ case 3:
2630
+ topSide = currentTop;
2631
+ middleSide = currentMiddle;
2632
+ bottomSide = getRandomSide();
2633
+ break;
2634
+ case 4:
2635
+ topSide = getRandomSide();
2636
+ middleSide = getRandomSide();
2637
+ bottomSide = currentBottom;
2638
+ break;
2639
+ case 5:
2640
+ topSide = getRandomSide();
2641
+ middleSide = currentMiddle;
2642
+ bottomSide = getRandomSide();
2643
+ break;
2644
+ case 6:
2645
+ topSide = currentTop;
2646
+ middleSide = getRandomSide();
2647
+ bottomSide = getRandomSide();
2648
+ break;
2649
+ default:
2650
+ this.deps.logger.error("Invalid level parameter for randomRotateLevels. Must be 0-6.", "[UDT][CMD]");
2651
+ return;
2652
+ }
2653
+ this.deps.logger.info(`Random rotating levels to: top:${topSide}, middle:${middleSide}, bottom:${bottomSide}`, "[UDT][CMD]");
2654
+ await this.rotate(topSide, middleSide, bottomSide);
2655
+ }
2656
+ /**
2657
+ * Decodes drum position from raw command byte value.
2658
+ * @param level - The drum level ('top', 'middle', 'bottom')
2659
+ * @param rawValue - The raw byte value from the command
2660
+ * @returns The position as a number (0=north, 1=east, 2=south, 3=west)
2661
+ */
2662
+ decodeDrumPositionFromRaw(level, rawValue) {
2663
+ const drumPositions = drumPositionCmds[level];
2664
+ for (const [side, value] of Object.entries(drumPositions)) {
2665
+ if (level === "middle") {
2666
+ if ((value & 192) === (rawValue & 192)) {
2667
+ return ["north", "east", "south", "west"].indexOf(side);
2668
+ }
2669
+ } else if (level === "top") {
2670
+ if ((value & 22) === (rawValue & 22)) {
2671
+ return ["north", "east", "south", "west"].indexOf(side);
2672
+ }
2673
+ } else {
2674
+ if (value === rawValue) {
2675
+ return ["north", "east", "south", "west"].indexOf(side);
2676
+ }
2677
+ }
2678
+ }
2679
+ return 0;
2680
+ }
2681
+ /**
2682
+ * Gets the current position of a specific drum level.
2683
+ * @param level - The drum level to get position for
2684
+ * @returns The current position of the specified drum level
2685
+ */
2686
+ getCurrentDrumPosition(level) {
2687
+ const towerState = this.deps.getCurrentTowerState();
2688
+ if (!towerState) {
2689
+ return "north";
2690
+ }
2691
+ const drumIndex = level === "top" ? 0 : level === "middle" ? 1 : 2;
2692
+ const position = towerState.drum[drumIndex].position;
2693
+ const sides = ["north", "east", "south", "west"];
2694
+ return sides[position] || "north";
2695
+ }
2696
+ //#region Stateful Command Methods
2697
+ /**
2698
+ * Sends a stateful LED command that only changes specific LEDs while preserving all other state.
2699
+ * @param layerIndex - Layer index (0-5)
2700
+ * @param lightIndex - Light index within layer (0-3)
2701
+ * @param effect - Light effect (0=off, 1=on, 2=slow pulse, etc.)
2702
+ * @param loop - Whether to loop the effect, defaults to true
2703
+ * @returns Promise that resolves when command is sent
2704
+ */
2705
+ async setLEDStateful(layerIndex, lightIndex, effect, loop = true) {
2706
+ const currentState = this.deps.getCurrentTowerState();
2707
+ const command = this.deps.commandFactory.createStatefulLEDCommand(currentState, layerIndex, lightIndex, effect, loop);
2708
+ this.deps.logger.info(`Setting LED layer ${layerIndex} light ${lightIndex} to effect ${effect}${loop ? " (looped)" : ""}`, "[UDT][CMD]");
2709
+ await this.sendTowerCommand(command, `setLEDStateful(${layerIndex}, ${lightIndex}, ${effect}, ${loop})`);
2710
+ }
2711
+ /**
2712
+ * Plays a sound using stateful commands that preserve existing tower state.
2713
+ * Audio state is not persisted to prevent sounds from replaying on subsequent commands.
2714
+ * @param soundIndex - Index of the sound to play (1-based)
2715
+ * @param loop - Whether to loop the audio
2716
+ * @param volume - Audio volume (0-15), optional
2717
+ * @returns Promise that resolves when command is sent
2718
+ */
2719
+ async playSoundStateful(soundIndex, loop = false, volume) {
2720
+ const invalidIndex = soundIndex === null || soundIndex > Object.keys(TOWER_AUDIO_LIBRARY).length || soundIndex <= 0;
2721
+ if (invalidIndex) {
2722
+ this.deps.logger.error(`attempt to play invalid sound index ${soundIndex}`, "[UDT][CMD]");
2723
+ return;
2724
+ }
2725
+ const currentState = this.deps.getCurrentTowerState();
2726
+ const { command } = this.deps.commandFactory.createTransientAudioCommand(currentState, soundIndex, loop, volume);
2727
+ this.deps.logger.info(`Playing sound ${soundIndex}${loop ? " (looped)" : ""}${volume !== void 0 ? ` at volume ${volume}` : ""}`, "[UDT][CMD]");
2728
+ await this.sendTowerCommand(command, `playSoundStateful(${soundIndex}, ${loop}${volume !== void 0 ? `, ${volume}` : ""})`);
2729
+ }
2730
+ /**
2731
+ * Rotates a single drum using stateful commands that preserve existing tower state.
2732
+ * @param drumIndex - Drum index (0=top, 1=middle, 2=bottom)
2733
+ * @param position - Target position (0=north, 1=east, 2=south, 3=west)
2734
+ * @param playSound - Whether to play sound during rotation
2735
+ * @returns Promise that resolves when command is sent
2736
+ */
2737
+ async rotateDrumStateful(drumIndex, position, playSound = false) {
2738
+ const currentState = this.deps.getCurrentTowerState();
2739
+ const command = this.deps.commandFactory.createStatefulDrumCommand(currentState, drumIndex, position, playSound);
2740
+ const drumNames = ["top", "middle", "bottom"];
2741
+ const positionNames = ["north", "east", "south", "west"];
2742
+ this.deps.logger.info(`Rotating ${drumNames[drumIndex]} drum to ${positionNames[position]}${playSound ? " with sound" : ""}`, "[UDT][CMD]");
2743
+ this.deps.bleConnection.performingLongCommand = true;
2744
+ await this.sendTowerCommand(command, `rotateDrumStateful(${drumIndex}, ${position}, ${playSound})`);
2745
+ setTimeout(() => {
2746
+ this.deps.bleConnection.performingLongCommand = false;
2747
+ this.deps.bleConnection.lastBatteryHeartbeat = Date.now();
2748
+ }, this.deps.bleConnection.longTowerCommandTimeout);
2749
+ }
2750
+ /**
2751
+ * Sends a complete tower state using stateful commands.
2752
+ * Audio state is automatically cleared to prevent sounds from persisting across commands.
2753
+ * @param state - Complete tower state to send
2754
+ * @returns Promise that resolves when command is sent
2755
+ */
2756
+ async sendTowerStateStateful(state) {
2757
+ const stateToSend = { ...state };
2758
+ stateToSend.audio = { sample: 0, loop: false, volume: 0 };
2759
+ const command = this.deps.commandFactory.packTowerStateCommand(stateToSend);
2760
+ this.deps.logger.info("Sending complete tower state", "[UDT][CMD]");
2761
+ this.deps.setTowerState(stateToSend, "sendTowerStateStateful");
2762
+ await this.sendTowerCommand(command, "sendTowerStateStateful");
2763
+ }
2764
+ //#endregion
2765
+ /**
2766
+ * Public access to sendTowerCommandDirect for testing purposes.
2767
+ * This bypasses the command queue and sends commands directly.
2768
+ * @param command - The command packet to send directly to the tower
2769
+ * @returns Promise that resolves when command is sent
2770
+ */
2771
+ async sendTowerCommandDirectPublic(command) {
2772
+ return await this.sendTowerCommandDirect(command);
2773
+ }
2774
+ /**
2775
+ * Called when a tower response is received to notify the command queue
2776
+ * This should be called from the BLE connection response handler
2777
+ */
2778
+ onTowerResponse() {
2779
+ this.commandQueue.onResponse();
2780
+ }
2781
+ /**
2782
+ * Get command queue status for debugging
2783
+ */
2784
+ getQueueStatus() {
2785
+ return this.commandQueue.getStatus();
2786
+ }
2787
+ /**
2788
+ * Clear the command queue (for cleanup or error recovery)
2789
+ */
2790
+ clearQueue() {
2791
+ this.commandQueue.clear();
2792
+ }
2793
+ };
2794
+
2795
+ // src/UltimateDarkTower.ts
2796
+ var UltimateDarkTower = class {
2797
+ constructor(config) {
2798
+ // tower configuration
2799
+ this.retrySendCommandCountRef = { value: 0 };
2800
+ this.retrySendCommandMax = DEFAULT_RETRY_SEND_COMMAND_MAX;
2801
+ // tower state
2802
+ this.currentBatteryValue = 0;
2803
+ this.previousBatteryValue = 0;
2804
+ this.currentBatteryPercentage = 0;
2805
+ this.previousBatteryPercentage = 0;
2806
+ this.brokenSeals = /* @__PURE__ */ new Set();
2807
+ // Complete tower state tracking for stateful commands
2808
+ this.currentTowerState = createDefaultTowerState();
2809
+ // glyph position tracking
2810
+ this.glyphPositions = {
2811
+ cleanse: null,
2812
+ quest: null,
2813
+ battle: null,
2814
+ banner: null,
2815
+ reinforce: null
2816
+ };
2817
+ // Event callback functions
2818
+ // Override these with your own functions to handle events in your app
2819
+ this.onTowerConnect = () => {
2820
+ };
2821
+ this.onTowerDisconnect = () => {
2822
+ };
2823
+ this.onCalibrationComplete = () => {
2824
+ };
2825
+ this.onSkullDrop = (towerSkullCount) => {
2826
+ };
2827
+ this.onBatteryLevelNotify = (millivolts) => {
2828
+ };
2829
+ this.onTowerStateUpdate = (newState, oldState, source) => {
2830
+ };
2831
+ // utility
2832
+ this._logDetail = false;
2833
+ this.initializeLogger();
2834
+ this.initializeComponents(config);
2835
+ this.setupTowerResponseCallback();
2836
+ }
2837
+ /**
2838
+ * Initialize the logger with default console output
2839
+ */
2840
+ initializeLogger() {
2841
+ this.logger = new Logger();
2842
+ this.logger.addOutput(new ConsoleOutput());
2843
+ }
2844
+ /**
2845
+ * Initialize all tower components and their dependencies
2846
+ */
2847
+ initializeComponents(config) {
2848
+ let adapter;
2849
+ if (config?.adapter) {
2850
+ adapter = config.adapter;
2851
+ } else if (config?.platform) {
2852
+ adapter = BluetoothAdapterFactory.create(config.platform);
2853
+ }
2854
+ this.towerEventCallbacks = this.createTowerEventCallbacks();
2855
+ this.bleConnection = new UdtBleConnection(this.logger, this.towerEventCallbacks, adapter);
2856
+ this.responseProcessor = new TowerResponseProcessor(this.logDetail);
2857
+ this.commandFactory = new UdtCommandFactory();
2858
+ const commandDependencies = this.createCommandDependencies();
2859
+ this.towerCommands = new UdtTowerCommands(commandDependencies);
2860
+ }
2861
+ /**
2862
+ * Set up the tower response callback after all components are initialized
2863
+ */
2864
+ setupTowerResponseCallback() {
2865
+ this.towerEventCallbacks.onTowerResponse = (response) => {
2866
+ this.towerCommands.onTowerResponse();
2867
+ if (response.length >= TOWER_STATE_RESPONSE_MIN_LENGTH) {
2868
+ const { cmdKey } = this.responseProcessor.getTowerCommand(response[0]);
2869
+ if (this.responseProcessor.isTowerStateResponse(cmdKey)) {
2870
+ const stateData = response.slice(TOWER_STATE_DATA_OFFSET, TOWER_STATE_RESPONSE_MIN_LENGTH);
2871
+ this.updateTowerStateFromResponse(stateData);
2872
+ }
2873
+ }
2874
+ };
2875
+ }
2876
+ /**
2877
+ * Create tower event callbacks for BLE connection
2878
+ */
2879
+ createTowerEventCallbacks() {
2880
+ return {
2881
+ onTowerConnect: () => this.onTowerConnect(),
2882
+ onTowerDisconnect: () => {
2883
+ this.onTowerDisconnect();
2884
+ if (this.towerCommands) {
2885
+ this.towerCommands.clearQueue();
2886
+ }
2887
+ },
2888
+ onBatteryLevelNotify: (millivolts) => {
2889
+ this.updateBatteryState(millivolts);
2890
+ this.onBatteryLevelNotify(millivolts);
2891
+ },
2892
+ onCalibrationComplete: () => {
2893
+ this.setGlyphPositionsFromCalibration();
2894
+ this.onCalibrationComplete();
2895
+ },
2896
+ onSkullDrop: (towerSkullCount) => this.onSkullDrop(towerSkullCount),
2897
+ // onTowerResponse will be set up after tower commands are initialized
2898
+ onTowerResponse: () => {
2899
+ }
2900
+ };
2901
+ }
2902
+ /**
2903
+ * Create command dependencies object for tower commands
2904
+ */
2905
+ createCommandDependencies() {
2906
+ return {
2907
+ logger: this.logger,
2908
+ commandFactory: this.commandFactory,
2909
+ bleConnection: this.bleConnection,
2910
+ responseProcessor: this.responseProcessor,
2911
+ logDetail: this.logDetail,
2912
+ retrySendCommandCount: this.retrySendCommandCountRef,
2913
+ retrySendCommandMax: this.retrySendCommandMax,
2914
+ getCurrentTowerState: () => this.currentTowerState,
2915
+ setTowerState: (newState, source) => this.setTowerState(newState, source)
2916
+ };
2917
+ }
2918
+ /**
2919
+ * Update battery state values
2920
+ */
2921
+ updateBatteryState(millivolts) {
2922
+ this.previousBatteryValue = this.currentBatteryValue;
2923
+ this.currentBatteryValue = millivolts;
2924
+ this.previousBatteryPercentage = this.currentBatteryPercentage;
2925
+ this.currentBatteryPercentage = milliVoltsToPercentageNumber(millivolts);
2926
+ }
2927
+ get logDetail() {
2928
+ return this._logDetail;
2929
+ }
2930
+ set logDetail(value) {
2931
+ this._logDetail = value;
2932
+ this.responseProcessor.setDetailedLogging(value);
2933
+ if (this.towerCommands) {
2934
+ this.updateTowerCommandDependencies();
2935
+ }
2936
+ }
2937
+ /**
2938
+ * Update tower command dependencies when configuration changes
2939
+ */
2940
+ updateTowerCommandDependencies() {
2941
+ const commandDependencies = this.createCommandDependencies();
2942
+ this.towerCommands = new UdtTowerCommands(commandDependencies);
2943
+ }
2944
+ // Getter methods for connection state
2945
+ get isConnected() {
2946
+ return this.bleConnection.isConnected;
2947
+ }
2948
+ get isCalibrated() {
2949
+ return isCalibrated(this.currentTowerState);
2950
+ }
2951
+ get performingCalibration() {
2952
+ return this.bleConnection.performingCalibration;
2953
+ }
2954
+ get performingLongCommand() {
2955
+ return this.bleConnection.performingLongCommand;
2956
+ }
2957
+ get towerSkullDropCount() {
2958
+ return this.bleConnection.towerSkullDropCount;
2959
+ }
2960
+ // Getter methods for battery state
2961
+ get currentBattery() {
2962
+ return this.currentBatteryValue;
2963
+ }
2964
+ get previousBattery() {
2965
+ return this.previousBatteryValue;
2966
+ }
2967
+ get currentBatteryPercent() {
2968
+ return this.currentBatteryPercentage;
2969
+ }
2970
+ get previousBatteryPercent() {
2971
+ return this.previousBatteryPercentage;
2972
+ }
2973
+ // Getter/setter methods for connection configuration
2974
+ get batteryNotifyFrequency() {
2975
+ return this.bleConnection.batteryNotifyFrequency;
2976
+ }
2977
+ set batteryNotifyFrequency(value) {
2978
+ this.bleConnection.batteryNotifyFrequency = value;
2979
+ }
2980
+ get batteryNotifyOnValueChangeOnly() {
2981
+ return this.bleConnection.batteryNotifyOnValueChangeOnly;
2982
+ }
2983
+ set batteryNotifyOnValueChangeOnly(value) {
2984
+ this.bleConnection.batteryNotifyOnValueChangeOnly = value;
2985
+ }
2986
+ get batteryNotifyEnabled() {
2987
+ return this.bleConnection.batteryNotifyEnabled;
2988
+ }
2989
+ set batteryNotifyEnabled(value) {
2990
+ this.bleConnection.batteryNotifyEnabled = value;
2991
+ }
2992
+ get logTowerResponses() {
2993
+ return this.bleConnection.logTowerResponses;
2994
+ }
2995
+ set logTowerResponses(value) {
2996
+ this.bleConnection.logTowerResponses = value;
2997
+ }
2998
+ get logTowerResponseConfig() {
2999
+ return this.bleConnection.logTowerResponseConfig;
3000
+ }
3001
+ set logTowerResponseConfig(value) {
3002
+ this.bleConnection.logTowerResponseConfig = value;
3003
+ }
3004
+ //#region Tower Commands
3005
+ /**
3006
+ * Initiates tower calibration to determine the current position of all tower drums.
3007
+ * This must be performed after connection before other tower operations.
3008
+ * @returns Promise that resolves when calibration command is sent
3009
+ */
3010
+ async calibrate() {
3011
+ return this.towerCommands.calibrate();
3012
+ }
3013
+ /**
3014
+ * Plays a sound from the tower's audio library.
3015
+ * @param soundIndex - Index of the sound to play (1-based, must be valid in TOWER_AUDIO_LIBRARY)
3016
+ * @returns Promise that resolves when sound command is sent
3017
+ */
3018
+ async playSound(soundIndex) {
3019
+ return this.towerCommands.playSound(soundIndex);
3020
+ }
3021
+ /**
3022
+ * Controls the tower's LED lights including doorway, ledge, and base lights.
3023
+ * @param lights - Light configuration object specifying which lights to control and their effects
3024
+ * @returns Promise that resolves when light command is sent
3025
+ */
3026
+ async lights(lights) {
3027
+ return this.towerCommands.lights(lights);
3028
+ }
3029
+ /**
3030
+ * Controls the tower's LED lights including doorway, ledge, and base lights.
3031
+ * @deprecated Use `lights()` instead. This method will be removed in a future version.
3032
+ * @param lights - Light configuration object specifying which lights to control and their effects
3033
+ * @returns Promise that resolves when light command is sent
3034
+ */
3035
+ async Lights(lights) {
3036
+ return this.lights(lights);
3037
+ }
3038
+ /**
3039
+ * Sends a raw command packet directly to the tower (for testing purposes).
3040
+ * @param command - The raw command packet to send
3041
+ * @returns Promise that resolves when command is sent
3042
+ */
3043
+ async sendTowerCommandDirect(command) {
3044
+ return this.towerCommands.sendTowerCommandDirectPublic(command);
3045
+ }
3046
+ /**
3047
+ * Sends a light override command to control specific light patterns.
3048
+ * @param light - Light override value to send
3049
+ * @param soundIndex - Optional sound to play with the light override
3050
+ * @returns Promise that resolves when light override command is sent
3051
+ */
3052
+ async lightOverrides(light, soundIndex) {
3053
+ return await this.towerCommands.lightOverrides(light, soundIndex);
3054
+ }
3055
+ /**
3056
+ * Rotates tower drums to specified positions.
3057
+ * @param top - Position for the top drum ('north', 'east', 'south', 'west')
3058
+ * @param middle - Position for the middle drum
3059
+ * @param bottom - Position for the bottom drum
3060
+ * @param soundIndex - Optional sound to play during rotation
3061
+ * @returns Promise that resolves when rotate command is sent
3062
+ */
3063
+ async Rotate(top, middle, bottom, soundIndex) {
3064
+ const oldTopPosition = this.getCurrentDrumPosition("top");
3065
+ const oldMiddlePosition = this.getCurrentDrumPosition("middle");
3066
+ const oldBottomPosition = this.getCurrentDrumPosition("bottom");
3067
+ const result = await this.towerCommands.rotate(top, middle, bottom, soundIndex);
3068
+ this.calculateAndUpdateGlyphPositions("top", oldTopPosition, top);
3069
+ this.calculateAndUpdateGlyphPositions("middle", oldMiddlePosition, middle);
3070
+ this.calculateAndUpdateGlyphPositions("bottom", oldBottomPosition, bottom);
3071
+ return result;
3072
+ }
3073
+ /**
3074
+ * Resets the tower's internal skull drop counter to zero.
3075
+ * @returns Promise that resolves when reset command is sent
3076
+ */
3077
+ async resetTowerSkullCount() {
3078
+ return await this.towerCommands.resetTowerSkullCount();
3079
+ }
3080
+ //#endregion
3081
+ //#region Stateful Tower Commands
3082
+ /**
3083
+ * Sets a specific LED using stateful commands that preserve all other tower state.
3084
+ * This is the recommended way to control individual LEDs.
3085
+ * @param layerIndex - Layer index (0-5: TopRing, MiddleRing, BottomRing, Ledge, Base1, Base2)
3086
+ * @param lightIndex - Light index within layer (0-3)
3087
+ * @param effect - Light effect (0=off, 1=on, 2=slow pulse, 3=fast pulse, etc.)
3088
+ * @param loop - Whether to loop the effect
3089
+ * @returns Promise that resolves when command is sent
3090
+ */
3091
+ async setLED(layerIndex, lightIndex, effect, loop = false) {
3092
+ return await this.towerCommands.setLEDStateful(layerIndex, lightIndex, effect, loop);
3093
+ }
3094
+ /**
3095
+ * Plays a sound using stateful commands that preserve existing tower state.
3096
+ * @param soundIndex - Index of the sound to play (1-based)
3097
+ * @param loop - Whether to loop the audio
3098
+ * @param volume - Audio volume (0-15), optional
3099
+ * @returns Promise that resolves when command is sent
3100
+ */
3101
+ async playSoundStateful(soundIndex, loop = false, volume) {
3102
+ return await this.towerCommands.playSoundStateful(soundIndex, loop, volume);
3103
+ }
3104
+ /**
3105
+ * Rotates a single drum using stateful commands that preserve existing tower state.
3106
+ * @param drumIndex - Drum index (0=top, 1=middle, 2=bottom)
3107
+ * @param position - Target position (0=north, 1=east, 2=south, 3=west)
3108
+ * @param playSound - Whether to play sound during rotation
3109
+ * @returns Promise that resolves when command is sent
3110
+ */
3111
+ async rotateDrumStateful(drumIndex, position, playSound = false) {
3112
+ return await this.towerCommands.rotateDrumStateful(drumIndex, position, playSound);
3113
+ }
3114
+ /**
3115
+ * Rotates tower drums to specified positions using stateful commands that preserve existing tower state.
3116
+ * This is the recommended way to rotate drums as it preserves LEDs and other tower state.
3117
+ * @param top - Position for the top drum ('north', 'east', 'south', 'west')
3118
+ * @param middle - Position for the middle drum
3119
+ * @param bottom - Position for the bottom drum
3120
+ * @param soundIndex - Optional sound to play during rotation
3121
+ * @returns Promise that resolves when rotate command is sent
3122
+ */
3123
+ async rotateWithState(top, middle, bottom, soundIndex) {
3124
+ const oldTopPosition = this.getCurrentDrumPosition("top");
3125
+ const oldMiddlePosition = this.getCurrentDrumPosition("middle");
3126
+ const oldBottomPosition = this.getCurrentDrumPosition("bottom");
3127
+ const result = await this.towerCommands.rotateWithState(top, middle, bottom, soundIndex);
3128
+ this.calculateAndUpdateGlyphPositions("top", oldTopPosition, top);
3129
+ this.calculateAndUpdateGlyphPositions("middle", oldMiddlePosition, middle);
3130
+ this.calculateAndUpdateGlyphPositions("bottom", oldBottomPosition, bottom);
3131
+ return result;
3132
+ }
3133
+ //#endregion
3134
+ //#region Tower State Management
3135
+ /**
3136
+ * Gets the current complete tower state if available.
3137
+ * @returns The current tower state object
3138
+ */
3139
+ getCurrentTowerState() {
3140
+ return { ...this.currentTowerState };
3141
+ }
3142
+ /**
3143
+ * Sends a complete tower state to the tower, preserving existing state.
3144
+ * Audio state is automatically cleared to prevent sounds from persisting across commands.
3145
+ * @param towerState - The tower state to send
3146
+ * @returns Promise that resolves when the command is sent
3147
+ */
3148
+ async sendTowerState(towerState) {
3149
+ const { rtdt_pack_state: rtdt_pack_state2 } = await Promise.resolve().then(() => (init_udtTowerState(), udtTowerState_exports));
3150
+ const stateToSend = { ...towerState };
3151
+ stateToSend.audio = { sample: 0, loop: false, volume: 0 };
3152
+ const stateData = new Uint8Array(TOWER_STATE_DATA_SIZE);
3153
+ const success = rtdt_pack_state2(stateData, TOWER_STATE_DATA_SIZE, stateToSend);
3154
+ if (!success) {
3155
+ throw new Error("Failed to pack tower state data");
3156
+ }
3157
+ const command = new Uint8Array(TOWER_COMMAND_PACKET_SIZE);
3158
+ command[0] = TOWER_COMMAND_TYPE_TOWER_STATE;
3159
+ command.set(stateData, TOWER_STATE_DATA_OFFSET);
3160
+ this.setTowerState({ ...stateToSend }, "sendTowerState");
3161
+ return await this.sendTowerCommandDirect(command);
3162
+ }
3163
+ /**
3164
+ * Sets the tower state with comprehensive logging of changes.
3165
+ * @param newState - The new tower state to set
3166
+ * @param source - Source identifier for logging (e.g., "sendTowerState", "tower response")
3167
+ */
3168
+ setTowerState(newState, source) {
3169
+ const oldState = this.currentTowerState;
3170
+ this.currentTowerState = newState;
3171
+ this.logger.logTowerStateChange(oldState, newState, source, this.logDetail);
3172
+ this.onTowerStateUpdate(newState, oldState, source);
3173
+ }
3174
+ /**
3175
+ * Updates the current tower state from a tower response.
3176
+ * Called internally when tower state responses are received.
3177
+ * Audio state is reset to prevent sounds from persisting across commands.
3178
+ * @param stateData - The 19-byte state data from tower response
3179
+ */
3180
+ updateTowerStateFromResponse(stateData) {
3181
+ Promise.resolve().then(() => (init_udtTowerState(), udtTowerState_exports)).then(({ rtdt_unpack_state: rtdt_unpack_state2 }) => {
3182
+ const newState = rtdt_unpack_state2(stateData);
3183
+ newState.audio = { sample: 0, loop: false, volume: this.currentTowerState.audio.volume };
3184
+ this.setTowerState(newState, "tower response");
3185
+ });
3186
+ }
3187
+ //#endregion
3188
+ /**
3189
+ * Breaks a single seal on the tower, playing appropriate sound and lighting effects.
3190
+ * @param seal - Seal identifier to break (e.g., {side: 'north', level: 'middle'})
3191
+ * @param volume - Optional volume override (0=loud, 1=medium, 2=quiet, 3=mute). Uses current tower state if not provided.
3192
+ * @returns Promise that resolves when seal break sequence is complete
3193
+ */
3194
+ async breakSeal(seal, volume) {
3195
+ const result = await this.towerCommands.breakSeal(seal, volume);
3196
+ const sealKey = `${seal.level}-${seal.side}`;
3197
+ this.brokenSeals.add(sealKey);
3198
+ return result;
3199
+ }
3200
+ /**
3201
+ * Randomly rotates specified tower levels to random positions.
3202
+ * @param level - Level configuration: 0=all, 1=top, 2=middle, 3=bottom, 4=top&middle, 5=top&bottom, 6=middle&bottom
3203
+ * @returns Promise that resolves when rotation command is sent
3204
+ */
3205
+ async randomRotateLevels(level = 0) {
3206
+ const beforeTop = this.getCurrentDrumPosition("top");
3207
+ const beforeMiddle = this.getCurrentDrumPosition("middle");
3208
+ const beforeBottom = this.getCurrentDrumPosition("bottom");
3209
+ const result = await this.towerCommands.randomRotateLevels(level);
3210
+ const afterTop = this.getCurrentDrumPosition("top");
3211
+ const afterMiddle = this.getCurrentDrumPosition("middle");
3212
+ const afterBottom = this.getCurrentDrumPosition("bottom");
3213
+ if (beforeTop !== afterTop) {
3214
+ this.calculateAndUpdateGlyphPositions("top", beforeTop, afterTop);
3215
+ }
3216
+ if (beforeMiddle !== afterMiddle) {
3217
+ this.calculateAndUpdateGlyphPositions("middle", beforeMiddle, afterMiddle);
3218
+ }
3219
+ if (beforeBottom !== afterBottom) {
3220
+ this.calculateAndUpdateGlyphPositions("bottom", beforeBottom, afterBottom);
3221
+ }
3222
+ return result;
3223
+ }
3224
+ /**
3225
+ * Gets the current position of a specific drum level.
3226
+ * @param level - The drum level to get position for
3227
+ * @returns The current position of the specified drum level
3228
+ */
3229
+ getCurrentDrumPosition(level) {
3230
+ return this.towerCommands.getCurrentDrumPosition(level);
3231
+ }
3232
+ /**
3233
+ * Sets the initial glyph positions from calibration.
3234
+ * Called automatically when calibration completes.
3235
+ */
3236
+ setGlyphPositionsFromCalibration() {
3237
+ for (const glyphKey in GLYPHS) {
3238
+ const glyph = glyphKey;
3239
+ this.glyphPositions[glyph] = GLYPHS[glyph].side;
3240
+ }
3241
+ }
3242
+ /**
3243
+ * Gets the current position of a specific glyph.
3244
+ * @param glyph - The glyph to get position for
3245
+ * @returns The current position of the glyph, or null if not calibrated
3246
+ */
3247
+ getGlyphPosition(glyph) {
3248
+ return this.glyphPositions[glyph];
3249
+ }
3250
+ /**
3251
+ * Gets all current glyph positions.
3252
+ * @returns Object mapping each glyph to its current position (or null if not calibrated)
3253
+ */
3254
+ getAllGlyphPositions() {
3255
+ return { ...this.glyphPositions };
3256
+ }
3257
+ /**
3258
+ * Gets all glyphs currently facing a specific direction.
3259
+ * @param direction - The direction to check for (north, east, south, west)
3260
+ * @returns Array of glyph names that are currently facing the specified direction
3261
+ */
3262
+ getGlyphsFacingDirection(direction) {
3263
+ const glyphsFacing = [];
3264
+ for (const glyphKey in this.glyphPositions) {
3265
+ const glyph = glyphKey;
3266
+ const position = this.glyphPositions[glyph];
3267
+ if (position && position.toLowerCase() === direction.toLowerCase()) {
3268
+ glyphsFacing.push(glyph);
3269
+ }
3270
+ }
3271
+ return glyphsFacing;
3272
+ }
3273
+ /**
3274
+ * Updates glyph positions after a drum rotation.
3275
+ * @param level - The drum level that was rotated
3276
+ * @param rotationSteps - Number of steps rotated (1 = 90 degrees clockwise)
3277
+ */
3278
+ updateGlyphPositionsAfterRotation(level, rotationSteps) {
3279
+ const sides = ["north", "east", "south", "west"];
3280
+ for (const glyphKey in GLYPHS) {
3281
+ const glyph = glyphKey;
3282
+ const glyphData = GLYPHS[glyph];
3283
+ if (glyphData.level === level && this.glyphPositions[glyph] !== null) {
3284
+ const currentPosition = this.glyphPositions[glyph];
3285
+ const currentIndex = sides.indexOf(currentPosition);
3286
+ const newIndex = (currentIndex + rotationSteps) % sides.length;
3287
+ this.glyphPositions[glyph] = sides[newIndex];
3288
+ }
3289
+ }
3290
+ }
3291
+ /**
3292
+ * Calculates rotation steps and updates glyph positions for a specific level.
3293
+ * @param level - The drum level that was rotated
3294
+ * @param oldPosition - The position before rotation
3295
+ * @param newPosition - The position after rotation
3296
+ */
3297
+ calculateAndUpdateGlyphPositions(level, oldPosition, newPosition) {
3298
+ const sides = ["north", "east", "south", "west"];
3299
+ const oldIndex = sides.indexOf(oldPosition);
3300
+ const newIndex = sides.indexOf(newPosition);
3301
+ let rotationSteps = newIndex - oldIndex;
3302
+ if (rotationSteps < 0) {
3303
+ rotationSteps += TOWER_SIDES_COUNT;
3304
+ }
3305
+ if (rotationSteps > 0) {
3306
+ this.updateGlyphPositionsAfterRotation(level, rotationSteps);
3307
+ }
3308
+ }
3309
+ /**
3310
+ * Updates glyph positions for a specific level rotation.
3311
+ * @param level - The drum level that was rotated
3312
+ * @param newPosition - The new position the drum was rotated to
3313
+ * @deprecated Use calculateAndUpdateGlyphPositions instead
3314
+ */
3315
+ updateGlyphPositionsForRotation(level, newPosition) {
3316
+ const currentPosition = this.getCurrentDrumPosition(level);
3317
+ const sides = ["north", "east", "south", "west"];
3318
+ const currentIndex = sides.indexOf(currentPosition);
3319
+ const newIndex = sides.indexOf(newPosition);
3320
+ let rotationSteps = newIndex - currentIndex;
3321
+ if (rotationSteps < 0) {
3322
+ rotationSteps += TOWER_SIDES_COUNT;
3323
+ }
3324
+ this.updateGlyphPositionsAfterRotation(level, rotationSteps);
3325
+ }
3326
+ /**
3327
+ * Checks if a specific seal is broken.
3328
+ * @param seal - The seal identifier to check
3329
+ * @returns True if the seal is broken, false otherwise
3330
+ */
3331
+ isSealBroken(seal) {
3332
+ const sealKey = `${seal.level}-${seal.side}`;
3333
+ return this.brokenSeals.has(sealKey);
3334
+ }
3335
+ /**
3336
+ * Gets a list of all broken seals.
3337
+ * @returns Array of SealIdentifier objects representing all broken seals
3338
+ */
3339
+ getBrokenSeals() {
3340
+ return Array.from(this.brokenSeals).map((sealKey) => {
3341
+ const [level, side] = sealKey.split("-");
3342
+ return { level, side };
3343
+ });
3344
+ }
3345
+ /**
3346
+ * Resets the broken seals tracking (clears all broken seals).
3347
+ */
3348
+ resetBrokenSeals() {
3349
+ this.brokenSeals.clear();
3350
+ }
3351
+ /**
3352
+ * Gets a random unbroken seal that can be passed to breakSeal().
3353
+ * @returns A random SealIdentifier that is not currently broken, or null if all seals are broken
3354
+ */
3355
+ getRandomUnbrokenSeal() {
3356
+ const allSeals = [];
3357
+ const levels = ["top", "middle", "bottom"];
3358
+ const sides = ["north", "east", "south", "west"];
3359
+ for (const level of levels) {
3360
+ for (const side of sides) {
3361
+ allSeals.push({ level, side });
3362
+ }
3363
+ }
3364
+ const unbrokenSeals = allSeals.filter((seal) => !this.isSealBroken(seal));
3365
+ if (unbrokenSeals.length === 0) {
3366
+ return null;
3367
+ }
3368
+ const randomIndex = Math.floor(Math.random() * unbrokenSeals.length);
3369
+ return unbrokenSeals[randomIndex];
3370
+ }
3371
+ //#region bluetooth
3372
+ /**
3373
+ * Establishes a Bluetooth connection to the Dark Tower device.
3374
+ * Initializes GATT services, characteristics, and starts connection monitoring.
3375
+ * @returns {Promise<void>} Promise that resolves when connection is established
3376
+ */
3377
+ async connect() {
3378
+ await this.bleConnection.connect();
3379
+ }
3380
+ /**
3381
+ * Disconnects from the tower device and cleans up resources.
3382
+ * @returns {Promise<void>} Promise that resolves when disconnection is complete
3383
+ */
3384
+ async disconnect() {
3385
+ await this.bleConnection.disconnect();
3386
+ }
3387
+ //#endregion
3388
+ //#region utility
3389
+ /**
3390
+ * Configure logger outputs for this UltimateDarkTower instance
3391
+ * @param {LogOutput[]} outputs - Array of log outputs to use (e.g., ConsoleOutput, DOMOutput)
3392
+ */
3393
+ setLoggerOutputs(outputs) {
3394
+ this.logger.outputs = [];
3395
+ outputs.forEach((output) => this.logger.addOutput(output));
3396
+ }
3397
+ /**
3398
+ * Sends a command packet to the tower via Bluetooth with error handling and retry logic.
3399
+ * @param {Uint8Array} command - The command packet to send to the tower
3400
+ * @returns {Promise<void>} Promise that resolves when command is sent successfully
3401
+ */
3402
+ async sendTowerCommand(command) {
3403
+ return await this.towerCommands.sendTowerCommand(command);
3404
+ }
3405
+ /**
3406
+ * Converts a command packet to a hex string representation for debugging.
3407
+ * @param {Uint8Array} command - Command packet to convert
3408
+ * @returns {string} Hex string representation of the command packet
3409
+ */
3410
+ commandToPacketString(command) {
3411
+ return commandToPacketString(command);
3412
+ }
3413
+ /**
3414
+ * Converts battery voltage in millivolts to percentage.
3415
+ * @param {number} mv - Battery voltage in millivolts
3416
+ * @returns {string} Battery percentage as formatted string (e.g., "75%")
3417
+ */
3418
+ milliVoltsToPercentage(mv) {
3419
+ return milliVoltsToPercentage(mv);
3420
+ }
3421
+ //#endregion
3422
+ //#region Connection Management
3423
+ /**
3424
+ * Enable or disable connection monitoring
3425
+ * @param {boolean} enabled - Whether to enable connection monitoring
3426
+ */
3427
+ setConnectionMonitoring(enabled) {
3428
+ this.bleConnection.setConnectionMonitoring(enabled);
3429
+ }
3430
+ /**
3431
+ * Configure connection monitoring parameters
3432
+ * @param {number} [frequency=2000] - How often to check connection (milliseconds)
3433
+ * @param {number} [timeout=30000] - How long to wait for responses before considering connection lost (milliseconds)
3434
+ */
3435
+ configureConnectionMonitoring(frequency = DEFAULT_CONNECTION_MONITORING_FREQUENCY, timeout = DEFAULT_CONNECTION_MONITORING_TIMEOUT) {
3436
+ this.bleConnection.configureConnectionMonitoring(frequency, timeout);
3437
+ }
3438
+ /**
3439
+ * Configure battery heartbeat monitoring parameters
3440
+ * Tower sends battery status every ~200ms, so this is the most reliable disconnect indicator
3441
+ * @param {boolean} [enabled=true] - Whether to enable battery heartbeat monitoring
3442
+ * @param {number} [timeout=3000] - How long to wait for battery status before considering disconnected (milliseconds)
3443
+ * @param {boolean} [verifyConnection=true] - Whether to verify connection status before triggering disconnection on heartbeat timeout
3444
+ */
3445
+ configureBatteryHeartbeatMonitoring(enabled = true, timeout = DEFAULT_BATTERY_HEARTBEAT_TIMEOUT, verifyConnection = true) {
3446
+ this.bleConnection.configureBatteryHeartbeatMonitoring(enabled, timeout, verifyConnection);
3447
+ }
3448
+ /**
3449
+ * Check if the tower is currently connected
3450
+ * @returns {Promise<boolean>} True if connected and responsive
3451
+ */
3452
+ async isConnectedAndResponsive() {
3453
+ return await this.bleConnection.isConnectedAndResponsive();
3454
+ }
3455
+ /**
3456
+ * Get detailed connection status including heartbeat information
3457
+ * @returns {Object} Object with connection details
3458
+ */
3459
+ getConnectionStatus() {
3460
+ return this.bleConnection.getConnectionStatus();
3461
+ }
3462
+ /**
3463
+ * Get device information read from the tower's Device Information Service (DIS)
3464
+ * @returns {DeviceInformation} Object with manufacturer, model, serial, firmware, etc.
3465
+ */
3466
+ getDeviceInformation() {
3467
+ return this.bleConnection.getDeviceInformation();
3468
+ }
3469
+ //#endregion
3470
+ //#region cleanup
3471
+ /**
3472
+ * Clean up resources and disconnect properly
3473
+ * @returns {Promise<void>} Promise that resolves when cleanup is complete
3474
+ */
3475
+ async cleanup() {
3476
+ this.logger.info("Cleaning up UltimateDarkTower instance", "[UDT]");
3477
+ this.towerCommands.clearQueue();
3478
+ await this.bleConnection.cleanup();
3479
+ }
3480
+ //#endregion
3481
+ };
3482
+ var UltimateDarkTower_default = UltimateDarkTower;
3483
+
3484
+ // src/index.ts
3485
+ init_udtConstants();
3486
+ init_udtBluetoothAdapter();
3487
+ init_udtTowerState();
3488
+ var src_default = UltimateDarkTower_default;
3489
+ export {
3490
+ AUDIO_COMMAND_POS,
3491
+ BATTERY_STATUS_FREQUENCY,
3492
+ BluetoothAdapterFactory,
3493
+ BluetoothConnectionError,
3494
+ BluetoothDeviceNotFoundError,
3495
+ BluetoothError,
3496
+ BluetoothPlatform,
3497
+ BluetoothTimeoutError,
3498
+ BluetoothUserCancelledError,
3499
+ BufferOutput,
3500
+ ConsoleOutput,
3501
+ DEFAULT_BATTERY_HEARTBEAT_TIMEOUT,
3502
+ DEFAULT_CONNECTION_MONITORING_FREQUENCY,
3503
+ DEFAULT_CONNECTION_MONITORING_TIMEOUT,
3504
+ DEFAULT_RETRY_SEND_COMMAND_MAX,
3505
+ DIS_FIRMWARE_REVISION_UUID,
3506
+ DIS_HARDWARE_REVISION_UUID,
3507
+ DIS_IEEE_REGULATORY_UUID,
3508
+ DIS_MANUFACTURER_NAME_UUID,
3509
+ DIS_MODEL_NUMBER_UUID,
3510
+ DIS_PNP_ID_UUID,
3511
+ DIS_SERIAL_NUMBER_UUID,
3512
+ DIS_SERVICE_UUID,
3513
+ DIS_SOFTWARE_REVISION_UUID,
3514
+ DIS_SYSTEM_ID_UUID,
3515
+ DOMOutput,
3516
+ DRUM_PACKETS,
3517
+ GLYPHS,
3518
+ LAYER_TO_POSITION,
3519
+ LEDGE_BASE_LIGHT_POSITIONS,
3520
+ LED_CHANNEL_LOOKUP,
3521
+ LIGHT_EFFECTS,
3522
+ LIGHT_INDEX_TO_DIRECTION,
3523
+ Logger,
3524
+ RING_LIGHT_POSITIONS,
3525
+ SKULL_DROP_COUNT_POS,
3526
+ STATE_DATA_LENGTH,
3527
+ TC,
3528
+ TOWER_AUDIO_LIBRARY,
3529
+ TOWER_COMMANDS,
3530
+ TOWER_COMMAND_HEADER_SIZE,
3531
+ TOWER_COMMAND_PACKET_SIZE,
3532
+ TOWER_COMMAND_TYPE_TOWER_STATE,
3533
+ TOWER_DEVICE_NAME,
3534
+ TOWER_LAYERS,
3535
+ TOWER_LIGHT_SEQUENCES,
3536
+ TOWER_MESSAGES,
3537
+ TOWER_SIDES_COUNT,
3538
+ TOWER_STATE_DATA_OFFSET,
3539
+ TOWER_STATE_DATA_SIZE,
3540
+ TOWER_STATE_RESPONSE_MIN_LENGTH,
3541
+ UART_RX_CHARACTERISTIC_UUID,
3542
+ UART_SERVICE_UUID,
3543
+ UART_TX_CHARACTERISTIC_UUID,
3544
+ UltimateDarkTower_default as UltimateDarkTower,
3545
+ VOLTAGE_LEVELS,
3546
+ VOLUME_DESCRIPTIONS,
3547
+ VOLUME_ICONS,
3548
+ createDefaultTowerState,
3549
+ src_default as default,
3550
+ drumPositionCmds,
3551
+ isCalibrated,
3552
+ logger,
3553
+ milliVoltsToPercentage,
3554
+ milliVoltsToPercentageNumber,
3555
+ parseDifferentialReadings,
3556
+ rtdt_pack_state,
3557
+ rtdt_unpack_state
3558
+ };