@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.
@@ -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 { OneKeyDeviceInfo, ProtocolType, TransportCallOptions } from '@onekeyfe/hd-transport';
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 BLE_PACKET_SIZE = 192;
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 Initialize timeout by probing Protocol V2 Ping.
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.release(uuid);
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.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');
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
- : this.deviceProtocolHints.get(uuid) ?? inferProtocolHintFromDeviceName(device.name);
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 (disconnectedDevice.id === uuid) {
354
+ if (
355
+ disconnectedDevice.id === uuid &&
356
+ this.notificationTokens.get(uuid) === connectionToken
357
+ ) {
304
358
  this.cleanupDeviceState(uuid);
305
- this.emitter?.emit('device-disconnect', {
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 Initialize or Protocol V2 Ping'
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
- // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景,
394
- // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。
395
- this.deviceProtocol.set(uuid, 'V2');
396
- this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
397
- return 'V2';
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
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
401
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
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] resubscribe after Protocol ${protocol} probe failed:`, error);
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
- await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
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 Initialize probe failed:', error);
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 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;
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
- 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;
623
+ await nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
624
+ }
530
625
 
531
- await this.writeOnce(uuid, chunkHex);
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
- if (offset < hexData.length) {
534
- await wait(BLE_WRITE_DELAY_MS);
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 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
- }
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
- await nobleBle.write(uuid, hexData);
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.v2FrameQueues.delete(uuid);
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 (result.isComplete && result.completePacket && this.runPromise) {
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
- // 高频文件写入不要逐包发 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);
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';