@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 +1 -1
- package/__tests__/electron-ble-transport.test.ts +176 -14
- 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 -80
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/electron-ble-transport.ts +103 -84
- 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,21 +665,40 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
568
665
|
expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
|
|
569
666
|
});
|
|
570
667
|
|
|
571
|
-
test('
|
|
572
|
-
const device = { id: 'reset-
|
|
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
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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('
|
|
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.
|
|
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
|
|
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":";AA0BA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAA4B,MAAM,iCAAiC,CAAC;AAC5F,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EAEZ,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AAIvC,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,UAAU,CAAC,EAAE,UAAU,CAAC;KACzB;CACF;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,gBAAgB,CAAC,EAAE,YAAY,CAAC;IAChC,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAiDF,MAAM,CAAC,OAAO,OAAO,oBAAoB;IACvC,OAAO,CAAC,SAAS,CAA0D;IAE3E,OAAO,CAAC,WAAW,CAA0D;IAE7E,OAAO,CAAC,6BAA6B,CAAqB;IAE1D,IAAI,SAA0B;IAE9B,UAAU,UAAS;IAEnB,UAAU,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,GAAG,IAAI,CAAQ;IAExD,OAAO,CAAC,kBAAkB,CAAuB;IAEjD,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAEvB,OAAO,CAAC,gBAAgB,CAA0B;IASlD,OAAO,CAAC,gBAAgB,CAA0B;IAElD,OAAO,CAAC,cAAc,CAAwC;IAE9D,OAAO,CAAC,mBAAmB,CAAwC;IAEnE,OAAO,CAAC,UAAU,CAAkC;IAEpD,OAAO,CAAC,sBAAsB,CAAkC;IAEhE,OAAO,CAAC,SAAS,CAAsE;IAEvF,OAAO,CAAC,YAAY,CAAoD;IAExE,OAAO,CAAC,aAAa,CAAwC;IAE7D,OAAO,CAAC,eAAe,CAAgD;IAEvE,OAAO,CAAC,eAAe,CAmBpB;IAGH,OAAO,CAAC,oBAAoB,CAAS;IAErC,OAAO,CAAC,oBAAoB,CAAsC;IAElE,OAAO,CAAC,WAAW,CAAsC;IAUzD,OAAO,CAAC,qBAAqB,CAAC,CAAa;IAE3C,OAAO,CAAC,kBAAkB,CAAkC;IAE5D,OAAO,CAAC,qBAAqB,CAAK;IAElC,OAAO,CAAC,uBAAuB;IAsC/B,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,kBAAkB;IAiC1B,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,YAAY;IAqBxC,OAAO,CAAC,wBAAwB;IAgChC,SAAS,CAAC,UAAU,EAAE,GAAG;IAKzB,mBAAmB,CAAC,UAAU,EAAE,GAAG;IAgB7B,MAAM;IAIN,SAAS,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAiBxC,OAAO,CAAC,KAAK,EAAE,eAAe;;;;;;;;;;;IA+F9B,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,OAAO;IAY7D,UAAU,CAAC,EAAE,EAAE,MAAM;YAgBb,aAAa;YAsBb,cAAc;IAwB5B,OAAO,CAAC,2BAA2B;IAOnC,OAAO,CAAC,4BAA4B;IAOpC,OAAO,CAAC,kBAAkB;YAMZ,cAAc;IAuD5B,OAAO,CAAC,8BAA8B;YAgBxB,iCAAiC;YAwBjC,eAAe;YAqBf,eAAe;YA4Bf,SAAS;YAaT,wBAAwB;IAMtC,OAAO,CAAC,uBAAuB;IAc/B,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,oBAAoB;IAqB5B,OAAO,CAAC,kBAAkB;IA+B1B,OAAO,CAAC,iCAAiC;IAazC,OAAO,CAAC,4BAA4B;IAkBpC,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,sBAAsB;YAShB,mBAAmB;IAiBjC,OAAO,CAAC,4BAA4B;IAqB9B,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,oBAAoB;IAuB1B,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;YAatD,cAAc;YAqFd,cAAc;IA0B5B,OAAO,CAAC,uBAAuB;IAyC/B,OAAO,CAAC,6BAA6B;IA4CrC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;CAGxD"}
|
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,40 +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
|
-
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
|
-
|
|
896
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
|
|
902
897
|
}
|
|
903
898
|
if (error.code === hdShared.HardwareErrorCode.BleUnsupported) {
|
|
904
|
-
|
|
899
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
|
|
905
900
|
}
|
|
906
901
|
if (error.code === hdShared.HardwareErrorCode.BlePermissionError) {
|
|
907
|
-
|
|
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
|
-
|
|
910
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
|
|
916
911
|
}
|
|
917
912
|
if (errorMessage.includes(unsupportedMessage) || errorMessage.includes('unsupported')) {
|
|
918
|
-
|
|
913
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
|
|
919
914
|
}
|
|
920
915
|
if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
|
|
921
|
-
|
|
916
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
|
|
922
917
|
}
|
|
923
918
|
}
|
|
924
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
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
|
+
});
|
|
1131
1141
|
}
|
|
1132
|
-
|
|
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
|
|
1166
|
-
|
|
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
|
|
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 =>
|
|
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
|
-
|
|
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
|
|
1318
|
+
var _a;
|
|
1315
1319
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1316
|
-
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;
|
|
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)
|
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-alpha.
|
|
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.
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.2-alpha.
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
-
|
|
198
|
+
return ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
|
|
198
199
|
}
|
|
199
200
|
if (error.code === HardwareErrorCode.BleUnsupported) {
|
|
200
|
-
|
|
201
|
+
return ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
|
|
201
202
|
}
|
|
202
203
|
if (error.code === HardwareErrorCode.BlePermissionError) {
|
|
203
|
-
|
|
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
|
-
|
|
213
|
+
return ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
|
|
213
214
|
}
|
|
214
215
|
if (errorMessage.includes(unsupportedMessage) || errorMessage.includes('unsupported')) {
|
|
215
|
-
|
|
216
|
+
return ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
|
|
216
217
|
}
|
|
217
218
|
if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
|
|
218
|
-
|
|
219
|
+
return ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
|
|
219
220
|
}
|
|
220
221
|
}
|
|
221
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
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
|
+
});
|
|
465
490
|
}
|
|
466
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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);
|