mirror of
https://github.com/alexhopeoconnor/DFTE.git
synced 2026-10-04 04:28:10 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aa53c08d9 | ||
|
|
85d34e60c5 | ||
|
|
ec4c32db32 | ||
|
|
42cb8f2324 | ||
|
|
065aed893d | ||
|
|
dbbb29484a | ||
|
|
a09f9e331a | ||
|
|
ff33de6293 |
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(®istry);
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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).
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Executable
+46
@@ -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."
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user