diff --git a/dist/base_installer.js b/dist/base_installer.js new file mode 100644 index 0000000..84a2f13 --- /dev/null +++ b/dist/base_installer.js @@ -0,0 +1,465 @@ +// SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +'use strict'; +import {html, render} from 'https://unpkg.com/lit-html?module'; +import {asyncAppend} from 'https://unpkg.com/lit-html/directives/async-append?module'; +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. +// + +export const ESP_ROM_BAUD = 115200; + +export class InstallButton extends HTMLButtonElement { + static isSupported = 'serial' in navigator; + static isAllowed = window.isSecureContext; + + constructor() { + super(); + this.dialogElements = {}; + this.currentFlow = null; + this.currentStep = 0; + this.currentDialogElement = null; + this.port = null; + this.espStub = null; + this.dialogCssClass = "install-dialog"; + this.connected = this.connectionStates.DISCONNECTED; + this.menuTitle = "Installer Menu"; + } + + init() { + this.preloadDialogs(); + } + + // Define some common buttons + /* Buttons should have a label, and a callback and optionally a condition function on whether they should be enabled */ + previousButton = { + label: "Previous", + onClick: this.prevStep, + isEnabled: async () => { return this.currentStep > 0 }, + } + + nextButton = { + label: "Next", + onClick: this.nextStep, + isEnabled: async () => { return this.currentStep < this.currentFlow.steps.length - 1; }, + } + + closeButton = { + label: "Close", + onClick: async (e) => { + this.closeDialog(); + }, + } + + // Default Buttons + defaultButtons = [this.previousButton, this.nextButton]; + + // States and Button Labels + connectionStates = { + DISCONNECTED: "Connect", + CONNECTING: "Connecting...", + CONNECTED: "Disconnect", + } + + dialogs = { + notSupported: { + preload: false, + closeable: true, + template: (data) => html` + Sorry, Web Serial is not supported on your browser at this time. Browsers we expect to work: + + `, + buttons: [this.closeButton], + }, + menu: { + closeable: true, + template: (data) => html` +

${this.menuTitle}

+ `, + buttons: [this.closeButton], + }, + }; + + flows = {}; + + baudRates = [ + 115200, + 128000, + 153600, + 230400, + 460800, + 921600, + 1500000, + 2000000, + ]; + + connectedCallback() { + if (InstallButton.isSupported && InstallButton.isAllowed) { + this.toggleAttribute("install-supported", true); + } else { + this.toggleAttribute("install-unsupported", true); + } + + this.addEventListener("click", async (e) => { + e.preventDefault(); + // WebSerial feature detection + if (!InstallButton.isSupported) { + await this.showNotSupported(); + } else { + await this.showMenu(); + } + }); + } + + // Parse out the url parameters from the current url + getUrlParams() { + // This should look for and validate very specific values + var hashParams = {}; + if (location.hash) { + location.hash.substr(1).split("&").forEach(function(item) {hashParams[item.split("=")[0]] = item.split("=")[1];}); + } + return hashParams; + } + + // Get a url parameter by name and optionally remove it from the current url in the process + getUrlParam(name) { + let urlParams = this.getUrlParams(); + let paramValue = null; + if (name in urlParams) { + paramValue = urlParams[name]; + } + + return paramValue; + } + + async enabledFlowCount() { + let enabledFlowCount = 0; + for (const [flowId, flow] of Object.entries(this.flows)) { + if (await flow.isEnabled()) { + enabledFlowCount++; + } + } + return enabledFlowCount; + } + + async * generateMenu(templateFunc) { + if (await this.enabledFlowCount() == 0) { + yield html`
  • No installable options available for this board.
  • `; + } + for (const [flowId, flow] of Object.entries(this.flows)) { + if (await flow.isEnabled()) { + yield templateFunc(flowId, flow); + } + } + } + + preloadDialogs() { + for (const [id, dialog] of Object.entries(this.dialogs)) { + if ('preload' in dialog && !dialog.preload) { + continue; + } + this.dialogElements[id] = this.getDialogElement(dialog); + } + } + + createIdFromLabel(text) { + return text.replace(/^[^a-z]+|[^\w:.-]+/gi, ""); + } + + createDialogElement(id, dialogData) { + // Check if an existing dialog with the same id exists and remove it if so + let existingDialog = this.querySelector(`#cp-installer-${id}`); + if (existingDialog) { + this.remove(existingDialog); + } + + // Create a dialog element + let dialogElement = document.createElement("dialog"); + dialogElement.id = id; + dialogElement.classList.add(this.dialogCssClass); + + // Add a close button + let closeButton = document.createElement("button"); + closeButton.href = "#"; + closeButton.classList.add("close-button"); + closeButton.addEventListener("click", (e) => { + e.preventDefault(); + dialogElement.close(); + }); + dialogElement.appendChild(closeButton); + + // Add a body element + let body = document.createElement("div"); + body.classList.add("dialog-body"); + dialogElement.appendChild(body); + + let buttons = this.defaultButtons; + if (dialogData && dialogData.buttons) { + buttons = dialogData.buttons; + } + + dialogElement.appendChild( + this.createNavigation(buttons) + ); + + // Return the dialog element + document.body.appendChild(dialogElement); + return dialogElement; + } + + createNavigation(buttonData) { + // Add buttons according to config data + const navigation = document.createElement("div"); + navigation.classList.add("dialog-navigation"); + + for (const button of buttonData) { + let buttonElement = document.createElement("button"); + buttonElement.innerText = button.label; + buttonElement.id = this.createIdFromLabel(button.label); + buttonElement.addEventListener("click", async (e) => { + e.preventDefault(); + await button.onClick.bind(this)(); + }); + buttonElement.addEventListener("update", async (e) => { + if ("onUpdate" in button) { + await button.onUpdate.bind(this)(e); + } + if ("isEnabled" in button) { + e.target.disabled = !(await button.isEnabled.bind(this)()); + } + }); + + navigation.appendChild(buttonElement); + } + + return navigation; + } + + getDialogElement(dialog, forceReload = false) { + function getKeyByValue(object, value) { + return Object.keys(object).find(key => object[key] === value); + } + + const dialogId = getKeyByValue(this.dialogs, dialog); + + if (dialogId) { + if (dialogId in this.dialogElements && !forceReload) { + return this.dialogElements[dialogId]; + } else { + return this.createDialogElement(dialogId, dialog); + } + } + return null; + } + + updateButtons() { + // Call each button's custom update event for the current dialog + if (this.currentDialogElement) { + const navButtons = this.currentDialogElement.querySelectorAll(".dialog-navigation button"); + for (const button of navButtons) { + button.dispatchEvent(new Event("update")); + } + } + } + + showDialog(dialog, templateData = {}) { + if (this.currentDialogElement) { + this.closeDialog(); + } + + this.currentDialogElement = this.getDialogElement(dialog); + if (!this.currentDialogElement) { + console.error(`Dialog not found`); + } + + if (this.currentDialogElement) { + const dialogBody = this.currentDialogElement.querySelector(".dialog-body"); + if ('template' in dialog) { + render(dialog.template(templateData), dialogBody); + } + + // Close button should probably hide during certain steps such as flashing and erasing + if ("closeable" in dialog && dialog.closeable) { + this.currentDialogElement.querySelector(".close-button").style.display = "block"; + } else { + this.currentDialogElement.querySelector(".close-button").style.display = "none"; + } + + let dialogButtons = this.defaultButtons; + if ('buttons' in dialog) { + dialogButtons = dialog.buttons; + } + + this.updateButtons(); + this.currentDialogElement.showModal(); + } + } + + closeDialog() { + this.currentDialogElement.close(); + this.currentDialogElement = null; + } + + errorMsg(text) { + text = this.stripHtml(text); + console.error(text); + this.showError(text); + } + + logMsg(text, showTrace = false) { + // TODO: Eventually add to an internal log that the user can bring up + console.info(this.stripHtml(text)); + if (showTrace) { + console.trace(); + } + } + + updateEspConnected(connected) { + if (Object.values(this.connectionStates).includes(connected)) { + this.connected = connected; + this.updateButtons(); + } + } + + stripHtml(html) { + let tmp = document.createElement("div"); + tmp.innerHTML = html; + return tmp.textContent || tmp.innerText || ""; + } + + formatMacAddr(macAddr) { + return macAddr.map((value) => value.toString(16).toUpperCase().padStart(2, "0")).join(":"); + } + + async disconnect() { + if (this.espStub) { + await espStub.disconnect(); + await espStub.port.close(); + this.updateUIConnected(this.connectionStates.DISCONNECTED); + this.espStub = null; + } + } + + async runFlow(flow) { + if (flow instanceof Event) { + flow.preventDefault(); + flow.stopImmediatePropagation(); + if (flow.target.id in this.flows) { + flow = this.flows[flow.target.id]; + } else { + return; + } + } + + this.currentFlow = flow; + this.currentStep = 0; + await this.currentFlow.steps[this.currentStep].bind(this)(); + } + + async nextStep() { + if (!this.currentFlow) { + return; + } + + if (this.currentStep < this.currentFlow.steps.length) { + this.currentStep++; + await this.currentFlow.steps[this.currentStep].bind(this)(); + } + } + + async prevStep() { + if (!this.currentFlow) { + return; + } + + if (this.currentStep > 0) { + this.currentStep--; + await this.currentFlow.steps[this.currentStep].bind(this)(); + } + } + + async showMenu() { + // Display Menu + this.showDialog(this.dialogs.menu); + } + + async showNotSupported() { + // Display Not Supported Message + this.showDialog(this.dialogs.notSupported); + } + + async showError(message) { + // Display Menu + this.showDialog(this.dialogs.error, {message: message}); + } + + async setBaudRateIfChipSupports(chipType, baud) { + if (baud == ESP_ROM_BAUD) { return } // already the default + + if (chipType == esptoolPackage.CHIP_FAMILY_ESP32) { // only supports the default + this.logMsg(`ESP32 Chip only works at 115200 instead of the preferred ${baud}. Staying at 115200...`); + return + } + + await this.changeBaudRate(baud); + } + + async changeBaudRate(baud) { + if (this.espStub && this.baudRates.includes(baud)) { + await this.espStub.setBaudrate(baud); + } + } + + async espHardReset(bootloader = false) { + if (this.espStub) { + await this.espStub.hardReset(bootloader); + } + } + + async espConnect(logger) { + // - Request a port and open a connection. + this.port = await navigator.serial.requestPort(); + + logger.log("Connecting..."); + await this.port.open({ baudRate: ESP_ROM_BAUD }); + + logger.log("Connected successfully."); + + return new esptoolPackage.ESPLoader(this.port, logger); + }; +} \ No newline at end of file diff --git a/dist/base_installer.min.js b/dist/base_installer.min.js new file mode 100644 index 0000000..16eeb23 --- /dev/null +++ b/dist/base_installer.min.js @@ -0,0 +1,12 @@ +"use strict";import{html,render}from"https://unpkg.com/lit-html?module";import{asyncAppend}from"https://unpkg.com/lit-html/directives/async-append?module";import*as esptoolPackage from"https://unpkg.com/esp-web-flasher@5.1.2/dist/web/index.js?module";const ESP_ROM_BAUD=115200;class InstallButton extends HTMLButtonElement{static isSupported="serial"in navigator;static isAllowed=window.isSecureContext;constructor(){super(),this.dialogElements={},this.currentFlow=null,this.currentStep=0,this.currentDialogElement=null,this.port=null,this.espStub=null,this.dialogCssClass="install-dialog",this.connected=this.connectionStates.DISCONNECTED,this.menuTitle="Installer Menu"}init(){this.preloadDialogs()}previousButton={label:"Previous",onClick:this.prevStep,isEnabled:async()=>0this.currentStep{this.closeDialog()}};defaultButtons=[this.previousButton,this.nextButton];connectionStates={DISCONNECTED:"Connect",CONNECTING:"Connecting...",CONNECTED:"Disconnect"};dialogs={notSupported:{preload:!1,closeable:!0,template:t=>html` + Sorry, Web Serial is not supported on your browser at this time. Browsers we expect to work: + + `,buttons:[this.closeButton]},menu:{closeable:!0,template:t=>html` +

    ${this.menuTitle}

    + `,buttons:[this.closeButton]}};flows={};baudRates=[115200,128e3,153600,230400,460800,921600,15e5,2e6];connectedCallback(){InstallButton.isSupported&&InstallButton.isAllowed?this.toggleAttribute("install-supported",!0):this.toggleAttribute("install-unsupported",!0),this.addEventListener("click",async t=>{t.preventDefault(),InstallButton.isSupported?await this.showMenu():await this.showNotSupported()})}getUrlParams(){var e={};return location.hash&&location.hash.substr(1).split("&").forEach(function(t){e[t.split("=")[0]]=t.split("=")[1]}),e}getUrlParam(t){var e=this.getUrlParams();let s=null;return s=t in e?e[t]:s}async enabledFlowCount(){let t=0;for(var[e,s]of Object.entries(this.flows))await s.isEnabled()&&t++;return t}async*generateMenu(t){0==await this.enabledFlowCount()&&(yield html`
  • No installable options available for this board.
  • `);for(var[e,s]of Object.entries(this.flows))await s.isEnabled()&&(yield t(e,s))}preloadDialogs(){for(var[t,e]of Object.entries(this.dialogs))"preload"in e&&!e.preload||(this.dialogElements[t]=this.getDialogElement(e))}createIdFromLabel(t){return t.replace(/^[^a-z]+|[^\w:.-]+/gi,"")}createDialogElement(t,e){var s=this.querySelector("#cp-installer-"+t);s&&this.remove(s);let i=document.createElement("dialog");i.id=t,i.classList.add(this.dialogCssClass);s=document.createElement("button"),s.href="#",s.classList.add("close-button"),s.addEventListener("click",t=>{t.preventDefault(),i.close()}),i.appendChild(s),t=document.createElement("div");t.classList.add("dialog-body"),i.appendChild(t);let n=this.defaultButtons;return e&&e.buttons&&(n=e.buttons),i.appendChild(this.createNavigation(n)),document.body.appendChild(i),i}createNavigation(t){var e=document.createElement("div");e.classList.add("dialog-navigation");for(const i of t){var s=document.createElement("button");s.innerText=i.label,s.id=this.createIdFromLabel(i.label),s.addEventListener("click",async t=>{t.preventDefault(),await i.onClick.bind(this)()}),s.addEventListener("update",async t=>{"onUpdate"in i&&await i.onUpdate.bind(this)(t),"isEnabled"in i&&(t.target.disabled=!await i.isEnabled.bind(this)())}),e.appendChild(s)}return e}getDialogElement(t,e=!1){s=this.dialogs,i=t;var s,i,n=Object.keys(s).find(t=>s[t]===i);return n?n in this.dialogElements&&!e?this.dialogElements[n]:this.createDialogElement(n,t):null}updateButtons(){if(this.currentDialogElement)for(const t of this.currentDialogElement.querySelectorAll(".dialog-navigation button"))t.dispatchEvent(new Event("update"))}showDialog(t,e={}){var s;this.currentDialogElement&&this.closeDialog(),this.currentDialogElement=this.getDialogElement(t),this.currentDialogElement||console.error("Dialog not found"),this.currentDialogElement&&(s=this.currentDialogElement.querySelector(".dialog-body"),"template"in t&&render(t.template(e),s),"closeable"in t&&t.closeable?this.currentDialogElement.querySelector(".close-button").style.display="block":this.currentDialogElement.querySelector(".close-button").style.display="none",this.defaultButtons,"buttons"in t&&t.buttons,this.updateButtons(),this.currentDialogElement.showModal())}closeDialog(){this.currentDialogElement.close(),this.currentDialogElement=null}errorMsg(t){t=this.stripHtml(t),console.error(t),this.showError(t)}logMsg(t,e=!1){console.info(this.stripHtml(t)),e&&console.trace()}updateEspConnected(t){Object.values(this.connectionStates).includes(t)&&(this.connected=t,this.updateButtons())}stripHtml(t){var e=document.createElement("div");return e.innerHTML=t,e.textContent||e.innerText||""}formatMacAddr(t){return t.map(t=>t.toString(16).toUpperCase().padStart(2,"0")).join(":")}async disconnect(){this.espStub&&(await espStub.disconnect(),await espStub.port.close(),this.updateUIConnected(this.connectionStates.DISCONNECTED),this.espStub=null)}async runFlow(t){if(t instanceof Event){if(t.preventDefault(),t.stopImmediatePropagation(),!(t.target.id in this.flows))return;t=this.flows[t.target.id]}this.currentFlow=t,this.currentStep=0,await this.currentFlow.steps[this.currentStep].bind(this)()}async nextStep(){this.currentFlow&&this.currentStep= 4) { + versionInfo.major = matches[1]; + versionInfo.minor = matches[2]; + versionInfo.patch = matches[3]; + if (matches[4] && matches[5]) { + versionInfo.suffix = matches[4]; + versionInfo.suffixVersion = matches[5]; + } else { + versionInfo.suffix = "stable"; + versionInfo.suffixVersion = 0; + } + } + return versionInfo; + } + + sortReleases(releases) { + // Return a sorted list of releases by parsed version number + const sortHieratchy = ["major", "minor", "patch", "suffix", "suffixVersion"]; + releases.sort((a, b) => { + const aVersionInfo = this.parseVersion(a.version); + const bVersionInfo = this.parseVersion(b.version); + for (let sortKey of sortHieratchy) { + if (aVersionInfo[sortKey] < bVersionInfo[sortKey]) { + return -1; + } else if (aVersionInfo[sortKey] > bVersionInfo[sortKey]) { + return 1; + } + } + return 0; + }); + + return releases; + } + + async connectedCallback() { + // Required + this.boardId = this.getAttribute("boardid"); + + // If not provided, it will use the stable release if DEFAULT_RELEASE_LATEST is false + if (this.getAttribute("version")) { + this.releaseVersion = this.getAttribute("version"); + } + + // Pull in the info from the json as the default values. These can be overwritten by the attributes. + const response = await fetch(BOARD_DEFS); + const boardDefs = await response.json(); + let releaseInfo = null; + + if (Object.keys(boardDefs).includes(this.boardId)) { + const boardDef = boardDefs[this.boardId]; + this.chipFamily = boardDef.chipfamily; + if (boardDef.name) { + this.boardName = boardDef.name; + } + if (boardDef.bootloader) { + this.bootloaderUrl = this.updateBinaryUrl(boardDef.bootloader); + } + const sortedReleases = this.sortReleases(boardDef.releases); + + if (this.releaseVersion) { // User specified a release + for (let release of sortedReleases) { + if (release.version == this.releaseVersion) { + releaseInfo = release; + break; + } + } + } + if (!releaseInfo) { // Release version not found or not specified + if (DEFAULT_RELEASE_LATEST) { + releaseInfo = sortedReleases[sortedReleases.length - 1]; + } else { + releaseInfo = sortedReleases[0]; + } + this.releaseVersion = releaseInfo.version; + } + if (releaseInfo.uf2file) { + this.uf2FileUrl = this.updateBinaryUrl(releaseInfo.uf2file); + } + if (releaseInfo.binfile) { + this.binFileUrl = this.updateBinaryUrl(releaseInfo.binfile); + } + } + + // Nice to have for now + if (this.getAttribute("chipfamily")) { + this.chipFamily = this.getAttribute("chipfamily"); + } + + if (this.getAttribute("boardname")) { + this.boardName = this.getAttribute("boardname"); + } + this.menuTitle = `CircuitPython Installer for ${this.boardName}`; + + super.connectedCallback(); + } + + attributeChangedCallback(attribute, previousValue, currentValue) { + const classVar = attrMap[attribute]; + this[classVar] = currentValue ? this.updateBinaryUrl(currentValue) : null; + } + + updateBinaryUrl(url) { + //if (location.hostname == "localhost") { + if (url) { + url = url.replace("https://downloads.circuitpython.org/", "https://adafruit-circuit-python.s3.amazonaws.com/"); + } + //} + + return url; + } + + // These are a series of the valid steps that should be part of a program flow + // Some steps currently need to be grouped together + flows = { + uf2FullProgram: { // Native USB Install + label: `Full CircuitPython [version] Install`, + steps: [this.stepWelcome, this.stepSerialConnect, this.stepConfirm, this.stepEraseAll, this.stepBootloader, this.stepSelectBootDrive, this.stepCopyUf2, this.stepSelectCpyDrive, this.stepCredentials, this.stepSuccess], + isEnabled: async () => { return this.hasNativeUsb() && !!this.bootloaderUrl && !!this.uf2FileUrl }, + }, + binFullProgram: { // Non-native USB Install (Once we have boot drive disable working, we can remove hasNativeUsb() check) + label: `Full CircuitPython [version] Install`, + steps: [this.stepWelcome, this.stepSerialConnect, this.stepConfirm, this.stepEraseAll, this.stepFlashBin, this.stepSetupRepl, this.stepCredentials, this.stepSuccess], + isEnabled: async () => { return !this.hasNativeUsb() && !!this.binFileUrl }, + }, + uf2Only: { // Upgrade when Bootloader is already installer + label: `Upgrade/Install CircuitPython [version] UF2 Only`, + steps: [this.stepWelcome, this.stepSelectBootDrive, this.stepCopyUf2, this.stepSelectCpyDrive, this.stepCredentials, this.stepSuccess], + isEnabled: async () => { return this.hasNativeUsb() && !!this.uf2FileUrl }, + }, + binOnly: { + label: `Upgrade CircuitPython [version] Bin Only`, + steps: [this.stepWelcome, this.stepSerialConnect, this.stepConfirm, this.stepEraseAll, this.stepFlashBin, this.stepSuccess], + isEnabled: async () => { return !!this.binFileUrl }, + }, + bootloaderOnly: { // Used to allow UF2 Upgrade/Install + label: "Install Bootloader Only", + steps: [this.stepWelcome, this.stepSerialConnect, this.stepConfirm, this.stepEraseAll, this.stepBootloader, this.stepSuccess], + isEnabled: async () => { return this.hasNativeUsb() && !!this.bootloaderUrl }, + }, + credentialsOnlyRepl: { // Update via REPL + label: "Update WiFi credentials", + steps: [this.stepWelcome, this.stepSetupRepl, this.stepCredentials, this.stepSuccess], + isEnabled: async () => { return !this.hasNativeUsb() }, + }, + credentialsOnlyDrive: { // Update via CIRCUITPY Drive + label: "Update WiFi credentials", + steps: [this.stepWelcome, this.stepSelectCpyDrive, this.stepCredentials, this.stepSuccess], + isEnabled: async () => { return this.hasNativeUsb() }, + } + } + + // This is the data for the CircuitPython specific dialogs. Some are reused. + cpDialogs = { + welcome: { + closeable: true, + template: (data) => html` +

    + Welcome to the CircuitPython Installer. This tool will install CircuitPython on your ${data.boardName}. +

    +

    + This tool is new and experimental. If you experience any issues, feel free to check out + https://github.com/adafruit/circuitpython-org/issues + to see if somebody has already submitted the same issue you are experiencing. If not, feel free to open a new issue. If + you do see the same issue and are able to contribute additional information, that would be appreciated. +

    +

    + If you are unable to use this tool, then the manual installation methods should still work. +

    + ` + }, + espSerialConnect: { + closeable: true, + template: (data) => html` +

    + 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.
    • +
    +

    + + 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", "JTAG", and "Bridge Controller". There may be more than one right option depending on your system configuration. Experiment if needed. +

    + `, + buttons: [this.previousButton, { + label: "Next", + onClick: this.nextStep, + isEnabled: async () => { return (this.currentStep < this.currentFlow.steps.length - 1) && this.connected == this.connectionStates.CONNECTED }, + onUpdate: async (e) => { this.currentDialogElement.querySelector("#butConnect").innerText = this.connected; }, + }], + }, + confirm: { + template: (data) => html` +

    This will overwrite everything on the ${data.boardName}.

    + `, + buttons: [ + this.previousButton, + { + label: "Continue", + onClick: this.nextStep, + } + ], + }, + bootDriveSelect: { + closeable: true, + template: (data) => html` +

    + Please select the ${data.drivename} Drive where the UF2 file will be copied. +

    +

    + If you just installed the bootloader, you may need to reset your board. If you already had the bootloader installed, + you may need to double press the reset button. +

    +

    + +

    + `, + buttons: [], + }, + circuitpyDriveSelect: { + closeable: true, + template: (data) => html` +

    + Please select the CIRCUITPY Drive. If you don't see your CIRCUITPY drive, it may be disabled in boot.py or you may have renamed it at some point. +

    +

    + +

    + `, + buttons: [], + }, + actionWaiting: { + template: (data) => html` +

    ${data.action}...

    +
    + `, + buttons: [], + }, + actionProgress: { + template: (data) => html` +

    ${data.action}...

    + ${data.percentage}% + `, + buttons: [], + }, + cpSerial: { + closeable: true, + template: (data) => html` +

    + The next step is to write your credentials to settings.toml. Make sure your board is running CircuitPython. If you just installed CircuitPython, you may to reset the board first. +

    +

    + + Click this button to open the Web Serial connection menu. If it is already connected, pressing again will allow you to select a different port. +

    + +

    ${data.serialPortInstructions}

    + `, + buttons: [this.previousButton, { + label: "Next", + onClick: this.nextStep, + isEnabled: async () => { return (this.currentStep < this.currentFlow.steps.length - 1) && !!this.replSerialDevice; }, + onUpdate: async (e) => { this.currentDialogElement.querySelector("#butConnect").innerText = !!this.replSerialDevice ? "Connected" : "Connect"; }, + }], + }, + + credentials: { + closeable: true, + template: (data) => html` +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + ${data.mass_storage_disabled === true || data.mass_storage_disabled === false ? + html`
    + +
    ` : ''} +
    + `, + buttons: [this.previousButton, { + label: "Next", + onClick: this.saveCredentials, + }] + }, + success: { + closeable: true, + template: (data) => html` +

    Successfully Completed

    + ${data.ip ? + html`

    + You can edit files by going to http://${data.ip}/code/. +

    ` : ''} + `, + buttons: [this.closeButton], + }, + error: { + closeable: true, + template: (data) => html` +

    Installation Error: ${data.message}

    + `, + buttons: [this.closeButton], + }, + } + + + ////////// STEP FUNCTIONS ////////// + + async stepWelcome() { + // Display Welcome Dialog + this.showDialog(this.dialogs.welcome, {boardName: this.boardName}); + } + + async stepSerialConnect() { + // Display Serial Connect Dialog + this.showDialog(this.dialogs.espSerialConnect); + } + + async stepConfirm() { + // Display Confirm Dialog + this.showDialog(this.dialogs.confirm, {boardName: this.boardName}); + } + + async stepEraseAll() { + // Display Erase Dialog + this.showDialog(this.dialogs.actionWaiting, { + action: "Erasing Flash", + }); + try { + await this.espStub.eraseFlash(); + } catch (err) { + this.errorMsg("Unable to finish erasing Flash memory. Please try again."); + } + await this.nextStep(); + } + + async stepFlashBin() { + if (!this.binFileUrl) { + // We shouldn't be able to get here, but just in case + this.errorMsg("Missing bin file URL. Please make sure the installer button has this specified."); + return; + } + + await this.downloadAndInstall(this.binFileUrl); + await this.espHardReset(); + await this.nextStep(); + } + + async stepBootloader() { + if (!this.bootloaderUrl) { + // We shouldn't be able to get here, but just in case + this.errorMsg("Missing bootloader file URL. Please make sure the installer button has this specified."); + return; + } + // Display Bootloader Dialog + await this.downloadAndInstall(this.bootloaderUrl, 'combined.bin', true); + await this.nextStep(); + } + + async stepSelectBootDrive() { + const bootloaderVolume = await this.getBootDriveName(); + + if (bootloaderVolume) { + this.logMsg(`Waiting for user to select a bootloader volume named ${bootloaderVolume}`); + } + + // Display Select Bootloader Drive Dialog + this.showDialog(this.dialogs.bootDriveSelect, { + drivename: bootloaderVolume ? bootloaderVolume : "Bootloader", + }); + } + + async stepSelectCpyDrive() { + this.logMsg(`Waiting for user to select CIRCUITPY drive`); + + // Display Select CIRCUITPY Drive Dialog + this.showDialog(this.dialogs.circuitpyDriveSelect); + } + + async stepCopyUf2() { + if (!this.bootDriveHandle) { + this.errorMsg("No boot drive selected. stepSelectBootDrive should preceed this step."); + return; + } + // Display Progress Dialog + this.showDialog(this.dialogs.actionProgress, { + action: `Copying ${this.uf2FileUrl}`, + }); + + // Do a copy and update progress along the way + await this.downloadAndCopy(this.uf2FileUrl); + + // Once done, call nextstep + await this.nextStep(); + } + + async stepSetupRepl() { + // TODO: Try and reuse the existing connection so user doesn't need to select it again + /*if (this.port) { + this.replSerialDevice = this.port; + await this.setupRepl(); + }*/ + const serialPortName = await this.getSerialPortName(); + let serialPortInstructions ="There may be several devices listed. If you aren't sure which to choose, look for one that includes the name of your microcontroller."; + if (serialPortName) { + serialPortInstructions =`There may be several devices listed, but look for one called something like ${serialPortName}.` + } + this.showDialog(this.dialogs.cpSerial, { + serialPortInstructions: serialPortInstructions + }); + } + + async stepCredentials() { + // We may want to see if the board has previously been set up and fill in any values from settings.toml and boot.py + this.tomlSettings = await this.getCurrentSettings(); + console.log(this.tomlSettings); + const parameters = { + wifi_ssid: this.getSetting('CIRCUITPY_WIFI_SSID'), + wifi_password: this.getSetting('CIRCUITPY_WIFI_PASSWORD'), + api_password: this.getSetting('CIRCUITPY_WEB_API_PASSWORD', 'passw0rd'), + api_port: this.getSetting('CIRCUITPY_WEB_API_PORT', 80), + } + + if (this.hasNativeUsb()) { + // TODO: Currently the control is just disabled and not used because we don't have anything to modify boot.py in place. + // Setting mass_storage_disabled to true/false will display the checkbox with the appropriately checked state. + //parameters.mass_storage_disabled = true; + } + + // Display Credentials Request Dialog + this.showDialog(this.dialogs.credentials, parameters); + } + + async stepSuccess() { + let deviceHostInfo = {}; + if (this.repl) { + await this.repl.waitForPrompt(); + // If we were setting up Web Workflow, we may want to provide a link to code.circuitpython.org + if (this.currentFlow || this.currentFlow.steps.includes(this.stepCredentials)) { + deviceHostInfo = await this.getDeviceHostInfo(); + } + } + + // Display Success Dialog + this.showDialog(this.dialogs.success, deviceHostInfo); + } + + async stepClose() { + // Close the currently loaded dialog + this.closeDialog(); + } + + ////////// HANDLERS ////////// + + async bootDriveSelectHandler(e) { + const bootloaderVolume = await this.getBootDriveName(); + let dirHandle; + + // This will need to show a dialog selector + try { + dirHandle = await window.showDirectoryPicker({mode: 'readwrite'}); + } catch (e) { + // Likely the user cancelled the dialog + return; + } + if (bootloaderVolume && bootloaderVolume != dirHandle.name) { + alert(`The selected drive named ${dirHandle.name} does not match the expected name of ${bootloaderVolume}. Please select the correct drive.`); + return; + } + if (!await this._verifyPermission(dirHandle)) { + alert("Unable to write to the selected folder"); + return; + } + + this.bootDriveHandle = dirHandle; + await this.nextStep(); + } + + async circuitpyDriveSelectHandler(e) { + let dirHandle; + + // This will need to show a dialog selector + try { + dirHandle = await window.showDirectoryPicker({mode: 'readwrite'}); + } catch (e) { + // Likely the user cancelled the dialog + return; + } + // Check if boot_out.txt exists + if (!(await this.getBootOut(dirHandle))) { + alert(`Expecting a folder with boot_out.txt. Please select the root folder of your CIRCUITPY drive.`); + return; + } + if (!await this._verifyPermission(dirHandle)) { + alert("Unable to write to the selected folder"); + return; + } + + this.circuitpyDriveHandle = dirHandle; + await this.nextStep(); + } + + async espToolConnectHandler(e) { + await this.onReplDisconnected(e); + await this.espDisconnect(); + let esploader; + try { + esploader = await this.espConnect({ + log: (...args) => this.logMsg(...args), + debug: (...args) => {}, + error: (...args) => this.errorMsg(...args), + }); + } catch (err) { + // It's possible the dialog was also canceled here + this.errorMsg("Unable to open Serial connection to board. Make sure the port is not already in use by another application or in another browser tab."); + return; + } + + try { + this.updateEspConnected(this.connectionStates.CONNECTING); + await esploader.initialize(); + this.updateEspConnected(this.connectionStates.CONNECTED); + } catch (err) { + await esploader.disconnect(); + // Disconnection before complete + this.updateEspConnected(this.connectionStates.DISCONNECTED); + this.errorMsg("Unable to connect to the board. Make sure it is in bootloader mode by holding the boot0 button when powering on and try again.") + return; + } + + try { + this.logMsg(`Connected to ${esploader.chipName}`); + this.logMsg(`MAC Address: ${this.formatMacAddr(esploader.macAddr())}`); + + // check chip compatibility + if (FAMILY_TO_CHIP_MAP[this.chipFamily] == esploader.chipFamily) { + this.logMsg("This chip checks out"); + this.espStub = await esploader.runStub(); + this.espStub.addEventListener("disconnect", () => { + this.updateEspConnected(this.connectionStates.DISCONNECTED); + this.espStub = null; + }); + + await this.setBaudRateIfChipSupports(esploader.chipFamily, PREFERRED_BAUDRATE); + await this.nextStep(); + return; + } + + // Can't use it so disconnect now + this.errorMsg("Oops, this is the wrong firmware for your board.") + await this.espDisconnect(); + + } catch (err) { + await esploader.disconnect(); + // Disconnection before complete + this.updateEspConnected(this.connectionStates.DISCONNECTED); + this.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.") + } + } + + async onSerialReceive(e) { + await this.repl.onSerialReceive(e); + } + + async cpSerialConnectHandler(e) { + // Disconnect from the ESP Tool if Connected + await this.espDisconnect(); + + await this.onReplDisconnected(e); + + // Connect to the Serial Port and interact with the REPL + try { + this.replSerialDevice = await navigator.serial.requestPort(); + } catch (e) { + // Likely the user cancelled the dialog + return; + } + + try { + await this.replSerialDevice.open({baudRate: ESP_ROM_BAUD}); + } catch (e) { + console.error("Error. Unable to open Serial Port. Make sure it isn't already in use in another tab or application."); + } + + await this.setupRepl(); + + this.nextStep(); + } + + async setupRepl() { + if (this.replSerialDevice) { + this.repl = new REPL(); + this.repl.serialTransmit = this.serialTransmit.bind(this); + + this.replSerialDevice.addEventListener("message", this.onSerialReceive.bind(this)); + + // Start the read loop + this._readLoopPromise = this._readSerialLoop().catch( + async function(error) { + await this.onReplDisconnected(); + }.bind(this) + ); + + if (this.replSerialDevice.writable) { + this.writer = this.replSerialDevice.writable.getWriter(); + await this.writer.ready; + } + } + } + + async onReplDisconnected(e) { + if (this.reader) { + try { + await this.reader.cancel(); + } catch(e) { + // Ignore + } + this.reader = null; + } + if (this.writer) { + await this.writer.releaseLock(); + this.writer = null; + } + + if (this.replSerialDevice) { + try { + await this.replSerialDevice.close(); + } catch(e) { + // Ignore + } + this.replSerialDevice = null; + } + } + + //////////////// FILE HELPERS //////////////// + + async getBootDriveName() { + if (this._bootDriveName) { + return this._bootDriveName; + } + await this.extractBootloaderInfo(); + + return this._bootDriveName; + } + + async getSerialPortName() { + if (this._serialPortName) { + return this._serialPortName; + } + await this.extractBootloaderInfo(); + + return this._serialPortName; + } + + async _verifyPermission(folderHandle) { + const options = {mode: 'readwrite'}; + + if (await folderHandle.queryPermission(options) === 'granted') { + return true; + } + + if (await folderHandle.requestPermission(options) === 'granted') { + return true; + } + + return false; + } + + async extractBootloaderInfo() { + if (!this.bootloaderUrl) { + return false; + } + + // Download the bootloader zip file + let [filename, fileBlob] = await this.downloadAndExtract(this.bootloaderUrl, 'tinyuf2.bin'); + const fileContents = await fileBlob.text(); + + const bootDriveRegex = /B\x00B\x00([A-Z0-9\x00]{11})FAT16/; + const serialNameRegex = /0123456789ABCDEF(.+)\x00UF2/; + // Not sure if manufacturer is displayed. If not, we should use this instead + // const serialNameRegex = /0123456789ABCDEF(?:.*\x00)?(.+)\x00UF2/; + + let matches = fileContents.match(bootDriveRegex); + if (matches && matches.length >= 2) { + // Strip any null characters from the name + this._bootDriveName = matches[1].replace(/\0/g, ''); + } + + matches = fileContents.match(serialNameRegex); + if (matches && matches.length >= 2) { + // Replace any null characters with spaces + this._serialPortName = matches[1].replace(/\0/g, ' '); + } + + this.removeCachedFile(this.bootloaderUrl.split("/").pop()); + } + + async getBootOut(dirHandle) { + return await this.readFile("boot_out.txt", dirHandle); + } + + async readFile(filename, dirHandle = null) { + // Read a file from the given directory handle + + if (!dirHandle) { + dirHandle = this.circuitpyDriveHandle; + } + if (!dirHandle) { + console.warn("CIRCUITPY Drive not selected and no Directory Handle provided"); + return null; + } + try { + const fileHandle = await dirHandle.getFileHandle(filename); + const fileData = await fileHandle.getFile(); + + return await fileData.text(); + } catch (e) { + return null; + } + } + + async writeFile(filename, contents, dirHandle = null) { + // Write a file to the given directory handle + if (!dirHandle) { + dirHandle = this.circuitpyDriveHandle; + } + if (!dirHandle) { + console.warn("CIRCUITPY Drive not selected and no Directory Handle provided"); + return null; + } + + const fileHandle = await dirHandle.getFileHandle(filename, {create: true}); + const writable = await fileHandle.createWritable(); + await writable.write(contents); + await writable.close(); + } + + + //////////////// DOWNLOAD HELPERS //////////////// + + addCachedFile(filename, blob) { + this.fileCache.push({ + filename: filename, + blob: blob + }); + } + + getCachedFile(filename) { + for (let file of this.fileCache) { + if (file.filename === filename) { + return file.contents; + } + } + return null; + } + + removeCachedFile(filename) { + for (let file of this.fileCache) { + if (file.filename === filename) { + this.fileCache.splice(this.fileCache.indexOf(file), 1); + } + } + } + + async downloadFile(url, progressElement) { + let response; + try { + response = await fetch(url); + } catch (err) { + this.errorMsg(`Unable to download file: ${url}`); + return null; + } + + const body = response.body; + const reader = body.getReader(); + const contentLength = +response.headers.get('Content-Length'); + let receivedLength = 0; + let chunks = []; + while(true) { + const {done, value} = await reader.read(); + if (done) { + break; + } + chunks.push(value); + receivedLength += value.length; + progressElement.value = Math.round((receivedLength / contentLength) * 100); + this.logMsg(`Received ${receivedLength} of ${contentLength}`) + } + let chunksAll = new Uint8Array(receivedLength); + let position = 0; + for(let chunk of chunks) { + chunksAll.set(chunk, position); + position += chunk.length; + } + + let result = new Blob([chunksAll]); + + return result; + } + + async downloadAndExtract(url, fileToExtract = null, cacheFile = false) { + // Display Progress Dialog + let filename = url.split("/").pop(); + let fileBlob = this.getCachedFile(filename); + + if (!fileBlob) { + this.showDialog(this.dialogs.actionProgress, { + action: `Downloading ${filename}` + }); + + const progressElement = this.currentDialogElement.querySelector("#stepProgress"); + + // Download the file at the url updating the progress in the process + fileBlob = await this.downloadFile(url, progressElement); + + if (cacheFile) { + this.addCachedFile(filename, fileBlob); + } + } + + // If the file is a zip file, unzip and find the file to extract + if (filename.endsWith(".zip") && fileToExtract) { + let foundFile; + // Update the Progress dialog + this.showDialog(this.dialogs.actionProgress, { + action: `Extracting ${fileToExtract}` + }); + + // Set that to the current file to flash + [foundFile, fileBlob] = await this.findAndExtractFromZip(fileBlob, fileToExtract); + if (!fileBlob) { + this.errorMsg(`Unable to find ${fileToExtract} in ${filename}`); + return; + } + filename = foundFile; + } + + return [filename, fileBlob]; + } + + async downloadAndInstall(url, fileToExtract = null, cacheFile = false) { + let [filename, fileBlob] = await this.downloadAndExtract(url, fileToExtract, cacheFile); + + // Update the Progress dialog + if (fileBlob) { + const fileContents = (new Uint8Array(await fileBlob.arrayBuffer())).buffer; + let lastPercent = 0; + this.showDialog(this.dialogs.actionProgress, { + action: `Flashing ${filename}` + }); + + const progressElement = this.currentDialogElement.querySelector("#stepProgress"); + progressElement.value = 0; + + try { + await this.espStub.flashData(fileContents, (bytesWritten, totalBytes) => { + let percentage = Math.round((bytesWritten / totalBytes) * 100); + if (percentage > lastPercent) { + progressElement.value = percentage; + this.logMsg(`${percentage}% (${bytesWritten}/${totalBytes})...`); + lastPercent = percentage; + } + }, 0, 0); + } catch (err) { + this.errorMsg(`Unable to flash file: ${filename}. Error Message: ${err}`); + } + } + } + + async downloadAndCopy(url, dirHandle = null) { + if (!dirHandle) { + dirHandle = this.bootDriveHandle; + } + if (!dirHandle) { + this.errorMsg("No drive handle available"); + return; + } + + const progressElement = this.currentDialogElement.querySelector("#stepProgress"); + progressElement.value = 0; + + let [filename, fileBlob] = await this.downloadAndExtract(url); + const fileHandle = await dirHandle.getFileHandle(filename, {create: true}); + const writableStream = await fileHandle.createWritable(); + const totalSize = fileBlob.size; + let bytesWritten = 0; + let chunk; + while(bytesWritten < totalSize) { + chunk = fileBlob.slice(bytesWritten, bytesWritten + COPY_CHUNK_SIZE); + await writableStream.write(chunk, {position: bytesWritten, size: chunk.size}); + + bytesWritten += chunk.size; + progressElement.value = Math.round(bytesWritten / totalSize * 100); + this.logMsg(`${Math.round(bytesWritten / totalSize * 100)}% (${bytesWritten} / ${totalSize}) written...`); + } + this.logMsg("File successfully written"); + try { + // Attempt to close the file, but since the device reboots, it may error + await writableStream.close(); + this.logMsg("File successfully closed"); + } catch (err) { + this.logMsg("Error closing file, probably due to board reset. Continuing..."); + } + } + + async findAndExtractFromZip(zipBlob, filename) { + const reader = new zip.ZipReader(new zip.BlobReader(zipBlob)); + + // unzip into local file cache + let zipContents = await reader.getEntries(); + + for(const zipEntry of zipContents) { + if (zipEntry.filename.localeCompare(filename) === 0) { + const extractedFile = await zipEntry.getData(new zip.BlobWriter()); + return [zipEntry.filename, extractedFile]; + } + } + + return [null, null]; + } + + + //////////////// OTHER HELPERS //////////////// + + async saveCredentials() { + this.saveSetting('CIRCUITPY_WIFI_SSID'); + this.saveSetting('CIRCUITPY_WIFI_PASSWORD'); + this.saveSetting('CIRCUITPY_WEB_API_PASSWORD'); + this.saveSetting('CIRCUITPY_WEB_API_PORT'); + + await this.writeSettings(this.tomlSettings); + if (this.hasNativeUsb()) { + //this.setBootDisabled(true); + } + await this.nextStep(); + } + + getSetting(setting, defaultValue = '') { + if (this.tomlSettings && this.tomlSettings.hasOwnProperty(setting)) { + return this.tomlSettings[setting]; + } + + return defaultValue; + } + + async getBootDisabled() { + // This is a very simple check for now. If there is something more complicated like a disable + // command behind an if statement, this will not detect it is enabled. + let fileContents; + if (this.repl) { + return true; // Always disabled in this case + } else if (this.circuitpyDriveHandle) { + fileContents = await this.readFile("boot.py"); + // TODO: Compare board's boot.py to our boot.py by + // searching for storage.disable_usb_drive() at the beginning of the line + } else { + this.errorMsg("Connect to the CIRCUITPY drive or the REPL first"); + return {}; + } + + if (fileContents) { + return toml.parse(fileContents); + } + this.logMsg("Unable to read settings.toml from CircuitPython. It may not exist. Continuing..."); + return {}; + } + + saveBootDisabled(disabled) { + // TODO: Save/remove a copy of boot.py on the CIRCUITPY drive + // This depends on whether it is currently disabled in boot.py and what the value of disabled is + // If they are the same, we can skip + // An idea is to only deal with this if boot.py doesn't exist and just use a generic boot.py + // For disabling, we can compare to the generic and if they are different refuse to touch it + const formElement = this.currentDialogElement.querySelector('#circuitpy_drive'); + if (formElement) { + if (formElement.checked) { + this.tomlSettings['CIRCUITPY_DRIVE'] = "disabled"; + } else { + this.tomlSettings['CIRCUITPY_DRIVE'] = "enabled"; + } + } + + } + + saveSetting(settingName) { + const formElement = this.currentDialogElement.querySelector(`#${settingName.toLowerCase()}`) + if (formElement) { + if (formElement.type == "number") { + this.tomlSettings[settingName] = parseInt(formElement.value); + } else if (formElement.type == "text" || formElement.type == "password") { + this.tomlSettings[settingName] = formElement.value; + } else { + this.errorMsg(`A setting was found, but a form element of type ${formElement.type} was not expected.`); + } + } else { + this.errorMsg(`A setting named '${settingName}' was not found.`); + } + } + + async runCode(code, outputToConsole = true) { + if (Array.isArray(code)) { + code = code.join("\n"); + } + + if (this.repl) { + const output = await this.repl.runCode(code); + + if (outputToConsole) { + console.log(output); + } + } + } + + async writeSettings(settings) { + if (this.repl) { + await this.runCode(`import storage`); + await this.runCode(`storage.remount("/", False)`); + await this.runCode(`f = open('settings.toml', 'w')`); + + for (const [setting, value] of Object.entries(settings)) { + if (typeof value === "string") { + await this.runCode(`f.write('${setting} = "${value}"\\n')`); + } else { + await this.runCode(`f.write('${setting} = ${value}\\n')`); + } + } + await this.runCode(`f.close()`); + + // Perform a soft restart to avoid losing the connection and get an IP address + this.showDialog(this.dialogs.actionWaiting, { + action: "Waiting for IP Address...", + }); + await this.repl.softRestart(); + try { + await this.timeout( + async () => { + let deviceInfo = {}; + while (Object.entries(deviceInfo).length == 0 || deviceInfo.ip === null) { + deviceInfo = await this.getDeviceHostInfo(); + await this.sleep(300); + } + }, 10000 + ); + } catch (error) { + console.warn("Unable to get IP Address. Network Credentials may be incorrect"); + return null; + } + } else if (this.circuitpyDriveHandle) { + const contents = toml.stringify(settings); + await this.writeFile("settings.toml", contents); + } else { + this.errorMsg("Connect to the CIRCUITPY drive or the REPL first"); + return null; + } + } + + async getCurrentSettings() { + let fileContents; + if (this.repl) { + fileContents = await this.runCode(["f = open('settings.toml', 'r')", "print(f.read())", "f.close()"]); + } else if (this.circuitpyDriveHandle) { + fileContents = await this.readFile("settings.toml"); + } else { + this.errorMsg("Connect to the CIRCUITPY drive or the REPL first"); + return {}; + } + + if (fileContents) { + return toml.parse(fileContents); + } + this.logMsg("Unable to read settings.toml from CircuitPython. It may not exist. Continuing..."); + return {}; + } + + async espDisconnect() { + // Disconnect the ESPTool + if (this.espStub) { + await this.espStub.disconnect(); + this.espStub.removeEventListener("disconnect", this.espDisconnect.bind(this)); + this.updateEspConnected(this.connectionStates.DISCONNECTED); + this.espStub = null; + } + if (this.port) { + await this.port.close(); + this.port = null; + } + } + + async serialTransmit(msg) { + const encoder = new TextEncoder(); + if (this.writer) { + const encMessage = encoder.encode(msg); + await this.writer.ready.catch((err) => { + this.errorMsg(`Ready error: ${err}`); + }); + await this.writer.write(encMessage).catch((err) => { + this.errorMsg(`Chunk error: ${err}`); + }); + await this.writer.ready; + } + } + + async _readSerialLoop() { + if (!this.replSerialDevice) { + return; + } + + const messageEvent = new Event("message"); + const decoder = new TextDecoder(); + + if (this.replSerialDevice.readable) { + this.reader = this.replSerialDevice.readable.getReader(); + while (true) { + const {value, done} = await this.reader.read(); + if (value) { + messageEvent.data = decoder.decode(value); + this.replSerialDevice.dispatchEvent(messageEvent); + } + if (done) { + this.reader.releaseLock(); + await this.onReplDisconnected(); + break; + } + } + } + + this.logMsg("Read Loop Stopped. Closing Serial Port."); + } + + async getDeviceHostInfo() { + // For now return info from title + if (this.repl) { + return { + ip: this.repl.getIpAddress(), + version: this.repl.getVersion(), + }; + } + + return {}; + + // TODO: (Maybe) Retreive some device info via the REPL (mDNS Hostname and IP Address) + // import wifi + // import mdns + // wifi.radio.ipv4_address + // server = mdns.Server(wifi.radio) + // server.hostname + } + + // This is necessary because chips with native USB will have a CIRCUITPY drive, which blocks writing via REPL + hasNativeUsb() { + if (!this.chipFamily || ("esp32", "esp32c3").includes(this.chipFamily)) { + return false; + } + + // Since most new chips have it, we return true by default. + return true; + } + + sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + timeout(callback, ms) { + return Promise.race([callback(), this.sleep(ms).then(() => {throw Error("Timed Out");})]); + } +} + + +customElements.define('cp-install-button', CPInstallButton, {extends: "button"}); \ No newline at end of file diff --git a/dist/cpinstaller.min.js b/dist/cpinstaller.min.js new file mode 100644 index 0000000..39333d1 --- /dev/null +++ b/dist/cpinstaller.min.js @@ -0,0 +1,96 @@ +"use strict";import{html}from"https://unpkg.com/lit-html?module";import*as toml from"https://unpkg.com/iarna-toml-esm@3.0.5/toml-esm.mjs";import*as zip from"https://cdn.jsdelivr.net/npm/@zip.js/zip.js@2.6.65/+esm";import*as esptoolPackage from"https://unpkg.com/esp-web-flasher@5.1.2/dist/web/index.js?module";import{REPL}from"https://cdn.jsdelivr.net/gh/adafruit/circuitpython-repl-js@1.2.1/repl.js";import{InstallButton,ESP_ROM_BAUD}from"./base_installer.js";const PREFERRED_BAUDRATE=921600,COPY_CHUNK_SIZE=65536,DEFAULT_RELEASE_LATEST=!1,BOARD_DEFS="https://adafruit-circuit-python.s3.amazonaws.com/esp32_boards.json",CSS_DIALOG_CLASS="cp-installer-dialog",FAMILY_TO_CHIP_MAP={esp32s2:esptoolPackage.CHIP_FAMILY_ESP32S2,esp32s3:esptoolPackage.CHIP_FAMILY_ESP32S3,esp32c3:esptoolPackage.CHIP_FAMILY_ESP32C3,esp32:esptoolPackage.CHIP_FAMILY_ESP32},attrMap={bootloader:"bootloaderUrl",uf2file:"uf2FileUrl",binfile:"binFileUrl"};class CPInstallButton extends InstallButton{constructor(){super(),this.releaseVersion="[version]",this.boardName="ESP32-based device",this.boardId=null,this.bootloaderUrl=null,this.uf2FileUrl=null,this.binFileUrl=null,this.releaseVersion=0,this.chipFamily=null,this.dialogCssClass=CSS_DIALOG_CLASS,this.dialogs={...this.dialogs,...this.cpDialogs},this.bootDriveHandle=null,this.circuitpyDriveHandle=null,this._bootDriveName=null,this._serialPortName=null,this.replSerialDevice=null,this.repl=null,this.fileCache=[],this.reader=null,this.writer=null,this.tomlSettings=null,this.init()}static get observedAttributes(){return Object.keys(attrMap)}parseVersion(e){var t={},e=e.match(/(\d+)\.(\d+)\.(\d+)(?:-([a-z]+)\.(\d+))?/);return e&&4<=e.length&&(t.major=e[1],t.minor=e[2],t.patch=e[3],e[4]&&e[5]?(t.suffix=e[4],t.suffixVersion=e[5]):(t.suffix="stable",t.suffixVersion=0)),t}sortReleases(e){const r=["major","minor","patch","suffix","suffixVersion"];return e.sort((e,t)=>{var i,s=this.parseVersion(e.version),a=this.parseVersion(t.version);for(i of r){if(s[i]a[i])return 1}return 0}),e}async connectedCallback(){this.boardId=this.getAttribute("boardid"),this.getAttribute("version")&&(this.releaseVersion=this.getAttribute("version"));var e=await(await fetch(BOARD_DEFS)).json();let t=null;if(Object.keys(e).includes(this.boardId)){e=e[this.boardId],e=(this.chipFamily=e.chipfamily,e.name&&(this.boardName=e.name),e.bootloader&&(this.bootloaderUrl=this.updateBinaryUrl(e.bootloader)),this.sortReleases(e.releases));if(this.releaseVersion)for(var i of e)if(i.version==this.releaseVersion){t=i;break}t||(t=DEFAULT_RELEASE_LATEST?e[e.length-1]:e[0],this.releaseVersion=t.version),t.uf2file&&(this.uf2FileUrl=this.updateBinaryUrl(t.uf2file)),t.binfile&&(this.binFileUrl=this.updateBinaryUrl(t.binfile))}this.getAttribute("chipfamily")&&(this.chipFamily=this.getAttribute("chipfamily")),this.getAttribute("boardname")&&(this.boardName=this.getAttribute("boardname")),this.menuTitle="CircuitPython Installer for "+this.boardName,super.connectedCallback()}attributeChangedCallback(e,t,i){this[attrMap[e]]=i?this.updateBinaryUrl(i):null}updateBinaryUrl(e){return e=e&&e.replace("https://downloads.circuitpython.org/","https://adafruit-circuit-python.s3.amazonaws.com/")}flows={uf2FullProgram:{label:"Full CircuitPython [version] Install",steps:[this.stepWelcome,this.stepSerialConnect,this.stepConfirm,this.stepEraseAll,this.stepBootloader,this.stepSelectBootDrive,this.stepCopyUf2,this.stepSelectCpyDrive,this.stepCredentials,this.stepSuccess],isEnabled:async()=>this.hasNativeUsb()&&!!this.bootloaderUrl&&!!this.uf2FileUrl},binFullProgram:{label:"Full CircuitPython [version] Install",steps:[this.stepWelcome,this.stepSerialConnect,this.stepConfirm,this.stepEraseAll,this.stepFlashBin,this.stepSetupRepl,this.stepCredentials,this.stepSuccess],isEnabled:async()=>!this.hasNativeUsb()&&!!this.binFileUrl},uf2Only:{label:"Upgrade/Install CircuitPython [version] UF2 Only",steps:[this.stepWelcome,this.stepSelectBootDrive,this.stepCopyUf2,this.stepSelectCpyDrive,this.stepCredentials,this.stepSuccess],isEnabled:async()=>this.hasNativeUsb()&&!!this.uf2FileUrl},binOnly:{label:"Upgrade CircuitPython [version] Bin Only",steps:[this.stepWelcome,this.stepSerialConnect,this.stepConfirm,this.stepEraseAll,this.stepFlashBin,this.stepSuccess],isEnabled:async()=>!!this.binFileUrl},bootloaderOnly:{label:"Install Bootloader Only",steps:[this.stepWelcome,this.stepSerialConnect,this.stepConfirm,this.stepEraseAll,this.stepBootloader,this.stepSuccess],isEnabled:async()=>this.hasNativeUsb()&&!!this.bootloaderUrl},credentialsOnlyRepl:{label:"Update WiFi credentials",steps:[this.stepWelcome,this.stepSetupRepl,this.stepCredentials,this.stepSuccess],isEnabled:async()=>!this.hasNativeUsb()},credentialsOnlyDrive:{label:"Update WiFi credentials",steps:[this.stepWelcome,this.stepSelectCpyDrive,this.stepCredentials,this.stepSuccess],isEnabled:async()=>this.hasNativeUsb()}};cpDialogs={welcome:{closeable:!0,template:e=>html` +

    + Welcome to the CircuitPython Installer. This tool will install CircuitPython on your ${e.boardName}. +

    +

    + This tool is new and experimental. If you experience any issues, feel free to check out + https://github.com/adafruit/circuitpython-org/issues + to see if somebody has already submitted the same issue you are experiencing. If not, feel free to open a new issue. If + you do see the same issue and are able to contribute additional information, that would be appreciated. +

    +

    + If you are unable to use this tool, then the manual installation methods should still work. +

    + `},espSerialConnect:{closeable:!0,template:e=>html` +

    + 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.
    • +
    +

    + + 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", "JTAG", and "Bridge Controller". There may be more than one right option depending on your system configuration. Experiment if needed. +

    + `,buttons:[this.previousButton,{label:"Next",onClick:this.nextStep,isEnabled:async()=>this.currentStep{this.currentDialogElement.querySelector("#butConnect").innerText=this.connected}}]},confirm:{template:e=>html` +

    This will overwrite everything on the ${e.boardName}.

    + `,buttons:[this.previousButton,{label:"Continue",onClick:this.nextStep}]},bootDriveSelect:{closeable:!0,template:e=>html` +

    + Please select the ${e.drivename} Drive where the UF2 file will be copied. +

    +

    + If you just installed the bootloader, you may need to reset your board. If you already had the bootloader installed, + you may need to double press the reset button. +

    +

    + +

    + `,buttons:[]},circuitpyDriveSelect:{closeable:!0,template:e=>html` +

    + Please select the CIRCUITPY Drive. If you don't see your CIRCUITPY drive, it may be disabled in boot.py or you may have renamed it at some point. +

    +

    + +

    + `,buttons:[]},actionWaiting:{template:e=>html` +

    ${e.action}...

    +
    + `,buttons:[]},actionProgress:{template:e=>html` +

    ${e.action}...

    + ${e.percentage}% + `,buttons:[]},cpSerial:{closeable:!0,template:e=>html` +

    + The next step is to write your credentials to settings.toml. Make sure your board is running CircuitPython. If you just installed CircuitPython, you may to reset the board first. +

    +

    + + Click this button to open the Web Serial connection menu. If it is already connected, pressing again will allow you to select a different port. +

    + +

    ${e.serialPortInstructions}

    + `,buttons:[this.previousButton,{label:"Next",onClick:this.nextStep,isEnabled:async()=>this.currentStep{this.currentDialogElement.querySelector("#butConnect").innerText=this.replSerialDevice?"Connected":"Connect"}}]},credentials:{closeable:!0,template:e=>html` +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + ${!0===e.mass_storage_disabled||!1===e.mass_storage_disabled?html`
    + +
    `:""} +
    + `,buttons:[this.previousButton,{label:"Next",onClick:this.saveCredentials}]},success:{closeable:!0,template:e=>html` +

    Successfully Completed

    + ${e.ip?html`

    + You can edit files by going to http://${e.ip}/code/. +

    `:""} + `,buttons:[this.closeButton]},error:{closeable:!0,template:e=>html` +

    Installation Error: ${e.message}

    + `,buttons:[this.closeButton]}};async stepWelcome(){this.showDialog(this.dialogs.welcome,{boardName:this.boardName})}async stepSerialConnect(){this.showDialog(this.dialogs.espSerialConnect)}async stepConfirm(){this.showDialog(this.dialogs.confirm,{boardName:this.boardName})}async stepEraseAll(){this.showDialog(this.dialogs.actionWaiting,{action:"Erasing Flash"});try{await this.espStub.eraseFlash()}catch(e){this.errorMsg("Unable to finish erasing Flash memory. Please try again.")}await this.nextStep()}async stepFlashBin(){this.binFileUrl?(await this.downloadAndInstall(this.binFileUrl),await this.espHardReset(),await this.nextStep()):this.errorMsg("Missing bin file URL. Please make sure the installer button has this specified.")}async stepBootloader(){this.bootloaderUrl?(await this.downloadAndInstall(this.bootloaderUrl,"combined.bin",!0),await this.nextStep()):this.errorMsg("Missing bootloader file URL. Please make sure the installer button has this specified.")}async stepSelectBootDrive(){var e=await this.getBootDriveName();e&&this.logMsg("Waiting for user to select a bootloader volume named "+e),this.showDialog(this.dialogs.bootDriveSelect,{drivename:e||"Bootloader"})}async stepSelectCpyDrive(){this.logMsg("Waiting for user to select CIRCUITPY drive"),this.showDialog(this.dialogs.circuitpyDriveSelect)}async stepCopyUf2(){this.bootDriveHandle?(this.showDialog(this.dialogs.actionProgress,{action:"Copying "+this.uf2FileUrl}),await this.downloadAndCopy(this.uf2FileUrl),await this.nextStep()):this.errorMsg("No boot drive selected. stepSelectBootDrive should preceed this step.")}async stepSetupRepl(){var e=await this.getSerialPortName();let t=e?`There may be several devices listed, but look for one called something like ${e}.`:"There may be several devices listed. If you aren't sure which to choose, look for one that includes the name of your microcontroller.";this.showDialog(this.dialogs.cpSerial,{serialPortInstructions:t})}async stepCredentials(){this.tomlSettings=await this.getCurrentSettings(),console.log(this.tomlSettings);var e={wifi_ssid:this.getSetting("CIRCUITPY_WIFI_SSID"),wifi_password:this.getSetting("CIRCUITPY_WIFI_PASSWORD"),api_password:this.getSetting("CIRCUITPY_WEB_API_PASSWORD","passw0rd"),api_port:this.getSetting("CIRCUITPY_WEB_API_PORT",80)};this.hasNativeUsb(),this.showDialog(this.dialogs.credentials,e)}async stepSuccess(){let e={};this.repl&&(await this.repl.waitForPrompt(),this.currentFlow||this.currentFlow.steps.includes(this.stepCredentials))&&(e=await this.getDeviceHostInfo()),this.showDialog(this.dialogs.success,e)}async stepClose(){this.closeDialog()}async bootDriveSelectHandler(e){var t=await this.getBootDriveName();let i;try{i=await window.showDirectoryPicker({mode:"readwrite"})}catch(e){return}t&&t!=i.name?alert(`The selected drive named ${i.name} does not match the expected name of ${t}. Please select the correct drive.`):await this._verifyPermission(i)?(this.bootDriveHandle=i,await this.nextStep()):alert("Unable to write to the selected folder")}async circuitpyDriveSelectHandler(e){let t;try{t=await window.showDirectoryPicker({mode:"readwrite"})}catch(e){return}await this.getBootOut(t)?await this._verifyPermission(t)?(this.circuitpyDriveHandle=t,await this.nextStep()):alert("Unable to write to the selected folder"):alert("Expecting a folder with boot_out.txt. Please select the root folder of your CIRCUITPY drive.")}async espToolConnectHandler(e){await this.onReplDisconnected(e),await this.espDisconnect();let t;try{t=await this.espConnect({log:(...e)=>this.logMsg(...e),debug:()=>{},error:(...e)=>this.errorMsg(...e)})}catch(e){return void this.errorMsg("Unable to open Serial connection to board. Make sure the port is not already in use by another application or in another browser tab.")}try{this.updateEspConnected(this.connectionStates.CONNECTING),await t.initialize(),this.updateEspConnected(this.connectionStates.CONNECTED)}catch(e){return await t.disconnect(),this.updateEspConnected(this.connectionStates.DISCONNECTED),void this.errorMsg("Unable to connect to the board. Make sure it is in bootloader mode by holding the boot0 button when powering on and try again.")}try{this.logMsg("Connected to "+t.chipName),this.logMsg("MAC Address: "+this.formatMacAddr(t.macAddr())),FAMILY_TO_CHIP_MAP[this.chipFamily]==t.chipFamily?(this.logMsg("This chip checks out"),this.espStub=await t.runStub(),this.espStub.addEventListener("disconnect",()=>{this.updateEspConnected(this.connectionStates.DISCONNECTED),this.espStub=null}),await this.setBaudRateIfChipSupports(t.chipFamily,PREFERRED_BAUDRATE),await this.nextStep()):(this.errorMsg("Oops, this is the wrong firmware for your board."),await this.espDisconnect())}catch(e){await t.disconnect(),this.updateEspConnected(this.connectionStates.DISCONNECTED),this.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.")}}async onSerialReceive(e){await this.repl.onSerialReceive(e)}async cpSerialConnectHandler(e){await this.espDisconnect(),await this.onReplDisconnected(e);try{this.replSerialDevice=await navigator.serial.requestPort()}catch(e){return}try{await this.replSerialDevice.open({baudRate:ESP_ROM_BAUD})}catch(e){console.error("Error. Unable to open Serial Port. Make sure it isn't already in use in another tab or application.")}await this.setupRepl(),this.nextStep()}async setupRepl(){this.replSerialDevice&&(this.repl=new REPL,this.repl.serialTransmit=this.serialTransmit.bind(this),this.replSerialDevice.addEventListener("message",this.onSerialReceive.bind(this)),this._readLoopPromise=this._readSerialLoop().catch(async function(e){await this.onReplDisconnected()}.bind(this)),this.replSerialDevice.writable)&&(this.writer=this.replSerialDevice.writable.getWriter(),await this.writer.ready)}async onReplDisconnected(e){if(this.reader){try{await this.reader.cancel()}catch(e){}this.reader=null}if(this.writer&&(await this.writer.releaseLock(),this.writer=null),this.replSerialDevice){try{await this.replSerialDevice.close()}catch(e){}this.replSerialDevice=null}}async getBootDriveName(){return this._bootDriveName||await this.extractBootloaderInfo(),this._bootDriveName}async getSerialPortName(){return this._serialPortName||await this.extractBootloaderInfo(),this._serialPortName}async _verifyPermission(e){var t={mode:"readwrite"};return"granted"===await e.queryPermission(t)||"granted"===await e.requestPermission(t)}async extractBootloaderInfo(){if(!this.bootloaderUrl)return!1;var[,e]=await this.downloadAndExtract(this.bootloaderUrl,"tinyuf2.bin"),e=await e.text();let t=e.match(/B\x00B\x00([A-Z0-9\x00]{11})FAT16/);t&&2<=t.length&&(this._bootDriveName=t[1].replace(/\0/g,"")),(t=e.match(/0123456789ABCDEF(.+)\x00UF2/))&&2<=t.length&&(this._serialPortName=t[1].replace(/\0/g," ")),this.removeCachedFile(this.bootloaderUrl.split("/").pop())}async getBootOut(e){return this.readFile("boot_out.txt",e)}async readFile(e,t=null){if(!(t=t||this.circuitpyDriveHandle))return console.warn("CIRCUITPY Drive not selected and no Directory Handle provided"),null;try{return await(await(await t.getFileHandle(e)).getFile()).text()}catch(e){return null}}async writeFile(e,t,i=null){if(!(i=i||this.circuitpyDriveHandle))return console.warn("CIRCUITPY Drive not selected and no Directory Handle provided"),null;i=await(await i.getFileHandle(e,{create:!0})).createWritable();await i.write(t),await i.close()}addCachedFile(e,t){this.fileCache.push({filename:e,blob:t})}getCachedFile(e){for(var t of this.fileCache)if(t.filename===e)return t.contents;return null}removeCachedFile(e){for(var t of this.fileCache)t.filename===e&&this.fileCache.splice(this.fileCache.indexOf(t),1)}async downloadFile(t,e){let i;try{i=await fetch(t)}catch(e){return this.errorMsg("Unable to download file: "+t),null}var s=i.body.getReader(),a=+i.headers.get("Content-Length");let r=0;for(var o=[];;){var{done:l,value:n}=await s.read();if(l)break;o.push(n),r+=n.length,e.value=Math.round(r/a*100),this.logMsg(`Received ${r} of `+a)}var h,c=new Uint8Array(r);let d=0;for(h of o)c.set(h,d),d+=h.length;return new Blob([c])}async downloadAndExtract(e,t=null,i=!1){let s=e.split("/").pop(),a=this.getCachedFile(s);var r;if(a||(this.showDialog(this.dialogs.actionProgress,{action:"Downloading "+s}),r=this.currentDialogElement.querySelector("#stepProgress"),a=await this.downloadFile(e,r),i&&this.addCachedFile(s,a)),s.endsWith(".zip")&&t){if(this.showDialog(this.dialogs.actionProgress,{action:"Extracting "+t}),[e,a]=await this.findAndExtractFromZip(a,t),!a)return void this.errorMsg(`Unable to find ${t} in `+s);s=e}return[s,a]}async downloadAndInstall(t,e=null,i=!1){var[t,e]=await this.downloadAndExtract(t,e,i);if(e){i=new Uint8Array(await e.arrayBuffer()).buffer;let s=0;this.showDialog(this.dialogs.actionProgress,{action:"Flashing "+t});const a=this.currentDialogElement.querySelector("#stepProgress");a.value=0;try{await this.espStub.flashData(i,(e,t)=>{var i=Math.round(e/t*100);i>s&&(a.value=i,this.logMsg(i+`% (${e}/${t})...`),s=i)},0,0)}catch(e){this.errorMsg(`Unable to flash file: ${t}. Error Message: `+e)}}}async downloadAndCopy(t,i=null){if(i=i||this.bootDriveHandle){var s,a=this.currentDialogElement.querySelector("#stepProgress"),[t,r]=(a.value=0,await this.downloadAndExtract(t)),o=await(await i.getFileHandle(t,{create:!0})).createWritable(),l=r.size;let e=0;for(;e{let e={};for(;0==Object.entries(e).length||null===e.ip;)e=await this.getDeviceHostInfo(),await this.sleep(300)},1e4)}catch(e){return console.warn("Unable to get IP Address. Network Credentials may be incorrect"),null}}else{if(!this.circuitpyDriveHandle)return this.errorMsg("Connect to the CIRCUITPY drive or the REPL first"),null;e=toml.stringify(e);await this.writeFile("settings.toml",e)}}async getCurrentSettings(){let e;if(this.repl)e=await this.runCode(["f = open('settings.toml', 'r')","print(f.read())","f.close()"]);else{if(!this.circuitpyDriveHandle)return this.errorMsg("Connect to the CIRCUITPY drive or the REPL first"),{};e=await this.readFile("settings.toml")}return e?toml.parse(e):(this.logMsg("Unable to read settings.toml from CircuitPython. It may not exist. Continuing..."),{})}async espDisconnect(){this.espStub&&(await this.espStub.disconnect(),this.espStub.removeEventListener("disconnect",this.espDisconnect.bind(this)),this.updateEspConnected(this.connectionStates.DISCONNECTED),this.espStub=null),this.port&&(await this.port.close(),this.port=null)}async serialTransmit(e){var t=new TextEncoder;this.writer&&(t=t.encode(e),await this.writer.ready.catch(e=>{this.errorMsg("Ready error: "+e)}),await this.writer.write(t).catch(e=>{this.errorMsg("Chunk error: "+e)}),await this.writer.ready)}async _readSerialLoop(){if(this.replSerialDevice){var e=new Event("message"),t=new TextDecoder;if(this.replSerialDevice.readable)for(this.reader=this.replSerialDevice.readable.getReader();;){var{value:i,done:s}=await this.reader.read();if(i&&(e.data=t.decode(i),this.replSerialDevice.dispatchEvent(e)),s){this.reader.releaseLock(),await this.onReplDisconnected();break}}this.logMsg("Read Loop Stopped. Closing Serial Port.")}}async getDeviceHostInfo(){return this.repl?{ip:this.repl.getIpAddress(),version:this.repl.getVersion()}:{}}hasNativeUsb(){return!(!this.chipFamily||"esp32c3".includes(this.chipFamily))}sleep(t){return new Promise(e=>setTimeout(e,t))}timeout(e,t){return Promise.race([e(),this.sleep(t).then(()=>{throw Error("Timed Out")})])}}customElements.define("cp-install-button",CPInstallButton,{extends:"button"});export{CPInstallButton}; \ No newline at end of file diff --git a/dist/package-lock.json b/dist/package-lock.json new file mode 100644 index 0000000..28c7547 --- /dev/null +++ b/dist/package-lock.json @@ -0,0 +1,14 @@ +{ + "name": "@adafruit/web-firmware-installer-js", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@adafruit/web-firmware-installer-js", + "version": "1.0.0", + "license": "MIT", + "devDependencies": {} + } + } +} diff --git a/dist/package.json b/dist/package.json new file mode 100644 index 0000000..e85cb83 --- /dev/null +++ b/dist/package.json @@ -0,0 +1,33 @@ +{ + "name": "@adafruit/web-firmware-installer-js", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, + "version": "1.0.0", + "description": "ESP32 Web-based Firmware Installation Tool for CircuitPython and more", + "main": "base_installer.js", + "dependencies": { + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git://github.com/adafruit/web-firmware-installer-js.git" + }, + "keywords": [ + "CircuitPython", + "installer", + "install", + "install tool", + "firmware", + "esp32" + ], + "author": "Melissa LeBlanc-Williams", + "license": "MIT", + "bugs": { + "url": "https://github.com/adafruit/web-firmware-installer-js/issues" + }, + "homepage": "https://github.com/adafruit/web-firmware-installer-js#readme" +}