Unzipping and Flashing now working

This commit is contained in:
Melissa LeBlanc-Williams 2023-01-16 17:10:20 -08:00
parent af54b570bb
commit 00d234801e
6 changed files with 210 additions and 178 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

@ -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

@ -117,7 +117,9 @@
{% 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 %}
{% comment %}
{{ extension }}file="{{ board_url }}/en_US/adafruit-circuitpython-{{ board_id }}-en_US-{{ version.version }}.{{ extension }}"
{% endcomment %}
{% endfor %}
{% if bootloader_version and bootloader_id %}
bootloader="https://github.com/adafruit/tinyuf2/releases/download/{{ bootloader_version }}/tinyuf2-{{ bootloader_id }}-{{ bootloader_version }}.zip"

Binary file not shown.

View file

@ -1,5 +1,6 @@
'use strict';
import {html, render} from 'https://unpkg.com/lit-html?module';
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 { InstallButton } from "./installer.js";
@ -62,12 +63,15 @@ export class CPInstallButton extends InstallButton {
// 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");
if (this.bootloaderUrl) {
this.bootloaderUrl = `/bin/${this.bootloaderUrl.split("/").pop()}`;
}
this.uf2FileUrl = this.getAttribute("uf2file");
this.binFileUrl = this.getAttribute("binfile");
this.releaseVersion = this.getAttribute("version");
// Nice to have for now
this.chipFamily = this.getAttribute("chipfamily");
@ -104,6 +108,7 @@ export class CPInstallButton extends InstallButton {
// 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}.
@ -148,24 +153,21 @@ export class CPInstallButton extends InstallButton {
<p class="centered">Erasing Flash...</p>
<div class="loader"><div></div><div></div><div></div><div></div></div>
`,
closeable: false,
buttons: [],
},
flash: {
template: (data) => html`
<p class="centered">Flashing ${data.contents}...</p>
<progress id="flashProgress" max="100" value="${data.percentage}"> ${data.percentage}% </progress>
<p class="centered">${data.action} ${data.file}...</p>
<progress id="flashProgress" max="100" value="0"></progress>
`,
closeable: false,
buttons: [this.nextButton],
buttons: [],
},
// We may have a waiting for Bootloader to start dialog
copyUf2: {
template: (data) => html`
<p class="centered">Copying ${data.uf2file}...</p>
<progress id="copyProgress" max="100" value="${data.percentage}"> ${data.percentage}% </progress>
<p class="centered">Copying ${data.file}...</p>
<progress id="copyProgress" max="100" value="0"></progress>
`,
closeable: false,
buttons: [this.nextButton],
},
credentials: {
@ -200,12 +202,14 @@ export class CPInstallButton extends InstallButton {
`,
},
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>
`,
@ -254,11 +258,10 @@ export class CPInstallButton extends InstallButton {
});
await this.setBaudRateIfChipSupports(esploader.chipFamily, PREFERRED_BAUDRATE);
console.log("Done");
return
}
// can't use it so disconnect now
// Can't use it so disconnect now
this.errorMsg("Oops, this is the wrong firmware for your board.")
await this.disconnect()
@ -271,17 +274,17 @@ export class CPInstallButton extends InstallButton {
}
async stepSerialConnect() {
// Display Serial Connect Text
// Display Serial Connect Dialog
this.showDialog(this.dialogs.serialConnect, {boardName: this.boardName});
}
async stepConfirm() {
// Display Confirm Step
// Display Confirm Dialog
this.showDialog(this.dialogs.confirm, {boardName: this.boardName});
}
async stepEraseAll() {
// Display EraseAll Step
// Display Erase Dialog
this.showDialog(this.dialogs.erase);
try {
await this.espStub.eraseFlash();
@ -292,40 +295,45 @@ export class CPInstallButton extends InstallButton {
}
async stepFlashBin() {
// Display FlashBin Step
this.showDialog(this.dialogs.flash, {contents: "Bin File"});
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;
}
// TODO: This should go to the next step automatically when finished
await this.downloadAndInstall(this.binFileUrl);
await this.nextStep();
}
async stepBootloader() {
// Display Bootloader Step
this.showDialog(this.dialogs.flash, {contents: "Bootloader"});
// TODO:
// Download zip file (this may need to come from Amazon S3)
// Unzip file
// Program combined.bin
// Reboot
// Go to next step
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 Step
this.showDialog(this.dialogs.copyUf2);
// Display CopyUf2 Dialog
this.showDialog(this.dialogs.copyUf2, {file: this.uf2FileUrl});
}
async stepSettings() {
// Display Settings Step
// Display Settings Dialog
this.showDialog(this.dialogs.settings);
}
async stepCredentials() {
// Display Credentials Step
// Display Credentials Request Dialog
this.showDialog(this.dialogs.credentials);
}
async stepSuccess() {
// Display Success Step
// Display Success Dialog
this.showDialog(this.dialogs.success);
}
@ -339,24 +347,120 @@ export class CPInstallButton extends InstallButton {
// 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"});
// Wizard screens
// - Menu
// - Verify user wants to install
// - erase flash
// - if esp32 or c3 flash bin
// - if s2 or s3, flash bootloader
// - 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
// So we will have a couple of wizard flows and we'll need to associate the flow with the board
// We should add the info to the board's page in the boards folder
// Changes to make:
// Hide the log and make it accessible via the menu (future feature, console.log for now)
// Generate dialogs on the fly
@ -548,32 +652,7 @@ function updateObject(obj, path, value) {
}
let chipFiles
async function fetchFirmwareForSelectedBoard() {
const firmware = lookupFirmwareByBinSelector()
this.logMsg(`Fetching latest firmware...`)
const response = await fetch(`${FIRMWARE_API}/wipper_releases/${firmware.id}`, {
headers: { Accept: 'application/octet-stream' }
})
// Zip stuff
this.logMsg("Unzipping firmware bundle...")
const blob = await response.blob()
const reader = new zip.ZipReader(new zip.BlobReader(blob));
// unzip into local file cache
chipFiles = await reader.getEntries();
}
function findInZip(filename) {
const regex = RegExp(filename.replace("VERSION", "(.*)"))
for (let i = 0; i < chipFiles.length; i++) {
if (chipFiles[i].filename.match(regex)) {
return chipFiles[i]
}
}
}
async function mergeSettings() {
const { settings } = lookupFirmwareByBinSelector()
@ -767,67 +846,4 @@ async function programScript(stages) {
this.logMsg("To run the new firmware, please reset your device.");
showStep(6);
}
function getValidFields() {
// Validate user inputs
const validFields = [];
for (let i = 0; i < 3; i++) {
const { id, value } = partitionData[i]
// password & brightness can be blank, the rest must have some value
if (id === "network_type_wifi.network_password" ||
value.length > 0) {
validFields.push(i);
}
}
return validFields;
}
async function checkProgrammable() {
if (getValidFields().length < 5) {
hideStep(4)
} else {
showStep(4, { dimLowerSteps: false })
}
}
async function clickClear() {
reset();
}
function loadAllSettings() {
// Load all saved settings or defaults
//autoscroll.checked = loadSetting("autoscroll", true);
//baudRate.value = loadSetting("baudrate", this.baudRates[0]);
showConsole = loadSetting('showConsole', false);
toggleConsole(showConsole);
}
function loadSetting(setting, defaultValue) {
return JSON.parse(window.localStorage.getItem(setting)) || defaultValue;
}
function saveSetting(setting, value) {
window.localStorage.setItem(setting, JSON.stringify(value));
}
async function getFirmware(filename) {
const file = findInZip(filename)
if (!file) {
const msg = `No firmware file name ${filename} found in the zip!`
this.errorMsg(msg)
throw new Error(msg)
}
this.logMsg(`Unzipping ${filename.replace('VERSION', semver)}...`)
const firmwareFile = await file.getData(new zip.Uint8ArrayWriter())
return firmwareFile.buffer // ESPTool wants an ArrayBuffer
}
async function getFileText(path) {
let response = await fetch(path);
let contents = await response.text();
return contents;
}
*/

View file

@ -83,6 +83,7 @@ export class InstallButton extends HTMLButtonElement {
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>
@ -94,6 +95,7 @@ export class InstallButton extends HTMLButtonElement {
buttons: [this.closeButton],
},
menu: {
closeable: true,
template: (data) => html`
<p>CircuitPython Installer for ${data.boardName}</p>
<ul class="flow-menu">
@ -109,21 +111,21 @@ export class InstallButton extends HTMLButtonElement {
baudRates = [
115200,
128000,
153600,
230400,
460800,
921600,
1500000,
2000000,
];
connectedCallback() {
if (!InstallButton.isSupported || !InstallButton.isAllowed) {
this.toggleAttribute("install-unsupported", true);
this.innerHTML = !InstallButton.isAllowed
? "<slot name='not-allowed'>You can only install ESP devices on HTTPS websites or on the localhost.</slot>"
: "<slot name='unsupported'>Your browser does not support installing things on ESP devices. Use Google Chrome or Microsoft Edge.</slot>";
return;
}
if (InstallButton.isSupported && InstallButton.isAllowed) {
this.toggleAttribute("install-supported", true);
} else {
this.toggleAttribute("install-unsupported", true);
}
this.addEventListener("click", async (e) => {
e.preventDefault();
@ -136,7 +138,20 @@ export class InstallButton extends HTMLButtonElement {
});
}
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>No installable options for this board.</li>`;
}
for (const [flowId, flow] of Object.entries(this.flows)) {
if (flow.isEnabled()) {
yield templateFunc(flowId, flow);
@ -254,10 +269,6 @@ export class InstallButton extends HTMLButtonElement {
}
showDialog(dialog, templateData = {}) {
let dialogButtons;
// TODO: Add ability to stack dialogs by making this.currentDialogElement work as an array
// We also may want to make it so we just get the top step type dialog
if (this.currentDialogElement) {
this.closeDialog();
}
@ -274,16 +285,15 @@ export class InstallButton extends HTMLButtonElement {
}
// 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 = "none";
} else {
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;
} else {
dialogButtons = this.defaultButtons;
}
this.updateButtons();