@onekeyfe/hd-transport 1.2.0-alpha.13 → 1.2.0-alpha.131

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 (59) hide show
  1. package/__tests__/messages.test.js +144 -4
  2. package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
  3. package/__tests__/protocol-v2-link-manager.test.js +176 -9
  4. package/__tests__/protocol-v2-usb-transport-base.test.js +36 -2
  5. package/__tests__/protocol-v2.test.js +540 -108
  6. package/__tests__/transport-log.test.js +79 -0
  7. package/dist/constants.d.ts +3 -1
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/index.d.ts +645 -217
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +679 -153
  12. package/dist/protocols/index.d.ts +17 -1
  13. package/dist/protocols/index.d.ts.map +1 -1
  14. package/dist/protocols/v2/ble-frame-writer.d.ts +15 -0
  15. package/dist/protocols/v2/ble-frame-writer.d.ts.map +1 -0
  16. package/dist/protocols/v2/decode.d.ts +11 -0
  17. package/dist/protocols/v2/decode.d.ts.map +1 -1
  18. package/dist/protocols/v2/errors.d.ts +16 -0
  19. package/dist/protocols/v2/errors.d.ts.map +1 -0
  20. package/dist/protocols/v2/frame-assembler.d.ts.map +1 -1
  21. package/dist/protocols/v2/index.d.ts +2 -0
  22. package/dist/protocols/v2/index.d.ts.map +1 -1
  23. package/dist/protocols/v2/link-manager.d.ts +5 -0
  24. package/dist/protocols/v2/link-manager.d.ts.map +1 -1
  25. package/dist/protocols/v2/session.d.ts +14 -4
  26. package/dist/protocols/v2/session.d.ts.map +1 -1
  27. package/dist/protocols/v2/usb-transport-base.d.ts +2 -0
  28. package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -1
  29. package/dist/serialization/protobuf/decode.d.ts.map +1 -1
  30. package/dist/serialization/protobuf/encode.d.ts.map +1 -1
  31. package/dist/types/messages.d.ts +301 -156
  32. package/dist/types/messages.d.ts.map +1 -1
  33. package/dist/types/transport.d.ts +21 -3
  34. package/dist/types/transport.d.ts.map +1 -1
  35. package/dist/utils/transportLog.d.ts +9 -0
  36. package/dist/utils/transportLog.d.ts.map +1 -0
  37. package/messages-protocol-v2.json +523 -449
  38. package/package.json +2 -2
  39. package/scripts/protobuf-build.sh +112 -32
  40. package/scripts/protobuf-patches/index.js +1 -0
  41. package/scripts/protobuf-types.js +10 -1
  42. package/src/constants.ts +30 -16
  43. package/src/index.ts +5 -1
  44. package/src/protocols/index.ts +114 -3
  45. package/src/protocols/v2/ble-frame-writer.ts +77 -0
  46. package/src/protocols/v2/crc8.ts +1 -1
  47. package/src/protocols/v2/decode.ts +49 -12
  48. package/src/protocols/v2/encode.ts +1 -1
  49. package/src/protocols/v2/errors.ts +54 -0
  50. package/src/protocols/v2/frame-assembler.ts +6 -4
  51. package/src/protocols/v2/index.ts +2 -0
  52. package/src/protocols/v2/link-manager.ts +65 -5
  53. package/src/protocols/v2/session.ts +224 -80
  54. package/src/protocols/v2/usb-transport-base.ts +42 -10
  55. package/src/serialization/protobuf/decode.ts +4 -1
  56. package/src/serialization/protobuf/encode.ts +5 -1
  57. package/src/types/messages.ts +375 -198
  58. package/src/types/transport.ts +30 -3
  59. package/src/utils/transportLog.ts +102 -0
@@ -1,4 +1,9 @@
1
- import { PROTO_DATA_TYPE_ACK, PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
1
+ import {
2
+ PROTO_DATA_TYPE_ACK,
3
+ PROTO_DATA_TYPE_PACKET,
4
+ PROTO_HEAD_CRC_SIZE,
5
+ PROTO_HEAD_SOF,
6
+ } from './constants';
2
7
  import { crc8 } from './crc8';
3
8
 
4
9
  export interface ProtoV2Frame {
@@ -8,9 +13,23 @@ export interface ProtoV2Frame {
8
13
  pbPayload: Uint8Array;
9
14
  /** Sequence number from the frame header */
10
15
  seq: number;
16
+ /** Routing channel from the frame header */
17
+ router: number;
18
+ /** Packet source from the frame header */
19
+ packetSrc: number;
20
+ /** Packet or ACK discriminator from the frame header */
21
+ dataType: number;
11
22
  }
12
23
 
13
- function validateFrame(data: Uint8Array): number {
24
+ export type ProtoV2FrameHeader = {
25
+ frameLen: number;
26
+ router: number;
27
+ packetSrc: number;
28
+ dataType: number;
29
+ seq: number;
30
+ };
31
+
32
+ export function inspectFrameHeader(data: Uint8Array): ProtoV2FrameHeader {
14
33
  if (data.length < PROTO_HEAD_CRC_SIZE) {
15
34
  throw new Error(`Protocol V2 frame too short: ${data.length} bytes`);
16
35
  }
@@ -23,8 +42,10 @@ function validateFrame(data: Uint8Array): number {
23
42
 
24
43
  const frameLen = data[1] + data[2] * 256;
25
44
 
26
- if (data.length < frameLen) {
27
- throw new Error(`Frame truncated: expected ${frameLen} bytes, got ${data.length}`);
45
+ if (data.length !== frameLen) {
46
+ throw new Error(
47
+ `Protocol V2 frame length mismatch: expected ${frameLen} bytes, got ${data.length}`
48
+ );
28
49
  }
29
50
 
30
51
  const expectedHeaderCrc = crc8(data, 3);
@@ -45,7 +66,22 @@ function validateFrame(data: Uint8Array): number {
45
66
  );
46
67
  }
47
68
 
48
- return frameLen;
69
+ // eslint-disable-next-line no-bitwise
70
+ const dataType = data[5] & 0x03;
71
+ // eslint-disable-next-line no-bitwise
72
+ const packetSrc = (data[5] >> 2) & 0x0f;
73
+ const seq = data[6];
74
+ if (seq === 0) {
75
+ throw new Error('Invalid Protocol V2 sequence: 0 is reserved');
76
+ }
77
+
78
+ return {
79
+ frameLen,
80
+ router: data[4],
81
+ packetSrc,
82
+ dataType,
83
+ seq,
84
+ };
49
85
  }
50
86
 
51
87
  export function isAckFrame(data: Uint8Array): boolean {
@@ -54,9 +90,9 @@ export function isAckFrame(data: Uint8Array): boolean {
54
90
  return false;
55
91
  }
56
92
 
57
- const frameLen = validateFrame(data);
58
- if (frameLen !== PROTO_HEAD_CRC_SIZE) {
59
- throw new Error(`Invalid Protocol V2 ACK frame length: ${frameLen}`);
93
+ const header = inspectFrameHeader(data);
94
+ if (header.frameLen !== PROTO_HEAD_CRC_SIZE) {
95
+ throw new Error(`Invalid Protocol V2 ACK frame length: ${header.frameLen}`);
60
96
  }
61
97
  return true;
62
98
  }
@@ -72,9 +108,10 @@ export function isAckFrame(data: Uint8Array): boolean {
72
108
  * Returns the decoded messageTypeId, raw protobuf payload, and sequence number.
73
109
  */
74
110
  export function decodeFrame(data: Uint8Array): ProtoV2Frame {
75
- const frameLen = validateFrame(data);
76
-
77
- const seq = data[6];
111
+ const { frameLen, router, packetSrc, dataType, seq } = inspectFrameHeader(data);
112
+ if (dataType !== PROTO_DATA_TYPE_PACKET) {
113
+ throw new Error(`Invalid Protocol V2 data type: expected packet, got ${dataType}`);
114
+ }
78
115
  // Payload spans bytes 7 to frameLen-2 (inclusive), excluding final CRC byte
79
116
  const payloadData = data.slice(7, frameLen - 1);
80
117
 
@@ -85,5 +122,5 @@ export function decodeFrame(data: Uint8Array): ProtoV2Frame {
85
122
  const messageTypeId = payloadData[0] + payloadData[1] * 256;
86
123
  const pbPayload = payloadData.slice(2);
87
124
 
88
- return { messageTypeId, pbPayload, seq };
125
+ return { messageTypeId, pbPayload, seq, router, packetSrc, dataType };
89
126
  }
@@ -43,7 +43,7 @@ export function encodeFrame(
43
43
  const payloadLen = payload ? payload.length : 0;
44
44
  const frameLen = payloadLen + PROTO_HEAD_CRC_SIZE;
45
45
  if (frameLen > PROTOCOL_V2_FRAME_MAX_BYTES) {
46
- throw new Error(`Protocol V2 frame too large: ${frameLen}`);
46
+ throw new Error(`Protocol V2 frame too large: ${frameLen} > ${PROTOCOL_V2_FRAME_MAX_BYTES}`);
47
47
  }
48
48
  const frame = new Uint8Array(frameLen);
49
49
 
@@ -0,0 +1,54 @@
1
+ export type ProtocolV2LinkErrorCode =
2
+ | 'response-timeout'
3
+ | 'io'
4
+ | 'generation'
5
+ | 'router'
6
+ | 'packet-source'
7
+ | 'ack-sequence'
8
+ | 'response-sequence'
9
+ | 'frame';
10
+
11
+ export class ProtocolV2LinkError extends Error {
12
+ readonly code: ProtocolV2LinkErrorCode;
13
+
14
+ readonly cause?: unknown;
15
+
16
+ constructor(code: ProtocolV2LinkErrorCode, message: string, cause?: unknown) {
17
+ super(message);
18
+ this.name = 'ProtocolV2LinkError';
19
+ this.code = code;
20
+ this.cause = cause;
21
+ }
22
+ }
23
+
24
+ export const isProtocolV2LinkError = (error: unknown): error is ProtocolV2LinkError =>
25
+ error instanceof ProtocolV2LinkError;
26
+
27
+ export type ProtocolV2LinkDisabledError = Error & {
28
+ name: 'ProtocolV2LinkDisabledError';
29
+ failureCode: string | number;
30
+ firmwareMessage: string;
31
+ };
32
+
33
+ export const createProtocolV2LinkDisabledError = (
34
+ failureCode: string | number,
35
+ firmwareMessage: string
36
+ ): ProtocolV2LinkDisabledError =>
37
+ Object.assign(new Error(firmwareMessage), {
38
+ name: 'ProtocolV2LinkDisabledError' as const,
39
+ failureCode,
40
+ firmwareMessage,
41
+ });
42
+
43
+ export const isProtocolV2LinkDisabledError = (
44
+ error: unknown
45
+ ): error is ProtocolV2LinkDisabledError =>
46
+ error instanceof Error &&
47
+ error.name === 'ProtocolV2LinkDisabledError' &&
48
+ 'failureCode' in error &&
49
+ 'firmwareMessage' in error;
50
+
51
+ export const isProtocolV2LinkDisabledFailure = (failureCode: unknown, firmwareMessage: unknown) =>
52
+ (failureCode === 'Failure_ProcessError' || failureCode === 5) &&
53
+ typeof firmwareMessage === 'string' &&
54
+ firmwareMessage.trim().toLowerCase() === 'link disabled';
@@ -1,6 +1,7 @@
1
1
  import { PROTOCOL_V2_FRAME_MAX_BYTES } from '../../constants';
2
2
  import { PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF, PROTO_PRE_HEAD_SIZE } from './constants';
3
3
  import { crc8 } from './crc8';
4
+ import { ProtocolV2LinkError } from './errors';
4
5
 
5
6
  export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
6
7
  const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
@@ -58,7 +59,7 @@ export class ProtocolV2FrameAssembler {
58
59
 
59
60
  if (this.buffer[0] !== PROTO_HEAD_SOF) {
60
61
  this.reset();
61
- throw new Error('Invalid Protocol V2 SOF');
62
+ throw new ProtocolV2LinkError('frame', 'Invalid Protocol V2 SOF');
62
63
  }
63
64
 
64
65
  const expectedLen = this.buffer[1] + this.buffer[2] * 256;
@@ -67,11 +68,11 @@ export class ProtocolV2FrameAssembler {
67
68
  // complete frame: without resetting, this poison prefix would stay in
68
69
  // the buffer forever and deadlock the caller's drain loop.
69
70
  this.reset();
70
- throw new Error(`Protocol V2 frame length too small: ${expectedLen}`);
71
+ throw new ProtocolV2LinkError('frame', `Protocol V2 frame length too small: ${expectedLen}`);
71
72
  }
72
73
  if (expectedLen > this.maxFrameBytes) {
73
74
  this.reset();
74
- throw new Error(`Protocol V2 frame too large: ${expectedLen}`);
75
+ throw new ProtocolV2LinkError('frame', `Protocol V2 frame too large: ${expectedLen}`);
75
76
  }
76
77
 
77
78
  if (this.buffer.length < PROTO_PRE_HEAD_SIZE) return undefined;
@@ -82,7 +83,8 @@ export class ProtocolV2FrameAssembler {
82
83
  const expectedHeaderCrc = crc8(this.buffer, 3);
83
84
  if (this.buffer[3] !== expectedHeaderCrc) {
84
85
  this.reset();
85
- throw new Error(
86
+ throw new ProtocolV2LinkError(
87
+ 'frame',
86
88
  `Protocol V2 header CRC mismatch: expected 0x${expectedHeaderCrc
87
89
  .toString(16)
88
90
  .padStart(2, '0')}`
@@ -2,4 +2,6 @@ export * from './constants';
2
2
  export * from './crc8';
3
3
  export * from './encode';
4
4
  export * from './decode';
5
+ export * from './errors';
5
6
  export * from './frame-assembler';
7
+ export * from './ble-frame-writer';
@@ -1,4 +1,5 @@
1
1
  import { ProtocolV2SequenceCursor } from './sequence-cursor';
2
+ import { ProtocolV2LinkError } from './errors';
2
3
  import { ProtocolV2Session, getErrorMessage } from './session';
3
4
 
4
5
  import type { MessageFromOneKey, TransportCallOptions } from '../../types';
@@ -40,6 +41,12 @@ export class ProtocolV2LinkManager<Key> {
40
41
 
41
42
  private readonly callQueues = new Map<Key, Promise<unknown>>();
42
43
 
44
+ private readonly generations = new Map<Key, number>();
45
+
46
+ private readonly invalidationReasons = new Map<Key, { generation: number; reason: string }>();
47
+
48
+ private readonly invalidations = new Map<Key, Promise<void>>();
49
+
43
50
  private readonly options: ProtocolV2LinkManagerOptions<Key>;
44
51
 
45
52
  constructor(options: ProtocolV2LinkManagerOptions<Key>) {
@@ -53,7 +60,11 @@ export class ProtocolV2LinkManager<Key> {
53
60
  data: Record<string, unknown>,
54
61
  options?: TransportCallOptions
55
62
  ): Promise<MessageFromOneKey> {
56
- const run = () => this.executeCall(key, createAdapter, name, data, options);
63
+ const generation = this.generations.get(key) ?? 0;
64
+ const run = () => {
65
+ this.assertCallGeneration(key, generation);
66
+ return this.executeCall(key, createAdapter, name, data, options);
67
+ };
57
68
  const previous = this.callQueues.get(key) ?? Promise.resolve();
58
69
  const result = previous.then(run, run);
59
70
  const queue = result.catch(() => undefined);
@@ -67,24 +78,61 @@ export class ProtocolV2LinkManager<Key> {
67
78
  return result;
68
79
  }
69
80
 
81
+ sendFlowControl(
82
+ key: Key,
83
+ createAdapter: () => ProtocolV2LinkAdapter,
84
+ name: string,
85
+ data: Record<string, unknown>
86
+ ): Promise<MessageFromOneKey> {
87
+ const generation = this.generations.get(key) ?? 0;
88
+ this.assertCallGeneration(key, generation);
89
+ return this.getOrCreateLink(key, createAdapter).session.sendFlowControl(name, data);
90
+ }
91
+
70
92
  async invalidateLink(key: Key, reason: string): Promise<void> {
93
+ const generation = (this.generations.get(key) ?? 0) + 1;
94
+ this.generations.set(key, generation);
95
+ this.invalidationReasons.set(key, { generation, reason });
96
+
97
+ const pendingInvalidation = this.invalidations.get(key);
71
98
  const link = this.links.get(key);
72
- if (!link) return;
99
+ if (!link) {
100
+ await pendingInvalidation;
101
+ return;
102
+ }
73
103
 
74
104
  this.links.delete(key);
75
105
  link.state.invalidatedReason = reason;
76
- await link.adapter.reset(reason);
77
- await this.options.onLinkInvalidated?.(key, reason);
106
+ const invalidation = (async () => {
107
+ await pendingInvalidation?.catch(() => undefined);
108
+ await link.adapter.reset(reason);
109
+ await this.options.onLinkInvalidated?.(key, reason);
110
+ })();
111
+ this.invalidations.set(key, invalidation);
112
+
113
+ try {
114
+ await invalidation;
115
+ } finally {
116
+ if (this.invalidations.get(key) === invalidation) {
117
+ this.invalidations.delete(key);
118
+ }
119
+ }
78
120
  }
79
121
 
80
122
  async invalidateAllLinks(reason: string): Promise<void> {
81
- await Promise.all(Array.from(this.links.keys(), key => this.invalidateLink(key, reason)));
123
+ const keys = new Set([
124
+ ...this.links.keys(),
125
+ ...this.callQueues.keys(),
126
+ ...this.invalidations.keys(),
127
+ ]);
128
+ await Promise.all(Array.from(keys, key => this.invalidateLink(key, reason)));
82
129
  }
83
130
 
84
131
  async dispose(reason: string): Promise<void> {
85
132
  await this.invalidateAllLinks(reason);
86
133
  this.sequences.clear();
87
134
  this.callQueues.clear();
135
+ this.invalidations.clear();
88
136
  }
89
137
 
90
138
  private getOrCreateLink(key: Key, createAdapter: () => ProtocolV2LinkAdapter): ProtocolV2Link {
@@ -157,4 +205,16 @@ export class ProtocolV2LinkManager<Key> {
157
205
  this.callQueues.delete(key);
158
206
  }
159
207
  }
208
+
209
+ private assertCallGeneration(key: Key, generation: number) {
210
+ const currentGeneration = this.generations.get(key) ?? 0;
211
+ if (currentGeneration === generation) return;
212
+
213
+ const invalidation = this.invalidationReasons.get(key);
214
+ const reason =
215
+ invalidation?.generation === currentGeneration
216
+ ? invalidation.reason
217
+ : 'Protocol V2 link generation changed';
218
+ throw new ProtocolV2LinkError('generation', reason);
219
+ }
160
220
  }