@onekeyfe/hd-transport-web-device 1.2.0-alpha.11 → 1.2.0-alpha.110
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 +386 -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 +339 -253
- 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 +231 -100
- package/src/transportLog.ts +1 -0
- package/src/webusb.ts +181 -220
|
@@ -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,20 @@ import {
|
|
|
14
16
|
HardwareErrorCodeMessage,
|
|
15
17
|
createDeferred,
|
|
16
18
|
isHeaderChunk,
|
|
17
|
-
wait,
|
|
18
19
|
} from '@onekeyfe/hd-shared';
|
|
19
20
|
|
|
21
|
+
import { resolveBlePacketCapacity } from './ble-packet-capacity';
|
|
22
|
+
|
|
20
23
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
21
24
|
import type { DesktopAPI } from '@onekeyfe/hd-transport-electron';
|
|
22
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
OneKeyDeviceInfo,
|
|
27
|
+
ProtocolType,
|
|
28
|
+
ProtocolV2CallContext,
|
|
29
|
+
TransportCallOptions,
|
|
30
|
+
} from '@onekeyfe/hd-transport';
|
|
23
31
|
import type EventEmitter from 'events';
|
|
24
32
|
|
|
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
33
|
const { parseConfigure, ProtocolV1, check } = transport;
|
|
37
34
|
|
|
38
35
|
declare global {
|
|
@@ -45,6 +42,7 @@ export type BleAcquireInput = {
|
|
|
45
42
|
uuid: string;
|
|
46
43
|
forceCleanRunPromise?: boolean;
|
|
47
44
|
expectedProtocol?: ProtocolType;
|
|
45
|
+
protocolHint?: ProtocolType;
|
|
48
46
|
};
|
|
49
47
|
|
|
50
48
|
interface PacketProcessResult {
|
|
@@ -70,9 +68,9 @@ const toBleDescriptor = (
|
|
|
70
68
|
...(protocolType ? { protocolType } : {}),
|
|
71
69
|
} as OneKeyDeviceInfo);
|
|
72
70
|
|
|
73
|
-
const
|
|
71
|
+
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
72
|
+
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
74
73
|
const BLE_WRITE_DELAY_MS = 5;
|
|
75
|
-
const BLE_RESPONSE_TIMEOUT_MS = 30_000;
|
|
76
74
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
77
75
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
78
76
|
|
|
@@ -80,19 +78,23 @@ const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
|
80
78
|
* Desktop Electron BLE transport with automatic Protocol V1/V2 detection.
|
|
81
79
|
*
|
|
82
80
|
* Protocol V1 devices continue using chunked packets. Protocol V2 is detected
|
|
83
|
-
* after a Protocol V1
|
|
81
|
+
* after a Protocol V1 GetFeatures timeout by probing Protocol V2 Ping.
|
|
84
82
|
*/
|
|
85
83
|
export default class ElectronBleTransport {
|
|
86
84
|
private _messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
87
85
|
|
|
88
86
|
private _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
89
87
|
|
|
88
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
89
|
+
|
|
90
90
|
name = 'ElectronBleTransport';
|
|
91
91
|
|
|
92
92
|
configured = false;
|
|
93
93
|
|
|
94
94
|
runPromise: Deferred<Uint8Array | string> | null = null;
|
|
95
95
|
|
|
96
|
+
private runPromiseDeviceId: string | null = null;
|
|
97
|
+
|
|
96
98
|
Log?: any;
|
|
97
99
|
|
|
98
100
|
emitter?: EventEmitter;
|
|
@@ -103,6 +105,10 @@ export default class ElectronBleTransport {
|
|
|
103
105
|
|
|
104
106
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
105
107
|
|
|
108
|
+
private deviceMtus: Map<string, number> = new Map();
|
|
109
|
+
|
|
110
|
+
private devicePacketCapacities: Map<string, number> = new Map();
|
|
111
|
+
|
|
106
112
|
private v1Buffers: Map<string, { buffer: number[]; bufferLength: number }> = new Map();
|
|
107
113
|
|
|
108
114
|
private v2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
@@ -127,13 +133,18 @@ export default class ElectronBleTransport {
|
|
|
127
133
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
128
134
|
this.Log?.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
|
|
129
135
|
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
130
|
-
await this.
|
|
136
|
+
await this.releaseNative(uuid);
|
|
131
137
|
}
|
|
132
138
|
},
|
|
133
139
|
});
|
|
134
140
|
|
|
141
|
+
/** One-shot guard so a missing preload `release` bridge is reported once. */
|
|
142
|
+
private warnedMissingRelease = false;
|
|
143
|
+
|
|
135
144
|
private notificationCleanups: Map<string, () => void> = new Map();
|
|
136
145
|
|
|
146
|
+
private mtuCleanups: Map<string, () => void> = new Map();
|
|
147
|
+
|
|
137
148
|
private disconnectCleanups: Map<string, () => void> = new Map();
|
|
138
149
|
|
|
139
150
|
private notificationTokens: Map<string, number> = new Map();
|
|
@@ -177,12 +188,19 @@ export default class ElectronBleTransport {
|
|
|
177
188
|
.catch(error => this.Log?.debug('[Electron BLE] link cleanup failed:', error));
|
|
178
189
|
this.connectedDevices.delete(deviceId);
|
|
179
190
|
this.deviceProtocol.delete(deviceId);
|
|
191
|
+
this.deviceMtus.delete(deviceId);
|
|
192
|
+
this.devicePacketCapacities.delete(deviceId);
|
|
180
193
|
// Keep deviceProtocolHints — it's inferred from device name (e.g. "Pro 2" → V2)
|
|
181
194
|
// and doesn't depend on connection state. Preserving it avoids redundant V1 probe on reconnect.
|
|
182
195
|
this.v1Buffers.delete(deviceId);
|
|
183
196
|
this.v2Assemblers.delete(deviceId);
|
|
184
197
|
this.resetProtocolV2Frames(deviceId);
|
|
185
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
|
+
}
|
|
186
204
|
|
|
187
205
|
const notifyCleanup = this.notificationCleanups.get(deviceId);
|
|
188
206
|
if (notifyCleanup) {
|
|
@@ -190,6 +208,12 @@ export default class ElectronBleTransport {
|
|
|
190
208
|
this.notificationCleanups.delete(deviceId);
|
|
191
209
|
}
|
|
192
210
|
|
|
211
|
+
const mtuCleanup = this.mtuCleanups.get(deviceId);
|
|
212
|
+
if (mtuCleanup) {
|
|
213
|
+
mtuCleanup();
|
|
214
|
+
this.mtuCleanups.delete(deviceId);
|
|
215
|
+
}
|
|
216
|
+
|
|
193
217
|
const disconnectCleanup = this.disconnectCleanups.get(deviceId);
|
|
194
218
|
if (disconnectCleanup) {
|
|
195
219
|
disconnectCleanup();
|
|
@@ -217,11 +241,19 @@ export default class ElectronBleTransport {
|
|
|
217
241
|
}
|
|
218
242
|
|
|
219
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;
|
|
220
250
|
this._messagesV2 = parseConfigure(signedData);
|
|
221
|
-
this.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
}
|
|
225
257
|
}
|
|
226
258
|
|
|
227
259
|
async listen() {
|
|
@@ -263,8 +295,8 @@ export default class ElectronBleTransport {
|
|
|
263
295
|
if (forceCleanRunPromise && this.runPromise) {
|
|
264
296
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
265
297
|
this.runPromise.reject(error);
|
|
266
|
-
this.rejectAllProtocolV2Frames(error);
|
|
267
298
|
this.runPromise = null;
|
|
299
|
+
this.runPromiseDeviceId = null;
|
|
268
300
|
}
|
|
269
301
|
|
|
270
302
|
try {
|
|
@@ -278,7 +310,9 @@ export default class ElectronBleTransport {
|
|
|
278
310
|
}
|
|
279
311
|
const protocolHint = expectedProtocol
|
|
280
312
|
? undefined
|
|
281
|
-
:
|
|
313
|
+
: input.protocolHint ??
|
|
314
|
+
this.deviceProtocolHints.get(uuid) ??
|
|
315
|
+
inferProtocolHintFromDeviceName(device.name);
|
|
282
316
|
if (protocolHint) {
|
|
283
317
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
284
318
|
}
|
|
@@ -290,19 +324,38 @@ export default class ElectronBleTransport {
|
|
|
290
324
|
this.handleBluetoothError(error);
|
|
291
325
|
}
|
|
292
326
|
|
|
327
|
+
const mtuCleanup = this.createMtuSubscription(uuid);
|
|
328
|
+
if (mtuCleanup) {
|
|
329
|
+
this.mtuCleanups.set(uuid, mtuCleanup);
|
|
330
|
+
}
|
|
331
|
+
|
|
293
332
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
294
|
-
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
333
|
+
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
|
|
295
334
|
|
|
296
335
|
await window.desktopApi.nobleBle.subscribe(uuid);
|
|
336
|
+
await this.refreshBlePacketCapacity(uuid);
|
|
297
337
|
|
|
298
338
|
const cleanup = this.createNotificationSubscription(uuid);
|
|
299
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
|
+
}
|
|
300
350
|
|
|
301
351
|
const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
|
|
302
352
|
(disconnectedDevice: any) => {
|
|
303
|
-
if (
|
|
353
|
+
if (
|
|
354
|
+
disconnectedDevice.id === uuid &&
|
|
355
|
+
this.notificationTokens.get(uuid) === connectionToken
|
|
356
|
+
) {
|
|
304
357
|
this.cleanupDeviceState(uuid);
|
|
305
|
-
this.emitter?.emit(
|
|
358
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
306
359
|
name: disconnectedDevice.name,
|
|
307
360
|
id: disconnectedDevice.id,
|
|
308
361
|
connectId: disconnectedDevice.id,
|
|
@@ -312,14 +365,6 @@ export default class ElectronBleTransport {
|
|
|
312
365
|
);
|
|
313
366
|
this.disconnectCleanups.set(uuid, disconnectCleanup);
|
|
314
367
|
|
|
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
368
|
return {
|
|
324
369
|
...toBleDescriptor({ id: device.id, name: device.name }, protocolType),
|
|
325
370
|
uuid,
|
|
@@ -339,9 +384,25 @@ export default class ElectronBleTransport {
|
|
|
339
384
|
}
|
|
340
385
|
}
|
|
341
386
|
|
|
342
|
-
async release(id: string) {
|
|
387
|
+
async release(id: string, _onclose?: boolean, keepSession?: boolean) {
|
|
343
388
|
try {
|
|
344
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 {
|
|
345
406
|
if (this.connectedDevices.has(id)) {
|
|
346
407
|
if (window.desktopApi?.nobleBle) {
|
|
347
408
|
await window.desktopApi.nobleBle.unsubscribe(id);
|
|
@@ -355,6 +416,32 @@ export default class ElectronBleTransport {
|
|
|
355
416
|
}
|
|
356
417
|
}
|
|
357
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
|
+
|
|
358
445
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
359
446
|
return ERRORS.TypedError(
|
|
360
447
|
HardwareErrorCode.RuntimeError,
|
|
@@ -365,7 +452,7 @@ export default class ElectronBleTransport {
|
|
|
365
452
|
private createProtocolDetectionError() {
|
|
366
453
|
return ERRORS.TypedError(
|
|
367
454
|
HardwareErrorCode.BleTimeoutError,
|
|
368
|
-
'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'
|
|
369
456
|
);
|
|
370
457
|
}
|
|
371
458
|
|
|
@@ -390,23 +477,23 @@ export default class ElectronBleTransport {
|
|
|
390
477
|
}
|
|
391
478
|
|
|
392
479
|
if (expectedProtocol === 'V2') {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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);
|
|
398
486
|
}
|
|
399
487
|
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// 不能作为最终结论。
|
|
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.
|
|
403
490
|
const probeOrder: ProtocolType[] =
|
|
404
491
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
405
492
|
|
|
406
493
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
407
494
|
const protocol = probeOrder[i];
|
|
408
495
|
if (i > 0) {
|
|
409
|
-
//
|
|
496
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
410
497
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
411
498
|
}
|
|
412
499
|
const detected =
|
|
@@ -446,10 +533,11 @@ export default class ElectronBleTransport {
|
|
|
446
533
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
447
534
|
this.v2Assemblers.get(uuid)?.reset();
|
|
448
535
|
this.resetProtocolV2Frames(uuid);
|
|
449
|
-
if (this.runPromise) {
|
|
536
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
450
537
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
451
538
|
this.runPromise.reject(error);
|
|
452
539
|
this.runPromise = null;
|
|
540
|
+
this.runPromiseDeviceId = null;
|
|
453
541
|
}
|
|
454
542
|
|
|
455
543
|
const notifyCleanup = this.notificationCleanups.get(uuid);
|
|
@@ -465,9 +553,18 @@ export default class ElectronBleTransport {
|
|
|
465
553
|
this.Log?.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error);
|
|
466
554
|
}
|
|
467
555
|
try {
|
|
556
|
+
await window.desktopApi?.nobleBle?.disconnect(uuid);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
this.Log?.debug(`[Electron BLE] disconnect after Protocol ${protocol} probe failed:`, error);
|
|
559
|
+
}
|
|
560
|
+
this.connectedDevices.delete(uuid);
|
|
561
|
+
try {
|
|
562
|
+
await window.desktopApi?.nobleBle?.connect(uuid);
|
|
563
|
+
this.connectedDevices.add(uuid);
|
|
468
564
|
await window.desktopApi?.nobleBle?.subscribe(uuid);
|
|
565
|
+
await this.refreshBlePacketCapacity(uuid);
|
|
469
566
|
} catch (error) {
|
|
470
|
-
this.Log?.debug(`[Electron BLE]
|
|
567
|
+
this.Log?.debug(`[Electron BLE] reconnect after Protocol ${protocol} probe failed:`, error);
|
|
471
568
|
throw error;
|
|
472
569
|
}
|
|
473
570
|
|
|
@@ -482,11 +579,13 @@ export default class ElectronBleTransport {
|
|
|
482
579
|
|
|
483
580
|
try {
|
|
484
581
|
this.deviceProtocol.set(uuid, 'V1');
|
|
485
|
-
|
|
582
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
583
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
584
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
486
585
|
return true;
|
|
487
586
|
} catch (error) {
|
|
488
587
|
this.clearProbeProtocol(uuid, 'V1');
|
|
489
|
-
this.Log?.debug('[Electron BLE] Protocol V1
|
|
588
|
+
this.Log?.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
|
|
490
589
|
return false;
|
|
491
590
|
}
|
|
492
591
|
}
|
|
@@ -514,35 +613,63 @@ export default class ElectronBleTransport {
|
|
|
514
613
|
return detected;
|
|
515
614
|
}
|
|
516
615
|
|
|
517
|
-
private async
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
await wait(BLE_WRITE_DELAY_MS);
|
|
522
|
-
await this.writeOnce(uuid, hexData);
|
|
523
|
-
return;
|
|
616
|
+
private async writeOnce(uuid: string, hexData: string): Promise<void> {
|
|
617
|
+
const nobleBle = window.desktopApi?.nobleBle;
|
|
618
|
+
if (!nobleBle) {
|
|
619
|
+
throw new Error('Noble BLE API not available');
|
|
524
620
|
}
|
|
525
621
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const chunkHex = hexData.substring(offset, offset + chunkHexLen);
|
|
529
|
-
offset += chunkHexLen;
|
|
622
|
+
await nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
|
|
623
|
+
}
|
|
530
624
|
|
|
531
|
-
|
|
625
|
+
private async refreshBlePacketCapacity(uuid: string): Promise<void> {
|
|
626
|
+
const device = await window.desktopApi?.nobleBle?.getDevice(uuid);
|
|
627
|
+
this.updateBlePacketCapacity(uuid, device?.mtu);
|
|
628
|
+
}
|
|
532
629
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
630
|
+
private updateBlePacketCapacity(uuid: string, mtu?: number): void {
|
|
631
|
+
const packetCapacity = resolveBlePacketCapacity(
|
|
632
|
+
mtu,
|
|
633
|
+
BLE_PACKET_SIZE_MAXIMUM,
|
|
634
|
+
BLE_PACKET_SIZE_FALLBACK
|
|
635
|
+
);
|
|
636
|
+
if (typeof mtu === 'number') {
|
|
637
|
+
this.deviceMtus.set(uuid, mtu);
|
|
638
|
+
} else {
|
|
639
|
+
this.deviceMtus.delete(uuid);
|
|
536
640
|
}
|
|
641
|
+
this.devicePacketCapacities.set(uuid, packetCapacity);
|
|
537
642
|
}
|
|
538
643
|
|
|
539
|
-
private
|
|
540
|
-
const
|
|
541
|
-
if (!
|
|
542
|
-
|
|
543
|
-
|
|
644
|
+
private createMtuSubscription(uuid: string): (() => void) | undefined {
|
|
645
|
+
const onMtuChanged = window.desktopApi?.nobleBle?.onMtuChanged;
|
|
646
|
+
if (!onMtuChanged) return undefined;
|
|
647
|
+
return onMtuChanged(device => {
|
|
648
|
+
if (device.id === uuid) {
|
|
649
|
+
this.updateBlePacketCapacity(uuid, device.mtu);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
544
653
|
|
|
545
|
-
|
|
654
|
+
private writeProtocolV2Frame(
|
|
655
|
+
uuid: string,
|
|
656
|
+
frame: Uint8Array,
|
|
657
|
+
context: ProtocolV2CallContext,
|
|
658
|
+
assertCurrentGeneration: () => void
|
|
659
|
+
) {
|
|
660
|
+
const packetCapacity = this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK;
|
|
661
|
+
const shouldPace = !context.highThroughput;
|
|
662
|
+
return writeProtocolV2BleFrame({
|
|
663
|
+
frame,
|
|
664
|
+
packetCapacity,
|
|
665
|
+
assertActive: assertCurrentGeneration,
|
|
666
|
+
signal: context.signal,
|
|
667
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
668
|
+
initialDelayMs: shouldPace && frame.length <= packetCapacity ? BLE_WRITE_DELAY_MS : 0,
|
|
669
|
+
burstSize: shouldPace ? 1 : undefined,
|
|
670
|
+
burstPauseMs: shouldPace ? BLE_WRITE_DELAY_MS : 0,
|
|
671
|
+
writePacket: packet => this.writeOnce(uuid, bytesToHex(packet)),
|
|
672
|
+
});
|
|
546
673
|
}
|
|
547
674
|
|
|
548
675
|
private handleNotification(deviceId: string, hexData: string): void {
|
|
@@ -551,7 +678,7 @@ export default class ElectronBleTransport {
|
|
|
551
678
|
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled);
|
|
552
679
|
if (this.deviceProtocol.get(deviceId) === 'V2') {
|
|
553
680
|
this.rejectProtocolV2Frames(deviceId, error);
|
|
554
|
-
} else if (this.runPromise) {
|
|
681
|
+
} else if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
555
682
|
this.runPromise.reject(error);
|
|
556
683
|
}
|
|
557
684
|
return;
|
|
@@ -606,17 +733,8 @@ export default class ElectronBleTransport {
|
|
|
606
733
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
607
734
|
}
|
|
608
735
|
|
|
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
736
|
private resetProtocolV2Frames(uuid: string) {
|
|
618
|
-
this.
|
|
619
|
-
this.v2FramePromises.delete(uuid);
|
|
737
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
620
738
|
}
|
|
621
739
|
|
|
622
740
|
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
@@ -650,13 +768,18 @@ export default class ElectronBleTransport {
|
|
|
650
768
|
|
|
651
769
|
if (result.error) {
|
|
652
770
|
this.Log?.error('[Electron BLE] Protocol V1 packet processing error:', result.error);
|
|
653
|
-
if (this.runPromise) {
|
|
771
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
654
772
|
this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
|
|
655
773
|
}
|
|
656
774
|
return;
|
|
657
775
|
}
|
|
658
776
|
|
|
659
|
-
if (
|
|
777
|
+
if (
|
|
778
|
+
result.isComplete &&
|
|
779
|
+
result.completePacket &&
|
|
780
|
+
this.runPromise &&
|
|
781
|
+
this.runPromiseDeviceId === deviceId
|
|
782
|
+
) {
|
|
660
783
|
this.runPromise.resolve(result.completePacket);
|
|
661
784
|
}
|
|
662
785
|
}
|
|
@@ -682,14 +805,6 @@ export default class ElectronBleTransport {
|
|
|
682
805
|
`Device protocol has not been detected for ${uuid}`
|
|
683
806
|
);
|
|
684
807
|
}
|
|
685
|
-
if (shouldSuppressHighVolumeCallLog(name)) {
|
|
686
|
-
// 高频文件写入不要逐包发 debug 事件,否则调试日志会反向拖慢传输。
|
|
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);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
808
|
if (protocol === 'V2') {
|
|
694
809
|
return this.callProtocolV2(uuid, name, data, options);
|
|
695
810
|
}
|
|
@@ -714,6 +829,7 @@ export default class ElectronBleTransport {
|
|
|
714
829
|
const runPromise = createDeferred<Uint8Array | string>();
|
|
715
830
|
runPromise.promise.catch(() => undefined);
|
|
716
831
|
this.runPromise = runPromise;
|
|
832
|
+
this.runPromiseDeviceId = uuid;
|
|
717
833
|
const messages = this._messages;
|
|
718
834
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
719
835
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -758,11 +874,24 @@ export default class ElectronBleTransport {
|
|
|
758
874
|
return check.call(jsonData);
|
|
759
875
|
} catch (e) {
|
|
760
876
|
this.Log?.error('[Electron BLE] Protocol V1 call error:', e);
|
|
877
|
+
const isProbeTimeout =
|
|
878
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
879
|
+
if ((e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError) {
|
|
880
|
+
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
881
|
+
const notifyCleanup = this.notificationCleanups.get(uuid);
|
|
882
|
+
notifyCleanup?.();
|
|
883
|
+
this.notificationCleanups.delete(uuid);
|
|
884
|
+
this.notificationTokens.delete(uuid);
|
|
885
|
+
if (!isProbeTimeout) {
|
|
886
|
+
await this.releaseNative(uuid);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
761
889
|
throw e;
|
|
762
890
|
} finally {
|
|
763
891
|
if (timeout) clearTimeout(timeout);
|
|
764
892
|
if (this.runPromise === runPromise) {
|
|
765
893
|
this.runPromise = null;
|
|
894
|
+
this.runPromiseDeviceId = null;
|
|
766
895
|
}
|
|
767
896
|
}
|
|
768
897
|
}
|
|
@@ -777,10 +906,7 @@ export default class ElectronBleTransport {
|
|
|
777
906
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
778
907
|
}
|
|
779
908
|
|
|
780
|
-
const callOptions =
|
|
781
|
-
...options,
|
|
782
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
783
|
-
};
|
|
909
|
+
const callOptions = options;
|
|
784
910
|
|
|
785
911
|
try {
|
|
786
912
|
return await this.protocolV2Links.call(
|
|
@@ -806,16 +932,15 @@ export default class ElectronBleTransport {
|
|
|
806
932
|
|
|
807
933
|
return {
|
|
808
934
|
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
935
|
+
maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
809
936
|
generation,
|
|
810
937
|
prepareCall: () => {
|
|
811
938
|
assertCurrentGeneration();
|
|
812
939
|
this.v2Assemblers.get(uuid)?.reset();
|
|
813
940
|
this.resetProtocolV2Frames(uuid);
|
|
814
941
|
},
|
|
815
|
-
writeFrame: (frame: Uint8Array) =>
|
|
816
|
-
assertCurrentGeneration
|
|
817
|
-
return this.writeWithChunking(uuid, bytesToHex(frame));
|
|
818
|
-
},
|
|
942
|
+
writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) =>
|
|
943
|
+
this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
|
|
819
944
|
readFrame: async () => {
|
|
820
945
|
assertCurrentGeneration();
|
|
821
946
|
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
@@ -859,6 +984,12 @@ export default class ElectronBleTransport {
|
|
|
859
984
|
bufferState.bufferLength = dataView.getInt32(5, false);
|
|
860
985
|
bufferState.buffer = [...data.subarray(3)];
|
|
861
986
|
} else {
|
|
987
|
+
if (bufferState.buffer.length === 0) {
|
|
988
|
+
// Tail of a cancelled call, arriving after the buffer was reset. Not an
|
|
989
|
+
// `error`: the caller rejects the in-flight call on that field.
|
|
990
|
+
this.Log?.debug('[Electron BLE] Orphan continuation chunk discarded');
|
|
991
|
+
return { isComplete: false };
|
|
992
|
+
}
|
|
862
993
|
bufferState.buffer = bufferState.buffer.concat([...data]);
|
|
863
994
|
}
|
|
864
995
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createTransportCallLog, shouldSuppressHighVolumeCallLog } from '@onekeyfe/hd-transport';
|