@onekeyfe/hd-core 1.2.8-alpha.121 → 1.2.8-alpha.122

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.
@@ -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';
@@ -101,6 +103,14 @@ const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
101
103
  const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
102
104
  const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
103
105
  const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
106
+ const PROTOCOL_V2_RESOURCE_HEADER_RECOVERY_LIMIT = 2;
107
+ // Minimum running bootloader that receives two in-flight BLE FilesystemFileWrite frames.
108
+ // Bootloaders with the 2048-byte BLE UART FIFO have no room beside one waiting FileWrite frame,
109
+ // so a second frame can overflow it. Released bootloaders report 1.0.2 with that FIFO and stay
110
+ // serial. Firmware has been asked to ship the 4096-byte FIFO from 1.0.3 but has not confirmed it,
111
+ // nor the BLE coprocessor margin for two frames; any bootloader reporting 1.0.3 or later gets the
112
+ // window. Set it to undefined to keep every transfer serial.
113
+ const PROTOCOL_V2_BLE_FILE_WRITE_WINDOW_MIN_BOOTLOADER_VERSION: string | undefined = '1.0.3';
104
114
  const PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS = 1000;
105
115
  const PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE = 'FirmwareInstallStatusConflict';
106
116
  const PROTOCOL_V2_INSTALL_FAILED_CODE = 'FirmwareInstallFailed';
@@ -170,6 +180,18 @@ export function assertProtocolV2FirmwareTargetsSupported(
170
180
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotSupportMethod, message);
171
181
  }
172
182
 
183
+ /** Whether the running bootloader accepts a second in-flight BLE FilesystemFileWrite. */
184
+ export function supportsProtocolV2BleFileWriteWindow(
185
+ bootloaderVersion: string,
186
+ minBootloaderVersion = PROTOCOL_V2_BLE_FILE_WRITE_WINDOW_MIN_BOOTLOADER_VERSION
187
+ ) {
188
+ return (
189
+ !!minBootloaderVersion &&
190
+ !!semver.valid(bootloaderVersion) &&
191
+ semver.gte(bootloaderVersion, minBootloaderVersion)
192
+ );
193
+ }
194
+
173
195
  const getProtocolV2DeviceTransferProgress = (
174
196
  bytesBeforeChunk: number,
175
197
  bytesAfterChunk: number,
@@ -184,6 +206,12 @@ const getProtocolV2DeviceTransferProgress = (
184
206
  return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
185
207
  };
186
208
 
209
+ const getProtocolV2TransferRate = (bytes: number, elapsedMs: number) =>
210
+ elapsedMs > 0 ? Math.round((bytes / elapsedMs) * 1000) : undefined;
211
+
212
+ const getProtocolV2ElapsedMs = (startedAt: number, now = Date.now()) =>
213
+ Math.max(now - startedAt, 0);
214
+
187
215
  type ProtocolV2FirmwareUpdateStatusTarget = {
188
216
  target_id: number | string;
189
217
  status?: number | string;
@@ -382,6 +410,23 @@ const getProtocolV2ErrorCode = (error: unknown): number | string | undefined =>
382
410
  return typeof causeCode === 'number' || typeof causeCode === 'string' ? causeCode : undefined;
383
411
  };
384
412
 
413
+ /** Errors that mean the link is gone, so the read says nothing about the installed bundle. */
414
+ const isProtocolV2ResourceHeaderLinkError = (error: unknown) => {
415
+ if (isProtocolV2DeviceDisconnectedError(error)) return true;
416
+ const errorCode = getProtocolV2ErrorCode(error);
417
+ if (errorCode === HardwareErrorCode.BleTimeoutError) return true;
418
+ const message = getProtocolV2UnknownErrorText(error);
419
+ if (errorCode === HardwareErrorCode.TransportNotFound && /not connected/i.test(message)) {
420
+ return true;
421
+ }
422
+ return (
423
+ message.includes('ProtocolV2LinkError') ||
424
+ message.includes('Device protocol has not been detected') ||
425
+ message.includes('Electron BLE device state cleaned') ||
426
+ message.includes('Electron BLE transport released')
427
+ );
428
+ };
429
+
385
430
  const isProtocolV2TerminalInstallStatusError = (error: unknown) =>
386
431
  error instanceof HardwareError &&
387
432
  (error.errorCode === HardwareErrorCode.ActionCancelled ||
@@ -586,6 +631,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
586
631
 
587
632
  private protocolV2LastTransferredBytes = 0;
588
633
 
634
+ /** Set when a windowed FileWrite attempt fails: the rest of this update writes serially. */
635
+ private protocolV2FileWriteWindowDisabled = false;
636
+
589
637
  init() {
590
638
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
591
639
  this.requireDeviceMode = [];
@@ -775,7 +823,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
775
823
  };
776
824
  }
777
825
 
778
- private getProtocolV2FirmwareChunkSize(direction: 'read' | 'write', filePath?: string) {
826
+ private getProtocolV2FirmwareChunkSize(
827
+ direction: 'read' | 'write',
828
+ filePath?: string,
829
+ fileWriteWindow = false
830
+ ) {
779
831
  const payloadChunkSize = Number(this.params?.chunkSize);
780
832
  const env = DataManager.getSettings('env');
781
833
  const isBle = this.params?.platform === 'native' || (env && DataManager.isBleConnect(env));
@@ -783,6 +835,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
783
835
  if (isBle) {
784
836
  if (direction === 'read') {
785
837
  maxChunkSize = PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE;
838
+ } else if (fileWriteWindow) {
839
+ // Skip the larger staging chunk so two in-flight frames fit the UART FIFO; see
840
+ // PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW.
841
+ maxChunkSize = PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
786
842
  } else {
787
843
  // Firmware staging writes tolerate a larger BLE chunk than generic filesystem writes.
788
844
  maxChunkSize = PROTOCOL_V2_FIRMWARE_STAGING_PATHS.has(filePath ?? '')
@@ -1869,11 +1925,47 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1869
1925
  if (!expectedHeaderHash || header.headerHash !== expectedHeaderHash) return false;
1870
1926
  return true;
1871
1927
  } catch (error) {
1928
+ if (isProtocolV2ResourceHeaderLinkError(error)) throw error;
1872
1929
  Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
1873
1930
  return false;
1874
1931
  }
1875
1932
  }
1876
1933
 
1934
+ /**
1935
+ * A lost link would fail every remaining header check and resend bundles that are already
1936
+ * installed, so reconnect (a bounded number of times per transfer phase) and ask again.
1937
+ * Header reads have no side effects. Once the budget is used up the bundle is resent as before.
1938
+ * A failed reconnect aborts the update like a failed file-transfer recovery: reconnecting
1939
+ * replaces the session, and nothing may be written to a device whose identity was not
1940
+ * re-verified.
1941
+ */
1942
+ private async isProtocolV2ResourceBundleUpToDateWithRecovery(
1943
+ bundle: ProtocolV2ResourceBundleSource,
1944
+ recovery: { remaining: number }
1945
+ ): Promise<boolean> {
1946
+ for (;;) {
1947
+ try {
1948
+ return await this.isProtocolV2ResourceBundleUpToDate(bundle);
1949
+ } catch (error) {
1950
+ this.throwIfAborted();
1951
+ if (recovery.remaining <= 0) {
1952
+ Log.log(
1953
+ `[FirmwareUpdateV4] RESC bundle ${bundle.name} header check lost the link; reconnect budget used up: `,
1954
+ getProtocolV2UnknownErrorText(error)
1955
+ );
1956
+ return false;
1957
+ }
1958
+ recovery.remaining -= 1;
1959
+ Log.log(
1960
+ `[FirmwareUpdateV4] RESC bundle ${bundle.name} header check lost the link; reconnecting: `,
1961
+ getProtocolV2UnknownErrorText(error)
1962
+ );
1963
+ }
1964
+ await this.recoverProtocolV2FileTransfer();
1965
+ this.throwIfAborted();
1966
+ }
1967
+ }
1968
+
1877
1969
  private isProtocolV2BootloaderMode() {
1878
1970
  if (typeof this.device.isBootloader === 'function') {
1879
1971
  return !!this.device.isBootloader();
@@ -2112,8 +2204,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2112
2204
  }: ProtocolV2TransferBatch) {
2113
2205
  let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
2114
2206
  const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
2207
+ const headerRecovery = { remaining: PROTOCOL_V2_RESOURCE_HEADER_RECOVERY_LIMIT };
2115
2208
  for (const resource of resourceSources) {
2116
- if (await this.isProtocolV2ResourceBundleUpToDate(resource)) {
2209
+ if (await this.isProtocolV2ResourceBundleUpToDateWithRecovery(resource, headerRecovery)) {
2117
2210
  Log.log(`[FirmwareUpdateV4] skip RESC bundle ${resource.name}; already up to date`);
2118
2211
  } else {
2119
2212
  resourcesToSync.push(resource);
@@ -2162,12 +2255,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2162
2255
  }
2163
2256
 
2164
2257
  if (totalSize > 0) {
2165
- const elapsedMs = Math.max(Date.now() - transferStartedAt, 0);
2258
+ const elapsedMs = getProtocolV2ElapsedMs(transferStartedAt);
2259
+ const rateBytesPerSecond = getProtocolV2TransferRate(totalSize, elapsedMs);
2166
2260
  this.postProgressMessage(100, 'transferData', {
2167
2261
  transferredBytes: totalSize,
2168
2262
  totalBytes: totalSize,
2169
- rateBytesPerSecond: elapsedMs > 0 ? Math.round((totalSize / elapsedMs) * 1000) : undefined,
2263
+ rateBytesPerSecond,
2264
+ elapsedMs,
2265
+ });
2266
+ Log.debug('[FirmwareUpdateV4] Protocol V2 transfer completed', {
2267
+ files: resourcesToSync.length + installSources.length,
2268
+ totalBytes: totalSize,
2170
2269
  elapsedMs,
2270
+ rateBytesPerSecond,
2171
2271
  });
2172
2272
  }
2173
2273
  if (stagedInstallTargets.length === 0) {
@@ -2179,10 +2279,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2179
2279
  target_id: item.targetId,
2180
2280
  path: item.path,
2181
2281
  }));
2282
+ // Includes the wait for on-device confirmation.
2283
+ const installStartedAt = Date.now();
2182
2284
  try {
2183
2285
  await this.protocolV2StartFirmwareUpdate({ targets });
2184
2286
  await wait(PROTOCOL_V2_INSTALL_STATUS_INITIAL_DELAY);
2185
2287
  await this.waitForProtocolV2FirmwareUpdateComplete(targets, true);
2288
+ Log.debug('[FirmwareUpdateV4] Protocol V2 install completed', {
2289
+ targets: targets.length,
2290
+ elapsedMs: getProtocolV2ElapsedMs(installStartedAt),
2291
+ });
2186
2292
  } finally {
2187
2293
  this.device?.clearCancelableAction();
2188
2294
  }
@@ -2201,66 +2307,98 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2201
2307
  totalSize: number;
2202
2308
  transferStartedAt?: number;
2203
2309
  }) {
2310
+ const reportConfirmedChunk = (chunkEnd: number) => {
2311
+ const transferredBytes = processedSize + chunkEnd;
2312
+ const now = Date.now();
2313
+ const elapsedMs = getProtocolV2ElapsedMs(transferStartedAt, now);
2314
+ const progress = Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99);
2315
+ const shouldPostProgress =
2316
+ transferredBytes >= this.protocolV2LastTransferredBytes &&
2317
+ (progress !== this.protocolV2LastTransferProgress ||
2318
+ now - this.protocolV2LastTransferProgressAt >=
2319
+ PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS ||
2320
+ chunkEnd === source.size);
2321
+ if (shouldPostProgress) {
2322
+ this.protocolV2LastTransferProgress = progress;
2323
+ this.protocolV2LastTransferProgressAt = now;
2324
+ this.protocolV2LastTransferredBytes = transferredBytes;
2325
+ this.postProgressMessage(progress, 'transferData', {
2326
+ transferredBytes,
2327
+ totalBytes: totalSize,
2328
+ rateBytesPerSecond: getProtocolV2TransferRate(transferredBytes, elapsedMs),
2329
+ elapsedMs,
2330
+ });
2331
+ }
2332
+ };
2204
2333
  let lastError: unknown;
2205
2334
  for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
2206
2335
  this.throwIfAborted();
2336
+ const attemptStartedAt = Date.now();
2337
+ let windowed = false;
2207
2338
  try {
2208
- await writeFirmwareByteSource({
2209
- source,
2210
- chunkSize: this.getProtocolV2FirmwareChunkSize('write', filePath),
2211
- write: async ({ data, sourceOffset, length, first }) => {
2212
- this.throwIfAborted();
2213
- const chunkEnd = sourceOffset + length;
2214
- const deviceProgress = getProtocolV2DeviceTransferProgress(
2215
- processedSize + sourceOffset,
2216
- processedSize + chunkEnd,
2217
- totalSize
2218
- );
2219
- const response = await this.fileWriteChunk(
2220
- filePath,
2221
- source.size,
2222
- sourceOffset,
2223
- data,
2224
- first,
2225
- deviceProgress
2226
- );
2227
- const rawProcessedByte = response.message.processed_byte;
2228
- const nextOffset = rawProcessedByte === undefined ? chunkEnd : Number(rawProcessedByte);
2229
- if (!Number.isFinite(nextOffset) || nextOffset !== chunkEnd) {
2230
- throw ERRORS.TypedError(
2231
- HardwareErrorCode.EmmcFileWriteFirmwareError,
2232
- `invalid processed_byte ${rawProcessedByte} for offset ${sourceOffset}`
2339
+ windowed =
2340
+ !this.protocolV2FileWriteWindowDisabled && this.shouldUseProtocolV2BleFileWriteWindow();
2341
+ const chunkSize = this.getProtocolV2FirmwareChunkSize('write', filePath, windowed);
2342
+ if (windowed) {
2343
+ await this.writeProtocolV2SourceWindowed({
2344
+ source,
2345
+ filePath,
2346
+ chunkSize,
2347
+ processedSize,
2348
+ totalSize,
2349
+ onChunkConfirmed: reportConfirmedChunk,
2350
+ });
2351
+ } else {
2352
+ await writeFirmwareByteSource({
2353
+ source,
2354
+ chunkSize,
2355
+ write: async ({ data, sourceOffset, length, first }) => {
2356
+ this.throwIfAborted();
2357
+ const chunkEnd = sourceOffset + length;
2358
+ const deviceProgress = getProtocolV2DeviceTransferProgress(
2359
+ processedSize + sourceOffset,
2360
+ processedSize + chunkEnd,
2361
+ totalSize
2233
2362
  );
2234
- }
2235
- const transferredBytes = processedSize + chunkEnd;
2236
- const now = Date.now();
2237
- const elapsedMs = Math.max(now - transferStartedAt, 0);
2238
- const progress = Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99);
2239
- const shouldPostProgress =
2240
- transferredBytes >= this.protocolV2LastTransferredBytes &&
2241
- (progress !== this.protocolV2LastTransferProgress ||
2242
- now - this.protocolV2LastTransferProgressAt >=
2243
- PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS ||
2244
- chunkEnd === source.size);
2245
- if (shouldPostProgress) {
2246
- this.protocolV2LastTransferProgress = progress;
2247
- this.protocolV2LastTransferProgressAt = now;
2248
- this.protocolV2LastTransferredBytes = transferredBytes;
2249
- this.postProgressMessage(progress, 'transferData', {
2250
- transferredBytes,
2251
- totalBytes: totalSize,
2252
- rateBytesPerSecond:
2253
- elapsedMs > 0 ? Math.round((transferredBytes / elapsedMs) * 1000) : undefined,
2254
- elapsedMs,
2255
- });
2256
- }
2257
- return length;
2258
- },
2363
+ const response = await this.fileWriteChunk(
2364
+ filePath,
2365
+ source.size,
2366
+ sourceOffset,
2367
+ data,
2368
+ first,
2369
+ deviceProgress
2370
+ );
2371
+ const rawProcessedByte = response.message.processed_byte;
2372
+ const nextOffset =
2373
+ rawProcessedByte === undefined ? chunkEnd : Number(rawProcessedByte);
2374
+ if (!Number.isFinite(nextOffset) || nextOffset !== chunkEnd) {
2375
+ throw ERRORS.TypedError(
2376
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
2377
+ `invalid processed_byte ${rawProcessedByte} for offset ${sourceOffset}`
2378
+ );
2379
+ }
2380
+ reportConfirmedChunk(chunkEnd);
2381
+ return length;
2382
+ },
2383
+ });
2384
+ }
2385
+ Log.debug('[FirmwareUpdateV4] Protocol V2 file transferred', {
2386
+ path: filePath,
2387
+ bytes: source.size,
2388
+ elapsedMs: getProtocolV2ElapsedMs(attemptStartedAt),
2389
+ attempt,
2390
+ fileWriteWindow: windowed ? PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW : 1,
2259
2391
  });
2260
2392
  return processedSize + source.size;
2261
2393
  } catch (error) {
2262
2394
  this.throwIfAborted();
2263
2395
  lastError = error;
2396
+ Log.debug('[FirmwareUpdateV4] Protocol V2 file transfer attempt failed', {
2397
+ path: filePath,
2398
+ attempt,
2399
+ fileWriteWindow: windowed ? PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW : 1,
2400
+ error: getProtocolV2UnknownErrorText(error),
2401
+ });
2264
2402
  if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
2265
2403
  await this.recoverProtocolV2FileTransfer();
2266
2404
  this.throwIfAborted();
@@ -3192,15 +3330,130 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3192
3330
  return writeRes;
3193
3331
  }
3194
3332
 
3195
- private async recoverProtocolV2FileTransfer() {
3333
+ private shouldUseProtocolV2BleFileWriteWindow(
3334
+ minBootloaderVersion = PROTOCOL_V2_BLE_FILE_WRITE_WINDOW_MIN_BOOTLOADER_VERSION
3335
+ ) {
3196
3336
  const env = DataManager.getSettings('env');
3197
- if (DataManager.isBleConnect(env)) {
3198
- await wait(3000);
3337
+ // These adapters queue frames that arrive while no read is pending. LowLevel plugins are not
3338
+ // required to keep notifications between receive() calls, so they stay serial.
3339
+ if (
3340
+ (env !== 'react-native' && env !== 'desktop-web-ble') ||
3341
+ !this.isProtocolV2BootloaderMode()
3342
+ ) {
3343
+ return false;
3344
+ }
3345
+ // The transfer runs in the device's current bootloader, not the one being installed. The
3346
+ // parsed version keeps major.minor.patch only, so a 1.0.3 prerelease counts as 1.0.3.
3347
+ const bootloaderVersion = getDeviceBootloaderVersion(this.device.features).join('.');
3348
+ const enabled = supportsProtocolV2BleFileWriteWindow(bootloaderVersion, minBootloaderVersion);
3349
+ Log.debug('[FirmwareUpdateV4] BLE FileWrite window', {
3350
+ enabled,
3351
+ bootloaderVersion,
3352
+ bootloaderBuildId: this.device.features?.verify?.bootloaderBuildId,
3353
+ });
3354
+ return enabled;
3355
+ }
3356
+
3357
+ /**
3358
+ * Streams one staged file as a single ordered FilesystemFileWrite pipeline. Every chunk is
3359
+ * still confirmed by the device; a failure restarts the whole file like the serial path.
3360
+ * Cancellation and source errors only stop the pipeline and are raised after the transport
3361
+ * call returns: an error thrown inside it would invalidate the BLE link.
3362
+ */
3363
+ private async writeProtocolV2SourceWindowed({
3364
+ source,
3365
+ filePath,
3366
+ chunkSize,
3367
+ processedSize,
3368
+ totalSize,
3369
+ onChunkConfirmed,
3370
+ }: {
3371
+ source: FirmwareByteSource;
3372
+ filePath: string;
3373
+ chunkSize: number;
3374
+ processedSize: number;
3375
+ totalSize: number;
3376
+ onChunkConfirmed: (chunkEnd: number) => void;
3377
+ }) {
3378
+ let sendOffset = 0;
3379
+ let sourceError: Error | undefined;
3380
+ const readRequest = async () => {
3381
+ const offset = sendOffset;
3382
+ const length = Math.min(chunkSize, source.size - offset);
3383
+ const data = await source.readAt(offset, length);
3384
+ sendOffset += length;
3385
+ return {
3386
+ file: { path: filePath, offset, total_size: source.size, data },
3387
+ overwrite: offset === 0,
3388
+ append: false,
3389
+ ui_percentage: getProtocolV2DeviceTransferProgress(
3390
+ processedSize + offset,
3391
+ processedSize + sendOffset,
3392
+ totalSize
3393
+ ),
3394
+ };
3395
+ };
3396
+ const nextRequest = async () => {
3397
+ if (this.abortSignal?.aborted || sendOffset >= source.size) return undefined;
3398
+ try {
3399
+ return await readRequest();
3400
+ } catch (error) {
3401
+ sourceError = error;
3402
+ return undefined;
3403
+ }
3404
+ };
3405
+ this.throwIfAborted();
3406
+ if (source.size === 0) return;
3407
+ const firstRequest = await readRequest();
3408
+
3409
+ // A failed exchange may mean the running bootloader cannot hold two in-flight frames, so the
3410
+ // retry and every later file of this update are written serially. Source read errors say
3411
+ // nothing about the bootloader and keep the window.
3412
+ const response = await this.device
3413
+ .getCommands()
3414
+ .typedCall('FilesystemFileWrite', 'FilesystemFile', firstRequest, {
3415
+ // Both windowed transports (React Native and Electron BLE) invalidate a timed-out link,
3416
+ // so a lost chunk is retried after 10 s instead of the 5-minute default.
3417
+ timeoutMs: PROTOCOL_V2_FILE_WRITE_RESPONSE_TIMEOUT,
3418
+ writeWithResponse: false,
3419
+ fileWritePipeline: {
3420
+ next: nextRequest,
3421
+ onResponse: confirmed => {
3422
+ onChunkConfirmed(Number(confirmed.message.processed_byte));
3423
+ },
3424
+ },
3425
+ })
3426
+ .catch((error: unknown) => {
3427
+ this.protocolV2FileWriteWindowDisabled = true;
3428
+ throw error;
3429
+ });
3430
+ this.throwIfAborted();
3431
+ if (sourceError) throw sourceError;
3432
+ const rawProcessedByte = response.message.processed_byte;
3433
+ if (Number(rawProcessedByte) !== source.size) {
3434
+ this.protocolV2FileWriteWindowDisabled = true;
3435
+ throw ERRORS.TypedError(
3436
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
3437
+ `invalid processed_byte ${rawProcessedByte} for file size ${source.size}`
3438
+ );
3439
+ }
3440
+ }
3441
+
3442
+ private async recoverProtocolV2FileTransfer() {
3443
+ try {
3444
+ const env = DataManager.getSettings('env');
3445
+ if (DataManager.isBleConnect(env)) {
3446
+ await wait(3000);
3447
+ }
3448
+ await this.reconnectProtocolV2Device();
3449
+ await this.verifyProtocolV2ReconnectIdentity();
3450
+ await this.device.initialize();
3451
+ await wait(2000);
3452
+ } catch (error) {
3453
+ // A cancel disposes the link mid-recovery; report the cancel rather than its side effect.
3454
+ this.throwIfAborted();
3455
+ throw error;
3199
3456
  }
3200
- await this.reconnectProtocolV2Device();
3201
- await this.verifyProtocolV2ReconnectIdentity();
3202
- await this.device.initialize();
3203
- await wait(2000);
3204
3457
  }
3205
3458
 
3206
3459
  private async acquireProtocolV2BleDevice(skipProtocolProbe = false) {
@@ -78,6 +78,10 @@ class Log {
78
78
  const _logs: { [k: string]: Log } = {};
79
79
 
80
80
  export const initLog = (prefix: string, enabled?: boolean) => {
81
+ // Loggers that share a prefix must share one instance; otherwise enableLog only reaches the
82
+ // last one registered and the others never print.
83
+ const existing = _logs[prefix];
84
+ if (existing) return existing;
81
85
  const instance = new Log(prefix, !!enabled);
82
86
  _logs[prefix] = instance;
83
87
  return instance;