@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.
@@ -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 { OneKeyDeviceInfo, ProtocolType, TransportCallOptions } from '@onekeyfe/hd-transport';
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 BLE_PACKET_SIZE = 192;
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 Initialize timeout by probing Protocol V2 Ping.
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.release(uuid);
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.protocolV2Links
222
- .invalidateAllLinks('Protocol V2 schema reconfigured')
223
- .catch(error => this.Log?.debug('[Electron BLE] schema link cleanup failed:', error));
224
- this.Log?.debug('[Electron BLE] Protocol V2 schema configured');
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
- : this.deviceProtocolHints.get(uuid) ?? inferProtocolHintFromDeviceName(device.name);
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 (disconnectedDevice.id === uuid) {
353
+ if (
354
+ disconnectedDevice.id === uuid &&
355
+ this.notificationTokens.get(uuid) === connectionToken
356
+ ) {
304
357
  this.cleanupDeviceState(uuid);
305
- this.emitter?.emit('device-disconnect', {
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 Initialize or Protocol V2 Ping'
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
- // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景,
394
- // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。
395
- this.deviceProtocol.set(uuid, 'V2');
396
- this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
397
- return 'V2';
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
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
401
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
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] resubscribe after Protocol ${protocol} probe failed:`, error);
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
- await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
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 Initialize probe failed:', error);
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 writeWithChunking(uuid: string, hexData: string): Promise<void> {
518
- const totalBytes = hexData.length / 2;
519
-
520
- if (totalBytes <= BLE_PACKET_SIZE) {
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
- for (let offset = 0; offset < hexData.length; ) {
527
- const chunkHexLen = Math.min(BLE_PACKET_SIZE * 2, hexData.length - offset);
528
- const chunkHex = hexData.substring(offset, offset + chunkHexLen);
529
- offset += chunkHexLen;
622
+ await nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
623
+ }
530
624
 
531
- await this.writeOnce(uuid, chunkHex);
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
- if (offset < hexData.length) {
534
- await wait(BLE_WRITE_DELAY_MS);
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 async writeOnce(uuid: string, hexData: string): Promise<void> {
540
- const nobleBle = window.desktopApi?.nobleBle;
541
- if (!nobleBle) {
542
- throw new Error('Noble BLE API not available');
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
- await nobleBle.write(uuid, hexData);
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.v2FrameQueues.delete(uuid);
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 (result.isComplete && result.completePacket && this.runPromise) {
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';