diff --git a/_includes/download/board.html b/_includes/download/board.html index 20bc8a43..44b3f221 100644 --- a/_includes/download/board.html +++ b/_includes/download/board.html @@ -111,6 +111,9 @@ {% for extension in version.extensions %} DOWNLOAD .{{ extension | upcase }} NOW {% endfor %} + {% if page.family == 'esp32s2' or page.family == 'esp32c3' or page.family == 'esp32s3' or page.family == 'esp32' %} + OPEN INSTALLER + {% endif %} {% if version.modules %} diff --git a/_includes/downloads/esp_installer.html b/_includes/downloads/esp_installer.html new file mode 100644 index 00000000..b0581db1 --- /dev/null +++ b/_includes/downloads/esp_installer.html @@ -0,0 +1,97 @@ +
+
+
+ Sorry, Web Serial is not supported on your browser at this time. Browsers we expect to work: +
    +
  • Google Chrome 89 (and higher)
  • +
  • Microsoft Edge 89 (and higher)
  • +
  • Opera 75 (and higher)
  • +
+
+
+
+ ESP32 CircuitPython Installer + This tool will install CircuitPython on ESP32 boards. Click below to see if your board is supported, then follow the instructions to install the firmware. +
+
+
+
    +
  1. +

    + Make sure your board is plugged into this computer via a Serial connection using a USB Cable. +

    +
      +
    • NOTE: A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.

    • +
    +
  2. + +
  3. +

    + + Click this button to open the Web Serial connection menu. +

    + +

    There may be many devices listed, such as your remembered Bluetooth peripherals, anything else plugged into USB, etc.

    + +

    + If you aren't sure which to choose, look for words like "USB", "UART", and "Bridge Controller". There may be more than one right option depending on your system configuration. Experiment if needed. +

    +
  4. + + + + + + + + + + +
+
+ + +
+
diff --git a/_layouts/download.html b/_layouts/download.html index 9b4cf18a..9512261e 100644 --- a/_layouts/download.html +++ b/_layouts/download.html @@ -27,4 +27,8 @@ layout: default +{% if page.family == 'esp32' or page.family == 'esp32s2' or page.family == 'esp32c3' or page.family == 'esp32s3' %} +{% include downloads/esp_installer.html %} + +{% endif %} diff --git a/assets/javascript/espinstaller.js b/assets/javascript/espinstaller.js index e69de29b..a5ea71c3 100644 --- a/assets/javascript/espinstaller.js +++ b/assets/javascript/espinstaller.js @@ -0,0 +1,1034 @@ +'use strict'; + +import * as esptoolPackage from "https://unpkg.com/esp-web-flasher@5.1.2/dist/web/index.js?module" + +// TODO: Figure out how to make the Web Serial from ESPTool and Web Serial to communicate with CircuitPython not conflict +// I think at the very least we'll have to reuse the same port so the user doesn't need to reselct, though it's possible it +// may change after reset. Since it's not +// +// For now, we'll use the following procedure for ESP32-S2 and ESP32-S3: +// 1. Install the bin file +// 2. Reset the board +// (if version 8.0.0-beta.6 or later) +// 3. Generate the settings.toml file +// 4. Write the settings.toml to the board via the REPL +// 5. Reset the board again +// +// For the esp32 and esp32c3, the procedure may be slightly different and going through the +// REPL may be required for the settings.toml file. +// 1. Install the bin file +// 2. Reset the board +// (if version 8.0.0-beta.6 or later) +// 3. Generate the settings.toml file +// 4. Write the settings.toml to the board via the REPL +// 5. Reset the board again +// +// To run REPL code, I may need to modularize the work I did for code.circuitpython.org +// That allows you to run code in the REPL and get the output back. I may end up creating a +// library that uses Web Serial and allows you to run code in the REPL and get the output back +// because it's very integrated into the serial recieve and send code. +// + +let espStub; + +const baudRates = [ + 115200, + 230400, + 460800, + 921600, +]; + +const stage_erase_all = 0x01; +const stage_flash_cpbin = 0x02; +const stage_flash_bootloader = 0x03; +const stage_copy_uf2 = 0x04; +const stage_program_settings = 0x05; + +const full_bin_program = [stage_erase_all, stage_flash_cpbin, stage_program_settings]; +const full_uf2_program = [stage_erase_all, stage_flash_bootloader, stage_copy_uf2, stage_program_settings]; +const factory_reset_program = [stage_erase_all, stage_flash_bootloader]; + +const maxLogLength = 100; +const log = document.getElementById("log"); +const semverLabel = document.getElementById("semver"); +const butShowConsole = document.getElementById("butShowConsole"); +const consoleItems = document.getElementsByClassName("console-item"); +const butConnect = document.getElementById("butConnect"); +const binSelector = document.getElementById("binSelector"); +const baudRate = document.getElementById("baudRate"); +const butClear = document.getElementById("butClear"); +const butProgram = document.getElementById("butProgram"); +const butProgramBootloader = document.getElementById("butProgramBootloader"); +const autoscroll = document.getElementById("autoscroll"); +const lightSS = document.getElementById("light"); +const darkSS = document.getElementById("dark"); +const darkMode = document.getElementById("darkmode"); + +// TODO: This should grab the stuff for settings.toml +const partitionData = document.querySelectorAll(".field input.partition-data"); + +const progress = document.getElementById("progressBar"); +const stepname = document.getElementById("stepname"); +const appDiv = document.getElementById("app"); + +const disableWhileBusy = [partitionData, butProgram, butProgramBootloader, baudRate]; + +let showConsole = false; +let debug; + +// querystring options +const QUERYSTRING_BOARD_KEY = 'board' +const QUERYSTRING_DEBUG_KEY = 'debug' +const QUERYSTRING_STAGING_KEY = 'staging' + +function getFromQuerystring(key) { + const location = new URL(document.location) + const params = new URLSearchParams(location.search) + return params.get(key) +} + +document.addEventListener("DOMContentLoaded", () => { + // detect debug setting from querystring + debug = getFromQuerystring(QUERYSTRING_DEBUG_KEY); + var getArgs = {}; + location.search + .substr(1) + .split("&") + .forEach(function (item) { + getArgs[item.split("=")[0]] = item.split("=")[1]; + }); + if (getArgs["debug"] !== undefined) { + debug = getArgs["debug"] == "1" || getArgs["debug"].toLowerCase() == "true"; + } + + butShowConsole.addEventListener("click", () => { + showConsole = !showConsole + saveSetting("showConsole", showConsole) + toggleConsole(showConsole) + }) + + // register dom event listeners + butConnect.addEventListener("click", () => { + clickConnect().catch(async (e) => { + // Default Help Message: + // if we've failed to catch the message before now, we need to give + // the generic advice: reconnect, refresh, go to support + errorMsg( + `Connection Error, your board may be incompatible. Things to try:\n` + + `1. Reset your board and try again.\n` + + ` - Look for a little black button near the power port.\n` + + `2. Refresh your browser and try again.\n` + + `3. Make sure you are not connected in another browser tab.\n` + + `4. Double-check your board type and serial port selection.\n` + + `5. Post on the Support Forum (link above) with this info:\n\n` + + `"Firmware Tool: ${e}"\n` + ); + await disconnect(); + toggleUIConnected(false); + }); + }); + butClear.addEventListener("click", clickClear); + butProgram.addEventListener("click", clickProgram); + butProgramNvm.addEventListener("click", clickProgramNvm); + for (let i = 0; i < partitionData.length; i++) { + partitionData[i].addEventListener("change", checkProgrammable); + partitionData[i].addEventListener("keydown", checkProgrammable); + partitionData[i].addEventListener("input", checkProgrammable); + } + autoscroll.addEventListener("click", clickAutoscroll); + baudRate.addEventListener("change", changeBaudRate); + darkMode.addEventListener("click", clickDarkMode); + + // handle runaway errors + window.addEventListener("error", event => { + console.warn(`Uncaught error: ${event.error}`); + }); + + // handle runaway rejections + window.addEventListener("unhandledrejection", event => { + console.warn(`Unhandled rejection: ${event.reason}`); + }); + + // WebSerial feature detection + if ("serial" in navigator) { + const notSupported = document.getElementById("notSupported"); + notSupported.classList.add("hidden"); + } + + initBinSelector(); + initBaudRate(); + loadAllSettings(); + updateTheme(); + logMsg("CircuitPython ESP32 Installer loaded."); + checkProgrammable(); +}); + +function createOption(value, text) { + const option = document.createElement("option"); + option.text = text; + option.value = value; + return option; +} + +let latestFirmwares = [] +async function initBinSelector() { + // fetch firmware index from io-rails, a list of available littlefs + // firmware items, like the example above + const response = await fetch(`${FIRMWARE_API}/wipper_releases`) + // extract the semver from the custom header + if (!initSemver(response.headers.get('AIO-WS-Firmware-Semver'))) { + console.error("No semver information in the response headers!") + } + // parse and store firmware data for reuse + latestFirmwares = await(response.json()) + + // populate the bin select element + populateBinSelector("Click Here to Find Your Board:") + + // pull default board id out of querystring + if (setDefaultBoard()) { + // inject board name into alternate step 1 + const boardNameItems = document.getElementsByClassName('selected-board-name') + for (let idx = 0; idx < boardNameItems.length; idx++) { + boardNameItems[idx].innerHTML = binSelector.selectedOptions[0].text; + } + // show alternate step 1 + showAltStepOne() + } else { + binSelector.addEventListener("change", changeBin); + } +} + +function populateBinSelector(title, filter=() => true) { + binSelector.innerHTML = ''; + + const filteredFirmwares = latestFirmwares.filter(filter) + const any = filteredFirmwares.length > 0 + + binSelector.add(createOption(null, any ? title : 'No Compatible Boards')) + + filteredFirmwares.forEach(firmware => { + binSelector.add(createOption(firmware.id, firmware.name)); + }) + + return any +} + +function returnToStepOne() { + showStep(1, { hideHigherSteps: false }); + doThingOnClass("add", "dimmed", "step-2") + // yellow fade like 2005 + setTimeout(() => doThingOnClass("add", "highlight", "step-1"), 0) + setTimeout(() => doThingOnClass("remove", "highlight", "step-1"), 1500) + doThingOnClass("add", "hidden", "step-1 alt") +} + +function showAltStepOne() { + doThingOnClass("add", "hidden", "step-1") + doThingOnClass("remove", "hidden", "step-1 alt") +} + +function doThingOnClass(method, thing, classSelector) { + const classItems = document.getElementsByClassName(classSelector) + for (let idx = 0; idx < classItems.length; idx++) { + classItems.item(idx).classList[method](thing) + } +} + +function setDefaultBoard() { + const board = getFromQuerystring(QUERYSTRING_BOARD_KEY) + if (board && hasBoard(board)) { + binSelector.value = board + showStep(2, { dimLowerSteps: false }) + return true + } +} + +function hasBoard(board) { + for (let opt of binSelector.options) { + if (opt.value == board) { return opt } + } +} + +function changeBin(evt) { + (evt.target.value && evt.target.value != "null") ? + showStep(2) : + hideStep(2) +} + +function showStep(stepNumber, options={}) { + const dimLowerSteps = !(options.dimLowerSteps === false) + const hideHigherSteps = !(options.hideHigherSteps === false) + // reveal the new step + doThingOnClass("remove", "hidden", `step-${stepNumber}`) + doThingOnClass("remove", "dimmed", `step-${stepNumber}`) + + if (dimLowerSteps) { + for (let step = stepNumber - 1; step > 0; step--) { + doThingOnClass("add", "dimmed", `step-${step}`) + } + } + + if (hideHigherSteps) { + for (let step = stepNumber + 1; step <= 6; step++) { + doThingOnClass("add", "hidden", `step-${step}`) + } + } + + // per-step things, like a state machine + switch(stepNumber) { + case 3: + checkProgrammable() + break; + case 4: + butProgram.disabled = false + butProgramNvm.disabled = false + break; + } + + // scroll to the bottom next frame + setTimeout((() => appDiv.scrollTop = appDiv.scrollHeight), 0) +} + +function hideStep(stepNumber) { + doThingOnClass("add", "hidden", `step-${stepNumber}`) +} + +function toggleConsole(show) { + // hide/show the console log and its widgets + const consoleItemsMethod = show ? "remove" : "add" + for (let idx = 0; idx < consoleItems.length; idx++) { + consoleItems.item(idx).classList[consoleItemsMethod]("hidden") + } + // toggle the button + butShowConsole.checked = show + // tell the app if it's sharing space with the console + const appDivMethod = show ? "add" : "remove" + appDiv.classList[appDivMethod]("with-console") + + // scroll both to the bottom a moment after adding + setTimeout(() => { + log.scrollTop = log.scrollHeight + appDiv.scrollTop = appDiv.scrollHeight + }, 200) +} + +let semver +function initSemver(newSemver) { + if (!newSemver) { return } + + semver = newSemver + semverLabel.innerHTML = semver + + return true +} + +function lookupFirmwareByBinSelector() { + // get the currently selected board id + const selectedId = binSelector.value + if (!selectedId || selectedId === 'null') { throw new Error("No board selected.") } + + // grab the stored firmware settings for this id + let selectedFirmware + for (let firmware of latestFirmwares) { + if (firmware.id === selectedId) { + selectedFirmware = firmware + break + } + } + + if (!selectedFirmware) { + const { text, value } = binSelector.selectedOptions[0] + throw new Error(`No firmware entry for: ${text} (${value})`) + } + + return selectedFirmware +} + +function initBaudRate() { + for (let rate of baudRates) { + baudRate.add(createOption(rate, `${rate} Baud`)); + } +} + +let lastPercent = 0; + +/** + * @name disconnect + * Closes the Web Serial connection. + */ +async function disconnect() { + toggleUIToolbar(false); + if (espStub) { + await espStub.disconnect(); + await espStub.port.close(); + toggleUIConnected(false); + espStub = undefined; + } +} + +function logMsg(text) { + log.innerHTML += text.replaceAll("\n", "
") + "
"; + + // Remove old log content + if (log.textContent.split("\n").length > maxLogLength + 1) { + let logLines = log.innerHTML.replace(/(\n)/gm, "").split("
"); + log.innerHTML = logLines.splice(-maxLogLength).join("
\n"); + } + + if (autoscroll.checked) { + log.scrollTop = log.scrollHeight; + } +} + +function debugMsg(...args) { + function getStackTrace() { + let stack = new Error().stack; + stack = stack.split("\n").map((v) => v.trim()); + for (let i = 0; i < 3; i++) { + stack.shift(); + } + + let trace = []; + for (let line of stack) { + line = line.replace("at ", ""); + trace.push({ + func: line.substr(0, line.indexOf("(") - 1), + pos: line.substring(line.indexOf(".js:") + 4, line.lastIndexOf(":")), + }); + } + + return trace; + } + + let stack = getStackTrace(); + stack.shift(); + let top = stack.shift(); + let prefix = '[' + top.func + ":" + top.pos + "] "; + for (let arg of args) { + if (typeof arg == "string") { + logMsg(prefix + arg); + } else if (typeof arg == "number") { + logMsg(prefix + arg); + } else if (typeof arg == "boolean") { + logMsg(prefix + arg ? "true" : "false"); + } else if (Array.isArray(arg)) { + logMsg(prefix + "[" + arg.map((value) => espStub.toHex(value)).join(", ") + "]"); + } else if (typeof arg == "object" && arg instanceof Uint8Array) { + logMsg( + prefix + + "[" + + Array.from(arg) + .map((value) => espStub.toHex(value)) + .join(", ") + + "]" + ); + } else { + logMsg(prefix + "Unhandled type of argument:" + typeof arg); + console.log(arg); + } + prefix = ""; // Only show for first argument + } +} + +function errorMsg(text, forwardLink=null) { + // regular log with red Error: prefix + logMsg('Error: ' + text); + // strip html for console and alerts + const strippedText = text.replaceAll(/<.*?>/g, "") + // all errors go to the browser dev console + console.error(strippedText); + // Make sure user sees the error if the log is closed + if (!showConsole) { + if (forwardLink) { + if (confirm(`${strippedText}\nClick 'OK' to be forwarded there now.`)) { + document.location = forwardLink + } + } else { + alert(strippedText) + } + } +} + +function formatMacAddr(macAddr) { + return macAddr.map((value) => value.toString(16).toUpperCase().padStart(2, "0")).join(":"); +} + +/** + * @name updateTheme + * Sets the theme to Adafruit (dark) mode. Can be refactored later for more themes + */ +function updateTheme() { + // Disable all themes + document.querySelectorAll("link[rel=stylesheet].alternate").forEach((styleSheet) => { + enableStyleSheet(styleSheet, false); + }); + + if (darkMode.checked) { + enableStyleSheet(darkSS, true); + } else { + enableStyleSheet(lightSS, true); + } +} + +function enableStyleSheet(node, enabled) { + node.disabled = !enabled; +} + +/** + * @name reset + * Reset the Panels, Log, and associated data + */ +async function reset() { + // Clear the log + log.innerHTML = ""; +} + +/** + * @name clickConnect + * Click handler for the connect/disconnect button. + */ +async function clickConnect() { + await disconnect(); + + butConnect.textContent = "Connecting..."; + butConnect.disabled = true + + const esploader = await esptoolPackage.connect({ + log: (...args) => logMsg(...args), + debug: debug ? (...args) => debugMsg(...args) : (...args) => {}, + error: (...args) => errorMsg(...args), + }); + + try { + await esploader.initialize(); + + const chipType = esploader.chipFamily; + const chipName = esploader.chipName; + toggleUIConnected(true); + toggleUIToolbar(true); + appDiv.classList.add("connected"); + + logMsg("Connected to " + esploader.chipName); + logMsg("MAC Address: " + formatMacAddr(esploader.macAddr())); + + const nextStepCallback = async () => { + showStep(3) + espStub = await esploader.runStub(); + espStub.addEventListener("disconnect", () => { + toggleUIConnected(false); + espStub = false; + }); + await setBaudRateIfChipSupports(chipType); + } + + // check chip compatibility + if (checkChipTypeMatchesSelectedBoard(chipType)) { + await nextStepCallback() + return + } + + // not compatible, grab the board name for messaging... + const boardName = lookupFirmwareByBinSelector().name + // ...and reset the selector to only compatible boards, if any! + const any = populateBinSelector(`Possible ${chipName} Boards:`, firmware => { + return (BOARD_TO_CHIP_MAP[firmware.id] == chipType) + }) + + if (any) { + // there are compatible boards available + // reset the bin selector + binSelector.disabled = false + binSelector.removeEventListener("change", changeBin); + binSelector.addEventListener("change", async evt => { + // upon compatible board selection, reveal next step + if (evt.target.value && evt.target.value != "null" && checkChipTypeMatchesSelectedBoard(chipType)) { + logMsg(`Compatible board selected: ${boardName}`) + await nextStepCallback() + } + }); + + // explain all this to the user + errorMsg(`Oops, wrong board!\n` + + `- you selected: ${boardName}\n` + + `- you connected: ${chipName}\n` + + `You can:\n` + + `- go back to Step 1 and select a compatible board\n` + + `- connect a different board and refresh the browser`) + + // reveal step one + returnToStepOne() + return + } + + // no compatible boards available + // explain to the user with a link to the appropriate guide + errorMsg(`Oops! This tool doesn't support your board, ${chipName}, but WipperSnapper still might!\n` + + `Visit the quick-start guide for a list of supported boards and their install instructions.`, QUICK_START_LINK) + // can't use it so disconnect now + await disconnect() + + } catch (err) { + await esploader.disconnect(); + // Disconnection before complete + toggleUIConnected(false); + showStep(2, { hideHigherSteps: true }) + errorMsg("Oops, we lost connection to your board before completing the install. Please check your USB connection and click Connect again. Refresh the browser if it becomes unresponsive.") + } +} + +function checkChipTypeMatchesSelectedBoard(chipType, boardId=null) { + // allow overriding which board we're checking against + boardId = boardId || binSelector.value + // wrap the lookup + return (BOARD_TO_CHIP_MAP[boardId] == chipType) +} + +async function setBaudRateIfChipSupports(chipType) { + const baud = parseInt(baudRate.value); + if (baud == espStub.ESP_ROM_BAUD) { return } // already the default + + if (chipType == espStub.ESP32) { // only supports the default + logMsg("WARNING: ESP32 is having issues working at speeds faster than 115200. Continuing at 115200 for now..."); + return + } + + await changeBaudRate(baud); +} + +/** + * @name changeBaudRate + * Change handler for the Baud Rate selector. + */ +async function changeBaudRate() { + saveSetting("baudrate", baudRate.value); + if (espStub) { + let baud = parseInt(baudRate.value); + if (baudRates.includes(baud)) { + await espStub.setBaudrate(baud); + } + } +} + +/** + * @name clickAutoscroll + * Change handler for the Autoscroll checkbox. + */ +async function clickAutoscroll() { + saveSetting("autoscroll", autoscroll.checked); +} + +/** + * @name clickDarkMode + * Change handler for the Dark Mode checkbox. + */ +async function clickDarkMode() { + updateTheme(); + saveSetting("darkmode", darkMode.checked); +} + +/** + * @name clickProgram + * Click handler for the program button. + */ +async function clickProgram() { + await programScript(full_bin_program); +} + +/** + * @name clickProgramNvm + * Click handler for the program button. + */ +async function clickProgramNvm() { + await programScript(factory_reset_program); +} + +function stagingFlagSet() { + return getFromQuerystring(QUERYSTRING_STAGING_KEY) +} + +async function populateSecretsFile(path) { + let response = await fetch(path); + let contents = await response.json(); + + // Get the secrets data + for (let field of getValidFields()) { + const { id, value } = partitionData[field] + if(id === "status_pixel_brightness") { + const floatValue = parseFloat(value) + updateObject(contents, id, isNaN(floatValue) ? 0.2 : floatValue); + } else { + updateObject(contents, id, value); + } + } + + // add "io_url" property to json root with the staging url override + if(stagingFlagSet()) { + updateObject(contents, 'io_url', 'io.adafruit.us') + } + // Convert the data to text and return + return JSON.stringify(contents, null, 4); +} + +function updateObject(obj, path, value) { + if (typeof obj === "undefined") { + return false; + } + + var _index = path.indexOf("."); + if (_index > -1) { + return updateObject(obj[path.substring(0, _index)], path.substr(_index + 1), value); + } + + obj[path] = value; +} + + +let chipFiles +async function fetchFirmwareForSelectedBoard() { + const firmware = lookupFirmwareByBinSelector() + + logMsg(`Fetching latest firmware...`) + const response = await fetch(`${FIRMWARE_API}/wipper_releases/${firmware.id}`, { + headers: { Accept: 'application/octet-stream' } + }) + + // Zip stuff + logMsg("Unzipping firmware bundle...") + const blob = await response.blob() + const reader = new zip.ZipReader(new zip.BlobReader(blob)); + + // unzip into local file cache + chipFiles = await reader.getEntries(); +} + +const BASE_SETTINGS = { + files: [ + { + filename: "secrets.json", + callback: populateSecretsFile, + }, + ], + rootFolder: "files", +}; + +function findInZip(filename) { + const regex = RegExp(filename.replace("VERSION", "(.*)")) + for (let i = 0; i < chipFiles.length; i++) { + if (chipFiles[i].filename.match(regex)) { + return chipFiles[i] + } + } +} + +async function mergeSettings() { + const { settings } = lookupFirmwareByBinSelector() + + const transformedSettings = { + ...settings, + // convert the offset value from hex string to number + offset: parseInt(settings.offset, 16), + // replace the structure object with one where the keys have been converted + // from hex strings to numbers + structure: Object.keys(settings.structure).reduce((newObj, hexString) => { + // new object, converted key (hex string -> numeric), same value + newObj[parseInt(hexString, 16)] = settings.structure[hexString] + + return newObj + }, {}) + } + + // merge with the defaults and send back + return { + ...BASE_SETTINGS, + ...transformedSettings + } +} + +async function programScript(stages) { + butProgram.disabled = true + butProgramNvm.disabled = true + try { + await fetchFirmwareForSelectedBoard() + } catch(error) { + errorMsg(error.message) + return + } + + // pretty print the settings object with VERSION placeholders filled + const settings = await mergeSettings() + const settingsString = JSON.stringify(settings, null, 2) + const strippedSettings = settingsString.replaceAll('VERSION', semver) + logMsg(`Flashing with settings:
${strippedSettings}
`) + + let steps = []; + for (let i = 0; i < stages.length; i++) { + if (stages[i] == stage_erase_all) { + steps.push({ + name: "Erasing Flash", + func: async function () { + await espStub.eraseFlash(); + }, + params: {}, + }); + } else if (stages[i] == stage_flash_cpbin) { + for (const [offset, filename] of Object.entries(settings.structure)) { + steps.push({ + name: "Flashing " + filename.replace('VERSION', semver), + func: async function (params) { + const firmware = await getFirmware(params.filename); + const progressBar = progress.querySelector("div"); + lastPercent = 0; + await espStub.flashData( + firmware, + (bytesWritten, totalBytes + ) => { + let percentage = Math.floor((bytesWritten / totalBytes) * 100) + if (percentage != lastPercent) { + logMsg(`${percentage}% (${bytesWritten}/${totalBytes})...`); + lastPercent = percentage; + } + progressBar.style.width = percentage + "%"; + }, + params.offset, + 0 + ); + }, + params: { + filename: filename, + offset: offset, + }, + }); + } + } else if (stages[i] == stage_flash_bootloader) { + for (const [offset, filename] of Object.entries(settings.structure)) { + steps.push({ + name: "Flashing " + filename.replace('VERSION', semver), + func: async function (params) { + const firmware = await getFirmware(params.filename); + const progressBar = progress.querySelector("div"); + lastPercent = 0; + await espStub.flashData( + firmware, + (bytesWritten, totalBytes + ) => { + let percentage = Math.floor((bytesWritten / totalBytes) * 100) + if (percentage != lastPercent) { + logMsg(`${percentage}% (${bytesWritten}/${totalBytes})...`); + lastPercent = percentage; + } + progressBar.style.width = percentage + "%"; + }, + params.offset, + 0 + ); + }, + params: { + filename: filename, + offset: offset, + }, + }); + } + } else if (stages[i] == stage_program_settings) { + // TODO: This needs to be rewritten to talk with circuitpython + steps.push({ + name: "Generating and Flashing LittleFS Partition", + func: async function (params) { + let fileSystemImage = await generate(params.flashParams); + + if (DO_DOWNLOAD) { + // Download the Partition + var blob = new Blob([new Uint8Array(fileSystemImage)], { + type: "application/octet-stream", + }); + var link = document.createElement("a"); + link.href = window.URL.createObjectURL(blob); + link.download = "littleFS.bin"; + link.click(); + link.remove(); + } else { + const progressBar = progress.querySelector("div"); + lastPercent = 0; + await espStub.flashData( + new Uint8Array(fileSystemImage).buffer, + (bytesWritten, totalBytes) => { + let percentage = Math.floor((bytesWritten / totalBytes) * 100) + if (percentage != lastPercent) { + logMsg(`${percentage}% (${bytesWritten}/${totalBytes})...`); + lastPercent = percentage; + } + progressBar.style.width = percentage + "%"; + }, + params.flashParams.offset, + 0 + ); + } + }, + params: { + flashParams: settings, + }, + }); + } + } + + for (let i = 0; i < disableWhileBusy.length; i++) { + if (Array.isArray(disableWhileBusy[i])) { + for (let j = 0; j < disableWhileBusy[i].length; i++) { + disableWhileBusy[i][j].disable = true; + } + } else { + disableWhileBusy[i].disable = true; + } + } + + progress.classList.remove("hidden"); + stepname.classList.remove("hidden"); + showStep(5) + + for (let i = 0; i < steps.length; i++) { + stepname.innerText = steps[i].name + " (" + (i + 1) + "/" + steps.length + ")..."; + await steps[i].func(steps[i].params); + } + + stepname.classList.add("hidden"); + stepname.innerText = ""; + progress.classList.add("hidden"); + progress.querySelector("div").style.width = "0"; + + for (let i = 0; i < disableWhileBusy.length; i++) { + if (Array.isArray(disableWhileBusy[i])) { + for (let j = 0; j < disableWhileBusy[i].length; i++) { + disableWhileBusy[i][j].disable = false; + } + } else { + disableWhileBusy[i].disable = false; + } + } + + checkProgrammable(); + await disconnect(); + logMsg("To run the new firmware, please reset your device."); + showStep(6); +} + +function getValidFields() { + // Validate user inputs + const validFields = []; + for (let i = 0; i < 5; i++) { + const { id, value } = partitionData[i] + // password & brightness can be blank, the rest must have some value + if (id === "network_type_wifi.network_password" || + id === "status_pixel_brightness" || + value.length > 0) { + validFields.push(i); + } + } + return validFields; +} + +/** + * @name checkProgrammable + * Check if the conditions to program the device are sufficient + */ +async function checkProgrammable() { + if (getValidFields().length < 5) { + hideStep(4) + } else { + showStep(4, { dimLowerSteps: false }) + } +} + +/** + * @name checkFirmware + * Handler for firmware upload changes + */ +async function checkFirmware(event) { + let filename = event.target.value.split("\\").pop(); + let label = event.target.parentNode.querySelector("span"); + let icon = event.target.parentNode.querySelector("svg"); + if (filename != "") { + if (filename.length > 17) { + label.innerHTML = filename.substring(0, 14) + "…"; + } else { + label.innerHTML = filename; + } + icon.classList.add("hidden"); + } else { + label.innerHTML = "Choose a file…"; + icon.classList.remove("hidden"); + } + + await checkProgrammable(); +} + +/** + * @name clickClear + * Click handler for the clear button. + */ +async function clickClear() { + reset(); +} + +function toggleUIToolbar(show) { + for (let i = 0; i < 4; i++) { + progress.classList.add("hidden"); + progress.querySelector("div").style.width = "0"; + } + if (show) { + appDiv.classList.add("connected"); + } else { + appDiv.classList.remove("connected"); + } +} + +function toggleUIConnected(connected) { + let lbl = "Connect"; + if (connected) { + lbl = "Connected"; + butConnect.disabled = true + binSelector.disabled = true + } else { + toggleUIToolbar(false); + butConnect.disabled = false + binSelector.disabled = false + } + butConnect.textContent = lbl; +} + +function loadAllSettings() { + // Load all saved settings or defaults + autoscroll.checked = loadSetting("autoscroll", true); + baudRate.value = loadSetting("baudrate", baudRates[0]); + darkMode.checked = loadSetting("darkmode", false); + showConsole = loadSetting('showConsole', false); + toggleConsole(showConsole); +} + +function loadSetting(setting, defaultValue) { + return JSON.parse(window.localStorage.getItem(setting)) || defaultValue; +} + +function saveSetting(setting, value) { + window.localStorage.setItem(setting, JSON.stringify(value)); +} + +async function getFirmware(filename) { + const file = findInZip(filename) + + if (!file) { + const msg = `No firmware file name ${filename} found in the zip!` + errorMsg(msg) + throw new Error(msg) + } + + logMsg(`Unzipping ${filename.replace('VERSION', semver)}...`) + const firmwareFile = await file.getData(new zip.Uint8ArrayWriter()) + + return firmwareFile.buffer // ESPTool wants an ArrayBuffer +} + +async function getFileText(path) { + let response = await fetch(path); + let contents = await response.text(); + return contents; +}