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