@onekeyfe/hd-core 1.2.0-alpha.107 → 1.2.0-alpha.109

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 (74) hide show
  1. package/__tests__/DeviceCommands.test.ts +140 -26
  2. package/__tests__/base64Data.test.ts +70 -0
  3. package/__tests__/device-lifecycle-events.test.ts +113 -2
  4. package/__tests__/device-pool-state.test.ts +25 -0
  5. package/__tests__/device-state-mapper.test.ts +4 -4
  6. package/__tests__/device-utils.test.ts +1 -1
  7. package/__tests__/deviceSettings.test.ts +0 -1
  8. package/__tests__/deviceUploadNft.test.ts +33 -4
  9. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +125 -0
  10. package/__tests__/get-device-state.test.ts +60 -21
  11. package/__tests__/logBlockEvent.test.ts +13 -1
  12. package/__tests__/protocol-v2-resources.test.ts +8 -0
  13. package/__tests__/protocol-v2.test.ts +821 -227
  14. package/__tests__/refresh-device-state.test.ts +3 -1
  15. package/__tests__/resourceBase64Boundary.test.ts +48 -0
  16. package/__tests__/ton-sign-message.test.ts +84 -0
  17. package/dist/api/FirmwareUpdateV4.d.ts +7 -3
  18. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  19. package/dist/api/UploadPortfolio.d.ts +1 -1
  20. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  21. package/dist/api/helpers/base64Data.d.ts +16 -0
  22. package/dist/api/helpers/base64Data.d.ts.map +1 -0
  23. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
  24. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  25. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
  26. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  27. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
  28. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  29. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  30. package/dist/core/index.d.ts.map +1 -1
  31. package/dist/device/Device.d.ts +7 -2
  32. package/dist/device/Device.d.ts.map +1 -1
  33. package/dist/device/DeviceCommands.d.ts.map +1 -1
  34. package/dist/device/DevicePool.d.ts.map +1 -1
  35. package/dist/events/logBlockEvent.d.ts.map +1 -1
  36. package/dist/index.d.ts +16 -34
  37. package/dist/index.js +506 -329
  38. package/dist/protocols/protocol-v2/features.d.ts +13 -5
  39. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  40. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  41. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  42. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  43. package/dist/types/api/protocolV2.d.ts +1 -1
  44. package/dist/types/api/protocolV2.d.ts.map +1 -1
  45. package/dist/types/settings.d.ts +4 -19
  46. package/dist/types/settings.d.ts.map +1 -1
  47. package/dist/utils/patch.d.ts +1 -1
  48. package/dist/utils/patch.d.ts.map +1 -1
  49. package/dist/utils/pro2Nft.d.ts +7 -0
  50. package/dist/utils/pro2Nft.d.ts.map +1 -1
  51. package/package.json +6 -4
  52. package/src/api/FirmwareUpdateV4.ts +307 -245
  53. package/src/api/UploadPortfolio.ts +9 -2
  54. package/src/api/helpers/base64Data.ts +85 -0
  55. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
  56. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
  57. package/src/api/protocol-v2/DeviceUploadNft.ts +56 -8
  58. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +37 -18
  59. package/src/api/ton/TonSignMessage.ts +5 -3
  60. package/src/core/index.ts +6 -2
  61. package/src/data/messages/messages-protocol-v2.json +31 -14
  62. package/src/device/Device.ts +53 -31
  63. package/src/device/DeviceCommands.ts +4 -14
  64. package/src/device/DevicePool.ts +6 -2
  65. package/src/device/DeviceStateMapper.ts +15 -15
  66. package/src/deviceProfile/buildDeviceFeatures.ts +3 -3
  67. package/src/events/logBlockEvent.ts +14 -1
  68. package/src/protocols/protocol-v2/features.ts +22 -5
  69. package/src/protocols/protocol-v2/index.ts +2 -0
  70. package/src/protocols/protocol-v2/resources.ts +9 -46
  71. package/src/types/api/protocolV2.ts +1 -1
  72. package/src/types/settings.ts +4 -19
  73. package/src/utils/deviceSettings.ts +1 -1
  74. package/src/utils/pro2Nft.ts +31 -8
@@ -69,6 +69,7 @@ import type {
69
69
  Features,
70
70
  IFirmwareReleaseInfo,
71
71
  IProtocolV2FirmwareComponent,
72
+ IProtocolV2ResourceManifestFile,
72
73
  IVersionArray,
73
74
  } from '../types';
74
75
  import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
@@ -88,7 +89,6 @@ const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
88
89
  const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
89
90
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
90
91
  const PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT = 15 * 1000;
91
- const PROTOCOL_V2_START_UPDATE_TIMEOUT = 3 * 60 * 1000;
92
92
  const PROTOCOL_V2_INSTALL_TIMEOUT = 8 * 60 * 1000;
93
93
  const PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT = 30 * 1000;
94
94
  const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
@@ -112,6 +112,13 @@ const PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES = 1024 * 1024;
112
112
  const PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT = 512;
113
113
  const PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES = 256 * 1024 * 1024;
114
114
 
115
+ const getProtocolV2LocalResourceArchivePath = (entryName: string) => {
116
+ const match = entryName.match(
117
+ /(?:^|\/)((?:bundles\/|loaders\/(?:bootloader|rom)\/).+\.okpkg)$/iu
118
+ );
119
+ return match?.[1];
120
+ };
121
+
115
122
  const PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS = new Set<FirmwareUpdateV4Target>(['se03', 'se04']);
116
123
 
117
124
  const getProtocolV2ZipEntrySizes = (entry: JSZip.JSZipObject) => {
@@ -198,8 +205,6 @@ type ProtocolV2FirmwareUpdateStatusTarget = {
198
205
  path?: string;
199
206
  };
200
207
 
201
- type ProtocolV2FirmwareUpdateStartResponse = TypedResponseMessage<'Success'>;
202
-
203
208
  type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId: number };
204
209
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
205
210
  kind: ProtocolV2RemoteComponentTarget['kind'];
@@ -243,15 +248,7 @@ type JSZipSizedEntry = JSZip.JSZipObject & {
243
248
  };
244
249
  };
245
250
 
246
- type ProtocolV2ExecutionPhaseKind =
247
- | 'resource-sync'
248
- | 'bootloader-install'
249
- | 'bootloader-verify'
250
- | 'component-install'
251
- | 'final-verify';
252
-
253
- type ProtocolV2ExecutionPhase = {
254
- kind: ProtocolV2ExecutionPhaseKind;
251
+ type ProtocolV2TransferBatch = {
255
252
  installSources: ProtocolV2InstallSource[];
256
253
  resourceSources: ProtocolV2ResourceBundleSource[];
257
254
  };
@@ -569,7 +566,7 @@ export const assertProtocolV2ReconnectIdentity = (
569
566
  *
570
567
  * It intentionally does not fall back to FirmwareUpdateV3/V1 behavior:
571
568
  * - upload uses FilesystemFileWrite
572
- * - install uses DeviceFirmwareUpdateRequest
569
+ * - install uses DeviceFirmwareUpdateStage followed by an empty DeviceFirmwareUpdateRequest
573
570
  * - completion waits for target status to finish, reboots to normal, then polls DeviceInfo
574
571
  */
575
572
  export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareUpdateV4Params> {
@@ -595,6 +592,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
595
592
 
596
593
  private protocolV2FinalStatusVerified = false;
597
594
 
595
+ private protocolV2InstallBaselineVersions = new Map<number, string>();
596
+
597
+ private protocolV2LastRuntimeProbeFeatures?: Features;
598
+
598
599
  init() {
599
600
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
600
601
  this.requireDeviceMode = [];
@@ -813,6 +814,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
813
814
  private async runProtocolV2() {
814
815
  await this.captureProtocolV2PhysicalIdentity();
815
816
  const deviceFeatures = await this.getProtocolV2DeviceFeatures();
817
+ this.protocolV2InstallBaselineVersions =
818
+ this.getProtocolV2ObservableTargetVersions(deviceFeatures);
819
+ this.protocolV2LastRuntimeProbeFeatures = undefined;
816
820
  const currentDeviceType = this.device.getCurrentDeviceType();
817
821
  const capabilityDeviceType =
818
822
  currentDeviceType === EDeviceType.Pro2 || currentDeviceType === EDeviceType.Neo
@@ -826,6 +830,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
826
830
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
827
831
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
828
832
  this.validateExpectedTargetVersions();
833
+ const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
829
834
 
830
835
  if (
831
836
  !this.params.preparedPlan &&
@@ -849,11 +854,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
849
854
  if (this.params.preparedPlan || hasPreparedComponentArtifacts) {
850
855
  return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
851
856
  }
852
- const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
853
857
 
854
858
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
855
859
  let bootloaderBinary: ArrayBuffer | null = null;
856
860
  let installItems: ProtocolV2InstallItem[] | undefined;
861
+ let resourceMemoryHost: FirmwareUpdateV4MemoryHost | undefined;
857
862
  try {
858
863
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
859
864
  fwBinaryMap = this.collectExplicitTargetBinaries();
@@ -866,17 +871,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
866
871
  const needsRemoteFirmware = this.params.targetsToUpdate?.length
867
872
  ? missingFirmwareTargets.length > 0
868
873
  : explicitInstallItems.length === 0;
869
- if (wantsResources) {
870
- throw ERRORS.TypedError(
871
- HardwareErrorCode.RuntimeError,
872
- 'Protocol V2 resource archive must be provided through a local or external PreparedPlan',
873
- {
874
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
875
- }
876
- );
877
- }
874
+ const needsSdkManagedArtifacts = needsRemoteFirmware || wantsResources;
878
875
  if (
879
- needsRemoteFirmware &&
876
+ needsSdkManagedArtifacts &&
880
877
  (this.params.artifactReader ||
881
878
  DataManager.getSettings('firmwareManifestMode') === 'external-only')
882
879
  ) {
@@ -888,9 +885,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
888
885
  }
889
886
  );
890
887
  }
891
- if (needsRemoteFirmware) {
888
+ if (needsSdkManagedArtifacts) {
892
889
  // Remote updates must use a freshly fetched config before any reboot or file write.
893
- await DataManager.forceReloadData();
890
+ await DataManager.forceReloadData({
891
+ requireResources: wantsResources,
892
+ resourceDeviceType:
893
+ capabilityDeviceType === EDeviceType.Neo ? EDeviceType.Neo : EDeviceType.Pro2,
894
+ });
894
895
  }
895
896
  if (needsRemoteFirmware) {
896
897
  const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
@@ -913,8 +914,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
913
914
  targetId: item.targetId,
914
915
  }));
915
916
  }
917
+ if (wantsResources) {
918
+ this.params.resourceArchiveBinary = await this.downloadRemoteProtocolV2ResourceArchive(
919
+ deviceFeatures
920
+ );
921
+ resourceMemoryHost = await this.prepareProtocolV2LocalMemoryHost({
922
+ features: deviceFeatures,
923
+ firmwareType,
924
+ availableInstallItems: installItems ?? explicitInstallItems,
925
+ });
926
+ }
916
927
  this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
917
928
  } catch (err) {
929
+ resourceMemoryHost?.release();
918
930
  if (
919
931
  typeof err === 'object' &&
920
932
  err !== null &&
@@ -929,6 +941,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
929
941
  throw normalizeFirmwarePreparationError(err);
930
942
  }
931
943
 
944
+ if (resourceMemoryHost) {
945
+ try {
946
+ return await this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType, false);
947
+ } finally {
948
+ resourceMemoryHost.release();
949
+ }
950
+ }
951
+
932
952
  if (!bootloaderBinary && fwBinaryMap.length === 0 && !installItems?.length) {
933
953
  throw ERRORS.TypedError(
934
954
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
@@ -1082,14 +1102,15 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1082
1102
  private async prepareProtocolV2LocalMemoryHost({
1083
1103
  features,
1084
1104
  firmwareType,
1105
+ availableInstallItems = this.buildProtocolV2InstallItems({
1106
+ bootloaderBinary: this.prepareBootloaderBinary(),
1107
+ fwBinaryMap: this.collectExplicitTargetBinaries(),
1108
+ }),
1085
1109
  }: {
1086
1110
  features: Features;
1087
1111
  firmwareType: EFirmwareType;
1112
+ availableInstallItems?: ProtocolV2InstallItem[];
1088
1113
  }): Promise<FirmwareUpdateV4MemoryHost> {
1089
- const availableInstallItems = this.buildProtocolV2InstallItems({
1090
- bootloaderBinary: this.prepareBootloaderBinary(),
1091
- fwBinaryMap: this.collectExplicitTargetBinaries(),
1092
- });
1093
1114
  const requestedComponentTargets = new Set(
1094
1115
  (this.params.targetsToUpdate ?? []).filter(
1095
1116
  (target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
@@ -1238,92 +1259,86 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1238
1259
  );
1239
1260
  }
1240
1261
  const entries = zipEntries.filter(entry => !entry.dir);
1241
- if (entries.length === 0 || entries.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT + 1) {
1262
+ if (entries.length === 0) {
1242
1263
  throw ERRORS.TypedError(
1243
1264
  HardwareErrorCode.RuntimeError,
1244
1265
  'Protocol V2 local resource ZIP entry set is invalid',
1245
1266
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1246
1267
  );
1247
1268
  }
1248
- let declaredUncompressedSize = 0;
1249
- let declaredCompressedSize = 0;
1250
- for (const entry of entries) {
1251
- const sizes = getProtocolV2ZipEntrySizes(entry);
1252
- declaredCompressedSize += sizes.compressedSize;
1253
- declaredUncompressedSize += sizes.uncompressedSize;
1254
- const entryLimit =
1255
- entry.name === 'manifest.json'
1256
- ? PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1257
- : PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES;
1269
+ const manifestEntry = entries.find(entry => entry.name.split('/').pop() === 'manifest.json');
1270
+ let manifestBinary: ArrayBuffer | undefined;
1271
+ let manifestDirectory = '';
1272
+ let selectedFiles: IProtocolV2ResourceManifestFile[];
1273
+ if (manifestEntry) {
1258
1274
  if (
1259
- sizes.uncompressedSize > entryLimit ||
1260
- declaredCompressedSize > binary.byteLength ||
1261
- declaredUncompressedSize >
1262
- PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES + PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1275
+ getProtocolV2ZipEntrySizes(manifestEntry).uncompressedSize >
1276
+ PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1263
1277
  ) {
1264
1278
  throw ERRORS.TypedError(
1265
1279
  HardwareErrorCode.RuntimeError,
1266
- 'Protocol V2 local resource ZIP declared size exceeds the allowed limit',
1280
+ 'Protocol V2 local resource manifest size is invalid',
1267
1281
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1268
1282
  );
1269
1283
  }
1284
+ manifestBinary = await manifestEntry.async('arraybuffer');
1285
+ if (
1286
+ manifestBinary.byteLength <= 0 ||
1287
+ manifestBinary.byteLength > PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1288
+ ) {
1289
+ throw ERRORS.TypedError(
1290
+ HardwareErrorCode.RuntimeError,
1291
+ 'Protocol V2 local resource manifest size is invalid',
1292
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1293
+ );
1294
+ }
1295
+ let manifestValue: unknown;
1296
+ try {
1297
+ manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
1298
+ } catch (error) {
1299
+ throw ERRORS.TypedError(
1300
+ HardwareErrorCode.RuntimeError,
1301
+ `Protocol V2 local resource manifest is invalid: ${String(error)}`,
1302
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1303
+ );
1304
+ }
1305
+ selectedFiles = selectProtocolV2ResourceManifestFiles({
1306
+ manifest: parseProtocolV2ResourceManifest(manifestValue),
1307
+ targetsToUpdate: this.params.targetsToUpdate ?? [],
1308
+ });
1309
+ manifestDirectory = manifestEntry.name.slice(0, -'manifest.json'.length);
1310
+ } else {
1311
+ selectedFiles = entries.flatMap(entry => {
1312
+ const archivePath = getProtocolV2LocalResourceArchivePath(entry.name);
1313
+ if (!archivePath) return [];
1314
+ return [
1315
+ {
1316
+ archive_path: archivePath,
1317
+ original_name: archivePath.split('/').pop() ?? archivePath,
1318
+ device_path: `vol0:/${archivePath}`,
1319
+ size: getProtocolV2ZipEntrySizes(entry).uncompressedSize,
1320
+ sha256: '',
1321
+ },
1322
+ ];
1323
+ });
1270
1324
  }
1271
- const manifestEntry = zip.file('manifest.json');
1272
- if (!manifestEntry) {
1273
- throw ERRORS.TypedError(
1274
- HardwareErrorCode.RuntimeError,
1275
- 'Protocol V2 local resource ZIP has no manifest.json',
1276
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1277
- );
1278
- }
1279
- const manifestBinary = await manifestEntry.async('arraybuffer');
1280
- if (
1281
- manifestBinary.byteLength <= 0 ||
1282
- manifestBinary.byteLength > PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1283
- ) {
1284
- throw ERRORS.TypedError(
1285
- HardwareErrorCode.RuntimeError,
1286
- 'Protocol V2 local resource manifest size is invalid',
1287
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1288
- );
1289
- }
1290
-
1291
- let manifestValue: unknown;
1292
- try {
1293
- manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
1294
- } catch (error) {
1295
- throw ERRORS.TypedError(
1296
- HardwareErrorCode.RuntimeError,
1297
- `Protocol V2 local resource manifest is invalid: ${String(error)}`,
1298
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1299
- );
1300
- }
1301
- const manifest = parseProtocolV2ResourceManifest(manifestValue);
1302
- const selectedFiles = selectProtocolV2ResourceManifestFiles({
1303
- manifest,
1304
- targetsToUpdate: this.params.targetsToUpdate ?? [],
1305
- });
1306
- const expectedEntryNames = new Set([
1307
- 'manifest.json',
1308
- ...selectedFiles.map(file => file.archive_path),
1309
- ]);
1310
- if (
1311
- entries.length !== expectedEntryNames.size ||
1312
- entries.some(entry => !expectedEntryNames.has(entry.name))
1313
- ) {
1325
+ if (selectedFiles.length === 0 || selectedFiles.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT) {
1314
1326
  throw ERRORS.TypedError(
1315
1327
  HardwareErrorCode.RuntimeError,
1316
- 'Protocol V2 local resource ZIP contains an unexpected entry',
1328
+ 'Protocol V2 local resource ZIP has no resource packages',
1317
1329
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1318
1330
  );
1319
1331
  }
1320
1332
 
1321
1333
  let totalSize = 0;
1322
- const materializedEntries: FirmwareMemoryArtifactEntry[] = [
1323
- { entryName: 'manifest.json', binary: manifestBinary },
1324
- ];
1334
+ const materializedEntries: FirmwareMemoryArtifactEntry[] = [];
1335
+ const normalizedFiles: IProtocolV2ResourceManifestFile[] = [];
1325
1336
  for (const file of selectedFiles) {
1326
- const entry = zip.file(file.archive_path);
1337
+ const entry = manifestEntry
1338
+ ? zip.file(`${manifestDirectory}${file.archive_path}`)
1339
+ : entries.find(
1340
+ candidate => getProtocolV2LocalResourceArchivePath(candidate.name) === file.archive_path
1341
+ );
1327
1342
  if (!entry) {
1328
1343
  throw ERRORS.TypedError(
1329
1344
  HardwareErrorCode.RuntimeError,
@@ -1342,15 +1357,21 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1342
1357
  }
1343
1358
  const fileBinary = await entry.async('arraybuffer');
1344
1359
  const digest = bytesToHex(sha256(new Uint8Array(fileBinary)));
1345
- if (fileBinary.byteLength !== file.size || digest !== file.sha256.toLowerCase()) {
1360
+ if (
1361
+ fileBinary.byteLength !== file.size ||
1362
+ (file.sha256 && digest !== file.sha256.toLowerCase())
1363
+ ) {
1346
1364
  throw ERRORS.TypedError(
1347
1365
  HardwareErrorCode.RuntimeError,
1348
1366
  `Protocol V2 local resource file does not match manifest: ${file.archive_path}`,
1349
1367
  { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1350
1368
  );
1351
1369
  }
1370
+ normalizedFiles.push({ ...file, sha256: digest });
1352
1371
  materializedEntries.push({ entryName: file.archive_path, binary: fileBinary });
1353
1372
  }
1373
+ manifestBinary ??= new TextEncoder().encode(JSON.stringify({ files: normalizedFiles })).buffer;
1374
+ materializedEntries.unshift({ entryName: 'manifest.json', binary: manifestBinary });
1354
1375
  return { binary, materializedEntries };
1355
1376
  }
1356
1377
 
@@ -1517,9 +1538,15 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1517
1538
  return sources;
1518
1539
  }
1519
1540
 
1520
- private async runProtocolV2PreparedArtifacts(features: Features, firmwareType: EFirmwareType) {
1541
+ private async runProtocolV2PreparedArtifacts(
1542
+ features: Features,
1543
+ firmwareType: EFirmwareType,
1544
+ announceDownload = true
1545
+ ) {
1521
1546
  try {
1522
- this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1547
+ if (announceDownload) {
1548
+ this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1549
+ }
1523
1550
  const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
1524
1551
  const resourceSources = await this.prepareProtocolV2ResourceSources();
1525
1552
  if (installSources.length === 0 && resourceSources.length === 0) {
@@ -1528,7 +1555,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1528
1555
  'No firmware to update'
1529
1556
  );
1530
1557
  }
1531
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1558
+ if (announceDownload) {
1559
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1560
+ }
1532
1561
 
1533
1562
  return await this.executeProtocolV2SourceUpdate({
1534
1563
  installSources,
@@ -1882,6 +1911,44 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1882
1911
  };
1883
1912
  }
1884
1913
 
1914
+ private async downloadRemoteProtocolV2ResourceArchive(features: Features): Promise<ArrayBuffer> {
1915
+ const deviceType = getDeviceType(features);
1916
+ if (deviceType !== EDeviceType.Pro2 && deviceType !== EDeviceType.Neo) {
1917
+ throw ERRORS.TypedError(
1918
+ HardwareErrorCode.RuntimeError,
1919
+ 'Protocol V2 resource archive requires a Pro2 or Neo device'
1920
+ );
1921
+ }
1922
+ const source = DataManager.getProtocolV2ResourceSource(deviceType);
1923
+ const expectedSha256 = normalizeProtocolV2Hex(source?.archiveSha256);
1924
+ if (
1925
+ !source?.archiveUrl ||
1926
+ !Number.isSafeInteger(source.archiveSize) ||
1927
+ source.archiveSize <= 0 ||
1928
+ source.archiveSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES ||
1929
+ !expectedSha256 ||
1930
+ !/^[0-9a-f]{64}$/u.test(expectedSha256)
1931
+ ) {
1932
+ throw ERRORS.TypedError(
1933
+ HardwareErrorCode.RuntimeError,
1934
+ 'Protocol V2 resource archive integrity metadata is invalid',
1935
+ { firmwareUpdateCode: 'FirmwarePlanInvalid' }
1936
+ );
1937
+ }
1938
+ const { binary } = await getSysResourceBinary(source.archiveUrl);
1939
+ if (
1940
+ binary.byteLength !== source.archiveSize ||
1941
+ bytesToHex(sha256(new Uint8Array(binary))) !== expectedSha256
1942
+ ) {
1943
+ throw ERRORS.TypedError(
1944
+ HardwareErrorCode.RuntimeError,
1945
+ 'Protocol V2 resource archive does not match the remote config',
1946
+ { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1947
+ );
1948
+ }
1949
+ return binary;
1950
+ }
1951
+
1885
1952
  private async prepareRemoteProtocolV2Binaries(
1886
1953
  firmwareType: EFirmwareType,
1887
1954
  features: Features,
@@ -2067,21 +2134,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2067
2134
  return this.device.features?.mode === 'romloader';
2068
2135
  }
2069
2136
 
2070
- async enterProtocolV2BootloaderMode() {
2071
- // romloader is the first update environment and forwards targets to bootloader.
2072
- // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
2073
- if (this.isProtocolV2RomloaderMode()) {
2074
- Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
2075
- this.protocolV2ExecutionInLoader = true;
2076
- return false;
2077
- }
2078
- if (this.isProtocolV2BootloaderMode()) {
2079
- Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
2080
- this.protocolV2ExecutionInLoader = true;
2081
- this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
2082
- return false;
2083
- }
2084
-
2137
+ private async rebootProtocolV2ToBootloader() {
2085
2138
  try {
2086
2139
  this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader);
2087
2140
  await this.protocolV2Reboot(DeviceRebootType.Bootloader);
@@ -2099,6 +2152,24 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2099
2152
  }
2100
2153
  }
2101
2154
 
2155
+ async enterProtocolV2BootloaderMode() {
2156
+ // romloader is the first update environment and forwards targets to bootloader.
2157
+ // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
2158
+ if (this.isProtocolV2RomloaderMode()) {
2159
+ Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
2160
+ this.protocolV2ExecutionInLoader = true;
2161
+ return false;
2162
+ }
2163
+ if (this.isProtocolV2BootloaderMode()) {
2164
+ Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
2165
+ this.protocolV2ExecutionInLoader = true;
2166
+ this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
2167
+ return false;
2168
+ }
2169
+
2170
+ return this.rebootProtocolV2ToBootloader();
2171
+ }
2172
+
2102
2173
  private async waitForProtocolV2BootloaderMode(
2103
2174
  timeout = PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT,
2104
2175
  retryInterval = 1000
@@ -2183,68 +2254,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2183
2254
  return entries;
2184
2255
  }
2185
2256
 
2186
- private buildProtocolV2ExecutionPhases({
2187
- installSources,
2188
- resourceSources,
2189
- }: {
2190
- installSources: ProtocolV2InstallSource[];
2191
- resourceSources: ProtocolV2ResourceBundleSource[];
2192
- }): ProtocolV2ExecutionPhase[] {
2193
- const phases: ProtocolV2ExecutionPhase[] = [];
2194
- const bootResourceSources = resourceSources.filter(source =>
2195
- isProtocolV2BootResourcePackagePath(source.devicePath)
2196
- );
2197
- const remainingResourceSources = resourceSources.filter(
2198
- source => !isProtocolV2BootResourcePackagePath(source.devicePath)
2199
- );
2200
- // A previous interrupted run may have left a promotable staging file. Replace
2201
- // it with the current approved package before any firmware-triggered reboot.
2202
- if (bootResourceSources.length > 0) {
2203
- phases.push({
2204
- kind: 'resource-sync',
2205
- installSources: [],
2206
- resourceSources: bootResourceSources,
2207
- });
2208
- }
2209
- const bootloaderSources = installSources.filter(source => source.kind === 'bootloader');
2210
- if (bootloaderSources.length > 0) {
2211
- phases.push(
2212
- {
2213
- kind: 'bootloader-install',
2214
- installSources: bootloaderSources,
2215
- resourceSources: [],
2216
- },
2217
- {
2218
- kind: 'bootloader-verify',
2219
- installSources: [],
2220
- resourceSources: [],
2221
- }
2222
- );
2223
- }
2224
- if (remainingResourceSources.length > 0) {
2225
- phases.push({
2226
- kind: 'resource-sync',
2227
- installSources: [],
2228
- resourceSources: remainingResourceSources,
2229
- });
2230
- }
2231
- const componentSources = installSources.filter(source => source.kind !== 'bootloader');
2232
- if (componentSources.length > 0) {
2233
- phases.push({
2234
- kind: 'component-install',
2235
- installSources: componentSources,
2236
- resourceSources: [],
2237
- });
2238
- }
2239
- phases.push({
2240
- kind: 'final-verify',
2241
- installSources: [],
2242
- resourceSources: [],
2243
- });
2244
- return phases;
2245
- }
2246
-
2247
- private async executeProtocolV2Phases({
2257
+ private async executeProtocolV2SourceUpdate({
2248
2258
  installSources,
2249
2259
  resourceSources,
2250
2260
  }: {
@@ -2252,38 +2262,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2252
2262
  resourceSources: ProtocolV2ResourceBundleSource[];
2253
2263
  }) {
2254
2264
  this.protocolV2BootResourceStagingSafe = false;
2255
- const phases = this.buildProtocolV2ExecutionPhases({
2256
- installSources,
2257
- resourceSources,
2258
- });
2259
- for (const phase of phases) {
2260
- if (phase.kind === 'final-verify') {
2261
- return this.completeProtocolV2FinalVerification();
2262
- }
2263
- if (phase.kind === 'resource-sync') {
2264
- await this.enterProtocolV2BootloaderMode();
2265
- if (
2266
- !phase.resourceSources.some(source =>
2267
- isProtocolV2BootResourcePackagePath(source.devicePath)
2268
- )
2269
- ) {
2270
- await this.ensureProtocolV2BootResourceStagingIsEmpty();
2271
- }
2272
- await this.executeProtocolV2TransferPhase(phase);
2273
- } else if (phase.kind === 'bootloader-install' || phase.kind === 'component-install') {
2274
- await this.enterProtocolV2BootloaderMode();
2275
- await this.ensureProtocolV2BootResourceStagingIsEmpty();
2276
- await this.executeProtocolV2TransferPhase(phase);
2277
- await this.exitProtocolV2BootloaderToNormal();
2278
- } else if (phase.kind === 'bootloader-verify') {
2279
- await this.waitForProtocolV2FinalFeatures();
2280
- this.assertExpectedProtocolV2Versions(['boot']);
2281
- }
2265
+ if (installSources.length > 0 || resourceSources.length > 0) {
2266
+ await this.enterProtocolV2BootloaderMode();
2267
+ // Clear stale boot-resource staging before writing any artifacts. The new
2268
+ // boot resource, resources and firmware then share one transfer session,
2269
+ // one global progress range and one multi-target install request.
2270
+ // Preserve the approved source order instead of synthesizing a boot-first
2271
+ // phase. Loader firmware owns the cross-stage handoff: bootloader runs its
2272
+ // component targets and leaves boot pending for romloader, while pending
2273
+ // runner-backed records always resume before the application boots.
2274
+ await this.ensureProtocolV2BootResourceStagingIsEmpty();
2275
+ await this.executeProtocolV2TransferPhase({
2276
+ installSources,
2277
+ resourceSources,
2278
+ });
2282
2279
  }
2283
- throw ERRORS.TypedError(
2284
- HardwareErrorCode.RuntimeError,
2285
- 'Protocol V2 execution has no final verification phase'
2286
- );
2280
+ return this.completeProtocolV2FinalVerification();
2287
2281
  }
2288
2282
 
2289
2283
  private async ensureProtocolV2BootResourceStagingIsEmpty() {
@@ -2315,19 +2309,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2315
2309
  this.protocolV2BootResourceStagingSafe = true;
2316
2310
  }
2317
2311
 
2318
- private async executeProtocolV2SourceUpdate({
2319
- installSources,
2320
- resourceSources,
2321
- }: {
2322
- installSources: ProtocolV2InstallSource[];
2323
- resourceSources: ProtocolV2ResourceBundleSource[];
2324
- }) {
2325
- return this.executeProtocolV2Phases({
2326
- installSources,
2327
- resourceSources,
2328
- });
2329
- }
2330
-
2331
2312
  private async executeProtocolV2Update({
2332
2313
  fwBinaryMap,
2333
2314
  bootloaderBinary,
@@ -2352,7 +2333,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2352
2333
  kind: item.kind,
2353
2334
  }))
2354
2335
  );
2355
- return await this.executeProtocolV2Phases({
2336
+ return await this.executeProtocolV2SourceUpdate({
2356
2337
  installSources,
2357
2338
  resourceSources: [],
2358
2339
  });
@@ -2364,7 +2345,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2364
2345
  private async executeProtocolV2TransferPhase({
2365
2346
  installSources,
2366
2347
  resourceSources,
2367
- }: ProtocolV2ExecutionPhase) {
2348
+ }: ProtocolV2TransferBatch) {
2368
2349
  let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
2369
2350
  const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
2370
2351
  for (const resource of resourceSources) {
@@ -2637,6 +2618,47 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2637
2618
  return Array.from(expectedTargetIds).filter(targetId => !reportedTargetIds.has(targetId));
2638
2619
  }
2639
2620
 
2621
+ private getProtocolV2ObservableTargetVersions(features: Features) {
2622
+ const versions = new Map<number, string>();
2623
+ versions.set(
2624
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER,
2625
+ getDeviceBootloaderVersion(features).join('.')
2626
+ );
2627
+ const applicationVersion = getDeviceFirmwareVersion(features).join('.');
2628
+ versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, applicationVersion);
2629
+ versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, applicationVersion);
2630
+ versions.set(
2631
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR,
2632
+ getDeviceBLEFirmwareVersion(features).join('.')
2633
+ );
2634
+ const secureElementVersions: Array<[number, string | null | undefined]> = [
2635
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01, features.se01Version],
2636
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02, features.se02Version],
2637
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03, features.se03Version],
2638
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04, features.se04Version],
2639
+ ];
2640
+ secureElementVersions.forEach(([targetId, version]) => {
2641
+ if (version) versions.set(targetId, version);
2642
+ });
2643
+ return versions;
2644
+ }
2645
+
2646
+ private hasProtocolV2InstallVersionChanged(expectedTargetIds: Set<number>) {
2647
+ if (!this.protocolV2LastRuntimeProbeFeatures) return false;
2648
+ const currentVersions = this.getProtocolV2ObservableTargetVersions(
2649
+ this.protocolV2LastRuntimeProbeFeatures
2650
+ );
2651
+ return Array.from(expectedTargetIds).some(targetId => {
2652
+ const previousVersion = this.protocolV2InstallBaselineVersions.get(targetId);
2653
+ const currentVersion = currentVersions.get(targetId);
2654
+ return (
2655
+ previousVersion !== undefined &&
2656
+ currentVersion !== undefined &&
2657
+ previousVersion !== currentVersion
2658
+ );
2659
+ });
2660
+ }
2661
+
2640
2662
  private async waitForProtocolV2FirmwareUpdateComplete(
2641
2663
  targets: Array<{ target_id: number; path: string }>
2642
2664
  ) {
@@ -2649,12 +2671,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2649
2671
  let deviceInfo: ProtocolV2DeviceInfo | undefined;
2650
2672
  let missingTargetStatusSince: number | undefined;
2651
2673
  let missingTargetStatusKey: string | undefined;
2674
+ let normalModeWithoutInstallEvidenceSince: number | undefined;
2675
+ let installEvidenceObserved = false;
2652
2676
  const resetMissingTargetStatusGrace = () => {
2653
2677
  missingTargetStatusSince = undefined;
2654
2678
  missingTargetStatusKey = undefined;
2655
2679
  };
2656
2680
 
2657
2681
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
2682
+ // A transport release caused by an explicit workflow cancellation must not
2683
+ // be mistaken for the expected device reboot during installation.
2684
+ this.throwIfAborted();
2658
2685
  try {
2659
2686
  if (shouldReconnect) {
2660
2687
  await this.reconnectProtocolV2Device();
@@ -2665,7 +2692,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2665
2692
  try {
2666
2693
  const statusResponse = await this.device.getCommands().typedCall(
2667
2694
  'DeviceFirmwareUpdateStatusGet',
2668
- 'DeviceFirmwareUpdateStatus',
2695
+ ['DeviceFirmwareUpdateStatus', 'Success'],
2669
2696
  {
2670
2697
  fields: {
2671
2698
  status: true,
@@ -2675,23 +2702,40 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2675
2702
  },
2676
2703
  { timeoutMs: PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT }
2677
2704
  );
2705
+ if (statusResponse.type === 'Success') {
2706
+ this.protocolV2FinalStatusVerified = true;
2707
+ this.postProgressMessage(100, 'installingFirmware');
2708
+ return;
2709
+ }
2678
2710
  const statusTargets = (statusResponse.message.records ??
2679
2711
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
2712
+ if (
2713
+ statusTargets.some(target => {
2714
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2715
+ return targetId !== undefined && expectedTargetIds.has(targetId);
2716
+ })
2717
+ ) {
2718
+ installEvidenceObserved = true;
2719
+ normalModeWithoutInstallEvidenceSince = undefined;
2720
+ }
2680
2721
  if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
2681
2722
  this.protocolV2FinalStatusVerified = true;
2682
2723
  return;
2683
2724
  }
2684
2725
 
2685
- if (
2686
- statusTargets.length === 0 &&
2687
- currentDeviceInfo &&
2688
- (await this.probeProtocolV2NormalMode(currentDeviceInfo))
2689
- ) {
2690
- Log.log(
2691
- '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
2692
- );
2693
- this.postProgressMessage(100, 'installingFirmware');
2694
- return;
2726
+ if (statusTargets.length === 0 && currentDeviceInfo) {
2727
+ const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2728
+ if (
2729
+ isNormalMode &&
2730
+ (installEvidenceObserved ||
2731
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds))
2732
+ ) {
2733
+ Log.log(
2734
+ '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
2735
+ );
2736
+ this.postProgressMessage(100, 'installingFirmware');
2737
+ return;
2738
+ }
2695
2739
  }
2696
2740
 
2697
2741
  const missingTargetIds = this.getProtocolV2MissingTargetIds(
@@ -2741,16 +2785,39 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2741
2785
  'Protocol V2 device identity is unavailable during install polling'
2742
2786
  );
2743
2787
  }
2744
- if (await this.probeProtocolV2NormalMode(currentDeviceInfo)) {
2788
+ const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2789
+ if (
2790
+ isNormalMode &&
2791
+ (installEvidenceObserved ||
2792
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds))
2793
+ ) {
2745
2794
  Log.log(
2746
2795
  '[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot'
2747
2796
  );
2748
2797
  this.postProgressMessage(100, 'installingFirmware');
2749
2798
  return;
2750
2799
  }
2751
- lastError = new Error(
2752
- 'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
2753
- );
2800
+ if (isNormalMode) {
2801
+ const now = Date.now();
2802
+ normalModeWithoutInstallEvidenceSince ??= now;
2803
+ if (
2804
+ now - normalModeWithoutInstallEvidenceSince >=
2805
+ PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT
2806
+ ) {
2807
+ throw ERRORS.TypedError(
2808
+ HardwareErrorCode.FirmwareError,
2809
+ 'Protocol V2 device returned to normal mode without install ACK, target status, or version change'
2810
+ );
2811
+ }
2812
+ lastError = new Error(
2813
+ 'Protocol V2 device is in normal mode but installation is not yet confirmed'
2814
+ );
2815
+ } else {
2816
+ normalModeWithoutInstallEvidenceSince = undefined;
2817
+ lastError = new Error(
2818
+ 'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
2819
+ );
2820
+ }
2754
2821
  } else {
2755
2822
  shouldReconnect = true;
2756
2823
  deviceInfo = undefined;
@@ -2811,6 +2878,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2811
2878
  deviceInfo,
2812
2879
  PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2813
2880
  );
2881
+ this.protocolV2LastRuntimeProbeFeatures = features;
2814
2882
  return features.mode === 'normal' && !features.bootloaderMode;
2815
2883
  }
2816
2884
 
@@ -3043,18 +3111,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3043
3111
  }: {
3044
3112
  targets: Array<{ target_id: number; path: string }>;
3045
3113
  }) {
3114
+ this.protocolV2LastRuntimeProbeFeatures = undefined;
3046
3115
  const commands = this.device.getCommands();
3047
- const response: ProtocolV2FirmwareUpdateStartResponse = await commands.typedCall(
3048
- 'DeviceFirmwareUpdateRequest',
3049
- 'Success',
3050
- { targets },
3051
- { timeoutMs: PROTOCOL_V2_START_UPDATE_TIMEOUT }
3052
- );
3053
- // Success acknowledges that the device accepted installation. End confirmation
3054
- // and begin status polling only after receiving this ACK.
3116
+ await commands.typedCall('DeviceFirmwareUpdateStage', 'Success', { targets });
3117
+ await commands.call('DeviceFirmwareUpdateRequest', {}, { returnAfterWrite: true });
3055
3118
  this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdating);
3056
3119
  this.postProgressMessage(0, 'installingFirmware');
3057
- return response;
3058
3120
  }
3059
3121
 
3060
3122
  private async protocolV2Reboot(rebootType: DeviceRebootType) {