@onekeyfe/hd-transport 1.1.27-patch.1 → 1.2.0-alpha.0
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 +2 -4
- package/__tests__/build-receive.test.js +6 -8
- package/__tests__/decode-features.test.js +3 -2
- package/__tests__/messages.test.js +8 -0
- package/__tests__/protocol-v2.test.js +754 -0
- package/dist/constants.d.ts +14 -5
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +934 -41
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +827 -84
- package/dist/protocols/index.d.ts +45 -0
- package/dist/protocols/index.d.ts.map +1 -0
- package/dist/protocols/v1/decode.d.ts +11 -0
- package/dist/protocols/v1/decode.d.ts.map +1 -0
- package/dist/protocols/v1/encode.d.ts +11 -0
- package/dist/protocols/v1/encode.d.ts.map +1 -0
- package/dist/protocols/v1/index.d.ts +5 -0
- package/dist/protocols/v1/index.d.ts.map +1 -0
- package/dist/protocols/v1/packets.d.ts +7 -0
- package/dist/protocols/v1/packets.d.ts.map +1 -0
- package/dist/{serialization → protocols/v1}/receive.d.ts +1 -1
- package/dist/protocols/v1/receive.d.ts.map +1 -0
- package/dist/protocols/v2/constants.d.ts +7 -0
- package/dist/protocols/v2/constants.d.ts.map +1 -0
- package/dist/protocols/v2/crc8.d.ts +3 -0
- package/dist/protocols/v2/crc8.d.ts.map +1 -0
- package/dist/protocols/v2/debug.d.ts +13 -0
- package/dist/protocols/v2/debug.d.ts.map +1 -0
- package/dist/protocols/v2/decode.d.ts +8 -0
- package/dist/protocols/v2/decode.d.ts.map +1 -0
- package/dist/protocols/v2/encode.d.ts +5 -0
- package/dist/protocols/v2/encode.d.ts.map +1 -0
- package/dist/protocols/v2/frame-assembler.d.ts +12 -0
- package/dist/protocols/v2/frame-assembler.d.ts.map +1 -0
- package/dist/protocols/v2/index.d.ts +7 -0
- package/dist/protocols/v2/index.d.ts.map +1 -0
- package/dist/protocols/v2/session.d.ts +50 -0
- package/dist/protocols/v2/session.d.ts.map +1 -0
- package/dist/serialization/index.d.ts +6 -3
- package/dist/serialization/index.d.ts.map +1 -1
- package/dist/serialization/protobuf/decode.d.ts.map +1 -1
- package/dist/serialization/protobuf/messages.d.ts +1 -1
- package/dist/serialization/protobuf/messages.d.ts.map +1 -1
- package/dist/types/messages.d.ts +461 -11
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +14 -2
- package/dist/types/transport.d.ts.map +1 -1
- package/dist/utils/logBlockCommand.d.ts.map +1 -1
- package/messages-protocol-v2.json +13369 -0
- package/package.json +3 -3
- package/scripts/protobuf-build.sh +314 -20
- package/scripts/protobuf-patches/TxInputType.js +1 -0
- package/scripts/protobuf-patches/index.js +2 -0
- package/scripts/protobuf-types.js +224 -18
- package/src/constants.ts +42 -6
- package/src/index.ts +39 -11
- package/src/protocols/index.ts +144 -0
- package/src/{serialization/protocol → protocols/v1}/decode.ts +4 -4
- package/src/{serialization/protocol → protocols/v1}/encode.ts +18 -13
- package/src/protocols/v1/index.ts +4 -0
- package/src/protocols/v1/packets.ts +53 -0
- package/src/{serialization → protocols/v1}/receive.ts +5 -5
- package/src/protocols/v2/constants.ts +6 -0
- package/src/protocols/v2/crc8.ts +34 -0
- package/src/protocols/v2/debug.ts +26 -0
- package/src/protocols/v2/decode.ts +92 -0
- package/src/protocols/v2/encode.ts +116 -0
- package/src/protocols/v2/frame-assembler.ts +98 -0
- package/src/protocols/v2/index.ts +6 -0
- package/src/protocols/v2/session.ts +429 -0
- package/src/serialization/index.ts +6 -5
- package/src/serialization/protobuf/decode.ts +7 -0
- package/src/serialization/protobuf/messages.ts +8 -4
- package/src/types/messages.ts +606 -13
- package/src/types/transport.ts +26 -2
- package/src/utils/logBlockCommand.ts +9 -1
- package/dist/serialization/protocol/decode.d.ts +0 -11
- package/dist/serialization/protocol/decode.d.ts.map +0 -1
- package/dist/serialization/protocol/encode.d.ts +0 -11
- package/dist/serialization/protocol/encode.d.ts.map +0 -1
- package/dist/serialization/protocol/index.d.ts +0 -3
- package/dist/serialization/protocol/index.d.ts.map +0 -1
- package/dist/serialization/receive.d.ts.map +0 -1
- package/dist/serialization/send.d.ts +0 -7
- package/dist/serialization/send.d.ts.map +0 -1
- package/src/serialization/protocol/index.ts +0 -2
- package/src/serialization/send.ts +0 -58
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import ByteBuffer from 'bytebuffer';
|
|
2
|
+
|
|
3
|
+
import { encode as encodeProtobuf } from '../../serialization/protobuf';
|
|
4
|
+
import { encodeEnvelope } from './encode';
|
|
5
|
+
import { createMessageFromName } from '../../serialization/protobuf/messages';
|
|
6
|
+
import { PROTOCOL_V1_CHUNK_PAYLOAD_SIZE, PROTOCOL_V1_REPORT_ID } from '../../constants';
|
|
7
|
+
|
|
8
|
+
import type { Root } from 'protobufjs/light';
|
|
9
|
+
|
|
10
|
+
export function encodeEnvelopeMessage(messages: Root, name: string, data: Record<string, unknown>) {
|
|
11
|
+
const { Message, messageTypeId } = createMessageFromName(messages, name);
|
|
12
|
+
|
|
13
|
+
const buffer = encodeProtobuf(Message, data);
|
|
14
|
+
return encodeEnvelope(buffer, {
|
|
15
|
+
addTrezorHeaders: false,
|
|
16
|
+
chunked: false,
|
|
17
|
+
messageTypeId,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const encodeMessageChunks = (
|
|
22
|
+
messages: Root,
|
|
23
|
+
name: string,
|
|
24
|
+
data: Record<string, unknown>
|
|
25
|
+
) => {
|
|
26
|
+
const { Message, messageTypeId } = createMessageFromName(messages, name);
|
|
27
|
+
const buffer = encodeProtobuf(Message, data);
|
|
28
|
+
return encodeEnvelope(buffer, {
|
|
29
|
+
addTrezorHeaders: true,
|
|
30
|
+
chunked: true,
|
|
31
|
+
messageTypeId,
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const encodeTransportPackets = (
|
|
36
|
+
messages: Root,
|
|
37
|
+
name: string,
|
|
38
|
+
data: Record<string, unknown>
|
|
39
|
+
) => {
|
|
40
|
+
const encodeBuffers = encodeMessageChunks(messages, name, data);
|
|
41
|
+
|
|
42
|
+
const outBuffers: ByteBuffer[] = [];
|
|
43
|
+
|
|
44
|
+
for (const buf of encodeBuffers) {
|
|
45
|
+
const chunkBuffer = new ByteBuffer(PROTOCOL_V1_CHUNK_PAYLOAD_SIZE + 1);
|
|
46
|
+
chunkBuffer.writeByte(PROTOCOL_V1_REPORT_ID);
|
|
47
|
+
chunkBuffer.append(buf);
|
|
48
|
+
chunkBuffer.reset();
|
|
49
|
+
outBuffers.push(chunkBuffer);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return outBuffers;
|
|
53
|
+
};
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import ByteBuffer from 'bytebuffer';
|
|
2
2
|
|
|
3
|
-
import * as decodeProtobuf from '
|
|
4
|
-
import
|
|
5
|
-
import { createMessageFromType } from '
|
|
3
|
+
import * as decodeProtobuf from '../../serialization/protobuf/decode';
|
|
4
|
+
import { decodeEnvelope } from './decode';
|
|
5
|
+
import { createMessageFromType } from '../../serialization/protobuf/messages';
|
|
6
6
|
|
|
7
7
|
import type { Root } from 'protobufjs/light';
|
|
8
8
|
|
|
9
|
-
export function
|
|
9
|
+
export function decodeMessage(messages: Root, data: string) {
|
|
10
10
|
const bytebuffer = ByteBuffer.wrap(data, 'hex');
|
|
11
11
|
|
|
12
|
-
const { typeId, buffer } =
|
|
12
|
+
const { typeId, buffer } = decodeEnvelope(bytebuffer);
|
|
13
13
|
const { Message, messageName } = createMessageFromType(messages, typeId);
|
|
14
14
|
const message = decodeProtobuf.decode(Message, buffer);
|
|
15
15
|
return {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CRC8_INIT } from './constants';
|
|
2
|
+
|
|
3
|
+
// Protocol V2 帧校验用的 CRC-8 查表,不是设备或业务数据。
|
|
4
|
+
// 表值与固件侧 crc8 实现保持一致;初始值见 CRC8_INIT。
|
|
5
|
+
export const CRC8_TABLE = new Uint8Array([
|
|
6
|
+
0x00, 0x5e, 0xbc, 0xe2, 0x61, 0x3f, 0xdd, 0x83, 0xc2, 0x9c, 0x7e, 0x20, 0xa3, 0xfd, 0x1f, 0x41,
|
|
7
|
+
0x9d, 0xc3, 0x21, 0x7f, 0xfc, 0xa2, 0x40, 0x1e, 0x5f, 0x01, 0xe3, 0xbd, 0x3e, 0x60, 0x82, 0xdc,
|
|
8
|
+
0x23, 0x7d, 0x9f, 0xc1, 0x42, 0x1c, 0xfe, 0xa0, 0xe1, 0xbf, 0x5d, 0x03, 0x80, 0xde, 0x3c, 0x62,
|
|
9
|
+
0xbe, 0xe0, 0x02, 0x5c, 0xdf, 0x81, 0x63, 0x3d, 0x7c, 0x22, 0xc0, 0x9e, 0x1d, 0x43, 0xa1, 0xff,
|
|
10
|
+
0x46, 0x18, 0xfa, 0xa4, 0x27, 0x79, 0x9b, 0xc5, 0x84, 0xda, 0x38, 0x66, 0xe5, 0xbb, 0x59, 0x07,
|
|
11
|
+
0xdb, 0x85, 0x67, 0x39, 0xba, 0xe4, 0x06, 0x58, 0x19, 0x47, 0xa5, 0xfb, 0x78, 0x26, 0xc4, 0x9a,
|
|
12
|
+
0x65, 0x3b, 0xd9, 0x87, 0x04, 0x5a, 0xb8, 0xe6, 0xa7, 0xf9, 0x1b, 0x45, 0xc6, 0x98, 0x7a, 0x24,
|
|
13
|
+
0xf8, 0xa6, 0x44, 0x1a, 0x99, 0xc7, 0x25, 0x7b, 0x3a, 0x64, 0x86, 0xd8, 0x5b, 0x05, 0xe7, 0xb9,
|
|
14
|
+
0x8c, 0xd2, 0x30, 0x6e, 0xed, 0xb3, 0x51, 0x0f, 0x4e, 0x10, 0xf2, 0xac, 0x2f, 0x71, 0x93, 0xcd,
|
|
15
|
+
0x11, 0x4f, 0xad, 0xf3, 0x70, 0x2e, 0xcc, 0x92, 0xd3, 0x8d, 0x6f, 0x31, 0xb2, 0xec, 0x0e, 0x50,
|
|
16
|
+
0xaf, 0xf1, 0x13, 0x4d, 0xce, 0x90, 0x72, 0x2c, 0x6d, 0x33, 0xd1, 0x8f, 0x0c, 0x52, 0xb0, 0xee,
|
|
17
|
+
0x32, 0x6c, 0x8e, 0xd0, 0x53, 0x0d, 0xef, 0xb1, 0xf0, 0xae, 0x4c, 0x12, 0x91, 0xcf, 0x2d, 0x73,
|
|
18
|
+
0xca, 0x94, 0x76, 0x28, 0xab, 0xf5, 0x17, 0x49, 0x08, 0x56, 0xb4, 0xea, 0x69, 0x37, 0xd5, 0x8b,
|
|
19
|
+
0x57, 0x09, 0xeb, 0xb5, 0x36, 0x68, 0x8a, 0xd4, 0x95, 0xcb, 0x29, 0x77, 0xf4, 0xaa, 0x48, 0x16,
|
|
20
|
+
0xe9, 0xb7, 0x55, 0x0b, 0x88, 0xd6, 0x34, 0x6a, 0x2b, 0x75, 0x97, 0xc9, 0x4a, 0x14, 0xf6, 0xa8,
|
|
21
|
+
0x74, 0x2a, 0xc8, 0x96, 0x15, 0x4b, 0xa9, 0xf7, 0xb6, 0xe8, 0x0a, 0x54, 0xd7, 0x89, 0x6b, 0x35,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 计算 data 前 len 个字节的 CRC-8,使用与固件侧一致的初始值。
|
|
26
|
+
*/
|
|
27
|
+
export function crc8(data: Uint8Array, len: number): number {
|
|
28
|
+
let crc = CRC8_INIT;
|
|
29
|
+
for (let i = 0; i < len; i++) {
|
|
30
|
+
// eslint-disable-next-line no-bitwise
|
|
31
|
+
crc = CRC8_TABLE[crc ^ data[i]];
|
|
32
|
+
}
|
|
33
|
+
return crc;
|
|
34
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ProtocolV2DebugLogger = {
|
|
2
|
+
debug?: (...args: unknown[]) => void;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type ProtocolV2FrameDebugOptions = {
|
|
6
|
+
logger?: ProtocolV2DebugLogger;
|
|
7
|
+
logPrefix?: string;
|
|
8
|
+
context?: string;
|
|
9
|
+
messageName?: string;
|
|
10
|
+
messageTypeId?: number;
|
|
11
|
+
pbPayloadLength?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function logProtocolV2Debug(
|
|
15
|
+
options: ProtocolV2FrameDebugOptions | undefined,
|
|
16
|
+
stage: string,
|
|
17
|
+
details: Record<string, unknown>
|
|
18
|
+
) {
|
|
19
|
+
options?.logger?.debug?.(`[${options.logPrefix ?? 'ProtocolV2'}] ${stage}`, {
|
|
20
|
+
...(options.context ? { context: options.context } : {}),
|
|
21
|
+
...(options.messageName ? { messageName: options.messageName } : {}),
|
|
22
|
+
...(options.messageTypeId !== undefined ? { messageTypeId: options.messageTypeId } : {}),
|
|
23
|
+
...(options.pbPayloadLength !== undefined ? { pbPayloadLength: options.pbPayloadLength } : {}),
|
|
24
|
+
...details,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
|
|
2
|
+
import { crc8 } from './crc8';
|
|
3
|
+
import { logProtocolV2Debug } from './debug';
|
|
4
|
+
|
|
5
|
+
import type { ProtocolV2FrameDebugOptions } from './debug';
|
|
6
|
+
|
|
7
|
+
export interface ProtoV2Frame {
|
|
8
|
+
/** Little-endian message type ID */
|
|
9
|
+
messageTypeId: number;
|
|
10
|
+
/** Raw protobuf-encoded payload (bytes after the 2-byte messageTypeId) */
|
|
11
|
+
pbPayload: Uint8Array;
|
|
12
|
+
/** Sequence number from the frame header */
|
|
13
|
+
seq: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse and validate a Protocol V2 response frame.
|
|
18
|
+
*
|
|
19
|
+
* Validates:
|
|
20
|
+
* - SOF byte (0x5A)
|
|
21
|
+
* - Header CRC (bytes 0-2)
|
|
22
|
+
* - Frame CRC (full frame except last byte)
|
|
23
|
+
*
|
|
24
|
+
* Returns the decoded messageTypeId, raw protobuf payload, and sequence number.
|
|
25
|
+
*/
|
|
26
|
+
export function decodeFrame(
|
|
27
|
+
data: Uint8Array,
|
|
28
|
+
debugOptions?: ProtocolV2FrameDebugOptions
|
|
29
|
+
): ProtoV2Frame {
|
|
30
|
+
if (data.length < PROTO_HEAD_CRC_SIZE) {
|
|
31
|
+
throw new Error(`Protocol V2 frame too short: ${data.length} bytes`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (data[0] !== PROTO_HEAD_SOF) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Invalid SOF byte: expected 0x5A, got 0x${data[0].toString(16).padStart(2, '0')}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const frameLen = data[1] + data[2] * 256;
|
|
41
|
+
|
|
42
|
+
if (data.length < frameLen) {
|
|
43
|
+
throw new Error(`Frame truncated: expected ${frameLen} bytes, got ${data.length}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Verify pre-header CRC (bytes 0-2)
|
|
47
|
+
const expectedHeaderCrc = crc8(data, 3);
|
|
48
|
+
if (data[3] !== expectedHeaderCrc) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Header CRC mismatch: expected 0x${expectedHeaderCrc
|
|
51
|
+
.toString(16)
|
|
52
|
+
.padStart(2, '0')}, got 0x${data[3].toString(16).padStart(2, '0')}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Verify frame CRC (all bytes except last)
|
|
57
|
+
const expectedFrameCrc = crc8(data, frameLen - 1);
|
|
58
|
+
if (data[frameLen - 1] !== expectedFrameCrc) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Frame CRC mismatch: expected 0x${expectedFrameCrc
|
|
61
|
+
.toString(16)
|
|
62
|
+
.padStart(2, '0')}, got 0x${data[frameLen - 1].toString(16).padStart(2, '0')}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const seq = data[6];
|
|
67
|
+
// Payload spans bytes 7 to frameLen-2 (inclusive), excluding final CRC byte
|
|
68
|
+
const payloadData = data.slice(7, frameLen - 1);
|
|
69
|
+
|
|
70
|
+
if (payloadData.length < 2) {
|
|
71
|
+
throw new Error(`Protocol V2 payload too short (need >=2 bytes for messageTypeId)`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const messageTypeId = payloadData[0] + payloadData[1] * 256;
|
|
75
|
+
const pbPayload = payloadData.slice(2);
|
|
76
|
+
|
|
77
|
+
logProtocolV2Debug(debugOptions, 'decode raw frame', {
|
|
78
|
+
frameLen,
|
|
79
|
+
dataLength: data.length,
|
|
80
|
+
router: data[4],
|
|
81
|
+
attr: data[5],
|
|
82
|
+
seq,
|
|
83
|
+
headerCrc: data[3],
|
|
84
|
+
expectedHeaderCrc,
|
|
85
|
+
frameCrc: data[frameLen - 1],
|
|
86
|
+
expectedFrameCrc,
|
|
87
|
+
messageTypeId,
|
|
88
|
+
pbPayloadLength: pbPayload.length,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return { messageTypeId, pbPayload, seq };
|
|
92
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { PROTO_DATA_TYPE_PACKET, PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
|
|
2
|
+
import { crc8 } from './crc8';
|
|
3
|
+
import { logProtocolV2Debug } from './debug';
|
|
4
|
+
import { PROTOCOL_V2_FRAME_MAX_BYTES } from '../../constants';
|
|
5
|
+
|
|
6
|
+
import type { ProtocolV2FrameDebugOptions } from './debug';
|
|
7
|
+
|
|
8
|
+
// Default sequence number when callers do not manage one themselves.
|
|
9
|
+
// Stateful per-session sequencing lives in ProtocolV2Session (session.ts),
|
|
10
|
+
// which advances the counter via nextProtoSeq() and passes it in explicitly.
|
|
11
|
+
const PROTO_SEQ_DEFAULT = 1;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Advance a Protocol V2 sequence counter: 1-255, wraps around skipping 0.
|
|
15
|
+
*/
|
|
16
|
+
export function nextProtoSeq(current: number): number {
|
|
17
|
+
return current >= 255 ? 1 : current + 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build a raw Protocol V2 frame (0x5A framing).
|
|
22
|
+
*
|
|
23
|
+
* Frame layout (PROTO_HEAD_CRC_SIZE = 8 overhead bytes):
|
|
24
|
+
* [0] SOF = 0x5A
|
|
25
|
+
* [1] frameLen low byte
|
|
26
|
+
* [2] frameLen high byte
|
|
27
|
+
* [3] CRC8 of bytes 0-2 (pre-header CRC)
|
|
28
|
+
* [4] router
|
|
29
|
+
* [5] attr = ((packetSrc & 0x0F) << 2) | dataType
|
|
30
|
+
* [6] seq (1-255, wraps skipping 0)
|
|
31
|
+
* [7..N-2] payload
|
|
32
|
+
* [N-1] CRC8 of bytes 0 to N-2 (frame CRC)
|
|
33
|
+
*/
|
|
34
|
+
export function encodeFrame(
|
|
35
|
+
payload: Uint8Array | null,
|
|
36
|
+
packetSrc?: number,
|
|
37
|
+
router?: number,
|
|
38
|
+
debugOptions?: ProtocolV2FrameDebugOptions,
|
|
39
|
+
seq?: number
|
|
40
|
+
): Uint8Array {
|
|
41
|
+
const resolvedPacketSrc = packetSrc ?? 0;
|
|
42
|
+
const resolvedRouter = router ?? 0;
|
|
43
|
+
const resolvedSeq = seq ?? PROTO_SEQ_DEFAULT;
|
|
44
|
+
if (!Number.isInteger(resolvedSeq) || resolvedSeq < 1 || resolvedSeq > 255) {
|
|
45
|
+
throw new Error(`Protocol V2 seq out of range (1-255): ${resolvedSeq}`);
|
|
46
|
+
}
|
|
47
|
+
const payloadLen = payload ? payload.length : 0;
|
|
48
|
+
const frameLen = payloadLen + PROTO_HEAD_CRC_SIZE;
|
|
49
|
+
if (frameLen > PROTOCOL_V2_FRAME_MAX_BYTES) {
|
|
50
|
+
throw new Error(`Protocol V2 frame too large: ${frameLen}`);
|
|
51
|
+
}
|
|
52
|
+
const frame = new Uint8Array(frameLen);
|
|
53
|
+
|
|
54
|
+
frame[0] = PROTO_HEAD_SOF;
|
|
55
|
+
frame[1] = frameLen % 256;
|
|
56
|
+
frame[2] = Math.floor(frameLen / 256) % 256;
|
|
57
|
+
frame[3] = 0; // placeholder — filled in below
|
|
58
|
+
frame[4] = resolvedRouter % 256;
|
|
59
|
+
frame[5] = (resolvedPacketSrc % 16) * 4 + (PROTO_DATA_TYPE_PACKET % 4);
|
|
60
|
+
frame[6] = resolvedSeq;
|
|
61
|
+
|
|
62
|
+
// CRC8 over first 3 bytes (SOF + length)
|
|
63
|
+
frame[3] = crc8(frame, 3);
|
|
64
|
+
|
|
65
|
+
if (payload && payloadLen > 0) {
|
|
66
|
+
frame.set(payload, 7);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// CRC8 over entire frame except last byte
|
|
70
|
+
frame[frameLen - 1] = crc8(frame, frameLen - 1);
|
|
71
|
+
|
|
72
|
+
logProtocolV2Debug(debugOptions, 'encode raw frame', {
|
|
73
|
+
frameLen,
|
|
74
|
+
payloadLen,
|
|
75
|
+
packetSrc: resolvedPacketSrc,
|
|
76
|
+
router: frame[4],
|
|
77
|
+
attr: frame[5],
|
|
78
|
+
seq: frame[6],
|
|
79
|
+
headerCrc: frame[3],
|
|
80
|
+
frameCrc: frame[frameLen - 1],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return frame;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a Protocol V2 frame carrying a protobuf message.
|
|
88
|
+
*
|
|
89
|
+
* Payload layout:
|
|
90
|
+
* [0-1] messageTypeId as little-endian uint16
|
|
91
|
+
* [2..] protobuf-encoded message bytes
|
|
92
|
+
*/
|
|
93
|
+
export function encodeProtobufFrame(
|
|
94
|
+
messageTypeId: number,
|
|
95
|
+
pbPayload: Uint8Array,
|
|
96
|
+
packetSrc?: number,
|
|
97
|
+
router?: number,
|
|
98
|
+
debugOptions?: ProtocolV2FrameDebugOptions,
|
|
99
|
+
seq?: number
|
|
100
|
+
): Uint8Array {
|
|
101
|
+
const payload = new Uint8Array(2 + pbPayload.length);
|
|
102
|
+
payload[0] = messageTypeId % 256;
|
|
103
|
+
payload[1] = Math.floor(messageTypeId / 256) % 256;
|
|
104
|
+
payload.set(pbPayload, 2);
|
|
105
|
+
return encodeFrame(
|
|
106
|
+
payload,
|
|
107
|
+
packetSrc,
|
|
108
|
+
router,
|
|
109
|
+
{
|
|
110
|
+
...debugOptions,
|
|
111
|
+
messageTypeId,
|
|
112
|
+
pbPayloadLength: pbPayload.length,
|
|
113
|
+
},
|
|
114
|
+
seq
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { PROTOCOL_V2_FRAME_MAX_BYTES } from '../../constants';
|
|
2
|
+
import { PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF, PROTO_PRE_HEAD_SIZE } from './constants';
|
|
3
|
+
import { crc8 } from './crc8';
|
|
4
|
+
|
|
5
|
+
export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
|
|
6
|
+
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
7
|
+
const result = new Uint8Array(totalLength);
|
|
8
|
+
let offset = 0;
|
|
9
|
+
for (const arr of arrays) {
|
|
10
|
+
result.set(arr, offset);
|
|
11
|
+
offset += arr.length;
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ProtocolV2FrameAssembler {
|
|
17
|
+
private buffer = new Uint8Array(0);
|
|
18
|
+
|
|
19
|
+
private readonly maxFrameBytes: number;
|
|
20
|
+
|
|
21
|
+
constructor(maxFrameBytes = PROTOCOL_V2_FRAME_MAX_BYTES) {
|
|
22
|
+
this.maxFrameBytes = maxFrameBytes;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
reset() {
|
|
26
|
+
this.buffer = new Uint8Array(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
push(chunk: Uint8Array): Uint8Array | undefined {
|
|
30
|
+
this.append(chunk);
|
|
31
|
+
return this.extractFrame();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Append a chunk (optional) and extract every complete frame currently
|
|
36
|
+
* buffered. Same validation/throw semantics as push(); push() stays
|
|
37
|
+
* backward compatible for callers that drain one frame at a time.
|
|
38
|
+
*/
|
|
39
|
+
drain(chunk: Uint8Array = new Uint8Array(0)): Uint8Array[] {
|
|
40
|
+
this.append(chunk);
|
|
41
|
+
const frames: Uint8Array[] = [];
|
|
42
|
+
for (;;) {
|
|
43
|
+
const frame = this.extractFrame();
|
|
44
|
+
if (!frame) break;
|
|
45
|
+
frames.push(frame);
|
|
46
|
+
}
|
|
47
|
+
return frames;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private append(chunk: Uint8Array) {
|
|
51
|
+
if (chunk.length > 0) {
|
|
52
|
+
this.buffer = concatUint8Arrays([this.buffer, chunk]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private extractFrame(): Uint8Array | undefined {
|
|
57
|
+
if (this.buffer.length < 3) return undefined;
|
|
58
|
+
|
|
59
|
+
if (this.buffer[0] !== PROTO_HEAD_SOF) {
|
|
60
|
+
this.reset();
|
|
61
|
+
throw new Error('Invalid Protocol V2 SOF');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const expectedLen = this.buffer[1] + this.buffer[2] * 256;
|
|
65
|
+
if (expectedLen < PROTO_HEAD_CRC_SIZE) {
|
|
66
|
+
// A declared length below the 8-byte frame overhead can never become a
|
|
67
|
+
// complete frame: without resetting, this poison prefix would stay in
|
|
68
|
+
// the buffer forever and deadlock the caller's drain loop.
|
|
69
|
+
this.reset();
|
|
70
|
+
throw new Error(`Protocol V2 frame length too small: ${expectedLen}`);
|
|
71
|
+
}
|
|
72
|
+
if (expectedLen > this.maxFrameBytes) {
|
|
73
|
+
this.reset();
|
|
74
|
+
throw new Error(`Protocol V2 frame too large: ${expectedLen}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (this.buffer.length < PROTO_PRE_HEAD_SIZE) return undefined;
|
|
78
|
+
|
|
79
|
+
// Validate the pre-header CRC (byte 3 covers bytes 0-2) as soon as the
|
|
80
|
+
// first 4 bytes arrive, so a corrupted length field fails fast instead of
|
|
81
|
+
// waiting for bytes that will never come.
|
|
82
|
+
const expectedHeaderCrc = crc8(this.buffer, 3);
|
|
83
|
+
if (this.buffer[3] !== expectedHeaderCrc) {
|
|
84
|
+
this.reset();
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Protocol V2 header CRC mismatch: expected 0x${expectedHeaderCrc
|
|
87
|
+
.toString(16)
|
|
88
|
+
.padStart(2, '0')}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (this.buffer.length < expectedLen) return undefined;
|
|
93
|
+
|
|
94
|
+
const frame = this.buffer.slice(0, expectedLen);
|
|
95
|
+
this.buffer = this.buffer.slice(expectedLen);
|
|
96
|
+
return frame;
|
|
97
|
+
}
|
|
98
|
+
}
|