Files
k-frame/docs/superpowers/specs/2026-06-18-k-frame-design.md
Gabriel Kaszewski 6ad76b98a2 add domain glossary, design spec, and ADRs
CONTEXT.md: domain model with entities (WidgetConfig, DataSource, LayoutPreset),
value objects (Layout, LayoutNode, KeyMapping, WidgetState, Sizing),
architecture decisions (CQRS, static dispatch, postcard), client domain model,
and design constraints.
2026-06-18 18:05:19 +02:00

174 lines
7.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# K-Frame — Design Spec
IoT dashboard system. Server aggregates data from configurable sources, pushes to connected display clients via FlatBuffers/TCP. Clients own all rendering.
## Hardware
- **Server**: Intel N100 homeserver
- **Client**: LOLIN Mini Kit ESP32 (ESP-WROOM-32) + 2.4" ILI9341 LCD (240×320, SPI)
## Domain Model
- **Widget** — generic display primitive. Has an ID, type hint (text, icon+text, etc.), and key-value data payload. Purely semantic, no visual info.
- **LayoutNode** — recursive tree. Either a Container (row/column with children) or a Leaf (references widget by ID).
- **Screen** — named layout tree + its widget set. The unit sent to clients.
- **DataSource** — configured external data feed. Has a type, config (URL, API key, JSON path, etc.), poll interval. Maps output to one or more widget IDs.
- **Client** — connected consumer (ESP32, future KDE Plasma widget, etc.). Receives screen updates.
## Server Architecture
Hexagonal (ports & adapters) with DDD. Same patterns as thoughts/ and movies-diary/ projects.
### Crate structure
```
crates/
domain/ — models, value objects, port traits. Zero deps.
application/ — use cases: manage config, poll data sources, push to clients
api-types/ — DTOs for web UI API
presentation/ — Axum HTTP (config web UI) + TCP listener (client connections)
bootstrap/ — composition root, wires adapters into ports, starts everything
adapters/
config-store/ — ConfigRepository impl (TOML or SQLite, behind the port)
weather/ — weather API adapter
media/ — Navidrome/Subsonic adapter
xtb/ — XTB portfolio adapter
rss/ — RSS feed adapter
http-json/ — generic HTTP + JSON path extraction adapter
webhook/ — webhook receiver (incoming HTTP → widget data)
```
A shared `protocol` crate lives at workspace root level, used by both server and client:
```
crates/
protocol/ — FlatBuffers .fbs schema + generated Rust code, no_std compatible
```
### Dependency rule
```
bootstrap ──► presentation ──► application ──► domain ◄── adapters
```
- **domain** — zero framework deps, pure business logic, defines all port traits
- **application** — orchestrates use cases, depends only on domain
- **presentation** — HTTP handlers (Axum) + TCP server, depends on domain + application
- **adapters** — implement domain ports, depend inward on domain only
- **bootstrap** — composition root, wires adapters into ports
### Domain ports
- **ConfigRepository** — CRUD for screens, widgets, data sources, layouts
- **DataSourcePort** — trait each data source adapter implements: `async fn poll(&self, config) -> WidgetData`
- **ClientBroadcaster** — push layout/data updates to connected clients
- **DataSourceRegistry** — manages polling loops per data source, detects changes, only pushes diffs
### Data flow
1. Bootstrap starts polling loops per configured DataSource (each with its own configurable interval)
2. Adapter polls external API → returns new widget data
3. Application compares with previous value → if changed, pushes DataUpdate to all clients via ClientBroadcaster
4. User changes layout in web UI → Application pushes LayoutUpdate to all clients
5. User changes config → Application restarts/adds/removes polling loops as needed
## Protocol
FlatBuffers over TCP. Length-prefixed framing: 4 bytes (u32 big-endian) message length, then FlatBuffers payload.
### Server → Client
- **ScreenUpdate** — full layout tree + all widget data. Sent on initial connection and when user changes layout.
- **DataUpdate** — one or more (widget ID + new data) pairs. Sent only when polled data actually changes. Only changed widgets included.
### Client → Server
- **Heartbeat** — keepalive so server can detect dead clients.
### Widget data format
Generic key-value map in FlatBuffers. E.g. weather widget: `{"icon": "cloud_rain", "temperature": "5.4", "unit": "°C"}`. Client interprets keys however it wants. Server doesn't prescribe rendering.
### Bandwidth
- Only changed widgets sent (diff against previous state)
- FlatBuffers is zero-copy and compact
- Layout tree not resent unless it actually changes
- Heartbeat is minimal
### Extensibility
Protocol should be designed so auth handshake can be added later without breaking existing message types.
## Client (ESP32 Firmware)
Rust firmware using `esp-idf-hal` / `esp-idf-svc`.
### Boot sequence
1. Show K logo splash (baked into firmware as RGB565 bitmap)
2. Connect WiFi (credentials in firmware config / NVS)
3. Connect TCP to server
4. Receive ScreenUpdate → render layout
5. Listen for DataUpdates, re-render changed widgets only
### Rendering
- `embedded-graphics` for drawing primitives (text, rectangles, bitmaps)
- Custom layout engine: walks LayoutNode tree, computes bounding boxes, assigns positions
- Container nodes split available space among children (row = horizontal, column = vertical)
- Leaf nodes render their widget using generic key-value data
- Client decides: fonts, sizes, overflow behavior, marquee, truncation, padding
- Small icon set baked into firmware as bitmaps
### Composable layouts
User composes layout tree freely. If it doesn't fit or renders poorly, user's responsibility to fix. Server may provide hints/warnings but does not enforce.
### Marquee
If text overflows its computed bounding box, client auto-scrolls horizontally. Smooth pixel-by-pixel animation.
### Offline behavior
- Keep last received data in memory, keep rendering
- Show small indicator (dot/icon in corner) when TCP connection lost
- Reconnect loop with backoff
### Display-agnostic server
Server sends semantic data only. All rendering decisions (overflow, marquee, truncation, font, color, layout computation) are client-side. Future clients (KDE Plasma widget, web dashboard, etc.) interpret the same protocol differently.
## Web UI (Config)
Served by same binary via Axum. Simple HTML (static with forms or lightweight SPA).
### Features
- **Manage data sources** — add/edit/remove. Configure type, URL, API keys, poll interval, JSON path.
- **Manage widgets** — create widgets, assign to data source, define key mappings.
- **Compose layouts** — build layout tree: add containers (row/column), nest them, place widgets as leaves.
- **Push changes** — saving config triggers immediate LayoutUpdate to all connected clients.
- **View connected clients** — see which clients are online (nice-to-have).
No auth for now. Presentation crate can add middleware later.
## Initial Data Source Adapters
| Adapter | Direction | Description |
|------------|-----------|--------------------------------------------------|
| weather | poll | OpenWeather or similar, configurable interval |
| media | poll | Navidrome/Subsonic API (now playing, etc.) |
| xtb | poll/ws | XTB portfolio value |
| rss | poll | RSS feed, configurable interval |
| http-json | poll | Generic: poll any URL, extract via JSON path |
| webhook | push | Server exposes endpoint, external services POST |
## Multi-client Support
Multiple clients (ESP32 devices, future Plasma widgets) connect to same server. All receive same layout tree and data updates via broadcast.
## Config Storage
Abstracted behind ConfigRepository port. Initial adapter can be TOML file or SQLite — implementation detail hidden from domain. Web UI reads/writes through the same port.