Firmware update: file-picker fallback so the OFFLINE on-device editor can update too

The editor that ships on the device opens via file://, whose null origin can't
fetch() anything — so "Update firmware" died at the download-the-latest step
(CORS-blocked) before the USB-MIDI push. Now _firmwareSource() tries the site
(same-origin on the https editor; absolute URL when online) and, failing that,
lets the user pick app.py — mirroring the programs.json download/drag fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-29 08:10:51 -05:00
parent 72bf3a2da2
commit e24a39e4e8

View file

@ -1189,14 +1189,13 @@ async function updateFirmware() { // A/B firmware update over USB-MIDI, with a
if (!(await _ensureMidi()) || !_midiOutputs().length)
return alert("Connect the PM_K-1 (Chrome/Edge/Firefox), then try again.");
const dev = await _queryDeviceVersion();
let src;
try { src = await (await fetch("/pico-cp-app.py", { cache: "no-store" })).text(); }
catch (e) { return alert("Couldn't fetch the latest firmware from the site."); }
const src = await _firmwareSource();
if (src == null) return; // offline + user cancelled the picker
const m = src.match(/APP_VERSION\s*=\s*["']([^"']+)["']/); const latest = m && m[1];
if (!latest) return alert("Couldn't read the latest firmware version.");
if (!latest) return alert("That file isn't PM_K-1 firmware (no APP_VERSION line).");
const upToDate = dev && dev === latest;
if (!confirm("Device firmware: " + (dev || "unknown") + "\nLatest: " + latest +
(upToDate ? "\n\nYou're up to date. Re-install anyway?"
if (!confirm("Device firmware: " + (dev || "unknown") + "\nNew build: " + latest +
(upToDate ? "\n\nSame version. Re-install anyway?"
: "\n\nUpdate now? The device reboots, runs the new build, and auto-rolls-back if it fails to start."))) return;
const bytes = [0xF0, 0x7D, 0x20];
for (let i = 0; i < src.length; i++) bytes.push(src.charCodeAt(i) & 0x7F); // app.py is ASCII
@ -1208,6 +1207,29 @@ async function updateFirmware() { // A/B firmware update over USB-MIDI, with a
: ok === false ? "The device is in editor mode (read-only to the updater). Reboot it normally (don't hold A) and try again."
: "No acknowledgement from the device. Make sure it's connected and not in editor mode — or drag app.py onto the drive in editor mode.");
}
// Where the new app.py comes from: the site when online (the https editor, same-origin), else let the user
// pick the file — so the OFFLINE editor that ships ON the device can update too (file:// pages can't fetch).
async function _firmwareSource() {
for (const url of ["/pico-cp-app.py", "https://metronome.varasys.io/pico-cp-app.py"]) {
try { const r = await fetch(url, { cache: "no-store" }); if (r.ok) return await r.text(); } catch (_) {}
}
alert("Can't reach the site from this offline copy.\n\nPick the firmware file (app.py) to flash — download it from\n" +
"metronome.varasys.io/pico-cp-app.py, or just open the online editor at\nmetronome.varasys.io/editor.html (it updates automatically).");
return await _pickFile(".py,text/x-python,text/plain", "PM_K-1 firmware (app.py)", { "text/x-python": [".py"] });
}
function _pickFile(accept, desc, fsTypes) { // resolve to the chosen file's text, or null if cancelled
return new Promise(async (res) => {
if (window.showOpenFilePicker) {
try { const [h] = await showOpenFilePicker({ types: [{ description: desc, accept: fsTypes }] });
return res(await (await h.getFile()).text()); }
catch (e) { if (e.name === "AbortError") return res(null); }
}
const inp = document.createElement("input"); inp.type = "file"; inp.accept = accept;
inp.onchange = () => { inp.files[0] ? inp.files[0].text().then(res) : res(null); };
inp.oncancel = () => res(null);
inp.click();
});
}
// Apply a shared link on load. Returns true if it set the metronome state.
function applyHashShare() {