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

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