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.
This commit is contained in:
2026-06-18 18:05:19 +02:00
commit 6ad76b98a2
6 changed files with 318 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
# 0001 — Event-driven CQRS
**Status:** accepted
**Date:** 2026-06-18
## Context
K-Frame has a natural write/read split: the write model is config (widgets, data sources, layout) mutated via web UI, the read model is runtime state (current widget data, active layout) pushed to display clients. We need a mechanism to bridge the two — when config changes, runtime behavior must update (restart polling loops, push new layout to clients).
## Decision
Full event-driven CQRS. Commands mutate config and emit domain events. The read side projects current state from events and DataSource poll results. Events drive side effects: restarting poll loops, pushing updates to connected clients.
## Alternatives considered
**Imperative approach** — commands mutate config, then application layer imperatively calls "restart poll loop" / "push to clients." Simpler, but tangles command handling with side-effect orchestration. Adding new reactions to config changes requires modifying command handlers.
## Consequences
- Clean separation: command handlers only mutate config and emit events, never trigger side effects directly.
- New reactions to config changes are just new event listeners — no command handler modification.
- More moving parts than the imperative approach — event bus, event handlers, projections.
- Events are in-memory only (no event store / event sourcing). This is CQRS, not ES.

View File

@@ -0,0 +1,23 @@
# 0002 — Static dispatch over trait objects
**Status:** accepted
**Date:** 2026-06-18
## Context
Hexagonal architecture uses port traits extensively. Rust offers two ways to consume them: trait objects (`Box<dyn Port>`, dynamic dispatch) or generics (`T: Port`, static dispatch / monomorphization).
## Decision
Use generics for all port traits. No `Box<dyn>` anywhere. Composition roots (bootstrap on server, firmware crate on client) are the only places that know concrete types. Generic parameters propagate through application and presentation layers.
## Alternatives considered
**Trait objects** — simpler type signatures, less generic noise. But adds vtable indirection, heap allocation per trait object, and prevents compiler optimizations. On ESP32 with limited RAM, every allocation matters.
## Consequences
- Zero-cost abstractions — port calls are monomorphized, inlined where possible.
- More verbose type signatures — structs and functions carry generic parameters.
- Compile times may increase due to monomorphization.
- Consistent approach across server and client codebases.

View File

@@ -0,0 +1,26 @@
# 0003 — postcard over FlatBuffers
**Status:** accepted
**Date:** 2026-06-18
## Context
Need a binary serialization format for the TCP protocol between server and ESP32 clients. Must be compact (bandwidth-constrained), ideally `no_std` compatible for future bare-metal targets.
## Decision
Use `postcard` with `serde` derives instead of FlatBuffers.
## Alternatives considered
**FlatBuffers** — zero-copy reads, schema-driven. But requires external codegen toolchain (flatc), `no_std` support is limited, and zero-copy is overkill for our small messages (a handful of widgets).
**Custom binary protocol** — maximum control, zero deps. But significant implementation effort for serialize/deserialize code, error-prone, and postcard already solves this.
## Consequences
- `no_std` protocol crate out of the box — future-proof for bare-metal targets.
- No codegen step — types are normal Rust structs with serde derives.
- Compact wire format via varint encoding.
- No zero-copy reads — messages are deserialized into owned types. Acceptable for our message sizes.
- Adds `serde` and `postcard` as dependencies to the protocol crate.

View File

@@ -0,0 +1,173 @@
# 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.