@onekeyfe/hd-transport 1.2.0-alpha.99 → 1.2.1-alpha.0
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 +25 -0
- package/__tests__/protocol-v2-link-manager.test.js +126 -0
- package/__tests__/protocol-v2-usb-transport-base.test.js +6 -4
- package/__tests__/protocol-v2.test.js +309 -27
- package/dist/index.d.ts +131 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +255 -15
- package/dist/protocols/index.d.ts +12 -0
- package/dist/protocols/index.d.ts.map +1 -1
- package/dist/protocols/v2/errors.d.ts +8 -0
- package/dist/protocols/v2/errors.d.ts.map +1 -1
- package/dist/protocols/v2/link-manager.d.ts +2 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -1
- package/dist/protocols/v2/session.d.ts +13 -1
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/protocols/v2/usb-transport-base.d.ts +1 -0
- package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -1
- package/dist/types/messages.d.ts +50 -7
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +4 -0
- package/dist/types/transport.d.ts.map +1 -1
- package/messages-protocol-v2.json +140 -13
- package/package.json +2 -2
- package/src/protocols/index.ts +91 -0
- package/src/protocols/v2/errors.ts +29 -0
- package/src/protocols/v2/link-manager.ts +57 -5
- package/src/protocols/v2/session.ts +133 -4
- package/src/protocols/v2/usb-transport-base.ts +13 -0
- package/src/types/messages.ts +61 -7
- package/src/types/transport.ts +11 -0
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ In order to be able to use new features of onekey-firmware you need to update pr
|
|
|
20
20
|
1. `yarn update-submodules` to update firmware submodule
|
|
21
21
|
1. `yarn update-protobuf` to generate new `./messages.json`, `./messages-protocol-v2.json` and `./src/types/messages.ts`
|
|
22
22
|
|
|
23
|
-
The same task can be run from the repository root with `yarn update-protobuf`. The Protocol V2 schema requires the `firmware-pro2` submodule checked out on branch `
|
|
23
|
+
The same task can be run from the repository root with `yarn update-protobuf`. The Protocol V2 schema requires the `firmware-pro2` submodule checked out on branch `main`.
|
|
24
24
|
|
|
25
25
|
## Docs
|
|
26
26
|
|
|
@@ -88,6 +88,31 @@ describe('messages', () => {
|
|
|
88
88
|
});
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
test('Protocol V2 firmware update progress matches firmware-pro2 main', () => {
|
|
92
|
+
expect(v2Messages.nested.MessageType.values).toMatchObject({
|
|
93
|
+
MessageType_DeviceFindMyTokenState: 60450,
|
|
94
|
+
MessageType_DeviceFindMyTokenUpdate: 60451,
|
|
95
|
+
MessageType_DeviceFindMyTokenStateGet: 60452,
|
|
96
|
+
});
|
|
97
|
+
expect(v2Messages.nested.DeviceFirmwareUpdatePhase.values).toEqual({
|
|
98
|
+
FW_MGMT_UPDATER_PHASE_PREPARE: 0,
|
|
99
|
+
FW_MGMT_UPDATER_PHASE_INSTALL: 1,
|
|
100
|
+
FW_MGMT_UPDATER_PHASE_VERIFY: 2,
|
|
101
|
+
});
|
|
102
|
+
expect(v2Messages.nested.DeviceFirmwareUpdateRequest.fields.reboot_after_update).toMatchObject({
|
|
103
|
+
id: 1,
|
|
104
|
+
type: 'bool',
|
|
105
|
+
});
|
|
106
|
+
expect(v2Messages.nested.DeviceFirmwareUpdateRecord.fields).toMatchObject({
|
|
107
|
+
progress_percent: { id: 11, type: 'uint32' },
|
|
108
|
+
phase_info: { id: 12, type: 'DeviceFirmwareUpdatePhaseInfo' },
|
|
109
|
+
});
|
|
110
|
+
expect(v2Messages.nested.DeviceFirmwareUpdateRecordFields.fields).toMatchObject({
|
|
111
|
+
progress_percent: { id: 11, type: 'bool' },
|
|
112
|
+
phase_info: { id: 12, type: 'bool' },
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
91
116
|
test('Protocol V2 conflicting enums keep their own wire values', () => {
|
|
92
117
|
expect(generatedTypes.ProtocolV2FailureType).toMatchObject({
|
|
93
118
|
Failure_DataError: 4,
|
|
@@ -26,6 +26,9 @@ const protocolV2Messages = parseConfigure({
|
|
|
26
26
|
message: { type: 'string', id: 1 },
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
|
+
Cancel: {
|
|
30
|
+
fields: {},
|
|
31
|
+
},
|
|
29
32
|
Success: {
|
|
30
33
|
fields: {
|
|
31
34
|
message: { type: 'string', id: 1 },
|
|
@@ -35,6 +38,7 @@ const protocolV2Messages = parseConfigure({
|
|
|
35
38
|
values: {
|
|
36
39
|
MessageType_Ping: 60206,
|
|
37
40
|
MessageType_Success: 60207,
|
|
41
|
+
MessageType_Cancel: 60004,
|
|
38
42
|
},
|
|
39
43
|
},
|
|
40
44
|
},
|
|
@@ -313,6 +317,128 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
313
317
|
]);
|
|
314
318
|
});
|
|
315
319
|
|
|
320
|
+
test('times out a queued call without sending it after the active call settles', async () => {
|
|
321
|
+
let releaseActiveRead;
|
|
322
|
+
let markActiveReadStarted;
|
|
323
|
+
const activeReadStarted = new Promise(resolve => {
|
|
324
|
+
markActiveReadStarted = resolve;
|
|
325
|
+
});
|
|
326
|
+
const activeReadBlocked = new Promise(resolve => {
|
|
327
|
+
releaseActiveRead = resolve;
|
|
328
|
+
});
|
|
329
|
+
const sentSeqs = [];
|
|
330
|
+
const success = ProtocolV2.encodeFrame(
|
|
331
|
+
schemas,
|
|
332
|
+
'Success',
|
|
333
|
+
{ message: 'ok' },
|
|
334
|
+
{ router: 1, packetSrc: 0, seq: 1 }
|
|
335
|
+
);
|
|
336
|
+
let requestSeq = 0;
|
|
337
|
+
let readCount = 0;
|
|
338
|
+
const adapter = {
|
|
339
|
+
router: 1,
|
|
340
|
+
generation: 1,
|
|
341
|
+
prepareCall: jest.fn(),
|
|
342
|
+
writeFrame: jest.fn(frame => {
|
|
343
|
+
[, , , , , , requestSeq] = frame;
|
|
344
|
+
sentSeqs.push(requestSeq);
|
|
345
|
+
return Promise.resolve();
|
|
346
|
+
}),
|
|
347
|
+
readFrame: jest.fn(async () => {
|
|
348
|
+
readCount += 1;
|
|
349
|
+
if (readCount === 1) {
|
|
350
|
+
markActiveReadStarted();
|
|
351
|
+
await activeReadBlocked;
|
|
352
|
+
}
|
|
353
|
+
return rewriteSeq(success, requestSeq);
|
|
354
|
+
}),
|
|
355
|
+
reset: jest.fn(),
|
|
356
|
+
createTimeoutError: (name, timeoutMs) =>
|
|
357
|
+
new Error(`response timeout after ${timeoutMs}ms for ${name}`),
|
|
358
|
+
};
|
|
359
|
+
const manager = new ProtocolV2LinkManager({
|
|
360
|
+
getSchemas: () => schemas,
|
|
361
|
+
classifyError: () => 'recoverable',
|
|
362
|
+
});
|
|
363
|
+
const createAdapter = jest.fn(() => adapter);
|
|
364
|
+
|
|
365
|
+
const activeCall = manager.call('device-a', createAdapter, 'Ping', { message: 'active' });
|
|
366
|
+
await activeReadStarted;
|
|
367
|
+
const queuedCall = manager.call(
|
|
368
|
+
'device-a',
|
|
369
|
+
createAdapter,
|
|
370
|
+
'Ping',
|
|
371
|
+
{ message: 'queued' },
|
|
372
|
+
{ timeoutMs: 20 }
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
await expect(queuedCall).rejects.toThrow('response timeout after 20ms for Ping');
|
|
376
|
+
expect(sentSeqs).toEqual([1]);
|
|
377
|
+
|
|
378
|
+
releaseActiveRead();
|
|
379
|
+
await activeCall;
|
|
380
|
+
await expect(
|
|
381
|
+
manager.call('device-a', createAdapter, 'Ping', { message: 'after-timeout' })
|
|
382
|
+
).resolves.toEqual({
|
|
383
|
+
type: 'Success',
|
|
384
|
+
message: { message: 'ok' },
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('writes flow control while the active call is waiting for its response', async () => {
|
|
391
|
+
const sentSeqs = [];
|
|
392
|
+
let releaseRead;
|
|
393
|
+
let markReadStarted;
|
|
394
|
+
const readStarted = new Promise(resolve => {
|
|
395
|
+
markReadStarted = resolve;
|
|
396
|
+
});
|
|
397
|
+
const readBlocked = new Promise(resolve => {
|
|
398
|
+
releaseRead = resolve;
|
|
399
|
+
});
|
|
400
|
+
const success = ProtocolV2.encodeFrame(
|
|
401
|
+
schemas,
|
|
402
|
+
'Success',
|
|
403
|
+
{ message: 'ok' },
|
|
404
|
+
{ router: 1, packetSrc: 0, seq: 1 }
|
|
405
|
+
);
|
|
406
|
+
let requestSeq = 0;
|
|
407
|
+
const adapter = {
|
|
408
|
+
router: 1,
|
|
409
|
+
generation: 1,
|
|
410
|
+
prepareCall: jest.fn(),
|
|
411
|
+
writeFrame: jest.fn(frame => {
|
|
412
|
+
[, , , , , , requestSeq] = frame;
|
|
413
|
+
sentSeqs.push(requestSeq);
|
|
414
|
+
return Promise.resolve();
|
|
415
|
+
}),
|
|
416
|
+
readFrame: jest.fn(async () => {
|
|
417
|
+
markReadStarted();
|
|
418
|
+
await readBlocked;
|
|
419
|
+
return rewriteSeq(success, 1);
|
|
420
|
+
}),
|
|
421
|
+
reset: jest.fn(),
|
|
422
|
+
};
|
|
423
|
+
const manager = new ProtocolV2LinkManager({
|
|
424
|
+
getSchemas: () => schemas,
|
|
425
|
+
classifyError: () => 'recoverable',
|
|
426
|
+
});
|
|
427
|
+
const createAdapter = jest.fn(() => adapter);
|
|
428
|
+
|
|
429
|
+
const activeCall = manager.call('device-a', createAdapter, 'Ping', { message: 'pending' });
|
|
430
|
+
await readStarted;
|
|
431
|
+
await expect(manager.sendFlowControl('device-a', createAdapter, 'Cancel', {})).resolves.toEqual(
|
|
432
|
+
{ type: 'WriteCompleted', message: {} }
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
436
|
+
expect(adapter.prepareCall).toHaveBeenCalledTimes(1);
|
|
437
|
+
expect(adapter.readFrame).toHaveBeenCalledTimes(1);
|
|
438
|
+
releaseRead();
|
|
439
|
+
await activeCall;
|
|
440
|
+
});
|
|
441
|
+
|
|
316
442
|
test('keeps a recoverable link after a call error', async () => {
|
|
317
443
|
const sentSeqs = [];
|
|
318
444
|
const { adapters, createAdapter } = createAdapterFactory(sentSeqs);
|
|
@@ -217,7 +217,7 @@ describe('ProtocolV2UsbTransportBase', () => {
|
|
|
217
217
|
expect(transport.nativeResets).toEqual([['device-a', 'USB reconnected']]);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
-
test('passes each queued call its
|
|
220
|
+
test('passes each queued call its remaining timeout context', async () => {
|
|
221
221
|
const transport = new FakeUsbTransport();
|
|
222
222
|
await transport.rotate('device-a');
|
|
223
223
|
|
|
@@ -226,12 +226,14 @@ describe('ProtocolV2UsbTransportBase', () => {
|
|
|
226
226
|
transport.callDevice('device-a', 'second', 222),
|
|
227
227
|
]);
|
|
228
228
|
|
|
229
|
-
expect(transport.readContexts).toEqual([
|
|
229
|
+
expect(transport.readContexts.slice(0, 2)).toEqual([
|
|
230
230
|
['device-a', 111],
|
|
231
231
|
['device-a', 111],
|
|
232
|
-
['device-a', 222],
|
|
233
|
-
['device-a', 222],
|
|
234
232
|
]);
|
|
233
|
+
const secondCallTimeouts = transport.readContexts.slice(2).map(([, timeoutMs]) => timeoutMs);
|
|
234
|
+
expect(secondCallTimeouts[0]).toBeGreaterThan(0);
|
|
235
|
+
expect(secondCallTimeouts[0]).toBeLessThanOrEqual(222);
|
|
236
|
+
expect(secondCallTimeouts[1]).toBe(secondCallTimeouts[0]);
|
|
235
237
|
});
|
|
236
238
|
|
|
237
239
|
test('keeps a coalesced response buffered for the next call', async () => {
|
|
@@ -5,9 +5,11 @@ const {
|
|
|
5
5
|
ProtocolV2LinkError,
|
|
6
6
|
ProtocolV2SequenceCursor,
|
|
7
7
|
ProtocolV2Session,
|
|
8
|
+
detectProtocolV2LinkDisabledError,
|
|
8
9
|
hexToBytes,
|
|
9
10
|
isProtocolV2HighThroughputCall,
|
|
10
11
|
probeProtocolV2,
|
|
12
|
+
isProtocolV2LinkDisabledFailure,
|
|
11
13
|
} = require('../src/protocols/v2/session');
|
|
12
14
|
const protocolV2 = require('../src/protocols/v2');
|
|
13
15
|
const {
|
|
@@ -137,7 +139,7 @@ const protocolV2Messages = parseConfigure({
|
|
|
137
139
|
},
|
|
138
140
|
},
|
|
139
141
|
},
|
|
140
|
-
|
|
142
|
+
DeviceFirmwareUpdateStage: {
|
|
141
143
|
fields: {
|
|
142
144
|
targets: {
|
|
143
145
|
type: 'DeviceFirmwareTarget',
|
|
@@ -146,6 +148,9 @@ const protocolV2Messages = parseConfigure({
|
|
|
146
148
|
},
|
|
147
149
|
},
|
|
148
150
|
},
|
|
151
|
+
DeviceFirmwareUpdateRequest: {
|
|
152
|
+
fields: {},
|
|
153
|
+
},
|
|
149
154
|
DeviceFirmwareUpdateRecord: {
|
|
150
155
|
fields: {
|
|
151
156
|
target_id: {
|
|
@@ -218,7 +223,8 @@ const protocolV2Messages = parseConfigure({
|
|
|
218
223
|
MessageType_Success: 60207,
|
|
219
224
|
MessageType_Failure: 60208,
|
|
220
225
|
MessageType_FileWrite: 60805,
|
|
221
|
-
|
|
226
|
+
MessageType_DeviceFirmwareUpdateStage: 61000,
|
|
227
|
+
MessageType_DeviceFirmwareUpdateRequest: 61001,
|
|
222
228
|
MessageType_DeviceFirmwareUpdateStatus: 61002,
|
|
223
229
|
MessageType_DeviceSession: 61201,
|
|
224
230
|
MessageType_PartialNested: 62000,
|
|
@@ -233,6 +239,32 @@ const schemas = {
|
|
|
233
239
|
};
|
|
234
240
|
const productionProtocolV2Messages = parseConfigure(require('../messages-protocol-v2.json'));
|
|
235
241
|
|
|
242
|
+
const legacyProtocolInfoMessage = parseConfigure({
|
|
243
|
+
nested: {
|
|
244
|
+
ProtocolInfo: {
|
|
245
|
+
fields: {
|
|
246
|
+
version: {
|
|
247
|
+
type: 'uint32',
|
|
248
|
+
id: 1,
|
|
249
|
+
rule: 'required',
|
|
250
|
+
},
|
|
251
|
+
supported_messages: {
|
|
252
|
+
type: 'uint32',
|
|
253
|
+
id: 2,
|
|
254
|
+
rule: 'repeated',
|
|
255
|
+
options: {
|
|
256
|
+
packed: false,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
protobuf_definition: {
|
|
260
|
+
type: 'string',
|
|
261
|
+
id: 3,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
}).lookupType('ProtocolInfo');
|
|
267
|
+
|
|
236
268
|
const rewriteSeq = (frame, seq) => {
|
|
237
269
|
const copy = new Uint8Array(frame);
|
|
238
270
|
copy[4] = 1;
|
|
@@ -267,6 +299,64 @@ describe('Protocol V2 framing and session', () => {
|
|
|
267
299
|
});
|
|
268
300
|
});
|
|
269
301
|
|
|
302
|
+
test('decodes a two-byte legacy ProtocolInfo at the generic frame boundary', () => {
|
|
303
|
+
const frame = protocolV2.encodeProtobufFrame(60201, new Uint8Array([0x08, 0x01]));
|
|
304
|
+
const productionSchemas = {
|
|
305
|
+
protocolV1: protocolV1Messages,
|
|
306
|
+
protocolV2: productionProtocolV2Messages,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
expect(ProtocolV2.decodeFrame(productionSchemas, frame)).toMatchObject({
|
|
310
|
+
type: 'ProtocolInfo',
|
|
311
|
+
message: {
|
|
312
|
+
version: 1,
|
|
313
|
+
build_fingerprint: '',
|
|
314
|
+
supported_messages: [],
|
|
315
|
+
protobuf_definition: null,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('does not reinterpret a malformed current ProtocolInfo as the legacy layout', () => {
|
|
321
|
+
const frame = protocolV2.encodeProtobufFrame(60201, new Uint8Array([0x08, 0x01, 0x18, 0x01]));
|
|
322
|
+
|
|
323
|
+
expect(() =>
|
|
324
|
+
ProtocolV2.decodeFrame(
|
|
325
|
+
{
|
|
326
|
+
protocolV1: protocolV1Messages,
|
|
327
|
+
protocolV2: productionProtocolV2Messages,
|
|
328
|
+
},
|
|
329
|
+
frame
|
|
330
|
+
)
|
|
331
|
+
).toThrow('Protocol V2 protobuf decode failed for "ProtocolInfo"');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('preserves legacy ProtocolInfo capabilities when using the old field numbers', () => {
|
|
335
|
+
const payload = legacyProtocolInfoMessage
|
|
336
|
+
.encode({
|
|
337
|
+
version: 1,
|
|
338
|
+
supported_messages: [60602],
|
|
339
|
+
protobuf_definition: 'legacy',
|
|
340
|
+
})
|
|
341
|
+
.finish();
|
|
342
|
+
const frame = protocolV2.encodeProtobufFrame(60201, payload);
|
|
343
|
+
|
|
344
|
+
const decoded = ProtocolV2.decodeFrame(
|
|
345
|
+
{
|
|
346
|
+
protocolV1: protocolV1Messages,
|
|
347
|
+
protocolV2: productionProtocolV2Messages,
|
|
348
|
+
},
|
|
349
|
+
frame
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
expect(decoded.message).toEqual({
|
|
353
|
+
version: 1,
|
|
354
|
+
build_fingerprint: '',
|
|
355
|
+
supported_messages: [60602],
|
|
356
|
+
protobuf_definition: null,
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
270
360
|
test('does not encode V1-only messages into Protocol V2 frames', () => {
|
|
271
361
|
expect(() => ProtocolV2.encodeFrame(schemas, 'Ping', { message: 'ok' })).not.toThrow();
|
|
272
362
|
expect(() => ProtocolV2.encodeFrame(schemas, 'OnekeyGetFeatures', {})).toThrow(
|
|
@@ -446,6 +536,38 @@ describe('Protocol V2 framing and session', () => {
|
|
|
446
536
|
});
|
|
447
537
|
});
|
|
448
538
|
|
|
539
|
+
test('session decodes legacy ProtocolInfo through the generic frame boundary', async () => {
|
|
540
|
+
const productionSchemas = {
|
|
541
|
+
protocolV1: protocolV1Messages,
|
|
542
|
+
protocolV2: productionProtocolV2Messages,
|
|
543
|
+
};
|
|
544
|
+
const response = protocolV2.encodeProtobufFrame(60201, new Uint8Array([0x08, 0x01]));
|
|
545
|
+
const session = new ProtocolV2Session({
|
|
546
|
+
schemas: productionSchemas,
|
|
547
|
+
router: 1,
|
|
548
|
+
writeFrame: () => Promise.resolve(),
|
|
549
|
+
readFrame: () => Promise.resolve(rewriteSeq(response, 1)),
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
await expect(
|
|
553
|
+
session.call(
|
|
554
|
+
'ProtocolInfoRequest',
|
|
555
|
+
{ eventless_wallet_session: true },
|
|
556
|
+
{
|
|
557
|
+
expectedTypes: ['ProtocolInfo'],
|
|
558
|
+
}
|
|
559
|
+
)
|
|
560
|
+
).resolves.toEqual({
|
|
561
|
+
type: 'ProtocolInfo',
|
|
562
|
+
message: {
|
|
563
|
+
version: 1,
|
|
564
|
+
build_fingerprint: '',
|
|
565
|
+
supported_messages: [],
|
|
566
|
+
protobuf_definition: null,
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
449
571
|
test('session does not log Protocol V2 TX and RX frames', async () => {
|
|
450
572
|
const written = [];
|
|
451
573
|
const logger = { debug: jest.fn() };
|
|
@@ -850,24 +972,10 @@ describe('Protocol V2 framing and session', () => {
|
|
|
850
972
|
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('skip unexpected response'));
|
|
851
973
|
});
|
|
852
974
|
|
|
853
|
-
test('session
|
|
975
|
+
test('session can return after a request frame is written', async () => {
|
|
854
976
|
const written = [];
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
});
|
|
858
|
-
const success = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
859
|
-
message: 'ok',
|
|
860
|
-
});
|
|
861
|
-
const onIntermediateResponse = jest.fn();
|
|
862
|
-
const readFrame = jest.fn(() => {
|
|
863
|
-
const [writtenFrame] = written;
|
|
864
|
-
const { seq } = protocolV2.decodeFrame(writtenFrame);
|
|
865
|
-
return Promise.resolve(
|
|
866
|
-
readFrame.mock.calls.length === 1
|
|
867
|
-
? rewriteSeq(progress, seq)
|
|
868
|
-
: rewriteSeq(success, protocolV2.nextProtoSeq(seq))
|
|
869
|
-
);
|
|
870
|
-
});
|
|
977
|
+
const readFrame = jest.fn();
|
|
978
|
+
const onWriteCompleted = jest.fn();
|
|
871
979
|
const session = new ProtocolV2Session({
|
|
872
980
|
schemas,
|
|
873
981
|
router: 1,
|
|
@@ -880,26 +988,140 @@ describe('Protocol V2 framing and session', () => {
|
|
|
880
988
|
|
|
881
989
|
const result = await session.call(
|
|
882
990
|
'DeviceFirmwareUpdateRequest',
|
|
883
|
-
{
|
|
991
|
+
{},
|
|
884
992
|
{
|
|
885
|
-
|
|
886
|
-
|
|
993
|
+
returnAfterWrite: true,
|
|
994
|
+
onWriteCompleted,
|
|
887
995
|
}
|
|
888
996
|
);
|
|
889
997
|
|
|
890
|
-
expect(
|
|
891
|
-
expect(
|
|
998
|
+
expect(written).toHaveLength(1);
|
|
999
|
+
expect(ProtocolV2.decodeFrame(schemas, written[0]).messageName).toBe(
|
|
1000
|
+
'DeviceFirmwareUpdateRequest'
|
|
1001
|
+
);
|
|
1002
|
+
expect(readFrame).not.toHaveBeenCalled();
|
|
1003
|
+
expect(onWriteCompleted).toHaveBeenCalledTimes(1);
|
|
1004
|
+
expect(result).toEqual({ type: 'WriteCompleted', message: {} });
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
test('session consumes a delayed write-only response without completing the next call', async () => {
|
|
1008
|
+
const requestSuccess = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
1009
|
+
message: 'install accepted',
|
|
1010
|
+
});
|
|
1011
|
+
const statusResponse = ProtocolV2.encodeFrame(schemas, 'DeviceFirmwareUpdateStatus', {
|
|
1012
|
+
records: [{ target_id: 6, status: 1, path: 'vol0:/coprocessor.bin' }],
|
|
1013
|
+
});
|
|
1014
|
+
const prepareCall = jest.fn();
|
|
1015
|
+
const onResponseAfterWrite = jest.fn();
|
|
1016
|
+
const readFrame = jest
|
|
1017
|
+
.fn()
|
|
1018
|
+
.mockResolvedValueOnce(rewriteSeq(requestSuccess, 1))
|
|
1019
|
+
.mockResolvedValueOnce(rewriteSeq(statusResponse, 2));
|
|
1020
|
+
const session = new ProtocolV2Session({
|
|
1021
|
+
schemas,
|
|
1022
|
+
router: 1,
|
|
1023
|
+
prepareCall,
|
|
1024
|
+
writeFrame: () => Promise.resolve(),
|
|
1025
|
+
readFrame,
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
await expect(
|
|
1029
|
+
session.call(
|
|
1030
|
+
'DeviceFirmwareUpdateRequest',
|
|
1031
|
+
{},
|
|
1032
|
+
{
|
|
1033
|
+
returnAfterWrite: true,
|
|
1034
|
+
expectedTypes: ['Success'],
|
|
1035
|
+
onResponseAfterWrite,
|
|
1036
|
+
}
|
|
1037
|
+
)
|
|
1038
|
+
).resolves.toEqual({ type: 'WriteCompleted', message: {} });
|
|
1039
|
+
|
|
1040
|
+
await expect(
|
|
1041
|
+
session.call(
|
|
1042
|
+
'Ping',
|
|
1043
|
+
{ message: 'status-poll' },
|
|
1044
|
+
{
|
|
1045
|
+
expectedTypes: ['DeviceFirmwareUpdateStatus'],
|
|
1046
|
+
}
|
|
1047
|
+
)
|
|
1048
|
+
).resolves.toMatchObject({
|
|
892
1049
|
type: 'DeviceFirmwareUpdateStatus',
|
|
893
1050
|
message: {
|
|
894
|
-
records: [{ target_id:
|
|
1051
|
+
records: [{ target_id: 6, status: 1, path: 'vol0:/coprocessor.bin' }],
|
|
895
1052
|
},
|
|
896
1053
|
});
|
|
897
|
-
|
|
1054
|
+
|
|
1055
|
+
expect(prepareCall).toHaveBeenCalledTimes(1);
|
|
1056
|
+
expect(onResponseAfterWrite).toHaveBeenCalledWith({
|
|
898
1057
|
type: 'Success',
|
|
1058
|
+
message: { message: 'install accepted' },
|
|
1059
|
+
});
|
|
1060
|
+
expect(readFrame).toHaveBeenCalledTimes(2);
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
test('session preserves a delayed write-only Failure for the next caller', async () => {
|
|
1064
|
+
const requestFailure = ProtocolV2.encodeFrame(schemas, 'Failure', {
|
|
1065
|
+
code: 4,
|
|
1066
|
+
message: 'install cancelled',
|
|
1067
|
+
});
|
|
1068
|
+
const prepareCall = jest.fn();
|
|
1069
|
+
const onResponseAfterWrite = jest.fn();
|
|
1070
|
+
const readFrame = jest.fn().mockResolvedValueOnce(rewriteSeq(requestFailure, 1));
|
|
1071
|
+
const session = new ProtocolV2Session({
|
|
1072
|
+
schemas,
|
|
1073
|
+
router: 1,
|
|
1074
|
+
prepareCall,
|
|
1075
|
+
writeFrame: () => Promise.resolve(),
|
|
1076
|
+
readFrame,
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
await expect(
|
|
1080
|
+
session.call(
|
|
1081
|
+
'DeviceFirmwareUpdateRequest',
|
|
1082
|
+
{},
|
|
1083
|
+
{
|
|
1084
|
+
returnAfterWrite: true,
|
|
1085
|
+
expectedTypes: ['Success'],
|
|
1086
|
+
onResponseAfterWrite,
|
|
1087
|
+
}
|
|
1088
|
+
)
|
|
1089
|
+
).resolves.toEqual({ type: 'WriteCompleted', message: {} });
|
|
1090
|
+
|
|
1091
|
+
await expect(
|
|
1092
|
+
session.call(
|
|
1093
|
+
'Ping',
|
|
1094
|
+
{ message: 'status-poll' },
|
|
1095
|
+
{
|
|
1096
|
+
expectedTypes: ['DeviceFirmwareUpdateStatus'],
|
|
1097
|
+
}
|
|
1098
|
+
)
|
|
1099
|
+
).resolves.toMatchObject({
|
|
1100
|
+
type: 'Failure',
|
|
899
1101
|
message: {
|
|
900
|
-
|
|
1102
|
+
code: 4,
|
|
1103
|
+
message: 'install cancelled',
|
|
901
1104
|
},
|
|
902
1105
|
});
|
|
1106
|
+
|
|
1107
|
+
expect(prepareCall).toHaveBeenCalledTimes(1);
|
|
1108
|
+
expect(onResponseAfterWrite).not.toHaveBeenCalled();
|
|
1109
|
+
expect(readFrame).toHaveBeenCalledTimes(1);
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
test('probeProtocolV2 rethrows caller-selected fatal errors without treating them as a miss', async () => {
|
|
1113
|
+
const onProbeFailed = jest.fn();
|
|
1114
|
+
const staleBond = Object.assign(new Error('Bluetooth pairing failed'), { errorCode: 715 });
|
|
1115
|
+
|
|
1116
|
+
await expect(
|
|
1117
|
+
probeProtocolV2({
|
|
1118
|
+
call: () => Promise.reject(staleBond),
|
|
1119
|
+
timeoutMs: 1,
|
|
1120
|
+
onProbeFailed,
|
|
1121
|
+
shouldRethrow: error => error?.errorCode === 715,
|
|
1122
|
+
})
|
|
1123
|
+
).rejects.toBe(staleBond);
|
|
1124
|
+
expect(onProbeFailed).not.toHaveBeenCalled();
|
|
903
1125
|
});
|
|
904
1126
|
|
|
905
1127
|
test('probeProtocolV2 accepts Success as a normal V2 probe response', async () => {
|
|
@@ -918,6 +1140,66 @@ describe('Protocol V2 framing and session', () => {
|
|
|
918
1140
|
).resolves.toBe(false);
|
|
919
1141
|
});
|
|
920
1142
|
|
|
1143
|
+
test('recognizes USB-priority firmware failures independently of surrounding whitespace', () => {
|
|
1144
|
+
expect(isProtocolV2LinkDisabledFailure('Failure_ProcessError', ' link disabled ')).toBe(true);
|
|
1145
|
+
expect(isProtocolV2LinkDisabledFailure(5, 'Link Disabled')).toBe(true);
|
|
1146
|
+
expect(isProtocolV2LinkDisabledFailure('Failure_ProcessError', 'busy')).toBe(false);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
test('detects a split Protocol V2 link-disabled frame in the shared transport layer', () => {
|
|
1150
|
+
const assembler = new ProtocolV2FrameAssembler();
|
|
1151
|
+
const frame = ProtocolV2.encodeFrame(
|
|
1152
|
+
schemas,
|
|
1153
|
+
'Failure',
|
|
1154
|
+
{ code: 5, message: 'link disabled' },
|
|
1155
|
+
{ router: 2 }
|
|
1156
|
+
);
|
|
1157
|
+
const splitAt = 4;
|
|
1158
|
+
|
|
1159
|
+
expect(
|
|
1160
|
+
detectProtocolV2LinkDisabledError({
|
|
1161
|
+
schemas,
|
|
1162
|
+
assembler,
|
|
1163
|
+
bytes: frame.subarray(0, splitAt),
|
|
1164
|
+
})
|
|
1165
|
+
).toBeUndefined();
|
|
1166
|
+
expect(
|
|
1167
|
+
detectProtocolV2LinkDisabledError({
|
|
1168
|
+
schemas,
|
|
1169
|
+
assembler,
|
|
1170
|
+
bytes: frame.subarray(splitAt),
|
|
1171
|
+
})
|
|
1172
|
+
).toMatchObject({
|
|
1173
|
+
name: 'ProtocolV2LinkDisabledError',
|
|
1174
|
+
failureCode: 5,
|
|
1175
|
+
firmwareMessage: 'link disabled',
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
test.each(['Failure_ProcessError', 5])(
|
|
1180
|
+
'probeProtocolV2 surfaces link disabled without resetting the link for code %s',
|
|
1181
|
+
async code => {
|
|
1182
|
+
const onProbeFailed = jest.fn();
|
|
1183
|
+
|
|
1184
|
+
await expect(
|
|
1185
|
+
probeProtocolV2({
|
|
1186
|
+
call: () =>
|
|
1187
|
+
Promise.resolve({
|
|
1188
|
+
type: 'Failure',
|
|
1189
|
+
message: { code, message: ' link disabled ' },
|
|
1190
|
+
}),
|
|
1191
|
+
timeoutMs: 1,
|
|
1192
|
+
onProbeFailed,
|
|
1193
|
+
})
|
|
1194
|
+
).rejects.toMatchObject({
|
|
1195
|
+
name: 'ProtocolV2LinkDisabledError',
|
|
1196
|
+
failureCode: code,
|
|
1197
|
+
firmwareMessage: ' link disabled ',
|
|
1198
|
+
});
|
|
1199
|
+
expect(onProbeFailed).not.toHaveBeenCalled();
|
|
1200
|
+
}
|
|
1201
|
+
);
|
|
1202
|
+
|
|
921
1203
|
test('decodeFrame rejects frames that are too short', () => {
|
|
922
1204
|
expect(() => protocolV2.decodeFrame(new Uint8Array([0x5a, 0x08, 0x00]))).toThrow(
|
|
923
1205
|
'Protocol V2 frame too short'
|