feat: prepare DFTE 1.0.2

This commit is contained in:
2026-08-27 14:25:38 +10:00
parent 361803c2b3
commit 6a5b3aab9e
22 changed files with 361 additions and 309 deletions
+12 -2
View File
@@ -2,22 +2,32 @@ name: Build
on: on:
push: push:
branches: [main]
pull_request: pull_request:
concurrency:
group: build-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: permissions:
contents: read contents: read
jobs: jobs:
documentation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./scripts/check-docs.sh
compile-tests: compile-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
environment: ["test_template_engine_8266", "test_template_engine_esp32"] platform: [esp8266, esp32]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.11' python-version: '3.11'
- run: python -m pip install --upgrade platformio==6.1.19 - run: python -m pip install --upgrade platformio==6.1.19
- run: pio test -e ${{ matrix.environment }} --without-uploading --without-testing - run: ./scripts/test.sh compile --platform ${{ matrix.platform }}
+5 -1
View File
@@ -17,7 +17,11 @@ jobs:
with: with:
python-version: '3.11' python-version: '3.11'
- run: python -m pip install --upgrade platformio==6.1.19 - run: python -m pip install --upgrade platformio==6.1.19
- run: ./scripts/test.sh compile --platform esp8266
- run: ./scripts/test.sh compile --platform esp32
- run: ./scripts/check-docs.sh
- run: ./scripts/prepare-release.sh "$GITHUB_REF_NAME" - run: ./scripts/prepare-release.sh "$GITHUB_REF_NAME"
- run: gh release create "$GITHUB_REF_NAME" --generate-notes --title "$GITHUB_REF_NAME" - run: ./scripts/release-notes.sh "$GITHUB_REF_NAME" > "$RUNNER_TEMP/release-notes.md"
- run: gh release create "$GITHUB_REF_NAME" --title "DeviceFrameworkTemplateEngine $GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/release-notes.md"
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
+40 -303
View File
@@ -1,333 +1,70 @@
# Device Framework Template Engine (DFTE) # Device Framework Template Engine
DFTE is a lightweight C++ template engine for ESP8266 and ESP32 Arduino 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.
projects that need to render HTML dashboards or textual feeds without allocating
giant buffers. It streams HTML over chunked HTTP, stitches together nested
layouts from PROGMEM, and injects live device data from RAM getters while
keeping the microcontroller responsive.
The published package and CI support ESP8266 and ESP32. The code may be useful ## Why use it
on other Arduino-compatible targets, but those targets are not currently part
of the supported release matrix and should be validated by the consuming
project.
## Quickstart - **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.
1. Define your root template in PROGMEM (or RAM if you prefer): ## Try it
```
static const char ROOT_TEMPLATE_PROGMEM[] PROGMEM = R"DFTE(
<html>
<head><title>%APP_TITLE%</title></head>
<body>
<h1>%APP_TITLE%</h1>
<p>Uptime: %UPTIME%</p>
</body>
</html>
)DFTE";
```
2. In your sketch, register placeholders and stream them out:
```
#include <TemplateEngine.h>
PlaceholderRegistry registry;
TemplateContext ctx;
void setup() {
Serial.begin(115200);
registry.registerProgmemData(PSTR("%APP_TITLE%"), PSTR("DFTE Quickstart"));
registry.registerRamData(PSTR("%UPTIME%"), []() -> const char* {
static char buffer[16];
snprintf(buffer, sizeof(buffer), "%lus", millis() / 1000);
return buffer;
});
registry.registerProgmemTemplate(PSTR("%ROOT%"), ROOT_TEMPLATE_PROGMEM);
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, PSTR("%ROOT%"));
}
void loop() {
static uint8_t buffer[128];
if (!TemplateRenderer::isComplete(ctx) && !TemplateRenderer::hasError(ctx)) {
size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer));
Serial.write(buffer, written);
}
}
```
3. Open the serial monitor (or send chunks to `AsyncWebServer`) to watch the template stream without ever allocating the full HTML in RAM.
## Why DFTE vs Alternatives?
| Approach | Streaming | Dynamic data | Template reuse | Notes |
|----------|-----------|--------------|---------------|-------|
| DFTE (this library) | ✅ Streams in 128–512 byte chunks | ✅ Registry handles placeholders, conditionals, iterators | ✅ PROGMEM templates shared across requests | Built for async HTTP; keeps RAM usage predictable |
| Static SPIFFS/LittleFS files | ❌ Full file send only | ⚠️ Requires pre-generated HTML | ✅ Stored once on flash filesystem | Great for static assets, not ideal for live telemetry |
| Arduino `String` concatenation | ❌ Concatenates into one RAM buffer | ✅ Manual `String` inserts | ❌ Template duplicated per build | Fast to prototype but fragments heap on longer sketches |
| AsyncWebServer template callback | ⚠️ Streams callback output but copies per placeholder | ✅ Values supplied in callback | ❌ No shared layout or nesting support | Fine for small pages; scales poorly with complex UIs |
| Server-side proxy (external backend) | ✅ Offloaded to external service | ✅ Managed by backend | ❌ Device ships only proxy stub | Requires constant connectivity and extra infrastructure |
## Core Types & API
- `PlaceholderRegistry`
- `registerProgmemData(const char*, const char*)` – link `%TOKEN%` to flash-resident data.
- `registerRamData(const char*, PlaceholderDataGetter)` – provide dynamic strings from getters.
- `registerProgmemTemplate(const char*, const char*)` – nest other templates.
- `registerDynamicTemplate(const char*, const DynamicTemplateDescriptor*)` – compute template fragments at render time.
- `registerConditional(const char*, const ConditionalDescriptor*)` – choose between delegates (`TRUE_BRANCH`, `FALSE_BRANCH`, `SKIP`).
- `registerIterator(const char*, const IteratorDescriptor*)` – stream repeated sections item-by-item.
- `getPlaceholder`, `getCount`, `clear` – inspection/utilities used throughout the tests.
- `TemplateContext`
- Holds the render stack, buffers, and statistics.
- `setRegistry(PlaceholderRegistry*)` – inject the registry you populated.
- `reset()` – reuse the context without re-allocating buffers.
- `isComplete()`, `hasError()`, `getStateString()` – status helpers.
- `TemplateRenderer`
- `initializeContext(TemplateContext&, const char*, bool templateInProgmem = true)` – prime the context with the root template.
- `renderNextChunk(TemplateContext&, uint8_t* buf, size_t len)` – stream out the next chunk; returns written bytes.
- `isComplete(const TemplateContext&)`, `hasError(const TemplateContext&)` – convenience checks.
- `DeviceFrameworkTemplateEngineDebug`
- Optional logging interface; create a `DeviceFrameworkTemplateEngineLogger` subclass and call `deviceFrameworkTemplateEngineEnableLogging(&logger)` (or the two-argument overload with an owner tag, e.g. `this`, for `deviceFrameworkTemplateEngineDisableLoggingForOwner`).
All public headers are re-exported from `TemplateEngine.h`, so typical sketches only include that file.
## Usage Overview
1. Include `TemplateEngine.h`.
```
#include <TemplateEngine.h>
```
2. Register placeholders on a `PlaceholderRegistry`.
```
PlaceholderRegistry registry;
registry.registerProgmemData(PSTR("%APP_TITLE%"), PSTR("DFTE Dashboard"));
registry.registerRamData(PSTR("%UPTIME%"), []() -> const char* {
static char buffer[16];
snprintf(buffer, sizeof(buffer), "%lus", millis() / 1000);
return buffer;
});
registry.registerProgmemTemplate(PSTR("%ROOT%"), ROOT_TEMPLATE_PROGMEM);
```
3. Attach the registry to a `TemplateContext`.
```
TemplateContext ctx;
ctx.setRegistry(&registry);
```
4. Feed the root template to `TemplateRenderer::initializeContext`.
```
TemplateRenderer::initializeContext(ctx, PSTR("%ROOT%"));
```
5. Loop on `renderNextChunk` until `TemplateRenderer::isComplete(ctx)` is `true`.
```
uint8_t buffer[128];
while (!TemplateRenderer::isComplete(ctx) && !TemplateRenderer::hasError(ctx)) {
size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer));
Serial.write(buffer, written);
}
```
6. Optionally reuse the same context for additional templates by calling `ctx.reset()`.
```
ctx.reset();
TemplateRenderer::initializeContext(ctx, PSTR("%DETAIL_PANEL%"));
```
### Async Streaming Pattern
When serving requests with ESPAsyncWebServer, include `TemplateEngineAsyncWeb.h` and give every request its own `TemplateContext` so chunked rendering cannot be corrupted by overlapping clients. Build and cache your `PlaceholderRegistry` once during setup, then share it across handlers. The same pattern powers the DeviceFramework web UI and the DFTE examples:
```cpp ```cpp
#include <TemplateEngine.h> #include <TemplateEngine.h>
#include <TemplateEngineAsyncWeb.h>
/** Global registry prepared during setup() */ static const char PAGE[] PROGMEM = "<h1>%TITLE%</h1>";
std::shared_ptr<PlaceholderRegistry> registry; Serial.begin(115200);
PlaceholderRegistry registry;
void setupRegistry() { TemplateContext context;
registry = std::make_shared<PlaceholderRegistry>();
registry->registerProgmemData(PSTR("%APP_TITLE%"), PSTR("DFTE Async Portal"));
registry->registerRamData(PSTR("%UPTIME%"), []() -> const char* {
static char buffer[16];
snprintf(buffer, sizeof(buffer), "%lus", millis() / 1000);
return buffer;
});
registry->registerProgmemTemplate(PSTR("%ROOT%"), ROOT_TEMPLATE_PROGMEM);
}
void streamTemplate(AsyncWebServerRequest* request, const char* rootTemplate) {
auto ctx = std::make_shared<TemplateContext>();
ctx->setRegistry(registry.get());
TemplateRenderer::initializeContext(*ctx, rootTemplate);
request->onDisconnect([ctx]() mutable { ctx.reset(); });
AsyncWebServerResponse* response =
TemplateEngineAsyncWeb::beginSafeTemplateResponse(
request, "text/html; charset=utf-8", ctx, 128);
response->addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response->addHeader("Pragma", "no-cache");
response->addHeader("Expires", "-1");
request->send(response);
}
void setup() { void setup() {
setupRegistry(); registry.registerProgmemData(PSTR("%TITLE%"), PSTR("Hello DFTE"));
server.on("/", [](AsyncWebServerRequest* request) { registry.registerProgmemTemplate(PSTR("%PAGE%"), PAGE);
streamTemplate(request, PSTR("%ROOT%")); context.setRegistry(&registry);
}); TemplateRenderer::initializeContext(context, PSTR("%PAGE%"));
server.begin(); }
void loop() {
uint8_t chunk[128];
if (!TemplateRenderer::isComplete(context) && !TemplateRenderer::hasError(context)) {
Serial.write(chunk, TemplateRenderer::renderNextChunk(context, chunk, sizeof(chunk)));
}
} }
``` ```
1. Build and cache the `PlaceholderRegistry` during startup so heavy PROGMEM registration runs once. ## Featured examples
2. Allocate a request-scoped `TemplateContext`, initialise it with the shared registry, and render inside the chunked callback.
3. Tear everything down on completion or disconnect to avoid state bleed between clients.
### Template Syntax | Goal | Example |
| --- | --- |
| 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/) |
Every placeholder in a template uses `%NAME%`. DFTE looks up `NAME` in the registry and decides how to render it based on the registered type. Templates can be nested arbitrarily (up to `DFTE_MAX_STACK_DEPTH_DEFAULT` unless you raise it). ## Install
### Placeholder Types
- **Static data** – `registerProgmemData("%CSS%", PROGMEM_BLOCK)` streams literal content from flash or RAM.
- **Nested template** – `registerProgmemTemplate("%HEADER%", HEADER_TEMPLATE)` injects another template that can contain its own placeholders.
- **Dynamic value** – `registerRamData("%UPTIME%", getter)` calls a function that returns the current value as a `const char*`.
- **Dynamic template** – `registerDynamicTemplate("%CONTENT%", &DynamicTemplateDescriptor{getter, getLength, userData})` asks your getter to return template text at render time.
- **Conditional** – `registerConditional("%IS_ONLINE%", &ConditionalDescriptor{evaluate, "%ONLINE%", "%OFFLINE%", userData})` chooses which delegate placeholder to render based on the evaluator result.
- **Iterator** – `registerIterator("%SENSORS%", &IteratorDescriptor{open, next, close, userData})` opens a handle, streams each item template through `IteratorItemView`, and finalises with `close`.
### Buildable Examples
All demos under `examples/` are standalone PlatformIO projects that use the library via `lib_extra_dirs`. Each contains a `platformio.ini` with ready-to-build environments, so you can compile and upload without touching your primary application.
- `examples/AsyncDashboardDemo/` – Full SoftAP dashboard with iterators, conditionals, and runtime telemetry.
- `examples/StreamingAsync/` – Minimal captive portal that streams the template directly to the HTTP response.
- `examples/NestedLayouts/` – Demonstrates layout stacking, partials, and conditional fragments.
- `examples/HelloPlaceholder/` – Smallest possible sketch that renders a single placeholder.
Typical workflow (replace the path/env as needed):
```
# Build
pio run -d examples/StreamingAsync -e example_esp32
# Flash
pio run -d examples/StreamingAsync -e example_esp32 -t upload
# Monitor (optional)
pio run -d examples/StreamingAsync -e example_esp32 -t monitor
```
ESP8266 variants use the `example_esp8266` or `dashboard_esp8266` environments, while ESP32 boards use `example_esp32` or `dashboard_esp32`. Update Wi-Fi credentials inside each example’s `src/main.cpp`, then connect to the serial monitor or SoftAP as documented in the per-example README.
Refer to the Unity tests in `test/test_template_engine/tests` for exhaustive combinations of placeholders, nested templates, conditionals, and iterators.
### Memory & Configuration Knobs
DFTE has two kinds of limits. Set build-wide limits with PlatformIO `build_flags`, so the library and every consuming translation unit use the exact same object layout. Do not set these only in a sketch header.
- `DFTE_BUFFER_SIZE` (default 512) – streaming buffer inside `TemplateContext`.
- `DFTE_MAX_STACK_DEPTH` (default 16) – rendering stack frames. A nested template generally uses two frames, so set this to at least twice the intended nesting plus one.
- `DFTE_PLACEHOLDER_NAME_SIZE` (default 24) – maximum placeholder-token length including `%` characters.
- `DFTE_PROGMEM_CHUNK_SIZE` (default 512) and `DFTE_RAM_CHUNK_SIZE` (default 128) – source copy windows.
- `DFTE_MAX_ITERATIONS` (default 50) – safety cap for an individual render call.
- `DFTE_MAX_PLACEHOLDERS_DEFAULT` (default 16) – constructor default only; alternatively pass an explicit capacity to `PlaceholderRegistry`.
```ini
; platformio.ini
build_flags =
-DDFTE_BUFFER_SIZE=768
-DDFTE_MAX_STACK_DEPTH=24
-DDFTE_PLACEHOLDER_NAME_SIZE=32
-DDFTE_MAX_ITERATIONS=80
```
DFTE is deliberately independent of DeviceFramework's runtime `CONFIG_template*` values. DeviceFramework may choose a registry capacity at runtime, but any fixed DFTE layout must be configured by the shared `DFTE_*` build flags above. This prevents an application header and the compiled library from disagreeing about object size.
## PlatformIO Usage
### Consume as a dependency
Pin a released Git tag in the application's `platformio.ini`:
```ini ```ini
lib_deps = lib_deps =
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.0.2 DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.0.2
``` ```
The suffix after `#` is a Git ref. PlatformIO clones the repository and checks PlatformIO checks out the Git ref after `#`; GitHub Release assets are unrelated. DFTE’s supported release targets are ESP8266 and ESP32.
out that tag; GitHub Release assets are unrelated. A tag is reproducible,
whereas `#main` deliberately follows moving development. Use a local
`symlink://` or `file://` dependency only while working on DFTE itself.
### Test without hardware ## Documentation
The test environments compile the full Unity suites without uploading or Read the [documentation index](docs/README.md) for template syntax, async web responses, ABI-safe build flags, examples, tests, and releases.
executing them, so no board is required:
```bash ## Development and releases
pio test -e test_template_engine_8266 --without-uploading --without-testing
pio test -e test_template_engine_esp32 --without-uploading --without-testing
```
Both environments set `test_build_src = yes`, so the library sources are
included. CI runs the two commands above on every push and pull request.
### Publish a release
Update `library.json`, `CHANGELOG.md`, and the public documentation, run
both compile checks, then create the annotated tag:
```bash ```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 ./scripts/prepare-release.sh vMAJOR.MINOR.PATCH --tag
``` ```
Push the branch and tag. The tag workflow validates the PlatformIO package 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.
again and creates the GitHub Release.
### Debug Logging See the [changelog](CHANGELOG.md) and [licence](LICENSE).
DFTE’s logger is opt-in and costs nothing until you enable it. Implement `DeviceFrameworkTemplateEngineLogger`, register it once, and all internal `DFTE_LOG_*` calls stream through your logger.
```
#include <TemplateEngine.h>
#include <DeviceFrameworkTemplateEngineDebug.h>
class SerialLogger : public DeviceFrameworkTemplateEngineLogger {
public:
void error(const String& msg) override { Serial.println("[DFTE][E] " + msg); }
void warn(const String& msg) override { Serial.println("[DFTE][W] " + msg); }
void info(const String& msg) override { Serial.println("[DFTE][I] " + msg); }
void debug(const String& msg) override { Serial.println("[DFTE][D] " + msg); }
};
SerialLogger logger;
void setup() {
Serial.begin(115200);
deviceFrameworkTemplateEngineEnableLogging(&logger, static_cast<const void*>(&logger));
}
```
Use `deviceFrameworkTemplateEngineDisableLogging()` to silence output, `deviceFrameworkTemplateEngineDisableLoggingForOwner(tag)` to tear down only the registration made with that owner (for example WiFiManager’s server instance), or `deviceFrameworkTemplateEngineIsLoggingEnabled()` to inspect the current state.
## License
This project is released under the MIT License. See `LICENSE` for details.
+29
View File
@@ -0,0 +1,29 @@
# 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.
```cpp
#include <TemplateEngine.h>
#include <TemplateEngineAsyncWeb.h>
std::shared_ptr<PlaceholderRegistry> registry;
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(); });
AsyncWebServerResponse* response =
TemplateEngineAsyncWeb::beginSafeTemplateResponse(
request, "text/html; charset=utf-8", context, 128
);
request->send(response);
}
```
The response retains the context while it streams. Releasing the request-owned `shared_ptr` on disconnect prevents state from leaking into later requests.
Use [StreamingAsync](../examples/StreamingAsync/) for a complete SoftAP/captive-portal project.
Back to [documentation](README.md) · [project overview](../README.md).
+25
View File
@@ -0,0 +1,25 @@
# Configuration and memory limits
DFTE object layouts depend on fixed `DFTE_*` compile-time limits. Define any changes through shared PlatformIO `build_flags` so the library and every consuming translation unit agree on the same layout.
```ini
build_flags =
-DDFTE_BUFFER_SIZE=768
-DDFTE_MAX_STACK_DEPTH=24
-DDFTE_PLACEHOLDER_NAME_SIZE=32
-DDFTE_MAX_ITERATIONS=80
```
| Flag | Default | Meaning |
| --- | --- | --- |
| `DFTE_BUFFER_SIZE` | 512 | Streaming buffer size in `TemplateContext` |
| `DFTE_MAX_STACK_DEPTH` | 16 | Render stack frames; nested templates usually need two frames each |
| `DFTE_PLACEHOLDER_NAME_SIZE` | 24 | Maximum token length including `%` characters |
| `DFTE_PROGMEM_CHUNK_SIZE` | 512 | PROGMEM source copy window |
| `DFTE_RAM_CHUNK_SIZE` | 128 | RAM source copy window |
| `DFTE_MAX_ITERATIONS` | 50 | Safety cap for one render call |
| `DFTE_MAX_PLACEHOLDERS_DEFAULT` | 16 | Default registry constructor capacity |
`DFTE_MAX_PLACEHOLDERS_DEFAULT` is only a constructor default; pass an explicit registry capacity where a device needs more. DeviceFramework runtime template parameters do not change DFTE’s compile-time object layout.
Back to [documentation](README.md) · [project overview](../README.md).
+16
View File
@@ -0,0 +1,16 @@
# Development and releases
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:
```bash
./scripts/check-docs.sh
./scripts/test.sh compile --platform esp8266
./scripts/test.sh compile --platform esp32
./scripts/prepare-release.sh vMAJOR.MINOR.PATCH --tag
```
Push the branch and annotated tag. GitHub Actions repeats the board-free compile checks, validates the package, and creates a GitHub Release from that version’s changelog section. It does not publish to the PlatformIO Registry.
Back to [documentation](README.md) · [project overview](../README.md).
+42
View File
@@ -0,0 +1,42 @@
# Getting started
DFTE renders a named root template through a `TemplateContext`. Register templates and placeholder values once, then ask the renderer for fixed-size chunks until completion.
```cpp
#include <Arduino.h>
#include <TemplateEngine.h>
static const char ROOT[] PROGMEM = R"DFTE(
<h1>%TITLE%</h1><p>Uptime: %UPTIME%</p>
)DFTE";
PlaceholderRegistry registry;
TemplateContext context;
void setup() {
Serial.begin(115200);
registry.registerProgmemData(PSTR("%TITLE%"), PSTR("DFTE Quickstart"));
registry.registerRamData(PSTR("%UPTIME%"), []() -> const char* {
static char value[16];
snprintf(value, sizeof(value), "%lus", millis() / 1000);
return value;
});
registry.registerProgmemTemplate(PSTR("%ROOT%"), ROOT);
context.setRegistry(&registry);
TemplateRenderer::initializeContext(context, PSTR("%ROOT%"));
}
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);
}
}
```
`registerProgmemData()` is for static flash data. `registerRamData()` takes a getter for a value that can change as chunks are rendered. `registerProgmemTemplate()` lets a placeholder expand to another template.
For a buildable project, start with [HelloPlaceholder](../examples/HelloPlaceholder/). Next: [template language](TEMPLATE_LANGUAGE.md).
Back to [documentation](README.md) · [project overview](../README.md).
+20
View File
@@ -0,0 +1,20 @@
# DFTE documentation
DFTE documentation is organised by the rendering problem being solved.
| I want to… | Read |
| --- | --- |
| Render a first template | [Getting started](GETTING_STARTED.md) |
| 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) |
| 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).
+30
View File
@@ -0,0 +1,30 @@
# Template language and lifecycle
Every token has the form `%NAME%`. DFTE resolves it from a `PlaceholderRegistry` according to its registered type.
| Registered type | Purpose |
| --- | --- |
| `registerProgmemData()` | Static flash-resident text |
| `registerRamData()` | Getter returning current `const char*` data |
| `registerProgmemTemplate()` | Nested PROGMEM template |
| `registerDynamicTemplate()` | Template fragment supplied at render time |
| `registerConditional()` | Choose a true, false, or skipped delegate |
| `registerIterator()` | Stream repeated item templates through an iterator handle |
## Context lifecycle
1. Populate a registry during setup.
2. Attach it to a `TemplateContext`.
3. Initialise the context with the root placeholder.
4. Call `renderNextChunk()` until complete or error.
5. Call `reset()` before reusing a completed context for another root.
Do not treat `isComplete()` as a success result by itself; check `hasError()` as well.
## Iterators
An iterator opens a handle, produces `IteratorItemView` values, then closes the handle. Handles are closed when rendering finishes, resets, stalls, or fails. The `open`, `next`, and `close` callbacks must therefore tolerate early termination.
Use [AsyncDashboardDemo](../examples/AsyncDashboardDemo/) for a complete iterator example. Use [NestedLayouts](../examples/NestedLayouts/) for partials and conditions.
Back to [documentation](README.md) · [project overview](../README.md).
+14
View File
@@ -0,0 +1,14 @@
# Testing
The two 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
```
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 standalone examples are buildable PlatformIO projects; see [examples](../examples/README.md).
Back to [documentation](README.md) · [project overview](../README.md).
+2
View File
@@ -17,6 +17,8 @@ pio run -d examples/AsyncDashboardDemo -e dashboard_esp32
## Run ## Run
See the shared [examples guide](../README.md) and [async web guidance](../../docs/ASYNC_WEB.md).
1. Flash your target board (`pio run -d examples/AsyncDashboardDemo -e <env> -t upload --upload-port <port>`). 1. Flash your target board (`pio run -d examples/AsyncDashboardDemo -e <env> -t upload --upload-port <port>`).
2. Open the serial monitor at 115200 baud to confirm the SoftAP credentials. 2. Open the serial monitor at 115200 baud to confirm the SoftAP credentials.
3. Connect to the advertised network and browse to `http://192.168.4.1/`. The captive portal should automatically redirect; if not, navigate manually. 3. Connect to the advertised network and browse to `http://192.168.4.1/`. The captive portal should automatically redirect; if not, navigate manually.
+2
View File
@@ -13,5 +13,7 @@ pio run -d examples/HelloPlaceholder -e example_esp8266
pio run -d examples/HelloPlaceholder -e example_esp32 pio run -d examples/HelloPlaceholder -e example_esp32
``` ```
See the shared [examples guide](../README.md) and [quickstart](../../docs/GETTING_STARTED.md).
Flash the firmware and open the serial monitor at 115200 baud to see the rendered text. Flash the firmware and open the serial monitor at 115200 baud to see the rendered text.
+2
View File
@@ -14,5 +14,7 @@ pio run -d examples/NestedLayouts -e example_esp8266
pio run -d examples/NestedLayouts -e example_esp32 pio run -d examples/NestedLayouts -e example_esp32
``` ```
See the shared [examples guide](../README.md) and [template-language guide](../../docs/TEMPLATE_LANGUAGE.md).
Open the serial monitor at 115200 baud to inspect the rendered output. Open the serial monitor at 115200 baud to inspect the rendered output.
+22
View File
@@ -0,0 +1,22 @@
# DFTE examples
Every example is a standalone PlatformIO project. Build, upload, and monitor from its directory:
```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
```
Choose the ESP32 environment where provided.
| Example | What it demonstrates |
| --- | --- |
| [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 |
The SoftAP examples print their network names and passwords to serial. They are demonstrations, not production provisioning implementations.
Back to [DFTE documentation](../docs/README.md) · [project overview](../README.md).
+2
View File
@@ -11,6 +11,8 @@ Small ESPAsyncWebServer demo that exposes a captive portal over a SoftAP and ren
``` ```
pio run -d examples/StreamingAsync -e example_esp8266 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 pio run -d examples/StreamingAsync -e example_esp32
``` ```
+11
View File
@@ -34,5 +34,16 @@
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/alexhopeoconnor/DFTE.git" "url": "https://github.com/alexhopeoconnor/DFTE.git"
},
"export": {
"include": [
"include",
"src",
"examples",
"README.md",
"CHANGELOG.md",
"LICENSE",
"library.json"
]
} }
} }
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
required=(
README.md CHANGELOG.md
docs/README.md docs/GETTING_STARTED.md docs/TEMPLATE_LANGUAGE.md
docs/ASYNC_WEB.md docs/CONFIGURATION.md docs/TESTING.md docs/DEVELOPMENT.md
examples/README.md
)
for path in "${required[@]}"; do
[[ -f "$root/$path" ]] || { echo "Missing required documentation: $path" >&2; exit 1; }
done
while IFS= read -r -d '' markdown; do
while IFS= read -r target; do
[[ -z "$target" || "$target" == \#* || "$target" == http://* || "$target" == https://* || "$target" == mailto:* ]] && continue
target="${target%%#*}"
case "$target" in
/*) candidate="$root/${target#/}" ;;
*) candidate="$(dirname "$markdown")/$target" ;;
esac
[[ -e "$candidate" ]] || { echo "Broken relative link in ${markdown#$root/}: $target" >&2; exit 1; }
done < <(sed -nE 's/.*\]\(([^ )]+)( "[^"]*")?\).*/\1/p' "$markdown")
done < <(find "$root" -path "$root/.git" -prune -o -path '*/.pio' -prune -o -name '*.md' -type f -print0)
echo "Documentation links and required files passed"
+7
View File
@@ -27,6 +27,13 @@ if [[ -f "$root/library.properties" ]]; then
fi fi
fi fi
if ! grep -q "^## ${version}$" "$root/CHANGELOG.md"; then
echo "CHANGELOG.md is missing a ## ${version} section" >&2
exit 1
fi
git -C "$root" diff --check
package_dir="$(mktemp -d)" package_dir="$(mktemp -d)"
trap 'rm -rf "$package_dir"' EXIT trap 'rm -rf "$package_dir"' EXIT
pio pkg pack "$root" --output "$package_dir/package.tar.gz" >/dev/null pio pkg pack "$root" --output "$package_dir/package.tar.gz" >/dev/null
+30
View File
@@ -0,0 +1,30 @@
#!/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}"
changelog="$root/CHANGELOG.md"
output="$(mktemp)"
trap 'rm -f "$output"' EXIT
awk -v version="$version" '
$0 == "## " version { capture = 1; next }
capture && /^## / { exit }
capture { print }
' "$changelog" > "$output"
if [[ ! -s "$output" ]]; then
echo "No release notes found for $tag in CHANGELOG.md" >&2
exit 1
fi
printf '%s\n\n' "# DeviceFrameworkTemplateEngine $tag"
cat "$output"
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 compile --platform esp8266|esp32" >&2
exit 2
}
[[ "${1:-}" == "compile" && "${2:-}" == "--platform" && $# -eq 3 ]] || usage
case "${3:-}" in
esp8266) environment="test_template_engine_8266" ;;
esp32) environment="test_template_engine_esp32" ;;
*) 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}"
+1 -1
View File
@@ -154,7 +154,7 @@ RenderingContextType DeviceFrameworkTemplateContext::getCurrentContextType() con
} }
bool DeviceFrameworkTemplateContext::isComplete() const { bool DeviceFrameworkTemplateContext::isComplete() const {
return state == TemplateRenderState::COMPLETE || state == TemplateRenderState::ERROR; return state == TemplateRenderState::COMPLETE;
} }
bool DeviceFrameworkTemplateContext::hasError() const { bool DeviceFrameworkTemplateContext::hasError() const {
@@ -38,7 +38,7 @@ void test_template_context_initialization() {
ctx.state = TemplateRenderState::COMPLETE; ctx.state = TemplateRenderState::COMPLETE;
TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "Should be complete when state is COMPLETE"); TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "Should be complete when state is COMPLETE");
ctx.state = TemplateRenderState::ERROR; ctx.state = TemplateRenderState::ERROR;
TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "Should be complete when state is ERROR"); TEST_ASSERT_FALSE_MESSAGE(ctx.isComplete(), "An error must not report successful completion");
// Test hasError // Test hasError
ctx.state = TemplateRenderState::TEXT; ctx.state = TemplateRenderState::TEXT;
@@ -310,7 +310,7 @@ void test_template_context_state() {
TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "COMPLETE should be complete"); TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "COMPLETE should be complete");
ctx.state = TemplateRenderState::ERROR; ctx.state = TemplateRenderState::ERROR;
TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "ERROR should be complete"); TEST_ASSERT_FALSE_MESSAGE(ctx.isComplete(), "ERROR should not be complete");
// Test hasError for all states // Test hasError for all states
ctx.state = TemplateRenderState::TEXT; ctx.state = TemplateRenderState::TEXT;