HackyLens 0.4.0 is modular firmware built from common firmware/src layers, the selected boards/<id> BSP, and the K210 HAL/startup under platforms/k210.
HackyLens v0.4 is a layered K210 reference firmware and MicroPython technology preview.
This is the current architecture and implementation status. Roadmap contains product priorities; technical references describe public interfaces. Historical plans and decisions remain in Git history.
The project aims to reuse feature logic across K210 boards, compose firmware from isolated native apps, and share hardware services with MicroPython. Portability and Python-to-native migration must be demonstrated with real applications, measurements and physical ports. They are not established by the number of interfaces or by compiling a second board descriptor.
Native feature apps MicroPython scripts
| |
App runtime Python bindings
+------------+------------+
|
Typed services
|
Drivers / K210 HAL
|
Selected board BSP
Native apps are statically linked C/C++ code; MicroPython executes scripts. There is no runtime native loader, manifest parser or provider discovery. Camera/KPU/vision are not currently exposed by MicroPython API v1.
All twelve apps use start/event/render/stop, one foreground switch and one
build-generated immutable registry. Render is optional and separate because
runtime owns Display transactions. App identity and persisted autostart IDs do
not depend on menu order. Existing bundled portable firmware services are not
therefore standalone public SDK APIs.
Time and Input have immutable board-lifetime bindings. Input retains one sampler, debounce state and event ring with independent reader cursors. Lights uses stable channel sessions; Display uses stable plane sessions and transactions; External Link owns exclusive mode, incremental operations and IRQ handoff. Native code and MicroPython use the same production implementations. The old generic broker, owner/grant/lease tables and inventory generator are removed. Build-time service selection checks board resources/routes and app requirements; absent optional services have null accessors.
Runtime prepares BASE before app start. Failed preparation and failed stop both reach cleanup. Teardown creates one absolute deadline and passes it unchanged to every scoped retirement, attempts all cleanup, retains the first error and invalidates state even after expiry. Failed safe-off quarantines only the relevant resource. Session storage must not move or be copied while claimed. Context, frame, surface, workspace and asynchronous operation generations remain where they reject stale work. Persistent settings and MicroPython sessions have separate lifetimes; MP resources retire only after worker terminal handoff. Camera frame borrows, KPU completion and core1 executor ownership remain explicit.
Cooperative deadlines are not forced preemption of arbitrary C callbacks, and fixed buffers are not memory isolation for untrusted native code. New shared abstractions should serve a present use case and replace more complexity than they add. Ordinary changes update code, consumers, tests and relevant API docs; they do not require another ADR, evidence schema or governance checker.
firmware/targets/full.c is a small composition root. It configures the runtime loop in runtime/hk_main.c, whose input polling and sleep timing remain platform-dependent. core owns app contracts, screen model, dispatch contracts, and neutral data contracts such as core/pixel_source.h; it does not access hk_input or hal_time directly.
runtime/firmware_startup.c owns startup orchestration. It initializes the platform clocks and hardware through platforms/k210/startup/platform_bootstrap.c, loads persisted settings, applies brightness and illumination/RGB settings, initializes the boot controller, shows the boot screen, and mounts storage. The neutral autostart controller then opens the persisted registry target or falls back to the menu; it never includes feature headers.
platform_bootstrap is limited to board, HAL, LCD, and hardware-driver initialization. controllers/boot_controller.c registers shell callbacks, prepares the menu view, and writes the boot banner; feature view initialization belongs to each app's start callback.
driverscontain board-independent device APIs. The selectedboards/<id>BSP owns board wiring andplatforms/k210/halowns K210 SDK access.runtimeadapts core lifecycle and startup to platform facilities.controllerscoordinate scenarios and pass state to services and UI views.servicesown runtime operations such as camera sessions, settings application, debug console I/O, LCD screenshot sourcing, and screenshot UART streaming.storageencodes and persists data.storage/screenshot_bmp.conly encodes BMP using a suppliedscreenshot_pixel_source_t; it never accesses the LCD or UART.uirenders views through the typed Display session; raw LCD transport stays below its provider.
Screenshot UART output keeps the HKSHOT BEGIN BMP24 / HKSHOT END protocol and CRC. services/screenshot_source.c supplies the LCD shadow source, storage/screenshot_bmp.c encodes bytes, and services/debug_screenshot_stream.c sends them through services/debug_console_service.c.
tools/check_arch.py guards include boundaries, forbidden compatibility headers, SDK-token placement, include cycles, and feature-module ownership.
The shared AI platform is split across the normal layers. core/ai_model_types.h
describes tensors, normalization, post-processing, and exact KModel contracts.
storage/ai_model_storage.* owns aligned FAT32 loading plus the optional
CRC-protected SD manifest. services/ai_model_runtime.* owns the single-KPU
lease, descriptor/manifest/model-identity/output validation, asynchronous run timing, and
deferred stop/unload state machine. platforms/k210/hal/hal_kpu.* remains limited to SDK and
peripheral operations. None of these shared files knows about a camera, DVP
capture policy, a feature app, or a particular post-processor.
Camera KPU consumers share one aligned 320x240 planar RGB input through
services/camera_ai_input.*. Neutral conversion hooks in camera_stream
arm the DVP AI output at frame start and freeze it at frame finish. KPU
therefore receives a complete immutable frame with the exact camera sequence,
instead of racing the next display capture. services/core1_executor.*
similarly owns the one reusable core-1 loop and accepts one bounded job at a
time; feature modules do not replace each other's core-1 entry point.
The shared settings_menu controller is an instance-based UI state machine driven by a constant item descriptor table and owner callbacks. Its passive view owns only LCD rendering. It has no camera, application, storage, persistence, or screen-navigation dependencies; owner controllers retain opening/closing lifecycle and all settings side effects. CAMERA, QR, APRILTAG, OBJECT DETECT, and the system SETTINGS app supply separate descriptor adapters. Cycle-on-OK and edit-on-OK rows share navigation, partial redraw, hold-repeat, commit lifecycle, and optional dynamic choice providers without sharing their data models.
Backlight, illumination, and RGB writes cross the public Lights capability. Its fixed K210 provider owns exclusive overlapping channel masks, validates cancellation/deadlines before register writes, and performs safe-off cleanup. Settings and temporary camera/MicroPython policy remain above that provider; only the K210 adapter includes the lights driver interface.
Display 0.1 is implemented by the production K210 provider and the deterministic fixed-capacity host fake. The provider owns BASE/OVERLAY plane sessions, bounded batch/surface staging, clipped dirty regions, borrowed-buffer lifetime, present retry, repair, and cleanup. A private UI binding holds the stable typed BASE session; MicroPython holds OVERLAY. Both reuse the one ST7789 shadow framebuffer over a raw transport that has no app, run-ID, or Python policy. The provider reports composition-specific bounded batch limits: the full profile reserves the retained MicroPython canvas, while a MicroPython-disabled composition keeps a minimal truthful batch implementation and does not reserve that absent consumer's command/text budget.
All twelve menu applications are self-contained modules: apps/terminal/,
apps/camera/, apps/qr_camera/, apps/face_detect/, apps/apriltag/,
apps/object_detect/, apps/micropython/, apps/files/, apps/buttons/,
apps/pong/, apps/settings/, and apps/sleep/. Each owns its app entry point,
controller, view, icon, feature configuration, and feature-specific
state/services. The only public header of a module is its *_app.h, and the app
implementation owns its typed lifecycle entry. Generated registry code uses typed
extern entry objects and does not include app-private headers.
The build manifest maps each app ID to its whole directory. --disable-app
therefore removes every source and private header of that feature. Shared camera
sources remain while CAMERA, QR-CAMERA, FACE DETECT, APRILTAG, or OBJECT DETECT
is enabled; quirc is staged only for QR-CAMERA. The planar AI input is staged
only for FACE/OBJECT. The shared core-1 executor is retained while APRILTAG or
MICROPYTHON needs it. With no camera consumer, sensor/DVP/camera runtime sources
are omitted while the general KPU HAL remains available.
Canonical manifests generate the immutable descriptor and menu arrays. The small app-neutral core registry dispatches primary and secondary screen ownership, menu icons and explicit debug commands. The single foreground runtime dispatches lifecycle callbacks, timer and SD events; inactive apps do not receive background ticks. Shared screen, SD, debug, boot, and system-tick controllers do not include feature headers or select features with conditionals.
CAMERA owns photo capture orchestration, encoders/writers, photo paths, settings adapter, and its view. QR-CAMERA owns quirc integration, luma conversion, result state/view, text persistence, settings, and its view. Both reuse the shared camera session, sensor/frame pipeline, camera preview renderer, and settings persistence.
FILES owns browser navigation/state, previews and deletion, all BMP/PNG/PPM/RAW/GIF decoders, and its view. Shared FAT32 provides neutral mount, directory scan, file, allocation, and stream contracts and has no dependency on FILES browser state. BUTTONS, system SETTINGS, and SLEEP likewise own their controllers and views; the shared auto-sleep controller observes input activity without polling inactive apps.
FACE DETECT owns its YOLO detector adapter, view, icon, and debug command. The feature supplies a constant model descriptor and keeps all face-specific output decoding, while generic aligned storage, KPU ownership, DVP frame handoff, output validation, timing, and deferred unload belong to shared services. Its model remains /hackylens.kmodels/detect.kmodel on the SD card and is not embedded in firmware flash.
APRILTAG owns its TAG36H11 detector adapter, grayscale downsampler, settings/selection model and descriptor adapter, stabilized in-frame result overlay, icon, and HKTAG/HKTAGINFO commands. Its descriptor adapter uses the shared camera-independent settings_menu, while APRILTAG retains persistence and camera pause/resume policy. It reuses the shared 320x240 camera runtime and publishes native tag IDs through vision_result_service as BLOCK results. Core 0 downsamples a leased camera frame only when the single uncached 160x120 luma handoff and shared core-1 executor are free, then immediately releases the frame. Frames seen while detection is busy are discarded instead of queued, preventing stale-result latency. Core-1 create/detect/destroy jobs atomically publish completed result banks and leave the executor available to other apps. refine_edges is a persisted runtime request applied before the next detection rather than a detector restart. The app uses camera session overrides for its independent FPS and LED/RGB profile, leaving CAMERA and QR settings unchanged. Its ALL/SELECTED filter is applied before the shared result snapshot, so the UART/I2C wire format remains unchanged. Preview overlays are composed into the LCD shadow before the full-frame transfer, so rectangles are not temporarily erased by the following camera frame. The detector does not use a KPU model. The BSD-licensed OpenMV AprilTag core is staged from firmware/third_party/apriltag only when this feature is enabled.
OBJECT DETECT owns the VOC20 labels, YOLOv2 decoder/NMS, settings adapter,
overlay, icon, and HKOBJECT/HKOBJECTINFO commands. Its pinned KModel v3
accepts planar 1x3x240x320 bytes and returns 1x125x7x10 floats. KPU
inference is asynchronous; completed output is decoded on core 0 before a new
DVP input is armed, avoiding K210's non-coherent cross-core cache boundary.
Newer camera frames are discarded rather than queued. Double result banks and
session epochs prevent partial or
pre-settings results from becoming visible. Boxes are composed before the
single LCD present and published as the existing transport-neutral BLOCK
format with class IDs 0..19.
Terminal owns its bounded line ring, viewport, scrolling, font geometry, and log-sink lifecycle. The shared logging service exposes only a generic optional sink and remains independent of Terminal. Font selection remains in reserved feature bits inherited from the legacy settings payload, so old records and erased flash continue to decode as TERMINAL_FONT_NORMAL.
Settings record v5 retains the v4 data layout and widens the stable autostart
ID from uint8 to uint16 without increasing the aligned 124-byte record. The 88
opaque app bytes remain unchanged: bytes 0..79 contain APRILTAG preferences
and its 587-bit selected-ID map; OBJECT DETECT owns bytes 80..87. The storage
layer validates and migrates v1, v2, v3, and v4 records. V4 autostart values are
zero-extended, so existing IDs 0..10 and all CAMERA, external-link, APRILTAG,
and OBJECT data remain unchanged.
tools/check_arch.py uses one declarative table for all twelve feature directories. It rejects legacy paths, flat app implementations, external inclusion of private feature headers, private settings-menu view access, layer inversions, include cycles, a mismatch between feature directories and the build manifest, app access to AI storage/HAL internals, and camera/feature dependencies in the shared AI platform.
SEN0305 is the physically accepted runtime port. Maix Cube is descriptor/BSP
compile conformance only: its full runtime, packaging and flashing are not
qualified. Even a future second K210 port would not establish portability
across arbitrary MCU families. Every build explicitly selects a board; its
board.toml owns wiring, defaults, flash layout and programming metadata.
Accepted firmware 8527aab is installed on COM10. Full raw image is 1,545,272 B;
static RAM (data + bss) is 2,893,216 B. The MicroPython-disabled raw image is
1,353,528 B. Full raw SHA-256 is
811be8cdc6ea18b66fa1d657e1c3850de568ae1e58c76a8408b1306e22bbf18b.
Against accepted S7 0405a09, this saves 21,120 B flash and 5,256 B static RAM.
The 227-test suite, both builds, architecture/provider evidence and resource
guard passed; CI for documentation HEAD 1f304e4 passed in run 35564255150.
The consolidated acceptance includes user-confirmed QR, Input, Lights, Display, Sleep and FILES/GIF; the user confirmed remaining UART/I2C exchange on 2026-09-21. Automated device checks cover boot, settings/camera/Pong/menu switches, four MP terminal paths and an external MP OVERLAY run surviving native SETTINGS → MENU switches. CAMERA present was observed near 31 ms; this is a smoke observation, not a matched-workload latency distribution. No repeat of unaffected accepted checks is needed for documentation/tooling cleanup.
Heavy GIFs can still play below nominal speed; button response may wait for a frame to finish. Original-firmware parity, long-run qualification, a physically qualified second board, matched Python/native migration experiments and broader MicroPython hardware APIs remain product/research work. User UART/I2C acceptance is not an instrumented throughput or electrical-characterization dataset.
A useful Python-to-native experiment separates domain state from hardware, implements equivalent behavior through these shared services, then compares correctness, code changes, latency and memory under the same fixtures. No skeleton generator or project manager is a prerequisite.
The starting revision 71ec63fffe2926642fba00ff8d28519fd93750d9 was rebuilt
with the pinned K210 toolchain for this comparison. Full raw image: 1,562,168 B;
static RAM: 2,860,328 B. The accepted final firmware is 16,896 B smaller in flash
and uses 32,888 B more static RAM. This is not an across-the-board RAM reduction.
Symbol comparison attributes the main additions to 12 × 1,024 B bounded app
state and 25,744 B of QR decoder scratch moved from stack to static storage;
removed broker/runtime tables offset part of that growth. The QR scratch change
addresses the working decoder path and stack pressure; it is not governance
metadata or a new full framebuffer. Static RAM alone does not measure peak
stack/heap usage. Older plans and exact resource records can be retrieved from
Git history; current budgets remain executable in tools/check_resources.py.
The documentation/tooling cleanup leaves the accepted firmware source unchanged.
The current host suite has 218 tests; removed cases checked retired historical
phase schemas and provenance, not the surviving service behavior. Normal-push CI
builds full SEN0305 and MicroPython-disabled images and checks their architecture
and resource limits. tools/package_release.py --board huskylens-sen0305 validates
image/sidecar/attestation identity and creates the release package locally; it
does not publish a release. Exact source and workflow status are available in Git
and CI, without another rolling qualification schema. Hardware acceptance above
carries forward only while the corresponding implementation and image are
unchanged. Matched original-versus-final latency distributions remain a research
measurement, not a result inferred from reduced source size.