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.
- package/CHANGELOG.md +86 -0
- package/README.md +20 -0
- package/dist/esm/index.mjs +3558 -0
- package/dist/src/UltimateDarkTower.d.ts +1 -15
- package/dist/src/UltimateDarkTower.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +8 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/udtTowerCommands.js +0 -1
- package/dist/src/udtTowerCommands.js.map +1 -1
- package/dist/src/udtTowerResponse.d.ts +16 -1
- package/dist/src/udtTowerResponse.js.map +1 -1
- package/package.json +8 -5
|
@@ -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
|
+
};
|