@onekeyfe/hd-transport 1.2.0-alpha.8 → 1.2.0-alpha.80
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 +535 -53
- package/__tests__/transport-log.test.js +79 -0
- package/dist/constants.d.ts +5 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +822 -199
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +853 -229
- 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 +24 -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 +21 -4
- 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 +34 -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 +187 -200
- 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 +29 -4
- 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
|
+
highThroughput: 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,16 +81,7 @@ export function bytesToHex(bytes: Uint8Array): string {
|
|
|
59
81
|
.join('');
|
|
60
82
|
}
|
|
61
83
|
|
|
62
|
-
|
|
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
|
-
const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
|
|
84
|
+
const REDUCED_DEBUG_PROTOCOL_V2_CALLS = new Set([
|
|
72
85
|
...LogBlockCommand,
|
|
73
86
|
'FilesystemFileRead',
|
|
74
87
|
'FileRead',
|
|
@@ -76,103 +89,22 @@ const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
|
|
|
76
89
|
]);
|
|
77
90
|
|
|
78
91
|
function shouldReduceProtocolV2Debug(name: string) {
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
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}]`;
|
|
92
|
+
return REDUCED_DEBUG_PROTOCOL_V2_CALLS.has(name);
|
|
123
93
|
}
|
|
124
94
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
}
|
|
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
|
+
]);
|
|
164
105
|
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
106
|
+
export function isProtocolV2HighThroughputCall(name: string) {
|
|
107
|
+
return HIGH_THROUGHPUT_PROTOCOL_V2_CALLS.has(name);
|
|
176
108
|
}
|
|
177
109
|
|
|
178
110
|
const COMMON_TERMINAL_RESPONSE_TYPES = new Set([
|
|
@@ -207,11 +139,13 @@ export async function withProtocolTimeout<T>(
|
|
|
207
139
|
promise: Promise<T>,
|
|
208
140
|
timeoutMs: number | undefined,
|
|
209
141
|
createTimeoutError: () => Error,
|
|
210
|
-
onTimeout?: () => void
|
|
142
|
+
onTimeout?: () => void,
|
|
143
|
+
abortSignal?: AbortSignal
|
|
211
144
|
): Promise<T> {
|
|
212
145
|
if (!timeoutMs) return promise;
|
|
213
146
|
|
|
214
147
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
148
|
+
let abortHandler: (() => void) | undefined;
|
|
215
149
|
try {
|
|
216
150
|
return await Promise.race([
|
|
217
151
|
promise,
|
|
@@ -223,29 +157,43 @@ export async function withProtocolTimeout<T>(
|
|
|
223
157
|
reject(createTimeoutError());
|
|
224
158
|
}, timeoutMs);
|
|
225
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
|
+
: []),
|
|
226
174
|
]);
|
|
227
175
|
} finally {
|
|
228
176
|
if (timer) clearTimeout(timer);
|
|
177
|
+
if (abortHandler) {
|
|
178
|
+
abortSignal?.removeEventListener('abort', abortHandler);
|
|
179
|
+
}
|
|
229
180
|
}
|
|
230
181
|
}
|
|
231
182
|
|
|
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
183
|
export class ProtocolV2Session {
|
|
238
184
|
private readonly options: ProtocolV2SessionOptions;
|
|
239
185
|
|
|
186
|
+
private readonly sequenceCursor: ProtocolV2SequenceCursor;
|
|
187
|
+
|
|
240
188
|
// Serializes call() invocations: responses are matched only by type, so two
|
|
241
189
|
// in-flight calls on the same session would steal each other's responses.
|
|
242
190
|
private pendingCall: Promise<unknown> = Promise.resolve();
|
|
243
191
|
|
|
244
|
-
|
|
245
|
-
private protoSeq = 0;
|
|
192
|
+
private lastResponseSequence?: number;
|
|
246
193
|
|
|
247
194
|
constructor(options: ProtocolV2SessionOptions) {
|
|
248
195
|
this.options = options;
|
|
196
|
+
this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor();
|
|
249
197
|
}
|
|
250
198
|
|
|
251
199
|
call(
|
|
@@ -261,7 +209,7 @@ export class ProtocolV2Session {
|
|
|
261
209
|
return result;
|
|
262
210
|
}
|
|
263
211
|
|
|
264
|
-
private
|
|
212
|
+
private executeCall(
|
|
265
213
|
name: string,
|
|
266
214
|
data: Record<string, unknown>,
|
|
267
215
|
callOptions: ProtocolV2CallOptions
|
|
@@ -270,118 +218,154 @@ export class ProtocolV2Session {
|
|
|
270
218
|
schemas,
|
|
271
219
|
router,
|
|
272
220
|
packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
221
|
+
maxFrameBytes,
|
|
222
|
+
prepareCall,
|
|
273
223
|
writeFrame,
|
|
274
224
|
readFrame,
|
|
275
225
|
logger,
|
|
276
226
|
logPrefix = 'ProtocolV2',
|
|
277
227
|
createTimeoutError,
|
|
228
|
+
generation = 0,
|
|
278
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();
|
|
279
235
|
|
|
280
236
|
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
|
-
}
|
|
237
|
+
const baseCallContext: ProtocolV2CallContext = {
|
|
238
|
+
messageName: name,
|
|
239
|
+
timeoutMs,
|
|
240
|
+
highThroughput: isProtocolV2HighThroughputCall(name),
|
|
241
|
+
...(callOptions.writeWithResponse === undefined
|
|
242
|
+
? {}
|
|
243
|
+
: { writeWithResponse: callOptions.writeWithResponse }),
|
|
244
|
+
generation,
|
|
245
|
+
signal: abortController.signal,
|
|
246
|
+
};
|
|
303
247
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
}
|
|
314
262
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
}
|
|
320
273
|
|
|
321
|
-
const readResponse = async (): Promise<MessageFromOneKey> => {
|
|
322
274
|
// Some Protocol V2 operations emit progress notifications before the
|
|
323
275
|
// terminal response. Consume those frames here so callers still see a
|
|
324
276
|
// request/terminal-response shaped API.
|
|
325
|
-
while (!
|
|
326
|
-
const rxFrame = await readFrame();
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
336
289
|
);
|
|
337
290
|
}
|
|
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}`
|
|
291
|
+
if (header.router !== router) {
|
|
292
|
+
throw new ProtocolV2LinkError(
|
|
293
|
+
'router',
|
|
294
|
+
`Protocol V2 router mismatch: expected ${router}, got ${header.router}`
|
|
346
295
|
);
|
|
347
296
|
}
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
logger?.debug?.(
|
|
353
|
-
`[${logPrefix}] RX payload type=${decoded.type} messageTypeId=${decoded.messageTypeId}`,
|
|
354
|
-
sanitizeProtocolV2DebugPayload(decoded.message)
|
|
297
|
+
if (header.packetSrc !== packetSrc) {
|
|
298
|
+
throw new ProtocolV2LinkError(
|
|
299
|
+
'packet-source',
|
|
300
|
+
`Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
|
|
355
301
|
);
|
|
356
302
|
}
|
|
357
303
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
'|'
|
|
367
|
-
)} got=${response.type}`
|
|
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
|
|
368
312
|
);
|
|
369
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}`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
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
|
|
330
|
+
);
|
|
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}`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
this.lastResponseSequence = decoded.seq;
|
|
341
|
+
|
|
342
|
+
const response = check.call(decoded);
|
|
343
|
+
if (callOptions.intermediateTypes?.includes(response.type)) {
|
|
344
|
+
callOptions.onIntermediateResponse?.(response);
|
|
345
|
+
} else if (isExpectedTerminalResponse(response, callOptions.expectedTypes)) {
|
|
346
|
+
return response;
|
|
347
|
+
} else if (!shouldReduceDebug) {
|
|
348
|
+
logger?.debug?.(
|
|
349
|
+
`[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join(
|
|
350
|
+
'|'
|
|
351
|
+
)} got=${response.type}`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
370
355
|
}
|
|
371
|
-
|
|
372
|
-
// rejected by the timeout, so this rejection is consumed by the race.
|
|
356
|
+
|
|
373
357
|
throw new Error(`Protocol V2 read loop cancelled after timeout for ${name}`);
|
|
374
358
|
};
|
|
375
359
|
|
|
376
360
|
return withProtocolTimeout(
|
|
377
|
-
|
|
378
|
-
|
|
361
|
+
runCall(),
|
|
362
|
+
timeoutMs,
|
|
379
363
|
() =>
|
|
380
364
|
createTimeoutError
|
|
381
|
-
? createTimeoutError(name,
|
|
382
|
-
: new Error(`Protocol V2 response timeout after ${
|
|
365
|
+
? createTimeoutError(name, timeoutMs)
|
|
366
|
+
: new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
|
|
383
367
|
() => {
|
|
384
|
-
|
|
368
|
+
abortController.abort();
|
|
385
369
|
}
|
|
386
370
|
);
|
|
387
371
|
}
|
|
@@ -412,7 +396,10 @@ export async function probeProtocolV2({
|
|
|
412
396
|
const response = await call(
|
|
413
397
|
'Ping',
|
|
414
398
|
{ message: 'protocol-v2-probe' },
|
|
415
|
-
{
|
|
399
|
+
{
|
|
400
|
+
timeoutMs,
|
|
401
|
+
expectedTypes: ['Success'],
|
|
402
|
+
}
|
|
416
403
|
);
|
|
417
404
|
if (response.type === 'Success') {
|
|
418
405
|
return true;
|