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
};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 propertiesstruct 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"} };
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. LayoutNode structure.
- 2.3 Rationale & Design Specifics
- Flash Memory Allocation (
PROGMEM): By marking the table withPROGMEM(or placing it in the.rodatasection), 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_idlinking): 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 simplestrcmp()lookups. - Registry Decoupling: The
registry_idfield is a loose string reference. It does not contain an actual pointer or index. This is critical because it allows theLayoutTableto be compiled independently of theRegistrystate. The actual binding to the registry index occurs in the Boot-Time Resolution Phase (Section 3). - Widget Portability: The
WidgetTypeenum acts as a contract for the Renderer. The renderer is programmed to know how to draw aWIDGET_BAR, but theLayoutNodedoesn’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 runtimerenderContainerfunction (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_tableexists 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_idxto255on 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
Stringconcatenation, 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
idattribute corresponding to the Registry ID. This ensures the client-siderefreshData()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 therenderWidgetswitch-case. - Safe Traversal: Because the
parent_idhierarchy is enforced by theResolvedTable, 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
sendContentensures 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 callsapp_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 theapp_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 theresolved_tablefor all children ofroot. 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
idattribute that is identical to theRegistry 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 elementidmatches the Registry ID provided in theapi/manifest. - Rationale: This removes the need for client-side
modifications when a developer changes the UI layout. Moving a sensor
from the
status_pageto theirrigation_pageis strictly a configuration change in thelayout_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:
- Orphan Handling: Any
LayoutNodedefined with aparent_idthat does not exist in thelayout_tableis ignored by the renderer at runtime. This prevents broken tree structures from crashing the web server. - 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. - 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.