@onekeyfe/hd-core 1.2.0-alpha.69 → 1.2.0-alpha.70

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 (121) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +0 -28
  2. package/__tests__/connectSettings.test.ts +45 -5
  3. package/__tests__/device-initialize-timeout.test.ts +56 -0
  4. package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
  5. package/__tests__/firmware-update/device-update-bootloader.test.ts +87 -0
  6. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  7. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  8. package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
  9. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
  10. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  11. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +394 -0
  12. package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
  13. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +231 -0
  14. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +59 -31
  15. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +63 -0
  16. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  17. package/__tests__/protocol-v2-firmware-targets.test.ts +1 -33
  18. package/__tests__/protocol-v2-resources.test.ts +6 -9
  19. package/__tests__/protocol-v2.test.ts +383 -174
  20. package/__tests__/protocolV2FileWrite.test.ts +7 -48
  21. package/dist/api/CheckAllFirmwareRelease.d.ts +1 -2
  22. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV2.d.ts +10 -0
  25. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  26. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  27. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV4.d.ts +25 -6
  29. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  30. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  31. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  32. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  33. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  34. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  35. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  36. package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
  37. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
  38. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +8 -0
  39. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  40. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  41. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  42. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
  43. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  44. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
  45. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  46. package/dist/api/firmware/getBinary.d.ts +7 -0
  47. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  48. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  49. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  50. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  51. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  52. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  53. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  54. package/dist/data-manager/DataManager.d.ts +1 -0
  55. package/dist/data-manager/DataManager.d.ts.map +1 -1
  56. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  57. package/dist/device/Device.d.ts +1 -0
  58. package/dist/device/Device.d.ts.map +1 -1
  59. package/dist/index.d.ts +180 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +3038 -1041
  62. package/dist/inject.d.ts.map +1 -1
  63. package/dist/lowLevelInject.d.ts.map +1 -1
  64. package/dist/protocols/protocol-v2/resources.d.ts +1 -1
  65. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  66. package/dist/topLevelInject.d.ts.map +1 -1
  67. package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -0
  68. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  69. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  70. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  71. package/dist/types/api/export.d.ts +4 -1
  72. package/dist/types/api/export.d.ts.map +1 -1
  73. package/dist/types/api/firmwareUpdate.d.ts +68 -0
  74. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  75. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  76. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  77. package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
  78. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  79. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  80. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  81. package/dist/types/api/index.d.ts +9 -1
  82. package/dist/types/api/index.d.ts.map +1 -1
  83. package/dist/types/settings.d.ts +14 -1
  84. package/dist/types/settings.d.ts.map +1 -1
  85. package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
  86. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  87. package/package.json +4 -4
  88. package/src/api/CheckAllFirmwareRelease.ts +61 -25
  89. package/src/api/FirmwareUpdate.ts +13 -1
  90. package/src/api/FirmwareUpdateV2.ts +296 -121
  91. package/src/api/FirmwareUpdateV3.ts +263 -57
  92. package/src/api/FirmwareUpdateV4.ts +865 -287
  93. package/src/api/device/DeviceUpdateBootloader.ts +125 -6
  94. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  95. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  96. package/src/api/firmware/FirmwarePreparationError.ts +12 -0
  97. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +72 -1
  98. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  99. package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
  100. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +449 -0
  101. package/src/api/firmware/updateBootloader.ts +19 -4
  102. package/src/api/firmware/uploadFirmware.ts +140 -22
  103. package/src/api/helpers/protocolV2FileWrite.ts +4 -54
  104. package/src/data-manager/DataManager.ts +62 -54
  105. package/src/data-manager/connectSettings.ts +11 -0
  106. package/src/device/Device.ts +6 -1
  107. package/src/index.ts +5 -0
  108. package/src/inject.ts +22 -2
  109. package/src/lowLevelInject.ts +5 -1
  110. package/src/protocols/protocol-v2/resources.ts +1 -5
  111. package/src/topLevelInject.ts +5 -1
  112. package/src/types/api/checkAllFirmwareRelease.ts +4 -0
  113. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  114. package/src/types/api/export.ts +18 -0
  115. package/src/types/api/firmwareUpdate.ts +75 -0
  116. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  117. package/src/types/api/firmwareUpdatePlan.ts +38 -0
  118. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  119. package/src/types/api/index.ts +14 -0
  120. package/src/types/settings.ts +14 -2
  121. 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,18 +9,17 @@ 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,
15
15
  getDeviceBLEFirmwareVersion,
16
16
  getDeviceBootloaderVersion,
17
17
  getDeviceFirmwareVersion,
18
- getDeviceType,
19
18
  getFirmwareType,
20
19
  getLogger,
21
20
  } from '../utils';
22
21
  import { getSysResourceBinary } from './firmware/getBinary';
22
+ import { normalizeFirmwarePreparationError } from './firmware/FirmwarePreparationError';
23
23
  import { DataManager } from '../data-manager';
24
24
  import { FirmwareUpdateBaseMethod } from './firmware/FirmwareUpdateBaseMethod';
25
25
  import { DevicePool } from '../device/DevicePool';
@@ -38,11 +38,20 @@ import {
38
38
  getProtocolV2UnknownErrorText,
39
39
  isProtocolV2DeviceDisconnectedError,
40
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';
41
47
 
42
- import type { FirmwareUpdateV4Params, FirmwareUpdateV4Target } from '../types/api/firmwareUpdate';
48
+ import type {
49
+ FirmwareArtifactReference,
50
+ FirmwareUpdateV4Params,
51
+ FirmwareUpdateV4Target,
52
+ } from '../types/api/firmwareUpdate';
43
53
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
44
54
  import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
45
- import type { PROTO } from '../constants';
46
55
  import type { TypedResponseMessage } from '../device/DeviceCommands';
47
56
  import type {
48
57
  Features,
@@ -52,6 +61,7 @@ import type {
52
61
  IProtocolV2ResourceFile,
53
62
  IVersionArray,
54
63
  } from '../types';
64
+ import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
55
65
 
56
66
  const Log = getLogger(LoggerNames.Method);
57
67
 
@@ -74,35 +84,12 @@ const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
74
84
  const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
75
85
  const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
76
86
  const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
87
+ const PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE = 'FirmwareInstallStatusConflict';
77
88
  const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
78
89
  const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
79
90
  const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
80
91
  const PROTOCOL_V2_OKPP_HASH_SIZE = 64;
81
92
 
82
- const PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS = new Set<FirmwareUpdateV4Target>(['se03', 'se04']);
83
-
84
- export function assertProtocolV2FirmwareTargetsSupported(
85
- deviceType: EDeviceType | string | undefined,
86
- params: FirmwareUpdateV4Params
87
- ) {
88
- if (deviceType !== EDeviceType.Neo) return;
89
-
90
- const unsupportedTargets = new Set(
91
- (params.targetsToUpdate ?? []).filter(target => PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS.has(target))
92
- );
93
- if (params.se03Binary) unsupportedTargets.add('se03');
94
- if (params.se04Binary) unsupportedTargets.add('se04');
95
-
96
- if (unsupportedTargets.size) {
97
- throw ERRORS.TypedError(
98
- HardwareErrorCode.RuntimeError,
99
- `Neo only supports SE01 and SE02; unsupported firmware targets: ${Array.from(
100
- unsupportedTargets
101
- ).join(', ')}`
102
- );
103
- }
104
- }
105
-
106
93
  const getProtocolV2DeviceTransferProgress = (
107
94
  bytesBeforeChunk: number,
108
95
  bytesAfterChunk: number,
@@ -120,11 +107,6 @@ const getProtocolV2DeviceTransferProgress = (
120
107
  return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
121
108
  };
122
109
 
123
- const formatProtocolV2TransferSpeed = (bytes: number, elapsedMs: number) => {
124
- const safeElapsedMs = Math.max(elapsedMs, 1);
125
- return (bytes / 1024 / (safeElapsedMs / 1000)).toFixed(2);
126
- };
127
-
128
110
  type ProtocolV2FirmwareUpdateStatusTarget = {
129
111
  target_id: number | string;
130
112
  status?: number | string;
@@ -138,17 +120,6 @@ type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId:
138
120
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
139
121
  kind: ProtocolV2RemoteComponentTarget['kind'];
140
122
  };
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
123
  /** Protocol V2 resource file written independently to devicePath through FileWrite. */
153
124
  type ProtocolV2ResourceBundleBinary = {
154
125
  name: string;
@@ -166,6 +137,35 @@ type ProtocolV2RemoteComponentTarget = {
166
137
  kind: 'bootloader' | 'firmware';
167
138
  };
168
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
+
169
169
  type ProtocolV2OkppHeader = {
170
170
  type: string;
171
171
  version: IVersionArray;
@@ -268,6 +268,12 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
268
268
  );
269
269
  };
270
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
+
271
277
  const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) =>
272
278
  normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED;
273
279
 
@@ -305,6 +311,18 @@ const normalizeProtocolV2TargetStatus = (
305
311
 
306
312
  const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
307
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
+
308
326
  const bytesToHex = (bytes: Uint8Array) =>
309
327
  Array.from(bytes)
310
328
  .map(byte => byte.toString(16).padStart(2, '0'))
@@ -438,6 +456,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
438
456
  return ['V2'] as const;
439
457
  }
440
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
+
441
469
  init() {
442
470
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
443
471
  this.requireDeviceMode = [];
@@ -475,10 +503,62 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
475
503
  { name: 'firmwareType', type: 'string' },
476
504
  { name: 'targetsToUpdate', type: 'array', allowEmpty: true },
477
505
  { name: 'platform', type: 'string' },
506
+ { name: 'expectedDeviceId', type: 'string' },
478
507
  { name: 'resourceFiles', type: 'array', allowEmpty: true },
479
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
+ }
480
559
 
481
560
  this.params = {
561
+ preparedPlan: payload.preparedPlan,
482
562
  chunkSize: payload.chunkSize,
483
563
  forcedUpdateRes: payload.forcedUpdateRes,
484
564
  bootloaderBinary: payload.bootloaderBinary,
@@ -493,17 +573,25 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
493
573
  resourceFiles: payload.resourceFiles,
494
574
  firmwareType: payload.firmwareType,
495
575
  targetsToUpdate: payload.targetsToUpdate,
576
+ expectedTargetVersions: payload.expectedTargetVersions,
496
577
  platform: payload.platform,
578
+ expectedDeviceId: payload.expectedDeviceId,
579
+ artifactReader: hostBinding?.artifactReader ?? payload.artifactReader,
580
+ componentArtifacts: payload.componentArtifacts,
581
+ resourceBundleArtifacts: payload.resourceBundleArtifacts,
497
582
  };
498
583
  }
499
584
 
500
- private getProtocolV2FirmwareChunkSize() {
585
+ private getProtocolV2FirmwareChunkSize(direction: 'read' | 'write' = 'write') {
501
586
  const payloadChunkSize = Number(this.params?.chunkSize);
502
587
  const env = DataManager.getSettings('env');
503
588
  const isBle = this.params?.platform === 'native' || (env && DataManager.isBleConnect(env));
504
589
  let maxChunkSize = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
505
590
  if (isBle) {
506
- 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;
507
595
  }
508
596
  if (!Number.isFinite(payloadChunkSize) || payloadChunkSize <= 0) {
509
597
  return maxChunkSize;
@@ -522,16 +610,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
522
610
  private async runProtocolV2() {
523
611
  await this.captureProtocolV2PhysicalIdentity();
524
612
  const deviceFeatures = await this.getProtocolV2DeviceFeatures();
525
- const currentDeviceType = this.device.getCurrentDeviceType();
526
- const capabilityDeviceType =
527
- currentDeviceType === EDeviceType.Pro2 || currentDeviceType === EDeviceType.Neo
528
- ? currentDeviceType
529
- : getDeviceType(deviceFeatures);
530
- assertProtocolV2FirmwareTargetsSupported(capabilityDeviceType, this.params);
531
613
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
532
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
+ }
533
623
  const needsRemoteResources =
534
624
  !this.params.resourceFiles?.length && !!this.params.targetsToUpdate?.includes('resource');
625
+ const needsRemoteBootResources = !!this.params.targetsToUpdate?.includes('boot_resources');
535
626
 
536
627
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
537
628
  let bootloaderBinary: ArrayBuffer | null = null;
@@ -543,9 +634,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
543
634
  fwBinaryMap = this.collectExplicitTargetBinaries();
544
635
  bootloaderBinary = this.prepareBootloaderBinary();
545
636
  const needsRemoteFirmware = !this.hasExplicitProtocolV2Payload(fwBinaryMap);
546
- const needsRemoteBootResources =
547
- !!this.params.targetsToUpdate?.includes('resource') ||
548
- !!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
+ }
549
650
  if (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) {
550
651
  // Remote updates must use a freshly fetched config before any reboot or file write.
551
652
  await DataManager.forceReloadData({
@@ -572,7 +673,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
572
673
  if (err instanceof HardwareError && err.errorCode === HardwareErrorCode.NetworkError) {
573
674
  throw err;
574
675
  }
575
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
676
+ throw normalizeFirmwarePreparationError(err);
576
677
  }
577
678
 
578
679
  if (
@@ -588,9 +689,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
588
689
  );
589
690
  }
590
691
 
591
- const enteredBootloader = await this.enterProtocolV2BootloaderMode();
592
-
593
692
  if (needsRemoteResources) {
693
+ const enteredBootloader = await this.enterProtocolV2BootloaderMode();
594
694
  try {
595
695
  const stableResources = await this.prepareProtocolV2ResourceBundles();
596
696
  resourceBundles = [...(resourceBundles ?? []), ...(stableResources ?? [])];
@@ -606,25 +706,293 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
606
706
  );
607
707
  }
608
708
  }
609
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
709
+ throw normalizeFirmwarePreparationError(err);
610
710
  }
611
711
  }
612
712
 
613
- await this.executeProtocolV2Update({
713
+ return this.executeProtocolV2Update({
614
714
  fwBinaryMap,
615
715
  bootloaderBinary,
616
716
  ...(installItems ? { installItems } : undefined),
617
717
  ...(resourceBundles?.length ? { resourceBundles } : undefined),
618
718
  });
719
+ }
720
+
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
+ }
619
738
 
620
- this.postTipMessage(FirmwareUpdateTipMessage.SwitchFirmwareReconnectDevice);
621
- await this.exitProtocolV2BootloaderToNormal();
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
+ }
622
750
 
623
- const versions = await this.waitForProtocolV2FinalFeatures();
624
- this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
625
- DevicePool.resetState();
751
+ private async closeProtocolV2PreparedSources() {
752
+ const sources = this.protocolV2PreparedSources.splice(0);
753
+ await Promise.all(sources.map(source => source.close().catch(() => undefined)));
754
+ }
626
755
 
627
- return versions;
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);
628
996
  }
629
997
 
630
998
  private async getProtocolV2DeviceFeatures(): Promise<Features> {
@@ -668,8 +1036,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
668
1036
  private async captureProtocolV2PhysicalIdentity() {
669
1037
  const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
670
1038
  const serialNumber = this.getProtocolV2SerialNumber(deviceInfo);
671
- const path = this.device.originalDescriptor.path?.trim() || undefined;
672
- 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
+ }
673
1050
  this.protocolV2ExpectedSerialNumber = serialNumber;
674
1051
  this.protocolV2ExpectedPath = path;
675
1052
  }
@@ -846,12 +1223,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
846
1223
  private async prepareProtocolV2BootResources(): Promise<
847
1224
  ProtocolV2ResourceBundleBinary[] | undefined
848
1225
  > {
849
- if (
850
- !this.params.targetsToUpdate?.includes('resource') &&
851
- !this.params.targetsToUpdate?.includes('boot_resources')
852
- ) {
853
- return undefined;
854
- }
1226
+ if (!this.params.targetsToUpdate?.includes('boot_resources')) return undefined;
855
1227
  const resource = DataManager.getProtocolV2BootResources(this.getProtocolV2DeviceType());
856
1228
  if (!resource) throw new Error('Missing Protocol V2 boot resources configuration');
857
1229
 
@@ -958,38 +1330,88 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
958
1330
  };
959
1331
  }
960
1332
 
961
- private async syncProtocolV2ResourceBundles(
962
- bundles: ProtocolV2ResourceBundleBinary[],
963
- firmwareSize: number
964
- ): Promise<{ processedSize: number; totalSize: number }> {
965
- const transferStartTime = Date.now();
966
- 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
+ }
967
1338
 
968
- // Write directly to the device through FileWrite.
969
- let totalSize = 0;
970
- 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
+ }
971
1354
 
972
- const transferTotalSize = totalSize + firmwareSize;
973
- let processedSize = 0;
974
- for (const bundle of bundles) {
975
- Log.log(
976
- `[FirmwareUpdateV4] syncing resource ${bundle.name} -> ${bundle.devicePath} bytes=${bundle.binary.byteLength}`
977
- );
978
- processedSize = await this.protocolV2CommonUpdateProcess({
979
- payload: bundle.binary,
980
- filePath: bundle.devicePath,
981
- processedSize,
982
- 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,
983
1368
  });
1369
+ const data = toProtocolV2Bytes(res.message?.data);
1370
+ if (data.byteLength === 0) return null;
1371
+ chunks.push(data);
1372
+ offset += data.byteLength;
984
1373
  }
985
1374
 
986
- const elapsedMs = Date.now() - transferStartTime;
987
- Log.log(
988
- `[FirmwareUpdateV4] resource sync finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
989
- elapsedMs / 1000
990
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
991
- );
992
- 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
+ }
993
1415
  }
994
1416
 
995
1417
  private isProtocolV2BootloaderMode() {
@@ -1021,10 +1443,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1021
1443
  // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
1022
1444
  if (this.isProtocolV2RomloaderMode()) {
1023
1445
  Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
1446
+ this.protocolV2ExecutionInLoader = true;
1024
1447
  return false;
1025
1448
  }
1026
1449
  if (this.isProtocolV2BootloaderMode()) {
1027
1450
  Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
1451
+ this.protocolV2ExecutionInLoader = true;
1028
1452
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1029
1453
  return false;
1030
1454
  }
@@ -1034,6 +1458,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1034
1458
  await this.protocolV2Reboot(DeviceRebootType.Bootloader);
1035
1459
  await wait(1000);
1036
1460
  await this.waitForProtocolV2BootloaderMode();
1461
+ this.protocolV2ExecutionInLoader = true;
1037
1462
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1038
1463
  return true;
1039
1464
  } catch (error) {
@@ -1129,6 +1554,98 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1129
1554
  return entries;
1130
1555
  }
1131
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
+
1132
1649
  private async executeProtocolV2Update({
1133
1650
  fwBinaryMap,
1134
1651
  bootloaderBinary,
@@ -1140,104 +1657,168 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1140
1657
  installItems?: ProtocolV2InstallItem[];
1141
1658
  resourceBundles?: ProtocolV2ResourceBundleBinary[];
1142
1659
  }) {
1143
- const orderedInstallItems =
1660
+ const memoryInstallItems =
1144
1661
  installItems ??
1145
1662
  this.buildProtocolV2InstallItems({
1146
- bootloaderBinary: bootloaderBinary ?? null,
1147
1663
  fwBinaryMap: fwBinaryMap ?? [],
1664
+ bootloaderBinary: bootloaderBinary ?? null,
1148
1665
  });
1149
-
1150
- let firmwareSize = 0;
1151
- for (const item of orderedInstallItems) firmwareSize += item.binary.byteLength;
1152
-
1153
- this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
1154
-
1155
- let processedSize = 0;
1156
- let totalSize = firmwareSize;
1157
- if (resourceBundles?.length) {
1158
- const resourceTransfer = await this.syncProtocolV2ResourceBundles(
1159
- resourceBundles,
1160
- 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
+ }))
1161
1681
  );
1162
- processedSize = resourceTransfer.processedSize;
1163
- totalSize = resourceTransfer.totalSize;
1682
+ return await this.executeProtocolV2Phases({
1683
+ installSources,
1684
+ resourceSources,
1685
+ });
1686
+ } finally {
1687
+ await this.closeProtocolV2PreparedSources();
1164
1688
  }
1689
+ }
1165
1690
 
1166
- // Skip staging and installation when the update contains resource files only.
1167
- if (orderedInstallItems.length === 0) {
1168
- Log.log('[FirmwareUpdateV4] no firmware targets to install (resource files only)');
1169
- if (totalSize > 0) {
1170
- 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;
1171
1703
  }
1172
- return;
1173
1704
  }
1174
- let transferredSize = processedSize;
1175
- const transferStartTime = Date.now();
1176
- const transferTransport = this.getProtocolV2FirmwareTransferTransport();
1177
- const chunkSize = this.getProtocolV2FirmwareChunkSize();
1178
- const onTransferredBytes = (bytes: number) => {
1179
- transferredSize = bytes;
1180
- };
1181
- Log.log(
1182
- `[FirmwareUpdateV4] transfer started transport=${transferTransport} total=${totalSize} bytes chunk=${chunkSize} bytes`
1183
- );
1184
1705
 
1185
- const stagedInstallTargets: ProtocolV2InstallTarget[] = [];
1186
-
1187
- try {
1188
- for (const item of orderedInstallItems) {
1189
- const filePath = this.getProtocolV2InstallItemStagingPath(item);
1190
- Log.log(
1191
- `[FirmwareUpdateV4] staging ${item.kind} via FilesystemFileWrite target=${item.targetId} path=${filePath} source=${item.fileName} bytes=${item.binary.byteLength}`
1192
- );
1193
- processedSize = await this.protocolV2CommonUpdateProcess({
1194
- payload: item.binary,
1195
- filePath,
1196
- processedSize,
1197
- totalSize,
1198
- onTransferredBytes,
1199
- });
1200
- transferredSize = processedSize;
1201
- await this.verifyProtocolV2StagedFile(filePath, item.binary.byteLength);
1202
-
1203
- stagedInstallTargets.push({
1204
- ...item,
1205
- path: filePath,
1206
- });
1207
- }
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
+ }
1208
1717
 
1209
- if (totalSize > 0) {
1210
- this.postProgressMessage(100, 'transferData');
1211
- }
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
+ }
1212
1733
 
1213
- const elapsedMs = Date.now() - transferStartTime;
1214
- Log.log(
1215
- `[FirmwareUpdateV4] transfer finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
1216
- elapsedMs / 1000
1217
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
1218
- );
1219
- } catch (error) {
1220
- const elapsedMs = Date.now() - transferStartTime;
1221
- Log.warn(
1222
- `[FirmwareUpdateV4] transfer failed transport=${transferTransport} bytes=${transferredSize}/${totalSize} elapsed=${(
1223
- elapsedMs / 1000
1224
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(transferredSize, elapsedMs)} KB/s`
1225
- );
1226
- throw error;
1734
+ if (totalSize > 0) {
1735
+ this.postProgressMessage(100, 'transferData');
1736
+ }
1737
+ if (stagedInstallTargets.length === 0) {
1738
+ return;
1227
1739
  }
1228
1740
 
1229
1741
  this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
1230
-
1231
- const allTargets = stagedInstallTargets.map(item => ({
1742
+ const targets = stagedInstallTargets.map(item => ({
1232
1743
  target_id: item.targetId,
1233
1744
  path: item.path,
1234
1745
  }));
1235
- Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(allTargets)}`);
1236
- await this.protocolV2StartFirmwareUpdate({ targets: allTargets });
1237
- 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
+ );
1238
1819
  }
1239
1820
 
1240
- private getProtocolV2InstallItemStagingPath(item: ProtocolV2InstallItem) {
1821
+ private getProtocolV2InstallItemStagingPath(item: Pick<ProtocolV2InstallItem, 'fileName'>) {
1241
1822
  return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`;
1242
1823
  }
1243
1824
 
@@ -1256,7 +1837,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1256
1837
 
1257
1838
  private assertProtocolV2TargetStatus(
1258
1839
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
1259
- expectedTargetIds: Set<number>
1840
+ expectedTargetIds: Set<number>,
1841
+ expectedPaths = new Map<number, string>()
1260
1842
  ) {
1261
1843
  Log.log(
1262
1844
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
@@ -1287,17 +1869,57 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1287
1869
  const matchingTargets = statusTargets.filter(target =>
1288
1870
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
1289
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
+ );
1290
1893
  const completedTargetIds = new Set<number>();
1291
- matchingTargets.forEach(target => {
1894
+ completedTargets.forEach(target => {
1292
1895
  const targetId = normalizeProtocolV2TargetId(target.target_id);
1293
- if (targetId !== undefined && isProtocolV2TargetStatusFinished(target.status)) {
1896
+ if (targetId !== undefined) {
1294
1897
  completedTargetIds.add(targetId);
1295
1898
  }
1296
1899
  });
1297
- const allExpectedTargetsCompleted =
1298
- expectedTargetIds.size > 0 &&
1299
- Array.from(expectedTargetIds).every(targetId => completedTargetIds.has(targetId));
1300
- 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
+ }
1301
1923
  this.postProgressMessage(100, 'installingFirmware');
1302
1924
  return true;
1303
1925
  }
@@ -1335,7 +1957,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1335
1957
  private async waitForProtocolV2FirmwareUpdateComplete(
1336
1958
  targets: Array<{ target_id: number; path: string }>
1337
1959
  ) {
1960
+ this.protocolV2FinalStatusVerified = false;
1338
1961
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
1962
+ const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
1339
1963
  const startTime = Date.now();
1340
1964
  let lastError: unknown;
1341
1965
  let shouldReconnect = true;
@@ -1370,7 +1994,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1370
1994
  );
1371
1995
  const statusTargets = (statusResponse.message.records ??
1372
1996
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
1373
- if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) {
1997
+ if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
1998
+ this.protocolV2FinalStatusVerified = true;
1374
1999
  return;
1375
2000
  }
1376
2001
 
@@ -1418,10 +2043,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1418
2043
  lastError = new Error('Protocol V2 firmware targets are still installing');
1419
2044
  }
1420
2045
  } catch (error) {
1421
- if (
1422
- error instanceof HardwareError &&
1423
- error.errorCode === HardwareErrorCode.FirmwareError
1424
- ) {
2046
+ if (isProtocolV2TerminalInstallStatusError(error)) {
1425
2047
  throw error;
1426
2048
  }
1427
2049
  // Only consecutive full status dumps can prove that a record is absent.
@@ -1461,7 +2083,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1461
2083
  if (this.isProtocolV2ReconnectIdentityError(error)) {
1462
2084
  throw error;
1463
2085
  }
1464
- 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
+ ) {
1465
2093
  throw error;
1466
2094
  }
1467
2095
  shouldReconnect = true;
@@ -1492,6 +2120,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1492
2120
  Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
1493
2121
  }
1494
2122
  await this.protocolV2Reboot(DeviceRebootType.Normal);
2123
+ this.protocolV2ExecutionInLoader = false;
1495
2124
  }
1496
2125
 
1497
2126
  private async probeProtocolV2NormalMode(deviceInfo: ProtocolV2DeviceInfo) {
@@ -1514,6 +2143,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1514
2143
  const bootloaderVersion = getDeviceBootloaderVersion(features).join('.');
1515
2144
  const bleVersion = getDeviceBLEFirmwareVersion(features).join('.');
1516
2145
  const firmwareVersion = getDeviceFirmwareVersion(features).join('.');
2146
+ this.protocolV2LatestFinalFeatures = features;
1517
2147
  if (firmwareVersion === '0.0.0') {
1518
2148
  Log.warn(
1519
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.'
@@ -1527,6 +2157,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1527
2157
  };
1528
2158
  }
1529
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
+
1530
2171
  private async waitForProtocolV2ReconnectAndFeatures(timeout: number) {
1531
2172
  const startTime = Date.now();
1532
2173
  let lastError: unknown;
@@ -1648,106 +2289,43 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1648
2289
  }
1649
2290
 
1650
2291
  private isProtocolV2ReconnectIdentityError(error: unknown) {
1651
- // 序列号在 Loader 刚启动时可能暂时不可用,BLE descriptor 也没有稳定的
1652
- // path。此时应继续轮询,只有明确读到不同序列号时才立即终止更新。
1653
- return this.normalizeErrorMessage(error).includes(
1654
- 'Protocol V2 reconnect physical identity mismatch'
1655
- );
2292
+ return this.normalizeErrorMessage(error).includes('Protocol V2 reconnect physical identity');
1656
2293
  }
1657
2294
 
1658
- private async protocolV2CommonUpdateProcess(params: ProtocolV2FileTransferParams) {
1659
- let lastError: unknown;
1660
- for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1661
- try {
1662
- return await this.protocolV2WriteWholeFile(params);
1663
- } catch (error) {
1664
- lastError = error;
1665
- Log.error(
1666
- `Protocol V2 file transfer failed path=${params.filePath} attempt=${attempt}/${PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT}; restarting from offset 0`,
1667
- error
1668
- );
1669
- if (attempt === PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1670
- break;
1671
- }
1672
- await this.recoverProtocolV2FileTransfer();
1673
- }
1674
- }
1675
-
1676
- throw ERRORS.TypedError(
1677
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1678
- `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1679
- );
1680
- }
1681
-
1682
- private async protocolV2WriteWholeFile({
1683
- payload,
1684
- filePath,
1685
- processedSize,
1686
- totalSize,
1687
- onTransferredBytes,
1688
- }: ProtocolV2FileTransferParams) {
1689
- const chunkSize = this.getProtocolV2FirmwareChunkSize();
1690
- const getUploadProgress = (fileOffset: number) => {
1691
- if (totalSize !== undefined && processedSize !== undefined) {
1692
- return Math.min(Math.ceil(((processedSize + fileOffset) / totalSize) * 100), 99);
1693
- }
1694
- return Math.min(Math.ceil((fileOffset / payload.byteLength) * 100), 99);
1695
- };
1696
-
1697
- try {
1698
- await writeProtocolV2File({
1699
- commands: this.device.getCommands(),
1700
- path: filePath,
1701
- data: payload,
1702
- totalSize: payload.byteLength,
1703
- chunkSize,
1704
- overwrite: true,
1705
- append: false,
1706
- writeWithResponse: false,
1707
- maxChunkRetries: 0,
1708
- getUiPercentage: ({ offset, chunkLength }) =>
1709
- getProtocolV2DeviceTransferProgress(
1710
- (processedSize ?? 0) + offset,
1711
- (processedSize ?? 0) + offset + chunkLength,
1712
- totalSize ?? payload.byteLength
1713
- ),
1714
- onProgress: progress => {
1715
- const transferredBytes = (processedSize ?? 0) + progress.transferredBytes;
1716
- onTransferredBytes?.(transferredBytes);
1717
- this.postProgressMessage(getUploadProgress(progress.transferredBytes), 'transferData', {
1718
- transferredBytes,
1719
- totalBytes: totalSize ?? payload.byteLength,
1720
- rateBytesPerSecond: progress.rateBytesPerSecond,
1721
- elapsedMs: progress.elapsedMs,
1722
- });
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,
1723
2313
  },
1724
- });
1725
- } catch (error) {
1726
- if (
1727
- error instanceof HardwareError &&
1728
- error.errorCode === HardwareErrorCode.RuntimeError &&
1729
- error.message.includes('FilesystemFileWrite')
1730
- ) {
1731
- 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
+ }
1732
2325
  }
1733
- throw error;
1734
- }
1735
-
1736
- return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0;
1737
- }
1738
-
1739
- private getProtocolV2FirmwareTransferTransport() {
1740
- const env = DataManager.getSettings('env');
1741
- if (env && DataManager.isBleConnect(env)) {
1742
- return 'BLE';
1743
- }
1744
- if (
1745
- env &&
1746
- (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'web')
1747
- ) {
1748
- return 'WebUSB';
2326
+ throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
1749
2327
  }
1750
- return env ?? 'unknown';
2328
+ return writeRes;
1751
2329
  }
1752
2330
 
1753
2331
  private async recoverProtocolV2FileTransfer() {