@onekeyfe/hd-transport 1.2.0-alpha.7 → 1.2.0-alpha.71
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 +220 -0
- package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
- package/__tests__/protocol-v2-link-manager.test.js +437 -0
- package/__tests__/protocol-v2-usb-transport-base.test.js +330 -0
- package/__tests__/protocol-v2.test.js +492 -53
- package/__tests__/transport-log.test.js +79 -0
- package/dist/constants.d.ts +4 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +810 -198
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +836 -228
- package/dist/protocols/index.d.ts +14 -5
- 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/constants.d.ts +1 -0
- package/dist/protocols/v2/constants.d.ts.map +1 -1
- package/dist/protocols/v2/decode.d.ts +13 -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 +39 -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 +23 -6
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +32 -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/encode.d.ts.map +1 -1
- package/dist/types/messages.d.ts +386 -110
- 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 +1138 -441
- package/package.json +3 -3
- package/scripts/protobuf-build.sh +86 -208
- package/scripts/protobuf-patches/index.js +1 -0
- package/scripts/protobuf-types.js +19 -1
- package/src/constants.ts +28 -14
- package/src/index.ts +13 -1
- package/src/protocols/index.ts +39 -40
- package/src/protocols/v2/ble-frame-writer.ts +77 -0
- package/src/protocols/v2/constants.ts +1 -0
- package/src/protocols/v2/crc8.ts +1 -1
- package/src/protocols/v2/decode.ts +71 -37
- 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 +209 -0
- package/src/protocols/v2/sequence-cursor.ts +10 -0
- package/src/protocols/v2/session.ts +173 -201
- package/src/protocols/v2/usb-transport-base.ts +213 -0
- package/src/serialization/protobuf/decode.ts +4 -1
- package/src/serialization/protobuf/encode.ts +5 -1
- package/src/types/messages.ts +471 -142
- package/src/types/transport.ts +23 -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,18 +1,33 @@
|
|
|
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
|
-
import {
|
|
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;
|
|
13
19
|
protocolV2: Root;
|
|
14
20
|
};
|
|
15
21
|
|
|
22
|
+
export type ProtocolV2CallContext = {
|
|
23
|
+
messageName: string;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
highVolume: boolean;
|
|
26
|
+
writeWithResponse?: boolean;
|
|
27
|
+
generation: number;
|
|
28
|
+
signal: AbortSignal;
|
|
29
|
+
};
|
|
30
|
+
|
|
16
31
|
type ProtocolLogger = {
|
|
17
32
|
debug?: (...args: any[]) => void;
|
|
18
33
|
error?: (...args: any[]) => void;
|
|
@@ -22,11 +37,15 @@ export type ProtocolV2SessionOptions = {
|
|
|
22
37
|
schemas: ProtocolV2Schemas;
|
|
23
38
|
router: number;
|
|
24
39
|
packetSrc?: number;
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
maxFrameBytes?: number;
|
|
41
|
+
prepareCall?: (context: ProtocolV2CallContext) => Promise<void> | void;
|
|
42
|
+
writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) => Promise<void>;
|
|
43
|
+
readFrame: (context: ProtocolV2CallContext) => Promise<Uint8Array>;
|
|
27
44
|
logger?: ProtocolLogger;
|
|
28
45
|
logPrefix?: string;
|
|
29
46
|
createTimeoutError?: (name: string, timeoutMs: number) => Error;
|
|
47
|
+
sequenceCursor?: ProtocolV2SequenceCursor;
|
|
48
|
+
generation?: number;
|
|
30
49
|
};
|
|
31
50
|
|
|
32
51
|
export type ProtocolV2CallOptions = {
|
|
@@ -34,9 +53,12 @@ export type ProtocolV2CallOptions = {
|
|
|
34
53
|
expectedTypes?: string[];
|
|
35
54
|
intermediateTypes?: string[];
|
|
36
55
|
onIntermediateResponse?: (response: MessageFromOneKey) => void;
|
|
56
|
+
onWriteCompleted?: (metrics: TransportWriteMetrics) => void;
|
|
57
|
+
writeWithResponse?: boolean;
|
|
37
58
|
};
|
|
38
59
|
|
|
39
60
|
export { concatUint8Arrays, ProtocolV2FrameAssembler };
|
|
61
|
+
export { ProtocolV2SequenceCursor };
|
|
40
62
|
|
|
41
63
|
export function hexToBytes(hex: string): Uint8Array {
|
|
42
64
|
const clean = hex.replace(/\s+/g, '');
|
|
@@ -59,15 +81,6 @@ export function bytesToHex(bytes: Uint8Array): string {
|
|
|
59
81
|
.join('');
|
|
60
82
|
}
|
|
61
83
|
|
|
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
84
|
const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
|
|
72
85
|
...LogBlockCommand,
|
|
73
86
|
'FilesystemFileRead',
|
|
@@ -79,102 +92,6 @@ function shouldReduceProtocolV2Debug(name: string) {
|
|
|
79
92
|
return HIGH_VOLUME_PROTOCOL_V2_CALLS.has(name);
|
|
80
93
|
}
|
|
81
94
|
|
|
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
95
|
const COMMON_TERMINAL_RESPONSE_TYPES = new Set([
|
|
179
96
|
'Failure',
|
|
180
97
|
'ButtonRequest',
|
|
@@ -207,11 +124,13 @@ export async function withProtocolTimeout<T>(
|
|
|
207
124
|
promise: Promise<T>,
|
|
208
125
|
timeoutMs: number | undefined,
|
|
209
126
|
createTimeoutError: () => Error,
|
|
210
|
-
onTimeout?: () => void
|
|
127
|
+
onTimeout?: () => void,
|
|
128
|
+
abortSignal?: AbortSignal
|
|
211
129
|
): Promise<T> {
|
|
212
130
|
if (!timeoutMs) return promise;
|
|
213
131
|
|
|
214
132
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
133
|
+
let abortHandler: (() => void) | undefined;
|
|
215
134
|
try {
|
|
216
135
|
return await Promise.race([
|
|
217
136
|
promise,
|
|
@@ -223,29 +142,43 @@ export async function withProtocolTimeout<T>(
|
|
|
223
142
|
reject(createTimeoutError());
|
|
224
143
|
}, timeoutMs);
|
|
225
144
|
}),
|
|
145
|
+
...(abortSignal
|
|
146
|
+
? [
|
|
147
|
+
new Promise<never>((_, reject) => {
|
|
148
|
+
abortHandler = () => {
|
|
149
|
+
reject(new Error('Protocol V2 operation aborted'));
|
|
150
|
+
};
|
|
151
|
+
if (abortSignal.aborted) {
|
|
152
|
+
abortHandler();
|
|
153
|
+
} else {
|
|
154
|
+
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
155
|
+
}
|
|
156
|
+
}),
|
|
157
|
+
]
|
|
158
|
+
: []),
|
|
226
159
|
]);
|
|
227
160
|
} finally {
|
|
228
161
|
if (timer) clearTimeout(timer);
|
|
162
|
+
if (abortHandler) {
|
|
163
|
+
abortSignal?.removeEventListener('abort', abortHandler);
|
|
164
|
+
}
|
|
229
165
|
}
|
|
230
166
|
}
|
|
231
167
|
|
|
232
|
-
// Write completion is owned by the concrete transport. A Promise.race watchdog
|
|
233
|
-
// here can return while libusb/WebUSB is still flushing a large Protocol V2
|
|
234
|
-
// frame, which desynchronizes later request/response pairs.
|
|
235
|
-
export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 0;
|
|
236
|
-
|
|
237
168
|
export class ProtocolV2Session {
|
|
238
169
|
private readonly options: ProtocolV2SessionOptions;
|
|
239
170
|
|
|
171
|
+
private readonly sequenceCursor: ProtocolV2SequenceCursor;
|
|
172
|
+
|
|
240
173
|
// Serializes call() invocations: responses are matched only by type, so two
|
|
241
174
|
// in-flight calls on the same session would steal each other's responses.
|
|
242
175
|
private pendingCall: Promise<unknown> = Promise.resolve();
|
|
243
176
|
|
|
244
|
-
|
|
245
|
-
private protoSeq = 0;
|
|
177
|
+
private lastResponseSequence?: number;
|
|
246
178
|
|
|
247
179
|
constructor(options: ProtocolV2SessionOptions) {
|
|
248
180
|
this.options = options;
|
|
181
|
+
this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor();
|
|
249
182
|
}
|
|
250
183
|
|
|
251
184
|
call(
|
|
@@ -261,7 +194,7 @@ export class ProtocolV2Session {
|
|
|
261
194
|
return result;
|
|
262
195
|
}
|
|
263
196
|
|
|
264
|
-
private
|
|
197
|
+
private executeCall(
|
|
265
198
|
name: string,
|
|
266
199
|
data: Record<string, unknown>,
|
|
267
200
|
callOptions: ProtocolV2CallOptions
|
|
@@ -270,118 +203,154 @@ export class ProtocolV2Session {
|
|
|
270
203
|
schemas,
|
|
271
204
|
router,
|
|
272
205
|
packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
206
|
+
maxFrameBytes,
|
|
207
|
+
prepareCall,
|
|
273
208
|
writeFrame,
|
|
274
209
|
readFrame,
|
|
275
210
|
logger,
|
|
276
211
|
logPrefix = 'ProtocolV2',
|
|
277
212
|
createTimeoutError,
|
|
213
|
+
generation = 0,
|
|
278
214
|
} = this.options;
|
|
215
|
+
const timeoutMs =
|
|
216
|
+
callOptions.timeoutMs && callOptions.timeoutMs > 0
|
|
217
|
+
? callOptions.timeoutMs
|
|
218
|
+
: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
|
|
219
|
+
const abortController = new AbortController();
|
|
279
220
|
|
|
280
221
|
const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (!shouldReduceDebug) {
|
|
293
|
-
logger?.debug?.(
|
|
294
|
-
`[${logPrefix}] TX payload name=${name}`,
|
|
295
|
-
sanitizeProtocolV2DebugPayload(data)
|
|
296
|
-
);
|
|
297
|
-
logger?.debug?.(
|
|
298
|
-
`[${logPrefix}] TX frame name=${name} len=${frame.length} router=${frame[4]} attr=${
|
|
299
|
-
frame[5]
|
|
300
|
-
} seq=${expectedSeq} headerHex=${frameHeaderDebugHex(frame)}`
|
|
301
|
-
);
|
|
302
|
-
}
|
|
222
|
+
const baseCallContext: ProtocolV2CallContext = {
|
|
223
|
+
messageName: name,
|
|
224
|
+
timeoutMs,
|
|
225
|
+
highVolume: shouldReduceDebug,
|
|
226
|
+
...(callOptions.writeWithResponse === undefined
|
|
227
|
+
? {}
|
|
228
|
+
: { writeWithResponse: callOptions.writeWithResponse }),
|
|
229
|
+
generation,
|
|
230
|
+
signal: abortController.signal,
|
|
231
|
+
};
|
|
303
232
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
233
|
+
const runCall = async (): Promise<MessageFromOneKey> => {
|
|
234
|
+
await prepareCall?.(baseCallContext);
|
|
235
|
+
const protoSeq = this.sequenceCursor.next();
|
|
236
|
+
const frame = ProtocolV2.encodeFrame(schemas, name, data, {
|
|
237
|
+
packetSrc,
|
|
238
|
+
router,
|
|
239
|
+
seq: protoSeq,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
314
247
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
248
|
+
const writeStartedAt = Date.now();
|
|
249
|
+
await writeFrame(frame, baseCallContext);
|
|
250
|
+
try {
|
|
251
|
+
callOptions.onWriteCompleted?.({
|
|
252
|
+
elapsedMs: Math.max(Date.now() - writeStartedAt, 0),
|
|
253
|
+
frameBytes: frame.byteLength,
|
|
254
|
+
});
|
|
255
|
+
} catch (error) {
|
|
256
|
+
logger?.error?.(`${logPrefix} write metrics callback failed: ${String(error)}`);
|
|
257
|
+
}
|
|
320
258
|
|
|
321
|
-
const readResponse = async (): Promise<MessageFromOneKey> => {
|
|
322
259
|
// Some Protocol V2 operations emit progress notifications before the
|
|
323
260
|
// terminal response. Consume those frames here so callers still see a
|
|
324
261
|
// request/terminal-response shaped API.
|
|
325
|
-
while (!
|
|
326
|
-
const rxFrame = await readFrame();
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
262
|
+
while (!abortController.signal.aborted) {
|
|
263
|
+
const rxFrame = await readFrame(baseCallContext);
|
|
264
|
+
if (abortController.signal.aborted) break;
|
|
265
|
+
|
|
266
|
+
let header: ReturnType<typeof ProtocolV2.inspectFrameHeader>;
|
|
267
|
+
try {
|
|
268
|
+
header = ProtocolV2.inspectFrameHeader(rxFrame);
|
|
269
|
+
} catch (cause) {
|
|
270
|
+
throw new ProtocolV2LinkError(
|
|
271
|
+
'frame',
|
|
272
|
+
`Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
|
|
273
|
+
cause
|
|
336
274
|
);
|
|
337
275
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
});
|
|
343
|
-
if (!shouldReduceDebug && decoded.seq !== expectedSeq) {
|
|
344
|
-
logger?.debug?.(
|
|
345
|
-
`[${logPrefix}] seq differs for ${name}: tx=${expectedSeq}, rx=${decoded.seq}`
|
|
276
|
+
if (header.router !== router) {
|
|
277
|
+
throw new ProtocolV2LinkError(
|
|
278
|
+
'router',
|
|
279
|
+
`Protocol V2 router mismatch: expected ${router}, got ${header.router}`
|
|
346
280
|
);
|
|
347
281
|
}
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
logger?.debug?.(
|
|
353
|
-
`[${logPrefix}] RX payload type=${decoded.type} messageTypeId=${decoded.messageTypeId}`,
|
|
354
|
-
sanitizeProtocolV2DebugPayload(decoded.message)
|
|
282
|
+
if (header.packetSrc !== packetSrc) {
|
|
283
|
+
throw new ProtocolV2LinkError(
|
|
284
|
+
'packet-source',
|
|
285
|
+
`Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
|
|
355
286
|
);
|
|
356
287
|
}
|
|
357
288
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
'|'
|
|
367
|
-
)} got=${response.type}`
|
|
289
|
+
let isAck: boolean;
|
|
290
|
+
try {
|
|
291
|
+
isAck = ProtocolV2.isAckFrame(rxFrame);
|
|
292
|
+
} catch (cause) {
|
|
293
|
+
throw new ProtocolV2LinkError(
|
|
294
|
+
'frame',
|
|
295
|
+
`Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
|
|
296
|
+
cause
|
|
368
297
|
);
|
|
369
298
|
}
|
|
299
|
+
if (isAck) {
|
|
300
|
+
if (header.seq !== protoSeq) {
|
|
301
|
+
throw new ProtocolV2LinkError(
|
|
302
|
+
'ack-sequence',
|
|
303
|
+
`Protocol V2 ACK sequence mismatch: expected ${protoSeq}, got ${header.seq}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
|
|
308
|
+
try {
|
|
309
|
+
decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
|
|
310
|
+
} catch (cause) {
|
|
311
|
+
throw new ProtocolV2LinkError(
|
|
312
|
+
'frame',
|
|
313
|
+
`Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
|
|
314
|
+
cause
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
// Firmware owns one TX sequence across all channels and packet sources,
|
|
318
|
+
// so frames routed elsewhere create valid gaps in this session.
|
|
319
|
+
if (this.lastResponseSequence === decoded.seq) {
|
|
320
|
+
throw new ProtocolV2LinkError(
|
|
321
|
+
'response-sequence',
|
|
322
|
+
`Protocol V2 duplicate response sequence: ${decoded.seq}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
this.lastResponseSequence = decoded.seq;
|
|
326
|
+
|
|
327
|
+
const response = check.call(decoded);
|
|
328
|
+
if (callOptions.intermediateTypes?.includes(response.type)) {
|
|
329
|
+
callOptions.onIntermediateResponse?.(response);
|
|
330
|
+
} else if (isExpectedTerminalResponse(response, callOptions.expectedTypes)) {
|
|
331
|
+
return response;
|
|
332
|
+
} else if (!shouldReduceDebug) {
|
|
333
|
+
logger?.debug?.(
|
|
334
|
+
`[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join(
|
|
335
|
+
'|'
|
|
336
|
+
)} got=${response.type}`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
370
340
|
}
|
|
371
|
-
|
|
372
|
-
// rejected by the timeout, so this rejection is consumed by the race.
|
|
341
|
+
|
|
373
342
|
throw new Error(`Protocol V2 read loop cancelled after timeout for ${name}`);
|
|
374
343
|
};
|
|
375
344
|
|
|
376
345
|
return withProtocolTimeout(
|
|
377
|
-
|
|
378
|
-
|
|
346
|
+
runCall(),
|
|
347
|
+
timeoutMs,
|
|
379
348
|
() =>
|
|
380
349
|
createTimeoutError
|
|
381
|
-
? createTimeoutError(name,
|
|
382
|
-
: new Error(`Protocol V2 response timeout after ${
|
|
350
|
+
? createTimeoutError(name, timeoutMs)
|
|
351
|
+
: new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
|
|
383
352
|
() => {
|
|
384
|
-
|
|
353
|
+
abortController.abort();
|
|
385
354
|
}
|
|
386
355
|
);
|
|
387
356
|
}
|
|
@@ -412,7 +381,10 @@ export async function probeProtocolV2({
|
|
|
412
381
|
const response = await call(
|
|
413
382
|
'Ping',
|
|
414
383
|
{ message: 'protocol-v2-probe' },
|
|
415
|
-
{
|
|
384
|
+
{
|
|
385
|
+
timeoutMs,
|
|
386
|
+
expectedTypes: ['Success'],
|
|
387
|
+
}
|
|
416
388
|
);
|
|
417
389
|
if (response.type === 'Success') {
|
|
418
390
|
return true;
|