@onekeyfe/hd-transport 1.2.2-alpha.100 → 1.2.2-alpha.12
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 +87 -18
- package/__tests__/protocol-v2-link-manager.test.js +70 -0
- package/__tests__/protocol-v2-usb-transport-base.test.js +6 -4
- package/__tests__/protocol-v2.test.js +185 -3
- package/dist/index.d.ts +125 -19
- package/dist/index.js +98 -13
- package/dist/protocols/v2/link-manager.d.ts +1 -0
- package/dist/protocols/v2/link-manager.d.ts.map +1 -1
- package/dist/protocols/v2/session.d.ts +9 -1
- package/dist/protocols/v2/session.d.ts.map +1 -1
- package/dist/types/messages.d.ts +80 -10
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +3 -0
- package/dist/types/transport.d.ts.map +1 -1
- package/messages-protocol-v2.json +1589 -1375
- package/package.json +2 -2
- package/src/constants.ts +1 -1
- package/src/protocols/v2/link-manager.ts +46 -5
- package/src/protocols/v2/session.ts +67 -3
- package/src/types/messages.ts +104 -15
- package/src/types/transport.ts +6 -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,
|
|
@@ -125,11 +150,6 @@ describe('messages', () => {
|
|
|
125
150
|
expect(v2Messages.nested.DeviceSessionGet.fields).toEqual({
|
|
126
151
|
session_id: { id: 1, type: 'bytes' },
|
|
127
152
|
btc_test_address: { id: 2, type: 'string' },
|
|
128
|
-
seed_domains: {
|
|
129
|
-
id: 3,
|
|
130
|
-
rule: 'repeated',
|
|
131
|
-
type: 'DeviceSessionSeedDomain',
|
|
132
|
-
},
|
|
133
153
|
});
|
|
134
154
|
expect(v2Messages.nested.DeviceSessionSeedDomain.values).toEqual({
|
|
135
155
|
SeedDomain_Standard: 1,
|
|
@@ -151,9 +171,14 @@ describe('messages', () => {
|
|
|
151
171
|
expect(v2Messages.nested).not.toHaveProperty('DeviceWalletSelect');
|
|
152
172
|
expect(v2Messages.nested).not.toHaveProperty('DeviceWalletType');
|
|
153
173
|
expect(v2Messages.nested).not.toHaveProperty('DeviceHiddenWalletSelect');
|
|
154
|
-
expect(v2Messages.nested.DeviceSession.fields).
|
|
174
|
+
expect(v2Messages.nested.DeviceSession.fields).toEqual({
|
|
155
175
|
session_id: { id: 1, type: 'bytes' },
|
|
156
176
|
btc_test_address: { id: 2, type: 'string' },
|
|
177
|
+
seed_domains: {
|
|
178
|
+
rule: 'repeated',
|
|
179
|
+
type: 'DeviceSessionSeedDomain',
|
|
180
|
+
id: 3,
|
|
181
|
+
},
|
|
157
182
|
});
|
|
158
183
|
expect(v2Messages.nested.DeviceSessionAskPin.fields.type).toMatchObject({
|
|
159
184
|
id: 1,
|
|
@@ -170,6 +195,11 @@ describe('messages', () => {
|
|
|
170
195
|
type: 'bool',
|
|
171
196
|
id: 2,
|
|
172
197
|
},
|
|
198
|
+
seed_domains: {
|
|
199
|
+
rule: 'repeated',
|
|
200
|
+
type: 'DeviceSessionSeedDomain',
|
|
201
|
+
id: 3,
|
|
202
|
+
},
|
|
173
203
|
},
|
|
174
204
|
});
|
|
175
205
|
expect(v2Messages.nested.DeviceSessionAskPin_FailureSubCodes.values).toEqual({
|
|
@@ -187,38 +217,77 @@ describe('messages', () => {
|
|
|
187
217
|
const messages = parseConfigure(v2Messages);
|
|
188
218
|
const { Message } = createMessageFromName(messages, 'DeviceSessionAskPassphrase');
|
|
189
219
|
|
|
190
|
-
const standardWallet = encode(Message, {
|
|
220
|
+
const standardWallet = encode(Message, {
|
|
221
|
+
passphrase: '',
|
|
222
|
+
on_device: false,
|
|
223
|
+
seed_domains: [],
|
|
224
|
+
});
|
|
191
225
|
const onHost = Message.encode(
|
|
192
|
-
Message.create({
|
|
226
|
+
Message.create({
|
|
227
|
+
passphrase: 'host hidden wallet',
|
|
228
|
+
on_device: false,
|
|
229
|
+
seed_domains: [
|
|
230
|
+
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
|
|
231
|
+
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
|
|
232
|
+
],
|
|
233
|
+
})
|
|
234
|
+
).finish();
|
|
235
|
+
const onDevice = Message.encode(
|
|
236
|
+
Message.create({
|
|
237
|
+
on_device: true,
|
|
238
|
+
seed_domains: [generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard],
|
|
239
|
+
})
|
|
193
240
|
).finish();
|
|
194
|
-
const onDevice = Message.encode(Message.create({ on_device: true })).finish();
|
|
195
241
|
|
|
196
242
|
expect(standardWallet.toString('hex')).toBe('0a001000');
|
|
197
243
|
expect(Buffer.from(onHost).toString('hex')).toBe(
|
|
198
|
-
'
|
|
244
|
+
'0a12686f73742068696464656e2077616c6c657410001a020102'
|
|
199
245
|
);
|
|
200
|
-
expect(Buffer.from(onDevice).toString('hex')).toBe('
|
|
246
|
+
expect(Buffer.from(onDevice).toString('hex')).toBe('10011a0101');
|
|
201
247
|
expect(Message.decode(onHost)).toMatchObject({
|
|
202
248
|
passphrase: 'host hidden wallet',
|
|
203
249
|
on_device: false,
|
|
250
|
+
seed_domains: [
|
|
251
|
+
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
|
|
252
|
+
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
expect(Message.decode(onDevice)).toMatchObject({
|
|
256
|
+
on_device: true,
|
|
257
|
+
seed_domains: [generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard],
|
|
204
258
|
});
|
|
205
|
-
expect(Message.decode(onDevice)).toMatchObject({ on_device: true });
|
|
206
259
|
});
|
|
207
260
|
|
|
208
|
-
test('Protocol V2 wallet recovery carries the expected wallet
|
|
261
|
+
test('Protocol V2 wallet recovery carries the expected wallet on wire', () => {
|
|
209
262
|
const messages = parseConfigure(v2Messages);
|
|
210
263
|
const { Message } = createMessageFromName(messages, 'DeviceSessionGet');
|
|
211
264
|
const payload = encode(Message, {
|
|
212
265
|
btc_test_address: 'tb1qwallet',
|
|
213
|
-
seed_domains: [
|
|
214
|
-
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
|
|
215
|
-
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
|
|
216
|
-
],
|
|
217
266
|
});
|
|
218
267
|
|
|
219
|
-
expect(payload.toString('hex')).toBe('
|
|
268
|
+
expect(payload.toString('hex')).toBe('120a7462317177616c6c6574');
|
|
220
269
|
expect(Message.decode(payload.toBuffer())).toMatchObject({
|
|
221
270
|
btc_test_address: 'tb1qwallet',
|
|
271
|
+
});
|
|
272
|
+
expect(Message.decode(payload.toBuffer())).not.toHaveProperty('seed_domains');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('Protocol V2 DeviceSession reports generated seed domains on wire', () => {
|
|
276
|
+
const messages = parseConfigure(v2Messages);
|
|
277
|
+
const { Message } = createMessageFromName(messages, 'DeviceSession');
|
|
278
|
+
const encoded = Message.encode(
|
|
279
|
+
Message.create({
|
|
280
|
+
btc_test_address: 'tb1qwallet',
|
|
281
|
+
seed_domains: [
|
|
282
|
+
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
|
|
283
|
+
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
|
|
284
|
+
],
|
|
285
|
+
})
|
|
286
|
+
).finish();
|
|
287
|
+
|
|
288
|
+
expect(Buffer.from(encoded).toString('hex')).toBe('120a7462317177616c6c65741a020102');
|
|
289
|
+
expect(Message.decode(encoded)).toMatchObject({
|
|
290
|
+
btc_test_address: 'tb1qwallet',
|
|
222
291
|
seed_domains: [
|
|
223
292
|
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
|
|
224
293
|
generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
|
|
@@ -317,6 +317,76 @@ describe('ProtocolV2LinkManager', () => {
|
|
|
317
317
|
]);
|
|
318
318
|
});
|
|
319
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
|
+
|
|
320
390
|
test('writes flow control while the active call is waiting for its response', async () => {
|
|
321
391
|
const sentSeqs = [];
|
|
322
392
|
let releaseRead;
|
|
@@ -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,6 +5,7 @@ const {
|
|
|
5
5
|
ProtocolV2LinkError,
|
|
6
6
|
ProtocolV2SequenceCursor,
|
|
7
7
|
ProtocolV2Session,
|
|
8
|
+
detectProtocolV2LinkDisabledError,
|
|
8
9
|
hexToBytes,
|
|
9
10
|
isProtocolV2HighThroughputCall,
|
|
10
11
|
probeProtocolV2,
|
|
@@ -12,6 +13,7 @@ const {
|
|
|
12
13
|
} = require('../src/protocols/v2/session');
|
|
13
14
|
const protocolV2 = require('../src/protocols/v2');
|
|
14
15
|
const {
|
|
16
|
+
PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
|
|
15
17
|
PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE,
|
|
16
18
|
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
17
19
|
PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
@@ -451,12 +453,13 @@ describe('Protocol V2 framing and session', () => {
|
|
|
451
453
|
).toThrow('Protocol V2 frame too large: 4201 > 4200');
|
|
452
454
|
});
|
|
453
455
|
|
|
454
|
-
test('keeps optimized BLE
|
|
456
|
+
test('keeps optimized BLE fixed-path chunks inside the transport frame boundary', () => {
|
|
455
457
|
const productionSchemas = {
|
|
456
458
|
protocolV1: protocolV1Messages,
|
|
457
459
|
protocolV2: productionProtocolV2Messages,
|
|
458
460
|
};
|
|
459
|
-
const
|
|
461
|
+
const fixedPaths = [
|
|
462
|
+
'vol1:/wallpapers/wallpaper.okpkg',
|
|
460
463
|
'vol0:/bootloader.bin',
|
|
461
464
|
'vol0:/application_p1.bin',
|
|
462
465
|
'vol0:/application_p2.bin',
|
|
@@ -467,7 +470,7 @@ describe('Protocol V2 framing and session', () => {
|
|
|
467
470
|
'vol0:/se04.bin',
|
|
468
471
|
];
|
|
469
472
|
|
|
470
|
-
for (const path of
|
|
473
|
+
for (const path of fixedPaths) {
|
|
471
474
|
const frame = ProtocolV2.encodeFrame(productionSchemas, 'FilesystemFileWrite', {
|
|
472
475
|
file: {
|
|
473
476
|
path,
|
|
@@ -484,6 +487,35 @@ describe('Protocol V2 framing and session', () => {
|
|
|
484
487
|
}
|
|
485
488
|
});
|
|
486
489
|
|
|
490
|
+
test('keeps the generic BLE chunk safe for the longest valid filesystem path', () => {
|
|
491
|
+
const productionSchemas = {
|
|
492
|
+
protocolV1: protocolV1Messages,
|
|
493
|
+
protocolV2: productionProtocolV2Messages,
|
|
494
|
+
};
|
|
495
|
+
const longestValidPath = `vol0:/${'a'.repeat(121)}`;
|
|
496
|
+
const encodeFileWrite = dataLength =>
|
|
497
|
+
ProtocolV2.encodeFrame(productionSchemas, 'FilesystemFileWrite', {
|
|
498
|
+
file: {
|
|
499
|
+
path: longestValidPath,
|
|
500
|
+
offset: 0xffffffff,
|
|
501
|
+
total_size: 0xffffffff,
|
|
502
|
+
data: new Uint8Array(dataLength),
|
|
503
|
+
},
|
|
504
|
+
overwrite: true,
|
|
505
|
+
append: true,
|
|
506
|
+
ui_percentage: 100,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(Buffer.byteLength(longestValidPath, 'utf8')).toBe(127);
|
|
510
|
+
expect(encodeFileWrite(PROTOCOL_V2_BLE_FILE_CHUNK_SIZE).length).toBeLessThanOrEqual(
|
|
511
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES
|
|
512
|
+
);
|
|
513
|
+
expect(encodeFileWrite(PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE).length).toBeGreaterThan(
|
|
514
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES
|
|
515
|
+
);
|
|
516
|
+
expect(encodeFileWrite(1885)).toHaveLength(PROTOCOL_V2_BLE_FRAME_MAX_BYTES);
|
|
517
|
+
});
|
|
518
|
+
|
|
487
519
|
test('keeps bytes after the first complete frame for the next read', () => {
|
|
488
520
|
const first = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
|
|
489
521
|
version: 1,
|
|
@@ -1003,6 +1035,126 @@ describe('Protocol V2 framing and session', () => {
|
|
|
1003
1035
|
expect(result).toEqual({ type: 'WriteCompleted', message: {} });
|
|
1004
1036
|
});
|
|
1005
1037
|
|
|
1038
|
+
test('session consumes a delayed write-only response without completing the next call', async () => {
|
|
1039
|
+
const requestSuccess = ProtocolV2.encodeFrame(schemas, 'Success', {
|
|
1040
|
+
message: 'install accepted',
|
|
1041
|
+
});
|
|
1042
|
+
const statusResponse = ProtocolV2.encodeFrame(schemas, 'DeviceFirmwareUpdateStatus', {
|
|
1043
|
+
records: [{ target_id: 6, status: 1, path: 'vol0:/coprocessor.bin' }],
|
|
1044
|
+
});
|
|
1045
|
+
const prepareCall = jest.fn();
|
|
1046
|
+
const onResponseAfterWrite = jest.fn();
|
|
1047
|
+
const readFrame = jest
|
|
1048
|
+
.fn()
|
|
1049
|
+
.mockResolvedValueOnce(rewriteSeq(requestSuccess, 1))
|
|
1050
|
+
.mockResolvedValueOnce(rewriteSeq(statusResponse, 2));
|
|
1051
|
+
const session = new ProtocolV2Session({
|
|
1052
|
+
schemas,
|
|
1053
|
+
router: 1,
|
|
1054
|
+
prepareCall,
|
|
1055
|
+
writeFrame: () => Promise.resolve(),
|
|
1056
|
+
readFrame,
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
await expect(
|
|
1060
|
+
session.call(
|
|
1061
|
+
'DeviceFirmwareUpdateRequest',
|
|
1062
|
+
{},
|
|
1063
|
+
{
|
|
1064
|
+
returnAfterWrite: true,
|
|
1065
|
+
expectedTypes: ['Success'],
|
|
1066
|
+
onResponseAfterWrite,
|
|
1067
|
+
}
|
|
1068
|
+
)
|
|
1069
|
+
).resolves.toEqual({ type: 'WriteCompleted', message: {} });
|
|
1070
|
+
|
|
1071
|
+
await expect(
|
|
1072
|
+
session.call(
|
|
1073
|
+
'Ping',
|
|
1074
|
+
{ message: 'status-poll' },
|
|
1075
|
+
{
|
|
1076
|
+
expectedTypes: ['DeviceFirmwareUpdateStatus'],
|
|
1077
|
+
}
|
|
1078
|
+
)
|
|
1079
|
+
).resolves.toMatchObject({
|
|
1080
|
+
type: 'DeviceFirmwareUpdateStatus',
|
|
1081
|
+
message: {
|
|
1082
|
+
records: [{ target_id: 6, status: 1, path: 'vol0:/coprocessor.bin' }],
|
|
1083
|
+
},
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
expect(prepareCall).toHaveBeenCalledTimes(1);
|
|
1087
|
+
expect(onResponseAfterWrite).toHaveBeenCalledWith({
|
|
1088
|
+
type: 'Success',
|
|
1089
|
+
message: { message: 'install accepted' },
|
|
1090
|
+
});
|
|
1091
|
+
expect(readFrame).toHaveBeenCalledTimes(2);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
test('session preserves a delayed write-only Failure for the next caller', async () => {
|
|
1095
|
+
const requestFailure = ProtocolV2.encodeFrame(schemas, 'Failure', {
|
|
1096
|
+
code: 4,
|
|
1097
|
+
message: 'install cancelled',
|
|
1098
|
+
});
|
|
1099
|
+
const prepareCall = jest.fn();
|
|
1100
|
+
const onResponseAfterWrite = jest.fn();
|
|
1101
|
+
const readFrame = jest.fn().mockResolvedValueOnce(rewriteSeq(requestFailure, 1));
|
|
1102
|
+
const session = new ProtocolV2Session({
|
|
1103
|
+
schemas,
|
|
1104
|
+
router: 1,
|
|
1105
|
+
prepareCall,
|
|
1106
|
+
writeFrame: () => Promise.resolve(),
|
|
1107
|
+
readFrame,
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
await expect(
|
|
1111
|
+
session.call(
|
|
1112
|
+
'DeviceFirmwareUpdateRequest',
|
|
1113
|
+
{},
|
|
1114
|
+
{
|
|
1115
|
+
returnAfterWrite: true,
|
|
1116
|
+
expectedTypes: ['Success'],
|
|
1117
|
+
onResponseAfterWrite,
|
|
1118
|
+
}
|
|
1119
|
+
)
|
|
1120
|
+
).resolves.toEqual({ type: 'WriteCompleted', message: {} });
|
|
1121
|
+
|
|
1122
|
+
await expect(
|
|
1123
|
+
session.call(
|
|
1124
|
+
'Ping',
|
|
1125
|
+
{ message: 'status-poll' },
|
|
1126
|
+
{
|
|
1127
|
+
expectedTypes: ['DeviceFirmwareUpdateStatus'],
|
|
1128
|
+
}
|
|
1129
|
+
)
|
|
1130
|
+
).resolves.toMatchObject({
|
|
1131
|
+
type: 'Failure',
|
|
1132
|
+
message: {
|
|
1133
|
+
code: 4,
|
|
1134
|
+
message: 'install cancelled',
|
|
1135
|
+
},
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
expect(prepareCall).toHaveBeenCalledTimes(1);
|
|
1139
|
+
expect(onResponseAfterWrite).not.toHaveBeenCalled();
|
|
1140
|
+
expect(readFrame).toHaveBeenCalledTimes(1);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
test('probeProtocolV2 rethrows caller-selected fatal errors without treating them as a miss', async () => {
|
|
1144
|
+
const onProbeFailed = jest.fn();
|
|
1145
|
+
const staleBond = Object.assign(new Error('Bluetooth pairing failed'), { errorCode: 715 });
|
|
1146
|
+
|
|
1147
|
+
await expect(
|
|
1148
|
+
probeProtocolV2({
|
|
1149
|
+
call: () => Promise.reject(staleBond),
|
|
1150
|
+
timeoutMs: 1,
|
|
1151
|
+
onProbeFailed,
|
|
1152
|
+
shouldRethrow: error => error?.errorCode === 715,
|
|
1153
|
+
})
|
|
1154
|
+
).rejects.toBe(staleBond);
|
|
1155
|
+
expect(onProbeFailed).not.toHaveBeenCalled();
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1006
1158
|
test('probeProtocolV2 accepts Success as a normal V2 probe response', async () => {
|
|
1007
1159
|
await expect(
|
|
1008
1160
|
probeProtocolV2({
|
|
@@ -1025,6 +1177,36 @@ describe('Protocol V2 framing and session', () => {
|
|
|
1025
1177
|
expect(isProtocolV2LinkDisabledFailure('Failure_ProcessError', 'busy')).toBe(false);
|
|
1026
1178
|
});
|
|
1027
1179
|
|
|
1180
|
+
test('detects a split Protocol V2 link-disabled frame in the shared transport layer', () => {
|
|
1181
|
+
const assembler = new ProtocolV2FrameAssembler();
|
|
1182
|
+
const frame = ProtocolV2.encodeFrame(
|
|
1183
|
+
schemas,
|
|
1184
|
+
'Failure',
|
|
1185
|
+
{ code: 5, message: 'link disabled' },
|
|
1186
|
+
{ router: 2 }
|
|
1187
|
+
);
|
|
1188
|
+
const splitAt = 4;
|
|
1189
|
+
|
|
1190
|
+
expect(
|
|
1191
|
+
detectProtocolV2LinkDisabledError({
|
|
1192
|
+
schemas,
|
|
1193
|
+
assembler,
|
|
1194
|
+
bytes: frame.subarray(0, splitAt),
|
|
1195
|
+
})
|
|
1196
|
+
).toBeUndefined();
|
|
1197
|
+
expect(
|
|
1198
|
+
detectProtocolV2LinkDisabledError({
|
|
1199
|
+
schemas,
|
|
1200
|
+
assembler,
|
|
1201
|
+
bytes: frame.subarray(splitAt),
|
|
1202
|
+
})
|
|
1203
|
+
).toMatchObject({
|
|
1204
|
+
name: 'ProtocolV2LinkDisabledError',
|
|
1205
|
+
failureCode: 5,
|
|
1206
|
+
firmwareMessage: 'link disabled',
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1028
1210
|
test.each(['Failure_ProcessError', 5])(
|
|
1029
1211
|
'probeProtocolV2 surfaces link disabled without resetting the link for code %s',
|
|
1030
1212
|
async code => {
|