Add examples and expand README for release

This commit is contained in:
2025-11-09 13:11:26 +10:00
parent 8351a13d82
commit 2f220c32ea
15 changed files with 1251 additions and 10 deletions
+23
View File
@@ -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
+298
View File
@@ -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();
}
+17
View File
@@ -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.
+18
View File
@@ -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
+51
View File
@@ -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(&registry);
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.
}
+18
View File
@@ -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.
+18
View File
@@ -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
+213
View File
@@ -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.
}
+18
View File
@@ -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/`.
+33
View File
@@ -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
+159
View File
@@ -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>&copy; 2025 DFTE Examples</small>
</footer>
)HTML";
} // namespace DFTEExamples