@onekeyfe/hd-transport-web-device 1.2.0-alpha.15 → 1.2.0-alpha.151

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,5 +1,6 @@
1
1
  import transport, { PROTOCOL_V2_CHANNEL_BLE_UART, bytesToHex } from '@onekeyfe/hd-transport';
2
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+ import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
3
+ import EventEmitter from 'events';
3
4
 
4
5
  import ElectronBleTransport from '../src/electron-ble-transport';
5
6
 
@@ -10,6 +11,9 @@ const protocolV1Schema = {
10
11
  Initialize: {
11
12
  fields: {},
12
13
  },
14
+ GetFeatures: {
15
+ fields: {},
16
+ },
13
17
  Success: {
14
18
  fields: {
15
19
  message: {
@@ -22,6 +26,7 @@ const protocolV1Schema = {
22
26
  values: {
23
27
  MessageType_Initialize: 1,
24
28
  MessageType_Success: 2,
29
+ MessageType_GetFeatures: 55,
25
30
  },
26
31
  },
27
32
  },
@@ -100,6 +105,7 @@ const createNobleBle = (device = { id: 'flaky-pro2-id', name: 'Unknown BLE Devic
100
105
  unsubscribe: jest.fn(() => Promise.resolve()),
101
106
  write: jest.fn(() => Promise.resolve()),
102
107
  onNotification: jest.fn(() => jest.fn()),
108
+ onMtuChanged: jest.fn(() => jest.fn()),
103
109
  onDeviceDisconnected: jest.fn(() => jest.fn()),
104
110
  checkAvailability: jest.fn(() =>
105
111
  Promise.resolve({
@@ -111,7 +117,10 @@ const createNobleBle = (device = { id: 'flaky-pro2-id', name: 'Unknown BLE Devic
111
117
  ),
112
118
  });
113
119
 
114
- const configureTransport = (nobleBle: ReturnType<typeof createNobleBle>) => {
120
+ const configureTransport = (
121
+ nobleBle: ReturnType<typeof createNobleBle>,
122
+ emitter?: EventEmitter
123
+ ) => {
115
124
  (global as any).window = {
116
125
  desktopApi: {
117
126
  nobleBle,
@@ -119,7 +128,7 @@ const configureTransport = (nobleBle: ReturnType<typeof createNobleBle>) => {
119
128
  };
120
129
 
121
130
  const transport = new ElectronBleTransport();
122
- transport.init(createLogger());
131
+ transport.init(createLogger(), emitter);
123
132
  transport.configure(protocolV1Schema);
124
133
  transport.configureProtocolV2(protocolV2Schema);
125
134
  return transport;
@@ -131,6 +140,128 @@ describe('ElectronBleTransport protocol detection', () => {
131
140
  jest.clearAllMocks();
132
141
  });
133
142
 
143
+ test('keeps raw BLE lifecycle payloads off the public device event channel', async () => {
144
+ const device = { id: 'lifecycle-pro2-id', name: 'OneKey Pro 2' };
145
+ const nobleBle = createNobleBle(device);
146
+ const emitter = new EventEmitter();
147
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
148
+ let disconnectHandler: ((device: { id: string; name: string | null }) => void) | undefined;
149
+ let responseSeq = 0;
150
+
151
+ nobleBle.onNotification.mockImplementation(handler => {
152
+ notificationHandler = handler;
153
+ return jest.fn();
154
+ });
155
+ nobleBle.onDeviceDisconnected.mockImplementation(handler => {
156
+ disconnectHandler = handler;
157
+ return jest.fn();
158
+ });
159
+ nobleBle.write.mockImplementation(() => {
160
+ responseSeq += 1;
161
+ const response = ProtocolV2.encodeFrame(
162
+ schemas,
163
+ 'Success',
164
+ { message: 'ok' },
165
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
166
+ );
167
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
168
+ return Promise.resolve();
169
+ });
170
+
171
+ const publicConnect = jest.fn();
172
+ const publicDisconnect = jest.fn();
173
+ const transportDisconnect = jest.fn();
174
+ emitter.on('device-connect', publicConnect);
175
+ emitter.on('device-disconnect', publicDisconnect);
176
+ emitter.on('transport-device-disconnect', transportDisconnect);
177
+ const bleTransport = configureTransport(nobleBle, emitter);
178
+
179
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
180
+ expect(nobleBle.subscribe.mock.invocationCallOrder[0]).toBeLessThan(
181
+ nobleBle.getDevice.mock.invocationCallOrder[1]
182
+ );
183
+ disconnectHandler?.(device);
184
+
185
+ expect(publicConnect).not.toHaveBeenCalled();
186
+ expect(publicDisconnect).not.toHaveBeenCalled();
187
+ expect(transportDisconnect).toHaveBeenCalledWith({
188
+ id: device.id,
189
+ connectId: device.id,
190
+ name: device.name,
191
+ });
192
+ });
193
+
194
+ test('uses the Protocol V2 BLE writer with the Electron packet size', async () => {
195
+ const device = { id: 'chunked-pro2-id', name: 'OneKey Pro 2' };
196
+ const nobleBle = createNobleBle(device);
197
+ const bleTransport = configureTransport(nobleBle) as any;
198
+ const context = {
199
+ messageName: 'Ping',
200
+ timeoutMs: 1000,
201
+ highThroughput: false,
202
+ generation: 1,
203
+ signal: new AbortController().signal,
204
+ };
205
+
206
+ await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(193), context, jest.fn());
207
+
208
+ expect(nobleBle.write).toHaveBeenCalledTimes(2);
209
+ expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([192, 1]);
210
+ expect(nobleBle.write.mock.calls.every(([, , options]) => options?.pacingDelayMs === 0)).toBe(
211
+ true
212
+ );
213
+ });
214
+
215
+ test('uses the negotiated Noble MTU for Protocol V2 BLE writes', async () => {
216
+ const device = { id: 'mtu-pro2-id', name: 'OneKey Pro 2', mtu: 247 };
217
+ const nobleBle = createNobleBle(device);
218
+ const bleTransport = configureTransport(nobleBle) as any;
219
+ const context = {
220
+ messageName: 'FilesystemFileWrite',
221
+ timeoutMs: 1000,
222
+ highThroughput: true,
223
+ generation: 1,
224
+ signal: new AbortController().signal,
225
+ };
226
+
227
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
228
+ try {
229
+ await bleTransport.refreshBlePacketCapacity(device.id);
230
+ await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(245), context, jest.fn());
231
+
232
+ expect(setTimeoutSpy).not.toHaveBeenCalled();
233
+ } finally {
234
+ setTimeoutSpy.mockRestore();
235
+ }
236
+
237
+ expect(nobleBle.write).toHaveBeenCalledTimes(2);
238
+ expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([244, 1]);
239
+ });
240
+
241
+ test('updates Protocol V2 packet capacity when Noble reports a new MTU', async () => {
242
+ const device = { id: 'mtu-event-pro2-id', name: 'OneKey Pro 2' };
243
+ const nobleBle = createNobleBle(device);
244
+ let mtuHandler: ((changedDevice: { id: string; mtu: number }) => void) | undefined;
245
+ nobleBle.onMtuChanged.mockImplementation(handler => {
246
+ mtuHandler = handler;
247
+ return jest.fn();
248
+ });
249
+ const bleTransport = configureTransport(nobleBle) as any;
250
+ const context = {
251
+ messageName: 'FilesystemFileWrite',
252
+ timeoutMs: 1000,
253
+ highThroughput: true,
254
+ generation: 1,
255
+ signal: new AbortController().signal,
256
+ };
257
+
258
+ bleTransport.createMtuSubscription(device.id);
259
+ mtuHandler?.({ id: device.id, mtu: 247 });
260
+ await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(245), context, jest.fn());
261
+
262
+ expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([244, 1]);
263
+ });
264
+
134
265
  test('detects Protocol V2 after Protocol V1 probe timeout', async () => {
135
266
  const device = { id: 'unknown-pro2-id', name: 'Unknown BLE Device' };
136
267
  const nobleBle = createNobleBle(device);
@@ -141,7 +272,6 @@ describe('ElectronBleTransport protocol detection', () => {
141
272
  { message: 'ok' },
142
273
  { router: PROTOCOL_V2_CHANNEL_BLE_UART }
143
274
  );
144
-
145
275
  nobleBle.onNotification.mockImplementation(handler => {
146
276
  notificationHandler = handler;
147
277
  return jest.fn();
@@ -164,12 +294,16 @@ describe('ElectronBleTransport protocol detection', () => {
164
294
  })
165
295
  );
166
296
  expect(transport.getProtocolType(device.id)).toBe('V2');
297
+ expect(nobleBle.connect).toHaveBeenCalledTimes(1);
298
+ expect(nobleBle.subscribe).toHaveBeenCalledTimes(1);
299
+ expect(nobleBle.unsubscribe).not.toHaveBeenCalled();
300
+ expect(nobleBle.disconnect).not.toHaveBeenCalled();
167
301
  } finally {
168
302
  await transport.release(device.id);
169
303
  }
170
304
  });
171
305
 
172
- test('detects Protocol V1 when device responds to Initialize', async () => {
306
+ test('reconnects Protocol V1 with a non-destructive GetFeatures probe', async () => {
173
307
  const device = { id: 'classic-id', name: 'OneKey Classic' };
174
308
  const nobleBle = createNobleBle(device);
175
309
  let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
@@ -183,11 +317,12 @@ describe('ElectronBleTransport protocol detection', () => {
183
317
  return jest.fn();
184
318
  });
185
319
  nobleBle.write.mockImplementation(() => {
186
- // Respond to first write (V1 Initialize probe) with V1 Success
320
+ // The first write is the V1 GetFeatures probe; answer with a V1 Success response.
187
321
  setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
188
322
  return Promise.resolve();
189
323
  });
190
324
  const transport = configureTransport(nobleBle);
325
+ const protocolV2Writer = jest.spyOn(transport as any, 'writeProtocolV2Frame');
191
326
 
192
327
  try {
193
328
  await expect(transport.acquire({ uuid: device.id })).resolves.toEqual(
@@ -196,11 +331,100 @@ describe('ElectronBleTransport protocol detection', () => {
196
331
  })
197
332
  );
198
333
  expect(transport.getProtocolType(device.id)).toBe('V1');
334
+ await expect(transport.acquire({ uuid: device.id, expectedProtocol: 'V1' })).resolves.toEqual(
335
+ expect.objectContaining({
336
+ uuid: device.id,
337
+ })
338
+ );
339
+ expect(nobleBle.write).toHaveBeenCalledTimes(2);
340
+ expect(nobleBle.write.mock.calls.every(([, hex]) => /^3f23230037/.test(hex))).toBe(true);
341
+ expect(protocolV2Writer).not.toHaveBeenCalled();
199
342
  } finally {
200
343
  await transport.release(device.id);
201
344
  }
202
345
  });
203
346
 
347
+ test('invalidates and disconnects a Protocol V1 link after a response timeout', async () => {
348
+ const device = { id: 'classic-timeout-id', name: 'OneKey Classic' };
349
+ const nobleBle = createNobleBle(device);
350
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
351
+ const v1ResponseHex = '3f23230002000000040a026f6b';
352
+ let writeCount = 0;
353
+
354
+ nobleBle.onNotification.mockImplementation(handler => {
355
+ notificationHandler = handler;
356
+ return jest.fn();
357
+ });
358
+ nobleBle.write.mockImplementation(() => {
359
+ writeCount += 1;
360
+ if (writeCount === 1) {
361
+ setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
362
+ }
363
+ return Promise.resolve();
364
+ });
365
+ const bleTransport = configureTransport(nobleBle);
366
+
367
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V1' });
368
+ await expect(
369
+ bleTransport.call(device.id, 'Initialize', {}, { timeoutMs: 5 })
370
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.BleTimeoutError });
371
+
372
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
373
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
374
+ expect(bleTransport.getProtocolType(device.id)).toBeUndefined();
375
+ });
376
+
377
+ test('keeps another device V2 reader when force-cleaning a V1 call', async () => {
378
+ const device = { id: 'classic-force-clean-id', name: 'OneKey Classic' };
379
+ const nobleBle = createNobleBle(device);
380
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
381
+ const v1ResponseHex = '3f23230002000000040a026f6b';
382
+ nobleBle.onNotification.mockImplementation(handler => {
383
+ notificationHandler = handler;
384
+ return jest.fn();
385
+ });
386
+ nobleBle.write.mockImplementation(() => {
387
+ setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
388
+ return Promise.resolve();
389
+ });
390
+ const bleTransport = configureTransport(nobleBle) as any;
391
+ const activeV1Call = createDeferred<string>();
392
+ const otherDeviceReader = createDeferred<Uint8Array>();
393
+ activeV1Call.promise.catch(() => undefined);
394
+ otherDeviceReader.promise.catch(() => undefined);
395
+ bleTransport.runPromise = activeV1Call;
396
+ bleTransport.v2FramePromises.set('device-b', otherDeviceReader);
397
+
398
+ await bleTransport.acquire({
399
+ uuid: device.id,
400
+ expectedProtocol: 'V1',
401
+ forceCleanRunPromise: true,
402
+ });
403
+
404
+ expect(bleTransport.v2FramePromises.get('device-b')).toBe(otherDeviceReader);
405
+ await bleTransport.release(device.id);
406
+ });
407
+
408
+ test('rejects a pending V2 reader when its device frame state resets', async () => {
409
+ const nobleBle = createNobleBle();
410
+ const bleTransport = configureTransport(nobleBle) as any;
411
+ const reader = createDeferred<Uint8Array>();
412
+ bleTransport.v2FramePromises.set('device-a', reader);
413
+ const result = Promise.race([
414
+ reader.promise.then(
415
+ () => 'resolved',
416
+ () => 'rejected'
417
+ ),
418
+ new Promise(resolve => {
419
+ setTimeout(() => resolve('pending'), 20);
420
+ }),
421
+ ]);
422
+
423
+ bleTransport.resetProtocolV2Frames('device-a');
424
+
425
+ await expect(result).resolves.toBe('rejected');
426
+ });
427
+
204
428
  test('throws when both protocol probes fail', async () => {
205
429
  const device = { id: 'dead-device-id', name: 'Unknown Device' };
206
430
  const nobleBle = createNobleBle(device);
@@ -216,23 +440,82 @@ describe('ElectronBleTransport protocol detection', () => {
216
440
  expect(transport.getProtocolType(device.id)).toBeUndefined();
217
441
  });
218
442
 
219
- test('probes Protocol V2 instead of trusting the Pro2 name hint', async () => {
443
+ test('keeps a first expected Protocol V2 probe miss retryable', async () => {
444
+ const device = { id: 'first-v2-id', name: 'OneKey Pro 2' };
445
+ const nobleBle = createNobleBle(device);
446
+ const transport = configureTransport(nobleBle);
447
+ jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
448
+
449
+ await expect(
450
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
451
+ ).rejects.toMatchObject({
452
+ errorCode: HardwareErrorCode.RuntimeError,
453
+ });
454
+
455
+ expect(nobleBle.connect).toHaveBeenCalledTimes(1);
456
+ expect(transport.getProtocolType(device.id)).toBeUndefined();
457
+ });
458
+
459
+ test('keeps a second expected Protocol V2 probe miss retryable', async () => {
460
+ const device = { id: 'retry-pro2-id', name: 'OneKey Pro 2' };
461
+ const nobleBle = createNobleBle(device);
462
+ const transport = configureTransport(nobleBle);
463
+ jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
464
+
465
+ await expect(
466
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
467
+ ).rejects.toMatchObject({
468
+ errorCode: HardwareErrorCode.RuntimeError,
469
+ });
470
+ await expect(
471
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
472
+ ).rejects.toMatchObject({
473
+ errorCode: HardwareErrorCode.RuntimeError,
474
+ });
475
+
476
+ expect(transport.getProtocolType(device.id)).toBeUndefined();
477
+ });
478
+
479
+ test('reports a stale bond when a previously confirmed Protocol V2 device stops responding', async () => {
480
+ const device = { id: 'reset-pro2-id', name: 'OneKey Pro 2' };
481
+ const nobleBle = createNobleBle(device);
482
+ const transport = configureTransport(nobleBle);
483
+ const probe = jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(true);
484
+
485
+ await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
486
+ await transport.release(device.id);
487
+ probe.mockResolvedValue(false);
488
+
489
+ await expect(
490
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
491
+ ).rejects.toMatchObject({
492
+ errorCode: HardwareErrorCode.BleDeviceBondError,
493
+ });
494
+
495
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
496
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
497
+ expect(transport.getProtocolType(device.id)).toBeUndefined();
498
+ });
499
+
500
+ test('does not take a Protocol V2 hint from the BLE name', async () => {
220
501
  const device = { id: 'named-pro2-id', name: 'OneKey Pro 2' };
221
502
  const nobleBle = createNobleBle(device);
222
503
  let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
223
- const probeResponse = ProtocolV2.encodeFrame(
224
- schemas,
225
- 'Success',
226
- { message: 'ok' },
227
- { router: PROTOCOL_V2_CHANNEL_BLE_UART }
228
- );
229
504
 
230
505
  nobleBle.onNotification.mockImplementation(handler => {
231
506
  notificationHandler = handler;
232
507
  return jest.fn();
233
508
  });
509
+ let responseSeq = 0;
234
510
  nobleBle.write.mockImplementation(() => {
235
- setTimeout(() => notificationHandler?.(device.id, bytesToHex(probeResponse)), 0);
511
+ responseSeq += 1;
512
+ const response = ProtocolV2.encodeFrame(
513
+ schemas,
514
+ 'Success',
515
+ { message: 'ok' },
516
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
517
+ );
518
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
236
519
  return Promise.resolve();
237
520
  });
238
521
  const transport = configureTransport(nobleBle);
@@ -244,15 +527,15 @@ describe('ElectronBleTransport protocol detection', () => {
244
527
  protocolType: 'V2',
245
528
  })
246
529
  );
247
- expect(nobleBle.write).toHaveBeenCalledTimes(1);
530
+ expect(nobleBle.write.mock.calls.length).toBeGreaterThan(1);
248
531
  expect(transport.getProtocolType(device.id)).toBe('V2');
249
532
  await expect(transport.call(device.id, 'Ping', { message: 'after-probe' })).resolves.toEqual({
250
533
  type: 'Success',
251
534
  message: { message: 'ok' },
252
535
  });
253
- const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
254
- Number.parseInt(hex.slice(12, 14), 16)
255
- );
536
+ const sentSeqs = nobleBle.write.mock.calls
537
+ .map(([, hex]) => Number.parseInt(hex.slice(12, 14), 16))
538
+ .filter(seq => seq > 0);
256
539
  expect(sentSeqs).toEqual([1, 2]);
257
540
  } finally {
258
541
  await transport.release(device.id);
@@ -304,19 +587,20 @@ describe('ElectronBleTransport protocol detection', () => {
304
587
  const device = { id: 'repeated-acquire-pro2-id', name: 'OneKey Pro 2' };
305
588
  const nobleBle = createNobleBle(device);
306
589
  let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
307
- const response = ProtocolV2.encodeFrame(
308
- schemas,
309
- 'Success',
310
- { message: 'ok' },
311
- { router: PROTOCOL_V2_CHANNEL_BLE_UART }
312
- );
313
-
314
590
  nobleBle.onNotification.mockImplementation(handler => {
315
591
  notificationHandler = handler;
316
592
  return jest.fn();
317
593
  });
594
+ let responseSeq = 0;
318
595
  nobleBle.write.mockImplementation(() => {
319
- setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
596
+ responseSeq += 1;
597
+ const sequencedResponse = ProtocolV2.encodeFrame(
598
+ schemas,
599
+ 'Success',
600
+ { message: 'ok' },
601
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
602
+ );
603
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(sequencedResponse)), 0);
320
604
  return Promise.resolve();
321
605
  });
322
606
  const transport = configureTransport(nobleBle);
@@ -331,12 +615,140 @@ describe('ElectronBleTransport protocol detection', () => {
331
615
  message: { message: 'ok' },
332
616
  });
333
617
 
618
+ const sentSeqs = nobleBle.write.mock.calls
619
+ .map(([, hex]) => Number.parseInt(hex.slice(12, 14), 16))
620
+ .filter(seq => seq > 0);
621
+ expect(sentSeqs).toEqual([1, 2, 3]);
622
+ } finally {
623
+ await transport.release(device.id);
624
+ }
625
+ });
626
+
627
+ test('ignores a delayed disconnect event from the previous BLE connection', async () => {
628
+ const device = { id: 'delayed-disconnect-pro2-id', name: 'OneKey Pro 2' };
629
+ const nobleBle = createNobleBle(device);
630
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
631
+ const disconnectHandlers: Array<
632
+ (disconnectedDevice: { id: string; name: string | null }) => void
633
+ > = [];
634
+ nobleBle.onNotification.mockImplementation(handler => {
635
+ notificationHandler = handler;
636
+ return jest.fn();
637
+ });
638
+ nobleBle.onDeviceDisconnected.mockImplementation(handler => {
639
+ disconnectHandlers.push(handler);
640
+ return jest.fn();
641
+ });
642
+ let responseSeq = 0;
643
+ nobleBle.write.mockImplementation(() => {
644
+ responseSeq += 1;
645
+ const response = ProtocolV2.encodeFrame(
646
+ schemas,
647
+ 'Success',
648
+ { message: 'ok' },
649
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
650
+ );
651
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
652
+ return Promise.resolve();
653
+ });
654
+ const bleTransport = configureTransport(nobleBle);
655
+
656
+ try {
657
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
658
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
659
+
660
+ disconnectHandlers[0]?.(device);
661
+
662
+ expect(bleTransport.getProtocolType(device.id)).toBe('V2');
663
+ await expect(
664
+ bleTransport.call(device.id, 'Ping', { message: 'after-stale-disconnect' })
665
+ ).resolves.toEqual({
666
+ type: 'Success',
667
+ message: { message: 'ok' },
668
+ });
669
+ } finally {
670
+ await bleTransport.release(device.id);
671
+ }
672
+ });
673
+
674
+ test('preserves the active Protocol V2 link when the same schema is configured again', async () => {
675
+ const device = { id: 'stable-schema-pro2-id', name: 'OneKey Pro 2' };
676
+ const nobleBle = createNobleBle(device);
677
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
678
+ nobleBle.onNotification.mockImplementation(handler => {
679
+ notificationHandler = handler;
680
+ return jest.fn();
681
+ });
682
+ let responseSeq = 0;
683
+ nobleBle.write.mockImplementation(() => {
684
+ responseSeq += 1;
685
+ const response = ProtocolV2.encodeFrame(
686
+ schemas,
687
+ 'Success',
688
+ { message: 'ok' },
689
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
690
+ );
691
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
692
+ return Promise.resolve();
693
+ });
694
+ const bleTransport = configureTransport(nobleBle);
695
+
696
+ try {
697
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
698
+ const invalidateAllLinks = jest.spyOn(
699
+ (bleTransport as any).protocolV2Links,
700
+ 'invalidateAllLinks'
701
+ );
702
+ bleTransport.configureProtocolV2(protocolV2Schema);
703
+ await new Promise<void>(resolve => {
704
+ setTimeout(resolve, 0);
705
+ });
706
+ expect(invalidateAllLinks).not.toHaveBeenCalled();
707
+ await expect(
708
+ bleTransport.call(device.id, 'Ping', { message: 'same-schema' })
709
+ ).resolves.toEqual({
710
+ type: 'Success',
711
+ message: { message: 'ok' },
712
+ });
334
713
  const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
335
714
  Number.parseInt(hex.slice(12, 14), 16)
336
715
  );
337
716
  expect(sentSeqs).toEqual([1, 2]);
338
717
  } finally {
339
- await transport.release(device.id);
718
+ await bleTransport.release(device.id);
719
+ }
720
+ });
721
+
722
+ test('rejects oversized Protocol V2 requests before writing to Electron BLE', async () => {
723
+ const device = { id: 'oversized-frame-pro2-id', name: 'OneKey Pro 2' };
724
+ const nobleBle = createNobleBle(device);
725
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
726
+ const probeResponse = ProtocolV2.encodeFrame(
727
+ schemas,
728
+ 'Success',
729
+ { message: 'ok' },
730
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
731
+ );
732
+ nobleBle.onNotification.mockImplementation(handler => {
733
+ notificationHandler = handler;
734
+ return jest.fn();
735
+ });
736
+ nobleBle.write.mockImplementation(() => {
737
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(probeResponse)), 0);
738
+ return Promise.resolve();
739
+ });
740
+ const bleTransport = configureTransport(nobleBle);
741
+
742
+ try {
743
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
744
+ expect(nobleBle.write).toHaveBeenCalledTimes(1);
745
+
746
+ await expect(
747
+ bleTransport.call(device.id, 'Ping', { message: 'x'.repeat(2048) })
748
+ ).rejects.toThrow(/Protocol V2 frame too large for transport/);
749
+ expect(nobleBle.write).toHaveBeenCalledTimes(1);
750
+ } finally {
751
+ await bleTransport.release(device.id);
340
752
  }
341
753
  });
342
754
  });