BluePrint IoT Framework — Configuration Manual
How to add a sensor, display it on a web page, and document it
Overview
Everything a developer touches lives in a single .ino
file. The framework is a black box — you never modify it. Your job is to
populate three tables:
| Table | What it defines | Where |
|---|---|---|
app_register_items() |
What the device knows — sensors, controls, callbacks | Your .ino file |
layout_table[] |
What the website looks like — pages, cards, widgets | Your .ino file |
help_table[] |
What the website explains — hover tooltips, inline text | Your .ino file |
This manual walks through adding a humidity sensor from scratch,
using the actual humidity_a sensor from the Weather Station
project as the working example.
Step 1 — Write the Hardware Callback
The callback is the function that reads your hardware and pushes the value into the registry. It is called by the scheduler on Core 1 on a repeating timer.
The signature is always the same:
int myCallback(struct _task_entry_type* task, int idx, int) {
// reschedule yourself for next reading
AddTaskMilli(task, registry.getItem_id(idx)->update_interval_ms, &myCallback, idx, 0);
// read the hardware
float value = /* your sensor reading here */;
// sanity check — discard garbage readings
if (value < 0.3) return 0;
// push value into the registry
registry.set_id(idx, value);
return 0;
}The idx parameter is the numeric registry index of this
sensor — the framework passes it in automatically. You never hardcode an
index. registry.getItem_id(idx)->update_interval_ms
reads the polling interval you set in the registry, so the interval is
defined in one place only.
The actual humidity callback from the Weather Station:
int readAM2302a_humidity(struct _task_entry_type* task, int idx, int) {
AddTaskMilli(task, registry.getItem_id(idx)->update_interval_ms, &readAM2302a_humidity, idx, 0);
am2302a.read();
float humidity = am2302a.get_Humidity();
if (humidity < 0.3) return 0;
registry.set_id(idx, humidity);
return 0;
}Step 2 — Register the Sensor in the Registry
Inside app_register_items(), add one macro call. This is
the only place you define what the sensor is — its ID, display
name, polling interval, unit, and callback.
For a sensor that polls automatically on a timer:
SENSOR_AUTO(ID, NAME, INTERVAL_MS, UNIT, CALLBACK)| Parameter | Description |
|---|---|
ID |
Unique string identifier — used everywhere to reference this sensor |
NAME |
Human-readable name shown in the web UI |
INTERVAL_MS |
How often to poll in milliseconds |
UNIT |
Unit string shown next to the value |
CALLBACK |
The function pointer for the read callback |
The humidity sensor registration:
SENSOR_AUTO("humidity_a", "Humidity A", 7003, "%", readAM2302a_humidity);The interval is 7003ms — a prime-ish number deliberately offset from other sensors so they don’t all fire at the same moment and stack up on Core 1.
For a sensor updated manually by another callback (computed values like heat index):
SENSOR_MANUAL("heat_index", "Heat Index", "°F");No interval, no callback — another function calls
registry.set("heat_index", value) when it has a new
reading.
For controls:
CONTROL_SLIDER("moisture_target", "Target Moisture", 40, 20, 80, 5, "%");
// ID NAME DEFAULT MIN MAX STEP UNIT
CONTROL_BUTTON("water_now", "Manual Water");
// ID NAMEThe registry ID is the critical link between all three tables. Get it right here and everything else follows.
Step 3 — Add the Sensor to the Layout Table
The layout table defines your website. It is a flat array of
LayoutNode structs. Every node has a parent — this implicit
tree defines pages, cards, and what goes in them.
struct LayoutNode {
const char* id; // unique id for this UI node
const char* parent_id; // id of the parent node ("" for root children)
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)
};3a — Declare the tree structure first
Every layout starts with a root, then pages, then cards. These are
containers — they have no registry_id.
const LayoutNode layout_table[] = {
{"root", "", "Root", "", W_ROOT, ""},
{"main_page", "root", "Weather Station","", W_PAGE, ""},
{"sensor_card", "main_page","Live Sensors", "", W_CARD, ""},
// ...
};3b — Add a widget for your sensor
Inside the card, add a leaf node that references your registry ID:
{"w_humidity_a", "sensor_card", "Humidity A", "humidity_a", W_BAR, "min:0,max:100"},| Field | Value | Meaning |
|---|---|---|
id |
"w_humidity_a" |
Unique UI node ID |
parent_id |
"sensor_card" |
Which card this appears in |
name |
"Humidity A" |
Not used for leaf widgets — display name comes from the registry |
registry_id |
"humidity_a" |
Must match the ID from Step 2 exactly |
widget |
W_BAR |
Render as a horizontal progress bar |
props |
"min:0,max:100" |
Scale the bar from 0% to 100% |
Widget types
| Type | What it renders | Props |
|---|---|---|
W_TEXT |
Plain numeric value with label and unit | none |
W_BAR |
Horizontal progress bar | min:N,max:N |
W_DIAL |
SVG arc gauge | min:N,max:N |
W_SLIDER |
Range slider control | none (uses registry min/max/step) |
W_BUTTON |
Momentary trigger button | none |
W_HELP |
ⓘ icon — hover to show tooltip | none |
W_HTML |
Inline static HTML | none |
Container types
| Type | What it renders |
|---|---|
W_ROOT |
Invisible tree root — always first, always one |
W_PAGE |
Top-level page — gets its own URL and nav tab |
W_CARD |
Plain card — children stacked vertically |
W_COLLAPSIBLE |
Card with a show/hide toggle header |
W_TABBED |
Child cards rendered as tabs |
W_RADIO |
Child buttons are mutually exclusive |
Props reference
| Key | Applies to | Effect |
|---|---|---|
min:N |
W_BAR, W_DIAL |
Sets the zero end of the scale |
max:N |
W_BAR, W_DIAL |
Sets the full end of the scale |
width:N |
W_CARD |
Sets card width in pixels |
momentary:true |
W_BUTTON |
Button resets to 0 after trigger |
Declaration order matters
The renderer walks the table top to bottom within each parent. Nodes
appear on screen in the order they are declared. Put the
W_HELP node immediately after the widget it documents — the
renderer checks for this and renders the ⓘ icon inline on the same line
as the value.
{"w_humidity_a", "sensor_card", "", "humidity_a", W_BAR, "min:0,max:100"},
{"help_humidity", "sensor_card", "", "", W_HELP, ""},Step 4 — Add Documentation in the Help Table
The help table holds static HTML strings. They live in Flash — never in RAM — and are streamed to the browser on demand.
struct HelpNode {
const char* id; // must match the id of the W_HELP or W_HTML node in layout_table
const char* html; // HTML content — any valid HTML, no size limit
};Add an entry whose id matches the W_HELP
node id you declared in the layout table:
const HelpNode help_table[] = {
{"help_humidity",
"<b>Humidity A</b><br>"
"AM2302 sensor on GPIO 2.<br>"
"Range: 0–100% RH.<br>"
"Updates every ~7 seconds."},
};
const int help_count = sizeof(help_table) / sizeof(HelpNode);The id field "help_humidity" matches the
W_HELP node id declared in the layout table.
That is the only connection between them.
You can use any HTML inside the string — bold, line breaks, lists, inline styles. The tooltip renders inside a positioned div with the framework’s dark theme CSS already applied.
Inline HTML with W_HTML
For descriptive text that appears directly in the card rather than
behind a hover, use W_HTML instead:
// In layout_table:
{"sensor_card_info", "sensor_card", "", "", W_HTML, ""},
// In help_table:
{"sensor_card_info",
"<p><strong>Outdoor Conditions</strong></p>"
"<p>Live readings from the <strong>AM2302</strong> sensor on GPIO 2. "
"Temperature and humidity are polled every 6–7 seconds.</p>"
"<p style='color:#888;font-size:.85em'>Range: -40 to 80°C • 0–100% RH</p>"},Place the W_HTML node at the bottom of the card’s
children to have it appear below the sensor readings.
Complete Example
Here is the complete definition for the humidity sensor across all three tables:
app_register_items()
SENSOR_AUTO("humidity_a", "Humidity A", 7003, "%", readAM2302a_humidity);layout_table[]
{"sensor_card", "main_page", "Live Sensors", "", W_CARD, ""},
{"w_humidity_a", "sensor_card","Humidity A", "humidity_a", W_BAR, "min:0,max:100"},
{"help_humidity", "sensor_card","", "", W_HELP, ""},help_table[]
{"help_humidity",
"<b>Humidity A</b><br>AM2302 sensor on GPIO 2.<br>"
"Range: 0–100% RH.<br>Updates every ~7 seconds."},That is the complete definition of a sensor that reads hardware on a 7-second timer, displays as a scaled bar graph, and shows a hover tooltip with technical details. The framework handles all HTML generation, CSS, JavaScript data binding, and live updates automatically.
What Happens at Boot
When the device starts, it runs through a resolution phase before the web server starts:
registry.begin()callsapp_register_items()and stores all sensors and controls in the registry array.setupLayoutResolution()walkslayout_table[]and resolves everyregistry_idstring to a numeric array index. This is the only moment string lookups happen. All runtime access is O(1) array indexing.- If a
registry_idin the layout table doesn’t match any registry entry, the error is logged immediately to the serial port with the node id and the bad string. The device boots anyway — the broken node is skipped. - Page endpoints are registered dynamically — one URL per
W_PAGEnode found in the layout table.
You will see output like this on the serial monitor during boot:
>> Registry Begin: Item Count: 10
>> [Layout] Boot resolution starting...
>> [Layout] Total nodes to resolve: 22
>> [Layout] node[0] id='root' parent='(root)' widget=0 container=YES
>> [Layout] node[6] id='w_humidity_a' parent='sensor_card' 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
If you see [Layout ERROR] — check the
registry_id in your layout node against the ID
in your SENSOR_AUTO or SENSOR_MANUAL macro.
They must match exactly, including case.
Quick Reference
To add a new sensor:
- Write the callback function
- Add
SENSOR_AUTO(...)inapp_register_items() - Add a widget node in
layout_table[]withregistry_idmatching step 2 - Optionally add a
W_HELPnode immediately after it inlayout_table[] - Optionally add a matching entry in
help_table[]
To move a sensor to a different page or card:
Change the parent_id in its layout node. Nothing else
changes.
To change how a sensor is displayed:
Change the widget type in its layout node. Add or modify
props for scaling.
To add a new page:
Add a W_PAGE node as a child of "root". Add
cards as children of that page. The framework registers the URL and adds
the nav tab automatically.
To add a new card to an existing page:
Add a W_CARD node with the page as its
parent_id. Add widget nodes as children of the card.
No comments:
Post a Comment