mirror of
https://github.com/alexhopeoconnor/DFTE.git
synced 2026-10-09 19:32:18 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65d2cb38ae | ||
|
|
6a5b3aab9e | ||
|
|
361803c2b3 |
@@ -2,22 +2,33 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
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 }}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## 1.1.0
|
||||
|
||||
- Replace layout-affecting compile-time storage overrides with per-context,
|
||||
caller-selected rendering depth and read-buffer capacity. This keeps the
|
||||
public ABI stable across translation units while allowing constrained
|
||||
responses to use smaller storage.
|
||||
- Pin ESP32 tests to the Arduino 3-compatible pioarduino platform release.
|
||||
|
||||
## 1.0.2
|
||||
|
||||
- Ensure open iterator handles are closed when rendering resets, stalls, or
|
||||
terminates with an error.
|
||||
- Document the supported build-flag configuration model so every translation
|
||||
unit uses the same DFTE object layout.
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- Establish the first semantic-versioned release of the maintained DFTE
|
||||
package.
|
||||
@@ -1,303 +1,70 @@
|
||||
# Device Framework Template Engine (DFTE)
|
||||
# Device Framework Template Engine
|
||||
|
||||
DFTE is a lightweight C++ template engine tailored for Arduino-class hardware (ESP8266, ESP32, RP2040, and friends) that need to render rich HTML dashboards or textual feeds without allocating giant buffers. It streams HTML over chunked HTTP, stitches together deeply nested layouts from PROGMEM, and injects live device data sourced from RAM getters—all while keeping your microcontroller responsive. If you are searching for an “ESP32 streaming HTML template engine” or a way to “render dynamic Arduino web UI without SPIFFS,” DFTE is the solution.
|
||||
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.
|
||||
|
||||
## Why use it
|
||||
|
||||
## Quickstart
|
||||
1. Define your root template in PROGMEM (or RAM if you prefer):
|
||||
- **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.
|
||||
|
||||
```
|
||||
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`.
|
||||
## Install
|
||||
|
||||
```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
|
||||
Add DFTE to your project’s `platformio.ini` using the Git repository URL:
|
||||
|
||||
```
|
||||
lib_deps =
|
||||
https://github.com/alexhopeoconnor/DFTE
|
||||
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.1.0
|
||||
```
|
||||
|
||||
Pin to a specific release tag if you need reproducible builds (for example `https://github.com/alexhopeoconnor/DFTE#v1.0.2`), or keep the `lib_deps` entry as-is to track the latest main branch during development.
|
||||
PlatformIO checks out the Git ref after `#`; GitHub Release assets are unrelated. DFTE’s supported release targets are ESP8266 and ESP32.
|
||||
|
||||
### Local Development & Testing
|
||||
```
|
||||
pio test -e test_template_engine # run full Unity suite
|
||||
pio test -e test_template_engine -f test_template_renderer # single test file
|
||||
pio run -e test_template_engine # compile without running tests
|
||||
```
|
||||
The default environment targets `d1_mini` (ESP8266) with `test_build_src = yes` so library sources are included during builds.
|
||||
## Documentation
|
||||
|
||||
### Debug Logging
|
||||
Read the [documentation index](docs/README.md) for template syntax, async web responses, per-context memory sizing, examples, tests, and releases.
|
||||
|
||||
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.
|
||||
## Development and releases
|
||||
|
||||
```
|
||||
#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));
|
||||
}
|
||||
```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
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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,41 @@
|
||||
# Configuration and memory limits
|
||||
|
||||
DFTE has a stable public object layout. `TemplateContext` owns its rendering
|
||||
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.
|
||||
}
|
||||
```
|
||||
|
||||
The context allocates its stack and read buffer when it is constructed. Its
|
||||
approximate heap use is `maxDepth * sizeof(RenderingContext) + bufferSize`, plus
|
||||
allocator overhead; its fixed 24-byte placeholder-token storage is part of the
|
||||
stable context object. Allocate one context per concurrently streaming request; use explicit
|
||||
small capacities for bounded pages rather than changing global definitions.
|
||||
Nested template expansion normally consumes two stack frames per level.
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `DFTE_MAX_STACK_DEPTH` | 16 | Default depth passed by the no-argument `TemplateContext` constructor |
|
||||
| `DFTE_BUFFER_SIZE` | 512 | Default read-buffer size passed by the no-argument constructor |
|
||||
| `DFTE_MAX_ITERATIONS` | 50 | Safety cap for one renderer call |
|
||||
| `DFTE_PROGMEM_CHUNK_SIZE` | 512 | Source-copy window for flash data |
|
||||
| `DFTE_RAM_CHUNK_SIZE` | 128 | Source-copy window for RAM data |
|
||||
| `DFTE_MAX_PLACEHOLDERS_DEFAULT` | 16 | Default `PlaceholderRegistry` capacity |
|
||||
|
||||
The first two are constructor defaults, not layout controls: a consuming
|
||||
translation unit can choose them without an ABI mismatch. The source-copy and
|
||||
iteration settings change renderer behaviour, so set them consistently for a
|
||||
whole PlatformIO build. `DFTE_PLACEHOLDER_NAME_SIZE` is no longer a supported
|
||||
setting; placeholder tokens have a fixed ABI-stable capacity of 23 characters
|
||||
plus the terminator.
|
||||
|
||||
`DFTE_MAX_PLACEHOLDERS_DEFAULT` is only a constructor default; pass an explicit
|
||||
registry capacity where a device needs more.
|
||||
|
||||
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.
|
||||
|
||||
@@ -24,11 +24,13 @@ build_flags =
|
||||
-I$PROJECT_LIBDEPS_DIR/$PIOENV/ESPAsyncTCP/src
|
||||
|
||||
[env:dashboard_esp32]
|
||||
platform = espressif32@6.13.0
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
ESP32Async/AsyncTCP
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DSOC_WIFI_SUPPORTED=1
|
||||
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
|
||||
-I$PROJECT_LIBDEPS_DIR/$PIOENV/AsyncTCP/src
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@ platform_packages =
|
||||
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
|
||||
|
||||
[env:example_esp32]
|
||||
platform = espressif32
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@ platform_packages =
|
||||
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
|
||||
|
||||
[env:example_esp32]
|
||||
platform = espressif32@6.13.0
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -24,12 +24,14 @@ build_flags =
|
||||
-I$PROJECT_LIBDEPS_DIR/$PIOENV/ESPAsyncTCP/src
|
||||
|
||||
[env:example_esp32]
|
||||
platform = espressif32
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
ESP32Async/AsyncTCP
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DSOC_WIFI_SUPPORTED=1
|
||||
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
|
||||
-I$PROJECT_LIBDEPS_DIR/$PIOENV/AsyncTCP/src
|
||||
|
||||
|
||||
@@ -10,10 +10,6 @@
|
||||
// When DeviceFrameworkConfig is included, extern variables are declared with CONFIG_* names
|
||||
// We use DFTE_* internal names for compile-time constants (constexpr) and default parameter values
|
||||
// NEVER define CONFIG_* macros here to avoid conflicts with extern declarations in DeviceFrameworkConfig
|
||||
#ifndef DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT
|
||||
#define DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT 24
|
||||
#endif
|
||||
|
||||
#ifndef DFTE_MAX_PLACEHOLDERS_DEFAULT
|
||||
#define DFTE_MAX_PLACEHOLDERS_DEFAULT 16
|
||||
#endif
|
||||
@@ -119,7 +115,7 @@ public:
|
||||
static size_t getDynamicTemplateLength(const DynamicTemplateDescriptor* descriptor, const char* templateData);
|
||||
|
||||
private:
|
||||
static constexpr uint16_t MAX_PLACEHOLDER_NAME_SIZE = DFTE_PLACEHOLDER_NAME_SIZE;
|
||||
static constexpr uint16_t MAX_PLACEHOLDER_NAME_SIZE = DFTE_PLACEHOLDER_NAME_CAPACITY;
|
||||
|
||||
PlaceholderEntry* placeholders; // Dynamically allocated array
|
||||
uint16_t maxPlaceholders; // Configurable size
|
||||
|
||||
@@ -16,11 +16,9 @@
|
||||
#define DFTE_BUFFER_SIZE_DEFAULT 512
|
||||
#endif
|
||||
|
||||
#ifndef DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT
|
||||
#define DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT 24
|
||||
#endif
|
||||
|
||||
// Layout-affecting settings must be supplied through build-wide DFTE_* flags.
|
||||
// These are default capacities for a general-purpose standalone context. A
|
||||
// caller can select smaller per-context capacities when its template topology
|
||||
// is known, without changing the public object layout across translation units.
|
||||
#ifndef DFTE_MAX_STACK_DEPTH
|
||||
#define DFTE_MAX_STACK_DEPTH DFTE_MAX_STACK_DEPTH_DEFAULT
|
||||
#endif
|
||||
@@ -43,20 +41,26 @@ public:
|
||||
// Context state
|
||||
State state;
|
||||
|
||||
// Unified rendering stack
|
||||
// Standalone defaults retained for callers that use the no-argument
|
||||
// constructor and for tests that exercise the maximum supported depth.
|
||||
static constexpr int MAX_RENDERING_DEPTH = DFTE_MAX_STACK_DEPTH;
|
||||
RenderingContext renderingStack[MAX_RENDERING_DEPTH];
|
||||
static const size_t BUFFER_SIZE = DFTE_BUFFER_SIZE;
|
||||
|
||||
// Per-context storage avoids layout-affecting build flags. This also lets
|
||||
// constrained applications request only the stack and read buffer they
|
||||
// actually need for a specific response.
|
||||
RenderingContext* renderingStack;
|
||||
size_t maxRenderingDepth;
|
||||
int renderingDepth;
|
||||
|
||||
// Current placeholder being built (only valid during BUILDING_PLACEHOLDER state)
|
||||
// Allocated to configured size - matches CONFIG_templatePlaceholderNameSize when DeviceFramework is present
|
||||
char placeholderName[DFTE_PLACEHOLDER_NAME_SIZE];
|
||||
// Fixed ABI-stable placeholder token storage.
|
||||
char placeholderName[DFTE_PLACEHOLDER_NAME_CAPACITY];
|
||||
size_t placeholderPos;
|
||||
|
||||
// Centralized buffer management
|
||||
// Allocated to configured size - matches CONFIG_templateBufferSize when DeviceFramework is present
|
||||
static const size_t BUFFER_SIZE = DFTE_BUFFER_SIZE;
|
||||
uint8_t readBuffer[BUFFER_SIZE];
|
||||
uint8_t* readBuffer;
|
||||
size_t readBufferSize;
|
||||
size_t bufferPos;
|
||||
size_t bufferLen;
|
||||
size_t bufferOffset;
|
||||
@@ -68,9 +72,17 @@ public:
|
||||
size_t totalBytesProcessed;
|
||||
unsigned long startTime;
|
||||
|
||||
DeviceFrameworkTemplateContext();
|
||||
explicit DeviceFrameworkTemplateContext(
|
||||
size_t maxDepth = MAX_RENDERING_DEPTH,
|
||||
size_t bufferSize = BUFFER_SIZE);
|
||||
~DeviceFrameworkTemplateContext();
|
||||
DeviceFrameworkTemplateContext(const DeviceFrameworkTemplateContext&) = delete;
|
||||
DeviceFrameworkTemplateContext& operator=(const DeviceFrameworkTemplateContext&) = delete;
|
||||
void reset();
|
||||
|
||||
bool isReady() const { return renderingStack != nullptr && readBuffer != nullptr; }
|
||||
size_t getMaxRenderingDepth() const { return maxRenderingDepth; }
|
||||
size_t getBufferSize() const { return readBufferSize; }
|
||||
|
||||
// Unified stack management methods
|
||||
bool pushContext(RenderingContextType type, const char* name);
|
||||
|
||||
@@ -3,18 +3,9 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Fallback defaults when DeviceFrameworkConfig is not available (standalone usage)
|
||||
// Always use internal macro names (DFTE_*) to avoid conflicts with DeviceFrameworkConfig extern declarations
|
||||
#ifndef DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT
|
||||
#define DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT 24
|
||||
#endif
|
||||
|
||||
// Layout-affecting settings must be passed as build-wide DFTE_* flags so every
|
||||
// translation unit sees the same object layout. DeviceFramework runtime config is
|
||||
// intentionally not used for fixed-size storage.
|
||||
#ifndef DFTE_PLACEHOLDER_NAME_SIZE
|
||||
#define DFTE_PLACEHOLDER_NAME_SIZE DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT
|
||||
#endif
|
||||
// Placeholder-name storage has a fixed, ABI-stable capacity.
|
||||
// DFTE_PLACEHOLDER_NAME_SIZE overrides are intentionally ignored.
|
||||
static constexpr size_t DFTE_PLACEHOLDER_NAME_CAPACITY = 24;
|
||||
|
||||
/**
|
||||
* Placeholder types for template substitution
|
||||
@@ -82,7 +73,7 @@ struct ConditionalDescriptor {
|
||||
*/
|
||||
struct PlaceholderEntry {
|
||||
// Allocated to configured size - matches CONFIG_templatePlaceholderNameSize when DeviceFramework is present
|
||||
char name[DFTE_PLACEHOLDER_NAME_SIZE];
|
||||
char name[DFTE_PLACEHOLDER_NAME_CAPACITY];
|
||||
PlaceholderType type;
|
||||
const void* data;
|
||||
PlaceholderLengthGetter getLength;
|
||||
|
||||
+12
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "DeviceFrameworkTemplateEngine",
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.0",
|
||||
"description": "Memory-efficient streaming template engine for ESP8266/ESP32 with chunked rendering support. Designed for embedded web interfaces with PROGMEM template support.",
|
||||
"keywords": [
|
||||
"template",
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ test_framework = unity
|
||||
test_build_src = yes
|
||||
|
||||
[env:test_template_engine_esp32]
|
||||
platform = espressif32@6.13.0
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
board = nodemcu-32s
|
||||
framework = arduino
|
||||
test_framework = unity
|
||||
|
||||
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}"
|
||||
@@ -1,14 +1,26 @@
|
||||
#include "DeviceFrameworkTemplateContext.h"
|
||||
#include "DeviceFrameworkTemplateEngineDebug.h"
|
||||
#include <new>
|
||||
|
||||
DeviceFrameworkTemplateContext::DeviceFrameworkTemplateContext()
|
||||
: state(TemplateRenderState::TEXT), renderingDepth(0), placeholderPos(0),
|
||||
bufferPos(0), bufferLen(0), bufferOffset(0),
|
||||
registry(nullptr),
|
||||
DeviceFrameworkTemplateContext::DeviceFrameworkTemplateContext(size_t maxDepth, size_t bufferSize)
|
||||
: state(TemplateRenderState::TEXT), renderingStack(nullptr), maxRenderingDepth(maxDepth),
|
||||
renderingDepth(0), placeholderPos(0), readBuffer(nullptr), readBufferSize(bufferSize),
|
||||
bufferPos(0), bufferLen(0), bufferOffset(0), registry(nullptr),
|
||||
totalBytesProcessed(0), startTime(0) {
|
||||
memset(placeholderName, 0, sizeof(placeholderName));
|
||||
for (int i = 0; i < MAX_RENDERING_DEPTH; ++i) {
|
||||
renderingStack[i] = RenderingContext();
|
||||
if (maxRenderingDepth == 0 || readBufferSize == 0) {
|
||||
state = TemplateRenderState::ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
renderingStack = new (std::nothrow) RenderingContext[maxRenderingDepth];
|
||||
readBuffer = new (std::nothrow) uint8_t[readBufferSize];
|
||||
if (!isReady()) {
|
||||
delete[] renderingStack;
|
||||
delete[] readBuffer;
|
||||
renderingStack = nullptr;
|
||||
readBuffer = nullptr;
|
||||
state = TemplateRenderState::ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +28,16 @@ DeviceFrameworkTemplateContext::~DeviceFrameworkTemplateContext() {
|
||||
while (renderingDepth > 0) {
|
||||
popContext();
|
||||
}
|
||||
delete[] readBuffer;
|
||||
delete[] renderingStack;
|
||||
}
|
||||
|
||||
void DeviceFrameworkTemplateContext::reset() {
|
||||
if (!isReady()) {
|
||||
state = TemplateRenderState::ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
// An interrupted render may own an iterator handle. Pop through the stack
|
||||
// before overwriting it so every descriptor receives its close callback.
|
||||
while (renderingDepth > 0) {
|
||||
@@ -34,14 +53,14 @@ void DeviceFrameworkTemplateContext::reset() {
|
||||
totalBytesProcessed = 0;
|
||||
startTime = millis();
|
||||
memset(placeholderName, 0, sizeof(placeholderName));
|
||||
for (int i = 0; i < MAX_RENDERING_DEPTH; ++i) {
|
||||
for (size_t i = 0; i < maxRenderingDepth; ++i) {
|
||||
renderingStack[i] = RenderingContext();
|
||||
}
|
||||
}
|
||||
|
||||
// Unified stack management methods
|
||||
bool DeviceFrameworkTemplateContext::pushContext(RenderingContextType type, const char* name) {
|
||||
if (renderingDepth >= MAX_RENDERING_DEPTH) {
|
||||
if (!isReady() || static_cast<size_t>(renderingDepth) >= maxRenderingDepth) {
|
||||
DFTE_LOG_ERROR("Rendering stack overflow! Depth=" + String(renderingDepth));
|
||||
state = TemplateRenderState::ERROR;
|
||||
return false;
|
||||
@@ -154,7 +173,7 @@ RenderingContextType DeviceFrameworkTemplateContext::getCurrentContextType() con
|
||||
}
|
||||
|
||||
bool DeviceFrameworkTemplateContext::isComplete() const {
|
||||
return state == TemplateRenderState::COMPLETE || state == TemplateRenderState::ERROR;
|
||||
return state == TemplateRenderState::COMPLETE;
|
||||
}
|
||||
|
||||
bool DeviceFrameworkTemplateContext::hasError() const {
|
||||
@@ -195,6 +214,11 @@ String DeviceFrameworkTemplateContext::getStackTrace() const {
|
||||
}
|
||||
|
||||
bool DeviceFrameworkTemplateContext::refillBuffer() {
|
||||
if (!isReady()) {
|
||||
state = TemplateRenderState::ERROR;
|
||||
return false;
|
||||
}
|
||||
|
||||
RenderingContext* currentCtx = getCurrentContext();
|
||||
if (!currentCtx || currentCtx->type != RenderingContextType::TEMPLATE) {
|
||||
return false;
|
||||
@@ -205,7 +229,7 @@ bool DeviceFrameworkTemplateContext::refillBuffer() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bufferLen = min(BUFFER_SIZE, templateCtx.templateLen - templateCtx.position);
|
||||
bufferLen = min(readBufferSize, templateCtx.templateLen - templateCtx.position);
|
||||
|
||||
if (bufferLen > 0) {
|
||||
if (templateCtx.isProgmem) {
|
||||
|
||||
@@ -19,6 +19,27 @@ void test_template_context_initialization() {
|
||||
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.bufferPos, "Initial bufferPos should be 0");
|
||||
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.bufferLen, "Initial bufferLen should be 0");
|
||||
TEST_ASSERT_NULL_MESSAGE(ctx.registry, "Initial registry should be null");
|
||||
TEST_ASSERT_TRUE_MESSAGE(ctx.isReady(), "Default context storage should allocate");
|
||||
TEST_ASSERT_EQUAL_UINT32(TemplateContext::MAX_RENDERING_DEPTH, ctx.getMaxRenderingDepth());
|
||||
TEST_ASSERT_EQUAL_UINT32(TemplateContext::BUFFER_SIZE, ctx.getBufferSize());
|
||||
|
||||
TemplateContext constrained(3, 64);
|
||||
TEST_ASSERT_TRUE_MESSAGE(constrained.isReady(), "Constrained context storage should allocate");
|
||||
TEST_ASSERT_EQUAL_UINT32(3, constrained.getMaxRenderingDepth());
|
||||
TEST_ASSERT_EQUAL_UINT32(64, constrained.getBufferSize());
|
||||
TEST_ASSERT_TRUE_MESSAGE(constrained.pushContext(RenderingContextType::TEMPLATE, "%ONE%"),
|
||||
"First constrained stack frame should fit");
|
||||
TEST_ASSERT_TRUE_MESSAGE(constrained.pushContext(RenderingContextType::TEMPLATE, "%TWO%"),
|
||||
"Second constrained stack frame should fit");
|
||||
TEST_ASSERT_TRUE_MESSAGE(constrained.pushContext(RenderingContextType::TEMPLATE, "%THREE%"),
|
||||
"Third constrained stack frame should fit");
|
||||
TEST_ASSERT_FALSE_MESSAGE(constrained.pushContext(RenderingContextType::TEMPLATE, "%FOUR%"),
|
||||
"A constrained context must reject stack overflow");
|
||||
TEST_ASSERT_TRUE_MESSAGE(constrained.hasError(), "Constrained stack overflow should be reported");
|
||||
|
||||
TemplateContext invalid(0, 64);
|
||||
TEST_ASSERT_FALSE_MESSAGE(invalid.isReady(), "A zero-depth context must fail safely");
|
||||
TEST_ASSERT_TRUE_MESSAGE(invalid.hasError(), "An invalid context must report an error");
|
||||
|
||||
// Test reset
|
||||
ctx.pushContext(RenderingContextType::TEMPLATE, "TEST");
|
||||
@@ -38,7 +59,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 +331,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