@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 +1 -1
- package/__tests__/electron-ble-transport.test.ts +198 -5
- package/__tests__/webusb-protocol-cache.test.ts +33 -0
- package/dist/electron-ble-transport.d.ts +2 -2
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +85 -79
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/electron-ble-transport.ts +103 -83
- package/src/webusb.ts +8 -4
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 [
|
|
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('
|
|
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(
|
|
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.
|
|
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('
|
|
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.
|
|
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
|
|
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":";
|
|
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
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
896
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
|
|
901
897
|
}
|
|
902
898
|
if (error.code === hdShared.HardwareErrorCode.BleUnsupported) {
|
|
903
|
-
|
|
899
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
|
|
904
900
|
}
|
|
905
901
|
if (error.code === hdShared.HardwareErrorCode.BlePermissionError) {
|
|
906
|
-
|
|
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
|
-
|
|
910
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
|
|
915
911
|
}
|
|
916
912
|
if (errorMessage.includes(unsupportedMessage) || errorMessage.includes('unsupported')) {
|
|
917
|
-
|
|
913
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
|
|
918
914
|
}
|
|
919
915
|
if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
|
|
920
|
-
|
|
916
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
|
|
921
917
|
}
|
|
922
918
|
}
|
|
923
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
yield
|
|
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
|
-
|
|
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
|
|
1165
|
-
|
|
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
|
|
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 =>
|
|
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
|
-
|
|
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
|
|
1318
|
+
var _a;
|
|
1314
1319
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1315
|
-
const
|
|
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)
|
package/dist/webusb.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
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
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.2
|
|
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
|
|
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": "
|
|
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
|
|
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
|
-
|
|
198
|
+
return ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
|
|
197
199
|
}
|
|
198
200
|
if (error.code === HardwareErrorCode.BleUnsupported) {
|
|
199
|
-
|
|
201
|
+
return ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
|
|
200
202
|
}
|
|
201
203
|
if (error.code === HardwareErrorCode.BlePermissionError) {
|
|
202
|
-
|
|
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
|
-
|
|
213
|
+
return ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
|
|
212
214
|
}
|
|
213
215
|
if (errorMessage.includes(unsupportedMessage) || errorMessage.includes('unsupported')) {
|
|
214
|
-
|
|
216
|
+
return ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
|
|
215
217
|
}
|
|
216
218
|
if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
|
|
217
|
-
|
|
219
|
+
return ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
|
|
218
220
|
}
|
|
219
221
|
}
|
|
220
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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);
|