App Framework¶
scrollkit.app provides the two entry points for building applications.
MinimalLEDApp¶
scrollkit.app.minimal.MinimalLEDApp — a lightweight, synchronous-feeling
wrapper for simple scripts and the lowest memory footprint. It auto-detects the
environment and delegates to a CircuitPython or desktop implementation.
from scrollkit.app.minimal import MinimalLEDApp
app = MinimalLEDApp()
app.show_text("Ready", color="green") # static text; color name or (r,g,b)
app.scroll_text("Hello!", color=(0, 170, 255))
app.clear()
ScrollKitApp¶
scrollkit.app.base.ScrollKitApp — the full-featured async base class. Subclass
it and override the hooks you need.
class MyApp(ScrollKitApp):
def __init__(self):
super().__init__(enable_web=True, update_interval=300)
async def setup(self): # once, at startup
...
async def update_data(self): # every update_interval seconds
...
async def prepare_display_content(self): # each display frame
return await self.content_queue.get_current() # default behaviour
Three-process architecture¶
run() launches up to three cooperative async tasks, gated by available RAM:
| Process | Runs when | Job |
|---|---|---|
| Display | always | render content at ~20 FPS |
| Data update | ≥ ~30 KB free | call update_data() every update_interval |
| Web server | enable_web and ≥ ~50 KB free |
serve the config UI |
On low-memory devices the data and web processes are skipped automatically so the display always keeps running — graceful degradation rather than a crash.
Naming
ScrollKitApp is the public name; SLDKApp remains as a backward-compatible
alias.
Pausing the display during a blocking update¶
update_data() often paints an off-queue status frame ("Updating…") and then makes
a blocking fetch. Because the synchronous fetch freezes the loop, the previous
queue item can ghost over the status frame. Suspend rendering for that window — the
queue keeps its items, so a failed fetch resumes the last-good content instead
of going black:
async def update_data(self):
with self.suspended_render(): # always resumes, even on exception
await self._teardown_active_content()
await self.paint_status_frame("Updating")
ok = await self.fetch() # loop frozen anyway → no ghosting
suspend_render() / resume_render() and the render_suspended property are also
available if you can't use the context manager. While suspended the base
prepare_display_content() returns None, so you no longer override it just to gate
rendering. Default is not suspended.
Reliability: watchdog + NVM diagnostics¶
Pair the hardware watchdog (enable_watchdog=True) with
scrollkit.utils.diagnostics for a device
that self-heals and can explain itself:
from scrollkit.utils import diagnostics
diag = diagnostics.open() # NVM on device, no-op on desktop
diag.record_boot(diagnostics.read_reset_reason())
if diag.safe_mode: # too many fault-reboots in a row
... # skip the fetch; keep the config UI up
diag.note_fetch_result(ok=True) # on a healthy refresh
The record lives in microcontroller.nvm, so it survives both soft resets and power
loss (unlike a flash log a crash can wipe). After RAPID_BOOT_LIMIT fault-reboots
with no clean run it trips safe mode — break a deterministic boot loop instead of
resetting forever — and it keeps the last reset reason + exception text for a config
page post-mortem.