@onekeyfe/hwk-desktop-noble-ble 1.2.3-alpha.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/dist/chunk-RNZYQRXV.mjs +39 -0
- package/dist/chunk-RNZYQRXV.mjs.map +1 -0
- package/dist/constants.d.mts +36 -0
- package/dist/constants.d.ts +36 -0
- package/dist/constants.js +59 -0
- package/dist/constants.js.map +1 -0
- package/dist/constants.mjs +15 -0
- package/dist/constants.mjs.map +1 -0
- package/dist/index.d.mts +357 -0
- package/dist/index.d.ts +357 -0
- package/dist/index.js +948 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +900 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +82 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
import {
|
|
2
|
+
THIRD_PARTY_BLE_CHANNELS,
|
|
3
|
+
THIRD_PARTY_BLE_DEVICE_TTL_MS,
|
|
4
|
+
THIRD_PARTY_BLE_POWER_ON_TIMEOUT_MS,
|
|
5
|
+
THIRD_PARTY_BLE_SCAN_DURATION_MS,
|
|
6
|
+
THIRD_PARTY_BLE_SCAN_IDLE_STOP_MS,
|
|
7
|
+
__require
|
|
8
|
+
} from "./chunk-RNZYQRXV.mjs";
|
|
9
|
+
|
|
10
|
+
// src/debugLog.ts
|
|
11
|
+
var REDACTED_KEYS = /* @__PURE__ */ new Set([
|
|
12
|
+
"credential",
|
|
13
|
+
"credentials",
|
|
14
|
+
"privateKey",
|
|
15
|
+
"publicKey",
|
|
16
|
+
"hostKey",
|
|
17
|
+
"pin",
|
|
18
|
+
"passphrase",
|
|
19
|
+
"hexData",
|
|
20
|
+
"payload",
|
|
21
|
+
"bytes"
|
|
22
|
+
]);
|
|
23
|
+
function redactBleDebugLogData(data) {
|
|
24
|
+
if (!data) return void 0;
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const [key, value] of Object.entries(data)) {
|
|
27
|
+
out[key] = REDACTED_KEYS.has(key) ? "[redacted]" : value;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/NobleBleHandler.ts
|
|
33
|
+
var DEFAULT_NOBLE_FACTORY = () => {
|
|
34
|
+
const noble = __require("@stoprocent/noble");
|
|
35
|
+
return noble;
|
|
36
|
+
};
|
|
37
|
+
var normalizeUuid = (uuid) => uuid.replace(/-/g, "").toLowerCase();
|
|
38
|
+
var matchesPeripheral = (p, match) => {
|
|
39
|
+
if (!match) return false;
|
|
40
|
+
const adv = p.advertisement ?? {};
|
|
41
|
+
const advertised = adv.serviceUuids ?? [];
|
|
42
|
+
if (match.serviceUuids?.length && advertised.some(
|
|
43
|
+
(uuid) => match.serviceUuids?.some((wanted) => normalizeUuid(wanted) === normalizeUuid(uuid))
|
|
44
|
+
)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const name = adv.localName;
|
|
48
|
+
if (!match.namePatterns?.length || !name) return false;
|
|
49
|
+
return match.namePatterns.every((pattern) => new RegExp(pattern, "i").test(name));
|
|
50
|
+
};
|
|
51
|
+
var peripheralToInfo = (p) => {
|
|
52
|
+
const adv = p.advertisement ?? {};
|
|
53
|
+
return {
|
|
54
|
+
id: p.id,
|
|
55
|
+
name: adv.localName,
|
|
56
|
+
localName: adv.localName,
|
|
57
|
+
rssi: p.rssi,
|
|
58
|
+
isConnectable: p.connectable ?? null,
|
|
59
|
+
advertisedServiceUuids: adv.serviceUuids,
|
|
60
|
+
serviceSolicitationUuids: adv.serviceSolicitationUuids,
|
|
61
|
+
txPowerLevel: adv.txPowerLevel,
|
|
62
|
+
manufacturerDataHex: adv.manufacturerData ? Buffer.from(adv.manufacturerData).toString("hex") : void 0,
|
|
63
|
+
serviceData: adv.serviceData?.map((entry) => ({
|
|
64
|
+
uuid: entry.uuid,
|
|
65
|
+
dataHex: Buffer.from(entry.data).toString("hex")
|
|
66
|
+
})),
|
|
67
|
+
address: p.address,
|
|
68
|
+
addressType: p.addressType,
|
|
69
|
+
state: p.state
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
|
+
var BLE_CONNECT_SETTLE_MS = 300;
|
|
74
|
+
var BLE_CONNECT_TIMEOUT_MS = 31e3;
|
|
75
|
+
var BLE_DISCONNECT_TIMEOUT_MS = 2e3;
|
|
76
|
+
var NOBLE_RECOVER_COOLDOWN_MS = 1e4;
|
|
77
|
+
var NobleBleHandler = class {
|
|
78
|
+
constructor(options = {}) {
|
|
79
|
+
this._discovered = /* @__PURE__ */ new Map();
|
|
80
|
+
// id -> last advertisement time (snapshot TTL).
|
|
81
|
+
this._lastSeen = /* @__PURE__ */ new Map();
|
|
82
|
+
this._connected = /* @__PURE__ */ new Map();
|
|
83
|
+
this._scanning = false;
|
|
84
|
+
this._initialized = false;
|
|
85
|
+
this._disposed = false;
|
|
86
|
+
this._nobleInstances = /* @__PURE__ */ new Set();
|
|
87
|
+
this._pendingCancellations = /* @__PURE__ */ new Set();
|
|
88
|
+
this._connectAttempts = /* @__PURE__ */ new Set();
|
|
89
|
+
this._nativeReleased = false;
|
|
90
|
+
this._factory = options.nobleFactory ?? DEFAULT_NOBLE_FACTORY;
|
|
91
|
+
this._connectTimeoutMs = options.connectTimeoutMs ?? BLE_CONNECT_TIMEOUT_MS;
|
|
92
|
+
this._logger = options.logger;
|
|
93
|
+
}
|
|
94
|
+
setNotificationListener(handler) {
|
|
95
|
+
this._onNotification = handler;
|
|
96
|
+
}
|
|
97
|
+
setDisconnectedListener(handler) {
|
|
98
|
+
this._onDeviceDisconnected = handler;
|
|
99
|
+
}
|
|
100
|
+
async init() {
|
|
101
|
+
this._assertActive();
|
|
102
|
+
if (this._initialized) return;
|
|
103
|
+
if (this._initPromise) return this._initPromise;
|
|
104
|
+
this._initPromise = (async () => {
|
|
105
|
+
this._noble ?? (this._noble = this._factory());
|
|
106
|
+
this._nobleInstances.add(this._noble);
|
|
107
|
+
this._discoverHandler ?? (this._discoverHandler = (peripheral) => {
|
|
108
|
+
this._discovered.set(peripheral.id, peripheral);
|
|
109
|
+
this._lastSeen.set(peripheral.id, Date.now());
|
|
110
|
+
});
|
|
111
|
+
this._noble.removeListener("discover", this._discoverHandler);
|
|
112
|
+
this._noble.on("discover", this._discoverHandler);
|
|
113
|
+
await this._waitForPoweredOn(THIRD_PARTY_BLE_POWER_ON_TIMEOUT_MS);
|
|
114
|
+
this._assertActive();
|
|
115
|
+
this._initialized = true;
|
|
116
|
+
})();
|
|
117
|
+
try {
|
|
118
|
+
await this._initPromise;
|
|
119
|
+
} finally {
|
|
120
|
+
this._initPromise = void 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async checkAvailability() {
|
|
124
|
+
this._assertActive();
|
|
125
|
+
if (!this._noble) {
|
|
126
|
+
try {
|
|
127
|
+
this._noble = this._factory();
|
|
128
|
+
this._nobleInstances.add(this._noble);
|
|
129
|
+
} catch {
|
|
130
|
+
return { available: false, state: "unsupported", initialized: false };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const state = this._noble?.state ?? "unknown";
|
|
134
|
+
return {
|
|
135
|
+
available: state === "poweredOn",
|
|
136
|
+
state,
|
|
137
|
+
initialized: this._initialized
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Lazy-start a continuous scan and return the current snapshot immediately.
|
|
142
|
+
*
|
|
143
|
+
* Scans UNFILTERED and applies the caller's match criteria in `_snapshot()` instead. A
|
|
144
|
+
* service-UUID filter cannot be used here: noble's Windows backend applies it
|
|
145
|
+
* per RECEIVED PACKET (`BLEManager::OnScanResult`, lib/win/src/ble_manager.cc),
|
|
146
|
+
* and a Safe 7's ADV packet carries only its name — the service UUID lives in
|
|
147
|
+
* the scan response, which arrives as a separate, irregularly-timed event. So
|
|
148
|
+
* a filtered scan drops every ADV packet and the device appears to be
|
|
149
|
+
* undiscoverable for minutes at a time while it is plainly on air. (OneKey's
|
|
150
|
+
* own devices do advertise their service UUID, which is why the same filter is
|
|
151
|
+
* safe in `hd-transport-electron` and was copied here by mistake.)
|
|
152
|
+
*/
|
|
153
|
+
async scan(options) {
|
|
154
|
+
await this.init();
|
|
155
|
+
if (!this._scanning) {
|
|
156
|
+
this._scanning = true;
|
|
157
|
+
try {
|
|
158
|
+
await this._requireNoble().startScanningAsync([], true);
|
|
159
|
+
this._log("warn", "scan.start", {
|
|
160
|
+
ignoredServiceUuids: options?.serviceUuids,
|
|
161
|
+
allowDuplicates: true
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this._scanning = false;
|
|
165
|
+
this._log("warn", "scan.start.error", { error: String(error) });
|
|
166
|
+
await this._recoverNobleIfStuck(String(error));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
this._assertActive();
|
|
170
|
+
this._armIdleStop();
|
|
171
|
+
const devices = this._snapshot(options);
|
|
172
|
+
if (devices.length === 0) {
|
|
173
|
+
this._log("warn", "scan.empty", {
|
|
174
|
+
raw: this._discovered.size,
|
|
175
|
+
kept: 0,
|
|
176
|
+
named: [...this._discovered.values()].filter((p) => p.advertisement?.localName).length
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return devices;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Current in-range devices for the asking vendor, dropping any that aged past
|
|
183
|
+
* the liveness TTL. The match test replaces the service-UUID scan filter we cannot use
|
|
184
|
+
* (see `scan`): it matches the name from the ADV packet, or the service UUID
|
|
185
|
+
* once a scan response has merged into the same peripheral.
|
|
186
|
+
*
|
|
187
|
+
* Note the TTL only prunes what the CALLER sees. `_discovered` is a cache, not
|
|
188
|
+
* the source of truth for reachability — a device missing from here can still
|
|
189
|
+
* be connected to by id (`_directConnect`).
|
|
190
|
+
*/
|
|
191
|
+
_snapshot(options) {
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
const result = [];
|
|
194
|
+
for (const [id, peripheral] of this._discovered) {
|
|
195
|
+
if (now - (this._lastSeen.get(id) ?? 0) > THIRD_PARTY_BLE_DEVICE_TTL_MS) {
|
|
196
|
+
this._discovered.delete(id);
|
|
197
|
+
this._lastSeen.delete(id);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const match = options?.match ?? { serviceUuids: options?.serviceUuids };
|
|
201
|
+
if (!matchesPeripheral(peripheral, match)) continue;
|
|
202
|
+
result.push(peripheralToInfo(peripheral));
|
|
203
|
+
}
|
|
204
|
+
for (const [id, entry] of this._connected) {
|
|
205
|
+
if (entry.vendor !== options?.vendor) continue;
|
|
206
|
+
if (result.some((info) => info.id === id)) continue;
|
|
207
|
+
result.push(peripheralToInfo(entry.peripheral));
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
/** A noble instance with its own bindings, so a stuck one can be replaced. */
|
|
212
|
+
_createFreshNoble() {
|
|
213
|
+
const candidate = this._factory();
|
|
214
|
+
return typeof candidate.withBindings === "function" ? candidate.withBindings() : candidate;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Rebuild noble when its adapter state is stuck. Re-enumerating the Windows
|
|
218
|
+
* BLE stack (pairing, or removing the device from OS settings) can catch
|
|
219
|
+
* noble's RadioWatcher mid-churn: it latches `unsupported` and never
|
|
220
|
+
* re-evaluates, so every later scan fails until the process restarts. Fresh
|
|
221
|
+
* bindings restart that watcher, which is the in-process equivalent of the
|
|
222
|
+
* app restart that is otherwise the only cure.
|
|
223
|
+
*/
|
|
224
|
+
async _recoverNobleIfStuck(reason) {
|
|
225
|
+
if (this._disposed) return;
|
|
226
|
+
const state = this._noble?.state;
|
|
227
|
+
if (state === "poweredOn" || !this._initialized) return;
|
|
228
|
+
if (this._connected.size > 0) {
|
|
229
|
+
this._log("warn", "noble.recover.skip", {
|
|
230
|
+
reason,
|
|
231
|
+
state,
|
|
232
|
+
connected: this._connected.size
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
if (this._lastNobleRecoverAt && now - this._lastNobleRecoverAt < NOBLE_RECOVER_COOLDOWN_MS) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
this._lastNobleRecoverAt = now;
|
|
241
|
+
this._log("warn", "noble.recover.start", { reason, state });
|
|
242
|
+
try {
|
|
243
|
+
const previous = this._noble;
|
|
244
|
+
if (previous && this._discoverHandler) {
|
|
245
|
+
previous.removeListener("discover", this._discoverHandler);
|
|
246
|
+
}
|
|
247
|
+
const fresh = this._createFreshNoble();
|
|
248
|
+
this._noble = fresh;
|
|
249
|
+
this._nobleInstances.add(fresh);
|
|
250
|
+
if (this._discoverHandler) {
|
|
251
|
+
fresh.on("discover", this._discoverHandler);
|
|
252
|
+
}
|
|
253
|
+
this._scanning = false;
|
|
254
|
+
this._discovered.clear();
|
|
255
|
+
this._lastSeen.clear();
|
|
256
|
+
await this._waitForPoweredOn(THIRD_PARTY_BLE_POWER_ON_TIMEOUT_MS);
|
|
257
|
+
this._log("warn", "noble.recover.done", { state: this._noble?.state });
|
|
258
|
+
} catch (error) {
|
|
259
|
+
this._log("warn", "noble.recover.error", { error: String(error) });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
_armIdleStop() {
|
|
263
|
+
if (this._disposed) return;
|
|
264
|
+
this._clearIdleStop();
|
|
265
|
+
this._idleStopTimer = setTimeout(() => {
|
|
266
|
+
void this._stopContinuousScan();
|
|
267
|
+
}, THIRD_PARTY_BLE_SCAN_IDLE_STOP_MS);
|
|
268
|
+
}
|
|
269
|
+
_clearIdleStop() {
|
|
270
|
+
if (this._idleStopTimer) {
|
|
271
|
+
clearTimeout(this._idleStopTimer);
|
|
272
|
+
this._idleStopTimer = void 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Stop scanning but keep the discovered cache (used before connect). */
|
|
276
|
+
async _pauseScan() {
|
|
277
|
+
this._clearIdleStop();
|
|
278
|
+
if (!this._scanning) return;
|
|
279
|
+
this._scanning = false;
|
|
280
|
+
await this._noble?.stopScanningAsync().catch(() => void 0);
|
|
281
|
+
}
|
|
282
|
+
/** Stop scanning and forget discovered devices (idle timeout / teardown). */
|
|
283
|
+
async _stopContinuousScan() {
|
|
284
|
+
await this._pauseScan();
|
|
285
|
+
this._discovered.clear();
|
|
286
|
+
this._lastSeen.clear();
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Stops the process-wide scan no matter which vendor asked. Scan *results*
|
|
290
|
+
* are filtered per vendor, but the radio is not: one vendor's stopScan ends
|
|
291
|
+
* the other's discovery too. That is safe only because the adapter job queue
|
|
292
|
+
* serializes discovery across vendors, so two are never scanning at once.
|
|
293
|
+
* Anything that breaks that — a background presence probe, say — needs
|
|
294
|
+
* per-vendor refcounting here first.
|
|
295
|
+
*/
|
|
296
|
+
async stopScan() {
|
|
297
|
+
await this._stopContinuousScan();
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Look up a previously-scanned device by id (no extra BLE traffic).
|
|
301
|
+
* Returns null if the device hasn't been seen by a recent scan.
|
|
302
|
+
*/
|
|
303
|
+
getDevice(id) {
|
|
304
|
+
const p = this._discovered.get(id);
|
|
305
|
+
if (!p) return null;
|
|
306
|
+
return peripheralToInfo(p);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Read the current RSSI (in dBm) for a connected peripheral. Requires
|
|
310
|
+
* the device to be connected — noble can't read RSSI off a scan-only
|
|
311
|
+
* peripheral. Falls back to the cached scan-time rssi when the noble
|
|
312
|
+
* peripheral doesn't expose updateRssiAsync.
|
|
313
|
+
*/
|
|
314
|
+
async readRssi(id) {
|
|
315
|
+
const entry = this._requireEntry(id);
|
|
316
|
+
if (entry.peripheral.updateRssiAsync) {
|
|
317
|
+
return entry.peripheral.updateRssiAsync();
|
|
318
|
+
}
|
|
319
|
+
return entry.peripheral.rssi;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Abort the in-flight pairing flow: abandon a connect that is still running,
|
|
323
|
+
* stop scanning, disconnect every peripheral the host currently has open.
|
|
324
|
+
* Caller is responsible for surfacing the cancellation to the upper UI layer.
|
|
325
|
+
*
|
|
326
|
+
* Abandoning the connect is what actually ends the flow. Pairing happens
|
|
327
|
+
* inside connectAsync and the entry only reaches _connected after service
|
|
328
|
+
* discovery, so the loop below never sees the device being paired — without
|
|
329
|
+
* the abandon the caller waits out the full connect timeout, which is sized
|
|
330
|
+
* to the OS pairing window and so feels like a hang.
|
|
331
|
+
*/
|
|
332
|
+
async cancelPairing() {
|
|
333
|
+
const attempt = this._activeConnect;
|
|
334
|
+
if (attempt) {
|
|
335
|
+
this._activeConnect = void 0;
|
|
336
|
+
attempt.abandon(new Error(`connect cancelled: ${attempt.id}`));
|
|
337
|
+
}
|
|
338
|
+
await this.stopScan();
|
|
339
|
+
for (const id of Array.from(this._connected.keys())) {
|
|
340
|
+
await this.disconnect(id).catch(() => void 0);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Scan for a specific peripheral id and resolve THE MOMENT it's discovered,
|
|
345
|
+
* stopping the scan immediately (don't wait out the full window). The fast
|
|
346
|
+
* reconnect path for a stored connectId when the device IS advertising.
|
|
347
|
+
*
|
|
348
|
+
* This used to be the only reconnect path, on two assumptions that are both
|
|
349
|
+
* false: that noble cannot connect by id without a scan (it can — see
|
|
350
|
+
* `_directConnect`), and that "the device advertises continuously" (a bonded
|
|
351
|
+
* Safe 7 does not — it holds the link and goes silent). Callers must fall
|
|
352
|
+
* back to `_directConnect` when this returns undefined.
|
|
353
|
+
*/
|
|
354
|
+
async _scanUntilFound(id, timeoutMs) {
|
|
355
|
+
await this.init();
|
|
356
|
+
const existing = this._discovered.get(id);
|
|
357
|
+
if (existing) return existing;
|
|
358
|
+
const noble = this._requireNoble();
|
|
359
|
+
return new Promise((resolve) => {
|
|
360
|
+
let done = false;
|
|
361
|
+
const finish = (p) => {
|
|
362
|
+
if (done) return;
|
|
363
|
+
done = true;
|
|
364
|
+
this._pendingCancellations.delete(cancel);
|
|
365
|
+
clearTimeout(timer);
|
|
366
|
+
noble.removeListener("discover", onDiscover);
|
|
367
|
+
void noble.stopScanningAsync().catch(() => void 0);
|
|
368
|
+
resolve(p);
|
|
369
|
+
};
|
|
370
|
+
const onDiscover = (peripheral) => {
|
|
371
|
+
this._discovered.set(peripheral.id, peripheral);
|
|
372
|
+
if (peripheral.id === id) finish(peripheral);
|
|
373
|
+
};
|
|
374
|
+
const timer = setTimeout(() => finish(this._discovered.get(id)), timeoutMs);
|
|
375
|
+
const cancel = () => finish();
|
|
376
|
+
this._pendingCancellations.add(cancel);
|
|
377
|
+
noble.on("discover", onDiscover);
|
|
378
|
+
void noble.startScanningAsync([], false).catch(() => finish());
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Connect by id with no scan and no advertisement.
|
|
383
|
+
*
|
|
384
|
+
* This is the ONLY path that reaches a device which is connected but silent.
|
|
385
|
+
* A linked peripheral stops advertising while it HOLDS A LINK (standard BLE; a Safe 7's
|
|
386
|
+
* screen says "wait connection") — field-verified: bonding/THP handshake
|
|
387
|
+
* alone does NOT silence it, holding the connection does. So while a link is
|
|
388
|
+
* up, no amount of scanning will rediscover it — `_scanUntilFound` alone
|
|
389
|
+
* dead-ends with "device not found" on a device that is sitting right there,
|
|
390
|
+
* connected and reachable.
|
|
391
|
+
*
|
|
392
|
+
* noble supports this: `noble.connectAsync(id)` needs no prior `discover`,
|
|
393
|
+
* because both native backends materialize the peripheral themselves (Windows
|
|
394
|
+
* synthesizes one for an unknown address, macOS retrieves it by identifier)
|
|
395
|
+
* and then emit a `discover`, which our own handler turns back into a
|
|
396
|
+
* `_discovered` entry. OneKey's own noble handler calls this "direct
|
|
397
|
+
* connection mode"; Trezor Suite's equivalent is asking the adapter for its
|
|
398
|
+
* peripheral list instead of keeping a cache.
|
|
399
|
+
*
|
|
400
|
+
* Returns undefined (not throw) so the caller reports the normal
|
|
401
|
+
* "device not found" rather than a confusing noble-internal error.
|
|
402
|
+
*/
|
|
403
|
+
async _directConnect(id) {
|
|
404
|
+
const noble = this._requireNoble();
|
|
405
|
+
if (typeof noble.connectAsync !== "function") {
|
|
406
|
+
this._log("warn", "connect.direct.unavailable", { id });
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
this._log("warn", "connect.direct.start", { id });
|
|
410
|
+
const startedAt = Date.now();
|
|
411
|
+
try {
|
|
412
|
+
const peripheral = await noble.connectAsync(id);
|
|
413
|
+
const resolved = peripheral ?? this._discovered.get(id);
|
|
414
|
+
this._log("warn", "connect.direct.done", {
|
|
415
|
+
id,
|
|
416
|
+
elapsedMs: Date.now() - startedAt,
|
|
417
|
+
found: Boolean(resolved),
|
|
418
|
+
// The one field that says whether the fix actually worked: an open link,
|
|
419
|
+
// or merely an object. Anything other than 'connected' is a failure that
|
|
420
|
+
// would otherwise surface later as a confusing service-discovery error.
|
|
421
|
+
state: resolved?.state,
|
|
422
|
+
fromNoble: Boolean(peripheral)
|
|
423
|
+
});
|
|
424
|
+
return resolved;
|
|
425
|
+
} catch (error) {
|
|
426
|
+
this._log("warn", "connect.direct.error", {
|
|
427
|
+
id,
|
|
428
|
+
elapsedMs: Date.now() - startedAt,
|
|
429
|
+
error: String(error)
|
|
430
|
+
});
|
|
431
|
+
return void 0;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// noble's disconnectAsync hangs on a peripheral whose connect just failed (it
|
|
435
|
+
// waits for a CoreBluetooth disconnect event that never comes); bound it so a
|
|
436
|
+
// cleanup disconnect can't hang the connect flow.
|
|
437
|
+
async _safeDisconnect(peripheral) {
|
|
438
|
+
if (this._nativeReleased) return;
|
|
439
|
+
let timeout;
|
|
440
|
+
try {
|
|
441
|
+
await Promise.race([
|
|
442
|
+
peripheral.disconnectAsync().catch(() => void 0),
|
|
443
|
+
new Promise((resolve) => {
|
|
444
|
+
timeout = setTimeout(resolve, BLE_DISCONNECT_TIMEOUT_MS);
|
|
445
|
+
})
|
|
446
|
+
]);
|
|
447
|
+
} finally {
|
|
448
|
+
clearTimeout(timeout);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// noble has no connect timeout, so a stale bond hangs anywhere — connectAsync
|
|
452
|
+
// OR the post-connect (encrypted) service discovery. One overall timeout
|
|
453
|
+
// covers the whole flow. Two distinct failures reach the connector: a `timed
|
|
454
|
+
// out` reject (device unreachable) vs a connectAsync `connection failed`
|
|
455
|
+
// reject (link refused / stale bond) — mapped to different error codes there.
|
|
456
|
+
async connect(id, options) {
|
|
457
|
+
if (!options || !options.vendor || ![options.serviceUuid, options.writeUuid, options.notifyUuid].every(
|
|
458
|
+
(uuid) => typeof uuid === "string" && /^[0-9a-f]{32}$/i.test(normalizeUuid(uuid))
|
|
459
|
+
)) {
|
|
460
|
+
throw new Error("Invalid BLE GATT profile");
|
|
461
|
+
}
|
|
462
|
+
this._assertActive();
|
|
463
|
+
const claim = { abandoned: false };
|
|
464
|
+
let abandon;
|
|
465
|
+
const abandoned = new Promise((_, reject) => {
|
|
466
|
+
abandon = (error) => {
|
|
467
|
+
claim.abandoned = true;
|
|
468
|
+
reject(error);
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
const timer = setTimeout(
|
|
472
|
+
() => abandon(new Error(`connect timed out after ${this._connectTimeoutMs}ms`)),
|
|
473
|
+
this._connectTimeoutMs
|
|
474
|
+
);
|
|
475
|
+
const attempt = {
|
|
476
|
+
id,
|
|
477
|
+
abandon,
|
|
478
|
+
cancelNative: () => claim.cancelNative?.(),
|
|
479
|
+
settled: Promise.resolve(void 0)
|
|
480
|
+
};
|
|
481
|
+
this._activeConnect = attempt;
|
|
482
|
+
this._connectAttempts.add(attempt);
|
|
483
|
+
const nativeOperation = this._connectInner(id, claim, options);
|
|
484
|
+
const caller = (async () => {
|
|
485
|
+
try {
|
|
486
|
+
return await Promise.race([nativeOperation, abandoned]);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
const peripheral = this._discovered.get(id);
|
|
489
|
+
if (peripheral) await this._safeDisconnect(peripheral);
|
|
490
|
+
throw error;
|
|
491
|
+
} finally {
|
|
492
|
+
clearTimeout(timer);
|
|
493
|
+
if (this._activeConnect === attempt) this._activeConnect = void 0;
|
|
494
|
+
}
|
|
495
|
+
})();
|
|
496
|
+
attempt.settled = Promise.allSettled([nativeOperation, caller]).finally(() => {
|
|
497
|
+
this._connectAttempts.delete(attempt);
|
|
498
|
+
});
|
|
499
|
+
return caller;
|
|
500
|
+
}
|
|
501
|
+
async _connectInner(id, claim, options) {
|
|
502
|
+
await this.init();
|
|
503
|
+
await this._pauseScan();
|
|
504
|
+
await delay(BLE_CONNECT_SETTLE_MS);
|
|
505
|
+
this._assertActive();
|
|
506
|
+
let route = "cache";
|
|
507
|
+
let peripheral = this._discovered.get(id);
|
|
508
|
+
if (!peripheral) {
|
|
509
|
+
route = "scan";
|
|
510
|
+
peripheral = await this._scanUntilFound(id, THIRD_PARTY_BLE_SCAN_DURATION_MS);
|
|
511
|
+
}
|
|
512
|
+
if (!peripheral) {
|
|
513
|
+
route = "direct";
|
|
514
|
+
}
|
|
515
|
+
if (!peripheral) {
|
|
516
|
+
const native2 = this._requireNoble();
|
|
517
|
+
claim.cancelNative = () => native2.cancelConnect?.(id);
|
|
518
|
+
peripheral = await this._directConnect(id);
|
|
519
|
+
}
|
|
520
|
+
if (!peripheral) {
|
|
521
|
+
this._log("warn", "connect.notFound", {
|
|
522
|
+
id,
|
|
523
|
+
route: "none",
|
|
524
|
+
discoveredCount: this._discovered.size
|
|
525
|
+
});
|
|
526
|
+
throw new Error(`BLE device not found: ${id}`);
|
|
527
|
+
}
|
|
528
|
+
const abortIfAbandoned = async (stage) => {
|
|
529
|
+
if (!claim.abandoned && !this._disposed) return;
|
|
530
|
+
if (peripheral && peripheral.state === "connected" && !this._connected.has(id)) {
|
|
531
|
+
await this._safeDisconnect(peripheral);
|
|
532
|
+
}
|
|
533
|
+
this._log("warn", "connect.abandoned", { id, route, stage });
|
|
534
|
+
throw new Error(`connect abandoned after timeout: ${id}`);
|
|
535
|
+
};
|
|
536
|
+
await abortIfAbandoned("resolve");
|
|
537
|
+
const wasConnected = peripheral.state === "connected";
|
|
538
|
+
const connectingPeripheral = peripheral;
|
|
539
|
+
const native = this._requireNoble();
|
|
540
|
+
claim.cancelNative = () => {
|
|
541
|
+
if (connectingPeripheral.state === "connecting" && connectingPeripheral.cancelConnect) {
|
|
542
|
+
connectingPeripheral.cancelConnect();
|
|
543
|
+
} else {
|
|
544
|
+
native.cancelConnect?.(id);
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
this._log("warn", "connect.route", {
|
|
548
|
+
id,
|
|
549
|
+
route,
|
|
550
|
+
wasConnected,
|
|
551
|
+
name: peripheral.advertisement?.localName
|
|
552
|
+
});
|
|
553
|
+
if (!wasConnected) {
|
|
554
|
+
await peripheral.connectAsync();
|
|
555
|
+
await abortIfAbandoned("link");
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
const uuids = {
|
|
559
|
+
service: options.serviceUuid,
|
|
560
|
+
write: options.writeUuid,
|
|
561
|
+
notify: options.notifyUuid
|
|
562
|
+
};
|
|
563
|
+
const { characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync(
|
|
564
|
+
[uuids.service],
|
|
565
|
+
[uuids.write, uuids.notify]
|
|
566
|
+
);
|
|
567
|
+
const writeUuid = normalizeUuid(uuids.write);
|
|
568
|
+
const notifyUuid = normalizeUuid(uuids.notify);
|
|
569
|
+
const writeChar = characteristics.find((c) => normalizeUuid(c.uuid) === writeUuid);
|
|
570
|
+
const notifyChar = characteristics.find((c) => normalizeUuid(c.uuid) === notifyUuid);
|
|
571
|
+
if (!writeChar || !notifyChar) {
|
|
572
|
+
throw new Error(`BLE characteristics not found on device ${id}`);
|
|
573
|
+
}
|
|
574
|
+
await abortIfAbandoned("discovery");
|
|
575
|
+
const disconnectHandler = () => {
|
|
576
|
+
if (this._connected.get(id)?.disconnectHandler !== disconnectHandler) return;
|
|
577
|
+
this._cleanupDevice(
|
|
578
|
+
id,
|
|
579
|
+
/* unexpected */
|
|
580
|
+
true
|
|
581
|
+
);
|
|
582
|
+
};
|
|
583
|
+
peripheral.on("disconnect", disconnectHandler);
|
|
584
|
+
this._connected.set(id, {
|
|
585
|
+
peripheral,
|
|
586
|
+
writeChar,
|
|
587
|
+
notifyChar,
|
|
588
|
+
disconnectHandler,
|
|
589
|
+
vendor: options.vendor,
|
|
590
|
+
write: options.write
|
|
591
|
+
});
|
|
592
|
+
this._log("info", "connect.done", { id, name: peripheral.advertisement.localName });
|
|
593
|
+
return { id, name: peripheral.advertisement.localName };
|
|
594
|
+
} catch (error) {
|
|
595
|
+
if (!wasConnected) await this._safeDisconnect(peripheral);
|
|
596
|
+
throw error;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async disconnect(id) {
|
|
600
|
+
const entry = this._connected.get(id);
|
|
601
|
+
if (!entry) return;
|
|
602
|
+
if (entry.disconnectHandler) {
|
|
603
|
+
entry.peripheral.removeListener("disconnect", entry.disconnectHandler);
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
await entry.peripheral.disconnectAsync();
|
|
607
|
+
} catch (error) {
|
|
608
|
+
this._log("warn", "disconnect.error", { id, error: String(error) });
|
|
609
|
+
}
|
|
610
|
+
this._cleanupDevice(
|
|
611
|
+
id,
|
|
612
|
+
/* unexpected */
|
|
613
|
+
false
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
async subscribe(id) {
|
|
617
|
+
const entry = this._requireEntry(id);
|
|
618
|
+
if (!entry.notifyChar) throw new Error(`BLE notify characteristic missing for ${id}`);
|
|
619
|
+
if (entry.notifyHandler) return;
|
|
620
|
+
const handler = (data) => {
|
|
621
|
+
this._onNotification?.(id, data.toString("hex"));
|
|
622
|
+
};
|
|
623
|
+
entry.notifyHandler = handler;
|
|
624
|
+
entry.notifyChar.on("data", handler);
|
|
625
|
+
await entry.notifyChar.subscribeAsync();
|
|
626
|
+
}
|
|
627
|
+
async unsubscribe(id) {
|
|
628
|
+
const entry = this._connected.get(id);
|
|
629
|
+
if (!entry?.notifyChar) return;
|
|
630
|
+
if (entry.notifyHandler) {
|
|
631
|
+
entry.notifyChar.removeListener("data", entry.notifyHandler);
|
|
632
|
+
entry.notifyHandler = void 0;
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
await entry.notifyChar.unsubscribeAsync();
|
|
636
|
+
} catch (error) {
|
|
637
|
+
this._log("warn", "unsubscribe.error", { id, error: String(error) });
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async write(id, hexData) {
|
|
641
|
+
const entry = this._requireEntry(id);
|
|
642
|
+
if (!entry.writeChar) throw new Error(`BLE write characteristic missing for ${id}`);
|
|
643
|
+
const buffer = Buffer.from(hexData, "hex");
|
|
644
|
+
const framing = entry.write;
|
|
645
|
+
if (!framing) throw new Error(`No BLE write framing recorded for ${id}`);
|
|
646
|
+
if (framing.mode === "raw") {
|
|
647
|
+
const maxLength = framing.maxLength ?? buffer.length;
|
|
648
|
+
if (!/^(?:[0-9a-f]{2})+$/i.test(hexData) || buffer.length > maxLength) {
|
|
649
|
+
throw new Error(`Invalid BLE frame for ${entry.vendor ?? "device"}`);
|
|
650
|
+
}
|
|
651
|
+
await entry.writeChar.writeAsync(buffer, false);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const chunkSize = framing.chunkSize;
|
|
655
|
+
if (!chunkSize) throw new Error(`Padded BLE writes need a chunkSize for ${id}`);
|
|
656
|
+
for (let offset = 0; offset < buffer.length; offset += chunkSize) {
|
|
657
|
+
this._assertActive();
|
|
658
|
+
const slice = buffer.subarray(offset, offset + chunkSize);
|
|
659
|
+
const chunk = Buffer.alloc(chunkSize);
|
|
660
|
+
slice.copy(chunk);
|
|
661
|
+
await entry.writeChar.writeAsync(chunk, false);
|
|
662
|
+
if (offset + chunkSize < buffer.length && framing.chunkDelayMs) {
|
|
663
|
+
await delay(framing.chunkDelayMs);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/** Retire a renderer's handler without stopping a process-wide native manager. */
|
|
668
|
+
dispose() {
|
|
669
|
+
if (this._disposePromise) return this._disposePromise;
|
|
670
|
+
this._disposed = true;
|
|
671
|
+
this._clearIdleStop();
|
|
672
|
+
this._scanning = false;
|
|
673
|
+
this._onNotification = void 0;
|
|
674
|
+
this._onDeviceDisconnected = void 0;
|
|
675
|
+
const connections = Array.from(this._connectAttempts);
|
|
676
|
+
for (const attempt of connections) {
|
|
677
|
+
attempt.abandon(new Error("Desktop BLE is shutting down"));
|
|
678
|
+
try {
|
|
679
|
+
attempt.cancelNative();
|
|
680
|
+
} catch (error) {
|
|
681
|
+
this._log("warn", "dispose.cancelConnect.error", { error: String(error) });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (const cancel of this._pendingCancellations) cancel();
|
|
685
|
+
if (this._noble && this._discoverHandler) {
|
|
686
|
+
this._noble.removeListener("discover", this._discoverHandler);
|
|
687
|
+
}
|
|
688
|
+
const entries = Array.from(this._connected.entries());
|
|
689
|
+
for (const [id, entry] of entries) {
|
|
690
|
+
if (entry.disconnectHandler) {
|
|
691
|
+
entry.peripheral.removeListener("disconnect", entry.disconnectHandler);
|
|
692
|
+
}
|
|
693
|
+
this._cleanupDevice(id, false);
|
|
694
|
+
}
|
|
695
|
+
let timeout;
|
|
696
|
+
this._disposePromise = (async () => {
|
|
697
|
+
try {
|
|
698
|
+
await Promise.race([
|
|
699
|
+
Promise.allSettled([
|
|
700
|
+
...connections.map((attempt) => attempt.settled),
|
|
701
|
+
...Array.from(this._nobleInstances, async (instance) => instance.stopScanningAsync()),
|
|
702
|
+
...entries.map(async ([, entry]) => {
|
|
703
|
+
let unsubscribeTimeout;
|
|
704
|
+
try {
|
|
705
|
+
await Promise.race([
|
|
706
|
+
entry.notifyChar?.unsubscribeAsync().catch(() => void 0),
|
|
707
|
+
new Promise((resolve) => {
|
|
708
|
+
unsubscribeTimeout = setTimeout(resolve, 250);
|
|
709
|
+
})
|
|
710
|
+
]);
|
|
711
|
+
} finally {
|
|
712
|
+
clearTimeout(unsubscribeTimeout);
|
|
713
|
+
await this._safeDisconnect(entry.peripheral);
|
|
714
|
+
}
|
|
715
|
+
})
|
|
716
|
+
]),
|
|
717
|
+
new Promise((resolve) => {
|
|
718
|
+
timeout = setTimeout(() => {
|
|
719
|
+
this._log("warn", "dispose.timeout");
|
|
720
|
+
resolve();
|
|
721
|
+
}, 3500);
|
|
722
|
+
})
|
|
723
|
+
]);
|
|
724
|
+
} finally {
|
|
725
|
+
clearTimeout(timeout);
|
|
726
|
+
this._discovered.clear();
|
|
727
|
+
this._lastSeen.clear();
|
|
728
|
+
this._initialized = false;
|
|
729
|
+
}
|
|
730
|
+
})();
|
|
731
|
+
return this._disposePromise;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Terminal native release, including instances replaced by adapter recovery.
|
|
735
|
+
* A host sharing Noble must defer stop() until all transports have disposed,
|
|
736
|
+
* and deduplicate instances passed to releaseNoble across those transports.
|
|
737
|
+
*/
|
|
738
|
+
disposeForAppQuit(releaseNoble = (instance) => instance.stop?.()) {
|
|
739
|
+
if (!this._releasePromise) {
|
|
740
|
+
this._releasePromise = this.dispose().finally(() => {
|
|
741
|
+
this._nativeReleased = true;
|
|
742
|
+
let releaseError;
|
|
743
|
+
for (const instance of this._nobleInstances) {
|
|
744
|
+
try {
|
|
745
|
+
releaseNoble(instance);
|
|
746
|
+
} catch (error) {
|
|
747
|
+
releaseError = error instanceof Error ? error : new Error(String(error));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
this._nobleInstances.clear();
|
|
751
|
+
if (releaseError) throw releaseError;
|
|
752
|
+
this._log("info", "dispose.native.done");
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
return this._releasePromise;
|
|
756
|
+
}
|
|
757
|
+
_assertActive() {
|
|
758
|
+
if (this._disposed) throw new Error("Desktop BLE is shutting down");
|
|
759
|
+
}
|
|
760
|
+
_cleanupDevice(id, unexpected) {
|
|
761
|
+
const entry = this._connected.get(id);
|
|
762
|
+
if (!entry) return;
|
|
763
|
+
if (entry.notifyChar && entry.notifyHandler) {
|
|
764
|
+
entry.notifyChar.removeListener("data", entry.notifyHandler);
|
|
765
|
+
}
|
|
766
|
+
if (entry.disconnectHandler) {
|
|
767
|
+
entry.peripheral.removeListener("disconnect", entry.disconnectHandler);
|
|
768
|
+
}
|
|
769
|
+
this._connected.delete(id);
|
|
770
|
+
if (unexpected) {
|
|
771
|
+
this._log("warn", "disconnect.unexpected", { id });
|
|
772
|
+
this._onDeviceDisconnected?.(id);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
_requireEntry(id) {
|
|
776
|
+
this._assertActive();
|
|
777
|
+
const entry = this._connected.get(id);
|
|
778
|
+
if (!entry) throw new Error(`BLE device is not connected: ${id}`);
|
|
779
|
+
return entry;
|
|
780
|
+
}
|
|
781
|
+
_requireNoble() {
|
|
782
|
+
this._assertActive();
|
|
783
|
+
if (!this._noble) throw new Error("Desktop BLE: noble was not initialized");
|
|
784
|
+
return this._noble;
|
|
785
|
+
}
|
|
786
|
+
async _waitForPoweredOn(timeoutMs) {
|
|
787
|
+
const noble = this._requireNoble();
|
|
788
|
+
if (noble.state === "poweredOn") return;
|
|
789
|
+
await new Promise((resolve, reject) => {
|
|
790
|
+
const cleanup = () => {
|
|
791
|
+
clearTimeout(timer);
|
|
792
|
+
noble.removeListener("stateChange", handler);
|
|
793
|
+
this._pendingCancellations.delete(cancel);
|
|
794
|
+
};
|
|
795
|
+
const cancel = () => {
|
|
796
|
+
cleanup();
|
|
797
|
+
reject(new Error("Desktop BLE is shutting down"));
|
|
798
|
+
};
|
|
799
|
+
const timer = setTimeout(() => {
|
|
800
|
+
cleanup();
|
|
801
|
+
reject(
|
|
802
|
+
new Error(
|
|
803
|
+
`Desktop BLE: noble did not reach poweredOn within ${timeoutMs}ms (last state: ${noble.state})`
|
|
804
|
+
)
|
|
805
|
+
);
|
|
806
|
+
}, timeoutMs);
|
|
807
|
+
const handler = (state) => {
|
|
808
|
+
if (state === "poweredOn") {
|
|
809
|
+
cleanup();
|
|
810
|
+
resolve();
|
|
811
|
+
} else if (state === "unsupported" || state === "unauthorized") {
|
|
812
|
+
cleanup();
|
|
813
|
+
reject(new Error(`Desktop BLE: noble state ${state}`));
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
this._pendingCancellations.add(cancel);
|
|
817
|
+
noble.on("stateChange", handler);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
_log(level, event, data) {
|
|
821
|
+
this._logger?.({
|
|
822
|
+
level,
|
|
823
|
+
scope: "desktop-noble-ble",
|
|
824
|
+
event,
|
|
825
|
+
data: redactBleDebugLogData(data)
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
// src/main.ts
|
|
831
|
+
var DEFAULT_IPC_MAIN = () => {
|
|
832
|
+
const { ipcMain } = __require("electron");
|
|
833
|
+
return ipcMain;
|
|
834
|
+
};
|
|
835
|
+
function initThirdPartyBleSupport(webContents, options = {}) {
|
|
836
|
+
const ipcMain = options.ipcMain ?? DEFAULT_IPC_MAIN();
|
|
837
|
+
const handler = new NobleBleHandler(options);
|
|
838
|
+
let disposed = false;
|
|
839
|
+
handler.setNotificationListener((id, hexData) => {
|
|
840
|
+
webContents.send(THIRD_PARTY_BLE_CHANNELS.notification, id, hexData);
|
|
841
|
+
});
|
|
842
|
+
handler.setDisconnectedListener((id) => {
|
|
843
|
+
webContents.send(THIRD_PARTY_BLE_CHANNELS.disconnected, id);
|
|
844
|
+
});
|
|
845
|
+
const handle = (channel, fn) => {
|
|
846
|
+
ipcMain.handle(channel, async (_event, ...args) => {
|
|
847
|
+
if (disposed) throw new Error("Third-party BLE is shutting down");
|
|
848
|
+
return fn(...args);
|
|
849
|
+
});
|
|
850
|
+
};
|
|
851
|
+
handle(
|
|
852
|
+
THIRD_PARTY_BLE_CHANNELS.scan,
|
|
853
|
+
(options2) => handler.scan(options2)
|
|
854
|
+
);
|
|
855
|
+
handle(THIRD_PARTY_BLE_CHANNELS.stopScan, () => handler.stopScan());
|
|
856
|
+
handle(
|
|
857
|
+
THIRD_PARTY_BLE_CHANNELS.connect,
|
|
858
|
+
(id, options2) => handler.connect(id, options2)
|
|
859
|
+
);
|
|
860
|
+
handle(THIRD_PARTY_BLE_CHANNELS.disconnect, (id) => handler.disconnect(id));
|
|
861
|
+
handle(
|
|
862
|
+
THIRD_PARTY_BLE_CHANNELS.write,
|
|
863
|
+
(id, hexData) => handler.write(id, hexData)
|
|
864
|
+
);
|
|
865
|
+
handle(THIRD_PARTY_BLE_CHANNELS.subscribe, (id) => handler.subscribe(id));
|
|
866
|
+
handle(THIRD_PARTY_BLE_CHANNELS.unsubscribe, (id) => handler.unsubscribe(id));
|
|
867
|
+
handle(THIRD_PARTY_BLE_CHANNELS.availability, () => handler.checkAvailability());
|
|
868
|
+
handle(THIRD_PARTY_BLE_CHANNELS.getDevice, (id) => handler.getDevice(id));
|
|
869
|
+
handle(THIRD_PARTY_BLE_CHANNELS.readRssi, (id) => handler.readRssi(id));
|
|
870
|
+
handle(THIRD_PARTY_BLE_CHANNELS.cancelPairing, () => handler.cancelPairing());
|
|
871
|
+
const removeHandlers = () => {
|
|
872
|
+
if (disposed) return;
|
|
873
|
+
disposed = true;
|
|
874
|
+
for (const channel of Object.values(THIRD_PARTY_BLE_CHANNELS)) {
|
|
875
|
+
ipcMain.removeHandler(channel);
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
return {
|
|
879
|
+
handler,
|
|
880
|
+
dispose: () => {
|
|
881
|
+
removeHandlers();
|
|
882
|
+
return handler.dispose();
|
|
883
|
+
},
|
|
884
|
+
disposeForAppQuit: (releaseNoble) => {
|
|
885
|
+
removeHandlers();
|
|
886
|
+
return handler.disposeForAppQuit(releaseNoble);
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
export {
|
|
891
|
+
NobleBleHandler,
|
|
892
|
+
THIRD_PARTY_BLE_CHANNELS,
|
|
893
|
+
THIRD_PARTY_BLE_DEVICE_TTL_MS,
|
|
894
|
+
THIRD_PARTY_BLE_POWER_ON_TIMEOUT_MS,
|
|
895
|
+
THIRD_PARTY_BLE_SCAN_DURATION_MS,
|
|
896
|
+
THIRD_PARTY_BLE_SCAN_IDLE_STOP_MS,
|
|
897
|
+
initThirdPartyBleSupport,
|
|
898
|
+
redactBleDebugLogData
|
|
899
|
+
};
|
|
900
|
+
//# sourceMappingURL=index.mjs.map
|