Mastodon Politics, Power, and Science

Wednesday, March 18, 2026

What measurement is doing.

J. Rogers, SE Ohio

How measurement works.

When you measure an apple, you are not finding the property of mass in the apple. 
You are finding the relationship between an apple and the kg standard as a middleman.

But the kg is completely arbitrary, it is meaningless to the universe.
So we have to find the standards relationship to the natural ratios of the universe. 

That is what the Planck units are, they are not a unit chart.
They are a bridge between natural ratios and a single physical scale of the unified universe. 

natural ratio = apple in kg / m_P  in kg is not profound statement.
It just removes the arbitrary middleman we put between us and the universe.


The Gasket: How We Mistook Our Rulers for Reality

 J. Rogers, SE Ohio 


Every measurement you have ever made follows the same structure. Somewhere underneath the number on your instrument there is a pure, dimensionless ratio — the actual physical fact. To read it off a dial, you multiply it by an arbitrary scaling you chose in advance. The number you record is not the physics. It is the physics wearing your unit chart like a costume.

natural_ratio × unit_scaling = measured value

This is not a philosophical position. It is what measurement operationally does, every time, without exception. The natural ratio is invariant — every observer in every civilization finds it. The unit scaling is arbitrary — Babylonians, SI, CGS, it does not matter. The measured value changes with the unit chart. The natural ratio does not.

Physics is defined as the invariant. Therefore physics cannot carry units. If it has units, you are looking at a measurement — a natural ratio multiplied by an arbitrary scaling — not at the physics itself.


We Invented the Axes

Reality presents itself as unified. A falling rock, a warming gas, a vibrating string — one coherent substrate. We do not perceive unity. We perceive axes: mass, length, time, temperature, frequency. We experience these as independent because our senses are built that way. We measure them with different instruments. We assign them different units.

The independence is not in the physics. It is in the perception.

Once you impose multiple independent axes on a unified substrate, something is forced to appear: correction terms. Numbers that bridge the axes back together whenever a physical law tries to connect them. We gave those correction terms names. We called them constants of nature.

Newton understood this. He wrote his gravity law as F = MM/r² — a pure ratio, no correction term. The physics was in the ratio. He said explicitly that units have nothing to do with the physics.

After he died, we added G. We told ourselves we were completing his law. What we were actually doing was assuming that our SI units — kilograms, meters, seconds — were real, and patching the equation to work inside that assumption. G is the patch. Not a discovery. A patch for the coordinate origin we chose.


The Century of Constants

Newton's patch set the template. In rapid succession around 1900, three more constants appeared.

c — the speed of light. Appeared because we measured space in meters and time in seconds: two different arbitrary scalings for what turns out to be the same substrate axis. c is not a cosmic speed limit baked into the fabric of spacetime. c is l_P / t_P — the meter divided by the second.

h — Planck's constant. Appeared because we measured energy in joules and frequency in Hz: two different arbitrary scalings for the same substrate quantity. If you define E = f — one unit for both — h vanishes entirely. Not set to 1. Gone. It was never quantum mechanical content. It was a unit mismatch between two silos.

k_B — Boltzmann's constant. Appeared because we measured energy in joules and temperature in kelvin: two different arbitrary scalings for the same substrate quantity. k_B is not the bridge between mechanics and thermodynamics. It is the record that we gave thermal energy and mechanical energy different unit names.

Each one was celebrated as an independent discovery. Each one was seen as revealing something new about nature. Each one was recording that we had put a different arbitrary scaling on what was the same axis.

The dimensional formulas make this explicit:

c   = L/T        = l_P / t_P
h   = ML²/T      = m_P l_P² / t_P
k_B = ML²/(T²·θ) = m_P l_P² / (t_P² T_P)
G   = L³/(MT²)   = l_P³ / (m_P t_P²)

These are not abstract dimensional placeholders. L is l_P. T is t_P. M is m_P. θ is T_P. Every constant of nature is a specific combination of four numbers: the SI unit scalings for time, length, mass, and temperature. Those four numbers are the definitions of the second, the meter, the kilogram, and the kelvin — expressed in terms of the physics they are scaling away from.


Planck Found the Jacobians

In 1899, Max Planck derived a set of units by combining the constants in ways that produced pure numbers with no leftover SI factors. He found t_P, l_P, m_P, T_P. He looked at them and declared he had found God's units — nature's own fundamental scales, meaningful to any civilization in the universe.

He found the right numbers. He drew the wrong conclusion.

What Planck found were the Jacobians of a rotation — the components of the matrix that bridges SI coordinates to the natural ratios. They are not a destination. They are not a unit system. They are the numerical record of how far the second, the meter, the kilogram, and the kelvin missed the physics.

natural_ratio × t_P = time in seconds
natural_ratio × l_P = length in meters
natural_ratio × m_P = mass in kilograms

t_P is not a unit you switch to. t_P is already inside every SI time measurement ever made. You are already using it. The Planck scalings are implicit in SI. They were always there. Planck noticed them and thought he had found something beyond SI. He had found what was inside SI the whole time.

The natural ratios are not Planck units. They are the absence of any unit chart at all — the dimensionless physical reality that every unit chart is pointing at and none of them reach.


The 2019 Confession

In 2019, the SI system was redefined. The committee fixed the numerical values of c, h, k_B, and e to exact values by definition. They described this as anchoring the constants of nature.

What they operationally did:

fix t_P exactly          → defines the second
fix c = l_P/t_P exactly  → defines the meter via l_P = c·t_P
fix h = m_P l_P²/t_P     → defines the kg via m_P = h·t_P/l_P²

The SI base units are now defined by the Planck scalings. We did not say that is what we were doing. But operationally, that is what we did.

The 2019 redefinition is the institutional proof that the constants were always unit scalings. We locked them to exact values without admitting why those values were the right ones to lock — because admitting that would require admitting that G, c, h, k_B were never constants of nature in the first place.


The Gasket

Here is the contradiction that sat in plain sight for a century.

Accepted fact one: unit scaling is arbitrary. Every physicist agrees. Taught in the first lecture of every physics course.

Accepted fact two: physics is invariant to unit scaling. Every physicist agrees. Also taught in the first lecture.

What follows: if you write physics inside a unit chart, something must absorb the arbitrariness while keeping the physics invariant. That something is the constants. The constants are the gasket between the two accepted facts.

A gasket between two facts that both deny the constants are fundamental cannot itself be fundamental. The logic is direct. We accepted both facts. We refused to follow the implication one step further. We stopped exactly there and called what we found a deep mystery — the fine-tuning problem, the anthropic principle, the multiverse conjecture. Entire research programs built on the question of why the constants have the values they do.

The fine-tuning problem is asking why the Babylonians chose that particular time interval. That is the entire mystery. The values of the constants are the values of the second, the meter, the kilogram, the kelvin — set by Roman marching cadences, human body parts, a platinum cylinder in Paris, and the freezing point of water. The universe did not tune them. History did.


The Laws Were Never Independent

Once you see the constants as unit scalings, something else dissolves: the independence of the laws.

The celebrated unification formula — Hawking temperature — contains c, G, h, k_B all together. It is called the great meeting point of quantum mechanics, relativity, gravity, and thermodynamics. In natural ratios it says:

T_H / T_P = m_P / M × 1/(4π)²

A hot thing cools as it gets heavier. Scaled by the geometry of a sphere. That is the entire content. The four constants are the unit scalings for temperature and mass dressed up as a unification of four frameworks. Strip the unit chart and there is nothing left to unify, because the substrate was never fragmented.

Every fundamental law is one leg of a single tautology:

T/T_P = f·t_P = m/m_P = l_P/λ = E/E_P = p/p_P = X

One dimensionless scalar. Six different unit-scaled projections. Fifteen pairwise laws of physics. All of them are X = X.

E = mc² is X = X with a mass/energy Jacobian. E = hf is X = X with an energy/frequency Jacobian. λ = h/p is X = X with a momentum/wavelength Jacobian. de Broglie did not discover a new law. He found another angle on the same object.

The probability that fifteen independently discovered laws would form the complete combinatorial closure of six quantities — every pair connecting, every Jacobian consistent with every other — by accident is less than 10⁻²². It is not a coincidence. It is one thing.


What Newton Knew

We did not progress past Newton. We regressed.

Newton wrote ratios. He knew the physics was in the ratios. He knew the units were scaffolding. He left G out of his gravity law because G had no place in the physics — only in the unit chart.

We put G in. We reified the SI coordinate origin as the origin of physical reality. We added c, h, k_B in rapid succession, each time mistaking the gasket for the phenomenon. We built a century of physics on the assumption that the measured value — natural_ratio × unit_scaling — was the real thing, not a combination of the real thing and our arbitrary scaffolding.

We could not see it because we had no concept of how unit charts work as an operational fact. We did not know that every measurement is a tautology. We did not know that every constant is a Jacobian. We did not know that the Planck scalings were already inside SI, implicit, waiting to be noticed.

Planck noticed them in 1899 and thought he found God's rulers.

We fixed their values by international vote in 2019 and called it anchoring the constants of nature.

Both times we were looking directly at the unit scaling and seeing something else.


The Core Statement

Physics is invariant to unit scaling. Units are arbitrary. Therefore the constants — which change when units change — are unit scalings, not physics.

The natural ratios are what remains when every unit scaling is divided out. They are not a unit chart. Not Planck units. Not natural units. They are the absence of any measurement convention at all — the single dimensionless physical reality that every unit chart in every civilization is pointing at from a different arbitrary angle.

The laws of physics are not independent discoveries. They are projections of one tautology through different pairs of unit scalings. The constants are the projection coefficients. The physics is X = X.

We mistook perception for reality. The axes are ours. The substrate is not.


This argument is developed formally as a Grothendieck fibration in The Structural Necessity of Physical Law (Rogers, 2025), where the terminal object S_u, the forced emergence of Planck-like scales, and the derivation of all fundamental laws from a single equivalence chain are proved as categorical necessities, not empirical coincidences.

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.

What measurement is doing.

J. Rogers, SE Ohio How measurement works. When you measure an apple, you are not finding the property of mass in the apple.  You are finding...