Skip to content

Commit 31cb17d

Browse files
authored
Merge pull request #11 from makermelissa/main
Updated to use esptool-js backend
2 parents 5109645 + 83d9ff7 commit 31cb17d

File tree

3 files changed

+132
-87
lines changed

3 files changed

+132
-87
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ on:
44
push:
55
branches:
66
- main
7+
# Allows you to run this workflow manually from the Actions tab
8+
workflow_dispatch:
79

810
jobs:
911
build:

base_installer.js

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
'use strict';
66
import {html, render} from 'https://cdn.jsdelivr.net/npm/lit-html/+esm';
77
import {asyncAppend} from 'https://cdn.jsdelivr.net/npm/lit-html/directives/async-append/+esm';
8-
import * as esptoolPackage from "https://cdn.jsdelivr.net/npm/esp-web-flasher@5.1.2/dist/web/index.js/+esm"
9-
8+
import { ESPLoader, Transport } from "https://unpkg.com/esptool-js@0.5.3/bundle.js";
109
export const ESP_ROM_BAUD = 115200;
1110

1211
export class InstallButton extends HTMLButtonElement {
@@ -15,12 +14,15 @@ export class InstallButton extends HTMLButtonElement {
1514

1615
constructor() {
1716
super();
17+
this.baudRate = ESP_ROM_BAUD;
1818
this.dialogElements = {};
1919
this.currentFlow = null;
2020
this.currentStep = 0;
2121
this.currentDialogElement = null;
22-
this.port = null;
23-
this.espStub = null;
22+
this.device = null;
23+
this.transport = null;
24+
this.esploader = null;
25+
this.chip = null;
2426
this.dialogCssClass = "install-dialog";
2527
this.connected = this.connectionStates.DISCONNECTED;
2628
this.menuTitle = "Installer Menu";
@@ -343,13 +345,17 @@ export class InstallButton extends HTMLButtonElement {
343345
return macAddr.map((value) => value.toString(16).toUpperCase().padStart(2, "0")).join(":");
344346
}
345347

346-
async disconnect() {
347-
if (this.espStub) {
348-
await espStub.disconnect();
349-
await espStub.port.close();
350-
this.updateUIConnected(this.connectionStates.DISCONNECTED);
351-
this.espStub = null;
348+
async espDisconnect() {
349+
if (this.transport) {
350+
await this.transport.disconnect();
351+
await this.transport.waitForUnlock(1500);
352+
this.updateEspConnected(this.connectionStates.DISCONNECTED);
353+
this.transport = null;
354+
this.device = null;
355+
this.chip = null;
356+
return true;
352357
}
358+
return false;
353359
}
354360

355361
async runFlow(flow) {
@@ -390,6 +396,17 @@ export class InstallButton extends HTMLButtonElement {
390396
}
391397
}
392398

399+
async advanceSteps(stepCount) {
400+
if (!this.currentFlow) {
401+
return;
402+
}
403+
404+
if (this.currentStep <= this.currentFlow.steps.length + stepCount) {
405+
this.currentStep += stepCount;
406+
await this.currentFlow.steps[this.currentStep].bind(this)();
407+
}
408+
}
409+
393410
async showMenu() {
394411
// Display Menu
395412
this.showDialog(this.dialogs.menu);
@@ -405,38 +422,60 @@ export class InstallButton extends HTMLButtonElement {
405422
this.showDialog(this.dialogs.error, {message: message});
406423
}
407424

408-
async setBaudRateIfChipSupports(chipType, baud) {
409-
if (baud == ESP_ROM_BAUD) { return } // already the default
410-
411-
if (chipType == esptoolPackage.CHIP_FAMILY_ESP32) { // only supports the default
412-
this.logMsg(`ESP32 Chip only works at 115200 instead of the preferred ${baud}. Staying at 115200...`);
413-
return
414-
}
425+
async setBaudRateIfChipSupports(baud) {
426+
if (baud == this.baudRate) { return } // already the current setting
415427

416428
await this.changeBaudRate(baud);
417429
}
418430

419431
async changeBaudRate(baud) {
420-
if (this.espStub && this.baudRates.includes(baud)) {
421-
await this.espStub.setBaudrate(baud);
432+
if (this.baudRates.includes(baud)) {
433+
if (this.transport == null) {
434+
this.baudRate = baud;
435+
} else {
436+
this.errorMsg("Cannot change baud rate while connected.");
437+
}
422438
}
423439
}
424440

425-
async espHardReset(bootloader = false) {
426-
if (this.espStub) {
427-
await this.espStub.hardReset(bootloader);
441+
async espHardReset() {
442+
if (this.esploader) {
443+
await this.esploader.hardReset();
428444
}
429445
}
430446

431447
async espConnect(logger) {
432-
// - Request a port and open a connection.
433-
this.port = await navigator.serial.requestPort();
434-
435448
logger.log("Connecting...");
436-
await this.port.open({ baudRate: ESP_ROM_BAUD });
449+
450+
if (this.device === null) {
451+
this.device = await navigator.serial.requestPort({});
452+
this.transport = new Transport(this.device, true);
453+
}
454+
455+
const espLoaderTerminal = {
456+
clean() {
457+
// Clear the terminal
458+
},
459+
writeLine(data) {
460+
logger.log(data);
461+
},
462+
write(data) {
463+
logger.log(data);
464+
},
465+
};
466+
467+
const loaderOptions = {
468+
transport: this.transport,
469+
baudrate: this.baudRate,
470+
terminal: espLoaderTerminal,
471+
debugLogging: false,
472+
};
473+
474+
this.esploader = new ESPLoader(loaderOptions);
475+
this.chip = await this.esploader.main();
437476

438477
logger.log("Connected successfully.");
439478

440-
return new esptoolPackage.ESPLoader(this.port, logger);
479+
return this.esploader;
441480
};
442481
}

cpinstaller.js

Lines changed: 64 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { html } from 'https://cdn.jsdelivr.net/npm/lit-html/+esm';
77
import { map } from 'https://cdn.jsdelivr.net/npm/lit-html/directives/map/+esm';
88
import * as toml from "https://cdn.jsdelivr.net/npm/iarna-toml-esm@3.0.5/+esm"
99
import * as zip from "https://cdn.jsdelivr.net/npm/@zip.js/zip.js@2.6.65/+esm";
10-
import * as esptoolPackage from "https://cdn.jsdelivr.net/npm/esp-web-flasher@5.1.2/dist/web/index.js/+esm"
11-
import { REPL } from 'https://cdn.jsdelivr.net/gh/adafruit/circuitpython-repl-js/repl.js';
10+
import { default as CryptoJS } from "https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/+esm";
11+
import { REPL } from 'https://cdn.jsdelivr.net/gh/adafruit/circuitpython-repl-js@3.2.1/repl.js';
1212
import { InstallButton, ESP_ROM_BAUD } from "./base_installer.js";
1313

1414
// TODO: Combine multiple steps together. For now it was easier to make them separate,
@@ -19,19 +19,15 @@ import { InstallButton, ESP_ROM_BAUD } from "./base_installer.js";
1919
//
2020
// TODO: Hide the log and make it accessible via the menu (future feature, output to console for now)
2121
// May need to deal with the fact that the ESPTool uses Web Serial and CircuitPython REPL uses Web Serial
22+
//
23+
// TODO: Update File Operations to take advantage of the REPL FileOps class to allow non-CIRCUITPY drive access
2224

2325
const PREFERRED_BAUDRATE = 921600;
2426
const COPY_CHUNK_SIZE = 64 * 1024; // 64 KB Chunks
2527
const DEFAULT_RELEASE_LATEST = false; // Use the latest release or the stable release if not specified
2628
const BOARD_DEFS = "https://adafruit-circuit-python.s3.amazonaws.com/esp32_boards.json";
2729

2830
const CSS_DIALOG_CLASS = "cp-installer-dialog";
29-
const FAMILY_TO_CHIP_MAP = {
30-
'esp32s2': esptoolPackage.CHIP_FAMILY_ESP32S2,
31-
'esp32s3': esptoolPackage.CHIP_FAMILY_ESP32S3,
32-
'esp32c3': esptoolPackage.CHIP_FAMILY_ESP32C3,
33-
'esp32': esptoolPackage.CHIP_FAMILY_ESP32
34-
}
3531

3632
const attrMap = {
3733
"bootloader": "bootloaderUrl",
@@ -214,12 +210,12 @@ export class CPInstallButton extends InstallButton {
214210
isEnabled: async () => { return !this.hasNativeUsb() && !!this.binFileUrl },
215211
},
216212
uf2Only: { // Upgrade when Bootloader is already installer
217-
label: `Upgrade/Install CircuitPython [version] UF2 Only`,
213+
label: `Install CircuitPython [version] UF2 Only`,
218214
steps: [this.stepWelcome, this.stepSelectBootDrive, this.stepCopyUf2, this.stepSelectCpyDrive, this.stepCredentials, this.stepSuccess],
219215
isEnabled: async () => { return this.hasNativeUsb() && !!this.uf2FileUrl },
220216
},
221217
binOnly: {
222-
label: `Upgrade CircuitPython [version] Bin Only`,
218+
label: `Install CircuitPython [version] Bin Only`,
223219
steps: [this.stepWelcome, this.stepSerialConnect, this.stepConfirm, this.stepEraseAll, this.stepFlashBin, this.stepSuccess],
224220
isEnabled: async () => { return !!this.binFileUrl },
225221
},
@@ -311,6 +307,10 @@ export class CPInstallButton extends InstallButton {
311307
`,
312308
buttons: [
313309
this.previousButton,
310+
{
311+
label: "Skip Erase",
312+
onClick: async (e) => { if (confirm("Skipping the erase step may cause issues and is not recommended. Continue?")) { await this.advanceSteps(2); }},
313+
},
314314
{
315315
label: "Continue",
316316
onClick: this.nextStep,
@@ -481,7 +481,7 @@ export class CPInstallButton extends InstallButton {
481481
action: "Erasing Flash",
482482
});
483483
try {
484-
await this.espStub.eraseFlash();
484+
await this.esploader.eraseFlash();
485485
} catch (err) {
486486
this.errorMsg("Unable to finish erasing Flash memory. Please try again.");
487487
}
@@ -550,8 +550,8 @@ export class CPInstallButton extends InstallButton {
550550

551551
async stepSetupRepl() {
552552
// TODO: Try and reuse the existing connection so user doesn't need to select it again
553-
/*if (this.port) {
554-
this.replSerialDevice = this.port;
553+
/*if (this.device) {
554+
this.replSerialDevice = this.device;
555555
await this.setupRepl();
556556
}*/
557557
const serialPortName = await this.getSerialPortName();
@@ -579,6 +579,7 @@ export class CPInstallButton extends InstallButton {
579579
// TODO: Currently the control is just disabled and not used because we don't have anything to modify boot.py in place.
580580
// Setting mass_storage_disabled to true/false will display the checkbox with the appropriately checked state.
581581
//parameters.mass_storage_disabled = true;
582+
// This can be updated to use FileOps for ease of implementation
582583
}
583584

584585
// Display Credentials Request Dialog
@@ -658,45 +659,34 @@ export class CPInstallButton extends InstallButton {
658659
async espToolConnectHandler(e) {
659660
await this.onReplDisconnected(e);
660661
await this.espDisconnect();
661-
let esploader;
662+
await this.setBaudRateIfChipSupports(PREFERRED_BAUDRATE);
662663
try {
663-
esploader = await this.espConnect({
664+
this.updateEspConnected(this.connectionStates.CONNECTING);
665+
await this.espConnect({
664666
log: (...args) => this.logMsg(...args),
665667
debug: (...args) => {},
666668
error: (...args) => this.errorMsg(...args),
667669
});
668-
} catch (err) {
669-
// It's possible the dialog was also canceled here
670-
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.");
671-
return;
672-
}
673-
674-
try {
675-
this.updateEspConnected(this.connectionStates.CONNECTING);
676-
await esploader.initialize();
677670
this.updateEspConnected(this.connectionStates.CONNECTED);
678671
} catch (err) {
679-
await esploader.disconnect();
680-
// Disconnection before complete
672+
// It's possible the dialog was also canceled here
681673
this.updateEspConnected(this.connectionStates.DISCONNECTED);
682-
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.")
674+
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.");
683675
return;
684676
}
685677

686678
try {
687-
this.logMsg(`Connected to ${esploader.chipName}`);
688-
this.logMsg(`MAC Address: ${this.formatMacAddr(esploader.macAddr())}`);
679+
this.logMsg(`Connected to ${this.esploader.chip.CHIP_NAME}`);
689680

690681
// check chip compatibility
691-
if (FAMILY_TO_CHIP_MAP[this.chipFamily] == esploader.chipFamily) {
682+
if (this.chipFamily == `${this.esploader.chip.CHIP_NAME}`.toLowerCase().replaceAll("-", "")) {
692683
this.logMsg("This chip checks out");
693-
this.espStub = await esploader.runStub();
694-
this.espStub.addEventListener("disconnect", () => {
695-
this.updateEspConnected(this.connectionStates.DISCONNECTED);
696-
this.espStub = null;
697-
});
698684

699-
await this.setBaudRateIfChipSupports(esploader.chipFamily, PREFERRED_BAUDRATE);
685+
// esploader-js doesn't have a disconnect event, so we can't use this
686+
//this.esploader.addEventListener("disconnect", () => {
687+
// this.updateEspConnected(this.connectionStates.DISCONNECTED);
688+
//});
689+
700690
await this.nextStep();
701691
return;
702692
}
@@ -706,7 +696,9 @@ export class CPInstallButton extends InstallButton {
706696
await this.espDisconnect();
707697

708698
} catch (err) {
709-
await esploader.disconnect();
699+
if (this.transport) {
700+
await this.transport.disconnect();
701+
}
710702
// Disconnection before complete
711703
this.updateEspConnected(this.connectionStates.DISCONNECTED);
712704
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.")
@@ -1024,10 +1016,28 @@ export class CPInstallButton extends InstallButton {
10241016

10251017
async downloadAndInstall(url, fileToExtract = null, cacheFile = false) {
10261018
let [filename, fileBlob] = await this.downloadAndExtract(url, fileToExtract, cacheFile);
1019+
const fileArray = [];
1020+
1021+
const readBlobAsBinaryString = (inputFile) => {
1022+
const reader = new FileReader();
1023+
1024+
return new Promise((resolve, reject) => {
1025+
reader.onerror = () => {
1026+
reader.abort();
1027+
reject(new DOMException("Problem parsing input file."));
1028+
};
1029+
1030+
reader.onload = () => {
1031+
resolve(reader.result);
1032+
};
1033+
reader.readAsBinaryString(inputFile);
1034+
});
1035+
};
10271036

10281037
// Update the Progress dialog
10291038
if (fileBlob) {
1030-
const fileContents = (new Uint8Array(await fileBlob.arrayBuffer())).buffer;
1039+
fileArray.push({ data: await readBlobAsBinaryString(fileBlob), address: 0 });
1040+
10311041
let lastPercent = 0;
10321042
this.showDialog(this.dialogs.actionProgress, {
10331043
action: `Flashing ${filename}`
@@ -1037,14 +1047,22 @@ export class CPInstallButton extends InstallButton {
10371047
progressElement.value = 0;
10381048

10391049
try {
1040-
await this.espStub.flashData(fileContents, (bytesWritten, totalBytes) => {
1041-
let percentage = Math.round((bytesWritten / totalBytes) * 100);
1042-
if (percentage > lastPercent) {
1043-
progressElement.value = percentage;
1044-
this.logMsg(`${percentage}% (${bytesWritten}/${totalBytes})...`);
1045-
lastPercent = percentage;
1046-
}
1047-
}, 0, 0);
1050+
const flashOptions = {
1051+
fileArray: fileArray,
1052+
flashSize: "keep",
1053+
eraseAll: false,
1054+
compress: true,
1055+
reportProgress: (fileIndex, written, total) => {
1056+
let percentage = Math.round((written / total) * 100);
1057+
if (percentage > lastPercent) {
1058+
progressElement.value = percentage;
1059+
this.logMsg(`${percentage}% (${written}/${total})...`);
1060+
lastPercent = percentage;
1061+
}
1062+
},
1063+
calculateMD5Hash: (image) => CryptoJS.MD5(CryptoJS.enc.Latin1.parse(image)),
1064+
};
1065+
await this.esploader.writeFlash(flashOptions);
10481066
} catch (err) {
10491067
this.errorMsg(`Unable to flash file: ${filename}. Error Message: ${err}`);
10501068
}
@@ -1256,20 +1274,6 @@ export class CPInstallButton extends InstallButton {
12561274
return {};
12571275
}
12581276

1259-
async espDisconnect() {
1260-
// Disconnect the ESPTool
1261-
if (this.espStub) {
1262-
await this.espStub.disconnect();
1263-
this.espStub.removeEventListener("disconnect", this.espDisconnect.bind(this));
1264-
this.updateEspConnected(this.connectionStates.DISCONNECTED);
1265-
this.espStub = null;
1266-
}
1267-
if (this.port) {
1268-
await this.port.close();
1269-
this.port = null;
1270-
}
1271-
}
1272-
12731277
async serialTransmit(msg) {
12741278
const encoder = new TextEncoder();
12751279
if (this.writer) {

0 commit comments

Comments
 (0)