@onekeyfe/hd-transport-web-device 1.2.0-alpha.99 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/electron-ble-transport.test.ts +289 -32
- package/__tests__/webusb-protocol-cache.test.ts +276 -0
- package/__tests__/webusb-protocol-v2-timeout.test.ts +22 -8
- package/dist/electron-ble-transport.d.ts +6 -1
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +64 -3
- package/dist/index.js +232 -110
- package/dist/webusb.d.ts +11 -1
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/electron-ble-transport.ts +172 -85
- package/src/webusb.ts +201 -33
package/dist/webusb.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/// <reference types="w3c-web-usb" />
|
|
2
|
+
/// <reference types="node" />
|
|
2
3
|
import transport, { ProtocolV2UsbTransportBase } from '@onekeyfe/hd-transport';
|
|
4
|
+
import type EventEmitter from 'events';
|
|
3
5
|
import type { AcquireInput, OneKeyDeviceInfoBase, ProtocolType, ProtocolV2CallContext, ProtocolV2Schemas, TransportCallOptions } from '@onekeyfe/hd-transport';
|
|
4
6
|
export interface DeviceInfo extends OneKeyDeviceInfoBase {
|
|
5
7
|
path: string;
|
|
@@ -11,19 +13,26 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
11
13
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
12
14
|
private protocolV2SchemaSource;
|
|
13
15
|
private deviceProtocol;
|
|
16
|
+
private confirmedDeviceProtocols;
|
|
14
17
|
private deviceProtocolHints;
|
|
18
|
+
private staleProtocolPaths;
|
|
19
|
+
private acquiredPaths;
|
|
20
|
+
private probedDeviceObjects;
|
|
15
21
|
private deviceEndpoints;
|
|
16
22
|
name: string;
|
|
17
23
|
stopped: boolean;
|
|
18
24
|
configured: boolean;
|
|
19
25
|
Log?: any;
|
|
20
26
|
usb?: USB;
|
|
27
|
+
emitter?: EventEmitter;
|
|
21
28
|
deviceList: Array<DeviceInfo>;
|
|
22
29
|
configurationId: number;
|
|
23
30
|
endpointId: number;
|
|
24
31
|
interfaceId: number;
|
|
25
32
|
constructor();
|
|
26
|
-
init(logger: any): void;
|
|
33
|
+
init(logger: any, emitter?: EventEmitter): void;
|
|
34
|
+
emitDeviceDisconnect(path: string, device?: USBDevice): void;
|
|
35
|
+
markProtocolStale(path: string): void;
|
|
27
36
|
configure(signedData: any): void;
|
|
28
37
|
configureProtocolV2(signedData: any): void;
|
|
29
38
|
promptDeviceAccess(): Promise<USBDevice | null>;
|
|
@@ -40,6 +49,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
40
49
|
connectToDevice(path: string, first: boolean): Promise<void>;
|
|
41
50
|
private closeOpenDevice;
|
|
42
51
|
private clearEndpointHalt;
|
|
52
|
+
private assertAcquired;
|
|
43
53
|
post(session: string, name: string, data: Record<string, unknown>): Promise<void>;
|
|
44
54
|
private getErrorMessage;
|
|
45
55
|
private isRetryablePacketIoError;
|
package/dist/webusb.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-web-device",
|
|
3
|
-
"version": "1.2.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"author": "OneKey",
|
|
5
5
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"lint:fix": "eslint . --fix"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@onekeyfe/hd-shared": "1.2.0
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.0
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.0",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@onekeyfe/hd-transport-electron": "1.2.0
|
|
28
|
+
"@onekeyfe/hd-transport-electron": "1.2.0",
|
|
29
29
|
"@types/w3c-web-usb": "^1.0.6",
|
|
30
30
|
"@types/web-bluetooth": "^0.0.17"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "cc51df0a415bead21304ad1d8bc92b5312c91344"
|
|
33
33
|
}
|
|
@@ -6,19 +6,23 @@ import transport, {
|
|
|
6
6
|
ProtocolV2LinkManager,
|
|
7
7
|
TRANSPORT_EVENT,
|
|
8
8
|
bytesToHex,
|
|
9
|
+
detectProtocolV2LinkDisabledError,
|
|
9
10
|
hexToBytes,
|
|
11
|
+
isProtocolV2LinkDisabledError,
|
|
10
12
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
11
13
|
writeProtocolV2BleFrame,
|
|
12
14
|
} from '@onekeyfe/hd-transport';
|
|
13
15
|
import {
|
|
16
|
+
EBleDisconnectReason,
|
|
14
17
|
ERRORS,
|
|
15
18
|
HardwareErrorCode,
|
|
16
19
|
HardwareErrorCodeMessage,
|
|
17
20
|
createDeferred,
|
|
21
|
+
isBleStaleBondErrorText,
|
|
22
|
+
isBleStaleBondHardwareError,
|
|
18
23
|
isHeaderChunk,
|
|
19
24
|
} from '@onekeyfe/hd-shared';
|
|
20
25
|
|
|
21
|
-
import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
|
|
22
26
|
import { resolveBlePacketCapacity } from './ble-packet-capacity';
|
|
23
27
|
|
|
24
28
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
@@ -52,10 +56,6 @@ interface PacketProcessResult {
|
|
|
52
56
|
error?: string;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
56
|
-
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
59
|
const toBleDescriptor = (
|
|
60
60
|
device: { id: string; name: string | null },
|
|
61
61
|
protocolType?: ProtocolType
|
|
@@ -72,7 +72,7 @@ const toBleDescriptor = (
|
|
|
72
72
|
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
73
73
|
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
74
74
|
const BLE_WRITE_DELAY_MS = 5;
|
|
75
|
-
const PROTOCOL_PROBE_TIMEOUT_MS =
|
|
75
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 3000;
|
|
76
76
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -106,6 +106,9 @@ export default class ElectronBleTransport {
|
|
|
106
106
|
|
|
107
107
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
108
108
|
|
|
109
|
+
/** Endpoints that answered a V2 probe in this transport lifetime. Survives disconnect. */
|
|
110
|
+
private confirmedProtocolV2 = new Set<string>();
|
|
111
|
+
|
|
109
112
|
private deviceMtus: Map<string, number> = new Map();
|
|
110
113
|
|
|
111
114
|
private devicePacketCapacities: Map<string, number> = new Map();
|
|
@@ -146,13 +149,47 @@ export default class ElectronBleTransport {
|
|
|
146
149
|
|
|
147
150
|
private mtuCleanups: Map<string, () => void> = new Map();
|
|
148
151
|
|
|
149
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Transport-lifetime subscription to host BLE disconnects.
|
|
154
|
+
*
|
|
155
|
+
* This must NOT be scoped to acquire()/release(): a logical release keeps the
|
|
156
|
+
* native link alive for the keep-alive window, so a device that drops while
|
|
157
|
+
* idle would otherwise go unobserved and consumers would never learn it left
|
|
158
|
+
* (OK-60486).
|
|
159
|
+
*/
|
|
160
|
+
private hostDisconnectCleanup?: () => void;
|
|
150
161
|
|
|
151
162
|
private notificationTokens: Map<string, number> = new Map();
|
|
152
163
|
|
|
153
164
|
private nextNotificationToken = 1;
|
|
154
165
|
|
|
155
|
-
private
|
|
166
|
+
private toStaleBondError(error: unknown): Error | null {
|
|
167
|
+
if (isBleStaleBondHardwareError(error)) {
|
|
168
|
+
return error as Error;
|
|
169
|
+
}
|
|
170
|
+
const errorMessage =
|
|
171
|
+
error && typeof error === 'object' && 'message' in error
|
|
172
|
+
? String((error as { message?: unknown }).message ?? '')
|
|
173
|
+
: String(error ?? '');
|
|
174
|
+
if (!isBleStaleBondErrorText(errorMessage)) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const normalizedErrorMessage = errorMessage.toLowerCase();
|
|
178
|
+
return ERRORS.TypedError(
|
|
179
|
+
normalizedErrorMessage.includes('peer removed pairing information')
|
|
180
|
+
? HardwareErrorCode.BlePeerRemovedPairingInformation
|
|
181
|
+
: HardwareErrorCode.BleDeviceBondError,
|
|
182
|
+
errorMessage
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private handleBluetoothError(error: any, mapProtocolV2StaleBond = false): never {
|
|
187
|
+
if (mapProtocolV2StaleBond) {
|
|
188
|
+
const staleBondError = this.toStaleBondError(error);
|
|
189
|
+
if (staleBondError) {
|
|
190
|
+
throw staleBondError;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
156
193
|
if (error && typeof error === 'object') {
|
|
157
194
|
if ('code' in error) {
|
|
158
195
|
if (error.code === HardwareErrorCode.BlePoweredOff) {
|
|
@@ -214,12 +251,6 @@ export default class ElectronBleTransport {
|
|
|
214
251
|
mtuCleanup();
|
|
215
252
|
this.mtuCleanups.delete(deviceId);
|
|
216
253
|
}
|
|
217
|
-
|
|
218
|
-
const disconnectCleanup = this.disconnectCleanups.get(deviceId);
|
|
219
|
-
if (disconnectCleanup) {
|
|
220
|
-
disconnectCleanup();
|
|
221
|
-
this.disconnectCleanups.delete(deviceId);
|
|
222
|
-
}
|
|
223
254
|
}
|
|
224
255
|
|
|
225
256
|
init(logger: any, emitter?: EventEmitter) {
|
|
@@ -233,9 +264,48 @@ export default class ElectronBleTransport {
|
|
|
233
264
|
);
|
|
234
265
|
}
|
|
235
266
|
|
|
267
|
+
this.subscribeHostDisconnects();
|
|
268
|
+
|
|
236
269
|
this.Log?.debug('[Electron BLE] Transport initialized');
|
|
237
270
|
}
|
|
238
271
|
|
|
272
|
+
/**
|
|
273
|
+
* One host subscription for the whole transport lifetime. init() can run
|
|
274
|
+
* again after an SDK reset, so drop the previous listener first rather than
|
|
275
|
+
* stacking duplicates.
|
|
276
|
+
*/
|
|
277
|
+
private subscribeHostDisconnects() {
|
|
278
|
+
this.hostDisconnectCleanup?.();
|
|
279
|
+
this.hostDisconnectCleanup = window.desktopApi?.nobleBle?.onDeviceDisconnected(
|
|
280
|
+
(disconnectedDevice: { id: string; name: string; reason?: EBleDisconnectReason }) => {
|
|
281
|
+
const uuid = disconnectedDevice?.id;
|
|
282
|
+
if (!uuid) return;
|
|
283
|
+
|
|
284
|
+
// The link is gone, so renderer-side state must go with it; the next
|
|
285
|
+
// acquire reconnects from scratch.
|
|
286
|
+
this.cleanupDeviceState(uuid);
|
|
287
|
+
|
|
288
|
+
// Every link drop is reported, including the main process reclaiming
|
|
289
|
+
// an idle link on its keep-alive timer: consumers track whether a BLE
|
|
290
|
+
// link is live, not whether the peripheral is theoretically in range,
|
|
291
|
+
// and a link we closed ourselves is still a closed link. Nothing
|
|
292
|
+
// reconnects on its own, so this settles once until the user acts.
|
|
293
|
+
// `reason` is carried for diagnostics only — behaviour is uniform.
|
|
294
|
+
this.Log?.debug(
|
|
295
|
+
'[Electron BLE] Device link dropped:',
|
|
296
|
+
uuid,
|
|
297
|
+
disconnectedDevice.reason ?? EBleDisconnectReason.DeviceDisconnected
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
301
|
+
name: disconnectedDevice.name,
|
|
302
|
+
id: uuid,
|
|
303
|
+
connectId: uuid,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
239
309
|
configure(signedData: any) {
|
|
240
310
|
this._messages = parseConfigure(signedData);
|
|
241
311
|
this.configured = true;
|
|
@@ -270,10 +340,6 @@ export default class ElectronBleTransport {
|
|
|
270
340
|
this.Log?.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
|
|
271
341
|
for (const dev of devices) {
|
|
272
342
|
this.Log?.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`);
|
|
273
|
-
const protocolHint = inferProtocolHintFromDeviceName(dev.name);
|
|
274
|
-
if (protocolHint) {
|
|
275
|
-
this.deviceProtocolHints.set(dev.id, protocolHint);
|
|
276
|
-
}
|
|
277
343
|
}
|
|
278
344
|
return devices.map(device => toBleDescriptor(device));
|
|
279
345
|
} catch (error) {
|
|
@@ -284,6 +350,9 @@ export default class ElectronBleTransport {
|
|
|
284
350
|
|
|
285
351
|
async acquire(input: BleAcquireInput) {
|
|
286
352
|
const { uuid, forceCleanRunPromise, expectedProtocol } = input;
|
|
353
|
+
const shouldMapProtocolV2StaleBond = expectedProtocol
|
|
354
|
+
? expectedProtocol === 'V2'
|
|
355
|
+
: this.confirmedProtocolV2.has(uuid);
|
|
287
356
|
|
|
288
357
|
if (!uuid) {
|
|
289
358
|
throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
|
|
@@ -311,9 +380,7 @@ export default class ElectronBleTransport {
|
|
|
311
380
|
}
|
|
312
381
|
const protocolHint = expectedProtocol
|
|
313
382
|
? undefined
|
|
314
|
-
: input.protocolHint ??
|
|
315
|
-
this.deviceProtocolHints.get(uuid) ??
|
|
316
|
-
inferProtocolHintFromDeviceName(device.name);
|
|
383
|
+
: input.protocolHint ?? this.deviceProtocolHints.get(uuid);
|
|
317
384
|
if (protocolHint) {
|
|
318
385
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
319
386
|
}
|
|
@@ -322,7 +389,7 @@ export default class ElectronBleTransport {
|
|
|
322
389
|
await window.desktopApi.nobleBle.connect(uuid);
|
|
323
390
|
this.connectedDevices.add(uuid);
|
|
324
391
|
} catch (error) {
|
|
325
|
-
this.handleBluetoothError(error);
|
|
392
|
+
this.handleBluetoothError(error, shouldMapProtocolV2StaleBond);
|
|
326
393
|
}
|
|
327
394
|
|
|
328
395
|
const mtuCleanup = this.createMtuSubscription(uuid);
|
|
@@ -333,12 +400,15 @@ export default class ElectronBleTransport {
|
|
|
333
400
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
334
401
|
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
|
|
335
402
|
|
|
336
|
-
|
|
403
|
+
try {
|
|
404
|
+
await window.desktopApi.nobleBle.subscribe(uuid);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
this.handleBluetoothError(error, shouldMapProtocolV2StaleBond);
|
|
407
|
+
}
|
|
337
408
|
await this.refreshBlePacketCapacity(uuid);
|
|
338
409
|
|
|
339
410
|
const cleanup = this.createNotificationSubscription(uuid);
|
|
340
411
|
this.notificationCleanups.set(uuid, cleanup);
|
|
341
|
-
const connectionToken = this.notificationTokens.get(uuid);
|
|
342
412
|
|
|
343
413
|
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
344
414
|
if (protocolType === 'V2') {
|
|
@@ -349,23 +419,6 @@ export default class ElectronBleTransport {
|
|
|
349
419
|
});
|
|
350
420
|
}
|
|
351
421
|
|
|
352
|
-
const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
|
|
353
|
-
(disconnectedDevice: any) => {
|
|
354
|
-
if (
|
|
355
|
-
disconnectedDevice.id === uuid &&
|
|
356
|
-
this.notificationTokens.get(uuid) === connectionToken
|
|
357
|
-
) {
|
|
358
|
-
this.cleanupDeviceState(uuid);
|
|
359
|
-
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
360
|
-
name: disconnectedDevice.name,
|
|
361
|
-
id: disconnectedDevice.id,
|
|
362
|
-
connectId: disconnectedDevice.id,
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
);
|
|
367
|
-
this.disconnectCleanups.set(uuid, disconnectCleanup);
|
|
368
|
-
|
|
369
422
|
return {
|
|
370
423
|
...toBleDescriptor({ id: device.id, name: device.name }, protocolType),
|
|
371
424
|
uuid,
|
|
@@ -443,9 +496,12 @@ export default class ElectronBleTransport {
|
|
|
443
496
|
}
|
|
444
497
|
}
|
|
445
498
|
|
|
446
|
-
private createProtocolMismatchError(expected: ProtocolType) {
|
|
499
|
+
private createProtocolMismatchError(expected: ProtocolType, uuid: string) {
|
|
500
|
+
// A generic Ping miss is not a bond failure. Only a later miss after this
|
|
501
|
+
// endpoint already answered V2, or a native encryption/pairing error, is.
|
|
502
|
+
const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
|
|
447
503
|
return ERRORS.TypedError(
|
|
448
|
-
HardwareErrorCode.RuntimeError,
|
|
504
|
+
isStaleV2Bond ? HardwareErrorCode.BleDeviceBondError : HardwareErrorCode.RuntimeError,
|
|
449
505
|
`Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
|
|
450
506
|
);
|
|
451
507
|
}
|
|
@@ -468,22 +524,30 @@ export default class ElectronBleTransport {
|
|
|
468
524
|
expectedProtocol?: ProtocolType,
|
|
469
525
|
protocolHint?: ProtocolType
|
|
470
526
|
): Promise<ProtocolType> {
|
|
527
|
+
// A declared V1 is taken at face value, as the React Native transport
|
|
528
|
+
// already does on iOS: the caller reads the protocol off its own device
|
|
529
|
+
// record, so probing re-asks a question that is already answered and adds a
|
|
530
|
+
// round trip to every cold connect. It also fails in a way that costs the
|
|
531
|
+
// session: a device whose protocol session has stalled ignores the probe
|
|
532
|
+
// frame, and the timeout became a protocol-mismatch error that stopped Core
|
|
533
|
+
// from ever sending Initialize — the one frame such a device still answers,
|
|
534
|
+
// and how it gets revived. A declared V2 keeps probing, matching iOS, so a
|
|
535
|
+
// USB-priority "link disabled" surfaces here rather than as an unmapped
|
|
536
|
+
// error later. An undeclared protocol still goes through full detection.
|
|
471
537
|
if (expectedProtocol === 'V1') {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return 'V1';
|
|
476
|
-
}
|
|
477
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
538
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
539
|
+
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected, no probe)`);
|
|
540
|
+
return 'V1';
|
|
478
541
|
}
|
|
479
542
|
|
|
480
543
|
if (expectedProtocol === 'V2') {
|
|
481
544
|
if (await this.probeProtocolV2(uuid)) {
|
|
482
545
|
this.deviceProtocol.set(uuid, 'V2');
|
|
546
|
+
this.confirmedProtocolV2.add(uuid);
|
|
483
547
|
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
|
|
484
548
|
return 'V2';
|
|
485
549
|
}
|
|
486
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
550
|
+
throw this.createProtocolMismatchError(expectedProtocol, uuid);
|
|
487
551
|
}
|
|
488
552
|
|
|
489
553
|
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
@@ -494,13 +558,17 @@ export default class ElectronBleTransport {
|
|
|
494
558
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
495
559
|
const protocol = probeOrder[i];
|
|
496
560
|
if (i > 0) {
|
|
497
|
-
//
|
|
561
|
+
// Keep the physical BLE link while switching probes. Reconnecting here can
|
|
562
|
+
// summon a second OS pairing prompt for a device that is still onboarding.
|
|
498
563
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
499
564
|
}
|
|
500
565
|
const detected =
|
|
501
566
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
502
567
|
if (detected) {
|
|
503
568
|
this.deviceProtocol.set(uuid, protocol);
|
|
569
|
+
if (protocol === 'V2') {
|
|
570
|
+
this.confirmedProtocolV2.add(uuid);
|
|
571
|
+
}
|
|
504
572
|
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
|
|
505
573
|
return protocol;
|
|
506
574
|
}
|
|
@@ -541,36 +609,13 @@ export default class ElectronBleTransport {
|
|
|
541
609
|
this.runPromiseDeviceId = null;
|
|
542
610
|
}
|
|
543
611
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
try {
|
|
552
|
-
await window.desktopApi?.nobleBle?.unsubscribe(uuid);
|
|
553
|
-
} catch (error) {
|
|
554
|
-
this.Log?.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error);
|
|
555
|
-
}
|
|
556
|
-
try {
|
|
557
|
-
await window.desktopApi?.nobleBle?.disconnect(uuid);
|
|
558
|
-
} catch (error) {
|
|
559
|
-
this.Log?.debug(`[Electron BLE] disconnect after Protocol ${protocol} probe failed:`, error);
|
|
560
|
-
}
|
|
561
|
-
this.connectedDevices.delete(uuid);
|
|
562
|
-
try {
|
|
563
|
-
await window.desktopApi?.nobleBle?.connect(uuid);
|
|
564
|
-
this.connectedDevices.add(uuid);
|
|
565
|
-
await window.desktopApi?.nobleBle?.subscribe(uuid);
|
|
566
|
-
await this.refreshBlePacketCapacity(uuid);
|
|
567
|
-
} catch (error) {
|
|
568
|
-
this.Log?.debug(`[Electron BLE] reconnect after Protocol ${protocol} probe failed:`, error);
|
|
569
|
-
throw error;
|
|
612
|
+
// A timed-out V1 probe retires its renderer notification token so a late V1
|
|
613
|
+
// response cannot satisfy the V2 probe. Install a fresh listener without
|
|
614
|
+
// touching the native GATT subscription or physical connection.
|
|
615
|
+
if (!this.notificationCleanups.has(uuid)) {
|
|
616
|
+
const cleanup = this.createNotificationSubscription(uuid);
|
|
617
|
+
this.notificationCleanups.set(uuid, cleanup);
|
|
570
618
|
}
|
|
571
|
-
|
|
572
|
-
const cleanup = this.createNotificationSubscription(uuid);
|
|
573
|
-
this.notificationCleanups.set(uuid, cleanup);
|
|
574
619
|
}
|
|
575
620
|
|
|
576
621
|
private async probeProtocolV1(uuid: string) {
|
|
@@ -587,6 +632,9 @@ export default class ElectronBleTransport {
|
|
|
587
632
|
} catch (error) {
|
|
588
633
|
this.clearProbeProtocol(uuid, 'V1');
|
|
589
634
|
this.Log?.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
|
|
635
|
+
if (isProtocolV2LinkDisabledError(error)) {
|
|
636
|
+
throw error;
|
|
637
|
+
}
|
|
590
638
|
return false;
|
|
591
639
|
}
|
|
592
640
|
}
|
|
@@ -607,6 +655,8 @@ export default class ElectronBleTransport {
|
|
|
607
655
|
this.v2Assemblers.get(uuid)?.reset();
|
|
608
656
|
this.resetProtocolV2Frames(uuid);
|
|
609
657
|
},
|
|
658
|
+
shouldRethrow: error =>
|
|
659
|
+
isBleStaleBondHardwareError(error) || isProtocolV2LinkDisabledError(error),
|
|
610
660
|
});
|
|
611
661
|
if (!detected) {
|
|
612
662
|
this.clearProbeProtocol(uuid, 'V2');
|
|
@@ -620,7 +670,15 @@ export default class ElectronBleTransport {
|
|
|
620
670
|
throw new Error('Noble BLE API not available');
|
|
621
671
|
}
|
|
622
672
|
|
|
623
|
-
|
|
673
|
+
try {
|
|
674
|
+
await nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
|
|
675
|
+
} catch (error) {
|
|
676
|
+
const staleBondError = this.toStaleBondError(error);
|
|
677
|
+
if (staleBondError) {
|
|
678
|
+
throw staleBondError;
|
|
679
|
+
}
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
624
682
|
}
|
|
625
683
|
|
|
626
684
|
private async refreshBlePacketCapacity(uuid: string): Promise<void> {
|
|
@@ -694,9 +752,29 @@ export default class ElectronBleTransport {
|
|
|
694
752
|
this.handleProtocolV2Notification(deviceId, hexData);
|
|
695
753
|
return;
|
|
696
754
|
}
|
|
755
|
+
const linkDisabledError = this.readProtocolV2LinkDisabledFailure(deviceId, hexData);
|
|
756
|
+
if (linkDisabledError) {
|
|
757
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
758
|
+
this.runPromise.reject(linkDisabledError);
|
|
759
|
+
}
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
697
762
|
this.handleProtocolV1Notification(deviceId, hexData);
|
|
698
763
|
}
|
|
699
764
|
|
|
765
|
+
private readProtocolV2LinkDisabledFailure(deviceId: string, hexData: string) {
|
|
766
|
+
if (!this._messages || !this._messagesV2) return undefined;
|
|
767
|
+
|
|
768
|
+
const assembler = this.v2Assemblers.get(deviceId);
|
|
769
|
+
if (!assembler) return undefined;
|
|
770
|
+
|
|
771
|
+
return detectProtocolV2LinkDisabledError({
|
|
772
|
+
schemas: { protocolV1: this._messages, protocolV2: this._messagesV2 },
|
|
773
|
+
assembler,
|
|
774
|
+
bytes: hexToBytes(hexData),
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
700
778
|
private handleProtocolV2Notification(deviceId: string, hexData: string): void {
|
|
701
779
|
try {
|
|
702
780
|
const bytes = hexToBytes(hexData);
|
|
@@ -806,16 +884,25 @@ export default class ElectronBleTransport {
|
|
|
806
884
|
`Device protocol has not been detected for ${uuid}`
|
|
807
885
|
);
|
|
808
886
|
}
|
|
809
|
-
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
810
|
-
this.Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
811
|
-
}
|
|
812
|
-
|
|
813
887
|
if (protocol === 'V2') {
|
|
814
888
|
return this.callProtocolV2(uuid, name, data, options);
|
|
815
889
|
}
|
|
816
890
|
return this.callProtocolV1(uuid, name, data, options);
|
|
817
891
|
}
|
|
818
892
|
|
|
893
|
+
async post(uuid: string, name: string, data: Record<string, unknown>) {
|
|
894
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
895
|
+
await this.protocolV2Links.sendFlowControl(
|
|
896
|
+
uuid,
|
|
897
|
+
() => this.createProtocolV2Adapter(uuid),
|
|
898
|
+
name,
|
|
899
|
+
data
|
|
900
|
+
);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
await this.callProtocolV1(uuid, name, data);
|
|
904
|
+
}
|
|
905
|
+
|
|
819
906
|
private async callProtocolV1(
|
|
820
907
|
uuid: string,
|
|
821
908
|
name: string,
|