Fix firmware-update brick: app.py must be ASCII for the 7-bit MIDI push (+ guards)
Root cause: a non-ASCII em-dash in an app.py comment. The A/B updater pushes app.py as 7-bit SysEx (charCode & 0x7F), which turned the em-dash's bytes into a NUL byte -> corrupt source -> the pushed build crashed on boot (black screen, onboard LED blinking CircuitPython's error/safe-mode pattern). A dragged copy was fine (valid UTF-8); only the over-MIDI path mangled it. - Replace the em-dash with ASCII; app.py is now pure ASCII. - build.sh now ASSERTS pico-cp/app.py is pure ASCII (fails the build otherwise) so this class of bug can never ship again. - Device 0x20 handler VALIDATES the pushed app.py before installing (reject if it contains a NUL byte, or is missing App().run()/APP_VERSION) and now catches ALL exceptions (not just OSError) -> a corrupt/truncated/oversized push NAKs and keeps the working build instead of bricking. Longer pre-reload sleep so the ACK flushes. APP_VERSION -> 0.0.5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2b113a18cc
commit
711a02fcc1
3 changed files with 17 additions and 7 deletions
9
build.sh
9
build.sh
|
|
@ -37,8 +37,13 @@ pathlib.Path("dist/embed.js").write_text(pathlib.Path("embed.js").read_text())
|
||||||
print("copied embed.js")
|
print("copied embed.js")
|
||||||
pathlib.Path("dist/pico-main.py").write_text(pathlib.Path("pico/main.py").read_text()) # PM_K-1 firmware, downloadable
|
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")
|
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
|
_appsrc = pathlib.Path("pico-cp/app.py").read_text()
|
||||||
print("copied pico-cp-app.py")
|
# 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)
|
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:
|
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",
|
for f in ("code.py", "app.py", "boot.py", "programs.json", "font_s.bin", "font_m.bin", "font_l.bin",
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
import board, busio, digitalio, analogio, pwmio, displayio, vectorio, time, json, gc, os, supervisor
|
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
|
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:
|
try:
|
||||||
import rtc # set from the editor's clock SysEx so the log has real timestamps
|
import rtc # set from the editor's clock SysEx so the log has real timestamps
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -165,7 +165,7 @@ def make_text(s, font, fg, bg):
|
||||||
pen += adv
|
pen += adv
|
||||||
return displayio.TileGrid(bmp, pixel_shader=pal), w, h
|
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):
|
def load_alpha(path):
|
||||||
try:
|
try:
|
||||||
with open(path, "rb") as f: blob = f.read()
|
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
|
elif cmd == 0x20: # A/B firmware update: install new app.py to the trial slot
|
||||||
try:
|
try:
|
||||||
data = bytes(sx[2:])
|
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")
|
try: os.remove("/app.bak")
|
||||||
except OSError: pass
|
except OSError: pass
|
||||||
os.rename("/app.py", "/app.bak") # keep the current build as the rollback
|
os.rename("/app.py", "/app.bak") # keep the current build as the rollback
|
||||||
with open("/app.py", "wb") as f: f.write(data)
|
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
|
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
|
if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7F, 0xF7])) # ACK -> rebooting
|
||||||
time.sleep(0.3); supervisor.reload()
|
time.sleep(0.4); supervisor.reload()
|
||||||
except OSError:
|
except Exception: # catch ALL (OSError read-only, MemoryError, ...) -> never brick
|
||||||
if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7E, 0xF7])) # NAK: read-only (editor mode)
|
if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7E, 0xF7])) # NAK
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
if self.touch.addr is None:
|
if self.touch.addr is None:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue