This document consolidates all runtime (live) topics: providers, layout, runtime fields, alarms, styling metadata, and global CSS theming.
Runtime values are served by the Web UI using:
/runtime.jsonfor live values/runtime_meta.jsonfor field metadata/live_layout.jsonfor the optional page/card/group layout/user_theme.cssfor optional global CSS
A runtime provider fills a JsonObject when /runtime.json is requested.
ConfigManager.getRuntime().addRuntimeProvider("sensors", [](JsonObject &o) {
o["temp"] = readTemp();
o["hum"] = readHumidity();
o["dew"] = calcDewPoint();
}, 10);Keep providers fast and non-blocking.
The classic API still works and is useful for quick fields:
// Numeric
ConfigManager.defineRuntimeField("sensors", "temp", "Temperature", "C", 1);
// Numeric with thresholds
ConfigManager.defineRuntimeFieldThresholds(
"sensors", "temp", "Temperature", "C", 1,
1.0f, 30.0f,
0.0f, 32.0f,
true, true, true, true,
10);
// Boolean
ConfigManager.defineRuntimeBool("flags", "heater_on", "Heater On", false);
// String (dynamic)
ConfigManager.defineRuntimeString("system", "fw", "Firmware", CONFIGMANAGER_VERSION);
// String (static)
ConfigManager.defineRuntimeString("system", "build", "Build", "2025-09-30", 5);
// Divider
ConfigManager.defineRuntimeDivider("sensors", "Environment", 0);Field ordering uses order (lower first). Default is 100.
| Key | Meaning |
|---|---|
isString |
Render as plain text |
isDivider |
Render a divider line |
staticValue |
Value shown even without runtime value |
order |
Sort hint |
Layout is optional.
- No pages defined -> no tabs, cards are derived from providers.
- Pages defined -> tabs are enabled.
- Card is the H3 title.
- Group is optional and only inside a card (not recursive).
- If group is omitted, items render directly inside the card.
The builder lets you define layout and fields in one flow.
auto live = ConfigManager.liveGroup("sensors")
.page("Sensors", 10)
.card("BME280 - Temperature Sensor");
live.value("temp", []() { return temperature; })
.label("Temperature")
.unit("C")
.precision(1)
.order(10);auto dew = ConfigManager.liveGroup("sensors")
.page("Sensors", 10)
.card("BME280 - Temperature Sensor")
.group("Dewpoint", 20);
dew.value("dew", []() { return dewPoint; })
.label("Dewpoint")
.unit("C")
.precision(1)
.order(20);value(key, getter)for numeric/string/bool (auto type)value(getter)auto key (field_#) for quick demosboolValue(key, getter)with alarm semanticsdivider(label, order)button,checkbox,stateButton,momentaryButtonintSlider,floatSliderintInput,floatInput
Auto keys are generated in creation order; use explicit keys for stable CSS/automation.
Use AlarmManager for computed alarms and place them inside the layout.
cm::AlarmManager alarmManager;
alarmManager.addDigitalWarning({
.id = "dewRisk",
.name = "Dewpoint Risk",
.kind = cm::AlarmKind::DigitalActive,
.severity = cm::AlarmSeverity::Warning,
.enabled = true,
.getter = []() { return temperature < dewPoint; },
});
alarmManager.addWarningToLive(
"dewRisk",
30,
"Sensors",
"BME280 - Temperature Sensor",
"Dewpoint",
"Dewpoint Risk");Call alarmManager.update() periodically (every 1-3s is typical).
If you use defineRuntimeAlarm, call handleRuntimeAlarms() periodically:
ConfigManager.defineRuntimeAlarm(
"dewpoint_risk",
[](const JsonObject &root) {
if (!root.containsKey("sensors")) return false;
auto s = root["sensors"].as<JsonObject>();
if (!s.containsKey("temp") || !s.containsKey("dew")) return false;
return (s["temp"].as<float>() - s["dew"].as<float>()) <= 5.0f;
},
[]() { Serial.println("[I] dewpoint_risk ENTER"); },
[]() { Serial.println("[I] dewpoint_risk EXIT"); }
);
ConfigManager.handleRuntimeAlarms();Some subsystems (for example IOManager) expose dynamic thresholds.
When thresholds change at runtime, expose explicit boolean flags to reflect alarm state:
<key>_alarm_min<key>_alarm_max
The UI will use those flags to decide whether the numeric value is in alarm state.
Runtime fields can ship style metadata for specific targets:
rowlabelvaluesunitstatestateDotOnTruestateDotOnFalsestateDotOnAlarm
Example:
auto style = ConfigManagerClass::defaultNumericStyle(true);
style.rule("label").set("color", "#d00000").set("fontWeight", "700");
style.rule("values").set("color", "#0b3d91").set("fontWeight", "700");
style.rule("unit").set("color", "#0b3d91").set("fontWeight", "700");
ConfigManager.defineRuntimeFieldThresholds(
"sensors", "temp", "Temperature", "C", 1,
1.0f, 30.0f,
0.0f, 32.0f,
true, true, true, true,
10,
style);Hide an element:
style.rule("state").setVisible(false);The default overload applies the class to row + label + value + unit.
live.value("temp", []() { return temperature; })
.addCSSClass("myTempClass");Targeted overload:
live.value("temp", []() { return temperature; })
.addCSSClass("myLabelClass", ConfigManagerClass::LiveCssTarget::Label)
.addCSSClass("myDotAlarm", ConfigManagerClass::LiveCssTarget::DotOnAlarm);Available targets:
RowLabelValueUnitStateDotDotOnTrueDotOnFalseDotOnAlarm
You can serve a custom stylesheet from the device.
extern const char userTheme[] PROGMEM;
ConfigManager.setCustomCss(userTheme, strlen_P(userTheme));Disable metadata if you want CSS-only control:
ConfigManager.disableRuntimeStyleMeta(true);Runtime rows expose:
data-group(provider)data-key(field key)data-type(numeric/bool/string)data-state(safe/warn/alarm/on/off)
.rw[data-group="sensors"][data-key="temp"][data-state="alarm"] .val {
color: #ff2222;
font-weight: 700;
}You can override theme variables in /user_theme.css:
:root {
--cm-bg: #f6f7f9;
--cm-fg: #111827;
--cm-card-bg: #ffffff;
--cm-card-border: rgba(0,0,0,.06);
--cm-group-bg: rgba(17,24,39,.03);
--cm-group-border: rgba(0,0,0,.08);
--cm-group-title: #374151;
}ConfigManager.enableWebSocketPush();
ConfigManager.handleWebsocketPush();The UI prefers WebSocket and falls back to polling /runtime.json.
The default push interval is 5000 ms; explicit values are clamped to
550..60000 ms.
Provide a custom runtime JSON payload:
ConfigManager.setCustomLivePayloadBuilder([]() {
DynamicJsonDocument d(256);
d["uptime"] = millis();
d["heap"] = ESP.getFreeHeap();
String out;
serializeJson(d, out);
return out;
});You can trigger the same popup overlay used by validation:
ConfigManager.sendWarnMessage(
"Pump overheating",
"Outlet temperature above 70C",
"Automatic cooling disabled",
ConfigManagerClass::GUIMessageButtons::OkCancelRetry,
[]() { Serial.println("[I] Cooling engaged"); },
[]() { Serial.println("[I] User cancelled"); },
[]() { Serial.println("[I] User requested retry"); },
[](JsonObject &ctx) { ctx["runtimeProvider"] = "cooling"; }
);- Provider fill functions should be fast and non-blocking.
- Style metadata adds a small JSON overhead.
- Global CSS is cached by the browser.
- Open
/runtime.jsonand verify live values. - Open
/runtime_meta.jsonand verify field meta + style. - Open
/live_layout.jsonand verify pages/cards/groups. - Open
/user_theme.cssand verify CSS output. - Inspect a
.rwelement in dev tools for data attributes.
| Method | Overloads / Variants | Description | Notes |
|---|---|---|---|
ConfigManager.addRuntimeProvider |
addRuntimeProvider(const RuntimeValueProvider& provider)addRuntimeProvider(const String& name, std::function<void(JsonObject&)> fillFunc, int order = 100) |
Registers runtime data providers for the Live UI. | Provider callbacks should stay non-blocking. |
ConfigManager.enableWebSocketPush |
enableWebSocketPush(uint32_t intervalMs = 5000) |
Enables push updates for runtime/live data. | UI falls back to polling when push is disabled; intervals are clamped to 550..60000 ms. |
ConfigManager.setCustomLivePayloadBuilder |
setCustomLivePayloadBuilder(std::function<String()> fn) |
Replaces default runtime payload generation with custom JSON. | Advanced customization hook. |
ConfigManager.sendWarnMessage |
sendWarnMessage(...) |
Shows runtime warning dialog with optional callbacks/context. | Use for operator-visible runtime events. |