From b20514037bae559968b500e5e0f7f6f172bddb1a Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 12:56:52 +0530 Subject: [PATCH 01/16] Fix typo in format command handler The format command handler calls this.deviceManager.for() which does not exist, causing TypeError at runtime. The method should be format(). --- app/src/controller.manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/controller.manager.js b/app/src/controller.manager.js index bfe32a7..5d408d3 100644 --- a/app/src/controller.manager.js +++ b/app/src/controller.manager.js @@ -251,7 +251,7 @@ export class Controller { } case Command.CMD_TYPE.format: try { - this.deviceManager.for(cmd.partition); + this.deviceManager.format(cmd.partition); } catch (e) { console.error(e); // K1ZFP TODO } -- GitLab From c363b4e27efd9ecd68e263ec918e196fa4c0b89f Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 12:59:17 +0530 Subject: [PATCH 02/16] Fix infinite recursion in `flashBlob` on timeout flashBlob() recursively calls itself on TimeoutError without any retry limit. If the device consistently times out, this leads to stack overflow or indefinite hangs. Add a retry counter with a maximum of 3 attempts. After exhausting retries, throw an error with a descriptive message indicating the partition and retry count. --- app/src/controller/device/bootloader.class.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/controller/device/bootloader.class.js b/app/src/controller/device/bootloader.class.js index 5c34fd3..cde9d48 100644 --- a/app/src/controller/device/bootloader.class.js +++ b/app/src/controller/device/bootloader.class.js @@ -72,7 +72,8 @@ export class Bootloader extends Device { ); } - async flashBlob(partition, blob, onProgress) { + async flashBlob(partition, blob, onProgress, retryCount = 0) { + const MAX_RETRIES = 3; try { await this.device.flashBlob(partition, blob, (progress) => { onProgress(progress * blob.size, blob.size, partition); @@ -81,8 +82,11 @@ export class Bootloader extends Device { return true; } catch (e) { if (e instanceof TimeoutError) { - WDebug.log("Timeout on flashblob >" + partition); - return await this.flashBlob(partition, blob, onProgress); + WDebug.log(`Timeout on flashblob > ${partition} (attempt ${retryCount + 1}/${MAX_RETRIES})`); + if (retryCount < MAX_RETRIES) { + return await this.flashBlob(partition, blob, onProgress, retryCount + 1); + } + throw new Error(`Bootloader timeout: flashing ${partition} failed after ${MAX_RETRIES} retries`); } else { console.log("flashBlob error", e); throw new Error(`Bootloader error: ${e.message || e}`); -- GitLab From 62fa878ed081fdade36c573037952bd020220062 Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 13:05:18 +0530 Subject: [PATCH 03/16] Propagate format command errors instead of swallowing The format command handler catches exceptions but always returns true, masking failures from the caller. This causes the installation process to continue even when formatting fails, potentially leading to corrupt or incomplete installs. Return the actual result from deviceManager.format() and throw a descriptive error on failure so the user is notified and the process halts appropriately. --- app/src/controller.manager.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/controller.manager.js b/app/src/controller.manager.js index 5d408d3..d7f0b4f 100644 --- a/app/src/controller.manager.js +++ b/app/src/controller.manager.js @@ -251,11 +251,10 @@ export class Controller { } case Command.CMD_TYPE.format: try { - this.deviceManager.format(cmd.partition); + return this.deviceManager.format(cmd.partition); } catch (e) { - console.error(e); // K1ZFP TODO + throw new Error(`Format ${cmd.partition} failed: ${e.message || e}`); } - return true; case Command.CMD_TYPE.delay: await new Promise((resolve) => setTimeout(resolve, cmd.partition)); return true; -- GitLab From 8786b35de1bb24b2c3d9e50537adc8929228a2e5 Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 13:18:54 +0530 Subject: [PATCH 04/16] Await unlock/lock operations to fix broken error handling The unlock and lock handlers deliberately avoided await with the comment "Do not await thus display unlocking screen". However, this approach is flawed: a try-catch block cannot catch rejections from a non-awaited promise. The errors silently become unhandled promise rejections, masking failures during critical bootloader operations. The original intent appears to be non-blocking UI updates, but: 1. The subsequent step has needUserGesture=true, creating a natural pause for user confirmation anyway 2. Returning true immediately signals success before the operation completes, causing the process to proceed on a potentially still- locked bootloader 3. Unlock/lock failures during flashing are critical and should halt the process, not be silently ignored Add await to ensure errors are caught and propagated. Re-throw unexpected errors while preserving handling for "already unlocked/ locked" responses that some devices return. --- app/src/controller.manager.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/controller.manager.js b/app/src/controller.manager.js index d7f0b4f..9394777 100644 --- a/app/src/controller.manager.js +++ b/app/src/controller.manager.js @@ -191,13 +191,16 @@ export class Controller { ); if (!isUnlocked) { try { - this.deviceManager.unlock(cmd.command); // Do not await thus display unlocking screen + await this.deviceManager.unlock(cmd.command); } catch (e) { //on some device, check unlocked does not work but when we try the command, it throws an error with "already unlocked" if (e.bootloaderMessage?.includes("already")) { WDebug.log("device already unlocked"); } else if (e.bootloaderMessage?.includes("not allowed")) { - WDebug.log("device unlock is not allowed"); + WDebug.log("device unlock is not allowed"); + throw new Error(`Unlock not allowed: ${e.message || e}`); + } else { + throw e; } } } else { @@ -228,13 +231,15 @@ export class Controller { } if (!isLocked) { try { - this.deviceManager.lock(cmd.command); // Do not await thus display unlocking screen + await this.deviceManager.lock(cmd.command); + isLocked = true; } catch (e) { //on some device, check unlocked does not work but when we try the command, it throws an error with "already locked" if (e.bootloaderMessage?.includes("already")) { + WDebug.log("device already locked"); isLocked = true; } else { - console.error(e); //K1ZFP TODO + throw new Error(`Lock failed: ${e.message || e}`); } } } -- GitLab From 9ee8e76da5e8f36ddd3c94f2bf925d8f5ba2db40 Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 13:27:06 +0530 Subject: [PATCH 05/16] Fix null reference in `getProductName` and `getSerialNumber` getProductName() and getSerialNumber() access this.webusb which is declared in the constructor but never assigned during connect(). This causes TypeError when these methods are called. The connect() method stores the USB device in this.adbDaemonWebUsbDevice, not this.webusb. Fix the getters to use the correct property and access the standard WebUSB device attributes (productName, serialNumber). Also remove unused webusb and adbWebBackend properties from constructor, and initialize adbDaemonWebUsbDevice to null for clarity. --- app/src/controller/device/recovery.class.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/controller/device/recovery.class.js b/app/src/controller/device/recovery.class.js index 6746c61..285371e 100644 --- a/app/src/controller/device/recovery.class.js +++ b/app/src/controller/device/recovery.class.js @@ -8,9 +8,8 @@ import { ADB } from "./adb.class.js"; export class Recovery extends Device { constructor(device) { super(device); - this.webusb = null; this.count = 0; - this.adbWebBackend = null; + this.adbDaemonWebUsbDevice = null; } async isConnected() { @@ -211,11 +210,11 @@ export class Recovery extends Device { } getProductName() { - return this.webusb.name; + return this.adbDaemonWebUsbDevice?.productName; } getSerialNumber() { - return this.webusb.product; + return this.adbDaemonWebUsbDevice?.serialNumber; } async adbOpen(blob) { -- GitLab From 65bcecd29cf9ad4408bac49f2ad876e7538b296d Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 13:34:01 +0530 Subject: [PATCH 06/16] Split `wasAlreadyConnected` into query and command wasAlreadyConnected() both checks state and mutates it, violating command-query separation. The function name implies a pure query but has a hidden side effect of marking the device as connected. This pattern is error-prone and makes the code harder to reason about. Split into two explicit methods: - isFirstConnection(): pure query, returns true if not yet connected - markAsConnected(): explicit state mutation Update controller.manager.js to call both methods explicitly, making the control flow clear at the call site. --- app/src/controller.manager.js | 4 ++-- app/src/controller/device.manager.js | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/controller.manager.js b/app/src/controller.manager.js index 9394777..aa4546a 100644 --- a/app/src/controller.manager.js +++ b/app/src/controller.manager.js @@ -273,8 +273,8 @@ export class Controller { async onDeviceConnected() { const productName = this.deviceManager.getProductName(); - const wasAlreadyConnected = this.deviceManager.wasAlreadyConnected(); - if (!wasAlreadyConnected) { + if (this.deviceManager.isFirstConnection()) { + this.deviceManager.markAsConnected(); this.view.updateData("product-name", productName); this.model = productName; WDebug.log("ControllerManager Model:", this.model); diff --git a/app/src/controller/device.manager.js b/app/src/controller/device.manager.js index 74aff73..cb0188f 100644 --- a/app/src/controller/device.manager.js +++ b/app/src/controller/device.manager.js @@ -34,12 +34,12 @@ export class DeviceManager { await this.downloader.init(); } - wasAlreadyConnected() { - if (this.wasConnected == false) { - this.wasConnected = true; - return false; - } - return true; + isFirstConnection() { + return !this.wasConnected; + } + + markAsConnected() { + this.wasConnected = true; } setResources(folder, steps) { -- GitLab From 0793a5a697b490232116b5a9441a25fc68dfe69c Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 15:15:49 +0530 Subject: [PATCH 07/16] Rename inner loop variable to avoid shadowing The nested loop in downloadAndUnzipFolder() reuses variable name 'i' for both the outer folder iteration and inner zip entries iteration. While JavaScript's block scoping with 'let' makes this technically correct, it's confusing during debugging and prone to copy-paste errors. Rename inner loop variable from 'i' to 'j' for clarity. --- app/src/controller/downloader.manager.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/controller/downloader.manager.js b/app/src/controller/downloader.manager.js index 4a2937c..5099193 100644 --- a/app/src/controller/downloader.manager.js +++ b/app/src/controller/downloader.manager.js @@ -80,15 +80,15 @@ export class Downloader { if (file.unzip) { const zipReader = new ZipReader(new BlobReader(blob)); const filesEntries = await zipReader.getEntries(); - for (let i = 0; i < filesEntries.length; i++) { + for (let j = 0; j < filesEntries.length; j++) { const unzippedEntry = await this.getFileFromZip( - filesEntries[i], + filesEntries[j], (value, total) => { - onUnzipProgress(value, total, filesEntries[i].filename); + onUnzipProgress(value, total, filesEntries[j].filename); }, ); let filename = this.getMappedName( - filesEntries[i].filename, + filesEntries[j].filename, file.mapping, ); if (filesRequired.includes(filename)) { -- GitLab From b56fdfd15fbc5a76a34205afa6e8bf18a6c53f34 Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 15:16:47 +0530 Subject: [PATCH 08/16] Fix duplicate condition, add missing protocolCode check getInOutEndpoints() checks interfaceSubclass twice instead of checking all three USB interface attributes. The WebUsbDeviceFilter defines classCode, subclassCode, and protocolCode, but protocolCode was never validated. Replace the duplicate interfaceSubclass check with interfaceProtocol to properly filter USB interfaces matching the ADB protocol (class=0xff, subclass=0x42, protocol=1). --- app/src/controller/device/recovery.class.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/controller/device/recovery.class.js b/app/src/controller/device/recovery.class.js index 285371e..d84916b 100644 --- a/app/src/controller/device/recovery.class.js +++ b/app/src/controller/device/recovery.class.js @@ -100,9 +100,9 @@ export class Recovery extends Device { for (const interface_ of configuration.interfaces) { for (const alternate of interface_.alternates) { if ( - alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode && alternate.interfaceClass === WebUsbDeviceFilter.classCode && - alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode + alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode && + alternate.interfaceProtocol === WebUsbDeviceFilter.protocolCode ) { if ( ((_a = this.adbDaemonWebUsbDevice.configuration) === null || -- GitLab From 0e717cc880112204fa6c221a5803d78ec4fb2aee Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 15:20:03 +0530 Subject: [PATCH 09/16] Resolve incomplete error handling (K1ZFP TODOs) Several error handlers logged errors but returned false or swallowed exceptions, masking failures from callers. This caused silent failures during critical operations like reboot and sideload. Changes: - reboot: throw descriptive error instead of returning false - sideload: throw descriptive error instead of returning false - bootloader isUnlocked/isLocked: improve error log messages - bootloader unlock/lock: improve error messages with context - Remove stale K1ZFP TODO comments throughout Errors now propagate to the UI layer where they can be displayed to users, allowing them to understand and potentially retry failed operations. --- app/src/controller.manager.js | 10 +++------- app/src/controller/device/bootloader.class.js | 8 ++++---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/src/controller.manager.js b/app/src/controller.manager.js index aa4546a..50a3bf1 100644 --- a/app/src/controller.manager.js +++ b/app/src/controller.manager.js @@ -33,7 +33,6 @@ export class Controller { WDebug.log("Controller Manager Next", next); if (next) { - //K1ZFP check this if (next.mode) { //if next step require another mode [adb|fastboot|bootloader] if (this.deviceManager.isConnected() && !this.inInMode(next.mode)) { @@ -138,12 +137,10 @@ export class Controller { case Command.CMD_TYPE.reboot: try { await this.deviceManager.reboot(cmd.mode); + return true; } catch (e) { - console.error(e); - //K1ZFP TODO - return false; + throw new Error(`Reboot to ${cmd.mode} failed: ${e.message || e}`); } - return true; case Command.CMD_TYPE.connect: { const proposal = "Proposal: Check connection and that no other program is using the phone and retry."; @@ -251,8 +248,7 @@ export class Controller { await this.deviceManager.sideload(cmd.file); return true; } catch (e) { - console.error(e); // K1ZFP TODO - return false; + throw new Error(`Sideload ${cmd.file} failed: ${e.message || e}`); } case Command.CMD_TYPE.format: try { diff --git a/app/src/controller/device/bootloader.class.js b/app/src/controller/device/bootloader.class.js index cde9d48..cb4919e 100644 --- a/app/src/controller/device/bootloader.class.js +++ b/app/src/controller/device/bootloader.class.js @@ -104,7 +104,7 @@ export class Bootloader extends Device { const unlocked = await this.device.getVariable(variable); return !(!unlocked || unlocked === "no"); } catch (e) { - console.error(e); // K1ZFP TODO + console.error("isUnlocked check failed:", e); throw e; } } @@ -117,7 +117,7 @@ export class Bootloader extends Device { const unlocked = await this.device.getVariable(variable); return !unlocked || unlocked === "no"; } catch (e) { - console.error(e); //K1ZFP TODO + console.error("isLocked check failed:", e); throw e; } } @@ -128,7 +128,7 @@ export class Bootloader extends Device { if (command) { await this.device.runCommand(command); } else { - throw Error("no unlock command configured"); //K1ZFP TODO + throw new Error("No unlock command configured for this device"); } } @@ -137,7 +137,7 @@ export class Bootloader extends Device { await this.device.runCommand(command); return !(await this.isUnlocked()); } else { - throw Error("no lock command configured"); //K1ZFP TODO + throw new Error("No lock command configured for this device"); } } } -- GitLab From 3b2db0c334effc24223b8b1a8736576ee209330f Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 15:21:30 +0530 Subject: [PATCH 10/16] Fix isConnected to be sync and return boolean isConnected() in ADB and Recovery classes was marked async but never awaited anything, and returned the device object directly instead of a boolean. This caused two issues: 1. Unnecessary async overhead for a synchronous check 2. Return type inconsistency - callers expect boolean, got object Remove async keyword to match base Device class and Bootloader implementation. Use double-negation (!!) to coerce getDevice() result to boolean explicitly. --- app/src/controller/device/adb.class.js | 4 ++-- app/src/controller/device/recovery.class.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/controller/device/adb.class.js b/app/src/controller/device/adb.class.js index 04be405..1a18a24 100644 --- a/app/src/controller/device/adb.class.js +++ b/app/src/controller/device/adb.class.js @@ -13,12 +13,12 @@ export class ADB extends Device { this.webusb = null; } - async isConnected() { + isConnected() { if (!this.device) { return false; } try { - return this.device.getDevice(); + return !!this.device.getDevice(); } catch { return false; } diff --git a/app/src/controller/device/recovery.class.js b/app/src/controller/device/recovery.class.js index d84916b..85c4bde 100644 --- a/app/src/controller/device/recovery.class.js +++ b/app/src/controller/device/recovery.class.js @@ -12,12 +12,12 @@ export class Recovery extends Device { this.adbDaemonWebUsbDevice = null; } - async isConnected() { + isConnected() { if (!this.device) { return false; } try { - return this.device.getDevice(); + return !!this.device.getDevice(); } catch { return false; } -- GitLab From d22f2b4209bebcd93fc75720d937e2d9f603ff42 Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 15:22:39 +0530 Subject: [PATCH 11/16] Remove unused quota variable this.quota is assigned from navigator.storage.estimate() during init but never read anywhere in the codebase. Remove the dead code. Note: If storage quota checking is desired before downloading large firmware files, this should be implemented as a proper pre-download validation with user feedback, not just a stored variable. --- app/src/controller/downloader.manager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/controller/downloader.manager.js b/app/src/controller/downloader.manager.js index 5099193..385272c 100644 --- a/app/src/controller/downloader.manager.js +++ b/app/src/controller/downloader.manager.js @@ -22,7 +22,6 @@ export class Downloader { this.db = await this.openDBStore(); await this.clearDBStore(); - this.quota = await navigator.storage.estimate(); } /* -- GitLab From 4bc6fc52a139b26551530f8332b5dc3dd8408ae1 Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 16:33:50 +0530 Subject: [PATCH 12/16] Improve flash reliability with delays and checks Flash operations can timeout due to USB instability or device not being ready. The current retry logic retries immediately, which often fails again if the device needs time to stabilize. bootloader.class.js: - Add pre-flash device connection check - Add 1 second delay between retries to let device recover - Check device connection before each retry attempt - Log flash size for debugging - Improve error messages with troubleshooting hints controller.manager.js: - Add 500ms cooldown after each flash operation to prevent overwhelming the device with rapid successive writes These changes reduce timeout likelihood by giving the device time to process each operation. The root cause (library timeout threshold) may still need investigation in @e/fastboot, but these mitigations improve real-world reliability. --- app/src/controller.manager.js | 9 ++++++-- app/src/controller/device/bootloader.class.js | 22 ++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/src/controller.manager.js b/app/src/controller.manager.js index 50a3bf1..bfde75a 100644 --- a/app/src/controller.manager.js +++ b/app/src/controller.manager.js @@ -159,14 +159,19 @@ export class Controller { } case Command.CMD_TYPE.erase: return this.deviceManager.erase(cmd.partition); - case Command.CMD_TYPE.flash: - return this.deviceManager.flash( + case Command.CMD_TYPE.flash: { + const FLASH_COOLDOWN_MS = 500; // Brief pause after flash to let device stabilize + const result = await this.deviceManager.flash( cmd.file, cmd.partition, (done, total) => { this.view.onInstalling(cmd.file, done, total); }, ); + // Small delay between flash operations to prevent overwhelming the device + await new Promise(resolve => setTimeout(resolve, FLASH_COOLDOWN_MS)); + return result; + } case Command.CMD_TYPE.unlock: { //check if unlocked to avoid unnecessary command let isUnlocked = false; diff --git a/app/src/controller/device/bootloader.class.js b/app/src/controller/device/bootloader.class.js index cb4919e..3068fc9 100644 --- a/app/src/controller/device/bootloader.class.js +++ b/app/src/controller/device/bootloader.class.js @@ -74,7 +74,15 @@ export class Bootloader extends Device { async flashBlob(partition, blob, onProgress, retryCount = 0) { const MAX_RETRIES = 3; + const RETRY_DELAY_MS = 1000; // Wait before retry to let device stabilize + + // Pre-flash check: ensure device is still connected + if (!this.device.isConnected) { + throw new Error(`Device disconnected before flashing ${partition}`); + } + try { + WDebug.log(`Flashing ${partition} (${(blob.size / 1024 / 1024).toFixed(1)} MB)...`); await this.device.flashBlob(partition, blob, (progress) => { onProgress(progress * blob.size, blob.size, partition); }); @@ -84,9 +92,21 @@ export class Bootloader extends Device { if (e instanceof TimeoutError) { WDebug.log(`Timeout on flashblob > ${partition} (attempt ${retryCount + 1}/${MAX_RETRIES})`); if (retryCount < MAX_RETRIES) { + // Wait before retry to allow device to recover + WDebug.log(`Waiting ${RETRY_DELAY_MS}ms before retry...`); + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); + + // Check if device is still connected before retry + if (!this.device.isConnected) { + throw new Error(`Device disconnected during flash of ${partition}. Please reconnect and try again.`); + } + return await this.flashBlob(partition, blob, onProgress, retryCount + 1); } - throw new Error(`Bootloader timeout: flashing ${partition} failed after ${MAX_RETRIES} retries`); + throw new Error( + `Bootloader timeout: flashing ${partition} failed after ${MAX_RETRIES} retries. ` + + `Try using a different USB port or cable.` + ); } else { console.log("flashBlob error", e); throw new Error(`Bootloader error: ${e.message || e}`); -- GitLab From e2a4802a559abc504eddb77872e318f586082ce2 Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 16:49:39 +0530 Subject: [PATCH 13/16] Increase retry and flash cooldown delays --- app/src/controller.manager.js | 2 +- app/src/controller/device/bootloader.class.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/controller.manager.js b/app/src/controller.manager.js index bfde75a..301171d 100644 --- a/app/src/controller.manager.js +++ b/app/src/controller.manager.js @@ -160,7 +160,7 @@ export class Controller { case Command.CMD_TYPE.erase: return this.deviceManager.erase(cmd.partition); case Command.CMD_TYPE.flash: { - const FLASH_COOLDOWN_MS = 500; // Brief pause after flash to let device stabilize + const FLASH_COOLDOWN_MS = 5000; // Pause after flash to let device stabilize const result = await this.deviceManager.flash( cmd.file, cmd.partition, diff --git a/app/src/controller/device/bootloader.class.js b/app/src/controller/device/bootloader.class.js index 3068fc9..2f26180 100644 --- a/app/src/controller/device/bootloader.class.js +++ b/app/src/controller/device/bootloader.class.js @@ -74,7 +74,7 @@ export class Bootloader extends Device { async flashBlob(partition, blob, onProgress, retryCount = 0) { const MAX_RETRIES = 3; - const RETRY_DELAY_MS = 1000; // Wait before retry to let device stabilize + const RETRY_DELAY_MS = 3000; // Wait before retry to let device stabilize // Pre-flash check: ensure device is still connected if (!this.device.isConnected) { -- GitLab From 36e3474a3abec7a60dd2b3964a6604a6a8c1d42c Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Fri, 9 Jan 2026 16:58:56 +0530 Subject: [PATCH 14/16] Fix off-by-one error in flash retry counter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The retry logic used a 0-indexed retryCount but compared against MAX_RETRIES with < operator, causing 4 attempts instead of 3: [DEBUG] Timeout on flashblob > boot_a (attempt 1/3) [DEBUG] Timeout on flashblob > boot_a (attempt 2/3) [DEBUG] Timeout on flashblob > boot_a (attempt 3/3) [DEBUG] Timeout on flashblob > boot_a (attempt 4/3) <-- should not happen The issue: - retryCount=0: "1/3", 0 < 3 true → retry - retryCount=1: "2/3", 1 < 3 true → retry - retryCount=2: "3/3", 2 < 3 true → retry - retryCount=3: "4/3", 3 < 3 false → throw Rename retryCount to attempt (1-indexed) so the counter matches the displayed value and MAX_ATTEMPTS reflects actual attempt count. --- app/src/controller/device/bootloader.class.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/controller/device/bootloader.class.js b/app/src/controller/device/bootloader.class.js index 2f26180..7230bc1 100644 --- a/app/src/controller/device/bootloader.class.js +++ b/app/src/controller/device/bootloader.class.js @@ -72,8 +72,8 @@ export class Bootloader extends Device { ); } - async flashBlob(partition, blob, onProgress, retryCount = 0) { - const MAX_RETRIES = 3; + async flashBlob(partition, blob, onProgress, attempt = 1) { + const MAX_ATTEMPTS = 3; const RETRY_DELAY_MS = 3000; // Wait before retry to let device stabilize // Pre-flash check: ensure device is still connected @@ -90,8 +90,8 @@ export class Bootloader extends Device { return true; } catch (e) { if (e instanceof TimeoutError) { - WDebug.log(`Timeout on flashblob > ${partition} (attempt ${retryCount + 1}/${MAX_RETRIES})`); - if (retryCount < MAX_RETRIES) { + WDebug.log(`Timeout on flashblob > ${partition} (attempt ${attempt}/${MAX_ATTEMPTS})`); + if (attempt < MAX_ATTEMPTS) { // Wait before retry to allow device to recover WDebug.log(`Waiting ${RETRY_DELAY_MS}ms before retry...`); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); @@ -101,10 +101,10 @@ export class Bootloader extends Device { throw new Error(`Device disconnected during flash of ${partition}. Please reconnect and try again.`); } - return await this.flashBlob(partition, blob, onProgress, retryCount + 1); + return await this.flashBlob(partition, blob, onProgress, attempt + 1); } throw new Error( - `Bootloader timeout: flashing ${partition} failed after ${MAX_RETRIES} retries. ` + + `Bootloader timeout: flashing ${partition} failed after ${MAX_ATTEMPTS} attempts. ` + `Try using a different USB port or cable.` ); } else { -- GitLab From e091de4075c7fcb10df1a63287684cf170d575e3 Mon Sep 17 00:00:00 2001 From: "manu.suresh" Date: Mon, 12 Jan 2026 13:05:14 +0530 Subject: [PATCH 15/16] web-installer: npm run format --- app/src/controller.manager.js | 2 +- app/src/controller/device/bootloader.class.js | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/controller.manager.js b/app/src/controller.manager.js index 301171d..6c10606 100644 --- a/app/src/controller.manager.js +++ b/app/src/controller.manager.js @@ -169,7 +169,7 @@ export class Controller { }, ); // Small delay between flash operations to prevent overwhelming the device - await new Promise(resolve => setTimeout(resolve, FLASH_COOLDOWN_MS)); + await new Promise((resolve) => setTimeout(resolve, FLASH_COOLDOWN_MS)); return result; } case Command.CMD_TYPE.unlock: { diff --git a/app/src/controller/device/bootloader.class.js b/app/src/controller/device/bootloader.class.js index 7230bc1..eb36dc0 100644 --- a/app/src/controller/device/bootloader.class.js +++ b/app/src/controller/device/bootloader.class.js @@ -82,7 +82,9 @@ export class Bootloader extends Device { } try { - WDebug.log(`Flashing ${partition} (${(blob.size / 1024 / 1024).toFixed(1)} MB)...`); + WDebug.log( + `Flashing ${partition} (${(blob.size / 1024 / 1024).toFixed(1)} MB)...`, + ); await this.device.flashBlob(partition, blob, (progress) => { onProgress(progress * blob.size, blob.size, partition); }); @@ -90,22 +92,26 @@ export class Bootloader extends Device { return true; } catch (e) { if (e instanceof TimeoutError) { - WDebug.log(`Timeout on flashblob > ${partition} (attempt ${attempt}/${MAX_ATTEMPTS})`); + WDebug.log( + `Timeout on flashblob > ${partition} (attempt ${attempt}/${MAX_ATTEMPTS})`, + ); if (attempt < MAX_ATTEMPTS) { // Wait before retry to allow device to recover WDebug.log(`Waiting ${RETRY_DELAY_MS}ms before retry...`); - await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); + await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS)); // Check if device is still connected before retry if (!this.device.isConnected) { - throw new Error(`Device disconnected during flash of ${partition}. Please reconnect and try again.`); + throw new Error( + `Device disconnected during flash of ${partition}. Please reconnect and try again.`, + ); } return await this.flashBlob(partition, blob, onProgress, attempt + 1); } throw new Error( `Bootloader timeout: flashing ${partition} failed after ${MAX_ATTEMPTS} attempts. ` + - `Try using a different USB port or cable.` + `Try using a different USB port or cable.`, ); } else { console.log("flashBlob error", e); -- GitLab From 1125b4e19adb1a3c401d99dd70724258008203d2 Mon Sep 17 00:00:00 2001 From: Daniel Jacob Chittoor Date: Mon, 12 Jan 2026 15:04:49 +0530 Subject: [PATCH 16/16] Implement retry loop for bootloader connection --- app/src/controller/device/bootloader.class.js | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/app/src/controller/device/bootloader.class.js b/app/src/controller/device/bootloader.class.js index eb36dc0..d8ecb93 100644 --- a/app/src/controller/device/bootloader.class.js +++ b/app/src/controller/device/bootloader.class.js @@ -44,10 +44,34 @@ export class Bootloader extends Device { } async connect() { - try { - await this.device.connect(); - } catch (e) { - throw new Error("Cannot connect Bootloader", `${e.message || e}`); + const MAX_CONNECT_ATTEMPTS = 3; + const CONNECT_RETRY_DELAY = 2000; // 2 seconds + + for (let attempt = 1; attempt <= MAX_CONNECT_ATTEMPTS; attempt++) { + try { + WDebug.log(`Connecting to bootloader (attempt ${attempt}/${MAX_CONNECT_ATTEMPTS})...`); + await this.device.connect(); + WDebug.log(`Successfully connected to bootloader on attempt ${attempt}`); + return; + } catch (e) { + const errorMsg = e.message || String(e); + WDebug.log(`Bootloader connection attempt ${attempt} failed: ${errorMsg}`); + + // If this is the last attempt, throw the error + if (attempt === MAX_CONNECT_ATTEMPTS) { + throw new Error( + `Cannot connect to bootloader after ${MAX_CONNECT_ATTEMPTS} attempts. ` + + `The device may not be in bootloader mode yet. ` + + `Please ensure the device is in bootloader/fastboot mode and try again. ` + + `Error: ${errorMsg}` + ); + } + + // Wait before retry, with increasing delay + const delay = CONNECT_RETRY_DELAY * attempt; + WDebug.log(`Waiting ${delay}ms before retry...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + } } } -- GitLab