Mastodon Politics, Power, and Science

Tuesday, March 17, 2026

The “BluePrint” Platform: A Declarative UI & Data-Sync Framework for PicoW

J. Rogers, SE Ohio

The code is here that this is going to modify:
https://github.com/BuckRogers1965/Pico-IoT-Replacement/tree/main/WeatherStation


Abstract

This document formalizes the architecture for a local-first, data-driven IoT framework. By decoupling the “Model” (Registry) from the “View” (Layout Definition), the framework eliminates the traditional boilerplate associated with embedded web development. The architecture utilizes a boot-time index resolution phase to convert declarative UI maps into O(1) memory pointers, enabling a recursive, chunked streaming engine to generate dynamic, multi-page web interfaces on-the-fly. This approach ensures that the application developer only needs to define the project’s data points and UI hierarchy in a single configuration table, while the framework automatically handles thread-safe data synchronization, multi-page routing, and client-side DOM auto-wiring. The resulting system is a self-documenting, modular, and cloud-free platform that remains highly performant on resource-constrained microcontrollers.


Section 1: Theory & System Architecture

The transition from a hardcoded web interface to a data-driven UI engine is built on the principle of View-Model Decoupling. By treating the user interface as a projection of the existing Registry data rather than a static block of HTML, we eliminate boilerplate, reduce binary size, and enable infinite UI flexibility without modifying sensor logic.

1.1 The Model: The “Source of Truth” (Registry)

The Registry is the system’s Model. It serves as the thread-safe, core-agnostic data bus. It remains the absolute source of truth for the device’s state. * Decoupled State: The Registry is completely unaware of how its data is presented. It does not contain HTML, CSS, or layout information. * Immutable Data Flow: Whether a sensor is displayed as a simple text label on a “Status” page or a complex SVG gauge on a “Dashboard” page, the underlying data remains the same. The Registry simply exposes the value; it does not care who is looking at it.

1.2 The View-Definition: The “Blueprint” (Layout Table)

The Layout Table is a static, read-only map (stored in Flash/Core 0) that defines the structure and presentation of the UI. * Declarative UI: Instead of writing code to build the UI, the developer declares the UI as a tree structure. Each node in this table maps a Registry ID to a specific visual container or widget type. * Zero-State Footprint: This table holds no runtime data. It is purely an index that tells the web server “how” to draw the system state. By defining this in a flat table, we gain the ability to re-map the same Registry ID to multiple pages or multiple widgets simultaneously, enabling complex UIs from simple data definitions.

1.3 The Renderer: The “Dynamic Engine” (Recursive Streamer)

The Renderer is the glue between the Model and the View-Definition. It does not “build” pages; it “walks” the Layout Table. * Recursive Traversal: The engine treats the layout as a tree. When a client requests a page, the engine traverses the Layout Table, identifies the requested container, and streams the necessary components. * Lazy Streaming: Rather than constructing large HTML strings in RAM—which causes memory fragmentation—the engine streams content in small, discrete chunks directly to the web server’s buffer. * Implicit Synchronization: By assigning Registry IDs as DOM IDs during streaming, the renderer creates an implicit bridge to the existing JavaScript sync loop. The client-side code automatically binds to the new layout because the ID-to-Data contract remains identical to the old system.

1.4 Why this Theory is Robust

  • Separation of Concerns: Core 1 (Sensor Logic) remains isolated. It is not impacted by UI complexity. Core 0 (Web Server) acts as the “Presenter.”
  • Deterministic Failure: Because the Layout Table is resolved at boot, the system validates the UI configuration before it ever goes live. If a Registry ID is missing or misspelled, the error is caught at startup, not during a user’s web request.
  • Platform Extensibility: This architecture transforms the framework into a platform. Adding new UI functionality (like a new widget type or a system-wide documentation page) only requires updating the Renderer logic, not the individual application code.

This theoretical framework ensures that the UI generation is a pure function of the Layout Table and the current Registry state, resulting in a system that is predictable, memory-efficient, and entirely modular.


Section 2: The View Definition (The Layout Table)

To transition from hardcoded UI to a declarative model, the UI must be defined as a static, read-only configuration table. This table acts as a Map that links Registry data to visual presentation. By defining this in a flat array, we ensure the UI tree is defined once, stored in Flash, and processed identically by the rendering engine.

2.1 The Data Structure

The LayoutNode structure defines the hierarchy. Each node represents either a container (a page or section) or a widget (a visual representation of a registry item).

enum ContainerType { 
C_ROOT, // The root of the website
    C_PAGE, // A web page
    C_CARD, // A box on a web site
    C_TAB, // A Tabbed container that can hold boxes
C_DIV, // A Div container that is generic
C_RADIO, // A contaner that adds radio behavior
// to buttons inside it.
C_COLLAPSABLE, // holds a set of othrr containers
// that only has one open at a time
W_TEXT, // Simple value display W_LED, // Binary indicator W_BAR, // Progress/Level bar W_DIAL, // Gauge representation W_BUTTON // Control trigger
};
struct LayoutNode { const char id[20]; // Unique UI identifier for this node const char parent_id[20]; // ID of the parent node (empty for root) const char registry_id[20]; // ID of the linked Registry item (if applicable) WidgetType widget; // The rendering type };
We need to be able to add properrties like min, max, default, to control the behavior of the controls.   You cannot have a dial or a bar unless you know what it is scaling against. 

2.2 The Layout Table Definition

The developer defines the site hierarchy by populating this array. The “flat” nature of this table allows for complex tree structures to be defined using simple parent-child relationships.

Implementation Example:

const LayoutNode layout_table[] PROGMEM = {
    // Structural Roots
    {"root",            "",             "",              C_ROOT},
{"status_page", "root", "", C_PAGE}, {"irrigation_page", "root", "", C_PAGE},

{"status_card", "status_page", "", C_CARD},
{"irrigation_card", "irrigation_page", "", C_CARD},
// Mapping Registry items to UI widgets // ID Parent Registry ID Widget {"indoor_temp", "status_card", "temp_a", W_TEXT}, {"cpu_dial", "status_card", "cpu_temp_f", W_DIAL},
{"moisture_bar", "irrigation_card", "soil_moisture", W_BAR},
{"water_btn", "irrigation_card", "water_now", W_BUTTON} }; #define LAYOUT_COUNT (sizeof(layout_table) / sizeof(LayoutNode))

2.2.1 Adding properties

struct LayoutNode { const char id[20]; const char parent_id[20]; const char registry_id[20]; WidgetType widget; const char* props; // e.g., "min:0,max:100,on_color:#00FF00,off_color:#FF0000,threshold:50" }; const LayoutNode layout_table[] PROGMEM = { // root and pages {"root", "", "", C_ROOT, ""}, {"status_page", "root", "", C_PAGE, ""}, {"status_card", "status_page", "", C_CARD, ""}, // a dial with min/max/step {"cpu_dial", "status_card", "cpu_temp_f", W_DIAL, "min:0,max:120,step:1"}, // a bar with min/max {"moisture_bar", "status_card", "soil_moisture", W_BAR, "min:0,max:100"}, // an LED with color threshold {"led_alert", "status_card", "water_level", W_LED, "on_color:#00FF00,off_color:#FF0000,threshold:50.0"}, // a button with momentary flag {"water_btn", "status_card", "water_now", W_BUTTON, "momentary:true"}, // a text widget with format specifier (future) {"temp_text", "status_card", "temp_a", W_TEXT, "format:%.1f"} };
How It Works at Boot During setupLayoutResolution(), the framework iterates through layout_table. For each node, it copies the static data to a runtime ResolvedNode (in RAM) and parses the props string into a parallel ResolvedProps array (also in RAM). The parser splits the string by commas, then by colons, and fills a struct like:
cpp
struct ResolvedProps {
    bool has_min, has_max, has_step;
    float min, max, step;
    bool has_on_color, has_off_color, has_threshold;
    uint32_t on_color, off_color;
    float threshold;
    bool momentary;
    // ... other keys
};
Properties that are not specified remain at default values (e.g., has_min = false). The renderer checks these flags before applying the property. 

Benefits of This Approach - 
**Simple syntax** – all properties are visible inline in the layout table. - 
**Flexible** – you can add new property keys without changing the LayoutNode structure. - 
**Flash‑efficient** – property strings live in PROGMEM; only the resolved values consume RAM. - 
**Consistent with the platform’s design** – boot‑time resolution handles both registry index mapping and property parsing in one pass. This method gives you a clean, declarative way to attach configuration to any widget, while keeping the core framework lightweight and maintainable.

2.3 Rationale & Design Specifics

  • Flash Memory Allocation (PROGMEM): By marking the table with PROGMEM (or placing it in the .rodata section), the layout definition resides in Flash memory. This prevents the UI definition from consuming precious SRAM, which is reserved for the Registry and the TCP stack.
  • Flat Hierarchy (parent_id linking): Instead of using nested structs (which are difficult to traverse), the tree is defined by linking nodes via string IDs. This is memory-efficient and allows the renderer to walk the tree using simple strcmp() lookups.
  • Registry Decoupling: The registry_id field is a loose string reference. It does not contain an actual pointer or index. This is critical because it allows the LayoutTable to be compiled independently of the Registry state. The actual binding to the registry index occurs in the Boot-Time Resolution Phase (Section 3).
  • Widget Portability: The WidgetType enum acts as a contract for the Renderer. The renderer is programmed to know how to draw a WIDGET_BAR, but the LayoutNode doesn’t need to know how the bar works. This keeps the layout definition clean and focused solely on structure and identity.

2.4 Scalability

To display the same Registry value in two different formats (e.g., a simple text value on the dashboard and a bar graph on the details page), the developer simply adds two nodes to the layout_table pointing to the same registry_id. The renderer handles these as two distinct visual nodes, drawing from the same underlying registry index.


Section 3: Boot-Time Index Resolution (The “Compiler” Phase)

To optimize runtime performance, the system must resolve the string-based registry_id mapping into memory-efficient integer indices. This process occurs once during setup(), converting the static LayoutNode definition into a runtime-ready ResolvedNode structure.

3.1 The Resolved Structure

The ResolvedNode structure is stored in RAM. It mirrors the LayoutNode but replaces the char registry_id[20] (string) with a uint8_t registry_idx (integer index).

struct ResolvedNode {
    char id[20];
    char parent_id[20];
    uint8_t registry_idx; // Numeric index, resolved once at boot
    WidgetType widget;
    bool is_container;    // Derived from widget type for faster traversal
};

// Global heap-allocated lookup table
ResolvedNode resolved_table[LAYOUT_COUNT];

3.2 The Resolution Routine

The resolution routine serves as the “Compiler” for the UI. It iterates through the Flash-resident layout_table and maps IDs to Registry indices using registry.nameToIdx().

Implementation Logic:

void setupLayoutResolution() {
    Serial.println(">> [UI] Boot Resolution: Mapping Layout to Registry...");

    for (int i = 0; i < LAYOUT_COUNT; i++) {
        // 1. Copy static metadata to RAM
        strncpy(resolved_table[i].id, layout_table[i].id, 20);
        strncpy(resolved_table[i].parent_id, layout_table[i].parent_id, 20);
        resolved_table[i].widget = layout_table[i].widget;
        resolved_table[i].is_container = (layout_table[i].widget == WIDGET_CONTAINER);

        // 2. Resolve Registry String ID to Integer Index
        if (strlen(layout_table[i].registry_id) > 0) {
            uint8_t idx = registry.nameToIdx(layout_table[i].registry_id);
            
            if (idx == 255) {
                // 3. Error Handling: Catch misconfiguration at boot
                Serial.printf(">> [UI ERROR] Layout node '%s' maps to invalid Registry ID '%s'\n", 
                               layout_table[i].id, layout_table[i].registry_id);
                resolved_table[i].registry_idx = 255; // Flag for safety
            } else {
                resolved_table[i].registry_idx = idx;
                Serial.printf(">> [UI OK] '%s' -> Registry[%d]\n", layout_table[i].id, idx);
            }
        }
    }
}

3.3 Design Rationale & Theory

  • Computational Efficiency: Performing registry.nameToIdx() involves an O(N) string comparison. By resolving this at boot, the web server’s runtime renderContainer function (which is called every time a user refreshes the page) avoids these string operations entirely, performing simple array indexing (O(1)) instead.
  • Deterministic Validation: The system verifies the UI-to-Data contract during startup. If a developer renames a Registry item but forgets to update the Layout map, the serial port will immediately report a mapping failure. This provides a “fail-fast” mechanism that prevents the web server from serving a broken or corrupted interface.
  • Memory Isolation: The resolved_table exists only in Core 0’s memory space. Core 1, which performs the heavy sensor polling, remains completely unaware of the layout resolution, ensuring no performance degradation in the data-acquisition loop.
  • Safety Flagging: By setting registry_idx to 255 on failure, the renderer can identify broken nodes at runtime and choose to either hide them or render a placeholder, ensuring the framework remains robust even when faced with invalid UI configuration.

Section 4: Recursive Streaming Engine (The Renderer)

The Renderer is the final execution stage. It transforms the static ResolvedTable into a live HTML stream. By utilizing recursion and chunked streaming, the framework generates the UI in a memory-efficient manner without ever constructing a full-page string in RAM.

4.1 The Recursive Walker

The engine performs a depth-first traversal of the resolved_table. It treats the layout as a parent-child tree. When the renderer encounters a WIDGET_CONTAINER, it triggers a recursive call, nesting the children within the parent’s HTML structure.

Implementation Logic:

void renderContainer(const char* parent_id) {
    for (int i = 0; i < LAYOUT_COUNT; i++) {
        // Find nodes belonging to the current parent
        if (strcmp(resolved_table[i].parent_id, parent_id) == 0) {
            
            if (resolved_table[i].is_container) {
                // Open container and recurse
                server.sendContent("<div id='" + String(resolved_table[i].id) + "' class='container'>");
                renderContainer(resolved_table[i].id); 
                server.sendContent("</div>");
            } else {
                // Fetch data using the resolved index
                uint8_t idx = resolved_table[i].registry_idx;
                if (idx != 255) {
                    float val = registry.get_id(idx);
                    const char* name = registry.getItem_id(idx)->name;
                    const char* unit = registry.getItem_id(idx)->unit;
                    
                    // Stream widget HTML directly
                    renderWidget(resolved_table[i].widget, resolved_table[i].id, name, val, unit);
                }
            }
        }
    }
}

4.2 Chunked Streaming Theory

The renderer leverages the WebServer’s sendContent() method to transmit the UI in discrete segments.

  • Memory Efficiency: By avoiding String concatenation, the heap remains free of large UI buffers. Each HTML tag or attribute is streamed as soon as it is generated, keeping the Pico’s memory consumption constant regardless of page size.
  • Latency Mitigation: The browser begins rendering the page the moment the first sendContent() call hits the socket. This “progressive rendering” makes the interface feel instantaneous even on slow networks or high-latency connections.

4.3 The Widget Contract

The renderWidget function acts as the UI generator. It maps the WidgetType to specific CSS/HTML patterns, ensuring a uniform visual language across all projects built on the framework.

  • Standardized Mapping:
    • WIDGET_TEXT: Wraps data in a <span class="value"> for easy JS targeting.
    • WIDGET_BAR: Generates an HTML <progress> element.
    • WIDGET_DIAL: Generates a placeholder <div> or <canvas> that the client-side JavaScript can hydrate into a gauge.
  • The Implicit Contract: Every widget must output an id attribute corresponding to the Registry ID. This ensures the client-side refreshData() function can “auto-wire” the widget to the live API updates without hardcoded glue code.

4.4 Design Rationale

  • Decoupled Rendering: The renderer is completely agnostic of the content. It simply executes the layout logic provided by the ResolvedTable. Adding a new UI element involves only updating the renderWidget switch-case.
  • Safe Traversal: Because the parent_id hierarchy is enforced by the ResolvedTable, the system naturally prevents orphaned widgets—any node not assigned to an existing parent effectively becomes invisible, preventing rendering errors.
  • Operational Resilience: The use of sendContent ensures the server can handle high-frequency requests. If a request is interrupted, the renderer simply halts, and the server recovers; no global state is corrupted, and no system-wide locks are held.

Section 5: Framework Integration & Auto-Hooks

The final piece of the platform architecture is the integration of system-level functionality into the dynamic rendering engine. This ensures that every project, regardless of its specific function (e.g., weather, pool, or garage), inherits a consistent management interface without the developer writing redundant application code.

5.1 System-Level Hooks

The rendering engine treats specific IDs as Framework Reserved Hooks. When the recursive walker encounters these IDs, it routes the stream to internal framework generators rather than the standard registry-mapping logic.

  • settings_page: When this ID is hit, the renderer calls app_render_settings(). This function streams the framework’s standard configuration form, including WiFi credential management, static IP settings, and device re-naming.
  • docs_page: When this ID is hit, the renderer triggers the app_get_documentation() callback. This allows developers to inject project-specific technical manuals (e.g., sensor pinouts or calibration guides) directly into the UI.
  • nav_bar: Before rendering the "root" container, the framework performs a pre-scan of the resolved_table for all children of root. It streams these as a global navigation bar, ensuring the user can jump between pages without hardcoded links.

5.2 Frontend Auto-Wiring (The Client-Side Contract)

The system maintains consistency by enforcing an implicit contract between the server-side generator and the existing JavaScript client.

  • The ID Constraint: Every widget rendered by the dynamic engine must output an HTML id attribute that is identical to the Registry ID.
  • JavaScript Compatibility: Because the client-side refreshData() function operates by scanning the DOM for these specific IDs, the new dynamic pages are “auto-wired” at load time. The JS does not care which container the sensor lives in; it only cares that the DOM element id matches the Registry ID provided in the api/manifest.
  • Rationale: This removes the need for client-side modifications when a developer changes the UI layout. Moving a sensor from the status_page to the irrigation_page is strictly a configuration change in the layout_table; the JS logic and the Registry data remain untouched.

5.3 Deterministic System State

To ensure the platform remains stable across all implementations, the following behaviors are enforced:

  1. Orphan Handling: Any LayoutNode defined with a parent_id that does not exist in the layout_table is ignored by the renderer at runtime. This prevents broken tree structures from crashing the web server.
  2. Navigation Consistency: The auto-generated navigation bar is sorted based on the order of declaration in the layout_table, ensuring that the user experience is consistent across all devices built on this framework.
  3. UI/Data Decoupling: The framework allows the same Registry item to be mapped to multiple locations (e.g., a “Current Temperature” dial on the root page and a “Trend” bar on the details page). The renderer treats these as two unique DOM elements, both of which are updated simultaneously by the refreshData() loop.

5.4 Integration Roadmap Summary

By combining the Layout Map, Boot Resolution, and Recursive Rendering, the framework achieves a truly declarative state: 1. Define: The developer writes a single layout_table in C++. 2. Resolve: The framework validates the layout at startup. 3. Render: The framework streams the UI dynamically based on the current state of the Registry. 4. Sync: The client-side JS auto-wires to the generated DOM IDs.

This architecture creates a “Plug-and-Play” Platform where the application developer only defines what is in the system, while the framework handles the how of data synchronization, UI generation, and system management.


Closing

The transition to a declarative, index-resolved framework marks the shift from building individual “projects” to architecting a “platform.” By moving the logic of UI construction from hardcoded HTML blocks into a structured, resolved layout table, the framework becomes inherently modular and resilient.

The developer is freed from the mechanical repetition of manual DOM management and manual data-binding. Instead, the framework creates a single source of truth: the Registry defines the system state, and the Layout Table defines the system interface. Because these two pillars are independent, the system achieves a level of flexibility where sensors can be moved, replicated, or re-styled without ever touching the underlying logic. This is the foundation of a robust, professional-grade IoT platform—one that is maintainable, scalable, and entirely under the user’s control.

No comments:

Post a Comment

The “BluePrint” Platform: A Declarative UI & Data-Sync Framework for PicoW J. Rogers, SE Ohio The code is here that this is going to mo...