diff --git a/editor.html b/editor.html
index 3cf3ee5..c8ea865 100644
--- a/editor.html
+++ b/editor.html
@@ -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() {