@onekeyfe/hd-core 1.2.8-alpha.13 → 1.2.8-alpha.130

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 (125) hide show
  1. package/__tests__/allNetworkGetAddress-privacy-networks.test.ts +78 -0
  2. package/__tests__/core-dispose.test.ts +114 -1
  3. package/__tests__/core-error-output.test.ts +21 -0
  4. package/__tests__/device-connector-protocol.test.ts +5 -2
  5. package/__tests__/device-lifecycle-events.test.ts +118 -3
  6. package/__tests__/firmware-update/firmware-artifact-source.test.ts +11 -0
  7. package/__tests__/logBlockEvent.test.ts +292 -0
  8. package/__tests__/logger.test.ts +18 -0
  9. package/__tests__/monero-get-address.test.ts +147 -0
  10. package/__tests__/monero-key-image-sync.test.ts +272 -0
  11. package/__tests__/monero-sign-transaction.test.ts +436 -0
  12. package/__tests__/protocol-v2-resources.test.ts +14 -0
  13. package/__tests__/protocol-v2.test.ts +845 -3
  14. package/__tests__/protocolV2FileWrite.test.ts +434 -7
  15. package/__tests__/search-devices.test.ts +36 -0
  16. package/__tests__/usb-identity-probe.test.ts +358 -0
  17. package/__tests__/zcash-protobuf.test.ts +141 -0
  18. package/__tests__/zcash-sign-pczt.test.ts +446 -0
  19. package/dist/api/BaseMethod.d.ts.map +1 -1
  20. package/dist/api/FileWrite.d.ts.map +1 -1
  21. package/dist/api/FirmwareUpdateV4.d.ts +5 -0
  22. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  23. package/dist/api/SearchDevices.d.ts +21 -1
  24. package/dist/api/SearchDevices.d.ts.map +1 -1
  25. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  26. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
  27. package/dist/api/helpers/pathUtils.d.ts +1 -0
  28. package/dist/api/helpers/pathUtils.d.ts.map +1 -1
  29. package/dist/api/helpers/protocolV2FileWrite.d.ts +5 -1
  30. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  31. package/dist/api/index.d.ts +6 -0
  32. package/dist/api/index.d.ts.map +1 -1
  33. package/dist/api/monero/MoneroGetAddress.d.ts +11 -0
  34. package/dist/api/monero/MoneroGetAddress.d.ts.map +1 -0
  35. package/dist/api/monero/MoneroGetWatchKey.d.ts +11 -0
  36. package/dist/api/monero/MoneroGetWatchKey.d.ts.map +1 -0
  37. package/dist/api/monero/MoneroKeyImageSync.d.ts +20 -0
  38. package/dist/api/monero/MoneroKeyImageSync.d.ts.map +1 -0
  39. package/dist/api/monero/MoneroSignTransaction.d.ts +17 -0
  40. package/dist/api/monero/MoneroSignTransaction.d.ts.map +1 -0
  41. package/dist/api/monero/helpers.d.ts +5 -0
  42. package/dist/api/monero/helpers.d.ts.map +1 -0
  43. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  44. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  45. package/dist/api/zcash/ZcashGetAddress.d.ts +12 -0
  46. package/dist/api/zcash/ZcashGetAddress.d.ts.map +1 -0
  47. package/dist/api/zcash/ZcashSignPczt.d.ts +16 -0
  48. package/dist/api/zcash/ZcashSignPczt.d.ts.map +1 -0
  49. package/dist/core/index.d.ts +3 -1
  50. package/dist/core/index.d.ts.map +1 -1
  51. package/dist/device/Device.d.ts +1 -0
  52. package/dist/device/Device.d.ts.map +1 -1
  53. package/dist/device/DeviceCommands.d.ts +4 -4
  54. package/dist/events/logBlockEvent.d.ts +10 -0
  55. package/dist/events/logBlockEvent.d.ts.map +1 -1
  56. package/dist/events/ui-request.d.ts +2 -0
  57. package/dist/events/ui-request.d.ts.map +1 -1
  58. package/dist/index.d.ts +183 -7
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +5625 -4192
  61. package/dist/inject.d.ts.map +1 -1
  62. package/dist/protocols/protocol-v2/resources.d.ts +1 -0
  63. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  64. package/dist/types/api/allNetworkGetAddress.d.ts +5 -1
  65. package/dist/types/api/allNetworkGetAddress.d.ts.map +1 -1
  66. package/dist/types/api/export.d.ts +6 -0
  67. package/dist/types/api/export.d.ts.map +1 -1
  68. package/dist/types/api/index.d.ts +12 -0
  69. package/dist/types/api/index.d.ts.map +1 -1
  70. package/dist/types/api/moneroGetAddress.d.ts +15 -0
  71. package/dist/types/api/moneroGetAddress.d.ts.map +1 -0
  72. package/dist/types/api/moneroGetWatchKey.d.ts +15 -0
  73. package/dist/types/api/moneroGetWatchKey.d.ts.map +1 -0
  74. package/dist/types/api/moneroKeyImageSync.d.ts +24 -0
  75. package/dist/types/api/moneroKeyImageSync.d.ts.map +1 -0
  76. package/dist/types/api/moneroSignTransaction.d.ts +32 -0
  77. package/dist/types/api/moneroSignTransaction.d.ts.map +1 -0
  78. package/dist/types/api/zcashGetAddress.d.ts +22 -0
  79. package/dist/types/api/zcashGetAddress.d.ts.map +1 -0
  80. package/dist/types/api/zcashSignPczt.d.ts +9 -0
  81. package/dist/types/api/zcashSignPczt.d.ts.map +1 -0
  82. package/dist/types/device.d.ts +10 -0
  83. package/dist/types/device.d.ts.map +1 -1
  84. package/dist/utils/logger.d.ts.map +1 -1
  85. package/dist/utils/patch.d.ts +1 -1
  86. package/dist/utils/patch.d.ts.map +1 -1
  87. package/package.json +4 -4
  88. package/src/api/BaseMethod.ts +2 -0
  89. package/src/api/FileWrite.ts +7 -1
  90. package/src/api/FirmwareUpdateV4.ts +358 -74
  91. package/src/api/SearchDevices.ts +172 -7
  92. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +38 -1
  93. package/src/api/firmware/FirmwareArtifactSource.ts +12 -3
  94. package/src/api/helpers/pathUtils.ts +23 -0
  95. package/src/api/helpers/protocolV2FileWrite.ts +375 -111
  96. package/src/api/index.ts +7 -0
  97. package/src/api/monero/MoneroGetAddress.ts +71 -0
  98. package/src/api/monero/MoneroGetWatchKey.ts +69 -0
  99. package/src/api/monero/MoneroKeyImageSync.ts +164 -0
  100. package/src/api/monero/MoneroSignTransaction.ts +319 -0
  101. package/src/api/monero/helpers.ts +28 -0
  102. package/src/api/protocol-v2/DeviceUploadNft.ts +6 -1
  103. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +15 -1
  104. package/src/api/zcash/ZcashGetAddress.ts +119 -0
  105. package/src/api/zcash/ZcashSignPczt.ts +152 -0
  106. package/src/core/index.ts +49 -17
  107. package/src/data/messages/messages-protocol-v2.json +138 -1
  108. package/src/device/Device.ts +21 -3
  109. package/src/device/DeviceConnector.ts +4 -4
  110. package/src/events/logBlockEvent.ts +55 -3
  111. package/src/events/ui-request.ts +2 -0
  112. package/src/index.ts +1 -0
  113. package/src/inject.ts +13 -0
  114. package/src/protocols/protocol-v2/resources.ts +6 -1
  115. package/src/types/api/allNetworkGetAddress.ts +9 -1
  116. package/src/types/api/export.ts +15 -0
  117. package/src/types/api/index.ts +20 -0
  118. package/src/types/api/moneroGetAddress.ts +27 -0
  119. package/src/types/api/moneroGetWatchKey.ts +28 -0
  120. package/src/types/api/moneroKeyImageSync.ts +45 -0
  121. package/src/types/api/moneroSignTransaction.ts +54 -0
  122. package/src/types/api/zcashGetAddress.ts +35 -0
  123. package/src/types/api/zcashSignPczt.ts +18 -0
  124. package/src/types/device.ts +18 -0
  125. package/src/utils/logger.ts +4 -0
@@ -1,11 +1,13 @@
1
1
  import { EDeviceType, ERRORS, HardwareError, HardwareErrorCode, wait } from '@onekeyfe/hd-shared';
2
2
  import JSZip from 'jszip';
3
+ import semver from 'semver';
3
4
  import {
4
5
  DeviceRebootType,
5
6
  DeviceSessionPinType,
6
7
  PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
7
8
  PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE,
8
9
  PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE,
10
+ PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW,
9
11
  PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
10
12
  } from '@onekeyfe/hd-transport';
11
13
  import { sha256 } from '@noble/hashes/sha256';
@@ -34,6 +36,7 @@ import { requestProtocolV2DeviceInfo } from '../protocols/protocol-v2/features';
34
36
  import {
35
37
  PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH,
36
38
  PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH,
39
+ PROTOCOL_V2_RESOURCE_PACKAGE_COMPARE_SIZE,
37
40
  PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE,
38
41
  type ProtocolV2ResourcePackageHeader,
39
42
  isProtocolV2ResourceArchiveEntryName,
@@ -75,6 +78,7 @@ import type {
75
78
  import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
76
79
 
77
80
  const Log = getLogger(LoggerNames.Method);
81
+ const TransferLog = getLogger(LoggerNames.Core);
78
82
 
79
83
  // Restored after a rebase dropped the declaration while keeping its use in
80
84
  // fileWriteChunk; without it that error branch throws ReferenceError instead of
@@ -101,7 +105,16 @@ const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
101
105
  const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
102
106
  const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
103
107
  const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
108
+ const PROTOCOL_V2_RESOURCE_HEADER_RECOVERY_LIMIT = 2;
109
+ // Minimum running bootloader that receives two in-flight BLE FilesystemFileWrite frames.
110
+ // Bootloaders with the 2048-byte BLE UART FIFO have no room beside one waiting FileWrite frame,
111
+ // so a second frame can overflow it. Released bootloaders report 1.0.2 with that FIFO and stay
112
+ // serial. Firmware has been asked to ship the 4096-byte FIFO from 1.0.3 but has not confirmed it,
113
+ // nor the BLE coprocessor margin for two frames; any bootloader reporting 1.0.3 or later gets the
114
+ // window. Set it to undefined to keep every transfer serial.
115
+ const PROTOCOL_V2_BLE_FILE_WRITE_WINDOW_MIN_BOOTLOADER_VERSION: string | undefined = '1.0.3';
104
116
  const PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS = 1000;
117
+ const PROTOCOL_V2_TRANSFER_LOG_INTERVAL_MS = 10_000;
105
118
  const PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE = 'FirmwareInstallStatusConflict';
106
119
  const PROTOCOL_V2_INSTALL_FAILED_CODE = 'FirmwareInstallFailed';
107
120
  const PROTOCOL_V2_INSTALL_TIMEOUT_CODE = 'FirmwareInstallTimeout';
@@ -170,6 +183,18 @@ export function assertProtocolV2FirmwareTargetsSupported(
170
183
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotSupportMethod, message);
171
184
  }
172
185
 
186
+ /** Whether the running bootloader accepts a second in-flight BLE FilesystemFileWrite. */
187
+ export function supportsProtocolV2BleFileWriteWindow(
188
+ bootloaderVersion: string,
189
+ minBootloaderVersion = PROTOCOL_V2_BLE_FILE_WRITE_WINDOW_MIN_BOOTLOADER_VERSION
190
+ ) {
191
+ return (
192
+ !!minBootloaderVersion &&
193
+ !!semver.valid(bootloaderVersion) &&
194
+ semver.gte(bootloaderVersion, minBootloaderVersion)
195
+ );
196
+ }
197
+
173
198
  const getProtocolV2DeviceTransferProgress = (
174
199
  bytesBeforeChunk: number,
175
200
  bytesAfterChunk: number,
@@ -184,6 +209,12 @@ const getProtocolV2DeviceTransferProgress = (
184
209
  return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
185
210
  };
186
211
 
212
+ const getProtocolV2TransferRate = (bytes: number, elapsedMs: number) =>
213
+ elapsedMs > 0 ? Math.round((bytes / elapsedMs) * 1000) : undefined;
214
+
215
+ const getProtocolV2ElapsedMs = (startedAt: number, now = Date.now()) =>
216
+ Math.max(now - startedAt, 0);
217
+
187
218
  type ProtocolV2FirmwareUpdateStatusTarget = {
188
219
  target_id: number | string;
189
220
  status?: number | string;
@@ -352,13 +383,10 @@ const PROTOCOL_V2_TARGET_STATUS_BY_DECODED_NAME = new Map<string, number>([
352
383
  ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_ENTRY_OUT_OF_BOUNDS', 10],
353
384
  ]);
354
385
 
355
- const isProtocolV2ReconnectProbeError = (error: unknown) => {
356
- const message = getProtocolV2UnknownErrorText(error).toLowerCase();
357
- return (
358
- (message.includes('device protocol mismatch') && message.includes('expected v2')) ||
359
- message.includes('did not respond to expected protocol')
360
- );
361
- };
386
+ const isProtocolV2ReconnectProbeError = (error: unknown) =>
387
+ getProtocolV2ErrorCode(error) === HardwareErrorCode.TransportProtocolProbeFailed &&
388
+ (error as { params?: { expectedProtocol?: unknown } } | undefined)?.params?.expectedProtocol ===
389
+ 'V2';
362
390
 
363
391
  const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
364
392
  const message = getProtocolV2UnknownErrorText(error).toLowerCase();
@@ -385,6 +413,23 @@ const getProtocolV2ErrorCode = (error: unknown): number | string | undefined =>
385
413
  return typeof causeCode === 'number' || typeof causeCode === 'string' ? causeCode : undefined;
386
414
  };
387
415
 
416
+ /** Errors that mean the link is gone, so the read says nothing about the installed bundle. */
417
+ const isProtocolV2ResourceHeaderLinkError = (error: unknown) => {
418
+ if (isProtocolV2DeviceDisconnectedError(error)) return true;
419
+ const errorCode = getProtocolV2ErrorCode(error);
420
+ if (errorCode === HardwareErrorCode.BleTimeoutError) return true;
421
+ const message = getProtocolV2UnknownErrorText(error);
422
+ if (errorCode === HardwareErrorCode.TransportNotFound && /not connected/i.test(message)) {
423
+ return true;
424
+ }
425
+ return (
426
+ message.includes('ProtocolV2LinkError') ||
427
+ message.includes('Device protocol has not been detected') ||
428
+ message.includes('Electron BLE device state cleaned') ||
429
+ message.includes('Electron BLE transport released')
430
+ );
431
+ };
432
+
388
433
  const isProtocolV2TerminalInstallStatusError = (error: unknown) =>
389
434
  error instanceof HardwareError &&
390
435
  (error.errorCode === HardwareErrorCode.ActionCancelled ||
@@ -589,6 +634,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
589
634
 
590
635
  private protocolV2LastTransferredBytes = 0;
591
636
 
637
+ private protocolV2FileWriteWindowDisabled = false;
638
+
592
639
  init() {
593
640
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
594
641
  this.requireDeviceMode = [];
@@ -778,7 +825,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
778
825
  };
779
826
  }
780
827
 
781
- private getProtocolV2FirmwareChunkSize(direction: 'read' | 'write', filePath?: string) {
828
+ private getProtocolV2FirmwareChunkSize(
829
+ direction: 'read' | 'write',
830
+ filePath?: string,
831
+ fileWriteWindow = false
832
+ ) {
782
833
  const payloadChunkSize = Number(this.params?.chunkSize);
783
834
  const env = DataManager.getSettings('env');
784
835
  const isBle = this.params?.platform === 'native' || (env && DataManager.isBleConnect(env));
@@ -786,6 +837,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
786
837
  if (isBle) {
787
838
  if (direction === 'read') {
788
839
  maxChunkSize = PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE;
840
+ } else if (fileWriteWindow) {
841
+ // Skip the larger staging chunk so two in-flight frames fit the UART FIFO; see
842
+ // PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW.
843
+ maxChunkSize = PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
789
844
  } else {
790
845
  // Firmware staging writes tolerate a larger BLE chunk than generic filesystem writes.
791
846
  maxChunkSize = PROTOCOL_V2_FIRMWARE_STAGING_PATHS.has(filePath ?? '')
@@ -1815,8 +1870,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1815
1870
  const chunkSize = this.getProtocolV2FirmwareChunkSize('read');
1816
1871
  const chunks: Uint8Array[] = [];
1817
1872
  let offset = 0;
1818
- while (offset < PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE) {
1819
- const readLen = Math.min(chunkSize, PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE - offset);
1873
+ while (offset < PROTOCOL_V2_RESOURCE_PACKAGE_COMPARE_SIZE) {
1874
+ const readLen = Math.min(chunkSize, PROTOCOL_V2_RESOURCE_PACKAGE_COMPARE_SIZE - offset);
1820
1875
  const res = await typedCall('FilesystemFileRead', 'FilesystemFile', {
1821
1876
  file: {
1822
1877
  path: filePath,
@@ -1827,7 +1882,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1827
1882
  ui_percentage: undefined,
1828
1883
  });
1829
1884
  const data = toProtocolV2Bytes(res.message?.data);
1830
- if (data.byteLength === 0) return null;
1885
+ if (data.byteLength === 0 || data.byteLength > readLen) return null;
1831
1886
  chunks.push(data);
1832
1887
  offset += data.byteLength;
1833
1888
  }
@@ -1872,11 +1927,47 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1872
1927
  if (!expectedHeaderHash || header.headerHash !== expectedHeaderHash) return false;
1873
1928
  return true;
1874
1929
  } catch (error) {
1930
+ if (isProtocolV2ResourceHeaderLinkError(error)) throw error;
1875
1931
  Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
1876
1932
  return false;
1877
1933
  }
1878
1934
  }
1879
1935
 
1936
+ /**
1937
+ * A lost link would fail every remaining header check and resend bundles that are already
1938
+ * installed, so reconnect (a bounded number of times per transfer phase) and ask again.
1939
+ * Header reads have no side effects. Once the budget is used up the bundle is resent as before.
1940
+ * A failed reconnect aborts the update like a failed file-transfer recovery: reconnecting
1941
+ * replaces the session, and nothing may be written to a device whose identity was not
1942
+ * re-verified.
1943
+ */
1944
+ private async isProtocolV2ResourceBundleUpToDateWithRecovery(
1945
+ bundle: ProtocolV2ResourceBundleSource,
1946
+ recovery: { remaining: number }
1947
+ ): Promise<boolean> {
1948
+ for (;;) {
1949
+ try {
1950
+ return await this.isProtocolV2ResourceBundleUpToDate(bundle);
1951
+ } catch (error) {
1952
+ this.throwIfAborted();
1953
+ if (recovery.remaining <= 0) {
1954
+ Log.log(
1955
+ `[FirmwareUpdateV4] RESC bundle ${bundle.name} header check lost the link; reconnect budget used up: `,
1956
+ getProtocolV2UnknownErrorText(error)
1957
+ );
1958
+ return false;
1959
+ }
1960
+ recovery.remaining -= 1;
1961
+ Log.log(
1962
+ `[FirmwareUpdateV4] RESC bundle ${bundle.name} header check lost the link; reconnecting: `,
1963
+ getProtocolV2UnknownErrorText(error)
1964
+ );
1965
+ }
1966
+ await this.recoverProtocolV2FileTransfer();
1967
+ this.throwIfAborted();
1968
+ }
1969
+ }
1970
+
1880
1971
  private isProtocolV2BootloaderMode() {
1881
1972
  if (typeof this.device.isBootloader === 'function') {
1882
1973
  return !!this.device.isBootloader();
@@ -1906,6 +1997,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1906
1997
  this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader);
1907
1998
  await this.protocolV2Reboot(DeviceRebootType.Bootloader);
1908
1999
  await wait(1000);
2000
+ if (this.isBleReconnect()) {
2001
+ await this.device.deviceConnector?.disconnect(this.device.originalDescriptor.id);
2002
+ }
1909
2003
  await this.waitForProtocolV2BootloaderMode();
1910
2004
  this.protocolV2ExecutionInLoader = true;
1911
2005
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
@@ -1948,12 +2042,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1948
2042
  while (Date.now() - startTime < timeout) {
1949
2043
  try {
1950
2044
  if (shouldReconnect) {
1951
- await this.reconnectProtocolV2Device();
2045
+ if (this.isBleReconnect()) {
2046
+ await this.reconnectProtocolV2Device({ skipProtocolProbe: true });
2047
+ } else {
2048
+ await this.reconnectProtocolV2Device();
2049
+ }
1952
2050
  shouldReconnect = false;
1953
2051
  }
1954
2052
  const deviceInfo = await requestProtocolV2DeviceInfo({
1955
2053
  commands: this.device.getCommands(),
1956
- timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
2054
+ timeoutMs: Math.min(
2055
+ this.getProtocolV2DeviceInfoTimeout(),
2056
+ PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2057
+ ),
1957
2058
  });
1958
2059
  this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
1959
2060
  const features = await this.device.probeProtocolV2RuntimeState(
@@ -2115,8 +2216,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2115
2216
  }: ProtocolV2TransferBatch) {
2116
2217
  let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
2117
2218
  const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
2219
+ const headerRecovery = { remaining: PROTOCOL_V2_RESOURCE_HEADER_RECOVERY_LIMIT };
2118
2220
  for (const resource of resourceSources) {
2119
- if (await this.isProtocolV2ResourceBundleUpToDate(resource)) {
2221
+ if (await this.isProtocolV2ResourceBundleUpToDateWithRecovery(resource, headerRecovery)) {
2120
2222
  Log.log(`[FirmwareUpdateV4] skip RESC bundle ${resource.name}; already up to date`);
2121
2223
  } else {
2122
2224
  resourcesToSync.push(resource);
@@ -2165,13 +2267,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2165
2267
  }
2166
2268
 
2167
2269
  if (totalSize > 0) {
2168
- const elapsedMs = Math.max(Date.now() - transferStartedAt, 0);
2270
+ const elapsedMs = getProtocolV2ElapsedMs(transferStartedAt);
2271
+ const rateBytesPerSecond = getProtocolV2TransferRate(totalSize, elapsedMs);
2169
2272
  this.postProgressMessage(100, 'transferData', {
2170
2273
  transferredBytes: totalSize,
2171
2274
  totalBytes: totalSize,
2172
- rateBytesPerSecond: elapsedMs > 0 ? Math.round((totalSize / elapsedMs) * 1000) : undefined,
2275
+ rateBytesPerSecond,
2173
2276
  elapsedMs,
2174
2277
  });
2278
+ Log.debug('[FirmwareUpdateV4] Protocol V2 transfer completed', {
2279
+ files: resourcesToSync.length + installSources.length,
2280
+ totalBytes: totalSize,
2281
+ elapsedMs,
2282
+ rateBytesPerSecond,
2283
+ });
2175
2284
  }
2176
2285
  if (stagedInstallTargets.length === 0) {
2177
2286
  return;
@@ -2182,10 +2291,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2182
2291
  target_id: item.targetId,
2183
2292
  path: item.path,
2184
2293
  }));
2294
+ // Includes the wait for on-device confirmation.
2295
+ const installStartedAt = Date.now();
2185
2296
  try {
2186
2297
  await this.protocolV2StartFirmwareUpdate({ targets });
2187
2298
  await wait(PROTOCOL_V2_INSTALL_STATUS_INITIAL_DELAY);
2188
2299
  await this.waitForProtocolV2FirmwareUpdateComplete(targets, true);
2300
+ Log.debug('[FirmwareUpdateV4] Protocol V2 install completed', {
2301
+ targets: targets.length,
2302
+ elapsedMs: getProtocolV2ElapsedMs(installStartedAt),
2303
+ });
2189
2304
  } finally {
2190
2305
  this.device?.clearCancelableAction();
2191
2306
  }
@@ -2204,66 +2319,121 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2204
2319
  totalSize: number;
2205
2320
  transferStartedAt?: number;
2206
2321
  }) {
2322
+ const reportConfirmedChunk = (chunkEnd: number) => {
2323
+ const transferredBytes = processedSize + chunkEnd;
2324
+ const now = Date.now();
2325
+ const elapsedMs = getProtocolV2ElapsedMs(transferStartedAt, now);
2326
+ const progress = Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99);
2327
+ const shouldPostProgress =
2328
+ transferredBytes >= this.protocolV2LastTransferredBytes &&
2329
+ (progress !== this.protocolV2LastTransferProgress ||
2330
+ now - this.protocolV2LastTransferProgressAt >=
2331
+ PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS ||
2332
+ chunkEnd === source.size);
2333
+ if (shouldPostProgress) {
2334
+ this.protocolV2LastTransferProgress = progress;
2335
+ this.protocolV2LastTransferProgressAt = now;
2336
+ this.protocolV2LastTransferredBytes = transferredBytes;
2337
+ this.postProgressMessage(progress, 'transferData', {
2338
+ transferredBytes,
2339
+ totalBytes: totalSize,
2340
+ rateBytesPerSecond: getProtocolV2TransferRate(transferredBytes, elapsedMs),
2341
+ elapsedMs,
2342
+ });
2343
+ }
2344
+ };
2207
2345
  let lastError: unknown;
2208
2346
  for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
2209
2347
  this.throwIfAborted();
2348
+ const attemptStartedAt = Date.now();
2349
+ let windowed = false;
2350
+ let chunkSize = 0;
2351
+ let confirmedBytes = 0;
2352
+ let lastMetricsAt = attemptStartedAt;
2353
+ const logTransfer = (status: 'started' | 'progress' | 'completed' | 'failed') => {
2354
+ const elapsedMs = getProtocolV2ElapsedMs(attemptStartedAt);
2355
+ TransferLog.log('[FirmwareUpdateV4] FileWrite metrics', {
2356
+ status,
2357
+ path: filePath,
2358
+ fileWriteWindow: windowed ? PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW : 1,
2359
+ chunkSize,
2360
+ attempt,
2361
+ retryCount: attempt - 1,
2362
+ transferredBytes: confirmedBytes,
2363
+ totalBytes: source.size,
2364
+ elapsedMs,
2365
+ rateBytesPerSecond: getProtocolV2TransferRate(confirmedBytes, elapsedMs),
2366
+ });
2367
+ };
2368
+ const reportAttemptChunk = (chunkEnd: number) => {
2369
+ confirmedBytes = chunkEnd;
2370
+ reportConfirmedChunk(chunkEnd);
2371
+ const now = Date.now();
2372
+ if (now - lastMetricsAt >= PROTOCOL_V2_TRANSFER_LOG_INTERVAL_MS && chunkEnd < source.size) {
2373
+ logTransfer('progress');
2374
+ lastMetricsAt = now;
2375
+ }
2376
+ };
2210
2377
  try {
2211
- await writeFirmwareByteSource({
2212
- source,
2213
- chunkSize: this.getProtocolV2FirmwareChunkSize('write', filePath),
2214
- write: async ({ data, sourceOffset, length, first }) => {
2215
- this.throwIfAborted();
2216
- const chunkEnd = sourceOffset + length;
2217
- const deviceProgress = getProtocolV2DeviceTransferProgress(
2218
- processedSize + sourceOffset,
2219
- processedSize + chunkEnd,
2220
- totalSize
2221
- );
2222
- const response = await this.fileWriteChunk(
2223
- filePath,
2224
- source.size,
2225
- sourceOffset,
2226
- data,
2227
- first,
2228
- deviceProgress
2229
- );
2230
- const rawProcessedByte = response.message.processed_byte;
2231
- const nextOffset = rawProcessedByte === undefined ? chunkEnd : Number(rawProcessedByte);
2232
- if (!Number.isFinite(nextOffset) || nextOffset !== chunkEnd) {
2233
- throw ERRORS.TypedError(
2234
- HardwareErrorCode.EmmcFileWriteFirmwareError,
2235
- `invalid processed_byte ${rawProcessedByte} for offset ${sourceOffset}`
2378
+ windowed =
2379
+ !this.protocolV2FileWriteWindowDisabled && this.shouldUseProtocolV2BleFileWriteWindow();
2380
+ chunkSize = this.getProtocolV2FirmwareChunkSize('write', filePath, windowed);
2381
+ logTransfer('started');
2382
+ if (windowed) {
2383
+ await this.writeProtocolV2SourceWindowed({
2384
+ source,
2385
+ filePath,
2386
+ chunkSize,
2387
+ processedSize,
2388
+ totalSize,
2389
+ onChunkConfirmed: reportAttemptChunk,
2390
+ });
2391
+ } else {
2392
+ await writeFirmwareByteSource({
2393
+ source,
2394
+ chunkSize,
2395
+ write: async ({ data, sourceOffset, length, first }) => {
2396
+ this.throwIfAborted();
2397
+ const chunkEnd = sourceOffset + length;
2398
+ const deviceProgress = getProtocolV2DeviceTransferProgress(
2399
+ processedSize + sourceOffset,
2400
+ processedSize + chunkEnd,
2401
+ totalSize
2236
2402
  );
2237
- }
2238
- const transferredBytes = processedSize + chunkEnd;
2239
- const now = Date.now();
2240
- const elapsedMs = Math.max(now - transferStartedAt, 0);
2241
- const progress = Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99);
2242
- const shouldPostProgress =
2243
- transferredBytes >= this.protocolV2LastTransferredBytes &&
2244
- (progress !== this.protocolV2LastTransferProgress ||
2245
- now - this.protocolV2LastTransferProgressAt >=
2246
- PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS ||
2247
- chunkEnd === source.size);
2248
- if (shouldPostProgress) {
2249
- this.protocolV2LastTransferProgress = progress;
2250
- this.protocolV2LastTransferProgressAt = now;
2251
- this.protocolV2LastTransferredBytes = transferredBytes;
2252
- this.postProgressMessage(progress, 'transferData', {
2253
- transferredBytes,
2254
- totalBytes: totalSize,
2255
- rateBytesPerSecond:
2256
- elapsedMs > 0 ? Math.round((transferredBytes / elapsedMs) * 1000) : undefined,
2257
- elapsedMs,
2258
- });
2259
- }
2260
- return length;
2261
- },
2262
- });
2403
+ const response = await this.fileWriteChunk(
2404
+ filePath,
2405
+ source.size,
2406
+ sourceOffset,
2407
+ data,
2408
+ first,
2409
+ deviceProgress
2410
+ );
2411
+ const rawProcessedByte = response.message.processed_byte;
2412
+ const nextOffset =
2413
+ rawProcessedByte === undefined ? chunkEnd : Number(rawProcessedByte);
2414
+ if (!Number.isFinite(nextOffset) || nextOffset !== chunkEnd) {
2415
+ throw ERRORS.TypedError(
2416
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
2417
+ `invalid processed_byte ${rawProcessedByte} for offset ${sourceOffset}`
2418
+ );
2419
+ }
2420
+ reportAttemptChunk(chunkEnd);
2421
+ return length;
2422
+ },
2423
+ });
2424
+ }
2425
+ logTransfer('completed');
2263
2426
  return processedSize + source.size;
2264
2427
  } catch (error) {
2265
2428
  this.throwIfAborted();
2266
2429
  lastError = error;
2430
+ logTransfer('failed');
2431
+ TransferLog.debug('[FirmwareUpdateV4] Protocol V2 file transfer attempt failed', {
2432
+ path: filePath,
2433
+ attempt,
2434
+ fileWriteWindow: windowed ? PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW : 1,
2435
+ error: getProtocolV2UnknownErrorText(error),
2436
+ });
2267
2437
  if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
2268
2438
  await this.recoverProtocolV2FileTransfer();
2269
2439
  this.throwIfAborted();
@@ -3195,15 +3365,129 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3195
3365
  return writeRes;
3196
3366
  }
3197
3367
 
3198
- private async recoverProtocolV2FileTransfer() {
3368
+ private shouldUseProtocolV2BleFileWriteWindow(
3369
+ minBootloaderVersion = PROTOCOL_V2_BLE_FILE_WRITE_WINDOW_MIN_BOOTLOADER_VERSION
3370
+ ) {
3199
3371
  const env = DataManager.getSettings('env');
3200
- if (DataManager.isBleConnect(env)) {
3201
- await wait(3000);
3372
+ // These adapters queue frames that arrive while no read is pending. LowLevel plugins are not
3373
+ // required to keep notifications between receive() calls, so they stay serial.
3374
+ if (
3375
+ (env !== 'react-native' && env !== 'desktop-web-ble') ||
3376
+ !this.isProtocolV2BootloaderMode()
3377
+ ) {
3378
+ return false;
3379
+ }
3380
+ // The transfer runs in the device's current bootloader, not the one being installed. The
3381
+ // parsed version keeps major.minor.patch only, so a 1.0.3 prerelease counts as 1.0.3.
3382
+ const bootloaderVersion = getDeviceBootloaderVersion(this.device.features).join('.');
3383
+ const enabled = supportsProtocolV2BleFileWriteWindow(bootloaderVersion, minBootloaderVersion);
3384
+ TransferLog.debug('[FirmwareUpdateV4] BLE FileWrite window', {
3385
+ enabled,
3386
+ bootloaderVersion,
3387
+ bootloaderBuildId: this.device.features?.verify?.bootloaderBuildId,
3388
+ });
3389
+ return enabled;
3390
+ }
3391
+
3392
+ /**
3393
+ * Streams one staged file as a single ordered FilesystemFileWrite pipeline. Every chunk is
3394
+ * still confirmed by the device; a failure restarts the whole file like the serial path.
3395
+ * Cancellation and source errors only stop the pipeline and are raised after the transport
3396
+ * call returns: an error thrown inside it would invalidate the BLE link.
3397
+ */
3398
+ private async writeProtocolV2SourceWindowed({
3399
+ source,
3400
+ filePath,
3401
+ chunkSize,
3402
+ processedSize,
3403
+ totalSize,
3404
+ onChunkConfirmed,
3405
+ }: {
3406
+ source: FirmwareByteSource;
3407
+ filePath: string;
3408
+ chunkSize: number;
3409
+ processedSize: number;
3410
+ totalSize: number;
3411
+ onChunkConfirmed: (chunkEnd: number) => void;
3412
+ }) {
3413
+ let sendOffset = 0;
3414
+ let sourceError: Error | undefined;
3415
+ const readRequest = async () => {
3416
+ const offset = sendOffset;
3417
+ const length = Math.min(chunkSize, source.size - offset);
3418
+ const data = await source.readAt(offset, length);
3419
+ sendOffset += length;
3420
+ return {
3421
+ file: { path: filePath, offset, total_size: source.size, data },
3422
+ overwrite: offset === 0,
3423
+ append: false,
3424
+ ui_percentage: getProtocolV2DeviceTransferProgress(
3425
+ processedSize + offset,
3426
+ processedSize + sendOffset,
3427
+ totalSize
3428
+ ),
3429
+ };
3430
+ };
3431
+ const nextRequest = async () => {
3432
+ if (this.abortSignal?.aborted || sendOffset >= source.size) return undefined;
3433
+ try {
3434
+ return await readRequest();
3435
+ } catch (error) {
3436
+ sourceError = error;
3437
+ return undefined;
3438
+ }
3439
+ };
3440
+ this.throwIfAborted();
3441
+ if (source.size === 0) return;
3442
+ const firstRequest = await readRequest();
3443
+
3444
+ // A failed exchange disables the window for the rest of this update. Source read
3445
+ // failures are raised separately and do not change the bootloader capability decision.
3446
+ const response = await this.device
3447
+ .getCommands()
3448
+ .typedCall('FilesystemFileWrite', 'FilesystemFile', firstRequest, {
3449
+ // Both windowed transports (React Native and Electron BLE) invalidate a timed-out link,
3450
+ // so a lost chunk is retried after 10 s instead of the 5-minute default.
3451
+ timeoutMs: PROTOCOL_V2_FILE_WRITE_RESPONSE_TIMEOUT,
3452
+ writeWithResponse: false,
3453
+ fileWritePipeline: {
3454
+ next: nextRequest,
3455
+ onResponse: confirmed => {
3456
+ onChunkConfirmed(Number(confirmed.message.processed_byte));
3457
+ },
3458
+ },
3459
+ })
3460
+ .catch((error: unknown) => {
3461
+ this.protocolV2FileWriteWindowDisabled = true;
3462
+ throw error;
3463
+ });
3464
+ this.throwIfAborted();
3465
+ if (sourceError) throw sourceError;
3466
+ const rawProcessedByte = response.message.processed_byte;
3467
+ if (Number(rawProcessedByte) !== source.size) {
3468
+ this.protocolV2FileWriteWindowDisabled = true;
3469
+ throw ERRORS.TypedError(
3470
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
3471
+ `invalid processed_byte ${rawProcessedByte} for file size ${source.size}`
3472
+ );
3473
+ }
3474
+ }
3475
+
3476
+ private async recoverProtocolV2FileTransfer() {
3477
+ try {
3478
+ const env = DataManager.getSettings('env');
3479
+ if (DataManager.isBleConnect(env)) {
3480
+ await wait(3000);
3481
+ }
3482
+ await this.reconnectProtocolV2Device();
3483
+ await this.verifyProtocolV2ReconnectIdentity();
3484
+ await this.device.initialize();
3485
+ await wait(2000);
3486
+ } catch (error) {
3487
+ // A cancel disposes the link mid-recovery; report the cancel rather than its side effect.
3488
+ this.throwIfAborted();
3489
+ throw error;
3202
3490
  }
3203
- await this.reconnectProtocolV2Device();
3204
- await this.verifyProtocolV2ReconnectIdentity();
3205
- await this.device.initialize();
3206
- await wait(2000);
3207
3491
  }
3208
3492
 
3209
3493
  private async acquireProtocolV2BleDevice(skipProtocolProbe = false) {