mirror of
https://github.com/alexhopeoconnor/DFTE.git
synced 2026-10-03 22:21:59 +10:00
Release v1.1.0
This commit is contained in:
@@ -3,6 +3,7 @@ name: Build
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user