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:
68
CONTEXT.md
Normal file
68
CONTEXT.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# K-Frame — Domain Glossary
|
||||
|
||||
> Canonical language of the K-Frame domain. No implementation details — just terms and their meanings.
|
||||
|
||||
## Entities
|
||||
|
||||
Entities have identity and a lifecycle. They are created, mutated, and deleted.
|
||||
|
||||
- **WidgetConfig** — the persistent facet of a Widget. Has an ID (`u16`), a DisplayHint, a DataSource binding, and a set of KeyMappings. Write model — mutated via web UI commands.
|
||||
|
||||
- **DataSource** — a configured external data feed. Has an ID, type (weather, media, rss, etc.), connection config, and poll interval. Polls once, produces a raw response. Multiple Widgets can extract data from the same DataSource — the mapping lives in each WidgetConfig, not in the DataSource itself.
|
||||
|
||||
- **LayoutPreset** — a named, saved layout tree stored in config. Has an ID and a name. Created, listed, renamed, deleted from the web UI. Loading a preset replaces the active Layout.
|
||||
|
||||
## Value Objects
|
||||
|
||||
Value objects have no identity. They are defined entirely by their content and are immutable.
|
||||
|
||||
- **Layout** — the single active layout tree. A recursive structure of LayoutNodes. Always singular — clients display exactly one layout at a time. Replaced wholesale when user reconfigures via the web UI.
|
||||
|
||||
- **LayoutNode** — a node in the layout tree. Either a Container (row or column with ordered children) or a Leaf (references a Widget by ID). Each child in a Container has a sizing mode: Fixed(pixels) or Flex(weight). Layout engine sums fixed sizes, distributes remaining space among flex children by integer weight ratio. All integer math, no floats. Containers have an optional `gap: u8` (uniform spacing between children in pixels) and an optional `padding: u8` (uniform inset on all sides, typically used on root container to keep content off screen edges).
|
||||
|
||||
- **KeyMapping** — a rule inside WidgetConfig that extracts a value from a DataSource's raw response using a JSON path expression and maps it to a named key in the WidgetState. E.g. `"$.main.temp" → "value"`. Decouples widget data shape from API response shape. Keeps adapters dumb — they return raw responses, WidgetConfig defines extraction.
|
||||
|
||||
- **WidgetState** — the transient read-model facet of a Widget. A `BTreeMap<String, Value>` — values can be nested (arrays, objects). Projected from DataSource poll results via KeyMappings. Held in memory, never persisted. Client handles nested data for things like RSS headline lists.
|
||||
|
||||
- **Sizing** — how a child occupies space within a Container. Either `Fixed(u16)` (exact pixels) or `Flex(u8)` (weight-based proportional share of remaining space).
|
||||
|
||||
## Architecture
|
||||
|
||||
- **CQRS** — full event-driven command/query separation. Commands mutate config (write model) and emit domain events. Read model (WidgetStates, active Layout) is projected from events and DataSource poll results, held in memory. Domain events drive side effects: restarting poll loops, pushing updates to clients.
|
||||
|
||||
- **Heartbeat** — bidirectional keepalive. Client sends heartbeats so server can detect silent disconnects (e.g. ESP32 powered off). Server sends heartbeats so client can detect server death and show offline indicator. Each side sets a timeout — no heartbeat within N seconds → peer considered dead. Individual client adapters can disable server→client heartbeats if too resource-heavy.
|
||||
|
||||
- **Domain Event** — a fact emitted by the write model when config changes. E.g. WidgetCreated, WidgetUpdated, DataSourceAdded, DataSourceRemoved, LayoutChanged, LayoutPresetLoaded. Events are the bridge between write and read sides.
|
||||
|
||||
## Client Domain
|
||||
|
||||
The client has its own thin domain — hexagonal, chip-agnostic, display-agnostic. No persistence, no CQRS, no events. Pure layout computation and rendering rules.
|
||||
|
||||
- **RenderTree** — the client's "virtual display." A computed tree of positioned, sized nodes (BoundingBoxes + WidgetViews), held in memory. When a DataUpdate arrives, only the affected WidgetView is diffed and repainted within its BoundingBox — no layout recomputation, no full screen redraw. Full recomputation only happens on LayoutUpdate. Inspired by React's virtual DOM: diff against previous state, apply minimal changes to the actual display.
|
||||
|
||||
- **BoundingBox** — a rectangular region on screen (x, y, width, height) computed by the layout engine for each node.
|
||||
|
||||
- **WidgetView** — the rendered projection of a WidgetState. Contains resolved display data (text strings, icon references) within a BoundingBox. The client domain decides overflow behavior, truncation, and marquee.
|
||||
|
||||
- **Layout Engine** — pure domain logic. Given a LayoutNode tree and screen dimensions, computes BoundingBoxes for all nodes. Containers split space among children (row = horizontal, column = vertical). Leaves get their final bounds.
|
||||
|
||||
## Shared Concepts
|
||||
|
||||
- **DisplayHint** — a domain enum on WidgetConfig indicating how the client should render the widget. Closed set: IconValue, TextBlock, KeyValue, etc. Client handles each variant explicitly, with a fallback to plain text for unknown/unsupported hints. Gives type safety and validation on both sides while remaining forward-compatible. Lives in the protocol crate.
|
||||
|
||||
- **Value** — domain-owned value type representing structured data. An enum: String, Number, Bool, Array, Object, Null. Adapters convert their native formats (JSON, XML, etc.) into Value. KeyMapping and WidgetState operate on Value exclusively. Zero coupling to serialization libraries. Lives in the protocol crate.
|
||||
|
||||
- **WidgetError** — an optional error indicator on WidgetState. When a DataSource poll or KeyMapping extraction fails, the last known good data is preserved but an error flag is attached. Client decides how to surface it — stale data silently, warning badge, or error state. Server never suppresses information.
|
||||
|
||||
## Design Constraints
|
||||
|
||||
- **Static dispatch everywhere.** Port traits are used as generic bounds, not trait objects. No `Box<dyn>`, no dynamic dispatch. Composition root resolves all concrete types. Applies to both server and client. Verbose but zero-cost — critical on ESP32, consistent on server.
|
||||
|
||||
- **Bounded data sizes.** Three tiers of protection against memory exhaustion:
|
||||
1. *Per-widget soft limit* — WidgetConfig has a `max_data_size: u16` (bytes, default 2048). Server truncates at extraction time. Configurable per widget for text-heavy use cases (chyron, RSS).
|
||||
2. *Frame-level hard limit* — transport layer rejects frames exceeding a fixed cap (e.g. 32KB). Safety net for the wire.
|
||||
3. *Client memory budget* — fixed total memory pool for all WidgetStates combined. Compile-time constant in firmware. If accepting a DataUpdate would exceed budget, client drops oldest/lowest-priority data.
|
||||
|
||||
- **Widget IDs are `u16`.** Compact on the wire (2 bytes), fast comparison. WidgetConfig can have a human-readable name for the web UI, but the protocol and client use the integer ID exclusively.
|
||||
|
||||
- **`postcard` + `serde` for wire format.** Protocol crate uses postcard for binary serialization — compact varint encoding, `no_std` compatible, no codegen toolchain. Types are normal Rust structs with `#[derive(Serialize, Deserialize)]`. Replaces FlatBuffers. Protocol crate is `no_std` for maximum portability.
|
||||
Reference in New Issue
Block a user