@onekeyfe/hd-transport-web-device 1.2.0-alpha.7 → 1.2.0-alpha.71

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
- ProtocolV2Session,
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();
@@ -111,12 +118,31 @@ export default class ElectronBleTransport {
111
118
 
112
119
  private v2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
113
120
 
114
- private activeProtocolV2Call: { uuid: string; token: number } | null = null;
115
-
116
- private nextProtocolV2CallToken = 1;
121
+ private protocolV2Links = new ProtocolV2LinkManager<string>({
122
+ getSchemas: () => {
123
+ if (!this._messages || !this._messagesV2) {
124
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
125
+ }
126
+ return {
127
+ protocolV1: this._messages,
128
+ protocolV2: this._messagesV2,
129
+ };
130
+ },
131
+ classifyError: () => 'link-fatal',
132
+ onLinkInvalidated: async (uuid, reason) => {
133
+ this.v2Assemblers.get(uuid)?.reset();
134
+ this.rejectProtocolV2Frames(uuid, new Error(reason));
135
+ this.Log?.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
136
+ if (reason.startsWith('Protocol V2 link-fatal error:')) {
137
+ await this.releaseNative(uuid);
138
+ }
139
+ },
140
+ });
117
141
 
118
142
  private notificationCleanups: Map<string, () => void> = new Map();
119
143
 
144
+ private mtuCleanups: Map<string, () => void> = new Map();
145
+
120
146
  private disconnectCleanups: Map<string, () => void> = new Map();
121
147
 
122
148
  private notificationTokens: Map<string, number> = new Map();
@@ -155,17 +181,24 @@ export default class ElectronBleTransport {
155
181
  }
156
182
 
157
183
  private cleanupDeviceState(deviceId: string): void {
184
+ this.protocolV2Links
185
+ .invalidateLink(deviceId, 'Electron BLE device state cleaned')
186
+ .catch(error => this.Log?.debug('[Electron BLE] link cleanup failed:', error));
158
187
  this.connectedDevices.delete(deviceId);
159
188
  this.deviceProtocol.delete(deviceId);
189
+ this.deviceMtus.delete(deviceId);
190
+ this.devicePacketCapacities.delete(deviceId);
160
191
  // Keep deviceProtocolHints — it's inferred from device name (e.g. "Pro 2" → V2)
161
192
  // and doesn't depend on connection state. Preserving it avoids redundant V1 probe on reconnect.
162
193
  this.v1Buffers.delete(deviceId);
163
194
  this.v2Assemblers.delete(deviceId);
164
195
  this.resetProtocolV2Frames(deviceId);
165
- if (this.activeProtocolV2Call?.uuid === deviceId) {
166
- this.activeProtocolV2Call = null;
167
- }
168
196
  this.notificationTokens.delete(deviceId);
197
+ if (this.runPromise && this.runPromiseDeviceId === deviceId) {
198
+ this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
199
+ this.runPromise = null;
200
+ this.runPromiseDeviceId = null;
201
+ }
169
202
 
170
203
  const notifyCleanup = this.notificationCleanups.get(deviceId);
171
204
  if (notifyCleanup) {
@@ -173,6 +206,12 @@ export default class ElectronBleTransport {
173
206
  this.notificationCleanups.delete(deviceId);
174
207
  }
175
208
 
209
+ const mtuCleanup = this.mtuCleanups.get(deviceId);
210
+ if (mtuCleanup) {
211
+ mtuCleanup();
212
+ this.mtuCleanups.delete(deviceId);
213
+ }
214
+
176
215
  const disconnectCleanup = this.disconnectCleanups.get(deviceId);
177
216
  if (disconnectCleanup) {
178
217
  disconnectCleanup();
@@ -200,8 +239,19 @@ export default class ElectronBleTransport {
200
239
  }
201
240
 
202
241
  configureProtocolV2(signedData: any) {
242
+ const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
243
+ if (this.protocolV2SchemaConfiguration === configuration) {
244
+ return;
245
+ }
246
+
247
+ const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
203
248
  this._messagesV2 = parseConfigure(signedData);
204
- this.Log?.debug('[Electron BLE] Protocol V2 schema configured');
249
+ this.protocolV2SchemaConfiguration = configuration;
250
+ if (isReconfiguration) {
251
+ this.protocolV2Links
252
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
253
+ .catch(error => this.Log?.debug('[Electron BLE] schema link cleanup failed:', error));
254
+ }
205
255
  }
206
256
 
207
257
  async listen() {
@@ -236,12 +286,15 @@ export default class ElectronBleTransport {
236
286
  throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
237
287
  }
238
288
 
289
+ if (this.connectedDevices.has(uuid)) {
290
+ await this.release(uuid);
291
+ }
292
+
239
293
  if (forceCleanRunPromise && this.runPromise) {
240
294
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
241
295
  this.runPromise.reject(error);
242
- this.rejectAllProtocolV2Frames(error);
243
296
  this.runPromise = null;
244
- this.activeProtocolV2Call = null;
297
+ this.runPromiseDeviceId = null;
245
298
  }
246
299
 
247
300
  try {
@@ -255,7 +308,9 @@ export default class ElectronBleTransport {
255
308
  }
256
309
  const protocolHint = expectedProtocol
257
310
  ? undefined
258
- : this.deviceProtocolHints.get(uuid) ?? inferProtocolHintFromDeviceName(device.name);
311
+ : input.protocolHint ??
312
+ this.deviceProtocolHints.get(uuid) ??
313
+ inferProtocolHintFromDeviceName(device.name);
259
314
  if (protocolHint) {
260
315
  this.deviceProtocolHints.set(uuid, protocolHint);
261
316
  }
@@ -267,19 +322,38 @@ export default class ElectronBleTransport {
267
322
  this.handleBluetoothError(error);
268
323
  }
269
324
 
325
+ const mtuCleanup = this.createMtuSubscription(uuid);
326
+ if (mtuCleanup) {
327
+ this.mtuCleanups.set(uuid, mtuCleanup);
328
+ }
329
+
270
330
  this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
271
- this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
331
+ this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
272
332
 
273
333
  await window.desktopApi.nobleBle.subscribe(uuid);
334
+ await this.refreshBlePacketCapacity(uuid);
274
335
 
275
336
  const cleanup = this.createNotificationSubscription(uuid);
276
337
  this.notificationCleanups.set(uuid, cleanup);
338
+ const connectionToken = this.notificationTokens.get(uuid);
339
+
340
+ const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
341
+ if (protocolType === 'V2') {
342
+ this.Log?.debug('[Electron BLE] Protocol V2 write configured', {
343
+ writeMode: 'withoutResponse',
344
+ negotiatedMtu: this.deviceMtus.get(uuid),
345
+ packetCapacity: this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK,
346
+ });
347
+ }
277
348
 
278
349
  const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
279
350
  (disconnectedDevice: any) => {
280
- if (disconnectedDevice.id === uuid) {
351
+ if (
352
+ disconnectedDevice.id === uuid &&
353
+ this.notificationTokens.get(uuid) === connectionToken
354
+ ) {
281
355
  this.cleanupDeviceState(uuid);
282
- this.emitter?.emit('device-disconnect', {
356
+ this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
283
357
  name: disconnectedDevice.name,
284
358
  id: disconnectedDevice.id,
285
359
  connectId: disconnectedDevice.id,
@@ -289,14 +363,6 @@ export default class ElectronBleTransport {
289
363
  );
290
364
  this.disconnectCleanups.set(uuid, disconnectCleanup);
291
365
 
292
- const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
293
-
294
- this.emitter?.emit('device-connect', {
295
- name: device.name,
296
- id: device.id,
297
- connectId: device.id,
298
- });
299
-
300
366
  return {
301
367
  ...toBleDescriptor({ id: device.id, name: device.name }, protocolType),
302
368
  uuid,
@@ -317,6 +383,16 @@ export default class ElectronBleTransport {
317
383
  }
318
384
 
319
385
  async release(id: string) {
386
+ try {
387
+ await this.protocolV2Links.invalidateLink(id, 'Electron BLE transport released');
388
+ await this.releaseNative(id);
389
+ } catch (error) {
390
+ this.Log?.error('[Electron BLE] release failed:', error);
391
+ this.cleanupDeviceState(id);
392
+ }
393
+ }
394
+
395
+ private async releaseNative(id: string) {
320
396
  try {
321
397
  if (this.connectedDevices.has(id)) {
322
398
  if (window.desktopApi?.nobleBle) {
@@ -341,7 +417,7 @@ export default class ElectronBleTransport {
341
417
  private createProtocolDetectionError() {
342
418
  return ERRORS.TypedError(
343
419
  HardwareErrorCode.BleTimeoutError,
344
- 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
420
+ 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
345
421
  );
346
422
  }
347
423
 
@@ -366,23 +442,23 @@ export default class ElectronBleTransport {
366
442
  }
367
443
 
368
444
  if (expectedProtocol === 'V2') {
369
- // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景,
370
- // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。
371
- this.deviceProtocol.set(uuid, 'V2');
372
- this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
373
- return 'V2';
445
+ if (await this.probeProtocolV2(uuid)) {
446
+ this.deviceProtocol.set(uuid, 'V2');
447
+ this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
448
+ return 'V2';
449
+ }
450
+ throw this.createProtocolMismatchError(expectedProtocol);
374
451
  }
375
452
 
376
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
377
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
378
- // 不能作为最终结论。
453
+ // Protocol must be actively probed after connection. Name, PID, and descriptors only
454
+ // influence probe order; a V2 hint probes V2 first and falls back to V1.
379
455
  const probeOrder: ProtocolType[] =
380
456
  protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
381
457
 
382
458
  for (let i = 0; i < probeOrder.length; i += 1) {
383
459
  const protocol = probeOrder[i];
384
460
  if (i > 0) {
385
- // 上一个协议探测失败后,重置订阅与缓冲,避免残留数据干扰下一个协议的探测。
461
+ // Reset subscriptions and buffers after a failed probe before trying another protocol.
386
462
  await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
387
463
  }
388
464
  const detected =
@@ -415,16 +491,18 @@ export default class ElectronBleTransport {
415
491
  }
416
492
 
417
493
  private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
494
+ await this.protocolV2Links.invalidateLink(
495
+ uuid,
496
+ `Reset notify state after Protocol ${protocol} probe`
497
+ );
418
498
  this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
419
499
  this.v2Assemblers.get(uuid)?.reset();
420
500
  this.resetProtocolV2Frames(uuid);
421
- if (this.activeProtocolV2Call?.uuid === uuid) {
422
- this.activeProtocolV2Call = null;
423
- }
424
- if (this.runPromise) {
501
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
425
502
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
426
503
  this.runPromise.reject(error);
427
504
  this.runPromise = null;
505
+ this.runPromiseDeviceId = null;
428
506
  }
429
507
 
430
508
  const notifyCleanup = this.notificationCleanups.get(uuid);
@@ -440,9 +518,18 @@ export default class ElectronBleTransport {
440
518
  this.Log?.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error);
441
519
  }
442
520
  try {
521
+ await window.desktopApi?.nobleBle?.disconnect(uuid);
522
+ } catch (error) {
523
+ this.Log?.debug(`[Electron BLE] disconnect after Protocol ${protocol} probe failed:`, error);
524
+ }
525
+ this.connectedDevices.delete(uuid);
526
+ try {
527
+ await window.desktopApi?.nobleBle?.connect(uuid);
528
+ this.connectedDevices.add(uuid);
443
529
  await window.desktopApi?.nobleBle?.subscribe(uuid);
530
+ await this.refreshBlePacketCapacity(uuid);
444
531
  } catch (error) {
445
- this.Log?.debug(`[Electron BLE] resubscribe after Protocol ${protocol} probe failed:`, error);
532
+ this.Log?.debug(`[Electron BLE] reconnect after Protocol ${protocol} probe failed:`, error);
446
533
  throw error;
447
534
  }
448
535
 
@@ -457,11 +544,13 @@ export default class ElectronBleTransport {
457
544
 
458
545
  try {
459
546
  this.deviceProtocol.set(uuid, 'V1');
460
- await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
547
+ // GetFeatures identifies Protocol V1 without resetting an existing wallet
548
+ // session before Core has a chance to restore a hidden wallet.
549
+ await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
461
550
  return true;
462
551
  } catch (error) {
463
552
  this.clearProbeProtocol(uuid, 'V1');
464
- this.Log?.debug('[Electron BLE] Protocol V1 Initialize probe failed:', error);
553
+ this.Log?.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
465
554
  return false;
466
555
  }
467
556
  }
@@ -489,44 +578,73 @@ export default class ElectronBleTransport {
489
578
  return detected;
490
579
  }
491
580
 
492
- private async writeWithChunking(uuid: string, hexData: string): Promise<void> {
493
- const totalBytes = hexData.length / 2;
494
-
495
- if (totalBytes <= BLE_PACKET_SIZE) {
496
- await wait(BLE_WRITE_DELAY_MS);
497
- await this.writeOnce(uuid, hexData);
498
- return;
581
+ private async writeOnce(uuid: string, hexData: string): Promise<void> {
582
+ const nobleBle = window.desktopApi?.nobleBle;
583
+ if (!nobleBle) {
584
+ throw new Error('Noble BLE API not available');
499
585
  }
500
586
 
501
- for (let offset = 0; offset < hexData.length; ) {
502
- const chunkHexLen = Math.min(BLE_PACKET_SIZE * 2, hexData.length - offset);
503
- const chunkHex = hexData.substring(offset, offset + chunkHexLen);
504
- offset += chunkHexLen;
587
+ await nobleBle.write(uuid, hexData);
588
+ }
505
589
 
506
- await this.writeOnce(uuid, chunkHex);
590
+ private async refreshBlePacketCapacity(uuid: string): Promise<void> {
591
+ const device = await window.desktopApi?.nobleBle?.getDevice(uuid);
592
+ this.updateBlePacketCapacity(uuid, device?.mtu);
593
+ }
507
594
 
508
- if (offset < hexData.length) {
509
- await wait(BLE_WRITE_DELAY_MS);
510
- }
595
+ private updateBlePacketCapacity(uuid: string, mtu?: number): void {
596
+ const packetCapacity = resolveBlePacketCapacity(
597
+ mtu,
598
+ BLE_PACKET_SIZE_MAXIMUM,
599
+ BLE_PACKET_SIZE_FALLBACK
600
+ );
601
+ if (typeof mtu === 'number') {
602
+ this.deviceMtus.set(uuid, mtu);
603
+ } else {
604
+ this.deviceMtus.delete(uuid);
511
605
  }
606
+ this.devicePacketCapacities.set(uuid, packetCapacity);
512
607
  }
513
608
 
514
- private async writeOnce(uuid: string, hexData: string): Promise<void> {
515
- const nobleBle = window.desktopApi?.nobleBle;
516
- if (!nobleBle) {
517
- throw new Error('Noble BLE API not available');
518
- }
609
+ private createMtuSubscription(uuid: string): (() => void) | undefined {
610
+ const onMtuChanged = window.desktopApi?.nobleBle?.onMtuChanged;
611
+ if (!onMtuChanged) return undefined;
612
+ return onMtuChanged(device => {
613
+ if (device.id === uuid) {
614
+ this.updateBlePacketCapacity(uuid, device.mtu);
615
+ }
616
+ });
617
+ }
519
618
 
520
- await nobleBle.write(uuid, hexData);
619
+ private writeProtocolV2Frame(
620
+ uuid: string,
621
+ frame: Uint8Array,
622
+ context: ProtocolV2CallContext,
623
+ assertCurrentGeneration: () => void
624
+ ) {
625
+ const packetCapacity = this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK;
626
+ const shouldPace = !context.highVolume;
627
+ return writeProtocolV2BleFrame({
628
+ frame,
629
+ packetCapacity,
630
+ assertActive: assertCurrentGeneration,
631
+ signal: context.signal,
632
+ abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
633
+ initialDelayMs: shouldPace && frame.length <= packetCapacity ? BLE_WRITE_DELAY_MS : 0,
634
+ burstSize: shouldPace ? 1 : undefined,
635
+ burstPauseMs: shouldPace ? BLE_WRITE_DELAY_MS : 0,
636
+ writePacket: packet => this.writeOnce(uuid, bytesToHex(packet)),
637
+ });
521
638
  }
522
639
 
523
640
  private handleNotification(deviceId: string, hexData: string): void {
524
641
  if (hexData === 'PAIRING_REJECTED') {
525
642
  this.Log?.debug('[Electron BLE] Pairing rejection detected for device:', deviceId);
526
- if (this.runPromise) {
527
- const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled);
643
+ const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled);
644
+ if (this.deviceProtocol.get(deviceId) === 'V2') {
645
+ this.rejectProtocolV2Frames(deviceId, error);
646
+ } else if (this.runPromise && this.runPromiseDeviceId === deviceId) {
528
647
  this.runPromise.reject(error);
529
- this.rejectAllProtocolV2Frames(error);
530
648
  }
531
649
  return;
532
650
  }
@@ -545,12 +663,6 @@ export default class ElectronBleTransport {
545
663
 
546
664
  private handleProtocolV2Notification(deviceId: string, hexData: string): void {
547
665
  try {
548
- if (!this.runPromise || this.activeProtocolV2Call?.uuid !== deviceId) {
549
- this.v2Assemblers.get(deviceId)?.reset();
550
- this.resetProtocolV2Frames(deviceId);
551
- return;
552
- }
553
-
554
666
  const bytes = hexToBytes(hexData);
555
667
  if (bytes.length === 0) return;
556
668
 
@@ -562,11 +674,8 @@ export default class ElectronBleTransport {
562
674
  }
563
675
  } catch (error) {
564
676
  this.Log?.error('[Electron BLE] Protocol V2 notification error:', error);
565
- if (this.runPromise) {
566
- const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
567
- this.runPromise.reject(notifyError);
568
- this.rejectAllProtocolV2Frames(notifyError);
569
- }
677
+ const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
678
+ this.rejectProtocolV2Frames(deviceId, notifyError);
570
679
  }
571
680
  }
572
681
 
@@ -589,21 +698,17 @@ export default class ElectronBleTransport {
589
698
  this.getProtocolV2FrameQueue(uuid).push(frame);
590
699
  }
591
700
 
592
- private rejectAllProtocolV2Frames(error: Error) {
593
- this.v2FrameQueues.clear();
594
- for (const framePromise of this.v2FramePromises.values()) {
595
- framePromise.reject(error);
596
- }
597
- this.v2FramePromises.clear();
598
- }
599
-
600
701
  private resetProtocolV2Frames(uuid: string) {
601
- this.v2FrameQueues.delete(uuid);
602
- this.v2FramePromises.delete(uuid);
702
+ this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
603
703
  }
604
704
 
605
- private isActiveProtocolV2Call(uuid: string, token: number) {
606
- return this.activeProtocolV2Call?.uuid === uuid && this.activeProtocolV2Call.token === token;
705
+ private rejectProtocolV2Frames(uuid: string, error: Error) {
706
+ this.v2FrameQueues.delete(uuid);
707
+ const framePromise = this.v2FramePromises.get(uuid);
708
+ if (framePromise) {
709
+ this.v2FramePromises.delete(uuid);
710
+ framePromise.reject(error);
711
+ }
607
712
  }
608
713
 
609
714
  private async readProtocolV2Frame(uuid: string) {
@@ -628,13 +733,18 @@ export default class ElectronBleTransport {
628
733
 
629
734
  if (result.error) {
630
735
  this.Log?.error('[Electron BLE] Protocol V1 packet processing error:', result.error);
631
- if (this.runPromise) {
736
+ if (this.runPromise && this.runPromiseDeviceId === deviceId) {
632
737
  this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
633
738
  }
634
739
  return;
635
740
  }
636
741
 
637
- if (result.isComplete && result.completePacket && this.runPromise) {
742
+ if (
743
+ result.isComplete &&
744
+ result.completePacket &&
745
+ this.runPromise &&
746
+ this.runPromiseDeviceId === deviceId
747
+ ) {
638
748
  this.runPromise.resolve(result.completePacket);
639
749
  }
640
750
  }
@@ -660,12 +770,8 @@ export default class ElectronBleTransport {
660
770
  `Device protocol has not been detected for ${uuid}`
661
771
  );
662
772
  }
663
- if (shouldSuppressHighVolumeCallLog(name)) {
664
- // 高频文件写入不要逐包发 debug 事件,否则调试日志会反向拖慢传输。
665
- } else if (isLogBlockCommand(name)) {
666
- this.Log?.debug('[Electron BLE] call', 'name:', name, 'protocol:', protocol);
667
- } else {
668
- this.Log?.debug('[Electron BLE] call', 'name:', name, 'data:', data, 'protocol:', protocol);
773
+ if (!shouldSuppressHighVolumeCallLog(name)) {
774
+ this.Log?.debug('transport call', createTransportCallLog(name, protocol, data));
669
775
  }
670
776
 
671
777
  if (protocol === 'V2') {
@@ -692,6 +798,7 @@ export default class ElectronBleTransport {
692
798
  const runPromise = createDeferred<Uint8Array | string>();
693
799
  runPromise.promise.catch(() => undefined);
694
800
  this.runPromise = runPromise;
801
+ this.runPromiseDeviceId = uuid;
695
802
  const messages = this._messages;
696
803
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
697
804
  let timeout: ReturnType<typeof setTimeout> | undefined;
@@ -736,11 +843,24 @@ export default class ElectronBleTransport {
736
843
  return check.call(jsonData);
737
844
  } catch (e) {
738
845
  this.Log?.error('[Electron BLE] Protocol V1 call error:', e);
846
+ const isProbeTimeout =
847
+ name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
848
+ if ((e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError) {
849
+ this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
850
+ const notifyCleanup = this.notificationCleanups.get(uuid);
851
+ notifyCleanup?.();
852
+ this.notificationCleanups.delete(uuid);
853
+ this.notificationTokens.delete(uuid);
854
+ if (!isProbeTimeout) {
855
+ await this.releaseNative(uuid);
856
+ }
857
+ }
739
858
  throw e;
740
859
  } finally {
741
860
  if (timeout) clearTimeout(timeout);
742
861
  if (this.runPromise === runPromise) {
743
862
  this.runPromise = null;
863
+ this.runPromiseDeviceId = null;
744
864
  }
745
865
  }
746
866
  }
@@ -755,77 +875,61 @@ export default class ElectronBleTransport {
755
875
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
756
876
  }
757
877
 
758
- const forceRun = name === 'Initialize' || name === 'Cancel' || name === 'Ping';
759
- if (this.runPromise) {
760
- if (!forceRun) {
761
- throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
762
- }
763
- const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
764
- this.runPromise.reject(error);
765
- this.rejectAllProtocolV2Frames(error);
766
- this.runPromise = null;
767
- this.activeProtocolV2Call = null;
878
+ const callOptions = options;
879
+
880
+ try {
881
+ return await this.protocolV2Links.call(
882
+ uuid,
883
+ () => this.createProtocolV2Adapter(uuid),
884
+ name,
885
+ data,
886
+ callOptions
887
+ );
888
+ } catch (e) {
889
+ this.Log?.error('[Electron BLE] Protocol V2 call error:', e);
890
+ throw e;
768
891
  }
892
+ }
769
893
 
770
- const runPromise = createDeferred<Uint8Array | string>();
771
- runPromise.promise.catch(() => undefined);
772
- this.runPromise = runPromise;
773
- const callToken = this.nextProtocolV2CallToken++;
774
- this.activeProtocolV2Call = { uuid, token: callToken };
775
- this.v2Assemblers.get(uuid)?.reset();
776
- this.resetProtocolV2Frames(uuid);
777
- let completed = false;
778
- const callOptions = {
779
- ...options,
780
- timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
894
+ private createProtocolV2Adapter(uuid: string) {
895
+ const generation = this.notificationTokens.get(uuid) ?? 0;
896
+ const assertCurrentGeneration = () => {
897
+ if (this.notificationTokens.get(uuid) !== generation) {
898
+ throw new Error(`Protocol V2 notification generation changed for ${uuid}`);
899
+ }
781
900
  };
782
901
 
783
- try {
784
- const session = new ProtocolV2Session({
785
- schemas: {
786
- protocolV1: this._messages,
787
- protocolV2: this._messagesV2,
788
- },
789
- router: PROTOCOL_V2_CHANNEL_BLE_UART,
790
- writeFrame: (frame: Uint8Array) => this.writeWithChunking(uuid, bytesToHex(frame)),
791
- readFrame: async () => {
792
- const rxFrame = await this.readProtocolV2Frame(uuid);
793
- if (!(rxFrame instanceof Uint8Array)) {
794
- throw new Error('Response is not Uint8Array');
795
- }
796
- return rxFrame;
797
- },
798
- logger: this.Log,
799
- logPrefix: 'ProtocolV2 BLE',
800
- createTimeoutError: (_messageName: string, timeout: number) =>
801
- ERRORS.TypedError(
802
- HardwareErrorCode.BleTimeoutError,
803
- `BLE response timeout after ${timeout}ms for ${name}`
804
- ),
805
- });
806
-
807
- const result = await session.call(name, data, callOptions);
808
- completed = true;
809
- return result;
810
- } catch (e) {
811
- if (this.isActiveProtocolV2Call(uuid, callToken)) {
902
+ return {
903
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
904
+ maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
905
+ generation,
906
+ prepareCall: () => {
907
+ assertCurrentGeneration();
812
908
  this.v2Assemblers.get(uuid)?.reset();
813
909
  this.resetProtocolV2Frames(uuid);
814
- }
815
- this.Log?.error('[Electron BLE] Protocol V2 call error:', e);
816
- throw e;
817
- } finally {
818
- if (this.isActiveProtocolV2Call(uuid, callToken)) {
819
- if (!completed) {
820
- this.v2Assemblers.get(uuid)?.reset();
910
+ },
911
+ writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) =>
912
+ this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
913
+ readFrame: async () => {
914
+ assertCurrentGeneration();
915
+ const rxFrame = await this.readProtocolV2Frame(uuid);
916
+ if (!(rxFrame instanceof Uint8Array)) {
917
+ throw new Error('Response is not Uint8Array');
821
918
  }
822
- this.resetProtocolV2Frames(uuid);
823
- this.activeProtocolV2Call = null;
824
- }
825
- if (this.runPromise === runPromise) {
826
- this.runPromise = null;
827
- }
828
- }
919
+ return rxFrame;
920
+ },
921
+ reset: (reason: string) => {
922
+ this.v2Assemblers.get(uuid)?.reset();
923
+ this.rejectProtocolV2Frames(uuid, new Error(reason));
924
+ },
925
+ logger: this.Log,
926
+ logPrefix: 'ProtocolV2 BLE',
927
+ createTimeoutError: (messageName: string, timeout: number) =>
928
+ ERRORS.TypedError(
929
+ HardwareErrorCode.BleTimeoutError,
930
+ `BLE response timeout after ${timeout}ms for ${messageName}`
931
+ ),
932
+ };
829
933
  }
830
934
 
831
935
  private processProtocolV1Notification(deviceId: string, hexData: string): PacketProcessResult {