@onekeyfe/hd-transport 1.2.8-alpha.120 → 1.2.8-alpha.121
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/dist/constants.d.ts +0 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +2 -46
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +70 -218
- package/dist/protocols/v2/session.d.ts +1 -7
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/types/transport.d.ts +0 -5
- package/dist/types/transport.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/constants.ts +1 -10
- package/src/index.ts +0 -1
- package/src/protocols/v2/session.ts +96 -326
- package/src/types/transport.ts +0 -19
- package/__tests__/protocol-v2-file-write-pipeline.test.js +0 -478
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/types/transport.ts"],"names":[],"mappings":";;;AAAA,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AAEvC,eAAO,MAAM,eAAe;;CAElB,CAAC;AAEX,MAAM,MAAM,8BAA8B,GAAG;IAC3C,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvC,MAAM,MAAM,oBAAoB,GAC5B,KAAK,GACL,QAAQ,GACR,KAAK,GACL,QAAQ,GACR,cAAc,GACd,QAAQ,GACR,UAAU,CAAC;AAEf,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG,mBAAmB,GAAG;IAC9D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAMpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,oBAAoB,CAAC;CAChC,CAAC;AAGF,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,GACjD,2BAA2B,GAC3B,sBAAsB,GAAG;IACvB,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEJ,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,gBAAgB,CAAC,EAAE,YAAY,CAAC;IAChC,YAAY,CAAC,EAAE,YAAY,CAAC;IAK5B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAAE,CAAC;AAE/E,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/types/transport.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport",
|
|
3
|
-
"version": "1.2.8-alpha.
|
|
3
|
+
"version": "1.2.8-alpha.121",
|
|
4
4
|
"description": "Transport layer abstractions and utilities for OneKey hardware SDK.",
|
|
5
5
|
"author": "OneKey",
|
|
6
6
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"long": "^4.0.0",
|
|
29
29
|
"protobufjs": "^6.11.2"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "ce1078610eca281844be4caa5444d8dd2023fed4"
|
|
32
32
|
}
|
package/src/constants.ts
CHANGED
|
@@ -30,9 +30,8 @@ export const PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE = 4000;
|
|
|
30
30
|
export const PROTOCOL_V2_BLE_FILE_CHUNK_SIZE = 1800;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* FirmwareUpdateV4 chunk size for its fixed BLE staging paths
|
|
33
|
+
* FirmwareUpdateV4 chunk size for its fixed BLE staging paths.
|
|
34
34
|
* The longest current path still leaves 28 bytes below the 2048-byte BLE frame limit.
|
|
35
|
-
* Pipelined writes use PROTOCOL_V2_BLE_FILE_CHUNK_SIZE so two frames fit the UART FIFO.
|
|
36
35
|
*/
|
|
37
36
|
export const PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE = 1960;
|
|
38
37
|
|
|
@@ -42,14 +41,6 @@ export const PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE = 900;
|
|
|
42
41
|
/** Pro2 BLE/UART RX FIFO must hold a complete Proto Link frame. */
|
|
43
42
|
export const PROTOCOL_V2_BLE_FRAME_MAX_BYTES = 2048;
|
|
44
43
|
|
|
45
|
-
/**
|
|
46
|
-
* FilesystemFileWrite frames kept in flight by a file write pipeline. The main MCU copies a
|
|
47
|
-
* complete frame out of its UART FIFO before handling it, but a frame waiting to be parsed and
|
|
48
|
-
* the next one being received can both be resident: two full frames must fit (4096-byte FIFO).
|
|
49
|
-
* With 1800-byte chunks two frames take about 3.75 KB.
|
|
50
|
-
*/
|
|
51
|
-
export const PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW = 2;
|
|
52
|
-
|
|
53
44
|
/** @deprecated Use the transport-specific WebUSB or BLE file chunk constant. */
|
|
54
45
|
export const PROTOCOL_V2_FILE_CHUNK_SIZE = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
|
|
55
46
|
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
3
|
-
PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW,
|
|
4
3
|
PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
5
4
|
} from '../../constants';
|
|
6
5
|
import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
|
|
@@ -10,18 +9,13 @@ import {
|
|
|
10
9
|
createProtocolV2LinkDisabledError,
|
|
11
10
|
isProtocolV2LinkDisabledError,
|
|
12
11
|
isProtocolV2LinkDisabledFailure,
|
|
13
|
-
isProtocolV2LinkError,
|
|
14
12
|
} from './errors';
|
|
15
13
|
import { ProtocolV2 } from '..';
|
|
16
14
|
import * as check from '../../utils/highlevel-checks';
|
|
17
15
|
import { LogBlockCommand } from '../../utils/logBlockCommand';
|
|
18
16
|
|
|
19
17
|
import type { Root } from 'protobufjs/light';
|
|
20
|
-
import type {
|
|
21
|
-
MessageFromOneKey,
|
|
22
|
-
ProtocolV2FileWritePipeline,
|
|
23
|
-
TransportWriteMetrics,
|
|
24
|
-
} from '../../types';
|
|
18
|
+
import type { MessageFromOneKey, TransportWriteMetrics } from '../../types';
|
|
25
19
|
|
|
26
20
|
export * from './errors';
|
|
27
21
|
|
|
@@ -68,7 +62,6 @@ export type ProtocolV2CallOptions = {
|
|
|
68
62
|
returnAfterWrite?: boolean;
|
|
69
63
|
onResponseAfterWrite?: (response: MessageFromOneKey) => void;
|
|
70
64
|
writeWithResponse?: boolean;
|
|
71
|
-
fileWritePipeline?: ProtocolV2FileWritePipeline;
|
|
72
65
|
};
|
|
73
66
|
|
|
74
67
|
export { concatUint8Arrays, ProtocolV2FrameAssembler };
|
|
@@ -221,30 +214,6 @@ export async function withProtocolTimeout<T>(
|
|
|
221
214
|
}
|
|
222
215
|
}
|
|
223
216
|
|
|
224
|
-
const resolveProtocolV2CallTimeoutMs = (timeoutMs?: number) =>
|
|
225
|
-
timeoutMs && timeoutMs > 0 ? timeoutMs : PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
|
|
226
|
-
|
|
227
|
-
function parseFileWriteChunk(request: Record<string, unknown>) {
|
|
228
|
-
const file = request.file as
|
|
229
|
-
| { path?: unknown; offset?: unknown; total_size?: unknown; data?: { byteLength?: unknown } }
|
|
230
|
-
| undefined;
|
|
231
|
-
const offset = Number(file?.offset);
|
|
232
|
-
const totalSize = Number(file?.total_size);
|
|
233
|
-
const length = Number(file?.data?.byteLength);
|
|
234
|
-
if (
|
|
235
|
-
typeof file?.path !== 'string' ||
|
|
236
|
-
!Number.isSafeInteger(offset) ||
|
|
237
|
-
offset < 0 ||
|
|
238
|
-
!Number.isSafeInteger(totalSize) ||
|
|
239
|
-
!Number.isSafeInteger(length) ||
|
|
240
|
-
length <= 0 ||
|
|
241
|
-
offset + length > totalSize
|
|
242
|
-
) {
|
|
243
|
-
throw new Error('Protocol V2 file write pipeline chunk is invalid');
|
|
244
|
-
}
|
|
245
|
-
return { path: file.path, offset, end: offset + length, totalSize };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
217
|
export class ProtocolV2Session {
|
|
249
218
|
private readonly options: ProtocolV2SessionOptions;
|
|
250
219
|
|
|
@@ -324,46 +293,28 @@ export class ProtocolV2Session {
|
|
|
324
293
|
return result;
|
|
325
294
|
}
|
|
326
295
|
|
|
327
|
-
private reportWriteCompleted(
|
|
328
|
-
callOptions: ProtocolV2CallOptions,
|
|
329
|
-
writeStartedAt: number,
|
|
330
|
-
frameBytes: number
|
|
331
|
-
) {
|
|
332
|
-
try {
|
|
333
|
-
callOptions.onWriteCompleted?.({
|
|
334
|
-
elapsedMs: Math.max(Date.now() - writeStartedAt, 0),
|
|
335
|
-
frameBytes,
|
|
336
|
-
});
|
|
337
|
-
} catch (error) {
|
|
338
|
-
const { logger, logPrefix = 'ProtocolV2' } = this.options;
|
|
339
|
-
logger?.error?.(`${logPrefix} write metrics callback failed: ${String(error)}`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
private createCallTimeoutError(name: string, timeoutMs: number): Error {
|
|
344
|
-
const { createTimeoutError } = this.options;
|
|
345
|
-
return createTimeoutError
|
|
346
|
-
? createTimeoutError(name, timeoutMs)
|
|
347
|
-
: new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
296
|
private executeCall(
|
|
351
297
|
name: string,
|
|
352
298
|
data: Record<string, unknown>,
|
|
353
299
|
callOptions: ProtocolV2CallOptions
|
|
354
300
|
): Promise<MessageFromOneKey> {
|
|
355
|
-
if (callOptions.fileWritePipeline) {
|
|
356
|
-
return this.executeFileWritePipeline(name, data, callOptions, callOptions.fileWritePipeline);
|
|
357
|
-
}
|
|
358
301
|
const {
|
|
302
|
+
schemas,
|
|
303
|
+
router,
|
|
304
|
+
packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
305
|
+
maxFrameBytes,
|
|
359
306
|
prepareCall,
|
|
360
307
|
writeFrame,
|
|
361
308
|
readFrame,
|
|
362
309
|
logger,
|
|
363
310
|
logPrefix = 'ProtocolV2',
|
|
311
|
+
createTimeoutError,
|
|
364
312
|
generation = 0,
|
|
365
313
|
} = this.options;
|
|
366
|
-
const timeoutMs =
|
|
314
|
+
const timeoutMs =
|
|
315
|
+
callOptions.timeoutMs && callOptions.timeoutMs > 0
|
|
316
|
+
? callOptions.timeoutMs
|
|
317
|
+
: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
|
|
367
318
|
const abortController = new AbortController();
|
|
368
319
|
|
|
369
320
|
const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
|
|
@@ -384,11 +335,29 @@ export class ProtocolV2Session {
|
|
|
384
335
|
if (!this.pendingResponseAfterWrite) {
|
|
385
336
|
await prepareCall?.(baseCallContext);
|
|
386
337
|
}
|
|
387
|
-
const
|
|
338
|
+
const protoSeq = this.sequenceCursor.next();
|
|
339
|
+
const frame = ProtocolV2.encodeFrame(schemas, name, data, {
|
|
340
|
+
packetSrc,
|
|
341
|
+
router,
|
|
342
|
+
seq: protoSeq,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
388
350
|
|
|
389
351
|
const writeStartedAt = Date.now();
|
|
390
352
|
await this.serializeWrite(() => writeFrame(frame, baseCallContext));
|
|
391
|
-
|
|
353
|
+
try {
|
|
354
|
+
callOptions.onWriteCompleted?.({
|
|
355
|
+
elapsedMs: Math.max(Date.now() - writeStartedAt, 0),
|
|
356
|
+
frameBytes: frame.byteLength,
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
logger?.error?.(`${logPrefix} write metrics callback failed: ${String(error)}`);
|
|
360
|
+
}
|
|
392
361
|
|
|
393
362
|
if (callOptions.returnAfterWrite) {
|
|
394
363
|
if (callOptions.expectedTypes?.length) {
|
|
@@ -407,8 +376,68 @@ export class ProtocolV2Session {
|
|
|
407
376
|
const rxFrame = await readFrame(baseCallContext);
|
|
408
377
|
if (abortController.signal.aborted) break;
|
|
409
378
|
|
|
410
|
-
|
|
411
|
-
|
|
379
|
+
let header: ReturnType<typeof ProtocolV2.inspectFrameHeader>;
|
|
380
|
+
try {
|
|
381
|
+
header = ProtocolV2.inspectFrameHeader(rxFrame);
|
|
382
|
+
} catch (cause) {
|
|
383
|
+
throw new ProtocolV2LinkError(
|
|
384
|
+
'frame',
|
|
385
|
+
`Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
|
|
386
|
+
cause
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
if (header.router !== router) {
|
|
390
|
+
throw new ProtocolV2LinkError(
|
|
391
|
+
'router',
|
|
392
|
+
`Protocol V2 router mismatch: expected ${router}, got ${header.router}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
if (header.packetSrc !== packetSrc) {
|
|
396
|
+
throw new ProtocolV2LinkError(
|
|
397
|
+
'packet-source',
|
|
398
|
+
`Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let isAck: boolean;
|
|
403
|
+
try {
|
|
404
|
+
isAck = ProtocolV2.isAckFrame(rxFrame);
|
|
405
|
+
} catch (cause) {
|
|
406
|
+
throw new ProtocolV2LinkError(
|
|
407
|
+
'frame',
|
|
408
|
+
`Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
|
|
409
|
+
cause
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
if (isAck) {
|
|
413
|
+
if (header.seq !== protoSeq) {
|
|
414
|
+
throw new ProtocolV2LinkError(
|
|
415
|
+
'ack-sequence',
|
|
416
|
+
`Protocol V2 ACK sequence mismatch: expected ${protoSeq}, got ${header.seq}`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
|
|
421
|
+
try {
|
|
422
|
+
decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
|
|
423
|
+
} catch (cause) {
|
|
424
|
+
throw new ProtocolV2LinkError(
|
|
425
|
+
'frame',
|
|
426
|
+
`Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
|
|
427
|
+
cause
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
// Firmware owns one TX sequence across all channels and packet sources,
|
|
431
|
+
// so frames routed elsewhere create valid gaps in this session.
|
|
432
|
+
if (this.lastResponseSequence === decoded.seq) {
|
|
433
|
+
throw new ProtocolV2LinkError(
|
|
434
|
+
'response-sequence',
|
|
435
|
+
`Protocol V2 duplicate response sequence: ${decoded.seq}`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
this.lastResponseSequence = decoded.seq;
|
|
439
|
+
|
|
440
|
+
const response = check.call(decoded);
|
|
412
441
|
const { pendingResponseAfterWrite } = this;
|
|
413
442
|
let belongsToWriteOnlyCall = false;
|
|
414
443
|
if (pendingResponseAfterWrite) {
|
|
@@ -448,274 +477,15 @@ export class ProtocolV2Session {
|
|
|
448
477
|
return withProtocolTimeout(
|
|
449
478
|
runCall(),
|
|
450
479
|
timeoutMs,
|
|
451
|
-
() =>
|
|
480
|
+
() =>
|
|
481
|
+
createTimeoutError
|
|
482
|
+
? createTimeoutError(name, timeoutMs)
|
|
483
|
+
: new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
|
|
452
484
|
() => {
|
|
453
485
|
abortController.abort();
|
|
454
486
|
}
|
|
455
487
|
);
|
|
456
488
|
}
|
|
457
|
-
|
|
458
|
-
private encodeRequestFrame(name: string, data: Record<string, unknown>) {
|
|
459
|
-
const {
|
|
460
|
-
schemas,
|
|
461
|
-
router,
|
|
462
|
-
packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
463
|
-
maxFrameBytes,
|
|
464
|
-
} = this.options;
|
|
465
|
-
const seq = this.sequenceCursor.next();
|
|
466
|
-
const frame = ProtocolV2.encodeFrame(schemas, name, data, { packetSrc, router, seq });
|
|
467
|
-
if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
|
|
468
|
-
throw new Error(
|
|
469
|
-
`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
return { seq, frame };
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Validates one received frame for this session. Returns undefined for an ACK of one of
|
|
477
|
-
* `ackSequences`, otherwise the decoded response.
|
|
478
|
-
*/
|
|
479
|
-
private parseResponseFrame(
|
|
480
|
-
rxFrame: Uint8Array,
|
|
481
|
-
ackSequences: readonly number[]
|
|
482
|
-
): MessageFromOneKey | undefined {
|
|
483
|
-
const { schemas, router, packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND } = this.options;
|
|
484
|
-
let header: ReturnType<typeof ProtocolV2.inspectFrameHeader>;
|
|
485
|
-
try {
|
|
486
|
-
header = ProtocolV2.inspectFrameHeader(rxFrame);
|
|
487
|
-
} catch (cause) {
|
|
488
|
-
throw new ProtocolV2LinkError(
|
|
489
|
-
'frame',
|
|
490
|
-
`Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
|
|
491
|
-
cause
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
if (header.router !== router) {
|
|
495
|
-
throw new ProtocolV2LinkError(
|
|
496
|
-
'router',
|
|
497
|
-
`Protocol V2 router mismatch: expected ${router}, got ${header.router}`
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
if (header.packetSrc !== packetSrc) {
|
|
501
|
-
throw new ProtocolV2LinkError(
|
|
502
|
-
'packet-source',
|
|
503
|
-
`Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
let isAck: boolean;
|
|
508
|
-
try {
|
|
509
|
-
isAck = ProtocolV2.isAckFrame(rxFrame);
|
|
510
|
-
} catch (cause) {
|
|
511
|
-
throw new ProtocolV2LinkError(
|
|
512
|
-
'frame',
|
|
513
|
-
`Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
|
|
514
|
-
cause
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
if (isAck) {
|
|
518
|
-
if (!ackSequences.includes(header.seq)) {
|
|
519
|
-
throw new ProtocolV2LinkError(
|
|
520
|
-
'ack-sequence',
|
|
521
|
-
`Protocol V2 ACK sequence mismatch: expected ${ackSequences.join('|')}, got ${header.seq}`
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
return undefined;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
|
|
528
|
-
try {
|
|
529
|
-
decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
|
|
530
|
-
} catch (cause) {
|
|
531
|
-
throw new ProtocolV2LinkError(
|
|
532
|
-
'frame',
|
|
533
|
-
`Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
|
|
534
|
-
cause
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
// Firmware owns one TX sequence across all channels and packet sources,
|
|
538
|
-
// so frames routed elsewhere create valid gaps in this session.
|
|
539
|
-
if (this.lastResponseSequence === decoded.seq) {
|
|
540
|
-
throw new ProtocolV2LinkError(
|
|
541
|
-
'response-sequence',
|
|
542
|
-
`Protocol V2 duplicate response sequence: ${decoded.seq}`
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
this.lastResponseSequence = decoded.seq;
|
|
546
|
-
return check.call(decoded);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Writes one file as ordered FilesystemFileWrite chunks while keeping up to
|
|
551
|
-
* PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW chunks in flight. Firmware still processes and
|
|
552
|
-
* answers every chunk in order, so responses are matched to chunks by position. Upstream
|
|
553
|
-
* bootloaders append at their own cursor without checking the request offset, so the echoed
|
|
554
|
-
* offset is what exposes a lost chunk. A device Failure, or `next` returning undefined, stops
|
|
555
|
-
* sending; the remaining in-flight responses are drained so the link stays usable.
|
|
556
|
-
*/
|
|
557
|
-
private async executeFileWritePipeline(
|
|
558
|
-
name: string,
|
|
559
|
-
data: Record<string, unknown>,
|
|
560
|
-
callOptions: ProtocolV2CallOptions,
|
|
561
|
-
pipeline: ProtocolV2FileWritePipeline
|
|
562
|
-
): Promise<MessageFromOneKey> {
|
|
563
|
-
if (name !== 'FilesystemFileWrite') {
|
|
564
|
-
throw new Error(`Protocol V2 file write pipeline does not support ${name}`);
|
|
565
|
-
}
|
|
566
|
-
if (this.pendingResponseAfterWrite) {
|
|
567
|
-
throw new Error('Protocol V2 file write pipeline cannot start while a response is pending');
|
|
568
|
-
}
|
|
569
|
-
const { prepareCall, writeFrame, readFrame, generation = 0 } = this.options;
|
|
570
|
-
const timeoutMs = resolveProtocolV2CallTimeoutMs(callOptions.timeoutMs);
|
|
571
|
-
const abortController = new AbortController();
|
|
572
|
-
const context: ProtocolV2CallContext = {
|
|
573
|
-
messageName: name,
|
|
574
|
-
timeoutMs,
|
|
575
|
-
highThroughput: true,
|
|
576
|
-
...(callOptions.writeWithResponse === undefined
|
|
577
|
-
? {}
|
|
578
|
-
: { writeWithResponse: callOptions.writeWithResponse }),
|
|
579
|
-
generation,
|
|
580
|
-
signal: abortController.signal,
|
|
581
|
-
};
|
|
582
|
-
// Transport errors and timeouts propagate unchanged: each transport classifies its own.
|
|
583
|
-
let transportFailure: unknown;
|
|
584
|
-
// One file can take far longer than one chunk, so the watchdog bounds each step instead.
|
|
585
|
-
const withStepTimeout = <T>(promise: Promise<T>, stepTimeoutMs = timeoutMs) =>
|
|
586
|
-
withProtocolTimeout(
|
|
587
|
-
promise,
|
|
588
|
-
stepTimeoutMs,
|
|
589
|
-
() => this.createCallTimeoutError(name, timeoutMs),
|
|
590
|
-
() => {
|
|
591
|
-
abortController.abort();
|
|
592
|
-
}
|
|
593
|
-
).catch((error: unknown) => {
|
|
594
|
-
transportFailure = error;
|
|
595
|
-
throw error;
|
|
596
|
-
});
|
|
597
|
-
// ACKs and skipped frames must not extend the wait for an in-flight chunk's response.
|
|
598
|
-
let progressDeadline = 0;
|
|
599
|
-
const markProgress = () => {
|
|
600
|
-
progressDeadline = Date.now() + timeoutMs;
|
|
601
|
-
};
|
|
602
|
-
|
|
603
|
-
const inFlight: Array<{ seq: number; offset: number; end: number }> = [];
|
|
604
|
-
let path: string | undefined;
|
|
605
|
-
let totalSize: number | undefined;
|
|
606
|
-
let nextOffset: number | undefined;
|
|
607
|
-
let sourceDone = false;
|
|
608
|
-
let failure: MessageFromOneKey | undefined;
|
|
609
|
-
let lastConfirmed: MessageFromOneKey | undefined;
|
|
610
|
-
|
|
611
|
-
const sendChunk = async (request: Record<string, unknown>) => {
|
|
612
|
-
const chunk = parseFileWriteChunk(request);
|
|
613
|
-
if (path !== undefined && (chunk.path !== path || chunk.totalSize !== totalSize)) {
|
|
614
|
-
throw new Error('Protocol V2 file write pipeline chunk belongs to a different file');
|
|
615
|
-
}
|
|
616
|
-
if (nextOffset !== undefined && chunk.offset !== nextOffset) {
|
|
617
|
-
throw new Error(
|
|
618
|
-
`Protocol V2 file write pipeline chunk out of order: expected offset ${nextOffset}, got ${chunk.offset}`
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
path = chunk.path;
|
|
622
|
-
totalSize = chunk.totalSize;
|
|
623
|
-
nextOffset = chunk.end;
|
|
624
|
-
|
|
625
|
-
const { seq, frame } = this.encodeRequestFrame(name, request);
|
|
626
|
-
const writeStartedAt = Date.now();
|
|
627
|
-
await withStepTimeout(this.serializeWrite(() => writeFrame(frame, context)));
|
|
628
|
-
inFlight.push({ seq, offset: chunk.offset, end: chunk.end });
|
|
629
|
-
markProgress();
|
|
630
|
-
this.reportWriteCompleted(callOptions, writeStartedAt, frame.byteLength);
|
|
631
|
-
};
|
|
632
|
-
const sendNextChunk = async () => {
|
|
633
|
-
const request = await pipeline.next();
|
|
634
|
-
if (!request) {
|
|
635
|
-
sourceDone = true;
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
await sendChunk(request);
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
await withStepTimeout(Promise.resolve().then(() => prepareCall?.(context)));
|
|
642
|
-
try {
|
|
643
|
-
await sendChunk(data);
|
|
644
|
-
while (!sourceDone && inFlight.length < PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW) {
|
|
645
|
-
await sendNextChunk();
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
while (inFlight.length > 0) {
|
|
649
|
-
// Floor at 1 ms: withProtocolTimeout treats 0 as "no timeout", so an expired deadline
|
|
650
|
-
// must still time out.
|
|
651
|
-
const rxFrame = await withStepTimeout(
|
|
652
|
-
readFrame(context),
|
|
653
|
-
Math.max(progressDeadline - Date.now(), 1)
|
|
654
|
-
);
|
|
655
|
-
const response = this.parseResponseFrame(
|
|
656
|
-
rxFrame,
|
|
657
|
-
inFlight.map(item => item.seq)
|
|
658
|
-
);
|
|
659
|
-
if (!response) {
|
|
660
|
-
// ACK of an in-flight chunk; its FilesystemFile response follows.
|
|
661
|
-
} else if (response.type === 'FilesystemFile') {
|
|
662
|
-
const [expected] = inFlight;
|
|
663
|
-
const { message } = response;
|
|
664
|
-
if (
|
|
665
|
-
Number(message.processed_byte) !== expected.end ||
|
|
666
|
-
message.path !== path ||
|
|
667
|
-
Number(message.offset) !== expected.offset ||
|
|
668
|
-
Number(message.total_size) !== totalSize
|
|
669
|
-
) {
|
|
670
|
-
throw new ProtocolV2LinkError(
|
|
671
|
-
'frame',
|
|
672
|
-
`Protocol V2 file write response does not match chunk ${expected.offset}-${
|
|
673
|
-
expected.end
|
|
674
|
-
}: got offset ${String(message.offset)} processed_byte ${String(
|
|
675
|
-
message.processed_byte
|
|
676
|
-
)}`
|
|
677
|
-
);
|
|
678
|
-
}
|
|
679
|
-
inFlight.shift();
|
|
680
|
-
markProgress();
|
|
681
|
-
if (!failure) {
|
|
682
|
-
lastConfirmed = response;
|
|
683
|
-
pipeline.onResponse(response);
|
|
684
|
-
if (!sourceDone) {
|
|
685
|
-
await sendNextChunk();
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
} else if (response.type === 'Failure') {
|
|
689
|
-
inFlight.shift();
|
|
690
|
-
markProgress();
|
|
691
|
-
failure = failure ?? response;
|
|
692
|
-
sourceDone = true;
|
|
693
|
-
} else if (COMMON_TERMINAL_RESPONSE_TYPES.has(response.type)) {
|
|
694
|
-
throw new ProtocolV2LinkError(
|
|
695
|
-
'frame',
|
|
696
|
-
`Protocol V2 file write pipeline received unexpected ${response.type}`
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
} catch (error) {
|
|
701
|
-
// Unread responses of in-flight chunks would answer the next call on this link, so a
|
|
702
|
-
// session or caller error raised with chunks in flight must invalidate it.
|
|
703
|
-
if (inFlight.length === 0 || error === transportFailure || isProtocolV2LinkError(error)) {
|
|
704
|
-
throw error;
|
|
705
|
-
}
|
|
706
|
-
throw new ProtocolV2LinkError(
|
|
707
|
-
'frame',
|
|
708
|
-
`Protocol V2 file write pipeline aborted with chunks in flight: ${getErrorMessage(error)}`,
|
|
709
|
-
error
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const terminal = failure ?? lastConfirmed;
|
|
714
|
-
if (!terminal) {
|
|
715
|
-
throw new ProtocolV2LinkError('frame', 'Protocol V2 file write pipeline ended unconfirmed');
|
|
716
|
-
}
|
|
717
|
-
return terminal;
|
|
718
|
-
}
|
|
719
489
|
}
|
|
720
490
|
|
|
721
491
|
export async function probeProtocolV2({
|
package/src/types/transport.ts
CHANGED
|
@@ -76,19 +76,6 @@ export type TransportWriteMetrics = {
|
|
|
76
76
|
frameBytes: number;
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
/**
|
|
80
|
-
* Supplies the remaining chunks of one file for an ordered FilesystemFileWrite pipeline.
|
|
81
|
-
* The call's request data is the first chunk; `next` returns the following chunk in offset
|
|
82
|
-
* order, or undefined to stop. The call then resolves with the last confirmation (or the
|
|
83
|
-
* device Failure), so the caller must check `processed_byte` for completeness. Callbacks
|
|
84
|
-
* should not throw: an error escapes with chunks still in flight and invalidates the link.
|
|
85
|
-
*/
|
|
86
|
-
export type ProtocolV2FileWritePipeline = {
|
|
87
|
-
next: () => Promise<Record<string, unknown> | undefined>;
|
|
88
|
-
/** Called for each chunk after the device confirms it, in offset order. */
|
|
89
|
-
onResponse: (response: MessageFromOneKey) => void;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
79
|
export type TransportCallOptions = {
|
|
93
80
|
timeoutMs?: number;
|
|
94
81
|
expectedTypes?: string[];
|
|
@@ -102,12 +89,6 @@ export type TransportCallOptions = {
|
|
|
102
89
|
onResponseAfterWrite?: (response: MessageFromOneKey) => void;
|
|
103
90
|
/** Prefer acknowledged BLE characteristic writes for this call when supported. */
|
|
104
91
|
writeWithResponse?: boolean;
|
|
105
|
-
/**
|
|
106
|
-
* Protocol V2 only: keep up to PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW ordered
|
|
107
|
-
* FilesystemFileWrite frames of one file in flight within this call. `timeoutMs`
|
|
108
|
-
* bounds each frame write and each response instead of the whole file.
|
|
109
|
-
*/
|
|
110
|
-
fileWritePipeline?: ProtocolV2FileWritePipeline;
|
|
111
92
|
};
|
|
112
93
|
|
|
113
94
|
type ITransportInitFn = (
|