@onekeyfe/hd-transport 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/README.md +1 -1
- package/__tests__/messages.test.js +122 -0
- package/__tests__/protocol-v2-link-manager.test.js +326 -0
- package/__tests__/protocol-v2-usb-transport-base.test.js +296 -0
- package/__tests__/protocol-v2.test.js +371 -107
- package/dist/constants.d.ts +5 -3
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +912 -371
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +551 -254
- package/dist/protocols/index.d.ts +10 -5
- package/dist/protocols/index.d.ts.map +1 -1
- package/dist/protocols/v2/constants.d.ts +1 -0
- package/dist/protocols/v2/constants.d.ts.map +1 -1
- package/dist/protocols/v2/decode.d.ts +2 -2
- package/dist/protocols/v2/decode.d.ts.map +1 -1
- package/dist/protocols/v2/encode.d.ts +2 -3
- package/dist/protocols/v2/encode.d.ts.map +1 -1
- package/dist/protocols/v2/index.d.ts +0 -1
- package/dist/protocols/v2/index.d.ts.map +1 -1
- package/dist/protocols/v2/link-manager.d.ts +35 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -0
- package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
- package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
- package/dist/protocols/v2/session.d.ts +15 -4
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +31 -0
- package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
- package/dist/serialization/protobuf/decode.d.ts.map +1 -1
- package/dist/serialization/protobuf/messages.d.ts.map +1 -1
- package/dist/types/messages.d.ts +535 -226
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +5 -3
- package/dist/types/transport.d.ts.map +1 -1
- package/messages-protocol-v2.json +825 -709
- package/package.json +3 -3
- package/scripts/protobuf-build.sh +96 -310
- package/scripts/protobuf-patches/index.js +1 -0
- package/scripts/protobuf-types.js +12 -0
- package/src/constants.ts +21 -15
- package/src/index.ts +8 -0
- package/src/protocols/index.ts +31 -40
- package/src/protocols/v2/constants.ts +1 -0
- package/src/protocols/v2/crc8.ts +1 -1
- package/src/protocols/v2/decode.ts +31 -34
- package/src/protocols/v2/encode.ts +2 -28
- package/src/protocols/v2/index.ts +0 -1
- package/src/protocols/v2/link-manager.ts +160 -0
- package/src/protocols/v2/sequence-cursor.ts +10 -0
- package/src/protocols/v2/session.ts +79 -180
- package/src/protocols/v2/usb-transport-base.ts +194 -0
- package/src/serialization/protobuf/decode.ts +4 -1
- package/src/serialization/protobuf/messages.ts +6 -1
- package/src/types/messages.ts +637 -266
- package/src/types/transport.ts +3 -3
- package/dist/protocols/v2/debug.d.ts +0 -13
- package/dist/protocols/v2/debug.d.ts.map +0 -1
- package/src/protocols/v2/debug.ts +0 -26
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PROTOCOL_V2_PACKET_SRC_COMMAND } from '../../constants';
|
|
2
2
|
import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
|
|
3
|
-
import {
|
|
3
|
+
import { ProtocolV2SequenceCursor } from './sequence-cursor';
|
|
4
4
|
import { ProtocolV2 } from '..';
|
|
5
5
|
import * as check from '../../utils/highlevel-checks';
|
|
6
6
|
import { LogBlockCommand } from '../../utils/logBlockCommand';
|
|
@@ -13,6 +13,13 @@ export type ProtocolV2Schemas = {
|
|
|
13
13
|
protocolV2: Root;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export type ProtocolV2CallContext = {
|
|
17
|
+
messageName: string;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
highVolume: boolean;
|
|
20
|
+
generation: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
16
23
|
type ProtocolLogger = {
|
|
17
24
|
debug?: (...args: any[]) => void;
|
|
18
25
|
error?: (...args: any[]) => void;
|
|
@@ -22,11 +29,15 @@ export type ProtocolV2SessionOptions = {
|
|
|
22
29
|
schemas: ProtocolV2Schemas;
|
|
23
30
|
router: number;
|
|
24
31
|
packetSrc?: number;
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
maxFrameBytes?: number;
|
|
33
|
+
prepareCall?: (context: ProtocolV2CallContext) => Promise<void> | void;
|
|
34
|
+
writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) => Promise<void>;
|
|
35
|
+
readFrame: (context: ProtocolV2CallContext) => Promise<Uint8Array>;
|
|
27
36
|
logger?: ProtocolLogger;
|
|
28
37
|
logPrefix?: string;
|
|
29
38
|
createTimeoutError?: (name: string, timeoutMs: number) => Error;
|
|
39
|
+
sequenceCursor?: ProtocolV2SequenceCursor;
|
|
40
|
+
generation?: number;
|
|
30
41
|
};
|
|
31
42
|
|
|
32
43
|
export type ProtocolV2CallOptions = {
|
|
@@ -37,6 +48,7 @@ export type ProtocolV2CallOptions = {
|
|
|
37
48
|
};
|
|
38
49
|
|
|
39
50
|
export { concatUint8Arrays, ProtocolV2FrameAssembler };
|
|
51
|
+
export { ProtocolV2SequenceCursor };
|
|
40
52
|
|
|
41
53
|
export function hexToBytes(hex: string): Uint8Array {
|
|
42
54
|
const clean = hex.replace(/\s+/g, '');
|
|
@@ -59,15 +71,6 @@ export function bytesToHex(bytes: Uint8Array): string {
|
|
|
59
71
|
.join('');
|
|
60
72
|
}
|
|
61
73
|
|
|
62
|
-
// Frame header bytes worth logging: SOF, len lo/hi, header CRC, router, attr, seq.
|
|
63
|
-
// The rest of the frame is protobuf payload and may contain sensitive fields
|
|
64
|
-
// (mnemonic words via WordAck, PINs, seeds via LoadDevice, ...), so raw frame
|
|
65
|
-
// hex logs must never include it.
|
|
66
|
-
const PROTOCOL_V2_DEBUG_HEADER_BYTES = 7;
|
|
67
|
-
const PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT = 20;
|
|
68
|
-
const PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT = 40;
|
|
69
|
-
const PROTOCOL_V2_DEBUG_STRING_LIMIT = 512;
|
|
70
|
-
const PROTOCOL_V2_DEBUG_DEPTH_LIMIT = 4;
|
|
71
74
|
const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
|
|
72
75
|
...LogBlockCommand,
|
|
73
76
|
'FilesystemFileRead',
|
|
@@ -79,102 +82,6 @@ function shouldReduceProtocolV2Debug(name: string) {
|
|
|
79
82
|
return HIGH_VOLUME_PROTOCOL_V2_CALLS.has(name);
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
function frameHeaderDebugHex(frame: Uint8Array): string {
|
|
83
|
-
// Only the frame header is dumped as hex; the payload is logged separately
|
|
84
|
-
// in sanitized/structured form (sanitizeProtocolV2DebugPayload).
|
|
85
|
-
return bytesToHex(frame.slice(0, PROTOCOL_V2_DEBUG_HEADER_BYTES));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function getBinaryByteLength(value: unknown): number | undefined {
|
|
89
|
-
if (value instanceof ArrayBuffer) {
|
|
90
|
-
return value.byteLength;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (ArrayBuffer.isView(value)) {
|
|
94
|
-
return value.byteLength;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (typeof Blob !== 'undefined' && value instanceof Blob) {
|
|
98
|
-
return value.size;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function summarizeRedactedData(value: unknown): string {
|
|
105
|
-
const byteLength = getBinaryByteLength(value);
|
|
106
|
-
if (byteLength !== undefined) {
|
|
107
|
-
return `[redacted data: ${byteLength} bytes]`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (typeof value === 'string') {
|
|
111
|
-
return `[redacted data: string length=${value.length}]`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (Array.isArray(value)) {
|
|
115
|
-
return `[redacted data: array length=${value.length}]`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (value && typeof value === 'object') {
|
|
119
|
-
return `[redacted data: object keys=${Object.keys(value).length}]`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return `[redacted data: ${typeof value}]`;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function sanitizeProtocolV2DebugPayload(value: unknown, key = '', depth = 0): unknown {
|
|
126
|
-
if (/^(data|payload)$/i.test(key) && value !== null && value !== undefined) {
|
|
127
|
-
return summarizeRedactedData(value);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (/(passphrase|pin|mnemonic|seed|private)/i.test(key)) {
|
|
131
|
-
return '[redacted sensitive value]';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const byteLength = getBinaryByteLength(value);
|
|
135
|
-
if (byteLength !== undefined) {
|
|
136
|
-
return `[binary: ${byteLength} bytes]`;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (typeof value === 'string') {
|
|
140
|
-
return value.length > PROTOCOL_V2_DEBUG_STRING_LIMIT
|
|
141
|
-
? `${value.slice(0, PROTOCOL_V2_DEBUG_STRING_LIMIT)}... (len=${value.length})`
|
|
142
|
-
: value;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (!value || typeof value !== 'object') {
|
|
146
|
-
return value;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (depth >= PROTOCOL_V2_DEBUG_DEPTH_LIMIT) {
|
|
150
|
-
return Array.isArray(value)
|
|
151
|
-
? `[array length=${value.length}]`
|
|
152
|
-
: `[object keys=${Object.keys(value).length}]`;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (Array.isArray(value)) {
|
|
156
|
-
const items = value
|
|
157
|
-
.slice(0, PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT)
|
|
158
|
-
.map(item => sanitizeProtocolV2DebugPayload(item, key, depth + 1));
|
|
159
|
-
if (value.length > PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT) {
|
|
160
|
-
items.push(`... (${value.length - PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT} more)`);
|
|
161
|
-
}
|
|
162
|
-
return items;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const entries = Object.entries(value).slice(0, PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT);
|
|
166
|
-
const sanitized: Record<string, unknown> = {};
|
|
167
|
-
entries.forEach(([entryKey, entryValue]) => {
|
|
168
|
-
sanitized[entryKey] = sanitizeProtocolV2DebugPayload(entryValue, entryKey, depth + 1);
|
|
169
|
-
});
|
|
170
|
-
if (Object.keys(value).length > PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT) {
|
|
171
|
-
sanitized.__truncated__ = `${
|
|
172
|
-
Object.keys(value).length - PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT
|
|
173
|
-
} more keys`;
|
|
174
|
-
}
|
|
175
|
-
return sanitized;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
85
|
const COMMON_TERMINAL_RESPONSE_TYPES = new Set([
|
|
179
86
|
'Failure',
|
|
180
87
|
'ButtonRequest',
|
|
@@ -229,24 +136,18 @@ export async function withProtocolTimeout<T>(
|
|
|
229
136
|
}
|
|
230
137
|
}
|
|
231
138
|
|
|
232
|
-
// Watchdog for the write phase only. Writing a single frame (max 4608 bytes)
|
|
233
|
-
// is a bounded transport operation, unlike the read phase where long-running
|
|
234
|
-
// device operations (firmware install, user confirmation) can legitimately
|
|
235
|
-
// take minutes — so no default timeout is applied to reads.
|
|
236
|
-
export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 30000;
|
|
237
|
-
|
|
238
139
|
export class ProtocolV2Session {
|
|
239
140
|
private readonly options: ProtocolV2SessionOptions;
|
|
240
141
|
|
|
142
|
+
private readonly sequenceCursor: ProtocolV2SequenceCursor;
|
|
143
|
+
|
|
241
144
|
// Serializes call() invocations: responses are matched only by type, so two
|
|
242
145
|
// in-flight calls on the same session would steal each other's responses.
|
|
243
146
|
private pendingCall: Promise<unknown> = Promise.resolve();
|
|
244
147
|
|
|
245
|
-
// Per-session sequence counter (1-255, wraps skipping 0).
|
|
246
|
-
private protoSeq = 0;
|
|
247
|
-
|
|
248
148
|
constructor(options: ProtocolV2SessionOptions) {
|
|
249
149
|
this.options = options;
|
|
150
|
+
this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor();
|
|
250
151
|
}
|
|
251
152
|
|
|
252
153
|
call(
|
|
@@ -271,47 +172,52 @@ export class ProtocolV2Session {
|
|
|
271
172
|
schemas,
|
|
272
173
|
router,
|
|
273
174
|
packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
175
|
+
maxFrameBytes,
|
|
176
|
+
prepareCall,
|
|
274
177
|
writeFrame,
|
|
275
178
|
readFrame,
|
|
276
179
|
logger,
|
|
277
180
|
logPrefix = 'ProtocolV2',
|
|
278
181
|
createTimeoutError,
|
|
182
|
+
generation = 0,
|
|
279
183
|
} = this.options;
|
|
280
184
|
|
|
281
185
|
const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
|
|
282
|
-
|
|
186
|
+
const callContext: ProtocolV2CallContext = {
|
|
187
|
+
messageName: name,
|
|
188
|
+
timeoutMs: callOptions.timeoutMs,
|
|
189
|
+
highVolume: shouldReduceDebug,
|
|
190
|
+
generation,
|
|
191
|
+
};
|
|
192
|
+
await prepareCall?.(callContext);
|
|
193
|
+
const protoSeq = this.sequenceCursor.next();
|
|
283
194
|
const frame = ProtocolV2.encodeFrame(schemas, name, data, {
|
|
284
195
|
packetSrc,
|
|
285
196
|
router,
|
|
286
|
-
seq:
|
|
287
|
-
logger: shouldReduceDebug ? undefined : logger,
|
|
288
|
-
logPrefix,
|
|
289
|
-
context: `tx:${name}`,
|
|
197
|
+
seq: protoSeq,
|
|
290
198
|
});
|
|
291
|
-
|
|
199
|
+
// Suppress Protocol V2 TX frame logs to avoid excessive transport diagnostics.
|
|
200
|
+
/*
|
|
201
|
+
const txMetadata = ProtocolV2.inspectFrame(schemas, frame);
|
|
292
202
|
|
|
293
203
|
if (!shouldReduceDebug) {
|
|
294
|
-
logger?.debug?.(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
204
|
+
logger?.debug?.(`[${logPrefix}] TX`, {
|
|
205
|
+
method: name,
|
|
206
|
+
type: name,
|
|
207
|
+
typeId: txMetadata.messageTypeId,
|
|
208
|
+
seq: txMetadata.seq,
|
|
209
|
+
bytes: frame.length,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
|
|
302
217
|
);
|
|
303
218
|
}
|
|
304
219
|
|
|
305
|
-
|
|
306
|
-
// PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS for the rationale.
|
|
307
|
-
await withProtocolTimeout(
|
|
308
|
-
writeFrame(frame),
|
|
309
|
-
PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS,
|
|
310
|
-
() =>
|
|
311
|
-
new Error(
|
|
312
|
-
`Protocol V2 write timeout after ${PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS}ms for ${name}`
|
|
313
|
-
)
|
|
314
|
-
);
|
|
220
|
+
await writeFrame(frame, callContext);
|
|
315
221
|
|
|
316
222
|
// Cancellation flag for the read loop: when the response timeout fires,
|
|
317
223
|
// Promise.race alone would leave this loop running as a zombie that keeps
|
|
@@ -324,49 +230,42 @@ export class ProtocolV2Session {
|
|
|
324
230
|
// terminal response. Consume those frames here so callers still see a
|
|
325
231
|
// request/terminal-response shaped API.
|
|
326
232
|
while (!cancellation.cancelled) {
|
|
327
|
-
const rxFrame = await readFrame();
|
|
233
|
+
const rxFrame = await readFrame(callContext);
|
|
328
234
|
if (cancellation.cancelled) {
|
|
329
235
|
// Timed out while waiting: drop the late frame and stop reading.
|
|
330
236
|
break;
|
|
331
237
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return response;
|
|
364
|
-
} else if (!shouldReduceDebug) {
|
|
365
|
-
logger?.debug?.(
|
|
366
|
-
`[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join(
|
|
367
|
-
'|'
|
|
368
|
-
)} got=${response.type}`
|
|
369
|
-
);
|
|
238
|
+
const isAck = ProtocolV2.isAckFrame(rxFrame);
|
|
239
|
+
if (!isAck) {
|
|
240
|
+
// Suppress Protocol V2 RX frame logs to avoid excessive transport diagnostics.
|
|
241
|
+
/*
|
|
242
|
+
const rxMetadata = ProtocolV2.inspectFrame(schemas, rxFrame);
|
|
243
|
+
|
|
244
|
+
if (!shouldReduceDebug) {
|
|
245
|
+
logger?.debug?.(`[${logPrefix}] RX`, {
|
|
246
|
+
method: name,
|
|
247
|
+
type: rxMetadata.type,
|
|
248
|
+
typeId: rxMetadata.messageTypeId,
|
|
249
|
+
seq: rxMetadata.seq,
|
|
250
|
+
bytes: rxFrame.length,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
*/
|
|
254
|
+
|
|
255
|
+
const decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
|
|
256
|
+
|
|
257
|
+
const response = check.call(decoded);
|
|
258
|
+
if (callOptions.intermediateTypes?.includes(response.type)) {
|
|
259
|
+
callOptions.onIntermediateResponse?.(response);
|
|
260
|
+
} else if (isExpectedTerminalResponse(response, callOptions.expectedTypes)) {
|
|
261
|
+
return response;
|
|
262
|
+
} else if (!shouldReduceDebug) {
|
|
263
|
+
logger?.debug?.(
|
|
264
|
+
`[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join(
|
|
265
|
+
'|'
|
|
266
|
+
)} got=${response.type}`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
370
269
|
}
|
|
371
270
|
}
|
|
372
271
|
// Only reachable after cancellation; the outer promise has already been
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { ProtocolV2FrameAssembler } from './frame-assembler';
|
|
2
|
+
import { ProtocolV2LinkManager } from './link-manager';
|
|
3
|
+
|
|
4
|
+
import type { MessageFromOneKey, TransportCallOptions } from '../../types';
|
|
5
|
+
import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session';
|
|
6
|
+
|
|
7
|
+
export type ProtocolV2UsbTransportBaseOptions = {
|
|
8
|
+
router: number;
|
|
9
|
+
maxFrameBytes: number;
|
|
10
|
+
logPrefix: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type ProtocolV2UsbCancellation = {
|
|
14
|
+
generation: number;
|
|
15
|
+
reason?: string;
|
|
16
|
+
promise: Promise<string>;
|
|
17
|
+
cancel: (reason: string) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export abstract class ProtocolV2UsbTransportBase<Key> {
|
|
21
|
+
private readonly protocolV2UsbLinks: ProtocolV2LinkManager<Key>;
|
|
22
|
+
|
|
23
|
+
private readonly protocolV2UsbAssemblers = new Map<Key, ProtocolV2FrameAssembler>();
|
|
24
|
+
|
|
25
|
+
private readonly protocolV2UsbGenerations = new Map<Key, number>();
|
|
26
|
+
|
|
27
|
+
private readonly protocolV2UsbCancellations = new Map<Key, ProtocolV2UsbCancellation>();
|
|
28
|
+
|
|
29
|
+
private readonly protocolV2UsbOptions: ProtocolV2UsbTransportBaseOptions;
|
|
30
|
+
|
|
31
|
+
protected constructor(options: ProtocolV2UsbTransportBaseOptions) {
|
|
32
|
+
this.protocolV2UsbOptions = options;
|
|
33
|
+
this.protocolV2UsbLinks = new ProtocolV2LinkManager<Key>({
|
|
34
|
+
getSchemas: () => this.getProtocolV2UsbSchemas(),
|
|
35
|
+
classifyError: () => 'link-fatal',
|
|
36
|
+
onLinkInvalidated: async (key, reason) => {
|
|
37
|
+
this.protocolV2UsbAssemblers.get(key)?.reset();
|
|
38
|
+
await this.resetProtocolV2UsbNativeLink(key, reason);
|
|
39
|
+
await this.onProtocolV2UsbLinkInvalidated(key, reason);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected abstract getProtocolV2UsbSchemas(): ProtocolV2Schemas;
|
|
45
|
+
|
|
46
|
+
protected abstract getProtocolV2UsbLogger(): ProtocolV2SessionOptions['logger'];
|
|
47
|
+
|
|
48
|
+
protected abstract writeProtocolV2UsbPacket(
|
|
49
|
+
key: Key,
|
|
50
|
+
frame: Uint8Array,
|
|
51
|
+
context: ProtocolV2CallContext
|
|
52
|
+
): Promise<void>;
|
|
53
|
+
|
|
54
|
+
protected abstract readProtocolV2UsbPacket(
|
|
55
|
+
key: Key,
|
|
56
|
+
context: ProtocolV2CallContext
|
|
57
|
+
): Promise<Uint8Array>;
|
|
58
|
+
|
|
59
|
+
protected abstract resetProtocolV2UsbNativeLink(key: Key, reason: string): Promise<void>;
|
|
60
|
+
|
|
61
|
+
protected abstract createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error;
|
|
62
|
+
|
|
63
|
+
protected onProtocolV2UsbLinkInvalidated(_key: Key, _reason: string): Promise<void> | void {
|
|
64
|
+
// Subclasses may clear protocol caches or emit platform-specific diagnostics.
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected async rotateProtocolV2UsbGeneration(key: Key, reason: string): Promise<number> {
|
|
68
|
+
await this.protocolV2UsbLinks.invalidateLink(key, reason);
|
|
69
|
+
const generation = (this.protocolV2UsbGenerations.get(key) ?? 0) + 1;
|
|
70
|
+
this.protocolV2UsbGenerations.set(key, generation);
|
|
71
|
+
this.protocolV2UsbCancellations.set(key, this.createProtocolV2UsbCancellation(generation));
|
|
72
|
+
this.protocolV2UsbAssemblers.set(
|
|
73
|
+
key,
|
|
74
|
+
new ProtocolV2FrameAssembler(this.protocolV2UsbOptions.maxFrameBytes)
|
|
75
|
+
);
|
|
76
|
+
return generation;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
protected callProtocolV2Usb(
|
|
80
|
+
key: Key,
|
|
81
|
+
name: string,
|
|
82
|
+
data: Record<string, unknown>,
|
|
83
|
+
options?: TransportCallOptions
|
|
84
|
+
): Promise<MessageFromOneKey> {
|
|
85
|
+
return this.protocolV2UsbLinks.call(
|
|
86
|
+
key,
|
|
87
|
+
() => this.createProtocolV2UsbAdapter(key),
|
|
88
|
+
name,
|
|
89
|
+
data,
|
|
90
|
+
options
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
protected invalidateProtocolV2UsbLink(key: Key, reason: string): Promise<void> {
|
|
95
|
+
return this.protocolV2UsbLinks.invalidateLink(key, reason);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
protected invalidateAllProtocolV2UsbLinks(reason: string): Promise<void> {
|
|
99
|
+
return this.protocolV2UsbLinks.invalidateAllLinks(reason);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
protected async disposeProtocolV2UsbLinks(reason: string): Promise<void> {
|
|
103
|
+
await this.protocolV2UsbLinks.dispose(reason);
|
|
104
|
+
this.protocolV2UsbAssemblers.clear();
|
|
105
|
+
this.protocolV2UsbGenerations.clear();
|
|
106
|
+
this.protocolV2UsbCancellations.clear();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private createProtocolV2UsbAdapter(key: Key) {
|
|
110
|
+
const generation = this.protocolV2UsbGenerations.get(key);
|
|
111
|
+
if (generation === undefined) {
|
|
112
|
+
throw new Error('Protocol V2 USB generation has not been initialized');
|
|
113
|
+
}
|
|
114
|
+
const cancellation = this.protocolV2UsbCancellations.get(key);
|
|
115
|
+
if (!cancellation || cancellation.generation !== generation) {
|
|
116
|
+
throw new Error('Protocol V2 USB cancellation state has not been initialized');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const assertCurrentGeneration = () => {
|
|
120
|
+
if (cancellation.reason) {
|
|
121
|
+
throw new Error(cancellation.reason);
|
|
122
|
+
}
|
|
123
|
+
if (this.protocolV2UsbGenerations.get(key) !== generation) {
|
|
124
|
+
throw new Error('Protocol V2 USB connection generation changed');
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
router: this.protocolV2UsbOptions.router,
|
|
130
|
+
maxFrameBytes: this.protocolV2UsbOptions.maxFrameBytes,
|
|
131
|
+
generation,
|
|
132
|
+
prepareCall: () => {
|
|
133
|
+
assertCurrentGeneration();
|
|
134
|
+
this.getProtocolV2UsbAssembler(key).reset();
|
|
135
|
+
},
|
|
136
|
+
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
137
|
+
assertCurrentGeneration();
|
|
138
|
+
await this.writeProtocolV2UsbPacket(key, frame, context);
|
|
139
|
+
assertCurrentGeneration();
|
|
140
|
+
},
|
|
141
|
+
readFrame: async (context: ProtocolV2CallContext) => {
|
|
142
|
+
assertCurrentGeneration();
|
|
143
|
+
const assembler = this.getProtocolV2UsbAssembler(key);
|
|
144
|
+
let frame = assembler.push(new Uint8Array(0));
|
|
145
|
+
while (!frame) {
|
|
146
|
+
const packetRead = this.readProtocolV2UsbPacket(key, context).then(packet => ({
|
|
147
|
+
packet,
|
|
148
|
+
}));
|
|
149
|
+
const cancelled = cancellation.promise.then(reason => ({ reason }));
|
|
150
|
+
const result = await Promise.race([packetRead, cancelled]);
|
|
151
|
+
if ('reason' in result) {
|
|
152
|
+
throw new Error(result.reason);
|
|
153
|
+
}
|
|
154
|
+
assertCurrentGeneration();
|
|
155
|
+
frame = assembler.push(result.packet);
|
|
156
|
+
}
|
|
157
|
+
assertCurrentGeneration();
|
|
158
|
+
return frame;
|
|
159
|
+
},
|
|
160
|
+
reset: (reason: string) => {
|
|
161
|
+
cancellation.cancel(reason);
|
|
162
|
+
this.protocolV2UsbAssemblers.get(key)?.reset();
|
|
163
|
+
},
|
|
164
|
+
logger: this.getProtocolV2UsbLogger(),
|
|
165
|
+
logPrefix: this.protocolV2UsbOptions.logPrefix,
|
|
166
|
+
createTimeoutError: (messageName: string, timeoutMs: number) =>
|
|
167
|
+
this.createProtocolV2UsbTimeoutError(messageName, timeoutMs),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private getProtocolV2UsbAssembler(key: Key): ProtocolV2FrameAssembler {
|
|
172
|
+
const assembler = this.protocolV2UsbAssemblers.get(key);
|
|
173
|
+
if (!assembler) {
|
|
174
|
+
throw new Error('Protocol V2 USB assembler has not been initialized');
|
|
175
|
+
}
|
|
176
|
+
return assembler;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private createProtocolV2UsbCancellation(generation: number): ProtocolV2UsbCancellation {
|
|
180
|
+
let resolveCancellation: (reason: string) => void = () => undefined;
|
|
181
|
+
const cancellation: ProtocolV2UsbCancellation = {
|
|
182
|
+
generation,
|
|
183
|
+
promise: new Promise(resolve => {
|
|
184
|
+
resolveCancellation = resolve;
|
|
185
|
+
}),
|
|
186
|
+
cancel: reason => {
|
|
187
|
+
if (cancellation.reason) return;
|
|
188
|
+
cancellation.reason = reason;
|
|
189
|
+
resolveCancellation(reason);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
return cancellation;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -70,7 +70,10 @@ function messageToJSON(Message: Message<Record<string, unknown>>, fields: Type['
|
|
|
70
70
|
}
|
|
71
71
|
// message type
|
|
72
72
|
else if (field.resolvedType!.fields) {
|
|
73
|
-
|
|
73
|
+
// [compatibility]: absent optional sub-messages are not own properties
|
|
74
|
+
// of the decoded instance, so value is undefined here; decode them to
|
|
75
|
+
// null, matching the optional primitive-field convention above.
|
|
76
|
+
res[key] = value == null ? null : messageToJSON(value, (field.resolvedType as Type).fields);
|
|
74
77
|
} else {
|
|
75
78
|
throw new Error(`case not handled: ${key}`);
|
|
76
79
|
}
|
|
@@ -30,7 +30,12 @@ export const createMessageFromName = (messages: protobuf.Root, name: string) =>
|
|
|
30
30
|
export const createMessageFromType = (messages: protobuf.Root, typeId: number) => {
|
|
31
31
|
const MessageType = messages.lookupEnum('MessageType');
|
|
32
32
|
|
|
33
|
-
const
|
|
33
|
+
const rawMessageName = MessageType.valuesById[typeId];
|
|
34
|
+
if (!rawMessageName) {
|
|
35
|
+
throw new Error(`MessageType id "${typeId}" is not defined in protobuf schema`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const messageName = rawMessageName.replace('MessageType_', '');
|
|
34
39
|
|
|
35
40
|
const Message = messages.lookupType(messageName);
|
|
36
41
|
|