Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions firmware/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,36 @@ <h2>Files</h2>
</div>
</div>

<div class="section-head" style="margin-top: 36px;">
<h2 id="fw-file-title">Install from a file</h2>
<span class="hint">A .bin or .uf2 you built yourself</span>
</div>
<div id="fw-file" class="spec-block">
<p class="prose">Built your own firmware? Pick the file and this page writes it to your board, the same way as the builds above. The file stays on your computer.</p>
<label for="fw-file-input" class="fw-label">Firmware file</label>
<input type="file" id="fw-file-input" class="fw-file-input" accept=".bin,.uf2">
<div id="fw-file-chip-row" class="fw-file-row" hidden>
<label for="fw-file-chip" class="fw-label">Chip</label>
<select id="fw-file-chip" class="fw-select"></select>
</div>
<p id="fw-file-note" class="fw-muted" hidden></p>

<div id="fw-file-out" hidden>
<p class="fw-muted"><strong>Copy your files off the board first, <code>secrets.py</code> above all.</strong>
A build with a different layout leaves the filesystem empty.</p>
<div id="fw-file-install" hidden>
<h3>Install from the browser</h3>
<p class="prose" id="fw-file-install-text"></p>
<div id="fw-file-install-button"></div>
<p class="fw-muted" id="fw-file-install-hint"></p>
</div>
<h3 id="fw-file-steps-title">Steps</h3>
<ol id="fw-file-steps" class="fw-steps"></ol>
<h3>Or with mpftp</h3>
<div id="fw-file-mpftp" class="fw-cli prose"></div>
</div>
</div>

<div class="section-head" style="margin-top: 36px;">
<h2>Desktop interpreters</h2>
<span class="hint">Linux, Windows and the browser, with every PyDevices module</span>
Expand Down
9 changes: 7 additions & 2 deletions firmware/page.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
.fw-steps { margin: 0; padding-left: 22px; color: var(--ink-soft); }
.fw-steps li { margin: 0 0 14px; }
.fw-steps li:last-child { margin-bottom: 0; }
.fw-steps pre { margin: 8px 0 0; padding: 12px 14px; overflow-x: auto; border: 1px solid var(--border-soft);
.fw-steps pre, .fw-cli pre { margin: 8px 0 0; padding: 12px 14px; overflow-x: auto; border: 1px solid var(--border-soft);
border-radius: var(--radius-sm); background: var(--bg-soft); }
.fw-steps pre code, .fw-steps code { font-family: var(--mono); font-size: 0.88rem; }
.fw-steps pre code, .fw-steps code, .fw-cli pre code { font-family: var(--mono); font-size: 0.88rem; }
.fw-scroll { overflow-x: auto; }
.fw-table { width: 100%; border-collapse: collapse; font-size: 0.92rem; }
.fw-table th, .fw-table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border-soft); vertical-align: top; }
Expand All @@ -35,3 +35,8 @@
.fw-pins summary { cursor: pointer; font-weight: 600; }
.fw-pins table { margin-top: 12px; }
#fw-install-button .btn { font: inherit; }
.fw h3 { margin: 22px 0 10px; font-size: 1rem; }
.fw-file-input { display: block; max-width: 100%; font: inherit; color: var(--ink); }
.fw-file-row { margin-top: 16px; }
#fw-file-install-button .btn { font: inherit; }
.fw-cli p { margin: 0; }
200 changes: 188 additions & 12 deletions firmware/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
// Chips the browser flasher (ESP Web Tools) can write. Set a chip to false to send its
// boards to the esptool steps alone.
var BROWSER_INSTALL = { esp32s3: true, esp32p4: true };
// ESP Web Tools' names for the chips, as the release manifests use them.
var CHIP_FAMILY = { esp32s3: 'ESP32-S3', esp32p4: 'ESP32-P4', esp32: 'ESP32', esp32c3: 'ESP32-C3',
esp32c6: 'ESP32-C6', esp32s2: 'ESP32-S2' };
// Where a merged image, which starts with the bootloader, is written on each chip. A file
// that does not start with an image header is a whole-flash image and goes at 0x0.
var BOOT_OFFSET = { esp32s3: '0x0', esp32p4: '0x2000', esp32: '0x1000', esp32c3: '0x0',
esp32c6: '0x0', esp32s2: '0x1000' };
// The chip id an image header carries at byte 12 (esp_chip_id_t).
var CHIP_ID = { 0: 'esp32', 2: 'esp32s2', 5: 'esp32c3', 9: 'esp32s3', 13: 'esp32c6', 18: 'esp32p4' };
var ROLES = {
flash: 'Flash image: the file to write',
uf2: 'UF2: the file to copy',
Expand Down Expand Up @@ -124,21 +133,32 @@
'Chrome or Edge on a computer. Plug the board in, press Install, pick its serial port and confirm. ' +
'It asks whether to erase the board first: tick that when you come from another firmware. ' +
'Writing ' + mb(f.size) + ' takes a few minutes; leave the board plugged in until it says it is done.'));
var btn = document.createElement('esp-web-install-button');
btn.setAttribute('manifest', 'manifests/' + b.id + '.json');
btn.appendChild(h('button', { slot: 'activate', class: 'btn primary', type: 'button', text: 'Install ' + kind(b) }));
btn.appendChild(h('span', { slot: 'unsupported', class: 'fw-muted',
text: 'This browser can not write to a board. Use Chrome or Edge on a computer, or the esptool steps below.' }));
btn.appendChild(h('span', { slot: 'not-allowed', class: 'fw-muted',
text: 'Browsers only allow this on https pages or on localhost. Use the esptool steps below.' }));
holder.appendChild(btn);
holder.appendChild(installButton('manifests/' + b.id + '.json', 'Install ' + kind(b)));
var hint = 'If the board does not appear in the list, hold BOOT while pressing RESET, then try again. ';
if (b.chip === 'esp32p4') {
hint += 'The browser flasher knows the ESP32-P4 but is newer on it than on other chips. If it stops partway, the esptool steps below write the same file.';
}
$('fw-install-hint').textContent = hint;
}

function esptoolStep() {
return h('li', {}, 'Install esptool in a virtual environment and activate it, so the ', code('esptool'),
' you run is the one you just installed. In each new terminal, activate it again before you run esptool.',
pre('# Windows\npython -m venv v\nv\\Scripts\\activate\npip install esptool\n\n' +
'# Linux and macOS\npython3 -m venv v\nsource v/bin/activate\npip install esptool'));
}

function installButton(manifest, label) {
var btn = document.createElement('esp-web-install-button');
btn.setAttribute('manifest', manifest);
btn.appendChild(h('button', { slot: 'activate', class: 'btn primary', type: 'button', text: label }));
btn.appendChild(h('span', { slot: 'unsupported', class: 'fw-muted',
text: 'This browser can not write to a board. Use Chrome or Edge on a computer, or the esptool steps below.' }));
btn.appendChild(h('span', { slot: 'not-allowed', class: 'fw-muted',
text: 'Browsers only allow this on https pages or on localhost. Use the esptool steps below.' }));
return btn;
}

function renderSteps(b) {
var ol = clear($('fw-steps'));
var f = mainFile(b);
Expand All @@ -147,10 +167,7 @@
$('fw-steps-title').textContent = canBrowser(b) ? 'Or with esptool, in any browser' : 'Steps';

if (isEsp(b)) {
steps.push(h('li', {}, 'Install esptool in a virtual environment and activate it, so the ', code('esptool'),
' you run is the one you just installed. In each new terminal, activate it again before you run esptool.',
pre('# Windows\npython -m venv v\nv\\Scripts\\activate\npip install esptool\n\n' +
'# Linux and macOS\npython3 -m venv v\nsource v/bin/activate\npip install esptool')));
steps.push(esptoolStep());
steps.push(h('li', {}, link(local(f), 'Download ' + name), ' (' + mb(f.size) + '), then write it. ' +
'The port is COM3 or similar on Windows, /dev/ttyACM0 or /dev/ttyUSB0 on Linux, /dev/cu.usbmodem... on macOS. ' +
'On a board with two USB ports, use the one wired to the serial chip, usually marked UART. ',
Expand Down Expand Up @@ -211,6 +228,163 @@
$('fw-verify').textContent = 'gh attestation verify ' + mainFile(b).name + ' --repo ' + REPO;
}

// ---- install from a file -------------------------------------------------
// The chosen file never leaves the computer: it becomes a Blob URL, and so does an
// ESP Web Tools manifest whose one part names it. ESP Web Tools fetches both the way it
// fetches a manifest and part from this site.

var picked = null; // { file, uf2, head }
var pickSeq = 0;
var blobs = [];

function readHead(file) {
return file.slice(0, 36).arrayBuffer().then(function (buf) {
var b = new Uint8Array(buf);
var image = b.length >= 14 && b[0] === 0xE9;
var id = image ? b[12] | (b[13] << 8) : null;
return {
image: image,
chip: image && CHIP_ID.hasOwnProperty(id) ? CHIP_ID[id] : '',
// An application image (micropython.bin) has its descriptor right after the headers.
app: image && b.length >= 36 && new DataView(buf).getUint32(32, true) === 0xABCD5432,
padded: b.length > 0 && b[0] === 0xFF
};
});
}

function fileOffset(chip, head) { return head.image ? BOOT_OFFSET[chip] : '0x0'; }
function shellName(n) { return /^[\w.+-]+$/.test(n) ? n : '"' + n.replace(/(["\\$`])/g, '\\$1') + '"'; }

function fileManifest(file, chip, offset) {
var part = URL.createObjectURL(file);
var m = {
name: file.name,
version: 'from a file',
new_install_prompt_erase: true,
new_install_improv_wait_time: 0,
builds: [{ chipFamily: CHIP_FAMILY[chip], parts: [{ path: part, offset: parseInt(offset, 16) }] }]
};
var url = URL.createObjectURL(new Blob([JSON.stringify(m)], { type: 'application/json' }));
blobs.push(part, url);
return url;
}

function fileNote(text) {
var n = $('fw-file-note');
n.textContent = text || '';
n.hidden = !text;
}

function fileCli(text, line) {
var box = clear($('fw-file-mpftp'));
box.appendChild(h('p', {}, text));
box.appendChild(pre(line));
}

function renderFile() {
blobs.forEach(function (u) { URL.revokeObjectURL(u); });
blobs = [];
clear($('fw-file-install-button'));
$('fw-file-out').hidden = true;
$('fw-file-chip-row').hidden = true;
fileNote('');
if (!picked) return;
var file = picked.file;
var name = file.name;
if (picked.uf2) {
renderFileUf2(name);
$('fw-file-out').hidden = false;
return;
}
var head = picked.head;
var chip = $('fw-file-chip').value;
if (head.app) {
fileNote(name + ' is an application image, not the merged firmware.bin. Written where the bootloader goes, ' +
'it leaves the board restarting over and over. Pick the firmware.bin from the same build.');
return;
}
if (head.image && !head.chip) {
fileNote(name + ' is for a chip this page does not list. mpftp and esptool can write it: both read the chip from the file.');
return;
}
if (!head.image && !head.padded) {
fileNote(name + ' does not look like ESP32 firmware. Pick the firmware.bin from your build.');
return;
}
$('fw-file-chip-row').hidden = false;
if (!chip) { fileNote('Choose the chip on your board.'); return; }
if (head.chip && head.chip !== chip) {
fileNote(name + ' was built for the ' + CHIP_FAMILY[head.chip] + ', not the ' + CHIP_FAMILY[chip] +
'. Choose the chip it was built for.');
return;
}
var offset = fileOffset(chip, head);
var arg = shellName(name);
fileNote((head.chip ? 'The file says it is for the ' + CHIP_FAMILY[chip] + '. ' : '') +
'It is written at ' + offset + '.');
$('fw-file-out').hidden = false;

var browser = BROWSER_INSTALL[chip] === true;
$('fw-file-install').hidden = !browser;
if (browser) {
$('fw-file-install-text').textContent = 'Chrome or Edge on a computer. Plug the board in, press Install, ' +
'pick its serial port and confirm. It asks whether to erase the board first: tick that when you come from another firmware. ' +
'Writing ' + mb(file.size) + ' takes a few minutes; leave the board plugged in until it says it is done.';
$('fw-file-install-button').appendChild(installButton(fileManifest(file, chip, offset), 'Install this file'));
$('fw-file-install-hint').textContent = 'If the board does not appear in the list, hold BOOT while pressing RESET, then try again.' +
(chip === 'esp32p4' ? ' If it stops partway on the ESP32-P4, the esptool steps below write the same file.' : '');
}

$('fw-file-steps-title').textContent = browser ? 'Or with esptool, in any browser' : 'Steps';
var ol = clear($('fw-file-steps'));
ol.appendChild(esptoolStep());
ol.appendChild(h('li', {}, 'Write it, from the folder that holds ' + name + '. ' +
'The port is COM3 or similar on Windows, /dev/ttyACM0 or /dev/ttyUSB0 on Linux, /dev/cu.usbmodem... on macOS. ' +
'If esptool can not reach the board, put it in download mode: hold BOOT, press and release RESET, then release BOOT.',
pre('esptool --chip ' + chip + ' --port COM3 write-flash ' + offset + ' ' + arg)));
ol.appendChild(h('li', {}, 'Coming from another firmware, or want a clean board? Erase the flash first, then write as above (this removes your files):',
pre('esptool --chip ' + chip + ' --port COM3 erase-flash')));
fileCli('mpftp writes the same file from a terminal, and reads the chip and the offset from the file itself:',
'mpftp firmware flash --port esp32 --artifact ' + arg + ' -d COM3');
}

function renderFileUf2(name) {
$('fw-file-install').hidden = true;
$('fw-file-steps-title').textContent = 'Steps';
var ol = clear($('fw-file-steps'));
ol.appendChild(h('li', {}, 'Put the board in its UF2 bootloader. On a Raspberry Pi Pico or another RP2040 or RP2350 board, ' +
'hold BOOTSEL while you plug it into USB; on most other boards, double-tap RESET. ' +
'A drive appears, named RPI-RP2, RP2350 or after the board.'));
ol.appendChild(h('li', {}, 'Copy ' + name + ' onto that drive. The board restarts by itself when the copy finishes.'));
fileCli('mpftp finds the bootloader drive and copies the file for you. Give your board\'s port: rp2, samd or esp32.',
'mpftp firmware flash --port rp2 --uf2 --artifact ' + shellName(name));
}

function setupFile() {
var sel = $('fw-file-chip');
sel.appendChild(h('option', { value: '', text: 'Choose the chip' }));
Object.keys(CHIP_FAMILY).forEach(function (c) { sel.appendChild(h('option', { value: c, text: CHIP_FAMILY[c] })); });
sel.addEventListener('change', renderFile);
$('fw-file-input').addEventListener('change', function (e) {
var file = e.target.files && e.target.files[0];
var seq = ++pickSeq;
picked = null;
if (!file) { renderFile(); return; }
if (/\.uf2$/i.test(file.name)) { picked = { file: file, uf2: true }; renderFile(); return; }
if (!/\.bin$/i.test(file.name)) { renderFile(); fileNote('Pick a .bin or a .uf2 file.'); return; }
renderFile();
readHead(file).then(function (head) {
if (seq !== pickSeq) return;
picked = { file: file, uf2: false, head: head };
// A file whose header names no chip gets no choice carried over from an earlier pick.
sel.value = head.chip || '';
renderFile();
}, function (err) {
if (seq === pickSeq) fileNote('Could not read ' + file.name + ' (' + err.message + ').');
});
});
}

// ---- desktop interpreters ------------------------------------------------

function renderInterp() {
Expand Down Expand Up @@ -245,6 +419,8 @@
$('fw-board').innerHTML = '';
}

setupFile();

Promise.all([get('firmware.json'), get('interpreters.json', true)]).then(function (r) {
doc = r[0];
interp = r[1];
Expand Down