@onekeyfe/hd-transport-web-device 1.2.0-alpha.2 → 1.2.0-alpha.20
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 +102 -8
- package/__tests__/webusb-protocol-v2-timeout.test.ts +88 -0
- package/dist/electron-ble-transport.d.ts +4 -4
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +97 -4
- package/dist/index.js +167 -177
- package/dist/transportLog.d.ts +7 -0
- package/dist/transportLog.d.ts.map +1 -0
- package/dist/webusb.d.ts +3 -0
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/electron-ble-transport.ts +111 -138
- package/src/transportLog.ts +11 -0
- package/src/webusb.ts +85 -78
package/src/webusb.ts
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
/* eslint-disable no-undef */
|
|
2
2
|
import transport, {
|
|
3
|
-
LogBlockCommand,
|
|
4
3
|
PROTOCOL_V1_CHUNK_PAYLOAD_SIZE,
|
|
5
4
|
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
6
5
|
PROTOCOL_V1_REPORT_ID,
|
|
7
6
|
PROTOCOL_V1_USB_PACKET_SIZE,
|
|
8
7
|
PROTOCOL_V2_CHANNEL_USB,
|
|
9
8
|
PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
9
|
+
PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS,
|
|
10
10
|
ProtocolV2FrameAssembler,
|
|
11
|
+
ProtocolV2SequenceCursor,
|
|
11
12
|
ProtocolV2Session,
|
|
12
13
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
13
14
|
} from '@onekeyfe/hd-transport';
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
ERRORS,
|
|
17
|
+
HardwareErrorCode,
|
|
18
|
+
ONEKEY_WEBUSB_FILTER,
|
|
19
|
+
isKnownTrezorWebUsbDevice,
|
|
20
|
+
wait,
|
|
21
|
+
} from '@onekeyfe/hd-shared';
|
|
15
22
|
import ByteBuffer from 'bytebuffer';
|
|
16
23
|
|
|
24
|
+
import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
|
|
25
|
+
|
|
17
26
|
import type {
|
|
18
27
|
AcquireInput,
|
|
19
28
|
OneKeyDeviceInfoBase,
|
|
@@ -33,22 +42,6 @@ const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
|
|
|
33
42
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
34
43
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
35
44
|
const PROTOCOL_PROBE_TIMEOUT = 1000;
|
|
36
|
-
const WEBUSB_FILE_WRITE_LOG_BLOCK_PATTERN = /(?:^|[^a-z])(?:raw)?(?:filesystem|emmc)?filewrite$/i;
|
|
37
|
-
|
|
38
|
-
function shouldSuppressWebUsbCallLog(name: string) {
|
|
39
|
-
const normalized = name.replace(/[_\s-]/g, '');
|
|
40
|
-
return WEBUSB_FILE_WRITE_LOG_BLOCK_PATTERN.test(normalized);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isLogBlockCommand(name: string) {
|
|
44
|
-
return (LogBlockCommand as Set<string> | undefined)?.has?.(name) ?? false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function shouldBlockWebUsbCallDataLog(name: string) {
|
|
48
|
-
const normalized = name.replace(/[_\s-]/g, '');
|
|
49
|
-
return isLogBlockCommand(name) || WEBUSB_FILE_WRITE_LOG_BLOCK_PATTERN.test(normalized);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
45
|
function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
53
46
|
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
54
47
|
}
|
|
@@ -84,22 +77,26 @@ export default class WebUsbTransport {
|
|
|
84
77
|
|
|
85
78
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
86
79
|
|
|
87
|
-
/**
|
|
80
|
+
/** Per-device Protocol V2 assembler that retains extra frames from one read. */
|
|
88
81
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
89
82
|
|
|
90
|
-
/**
|
|
83
|
+
/** Per-device Protocol V2 session that keeps sequence numbers monotonic. */
|
|
91
84
|
private protocolV2Sessions: Map<string, ProtocolV2Session> = new Map();
|
|
92
85
|
|
|
93
|
-
/**
|
|
86
|
+
/** Sequence cursors survive ordinary reconnects and cached session rebuilds. */
|
|
87
|
+
private protocolV2Sequences: Map<string, ProtocolV2SequenceCursor> = new Map();
|
|
88
|
+
|
|
89
|
+
/** Read timeout for the current Protocol V2 call, consumed by cached readFrame. */
|
|
94
90
|
private protocolV2ReadTimeouts: Map<string, number | undefined> = new Map();
|
|
95
91
|
|
|
92
|
+
private protocolV2WriteTimeoutMs = PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS;
|
|
93
|
+
|
|
96
94
|
/** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
|
|
97
95
|
private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
|
|
98
96
|
|
|
99
97
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* 避免设备因为空 serial 被发现流程整体丢弃。重新插拔后实例变化,path 会重新生成。
|
|
98
|
+
* Early Pro2 boards have no USB serial number. Assign a session-stable mock path per
|
|
99
|
+
* USBDevice instance so discovery retains them; reconnecting creates a new instance/path.
|
|
103
100
|
*/
|
|
104
101
|
private mockSerialPaths: WeakMap<USBDevice, string> = new WeakMap();
|
|
105
102
|
|
|
@@ -159,7 +156,6 @@ export default class WebUsbTransport {
|
|
|
159
156
|
this.messagesV2 = parseConfigure(signedData);
|
|
160
157
|
this.protocolV2Sessions.clear();
|
|
161
158
|
this.protocolV2ReadTimeouts.clear();
|
|
162
|
-
this.Log?.debug('[WebUsbTransport] Protocol V2 schema configured');
|
|
163
159
|
}
|
|
164
160
|
|
|
165
161
|
/**
|
|
@@ -192,8 +188,7 @@ export default class WebUsbTransport {
|
|
|
192
188
|
}
|
|
193
189
|
|
|
194
190
|
/**
|
|
195
|
-
*
|
|
196
|
-
* 空 serial(早期工程板)回退到会话内稳定的 mock path。
|
|
191
|
+
* Use the USB serial as the device path, falling back to a session-stable mock path.
|
|
197
192
|
*/
|
|
198
193
|
private getDevicePath(device: USBDevice): string {
|
|
199
194
|
if (typeof device.serialNumber === 'string' && device.serialNumber.length > 0) {
|
|
@@ -218,11 +213,13 @@ export default class WebUsbTransport {
|
|
|
218
213
|
if (!this.usb) return [];
|
|
219
214
|
|
|
220
215
|
const devices = await this.usb.getDevices();
|
|
221
|
-
const onekeyDevices = devices.filter(dev =>
|
|
222
|
-
ONEKEY_WEBUSB_FILTER.some(
|
|
223
|
-
desc
|
|
224
|
-
|
|
225
|
-
|
|
216
|
+
const onekeyDevices = devices.filter(dev => {
|
|
217
|
+
const isOneKey = ONEKEY_WEBUSB_FILTER.some(
|
|
218
|
+
(desc: { vendorId: number; productId: number }) =>
|
|
219
|
+
dev.vendorId === desc.vendorId && dev.productId === desc.productId
|
|
220
|
+
);
|
|
221
|
+
return isOneKey && !isKnownTrezorWebUsbDevice(dev);
|
|
222
|
+
});
|
|
226
223
|
|
|
227
224
|
this.deviceList = onekeyDevices.map(device => {
|
|
228
225
|
const path = this.getDevicePath(device);
|
|
@@ -238,14 +235,6 @@ export default class WebUsbTransport {
|
|
|
238
235
|
};
|
|
239
236
|
});
|
|
240
237
|
|
|
241
|
-
// Debug: log all discovered devices. Protocol is detected after acquire via wire probe.
|
|
242
|
-
for (const dev of onekeyDevices) {
|
|
243
|
-
this.Log.debug(
|
|
244
|
-
`[WebUSB] Device: name="${dev.productName}" serial="${dev.serialNumber}" ` +
|
|
245
|
-
`VID=0x${dev.vendorId.toString(16)} PID=0x${dev.productId.toString(16)}`
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
238
|
return this.deviceList;
|
|
250
239
|
}
|
|
251
240
|
|
|
@@ -300,23 +289,21 @@ export default class WebUsbTransport {
|
|
|
300
289
|
if (expectedProtocol === 'V1') {
|
|
301
290
|
if (await this.probeProtocolV1(path)) {
|
|
302
291
|
this.deviceProtocol.set(path, 'V1');
|
|
303
|
-
this.Log.debug(`[WebUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
|
|
304
292
|
return 'V1';
|
|
305
293
|
}
|
|
294
|
+
await this.resetConnectionAfterProbe(path);
|
|
306
295
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
307
296
|
}
|
|
308
297
|
|
|
309
298
|
if (expectedProtocol === 'V2') {
|
|
310
|
-
//
|
|
311
|
-
//
|
|
299
|
+
// Skip probing when the caller explicitly confirms V2, such as reconnect after a
|
|
300
|
+
// firmware reboot where expectedProtocol carries the previously probed result.
|
|
312
301
|
this.deviceProtocol.set(path, 'V2');
|
|
313
|
-
this.Log.debug(`[WebUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
|
|
314
302
|
return 'V2';
|
|
315
303
|
}
|
|
316
304
|
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
// 不能作为最终结论。
|
|
305
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
306
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
320
307
|
const probeOrder: ProtocolType[] =
|
|
321
308
|
protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
322
309
|
|
|
@@ -325,9 +312,14 @@ export default class WebUsbTransport {
|
|
|
325
312
|
protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
|
|
326
313
|
if (detected) {
|
|
327
314
|
this.deviceProtocol.set(path, protocol);
|
|
328
|
-
this.Log.debug(`[WebUsbTransport] detectProtocol: path=${path} -> ${protocol}`);
|
|
329
315
|
return protocol;
|
|
330
316
|
}
|
|
317
|
+
if (protocol === 'V1') {
|
|
318
|
+
// A timed-out WebUSB transferIn cannot be cancelled in place. Closing and
|
|
319
|
+
// reopening the device guarantees the next protocol probe cannot consume a
|
|
320
|
+
// late V1 response. V2 timeout recovery is owned by callProtocolV2.
|
|
321
|
+
await this.resetConnectionAfterProbe(path);
|
|
322
|
+
}
|
|
331
323
|
}
|
|
332
324
|
|
|
333
325
|
this.deviceProtocol.delete(path);
|
|
@@ -414,13 +406,6 @@ export default class WebUsbTransport {
|
|
|
414
406
|
*/
|
|
415
407
|
async connectToDevice(path: string, first: boolean) {
|
|
416
408
|
let device: USBDevice = await this.findDevice(path);
|
|
417
|
-
this.Log.debug(
|
|
418
|
-
'[WebUsbTransport] connecting to device:',
|
|
419
|
-
device.productName,
|
|
420
|
-
'PID:',
|
|
421
|
-
device.productId
|
|
422
|
-
);
|
|
423
|
-
|
|
424
409
|
if (!device.opened) {
|
|
425
410
|
await device.open();
|
|
426
411
|
}
|
|
@@ -571,16 +556,7 @@ export default class WebUsbTransport {
|
|
|
571
556
|
let lastError: unknown;
|
|
572
557
|
for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt += 1) {
|
|
573
558
|
try {
|
|
574
|
-
|
|
575
|
-
if (!device.opened) {
|
|
576
|
-
await this.connect(path, false);
|
|
577
|
-
}
|
|
578
|
-
const endpoints = this.deviceEndpoints.get(path);
|
|
579
|
-
const endpointOut = endpoints?.endpointOut ?? this.endpointId;
|
|
580
|
-
const transferBuffer = this.toArrayBuffer(
|
|
581
|
-
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
582
|
-
);
|
|
583
|
-
await device.transferOut(endpointOut, transferBuffer);
|
|
559
|
+
await this.transferOutOnce(path, packet);
|
|
584
560
|
return;
|
|
585
561
|
} catch (error) {
|
|
586
562
|
lastError = error;
|
|
@@ -603,6 +579,19 @@ export default class WebUsbTransport {
|
|
|
603
579
|
throw lastError;
|
|
604
580
|
}
|
|
605
581
|
|
|
582
|
+
private async transferOutOnce(path: string, packet: Uint8Array) {
|
|
583
|
+
const device = await this.findDevice(path);
|
|
584
|
+
if (!device.opened) {
|
|
585
|
+
await this.connect(path, false);
|
|
586
|
+
}
|
|
587
|
+
const endpoints = this.deviceEndpoints.get(path);
|
|
588
|
+
const endpointOut = endpoints?.endpointOut ?? this.endpointId;
|
|
589
|
+
const transferBuffer = this.toArrayBuffer(
|
|
590
|
+
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
591
|
+
);
|
|
592
|
+
await device.transferOut(endpointOut, transferBuffer);
|
|
593
|
+
}
|
|
594
|
+
|
|
606
595
|
private async transferInWithRetry(
|
|
607
596
|
path: string,
|
|
608
597
|
length: number,
|
|
@@ -672,13 +661,12 @@ export default class WebUsbTransport {
|
|
|
672
661
|
}
|
|
673
662
|
|
|
674
663
|
private async withProtocolReadTimeout<T>(
|
|
675
|
-
|
|
664
|
+
_path: string,
|
|
676
665
|
promise: Promise<T>,
|
|
677
666
|
timeoutMs: number,
|
|
678
667
|
protocol: ProtocolType,
|
|
679
668
|
onTimeout?: () => void
|
|
680
669
|
): Promise<T> {
|
|
681
|
-
void path;
|
|
682
670
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
683
671
|
let timedOut = false;
|
|
684
672
|
const waitForeverAfterTimeout = () => new Promise<never>(() => {});
|
|
@@ -695,7 +683,7 @@ export default class WebUsbTransport {
|
|
|
695
683
|
return await Promise.race([
|
|
696
684
|
guardedPromise,
|
|
697
685
|
new Promise<never>((_, reject) => {
|
|
698
|
-
timer = setTimeout(
|
|
686
|
+
timer = setTimeout(() => {
|
|
699
687
|
timedOut = true;
|
|
700
688
|
onTimeout?.();
|
|
701
689
|
reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
|
|
@@ -715,8 +703,7 @@ export default class WebUsbTransport {
|
|
|
715
703
|
try {
|
|
716
704
|
await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
|
|
717
705
|
return true;
|
|
718
|
-
} catch (
|
|
719
|
-
this.Log.debug('[WebUsbTransport] Protocol V1 Initialize probe failed:', error);
|
|
706
|
+
} catch (_error) {
|
|
720
707
|
return false;
|
|
721
708
|
}
|
|
722
709
|
}
|
|
@@ -731,7 +718,6 @@ export default class WebUsbTransport {
|
|
|
731
718
|
timeoutMs: PROTOCOL_PROBE_TIMEOUT,
|
|
732
719
|
logger: this.Log,
|
|
733
720
|
logPrefix: 'ProtocolV2 WebUSB',
|
|
734
|
-
onProbeFailed: () => this.resetConnectionAfterProbe(path),
|
|
735
721
|
});
|
|
736
722
|
}
|
|
737
723
|
|
|
@@ -761,12 +747,8 @@ export default class WebUsbTransport {
|
|
|
761
747
|
);
|
|
762
748
|
}
|
|
763
749
|
|
|
764
|
-
if (
|
|
765
|
-
|
|
766
|
-
} else if (shouldBlockWebUsbCallDataLog(name)) {
|
|
767
|
-
this.Log.debug('call-', ' name: ', name, ' protocol: ', protocol);
|
|
768
|
-
} else {
|
|
769
|
-
this.Log.debug('call-', ' name: ', name, ' data: ', data, ' protocol: ', protocol);
|
|
750
|
+
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
751
|
+
this.Log.debug('transport call', createTransportCallLog(name, protocol));
|
|
770
752
|
}
|
|
771
753
|
|
|
772
754
|
if (protocol === 'V2') {
|
|
@@ -829,13 +811,20 @@ export default class WebUsbTransport {
|
|
|
829
811
|
|
|
830
812
|
let session = this.protocolV2Sessions.get(path);
|
|
831
813
|
if (!session) {
|
|
814
|
+
let sequenceCursor = this.protocolV2Sequences.get(path);
|
|
815
|
+
if (!sequenceCursor) {
|
|
816
|
+
sequenceCursor = new ProtocolV2SequenceCursor();
|
|
817
|
+
this.protocolV2Sequences.set(path, sequenceCursor);
|
|
818
|
+
}
|
|
832
819
|
session = new ProtocolV2Session({
|
|
833
820
|
schemas: {
|
|
834
821
|
protocolV1: protocolV1Messages,
|
|
835
822
|
protocolV2: this.messagesV2,
|
|
836
823
|
},
|
|
837
824
|
router: PROTOCOL_V2_CHANNEL_USB,
|
|
838
|
-
|
|
825
|
+
sequenceCursor,
|
|
826
|
+
writeTimeoutMs: this.protocolV2WriteTimeoutMs,
|
|
827
|
+
writeFrame: (frame: Uint8Array) => this.transferOutOnce(path, frame),
|
|
839
828
|
readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
|
|
840
829
|
logger: this.Log,
|
|
841
830
|
logPrefix: 'ProtocolV2 WebUSB',
|
|
@@ -849,6 +838,22 @@ export default class WebUsbTransport {
|
|
|
849
838
|
this.protocolV2Assemblers.get(path)?.reset();
|
|
850
839
|
try {
|
|
851
840
|
return await session.call(name, data, options);
|
|
841
|
+
} catch (error) {
|
|
842
|
+
const message =
|
|
843
|
+
error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
844
|
+
if (
|
|
845
|
+
message.includes('protocol v2 read timeout') ||
|
|
846
|
+
message.includes('protocol v2 write timeout') ||
|
|
847
|
+
message.includes('protocol v2 delivery timeout') ||
|
|
848
|
+
message.includes('response timeout')
|
|
849
|
+
) {
|
|
850
|
+
try {
|
|
851
|
+
await this.resetConnectionAfterProbe(path);
|
|
852
|
+
} catch (resetError) {
|
|
853
|
+
this.Log.debug('[WebUsbTransport] Protocol V2 timeout reset failed:', resetError);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
throw error;
|
|
852
857
|
} finally {
|
|
853
858
|
this.protocolV2ReadTimeouts.delete(path);
|
|
854
859
|
}
|
|
@@ -956,6 +961,8 @@ export default class WebUsbTransport {
|
|
|
956
961
|
this.deviceProtocolHints.delete(path);
|
|
957
962
|
this.protocolV2Assemblers.get(path)?.reset();
|
|
958
963
|
this.protocolV2Assemblers.delete(path);
|
|
964
|
+
this.protocolV2Sessions.delete(path);
|
|
965
|
+
this.protocolV2ReadTimeouts.delete(path);
|
|
959
966
|
this.deviceEndpoints.delete(path);
|
|
960
967
|
}
|
|
961
968
|
|