@onekeyfe/hd-core 1.2.0-alpha.75 → 1.2.0-alpha.76

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 (111) hide show
  1. package/__tests__/connectSettings.test.ts +45 -5
  2. package/__tests__/device-initialize-timeout.test.ts +56 -0
  3. package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +87 -0
  5. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  6. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  7. package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
  8. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
  9. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  10. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +394 -0
  11. package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
  12. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +231 -0
  13. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +59 -31
  14. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +64 -0
  15. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  16. package/__tests__/protocol-v2.test.ts +405 -100
  17. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  18. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  19. package/dist/api/FirmwareUpdateV2.d.ts +10 -0
  20. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  21. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  22. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdateV4.d.ts +25 -5
  24. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  25. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  26. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  27. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  28. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  29. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  30. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  31. package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
  32. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
  33. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +8 -0
  34. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  35. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  36. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  37. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
  38. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  39. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
  40. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  41. package/dist/api/firmware/getBinary.d.ts +7 -0
  42. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  43. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  44. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  45. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  46. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  47. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  48. package/dist/data-manager/DataManager.d.ts +1 -0
  49. package/dist/data-manager/DataManager.d.ts.map +1 -1
  50. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  51. package/dist/device/Device.d.ts +1 -0
  52. package/dist/device/Device.d.ts.map +1 -1
  53. package/dist/index.d.ts +179 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3079 -1062
  56. package/dist/inject.d.ts.map +1 -1
  57. package/dist/lowLevelInject.d.ts.map +1 -1
  58. package/dist/topLevelInject.d.ts.map +1 -1
  59. package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -0
  60. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  61. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  62. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  63. package/dist/types/api/export.d.ts +4 -1
  64. package/dist/types/api/export.d.ts.map +1 -1
  65. package/dist/types/api/firmwareUpdate.d.ts +68 -0
  66. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  67. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  68. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  69. package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
  70. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  71. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  72. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  73. package/dist/types/api/index.d.ts +9 -1
  74. package/dist/types/api/index.d.ts.map +1 -1
  75. package/dist/types/settings.d.ts +13 -0
  76. package/dist/types/settings.d.ts.map +1 -1
  77. package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
  78. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  79. package/package.json +4 -4
  80. package/src/api/CheckAllFirmwareRelease.ts +57 -6
  81. package/src/api/FirmwareUpdate.ts +13 -1
  82. package/src/api/FirmwareUpdateV2.ts +296 -121
  83. package/src/api/FirmwareUpdateV3.ts +263 -57
  84. package/src/api/FirmwareUpdateV4.ts +881 -349
  85. package/src/api/device/DeviceUpdateBootloader.ts +125 -6
  86. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  87. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  88. package/src/api/firmware/FirmwarePreparationError.ts +12 -0
  89. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +72 -1
  90. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  91. package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
  92. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +449 -0
  93. package/src/api/firmware/updateBootloader.ts +19 -4
  94. package/src/api/firmware/uploadFirmware.ts +140 -22
  95. package/src/data-manager/DataManager.ts +62 -54
  96. package/src/data-manager/connectSettings.ts +11 -0
  97. package/src/device/Device.ts +6 -1
  98. package/src/index.ts +5 -0
  99. package/src/inject.ts +22 -2
  100. package/src/lowLevelInject.ts +5 -1
  101. package/src/topLevelInject.ts +5 -1
  102. package/src/types/api/checkAllFirmwareRelease.ts +4 -0
  103. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  104. package/src/types/api/export.ts +18 -0
  105. package/src/types/api/firmwareUpdate.ts +75 -0
  106. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  107. package/src/types/api/firmwareUpdatePlan.ts +38 -0
  108. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  109. package/src/types/api/index.ts +14 -0
  110. package/src/types/settings.ts +13 -0
  111. package/src/utils/deviceFeaturesCompat.ts +6 -0
@@ -2,6 +2,7 @@ import { EDeviceType, ERRORS, HardwareError, HardwareErrorCode, wait } from '@on
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_BLE_FIRMWARE_FILE_CHUNK_SIZE,
6
7
  PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
7
8
  } from '@onekeyfe/hd-transport';
@@ -9,7 +10,6 @@ import { sha256 } from '@noble/hashes/sha256';
9
10
 
10
11
  import { FirmwareUpdateTipMessage, UI_REQUEST } from '../events/ui-request';
11
12
  import { validateProtocolV2FilesystemPath } from './helpers/filesystemValidation';
12
- import { writeProtocolV2File } from './helpers/protocolV2FileWrite';
13
13
  import { validateParams } from './helpers/paramsValidator';
14
14
  import {
15
15
  LoggerNames,
@@ -21,6 +21,7 @@ import {
21
21
  getLogger,
22
22
  } from '../utils';
23
23
  import { getSysResourceBinary } from './firmware/getBinary';
24
+ import { normalizeFirmwarePreparationError } from './firmware/FirmwarePreparationError';
24
25
  import { DataManager } from '../data-manager';
25
26
  import { FirmwareUpdateBaseMethod } from './firmware/FirmwareUpdateBaseMethod';
26
27
  import { DevicePool } from '../device/DevicePool';
@@ -39,11 +40,20 @@ import {
39
40
  getProtocolV2UnknownErrorText,
40
41
  isProtocolV2DeviceDisconnectedError,
41
42
  } from './protocol-v2/helpers';
43
+ import { openFirmwareByteSource, writeFirmwareByteSource } from './firmware/FirmwareArtifactSource';
44
+ import { resolveFirmwareUpdateHostBinding } from './firmware/FirmwareHostBinding';
45
+ import {
46
+ assertFirmwareUpdatePreparedPlanBinding,
47
+ assertFirmwareUpdatePreparedPlanDeviceIdentity,
48
+ } from './firmware/FirmwareUpdatePreparedPlan';
42
49
 
43
- import type { FirmwareUpdateV4Params, FirmwareUpdateV4Target } from '../types/api/firmwareUpdate';
50
+ import type {
51
+ FirmwareArtifactReference,
52
+ FirmwareUpdateV4Params,
53
+ FirmwareUpdateV4Target,
54
+ } from '../types/api/firmwareUpdate';
44
55
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
45
56
  import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
46
- import type { PROTO } from '../constants';
47
57
  import type { TypedResponseMessage } from '../device/DeviceCommands';
48
58
  import type {
49
59
  Features,
@@ -53,9 +63,14 @@ import type {
53
63
  IProtocolV2ResourceFile,
54
64
  IVersionArray,
55
65
  } from '../types';
66
+ import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
56
67
 
57
68
  const Log = getLogger(LoggerNames.Method);
58
69
 
70
+ // Restored after a rebase dropped the declaration while keeping its use in
71
+ // fileWriteChunk; without it that error branch throws ReferenceError instead of
72
+ // the intended typed session error.
73
+ const SESSION_ERROR = 'session not found';
59
74
  const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
60
75
  const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
61
76
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
@@ -75,7 +90,7 @@ const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
75
90
  const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
76
91
  const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
77
92
  const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
78
- const PROTOCOL_V2_PROGRESS_HEARTBEAT_INTERVAL = 1000;
93
+ const PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE = 'FirmwareInstallStatusConflict';
79
94
  const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
80
95
  const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
81
96
  const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
@@ -120,11 +135,6 @@ const getProtocolV2DeviceTransferProgress = (
120
135
  return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
121
136
  };
122
137
 
123
- const formatProtocolV2TransferSpeed = (bytes: number, elapsedMs: number) => {
124
- const safeElapsedMs = Math.max(elapsedMs, 1);
125
- return (bytes / 1024 / (safeElapsedMs / 1000)).toFixed(2);
126
- };
127
-
128
138
  type ProtocolV2FirmwareUpdateStatusTarget = {
129
139
  target_id: number | string;
130
140
  status?: number | string;
@@ -138,22 +148,6 @@ type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId:
138
148
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
139
149
  kind: ProtocolV2RemoteComponentTarget['kind'];
140
150
  };
141
- type ProtocolV2InstallTarget = ProtocolV2InstallItem & {
142
- path: string;
143
- };
144
-
145
- type ProtocolV2FileTransferParams = PROTO.FirmwareUpload & {
146
- filePath: string;
147
- processedSize?: number;
148
- totalSize?: number;
149
- onTransferredBytes?: (transferredBytes: number) => void;
150
- };
151
-
152
- type ProtocolV2ProgressReportState = {
153
- lastProgress?: number;
154
- lastReportedAt?: number;
155
- };
156
-
157
151
  /** Protocol V2 resource file written independently to devicePath through FileWrite. */
158
152
  type ProtocolV2ResourceBundleBinary = {
159
153
  name: string;
@@ -171,6 +165,35 @@ type ProtocolV2RemoteComponentTarget = {
171
165
  kind: 'bootloader' | 'firmware';
172
166
  };
173
167
 
168
+ type ProtocolV2InstallSource = {
169
+ fileName: string;
170
+ source: FirmwareByteSource;
171
+ targetId: number;
172
+ kind: ProtocolV2RemoteComponentTarget['kind'];
173
+ };
174
+
175
+ type ProtocolV2ResourceBundleSource = {
176
+ name: string;
177
+ source: FirmwareByteSource;
178
+ devicePath: string;
179
+ version?: IVersionArray;
180
+ payloadHash?: string;
181
+ headerHash?: string;
182
+ };
183
+
184
+ type ProtocolV2ExecutionPhaseKind =
185
+ | 'resource-sync'
186
+ | 'bootloader-install'
187
+ | 'bootloader-verify'
188
+ | 'component-install'
189
+ | 'final-verify';
190
+
191
+ type ProtocolV2ExecutionPhase = {
192
+ kind: ProtocolV2ExecutionPhaseKind;
193
+ installSources: ProtocolV2InstallSource[];
194
+ resourceSources: ProtocolV2ResourceBundleSource[];
195
+ };
196
+
174
197
  type ProtocolV2OkppHeader = {
175
198
  type: string;
176
199
  version: IVersionArray;
@@ -279,6 +302,12 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
279
302
  );
280
303
  };
281
304
 
305
+ const isProtocolV2TerminalInstallStatusError = (error: unknown) =>
306
+ error instanceof HardwareError &&
307
+ (error.errorCode === HardwareErrorCode.FirmwareError ||
308
+ error.errorCode === HardwareErrorCode.FirmwareVerificationFailed ||
309
+ error.params?.firmwareUpdateCode === PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE);
310
+
282
311
  const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) =>
283
312
  normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED;
284
313
 
@@ -316,6 +345,18 @@ const normalizeProtocolV2TargetStatus = (
316
345
 
317
346
  const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
318
347
 
348
+ const versionArrayToNumber = (version?: IVersionArray) => {
349
+ if (!version) return undefined;
350
+ return version[0] * 0x10000 + version[1] * 0x100 + version[2];
351
+ };
352
+
353
+ const compareProtocolV2Versions = (current?: IVersionArray, target?: IVersionArray) => {
354
+ const currentNumber = versionArrayToNumber(current);
355
+ const targetNumber = versionArrayToNumber(target);
356
+ if (currentNumber === undefined || targetNumber === undefined) return undefined;
357
+ return currentNumber - targetNumber;
358
+ };
359
+
319
360
  const bytesToHex = (bytes: Uint8Array) =>
320
361
  Array.from(bytes)
321
362
  .map(byte => byte.toString(16).padStart(2, '0'))
@@ -449,6 +490,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
449
490
  return ['V2'] as const;
450
491
  }
451
492
 
493
+ private protocolV2PreparedSources: FirmwareByteSource[] = [];
494
+
495
+ private protocolV2ExecutionInLoader = false;
496
+
497
+ private protocolV2CompletedTargetVersions = new Map<number, number>();
498
+
499
+ private protocolV2LatestFinalFeatures?: Features;
500
+
501
+ private protocolV2FinalStatusVerified = false;
502
+
452
503
  init() {
453
504
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
454
505
  this.requireDeviceMode = [];
@@ -486,10 +537,62 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
486
537
  { name: 'firmwareType', type: 'string' },
487
538
  { name: 'targetsToUpdate', type: 'array', allowEmpty: true },
488
539
  { name: 'platform', type: 'string' },
540
+ { name: 'expectedDeviceId', type: 'string' },
489
541
  { name: 'resourceFiles', type: 'array', allowEmpty: true },
490
542
  ]);
543
+ if (
544
+ payload.expectedDeviceId !== undefined &&
545
+ (payload.expectedDeviceId.length === 0 || payload.expectedDeviceId.length > 160)
546
+ ) {
547
+ throw ERRORS.TypedError(
548
+ HardwareErrorCode.CallMethodInvalidParameter,
549
+ 'Protocol V2 expected device identity is invalid'
550
+ );
551
+ }
552
+ const hostBinding =
553
+ payload.preparedPlan || payload.componentArtifacts || payload.resourceBundleArtifacts
554
+ ? resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration)
555
+ : undefined;
556
+ if (hostBinding) {
557
+ assertFirmwareUpdatePreparedPlanBinding({
558
+ preparedPlan: payload.preparedPlan,
559
+ executor: 'v4',
560
+ platform: payload.platform,
561
+ scopeTargets: [
562
+ 'boot',
563
+ 'app_v1',
564
+ 'app_v2',
565
+ 'coprocessor',
566
+ 'resource',
567
+ 'se01',
568
+ 'se02',
569
+ 'se03',
570
+ 'se04',
571
+ ],
572
+ bindings: [
573
+ ...Object.entries(payload.componentArtifacts ?? {}).flatMap(([target, artifact]) =>
574
+ artifact
575
+ ? [
576
+ {
577
+ target: target as Exclude<FirmwareUpdateV4Target, 'resource'>,
578
+ artifact,
579
+ },
580
+ ]
581
+ : []
582
+ ),
583
+ ...(payload.resourceBundleArtifacts ?? []).map(
584
+ (entry: { name: string; artifact: FirmwareArtifactReference }) => ({
585
+ target: 'resource' as const,
586
+ logicalName: entry.name,
587
+ artifact: entry.artifact,
588
+ })
589
+ ),
590
+ ],
591
+ });
592
+ }
491
593
 
492
594
  this.params = {
595
+ preparedPlan: payload.preparedPlan,
493
596
  chunkSize: payload.chunkSize,
494
597
  forcedUpdateRes: payload.forcedUpdateRes,
495
598
  bootloaderBinary: payload.bootloaderBinary,
@@ -504,19 +607,29 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
504
607
  resourceFiles: payload.resourceFiles,
505
608
  firmwareType: payload.firmwareType,
506
609
  targetsToUpdate: payload.targetsToUpdate,
610
+ expectedTargetVersions: payload.expectedTargetVersions,
507
611
  platform: payload.platform,
612
+ expectedDeviceId: payload.expectedDeviceId,
613
+ artifactReader: hostBinding?.artifactReader ?? payload.artifactReader,
614
+ componentArtifacts: payload.componentArtifacts,
615
+ resourceBundleArtifacts: payload.resourceBundleArtifacts,
508
616
  };
509
617
  }
510
618
 
511
- private getProtocolV2FirmwareChunkSize(filePath?: string) {
619
+ private getProtocolV2FirmwareChunkSize(direction: 'read' | 'write', filePath?: string) {
512
620
  const payloadChunkSize = Number(this.params?.chunkSize);
513
621
  const env = DataManager.getSettings('env');
514
622
  const isBle = this.params?.platform === 'native' || (env && DataManager.isBleConnect(env));
515
623
  let maxChunkSize = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
516
624
  if (isBle) {
517
- maxChunkSize = PROTOCOL_V2_FIRMWARE_STAGING_PATHS.has(filePath ?? '')
518
- ? PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE
519
- : PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
625
+ if (direction === 'read') {
626
+ maxChunkSize = PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE;
627
+ } else {
628
+ // Firmware staging writes tolerate a larger BLE chunk than generic filesystem writes.
629
+ maxChunkSize = PROTOCOL_V2_FIRMWARE_STAGING_PATHS.has(filePath ?? '')
630
+ ? PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE
631
+ : PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
632
+ }
520
633
  }
521
634
  if (!Number.isFinite(payloadChunkSize) || payloadChunkSize <= 0) {
522
635
  return maxChunkSize;
@@ -543,8 +656,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
543
656
  assertProtocolV2FirmwareTargetsSupported(capabilityDeviceType, this.params);
544
657
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
545
658
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
546
- const needsRemoteResources =
547
- !this.params.resourceFiles?.length && !!this.params.targetsToUpdate?.includes('resource');
659
+ this.validateExpectedTargetVersions();
660
+
661
+ if (
662
+ (this.params.componentArtifacts && Object.keys(this.params.componentArtifacts).length > 0) ||
663
+ this.params.resourceBundleArtifacts?.length
664
+ ) {
665
+ return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
666
+ }
667
+ const hasExplicitResourceFiles = !!this.params.resourceFiles?.length;
668
+ const wantsStableResources = !!this.params.targetsToUpdate?.includes('resource');
669
+ const wantsBootResources = !!this.params.targetsToUpdate?.includes('boot_resources');
670
+ const needsRemoteResources = !hasExplicitResourceFiles && wantsStableResources;
671
+ const needsRemoteBootResources =
672
+ !hasExplicitResourceFiles && (wantsBootResources || wantsStableResources);
548
673
 
549
674
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
550
675
  let bootloaderBinary: ArrayBuffer | null = null;
@@ -556,11 +681,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
556
681
  fwBinaryMap = this.collectExplicitTargetBinaries();
557
682
  bootloaderBinary = this.prepareBootloaderBinary();
558
683
  const needsRemoteFirmware = !this.hasExplicitProtocolV2Payload(fwBinaryMap);
559
- const hasExplicitResourceFiles = !!this.params.resourceFiles?.length;
560
- const wantsStableResources = !!this.params.targetsToUpdate?.includes('resource');
561
- const wantsBootResources = !!this.params.targetsToUpdate?.includes('boot_resources');
562
- const needsRemoteBootResources =
563
- !hasExplicitResourceFiles && (wantsBootResources || wantsStableResources);
684
+ if (
685
+ (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) &&
686
+ (this.params.artifactReader ||
687
+ DataManager.getSettings('firmwareManifestMode') === 'external-only')
688
+ ) {
689
+ throw ERRORS.TypedError(
690
+ HardwareErrorCode.RuntimeError,
691
+ 'Protocol V2 firmware artifacts must be prepared by the external firmware host',
692
+ {
693
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
694
+ }
695
+ );
696
+ }
564
697
  if (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) {
565
698
  // Remote updates must use a freshly fetched config before any reboot or file write.
566
699
  await DataManager.forceReloadData({
@@ -587,7 +720,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
587
720
  if (err instanceof HardwareError && err.errorCode === HardwareErrorCode.NetworkError) {
588
721
  throw err;
589
722
  }
590
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
723
+ throw normalizeFirmwarePreparationError(err);
591
724
  }
592
725
 
593
726
  if (
@@ -603,9 +736,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
603
736
  );
604
737
  }
605
738
 
606
- const enteredBootloader = await this.enterProtocolV2BootloaderMode();
607
-
608
739
  if (needsRemoteResources) {
740
+ const enteredBootloader = await this.enterProtocolV2BootloaderMode();
609
741
  try {
610
742
  const stableResources = await this.prepareProtocolV2ResourceBundles();
611
743
  resourceBundles = this.mergeProtocolV2ResourceBundles(resourceBundles, stableResources);
@@ -621,25 +753,293 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
621
753
  );
622
754
  }
623
755
  }
624
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
756
+ throw normalizeFirmwarePreparationError(err);
625
757
  }
626
758
  }
627
759
 
628
- await this.executeProtocolV2Update({
760
+ return this.executeProtocolV2Update({
629
761
  fwBinaryMap,
630
762
  bootloaderBinary,
631
763
  ...(installItems ? { installItems } : undefined),
632
764
  ...(resourceBundles?.length ? { resourceBundles } : undefined),
633
765
  });
766
+ }
634
767
 
635
- this.postTipMessage(FirmwareUpdateTipMessage.SwitchFirmwareReconnectDevice);
636
- await this.exitProtocolV2BootloaderToNormal();
768
+ private async openProtocolV2PreparedSource(artifact: FirmwareArtifactReference) {
769
+ const source = await openFirmwareByteSource({
770
+ artifact,
771
+ reader: this.params.artifactReader,
772
+ });
773
+ if (!source) {
774
+ throw ERRORS.TypedError(
775
+ HardwareErrorCode.RuntimeError,
776
+ `Firmware artifact ${artifact.artifactRef} is not prepared`,
777
+ {
778
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
779
+ }
780
+ );
781
+ }
782
+ this.protocolV2PreparedSources.push(source);
783
+ return source;
784
+ }
637
785
 
638
- const versions = await this.waitForProtocolV2FinalFeatures();
639
- this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
640
- DevicePool.resetState();
786
+ private async openProtocolV2MemorySource(binary: ArrayBuffer) {
787
+ const source = await openFirmwareByteSource({ binary });
788
+ if (!source) {
789
+ throw ERRORS.TypedError(
790
+ HardwareErrorCode.RuntimeError,
791
+ 'Protocol V2 firmware binary is empty'
792
+ );
793
+ }
794
+ this.protocolV2PreparedSources.push(source);
795
+ return source;
796
+ }
641
797
 
642
- return versions;
798
+ private async closeProtocolV2PreparedSources() {
799
+ const sources = this.protocolV2PreparedSources.splice(0);
800
+ await Promise.all(sources.map(source => source.close().catch(() => undefined)));
801
+ }
802
+
803
+ private async prepareProtocolV2InstallSources(
804
+ firmwareType: EFirmwareType,
805
+ features: Features
806
+ ): Promise<ProtocolV2InstallSource[]> {
807
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
808
+ if (!release) {
809
+ throw ERRORS.TypedError(
810
+ HardwareErrorCode.RuntimeError,
811
+ 'Protocol V2 firmware release is unavailable'
812
+ );
813
+ }
814
+ const componentArtifacts = this.params.componentArtifacts ?? {};
815
+ const requestedTargets = new Set(
816
+ this.params.targetsToUpdate ?? (Object.keys(componentArtifacts) as FirmwareUpdateV4Target[])
817
+ );
818
+ const installSources: ProtocolV2InstallSource[] = [];
819
+ const preparedTargets = new Set<FirmwareUpdateV4Target>();
820
+
821
+ for (const [key, component] of this.getRemoteComponentEntries(release)) {
822
+ const target = this.getRemoteComponentTarget(key, component);
823
+ const updateTarget = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId);
824
+ if (updateTarget && updateTarget !== 'resource' && requestedTargets.has(updateTarget)) {
825
+ const artifact = componentArtifacts[updateTarget];
826
+ if (!artifact) {
827
+ throw ERRORS.TypedError(
828
+ HardwareErrorCode.RuntimeError,
829
+ `Protocol V2 ${updateTarget} artifact is not prepared`,
830
+ {
831
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
832
+ artifactName: updateTarget,
833
+ }
834
+ );
835
+ }
836
+ installSources.push({
837
+ ...target,
838
+ source: await this.openProtocolV2PreparedSource(artifact),
839
+ });
840
+ preparedTargets.add(updateTarget);
841
+ }
842
+ }
843
+
844
+ const unknownTarget = Array.from(requestedTargets).find(
845
+ target => target !== 'resource' && !preparedTargets.has(target)
846
+ );
847
+ if (unknownTarget) {
848
+ throw ERRORS.TypedError(
849
+ HardwareErrorCode.RuntimeError,
850
+ `Protocol V2 release does not contain requested target ${unknownTarget}`
851
+ );
852
+ }
853
+ return installSources;
854
+ }
855
+
856
+ private async prepareProtocolV2ResourceSources(
857
+ firmwareType: EFirmwareType,
858
+ features: Features
859
+ ): Promise<ProtocolV2ResourceBundleSource[]> {
860
+ const preparedArtifacts = this.params.resourceBundleArtifacts ?? [];
861
+ const resourceRequested =
862
+ this.params.targetsToUpdate?.includes('resource') || preparedArtifacts.length > 0;
863
+ if (!resourceRequested) {
864
+ return [];
865
+ }
866
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
867
+ const descriptors = release?.resourceBundles ?? [];
868
+ const artifactByName = new Map(
869
+ preparedArtifacts.map(item => [item.name, item.artifact] as const)
870
+ );
871
+ const sources: ProtocolV2ResourceBundleSource[] = [];
872
+ for (const descriptor of descriptors) {
873
+ const artifact = artifactByName.get(descriptor.name);
874
+ if (!artifact) {
875
+ throw ERRORS.TypedError(
876
+ HardwareErrorCode.RuntimeError,
877
+ `Protocol V2 resource bundle ${descriptor.name} is not prepared`,
878
+ {
879
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
880
+ artifactName: descriptor.name,
881
+ }
882
+ );
883
+ }
884
+ const devicePath = validateProtocolV2FilesystemPath(
885
+ descriptor.devicePath,
886
+ 'resourceBundles[].devicePath'
887
+ );
888
+ sources.push({
889
+ name: descriptor.name,
890
+ source: await this.openProtocolV2PreparedSource(artifact),
891
+ devicePath,
892
+ version: descriptor.version,
893
+ payloadHash: descriptor.payloadHash,
894
+ headerHash: descriptor.headerHash,
895
+ });
896
+ artifactByName.delete(descriptor.name);
897
+ }
898
+ if (artifactByName.size > 0) {
899
+ throw ERRORS.TypedError(
900
+ HardwareErrorCode.RuntimeError,
901
+ `Protocol V2 release does not contain resource bundle ${artifactByName.keys().next().value}`
902
+ );
903
+ }
904
+ return sources;
905
+ }
906
+
907
+ private async runProtocolV2PreparedArtifacts(features: Features, firmwareType: EFirmwareType) {
908
+ try {
909
+ this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
910
+ const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
911
+ const resourceSources = await this.prepareProtocolV2ResourceSources(firmwareType, features);
912
+ if (installSources.length === 0 && resourceSources.length === 0) {
913
+ throw ERRORS.TypedError(
914
+ HardwareErrorCode.FirmwareUpdateDownloadFailed,
915
+ 'No firmware to update'
916
+ );
917
+ }
918
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
919
+
920
+ return await this.executeProtocolV2SourceUpdate({
921
+ installSources,
922
+ resourceSources,
923
+ });
924
+ } finally {
925
+ await this.closeProtocolV2PreparedSources();
926
+ }
927
+ }
928
+
929
+ private validateExpectedTargetVersions() {
930
+ const expected = this.params.expectedTargetVersions;
931
+ if (expected === undefined) return;
932
+ if (typeof expected !== 'object' || expected === null || Array.isArray(expected)) {
933
+ throw ERRORS.TypedError(
934
+ HardwareErrorCode.RuntimeError,
935
+ 'Protocol V2 expected target versions are invalid'
936
+ );
937
+ }
938
+ for (const [target, version] of Object.entries(expected)) {
939
+ if (
940
+ !Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.values()).includes(
941
+ target as FirmwareUpdateV4Target
942
+ ) ||
943
+ typeof version !== 'string' ||
944
+ !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(version) ||
945
+ version.length > 64
946
+ ) {
947
+ throw ERRORS.TypedError(
948
+ HardwareErrorCode.RuntimeError,
949
+ 'Protocol V2 expected target version is invalid'
950
+ );
951
+ }
952
+ }
953
+ }
954
+
955
+ private getExpectedProtocolV2TargetVersion(targetId: number) {
956
+ const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(targetId);
957
+ const version = target ? this.params?.expectedTargetVersions?.[target] : undefined;
958
+ if (!version) return undefined;
959
+ const parts = version.split(/[+-]/u, 1)[0].split('.').map(Number);
960
+ if (
961
+ parts.length !== 3 ||
962
+ parts.some(part => !Number.isSafeInteger(part) || part < 0 || part > 0xff)
963
+ ) {
964
+ return undefined;
965
+ }
966
+ return parts[0] * 0x10000 + parts[1] * 0x100 + parts[2];
967
+ }
968
+
969
+ private assertExpectedProtocolV2Versions(targets?: readonly FirmwareUpdateV4Target[]) {
970
+ const expected = this.params?.expectedTargetVersions;
971
+ if (!expected) return;
972
+ const features = this.protocolV2LatestFinalFeatures;
973
+ const requestedTargets = this.getProtocolV2RequestedTargets();
974
+ const applicationTargets = requestedTargets.filter(
975
+ target => target === 'app_v1' || target === 'app_v2'
976
+ );
977
+ const visibleVersions: Partial<Record<FirmwareUpdateV4Target, string>> = {
978
+ boot: features ? getDeviceBootloaderVersion(features).join('.') : undefined,
979
+ coprocessor: features ? getDeviceBLEFirmwareVersion(features).join('.') : undefined,
980
+ se01: features?.se01Version ?? undefined,
981
+ se02: features?.se02Version ?? undefined,
982
+ se03: features?.se03Version ?? undefined,
983
+ se04: features?.se04Version ?? undefined,
984
+ };
985
+ if (features && applicationTargets.length === 1) {
986
+ visibleVersions[applicationTargets[0]] = getDeviceFirmwareVersion(features).join('.');
987
+ }
988
+ const expectedEntries = (
989
+ Object.entries(expected) as Array<[FirmwareUpdateV4Target, string]>
990
+ ).filter(([target]) => !targets || targets.includes(target));
991
+ for (const [target, expectedVersion] of expectedEntries) {
992
+ const targetId = Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.entries()).find(
993
+ ([, mappedTarget]) => mappedTarget === target
994
+ )?.[0];
995
+ const statusVersion =
996
+ targetId === undefined ? undefined : this.protocolV2CompletedTargetVersions.get(targetId);
997
+ const observedVersion =
998
+ statusVersion === undefined
999
+ ? visibleVersions[target]
1000
+ : `${Math.floor(statusVersion / 0x10000) % 0x100}.${
1001
+ Math.floor(statusVersion / 0x100) % 0x100
1002
+ }.${statusVersion % 0x100}`;
1003
+ if (!observedVersion) {
1004
+ if (this.protocolV2FinalStatusVerified) {
1005
+ Log.warn(
1006
+ `Protocol V2 target ${target} has no observable final version; completed target status is authoritative`
1007
+ );
1008
+ } else {
1009
+ throw ERRORS.TypedError(
1010
+ HardwareErrorCode.FirmwareVerificationFailed,
1011
+ `Protocol V2 target ${target} has no observable final version after status fallback`
1012
+ );
1013
+ }
1014
+ } else if (observedVersion !== expectedVersion) {
1015
+ throw ERRORS.TypedError(
1016
+ HardwareErrorCode.FirmwareVerificationFailed,
1017
+ `Protocol V2 target ${target} reached ${observedVersion}, expected ${expectedVersion}`
1018
+ );
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+ private getProtocolV2RequestedTargets(): FirmwareUpdateV4Target[] {
1024
+ if (this.params.targetsToUpdate?.length) {
1025
+ return [...new Set(this.params.targetsToUpdate)];
1026
+ }
1027
+ const targets = new Set<FirmwareUpdateV4Target>();
1028
+ Object.keys(this.params.componentArtifacts ?? {}).forEach(target =>
1029
+ targets.add(target as FirmwareUpdateV4Target)
1030
+ );
1031
+ if (this.params.resourceBundleArtifacts?.length || this.params.resourceFiles?.length) {
1032
+ targets.add('resource');
1033
+ }
1034
+ if (this.params.bootloaderBinary) targets.add('boot');
1035
+ if (this.params.applicationP1Binary) targets.add('app_v1');
1036
+ if (this.params.applicationP2Binary) targets.add('app_v2');
1037
+ if (this.params.coprocessorBinary) targets.add('coprocessor');
1038
+ if (this.params.se01Binary) targets.add('se01');
1039
+ if (this.params.se02Binary) targets.add('se02');
1040
+ if (this.params.se03Binary) targets.add('se03');
1041
+ if (this.params.se04Binary) targets.add('se04');
1042
+ return Array.from(targets);
643
1043
  }
644
1044
 
645
1045
  private async getProtocolV2DeviceFeatures(): Promise<Features> {
@@ -683,8 +1083,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
683
1083
  private async captureProtocolV2PhysicalIdentity() {
684
1084
  const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
685
1085
  const serialNumber = this.getProtocolV2SerialNumber(deviceInfo);
686
- const path = this.device.originalDescriptor.path?.trim() || undefined;
687
- assertProtocolV2ReconnectIdentity(serialNumber, serialNumber, path, path);
1086
+ const path = this.device.originalDescriptor?.path?.trim() || undefined;
1087
+ if (this.params?.preparedPlan) {
1088
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
1089
+ preparedPlan: this.params.preparedPlan,
1090
+ deviceIdentity: serialNumber,
1091
+ });
1092
+ } else if (this.params?.expectedDeviceId) {
1093
+ assertProtocolV2ReconnectIdentity(this.params?.expectedDeviceId, serialNumber);
1094
+ } else {
1095
+ assertProtocolV2ReconnectIdentity(serialNumber, serialNumber, path, path);
1096
+ }
688
1097
  this.protocolV2ExpectedSerialNumber = serialNumber;
689
1098
  this.protocolV2ExpectedPath = path;
690
1099
  }
@@ -920,7 +1329,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
920
1329
  }
921
1330
 
922
1331
  const digest = sha256.create();
923
- const chunkSize = this.getProtocolV2FirmwareChunkSize();
1332
+ const chunkSize = this.getProtocolV2FirmwareChunkSize('write');
924
1333
  let offset = 0;
925
1334
  while (offset < file.size) {
926
1335
  const response = await commands.typedCall(
@@ -1015,7 +1424,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1015
1424
  : await readProtocolV2ResourceInventory({
1016
1425
  commands: this.device.getCommands(),
1017
1426
  resources,
1018
- chunkSize: this.getProtocolV2FirmwareChunkSize(),
1427
+ chunkSize: this.getProtocolV2FirmwareChunkSize('write'),
1019
1428
  timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1020
1429
  });
1021
1430
  const plan = buildProtocolV2ResourceUpdatePlan({
@@ -1050,38 +1459,88 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1050
1459
  };
1051
1460
  }
1052
1461
 
1053
- private async syncProtocolV2ResourceBundles(
1054
- bundles: ProtocolV2ResourceBundleBinary[],
1055
- firmwareSize: number
1056
- ): Promise<{ processedSize: number; totalSize: number }> {
1057
- const transferStartTime = Date.now();
1058
- const transferTransport = this.getProtocolV2FirmwareTransferTransport();
1462
+ private getProtocolV2ResourceFilePath(path: string) {
1463
+ if (path.startsWith('vol')) return path;
1464
+ if (path.startsWith('/')) return `vol0:${path}`;
1465
+ return `vol0:/${path}`;
1466
+ }
1059
1467
 
1060
- // Write directly to the device through FileWrite.
1061
- let totalSize = 0;
1062
- for (const b of bundles) totalSize += b.binary.byteLength;
1468
+ private async readProtocolV2DeviceFileHeader(path: string) {
1469
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1470
+ const filePath = this.getProtocolV2ResourceFilePath(path);
1471
+ const pathInfoRes = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
1472
+ path: filePath,
1473
+ });
1474
+ const fileSize = toProtocolV2FiniteNumber(pathInfoRes.message?.size);
1475
+ if (
1476
+ !pathInfoRes.message?.exist ||
1477
+ pathInfoRes.message?.directory ||
1478
+ fileSize === undefined ||
1479
+ fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE
1480
+ ) {
1481
+ return null;
1482
+ }
1063
1483
 
1064
- const transferTotalSize = totalSize + firmwareSize;
1065
- let processedSize = 0;
1066
- for (const bundle of bundles) {
1067
- Log.log(
1068
- `[FirmwareUpdateV4] syncing resource ${bundle.name} -> ${bundle.devicePath} bytes=${bundle.binary.byteLength}`
1069
- );
1070
- processedSize = await this.protocolV2CommonUpdateProcess({
1071
- payload: bundle.binary,
1072
- filePath: bundle.devicePath,
1073
- processedSize,
1074
- totalSize: transferTotalSize,
1484
+ const chunkSize = this.getProtocolV2FirmwareChunkSize('read');
1485
+ const chunks: Uint8Array[] = [];
1486
+ let offset = 0;
1487
+ while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) {
1488
+ const readLen = Math.min(chunkSize, PROTOCOL_V2_OKPP_HEADER_SIZE - offset);
1489
+ const res = await typedCall('FilesystemFileRead', 'FilesystemFile', {
1490
+ file: {
1491
+ path: filePath,
1492
+ offset,
1493
+ total_size: 0,
1494
+ },
1495
+ chunk_len: readLen,
1496
+ ui_percentage: undefined,
1075
1497
  });
1498
+ const data = toProtocolV2Bytes(res.message?.data);
1499
+ if (data.byteLength === 0) return null;
1500
+ chunks.push(data);
1501
+ offset += data.byteLength;
1076
1502
  }
1077
1503
 
1078
- const elapsedMs = Date.now() - transferStartTime;
1079
- Log.log(
1080
- `[FirmwareUpdateV4] resource sync finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
1081
- elapsedMs / 1000
1082
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
1083
- );
1084
- return { processedSize, totalSize: transferTotalSize };
1504
+ const headerBytes = new Uint8Array(offset);
1505
+ let cursor = 0;
1506
+ chunks.forEach(chunk => {
1507
+ headerBytes.set(chunk, cursor);
1508
+ cursor += chunk.byteLength;
1509
+ });
1510
+ return parseProtocolV2OkppHeader(headerBytes);
1511
+ }
1512
+
1513
+ /** Compare a prepared okpkg header when its manifest supplies version or hash metadata. */
1514
+ private async isProtocolV2ResourceBundleUpToDate(
1515
+ bundle: Pick<
1516
+ ProtocolV2ResourceBundleSource,
1517
+ 'name' | 'devicePath' | 'version' | 'payloadHash' | 'headerHash'
1518
+ >
1519
+ ): Promise<boolean> {
1520
+ if (this.params?.forcedUpdateRes) return false;
1521
+ if (!bundle.version && !bundle.payloadHash) return false;
1522
+
1523
+ try {
1524
+ const header = await this.readProtocolV2DeviceFileHeader(bundle.devicePath);
1525
+ if (!header) return false;
1526
+
1527
+ if (bundle.version) {
1528
+ const cmp = compareProtocolV2Versions(header.version, bundle.version);
1529
+ if (cmp === undefined || cmp !== 0) return false;
1530
+ }
1531
+ if (bundle.payloadHash) {
1532
+ const expected = normalizeProtocolV2Hex(bundle.payloadHash);
1533
+ if (expected && header.payloadHash !== expected) return false;
1534
+ }
1535
+ if (bundle.headerHash) {
1536
+ const expected = normalizeProtocolV2Hex(bundle.headerHash);
1537
+ if (expected && header.headerHash !== expected) return false;
1538
+ }
1539
+ return true;
1540
+ } catch (error) {
1541
+ Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
1542
+ return false;
1543
+ }
1085
1544
  }
1086
1545
 
1087
1546
  private isProtocolV2BootloaderMode() {
@@ -1113,10 +1572,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1113
1572
  // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
1114
1573
  if (this.isProtocolV2RomloaderMode()) {
1115
1574
  Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
1575
+ this.protocolV2ExecutionInLoader = true;
1116
1576
  return false;
1117
1577
  }
1118
1578
  if (this.isProtocolV2BootloaderMode()) {
1119
1579
  Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
1580
+ this.protocolV2ExecutionInLoader = true;
1120
1581
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1121
1582
  return false;
1122
1583
  }
@@ -1126,6 +1587,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1126
1587
  await this.protocolV2Reboot(DeviceRebootType.Bootloader);
1127
1588
  await wait(1000);
1128
1589
  await this.waitForProtocolV2BootloaderMode();
1590
+ this.protocolV2ExecutionInLoader = true;
1129
1591
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1130
1592
  return true;
1131
1593
  } catch (error) {
@@ -1221,6 +1683,98 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1221
1683
  return entries;
1222
1684
  }
1223
1685
 
1686
+ private buildProtocolV2ExecutionPhases({
1687
+ installSources,
1688
+ resourceSources,
1689
+ }: {
1690
+ installSources: ProtocolV2InstallSource[];
1691
+ resourceSources: ProtocolV2ResourceBundleSource[];
1692
+ }): ProtocolV2ExecutionPhase[] {
1693
+ const phases: ProtocolV2ExecutionPhase[] = [];
1694
+ if (resourceSources.length > 0) {
1695
+ phases.push({
1696
+ kind: 'resource-sync',
1697
+ installSources: [],
1698
+ resourceSources,
1699
+ });
1700
+ }
1701
+ const bootloaderSources = installSources.filter(source => source.kind === 'bootloader');
1702
+ if (bootloaderSources.length > 0) {
1703
+ phases.push(
1704
+ {
1705
+ kind: 'bootloader-install',
1706
+ installSources: bootloaderSources,
1707
+ resourceSources: [],
1708
+ },
1709
+ {
1710
+ kind: 'bootloader-verify',
1711
+ installSources: [],
1712
+ resourceSources: [],
1713
+ }
1714
+ );
1715
+ }
1716
+ const componentSources = installSources.filter(source => source.kind !== 'bootloader');
1717
+ if (componentSources.length > 0) {
1718
+ phases.push({
1719
+ kind: 'component-install',
1720
+ installSources: componentSources,
1721
+ resourceSources: [],
1722
+ });
1723
+ }
1724
+ phases.push({
1725
+ kind: 'final-verify',
1726
+ installSources: [],
1727
+ resourceSources: [],
1728
+ });
1729
+ return phases;
1730
+ }
1731
+
1732
+ private async executeProtocolV2Phases({
1733
+ installSources,
1734
+ resourceSources,
1735
+ }: {
1736
+ installSources: ProtocolV2InstallSource[];
1737
+ resourceSources: ProtocolV2ResourceBundleSource[];
1738
+ }) {
1739
+ const phases = this.buildProtocolV2ExecutionPhases({
1740
+ installSources,
1741
+ resourceSources,
1742
+ });
1743
+ for (const phase of phases) {
1744
+ if (phase.kind === 'final-verify') {
1745
+ return this.completeProtocolV2FinalVerification();
1746
+ }
1747
+ if (phase.kind === 'resource-sync') {
1748
+ await this.enterProtocolV2BootloaderMode();
1749
+ await this.executeProtocolV2TransferPhase(phase);
1750
+ } else if (phase.kind === 'bootloader-install' || phase.kind === 'component-install') {
1751
+ await this.enterProtocolV2BootloaderMode();
1752
+ await this.executeProtocolV2TransferPhase(phase);
1753
+ await this.exitProtocolV2BootloaderToNormal();
1754
+ } else if (phase.kind === 'bootloader-verify') {
1755
+ await this.waitForProtocolV2FinalFeatures();
1756
+ this.assertExpectedProtocolV2Versions(['boot']);
1757
+ }
1758
+ }
1759
+ throw ERRORS.TypedError(
1760
+ HardwareErrorCode.RuntimeError,
1761
+ 'Protocol V2 execution has no final verification phase'
1762
+ );
1763
+ }
1764
+
1765
+ private async executeProtocolV2SourceUpdate({
1766
+ installSources,
1767
+ resourceSources,
1768
+ }: {
1769
+ installSources: ProtocolV2InstallSource[];
1770
+ resourceSources: ProtocolV2ResourceBundleSource[];
1771
+ }) {
1772
+ return this.executeProtocolV2Phases({
1773
+ installSources,
1774
+ resourceSources,
1775
+ });
1776
+ }
1777
+
1224
1778
  private async executeProtocolV2Update({
1225
1779
  fwBinaryMap,
1226
1780
  bootloaderBinary,
@@ -1232,106 +1786,168 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1232
1786
  installItems?: ProtocolV2InstallItem[];
1233
1787
  resourceBundles?: ProtocolV2ResourceBundleBinary[];
1234
1788
  }) {
1235
- const orderedInstallItems =
1789
+ const memoryInstallItems =
1236
1790
  installItems ??
1237
1791
  this.buildProtocolV2InstallItems({
1238
- bootloaderBinary: bootloaderBinary ?? null,
1239
1792
  fwBinaryMap: fwBinaryMap ?? [],
1793
+ bootloaderBinary: bootloaderBinary ?? null,
1240
1794
  });
1241
-
1242
- let firmwareSize = 0;
1243
- for (const item of orderedInstallItems) firmwareSize += item.binary.byteLength;
1244
-
1245
- this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
1246
-
1247
- let processedSize = 0;
1248
- let totalSize = firmwareSize;
1249
- if (resourceBundles?.length) {
1250
- const resourceTransfer = await this.syncProtocolV2ResourceBundles(
1251
- resourceBundles,
1252
- firmwareSize
1795
+ try {
1796
+ const installSources = await Promise.all(
1797
+ memoryInstallItems.map(async item => ({
1798
+ fileName: item.fileName,
1799
+ source: await this.openProtocolV2MemorySource(item.binary),
1800
+ targetId: item.targetId,
1801
+ kind: item.kind,
1802
+ }))
1253
1803
  );
1254
- processedSize = resourceTransfer.processedSize;
1255
- totalSize = resourceTransfer.totalSize;
1804
+ const resourceSources = await Promise.all(
1805
+ (resourceBundles ?? []).map(async bundle => ({
1806
+ name: bundle.name,
1807
+ source: await this.openProtocolV2MemorySource(bundle.binary),
1808
+ devicePath: bundle.devicePath,
1809
+ }))
1810
+ );
1811
+ return await this.executeProtocolV2Phases({
1812
+ installSources,
1813
+ resourceSources,
1814
+ });
1815
+ } finally {
1816
+ await this.closeProtocolV2PreparedSources();
1256
1817
  }
1818
+ }
1257
1819
 
1258
- // Skip staging and installation when the update contains resource files only.
1259
- if (orderedInstallItems.length === 0) {
1260
- Log.log('[FirmwareUpdateV4] no firmware targets to install (resource files only)');
1261
- if (totalSize > 0) {
1262
- this.postProgressMessage(100, 'transferData');
1820
+ private async executeProtocolV2TransferPhase({
1821
+ installSources,
1822
+ resourceSources,
1823
+ }: ProtocolV2ExecutionPhase) {
1824
+ let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
1825
+ const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
1826
+ for (const resource of resourceSources) {
1827
+ if (await this.isProtocolV2ResourceBundleUpToDate(resource)) {
1828
+ Log.log(`[FirmwareUpdateV4] skip RESC bundle ${resource.name}; already up to date`);
1829
+ } else {
1830
+ resourcesToSync.push(resource);
1831
+ totalSize += resource.source.size;
1263
1832
  }
1264
- return;
1265
1833
  }
1266
- let transferredSize = processedSize;
1267
- const transferStartTime = Date.now();
1268
- const transferTransport = this.getProtocolV2FirmwareTransferTransport();
1269
- const chunkSize = this.getProtocolV2FirmwareChunkSize(
1270
- this.getProtocolV2InstallItemStagingPath(orderedInstallItems[0])
1271
- );
1272
- const onTransferredBytes = (bytes: number) => {
1273
- transferredSize = bytes;
1274
- };
1275
- Log.log(
1276
- `[FirmwareUpdateV4] transfer started transport=${transferTransport} total=${totalSize} bytes chunk=${chunkSize} bytes`
1277
- );
1278
1834
 
1279
- const stagedInstallTargets: ProtocolV2InstallTarget[] = [];
1280
-
1281
- try {
1282
- for (const item of orderedInstallItems) {
1283
- const filePath = this.getProtocolV2InstallItemStagingPath(item);
1284
- Log.log(
1285
- `[FirmwareUpdateV4] staging ${item.kind} via FilesystemFileWrite target=${item.targetId} path=${filePath} source=${item.fileName} bytes=${item.binary.byteLength}`
1286
- );
1287
- processedSize = await this.protocolV2CommonUpdateProcess({
1288
- payload: item.binary,
1289
- filePath,
1290
- processedSize,
1291
- totalSize,
1292
- onTransferredBytes,
1293
- });
1294
- transferredSize = processedSize;
1295
- await this.verifyProtocolV2StagedFile(filePath, item.binary.byteLength);
1296
-
1297
- stagedInstallTargets.push({
1298
- ...item,
1299
- path: filePath,
1300
- });
1301
- }
1835
+ this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
1836
+ let processedSize = 0;
1837
+ for (const resource of resourcesToSync) {
1838
+ processedSize = await this.protocolV2SourceUpdateProcess({
1839
+ source: resource.source,
1840
+ filePath: resource.devicePath,
1841
+ processedSize,
1842
+ totalSize,
1843
+ });
1844
+ await this.verifyProtocolV2StagedFile(resource.devicePath, resource.source.size);
1845
+ }
1302
1846
 
1303
- if (totalSize > 0) {
1304
- this.postProgressMessage(100, 'transferData');
1305
- }
1847
+ const stagedInstallTargets: Array<{ targetId: number; path: string }> = [];
1848
+ for (const item of installSources) {
1849
+ const filePath = this.getProtocolV2InstallItemStagingPath(item);
1850
+ processedSize = await this.protocolV2SourceUpdateProcess({
1851
+ source: item.source,
1852
+ filePath,
1853
+ processedSize,
1854
+ totalSize,
1855
+ });
1856
+ await this.verifyProtocolV2StagedFile(filePath, item.source.size);
1857
+ stagedInstallTargets.push({
1858
+ targetId: item.targetId,
1859
+ path: filePath,
1860
+ });
1861
+ }
1306
1862
 
1307
- const elapsedMs = Date.now() - transferStartTime;
1308
- Log.log(
1309
- `[FirmwareUpdateV4] transfer finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
1310
- elapsedMs / 1000
1311
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
1312
- );
1313
- } catch (error) {
1314
- const elapsedMs = Date.now() - transferStartTime;
1315
- Log.warn(
1316
- `[FirmwareUpdateV4] transfer failed transport=${transferTransport} bytes=${transferredSize}/${totalSize} elapsed=${(
1317
- elapsedMs / 1000
1318
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(transferredSize, elapsedMs)} KB/s`
1319
- );
1320
- throw error;
1863
+ if (totalSize > 0) {
1864
+ this.postProgressMessage(100, 'transferData');
1865
+ }
1866
+ if (stagedInstallTargets.length === 0) {
1867
+ return;
1321
1868
  }
1322
1869
 
1323
1870
  this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
1324
-
1325
- const allTargets = stagedInstallTargets.map(item => ({
1871
+ const targets = stagedInstallTargets.map(item => ({
1326
1872
  target_id: item.targetId,
1327
1873
  path: item.path,
1328
1874
  }));
1329
- Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(allTargets)}`);
1330
- await this.protocolV2StartFirmwareUpdate({ targets: allTargets });
1331
- await this.waitForProtocolV2FirmwareUpdateComplete(allTargets);
1875
+ await this.protocolV2StartFirmwareUpdate({ targets });
1876
+ await this.waitForProtocolV2FirmwareUpdateComplete(targets);
1877
+ }
1878
+
1879
+ private async protocolV2SourceUpdateProcess({
1880
+ source,
1881
+ filePath,
1882
+ processedSize,
1883
+ totalSize,
1884
+ }: {
1885
+ source: FirmwareByteSource;
1886
+ filePath: string;
1887
+ processedSize: number;
1888
+ totalSize: number;
1889
+ }) {
1890
+ let lastError: unknown;
1891
+ for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1892
+ try {
1893
+ const transferStartedAt = Date.now();
1894
+ await writeFirmwareByteSource({
1895
+ source,
1896
+ chunkSize: this.getProtocolV2FirmwareChunkSize('write', filePath),
1897
+ write: async ({ data, sourceOffset, length, first }) => {
1898
+ const chunkEnd = sourceOffset + length;
1899
+ const deviceProgress = getProtocolV2DeviceTransferProgress(
1900
+ processedSize + sourceOffset,
1901
+ processedSize + chunkEnd,
1902
+ totalSize
1903
+ );
1904
+ const response = await this.fileWriteChunk(
1905
+ filePath,
1906
+ source.size,
1907
+ sourceOffset,
1908
+ data,
1909
+ first,
1910
+ deviceProgress
1911
+ );
1912
+ const rawProcessedByte = response.message.processed_byte;
1913
+ const nextOffset = rawProcessedByte === undefined ? chunkEnd : Number(rawProcessedByte);
1914
+ if (!Number.isFinite(nextOffset) || nextOffset !== chunkEnd) {
1915
+ throw ERRORS.TypedError(
1916
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
1917
+ `invalid processed_byte ${rawProcessedByte} for offset ${sourceOffset}`
1918
+ );
1919
+ }
1920
+ const transferredBytes = processedSize + chunkEnd;
1921
+ const elapsedMs = Math.max(Date.now() - transferStartedAt, 0);
1922
+ this.postProgressMessage(
1923
+ Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99),
1924
+ 'transferData',
1925
+ {
1926
+ transferredBytes,
1927
+ totalBytes: totalSize,
1928
+ rateBytesPerSecond:
1929
+ elapsedMs > 0 ? Math.round((chunkEnd / elapsedMs) * 1000) : undefined,
1930
+ elapsedMs,
1931
+ }
1932
+ );
1933
+ return length;
1934
+ },
1935
+ });
1936
+ return processedSize + source.size;
1937
+ } catch (error) {
1938
+ lastError = error;
1939
+ if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1940
+ await this.recoverProtocolV2FileTransfer();
1941
+ }
1942
+ }
1943
+ }
1944
+ throw ERRORS.TypedError(
1945
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
1946
+ `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1947
+ );
1332
1948
  }
1333
1949
 
1334
- private getProtocolV2InstallItemStagingPath(item: ProtocolV2InstallItem) {
1950
+ private getProtocolV2InstallItemStagingPath(item: Pick<ProtocolV2InstallItem, 'fileName'>) {
1335
1951
  return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`;
1336
1952
  }
1337
1953
 
@@ -1350,7 +1966,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1350
1966
 
1351
1967
  private assertProtocolV2TargetStatus(
1352
1968
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
1353
- expectedTargetIds: Set<number>
1969
+ expectedTargetIds: Set<number>,
1970
+ expectedPaths = new Map<number, string>()
1354
1971
  ) {
1355
1972
  Log.log(
1356
1973
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
@@ -1381,17 +1998,57 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1381
1998
  const matchingTargets = statusTargets.filter(target =>
1382
1999
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
1383
2000
  );
2001
+ const seenTargetIds = new Set<number>();
2002
+ for (const target of matchingTargets) {
2003
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2004
+ if (
2005
+ targetId === undefined ||
2006
+ seenTargetIds.has(targetId) ||
2007
+ (target.path && expectedPaths.get(targetId) && target.path !== expectedPaths.get(targetId))
2008
+ ) {
2009
+ throw ERRORS.TypedError(
2010
+ HardwareErrorCode.RuntimeError,
2011
+ `Protocol V2 install status conflicts with target ${target.target_id}`,
2012
+ {
2013
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE,
2014
+ }
2015
+ );
2016
+ }
2017
+ seenTargetIds.add(targetId);
2018
+ }
2019
+ const completedTargets = matchingTargets.filter(target =>
2020
+ isProtocolV2TargetStatusFinished(target.status)
2021
+ );
1384
2022
  const completedTargetIds = new Set<number>();
1385
- matchingTargets.forEach(target => {
2023
+ completedTargets.forEach(target => {
1386
2024
  const targetId = normalizeProtocolV2TargetId(target.target_id);
1387
- if (targetId !== undefined && isProtocolV2TargetStatusFinished(target.status)) {
2025
+ if (targetId !== undefined) {
1388
2026
  completedTargetIds.add(targetId);
1389
2027
  }
1390
2028
  });
1391
- const allExpectedTargetsCompleted =
1392
- expectedTargetIds.size > 0 &&
1393
- Array.from(expectedTargetIds).every(targetId => completedTargetIds.has(targetId));
1394
- if (allExpectedTargetsCompleted) {
2029
+ if (completedTargetIds.size === expectedTargetIds.size && expectedTargetIds.size > 0) {
2030
+ for (const target of completedTargets) {
2031
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2032
+ const payloadVersion = toProtocolV2FiniteNumber(target.payload_version);
2033
+ if (targetId === undefined) {
2034
+ throw ERRORS.TypedError(
2035
+ HardwareErrorCode.RuntimeError,
2036
+ 'Protocol V2 completed target identity is invalid'
2037
+ );
2038
+ }
2039
+ const expectedVersion = this.getExpectedProtocolV2TargetVersion(targetId);
2040
+ if (expectedVersion !== undefined && payloadVersion !== undefined) {
2041
+ if (payloadVersion !== expectedVersion) {
2042
+ throw ERRORS.TypedError(
2043
+ HardwareErrorCode.FirmwareVerificationFailed,
2044
+ `Protocol V2 target ${targetId} reached payload version ${payloadVersion}, expected ${expectedVersion}`
2045
+ );
2046
+ }
2047
+ }
2048
+ if (payloadVersion !== undefined) {
2049
+ this.protocolV2CompletedTargetVersions.set(targetId, payloadVersion);
2050
+ }
2051
+ }
1395
2052
  this.postProgressMessage(100, 'installingFirmware');
1396
2053
  return true;
1397
2054
  }
@@ -1429,7 +2086,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1429
2086
  private async waitForProtocolV2FirmwareUpdateComplete(
1430
2087
  targets: Array<{ target_id: number; path: string }>
1431
2088
  ) {
2089
+ this.protocolV2FinalStatusVerified = false;
1432
2090
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
2091
+ const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
1433
2092
  const startTime = Date.now();
1434
2093
  let lastError: unknown;
1435
2094
  let shouldReconnect = true;
@@ -1464,7 +2123,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1464
2123
  );
1465
2124
  const statusTargets = (statusResponse.message.records ??
1466
2125
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
1467
- if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) {
2126
+ if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
2127
+ this.protocolV2FinalStatusVerified = true;
1468
2128
  return;
1469
2129
  }
1470
2130
 
@@ -1512,10 +2172,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1512
2172
  lastError = new Error('Protocol V2 firmware targets are still installing');
1513
2173
  }
1514
2174
  } catch (error) {
1515
- if (
1516
- error instanceof HardwareError &&
1517
- error.errorCode === HardwareErrorCode.FirmwareError
1518
- ) {
2175
+ if (isProtocolV2TerminalInstallStatusError(error)) {
1519
2176
  throw error;
1520
2177
  }
1521
2178
  // Only consecutive full status dumps can prove that a record is absent.
@@ -1555,7 +2212,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1555
2212
  if (this.isProtocolV2ReconnectIdentityError(error)) {
1556
2213
  throw error;
1557
2214
  }
1558
- if (error instanceof HardwareError && error.errorCode === HardwareErrorCode.FirmwareError) {
2215
+ if (isProtocolV2TerminalInstallStatusError(error)) {
2216
+ throw error;
2217
+ }
2218
+ if (
2219
+ error instanceof HardwareError &&
2220
+ error.params?.firmwareUpdateCode === 'FirmwareInstallStatusUnavailable'
2221
+ ) {
1559
2222
  throw error;
1560
2223
  }
1561
2224
  shouldReconnect = true;
@@ -1586,6 +2249,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1586
2249
  Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
1587
2250
  }
1588
2251
  await this.protocolV2Reboot(DeviceRebootType.Normal);
2252
+ this.protocolV2ExecutionInLoader = false;
1589
2253
  }
1590
2254
 
1591
2255
  private async probeProtocolV2NormalMode(deviceInfo: ProtocolV2DeviceInfo) {
@@ -1608,6 +2272,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1608
2272
  const bootloaderVersion = getDeviceBootloaderVersion(features).join('.');
1609
2273
  const bleVersion = getDeviceBLEFirmwareVersion(features).join('.');
1610
2274
  const firmwareVersion = getDeviceFirmwareVersion(features).join('.');
2275
+ this.protocolV2LatestFinalFeatures = features;
1611
2276
  if (firmwareVersion === '0.0.0') {
1612
2277
  Log.warn(
1613
2278
  'Protocol V2 firmware update finished but app firmware version is still 0.0.0. This is allowed for Pro2 debug BLE-only update flows.'
@@ -1621,6 +2286,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1621
2286
  };
1622
2287
  }
1623
2288
 
2289
+ private async completeProtocolV2FinalVerification() {
2290
+ if (this.protocolV2ExecutionInLoader || this.isProtocolV2BootloaderMode()) {
2291
+ await this.exitProtocolV2BootloaderToNormal();
2292
+ }
2293
+ const versions = await this.waitForProtocolV2FinalFeatures();
2294
+ this.assertExpectedProtocolV2Versions();
2295
+ this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
2296
+ DevicePool.resetState();
2297
+ return versions;
2298
+ }
2299
+
1624
2300
  private async waitForProtocolV2ReconnectAndFeatures(timeout: number) {
1625
2301
  const startTime = Date.now();
1626
2302
  let lastError: unknown;
@@ -1742,194 +2418,50 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1742
2418
  }
1743
2419
 
1744
2420
  private isProtocolV2ReconnectIdentityError(error: unknown) {
1745
- // 序列号在 Loader 刚启动时可能暂时不可用,BLE descriptor 也没有稳定的
1746
- // path。此时应继续轮询,只有明确读到不同序列号时才立即终止更新。
2421
+ // A serial can be temporarily unavailable while Loader starts, and BLE has no stable path.
2422
+ // Keep polling unless the device explicitly reports a different serial.
1747
2423
  return this.normalizeErrorMessage(error).includes(
1748
2424
  'Protocol V2 reconnect physical identity mismatch'
1749
2425
  );
1750
2426
  }
1751
2427
 
1752
- private async protocolV2CommonUpdateProcess(params: ProtocolV2FileTransferParams) {
1753
- let lastError: unknown;
1754
- let confirmedFileOffset = 0;
1755
- const progressReportState: ProtocolV2ProgressReportState = {};
1756
- const updateConfirmedFileOffset = (offset: number) => {
1757
- confirmedFileOffset = offset;
1758
- };
1759
- for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1760
- try {
1761
- return await this.protocolV2WriteWholeFile(
1762
- params,
1763
- confirmedFileOffset,
1764
- updateConfirmedFileOffset,
1765
- progressReportState
1766
- );
1767
- } catch (error) {
1768
- lastError = error;
1769
- Log.error(
1770
- `Protocol V2 file transfer failed path=${params.filePath} attempt=${attempt}/${PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT} confirmedOffset=${confirmedFileOffset}`,
1771
- error
1772
- );
1773
- if (attempt === PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1774
- break;
1775
- }
1776
- await this.recoverProtocolV2FileTransfer();
1777
- confirmedFileOffset = await this.getProtocolV2ResumeOffset(
1778
- params.filePath,
1779
- confirmedFileOffset,
1780
- params.payload.byteLength
1781
- );
1782
- }
1783
- }
1784
-
1785
- throw ERRORS.TypedError(
1786
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1787
- `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1788
- );
1789
- }
1790
-
1791
- private async getProtocolV2ResumeOffset(
2428
+ private async fileWriteChunk(
1792
2429
  filePath: string,
1793
- confirmedFileOffset: number,
1794
- totalSize: number
1795
- ) {
1796
- if (confirmedFileOffset <= 0) {
1797
- return 0;
1798
- }
1799
-
1800
- try {
1801
- const response = await this.device
1802
- .getCommands()
1803
- .typedCall(
1804
- 'FilesystemPathInfoQuery',
1805
- 'FilesystemPathInfo',
1806
- { path: filePath },
1807
- { timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT }
1808
- );
1809
- const remoteSize = toProtocolV2FiniteNumber(response.message?.size);
1810
- const canResume =
1811
- response.message?.exist &&
1812
- !response.message?.directory &&
1813
- remoteSize !== undefined &&
1814
- Number.isSafeInteger(remoteSize) &&
1815
- remoteSize >= confirmedFileOffset &&
1816
- remoteSize <= totalSize;
1817
- if (canResume) {
1818
- Log.log(
1819
- `Protocol V2 file transfer resuming path=${filePath} confirmedOffset=${confirmedFileOffset} remoteSize=${remoteSize}`
1820
- );
1821
- return confirmedFileOffset;
1822
- }
1823
- Log.warn(
1824
- `Protocol V2 file transfer cannot resume path=${filePath} confirmedOffset=${confirmedFileOffset} remoteSize=${
1825
- remoteSize ?? 'unknown'
1826
- } exist=${!!response.message?.exist} directory=${!!response.message
1827
- ?.directory}; restarting from offset 0`
1828
- );
1829
- } catch (error) {
1830
- Log.warn(
1831
- `Protocol V2 file transfer staging state query failed path=${filePath}; restarting from offset 0`,
1832
- error
1833
- );
1834
- }
1835
- return 0;
1836
- }
1837
-
1838
- private async protocolV2WriteWholeFile(
1839
- {
1840
- payload,
1841
- filePath,
1842
- processedSize,
1843
- totalSize,
1844
- onTransferredBytes,
1845
- }: ProtocolV2FileTransferParams,
1846
- resumeOffset?: number,
1847
- onConfirmedFileOffset?: (confirmedFileOffset: number) => void,
1848
- progressReportState?: ProtocolV2ProgressReportState
1849
- ) {
1850
- const chunkSize = this.getProtocolV2FirmwareChunkSize(filePath);
1851
- const reportState = progressReportState ?? {};
1852
- const normalizedResumeOffset =
1853
- resumeOffset !== undefined && Number.isSafeInteger(resumeOffset) && resumeOffset > 0
1854
- ? Math.min(resumeOffset, payload.byteLength)
1855
- : 0;
1856
- const remainingPayload = payload.slice(normalizedResumeOffset);
1857
- const getUploadProgress = (fileOffset: number) => {
1858
- if (totalSize !== undefined && processedSize !== undefined) {
1859
- return Math.min(Math.ceil(((processedSize + fileOffset) / totalSize) * 100), 99);
1860
- }
1861
- return Math.min(Math.ceil((fileOffset / payload.byteLength) * 100), 99);
1862
- };
1863
-
1864
- try {
1865
- await writeProtocolV2File({
1866
- commands: this.device.getCommands(),
1867
- path: filePath,
1868
- data: remainingPayload,
1869
- offset: normalizedResumeOffset,
1870
- totalSize: payload.byteLength,
1871
- chunkSize,
1872
- chunkSizeLimit: chunkSize,
1873
- overwrite: normalizedResumeOffset === 0,
2430
+ totalFileSize: number,
2431
+ offset: number,
2432
+ chunk: ArrayBuffer | Buffer,
2433
+ overwrite: boolean,
2434
+ progress: number | null
2435
+ ): Promise<TypedResponseMessage<'FilesystemFile'>> {
2436
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
2437
+ const writeRes = await typedCall(
2438
+ 'FilesystemFileWrite',
2439
+ 'FilesystemFile',
2440
+ {
2441
+ file: {
2442
+ path: filePath,
2443
+ offset,
2444
+ total_size: totalFileSize,
2445
+ data: chunk,
2446
+ },
2447
+ overwrite,
1874
2448
  append: false,
2449
+ ui_percentage: progress ?? undefined,
2450
+ },
2451
+ {
1875
2452
  writeWithResponse: false,
1876
- maxChunkRetries: 3,
1877
- getUiPercentage: ({ offset, chunkLength }) =>
1878
- getProtocolV2DeviceTransferProgress(
1879
- (processedSize ?? 0) + offset,
1880
- (processedSize ?? 0) + offset + chunkLength,
1881
- totalSize ?? payload.byteLength
1882
- ),
1883
- onProgress: progress => {
1884
- const confirmedFileOffset = normalizedResumeOffset + progress.transferredBytes;
1885
- const transferredBytes = (processedSize ?? 0) + confirmedFileOffset;
1886
- onConfirmedFileOffset?.(confirmedFileOffset);
1887
- onTransferredBytes?.(transferredBytes);
1888
- const uploadProgress = getUploadProgress(confirmedFileOffset);
1889
- const now = Date.now();
1890
- const shouldReportProgress =
1891
- uploadProgress !== reportState.lastProgress ||
1892
- reportState.lastReportedAt === undefined ||
1893
- now - reportState.lastReportedAt >= PROTOCOL_V2_PROGRESS_HEARTBEAT_INTERVAL ||
1894
- confirmedFileOffset >= payload.byteLength;
1895
- if (shouldReportProgress) {
1896
- this.postProgressMessage(uploadProgress, 'transferData', {
1897
- transferredBytes,
1898
- totalBytes: totalSize ?? payload.byteLength,
1899
- rateBytesPerSecond: progress.rateBytesPerSecond,
1900
- elapsedMs: progress.elapsedMs,
1901
- });
1902
- reportState.lastProgress = uploadProgress;
1903
- reportState.lastReportedAt = now;
1904
- }
1905
- },
1906
- });
1907
- } catch (error) {
1908
- if (
1909
- error instanceof HardwareError &&
1910
- error.errorCode === HardwareErrorCode.RuntimeError &&
1911
- error.message.includes('FilesystemFileWrite')
1912
- ) {
1913
- throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, error.message);
2453
+ onWriteCompleted: () => undefined,
1914
2454
  }
1915
- throw error;
1916
- }
1917
-
1918
- return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0;
1919
- }
1920
-
1921
- private getProtocolV2FirmwareTransferTransport() {
1922
- const env = DataManager.getSettings('env');
1923
- if (env && DataManager.isBleConnect(env)) {
1924
- return 'BLE';
1925
- }
1926
- if (
1927
- env &&
1928
- (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'web')
1929
- ) {
1930
- return 'WebUSB';
2455
+ );
2456
+ if (writeRes.type !== 'FilesystemFile') {
2457
+ if ((writeRes as any).type === 'CallMethodError') {
2458
+ if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) {
2459
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
2460
+ }
2461
+ }
2462
+ throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
1931
2463
  }
1932
- return env ?? 'unknown';
2464
+ return writeRes;
1933
2465
  }
1934
2466
 
1935
2467
  private async recoverProtocolV2FileTransfer() {