mirror of
https://github.com/alexhopeoconnor/DFTE.git
synced 2026-10-22 08:42:08 +10:00
Add examples and expand README for release
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# Async Dashboard Demo
|
||||
|
||||
Full-featured DFTE demo that combines nested templates, iterators, and a captive portal served via ESPAsyncWebServer. The device exposes its own SoftAP so multiple users can connect and view the dashboard without additional infrastructure.
|
||||
|
||||
Highlights:
|
||||
- SoftAP + DNS captive portal (`DFTE-Dashboard-8266` or `DFTE-Dashboard-ESP32`, password `dfte-dashboard`)
|
||||
- Aggregated metrics section populated from RAM getters (uptime, device count, client count)
|
||||
- Iterator-driven device table with per-entry overrides
|
||||
- Streaming HTTP responses to avoid buffering the entire page in RAM
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
pio run -d examples/AsyncDashboardDemo -e dashboard_esp8266
|
||||
pio run -d examples/AsyncDashboardDemo -e dashboard_esp32
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
1. Flash your target board (`pio run -d examples/AsyncDashboardDemo -e <env> -t upload --upload-port <port>`).
|
||||
2. Open the serial monitor at 115200 baud to confirm the SoftAP credentials.
|
||||
3. Connect to the advertised network and browse to `http://192.168.4.1/`. The captive portal should automatically redirect; if not, navigate manually.
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
[platformio]
|
||||
default_envs = dashboard_esp8266
|
||||
|
||||
[env]
|
||||
framework = arduino
|
||||
build_flags =
|
||||
-O2
|
||||
lib_extra_dirs = ../../
|
||||
lib_deps =
|
||||
DFTE=symlink://../../
|
||||
ESP32Async/ESPAsyncWebServer
|
||||
monitor_speed = 115200
|
||||
|
||||
[env:dashboard_esp8266]
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
ESP32Async/ESPAsyncTCP
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-I$PROJECT_LIBDEPS_DIR/$PIOENV/ESPAsyncTCP/src
|
||||
|
||||
[env:dashboard_esp32]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
ESP32Async/AsyncTCP
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-I$PROJECT_LIBDEPS_DIR/$PIOENV/AsyncTCP/src
|
||||
@@ -0,0 +1,298 @@
|
||||
#include <Arduino.h>
|
||||
#include <TemplateEngine.h>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <DNSServer.h>
|
||||
|
||||
#if defined(ESP32)
|
||||
#include <WiFi.h>
|
||||
#include <AsyncTCP.h>
|
||||
#elif defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESPAsyncTCP.h>
|
||||
#endif
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
#include "../../common/templates/LayoutSnippets.h"
|
||||
|
||||
AsyncWebServer server(80);
|
||||
DNSServer dnsServer;
|
||||
std::shared_ptr<PlaceholderRegistry> registry;
|
||||
|
||||
constexpr byte DNS_PORT = 53;
|
||||
constexpr const char* AP_PASSWORD = "dfte-dashboard";
|
||||
#if defined(ESP32)
|
||||
constexpr const char* AP_SSID = "DFTE-Dashboard-ESP32";
|
||||
#else
|
||||
constexpr const char* AP_SSID = "DFTE-Dashboard-8266";
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Template definitions (kept in PROGMEM to reduce RAM usage)
|
||||
// ---------------------------------------------------------------------------
|
||||
static const char PROGMEM kLayoutTemplate[] = R"HTML(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>%PAGE_TITLE%</title>
|
||||
<style>%GLOBAL_CSS%</style>
|
||||
</head>
|
||||
<body>
|
||||
%HEADER%
|
||||
<main>
|
||||
<section class="meta">
|
||||
<h2>Overview</h2>
|
||||
<dl>
|
||||
<dt>Connected Clients</dt><dd>%CLIENT_COUNT%</dd>
|
||||
<dt>Uptime</dt><dd>%UPTIME%</dd>
|
||||
<dt>Device Entries</dt><dd>%DEVICE_COUNT%</dd>
|
||||
</dl>
|
||||
</section>
|
||||
<section class="devices">
|
||||
<h2>Devices</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Status</th><th>Last Seen</th></tr>
|
||||
</thead>
|
||||
<tbody>%DEVICE_ROWS%</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
%FOOTER%
|
||||
</body>
|
||||
</html>
|
||||
)HTML";
|
||||
|
||||
static const char PROGMEM kDeviceRowTemplate[] = R"HTML(
|
||||
<tr class="%STATUS_CLASS%">
|
||||
<td>%DEVICE_NAME%</td>
|
||||
<td>%DEVICE_STATUS%</td>
|
||||
<td>%DEVICE_LAST_SEEN%</td>
|
||||
</tr>
|
||||
)HTML";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock data sources (replace with your own business logic)
|
||||
// ---------------------------------------------------------------------------
|
||||
struct DeviceInfo {
|
||||
const char* name;
|
||||
const char* status;
|
||||
const char* statusClass;
|
||||
const char* lastSeen;
|
||||
};
|
||||
|
||||
static DeviceInfo kDevices[] = {
|
||||
{"Living Room Light", "Online", "ok", "5s ago"},
|
||||
{"Garage Door", "Warning", "warn", "18s ago"},
|
||||
{"Garden Pump", "Offline", "error", "2m ago"}
|
||||
};
|
||||
static constexpr size_t kDeviceCount = sizeof(kDevices) / sizeof(DeviceInfo);
|
||||
|
||||
static String uptimeBuffer;
|
||||
static String clientCountBuffer;
|
||||
static String deviceCountBuffer;
|
||||
|
||||
const char* getPageTitle() { return "DFTE Dashboard"; }
|
||||
const char* getTagline() { return "Rendered chunk-by-chunk from flash + dynamic data"; }
|
||||
|
||||
const char* getDeviceCount() {
|
||||
deviceCountBuffer = String(kDeviceCount);
|
||||
return deviceCountBuffer.c_str();
|
||||
}
|
||||
|
||||
const char* getUptime() {
|
||||
uptimeBuffer = String(millis() / 1000) + "s";
|
||||
return uptimeBuffer.c_str();
|
||||
}
|
||||
|
||||
const char* getClientCount() {
|
||||
#if defined(ESP32)
|
||||
clientCountBuffer = String(WiFi.softAPgetStationNum());
|
||||
#else
|
||||
clientCountBuffer = String(WiFi.softAPgetStationNum());
|
||||
#endif
|
||||
return clientCountBuffer.c_str();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Iterator wiring for %DEVICE_ROWS%
|
||||
// ---------------------------------------------------------------------------
|
||||
struct DeviceIteratorState {
|
||||
size_t index;
|
||||
};
|
||||
|
||||
DeviceIteratorState gIteratorState;
|
||||
static PlaceholderEntry gDeviceOverrides[kDeviceCount][4];
|
||||
static bool gOverridesInitialized = false;
|
||||
|
||||
void initializeDeviceOverrides() {
|
||||
if (gOverridesInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* names[4] = {
|
||||
"%DEVICE_NAME%", "%DEVICE_STATUS%", "%STATUS_CLASS%", "%DEVICE_LAST_SEEN%"
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < kDeviceCount; ++i) {
|
||||
const char* values[4] = {
|
||||
kDevices[i].name,
|
||||
kDevices[i].status,
|
||||
kDevices[i].statusClass,
|
||||
kDevices[i].lastSeen
|
||||
};
|
||||
|
||||
for (size_t field = 0; field < 4; ++field) {
|
||||
PlaceholderEntry& entry = gDeviceOverrides[i][field];
|
||||
memset(entry.name, 0, sizeof(entry.name));
|
||||
strncpy(entry.name, names[field], sizeof(entry.name) - 1);
|
||||
entry.type = PlaceholderType::PROGMEM_DATA;
|
||||
entry.data = values[field];
|
||||
entry.getLength = DeviceFrameworkPlaceholderRegistry::getProgmemLength;
|
||||
}
|
||||
}
|
||||
|
||||
gOverridesInitialized = true;
|
||||
}
|
||||
|
||||
void* deviceIteratorOpen(void* userData) {
|
||||
auto* state = static_cast<DeviceIteratorState*>(userData);
|
||||
state->index = 0;
|
||||
initializeDeviceOverrides();
|
||||
return state;
|
||||
}
|
||||
|
||||
IteratorStepResult deviceIteratorNext(void* handle, IteratorItemView& view) {
|
||||
auto* state = static_cast<DeviceIteratorState*>(handle);
|
||||
if (state->index >= kDeviceCount) {
|
||||
return IteratorStepResult::COMPLETE;
|
||||
}
|
||||
|
||||
view.templateData = kDeviceRowTemplate;
|
||||
view.templateLength = strlen_P(kDeviceRowTemplate);
|
||||
view.templateIsProgmem = true;
|
||||
view.placeholders = gDeviceOverrides[state->index];
|
||||
view.placeholderCount = 4;
|
||||
|
||||
state->index++;
|
||||
return IteratorStepResult::ITEM_READY;
|
||||
}
|
||||
|
||||
void deviceIteratorClose(void*) {}
|
||||
|
||||
IteratorDescriptor gDeviceIterator = {
|
||||
.open = deviceIteratorOpen,
|
||||
.next = deviceIteratorNext,
|
||||
.close = deviceIteratorClose,
|
||||
.userData = &gIteratorState
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registry + rendering helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
void initialiseRegistry(PlaceholderRegistry& registryRef) {
|
||||
registryRef.clear();
|
||||
|
||||
// Static assets and nested templates
|
||||
registryRef.registerProgmemData("%GLOBAL_CSS%", DFTEExamples::SHARED_CSS);
|
||||
registryRef.registerProgmemTemplate("%HEADER%", DFTEExamples::SHARED_HEADER);
|
||||
registryRef.registerProgmemTemplate("%FOOTER%", DFTEExamples::SHARED_FOOTER);
|
||||
|
||||
// Header content
|
||||
registryRef.registerRamData("%PAGE_TITLE%", getPageTitle);
|
||||
registryRef.registerRamData("%TAGLINE%", getTagline);
|
||||
|
||||
// Overview metrics
|
||||
registryRef.registerRamData("%CLIENT_COUNT%", getClientCount);
|
||||
registryRef.registerRamData("%UPTIME%", getUptime);
|
||||
registryRef.registerRamData("%DEVICE_COUNT%", getDeviceCount);
|
||||
|
||||
// Iterator to populate the device table
|
||||
registryRef.registerIterator("%DEVICE_ROWS%", &gDeviceIterator);
|
||||
}
|
||||
|
||||
void streamTemplate(AsyncWebServerRequest* request,
|
||||
std::shared_ptr<PlaceholderRegistry> registryPtr,
|
||||
const char* rootTemplate) {
|
||||
auto ctx = std::make_shared<TemplateContext>();
|
||||
ctx->setRegistry(registryPtr.get());
|
||||
TemplateRenderer::initializeContext(*ctx, rootTemplate);
|
||||
|
||||
request->onDisconnect([ctx]() mutable {
|
||||
ctx.reset();
|
||||
});
|
||||
|
||||
AsyncWebServerResponse* response = request->beginChunkedResponse(
|
||||
"text/html; charset=utf-8",
|
||||
[ctx](uint8_t* buffer, size_t maxLen, size_t /*index*/) mutable -> size_t {
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t written = TemplateRenderer::renderNextChunk(*ctx, buffer, maxLen);
|
||||
|
||||
if (!written || TemplateRenderer::isComplete(*ctx) || TemplateRenderer::hasError(*ctx)) {
|
||||
ctx.reset();
|
||||
}
|
||||
|
||||
return written;
|
||||
});
|
||||
|
||||
response->addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
response->addHeader("Pragma", "no-cache");
|
||||
response->addHeader("Expires", "-1");
|
||||
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void handleDashboard(AsyncWebServerRequest* request) {
|
||||
if (!registry) {
|
||||
registry = std::make_shared<PlaceholderRegistry>(24);
|
||||
initialiseRegistry(*registry);
|
||||
}
|
||||
streamTemplate(request, registry, kLayoutTemplate);
|
||||
}
|
||||
|
||||
void startCaptivePortal() {
|
||||
WiFi.mode(WIFI_AP);
|
||||
IPAddress apIP(192, 168, 4, 1);
|
||||
IPAddress netmask(255, 255, 255, 0);
|
||||
WiFi.softAPConfig(apIP, apIP, netmask);
|
||||
WiFi.softAP(AP_SSID, AP_PASSWORD);
|
||||
dnsServer.start(DNS_PORT, "*", apIP);
|
||||
|
||||
Serial.println(F("Captive portal active"));
|
||||
Serial.print(F("SSID: "));
|
||||
Serial.println(AP_SSID);
|
||||
Serial.print(F("Password: "));
|
||||
Serial.println(AP_PASSWORD);
|
||||
Serial.print(F("Portal IP: "));
|
||||
Serial.println(apIP);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Arduino entry points
|
||||
// ---------------------------------------------------------------------------
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial) { /* wait */ }
|
||||
|
||||
Serial.println();
|
||||
Serial.println(F("=== DFTE Async Dashboard Demo ==="));
|
||||
|
||||
registry = std::make_shared<PlaceholderRegistry>(24);
|
||||
initialiseRegistry(*registry);
|
||||
startCaptivePortal();
|
||||
|
||||
server.on("/", HTTP_GET, handleDashboard);
|
||||
server.onNotFound(handleDashboard);
|
||||
server.begin();
|
||||
|
||||
Serial.println(F("Dashboard available at http://192.168.4.1/"));
|
||||
}
|
||||
|
||||
void loop() {
|
||||
dnsServer.processNextRequest();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Hello Placeholder
|
||||
|
||||
This introductory sketch demonstrates the minimum DFTE setup:
|
||||
|
||||
- Create a `PlaceholderRegistry` and register simple RAM getters
|
||||
- Initialise a `TemplateContext` with a RAM template
|
||||
- Stream the rendered output into a Serial buffer
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
pio run -d examples/HelloPlaceholder -e example_esp8266
|
||||
pio run -d examples/HelloPlaceholder -e example_esp32
|
||||
```
|
||||
|
||||
Flash the firmware and open the serial monitor at 115200 baud to see the rendered text.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[platformio]
|
||||
default_envs = example_esp8266
|
||||
|
||||
[env]
|
||||
framework = arduino
|
||||
lib_extra_dirs = ../../
|
||||
lib_deps =
|
||||
DFTE=symlink://../../
|
||||
monitor_speed = 115200
|
||||
|
||||
[env:example_esp8266]
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
|
||||
[env:example_esp32]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#include <Arduino.h>
|
||||
#include <TemplateEngine.h>
|
||||
|
||||
namespace {
|
||||
|
||||
static String deviceName = "DFTE Getting Started";
|
||||
static String buildId = "v1.0.0";
|
||||
|
||||
const char SIMPLE_TEMPLATE[] = R"TPL(
|
||||
Device: %DEVICE_NAME%
|
||||
Build: %BUILD_ID%
|
||||
|
||||
DFTE lets you reuse this template across Serial, HTTP, or any other transport.
|
||||
)TPL";
|
||||
|
||||
const char* getDeviceName() { return deviceName.c_str(); }
|
||||
const char* getBuildId() { return buildId.c_str(); }
|
||||
|
||||
} // namespace
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial) { /* wait for USB CDC */ }
|
||||
|
||||
Serial.println();
|
||||
Serial.println(F("=== DFTE Hello Placeholder ==="));
|
||||
|
||||
PlaceholderRegistry registry;
|
||||
registry.registerRamData("%DEVICE_NAME%", getDeviceName);
|
||||
registry.registerRamData("%BUILD_ID%", getBuildId);
|
||||
|
||||
TemplateContext ctx;
|
||||
ctx.setRegistry(®istry);
|
||||
TemplateRenderer::initializeContext(ctx, SIMPLE_TEMPLATE, false);
|
||||
|
||||
uint8_t buffer[64];
|
||||
while (!TemplateRenderer::isComplete(ctx) && !TemplateRenderer::hasError(ctx)) {
|
||||
size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer));
|
||||
if (!written) {
|
||||
break;
|
||||
}
|
||||
Serial.write(buffer, written);
|
||||
}
|
||||
|
||||
Serial.println(F("\nRendering complete."));
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Nothing else to do in the basic example.
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Nested Layouts
|
||||
|
||||
Demonstrates nested templates, conditional placeholders, and iterator-driven sections without a web server dependency. Ideal for learning how DFTE composes complex layouts on memory-constrained boards.
|
||||
|
||||
Highlights:
|
||||
- Shared header/footer partials referenced via `%HEADER%` and `%FOOTER%`
|
||||
- Conditional placeholder toggling a "maintenance" banner
|
||||
- Iterator rendering a list of subsystem status entries with per-item overrides
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
pio run -d examples/NestedLayouts -e example_esp8266
|
||||
pio run -d examples/NestedLayouts -e example_esp32
|
||||
```
|
||||
|
||||
Open the serial monitor at 115200 baud to inspect the rendered output.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[platformio]
|
||||
default_envs = example_esp8266
|
||||
|
||||
[env]
|
||||
framework = arduino
|
||||
lib_extra_dirs = ../../
|
||||
lib_deps =
|
||||
DFTE=symlink://../../
|
||||
monitor_speed = 115200
|
||||
|
||||
[env:example_esp8266]
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
|
||||
[env:example_esp32]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
#include <Arduino.h>
|
||||
#include <TemplateEngine.h>
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
|
||||
#include "../../common/templates/LayoutSnippets.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct SubsystemStatus {
|
||||
const char* name;
|
||||
const char* detail;
|
||||
const char* severityClass;
|
||||
};
|
||||
|
||||
constexpr SubsystemStatus SUBSYSTEMS[] = {
|
||||
{"Wi-Fi", "Connected", "ok"},
|
||||
{"MQTT", "Disconnected", "warn"},
|
||||
{"Storage", "Healthy", "ok"},
|
||||
{"OTA", "Idle", "info"},
|
||||
};
|
||||
constexpr size_t SUBSYSTEM_COUNT = sizeof(SUBSYSTEMS) / sizeof(SubsystemStatus);
|
||||
|
||||
bool maintenanceMode = false;
|
||||
|
||||
// Templates
|
||||
inline const char PROGMEM BASE_TEMPLATE[] = R"HTML(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>%PAGE_TITLE%</title>
|
||||
<style>%CSS%</style>
|
||||
</head>
|
||||
<body>
|
||||
%HEADER%
|
||||
<section>
|
||||
<h2>Environment</h2>
|
||||
<p><strong>Firmware:</strong> %FIRMWARE_VERSION%</p>
|
||||
<p><strong>Boot Count:</strong> %BOOT_COUNT%</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Maintenance</h2>
|
||||
%MAINTENANCE_BANNER%
|
||||
</section>
|
||||
<section>
|
||||
<h2>Subsystems</h2>
|
||||
<ul class="subsystems">
|
||||
%SUBSYSTEM_LIST%
|
||||
</ul>
|
||||
</section>
|
||||
%FOOTER%
|
||||
</body>
|
||||
</html>
|
||||
)HTML";
|
||||
|
||||
inline const char PROGMEM SUBSYSTEM_ITEM_TEMPLATE[] = R"HTML(
|
||||
<li class="%SEVERITY%">
|
||||
<strong>%NAME%</strong>
|
||||
<span>%DETAIL%</span>
|
||||
</li>
|
||||
)HTML";
|
||||
|
||||
inline const char PROGMEM MAINTENANCE_TRUE_TEMPLATE[] = R"HTML(
|
||||
<div class="notice warn">System in maintenance mode. Automations disabled.</div>
|
||||
)HTML";
|
||||
|
||||
inline const char PROGMEM MAINTENANCE_FALSE_TEMPLATE[] = R"HTML(
|
||||
<div class="notice ok">All services operating normally.</div>
|
||||
)HTML";
|
||||
|
||||
// RAM data
|
||||
static String bootCountBuffer;
|
||||
static uint32_t bootCount = 42;
|
||||
|
||||
const char* getFirmwareVersion() { return "2.3.1"; }
|
||||
|
||||
const char* getBootCount() {
|
||||
bootCountBuffer = String(bootCount);
|
||||
return bootCountBuffer.c_str();
|
||||
}
|
||||
|
||||
// Conditional descriptor
|
||||
struct MaintenanceState {
|
||||
bool enabled;
|
||||
} maintenanceState{maintenanceMode};
|
||||
|
||||
ConditionalBranchResult evaluateMaintenance(void* userData) {
|
||||
auto* state = static_cast<MaintenanceState*>(userData);
|
||||
return state->enabled ? ConditionalBranchResult::TRUE_BRANCH : ConditionalBranchResult::FALSE_BRANCH;
|
||||
}
|
||||
|
||||
inline const ConditionalDescriptor MAINTENANCE_DESCRIPTOR = {
|
||||
evaluateMaintenance,
|
||||
"%MAINTENANCE_TRUE%",
|
||||
"%MAINTENANCE_FALSE%",
|
||||
(void*)&maintenanceState};
|
||||
|
||||
// Iterator plumbing
|
||||
struct SubsystemIteratorState {
|
||||
size_t index = 0;
|
||||
};
|
||||
|
||||
SubsystemIteratorState iteratorState;
|
||||
PlaceholderEntry subsystemOverrides[SUBSYSTEM_COUNT][3];
|
||||
|
||||
void initialiseOverrides() {
|
||||
for (size_t i = 0; i < SUBSYSTEM_COUNT; ++i) {
|
||||
const SubsystemStatus& status = SUBSYSTEMS[i];
|
||||
|
||||
PlaceholderEntry& name = subsystemOverrides[i][0];
|
||||
strncpy(name.name, "%NAME%", sizeof(name.name) - 1);
|
||||
name.type = PlaceholderType::RAM_DATA;
|
||||
name.data = status.name;
|
||||
name.getLength = DeviceFrameworkPlaceholderRegistry::getRamLength;
|
||||
|
||||
PlaceholderEntry& detail = subsystemOverrides[i][1];
|
||||
strncpy(detail.name, "%DETAIL%", sizeof(detail.name) - 1);
|
||||
detail.type = PlaceholderType::RAM_DATA;
|
||||
detail.data = status.detail;
|
||||
detail.getLength = DeviceFrameworkPlaceholderRegistry::getRamLength;
|
||||
|
||||
PlaceholderEntry& severity = subsystemOverrides[i][2];
|
||||
strncpy(severity.name, "%SEVERITY%", sizeof(severity.name) - 1);
|
||||
severity.type = PlaceholderType::RAM_DATA;
|
||||
severity.data = status.severityClass;
|
||||
severity.getLength = DeviceFrameworkPlaceholderRegistry::getRamLength;
|
||||
}
|
||||
}
|
||||
|
||||
void* iteratorOpen(void* userData) {
|
||||
initialiseOverrides();
|
||||
auto* state = static_cast<SubsystemIteratorState*>(userData);
|
||||
state->index = 0;
|
||||
return state;
|
||||
}
|
||||
|
||||
IteratorStepResult iteratorNext(void* handle, IteratorItemView& view) {
|
||||
auto* state = static_cast<SubsystemIteratorState*>(handle);
|
||||
if (state->index >= SUBSYSTEM_COUNT) {
|
||||
return IteratorStepResult::COMPLETE;
|
||||
}
|
||||
|
||||
view.templateData = SUBSYSTEM_ITEM_TEMPLATE;
|
||||
view.templateLength = strlen_P(SUBSYSTEM_ITEM_TEMPLATE);
|
||||
view.templateIsProgmem = true;
|
||||
view.placeholders = subsystemOverrides[state->index];
|
||||
view.placeholderCount = 3;
|
||||
state->index++;
|
||||
return IteratorStepResult::ITEM_READY;
|
||||
}
|
||||
|
||||
void iteratorClose(void*) {}
|
||||
|
||||
IteratorDescriptor iteratorDescriptor = {
|
||||
iteratorOpen,
|
||||
iteratorNext,
|
||||
iteratorClose,
|
||||
&iteratorState};
|
||||
|
||||
std::shared_ptr<PlaceholderRegistry> buildRegistry() {
|
||||
auto registry = std::make_shared<PlaceholderRegistry>();
|
||||
registry->registerProgmemData("%CSS%", DFTEExamples::SHARED_CSS);
|
||||
registry->registerProgmemTemplate("%HEADER%", DFTEExamples::SHARED_HEADER);
|
||||
registry->registerProgmemTemplate("%FOOTER%", DFTEExamples::SHARED_FOOTER);
|
||||
|
||||
registry->registerRamData("%PAGE_TITLE%", []() -> const char* { return "DFTE Nested Layouts"; });
|
||||
registry->registerRamData("%TAGLINE%", []() -> const char* { return "Composing templates with conditionals and iterators"; });
|
||||
registry->registerRamData("%FIRMWARE_VERSION%", getFirmwareVersion);
|
||||
registry->registerRamData("%BOOT_COUNT%", getBootCount);
|
||||
|
||||
registry->registerConditional("%MAINTENANCE_BANNER%", &MAINTENANCE_DESCRIPTOR);
|
||||
registry->registerProgmemTemplate("%MAINTENANCE_TRUE%", MAINTENANCE_TRUE_TEMPLATE);
|
||||
registry->registerProgmemTemplate("%MAINTENANCE_FALSE%", MAINTENANCE_FALSE_TEMPLATE);
|
||||
|
||||
registry->registerIterator("%SUBSYSTEM_LIST%", &iteratorDescriptor);
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
void renderToSerial() {
|
||||
auto registry = buildRegistry();
|
||||
TemplateContext ctx;
|
||||
ctx.setRegistry(registry.get());
|
||||
TemplateRenderer::initializeContext(ctx, BASE_TEMPLATE);
|
||||
|
||||
uint8_t buffer[128];
|
||||
while (!TemplateRenderer::isComplete(ctx) && !TemplateRenderer::hasError(ctx)) {
|
||||
size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer));
|
||||
if (!written) {
|
||||
break;
|
||||
}
|
||||
Serial.write(buffer, written);
|
||||
}
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial) { /* wait */ }
|
||||
|
||||
Serial.println();
|
||||
Serial.println(F("=== DFTE Nested Layouts Example ==="));
|
||||
|
||||
renderToSerial();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Nothing to do in loop for this example.
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Streaming Async
|
||||
|
||||
Small ESPAsyncWebServer demo that exposes a captive portal over a SoftAP and renders a DFTE template directly to the HTTP response stream. It highlights:
|
||||
|
||||
- Per-request `TemplateContext` ownership to avoid concurrent request clashes
|
||||
- PROGMEM template with shared CSS/header/footer snippets
|
||||
- Runtime data from RAM getters (uptime, connected station count)
|
||||
- Captive portal DNS redirect so clients automatically receive the dashboard
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
pio run -d examples/StreamingAsync -e example_esp8266
|
||||
pio run -d examples/StreamingAsync -e example_esp32
|
||||
```
|
||||
|
||||
Flash the board, connect to the SoftAP announced by the device (`DFTE-Portal-8266` or `DFTE-Portal-ESP32`, password `dfte-demo`), and your browser should automatically present the streamed dashboard. If it does not, navigate manually to `http://192.168.4.1/`.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
[platformio]
|
||||
default_envs = example_esp8266
|
||||
|
||||
[env]
|
||||
framework = arduino
|
||||
lib_extra_dirs = ../../
|
||||
lib_deps =
|
||||
DFTE=symlink://../../
|
||||
ESP32Async/ESPAsyncWebServer
|
||||
build_flags =
|
||||
-O2
|
||||
monitor_speed = 115200
|
||||
|
||||
[env:example_esp8266]
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
ESP32Async/ESPAsyncTCP
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-I$PROJECT_LIBDEPS_DIR/$PIOENV/ESPAsyncTCP/src
|
||||
|
||||
[env:example_esp32]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
ESP32Async/AsyncTCP
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-I$PROJECT_LIBDEPS_DIR/$PIOENV/AsyncTCP/src
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#include <Arduino.h>
|
||||
#include <TemplateEngine.h>
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
#include <DNSServer.h>
|
||||
|
||||
#if defined(ESP32)
|
||||
#include <WiFi.h>
|
||||
#include <AsyncTCP.h>
|
||||
#elif defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESPAsyncTCP.h>
|
||||
#endif
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
#include "../../common/templates/LayoutSnippets.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* AP_SSID =
|
||||
#if defined(ESP32)
|
||||
"DFTE-Portal-ESP32";
|
||||
#else
|
||||
"DFTE-Portal-8266";
|
||||
#endif
|
||||
constexpr const char* AP_PASSWORD = "dfte-demo";
|
||||
constexpr byte DNS_PORT = 53;
|
||||
|
||||
DNSServer dnsServer;
|
||||
|
||||
AsyncWebServer server(80);
|
||||
std::shared_ptr<PlaceholderRegistry> registry;
|
||||
|
||||
inline const char PROGMEM PAGE_TEMPLATE[] = R"HTML(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>%PAGE_TITLE%</title>
|
||||
<style>%CSS%</style>
|
||||
</head>
|
||||
<body>
|
||||
%HEADER%
|
||||
<section>
|
||||
<h2>Device Snapshot</h2>
|
||||
<p><strong>Uptime:</strong> %UPTIME%</p>
|
||||
<p><strong>Connected Clients:</strong> %CLIENT_COUNT%</p>
|
||||
</section>
|
||||
%FOOTER%
|
||||
</body>
|
||||
</html>
|
||||
)HTML";
|
||||
|
||||
String uptimeBuffer;
|
||||
String clientBuffer;
|
||||
|
||||
const char* getUptime() {
|
||||
uptimeBuffer = String(millis() / 1000) + "s";
|
||||
return uptimeBuffer.c_str();
|
||||
}
|
||||
|
||||
const char* getClientCount() {
|
||||
#if defined(ESP32)
|
||||
clientBuffer = String(WiFi.softAPgetStationNum());
|
||||
#else
|
||||
clientBuffer = String(WiFi.softAPgetStationNum());
|
||||
#endif
|
||||
return clientBuffer.c_str();
|
||||
}
|
||||
|
||||
void initialiseRegistry(PlaceholderRegistry& registryRef) {
|
||||
registryRef.clear();
|
||||
registryRef.registerProgmemData("%CSS%", DFTEExamples::SHARED_CSS);
|
||||
registryRef.registerProgmemTemplate("%HEADER%", DFTEExamples::SHARED_HEADER);
|
||||
registryRef.registerProgmemTemplate("%FOOTER%", DFTEExamples::SHARED_FOOTER);
|
||||
registryRef.registerRamData("%PAGE_TITLE%", []() -> const char* { return "DFTE Streaming Async"; });
|
||||
registryRef.registerRamData("%TAGLINE%", []() -> const char* { return "Chunked rendering with ESPAsyncWebServer"; });
|
||||
registryRef.registerRamData("%UPTIME%", getUptime);
|
||||
registryRef.registerRamData("%CLIENT_COUNT%", getClientCount);
|
||||
}
|
||||
|
||||
void streamTemplate(AsyncWebServerRequest* request,
|
||||
std::shared_ptr<PlaceholderRegistry> registryPtr,
|
||||
const char* tpl) {
|
||||
auto ctx = std::make_shared<TemplateContext>();
|
||||
ctx->setRegistry(registryPtr.get());
|
||||
TemplateRenderer::initializeContext(*ctx, tpl);
|
||||
|
||||
request->onDisconnect([ctx]() mutable {
|
||||
ctx.reset();
|
||||
});
|
||||
|
||||
AsyncWebServerResponse* response = request->beginChunkedResponse(
|
||||
"text/html; charset=utf-8",
|
||||
[ctx](uint8_t* buffer, size_t maxLen, size_t) mutable -> size_t {
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t written = TemplateRenderer::renderNextChunk(*ctx, buffer, maxLen);
|
||||
if (!written || TemplateRenderer::isComplete(*ctx) || TemplateRenderer::hasError(*ctx)) {
|
||||
ctx.reset();
|
||||
}
|
||||
return written;
|
||||
});
|
||||
|
||||
response->addHeader("Cache-Control", "no-cache");
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void handleRoot(AsyncWebServerRequest* request) {
|
||||
if (!registry) {
|
||||
registry = std::make_shared<PlaceholderRegistry>();
|
||||
initialiseRegistry(*registry);
|
||||
}
|
||||
streamTemplate(request, registry, PAGE_TEMPLATE);
|
||||
}
|
||||
|
||||
void startCaptivePortal() {
|
||||
WiFi.mode(WIFI_AP);
|
||||
IPAddress apIP(192, 168, 4, 1);
|
||||
IPAddress netMask(255, 255, 255, 0);
|
||||
WiFi.softAPConfig(apIP, apIP, netMask);
|
||||
WiFi.softAP(AP_SSID, AP_PASSWORD);
|
||||
|
||||
dnsServer.start(DNS_PORT, "*", apIP);
|
||||
|
||||
Serial.println(F("Captive portal active"));
|
||||
Serial.print(F("SSID: "));
|
||||
Serial.println(AP_SSID);
|
||||
Serial.print(F("Password: "));
|
||||
Serial.println(AP_PASSWORD);
|
||||
Serial.print(F("Portal IP: "));
|
||||
Serial.println(apIP);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial) { /* wait */ }
|
||||
|
||||
Serial.println();
|
||||
Serial.println(F("=== DFTE Streaming Async Example ==="));
|
||||
|
||||
registry = std::make_shared<PlaceholderRegistry>();
|
||||
initialiseRegistry(*registry);
|
||||
startCaptivePortal();
|
||||
|
||||
server.on("/", HTTP_GET, handleRoot);
|
||||
server.onNotFound(handleRoot);
|
||||
server.begin();
|
||||
Serial.println(F("HTTP server listening on port 80"));
|
||||
}
|
||||
|
||||
void loop() {
|
||||
dnsServer.processNextRequest();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace DFTEExamples {
|
||||
|
||||
// Shared CSS snippet stored in flash to minimise RAM usage.
|
||||
inline const char PROGMEM SHARED_CSS[] = R"CSS(
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 1.5rem;
|
||||
background: #f4f6f9;
|
||||
color: #222;
|
||||
}
|
||||
h1, h2 {
|
||||
color: #0a3d62;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
section {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
)CSS";
|
||||
|
||||
// Minimal header/footer fragments that examples can reuse.
|
||||
inline const char PROGMEM SHARED_HEADER[] = R"HTML(
|
||||
<header>
|
||||
<h1>%PAGE_TITLE%</h1>
|
||||
<p>%TAGLINE%</p>
|
||||
</header>
|
||||
)HTML";
|
||||
|
||||
inline const char PROGMEM SHARED_FOOTER[] = R"HTML(
|
||||
<footer>
|
||||
<small>© 2025 DFTE Examples</small>
|
||||
</footer>
|
||||
)HTML";
|
||||
|
||||
} // namespace DFTEExamples
|
||||
|
||||
Reference in New Issue
Block a user