Merge pull request #1137 from makermelissa/esptool

Initial CircuitPython Installer Coding
This commit is contained in:
Melissa LeBlanc-Williams 2023-01-31 11:48:52 -08:00 committed by GitHub
commit 81c96567b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1620 additions and 39 deletions

View file

@ -1,79 +1,83 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
colorator (1.1.0)
concurrent-ruby (1.1.9)
deep_merge (1.2.1)
concurrent-ruby (1.1.10)
deep_merge (1.2.2)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0)
eventmachine (1.2.7)
ffi (1.15.5)
forwardable-extended (2.6.0)
google-protobuf (3.21.12)
google-protobuf (3.21.12-x86_64-linux)
http_parser.rb (0.8.0)
i18n (1.8.11)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
jekyll (4.2.1)
jekyll (4.3.1)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 1.0)
jekyll-sass-converter (~> 2.0)
jekyll-sass-converter (>= 2.0, < 4.0)
jekyll-watch (~> 2.0)
kramdown (~> 2.3)
kramdown (~> 2.3, >= 2.3.1)
kramdown-parser-gfm (~> 1.0)
liquid (~> 4.0)
mercenary (~> 0.4.0)
mercenary (>= 0.3.6, < 0.5)
pathutil (~> 0.9)
rouge (~> 3.0)
rouge (>= 3.0, < 5.0)
safe_yaml (~> 1.0)
terminal-table (~> 2.0)
terminal-table (>= 1.8, < 4.0)
webrick (~> 1.7)
jekyll-get-json (1.0.0)
deep_merge (~> 1.2)
jekyll (>= 3.0)
jekyll-paginate (1.1.0)
jekyll-redirect-from (0.16.0)
jekyll (>= 3.3, < 5.0)
jekyll-sass-converter (2.1.0)
sassc (> 2.0.1, < 3.0)
jekyll-seo-tag (2.7.1)
jekyll-sass-converter (3.0.0)
sass-embedded (~> 1.54)
jekyll-seo-tag (2.8.0)
jekyll (>= 3.8, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
kramdown (2.3.1)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.3)
listen (3.7.0)
liquid (4.0.4)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.4.0)
mini_magick (4.11.0)
mini_magick (4.12.0)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (4.0.6)
public_suffix (5.0.1)
rake (13.0.6)
rb-fsevent (0.11.0)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.5)
rouge (3.26.1)
rouge (4.0.1)
ruby-vips (2.1.4)
ffi (~> 1.12)
safe_yaml (1.0.5)
sassc (2.4.0)
ffi (~> 1.9)
terminal-table (2.0.0)
unicode-display_width (~> 1.1, >= 1.1.1)
unicode-display_width (1.8.0)
sass-embedded (1.54.6)
google-protobuf (~> 3.19)
rake (>= 10.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
unicode-display_width (2.4.2)
webrick (1.7.0)
PLATFORMS

View file

@ -20,7 +20,7 @@ features:
- Breadboard-Friendly
---
- We've got a new machine here at Adafruit, it can uncover your deepest desires. Don't believe me? I'll turn it on right now to prove it to you! What, you want your very own soft serve ice cream machine? OK well, that's not something we can provide. But we can provide your *second*-deepest desire: an **ESP32-S\*3\* Feather board with a built in IPS TFT color display**. It's got all the delicious creamy goodness features of a Feather main board, the comforting warmth of an ESP32-S3 WiFi+BLE microcontroller, and the crispness of a 240x135 pixel color TFT display. All that and it will even plug in nicely into a breadboard, [terminal block wing](https://www.adafruit.com/product/2926), or [Feather Doubler](https://www.adafruit.com/product/2890) or even just stack on top of another wing.
- We've got a new machine here at Adafruit, it can uncover your deepest desires. Don't believe me? I'll turn it on right now to prove it to you! What, you want your very own soft serve ice cream machine? OK well, that's not something we can provide. But we can provide your *second*-deepest desire: an **ESP32-S*3* Feather board with a built in IPS TFT color display**. It's got all the delicious creamy goodness features of a Feather main board, the comforting warmth of an ESP32-S3 WiFi+BLE microcontroller, and the crispness of a 240x135 pixel color TFT display. All that and it will even plug in nicely into a breadboard, [terminal block wing](https://www.adafruit.com/product/2926), or [Feather Doubler](https://www.adafruit.com/product/2890) or even just stack on top of another wing.
This Feather comes with native USB and **4 MB Flash + 2 MB of PSRAM**, so it is perfect for use with CircuitPython or Arduino with low-cost WiFi. Native USB means it can act like a keyboard or a disk drive. WiFi means it's awesome for IoT projects. And Feather means it works with the large community of Feather Wings for expandability.

View file

@ -7,10 +7,10 @@
"version": "v3.14.0"
},
"esp32s2": {
"version": "0.11.0"
"version": "0.12.0"
},
"esp32s3": {
"version": "0.11.0"
"version": "0.12.0"
},
"esp32c3": {},
"esp32": {},

View file

@ -6,6 +6,9 @@
{% if board[0] %}
{% assign stable = board[0].versions | where: "stable", true | sort: "version" %}
{% assign unstable = board[0].versions | where: "stable", false | sort: "version" %}
{% assign bootloaders = site.data.bootloaders.bootloaders %}
{% assign bootloader_version = bootloaders[page.family].version %}
{% assign bootloader_id = page.bootloader_id %}
{% assign releases = stable | concat: unstable %}
{% capture board_url %}https://downloads.circuitpython.org/bin/{{ board_id}}{% endcapture %}
{% for version in releases %}
@ -111,6 +114,19 @@
{% for extension in version.extensions %}
<a class="download-button {% if version.stable %}stable{% else %}unstable{% endif %} {{ extension }}" href="{{ board_url }}/en_US/adafruit-circuitpython-{{ board_id }}-en_US-{{ version.version }}.{{ extension }}">DOWNLOAD .{{ extension | upcase }} NOW <i class="fas fa-download" aria-hidden="true"></i></a>
{% endfor %}
{% if page.family == 'esp32s2' or page.family == 'esp32c3' or page.family == 'esp32s3' or page.family == 'esp32' %}
<button is="cp-install-button" class="installer-button" boardname="{{ page.name }}" boardid="{{ board_id }}"
{% for extension in version.extensions %}
{{ extension }}file="{{ board_url }}/en_US/adafruit-circuitpython-{{ board_id }}-en_US-{{ version.version }}.{{ extension }}"
{% endfor %}
{% if bootloader_version and bootloader_id %}
bootloader="https://github.com/adafruit/tinyuf2/releases/download/{{ bootloader_version }}/tinyuf2-{{ bootloader_id }}-{{ bootloader_version }}.zip"
bootloaderid="{{ bootloader_id }}"
{% endif %}
version="{{ version.version }}"
chipfamily="{{ page.family }}"
>OPEN INSTALLER <i class="fas fa-magic" aria-hidden="true"></i></button>
{% endif %}
</div>
</div>
{% if version.modules %}
@ -132,7 +148,7 @@
<h3>Absolute Newest</h3>
<p>
Every time we commit new code to CircuitPython we automatically
build binaries for each board and language. The binaries are
build binaries for each board and language. The binaries are
stored on Amazon S3, organized by board, and then by
language. Try them if you want the absolute latest and are
feeling daring or want to see if a problem has been fixed.
@ -146,10 +162,10 @@
<h3>Past Releases</h3>
<p>
All previous releases are listed on GitHub, with release notes,
and are available for download from Amazon S3. They are handy for
and are available for download from Amazon S3. They are handy for
testing, but otherwise we recommend using the latest stable
release. Some older GitHub release pages include the same
binaries for downloading. But we have discontinued including
release. Some older GitHub release pages include the same
binaries for downloading. But we have discontinued including
binaries as assets on newer release pages because of the large
number of files for each release.
</p>
@ -168,11 +184,7 @@ version of its bootloader family.
By the way, boolean operation precedence is right to left! (yeesh)
{% endcomment %}
{% assign bootloaders = site.data.bootloaders.bootloaders %}
{% assign bootloader_version = bootloaders[page.family].version %}
{% assign bootloader_id = page.bootloader_id %}
{% if bootloader_version and bootloader_id %}
{% if page.family == 'esp32s2' or page.family == 'esp32c3' or page.family == 'esp32s3' %}
<div class="section unrecommended">
<h3>Install, Repair, or Update UF2 Bootloader</h3>

View file

@ -27,4 +27,7 @@ layout: default
</div>
</div>
{% if page.family == 'esp32' or page.family == 'esp32s2' or page.family == 'esp32c3' or page.family == 'esp32s3' %}
<script src="/cpinstaller/src/cpinstaller.js" type="module"></script>
{% endif %}
<script src="/assets/javascript/download.js"></script>

View file

@ -21,3 +21,4 @@
@import 'pages/stats';
@import 'pages/libraries';
@import 'pages/contributing';
@import 'pages/espinstaller';

View file

@ -44,13 +44,14 @@
}
.download {
a.download-button, a.download-button-unrecommended {
a.download-button, a.download-button-unrecommended, button[is="cp-install-button"] {
display: inline-block;
width: auto;
padding: 15px 30px 15px 30px;
color: #fff;
border-radius: 5px;
border: none;
cursor: pointer;
i {
padding-left: 10px;
@ -143,8 +144,14 @@
}
}
button[is="cp-install-button"] {
color: $purple;
background-color: transparent;
border: 2px solid $purple;
}
.download-buttons {
a {
a, button {
width: 100%;
height: 49px;
padding: 0;

View file

@ -0,0 +1,110 @@
.cp-installer-dialog {
min-width: 300px;
min-height: 100px;
max-width: 600px;
padding-bottom: 50px;
border-radius: 15px;
&::backdrop {
backdrop-filter: blur(2px);
}
.close-button {
position: absolute;
right: 10px;
top: 10px;
width: 24px;
height: 24px;
opacity: 0.3;
background: transparent;
border: none;
&:hover {
opacity: 1;
}
&:before, &:after {
position: absolute;
left: 12px;
top: 0;
content: ' ';
height: 25px;
width: 2px;
background-color: #333;
}
&:before {
transform: rotate(45deg);
}
&:after {
transform: rotate(-45deg);
}
}
.dialog-body {
margin-top: 24px;
p.centered {
text-align: center;
}
progress {
width: 100%;
}
.loader {
display: block;
position: relative;
width: 80px;
height: 80px;
margin: 0 auto;
div {
box-sizing: border-box;
display: block;
position: absolute;
width: 64px;
height: 64px;
margin: 8px;
border: 8px solid #fff;
border-radius: 50%;
animation: loader 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #3498db transparent transparent transparent;
&:nth-child(1) {
animation-delay: -0.45s;
}
&:nth-child(2) {
animation-delay: -0.3s;
}
&:nth-child(3) {
animation-delay: -0.15s;
}
}
}
@-webkit-keyframes loader {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes loader {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
}
.dialog-navigation {
display: flex;
justify-content: space-evenly;
position: absolute;
bottom: 20px;
padding-left: inherit;
padding-right: inherit;
left: 0;
right: 0;
button {
margin: 10px 20px;
}
}
}

View file

@ -0,0 +1,147 @@
<!--
// Wizard screens
// - Menu (installerMenu)
// - Verify user wants to install (installerVerify)
// - erase flash (installerErase)
// - if esp32 or c3 flash bin (installerFlash)
// - if s2 or s3, flash bootloader (installerFlash)
// - if s2 or s3, copy uf2 (May need to use File System Access API)
// - request wifi credentials (skip, connect buttons) and AP password
// - generate and program settings.toml via REPL
// - install complete
-->
<dialog class="installer" id="installerMenu">
</dialog>
<dialog class="installer" id="installerVerify">
</dialog>
<dialog class="installer" id="installerErase">
</dialog>
<!-- Used for flashing either bin or uf2 -->
<dialog class="installer" id="installerFlash">
</dialog>
<dialog class="installer" id="installerCopyUf2">
</dialog>
<dialog class="installer" id="installerCredentials">
</dialog>
<dialog class="installer" id="installerGenerateSettings">
</dialog>
<dialog class="installer" id="installerSuccess">
</dialog>
<dialog class="installer" id="installerError">
</dialog>
<dialog id="installerDialog">
<main class="main">
<div id="notSupported" class="notSupported">
Sorry, <b>Web Serial</b> is not supported on your browser at this time. Browsers we expect to work:
<ul>
<li>Google Chrome 89 (and higher)</li>
<li>Microsoft Edge 89 (and higher)</li>
<li>Opera 75 (and higher)</li>
</ul>
</div>
<div class="subheader">
<div class="left">
<span class="title">ESP32 CircuitPython Installer</span>
<span><strong>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.</strong></span>
</div>
</div>
<div id="app">
<ol class="tutorial-steps">
<li class="step-1">
<p>
Make sure your board is plugged into this computer via a Serial connection using a USB Cable.
</p>
<ul>
<li><p><em><strong>NOTE:</strong> 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.</em></p></li>
</ul>
</li>
<li class="step-1">
<p>
<button id="butConnect" type="button">Connect</button>
Click this button to open the Web Serial connection menu.
</p>
<p>There may be many devices listed, such as your remembered Bluetooth peripherals, anything else plugged into USB, etc.</p>
<p>
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.
</p>
</li>
<li class="step-2 hidden">
<p><strong>Do not disconnect your board from your computer from this point!</strong></p>
</li>
<li class="step-2 hidden">
<p>Enter the credentials for the wifi network your board will connect to, as well as the initial Web Workflow password.</p>
<ul>
<li><p><em><strong>NOTE:</strong> Your wifi network must be using 2.4Ghz frequency.</em></p></li>
</ul>
<div id="commands">
<div class="field">
<label><span>WiFi Network Name (SSID):</span>
<input id="network_type_wifi.network_ssid" class="partition-data" type="text" placeholder="WiFi SSID" value="" />
</label>
</div>
<div class="field">
<label><span>WiFi Password:</span>
<input id="network_type_wifi.network_password" class="partition-data" type="text" placeholder="WiFi Password" value="" />
</label>
</div>
<div class="field">
<label><span>Web Workflow Password:</span>
<input id="web_workflow_password" class="partition-data" type="text" placeholder="Web Workflow Password" value="" />
</label>
</div>
</div>
</li>
<li class="step-3 hidden">
<p>Click "Install CircuitPython" to get the process started. If you would like to manually install CircuitPython and simply want to perform a factory reset, click "Install UF2 Bootloader ONLY".</p>
<div class="buttons" style="text-align: center;">
<button id="butProgram" type="button">Install CircuitPython</button>
<button id="butProgramBootloader" type="button">Install UF2 Bootloader ONLY</button>
</div>
</li>
<li class="step-4 hidden">
<p>CircuitPython is installing, please wait...</p>
<div class="buttons">
<div id="stepname" class="hidden"></div>
</div>
<div class="buttons">
<div id="progressBar" class="progress-bar hidden"><div></div></div>
</div>
</li>
<li class="step-5 hidden">
<p>CircuitPython successfully installed. <strong>Reset your board</strong>!</a></p>
</li>
</ol>
</div>
<div id="log" class="hidden console-item"></div>
</main>
</dialog>

View file

@ -0,0 +1,440 @@
'use strict';
import {html, render} from 'https://unpkg.com/lit-html?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 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.serial = null;
this.espStub = null;
this.mode = "";
this.dialogCssClass = "install-dialog";
this.connected = this.connectionStates.DISCONNECTED;
}
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: () => { return this.currentStep > 0 },
}
nextButton = {
label: "Next",
onClick: this.nextStep,
isEnabled: () => { 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, <b>Web Serial</b> is not supported on your browser at this time. Browsers we expect to work:
<ul>
<li>Google Chrome 89 (and higher)</li>
<li>Microsoft Edge 89 (and higher)</li>
<li>Opera 75 (and higher)</li>
</ul>
`,
buttons: [this.closeButton],
},
menu: {
closeable: true,
template: (data) => html`
<p>CircuitPython Installer for ${data.boardName}</p>
<ul class="flow-menu">
${this.generateMenu(
(flowId, flow) => html`<li><a href="#" @click=${this.runFlow.bind(this)} id="${flowId}">${flow.label.replace('[version]', this.releaseVersion)}</a></li>`
)}
</ul>`,
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.mode = this.getUrlParam("mode");
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;
}
enabledFlowCount() {
let enabledFlowCount = 0;
for (const [flowId, flow] of Object.entries(this.flows)) {
if (flow.isEnabled()) {
enabledFlowCount++;
}
}
return enabledFlowCount;
}
* generateMenu(templateFunc) {
if (this.enabledFlowCount() == 0) {
yield html`<li>Coming soon. Check back later.</li>`;
//yield html`<li>No installable options available for this board.</li>`;
}
for (const [flowId, flow] of Object.entries(this.flows)) {
if (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 = !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) {
// TODO: Eventually add to an internal log that the user can bring up
console.info(this.stripHtml(text));
}
updateUIConnected(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, {boardName: this.boardName});
}
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 == esptoolPackage.ESP_ROM_BAUD) { return } // already the default
if (chipType == esptoolPackage.CHIP_FAMILY_ESP32) { // only supports the default
this.logMsg("WARNING: ESP32 is having issues working at speeds faster than 115200. Continuing at 115200 for now...");
return
}
await this.changeBaudRate(baud);
}
async changeBaudRate(baud) {
if (this.espStub && this.baudRates.includes(baud)) {
await this.espStub.setBaudrate(baud);
}
}
}

View file

@ -0,0 +1,856 @@
'use strict';
import { html } from 'https://unpkg.com/lit-html?module';
import * as zip from "https://deno.land/x/zipjs/index.js";
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.0.0/repl.js';
import { InstallButton } from "./base_installer.js";
// 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
//
const PREFERRED_BAUDRATE = 921600;
const CSS_DIALOG_CLASS = "cp-installer-dialog";
const FAMILY_TO_CHIP_MAP = {
'esp32s2': esptoolPackage.CHIP_FAMILY_ESP32S2,
'esp32s3': esptoolPackage.CHIP_FAMILY_ESP32S3,
'esp32c3': esptoolPackage.CHIP_FAMILY_ESP32C3,
'esp32': esptoolPackage.CHIP_FAMILY_ESP32
}
export class CPInstallButton extends InstallButton {
constructor() {
super();
this.releaseVersion = "[version]";
this.boardName = "ESP32-based device";
this.boardId = null;
this.bootloaderUrl = "";
this.uf2FileUrl = "";
this.binFileUrl = "";
this.releaseVersion = 0;
this.chipFamily = null;
this.bootloadId = null;
this.dialogCssClass = CSS_DIALOG_CLASS;
this.dialogs = { ...this.dialogs, ...this.cpDialogs };
this.init();
}
connectedCallback() {
this.boardName = this.getAttribute("boardname") || "ESP32-based device";
// If this is empty, it's a problem
this.boardId = this.getAttribute("boardid");
this.releaseVersion = this.getAttribute("version");
// We need either the bootloader and uf2 or bin file to continue
this.bootloaderUrl = this.getAttribute("bootloader");
this.uf2FileUrl = this.getAttribute("uf2file");
this.binFileUrl = this.getAttribute("binfile");
// Nice to have for now
this.chipFamily = this.getAttribute("chipfamily");
this.bootloadId = this.getAttribute("bootloadid"); // This could be used to check serial output from board matches the UF2 file
super.connectedCallback();
}
// This may end up moving to a superclass that extends the installer
// These are a series of the valid steps that should be part of a program flow
flows = {
binProgram: {
label: `Install CircuitPython [version] Bin`,
steps: [this.stepSerialConnect, this.stepConfirm, this.stepEraseAll, this.stepFlashBin, this.stepSuccess],
isEnabled: () => { return this.mode=="test" && !!this.binFileUrl },
},
uf2Program: {
label: `Install CircuitPython [version] UF2 and Bootloader`,
steps: [this.stepSerialConnect, this.stepConfirm, this.stepEraseAll, this.stepBootloader, this.stepCopyUf2, this.stepSettings, this.stepSuccess],
isEnabled: () => { return this.mode=="test" && !!this.bootloaderUrl && !!this.uf2FileUrl },
},
bootloaderOnly: {
label: "Install Bootloader Only",
steps: [this.stepSerialConnect, this.stepConfirm, this.stepEraseAll, this.stepBootloader, this.stepSuccess],
isEnabled: () => { return this.mode=="test" && !!this.bootloaderUrl },
},
settingsOnly: {
label: "Update WiFi credentials",
steps: [this.stepSerialConnect, this.stepCredentials, this.stepSettings, this.stepSuccess],
isEnabled: () => { return this.mode=="test" && this.cpDetected() },
}
}
// This is the data for the dialogs
cpDialogs = {
serialConnect: {
closeable: true,
template: (data) => html`
<p>
Welcome to the CircuitPython Installer. This tool will install CircuitPython on your ${data.boardName}.
</p>
<p>Make sure your board is plugged into this computer via a Serial connection using a USB Cable.
</p>
<ul>
<li><em><strong>NOTE:</strong> 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.</em></li>
</ul>
<p>
<button id="butConnect" type="button" @click=${this.connectHandler.bind(this)}>Connect</button>
Click this button to open the Web Serial connection menu.
</p>
<p>There may be many devices listed, such as your remembered Bluetooth peripherals, anything else plugged into USB, etc.</p>
<p>
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.
</p>
`,
buttons: [this.previousButton, {
label: "Next",
onClick: this.nextStep,
isEnabled: () => { return (this.currentStep < this.currentFlow.steps.length - 1) && this.connected == this.connectionStates.CONNECTED },
onUpdate: async (e) => { console.log("updating"); this.currentDialogElement.querySelector("#butConnect").innerText = this.connected; },
}],
},
confirm: {
template: (data) => html`
<p>This will overwrite everything on the ${data.boardName}.</p>
`,
buttons: [
this.previousButton,
{
label: "Continue",
onClick: this.nextStep,
}
],
},
erase: {
template: (data) => html`
<p class="centered">Erasing Flash...</p>
<div class="loader"><div></div><div></div><div></div><div></div></div>
`,
buttons: [],
},
flash: {
template: (data) => html`
<p class="centered">${data.action} ${data.file}...</p>
<progress id="flashProgress" max="100" value="0"></progress>
`,
buttons: [],
},
// We may have a waiting for Bootloader to start dialog
copyUf2: {
template: (data) => html`
<p class="centered">Copying ${data.file}...</p>
<progress id="copyProgress" max="100" value="0"></progress>
`,
buttons: [this.nextButton],
},
credentials: {
template: (data) => html`
<div class="field">
<label for="circuitpy_wifi_ssid">WiFi Network Name (SSID):</label>
<input id="circuitpy_wifi_ssid" class="setting-data" type="text" placeholder="WiFi SSID" value="" />
</div>
<div class="field">
<label for="circuitpy_wifi_password">WiFi Password:</label>
<input id="circuitpy_wifi_password" class="setting-data" type="text" placeholder="WiFi Password" value="" />
</div>
<div class="field">
<label for="circuitpy_web_api_password">Web Workflow API Password:</label>
<input id="circuitpy_web_api_password" class="setting-data" type="text" placeholder="Web Workflow API Password" value="" />
</div>
<div class="field">
<!-- Alternatively "Disable USB Mass Storage" -->
<input id="circuitpy_drive" class="setting" type="checkbox" value="disabled" checked />
<label for="circuitpy_drive">Disable CIRCUITPY Drive (Required for write access)</label>
</div>
`,
},
circuitPythonCheck: {
template: (data) => html`
<p>Looking for CircuitPython...</p>
<progress id="copyProgress" max="100" value="${data.percentage}"> ${data.percentage}% </progress>
`,
},
setUpWebWorkflow: {
template: (data) => html`
<p>Setting up Web Workflow...</p>
<progress id="copyProgress" max="100" value="${data.percentage}"> ${data.percentage}% </progress>
`,
},
success: {
closeable: true,
template: (data) => html`
<p>Successfully Completed Installation</p>
`,
buttons: [this.closeButton],
},
error: {
closeable: true,
template: (data) => html`
<p>Installation Error: ${data.message}</p>
`,
closeable: false,
buttons: [this.closeButton],
},
}
async connectHandler(e) {
await this.disconnect();
let esploader;
try {
esploader = await esptoolPackage.connect({
log: (...args) => this.logMsg(...args),
debug: (...args) => {},
error: (...args) => this.errorMsg(...args),
});
} catch (err) {
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.updateUIConnected(this.connectionStates.CONNECTING);
await esploader.initialize();
this.updateUIConnected(this.connectionStates.CONNECTED);
} catch (err) {
await esploader.disconnect();
// Disconnection before complete
this.updateUIConnected(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) {
console.log("This chip checks out");
this.espStub = await esploader.runStub();
this.espStub.addEventListener("disconnect", () => {
this.updateUIConnected(this.connectionStates.DISCONNECTED);
this.espStub = null;
});
await this.setBaudRateIfChipSupports(esploader.chipFamily, PREFERRED_BAUDRATE);
return
}
// Can't use it so disconnect now
this.errorMsg("Oops, this is the wrong firmware for your board.")
await this.disconnect()
} catch (err) {
await esploader.disconnect();
// Disconnection before complete
this.updateUIConnected(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 stepSerialConnect() {
// Display Serial Connect Dialog
this.showDialog(this.dialogs.serialConnect, {boardName: this.boardName});
}
async stepConfirm() {
// Display Confirm Dialog
this.showDialog(this.dialogs.confirm, {boardName: this.boardName});
}
async stepEraseAll() {
// Display Erase Dialog
this.showDialog(this.dialogs.erase);
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.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');
// TODO: Reboot into bootloader
await this.nextStep();
}
async stepCopyUf2() {
// Display CopyUf2 Dialog
this.showDialog(this.dialogs.copyUf2, {file: this.uf2FileUrl});
}
async stepSettings() {
// Display Settings Dialog
this.showDialog(this.dialogs.settings);
}
async stepCredentials() {
// Display Credentials Request Dialog
this.showDialog(this.dialogs.credentials);
}
async stepWebWorkflow() {
// Display Dialog to set up Web Workflow
// Wait for CircuitPython to be detected
// Write the Settings.toml file
// If Disable CIRCUITPY Drive was checked, write the boot.py file
// Reboot
this.showDialog(this.dialogs.setUpWebWorkflow);
}
async stepSuccess() {
// Display Success Dialog
this.showDialog(this.dialogs.success);
// If we were setting up Web Workflow, we may want to provide a link to code.circuitpython.org
// Alternatively, we may want a separate dialog with a link
}
async stepClose() {
// Close the currently loaded dialog
this.closeDialog();
}
cpDetected() {
// TODO: Actually detect CircuitPython
// We may also want to have it return the version number and return null if not detected
return false;
}
async downloadFile(url, progressElement) {
let response;
try {
response = await fetch(url, {mode: "cors"});
} 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;
console.log(`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 downloadAndInstall(url, fileToExtract = null) {
// Display Flash Dialog
let filename = url.split("/").pop();
this.showDialog(this.dialogs.flash, {
action: "Downloading",
file: filename,
});
const progressElement = this.currentDialogElement.querySelector("#flashProgress");
// Download the file at the url updating the progress in the process
let fileContents = await this.downloadFile(url, progressElement);
// If the file is a zip file, unzip and find the file to extract
if (filename.endsWith(".zip") && fileToExtract) {
let foundFile;
console.log("Extracting step");
// Update the flash dialog
this.showDialog(this.dialogs.flash, {
action: "Extracting",
file: fileToExtract,
});
// Set that to the current file to flash
[foundFile, fileContents] = await this.findAndExtractFromZip(fileContents, fileToExtract);
if (!fileContents) {
this.errorMsg("Unable to find " + fileToExtract + " in " + filename);
return;
}
filename = foundFile;
}
// Update the flash dialog
if (fileContents) {
console.log("Flash step");
let lastPercent = 0;
this.showDialog(this.dialogs.flash, {
action: "Flashing",
file: filename,
});
try {
await this.espStub.flashData(fileContents, (bytesWritten, totalBytes) => {
let percentage = Math.floor((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);
console.log(err);
}
}
}
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) {
console.log(filename, zipEntry.filename, zipEntry.filename.localeCompare(filename));
if (zipEntry.filename.localeCompare(filename) === 0) {
const extractedFile = await zipEntry.getData(new zip.Uint8ArrayWriter());
return [zipEntry.filename, extractedFile.buffer]; // ESPTool wants an ArrayBuffer
}
}
return [null, null];
}
}
customElements.define('cp-install-button', CPInstallButton, {extends: "button"});
// Changes to make:
// Hide the log and make it accessible via the menu (future feature, console.log for now)
// Generate dialogs on the fly
// Make a drop-in component
// Keep in mind it will be used for LEARN too
// May need to deal with CORS issues
// May need to deal with the fact that the ESPTool uses Web Serial and CircuitPython REPL uses Web Serial
/*
const maxLogLength = 100;
const disableWhileBusy = [partitionData, butProgram, butProgramBootloader, baudRate];
document.addEventListener("DOMContentLoaded", () => {
// 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
this.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();
updateUIConnected(this.connectionStates.DISCONNECTED);
});
});
//butClear.addEventListener("click", clickClear);
butProgram.addEventListener("click", clickProgram);
butProgramBootloader.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);
// 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();
this.logMsg("CircuitPython ESP32 Installer loaded.");
checkProgrammable();
});
let latestFirmwares = []
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
return true
}
}
function hasBoard(board) {
for (let opt of binSelector.options) {
if (opt.value == board) { return opt }
}
}
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
}
let lastPercent = 0;
async function clickAutoscroll() {
saveSetting("autoscroll", autoscroll.checked);
}
async function clickProgram() {
await programScript(full_bin_program);
}
async function clickProgramNvm() {
await programScript(factory_reset_program);
}
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);
}
}
// 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;
}
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) {
this.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)
this.logMsg(`Flashing with settings: <pre>${strippedSettings}</pre>`)
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 this.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 this.espStub.flashData(
firmware,
(bytesWritten, totalBytes
) => {
let percentage = Math.floor((bytesWritten / totalBytes) * 100)
if (percentage != lastPercent) {
this.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 this.espStub.flashData(
firmware,
(bytesWritten, totalBytes
) => {
let percentage = Math.floor((bytesWritten / totalBytes) * 100)
if (percentage != lastPercent) {
this.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
// and run python code via the repl to write a settings.toml file
// See https://learn.adafruit.com/circuitpython-with-esp32-quick-start/setting-up-web-workflow
// and https://github.com/circuitpython/web-editor/pull/46
steps.push({
name: "Generating and Writing the WiFi Settings",
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 this.espStub.flashData(
new Uint8Array(fileSystemImage).buffer,
(bytesWritten, totalBytes) => {
let percentage = Math.floor((bytesWritten / totalBytes) * 100)
if (percentage != lastPercent) {
this.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();
this.logMsg("To run the new firmware, please reset your device.");
showStep(6);
}
*/

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#fff;opacity:0;}.cls-2{fill:#231f20;}</style></defs><title>close</title><g id="Layer_2" data-name="Layer 2"><g id="close"><g id="close-2" data-name="close"><rect class="cls-1" width="24" height="24" transform="translate(24 24) rotate(180)"/><path class="cls-2" d="M13.41,12l4.3-4.29a1,1,0,1,0-1.42-1.42L12,10.59,7.71,6.29A1,1,0,0,0,6.29,7.71L10.59,12l-4.3,4.29a1,1,0,0,0,0,1.42,1,1,0,0,0,1.42,0L12,13.41l4.29,4.3a1,1,0,0,0,1.42,0,1,1,0,0,0,0-1.42Z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 553 B