# 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`, 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_std` compatible, 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](https://docs.esp-rs.org/book/) - **Node.js / Bun** — SPA development - **SQLite** — server persistence (created automatically) ## Quick start ```bash # 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 ```bash # 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 ```bash make desktop # Connects to localhost:2699, prints render commands to terminal ``` ## Development ```bash # 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 1. Fork the repo 2. Create a feature branch 3. Run `make check` before pushing — CI runs the same checks 4. 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. ## License [MIT](LICENSE)