@onekeyfe/hd-core 1.2.0-alpha.105 → 1.2.0-alpha.106

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 (44) hide show
  1. package/__tests__/DeviceCommands.test.ts +90 -26
  2. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +0 -2
  3. package/__tests__/device-pool-state.test.ts +1 -5
  4. package/__tests__/deviceSettings.test.ts +0 -1
  5. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +145 -0
  6. package/__tests__/logBlockEvent.test.ts +13 -1
  7. package/__tests__/protocol-v2.test.ts +447 -122
  8. package/__tests__/refresh-device-state.test.ts +0 -1
  9. package/__tests__/search-devices.test.ts +0 -2
  10. package/dist/api/FirmwareUpdateV4.d.ts +7 -3
  11. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  12. package/dist/api/GetDeviceState.d.ts.map +1 -1
  13. package/dist/api/SearchDevices.d.ts.map +1 -1
  14. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
  15. package/dist/core/index.d.ts.map +1 -1
  16. package/dist/device/Device.d.ts +0 -3
  17. package/dist/device/Device.d.ts.map +1 -1
  18. package/dist/device/DeviceCommands.d.ts.map +1 -1
  19. package/dist/device/DevicePool.d.ts +1 -1
  20. package/dist/device/DevicePool.d.ts.map +1 -1
  21. package/dist/events/logBlockEvent.d.ts.map +1 -1
  22. package/dist/index.d.ts +0 -6
  23. package/dist/index.js +251 -184
  24. package/dist/protocols/protocol-v2/features.d.ts +1 -1
  25. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  26. package/dist/types/api/getDeviceState.d.ts +0 -1
  27. package/dist/types/api/getDeviceState.d.ts.map +1 -1
  28. package/package.json +4 -4
  29. package/src/api/FirmwareUpdateV4.ts +262 -172
  30. package/src/api/GetDeviceState.ts +0 -1
  31. package/src/api/SearchDevices.ts +0 -2
  32. package/src/api/firmware/protocolV2Release.ts +0 -1
  33. package/src/core/index.ts +6 -6
  34. package/src/device/Device.ts +11 -48
  35. package/src/device/DeviceCommands.ts +4 -14
  36. package/src/device/DevicePool.ts +3 -11
  37. package/src/events/logBlockEvent.ts +14 -1
  38. package/src/protocols/protocol-v2/features.ts +9 -2
  39. package/src/types/api/getDeviceState.ts +0 -2
  40. package/src/utils/deviceSettings.ts +1 -1
  41. package/__tests__/protocol-v2-legacy-recovery.test.ts +0 -29
  42. package/dist/core/protocolV2LegacyRecovery.d.ts +0 -5
  43. package/dist/core/protocolV2LegacyRecovery.d.ts.map +0 -1
  44. package/src/core/protocolV2LegacyRecovery.ts +0 -12
@@ -244,15 +244,7 @@ type JSZipSizedEntry = JSZip.JSZipObject & {
244
244
  };
245
245
  };
246
246
 
247
- type ProtocolV2ExecutionPhaseKind =
248
- | 'resource-sync'
249
- | 'bootloader-install'
250
- | 'bootloader-verify'
251
- | 'component-install'
252
- | 'final-verify';
253
-
254
- type ProtocolV2ExecutionPhase = {
255
- kind: ProtocolV2ExecutionPhaseKind;
247
+ type ProtocolV2TransferBatch = {
256
248
  installSources: ProtocolV2InstallSource[];
257
249
  resourceSources: ProtocolV2ResourceBundleSource[];
258
250
  };
@@ -385,6 +377,19 @@ const isProtocolV2ReconnectProbeError = (error: unknown) => {
385
377
  );
386
378
  };
387
379
 
380
+ const isProtocolV2BleTransportReleasedError = (error: unknown) => {
381
+ const message = getProtocolV2UnknownErrorText(error).toLowerCase();
382
+ const compactMessage = message.replace(/\s+/gu, '');
383
+ return (
384
+ message.includes('react native ble transport released') ||
385
+ message.includes('bledevicedisconnected') ||
386
+ message.includes('bleconnectederror') ||
387
+ (compactMessage.includes('rxerrorerror6') &&
388
+ (compactMessage.includes('multiplatformbleadapter') ||
389
+ compactMessage.includes('multipalformebleadapter')))
390
+ );
391
+ };
392
+
388
393
  const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
389
394
  const message = getProtocolV2UnknownErrorText(error).toLowerCase();
390
395
  return (
@@ -605,6 +610,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
605
610
 
606
611
  private protocolV2FinalStatusVerified = false;
607
612
 
613
+ private protocolV2InstallAckReceived = false;
614
+
615
+ private protocolV2InstallBaselineVersions = new Map<number, string>();
616
+
617
+ private protocolV2LastRuntimeProbeFeatures?: Features;
618
+
608
619
  init() {
609
620
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
610
621
  this.requireDeviceMode = [];
@@ -823,6 +834,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
823
834
  private async runProtocolV2() {
824
835
  await this.captureProtocolV2PhysicalIdentity();
825
836
  const deviceFeatures = await this.getProtocolV2DeviceFeatures();
837
+ this.protocolV2InstallBaselineVersions =
838
+ this.getProtocolV2ObservableTargetVersions(deviceFeatures);
839
+ this.protocolV2LastRuntimeProbeFeatures = undefined;
826
840
  const currentDeviceType = this.device.getCurrentDeviceType();
827
841
  const capabilityDeviceType =
828
842
  currentDeviceType === EDeviceType.Pro2 || currentDeviceType === EDeviceType.Neo
@@ -836,6 +850,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
836
850
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
837
851
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
838
852
  this.validateExpectedTargetVersions();
853
+ const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
839
854
 
840
855
  if (
841
856
  !this.params.preparedPlan &&
@@ -859,11 +874,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
859
874
  if (this.params.preparedPlan || hasPreparedComponentArtifacts) {
860
875
  return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
861
876
  }
862
- const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
863
877
 
864
878
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
865
879
  let bootloaderBinary: ArrayBuffer | null = null;
866
880
  let installItems: ProtocolV2InstallItem[] | undefined;
881
+ let resourceMemoryHost: FirmwareUpdateV4MemoryHost | undefined;
867
882
  try {
868
883
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
869
884
  fwBinaryMap = this.collectExplicitTargetBinaries();
@@ -876,17 +891,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
876
891
  const needsRemoteFirmware = this.params.targetsToUpdate?.length
877
892
  ? missingFirmwareTargets.length > 0
878
893
  : explicitInstallItems.length === 0;
879
- if (wantsResources) {
880
- throw ERRORS.TypedError(
881
- HardwareErrorCode.RuntimeError,
882
- 'Protocol V2 resource archive must be provided through a local or external PreparedPlan',
883
- {
884
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
885
- }
886
- );
887
- }
894
+ const needsSdkManagedArtifacts = needsRemoteFirmware || wantsResources;
888
895
  if (
889
- needsRemoteFirmware &&
896
+ needsSdkManagedArtifacts &&
890
897
  (this.params.artifactReader ||
891
898
  DataManager.getSettings('firmwareManifestMode') === 'external-only')
892
899
  ) {
@@ -898,9 +905,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
898
905
  }
899
906
  );
900
907
  }
901
- if (needsRemoteFirmware) {
908
+ if (needsSdkManagedArtifacts) {
902
909
  // Remote updates must use a freshly fetched config before any reboot or file write.
903
- await DataManager.forceReloadData();
910
+ await DataManager.forceReloadData({
911
+ requireResources: wantsResources,
912
+ resourceDeviceType:
913
+ capabilityDeviceType === EDeviceType.Neo ? EDeviceType.Neo : EDeviceType.Pro2,
914
+ });
904
915
  }
905
916
  if (needsRemoteFirmware) {
906
917
  const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
@@ -923,8 +934,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
923
934
  targetId: item.targetId,
924
935
  }));
925
936
  }
937
+ if (wantsResources) {
938
+ this.params.resourceArchiveBinary = await this.downloadRemoteProtocolV2ResourceArchive(
939
+ deviceFeatures
940
+ );
941
+ resourceMemoryHost = await this.prepareProtocolV2LocalMemoryHost({
942
+ features: deviceFeatures,
943
+ firmwareType,
944
+ availableInstallItems: installItems ?? explicitInstallItems,
945
+ });
946
+ }
926
947
  this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
927
948
  } catch (err) {
949
+ resourceMemoryHost?.release();
928
950
  if (
929
951
  typeof err === 'object' &&
930
952
  err !== null &&
@@ -939,6 +961,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
939
961
  throw normalizeFirmwarePreparationError(err);
940
962
  }
941
963
 
964
+ if (resourceMemoryHost) {
965
+ try {
966
+ return await this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType, false);
967
+ } finally {
968
+ resourceMemoryHost.release();
969
+ }
970
+ }
971
+
942
972
  if (!bootloaderBinary && fwBinaryMap.length === 0 && !installItems?.length) {
943
973
  throw ERRORS.TypedError(
944
974
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
@@ -1092,14 +1122,15 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1092
1122
  private async prepareProtocolV2LocalMemoryHost({
1093
1123
  features,
1094
1124
  firmwareType,
1125
+ availableInstallItems = this.buildProtocolV2InstallItems({
1126
+ bootloaderBinary: this.prepareBootloaderBinary(),
1127
+ fwBinaryMap: this.collectExplicitTargetBinaries(),
1128
+ }),
1095
1129
  }: {
1096
1130
  features: Features;
1097
1131
  firmwareType: EFirmwareType;
1132
+ availableInstallItems?: ProtocolV2InstallItem[];
1098
1133
  }): Promise<FirmwareUpdateV4MemoryHost> {
1099
- const availableInstallItems = this.buildProtocolV2InstallItems({
1100
- bootloaderBinary: this.prepareBootloaderBinary(),
1101
- fwBinaryMap: this.collectExplicitTargetBinaries(),
1102
- });
1103
1134
  const requestedComponentTargets = new Set(
1104
1135
  (this.params.targetsToUpdate ?? []).filter(
1105
1136
  (target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
@@ -1527,9 +1558,15 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1527
1558
  return sources;
1528
1559
  }
1529
1560
 
1530
- private async runProtocolV2PreparedArtifacts(features: Features, firmwareType: EFirmwareType) {
1561
+ private async runProtocolV2PreparedArtifacts(
1562
+ features: Features,
1563
+ firmwareType: EFirmwareType,
1564
+ announceDownload = true
1565
+ ) {
1531
1566
  try {
1532
- this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1567
+ if (announceDownload) {
1568
+ this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1569
+ }
1533
1570
  const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
1534
1571
  const resourceSources = await this.prepareProtocolV2ResourceSources();
1535
1572
  if (installSources.length === 0 && resourceSources.length === 0) {
@@ -1538,7 +1575,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1538
1575
  'No firmware to update'
1539
1576
  );
1540
1577
  }
1541
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1578
+ if (announceDownload) {
1579
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1580
+ }
1542
1581
 
1543
1582
  return await this.executeProtocolV2SourceUpdate({
1544
1583
  installSources,
@@ -1892,6 +1931,44 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1892
1931
  };
1893
1932
  }
1894
1933
 
1934
+ private async downloadRemoteProtocolV2ResourceArchive(features: Features): Promise<ArrayBuffer> {
1935
+ const deviceType = getDeviceType(features);
1936
+ if (deviceType !== EDeviceType.Pro2 && deviceType !== EDeviceType.Neo) {
1937
+ throw ERRORS.TypedError(
1938
+ HardwareErrorCode.RuntimeError,
1939
+ 'Protocol V2 resource archive requires a Pro2 or Neo device'
1940
+ );
1941
+ }
1942
+ const source = DataManager.getProtocolV2ResourceSource(deviceType);
1943
+ const expectedSha256 = normalizeProtocolV2Hex(source?.archiveSha256);
1944
+ if (
1945
+ !source?.archiveUrl ||
1946
+ !Number.isSafeInteger(source.archiveSize) ||
1947
+ source.archiveSize <= 0 ||
1948
+ source.archiveSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES ||
1949
+ !expectedSha256 ||
1950
+ !/^[0-9a-f]{64}$/u.test(expectedSha256)
1951
+ ) {
1952
+ throw ERRORS.TypedError(
1953
+ HardwareErrorCode.RuntimeError,
1954
+ 'Protocol V2 resource archive integrity metadata is invalid',
1955
+ { firmwareUpdateCode: 'FirmwarePlanInvalid' }
1956
+ );
1957
+ }
1958
+ const { binary } = await getSysResourceBinary(source.archiveUrl);
1959
+ if (
1960
+ binary.byteLength !== source.archiveSize ||
1961
+ bytesToHex(sha256(new Uint8Array(binary))) !== expectedSha256
1962
+ ) {
1963
+ throw ERRORS.TypedError(
1964
+ HardwareErrorCode.RuntimeError,
1965
+ 'Protocol V2 resource archive does not match the remote config',
1966
+ { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1967
+ );
1968
+ }
1969
+ return binary;
1970
+ }
1971
+
1895
1972
  private async prepareRemoteProtocolV2Binaries(
1896
1973
  firmwareType: EFirmwareType,
1897
1974
  features: Features,
@@ -2142,8 +2219,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2142
2219
  this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
2143
2220
  const features = await this.device.probeProtocolV2RuntimeState(
2144
2221
  deviceInfo,
2145
- PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
2146
- { allowLegacyProtocolV2ProtocolInfo: true }
2222
+ PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2147
2223
  );
2148
2224
  if (features?.mode === 'bootloader') {
2149
2225
  return features;
@@ -2206,68 +2282,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2206
2282
  return entries;
2207
2283
  }
2208
2284
 
2209
- private buildProtocolV2ExecutionPhases({
2210
- installSources,
2211
- resourceSources,
2212
- }: {
2213
- installSources: ProtocolV2InstallSource[];
2214
- resourceSources: ProtocolV2ResourceBundleSource[];
2215
- }): ProtocolV2ExecutionPhase[] {
2216
- const phases: ProtocolV2ExecutionPhase[] = [];
2217
- const bootResourceSources = resourceSources.filter(source =>
2218
- isProtocolV2BootResourcePackagePath(source.devicePath)
2219
- );
2220
- const remainingResourceSources = resourceSources.filter(
2221
- source => !isProtocolV2BootResourcePackagePath(source.devicePath)
2222
- );
2223
- // A previous interrupted run may have left a promotable staging file. Replace
2224
- // it with the current approved package before any firmware-triggered reboot.
2225
- if (bootResourceSources.length > 0) {
2226
- phases.push({
2227
- kind: 'resource-sync',
2228
- installSources: [],
2229
- resourceSources: bootResourceSources,
2230
- });
2231
- }
2232
- const bootloaderSources = installSources.filter(source => source.kind === 'bootloader');
2233
- if (bootloaderSources.length > 0) {
2234
- phases.push(
2235
- {
2236
- kind: 'bootloader-install',
2237
- installSources: bootloaderSources,
2238
- resourceSources: [],
2239
- },
2240
- {
2241
- kind: 'bootloader-verify',
2242
- installSources: [],
2243
- resourceSources: [],
2244
- }
2245
- );
2246
- }
2247
- if (remainingResourceSources.length > 0) {
2248
- phases.push({
2249
- kind: 'resource-sync',
2250
- installSources: [],
2251
- resourceSources: remainingResourceSources,
2252
- });
2253
- }
2254
- const componentSources = installSources.filter(source => source.kind !== 'bootloader');
2255
- if (componentSources.length > 0) {
2256
- phases.push({
2257
- kind: 'component-install',
2258
- installSources: componentSources,
2259
- resourceSources: [],
2260
- });
2261
- }
2262
- phases.push({
2263
- kind: 'final-verify',
2264
- installSources: [],
2265
- resourceSources: [],
2266
- });
2267
- return phases;
2268
- }
2269
-
2270
- private async executeProtocolV2Phases({
2285
+ private async executeProtocolV2SourceUpdate({
2271
2286
  installSources,
2272
2287
  resourceSources,
2273
2288
  }: {
@@ -2275,38 +2290,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2275
2290
  resourceSources: ProtocolV2ResourceBundleSource[];
2276
2291
  }) {
2277
2292
  this.protocolV2BootResourceStagingSafe = false;
2278
- const phases = this.buildProtocolV2ExecutionPhases({
2279
- installSources,
2280
- resourceSources,
2281
- });
2282
- for (const phase of phases) {
2283
- if (phase.kind === 'final-verify') {
2284
- return this.completeProtocolV2FinalVerification();
2285
- }
2286
- if (phase.kind === 'resource-sync') {
2287
- await this.enterProtocolV2BootloaderMode();
2288
- if (
2289
- !phase.resourceSources.some(source =>
2290
- isProtocolV2BootResourcePackagePath(source.devicePath)
2291
- )
2292
- ) {
2293
- await this.ensureProtocolV2BootResourceStagingIsEmpty();
2294
- }
2295
- await this.executeProtocolV2TransferPhase(phase);
2296
- } else if (phase.kind === 'bootloader-install' || phase.kind === 'component-install') {
2297
- await this.enterProtocolV2BootloaderMode();
2298
- await this.ensureProtocolV2BootResourceStagingIsEmpty();
2299
- await this.executeProtocolV2TransferPhase(phase);
2300
- await this.exitProtocolV2BootloaderToNormal();
2301
- } else if (phase.kind === 'bootloader-verify') {
2302
- await this.waitForProtocolV2FinalFeatures();
2303
- this.assertExpectedProtocolV2Versions(['boot']);
2304
- }
2293
+ if (installSources.length > 0 || resourceSources.length > 0) {
2294
+ await this.enterProtocolV2BootloaderMode();
2295
+ // Clear stale boot-resource staging before writing any artifacts. The new
2296
+ // boot resource, resources and firmware then share one transfer session,
2297
+ // one global progress range and one multi-target install request.
2298
+ // Preserve the approved source order instead of synthesizing a boot-first
2299
+ // phase. Loader firmware owns the cross-stage handoff: bootloader runs its
2300
+ // component targets and leaves boot pending for romloader, while pending
2301
+ // runner-backed records always resume before the application boots.
2302
+ await this.ensureProtocolV2BootResourceStagingIsEmpty();
2303
+ await this.executeProtocolV2TransferPhase({
2304
+ installSources,
2305
+ resourceSources,
2306
+ });
2305
2307
  }
2306
- throw ERRORS.TypedError(
2307
- HardwareErrorCode.RuntimeError,
2308
- 'Protocol V2 execution has no final verification phase'
2309
- );
2308
+ return this.completeProtocolV2FinalVerification();
2310
2309
  }
2311
2310
 
2312
2311
  private async ensureProtocolV2BootResourceStagingIsEmpty() {
@@ -2338,19 +2337,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2338
2337
  this.protocolV2BootResourceStagingSafe = true;
2339
2338
  }
2340
2339
 
2341
- private async executeProtocolV2SourceUpdate({
2342
- installSources,
2343
- resourceSources,
2344
- }: {
2345
- installSources: ProtocolV2InstallSource[];
2346
- resourceSources: ProtocolV2ResourceBundleSource[];
2347
- }) {
2348
- return this.executeProtocolV2Phases({
2349
- installSources,
2350
- resourceSources,
2351
- });
2352
- }
2353
-
2354
2340
  private async executeProtocolV2Update({
2355
2341
  fwBinaryMap,
2356
2342
  bootloaderBinary,
@@ -2375,7 +2361,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2375
2361
  kind: item.kind,
2376
2362
  }))
2377
2363
  );
2378
- return await this.executeProtocolV2Phases({
2364
+ return await this.executeProtocolV2SourceUpdate({
2379
2365
  installSources,
2380
2366
  resourceSources: [],
2381
2367
  });
@@ -2387,7 +2373,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2387
2373
  private async executeProtocolV2TransferPhase({
2388
2374
  installSources,
2389
2375
  resourceSources,
2390
- }: ProtocolV2ExecutionPhase) {
2376
+ }: ProtocolV2TransferBatch) {
2391
2377
  let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
2392
2378
  const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
2393
2379
  for (const resource of resourceSources) {
@@ -2660,6 +2646,47 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2660
2646
  return Array.from(expectedTargetIds).filter(targetId => !reportedTargetIds.has(targetId));
2661
2647
  }
2662
2648
 
2649
+ private getProtocolV2ObservableTargetVersions(features: Features) {
2650
+ const versions = new Map<number, string>();
2651
+ versions.set(
2652
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER,
2653
+ getDeviceBootloaderVersion(features).join('.')
2654
+ );
2655
+ const applicationVersion = getDeviceFirmwareVersion(features).join('.');
2656
+ versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, applicationVersion);
2657
+ versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, applicationVersion);
2658
+ versions.set(
2659
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR,
2660
+ getDeviceBLEFirmwareVersion(features).join('.')
2661
+ );
2662
+ const secureElementVersions: Array<[number, string | null | undefined]> = [
2663
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01, features.se01Version],
2664
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02, features.se02Version],
2665
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03, features.se03Version],
2666
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04, features.se04Version],
2667
+ ];
2668
+ secureElementVersions.forEach(([targetId, version]) => {
2669
+ if (version) versions.set(targetId, version);
2670
+ });
2671
+ return versions;
2672
+ }
2673
+
2674
+ private hasProtocolV2InstallVersionChanged(expectedTargetIds: Set<number>) {
2675
+ if (!this.protocolV2LastRuntimeProbeFeatures) return false;
2676
+ const currentVersions = this.getProtocolV2ObservableTargetVersions(
2677
+ this.protocolV2LastRuntimeProbeFeatures
2678
+ );
2679
+ return Array.from(expectedTargetIds).some(targetId => {
2680
+ const previousVersion = this.protocolV2InstallBaselineVersions.get(targetId);
2681
+ const currentVersion = currentVersions.get(targetId);
2682
+ return (
2683
+ previousVersion !== undefined &&
2684
+ currentVersion !== undefined &&
2685
+ previousVersion !== currentVersion
2686
+ );
2687
+ });
2688
+ }
2689
+
2663
2690
  private async waitForProtocolV2FirmwareUpdateComplete(
2664
2691
  targets: Array<{ target_id: number; path: string }>
2665
2692
  ) {
@@ -2672,12 +2699,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2672
2699
  let deviceInfo: ProtocolV2DeviceInfo | undefined;
2673
2700
  let missingTargetStatusSince: number | undefined;
2674
2701
  let missingTargetStatusKey: string | undefined;
2702
+ let normalModeWithoutInstallEvidenceSince: number | undefined;
2703
+ let installEvidenceObserved = this.protocolV2InstallAckReceived;
2675
2704
  const resetMissingTargetStatusGrace = () => {
2676
2705
  missingTargetStatusSince = undefined;
2677
2706
  missingTargetStatusKey = undefined;
2678
2707
  };
2679
2708
 
2680
2709
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
2710
+ // A transport release caused by an explicit workflow cancellation must not
2711
+ // be mistaken for the expected device reboot during installation.
2712
+ this.throwIfAborted();
2681
2713
  try {
2682
2714
  if (shouldReconnect) {
2683
2715
  await this.reconnectProtocolV2Device();
@@ -2700,21 +2732,33 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2700
2732
  );
2701
2733
  const statusTargets = (statusResponse.message.records ??
2702
2734
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
2735
+ if (
2736
+ statusTargets.some(target => {
2737
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2738
+ return targetId !== undefined && expectedTargetIds.has(targetId);
2739
+ })
2740
+ ) {
2741
+ installEvidenceObserved = true;
2742
+ normalModeWithoutInstallEvidenceSince = undefined;
2743
+ }
2703
2744
  if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
2704
2745
  this.protocolV2FinalStatusVerified = true;
2705
2746
  return;
2706
2747
  }
2707
2748
 
2708
- if (
2709
- statusTargets.length === 0 &&
2710
- currentDeviceInfo &&
2711
- (await this.probeProtocolV2NormalMode(currentDeviceInfo))
2712
- ) {
2713
- Log.log(
2714
- '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
2715
- );
2716
- this.postProgressMessage(100, 'installingFirmware');
2717
- return;
2749
+ if (statusTargets.length === 0 && currentDeviceInfo) {
2750
+ const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2751
+ if (
2752
+ isNormalMode &&
2753
+ (installEvidenceObserved ||
2754
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds))
2755
+ ) {
2756
+ Log.log(
2757
+ '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
2758
+ );
2759
+ this.postProgressMessage(100, 'installingFirmware');
2760
+ return;
2761
+ }
2718
2762
  }
2719
2763
 
2720
2764
  const missingTargetIds = this.getProtocolV2MissingTargetIds(
@@ -2764,16 +2808,39 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2764
2808
  'Protocol V2 device identity is unavailable during install polling'
2765
2809
  );
2766
2810
  }
2767
- if (await this.probeProtocolV2NormalMode(currentDeviceInfo)) {
2811
+ const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2812
+ if (
2813
+ isNormalMode &&
2814
+ (installEvidenceObserved ||
2815
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds))
2816
+ ) {
2768
2817
  Log.log(
2769
2818
  '[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot'
2770
2819
  );
2771
2820
  this.postProgressMessage(100, 'installingFirmware');
2772
2821
  return;
2773
2822
  }
2774
- lastError = new Error(
2775
- 'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
2776
- );
2823
+ if (isNormalMode) {
2824
+ const now = Date.now();
2825
+ normalModeWithoutInstallEvidenceSince ??= now;
2826
+ if (
2827
+ now - normalModeWithoutInstallEvidenceSince >=
2828
+ PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT
2829
+ ) {
2830
+ throw ERRORS.TypedError(
2831
+ HardwareErrorCode.FirmwareError,
2832
+ 'Protocol V2 device returned to normal mode without install ACK, target status, or version change'
2833
+ );
2834
+ }
2835
+ lastError = new Error(
2836
+ 'Protocol V2 device is in normal mode but installation is not yet confirmed'
2837
+ );
2838
+ } else {
2839
+ normalModeWithoutInstallEvidenceSince = undefined;
2840
+ lastError = new Error(
2841
+ 'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
2842
+ );
2843
+ }
2777
2844
  } else {
2778
2845
  shouldReconnect = true;
2779
2846
  deviceInfo = undefined;
@@ -2832,9 +2899,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2832
2899
  private async probeProtocolV2NormalMode(deviceInfo: ProtocolV2DeviceInfo) {
2833
2900
  const features = await this.device.probeProtocolV2RuntimeState(
2834
2901
  deviceInfo,
2835
- PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
2836
- { allowLegacyProtocolV2ProtocolInfo: true }
2902
+ PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2837
2903
  );
2904
+ this.protocolV2LastRuntimeProbeFeatures = features;
2838
2905
  return features.mode === 'normal' && !features.bootloaderMode;
2839
2906
  }
2840
2907
 
@@ -2895,8 +2962,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2895
2962
  this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
2896
2963
  const features = await this.device.probeProtocolV2RuntimeState(
2897
2964
  deviceInfo,
2898
- PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
2899
- { allowLegacyProtocolV2ProtocolInfo: true }
2965
+ PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2900
2966
  );
2901
2967
  if (features.mode === 'normal' && !features.bootloaderMode) {
2902
2968
  return features;
@@ -3068,6 +3134,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3068
3134
  }: {
3069
3135
  targets: Array<{ target_id: number; path: string }>;
3070
3136
  }) {
3137
+ this.protocolV2InstallAckReceived = false;
3138
+ this.protocolV2LastRuntimeProbeFeatures = undefined;
3071
3139
  const startUpdate = () =>
3072
3140
  this.device
3073
3141
  .getCommands()
@@ -3077,29 +3145,51 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3077
3145
  { targets },
3078
3146
  { timeoutMs: PROTOCOL_V2_START_UPDATE_TIMEOUT }
3079
3147
  );
3080
- let response: ProtocolV2FirmwareUpdateStartResponse;
3148
+ let response: ProtocolV2FirmwareUpdateStartResponse | undefined;
3081
3149
  try {
3082
3150
  response = await startUpdate();
3151
+ this.protocolV2InstallAckReceived = true;
3083
3152
  } catch (error) {
3084
- if (
3085
- !this.protocolV2LegacyDirectUpdate ||
3086
- !isProtocolV2FirmwareUpdateEndpointUnavailable(error)
3153
+ if (this.isBleReconnect() && isProtocolV2BleTransportReleasedError(error)) {
3154
+ this.throwIfAborted();
3155
+ // Installation can reboot the device before the Success response reaches
3156
+ // the host. The request has side effects, so do not replay it; reconnect
3157
+ // and let status polling determine whether installation was accepted.
3158
+ Log.log(
3159
+ '[FirmwareUpdateV4] install request interrupted by device reboot; continue status polling: ',
3160
+ error
3161
+ );
3162
+ } else if (
3163
+ this.protocolV2LegacyDirectUpdate &&
3164
+ isProtocolV2FirmwareUpdateEndpointUnavailable(error)
3087
3165
  ) {
3166
+ // Both legacy loaders register this request. A missing handler therefore identifies
3167
+ // a legacy App before dispatch, so it is safe to reboot and retry the unhandled request.
3168
+ this.protocolV2LegacyDirectUpdate = false;
3169
+ Log.debug(
3170
+ '[FirmwareUpdateV4] legacy App does not expose DeviceFirmwareUpdateRequest; rebooting to bootloader'
3171
+ );
3172
+ await this.rebootProtocolV2ToBootloader();
3173
+ try {
3174
+ response = await startUpdate();
3175
+ this.protocolV2InstallAckReceived = true;
3176
+ } catch (retryError) {
3177
+ if (!(this.isBleReconnect() && isProtocolV2BleTransportReleasedError(retryError))) {
3178
+ throw retryError;
3179
+ }
3180
+ this.throwIfAborted();
3181
+ Log.log(
3182
+ '[FirmwareUpdateV4] install request interrupted after legacy reboot; continue status polling: ',
3183
+ retryError
3184
+ );
3185
+ }
3186
+ } else {
3088
3187
  throw error;
3089
3188
  }
3090
-
3091
- // Both legacy loaders register this request. A missing handler therefore identifies
3092
- // a legacy App before dispatch, so it is safe to reboot and retry the unhandled request.
3093
- this.protocolV2LegacyDirectUpdate = false;
3094
- Log.debug(
3095
- '[FirmwareUpdateV4] legacy App does not expose DeviceFirmwareUpdateRequest; rebooting to bootloader'
3096
- );
3097
- await this.rebootProtocolV2ToBootloader();
3098
- response = await startUpdate();
3099
3189
  }
3100
3190
  this.protocolV2LegacyDirectUpdate = false;
3101
- // Success acknowledges that the device accepted installation. End confirmation
3102
- // and begin status polling only after receiving this ACK.
3191
+ // A Success ACK or an install-time disconnect both end confirmation. In the
3192
+ // latter case only status polling may establish the final outcome.
3103
3193
  this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdating);
3104
3194
  this.postProgressMessage(0, 'installingFirmware');
3105
3195
  return response;
@@ -43,7 +43,6 @@ export default class GetDeviceState extends BaseMethod<
43
43
  async run() {
44
44
  const state = await this.device.getDeviceState({
45
45
  refreshSections: SCOPE_SECTIONS[this.params.scope],
46
- ...(this.params.scope === 'firmware' ? { allowLegacyProtocolV2ProtocolInfo: true } : {}),
47
46
  });
48
47
  if (
49
48
  this.params.scope === 'settings' &&
@@ -63,8 +63,6 @@ export default class SearchDevices extends BaseMethod {
63
63
  connectProtocol: undefined,
64
64
  forceProtocolDetection: true,
65
65
  refreshRuntimeState: true,
66
- // Old loader firmware predates build_fingerprint. Keep this recovery-only.
67
- allowLegacyProtocolV2ProtocolInfo: true,
68
66
  });
69
67
  deviceList.push(...result.deviceList);
70
68
  } catch (error) {