diff --git a/build.sh b/build.sh index 3b60e88..b4886ea 100755 --- a/build.sh +++ b/build.sh @@ -37,8 +37,13 @@ pathlib.Path("dist/embed.js").write_text(pathlib.Path("embed.js").read_text()) print("copied embed.js") pathlib.Path("dist/pico-main.py").write_text(pathlib.Path("pico/main.py").read_text()) # PM_K-1 firmware, downloadable print("copied pico-main.py") -pathlib.Path("dist/pico-cp-app.py").write_text(pathlib.Path("pico-cp/app.py").read_text()) # served for the editor's A/B firmware updater -print("copied pico-cp-app.py") +_appsrc = pathlib.Path("pico-cp/app.py").read_text() +# The A/B updater pushes app.py over USB-MIDI as 7-bit data, so it MUST be pure ASCII -- a stray +# non-ASCII char (e.g. an em-dash in a comment) gets mangled to a NUL byte and bricks the device. +_bad = [(i, c) for i, c in enumerate(_appsrc) if ord(c) > 0x7F] +assert not _bad, "pico-cp/app.py has non-ASCII at %r -- the MIDI updater needs pure ASCII" % (_bad[:5],) +pathlib.Path("dist/pico-cp-app.py").write_text(_appsrc) # served for the editor's A/B firmware updater +print("copied pico-cp-app.py (ascii-checked)") import zipfile # PM_K-1 CircuitPython drive bundle (download → unzip onto CIRCUITPY) with zipfile.ZipFile("dist/pm_k1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z: for f in ("code.py", "app.py", "boot.py", "programs.json", "font_s.bin", "font_m.bin", "font_l.bin", diff --git a/pico-cp/__pycache__/app.cpython-312.pyc b/pico-cp/__pycache__/app.cpython-312.pyc index 67d1851..20ce088 100644 Binary files a/pico-cp/__pycache__/app.cpython-312.pyc and b/pico-cp/__pycache__/app.cpython-312.pyc differ diff --git a/pico-cp/app.py b/pico-cp/app.py index 0f15b00..d2a5a59 100644 --- a/pico-cp/app.py +++ b/pico-cp/app.py @@ -18,7 +18,7 @@ import board, busio, digitalio, analogio, pwmio, displayio, vectorio, time, json, gc, os, supervisor supervisor.runtime.autoreload = False # we write our own files (log + pushed programs); never self-restart -APP_VERSION = "0.0.4" # firmware version (the A/B updater pushes/compares this) +APP_VERSION = "0.0.5" # firmware version (the A/B updater pushes/compares this) try: import rtc # set from the editor's clock SysEx so the log has real timestamps except ImportError: @@ -165,7 +165,7 @@ def make_text(s, font, fg, bg): pen += adv return displayio.TileGrid(bmp, pixel_shader=pal), w, h -# ---- single-image alpha assets (logo, status icons) — blit like a one-off glyph; see gen_assets.py ---- +# ---- single-image alpha assets (logo, status icons) - blit like a one-off glyph; see gen_assets.py ---- def load_alpha(path): try: with open(path, "rb") as f: blob = f.read() @@ -704,15 +704,20 @@ class App: elif cmd == 0x20: # A/B firmware update: install new app.py to the trial slot try: data = bytes(sx[2:]) + # sanity-check the transfer before touching the working build: a corrupt/truncated push + # (e.g. a dropped MIDI byte, or a 7-bit-mangled non-ASCII char -> NUL) must NOT be installed. + if (0 in data) or (b"App().run()" not in data) or (b"APP_VERSION" not in data): + if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7E, 0xF7])) # NAK: rejected, kept current build + return try: os.remove("/app.bak") except OSError: pass os.rename("/app.py", "/app.bak") # keep the current build as the rollback with open("/app.py", "wb") as f: f.write(data) open("/trial", "w").close() # arm the trial; the loader reverts if it won't boot if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7F, 0xF7])) # ACK -> rebooting - time.sleep(0.3); supervisor.reload() - except OSError: - if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7E, 0xF7])) # NAK: read-only (editor mode) + time.sleep(0.4); supervisor.reload() + except Exception: # catch ALL (OSError read-only, MemoryError, ...) -> never brick + if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7E, 0xF7])) # NAK def run(self): if self.touch.addr is None: