Mastodon Politics, Power, and Science

Saturday, March 21, 2026

BluePrint IoT Framework — Layout Table Reference Manual

Complete reference for layout_table[], help_table[], all widget types, all container types, all props, and documentation patterns


The LayoutNode Structure

Every entry in layout_table[] is a LayoutNode:

struct LayoutNode {
    const char* id;           // unique id for this UI node
    const char* parent_id;    // id of the parent node ("" for children of root)
    const char* name;         // display name shown in the UI
    const char* registry_id;  // registry item this widget displays ("" for containers)
    WidgetType  widget;       // how to render this node
    const char* props;        // optional key:value properties ("" for none)
};

All fields are const char* — string literals stored in Flash. No size limits on any field. The framework copies id, parent_id, and name into a fixed RAM structure at boot (20, 20, and 32 bytes respectively — truncated if longer), so keep those short. The registry_id and props strings are only accessed during boot resolution and are never copied to RAM.

Field rules

id — Must be unique across the entire layout table. Used internally to build the parent-child tree and to match W_HELP/W_HTML nodes against help_table entries. Keep under 20 characters.

parent_id — Must exactly match the id of another node in the table. Use "" only for children of "root". Declaration order within a parent determines render order on screen.

name — The display name shown in the UI. For container nodes (W_PAGE, W_CARD, W_COLLAPSIBLE, W_RADIO) this is rendered as the heading. For leaf widget nodes (W_TEXT, W_SLIDER, etc.) the display name comes from the registry item instead — the name field here is ignored for those. Keep under 32 characters.

registry_id — The string ID of the registry item from app_register_items(). Must match exactly, including case. Use "" for containers and for W_HELP/W_HTML nodes. If this field is non-empty and doesn’t match any registry entry, the error is logged at boot and the node is skipped.

props — A comma-separated list of key:value pairs. Use "" if no props are needed. See the Props Reference section for all supported keys and their defaults.


Table Limits

  • Maximum nodes: 64 (MAX_LAYOUT_NODES)
  • Maximum nesting depth: 64 (safety counter in the ancestor-walk function)
  • id stored in RAM: 20 bytes (truncated if longer)
  • parent_id stored in RAM: 20 bytes (truncated if longer)
  • name stored in RAM: 32 bytes (truncated if longer)

The Tree Structure

The layout table is a flat array that defines an implicit tree using parent_id links. The tree must have exactly one root node. Pages are children of root. Cards are children of pages. Widgets are children of cards.

root  (W_ROOT)
├── main_page  (W_PAGE)
│   ├── sensor_card  (W_CARD)
│   │   ├── w_temp_a     (W_DIAL)
│   │   ├── help_temp    (W_HELP)
│   │   └── sensor_info  (W_HTML)
│   └── control_card  (W_CARD)
│       ├── w_target   (W_SLIDER)
│       └── w_btn      (W_BUTTON)
└── system_page  (W_PAGE)
    └── status_card  (W_CARD)
        └── w_cpu  (W_TEXT)

The array does not need to be in tree order. The renderer finds children by scanning for matching parent_id values. However, within a given parent, children render in the order they appear in the array — top to bottom.


Container Types

Containers are structural nodes. They have no registry_id. Their name field is rendered as a heading.


W_ROOT

The invisible root of the tree. Every layout must have exactly one. It has no visual representation and is never rendered.

{"root", "", "Root", "", W_ROOT, ""}
  • parent_id: must be ""
  • name: ignored
  • registry_id: must be ""
  • props: ignored
  • Only one per layout

W_PAGE

A top-level page. Each W_PAGE node gets its own URL (/page_id) registered at boot and its own entry in the navigation bar. The root URL (/) redirects to the first W_PAGE node found in the table.

{"main_page", "root", "Weather Station", "", W_PAGE, ""}
  • parent_id: must be "root"
  • name: shown in the nav bar tab
  • registry_id: must be ""
  • props: none supported
  • Children: W_CARD, W_COLLAPSIBLE, W_RADIO, W_TABBED

The nav bar on every page shows all W_PAGE nodes as links, in declaration order. The current page’s tab is highlighted.


W_CARD

A plain rectangular card. Children are stacked vertically inside it. This is the most common container.

{"sensor_card", "main_page", "Live Sensors", "", W_CARD, ""}
{"sensor_card", "main_page", "Live Sensors", "", W_CARD, "width:400"}
  • parent_id: a W_PAGE id, or another container id
  • name: rendered as the card heading (<h3>)
  • registry_id: must be ""
  • props:
    • width:N — sets card width in pixels. Default: 0 (auto — card sizes to content and flexbox layout)
  • Children: any widget type, any container type except W_ROOT and W_PAGE

Default behavior without props: card width is automatic. All cards on a page sit in a flex row that wraps. Cards without a width prop have a minimum width of 280px and grow to fit their content.


W_COLLAPSIBLE

A card with a clickable header that shows or hides its contents. Starts expanded. The header shows a ▼ arrow that toggles on click.

{"outdoor_section", "env_page", "Outdoor Sensors", "", W_COLLAPSIBLE, ""}
  • parent_id: a W_PAGE id or W_CARD id
  • name: rendered as the collapsible header text
  • registry_id: must be ""
  • props: none supported
  • Children: any widget type or container type except W_ROOT and W_PAGE

Default behavior: starts visible. Clicking the header hides the body. Clicking again shows it.


W_RADIO

A card containing buttons that behave as a mutually exclusive group. Clicking one button deselects all others in the group and highlights the selected one. Each button sends its value (1) to its registry item when clicked.

{"zone_selector", "irrigation_page", "Water Zones", "", W_RADIO, ""}
  • parent_id: a W_PAGE id or W_CARD id
  • name: rendered as the card heading
  • registry_id: must be ""
  • props: none supported
  • Children: W_BUTTON nodes only

Default behavior: no button is pre-selected. Clicking a button sends value 1 to its registry item and highlights it in teal. All other buttons return to their inactive style.


W_TABBED

A card where each direct child container becomes a tab. Clicking a tab hides all other tab panels and shows the selected one.

{"tabbed_sensors", "main_page", "Sensors", "", W_TABBED, ""}
  • parent_id: a W_PAGE id
  • name: rendered as the card heading
  • registry_id: must be ""
  • props: none supported
  • Children: container nodes — each becomes one tab. The child’s name field is used as the tab label.

Default behavior: the first tab is shown. Others are hidden until clicked.


Leaf Widget Types

Leaf widgets display or control a single registry item. They must have a valid registry_id.


W_TEXT

Displays the registry item’s value as a plain number. The display name and unit come from the registry item.

{"w_cpu_temp", "status_card", "", "cpu_temp_f", W_TEXT, ""}

Renders as:

CPU Temperature:  102.75  °F
  • registry_id: required
  • props: none
  • Display name: from registry item’s name field
  • Unit: from registry item’s unit field
  • Value: updates every 5 seconds via the JS refresh loop
  • Default value shown: -- until first data arrives
  • W_HELP behavior: if the immediately following sibling node in the table is a W_HELP node with the same parent, the ⓘ icon is rendered inline on the same line as the value, after the unit. The W_HELP node then renders nothing on its own.

W_BAR

Displays the registry item’s value as a horizontal progress bar with a numeric readout. Requires min and max props to scale correctly.

{"w_humidity", "sensor_card", "", "humidity_a", W_BAR, "min:0,max:100"}

Renders as a labeled bar that fills proportionally between min and max, with the numeric value displayed to the right.

  • registry_id: required
  • props:
    • min:N — left (empty) end of scale. Default: 0.0
    • max:N — right (full) end of scale. Default: 100.0
  • Display name: from registry item’s name field (shown above the bar)
  • Unit: from registry item’s unit field (shown after the numeric value)
  • Value: updates every 5 seconds
  • Default: bar at 0% width, numeric value shows --
  • Note: if min and max are not set, the bar defaults to a 0–100 scale. A value outside the min/max range is clamped to 0% or 100%.

W_DIAL

Displays the registry item’s value as an SVG arc gauge. The arc sweeps 240 degrees from lower-left to lower-right over the top. Requires min and max props.

{"w_temp", "sensor_card", "", "temp_a", W_DIAL, "min:0,max:120"}

Renders as a 110×68px SVG with a dark grey track arc and a teal fill arc. The current value is shown as text in the center of the arc.

  • registry_id: required
  • props:
    • min:N — value at which the arc is empty. Default: 0.0
    • max:N — value at which the arc is full. Default: 100.0
  • Display name: from registry item’s name field (shown above the dial)
  • Unit: from registry item’s unit field (shown to the right of the dial)
  • Value: updates every 5 seconds. Arc fill and centre text update simultaneously.
  • Default: arc at 0% fill, centre text shows --
  • Note: uses pathLength="1" normalization so the arc scales correctly at any SVG size. Values outside min/max are clamped.

W_SLIDER

Renders a range slider control for a registry item. The slider’s min, max, step, default value, and unit all come from the registry item definition — not from props.

{"w_target", "control_card", "", "moisture_target", W_SLIDER, ""}

Renders as a labeled slider showing the current value. Moving the slider immediately sends the new value to the registry via /api/update.

  • registry_id: required — must reference a CONTROL_SLIDER registry item
  • props: none (min, max, step, default come from the registry CONTROL_SLIDER definition)
  • Display name: from registry item’s name field
  • Unit: from registry item’s unit field
  • Current value: shown in the label, updates as the slider moves
  • Sends: POST to /api/update with the registry string id and new float value on every input event

W_BUTTON

Renders a push button. Clicking it sends value 1 to the registry item.

{"w_water_now", "settings_card", "", "water_now", W_BUTTON, ""}
  • registry_id: required — must reference a CONTROL_BUTTON registry item
  • props:
    • momentary:true — reserved for future use. Currently the button always sends 1 on click.
  • Display name: from registry item’s name field
  • Sends: POST to /api/update with the registry string id and value 1 on click
  • In a W_RADIO container: the button participates in mutual exclusion — clicking highlights it and deselects siblings. Behavior is controlled by the parent container, not the button itself.

W_HELP

Renders an ⓘ icon. Hovering over the icon shows a tooltip containing the HTML from the matching help_table entry. No JavaScript — pure CSS hover.

{"help_temp", "sensor_card", "", "", W_HELP, ""}
  • registry_id: must be ""
  • props: none
  • id: must match the id of an entry in help_table[]
  • name: ignored

Inline behavior: if this node immediately follows a W_TEXT node in the table and both share the same parent_id, the ⓘ icon is rendered inline on the same line as the W_TEXT value — after the unit. The W_HELP node renders nothing on its own in this case. This is automatic and requires no configuration.

Standalone behavior: if this node does not immediately follow a W_TEXT sibling, it renders as its own block — a standalone ⓘ icon with the hover tooltip.

Tooltip position: appears to the right of the icon, overlapping adjacent content. Uses position:absolute so it does not push other elements.

If no matching help_table entry exists: nothing is rendered and a warning is logged to the serial port.


W_HTML

Streams the HTML string from the matching help_table entry directly into the container with no wrapper, icon, or interaction. The HTML appears exactly where the node is declared in the table.

{"sensor_card_info", "sensor_card", "", "", W_HTML, ""}
  • registry_id: must be ""
  • props: none
  • id: must match the id of an entry in help_table[]
  • name: ignored

Use this for descriptive text, headings, callout boxes, or any static content that should appear directly in a card. Place it at the bottom of a card’s children to have it appear below the sensor readings. Place it at the top to have it appear above them.

If no matching help_table entry exists: nothing is rendered and a warning is logged.


Props Reference

Props are specified as a comma-separated string of key:value pairs. Keys and values are case-sensitive. The entire props string must fit in 64 bytes (it is only read at boot, not stored in RAM).

Key Applies to Type Default Effect
min W_BAR, W_DIAL float 0.0 Value at which the widget shows empty/zero
max W_BAR, W_DIAL float 100.0 Value at which the widget shows full
width W_CARD integer 0 (auto) Card width in pixels
momentary W_BUTTON true false Reserved — no current effect

Multiple props: separate with commas, no spaces.

"min:0,max:120"
"min:-40,max:80"
"width:400"

Unrecognized keys are silently ignored.

min and max without has_min/has_max flags: if you omit min or max, the framework uses 0.0 and 100.0 as internal defaults but does not set the has_min/has_max flags. The renderer checks these flags before applying — if neither flag is set and you omit both props, the widget still defaults to 0–100 scale because the renderer falls back to 0.0f and 100.0f directly.


The Help Table

struct HelpNode {
    const char* id;    // matches the id of a W_HELP or W_HTML node in layout_table
    const char* html;  // HTML content — any valid HTML, stored in Flash
};

extern const HelpNode help_table[];
extern const int help_count;

Both fields are const char* — string literals live in Flash. There is no size limit on the html field. The string is streamed directly to the network buffer with sendContent() when requested — it is never copied into RAM.

Defining the help table

const HelpNode help_table[] = {
    {"help_temp",
     "<b>Temperature A</b><br>"
     "AM2302 sensor on GPIO 2.<br>"
     "Range: -40 to 80&deg;C / -40 to 176&deg;F.<br>"
     "Updates every ~6 seconds."},

    {"sensor_card_info",
     "<p><strong>Outdoor Conditions</strong></p>"
     "<p>Live readings from the <strong>AM2302</strong> sensor on GPIO 2.</p>"
     "<p style='color:#888;font-size:.85em'>Range: -40&ndash;80&deg;C &bull; 0&ndash;100% RH</p>"},
};
const int help_count = sizeof(help_table) / sizeof(HelpNode);

help_count must be declared exactly this way — the framework uses it to know how many entries to search.

Linking help content to a layout node

The id field of a HelpNode must exactly match the id field of a W_HELP or W_HTML node in layout_table. This is the only connection. There is no other field to set.

Layout node id Help table id Result
"help_temp" "help_temp" ✓ matched — content rendered
"help_temp" "Help_temp" ✗ not matched — warning logged, nothing rendered
"help_temp" "help_temperature" ✗ not matched — warning logged, nothing rendered

HTML in help content

Any valid HTML is accepted. The tooltip and inline containers already have the framework’s dark theme CSS applied. Some useful patterns:

<!-- Bold label with details -->
<b>Sensor Name</b><br>Description here.<br>Range: 0–100%.

<!-- Section heading + paragraph -->
<p><strong>Card Title</strong></p><p>Descriptive text about what this card shows.</p>

<!-- Muted fine print -->
<p style='color:#888;font-size:.85em'>Technical note here.</p>

<!-- Bullet list -->
<ul style='margin:4px 0;padding-left:16px'>
  <li>Point one</li>
  <li>Point two</li>
</ul>

Use HTML entities for special characters: &deg; for °, &ndash; for –, &bull; for •, &lt; for <.


Documentation Patterns

Pattern 1 — Inline icon on a sensor value (W_TEXT + W_HELP)

Place the W_HELP node immediately after the W_TEXT node, both with the same parent. The icon appears inline after the unit on the same line.

// layout_table
{"w_temp",    "sensor_card", "", "temp_a",  W_TEXT, ""},
{"help_temp", "sensor_card", "", "",        W_HELP, ""},

// help_table
{"help_temp", "<b>Temperature A</b><br>AM2302 on GPIO 2. Updates every 6s."},

Result: Temperature A: 78.08 °F ⓘ — hover the ⓘ to see the tooltip.


Pattern 2 — Standalone icon (W_HELP not preceded by W_TEXT)

Place W_HELP after a W_BAR or W_DIAL node, or anywhere it is not directly after a W_TEXT sibling. It renders as its own block with the icon on its own line.

{"w_humidity",    "sensor_card", "", "humidity_a", W_BAR,  "min:0,max:100"},
{"help_humidity", "sensor_card", "", "",           W_HELP, ""},

Result: bar renders on one line, then ⓘ icon appears below it on its own line.


Pattern 3 — Inline descriptive text in a card (W_HTML)

Place a W_HTML node anywhere in a card’s children. It streams its HTML directly into the card at that position.

// At the bottom of the card — appears after all sensor values
{"sensor_card_info", "sensor_card", "", "", W_HTML, ""},

// help_table
{"sensor_card_info",
 "<p><strong>Outdoor Conditions</strong></p>"
 "<p>Live readings from the AM2302 sensor on GPIO 2.</p>"},

Pattern 4 — Card description at the top

Declare the W_HTML node before the widget nodes to have it appear above the sensor values:

{"sensor_card",      "main_page",  "Live Sensors", "", W_CARD, ""},
{"sensor_card_info", "sensor_card","",             "", W_HTML, ""},  // appears first
{"w_temp_a",         "sensor_card","",    "temp_a", W_DIAL, "min:0,max:120"},

Pattern 5 — Same registry item shown on multiple pages

The same registry item can appear in multiple W_TEXT, W_BAR, or W_DIAL nodes pointing to the same registry_id. Each node must have a unique id. The framework renders them independently and the JS refresh loop updates all of them simultaneously.

{"w_temp_main",   "sensor_card",  "", "temp_a", W_DIAL, "min:0,max:120"},
{"w_temp_summary","overview_card","", "temp_a", W_TEXT, ""},

Boot Diagnostics

The serial port logs every resolution step at boot. Watch for these messages:

>> [Layout] Boot resolution starting...
>> [Layout] Total nodes to resolve: 22
>> [Layout] node[6] id='w_humidity_a' parent='sensor_card' name='' widget=8 container=NO
>> [Layout OK]    node 'w_humidity_a' -> registry[1] id='humidity_a' name='Humidity A'
>> [Layout] Boot resolution complete. 22 nodes resolved.
>> Registered page endpoint: /main_page
>> Registered page endpoint: /system_page

[Layout OK] — node resolved successfully. Shows the numeric registry index and the registry item’s name for verification.

[Layout ERROR]registry_id string did not match any registry entry. The node is skipped at render time. Fix the registry_id in the layout table to match the ID in SENSOR_AUTO or SENSOR_MANUAL exactly.

[Render] WARNING: no help content found — a W_HELP or W_HTML node’s id did not match any entry in help_table. Add the matching HelpNode or correct the id.

[Render] WARNING: no children found — a container node has no children in the layout table. The container heading will render but the card body will be empty.

No comments:

Post a Comment

Measurement as Ratio: The Invariant Beyond Units and Constants

J. Rogers, SE Ohio This paper is at:  https://github.com/BuckRogers1965/Physics-Unit-Coordinate-System/tree/main/docs Abstract Measurement i...