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.

Graph Specification JSON Format and Bridge Interaction

 J. Rogers,  SE Ohio

This document formally defines the JSON format used by Pico devices to declare their graphing requirements to the bridge, and describes how the bridge and Pico cooperate to provide embedded time‑series graphs in web pages.

1. Overview

The system consists of:

  • Pico devices – run the lightweight framework, read sensors, serve current data (/api/data), and serve a static graph specification (/graphlout/). They do not store history or render graphs.

  • Bridge – a persistent process on a local machine (discovered via mDNS) that polls each Pico’s data, logs it according to the graph specification, and serves SVG graphs on demand.

Flow:

  1. Discovery: Bridge discovers Pico via mDNS and fetches its manifest and the graph specification from /graphlout/.

  2. IP Storage: Pico records the bridge’s IP address (from the HTTP request or via a separate discovery response).

  3. Bridge Setup: Bridge parses the JSON, creates database tables for each named graph, and starts logging polled values.

  4. Page Generation: When a client requests a page from the Pico, the Pico’s web server generates HTML. For each graph widget (as defined in the Pico’s layout table), it inserts an <img> tag with src pointing to http://<bridge-ip>/graph/<graph_name>?range=....

  5. Graph Serving: Browser requests the SVG from the bridge; the bridge queries its stored data, renders the graph per the specification, and returns the SVG.

2. Graph Specification Endpoint

Endpoint: GET /graphlout/ on the Pico.

Response: A JSON object conforming to the schema below.

The bridge fetches this endpoint once per device during discovery. If the Pico does not provide this endpoint (404), the bridge assumes no graphing is configured.

3. JSON Schema

The root object contains a "graphs" array. Each element describes one logical graph, which can be referenced by name in image URLs.

3.1 Root Object

FieldTypeDescription
devicestringOptional. Device identifier (e.g., hostname).
graphsarray of objectList of graph definitions.

3.2 Graph Definition Object

FieldTypeRequiredDescription
namestringyesUnique identifier for the graph. Used in URLs: /graph/<name>.
titlestringnoHuman‑readable title (displayed on the graph). Defaults to name.
data_sourcesarray of objectyesList of one or more registry items whose values are plotted. Each source has its own styling and scaling (see below).
time_rangestring / objectnoDefault time window to display. Can be a string like "24h" or an object with start and end (ISO 8601). Web client may override. Default "24h".
sampling_intervalintegernoHow often (in seconds) the bridge should log this graph’s data sources. If omitted, the bridge logs every poll (typically every 10 seconds).
retentionstringnoHow long to keep data, e.g., "30d", "1y". If omitted, bridge keeps data indefinitely (or uses its own default).
axesobjectnoConfiguration for X and Y axes. See below.
legendboolean / objectnoShow legend? Can be true/false or an object with positioning. Default true.
widthintegernoDefault SVG width in pixels. Web client may override. Default 600.
heightintegernoDefault SVG height in pixels. Default 300.
backgroundstringnoBackground color (CSS color). Default "#ffffff".
borderstring / objectnoBorder style (CSS).

3.2.1 data_sources object

Each element corresponds to one series on the graph.

FieldTypeRequiredDescription
idstringyesRegistry item identifier (e.g., "pressure", "temp_a"). Must match an item in the device’s /api/data.
labelstringnoLabel for this series in the legend. Defaults to id.
colorstringnoCSS color for the line/points. If omitted, the bridge picks a color automatically.
unitstringnoOverride the unit from the registry (e.g., "°C"). If not given, the unit from the registry is used.
scaleobjectnoScaling parameters for this series. See below.
typestringnoPlot type: "line", "step", "scatter", "bar". Default "line".
y_axisstringnoWhich Y‑axis to use: "left" (default) or "right". Allows multiple axes in one graph.

3.2.2 scale object

FieldTypeRequiredDescription
minnumbernoMinimum value for this series. If omitted, auto‑scale based on data.
maxnumbernoMaximum value. If omitted, auto‑scale.
lockedbooleannoIf true, the bridge should not auto‑scale even if data goes outside. Default false.

3.2.3 axes object

FieldTypeDescription
xobjectConfiguration for X‑axis (time). See below.
y_leftobjectConfiguration for left Y‑axis.
y_rightobjectConfiguration for right Y‑axis (if any series uses it).

Axis object fields (for x, y_left, y_right):

FieldTypeDescription
labelstringAxis label (e.g., "Time", "Pressure (hPa)").
minnumberFor Y axes: fixed minimum. For X axis: not used (time is handled separately).
maxnumberFixed maximum for Y axis.
tick_formatstringFor X axis: time format (e.g., "%H:%M", "%Y-%m-%d"). For Y axis: number format (e.g., ".1f").
gridbooleanShow grid lines? Default true.
logbooleanUse logarithmic scale? (Y axes only). Default false.

3.3 Example

{
  "device": "weather_station_1",
  "graphs": [
    {
      "name": "pressure_24h",
      "title": "Barometric Pressure (24h)",
      "data_sources": [
        {
          "id": "pressure",
          "label": "Pressure",
          "color": "#1f77b4",
          "unit": "inHg",
          "scale": {
            "min": 28.5,
            "max": 31.5
          }
        }
      ],
      "time_range": "24h",
      "sampling_interval": 300,
      "retention": "30d",
      "axes": {
        "x": {
          "label": "Time",
          "tick_format": "%H:%M",
          "grid": true
        },
        "y_left": {
          "label": "Pressure (inHg)",
          "tick_format": ".2f"
        }
      },
      "width": 800,
      "height": 400
    },
    {
      "name": "temp_humidity_week",
      "title": "Temperature & Humidity (7 days)",
      "data_sources": [
        {
          "id": "temp_a",
          "label": "Temperature",
          "color": "#d62728",
          "unit": "°F",
          "y_axis": "left"
        },
        {
          "id": "humidity_a",
          "label": "Humidity",
          "color": "#2ca02c",
          "unit": "%",
          "y_axis": "right"
        }
      ],
      "time_range": "7d",
      "sampling_interval": 600,
      "retention": "90d",
      "axes": {
        "x": {
          "label": "Date",
          "tick_format": "%m-%d"
        },
        "y_left": {
          "label": "Temperature (°F)",
          "min": 0,
          "max": 120,
          "grid": true
        },
        "y_right": {
          "label": "Humidity (%)",
          "min": 0,
          "max": 100,
          "grid": false
        }
      }
    }
  ]
}

4. Bridge Behavior

Upon discovering a Pico (via mDNS), the bridge:

  • Fetches http://<pico-ip>/graph_definitions/.

  • Parses the JSON. For each graph:

    • Creates a time‑series table (or InfluxDB measurement, etc.) with appropriate retention policy.

    • Starts logging the listed data sources at the requested sampling_interval (or every poll if omitted).

  • Serves SVG graphs at http://<bridge-ip>:<port>/<pico host name>/<name of graph>.svg. Supported query parameters:

    • Other parameters may be added as needed.

When a graph request arrives, the bridge:

  • Retrieves the stored spec for that graph.

  • Queries the logged data for the requested time range.

  • Applies scaling, axis formatting, and styling from the spec.

  • Renders an SVG (e.g., using a library like svgwrite or matplotlib).

  • Returns the SVG with Content-Type: image/svg+xml.

The bridge must handle multiple devices and graphs concurrently.

5. Pico Behavior

The Pico's responsibilities are minimal:

  • Serve the static JSON spec from /graphlout/. The spec is defined in the application code (e.g., as a constant string or generated by a function). This spec contains the graph name, data sources, sampling interval, retention period, and the time range to display (e.g., 24 hours, 100 days).

  • Store the bridge IP address when the bridge first contacts it. This can be done by recording the source IP of the HTTP request to /graph_defintion/ (the bridge's IP).

  • When generating HTML pages (based on its internal layout table), for any graph widget, embed an <img> tag with src pointing to:

    http://<stored-bridge-ip>:port/<pico-hostname>/<graph_name>.svg

    The graph name comes from the graph definition. The time range is not included in the URL—it is already defined in the graph specification that the bridge fetched from /graphlout/. The bridge uses that stored range when rendering the SVG.

For example, if the layout includes a graph widget for "pressure_24h" on a device named "weather_pico", the Pico's web server outputs:

<img src="http://192.168.1.100:5001/weather_pico/pressure_24h.svg">

The IP address is the stored bridge IP; the device hostname and graph name identify which specification to use. The bridge already knows from the spec that this graph should show 24 hours of data.

If no bridge IP has been stored (e.g., bridge not yet discovered), the Pico may omit the image or show a placeholder.

6. Discovery and IP Storage Details

The exact mechanism for IP storage is implementation‑dependent but must be reliable:

  • Option A: Bridge fetches /graphlout/; the Pico records the remote IP from the TCP connection. This requires the bridge to be on the same network and the request to come directly.

  • Option B: Bridge announces itself via mDNS (e.g., _pico-bridge._tcp). Pico listens for these announcements and stores the IP.

  • Option C: Pico provides an endpoint /api/bridge that the bridge can POST its IP to after discovery.

Any method works as long as the Pico ends up with a valid bridge IP to use in image URLs.

7. Embedding Graphs in Pages

The Pico’s layout system (separate from the graph spec) determines where graph widgets appear. When rendering a page, the framework:

  • Iterates through layout containers and maps.

  • For each MAP with widget type GRAPH, it finds the corresponding graph definition by name (the graph name is stored in the map or derived from the registry ID).

  • It then generates an <img> tag using the stored bridge IP, the graph name, and the default time range from the spec.

  • The resulting HTML is sent to the client.

The browser loads the image directly from the bridge, which returns an SVG. The graph is fully rendered server‑side; no JavaScript is required on the client.

8. Extensibility

The JSON schema is designed to be forward‑compatible. Additional fields (e.g., annotations, thresholds, fill_area, theme) can be added without breaking existing implementations. The bridge must ignore unknown fields.

9. Versioning

This document defines version 1.0 of the Graph Specification JSON Format. Future versions may introduce new fields or change semantics; a version field in the root object can be added if needed.


This specification is part of the Pico Discovery Bridge ecosystem.

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