diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a2d311..567ebff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,7 @@ name: Build on: push: branches: [main] + tags: ["v*"] pull_request: concurrency: diff --git a/CHANGELOG.md b/CHANGELOG.md index de104a1..0ad51d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 08934c0..3ff48bf 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d7c9259..3d2ad6f 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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). diff --git a/examples/AsyncDashboardDemo/platformio.ini b/examples/AsyncDashboardDemo/platformio.ini index 748f08c..534482c 100644 --- a/examples/AsyncDashboardDemo/platformio.ini +++ b/examples/AsyncDashboardDemo/platformio.ini @@ -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 diff --git a/examples/HelloPlaceholder/platformio.ini b/examples/HelloPlaceholder/platformio.ini index 5c7e4a3..fced0a1 100644 --- a/examples/HelloPlaceholder/platformio.ini +++ b/examples/HelloPlaceholder/platformio.ini @@ -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 diff --git a/examples/NestedLayouts/platformio.ini b/examples/NestedLayouts/platformio.ini index f289a4c..fced0a1 100644 --- a/examples/NestedLayouts/platformio.ini +++ b/examples/NestedLayouts/platformio.ini @@ -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 diff --git a/examples/StreamingAsync/platformio.ini b/examples/StreamingAsync/platformio.ini index 972740f..a5fa4c9 100644 --- a/examples/StreamingAsync/platformio.ini +++ b/examples/StreamingAsync/platformio.ini @@ -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 diff --git a/include/DeviceFrameworkPlaceholderRegistry.h b/include/DeviceFrameworkPlaceholderRegistry.h index 52ef90f..ae47a17 100644 --- a/include/DeviceFrameworkPlaceholderRegistry.h +++ b/include/DeviceFrameworkPlaceholderRegistry.h @@ -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 diff --git a/include/DeviceFrameworkTemplateContext.h b/include/DeviceFrameworkTemplateContext.h index 0d5cdc2..66aa211 100644 --- a/include/DeviceFrameworkTemplateContext.h +++ b/include/DeviceFrameworkTemplateContext.h @@ -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); diff --git a/include/DeviceFrameworkTemplateTypes.h b/include/DeviceFrameworkTemplateTypes.h index 4eeb5a6..e60470e 100644 --- a/include/DeviceFrameworkTemplateTypes.h +++ b/include/DeviceFrameworkTemplateTypes.h @@ -3,18 +3,9 @@ #include -// 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; diff --git a/library.json b/library.json index ec5017a..6872308 100644 --- a/library.json +++ b/library.json @@ -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", diff --git a/platformio.ini b/platformio.ini index 9798795..1a6cce4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/src/DeviceFrameworkTemplateContext.cpp b/src/DeviceFrameworkTemplateContext.cpp index 6482f21..c18d6c7 100644 --- a/src/DeviceFrameworkTemplateContext.cpp +++ b/src/DeviceFrameworkTemplateContext.cpp @@ -1,14 +1,26 @@ #include "DeviceFrameworkTemplateContext.h" #include "DeviceFrameworkTemplateEngineDebug.h" +#include -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(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) { diff --git a/test/test_template_engine/tests/test_template_context.cpp b/test/test_template_engine/tests/test_template_context.cpp index 550fe28..146c04b 100644 --- a/test/test_template_engine/tests/test_template_context.cpp +++ b/test/test_template_engine/tests/test_template_context.cpp @@ -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");