@onekeyfe/hd-core 1.2.0-alpha.66 → 1.2.0-alpha.68

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 +47 -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 +63 -0
  15. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  16. package/__tests__/protocol-v2.test.ts +371 -94
  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 -4
  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 +3020 -955
  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 +863 -243
  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 +59 -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
@@ -1,6 +1,7 @@
1
1
  import { EDeviceType, ERRORS, HardwareError, HardwareErrorCode, wait } from '@onekeyfe/hd-shared';
2
2
  import {
3
3
  DeviceRebootType,
4
+ PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE,
4
5
  PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
5
6
  PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
6
7
  } from '@onekeyfe/hd-transport';
@@ -8,7 +9,6 @@ import { sha256 } from '@noble/hashes/sha256';
8
9
 
9
10
  import { FirmwareUpdateTipMessage, UI_REQUEST } from '../events/ui-request';
10
11
  import { validateProtocolV2FilesystemPath } from './helpers/filesystemValidation';
11
- import { writeProtocolV2File } from './helpers/protocolV2FileWrite';
12
12
  import { validateParams } from './helpers/paramsValidator';
13
13
  import {
14
14
  LoggerNames,
@@ -19,6 +19,7 @@ import {
19
19
  getLogger,
20
20
  } from '../utils';
21
21
  import { getSysResourceBinary } from './firmware/getBinary';
22
+ import { normalizeFirmwarePreparationError } from './firmware/FirmwarePreparationError';
22
23
  import { DataManager } from '../data-manager';
23
24
  import { FirmwareUpdateBaseMethod } from './firmware/FirmwareUpdateBaseMethod';
24
25
  import { DevicePool } from '../device/DevicePool';
@@ -37,11 +38,20 @@ import {
37
38
  getProtocolV2UnknownErrorText,
38
39
  isProtocolV2DeviceDisconnectedError,
39
40
  } from './protocol-v2/helpers';
41
+ import { openFirmwareByteSource, writeFirmwareByteSource } from './firmware/FirmwareArtifactSource';
42
+ import { resolveFirmwareUpdateHostBinding } from './firmware/FirmwareHostBinding';
43
+ import {
44
+ assertFirmwareUpdatePreparedPlanBinding,
45
+ assertFirmwareUpdatePreparedPlanDeviceIdentity,
46
+ } from './firmware/FirmwareUpdatePreparedPlan';
40
47
 
41
- import type { FirmwareUpdateV4Params, FirmwareUpdateV4Target } from '../types/api/firmwareUpdate';
48
+ import type {
49
+ FirmwareArtifactReference,
50
+ FirmwareUpdateV4Params,
51
+ FirmwareUpdateV4Target,
52
+ } from '../types/api/firmwareUpdate';
42
53
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
43
54
  import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
44
- import type { PROTO } from '../constants';
45
55
  import type { TypedResponseMessage } from '../device/DeviceCommands';
46
56
  import type {
47
57
  Features,
@@ -51,6 +61,7 @@ import type {
51
61
  IProtocolV2ResourceFile,
52
62
  IVersionArray,
53
63
  } from '../types';
64
+ import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
54
65
 
55
66
  const Log = getLogger(LoggerNames.Method);
56
67
 
@@ -73,6 +84,7 @@ const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
73
84
  const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
74
85
  const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
75
86
  const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
87
+ const PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE = 'FirmwareInstallStatusConflict';
76
88
  const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
77
89
  const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
78
90
  const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
@@ -95,11 +107,6 @@ const getProtocolV2DeviceTransferProgress = (
95
107
  return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
96
108
  };
97
109
 
98
- const formatProtocolV2TransferSpeed = (bytes: number, elapsedMs: number) => {
99
- const safeElapsedMs = Math.max(elapsedMs, 1);
100
- return (bytes / 1024 / (safeElapsedMs / 1000)).toFixed(2);
101
- };
102
-
103
110
  type ProtocolV2FirmwareUpdateStatusTarget = {
104
111
  target_id: number | string;
105
112
  status?: number | string;
@@ -113,17 +120,6 @@ type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId:
113
120
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
114
121
  kind: ProtocolV2RemoteComponentTarget['kind'];
115
122
  };
116
- type ProtocolV2InstallTarget = ProtocolV2InstallItem & {
117
- path: string;
118
- };
119
-
120
- type ProtocolV2FileTransferParams = PROTO.FirmwareUpload & {
121
- filePath: string;
122
- processedSize?: number;
123
- totalSize?: number;
124
- onTransferredBytes?: (transferredBytes: number) => void;
125
- };
126
-
127
123
  /** Protocol V2 resource file written independently to devicePath through FileWrite. */
128
124
  type ProtocolV2ResourceBundleBinary = {
129
125
  name: string;
@@ -141,6 +137,35 @@ type ProtocolV2RemoteComponentTarget = {
141
137
  kind: 'bootloader' | 'firmware';
142
138
  };
143
139
 
140
+ type ProtocolV2InstallSource = {
141
+ fileName: string;
142
+ source: FirmwareByteSource;
143
+ targetId: number;
144
+ kind: ProtocolV2RemoteComponentTarget['kind'];
145
+ };
146
+
147
+ type ProtocolV2ResourceBundleSource = {
148
+ name: string;
149
+ source: FirmwareByteSource;
150
+ devicePath: string;
151
+ version?: IVersionArray;
152
+ payloadHash?: string;
153
+ headerHash?: string;
154
+ };
155
+
156
+ type ProtocolV2ExecutionPhaseKind =
157
+ | 'resource-sync'
158
+ | 'bootloader-install'
159
+ | 'bootloader-verify'
160
+ | 'component-install'
161
+ | 'final-verify';
162
+
163
+ type ProtocolV2ExecutionPhase = {
164
+ kind: ProtocolV2ExecutionPhaseKind;
165
+ installSources: ProtocolV2InstallSource[];
166
+ resourceSources: ProtocolV2ResourceBundleSource[];
167
+ };
168
+
144
169
  type ProtocolV2OkppHeader = {
145
170
  type: string;
146
171
  version: IVersionArray;
@@ -243,6 +268,12 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
243
268
  );
244
269
  };
245
270
 
271
+ const isProtocolV2TerminalInstallStatusError = (error: unknown) =>
272
+ error instanceof HardwareError &&
273
+ (error.errorCode === HardwareErrorCode.FirmwareError ||
274
+ error.errorCode === HardwareErrorCode.FirmwareVerificationFailed ||
275
+ error.params?.firmwareUpdateCode === PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE);
276
+
246
277
  const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) =>
247
278
  normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED;
248
279
 
@@ -280,6 +311,18 @@ const normalizeProtocolV2TargetStatus = (
280
311
 
281
312
  const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
282
313
 
314
+ const versionArrayToNumber = (version?: IVersionArray) => {
315
+ if (!version) return undefined;
316
+ return version[0] * 0x10000 + version[1] * 0x100 + version[2];
317
+ };
318
+
319
+ const compareProtocolV2Versions = (current?: IVersionArray, target?: IVersionArray) => {
320
+ const currentNumber = versionArrayToNumber(current);
321
+ const targetNumber = versionArrayToNumber(target);
322
+ if (currentNumber === undefined || targetNumber === undefined) return undefined;
323
+ return currentNumber - targetNumber;
324
+ };
325
+
283
326
  const bytesToHex = (bytes: Uint8Array) =>
284
327
  Array.from(bytes)
285
328
  .map(byte => byte.toString(16).padStart(2, '0'))
@@ -413,6 +456,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
413
456
  return ['V2'] as const;
414
457
  }
415
458
 
459
+ private protocolV2PreparedSources: FirmwareByteSource[] = [];
460
+
461
+ private protocolV2ExecutionInLoader = false;
462
+
463
+ private protocolV2CompletedTargetVersions = new Map<number, number>();
464
+
465
+ private protocolV2LatestFinalFeatures?: Features;
466
+
467
+ private protocolV2FinalStatusVerified = false;
468
+
416
469
  init() {
417
470
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
418
471
  this.requireDeviceMode = [];
@@ -450,10 +503,62 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
450
503
  { name: 'firmwareType', type: 'string' },
451
504
  { name: 'targetsToUpdate', type: 'array', allowEmpty: true },
452
505
  { name: 'platform', type: 'string' },
506
+ { name: 'expectedDeviceId', type: 'string' },
453
507
  { name: 'resourceFiles', type: 'array', allowEmpty: true },
454
508
  ]);
509
+ if (
510
+ payload.expectedDeviceId !== undefined &&
511
+ (payload.expectedDeviceId.length === 0 || payload.expectedDeviceId.length > 160)
512
+ ) {
513
+ throw ERRORS.TypedError(
514
+ HardwareErrorCode.CallMethodInvalidParameter,
515
+ 'Protocol V2 expected device identity is invalid'
516
+ );
517
+ }
518
+ const hostBinding =
519
+ payload.preparedPlan || payload.componentArtifacts || payload.resourceBundleArtifacts
520
+ ? resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration)
521
+ : undefined;
522
+ if (hostBinding) {
523
+ assertFirmwareUpdatePreparedPlanBinding({
524
+ preparedPlan: payload.preparedPlan,
525
+ executor: 'v4',
526
+ platform: payload.platform,
527
+ scopeTargets: [
528
+ 'boot',
529
+ 'app_v1',
530
+ 'app_v2',
531
+ 'coprocessor',
532
+ 'resource',
533
+ 'se01',
534
+ 'se02',
535
+ 'se03',
536
+ 'se04',
537
+ ],
538
+ bindings: [
539
+ ...Object.entries(payload.componentArtifacts ?? {}).flatMap(([target, artifact]) =>
540
+ artifact
541
+ ? [
542
+ {
543
+ target: target as Exclude<FirmwareUpdateV4Target, 'resource'>,
544
+ artifact,
545
+ },
546
+ ]
547
+ : []
548
+ ),
549
+ ...(payload.resourceBundleArtifacts ?? []).map(
550
+ (entry: { name: string; artifact: FirmwareArtifactReference }) => ({
551
+ target: 'resource' as const,
552
+ logicalName: entry.name,
553
+ artifact: entry.artifact,
554
+ })
555
+ ),
556
+ ],
557
+ });
558
+ }
455
559
 
456
560
  this.params = {
561
+ preparedPlan: payload.preparedPlan,
457
562
  chunkSize: payload.chunkSize,
458
563
  forcedUpdateRes: payload.forcedUpdateRes,
459
564
  bootloaderBinary: payload.bootloaderBinary,
@@ -468,17 +573,25 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
468
573
  resourceFiles: payload.resourceFiles,
469
574
  firmwareType: payload.firmwareType,
470
575
  targetsToUpdate: payload.targetsToUpdate,
576
+ expectedTargetVersions: payload.expectedTargetVersions,
471
577
  platform: payload.platform,
578
+ expectedDeviceId: payload.expectedDeviceId,
579
+ artifactReader: hostBinding?.artifactReader ?? payload.artifactReader,
580
+ componentArtifacts: payload.componentArtifacts,
581
+ resourceBundleArtifacts: payload.resourceBundleArtifacts,
472
582
  };
473
583
  }
474
584
 
475
- private getProtocolV2FirmwareChunkSize() {
585
+ private getProtocolV2FirmwareChunkSize(direction: 'read' | 'write' = 'write') {
476
586
  const payloadChunkSize = Number(this.params?.chunkSize);
477
587
  const env = DataManager.getSettings('env');
478
588
  const isBle = this.params?.platform === 'native' || (env && DataManager.isBleConnect(env));
479
589
  let maxChunkSize = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
480
590
  if (isBle) {
481
- maxChunkSize = PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
591
+ maxChunkSize =
592
+ direction === 'read'
593
+ ? PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE
594
+ : PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
482
595
  }
483
596
  if (!Number.isFinite(payloadChunkSize) || payloadChunkSize <= 0) {
484
597
  return maxChunkSize;
@@ -499,8 +612,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
499
612
  const deviceFeatures = await this.getProtocolV2DeviceFeatures();
500
613
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
501
614
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
615
+ this.validateExpectedTargetVersions();
616
+
617
+ if (
618
+ (this.params.componentArtifacts && Object.keys(this.params.componentArtifacts).length > 0) ||
619
+ this.params.resourceBundleArtifacts?.length
620
+ ) {
621
+ return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
622
+ }
502
623
  const needsRemoteResources =
503
624
  !this.params.resourceFiles?.length && !!this.params.targetsToUpdate?.includes('resource');
625
+ const needsRemoteBootResources = !!this.params.targetsToUpdate?.includes('boot_resources');
504
626
 
505
627
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
506
628
  let bootloaderBinary: ArrayBuffer | null = null;
@@ -512,7 +634,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
512
634
  fwBinaryMap = this.collectExplicitTargetBinaries();
513
635
  bootloaderBinary = this.prepareBootloaderBinary();
514
636
  const needsRemoteFirmware = !this.hasExplicitProtocolV2Payload(fwBinaryMap);
515
- const needsRemoteBootResources = !!this.params.targetsToUpdate?.includes('boot_resources');
637
+ if (
638
+ (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) &&
639
+ (this.params.artifactReader ||
640
+ DataManager.getSettings('firmwareManifestMode') === 'external-only')
641
+ ) {
642
+ throw ERRORS.TypedError(
643
+ HardwareErrorCode.RuntimeError,
644
+ 'Protocol V2 firmware artifacts must be prepared by the external firmware host',
645
+ {
646
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
647
+ }
648
+ );
649
+ }
516
650
  if (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) {
517
651
  // Remote updates must use a freshly fetched config before any reboot or file write.
518
652
  await DataManager.forceReloadData({
@@ -539,7 +673,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
539
673
  if (err instanceof HardwareError && err.errorCode === HardwareErrorCode.NetworkError) {
540
674
  throw err;
541
675
  }
542
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
676
+ throw normalizeFirmwarePreparationError(err);
543
677
  }
544
678
 
545
679
  if (
@@ -555,9 +689,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
555
689
  );
556
690
  }
557
691
 
558
- const enteredBootloader = await this.enterProtocolV2BootloaderMode();
559
-
560
692
  if (needsRemoteResources) {
693
+ const enteredBootloader = await this.enterProtocolV2BootloaderMode();
561
694
  try {
562
695
  const stableResources = await this.prepareProtocolV2ResourceBundles();
563
696
  resourceBundles = [...(resourceBundles ?? []), ...(stableResources ?? [])];
@@ -573,25 +706,293 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
573
706
  );
574
707
  }
575
708
  }
576
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
709
+ throw normalizeFirmwarePreparationError(err);
577
710
  }
578
711
  }
579
712
 
580
- await this.executeProtocolV2Update({
713
+ return this.executeProtocolV2Update({
581
714
  fwBinaryMap,
582
715
  bootloaderBinary,
583
716
  ...(installItems ? { installItems } : undefined),
584
717
  ...(resourceBundles?.length ? { resourceBundles } : undefined),
585
718
  });
719
+ }
586
720
 
587
- this.postTipMessage(FirmwareUpdateTipMessage.SwitchFirmwareReconnectDevice);
588
- await this.exitProtocolV2BootloaderToNormal();
721
+ private async openProtocolV2PreparedSource(artifact: FirmwareArtifactReference) {
722
+ const source = await openFirmwareByteSource({
723
+ artifact,
724
+ reader: this.params.artifactReader,
725
+ });
726
+ if (!source) {
727
+ throw ERRORS.TypedError(
728
+ HardwareErrorCode.RuntimeError,
729
+ `Firmware artifact ${artifact.artifactRef} is not prepared`,
730
+ {
731
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
732
+ }
733
+ );
734
+ }
735
+ this.protocolV2PreparedSources.push(source);
736
+ return source;
737
+ }
589
738
 
590
- const versions = await this.waitForProtocolV2FinalFeatures();
591
- this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
592
- DevicePool.resetState();
739
+ private async openProtocolV2MemorySource(binary: ArrayBuffer) {
740
+ const source = await openFirmwareByteSource({ binary });
741
+ if (!source) {
742
+ throw ERRORS.TypedError(
743
+ HardwareErrorCode.RuntimeError,
744
+ 'Protocol V2 firmware binary is empty'
745
+ );
746
+ }
747
+ this.protocolV2PreparedSources.push(source);
748
+ return source;
749
+ }
593
750
 
594
- return versions;
751
+ private async closeProtocolV2PreparedSources() {
752
+ const sources = this.protocolV2PreparedSources.splice(0);
753
+ await Promise.all(sources.map(source => source.close().catch(() => undefined)));
754
+ }
755
+
756
+ private async prepareProtocolV2InstallSources(
757
+ firmwareType: EFirmwareType,
758
+ features: Features
759
+ ): Promise<ProtocolV2InstallSource[]> {
760
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
761
+ if (!release) {
762
+ throw ERRORS.TypedError(
763
+ HardwareErrorCode.RuntimeError,
764
+ 'Protocol V2 firmware release is unavailable'
765
+ );
766
+ }
767
+ const componentArtifacts = this.params.componentArtifacts ?? {};
768
+ const requestedTargets = new Set(
769
+ this.params.targetsToUpdate ?? (Object.keys(componentArtifacts) as FirmwareUpdateV4Target[])
770
+ );
771
+ const installSources: ProtocolV2InstallSource[] = [];
772
+ const preparedTargets = new Set<FirmwareUpdateV4Target>();
773
+
774
+ for (const [key, component] of this.getRemoteComponentEntries(release)) {
775
+ const target = this.getRemoteComponentTarget(key, component);
776
+ const updateTarget = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId);
777
+ if (updateTarget && updateTarget !== 'resource' && requestedTargets.has(updateTarget)) {
778
+ const artifact = componentArtifacts[updateTarget];
779
+ if (!artifact) {
780
+ throw ERRORS.TypedError(
781
+ HardwareErrorCode.RuntimeError,
782
+ `Protocol V2 ${updateTarget} artifact is not prepared`,
783
+ {
784
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
785
+ artifactName: updateTarget,
786
+ }
787
+ );
788
+ }
789
+ installSources.push({
790
+ ...target,
791
+ source: await this.openProtocolV2PreparedSource(artifact),
792
+ });
793
+ preparedTargets.add(updateTarget);
794
+ }
795
+ }
796
+
797
+ const unknownTarget = Array.from(requestedTargets).find(
798
+ target => target !== 'resource' && !preparedTargets.has(target)
799
+ );
800
+ if (unknownTarget) {
801
+ throw ERRORS.TypedError(
802
+ HardwareErrorCode.RuntimeError,
803
+ `Protocol V2 release does not contain requested target ${unknownTarget}`
804
+ );
805
+ }
806
+ return installSources;
807
+ }
808
+
809
+ private async prepareProtocolV2ResourceSources(
810
+ firmwareType: EFirmwareType,
811
+ features: Features
812
+ ): Promise<ProtocolV2ResourceBundleSource[]> {
813
+ const preparedArtifacts = this.params.resourceBundleArtifacts ?? [];
814
+ const resourceRequested =
815
+ this.params.targetsToUpdate?.includes('resource') || preparedArtifacts.length > 0;
816
+ if (!resourceRequested) {
817
+ return [];
818
+ }
819
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
820
+ const descriptors = release?.resourceBundles ?? [];
821
+ const artifactByName = new Map(
822
+ preparedArtifacts.map(item => [item.name, item.artifact] as const)
823
+ );
824
+ const sources: ProtocolV2ResourceBundleSource[] = [];
825
+ for (const descriptor of descriptors) {
826
+ const artifact = artifactByName.get(descriptor.name);
827
+ if (!artifact) {
828
+ throw ERRORS.TypedError(
829
+ HardwareErrorCode.RuntimeError,
830
+ `Protocol V2 resource bundle ${descriptor.name} is not prepared`,
831
+ {
832
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
833
+ artifactName: descriptor.name,
834
+ }
835
+ );
836
+ }
837
+ const devicePath = validateProtocolV2FilesystemPath(
838
+ descriptor.devicePath,
839
+ 'resourceBundles[].devicePath'
840
+ );
841
+ sources.push({
842
+ name: descriptor.name,
843
+ source: await this.openProtocolV2PreparedSource(artifact),
844
+ devicePath,
845
+ version: descriptor.version,
846
+ payloadHash: descriptor.payloadHash,
847
+ headerHash: descriptor.headerHash,
848
+ });
849
+ artifactByName.delete(descriptor.name);
850
+ }
851
+ if (artifactByName.size > 0) {
852
+ throw ERRORS.TypedError(
853
+ HardwareErrorCode.RuntimeError,
854
+ `Protocol V2 release does not contain resource bundle ${artifactByName.keys().next().value}`
855
+ );
856
+ }
857
+ return sources;
858
+ }
859
+
860
+ private async runProtocolV2PreparedArtifacts(features: Features, firmwareType: EFirmwareType) {
861
+ try {
862
+ this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
863
+ const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
864
+ const resourceSources = await this.prepareProtocolV2ResourceSources(firmwareType, features);
865
+ if (installSources.length === 0 && resourceSources.length === 0) {
866
+ throw ERRORS.TypedError(
867
+ HardwareErrorCode.FirmwareUpdateDownloadFailed,
868
+ 'No firmware to update'
869
+ );
870
+ }
871
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
872
+
873
+ return await this.executeProtocolV2SourceUpdate({
874
+ installSources,
875
+ resourceSources,
876
+ });
877
+ } finally {
878
+ await this.closeProtocolV2PreparedSources();
879
+ }
880
+ }
881
+
882
+ private validateExpectedTargetVersions() {
883
+ const expected = this.params.expectedTargetVersions;
884
+ if (expected === undefined) return;
885
+ if (typeof expected !== 'object' || expected === null || Array.isArray(expected)) {
886
+ throw ERRORS.TypedError(
887
+ HardwareErrorCode.RuntimeError,
888
+ 'Protocol V2 expected target versions are invalid'
889
+ );
890
+ }
891
+ for (const [target, version] of Object.entries(expected)) {
892
+ if (
893
+ !Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.values()).includes(
894
+ target as FirmwareUpdateV4Target
895
+ ) ||
896
+ typeof version !== 'string' ||
897
+ !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(version) ||
898
+ version.length > 64
899
+ ) {
900
+ throw ERRORS.TypedError(
901
+ HardwareErrorCode.RuntimeError,
902
+ 'Protocol V2 expected target version is invalid'
903
+ );
904
+ }
905
+ }
906
+ }
907
+
908
+ private getExpectedProtocolV2TargetVersion(targetId: number) {
909
+ const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(targetId);
910
+ const version = target ? this.params?.expectedTargetVersions?.[target] : undefined;
911
+ if (!version) return undefined;
912
+ const parts = version.split(/[+-]/u, 1)[0].split('.').map(Number);
913
+ if (
914
+ parts.length !== 3 ||
915
+ parts.some(part => !Number.isSafeInteger(part) || part < 0 || part > 0xff)
916
+ ) {
917
+ return undefined;
918
+ }
919
+ return parts[0] * 0x10000 + parts[1] * 0x100 + parts[2];
920
+ }
921
+
922
+ private assertExpectedProtocolV2Versions(targets?: readonly FirmwareUpdateV4Target[]) {
923
+ const expected = this.params?.expectedTargetVersions;
924
+ if (!expected) return;
925
+ const features = this.protocolV2LatestFinalFeatures;
926
+ const requestedTargets = this.getProtocolV2RequestedTargets();
927
+ const applicationTargets = requestedTargets.filter(
928
+ target => target === 'app_v1' || target === 'app_v2'
929
+ );
930
+ const visibleVersions: Partial<Record<FirmwareUpdateV4Target, string>> = {
931
+ boot: features ? getDeviceBootloaderVersion(features).join('.') : undefined,
932
+ coprocessor: features ? getDeviceBLEFirmwareVersion(features).join('.') : undefined,
933
+ se01: features?.se01Version ?? undefined,
934
+ se02: features?.se02Version ?? undefined,
935
+ se03: features?.se03Version ?? undefined,
936
+ se04: features?.se04Version ?? undefined,
937
+ };
938
+ if (features && applicationTargets.length === 1) {
939
+ visibleVersions[applicationTargets[0]] = getDeviceFirmwareVersion(features).join('.');
940
+ }
941
+ const expectedEntries = (
942
+ Object.entries(expected) as Array<[FirmwareUpdateV4Target, string]>
943
+ ).filter(([target]) => !targets || targets.includes(target));
944
+ for (const [target, expectedVersion] of expectedEntries) {
945
+ const targetId = Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.entries()).find(
946
+ ([, mappedTarget]) => mappedTarget === target
947
+ )?.[0];
948
+ const statusVersion =
949
+ targetId === undefined ? undefined : this.protocolV2CompletedTargetVersions.get(targetId);
950
+ const observedVersion =
951
+ statusVersion === undefined
952
+ ? visibleVersions[target]
953
+ : `${Math.floor(statusVersion / 0x10000) % 0x100}.${
954
+ Math.floor(statusVersion / 0x100) % 0x100
955
+ }.${statusVersion % 0x100}`;
956
+ if (!observedVersion) {
957
+ if (this.protocolV2FinalStatusVerified) {
958
+ Log.warn(
959
+ `Protocol V2 target ${target} has no observable final version; completed target status is authoritative`
960
+ );
961
+ } else {
962
+ throw ERRORS.TypedError(
963
+ HardwareErrorCode.FirmwareVerificationFailed,
964
+ `Protocol V2 target ${target} has no observable final version after status fallback`
965
+ );
966
+ }
967
+ } else if (observedVersion !== expectedVersion) {
968
+ throw ERRORS.TypedError(
969
+ HardwareErrorCode.FirmwareVerificationFailed,
970
+ `Protocol V2 target ${target} reached ${observedVersion}, expected ${expectedVersion}`
971
+ );
972
+ }
973
+ }
974
+ }
975
+
976
+ private getProtocolV2RequestedTargets(): FirmwareUpdateV4Target[] {
977
+ if (this.params.targetsToUpdate?.length) {
978
+ return [...new Set(this.params.targetsToUpdate)];
979
+ }
980
+ const targets = new Set<FirmwareUpdateV4Target>();
981
+ Object.keys(this.params.componentArtifacts ?? {}).forEach(target =>
982
+ targets.add(target as FirmwareUpdateV4Target)
983
+ );
984
+ if (this.params.resourceBundleArtifacts?.length || this.params.resourceFiles?.length) {
985
+ targets.add('resource');
986
+ }
987
+ if (this.params.bootloaderBinary) targets.add('boot');
988
+ if (this.params.applicationP1Binary) targets.add('app_v1');
989
+ if (this.params.applicationP2Binary) targets.add('app_v2');
990
+ if (this.params.coprocessorBinary) targets.add('coprocessor');
991
+ if (this.params.se01Binary) targets.add('se01');
992
+ if (this.params.se02Binary) targets.add('se02');
993
+ if (this.params.se03Binary) targets.add('se03');
994
+ if (this.params.se04Binary) targets.add('se04');
995
+ return Array.from(targets);
595
996
  }
596
997
 
597
998
  private async getProtocolV2DeviceFeatures(): Promise<Features> {
@@ -635,8 +1036,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
635
1036
  private async captureProtocolV2PhysicalIdentity() {
636
1037
  const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
637
1038
  const serialNumber = this.getProtocolV2SerialNumber(deviceInfo);
638
- const path = this.device.originalDescriptor.path?.trim() || undefined;
639
- assertProtocolV2ReconnectIdentity(serialNumber, serialNumber, path, path);
1039
+ const path = this.device.originalDescriptor?.path?.trim() || undefined;
1040
+ if (this.params?.preparedPlan) {
1041
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
1042
+ preparedPlan: this.params.preparedPlan,
1043
+ deviceIdentity: serialNumber,
1044
+ });
1045
+ } else if (this.params?.expectedDeviceId) {
1046
+ assertProtocolV2ReconnectIdentity(this.params?.expectedDeviceId, serialNumber);
1047
+ } else {
1048
+ assertProtocolV2ReconnectIdentity(serialNumber, serialNumber, path, path);
1049
+ }
640
1050
  this.protocolV2ExpectedSerialNumber = serialNumber;
641
1051
  this.protocolV2ExpectedPath = path;
642
1052
  }
@@ -920,38 +1330,88 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
920
1330
  };
921
1331
  }
922
1332
 
923
- private async syncProtocolV2ResourceBundles(
924
- bundles: ProtocolV2ResourceBundleBinary[],
925
- firmwareSize: number
926
- ): Promise<{ processedSize: number; totalSize: number }> {
927
- const transferStartTime = Date.now();
928
- const transferTransport = this.getProtocolV2FirmwareTransferTransport();
1333
+ private getProtocolV2ResourceFilePath(path: string) {
1334
+ if (path.startsWith('vol')) return path;
1335
+ if (path.startsWith('/')) return `vol0:${path}`;
1336
+ return `vol0:/${path}`;
1337
+ }
929
1338
 
930
- // Write directly to the device through FileWrite.
931
- let totalSize = 0;
932
- for (const b of bundles) totalSize += b.binary.byteLength;
1339
+ private async readProtocolV2DeviceFileHeader(path: string) {
1340
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1341
+ const filePath = this.getProtocolV2ResourceFilePath(path);
1342
+ const pathInfoRes = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
1343
+ path: filePath,
1344
+ });
1345
+ const fileSize = toProtocolV2FiniteNumber(pathInfoRes.message?.size);
1346
+ if (
1347
+ !pathInfoRes.message?.exist ||
1348
+ pathInfoRes.message?.directory ||
1349
+ fileSize === undefined ||
1350
+ fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE
1351
+ ) {
1352
+ return null;
1353
+ }
933
1354
 
934
- const transferTotalSize = totalSize + firmwareSize;
935
- let processedSize = 0;
936
- for (const bundle of bundles) {
937
- Log.log(
938
- `[FirmwareUpdateV4] syncing resource ${bundle.name} -> ${bundle.devicePath} bytes=${bundle.binary.byteLength}`
939
- );
940
- processedSize = await this.protocolV2CommonUpdateProcess({
941
- payload: bundle.binary,
942
- filePath: bundle.devicePath,
943
- processedSize,
944
- totalSize: transferTotalSize,
1355
+ const chunkSize = this.getProtocolV2FirmwareChunkSize('read');
1356
+ const chunks: Uint8Array[] = [];
1357
+ let offset = 0;
1358
+ while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) {
1359
+ const readLen = Math.min(chunkSize, PROTOCOL_V2_OKPP_HEADER_SIZE - offset);
1360
+ const res = await typedCall('FilesystemFileRead', 'FilesystemFile', {
1361
+ file: {
1362
+ path: filePath,
1363
+ offset,
1364
+ total_size: 0,
1365
+ },
1366
+ chunk_len: readLen,
1367
+ ui_percentage: undefined,
945
1368
  });
1369
+ const data = toProtocolV2Bytes(res.message?.data);
1370
+ if (data.byteLength === 0) return null;
1371
+ chunks.push(data);
1372
+ offset += data.byteLength;
946
1373
  }
947
1374
 
948
- const elapsedMs = Date.now() - transferStartTime;
949
- Log.log(
950
- `[FirmwareUpdateV4] resource sync finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
951
- elapsedMs / 1000
952
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
953
- );
954
- return { processedSize, totalSize: transferTotalSize };
1375
+ const headerBytes = new Uint8Array(offset);
1376
+ let cursor = 0;
1377
+ chunks.forEach(chunk => {
1378
+ headerBytes.set(chunk, cursor);
1379
+ cursor += chunk.byteLength;
1380
+ });
1381
+ return parseProtocolV2OkppHeader(headerBytes);
1382
+ }
1383
+
1384
+ /** Compare a prepared okpkg header when its manifest supplies version or hash metadata. */
1385
+ private async isProtocolV2ResourceBundleUpToDate(
1386
+ bundle: Pick<
1387
+ ProtocolV2ResourceBundleSource,
1388
+ 'name' | 'devicePath' | 'version' | 'payloadHash' | 'headerHash'
1389
+ >
1390
+ ): Promise<boolean> {
1391
+ if (this.params?.forcedUpdateRes) return false;
1392
+ if (!bundle.version && !bundle.payloadHash) return false;
1393
+
1394
+ try {
1395
+ const header = await this.readProtocolV2DeviceFileHeader(bundle.devicePath);
1396
+ if (!header) return false;
1397
+
1398
+ if (bundle.version) {
1399
+ const cmp = compareProtocolV2Versions(header.version, bundle.version);
1400
+ if (cmp === undefined || cmp !== 0) return false;
1401
+ }
1402
+ if (bundle.payloadHash) {
1403
+ const expected = normalizeProtocolV2Hex(bundle.payloadHash);
1404
+ if (expected && header.payloadHash !== expected) return false;
1405
+ }
1406
+ if (bundle.headerHash) {
1407
+ const expected = normalizeProtocolV2Hex(bundle.headerHash);
1408
+ if (expected && header.headerHash !== expected) return false;
1409
+ }
1410
+ return true;
1411
+ } catch (error) {
1412
+ Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
1413
+ return false;
1414
+ }
955
1415
  }
956
1416
 
957
1417
  private isProtocolV2BootloaderMode() {
@@ -983,10 +1443,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
983
1443
  // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
984
1444
  if (this.isProtocolV2RomloaderMode()) {
985
1445
  Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
1446
+ this.protocolV2ExecutionInLoader = true;
986
1447
  return false;
987
1448
  }
988
1449
  if (this.isProtocolV2BootloaderMode()) {
989
1450
  Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
1451
+ this.protocolV2ExecutionInLoader = true;
990
1452
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
991
1453
  return false;
992
1454
  }
@@ -996,6 +1458,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
996
1458
  await this.protocolV2Reboot(DeviceRebootType.Bootloader);
997
1459
  await wait(1000);
998
1460
  await this.waitForProtocolV2BootloaderMode();
1461
+ this.protocolV2ExecutionInLoader = true;
999
1462
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1000
1463
  return true;
1001
1464
  } catch (error) {
@@ -1091,6 +1554,98 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1091
1554
  return entries;
1092
1555
  }
1093
1556
 
1557
+ private buildProtocolV2ExecutionPhases({
1558
+ installSources,
1559
+ resourceSources,
1560
+ }: {
1561
+ installSources: ProtocolV2InstallSource[];
1562
+ resourceSources: ProtocolV2ResourceBundleSource[];
1563
+ }): ProtocolV2ExecutionPhase[] {
1564
+ const phases: ProtocolV2ExecutionPhase[] = [];
1565
+ if (resourceSources.length > 0) {
1566
+ phases.push({
1567
+ kind: 'resource-sync',
1568
+ installSources: [],
1569
+ resourceSources,
1570
+ });
1571
+ }
1572
+ const bootloaderSources = installSources.filter(source => source.kind === 'bootloader');
1573
+ if (bootloaderSources.length > 0) {
1574
+ phases.push(
1575
+ {
1576
+ kind: 'bootloader-install',
1577
+ installSources: bootloaderSources,
1578
+ resourceSources: [],
1579
+ },
1580
+ {
1581
+ kind: 'bootloader-verify',
1582
+ installSources: [],
1583
+ resourceSources: [],
1584
+ }
1585
+ );
1586
+ }
1587
+ const componentSources = installSources.filter(source => source.kind !== 'bootloader');
1588
+ if (componentSources.length > 0) {
1589
+ phases.push({
1590
+ kind: 'component-install',
1591
+ installSources: componentSources,
1592
+ resourceSources: [],
1593
+ });
1594
+ }
1595
+ phases.push({
1596
+ kind: 'final-verify',
1597
+ installSources: [],
1598
+ resourceSources: [],
1599
+ });
1600
+ return phases;
1601
+ }
1602
+
1603
+ private async executeProtocolV2Phases({
1604
+ installSources,
1605
+ resourceSources,
1606
+ }: {
1607
+ installSources: ProtocolV2InstallSource[];
1608
+ resourceSources: ProtocolV2ResourceBundleSource[];
1609
+ }) {
1610
+ const phases = this.buildProtocolV2ExecutionPhases({
1611
+ installSources,
1612
+ resourceSources,
1613
+ });
1614
+ for (const phase of phases) {
1615
+ if (phase.kind === 'final-verify') {
1616
+ return this.completeProtocolV2FinalVerification();
1617
+ }
1618
+ if (phase.kind === 'resource-sync') {
1619
+ await this.enterProtocolV2BootloaderMode();
1620
+ await this.executeProtocolV2TransferPhase(phase);
1621
+ } else if (phase.kind === 'bootloader-install' || phase.kind === 'component-install') {
1622
+ await this.enterProtocolV2BootloaderMode();
1623
+ await this.executeProtocolV2TransferPhase(phase);
1624
+ await this.exitProtocolV2BootloaderToNormal();
1625
+ } else if (phase.kind === 'bootloader-verify') {
1626
+ await this.waitForProtocolV2FinalFeatures();
1627
+ this.assertExpectedProtocolV2Versions(['boot']);
1628
+ }
1629
+ }
1630
+ throw ERRORS.TypedError(
1631
+ HardwareErrorCode.RuntimeError,
1632
+ 'Protocol V2 execution has no final verification phase'
1633
+ );
1634
+ }
1635
+
1636
+ private async executeProtocolV2SourceUpdate({
1637
+ installSources,
1638
+ resourceSources,
1639
+ }: {
1640
+ installSources: ProtocolV2InstallSource[];
1641
+ resourceSources: ProtocolV2ResourceBundleSource[];
1642
+ }) {
1643
+ return this.executeProtocolV2Phases({
1644
+ installSources,
1645
+ resourceSources,
1646
+ });
1647
+ }
1648
+
1094
1649
  private async executeProtocolV2Update({
1095
1650
  fwBinaryMap,
1096
1651
  bootloaderBinary,
@@ -1102,104 +1657,168 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1102
1657
  installItems?: ProtocolV2InstallItem[];
1103
1658
  resourceBundles?: ProtocolV2ResourceBundleBinary[];
1104
1659
  }) {
1105
- const orderedInstallItems =
1660
+ const memoryInstallItems =
1106
1661
  installItems ??
1107
1662
  this.buildProtocolV2InstallItems({
1108
- bootloaderBinary: bootloaderBinary ?? null,
1109
1663
  fwBinaryMap: fwBinaryMap ?? [],
1664
+ bootloaderBinary: bootloaderBinary ?? null,
1110
1665
  });
1111
-
1112
- let firmwareSize = 0;
1113
- for (const item of orderedInstallItems) firmwareSize += item.binary.byteLength;
1114
-
1115
- this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
1116
-
1117
- let processedSize = 0;
1118
- let totalSize = firmwareSize;
1119
- if (resourceBundles?.length) {
1120
- const resourceTransfer = await this.syncProtocolV2ResourceBundles(
1121
- resourceBundles,
1122
- firmwareSize
1666
+ try {
1667
+ const installSources = await Promise.all(
1668
+ memoryInstallItems.map(async item => ({
1669
+ fileName: item.fileName,
1670
+ source: await this.openProtocolV2MemorySource(item.binary),
1671
+ targetId: item.targetId,
1672
+ kind: item.kind,
1673
+ }))
1674
+ );
1675
+ const resourceSources = await Promise.all(
1676
+ (resourceBundles ?? []).map(async bundle => ({
1677
+ name: bundle.name,
1678
+ source: await this.openProtocolV2MemorySource(bundle.binary),
1679
+ devicePath: bundle.devicePath,
1680
+ }))
1123
1681
  );
1124
- processedSize = resourceTransfer.processedSize;
1125
- totalSize = resourceTransfer.totalSize;
1682
+ return await this.executeProtocolV2Phases({
1683
+ installSources,
1684
+ resourceSources,
1685
+ });
1686
+ } finally {
1687
+ await this.closeProtocolV2PreparedSources();
1126
1688
  }
1689
+ }
1127
1690
 
1128
- // Skip staging and installation when the update contains resource files only.
1129
- if (orderedInstallItems.length === 0) {
1130
- Log.log('[FirmwareUpdateV4] no firmware targets to install (resource files only)');
1131
- if (totalSize > 0) {
1132
- this.postProgressMessage(100, 'transferData');
1691
+ private async executeProtocolV2TransferPhase({
1692
+ installSources,
1693
+ resourceSources,
1694
+ }: ProtocolV2ExecutionPhase) {
1695
+ let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
1696
+ const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
1697
+ for (const resource of resourceSources) {
1698
+ if (await this.isProtocolV2ResourceBundleUpToDate(resource)) {
1699
+ Log.log(`[FirmwareUpdateV4] skip RESC bundle ${resource.name}; already up to date`);
1700
+ } else {
1701
+ resourcesToSync.push(resource);
1702
+ totalSize += resource.source.size;
1133
1703
  }
1134
- return;
1135
1704
  }
1136
- let transferredSize = processedSize;
1137
- const transferStartTime = Date.now();
1138
- const transferTransport = this.getProtocolV2FirmwareTransferTransport();
1139
- const chunkSize = this.getProtocolV2FirmwareChunkSize();
1140
- const onTransferredBytes = (bytes: number) => {
1141
- transferredSize = bytes;
1142
- };
1143
- Log.log(
1144
- `[FirmwareUpdateV4] transfer started transport=${transferTransport} total=${totalSize} bytes chunk=${chunkSize} bytes`
1145
- );
1146
-
1147
- const stagedInstallTargets: ProtocolV2InstallTarget[] = [];
1148
1705
 
1149
- try {
1150
- for (const item of orderedInstallItems) {
1151
- const filePath = this.getProtocolV2InstallItemStagingPath(item);
1152
- Log.log(
1153
- `[FirmwareUpdateV4] staging ${item.kind} via FilesystemFileWrite target=${item.targetId} path=${filePath} source=${item.fileName} bytes=${item.binary.byteLength}`
1154
- );
1155
- processedSize = await this.protocolV2CommonUpdateProcess({
1156
- payload: item.binary,
1157
- filePath,
1158
- processedSize,
1159
- totalSize,
1160
- onTransferredBytes,
1161
- });
1162
- transferredSize = processedSize;
1163
- await this.verifyProtocolV2StagedFile(filePath, item.binary.byteLength);
1164
-
1165
- stagedInstallTargets.push({
1166
- ...item,
1167
- path: filePath,
1168
- });
1169
- }
1706
+ this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
1707
+ let processedSize = 0;
1708
+ for (const resource of resourcesToSync) {
1709
+ processedSize = await this.protocolV2SourceUpdateProcess({
1710
+ source: resource.source,
1711
+ filePath: resource.devicePath,
1712
+ processedSize,
1713
+ totalSize,
1714
+ });
1715
+ await this.verifyProtocolV2StagedFile(resource.devicePath, resource.source.size);
1716
+ }
1170
1717
 
1171
- if (totalSize > 0) {
1172
- this.postProgressMessage(100, 'transferData');
1173
- }
1718
+ const stagedInstallTargets: Array<{ targetId: number; path: string }> = [];
1719
+ for (const item of installSources) {
1720
+ const filePath = this.getProtocolV2InstallItemStagingPath(item);
1721
+ processedSize = await this.protocolV2SourceUpdateProcess({
1722
+ source: item.source,
1723
+ filePath,
1724
+ processedSize,
1725
+ totalSize,
1726
+ });
1727
+ await this.verifyProtocolV2StagedFile(filePath, item.source.size);
1728
+ stagedInstallTargets.push({
1729
+ targetId: item.targetId,
1730
+ path: filePath,
1731
+ });
1732
+ }
1174
1733
 
1175
- const elapsedMs = Date.now() - transferStartTime;
1176
- Log.log(
1177
- `[FirmwareUpdateV4] transfer finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
1178
- elapsedMs / 1000
1179
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
1180
- );
1181
- } catch (error) {
1182
- const elapsedMs = Date.now() - transferStartTime;
1183
- Log.warn(
1184
- `[FirmwareUpdateV4] transfer failed transport=${transferTransport} bytes=${transferredSize}/${totalSize} elapsed=${(
1185
- elapsedMs / 1000
1186
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(transferredSize, elapsedMs)} KB/s`
1187
- );
1188
- throw error;
1734
+ if (totalSize > 0) {
1735
+ this.postProgressMessage(100, 'transferData');
1736
+ }
1737
+ if (stagedInstallTargets.length === 0) {
1738
+ return;
1189
1739
  }
1190
1740
 
1191
1741
  this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
1192
-
1193
- const allTargets = stagedInstallTargets.map(item => ({
1742
+ const targets = stagedInstallTargets.map(item => ({
1194
1743
  target_id: item.targetId,
1195
1744
  path: item.path,
1196
1745
  }));
1197
- Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(allTargets)}`);
1198
- await this.protocolV2StartFirmwareUpdate({ targets: allTargets });
1199
- await this.waitForProtocolV2FirmwareUpdateComplete(allTargets);
1746
+ await this.protocolV2StartFirmwareUpdate({ targets });
1747
+ await this.waitForProtocolV2FirmwareUpdateComplete(targets);
1748
+ }
1749
+
1750
+ private async protocolV2SourceUpdateProcess({
1751
+ source,
1752
+ filePath,
1753
+ processedSize,
1754
+ totalSize,
1755
+ }: {
1756
+ source: FirmwareByteSource;
1757
+ filePath: string;
1758
+ processedSize: number;
1759
+ totalSize: number;
1760
+ }) {
1761
+ let lastError: unknown;
1762
+ for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1763
+ try {
1764
+ const transferStartedAt = Date.now();
1765
+ await writeFirmwareByteSource({
1766
+ source,
1767
+ chunkSize: this.getProtocolV2FirmwareChunkSize(),
1768
+ write: async ({ data, sourceOffset, length, first }) => {
1769
+ const chunkEnd = sourceOffset + length;
1770
+ const deviceProgress = getProtocolV2DeviceTransferProgress(
1771
+ processedSize + sourceOffset,
1772
+ processedSize + chunkEnd,
1773
+ totalSize
1774
+ );
1775
+ const response = await this.fileWriteChunk(
1776
+ filePath,
1777
+ source.size,
1778
+ sourceOffset,
1779
+ data,
1780
+ first,
1781
+ deviceProgress
1782
+ );
1783
+ const rawProcessedByte = response.message.processed_byte;
1784
+ const nextOffset = rawProcessedByte === undefined ? chunkEnd : Number(rawProcessedByte);
1785
+ if (!Number.isFinite(nextOffset) || nextOffset !== chunkEnd) {
1786
+ throw ERRORS.TypedError(
1787
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
1788
+ `invalid processed_byte ${rawProcessedByte} for offset ${sourceOffset}`
1789
+ );
1790
+ }
1791
+ const transferredBytes = processedSize + chunkEnd;
1792
+ const elapsedMs = Math.max(Date.now() - transferStartedAt, 0);
1793
+ this.postProgressMessage(
1794
+ Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99),
1795
+ 'transferData',
1796
+ {
1797
+ transferredBytes,
1798
+ totalBytes: totalSize,
1799
+ rateBytesPerSecond:
1800
+ elapsedMs > 0 ? Math.round((chunkEnd / elapsedMs) * 1000) : undefined,
1801
+ elapsedMs,
1802
+ }
1803
+ );
1804
+ return length;
1805
+ },
1806
+ });
1807
+ return processedSize + source.size;
1808
+ } catch (error) {
1809
+ lastError = error;
1810
+ if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1811
+ await this.recoverProtocolV2FileTransfer();
1812
+ }
1813
+ }
1814
+ }
1815
+ throw ERRORS.TypedError(
1816
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
1817
+ `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1818
+ );
1200
1819
  }
1201
1820
 
1202
- private getProtocolV2InstallItemStagingPath(item: ProtocolV2InstallItem) {
1821
+ private getProtocolV2InstallItemStagingPath(item: Pick<ProtocolV2InstallItem, 'fileName'>) {
1203
1822
  return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`;
1204
1823
  }
1205
1824
 
@@ -1218,7 +1837,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1218
1837
 
1219
1838
  private assertProtocolV2TargetStatus(
1220
1839
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
1221
- expectedTargetIds: Set<number>
1840
+ expectedTargetIds: Set<number>,
1841
+ expectedPaths = new Map<number, string>()
1222
1842
  ) {
1223
1843
  Log.log(
1224
1844
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
@@ -1249,17 +1869,57 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1249
1869
  const matchingTargets = statusTargets.filter(target =>
1250
1870
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
1251
1871
  );
1872
+ const seenTargetIds = new Set<number>();
1873
+ for (const target of matchingTargets) {
1874
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
1875
+ if (
1876
+ targetId === undefined ||
1877
+ seenTargetIds.has(targetId) ||
1878
+ (target.path && expectedPaths.get(targetId) && target.path !== expectedPaths.get(targetId))
1879
+ ) {
1880
+ throw ERRORS.TypedError(
1881
+ HardwareErrorCode.RuntimeError,
1882
+ `Protocol V2 install status conflicts with target ${target.target_id}`,
1883
+ {
1884
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE,
1885
+ }
1886
+ );
1887
+ }
1888
+ seenTargetIds.add(targetId);
1889
+ }
1890
+ const completedTargets = matchingTargets.filter(target =>
1891
+ isProtocolV2TargetStatusFinished(target.status)
1892
+ );
1252
1893
  const completedTargetIds = new Set<number>();
1253
- matchingTargets.forEach(target => {
1894
+ completedTargets.forEach(target => {
1254
1895
  const targetId = normalizeProtocolV2TargetId(target.target_id);
1255
- if (targetId !== undefined && isProtocolV2TargetStatusFinished(target.status)) {
1896
+ if (targetId !== undefined) {
1256
1897
  completedTargetIds.add(targetId);
1257
1898
  }
1258
1899
  });
1259
- const allExpectedTargetsCompleted =
1260
- expectedTargetIds.size > 0 &&
1261
- Array.from(expectedTargetIds).every(targetId => completedTargetIds.has(targetId));
1262
- if (allExpectedTargetsCompleted) {
1900
+ if (completedTargetIds.size === expectedTargetIds.size && expectedTargetIds.size > 0) {
1901
+ for (const target of completedTargets) {
1902
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
1903
+ const payloadVersion = toProtocolV2FiniteNumber(target.payload_version);
1904
+ if (targetId === undefined) {
1905
+ throw ERRORS.TypedError(
1906
+ HardwareErrorCode.RuntimeError,
1907
+ 'Protocol V2 completed target identity is invalid'
1908
+ );
1909
+ }
1910
+ const expectedVersion = this.getExpectedProtocolV2TargetVersion(targetId);
1911
+ if (expectedVersion !== undefined && payloadVersion !== undefined) {
1912
+ if (payloadVersion !== expectedVersion) {
1913
+ throw ERRORS.TypedError(
1914
+ HardwareErrorCode.FirmwareVerificationFailed,
1915
+ `Protocol V2 target ${targetId} reached payload version ${payloadVersion}, expected ${expectedVersion}`
1916
+ );
1917
+ }
1918
+ }
1919
+ if (payloadVersion !== undefined) {
1920
+ this.protocolV2CompletedTargetVersions.set(targetId, payloadVersion);
1921
+ }
1922
+ }
1263
1923
  this.postProgressMessage(100, 'installingFirmware');
1264
1924
  return true;
1265
1925
  }
@@ -1297,7 +1957,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1297
1957
  private async waitForProtocolV2FirmwareUpdateComplete(
1298
1958
  targets: Array<{ target_id: number; path: string }>
1299
1959
  ) {
1960
+ this.protocolV2FinalStatusVerified = false;
1300
1961
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
1962
+ const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
1301
1963
  const startTime = Date.now();
1302
1964
  let lastError: unknown;
1303
1965
  let shouldReconnect = true;
@@ -1332,7 +1994,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1332
1994
  );
1333
1995
  const statusTargets = (statusResponse.message.records ??
1334
1996
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
1335
- if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) {
1997
+ if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
1998
+ this.protocolV2FinalStatusVerified = true;
1336
1999
  return;
1337
2000
  }
1338
2001
 
@@ -1380,10 +2043,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1380
2043
  lastError = new Error('Protocol V2 firmware targets are still installing');
1381
2044
  }
1382
2045
  } catch (error) {
1383
- if (
1384
- error instanceof HardwareError &&
1385
- error.errorCode === HardwareErrorCode.FirmwareError
1386
- ) {
2046
+ if (isProtocolV2TerminalInstallStatusError(error)) {
1387
2047
  throw error;
1388
2048
  }
1389
2049
  // Only consecutive full status dumps can prove that a record is absent.
@@ -1423,7 +2083,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1423
2083
  if (this.isProtocolV2ReconnectIdentityError(error)) {
1424
2084
  throw error;
1425
2085
  }
1426
- if (error instanceof HardwareError && error.errorCode === HardwareErrorCode.FirmwareError) {
2086
+ if (isProtocolV2TerminalInstallStatusError(error)) {
2087
+ throw error;
2088
+ }
2089
+ if (
2090
+ error instanceof HardwareError &&
2091
+ error.params?.firmwareUpdateCode === 'FirmwareInstallStatusUnavailable'
2092
+ ) {
1427
2093
  throw error;
1428
2094
  }
1429
2095
  shouldReconnect = true;
@@ -1454,6 +2120,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1454
2120
  Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
1455
2121
  }
1456
2122
  await this.protocolV2Reboot(DeviceRebootType.Normal);
2123
+ this.protocolV2ExecutionInLoader = false;
1457
2124
  }
1458
2125
 
1459
2126
  private async probeProtocolV2NormalMode(deviceInfo: ProtocolV2DeviceInfo) {
@@ -1476,6 +2143,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1476
2143
  const bootloaderVersion = getDeviceBootloaderVersion(features).join('.');
1477
2144
  const bleVersion = getDeviceBLEFirmwareVersion(features).join('.');
1478
2145
  const firmwareVersion = getDeviceFirmwareVersion(features).join('.');
2146
+ this.protocolV2LatestFinalFeatures = features;
1479
2147
  if (firmwareVersion === '0.0.0') {
1480
2148
  Log.warn(
1481
2149
  'Protocol V2 firmware update finished but app firmware version is still 0.0.0. This is allowed for Pro2 debug BLE-only update flows.'
@@ -1489,6 +2157,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1489
2157
  };
1490
2158
  }
1491
2159
 
2160
+ private async completeProtocolV2FinalVerification() {
2161
+ if (this.protocolV2ExecutionInLoader || this.isProtocolV2BootloaderMode()) {
2162
+ await this.exitProtocolV2BootloaderToNormal();
2163
+ }
2164
+ const versions = await this.waitForProtocolV2FinalFeatures();
2165
+ this.assertExpectedProtocolV2Versions();
2166
+ this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
2167
+ DevicePool.resetState();
2168
+ return versions;
2169
+ }
2170
+
1492
2171
  private async waitForProtocolV2ReconnectAndFeatures(timeout: number) {
1493
2172
  const startTime = Date.now();
1494
2173
  let lastError: unknown;
@@ -1613,99 +2292,40 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1613
2292
  return this.normalizeErrorMessage(error).includes('Protocol V2 reconnect physical identity');
1614
2293
  }
1615
2294
 
1616
- private async protocolV2CommonUpdateProcess(params: ProtocolV2FileTransferParams) {
1617
- let lastError: unknown;
1618
- for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1619
- try {
1620
- return await this.protocolV2WriteWholeFile(params);
1621
- } catch (error) {
1622
- lastError = error;
1623
- Log.error(
1624
- `Protocol V2 file transfer failed path=${params.filePath} attempt=${attempt}/${PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT}; restarting from offset 0`,
1625
- error
1626
- );
1627
- if (attempt === PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1628
- break;
1629
- }
1630
- await this.recoverProtocolV2FileTransfer();
1631
- }
1632
- }
1633
-
1634
- throw ERRORS.TypedError(
1635
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1636
- `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1637
- );
1638
- }
1639
-
1640
- private async protocolV2WriteWholeFile({
1641
- payload,
1642
- filePath,
1643
- processedSize,
1644
- totalSize,
1645
- onTransferredBytes,
1646
- }: ProtocolV2FileTransferParams) {
1647
- const chunkSize = this.getProtocolV2FirmwareChunkSize();
1648
- const getUploadProgress = (fileOffset: number) => {
1649
- if (totalSize !== undefined && processedSize !== undefined) {
1650
- return Math.min(Math.ceil(((processedSize + fileOffset) / totalSize) * 100), 99);
1651
- }
1652
- return Math.min(Math.ceil((fileOffset / payload.byteLength) * 100), 99);
1653
- };
1654
-
1655
- try {
1656
- await writeProtocolV2File({
1657
- commands: this.device.getCommands(),
1658
- path: filePath,
1659
- data: payload,
1660
- totalSize: payload.byteLength,
1661
- chunkSize,
1662
- overwrite: true,
1663
- append: false,
1664
- writeWithResponse: true,
1665
- maxChunkRetries: 0,
1666
- getUiPercentage: ({ offset, chunkLength }) =>
1667
- getProtocolV2DeviceTransferProgress(
1668
- (processedSize ?? 0) + offset,
1669
- (processedSize ?? 0) + offset + chunkLength,
1670
- totalSize ?? payload.byteLength
1671
- ),
1672
- onProgress: progress => {
1673
- const transferredBytes = (processedSize ?? 0) + progress.transferredBytes;
1674
- onTransferredBytes?.(transferredBytes);
1675
- this.postProgressMessage(getUploadProgress(progress.transferredBytes), 'transferData', {
1676
- transferredBytes,
1677
- totalBytes: totalSize ?? payload.byteLength,
1678
- rateBytesPerSecond: progress.rateBytesPerSecond,
1679
- elapsedMs: progress.elapsedMs,
1680
- });
2295
+ private async fileWriteChunk(
2296
+ filePath: string,
2297
+ totalFileSize: number,
2298
+ offset: number,
2299
+ chunk: ArrayBuffer | Buffer,
2300
+ overwrite: boolean,
2301
+ progress: number | null
2302
+ ): Promise<TypedResponseMessage<'FilesystemFile'>> {
2303
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
2304
+ const writeRes = await typedCall(
2305
+ 'FilesystemFileWrite',
2306
+ 'FilesystemFile',
2307
+ {
2308
+ file: {
2309
+ path: filePath,
2310
+ offset,
2311
+ total_size: totalFileSize,
2312
+ data: chunk,
1681
2313
  },
1682
- });
1683
- } catch (error) {
1684
- if (
1685
- error instanceof HardwareError &&
1686
- error.errorCode === HardwareErrorCode.RuntimeError &&
1687
- error.message.includes('FilesystemFileWrite')
1688
- ) {
1689
- throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, error.message);
2314
+ overwrite,
2315
+ append: false,
2316
+ ui_percentage: progress ?? undefined,
2317
+ },
2318
+ { writeWithResponse: true }
2319
+ );
2320
+ if (writeRes.type !== 'FilesystemFile') {
2321
+ if ((writeRes as any).type === 'CallMethodError') {
2322
+ if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) {
2323
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
2324
+ }
1690
2325
  }
1691
- throw error;
1692
- }
1693
-
1694
- return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0;
1695
- }
1696
-
1697
- private getProtocolV2FirmwareTransferTransport() {
1698
- const env = DataManager.getSettings('env');
1699
- if (env && DataManager.isBleConnect(env)) {
1700
- return 'BLE';
1701
- }
1702
- if (
1703
- env &&
1704
- (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'web')
1705
- ) {
1706
- return 'WebUSB';
2326
+ throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
1707
2327
  }
1708
- return env ?? 'unknown';
2328
+ return writeRes;
1709
2329
  }
1710
2330
 
1711
2331
  private async recoverProtocolV2FileTransfer() {