@onekeyfe/hd-transport 1.2.0-alpha.1 → 1.2.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/__tests__/messages.test.js +76 -0
- package/__tests__/protocol-v2-link-manager.test.js +326 -0
- package/__tests__/protocol-v2-usb-transport-base.test.js +296 -0
- package/__tests__/protocol-v2.test.js +249 -81
- package/dist/constants.d.ts +4 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +547 -235
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +442 -72
- package/dist/protocols/index.d.ts +2 -0
- package/dist/protocols/index.d.ts.map +1 -1
- package/dist/protocols/v2/constants.d.ts +1 -0
- package/dist/protocols/v2/constants.d.ts.map +1 -1
- package/dist/protocols/v2/decode.d.ts +1 -0
- package/dist/protocols/v2/decode.d.ts.map +1 -1
- package/dist/protocols/v2/link-manager.d.ts +35 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -0
- package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
- package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
- package/dist/protocols/v2/session.d.ts +16 -4
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +31 -0
- package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
- package/dist/serialization/protobuf/messages.d.ts.map +1 -1
- package/dist/types/messages.d.ts +329 -156
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +1 -1
- package/dist/types/transport.d.ts.map +1 -1
- package/messages-protocol-v2.json +760 -1110
- package/package.json +2 -2
- package/scripts/protobuf-build.sh +60 -142
- package/src/constants.ts +8 -2
- package/src/index.ts +8 -0
- package/src/protocols/index.ts +4 -1
- package/src/protocols/v2/constants.ts +1 -0
- package/src/protocols/v2/decode.ts +36 -17
- package/src/protocols/v2/link-manager.ts +160 -0
- package/src/protocols/v2/sequence-cursor.ts +10 -0
- package/src/protocols/v2/session.ts +80 -45
- package/src/protocols/v2/usb-transport-base.ts +194 -0
- package/src/serialization/protobuf/messages.ts +6 -1
- package/src/types/messages.ts +401 -186
- package/src/types/transport.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.11",
|
|
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": "05d3421ec00e07e99f6665e6436a612503e32dfc"
|
|
32
32
|
}
|
|
@@ -126,36 +126,6 @@ if [ -d "$SRC_PRO2_LATEST" ] && ls "$SRC_PRO2_LATEST"/messages*.proto 1>/dev/nul
|
|
|
126
126
|
| sed 's/^option /\/\/ option /' \
|
|
127
127
|
| grep -v ' reserved '
|
|
128
128
|
|
|
129
|
-
if ! grep -q '^message TonSignData ' "$SRC_PRO2_LEGACY"/messages*.proto; then
|
|
130
|
-
echo ''
|
|
131
|
-
echo '// --- TON signData (kept until firmware-pro2 legacy proto exports it) ---'
|
|
132
|
-
echo 'message TonSignData {'
|
|
133
|
-
echo ' repeated uint32 address_n = 1;'
|
|
134
|
-
echo ' required TonSignDataType type = 2;'
|
|
135
|
-
echo ' required bytes payload = 3;'
|
|
136
|
-
echo ' optional string schema = 4;'
|
|
137
|
-
echo ' required string appdomain = 5;'
|
|
138
|
-
echo ' required uint64 timestamp = 6;'
|
|
139
|
-
echo ' optional string from_address = 7;'
|
|
140
|
-
echo ' optional TonWalletVersion wallet_version = 8 [default=V4R2];'
|
|
141
|
-
echo ' optional uint32 wallet_id = 9 [default=698983191];'
|
|
142
|
-
echo ' optional TonWorkChain workchain = 10 [default=BASECHAIN];'
|
|
143
|
-
echo ' optional bool is_bounceable = 11 [default=false];'
|
|
144
|
-
echo ' optional bool is_testnet_only = 12 [default=false];'
|
|
145
|
-
echo ''
|
|
146
|
-
echo ' enum TonSignDataType {'
|
|
147
|
-
echo ' TEXT = 0;'
|
|
148
|
-
echo ' BINARY = 1;'
|
|
149
|
-
echo ' CELL = 2;'
|
|
150
|
-
echo ' }'
|
|
151
|
-
echo '}'
|
|
152
|
-
echo ''
|
|
153
|
-
echo 'message TonSignedData {'
|
|
154
|
-
echo ' optional bytes signature = 1;'
|
|
155
|
-
echo ' optional bytes digest = 2;'
|
|
156
|
-
echo '}'
|
|
157
|
-
fi
|
|
158
|
-
|
|
159
129
|
echo ''
|
|
160
130
|
echo '// --- Protocol V2 system messages ---'
|
|
161
131
|
grep -hv \
|
|
@@ -164,83 +134,50 @@ if [ -d "$SRC_PRO2_LATEST" ] && ls "$SRC_PRO2_LATEST"/messages*.proto 1>/dev/nul
|
|
|
164
134
|
"$SRC_PRO2_LATEST"/messages*.proto \
|
|
165
135
|
| grep -v ' reserved '
|
|
166
136
|
|
|
167
|
-
if ! grep -q '^message GetOnboardingStatus ' "$SRC_PRO2_LATEST"/messages*.proto; then
|
|
168
|
-
echo ''
|
|
169
|
-
echo '// --- Onboarding status (kept until firmware-pro2 latest proto exports it) ---'
|
|
170
|
-
echo 'enum OnboardingStep {'
|
|
171
|
-
echo ' ONBOARDING_STEP_UNKNOWN = 0;'
|
|
172
|
-
echo ' ONBOARDING_STEP_DEVICE_VERIFICATION = 1;'
|
|
173
|
-
echo ' ONBOARDING_STEP_PERSONALIZATION = 2;'
|
|
174
|
-
echo ' ONBOARDING_STEP_SETUP = 3;'
|
|
175
|
-
echo ' ONBOARDING_STEP_FIRMWARE = 4;'
|
|
176
|
-
echo '}'
|
|
177
|
-
echo ''
|
|
178
|
-
echo 'message GetOnboardingStatus {'
|
|
179
|
-
echo '}'
|
|
180
|
-
echo ''
|
|
181
|
-
echo 'message OnboardingStatus {'
|
|
182
|
-
echo ' message Setup {'
|
|
183
|
-
echo ' message NewDevice {'
|
|
184
|
-
echo ' optional bool seedcard_backup = 1;'
|
|
185
|
-
echo ' }'
|
|
186
|
-
echo ' message Restore {'
|
|
187
|
-
echo ' optional bool mnemonic = 1;'
|
|
188
|
-
echo ' optional bool seedcard = 2;'
|
|
189
|
-
echo ' }'
|
|
190
|
-
echo ' optional NewDevice new_device = 1;'
|
|
191
|
-
echo ' optional Restore restore = 2;'
|
|
192
|
-
echo ' }'
|
|
193
|
-
echo ' required OnboardingStep step = 1;'
|
|
194
|
-
echo ' optional Setup setup = 2;'
|
|
195
|
-
echo ' optional uint32 detail_code = 3;'
|
|
196
|
-
echo ' optional string detail_str = 4;'
|
|
197
|
-
echo '}'
|
|
198
|
-
fi
|
|
199
137
|
} > "$TMP_PROTO"
|
|
200
138
|
|
|
201
|
-
|
|
202
|
-
node - "$TMP_PROTO" <<'NODE'
|
|
203
|
-
const fs = require('fs');
|
|
204
|
-
|
|
205
|
-
const protoPath = process.argv[2];
|
|
206
|
-
const proto = fs.readFileSync(protoPath, 'utf8');
|
|
207
|
-
const updated = proto.replace(
|
|
208
|
-
/( MessageType_TonTxAck\s*=\s*11907[^\n]*;\n)/,
|
|
209
|
-
`$1 MessageType_TonSignData = 11908 [(wire_in) = true];\n MessageType_TonSignedData = 11909 [(wire_out) = true];\n`
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
if (updated === proto) {
|
|
213
|
-
throw new Error('Unable to insert TON signData MessageType entries into Pro2 schema');
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
fs.writeFileSync(protoPath, updated);
|
|
217
|
-
NODE
|
|
218
|
-
fi
|
|
219
|
-
|
|
220
|
-
if ! grep -q 'MessageType_GetOnboardingStatus' "$TMP_PROTO"; then
|
|
221
|
-
node - "$TMP_PROTO" <<'NODE'
|
|
139
|
+
node - "$TMP_PROTO" <<'NODE'
|
|
222
140
|
const fs = require('fs');
|
|
223
141
|
|
|
224
142
|
const protoPath = process.argv[2];
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
143
|
+
let proto = fs.readFileSync(protoPath, 'utf8');
|
|
144
|
+
|
|
145
|
+
const removeTopLevelMessage = (source, name) => {
|
|
146
|
+
const pattern = new RegExp(`(^|\\n)message\\s+${name}\\s*\\{`, 'm');
|
|
147
|
+
const match = pattern.exec(source);
|
|
148
|
+
if (!match) return source;
|
|
149
|
+
|
|
150
|
+
const start = match.index + (match[1] ? match[1].length : 0);
|
|
151
|
+
let index = start;
|
|
152
|
+
let depth = 0;
|
|
153
|
+
while (index < source.length) {
|
|
154
|
+
const char = source[index];
|
|
155
|
+
if (char === '{') {
|
|
156
|
+
depth += 1;
|
|
157
|
+
} else if (char === '}') {
|
|
158
|
+
depth -= 1;
|
|
159
|
+
if (depth === 0) {
|
|
160
|
+
index += 1;
|
|
161
|
+
while (index < source.length && /\s/.test(source[index])) index += 1;
|
|
162
|
+
return `${source.slice(0, start)}${source.slice(index)}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
index += 1;
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`Unterminated message block: ${name}`);
|
|
168
|
+
};
|
|
234
169
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
170
|
+
[
|
|
171
|
+
'Initialize',
|
|
172
|
+
'GetFeatures',
|
|
173
|
+
'OnekeyGetFeatures',
|
|
174
|
+
'Features',
|
|
175
|
+
].forEach(name => {
|
|
176
|
+
proto = removeTopLevelMessage(proto, name);
|
|
177
|
+
});
|
|
238
178
|
|
|
239
|
-
|
|
240
|
-
const fs = require('fs');
|
|
179
|
+
fs.writeFileSync(protoPath, proto);
|
|
241
180
|
|
|
242
|
-
const protoPath = process.argv[2];
|
|
243
|
-
const proto = fs.readFileSync(protoPath, 'utf8');
|
|
244
181
|
const messageNames = new Set(
|
|
245
182
|
Array.from(proto.matchAll(/^\s*message\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/gm)).map(
|
|
246
183
|
match => match[1]
|
|
@@ -252,17 +189,29 @@ const messageTypeNames = new Set(
|
|
|
252
189
|
)
|
|
253
190
|
);
|
|
254
191
|
const requiredMessages = [
|
|
192
|
+
'DeviceFactoryInfoSet',
|
|
193
|
+
'DeviceFactoryInfoGet',
|
|
194
|
+
'DeviceFactoryInfo',
|
|
195
|
+
'ProtocolInfoRequest',
|
|
196
|
+
'ProtocolInfo',
|
|
255
197
|
'Ping',
|
|
256
198
|
'Success',
|
|
257
199
|
'Failure',
|
|
258
|
-
'
|
|
259
|
-
'
|
|
200
|
+
'DeviceReboot',
|
|
201
|
+
'DeviceInfoGet',
|
|
260
202
|
'DeviceInfo',
|
|
261
|
-
'
|
|
262
|
-
'
|
|
263
|
-
'
|
|
264
|
-
'
|
|
265
|
-
'
|
|
203
|
+
'DeviceStatusGet',
|
|
204
|
+
'DeviceStatus',
|
|
205
|
+
'DevGetOnboardingStatus',
|
|
206
|
+
'DevOnboardingStatus',
|
|
207
|
+
'DeviceSessionGet',
|
|
208
|
+
'DeviceSession',
|
|
209
|
+
'DeviceSessionAskPin',
|
|
210
|
+
'DeviceSessionPinResult',
|
|
211
|
+
'DeviceFirmwareUpdateRequest',
|
|
212
|
+
'DeviceFirmwareUpdateStatusGet',
|
|
213
|
+
'DeviceFirmwareUpdateStatus',
|
|
214
|
+
'FilesystemPermissionFix',
|
|
266
215
|
'FilesystemPathInfo',
|
|
267
216
|
'FilesystemPathInfoQuery',
|
|
268
217
|
'FilesystemFile',
|
|
@@ -274,10 +223,7 @@ const requiredMessages = [
|
|
|
274
223
|
'FilesystemDirMake',
|
|
275
224
|
'FilesystemDirRemove',
|
|
276
225
|
'FilesystemFormat',
|
|
277
|
-
'
|
|
278
|
-
'TonSignedData',
|
|
279
|
-
'GetOnboardingStatus',
|
|
280
|
-
'OnboardingStatus',
|
|
226
|
+
'PortfolioUpdate',
|
|
281
227
|
];
|
|
282
228
|
const missingMessages = requiredMessages.filter(name => !messageNames.has(name));
|
|
283
229
|
const missingMessageTypes = requiredMessages.filter(name => !messageTypeNames.has(name));
|
|
@@ -288,39 +234,11 @@ if (missingMessages.length > 0 || missingMessageTypes.length > 0) {
|
|
|
288
234
|
', '
|
|
289
235
|
)}], messageTypes=[${missingMessageTypes.join(
|
|
290
236
|
', '
|
|
291
|
-
)}]. Make sure submodules/firmware-pro2 is checked out on branch
|
|
292
|
-
'(origin/
|
|
237
|
+
)}]. Make sure submodules/firmware-pro2 is checked out on branch dev ` +
|
|
238
|
+
'(origin/dev), which contains the latest Protocol V2 Device*/Filesystem* messages.'
|
|
293
239
|
);
|
|
294
240
|
}
|
|
295
241
|
|
|
296
|
-
// Provisional wire IDs injected by this script for messages the firmware proto does
|
|
297
|
-
// not export yet (pending firmware confirmation, see
|
|
298
|
-
// docs/protocol-v2-deviceinfo-field-gaps.md). If the firmware submodule starts
|
|
299
|
-
// exporting these MessageType entries itself, the injection above is skipped and the
|
|
300
|
-
// firmware-assigned IDs flow into TMP_PROTO — assert they still match the IDs the SDK
|
|
301
|
-
// was built against, and fail loudly on any drift.
|
|
302
|
-
const expectedInjectedIds = {
|
|
303
|
-
TonSignData: 11908,
|
|
304
|
-
TonSignedData: 11909,
|
|
305
|
-
GetOnboardingStatus: 60602,
|
|
306
|
-
OnboardingStatus: 60603,
|
|
307
|
-
};
|
|
308
|
-
const actualIds = {};
|
|
309
|
-
for (const match of proto.matchAll(/^\s*MessageType_([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(\d+)/gm)) {
|
|
310
|
-
actualIds[match[1]] = Number(match[2]);
|
|
311
|
-
}
|
|
312
|
-
const idMismatches = Object.entries(expectedInjectedIds)
|
|
313
|
-
.filter(([name, id]) => name in actualIds && actualIds[name] !== id)
|
|
314
|
-
.map(([name, id]) => `${name}: expected ${id}, firmware proto has ${actualIds[name]}`);
|
|
315
|
-
|
|
316
|
-
if (idMismatches.length > 0) {
|
|
317
|
-
throw new Error(
|
|
318
|
-
`Protocol V2 injected MessageType wire IDs conflict with the firmware proto: ${idMismatches.join(
|
|
319
|
-
'; '
|
|
320
|
-
)}. Update the injected IDs in protobuf-build.sh and the registry section in ` +
|
|
321
|
-
'docs/protocol-v2-deviceinfo-field-gaps.md.'
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
242
|
NODE
|
|
325
243
|
|
|
326
244
|
npx pbjs -t json \
|
|
@@ -345,8 +263,8 @@ else
|
|
|
345
263
|
# Unlike the optional Pro1 (legacy) section above, the firmware-pro2 submodule is
|
|
346
264
|
# the only source of the Protocol V2 schema, so a missing checkout is a hard error.
|
|
347
265
|
echo "firmware-pro2 latest protobuf schema not found at $SRC_PRO2_LATEST"
|
|
348
|
-
echo "The Protocol V2 schema requires firmware-pro2 on branch
|
|
266
|
+
echo "The Protocol V2 schema requires firmware-pro2 on branch dev."
|
|
349
267
|
echo "Run: git submodule update --init submodules/firmware-pro2"
|
|
350
|
-
echo "Then: git -C submodules/firmware-pro2 fetch origin
|
|
268
|
+
echo "Then: git -C submodules/firmware-pro2 fetch origin dev && git -C submodules/firmware-pro2 checkout origin/dev"
|
|
351
269
|
exit 1
|
|
352
270
|
fi
|
package/src/constants.ts
CHANGED
|
@@ -20,15 +20,21 @@ export const PROTOCOL_V1_ENVELOPE_HEADER_SIZE = 1 + 1 + PROTOCOL_V1_MESSAGE_HEAD
|
|
|
20
20
|
|
|
21
21
|
// ---- Protocol V2 (Pro2 USB / BLE transports) ----
|
|
22
22
|
|
|
23
|
-
/** Protocol V2 单帧最大长度,包含 header、payload 和 CRC;需容纳
|
|
23
|
+
/** Protocol V2 单帧最大长度,包含 header、payload 和 CRC;需容纳 4000 数据块及 protobuf 开销。 */
|
|
24
24
|
export const PROTOCOL_V2_FRAME_MAX_BYTES = 4608;
|
|
25
25
|
|
|
26
26
|
/** WebUSB 下 FilesystemFileWrite 的文件数据分块大小。 */
|
|
27
|
-
export const PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE =
|
|
27
|
+
export const PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE = 4000;
|
|
28
28
|
|
|
29
29
|
/** BLE 下 FilesystemFileWrite 的文件数据分块大小。 */
|
|
30
30
|
export const PROTOCOL_V2_BLE_FILE_CHUNK_SIZE = 1800;
|
|
31
31
|
|
|
32
|
+
/** BLE 下 FilesystemFileRead 的文件数据分块大小,受 Pro2 UART 1024B TX 缓冲限制。 */
|
|
33
|
+
export const PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE = 900;
|
|
34
|
+
|
|
35
|
+
/** Pro2 BLE/UART 接收 FIFO 必须容纳完整 Proto Link 帧。 */
|
|
36
|
+
export const PROTOCOL_V2_BLE_FRAME_MAX_BYTES = 2048;
|
|
37
|
+
|
|
32
38
|
/** @deprecated 使用按传输区分的 PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE / PROTOCOL_V2_BLE_FILE_CHUNK_SIZE。 */
|
|
33
39
|
export const PROTOCOL_V2_FILE_CHUNK_SIZE = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
|
|
34
40
|
|
package/src/index.ts
CHANGED
|
@@ -10,8 +10,11 @@ import {
|
|
|
10
10
|
} from './serialization';
|
|
11
11
|
import { PROTOCOL_V2_SYS_MESSAGE_THRESHOLD, ProtocolV1, ProtocolV2 } from './protocols';
|
|
12
12
|
import * as protocolV2Codec from './protocols/v2';
|
|
13
|
+
import { ProtocolV2LinkManager } from './protocols/v2/link-manager';
|
|
14
|
+
import { ProtocolV2UsbTransportBase } from './protocols/v2/usb-transport-base';
|
|
13
15
|
import {
|
|
14
16
|
ProtocolV2FrameAssembler,
|
|
17
|
+
ProtocolV2SequenceCursor,
|
|
15
18
|
ProtocolV2Session,
|
|
16
19
|
bytesToHex,
|
|
17
20
|
concatUint8Arrays,
|
|
@@ -49,6 +52,8 @@ export * from './protocols';
|
|
|
49
52
|
export * as protocolV1 from './protocols/v1';
|
|
50
53
|
export * as protocolV2 from './protocols/v2';
|
|
51
54
|
export * from './protocols/v2/session';
|
|
55
|
+
export * from './protocols/v2/link-manager';
|
|
56
|
+
export * from './protocols/v2/usb-transport-base';
|
|
52
57
|
|
|
53
58
|
export default {
|
|
54
59
|
check,
|
|
@@ -58,7 +63,10 @@ export default {
|
|
|
58
63
|
ProtocolV2,
|
|
59
64
|
PROTOCOL_V2_SYS_MESSAGE_THRESHOLD,
|
|
60
65
|
ProtocolV2FrameAssembler,
|
|
66
|
+
ProtocolV2LinkManager,
|
|
67
|
+
ProtocolV2SequenceCursor,
|
|
61
68
|
ProtocolV2Session,
|
|
69
|
+
ProtocolV2UsbTransportBase,
|
|
62
70
|
bytesToHex,
|
|
63
71
|
concatUint8Arrays,
|
|
64
72
|
createMessageFromName,
|
package/src/protocols/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { decodeMessage as decodeV1Message } from './v1/receive';
|
|
|
6
6
|
import { createMessageFromName, createMessageFromType } from '../serialization/protobuf/messages';
|
|
7
7
|
import { encode as encodeProtobuf } from '../serialization/protobuf/encode';
|
|
8
8
|
import { decode as decodeProtobuf } from '../serialization/protobuf/decode';
|
|
9
|
-
import { decodeFrame as decodeV2Frame, encodeProtobufFrame } from './v2';
|
|
9
|
+
import { decodeFrame as decodeV2Frame, encodeProtobufFrame, isAckFrame } from './v2';
|
|
10
10
|
|
|
11
11
|
import type { Root } from 'protobufjs/light';
|
|
12
12
|
import type { ProtocolV2DebugLogger } from './v2';
|
|
@@ -38,6 +38,7 @@ const resolveProtocolV2EncodeSchema = (name: string, schemas: ProtocolV2Schemas)
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
const PROTOCOL_V2_LEGACY_DECODE_ALLOWLIST = new Set([
|
|
41
|
+
'Failure',
|
|
41
42
|
'ButtonRequest',
|
|
42
43
|
'EntropyRequest',
|
|
43
44
|
'PinMatrixRequest',
|
|
@@ -73,6 +74,8 @@ export const ProtocolV1 = {
|
|
|
73
74
|
};
|
|
74
75
|
|
|
75
76
|
export const ProtocolV2 = {
|
|
77
|
+
isAckFrame,
|
|
78
|
+
|
|
76
79
|
encodeFrame(
|
|
77
80
|
schemas: ProtocolV2Schemas,
|
|
78
81
|
name: string,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
|
|
1
|
+
import { PROTO_DATA_TYPE_ACK, PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
|
|
2
2
|
import { crc8 } from './crc8';
|
|
3
3
|
import { logProtocolV2Debug } from './debug';
|
|
4
4
|
|
|
@@ -13,20 +13,7 @@ export interface ProtoV2Frame {
|
|
|
13
13
|
seq: number;
|
|
14
14
|
}
|
|
15
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 {
|
|
16
|
+
function validateFrame(data: Uint8Array): number {
|
|
30
17
|
if (data.length < PROTO_HEAD_CRC_SIZE) {
|
|
31
18
|
throw new Error(`Protocol V2 frame too short: ${data.length} bytes`);
|
|
32
19
|
}
|
|
@@ -43,7 +30,6 @@ export function decodeFrame(
|
|
|
43
30
|
throw new Error(`Frame truncated: expected ${frameLen} bytes, got ${data.length}`);
|
|
44
31
|
}
|
|
45
32
|
|
|
46
|
-
// Verify pre-header CRC (bytes 0-2)
|
|
47
33
|
const expectedHeaderCrc = crc8(data, 3);
|
|
48
34
|
if (data[3] !== expectedHeaderCrc) {
|
|
49
35
|
throw new Error(
|
|
@@ -53,7 +39,6 @@ export function decodeFrame(
|
|
|
53
39
|
);
|
|
54
40
|
}
|
|
55
41
|
|
|
56
|
-
// Verify frame CRC (all bytes except last)
|
|
57
42
|
const expectedFrameCrc = crc8(data, frameLen - 1);
|
|
58
43
|
if (data[frameLen - 1] !== expectedFrameCrc) {
|
|
59
44
|
throw new Error(
|
|
@@ -63,6 +48,40 @@ export function decodeFrame(
|
|
|
63
48
|
);
|
|
64
49
|
}
|
|
65
50
|
|
|
51
|
+
return frameLen;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isAckFrame(data: Uint8Array): boolean {
|
|
55
|
+
// eslint-disable-next-line no-bitwise
|
|
56
|
+
if (data.length < 6 || (data[5] & 0x03) !== PROTO_DATA_TYPE_ACK) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const frameLen = validateFrame(data);
|
|
61
|
+
if (frameLen !== PROTO_HEAD_CRC_SIZE) {
|
|
62
|
+
throw new Error(`Invalid Protocol V2 ACK frame length: ${frameLen}`);
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse and validate a Protocol V2 response frame.
|
|
69
|
+
*
|
|
70
|
+
* Validates:
|
|
71
|
+
* - SOF byte (0x5A)
|
|
72
|
+
* - Header CRC (bytes 0-2)
|
|
73
|
+
* - Frame CRC (full frame except last byte)
|
|
74
|
+
*
|
|
75
|
+
* Returns the decoded messageTypeId, raw protobuf payload, and sequence number.
|
|
76
|
+
*/
|
|
77
|
+
export function decodeFrame(
|
|
78
|
+
data: Uint8Array,
|
|
79
|
+
debugOptions?: ProtocolV2FrameDebugOptions
|
|
80
|
+
): ProtoV2Frame {
|
|
81
|
+
const frameLen = validateFrame(data);
|
|
82
|
+
const expectedHeaderCrc = data[3];
|
|
83
|
+
const expectedFrameCrc = data[frameLen - 1];
|
|
84
|
+
|
|
66
85
|
const seq = data[6];
|
|
67
86
|
// Payload spans bytes 7 to frameLen-2 (inclusive), excluding final CRC byte
|
|
68
87
|
const payloadData = data.slice(7, frameLen - 1);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { ProtocolV2SequenceCursor } from './sequence-cursor';
|
|
2
|
+
import { ProtocolV2Session, getErrorMessage } from './session';
|
|
3
|
+
|
|
4
|
+
import type { MessageFromOneKey, TransportCallOptions } from '../../types';
|
|
5
|
+
import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session';
|
|
6
|
+
|
|
7
|
+
export type ProtocolV2LinkErrorClassification = 'link-fatal' | 'recoverable';
|
|
8
|
+
|
|
9
|
+
export interface ProtocolV2LinkAdapter {
|
|
10
|
+
router: number;
|
|
11
|
+
maxFrameBytes?: number;
|
|
12
|
+
generation: number;
|
|
13
|
+
prepareCall(context: ProtocolV2CallContext): Promise<void> | void;
|
|
14
|
+
writeFrame(frame: Uint8Array, context: ProtocolV2CallContext): Promise<void>;
|
|
15
|
+
readFrame(context: ProtocolV2CallContext): Promise<Uint8Array>;
|
|
16
|
+
reset(reason: string): Promise<void> | void;
|
|
17
|
+
logger?: ProtocolV2SessionOptions['logger'];
|
|
18
|
+
logPrefix?: string;
|
|
19
|
+
createTimeoutError?: ProtocolV2SessionOptions['createTimeoutError'];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ProtocolV2LinkManagerOptions<Key> = {
|
|
23
|
+
getSchemas: () => ProtocolV2Schemas;
|
|
24
|
+
classifyError: (error: unknown) => ProtocolV2LinkErrorClassification;
|
|
25
|
+
onLinkInvalidated?: (key: Key, reason: string) => Promise<void> | void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type ProtocolV2Link = {
|
|
29
|
+
adapter: ProtocolV2LinkAdapter;
|
|
30
|
+
session: ProtocolV2Session;
|
|
31
|
+
state: {
|
|
32
|
+
invalidatedReason?: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export class ProtocolV2LinkManager<Key> {
|
|
37
|
+
private readonly links = new Map<Key, ProtocolV2Link>();
|
|
38
|
+
|
|
39
|
+
private readonly sequences = new Map<Key, ProtocolV2SequenceCursor>();
|
|
40
|
+
|
|
41
|
+
private readonly callQueues = new Map<Key, Promise<unknown>>();
|
|
42
|
+
|
|
43
|
+
private readonly options: ProtocolV2LinkManagerOptions<Key>;
|
|
44
|
+
|
|
45
|
+
constructor(options: ProtocolV2LinkManagerOptions<Key>) {
|
|
46
|
+
this.options = options;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
call(
|
|
50
|
+
key: Key,
|
|
51
|
+
createAdapter: () => ProtocolV2LinkAdapter,
|
|
52
|
+
name: string,
|
|
53
|
+
data: Record<string, unknown>,
|
|
54
|
+
options?: TransportCallOptions
|
|
55
|
+
): Promise<MessageFromOneKey> {
|
|
56
|
+
const run = () => this.executeCall(key, createAdapter, name, data, options);
|
|
57
|
+
const previous = this.callQueues.get(key) ?? Promise.resolve();
|
|
58
|
+
const result = previous.then(run, run);
|
|
59
|
+
const queue = result.catch(() => undefined);
|
|
60
|
+
this.callQueues.set(key, queue);
|
|
61
|
+
result
|
|
62
|
+
.then(
|
|
63
|
+
() => this.clearSettledCallQueue(key, queue),
|
|
64
|
+
() => this.clearSettledCallQueue(key, queue)
|
|
65
|
+
)
|
|
66
|
+
.catch(() => undefined);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async invalidateLink(key: Key, reason: string): Promise<void> {
|
|
71
|
+
const link = this.links.get(key);
|
|
72
|
+
if (!link) return;
|
|
73
|
+
|
|
74
|
+
this.links.delete(key);
|
|
75
|
+
link.state.invalidatedReason = reason;
|
|
76
|
+
await link.adapter.reset(reason);
|
|
77
|
+
await this.options.onLinkInvalidated?.(key, reason);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async invalidateAllLinks(reason: string): Promise<void> {
|
|
81
|
+
await Promise.all(Array.from(this.links.keys(), key => this.invalidateLink(key, reason)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async dispose(reason: string): Promise<void> {
|
|
85
|
+
await this.invalidateAllLinks(reason);
|
|
86
|
+
this.sequences.clear();
|
|
87
|
+
this.callQueues.clear();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private getOrCreateLink(key: Key, createAdapter: () => ProtocolV2LinkAdapter): ProtocolV2Link {
|
|
91
|
+
const existing = this.links.get(key);
|
|
92
|
+
if (existing) return existing;
|
|
93
|
+
|
|
94
|
+
const adapter = createAdapter();
|
|
95
|
+
const state: ProtocolV2Link['state'] = {};
|
|
96
|
+
const assertLinkActive = () => {
|
|
97
|
+
if (state.invalidatedReason) {
|
|
98
|
+
throw new Error(state.invalidatedReason);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
let sequenceCursor = this.sequences.get(key);
|
|
102
|
+
if (!sequenceCursor) {
|
|
103
|
+
sequenceCursor = new ProtocolV2SequenceCursor();
|
|
104
|
+
this.sequences.set(key, sequenceCursor);
|
|
105
|
+
}
|
|
106
|
+
const session = new ProtocolV2Session({
|
|
107
|
+
schemas: this.options.getSchemas(),
|
|
108
|
+
router: adapter.router,
|
|
109
|
+
maxFrameBytes: adapter.maxFrameBytes,
|
|
110
|
+
generation: adapter.generation,
|
|
111
|
+
sequenceCursor,
|
|
112
|
+
prepareCall: async context => {
|
|
113
|
+
assertLinkActive();
|
|
114
|
+
await adapter.prepareCall(context);
|
|
115
|
+
assertLinkActive();
|
|
116
|
+
},
|
|
117
|
+
writeFrame: async (frame, context) => {
|
|
118
|
+
assertLinkActive();
|
|
119
|
+
await adapter.writeFrame(frame, context);
|
|
120
|
+
assertLinkActive();
|
|
121
|
+
},
|
|
122
|
+
readFrame: async context => {
|
|
123
|
+
assertLinkActive();
|
|
124
|
+
const frame = await adapter.readFrame(context);
|
|
125
|
+
assertLinkActive();
|
|
126
|
+
return frame;
|
|
127
|
+
},
|
|
128
|
+
logger: adapter.logger,
|
|
129
|
+
logPrefix: adapter.logPrefix,
|
|
130
|
+
createTimeoutError: adapter.createTimeoutError,
|
|
131
|
+
});
|
|
132
|
+
const link = { adapter, session, state };
|
|
133
|
+
this.links.set(key, link);
|
|
134
|
+
return link;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async executeCall(
|
|
138
|
+
key: Key,
|
|
139
|
+
createAdapter: () => ProtocolV2LinkAdapter,
|
|
140
|
+
name: string,
|
|
141
|
+
data: Record<string, unknown>,
|
|
142
|
+
options?: TransportCallOptions
|
|
143
|
+
): Promise<MessageFromOneKey> {
|
|
144
|
+
try {
|
|
145
|
+
return await this.getOrCreateLink(key, createAdapter).session.call(name, data, options);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (this.options.classifyError(error) === 'link-fatal') {
|
|
148
|
+
const errorMessage = getErrorMessage(error) || 'unknown error';
|
|
149
|
+
await this.invalidateLink(key, `Protocol V2 link-fatal error: ${errorMessage}`);
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private clearSettledCallQueue(key: Key, queue: Promise<unknown>) {
|
|
156
|
+
if (this.callQueues.get(key) === queue) {
|
|
157
|
+
this.callQueues.delete(key);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|