4 Commits
Author SHA1 Message Date
alex 4aa53c08d9 build: share maintained ESP32 cache 2026-09-20 19:43:40 +10:00
alex 85d34e60c5 build: standardize ESP32 3.3.11 test baseline 2026-09-18 02:41:40 +10:00
alex ec4c32db32 ci: add current ESP32 validation lane 2026-09-17 12:10:53 +10:00
alex 42cb8f2324 docs: improve async guidance and examples 2026-09-17 09:11:29 +10:00
16 changed files with 155 additions and 34 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.
}
}
```
+32
View File
@@ -2,6 +2,36 @@
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
DFTE uses one maintained ESP32 fixture baseline:
| Test selector | pioarduino platform | Framework stack | Purpose |
| --- | --- | --- | --- |
| `esp32` | `55.03.311` | Arduino-ESP32 3.3.11 / ESP-IDF 5.5.5 | Maintained baseline |
These are test-harness fixture lanes, not DFTE package dependencies: a consuming
application owns its `platform` pin and tests the complete framework/toolchain
stack. Do not copy a compiler or toolchain package between lanes; each pinned
pioarduino platform resolves its matched framework, uploader, and toolchain.
All DFTE ESP8266 test and example 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 cache-collision diagnosis, see
[DeviceFramework's toolchain guide](https://github.com/alexhopeoconnor/DeviceFramework/blob/main/docs/TOOLCHAINS.md).
`./scripts/test.sh` uses the persistent PlatformIO Core/cache shared by the
maintained framework repositories:
`${XDG_CACHE_HOME:-$HOME/.cache}/arduino-framework-platformio/core-3.3.11`.
All maintained ESP32 lanes pin this exact graph, avoiding repeated downloads
while keeping stale global `tool-esptoolpy` metadata from shadowing the current
pioarduino uploader. Override it with `DFTE_PLATFORMIO_CORE_DIR`,
`DFTE_PLATFORMIO_PACKAGES_DIR`, and `DFTE_PLATFORMIO_CACHE_DIR` for another
disk; the script never clears that cache or pins one compiler separately.
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 +40,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);
}
```
+12 -2
View File
@@ -1,13 +1,23 @@
# Testing
The two PlatformIO Unity commands compile the complete DFTE test suites without uploading or executing them, so they require no attached board.
The PlatformIO Unity commands compile the complete DFTE test suites without uploading or executing them, so they require no attached board.
```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.
`esp32` uses pioarduino `55.03.311` (Arduino-ESP32 3.3.11 / ESP-IDF 5.5.5).
The `compile` commands compile the complete 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 every listed target lane on the
maintained branch and pull requests.
The ESP32 lane uses a persistent dedicated Core/cache directory so the
package-form Arduino-ESP32 uploader cannot inherit stale global Python
metadata. The test script never clears that cache.
The standalone examples are buildable PlatformIO projects; see [examples](../examples/README.md).
+1 -1
View File
@@ -24,7 +24,7 @@ build_flags =
-I$PROJECT_LIBDEPS_DIR/$PIOENV/ESPAsyncTCP/src
[env:dashboard_esp32]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
board = esp32dev
lib_deps =
${env.lib_deps}
+1 -2
View File
@@ -15,6 +15,5 @@ platform_packages =
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
[env:example_esp32]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
board = esp32dev
+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.
}
+1 -2
View File
@@ -15,6 +15,5 @@ platform_packages =
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
[env:example_esp32]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
board = esp32dev
+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.
}
+4
View File
@@ -10,6 +10,10 @@ pio device monitor -d examples/HelloPlaceholder -e example_esp8266
Choose the corresponding ESP32 environment where provided. The examples use the checked-out DFTE source, so they double as practical integration checks for this repository.
The `example_esp32` / `dashboard_esp32` environments use Arduino-ESP32 3.3.11.
A consuming application should select and pin its own complete PlatformIO
platform stack.
| Example | Start here when you want to… |
| --- | --- |
| [HelloPlaceholder](HelloPlaceholder/) | understand the smallest registry/context/chunk flow over serial |
+1 -2
View File
@@ -24,7 +24,7 @@ build_flags =
-I$PROJECT_LIBDEPS_DIR/$PIOENV/ESPAsyncTCP/src
[env:example_esp32]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
board = esp32dev
lib_deps =
${env.lib_deps}
@@ -34,4 +34,3 @@ build_flags =
-DSOC_WIFI_SUPPORTED=1
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
-I$PROJECT_LIBDEPS_DIR/$PIOENV/AsyncTCP/src
+4 -1
View File
@@ -9,7 +9,10 @@ test_framework = unity
test_build_src = yes
[env:test_template_engine_esp32]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
; Arduino-ESP32 3.3.11 / ESP-IDF 5.5.5 is the maintained ESP32 baseline.
; Pin the complete pioarduino platform so its framework and toolchain stay
; matched; PlatformIO must not select one from an unrelated package cache.
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
board = nodemcu-32s
framework = arduino
test_framework = unity
+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; }
+24 -4
View File
@@ -13,15 +13,35 @@ case "${3:-}" in
esac
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [[ "$1" == "compile" ]]; then
pio_for_platform() {
if [[ "$platform" != "esp32" ]]; then
pio "$@"
return
fi
# Keep the maintained Core 3.3.11 package-form uploader in one persistent,
# repository-owned cache. This avoids unrelated global package metadata;
# it is never cleared by this script.
local core_dir packages_dir cache_dir
core_dir="${DFTE_PLATFORMIO_CORE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/arduino-framework-platformio/core-3.3.11}"
packages_dir="${DFTE_PLATFORMIO_PACKAGES_DIR:-$core_dir/packages}"
cache_dir="${DFTE_PLATFORMIO_CACHE_DIR:-$core_dir/cache}"
install -d -m 700 "$core_dir" "$packages_dir" "$cache_dir"
PLATFORMIO_CORE_DIR="$core_dir" PLATFORMIO_PACKAGES_DIR="$packages_dir" \
PLATFORMIO_CACHE_DIR="$cache_dir" pio "$@"
}
case "$1" in
compile)
case "$platform" in
esp8266) test_environment="test_template_engine_8266" ;;
esp32) test_environment="test_template_engine_esp32" ;;
esac
pio test -d "$root" -e "$test_environment" --without-uploading --without-testing
pio_for_platform test -d "$root" -e "$test_environment" --without-uploading --without-testing
echo "DFTE compile check passed for $platform"
exit 0
fi
;;
esac
suffix="$platform"
mapfile -t examples < <(find "$root/examples" -mindepth 2 -maxdepth 2 -type f -name platformio.ini -printf '%h\n' | sort)
@@ -32,6 +52,6 @@ fi
for example in "${examples[@]}"; do
env_name="example_$suffix"
[[ "$(basename "$example")" == "AsyncDashboardDemo" ]] && env_name="dashboard_$suffix"
pio run -d "$example" -e "$env_name" </dev/null
pio_for_platform run -d "$example" -e "$env_name" </dev/null
done
echo "DFTE examples compile check passed for $platform"