@onekeyfe/hd-transport-web-device 1.2.2-alpha.8 → 1.2.2

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.
package/README.md CHANGED
@@ -26,4 +26,4 @@ yar update:protobuf to generate new ./messages.json and ./src/types/messages.ts
26
26
 
27
27
  ## Docs
28
28
 
29
- Documentation is available [hardware-js-sdk](https://developer.onekey.so/connect-to-hardware/hardware-sdk/start)
29
+ Documentation is available [Hardware SDK Getting Started](https://developer.onekey.so/en/hardware-sdk/getting-started)
@@ -182,6 +182,80 @@ describe('ElectronBleTransport protocol detection', () => {
182
182
  jest.clearAllMocks();
183
183
  });
184
184
 
185
+ test('does not treat an error envelope from a legacy preload as a successful connect', async () => {
186
+ const device = { id: 'stale-bond-id', name: 'OneKey Pro 2' };
187
+ const nobleBle = createNobleBle(device);
188
+ nobleBle.connect.mockResolvedValue({
189
+ type: 'NobleBleIpcError',
190
+ success: false,
191
+ error: {
192
+ name: 'HardwareError',
193
+ message: 'Bluetooth pairing information is no longer valid',
194
+ errorCode: HardwareErrorCode.BleBondInvalid,
195
+ },
196
+ } as never);
197
+ const bleTransport = configureTransport(nobleBle);
198
+
199
+ await expect(
200
+ bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
201
+ ).rejects.toMatchObject({
202
+ errorCode: HardwareErrorCode.BleBondInvalid,
203
+ });
204
+ expect(nobleBle.subscribe).not.toHaveBeenCalled();
205
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
206
+ });
207
+
208
+ test('cancels a pending native connect acquire has not registered yet', async () => {
209
+ const device = { id: 'pending-connect-id', name: 'OneKey Pro 2' };
210
+ const nobleBle = createNobleBle(device);
211
+ // Never calls back: the field case the Core acquire deadline has to break.
212
+ nobleBle.connect.mockImplementation(
213
+ () =>
214
+ new Promise<void>(() => {
215
+ // intentionally never settles
216
+ })
217
+ );
218
+ const bleTransport = configureTransport(nobleBle) as any;
219
+
220
+ const pendingAcquire = bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
221
+ pendingAcquire.catch(() => undefined);
222
+ await new Promise(resolve => {
223
+ setTimeout(resolve, 10);
224
+ });
225
+
226
+ // What the deadline / user abort calls. connectedDevices is still empty
227
+ // here, so this only works because the acquire is tracked as in flight.
228
+ await bleTransport.disconnect(device.id);
229
+
230
+ expect(nobleBle.unsubscribe).not.toHaveBeenCalled();
231
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
232
+ });
233
+
234
+ test('leaves a kept-alive link up when a released device is disconnected', async () => {
235
+ const device = { id: 'kept-alive-id', name: 'OneKey Pro 2' };
236
+ const nobleBle = createNobleBle(device);
237
+ const bleTransport = configureTransport(nobleBle) as any;
238
+
239
+ // No acquire in flight and nothing in connectedDevices: the link was
240
+ // released logically and belongs to the main-process keep-alive timer.
241
+ // The routine post-call cancel must not cost the next call a cold connect.
242
+ await bleTransport.disconnect(device.id);
243
+
244
+ expect(nobleBle.disconnect).not.toHaveBeenCalled();
245
+ expect(nobleBle.unsubscribe).not.toHaveBeenCalled();
246
+ });
247
+
248
+ test('forceNative disconnects a device the renderer no longer tracks', async () => {
249
+ const device = { id: 'presumed-dead-id', name: 'OneKey Pro 2' };
250
+ const nobleBle = createNobleBle(device);
251
+ const bleTransport = configureTransport(nobleBle) as any;
252
+
253
+ await bleTransport.releaseNative(device.id, { forceNative: true });
254
+
255
+ expect(nobleBle.unsubscribe).not.toHaveBeenCalled();
256
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
257
+ });
258
+
185
259
  test('keeps raw BLE lifecycle payloads off the public device event channel', async () => {
186
260
  const device = { id: 'lifecycle-pro2-id', name: 'OneKey Pro 2' };
187
261
  const nobleBle = createNobleBle(device);
@@ -464,7 +538,7 @@ describe('ElectronBleTransport protocol detection', () => {
464
538
  await expect(result).resolves.toBe('rejected');
465
539
  });
466
540
 
467
- test('throws when both protocol probes fail', async () => {
541
+ test('disconnects when both protocol probes fail and reconnects for the next acquire', async () => {
468
542
  const device = { id: 'dead-device-id', name: 'Unknown Device' };
469
543
  const nobleBle = createNobleBle(device);
470
544
 
@@ -476,7 +550,25 @@ describe('ElectronBleTransport protocol detection', () => {
476
550
  await expect(transport.acquire({ uuid: device.id })).rejects.toThrow(
477
551
  /Unable to detect BLE protocol/
478
552
  );
553
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
554
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
479
555
  expect(transport.getProtocolType(device.id)).toBeUndefined();
556
+
557
+ echoProtocolV2(nobleBle, device.id);
558
+ await expect(
559
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
560
+ ).resolves.toMatchObject({ uuid: device.id });
561
+ expect(nobleBle.connect).toHaveBeenCalledTimes(2);
562
+ expect(nobleBle.disconnect.mock.invocationCallOrder.at(-1)).toBeLessThan(
563
+ nobleBle.connect.mock.invocationCallOrder[1]
564
+ );
565
+ await expect(
566
+ transport.call(device.id, 'Ping', { message: 'after-reconnect' })
567
+ ).resolves.toMatchObject({
568
+ type: 'Success',
569
+ message: { message: 'ok' },
570
+ });
571
+ await transport.release(device.id);
480
572
  });
481
573
 
482
574
  test('surfaces Protocol V2 link disabled while the initial V1 probe is active', async () => {
@@ -553,14 +645,19 @@ describe('ElectronBleTransport protocol detection', () => {
553
645
  test('fails Protocol V2 acquire immediately when subscribe reports insufficient encryption', async () => {
554
646
  const device = { id: 'stale-bond-pro2-id', name: 'OneKey Pro 2' };
555
647
  const nobleBle = createNobleBle(device);
556
- nobleBle.subscribe.mockRejectedValue(new Error('Encryption is insufficient'));
648
+ nobleBle.subscribe.mockRejectedValue({
649
+ name: 'HardwareError',
650
+ message: 'Bluetooth pairing information is no longer valid',
651
+ errorCode: HardwareErrorCode.BleBondInvalid,
652
+ params: { nativeErrorMessage: 'Encryption is insufficient' },
653
+ });
557
654
  const transport = configureTransport(nobleBle);
558
655
  const probe = jest.spyOn(transport as any, 'probeProtocolV2');
559
656
 
560
657
  await expect(
561
658
  transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
562
659
  ).rejects.toMatchObject({
563
- errorCode: HardwareErrorCode.BleDeviceBondError,
660
+ errorCode: HardwareErrorCode.BleBondInvalid,
564
661
  });
565
662
 
566
663
  expect(probe).not.toHaveBeenCalled();
@@ -568,6 +665,56 @@ describe('ElectronBleTransport protocol detection', () => {
568
665
  expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
569
666
  });
570
667
 
668
+ test('rehydrates a structured stale-bond error before the protocol is known', async () => {
669
+ const device = { id: 'reset-unknown-protocol-id', name: 'OneKey Pro 2' };
670
+ const nobleBle = createNobleBle(device);
671
+ nobleBle.connect.mockRejectedValue({
672
+ name: 'HardwareError',
673
+ message: 'Bluetooth pairing information is no longer valid',
674
+ errorCode: HardwareErrorCode.BleBondInvalid,
675
+ params: { nativeErrorMessage: 'CBErrorDomain:14 native message' },
676
+ });
677
+ const transport = configureTransport(nobleBle);
678
+
679
+ await expect(transport.acquire({ uuid: device.id })).rejects.toMatchObject({
680
+ name: 'HardwareError',
681
+ errorCode: HardwareErrorCode.BleBondInvalid,
682
+ params: { nativeErrorMessage: 'CBErrorDomain:14 native message' },
683
+ });
684
+ });
685
+
686
+ test('does not infer hardware errors from native error text in the renderer', async () => {
687
+ const device = { id: 'reset-pro2-localized-id', name: 'OneKey Pro 2' };
688
+ const nobleBle = createNobleBle(device);
689
+ nobleBle.connect.mockRejectedValue(new Error('CBATTErrorDomain:14 localized native message'));
690
+ const transport = configureTransport(nobleBle);
691
+
692
+ try {
693
+ await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
694
+ throw new Error('Expected acquire to fail');
695
+ } catch (error) {
696
+ expect(error).toBeInstanceOf(Error);
697
+ expect((error as Error).message).toBe('CBATTErrorDomain:14 localized native message');
698
+ expect((error as { errorCode?: unknown }).errorCode).toBeUndefined();
699
+ }
700
+ });
701
+
702
+ test('does not classify a generic macOS connection failure as a stale bond', async () => {
703
+ const device = { id: 'offline-pro2-macos-id', name: 'OneKey Pro 2' };
704
+ const nobleBle = createNobleBle(device);
705
+ nobleBle.connect.mockRejectedValue(new Error('connection failed'));
706
+ const transport = configureTransport(nobleBle);
707
+
708
+ try {
709
+ await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
710
+ throw new Error('Expected acquire to fail');
711
+ } catch (error) {
712
+ expect(error).toBeInstanceOf(Error);
713
+ expect((error as Error).message).toBe('connection failed');
714
+ expect((error as { errorCode?: unknown }).errorCode).toBeUndefined();
715
+ }
716
+ });
717
+
571
718
  test('keeps stale-bond subscribe mapping out of Protocol V1 acquire', async () => {
572
719
  const device = { id: 'classic-v1-id', name: 'OneKey Classic' };
573
720
  const nobleBle = createNobleBle(device);
@@ -584,7 +731,7 @@ describe('ElectronBleTransport protocol detection', () => {
584
731
  }
585
732
  });
586
733
 
587
- test('reports a stale bond when a previously confirmed Protocol V2 device stops responding', async () => {
734
+ test('keeps a probe miss retryable after a previously confirmed Protocol V2 connection', async () => {
588
735
  const device = { id: 'reset-pro2-id', name: 'OneKey Pro 2' };
589
736
  const nobleBle = createNobleBle(device);
590
737
  const transport = configureTransport(nobleBle);
@@ -597,7 +744,7 @@ describe('ElectronBleTransport protocol detection', () => {
597
744
  await expect(
598
745
  transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
599
746
  ).rejects.toMatchObject({
600
- errorCode: HardwareErrorCode.BleDeviceBondError,
747
+ errorCode: HardwareErrorCode.RuntimeError,
601
748
  });
602
749
 
603
750
  expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
@@ -605,6 +752,52 @@ describe('ElectronBleTransport protocol detection', () => {
605
752
  expect(transport.getProtocolType(device.id)).toBeUndefined();
606
753
  });
607
754
 
755
+ test.each([false, true])(
756
+ 'preserves an expected Protocol V2 Ping timeout with a prior successful connection: %s',
757
+ async previouslyConnected => {
758
+ const device = { id: 'timeout-pro2-id', name: 'OneKey Pro 2' };
759
+ const nobleBle = createNobleBle(device);
760
+ const transport = configureTransport(nobleBle);
761
+
762
+ if (previouslyConnected) {
763
+ echoProtocolV2(nobleBle, device.id);
764
+ await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
765
+ await transport.release(device.id);
766
+ }
767
+ // Keep the real probe and response timer, but drop its notification.
768
+ nobleBle.write.mockImplementation(() => Promise.resolve());
769
+
770
+ await expect(
771
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
772
+ ).rejects.toMatchObject({
773
+ errorCode: HardwareErrorCode.BleTimeoutError,
774
+ message: 'BLE response timeout after 5000ms for Ping',
775
+ });
776
+
777
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
778
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
779
+ expect(transport.getProtocolType(device.id)).toBeUndefined();
780
+ }
781
+ );
782
+
783
+ test('preserves a native stale-bond error during an expected Protocol V2 probe', async () => {
784
+ const device = { id: 'stale-bond-probe-id', name: 'OneKey Pro 2' };
785
+ const nobleBle = createNobleBle(device);
786
+ nobleBle.write.mockRejectedValue({
787
+ name: 'HardwareError',
788
+ message: 'Bluetooth pairing information is no longer valid',
789
+ errorCode: HardwareErrorCode.BleBondInvalid,
790
+ });
791
+ const transport = configureTransport(nobleBle);
792
+
793
+ await expect(
794
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
795
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.BleBondInvalid });
796
+
797
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
798
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
799
+ });
800
+
608
801
  test('does not take a Protocol V2 hint from the BLE name', async () => {
609
802
  const device = { id: 'named-pro2-id', name: 'OneKey Pro 2' };
610
803
  const nobleBle = createNobleBle(device);
@@ -31,6 +31,39 @@ function buildAcquirableTransport(path = 'pro-webusb') {
31
31
  }
32
32
 
33
33
  describe('WebUsbTransport protocol probe cache', () => {
34
+ test.each([
35
+ ['V1', false, 0],
36
+ ['V1', true, 1],
37
+ ['V2', false, 1],
38
+ [undefined, false, 1],
39
+ ] as const)(
40
+ 'reconnect protocol=%s first=%s resets USB %s times',
41
+ async (protocol, first, resets) => {
42
+ const webusb = new WebUsbTransport();
43
+ const path = 'connected-usb-device';
44
+ const device = {
45
+ opened: true,
46
+ configuration: { configurationValue: 1 },
47
+ configurations: [],
48
+ reset: jest.fn().mockResolvedValue(undefined),
49
+ selectConfiguration: jest.fn().mockResolvedValue(undefined),
50
+ claimInterface: jest.fn().mockResolvedValue(undefined),
51
+ clearHalt: jest.fn().mockResolvedValue(undefined),
52
+ };
53
+ jest.spyOn(webusb, 'findDevice').mockResolvedValue(device as unknown as USBDevice);
54
+ jest.spyOn(webusb, 'getConnectedDevices').mockResolvedValue([]);
55
+ if (protocol) {
56
+ const state = webusb as unknown as { deviceProtocol: Map<string, 'V1' | 'V2'> };
57
+ state.deviceProtocol.set(path, protocol);
58
+ }
59
+
60
+ await webusb.connectToDevice(path, first);
61
+
62
+ expect(device.reset).toHaveBeenCalledTimes(resets);
63
+ expect(device.claimInterface).toHaveBeenCalledWith(0);
64
+ }
65
+ );
66
+
34
67
  test('acquire skips the wire probe when the protocol is already cached', async () => {
35
68
  const webusb = buildAcquirableTransport();
36
69
  const path = 'pro-webusb';
@@ -25,9 +25,9 @@ export default class ElectronBleTransport {
25
25
  Log?: any;
26
26
  emitter?: EventEmitter;
27
27
  private connectedDevices;
28
+ private acquiringDevices;
28
29
  private deviceProtocol;
29
30
  private deviceProtocolHints;
30
- private confirmedProtocolV2;
31
31
  private deviceMtus;
32
32
  private devicePacketCapacities;
33
33
  private v1Buffers;
@@ -41,7 +41,7 @@ export default class ElectronBleTransport {
41
41
  private hostDisconnectCleanup?;
42
42
  private notificationTokens;
43
43
  private nextNotificationToken;
44
- private toStaleBondError;
44
+ private normalizeBluetoothError;
45
45
  private handleBluetoothError;
46
46
  private cleanupDeviceState;
47
47
  init(logger: any, emitter?: EventEmitter): void;
@@ -1 +1 @@
1
- {"version":3,"file":"electron-ble-transport.d.ts","sourceRoot":"","sources":["../src/electron-ble-transport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"electron-ble-transport.d.ts","sourceRoot":"","sources":["../src/electron-ble-transport.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -216,10 +216,16 @@ declare class ElectronBleTransport {
216
216
  Log?: any;
217
217
  emitter?: EventEmitter;
218
218
  private connectedDevices;
219
+ /**
220
+ * Devices whose acquire() is still in flight.
221
+ *
222
+ * A native connect that never calls back has not reached `connectedDevices`
223
+ * yet, so this is the only record that the renderer is still trying to own
224
+ * the link; `releaseNative()` needs it to cancel that pending connect.
225
+ */
226
+ private acquiringDevices;
219
227
  private deviceProtocol;
220
228
  private deviceProtocolHints;
221
- /** Endpoints that answered a V2 probe in this transport lifetime. Survives disconnect. */
222
- private confirmedProtocolV2;
223
229
  private deviceMtus;
224
230
  private devicePacketCapacities;
225
231
  private v1Buffers;
@@ -242,7 +248,7 @@ declare class ElectronBleTransport {
242
248
  private hostDisconnectCleanup?;
243
249
  private notificationTokens;
244
250
  private nextNotificationToken;
245
- private toStaleBondError;
251
+ private normalizeBluetoothError;
246
252
  private handleBluetoothError;
247
253
  private cleanupDeviceState;
248
254
  init(logger: any, emitter?: EventEmitter): void;
package/dist/index.js CHANGED
@@ -375,11 +375,13 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
375
375
  if (!device.opened) {
376
376
  yield device.open();
377
377
  }
378
- try {
379
- yield device.reset();
380
- }
381
- catch (error) {
382
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
378
+ if (first || this.deviceProtocol.get(path) !== 'V1') {
379
+ try {
380
+ yield device.reset();
381
+ }
382
+ catch (error) {
383
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
384
+ }
383
385
  }
384
386
  yield this.getConnectedDevices();
385
387
  device = yield this.findDevice(path);
@@ -823,6 +825,19 @@ function resolveBlePacketCapacity(mtu, maximumPacketCapacity, fallbackPacketCapa
823
825
 
824
826
  const { parseConfigure, ProtocolV1, check } = transport__default["default"];
825
827
  const toBleDescriptor = (device, protocolType) => (Object.assign({ id: device.id, name: device.name, path: device.id, debug: false, commType: 'electron-ble' }, (protocolType ? { protocolType } : {})));
828
+ const invokeNobleBle = (request) => __awaiter(void 0, void 0, void 0, function* () {
829
+ const response = yield request;
830
+ if (response &&
831
+ typeof response === 'object' &&
832
+ 'type' in response &&
833
+ response.type === 'NobleBleIpcError' &&
834
+ 'success' in response &&
835
+ response.success === false &&
836
+ 'error' in response) {
837
+ return Promise.reject(response.error);
838
+ }
839
+ return response;
840
+ });
826
841
  const BLE_PACKET_SIZE_FALLBACK = 192;
827
842
  const BLE_PACKET_SIZE_MAXIMUM = 244;
828
843
  const BLE_WRITE_DELAY_MS = 5;
@@ -835,9 +850,9 @@ class ElectronBleTransport {
835
850
  this.runPromise = null;
836
851
  this.runPromiseDeviceId = null;
837
852
  this.connectedDevices = new Set();
853
+ this.acquiringDevices = new Set();
838
854
  this.deviceProtocol = new Map();
839
855
  this.deviceProtocolHints = new Map();
840
- this.confirmedProtocolV2 = new Set();
841
856
  this.deviceMtus = new Map();
842
857
  this.devicePacketCapacities = new Map();
843
858
  this.v1Buffers = new Map();
@@ -861,7 +876,7 @@ class ElectronBleTransport {
861
876
  this.rejectProtocolV2Frames(uuid, new Error(reason));
862
877
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
863
878
  if (reason.startsWith('Protocol V2 link-fatal error:')) {
864
- yield this.releaseNative(uuid);
879
+ yield this.releaseNative(uuid, { forceNative: true });
865
880
  }
866
881
  }),
867
882
  });
@@ -871,39 +886,20 @@ class ElectronBleTransport {
871
886
  this.notificationTokens = new Map();
872
887
  this.nextNotificationToken = 1;
873
888
  }
874
- toStaleBondError(error) {
875
- var _a;
876
- if (hdShared.isBleStaleBondHardwareError(error)) {
877
- return error;
878
- }
879
- const errorMessage = error && typeof error === 'object' && 'message' in error
880
- ? String((_a = error.message) !== null && _a !== void 0 ? _a : '')
881
- : String(error !== null && error !== void 0 ? error : '');
882
- if (!hdShared.isBleStaleBondErrorText(errorMessage)) {
883
- return null;
884
- }
885
- const normalizedErrorMessage = errorMessage.toLowerCase();
886
- return hdShared.ERRORS.TypedError(normalizedErrorMessage.includes('peer removed pairing information')
887
- ? hdShared.HardwareErrorCode.BlePeerRemovedPairingInformation
888
- : hdShared.HardwareErrorCode.BleDeviceBondError, errorMessage);
889
- }
890
- handleBluetoothError(error, mapProtocolV2StaleBond = false) {
891
- if (mapProtocolV2StaleBond) {
892
- const staleBondError = this.toStaleBondError(error);
893
- if (staleBondError) {
894
- throw staleBondError;
895
- }
896
- }
889
+ normalizeBluetoothError(error) {
897
890
  if (error && typeof error === 'object') {
891
+ if (typeof error.errorCode === 'number') {
892
+ return hdShared.ERRORS.TypedError(error.errorCode, typeof error.message === 'string' ? error.message : undefined, error.params);
893
+ }
898
894
  if ('code' in error) {
899
895
  if (error.code === hdShared.HardwareErrorCode.BlePoweredOff) {
900
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
896
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
901
897
  }
902
898
  if (error.code === hdShared.HardwareErrorCode.BleUnsupported) {
903
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
899
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
904
900
  }
905
901
  if (error.code === hdShared.HardwareErrorCode.BlePermissionError) {
906
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
902
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
907
903
  }
908
904
  }
909
905
  const errorMessage = error.message || String(error);
@@ -911,16 +907,19 @@ class ElectronBleTransport {
911
907
  const unsupportedMessage = hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BleUnsupported];
912
908
  const permissionMessage = hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BlePermissionError];
913
909
  if (errorMessage.includes(poweredOffMessage) || errorMessage.includes('poweredOff')) {
914
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
910
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
915
911
  }
916
912
  if (errorMessage.includes(unsupportedMessage) || errorMessage.includes('unsupported')) {
917
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
913
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
918
914
  }
919
915
  if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
920
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
916
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
921
917
  }
922
918
  }
923
- throw error;
919
+ return error;
920
+ }
921
+ handleBluetoothError(error) {
922
+ throw this.normalizeBluetoothError(error);
924
923
  }
925
924
  cleanupDeviceState(deviceId) {
926
925
  this.protocolV2Links
@@ -1007,7 +1006,7 @@ class ElectronBleTransport {
1007
1006
  if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
1008
1007
  throw new Error('Noble BLE API not available');
1009
1008
  }
1010
- const devices = yield window.desktopApi.nobleBle.enumerate();
1009
+ const devices = yield invokeNobleBle(window.desktopApi.nobleBle.enumerate());
1011
1010
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
1012
1011
  for (const dev of devices) {
1013
1012
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`);
@@ -1024,9 +1023,6 @@ class ElectronBleTransport {
1024
1023
  var _a, _b, _c, _d, _e, _f, _g;
1025
1024
  return __awaiter(this, void 0, void 0, function* () {
1026
1025
  const { uuid, forceCleanRunPromise, expectedProtocol } = input;
1027
- const shouldMapProtocolV2StaleBond = expectedProtocol
1028
- ? expectedProtocol === 'V2'
1029
- : this.confirmedProtocolV2.has(uuid);
1030
1026
  if (!uuid) {
1031
1027
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleRequiredUUID);
1032
1028
  }
@@ -1039,11 +1035,12 @@ class ElectronBleTransport {
1039
1035
  this.runPromise = null;
1040
1036
  this.runPromiseDeviceId = null;
1041
1037
  }
1038
+ this.acquiringDevices.add(uuid);
1042
1039
  try {
1043
1040
  if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
1044
1041
  throw new Error('Noble BLE API not available');
1045
1042
  }
1046
- const device = yield window.desktopApi.nobleBle.getDevice(uuid);
1043
+ const device = yield invokeNobleBle(window.desktopApi.nobleBle.getDevice(uuid));
1047
1044
  if (!device) {
1048
1045
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${uuid} not found`);
1049
1046
  }
@@ -1054,11 +1051,11 @@ class ElectronBleTransport {
1054
1051
  this.deviceProtocolHints.set(uuid, protocolHint);
1055
1052
  }
1056
1053
  try {
1057
- yield window.desktopApi.nobleBle.connect(uuid);
1054
+ yield invokeNobleBle(window.desktopApi.nobleBle.connect(uuid));
1058
1055
  this.connectedDevices.add(uuid);
1059
1056
  }
1060
1057
  catch (error) {
1061
- this.handleBluetoothError(error, shouldMapProtocolV2StaleBond);
1058
+ this.handleBluetoothError(error);
1062
1059
  }
1063
1060
  const mtuCleanup = this.createMtuSubscription(uuid);
1064
1061
  if (mtuCleanup) {
@@ -1067,10 +1064,10 @@ class ElectronBleTransport {
1067
1064
  this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
1068
1065
  this.v2Assemblers.set(uuid, new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
1069
1066
  try {
1070
- yield window.desktopApi.nobleBle.subscribe(uuid);
1067
+ yield invokeNobleBle(window.desktopApi.nobleBle.subscribe(uuid));
1071
1068
  }
1072
1069
  catch (error) {
1073
- this.handleBluetoothError(error, shouldMapProtocolV2StaleBond);
1070
+ this.handleBluetoothError(error);
1074
1071
  }
1075
1072
  yield this.refreshBlePacketCapacity(uuid);
1076
1073
  const cleanup = this.createNotificationSubscription(uuid);
@@ -1088,16 +1085,25 @@ class ElectronBleTransport {
1088
1085
  catch (error) {
1089
1086
  (_e = this.Log) === null || _e === void 0 ? void 0 : _e.error('[Electron BLE] acquire failed:', error);
1090
1087
  try {
1091
- if (((_f = window.desktopApi) === null || _f === void 0 ? void 0 : _f.nobleBle) && this.connectedDevices.has(uuid)) {
1092
- yield window.desktopApi.nobleBle.unsubscribe(uuid);
1093
- yield window.desktopApi.nobleBle.disconnect(uuid);
1088
+ const nobleBle = (_f = window.desktopApi) === null || _f === void 0 ? void 0 : _f.nobleBle;
1089
+ if (nobleBle) {
1090
+ if (this.connectedDevices.has(uuid)) {
1091
+ yield invokeNobleBle(nobleBle.unsubscribe(uuid)).catch(cleanupError => {
1092
+ var _a;
1093
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] acquire unsubscribe failed:', cleanupError);
1094
+ });
1095
+ }
1096
+ yield invokeNobleBle(nobleBle.disconnect(uuid));
1094
1097
  }
1095
1098
  }
1096
1099
  catch (cleanupError) {
1097
1100
  (_g = this.Log) === null || _g === void 0 ? void 0 : _g.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
1098
1101
  }
1099
1102
  this.cleanupDeviceState(uuid);
1100
- throw error;
1103
+ this.handleBluetoothError(error);
1104
+ }
1105
+ finally {
1106
+ this.acquiringDevices.delete(uuid);
1101
1107
  }
1102
1108
  });
1103
1109
  }
@@ -1119,17 +1125,23 @@ class ElectronBleTransport {
1119
1125
  return this.releaseNative(id);
1120
1126
  });
1121
1127
  }
1122
- releaseNative(id) {
1128
+ releaseNative(id, options) {
1123
1129
  var _a, _b;
1124
1130
  return __awaiter(this, void 0, void 0, function* () {
1131
+ const rendererOwnsLink = this.connectedDevices.has(id) || this.acquiringDevices.has(id);
1132
+ const shouldDisconnect = (options === null || options === void 0 ? void 0 : options.forceNative) === true || rendererOwnsLink;
1125
1133
  try {
1126
- if (this.connectedDevices.has(id)) {
1127
- if ((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) {
1128
- yield window.desktopApi.nobleBle.unsubscribe(id);
1129
- yield window.desktopApi.nobleBle.disconnect(id);
1134
+ const nobleBle = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle;
1135
+ if (nobleBle && shouldDisconnect) {
1136
+ if (this.connectedDevices.has(id)) {
1137
+ yield invokeNobleBle(nobleBle.unsubscribe(id)).catch(error => {
1138
+ var _a;
1139
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] release unsubscribe failed:', error);
1140
+ });
1130
1141
  }
1131
- this.cleanupDeviceState(id);
1142
+ yield invokeNobleBle(nobleBle.disconnect(id));
1132
1143
  }
1144
+ this.cleanupDeviceState(id);
1133
1145
  }
1134
1146
  catch (error) {
1135
1147
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.error('[Electron BLE] release failed:', error);
@@ -1153,7 +1165,7 @@ class ElectronBleTransport {
1153
1165
  }
1154
1166
  return;
1155
1167
  }
1156
- yield release(id, keepSession);
1168
+ yield invokeNobleBle(release(id, keepSession));
1157
1169
  }
1158
1170
  catch (error) {
1159
1171
  (_d = this.Log) === null || _d === void 0 ? void 0 : _d.error('[Electron BLE] logical release failed:', error);
@@ -1161,9 +1173,8 @@ class ElectronBleTransport {
1161
1173
  }
1162
1174
  });
1163
1175
  }
1164
- createProtocolMismatchError(expected, uuid) {
1165
- const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
1166
- return hdShared.ERRORS.TypedError(isStaleV2Bond ? hdShared.HardwareErrorCode.BleDeviceBondError : hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
1176
+ createProtocolMismatchError(expected) {
1177
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
1167
1178
  }
1168
1179
  createProtocolDetectionError() {
1169
1180
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleTimeoutError, 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping');
@@ -1182,13 +1193,12 @@ class ElectronBleTransport {
1182
1193
  return 'V1';
1183
1194
  }
1184
1195
  if (expectedProtocol === 'V2') {
1185
- if (yield this.probeProtocolV2(uuid)) {
1196
+ if (yield this.probeProtocolV2(uuid, expectedProtocol)) {
1186
1197
  this.deviceProtocol.set(uuid, 'V2');
1187
- this.confirmedProtocolV2.add(uuid);
1188
1198
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1189
1199
  return 'V2';
1190
1200
  }
1191
- throw this.createProtocolMismatchError(expectedProtocol, uuid);
1201
+ throw this.createProtocolMismatchError(expectedProtocol);
1192
1202
  }
1193
1203
  const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1194
1204
  for (let i = 0; i < probeOrder.length; i += 1) {
@@ -1199,9 +1209,6 @@ class ElectronBleTransport {
1199
1209
  const detected = protocol === 'V1' ? yield this.probeProtocolV1(uuid) : yield this.probeProtocolV2(uuid);
1200
1210
  if (detected) {
1201
1211
  this.deviceProtocol.set(uuid, protocol);
1202
- if (protocol === 'V2') {
1203
- this.confirmedProtocolV2.add(uuid);
1204
- }
1205
1212
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
1206
1213
  return protocol;
1207
1214
  }
@@ -1264,7 +1271,7 @@ class ElectronBleTransport {
1264
1271
  }
1265
1272
  });
1266
1273
  }
1267
- probeProtocolV2(uuid) {
1274
+ probeProtocolV2(uuid, expectedProtocol) {
1268
1275
  var _a;
1269
1276
  return __awaiter(this, void 0, void 0, function* () {
1270
1277
  if (!this._messages || !this._messagesV2) {
@@ -1282,7 +1289,9 @@ class ElectronBleTransport {
1282
1289
  (_a = this.v2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
1283
1290
  this.resetProtocolV2Frames(uuid);
1284
1291
  },
1285
- shouldRethrow: error => hdShared.isBleStaleBondHardwareError(error) || transport.isProtocolV2LinkDisabledError(error),
1292
+ shouldRethrow: error => expectedProtocol === 'V2' ||
1293
+ hdShared.isBleStaleBondHardwareError(error) ||
1294
+ transport.isProtocolV2LinkDisabledError(error),
1286
1295
  });
1287
1296
  if (!detected) {
1288
1297
  this.clearProbeProtocol(uuid, 'V2');
@@ -1298,21 +1307,18 @@ class ElectronBleTransport {
1298
1307
  throw new Error('Noble BLE API not available');
1299
1308
  }
1300
1309
  try {
1301
- yield nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
1310
+ yield invokeNobleBle(nobleBle.write(uuid, hexData, { pacingDelayMs: 0 }));
1302
1311
  }
1303
1312
  catch (error) {
1304
- const staleBondError = this.toStaleBondError(error);
1305
- if (staleBondError) {
1306
- throw staleBondError;
1307
- }
1308
- throw error;
1313
+ this.handleBluetoothError(error);
1309
1314
  }
1310
1315
  });
1311
1316
  }
1312
1317
  refreshBlePacketCapacity(uuid) {
1313
- var _a, _b;
1318
+ var _a;
1314
1319
  return __awaiter(this, void 0, void 0, function* () {
1315
- const device = yield ((_b = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) === null || _b === void 0 ? void 0 : _b.getDevice(uuid));
1320
+ const nobleBle = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle;
1321
+ const device = nobleBle ? yield invokeNobleBle(nobleBle.getDevice(uuid)) : undefined;
1316
1322
  this.updateBlePacketCapacity(uuid, device === null || device === void 0 ? void 0 : device.mtu);
1317
1323
  });
1318
1324
  }
@@ -1535,7 +1541,7 @@ class ElectronBleTransport {
1535
1541
  if (hexString.length === 0) {
1536
1542
  throw new Error(`Buffer ${i + 1} is empty`);
1537
1543
  }
1538
- yield window.desktopApi.nobleBle.write(uuid, hexString);
1544
+ yield invokeNobleBle(window.desktopApi.nobleBle.write(uuid, hexString));
1539
1545
  }
1540
1546
  const response = yield Promise.race([
1541
1547
  runPromise.promise,
@@ -1565,10 +1571,10 @@ class ElectronBleTransport {
1565
1571
  this.notificationCleanups.delete(uuid);
1566
1572
  this.notificationTokens.delete(uuid);
1567
1573
  if (!isProbeTimeout) {
1568
- yield this.releaseNative(uuid);
1574
+ yield this.releaseNative(uuid, { forceNative: true });
1569
1575
  }
1570
1576
  }
1571
- throw e;
1577
+ throw this.normalizeBluetoothError(e);
1572
1578
  }
1573
1579
  finally {
1574
1580
  if (timeout)
@@ -1 +1 @@
1
- {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-web-device",
3
- "version": "1.2.2-alpha.8",
3
+ "version": "1.2.2",
4
4
  "author": "OneKey",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -21,13 +21,13 @@
21
21
  "lint:fix": "eslint . --fix"
22
22
  },
23
23
  "dependencies": {
24
- "@onekeyfe/hd-shared": "1.2.2-alpha.8",
25
- "@onekeyfe/hd-transport": "1.2.2-alpha.8"
24
+ "@onekeyfe/hd-shared": "1.2.2",
25
+ "@onekeyfe/hd-transport": "1.2.2"
26
26
  },
27
27
  "devDependencies": {
28
- "@onekeyfe/hd-transport-electron": "1.2.2-alpha.8",
28
+ "@onekeyfe/hd-transport-electron": "1.2.2",
29
29
  "@types/w3c-web-usb": "^1.0.6",
30
30
  "@types/web-bluetooth": "^0.0.17"
31
31
  },
32
- "gitHead": "7a5d129ea1db0b3eb230e309ab29ed3e9e7f141e"
32
+ "gitHead": "859070892dad35c285433c032e5173c9db332ce7"
33
33
  }
@@ -18,7 +18,6 @@ import {
18
18
  HardwareErrorCode,
19
19
  HardwareErrorCodeMessage,
20
20
  createDeferred,
21
- isBleStaleBondErrorText,
22
21
  isBleStaleBondHardwareError,
23
22
  isHeaderChunk,
24
23
  } from '@onekeyfe/hd-shared';
@@ -26,7 +25,7 @@ import {
26
25
  import { resolveBlePacketCapacity } from './ble-packet-capacity';
27
26
 
28
27
  import type { Deferred } from '@onekeyfe/hd-shared';
29
- import type { DesktopAPI } from '@onekeyfe/hd-transport-electron';
28
+ import type { DesktopAPI, NobleBleIpcErrorResponse } from '@onekeyfe/hd-transport-electron';
30
29
  import type {
31
30
  OneKeyDeviceInfo,
32
31
  ProtocolType,
@@ -69,6 +68,22 @@ const toBleDescriptor = (
69
68
  ...(protocolType ? { protocolType } : {}),
70
69
  } as OneKeyDeviceInfo);
71
70
 
71
+ const invokeNobleBle = async <T>(request: Promise<T>): Promise<T> => {
72
+ const response = await (request as Promise<T | NobleBleIpcErrorResponse>);
73
+ if (
74
+ response &&
75
+ typeof response === 'object' &&
76
+ 'type' in response &&
77
+ response.type === 'NobleBleIpcError' &&
78
+ 'success' in response &&
79
+ response.success === false &&
80
+ 'error' in response
81
+ ) {
82
+ return Promise.reject(response.error);
83
+ }
84
+ return response as T;
85
+ };
86
+
72
87
  const BLE_PACKET_SIZE_FALLBACK = 192;
73
88
  const BLE_PACKET_SIZE_MAXIMUM = 244;
74
89
  const BLE_WRITE_DELAY_MS = 5;
@@ -102,13 +117,19 @@ export default class ElectronBleTransport {
102
117
 
103
118
  private connectedDevices: Set<string> = new Set();
104
119
 
120
+ /**
121
+ * Devices whose acquire() is still in flight.
122
+ *
123
+ * A native connect that never calls back has not reached `connectedDevices`
124
+ * yet, so this is the only record that the renderer is still trying to own
125
+ * the link; `releaseNative()` needs it to cancel that pending connect.
126
+ */
127
+ private acquiringDevices: Set<string> = new Set();
128
+
105
129
  private deviceProtocol: Map<string, ProtocolType> = new Map();
106
130
 
107
131
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
108
132
 
109
- /** Endpoints that answered a V2 probe in this transport lifetime. Survives disconnect. */
110
- private confirmedProtocolV2 = new Set<string>();
111
-
112
133
  private deviceMtus: Map<string, number> = new Map();
113
134
 
114
135
  private devicePacketCapacities: Map<string, number> = new Map();
@@ -137,7 +158,7 @@ export default class ElectronBleTransport {
137
158
  this.rejectProtocolV2Frames(uuid, new Error(reason));
138
159
  this.Log?.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
139
160
  if (reason.startsWith('Protocol V2 link-fatal error:')) {
140
- await this.releaseNative(uuid);
161
+ await this.releaseNative(uuid, { forceNative: true });
141
162
  }
142
163
  },
143
164
  });
@@ -163,43 +184,24 @@ export default class ElectronBleTransport {
163
184
 
164
185
  private nextNotificationToken = 1;
165
186
 
166
- private toStaleBondError(error: unknown): Error | null {
167
- if (isBleStaleBondHardwareError(error)) {
168
- return error as Error;
169
- }
170
- const errorMessage =
171
- error && typeof error === 'object' && 'message' in error
172
- ? String((error as { message?: unknown }).message ?? '')
173
- : String(error ?? '');
174
- if (!isBleStaleBondErrorText(errorMessage)) {
175
- return null;
176
- }
177
- const normalizedErrorMessage = errorMessage.toLowerCase();
178
- return ERRORS.TypedError(
179
- normalizedErrorMessage.includes('peer removed pairing information')
180
- ? HardwareErrorCode.BlePeerRemovedPairingInformation
181
- : HardwareErrorCode.BleDeviceBondError,
182
- errorMessage
183
- );
184
- }
185
-
186
- private handleBluetoothError(error: any, mapProtocolV2StaleBond = false): never {
187
- if (mapProtocolV2StaleBond) {
188
- const staleBondError = this.toStaleBondError(error);
189
- if (staleBondError) {
190
- throw staleBondError;
191
- }
192
- }
187
+ private normalizeBluetoothError(error: any): any {
193
188
  if (error && typeof error === 'object') {
189
+ if (typeof error.errorCode === 'number') {
190
+ return ERRORS.TypedError(
191
+ error.errorCode,
192
+ typeof error.message === 'string' ? error.message : undefined,
193
+ error.params
194
+ );
195
+ }
194
196
  if ('code' in error) {
195
197
  if (error.code === HardwareErrorCode.BlePoweredOff) {
196
- throw ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
198
+ return ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
197
199
  }
198
200
  if (error.code === HardwareErrorCode.BleUnsupported) {
199
- throw ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
201
+ return ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
200
202
  }
201
203
  if (error.code === HardwareErrorCode.BlePermissionError) {
202
- throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
204
+ return ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
203
205
  }
204
206
  }
205
207
  const errorMessage = error.message || String(error);
@@ -208,16 +210,20 @@ export default class ElectronBleTransport {
208
210
  const permissionMessage = HardwareErrorCodeMessage[HardwareErrorCode.BlePermissionError];
209
211
 
210
212
  if (errorMessage.includes(poweredOffMessage) || errorMessage.includes('poweredOff')) {
211
- throw ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
213
+ return ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
212
214
  }
213
215
  if (errorMessage.includes(unsupportedMessage) || errorMessage.includes('unsupported')) {
214
- throw ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
216
+ return ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
215
217
  }
216
218
  if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
217
- throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
219
+ return ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
218
220
  }
219
221
  }
220
- throw error;
222
+ return error;
223
+ }
224
+
225
+ private handleBluetoothError(error: any): never {
226
+ throw this.normalizeBluetoothError(error);
221
227
  }
222
228
 
223
229
  private cleanupDeviceState(deviceId: string): void {
@@ -336,7 +342,7 @@ export default class ElectronBleTransport {
336
342
  if (!window.desktopApi?.nobleBle) {
337
343
  throw new Error('Noble BLE API not available');
338
344
  }
339
- const devices = await window.desktopApi.nobleBle.enumerate();
345
+ const devices = await invokeNobleBle(window.desktopApi.nobleBle.enumerate());
340
346
  this.Log?.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
341
347
  for (const dev of devices) {
342
348
  this.Log?.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`);
@@ -350,9 +356,6 @@ export default class ElectronBleTransport {
350
356
 
351
357
  async acquire(input: BleAcquireInput) {
352
358
  const { uuid, forceCleanRunPromise, expectedProtocol } = input;
353
- const shouldMapProtocolV2StaleBond = expectedProtocol
354
- ? expectedProtocol === 'V2'
355
- : this.confirmedProtocolV2.has(uuid);
356
359
 
357
360
  if (!uuid) {
358
361
  throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
@@ -369,12 +372,13 @@ export default class ElectronBleTransport {
369
372
  this.runPromiseDeviceId = null;
370
373
  }
371
374
 
375
+ this.acquiringDevices.add(uuid);
372
376
  try {
373
377
  if (!window.desktopApi?.nobleBle) {
374
378
  throw new Error('Noble BLE API not available');
375
379
  }
376
380
 
377
- const device = await window.desktopApi.nobleBle.getDevice(uuid);
381
+ const device = await invokeNobleBle(window.desktopApi.nobleBle.getDevice(uuid));
378
382
  if (!device) {
379
383
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${uuid} not found`);
380
384
  }
@@ -386,10 +390,10 @@ export default class ElectronBleTransport {
386
390
  }
387
391
 
388
392
  try {
389
- await window.desktopApi.nobleBle.connect(uuid);
393
+ await invokeNobleBle(window.desktopApi.nobleBle.connect(uuid));
390
394
  this.connectedDevices.add(uuid);
391
395
  } catch (error) {
392
- this.handleBluetoothError(error, shouldMapProtocolV2StaleBond);
396
+ this.handleBluetoothError(error);
393
397
  }
394
398
 
395
399
  const mtuCleanup = this.createMtuSubscription(uuid);
@@ -401,9 +405,9 @@ export default class ElectronBleTransport {
401
405
  this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
402
406
 
403
407
  try {
404
- await window.desktopApi.nobleBle.subscribe(uuid);
408
+ await invokeNobleBle(window.desktopApi.nobleBle.subscribe(uuid));
405
409
  } catch (error) {
406
- this.handleBluetoothError(error, shouldMapProtocolV2StaleBond);
410
+ this.handleBluetoothError(error);
407
411
  }
408
412
  await this.refreshBlePacketCapacity(uuid);
409
413
 
@@ -426,15 +430,22 @@ export default class ElectronBleTransport {
426
430
  } catch (error) {
427
431
  this.Log?.error('[Electron BLE] acquire failed:', error);
428
432
  try {
429
- if (window.desktopApi?.nobleBle && this.connectedDevices.has(uuid)) {
430
- await window.desktopApi.nobleBle.unsubscribe(uuid);
431
- await window.desktopApi.nobleBle.disconnect(uuid);
433
+ const nobleBle = window.desktopApi?.nobleBle;
434
+ if (nobleBle) {
435
+ if (this.connectedDevices.has(uuid)) {
436
+ await invokeNobleBle(nobleBle.unsubscribe(uuid)).catch(cleanupError => {
437
+ this.Log?.debug('[Electron BLE] acquire unsubscribe failed:', cleanupError);
438
+ });
439
+ }
440
+ await invokeNobleBle(nobleBle.disconnect(uuid));
432
441
  }
433
442
  } catch (cleanupError) {
434
443
  this.Log?.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
435
444
  }
436
445
  this.cleanupDeviceState(uuid);
437
- throw error;
446
+ this.handleBluetoothError(error);
447
+ } finally {
448
+ this.acquiringDevices.delete(uuid);
438
449
  }
439
450
  }
440
451
 
@@ -455,15 +466,31 @@ export default class ElectronBleTransport {
455
466
  }
456
467
 
457
468
  // Hard teardown, error paths only: a link presumed dead must not be reused.
458
- private async releaseNative(id: string) {
469
+ //
470
+ // The native disconnect is sent while the renderer still owns the link, or is
471
+ // still trying to get it: a stuck acquire has not reached `connectedDevices`
472
+ // yet, and its pending native connect can only be cancelled from here, so the
473
+ // Core acquire deadline would otherwise be unable to terminate it.
474
+ //
475
+ // It is NOT sent for a device the renderer has already released logically.
476
+ // That link belongs to the main-process keep-alive timer, and dropping it on
477
+ // the routine post-call `cancel()` (Device.interruptionFromUser, no acquire
478
+ // held, empty request queue) costs a full cold reconnect on the very next
479
+ // operation — measured 2.93s on Pro and 17-26s on Pro 2.
480
+ private async releaseNative(id: string, options?: { forceNative?: boolean }) {
481
+ const rendererOwnsLink = this.connectedDevices.has(id) || this.acquiringDevices.has(id);
482
+ const shouldDisconnect = options?.forceNative === true || rendererOwnsLink;
459
483
  try {
460
- if (this.connectedDevices.has(id)) {
461
- if (window.desktopApi?.nobleBle) {
462
- await window.desktopApi.nobleBle.unsubscribe(id);
463
- await window.desktopApi.nobleBle.disconnect(id);
484
+ const nobleBle = window.desktopApi?.nobleBle;
485
+ if (nobleBle && shouldDisconnect) {
486
+ if (this.connectedDevices.has(id)) {
487
+ await invokeNobleBle(nobleBle.unsubscribe(id)).catch(error => {
488
+ this.Log?.debug('[Electron BLE] release unsubscribe failed:', error);
489
+ });
464
490
  }
465
- this.cleanupDeviceState(id);
491
+ await invokeNobleBle(nobleBle.disconnect(id));
466
492
  }
493
+ this.cleanupDeviceState(id);
467
494
  } catch (error) {
468
495
  this.Log?.error('[Electron BLE] release failed:', error);
469
496
  this.cleanupDeviceState(id);
@@ -489,19 +516,16 @@ export default class ElectronBleTransport {
489
516
  }
490
517
  return;
491
518
  }
492
- await release(id, keepSession);
519
+ await invokeNobleBle(release(id, keepSession));
493
520
  } catch (error) {
494
521
  this.Log?.error('[Electron BLE] logical release failed:', error);
495
522
  this.cleanupDeviceState(id);
496
523
  }
497
524
  }
498
525
 
499
- private createProtocolMismatchError(expected: ProtocolType, uuid: string) {
500
- // A generic Ping miss is not a bond failure. Only a later miss after this
501
- // endpoint already answered V2, or a native encryption/pairing error, is.
502
- const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
526
+ private createProtocolMismatchError(expected: ProtocolType) {
503
527
  return ERRORS.TypedError(
504
- isStaleV2Bond ? HardwareErrorCode.BleDeviceBondError : HardwareErrorCode.RuntimeError,
528
+ HardwareErrorCode.RuntimeError,
505
529
  `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
506
530
  );
507
531
  }
@@ -541,13 +565,12 @@ export default class ElectronBleTransport {
541
565
  }
542
566
 
543
567
  if (expectedProtocol === 'V2') {
544
- if (await this.probeProtocolV2(uuid)) {
568
+ if (await this.probeProtocolV2(uuid, expectedProtocol)) {
545
569
  this.deviceProtocol.set(uuid, 'V2');
546
- this.confirmedProtocolV2.add(uuid);
547
570
  this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
548
571
  return 'V2';
549
572
  }
550
- throw this.createProtocolMismatchError(expectedProtocol, uuid);
573
+ throw this.createProtocolMismatchError(expectedProtocol);
551
574
  }
552
575
 
553
576
  // Protocol must be actively probed after connection. Name, PID, and descriptors only
@@ -566,9 +589,6 @@ export default class ElectronBleTransport {
566
589
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
567
590
  if (detected) {
568
591
  this.deviceProtocol.set(uuid, protocol);
569
- if (protocol === 'V2') {
570
- this.confirmedProtocolV2.add(uuid);
571
- }
572
592
  this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
573
593
  return protocol;
574
594
  }
@@ -639,7 +659,7 @@ export default class ElectronBleTransport {
639
659
  }
640
660
  }
641
661
 
642
- private async probeProtocolV2(uuid: string) {
662
+ private async probeProtocolV2(uuid: string, expectedProtocol?: ProtocolType) {
643
663
  if (!this._messages || !this._messagesV2) {
644
664
  return false;
645
665
  }
@@ -655,8 +675,11 @@ export default class ElectronBleTransport {
655
675
  this.v2Assemblers.get(uuid)?.reset();
656
676
  this.resetProtocolV2Frames(uuid);
657
677
  },
678
+ // A declared V2 protocol needs no fallback; preserve the actual link failure.
658
679
  shouldRethrow: error =>
659
- isBleStaleBondHardwareError(error) || isProtocolV2LinkDisabledError(error),
680
+ expectedProtocol === 'V2' ||
681
+ isBleStaleBondHardwareError(error) ||
682
+ isProtocolV2LinkDisabledError(error),
660
683
  });
661
684
  if (!detected) {
662
685
  this.clearProbeProtocol(uuid, 'V2');
@@ -671,18 +694,15 @@ export default class ElectronBleTransport {
671
694
  }
672
695
 
673
696
  try {
674
- await nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
697
+ await invokeNobleBle(nobleBle.write(uuid, hexData, { pacingDelayMs: 0 }));
675
698
  } catch (error) {
676
- const staleBondError = this.toStaleBondError(error);
677
- if (staleBondError) {
678
- throw staleBondError;
679
- }
680
- throw error;
699
+ this.handleBluetoothError(error);
681
700
  }
682
701
  }
683
702
 
684
703
  private async refreshBlePacketCapacity(uuid: string): Promise<void> {
685
- const device = await window.desktopApi?.nobleBle?.getDevice(uuid);
704
+ const nobleBle = window.desktopApi?.nobleBle;
705
+ const device = nobleBle ? await invokeNobleBle(nobleBle.getDevice(uuid)) : undefined;
686
706
  this.updateBlePacketCapacity(uuid, device?.mtu);
687
707
  }
688
708
 
@@ -940,7 +960,7 @@ export default class ElectronBleTransport {
940
960
  if (hexString.length === 0) {
941
961
  throw new Error(`Buffer ${i + 1} is empty`);
942
962
  }
943
- await window.desktopApi.nobleBle.write(uuid, hexString);
963
+ await invokeNobleBle(window.desktopApi.nobleBle.write(uuid, hexString));
944
964
  }
945
965
 
946
966
  const response = await Promise.race([
@@ -975,10 +995,10 @@ export default class ElectronBleTransport {
975
995
  this.notificationCleanups.delete(uuid);
976
996
  this.notificationTokens.delete(uuid);
977
997
  if (!isProbeTimeout) {
978
- await this.releaseNative(uuid);
998
+ await this.releaseNative(uuid, { forceNative: true });
979
999
  }
980
1000
  }
981
- throw e;
1001
+ throw this.normalizeBluetoothError(e);
982
1002
  } finally {
983
1003
  if (timeout) clearTimeout(timeout);
984
1004
  if (this.runPromise === runPromise) {
package/src/webusb.ts CHANGED
@@ -566,10 +566,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
566
566
  if (!device.opened) {
567
567
  await device.open();
568
568
  }
569
- try {
570
- await device.reset();
571
- } catch (error) {
572
- this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
569
+ // A V1 packet retry continues an in-flight exchange. Preserve its endpoint
570
+ // state as in the legacy transport; probing and V2 recovery still reset.
571
+ if (first || this.deviceProtocol.get(path) !== 'V1') {
572
+ try {
573
+ await device.reset();
574
+ } catch (error) {
575
+ this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
576
+ }
573
577
  }
574
578
  await this.getConnectedDevices();
575
579
  device = await this.findDevice(path);