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

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 (61) hide show
  1. package/__tests__/DeviceCommands.test.ts +28 -100
  2. package/__tests__/device-lifecycle-events.test.ts +2 -113
  3. package/__tests__/device-pool-state.test.ts +0 -25
  4. package/__tests__/deviceSettings.test.ts +1 -0
  5. package/__tests__/deviceUploadNft.test.ts +4 -33
  6. package/__tests__/get-device-state.test.ts +13 -52
  7. package/__tests__/logBlockEvent.test.ts +1 -13
  8. package/__tests__/protocol-v2.test.ts +121 -758
  9. package/__tests__/refresh-device-state.test.ts +1 -3
  10. package/__tests__/search-devices.test.ts +38 -32
  11. package/dist/api/FirmwareUpdateV4.d.ts +3 -10
  12. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  13. package/dist/api/UploadPortfolio.d.ts +1 -1
  14. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  15. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +3 -2
  16. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  17. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +3 -2
  18. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  19. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/device/Device.d.ts +2 -7
  22. package/dist/device/Device.d.ts.map +1 -1
  23. package/dist/device/DeviceCommands.d.ts.map +1 -1
  24. package/dist/device/DevicePool.d.ts.map +1 -1
  25. package/dist/events/logBlockEvent.d.ts.map +1 -1
  26. package/dist/index.d.ts +16 -12
  27. package/dist/index.js +201 -446
  28. package/dist/protocols/protocol-v2/features.d.ts +1 -9
  29. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  30. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  31. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  32. package/dist/types/api/protocolV2.d.ts +1 -1
  33. package/dist/types/api/protocolV2.d.ts.map +1 -1
  34. package/dist/types/device.d.ts.map +1 -1
  35. package/dist/utils/pro2Nft.d.ts +0 -7
  36. package/dist/utils/pro2Nft.d.ts.map +1 -1
  37. package/package.json +4 -6
  38. package/src/api/FirmwareUpdateV4.ts +174 -312
  39. package/src/api/SearchDevices.ts +2 -2
  40. package/src/api/UploadPortfolio.ts +2 -9
  41. package/src/api/protocol-v2/DeviceUploadNft.ts +8 -56
  42. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +18 -37
  43. package/src/api/ton/TonSignMessage.ts +3 -5
  44. package/src/core/index.ts +2 -6
  45. package/src/device/Device.ts +31 -53
  46. package/src/device/DeviceCommands.ts +14 -4
  47. package/src/device/DevicePool.ts +2 -6
  48. package/src/events/logBlockEvent.ts +1 -14
  49. package/src/protocols/protocol-v2/features.ts +2 -19
  50. package/src/protocols/protocol-v2/index.ts +0 -2
  51. package/src/types/api/protocolV2.ts +1 -1
  52. package/src/types/device.ts +2 -1
  53. package/src/utils/deviceSettings.ts +1 -1
  54. package/src/utils/pro2Nft.ts +8 -31
  55. package/__tests__/base64Data.test.ts +0 -70
  56. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +0 -145
  57. package/__tests__/resourceBase64Boundary.test.ts +0 -48
  58. package/__tests__/ton-sign-message.test.ts +0 -84
  59. package/dist/api/helpers/base64Data.d.ts +0 -16
  60. package/dist/api/helpers/base64Data.d.ts.map +0 -1
  61. package/src/api/helpers/base64Data.ts +0 -85
@@ -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 {
@@ -244,7 +243,15 @@ type JSZipSizedEntry = JSZip.JSZipObject & {
244
243
  };
245
244
  };
246
245
 
247
- type ProtocolV2TransferBatch = {
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;
248
255
  installSources: ProtocolV2InstallSource[];
249
256
  resourceSources: ProtocolV2ResourceBundleSource[];
250
257
  };
@@ -377,19 +384,6 @@ const isProtocolV2ReconnectProbeError = (error: unknown) => {
377
384
  );
378
385
  };
379
386
 
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
-
393
387
  const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
394
388
  const message = getProtocolV2UnknownErrorText(error).toLowerCase();
395
389
  return (
@@ -399,13 +393,6 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
399
393
  );
400
394
  };
401
395
 
402
- const isProtocolV2FirmwareUpdateEndpointUnavailable = (error: unknown) => {
403
- const message = getProtocolV2UnknownErrorText(error).toLowerCase();
404
- return (
405
- message.includes('handler not registered') || message.includes('message handler not found')
406
- );
407
- };
408
-
409
396
  const isProtocolV2TerminalInstallStatusError = (error: unknown) =>
410
397
  error instanceof HardwareError &&
411
398
  (error.errorCode === HardwareErrorCode.FirmwareError ||
@@ -600,8 +587,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
600
587
 
601
588
  private protocolV2ExecutionInLoader = false;
602
589
 
603
- private protocolV2LegacyDirectUpdate = false;
604
-
605
590
  private protocolV2BootResourceStagingSafe = false;
606
591
 
607
592
  private protocolV2CompletedTargetVersions = new Map<number, number>();
@@ -610,12 +595,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
610
595
 
611
596
  private protocolV2FinalStatusVerified = false;
612
597
 
613
- private protocolV2InstallAckReceived = false;
614
-
615
- private protocolV2InstallBaselineVersions = new Map<number, string>();
616
-
617
- private protocolV2LastRuntimeProbeFeatures?: Features;
618
-
619
598
  init() {
620
599
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
621
600
  this.requireDeviceMode = [];
@@ -834,9 +813,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
834
813
  private async runProtocolV2() {
835
814
  await this.captureProtocolV2PhysicalIdentity();
836
815
  const deviceFeatures = await this.getProtocolV2DeviceFeatures();
837
- this.protocolV2InstallBaselineVersions =
838
- this.getProtocolV2ObservableTargetVersions(deviceFeatures);
839
- this.protocolV2LastRuntimeProbeFeatures = undefined;
840
816
  const currentDeviceType = this.device.getCurrentDeviceType();
841
817
  const capabilityDeviceType =
842
818
  currentDeviceType === EDeviceType.Pro2 || currentDeviceType === EDeviceType.Neo
@@ -850,7 +826,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
850
826
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
851
827
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
852
828
  this.validateExpectedTargetVersions();
853
- const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
854
829
 
855
830
  if (
856
831
  !this.params.preparedPlan &&
@@ -874,11 +849,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
874
849
  if (this.params.preparedPlan || hasPreparedComponentArtifacts) {
875
850
  return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
876
851
  }
852
+ const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
877
853
 
878
854
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
879
855
  let bootloaderBinary: ArrayBuffer | null = null;
880
856
  let installItems: ProtocolV2InstallItem[] | undefined;
881
- let resourceMemoryHost: FirmwareUpdateV4MemoryHost | undefined;
882
857
  try {
883
858
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
884
859
  fwBinaryMap = this.collectExplicitTargetBinaries();
@@ -891,9 +866,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
891
866
  const needsRemoteFirmware = this.params.targetsToUpdate?.length
892
867
  ? missingFirmwareTargets.length > 0
893
868
  : explicitInstallItems.length === 0;
894
- const needsSdkManagedArtifacts = needsRemoteFirmware || wantsResources;
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
+ }
895
878
  if (
896
- needsSdkManagedArtifacts &&
879
+ needsRemoteFirmware &&
897
880
  (this.params.artifactReader ||
898
881
  DataManager.getSettings('firmwareManifestMode') === 'external-only')
899
882
  ) {
@@ -905,13 +888,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
905
888
  }
906
889
  );
907
890
  }
908
- if (needsSdkManagedArtifacts) {
891
+ if (needsRemoteFirmware) {
909
892
  // Remote updates must use a freshly fetched config before any reboot or file write.
910
- await DataManager.forceReloadData({
911
- requireResources: wantsResources,
912
- resourceDeviceType:
913
- capabilityDeviceType === EDeviceType.Neo ? EDeviceType.Neo : EDeviceType.Pro2,
914
- });
893
+ await DataManager.forceReloadData();
915
894
  }
916
895
  if (needsRemoteFirmware) {
917
896
  const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
@@ -934,19 +913,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
934
913
  targetId: item.targetId,
935
914
  }));
936
915
  }
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
- }
947
916
  this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
948
917
  } catch (err) {
949
- resourceMemoryHost?.release();
950
918
  if (
951
919
  typeof err === 'object' &&
952
920
  err !== null &&
@@ -961,14 +929,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
961
929
  throw normalizeFirmwarePreparationError(err);
962
930
  }
963
931
 
964
- if (resourceMemoryHost) {
965
- try {
966
- return await this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType, false);
967
- } finally {
968
- resourceMemoryHost.release();
969
- }
970
- }
971
-
972
932
  if (!bootloaderBinary && fwBinaryMap.length === 0 && !installItems?.length) {
973
933
  throw ERRORS.TypedError(
974
934
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
@@ -1122,15 +1082,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1122
1082
  private async prepareProtocolV2LocalMemoryHost({
1123
1083
  features,
1124
1084
  firmwareType,
1125
- availableInstallItems = this.buildProtocolV2InstallItems({
1126
- bootloaderBinary: this.prepareBootloaderBinary(),
1127
- fwBinaryMap: this.collectExplicitTargetBinaries(),
1128
- }),
1129
1085
  }: {
1130
1086
  features: Features;
1131
1087
  firmwareType: EFirmwareType;
1132
- availableInstallItems?: ProtocolV2InstallItem[];
1133
1088
  }): Promise<FirmwareUpdateV4MemoryHost> {
1089
+ const availableInstallItems = this.buildProtocolV2InstallItems({
1090
+ bootloaderBinary: this.prepareBootloaderBinary(),
1091
+ fwBinaryMap: this.collectExplicitTargetBinaries(),
1092
+ });
1134
1093
  const requestedComponentTargets = new Set(
1135
1094
  (this.params.targetsToUpdate ?? []).filter(
1136
1095
  (target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
@@ -1558,15 +1517,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1558
1517
  return sources;
1559
1518
  }
1560
1519
 
1561
- private async runProtocolV2PreparedArtifacts(
1562
- features: Features,
1563
- firmwareType: EFirmwareType,
1564
- announceDownload = true
1565
- ) {
1520
+ private async runProtocolV2PreparedArtifacts(features: Features, firmwareType: EFirmwareType) {
1566
1521
  try {
1567
- if (announceDownload) {
1568
- this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1569
- }
1522
+ this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1570
1523
  const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
1571
1524
  const resourceSources = await this.prepareProtocolV2ResourceSources();
1572
1525
  if (installSources.length === 0 && resourceSources.length === 0) {
@@ -1575,9 +1528,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1575
1528
  'No firmware to update'
1576
1529
  );
1577
1530
  }
1578
- if (announceDownload) {
1579
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1580
- }
1531
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1581
1532
 
1582
1533
  return await this.executeProtocolV2SourceUpdate({
1583
1534
  installSources,
@@ -1931,44 +1882,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1931
1882
  };
1932
1883
  }
1933
1884
 
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
-
1972
1885
  private async prepareRemoteProtocolV2Binaries(
1973
1886
  firmwareType: EFirmwareType,
1974
1887
  features: Features,
@@ -2154,12 +2067,21 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2154
2067
  return this.device.features?.mode === 'romloader';
2155
2068
  }
2156
2069
 
2157
- private isLegacyProtocolV2Runtime() {
2158
- const protocolInfo = this.device.state?.raw?.protocolV2ProtocolInfo;
2159
- return protocolInfo ? isLegacyProtocolV2ProtocolInfo(protocolInfo) : false;
2160
- }
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
+ }
2161
2084
 
2162
- private async rebootProtocolV2ToBootloader() {
2163
2085
  try {
2164
2086
  this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader);
2165
2087
  await this.protocolV2Reboot(DeviceRebootType.Bootloader);
@@ -2177,27 +2099,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2177
2099
  }
2178
2100
  }
2179
2101
 
2180
- async enterProtocolV2BootloaderMode() {
2181
- this.protocolV2LegacyDirectUpdate = false;
2182
- // romloader is the first update environment and forwards targets to bootloader.
2183
- // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
2184
- if (this.isProtocolV2RomloaderMode()) {
2185
- Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
2186
- this.protocolV2LegacyDirectUpdate = this.isLegacyProtocolV2Runtime();
2187
- this.protocolV2ExecutionInLoader = true;
2188
- return false;
2189
- }
2190
- if (this.isProtocolV2BootloaderMode()) {
2191
- Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
2192
- this.protocolV2LegacyDirectUpdate = this.isLegacyProtocolV2Runtime();
2193
- this.protocolV2ExecutionInLoader = true;
2194
- this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
2195
- return false;
2196
- }
2197
-
2198
- return this.rebootProtocolV2ToBootloader();
2199
- }
2200
-
2201
2102
  private async waitForProtocolV2BootloaderMode(
2202
2103
  timeout = PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT,
2203
2104
  retryInterval = 1000
@@ -2282,7 +2183,68 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2282
2183
  return entries;
2283
2184
  }
2284
2185
 
2285
- private async executeProtocolV2SourceUpdate({
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({
2286
2248
  installSources,
2287
2249
  resourceSources,
2288
2250
  }: {
@@ -2290,22 +2252,38 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2290
2252
  resourceSources: ProtocolV2ResourceBundleSource[];
2291
2253
  }) {
2292
2254
  this.protocolV2BootResourceStagingSafe = false;
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
- });
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
+ }
2307
2282
  }
2308
- return this.completeProtocolV2FinalVerification();
2283
+ throw ERRORS.TypedError(
2284
+ HardwareErrorCode.RuntimeError,
2285
+ 'Protocol V2 execution has no final verification phase'
2286
+ );
2309
2287
  }
2310
2288
 
2311
2289
  private async ensureProtocolV2BootResourceStagingIsEmpty() {
@@ -2337,6 +2315,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2337
2315
  this.protocolV2BootResourceStagingSafe = true;
2338
2316
  }
2339
2317
 
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
+
2340
2331
  private async executeProtocolV2Update({
2341
2332
  fwBinaryMap,
2342
2333
  bootloaderBinary,
@@ -2361,7 +2352,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2361
2352
  kind: item.kind,
2362
2353
  }))
2363
2354
  );
2364
- return await this.executeProtocolV2SourceUpdate({
2355
+ return await this.executeProtocolV2Phases({
2365
2356
  installSources,
2366
2357
  resourceSources: [],
2367
2358
  });
@@ -2373,7 +2364,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2373
2364
  private async executeProtocolV2TransferPhase({
2374
2365
  installSources,
2375
2366
  resourceSources,
2376
- }: ProtocolV2TransferBatch) {
2367
+ }: ProtocolV2ExecutionPhase) {
2377
2368
  let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
2378
2369
  const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
2379
2370
  for (const resource of resourceSources) {
@@ -2646,47 +2637,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2646
2637
  return Array.from(expectedTargetIds).filter(targetId => !reportedTargetIds.has(targetId));
2647
2638
  }
2648
2639
 
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
-
2690
2640
  private async waitForProtocolV2FirmwareUpdateComplete(
2691
2641
  targets: Array<{ target_id: number; path: string }>
2692
2642
  ) {
@@ -2699,17 +2649,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2699
2649
  let deviceInfo: ProtocolV2DeviceInfo | undefined;
2700
2650
  let missingTargetStatusSince: number | undefined;
2701
2651
  let missingTargetStatusKey: string | undefined;
2702
- let normalModeWithoutInstallEvidenceSince: number | undefined;
2703
- let installEvidenceObserved = this.protocolV2InstallAckReceived;
2704
2652
  const resetMissingTargetStatusGrace = () => {
2705
2653
  missingTargetStatusSince = undefined;
2706
2654
  missingTargetStatusKey = undefined;
2707
2655
  };
2708
2656
 
2709
2657
  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();
2713
2658
  try {
2714
2659
  if (shouldReconnect) {
2715
2660
  await this.reconnectProtocolV2Device();
@@ -2732,33 +2677,21 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2732
2677
  );
2733
2678
  const statusTargets = (statusResponse.message.records ??
2734
2679
  []) 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
- }
2744
2680
  if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
2745
2681
  this.protocolV2FinalStatusVerified = true;
2746
2682
  return;
2747
2683
  }
2748
2684
 
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
- }
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;
2762
2695
  }
2763
2696
 
2764
2697
  const missingTargetIds = this.getProtocolV2MissingTargetIds(
@@ -2808,39 +2741,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2808
2741
  'Protocol V2 device identity is unavailable during install polling'
2809
2742
  );
2810
2743
  }
2811
- const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2812
- if (
2813
- isNormalMode &&
2814
- (installEvidenceObserved ||
2815
- this.hasProtocolV2InstallVersionChanged(expectedTargetIds))
2816
- ) {
2744
+ if (await this.probeProtocolV2NormalMode(currentDeviceInfo)) {
2817
2745
  Log.log(
2818
2746
  '[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot'
2819
2747
  );
2820
2748
  this.postProgressMessage(100, 'installingFirmware');
2821
2749
  return;
2822
2750
  }
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
- }
2751
+ lastError = new Error(
2752
+ 'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
2753
+ );
2844
2754
  } else {
2845
2755
  shouldReconnect = true;
2846
2756
  deviceInfo = undefined;
@@ -2901,7 +2811,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2901
2811
  deviceInfo,
2902
2812
  PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2903
2813
  );
2904
- this.protocolV2LastRuntimeProbeFeatures = features;
2905
2814
  return features.mode === 'normal' && !features.bootloaderMode;
2906
2815
  }
2907
2816
 
@@ -3134,62 +3043,15 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3134
3043
  }: {
3135
3044
  targets: Array<{ target_id: number; path: string }>;
3136
3045
  }) {
3137
- this.protocolV2InstallAckReceived = false;
3138
- this.protocolV2LastRuntimeProbeFeatures = undefined;
3139
- const startUpdate = () =>
3140
- this.device
3141
- .getCommands()
3142
- .typedCall(
3143
- 'DeviceFirmwareUpdateRequest',
3144
- 'Success',
3145
- { targets },
3146
- { timeoutMs: PROTOCOL_V2_START_UPDATE_TIMEOUT }
3147
- );
3148
- let response: ProtocolV2FirmwareUpdateStartResponse | undefined;
3149
- try {
3150
- response = await startUpdate();
3151
- this.protocolV2InstallAckReceived = true;
3152
- } catch (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)
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 {
3187
- throw error;
3188
- }
3189
- }
3190
- this.protocolV2LegacyDirectUpdate = false;
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.
3046
+ 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.
3193
3055
  this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdating);
3194
3056
  this.postProgressMessage(0, 'installingFirmware');
3195
3057
  return response;