@onekeyfe/hd-transport-web-device 1.1.28 → 1.2.0-alpha.1

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,20 +1,40 @@
1
- import transport, { COMMON_HEADER_SIZE, LogBlockCommand } from '@onekeyfe/hd-transport';
1
+ import transport, {
2
+ LogBlockCommand,
3
+ PROTOCOL_V1_MESSAGE_HEADER_SIZE,
4
+ PROTOCOL_V2_CHANNEL_BLE_UART,
5
+ ProtocolV2FrameAssembler,
6
+ ProtocolV2Session,
7
+ bytesToHex,
8
+ hexToBytes,
9
+ probeProtocolV2 as probeProtocolV2Helper,
10
+ } from '@onekeyfe/hd-transport';
2
11
  import {
3
12
  ERRORS,
4
13
  HardwareErrorCode,
5
14
  HardwareErrorCodeMessage,
6
15
  createDeferred,
7
16
  isHeaderChunk,
17
+ wait,
8
18
  } from '@onekeyfe/hd-shared';
9
19
 
10
20
  import type { Deferred } from '@onekeyfe/hd-shared';
11
- import type EventEmitter from 'events';
12
- // Import DesktopAPI type from hd-transport-electron
13
21
  import type { DesktopAPI } from '@onekeyfe/hd-transport-electron';
22
+ import type { OneKeyDeviceInfo, ProtocolType, TransportCallOptions } from '@onekeyfe/hd-transport';
23
+ import type EventEmitter from 'events';
24
+
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
+ }
14
35
 
15
- const { parseConfigure, buildBuffers, receiveOne, check } = transport;
36
+ const { parseConfigure, ProtocolV1, check } = transport;
16
37
 
17
- // Noble BLE specific API interface
18
38
  declare global {
19
39
  interface Window {
20
40
  desktopApi?: DesktopAPI;
@@ -24,44 +44,89 @@ declare global {
24
44
  export type BleAcquireInput = {
25
45
  uuid: string;
26
46
  forceCleanRunPromise?: boolean;
47
+ expectedProtocol?: ProtocolType;
27
48
  };
28
49
 
29
- // Packet processing result interface
30
50
  interface PacketProcessResult {
31
51
  isComplete: boolean;
32
52
  completePacket?: string;
33
53
  error?: string;
34
54
  }
35
55
 
56
+ function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
57
+ return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
58
+ }
59
+
60
+ const toBleDescriptor = (
61
+ device: { id: string; name: string | null },
62
+ protocolType?: ProtocolType
63
+ ): OneKeyDeviceInfo =>
64
+ ({
65
+ id: device.id,
66
+ name: device.name,
67
+ path: device.id,
68
+ debug: false,
69
+ commType: 'electron-ble',
70
+ ...(protocolType ? { protocolType } : {}),
71
+ } as OneKeyDeviceInfo);
72
+
73
+ const BLE_PACKET_SIZE = 192;
74
+ const BLE_WRITE_DELAY_MS = 5;
75
+ const BLE_WRITE_MAX_RETRIES = 3;
76
+ const BLE_WRITE_RETRY_DELAY_MS = 300;
77
+ const BLE_RESPONSE_TIMEOUT_MS = 30_000;
78
+ const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
79
+ const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
80
+
81
+ /**
82
+ * Desktop Electron BLE transport with automatic Protocol V1/V2 detection.
83
+ *
84
+ * Protocol V1 devices continue using chunked packets. Protocol V2 is detected
85
+ * after a Protocol V1 Initialize timeout by probing Protocol V2 Ping.
86
+ */
36
87
  export default class ElectronBleTransport {
37
- _messages: ReturnType<typeof transport.parseConfigure> | undefined;
88
+ private _messages: ReturnType<typeof transport.parseConfigure> | undefined;
89
+
90
+ private _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
38
91
 
39
92
  name = 'ElectronBleTransport';
40
93
 
41
94
  configured = false;
42
95
 
43
- runPromise: Deferred<any> | null = null;
96
+ runPromise: Deferred<Uint8Array | string> | null = null;
44
97
 
45
98
  Log?: any;
46
99
 
47
100
  emitter?: EventEmitter;
48
101
 
49
- // Cache for connected devices
50
102
  private connectedDevices: Set<string> = new Set();
51
103
 
52
- // Data processing state
53
- private dataBuffers: Map<string, { buffer: number[]; bufferLength: number }> = new Map();
104
+ private deviceProtocol: Map<string, ProtocolType> = new Map();
105
+
106
+ private deviceProtocolHints: Map<string, ProtocolType> = new Map();
107
+
108
+ private v1Buffers: Map<string, { buffer: number[]; bufferLength: number }> = new Map();
109
+
110
+ private v2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
111
+
112
+ private v2FrameQueues: Map<string, Uint8Array[]> = new Map();
113
+
114
+ private v2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
115
+
116
+ private activeProtocolV2Call: { uuid: string; token: number } | null = null;
117
+
118
+ private nextProtocolV2CallToken = 1;
54
119
 
55
- // Notification cleanup functions
56
120
  private notificationCleanups: Map<string, () => void> = new Map();
57
121
 
58
- // Disconnect listener cleanup functions
59
122
  private disconnectCleanups: Map<string, () => void> = new Map();
60
123
 
61
- // Handle bluetooth related errors with proper error code mapping
124
+ private notificationTokens: Map<string, number> = new Map();
125
+
126
+ private nextNotificationToken = 1;
127
+
62
128
  private handleBluetoothError(error: any): never {
63
129
  if (error && typeof error === 'object') {
64
- // Check for specific bluetooth error codes
65
130
  if ('code' in error) {
66
131
  if (error.code === HardwareErrorCode.BlePoweredOff) {
67
132
  throw ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
@@ -73,7 +138,6 @@ export default class ElectronBleTransport {
73
138
  throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
74
139
  }
75
140
  }
76
- // Check for error message containing bluetooth state related text using predefined messages
77
141
  const errorMessage = error.message || String(error);
78
142
  const poweredOffMessage = HardwareErrorCodeMessage[HardwareErrorCode.BlePoweredOff];
79
143
  const unsupportedMessage = HardwareErrorCodeMessage[HardwareErrorCode.BleUnsupported];
@@ -89,23 +153,28 @@ export default class ElectronBleTransport {
89
153
  throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
90
154
  }
91
155
  }
92
-
93
156
  throw error;
94
157
  }
95
158
 
96
- // Clean up all device state and listeners - unified cleanup function
97
159
  private cleanupDeviceState(deviceId: string): void {
98
160
  this.connectedDevices.delete(deviceId);
99
- this.dataBuffers.delete(deviceId);
161
+ this.deviceProtocol.delete(deviceId);
162
+ // Keep deviceProtocolHints — it's inferred from device name (e.g. "Pro 2" → V2)
163
+ // and doesn't depend on connection state. Preserving it avoids redundant V1 probe on reconnect.
164
+ this.v1Buffers.delete(deviceId);
165
+ this.v2Assemblers.delete(deviceId);
166
+ this.resetProtocolV2Frames(deviceId);
167
+ if (this.activeProtocolV2Call?.uuid === deviceId) {
168
+ this.activeProtocolV2Call = null;
169
+ }
170
+ this.notificationTokens.delete(deviceId);
100
171
 
101
- // Clean up notification listener
102
172
  const notifyCleanup = this.notificationCleanups.get(deviceId);
103
173
  if (notifyCleanup) {
104
174
  notifyCleanup();
105
175
  this.notificationCleanups.delete(deviceId);
106
176
  }
107
177
 
108
- // Clean up disconnect listener
109
178
  const disconnectCleanup = this.disconnectCleanups.get(deviceId);
110
179
  if (disconnectCleanup) {
111
180
  disconnectCleanup();
@@ -117,7 +186,6 @@ export default class ElectronBleTransport {
117
186
  this.Log = logger;
118
187
  this.emitter = emitter;
119
188
 
120
- // Check if Noble BLE API is available
121
189
  if (!window.desktopApi?.nobleBle) {
122
190
  throw ERRORS.TypedError(
123
191
  HardwareErrorCode.RuntimeError,
@@ -125,41 +193,57 @@ export default class ElectronBleTransport {
125
193
  );
126
194
  }
127
195
 
128
- this.Log?.debug('[Transport] Noble BLE Transport initialized');
196
+ this.Log?.debug('[Electron BLE] Transport initialized');
129
197
  }
130
198
 
131
199
  configure(signedData: any) {
132
- const messages = parseConfigure(signedData);
200
+ this._messages = parseConfigure(signedData);
133
201
  this.configured = true;
134
- this._messages = messages;
135
202
  }
136
203
 
137
- listen() {}
204
+ configureProtocolV2(signedData: any) {
205
+ this._messagesV2 = parseConfigure(signedData);
206
+ this.Log?.debug('[Electron BLE] Protocol V2 schema configured');
207
+ }
208
+
209
+ async listen() {
210
+ return this.enumerate();
211
+ }
138
212
 
139
- async enumerate(): Promise<{ id: string; name: string }[]> {
213
+ async enumerate(): Promise<OneKeyDeviceInfo[]> {
140
214
  try {
141
215
  if (!window.desktopApi?.nobleBle) {
142
216
  throw new Error('Noble BLE API not available');
143
217
  }
144
-
145
218
  const devices = await window.desktopApi.nobleBle.enumerate();
146
- return devices;
219
+ this.Log?.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
220
+ for (const dev of devices) {
221
+ this.Log?.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`);
222
+ const protocolHint = inferProtocolHintFromDeviceName(dev.name);
223
+ if (protocolHint) {
224
+ this.deviceProtocolHints.set(dev.id, protocolHint);
225
+ }
226
+ }
227
+ return devices.map(device => toBleDescriptor(device));
147
228
  } catch (error) {
148
- this.Log?.error('[Transport] Noble BLE enumerate failed:', error);
229
+ this.Log?.error('[Electron BLE] enumerate failed:', error);
149
230
  this.handleBluetoothError(error);
150
231
  }
151
232
  }
152
233
 
153
234
  async acquire(input: BleAcquireInput) {
154
- const { uuid, forceCleanRunPromise } = input;
235
+ const { uuid, forceCleanRunPromise, expectedProtocol } = input;
155
236
 
156
237
  if (!uuid) {
157
238
  throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
158
239
  }
159
240
 
160
- // Force clean running Promise
161
241
  if (forceCleanRunPromise && this.runPromise) {
162
- this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
242
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
243
+ this.runPromise.reject(error);
244
+ this.rejectAllProtocolV2Frames(error);
245
+ this.runPromise = null;
246
+ this.activeProtocolV2Call = null;
163
247
  }
164
248
 
165
249
  try {
@@ -167,13 +251,17 @@ export default class ElectronBleTransport {
167
251
  throw new Error('Noble BLE API not available');
168
252
  }
169
253
 
170
- // Check if device is available
171
254
  const device = await window.desktopApi.nobleBle.getDevice(uuid);
172
255
  if (!device) {
173
256
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${uuid} not found`);
174
257
  }
258
+ const protocolHint = expectedProtocol
259
+ ? undefined
260
+ : this.deviceProtocolHints.get(uuid) ?? inferProtocolHintFromDeviceName(device.name);
261
+ if (protocolHint) {
262
+ this.deviceProtocolHints.set(uuid, protocolHint);
263
+ }
175
264
 
176
- // Connect to device
177
265
  try {
178
266
  await window.desktopApi.nobleBle.connect(uuid);
179
267
  this.connectedDevices.add(uuid);
@@ -181,29 +269,18 @@ export default class ElectronBleTransport {
181
269
  this.handleBluetoothError(error);
182
270
  }
183
271
 
184
- // Initialize data buffer for this device
185
- this.dataBuffers.set(uuid, { buffer: [], bufferLength: 0 });
272
+ this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
273
+ this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
186
274
 
187
- // Subscribe to notifications
188
275
  await window.desktopApi.nobleBle.subscribe(uuid);
189
276
 
190
- // Set up notification listener
191
- const cleanup = window.desktopApi.nobleBle.onNotification(
192
- (deviceId: string, data: string) => {
193
- if (deviceId === uuid) {
194
- this.handleNotificationData(uuid, data);
195
- }
196
- }
197
- );
277
+ const cleanup = this.createNotificationSubscription(uuid);
198
278
  this.notificationCleanups.set(uuid, cleanup);
199
279
 
200
- // Set up disconnect listener
201
280
  const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
202
281
  (disconnectedDevice: any) => {
203
282
  if (disconnectedDevice.id === uuid) {
204
283
  this.cleanupDeviceState(uuid);
205
-
206
- // Trigger disconnect event
207
284
  this.emitter?.emit('device-disconnect', {
208
285
  name: disconnectedDevice.name,
209
286
  id: disconnectedDevice.id,
@@ -214,16 +291,29 @@ export default class ElectronBleTransport {
214
291
  );
215
292
  this.disconnectCleanups.set(uuid, disconnectCleanup);
216
293
 
217
- // Trigger connect event
294
+ const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
295
+
218
296
  this.emitter?.emit('device-connect', {
219
297
  name: device.name,
220
298
  id: device.id,
221
299
  connectId: device.id,
222
300
  });
223
301
 
224
- return { uuid, path: uuid };
302
+ return {
303
+ ...toBleDescriptor({ id: device.id, name: device.name }, protocolType),
304
+ uuid,
305
+ };
225
306
  } catch (error) {
226
- this.Log?.error('[Transport] Noble BLE acquire failed:', error);
307
+ this.Log?.error('[Electron BLE] acquire failed:', error);
308
+ try {
309
+ if (window.desktopApi?.nobleBle && this.connectedDevices.has(uuid)) {
310
+ await window.desktopApi.nobleBle.unsubscribe(uuid);
311
+ await window.desktopApi.nobleBle.disconnect(uuid);
312
+ }
313
+ } catch (cleanupError) {
314
+ this.Log?.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
315
+ }
316
+ this.cleanupDeviceState(uuid);
227
317
  throw error;
228
318
  }
229
319
  }
@@ -231,157 +321,550 @@ export default class ElectronBleTransport {
231
321
  async release(id: string) {
232
322
  try {
233
323
  if (this.connectedDevices.has(id)) {
234
- // Unsubscribe from notifications
235
324
  if (window.desktopApi?.nobleBle) {
236
325
  await window.desktopApi.nobleBle.unsubscribe(id);
237
- }
238
-
239
- // Disconnect device
240
- if (window.desktopApi?.nobleBle) {
241
326
  await window.desktopApi.nobleBle.disconnect(id);
242
327
  }
243
-
244
- // Clean up all device state
245
328
  this.cleanupDeviceState(id);
246
329
  }
247
330
  } catch (error) {
248
- this.Log?.error('[Transport] Noble BLE release failed:', error);
249
- // Clean up local state even if release fails
331
+ this.Log?.error('[Electron BLE] release failed:', error);
250
332
  this.cleanupDeviceState(id);
251
333
  }
252
334
  }
253
335
 
254
- // Handle notification data from Noble BLE
255
- private handleNotificationData(deviceId: string, hexData: string): void {
256
- // Check for pairing rejection
336
+ private createProtocolMismatchError(expected: ProtocolType) {
337
+ return ERRORS.TypedError(
338
+ HardwareErrorCode.RuntimeError,
339
+ `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
340
+ );
341
+ }
342
+
343
+ private createProtocolDetectionError() {
344
+ return ERRORS.TypedError(
345
+ HardwareErrorCode.BleTimeoutError,
346
+ 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
347
+ );
348
+ }
349
+
350
+ private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
351
+ if (this.deviceProtocol.get(uuid) === protocol) {
352
+ this.deviceProtocol.delete(uuid);
353
+ }
354
+ }
355
+
356
+ private async detectProtocol(
357
+ uuid: string,
358
+ expectedProtocol?: ProtocolType,
359
+ protocolHint?: ProtocolType
360
+ ): Promise<ProtocolType> {
361
+ if (expectedProtocol === 'V1') {
362
+ if (await this.probeProtocolV1(uuid)) {
363
+ this.deviceProtocol.set(uuid, 'V1');
364
+ this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected)`);
365
+ return 'V1';
366
+ }
367
+ throw this.createProtocolMismatchError(expectedProtocol);
368
+ }
369
+
370
+ if (expectedProtocol === 'V2') {
371
+ // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景,
372
+ // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。
373
+ this.deviceProtocol.set(uuid, 'V2');
374
+ this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
375
+ return 'V2';
376
+ }
377
+
378
+ // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
379
+ // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
380
+ // 不能作为最终结论。
381
+ const probeOrder: ProtocolType[] =
382
+ protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
383
+
384
+ for (let i = 0; i < probeOrder.length; i += 1) {
385
+ const protocol = probeOrder[i];
386
+ if (i > 0) {
387
+ // 上一个协议探测失败后,重置订阅与缓冲,避免残留数据干扰下一个协议的探测。
388
+ await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
389
+ }
390
+ const detected =
391
+ protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
392
+ if (detected) {
393
+ this.deviceProtocol.set(uuid, protocol);
394
+ this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
395
+ return protocol;
396
+ }
397
+ }
398
+
399
+ this.deviceProtocol.delete(uuid);
400
+ throw this.createProtocolDetectionError();
401
+ }
402
+
403
+ private createNotificationSubscription(uuid: string) {
404
+ if (!window.desktopApi?.nobleBle) {
405
+ throw new Error('Noble BLE API not available');
406
+ }
407
+
408
+ const notificationToken = this.nextNotificationToken;
409
+ this.nextNotificationToken += 1;
410
+ this.notificationTokens.set(uuid, notificationToken);
411
+
412
+ return window.desktopApi.nobleBle.onNotification((deviceId: string, data: string) => {
413
+ if (deviceId === uuid && this.notificationTokens.get(uuid) === notificationToken) {
414
+ this.handleNotification(uuid, data);
415
+ }
416
+ });
417
+ }
418
+
419
+ private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
420
+ this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
421
+ this.v2Assemblers.get(uuid)?.reset();
422
+ this.resetProtocolV2Frames(uuid);
423
+ if (this.activeProtocolV2Call?.uuid === uuid) {
424
+ this.activeProtocolV2Call = null;
425
+ }
426
+ if (this.runPromise) {
427
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
428
+ this.runPromise.reject(error);
429
+ this.runPromise = null;
430
+ }
431
+
432
+ const notifyCleanup = this.notificationCleanups.get(uuid);
433
+ if (notifyCleanup) {
434
+ notifyCleanup();
435
+ this.notificationCleanups.delete(uuid);
436
+ }
437
+ this.notificationTokens.delete(uuid);
438
+
439
+ try {
440
+ await window.desktopApi?.nobleBle?.unsubscribe(uuid);
441
+ } catch (error) {
442
+ this.Log?.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error);
443
+ }
444
+ try {
445
+ await window.desktopApi?.nobleBle?.subscribe(uuid);
446
+ } catch (error) {
447
+ this.Log?.debug(`[Electron BLE] resubscribe after Protocol ${protocol} probe failed:`, error);
448
+ throw error;
449
+ }
450
+
451
+ const cleanup = this.createNotificationSubscription(uuid);
452
+ this.notificationCleanups.set(uuid, cleanup);
453
+ }
454
+
455
+ private async probeProtocolV1(uuid: string) {
456
+ if (!this._messages) {
457
+ return false;
458
+ }
459
+
460
+ try {
461
+ this.deviceProtocol.set(uuid, 'V1');
462
+ await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
463
+ return true;
464
+ } catch (error) {
465
+ this.clearProbeProtocol(uuid, 'V1');
466
+ this.Log?.debug('[Electron BLE] Protocol V1 Initialize probe failed:', error);
467
+ return false;
468
+ }
469
+ }
470
+
471
+ private async probeProtocolV2(uuid: string) {
472
+ if (!this._messages || !this._messagesV2) {
473
+ return false;
474
+ }
475
+
476
+ this.deviceProtocol.set(uuid, 'V2');
477
+ this.v2Assemblers.get(uuid)?.reset();
478
+ const detected = await probeProtocolV2Helper({
479
+ call: (name, data, options) => this.callProtocolV2(uuid, name, data, options),
480
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
481
+ logger: this.Log,
482
+ logPrefix: 'ProtocolV2 BLE',
483
+ onProbeFailed: () => {
484
+ this.v2Assemblers.get(uuid)?.reset();
485
+ this.resetProtocolV2Frames(uuid);
486
+ },
487
+ });
488
+ if (!detected) {
489
+ this.clearProbeProtocol(uuid, 'V2');
490
+ }
491
+ return detected;
492
+ }
493
+
494
+ private async writeWithChunking(uuid: string, hexData: string): Promise<void> {
495
+ const totalBytes = hexData.length / 2;
496
+
497
+ if (totalBytes <= BLE_PACKET_SIZE) {
498
+ await wait(BLE_WRITE_DELAY_MS);
499
+ await this.writeWithRetry(uuid, hexData);
500
+ return;
501
+ }
502
+
503
+ for (let offset = 0; offset < hexData.length; ) {
504
+ const chunkHexLen = Math.min(BLE_PACKET_SIZE * 2, hexData.length - offset);
505
+ const chunkHex = hexData.substring(offset, offset + chunkHexLen);
506
+ offset += chunkHexLen;
507
+
508
+ await this.writeWithRetry(uuid, chunkHex);
509
+
510
+ if (offset < hexData.length) {
511
+ await wait(BLE_WRITE_DELAY_MS);
512
+ }
513
+ }
514
+ }
515
+
516
+ private async writeWithRetry(uuid: string, hexData: string): Promise<void> {
517
+ let lastError: any;
518
+ const nobleBle = window.desktopApi?.nobleBle;
519
+ if (!nobleBle) {
520
+ throw new Error('Noble BLE API not available');
521
+ }
522
+
523
+ for (let attempt = 1; attempt <= BLE_WRITE_MAX_RETRIES; attempt++) {
524
+ try {
525
+ await nobleBle.write(uuid, hexData);
526
+ return;
527
+ } catch (error) {
528
+ lastError = error;
529
+ this.Log?.error(
530
+ `[Electron BLE] write failed (attempt ${attempt}/${BLE_WRITE_MAX_RETRIES}):`,
531
+ error
532
+ );
533
+ if (attempt < BLE_WRITE_MAX_RETRIES) {
534
+ await wait(BLE_WRITE_RETRY_DELAY_MS);
535
+ }
536
+ }
537
+ }
538
+ throw ERRORS.TypedError(
539
+ HardwareErrorCode.BleWriteCharacteristicError,
540
+ `BLE write failed after ${BLE_WRITE_MAX_RETRIES} attempts: ${lastError?.message ?? lastError}`
541
+ );
542
+ }
543
+
544
+ private handleNotification(deviceId: string, hexData: string): void {
257
545
  if (hexData === 'PAIRING_REJECTED') {
258
- this.Log?.debug('[Transport] Pairing rejection detected for device:', deviceId);
546
+ this.Log?.debug('[Electron BLE] Pairing rejection detected for device:', deviceId);
259
547
  if (this.runPromise) {
260
- this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled));
548
+ const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled);
549
+ this.runPromise.reject(error);
550
+ this.rejectAllProtocolV2Frames(error);
261
551
  }
262
552
  return;
263
553
  }
264
554
 
265
- const result = this.processNotificationPacket(deviceId, hexData);
555
+ const protocol = this.deviceProtocol.get(deviceId);
556
+ if (!protocol) {
557
+ this.Log?.debug('[Electron BLE] Ignore notification before protocol detection:', deviceId);
558
+ return;
559
+ }
560
+ if (protocol === 'V2') {
561
+ this.handleProtocolV2Notification(deviceId, hexData);
562
+ return;
563
+ }
564
+ this.handleProtocolV1Notification(deviceId, hexData);
565
+ }
266
566
 
267
- if (result.error) {
268
- this.Log?.error('[Transport] Packet processing error:', result.error);
567
+ private handleProtocolV2Notification(deviceId: string, hexData: string): void {
568
+ try {
569
+ if (!this.runPromise || this.activeProtocolV2Call?.uuid !== deviceId) {
570
+ this.v2Assemblers.get(deviceId)?.reset();
571
+ this.resetProtocolV2Frames(deviceId);
572
+ return;
573
+ }
574
+
575
+ const bytes = hexToBytes(hexData);
576
+ if (bytes.length === 0) return;
577
+
578
+ const assembler = this.v2Assemblers.get(deviceId);
579
+ if (!assembler) return;
580
+
581
+ for (const frameData of assembler.drain(bytes)) {
582
+ this.resolveProtocolV2Frame(deviceId, frameData);
583
+ }
584
+ } catch (error) {
585
+ this.Log?.error('[Electron BLE] Protocol V2 notification error:', error);
269
586
  if (this.runPromise) {
270
- this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
587
+ const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
588
+ this.runPromise.reject(notifyError);
589
+ this.rejectAllProtocolV2Frames(notifyError);
271
590
  }
591
+ }
592
+ }
593
+
594
+ private getProtocolV2FrameQueue(uuid: string) {
595
+ let queue = this.v2FrameQueues.get(uuid);
596
+ if (!queue) {
597
+ queue = [];
598
+ this.v2FrameQueues.set(uuid, queue);
599
+ }
600
+ return queue;
601
+ }
602
+
603
+ private resolveProtocolV2Frame(uuid: string, frame: Uint8Array) {
604
+ const framePromise = this.v2FramePromises.get(uuid);
605
+ if (framePromise) {
606
+ framePromise.resolve(frame);
607
+ this.v2FramePromises.delete(uuid);
272
608
  return;
273
609
  }
610
+ this.getProtocolV2FrameQueue(uuid).push(frame);
611
+ }
274
612
 
275
- if (result.isComplete && result.completePacket) {
276
- if (this.runPromise) {
277
- this.runPromise.resolve(result.completePacket);
613
+ private rejectAllProtocolV2Frames(error: Error) {
614
+ this.v2FrameQueues.clear();
615
+ for (const framePromise of this.v2FramePromises.values()) {
616
+ framePromise.reject(error);
617
+ }
618
+ this.v2FramePromises.clear();
619
+ }
620
+
621
+ private resetProtocolV2Frames(uuid: string) {
622
+ this.v2FrameQueues.delete(uuid);
623
+ this.v2FramePromises.delete(uuid);
624
+ }
625
+
626
+ private isActiveProtocolV2Call(uuid: string, token: number) {
627
+ return this.activeProtocolV2Call?.uuid === uuid && this.activeProtocolV2Call.token === token;
628
+ }
629
+
630
+ private async readProtocolV2Frame(uuid: string) {
631
+ const queuedFrame = this.getProtocolV2FrameQueue(uuid).shift();
632
+ if (queuedFrame) {
633
+ return queuedFrame;
634
+ }
635
+
636
+ const framePromise = createDeferred<Uint8Array>();
637
+ this.v2FramePromises.set(uuid, framePromise);
638
+ try {
639
+ return await framePromise.promise;
640
+ } finally {
641
+ if (this.v2FramePromises.get(uuid) === framePromise) {
642
+ this.v2FramePromises.delete(uuid);
278
643
  }
279
644
  }
280
645
  }
281
646
 
282
- async call(uuid: string, name: string, data: Record<string, unknown>) {
283
- if (this._messages == null) {
284
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
647
+ private handleProtocolV1Notification(deviceId: string, hexData: string): void {
648
+ const result = this.processProtocolV1Notification(deviceId, hexData);
649
+
650
+ if (result.error) {
651
+ this.Log?.error('[Electron BLE] Protocol V1 packet processing error:', result.error);
652
+ if (this.runPromise) {
653
+ this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
654
+ }
655
+ return;
285
656
  }
286
657
 
287
- const forceRun = name === 'Initialize' || name === 'Cancel';
658
+ if (result.isComplete && result.completePacket && this.runPromise) {
659
+ this.runPromise.resolve(result.completePacket);
660
+ }
661
+ }
288
662
 
289
- if (this.runPromise && !forceRun) {
290
- throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
663
+ async call(
664
+ uuid: string,
665
+ name: string,
666
+ data: Record<string, unknown>,
667
+ options?: TransportCallOptions
668
+ ) {
669
+ if (!this._messages) {
670
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
291
671
  }
292
672
 
293
673
  if (!this.connectedDevices.has(uuid)) {
294
674
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound, `Device ${uuid} not connected`);
295
675
  }
296
676
 
297
- this.runPromise = createDeferred();
298
- const messages = this._messages;
299
-
300
- // Log different types of commands appropriately
301
- if (name === 'ResourceUpdate' || name === 'ResourceAck') {
302
- this.Log?.debug('[Transport] Noble BLE call', 'name:', name, 'data:', {
303
- file_name: data?.file_name,
304
- hash: data?.hash,
305
- });
306
- } else if (LogBlockCommand.has(name)) {
307
- this.Log?.debug('[Transport] Noble BLE call', 'name:', name);
677
+ const protocol = this.deviceProtocol.get(uuid);
678
+ if (!protocol) {
679
+ throw ERRORS.TypedError(
680
+ HardwareErrorCode.RuntimeError,
681
+ `Device protocol has not been detected for ${uuid}`
682
+ );
683
+ }
684
+ if (shouldSuppressHighVolumeCallLog(name)) {
685
+ // 高频文件写入不要逐包发 debug 事件,否则调试日志会反向拖慢传输。
686
+ } else if (isLogBlockCommand(name)) {
687
+ this.Log?.debug('[Electron BLE] call', 'name:', name, 'protocol:', protocol);
308
688
  } else {
309
- this.Log?.debug('[Transport] Noble BLE call', 'name:', name, 'data:', data);
689
+ this.Log?.debug('[Electron BLE] call', 'name:', name, 'data:', data, 'protocol:', protocol);
690
+ }
691
+
692
+ if (protocol === 'V2') {
693
+ return this.callProtocolV2(uuid, name, data, options);
310
694
  }
695
+ return this.callProtocolV1(uuid, name, data, options);
696
+ }
311
697
 
312
- const buffers = buildBuffers(messages, name, data);
698
+ private async callProtocolV1(
699
+ uuid: string,
700
+ name: string,
701
+ data: Record<string, unknown>,
702
+ options?: TransportCallOptions
703
+ ) {
704
+ if (!this._messages) {
705
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
706
+ }
707
+
708
+ const forceRun = name === 'Initialize' || name === 'Cancel';
709
+ if (this.runPromise && !forceRun) {
710
+ throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
711
+ }
712
+
713
+ const runPromise = createDeferred<Uint8Array | string>();
714
+ runPromise.promise.catch(() => undefined);
715
+ this.runPromise = runPromise;
716
+ const messages = this._messages;
717
+ const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
718
+ let timeout: ReturnType<typeof setTimeout> | undefined;
313
719
 
314
720
  try {
315
721
  if (!window.desktopApi?.nobleBle) {
316
722
  throw new Error('Noble BLE write API not available');
317
723
  }
318
724
 
319
- // Write each buffer to the device
320
725
  for (let i = 0; i < buffers.length; i++) {
321
726
  const buffer = buffers[i];
322
-
323
727
  if (!buffer || typeof buffer.toString !== 'function') {
324
- this.Log?.error(`[Transport] Noble BLE buffer ${i + 1} is invalid:`, buffer);
325
728
  throw new Error(`Buffer ${i + 1} is invalid`);
326
729
  }
327
-
328
- // Use ByteBuffer's toString('hex') method directly, similar to other transports
329
730
  const hexString = buffer.toString('hex');
330
-
331
731
  if (hexString.length === 0) {
332
- this.Log?.error(`[Transport] Noble BLE buffer ${i + 1} generated empty hex string`);
333
732
  throw new Error(`Buffer ${i + 1} is empty`);
334
733
  }
335
-
336
734
  await window.desktopApi.nobleBle.write(uuid, hexString);
337
735
  }
338
736
 
339
- // Wait for response
340
- const response = await this.runPromise.promise;
341
-
737
+ const response = await Promise.race([
738
+ runPromise.promise,
739
+ new Promise<never>((_, reject) => {
740
+ if (options?.timeoutMs) {
741
+ timeout = setTimeout(() => {
742
+ const error = ERRORS.TypedError(
743
+ HardwareErrorCode.BleTimeoutError,
744
+ `BLE response timeout after ${options.timeoutMs}ms for ${name}`
745
+ );
746
+ runPromise.reject(error);
747
+ reject(error);
748
+ }, options.timeoutMs);
749
+ }
750
+ }),
751
+ ]);
342
752
  if (typeof response !== 'string') {
343
753
  throw new Error('Returning data is not string.');
344
754
  }
345
755
 
346
- const jsonData = receiveOne(messages, response);
756
+ const jsonData = ProtocolV1.decodeMessage(messages, response);
347
757
  return check.call(jsonData);
348
758
  } catch (e) {
349
- this.Log?.error('[Transport] Noble BLE call error:', e);
759
+ this.Log?.error('[Electron BLE] Protocol V1 call error:', e);
350
760
  throw e;
351
761
  } finally {
762
+ if (timeout) clearTimeout(timeout);
763
+ if (this.runPromise === runPromise) {
764
+ this.runPromise = null;
765
+ }
766
+ }
767
+ }
768
+
769
+ private async callProtocolV2(
770
+ uuid: string,
771
+ name: string,
772
+ data: Record<string, unknown>,
773
+ options?: TransportCallOptions
774
+ ) {
775
+ if (!this._messages || !this._messagesV2) {
776
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
777
+ }
778
+
779
+ const forceRun = name === 'Initialize' || name === 'Cancel' || name === 'Ping';
780
+ if (this.runPromise) {
781
+ if (!forceRun) {
782
+ throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
783
+ }
784
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
785
+ this.runPromise.reject(error);
786
+ this.rejectAllProtocolV2Frames(error);
352
787
  this.runPromise = null;
788
+ this.activeProtocolV2Call = null;
789
+ }
790
+
791
+ const runPromise = createDeferred<Uint8Array | string>();
792
+ runPromise.promise.catch(() => undefined);
793
+ this.runPromise = runPromise;
794
+ const callToken = this.nextProtocolV2CallToken++;
795
+ this.activeProtocolV2Call = { uuid, token: callToken };
796
+ this.v2Assemblers.get(uuid)?.reset();
797
+ this.resetProtocolV2Frames(uuid);
798
+ let completed = false;
799
+ const callOptions = {
800
+ ...options,
801
+ timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
802
+ };
803
+
804
+ try {
805
+ const session = new ProtocolV2Session({
806
+ schemas: {
807
+ protocolV1: this._messages,
808
+ protocolV2: this._messagesV2,
809
+ },
810
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
811
+ writeFrame: (frame: Uint8Array) => this.writeWithChunking(uuid, bytesToHex(frame)),
812
+ readFrame: async () => {
813
+ const rxFrame = await this.readProtocolV2Frame(uuid);
814
+ if (!(rxFrame instanceof Uint8Array)) {
815
+ throw new Error('Response is not Uint8Array');
816
+ }
817
+ return rxFrame;
818
+ },
819
+ logger: this.Log,
820
+ logPrefix: 'ProtocolV2 BLE',
821
+ createTimeoutError: (_messageName: string, timeout: number) =>
822
+ ERRORS.TypedError(
823
+ HardwareErrorCode.BleTimeoutError,
824
+ `BLE response timeout after ${timeout}ms for ${name}`
825
+ ),
826
+ });
827
+
828
+ const result = await session.call(name, data, callOptions);
829
+ completed = true;
830
+ return result;
831
+ } catch (e) {
832
+ if (this.isActiveProtocolV2Call(uuid, callToken)) {
833
+ this.v2Assemblers.get(uuid)?.reset();
834
+ this.resetProtocolV2Frames(uuid);
835
+ }
836
+ this.Log?.error('[Electron BLE] Protocol V2 call error:', e);
837
+ throw e;
838
+ } finally {
839
+ if (this.isActiveProtocolV2Call(uuid, callToken)) {
840
+ if (!completed) {
841
+ this.v2Assemblers.get(uuid)?.reset();
842
+ }
843
+ this.resetProtocolV2Frames(uuid);
844
+ this.activeProtocolV2Call = null;
845
+ }
846
+ if (this.runPromise === runPromise) {
847
+ this.runPromise = null;
848
+ }
353
849
  }
354
850
  }
355
851
 
356
- // Process hex data from notification with validation and packet reassembly
357
- private processNotificationPacket(deviceId: string, hexData: string): PacketProcessResult {
852
+ private processProtocolV1Notification(deviceId: string, hexData: string): PacketProcessResult {
358
853
  try {
359
- // Validate input
360
854
  if (typeof hexData !== 'string') {
361
855
  return { isComplete: false, error: 'Invalid hexData type' };
362
856
  }
363
857
 
364
- // Clean and validate hex format
365
- const cleanHexData = hexData.replace(/\s+/g, '');
366
- if (!/^[0-9A-Fa-f]*$/.test(cleanHexData)) {
367
- return { isComplete: false, error: 'Invalid hex data format' };
858
+ const data = hexToBytes(hexData);
859
+ if (data.length === 0) {
860
+ return { isComplete: false, error: 'Empty or invalid hex data' };
368
861
  }
369
862
 
370
- // Convert hex string to Uint8Array
371
- const hexMatch = cleanHexData.match(/.{1,2}/g);
372
- if (!hexMatch) {
373
- return { isComplete: false, error: 'Failed to parse hex data' };
374
- }
375
-
376
- const data = new Uint8Array(hexMatch.map(byte => parseInt(byte, 16)));
377
-
378
- // Get buffer state
379
- const bufferState = this.dataBuffers.get(deviceId);
863
+ const bufferState = this.v1Buffers.get(deviceId);
380
864
  if (!bufferState) {
381
865
  return { isComplete: false, error: 'No buffer state for device' };
382
866
  }
383
867
 
384
- // Process header or data chunk
385
868
  if (isHeaderChunk(data)) {
386
869
  const dataView = new DataView(data.buffer);
387
870
  bufferState.bufferLength = dataView.getInt32(5, false);
@@ -390,20 +873,12 @@ export default class ElectronBleTransport {
390
873
  bufferState.buffer = bufferState.buffer.concat([...data]);
391
874
  }
392
875
 
393
- // Check if packet is complete
394
- if (bufferState.buffer.length - COMMON_HEADER_SIZE >= bufferState.bufferLength) {
876
+ if (bufferState.buffer.length - PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferState.bufferLength) {
395
877
  const completeBuffer = new Uint8Array(bufferState.buffer);
396
-
397
- // Reset buffer state
398
878
  bufferState.bufferLength = 0;
399
879
  bufferState.buffer = [];
400
880
 
401
- // Convert to hex string
402
- const hexString = Array.from(completeBuffer)
403
- .map(b => b.toString(16).padStart(2, '0'))
404
- .join('');
405
-
406
- return { isComplete: true, completePacket: hexString };
881
+ return { isComplete: true, completePacket: bytesToHex(completeBuffer) };
407
882
  }
408
883
 
409
884
  return { isComplete: false };
@@ -411,4 +886,8 @@ export default class ElectronBleTransport {
411
886
  return { isComplete: false, error: `Packet processing error: ${error}` };
412
887
  }
413
888
  }
889
+
890
+ getProtocolType(path: string): ProtocolType | undefined {
891
+ return this.deviceProtocol.get(path);
892
+ }
414
893
  }