@onekeyfe/hd-transport-web-device 1.2.0-alpha.13 → 1.2.0-alpha.131
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 +377 -22
- package/__tests__/webusb-protocol-v2-timeout.test.ts +357 -11
- package/dist/ble-packet-capacity.d.ts +2 -0
- package/dist/ble-packet-capacity.d.ts.map +1 -0
- package/dist/electron-ble-transport.d.ts +16 -3
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +110 -12
- package/dist/index.js +345 -255
- package/dist/transportLog.d.ts +1 -6
- package/dist/transportLog.d.ts.map +1 -1
- package/dist/webusb.d.ts +15 -10
- package/dist/webusb.d.ts.map +1 -1
- package/jest.config.js +5 -0
- package/package.json +6 -5
- package/src/ble-packet-capacity.ts +13 -0
- package/src/electron-ble-transport.ts +241 -104
- package/src/transportLog.ts +1 -11
- package/src/webusb.ts +173 -189
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import transport, {
|
|
2
2
|
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
3
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
3
4
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
4
5
|
ProtocolV2FrameAssembler,
|
|
5
6
|
ProtocolV2LinkManager,
|
|
7
|
+
TRANSPORT_EVENT,
|
|
6
8
|
bytesToHex,
|
|
7
9
|
hexToBytes,
|
|
8
10
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
11
|
+
writeProtocolV2BleFrame,
|
|
9
12
|
} from '@onekeyfe/hd-transport';
|
|
10
13
|
import {
|
|
11
14
|
ERRORS,
|
|
@@ -13,14 +16,18 @@ import {
|
|
|
13
16
|
HardwareErrorCodeMessage,
|
|
14
17
|
createDeferred,
|
|
15
18
|
isHeaderChunk,
|
|
16
|
-
wait,
|
|
17
19
|
} from '@onekeyfe/hd-shared';
|
|
18
20
|
|
|
19
|
-
import {
|
|
21
|
+
import { resolveBlePacketCapacity } from './ble-packet-capacity';
|
|
20
22
|
|
|
21
23
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
22
24
|
import type { DesktopAPI } from '@onekeyfe/hd-transport-electron';
|
|
23
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
OneKeyDeviceInfo,
|
|
27
|
+
ProtocolType,
|
|
28
|
+
ProtocolV2CallContext,
|
|
29
|
+
TransportCallOptions,
|
|
30
|
+
} from '@onekeyfe/hd-transport';
|
|
24
31
|
import type EventEmitter from 'events';
|
|
25
32
|
|
|
26
33
|
const { parseConfigure, ProtocolV1, check } = transport;
|
|
@@ -35,6 +42,7 @@ export type BleAcquireInput = {
|
|
|
35
42
|
uuid: string;
|
|
36
43
|
forceCleanRunPromise?: boolean;
|
|
37
44
|
expectedProtocol?: ProtocolType;
|
|
45
|
+
protocolHint?: ProtocolType;
|
|
38
46
|
};
|
|
39
47
|
|
|
40
48
|
interface PacketProcessResult {
|
|
@@ -60,29 +68,33 @@ const toBleDescriptor = (
|
|
|
60
68
|
...(protocolType ? { protocolType } : {}),
|
|
61
69
|
} as OneKeyDeviceInfo);
|
|
62
70
|
|
|
63
|
-
const
|
|
71
|
+
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
72
|
+
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
64
73
|
const BLE_WRITE_DELAY_MS = 5;
|
|
65
|
-
const
|
|
66
|
-
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
74
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 3000;
|
|
67
75
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
68
76
|
|
|
69
77
|
/**
|
|
70
78
|
* Desktop Electron BLE transport with automatic Protocol V1/V2 detection.
|
|
71
79
|
*
|
|
72
80
|
* Protocol V1 devices continue using chunked packets. Protocol V2 is detected
|
|
73
|
-
* after a Protocol V1
|
|
81
|
+
* after a Protocol V1 GetFeatures timeout by probing Protocol V2 Ping.
|
|
74
82
|
*/
|
|
75
83
|
export default class ElectronBleTransport {
|
|
76
84
|
private _messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
77
85
|
|
|
78
86
|
private _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
79
87
|
|
|
88
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
89
|
+
|
|
80
90
|
name = 'ElectronBleTransport';
|
|
81
91
|
|
|
82
92
|
configured = false;
|
|
83
93
|
|
|
84
94
|
runPromise: Deferred<Uint8Array | string> | null = null;
|
|
85
95
|
|
|
96
|
+
private runPromiseDeviceId: string | null = null;
|
|
97
|
+
|
|
86
98
|
Log?: any;
|
|
87
99
|
|
|
88
100
|
emitter?: EventEmitter;
|
|
@@ -93,6 +105,10 @@ export default class ElectronBleTransport {
|
|
|
93
105
|
|
|
94
106
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
95
107
|
|
|
108
|
+
private deviceMtus: Map<string, number> = new Map();
|
|
109
|
+
|
|
110
|
+
private devicePacketCapacities: Map<string, number> = new Map();
|
|
111
|
+
|
|
96
112
|
private v1Buffers: Map<string, { buffer: number[]; bufferLength: number }> = new Map();
|
|
97
113
|
|
|
98
114
|
private v2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
@@ -117,13 +133,18 @@ export default class ElectronBleTransport {
|
|
|
117
133
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
118
134
|
this.Log?.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
|
|
119
135
|
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
120
|
-
await this.
|
|
136
|
+
await this.releaseNative(uuid);
|
|
121
137
|
}
|
|
122
138
|
},
|
|
123
139
|
});
|
|
124
140
|
|
|
141
|
+
/** One-shot guard so a missing preload `release` bridge is reported once. */
|
|
142
|
+
private warnedMissingRelease = false;
|
|
143
|
+
|
|
125
144
|
private notificationCleanups: Map<string, () => void> = new Map();
|
|
126
145
|
|
|
146
|
+
private mtuCleanups: Map<string, () => void> = new Map();
|
|
147
|
+
|
|
127
148
|
private disconnectCleanups: Map<string, () => void> = new Map();
|
|
128
149
|
|
|
129
150
|
private notificationTokens: Map<string, number> = new Map();
|
|
@@ -167,12 +188,19 @@ export default class ElectronBleTransport {
|
|
|
167
188
|
.catch(error => this.Log?.debug('[Electron BLE] link cleanup failed:', error));
|
|
168
189
|
this.connectedDevices.delete(deviceId);
|
|
169
190
|
this.deviceProtocol.delete(deviceId);
|
|
191
|
+
this.deviceMtus.delete(deviceId);
|
|
192
|
+
this.devicePacketCapacities.delete(deviceId);
|
|
170
193
|
// Keep deviceProtocolHints — it's inferred from device name (e.g. "Pro 2" → V2)
|
|
171
194
|
// and doesn't depend on connection state. Preserving it avoids redundant V1 probe on reconnect.
|
|
172
195
|
this.v1Buffers.delete(deviceId);
|
|
173
196
|
this.v2Assemblers.delete(deviceId);
|
|
174
197
|
this.resetProtocolV2Frames(deviceId);
|
|
175
198
|
this.notificationTokens.delete(deviceId);
|
|
199
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
200
|
+
this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
201
|
+
this.runPromise = null;
|
|
202
|
+
this.runPromiseDeviceId = null;
|
|
203
|
+
}
|
|
176
204
|
|
|
177
205
|
const notifyCleanup = this.notificationCleanups.get(deviceId);
|
|
178
206
|
if (notifyCleanup) {
|
|
@@ -180,6 +208,12 @@ export default class ElectronBleTransport {
|
|
|
180
208
|
this.notificationCleanups.delete(deviceId);
|
|
181
209
|
}
|
|
182
210
|
|
|
211
|
+
const mtuCleanup = this.mtuCleanups.get(deviceId);
|
|
212
|
+
if (mtuCleanup) {
|
|
213
|
+
mtuCleanup();
|
|
214
|
+
this.mtuCleanups.delete(deviceId);
|
|
215
|
+
}
|
|
216
|
+
|
|
183
217
|
const disconnectCleanup = this.disconnectCleanups.get(deviceId);
|
|
184
218
|
if (disconnectCleanup) {
|
|
185
219
|
disconnectCleanup();
|
|
@@ -207,10 +241,19 @@ export default class ElectronBleTransport {
|
|
|
207
241
|
}
|
|
208
242
|
|
|
209
243
|
configureProtocolV2(signedData: any) {
|
|
244
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
245
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
210
250
|
this._messagesV2 = parseConfigure(signedData);
|
|
211
|
-
this.
|
|
212
|
-
|
|
213
|
-
|
|
251
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
252
|
+
if (isReconfiguration) {
|
|
253
|
+
this.protocolV2Links
|
|
254
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
255
|
+
.catch(error => this.Log?.debug('[Electron BLE] schema link cleanup failed:', error));
|
|
256
|
+
}
|
|
214
257
|
}
|
|
215
258
|
|
|
216
259
|
async listen() {
|
|
@@ -252,8 +295,8 @@ export default class ElectronBleTransport {
|
|
|
252
295
|
if (forceCleanRunPromise && this.runPromise) {
|
|
253
296
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
254
297
|
this.runPromise.reject(error);
|
|
255
|
-
this.rejectAllProtocolV2Frames(error);
|
|
256
298
|
this.runPromise = null;
|
|
299
|
+
this.runPromiseDeviceId = null;
|
|
257
300
|
}
|
|
258
301
|
|
|
259
302
|
try {
|
|
@@ -267,7 +310,9 @@ export default class ElectronBleTransport {
|
|
|
267
310
|
}
|
|
268
311
|
const protocolHint = expectedProtocol
|
|
269
312
|
? undefined
|
|
270
|
-
:
|
|
313
|
+
: input.protocolHint ??
|
|
314
|
+
this.deviceProtocolHints.get(uuid) ??
|
|
315
|
+
inferProtocolHintFromDeviceName(device.name);
|
|
271
316
|
if (protocolHint) {
|
|
272
317
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
273
318
|
}
|
|
@@ -279,19 +324,38 @@ export default class ElectronBleTransport {
|
|
|
279
324
|
this.handleBluetoothError(error);
|
|
280
325
|
}
|
|
281
326
|
|
|
327
|
+
const mtuCleanup = this.createMtuSubscription(uuid);
|
|
328
|
+
if (mtuCleanup) {
|
|
329
|
+
this.mtuCleanups.set(uuid, mtuCleanup);
|
|
330
|
+
}
|
|
331
|
+
|
|
282
332
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
283
|
-
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
333
|
+
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
|
|
284
334
|
|
|
285
335
|
await window.desktopApi.nobleBle.subscribe(uuid);
|
|
336
|
+
await this.refreshBlePacketCapacity(uuid);
|
|
286
337
|
|
|
287
338
|
const cleanup = this.createNotificationSubscription(uuid);
|
|
288
339
|
this.notificationCleanups.set(uuid, cleanup);
|
|
340
|
+
const connectionToken = this.notificationTokens.get(uuid);
|
|
341
|
+
|
|
342
|
+
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
343
|
+
if (protocolType === 'V2') {
|
|
344
|
+
this.Log?.debug('[Electron BLE] Protocol V2 write configured', {
|
|
345
|
+
writeMode: 'withoutResponse',
|
|
346
|
+
negotiatedMtu: this.deviceMtus.get(uuid),
|
|
347
|
+
packetCapacity: this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
289
350
|
|
|
290
351
|
const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
|
|
291
352
|
(disconnectedDevice: any) => {
|
|
292
|
-
if (
|
|
353
|
+
if (
|
|
354
|
+
disconnectedDevice.id === uuid &&
|
|
355
|
+
this.notificationTokens.get(uuid) === connectionToken
|
|
356
|
+
) {
|
|
293
357
|
this.cleanupDeviceState(uuid);
|
|
294
|
-
this.emitter?.emit(
|
|
358
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
295
359
|
name: disconnectedDevice.name,
|
|
296
360
|
id: disconnectedDevice.id,
|
|
297
361
|
connectId: disconnectedDevice.id,
|
|
@@ -301,14 +365,6 @@ export default class ElectronBleTransport {
|
|
|
301
365
|
);
|
|
302
366
|
this.disconnectCleanups.set(uuid, disconnectCleanup);
|
|
303
367
|
|
|
304
|
-
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
305
|
-
|
|
306
|
-
this.emitter?.emit('device-connect', {
|
|
307
|
-
name: device.name,
|
|
308
|
-
id: device.id,
|
|
309
|
-
connectId: device.id,
|
|
310
|
-
});
|
|
311
|
-
|
|
312
368
|
return {
|
|
313
369
|
...toBleDescriptor({ id: device.id, name: device.name }, protocolType),
|
|
314
370
|
uuid,
|
|
@@ -328,9 +384,25 @@ export default class ElectronBleTransport {
|
|
|
328
384
|
}
|
|
329
385
|
}
|
|
330
386
|
|
|
331
|
-
async release(id: string) {
|
|
387
|
+
async release(id: string, _onclose?: boolean, keepSession?: boolean) {
|
|
332
388
|
try {
|
|
333
389
|
await this.protocolV2Links.invalidateLink(id, 'Electron BLE transport released');
|
|
390
|
+
await this.releaseLogical(id, keepSession);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
this.Log?.error('[Electron BLE] release failed:', error);
|
|
393
|
+
this.cleanupDeviceState(id);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// DeviceConnector.disconnect feature-detects this; without it REQUIRE_DISCONNECT
|
|
398
|
+
// recovery is a no-op and keep-alive hands the wedged link to every retry.
|
|
399
|
+
async disconnect(id: string) {
|
|
400
|
+
return this.releaseNative(id);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Hard teardown, error paths only: a link presumed dead must not be reused.
|
|
404
|
+
private async releaseNative(id: string) {
|
|
405
|
+
try {
|
|
334
406
|
if (this.connectedDevices.has(id)) {
|
|
335
407
|
if (window.desktopApi?.nobleBle) {
|
|
336
408
|
await window.desktopApi.nobleBle.unsubscribe(id);
|
|
@@ -344,6 +416,32 @@ export default class ElectronBleTransport {
|
|
|
344
416
|
}
|
|
345
417
|
}
|
|
346
418
|
|
|
419
|
+
// Logical release: link and subscription stay up for the next call. Renderer
|
|
420
|
+
// listeners are still torn down, or fresh ones would double-process packets.
|
|
421
|
+
private async releaseLogical(id: string, keepSession?: boolean) {
|
|
422
|
+
try {
|
|
423
|
+
if (!this.connectedDevices.has(id)) return;
|
|
424
|
+
this.cleanupDeviceState(id);
|
|
425
|
+
|
|
426
|
+
const release = window.desktopApi?.nobleBle?.release;
|
|
427
|
+
if (!release) {
|
|
428
|
+
// Degraded, not broken: say it once, it just looks like a slow device.
|
|
429
|
+
if (!this.warnedMissingRelease) {
|
|
430
|
+
this.warnedMissingRelease = true;
|
|
431
|
+
this.Log?.error(
|
|
432
|
+
'[Electron BLE] desktopApi.nobleBle.release is missing from the preload bridge; ' +
|
|
433
|
+
'keep-alive idle countdown will never start — map NOBLE_BLE_RELEASE in the desktop preload'
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
await release(id, keepSession);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
this.Log?.error('[Electron BLE] logical release failed:', error);
|
|
441
|
+
this.cleanupDeviceState(id);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
347
445
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
348
446
|
return ERRORS.TypedError(
|
|
349
447
|
HardwareErrorCode.RuntimeError,
|
|
@@ -354,7 +452,7 @@ export default class ElectronBleTransport {
|
|
|
354
452
|
private createProtocolDetectionError() {
|
|
355
453
|
return ERRORS.TypedError(
|
|
356
454
|
HardwareErrorCode.BleTimeoutError,
|
|
357
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
455
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
358
456
|
);
|
|
359
457
|
}
|
|
360
458
|
|
|
@@ -379,23 +477,24 @@ export default class ElectronBleTransport {
|
|
|
379
477
|
}
|
|
380
478
|
|
|
381
479
|
if (expectedProtocol === 'V2') {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
480
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
481
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
482
|
+
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
|
|
483
|
+
return 'V2';
|
|
484
|
+
}
|
|
485
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
387
486
|
}
|
|
388
487
|
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
// 不能作为最终结论。
|
|
488
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
489
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
392
490
|
const probeOrder: ProtocolType[] =
|
|
393
491
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
394
492
|
|
|
395
493
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
396
494
|
const protocol = probeOrder[i];
|
|
397
495
|
if (i > 0) {
|
|
398
|
-
//
|
|
496
|
+
// Keep the physical BLE link while switching probes. Reconnecting here can
|
|
497
|
+
// summon a second OS pairing prompt for a device that is still onboarding.
|
|
399
498
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
400
499
|
}
|
|
401
500
|
const detected =
|
|
@@ -435,33 +534,20 @@ export default class ElectronBleTransport {
|
|
|
435
534
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
436
535
|
this.v2Assemblers.get(uuid)?.reset();
|
|
437
536
|
this.resetProtocolV2Frames(uuid);
|
|
438
|
-
if (this.runPromise) {
|
|
537
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
439
538
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
440
539
|
this.runPromise.reject(error);
|
|
441
540
|
this.runPromise = null;
|
|
541
|
+
this.runPromiseDeviceId = null;
|
|
442
542
|
}
|
|
443
543
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
try {
|
|
452
|
-
await window.desktopApi?.nobleBle?.unsubscribe(uuid);
|
|
453
|
-
} catch (error) {
|
|
454
|
-
this.Log?.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error);
|
|
455
|
-
}
|
|
456
|
-
try {
|
|
457
|
-
await window.desktopApi?.nobleBle?.subscribe(uuid);
|
|
458
|
-
} catch (error) {
|
|
459
|
-
this.Log?.debug(`[Electron BLE] resubscribe after Protocol ${protocol} probe failed:`, error);
|
|
460
|
-
throw error;
|
|
544
|
+
// A timed-out V1 probe retires its renderer notification token so a late V1
|
|
545
|
+
// response cannot satisfy the V2 probe. Install a fresh listener without
|
|
546
|
+
// touching the native GATT subscription or physical connection.
|
|
547
|
+
if (!this.notificationCleanups.has(uuid)) {
|
|
548
|
+
const cleanup = this.createNotificationSubscription(uuid);
|
|
549
|
+
this.notificationCleanups.set(uuid, cleanup);
|
|
461
550
|
}
|
|
462
|
-
|
|
463
|
-
const cleanup = this.createNotificationSubscription(uuid);
|
|
464
|
-
this.notificationCleanups.set(uuid, cleanup);
|
|
465
551
|
}
|
|
466
552
|
|
|
467
553
|
private async probeProtocolV1(uuid: string) {
|
|
@@ -471,11 +557,13 @@ export default class ElectronBleTransport {
|
|
|
471
557
|
|
|
472
558
|
try {
|
|
473
559
|
this.deviceProtocol.set(uuid, 'V1');
|
|
474
|
-
|
|
560
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
561
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
562
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
475
563
|
return true;
|
|
476
564
|
} catch (error) {
|
|
477
565
|
this.clearProbeProtocol(uuid, 'V1');
|
|
478
|
-
this.Log?.debug('[Electron BLE] Protocol V1
|
|
566
|
+
this.Log?.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
|
|
479
567
|
return false;
|
|
480
568
|
}
|
|
481
569
|
}
|
|
@@ -503,35 +591,63 @@ export default class ElectronBleTransport {
|
|
|
503
591
|
return detected;
|
|
504
592
|
}
|
|
505
593
|
|
|
506
|
-
private async
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
await wait(BLE_WRITE_DELAY_MS);
|
|
511
|
-
await this.writeOnce(uuid, hexData);
|
|
512
|
-
return;
|
|
594
|
+
private async writeOnce(uuid: string, hexData: string): Promise<void> {
|
|
595
|
+
const nobleBle = window.desktopApi?.nobleBle;
|
|
596
|
+
if (!nobleBle) {
|
|
597
|
+
throw new Error('Noble BLE API not available');
|
|
513
598
|
}
|
|
514
599
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const chunkHex = hexData.substring(offset, offset + chunkHexLen);
|
|
518
|
-
offset += chunkHexLen;
|
|
600
|
+
await nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
|
|
601
|
+
}
|
|
519
602
|
|
|
520
|
-
|
|
603
|
+
private async refreshBlePacketCapacity(uuid: string): Promise<void> {
|
|
604
|
+
const device = await window.desktopApi?.nobleBle?.getDevice(uuid);
|
|
605
|
+
this.updateBlePacketCapacity(uuid, device?.mtu);
|
|
606
|
+
}
|
|
521
607
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
608
|
+
private updateBlePacketCapacity(uuid: string, mtu?: number): void {
|
|
609
|
+
const packetCapacity = resolveBlePacketCapacity(
|
|
610
|
+
mtu,
|
|
611
|
+
BLE_PACKET_SIZE_MAXIMUM,
|
|
612
|
+
BLE_PACKET_SIZE_FALLBACK
|
|
613
|
+
);
|
|
614
|
+
if (typeof mtu === 'number') {
|
|
615
|
+
this.deviceMtus.set(uuid, mtu);
|
|
616
|
+
} else {
|
|
617
|
+
this.deviceMtus.delete(uuid);
|
|
525
618
|
}
|
|
619
|
+
this.devicePacketCapacities.set(uuid, packetCapacity);
|
|
526
620
|
}
|
|
527
621
|
|
|
528
|
-
private
|
|
529
|
-
const
|
|
530
|
-
if (!
|
|
531
|
-
|
|
532
|
-
|
|
622
|
+
private createMtuSubscription(uuid: string): (() => void) | undefined {
|
|
623
|
+
const onMtuChanged = window.desktopApi?.nobleBle?.onMtuChanged;
|
|
624
|
+
if (!onMtuChanged) return undefined;
|
|
625
|
+
return onMtuChanged(device => {
|
|
626
|
+
if (device.id === uuid) {
|
|
627
|
+
this.updateBlePacketCapacity(uuid, device.mtu);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
533
631
|
|
|
534
|
-
|
|
632
|
+
private writeProtocolV2Frame(
|
|
633
|
+
uuid: string,
|
|
634
|
+
frame: Uint8Array,
|
|
635
|
+
context: ProtocolV2CallContext,
|
|
636
|
+
assertCurrentGeneration: () => void
|
|
637
|
+
) {
|
|
638
|
+
const packetCapacity = this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK;
|
|
639
|
+
const shouldPace = !context.highThroughput;
|
|
640
|
+
return writeProtocolV2BleFrame({
|
|
641
|
+
frame,
|
|
642
|
+
packetCapacity,
|
|
643
|
+
assertActive: assertCurrentGeneration,
|
|
644
|
+
signal: context.signal,
|
|
645
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
646
|
+
initialDelayMs: shouldPace && frame.length <= packetCapacity ? BLE_WRITE_DELAY_MS : 0,
|
|
647
|
+
burstSize: shouldPace ? 1 : undefined,
|
|
648
|
+
burstPauseMs: shouldPace ? BLE_WRITE_DELAY_MS : 0,
|
|
649
|
+
writePacket: packet => this.writeOnce(uuid, bytesToHex(packet)),
|
|
650
|
+
});
|
|
535
651
|
}
|
|
536
652
|
|
|
537
653
|
private handleNotification(deviceId: string, hexData: string): void {
|
|
@@ -540,7 +656,7 @@ export default class ElectronBleTransport {
|
|
|
540
656
|
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled);
|
|
541
657
|
if (this.deviceProtocol.get(deviceId) === 'V2') {
|
|
542
658
|
this.rejectProtocolV2Frames(deviceId, error);
|
|
543
|
-
} else if (this.runPromise) {
|
|
659
|
+
} else if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
544
660
|
this.runPromise.reject(error);
|
|
545
661
|
}
|
|
546
662
|
return;
|
|
@@ -595,17 +711,8 @@ export default class ElectronBleTransport {
|
|
|
595
711
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
596
712
|
}
|
|
597
713
|
|
|
598
|
-
private rejectAllProtocolV2Frames(error: Error) {
|
|
599
|
-
this.v2FrameQueues.clear();
|
|
600
|
-
for (const framePromise of this.v2FramePromises.values()) {
|
|
601
|
-
framePromise.reject(error);
|
|
602
|
-
}
|
|
603
|
-
this.v2FramePromises.clear();
|
|
604
|
-
}
|
|
605
|
-
|
|
606
714
|
private resetProtocolV2Frames(uuid: string) {
|
|
607
|
-
this.
|
|
608
|
-
this.v2FramePromises.delete(uuid);
|
|
715
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
609
716
|
}
|
|
610
717
|
|
|
611
718
|
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
@@ -639,13 +746,18 @@ export default class ElectronBleTransport {
|
|
|
639
746
|
|
|
640
747
|
if (result.error) {
|
|
641
748
|
this.Log?.error('[Electron BLE] Protocol V1 packet processing error:', result.error);
|
|
642
|
-
if (this.runPromise) {
|
|
749
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
643
750
|
this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
|
|
644
751
|
}
|
|
645
752
|
return;
|
|
646
753
|
}
|
|
647
754
|
|
|
648
|
-
if (
|
|
755
|
+
if (
|
|
756
|
+
result.isComplete &&
|
|
757
|
+
result.completePacket &&
|
|
758
|
+
this.runPromise &&
|
|
759
|
+
this.runPromiseDeviceId === deviceId
|
|
760
|
+
) {
|
|
649
761
|
this.runPromise.resolve(result.completePacket);
|
|
650
762
|
}
|
|
651
763
|
}
|
|
@@ -671,16 +783,25 @@ export default class ElectronBleTransport {
|
|
|
671
783
|
`Device protocol has not been detected for ${uuid}`
|
|
672
784
|
);
|
|
673
785
|
}
|
|
674
|
-
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
675
|
-
this.Log?.debug('transport call', createTransportCallLog(name, protocol));
|
|
676
|
-
}
|
|
677
|
-
|
|
678
786
|
if (protocol === 'V2') {
|
|
679
787
|
return this.callProtocolV2(uuid, name, data, options);
|
|
680
788
|
}
|
|
681
789
|
return this.callProtocolV1(uuid, name, data, options);
|
|
682
790
|
}
|
|
683
791
|
|
|
792
|
+
async post(uuid: string, name: string, data: Record<string, unknown>) {
|
|
793
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
794
|
+
await this.protocolV2Links.sendFlowControl(
|
|
795
|
+
uuid,
|
|
796
|
+
() => this.createProtocolV2Adapter(uuid),
|
|
797
|
+
name,
|
|
798
|
+
data
|
|
799
|
+
);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
await this.callProtocolV1(uuid, name, data);
|
|
803
|
+
}
|
|
804
|
+
|
|
684
805
|
private async callProtocolV1(
|
|
685
806
|
uuid: string,
|
|
686
807
|
name: string,
|
|
@@ -699,6 +820,7 @@ export default class ElectronBleTransport {
|
|
|
699
820
|
const runPromise = createDeferred<Uint8Array | string>();
|
|
700
821
|
runPromise.promise.catch(() => undefined);
|
|
701
822
|
this.runPromise = runPromise;
|
|
823
|
+
this.runPromiseDeviceId = uuid;
|
|
702
824
|
const messages = this._messages;
|
|
703
825
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
704
826
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -743,11 +865,24 @@ export default class ElectronBleTransport {
|
|
|
743
865
|
return check.call(jsonData);
|
|
744
866
|
} catch (e) {
|
|
745
867
|
this.Log?.error('[Electron BLE] Protocol V1 call error:', e);
|
|
868
|
+
const isProbeTimeout =
|
|
869
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
870
|
+
if ((e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError) {
|
|
871
|
+
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
872
|
+
const notifyCleanup = this.notificationCleanups.get(uuid);
|
|
873
|
+
notifyCleanup?.();
|
|
874
|
+
this.notificationCleanups.delete(uuid);
|
|
875
|
+
this.notificationTokens.delete(uuid);
|
|
876
|
+
if (!isProbeTimeout) {
|
|
877
|
+
await this.releaseNative(uuid);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
746
880
|
throw e;
|
|
747
881
|
} finally {
|
|
748
882
|
if (timeout) clearTimeout(timeout);
|
|
749
883
|
if (this.runPromise === runPromise) {
|
|
750
884
|
this.runPromise = null;
|
|
885
|
+
this.runPromiseDeviceId = null;
|
|
751
886
|
}
|
|
752
887
|
}
|
|
753
888
|
}
|
|
@@ -762,10 +897,7 @@ export default class ElectronBleTransport {
|
|
|
762
897
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
763
898
|
}
|
|
764
899
|
|
|
765
|
-
const callOptions =
|
|
766
|
-
...options,
|
|
767
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
768
|
-
};
|
|
900
|
+
const callOptions = options;
|
|
769
901
|
|
|
770
902
|
try {
|
|
771
903
|
return await this.protocolV2Links.call(
|
|
@@ -791,16 +923,15 @@ export default class ElectronBleTransport {
|
|
|
791
923
|
|
|
792
924
|
return {
|
|
793
925
|
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
926
|
+
maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
794
927
|
generation,
|
|
795
928
|
prepareCall: () => {
|
|
796
929
|
assertCurrentGeneration();
|
|
797
930
|
this.v2Assemblers.get(uuid)?.reset();
|
|
798
931
|
this.resetProtocolV2Frames(uuid);
|
|
799
932
|
},
|
|
800
|
-
writeFrame: (frame: Uint8Array) =>
|
|
801
|
-
assertCurrentGeneration
|
|
802
|
-
return this.writeWithChunking(uuid, bytesToHex(frame));
|
|
803
|
-
},
|
|
933
|
+
writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) =>
|
|
934
|
+
this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
|
|
804
935
|
readFrame: async () => {
|
|
805
936
|
assertCurrentGeneration();
|
|
806
937
|
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
@@ -844,6 +975,12 @@ export default class ElectronBleTransport {
|
|
|
844
975
|
bufferState.bufferLength = dataView.getInt32(5, false);
|
|
845
976
|
bufferState.buffer = [...data.subarray(3)];
|
|
846
977
|
} else {
|
|
978
|
+
if (bufferState.buffer.length === 0) {
|
|
979
|
+
// Tail of a cancelled call, arriving after the buffer was reset. Not an
|
|
980
|
+
// `error`: the caller rejects the in-flight call on that field.
|
|
981
|
+
this.Log?.debug('[Electron BLE] Orphan continuation chunk discarded');
|
|
982
|
+
return { isComplete: false };
|
|
983
|
+
}
|
|
847
984
|
bufferState.buffer = bufferState.buffer.concat([...data]);
|
|
848
985
|
}
|
|
849
986
|
|
package/src/transportLog.ts
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const HIGH_VOLUME_CALLS = new Set(['FileWrite', 'FilesystemFileWrite', 'EmmcFileWrite']);
|
|
4
|
-
|
|
5
|
-
export function shouldSuppressHighVolumeCallLog(name: string) {
|
|
6
|
-
return HIGH_VOLUME_CALLS.has(name);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function createTransportCallLog(name: string, protocol: ProtocolType) {
|
|
10
|
-
return { name, protocol };
|
|
11
|
-
}
|
|
1
|
+
export { createTransportCallLog, shouldSuppressHighVolumeCallLog } from '@onekeyfe/hd-transport';
|