@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.
Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/__tests__/messages.test.js +76 -0
  3. package/__tests__/protocol-v2-link-manager.test.js +326 -0
  4. package/__tests__/protocol-v2-usb-transport-base.test.js +296 -0
  5. package/__tests__/protocol-v2.test.js +249 -81
  6. package/dist/constants.d.ts +4 -2
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/index.d.ts +547 -235
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +442 -72
  11. package/dist/protocols/index.d.ts +2 -0
  12. package/dist/protocols/index.d.ts.map +1 -1
  13. package/dist/protocols/v2/constants.d.ts +1 -0
  14. package/dist/protocols/v2/constants.d.ts.map +1 -1
  15. package/dist/protocols/v2/decode.d.ts +1 -0
  16. package/dist/protocols/v2/decode.d.ts.map +1 -1
  17. package/dist/protocols/v2/link-manager.d.ts +35 -0
  18. package/dist/protocols/v2/link-manager.d.ts.map +1 -0
  19. package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
  20. package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
  21. package/dist/protocols/v2/session.d.ts +16 -4
  22. package/dist/protocols/v2/session.d.ts.map +1 -1
  23. package/dist/protocols/v2/usb-transport-base.d.ts +31 -0
  24. package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
  25. package/dist/serialization/protobuf/messages.d.ts.map +1 -1
  26. package/dist/types/messages.d.ts +329 -156
  27. package/dist/types/messages.d.ts.map +1 -1
  28. package/dist/types/transport.d.ts +1 -1
  29. package/dist/types/transport.d.ts.map +1 -1
  30. package/messages-protocol-v2.json +760 -1110
  31. package/package.json +2 -2
  32. package/scripts/protobuf-build.sh +60 -142
  33. package/src/constants.ts +8 -2
  34. package/src/index.ts +8 -0
  35. package/src/protocols/index.ts +4 -1
  36. package/src/protocols/v2/constants.ts +1 -0
  37. package/src/protocols/v2/decode.ts +36 -17
  38. package/src/protocols/v2/link-manager.ts +160 -0
  39. package/src/protocols/v2/sequence-cursor.ts +10 -0
  40. package/src/protocols/v2/session.ts +80 -45
  41. package/src/protocols/v2/usb-transport-base.ts +194 -0
  42. package/src/serialization/protobuf/messages.ts +6 -1
  43. package/src/types/messages.ts +401 -186
  44. package/src/types/transport.ts +1 -1
@@ -1,6 +1,6 @@
1
1
  import { PROTOCOL_V2_PACKET_SRC_COMMAND } from '../../constants';
2
2
  import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
3
- import { nextProtoSeq } from './encode';
3
+ import { ProtocolV2SequenceCursor } from './sequence-cursor';
4
4
  import { ProtocolV2 } from '..';
5
5
  import * as check from '../../utils/highlevel-checks';
6
6
  import { LogBlockCommand } from '../../utils/logBlockCommand';
@@ -13,6 +13,13 @@ export type ProtocolV2Schemas = {
13
13
  protocolV2: Root;
14
14
  };
15
15
 
16
+ export type ProtocolV2CallContext = {
17
+ messageName: string;
18
+ timeoutMs?: number;
19
+ highVolume: boolean;
20
+ generation: number;
21
+ };
22
+
16
23
  type ProtocolLogger = {
17
24
  debug?: (...args: any[]) => void;
18
25
  error?: (...args: any[]) => void;
@@ -22,11 +29,15 @@ export type ProtocolV2SessionOptions = {
22
29
  schemas: ProtocolV2Schemas;
23
30
  router: number;
24
31
  packetSrc?: number;
25
- writeFrame: (frame: Uint8Array) => Promise<void>;
26
- readFrame: () => Promise<Uint8Array>;
32
+ maxFrameBytes?: number;
33
+ prepareCall?: (context: ProtocolV2CallContext) => Promise<void> | void;
34
+ writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) => Promise<void>;
35
+ readFrame: (context: ProtocolV2CallContext) => Promise<Uint8Array>;
27
36
  logger?: ProtocolLogger;
28
37
  logPrefix?: string;
29
38
  createTimeoutError?: (name: string, timeoutMs: number) => Error;
39
+ sequenceCursor?: ProtocolV2SequenceCursor;
40
+ generation?: number;
30
41
  };
31
42
 
32
43
  export type ProtocolV2CallOptions = {
@@ -37,6 +48,7 @@ export type ProtocolV2CallOptions = {
37
48
  };
38
49
 
39
50
  export { concatUint8Arrays, ProtocolV2FrameAssembler };
51
+ export { ProtocolV2SequenceCursor };
40
52
 
41
53
  export function hexToBytes(hex: string): Uint8Array {
42
54
  const clean = hex.replace(/\s+/g, '');
@@ -229,24 +241,23 @@ export async function withProtocolTimeout<T>(
229
241
  }
230
242
  }
231
243
 
232
- // Watchdog for the write phase only. Writing a single frame (max 4608 bytes)
233
- // is a bounded transport operation, unlike the read phase where long-running
234
- // device operations (firmware install, user confirmation) can legitimately
235
- // take minutes so no default timeout is applied to reads.
236
- export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 30000;
244
+ // Write completion is owned by the concrete transport. A Promise.race watchdog
245
+ // here can return while libusb/WebUSB is still flushing a large Protocol V2
246
+ // frame, which desynchronizes later request/response pairs.
247
+ export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 0;
237
248
 
238
249
  export class ProtocolV2Session {
239
250
  private readonly options: ProtocolV2SessionOptions;
240
251
 
252
+ private readonly sequenceCursor: ProtocolV2SequenceCursor;
253
+
241
254
  // Serializes call() invocations: responses are matched only by type, so two
242
255
  // in-flight calls on the same session would steal each other's responses.
243
256
  private pendingCall: Promise<unknown> = Promise.resolve();
244
257
 
245
- // Per-session sequence counter (1-255, wraps skipping 0).
246
- private protoSeq = 0;
247
-
248
258
  constructor(options: ProtocolV2SessionOptions) {
249
259
  this.options = options;
260
+ this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor();
250
261
  }
251
262
 
252
263
  call(
@@ -271,25 +282,41 @@ export class ProtocolV2Session {
271
282
  schemas,
272
283
  router,
273
284
  packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
285
+ maxFrameBytes,
286
+ prepareCall,
274
287
  writeFrame,
275
288
  readFrame,
276
289
  logger,
277
290
  logPrefix = 'ProtocolV2',
278
291
  createTimeoutError,
292
+ generation = 0,
279
293
  } = this.options;
280
294
 
281
295
  const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
282
- this.protoSeq = nextProtoSeq(this.protoSeq);
296
+ const callContext: ProtocolV2CallContext = {
297
+ messageName: name,
298
+ timeoutMs: callOptions.timeoutMs,
299
+ highVolume: shouldReduceDebug,
300
+ generation,
301
+ };
302
+ await prepareCall?.(callContext);
303
+ const protoSeq = this.sequenceCursor.next();
283
304
  const frame = ProtocolV2.encodeFrame(schemas, name, data, {
284
305
  packetSrc,
285
306
  router,
286
- seq: this.protoSeq,
307
+ seq: protoSeq,
287
308
  logger: shouldReduceDebug ? undefined : logger,
288
309
  logPrefix,
289
310
  context: `tx:${name}`,
290
311
  });
291
312
  const expectedSeq = frame[6];
292
313
 
314
+ if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
315
+ throw new Error(
316
+ `Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
317
+ );
318
+ }
319
+
293
320
  if (!shouldReduceDebug) {
294
321
  logger?.debug?.(
295
322
  `[${logPrefix}] TX payload name=${name}`,
@@ -305,7 +332,7 @@ export class ProtocolV2Session {
305
332
  // Lenient watchdog on the write phase only — see
306
333
  // PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS for the rationale.
307
334
  await withProtocolTimeout(
308
- writeFrame(frame),
335
+ writeFrame(frame, callContext),
309
336
  PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS,
310
337
  () =>
311
338
  new Error(
@@ -324,7 +351,7 @@ export class ProtocolV2Session {
324
351
  // terminal response. Consume those frames here so callers still see a
325
352
  // request/terminal-response shaped API.
326
353
  while (!cancellation.cancelled) {
327
- const rxFrame = await readFrame();
354
+ const rxFrame = await readFrame(callContext);
328
355
  if (cancellation.cancelled) {
329
356
  // Timed out while waiting: drop the late frame and stop reading.
330
357
  break;
@@ -336,37 +363,45 @@ export class ProtocolV2Session {
336
363
  } seq=${rxFrame[6]} headerHex=${frameHeaderDebugHex(rxFrame)}`
337
364
  );
338
365
  }
339
- const decoded = ProtocolV2.decodeFrame(schemas, rxFrame, {
340
- logger: shouldReduceDebug ? undefined : logger,
341
- logPrefix,
342
- context: `rx:${name}`,
343
- });
344
- if (!shouldReduceDebug && decoded.seq !== expectedSeq) {
345
- logger?.debug?.(
346
- `[${logPrefix}] seq differs for ${name}: tx=${expectedSeq}, rx=${decoded.seq}`
347
- );
348
- }
349
- if (!shouldReduceDebug) {
350
- logger?.debug?.(
351
- `[${logPrefix}] TX name=${name} seq=${expectedSeq} | RX seq=${decoded.seq} messageTypeId=${decoded.messageTypeId} pbPayload=${decoded.pbPayload.length}B`
352
- );
353
- logger?.debug?.(
354
- `[${logPrefix}] RX payload type=${decoded.type} messageTypeId=${decoded.messageTypeId}`,
355
- sanitizeProtocolV2DebugPayload(decoded.message)
356
- );
366
+ const isAck = ProtocolV2.isAckFrame(rxFrame);
367
+ if (isAck) {
368
+ if (!shouldReduceDebug) {
369
+ logger?.debug?.(`[${logPrefix}] skip Proto Link ACK seq=${rxFrame[6]}`);
370
+ }
357
371
  }
358
-
359
- const response = check.call(decoded);
360
- if (callOptions.intermediateTypes?.includes(response.type)) {
361
- callOptions.onIntermediateResponse?.(response);
362
- } else if (isExpectedTerminalResponse(response, callOptions.expectedTypes)) {
363
- return response;
364
- } else if (!shouldReduceDebug) {
365
- logger?.debug?.(
366
- `[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join(
367
- '|'
368
- )} got=${response.type}`
369
- );
372
+ if (!isAck) {
373
+ const decoded = ProtocolV2.decodeFrame(schemas, rxFrame, {
374
+ logger: shouldReduceDebug ? undefined : logger,
375
+ logPrefix,
376
+ context: `rx:${name}`,
377
+ });
378
+ if (!shouldReduceDebug && decoded.seq !== expectedSeq) {
379
+ logger?.debug?.(
380
+ `[${logPrefix}] seq differs for ${name}: tx=${expectedSeq}, rx=${decoded.seq}`
381
+ );
382
+ }
383
+ if (!shouldReduceDebug) {
384
+ logger?.debug?.(
385
+ `[${logPrefix}] TX name=${name} seq=${expectedSeq} | RX seq=${decoded.seq} messageTypeId=${decoded.messageTypeId} pbPayload=${decoded.pbPayload.length}B`
386
+ );
387
+ logger?.debug?.(
388
+ `[${logPrefix}] RX payload type=${decoded.type} messageTypeId=${decoded.messageTypeId}`,
389
+ sanitizeProtocolV2DebugPayload(decoded.message)
390
+ );
391
+ }
392
+
393
+ const response = check.call(decoded);
394
+ if (callOptions.intermediateTypes?.includes(response.type)) {
395
+ callOptions.onIntermediateResponse?.(response);
396
+ } else if (isExpectedTerminalResponse(response, callOptions.expectedTypes)) {
397
+ return response;
398
+ } else if (!shouldReduceDebug) {
399
+ logger?.debug?.(
400
+ `[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join(
401
+ '|'
402
+ )} got=${response.type}`
403
+ );
404
+ }
370
405
  }
371
406
  }
372
407
  // Only reachable after cancellation; the outer promise has already been
@@ -0,0 +1,194 @@
1
+ import { ProtocolV2FrameAssembler } from './frame-assembler';
2
+ import { ProtocolV2LinkManager } from './link-manager';
3
+
4
+ import type { MessageFromOneKey, TransportCallOptions } from '../../types';
5
+ import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session';
6
+
7
+ export type ProtocolV2UsbTransportBaseOptions = {
8
+ router: number;
9
+ maxFrameBytes: number;
10
+ logPrefix: string;
11
+ };
12
+
13
+ type ProtocolV2UsbCancellation = {
14
+ generation: number;
15
+ reason?: string;
16
+ promise: Promise<string>;
17
+ cancel: (reason: string) => void;
18
+ };
19
+
20
+ export abstract class ProtocolV2UsbTransportBase<Key> {
21
+ private readonly protocolV2UsbLinks: ProtocolV2LinkManager<Key>;
22
+
23
+ private readonly protocolV2UsbAssemblers = new Map<Key, ProtocolV2FrameAssembler>();
24
+
25
+ private readonly protocolV2UsbGenerations = new Map<Key, number>();
26
+
27
+ private readonly protocolV2UsbCancellations = new Map<Key, ProtocolV2UsbCancellation>();
28
+
29
+ private readonly protocolV2UsbOptions: ProtocolV2UsbTransportBaseOptions;
30
+
31
+ protected constructor(options: ProtocolV2UsbTransportBaseOptions) {
32
+ this.protocolV2UsbOptions = options;
33
+ this.protocolV2UsbLinks = new ProtocolV2LinkManager<Key>({
34
+ getSchemas: () => this.getProtocolV2UsbSchemas(),
35
+ classifyError: () => 'link-fatal',
36
+ onLinkInvalidated: async (key, reason) => {
37
+ this.protocolV2UsbAssemblers.get(key)?.reset();
38
+ await this.resetProtocolV2UsbNativeLink(key, reason);
39
+ await this.onProtocolV2UsbLinkInvalidated(key, reason);
40
+ },
41
+ });
42
+ }
43
+
44
+ protected abstract getProtocolV2UsbSchemas(): ProtocolV2Schemas;
45
+
46
+ protected abstract getProtocolV2UsbLogger(): ProtocolV2SessionOptions['logger'];
47
+
48
+ protected abstract writeProtocolV2UsbPacket(
49
+ key: Key,
50
+ frame: Uint8Array,
51
+ context: ProtocolV2CallContext
52
+ ): Promise<void>;
53
+
54
+ protected abstract readProtocolV2UsbPacket(
55
+ key: Key,
56
+ context: ProtocolV2CallContext
57
+ ): Promise<Uint8Array>;
58
+
59
+ protected abstract resetProtocolV2UsbNativeLink(key: Key, reason: string): Promise<void>;
60
+
61
+ protected abstract createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error;
62
+
63
+ protected onProtocolV2UsbLinkInvalidated(_key: Key, _reason: string): Promise<void> | void {
64
+ // 子类按需清理协议缓存或记录平台日志。
65
+ }
66
+
67
+ protected async rotateProtocolV2UsbGeneration(key: Key, reason: string): Promise<number> {
68
+ await this.protocolV2UsbLinks.invalidateLink(key, reason);
69
+ const generation = (this.protocolV2UsbGenerations.get(key) ?? 0) + 1;
70
+ this.protocolV2UsbGenerations.set(key, generation);
71
+ this.protocolV2UsbCancellations.set(key, this.createProtocolV2UsbCancellation(generation));
72
+ this.protocolV2UsbAssemblers.set(
73
+ key,
74
+ new ProtocolV2FrameAssembler(this.protocolV2UsbOptions.maxFrameBytes)
75
+ );
76
+ return generation;
77
+ }
78
+
79
+ protected callProtocolV2Usb(
80
+ key: Key,
81
+ name: string,
82
+ data: Record<string, unknown>,
83
+ options?: TransportCallOptions
84
+ ): Promise<MessageFromOneKey> {
85
+ return this.protocolV2UsbLinks.call(
86
+ key,
87
+ () => this.createProtocolV2UsbAdapter(key),
88
+ name,
89
+ data,
90
+ options
91
+ );
92
+ }
93
+
94
+ protected invalidateProtocolV2UsbLink(key: Key, reason: string): Promise<void> {
95
+ return this.protocolV2UsbLinks.invalidateLink(key, reason);
96
+ }
97
+
98
+ protected invalidateAllProtocolV2UsbLinks(reason: string): Promise<void> {
99
+ return this.protocolV2UsbLinks.invalidateAllLinks(reason);
100
+ }
101
+
102
+ protected async disposeProtocolV2UsbLinks(reason: string): Promise<void> {
103
+ await this.protocolV2UsbLinks.dispose(reason);
104
+ this.protocolV2UsbAssemblers.clear();
105
+ this.protocolV2UsbGenerations.clear();
106
+ this.protocolV2UsbCancellations.clear();
107
+ }
108
+
109
+ private createProtocolV2UsbAdapter(key: Key) {
110
+ const generation = this.protocolV2UsbGenerations.get(key);
111
+ if (generation === undefined) {
112
+ throw new Error('Protocol V2 USB generation has not been initialized');
113
+ }
114
+ const cancellation = this.protocolV2UsbCancellations.get(key);
115
+ if (!cancellation || cancellation.generation !== generation) {
116
+ throw new Error('Protocol V2 USB cancellation state has not been initialized');
117
+ }
118
+
119
+ const assertCurrentGeneration = () => {
120
+ if (cancellation.reason) {
121
+ throw new Error(cancellation.reason);
122
+ }
123
+ if (this.protocolV2UsbGenerations.get(key) !== generation) {
124
+ throw new Error('Protocol V2 USB connection generation changed');
125
+ }
126
+ };
127
+
128
+ return {
129
+ router: this.protocolV2UsbOptions.router,
130
+ maxFrameBytes: this.protocolV2UsbOptions.maxFrameBytes,
131
+ generation,
132
+ prepareCall: () => {
133
+ assertCurrentGeneration();
134
+ this.getProtocolV2UsbAssembler(key).reset();
135
+ },
136
+ writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
137
+ assertCurrentGeneration();
138
+ await this.writeProtocolV2UsbPacket(key, frame, context);
139
+ assertCurrentGeneration();
140
+ },
141
+ readFrame: async (context: ProtocolV2CallContext) => {
142
+ assertCurrentGeneration();
143
+ const assembler = this.getProtocolV2UsbAssembler(key);
144
+ let frame = assembler.push(new Uint8Array(0));
145
+ while (!frame) {
146
+ const packetRead = this.readProtocolV2UsbPacket(key, context).then(packet => ({
147
+ packet,
148
+ }));
149
+ const cancelled = cancellation.promise.then(reason => ({ reason }));
150
+ const result = await Promise.race([packetRead, cancelled]);
151
+ if ('reason' in result) {
152
+ throw new Error(result.reason);
153
+ }
154
+ assertCurrentGeneration();
155
+ frame = assembler.push(result.packet);
156
+ }
157
+ assertCurrentGeneration();
158
+ return frame;
159
+ },
160
+ reset: (reason: string) => {
161
+ cancellation.cancel(reason);
162
+ this.protocolV2UsbAssemblers.get(key)?.reset();
163
+ },
164
+ logger: this.getProtocolV2UsbLogger(),
165
+ logPrefix: this.protocolV2UsbOptions.logPrefix,
166
+ createTimeoutError: (messageName: string, timeoutMs: number) =>
167
+ this.createProtocolV2UsbTimeoutError(messageName, timeoutMs),
168
+ };
169
+ }
170
+
171
+ private getProtocolV2UsbAssembler(key: Key): ProtocolV2FrameAssembler {
172
+ const assembler = this.protocolV2UsbAssemblers.get(key);
173
+ if (!assembler) {
174
+ throw new Error('Protocol V2 USB assembler has not been initialized');
175
+ }
176
+ return assembler;
177
+ }
178
+
179
+ private createProtocolV2UsbCancellation(generation: number): ProtocolV2UsbCancellation {
180
+ let resolveCancellation: (reason: string) => void = () => undefined;
181
+ const cancellation: ProtocolV2UsbCancellation = {
182
+ generation,
183
+ promise: new Promise(resolve => {
184
+ resolveCancellation = resolve;
185
+ }),
186
+ cancel: reason => {
187
+ if (cancellation.reason) return;
188
+ cancellation.reason = reason;
189
+ resolveCancellation(reason);
190
+ },
191
+ };
192
+ return cancellation;
193
+ }
194
+ }
@@ -30,7 +30,12 @@ export const createMessageFromName = (messages: protobuf.Root, name: string) =>
30
30
  export const createMessageFromType = (messages: protobuf.Root, typeId: number) => {
31
31
  const MessageType = messages.lookupEnum('MessageType');
32
32
 
33
- const messageName = MessageType.valuesById[typeId].replace('MessageType_', '');
33
+ const rawMessageName = MessageType.valuesById[typeId];
34
+ if (!rawMessageName) {
35
+ throw new Error(`MessageType id "${typeId}" is not defined in protobuf schema`);
36
+ }
37
+
38
+ const messageName = rawMessageName.replace('MessageType_', '');
34
39
 
35
40
  const Message = messages.lookupType(messageName);
36
41