8 Commits
25 changed files with 490 additions and 100 deletions
+2
View File
@@ -18,6 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: bash -n scripts/*.sh
- run: ./scripts/check-docs.sh
compile-tests:
runs-on: ubuntu-latest
@@ -32,3 +33,4 @@ jobs:
python-version: '3.11'
- run: python -m pip install --upgrade platformio==6.1.19
- run: ./scripts/test.sh compile --platform ${{ matrix.platform }}
- run: ./scripts/test.sh examples --platform ${{ matrix.platform }}
+2
View File
@@ -18,7 +18,9 @@ jobs:
python-version: '3.11'
- run: python -m pip install --upgrade platformio==6.1.19
- run: ./scripts/test.sh compile --platform esp8266
- run: ./scripts/test.sh examples --platform esp8266
- run: ./scripts/test.sh compile --platform esp32
- run: ./scripts/test.sh examples --platform esp32
- run: ./scripts/check-docs.sh
- run: ./scripts/prepare-release.sh "$GITHUB_REF_NAME"
- run: ./scripts/release-notes.sh "$GITHUB_REF_NAME" > "$RUNNER_TEMP/release-notes.md"
+12
View File
@@ -1,5 +1,17 @@
# Changelog
## 1.2.1
- Clarify the safe shared-response and fixed-slot borrowed-response helpers so
embedded web clients can choose the ownership model that matches their
response lifetime.
- Refresh the package documentation and canonical PlatformIO installation tag.
## 1.2.0
- Add a borrowed async-response helper for fixed, caller-owned response slots. It avoids a per-request `shared_ptr` control allocation while releasing the slot safely on completion or disconnect.
- Add callback-enabled safe shared response helpers so dynamic contexts can return a bounded owner permit at the same time as their shared state is released.
## 1.1.0
- Replace layout-affecting compile-time storage overrides with per-context,
+30 -38
View File
@@ -1,25 +1,18 @@
# Device Framework Template Engine
# DeviceFramework Template Engine (DFTE)
DFTE streams HTML and text for ESP8266 and ESP32 without constructing a giant response in RAM. It combines PROGMEM templates, live getters, nested layouts, conditions, and iterators with request-safe asynchronous HTTP rendering.
DFTE streams HTML and text from ESP8266 and ESP32 firmware without constructing a complete response in RAM. Store layouts in PROGMEM, resolve changing values through getters, and render fixed-size chunks directly to serial or an asynchronous HTTP response.
## Why use it
- **Predictable memory:** render fixed-size chunks instead of one large `String`.
- **Reusable templates:** keep layouts, partials, CSS, and HTML in flash.
- **Live device data:** resolve values from getters only when a chunk is rendered.
- **Safe async responses:** use one render context per HTTP request while sharing one prepared registry.
## Try it
## Render a first template
```cpp
#include <TemplateEngine.h>
static const char PAGE[] PROGMEM = "<h1>%TITLE%</h1>";
Serial.begin(115200);
PlaceholderRegistry registry;
TemplateContext context;
void setup() {
Serial.begin(115200);
registry.registerProgmemData(PSTR("%TITLE%"), PSTR("Hello DFTE"));
registry.registerProgmemTemplate(PSTR("%PAGE%"), PAGE);
context.setRegistry(&registry);
@@ -27,44 +20,43 @@ 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);
}
```
## Featured examples
Build [Hello Placeholder](examples/HelloPlaceholder/) to see the rendered result over serial.
| Goal | Example |
## Choose an example
| Example | What you will build |
| --- | --- |
| Smallest placeholder render | [HelloPlaceholder](examples/HelloPlaceholder/) |
| Layouts, partials, and conditions | [NestedLayouts](examples/NestedLayouts/) |
| Async HTTP streaming | [StreamingAsync](examples/StreamingAsync/) |
| Dashboard, iterators, and telemetry | [AsyncDashboardDemo](examples/AsyncDashboardDemo/) |
| [Hello Placeholder](examples/HelloPlaceholder/) | the smallest registry, context, and serial-rendering flow |
| [Nested Layouts](examples/NestedLayouts/) | reusable partials, conditions, and iterator sections |
| [Streaming Async](examples/StreamingAsync/) | a streamed ESPAsyncWebServer response over a SoftAP |
| [Async Dashboard Demo](examples/AsyncDashboardDemo/) | a live dashboard with iterator rows and captive-portal access |
## Why DFTE
- **Bounded response memory:** render fixed-size chunks instead of allocating one large `String`.
- **Flash-resident layouts:** keep templates, CSS, and shared fragments in PROGMEM.
- **Live values:** resolve dynamic information only as the active response needs it.
- **Async-safe rendering:** each HTTP request owns its rendering context while sharing the prepared registry.
## Install
```ini
lib_deps =
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.1.0
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.2.1
```
PlatformIO checks out the Git ref after `#`; GitHub Release assets are unrelated. DFTE’s supported release targets are ESP8266 and ESP32.
PlatformIO checks out the Git ref after `#`; GitHub Release assets are unrelated. DFTE supports ESP8266 and ESP32 Arduino projects.
## Documentation
Read the [documentation index](docs/README.md) for template syntax, async web responses, per-context memory sizing, examples, tests, and releases.
## Development and releases
```bash
./scripts/test.sh compile --platform esp8266
./scripts/test.sh compile --platform esp32
./scripts/check-docs.sh
./scripts/prepare-release.sh vMAJOR.MINOR.PATCH --tag
```
Tagging repeats the board-free compile checks, validates the package, and creates a GitHub Release from the matching changelog section. It does not publish to the PlatformIO Registry or deploy firmware.
See the [changelog](CHANGELOG.md) and [licence](LICENSE).
See [getting started](docs/GETTING_STARTED.md), the [documentation index](docs/README.md), [examples](examples/README.md), [changelog](CHANGELOG.md), and [licence](LICENSE).
+59 -2
View File
@@ -1,6 +1,11 @@
# 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 |
| --- | --- |
| A request-owned dynamic render context | `beginSafeTemplateResponse()` |
| A fixed response-slot pool owned by the application | `beginBorrowedChunkedResponse()` |
```cpp
#include <TemplateEngine.h>
@@ -12,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
@@ -24,6 +29,58 @@ void sendTemplate(AsyncWebServerRequest* request, const char* root) {
The response retains the context while it streams. Releasing the request-owned `shared_ptr` on disconnect prevents state from leaking into later requests.
## Bounded fixed slots
Use `beginBorrowedChunkedResponse()` when the application already owns a small, fixed response pool. It does not allocate a `shared_ptr` control block for the request. The release callback must be idempotent because it can run once when rendering finishes and again if the connection later disconnects.
```cpp
struct ResponseSlot {
bool busy = false;
uint32_t lease = 0;
TemplateContext context;
};
ResponseSlot slots[2];
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;
}
void sendBoundedTemplate(AsyncWebServerRequest* request, const char* root) {
ResponseSlot* slot = nullptr;
for (auto& candidate : slots) {
if (!candidate.busy) {
slot = &candidate;
break;
}
}
if (slot == nullptr) {
request->send(503, "text/plain", "Busy");
return;
}
slot->busy = true;
const uint32_t lease = ++slot->lease;
slot->context.setRegistry(registry.get());
TemplateRenderer::initializeContext(slot->context, root);
request->send(TemplateEngineAsyncWeb::beginBorrowedChunkedResponse(
request, "text/html; charset=utf-8", slot,
[](ResponseSlot& state, uint8_t* out, size_t size, size_t) {
return TemplateEngineAsyncWeb::renderTemplateChunkWithRetries(
state.context, out, size, 128);
},
[](const ResponseSlot& state) {
return TemplateEngineAsyncWeb::isTemplateTerminal(state.context);
},
[lease](ResponseSlot& state) { releaseSlot(state, lease); }));
}
```
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.
Back to [documentation](README.md) · [project overview](../README.md).
+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.
}
}
```
+35 -1
View File
@@ -2,12 +2,46 @@
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.
Before a release, update `library.json`, `CHANGELOG.md`, and the relevant guides, then run:
## 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
./scripts/bump-version.sh vMAJOR.MINOR.PATCH
# Replace the generated CHANGELOG TODO with the release summary.
./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);
}
```
+2 -10
View File
@@ -1,6 +1,4 @@
# DFTE documentation
DFTE documentation is organised by the rendering problem being solved.
# DeviceFramework Template Engine (DFTE) documentation
| I want to… | Read |
| --- | --- |
@@ -8,13 +6,7 @@ DFTE documentation is organised by the rendering problem being solved.
| Use placeholders, templates, conditions, dynamic fragments, or iterators | [Template language](TEMPLATE_LANGUAGE.md) |
| Stream a response through ESPAsyncWebServer | [Async web responses](ASYNC_WEB.md) |
| Change buffer, stack, or placeholder limits safely | [Configuration](CONFIGURATION.md) |
| Build the standalone examples | [Examples](../examples/README.md) |
| Build, flash, and observe a complete example | [Examples](../examples/README.md) |
| Run checks or prepare a release | [Testing](TESTING.md) · [Development](DEVELOPMENT.md) |
## Documentation rules
- The template-language guide owns DFTE semantics and lifecycle contracts.
- The configuration guide owns ABI-sensitive build flags.
- The project README is an entry point and example chooser, not a full API manual.
Back to the [project overview](../README.md).
+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.
}
+13 -9
View File
@@ -1,22 +1,26 @@
# DFTE examples
Every example is a standalone PlatformIO project. Build, upload, and monitor from its directory:
Every example is a standalone PlatformIO project. Build, upload, and monitor from the repository root:
```bash
pio run -d examples/HelloPlaceholder -e example_esp8266
pio run -d examples/HelloPlaceholder -e example_esp8266 -t upload
pio run -d examples/HelloPlaceholder -e example_esp8266 -t monitor
pio device monitor -d examples/HelloPlaceholder -e example_esp8266
```
Choose the ESP32 environment where provided.
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.
| Example | What it demonstrates |
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/) | Smallest registry/context/chunk flow over serial |
| [NestedLayouts](NestedLayouts/) | Templates, partials, conditionals, and iterators without a web server |
| [StreamingAsync](StreamingAsync/) | Request-scoped streaming HTTP response through a SoftAP portal |
| [AsyncDashboardDemo](AsyncDashboardDemo/) | Dashboard telemetry, iterator rows, and captive-portal flow |
| [HelloPlaceholder](HelloPlaceholder/) | understand the smallest registry/context/chunk flow over serial |
| [NestedLayouts](NestedLayouts/) | compose a page with partials, conditions, and repeating data |
| [StreamingAsync](StreamingAsync/) | serve one streamed page through an asynchronous web server |
| [AsyncDashboardDemo](AsyncDashboardDemo/) | inspect a richer browser-facing dashboard without buffering the response |
The SoftAP examples print their network names and passwords to serial. They are demonstrations, not production provisioning implementations.
The SoftAP examples print their network names and passwords to serial. They are self-contained browser demonstrations, not Wi-Fi provisioning implementations.
Back to [DFTE documentation](../docs/README.md) · [project overview](../README.md).
+13 -9
View File
@@ -1,20 +1,24 @@
# 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:
This ESPAsyncWebServer example exposes a captive portal over a SoftAP and streams a DFTE template directly to each HTTP response. It demonstrates:
- 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
- one request-scoped `TemplateContext` per response;
- a PROGMEM page with shared CSS/header/footer snippets;
- runtime getters for uptime and connected-station count;
- a captive-portal DNS redirect for browsers that support it.
## Build
```
```bash
pio run -d examples/StreamingAsync -e example_esp8266
See the shared [examples guide](../README.md) and [async web guidance](../../docs/ASYNC_WEB.md).
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/`.
## Run
1. Flash your target with `pio run -d examples/StreamingAsync -e <env> -t upload --upload-port <port>`.
2. Open the serial monitor at 115200 baud and confirm the SoftAP credentials.
3. Connect to `DFTE-Portal-8266` or `DFTE-Portal-ESP32` using password `dfte-demo`.
4. Browse to `http://192.168.4.1/` if the captive portal does not open automatically.
See the shared [examples guide](../README.md) and [async web guidance](../../docs/ASYNC_WEB.md).
+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
+116
View File
@@ -85,6 +85,103 @@ AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request,
});
}
// A bounded embedded server can own response state in a fixed slot instead of
// allocating a shared control block for every request. The caller guarantees
// that state remains valid until release() is called. release() must be safe
// to call more than once because a completed request can later disconnect.
template <typename StateT, typename FillFn, typename IsDoneFn, typename ReleaseFn>
AsyncWebServerResponse* beginBorrowedChunkedResponse(AsyncWebServerRequest* request,
const char* contentType,
StateT* state,
FillFn fill,
IsDoneFn isDone,
ReleaseFn release) {
request->onDisconnect([state, release]() mutable {
if (state != nullptr) {
release(*state);
}
});
return request->beginChunkedResponse(contentType,
[state, fill, isDone, release](uint8_t* buffer, size_t maxLen, size_t index) mutable -> size_t {
if (state == nullptr) {
return 0;
}
if (isDone(*state)) {
release(*state);
return 0;
}
if (maxLen == 0) {
return RESPONSE_TRY_AGAIN;
}
size_t written = fill(*state, buffer, maxLen, index);
if (written == RESPONSE_TRY_AGAIN) {
return RESPONSE_TRY_AGAIN;
}
if (written > 0) {
return written;
}
if (isDone(*state)) {
release(*state);
return 0;
}
yieldForChunkRetry();
return RESPONSE_TRY_AGAIN;
});
}
// Variant for dynamically allocated state when the owner also needs an
// idempotent completion notification (for example, returning a bounded web
// response permit). The original overload intentionally remains available for
// callers that only need shared ownership.
template <typename StateT, typename FillFn, typename IsDoneFn, typename ReleaseFn>
AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request,
const char* contentType,
const std::shared_ptr<StateT>& sharedState,
FillFn fill,
IsDoneFn isDone,
ReleaseFn release) {
request->onDisconnect([state = sharedState, release]() mutable {
release();
state.reset();
});
return request->beginChunkedResponse(contentType,
[state = sharedState, fill, isDone, release](uint8_t* buffer, size_t maxLen, size_t index) mutable -> size_t {
if (!state) {
return 0;
}
if (maxLen == 0) {
return RESPONSE_TRY_AGAIN;
}
size_t written = fill(*state, buffer, maxLen, index);
if (written == RESPONSE_TRY_AGAIN) {
return RESPONSE_TRY_AGAIN;
}
if (written > 0) {
return written;
}
if (isDone(*state)) {
release();
state.reset();
return 0;
}
yieldForChunkRetry();
return RESPONSE_TRY_AGAIN;
});
}
template <typename StateT, typename FillFn, typename IsDoneFn>
AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request,
const String& contentType,
@@ -128,6 +225,25 @@ AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request,
});
}
template <typename ContextT, typename ReleaseFn>
AsyncWebServerResponse* beginSafeTemplateResponse(AsyncWebServerRequest* request,
const char* contentType,
const std::shared_ptr<ContextT>& sharedContext,
unsigned maxNoProgressRetries,
ReleaseFn release) {
return beginSafeChunkedResponse(
request,
contentType,
sharedContext,
[maxNoProgressRetries](ContextT& context, uint8_t* buffer, size_t maxLen, size_t /*index*/) -> size_t {
return renderTemplateChunkWithRetries(context, buffer, maxLen, maxNoProgressRetries);
},
[](const ContextT& context) -> bool {
return isTemplateTerminal(context);
},
release);
}
template <typename ContextT>
AsyncWebServerResponse* beginSafeTemplateResponse(AsyncWebServerRequest* request,
const char* contentType,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "DeviceFrameworkTemplateEngine",
"version": "1.1.0",
"version": "1.2.1",
"description": "Memory-efficient streaming template engine for ESP8266/ESP32 with chunked rendering support. Designed for embedded web interfaces with PROGMEM template support.",
"keywords": [
"template",
+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
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 vMAJOR.MINOR.PATCH" >&2
exit 2
}
tag="${1:-}"
[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
version="${tag#v}"
repo_url="https://github.com/alexhopeoconnor/DFTE.git"
reference_files=(README.md)
current_version="$(sed -n 's/.*"version": "\([^"]*\)".*/\1/p' "$root/library.json" | head -n 1)"
[[ "$current_version" != "$version" ]] || {
echo "library.json already declares $version; choose a new version." >&2
exit 1
}
grep -q "^## $version$" "$root/CHANGELOG.md" && {
echo "CHANGELOG.md already has a $version section; choose a new version." >&2
exit 1
}
sed -i -E '0,/"version": "[0-9]+\.[0-9]+\.[0-9]+"/s//"version": "'"$version"'"/' "$root/library.json"
for file in "${reference_files[@]}"; do
sed -i -E "s|${repo_url}#v[0-9]+\.[0-9]+\.[0-9]+|${repo_url}#v${version}|g" "$root/$file"
done
temp_file="$(mktemp)"
trap 'rm -f "$temp_file"' EXIT
{
IFS= read -r changelog_heading < "$root/CHANGELOG.md"
[[ "$changelog_heading" == "# Changelog" ]] || {
echo "CHANGELOG.md must begin with # Changelog" >&2
exit 1
}
printf '%s\n\n## %s\n\n- TODO: Describe this release.\n' "$changelog_heading" "$version"
tail -n +2 "$root/CHANGELOG.md"
} > "$temp_file"
mv "$temp_file" "$root/CHANGELOG.md"
echo "Updated DFTE declarations and canonical install references to $tag."
echo "Replace the generated changelog TODO with the release summary, then run scripts/check-docs.sh and scripts/prepare-release.sh $tag."
+47
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,4 +51,25 @@ 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; }
done
find "$example/src" -type f \( -name '*.ino' -o -name '*.cpp' \) -print -quit | grep -q . || {
echo "Incomplete example: ${example#$root/} has no source" >&2
exit 1
}
done < <(find "$root/examples" -mindepth 2 -maxdepth 2 -type f -name platformio.ini -printf '%h\n' | sort)
while IFS= read -r markdown; do
(( $(grep -Ec '^[[:space:]]*```' "$markdown") % 2 == 0 )) || {
echo "Unclosed code fence in ${markdown#$root/}" >&2
exit 1
}
done < <(find "$root" -path "$root/.git" -prune -o -path '*/.pio' -prune -o -name '*.md' -type f -print)
echo "Documentation links and required files passed"
+19 -1
View File
@@ -32,12 +32,30 @@ if ! grep -q "^## ${version}$" "$root/CHANGELOG.md"; then
exit 1
fi
if awk -v heading="## $version" '
$0 == heading { found = 1; next }
found && /^## / { exit }
found { print }
' "$root/CHANGELOG.md" | grep -Fq 'TODO: Describe this release.'; then
echo "CHANGELOG.md still has the generated TODO for $version" >&2
exit 1
fi
repo_url="https://github.com/alexhopeoconnor/DFTE.git"
validate_reference() {
local file="$1"
local reference_count
reference_count="$(grep -F "$repo_url#v" "$root/$file" | wc -l)"
[[ "$reference_count" -eq 1 ]] || { echo "$file must contain exactly one canonical release reference" >&2; exit 1; }
grep -Fq "$repo_url#$tag" "$root/$file" || { echo "$file does not reference $tag" >&2; exit 1; }
}
validate_reference README.md
git -C "$root" diff --check
package_dir="$(mktemp -d)"
trap 'rm -rf "$package_dir"' EXIT
pio pkg pack "$root" --output "$package_dir/package.tar.gz" >/dev/null
echo "Validated PlatformIO package for $tag"
echo "Validated release metadata and PlatformIO package for $tag"
if [[ "${2:-}" == "--tag" ]]; then
git -C "$root" diff --quiet
+45 -7
View File
@@ -2,18 +2,56 @@
set -euo pipefail
usage() {
echo "Usage: $0 compile --platform esp8266|esp32" >&2
echo "Usage: $0 compile|examples --platform esp8266|esp32" >&2
exit 2
}
[[ "${1:-}" == "compile" && "${2:-}" == "--platform" && $# -eq 3 ]] || usage
[[ $# -eq 3 && ( "${1:-}" == "compile" || "${1:-}" == "examples" ) && "${2:-}" == "--platform" ]] || usage
case "${3:-}" in
esp8266) environment="test_template_engine_8266" ;;
esp32) environment="test_template_engine_esp32" ;;
esp8266|esp32) platform="$3" ;;
*) usage ;;
esac
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
pio test -d "$root" -e "$environment" --without-uploading --without-testing
echo "DFTE compile check passed for ${3}"
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_for_platform test -d "$root" -e "$test_environment" --without-uploading --without-testing
echo "DFTE compile check passed for $platform"
exit 0
;;
esac
suffix="$platform"
mapfile -t examples < <(find "$root/examples" -mindepth 2 -maxdepth 2 -type f -name platformio.ini -printf '%h\n' | sort)
if (( ${#examples[@]} == 0 )); then
echo "No example projects found" >&2
exit 1
fi
for example in "${examples[@]}"; do
env_name="example_$suffix"
[[ "$(basename "$example")" == "AsyncDashboardDemo" ]] && env_name="dashboard_$suffix"
pio_for_platform run -d "$example" -e "$env_name" </dev/null
done
echo "DFTE examples compile check passed for $platform"