@onekeyfe/hd-transport-web-device 1.2.0-alpha.9 → 1.2.0-alpha.91
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 +373 -22
- package/__tests__/webusb-protocol-v2-timeout.test.ts +385 -0
- 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 +15 -3
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +109 -12
- package/dist/index.js +344 -251
- package/dist/transportLog.d.ts +2 -0
- package/dist/transportLog.d.ts.map +1 -0
- 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 +234 -98
- package/src/transportLog.ts +1 -0
- package/src/webusb.ts +184 -217
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import transport, {
|
|
2
|
-
LogBlockCommand,
|
|
3
2
|
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
3
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
4
4
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
5
5
|
ProtocolV2FrameAssembler,
|
|
6
6
|
ProtocolV2LinkManager,
|
|
7
|
+
TRANSPORT_EVENT,
|
|
7
8
|
bytesToHex,
|
|
8
9
|
hexToBytes,
|
|
9
10
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
11
|
+
writeProtocolV2BleFrame,
|
|
10
12
|
} from '@onekeyfe/hd-transport';
|
|
11
13
|
import {
|
|
12
14
|
ERRORS,
|
|
@@ -14,25 +16,21 @@ import {
|
|
|
14
16
|
HardwareErrorCodeMessage,
|
|
15
17
|
createDeferred,
|
|
16
18
|
isHeaderChunk,
|
|
17
|
-
wait,
|
|
18
19
|
} from '@onekeyfe/hd-shared';
|
|
19
20
|
|
|
21
|
+
import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
|
|
22
|
+
import { resolveBlePacketCapacity } from './ble-packet-capacity';
|
|
23
|
+
|
|
20
24
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
21
25
|
import type { DesktopAPI } from '@onekeyfe/hd-transport-electron';
|
|
22
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
OneKeyDeviceInfo,
|
|
28
|
+
ProtocolType,
|
|
29
|
+
ProtocolV2CallContext,
|
|
30
|
+
TransportCallOptions,
|
|
31
|
+
} from '@onekeyfe/hd-transport';
|
|
23
32
|
import type EventEmitter from 'events';
|
|
24
33
|
|
|
25
|
-
const FILE_WRITE_LOG_BLOCK_PATTERN = /(?:^|[^a-z])(?:raw)?(?:filesystem|emmc)?filewrite$/i;
|
|
26
|
-
|
|
27
|
-
function shouldSuppressHighVolumeCallLog(name: string) {
|
|
28
|
-
const normalized = name.replace(/[_\s-]/g, '');
|
|
29
|
-
return FILE_WRITE_LOG_BLOCK_PATTERN.test(normalized);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isLogBlockCommand(name: string) {
|
|
33
|
-
return (LogBlockCommand as Set<string> | undefined)?.has?.(name) ?? false;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
34
|
const { parseConfigure, ProtocolV1, check } = transport;
|
|
37
35
|
|
|
38
36
|
declare global {
|
|
@@ -45,6 +43,7 @@ export type BleAcquireInput = {
|
|
|
45
43
|
uuid: string;
|
|
46
44
|
forceCleanRunPromise?: boolean;
|
|
47
45
|
expectedProtocol?: ProtocolType;
|
|
46
|
+
protocolHint?: ProtocolType;
|
|
48
47
|
};
|
|
49
48
|
|
|
50
49
|
interface PacketProcessResult {
|
|
@@ -70,9 +69,9 @@ const toBleDescriptor = (
|
|
|
70
69
|
...(protocolType ? { protocolType } : {}),
|
|
71
70
|
} as OneKeyDeviceInfo);
|
|
72
71
|
|
|
73
|
-
const
|
|
72
|
+
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
73
|
+
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
74
74
|
const BLE_WRITE_DELAY_MS = 5;
|
|
75
|
-
const BLE_RESPONSE_TIMEOUT_MS = 30_000;
|
|
76
75
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
77
76
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
78
77
|
|
|
@@ -80,19 +79,23 @@ const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
|
80
79
|
* Desktop Electron BLE transport with automatic Protocol V1/V2 detection.
|
|
81
80
|
*
|
|
82
81
|
* Protocol V1 devices continue using chunked packets. Protocol V2 is detected
|
|
83
|
-
* after a Protocol V1
|
|
82
|
+
* after a Protocol V1 GetFeatures timeout by probing Protocol V2 Ping.
|
|
84
83
|
*/
|
|
85
84
|
export default class ElectronBleTransport {
|
|
86
85
|
private _messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
87
86
|
|
|
88
87
|
private _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
89
88
|
|
|
89
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
90
|
+
|
|
90
91
|
name = 'ElectronBleTransport';
|
|
91
92
|
|
|
92
93
|
configured = false;
|
|
93
94
|
|
|
94
95
|
runPromise: Deferred<Uint8Array | string> | null = null;
|
|
95
96
|
|
|
97
|
+
private runPromiseDeviceId: string | null = null;
|
|
98
|
+
|
|
96
99
|
Log?: any;
|
|
97
100
|
|
|
98
101
|
emitter?: EventEmitter;
|
|
@@ -103,6 +106,10 @@ export default class ElectronBleTransport {
|
|
|
103
106
|
|
|
104
107
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
105
108
|
|
|
109
|
+
private deviceMtus: Map<string, number> = new Map();
|
|
110
|
+
|
|
111
|
+
private devicePacketCapacities: Map<string, number> = new Map();
|
|
112
|
+
|
|
106
113
|
private v1Buffers: Map<string, { buffer: number[]; bufferLength: number }> = new Map();
|
|
107
114
|
|
|
108
115
|
private v2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
@@ -127,13 +134,18 @@ export default class ElectronBleTransport {
|
|
|
127
134
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
128
135
|
this.Log?.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
|
|
129
136
|
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
130
|
-
await this.
|
|
137
|
+
await this.releaseNative(uuid);
|
|
131
138
|
}
|
|
132
139
|
},
|
|
133
140
|
});
|
|
134
141
|
|
|
142
|
+
/** One-shot guard so a missing preload `release` bridge is reported once. */
|
|
143
|
+
private warnedMissingRelease = false;
|
|
144
|
+
|
|
135
145
|
private notificationCleanups: Map<string, () => void> = new Map();
|
|
136
146
|
|
|
147
|
+
private mtuCleanups: Map<string, () => void> = new Map();
|
|
148
|
+
|
|
137
149
|
private disconnectCleanups: Map<string, () => void> = new Map();
|
|
138
150
|
|
|
139
151
|
private notificationTokens: Map<string, number> = new Map();
|
|
@@ -177,12 +189,19 @@ export default class ElectronBleTransport {
|
|
|
177
189
|
.catch(error => this.Log?.debug('[Electron BLE] link cleanup failed:', error));
|
|
178
190
|
this.connectedDevices.delete(deviceId);
|
|
179
191
|
this.deviceProtocol.delete(deviceId);
|
|
192
|
+
this.deviceMtus.delete(deviceId);
|
|
193
|
+
this.devicePacketCapacities.delete(deviceId);
|
|
180
194
|
// Keep deviceProtocolHints — it's inferred from device name (e.g. "Pro 2" → V2)
|
|
181
195
|
// and doesn't depend on connection state. Preserving it avoids redundant V1 probe on reconnect.
|
|
182
196
|
this.v1Buffers.delete(deviceId);
|
|
183
197
|
this.v2Assemblers.delete(deviceId);
|
|
184
198
|
this.resetProtocolV2Frames(deviceId);
|
|
185
199
|
this.notificationTokens.delete(deviceId);
|
|
200
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
201
|
+
this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
202
|
+
this.runPromise = null;
|
|
203
|
+
this.runPromiseDeviceId = null;
|
|
204
|
+
}
|
|
186
205
|
|
|
187
206
|
const notifyCleanup = this.notificationCleanups.get(deviceId);
|
|
188
207
|
if (notifyCleanup) {
|
|
@@ -190,6 +209,12 @@ export default class ElectronBleTransport {
|
|
|
190
209
|
this.notificationCleanups.delete(deviceId);
|
|
191
210
|
}
|
|
192
211
|
|
|
212
|
+
const mtuCleanup = this.mtuCleanups.get(deviceId);
|
|
213
|
+
if (mtuCleanup) {
|
|
214
|
+
mtuCleanup();
|
|
215
|
+
this.mtuCleanups.delete(deviceId);
|
|
216
|
+
}
|
|
217
|
+
|
|
193
218
|
const disconnectCleanup = this.disconnectCleanups.get(deviceId);
|
|
194
219
|
if (disconnectCleanup) {
|
|
195
220
|
disconnectCleanup();
|
|
@@ -217,11 +242,19 @@ export default class ElectronBleTransport {
|
|
|
217
242
|
}
|
|
218
243
|
|
|
219
244
|
configureProtocolV2(signedData: any) {
|
|
245
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
246
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
220
251
|
this._messagesV2 = parseConfigure(signedData);
|
|
221
|
-
this.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
252
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
253
|
+
if (isReconfiguration) {
|
|
254
|
+
this.protocolV2Links
|
|
255
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
256
|
+
.catch(error => this.Log?.debug('[Electron BLE] schema link cleanup failed:', error));
|
|
257
|
+
}
|
|
225
258
|
}
|
|
226
259
|
|
|
227
260
|
async listen() {
|
|
@@ -263,8 +296,8 @@ export default class ElectronBleTransport {
|
|
|
263
296
|
if (forceCleanRunPromise && this.runPromise) {
|
|
264
297
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
265
298
|
this.runPromise.reject(error);
|
|
266
|
-
this.rejectAllProtocolV2Frames(error);
|
|
267
299
|
this.runPromise = null;
|
|
300
|
+
this.runPromiseDeviceId = null;
|
|
268
301
|
}
|
|
269
302
|
|
|
270
303
|
try {
|
|
@@ -278,7 +311,9 @@ export default class ElectronBleTransport {
|
|
|
278
311
|
}
|
|
279
312
|
const protocolHint = expectedProtocol
|
|
280
313
|
? undefined
|
|
281
|
-
:
|
|
314
|
+
: input.protocolHint ??
|
|
315
|
+
this.deviceProtocolHints.get(uuid) ??
|
|
316
|
+
inferProtocolHintFromDeviceName(device.name);
|
|
282
317
|
if (protocolHint) {
|
|
283
318
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
284
319
|
}
|
|
@@ -290,19 +325,38 @@ export default class ElectronBleTransport {
|
|
|
290
325
|
this.handleBluetoothError(error);
|
|
291
326
|
}
|
|
292
327
|
|
|
328
|
+
const mtuCleanup = this.createMtuSubscription(uuid);
|
|
329
|
+
if (mtuCleanup) {
|
|
330
|
+
this.mtuCleanups.set(uuid, mtuCleanup);
|
|
331
|
+
}
|
|
332
|
+
|
|
293
333
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
294
|
-
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
334
|
+
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
|
|
295
335
|
|
|
296
336
|
await window.desktopApi.nobleBle.subscribe(uuid);
|
|
337
|
+
await this.refreshBlePacketCapacity(uuid);
|
|
297
338
|
|
|
298
339
|
const cleanup = this.createNotificationSubscription(uuid);
|
|
299
340
|
this.notificationCleanups.set(uuid, cleanup);
|
|
341
|
+
const connectionToken = this.notificationTokens.get(uuid);
|
|
342
|
+
|
|
343
|
+
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
344
|
+
if (protocolType === 'V2') {
|
|
345
|
+
this.Log?.debug('[Electron BLE] Protocol V2 write configured', {
|
|
346
|
+
writeMode: 'withoutResponse',
|
|
347
|
+
negotiatedMtu: this.deviceMtus.get(uuid),
|
|
348
|
+
packetCapacity: this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
300
351
|
|
|
301
352
|
const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
|
|
302
353
|
(disconnectedDevice: any) => {
|
|
303
|
-
if (
|
|
354
|
+
if (
|
|
355
|
+
disconnectedDevice.id === uuid &&
|
|
356
|
+
this.notificationTokens.get(uuid) === connectionToken
|
|
357
|
+
) {
|
|
304
358
|
this.cleanupDeviceState(uuid);
|
|
305
|
-
this.emitter?.emit(
|
|
359
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
306
360
|
name: disconnectedDevice.name,
|
|
307
361
|
id: disconnectedDevice.id,
|
|
308
362
|
connectId: disconnectedDevice.id,
|
|
@@ -312,14 +366,6 @@ export default class ElectronBleTransport {
|
|
|
312
366
|
);
|
|
313
367
|
this.disconnectCleanups.set(uuid, disconnectCleanup);
|
|
314
368
|
|
|
315
|
-
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
316
|
-
|
|
317
|
-
this.emitter?.emit('device-connect', {
|
|
318
|
-
name: device.name,
|
|
319
|
-
id: device.id,
|
|
320
|
-
connectId: device.id,
|
|
321
|
-
});
|
|
322
|
-
|
|
323
369
|
return {
|
|
324
370
|
...toBleDescriptor({ id: device.id, name: device.name }, protocolType),
|
|
325
371
|
uuid,
|
|
@@ -339,9 +385,25 @@ export default class ElectronBleTransport {
|
|
|
339
385
|
}
|
|
340
386
|
}
|
|
341
387
|
|
|
342
|
-
async release(id: string) {
|
|
388
|
+
async release(id: string, _onclose?: boolean, keepSession?: boolean) {
|
|
343
389
|
try {
|
|
344
390
|
await this.protocolV2Links.invalidateLink(id, 'Electron BLE transport released');
|
|
391
|
+
await this.releaseLogical(id, keepSession);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
this.Log?.error('[Electron BLE] release failed:', error);
|
|
394
|
+
this.cleanupDeviceState(id);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// DeviceConnector.disconnect feature-detects this; without it REQUIRE_DISCONNECT
|
|
399
|
+
// recovery is a no-op and keep-alive hands the wedged link to every retry.
|
|
400
|
+
async disconnect(id: string) {
|
|
401
|
+
return this.releaseNative(id);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Hard teardown, error paths only: a link presumed dead must not be reused.
|
|
405
|
+
private async releaseNative(id: string) {
|
|
406
|
+
try {
|
|
345
407
|
if (this.connectedDevices.has(id)) {
|
|
346
408
|
if (window.desktopApi?.nobleBle) {
|
|
347
409
|
await window.desktopApi.nobleBle.unsubscribe(id);
|
|
@@ -355,6 +417,32 @@ export default class ElectronBleTransport {
|
|
|
355
417
|
}
|
|
356
418
|
}
|
|
357
419
|
|
|
420
|
+
// Logical release: link and subscription stay up for the next call. Renderer
|
|
421
|
+
// listeners are still torn down, or fresh ones would double-process packets.
|
|
422
|
+
private async releaseLogical(id: string, keepSession?: boolean) {
|
|
423
|
+
try {
|
|
424
|
+
if (!this.connectedDevices.has(id)) return;
|
|
425
|
+
this.cleanupDeviceState(id);
|
|
426
|
+
|
|
427
|
+
const release = window.desktopApi?.nobleBle?.release;
|
|
428
|
+
if (!release) {
|
|
429
|
+
// Degraded, not broken: say it once, it just looks like a slow device.
|
|
430
|
+
if (!this.warnedMissingRelease) {
|
|
431
|
+
this.warnedMissingRelease = true;
|
|
432
|
+
this.Log?.error(
|
|
433
|
+
'[Electron BLE] desktopApi.nobleBle.release is missing from the preload bridge; ' +
|
|
434
|
+
'keep-alive idle countdown will never start — map NOBLE_BLE_RELEASE in the desktop preload'
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
await release(id, keepSession);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
this.Log?.error('[Electron BLE] logical release failed:', error);
|
|
442
|
+
this.cleanupDeviceState(id);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
358
446
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
359
447
|
return ERRORS.TypedError(
|
|
360
448
|
HardwareErrorCode.RuntimeError,
|
|
@@ -365,7 +453,7 @@ export default class ElectronBleTransport {
|
|
|
365
453
|
private createProtocolDetectionError() {
|
|
366
454
|
return ERRORS.TypedError(
|
|
367
455
|
HardwareErrorCode.BleTimeoutError,
|
|
368
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
456
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
369
457
|
);
|
|
370
458
|
}
|
|
371
459
|
|
|
@@ -390,23 +478,23 @@ export default class ElectronBleTransport {
|
|
|
390
478
|
}
|
|
391
479
|
|
|
392
480
|
if (expectedProtocol === 'V2') {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
481
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
482
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
483
|
+
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
|
|
484
|
+
return 'V2';
|
|
485
|
+
}
|
|
486
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
398
487
|
}
|
|
399
488
|
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// 不能作为最终结论。
|
|
489
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
490
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
403
491
|
const probeOrder: ProtocolType[] =
|
|
404
492
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
405
493
|
|
|
406
494
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
407
495
|
const protocol = probeOrder[i];
|
|
408
496
|
if (i > 0) {
|
|
409
|
-
//
|
|
497
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
410
498
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
411
499
|
}
|
|
412
500
|
const detected =
|
|
@@ -446,10 +534,11 @@ export default class ElectronBleTransport {
|
|
|
446
534
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
447
535
|
this.v2Assemblers.get(uuid)?.reset();
|
|
448
536
|
this.resetProtocolV2Frames(uuid);
|
|
449
|
-
if (this.runPromise) {
|
|
537
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
450
538
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
451
539
|
this.runPromise.reject(error);
|
|
452
540
|
this.runPromise = null;
|
|
541
|
+
this.runPromiseDeviceId = null;
|
|
453
542
|
}
|
|
454
543
|
|
|
455
544
|
const notifyCleanup = this.notificationCleanups.get(uuid);
|
|
@@ -465,9 +554,18 @@ export default class ElectronBleTransport {
|
|
|
465
554
|
this.Log?.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error);
|
|
466
555
|
}
|
|
467
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);
|
|
468
565
|
await window.desktopApi?.nobleBle?.subscribe(uuid);
|
|
566
|
+
await this.refreshBlePacketCapacity(uuid);
|
|
469
567
|
} catch (error) {
|
|
470
|
-
this.Log?.debug(`[Electron BLE]
|
|
568
|
+
this.Log?.debug(`[Electron BLE] reconnect after Protocol ${protocol} probe failed:`, error);
|
|
471
569
|
throw error;
|
|
472
570
|
}
|
|
473
571
|
|
|
@@ -482,11 +580,13 @@ export default class ElectronBleTransport {
|
|
|
482
580
|
|
|
483
581
|
try {
|
|
484
582
|
this.deviceProtocol.set(uuid, 'V1');
|
|
485
|
-
|
|
583
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
584
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
585
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
486
586
|
return true;
|
|
487
587
|
} catch (error) {
|
|
488
588
|
this.clearProbeProtocol(uuid, 'V1');
|
|
489
|
-
this.Log?.debug('[Electron BLE] Protocol V1
|
|
589
|
+
this.Log?.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
|
|
490
590
|
return false;
|
|
491
591
|
}
|
|
492
592
|
}
|
|
@@ -514,35 +614,63 @@ export default class ElectronBleTransport {
|
|
|
514
614
|
return detected;
|
|
515
615
|
}
|
|
516
616
|
|
|
517
|
-
private async
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
await wait(BLE_WRITE_DELAY_MS);
|
|
522
|
-
await this.writeOnce(uuid, hexData);
|
|
523
|
-
return;
|
|
617
|
+
private async writeOnce(uuid: string, hexData: string): Promise<void> {
|
|
618
|
+
const nobleBle = window.desktopApi?.nobleBle;
|
|
619
|
+
if (!nobleBle) {
|
|
620
|
+
throw new Error('Noble BLE API not available');
|
|
524
621
|
}
|
|
525
622
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const chunkHex = hexData.substring(offset, offset + chunkHexLen);
|
|
529
|
-
offset += chunkHexLen;
|
|
623
|
+
await nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
|
|
624
|
+
}
|
|
530
625
|
|
|
531
|
-
|
|
626
|
+
private async refreshBlePacketCapacity(uuid: string): Promise<void> {
|
|
627
|
+
const device = await window.desktopApi?.nobleBle?.getDevice(uuid);
|
|
628
|
+
this.updateBlePacketCapacity(uuid, device?.mtu);
|
|
629
|
+
}
|
|
532
630
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
631
|
+
private updateBlePacketCapacity(uuid: string, mtu?: number): void {
|
|
632
|
+
const packetCapacity = resolveBlePacketCapacity(
|
|
633
|
+
mtu,
|
|
634
|
+
BLE_PACKET_SIZE_MAXIMUM,
|
|
635
|
+
BLE_PACKET_SIZE_FALLBACK
|
|
636
|
+
);
|
|
637
|
+
if (typeof mtu === 'number') {
|
|
638
|
+
this.deviceMtus.set(uuid, mtu);
|
|
639
|
+
} else {
|
|
640
|
+
this.deviceMtus.delete(uuid);
|
|
536
641
|
}
|
|
642
|
+
this.devicePacketCapacities.set(uuid, packetCapacity);
|
|
537
643
|
}
|
|
538
644
|
|
|
539
|
-
private
|
|
540
|
-
const
|
|
541
|
-
if (!
|
|
542
|
-
|
|
543
|
-
|
|
645
|
+
private createMtuSubscription(uuid: string): (() => void) | undefined {
|
|
646
|
+
const onMtuChanged = window.desktopApi?.nobleBle?.onMtuChanged;
|
|
647
|
+
if (!onMtuChanged) return undefined;
|
|
648
|
+
return onMtuChanged(device => {
|
|
649
|
+
if (device.id === uuid) {
|
|
650
|
+
this.updateBlePacketCapacity(uuid, device.mtu);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
544
654
|
|
|
545
|
-
|
|
655
|
+
private writeProtocolV2Frame(
|
|
656
|
+
uuid: string,
|
|
657
|
+
frame: Uint8Array,
|
|
658
|
+
context: ProtocolV2CallContext,
|
|
659
|
+
assertCurrentGeneration: () => void
|
|
660
|
+
) {
|
|
661
|
+
const packetCapacity = this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK;
|
|
662
|
+
const shouldPace = !context.highThroughput;
|
|
663
|
+
return writeProtocolV2BleFrame({
|
|
664
|
+
frame,
|
|
665
|
+
packetCapacity,
|
|
666
|
+
assertActive: assertCurrentGeneration,
|
|
667
|
+
signal: context.signal,
|
|
668
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
669
|
+
initialDelayMs: shouldPace && frame.length <= packetCapacity ? BLE_WRITE_DELAY_MS : 0,
|
|
670
|
+
burstSize: shouldPace ? 1 : undefined,
|
|
671
|
+
burstPauseMs: shouldPace ? BLE_WRITE_DELAY_MS : 0,
|
|
672
|
+
writePacket: packet => this.writeOnce(uuid, bytesToHex(packet)),
|
|
673
|
+
});
|
|
546
674
|
}
|
|
547
675
|
|
|
548
676
|
private handleNotification(deviceId: string, hexData: string): void {
|
|
@@ -551,7 +679,7 @@ export default class ElectronBleTransport {
|
|
|
551
679
|
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled);
|
|
552
680
|
if (this.deviceProtocol.get(deviceId) === 'V2') {
|
|
553
681
|
this.rejectProtocolV2Frames(deviceId, error);
|
|
554
|
-
} else if (this.runPromise) {
|
|
682
|
+
} else if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
555
683
|
this.runPromise.reject(error);
|
|
556
684
|
}
|
|
557
685
|
return;
|
|
@@ -606,17 +734,8 @@ export default class ElectronBleTransport {
|
|
|
606
734
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
607
735
|
}
|
|
608
736
|
|
|
609
|
-
private rejectAllProtocolV2Frames(error: Error) {
|
|
610
|
-
this.v2FrameQueues.clear();
|
|
611
|
-
for (const framePromise of this.v2FramePromises.values()) {
|
|
612
|
-
framePromise.reject(error);
|
|
613
|
-
}
|
|
614
|
-
this.v2FramePromises.clear();
|
|
615
|
-
}
|
|
616
|
-
|
|
617
737
|
private resetProtocolV2Frames(uuid: string) {
|
|
618
|
-
this.
|
|
619
|
-
this.v2FramePromises.delete(uuid);
|
|
738
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
620
739
|
}
|
|
621
740
|
|
|
622
741
|
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
@@ -650,13 +769,18 @@ export default class ElectronBleTransport {
|
|
|
650
769
|
|
|
651
770
|
if (result.error) {
|
|
652
771
|
this.Log?.error('[Electron BLE] Protocol V1 packet processing error:', result.error);
|
|
653
|
-
if (this.runPromise) {
|
|
772
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
654
773
|
this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
|
|
655
774
|
}
|
|
656
775
|
return;
|
|
657
776
|
}
|
|
658
777
|
|
|
659
|
-
if (
|
|
778
|
+
if (
|
|
779
|
+
result.isComplete &&
|
|
780
|
+
result.completePacket &&
|
|
781
|
+
this.runPromise &&
|
|
782
|
+
this.runPromiseDeviceId === deviceId
|
|
783
|
+
) {
|
|
660
784
|
this.runPromise.resolve(result.completePacket);
|
|
661
785
|
}
|
|
662
786
|
}
|
|
@@ -682,12 +806,8 @@ export default class ElectronBleTransport {
|
|
|
682
806
|
`Device protocol has not been detected for ${uuid}`
|
|
683
807
|
);
|
|
684
808
|
}
|
|
685
|
-
if (shouldSuppressHighVolumeCallLog(name)) {
|
|
686
|
-
|
|
687
|
-
} else if (isLogBlockCommand(name)) {
|
|
688
|
-
this.Log?.debug('[Electron BLE] call', 'name:', name, 'protocol:', protocol);
|
|
689
|
-
} else {
|
|
690
|
-
this.Log?.debug('[Electron BLE] call', 'name:', name, 'data:', data, 'protocol:', protocol);
|
|
809
|
+
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
810
|
+
this.Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
691
811
|
}
|
|
692
812
|
|
|
693
813
|
if (protocol === 'V2') {
|
|
@@ -714,6 +834,7 @@ export default class ElectronBleTransport {
|
|
|
714
834
|
const runPromise = createDeferred<Uint8Array | string>();
|
|
715
835
|
runPromise.promise.catch(() => undefined);
|
|
716
836
|
this.runPromise = runPromise;
|
|
837
|
+
this.runPromiseDeviceId = uuid;
|
|
717
838
|
const messages = this._messages;
|
|
718
839
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
719
840
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -758,11 +879,24 @@ export default class ElectronBleTransport {
|
|
|
758
879
|
return check.call(jsonData);
|
|
759
880
|
} catch (e) {
|
|
760
881
|
this.Log?.error('[Electron BLE] Protocol V1 call error:', e);
|
|
882
|
+
const isProbeTimeout =
|
|
883
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
884
|
+
if ((e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError) {
|
|
885
|
+
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
886
|
+
const notifyCleanup = this.notificationCleanups.get(uuid);
|
|
887
|
+
notifyCleanup?.();
|
|
888
|
+
this.notificationCleanups.delete(uuid);
|
|
889
|
+
this.notificationTokens.delete(uuid);
|
|
890
|
+
if (!isProbeTimeout) {
|
|
891
|
+
await this.releaseNative(uuid);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
761
894
|
throw e;
|
|
762
895
|
} finally {
|
|
763
896
|
if (timeout) clearTimeout(timeout);
|
|
764
897
|
if (this.runPromise === runPromise) {
|
|
765
898
|
this.runPromise = null;
|
|
899
|
+
this.runPromiseDeviceId = null;
|
|
766
900
|
}
|
|
767
901
|
}
|
|
768
902
|
}
|
|
@@ -777,10 +911,7 @@ export default class ElectronBleTransport {
|
|
|
777
911
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
778
912
|
}
|
|
779
913
|
|
|
780
|
-
const callOptions =
|
|
781
|
-
...options,
|
|
782
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
783
|
-
};
|
|
914
|
+
const callOptions = options;
|
|
784
915
|
|
|
785
916
|
try {
|
|
786
917
|
return await this.protocolV2Links.call(
|
|
@@ -806,16 +937,15 @@ export default class ElectronBleTransport {
|
|
|
806
937
|
|
|
807
938
|
return {
|
|
808
939
|
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
940
|
+
maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
809
941
|
generation,
|
|
810
942
|
prepareCall: () => {
|
|
811
943
|
assertCurrentGeneration();
|
|
812
944
|
this.v2Assemblers.get(uuid)?.reset();
|
|
813
945
|
this.resetProtocolV2Frames(uuid);
|
|
814
946
|
},
|
|
815
|
-
writeFrame: (frame: Uint8Array) =>
|
|
816
|
-
assertCurrentGeneration
|
|
817
|
-
return this.writeWithChunking(uuid, bytesToHex(frame));
|
|
818
|
-
},
|
|
947
|
+
writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) =>
|
|
948
|
+
this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
|
|
819
949
|
readFrame: async () => {
|
|
820
950
|
assertCurrentGeneration();
|
|
821
951
|
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
@@ -859,6 +989,12 @@ export default class ElectronBleTransport {
|
|
|
859
989
|
bufferState.bufferLength = dataView.getInt32(5, false);
|
|
860
990
|
bufferState.buffer = [...data.subarray(3)];
|
|
861
991
|
} else {
|
|
992
|
+
if (bufferState.buffer.length === 0) {
|
|
993
|
+
// Tail of a cancelled call, arriving after the buffer was reset. Not an
|
|
994
|
+
// `error`: the caller rejects the in-flight call on that field.
|
|
995
|
+
this.Log?.debug('[Electron BLE] Orphan continuation chunk discarded');
|
|
996
|
+
return { isComplete: false };
|
|
997
|
+
}
|
|
862
998
|
bufferState.buffer = bufferState.buffer.concat([...data]);
|
|
863
999
|
}
|
|
864
1000
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createTransportCallLog, shouldSuppressHighVolumeCallLog } from '@onekeyfe/hd-transport';
|