@onekeyfe/hd-transport 1.2.0-alpha.9 → 1.2.0-alpha.90

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 (64) hide show
  1. package/__tests__/messages.test.js +169 -11
  2. package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
  3. package/__tests__/protocol-v2-link-manager.test.js +120 -9
  4. package/__tests__/protocol-v2-usb-transport-base.test.js +36 -2
  5. package/__tests__/protocol-v2.test.js +438 -64
  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 +707 -242
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +597 -296
  12. package/dist/protocols/index.d.ts +13 -6
  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 +12 -2
  17. package/dist/protocols/v2/decode.d.ts.map +1 -1
  18. package/dist/protocols/v2/encode.d.ts +2 -3
  19. package/dist/protocols/v2/encode.d.ts.map +1 -1
  20. package/dist/protocols/v2/errors.d.ts +8 -0
  21. package/dist/protocols/v2/errors.d.ts.map +1 -0
  22. package/dist/protocols/v2/frame-assembler.d.ts.map +1 -1
  23. package/dist/protocols/v2/index.d.ts +2 -1
  24. package/dist/protocols/v2/index.d.ts.map +1 -1
  25. package/dist/protocols/v2/link-manager.d.ts +4 -0
  26. package/dist/protocols/v2/link-manager.d.ts.map +1 -1
  27. package/dist/protocols/v2/session.d.ts +10 -4
  28. package/dist/protocols/v2/session.d.ts.map +1 -1
  29. package/dist/protocols/v2/usb-transport-base.d.ts +1 -0
  30. package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -1
  31. package/dist/serialization/protobuf/decode.d.ts.map +1 -1
  32. package/dist/serialization/protobuf/encode.d.ts.map +1 -1
  33. package/dist/types/messages.d.ts +370 -144
  34. package/dist/types/messages.d.ts.map +1 -1
  35. package/dist/types/transport.d.ts +20 -3
  36. package/dist/types/transport.d.ts.map +1 -1
  37. package/dist/utils/transportLog.d.ts +9 -0
  38. package/dist/utils/transportLog.d.ts.map +1 -0
  39. package/messages-protocol-v2.json +1113 -547
  40. package/package.json +3 -3
  41. package/scripts/protobuf-build.sh +85 -253
  42. package/scripts/protobuf-patches/index.js +1 -0
  43. package/scripts/protobuf-types.js +19 -1
  44. package/src/constants.ts +30 -16
  45. package/src/index.ts +5 -1
  46. package/src/protocols/index.ts +37 -40
  47. package/src/protocols/v2/ble-frame-writer.ts +77 -0
  48. package/src/protocols/v2/crc8.ts +1 -1
  49. package/src/protocols/v2/decode.ts +50 -35
  50. package/src/protocols/v2/encode.ts +2 -28
  51. package/src/protocols/v2/errors.ts +25 -0
  52. package/src/protocols/v2/frame-assembler.ts +6 -4
  53. package/src/protocols/v2/index.ts +2 -1
  54. package/src/protocols/v2/link-manager.ts +54 -5
  55. package/src/protocols/v2/session.ts +151 -200
  56. package/src/protocols/v2/usb-transport-base.ts +29 -10
  57. package/src/serialization/protobuf/decode.ts +4 -1
  58. package/src/serialization/protobuf/encode.ts +5 -1
  59. package/src/types/messages.ts +450 -185
  60. package/src/types/transport.ts +28 -3
  61. package/src/utils/transportLog.ts +102 -0
  62. package/dist/protocols/v2/debug.d.ts +0 -13
  63. package/dist/protocols/v2/debug.d.ts.map +0 -1
  64. package/src/protocols/v2/debug.ts +0 -26
@@ -6,10 +6,14 @@ 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, isAckFrame } from './v2';
9
+ import {
10
+ decodeFrame as decodeV2Frame,
11
+ encodeProtobufFrame,
12
+ inspectFrameHeader,
13
+ isAckFrame,
14
+ } from './v2';
10
15
 
11
16
  import type { Root } from 'protobufjs/light';
12
- import type { ProtocolV2DebugLogger } from './v2';
13
17
 
14
18
  export const PROTOCOL_V2_SYS_MESSAGE_THRESHOLD = 60000;
15
19
 
@@ -23,9 +27,6 @@ type ProtocolV2FrameOptions = {
23
27
  router?: number;
24
28
  /** Sequence number (1-255). Managed per-session by ProtocolV2Session. */
25
29
  seq?: number;
26
- logger?: ProtocolV2DebugLogger;
27
- logPrefix?: string;
28
- context?: string;
29
30
  };
30
31
 
31
32
  const resolveProtocolV2EncodeSchema = (name: string, schemas: ProtocolV2Schemas) => {
@@ -75,6 +76,22 @@ export const ProtocolV1 = {
75
76
 
76
77
  export const ProtocolV2 = {
77
78
  isAckFrame,
79
+ inspectFrameHeader,
80
+
81
+ inspectFrame(schemas: ProtocolV2Schemas, frame: Uint8Array) {
82
+ const { messageTypeId, pbPayload, seq, router, packetSrc, dataType } = decodeV2Frame(frame);
83
+ const { messageName } = createProtocolV2MessageFromType(messageTypeId, schemas);
84
+ return {
85
+ messageName,
86
+ messageTypeId,
87
+ pbPayload,
88
+ seq,
89
+ router,
90
+ packetSrc,
91
+ dataType,
92
+ type: messageName,
93
+ };
94
+ },
78
95
 
79
96
  encodeFrame(
80
97
  schemas: ProtocolV2Schemas,
@@ -89,51 +106,31 @@ export const ProtocolV2 = {
89
106
  const rawPbBuffer = pbBuffer.toBuffer() as unknown as ArrayBuffer;
90
107
  const pbBytes = new Uint8Array(rawPbBuffer);
91
108
 
92
- options.logger?.debug?.(`[${options.logPrefix ?? 'ProtocolV2'}] encode protobuf`, {
93
- context: options.context ?? `encode:${name}`,
94
- messageName: name,
95
- messageTypeId,
96
- pbPayloadLength: pbBytes.length,
97
- packetSrc: options.packetSrc ?? 0,
98
- router: options.router ?? 0,
99
- });
100
-
101
109
  return encodeProtobufFrame(
102
110
  messageTypeId,
103
111
  pbBytes,
104
112
  options.packetSrc,
105
113
  options.router,
106
- {
107
- logger: options.logger,
108
- logPrefix: options.logPrefix,
109
- context: options.context ?? `encode:${name}`,
110
- messageName: name,
111
- },
112
114
  options.seq
113
115
  );
114
116
  },
115
117
 
116
- decodeFrame(
117
- schemas: ProtocolV2Schemas,
118
- frame: Uint8Array,
119
- options: Pick<ProtocolV2FrameOptions, 'logger' | 'logPrefix' | 'context'> = {}
120
- ) {
121
- const { messageTypeId, pbPayload, seq } = decodeV2Frame(frame, {
122
- logger: options.logger,
123
- logPrefix: options.logPrefix,
124
- context: options.context ?? 'decode',
125
- });
126
- const { Message, messageName } = createProtocolV2MessageFromType(messageTypeId, schemas);
118
+ decodeFrame(schemas: ProtocolV2Schemas, frame: Uint8Array) {
119
+ const { messageTypeId, pbPayload, seq, messageName } = this.inspectFrame(schemas, frame);
120
+ const { Message } = createProtocolV2MessageFromType(messageTypeId, schemas);
127
121
  const rxByteBuffer = ByteBuffer.wrap(Buffer.from(pbPayload) as unknown as ArrayBuffer);
128
- const message = decodeProtobuf(Message, rxByteBuffer);
129
-
130
- options.logger?.debug?.(`[${options.logPrefix ?? 'ProtocolV2'}] decode protobuf`, {
131
- context: options.context ?? `decode:${messageName}`,
132
- messageName,
133
- messageTypeId,
134
- pbPayloadLength: pbPayload.length,
135
- seq,
136
- });
122
+ let message: ReturnType<typeof decodeProtobuf>;
123
+ try {
124
+ message = decodeProtobuf(Message, rxByteBuffer);
125
+ } catch (cause) {
126
+ const error = new Error(
127
+ `Protocol V2 protobuf decode failed for "${messageName}" ` +
128
+ `(${messageTypeId}, ${pbPayload.length}-byte payload); ` +
129
+ 'the payload is malformed or incompatible with the active SDK schema.'
130
+ );
131
+ (error as Error & { cause?: unknown }).cause = cause;
132
+ throw error;
133
+ }
137
134
 
138
135
  return {
139
136
  message,
@@ -0,0 +1,77 @@
1
+ export type ProtocolV2BleFrameWriterOptions = {
2
+ frame: Uint8Array;
3
+ packetCapacity: number;
4
+ writePacket: (packet: Uint8Array, packetIndex: number) => Promise<void>;
5
+ assertActive?: () => void;
6
+ signal?: AbortSignal;
7
+ abortMessage?: string;
8
+ initialDelayMs?: number;
9
+ burstSize?: number;
10
+ burstPauseMs?: number;
11
+ flushDelayMs?: number;
12
+ wait?: (timeoutMs: number) => Promise<void>;
13
+ };
14
+
15
+ const defaultWait = (timeoutMs: number) =>
16
+ new Promise<void>(resolve => {
17
+ setTimeout(resolve, timeoutMs);
18
+ });
19
+
20
+ export async function writeProtocolV2BleFrame({
21
+ frame,
22
+ packetCapacity,
23
+ writePacket,
24
+ assertActive,
25
+ signal,
26
+ abortMessage = 'Protocol V2 BLE write aborted',
27
+ initialDelayMs = 0,
28
+ burstSize,
29
+ burstPauseMs = 0,
30
+ flushDelayMs = 0,
31
+ wait = defaultWait,
32
+ }: ProtocolV2BleFrameWriterOptions): Promise<void> {
33
+ if (!Number.isInteger(packetCapacity) || packetCapacity <= 0) {
34
+ throw new Error('Protocol V2 BLE packet capacity must be a positive integer');
35
+ }
36
+ if (burstSize !== undefined && (!Number.isInteger(burstSize) || burstSize <= 0)) {
37
+ throw new Error('Protocol V2 BLE burst size must be a positive integer');
38
+ }
39
+
40
+ const assertWritable = () => {
41
+ assertActive?.();
42
+ if (signal?.aborted) {
43
+ throw new Error(abortMessage);
44
+ }
45
+ };
46
+
47
+ if (frame.length > 0 && initialDelayMs > 0) {
48
+ assertWritable();
49
+ await wait(initialDelayMs);
50
+ assertWritable();
51
+ }
52
+
53
+ let packetsWritten = 0;
54
+ for (let offset = 0; offset < frame.length; offset += packetCapacity) {
55
+ assertWritable();
56
+ const packet = frame.slice(offset, offset + packetCapacity);
57
+ await writePacket(packet, packetsWritten);
58
+ packetsWritten += 1;
59
+ assertWritable();
60
+
61
+ const hasMorePackets = offset + packetCapacity < frame.length;
62
+ if (
63
+ hasMorePackets &&
64
+ burstSize !== undefined &&
65
+ packetsWritten % burstSize === 0 &&
66
+ burstPauseMs > 0
67
+ ) {
68
+ await wait(burstPauseMs);
69
+ assertWritable();
70
+ }
71
+ }
72
+
73
+ if (burstSize !== undefined && packetsWritten > burstSize && flushDelayMs > 0) {
74
+ await wait(flushDelayMs);
75
+ assertWritable();
76
+ }
77
+ }
@@ -22,7 +22,7 @@ export const CRC8_TABLE = new Uint8Array([
22
22
  ]);
23
23
 
24
24
  /**
25
- * 计算 data 前 len 个字节的 CRC-8,使用与固件侧一致的初始值。
25
+ * Compute CRC-8 over the first len bytes using the firmware-compatible initial value.
26
26
  */
27
27
  export function crc8(data: Uint8Array, len: number): number {
28
28
  let crc = CRC8_INIT;
@@ -1,8 +1,10 @@
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
- import { logProtocolV2Debug } from './debug';
4
-
5
- import type { ProtocolV2FrameDebugOptions } from './debug';
6
8
 
7
9
  export interface ProtoV2Frame {
8
10
  /** Little-endian message type ID */
@@ -11,9 +13,23 @@ export interface ProtoV2Frame {
11
13
  pbPayload: Uint8Array;
12
14
  /** Sequence number from the frame header */
13
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;
14
22
  }
15
23
 
16
- 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 {
17
33
  if (data.length < PROTO_HEAD_CRC_SIZE) {
18
34
  throw new Error(`Protocol V2 frame too short: ${data.length} bytes`);
19
35
  }
@@ -26,8 +42,10 @@ function validateFrame(data: Uint8Array): number {
26
42
 
27
43
  const frameLen = data[1] + data[2] * 256;
28
44
 
29
- if (data.length < frameLen) {
30
- 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
+ );
31
49
  }
32
50
 
33
51
  const expectedHeaderCrc = crc8(data, 3);
@@ -48,7 +66,22 @@ function validateFrame(data: Uint8Array): number {
48
66
  );
49
67
  }
50
68
 
51
- 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
+ };
52
85
  }
53
86
 
54
87
  export function isAckFrame(data: Uint8Array): boolean {
@@ -57,9 +90,9 @@ export function isAckFrame(data: Uint8Array): boolean {
57
90
  return false;
58
91
  }
59
92
 
60
- const frameLen = validateFrame(data);
61
- if (frameLen !== PROTO_HEAD_CRC_SIZE) {
62
- 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}`);
63
96
  }
64
97
  return true;
65
98
  }
@@ -74,15 +107,11 @@ export function isAckFrame(data: Uint8Array): boolean {
74
107
  *
75
108
  * Returns the decoded messageTypeId, raw protobuf payload, and sequence number.
76
109
  */
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
-
85
- const seq = data[6];
110
+ export function decodeFrame(data: Uint8Array): ProtoV2Frame {
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
+ }
86
115
  // Payload spans bytes 7 to frameLen-2 (inclusive), excluding final CRC byte
87
116
  const payloadData = data.slice(7, frameLen - 1);
88
117
 
@@ -93,19 +122,5 @@ export function decodeFrame(
93
122
  const messageTypeId = payloadData[0] + payloadData[1] * 256;
94
123
  const pbPayload = payloadData.slice(2);
95
124
 
96
- logProtocolV2Debug(debugOptions, 'decode raw frame', {
97
- frameLen,
98
- dataLength: data.length,
99
- router: data[4],
100
- attr: data[5],
101
- seq,
102
- headerCrc: data[3],
103
- expectedHeaderCrc,
104
- frameCrc: data[frameLen - 1],
105
- expectedFrameCrc,
106
- messageTypeId,
107
- pbPayloadLength: pbPayload.length,
108
- });
109
-
110
- return { messageTypeId, pbPayload, seq };
125
+ return { messageTypeId, pbPayload, seq, router, packetSrc, dataType };
111
126
  }
@@ -1,10 +1,7 @@
1
1
  import { PROTO_DATA_TYPE_PACKET, PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
2
2
  import { crc8 } from './crc8';
3
- import { logProtocolV2Debug } from './debug';
4
3
  import { PROTOCOL_V2_FRAME_MAX_BYTES } from '../../constants';
5
4
 
6
- import type { ProtocolV2FrameDebugOptions } from './debug';
7
-
8
5
  // Default sequence number when callers do not manage one themselves.
9
6
  // Stateful per-session sequencing lives in ProtocolV2Session (session.ts),
10
7
  // which advances the counter via nextProtoSeq() and passes it in explicitly.
@@ -35,7 +32,6 @@ export function encodeFrame(
35
32
  payload: Uint8Array | null,
36
33
  packetSrc?: number,
37
34
  router?: number,
38
- debugOptions?: ProtocolV2FrameDebugOptions,
39
35
  seq?: number
40
36
  ): Uint8Array {
41
37
  const resolvedPacketSrc = packetSrc ?? 0;
@@ -47,7 +43,7 @@ export function encodeFrame(
47
43
  const payloadLen = payload ? payload.length : 0;
48
44
  const frameLen = payloadLen + PROTO_HEAD_CRC_SIZE;
49
45
  if (frameLen > PROTOCOL_V2_FRAME_MAX_BYTES) {
50
- throw new Error(`Protocol V2 frame too large: ${frameLen}`);
46
+ throw new Error(`Protocol V2 frame too large: ${frameLen} > ${PROTOCOL_V2_FRAME_MAX_BYTES}`);
51
47
  }
52
48
  const frame = new Uint8Array(frameLen);
53
49
 
@@ -69,17 +65,6 @@ export function encodeFrame(
69
65
  // CRC8 over entire frame except last byte
70
66
  frame[frameLen - 1] = crc8(frame, frameLen - 1);
71
67
 
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
68
  return frame;
84
69
  }
85
70
 
@@ -95,22 +80,11 @@ export function encodeProtobufFrame(
95
80
  pbPayload: Uint8Array,
96
81
  packetSrc?: number,
97
82
  router?: number,
98
- debugOptions?: ProtocolV2FrameDebugOptions,
99
83
  seq?: number
100
84
  ): Uint8Array {
101
85
  const payload = new Uint8Array(2 + pbPayload.length);
102
86
  payload[0] = messageTypeId % 256;
103
87
  payload[1] = Math.floor(messageTypeId / 256) % 256;
104
88
  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
- );
89
+ return encodeFrame(payload, packetSrc, router, seq);
116
90
  }
@@ -0,0 +1,25 @@
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;
@@ -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')}`
@@ -1,6 +1,7 @@
1
1
  export * from './constants';
2
2
  export * from './crc8';
3
- export * from './debug';
4
3
  export * from './encode';
5
4
  export * from './decode';
5
+ export * from './errors';
6
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);
@@ -68,23 +79,49 @@ export class ProtocolV2LinkManager<Key> {
68
79
  }
69
80
 
70
81
  async invalidateLink(key: Key, reason: string): Promise<void> {
82
+ const generation = (this.generations.get(key) ?? 0) + 1;
83
+ this.generations.set(key, generation);
84
+ this.invalidationReasons.set(key, { generation, reason });
85
+
86
+ const pendingInvalidation = this.invalidations.get(key);
71
87
  const link = this.links.get(key);
72
- if (!link) return;
88
+ if (!link) {
89
+ await pendingInvalidation;
90
+ return;
91
+ }
73
92
 
74
93
  this.links.delete(key);
75
94
  link.state.invalidatedReason = reason;
76
- await link.adapter.reset(reason);
77
- await this.options.onLinkInvalidated?.(key, reason);
95
+ const invalidation = (async () => {
96
+ await pendingInvalidation?.catch(() => undefined);
97
+ await link.adapter.reset(reason);
98
+ await this.options.onLinkInvalidated?.(key, reason);
99
+ })();
100
+ this.invalidations.set(key, invalidation);
101
+
102
+ try {
103
+ await invalidation;
104
+ } finally {
105
+ if (this.invalidations.get(key) === invalidation) {
106
+ this.invalidations.delete(key);
107
+ }
108
+ }
78
109
  }
79
110
 
80
111
  async invalidateAllLinks(reason: string): Promise<void> {
81
- await Promise.all(Array.from(this.links.keys(), key => this.invalidateLink(key, reason)));
112
+ const keys = new Set([
113
+ ...this.links.keys(),
114
+ ...this.callQueues.keys(),
115
+ ...this.invalidations.keys(),
116
+ ]);
117
+ await Promise.all(Array.from(keys, key => this.invalidateLink(key, reason)));
82
118
  }
83
119
 
84
120
  async dispose(reason: string): Promise<void> {
85
121
  await this.invalidateAllLinks(reason);
86
122
  this.sequences.clear();
87
123
  this.callQueues.clear();
124
+ this.invalidations.clear();
88
125
  }
89
126
 
90
127
  private getOrCreateLink(key: Key, createAdapter: () => ProtocolV2LinkAdapter): ProtocolV2Link {
@@ -157,4 +194,16 @@ export class ProtocolV2LinkManager<Key> {
157
194
  this.callQueues.delete(key);
158
195
  }
159
196
  }
197
+
198
+ private assertCallGeneration(key: Key, generation: number) {
199
+ const currentGeneration = this.generations.get(key) ?? 0;
200
+ if (currentGeneration === generation) return;
201
+
202
+ const invalidation = this.invalidationReasons.get(key);
203
+ const reason =
204
+ invalidation?.generation === currentGeneration
205
+ ? invalidation.reason
206
+ : 'Protocol V2 link generation changed';
207
+ throw new ProtocolV2LinkError('generation', reason);
208
+ }
160
209
  }