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

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 (120) hide show
  1. package/__tests__/connectSettings.test.ts +5 -45
  2. package/__tests__/devicePoolBootloaderPath.test.ts +64 -0
  3. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +31 -59
  4. package/__tests__/protocol-v2.test.ts +100 -405
  5. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  6. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  7. package/dist/api/FirmwareUpdateV2.d.ts +0 -10
  8. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  9. package/dist/api/FirmwareUpdateV3.d.ts +3 -7
  10. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  11. package/dist/api/FirmwareUpdateV4.d.ts +5 -25
  12. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  13. package/dist/api/device/DeviceUpdateBootloader.d.ts +1 -5
  14. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  15. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +0 -8
  16. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  17. package/dist/api/firmware/getBinary.d.ts +0 -7
  18. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  19. package/dist/api/firmware/updateBootloader.d.ts +0 -2
  20. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  21. package/dist/api/firmware/uploadFirmware.d.ts +1 -9
  22. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  23. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  24. package/dist/core/index.d.ts.map +1 -1
  25. package/dist/data-manager/DataManager.d.ts +0 -1
  26. package/dist/data-manager/DataManager.d.ts.map +1 -1
  27. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  28. package/dist/device/Device.d.ts +0 -1
  29. package/dist/device/Device.d.ts.map +1 -1
  30. package/dist/device/DeviceConnector.d.ts +1 -1
  31. package/dist/device/DeviceConnector.d.ts.map +1 -1
  32. package/dist/device/DevicePool.d.ts +1 -0
  33. package/dist/device/DevicePool.d.ts.map +1 -1
  34. package/dist/index.d.ts +2 -180
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1078 -3083
  37. package/dist/inject.d.ts.map +1 -1
  38. package/dist/lowLevelInject.d.ts.map +1 -1
  39. package/dist/topLevelInject.d.ts.map +1 -1
  40. package/dist/types/api/checkAllFirmwareRelease.d.ts +0 -4
  41. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  42. package/dist/types/api/deviceUpdateBootloader.d.ts +0 -6
  43. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  44. package/dist/types/api/export.d.ts +1 -4
  45. package/dist/types/api/export.d.ts.map +1 -1
  46. package/dist/types/api/firmwareUpdate.d.ts +0 -68
  47. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  48. package/dist/types/api/index.d.ts +1 -9
  49. package/dist/types/api/index.d.ts.map +1 -1
  50. package/dist/types/settings.d.ts +0 -13
  51. package/dist/types/settings.d.ts.map +1 -1
  52. package/dist/utils/deviceFeaturesCompat.d.ts +0 -1
  53. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  54. package/package.json +4 -4
  55. package/src/api/CheckAllFirmwareRelease.ts +6 -57
  56. package/src/api/FirmwareUpdate.ts +1 -13
  57. package/src/api/FirmwareUpdateV2.ts +121 -296
  58. package/src/api/FirmwareUpdateV3.ts +57 -263
  59. package/src/api/FirmwareUpdateV4.ts +349 -881
  60. package/src/api/device/DeviceUpdateBootloader.ts +6 -125
  61. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +1 -72
  62. package/src/api/firmware/updateBootloader.ts +4 -19
  63. package/src/api/firmware/uploadFirmware.ts +22 -140
  64. package/src/core/index.ts +16 -0
  65. package/src/data-manager/DataManager.ts +54 -62
  66. package/src/data-manager/connectSettings.ts +0 -11
  67. package/src/device/Device.ts +3 -7
  68. package/src/device/DeviceConnector.ts +2 -2
  69. package/src/device/DevicePool.ts +15 -1
  70. package/src/index.ts +0 -5
  71. package/src/inject.ts +2 -22
  72. package/src/lowLevelInject.ts +1 -5
  73. package/src/topLevelInject.ts +1 -5
  74. package/src/types/api/checkAllFirmwareRelease.ts +0 -4
  75. package/src/types/api/deviceUpdateBootloader.ts +0 -6
  76. package/src/types/api/export.ts +0 -18
  77. package/src/types/api/firmwareUpdate.ts +0 -75
  78. package/src/types/api/index.ts +0 -14
  79. package/src/types/settings.ts +0 -13
  80. package/src/utils/deviceFeaturesCompat.ts +0 -6
  81. package/__tests__/device-initialize-timeout.test.ts +0 -56
  82. package/__tests__/firmware-update/check-all-firmware-release.test.ts +0 -175
  83. package/__tests__/firmware-update/device-update-bootloader.test.ts +0 -87
  84. package/__tests__/firmware-update/firmware-artifact-source.test.ts +0 -119
  85. package/__tests__/firmware-update/firmware-host-binding.test.ts +0 -70
  86. package/__tests__/firmware-update/firmware-preparation-error.test.ts +0 -27
  87. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +0 -200
  88. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +0 -13
  89. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +0 -394
  90. package/__tests__/firmware-update/firmware-update-plan.test.ts +0 -593
  91. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +0 -231
  92. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +0 -64
  93. package/__tests__/firmware-update/firmware-upload-source.test.ts +0 -70
  94. package/dist/api/firmware/FirmwareArtifactSource.d.ts +0 -25
  95. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +0 -1
  96. package/dist/api/firmware/FirmwareHostBinding.d.ts +0 -6
  97. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +0 -1
  98. package/dist/api/firmware/FirmwarePreparationError.d.ts +0 -3
  99. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +0 -1
  100. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +0 -3
  101. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +0 -1
  102. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -25
  103. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +0 -1
  104. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +0 -32
  105. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +0 -1
  106. package/dist/types/api/firmwareUpdateCapabilities.d.ts +0 -9
  107. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +0 -1
  108. package/dist/types/api/firmwareUpdatePlan.d.ts +0 -28
  109. package/dist/types/api/firmwareUpdatePlan.d.ts.map +0 -1
  110. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +0 -42
  111. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +0 -1
  112. package/src/api/firmware/FirmwareArtifactSource.ts +0 -278
  113. package/src/api/firmware/FirmwareHostBinding.ts +0 -100
  114. package/src/api/firmware/FirmwarePreparationError.ts +0 -12
  115. package/src/api/firmware/FirmwareUpdateCapabilities.ts +0 -9
  116. package/src/api/firmware/FirmwareUpdatePlan.ts +0 -772
  117. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +0 -449
  118. package/src/types/api/firmwareUpdateCapabilities.ts +0 -9
  119. package/src/types/api/firmwareUpdatePlan.ts +0 -38
  120. package/src/types/api/firmwareUpdatePreparedPlan.ts +0 -53
@@ -2,7 +2,6 @@ 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,
6
5
  PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE,
7
6
  PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
8
7
  } from '@onekeyfe/hd-transport';
@@ -10,6 +9,7 @@ import { sha256 } from '@noble/hashes/sha256';
10
9
 
11
10
  import { FirmwareUpdateTipMessage, UI_REQUEST } from '../events/ui-request';
12
11
  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,7 +21,6 @@ import {
21
21
  getLogger,
22
22
  } from '../utils';
23
23
  import { getSysResourceBinary } from './firmware/getBinary';
24
- import { normalizeFirmwarePreparationError } from './firmware/FirmwarePreparationError';
25
24
  import { DataManager } from '../data-manager';
26
25
  import { FirmwareUpdateBaseMethod } from './firmware/FirmwareUpdateBaseMethod';
27
26
  import { DevicePool } from '../device/DevicePool';
@@ -40,20 +39,11 @@ import {
40
39
  getProtocolV2UnknownErrorText,
41
40
  isProtocolV2DeviceDisconnectedError,
42
41
  } 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';
49
42
 
50
- import type {
51
- FirmwareArtifactReference,
52
- FirmwareUpdateV4Params,
53
- FirmwareUpdateV4Target,
54
- } from '../types/api/firmwareUpdate';
43
+ import type { FirmwareUpdateV4Params, FirmwareUpdateV4Target } from '../types/api/firmwareUpdate';
55
44
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
56
45
  import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
46
+ import type { PROTO } from '../constants';
57
47
  import type { TypedResponseMessage } from '../device/DeviceCommands';
58
48
  import type {
59
49
  Features,
@@ -63,14 +53,9 @@ import type {
63
53
  IProtocolV2ResourceFile,
64
54
  IVersionArray,
65
55
  } from '../types';
66
- import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
67
56
 
68
57
  const Log = getLogger(LoggerNames.Method);
69
58
 
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';
74
59
  const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
75
60
  const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
76
61
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
@@ -90,7 +75,7 @@ const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
90
75
  const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
91
76
  const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
92
77
  const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
93
- const PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE = 'FirmwareInstallStatusConflict';
78
+ const PROTOCOL_V2_PROGRESS_HEARTBEAT_INTERVAL = 1000;
94
79
  const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
95
80
  const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
96
81
  const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
@@ -135,6 +120,11 @@ const getProtocolV2DeviceTransferProgress = (
135
120
  return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
136
121
  };
137
122
 
123
+ const formatProtocolV2TransferSpeed = (bytes: number, elapsedMs: number) => {
124
+ const safeElapsedMs = Math.max(elapsedMs, 1);
125
+ return (bytes / 1024 / (safeElapsedMs / 1000)).toFixed(2);
126
+ };
127
+
138
128
  type ProtocolV2FirmwareUpdateStatusTarget = {
139
129
  target_id: number | string;
140
130
  status?: number | string;
@@ -148,6 +138,22 @@ type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId:
148
138
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
149
139
  kind: ProtocolV2RemoteComponentTarget['kind'];
150
140
  };
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
+
151
157
  /** Protocol V2 resource file written independently to devicePath through FileWrite. */
152
158
  type ProtocolV2ResourceBundleBinary = {
153
159
  name: string;
@@ -165,35 +171,6 @@ type ProtocolV2RemoteComponentTarget = {
165
171
  kind: 'bootloader' | 'firmware';
166
172
  };
167
173
 
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
-
197
174
  type ProtocolV2OkppHeader = {
198
175
  type: string;
199
176
  version: IVersionArray;
@@ -302,12 +279,6 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
302
279
  );
303
280
  };
304
281
 
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
-
311
282
  const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) =>
312
283
  normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED;
313
284
 
@@ -345,18 +316,6 @@ const normalizeProtocolV2TargetStatus = (
345
316
 
346
317
  const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
347
318
 
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
-
360
319
  const bytesToHex = (bytes: Uint8Array) =>
361
320
  Array.from(bytes)
362
321
  .map(byte => byte.toString(16).padStart(2, '0'))
@@ -490,16 +449,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
490
449
  return ['V2'] as const;
491
450
  }
492
451
 
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
-
503
452
  init() {
504
453
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
505
454
  this.requireDeviceMode = [];
@@ -537,62 +486,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
537
486
  { name: 'firmwareType', type: 'string' },
538
487
  { name: 'targetsToUpdate', type: 'array', allowEmpty: true },
539
488
  { name: 'platform', type: 'string' },
540
- { name: 'expectedDeviceId', type: 'string' },
541
489
  { name: 'resourceFiles', type: 'array', allowEmpty: true },
542
490
  ]);
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
- }
593
491
 
594
492
  this.params = {
595
- preparedPlan: payload.preparedPlan,
596
493
  chunkSize: payload.chunkSize,
597
494
  forcedUpdateRes: payload.forcedUpdateRes,
598
495
  bootloaderBinary: payload.bootloaderBinary,
@@ -607,29 +504,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
607
504
  resourceFiles: payload.resourceFiles,
608
505
  firmwareType: payload.firmwareType,
609
506
  targetsToUpdate: payload.targetsToUpdate,
610
- expectedTargetVersions: payload.expectedTargetVersions,
611
507
  platform: payload.platform,
612
- expectedDeviceId: payload.expectedDeviceId,
613
- artifactReader: hostBinding?.artifactReader ?? payload.artifactReader,
614
- componentArtifacts: payload.componentArtifacts,
615
- resourceBundleArtifacts: payload.resourceBundleArtifacts,
616
508
  };
617
509
  }
618
510
 
619
- private getProtocolV2FirmwareChunkSize(direction: 'read' | 'write', filePath?: string) {
511
+ private getProtocolV2FirmwareChunkSize(filePath?: string) {
620
512
  const payloadChunkSize = Number(this.params?.chunkSize);
621
513
  const env = DataManager.getSettings('env');
622
514
  const isBle = this.params?.platform === 'native' || (env && DataManager.isBleConnect(env));
623
515
  let maxChunkSize = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
624
516
  if (isBle) {
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
- }
517
+ maxChunkSize = PROTOCOL_V2_FIRMWARE_STAGING_PATHS.has(filePath ?? '')
518
+ ? PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE
519
+ : PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
633
520
  }
634
521
  if (!Number.isFinite(payloadChunkSize) || payloadChunkSize <= 0) {
635
522
  return maxChunkSize;
@@ -656,20 +543,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
656
543
  assertProtocolV2FirmwareTargetsSupported(capabilityDeviceType, this.params);
657
544
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
658
545
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
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);
546
+ const needsRemoteResources =
547
+ !this.params.resourceFiles?.length && !!this.params.targetsToUpdate?.includes('resource');
673
548
 
674
549
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
675
550
  let bootloaderBinary: ArrayBuffer | null = null;
@@ -681,19 +556,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
681
556
  fwBinaryMap = this.collectExplicitTargetBinaries();
682
557
  bootloaderBinary = this.prepareBootloaderBinary();
683
558
  const needsRemoteFirmware = !this.hasExplicitProtocolV2Payload(fwBinaryMap);
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
- }
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);
697
564
  if (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) {
698
565
  // Remote updates must use a freshly fetched config before any reboot or file write.
699
566
  await DataManager.forceReloadData({
@@ -720,7 +587,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
720
587
  if (err instanceof HardwareError && err.errorCode === HardwareErrorCode.NetworkError) {
721
588
  throw err;
722
589
  }
723
- throw normalizeFirmwarePreparationError(err);
590
+ throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
724
591
  }
725
592
 
726
593
  if (
@@ -736,8 +603,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
736
603
  );
737
604
  }
738
605
 
606
+ const enteredBootloader = await this.enterProtocolV2BootloaderMode();
607
+
739
608
  if (needsRemoteResources) {
740
- const enteredBootloader = await this.enterProtocolV2BootloaderMode();
741
609
  try {
742
610
  const stableResources = await this.prepareProtocolV2ResourceBundles();
743
611
  resourceBundles = this.mergeProtocolV2ResourceBundles(resourceBundles, stableResources);
@@ -753,293 +621,25 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
753
621
  );
754
622
  }
755
623
  }
756
- throw normalizeFirmwarePreparationError(err);
624
+ throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
757
625
  }
758
626
  }
759
627
 
760
- return this.executeProtocolV2Update({
628
+ await this.executeProtocolV2Update({
761
629
  fwBinaryMap,
762
630
  bootloaderBinary,
763
631
  ...(installItems ? { installItems } : undefined),
764
632
  ...(resourceBundles?.length ? { resourceBundles } : undefined),
765
633
  });
766
- }
767
-
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
- }
785
-
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
- }
797
-
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
634
 
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
- }
635
+ this.postTipMessage(FirmwareUpdateTipMessage.SwitchFirmwareReconnectDevice);
636
+ await this.exitProtocolV2BootloaderToNormal();
855
637
 
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
- }
638
+ const versions = await this.waitForProtocolV2FinalFeatures();
639
+ this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
640
+ DevicePool.resetState();
1022
641
 
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);
642
+ return versions;
1043
643
  }
1044
644
 
1045
645
  private async getProtocolV2DeviceFeatures(): Promise<Features> {
@@ -1083,17 +683,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1083
683
  private async captureProtocolV2PhysicalIdentity() {
1084
684
  const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
1085
685
  const serialNumber = this.getProtocolV2SerialNumber(deviceInfo);
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
- }
686
+ const path = this.device.originalDescriptor.path?.trim() || undefined;
687
+ assertProtocolV2ReconnectIdentity(serialNumber, serialNumber, path, path);
1097
688
  this.protocolV2ExpectedSerialNumber = serialNumber;
1098
689
  this.protocolV2ExpectedPath = path;
1099
690
  }
@@ -1329,7 +920,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1329
920
  }
1330
921
 
1331
922
  const digest = sha256.create();
1332
- const chunkSize = this.getProtocolV2FirmwareChunkSize('write');
923
+ const chunkSize = this.getProtocolV2FirmwareChunkSize();
1333
924
  let offset = 0;
1334
925
  while (offset < file.size) {
1335
926
  const response = await commands.typedCall(
@@ -1424,7 +1015,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1424
1015
  : await readProtocolV2ResourceInventory({
1425
1016
  commands: this.device.getCommands(),
1426
1017
  resources,
1427
- chunkSize: this.getProtocolV2FirmwareChunkSize('write'),
1018
+ chunkSize: this.getProtocolV2FirmwareChunkSize(),
1428
1019
  timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1429
1020
  });
1430
1021
  const plan = buildProtocolV2ResourceUpdatePlan({
@@ -1459,88 +1050,38 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1459
1050
  };
1460
1051
  }
1461
1052
 
1462
- private getProtocolV2ResourceFilePath(path: string) {
1463
- if (path.startsWith('vol')) return path;
1464
- if (path.startsWith('/')) return `vol0:${path}`;
1465
- return `vol0:/${path}`;
1466
- }
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();
1467
1059
 
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
- }
1060
+ // Write directly to the device through FileWrite.
1061
+ let totalSize = 0;
1062
+ for (const b of bundles) totalSize += b.binary.byteLength;
1483
1063
 
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,
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,
1497
1075
  });
1498
- const data = toProtocolV2Bytes(res.message?.data);
1499
- if (data.byteLength === 0) return null;
1500
- chunks.push(data);
1501
- offset += data.byteLength;
1502
1076
  }
1503
1077
 
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
- }
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 };
1544
1085
  }
1545
1086
 
1546
1087
  private isProtocolV2BootloaderMode() {
@@ -1572,12 +1113,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1572
1113
  // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
1573
1114
  if (this.isProtocolV2RomloaderMode()) {
1574
1115
  Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
1575
- this.protocolV2ExecutionInLoader = true;
1576
1116
  return false;
1577
1117
  }
1578
1118
  if (this.isProtocolV2BootloaderMode()) {
1579
1119
  Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
1580
- this.protocolV2ExecutionInLoader = true;
1581
1120
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1582
1121
  return false;
1583
1122
  }
@@ -1587,7 +1126,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1587
1126
  await this.protocolV2Reboot(DeviceRebootType.Bootloader);
1588
1127
  await wait(1000);
1589
1128
  await this.waitForProtocolV2BootloaderMode();
1590
- this.protocolV2ExecutionInLoader = true;
1591
1129
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1592
1130
  return true;
1593
1131
  } catch (error) {
@@ -1683,98 +1221,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1683
1221
  return entries;
1684
1222
  }
1685
1223
 
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
-
1778
1224
  private async executeProtocolV2Update({
1779
1225
  fwBinaryMap,
1780
1226
  bootloaderBinary,
@@ -1786,168 +1232,106 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1786
1232
  installItems?: ProtocolV2InstallItem[];
1787
1233
  resourceBundles?: ProtocolV2ResourceBundleBinary[];
1788
1234
  }) {
1789
- const memoryInstallItems =
1235
+ const orderedInstallItems =
1790
1236
  installItems ??
1791
1237
  this.buildProtocolV2InstallItems({
1792
- fwBinaryMap: fwBinaryMap ?? [],
1793
1238
  bootloaderBinary: bootloaderBinary ?? null,
1239
+ fwBinaryMap: fwBinaryMap ?? [],
1794
1240
  });
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
- }))
1803
- );
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();
1817
- }
1818
- }
1819
1241
 
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;
1832
- }
1833
- }
1242
+ let firmwareSize = 0;
1243
+ for (const item of orderedInstallItems) firmwareSize += item.binary.byteLength;
1834
1244
 
1835
1245
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
1246
+
1836
1247
  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);
1248
+ let totalSize = firmwareSize;
1249
+ if (resourceBundles?.length) {
1250
+ const resourceTransfer = await this.syncProtocolV2ResourceBundles(
1251
+ resourceBundles,
1252
+ firmwareSize
1253
+ );
1254
+ processedSize = resourceTransfer.processedSize;
1255
+ totalSize = resourceTransfer.totalSize;
1845
1256
  }
1846
1257
 
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
- });
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');
1263
+ }
1264
+ return;
1861
1265
  }
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
+ );
1862
1278
 
1863
- if (totalSize > 0) {
1864
- this.postProgressMessage(100, 'transferData');
1865
- }
1866
- if (stagedInstallTargets.length === 0) {
1867
- return;
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
+ }
1302
+
1303
+ if (totalSize > 0) {
1304
+ this.postProgressMessage(100, 'transferData');
1305
+ }
1306
+
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;
1868
1321
  }
1869
1322
 
1870
1323
  this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
1871
- const targets = stagedInstallTargets.map(item => ({
1324
+
1325
+ const allTargets = stagedInstallTargets.map(item => ({
1872
1326
  target_id: item.targetId,
1873
1327
  path: item.path,
1874
1328
  }));
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
- );
1329
+ Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(allTargets)}`);
1330
+ await this.protocolV2StartFirmwareUpdate({ targets: allTargets });
1331
+ await this.waitForProtocolV2FirmwareUpdateComplete(allTargets);
1948
1332
  }
1949
1333
 
1950
- private getProtocolV2InstallItemStagingPath(item: Pick<ProtocolV2InstallItem, 'fileName'>) {
1334
+ private getProtocolV2InstallItemStagingPath(item: ProtocolV2InstallItem) {
1951
1335
  return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`;
1952
1336
  }
1953
1337
 
@@ -1966,8 +1350,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1966
1350
 
1967
1351
  private assertProtocolV2TargetStatus(
1968
1352
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
1969
- expectedTargetIds: Set<number>,
1970
- expectedPaths = new Map<number, string>()
1353
+ expectedTargetIds: Set<number>
1971
1354
  ) {
1972
1355
  Log.log(
1973
1356
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
@@ -1998,57 +1381,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1998
1381
  const matchingTargets = statusTargets.filter(target =>
1999
1382
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
2000
1383
  );
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
- );
2022
1384
  const completedTargetIds = new Set<number>();
2023
- completedTargets.forEach(target => {
1385
+ matchingTargets.forEach(target => {
2024
1386
  const targetId = normalizeProtocolV2TargetId(target.target_id);
2025
- if (targetId !== undefined) {
1387
+ if (targetId !== undefined && isProtocolV2TargetStatusFinished(target.status)) {
2026
1388
  completedTargetIds.add(targetId);
2027
1389
  }
2028
1390
  });
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
- }
1391
+ const allExpectedTargetsCompleted =
1392
+ expectedTargetIds.size > 0 &&
1393
+ Array.from(expectedTargetIds).every(targetId => completedTargetIds.has(targetId));
1394
+ if (allExpectedTargetsCompleted) {
2052
1395
  this.postProgressMessage(100, 'installingFirmware');
2053
1396
  return true;
2054
1397
  }
@@ -2086,9 +1429,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2086
1429
  private async waitForProtocolV2FirmwareUpdateComplete(
2087
1430
  targets: Array<{ target_id: number; path: string }>
2088
1431
  ) {
2089
- this.protocolV2FinalStatusVerified = false;
2090
1432
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
2091
- const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
2092
1433
  const startTime = Date.now();
2093
1434
  let lastError: unknown;
2094
1435
  let shouldReconnect = true;
@@ -2123,8 +1464,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2123
1464
  );
2124
1465
  const statusTargets = (statusResponse.message.records ??
2125
1466
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
2126
- if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
2127
- this.protocolV2FinalStatusVerified = true;
1467
+ if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) {
2128
1468
  return;
2129
1469
  }
2130
1470
 
@@ -2172,7 +1512,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2172
1512
  lastError = new Error('Protocol V2 firmware targets are still installing');
2173
1513
  }
2174
1514
  } catch (error) {
2175
- if (isProtocolV2TerminalInstallStatusError(error)) {
1515
+ if (
1516
+ error instanceof HardwareError &&
1517
+ error.errorCode === HardwareErrorCode.FirmwareError
1518
+ ) {
2176
1519
  throw error;
2177
1520
  }
2178
1521
  // Only consecutive full status dumps can prove that a record is absent.
@@ -2212,13 +1555,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2212
1555
  if (this.isProtocolV2ReconnectIdentityError(error)) {
2213
1556
  throw error;
2214
1557
  }
2215
- if (isProtocolV2TerminalInstallStatusError(error)) {
2216
- throw error;
2217
- }
2218
- if (
2219
- error instanceof HardwareError &&
2220
- error.params?.firmwareUpdateCode === 'FirmwareInstallStatusUnavailable'
2221
- ) {
1558
+ if (error instanceof HardwareError && error.errorCode === HardwareErrorCode.FirmwareError) {
2222
1559
  throw error;
2223
1560
  }
2224
1561
  shouldReconnect = true;
@@ -2249,7 +1586,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2249
1586
  Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
2250
1587
  }
2251
1588
  await this.protocolV2Reboot(DeviceRebootType.Normal);
2252
- this.protocolV2ExecutionInLoader = false;
2253
1589
  }
2254
1590
 
2255
1591
  private async probeProtocolV2NormalMode(deviceInfo: ProtocolV2DeviceInfo) {
@@ -2272,7 +1608,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2272
1608
  const bootloaderVersion = getDeviceBootloaderVersion(features).join('.');
2273
1609
  const bleVersion = getDeviceBLEFirmwareVersion(features).join('.');
2274
1610
  const firmwareVersion = getDeviceFirmwareVersion(features).join('.');
2275
- this.protocolV2LatestFinalFeatures = features;
2276
1611
  if (firmwareVersion === '0.0.0') {
2277
1612
  Log.warn(
2278
1613
  'Protocol V2 firmware update finished but app firmware version is still 0.0.0. This is allowed for Pro2 debug BLE-only update flows.'
@@ -2286,17 +1621,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2286
1621
  };
2287
1622
  }
2288
1623
 
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
-
2300
1624
  private async waitForProtocolV2ReconnectAndFeatures(timeout: number) {
2301
1625
  const startTime = Date.now();
2302
1626
  let lastError: unknown;
@@ -2418,50 +1742,194 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2418
1742
  }
2419
1743
 
2420
1744
  private isProtocolV2ReconnectIdentityError(error: unknown) {
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.
1745
+ // 序列号在 Loader 刚启动时可能暂时不可用,BLE descriptor 也没有稳定的
1746
+ // path。此时应继续轮询,只有明确读到不同序列号时才立即终止更新。
2423
1747
  return this.normalizeErrorMessage(error).includes(
2424
1748
  'Protocol V2 reconnect physical identity mismatch'
2425
1749
  );
2426
1750
  }
2427
1751
 
2428
- private async fileWriteChunk(
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(
2429
1792
  filePath: string,
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,
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,
2448
1874
  append: false,
2449
- ui_percentage: progress ?? undefined,
2450
- },
2451
- {
2452
1875
  writeWithResponse: false,
2453
- onWriteCompleted: () => undefined,
2454
- }
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
- }
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);
2461
1914
  }
2462
- throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
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';
2463
1931
  }
2464
- return writeRes;
1932
+ return env ?? 'unknown';
2465
1933
  }
2466
1934
 
2467
1935
  private async recoverProtocolV2FileTransfer() {