@onekeyfe/hd-core 1.2.0-alpha.22 → 1.2.0-alpha.24

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 (151) hide show
  1. package/__tests__/DeviceCommands.test.ts +43 -8
  2. package/__tests__/connectSettings.test.ts +5 -47
  3. package/__tests__/device-lifecycle-events.test.ts +59 -0
  4. package/__tests__/device-settings.test.ts +1 -1
  5. package/__tests__/device-wallet-session-store.test.ts +41 -0
  6. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +30 -39
  7. package/__tests__/logBlockEvent.test.ts +17 -0
  8. package/__tests__/open-wallet-session.test.ts +233 -111
  9. package/__tests__/protocol-v2-ping.test.ts +28 -0
  10. package/__tests__/protocol-v2.test.ts +513 -313
  11. package/__tests__/public-pro2-api-boundary.test.ts +22 -0
  12. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  13. package/dist/api/FirmwareUpdateV2.d.ts +0 -11
  14. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  15. package/dist/api/FirmwareUpdateV3.d.ts +3 -7
  16. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  17. package/dist/api/FirmwareUpdateV4.d.ts +5 -21
  18. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  19. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  20. package/dist/api/OpenWalletSession.d.ts +1 -8
  21. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  22. package/dist/api/device/DeviceUpdateBootloader.d.ts +1 -5
  23. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  24. package/dist/api/device/DeviceWipe.d.ts.map +1 -1
  25. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +0 -7
  26. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  27. package/dist/api/firmware/getBinary.d.ts +0 -7
  28. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  29. package/dist/api/firmware/updateBootloader.d.ts +0 -2
  30. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  31. package/dist/api/firmware/uploadFirmware.d.ts +1 -9
  32. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  33. package/dist/api/index.d.ts +1 -0
  34. package/dist/api/index.d.ts.map +1 -1
  35. package/dist/api/protocol-v2/Ping.d.ts +2 -0
  36. package/dist/api/protocol-v2/Ping.d.ts.map +1 -1
  37. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  38. package/dist/core/index.d.ts.map +1 -1
  39. package/dist/data-manager/DataManager.d.ts +0 -1
  40. package/dist/data-manager/DataManager.d.ts.map +1 -1
  41. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  42. package/dist/device/Device.d.ts +3 -2
  43. package/dist/device/Device.d.ts.map +1 -1
  44. package/dist/device/DeviceCommands.d.ts.map +1 -1
  45. package/dist/events/device.d.ts +1 -0
  46. package/dist/events/device.d.ts.map +1 -1
  47. package/dist/events/logBlockEvent.d.ts.map +1 -1
  48. package/dist/events/ui-request.d.ts +1 -0
  49. package/dist/events/ui-request.d.ts.map +1 -1
  50. package/dist/index.d.ts +98 -278
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +876 -2647
  53. package/dist/inject.d.ts.map +1 -1
  54. package/dist/lowLevelInject.d.ts.map +1 -1
  55. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  56. package/dist/protocols/protocol-v2/unlockRetry.d.ts +2 -3
  57. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  58. package/dist/protocols/protocol-v2/walletSession.d.ts +12 -0
  59. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  60. package/dist/topLevelInject.d.ts.map +1 -1
  61. package/dist/types/api/checkAllFirmwareRelease.d.ts +0 -3
  62. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  63. package/dist/types/api/deviceUpdateBootloader.d.ts +0 -6
  64. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  65. package/dist/types/api/export.d.ts +1 -4
  66. package/dist/types/api/export.d.ts.map +1 -1
  67. package/dist/types/api/firmwareUpdate.d.ts +0 -69
  68. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  69. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  70. package/dist/types/api/index.d.ts +4 -10
  71. package/dist/types/api/index.d.ts.map +1 -1
  72. package/dist/types/api/openWalletSession.d.ts +0 -12
  73. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  74. package/dist/types/api/protocolV2.d.ts +4 -0
  75. package/dist/types/api/protocolV2.d.ts.map +1 -1
  76. package/dist/types/settings.d.ts +0 -13
  77. package/dist/types/settings.d.ts.map +1 -1
  78. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  79. package/dist/utils/patch.d.ts +1 -1
  80. package/dist/utils/patch.d.ts.map +1 -1
  81. package/package.json +4 -4
  82. package/src/api/CheckAllFirmwareRelease.ts +3 -16
  83. package/src/api/FirmwareUpdateV2.ts +119 -284
  84. package/src/api/FirmwareUpdateV3.ts +55 -248
  85. package/src/api/FirmwareUpdateV4.ts +293 -751
  86. package/src/api/GetPassphraseState.ts +11 -5
  87. package/src/api/OpenWalletSession.ts +23 -86
  88. package/src/api/device/DeviceUpdateBootloader.ts +6 -114
  89. package/src/api/device/DeviceWipe.ts +2 -0
  90. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +0 -50
  91. package/src/api/firmware/updateBootloader.ts +4 -19
  92. package/src/api/firmware/uploadFirmware.ts +22 -140
  93. package/src/api/index.ts +1 -0
  94. package/src/api/protocol-v2/Ping.ts +32 -1
  95. package/src/core/index.ts +40 -7
  96. package/src/data/messages/messages-protocol-v2.json +49 -56
  97. package/src/data-manager/DataManager.ts +33 -33
  98. package/src/data-manager/connectSettings.ts +0 -11
  99. package/src/device/Device.ts +29 -8
  100. package/src/device/DeviceCommands.ts +43 -151
  101. package/src/events/device.ts +1 -0
  102. package/src/events/logBlockEvent.ts +1 -15
  103. package/src/events/ui-request.ts +1 -0
  104. package/src/index.ts +0 -5
  105. package/src/inject.ts +4 -22
  106. package/src/lowLevelInject.ts +1 -5
  107. package/src/protocols/protocol-v2/deviceSession.ts +1 -1
  108. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +0 -1
  109. package/src/protocols/protocol-v2/unlockRetry.ts +20 -3
  110. package/src/protocols/protocol-v2/walletSession.ts +57 -18
  111. package/src/topLevelInject.ts +1 -5
  112. package/src/types/api/checkAllFirmwareRelease.ts +0 -3
  113. package/src/types/api/deviceUpdateBootloader.ts +0 -6
  114. package/src/types/api/export.ts +0 -18
  115. package/src/types/api/firmwareUpdate.ts +0 -76
  116. package/src/types/api/getPassphraseState.ts +4 -3
  117. package/src/types/api/index.ts +3 -14
  118. package/src/types/api/openWalletSession.ts +0 -19
  119. package/src/types/api/protocolV2.ts +10 -0
  120. package/src/types/settings.ts +0 -13
  121. package/src/utils/deviceInfoUtils.ts +2 -3
  122. package/__tests__/firmware-update/firmware-artifact-source.test.ts +0 -119
  123. package/__tests__/firmware-update/firmware-host-binding.test.ts +0 -70
  124. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +0 -13
  125. package/__tests__/firmware-update/firmware-update-plan.test.ts +0 -326
  126. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +0 -200
  127. package/__tests__/firmware-update/firmware-upload-source.test.ts +0 -70
  128. package/dist/api/firmware/FirmwareArtifactSource.d.ts +0 -26
  129. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +0 -1
  130. package/dist/api/firmware/FirmwareHostBinding.d.ts +0 -6
  131. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +0 -1
  132. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +0 -3
  133. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +0 -1
  134. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -24
  135. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +0 -1
  136. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +0 -25
  137. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +0 -1
  138. package/dist/types/api/firmwareUpdateCapabilities.d.ts +0 -9
  139. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +0 -1
  140. package/dist/types/api/firmwareUpdatePlan.d.ts +0 -27
  141. package/dist/types/api/firmwareUpdatePlan.d.ts.map +0 -1
  142. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +0 -42
  143. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +0 -1
  144. package/src/api/firmware/FirmwareArtifactSource.ts +0 -278
  145. package/src/api/firmware/FirmwareHostBinding.ts +0 -100
  146. package/src/api/firmware/FirmwareUpdateCapabilities.ts +0 -9
  147. package/src/api/firmware/FirmwareUpdatePlan.ts +0 -621
  148. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +0 -378
  149. package/src/types/api/firmwareUpdateCapabilities.ts +0 -9
  150. package/src/types/api/firmwareUpdatePlan.ts +0 -36
  151. package/src/types/api/firmwareUpdatePreparedPlan.ts +0 -53
@@ -2,6 +2,7 @@ import { ERRORS, HardwareError, HardwareErrorCode, wait } from '@onekeyfe/hd-sha
2
2
  import {
3
3
  DeviceRebootType,
4
4
  PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
5
+ PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE,
5
6
  PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
6
7
  } from '@onekeyfe/hd-transport';
7
8
 
@@ -29,17 +30,11 @@ import {
29
30
  getProtocolV2UnknownErrorText,
30
31
  isProtocolV2DeviceDisconnectedError,
31
32
  } from './protocol-v2/helpers';
32
- import { openFirmwareByteSource, writeFirmwareByteSource } from './firmware/FirmwareArtifactSource';
33
- import { resolveFirmwareUpdateHostBinding } from './firmware/FirmwareHostBinding';
34
- import { assertFirmwareUpdatePreparedPlanBinding } from './firmware/FirmwareUpdatePreparedPlan';
35
33
 
36
- import type {
37
- FirmwareArtifactReference,
38
- FirmwareUpdateV4Params,
39
- FirmwareUpdateV4Target,
40
- } from '../types/api/firmwareUpdate';
34
+ import type { FirmwareUpdateV4Params, FirmwareUpdateV4Target } from '../types/api/firmwareUpdate';
41
35
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
42
36
  import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
37
+ import type { PROTO } from '../constants';
43
38
  import type { TypedResponseMessage } from '../device/DeviceCommands';
44
39
  import type {
45
40
  Features,
@@ -47,7 +42,6 @@ import type {
47
42
  IProtocolV2FirmwareComponent,
48
43
  IVersionArray,
49
44
  } from '../types';
50
- import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
51
45
 
52
46
  const Log = getLogger(LoggerNames.Method);
53
47
 
@@ -91,6 +85,11 @@ const getProtocolV2DeviceTransferProgress = (
91
85
  return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
92
86
  };
93
87
 
88
+ const formatProtocolV2TransferSpeed = (bytes: number, elapsedMs: number) => {
89
+ const safeElapsedMs = Math.max(elapsedMs, 1);
90
+ return (bytes / 1024 / (safeElapsedMs / 1000)).toFixed(2);
91
+ };
92
+
94
93
  type ProtocolV2FirmwareUpdateStatusTarget = {
95
94
  target_id: number | string;
96
95
  status?: number | string;
@@ -104,6 +103,17 @@ type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId:
104
103
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
105
104
  kind: ProtocolV2RemoteComponentTarget['kind'];
106
105
  };
106
+ type ProtocolV2InstallTarget = ProtocolV2InstallItem & {
107
+ path: string;
108
+ };
109
+
110
+ type ProtocolV2FileTransferParams = PROTO.FirmwareUpload & {
111
+ filePath: string;
112
+ processedSize?: number;
113
+ totalSize?: number;
114
+ onTransferredBytes?: (transferredBytes: number) => void;
115
+ };
116
+
107
117
  /** RESC bundle okpkg written independently to devicePath through FileWrite. */
108
118
  type ProtocolV2ResourceBundleBinary = {
109
119
  name: string;
@@ -126,35 +136,6 @@ type ProtocolV2RemoteComponentTarget = {
126
136
  kind: 'bootloader' | 'firmware';
127
137
  };
128
138
 
129
- type ProtocolV2InstallSource = {
130
- fileName: string;
131
- source: FirmwareByteSource;
132
- targetId: number;
133
- kind: ProtocolV2RemoteComponentTarget['kind'];
134
- };
135
-
136
- type ProtocolV2ResourceBundleSource = {
137
- name: string;
138
- source: FirmwareByteSource;
139
- devicePath: string;
140
- version?: IVersionArray;
141
- payloadHash?: string;
142
- headerHash?: string;
143
- };
144
-
145
- type ProtocolV2ExecutionPhaseKind =
146
- | 'resource-sync'
147
- | 'bootloader-install'
148
- | 'bootloader-verify'
149
- | 'component-install'
150
- | 'final-verify';
151
-
152
- type ProtocolV2ExecutionPhase = {
153
- kind: ProtocolV2ExecutionPhaseKind;
154
- installSources: ProtocolV2InstallSource[];
155
- resourceSources: ProtocolV2ResourceBundleSource[];
156
- };
157
-
158
139
  type ProtocolV2OkppHeader = {
159
140
  type: string;
160
141
  version: IVersionArray;
@@ -392,7 +373,7 @@ export const assertProtocolV2ReconnectIdentity = (
392
373
  if (!expectedSerialNumber || !actualSerialNumber) {
393
374
  throw ERRORS.TypedError(
394
375
  HardwareErrorCode.DeviceNotFound,
395
- 'Protocol V2 reconnect physical identity unavailable'
376
+ 'Protocol V2 reconnect physical identity is unavailable'
396
377
  );
397
378
  }
398
379
  if (actualSerialNumber !== expectedSerialNumber) {
@@ -414,14 +395,6 @@ export const assertProtocolV2ReconnectIdentity = (
414
395
  export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareUpdateV4Params> {
415
396
  private protocolV2ExpectedSerialNumber?: string;
416
397
 
417
- private protocolV2PreparedSources: FirmwareByteSource[] = [];
418
-
419
- private protocolV2ExecutionInLoader = false;
420
-
421
- private protocolV2CompletedTargetVersions = new Map<number, number>();
422
-
423
- private protocolV2LatestFinalFeatures?: Features;
424
-
425
398
  init() {
426
399
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
427
400
  this.requireDeviceMode = [];
@@ -459,69 +432,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
459
432
  { name: 'firmwareType', type: 'string' },
460
433
  { name: 'targetsToUpdate', type: 'array', allowEmpty: true },
461
434
  { name: 'platform', type: 'string' },
462
- { name: 'expectedDeviceId', type: 'string' },
463
435
  { name: 'resourceBundleFiles', type: 'array', allowEmpty: true },
464
436
  ]);
465
- if (
466
- payload.expectedDeviceId !== undefined &&
467
- (payload.expectedDeviceId.length === 0 || payload.expectedDeviceId.length > 160)
468
- ) {
469
- throw ERRORS.TypedError(
470
- HardwareErrorCode.CallMethodInvalidParameter,
471
- 'Protocol V2 expected device identity is invalid'
472
- );
473
- }
474
- const hostBinding =
475
- payload.preparedPlan || payload.componentArtifacts || payload.resourceBundleArtifacts
476
- ? resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration)
477
- : undefined;
478
- if (hostBinding) {
479
- if (!payload.expectedDeviceId) {
480
- throw ERRORS.TypedError(
481
- HardwareErrorCode.CallMethodInvalidParameter,
482
- 'Protocol V2 prepared update requires the expected device identity'
483
- );
484
- }
485
- assertFirmwareUpdatePreparedPlanBinding({
486
- preparedPlan: payload.preparedPlan,
487
- executor: 'v4',
488
- platform: payload.platform,
489
- deviceIdentity: payload.expectedDeviceId,
490
- scopeTargets: [
491
- 'boot',
492
- 'app_v1',
493
- 'app_v2',
494
- 'coprocessor',
495
- 'resource',
496
- 'se01',
497
- 'se02',
498
- 'se03',
499
- 'se04',
500
- ],
501
- bindings: [
502
- ...Object.entries(payload.componentArtifacts ?? {}).flatMap(([target, artifact]) =>
503
- artifact
504
- ? [
505
- {
506
- target: target as Exclude<FirmwareUpdateV4Target, 'resource'>,
507
- artifact,
508
- },
509
- ]
510
- : []
511
- ),
512
- ...(payload.resourceBundleArtifacts ?? []).map(
513
- (entry: { name: string; artifact: FirmwareArtifactReference }) => ({
514
- target: 'resource' as const,
515
- logicalName: entry.name,
516
- artifact: entry.artifact,
517
- })
518
- ),
519
- ],
520
- });
521
- }
522
437
 
523
438
  this.params = {
524
- preparedPlan: payload.preparedPlan,
525
439
  chunkSize: payload.chunkSize,
526
440
  forcedUpdateRes: payload.forcedUpdateRes,
527
441
  bootloaderBinary: payload.bootloaderBinary,
@@ -536,22 +450,21 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
536
450
  resourceBundleFiles: payload.resourceBundleFiles,
537
451
  firmwareType: payload.firmwareType,
538
452
  targetsToUpdate: payload.targetsToUpdate,
539
- expectedTargetVersions: payload.expectedTargetVersions,
540
453
  platform: payload.platform,
541
- expectedDeviceId: payload.expectedDeviceId,
542
- artifactReader: hostBinding?.artifactReader ?? payload.artifactReader,
543
- componentArtifacts: payload.componentArtifacts,
544
- resourceBundleArtifacts: payload.resourceBundleArtifacts,
545
454
  };
546
455
  }
547
456
 
548
- private getProtocolV2FirmwareChunkSize() {
457
+ private getProtocolV2FirmwareChunkSize(direction: 'read' | 'write' = 'write') {
549
458
  const payloadChunkSize = Number(this.params?.chunkSize);
550
459
  const env = DataManager.getSettings('env');
551
- const maxChunkSize =
552
- this.params?.platform === 'native' || (env && DataManager.isBleConnect(env))
553
- ? PROTOCOL_V2_BLE_FILE_CHUNK_SIZE
554
- : PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
460
+ const isBle = this.params?.platform === 'native' || (env && DataManager.isBleConnect(env));
461
+ let maxChunkSize = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
462
+ if (isBle) {
463
+ maxChunkSize =
464
+ direction === 'read'
465
+ ? PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE
466
+ : PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
467
+ }
555
468
  if (!Number.isFinite(payloadChunkSize) || payloadChunkSize <= 0) {
556
469
  return maxChunkSize;
557
470
  }
@@ -578,14 +491,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
578
491
  const deviceFeatures = await this.getProtocolV2DeviceFeatures();
579
492
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
580
493
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
581
- this.validateExpectedTargetVersions();
582
-
583
- if (
584
- (this.params.componentArtifacts && Object.keys(this.params.componentArtifacts).length > 0) ||
585
- this.params.resourceBundleArtifacts?.length
586
- ) {
587
- return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
588
- }
589
494
 
590
495
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
591
496
  let bootloaderBinary: ArrayBuffer | null = null;
@@ -596,18 +501,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
596
501
  fwBinaryMap = this.collectExplicitTargetBinaries();
597
502
  bootloaderBinary = this.prepareBootloaderBinary();
598
503
  if (!this.hasExplicitProtocolV2Payload(fwBinaryMap)) {
599
- if (
600
- this.params.artifactReader ||
601
- DataManager.getSettings('firmwareManifestMode') === 'external-only'
602
- ) {
603
- throw ERRORS.TypedError(
604
- HardwareErrorCode.RuntimeError,
605
- 'Protocol V2 firmware artifacts must be prepared by the external firmware host',
606
- {
607
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
608
- }
609
- );
610
- }
611
504
  const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
612
505
  firmwareType,
613
506
  deviceFeatures
@@ -616,13 +509,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
616
509
  fwBinaryMap = remoteBinaries.fwBinaryMap;
617
510
  installItems = remoteBinaries.installItems;
618
511
  }
619
- const resourceDescriptors = this.prepareProtocolV2ResourceBundles(
620
- firmwareType,
621
- deviceFeatures
622
- );
623
- resourceBundles = resourceDescriptors?.length
624
- ? await this.materializeProtocolV2ResourceBundles(resourceDescriptors)
625
- : undefined;
512
+ resourceBundles = this.prepareProtocolV2ResourceBundles(firmwareType, deviceFeatures);
513
+ if (resourceBundles?.length) {
514
+ resourceBundles = await this.prefetchProtocolV2ResourceBundles(resourceBundles);
515
+ }
626
516
  this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
627
517
  } catch (err) {
628
518
  throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
@@ -635,280 +525,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
635
525
  );
636
526
  }
637
527
 
638
- return this.executeProtocolV2Update({
528
+ await this.enterProtocolV2BootloaderMode();
529
+
530
+ await this.executeProtocolV2Update({
639
531
  fwBinaryMap,
640
532
  bootloaderBinary,
641
533
  ...(installItems ? { installItems } : undefined),
642
534
  ...(resourceBundles?.length ? { resourceBundles } : undefined),
643
535
  });
644
- }
645
-
646
- private async openProtocolV2PreparedSource(artifact: FirmwareArtifactReference) {
647
- const source = await openFirmwareByteSource({
648
- artifact,
649
- reader: this.params.artifactReader,
650
- });
651
- if (!source) {
652
- throw ERRORS.TypedError(
653
- HardwareErrorCode.RuntimeError,
654
- `Firmware artifact ${artifact.artifactRef} is not prepared`,
655
- {
656
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
657
- }
658
- );
659
- }
660
- this.protocolV2PreparedSources.push(source);
661
- return source;
662
- }
663
536
 
664
- private async openProtocolV2MemorySource(binary: ArrayBuffer) {
665
- const source = await openFirmwareByteSource({ binary });
666
- if (!source) {
667
- throw ERRORS.TypedError(
668
- HardwareErrorCode.RuntimeError,
669
- 'Protocol V2 firmware binary is empty'
670
- );
671
- }
672
- this.protocolV2PreparedSources.push(source);
673
- return source;
674
- }
537
+ await this.exitProtocolV2BootloaderToNormal();
675
538
 
676
- private async closeProtocolV2PreparedSources() {
677
- const sources = this.protocolV2PreparedSources.splice(0);
678
- await Promise.all(sources.map(source => source.close().catch(() => undefined)));
679
- }
680
-
681
- private async prepareProtocolV2InstallSources(
682
- firmwareType: EFirmwareType,
683
- features: Features
684
- ): Promise<ProtocolV2InstallSource[]> {
685
- const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
686
- if (!release) {
687
- throw ERRORS.TypedError(
688
- HardwareErrorCode.RuntimeError,
689
- 'Protocol V2 firmware release is unavailable'
690
- );
691
- }
692
- const componentArtifacts = this.params.componentArtifacts ?? {};
693
- const requestedTargets = new Set(
694
- this.params.targetsToUpdate ?? (Object.keys(componentArtifacts) as FirmwareUpdateV4Target[])
695
- );
696
- const installSources: ProtocolV2InstallSource[] = [];
697
- const preparedTargets = new Set<FirmwareUpdateV4Target>();
698
-
699
- for (const [key, component] of this.getRemoteComponentEntries(release)) {
700
- const target = this.getRemoteComponentTarget(key, component);
701
- const updateTarget = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId);
702
- if (updateTarget && updateTarget !== 'resource' && requestedTargets.has(updateTarget)) {
703
- const artifact = componentArtifacts[updateTarget];
704
- if (!artifact) {
705
- throw ERRORS.TypedError(
706
- HardwareErrorCode.RuntimeError,
707
- `Protocol V2 ${updateTarget} artifact is not prepared`,
708
- {
709
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
710
- artifactName: updateTarget,
711
- }
712
- );
713
- }
714
- installSources.push({
715
- ...target,
716
- source: await this.openProtocolV2PreparedSource(artifact),
717
- });
718
- preparedTargets.add(updateTarget);
719
- }
720
- }
721
-
722
- const unknownTarget = Array.from(requestedTargets).find(
723
- target => target !== 'resource' && !preparedTargets.has(target)
724
- );
725
- if (unknownTarget) {
726
- throw ERRORS.TypedError(
727
- HardwareErrorCode.RuntimeError,
728
- `Protocol V2 release does not contain requested target ${unknownTarget}`
729
- );
730
- }
731
- return installSources;
732
- }
733
-
734
- private async prepareProtocolV2ResourceSources(
735
- firmwareType: EFirmwareType,
736
- features: Features
737
- ): Promise<ProtocolV2ResourceBundleSource[]> {
738
- const preparedArtifacts = this.params.resourceBundleArtifacts ?? [];
739
- const resourceRequested =
740
- this.params.targetsToUpdate?.includes('resource') || preparedArtifacts.length > 0;
741
- if (!resourceRequested) {
742
- return [];
743
- }
744
- const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
745
- const descriptors = release?.resourceBundles ?? [];
746
- const artifactByName = new Map(
747
- preparedArtifacts.map(item => [item.name, item.artifact] as const)
748
- );
749
- const sources: ProtocolV2ResourceBundleSource[] = [];
750
- for (const descriptor of descriptors) {
751
- const artifact = artifactByName.get(descriptor.name);
752
- if (!artifact) {
753
- throw ERRORS.TypedError(
754
- HardwareErrorCode.RuntimeError,
755
- `Protocol V2 resource bundle ${descriptor.name} is not prepared`,
756
- {
757
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
758
- artifactName: descriptor.name,
759
- }
760
- );
761
- }
762
- sources.push({
763
- name: descriptor.name,
764
- source: await this.openProtocolV2PreparedSource(artifact),
765
- devicePath: descriptor.devicePath,
766
- version: descriptor.version,
767
- payloadHash: descriptor.payloadHash,
768
- headerHash: descriptor.headerHash,
769
- });
770
- artifactByName.delete(descriptor.name);
771
- }
772
- if (artifactByName.size > 0) {
773
- throw ERRORS.TypedError(
774
- HardwareErrorCode.RuntimeError,
775
- `Protocol V2 release does not contain resource bundle ${artifactByName.keys().next().value}`
776
- );
777
- }
778
- return sources;
779
- }
780
-
781
- private async runProtocolV2PreparedArtifacts(features: Features, firmwareType: EFirmwareType) {
782
- try {
783
- this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
784
- const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
785
- const resourceSources = await this.prepareProtocolV2ResourceSources(firmwareType, features);
786
- if (installSources.length === 0 && resourceSources.length === 0) {
787
- throw ERRORS.TypedError(
788
- HardwareErrorCode.FirmwareUpdateDownloadFailed,
789
- 'No firmware to update'
790
- );
791
- }
792
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
793
-
794
- return await this.executeProtocolV2SourceUpdate({
795
- installSources,
796
- resourceSources,
797
- });
798
- } finally {
799
- await this.closeProtocolV2PreparedSources();
800
- }
801
- }
802
-
803
- private validateExpectedTargetVersions() {
804
- const expected = this.params.expectedTargetVersions;
805
- if (expected === undefined) return;
806
- if (typeof expected !== 'object' || expected === null || Array.isArray(expected)) {
807
- throw ERRORS.TypedError(
808
- HardwareErrorCode.RuntimeError,
809
- 'Protocol V2 expected target versions are invalid'
810
- );
811
- }
812
- for (const [target, version] of Object.entries(expected)) {
813
- if (
814
- !Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.values()).includes(
815
- target as FirmwareUpdateV4Target
816
- ) ||
817
- typeof version !== 'string' ||
818
- !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(version) ||
819
- version.length > 64
820
- ) {
821
- throw ERRORS.TypedError(
822
- HardwareErrorCode.RuntimeError,
823
- 'Protocol V2 expected target version is invalid'
824
- );
825
- }
826
- }
827
- }
828
-
829
- private getExpectedProtocolV2TargetVersion(targetId: number) {
830
- const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(targetId);
831
- const version = target ? this.params?.expectedTargetVersions?.[target] : undefined;
832
- if (!version) return undefined;
833
- const parts = version.split(/[+-]/u, 1)[0].split('.').map(Number);
834
- if (
835
- parts.length !== 3 ||
836
- parts.some(part => !Number.isSafeInteger(part) || part < 0 || part > 0xff)
837
- ) {
838
- return undefined;
839
- }
840
- return parts[0] * 0x10000 + parts[1] * 0x100 + parts[2];
841
- }
842
-
843
- private assertExpectedProtocolV2Versions(targets?: readonly FirmwareUpdateV4Target[]) {
844
- const expected = this.params?.expectedTargetVersions;
845
- if (!expected) return;
846
- const features = this.protocolV2LatestFinalFeatures;
847
- const requestedTargets = this.getProtocolV2RequestedTargets();
848
- const applicationTargets = requestedTargets.filter(
849
- target => target === 'app_v1' || target === 'app_v2'
850
- );
851
- const visibleVersions: Partial<Record<FirmwareUpdateV4Target, string>> = {
852
- boot: features ? getDeviceBootloaderVersion(features).join('.') : undefined,
853
- coprocessor: features ? getDeviceBLEFirmwareVersion(features).join('.') : undefined,
854
- se01: features?.se01Version ?? undefined,
855
- se02: features?.se02Version ?? undefined,
856
- se03: features?.se03Version ?? undefined,
857
- se04: features?.se04Version ?? undefined,
858
- };
859
- if (features && applicationTargets.length === 1) {
860
- visibleVersions[applicationTargets[0]] = getDeviceFirmwareVersion(features).join('.');
861
- }
862
- const expectedEntries = (
863
- Object.entries(expected) as Array<[FirmwareUpdateV4Target, string]>
864
- ).filter(([target]) => !targets || targets.includes(target));
865
- for (const [target, expectedVersion] of expectedEntries) {
866
- const targetId = Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.entries()).find(
867
- ([, mappedTarget]) => mappedTarget === target
868
- )?.[0];
869
- const statusVersion =
870
- targetId === undefined ? undefined : this.protocolV2CompletedTargetVersions.get(targetId);
871
- const observedVersion =
872
- statusVersion === undefined
873
- ? visibleVersions[target]
874
- : `${Math.floor(statusVersion / 0x10000) % 0x100}.${
875
- Math.floor(statusVersion / 0x100) % 0x100
876
- }.${statusVersion % 0x100}`;
877
- if (!observedVersion) {
878
- throw ERRORS.TypedError(
879
- HardwareErrorCode.RuntimeError,
880
- `Protocol V2 target ${target} has no observable final version`
881
- );
882
- }
883
- if (observedVersion !== expectedVersion) {
884
- throw ERRORS.TypedError(
885
- HardwareErrorCode.FirmwareVerificationFailed,
886
- `Protocol V2 target ${target} reached ${observedVersion}, expected ${expectedVersion}`
887
- );
888
- }
889
- }
890
- }
539
+ const versions = await this.waitForProtocolV2FinalFeatures();
540
+ this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
541
+ DevicePool.resetState();
891
542
 
892
- private getProtocolV2RequestedTargets(): FirmwareUpdateV4Target[] {
893
- if (this.params.targetsToUpdate?.length) {
894
- return [...new Set(this.params.targetsToUpdate)];
895
- }
896
- const targets = new Set<FirmwareUpdateV4Target>();
897
- Object.keys(this.params.componentArtifacts ?? {}).forEach(target =>
898
- targets.add(target as FirmwareUpdateV4Target)
899
- );
900
- if (this.params.resourceBundleArtifacts?.length || this.params.resourceBundleFiles?.length) {
901
- targets.add('resource');
902
- }
903
- if (this.params.bootloaderBinary) targets.add('boot');
904
- if (this.params.applicationP1Binary) targets.add('app_v1');
905
- if (this.params.applicationP2Binary) targets.add('app_v2');
906
- if (this.params.coprocessorBinary) targets.add('coprocessor');
907
- if (this.params.se01Binary) targets.add('se01');
908
- if (this.params.se02Binary) targets.add('se02');
909
- if (this.params.se03Binary) targets.add('se03');
910
- if (this.params.se04Binary) targets.add('se04');
911
- return Array.from(targets);
543
+ return versions;
912
544
  }
913
545
 
914
546
  private async getProtocolV2DeviceFeatures(): Promise<Features> {
@@ -1045,7 +677,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1045
677
  return null;
1046
678
  }
1047
679
 
1048
- const chunkSize = this.getProtocolV2FirmwareChunkSize();
680
+ const chunkSize = this.getProtocolV2FirmwareChunkSize('read');
1049
681
  const chunks: Uint8Array[] = [];
1050
682
  let offset = 0;
1051
683
  while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) {
@@ -1197,47 +829,98 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1197
829
  })) as ProtocolV2ResourceBundleBinary[];
1198
830
  }
1199
831
 
1200
- private async materializeProtocolV2ResourceBundles(
832
+ private async prefetchProtocolV2ResourceBundles(
1201
833
  bundles: ProtocolV2ResourceBundleBinary[]
1202
834
  ): Promise<ProtocolV2ResourceBundleBinary[]> {
835
+ const prefetched: ProtocolV2ResourceBundleBinary[] = [];
1203
836
  for (const bundle of bundles) {
1204
- if (bundle.binary.byteLength === 0 && bundle.url) {
1205
- if (
1206
- this.params.artifactReader ||
1207
- DataManager.getSettings('firmwareManifestMode') === 'external-only'
1208
- ) {
1209
- throw ERRORS.TypedError(
1210
- HardwareErrorCode.RuntimeError,
1211
- `Firmware resource bundle ${bundle.name} is not prepared`,
1212
- {
1213
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
1214
- artifactName: bundle.name,
1215
- }
1216
- );
837
+ let { binary } = bundle;
838
+ if (binary.byteLength === 0) {
839
+ if (!bundle.url) {
840
+ throw new Error(`Missing Protocol V2 RESC bundle binary: ${bundle.name}`);
1217
841
  }
1218
- Log.log(`[FirmwareUpdateV4] preloading RESC bundle ${bundle.name}`);
1219
- bundle.binary = (await getSysResourceBinary(bundle.url)).binary;
842
+ Log.log(`[FirmwareUpdateV4] downloading remote RESC bundle ${bundle.name}`);
843
+ ({ binary } = await getSysResourceBinary(bundle.url));
1220
844
  }
1221
- if (bundle.binary.byteLength === 0) {
1222
- throw ERRORS.TypedError(
1223
- HardwareErrorCode.FirmwareUpdateDownloadFailed,
1224
- `Protocol V2 RESC bundle is empty: ${bundle.name}`
1225
- );
845
+ if (binary.byteLength === 0) {
846
+ throw new Error(`Protocol V2 RESC bundle is empty: ${bundle.name}`);
1226
847
  }
848
+ prefetched.push({ ...bundle, binary });
1227
849
  }
1228
- return bundles;
850
+ return prefetched;
851
+ }
852
+
853
+ /**
854
+ * Synchronize RESC bundles to the device.
855
+ *
856
+ * Manual mode writes supplied binaries directly.
857
+ * Remote-config mode uses prefetched binaries and skips matching bundles after FileRead.
858
+ */
859
+ private async syncProtocolV2ResourceBundles(
860
+ bundles: ProtocolV2ResourceBundleBinary[],
861
+ firmwareSize: number
862
+ ): Promise<{ processedSize: number; totalSize: number }> {
863
+ const transferStartTime = Date.now();
864
+ const transferTransport = this.getProtocolV2FirmwareTransferTransport();
865
+
866
+ const isRemoteMode = bundles.some(bundle => !!bundle.url);
867
+ let bundlesToSync = bundles;
868
+
869
+ // Remote binaries were prefetched before bootloader entry. Only device-local
870
+ // header comparison and transfer are allowed in this phase.
871
+ if (isRemoteMode) {
872
+ const filtered: ProtocolV2ResourceBundleBinary[] = [];
873
+ for (const bundle of bundles) {
874
+ // Compare the existing device header.
875
+ const upToDate = await this.isProtocolV2ResourceBundleUpToDate(bundle);
876
+ if (upToDate) {
877
+ Log.log(`[FirmwareUpdateV4] skip RESC bundle ${bundle.name}; already up to date`);
878
+ } else {
879
+ filtered.push(bundle);
880
+ }
881
+ }
882
+ bundlesToSync = filtered;
883
+ }
884
+
885
+ if (bundlesToSync.length === 0) {
886
+ Log.log('[FirmwareUpdateV4] all RESC bundles up to date, nothing to sync');
887
+ return { processedSize: 0, totalSize: firmwareSize };
888
+ }
889
+
890
+ // Write directly to the device through FileWrite.
891
+ let totalSize = 0;
892
+ for (const b of bundlesToSync) totalSize += b.binary.byteLength;
893
+
894
+ const transferTotalSize = totalSize + firmwareSize;
895
+ let processedSize = 0;
896
+ for (const bundle of bundlesToSync) {
897
+ Log.log(
898
+ `[FirmwareUpdateV4] syncing RESC bundle ${bundle.name} -> ${bundle.devicePath} bytes=${bundle.binary.byteLength}`
899
+ );
900
+ processedSize = await this.protocolV2CommonUpdateProcess({
901
+ payload: bundle.binary,
902
+ filePath: bundle.devicePath,
903
+ processedSize,
904
+ totalSize: transferTotalSize,
905
+ });
906
+ }
907
+
908
+ const elapsedMs = Date.now() - transferStartTime;
909
+ Log.log(
910
+ `[FirmwareUpdateV4] RESC bundle sync finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
911
+ elapsedMs / 1000
912
+ ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
913
+ );
914
+ return { processedSize, totalSize: transferTotalSize };
1229
915
  }
1230
916
 
1231
917
  /**
1232
918
  * Compare the existing okpkg OKPP header in remote-config mode.
1233
919
  */
1234
920
  private async isProtocolV2ResourceBundleUpToDate(
1235
- bundle: Pick<
1236
- ProtocolV2ResourceBundleBinary,
1237
- 'name' | 'devicePath' | 'version' | 'payloadHash' | 'headerHash'
1238
- >
921
+ bundle: ProtocolV2ResourceBundleBinary
1239
922
  ): Promise<boolean> {
1240
- if (this.params?.forcedUpdateRes) return false;
923
+ if (this.params.forcedUpdateRes) return false;
1241
924
  if (!bundle.version && !bundle.payloadHash) return false;
1242
925
 
1243
926
  try {
@@ -1275,7 +958,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1275
958
  // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
1276
959
  if (this.isProtocolV2BootloaderMode() || this.device.features?.mode === 'romloader') {
1277
960
  Log.debug('Protocol V2 device is already in loader mode, skip reboot to bootloader');
1278
- this.protocolV2ExecutionInLoader = true;
1279
961
  return false;
1280
962
  }
1281
963
 
@@ -1285,7 +967,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1285
967
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1286
968
  await wait(1000);
1287
969
  await this.waitForProtocolV2BootloaderMode();
1288
- this.protocolV2ExecutionInLoader = true;
1289
970
  return true;
1290
971
  } catch (error) {
1291
972
  if (error instanceof HardwareError) {
@@ -1374,98 +1055,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1374
1055
  return entries;
1375
1056
  }
1376
1057
 
1377
- private buildProtocolV2ExecutionPhases({
1378
- installSources,
1379
- resourceSources,
1380
- }: {
1381
- installSources: ProtocolV2InstallSource[];
1382
- resourceSources: ProtocolV2ResourceBundleSource[];
1383
- }): ProtocolV2ExecutionPhase[] {
1384
- const phases: ProtocolV2ExecutionPhase[] = [];
1385
- if (resourceSources.length > 0) {
1386
- phases.push({
1387
- kind: 'resource-sync',
1388
- installSources: [],
1389
- resourceSources,
1390
- });
1391
- }
1392
- const bootloaderSources = installSources.filter(source => source.kind === 'bootloader');
1393
- if (bootloaderSources.length > 0) {
1394
- phases.push(
1395
- {
1396
- kind: 'bootloader-install',
1397
- installSources: bootloaderSources,
1398
- resourceSources: [],
1399
- },
1400
- {
1401
- kind: 'bootloader-verify',
1402
- installSources: [],
1403
- resourceSources: [],
1404
- }
1405
- );
1406
- }
1407
- const componentSources = installSources.filter(source => source.kind !== 'bootloader');
1408
- if (componentSources.length > 0) {
1409
- phases.push({
1410
- kind: 'component-install',
1411
- installSources: componentSources,
1412
- resourceSources: [],
1413
- });
1414
- }
1415
- phases.push({
1416
- kind: 'final-verify',
1417
- installSources: [],
1418
- resourceSources: [],
1419
- });
1420
- return phases;
1421
- }
1422
-
1423
- private async executeProtocolV2Phases({
1424
- installSources,
1425
- resourceSources,
1426
- }: {
1427
- installSources: ProtocolV2InstallSource[];
1428
- resourceSources: ProtocolV2ResourceBundleSource[];
1429
- }) {
1430
- const phases = this.buildProtocolV2ExecutionPhases({
1431
- installSources,
1432
- resourceSources,
1433
- });
1434
- for (const phase of phases) {
1435
- if (phase.kind === 'final-verify') {
1436
- return this.completeProtocolV2FinalVerification();
1437
- }
1438
- if (phase.kind === 'resource-sync') {
1439
- await this.enterProtocolV2BootloaderMode();
1440
- await this.executeProtocolV2TransferPhase(phase);
1441
- } else if (phase.kind === 'bootloader-install' || phase.kind === 'component-install') {
1442
- await this.enterProtocolV2BootloaderMode();
1443
- await this.executeProtocolV2TransferPhase(phase);
1444
- await this.exitProtocolV2BootloaderToNormal();
1445
- } else if (phase.kind === 'bootloader-verify') {
1446
- await this.waitForProtocolV2FinalFeatures();
1447
- this.assertExpectedProtocolV2Versions(['boot']);
1448
- }
1449
- }
1450
- throw ERRORS.TypedError(
1451
- HardwareErrorCode.RuntimeError,
1452
- 'Protocol V2 execution has no final verification phase'
1453
- );
1454
- }
1455
-
1456
- private async executeProtocolV2SourceUpdate({
1457
- installSources,
1458
- resourceSources,
1459
- }: {
1460
- installSources: ProtocolV2InstallSource[];
1461
- resourceSources: ProtocolV2ResourceBundleSource[];
1462
- }) {
1463
- return this.executeProtocolV2Phases({
1464
- installSources,
1465
- resourceSources,
1466
- });
1467
- }
1468
-
1469
1058
  private async executeProtocolV2Update({
1470
1059
  fwBinaryMap,
1471
1060
  bootloaderBinary,
@@ -1477,158 +1066,104 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1477
1066
  installItems?: ProtocolV2InstallItem[];
1478
1067
  resourceBundles?: ProtocolV2ResourceBundleBinary[];
1479
1068
  }) {
1480
- const memoryInstallItems =
1069
+ const orderedInstallItems =
1481
1070
  installItems ??
1482
1071
  this.buildProtocolV2InstallItems({
1483
- fwBinaryMap: fwBinaryMap ?? [],
1484
1072
  bootloaderBinary: bootloaderBinary ?? null,
1073
+ fwBinaryMap: fwBinaryMap ?? [],
1485
1074
  });
1486
- try {
1487
- const installSources = await Promise.all(
1488
- memoryInstallItems.map(async item => ({
1489
- fileName: item.fileName,
1490
- source: await this.openProtocolV2MemorySource(item.binary),
1491
- targetId: item.targetId,
1492
- kind: item.kind,
1493
- }))
1494
- );
1495
- const resourceSources = await Promise.all(
1496
- (resourceBundles ?? []).map(async bundle => ({
1497
- name: bundle.name,
1498
- source: await this.openProtocolV2MemorySource(bundle.binary),
1499
- devicePath: bundle.devicePath,
1500
- version: bundle.version,
1501
- payloadHash: bundle.payloadHash,
1502
- headerHash: bundle.headerHash,
1503
- }))
1504
- );
1505
- return await this.executeProtocolV2Phases({
1506
- installSources,
1507
- resourceSources,
1508
- });
1509
- } finally {
1510
- await this.closeProtocolV2PreparedSources();
1511
- }
1512
- }
1513
1075
 
1514
- private async executeProtocolV2TransferPhase({
1515
- installSources,
1516
- resourceSources,
1517
- }: ProtocolV2ExecutionPhase) {
1518
- let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
1519
- const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
1520
- for (const resource of resourceSources) {
1521
- if (await this.isProtocolV2ResourceBundleUpToDate(resource)) {
1522
- Log.log(`[FirmwareUpdateV4] skip RESC bundle ${resource.name}; already up to date`);
1523
- } else {
1524
- resourcesToSync.push(resource);
1525
- totalSize += resource.source.size;
1526
- }
1527
- }
1076
+ let firmwareSize = 0;
1077
+ for (const item of orderedInstallItems) firmwareSize += item.binary.byteLength;
1528
1078
 
1529
1079
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
1080
+
1530
1081
  let processedSize = 0;
1531
- for (const resource of resourcesToSync) {
1532
- processedSize = await this.protocolV2SourceUpdateProcess({
1533
- source: resource.source,
1534
- filePath: resource.devicePath,
1535
- processedSize,
1536
- totalSize,
1537
- });
1538
- await this.verifyProtocolV2StagedFile(resource.devicePath, resource.source.size);
1082
+ let totalSize = firmwareSize;
1083
+ if (resourceBundles?.length) {
1084
+ const resourceTransfer = await this.syncProtocolV2ResourceBundles(
1085
+ resourceBundles,
1086
+ firmwareSize
1087
+ );
1088
+ processedSize = resourceTransfer.processedSize;
1089
+ totalSize = resourceTransfer.totalSize;
1539
1090
  }
1540
1091
 
1541
- const stagedInstallTargets: Array<{ targetId: number; path: string }> = [];
1542
- for (const item of installSources) {
1543
- const filePath = this.getProtocolV2InstallItemStagingPath(item);
1544
- processedSize = await this.protocolV2SourceUpdateProcess({
1545
- source: item.source,
1546
- filePath,
1547
- processedSize,
1548
- totalSize,
1549
- });
1550
- await this.verifyProtocolV2StagedFile(filePath, item.source.size);
1551
- stagedInstallTargets.push({
1552
- targetId: item.targetId,
1553
- path: filePath,
1554
- });
1092
+ // Skip staging and installation when the update contains RESC bundles only.
1093
+ if (orderedInstallItems.length === 0) {
1094
+ Log.log('[FirmwareUpdateV4] no firmware targets to install (RESC bundles only)');
1095
+ if (totalSize > 0) {
1096
+ this.postProgressMessage(100, 'transferData');
1097
+ }
1098
+ return;
1555
1099
  }
1100
+ let transferredSize = processedSize;
1101
+ const transferStartTime = Date.now();
1102
+ const transferTransport = this.getProtocolV2FirmwareTransferTransport();
1103
+ const chunkSize = this.getProtocolV2FirmwareChunkSize();
1104
+ const onTransferredBytes = (bytes: number) => {
1105
+ transferredSize = bytes;
1106
+ };
1107
+ Log.log(
1108
+ `[FirmwareUpdateV4] transfer started transport=${transferTransport} total=${totalSize} bytes chunk=${chunkSize} bytes`
1109
+ );
1556
1110
 
1557
- if (totalSize > 0) {
1558
- this.postProgressMessage(100, 'transferData');
1559
- }
1560
- if (stagedInstallTargets.length === 0) {
1561
- return;
1111
+ const stagedInstallTargets: ProtocolV2InstallTarget[] = [];
1112
+
1113
+ try {
1114
+ for (const item of orderedInstallItems) {
1115
+ const filePath = this.getProtocolV2InstallItemStagingPath(item);
1116
+ Log.log(
1117
+ `[FirmwareUpdateV4] staging ${item.kind} via FilesystemFileWrite target=${item.targetId} path=${filePath} source=${item.fileName} bytes=${item.binary.byteLength}`
1118
+ );
1119
+ processedSize = await this.protocolV2CommonUpdateProcess({
1120
+ payload: item.binary,
1121
+ filePath,
1122
+ processedSize,
1123
+ totalSize,
1124
+ onTransferredBytes,
1125
+ });
1126
+ transferredSize = processedSize;
1127
+ await this.verifyProtocolV2StagedFile(filePath, item.binary.byteLength);
1128
+
1129
+ stagedInstallTargets.push({
1130
+ ...item,
1131
+ path: filePath,
1132
+ });
1133
+ }
1134
+
1135
+ if (totalSize > 0) {
1136
+ this.postProgressMessage(100, 'transferData');
1137
+ }
1138
+
1139
+ const elapsedMs = Date.now() - transferStartTime;
1140
+ Log.log(
1141
+ `[FirmwareUpdateV4] transfer finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
1142
+ elapsedMs / 1000
1143
+ ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
1144
+ );
1145
+ } catch (error) {
1146
+ const elapsedMs = Date.now() - transferStartTime;
1147
+ Log.warn(
1148
+ `[FirmwareUpdateV4] transfer failed transport=${transferTransport} bytes=${transferredSize}/${totalSize} elapsed=${(
1149
+ elapsedMs / 1000
1150
+ ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(transferredSize, elapsedMs)} KB/s`
1151
+ );
1152
+ throw error;
1562
1153
  }
1563
1154
 
1564
1155
  this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
1565
- const targets = stagedInstallTargets.map(item => ({
1156
+
1157
+ const allTargets = stagedInstallTargets.map(item => ({
1566
1158
  target_id: item.targetId,
1567
1159
  path: item.path,
1568
1160
  }));
1569
- await this.protocolV2StartFirmwareUpdate({ targets });
1570
- await this.waitForProtocolV2FirmwareUpdateComplete(targets);
1571
- }
1572
-
1573
- private async protocolV2SourceUpdateProcess({
1574
- source,
1575
- filePath,
1576
- processedSize,
1577
- totalSize,
1578
- }: {
1579
- source: FirmwareByteSource;
1580
- filePath: string;
1581
- processedSize: number;
1582
- totalSize: number;
1583
- }) {
1584
- let lastError: unknown;
1585
- for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1586
- try {
1587
- await writeFirmwareByteSource({
1588
- source,
1589
- chunkSize: this.getProtocolV2FirmwareChunkSize(),
1590
- write: async ({ data, sourceOffset, length, first }) => {
1591
- const chunkEnd = sourceOffset + length;
1592
- const progress = getProtocolV2DeviceTransferProgress(
1593
- processedSize + sourceOffset,
1594
- processedSize + chunkEnd,
1595
- totalSize
1596
- );
1597
- const response = await this.fileWriteChunk(
1598
- filePath,
1599
- source.size,
1600
- sourceOffset,
1601
- data,
1602
- first,
1603
- progress
1604
- );
1605
- const rawProcessedByte = response.message.processed_byte;
1606
- const nextOffset = rawProcessedByte === undefined ? chunkEnd : Number(rawProcessedByte);
1607
- if (!Number.isFinite(nextOffset) || nextOffset !== chunkEnd) {
1608
- throw ERRORS.TypedError(
1609
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1610
- `invalid processed_byte ${rawProcessedByte} for offset ${sourceOffset}`
1611
- );
1612
- }
1613
- this.postProgressMessage(progress, 'transferData');
1614
- return length;
1615
- },
1616
- });
1617
- return processedSize + source.size;
1618
- } catch (error) {
1619
- lastError = error;
1620
- if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1621
- await this.recoverProtocolV2FileTransfer();
1622
- }
1623
- }
1624
- }
1625
- throw ERRORS.TypedError(
1626
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1627
- `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1628
- );
1161
+ Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(allTargets)}`);
1162
+ await this.protocolV2StartFirmwareUpdate({ targets: allTargets });
1163
+ await this.waitForProtocolV2FirmwareUpdateComplete(allTargets);
1629
1164
  }
1630
1165
 
1631
- private getProtocolV2InstallItemStagingPath(item: Pick<ProtocolV2InstallItem, 'fileName'>) {
1166
+ private getProtocolV2InstallItemStagingPath(item: ProtocolV2InstallItem) {
1632
1167
  return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`;
1633
1168
  }
1634
1169
 
@@ -1647,8 +1182,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1647
1182
 
1648
1183
  private assertProtocolV2TargetStatus(
1649
1184
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
1650
- expectedTargetIds: Set<number>,
1651
- expectedPaths = new Map<number, string>()
1185
+ expectedTargetIds: Set<number>
1652
1186
  ) {
1653
1187
  Log.log(
1654
1188
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
@@ -1679,47 +1213,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1679
1213
  const matchingTargets = statusTargets.filter(target =>
1680
1214
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
1681
1215
  );
1682
- const seenTargetIds = new Set<number>();
1683
- for (const target of matchingTargets) {
1684
- const targetId = normalizeProtocolV2TargetId(target.target_id);
1685
- if (
1686
- targetId === undefined ||
1687
- seenTargetIds.has(targetId) ||
1688
- (target.path && expectedPaths.get(targetId) && target.path !== expectedPaths.get(targetId))
1689
- ) {
1690
- throw ERRORS.TypedError(
1691
- HardwareErrorCode.RuntimeError,
1692
- `Protocol V2 install status conflicts with target ${target.target_id}`
1693
- );
1694
- }
1695
- seenTargetIds.add(targetId);
1696
- }
1697
1216
  const completedTargets = matchingTargets.filter(target =>
1698
1217
  isProtocolV2TargetStatusFinished(target.status)
1699
1218
  );
1700
1219
  if (completedTargets.length === expectedTargetIds.size && expectedTargetIds.size > 0) {
1701
- for (const target of completedTargets) {
1702
- const targetId = normalizeProtocolV2TargetId(target.target_id);
1703
- const payloadVersion = toProtocolV2FiniteNumber(target.payload_version);
1704
- if (targetId === undefined) {
1705
- throw ERRORS.TypedError(
1706
- HardwareErrorCode.RuntimeError,
1707
- 'Protocol V2 completed target identity is invalid'
1708
- );
1709
- }
1710
- const expectedVersion = this.getExpectedProtocolV2TargetVersion(targetId);
1711
- if (expectedVersion !== undefined && payloadVersion !== undefined) {
1712
- if (payloadVersion !== expectedVersion) {
1713
- throw ERRORS.TypedError(
1714
- HardwareErrorCode.FirmwareVerificationFailed,
1715
- `Protocol V2 target ${targetId} reached payload version ${payloadVersion}, expected ${expectedVersion}`
1716
- );
1717
- }
1718
- }
1719
- if (payloadVersion !== undefined) {
1720
- this.protocolV2CompletedTargetVersions.set(targetId, payloadVersion);
1721
- }
1722
- }
1723
1220
  this.postProgressMessage(100, 'installingFirmware');
1724
1221
  return true;
1725
1222
  }
@@ -1744,7 +1241,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1744
1241
  targets: Array<{ target_id: number; path: string }>
1745
1242
  ) {
1746
1243
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
1747
- const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
1748
1244
  const startTime = Date.now();
1749
1245
  let lastError: unknown;
1750
1246
 
@@ -1768,8 +1264,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1768
1264
  if (
1769
1265
  this.assertProtocolV2TargetStatus(
1770
1266
  (statusResponse.message.records ?? []) as ProtocolV2FirmwareUpdateStatusTarget[],
1771
- expectedTargetIds,
1772
- expectedPaths
1267
+ expectedTargetIds
1773
1268
  )
1774
1269
  ) {
1775
1270
  return;
@@ -1786,34 +1281,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1786
1281
  // device already rebooted into App, this endpoint error signals that the
1787
1282
  // install phase ended; the later phase performs the final readiness check.
1788
1283
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
1789
- try {
1790
- const deviceInfo = await requestProtocolV2DeviceInfo({
1791
- commands: this.device.getCommands(),
1792
- timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1793
- });
1794
- this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
1795
- const observed = await this.device.probeProtocolV2RuntimeState(
1796
- deviceInfo,
1797
- PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
1798
- );
1799
- if (observed.mode === 'normal' && !observed.bootloaderMode) {
1800
- return;
1801
- }
1802
- lastError = new Error(
1803
- 'Protocol V2 status endpoint is unavailable outside normal mode'
1804
- );
1805
- } catch (probeError) {
1806
- lastError = probeError;
1807
- }
1808
- throw ERRORS.TypedError(
1809
- HardwareErrorCode.RuntimeError,
1810
- `Protocol V2 firmware install status is unavailable: ${this.normalizeErrorMessage(
1811
- lastError
1812
- )}`,
1813
- {
1814
- firmwareUpdateCode: 'FirmwareInstallStatusUnavailable',
1815
- }
1284
+ Log.log(
1285
+ '[FirmwareUpdateV4] firmware status endpoint unavailable after reboot; continue with normal-mode verification'
1816
1286
  );
1287
+ return;
1817
1288
  }
1818
1289
  lastError = error;
1819
1290
  Log.log(
@@ -1829,12 +1300,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1829
1300
  if (error instanceof HardwareError && error.errorCode === HardwareErrorCode.FirmwareError) {
1830
1301
  throw error;
1831
1302
  }
1832
- if (
1833
- error instanceof HardwareError &&
1834
- error.params?.firmwareUpdateCode === 'FirmwareInstallStatusUnavailable'
1835
- ) {
1836
- throw error;
1837
- }
1838
1303
  Log.log('Protocol V2 firmware install device readiness probe failed: ', error);
1839
1304
  }
1840
1305
  await wait(1000);
@@ -1852,7 +1317,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1852
1317
  // The connection may still be in bootloader. Request a Normal reboot directly;
1853
1318
  // repeating it after an automatic App reboot is idempotent.
1854
1319
  await this.protocolV2Reboot(DeviceRebootType.Normal);
1855
- this.protocolV2ExecutionInLoader = false;
1856
1320
  }
1857
1321
 
1858
1322
  private async waitForProtocolV2FinalFeatures() {
@@ -1863,7 +1327,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1863
1327
  const bootloaderVersion = getDeviceBootloaderVersion(features).join('.');
1864
1328
  const bleVersion = getDeviceBLEFirmwareVersion(features).join('.');
1865
1329
  const firmwareVersion = getDeviceFirmwareVersion(features).join('.');
1866
- this.protocolV2LatestFinalFeatures = features;
1867
1330
  if (firmwareVersion === '0.0.0') {
1868
1331
  Log.warn(
1869
1332
  'Protocol V2 firmware update finished but app firmware version is still 0.0.0. This is allowed for Pro2 debug BLE-only update flows.'
@@ -1877,17 +1340,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1877
1340
  };
1878
1341
  }
1879
1342
 
1880
- private async completeProtocolV2FinalVerification() {
1881
- if (this.protocolV2ExecutionInLoader || this.isProtocolV2BootloaderMode()) {
1882
- await this.exitProtocolV2BootloaderToNormal();
1883
- }
1884
- const versions = await this.waitForProtocolV2FinalFeatures();
1885
- this.assertExpectedProtocolV2Versions();
1886
- this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
1887
- DevicePool.resetState();
1888
- return versions;
1889
- }
1890
-
1891
1343
  private async waitForProtocolV2ReconnectAndFeatures(timeout: number) {
1892
1344
  const startTime = Date.now();
1893
1345
  let lastError: unknown;
@@ -1994,6 +1446,96 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1994
1446
  return this.normalizeErrorMessage(error).includes('Protocol V2 reconnect physical identity');
1995
1447
  }
1996
1448
 
1449
+ private async protocolV2CommonUpdateProcess(params: ProtocolV2FileTransferParams) {
1450
+ let lastError: unknown;
1451
+ for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1452
+ try {
1453
+ return await this.protocolV2WriteWholeFile(params);
1454
+ } catch (error) {
1455
+ lastError = error;
1456
+ Log.error(
1457
+ `Protocol V2 file transfer failed path=${params.filePath} attempt=${attempt}/${PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT}; restarting from offset 0`,
1458
+ error
1459
+ );
1460
+ if (attempt === PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1461
+ break;
1462
+ }
1463
+ await this.recoverProtocolV2FileTransfer();
1464
+ }
1465
+ }
1466
+
1467
+ throw ERRORS.TypedError(
1468
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
1469
+ `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1470
+ );
1471
+ }
1472
+
1473
+ private async protocolV2WriteWholeFile({
1474
+ payload,
1475
+ filePath,
1476
+ processedSize,
1477
+ totalSize,
1478
+ onTransferredBytes,
1479
+ }: ProtocolV2FileTransferParams) {
1480
+ const chunkSize = this.getProtocolV2FirmwareChunkSize();
1481
+ let offset = 0;
1482
+ const getUploadProgress = (fileOffset: number) => {
1483
+ if (totalSize !== undefined && processedSize !== undefined) {
1484
+ return Math.min(Math.ceil(((processedSize + fileOffset) / totalSize) * 100), 99);
1485
+ }
1486
+ return Math.min(Math.ceil((fileOffset / payload.byteLength) * 100), 99);
1487
+ };
1488
+
1489
+ while (offset < payload.byteLength) {
1490
+ const chunkEnd = Math.min(offset + chunkSize, payload.byteLength);
1491
+ const chunkLength = chunkEnd - offset;
1492
+ const chunk = payload.slice(offset, chunkEnd);
1493
+ const overwrite = offset === 0;
1494
+ const progress = getProtocolV2DeviceTransferProgress(
1495
+ (processedSize ?? 0) + offset,
1496
+ (processedSize ?? 0) + chunkEnd,
1497
+ totalSize ?? payload.byteLength
1498
+ );
1499
+
1500
+ const writeRes = await this.fileWriteChunk(
1501
+ filePath,
1502
+ payload.byteLength,
1503
+ offset,
1504
+ chunk,
1505
+ overwrite,
1506
+ progress
1507
+ );
1508
+ const rawProcessedByte = writeRes.message.processed_byte;
1509
+ const processedByte = Number(rawProcessedByte);
1510
+ const nextOffset = rawProcessedByte === undefined ? offset + chunkLength : processedByte;
1511
+ if (!Number.isFinite(nextOffset) || nextOffset <= offset || nextOffset > chunkEnd) {
1512
+ throw ERRORS.TypedError(
1513
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
1514
+ `invalid processed_byte ${writeRes.message.processed_byte} for offset ${offset}`
1515
+ );
1516
+ }
1517
+ offset = nextOffset;
1518
+ onTransferredBytes?.((processedSize ?? 0) + offset);
1519
+ this.postProgressMessage(getUploadProgress(offset), 'transferData');
1520
+ }
1521
+
1522
+ return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0;
1523
+ }
1524
+
1525
+ private getProtocolV2FirmwareTransferTransport() {
1526
+ const env = DataManager.getSettings('env');
1527
+ if (env && DataManager.isBleConnect(env)) {
1528
+ return 'BLE';
1529
+ }
1530
+ if (
1531
+ env &&
1532
+ (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'web')
1533
+ ) {
1534
+ return 'WebUSB';
1535
+ }
1536
+ return env ?? 'unknown';
1537
+ }
1538
+
1997
1539
  private async fileWriteChunk(
1998
1540
  filePath: string,
1999
1541
  totalFileSize: number,