mirror of
https://github.com/alexhopeoconnor/DFTE.git
synced 2026-10-18 12:41:44 +10:00
feat: prepare DFTE 1.0.2
This commit is contained in:
@@ -2,22 +2,32 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
documentation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ./scripts/check-docs.sh
|
||||
compile-tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
environment: ["test_template_engine_8266", "test_template_engine_esp32"]
|
||||
platform: [esp8266, esp32]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- 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 }}
|
||||
|
||||
@@ -17,7 +17,11 @@ jobs:
|
||||
with:
|
||||
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 compile --platform esp32
|
||||
- run: ./scripts/check-docs.sh
|
||||
- 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:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
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.
|
||||
|
||||
The published package and CI support ESP8266 and ESP32. The code may be useful
|
||||
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.
|
||||
## Why use it
|
||||
|
||||
## 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):
|
||||
|
||||
```
|
||||
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(®istry);
|
||||
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(®istry);
|
||||
```
|
||||
|
||||
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:
|
||||
## Try it
|
||||
|
||||
```cpp
|
||||
#include <TemplateEngine.h>
|
||||
#include <TemplateEngineAsyncWeb.h>
|
||||
|
||||
/** Global registry prepared during setup() */
|
||||
std::shared_ptr<PlaceholderRegistry> registry;
|
||||
|
||||
void setupRegistry() {
|
||||
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);
|
||||
}
|
||||
static const char PAGE[] PROGMEM = "<h1>%TITLE%</h1>";
|
||||
Serial.begin(115200);
|
||||
PlaceholderRegistry registry;
|
||||
TemplateContext context;
|
||||
|
||||
void setup() {
|
||||
setupRegistry();
|
||||
server.on("/", [](AsyncWebServerRequest* request) {
|
||||
streamTemplate(request, PSTR("%ROOT%"));
|
||||
});
|
||||
server.begin();
|
||||
registry.registerProgmemData(PSTR("%TITLE%"), PSTR("Hello DFTE"));
|
||||
registry.registerProgmemTemplate(PSTR("%PAGE%"), PAGE);
|
||||
context.setRegistry(®istry);
|
||||
TemplateRenderer::initializeContext(context, PSTR("%PAGE%"));
|
||||
}
|
||||
|
||||
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.
|
||||
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.
|
||||
## Featured examples
|
||||
|
||||
### 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).
|
||||
|
||||
### 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`:
|
||||
## Install
|
||||
|
||||
```ini
|
||||
lib_deps =
|
||||
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.0.2
|
||||
```
|
||||
|
||||
The suffix after `#` is a Git ref. PlatformIO clones the repository and checks
|
||||
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.
|
||||
PlatformIO checks out the Git ref after `#`; GitHub Release assets are unrelated. DFTE’s supported release targets are ESP8266 and ESP32.
|
||||
|
||||
### Test without hardware
|
||||
## Documentation
|
||||
|
||||
The test environments compile the full Unity suites without uploading or
|
||||
executing them, so no board is required:
|
||||
Read the [documentation index](docs/README.md) for template syntax, async web responses, ABI-safe build flags, examples, tests, and releases.
|
||||
|
||||
```bash
|
||||
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:
|
||||
## 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
|
||||
```
|
||||
|
||||
Push the branch and tag. The tag workflow validates the PlatformIO package
|
||||
again and creates the GitHub Release.
|
||||
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.
|
||||
|
||||
### Debug Logging
|
||||
|
||||
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.
|
||||
See the [changelog](CHANGELOG.md) and [licence](LICENSE).
|
||||
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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(®istry);
|
||||
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).
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -17,6 +17,8 @@ pio run -d examples/AsyncDashboardDemo -e dashboard_esp32
|
||||
|
||||
## 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>`).
|
||||
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.
|
||||
|
||||
@@ -13,5 +13,7 @@ pio run -d examples/HelloPlaceholder -e example_esp8266
|
||||
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.
|
||||
|
||||
|
||||
@@ -14,5 +14,7 @@ pio run -d examples/NestedLayouts -e example_esp8266
|
||||
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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -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
|
||||
|
||||
See the shared [examples guide](../README.md) and [async web guidance](../../docs/ASYNC_WEB.md).
|
||||
pio run -d examples/StreamingAsync -e example_esp32
|
||||
```
|
||||
|
||||
|
||||
@@ -34,5 +34,16 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/alexhopeoconnor/DFTE.git"
|
||||
},
|
||||
"export": {
|
||||
"include": [
|
||||
"include",
|
||||
"src",
|
||||
"examples",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
"library.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+28
@@ -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"
|
||||
@@ -27,6 +27,13 @@ if [[ -f "$root/library.properties" ]]; then
|
||||
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)"
|
||||
trap 'rm -rf "$package_dir"' EXIT
|
||||
pio pkg pack "$root" --output "$package_dir/package.tar.gz" >/dev/null
|
||||
|
||||
Executable
+30
@@ -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"
|
||||
Executable
+19
@@ -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}"
|
||||
@@ -154,7 +154,7 @@ RenderingContextType DeviceFrameworkTemplateContext::getCurrentContextType() con
|
||||
}
|
||||
|
||||
bool DeviceFrameworkTemplateContext::isComplete() const {
|
||||
return state == TemplateRenderState::COMPLETE || state == TemplateRenderState::ERROR;
|
||||
return state == TemplateRenderState::COMPLETE;
|
||||
}
|
||||
|
||||
bool DeviceFrameworkTemplateContext::hasError() const {
|
||||
|
||||
@@ -38,7 +38,7 @@ void test_template_context_initialization() {
|
||||
ctx.state = TemplateRenderState::COMPLETE;
|
||||
TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "Should be complete when state is COMPLETE");
|
||||
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
|
||||
ctx.state = TemplateRenderState::TEXT;
|
||||
@@ -310,7 +310,7 @@ void test_template_context_state() {
|
||||
TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "COMPLETE should be complete");
|
||||
|
||||
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
|
||||
ctx.state = TemplateRenderState::TEXT;
|
||||
|
||||
Reference in New Issue
Block a user