@onekeyfe/hd-transport-web-device 1.2.2-alpha.100 → 1.2.2-alpha.102
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/__tests__/electron-ble-transport.test.ts +184 -11
- package/__tests__/webusb-protocol-cache.test.ts +32 -0
- package/__tests__/webusb-protocol-v2-timeout.test.ts +8 -1
- package/dist/electron-ble-transport.d.ts +2 -0
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +122 -43
- package/dist/webusb.d.ts +1 -0
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/electron-ble-transport.ts +112 -33
- package/src/webusb.ts +37 -5
|
@@ -73,8 +73,26 @@ const protocolV2Schema = {
|
|
|
73
73
|
},
|
|
74
74
|
},
|
|
75
75
|
},
|
|
76
|
+
Failure: {
|
|
77
|
+
fields: {
|
|
78
|
+
code: {
|
|
79
|
+
type: 'FailureType',
|
|
80
|
+
id: 1,
|
|
81
|
+
},
|
|
82
|
+
message: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
id: 2,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
FailureType: {
|
|
89
|
+
values: {
|
|
90
|
+
Failure_ProcessError: 5,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
76
93
|
MessageType: {
|
|
77
94
|
values: {
|
|
95
|
+
MessageType_Failure: 3,
|
|
78
96
|
MessageType_ProtocolInfoRequest: 60200,
|
|
79
97
|
MessageType_ProtocolInfo: 60201,
|
|
80
98
|
MessageType_Ping: 60206,
|
|
@@ -164,6 +182,40 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
164
182
|
jest.clearAllMocks();
|
|
165
183
|
});
|
|
166
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('sends a native disconnect even before acquire marks the device connected', async () => {
|
|
209
|
+
const device = { id: 'pending-connect-id', name: 'OneKey Pro 2' };
|
|
210
|
+
const nobleBle = createNobleBle(device);
|
|
211
|
+
const bleTransport = configureTransport(nobleBle) as any;
|
|
212
|
+
|
|
213
|
+
await bleTransport.releaseNative(device.id);
|
|
214
|
+
|
|
215
|
+
expect(nobleBle.unsubscribe).not.toHaveBeenCalled();
|
|
216
|
+
expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
|
|
217
|
+
});
|
|
218
|
+
|
|
167
219
|
test('keeps raw BLE lifecycle payloads off the public device event channel', async () => {
|
|
168
220
|
const device = { id: 'lifecycle-pro2-id', name: 'OneKey Pro 2' };
|
|
169
221
|
const nobleBle = createNobleBle(device);
|
|
@@ -327,7 +379,7 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
327
379
|
}
|
|
328
380
|
});
|
|
329
381
|
|
|
330
|
-
test('reconnects Protocol V1
|
|
382
|
+
test('reconnects a declared Protocol V1 device without probing again', async () => {
|
|
331
383
|
const device = { id: 'classic-id', name: 'OneKey Classic' };
|
|
332
384
|
const nobleBle = createNobleBle(device);
|
|
333
385
|
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
@@ -360,7 +412,10 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
360
412
|
uuid: device.id,
|
|
361
413
|
})
|
|
362
414
|
);
|
|
363
|
-
|
|
415
|
+
// The first acquire probes because the protocol is unknown; the second
|
|
416
|
+
// declares V1 and must send nothing at all, leaving the first frame on
|
|
417
|
+
// the link to Core — which is what carries the wallet session.
|
|
418
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(1);
|
|
364
419
|
expect(nobleBle.write.mock.calls.every(([, hex]) => /^3f23230037/.test(hex))).toBe(true);
|
|
365
420
|
expect(protocolV2Writer).not.toHaveBeenCalled();
|
|
366
421
|
} finally {
|
|
@@ -373,19 +428,13 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
373
428
|
const nobleBle = createNobleBle(device);
|
|
374
429
|
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
375
430
|
const v1ResponseHex = '3f23230002000000040a026f6b';
|
|
376
|
-
let writeCount = 0;
|
|
377
|
-
|
|
378
431
|
nobleBle.onNotification.mockImplementation(handler => {
|
|
379
432
|
notificationHandler = handler;
|
|
380
433
|
return jest.fn();
|
|
381
434
|
});
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
|
|
386
|
-
}
|
|
387
|
-
return Promise.resolve();
|
|
388
|
-
});
|
|
435
|
+
// A declared V1 acquire writes nothing, so every write here belongs to the
|
|
436
|
+
// call under test and none of them is answered.
|
|
437
|
+
nobleBle.write.mockImplementation(() => Promise.resolve());
|
|
389
438
|
const bleTransport = configureTransport(nobleBle);
|
|
390
439
|
|
|
391
440
|
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V1' });
|
|
@@ -464,6 +513,41 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
464
513
|
expect(transport.getProtocolType(device.id)).toBeUndefined();
|
|
465
514
|
});
|
|
466
515
|
|
|
516
|
+
test('surfaces Protocol V2 link disabled while the initial V1 probe is active', async () => {
|
|
517
|
+
const device = { id: 'usb-priority-pro2-id', name: 'OneKey Pro 2' };
|
|
518
|
+
const nobleBle = createNobleBle(device);
|
|
519
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
520
|
+
|
|
521
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
522
|
+
notificationHandler = handler;
|
|
523
|
+
return jest.fn();
|
|
524
|
+
});
|
|
525
|
+
nobleBle.write.mockImplementation(() => {
|
|
526
|
+
const response = ProtocolV2.encodeFrame(
|
|
527
|
+
schemas,
|
|
528
|
+
'Failure',
|
|
529
|
+
{ code: 5, message: 'link disabled' },
|
|
530
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
531
|
+
);
|
|
532
|
+
const splitAt = 4;
|
|
533
|
+
setTimeout(() => {
|
|
534
|
+
notificationHandler?.(device.id, bytesToHex(response.subarray(0, splitAt)));
|
|
535
|
+
notificationHandler?.(device.id, bytesToHex(response.subarray(splitAt)));
|
|
536
|
+
}, 0);
|
|
537
|
+
return Promise.resolve();
|
|
538
|
+
});
|
|
539
|
+
const transport = configureTransport(nobleBle);
|
|
540
|
+
|
|
541
|
+
await expect(transport.acquire({ uuid: device.id })).rejects.toMatchObject({
|
|
542
|
+
name: 'ProtocolV2LinkDisabledError',
|
|
543
|
+
failureCode: 'Failure_ProcessError',
|
|
544
|
+
firmwareMessage: 'link disabled',
|
|
545
|
+
});
|
|
546
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(1);
|
|
547
|
+
expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
|
|
548
|
+
expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
|
|
549
|
+
});
|
|
550
|
+
|
|
467
551
|
test('keeps a first expected Protocol V2 probe miss retryable', async () => {
|
|
468
552
|
const device = { id: 'first-v2-id', name: 'OneKey Pro 2' };
|
|
469
553
|
const nobleBle = createNobleBle(device);
|
|
@@ -500,6 +584,95 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
500
584
|
expect(transport.getProtocolType(device.id)).toBeUndefined();
|
|
501
585
|
});
|
|
502
586
|
|
|
587
|
+
test('fails Protocol V2 acquire immediately when subscribe reports insufficient encryption', async () => {
|
|
588
|
+
const device = { id: 'stale-bond-pro2-id', name: 'OneKey Pro 2' };
|
|
589
|
+
const nobleBle = createNobleBle(device);
|
|
590
|
+
nobleBle.subscribe.mockRejectedValue({
|
|
591
|
+
name: 'HardwareError',
|
|
592
|
+
message: 'Bluetooth pairing information is no longer valid',
|
|
593
|
+
errorCode: HardwareErrorCode.BleBondInvalid,
|
|
594
|
+
params: { nativeErrorMessage: 'Encryption is insufficient' },
|
|
595
|
+
});
|
|
596
|
+
const transport = configureTransport(nobleBle);
|
|
597
|
+
const probe = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
598
|
+
|
|
599
|
+
await expect(
|
|
600
|
+
transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
|
|
601
|
+
).rejects.toMatchObject({
|
|
602
|
+
errorCode: HardwareErrorCode.BleBondInvalid,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
expect(probe).not.toHaveBeenCalled();
|
|
606
|
+
expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
|
|
607
|
+
expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test('rehydrates a structured stale-bond error before the protocol is known', async () => {
|
|
611
|
+
const device = { id: 'reset-unknown-protocol-id', name: 'OneKey Pro 2' };
|
|
612
|
+
const nobleBle = createNobleBle(device);
|
|
613
|
+
nobleBle.connect.mockRejectedValue({
|
|
614
|
+
name: 'HardwareError',
|
|
615
|
+
message: 'Bluetooth pairing information is no longer valid',
|
|
616
|
+
errorCode: HardwareErrorCode.BleBondInvalid,
|
|
617
|
+
params: { nativeErrorMessage: 'CBErrorDomain:14 native message' },
|
|
618
|
+
});
|
|
619
|
+
const transport = configureTransport(nobleBle);
|
|
620
|
+
|
|
621
|
+
await expect(transport.acquire({ uuid: device.id })).rejects.toMatchObject({
|
|
622
|
+
name: 'HardwareError',
|
|
623
|
+
errorCode: HardwareErrorCode.BleBondInvalid,
|
|
624
|
+
params: { nativeErrorMessage: 'CBErrorDomain:14 native message' },
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test('does not infer hardware errors from native error text in the renderer', async () => {
|
|
629
|
+
const device = { id: 'reset-pro2-localized-id', name: 'OneKey Pro 2' };
|
|
630
|
+
const nobleBle = createNobleBle(device);
|
|
631
|
+
nobleBle.connect.mockRejectedValue(new Error('CBATTErrorDomain:14 localized native message'));
|
|
632
|
+
const transport = configureTransport(nobleBle);
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
636
|
+
throw new Error('Expected acquire to fail');
|
|
637
|
+
} catch (error) {
|
|
638
|
+
expect(error).toBeInstanceOf(Error);
|
|
639
|
+
expect((error as Error).message).toBe('CBATTErrorDomain:14 localized native message');
|
|
640
|
+
expect((error as { errorCode?: unknown }).errorCode).toBeUndefined();
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test('does not classify a generic macOS connection failure as a stale bond', async () => {
|
|
645
|
+
const device = { id: 'offline-pro2-macos-id', name: 'OneKey Pro 2' };
|
|
646
|
+
const nobleBle = createNobleBle(device);
|
|
647
|
+
nobleBle.connect.mockRejectedValue(new Error('connection failed'));
|
|
648
|
+
const transport = configureTransport(nobleBle);
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
652
|
+
throw new Error('Expected acquire to fail');
|
|
653
|
+
} catch (error) {
|
|
654
|
+
expect(error).toBeInstanceOf(Error);
|
|
655
|
+
expect((error as Error).message).toBe('connection failed');
|
|
656
|
+
expect((error as { errorCode?: unknown }).errorCode).toBeUndefined();
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('keeps stale-bond subscribe mapping out of Protocol V1 acquire', async () => {
|
|
661
|
+
const device = { id: 'classic-v1-id', name: 'OneKey Classic' };
|
|
662
|
+
const nobleBle = createNobleBle(device);
|
|
663
|
+
nobleBle.subscribe.mockRejectedValue(new Error('Encryption is insufficient'));
|
|
664
|
+
const transport = configureTransport(nobleBle);
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
await transport.acquire({ uuid: device.id, expectedProtocol: 'V1' });
|
|
668
|
+
throw new Error('Expected Protocol V1 acquire to fail');
|
|
669
|
+
} catch (error) {
|
|
670
|
+
expect(error).toBeInstanceOf(Error);
|
|
671
|
+
expect((error as Error).message).toBe('Encryption is insufficient');
|
|
672
|
+
expect((error as { errorCode?: unknown }).errorCode).toBeUndefined();
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
503
676
|
test('reports a stale bond when a previously confirmed Protocol V2 device stops responding', async () => {
|
|
504
677
|
const device = { id: 'reset-pro2-id', name: 'OneKey Pro 2' };
|
|
505
678
|
const nobleBle = createNobleBle(device);
|
|
@@ -69,6 +69,38 @@ describe('WebUsbTransport protocol probe cache', () => {
|
|
|
69
69
|
expect(webusb.detectProtocol).toHaveBeenCalledWith(path, 'V1', undefined);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
test('acquire reuses a previously confirmed protocol during explicit no-probe recovery', async () => {
|
|
73
|
+
const webusb = buildAcquirableTransport();
|
|
74
|
+
const path = 'pro-webusb';
|
|
75
|
+
webusb.deviceProtocol.set(path, 'V1');
|
|
76
|
+
webusb.confirmedDeviceProtocols.set(path, 'V2');
|
|
77
|
+
webusb.markProtocolStale(path);
|
|
78
|
+
webusb.detectProtocol = jest.fn();
|
|
79
|
+
|
|
80
|
+
await expect(
|
|
81
|
+
webusb.acquire({ path, expectedProtocol: 'V2', skipProtocolProbe: true })
|
|
82
|
+
).resolves.toBe(path);
|
|
83
|
+
|
|
84
|
+
expect(webusb.detectProtocol).not.toHaveBeenCalled();
|
|
85
|
+
expect(webusb.deviceProtocol.get(path)).toBe('V2');
|
|
86
|
+
expect(webusb.staleProtocolPaths.has(path)).toBe(false);
|
|
87
|
+
expect(webusb.acquiredPaths.has(path)).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('acquire rejects no-probe recovery without a previously confirmed protocol', async () => {
|
|
91
|
+
const webusb = buildAcquirableTransport();
|
|
92
|
+
const path = 'pro-webusb';
|
|
93
|
+
webusb.detectProtocol = jest.fn();
|
|
94
|
+
|
|
95
|
+
await expect(
|
|
96
|
+
webusb.acquire({ path, expectedProtocol: 'V2', skipProtocolProbe: true })
|
|
97
|
+
).rejects.toThrow('previously confirmed protocol');
|
|
98
|
+
|
|
99
|
+
expect(webusb.detectProtocol).not.toHaveBeenCalled();
|
|
100
|
+
expect(webusb.deviceProtocol.has(path)).toBe(false);
|
|
101
|
+
expect(webusb.acquiredPaths.has(path)).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
72
104
|
test('forced protocol detection bypasses a valid cache and probes on the wire', async () => {
|
|
73
105
|
const webusb = buildAcquirableTransport();
|
|
74
106
|
const path = 'pro-webusb';
|
|
@@ -387,6 +387,13 @@ describe('WebUsbTransport Protocol V2 timeout recovery', () => {
|
|
|
387
387
|
webusb.callProtocolV2(path, 'Ping', { message: 'short' }, { timeoutMs: 25 }),
|
|
388
388
|
]);
|
|
389
389
|
|
|
390
|
-
|
|
390
|
+
// Each call keeps its own budget instead of inheriting the other's. The
|
|
391
|
+
// queued one is a deadline, so it arrives with whatever the first call left
|
|
392
|
+
// of its 25ms — asserting the exact remainder makes this fail on a loaded
|
|
393
|
+
// machine, which is timing, not behaviour.
|
|
394
|
+
expect(readTimeouts).toHaveLength(2);
|
|
395
|
+
expect(readTimeouts[0]).toBe(1_000);
|
|
396
|
+
expect(readTimeouts[1]).toBeGreaterThan(0);
|
|
397
|
+
expect(readTimeouts[1]).toBeLessThanOrEqual(25);
|
|
391
398
|
});
|
|
392
399
|
});
|
|
@@ -41,6 +41,7 @@ export default class ElectronBleTransport {
|
|
|
41
41
|
private hostDisconnectCleanup?;
|
|
42
42
|
private notificationTokens;
|
|
43
43
|
private nextNotificationToken;
|
|
44
|
+
private normalizeBluetoothError;
|
|
44
45
|
private handleBluetoothError;
|
|
45
46
|
private cleanupDeviceState;
|
|
46
47
|
init(logger: any, emitter?: EventEmitter): void;
|
|
@@ -78,6 +79,7 @@ export default class ElectronBleTransport {
|
|
|
78
79
|
private createMtuSubscription;
|
|
79
80
|
private writeProtocolV2Frame;
|
|
80
81
|
private handleNotification;
|
|
82
|
+
private readProtocolV2LinkDisabledFailure;
|
|
81
83
|
private handleProtocolV2Notification;
|
|
82
84
|
private getProtocolV2FrameQueue;
|
|
83
85
|
private resolveProtocolV2Frame;
|
|
@@ -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
|
@@ -19,6 +19,8 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
19
19
|
private protocolV2SchemaSource;
|
|
20
20
|
/** Per-path protocol type detected by active wire-level probe. */
|
|
21
21
|
private deviceProtocol;
|
|
22
|
+
/** Protocols previously confirmed by an active response for this transport instance. */
|
|
23
|
+
private confirmedDeviceProtocols;
|
|
22
24
|
private deviceProtocolHints;
|
|
23
25
|
/**
|
|
24
26
|
* Paths whose cached protocol must be re-probed on the next acquire (a USB
|
|
@@ -240,6 +242,7 @@ declare class ElectronBleTransport {
|
|
|
240
242
|
private hostDisconnectCleanup?;
|
|
241
243
|
private notificationTokens;
|
|
242
244
|
private nextNotificationToken;
|
|
245
|
+
private normalizeBluetoothError;
|
|
243
246
|
private handleBluetoothError;
|
|
244
247
|
private cleanupDeviceState;
|
|
245
248
|
init(logger: any, emitter?: EventEmitter): void;
|
|
@@ -282,6 +285,7 @@ declare class ElectronBleTransport {
|
|
|
282
285
|
private createMtuSubscription;
|
|
283
286
|
private writeProtocolV2Frame;
|
|
284
287
|
private handleNotification;
|
|
288
|
+
private readProtocolV2LinkDisabledFailure;
|
|
285
289
|
private handleProtocolV2Notification;
|
|
286
290
|
private getProtocolV2FrameQueue;
|
|
287
291
|
private resolveProtocolV2Frame;
|
package/dist/index.js
CHANGED
|
@@ -79,6 +79,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
79
79
|
logPrefix: 'ProtocolV2 WebUSB',
|
|
80
80
|
});
|
|
81
81
|
this.deviceProtocol = new Map();
|
|
82
|
+
this.confirmedDeviceProtocols = new Map();
|
|
82
83
|
this.deviceProtocolHints = new Map();
|
|
83
84
|
this.staleProtocolPaths = new Set();
|
|
84
85
|
this.acquiredPaths = new Set();
|
|
@@ -182,7 +183,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
182
183
|
});
|
|
183
184
|
}
|
|
184
185
|
acquire(input) {
|
|
185
|
-
var _a, _b, _c, _d, _e, _f;
|
|
186
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
186
187
|
return __awaiter(this, void 0, void 0, function* () {
|
|
187
188
|
if (!input.path)
|
|
188
189
|
return;
|
|
@@ -190,35 +191,53 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
190
191
|
yield this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
191
192
|
yield this.closeOpenDevice(input.path);
|
|
192
193
|
yield this.connect((_a = input.path) !== null && _a !== void 0 ? _a : '', true);
|
|
193
|
-
if (input.
|
|
194
|
+
if (input.skipProtocolProbe) {
|
|
195
|
+
if (!input.expectedProtocol) {
|
|
196
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'skipProtocolProbe requires an expected protocol');
|
|
197
|
+
}
|
|
198
|
+
if (this.confirmedDeviceProtocols.get(input.path) !== input.expectedProtocol) {
|
|
199
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'skipProtocolProbe requires a previously confirmed protocol for this WebUSB endpoint');
|
|
200
|
+
}
|
|
201
|
+
this.staleProtocolPaths.delete(input.path);
|
|
202
|
+
this.deviceProtocol.set(input.path, input.expectedProtocol);
|
|
203
|
+
const currentDevice = (_b = this.deviceList.find(d => d.path === input.path)) === null || _b === void 0 ? void 0 : _b.device;
|
|
204
|
+
if (currentDevice) {
|
|
205
|
+
this.probedDeviceObjects.set(input.path, currentDevice);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (input.forceProtocolDetection) {
|
|
194
209
|
this.staleProtocolPaths.delete(input.path);
|
|
195
210
|
this.deviceProtocol.delete(input.path);
|
|
196
211
|
this.deviceProtocolHints.delete(input.path);
|
|
197
212
|
}
|
|
198
|
-
if (this.staleProtocolPaths.has(input.path)) {
|
|
213
|
+
if (!input.skipProtocolProbe && this.staleProtocolPaths.has(input.path)) {
|
|
199
214
|
this.staleProtocolPaths.delete(input.path);
|
|
200
215
|
this.deviceProtocol.delete(input.path);
|
|
201
216
|
}
|
|
202
|
-
if (this.deviceProtocol.has(input.path)) {
|
|
203
|
-
const currentDevice = (
|
|
217
|
+
if (!input.skipProtocolProbe && this.deviceProtocol.has(input.path)) {
|
|
218
|
+
const currentDevice = (_c = this.deviceList.find(d => d.path === input.path)) === null || _c === void 0 ? void 0 : _c.device;
|
|
204
219
|
if (!currentDevice || currentDevice !== this.probedDeviceObjects.get(input.path)) {
|
|
205
220
|
this.deviceProtocol.delete(input.path);
|
|
206
221
|
}
|
|
207
222
|
}
|
|
208
223
|
const cachedProtocol = this.deviceProtocol.get(input.path);
|
|
209
|
-
if (
|
|
224
|
+
if (!input.skipProtocolProbe &&
|
|
225
|
+
cachedProtocol &&
|
|
226
|
+
input.expectedProtocol &&
|
|
227
|
+
cachedProtocol !== input.expectedProtocol) {
|
|
210
228
|
this.deviceProtocol.delete(input.path);
|
|
211
229
|
}
|
|
212
230
|
if (!this.deviceProtocol.has(input.path)) {
|
|
213
|
-
const usbDevice = (
|
|
231
|
+
const usbDevice = (_d = this.deviceList.find(device => device.path === input.path)) === null || _d === void 0 ? void 0 : _d.device;
|
|
214
232
|
const protocolHint = input.expectedProtocol
|
|
215
233
|
? undefined
|
|
216
|
-
: (
|
|
234
|
+
: (_f = (_e = input.protocolHint) !== null && _e !== void 0 ? _e : this.deviceProtocolHints.get(input.path)) !== null && _f !== void 0 ? _f : hdShared.inferProtocolHintFromUsbId(usbDevice === null || usbDevice === void 0 ? void 0 : usbDevice.vendorId, usbDevice === null || usbDevice === void 0 ? void 0 : usbDevice.productId);
|
|
217
235
|
if (protocolHint) {
|
|
218
236
|
this.deviceProtocolHints.set(input.path, protocolHint);
|
|
219
237
|
}
|
|
220
|
-
yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
|
|
221
|
-
|
|
238
|
+
const detectedProtocol = yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
|
|
239
|
+
this.confirmedDeviceProtocols.set(input.path, detectedProtocol);
|
|
240
|
+
const probedDevice = (_g = this.deviceList.find(d => d.path === input.path)) === null || _g === void 0 ? void 0 : _g.device;
|
|
222
241
|
if (probedDevice) {
|
|
223
242
|
this.probedDeviceObjects.set(input.path, probedDevice);
|
|
224
243
|
}
|
|
@@ -804,6 +823,19 @@ function resolveBlePacketCapacity(mtu, maximumPacketCapacity, fallbackPacketCapa
|
|
|
804
823
|
|
|
805
824
|
const { parseConfigure, ProtocolV1, check } = transport__default["default"];
|
|
806
825
|
const toBleDescriptor = (device, protocolType) => (Object.assign({ id: device.id, name: device.name, path: device.id, debug: false, commType: 'electron-ble' }, (protocolType ? { protocolType } : {})));
|
|
826
|
+
const invokeNobleBle = (request) => __awaiter(void 0, void 0, void 0, function* () {
|
|
827
|
+
const response = yield request;
|
|
828
|
+
if (response &&
|
|
829
|
+
typeof response === 'object' &&
|
|
830
|
+
'type' in response &&
|
|
831
|
+
response.type === 'NobleBleIpcError' &&
|
|
832
|
+
'success' in response &&
|
|
833
|
+
response.success === false &&
|
|
834
|
+
'error' in response) {
|
|
835
|
+
return Promise.reject(response.error);
|
|
836
|
+
}
|
|
837
|
+
return response;
|
|
838
|
+
});
|
|
807
839
|
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
808
840
|
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
809
841
|
const BLE_WRITE_DELAY_MS = 5;
|
|
@@ -852,17 +884,20 @@ class ElectronBleTransport {
|
|
|
852
884
|
this.notificationTokens = new Map();
|
|
853
885
|
this.nextNotificationToken = 1;
|
|
854
886
|
}
|
|
855
|
-
|
|
887
|
+
normalizeBluetoothError(error) {
|
|
856
888
|
if (error && typeof error === 'object') {
|
|
889
|
+
if (typeof error.errorCode === 'number') {
|
|
890
|
+
return hdShared.ERRORS.TypedError(error.errorCode, typeof error.message === 'string' ? error.message : undefined, error.params);
|
|
891
|
+
}
|
|
857
892
|
if ('code' in error) {
|
|
858
893
|
if (error.code === hdShared.HardwareErrorCode.BlePoweredOff) {
|
|
859
|
-
|
|
894
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
|
|
860
895
|
}
|
|
861
896
|
if (error.code === hdShared.HardwareErrorCode.BleUnsupported) {
|
|
862
|
-
|
|
897
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
|
|
863
898
|
}
|
|
864
899
|
if (error.code === hdShared.HardwareErrorCode.BlePermissionError) {
|
|
865
|
-
|
|
900
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
|
|
866
901
|
}
|
|
867
902
|
}
|
|
868
903
|
const errorMessage = error.message || String(error);
|
|
@@ -870,16 +905,19 @@ class ElectronBleTransport {
|
|
|
870
905
|
const unsupportedMessage = hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BleUnsupported];
|
|
871
906
|
const permissionMessage = hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BlePermissionError];
|
|
872
907
|
if (errorMessage.includes(poweredOffMessage) || errorMessage.includes('poweredOff')) {
|
|
873
|
-
|
|
908
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePoweredOff);
|
|
874
909
|
}
|
|
875
910
|
if (errorMessage.includes(unsupportedMessage) || errorMessage.includes('unsupported')) {
|
|
876
|
-
|
|
911
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleUnsupported);
|
|
877
912
|
}
|
|
878
913
|
if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
|
|
879
|
-
|
|
914
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BlePermissionError);
|
|
880
915
|
}
|
|
881
916
|
}
|
|
882
|
-
|
|
917
|
+
return error;
|
|
918
|
+
}
|
|
919
|
+
handleBluetoothError(error) {
|
|
920
|
+
throw this.normalizeBluetoothError(error);
|
|
883
921
|
}
|
|
884
922
|
cleanupDeviceState(deviceId) {
|
|
885
923
|
this.protocolV2Links
|
|
@@ -966,7 +1004,7 @@ class ElectronBleTransport {
|
|
|
966
1004
|
if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
|
|
967
1005
|
throw new Error('Noble BLE API not available');
|
|
968
1006
|
}
|
|
969
|
-
const devices = yield window.desktopApi.nobleBle.enumerate();
|
|
1007
|
+
const devices = yield invokeNobleBle(window.desktopApi.nobleBle.enumerate());
|
|
970
1008
|
(_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
|
|
971
1009
|
for (const dev of devices) {
|
|
972
1010
|
(_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`);
|
|
@@ -999,7 +1037,7 @@ class ElectronBleTransport {
|
|
|
999
1037
|
if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
|
|
1000
1038
|
throw new Error('Noble BLE API not available');
|
|
1001
1039
|
}
|
|
1002
|
-
const device = yield window.desktopApi.nobleBle.getDevice(uuid);
|
|
1040
|
+
const device = yield invokeNobleBle(window.desktopApi.nobleBle.getDevice(uuid));
|
|
1003
1041
|
if (!device) {
|
|
1004
1042
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${uuid} not found`);
|
|
1005
1043
|
}
|
|
@@ -1010,7 +1048,7 @@ class ElectronBleTransport {
|
|
|
1010
1048
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
1011
1049
|
}
|
|
1012
1050
|
try {
|
|
1013
|
-
yield window.desktopApi.nobleBle.connect(uuid);
|
|
1051
|
+
yield invokeNobleBle(window.desktopApi.nobleBle.connect(uuid));
|
|
1014
1052
|
this.connectedDevices.add(uuid);
|
|
1015
1053
|
}
|
|
1016
1054
|
catch (error) {
|
|
@@ -1022,7 +1060,12 @@ class ElectronBleTransport {
|
|
|
1022
1060
|
}
|
|
1023
1061
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
1024
1062
|
this.v2Assemblers.set(uuid, new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
|
|
1025
|
-
|
|
1063
|
+
try {
|
|
1064
|
+
yield invokeNobleBle(window.desktopApi.nobleBle.subscribe(uuid));
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
this.handleBluetoothError(error);
|
|
1068
|
+
}
|
|
1026
1069
|
yield this.refreshBlePacketCapacity(uuid);
|
|
1027
1070
|
const cleanup = this.createNotificationSubscription(uuid);
|
|
1028
1071
|
this.notificationCleanups.set(uuid, cleanup);
|
|
@@ -1039,16 +1082,22 @@ class ElectronBleTransport {
|
|
|
1039
1082
|
catch (error) {
|
|
1040
1083
|
(_e = this.Log) === null || _e === void 0 ? void 0 : _e.error('[Electron BLE] acquire failed:', error);
|
|
1041
1084
|
try {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1085
|
+
const nobleBle = (_f = window.desktopApi) === null || _f === void 0 ? void 0 : _f.nobleBle;
|
|
1086
|
+
if (nobleBle) {
|
|
1087
|
+
if (this.connectedDevices.has(uuid)) {
|
|
1088
|
+
yield invokeNobleBle(nobleBle.unsubscribe(uuid)).catch(cleanupError => {
|
|
1089
|
+
var _a;
|
|
1090
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] acquire unsubscribe failed:', cleanupError);
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
yield invokeNobleBle(nobleBle.disconnect(uuid));
|
|
1045
1094
|
}
|
|
1046
1095
|
}
|
|
1047
1096
|
catch (cleanupError) {
|
|
1048
1097
|
(_g = this.Log) === null || _g === void 0 ? void 0 : _g.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
|
|
1049
1098
|
}
|
|
1050
1099
|
this.cleanupDeviceState(uuid);
|
|
1051
|
-
|
|
1100
|
+
this.handleBluetoothError(error);
|
|
1052
1101
|
}
|
|
1053
1102
|
});
|
|
1054
1103
|
}
|
|
@@ -1074,13 +1123,17 @@ class ElectronBleTransport {
|
|
|
1074
1123
|
var _a, _b;
|
|
1075
1124
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1076
1125
|
try {
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
yield
|
|
1126
|
+
const nobleBle = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle;
|
|
1127
|
+
if (nobleBle) {
|
|
1128
|
+
if (this.connectedDevices.has(id)) {
|
|
1129
|
+
yield invokeNobleBle(nobleBle.unsubscribe(id)).catch(error => {
|
|
1130
|
+
var _a;
|
|
1131
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] release unsubscribe failed:', error);
|
|
1132
|
+
});
|
|
1081
1133
|
}
|
|
1082
|
-
|
|
1134
|
+
yield invokeNobleBle(nobleBle.disconnect(id));
|
|
1083
1135
|
}
|
|
1136
|
+
this.cleanupDeviceState(id);
|
|
1084
1137
|
}
|
|
1085
1138
|
catch (error) {
|
|
1086
1139
|
(_b = this.Log) === null || _b === void 0 ? void 0 : _b.error('[Electron BLE] release failed:', error);
|
|
@@ -1104,7 +1157,7 @@ class ElectronBleTransport {
|
|
|
1104
1157
|
}
|
|
1105
1158
|
return;
|
|
1106
1159
|
}
|
|
1107
|
-
yield release(id, keepSession);
|
|
1160
|
+
yield invokeNobleBle(release(id, keepSession));
|
|
1108
1161
|
}
|
|
1109
1162
|
catch (error) {
|
|
1110
1163
|
(_d = this.Log) === null || _d === void 0 ? void 0 : _d.error('[Electron BLE] logical release failed:', error);
|
|
@@ -1128,12 +1181,9 @@ class ElectronBleTransport {
|
|
|
1128
1181
|
var _a, _b, _c;
|
|
1129
1182
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1130
1183
|
if (expectedProtocol === 'V1') {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
return 'V1';
|
|
1135
|
-
}
|
|
1136
|
-
throw this.createProtocolMismatchError(expectedProtocol, uuid);
|
|
1184
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
1185
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected, no probe)`);
|
|
1186
|
+
return 'V1';
|
|
1137
1187
|
}
|
|
1138
1188
|
if (expectedProtocol === 'V2') {
|
|
1139
1189
|
if (yield this.probeProtocolV2(uuid)) {
|
|
@@ -1211,6 +1261,9 @@ class ElectronBleTransport {
|
|
|
1211
1261
|
catch (error) {
|
|
1212
1262
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1213
1263
|
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
|
|
1264
|
+
if (transport.isProtocolV2LinkDisabledError(error)) {
|
|
1265
|
+
throw error;
|
|
1266
|
+
}
|
|
1214
1267
|
return false;
|
|
1215
1268
|
}
|
|
1216
1269
|
});
|
|
@@ -1233,6 +1286,7 @@ class ElectronBleTransport {
|
|
|
1233
1286
|
(_a = this.v2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
|
|
1234
1287
|
this.resetProtocolV2Frames(uuid);
|
|
1235
1288
|
},
|
|
1289
|
+
shouldRethrow: error => hdShared.isBleStaleBondHardwareError(error) || transport.isProtocolV2LinkDisabledError(error),
|
|
1236
1290
|
});
|
|
1237
1291
|
if (!detected) {
|
|
1238
1292
|
this.clearProbeProtocol(uuid, 'V2');
|
|
@@ -1247,13 +1301,19 @@ class ElectronBleTransport {
|
|
|
1247
1301
|
if (!nobleBle) {
|
|
1248
1302
|
throw new Error('Noble BLE API not available');
|
|
1249
1303
|
}
|
|
1250
|
-
|
|
1304
|
+
try {
|
|
1305
|
+
yield invokeNobleBle(nobleBle.write(uuid, hexData, { pacingDelayMs: 0 }));
|
|
1306
|
+
}
|
|
1307
|
+
catch (error) {
|
|
1308
|
+
this.handleBluetoothError(error);
|
|
1309
|
+
}
|
|
1251
1310
|
});
|
|
1252
1311
|
}
|
|
1253
1312
|
refreshBlePacketCapacity(uuid) {
|
|
1254
|
-
var _a
|
|
1313
|
+
var _a;
|
|
1255
1314
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1256
|
-
const
|
|
1315
|
+
const nobleBle = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle;
|
|
1316
|
+
const device = nobleBle ? yield invokeNobleBle(nobleBle.getDevice(uuid)) : undefined;
|
|
1257
1317
|
this.updateBlePacketCapacity(uuid, device === null || device === void 0 ? void 0 : device.mtu);
|
|
1258
1318
|
});
|
|
1259
1319
|
}
|
|
@@ -1316,8 +1376,27 @@ class ElectronBleTransport {
|
|
|
1316
1376
|
this.handleProtocolV2Notification(deviceId, hexData);
|
|
1317
1377
|
return;
|
|
1318
1378
|
}
|
|
1379
|
+
const linkDisabledError = this.readProtocolV2LinkDisabledFailure(deviceId, hexData);
|
|
1380
|
+
if (linkDisabledError) {
|
|
1381
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
1382
|
+
this.runPromise.reject(linkDisabledError);
|
|
1383
|
+
}
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1319
1386
|
this.handleProtocolV1Notification(deviceId, hexData);
|
|
1320
1387
|
}
|
|
1388
|
+
readProtocolV2LinkDisabledFailure(deviceId, hexData) {
|
|
1389
|
+
if (!this._messages || !this._messagesV2)
|
|
1390
|
+
return undefined;
|
|
1391
|
+
const assembler = this.v2Assemblers.get(deviceId);
|
|
1392
|
+
if (!assembler)
|
|
1393
|
+
return undefined;
|
|
1394
|
+
return transport.detectProtocolV2LinkDisabledError({
|
|
1395
|
+
schemas: { protocolV1: this._messages, protocolV2: this._messagesV2 },
|
|
1396
|
+
assembler,
|
|
1397
|
+
bytes: transport.hexToBytes(hexData),
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1321
1400
|
handleProtocolV2Notification(deviceId, hexData) {
|
|
1322
1401
|
var _a;
|
|
1323
1402
|
try {
|
|
@@ -1457,7 +1536,7 @@ class ElectronBleTransport {
|
|
|
1457
1536
|
if (hexString.length === 0) {
|
|
1458
1537
|
throw new Error(`Buffer ${i + 1} is empty`);
|
|
1459
1538
|
}
|
|
1460
|
-
yield window.desktopApi.nobleBle.write(uuid, hexString);
|
|
1539
|
+
yield invokeNobleBle(window.desktopApi.nobleBle.write(uuid, hexString));
|
|
1461
1540
|
}
|
|
1462
1541
|
const response = yield Promise.race([
|
|
1463
1542
|
runPromise.promise,
|
|
@@ -1490,7 +1569,7 @@ class ElectronBleTransport {
|
|
|
1490
1569
|
yield this.releaseNative(uuid);
|
|
1491
1570
|
}
|
|
1492
1571
|
}
|
|
1493
|
-
throw e;
|
|
1572
|
+
throw this.normalizeBluetoothError(e);
|
|
1494
1573
|
}
|
|
1495
1574
|
finally {
|
|
1496
1575
|
if (timeout)
|
package/dist/webusb.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
13
13
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
14
14
|
private protocolV2SchemaSource;
|
|
15
15
|
private deviceProtocol;
|
|
16
|
+
private confirmedDeviceProtocols;
|
|
16
17
|
private deviceProtocolHints;
|
|
17
18
|
private staleProtocolPaths;
|
|
18
19
|
private acquiredPaths;
|
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;
|
|
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.102",
|
|
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.102",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.2-alpha.102"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@onekeyfe/hd-transport-electron": "1.2.2-alpha.
|
|
28
|
+
"@onekeyfe/hd-transport-electron": "1.2.2-alpha.102",
|
|
29
29
|
"@types/w3c-web-usb": "^1.0.6",
|
|
30
30
|
"@types/web-bluetooth": "^0.0.17"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "abddc95e03d794d9c6fce7765d59de1f48a684d0"
|
|
33
33
|
}
|
|
@@ -6,7 +6,9 @@ import transport, {
|
|
|
6
6
|
ProtocolV2LinkManager,
|
|
7
7
|
TRANSPORT_EVENT,
|
|
8
8
|
bytesToHex,
|
|
9
|
+
detectProtocolV2LinkDisabledError,
|
|
9
10
|
hexToBytes,
|
|
11
|
+
isProtocolV2LinkDisabledError,
|
|
10
12
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
11
13
|
writeProtocolV2BleFrame,
|
|
12
14
|
} from '@onekeyfe/hd-transport';
|
|
@@ -16,13 +18,14 @@ import {
|
|
|
16
18
|
HardwareErrorCode,
|
|
17
19
|
HardwareErrorCodeMessage,
|
|
18
20
|
createDeferred,
|
|
21
|
+
isBleStaleBondHardwareError,
|
|
19
22
|
isHeaderChunk,
|
|
20
23
|
} from '@onekeyfe/hd-shared';
|
|
21
24
|
|
|
22
25
|
import { resolveBlePacketCapacity } from './ble-packet-capacity';
|
|
23
26
|
|
|
24
27
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
25
|
-
import type { DesktopAPI } from '@onekeyfe/hd-transport-electron';
|
|
28
|
+
import type { DesktopAPI, NobleBleIpcErrorResponse } from '@onekeyfe/hd-transport-electron';
|
|
26
29
|
import type {
|
|
27
30
|
OneKeyDeviceInfo,
|
|
28
31
|
ProtocolType,
|
|
@@ -65,6 +68,22 @@ const toBleDescriptor = (
|
|
|
65
68
|
...(protocolType ? { protocolType } : {}),
|
|
66
69
|
} as OneKeyDeviceInfo);
|
|
67
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
|
+
|
|
68
87
|
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
69
88
|
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
70
89
|
const BLE_WRITE_DELAY_MS = 5;
|
|
@@ -159,17 +178,24 @@ export default class ElectronBleTransport {
|
|
|
159
178
|
|
|
160
179
|
private nextNotificationToken = 1;
|
|
161
180
|
|
|
162
|
-
private
|
|
181
|
+
private normalizeBluetoothError(error: any): any {
|
|
163
182
|
if (error && typeof error === 'object') {
|
|
183
|
+
if (typeof error.errorCode === 'number') {
|
|
184
|
+
return ERRORS.TypedError(
|
|
185
|
+
error.errorCode,
|
|
186
|
+
typeof error.message === 'string' ? error.message : undefined,
|
|
187
|
+
error.params
|
|
188
|
+
);
|
|
189
|
+
}
|
|
164
190
|
if ('code' in error) {
|
|
165
191
|
if (error.code === HardwareErrorCode.BlePoweredOff) {
|
|
166
|
-
|
|
192
|
+
return ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
|
|
167
193
|
}
|
|
168
194
|
if (error.code === HardwareErrorCode.BleUnsupported) {
|
|
169
|
-
|
|
195
|
+
return ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
|
|
170
196
|
}
|
|
171
197
|
if (error.code === HardwareErrorCode.BlePermissionError) {
|
|
172
|
-
|
|
198
|
+
return ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
|
|
173
199
|
}
|
|
174
200
|
}
|
|
175
201
|
const errorMessage = error.message || String(error);
|
|
@@ -178,16 +204,20 @@ export default class ElectronBleTransport {
|
|
|
178
204
|
const permissionMessage = HardwareErrorCodeMessage[HardwareErrorCode.BlePermissionError];
|
|
179
205
|
|
|
180
206
|
if (errorMessage.includes(poweredOffMessage) || errorMessage.includes('poweredOff')) {
|
|
181
|
-
|
|
207
|
+
return ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
|
|
182
208
|
}
|
|
183
209
|
if (errorMessage.includes(unsupportedMessage) || errorMessage.includes('unsupported')) {
|
|
184
|
-
|
|
210
|
+
return ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
|
|
185
211
|
}
|
|
186
212
|
if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
|
|
187
|
-
|
|
213
|
+
return ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
|
|
188
214
|
}
|
|
189
215
|
}
|
|
190
|
-
|
|
216
|
+
return error;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private handleBluetoothError(error: any): never {
|
|
220
|
+
throw this.normalizeBluetoothError(error);
|
|
191
221
|
}
|
|
192
222
|
|
|
193
223
|
private cleanupDeviceState(deviceId: string): void {
|
|
@@ -306,7 +336,7 @@ export default class ElectronBleTransport {
|
|
|
306
336
|
if (!window.desktopApi?.nobleBle) {
|
|
307
337
|
throw new Error('Noble BLE API not available');
|
|
308
338
|
}
|
|
309
|
-
const devices = await window.desktopApi.nobleBle.enumerate();
|
|
339
|
+
const devices = await invokeNobleBle(window.desktopApi.nobleBle.enumerate());
|
|
310
340
|
this.Log?.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
|
|
311
341
|
for (const dev of devices) {
|
|
312
342
|
this.Log?.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`);
|
|
@@ -341,7 +371,7 @@ export default class ElectronBleTransport {
|
|
|
341
371
|
throw new Error('Noble BLE API not available');
|
|
342
372
|
}
|
|
343
373
|
|
|
344
|
-
const device = await window.desktopApi.nobleBle.getDevice(uuid);
|
|
374
|
+
const device = await invokeNobleBle(window.desktopApi.nobleBle.getDevice(uuid));
|
|
345
375
|
if (!device) {
|
|
346
376
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${uuid} not found`);
|
|
347
377
|
}
|
|
@@ -353,7 +383,7 @@ export default class ElectronBleTransport {
|
|
|
353
383
|
}
|
|
354
384
|
|
|
355
385
|
try {
|
|
356
|
-
await window.desktopApi.nobleBle.connect(uuid);
|
|
386
|
+
await invokeNobleBle(window.desktopApi.nobleBle.connect(uuid));
|
|
357
387
|
this.connectedDevices.add(uuid);
|
|
358
388
|
} catch (error) {
|
|
359
389
|
this.handleBluetoothError(error);
|
|
@@ -367,7 +397,11 @@ export default class ElectronBleTransport {
|
|
|
367
397
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
368
398
|
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
|
|
369
399
|
|
|
370
|
-
|
|
400
|
+
try {
|
|
401
|
+
await invokeNobleBle(window.desktopApi.nobleBle.subscribe(uuid));
|
|
402
|
+
} catch (error) {
|
|
403
|
+
this.handleBluetoothError(error);
|
|
404
|
+
}
|
|
371
405
|
await this.refreshBlePacketCapacity(uuid);
|
|
372
406
|
|
|
373
407
|
const cleanup = this.createNotificationSubscription(uuid);
|
|
@@ -389,15 +423,20 @@ export default class ElectronBleTransport {
|
|
|
389
423
|
} catch (error) {
|
|
390
424
|
this.Log?.error('[Electron BLE] acquire failed:', error);
|
|
391
425
|
try {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
426
|
+
const nobleBle = window.desktopApi?.nobleBle;
|
|
427
|
+
if (nobleBle) {
|
|
428
|
+
if (this.connectedDevices.has(uuid)) {
|
|
429
|
+
await invokeNobleBle(nobleBle.unsubscribe(uuid)).catch(cleanupError => {
|
|
430
|
+
this.Log?.debug('[Electron BLE] acquire unsubscribe failed:', cleanupError);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
await invokeNobleBle(nobleBle.disconnect(uuid));
|
|
395
434
|
}
|
|
396
435
|
} catch (cleanupError) {
|
|
397
436
|
this.Log?.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
|
|
398
437
|
}
|
|
399
438
|
this.cleanupDeviceState(uuid);
|
|
400
|
-
|
|
439
|
+
this.handleBluetoothError(error);
|
|
401
440
|
}
|
|
402
441
|
}
|
|
403
442
|
|
|
@@ -420,13 +459,16 @@ export default class ElectronBleTransport {
|
|
|
420
459
|
// Hard teardown, error paths only: a link presumed dead must not be reused.
|
|
421
460
|
private async releaseNative(id: string) {
|
|
422
461
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
await
|
|
462
|
+
const nobleBle = window.desktopApi?.nobleBle;
|
|
463
|
+
if (nobleBle) {
|
|
464
|
+
if (this.connectedDevices.has(id)) {
|
|
465
|
+
await invokeNobleBle(nobleBle.unsubscribe(id)).catch(error => {
|
|
466
|
+
this.Log?.debug('[Electron BLE] release unsubscribe failed:', error);
|
|
467
|
+
});
|
|
427
468
|
}
|
|
428
|
-
|
|
469
|
+
await invokeNobleBle(nobleBle.disconnect(id));
|
|
429
470
|
}
|
|
471
|
+
this.cleanupDeviceState(id);
|
|
430
472
|
} catch (error) {
|
|
431
473
|
this.Log?.error('[Electron BLE] release failed:', error);
|
|
432
474
|
this.cleanupDeviceState(id);
|
|
@@ -452,7 +494,7 @@ export default class ElectronBleTransport {
|
|
|
452
494
|
}
|
|
453
495
|
return;
|
|
454
496
|
}
|
|
455
|
-
await release(id, keepSession);
|
|
497
|
+
await invokeNobleBle(release(id, keepSession));
|
|
456
498
|
} catch (error) {
|
|
457
499
|
this.Log?.error('[Electron BLE] logical release failed:', error);
|
|
458
500
|
this.cleanupDeviceState(id);
|
|
@@ -487,13 +529,20 @@ export default class ElectronBleTransport {
|
|
|
487
529
|
expectedProtocol?: ProtocolType,
|
|
488
530
|
protocolHint?: ProtocolType
|
|
489
531
|
): Promise<ProtocolType> {
|
|
532
|
+
// A declared V1 is taken at face value, as the React Native transport
|
|
533
|
+
// already does on iOS: the caller reads the protocol off its own device
|
|
534
|
+
// record, so probing re-asks a question that is already answered and adds a
|
|
535
|
+
// round trip to every cold connect. It also fails in a way that costs the
|
|
536
|
+
// session: a device whose protocol session has stalled ignores the probe
|
|
537
|
+
// frame, and the timeout became a protocol-mismatch error that stopped Core
|
|
538
|
+
// from ever sending Initialize — the one frame such a device still answers,
|
|
539
|
+
// and how it gets revived. A declared V2 keeps probing, matching iOS, so a
|
|
540
|
+
// USB-priority "link disabled" surfaces here rather than as an unmapped
|
|
541
|
+
// error later. An undeclared protocol still goes through full detection.
|
|
490
542
|
if (expectedProtocol === 'V1') {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return 'V1';
|
|
495
|
-
}
|
|
496
|
-
throw this.createProtocolMismatchError(expectedProtocol, uuid);
|
|
543
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
544
|
+
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected, no probe)`);
|
|
545
|
+
return 'V1';
|
|
497
546
|
}
|
|
498
547
|
|
|
499
548
|
if (expectedProtocol === 'V2') {
|
|
@@ -588,6 +637,9 @@ export default class ElectronBleTransport {
|
|
|
588
637
|
} catch (error) {
|
|
589
638
|
this.clearProbeProtocol(uuid, 'V1');
|
|
590
639
|
this.Log?.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
|
|
640
|
+
if (isProtocolV2LinkDisabledError(error)) {
|
|
641
|
+
throw error;
|
|
642
|
+
}
|
|
591
643
|
return false;
|
|
592
644
|
}
|
|
593
645
|
}
|
|
@@ -608,6 +660,8 @@ export default class ElectronBleTransport {
|
|
|
608
660
|
this.v2Assemblers.get(uuid)?.reset();
|
|
609
661
|
this.resetProtocolV2Frames(uuid);
|
|
610
662
|
},
|
|
663
|
+
shouldRethrow: error =>
|
|
664
|
+
isBleStaleBondHardwareError(error) || isProtocolV2LinkDisabledError(error),
|
|
611
665
|
});
|
|
612
666
|
if (!detected) {
|
|
613
667
|
this.clearProbeProtocol(uuid, 'V2');
|
|
@@ -621,11 +675,16 @@ export default class ElectronBleTransport {
|
|
|
621
675
|
throw new Error('Noble BLE API not available');
|
|
622
676
|
}
|
|
623
677
|
|
|
624
|
-
|
|
678
|
+
try {
|
|
679
|
+
await invokeNobleBle(nobleBle.write(uuid, hexData, { pacingDelayMs: 0 }));
|
|
680
|
+
} catch (error) {
|
|
681
|
+
this.handleBluetoothError(error);
|
|
682
|
+
}
|
|
625
683
|
}
|
|
626
684
|
|
|
627
685
|
private async refreshBlePacketCapacity(uuid: string): Promise<void> {
|
|
628
|
-
const
|
|
686
|
+
const nobleBle = window.desktopApi?.nobleBle;
|
|
687
|
+
const device = nobleBle ? await invokeNobleBle(nobleBle.getDevice(uuid)) : undefined;
|
|
629
688
|
this.updateBlePacketCapacity(uuid, device?.mtu);
|
|
630
689
|
}
|
|
631
690
|
|
|
@@ -695,9 +754,29 @@ export default class ElectronBleTransport {
|
|
|
695
754
|
this.handleProtocolV2Notification(deviceId, hexData);
|
|
696
755
|
return;
|
|
697
756
|
}
|
|
757
|
+
const linkDisabledError = this.readProtocolV2LinkDisabledFailure(deviceId, hexData);
|
|
758
|
+
if (linkDisabledError) {
|
|
759
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
760
|
+
this.runPromise.reject(linkDisabledError);
|
|
761
|
+
}
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
698
764
|
this.handleProtocolV1Notification(deviceId, hexData);
|
|
699
765
|
}
|
|
700
766
|
|
|
767
|
+
private readProtocolV2LinkDisabledFailure(deviceId: string, hexData: string) {
|
|
768
|
+
if (!this._messages || !this._messagesV2) return undefined;
|
|
769
|
+
|
|
770
|
+
const assembler = this.v2Assemblers.get(deviceId);
|
|
771
|
+
if (!assembler) return undefined;
|
|
772
|
+
|
|
773
|
+
return detectProtocolV2LinkDisabledError({
|
|
774
|
+
schemas: { protocolV1: this._messages, protocolV2: this._messagesV2 },
|
|
775
|
+
assembler,
|
|
776
|
+
bytes: hexToBytes(hexData),
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
701
780
|
private handleProtocolV2Notification(deviceId: string, hexData: string): void {
|
|
702
781
|
try {
|
|
703
782
|
const bytes = hexToBytes(hexData);
|
|
@@ -863,7 +942,7 @@ export default class ElectronBleTransport {
|
|
|
863
942
|
if (hexString.length === 0) {
|
|
864
943
|
throw new Error(`Buffer ${i + 1} is empty`);
|
|
865
944
|
}
|
|
866
|
-
await window.desktopApi.nobleBle.write(uuid, hexString);
|
|
945
|
+
await invokeNobleBle(window.desktopApi.nobleBle.write(uuid, hexString));
|
|
867
946
|
}
|
|
868
947
|
|
|
869
948
|
const response = await Promise.race([
|
|
@@ -901,7 +980,7 @@ export default class ElectronBleTransport {
|
|
|
901
980
|
await this.releaseNative(uuid);
|
|
902
981
|
}
|
|
903
982
|
}
|
|
904
|
-
throw e;
|
|
983
|
+
throw this.normalizeBluetoothError(e);
|
|
905
984
|
} finally {
|
|
906
985
|
if (timeout) clearTimeout(timeout);
|
|
907
986
|
if (this.runPromise === runPromise) {
|
package/src/webusb.ts
CHANGED
|
@@ -107,6 +107,9 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
107
107
|
/** Per-path protocol type detected by active wire-level probe. */
|
|
108
108
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
109
109
|
|
|
110
|
+
/** Protocols previously confirmed by an active response for this transport instance. */
|
|
111
|
+
private confirmedDeviceProtocols: Map<string, ProtocolType> = new Map();
|
|
112
|
+
|
|
110
113
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
111
114
|
|
|
112
115
|
/**
|
|
@@ -309,7 +312,26 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
309
312
|
await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
310
313
|
await this.closeOpenDevice(input.path);
|
|
311
314
|
await this.connect(input.path ?? '', true);
|
|
312
|
-
if (input.
|
|
315
|
+
if (input.skipProtocolProbe) {
|
|
316
|
+
if (!input.expectedProtocol) {
|
|
317
|
+
throw ERRORS.TypedError(
|
|
318
|
+
HardwareErrorCode.RuntimeError,
|
|
319
|
+
'skipProtocolProbe requires an expected protocol'
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (this.confirmedDeviceProtocols.get(input.path) !== input.expectedProtocol) {
|
|
323
|
+
throw ERRORS.TypedError(
|
|
324
|
+
HardwareErrorCode.RuntimeError,
|
|
325
|
+
'skipProtocolProbe requires a previously confirmed protocol for this WebUSB endpoint'
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
this.staleProtocolPaths.delete(input.path);
|
|
329
|
+
this.deviceProtocol.set(input.path, input.expectedProtocol);
|
|
330
|
+
const currentDevice = this.deviceList.find(d => d.path === input.path)?.device;
|
|
331
|
+
if (currentDevice) {
|
|
332
|
+
this.probedDeviceObjects.set(input.path, currentDevice);
|
|
333
|
+
}
|
|
334
|
+
} else if (input.forceProtocolDetection) {
|
|
313
335
|
// Explicit recovery/discovery (e.g. detectDeviceConnectProtocol) must
|
|
314
336
|
// probe on the wire regardless of any cached result — the cached
|
|
315
337
|
// binding may be exactly what the caller is trying to recover from.
|
|
@@ -317,13 +339,13 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
317
339
|
this.deviceProtocol.delete(input.path);
|
|
318
340
|
this.deviceProtocolHints.delete(input.path);
|
|
319
341
|
}
|
|
320
|
-
if (this.staleProtocolPaths.has(input.path)) {
|
|
342
|
+
if (!input.skipProtocolProbe && this.staleProtocolPaths.has(input.path)) {
|
|
321
343
|
// The device disconnected (possibly rebooting into another mode) since
|
|
322
344
|
// the protocol was probed — drop the cache so it is re-probed below.
|
|
323
345
|
this.staleProtocolPaths.delete(input.path);
|
|
324
346
|
this.deviceProtocol.delete(input.path);
|
|
325
347
|
}
|
|
326
|
-
if (this.deviceProtocol.has(input.path)) {
|
|
348
|
+
if (!input.skipProtocolProbe && this.deviceProtocol.has(input.path)) {
|
|
327
349
|
const currentDevice = this.deviceList.find(d => d.path === input.path)?.device;
|
|
328
350
|
if (!currentDevice || currentDevice !== this.probedDeviceObjects.get(input.path)) {
|
|
329
351
|
// The OS re-enumerated the device since the probe (a replug/reboot we
|
|
@@ -333,7 +355,12 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
333
355
|
}
|
|
334
356
|
}
|
|
335
357
|
const cachedProtocol = this.deviceProtocol.get(input.path);
|
|
336
|
-
if (
|
|
358
|
+
if (
|
|
359
|
+
!input.skipProtocolProbe &&
|
|
360
|
+
cachedProtocol &&
|
|
361
|
+
input.expectedProtocol &&
|
|
362
|
+
cachedProtocol !== input.expectedProtocol
|
|
363
|
+
) {
|
|
337
364
|
// The caller expects a different protocol than the cached probe result;
|
|
338
365
|
// the cache is stale — drop it and re-probe on the wire below.
|
|
339
366
|
this.deviceProtocol.delete(input.path);
|
|
@@ -348,7 +375,12 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
348
375
|
if (protocolHint) {
|
|
349
376
|
this.deviceProtocolHints.set(input.path, protocolHint);
|
|
350
377
|
}
|
|
351
|
-
await this.detectProtocol(
|
|
378
|
+
const detectedProtocol = await this.detectProtocol(
|
|
379
|
+
input.path,
|
|
380
|
+
input.expectedProtocol,
|
|
381
|
+
protocolHint
|
|
382
|
+
);
|
|
383
|
+
this.confirmedDeviceProtocols.set(input.path, detectedProtocol);
|
|
352
384
|
const probedDevice = this.deviceList.find(d => d.path === input.path)?.device;
|
|
353
385
|
if (probedDevice) {
|
|
354
386
|
this.probedDeviceObjects.set(input.path, probedDevice);
|