@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,12 +1,23 @@
1
- import { PROTOCOL_V2_PACKET_SRC_COMMAND } from '../../constants';
1
+ import {
2
+ PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
3
+ PROTOCOL_V2_PACKET_SRC_COMMAND,
4
+ } from '../../constants';
2
5
  import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
3
6
  import { ProtocolV2SequenceCursor } from './sequence-cursor';
7
+ import {
8
+ ProtocolV2LinkError,
9
+ createProtocolV2LinkDisabledError,
10
+ isProtocolV2LinkDisabledError,
11
+ isProtocolV2LinkDisabledFailure,
12
+ } from './errors';
4
13
  import { ProtocolV2 } from '..';
5
14
  import * as check from '../../utils/highlevel-checks';
6
15
  import { LogBlockCommand } from '../../utils/logBlockCommand';
7
16
 
8
17
  import type { Root } from 'protobufjs/light';
9
- import type { MessageFromOneKey } from '../../types';
18
+ import type { MessageFromOneKey, TransportWriteMetrics } from '../../types';
19
+
20
+ export * from './errors';
10
21
 
11
22
  export type ProtocolV2Schemas = {
12
23
  protocolV1: Root;
@@ -16,8 +27,10 @@ export type ProtocolV2Schemas = {
16
27
  export type ProtocolV2CallContext = {
17
28
  messageName: string;
18
29
  timeoutMs?: number;
19
- highVolume: boolean;
30
+ highThroughput: boolean;
31
+ writeWithResponse?: boolean;
20
32
  generation: number;
33
+ signal: AbortSignal;
21
34
  };
22
35
 
23
36
  type ProtocolLogger = {
@@ -45,6 +58,9 @@ export type ProtocolV2CallOptions = {
45
58
  expectedTypes?: string[];
46
59
  intermediateTypes?: string[];
47
60
  onIntermediateResponse?: (response: MessageFromOneKey) => void;
61
+ onWriteCompleted?: (metrics: TransportWriteMetrics) => void;
62
+ returnAfterWrite?: boolean;
63
+ writeWithResponse?: boolean;
48
64
  };
49
65
 
50
66
  export { concatUint8Arrays, ProtocolV2FrameAssembler };
@@ -71,7 +87,7 @@ export function bytesToHex(bytes: Uint8Array): string {
71
87
  .join('');
72
88
  }
73
89
 
74
- const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
90
+ const REDUCED_DEBUG_PROTOCOL_V2_CALLS = new Set([
75
91
  ...LogBlockCommand,
76
92
  'FilesystemFileRead',
77
93
  'FileRead',
@@ -79,7 +95,22 @@ const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
79
95
  ]);
80
96
 
81
97
  function shouldReduceProtocolV2Debug(name: string) {
82
- return HIGH_VOLUME_PROTOCOL_V2_CALLS.has(name);
98
+ return REDUCED_DEBUG_PROTOCOL_V2_CALLS.has(name);
99
+ }
100
+
101
+ const HIGH_THROUGHPUT_PROTOCOL_V2_CALLS = new Set([
102
+ 'FilesystemFileWrite',
103
+ 'FileWrite',
104
+ 'EmmcFileWrite',
105
+ 'FirmwareUpload',
106
+ 'ResourceAck',
107
+ 'FilesystemFileRead',
108
+ 'FileRead',
109
+ 'EmmcFileRead',
110
+ ]);
111
+
112
+ export function isProtocolV2HighThroughputCall(name: string) {
113
+ return HIGH_THROUGHPUT_PROTOCOL_V2_CALLS.has(name);
83
114
  }
84
115
 
85
116
  const COMMON_TERMINAL_RESPONSE_TYPES = new Set([
@@ -114,11 +145,13 @@ export async function withProtocolTimeout<T>(
114
145
  promise: Promise<T>,
115
146
  timeoutMs: number | undefined,
116
147
  createTimeoutError: () => Error,
117
- onTimeout?: () => void
148
+ onTimeout?: () => void,
149
+ abortSignal?: AbortSignal
118
150
  ): Promise<T> {
119
151
  if (!timeoutMs) return promise;
120
152
 
121
153
  let timer: ReturnType<typeof setTimeout> | undefined;
154
+ let abortHandler: (() => void) | undefined;
122
155
  try {
123
156
  return await Promise.race([
124
157
  promise,
@@ -130,17 +163,29 @@ export async function withProtocolTimeout<T>(
130
163
  reject(createTimeoutError());
131
164
  }, timeoutMs);
132
165
  }),
166
+ ...(abortSignal
167
+ ? [
168
+ new Promise<never>((_, reject) => {
169
+ abortHandler = () => {
170
+ reject(new Error('Protocol V2 operation aborted'));
171
+ };
172
+ if (abortSignal.aborted) {
173
+ abortHandler();
174
+ } else {
175
+ abortSignal.addEventListener('abort', abortHandler, { once: true });
176
+ }
177
+ }),
178
+ ]
179
+ : []),
133
180
  ]);
134
181
  } finally {
135
182
  if (timer) clearTimeout(timer);
183
+ if (abortHandler) {
184
+ abortSignal?.removeEventListener('abort', abortHandler);
185
+ }
136
186
  }
137
187
  }
138
188
 
139
- // Write completion is owned by the concrete transport. A Promise.race watchdog
140
- // here can return while libusb/WebUSB is still flushing a large Protocol V2
141
- // frame, which desynchronizes later request/response pairs.
142
- export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 0;
143
-
144
189
  export class ProtocolV2Session {
145
190
  private readonly options: ProtocolV2SessionOptions;
146
191
 
@@ -150,6 +195,12 @@ export class ProtocolV2Session {
150
195
  // in-flight calls on the same session would steal each other's responses.
151
196
  private pendingCall: Promise<unknown> = Promise.resolve();
152
197
 
198
+ // Flow-control messages may interrupt a call that is waiting for a device UI
199
+ // response, but their frames must never interleave with the active request write.
200
+ private pendingWrite: Promise<unknown> = Promise.resolve();
201
+
202
+ private lastResponseSequence?: number;
203
+
153
204
  constructor(options: ProtocolV2SessionOptions) {
154
205
  this.options = options;
155
206
  this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor();
@@ -168,7 +219,48 @@ export class ProtocolV2Session {
168
219
  return result;
169
220
  }
170
221
 
171
- private async executeCall(
222
+ sendFlowControl(name: string, data: Record<string, unknown>): Promise<MessageFromOneKey> {
223
+ const {
224
+ schemas,
225
+ router,
226
+ packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
227
+ maxFrameBytes,
228
+ writeFrame,
229
+ generation = 0,
230
+ } = this.options;
231
+ const abortController = new AbortController();
232
+ const context: ProtocolV2CallContext = {
233
+ messageName: name,
234
+ highThroughput: false,
235
+ generation,
236
+ signal: abortController.signal,
237
+ };
238
+ const protoSeq = this.sequenceCursor.next();
239
+ const frame = ProtocolV2.encodeFrame(schemas, name, data, {
240
+ packetSrc,
241
+ router,
242
+ seq: protoSeq,
243
+ });
244
+
245
+ if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
246
+ return Promise.reject(
247
+ new Error(`Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`)
248
+ );
249
+ }
250
+
251
+ return this.serializeWrite(() => writeFrame(frame, context)).then(() => ({
252
+ type: 'WriteCompleted',
253
+ message: {},
254
+ }));
255
+ }
256
+
257
+ private serializeWrite<T>(write: () => Promise<T>): Promise<T> {
258
+ const result = this.pendingWrite.then(write, write);
259
+ this.pendingWrite = result.catch(() => undefined);
260
+ return result;
261
+ }
262
+
263
+ private executeCall(
172
264
  name: string,
173
265
  data: Record<string, unknown>,
174
266
  callOptions: ProtocolV2CallOptions
@@ -186,81 +278,121 @@ export class ProtocolV2Session {
186
278
  createTimeoutError,
187
279
  generation = 0,
188
280
  } = this.options;
281
+ const timeoutMs =
282
+ callOptions.timeoutMs && callOptions.timeoutMs > 0
283
+ ? callOptions.timeoutMs
284
+ : PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
285
+ const abortController = new AbortController();
189
286
 
190
287
  const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
191
- const callContext: ProtocolV2CallContext = {
288
+ const baseCallContext: ProtocolV2CallContext = {
192
289
  messageName: name,
193
- timeoutMs: callOptions.timeoutMs,
194
- highVolume: shouldReduceDebug,
290
+ timeoutMs,
291
+ highThroughput: isProtocolV2HighThroughputCall(name),
292
+ ...(callOptions.writeWithResponse === undefined
293
+ ? {}
294
+ : { writeWithResponse: callOptions.writeWithResponse }),
195
295
  generation,
296
+ signal: abortController.signal,
196
297
  };
197
- await prepareCall?.(callContext);
198
- const protoSeq = this.sequenceCursor.next();
199
- const frame = ProtocolV2.encodeFrame(schemas, name, data, {
200
- packetSrc,
201
- router,
202
- seq: protoSeq,
203
- });
204
- const txMetadata = ProtocolV2.inspectFrame(schemas, frame);
205
298
 
206
- if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
207
- throw new Error(
208
- `Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
209
- );
210
- }
211
-
212
- if (!shouldReduceDebug) {
213
- logger?.debug?.(`[${logPrefix}] TX`, {
214
- method: name,
215
- type: name,
216
- typeId: txMetadata.messageTypeId,
217
- seq: txMetadata.seq,
218
- bytes: frame.length,
299
+ const runCall = async (): Promise<MessageFromOneKey> => {
300
+ await prepareCall?.(baseCallContext);
301
+ const protoSeq = this.sequenceCursor.next();
302
+ const frame = ProtocolV2.encodeFrame(schemas, name, data, {
303
+ packetSrc,
304
+ router,
305
+ seq: protoSeq,
219
306
  });
220
- }
221
307
 
222
- // Lenient watchdog on the write phase only — see
223
- // PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS for the rationale.
224
- await withProtocolTimeout(
225
- writeFrame(frame, callContext),
226
- PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS,
227
- () =>
228
- new Error(
229
- `Protocol V2 write timeout after ${PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS}ms for ${name}`
230
- )
231
- );
308
+ if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
309
+ throw new Error(
310
+ `Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
311
+ );
312
+ }
232
313
 
233
- // Cancellation flag for the read loop: when the response timeout fires,
234
- // Promise.race alone would leave this loop running as a zombie that keeps
235
- // consuming frames meant for the next call. The timeout callback flips the
236
- // flag so the loop exits and discards any late frame.
237
- const cancellation = { cancelled: false };
314
+ const writeStartedAt = Date.now();
315
+ await this.serializeWrite(() => writeFrame(frame, baseCallContext));
316
+ try {
317
+ callOptions.onWriteCompleted?.({
318
+ elapsedMs: Math.max(Date.now() - writeStartedAt, 0),
319
+ frameBytes: frame.byteLength,
320
+ });
321
+ } catch (error) {
322
+ logger?.error?.(`${logPrefix} write metrics callback failed: ${String(error)}`);
323
+ }
324
+
325
+ if (callOptions.returnAfterWrite) {
326
+ return { type: 'WriteCompleted', message: {} };
327
+ }
238
328
 
239
- const readResponse = async (): Promise<MessageFromOneKey> => {
240
329
  // Some Protocol V2 operations emit progress notifications before the
241
330
  // terminal response. Consume those frames here so callers still see a
242
331
  // request/terminal-response shaped API.
243
- while (!cancellation.cancelled) {
244
- const rxFrame = await readFrame(callContext);
245
- if (cancellation.cancelled) {
246
- // Timed out while waiting: drop the late frame and stop reading.
247
- break;
332
+ while (!abortController.signal.aborted) {
333
+ const rxFrame = await readFrame(baseCallContext);
334
+ if (abortController.signal.aborted) break;
335
+
336
+ let header: ReturnType<typeof ProtocolV2.inspectFrameHeader>;
337
+ try {
338
+ header = ProtocolV2.inspectFrameHeader(rxFrame);
339
+ } catch (cause) {
340
+ throw new ProtocolV2LinkError(
341
+ 'frame',
342
+ `Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
343
+ cause
344
+ );
345
+ }
346
+ if (header.router !== router) {
347
+ throw new ProtocolV2LinkError(
348
+ 'router',
349
+ `Protocol V2 router mismatch: expected ${router}, got ${header.router}`
350
+ );
351
+ }
352
+ if (header.packetSrc !== packetSrc) {
353
+ throw new ProtocolV2LinkError(
354
+ 'packet-source',
355
+ `Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
356
+ );
248
357
  }
249
- const isAck = ProtocolV2.isAckFrame(rxFrame);
250
- if (!isAck) {
251
- const rxMetadata = ProtocolV2.inspectFrame(schemas, rxFrame);
252
-
253
- if (!shouldReduceDebug) {
254
- logger?.debug?.(`[${logPrefix}] RX`, {
255
- method: name,
256
- type: rxMetadata.type,
257
- typeId: rxMetadata.messageTypeId,
258
- seq: rxMetadata.seq,
259
- bytes: rxFrame.length,
260
- });
261
- }
262
358
 
263
- const decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
359
+ let isAck: boolean;
360
+ try {
361
+ isAck = ProtocolV2.isAckFrame(rxFrame);
362
+ } catch (cause) {
363
+ throw new ProtocolV2LinkError(
364
+ 'frame',
365
+ `Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
366
+ cause
367
+ );
368
+ }
369
+ if (isAck) {
370
+ if (header.seq !== protoSeq) {
371
+ throw new ProtocolV2LinkError(
372
+ 'ack-sequence',
373
+ `Protocol V2 ACK sequence mismatch: expected ${protoSeq}, got ${header.seq}`
374
+ );
375
+ }
376
+ } else {
377
+ let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
378
+ try {
379
+ decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
380
+ } catch (cause) {
381
+ throw new ProtocolV2LinkError(
382
+ 'frame',
383
+ `Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
384
+ cause
385
+ );
386
+ }
387
+ // Firmware owns one TX sequence across all channels and packet sources,
388
+ // so frames routed elsewhere create valid gaps in this session.
389
+ if (this.lastResponseSequence === decoded.seq) {
390
+ throw new ProtocolV2LinkError(
391
+ 'response-sequence',
392
+ `Protocol V2 duplicate response sequence: ${decoded.seq}`
393
+ );
394
+ }
395
+ this.lastResponseSequence = decoded.seq;
264
396
 
265
397
  const response = check.call(decoded);
266
398
  if (callOptions.intermediateTypes?.includes(response.type)) {
@@ -276,20 +408,19 @@ export class ProtocolV2Session {
276
408
  }
277
409
  }
278
410
  }
279
- // Only reachable after cancellation; the outer promise has already been
280
- // rejected by the timeout, so this rejection is consumed by the race.
411
+
281
412
  throw new Error(`Protocol V2 read loop cancelled after timeout for ${name}`);
282
413
  };
283
414
 
284
415
  return withProtocolTimeout(
285
- readResponse(),
286
- callOptions.timeoutMs,
416
+ runCall(),
417
+ timeoutMs,
287
418
  () =>
288
419
  createTimeoutError
289
- ? createTimeoutError(name, callOptions.timeoutMs ?? 0)
290
- : new Error(`Protocol V2 response timeout after ${callOptions.timeoutMs}ms for ${name}`),
420
+ ? createTimeoutError(name, timeoutMs)
421
+ : new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
291
422
  () => {
292
- cancellation.cancelled = true;
423
+ abortController.abort();
293
424
  }
294
425
  );
295
426
  }
@@ -320,13 +451,26 @@ export async function probeProtocolV2({
320
451
  const response = await call(
321
452
  'Ping',
322
453
  { message: 'protocol-v2-probe' },
323
- { timeoutMs, expectedTypes: ['Success'] }
454
+ {
455
+ timeoutMs,
456
+ expectedTypes: ['Success'],
457
+ }
324
458
  );
325
459
  if (response.type === 'Success') {
326
460
  return true;
327
461
  }
462
+ if (response.type === 'Failure') {
463
+ const failureCode = response.message?.code;
464
+ const firmwareMessage = response.message?.message;
465
+ if (isProtocolV2LinkDisabledFailure(failureCode, firmwareMessage)) {
466
+ throw createProtocolV2LinkDisabledError(failureCode, firmwareMessage);
467
+ }
468
+ }
328
469
  probeError = new Error(`unexpected response type ${response.type}`);
329
470
  } catch (error) {
471
+ if (isProtocolV2LinkDisabledError(error)) {
472
+ throw error;
473
+ }
330
474
  probeError = error;
331
475
  }
332
476
 
@@ -1,5 +1,7 @@
1
1
  import { ProtocolV2FrameAssembler } from './frame-assembler';
2
+ import { ProtocolV2LinkError, isProtocolV2LinkError } from './errors';
2
3
  import { ProtocolV2LinkManager } from './link-manager';
4
+ import { getErrorMessage } from './session';
3
5
 
4
6
  import type { MessageFromOneKey, TransportCallOptions } from '../../types';
5
7
  import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session';
@@ -32,7 +34,7 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
32
34
  this.protocolV2UsbOptions = options;
33
35
  this.protocolV2UsbLinks = new ProtocolV2LinkManager<Key>({
34
36
  getSchemas: () => this.getProtocolV2UsbSchemas(),
35
- classifyError: () => 'link-fatal',
37
+ classifyError: error => (isProtocolV2LinkError(error) ? 'link-fatal' : 'recoverable'),
36
38
  onLinkInvalidated: async (key, reason) => {
37
39
  this.protocolV2UsbAssemblers.get(key)?.reset();
38
40
  await this.resetProtocolV2UsbNativeLink(key, reason);
@@ -61,7 +63,7 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
61
63
  protected abstract createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error;
62
64
 
63
65
  protected onProtocolV2UsbLinkInvalidated(_key: Key, _reason: string): Promise<void> | void {
64
- // 子类按需清理协议缓存或记录平台日志。
66
+ // Subclasses may clear protocol caches or emit platform-specific diagnostics.
65
67
  }
66
68
 
67
69
  protected async rotateProtocolV2UsbGeneration(key: Key, reason: string): Promise<number> {
@@ -91,6 +93,19 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
91
93
  );
92
94
  }
93
95
 
96
+ protected sendProtocolV2UsbFlowControl(
97
+ key: Key,
98
+ name: string,
99
+ data: Record<string, unknown>
100
+ ): Promise<MessageFromOneKey> {
101
+ return this.protocolV2UsbLinks.sendFlowControl(
102
+ key,
103
+ () => this.createProtocolV2UsbAdapter(key),
104
+ name,
105
+ data
106
+ );
107
+ }
108
+
94
109
  protected invalidateProtocolV2UsbLink(key: Key, reason: string): Promise<void> {
95
110
  return this.protocolV2UsbLinks.invalidateLink(key, reason);
96
111
  }
@@ -118,10 +133,13 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
118
133
 
119
134
  const assertCurrentGeneration = () => {
120
135
  if (cancellation.reason) {
121
- throw new Error(cancellation.reason);
136
+ throw new ProtocolV2LinkError('generation', cancellation.reason);
122
137
  }
123
138
  if (this.protocolV2UsbGenerations.get(key) !== generation) {
124
- throw new Error('Protocol V2 USB connection generation changed');
139
+ throw new ProtocolV2LinkError(
140
+ 'generation',
141
+ 'Protocol V2 USB connection generation changed'
142
+ );
125
143
  }
126
144
  };
127
145
 
@@ -131,11 +149,14 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
131
149
  generation,
132
150
  prepareCall: () => {
133
151
  assertCurrentGeneration();
134
- this.getProtocolV2UsbAssembler(key).reset();
135
152
  },
136
153
  writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
137
154
  assertCurrentGeneration();
138
- await this.writeProtocolV2UsbPacket(key, frame, context);
155
+ try {
156
+ await this.writeProtocolV2UsbPacket(key, frame, context);
157
+ } catch (error) {
158
+ throw this.createProtocolV2UsbIoError('write', error);
159
+ }
139
160
  assertCurrentGeneration();
140
161
  },
141
162
  readFrame: async (context: ProtocolV2CallContext) => {
@@ -143,13 +164,15 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
143
164
  const assembler = this.getProtocolV2UsbAssembler(key);
144
165
  let frame = assembler.push(new Uint8Array(0));
145
166
  while (!frame) {
146
- const packetRead = this.readProtocolV2UsbPacket(key, context).then(packet => ({
147
- packet,
148
- }));
167
+ const packetRead = this.readProtocolV2UsbPacket(key, context)
168
+ .then(packet => ({ packet }))
169
+ .catch(error => {
170
+ throw this.createProtocolV2UsbIoError('read', error);
171
+ });
149
172
  const cancelled = cancellation.promise.then(reason => ({ reason }));
150
173
  const result = await Promise.race([packetRead, cancelled]);
151
174
  if ('reason' in result) {
152
- throw new Error(result.reason);
175
+ throw new ProtocolV2LinkError('generation', result.reason);
153
176
  }
154
177
  assertCurrentGeneration();
155
178
  frame = assembler.push(result.packet);
@@ -191,4 +214,13 @@ export abstract class ProtocolV2UsbTransportBase<Key> {
191
214
  };
192
215
  return cancellation;
193
216
  }
217
+
218
+ private createProtocolV2UsbIoError(operation: 'read' | 'write', error: unknown) {
219
+ if (isProtocolV2LinkError(error)) return error;
220
+ return new ProtocolV2LinkError(
221
+ 'io',
222
+ `Protocol V2 USB ${operation} failed: ${getErrorMessage(error) || 'unknown error'}`,
223
+ error
224
+ );
225
+ }
194
226
  }
@@ -70,7 +70,10 @@ function messageToJSON(Message: Message<Record<string, unknown>>, fields: Type['
70
70
  }
71
71
  // message type
72
72
  else if (field.resolvedType!.fields) {
73
- res[key] = messageToJSON(value, field.resolvedType!.fields);
73
+ // [compatibility]: absent optional sub-messages are not own properties
74
+ // of the decoded instance, so value is undefined here; decode them to
75
+ // null, matching the optional primitive-field convention above.
76
+ res[key] = value == null ? null : messageToJSON(value, (field.resolvedType as Type).fields);
74
77
  } else {
75
78
  throw new Error(`case not handled: ${key}`);
76
79
  }
@@ -1,5 +1,6 @@
1
1
  import { Buffer } from 'buffer';
2
2
  import ByteBuffer from 'bytebuffer';
3
+ import { Enum } from 'protobufjs/light';
3
4
 
4
5
  import { isPrimitiveField } from '../../utils/protobuf';
5
6
 
@@ -49,7 +50,10 @@ export function patch(Message: Type, payload: any) {
49
50
  // repeated
50
51
  if (field.repeated) {
51
52
  const RefMessage = Message.lookupTypeOrEnum(field.type);
52
- patched[key] = value.map((v: any) => patch(RefMessage, v));
53
+ patched[key] =
54
+ RefMessage instanceof Enum
55
+ ? value.map((v: any) => transform(field.type, v))
56
+ : value.map((v: any) => patch(RefMessage, v));
53
57
  }
54
58
  // message type
55
59
  else if (typeof value === 'object' && value !== null) {