@onekeyfe/hd-core 1.2.0-alpha.115 → 1.2.0-alpha.117

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 (35) hide show
  1. package/__tests__/DeviceCommands.test.ts +2 -2
  2. package/__tests__/device-lifecycle-events.test.ts +1 -1
  3. package/__tests__/device-state-mapper.test.ts +4 -4
  4. package/__tests__/device-utils.test.ts +1 -1
  5. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +36 -89
  6. package/__tests__/get-device-state.test.ts +8 -8
  7. package/__tests__/protocol-v2-resources.test.ts +20 -0
  8. package/__tests__/protocol-v2.test.ts +411 -246
  9. package/dist/api/FirmwareUpdateV4.d.ts +0 -3
  10. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  11. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +3 -1
  12. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  13. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
  14. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  15. package/dist/index.d.ts +5 -20
  16. package/dist/index.js +191 -257
  17. package/dist/protocols/protocol-v2/features.d.ts +4 -4
  18. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  19. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  20. package/dist/types/settings.d.ts +4 -19
  21. package/dist/types/settings.d.ts.map +1 -1
  22. package/dist/utils/patch.d.ts +1 -1
  23. package/dist/utils/patch.d.ts.map +1 -1
  24. package/package.json +4 -4
  25. package/src/api/FirmwareUpdateV4.ts +170 -243
  26. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
  27. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +6 -28
  28. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
  29. package/src/data/messages/messages-protocol-v2.json +31 -14
  30. package/src/device/DeviceStateMapper.ts +15 -15
  31. package/src/deviceProfile/buildDeviceFeatures.ts +3 -3
  32. package/src/protocols/protocol-v2/features.ts +6 -6
  33. package/src/protocols/protocol-v2/resources.ts +10 -49
  34. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +1 -0
  35. package/src/types/settings.ts +4 -19
@@ -28,7 +28,6 @@ import { DevicePool } from '../device/DevicePool';
28
28
  import {
29
29
  PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
30
30
  ProtocolV2FirmwareTargetType,
31
- isLegacyProtocolV2ProtocolInfo,
32
31
  } from '../protocols/protocol-v2';
33
32
  import { requestProtocolV2DeviceInfo } from '../protocols/protocol-v2/features';
34
33
  import {
@@ -70,6 +69,7 @@ import type {
70
69
  Features,
71
70
  IFirmwareReleaseInfo,
72
71
  IProtocolV2FirmwareComponent,
72
+ IProtocolV2ResourceManifestFile,
73
73
  IVersionArray,
74
74
  } from '../types';
75
75
  import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
@@ -89,8 +89,8 @@ const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
89
89
  const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
90
90
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
91
91
  const PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT = 15 * 1000;
92
- const PROTOCOL_V2_START_UPDATE_TIMEOUT = 3 * 60 * 1000;
93
92
  const PROTOCOL_V2_INSTALL_TIMEOUT = 8 * 60 * 1000;
93
+ const PROTOCOL_V2_INSTALL_STATUS_INITIAL_DELAY = 1000;
94
94
  const PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT = 30 * 1000;
95
95
  const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
96
96
  const PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS = 1;
@@ -113,6 +113,13 @@ const PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES = 1024 * 1024;
113
113
  const PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT = 512;
114
114
  const PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES = 256 * 1024 * 1024;
115
115
 
116
+ const getProtocolV2LocalResourceArchivePath = (entryName: string) => {
117
+ const match = entryName.match(
118
+ /(?:^|\/)((?:bundles\/|loaders\/(?:bootloader|rom)\/).+\.okpkg)$/iu
119
+ );
120
+ return match?.[1];
121
+ };
122
+
116
123
  const PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS = new Set<FirmwareUpdateV4Target>(['se03', 'se04']);
117
124
 
118
125
  const getProtocolV2ZipEntrySizes = (entry: JSZip.JSZipObject) => {
@@ -181,14 +188,11 @@ const getProtocolV2DeviceTransferProgress = (
181
188
  totalBytes: number
182
189
  ) => {
183
190
  if (!Number.isFinite(totalBytes) || totalBytes <= 0) {
184
- return 100;
191
+ return 0;
185
192
  }
186
193
  if (bytesBeforeChunk <= 0 && bytesAfterChunk < totalBytes) {
187
194
  return 0;
188
195
  }
189
- if (bytesAfterChunk >= totalBytes) {
190
- return 100;
191
- }
192
196
  return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
193
197
  };
194
198
 
@@ -199,8 +203,6 @@ type ProtocolV2FirmwareUpdateStatusTarget = {
199
203
  path?: string;
200
204
  };
201
205
 
202
- type ProtocolV2FirmwareUpdateStartResponse = TypedResponseMessage<'Success'>;
203
-
204
206
  type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId: number };
205
207
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
206
208
  kind: ProtocolV2RemoteComponentTarget['kind'];
@@ -377,31 +379,6 @@ const isProtocolV2ReconnectProbeError = (error: unknown) => {
377
379
  );
378
380
  };
379
381
 
380
- const PROTOCOL_V2_BLE_INSTALL_INTERRUPTION_ERROR_CODES = new Set<number>([
381
- HardwareErrorCode.BleConnectedError,
382
- HardwareErrorCode.BleCharacteristicNotifyError,
383
- HardwareErrorCode.BleForceCleanRunPromise,
384
- HardwareErrorCode.BleDeviceDisconnected,
385
- ]);
386
-
387
- const isProtocolV2BleInstallInterruptionError = (error: unknown) => {
388
- if (
389
- error instanceof HardwareError &&
390
- PROTOCOL_V2_BLE_INSTALL_INTERRUPTION_ERROR_CODES.has(error.errorCode)
391
- ) {
392
- return true;
393
- }
394
-
395
- const message = getProtocolV2UnknownErrorText(error).toLowerCase();
396
- const compactMessage = message.replace(/\s+/gu, '');
397
- return (
398
- /react native ble transport (?:released|disconnected)/u.test(message) ||
399
- (compactMessage.includes('rxerrorerror6') &&
400
- (compactMessage.includes('multiplatformbleadapter') ||
401
- compactMessage.includes('multipalformebleadapter')))
402
- );
403
- };
404
-
405
382
  const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
406
383
  const message = getProtocolV2UnknownErrorText(error).toLowerCase();
407
384
  return (
@@ -411,15 +388,6 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
411
388
  );
412
389
  };
413
390
 
414
- const isProtocolV2FirmwareUpdateEndpointUnavailable = (error: unknown) => {
415
- const message = getProtocolV2UnknownErrorText(error).toLowerCase();
416
- return (
417
- message.includes('handler not registered') ||
418
- message.includes('message handler not found') ||
419
- message.includes('unsupported message')
420
- );
421
- };
422
-
423
391
  const isProtocolV2TerminalInstallStatusError = (error: unknown) =>
424
392
  error instanceof HardwareError &&
425
393
  (error.errorCode === HardwareErrorCode.FirmwareError ||
@@ -596,7 +564,7 @@ export const assertProtocolV2ReconnectIdentity = (
596
564
  *
597
565
  * It intentionally does not fall back to FirmwareUpdateV3/V1 behavior:
598
566
  * - upload uses FilesystemFileWrite
599
- * - install uses DeviceFirmwareUpdateRequest
567
+ * - install uses DeviceFirmwareUpdateStage followed by an empty DeviceFirmwareUpdateRequest
600
568
  * - completion waits for target status to finish, reboots to normal, then polls DeviceInfo
601
569
  */
602
570
  export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareUpdateV4Params> {
@@ -614,8 +582,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
614
582
 
615
583
  private protocolV2ExecutionInLoader = false;
616
584
 
617
- private protocolV2LegacyDirectUpdate = false;
618
-
619
585
  private protocolV2BootResourceStagingSafe = false;
620
586
 
621
587
  private protocolV2CompletedTargetVersions = new Map<number, number>();
@@ -624,8 +590,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
624
590
 
625
591
  private protocolV2FinalStatusVerified = false;
626
592
 
627
- private protocolV2InstallAckReceived = false;
628
-
629
593
  private protocolV2InstallBaselineVersions = new Map<number, string>();
630
594
 
631
595
  private protocolV2LastRuntimeProbeFeatures?: Features;
@@ -1293,92 +1257,86 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1293
1257
  );
1294
1258
  }
1295
1259
  const entries = zipEntries.filter(entry => !entry.dir);
1296
- if (entries.length === 0 || entries.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT + 1) {
1260
+ if (entries.length === 0) {
1297
1261
  throw ERRORS.TypedError(
1298
1262
  HardwareErrorCode.RuntimeError,
1299
1263
  'Protocol V2 local resource ZIP entry set is invalid',
1300
1264
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1301
1265
  );
1302
1266
  }
1303
- let declaredUncompressedSize = 0;
1304
- let declaredCompressedSize = 0;
1305
- for (const entry of entries) {
1306
- const sizes = getProtocolV2ZipEntrySizes(entry);
1307
- declaredCompressedSize += sizes.compressedSize;
1308
- declaredUncompressedSize += sizes.uncompressedSize;
1309
- const entryLimit =
1310
- entry.name === 'manifest.json'
1311
- ? PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1312
- : PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES;
1267
+ const manifestEntry = entries.find(entry => entry.name.split('/').pop() === 'manifest.json');
1268
+ let manifestBinary: ArrayBuffer | undefined;
1269
+ let manifestDirectory = '';
1270
+ let selectedFiles: IProtocolV2ResourceManifestFile[];
1271
+ if (manifestEntry) {
1313
1272
  if (
1314
- sizes.uncompressedSize > entryLimit ||
1315
- declaredCompressedSize > binary.byteLength ||
1316
- declaredUncompressedSize >
1317
- PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES + PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1273
+ getProtocolV2ZipEntrySizes(manifestEntry).uncompressedSize >
1274
+ PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1318
1275
  ) {
1319
1276
  throw ERRORS.TypedError(
1320
1277
  HardwareErrorCode.RuntimeError,
1321
- 'Protocol V2 local resource ZIP declared size exceeds the allowed limit',
1278
+ 'Protocol V2 local resource manifest size is invalid',
1322
1279
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1323
1280
  );
1324
1281
  }
1282
+ manifestBinary = await manifestEntry.async('arraybuffer');
1283
+ if (
1284
+ manifestBinary.byteLength <= 0 ||
1285
+ manifestBinary.byteLength > PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1286
+ ) {
1287
+ throw ERRORS.TypedError(
1288
+ HardwareErrorCode.RuntimeError,
1289
+ 'Protocol V2 local resource manifest size is invalid',
1290
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1291
+ );
1292
+ }
1293
+ let manifestValue: unknown;
1294
+ try {
1295
+ manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
1296
+ } catch (error) {
1297
+ throw ERRORS.TypedError(
1298
+ HardwareErrorCode.RuntimeError,
1299
+ `Protocol V2 local resource manifest is invalid: ${String(error)}`,
1300
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1301
+ );
1302
+ }
1303
+ selectedFiles = selectProtocolV2ResourceManifestFiles({
1304
+ manifest: parseProtocolV2ResourceManifest(manifestValue),
1305
+ targetsToUpdate: this.params.targetsToUpdate ?? [],
1306
+ });
1307
+ manifestDirectory = manifestEntry.name.slice(0, -'manifest.json'.length);
1308
+ } else {
1309
+ selectedFiles = entries.flatMap(entry => {
1310
+ const archivePath = getProtocolV2LocalResourceArchivePath(entry.name);
1311
+ if (!archivePath) return [];
1312
+ return [
1313
+ {
1314
+ archive_path: archivePath,
1315
+ original_name: archivePath.split('/').pop() ?? archivePath,
1316
+ device_path: `vol0:/${archivePath}`,
1317
+ size: getProtocolV2ZipEntrySizes(entry).uncompressedSize,
1318
+ sha256: '',
1319
+ },
1320
+ ];
1321
+ });
1325
1322
  }
1326
- const manifestEntry = zip.file('manifest.json');
1327
- if (!manifestEntry) {
1328
- throw ERRORS.TypedError(
1329
- HardwareErrorCode.RuntimeError,
1330
- 'Protocol V2 local resource ZIP has no manifest.json',
1331
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1332
- );
1333
- }
1334
- const manifestBinary = await manifestEntry.async('arraybuffer');
1335
- if (
1336
- manifestBinary.byteLength <= 0 ||
1337
- manifestBinary.byteLength > PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1338
- ) {
1339
- throw ERRORS.TypedError(
1340
- HardwareErrorCode.RuntimeError,
1341
- 'Protocol V2 local resource manifest size is invalid',
1342
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1343
- );
1344
- }
1345
-
1346
- let manifestValue: unknown;
1347
- try {
1348
- manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
1349
- } catch (error) {
1350
- throw ERRORS.TypedError(
1351
- HardwareErrorCode.RuntimeError,
1352
- `Protocol V2 local resource manifest is invalid: ${String(error)}`,
1353
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1354
- );
1355
- }
1356
- const manifest = parseProtocolV2ResourceManifest(manifestValue);
1357
- const selectedFiles = selectProtocolV2ResourceManifestFiles({
1358
- manifest,
1359
- targetsToUpdate: this.params.targetsToUpdate ?? [],
1360
- });
1361
- const expectedEntryNames = new Set([
1362
- 'manifest.json',
1363
- ...selectedFiles.map(file => file.archive_path),
1364
- ]);
1365
- if (
1366
- entries.length !== expectedEntryNames.size ||
1367
- entries.some(entry => !expectedEntryNames.has(entry.name))
1368
- ) {
1323
+ if (selectedFiles.length === 0 || selectedFiles.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT) {
1369
1324
  throw ERRORS.TypedError(
1370
1325
  HardwareErrorCode.RuntimeError,
1371
- 'Protocol V2 local resource ZIP contains an unexpected entry',
1326
+ 'Protocol V2 local resource ZIP has no resource packages',
1372
1327
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1373
1328
  );
1374
1329
  }
1375
1330
 
1376
1331
  let totalSize = 0;
1377
- const materializedEntries: FirmwareMemoryArtifactEntry[] = [
1378
- { entryName: 'manifest.json', binary: manifestBinary },
1379
- ];
1332
+ const materializedEntries: FirmwareMemoryArtifactEntry[] = [];
1333
+ const normalizedFiles: IProtocolV2ResourceManifestFile[] = [];
1380
1334
  for (const file of selectedFiles) {
1381
- const entry = zip.file(file.archive_path);
1335
+ const entry = manifestEntry
1336
+ ? zip.file(`${manifestDirectory}${file.archive_path}`)
1337
+ : entries.find(
1338
+ candidate => getProtocolV2LocalResourceArchivePath(candidate.name) === file.archive_path
1339
+ );
1382
1340
  if (!entry) {
1383
1341
  throw ERRORS.TypedError(
1384
1342
  HardwareErrorCode.RuntimeError,
@@ -1397,15 +1355,21 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1397
1355
  }
1398
1356
  const fileBinary = await entry.async('arraybuffer');
1399
1357
  const digest = bytesToHex(sha256(new Uint8Array(fileBinary)));
1400
- if (fileBinary.byteLength !== file.size || digest !== file.sha256.toLowerCase()) {
1358
+ if (
1359
+ fileBinary.byteLength !== file.size ||
1360
+ (file.sha256 && digest !== file.sha256.toLowerCase())
1361
+ ) {
1401
1362
  throw ERRORS.TypedError(
1402
1363
  HardwareErrorCode.RuntimeError,
1403
1364
  `Protocol V2 local resource file does not match manifest: ${file.archive_path}`,
1404
1365
  { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1405
1366
  );
1406
1367
  }
1368
+ normalizedFiles.push({ ...file, sha256: digest });
1407
1369
  materializedEntries.push({ entryName: file.archive_path, binary: fileBinary });
1408
1370
  }
1371
+ manifestBinary ??= new TextEncoder().encode(JSON.stringify({ files: normalizedFiles })).buffer;
1372
+ materializedEntries.unshift({ entryName: 'manifest.json', binary: manifestBinary });
1409
1373
  return { binary, materializedEntries };
1410
1374
  }
1411
1375
 
@@ -1471,29 +1435,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1471
1435
  );
1472
1436
  }
1473
1437
 
1474
- // materializedEntries 由宿主生成,只有与获批 ZIP 的规范字节完全一致后才能使用。
1438
+ // The verified archive bytes are authoritative. Host materialization is an
1439
+ // implementation detail and may preserve wrapper paths or extra metadata files.
1475
1440
  const verifiedArchive = await this.prepareProtocolV2LocalResourceArchive(archiveBinary);
1476
- const entries = archiveArtifact.materializedEntries ?? [];
1477
- const entriesByName = new Map(entries.map(entry => [entry.entryName, entry] as const));
1478
- if (
1479
- entries.length !== verifiedArchive.materializedEntries.length ||
1480
- verifiedArchive.materializedEntries.some(entry => {
1481
- const preparedEntry = entriesByName.get(entry.entryName);
1482
- const digest = bytesToHex(sha256(new Uint8Array(entry.binary)));
1483
- return (
1484
- !preparedEntry ||
1485
- preparedEntry.artifact.size !== entry.binary.byteLength ||
1486
- preparedEntry.artifact.sha256.toLowerCase() !== digest
1487
- );
1488
- })
1489
- ) {
1490
- throw ERRORS.TypedError(
1491
- HardwareErrorCode.RuntimeError,
1492
- 'Protocol V2 prepared resource entries do not match the approved archive',
1493
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1494
- );
1495
- }
1496
-
1497
1441
  const verifiedEntriesByName = new Map(
1498
1442
  verifiedArchive.materializedEntries.map(entry => [entry.entryName, entry.binary] as const)
1499
1443
  );
@@ -1929,16 +1873,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1929
1873
  `Protocol V2 firmware fingerprint mismatch: ${key}/${component.target}`
1930
1874
  );
1931
1875
  }
1932
- const expectedPayloadHash = normalizeProtocolV2Hex(component.payloadHash);
1933
- if (expectedPayloadHash) {
1934
- const header = parseProtocolV2OkppHeader(toProtocolV2Bytes(binary));
1935
- if (!header || header.payloadHash !== expectedPayloadHash) {
1936
- throw ERRORS.TypedError(
1937
- HardwareErrorCode.RuntimeError,
1938
- `Protocol V2 firmware payload hash mismatch: ${key}/${component.target}`
1939
- );
1940
- }
1941
- }
1942
1876
  return {
1943
1877
  ...target,
1944
1878
  binary,
@@ -2168,11 +2102,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2168
2102
  return this.device.features?.mode === 'romloader';
2169
2103
  }
2170
2104
 
2171
- private isLegacyProtocolV2Runtime() {
2172
- const protocolInfo = this.device.state?.raw?.protocolV2ProtocolInfo;
2173
- return protocolInfo ? isLegacyProtocolV2ProtocolInfo(protocolInfo) : false;
2174
- }
2175
-
2176
2105
  private async rebootProtocolV2ToBootloader() {
2177
2106
  try {
2178
2107
  this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader);
@@ -2192,18 +2121,15 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2192
2121
  }
2193
2122
 
2194
2123
  async enterProtocolV2BootloaderMode() {
2195
- this.protocolV2LegacyDirectUpdate = false;
2196
2124
  // romloader is the first update environment and forwards targets to bootloader.
2197
2125
  // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
2198
2126
  if (this.isProtocolV2RomloaderMode()) {
2199
2127
  Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
2200
- this.protocolV2LegacyDirectUpdate = this.isLegacyProtocolV2Runtime();
2201
2128
  this.protocolV2ExecutionInLoader = true;
2202
2129
  return false;
2203
2130
  }
2204
2131
  if (this.isProtocolV2BootloaderMode()) {
2205
2132
  Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
2206
- this.protocolV2LegacyDirectUpdate = this.isLegacyProtocolV2Runtime();
2207
2133
  this.protocolV2ExecutionInLoader = true;
2208
2134
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
2209
2135
  return false;
@@ -2450,7 +2376,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2450
2376
  path: item.path,
2451
2377
  }));
2452
2378
  await this.protocolV2StartFirmwareUpdate({ targets });
2453
- await this.waitForProtocolV2FirmwareUpdateComplete(targets);
2379
+ await wait(PROTOCOL_V2_INSTALL_STATUS_INITIAL_DELAY);
2380
+ await this.waitForProtocolV2FirmwareUpdateComplete(targets, true);
2454
2381
  }
2455
2382
 
2456
2383
  private async protocolV2SourceUpdateProcess({
@@ -2702,7 +2629,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2702
2629
  }
2703
2630
 
2704
2631
  private async waitForProtocolV2FirmwareUpdateComplete(
2705
- targets: Array<{ target_id: number; path: string }>
2632
+ targets: Array<{ target_id: number; path: string }>,
2633
+ requireCurrentInstallStatus = false
2706
2634
  ) {
2707
2635
  this.protocolV2FinalStatusVerified = false;
2708
2636
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
@@ -2714,7 +2642,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2714
2642
  let missingTargetStatusSince: number | undefined;
2715
2643
  let missingTargetStatusKey: string | undefined;
2716
2644
  let normalModeWithoutInstallEvidenceSince: number | undefined;
2717
- let installEvidenceObserved = this.protocolV2InstallAckReceived;
2645
+ let installEvidenceObserved = false;
2646
+ let currentInstallStatusObserved = false;
2718
2647
  const resetMissingTargetStatusGrace = () => {
2719
2648
  missingTargetStatusSince = undefined;
2720
2649
  missingTargetStatusKey = undefined;
@@ -2734,7 +2663,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2734
2663
  try {
2735
2664
  const statusResponse = await this.device.getCommands().typedCall(
2736
2665
  'DeviceFirmwareUpdateStatusGet',
2737
- 'DeviceFirmwareUpdateStatus',
2666
+ ['DeviceFirmwareUpdateStatus', 'Success'],
2738
2667
  {
2739
2668
  fields: {
2740
2669
  status: true,
@@ -2744,22 +2673,64 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2744
2673
  },
2745
2674
  { timeoutMs: PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT }
2746
2675
  );
2747
- const statusTargets = (statusResponse.message.records ??
2748
- []) as ProtocolV2FirmwareUpdateStatusTarget[];
2676
+ if (statusResponse.type === 'Success') {
2677
+ installEvidenceObserved = true;
2678
+ resetMissingTargetStatusGrace();
2679
+ lastError = new Error(
2680
+ 'Protocol V2 firmware install acknowledged; waiting for target status'
2681
+ );
2682
+ }
2683
+ const statusTargets =
2684
+ statusResponse.type === 'DeviceFirmwareUpdateStatus'
2685
+ ? ((statusResponse.message.records ?? []) as ProtocolV2FirmwareUpdateStatusTarget[])
2686
+ : [];
2749
2687
  if (
2750
2688
  statusTargets.some(target => {
2751
2689
  const targetId = normalizeProtocolV2TargetId(target.target_id);
2752
- return targetId !== undefined && expectedTargetIds.has(targetId);
2690
+ return (
2691
+ targetId !== undefined &&
2692
+ expectedTargetIds.has(targetId) &&
2693
+ isProtocolV2TargetStatusInProgress(target.status)
2694
+ );
2753
2695
  })
2696
+ ) {
2697
+ currentInstallStatusObserved = true;
2698
+ }
2699
+ const hasMatchingTargetStatus = statusTargets.some(target => {
2700
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2701
+ return targetId !== undefined && expectedTargetIds.has(targetId);
2702
+ });
2703
+ if (
2704
+ hasMatchingTargetStatus &&
2705
+ (!requireCurrentInstallStatus || currentInstallStatusObserved)
2754
2706
  ) {
2755
2707
  installEvidenceObserved = true;
2756
2708
  normalModeWithoutInstallEvidenceSince = undefined;
2757
2709
  }
2758
- if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
2710
+ const matchingStatusTargets = statusTargets.filter(target => {
2711
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2712
+ return targetId !== undefined && expectedTargetIds.has(targetId);
2713
+ });
2714
+ const shouldVerifyTargetCompletion =
2715
+ !requireCurrentInstallStatus || currentInstallStatusObserved;
2716
+ if (
2717
+ shouldVerifyTargetCompletion &&
2718
+ this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)
2719
+ ) {
2759
2720
  this.protocolV2FinalStatusVerified = true;
2760
2721
  return;
2761
2722
  }
2762
2723
 
2724
+ if (
2725
+ requireCurrentInstallStatus &&
2726
+ !currentInstallStatusObserved &&
2727
+ matchingStatusTargets.length > 0
2728
+ ) {
2729
+ lastError = new Error(
2730
+ 'Protocol V2 firmware status is stale; waiting for the current install to start'
2731
+ );
2732
+ }
2733
+
2763
2734
  if (statusTargets.length === 0 && currentDeviceInfo) {
2764
2735
  const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2765
2736
  if (
@@ -2775,36 +2746,45 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2775
2746
  }
2776
2747
  }
2777
2748
 
2778
- const missingTargetIds = this.getProtocolV2MissingTargetIds(
2779
- statusTargets,
2780
- expectedTargetIds
2781
- );
2782
- if (missingTargetIds.length > 0) {
2783
- const now = Date.now();
2784
- const missingKey = missingTargetIds.join(',');
2785
- if (missingTargetStatusSince === undefined || missingTargetStatusKey !== missingKey) {
2786
- missingTargetStatusSince = now;
2787
- missingTargetStatusKey = missingKey;
2788
- } else if (
2789
- now - missingTargetStatusSince >=
2790
- PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT
2791
- ) {
2792
- const reportedTargetIds = statusTargets
2793
- .map(target => normalizeProtocolV2TargetId(target.target_id))
2794
- .filter((targetId): targetId is number => targetId !== undefined);
2795
- throw ERRORS.TypedError(
2796
- HardwareErrorCode.FirmwareError,
2797
- `Protocol V2 firmware status is missing requested records: targetIds=${missingKey} reportedTargetIds=${reportedTargetIds.join(
2798
- ','
2799
- )}`
2749
+ if (statusTargets.length === 0) {
2750
+ resetMissingTargetStatusGrace();
2751
+ if (statusResponse.type !== 'Success') {
2752
+ lastError = new Error(
2753
+ 'Protocol V2 firmware update is waiting for user confirmation or target status'
2800
2754
  );
2801
2755
  }
2802
- lastError = new Error(
2803
- `Protocol V2 firmware status is temporarily missing targetIds=${missingKey}`
2804
- );
2805
2756
  } else {
2806
- resetMissingTargetStatusGrace();
2807
- lastError = new Error('Protocol V2 firmware targets are still installing');
2757
+ const missingTargetIds = this.getProtocolV2MissingTargetIds(
2758
+ statusTargets,
2759
+ expectedTargetIds
2760
+ );
2761
+ if (missingTargetIds.length > 0) {
2762
+ const now = Date.now();
2763
+ const missingKey = missingTargetIds.join(',');
2764
+ if (missingTargetStatusSince === undefined || missingTargetStatusKey !== missingKey) {
2765
+ missingTargetStatusSince = now;
2766
+ missingTargetStatusKey = missingKey;
2767
+ } else if (
2768
+ now - missingTargetStatusSince >=
2769
+ PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT
2770
+ ) {
2771
+ const reportedTargetIds = statusTargets
2772
+ .map(target => normalizeProtocolV2TargetId(target.target_id))
2773
+ .filter((targetId): targetId is number => targetId !== undefined);
2774
+ throw ERRORS.TypedError(
2775
+ HardwareErrorCode.FirmwareError,
2776
+ `Protocol V2 firmware status is missing requested records: targetIds=${missingKey} reportedTargetIds=${reportedTargetIds.join(
2777
+ ','
2778
+ )}`
2779
+ );
2780
+ }
2781
+ lastError = new Error(
2782
+ `Protocol V2 firmware status is temporarily missing targetIds=${missingKey}`
2783
+ );
2784
+ } else {
2785
+ resetMissingTargetStatusGrace();
2786
+ lastError = new Error('Protocol V2 firmware targets are still installing');
2787
+ }
2808
2788
  }
2809
2789
  } catch (error) {
2810
2790
  if (isProtocolV2TerminalInstallStatusError(error)) {
@@ -3148,65 +3128,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3148
3128
  }: {
3149
3129
  targets: Array<{ target_id: number; path: string }>;
3150
3130
  }) {
3151
- this.protocolV2InstallAckReceived = false;
3152
3131
  this.protocolV2LastRuntimeProbeFeatures = undefined;
3153
- const startUpdate = () =>
3154
- this.device
3155
- .getCommands()
3156
- .typedCall(
3157
- 'DeviceFirmwareUpdateRequest',
3158
- 'Success',
3159
- { targets },
3160
- { timeoutMs: PROTOCOL_V2_START_UPDATE_TIMEOUT }
3161
- );
3162
- let response: ProtocolV2FirmwareUpdateStartResponse | undefined;
3163
- try {
3164
- response = await startUpdate();
3165
- this.protocolV2InstallAckReceived = true;
3166
- } catch (error) {
3167
- if (this.isBleReconnect() && isProtocolV2BleInstallInterruptionError(error)) {
3168
- this.throwIfAborted();
3169
- // Installation can reboot the device before the Success response reaches
3170
- // the host. The request has side effects, so do not replay it; reconnect
3171
- // and let status polling determine whether installation was accepted.
3172
- Log.log(
3173
- '[FirmwareUpdateV4] install request interrupted by device reboot; continue status polling: ',
3174
- error
3175
- );
3176
- } else if (
3177
- this.protocolV2LegacyDirectUpdate &&
3178
- isProtocolV2FirmwareUpdateEndpointUnavailable(error)
3179
- ) {
3180
- // Both legacy loaders register this request. A missing handler therefore identifies
3181
- // a legacy App before dispatch, so it is safe to reboot and retry the unhandled request.
3182
- this.protocolV2LegacyDirectUpdate = false;
3183
- Log.debug(
3184
- '[FirmwareUpdateV4] legacy App does not expose DeviceFirmwareUpdateRequest; rebooting to bootloader'
3185
- );
3186
- await this.rebootProtocolV2ToBootloader();
3187
- try {
3188
- response = await startUpdate();
3189
- this.protocolV2InstallAckReceived = true;
3190
- } catch (retryError) {
3191
- if (!(this.isBleReconnect() && isProtocolV2BleInstallInterruptionError(retryError))) {
3192
- throw retryError;
3193
- }
3194
- this.throwIfAborted();
3195
- Log.log(
3196
- '[FirmwareUpdateV4] install request interrupted after legacy reboot; continue status polling: ',
3197
- retryError
3198
- );
3199
- }
3200
- } else {
3201
- throw error;
3202
- }
3203
- }
3204
- this.protocolV2LegacyDirectUpdate = false;
3205
- // A Success ACK or an install-time disconnect both end confirmation. In the
3206
- // latter case only status polling may establish the final outcome.
3132
+ const commands = this.device.getCommands();
3133
+ await commands.typedCall('DeviceFirmwareUpdateStage', 'Success', { targets });
3134
+ await commands.call('DeviceFirmwareUpdateRequest', {}, { returnAfterWrite: true });
3207
3135
  this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdating);
3208
3136
  this.postProgressMessage(0, 'installingFirmware');
3209
- return response;
3210
3137
  }
3211
3138
 
3212
3139
  private async protocolV2Reboot(rebootType: DeviceRebootType) {
@@ -25,7 +25,7 @@ export default class DeviceFactoryInfoSet extends BaseMethod<DeviceFactoryInfoSe
25
25
  info: {
26
26
  version: this.params.version,
27
27
  serial_number: this.params.serial_number,
28
- burn_in_completed: this.params.burn_in_completed,
28
+ factory_burn_in_completed: this.params.burn_in_completed,
29
29
  factory_test_completed: this.params.factory_test_completed,
30
30
  manufacture_time: this.params.manufacture_time,
31
31
  },