diff --git a/README.md b/README.md index 0dc2226..4b30310 100644 --- a/README.md +++ b/README.md @@ -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); } ``` diff --git a/docs/ASYNC_WEB.md b/docs/ASYNC_WEB.md index eb4a937..85e4352 100644 --- a/docs/ASYNC_WEB.md +++ b/docs/ASYNC_WEB.md @@ -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(); 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. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 3d2ad6f..56bd764 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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. + } } ``` diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d815511..4114854 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -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 ``` diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index cedd976..1870d34 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -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); } ``` diff --git a/docs/TESTING.md b/docs/TESTING.md index 53fe972..99a21ad 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -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). diff --git a/examples/HelloPlaceholder/src/main.cpp b/examples/HelloPlaceholder/src/main.cpp index 842a4ee..3159bc3 100644 --- a/examples/HelloPlaceholder/src/main.cpp +++ b/examples/HelloPlaceholder/src/main.cpp @@ -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. } - diff --git a/examples/NestedLayouts/src/main.cpp b/examples/NestedLayouts/src/main.cpp index 0c2c012..05e10fb 100644 --- a/examples/NestedLayouts/src/main.cpp +++ b/examples/NestedLayouts/src/main.cpp @@ -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. } - diff --git a/scripts/check-docs.sh b/scripts/check-docs.sh index f68db6d..3ad67c0 100755 --- a/scripts/check-docs.sh +++ b/scripts/check-docs.sh @@ -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; }