Release v1.1.0

This commit is contained in:
2026-09-01 10:57:59 +10:00
parent 6a5b3aab9e
commit 65d2cb38ae
15 changed files with 138 additions and 65 deletions
+1
View File
@@ -3,6 +3,7 @@ name: Build
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
concurrency:
+8
View File
@@ -1,5 +1,13 @@
# 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
+2 -2
View File
@@ -47,14 +47,14 @@ void loop() {
```ini
lib_deps =
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.0.2
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.1.0
```
PlatformIO checks out the Git ref after `#`; GitHub Release assets are unrelated. DFTE’s supported release targets are ESP8266 and ESP32.
## Documentation
Read the [documentation index](docs/README.md) for template syntax, async web responses, ABI-safe build flags, examples, tests, and releases.
Read the [documentation index](docs/README.md) for template syntax, async web responses, per-context memory sizing, examples, tests, and releases.
## Development and releases
+33 -17
View File
@@ -1,25 +1,41 @@
# 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.
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.
```ini
build_flags =
-DDFTE_BUFFER_SIZE=768
-DDFTE_MAX_STACK_DEPTH=24
-DDFTE_PLACEHOLDER_NAME_SIZE=32
-DDFTE_MAX_ITERATIONS=80
```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.
}
```
| 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 |
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.
`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.
| 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).
+3 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+3 -1
View File
@@ -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
+1 -5
View File
@@ -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
+25 -13
View File
@@ -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,10 +72,18 @@ 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);
void popContext();
+4 -13
View File
@@ -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;
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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
+33 -9
View File
@@ -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;
@@ -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");