docs: improve async guidance and examples

This commit is contained in:
2026-09-17 09:11:29 +10:00
parent 065aed893d
commit 42cb8f2324
9 changed files with 93 additions and 21 deletions
+8 -3
View File
@@ -20,10 +20,15 @@ void setup() {
}
void loop() {
uint8_t chunk[128];
if (!TemplateRenderer::isComplete(context) && !TemplateRenderer::hasError(context)) {
Serial.write(chunk, TemplateRenderer::renderNextChunk(context, chunk, sizeof(chunk)));
if (TemplateRenderer::hasError(context)) {
// Stop or report the invalid root/placeholder; completion alone is not success.
return;
}
if (TemplateRenderer::isComplete(context)) return;
uint8_t chunk[128];
const size_t written = TemplateRenderer::renderNextChunk(context, chunk, sizeof(chunk));
if (written > 0) Serial.write(chunk, written);
}
```
+9 -5
View File
@@ -1,6 +1,6 @@
# Async web responses
Build a `PlaceholderRegistry` once during setup, but allocate a fresh `TemplateContext` for each request. A shared context would mix rendering state when clients overlap.
Build a `PlaceholderRegistry` once during setup, but allocate a fresh `TemplateContext` for each request. A shared context would mix rendering state when clients overlap. The handler below assumes setup has populated the long-lived `registry`; see [StreamingAsync](../examples/StreamingAsync/) for the complete project.
| Need | Use |
| --- | --- |
@@ -17,8 +17,8 @@ void sendTemplate(AsyncWebServerRequest* request, const char* root) {
auto context = std::make_shared<TemplateContext>();
context->setRegistry(registry.get());
TemplateRenderer::initializeContext(*context, root);
request->onDisconnect([context]() mutable { context.reset(); });
// The helper retains the request-owned context until the response completes or disconnects.
AsyncWebServerResponse* response =
TemplateEngineAsyncWeb::beginSafeTemplateResponse(
request, "text/html; charset=utf-8", context, 128
@@ -36,12 +36,15 @@ Use `beginBorrowedChunkedResponse()` when the application already owns a small,
```cpp
struct ResponseSlot {
bool busy = false;
uint32_t lease = 0;
TemplateContext context;
};
ResponseSlot slots[2];
void releaseSlot(ResponseSlot& slot) {
void releaseSlot(ResponseSlot& slot, uint32_t lease) {
// A completed response can disconnect after this slot has been reused.
if (!slot.busy || slot.lease != lease) return;
slot.context.reset();
slot.busy = false;
}
@@ -60,6 +63,7 @@ void sendBoundedTemplate(AsyncWebServerRequest* request, const char* root) {
}
slot->busy = true;
const uint32_t lease = ++slot->lease;
slot->context.setRegistry(registry.get());
TemplateRenderer::initializeContext(slot->context, root);
request->send(TemplateEngineAsyncWeb::beginBorrowedChunkedResponse(
@@ -71,11 +75,11 @@ void sendBoundedTemplate(AsyncWebServerRequest* request, const char* root) {
[](const ResponseSlot& state) {
return TemplateEngineAsyncWeb::isTemplateTerminal(state.context);
},
releaseSlot));
[lease](ResponseSlot& state) { releaseSlot(state, lease); }));
}
```
Use this form only while the slot itself has static or otherwise guaranteed lifetime. Use the `shared_ptr` form above for a request-owned dynamic context.
Use this form only while the slot itself has static or otherwise guaranteed lifetime. The lease makes a late disconnect from an old response a no-op after the slot has been reused. Use the `shared_ptr` form above for a request-owned dynamic context.
Use [StreamingAsync](../examples/StreamingAsync/) for a complete SoftAP/captive-portal project.
+8 -4
View File
@@ -5,10 +5,14 @@ stack and read buffer at runtime, so choosing capacities cannot make a consumer
and the compiled library disagree about object size.
```cpp
TemplateContext standard; // default: 16 stack frames, 512-byte buffer
TemplateContext pageContext(6, 128); // known shallow response, smaller allocation
if (!pageContext.isReady()) {
// Allocation failed; do not start a response with this context.
// Keep each context alive for the full response that uses it.
TemplateContext standard; // Default: 16 stack frames, 512-byte buffer.
TemplateContext pageContext(6, 128); // Known shallow response, smaller allocation.
void setup() {
if (!standard.isReady() || !pageContext.isReady()) {
// Allocation failed; do not start a response with either context.
}
}
```
+15
View File
@@ -2,6 +2,19 @@
Released applications should use a public Git tag. While changing DFTE with a sibling library, select a local `symlink://` or `file://` dependency from an ignored PlatformIO override rather than changing tracked application dependencies.
## Target pins
The ESP32 test and example environments pin pioarduino `51.03.05`, which
selects Arduino-ESP32 3.0.5 / ESP-IDF 5.1.4+. This is a fixture contract, not a
DFTE package dependency: a consuming application owns its `platform` pin and
tests the complete framework/toolchain stack. The ESP8266 environments pin
framework commit `521ae60` for the upstream Postmortem large-jump linker fix;
the exact rationale and update rule are in the shared [ESP8266
linker-workaround note](https://github.com/alexhopeoconnor/arduino-home-assistant/blob/main/docs/ESP8266-LINKER-WORKAROUND.md).
For the pioarduino release-to-Core mapping and the narrow repair for a stale
global PlatformIO tool package, see [DeviceFramework's toolchain guide](https://github.com/alexhopeoconnor/DeviceFramework/blob/main/docs/TOOLCHAINS.md).
Start a release with `bump-version.sh`. It updates package metadata and canonical installation snippets, then creates the changelog section. Replace its generated TODO with the release summary and update any behavioural documentation before running:
```bash
@@ -10,6 +23,8 @@ Start a release with `bump-version.sh`. It updates package metadata and canonica
./scripts/check-docs.sh
./scripts/test.sh compile --platform esp8266
./scripts/test.sh compile --platform esp32
./scripts/test.sh examples --platform esp8266
./scripts/test.sh examples --platform esp32
./scripts/prepare-release.sh vMAJOR.MINOR.PATCH --tag
```
+8 -4
View File
@@ -27,11 +27,15 @@ void setup() {
}
void loop() {
uint8_t chunk[128];
if (!TemplateRenderer::isComplete(context) && !TemplateRenderer::hasError(context)) {
const size_t written = TemplateRenderer::renderNextChunk(context, chunk, sizeof(chunk));
Serial.write(chunk, written);
if (TemplateRenderer::hasError(context)) {
// Report the invalid root or placeholder once in production, then stop this response.
return;
}
if (TemplateRenderer::isComplete(context)) return;
uint8_t chunk[128];
const size_t written = TemplateRenderer::renderNextChunk(context, chunk, sizeof(chunk));
if (written > 0) Serial.write(chunk, written);
}
```
+3 -1
View File
@@ -5,9 +5,11 @@ The two PlatformIO Unity commands compile the complete DFTE test suites without
```bash
./scripts/test.sh compile --platform esp8266
./scripts/test.sh compile --platform esp32
./scripts/test.sh examples --platform esp8266
./scripts/test.sh examples --platform esp32
```
The test environments include the library sources with `test_build_src = yes`. CI runs both target checks on the maintained branch and pull requests.
The first two commands compile the library test suites with `test_build_src = yes`. The example commands compile every standalone project on each target, protecting the code that the documentation links users to. CI runs the library target checks on the maintained branch and pull requests.
The standalone examples are buildable PlatformIO projects; see [examples](../examples/README.md).
+6 -2
View File
@@ -37,15 +37,19 @@ void setup() {
while (!TemplateRenderer::isComplete(ctx) && !TemplateRenderer::hasError(ctx)) {
size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer));
if (!written) {
Serial.println(F("\nRendering stalled before completion."));
break;
}
Serial.write(buffer, written);
}
Serial.println(F("\nRendering complete."));
if (TemplateRenderer::hasError(ctx)) {
Serial.println(F("\nRendering failed."));
} else if (TemplateRenderer::isComplete(ctx)) {
Serial.println(F("\nRendering complete."));
}
}
void loop() {
// Nothing else to do in the basic example.
}
+6 -2
View File
@@ -196,11 +196,16 @@ void renderToSerial() {
while (!TemplateRenderer::isComplete(ctx) && !TemplateRenderer::hasError(ctx)) {
size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer));
if (!written) {
Serial.println(F("\nRendering stalled before completion."));
break;
}
Serial.write(buffer, written);
}
Serial.println();
if (TemplateRenderer::hasError(ctx)) {
Serial.println(F("\nRendering failed."));
} else if (TemplateRenderer::isComplete(ctx)) {
Serial.println();
}
}
} // namespace
@@ -218,4 +223,3 @@ void setup() {
void loop() {
// Nothing to do in loop for this example.
}
+30
View File
@@ -9,6 +9,32 @@ required=(
examples/README.md
)
check_cpp_fence_scope() {
local markdown="$1"
awk '
function brace_delta(line, copy) {
copy = line
return gsub(/\{/, "{", copy) - gsub(/\}/, "}", copy)
}
/^```cpp[[:space:]]*$/ { in_cpp = 1; depth = 0; next }
in_cpp && /^```[[:space:]]*$/ { in_cpp = 0; next }
in_cpp {
line = $0
sub(/^[[:space:]]+/, "", line)
if (depth == 0 &&
(line ~ /^(if|for|while|switch)[[:space:]]*\(/ ||
line ~ /^[A-Za-z_][A-Za-z0-9_:]*::[A-Za-z0-9_]+[[:space:]]*\(/ ||
line ~ /^[A-Za-z_][A-Za-z0-9_]*\./ ||
line ~ /^[A-Za-z_][A-Za-z0-9_]*[[:space:]]*\(/)) {
printf "%s:%d: C++ expression appears at namespace scope; wrap it in a function.\n", FILENAME, FNR > "/dev/stderr"
failed = 1
}
depth += brace_delta($0)
}
END { exit failed }
' "$markdown"
}
for path in "${required[@]}"; do
[[ -f "$root/$path" ]] || { echo "Missing required documentation: $path" >&2; exit 1; }
done
@@ -25,6 +51,10 @@ while IFS= read -r -d '' markdown; do
done < <(sed -nE 's/.*\]\(([^ )]+)( "[^"]*")?\).*/\1/p' "$markdown")
done < <(find "$root" -path "$root/.git" -prune -o -path '*/.pio' -prune -o -name '*.md' -type f -print0)
while IFS= read -r markdown; do
check_cpp_fence_scope "$markdown"
done < <(find "$root" -path "$root/.git" -prune -o -path '*/.pio' -prune -o -name '*.md' -type f -print)
while IFS= read -r example; do
for required in README.md platformio.ini; do
[[ -f "$example/$required" ]] || { echo "Incomplete example: ${example#$root/} is missing $required" >&2; exit 1; }