@onekeyfe/hd-transport 1.2.8-alpha.111 → 1.2.8-alpha.113
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__/protocol-v2-file-write-pipeline.test.js +417 -0
- package/__tests__/transport-log.test.js +0 -44
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +42 -68
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +187 -57
- package/dist/protocols/v2/session.d.ts +4 -1
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/types/messages.d.ts +0 -47
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +5 -0
- package/dist/types/transport.d.ts.map +1 -1
- package/dist/utils/transportLog.d.ts.map +1 -1
- package/messages-protocol-v2.json +1 -138
- package/package.json +2 -2
- package/src/constants.ts +7 -0
- package/src/index.ts +1 -0
- package/src/protocols/v2/session.ts +300 -63
- package/src/types/messages.ts +0 -63
- package/src/types/transport.ts +19 -0
- package/src/utils/transportLog.ts +1 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transportLog.d.ts","sourceRoot":"","sources":["../../src/utils/transportLog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"transportLog.d.ts","sourceRoot":"","sources":["../../src/utils/transportLog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AA+E7C,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAKxF;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;EAO9B;AAID,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,WAE3D"}
|
|
@@ -11034,61 +11034,6 @@
|
|
|
11034
11034
|
}
|
|
11035
11035
|
}
|
|
11036
11036
|
},
|
|
11037
|
-
"ZcashSignPczt": {
|
|
11038
|
-
"fields": {
|
|
11039
|
-
"pczt_length": {
|
|
11040
|
-
"rule": "required",
|
|
11041
|
-
"type": "uint32",
|
|
11042
|
-
"id": 1
|
|
11043
|
-
},
|
|
11044
|
-
"pczt_initial_chunk": {
|
|
11045
|
-
"rule": "required",
|
|
11046
|
-
"type": "bytes",
|
|
11047
|
-
"id": 2
|
|
11048
|
-
}
|
|
11049
|
-
}
|
|
11050
|
-
},
|
|
11051
|
-
"ZcashPcztChunkRequest": {
|
|
11052
|
-
"fields": {
|
|
11053
|
-
"chunk_length": {
|
|
11054
|
-
"rule": "required",
|
|
11055
|
-
"type": "uint32",
|
|
11056
|
-
"id": 1
|
|
11057
|
-
}
|
|
11058
|
-
}
|
|
11059
|
-
},
|
|
11060
|
-
"ZcashPcztChunkAck": {
|
|
11061
|
-
"fields": {
|
|
11062
|
-
"data_chunk": {
|
|
11063
|
-
"rule": "required",
|
|
11064
|
-
"type": "bytes",
|
|
11065
|
-
"id": 1
|
|
11066
|
-
}
|
|
11067
|
-
}
|
|
11068
|
-
},
|
|
11069
|
-
"ZcashSignedPczt": {
|
|
11070
|
-
"fields": {
|
|
11071
|
-
"pczt_length": {
|
|
11072
|
-
"rule": "required",
|
|
11073
|
-
"type": "uint32",
|
|
11074
|
-
"id": 1
|
|
11075
|
-
},
|
|
11076
|
-
"pczt_initial_chunk": {
|
|
11077
|
-
"rule": "required",
|
|
11078
|
-
"type": "bytes",
|
|
11079
|
-
"id": 2
|
|
11080
|
-
}
|
|
11081
|
-
}
|
|
11082
|
-
},
|
|
11083
|
-
"ZcashSignedPcztChunkRequest": {
|
|
11084
|
-
"fields": {
|
|
11085
|
-
"chunk_length": {
|
|
11086
|
-
"rule": "required",
|
|
11087
|
-
"type": "uint32",
|
|
11088
|
-
"id": 1
|
|
11089
|
-
}
|
|
11090
|
-
}
|
|
11091
|
-
},
|
|
11092
11037
|
"wire_in": {
|
|
11093
11038
|
"type": "bool",
|
|
11094
11039
|
"id": 50002,
|
|
@@ -11514,11 +11459,6 @@
|
|
|
11514
11459
|
"MessageType_NeoAddress": 12302,
|
|
11515
11460
|
"MessageType_NeoSignTx": 12303,
|
|
11516
11461
|
"MessageType_NeoSignedTx": 12304,
|
|
11517
|
-
"MessageType_ZcashSignPczt": 12402,
|
|
11518
|
-
"MessageType_ZcashSignedPczt": 12403,
|
|
11519
|
-
"MessageType_ZcashPcztChunkRequest": 12404,
|
|
11520
|
-
"MessageType_ZcashPcztChunkAck": 12405,
|
|
11521
|
-
"MessageType_ZcashSignedPcztChunkRequest": 12406,
|
|
11522
11462
|
"MessageType_DeviceFactoryInfoSet": 60000,
|
|
11523
11463
|
"MessageType_DeviceFactoryInfoGet": 60001,
|
|
11524
11464
|
"MessageType_DeviceFactoryInfo": 60002,
|
|
@@ -11571,9 +11511,7 @@
|
|
|
11571
11511
|
"MessageType_PortfolioUpdate": 61400,
|
|
11572
11512
|
"MessageType_NftUpdate": 61500,
|
|
11573
11513
|
"MessageType_OnboardingStatusGet": 61600,
|
|
11574
|
-
"MessageType_OnboardingStatus": 61601
|
|
11575
|
-
"MessageType_ZcashGetAddress": 12400,
|
|
11576
|
-
"MessageType_ZcashAddress": 12401
|
|
11514
|
+
"MessageType_OnboardingStatus": 61601
|
|
11577
11515
|
},
|
|
11578
11516
|
"reserved": [
|
|
11579
11517
|
[90, 92],
|
|
@@ -13792,81 +13730,6 @@
|
|
|
13792
13730
|
}
|
|
13793
13731
|
}
|
|
13794
13732
|
}
|
|
13795
|
-
},
|
|
13796
|
-
"ZcashAddressType": {
|
|
13797
|
-
"values": {
|
|
13798
|
-
"UNIFIED_ORCHARD_P2PKH": 0,
|
|
13799
|
-
"TRANSPARENT_P2PKH": 1,
|
|
13800
|
-
"UNIFIED_ORCHARD": 2
|
|
13801
|
-
}
|
|
13802
|
-
},
|
|
13803
|
-
"ZcashAddressScope": {
|
|
13804
|
-
"values": {
|
|
13805
|
-
"EXTERNAL": 0,
|
|
13806
|
-
"INTERNAL": 1
|
|
13807
|
-
}
|
|
13808
|
-
},
|
|
13809
|
-
"ZcashGetAddress": {
|
|
13810
|
-
"fields": {
|
|
13811
|
-
"address_n": {
|
|
13812
|
-
"rule": "repeated",
|
|
13813
|
-
"type": "uint32",
|
|
13814
|
-
"id": 1,
|
|
13815
|
-
"options": {
|
|
13816
|
-
"packed": false
|
|
13817
|
-
}
|
|
13818
|
-
},
|
|
13819
|
-
"show_display": {
|
|
13820
|
-
"type": "bool",
|
|
13821
|
-
"id": 2
|
|
13822
|
-
},
|
|
13823
|
-
"include_seed_fingerprint": {
|
|
13824
|
-
"type": "bool",
|
|
13825
|
-
"id": 3
|
|
13826
|
-
},
|
|
13827
|
-
"include_ufvk": {
|
|
13828
|
-
"type": "bool",
|
|
13829
|
-
"id": 4
|
|
13830
|
-
},
|
|
13831
|
-
"diversifier_index": {
|
|
13832
|
-
"type": "uint32",
|
|
13833
|
-
"id": 5,
|
|
13834
|
-
"options": {
|
|
13835
|
-
"default": 0
|
|
13836
|
-
}
|
|
13837
|
-
},
|
|
13838
|
-
"address_type": {
|
|
13839
|
-
"type": "ZcashAddressType",
|
|
13840
|
-
"id": 6,
|
|
13841
|
-
"options": {
|
|
13842
|
-
"default": "UNIFIED_ORCHARD_P2PKH"
|
|
13843
|
-
}
|
|
13844
|
-
},
|
|
13845
|
-
"scope": {
|
|
13846
|
-
"type": "ZcashAddressScope",
|
|
13847
|
-
"id": 7,
|
|
13848
|
-
"options": {
|
|
13849
|
-
"default": "EXTERNAL"
|
|
13850
|
-
}
|
|
13851
|
-
}
|
|
13852
|
-
}
|
|
13853
|
-
},
|
|
13854
|
-
"ZcashAddress": {
|
|
13855
|
-
"fields": {
|
|
13856
|
-
"address": {
|
|
13857
|
-
"rule": "required",
|
|
13858
|
-
"type": "string",
|
|
13859
|
-
"id": 1
|
|
13860
|
-
},
|
|
13861
|
-
"ufvk": {
|
|
13862
|
-
"type": "string",
|
|
13863
|
-
"id": 2
|
|
13864
|
-
},
|
|
13865
|
-
"seed_fingerprint": {
|
|
13866
|
-
"type": "bytes",
|
|
13867
|
-
"id": 3
|
|
13868
|
-
}
|
|
13869
|
-
}
|
|
13870
13733
|
}
|
|
13871
13734
|
}
|
|
13872
13735
|
}
|
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.113",
|
|
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": "c56caf8fdd87e3a00bdc90aca044e75693704ae3"
|
|
32
32
|
}
|
package/src/constants.ts
CHANGED
|
@@ -41,6 +41,13 @@ export const PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE = 900;
|
|
|
41
41
|
/** Pro2 BLE/UART RX FIFO must hold a complete Proto Link frame. */
|
|
42
42
|
export const PROTOCOL_V2_BLE_FRAME_MAX_BYTES = 2048;
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* FilesystemFileWrite frames kept in flight by a file write pipeline. The main MCU copies a
|
|
46
|
+
* complete frame out of its UART FIFO before handling it, but a frame waiting to be parsed and
|
|
47
|
+
* the next one being received can both be resident: two full frames must fit (4096-byte FIFO).
|
|
48
|
+
*/
|
|
49
|
+
export const PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW = 2;
|
|
50
|
+
|
|
44
51
|
/** @deprecated Use the transport-specific WebUSB or BLE file chunk constant. */
|
|
45
52
|
export const PROTOCOL_V2_FILE_CHUNK_SIZE = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
|
|
46
53
|
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
3
|
+
PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW,
|
|
3
4
|
PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
4
5
|
} from '../../constants';
|
|
5
6
|
import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
|
|
@@ -15,7 +16,11 @@ import * as check from '../../utils/highlevel-checks';
|
|
|
15
16
|
import { LogBlockCommand } from '../../utils/logBlockCommand';
|
|
16
17
|
|
|
17
18
|
import type { Root } from 'protobufjs/light';
|
|
18
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
MessageFromOneKey,
|
|
21
|
+
ProtocolV2FileWritePipeline,
|
|
22
|
+
TransportWriteMetrics,
|
|
23
|
+
} from '../../types';
|
|
19
24
|
|
|
20
25
|
export * from './errors';
|
|
21
26
|
|
|
@@ -62,6 +67,7 @@ export type ProtocolV2CallOptions = {
|
|
|
62
67
|
returnAfterWrite?: boolean;
|
|
63
68
|
onResponseAfterWrite?: (response: MessageFromOneKey) => void;
|
|
64
69
|
writeWithResponse?: boolean;
|
|
70
|
+
fileWritePipeline?: ProtocolV2FileWritePipeline;
|
|
65
71
|
};
|
|
66
72
|
|
|
67
73
|
export { concatUint8Arrays, ProtocolV2FrameAssembler };
|
|
@@ -214,6 +220,27 @@ export async function withProtocolTimeout<T>(
|
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
222
|
|
|
223
|
+
function readFileWriteChunk(request: Record<string, unknown>) {
|
|
224
|
+
const file = request.file as
|
|
225
|
+
| { path?: unknown; offset?: unknown; total_size?: unknown; data?: { byteLength?: unknown } }
|
|
226
|
+
| undefined;
|
|
227
|
+
const offset = Number(file?.offset);
|
|
228
|
+
const totalSize = Number(file?.total_size);
|
|
229
|
+
const length = Number(file?.data?.byteLength);
|
|
230
|
+
if (
|
|
231
|
+
typeof file?.path !== 'string' ||
|
|
232
|
+
!Number.isSafeInteger(offset) ||
|
|
233
|
+
offset < 0 ||
|
|
234
|
+
!Number.isSafeInteger(totalSize) ||
|
|
235
|
+
!Number.isSafeInteger(length) ||
|
|
236
|
+
length <= 0 ||
|
|
237
|
+
offset + length > totalSize
|
|
238
|
+
) {
|
|
239
|
+
throw new Error('Protocol V2 file write pipeline chunk is invalid');
|
|
240
|
+
}
|
|
241
|
+
return { path: file.path, offset, end: offset + length, totalSize };
|
|
242
|
+
}
|
|
243
|
+
|
|
217
244
|
export class ProtocolV2Session {
|
|
218
245
|
private readonly options: ProtocolV2SessionOptions;
|
|
219
246
|
|
|
@@ -298,6 +325,9 @@ export class ProtocolV2Session {
|
|
|
298
325
|
data: Record<string, unknown>,
|
|
299
326
|
callOptions: ProtocolV2CallOptions
|
|
300
327
|
): Promise<MessageFromOneKey> {
|
|
328
|
+
if (callOptions.fileWritePipeline) {
|
|
329
|
+
return this.executeFileWritePipeline(name, data, callOptions, callOptions.fileWritePipeline);
|
|
330
|
+
}
|
|
301
331
|
const {
|
|
302
332
|
schemas,
|
|
303
333
|
router,
|
|
@@ -376,68 +406,8 @@ export class ProtocolV2Session {
|
|
|
376
406
|
const rxFrame = await readFrame(baseCallContext);
|
|
377
407
|
if (abortController.signal.aborted) break;
|
|
378
408
|
|
|
379
|
-
|
|
380
|
-
|
|
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);
|
|
409
|
+
const response = this.parseResponseFrame(rxFrame, [protoSeq]);
|
|
410
|
+
if (response) {
|
|
441
411
|
const { pendingResponseAfterWrite } = this;
|
|
442
412
|
let belongsToWriteOnlyCall = false;
|
|
443
413
|
if (pendingResponseAfterWrite) {
|
|
@@ -486,6 +456,273 @@ export class ProtocolV2Session {
|
|
|
486
456
|
}
|
|
487
457
|
);
|
|
488
458
|
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Validates one received frame for this session. Returns undefined for an ACK of one of
|
|
462
|
+
* `ackSequences`, otherwise the decoded response.
|
|
463
|
+
*/
|
|
464
|
+
private parseResponseFrame(
|
|
465
|
+
rxFrame: Uint8Array,
|
|
466
|
+
ackSequences: readonly number[]
|
|
467
|
+
): MessageFromOneKey | undefined {
|
|
468
|
+
const { schemas, router, packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND } = this.options;
|
|
469
|
+
let header: ReturnType<typeof ProtocolV2.inspectFrameHeader>;
|
|
470
|
+
try {
|
|
471
|
+
header = ProtocolV2.inspectFrameHeader(rxFrame);
|
|
472
|
+
} catch (cause) {
|
|
473
|
+
throw new ProtocolV2LinkError(
|
|
474
|
+
'frame',
|
|
475
|
+
`Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
|
|
476
|
+
cause
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
if (header.router !== router) {
|
|
480
|
+
throw new ProtocolV2LinkError(
|
|
481
|
+
'router',
|
|
482
|
+
`Protocol V2 router mismatch: expected ${router}, got ${header.router}`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (header.packetSrc !== packetSrc) {
|
|
486
|
+
throw new ProtocolV2LinkError(
|
|
487
|
+
'packet-source',
|
|
488
|
+
`Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let isAck: boolean;
|
|
493
|
+
try {
|
|
494
|
+
isAck = ProtocolV2.isAckFrame(rxFrame);
|
|
495
|
+
} catch (cause) {
|
|
496
|
+
throw new ProtocolV2LinkError(
|
|
497
|
+
'frame',
|
|
498
|
+
`Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
|
|
499
|
+
cause
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (isAck) {
|
|
503
|
+
if (!ackSequences.includes(header.seq)) {
|
|
504
|
+
throw new ProtocolV2LinkError(
|
|
505
|
+
'ack-sequence',
|
|
506
|
+
`Protocol V2 ACK sequence mismatch: expected ${ackSequences.join('|')}, got ${header.seq}`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
|
|
513
|
+
try {
|
|
514
|
+
decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
|
|
515
|
+
} catch (cause) {
|
|
516
|
+
throw new ProtocolV2LinkError(
|
|
517
|
+
'frame',
|
|
518
|
+
`Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
|
|
519
|
+
cause
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
// Firmware owns one TX sequence across all channels and packet sources,
|
|
523
|
+
// so frames routed elsewhere create valid gaps in this session.
|
|
524
|
+
if (this.lastResponseSequence === decoded.seq) {
|
|
525
|
+
throw new ProtocolV2LinkError(
|
|
526
|
+
'response-sequence',
|
|
527
|
+
`Protocol V2 duplicate response sequence: ${decoded.seq}`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
this.lastResponseSequence = decoded.seq;
|
|
531
|
+
return check.call(decoded);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Writes one file as ordered FilesystemFileWrite chunks while keeping up to
|
|
536
|
+
* PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW chunks in flight. Firmware still processes and
|
|
537
|
+
* answers every chunk in order, so responses are matched to chunks by position. Upstream
|
|
538
|
+
* bootloaders append at their own cursor without checking the request offset, so the echoed
|
|
539
|
+
* offset is what exposes a lost chunk. A device Failure, or `next` returning undefined, stops
|
|
540
|
+
* sending; the remaining in-flight responses are drained so the link stays usable. The call
|
|
541
|
+
* then returns the Failure like a serial call, or the last confirmation, which the caller
|
|
542
|
+
* checks for completeness.
|
|
543
|
+
*/
|
|
544
|
+
private async executeFileWritePipeline(
|
|
545
|
+
name: string,
|
|
546
|
+
data: Record<string, unknown>,
|
|
547
|
+
callOptions: ProtocolV2CallOptions,
|
|
548
|
+
pipeline: ProtocolV2FileWritePipeline
|
|
549
|
+
): Promise<MessageFromOneKey> {
|
|
550
|
+
if (name !== 'FilesystemFileWrite') {
|
|
551
|
+
throw new Error(`Protocol V2 file write pipeline does not support ${name}`);
|
|
552
|
+
}
|
|
553
|
+
if (this.pendingResponseAfterWrite) {
|
|
554
|
+
throw new Error('Protocol V2 file write pipeline cannot start while a response is pending');
|
|
555
|
+
}
|
|
556
|
+
const {
|
|
557
|
+
schemas,
|
|
558
|
+
router,
|
|
559
|
+
packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
560
|
+
maxFrameBytes,
|
|
561
|
+
prepareCall,
|
|
562
|
+
writeFrame,
|
|
563
|
+
readFrame,
|
|
564
|
+
logger,
|
|
565
|
+
logPrefix = 'ProtocolV2',
|
|
566
|
+
createTimeoutError,
|
|
567
|
+
generation = 0,
|
|
568
|
+
} = this.options;
|
|
569
|
+
const timeoutMs =
|
|
570
|
+
callOptions.timeoutMs && callOptions.timeoutMs > 0
|
|
571
|
+
? callOptions.timeoutMs
|
|
572
|
+
: PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
|
|
573
|
+
const abortController = new AbortController();
|
|
574
|
+
const context: ProtocolV2CallContext = {
|
|
575
|
+
messageName: name,
|
|
576
|
+
timeoutMs,
|
|
577
|
+
highThroughput: true,
|
|
578
|
+
...(callOptions.writeWithResponse === undefined
|
|
579
|
+
? {}
|
|
580
|
+
: { writeWithResponse: callOptions.writeWithResponse }),
|
|
581
|
+
generation,
|
|
582
|
+
signal: abortController.signal,
|
|
583
|
+
};
|
|
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
|
+
() =>
|
|
590
|
+
createTimeoutError
|
|
591
|
+
? createTimeoutError(name, timeoutMs)
|
|
592
|
+
: new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
|
|
593
|
+
() => {
|
|
594
|
+
abortController.abort();
|
|
595
|
+
}
|
|
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; path: string; offset: number; end: number }> = [];
|
|
604
|
+
let firstRequest: Record<string, unknown> | undefined = data;
|
|
605
|
+
let path: string | undefined;
|
|
606
|
+
let totalSize: number | undefined;
|
|
607
|
+
let nextOffset: number | undefined;
|
|
608
|
+
let sourceDone = false;
|
|
609
|
+
let failure: MessageFromOneKey | undefined;
|
|
610
|
+
let lastConfirmed: MessageFromOneKey | undefined;
|
|
611
|
+
|
|
612
|
+
const sendNextChunk = async () => {
|
|
613
|
+
const request = firstRequest ?? (await pipeline.next());
|
|
614
|
+
firstRequest = undefined;
|
|
615
|
+
if (!request) {
|
|
616
|
+
sourceDone = true;
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const chunk = readFileWriteChunk(request);
|
|
620
|
+
if (
|
|
621
|
+
(path !== undefined && chunk.path !== path) ||
|
|
622
|
+
(totalSize !== undefined && chunk.totalSize !== totalSize) ||
|
|
623
|
+
(nextOffset !== undefined && chunk.offset !== nextOffset)
|
|
624
|
+
) {
|
|
625
|
+
throw new Error(
|
|
626
|
+
`Protocol V2 file write pipeline chunk out of order: expected offset ${String(
|
|
627
|
+
nextOffset
|
|
628
|
+
)}, got ${chunk.offset}`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
path = chunk.path;
|
|
632
|
+
totalSize = chunk.totalSize;
|
|
633
|
+
nextOffset = chunk.end;
|
|
634
|
+
|
|
635
|
+
const seq = this.sequenceCursor.next();
|
|
636
|
+
const frame = ProtocolV2.encodeFrame(schemas, name, request, {
|
|
637
|
+
packetSrc,
|
|
638
|
+
router,
|
|
639
|
+
seq,
|
|
640
|
+
});
|
|
641
|
+
if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
|
|
642
|
+
throw new Error(
|
|
643
|
+
`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
const writeStartedAt = Date.now();
|
|
647
|
+
await withStepTimeout(this.serializeWrite(() => writeFrame(frame, context)));
|
|
648
|
+
inFlight.push({ seq, path: chunk.path, offset: chunk.offset, end: chunk.end });
|
|
649
|
+
markProgress();
|
|
650
|
+
try {
|
|
651
|
+
callOptions.onWriteCompleted?.({
|
|
652
|
+
elapsedMs: Math.max(Date.now() - writeStartedAt, 0),
|
|
653
|
+
frameBytes: frame.byteLength,
|
|
654
|
+
});
|
|
655
|
+
} catch (error) {
|
|
656
|
+
logger?.error?.(`${logPrefix} write metrics callback failed: ${String(error)}`);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
await withStepTimeout(Promise.resolve().then(() => prepareCall?.(context)));
|
|
661
|
+
while (!sourceDone && inFlight.length < PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW) {
|
|
662
|
+
await sendNextChunk();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
while (inFlight.length > 0) {
|
|
666
|
+
const rxFrame = await withStepTimeout(
|
|
667
|
+
readFrame(context),
|
|
668
|
+
Math.max(progressDeadline - Date.now(), 1)
|
|
669
|
+
);
|
|
670
|
+
const response = this.parseResponseFrame(
|
|
671
|
+
rxFrame,
|
|
672
|
+
inFlight.map(item => item.seq)
|
|
673
|
+
);
|
|
674
|
+
if (!response) {
|
|
675
|
+
// ACK of an in-flight chunk; its FilesystemFile response follows.
|
|
676
|
+
} else if (response.type === 'FilesystemFile') {
|
|
677
|
+
const [expected] = inFlight;
|
|
678
|
+
const { message } = response;
|
|
679
|
+
if (
|
|
680
|
+
Number(message.processed_byte) !== expected.end ||
|
|
681
|
+
message.path !== expected.path ||
|
|
682
|
+
Number(message.offset) !== expected.offset ||
|
|
683
|
+
Number(message.total_size) !== totalSize
|
|
684
|
+
) {
|
|
685
|
+
throw new ProtocolV2LinkError(
|
|
686
|
+
'frame',
|
|
687
|
+
`Protocol V2 file write response does not match chunk ${expected.offset}-${
|
|
688
|
+
expected.end
|
|
689
|
+
}: got offset ${String(message.offset)} processed_byte ${String(
|
|
690
|
+
message.processed_byte
|
|
691
|
+
)}`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
inFlight.shift();
|
|
695
|
+
markProgress();
|
|
696
|
+
if (!failure) {
|
|
697
|
+
lastConfirmed = response;
|
|
698
|
+
pipeline.onResponse(response);
|
|
699
|
+
if (!sourceDone) {
|
|
700
|
+
await sendNextChunk();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
} else if (response.type === 'Failure') {
|
|
704
|
+
inFlight.shift();
|
|
705
|
+
markProgress();
|
|
706
|
+
failure = failure ?? response;
|
|
707
|
+
sourceDone = true;
|
|
708
|
+
} else if (COMMON_TERMINAL_RESPONSE_TYPES.has(response.type)) {
|
|
709
|
+
throw new ProtocolV2LinkError(
|
|
710
|
+
'frame',
|
|
711
|
+
`Protocol V2 file write pipeline received unexpected ${response.type}`
|
|
712
|
+
);
|
|
713
|
+
} else {
|
|
714
|
+
logger?.debug?.(
|
|
715
|
+
`[${logPrefix}] skip unexpected response for ${name}: expected=FilesystemFile got=${response.type}`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const terminal = failure ?? lastConfirmed;
|
|
721
|
+
if (!terminal) {
|
|
722
|
+
throw new ProtocolV2LinkError('frame', 'Protocol V2 file write pipeline ended unconfirmed');
|
|
723
|
+
}
|
|
724
|
+
return terminal;
|
|
725
|
+
}
|
|
489
726
|
}
|
|
490
727
|
|
|
491
728
|
export async function probeProtocolV2({
|