@onekeyfe/hd-core 1.2.0-alpha.7 → 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 +742 -536
  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 +651 -288
  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 +276 -239
  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,83 +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
- return `resource-${safeKey}.bin`;
792
- }
793
-
794
- private async shouldInstallRemoteProtocolV2Component(
712
+ private shouldInstallRemoteProtocolV2Component(
795
713
  release: IFirmwareReleaseInfo,
796
714
  key: string,
797
715
  component: IProtocolV2FirmwareComponent,
798
716
  target: ProtocolV2RemoteComponentTarget,
799
717
  features: Features
800
718
  ) {
801
- if (target.kind === 'resource') {
802
- if (
803
- this.params.forcedUpdateRes ||
804
- features.bootloaderMode ||
805
- features.mode === 'bootloader'
806
- ) {
807
- return true;
808
- }
809
- const resourceMatched = await this.isProtocolV2ResourceManifestSatisfied(
810
- this.getProtocolV2ResourceManifest(release, component)
811
- );
812
- if (resourceMatched) {
813
- Log.log(`[FirmwareUpdateV4] skip Protocol V2 resource component ${key}; manifest matched`);
814
- }
815
- return !resourceMatched;
816
- }
817
-
818
719
  const versionSatisfied = this.isProtocolV2ComponentVersionSatisfied(
819
720
  release,
820
721
  component,
@@ -849,14 +750,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
849
750
  private async prepareRemoteProtocolV2Binaries(firmwareType: EFirmwareType, features: Features) {
850
751
  const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
851
752
 
852
- const resourceBinaryMap: ProtocolV2TargetBinary[] = [];
853
753
  let bootloaderBinary: ArrayBuffer | null = null;
854
754
  const fwBinaryMap: ProtocolV2TargetBinary[] = [];
855
755
  const installItems: ProtocolV2InstallItem[] = [];
856
756
 
857
757
  if (!release) {
858
758
  return {
859
- resourceBinaryMap,
860
759
  bootloaderBinary,
861
760
  fwBinaryMap,
862
761
  installItems,
@@ -867,7 +766,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
867
766
 
868
767
  for (const [key, component] of entries) {
869
768
  const target = this.getRemoteComponentTarget(key, component);
870
- const shouldInstall = await this.shouldInstallRemoteProtocolV2Component(
769
+ const shouldInstall = this.shouldInstallRemoteProtocolV2Component(
871
770
  release,
872
771
  key,
873
772
  component,
@@ -876,15 +775,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
876
775
  );
877
776
  if (shouldInstall) {
878
777
  const remoteBinary = await this.downloadRemoteProtocolV2Component(key, component);
879
- if (remoteBinary.kind === 'resource') {
880
- const binaryEntry = {
881
- fileName: this.getProtocolV2ResourceComponentFileName(key),
882
- binary: remoteBinary.binary,
883
- targetId: remoteBinary.targetId,
884
- };
885
- resourceBinaryMap.push(binaryEntry);
886
- installItems.push({ ...binaryEntry, kind: remoteBinary.kind });
887
- } else if (remoteBinary.kind === 'bootloader') {
778
+ if (remoteBinary.kind === 'bootloader') {
888
779
  bootloaderBinary = remoteBinary.binary;
889
780
  installItems.push({
890
781
  fileName: remoteBinary.fileName,
@@ -905,13 +796,153 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
905
796
  }
906
797
 
907
798
  return {
908
- resourceBinaryMap,
909
799
  bootloaderBinary,
910
800
  fwBinaryMap,
911
801
  installItems,
912
802
  };
913
803
  }
914
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
+
915
946
  private isProtocolV2BootloaderMode() {
916
947
  if (typeof this.device.isBootloader === 'function') {
917
948
  return this.device.isBootloader();
@@ -1014,30 +1045,48 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1014
1045
  }
1015
1046
 
1016
1047
  private async executeProtocolV2Update({
1017
- resourceBinaryMap,
1018
1048
  fwBinaryMap,
1019
1049
  bootloaderBinary,
1020
1050
  installItems,
1051
+ resourceBundles,
1021
1052
  }: {
1022
- resourceBinaryMap?: ProtocolV2TargetBinary[];
1023
1053
  fwBinaryMap?: ProtocolV2TargetBinary[];
1024
1054
  bootloaderBinary?: ArrayBuffer | null;
1025
1055
  installItems?: ProtocolV2InstallItem[];
1056
+ resourceBundles?: ProtocolV2ResourceBundleBinary[];
1026
1057
  }) {
1027
1058
  const orderedInstallItems =
1028
1059
  installItems ??
1029
1060
  this.buildProtocolV2InstallItems({
1030
- resourceBinaryMap: resourceBinaryMap ?? [],
1031
1061
  bootloaderBinary: bootloaderBinary ?? null,
1032
1062
  fwBinaryMap: fwBinaryMap ?? [],
1033
1063
  });
1034
- let totalSize = 0;
1035
- let processedSize = 0;
1036
- let transferredSize = 0;
1037
1064
 
1038
- for (const item of orderedInstallItems) totalSize += item.binary.byteLength;
1065
+ let firmwareSize = 0;
1066
+ for (const item of orderedInstallItems) firmwareSize += item.binary.byteLength;
1039
1067
 
1040
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;
1041
1090
  const transferStartTime = Date.now();
1042
1091
  const transferTransport = this.getProtocolV2FirmwareTransferTransport();
1043
1092
  const chunkSize = this.getProtocolV2FirmwareChunkSize();
@@ -1064,6 +1113,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1064
1113
  onTransferredBytes,
1065
1114
  });
1066
1115
  transferredSize = processedSize;
1116
+ await this.verifyProtocolV2StagedFile(filePath, item.binary.byteLength);
1067
1117
 
1068
1118
  stagedInstallTargets.push({
1069
1119
  ...item,
@@ -1093,44 +1143,32 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1093
1143
 
1094
1144
  this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
1095
1145
 
1096
- const firmwareTargets: Array<{ target_id: number; path: string }> = [];
1097
- const flushFirmwareTargets = async () => {
1098
- if (firmwareTargets.length === 0) return;
1099
- const targets = firmwareTargets.splice(0, firmwareTargets.length);
1100
- Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(targets)}`);
1101
- const startResponse = await this.protocolV2StartFirmwareUpdate({ targets });
1102
- await this.waitForProtocolV2FirmwareUpdateComplete(targets, startResponse);
1103
- };
1104
-
1105
- for (const item of stagedInstallTargets) {
1106
- const target = {
1107
- target_id: item.targetId,
1108
- path: item.path,
1109
- };
1110
- if (item.kind === 'resource') {
1111
- await flushFirmwareTargets();
1112
- const resourceTargets = [target];
1113
- Log.log(
1114
- `[FirmwareUpdateV4] DeviceFirmwareUpdateRequest resources=${JSON.stringify(
1115
- resourceTargets
1116
- )}`
1117
- );
1118
- const startResponse = await this.protocolV2StartFirmwareUpdate({
1119
- targets: resourceTargets,
1120
- });
1121
- await this.waitForProtocolV2FirmwareUpdateComplete(resourceTargets, startResponse);
1122
- } else {
1123
- firmwareTargets.push(target);
1124
- }
1125
- }
1126
-
1127
- await flushFirmwareTargets();
1146
+ const allTargets = stagedInstallTargets.map(item => ({
1147
+ target_id: item.targetId,
1148
+ path: item.path,
1149
+ }));
1150
+ Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(allTargets)}`);
1151
+ const startResponse = await this.protocolV2StartFirmwareUpdate({ targets: allTargets });
1152
+ await this.waitForProtocolV2FirmwareUpdateComplete(allTargets, startResponse);
1128
1153
  }
1129
1154
 
1130
1155
  private getProtocolV2InstallItemStagingPath(item: ProtocolV2InstallItem) {
1131
1156
  return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`;
1132
1157
  }
1133
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
+
1134
1172
  private async queryProtocolV2FirmwareUpdateStatus() {
1135
1173
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1136
1174
  return typedCall(
@@ -1389,18 +1427,37 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1389
1427
  await this.device.initialize();
1390
1428
  }
1391
1429
 
1392
- 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({
1393
1455
  payload,
1394
1456
  filePath,
1395
1457
  processedSize,
1396
1458
  totalSize,
1397
1459
  onTransferredBytes,
1398
- }: PROTO.FirmwareUpload & {
1399
- filePath: string;
1400
- processedSize?: number;
1401
- totalSize?: number;
1402
- onTransferredBytes?: (transferredBytes: number) => void;
1403
- }) {
1460
+ }: ProtocolV2FileTransferParams) {
1404
1461
  const chunkSize = this.getProtocolV2FirmwareChunkSize();
1405
1462
  let offset = 0;
1406
1463
  const getUploadProgress = (fileOffset: number) => {
@@ -1421,7 +1478,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1421
1478
  totalSize ?? payload.byteLength
1422
1479
  );
1423
1480
 
1424
- const writeRes = await this.fileWriteWithRetry(
1481
+ const writeRes = await this.fileWriteChunk(
1425
1482
  filePath,
1426
1483
  payload.byteLength,
1427
1484
  offset,
@@ -1462,7 +1519,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1462
1519
  return env ?? 'unknown';
1463
1520
  }
1464
1521
 
1465
- private async fileWriteWithRetry(
1522
+ private async fileWriteChunk(
1466
1523
  filePath: string,
1467
1524
  totalFileSize: number,
1468
1525
  offset: number,
@@ -1470,57 +1527,37 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1470
1527
  overwrite: boolean,
1471
1528
  progress: number | null
1472
1529
  ): Promise<TypedResponseMessage<'FilesystemFile'>> {
1473
- const writeFunc = async () => {
1474
- const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1475
- const writeRes = await typedCall('FilesystemFileWrite', 'FilesystemFile', {
1476
- file: {
1477
- path: filePath,
1478
- offset,
1479
- total_size: totalFileSize,
1480
- data: chunk,
1481
- },
1482
- overwrite,
1483
- append: false,
1484
- ui_percentage: progress ?? undefined,
1485
- });
1486
- if (writeRes.type !== 'FilesystemFile') {
1487
- if ((writeRes as any).type === 'CallMethodError') {
1488
- if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) {
1489
- throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
1490
- }
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);
1491
1546
  }
1492
- throw ERRORS.TypedError(
1493
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1494
- 'transfer data error'
1495
- );
1496
1547
  }
1497
- return writeRes;
1498
- };
1548
+ throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
1549
+ }
1550
+ return writeRes;
1551
+ }
1499
1552
 
1500
- let retryCount = 10;
1501
- while (retryCount > 0) {
1502
- try {
1503
- const result = await writeFunc();
1504
- return result;
1505
- } catch (error) {
1506
- Log.error(`fileWrite error: `, error);
1507
- retryCount--;
1508
- if (retryCount === 0) {
1509
- throw ERRORS.TypedError(
1510
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1511
- 'transfer data error'
1512
- );
1513
- }
1514
- const env = DataManager.getSettings('env');
1515
- if (DataManager.isBleConnect(env)) {
1516
- await wait(3000);
1517
- await this.acquireProtocolV2BleDevice();
1518
- await this.device.initialize();
1519
- }
1520
- await wait(2000);
1521
- }
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();
1522
1559
  }
1523
- throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
1560
+ await wait(2000);
1524
1561
  }
1525
1562
 
1526
1563
  private async acquireProtocolV2BleDevice() {