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

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,21 +665,40 @@ describe('ElectronBleTransport protocol detection', () => {
568
665
  expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
569
666
  });
570
667
 
571
- test('maps macOS CoreBluetooth peer-removed pairing failures to the precise error', async () => {
572
- const device = { id: 'reset-pro2-macos-id', name: 'OneKey Pro 2' };
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' };
573
670
  const nobleBle = createNobleBle(device);
574
- nobleBle.connect.mockRejectedValue(
575
- new Error('CBErrorDomain:14 Peer removed pairing information on the device side')
576
- );
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
+ });
577
677
  const transport = configureTransport(nobleBle);
578
678
 
579
- await expect(
580
- transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
581
- ).rejects.toMatchObject({
582
- errorCode: HardwareErrorCode.BlePeerRemovedPairingInformation,
679
+ await expect(transport.acquire({ uuid: device.id })).rejects.toMatchObject({
680
+ name: 'HardwareError',
681
+ errorCode: HardwareErrorCode.BleBondInvalid,
682
+ params: { nativeErrorMessage: 'CBErrorDomain:14 native message' },
583
683
  });
584
684
  });
585
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
+
586
702
  test('does not classify a generic macOS connection failure as a stale bond', async () => {
587
703
  const device = { id: 'offline-pro2-macos-id', name: 'OneKey Pro 2' };
588
704
  const nobleBle = createNobleBle(device);
@@ -615,7 +731,7 @@ describe('ElectronBleTransport protocol detection', () => {
615
731
  }
616
732
  });
617
733
 
618
- 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 () => {
619
735
  const device = { id: 'reset-pro2-id', name: 'OneKey Pro 2' };
620
736
  const nobleBle = createNobleBle(device);
621
737
  const transport = configureTransport(nobleBle);
@@ -628,7 +744,7 @@ describe('ElectronBleTransport protocol detection', () => {
628
744
  await expect(
629
745
  transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
630
746
  ).rejects.toMatchObject({
631
- errorCode: HardwareErrorCode.BleDeviceBondError,
747
+ errorCode: HardwareErrorCode.RuntimeError,
632
748
  });
633
749
 
634
750
  expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
@@ -636,6 +752,52 @@ describe('ElectronBleTransport protocol detection', () => {
636
752
  expect(transport.getProtocolType(device.id)).toBeUndefined();
637
753
  });
638
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
+
639
801
  test('does not take a Protocol V2 hint from the BLE name', async () => {
640
802
  const device = { id: 'named-pro2-id', name: 'OneKey Pro 2' };
641
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,40 +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
- normalizedErrorMessage.includes('cberrordomain:14')
888
- ? hdShared.HardwareErrorCode.BlePeerRemovedPairingInformation
889
- : hdShared.HardwareErrorCode.BleDeviceBondError, errorMessage);
890
- }
891
- handleBluetoothError(error, mapProtocolV2StaleBond = false) {
892
- if (mapProtocolV2StaleBond) {
893
- const staleBondError = this.toStaleBondError(error);
894
- if (staleBondError) {
895
- throw staleBondError;
896
- }
897
- }
889
+ normalizeBluetoothError(error) {
898
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
+ }
899
894
  if ('code' in error) {
900
895
  if (error.code === hdShared.HardwareErrorCode.BlePoweredOff) {
901
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
896
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
902
897
  }
903
898
  if (error.code === hdShared.HardwareErrorCode.BleUnsupported) {
904
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
899
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
905
900
  }
906
901
  if (error.code === hdShared.HardwareErrorCode.BlePermissionError) {
907
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
902
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
908
903
  }
909
904
  }
910
905
  const errorMessage = error.message || String(error);
@@ -912,16 +907,19 @@ class ElectronBleTransport {
912
907
  const unsupportedMessage = hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BleUnsupported];
913
908
  const permissionMessage = hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BlePermissionError];
914
909
  if (errorMessage.includes(poweredOffMessage) || errorMessage.includes('poweredOff')) {
915
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
910
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
916
911
  }
917
912
  if (errorMessage.includes(unsupportedMessage) || errorMessage.includes('unsupported')) {
918
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
913
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
919
914
  }
920
915
  if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
921
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
916
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
922
917
  }
923
918
  }
924
- throw error;
919
+ return error;
920
+ }
921
+ handleBluetoothError(error) {
922
+ throw this.normalizeBluetoothError(error);
925
923
  }
926
924
  cleanupDeviceState(deviceId) {
927
925
  this.protocolV2Links
@@ -1008,7 +1006,7 @@ class ElectronBleTransport {
1008
1006
  if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
1009
1007
  throw new Error('Noble BLE API not available');
1010
1008
  }
1011
- const devices = yield window.desktopApi.nobleBle.enumerate();
1009
+ const devices = yield invokeNobleBle(window.desktopApi.nobleBle.enumerate());
1012
1010
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
1013
1011
  for (const dev of devices) {
1014
1012
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`);
@@ -1025,9 +1023,6 @@ class ElectronBleTransport {
1025
1023
  var _a, _b, _c, _d, _e, _f, _g;
1026
1024
  return __awaiter(this, void 0, void 0, function* () {
1027
1025
  const { uuid, forceCleanRunPromise, expectedProtocol } = input;
1028
- const shouldMapProtocolV2StaleBond = expectedProtocol
1029
- ? expectedProtocol === 'V2'
1030
- : this.confirmedProtocolV2.has(uuid);
1031
1026
  if (!uuid) {
1032
1027
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleRequiredUUID);
1033
1028
  }
@@ -1040,11 +1035,12 @@ class ElectronBleTransport {
1040
1035
  this.runPromise = null;
1041
1036
  this.runPromiseDeviceId = null;
1042
1037
  }
1038
+ this.acquiringDevices.add(uuid);
1043
1039
  try {
1044
1040
  if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
1045
1041
  throw new Error('Noble BLE API not available');
1046
1042
  }
1047
- const device = yield window.desktopApi.nobleBle.getDevice(uuid);
1043
+ const device = yield invokeNobleBle(window.desktopApi.nobleBle.getDevice(uuid));
1048
1044
  if (!device) {
1049
1045
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${uuid} not found`);
1050
1046
  }
@@ -1055,11 +1051,11 @@ class ElectronBleTransport {
1055
1051
  this.deviceProtocolHints.set(uuid, protocolHint);
1056
1052
  }
1057
1053
  try {
1058
- yield window.desktopApi.nobleBle.connect(uuid);
1054
+ yield invokeNobleBle(window.desktopApi.nobleBle.connect(uuid));
1059
1055
  this.connectedDevices.add(uuid);
1060
1056
  }
1061
1057
  catch (error) {
1062
- this.handleBluetoothError(error, shouldMapProtocolV2StaleBond);
1058
+ this.handleBluetoothError(error);
1063
1059
  }
1064
1060
  const mtuCleanup = this.createMtuSubscription(uuid);
1065
1061
  if (mtuCleanup) {
@@ -1068,10 +1064,10 @@ class ElectronBleTransport {
1068
1064
  this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
1069
1065
  this.v2Assemblers.set(uuid, new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
1070
1066
  try {
1071
- yield window.desktopApi.nobleBle.subscribe(uuid);
1067
+ yield invokeNobleBle(window.desktopApi.nobleBle.subscribe(uuid));
1072
1068
  }
1073
1069
  catch (error) {
1074
- this.handleBluetoothError(error, shouldMapProtocolV2StaleBond);
1070
+ this.handleBluetoothError(error);
1075
1071
  }
1076
1072
  yield this.refreshBlePacketCapacity(uuid);
1077
1073
  const cleanup = this.createNotificationSubscription(uuid);
@@ -1089,16 +1085,25 @@ class ElectronBleTransport {
1089
1085
  catch (error) {
1090
1086
  (_e = this.Log) === null || _e === void 0 ? void 0 : _e.error('[Electron BLE] acquire failed:', error);
1091
1087
  try {
1092
- if (((_f = window.desktopApi) === null || _f === void 0 ? void 0 : _f.nobleBle) && this.connectedDevices.has(uuid)) {
1093
- yield window.desktopApi.nobleBle.unsubscribe(uuid);
1094
- 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));
1095
1097
  }
1096
1098
  }
1097
1099
  catch (cleanupError) {
1098
1100
  (_g = this.Log) === null || _g === void 0 ? void 0 : _g.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
1099
1101
  }
1100
1102
  this.cleanupDeviceState(uuid);
1101
- throw error;
1103
+ this.handleBluetoothError(error);
1104
+ }
1105
+ finally {
1106
+ this.acquiringDevices.delete(uuid);
1102
1107
  }
1103
1108
  });
1104
1109
  }
@@ -1120,17 +1125,23 @@ class ElectronBleTransport {
1120
1125
  return this.releaseNative(id);
1121
1126
  });
1122
1127
  }
1123
- releaseNative(id) {
1128
+ releaseNative(id, options) {
1124
1129
  var _a, _b;
1125
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;
1126
1133
  try {
1127
- if (this.connectedDevices.has(id)) {
1128
- if ((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) {
1129
- yield window.desktopApi.nobleBle.unsubscribe(id);
1130
- 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
+ });
1131
1141
  }
1132
- this.cleanupDeviceState(id);
1142
+ yield invokeNobleBle(nobleBle.disconnect(id));
1133
1143
  }
1144
+ this.cleanupDeviceState(id);
1134
1145
  }
1135
1146
  catch (error) {
1136
1147
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.error('[Electron BLE] release failed:', error);
@@ -1154,7 +1165,7 @@ class ElectronBleTransport {
1154
1165
  }
1155
1166
  return;
1156
1167
  }
1157
- yield release(id, keepSession);
1168
+ yield invokeNobleBle(release(id, keepSession));
1158
1169
  }
1159
1170
  catch (error) {
1160
1171
  (_d = this.Log) === null || _d === void 0 ? void 0 : _d.error('[Electron BLE] logical release failed:', error);
@@ -1162,9 +1173,8 @@ class ElectronBleTransport {
1162
1173
  }
1163
1174
  });
1164
1175
  }
1165
- createProtocolMismatchError(expected, uuid) {
1166
- const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
1167
- 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`);
1168
1178
  }
1169
1179
  createProtocolDetectionError() {
1170
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');
@@ -1183,13 +1193,12 @@ class ElectronBleTransport {
1183
1193
  return 'V1';
1184
1194
  }
1185
1195
  if (expectedProtocol === 'V2') {
1186
- if (yield this.probeProtocolV2(uuid)) {
1196
+ if (yield this.probeProtocolV2(uuid, expectedProtocol)) {
1187
1197
  this.deviceProtocol.set(uuid, 'V2');
1188
- this.confirmedProtocolV2.add(uuid);
1189
1198
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1190
1199
  return 'V2';
1191
1200
  }
1192
- throw this.createProtocolMismatchError(expectedProtocol, uuid);
1201
+ throw this.createProtocolMismatchError(expectedProtocol);
1193
1202
  }
1194
1203
  const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1195
1204
  for (let i = 0; i < probeOrder.length; i += 1) {
@@ -1200,9 +1209,6 @@ class ElectronBleTransport {
1200
1209
  const detected = protocol === 'V1' ? yield this.probeProtocolV1(uuid) : yield this.probeProtocolV2(uuid);
1201
1210
  if (detected) {
1202
1211
  this.deviceProtocol.set(uuid, protocol);
1203
- if (protocol === 'V2') {
1204
- this.confirmedProtocolV2.add(uuid);
1205
- }
1206
1212
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
1207
1213
  return protocol;
1208
1214
  }
@@ -1265,7 +1271,7 @@ class ElectronBleTransport {
1265
1271
  }
1266
1272
  });
1267
1273
  }
1268
- probeProtocolV2(uuid) {
1274
+ probeProtocolV2(uuid, expectedProtocol) {
1269
1275
  var _a;
1270
1276
  return __awaiter(this, void 0, void 0, function* () {
1271
1277
  if (!this._messages || !this._messagesV2) {
@@ -1283,7 +1289,9 @@ class ElectronBleTransport {
1283
1289
  (_a = this.v2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
1284
1290
  this.resetProtocolV2Frames(uuid);
1285
1291
  },
1286
- shouldRethrow: error => hdShared.isBleStaleBondHardwareError(error) || transport.isProtocolV2LinkDisabledError(error),
1292
+ shouldRethrow: error => expectedProtocol === 'V2' ||
1293
+ hdShared.isBleStaleBondHardwareError(error) ||
1294
+ transport.isProtocolV2LinkDisabledError(error),
1287
1295
  });
1288
1296
  if (!detected) {
1289
1297
  this.clearProbeProtocol(uuid, 'V2');
@@ -1299,21 +1307,18 @@ class ElectronBleTransport {
1299
1307
  throw new Error('Noble BLE API not available');
1300
1308
  }
1301
1309
  try {
1302
- yield nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
1310
+ yield invokeNobleBle(nobleBle.write(uuid, hexData, { pacingDelayMs: 0 }));
1303
1311
  }
1304
1312
  catch (error) {
1305
- const staleBondError = this.toStaleBondError(error);
1306
- if (staleBondError) {
1307
- throw staleBondError;
1308
- }
1309
- throw error;
1313
+ this.handleBluetoothError(error);
1310
1314
  }
1311
1315
  });
1312
1316
  }
1313
1317
  refreshBlePacketCapacity(uuid) {
1314
- var _a, _b;
1318
+ var _a;
1315
1319
  return __awaiter(this, void 0, void 0, function* () {
1316
- 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;
1317
1322
  this.updateBlePacketCapacity(uuid, device === null || device === void 0 ? void 0 : device.mtu);
1318
1323
  });
1319
1324
  }
@@ -1536,7 +1541,7 @@ class ElectronBleTransport {
1536
1541
  if (hexString.length === 0) {
1537
1542
  throw new Error(`Buffer ${i + 1} is empty`);
1538
1543
  }
1539
- yield window.desktopApi.nobleBle.write(uuid, hexString);
1544
+ yield invokeNobleBle(window.desktopApi.nobleBle.write(uuid, hexString));
1540
1545
  }
1541
1546
  const response = yield Promise.race([
1542
1547
  runPromise.promise,
@@ -1566,10 +1571,10 @@ class ElectronBleTransport {
1566
1571
  this.notificationCleanups.delete(uuid);
1567
1572
  this.notificationTokens.delete(uuid);
1568
1573
  if (!isProbeTimeout) {
1569
- yield this.releaseNative(uuid);
1574
+ yield this.releaseNative(uuid, { forceNative: true });
1570
1575
  }
1571
1576
  }
1572
- throw e;
1577
+ throw this.normalizeBluetoothError(e);
1573
1578
  }
1574
1579
  finally {
1575
1580
  if (timeout)
@@ -1 +1 @@
1
- {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":";;AACA,OAAO,SAAS,EAAE,EAQhB,0BAA0B,EAG3B,MAAM,wBAAwB,CAAC;AAYhC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAuBhC,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,CAAC;IAClB,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAuCD,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,0BAA0B,CAAC,MAAM,CAAC;IAC7E,QAAQ,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAGlE,UAAU,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAEpE,OAAO,CAAC,sBAAsB,CAAqB;IAGnD,OAAO,CAAC,cAAc,CAAwC;IAG9D,OAAO,CAAC,wBAAwB,CAAwC;IAExE,OAAO,CAAC,mBAAmB,CAAwC;IAMnE,OAAO,CAAC,kBAAkB,CAA0B;IAGpD,OAAO,CAAC,aAAa,CAA0B;IAS/C,OAAO,CAAC,mBAAmB,CAAqC;IAGhE,OAAO,CAAC,eAAe,CAA2C;IAElE,IAAI,SAAqB;IAEzB,OAAO,UAAS;IAEhB,UAAU,UAAS;IAEnB,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAMvB,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAM;IAEnC,eAAe,SAAoB;IAEnC,UAAU,SAAe;IAEzB,WAAW,SAAgB;;IAa3B,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,YAAY;IAmBxC,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS;IAkBrD,iBAAiB,CAAC,IAAI,EAAE,MAAM;IAQ9B,SAAS,CAAC,UAAU,EAAE,GAAG;IASzB,mBAAmB,CAAC,UAAU,EAAE,GAAG;IAsB7B,kBAAkB;IAmBlB,SAAS;IAQT,mBAAmB;IAmCnB,OAAO,CAAC,KAAK,EAAE,YAAY;IA+FjC,OAAO,CAAC,2BAA2B;IAOnC,OAAO,CAAC,+BAA+B;IAOvC,OAAO,CAAC,4BAA4B;YAItB,cAAc;IAmEtB,UAAU,CAAC,IAAI,EAAE,MAAM;IAwBvB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;IAkB1C,OAAO,CAAC,iBAAiB;IAiCnB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;YAgCpC,eAAe;YAkBf,iBAAiB;IAsB/B,OAAO,CAAC,cAAc;IAMhB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IASvE,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,wBAAwB;YAalB,yBAAyB;IAsCvC,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,aAAa;YASP,oBAAoB;YA2BpB,eAAe;YAaf,mBAAmB;YA2CnB,cAAc;YAWd,yBAAyB;YAKzB,yBAAyB;YAMzB,uBAAuB;YA0CvB,eAAe;YAoBf,eAAe;IAgBvB,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,oBAAoB;YA2BlB,cAAc;YAkCd,cAAc;IAYtB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM;IAgD5C,OAAO,CAAC,IAAI,EAAE,MAAM;IAY1B,SAAS,CAAC,uBAAuB,IAAI,iBAAiB;IAUtD,SAAS,CAAC,sBAAsB;cAIhB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAAC,IAAI,CAAC;cAIA,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAAC,UAAU,CAAC;cASN,4BAA4B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1F,SAAS,CAAC,8BAA8B,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAKrE,SAAS,CAAC,+BAA+B,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,KAAK;IAWjF,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;CAGxD"}
1
+ {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":";;AACA,OAAO,SAAS,EAAE,EAQhB,0BAA0B,EAG3B,MAAM,wBAAwB,CAAC;AAYhC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAuBhC,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,CAAC;IAClB,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAuCD,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,0BAA0B,CAAC,MAAM,CAAC;IAC7E,QAAQ,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAGlE,UAAU,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAEpE,OAAO,CAAC,sBAAsB,CAAqB;IAGnD,OAAO,CAAC,cAAc,CAAwC;IAG9D,OAAO,CAAC,wBAAwB,CAAwC;IAExE,OAAO,CAAC,mBAAmB,CAAwC;IAMnE,OAAO,CAAC,kBAAkB,CAA0B;IAGpD,OAAO,CAAC,aAAa,CAA0B;IAS/C,OAAO,CAAC,mBAAmB,CAAqC;IAGhE,OAAO,CAAC,eAAe,CAA2C;IAElE,IAAI,SAAqB;IAEzB,OAAO,UAAS;IAEhB,UAAU,UAAS;IAEnB,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAMvB,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAM;IAEnC,eAAe,SAAoB;IAEnC,UAAU,SAAe;IAEzB,WAAW,SAAgB;;IAa3B,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,YAAY;IAmBxC,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS;IAkBrD,iBAAiB,CAAC,IAAI,EAAE,MAAM;IAQ9B,SAAS,CAAC,UAAU,EAAE,GAAG;IASzB,mBAAmB,CAAC,UAAU,EAAE,GAAG;IAsB7B,kBAAkB;IAmBlB,SAAS;IAQT,mBAAmB;IAmCnB,OAAO,CAAC,KAAK,EAAE,YAAY;IA+FjC,OAAO,CAAC,2BAA2B;IAOnC,OAAO,CAAC,+BAA+B;IAOvC,OAAO,CAAC,4BAA4B;YAItB,cAAc;IAmEtB,UAAU,CAAC,IAAI,EAAE,MAAM;IAwBvB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;IAkB1C,OAAO,CAAC,iBAAiB;IAiCnB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;YAoCpC,eAAe;YAkBf,iBAAiB;IAsB/B,OAAO,CAAC,cAAc;IAMhB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IASvE,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,wBAAwB;YAalB,yBAAyB;IAsCvC,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,aAAa;YASP,oBAAoB;YA2BpB,eAAe;YAaf,mBAAmB;YA2CnB,cAAc;YAWd,yBAAyB;YAKzB,yBAAyB;YAMzB,uBAAuB;YA0CvB,eAAe;YAoBf,eAAe;IAgBvB,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,oBAAoB;YA2BlB,cAAc;YAkCd,cAAc;IAYtB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM;IAgD5C,OAAO,CAAC,IAAI,EAAE,MAAM;IAY1B,SAAS,CAAC,uBAAuB,IAAI,iBAAiB;IAUtD,SAAS,CAAC,sBAAsB;cAIhB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAAC,IAAI,CAAC;cAIA,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAAC,UAAU,CAAC;cASN,4BAA4B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1F,SAAS,CAAC,8BAA8B,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAKrE,SAAS,CAAC,+BAA+B,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,KAAK;IAWjF,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;CAGxD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-web-device",
3
- "version": "1.2.2-alpha.12",
3
+ "version": "1.2.2-alpha.122",
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.12",
25
- "@onekeyfe/hd-transport": "1.2.2-alpha.12"
24
+ "@onekeyfe/hd-shared": "1.2.2-alpha.122",
25
+ "@onekeyfe/hd-transport": "1.2.2-alpha.122"
26
26
  },
27
27
  "devDependencies": {
28
- "@onekeyfe/hd-transport-electron": "1.2.2-alpha.12",
28
+ "@onekeyfe/hd-transport-electron": "1.2.2-alpha.122",
29
29
  "@types/w3c-web-usb": "^1.0.6",
30
30
  "@types/web-bluetooth": "^0.0.17"
31
31
  },
32
- "gitHead": "9b5a45bc55819d22ed752899b4c40aad70de4527"
32
+ "gitHead": "37770cd3ae109751f082715228b6155487b7da56"
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,44 +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
- normalizedErrorMessage.includes('cberrordomain:14')
181
- ? HardwareErrorCode.BlePeerRemovedPairingInformation
182
- : HardwareErrorCode.BleDeviceBondError,
183
- errorMessage
184
- );
185
- }
186
-
187
- private handleBluetoothError(error: any, mapProtocolV2StaleBond = false): never {
188
- if (mapProtocolV2StaleBond) {
189
- const staleBondError = this.toStaleBondError(error);
190
- if (staleBondError) {
191
- throw staleBondError;
192
- }
193
- }
187
+ private normalizeBluetoothError(error: any): any {
194
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
+ }
195
196
  if ('code' in error) {
196
197
  if (error.code === HardwareErrorCode.BlePoweredOff) {
197
- throw ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
198
+ return ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
198
199
  }
199
200
  if (error.code === HardwareErrorCode.BleUnsupported) {
200
- throw ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
201
+ return ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
201
202
  }
202
203
  if (error.code === HardwareErrorCode.BlePermissionError) {
203
- throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
204
+ return ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
204
205
  }
205
206
  }
206
207
  const errorMessage = error.message || String(error);
@@ -209,16 +210,20 @@ export default class ElectronBleTransport {
209
210
  const permissionMessage = HardwareErrorCodeMessage[HardwareErrorCode.BlePermissionError];
210
211
 
211
212
  if (errorMessage.includes(poweredOffMessage) || errorMessage.includes('poweredOff')) {
212
- throw ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
213
+ return ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
213
214
  }
214
215
  if (errorMessage.includes(unsupportedMessage) || errorMessage.includes('unsupported')) {
215
- throw ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
216
+ return ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
216
217
  }
217
218
  if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
218
- throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
219
+ return ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
219
220
  }
220
221
  }
221
- throw error;
222
+ return error;
223
+ }
224
+
225
+ private handleBluetoothError(error: any): never {
226
+ throw this.normalizeBluetoothError(error);
222
227
  }
223
228
 
224
229
  private cleanupDeviceState(deviceId: string): void {
@@ -337,7 +342,7 @@ export default class ElectronBleTransport {
337
342
  if (!window.desktopApi?.nobleBle) {
338
343
  throw new Error('Noble BLE API not available');
339
344
  }
340
- const devices = await window.desktopApi.nobleBle.enumerate();
345
+ const devices = await invokeNobleBle(window.desktopApi.nobleBle.enumerate());
341
346
  this.Log?.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
342
347
  for (const dev of devices) {
343
348
  this.Log?.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`);
@@ -351,9 +356,6 @@ export default class ElectronBleTransport {
351
356
 
352
357
  async acquire(input: BleAcquireInput) {
353
358
  const { uuid, forceCleanRunPromise, expectedProtocol } = input;
354
- const shouldMapProtocolV2StaleBond = expectedProtocol
355
- ? expectedProtocol === 'V2'
356
- : this.confirmedProtocolV2.has(uuid);
357
359
 
358
360
  if (!uuid) {
359
361
  throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
@@ -370,12 +372,13 @@ export default class ElectronBleTransport {
370
372
  this.runPromiseDeviceId = null;
371
373
  }
372
374
 
375
+ this.acquiringDevices.add(uuid);
373
376
  try {
374
377
  if (!window.desktopApi?.nobleBle) {
375
378
  throw new Error('Noble BLE API not available');
376
379
  }
377
380
 
378
- const device = await window.desktopApi.nobleBle.getDevice(uuid);
381
+ const device = await invokeNobleBle(window.desktopApi.nobleBle.getDevice(uuid));
379
382
  if (!device) {
380
383
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${uuid} not found`);
381
384
  }
@@ -387,10 +390,10 @@ export default class ElectronBleTransport {
387
390
  }
388
391
 
389
392
  try {
390
- await window.desktopApi.nobleBle.connect(uuid);
393
+ await invokeNobleBle(window.desktopApi.nobleBle.connect(uuid));
391
394
  this.connectedDevices.add(uuid);
392
395
  } catch (error) {
393
- this.handleBluetoothError(error, shouldMapProtocolV2StaleBond);
396
+ this.handleBluetoothError(error);
394
397
  }
395
398
 
396
399
  const mtuCleanup = this.createMtuSubscription(uuid);
@@ -402,9 +405,9 @@ export default class ElectronBleTransport {
402
405
  this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
403
406
 
404
407
  try {
405
- await window.desktopApi.nobleBle.subscribe(uuid);
408
+ await invokeNobleBle(window.desktopApi.nobleBle.subscribe(uuid));
406
409
  } catch (error) {
407
- this.handleBluetoothError(error, shouldMapProtocolV2StaleBond);
410
+ this.handleBluetoothError(error);
408
411
  }
409
412
  await this.refreshBlePacketCapacity(uuid);
410
413
 
@@ -427,15 +430,22 @@ export default class ElectronBleTransport {
427
430
  } catch (error) {
428
431
  this.Log?.error('[Electron BLE] acquire failed:', error);
429
432
  try {
430
- if (window.desktopApi?.nobleBle && this.connectedDevices.has(uuid)) {
431
- await window.desktopApi.nobleBle.unsubscribe(uuid);
432
- 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));
433
441
  }
434
442
  } catch (cleanupError) {
435
443
  this.Log?.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
436
444
  }
437
445
  this.cleanupDeviceState(uuid);
438
- throw error;
446
+ this.handleBluetoothError(error);
447
+ } finally {
448
+ this.acquiringDevices.delete(uuid);
439
449
  }
440
450
  }
441
451
 
@@ -456,15 +466,31 @@ export default class ElectronBleTransport {
456
466
  }
457
467
 
458
468
  // Hard teardown, error paths only: a link presumed dead must not be reused.
459
- 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;
460
483
  try {
461
- if (this.connectedDevices.has(id)) {
462
- if (window.desktopApi?.nobleBle) {
463
- await window.desktopApi.nobleBle.unsubscribe(id);
464
- 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
+ });
465
490
  }
466
- this.cleanupDeviceState(id);
491
+ await invokeNobleBle(nobleBle.disconnect(id));
467
492
  }
493
+ this.cleanupDeviceState(id);
468
494
  } catch (error) {
469
495
  this.Log?.error('[Electron BLE] release failed:', error);
470
496
  this.cleanupDeviceState(id);
@@ -490,19 +516,16 @@ export default class ElectronBleTransport {
490
516
  }
491
517
  return;
492
518
  }
493
- await release(id, keepSession);
519
+ await invokeNobleBle(release(id, keepSession));
494
520
  } catch (error) {
495
521
  this.Log?.error('[Electron BLE] logical release failed:', error);
496
522
  this.cleanupDeviceState(id);
497
523
  }
498
524
  }
499
525
 
500
- private createProtocolMismatchError(expected: ProtocolType, uuid: string) {
501
- // A generic Ping miss is not a bond failure. Only a later miss after this
502
- // endpoint already answered V2, or a native encryption/pairing error, is.
503
- const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
526
+ private createProtocolMismatchError(expected: ProtocolType) {
504
527
  return ERRORS.TypedError(
505
- isStaleV2Bond ? HardwareErrorCode.BleDeviceBondError : HardwareErrorCode.RuntimeError,
528
+ HardwareErrorCode.RuntimeError,
506
529
  `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
507
530
  );
508
531
  }
@@ -542,13 +565,12 @@ export default class ElectronBleTransport {
542
565
  }
543
566
 
544
567
  if (expectedProtocol === 'V2') {
545
- if (await this.probeProtocolV2(uuid)) {
568
+ if (await this.probeProtocolV2(uuid, expectedProtocol)) {
546
569
  this.deviceProtocol.set(uuid, 'V2');
547
- this.confirmedProtocolV2.add(uuid);
548
570
  this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
549
571
  return 'V2';
550
572
  }
551
- throw this.createProtocolMismatchError(expectedProtocol, uuid);
573
+ throw this.createProtocolMismatchError(expectedProtocol);
552
574
  }
553
575
 
554
576
  // Protocol must be actively probed after connection. Name, PID, and descriptors only
@@ -567,9 +589,6 @@ export default class ElectronBleTransport {
567
589
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
568
590
  if (detected) {
569
591
  this.deviceProtocol.set(uuid, protocol);
570
- if (protocol === 'V2') {
571
- this.confirmedProtocolV2.add(uuid);
572
- }
573
592
  this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
574
593
  return protocol;
575
594
  }
@@ -640,7 +659,7 @@ export default class ElectronBleTransport {
640
659
  }
641
660
  }
642
661
 
643
- private async probeProtocolV2(uuid: string) {
662
+ private async probeProtocolV2(uuid: string, expectedProtocol?: ProtocolType) {
644
663
  if (!this._messages || !this._messagesV2) {
645
664
  return false;
646
665
  }
@@ -656,8 +675,11 @@ export default class ElectronBleTransport {
656
675
  this.v2Assemblers.get(uuid)?.reset();
657
676
  this.resetProtocolV2Frames(uuid);
658
677
  },
678
+ // A declared V2 protocol needs no fallback; preserve the actual link failure.
659
679
  shouldRethrow: error =>
660
- isBleStaleBondHardwareError(error) || isProtocolV2LinkDisabledError(error),
680
+ expectedProtocol === 'V2' ||
681
+ isBleStaleBondHardwareError(error) ||
682
+ isProtocolV2LinkDisabledError(error),
661
683
  });
662
684
  if (!detected) {
663
685
  this.clearProbeProtocol(uuid, 'V2');
@@ -672,18 +694,15 @@ export default class ElectronBleTransport {
672
694
  }
673
695
 
674
696
  try {
675
- await nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
697
+ await invokeNobleBle(nobleBle.write(uuid, hexData, { pacingDelayMs: 0 }));
676
698
  } catch (error) {
677
- const staleBondError = this.toStaleBondError(error);
678
- if (staleBondError) {
679
- throw staleBondError;
680
- }
681
- throw error;
699
+ this.handleBluetoothError(error);
682
700
  }
683
701
  }
684
702
 
685
703
  private async refreshBlePacketCapacity(uuid: string): Promise<void> {
686
- const device = await window.desktopApi?.nobleBle?.getDevice(uuid);
704
+ const nobleBle = window.desktopApi?.nobleBle;
705
+ const device = nobleBle ? await invokeNobleBle(nobleBle.getDevice(uuid)) : undefined;
687
706
  this.updateBlePacketCapacity(uuid, device?.mtu);
688
707
  }
689
708
 
@@ -941,7 +960,7 @@ export default class ElectronBleTransport {
941
960
  if (hexString.length === 0) {
942
961
  throw new Error(`Buffer ${i + 1} is empty`);
943
962
  }
944
- await window.desktopApi.nobleBle.write(uuid, hexString);
963
+ await invokeNobleBle(window.desktopApi.nobleBle.write(uuid, hexString));
945
964
  }
946
965
 
947
966
  const response = await Promise.race([
@@ -976,10 +995,10 @@ export default class ElectronBleTransport {
976
995
  this.notificationCleanups.delete(uuid);
977
996
  this.notificationTokens.delete(uuid);
978
997
  if (!isProbeTimeout) {
979
- await this.releaseNative(uuid);
998
+ await this.releaseNative(uuid, { forceNative: true });
980
999
  }
981
1000
  }
982
- throw e;
1001
+ throw this.normalizeBluetoothError(e);
983
1002
  } finally {
984
1003
  if (timeout) clearTimeout(timeout);
985
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);