@onekeyfe/hd-transport 1.2.0-alpha.9 → 1.2.0-alpha.91
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__/messages.test.js +169 -11
- package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
- package/__tests__/protocol-v2-link-manager.test.js +120 -9
- package/__tests__/protocol-v2-usb-transport-base.test.js +36 -2
- package/__tests__/protocol-v2.test.js +438 -64
- package/__tests__/transport-log.test.js +79 -0
- package/dist/constants.d.ts +3 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +707 -242
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +597 -296
- package/dist/protocols/index.d.ts +13 -6
- package/dist/protocols/index.d.ts.map +1 -1
- package/dist/protocols/v2/ble-frame-writer.d.ts +15 -0
- package/dist/protocols/v2/ble-frame-writer.d.ts.map +1 -0
- package/dist/protocols/v2/decode.d.ts +12 -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/errors.d.ts +8 -0
- package/dist/protocols/v2/errors.d.ts.map +1 -0
- package/dist/protocols/v2/frame-assembler.d.ts.map +1 -1
- package/dist/protocols/v2/index.d.ts +2 -1
- package/dist/protocols/v2/index.d.ts.map +1 -1
- package/dist/protocols/v2/link-manager.d.ts +4 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -1
- package/dist/protocols/v2/session.d.ts +10 -4
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +1 -0
- package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -1
- package/dist/serialization/protobuf/decode.d.ts.map +1 -1
- package/dist/serialization/protobuf/encode.d.ts.map +1 -1
- package/dist/types/messages.d.ts +370 -144
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +20 -3
- package/dist/types/transport.d.ts.map +1 -1
- package/dist/utils/transportLog.d.ts +9 -0
- package/dist/utils/transportLog.d.ts.map +1 -0
- package/messages-protocol-v2.json +1113 -547
- package/package.json +3 -3
- package/scripts/protobuf-build.sh +85 -253
- package/scripts/protobuf-patches/index.js +1 -0
- package/scripts/protobuf-types.js +19 -1
- package/src/constants.ts +30 -16
- package/src/index.ts +5 -1
- package/src/protocols/index.ts +37 -40
- package/src/protocols/v2/ble-frame-writer.ts +77 -0
- package/src/protocols/v2/crc8.ts +1 -1
- package/src/protocols/v2/decode.ts +50 -35
- package/src/protocols/v2/encode.ts +2 -28
- package/src/protocols/v2/errors.ts +25 -0
- package/src/protocols/v2/frame-assembler.ts +6 -4
- package/src/protocols/v2/index.ts +2 -1
- package/src/protocols/v2/link-manager.ts +54 -5
- package/src/protocols/v2/session.ts +151 -200
- package/src/protocols/v2/usb-transport-base.ts +29 -10
- package/src/serialization/protobuf/decode.ts +4 -1
- package/src/serialization/protobuf/encode.ts +5 -1
- package/src/types/messages.ts +450 -185
- package/src/types/transport.ts +28 -3
- package/src/utils/transportLog.ts +102 -0
- 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,12 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
3
|
+
PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
4
|
+
} from '../../constants';
|
|
2
5
|
import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
|
|
3
6
|
import { ProtocolV2SequenceCursor } from './sequence-cursor';
|
|
7
|
+
import { ProtocolV2LinkError } from './errors';
|
|
4
8
|
import { ProtocolV2 } from '..';
|
|
5
9
|
import * as check from '../../utils/highlevel-checks';
|
|
6
10
|
import { LogBlockCommand } from '../../utils/logBlockCommand';
|
|
7
11
|
|
|
8
12
|
import type { Root } from 'protobufjs/light';
|
|
9
|
-
import type { MessageFromOneKey } from '../../types';
|
|
13
|
+
import type { MessageFromOneKey, TransportWriteMetrics } from '../../types';
|
|
14
|
+
|
|
15
|
+
export * from './errors';
|
|
10
16
|
|
|
11
17
|
export type ProtocolV2Schemas = {
|
|
12
18
|
protocolV1: Root;
|
|
@@ -16,8 +22,10 @@ export type ProtocolV2Schemas = {
|
|
|
16
22
|
export type ProtocolV2CallContext = {
|
|
17
23
|
messageName: string;
|
|
18
24
|
timeoutMs?: number;
|
|
19
|
-
|
|
25
|
+
highThroughput: boolean;
|
|
26
|
+
writeWithResponse?: boolean;
|
|
20
27
|
generation: number;
|
|
28
|
+
signal: AbortSignal;
|
|
21
29
|
};
|
|
22
30
|
|
|
23
31
|
type ProtocolLogger = {
|
|
@@ -45,6 +53,8 @@ export type ProtocolV2CallOptions = {
|
|
|
45
53
|
expectedTypes?: string[];
|
|
46
54
|
intermediateTypes?: string[];
|
|
47
55
|
onIntermediateResponse?: (response: MessageFromOneKey) => void;
|
|
56
|
+
onWriteCompleted?: (metrics: TransportWriteMetrics) => void;
|
|
57
|
+
writeWithResponse?: boolean;
|
|
48
58
|
};
|
|
49
59
|
|
|
50
60
|
export { concatUint8Arrays, ProtocolV2FrameAssembler };
|
|
@@ -71,16 +81,7 @@ export function bytesToHex(bytes: Uint8Array): string {
|
|
|
71
81
|
.join('');
|
|
72
82
|
}
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
// The rest of the frame is protobuf payload and may contain sensitive fields
|
|
76
|
-
// (mnemonic words via WordAck, PINs, seeds via LoadDevice, ...), so raw frame
|
|
77
|
-
// hex logs must never include it.
|
|
78
|
-
const PROTOCOL_V2_DEBUG_HEADER_BYTES = 7;
|
|
79
|
-
const PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT = 20;
|
|
80
|
-
const PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT = 40;
|
|
81
|
-
const PROTOCOL_V2_DEBUG_STRING_LIMIT = 512;
|
|
82
|
-
const PROTOCOL_V2_DEBUG_DEPTH_LIMIT = 4;
|
|
83
|
-
const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
|
|
84
|
+
const REDUCED_DEBUG_PROTOCOL_V2_CALLS = new Set([
|
|
84
85
|
...LogBlockCommand,
|
|
85
86
|
'FilesystemFileRead',
|
|
86
87
|
'FileRead',
|
|
@@ -88,103 +89,22 @@ const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
|
|
|
88
89
|
]);
|
|
89
90
|
|
|
90
91
|
function shouldReduceProtocolV2Debug(name: string) {
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function frameHeaderDebugHex(frame: Uint8Array): string {
|
|
95
|
-
// Only the frame header is dumped as hex; the payload is logged separately
|
|
96
|
-
// in sanitized/structured form (sanitizeProtocolV2DebugPayload).
|
|
97
|
-
return bytesToHex(frame.slice(0, PROTOCOL_V2_DEBUG_HEADER_BYTES));
|
|
92
|
+
return REDUCED_DEBUG_PROTOCOL_V2_CALLS.has(name);
|
|
98
93
|
}
|
|
99
94
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return value.size;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function summarizeRedactedData(value: unknown): string {
|
|
117
|
-
const byteLength = getBinaryByteLength(value);
|
|
118
|
-
if (byteLength !== undefined) {
|
|
119
|
-
return `[redacted data: ${byteLength} bytes]`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (typeof value === 'string') {
|
|
123
|
-
return `[redacted data: string length=${value.length}]`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (Array.isArray(value)) {
|
|
127
|
-
return `[redacted data: array length=${value.length}]`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (value && typeof value === 'object') {
|
|
131
|
-
return `[redacted data: object keys=${Object.keys(value).length}]`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return `[redacted data: ${typeof value}]`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function sanitizeProtocolV2DebugPayload(value: unknown, key = '', depth = 0): unknown {
|
|
138
|
-
if (/^(data|payload)$/i.test(key) && value !== null && value !== undefined) {
|
|
139
|
-
return summarizeRedactedData(value);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (/(passphrase|pin|mnemonic|seed|private)/i.test(key)) {
|
|
143
|
-
return '[redacted sensitive value]';
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const byteLength = getBinaryByteLength(value);
|
|
147
|
-
if (byteLength !== undefined) {
|
|
148
|
-
return `[binary: ${byteLength} bytes]`;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (typeof value === 'string') {
|
|
152
|
-
return value.length > PROTOCOL_V2_DEBUG_STRING_LIMIT
|
|
153
|
-
? `${value.slice(0, PROTOCOL_V2_DEBUG_STRING_LIMIT)}... (len=${value.length})`
|
|
154
|
-
: value;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (!value || typeof value !== 'object') {
|
|
158
|
-
return value;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (depth >= PROTOCOL_V2_DEBUG_DEPTH_LIMIT) {
|
|
162
|
-
return Array.isArray(value)
|
|
163
|
-
? `[array length=${value.length}]`
|
|
164
|
-
: `[object keys=${Object.keys(value).length}]`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (Array.isArray(value)) {
|
|
168
|
-
const items = value
|
|
169
|
-
.slice(0, PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT)
|
|
170
|
-
.map(item => sanitizeProtocolV2DebugPayload(item, key, depth + 1));
|
|
171
|
-
if (value.length > PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT) {
|
|
172
|
-
items.push(`... (${value.length - PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT} more)`);
|
|
173
|
-
}
|
|
174
|
-
return items;
|
|
175
|
-
}
|
|
95
|
+
const HIGH_THROUGHPUT_PROTOCOL_V2_CALLS = new Set([
|
|
96
|
+
'FilesystemFileWrite',
|
|
97
|
+
'FileWrite',
|
|
98
|
+
'EmmcFileWrite',
|
|
99
|
+
'FirmwareUpload',
|
|
100
|
+
'ResourceAck',
|
|
101
|
+
'FilesystemFileRead',
|
|
102
|
+
'FileRead',
|
|
103
|
+
'EmmcFileRead',
|
|
104
|
+
]);
|
|
176
105
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
entries.forEach(([entryKey, entryValue]) => {
|
|
180
|
-
sanitized[entryKey] = sanitizeProtocolV2DebugPayload(entryValue, entryKey, depth + 1);
|
|
181
|
-
});
|
|
182
|
-
if (Object.keys(value).length > PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT) {
|
|
183
|
-
sanitized.__truncated__ = `${
|
|
184
|
-
Object.keys(value).length - PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT
|
|
185
|
-
} more keys`;
|
|
186
|
-
}
|
|
187
|
-
return sanitized;
|
|
106
|
+
export function isProtocolV2HighThroughputCall(name: string) {
|
|
107
|
+
return HIGH_THROUGHPUT_PROTOCOL_V2_CALLS.has(name);
|
|
188
108
|
}
|
|
189
109
|
|
|
190
110
|
const COMMON_TERMINAL_RESPONSE_TYPES = new Set([
|
|
@@ -219,11 +139,13 @@ export async function withProtocolTimeout<T>(
|
|
|
219
139
|
promise: Promise<T>,
|
|
220
140
|
timeoutMs: number | undefined,
|
|
221
141
|
createTimeoutError: () => Error,
|
|
222
|
-
onTimeout?: () => void
|
|
142
|
+
onTimeout?: () => void,
|
|
143
|
+
abortSignal?: AbortSignal
|
|
223
144
|
): Promise<T> {
|
|
224
145
|
if (!timeoutMs) return promise;
|
|
225
146
|
|
|
226
147
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
148
|
+
let abortHandler: (() => void) | undefined;
|
|
227
149
|
try {
|
|
228
150
|
return await Promise.race([
|
|
229
151
|
promise,
|
|
@@ -235,17 +157,29 @@ export async function withProtocolTimeout<T>(
|
|
|
235
157
|
reject(createTimeoutError());
|
|
236
158
|
}, timeoutMs);
|
|
237
159
|
}),
|
|
160
|
+
...(abortSignal
|
|
161
|
+
? [
|
|
162
|
+
new Promise<never>((_, reject) => {
|
|
163
|
+
abortHandler = () => {
|
|
164
|
+
reject(new Error('Protocol V2 operation aborted'));
|
|
165
|
+
};
|
|
166
|
+
if (abortSignal.aborted) {
|
|
167
|
+
abortHandler();
|
|
168
|
+
} else {
|
|
169
|
+
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
170
|
+
}
|
|
171
|
+
}),
|
|
172
|
+
]
|
|
173
|
+
: []),
|
|
238
174
|
]);
|
|
239
175
|
} finally {
|
|
240
176
|
if (timer) clearTimeout(timer);
|
|
177
|
+
if (abortHandler) {
|
|
178
|
+
abortSignal?.removeEventListener('abort', abortHandler);
|
|
179
|
+
}
|
|
241
180
|
}
|
|
242
181
|
}
|
|
243
182
|
|
|
244
|
-
// Write completion is owned by the concrete transport. A Promise.race watchdog
|
|
245
|
-
// here can return while libusb/WebUSB is still flushing a large Protocol V2
|
|
246
|
-
// frame, which desynchronizes later request/response pairs.
|
|
247
|
-
export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 0;
|
|
248
|
-
|
|
249
183
|
export class ProtocolV2Session {
|
|
250
184
|
private readonly options: ProtocolV2SessionOptions;
|
|
251
185
|
|
|
@@ -255,6 +189,8 @@ export class ProtocolV2Session {
|
|
|
255
189
|
// in-flight calls on the same session would steal each other's responses.
|
|
256
190
|
private pendingCall: Promise<unknown> = Promise.resolve();
|
|
257
191
|
|
|
192
|
+
private lastResponseSequence?: number;
|
|
193
|
+
|
|
258
194
|
constructor(options: ProtocolV2SessionOptions) {
|
|
259
195
|
this.options = options;
|
|
260
196
|
this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor();
|
|
@@ -273,7 +209,7 @@ export class ProtocolV2Session {
|
|
|
273
209
|
return result;
|
|
274
210
|
}
|
|
275
211
|
|
|
276
|
-
private
|
|
212
|
+
private executeCall(
|
|
277
213
|
name: string,
|
|
278
214
|
data: Record<string, unknown>,
|
|
279
215
|
callOptions: ProtocolV2CallOptions
|
|
@@ -291,104 +227,117 @@ export class ProtocolV2Session {
|
|
|
291
227
|
createTimeoutError,
|
|
292
228
|
generation = 0,
|
|
293
229
|
} = this.options;
|
|
230
|
+
const timeoutMs =
|
|
231
|
+
callOptions.timeoutMs && callOptions.timeoutMs > 0
|
|
232
|
+
? callOptions.timeoutMs
|
|
233
|
+
: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
|
|
234
|
+
const abortController = new AbortController();
|
|
294
235
|
|
|
295
236
|
const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
|
|
296
|
-
const
|
|
237
|
+
const baseCallContext: ProtocolV2CallContext = {
|
|
297
238
|
messageName: name,
|
|
298
|
-
timeoutMs
|
|
299
|
-
|
|
239
|
+
timeoutMs,
|
|
240
|
+
highThroughput: isProtocolV2HighThroughputCall(name),
|
|
241
|
+
...(callOptions.writeWithResponse === undefined
|
|
242
|
+
? {}
|
|
243
|
+
: { writeWithResponse: callOptions.writeWithResponse }),
|
|
300
244
|
generation,
|
|
245
|
+
signal: abortController.signal,
|
|
301
246
|
};
|
|
302
|
-
await prepareCall?.(callContext);
|
|
303
|
-
const protoSeq = this.sequenceCursor.next();
|
|
304
|
-
const frame = ProtocolV2.encodeFrame(schemas, name, data, {
|
|
305
|
-
packetSrc,
|
|
306
|
-
router,
|
|
307
|
-
seq: protoSeq,
|
|
308
|
-
logger: shouldReduceDebug ? undefined : logger,
|
|
309
|
-
logPrefix,
|
|
310
|
-
context: `tx:${name}`,
|
|
311
|
-
});
|
|
312
|
-
const expectedSeq = frame[6];
|
|
313
|
-
|
|
314
|
-
if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
|
|
315
|
-
throw new Error(
|
|
316
|
-
`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
247
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
await withProtocolTimeout(
|
|
335
|
-
writeFrame(frame, callContext),
|
|
336
|
-
PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS,
|
|
337
|
-
() =>
|
|
338
|
-
new Error(
|
|
339
|
-
`Protocol V2 write timeout after ${PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS}ms for ${name}`
|
|
340
|
-
)
|
|
341
|
-
);
|
|
248
|
+
const runCall = async (): Promise<MessageFromOneKey> => {
|
|
249
|
+
await prepareCall?.(baseCallContext);
|
|
250
|
+
const protoSeq = this.sequenceCursor.next();
|
|
251
|
+
const frame = ProtocolV2.encodeFrame(schemas, name, data, {
|
|
252
|
+
packetSrc,
|
|
253
|
+
router,
|
|
254
|
+
seq: protoSeq,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
342
262
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
263
|
+
const writeStartedAt = Date.now();
|
|
264
|
+
await writeFrame(frame, baseCallContext);
|
|
265
|
+
try {
|
|
266
|
+
callOptions.onWriteCompleted?.({
|
|
267
|
+
elapsedMs: Math.max(Date.now() - writeStartedAt, 0),
|
|
268
|
+
frameBytes: frame.byteLength,
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
logger?.error?.(`${logPrefix} write metrics callback failed: ${String(error)}`);
|
|
272
|
+
}
|
|
348
273
|
|
|
349
|
-
const readResponse = async (): Promise<MessageFromOneKey> => {
|
|
350
274
|
// Some Protocol V2 operations emit progress notifications before the
|
|
351
275
|
// terminal response. Consume those frames here so callers still see a
|
|
352
276
|
// request/terminal-response shaped API.
|
|
353
|
-
while (!
|
|
354
|
-
const rxFrame = await readFrame(
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
|
|
277
|
+
while (!abortController.signal.aborted) {
|
|
278
|
+
const rxFrame = await readFrame(baseCallContext);
|
|
279
|
+
if (abortController.signal.aborted) break;
|
|
280
|
+
|
|
281
|
+
let header: ReturnType<typeof ProtocolV2.inspectFrameHeader>;
|
|
282
|
+
try {
|
|
283
|
+
header = ProtocolV2.inspectFrameHeader(rxFrame);
|
|
284
|
+
} catch (cause) {
|
|
285
|
+
throw new ProtocolV2LinkError(
|
|
286
|
+
'frame',
|
|
287
|
+
`Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
|
|
288
|
+
cause
|
|
289
|
+
);
|
|
358
290
|
}
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
} seq=${rxFrame[6]} headerHex=${frameHeaderDebugHex(rxFrame)}`
|
|
291
|
+
if (header.router !== router) {
|
|
292
|
+
throw new ProtocolV2LinkError(
|
|
293
|
+
'router',
|
|
294
|
+
`Protocol V2 router mismatch: expected ${router}, got ${header.router}`
|
|
364
295
|
);
|
|
365
296
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
297
|
+
if (header.packetSrc !== packetSrc) {
|
|
298
|
+
throw new ProtocolV2LinkError(
|
|
299
|
+
'packet-source',
|
|
300
|
+
`Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
|
|
301
|
+
);
|
|
371
302
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
303
|
+
|
|
304
|
+
let isAck: boolean;
|
|
305
|
+
try {
|
|
306
|
+
isAck = ProtocolV2.isAckFrame(rxFrame);
|
|
307
|
+
} catch (cause) {
|
|
308
|
+
throw new ProtocolV2LinkError(
|
|
309
|
+
'frame',
|
|
310
|
+
`Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
|
|
311
|
+
cause
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (isAck) {
|
|
315
|
+
if (header.seq !== protoSeq) {
|
|
316
|
+
throw new ProtocolV2LinkError(
|
|
317
|
+
'ack-sequence',
|
|
318
|
+
`Protocol V2 ACK sequence mismatch: expected ${protoSeq}, got ${header.seq}`
|
|
381
319
|
);
|
|
382
320
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
321
|
+
} else {
|
|
322
|
+
let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
|
|
323
|
+
try {
|
|
324
|
+
decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
|
|
325
|
+
} catch (cause) {
|
|
326
|
+
throw new ProtocolV2LinkError(
|
|
327
|
+
'frame',
|
|
328
|
+
`Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
|
|
329
|
+
cause
|
|
386
330
|
);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
331
|
+
}
|
|
332
|
+
// Firmware owns one TX sequence across all channels and packet sources,
|
|
333
|
+
// so frames routed elsewhere create valid gaps in this session.
|
|
334
|
+
if (this.lastResponseSequence === decoded.seq) {
|
|
335
|
+
throw new ProtocolV2LinkError(
|
|
336
|
+
'response-sequence',
|
|
337
|
+
`Protocol V2 duplicate response sequence: ${decoded.seq}`
|
|
390
338
|
);
|
|
391
339
|
}
|
|
340
|
+
this.lastResponseSequence = decoded.seq;
|
|
392
341
|
|
|
393
342
|
const response = check.call(decoded);
|
|
394
343
|
if (callOptions.intermediateTypes?.includes(response.type)) {
|
|
@@ -404,20 +353,19 @@ export class ProtocolV2Session {
|
|
|
404
353
|
}
|
|
405
354
|
}
|
|
406
355
|
}
|
|
407
|
-
|
|
408
|
-
// rejected by the timeout, so this rejection is consumed by the race.
|
|
356
|
+
|
|
409
357
|
throw new Error(`Protocol V2 read loop cancelled after timeout for ${name}`);
|
|
410
358
|
};
|
|
411
359
|
|
|
412
360
|
return withProtocolTimeout(
|
|
413
|
-
|
|
414
|
-
|
|
361
|
+
runCall(),
|
|
362
|
+
timeoutMs,
|
|
415
363
|
() =>
|
|
416
364
|
createTimeoutError
|
|
417
|
-
? createTimeoutError(name,
|
|
418
|
-
: new Error(`Protocol V2 response timeout after ${
|
|
365
|
+
? createTimeoutError(name, timeoutMs)
|
|
366
|
+
: new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
|
|
419
367
|
() => {
|
|
420
|
-
|
|
368
|
+
abortController.abort();
|
|
421
369
|
}
|
|
422
370
|
);
|
|
423
371
|
}
|
|
@@ -448,7 +396,10 @@ export async function probeProtocolV2({
|
|
|
448
396
|
const response = await call(
|
|
449
397
|
'Ping',
|
|
450
398
|
{ message: 'protocol-v2-probe' },
|
|
451
|
-
{
|
|
399
|
+
{
|
|
400
|
+
timeoutMs,
|
|
401
|
+
expectedTypes: ['Success'],
|
|
402
|
+
}
|
|
452
403
|
);
|
|
453
404
|
if (response.type === 'Success') {
|
|
454
405
|
return true;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ProtocolV2FrameAssembler } from './frame-assembler';
|
|
2
|
+
import { ProtocolV2LinkError, isProtocolV2LinkError } from './errors';
|
|
2
3
|
import { ProtocolV2LinkManager } from './link-manager';
|
|
4
|
+
import { getErrorMessage } from './session';
|
|
3
5
|
|
|
4
6
|
import type { MessageFromOneKey, TransportCallOptions } from '../../types';
|
|
5
7
|
import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session';
|
|
@@ -32,7 +34,7 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
|
|
|
32
34
|
this.protocolV2UsbOptions = options;
|
|
33
35
|
this.protocolV2UsbLinks = new ProtocolV2LinkManager<Key>({
|
|
34
36
|
getSchemas: () => this.getProtocolV2UsbSchemas(),
|
|
35
|
-
classifyError: ()
|
|
37
|
+
classifyError: error => (isProtocolV2LinkError(error) ? 'link-fatal' : 'recoverable'),
|
|
36
38
|
onLinkInvalidated: async (key, reason) => {
|
|
37
39
|
this.protocolV2UsbAssemblers.get(key)?.reset();
|
|
38
40
|
await this.resetProtocolV2UsbNativeLink(key, reason);
|
|
@@ -61,7 +63,7 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
|
|
|
61
63
|
protected abstract createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error;
|
|
62
64
|
|
|
63
65
|
protected onProtocolV2UsbLinkInvalidated(_key: Key, _reason: string): Promise<void> | void {
|
|
64
|
-
//
|
|
66
|
+
// Subclasses may clear protocol caches or emit platform-specific diagnostics.
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
protected async rotateProtocolV2UsbGeneration(key: Key, reason: string): Promise<number> {
|
|
@@ -118,10 +120,13 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
|
|
|
118
120
|
|
|
119
121
|
const assertCurrentGeneration = () => {
|
|
120
122
|
if (cancellation.reason) {
|
|
121
|
-
throw new
|
|
123
|
+
throw new ProtocolV2LinkError('generation', cancellation.reason);
|
|
122
124
|
}
|
|
123
125
|
if (this.protocolV2UsbGenerations.get(key) !== generation) {
|
|
124
|
-
throw new
|
|
126
|
+
throw new ProtocolV2LinkError(
|
|
127
|
+
'generation',
|
|
128
|
+
'Protocol V2 USB connection generation changed'
|
|
129
|
+
);
|
|
125
130
|
}
|
|
126
131
|
};
|
|
127
132
|
|
|
@@ -131,11 +136,14 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
|
|
|
131
136
|
generation,
|
|
132
137
|
prepareCall: () => {
|
|
133
138
|
assertCurrentGeneration();
|
|
134
|
-
this.getProtocolV2UsbAssembler(key).reset();
|
|
135
139
|
},
|
|
136
140
|
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
137
141
|
assertCurrentGeneration();
|
|
138
|
-
|
|
142
|
+
try {
|
|
143
|
+
await this.writeProtocolV2UsbPacket(key, frame, context);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw this.createProtocolV2UsbIoError('write', error);
|
|
146
|
+
}
|
|
139
147
|
assertCurrentGeneration();
|
|
140
148
|
},
|
|
141
149
|
readFrame: async (context: ProtocolV2CallContext) => {
|
|
@@ -143,13 +151,15 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
|
|
|
143
151
|
const assembler = this.getProtocolV2UsbAssembler(key);
|
|
144
152
|
let frame = assembler.push(new Uint8Array(0));
|
|
145
153
|
while (!frame) {
|
|
146
|
-
const packetRead = this.readProtocolV2UsbPacket(key, context)
|
|
147
|
-
packet
|
|
148
|
-
|
|
154
|
+
const packetRead = this.readProtocolV2UsbPacket(key, context)
|
|
155
|
+
.then(packet => ({ packet }))
|
|
156
|
+
.catch(error => {
|
|
157
|
+
throw this.createProtocolV2UsbIoError('read', error);
|
|
158
|
+
});
|
|
149
159
|
const cancelled = cancellation.promise.then(reason => ({ reason }));
|
|
150
160
|
const result = await Promise.race([packetRead, cancelled]);
|
|
151
161
|
if ('reason' in result) {
|
|
152
|
-
throw new
|
|
162
|
+
throw new ProtocolV2LinkError('generation', result.reason);
|
|
153
163
|
}
|
|
154
164
|
assertCurrentGeneration();
|
|
155
165
|
frame = assembler.push(result.packet);
|
|
@@ -191,4 +201,13 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
|
|
|
191
201
|
};
|
|
192
202
|
return cancellation;
|
|
193
203
|
}
|
|
204
|
+
|
|
205
|
+
private createProtocolV2UsbIoError(operation: 'read' | 'write', error: unknown) {
|
|
206
|
+
if (isProtocolV2LinkError(error)) return error;
|
|
207
|
+
return new ProtocolV2LinkError(
|
|
208
|
+
'io',
|
|
209
|
+
`Protocol V2 USB ${operation} failed: ${getErrorMessage(error) || 'unknown error'}`,
|
|
210
|
+
error
|
|
211
|
+
);
|
|
212
|
+
}
|
|
194
213
|
}
|
|
@@ -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
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Buffer } from 'buffer';
|
|
2
2
|
import ByteBuffer from 'bytebuffer';
|
|
3
|
+
import { Enum } from 'protobufjs/light';
|
|
3
4
|
|
|
4
5
|
import { isPrimitiveField } from '../../utils/protobuf';
|
|
5
6
|
|
|
@@ -49,7 +50,10 @@ export function patch(Message: Type, payload: any) {
|
|
|
49
50
|
// repeated
|
|
50
51
|
if (field.repeated) {
|
|
51
52
|
const RefMessage = Message.lookupTypeOrEnum(field.type);
|
|
52
|
-
patched[key] =
|
|
53
|
+
patched[key] =
|
|
54
|
+
RefMessage instanceof Enum
|
|
55
|
+
? value.map((v: any) => transform(field.type, v))
|
|
56
|
+
: value.map((v: any) => patch(RefMessage, v));
|
|
53
57
|
}
|
|
54
58
|
// message type
|
|
55
59
|
else if (typeof value === 'object' && value !== null) {
|