@onekeyfe/hd-transport 1.2.0-alpha.13 → 1.2.0-alpha.131
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 +144 -4
- package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
- package/__tests__/protocol-v2-link-manager.test.js +176 -9
- package/__tests__/protocol-v2-usb-transport-base.test.js +36 -2
- package/__tests__/protocol-v2.test.js +540 -108
- 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 +645 -217
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +679 -153
- package/dist/protocols/index.d.ts +17 -1
- 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 +11 -0
- package/dist/protocols/v2/decode.d.ts.map +1 -1
- package/dist/protocols/v2/errors.d.ts +16 -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 -0
- package/dist/protocols/v2/index.d.ts.map +1 -1
- package/dist/protocols/v2/link-manager.d.ts +5 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -1
- package/dist/protocols/v2/session.d.ts +14 -4
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +2 -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 +301 -156
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +21 -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 +523 -449
- package/package.json +2 -2
- package/scripts/protobuf-build.sh +112 -32
- package/scripts/protobuf-patches/index.js +1 -0
- package/scripts/protobuf-types.js +10 -1
- package/src/constants.ts +30 -16
- package/src/index.ts +5 -1
- package/src/protocols/index.ts +114 -3
- package/src/protocols/v2/ble-frame-writer.ts +77 -0
- package/src/protocols/v2/crc8.ts +1 -1
- package/src/protocols/v2/decode.ts +49 -12
- package/src/protocols/v2/encode.ts +1 -1
- package/src/protocols/v2/errors.ts +54 -0
- package/src/protocols/v2/frame-assembler.ts +6 -4
- package/src/protocols/v2/index.ts +2 -0
- package/src/protocols/v2/link-manager.ts +65 -5
- package/src/protocols/v2/session.ts +224 -80
- package/src/protocols/v2/usb-transport-base.ts +42 -10
- package/src/serialization/protobuf/decode.ts +4 -1
- package/src/serialization/protobuf/encode.ts +5 -1
- package/src/types/messages.ts +375 -198
- package/src/types/transport.ts +30 -3
- package/src/utils/transportLog.ts +102 -0
|
@@ -1,12 +1,23 @@
|
|
|
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 {
|
|
8
|
+
ProtocolV2LinkError,
|
|
9
|
+
createProtocolV2LinkDisabledError,
|
|
10
|
+
isProtocolV2LinkDisabledError,
|
|
11
|
+
isProtocolV2LinkDisabledFailure,
|
|
12
|
+
} from './errors';
|
|
4
13
|
import { ProtocolV2 } from '..';
|
|
5
14
|
import * as check from '../../utils/highlevel-checks';
|
|
6
15
|
import { LogBlockCommand } from '../../utils/logBlockCommand';
|
|
7
16
|
|
|
8
17
|
import type { Root } from 'protobufjs/light';
|
|
9
|
-
import type { MessageFromOneKey } from '../../types';
|
|
18
|
+
import type { MessageFromOneKey, TransportWriteMetrics } from '../../types';
|
|
19
|
+
|
|
20
|
+
export * from './errors';
|
|
10
21
|
|
|
11
22
|
export type ProtocolV2Schemas = {
|
|
12
23
|
protocolV1: Root;
|
|
@@ -16,8 +27,10 @@ export type ProtocolV2Schemas = {
|
|
|
16
27
|
export type ProtocolV2CallContext = {
|
|
17
28
|
messageName: string;
|
|
18
29
|
timeoutMs?: number;
|
|
19
|
-
|
|
30
|
+
highThroughput: boolean;
|
|
31
|
+
writeWithResponse?: boolean;
|
|
20
32
|
generation: number;
|
|
33
|
+
signal: AbortSignal;
|
|
21
34
|
};
|
|
22
35
|
|
|
23
36
|
type ProtocolLogger = {
|
|
@@ -45,6 +58,9 @@ export type ProtocolV2CallOptions = {
|
|
|
45
58
|
expectedTypes?: string[];
|
|
46
59
|
intermediateTypes?: string[];
|
|
47
60
|
onIntermediateResponse?: (response: MessageFromOneKey) => void;
|
|
61
|
+
onWriteCompleted?: (metrics: TransportWriteMetrics) => void;
|
|
62
|
+
returnAfterWrite?: boolean;
|
|
63
|
+
writeWithResponse?: boolean;
|
|
48
64
|
};
|
|
49
65
|
|
|
50
66
|
export { concatUint8Arrays, ProtocolV2FrameAssembler };
|
|
@@ -71,7 +87,7 @@ export function bytesToHex(bytes: Uint8Array): string {
|
|
|
71
87
|
.join('');
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
const
|
|
90
|
+
const REDUCED_DEBUG_PROTOCOL_V2_CALLS = new Set([
|
|
75
91
|
...LogBlockCommand,
|
|
76
92
|
'FilesystemFileRead',
|
|
77
93
|
'FileRead',
|
|
@@ -79,7 +95,22 @@ const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
|
|
|
79
95
|
]);
|
|
80
96
|
|
|
81
97
|
function shouldReduceProtocolV2Debug(name: string) {
|
|
82
|
-
return
|
|
98
|
+
return REDUCED_DEBUG_PROTOCOL_V2_CALLS.has(name);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const HIGH_THROUGHPUT_PROTOCOL_V2_CALLS = new Set([
|
|
102
|
+
'FilesystemFileWrite',
|
|
103
|
+
'FileWrite',
|
|
104
|
+
'EmmcFileWrite',
|
|
105
|
+
'FirmwareUpload',
|
|
106
|
+
'ResourceAck',
|
|
107
|
+
'FilesystemFileRead',
|
|
108
|
+
'FileRead',
|
|
109
|
+
'EmmcFileRead',
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
export function isProtocolV2HighThroughputCall(name: string) {
|
|
113
|
+
return HIGH_THROUGHPUT_PROTOCOL_V2_CALLS.has(name);
|
|
83
114
|
}
|
|
84
115
|
|
|
85
116
|
const COMMON_TERMINAL_RESPONSE_TYPES = new Set([
|
|
@@ -114,11 +145,13 @@ export async function withProtocolTimeout<T>(
|
|
|
114
145
|
promise: Promise<T>,
|
|
115
146
|
timeoutMs: number | undefined,
|
|
116
147
|
createTimeoutError: () => Error,
|
|
117
|
-
onTimeout?: () => void
|
|
148
|
+
onTimeout?: () => void,
|
|
149
|
+
abortSignal?: AbortSignal
|
|
118
150
|
): Promise<T> {
|
|
119
151
|
if (!timeoutMs) return promise;
|
|
120
152
|
|
|
121
153
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
154
|
+
let abortHandler: (() => void) | undefined;
|
|
122
155
|
try {
|
|
123
156
|
return await Promise.race([
|
|
124
157
|
promise,
|
|
@@ -130,17 +163,29 @@ export async function withProtocolTimeout<T>(
|
|
|
130
163
|
reject(createTimeoutError());
|
|
131
164
|
}, timeoutMs);
|
|
132
165
|
}),
|
|
166
|
+
...(abortSignal
|
|
167
|
+
? [
|
|
168
|
+
new Promise<never>((_, reject) => {
|
|
169
|
+
abortHandler = () => {
|
|
170
|
+
reject(new Error('Protocol V2 operation aborted'));
|
|
171
|
+
};
|
|
172
|
+
if (abortSignal.aborted) {
|
|
173
|
+
abortHandler();
|
|
174
|
+
} else {
|
|
175
|
+
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
176
|
+
}
|
|
177
|
+
}),
|
|
178
|
+
]
|
|
179
|
+
: []),
|
|
133
180
|
]);
|
|
134
181
|
} finally {
|
|
135
182
|
if (timer) clearTimeout(timer);
|
|
183
|
+
if (abortHandler) {
|
|
184
|
+
abortSignal?.removeEventListener('abort', abortHandler);
|
|
185
|
+
}
|
|
136
186
|
}
|
|
137
187
|
}
|
|
138
188
|
|
|
139
|
-
// Write completion is owned by the concrete transport. A Promise.race watchdog
|
|
140
|
-
// here can return while libusb/WebUSB is still flushing a large Protocol V2
|
|
141
|
-
// frame, which desynchronizes later request/response pairs.
|
|
142
|
-
export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 0;
|
|
143
|
-
|
|
144
189
|
export class ProtocolV2Session {
|
|
145
190
|
private readonly options: ProtocolV2SessionOptions;
|
|
146
191
|
|
|
@@ -150,6 +195,12 @@ export class ProtocolV2Session {
|
|
|
150
195
|
// in-flight calls on the same session would steal each other's responses.
|
|
151
196
|
private pendingCall: Promise<unknown> = Promise.resolve();
|
|
152
197
|
|
|
198
|
+
// Flow-control messages may interrupt a call that is waiting for a device UI
|
|
199
|
+
// response, but their frames must never interleave with the active request write.
|
|
200
|
+
private pendingWrite: Promise<unknown> = Promise.resolve();
|
|
201
|
+
|
|
202
|
+
private lastResponseSequence?: number;
|
|
203
|
+
|
|
153
204
|
constructor(options: ProtocolV2SessionOptions) {
|
|
154
205
|
this.options = options;
|
|
155
206
|
this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor();
|
|
@@ -168,7 +219,48 @@ export class ProtocolV2Session {
|
|
|
168
219
|
return result;
|
|
169
220
|
}
|
|
170
221
|
|
|
171
|
-
|
|
222
|
+
sendFlowControl(name: string, data: Record<string, unknown>): Promise<MessageFromOneKey> {
|
|
223
|
+
const {
|
|
224
|
+
schemas,
|
|
225
|
+
router,
|
|
226
|
+
packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
227
|
+
maxFrameBytes,
|
|
228
|
+
writeFrame,
|
|
229
|
+
generation = 0,
|
|
230
|
+
} = this.options;
|
|
231
|
+
const abortController = new AbortController();
|
|
232
|
+
const context: ProtocolV2CallContext = {
|
|
233
|
+
messageName: name,
|
|
234
|
+
highThroughput: false,
|
|
235
|
+
generation,
|
|
236
|
+
signal: abortController.signal,
|
|
237
|
+
};
|
|
238
|
+
const protoSeq = this.sequenceCursor.next();
|
|
239
|
+
const frame = ProtocolV2.encodeFrame(schemas, name, data, {
|
|
240
|
+
packetSrc,
|
|
241
|
+
router,
|
|
242
|
+
seq: protoSeq,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
|
|
246
|
+
return Promise.reject(
|
|
247
|
+
new Error(`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return this.serializeWrite(() => writeFrame(frame, context)).then(() => ({
|
|
252
|
+
type: 'WriteCompleted',
|
|
253
|
+
message: {},
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private serializeWrite<T>(write: () => Promise<T>): Promise<T> {
|
|
258
|
+
const result = this.pendingWrite.then(write, write);
|
|
259
|
+
this.pendingWrite = result.catch(() => undefined);
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private executeCall(
|
|
172
264
|
name: string,
|
|
173
265
|
data: Record<string, unknown>,
|
|
174
266
|
callOptions: ProtocolV2CallOptions
|
|
@@ -186,81 +278,121 @@ export class ProtocolV2Session {
|
|
|
186
278
|
createTimeoutError,
|
|
187
279
|
generation = 0,
|
|
188
280
|
} = this.options;
|
|
281
|
+
const timeoutMs =
|
|
282
|
+
callOptions.timeoutMs && callOptions.timeoutMs > 0
|
|
283
|
+
? callOptions.timeoutMs
|
|
284
|
+
: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
|
|
285
|
+
const abortController = new AbortController();
|
|
189
286
|
|
|
190
287
|
const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
|
|
191
|
-
const
|
|
288
|
+
const baseCallContext: ProtocolV2CallContext = {
|
|
192
289
|
messageName: name,
|
|
193
|
-
timeoutMs
|
|
194
|
-
|
|
290
|
+
timeoutMs,
|
|
291
|
+
highThroughput: isProtocolV2HighThroughputCall(name),
|
|
292
|
+
...(callOptions.writeWithResponse === undefined
|
|
293
|
+
? {}
|
|
294
|
+
: { writeWithResponse: callOptions.writeWithResponse }),
|
|
195
295
|
generation,
|
|
296
|
+
signal: abortController.signal,
|
|
196
297
|
};
|
|
197
|
-
await prepareCall?.(callContext);
|
|
198
|
-
const protoSeq = this.sequenceCursor.next();
|
|
199
|
-
const frame = ProtocolV2.encodeFrame(schemas, name, data, {
|
|
200
|
-
packetSrc,
|
|
201
|
-
router,
|
|
202
|
-
seq: protoSeq,
|
|
203
|
-
});
|
|
204
|
-
const txMetadata = ProtocolV2.inspectFrame(schemas, frame);
|
|
205
298
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
logger?.debug?.(`[${logPrefix}] TX`, {
|
|
214
|
-
method: name,
|
|
215
|
-
type: name,
|
|
216
|
-
typeId: txMetadata.messageTypeId,
|
|
217
|
-
seq: txMetadata.seq,
|
|
218
|
-
bytes: frame.length,
|
|
299
|
+
const runCall = async (): Promise<MessageFromOneKey> => {
|
|
300
|
+
await prepareCall?.(baseCallContext);
|
|
301
|
+
const protoSeq = this.sequenceCursor.next();
|
|
302
|
+
const frame = ProtocolV2.encodeFrame(schemas, name, data, {
|
|
303
|
+
packetSrc,
|
|
304
|
+
router,
|
|
305
|
+
seq: protoSeq,
|
|
219
306
|
});
|
|
220
|
-
}
|
|
221
307
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
() =>
|
|
228
|
-
new Error(
|
|
229
|
-
`Protocol V2 write timeout after ${PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS}ms for ${name}`
|
|
230
|
-
)
|
|
231
|
-
);
|
|
308
|
+
if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
232
313
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
314
|
+
const writeStartedAt = Date.now();
|
|
315
|
+
await this.serializeWrite(() => writeFrame(frame, baseCallContext));
|
|
316
|
+
try {
|
|
317
|
+
callOptions.onWriteCompleted?.({
|
|
318
|
+
elapsedMs: Math.max(Date.now() - writeStartedAt, 0),
|
|
319
|
+
frameBytes: frame.byteLength,
|
|
320
|
+
});
|
|
321
|
+
} catch (error) {
|
|
322
|
+
logger?.error?.(`${logPrefix} write metrics callback failed: ${String(error)}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (callOptions.returnAfterWrite) {
|
|
326
|
+
return { type: 'WriteCompleted', message: {} };
|
|
327
|
+
}
|
|
238
328
|
|
|
239
|
-
const readResponse = async (): Promise<MessageFromOneKey> => {
|
|
240
329
|
// Some Protocol V2 operations emit progress notifications before the
|
|
241
330
|
// terminal response. Consume those frames here so callers still see a
|
|
242
331
|
// request/terminal-response shaped API.
|
|
243
|
-
while (!
|
|
244
|
-
const rxFrame = await readFrame(
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
332
|
+
while (!abortController.signal.aborted) {
|
|
333
|
+
const rxFrame = await readFrame(baseCallContext);
|
|
334
|
+
if (abortController.signal.aborted) break;
|
|
335
|
+
|
|
336
|
+
let header: ReturnType<typeof ProtocolV2.inspectFrameHeader>;
|
|
337
|
+
try {
|
|
338
|
+
header = ProtocolV2.inspectFrameHeader(rxFrame);
|
|
339
|
+
} catch (cause) {
|
|
340
|
+
throw new ProtocolV2LinkError(
|
|
341
|
+
'frame',
|
|
342
|
+
`Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
|
|
343
|
+
cause
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (header.router !== router) {
|
|
347
|
+
throw new ProtocolV2LinkError(
|
|
348
|
+
'router',
|
|
349
|
+
`Protocol V2 router mismatch: expected ${router}, got ${header.router}`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
if (header.packetSrc !== packetSrc) {
|
|
353
|
+
throw new ProtocolV2LinkError(
|
|
354
|
+
'packet-source',
|
|
355
|
+
`Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
|
|
356
|
+
);
|
|
248
357
|
}
|
|
249
|
-
const isAck = ProtocolV2.isAckFrame(rxFrame);
|
|
250
|
-
if (!isAck) {
|
|
251
|
-
const rxMetadata = ProtocolV2.inspectFrame(schemas, rxFrame);
|
|
252
|
-
|
|
253
|
-
if (!shouldReduceDebug) {
|
|
254
|
-
logger?.debug?.(`[${logPrefix}] RX`, {
|
|
255
|
-
method: name,
|
|
256
|
-
type: rxMetadata.type,
|
|
257
|
-
typeId: rxMetadata.messageTypeId,
|
|
258
|
-
seq: rxMetadata.seq,
|
|
259
|
-
bytes: rxFrame.length,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
358
|
|
|
263
|
-
|
|
359
|
+
let isAck: boolean;
|
|
360
|
+
try {
|
|
361
|
+
isAck = ProtocolV2.isAckFrame(rxFrame);
|
|
362
|
+
} catch (cause) {
|
|
363
|
+
throw new ProtocolV2LinkError(
|
|
364
|
+
'frame',
|
|
365
|
+
`Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
|
|
366
|
+
cause
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (isAck) {
|
|
370
|
+
if (header.seq !== protoSeq) {
|
|
371
|
+
throw new ProtocolV2LinkError(
|
|
372
|
+
'ack-sequence',
|
|
373
|
+
`Protocol V2 ACK sequence mismatch: expected ${protoSeq}, got ${header.seq}`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
|
|
378
|
+
try {
|
|
379
|
+
decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
|
|
380
|
+
} catch (cause) {
|
|
381
|
+
throw new ProtocolV2LinkError(
|
|
382
|
+
'frame',
|
|
383
|
+
`Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
|
|
384
|
+
cause
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
// Firmware owns one TX sequence across all channels and packet sources,
|
|
388
|
+
// so frames routed elsewhere create valid gaps in this session.
|
|
389
|
+
if (this.lastResponseSequence === decoded.seq) {
|
|
390
|
+
throw new ProtocolV2LinkError(
|
|
391
|
+
'response-sequence',
|
|
392
|
+
`Protocol V2 duplicate response sequence: ${decoded.seq}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
this.lastResponseSequence = decoded.seq;
|
|
264
396
|
|
|
265
397
|
const response = check.call(decoded);
|
|
266
398
|
if (callOptions.intermediateTypes?.includes(response.type)) {
|
|
@@ -276,20 +408,19 @@ export class ProtocolV2Session {
|
|
|
276
408
|
}
|
|
277
409
|
}
|
|
278
410
|
}
|
|
279
|
-
|
|
280
|
-
// rejected by the timeout, so this rejection is consumed by the race.
|
|
411
|
+
|
|
281
412
|
throw new Error(`Protocol V2 read loop cancelled after timeout for ${name}`);
|
|
282
413
|
};
|
|
283
414
|
|
|
284
415
|
return withProtocolTimeout(
|
|
285
|
-
|
|
286
|
-
|
|
416
|
+
runCall(),
|
|
417
|
+
timeoutMs,
|
|
287
418
|
() =>
|
|
288
419
|
createTimeoutError
|
|
289
|
-
? createTimeoutError(name,
|
|
290
|
-
: new Error(`Protocol V2 response timeout after ${
|
|
420
|
+
? createTimeoutError(name, timeoutMs)
|
|
421
|
+
: new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
|
|
291
422
|
() => {
|
|
292
|
-
|
|
423
|
+
abortController.abort();
|
|
293
424
|
}
|
|
294
425
|
);
|
|
295
426
|
}
|
|
@@ -320,13 +451,26 @@ export async function probeProtocolV2({
|
|
|
320
451
|
const response = await call(
|
|
321
452
|
'Ping',
|
|
322
453
|
{ message: 'protocol-v2-probe' },
|
|
323
|
-
{
|
|
454
|
+
{
|
|
455
|
+
timeoutMs,
|
|
456
|
+
expectedTypes: ['Success'],
|
|
457
|
+
}
|
|
324
458
|
);
|
|
325
459
|
if (response.type === 'Success') {
|
|
326
460
|
return true;
|
|
327
461
|
}
|
|
462
|
+
if (response.type === 'Failure') {
|
|
463
|
+
const failureCode = response.message?.code;
|
|
464
|
+
const firmwareMessage = response.message?.message;
|
|
465
|
+
if (isProtocolV2LinkDisabledFailure(failureCode, firmwareMessage)) {
|
|
466
|
+
throw createProtocolV2LinkDisabledError(failureCode, firmwareMessage);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
328
469
|
probeError = new Error(`unexpected response type ${response.type}`);
|
|
329
470
|
} catch (error) {
|
|
471
|
+
if (isProtocolV2LinkDisabledError(error)) {
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
330
474
|
probeError = error;
|
|
331
475
|
}
|
|
332
476
|
|
|
@@ -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> {
|
|
@@ -91,6 +93,19 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
|
|
|
91
93
|
);
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
protected sendProtocolV2UsbFlowControl(
|
|
97
|
+
key: Key,
|
|
98
|
+
name: string,
|
|
99
|
+
data: Record<string, unknown>
|
|
100
|
+
): Promise<MessageFromOneKey> {
|
|
101
|
+
return this.protocolV2UsbLinks.sendFlowControl(
|
|
102
|
+
key,
|
|
103
|
+
() => this.createProtocolV2UsbAdapter(key),
|
|
104
|
+
name,
|
|
105
|
+
data
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
94
109
|
protected invalidateProtocolV2UsbLink(key: Key, reason: string): Promise<void> {
|
|
95
110
|
return this.protocolV2UsbLinks.invalidateLink(key, reason);
|
|
96
111
|
}
|
|
@@ -118,10 +133,13 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
|
|
|
118
133
|
|
|
119
134
|
const assertCurrentGeneration = () => {
|
|
120
135
|
if (cancellation.reason) {
|
|
121
|
-
throw new
|
|
136
|
+
throw new ProtocolV2LinkError('generation', cancellation.reason);
|
|
122
137
|
}
|
|
123
138
|
if (this.protocolV2UsbGenerations.get(key) !== generation) {
|
|
124
|
-
throw new
|
|
139
|
+
throw new ProtocolV2LinkError(
|
|
140
|
+
'generation',
|
|
141
|
+
'Protocol V2 USB connection generation changed'
|
|
142
|
+
);
|
|
125
143
|
}
|
|
126
144
|
};
|
|
127
145
|
|
|
@@ -131,11 +149,14 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
|
|
|
131
149
|
generation,
|
|
132
150
|
prepareCall: () => {
|
|
133
151
|
assertCurrentGeneration();
|
|
134
|
-
this.getProtocolV2UsbAssembler(key).reset();
|
|
135
152
|
},
|
|
136
153
|
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
137
154
|
assertCurrentGeneration();
|
|
138
|
-
|
|
155
|
+
try {
|
|
156
|
+
await this.writeProtocolV2UsbPacket(key, frame, context);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
throw this.createProtocolV2UsbIoError('write', error);
|
|
159
|
+
}
|
|
139
160
|
assertCurrentGeneration();
|
|
140
161
|
},
|
|
141
162
|
readFrame: async (context: ProtocolV2CallContext) => {
|
|
@@ -143,13 +164,15 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
|
|
|
143
164
|
const assembler = this.getProtocolV2UsbAssembler(key);
|
|
144
165
|
let frame = assembler.push(new Uint8Array(0));
|
|
145
166
|
while (!frame) {
|
|
146
|
-
const packetRead = this.readProtocolV2UsbPacket(key, context)
|
|
147
|
-
packet
|
|
148
|
-
|
|
167
|
+
const packetRead = this.readProtocolV2UsbPacket(key, context)
|
|
168
|
+
.then(packet => ({ packet }))
|
|
169
|
+
.catch(error => {
|
|
170
|
+
throw this.createProtocolV2UsbIoError('read', error);
|
|
171
|
+
});
|
|
149
172
|
const cancelled = cancellation.promise.then(reason => ({ reason }));
|
|
150
173
|
const result = await Promise.race([packetRead, cancelled]);
|
|
151
174
|
if ('reason' in result) {
|
|
152
|
-
throw new
|
|
175
|
+
throw new ProtocolV2LinkError('generation', result.reason);
|
|
153
176
|
}
|
|
154
177
|
assertCurrentGeneration();
|
|
155
178
|
frame = assembler.push(result.packet);
|
|
@@ -191,4 +214,13 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
|
|
|
191
214
|
};
|
|
192
215
|
return cancellation;
|
|
193
216
|
}
|
|
217
|
+
|
|
218
|
+
private createProtocolV2UsbIoError(operation: 'read' | 'write', error: unknown) {
|
|
219
|
+
if (isProtocolV2LinkError(error)) return error;
|
|
220
|
+
return new ProtocolV2LinkError(
|
|
221
|
+
'io',
|
|
222
|
+
`Protocol V2 USB ${operation} failed: ${getErrorMessage(error) || 'unknown error'}`,
|
|
223
|
+
error
|
|
224
|
+
);
|
|
225
|
+
}
|
|
194
226
|
}
|
|
@@ -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) {
|