@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.
@@ -0,0 +1,357 @@
1
+ import { ElectronBleScanOptions, ElectronBleConnectOptions } from '@onekeyfe/hwk-adapter-core';
2
+ export { THIRD_PARTY_BLE_CHANNELS, THIRD_PARTY_BLE_DEVICE_TTL_MS, THIRD_PARTY_BLE_POWER_ON_TIMEOUT_MS, THIRD_PARTY_BLE_SCAN_DURATION_MS, THIRD_PARTY_BLE_SCAN_IDLE_STOP_MS, ThirdPartyBleChannel } from './constants.js';
3
+
4
+ type BleDebugLogLevel = 'debug' | 'info' | 'warn' | 'error';
5
+ type BleDebugLogEntry = {
6
+ level: BleDebugLogLevel;
7
+ scope: string;
8
+ event: string;
9
+ data?: Record<string, unknown>;
10
+ };
11
+ type BleDebugLogger = (entry: BleDebugLogEntry) => void;
12
+ /** Replace sensitive values in place; everything else is forwarded verbatim. */
13
+ declare function redactBleDebugLogData(data?: Record<string, unknown>): Record<string, unknown> | undefined;
14
+
15
+ /**
16
+ * Shape of the API the renderer process talks to. In a real Electron app
17
+ * this is normally exposed via `contextBridge.exposeInMainWorld('desktopApi',
18
+ * { trezorBle: ... })`, but the transport accepts the bridge directly so
19
+ * non-Electron hosts (and unit tests) can plug in their own implementation.
20
+ */
21
+ interface ThirdPartyBleDeviceInfo {
22
+ /** Stable id (noble peripheral.id) used as connectId. */
23
+ id: string;
24
+ name?: string;
25
+ rssi?: number;
26
+ advertisedServiceUuids?: string[];
27
+ /** Same as `name`, kept explicit to mirror noble's `advertisement.localName`. */
28
+ localName?: string;
29
+ /** Whether the peripheral advertised itself as connectable. */
30
+ isConnectable?: boolean | null;
31
+ serviceSolicitationUuids?: string[];
32
+ txPowerLevel?: number;
33
+ /** Manufacturer-specific advertisement data, hex-encoded (may embed a serial). */
34
+ manufacturerDataHex?: string;
35
+ /** Per-service advertisement data, each hex-encoded. */
36
+ serviceData?: Array<{
37
+ uuid: string;
38
+ dataHex: string;
39
+ }>;
40
+ /** BLE MAC/address when the OS exposes it separately from `id`. */
41
+ address?: string;
42
+ addressType?: string;
43
+ /** noble peripheral connection state at scan time. */
44
+ state?: string;
45
+ }
46
+ interface ThirdPartyBleAvailability {
47
+ available: boolean;
48
+ /** noble state: `poweredOn` / `poweredOff` / `unauthorized` / `unsupported` / `resetting` / `unknown`. */
49
+ state: string;
50
+ initialized: boolean;
51
+ }
52
+ /** One definition of the scan contract; the IPC shape lives in adapter-core. */
53
+ type ThirdPartyBleScanOptions = ElectronBleScanOptions;
54
+ interface ThirdPartyBleApi {
55
+ scan(options?: ThirdPartyBleScanOptions): Promise<ThirdPartyBleDeviceInfo[]>;
56
+ stopScan(): Promise<void>;
57
+ connect(id: string, options: ElectronBleConnectOptions): Promise<{
58
+ id: string;
59
+ name?: string;
60
+ }>;
61
+ disconnect(id: string): Promise<void>;
62
+ /** Subscribe to the BLE notify characteristic for `id`. */
63
+ subscribe(id: string): Promise<void>;
64
+ unsubscribe(id: string): Promise<void>;
65
+ /**
66
+ * Write an already-framed payload. The main process applies the framing rule
67
+ * recorded at connect time — splitting and zero-padding for `padded`, or
68
+ * writing the buffer as given for `raw`.
69
+ */
70
+ write(id: string, hexData: string): Promise<void>;
71
+ checkAvailability(): Promise<ThirdPartyBleAvailability>;
72
+ /** Look up a previously-scanned device by id without re-scanning. */
73
+ getDevice(id: string): Promise<ThirdPartyBleDeviceInfo | null>;
74
+ /** Read current RSSI (dBm) of a *connected* peripheral. */
75
+ readRssi(id: string): Promise<number>;
76
+ /** Stop scan + disconnect every in-flight connection. */
77
+ cancelPairing(): Promise<void>;
78
+ /** Register a listener for incoming BLE notifications. Returns an unsubscribe fn. */
79
+ onNotification(handler: (id: string, hexData: string) => void): () => void;
80
+ /** Register a listener for unexpected disconnects. Returns an unsubscribe fn. */
81
+ onDeviceDisconnected(handler: (id: string) => void): () => void;
82
+ }
83
+
84
+ /**
85
+ * Subset of @stoprocent/noble we touch. We type as `any` to keep the package
86
+ * installable without the native module — it's loaded lazily inside main().
87
+ */
88
+ interface NobleLike {
89
+ state: string;
90
+ on(event: string, handler: (...args: any[]) => void): NobleLike;
91
+ removeListener(event: string, handler: (...args: any[]) => void): NobleLike;
92
+ startScanningAsync(serviceUuids: string[], allowDuplicates: boolean): Promise<void>;
93
+ stopScanningAsync(): Promise<void>;
94
+ stop?(): void;
95
+ /**
96
+ * Connect by id/address with NO scan. Both native backends support this and
97
+ * emit a `discover` for the peripheral as a side effect: Windows synthesizes
98
+ * one for an unknown address (`BLEManager::Connect`, lib/win/src/ble_manager.cc)
99
+ * and macOS resolves it via `retrievePeripheralsWithIdentifiers`
100
+ * (lib/mac/src/ble_manager.mm). Optional so a stub noble can omit it.
101
+ */
102
+ connectAsync?(idOrAddress: string): Promise<NoblePeripheralLike | undefined>;
103
+ cancelConnect?(idOrAddress: string): void;
104
+ reset?(): Promise<void>;
105
+ }
106
+ interface NoblePeripheralLike {
107
+ id: string;
108
+ advertisement: {
109
+ localName?: string;
110
+ serviceUuids?: string[];
111
+ manufacturerData?: Buffer;
112
+ serviceData?: Array<{
113
+ uuid: string;
114
+ data: Buffer;
115
+ }>;
116
+ txPowerLevel?: number;
117
+ serviceSolicitationUuids?: string[];
118
+ };
119
+ /** Some noble builds expose the BLE MAC/address separately from `id`. */
120
+ address?: string;
121
+ addressType?: string;
122
+ connectable?: boolean;
123
+ rssi: number;
124
+ state: string;
125
+ connectAsync(): Promise<void>;
126
+ cancelConnect?(): void;
127
+ disconnectAsync(): Promise<void>;
128
+ discoverSomeServicesAndCharacteristicsAsync(serviceUuids: string[], characteristicUuids: string[]): Promise<{
129
+ characteristics: NobleCharacteristicLike[];
130
+ }>;
131
+ /** Read live RSSI from the connected peripheral. Returns dBm. */
132
+ updateRssiAsync?(): Promise<number>;
133
+ on(event: string, handler: (...args: any[]) => void): NoblePeripheralLike;
134
+ removeListener(event: string, handler: (...args: any[]) => void): NoblePeripheralLike;
135
+ }
136
+ interface NobleCharacteristicLike {
137
+ uuid: string;
138
+ subscribeAsync(): Promise<void>;
139
+ unsubscribeAsync(): Promise<void>;
140
+ writeAsync(data: Buffer, withoutResponse: boolean): Promise<void>;
141
+ on(event: 'data', handler: (data: Buffer, isNotification: boolean) => void): NobleCharacteristicLike;
142
+ removeListener(event: 'data', handler: (data: Buffer, isNotification: boolean) => void): NobleCharacteristicLike;
143
+ }
144
+ type NobleFactory = () => NobleLike;
145
+ interface NobleBleHandlerOptions {
146
+ /** Override for tests; defaults to `require('@stoprocent/noble')`. */
147
+ nobleFactory?: NobleFactory;
148
+ /** Override the overall connect timeout (tests only; defaults to 31s). */
149
+ connectTimeoutMs?: number;
150
+ logger?: BleDebugLogger;
151
+ }
152
+ /**
153
+ * Core BLE logic, decoupled from Electron's IPC layer so it can be unit
154
+ * tested with a fake noble. Mirrors the OneKey `noble-ble-handler.ts`
155
+ * pattern (a single class that owns the peripheral cache + disconnect
156
+ * callbacks), but trimmed to the minimum surface we expose.
157
+ */
158
+ declare class NobleBleHandler {
159
+ private _noble;
160
+ private readonly _factory;
161
+ private readonly _connectTimeoutMs;
162
+ private readonly _logger?;
163
+ private readonly _discovered;
164
+ private readonly _lastSeen;
165
+ private readonly _connected;
166
+ private _discoverHandler?;
167
+ private _scanning;
168
+ private _idleStopTimer?;
169
+ private _onNotification?;
170
+ private _onDeviceDisconnected?;
171
+ private _initialized;
172
+ private _disposed;
173
+ private _disposePromise?;
174
+ private _releasePromise?;
175
+ private _initPromise?;
176
+ private readonly _nobleInstances;
177
+ private readonly _pendingCancellations;
178
+ private readonly _connectAttempts;
179
+ private _nativeReleased;
180
+ private _lastNobleRecoverAt?;
181
+ /** The connect currently in flight, so cancelPairing can abandon it. */
182
+ private _activeConnect?;
183
+ constructor(options?: NobleBleHandlerOptions);
184
+ setNotificationListener(handler: (id: string, hexData: string) => void): void;
185
+ setDisconnectedListener(handler: (id: string) => void): void;
186
+ init(): Promise<void>;
187
+ checkAvailability(): Promise<ThirdPartyBleAvailability>;
188
+ /**
189
+ * Lazy-start a continuous scan and return the current snapshot immediately.
190
+ *
191
+ * Scans UNFILTERED and applies the caller's match criteria in `_snapshot()` instead. A
192
+ * service-UUID filter cannot be used here: noble's Windows backend applies it
193
+ * per RECEIVED PACKET (`BLEManager::OnScanResult`, lib/win/src/ble_manager.cc),
194
+ * and a Safe 7's ADV packet carries only its name — the service UUID lives in
195
+ * the scan response, which arrives as a separate, irregularly-timed event. So
196
+ * a filtered scan drops every ADV packet and the device appears to be
197
+ * undiscoverable for minutes at a time while it is plainly on air. (OneKey's
198
+ * own devices do advertise their service UUID, which is why the same filter is
199
+ * safe in `hd-transport-electron` and was copied here by mistake.)
200
+ */
201
+ scan(options?: ElectronBleScanOptions): Promise<ThirdPartyBleDeviceInfo[]>;
202
+ /**
203
+ * Current in-range devices for the asking vendor, dropping any that aged past
204
+ * the liveness TTL. The match test replaces the service-UUID scan filter we cannot use
205
+ * (see `scan`): it matches the name from the ADV packet, or the service UUID
206
+ * once a scan response has merged into the same peripheral.
207
+ *
208
+ * Note the TTL only prunes what the CALLER sees. `_discovered` is a cache, not
209
+ * the source of truth for reachability — a device missing from here can still
210
+ * be connected to by id (`_directConnect`).
211
+ */
212
+ private _snapshot;
213
+ /** A noble instance with its own bindings, so a stuck one can be replaced. */
214
+ private _createFreshNoble;
215
+ /**
216
+ * Rebuild noble when its adapter state is stuck. Re-enumerating the Windows
217
+ * BLE stack (pairing, or removing the device from OS settings) can catch
218
+ * noble's RadioWatcher mid-churn: it latches `unsupported` and never
219
+ * re-evaluates, so every later scan fails until the process restarts. Fresh
220
+ * bindings restart that watcher, which is the in-process equivalent of the
221
+ * app restart that is otherwise the only cure.
222
+ */
223
+ private _recoverNobleIfStuck;
224
+ private _armIdleStop;
225
+ private _clearIdleStop;
226
+ /** Stop scanning but keep the discovered cache (used before connect). */
227
+ private _pauseScan;
228
+ /** Stop scanning and forget discovered devices (idle timeout / teardown). */
229
+ private _stopContinuousScan;
230
+ /**
231
+ * Stops the process-wide scan no matter which vendor asked. Scan *results*
232
+ * are filtered per vendor, but the radio is not: one vendor's stopScan ends
233
+ * the other's discovery too. That is safe only because the adapter job queue
234
+ * serializes discovery across vendors, so two are never scanning at once.
235
+ * Anything that breaks that — a background presence probe, say — needs
236
+ * per-vendor refcounting here first.
237
+ */
238
+ stopScan(): Promise<void>;
239
+ /**
240
+ * Look up a previously-scanned device by id (no extra BLE traffic).
241
+ * Returns null if the device hasn't been seen by a recent scan.
242
+ */
243
+ getDevice(id: string): ThirdPartyBleDeviceInfo | null;
244
+ /**
245
+ * Read the current RSSI (in dBm) for a connected peripheral. Requires
246
+ * the device to be connected — noble can't read RSSI off a scan-only
247
+ * peripheral. Falls back to the cached scan-time rssi when the noble
248
+ * peripheral doesn't expose updateRssiAsync.
249
+ */
250
+ readRssi(id: string): Promise<number>;
251
+ /**
252
+ * Abort the in-flight pairing flow: abandon a connect that is still running,
253
+ * stop scanning, disconnect every peripheral the host currently has open.
254
+ * Caller is responsible for surfacing the cancellation to the upper UI layer.
255
+ *
256
+ * Abandoning the connect is what actually ends the flow. Pairing happens
257
+ * inside connectAsync and the entry only reaches _connected after service
258
+ * discovery, so the loop below never sees the device being paired — without
259
+ * the abandon the caller waits out the full connect timeout, which is sized
260
+ * to the OS pairing window and so feels like a hang.
261
+ */
262
+ cancelPairing(): Promise<void>;
263
+ /**
264
+ * Scan for a specific peripheral id and resolve THE MOMENT it's discovered,
265
+ * stopping the scan immediately (don't wait out the full window). The fast
266
+ * reconnect path for a stored connectId when the device IS advertising.
267
+ *
268
+ * This used to be the only reconnect path, on two assumptions that are both
269
+ * false: that noble cannot connect by id without a scan (it can — see
270
+ * `_directConnect`), and that "the device advertises continuously" (a bonded
271
+ * Safe 7 does not — it holds the link and goes silent). Callers must fall
272
+ * back to `_directConnect` when this returns undefined.
273
+ */
274
+ private _scanUntilFound;
275
+ /**
276
+ * Connect by id with no scan and no advertisement.
277
+ *
278
+ * This is the ONLY path that reaches a device which is connected but silent.
279
+ * A linked peripheral stops advertising while it HOLDS A LINK (standard BLE; a Safe 7's
280
+ * screen says "wait connection") — field-verified: bonding/THP handshake
281
+ * alone does NOT silence it, holding the connection does. So while a link is
282
+ * up, no amount of scanning will rediscover it — `_scanUntilFound` alone
283
+ * dead-ends with "device not found" on a device that is sitting right there,
284
+ * connected and reachable.
285
+ *
286
+ * noble supports this: `noble.connectAsync(id)` needs no prior `discover`,
287
+ * because both native backends materialize the peripheral themselves (Windows
288
+ * synthesizes one for an unknown address, macOS retrieves it by identifier)
289
+ * and then emit a `discover`, which our own handler turns back into a
290
+ * `_discovered` entry. OneKey's own noble handler calls this "direct
291
+ * connection mode"; Trezor Suite's equivalent is asking the adapter for its
292
+ * peripheral list instead of keeping a cache.
293
+ *
294
+ * Returns undefined (not throw) so the caller reports the normal
295
+ * "device not found" rather than a confusing noble-internal error.
296
+ */
297
+ private _directConnect;
298
+ private _safeDisconnect;
299
+ connect(id: string, options: ElectronBleConnectOptions): Promise<{
300
+ id: string;
301
+ name?: string;
302
+ }>;
303
+ private _connectInner;
304
+ disconnect(id: string): Promise<void>;
305
+ subscribe(id: string): Promise<void>;
306
+ unsubscribe(id: string): Promise<void>;
307
+ write(id: string, hexData: string): Promise<void>;
308
+ /** Retire a renderer's handler without stopping a process-wide native manager. */
309
+ dispose(): Promise<void>;
310
+ /**
311
+ * Terminal native release, including instances replaced by adapter recovery.
312
+ * A host sharing Noble must defer stop() until all transports have disposed,
313
+ * and deduplicate instances passed to releaseNoble across those transports.
314
+ */
315
+ disposeForAppQuit(releaseNoble?: (instance: {
316
+ stop?(): void;
317
+ }) => void): Promise<void>;
318
+ private _assertActive;
319
+ private _cleanupDevice;
320
+ private _requireEntry;
321
+ private _requireNoble;
322
+ private _waitForPoweredOn;
323
+ private _log;
324
+ }
325
+
326
+ /** Minimal slice of Electron's `WebContents` we use (kept duck-typed so we
327
+ * don't take a hard dep on `electron`). */
328
+ interface WebContentsLike {
329
+ send(channel: string, ...args: unknown[]): void;
330
+ on?(event: string, listener: (...args: any[]) => void): void;
331
+ }
332
+ /** Minimal slice of Electron's `ipcMain` we use. */
333
+ interface IpcMainLike {
334
+ handle(channel: string, listener: (event: unknown, ...args: any[]) => Promise<unknown> | unknown): void;
335
+ removeHandler(channel: string): void;
336
+ }
337
+ interface InitThirdPartyBleSupportOptions extends NobleBleHandlerOptions {
338
+ /** Inject your own `ipcMain` (defaults to `require('electron').ipcMain`). */
339
+ ipcMain?: IpcMainLike;
340
+ }
341
+ interface ThirdPartyBleSupportHandle {
342
+ handler: NobleBleHandler;
343
+ dispose(): Promise<void>;
344
+ disposeForAppQuit(releaseNoble?: (instance: {
345
+ stop?(): void;
346
+ }) => void): Promise<void>;
347
+ }
348
+ /**
349
+ * Wire a `NobleBleHandler` to Electron's IPC so the renderer can drive BLE
350
+ * via `window.desktopApi.trezorBle`. Call once from the main process after
351
+ * `BrowserWindow` is ready.
352
+ *
353
+ * Call dispose() when retiring a renderer and disposeForAppQuit() before Node teardown.
354
+ */
355
+ declare function initThirdPartyBleSupport(webContents: WebContentsLike, options?: InitThirdPartyBleSupportOptions): ThirdPartyBleSupportHandle;
356
+
357
+ export { type BleDebugLogEntry, type BleDebugLogLevel, type BleDebugLogger, type InitThirdPartyBleSupportOptions, type IpcMainLike, NobleBleHandler, type NobleBleHandlerOptions, type ThirdPartyBleApi, type ThirdPartyBleAvailability, type ThirdPartyBleDeviceInfo, type ThirdPartyBleSupportHandle, initThirdPartyBleSupport, redactBleDebugLogData };