Phase 2: editor 'Save/Load to device' + bundle the editor on the drive
Add to the editor's set-list ⋯ menu: - 📟 Save to device — writes the active set list as programs.json (the same file the PM_K-1 firmware reads). Uses the File System Access API to write straight onto the CIRCUITPY drive (Chrome/Edge); falls back to a download to drag on. Reuses setupToPatch() per item -> {title, programs:[{name, prog}]}. - 📥 Load from device — reads a programs.json back into a new set list (patchToSetup per item; reuses the existing import path). Bundle the built editor.html into pm_k1_circuitpy.zip so the drive carries its own offline programmer. info-kit + pico-cp/README document the workflow. Verified: editor loads with no console errors; both menu buttons + all four functions present; zip contains editor.html. (FSA save needs a real user gesture to test on-device.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3664b7a29e
commit
d558dccbde
4 changed files with 64 additions and 5 deletions
1
build.sh
1
build.sh
|
|
@ -41,5 +41,6 @@ import zipfile # PM_K-1 CircuitPython drive bundle (download → unzip onto CIR
|
|||
with zipfile.ZipFile("dist/pm_k1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z:
|
||||
for f in ("code.py", "programs.json", "font_s.bin", "font_m.bin", "font_l.bin", "README.md"):
|
||||
z.write("pico-cp/" + f, f)
|
||||
z.write("dist/editor.html", "editor.html") # offline copy of the editor, on the drive
|
||||
print("zipped pm_k1_circuitpy.zip")
|
||||
PY
|
||||
|
|
|
|||
48
editor.html
48
editor.html
|
|
@ -347,6 +347,8 @@
|
|||
<button id="exportBtn">⭳ Export all (file)</button>
|
||||
<button id="importBtn">⭱ Import file…</button>
|
||||
<input type="file" id="importFile" accept="application/json" style="display:none">
|
||||
<button id="saveDeviceBtn" title="Write this set list to a PM_K-1 device drive (programs.json)">📟 Save to device</button>
|
||||
<button id="loadDeviceBtn" title="Load a device's programs.json into a new set list">📥 Load from device</button>
|
||||
<button id="clearLogBtn">🗑 Clear log</button>
|
||||
<button id="resetAllBtn" style="color:#ff7b6b">♻ Reset everything…</button>
|
||||
</div>
|
||||
|
|
@ -1069,6 +1071,50 @@ function shareSetlist() {
|
|||
openShare("Share “" + (sl.title || "set list") + "”", shareLink("sl=" + setlistToCode(sl)), "Shares the whole set list (each item's settings).");
|
||||
}
|
||||
|
||||
/* Device (PM_K-1) programs.json — the same grooves the firmware reads.
|
||||
Save: writes the active set list straight onto the CIRCUITPY drive (File System Access,
|
||||
Chrome/Edge) or downloads it to drag on. Load: reads a programs.json into a new set list. */
|
||||
function programsJSON() {
|
||||
const sl = getSL(); if (!sl) return null;
|
||||
return JSON.stringify({ title: sl.title || "PolyMeter", programs: sl.items.map((it) => ({ name: it.name, prog: setupToPatch(it) })) }, null, 2);
|
||||
}
|
||||
async function saveToDevice() {
|
||||
const json = programsJSON(); if (!json) return alert("No set list selected to save.");
|
||||
if (window.showSaveFilePicker) {
|
||||
try {
|
||||
const h = await showSaveFilePicker({ suggestedName: "programs.json",
|
||||
types: [{ description: "PolyMeter programs", accept: { "application/json": [".json"] } }] });
|
||||
const w = await h.createWritable(); await w.write(json); await w.close();
|
||||
return alert("Saved programs.json — pick your CIRCUITPY drive and the device auto-reloads with these grooves.");
|
||||
} catch (e) { if (e.name === "AbortError") return; } // cancelled or unsupported → fall through to download
|
||||
}
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(new Blob([json], { type: "application/json" }));
|
||||
a.download = "programs.json"; a.click(); URL.revokeObjectURL(a.href);
|
||||
alert("Downloaded programs.json — drag it onto the device's CIRCUITPY drive (it auto-reloads).");
|
||||
}
|
||||
function importPrograms(text) {
|
||||
try {
|
||||
const d = JSON.parse(text);
|
||||
const items = (d.programs || []).map((p) => ({ name: p.name || "Item", ...patchToSetup(p.prog) }));
|
||||
if (!items.length) return alert("No programs found in that file.");
|
||||
setlists.push({ title: d.title || "Device", description: "", items }); activeSL = setlists.length - 1; activeItem = 0;
|
||||
saveSetlists(); renderSetlists(); applySetup(items[0]);
|
||||
alert("Loaded " + items.length + " grooves from the device into a new set list.");
|
||||
} catch (e) { alert("Load failed: " + e.message); }
|
||||
}
|
||||
async function loadFromDevice() {
|
||||
if (window.showOpenFilePicker) {
|
||||
try {
|
||||
const [h] = await showOpenFilePicker({ types: [{ description: "PolyMeter programs", accept: { "application/json": [".json"] } }] });
|
||||
return importPrograms(await (await h.getFile()).text());
|
||||
} catch (e) { if (e.name === "AbortError") return; }
|
||||
}
|
||||
const inp = document.createElement("input"); inp.type = "file"; inp.accept = ".json,application/json";
|
||||
inp.onchange = () => { if (inp.files[0]) inp.files[0].text().then(importPrograms); };
|
||||
inp.click();
|
||||
}
|
||||
|
||||
// Apply a shared link on load. Returns true if it set the metronome state.
|
||||
function applyHashShare() {
|
||||
const h = location.hash || "";
|
||||
|
|
@ -1292,6 +1338,8 @@ $("shortcutsOverlay").addEventListener("click", (e) => { if (e.target.id === "sh
|
|||
$("exportBtn").addEventListener("click", () => { $("trayMenu").hidden = true; exportAll(); });
|
||||
$("importBtn").addEventListener("click", () => { $("trayMenu").hidden = true; $("importFile").click(); });
|
||||
$("importFile").addEventListener("change", (e) => { if (e.target.files[0]) importAll(e.target.files[0]); e.target.value = ""; });
|
||||
$("saveDeviceBtn").addEventListener("click", () => { $("trayMenu").hidden = true; saveToDevice(); });
|
||||
$("loadDeviceBtn").addEventListener("click", () => { $("trayMenu").hidden = true; loadFromDevice(); });
|
||||
$("clearLogBtn").addEventListener("click", () => { $("trayMenu").hidden = true; clearLog(); });
|
||||
$("resetAllBtn").addEventListener("click", () => { $("trayMenu").hidden = true; resetAll(); });
|
||||
$("shareSettingsBtn").addEventListener("click", () => { $("trayMenu").hidden = true; shareSettings(); });
|
||||
|
|
|
|||
|
|
@ -133,9 +133,10 @@
|
|||
<summary>CircuitPython edition — USB drive + editor (experimental)</summary>
|
||||
<div class="spec-body">
|
||||
<p class="sub">An alternative firmware that makes the Pico mount as a <b>USB drive</b> carrying the
|
||||
firmware and your tracks (<code>programs.json</code>) — edit on the web and reprogram it without
|
||||
Thonny. Coming next: one‑click "Save to device" and USB‑MIDI audio out to your computer's speakers.
|
||||
The MicroPython firmware above stays the simple, rock‑solid option.</p>
|
||||
firmware, your tracks (<code>programs.json</code>) and a copy of this editor — design grooves on the
|
||||
web and write them straight to the device, no Thonny. A full lanes/pads display with anti‑aliased
|
||||
text drives the touchscreen. The MicroPython firmware above stays the simple, rock‑solid option.
|
||||
(USB‑MIDI audio out to your computer's speakers is the next step.)</p>
|
||||
<p>
|
||||
<a class="dl" href="/pm_k1_circuitpy.zip" download>Download CircuitPython bundle ↓</a>
|
||||
<a class="dl alt" href="https://codeberg.org/VARASYS/metronome/src/branch/main/pico-cp" target="_blank" rel="noopener">Source + README ↗</a>
|
||||
|
|
@ -143,8 +144,11 @@
|
|||
<ol class="steps">
|
||||
<li>Flash <b>CircuitPython</b> (<a href="https://circuitpython.org/board/raspberry_pi_pico/" target="_blank" rel="noopener">raspberry_pi_pico</a>)
|
||||
via BOOTSEL → the <code>CIRCUITPY</code> drive appears.</li>
|
||||
<li>Unzip the bundle onto <code>CIRCUITPY</code> (<code>code.py</code> + <code>programs.json</code>) — it's a normal drive, just drag them on.</li>
|
||||
<li>It runs on boot; editing <code>programs.json</code> auto‑reloads the grooves. See the bundle's README for calibration flags.</li>
|
||||
<li>Unzip the bundle onto <code>CIRCUITPY</code> (it's a normal drive — just drag everything on). It runs on boot.</li>
|
||||
<li><b>Reprogram it from the web:</b> build a set list in the <a href="/editor.html">editor</a>, then the
|
||||
set‑list <b>⋯</b> menu → <b>📟 Save to device</b> → pick the <code>CIRCUITPY</code> drive. The Pico
|
||||
auto‑reloads with your grooves. (In Chrome/Edge it writes straight to the drive; otherwise it
|
||||
downloads <code>programs.json</code> to drag on.) <b>📥 Load from device</b> reads it back.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ same program‑string language as <https://metronome.varasys.io>.
|
|||
- `code.py` (this firmware — runs on boot)
|
||||
- `programs.json` (your grooves)
|
||||
- `font_s.bin`, `font_m.bin`, `font_l.bin` (the anti‑aliased fonts — kept as files to save RAM)
|
||||
- `editor.html` (an offline copy of the web editor, so the drive carries its own programmer)
|
||||
3. It starts immediately. Editing `programs.json` (or re‑saving it from the editor) makes CircuitPython
|
||||
**auto‑reload** with the new tracks.
|
||||
|
||||
|
|
@ -38,6 +39,11 @@ same program‑string language as <https://metronome.varasys.io>.
|
|||
|
||||
Each `prog` is a program string from the web editor. Add/replace entries and save — the device reloads.
|
||||
|
||||
**Easiest way to (re)program it:** in the editor (the web app, or the `editor.html` on the drive), build a
|
||||
set list, then the set‑list **⋯** menu → **📟 Save to device** → pick the `CIRCUITPY` drive. In Chrome/Edge it
|
||||
writes `programs.json` straight onto the drive (the Pico auto‑reloads); elsewhere it downloads the file to drag
|
||||
on. **📥 Load from device** reads a `programs.json` back into a new set list.
|
||||
|
||||
## Calibration (flip flags at the top of `code.py`)
|
||||
|
||||
- **Red/blue swapped:** flip `MADCTL` between `0x48` (default) and `0x40`.
|
||||
|
|
|
|||
Loading…
Reference in a new issue