v0.99.107
Portrait screens get the rest of the treatment 0.99.106 started: print status, print tune,
motion, bed mesh and the advanced panel each get an arrangement of their own rather than
landscape's row squeezed narrower, and overlays now come down from the top, above the bottom
navigation bar. Alongside that, WiFi gains a radio that actually turns off, a credential store
of its own and a forget-network path; filament Load and Unload stop diverging across the four
screens that can dispatch them; and the last 33 untranslated strings close out all eight
locales.
Added
- The WiFi radio can be turned off for real — the toggle previously detached from wpa_supplicant and left the radio powered and associating. It now drives rfkill, verifies the write, fails the toggle if the write fails, and remembers the choice across restarts. A live radio-off on a device whose only network path is the radio routes through a confirmation dialog first, since nothing else could turn it back on.
- Forget network — saved networks could be joined but never removed. Removal now mirrors to persistent storage rather than lasting until the next restart.
- HelixScreen keeps its own WiFi credential store — for the devices whose vendor wpa_supplicant config will not accept new credentials at all, which previously meant a network you joined was never saved.
- All eight locales are complete — every non-English locale sat at 98.7%: three keys were absent entirely (the only ones the build warned about) and thirty were present but empty, which falls back to English with no warning at all. Most were AFC filament-path and fault strings, plus the WiFi forget flow, the hardware-detect prompt, and the power-loss recovery messages. ja and zh introduce no new CJK characters, so the embedded fonts are unchanged.
- Power-loss recovery on Creality's Klipper fork (K1, K1C, K1 Max, K2 Plus, V3, Hi, i7) — recovery is exposed through a webhook Moonraker auto-registers, so it was always reachable from a third-party UI; only one undocumented parameter was missing. Mechanism verified against a physical K1C and K2 Plus. Resume is refused unless the detection probe confirmed, because that probe is also what arms the flag the stock sensorless-homing macro reads to choose a full Z clearance lift — without it, homing would drag the nozzle through a tall part.
- Screws tilt results can be shared by QR (#1226) — every screw's name, probed Z and adjustment string, plus a QR encoding the same values as plain text. A printer has no OS clipboard, so the QR is how the numbers reach a phone.
- CFS support for the community Kalico firmware on K2 Plus — the reimplemented box module shares zero keys with Creality's stock schema, so a fully populated 4-bay CFS rendered as an empty AMS panel. Schema and macro dialect are now detected independently, each read from the module's own registration table; a flat box whose module is unrecognized refuses control rather than emitting stock sequences it may not define.
- Heater icons carry the same thermal colour as the number beside them — muted off, red heating, green at temp, blue cooling, across print status, the temperature card, the preheat tiles and the temperature-graph overlay. Previously an icon pulsed red while its own number sat blue. In Maintaining mode the chamber icon now follows the chamber's cooling-ceiling rule the way its label always has.
- Print status shows the heater icon and its label together — they were exact breakpoint complements, so the card showed one or the other and never both.
- An Always Show Bypass Spool toggle for AFC (#1229) — for anyone who wants the bypass node visible on a machine where it is now hidden by default.
- Portrait overlays slide down from the top — anchored above the navigation bar that runs along the bottom in portrait, and full width rather than leaving an 8px sliver of backdrop down each edge.
Fixed
- A soft rfkill block locked the radio out permanently — startup treated any rfkill block as a fatal preflight failure and bailed before the code that can clear a block ever ran, so a block set by HelixScreen's own radio-off became a self-inflicted lockout that survived a reboot. This stranded a real WiFi-only printer for days during this work's own hardware testing. A soft block is now a warning and startup proceeds; a hard block (a physical switch we genuinely cannot clear) stays fatal. A stale block found at startup is cleared outright rather than waiting for someone to walk over and toggle the switch.
- Typed characters reached the debug bundle — the on-screen keyboard logged each pressed character at debug level, and a bundle ships the log ring verbatim with a debug floor regardless of configured verbosity, so a passphrase typed into a masked WiFi field left the device inside bundles shared in public channels. Keystrokes are redacted whenever the field is masked and demoted to trace, which also stops them evicting the diagnostic window — one bundle spent 1776 of its 2000 ring lines on this one handler, covering 3m50s.
- WiFi connect-failure toasts leaked the network name — the first-run wizard passed raw SSIDs into an error notification, which reaches the log under the hood. The PII gate that catches this everywhere else never looked inside notification macros.
- A non-UTF-8 SSID grew the credential store without bound — an SSID is a raw 802.11 octet string with no UTF-8 guarantee, and on every real ARM target such bytes passed validation, were mangled on write, and then never matched the raw SSID on the next connect. Deduplication silently stopped working and a fresh cleartext record was appended on every single connect to that network, forever. Stored values now round-trip losslessly.
- A saved network stacked a duplicate entry rather than reusing its own.
- An IP address was shown while not actually associated with a network.
- A failed WiFi scan trigger corrupted the scan suppression state.
- The WiFi interface, its wpa_supplicant daemon and its rfkill switch were three independent guesses — resolved once at init into one identity, with every daemon found logged.
- A failed radio change fed a false state back into the UI — the result of the change was discarded and the requested on/off state persisted regardless of whether it applied, so a denied rfkill write left the toggle lying about the radio and the next startup reasserting the wrong thing.
- Load and Unload behaved differently on each of the four screens that can dispatch them — the filament panel has always routed AMS backend → configured macro → raw G-code, but the AMS sidebar returned silently with no backend and both runout paths navigated away instead, so that ladder reached exactly one surface. All four now share one plan and one router. The sidebar's already-mounted toolchanger guard and its load-versus-swap rule reach the panel for the first time, and the unload-before-load question is answered per slot, so a mixed unit answers differently for a direct lane than a hub-routed one.
- A toolchanger Load spun for 120 seconds and refused every operation after it — selecting the tool already mounted is a firmware no-op that never touches the toolchanger's status, so nothing ever cleared the optimistic in-progress state and the panel stayed busy until restart.
- Toolhead badges named the wrong toolhead on AFC (#1229) — every T badge was an AFC per-lane map alias, which users read as a tool number; on the reporter's toolchanger AFC maps T0 to the extruder5 lane while Klipper's own T0 is extruder. T now means lane alias only, and toolheads carry Klipper extruder identity as E.
- AFC drew a bypass spool on machines with no bypass (#1229) — AFC publishes a virtual bypass sensor whether or not one is wired, so the node was drawn permanently and painted from the external-spool slot: the reporter saw a green ASA bypass spool on a machine that has no bypass at all. Other backends are untouched, where bypass is a real physical position.
- AFC's current slot froze on whichever writer got there first (#1229) — every other writer was gated behind "no slot yet", so on a toolchanger a parked toolhead lane was named current with an empty carriage, driving a wrong header, a phantom node in the filament path render, and an unload nobody asked for. It is now derived from carriage mount state at the end of a status frame instead of negotiated across eight parser sites.
- One unparsed lane tipped a pure-hub AFC unit into mixed routing (#1229) — Moonraker sends deltas and unit objects sort ahead of per-lane ones, so a frame can describe a unit whose lanes have not been seen yet, and unknown routing counted as direct. Users saw seven toolhead nodes for six extruders.
- The bypass toggle unloaded the active slot first on AFC and Happy Hare (#1229) — two AFC users asked for the opposite: send the one command and let the firmware answer. Backends whose users have no console to fall back on keep the existing behaviour.
- A QIDI tool remap badged one lane and operated on another — the mapping was written in one direction only, so with T0 remapped to slot 2 the AMS panel showed lane 2 while the filament panel gated and acted on lane 0. It was also the only mapping-capable backend that silently disabled the print-start remap snapshot.
- Every Load on a Snapmaker U1 seated the carriage and fed nothing — the unload-before-load rule encodes a serial assumption about clearing one shared path. The U1 is parallel — four toolheads, each with its own extruder — so the rule was true always and routed every Load through a tool change.
- The filament strip and the AMS panel could show different lanes as loaded — after an idle unload the embedded mini status kept a lane badged while the AMS panel had already cleared it.
- A crash while thumbnails were loading in the print file browser (#960) — four thumbnail callbacks checked a liveness flag and then dereferenced the panel on a worker thread, and the call was virtual, so it dispatched through a freed vtable pointer. Fetch results are also now always delivered on the main thread: which thread they arrived on depended on which internal path produced them, and callers set image sources from them (#1202).
- A crash when the orientation changed with the bed mesh panel open — the reactive portrait branch rebuilds the canvas in place and nothing nulled the panel's pointer to the old one, so every later use — including the async render thread's entry point — was a use-after-free.
- A crash shutting down while the printer was unreachable (#1212) — a retry timer armed on every failed connect was never recorded, so it could not be cancelled, and the event loop nulls its handle on one thread while still draining timers on another.
- A language change overlapping a Moonraker error read freed memory (#1219) — the event handler resolved its notification title on whichever thread raised it, while the translation table is freed and replaced from the main thread.
- A crash after a print-start preparation timeout (#1221) — a null check, then a queue drain that ran an already-queued overlay dismissal, then a use of the pointer that dismissal had just nulled. The optimistic-navigation unwind is now gated on print status still being on top, since after a 60-second timeout you may be several screens away and popping blind closed the screen you were on.
- A modal dismissed through the static hide path leaked its object (#1230) — its teardown hook never ran, its statics stayed non-null and its widget pointers dangled.
- A 16-bit PNG overran the decode buffer — decode output is allocated as a fixed four bytes per pixel while the decoder writes the file's own bits per pixel, so 16-bit RGB and RGBA overran by 1.5x and 2x. Such files are now rejected at allocation.
- The recovery dialog did not reappear after a backdrop dismiss — the next shutdown event was spent clearing a stale pointer and showed nothing; the dialog only returned on the event after that.
- Data races in WebSocket teardown (#1212) — the connection-state callback had a mutex guarding only the read, so replacing it freed the old target's storage while it was being copied on another thread.
- A 36-point bed mesh counted to 72 on a Qidi Q2 (#1224) — progress divided by a sample count read from the printer config, which only works when we recognise that printer's probe section, and the list of those is open-ended across firmware forks. Probe points are now deduplicated by position, which needs no configuration and absorbs retries a fixed divisor cannot.
- "Bed is Level!" on a bed that was not (#1225) — Klipper reports every screw relative to the first one in config order, so a mid-range base let two corners sit inside tolerance in opposite directions while the real corner-to-corner error was their sum: 0.111 mm reported as level. The verdict now measures signed spread across all screws, and the tolerance is converted through the real screw thread pitch read from the printer config, so an M4 bed is no longer judged with M3 slack.
- WLED strips took an error on every action (#1241) — Moonraker nests the strip map one level deeper than the code unwrapped, so it iterated a single entry named
strips, posted that as the strip name, and read every polled state as off. The mock emitted the same flattened shape, so the parser and its test fixture agreed with each other and no test caught it. - Power devices with hyphens in their name did nothing (#1241) — Moonraker names power devices after their config section, and a name like
-Power-was rejected by an identifier check before the request was ever built. The name is URL-encoded and never reaches G-code, so only control characters are rejected now. A rejection surfaces as a toast rather than a log line nobody sees. - The four z-step buttons were 0 px wide at 272x480 — the print-tune z-offset row carried flat pixel literals from landscape: rendered, bound, impossible to tap. A non-wrapping help label also overflowed the section by 54-67 px at every portrait size.
- The header action button rendered taller than the bar containing it — its height ladder exceeds the header's from the Large tier up, so at 480x800 an 80 px button sat in a 68 px header and clipped off the top of the screen.
- The advanced panel's E-Stop, Restart Klipper and Firmware Restart labels clipped mid-word in portrait — three buttons sharing one flex row, each getting a third of the screen. They stack in portrait now; landscape is unchanged.
- The portrait exclude-object list crowded out the object map — its height was a constant meaning "the controls take about 55% of the column". They take 14-34%, and because the list is a floating child its percentage never consults a measurement, so the surplus sat on top of a live tappable map: 43% of it at 480x800, and 636 px of empty list at 320x1480. Portrait now measures the controls; landscape keeps its existing rule.
- The portrait print-status preview reserved the wrong amount of space — the metadata strip has two stable heights depending on whether the print sets an M117 message, so no fixed reservation was correct. The split is computed with flex now, which also replaces a hand-tuned offset in landscape that had been clipping the top of the artwork. Portrait controls compress into one row of temperature chips, two merged status rows and a single row of five buttons, which roughly doubles the artwork band at 480x800 and frees enough height for the fan row to return; the reclaimed slack carries a temperature mini-graph.
- The bed mesh panel stacked into a column that starved its canvas in portrait — reserving height for both cards before sizing the canvas capped it at 117 px on a 480x640 screen. The two cards sit side by side in one row now and the canvas grows to 273 px. Max/Min coordinates round to whole millimetres and drop to an indented sub-line only where they still do not fit, instead of wrapping across three lines.
- The preparing overlay centred below the artwork, over the dimmed metadata strip — and its progress bar cost 12 px plus a gap while showing zero.
- A large modal spinner was sized from the cramped axis — a 320x1480 panel drew a 48 px spinner inside a 471 px card.
- A vertical icon layout was discarded when the icon came from a binding — the button parsed the icon position but only reached the code using it when a static icon attribute was also present.
- Hot reload brought a component back live but inert — reloading destroyed the component's scope and took C++-registered subjects with it, so every binding naming one resolved to nothing. Before a companion fix in the XML engine, the same path aborted on a heap error instead.
- A misleading 14m37s gap in the logs (#1218) — wall-clock-only timestamps make a clock step look like a stall, and one bundle appeared to show a long Moonraker reconnect gap that was really an 874-second step on a printer with no RTC. It cost real triage effort. Every line now carries a monotonic column and any line the wall clock jumped across is annotated with the size of the jump.
- A heap snapshot age of 33 hours against a 9.8 hour uptime — not stale but impossible, and the number was printed as fact with no way to tell which of two causes produced it. Raw clock stamps now ship alongside the age and the reporter says which explanation fits.
- The temperature chart evicted the entire diagnostic log — one debug line per series per sample interval, so a bundle arrived with 2000 of its 2000 ring lines being that one message and nothing else from the incident.
- Crash reports auto-filed GitHub issues for builds that were never published (#1240) — the ingest key is compiled into every binary and the repo is public, so any fork or local build could file an issue that can never be symbolicated, because no symbol file for that build exists anywhere. One arrived claiming a version this project has never held.
- Nightly test shards reported green with failures in them — the sharded loop collected the exit status of a text filter in a pipeline rather than the test binary's, so a shard with 8 failing cases still passed the job.
- A submodule patch that existed only in a worktree was silently never applied — and an unreadable patch file reported itself as already applied. Both now fail the build loudly.
Changed
- Slider settings persist when you let go, not on every tick of the drag — brightness, volume and LED startup brightness each wrote the settings file per tick: serialize, two fsyncs and a rolling backup copy, plus a forked shell per tick for the backlight on Sonic Pad. Durability is unchanged; only how often the durable write happens.
- A tap on the object map no longer does unbounded disk I/O (bundle C2CP6ZAW) — the G-code layer cache held its lock across a file seek and a parse, so work on one layer blocked every other, and the touch hit-test reached that path from inside a UI callback while a background builder walked all layers through it. On a two-core K2 Plus the reporter worked around it by forcing thumbnail-only rendering.
- Switching G-code files no longer freezes the UI for a whole parse — the background load only checked for cancellation after the entire file was parsed, and the main thread waits for that thread, so switching files stalled everything for seconds on a multi-megabyte file.
- Thumbnail PNGs are read on a worker thread — scrolling a G-code folder previously put a whole-file read on the main thread for every card, once per file as the listing populated.
- WiFi scans stop spending two thirds of their time off-channel.
- Layouts can branch on orientation and on height — orientation and the vertical size tier are published as bindable values, so a panel no longer has to infer either from the narrower of its two axes.