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() {