Files
k-frame/CONTEXT.md

9.9 KiB
Raw Permalink Blame History

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), an optional padding: u8 (uniform inset on all sides), a JustifyContent (main axis distribution: Start, Center, End, SpaceBetween, SpaceEvenly), and an AlignItems (cross axis alignment: Start, Center, End, Stretch). Both default to Start.

  • 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 spans with colors), scroll state, and computed positions within a BoundingBox. The client domain formats raw WidgetState data according to DisplayHint rules (e.g. KeyValue renders keys in secondary color), applies text alignment (HAlign, VAlign), detects overflow, and manages bounce-scroll animation. Scroll triggers only on overflow — static text stays static. Bounce speed auto-derives from overflow amount for consistent reading pace.

  • 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), applying JustifyContent and AlignItems for positioning. Leaves get their final bounds.

  • Render Engine — pure domain logic. Given a WidgetView and FontMetrics, parses inline color markup, performs word-based line wrapping (character fallback for long words), computes alignment offsets, applies scroll position, and emits positioned text spans for the DisplayPort to draw. Owns all text intelligence — the DisplayPort is a thin pixel-pusher.

  • FontMetrics — character dimensions for each font size (Small, Large), injected into the domain at init by the adapter. Enables pure-math text measurement (monospace: width = char_count × char_width) without hardware coupling.

  • Color — domain-owned RGB color value (u8, u8, u8). Used on all domain and port boundaries. Adapters convert to native format (e.g. Rgb565 on ESP32) at the last mile.

  • ThemeConfig — a set of five named Colors that define the visual appearance of the client: primary, secondary, accent, text (default for all unmarked content), and background (used for all fills/clears). Pushed from server to client independently of layout via a ThemeUpdate message. Client stores current theme and falls back to sensible defaults if no theme has been received. Configured from the web UI.

  • DisplayPort — the client's rendering abstraction. A thin pixel-pusher with three methods: draw_text_span(text, x, y, color, font_size), fill_rect(bounds, color), flush(). All text intelligence (wrapping, alignment, scrolling, markup parsing) lives in the domain's render engine, not in the port. Adapters convert domain Color to native format and delegate to hardware.

  • FontSize — enum selecting which bitmap font to use: Small (body text) or Large (icons). Passed with each draw_text_span call. The domain picks the font; the adapter maps it to a concrete bitmap font.

Shared Concepts

  • DisplayHint — a domain enum on WidgetConfig indicating how the client should render the widget. Closed set: IconValue, TextBlock, KeyValue, etc. Each variant is a rendering recipe — the client-domain's render engine formats raw WidgetState data into styled text spans according to the variant's rules (e.g. KeyValue renders keys in secondary color, values in text color). Carries content-level alignment: HAlign (Left, Center, Right) and VAlign (Top, Middle, Bottom). Client handles each variant explicitly with exhaustive match; fallback to plain text for unknown hints. Lives in the protocol crate.

  • Inline Color Markup — lightweight syntax embedded in Value strings for coloring text spans. Syntax: {#RRGGBB}text{/} for hex colors, {primary}text{/}, {secondary}text{/}, {accent}text{/} for theme colors. {/} resets to the theme's text color. Parsed by the client-domain render engine. No bold/italic — hardware constraint on current bitmap fonts.

  • 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.