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)
idstored in RAM: 20 bytes (truncated if longer)parent_idstored in RAM: 20 bytes (truncated if longer)namestored 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: ignoredregistry_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 tabregistry_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: aW_PAGEid, or another container idname: 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_ROOTandW_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: aW_PAGEid orW_CARDidname: rendered as the collapsible header textregistry_id: must be""props: none supported- Children: any widget type or container type except
W_ROOTandW_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: aW_PAGEid orW_CARDidname: rendered as the card headingregistry_id: must be""props: none supported- Children:
W_BUTTONnodes 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: aW_PAGEidname: rendered as the card headingregistry_id: must be""props: none supported- Children: container nodes — each becomes one tab.
The child’s
namefield 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: requiredprops: none- Display name: from registry item’s
namefield - Unit: from registry item’s
unitfield - 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_HELPnode with the same parent, the ⓘ icon is rendered inline on the same line as the value, after the unit. TheW_HELPnode 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: requiredprops:min:N— left (empty) end of scale. Default:0.0max:N— right (full) end of scale. Default:100.0
- Display name: from registry item’s
namefield (shown above the bar) - Unit: from registry item’s
unitfield (shown after the numeric value) - Value: updates every 5 seconds
- Default: bar at 0% width, numeric value shows
-- - Note: if
minandmaxare 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: requiredprops:min:N— value at which the arc is empty. Default:0.0max:N— value at which the arc is full. Default:100.0
- Display name: from registry item’s
namefield (shown above the dial) - Unit: from registry item’s
unitfield (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 aCONTROL_SLIDERregistry itemprops: none (min, max, step, default come from the registryCONTROL_SLIDERdefinition)- Display name: from registry item’s
namefield - Unit: from registry item’s
unitfield - Current value: shown in the label, updates as the slider moves
- Sends: POST to
/api/updatewith the registry string id and new float value on everyinputevent
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 aCONTROL_BUTTONregistry itemprops:momentary:true— reserved for future use. Currently the button always sends1on click.
- Display name: from registry item’s
namefield - Sends: POST to
/api/updatewith the registry string id and value1on 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
idof an entry inhelp_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
idof an entry inhelp_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°C / -40 to 176°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–80°C • 0–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: ° for
°, – for –, • for •,
< 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