Merge pull request #14 from dhalbert/installer-clarifications

Revamp of installer messages
This commit is contained in:
Melissa LeBlanc-Williams 2025-07-30 11:13:49 -07:00 committed by GitHub
commit a7594d7301
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -260,39 +260,61 @@ export class CPInstallButton extends InstallButton {
welcome: { welcome: {
closeable: true, closeable: true,
template: (data) => html` template: (data) => html`
<h3>Web Firmware Installer</h3>
<p> <p>
Welcome to the CircuitPython Installer. This tool will install CircuitPython on your ${data.boardName}. Welcome!
This tool will install a UF2 bootloader and/or CircuitPython on your ${data.boardName}.
</p> </p>
<p> <p>
This tool is <strong>new</strong> and <strong>experimental</strong>. If you experience any issues, feel free to check out This tool is <strong>experimental</strong>.
If you experience any issues, feel free to check out
<a href="https://github.com/adafruit/circuitpython-org/issues">https://github.com/adafruit/circuitpython-org/issues</a> <a href="https://github.com/adafruit/circuitpython-org/issues">https://github.com/adafruit/circuitpython-org/issues</a>
to see if somebody has already submitted the same issue you are experiencing. If not, feel free to open a new issue. If to see if the issue you are experiencing has already been reported.
you do see the same issue and are able to contribute additional information, that would be appreciated. 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.
</p> </p>
<p> <p>
If you are unable to use this tool, then the manual installation methods should still work. If you are unable to use this tool,
then the manual installation methods like the
<a href="https://adafruit.github.io/Adafruit_WebSerial_ESPTool/">Adafruit WebSerial Tool</a>
and esptool.py should still work.
</p> </p>
` `
}, },
espSerialConnect: { espSerialConnect: {
closeable: true, closeable: true,
template: (data) => html` template: (data) => html`
<p> <h3>
Make sure your board is plugged into this computer via a Serial connection using a USB Cable. Connect to Your Board
</p> </h3>
<ul> <ol>
<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> <li>
<p>
Plug your board into this computer.
<em>Make sure the USB cable is good for data sync, and is not a charge-only cable.</em>
</p>
</li>
<li>
<p>
<strong>Put your board into ROM bootloader mode</strong>,
by holding down the BOOT button (sometimes marked "B0"),
and clicking the RESET button (sometimes marked "RST").
If your board doesn't have a BOOT button, just press RESET.
</p>
</li>
<li>
<p>
<button id="butConnect" type="button" @click=${this.espToolConnectHandler.bind(this)}>Connect</button>
Click this button to open the Web Serial connection menu and choose the serial port for this board.
</p>
<p>
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.
</p>
</li>
</ul> </ul>
<p>
<button id="butConnect" type="button" @click=${this.espToolConnectHandler.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", "JTAG", and "Bridge Controller". There may be more than one right option depending on your system configuration. Experiment if needed.
</p>
`, `,
buttons: [this.previousButton, { buttons: [this.previousButton, {
label: "Next", label: "Next",
@ -303,7 +325,8 @@ export class CPInstallButton extends InstallButton {
}, },
confirm: { confirm: {
template: (data) => html` template: (data) => html`
<p>This will overwrite everything on the ${data.boardName}.</p> <h3>Erase Flash</h3>
<p>Now, optionally, erase everything on the ${data.boardName}.</p>
`, `,
buttons: [ buttons: [
this.previousButton, this.previousButton,
@ -320,41 +343,53 @@ export class CPInstallButton extends InstallButton {
bootDriveSelect: { bootDriveSelect: {
closeable: true, closeable: true,
template: (data) => html` template: (data) => html`
<p> <h3>Select the ${data.drivename} Drive</h3>
Please select the ${data.drivename} Drive where the UF2 file will be copied. <ol>
</p> <li>
<p> <p>
If you just installed the bootloader, you may need to reset your board. If you already had the bootloader installed, <strong>Reset your board</strong> if you just installed the UF2 bootloader,
you may need to double press the reset button. by pressing the RESET button.
</p> If you already had the UF2 bootloader installed,
<p> you may need to double-click the RESET button to start up the UF2 bootloader.
<button id="butSelectBootDrive" type="button" @click=${this.bootDriveSelectHandler.bind(this)}>Select ${data.drivename} Drive</button> </p>
</p> </li>
<li>
<p>
<button id="butSelectBootDrive" type="button" @click=${this.bootDriveSelectHandler.bind(this)}>Select ${data.drivename} Drive</button>
Select the ${data.drivename} drive where the UF2 file will be copied.
</p>
</li>
</ul>
`, `,
buttons: [], buttons: [],
}, },
circuitpyDriveSelect: { circuitpyDriveSelect: {
closeable: true, closeable: true,
template: (data) => html` template: (data) => html`
<p> <h3>Select the CIRCUITPY Drive</h3>
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. <ul>
</p> <li>
<p> <p>
<button id="butSelectCpyDrive" type="button" @click=${this.circuitpyDriveSelectHandler.bind(this)}>Select CIRCUITPY Drive</button> <button id="butSelectCpyDrive" type="button" @click=${this.circuitpyDriveSelectHandler.bind(this)}>Select CIRCUITPY Drive</button>
</p> Select the CIRCUITPY Drive.
You may need to wait a few seconds for it to appear.
If you don't see your CIRCUITPY drive, it may be disabled in boot.py or you may have previously renamed it.
</p>
</li>
</ul>
`, `,
buttons: [], buttons: [],
}, },
actionWaiting: { actionWaiting: {
template: (data) => html` template: (data) => html`
<p class="centered">${data.action}...</p> <p class="centered">${data.action}</p>
<div class="loader"><div></div><div></div><div></div><div></div></div> <div class="loader"><div></div><div></div><div></div><div></div></div>
`, `,
buttons: [], buttons: [],
}, },
actionProgress: { actionProgress: {
template: (data) => html` template: (data) => html`
<p>${data.action}...</p> <p>${data.action}</p>
<progress id="stepProgress" max="100" value="${data.percentage}"> ${data.percentage}% </progress> <progress id="stepProgress" max="100" value="${data.percentage}"> ${data.percentage}% </progress>
`, `,
buttons: [], buttons: [],
@ -362,14 +397,15 @@ export class CPInstallButton extends InstallButton {
cpSerial: { cpSerial: {
closeable: true, closeable: true,
template: (data) => html` template: (data) => html`
<p> <h3>Reconnect to serial</h3>
The next step is to write your credentials to settings.toml. Make sure your board is running CircuitPython. <strong>If you just installed CircuitPython, you may to reset the board first.</strong> <ul>
<li>
<button id="butConnect" type="button" @click=${this.cpSerialConnectHandler.bind(this)}>Connect</button>
Click this button to open the Web Serial connection menu.
If it is already connected, you can press it again if you need to select a different port.
</li>
</ul>
</p> </p>
<p>
<button id="butConnect" type="button" @click=${this.cpSerialConnectHandler.bind(this)}>Connect</button>
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.
</p>
<p>${data.serialPortInstructions}</p> <p>${data.serialPortInstructions}</p>
`, `,
buttons: [this.previousButton, { buttons: [this.previousButton, {
@ -383,6 +419,15 @@ export class CPInstallButton extends InstallButton {
credentials: { credentials: {
closeable: true, closeable: true,
template: (data) => html` template: (data) => html`
<h3>Fill in settings.toml</h3>
<p>
This step will write your network credentials to the settings.toml file on CIRCUITPY.
Make sure your board is running CircuitPython.
</p>
<p>
If you want to skip this step and fill in settings.toml later,
just close this dialog.
</p>
<fieldset> <fieldset>
<div class="field"> <div class="field">
<label for="circuitpy_wifi_ssid">WiFi Network Name (SSID):</label> <label for="circuitpy_wifi_ssid">WiFi Network Name (SSID):</label>
@ -401,9 +446,9 @@ export class CPInstallButton extends InstallButton {
<input id="circuitpy_web_api_port" class="setting-data" type="number" min="0" max="65535" placeholder="Web Workflow API Port" value="${data.api_port}" /> <input id="circuitpy_web_api_port" class="setting-data" type="number" min="0" max="65535" placeholder="Web Workflow API Port" value="${data.api_port}" />
</div> </div>
${data.mass_storage_disabled === true || data.mass_storage_disabled === false ? ${data.mass_storage_disabled === true || data.mass_storage_disabled === false ?
html`<div class="field"> html`<div class="field">
<label for="circuitpy_drive"><input id="circuitpy_drive" class="setting" type="checkbox" value="disabled" ${data.mass_storage_disabled ? "checked" : ""} />Disable CIRCUITPY Drive (Required for write access)</label> <label for="circuitpy_drive"><input id="circuitpy_drive" class="setting" type="checkbox" value="disabled" ${data.mass_storage_disabled ? "checked" : ""} />Disable CIRCUITPY Drive (Required for write access)</label>
</div>` : ''} </div>` : ''}
</fieldset> </fieldset>
`, `,
buttons: [this.previousButton, { buttons: [this.previousButton, {
@ -478,7 +523,7 @@ export class CPInstallButton extends InstallButton {
async stepEraseAll() { async stepEraseAll() {
// Display Erase Dialog // Display Erase Dialog
this.showDialog(this.dialogs.actionWaiting, { this.showDialog(this.dialogs.actionWaiting, {
action: "Erasing Flash", action: "Erasing Flash ...",
}); });
try { try {
await this.esploader.eraseFlash(); await this.esploader.eraseFlash();
@ -538,7 +583,7 @@ export class CPInstallButton extends InstallButton {
} }
// Display Progress Dialog // Display Progress Dialog
this.showDialog(this.dialogs.actionProgress, { this.showDialog(this.dialogs.actionProgress, {
action: `Copying ${this.uf2FileUrl}`, action: `Copying ${this.uf2FileUrl} ...`,
}); });
// Do a copy and update progress along the way // Do a copy and update progress along the way
@ -671,7 +716,7 @@ export class CPInstallButton extends InstallButton {
} catch (err) { } catch (err) {
// It's possible the dialog was also canceled here // It's possible the dialog was also canceled here
this.updateEspConnected(this.connectionStates.DISCONNECTED); this.updateEspConnected(this.connectionStates.DISCONNECTED);
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."); 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. If installing the bootloader, make sure you are in ROM bootloader mode.");
return; return;
} }
@ -848,7 +893,8 @@ export class CPInstallButton extends InstallButton {
} }
// Download the bootloader zip file // Download the bootloader zip file
let [filename, fileBlob] = await this.downloadAndExtract(this.bootloaderUrl, 'tinyuf2.bin'); let [filename, extracted_filename, fileBlob] =
await this.downloadAndExtract(this.bootloaderUrl, 'tinyuf2.bin');
const fileContents = await fileBlob.text(); const fileContents = await fileBlob.text();
const bootDriveRegex = /B\x00B\x00([A-Z0-9\x00]{11})FAT16/; const bootDriveRegex = /B\x00B\x00([A-Z0-9\x00]{11})FAT16/;
@ -981,7 +1027,7 @@ export class CPInstallButton extends InstallButton {
if (!fileBlob) { if (!fileBlob) {
this.showDialog(this.dialogs.actionProgress, { this.showDialog(this.dialogs.actionProgress, {
action: `Downloading ${filename}` action: `Downloading ${filename} ...`
}); });
const progressElement = this.currentDialogElement.querySelector("#stepProgress"); const progressElement = this.currentDialogElement.querySelector("#stepProgress");
@ -995,11 +1041,12 @@ export class CPInstallButton extends InstallButton {
} }
// If the file is a zip file, unzip and find the file to extract // If the file is a zip file, unzip and find the file to extract
let extracted_filename = null;
if (filename.endsWith(".zip") && fileToExtract) { if (filename.endsWith(".zip") && fileToExtract) {
let foundFile; let foundFile;
// Update the Progress dialog // Update the Progress dialog
this.showDialog(this.dialogs.actionProgress, { this.showDialog(this.dialogs.actionProgress, {
action: `Extracting ${fileToExtract}` action: html`<p>Downloaded ${filename}</p><p>Extracting ${fileToExtract} ...</p>`
}); });
// Set that to the current file to flash // Set that to the current file to flash
@ -1008,14 +1055,14 @@ export class CPInstallButton extends InstallButton {
this.errorMsg(`Unable to find ${fileToExtract} in ${filename}`); this.errorMsg(`Unable to find ${fileToExtract} in ${filename}`);
return; return;
} }
filename = foundFile; extracted_filename = foundFile;
} }
return [filename, fileBlob]; return [filename, extracted_filename, fileBlob];
} }
async downloadAndInstall(url, fileToExtract = null, cacheFile = false) { async downloadAndInstall(url, fileToExtract = null, cacheFile = false) {
let [filename, fileBlob] = await this.downloadAndExtract(url, fileToExtract, cacheFile); let [filename, extracted_filename, fileBlob] = await this.downloadAndExtract(url, fileToExtract, cacheFile);
const fileArray = []; const fileArray = [];
const readBlobAsBinaryString = (inputFile) => { const readBlobAsBinaryString = (inputFile) => {
@ -1024,7 +1071,7 @@ export class CPInstallButton extends InstallButton {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
reader.onerror = () => { reader.onerror = () => {
reader.abort(); reader.abort();
reject(new DOMException("Problem parsing input file.")); reject(new DOMException("Problem parsing input file"));
}; };
reader.onload = () => { reader.onload = () => {
@ -1040,7 +1087,9 @@ export class CPInstallButton extends InstallButton {
let lastPercent = 0; let lastPercent = 0;
this.showDialog(this.dialogs.actionProgress, { this.showDialog(this.dialogs.actionProgress, {
action: `Flashing ${filename}` action: fileToExtract
? html`<p>Downloaded ${filename}</p><p>Extracted ${fileToExtract}</p><p>Flashing (be patient; you will see pauses) ...</p>`
: html`<p>Downloaded ${filename}</p>Flashing (be patient; you will see pauses) ...</p>`
}); });
const progressElement = this.currentDialogElement.querySelector("#stepProgress"); const progressElement = this.currentDialogElement.querySelector("#stepProgress");
@ -1056,7 +1105,7 @@ export class CPInstallButton extends InstallButton {
let percentage = Math.round((written / total) * 100); let percentage = Math.round((written / total) * 100);
if (percentage > lastPercent) { if (percentage > lastPercent) {
progressElement.value = percentage; progressElement.value = percentage;
this.logMsg(`${percentage}% (${written}/${total})...`); this.logMsg(`${percentage}% (${written}/${total}) ...`);
lastPercent = percentage; lastPercent = percentage;
} }
}, },
@ -1064,7 +1113,7 @@ export class CPInstallButton extends InstallButton {
}; };
await this.esploader.writeFlash(flashOptions); await this.esploader.writeFlash(flashOptions);
} catch (err) { } catch (err) {
this.errorMsg(`Unable to flash file: ${filename}. Error Message: ${err}`); this.errorMsg(`Unable to flash file: ${fileToExtract}. Error Message: ${err}`);
} }
} }
} }
@ -1078,10 +1127,14 @@ export class CPInstallButton extends InstallButton {
return; return;
} }
let [filename, extracted_filename, fileBlob] = await this.downloadAndExtract(url);
this.showDialog(this.dialogs.actionProgress, {
action: html`<p>Downloaded: ${filename}</p><p>Flashing ...</p>`
});
const progressElement = this.currentDialogElement.querySelector("#stepProgress"); const progressElement = this.currentDialogElement.querySelector("#stepProgress");
progressElement.value = 0; progressElement.value = 0;
let [filename, fileBlob] = await this.downloadAndExtract(url);
const fileHandle = await dirHandle.getFileHandle(filename, {create: true}); const fileHandle = await dirHandle.getFileHandle(filename, {create: true});
const writableStream = await fileHandle.createWritable(); const writableStream = await fileHandle.createWritable();
const totalSize = fileBlob.size; const totalSize = fileBlob.size;
@ -1093,7 +1146,7 @@ export class CPInstallButton extends InstallButton {
bytesWritten += chunk.size; bytesWritten += chunk.size;
progressElement.value = Math.round(bytesWritten / totalSize * 100); progressElement.value = Math.round(bytesWritten / totalSize * 100);
this.logMsg(`${Math.round(bytesWritten / totalSize * 100)}% (${bytesWritten} / ${totalSize}) written...`); this.logMsg(`${Math.round(bytesWritten / totalSize * 100)}% (${bytesWritten} / ${totalSize}) written ...`);
} }
this.logMsg("File successfully written"); this.logMsg("File successfully written");
try { try {
@ -1101,7 +1154,7 @@ export class CPInstallButton extends InstallButton {
await writableStream.close(); await writableStream.close();
this.logMsg("File successfully closed"); this.logMsg("File successfully closed");
} catch (err) { } catch (err) {
this.logMsg("Error closing file, probably due to board reset. Continuing..."); this.logMsg("Error closing file, probably due to board reset. Continuing ...");
} }
} }
@ -1163,7 +1216,7 @@ export class CPInstallButton extends InstallButton {
if (fileContents) { if (fileContents) {
return toml.parse(fileContents); return toml.parse(fileContents);
} }
this.logMsg("Unable to read settings.toml from CircuitPython. It may not exist. Continuing..."); this.logMsg("Unable to read settings.toml from CircuitPython. It may not exist. Continuing ...");
return {}; return {};
} }
@ -1230,7 +1283,7 @@ export class CPInstallButton extends InstallButton {
// Perform a soft restart to avoid losing the connection and get an IP address // Perform a soft restart to avoid losing the connection and get an IP address
this.showDialog(this.dialogs.actionWaiting, { this.showDialog(this.dialogs.actionWaiting, {
action: "Waiting for IP Address...", action: "Waiting for IP Address ...",
}); });
await this.repl.softRestart(); await this.repl.softRestart();
try { try {
@ -1270,7 +1323,7 @@ export class CPInstallButton extends InstallButton {
if (fileContents) { if (fileContents) {
return toml.parse(fileContents); return toml.parse(fileContents);
} }
this.logMsg("Unable to read settings.toml from CircuitPython. It may not exist. Continuing..."); this.logMsg("Unable to read settings.toml from CircuitPython. It may not exist. Continuing ...");
return {}; return {};
} }
@ -1353,4 +1406,4 @@ export class CPInstallButton extends InstallButton {
} }
} }
customElements.define('cp-install-button', CPInstallButton, {extends: "button"}); customElements.define('cp-install-button', CPInstallButton, {extends: "button"});