7.4 KiB
K-Frame
IoT dashboard system. A server aggregates data from external sources (weather APIs, media players, RSS feeds, webhooks) and pushes rendered updates to ESP32 display clients over TCP.
Server polls data sources, projects values through configurable widget mappings, computes layouts, and broadcasts binary frames to connected clients. Configuration via REST API + web UI.
Clients receive structured data + layout trees, compute bounding boxes, format text per display hint rules, and render to hardware. The client domain is chip-agnostic — the same rendering engine runs on ESP32 (ILI9341 TFT) and desktop (terminal).
Web UI (SPA) configures data sources, widgets, layouts, themes, and presets. Includes a live layout preview that mirrors what the physical display shows.
Architecture
Hexagonal / ports-and-adapters with full CQRS. Domain logic has zero framework dependencies. Static dispatch everywhere (no Box<dyn>, no trait objects).
┌─────────────────── Server ───────────────────┐
│ domain/ entities, value objects, │
│ ports (traits) │
│ application/ use cases, data projection │
│ bootstrap/ composition root, polling │
│ │
│ adapters/ │
│ config-sqlite persistence │
│ http-api REST API (Axum) │
│ tcp-server binary protocol broadcast │
│ http-json external API polling │
│ media, rss source-specific adapters │
│ data-generators clock, static text │
│ auth argon2 + JWT │
├─────────────────── Shared ───────────────────┤
│ protocol/ wire types, postcard serde │
│ api-types/ REST DTOs │
├─────────────────── Client ───────────────────┤
│ client-domain/ layout engine, render engine │
│ markup parser, scroll state │
│ client-application/ message handling │
│ client-esp32/ ESP32 firmware (ILI9341) │
│ client-desktop/ terminal debug client │
├──────────────────── SPA ─────────────────────┤
│ spa/ React + TanStack Router │
│ shadcn/ui + Tailwind │
└──────────────────────────────────────────────┘
Key design decisions
- postcard + serde for the wire protocol — compact varint encoding,
no_stdcompatible, no codegen - Static dispatch — port traits used as generic bounds, not trait objects. Composition root resolves all types
- Event-driven CQRS — config mutations emit domain events, read model projected in memory
- Domain-owned rendering — client-domain owns text wrapping, alignment, color markup, scroll. DisplayPort is a thin pixel-pusher (
draw_text_span,fill_rect,flush) - WiFi provisioning — ESP32 boots into AP captive portal if no config in NVS, auto-falls back on WiFi failure
See docs/adr/ for architectural decision records and CONTEXT.md for the domain glossary.
Features
- Data sources: HTTP/JSON, weather, media (Subsonic/Navidrome), RSS, webhooks, clock, static text
- Layout engine: flexbox-like containers (row/column, fixed/flex sizing, gap, padding, justify-content, align-items)
- Theming: 5 configurable colors (primary, secondary, accent, text, background), live push to clients
- Rich text: inline color markup (
{primary}text{/},{#FF0000}hex{/}) - Widget alignment: per-widget horizontal/vertical text alignment (left/center/right, top/middle/bottom), reflected in layout preview
- Connection indicator: green/red dot on ESP32 display showing server connectivity
- Overflow scroll: bounce animation when content exceeds widget bounds, speed auto-derived from overflow
- Captive portal: ESP32 AP mode with DNS + HTTP config form for WiFi provisioning
- Auth: argon2 password hashing, JWT tokens, protected API routes
Prerequisites
- Rust (stable) — server, desktop client, shared crates
- Rust ESP32 toolchain —
espup, xtensa target. See esp-rs book - Node.js / Bun — SPA development
- SQLite — server persistence (created automatically)
Quick start
# 1. Clone and configure
cp .env.example .env
# Edit .env — set JWT_SECRET and KFRAME_ENCRYPTION_KEY
# 2. Start the server
make server
# 3. Start the SPA dev server (separate terminal)
cd spa && bun install && bun run dev
# Open http://localhost:5173
# 4. Register an account (first launch = setup mode)
# 5. Add a data source, create widgets, build a layout
ESP32 wiring
2.4" ILI9341 SPI LCD module → ESP-WROOM-32:
| LCD Pin | ESP32 GPIO | Function |
|---|---|---|
| VCC | 3V3 | Power |
| GND | GND | Ground |
| DIN | GPIO23 | SPI MOSI |
| CLK | GPIO18 | SPI SCLK |
| CS | GPIO26 | Chip select |
| DC | GPIO21 | Data/command |
| RST | GPIO22 | Reset |
| BL | 3V3 | Backlight (always on) |
Uses SPI2 (HSPI) at 26 MHz. Pin assignments are in crates/client-esp32/src/main.rs.
ESP32 client
# Build firmware
make esp-build
# Flash (default port: /dev/ttyACM0)
make esp-flash
# Flash with custom port
make esp-flash ESP_PORT=/dev/ttyUSB0
# Flash and monitor serial output
make esp-run
# On first boot: connect to "KFrame-Setup" WiFi, enter credentials in captive portal
Desktop client
make desktop
# Connects to localhost:2699, prints render commands to terminal
Development
# Full check suite (fmt + clippy + test)
make check
# Auto-fix formatting and clippy warnings
make fix
# Run tests only
make test
# SPA type checking
cd spa && bun run typecheck
Project structure
| Path | Description |
|---|---|
crates/domain/ |
Entities, value objects, port traits |
crates/application/ |
Use cases (ConfigService, DataProjection) |
crates/protocol/ |
Wire types, encode/decode (no_std) |
crates/bootstrap/ |
Server composition root |
crates/adapters/ |
All port implementations |
crates/client-domain/ |
Display-agnostic rendering engine |
crates/client-application/ |
Client message handling |
crates/client-esp32/ |
ESP32 firmware |
crates/client-desktop/ |
Terminal debug client |
crates/api-types/ |
REST API DTOs |
spa/ |
React SPA |
docs/adr/ |
Architecture decision records |
CONTEXT.md |
Domain glossary |
Contributing
- Fork the repo
- Create a feature branch
- Run
make checkbefore pushing — CI runs the same checks - Open a PR with a clear description of what changed and why
The domain glossary in CONTEXT.md defines the canonical language. Use it in code, commits, and PRs. If you're adding a new concept, update the glossary.
Architecture decisions are documented in docs/adr/. If your change involves a hard-to-reverse design choice, write an ADR.