@onekeyfe/hd-core 1.2.0-alpha.8 → 1.2.0-alpha.9

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 (63) hide show
  1. package/__tests__/device-wallet-session-store.test.ts +131 -0
  2. package/__tests__/protocol-v2-firmware-targets.test.ts +19 -0
  3. package/__tests__/protocol-v2.test.ts +738 -460
  4. package/dist/api/ClearSessionCache.d.ts +9 -0
  5. package/dist/api/ClearSessionCache.d.ts.map +1 -0
  6. package/dist/api/FirmwareUpdateV4.d.ts +7 -6
  7. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  8. package/dist/api/firmware/getBinary.d.ts +1 -1
  9. package/dist/api/index.d.ts +3 -0
  10. package/dist/api/index.d.ts.map +1 -1
  11. package/dist/api/protocol-v2/DeviceSessionGet.d.ts +9 -0
  12. package/dist/api/protocol-v2/DeviceSessionGet.d.ts.map +1 -0
  13. package/dist/api/protocol-v2/DeviceStatusGet.d.ts +6 -0
  14. package/dist/api/protocol-v2/DeviceStatusGet.d.ts.map +1 -0
  15. package/dist/api/protocol-v2/FilesystemFormat.d.ts.map +1 -1
  16. package/dist/api/protocol-v2/helpers.d.ts +1 -1
  17. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  18. package/dist/device/Device.d.ts +2 -2
  19. package/dist/device/Device.d.ts.map +1 -1
  20. package/dist/device/DeviceWalletSessionStore.d.ts +15 -0
  21. package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -0
  22. package/dist/index.d.ts +32 -19
  23. package/dist/index.js +644 -263
  24. package/dist/inject.d.ts.map +1 -1
  25. package/dist/protocols/protocol-v2/index.d.ts +1 -0
  26. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  27. package/dist/protocols/protocol-v2/walletSession.d.ts +13 -0
  28. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -0
  29. package/dist/types/api/firmwareUpdate.d.ts +4 -1
  30. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  31. package/dist/types/api/index.d.ts +6 -1
  32. package/dist/types/api/index.d.ts.map +1 -1
  33. package/dist/types/api/protocolV2.d.ts +6 -2
  34. package/dist/types/api/protocolV2.d.ts.map +1 -1
  35. package/dist/types/api/sessionCache.d.ts +10 -0
  36. package/dist/types/api/sessionCache.d.ts.map +1 -0
  37. package/dist/types/settings.d.ts +6 -14
  38. package/dist/types/settings.d.ts.map +1 -1
  39. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  40. package/dist/utils/patch.d.ts +1 -1
  41. package/dist/utils/patch.d.ts.map +1 -1
  42. package/package.json +4 -4
  43. package/src/api/ClearSessionCache.ts +28 -0
  44. package/src/api/FileRead.ts +2 -2
  45. package/src/api/FirmwareUpdateV4.ts +269 -210
  46. package/src/api/index.ts +3 -0
  47. package/src/api/protocol-v2/DeviceSessionGet.ts +26 -0
  48. package/src/api/protocol-v2/DeviceStatusGet.ts +14 -0
  49. package/src/api/protocol-v2/FilesystemFormat.ts +4 -1
  50. package/src/api/protocol-v2/helpers.ts +17 -14
  51. package/src/data/messages/messages-protocol-v2.json +134 -3
  52. package/src/data/messages/messages.json +8 -0
  53. package/src/device/Device.ts +64 -56
  54. package/src/device/DeviceWalletSessionStore.ts +78 -0
  55. package/src/inject.ts +5 -2
  56. package/src/protocols/protocol-v2/index.ts +1 -0
  57. package/src/protocols/protocol-v2/walletSession.ts +83 -0
  58. package/src/types/api/firmwareUpdate.ts +8 -2
  59. package/src/types/api/index.ts +10 -3
  60. package/src/types/api/protocolV2.ts +16 -2
  61. package/src/types/api/sessionCache.ts +14 -0
  62. package/src/types/settings.ts +14 -16
  63. package/src/utils/deviceFeaturesUtils.ts +22 -32
@@ -38,7 +38,6 @@ import type {
38
38
  Features,
39
39
  IFirmwareReleaseInfo,
40
40
  IProtocolV2FirmwareComponent,
41
- IProtocolV2ResourceManifestPackage,
42
41
  IVersionArray,
43
42
  } from '../types';
44
43
 
@@ -47,19 +46,20 @@ const Log = getLogger(LoggerNames.Method);
47
46
  const SESSION_ERROR = 'session not found';
48
47
  const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 60 * 1000;
49
48
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
50
- const PROTOCOL_V2_START_UPDATE_TIMEOUT = 60 * 1000;
49
+ const PROTOCOL_V2_START_UPDATE_TIMEOUT = 3 * 60 * 1000;
51
50
  const PROTOCOL_V2_INSTALL_TIMEOUT = 5 * 60 * 1000;
52
51
  const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
53
52
  const PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS = 1;
54
53
  const PROTOCOL_V2_TARGET_STATUS_FINISHED = 2;
55
54
  const PROTOCOL_V2_TARGET_STATUS_FAILED_MIN = 3;
56
55
  const PROTOCOL_V2_CONNECT_PROTOCOL = 'V2';
57
- const PROTOCOL_V2_FIRMWARE_STAGING_VOLUME = 'vol1:';
56
+ const PROTOCOL_V2_FIRMWARE_STAGING_VOLUME = 'vol0:/';
58
57
  const PROTOCOL_V2_MIN_FILE_CHUNK_SIZE = 64;
59
58
  const PROTOCOL_V2_CONNECT_RETRY_COUNT = 10;
60
59
  const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
61
60
  const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
62
61
  const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 60 * 1000;
62
+ const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
63
63
  const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
64
64
  const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
65
65
  const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
@@ -107,6 +107,25 @@ type ProtocolV2InstallTarget = ProtocolV2InstallItem & {
107
107
  path: string;
108
108
  };
109
109
 
110
+ type ProtocolV2FileTransferParams = PROTO.FirmwareUpload & {
111
+ filePath: string;
112
+ processedSize?: number;
113
+ totalSize?: number;
114
+ onTransferredBytes?: (transferredBytes: number) => void;
115
+ };
116
+
117
+ /** RESC bundle okpkg(FileWrite 直写模式),每个独立同步到 devicePath */
118
+ type ProtocolV2ResourceBundleBinary = {
119
+ name: string;
120
+ binary: ArrayBuffer;
121
+ devicePath: string;
122
+ /** 远端配置模式下的下载 URL(手动模式不填) */
123
+ url?: string;
124
+ version?: IVersionArray;
125
+ payloadHash?: string;
126
+ headerHash?: string;
127
+ };
128
+
110
129
  type ProtocolV2RemoteComponentBinary = ProtocolV2RemoteComponentTarget & {
111
130
  binary: ArrayBuffer;
112
131
  };
@@ -114,7 +133,7 @@ type ProtocolV2RemoteComponentBinary = ProtocolV2RemoteComponentTarget & {
114
133
  type ProtocolV2RemoteComponentTarget = {
115
134
  fileName: string;
116
135
  targetId: number;
117
- kind: 'bootloader' | 'firmware' | 'resource';
136
+ kind: 'bootloader' | 'firmware';
118
137
  };
119
138
 
120
139
  type ProtocolV2OkppHeader = {
@@ -167,11 +186,6 @@ const PROTOCOL_V2_REMOTE_COMPONENT_TARGETS: Readonly<
167
186
  targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04,
168
187
  kind: 'firmware',
169
188
  },
170
- CRATE: {
171
- fileName: 'resource.bin',
172
- targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_CRATE,
173
- kind: 'resource',
174
- },
175
189
  };
176
190
 
177
191
  const PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE =
@@ -393,7 +407,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
393
407
 
394
408
  validateParams(payload, [
395
409
  { name: 'chunkSize', type: 'number' },
396
- { name: 'resourceBinaries', type: 'array', allowEmpty: true },
397
410
  { name: 'forcedUpdateRes', type: 'boolean' },
398
411
  { name: 'bootloaderBinary', type: 'buffer' },
399
412
  { name: 'romloaderBinary', type: 'buffer' },
@@ -406,6 +419,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
406
419
  { name: 'se04Binary', type: 'buffer' },
407
420
  { name: 'firmwareType', type: 'string' },
408
421
  { name: 'platform', type: 'string' },
422
+ { name: 'resourceBundleFiles', type: 'array', allowEmpty: true },
409
423
  ]);
410
424
 
411
425
  this.params = {
@@ -420,7 +434,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
420
434
  se02Binary: payload.se02Binary,
421
435
  se03Binary: payload.se03Binary,
422
436
  se04Binary: payload.se04Binary,
423
- resourceBinaries: payload.resourceBinaries,
437
+ resourceBundleFiles: payload.resourceBundleFiles,
424
438
  firmwareType: payload.firmwareType,
425
439
  platform: payload.platform,
426
440
  };
@@ -459,13 +473,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
459
473
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
460
474
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
461
475
 
462
- let resourceBinaryMap: ProtocolV2TargetBinary[] = [];
463
476
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
464
477
  let bootloaderBinary: ArrayBuffer | null = null;
465
478
  let installItems: ProtocolV2InstallItem[] | undefined;
466
479
  try {
467
480
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
468
- resourceBinaryMap = await this.prepareResourceBinaries(firmwareType, deviceFeatures);
469
481
  fwBinaryMap = this.collectExplicitTargetBinaries();
470
482
  bootloaderBinary = this.prepareBootloaderBinary();
471
483
  if (!this.hasExplicitProtocolV2Payload(fwBinaryMap)) {
@@ -473,7 +485,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
473
485
  firmwareType,
474
486
  deviceFeatures
475
487
  );
476
- resourceBinaryMap = remoteBinaries.resourceBinaryMap;
477
488
  bootloaderBinary = remoteBinaries.bootloaderBinary;
478
489
  fwBinaryMap = remoteBinaries.fwBinaryMap;
479
490
  installItems = remoteBinaries.installItems;
@@ -483,7 +494,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
483
494
  throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
484
495
  }
485
496
 
486
- if (resourceBinaryMap.length === 0 && !bootloaderBinary && fwBinaryMap.length === 0) {
497
+ const resourceBundles = this.prepareProtocolV2ResourceBundles(firmwareType, deviceFeatures);
498
+
499
+ if (!bootloaderBinary && fwBinaryMap.length === 0 && !resourceBundles?.length) {
487
500
  throw ERRORS.TypedError(
488
501
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
489
502
  'No firmware to update'
@@ -493,10 +506,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
493
506
  await this.enterProtocolV2BootloaderMode();
494
507
 
495
508
  await this.executeProtocolV2Update({
496
- resourceBinaryMap,
497
509
  fwBinaryMap,
498
510
  bootloaderBinary,
499
511
  ...(installItems ? { installItems } : undefined),
512
+ ...(resourceBundles?.length ? { resourceBundles } : undefined),
500
513
  });
501
514
 
502
515
  await this.exitProtocolV2BootloaderToNormal();
@@ -519,68 +532,26 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
519
532
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Device features not available');
520
533
  }
521
534
 
522
- private async prepareResourceBinaries(firmwareType: EFirmwareType, features: Features) {
523
- if (this.params.resourceBinaries?.length) {
524
- this.params.resourceBinaries.forEach((binary, index) => {
525
- if (!(binary instanceof ArrayBuffer)) {
526
- throw ERRORS.TypedError(
527
- HardwareErrorCode.CallMethodInvalidParameter,
528
- `Parameter [resourceBinaries.${index}] is of type invalid and should be [buffer].`
529
- );
530
- }
531
- });
532
-
533
- return this.params.resourceBinaries.map((binary, index) => ({
534
- fileName: `resource-${index + 1}.bin`,
535
- binary,
536
- targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_CRATE,
537
- }));
538
- }
539
- const resourceUrl = DataManager.getSysResourcesLatestRelease({
540
- features,
541
- forcedUpdateRes: this.params.forcedUpdateRes,
542
- firmwareType,
543
- });
544
-
545
- if (resourceUrl) {
546
- const resource = (await getSysResourceBinary(resourceUrl)).binary;
547
- return [
548
- {
549
- fileName: 'resource.bin',
550
- binary: resource,
551
- targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_CRATE,
552
- },
553
- ];
554
- }
555
- Log.warn('No resource url found');
556
- return [];
557
- }
558
-
559
535
  private prepareBootloaderBinary(): ArrayBuffer | null {
560
536
  return this.params.bootloaderBinary ?? null;
561
537
  }
562
538
 
563
539
  private hasExplicitProtocolV2Payload(fwBinaryMap: ProtocolV2TargetBinary[]) {
564
540
  return (
565
- !!this.params.resourceBinaries?.length ||
541
+ !!this.params.resourceBundleFiles?.length ||
566
542
  !!this.params.bootloaderBinary ||
567
543
  fwBinaryMap.length > 0
568
544
  );
569
545
  }
570
546
 
571
547
  private buildProtocolV2InstallItems({
572
- resourceBinaryMap,
573
548
  bootloaderBinary,
574
549
  fwBinaryMap,
575
550
  }: {
576
- resourceBinaryMap: ProtocolV2TargetBinary[];
577
551
  bootloaderBinary: ArrayBuffer | null;
578
552
  fwBinaryMap: ProtocolV2TargetBinary[];
579
553
  }): ProtocolV2InstallItem[] {
580
- const installItems: ProtocolV2InstallItem[] = resourceBinaryMap.map(resource => ({
581
- ...resource,
582
- kind: 'resource',
583
- }));
554
+ const installItems: ProtocolV2InstallItem[] = [];
584
555
 
585
556
  if (bootloaderBinary) {
586
557
  installItems.push({
@@ -738,86 +709,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
738
709
  return parseProtocolV2OkppHeader(headerBytes);
739
710
  }
740
711
 
741
- private async isProtocolV2ResourcePackageMatched(
742
- pkg: IProtocolV2ResourceManifestPackage,
743
- manifestVersion?: IVersionArray
744
- ) {
745
- try {
746
- const header = await this.readProtocolV2DeviceFileHeader(pkg.path);
747
- if (!header) return false;
748
-
749
- const expectedType = pkg.type ?? 'RESC';
750
- const expectedVersion = pkg.version ?? manifestVersion;
751
- if (header.type !== expectedType) return false;
752
- if (expectedVersion && compareProtocolV2Versions(header.version, expectedVersion) !== 0) {
753
- return false;
754
- }
755
-
756
- const expectedPayloadHash = normalizeProtocolV2Hex(pkg.payloadHash);
757
- if (expectedPayloadHash && header.payloadHash !== expectedPayloadHash) return false;
758
-
759
- const expectedHeaderHash = normalizeProtocolV2Hex(pkg.headerHash);
760
- if (expectedHeaderHash && header.headerHash !== expectedHeaderHash) return false;
761
-
762
- return true;
763
- } catch (error) {
764
- Log.log(`Protocol V2 resource package check failed for ${pkg.path}: `, error);
765
- return false;
766
- }
767
- }
768
-
769
- private async isProtocolV2ResourceManifestSatisfied(
770
- manifest: IFirmwareReleaseInfo['resourceManifest']
771
- ) {
772
- if (!manifest?.packages?.length) return false;
773
-
774
- for (const pkg of manifest.packages) {
775
- if (!(await this.isProtocolV2ResourcePackageMatched(pkg, manifest.version))) {
776
- return false;
777
- }
778
- }
779
- return true;
780
- }
781
-
782
- private getProtocolV2ResourceManifest(
783
- release: IFirmwareReleaseInfo,
784
- component: IProtocolV2FirmwareComponent
785
- ) {
786
- return component.resourceManifest ?? release.resourceManifest;
787
- }
788
-
789
- private getProtocolV2ResourceComponentFileName(key: string) {
790
- const safeKey = key.replace(/[^a-z0-9_-]/gi, '_') || 'resource';
791
- if (safeKey === 'resource') {
792
- return PROTOCOL_V2_REMOTE_COMPONENT_TARGETS.CRATE.fileName;
793
- }
794
- return `resource-${safeKey}.bin`;
795
- }
796
-
797
- private async shouldInstallRemoteProtocolV2Component(
712
+ private shouldInstallRemoteProtocolV2Component(
798
713
  release: IFirmwareReleaseInfo,
799
714
  key: string,
800
715
  component: IProtocolV2FirmwareComponent,
801
716
  target: ProtocolV2RemoteComponentTarget,
802
717
  features: Features
803
718
  ) {
804
- if (target.kind === 'resource') {
805
- if (
806
- this.params.forcedUpdateRes ||
807
- features.bootloaderMode ||
808
- features.mode === 'bootloader'
809
- ) {
810
- return true;
811
- }
812
- const resourceMatched = await this.isProtocolV2ResourceManifestSatisfied(
813
- this.getProtocolV2ResourceManifest(release, component)
814
- );
815
- if (resourceMatched) {
816
- Log.log(`[FirmwareUpdateV4] skip Protocol V2 resource component ${key}; manifest matched`);
817
- }
818
- return !resourceMatched;
819
- }
820
-
821
719
  const versionSatisfied = this.isProtocolV2ComponentVersionSatisfied(
822
720
  release,
823
721
  component,
@@ -852,14 +750,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
852
750
  private async prepareRemoteProtocolV2Binaries(firmwareType: EFirmwareType, features: Features) {
853
751
  const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
854
752
 
855
- const resourceBinaryMap: ProtocolV2TargetBinary[] = [];
856
753
  let bootloaderBinary: ArrayBuffer | null = null;
857
754
  const fwBinaryMap: ProtocolV2TargetBinary[] = [];
858
755
  const installItems: ProtocolV2InstallItem[] = [];
859
756
 
860
757
  if (!release) {
861
758
  return {
862
- resourceBinaryMap,
863
759
  bootloaderBinary,
864
760
  fwBinaryMap,
865
761
  installItems,
@@ -870,7 +766,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
870
766
 
871
767
  for (const [key, component] of entries) {
872
768
  const target = this.getRemoteComponentTarget(key, component);
873
- const shouldInstall = await this.shouldInstallRemoteProtocolV2Component(
769
+ const shouldInstall = this.shouldInstallRemoteProtocolV2Component(
874
770
  release,
875
771
  key,
876
772
  component,
@@ -879,15 +775,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
879
775
  );
880
776
  if (shouldInstall) {
881
777
  const remoteBinary = await this.downloadRemoteProtocolV2Component(key, component);
882
- if (remoteBinary.kind === 'resource') {
883
- const binaryEntry = {
884
- fileName: this.getProtocolV2ResourceComponentFileName(key),
885
- binary: remoteBinary.binary,
886
- targetId: remoteBinary.targetId,
887
- };
888
- resourceBinaryMap.push(binaryEntry);
889
- installItems.push({ ...binaryEntry, kind: remoteBinary.kind });
890
- } else if (remoteBinary.kind === 'bootloader') {
778
+ if (remoteBinary.kind === 'bootloader') {
891
779
  bootloaderBinary = remoteBinary.binary;
892
780
  installItems.push({
893
781
  fileName: remoteBinary.fileName,
@@ -908,13 +796,153 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
908
796
  }
909
797
 
910
798
  return {
911
- resourceBinaryMap,
912
799
  bootloaderBinary,
913
800
  fwBinaryMap,
914
801
  installItems,
915
802
  };
916
803
  }
917
804
 
805
+ // ============================================================
806
+ // RESC bundle 直写增量同步(FileRead 比对 + FilesystemFileWrite 直写)
807
+ // ============================================================
808
+
809
+ /**
810
+ * 准备 RESC bundle 列表。
811
+ *
812
+ * 两种模式(同 FirmwareUpdateV3 的 binary vs version 模式):
813
+ * - 用户传了 resourceBundleFiles(binary 数组):直接用,不做版本比对。
814
+ * - 用户没传:从远端 config.json 的 release.resourceBundles 拉取,
815
+ * 此时 syncProtocolV2ResourceBundles 会按需下载 + 比对设备已有 header 跳过。
816
+ */
817
+ private prepareProtocolV2ResourceBundles(
818
+ firmwareType: EFirmwareType,
819
+ features: Features
820
+ ): ProtocolV2ResourceBundleBinary[] | undefined {
821
+ // 模式1:用户手动传入 binary,直接安装,不比对
822
+ if (this.params.resourceBundleFiles?.length) {
823
+ return this.params.resourceBundleFiles.map(file => ({
824
+ name: file.devicePath.split('/').pop() ?? file.devicePath,
825
+ binary: file.binary,
826
+ devicePath: file.devicePath,
827
+ }));
828
+ }
829
+
830
+ // 模式2:远端配置模式,后续按需下载 + 比对
831
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
832
+ if (!release?.resourceBundles?.length) return undefined;
833
+
834
+ return release.resourceBundles.map(bundle => ({
835
+ name: bundle.name,
836
+ binary: new ArrayBuffer(0),
837
+ devicePath: bundle.devicePath,
838
+ url: bundle.url,
839
+ version: bundle.version,
840
+ payloadHash: bundle.payloadHash,
841
+ headerHash: bundle.headerHash,
842
+ })) as ProtocolV2ResourceBundleBinary[];
843
+ }
844
+
845
+ /**
846
+ * 同步 RESC bundles 到设备。
847
+ *
848
+ * 手动传入模式(有 binary):直接 FileWrite 直写,不比对。
849
+ * 远端配置模式(无 binary,有 url):按需下载 + FileRead 比对跳过已更新的。
850
+ */
851
+ private async syncProtocolV2ResourceBundles(
852
+ bundles: ProtocolV2ResourceBundleBinary[],
853
+ firmwareSize: number
854
+ ): Promise<{ processedSize: number; totalSize: number }> {
855
+ const transferStartTime = Date.now();
856
+ const transferTransport = this.getProtocolV2FirmwareTransferTransport();
857
+
858
+ const isManualMode = bundles.every(b => b.binary.byteLength > 0);
859
+ let bundlesToSync = bundles;
860
+
861
+ // 远端配置模式:按需下载 + 比对跳过
862
+ if (!isManualMode) {
863
+ const filtered: ProtocolV2ResourceBundleBinary[] = [];
864
+ for (const bundle of bundles) {
865
+ // 下载 binary
866
+ if (bundle.binary.byteLength === 0 && bundle.url) {
867
+ Log.log(`[FirmwareUpdateV4] downloading RESC bundle ${bundle.name} from ${bundle.url}`);
868
+ const { binary } = await getSysResourceBinary(bundle.url);
869
+ bundle.binary = binary;
870
+ }
871
+ // 比对设备上已有 header
872
+ const upToDate = await this.isProtocolV2ResourceBundleUpToDate(bundle);
873
+ if (upToDate) {
874
+ Log.log(`[FirmwareUpdateV4] skip RESC bundle ${bundle.name}; already up to date`);
875
+ } else {
876
+ filtered.push(bundle);
877
+ }
878
+ }
879
+ bundlesToSync = filtered;
880
+ }
881
+
882
+ if (bundlesToSync.length === 0) {
883
+ Log.log('[FirmwareUpdateV4] all RESC bundles up to date, nothing to sync');
884
+ return { processedSize: 0, totalSize: firmwareSize };
885
+ }
886
+
887
+ // FileWrite 直写到设备
888
+ let totalSize = 0;
889
+ for (const b of bundlesToSync) totalSize += b.binary.byteLength;
890
+
891
+ const transferTotalSize = totalSize + firmwareSize;
892
+ let processedSize = 0;
893
+ for (const bundle of bundlesToSync) {
894
+ Log.log(
895
+ `[FirmwareUpdateV4] syncing RESC bundle ${bundle.name} -> ${bundle.devicePath} bytes=${bundle.binary.byteLength}`
896
+ );
897
+ processedSize = await this.protocolV2CommonUpdateProcess({
898
+ payload: bundle.binary,
899
+ filePath: bundle.devicePath,
900
+ processedSize,
901
+ totalSize: transferTotalSize,
902
+ });
903
+ }
904
+
905
+ const elapsedMs = Date.now() - transferStartTime;
906
+ Log.log(
907
+ `[FirmwareUpdateV4] RESC bundle sync finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
908
+ elapsedMs / 1000
909
+ ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
910
+ );
911
+ return { processedSize, totalSize: transferTotalSize };
912
+ }
913
+
914
+ /**
915
+ * 比对设备上已有 okpkg 的 OKPP header(仅远端配置模式使用)。
916
+ */
917
+ private async isProtocolV2ResourceBundleUpToDate(
918
+ bundle: ProtocolV2ResourceBundleBinary
919
+ ): Promise<boolean> {
920
+ if (this.params.forcedUpdateRes) return false;
921
+ if (!bundle.version && !bundle.payloadHash) return false;
922
+
923
+ try {
924
+ const header = await this.readProtocolV2DeviceFileHeader(bundle.devicePath);
925
+ if (!header) return false;
926
+
927
+ if (bundle.version) {
928
+ const cmp = compareProtocolV2Versions(header.version, bundle.version);
929
+ if (cmp === undefined || cmp !== 0) return false;
930
+ }
931
+ if (bundle.payloadHash) {
932
+ const expected = normalizeProtocolV2Hex(bundle.payloadHash);
933
+ if (expected && header.payloadHash !== expected) return false;
934
+ }
935
+ if (bundle.headerHash) {
936
+ const expected = normalizeProtocolV2Hex(bundle.headerHash);
937
+ if (expected && header.headerHash !== expected) return false;
938
+ }
939
+ return true;
940
+ } catch (error) {
941
+ Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
942
+ return false;
943
+ }
944
+ }
945
+
918
946
  private isProtocolV2BootloaderMode() {
919
947
  if (typeof this.device.isBootloader === 'function') {
920
948
  return this.device.isBootloader();
@@ -1017,30 +1045,48 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1017
1045
  }
1018
1046
 
1019
1047
  private async executeProtocolV2Update({
1020
- resourceBinaryMap,
1021
1048
  fwBinaryMap,
1022
1049
  bootloaderBinary,
1023
1050
  installItems,
1051
+ resourceBundles,
1024
1052
  }: {
1025
- resourceBinaryMap?: ProtocolV2TargetBinary[];
1026
1053
  fwBinaryMap?: ProtocolV2TargetBinary[];
1027
1054
  bootloaderBinary?: ArrayBuffer | null;
1028
1055
  installItems?: ProtocolV2InstallItem[];
1056
+ resourceBundles?: ProtocolV2ResourceBundleBinary[];
1029
1057
  }) {
1030
1058
  const orderedInstallItems =
1031
1059
  installItems ??
1032
1060
  this.buildProtocolV2InstallItems({
1033
- resourceBinaryMap: resourceBinaryMap ?? [],
1034
1061
  bootloaderBinary: bootloaderBinary ?? null,
1035
1062
  fwBinaryMap: fwBinaryMap ?? [],
1036
1063
  });
1037
- let totalSize = 0;
1038
- let processedSize = 0;
1039
- let transferredSize = 0;
1040
1064
 
1041
- for (const item of orderedInstallItems) totalSize += item.binary.byteLength;
1065
+ let firmwareSize = 0;
1066
+ for (const item of orderedInstallItems) firmwareSize += item.binary.byteLength;
1042
1067
 
1043
1068
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
1069
+
1070
+ let processedSize = 0;
1071
+ let totalSize = firmwareSize;
1072
+ if (resourceBundles?.length) {
1073
+ const resourceTransfer = await this.syncProtocolV2ResourceBundles(
1074
+ resourceBundles,
1075
+ firmwareSize
1076
+ );
1077
+ processedSize = resourceTransfer.processedSize;
1078
+ totalSize = resourceTransfer.totalSize;
1079
+ }
1080
+
1081
+ // 只有 RESC bundle、没有固件 target 时跳过 staging/install 阶段
1082
+ if (orderedInstallItems.length === 0) {
1083
+ Log.log('[FirmwareUpdateV4] no firmware targets to install (RESC bundles only)');
1084
+ if (totalSize > 0) {
1085
+ this.postProgressMessage(100, 'transferData');
1086
+ }
1087
+ return;
1088
+ }
1089
+ let transferredSize = processedSize;
1044
1090
  const transferStartTime = Date.now();
1045
1091
  const transferTransport = this.getProtocolV2FirmwareTransferTransport();
1046
1092
  const chunkSize = this.getProtocolV2FirmwareChunkSize();
@@ -1067,6 +1113,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1067
1113
  onTransferredBytes,
1068
1114
  });
1069
1115
  transferredSize = processedSize;
1116
+ await this.verifyProtocolV2StagedFile(filePath, item.binary.byteLength);
1070
1117
 
1071
1118
  stagedInstallTargets.push({
1072
1119
  ...item,
@@ -1109,6 +1156,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1109
1156
  return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`;
1110
1157
  }
1111
1158
 
1159
+ private async verifyProtocolV2StagedFile(path: string, expectedSize: number) {
1160
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1161
+ const response = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', { path });
1162
+ const actualSize = toProtocolV2FiniteNumber(response.message?.size);
1163
+ if (!response.message?.exist || response.message?.directory || actualSize !== expectedSize) {
1164
+ throw ERRORS.TypedError(
1165
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
1166
+ `staged file verification failed: path=${path} exist=${!!response.message
1167
+ ?.exist} expected=${expectedSize} actual=${actualSize ?? 'unknown'}`
1168
+ );
1169
+ }
1170
+ }
1171
+
1112
1172
  private async queryProtocolV2FirmwareUpdateStatus() {
1113
1173
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1114
1174
  return typedCall(
@@ -1367,18 +1427,37 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1367
1427
  await this.device.initialize();
1368
1428
  }
1369
1429
 
1370
- private async protocolV2CommonUpdateProcess({
1430
+ private async protocolV2CommonUpdateProcess(params: ProtocolV2FileTransferParams) {
1431
+ let lastError: unknown;
1432
+ for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1433
+ try {
1434
+ return await this.protocolV2WriteWholeFile(params);
1435
+ } catch (error) {
1436
+ lastError = error;
1437
+ Log.error(
1438
+ `Protocol V2 file transfer failed path=${params.filePath} attempt=${attempt}/${PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT}; restarting from offset 0`,
1439
+ error
1440
+ );
1441
+ if (attempt === PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1442
+ break;
1443
+ }
1444
+ await this.recoverProtocolV2FileTransfer();
1445
+ }
1446
+ }
1447
+
1448
+ throw ERRORS.TypedError(
1449
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
1450
+ `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1451
+ );
1452
+ }
1453
+
1454
+ private async protocolV2WriteWholeFile({
1371
1455
  payload,
1372
1456
  filePath,
1373
1457
  processedSize,
1374
1458
  totalSize,
1375
1459
  onTransferredBytes,
1376
- }: PROTO.FirmwareUpload & {
1377
- filePath: string;
1378
- processedSize?: number;
1379
- totalSize?: number;
1380
- onTransferredBytes?: (transferredBytes: number) => void;
1381
- }) {
1460
+ }: ProtocolV2FileTransferParams) {
1382
1461
  const chunkSize = this.getProtocolV2FirmwareChunkSize();
1383
1462
  let offset = 0;
1384
1463
  const getUploadProgress = (fileOffset: number) => {
@@ -1399,7 +1478,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1399
1478
  totalSize ?? payload.byteLength
1400
1479
  );
1401
1480
 
1402
- const writeRes = await this.fileWriteWithRetry(
1481
+ const writeRes = await this.fileWriteChunk(
1403
1482
  filePath,
1404
1483
  payload.byteLength,
1405
1484
  offset,
@@ -1440,7 +1519,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1440
1519
  return env ?? 'unknown';
1441
1520
  }
1442
1521
 
1443
- private async fileWriteWithRetry(
1522
+ private async fileWriteChunk(
1444
1523
  filePath: string,
1445
1524
  totalFileSize: number,
1446
1525
  offset: number,
@@ -1448,57 +1527,37 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1448
1527
  overwrite: boolean,
1449
1528
  progress: number | null
1450
1529
  ): Promise<TypedResponseMessage<'FilesystemFile'>> {
1451
- const writeFunc = async () => {
1452
- const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1453
- const writeRes = await typedCall('FilesystemFileWrite', 'FilesystemFile', {
1454
- file: {
1455
- path: filePath,
1456
- offset,
1457
- total_size: totalFileSize,
1458
- data: chunk,
1459
- },
1460
- overwrite,
1461
- append: false,
1462
- ui_percentage: progress ?? undefined,
1463
- });
1464
- if (writeRes.type !== 'FilesystemFile') {
1465
- if ((writeRes as any).type === 'CallMethodError') {
1466
- if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) {
1467
- throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
1468
- }
1530
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1531
+ const writeRes = await typedCall('FilesystemFileWrite', 'FilesystemFile', {
1532
+ file: {
1533
+ path: filePath,
1534
+ offset,
1535
+ total_size: totalFileSize,
1536
+ data: chunk,
1537
+ },
1538
+ overwrite,
1539
+ append: false,
1540
+ ui_percentage: progress ?? undefined,
1541
+ });
1542
+ if (writeRes.type !== 'FilesystemFile') {
1543
+ if ((writeRes as any).type === 'CallMethodError') {
1544
+ if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) {
1545
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
1469
1546
  }
1470
- throw ERRORS.TypedError(
1471
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1472
- 'transfer data error'
1473
- );
1474
1547
  }
1475
- return writeRes;
1476
- };
1548
+ throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
1549
+ }
1550
+ return writeRes;
1551
+ }
1477
1552
 
1478
- let retryCount = 10;
1479
- while (retryCount > 0) {
1480
- try {
1481
- const result = await writeFunc();
1482
- return result;
1483
- } catch (error) {
1484
- Log.error(`fileWrite error: `, error);
1485
- retryCount--;
1486
- if (retryCount === 0) {
1487
- throw ERRORS.TypedError(
1488
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1489
- 'transfer data error'
1490
- );
1491
- }
1492
- const env = DataManager.getSettings('env');
1493
- if (DataManager.isBleConnect(env)) {
1494
- await wait(3000);
1495
- await this.acquireProtocolV2BleDevice();
1496
- await this.device.initialize();
1497
- }
1498
- await wait(2000);
1499
- }
1553
+ private async recoverProtocolV2FileTransfer() {
1554
+ const env = DataManager.getSettings('env');
1555
+ if (DataManager.isBleConnect(env)) {
1556
+ await wait(3000);
1557
+ await this.acquireProtocolV2BleDevice();
1558
+ await this.device.initialize();
1500
1559
  }
1501
- throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
1560
+ await wait(2000);
1502
1561
  }
1503
1562
 
1504
1563
  private async acquireProtocolV2BleDevice() {