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

Commit 16ec63da authored by Daniel Jacob Chittoor's avatar Daniel Jacob Chittoor Committed by Jackeagle
Browse files

Wait for USB enumeration before bootloader connect step

After rebooting to bootloader mode, some USB host controllers (notably
AMD Ryzen) are slow to re-enumerate Mediatek devices. Add a
waitForDeviceOnBus() helper that listens for the navigator.usb connect
event before presenting the step UI, so the device is on the bus by
the time the user clicks and triggers requestDevice().

Add detailed debug logging across the bootloader connect flow:
paired device VID/PID before each attempt, elapsed time on
success/failure, USB bus wait event details, and resilient
resetDevice() error handling.
parent 370ba173
Loading
Loading
Loading
Loading
+25 −5
Original line number Diff line number Diff line
@@ -39,20 +39,40 @@ export class Controller {

    if (next) {
      if (next.mode) {
        const alreadyInMode = this.inInMode(next.mode);
        WDebug.log(
          `next() step="${next.name}" requires mode="${next.mode}", ` +
            `alreadyInMode=${alreadyInMode}, needUserGesture=${next.needUserGesture}`,
        );
        //if next step require another mode [adb|fastboot|bootloader]
        if (!this.inInMode(next.mode)) {
        if (!alreadyInMode) {
          //we need reboot
          WDebug.log(`next() rebooting to ${next.mode}...`);
          await this.deviceManager.reboot(next.mode);
        }
        // Skip connect if the step requires a user gesture, since WebUSB
        // requestDevice() can only be called from a user-initiated event.
        // The connect will happen via executeStep when the user clicks.
        if (!next.needUserGesture) {
          WDebug.log(`next() reboot to ${next.mode} completed`);
        }
        if (next.needUserGesture) {
          // Wait for the device to appear on the USB bus before showing the
          // step. Some host controllers (AMD Ryzen) are slow to re-enumerate
          // devices after a mode switch. The actual connect happens via
          // executeStep when the user clicks (WebUSB requestDevice() requires
          // a user gesture).
          WDebug.log(
            `next() waiting for device on USB bus (needUserGesture=true, deferring connect)...`,
          );
          await this.deviceManager.waitForDeviceOnBus();
          WDebug.log(`next() device wait complete, showing step to user`);
        } else {
          WDebug.log(`next() connecting to ${next.mode} automatically...`);
          await this.deviceManager.connect(next.mode);
          WDebug.log(`next() auto-connect to ${next.mode} completed`);
        }
      }
      this.currentIndex++;
      current = this.steps[this.currentIndex];
      WDebug.log(
        `next() advancing to step="${current.name}", needUserGesture=${current.needUserGesture}`,
      );
      this.view.onStepStarted(this.currentIndex, current);
      if (!current.needUserGesture) {
        await this.executeStep(current.name);
+55 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ import { Downloader } from "./downloader.manager.js";
import { ADB } from "./device/adb.class.js";
import { Recovery } from "./device/recovery.class.js";
import { Device } from "./device/device.class.js";
import { WDebug } from "../debug.js";
const MODE = {
  adb: "adb",
  recovery: "recovery",
@@ -192,6 +193,60 @@ export class DeviceManager {
    }
  }

  /**
   * Wait for a USB device to appear on the bus.
   * Some USB host controllers (e.g., AMD Ryzen) are slow to re-enumerate
   * devices after a mode switch, especially Mediatek bootloader devices.
   * Resolves when a device appears or after timeout (does not reject).
   */
  waitForDeviceOnBus(timeoutMs = 30000) {
    const startTime = Date.now();
    return new Promise((resolve) => {
      const devices = navigator.usb.getDevices();
      devices.then((list) => {
        WDebug.log(
          `waitForDeviceOnBus: getDevices() returned ${list.length} device(s)`,
          list.map((d) => `${d.vendorId}:${d.productId} "${d.productName}"`),
        );
        if (list.length > 0) {
          WDebug.log("waitForDeviceOnBus: device already visible, no wait needed");
          resolve();
          return;
        }

        WDebug.log(
          `waitForDeviceOnBus: no devices found, listening for USB connect event (timeout=${timeoutMs}ms)...`,
        );

        const timeout = setTimeout(() => {
          navigator.usb.removeEventListener("connect", onConnect);
          const elapsed = Date.now() - startTime;
          WDebug.log(
            `waitForDeviceOnBus: timeout after ${elapsed}ms, no device appeared. Proceeding anyway.`,
          );
          resolve();
        }, timeoutMs);

        const onConnect = (event) => {
          const elapsed = Date.now() - startTime;
          const d = event.device;
          WDebug.log(
            `waitForDeviceOnBus: USB connect event after ${elapsed}ms - ` +
              `vendorId=${d.vendorId} productId=${d.productId} ` +
              `productName="${d.productName}" serialNumber="${d.serialNumber}"`,
          );
          clearTimeout(timeout);
          navigator.usb.removeEventListener("connect", onConnect);
          // Small delay to let the device fully initialize after enumeration
          WDebug.log("waitForDeviceOnBus: waiting 1000ms for device to stabilize...");
          setTimeout(resolve, 1000);
        };

        navigator.usb.addEventListener("connect", onConnect);
      });
    });
  }

  async downloadAll(onProgress, onUnzip, onVerify) {
    try {
      if (this.downloader.hasLocalZip()) {
+32 −6
Original line number Diff line number Diff line
@@ -42,21 +42,38 @@ export class Bootloader extends Device {
  async connect() {
    const MAX_CONNECT_ATTEMPTS = 3;
    const CONNECT_RETRY_DELAY = 2000; // 2 seconds
    const connectStart = Date.now();

    WDebug.log(
      `Bootloader.connect() starting, maxAttempts=${MAX_CONNECT_ATTEMPTS}, ` +
        `retryDelay=${CONNECT_RETRY_DELAY}ms, ` +
        `device.isConnected=${this.device.isConnected}`,
    );

    for (let attempt = 1; attempt <= MAX_CONNECT_ATTEMPTS; attempt++) {
      try {
        // Log paired devices before each attempt for debugging
        const pairedDevices = await navigator.usb.getDevices();
        WDebug.log(
          `Connecting to bootloader (attempt ${attempt}/${MAX_CONNECT_ATTEMPTS})...`,
          `Bootloader.connect() attempt ${attempt}/${MAX_CONNECT_ATTEMPTS}: ` +
            `${pairedDevices.length} paired USB device(s)`,
          pairedDevices.map(
            (d) => `${d.vendorId}:${d.productId} "${d.productName}"`,
          ),
        );

        await this.device.connect();

        const elapsed = Date.now() - connectStart;
        WDebug.log(
          `Successfully connected to bootloader on attempt ${attempt}`,
          `Bootloader.connect() succeeded on attempt ${attempt} after ${elapsed}ms`,
        );
        return;
      } catch (e) {
        const errorMsg = e.message || String(e);
        const elapsed = Date.now() - connectStart;
        WDebug.log(
          `Bootloader connection attempt ${attempt} failed: ${errorMsg}`,
          `Bootloader.connect() attempt ${attempt} failed after ${elapsed}ms: ${errorMsg}`,
        );

        // If this is the last attempt, throw the error
@@ -71,13 +88,22 @@ export class Bootloader extends Device {

        // Wait before retry, with increasing delay
        const delay = CONNECT_RETRY_DELAY * attempt;
        WDebug.log(`Waiting ${delay}ms before retry...`);
        WDebug.log(
          `Bootloader.connect() waiting ${delay}ms before attempt ${attempt + 1}...`,
        );
        await new Promise((resolve) => setTimeout(resolve, delay));

        // Try to reset USB device to clear stale state
        if (typeof this.device.resetDevice === 'function') {
          WDebug.log("Attempting USB device reset before reconnect...");
          WDebug.log("Bootloader.connect() attempting USB device reset...");
          try {
            await this.device.resetDevice();
            WDebug.log("Bootloader.connect() USB device reset succeeded");
          } catch (resetErr) {
            WDebug.log(
              `Bootloader.connect() USB device reset failed: ${resetErr.message || resetErr}`,
            );
          }
        }
      }
    }