Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit aaf5beec authored by Jackeagle's avatar Jackeagle
Browse files

Fix flash reliability: data transfer timing, reconnect, and max-download-size parsing



- Add 250ms settle delay after data transfer before reading device
  response, preventing bootloaders from missing the OKAY acknowledgment
- Yield to browser event loop between USB transfer chunks so Chrome's
  USB stack can process completion events
- Verify bytesWritten on every transferOut to catch partial writes early
- Add post-flash getvar synchronization barrier so the device finishes
  internal processing before the next download begins
- Reset USB device before close during reconnect to abort dangling
  transferIn calls left by timed-out reads
- Fix max-download-size parsing: detect 0x prefix for hex, otherwise
  parse as decimal (matches AOSP fastboot behavior). Qualcomm ABL
  returns decimal strings which were incorrectly parsed as hex,
  inflating the limit from 768 MB to 34 GB and preventing sparse
  image splitting.
- Increase flash cooldown from 1000ms to 2500ms

Signed-off-by: default avatarJackeagle <jackeagle102@gmail.com>
parent 82dfa3c7
Loading
Loading
Loading
Loading
+9 −9
Original line number Diff line number Diff line
@@ -188,7 +188,7 @@ export class Controller {
      case Command.CMD_TYPE.erase:
        return this.deviceManager.erase(cmd.partition);
      case Command.CMD_TYPE.flash: {
        const FLASH_COOLDOWN_MS = 1000; // Pause after flash to let device stabilize
        const FLASH_COOLDOWN_MS = 2500; // Pause after flash to let device stabilize
        const result = await this.deviceManager.flash(
          cmd.file,
          cmd.partition,
+29 −1
Original line number Diff line number Diff line
@@ -210,13 +210,23 @@ export class FastbootDevice {
  /**
   * Get and cache the device's max-download-size.
   * Falls back to 512 MB if the variable is not available.
   *
   * Bootloaders vary in format:
   *   - Qualcomm ABL: decimal string ("805306368" = 768 MB)
   *   - MediaTek/Google: hex with 0x prefix ("0x30000000" = 768 MB)
   * Matches AOSP fastboot's strtoll(str, NULL, 0) behavior: 0x prefix
   * means hex, otherwise decimal.
   */
  private async getMaxDownloadSize(): Promise<number> {
    if (this._maxDownloadSize !== null) return this._maxDownloadSize;

    try {
      const value = await getVar(this._transport, "max-download-size");
      this._maxDownloadSize = parseInt(value, 16) || parseInt(value, 10);
      if (value.startsWith("0x") || value.startsWith("0X")) {
        this._maxDownloadSize = parseInt(value, 16);
      } else {
        this._maxDownloadSize = parseInt(value, 10);
      }
      if (isNaN(this._maxDownloadSize) || this._maxDownloadSize <= 0) {
        this._maxDownloadSize = 512 * 1024 * 1024;
      }
@@ -283,6 +293,7 @@ export class FastbootDevice {
    const data = new Uint8Array(await blob.arrayBuffer());
    await downloadData(this._transport, data, onProgress, FASTBOOT_FLASH_TIMEOUT_MS);
    await flashPartition(this._transport, partition, FASTBOOT_FLASH_TIMEOUT_MS);
    await this.waitDeviceReady();
  }

  /**
@@ -326,5 +337,22 @@ export class FastbootDevice {
          `(${subImageSize} bytes)`,
      );
    }

    await this.waitDeviceReady();
  }

  /**
   * Verify the device is responsive after a flash operation.
   * Some bootloaders (Qualcomm ABL) continue internal processing after
   * sending OKAY for a flash command. Starting the next download before
   * this finishes can cause the device to hang without responding.
   * A quick getvar round-trip acts as a synchronization barrier.
   */
  private async waitDeviceReady(): Promise<void> {
    try {
      await getVar(this._transport, "product", 5000);
    } catch {
      // FAIL or timeout — either way, the device had time to settle
    }
  }
}
+15 −0
Original line number Diff line number Diff line
@@ -94,6 +94,14 @@ export async function sendData(
    await transport.sendWithTimeout(chunk, timeoutMs);
    offset = end;
    onProgress?.(offset, total);

    // Yield to the browser event loop between chunks so Chrome's USB
    // stack can process completion events and hardware ACKs. Without
    // this, back-to-back transferOut calls can starve the USB driver's
    // completion handler, causing the device to miss data.
    if (offset < total) {
      await new Promise((resolve) => setTimeout(resolve, 0));
    }
  }
}

@@ -125,6 +133,13 @@ export async function downloadData(
  // Send the raw data
  await sendData(transport, data, onProgress, 512 * 1024, timeoutMs);

  // Let the device fully process the received data before we issue a
  // USB IN transfer for the OKAY response. Chrome's async transferOut
  // resolves when the host controller accepts the data, but the device
  // may still be DMA-ing the last packets. Issuing transferIn too early
  // can cause some bootloaders (Qualcomm ABL) to miss the response.
  await new Promise((resolve) => setTimeout(resolve, 250));

  // Read final OKAY
  await readResponse(transport, timeoutMs);
}
+16 −1
Original line number Diff line number Diff line
@@ -183,9 +183,19 @@ export class WebUsbTransport {
   */
  async reconnect(settleMs = 2000): Promise<void> {
    log("Reconnecting USB session...");

    // Reset first to abort any pending transferIn/transferOut calls.
    // After a timeout, Promise.race leaves the underlying USB transfer
    // still active, which blocks releaseInterface() during close().
    try {
      await this._device.reset();
    } catch {
      // Reset may fail if device is already disconnected — that's OK
    }

    await this.close();

    // Wait for USB bus to stabilize
    // Wait for USB bus to stabilize after reset
    await new Promise((resolve) => setTimeout(resolve, settleMs));

    // Re-open the connection
@@ -215,6 +225,11 @@ export class WebUsbTransport {
    if (result.status !== "ok") {
      throw new UsbError(`USB transferOut failed: status=${result.status}`);
    }
    if (result.bytesWritten !== undefined && result.bytesWritten !== data.byteLength) {
      throw new UsbError(
        `USB transferOut incomplete: wrote ${result.bytesWritten}/${data.byteLength} bytes`,
      );
    }
  }

  /**