From 711a02fcc112664bffb08e6d6d1da6286d7e1e8d Mon Sep 17 00:00:00 2001 From: Me Here Date: Fri, 29 May 2026 10:14:13 -0500 Subject: [PATCH] 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) --- build.sh | 9 +++++++-- pico-cp/__pycache__/app.cpython-312.pyc | Bin 54406 -> 54623 bytes pico-cp/app.py | 15 ++++++++++----- 3 files changed, 17 insertions(+), 7 deletions(-) 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 67d1851c9a1f7179e9b6cae625b3e73242166ee0..20ce088f4cc55974f9d44b59499d402b0279a86c 100644 GIT binary patch delta 547 zcmZo$$$WnmGv8@mUM>b85L+vmRWx%WUotDB>EmbNv~#v$S_XkbCI7cV8k`K?Vu^+ zk;!`w9%tm4eC%KolcvmMj>D4_in)Ob9SaIHH1&!~^E5Oe{D6S?FxQ}9Pk+D3vkoh= z&zcDWHzyxD{ETt$=9NcM82!4VKQQt0`ZL}Umzfc?fN=rSjIhbI6Ied5F^G!I5Si$? zz;1`gMP=Iqb{8UJFFMEG5SN}_IjM4i%5u$xn#=VU>TglKWD#)LDDa9>&LbT+f=3C+KA|V%qsM%b!O<4TIc3l3&CPt8ixEWfv3g6epBCl->N_oM?IX>6 zMv4(c%5nN=GoMjs2C6@!#Ry_+gV}nlKIZHKXN=iF5?0o}R?KHD8GSb&JnqZPcxf~L zX*MRtvy;Wn9%ENv^=JI-!npa-*{7`132cn=9~r=;3oipBOEYUbXCr4T_m2b8NLntLl|FqVUotDB$>t(fvF%J%Y?B*$B{nbGW5LW+bZzpz z{c@A54#+c#PM&o@Q&KENbTt!59|Hp;Ly07W#lVmvHu=s0Z)WiniOE_AS8_?NW`xKv zPUdrwpDbX+HJR;@DPzlIn?uJZyB=z0(v+Iqad^^Xt0Ri+#$a%Ha?+7!jBT4ekESq+ zcSV0-;^OsZydy3%BWMBR0;U;ZlWQliO#XeWhEaHO#c^Lr9tH{dO9Cp3SS|>t{CX~6 zcR|4J_ZJQZ5$VbIj_cI-04?9<$mMCza9D}QOPb-Zo}3pS^AR2=Q6T4-CZD$z!!dJF zZ#jnJJW4?JaXle#J?0Y(j5j3APa)k}i?lrRTS+bL;lZ)xV!Qj8!{ zj?-J4`LsGSQ2l8wMi5&Y%+_P|HfKN0Z_Eyou(I~CVm@QZ=(Bm|314Q$vzwovW@BPJ zGx^=wV{Fo_{*0fkHt#<7lvO&KjZyw11DLGhVPIrwW^Lze 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: