From d48a6d9620201d45d2bbe2594ded6e97b97d6b93 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 30 Nov 2025 19:59:56 -0600 Subject: [PATCH] Rnode flasher updates --- src/frontend/public/rnode-flasher/index.html | 751 +++++++++++-------- 1 file changed, 448 insertions(+), 303 deletions(-) diff --git a/src/frontend/public/rnode-flasher/index.html b/src/frontend/public/rnode-flasher/index.html index ee7d984..775c90d 100644 --- a/src/frontend/public/rnode-flasher/index.html +++ b/src/frontend/public/rnode-flasher/index.html @@ -22,40 +22,40 @@ - + -
+
-
-
+
+
-
-
RNode Flasher
-
Developed by Liam Cottle
+
+
RNode Flasher
+
Developed by Liam Cottle
-
+
-
- 1. Select your device +
+
1. Select your device
-
+
-
-
Product
-
-
-
Model
- @@ -64,194 +64,242 @@
-
-
-
-
+
+
Can't find your device? Open an issue on - GitHub + GitHub
-
+ +
-
- 2. Select firmware to flash (.zip) +
+
2. Select firmware to flash (.zip)
-
+
-
- +
+
-
-
-
- Flashing: {{flashingProgress}}% - Flashing: please wait... -
-
+
+
+ + Flashing: {{flashingProgress}}% + Connecting to device... + + {{ flashingStatus }} +
+
+
-
-
- Download Firmware - : {{ recommendedFirmwareFilename }} +
+
+ Download Firmware + : {{ recommendedFirmwareFilename }}
- -
-
Common error messages
-
• Hardware Failure: You need to provision the eeprom in step 3.
-
• Firmware Corrupt: You need to set the firmware hash in step 4.
+
+
Common Issues
+
+
Hardware Failure: You need to provision the eeprom in step 3.
+
Firmware Corrupt: You need to set the firmware hash in step 4.
+
-
+ +
+ + +
+ +
-
- 3. Provision EEPROM (sets device info, checksum and blank signature) -
+
+
3. Provision EEPROM
+
Sets device info, checksum and blank signature
+
-
- -
-
- +
+ +
+ + Provisioning: please wait...
-
Provisioning: please wait...
+
-
- -
- -
- 4. Set Firmware Hash (uses hash from board, will fix later)
-
- -
-
- - - - + +
+ + +
+ +
+
4. Set Firmware Hash
+
Uses hash from board
-
Setting Firmware Hash: please wait...
+ +
+ +
+ + + + + Setting Firmware Hash: please wait... +
+
+
+ + +
+ +
+
5. Done
+
+ +
+
• If you made it this far, and all previous steps were successful, your RNode should be ready to use.
+
• To use RNode with MeshChat, you will need to add an RNodeInterface in the Interfaces → Add Interface page.
+
• To use RNode with Sideband, you will need to configure it in Hardware → RNode and enable Connectivity → Connect via RNode.
+
• You must restart MeshChat and Sideband for interface setting changes to take effect, otherwise nothing will happen!
+
+ +
+
-
+ +
-
- 5. Done -
+ +
-
-
• If you made it this far, and all previous steps were successful, your RNode should be ready to use.
-
• To use RNode with MeshChat, you will need to add an RNodeInterface in the Interfaces → Add Interface page.
-
• To use RNode with Sideband, you will need to configure it in Hardware → RNode and enable Connectivity → Connect via RNode.
-
• You must restart MeshChat and Sideband for interface setting changes to take effect, otherwise nothing will happen!
-
+
+
Advanced Tools
+
-
+
-
- -
- Advanced Tools -
- -
- -
- - - - -
-
EEPROM dumps are shown in dev tools console.
+
EEPROM dumps are shown in dev tools console.
-
- +
+
-
+ +
-
- Configure Bluetooth (optional) -
+
+
Configure Bluetooth (optional)
+
-
+
• Bluetooth is not supported on all devices.
• Some devices use Bluetooth Classic, and some use BLE (Bluetooth Low Energy)
• Put the RNode into Bluetooth Pairing mode, then connect to it from Android Bluetooth settings.
• Once you have initiated a pair request from Android, a PIN should show on the RNode display.
-
• In Sideband you will need to enable Connect using Bluetooth in Hardware → RNode.
-
• If your device uses BLE you will also need to enable Device requires BLE in Hardware → RNode.
-
• Don't forget to restart Sideband for the setting changes to take effect, otherwise nothing will happen!
+
• In Sideband you will need to enable Connect using Bluetooth in Hardware → RNode.
+
• If your device uses BLE you will also need to enable Device requires BLE in Hardware → RNode.
+
• Don't forget to restart Sideband for the setting changes to take effect!
-
-
- - -
@@ -259,51 +307,52 @@
-
+ +
-
- Configure TNC Mode (optional) -
+
+
Configure TNC Mode (optional)
+
-
+
• TNC mode allows an RNode to be used as a KISS compatible TNC over the Serial Port.
• This mode makes it usable with amateur radio software that can talk to a KISS TNC over a serial port.
-
• You must leave TNC mode disabled when using RNode with apps like MeshChat or Sideband.
+
• You must leave TNC mode disabled when using RNode with apps like MeshChat or Sideband.
-
+
-
-
Frequency (Hz)
- +
+ +
-
-
Bandwidth
-
-
-
Tx Power (dBm)
- +
+ +
-
-
Spreading Factor
-
-
-
Coding Rate
-
-
- -
@@ -312,44 +361,43 @@
-
+
-
- Configure Display (optional) -
+ +
-
+
-
-
Rotation
- - - -
-
-
Reconditioning
- -
-
-
Setting display rotation requires firmware v1.80+
+
+
Setting display rotation requires firmware v1.80+
@@ -380,9 +428,12 @@ isFlashing: false, flashingProgress: 0, + flashingStatus: "", + flashError: null, isProvisioning: false, isSettingFirmwareHash: false, + isEnteringDfuMode: false, rnodeDisplayImage: null, @@ -951,58 +1002,94 @@ async askForSerialPort() { if(!navigator.serial){ - alert("Web Serial is not supported in this browser"); + this.flashError = "Web Serial is not supported in this browser. Please use Chrome, Edge, or another Chromium-based browser."; + alert("Web Serial is not supported in this browser. Please use Chrome, Edge, or another Chromium-based browser."); return null; } // close any existing rnode connection if(this.rnode){ - await this.rnode.close(); + try { + await this.rnode.close(); + } catch(e) { + console.log("Error closing existing connection:", e); + } this.rnode = null; } - // ask user to select device - return await navigator.serial.requestPort({ - filters: [], - }); + try { + // ask user to select device + return await navigator.serial.requestPort({ + filters: [], + }); + } catch(e) { + if(e.name === "NotFoundError" || e.message?.includes("No port selected")) { + this.flashError = "No device selected. Please select a serial port."; + } else { + this.flashError = "Failed to connect to device: " + (e.message || e); + } + throw e; + } }, async askForRNode() { - // ask for serial port - const serialPort = await this.askForSerialPort(); - if(!serialPort){ - return false; - } + try { + // ask for serial port + const serialPort = await this.askForSerialPort(); + if(!serialPort){ + return false; + } - // check if device is an rnode - this.rnode = await RNode.fromSerialPort(serialPort); - const isRNode = await this.rnode.detect(); - if(!isRNode){ - await this.rnode.close(); - alert("Selected device is not an RNode!"); - return false; - } + this.flashingStatus = "Connecting to device..."; - return this.rnode; + // check if device is an rnode + this.rnode = await RNode.fromSerialPort(serialPort); + this.flashingStatus = "Detecting RNode..."; + const isRNode = await this.rnode.detect(); + if(!isRNode){ + await this.rnode.close(); + this.flashError = "Selected device is not an RNode! Please make sure you've selected the correct serial port."; + alert("Selected device is not an RNode! Please make sure you've selected the correct serial port."); + this.flashingStatus = ""; + return false; + } + + this.flashingStatus = ""; + return this.rnode; + } catch(e) { + this.flashError = "Failed to connect to RNode: " + (e.message || e); + this.flashingStatus = ""; + throw e; + } }, async enterDfuMode() { - // ask for serial port - const serialPort = await this.askForSerialPort(); - if(!serialPort){ - return; + this.isEnteringDfuMode = true; + this.flashError = null; + + try { + // ask for serial port + const serialPort = await this.askForSerialPort(); + if(!serialPort){ + return; + } + + // enter dfu mode + console.log("entering dfu mode"); + const flasher = new Nrf52DfuFlasher(serialPort); + await flasher.enterDfuMode(); + console.log("entering dfu mode: done"); + + alert("Device should now be in DFU mode. You can now proceed with flashing firmware."); + } catch(e) { + this.flashError = "Failed to enter DFU mode: " + (e.message || e); + alert("Failed to enter DFU mode: " + (e.message || e)); + } finally { + this.isEnteringDfuMode = false; } - // enter dfu mode - console.log("entering dfu mode"); - const flasher = new Nrf52DfuFlasher(serialPort); - await flasher.enterDfuMode(); - console.log("entering dfu mode: done"); - - alert("Device should now be in DFU mode."); - }, async flash() { switch(this.selectedProduct?.platform){ @@ -1022,171 +1109,229 @@ }, async flashNrf52() { + this.flashError = null; + // ensure firmware file selected const file = this.$refs["file"].files[0]; if(!file){ - alert("Select a firmware file first"); + this.flashError = "Please select a firmware file (.zip) first."; + alert("Please select a firmware file (.zip) first."); return; } - // ask for serial port - const serialPort = await this.askForSerialPort(); - if(!serialPort){ - return; - } - - // update progress - this.isFlashing = true; - this.flashingProgress = 0; + let serialPort = null; try { + // ask for serial port + serialPort = await this.askForSerialPort(); + if(!serialPort){ + return; + } + + // update progress + this.isFlashing = true; + this.flashingProgress = 0; + this.flashingStatus = "Initializing flash process..."; // flash file const flasher = new Nrf52DfuFlasher(serialPort); await flasher.flash(file, (percentage, message) => { this.flashingProgress = percentage; + this.flashingStatus = message || `Flashing: ${percentage}%`; }); // flashing successful - alert("Firmware has been flashed!"); + this.flashingStatus = "Flash complete!"; + alert("Firmware has been flashed successfully!"); } catch(e) { - alert("Firmware flashing failed: " + e); - console.log(e); + const errorMsg = e.message || String(e); + if(errorMsg.includes("No port selected") || errorMsg.includes("NotFoundError")) { + this.flashError = "No device selected. Please select a serial port and try again."; + } else if(errorMsg.includes("not an RNode") || errorMsg.includes("detect")) { + this.flashError = "Failed to detect RNode. Make sure the device is connected and in the correct mode."; + } else if(errorMsg.includes("DFU") || errorMsg.includes("dfu")) { + this.flashError = "DFU mode error: " + errorMsg + ". Make sure the device is in DFU mode (use 'Enter DFU Mode' button first)."; + } else { + this.flashError = "Firmware flashing failed: " + errorMsg; + } + alert(this.flashError); + console.error("Flash error:", e); } finally { this.isFlashing = false; - } + this.flashingStatus = ""; - // close port - console.log("Closing serial port"); - try { - await this.serialPort.close(); - } catch(e) { - console.log("failed to close serial port, ignoring...", e); + // close port + if(serialPort) { + try { + await serialPort.close(); + } catch(e) { + console.log("Failed to close serial port:", e); + } + } } - }, async flashEsp32() { + this.flashError = null; + // ensure ESPLoader is available if(!window.ESPLoader){ - alert("esptool-js could not be loaded."); + this.flashError = "esptool-js could not be loaded. Please refresh the page and try again."; + alert("esptool-js could not be loaded. Please refresh the page and try again."); return; } // ensure flash config is known, use from selected model, or fallback to selected product const flashConfig = this.selectedModel?.flash_config ?? this.selectedProduct?.flash_config; if(!flashConfig){ - alert("Flash config is not available for the selected device."); + this.flashError = "Flash configuration is not available for the selected device. Please select a different device or check the firmware file."; + alert("Flash configuration is not available for the selected device. Please select a different device or check the firmware file."); return; } // ensure firmware file selected const file = this.$refs["file"].files[0]; if(!file){ - alert("Select a firmware file first"); + this.flashError = "Please select a firmware file (.zip) first."; + alert("Please select a firmware file (.zip) first."); return; } - // ask for serial port - const serialPort = await this.askForSerialPort(); - if(!serialPort){ - return; - } - - // update progress - this.isFlashing = true; - this.flashingProgress = 0; + let serialPort = null; try { - - // read zip file - const blobReader = new window.zip.BlobReader(file); - const zipReader = new window.zip.ZipReader(blobReader); - const zipEntries = await zipReader.getEntries(); - - // get files to flash - const filesToFlash = []; - for(const [address, filename] of Object.entries(flashConfig.flash_files)){ - - // find file inside zip - const file = zipEntries.find((zipEntry) => zipEntry.filename === filename); - if(!file){ - throw filename + " not found in firmware file!"; - } - - // get file data as binary string - const blob = await file.getData(new window.zip.BlobWriter()); - const data = await this.readAsBinaryString(blob); // fixme: deprecated, but works for now - - // add to files to flash - filesToFlash.push({ - "address": parseInt(address), - "data": data, - }); - + // ask for serial port + serialPort = await this.askForSerialPort(); + if(!serialPort){ + return; } - // init transport and esploader - const transport = new window.Transport(serialPort, true); - const esploader = new window.ESPLoader({ - transport: transport, - baudrate: 921600, - debugLogging: false, - enableTracing: false, - terminal: { - clean() { + // update progress + this.isFlashing = true; + this.flashingProgress = 0; + this.flashingStatus = "Connecting to ESP32 device..."; + try { + + // read zip file + this.flashingStatus = "Reading firmware file..."; + const blobReader = new window.zip.BlobReader(file); + const zipReader = new window.zip.ZipReader(blobReader); + const zipEntries = await zipReader.getEntries(); + + // get files to flash + this.flashingStatus = "Extracting firmware files..."; + const filesToFlash = []; + for(const [address, filename] of Object.entries(flashConfig.flash_files)){ + + // find file inside zip + const file = zipEntries.find((zipEntry) => zipEntry.filename === filename); + if(!file){ + throw new Error(`Required file "${filename}" not found in firmware ZIP file. Please download the correct firmware for your device.`); + } + + // get file data as binary string + const blob = await file.getData(new window.zip.BlobWriter()); + const data = await this.readAsBinaryString(blob); // fixme: deprecated, but works for now + + // add to files to flash + filesToFlash.push({ + "address": parseInt(address), + "data": data, + }); + + } + + // init transport and esploader + this.flashingStatus = "Connecting to ESP32..."; + const transport = new window.Transport(serialPort, true); + const esploader = new window.ESPLoader({ + transport: transport, + baudrate: 921600, + debugLogging: false, + enableTracing: false, + terminal: { + clean() { + + }, + writeLine(data) { + console.log(data); + }, + write(data) { + console.log(data); + }, }, - writeLine(data) { - console.log(data); + }); + + // load device info + this.flashingStatus = "Detecting ESP32 device..."; + await esploader.main(); + + // flash device + this.flashingStatus = "Flashing firmware..."; + await esploader.writeFlash({ + fileArray: filesToFlash, + flashSize: flashConfig.flash_size, + flashMode: "DIO", + flashFreq: "80MHz", + eraseAll: false, + compress: true, + calculateMD5Hash: (image) => CryptoJS.MD5(CryptoJS.enc.Latin1.parse(image)), + reportProgress: (fileIndex, written, total) => { + const currentFilePercentage = (written / total) * 100; + this.flashingProgress = Math.floor(currentFilePercentage); + this.flashingStatus = `Flashing file ${fileIndex + 1}/${filesToFlash.length}: ${Math.floor(currentFilePercentage)}%`; }, - write(data) { - console.log(data); - }, - }, - }); + }); - // load device info - await esploader.main(); + // reboot device + this.flashingStatus = "Rebooting device..."; + await transport.setDTR(false); + await new Promise((resolve) => setTimeout(resolve, 100)); + await transport.setDTR(true); - // flash device - await esploader.writeFlash({ - fileArray: filesToFlash, - flashSize: flashConfig.flash_size, - flashMode: "DIO", - flashFreq: "80MHz", - eraseAll: false, - compress: true, - calculateMD5Hash: (image) => CryptoJS.MD5(CryptoJS.enc.Latin1.parse(image)), - reportProgress: (fileIndex, written, total) => { - const currentFilePercentage = (written / total) * 100; - this.flashingProgress = Math.floor(currentFilePercentage); - }, - }); + // flashing successful + this.flashingStatus = "Flash complete!"; + alert("Firmware has been flashed successfully!"); - // reboot device - await transport.setDTR(false); - await new Promise((resolve) => setTimeout(resolve, 100)); - await transport.setDTR(true); - - // flashing successful - alert("Firmware has been flashed!"); + } catch(e) { + const errorMsg = e.message || String(e); + if(errorMsg.includes("not found in firmware")) { + this.flashError = errorMsg; + } else if(errorMsg.includes("Failed to connect") || errorMsg.includes("timeout")) { + this.flashError = "Failed to connect to ESP32 device. Make sure:\n• The device is connected via USB\n• The correct serial port is selected\n• The device is not in use by another program\n• Try pressing the BOOT button on the device"; + } else if(errorMsg.includes("flash")) { + this.flashError = "Flash error: " + errorMsg + "\n\nMake sure you're using the correct firmware file for your device model."; + } else { + this.flashError = "Firmware flashing failed: " + errorMsg; + } + alert(this.flashError); + console.error("Flash error:", e); + } finally { + this.isFlashing = false; + this.flashingStatus = ""; + // close port + if(serialPort) { + try { + await serialPort.close(); + } catch(e) { + console.log("Failed to close serial port:", e); + } + } + } } catch(e) { - alert("Firmware flashing failed: " + e); - console.log(e); - } finally { + const errorMsg = e.message || String(e); + if(errorMsg.includes("No port selected") || errorMsg.includes("NotFoundError")) { + this.flashError = "No device selected. Please select a serial port and try again."; + } else { + this.flashError = "Failed to start flash process: " + errorMsg; + } + alert(this.flashError); this.isFlashing = false; - } - - // close port - console.log("Closing serial port"); - try { - await serialPort.close(); - } catch(e) { - console.log("failed to close serial port, ignoring...", e); + this.flashingStatus = ""; } },