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