From fb0176a5c1e6431c1584cf5ab2c964e3b4149845 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Fri, 21 Aug 2026 10:45:46 +0200 Subject: [PATCH 01/11] fix: reserve bounded console input buffer during initialization (#10) --- src/console/ESPressio_Console.hpp | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/console/ESPressio_Console.hpp b/src/console/ESPressio_Console.hpp index ee4bf2d..eb41aa7 100644 --- a/src/console/ESPressio_Console.hpp +++ b/src/console/ESPressio_Console.hpp @@ -92,7 +92,6 @@ class Console final { static_cast( right[index] ) - ) ) { return false; } @@ -200,10 +199,22 @@ class Console final { Print& output, const ConsoleConfig& config = {} ) { + ConsoleConfig preparedConfig; + std::string preparedLine; + + try { + preparedConfig = config; + preparedLine.reserve( + preparedConfig.MaximumLineLength + ); + } catch (...) { + return false; + } + _input = &input; _output = &output; - _config = config; - _line.clear(); + _config = std::move(preparedConfig); + _line = std::move(preparedLine); _discardUntilNewline = false; PrintPrompt(); @@ -232,6 +243,12 @@ class Console final { return _output; } +#ifdef ESPRESSIO_SERIAL_TESTING + std::size_t __GetInputBufferCapacityForTesting() const noexcept { + return _line.capacity(); + } +#endif + uint32_t RegisterLineInterceptor( ConsoleLineInterceptor interceptor ) { From 0f40f147def2fc8f2f44e8c407ab38e2c7a3685f Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Fri, 21 Aug 2026 10:46:10 +0200 Subject: [PATCH 02/11] test: cover bounded console input reservation (#10) --- tests/test_console.cpp | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/test_console.cpp b/tests/test_console.cpp index 6820e30..cef9e04 100644 --- a/tests/test_console.cpp +++ b/tests/test_console.cpp @@ -56,6 +56,7 @@ int main() { ConsoleConfig config; config.ShowPrompt = false; + config.MaximumLineLength = 128; assert( console.Initialize( @@ -65,6 +66,14 @@ int main() { ) ); + const auto reservedCapacity = + console.__GetInputBufferCapacityForTesting(); + + assert( + reservedCapacity >= + config.MaximumLineLength + ); + bool called = false; std::string lastArguments; @@ -145,6 +154,48 @@ int main() { assert(called); assert(lastArguments == "from poll"); + assert( + console.__GetInputBufferCapacityForTesting() == + reservedCapacity + ); + + stream.Input = + std::string( + config.MaximumLineLength, + 'x' + ) + + "\n"; + + stream.ReadOffset = 0; + console.Poll(); + + assert( + console.__GetInputBufferCapacityForTesting() == + reservedCapacity + ); + + stream.Input = + std::string( + config.MaximumLineLength + 1, + 'y' + ) + + "\n"; + + stream.ReadOffset = 0; + stream.Output.clear(); + console.Poll(); + + assert( + stream.Output.find( + "Input rejected: line exceeds configured maximum length." + ) != + std::string::npos + ); + + assert( + console.__GetInputBufferCapacityForTesting() == + reservedCapacity + ); return 0; } From 62c8b5488b19c5ca57493ace09e9bc2ca6954d3e Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Fri, 21 Aug 2026 10:46:41 +0200 Subject: [PATCH 03/11] test: expose console buffer capacity only to host tests (#10) --- tests/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 10eddfc..9857137 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,7 @@ add_executable( target_compile_features(test_console PRIVATE cxx_std_17) target_compile_options(test_console PRIVATE -Wall -Wextra -Wpedantic -Werror) +target_compile_definitions(test_console PRIVATE ESPRESSIO_SERIAL_TESTING) target_include_directories(test_console PRIVATE stubs ../src) add_test(NAME Console COMMAND test_console) From 09c17cfcdcbf281a20ad4e1f8aacb4019f73d967 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Fri, 21 Aug 2026 10:47:11 +0200 Subject: [PATCH 04/11] chore: bump Serial patch version to 0.5.2 (#10) --- library.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.json b/library.json index 67f6bb2..7223bd4 100644 --- a/library.json +++ b/library.json @@ -16,7 +16,7 @@ "type": "git", "url": "https://github.com/Flowduino/ESPressio-Serial.git" }, - "version": "0.5.1", + "version": "0.5.2", "license": "Apache-2.0", "frameworks": "arduino", "platforms": "espressif32" From 280a4b66f82f1e236803086a9e7f732203c19029 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Fri, 21 Aug 2026 10:47:23 +0200 Subject: [PATCH 05/11] chore: update Arduino metadata for Serial 0.5.2 (#10) --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 6c52610..27344cf 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ESPressio-Serial -version=0.5.1 +version=0.5.2 author=Flowduino maintainer=Flowduino sentence=Serial console, diagnostics, logging and operator tooling for the ESPressio ecosystem. From fe951cb9d112753cf97a12bf9ce132afa371a41e Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Fri, 21 Aug 2026 10:47:37 +0200 Subject: [PATCH 06/11] chore: correct component version metadata for 0.5.2 (#10) --- component.mk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/component.mk b/component.mk index 02b1fdc..3492875 100644 --- a/component.mk +++ b/component.mk @@ -21,5 +21,5 @@ CPPFLAGS += \ -DESPRESSIO_SERIAL \ -DESPRESSIO_SERIAL_VERSION_MAJOR=0 \ -DESPRESSIO_SERIAL_VERSION_MINOR=5 \ - -DESPRESSIO_SERIAL_VERSION_PATCH=0 \ - -DESPRESSIO_SERIAL_VERSION_STRING=\"0.5.0\" + -DESPRESSIO_SERIAL_VERSION_PATCH=2 \ + -DESPRESSIO_SERIAL_VERSION_STRING=\"0.5.2\" From 458bda8581565cf308b0d6fa07f81fa1a6702747 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Fri, 21 Aug 2026 10:48:55 +0200 Subject: [PATCH 07/11] docs: document Serial 0.5.2 console heap-pressure fix (#10) --- CHANGELOG.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff1bf76..c101f8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## 0.5.2 + +### Fixed + +- Fixed a heap-pressure failure path in `Console::Poll()` where the bounded input line still grew its backing `std::string` incrementally and could reach `std::terminate()` if a late `push_back()` allocation failed. +- `Console::Initialize()` now reserves the configured `MaximumLineLength` capacity before publishing the console as initialized. +- If the bounded input capacity cannot be reserved, console initialization now fails cleanly instead of leaving a partially initialized console that can fail later while accepting input. +- Corrected `component.mk` version macros that had remained at 0.5.0 after the 0.5.1 release. + +### Tests + +- Added regression coverage confirming that the configured input capacity is established during initialization and is retained across normal polling, buffer clearing, maximum-length input, and over-length discard handling. +- The test-only capacity accessor is compiled only under `ESPRESSIO_SERIAL_TESTING` and does not alter the production API surface. + +### Compatibility + +- No breaking public API changes. +- Existing maximum-line-length, discard, prompt, echo, command, and interceptor behavior is unchanged. +- Core Serial remains dependency-free. + ## 0.5.1 ### Fixed @@ -104,7 +124,6 @@ The structure follows the principles of [Keep a Changelog](https://keepachangelo - Added optional pre-dispatch operator confirmation. - Added safe-by-default Event dispatch authorization using allow-list, allow-all, and deny-list policies; deny-list entries override broader access. - Added optional `ILoggerSink` audit integration for operator dispatch, denial, malformed JSON, and construction/dispatch failures. -- Added `Console`, `EventConsole`, and `EventConsoleLoopback` examples. - Added host-side `Console` tests and an `EventConsole` contract test covering discovery, description, access policy, JSON command handling, confirmation, and dispatch. - Added the 0.3.0 feature specification. From f20ff81ca075d65eabcc78dc15db1f9d281274cc Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Fri, 21 Aug 2026 10:50:00 +0200 Subject: [PATCH 08/11] docs: update dependency chart for Serial 0.5.2 (#10) --- ESPRESSIO_DEPENDENCY_CHART.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ESPRESSIO_DEPENDENCY_CHART.md b/ESPRESSIO_DEPENDENCY_CHART.md index 491818b..26ef535 100644 --- a/ESPRESSIO_DEPENDENCY_CHART.md +++ b/ESPRESSIO_DEPENDENCY_CHART.md @@ -1,8 +1,8 @@ -# ESPressio Dependency Chart — Serial 0.5.1 +# ESPressio Dependency Chart — Serial 0.5.2 ![ESPressio Library Dependency Chart](ESPRESSIO_DEPENDENCY_CHART.png) -## ESPressio Serial 0.5.1 +## ESPressio Serial 0.5.2 The Serial core and generic `Console` have no mandatory ESPressio dependencies. All ESPressio integrations remain opt-in. @@ -37,9 +37,10 @@ EventMonitor / EventConsole - - -> ESPressio Serializable >= 0.10.2 < 1.0.0 ``` -EventMonitor 0.5.1 uses Serializable 0.10.2's bounded, allocation-free ESPB -traversal API for structured diagnostics. Serializable 0.10.2 also contains the -strict-build warning correction required by Serial's `-Werror` host validation. +EventMonitor 0.5.2 retains Serializable 0.10.2's bounded, allocation-free ESPB +traversal API for structured diagnostics. Serial 0.5.2 additionally reserves +the generic Console's configured bounded input capacity during initialization, +removing avoidable line-buffer growth allocations from `Console::Poll()`. ## Current coordinated ecosystem @@ -73,7 +74,7 @@ EVENT └── Serializable >= 0.10.2 < 1.0.0 [optional] DIAGNOSTICS / OPERATOR -└── Serial 0.5.1 +└── Serial 0.5.2 ``` ## Dependency-direction rule From 1b5123e55347a47265d8f0900a078ccb1e52383f Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Fri, 21 Aug 2026 10:51:26 +0200 Subject: [PATCH 09/11] docs: document bounded Console allocation in Serial 0.5.2 (#10) --- README.md | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 07a84ab..06fb1d5 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ Serial and console-oriented components for the Flowduino ESPressio Development Platform. -Version 0.5.1 hardens the opt-in Event Monitor so structured Event Transport diagnostics are rendered directly from bounded ESPB bytes without constructing a second heap-backed `SerializationNode` tree. It retains the Observable-backed monitors introduced in 0.5.0 while preserving the dependency-free Serial core and existing Console/Event integrations. +Version 0.5.2 hardens the generic Console under heap pressure by reserving its configured bounded input capacity during initialization, so normal line polling no longer grows the backing `std::string` incrementally. It retains the EventMonitor safety work from 0.5.1 and the Observable-backed monitors introduced in 0.5.0 while preserving the dependency-free Serial core. -## Current Version — 0.5.1 +## Current Version — 0.5.2 -Version **0.5.1** fixes the structured EventMonitor crash path reproduced on ESP32 under low-memory conditions. Structured diagnostics now use ESPressio Serializable 0.10.2's allocation-free BinaryArchive traversal API with explicit depth, aggregate-node, collection, name, and string limits. Invalid or outside-limit payloads fall back to bounded hexadecimal output rather than becoming fatal diagnostic work. +Version **0.5.2** fixes a low-memory failure path reproduced on ESP32 where `Console::Poll()` could reach `std::terminate()` when `std::string::push_back()` needed to grow the input buffer after the system was already under severe heap pressure. `Console::Initialize()` now prepares the configuration and reserves `ConsoleConfig::MaximumLineLength` before publishing the console as initialized. If that bounded capacity cannot be reserved, initialization returns `false` cleanly. + +The EventMonitor structured-payload hardening from 0.5.1 remains unchanged: structured diagnostics use ESPressio Serializable 0.10.2's allocation-free BinaryArchive traversal API with explicit depth, aggregate-node, collection, name, and string limits. Invalid or outside-limit payloads fall back to bounded hexadecimal output rather than becoming fatal diagnostic work. The Observable-backed monitor integrations introduced in 0.5.0 remain available unchanged: @@ -34,7 +36,7 @@ These monitors subscribe directly to the originating library's Observable contra Historical documentation for earlier release generations remains below where useful. -Current coordinated dependency baselines for the 0.5.1 release are Units 0.2.3, Timing 2.2.4, Threads 3.1.4, ESP-Now 0.5.2, Event 5.8.2, and Serializable 0.10.2. Command 0.3.0, Security 0.2.0, and Sockets 0.5.0 remain the current optional integration baselines. +Current coordinated dependency baselines for the 0.5.2 release are Units 0.2.3, Timing 2.2.4, Threads 3.1.4, ESP-Now 0.5.2, Event 5.8.2, and Serializable 0.10.2. Command 0.3.0, Security 0.2.0, and Sockets 0.5.0 remain the current optional integration baselines. ## ESPressio Development Platform @@ -163,11 +165,14 @@ void setup() { ESPressio::Serial::ConsoleConfig config; config.Prompt = "espressio> "; - console.Initialize( + if (!console.Initialize( ::Serial, ::Serial, config - ); + )) { + // The bounded input buffer could not be reserved. + return; + } console.RegisterCommand( "hello", @@ -193,7 +198,9 @@ The line buffer is bounded through: ConsoleConfig::MaximumLineLength ``` -and the console supports: +Beginning with 0.5.2, that bounded capacity is reserved during `Initialize()`. A successful initialization therefore guarantees that ordinary input up to `MaximumLineLength` does not need to grow the backing line buffer while `Poll()` is running. If the reservation cannot be satisfied, initialization returns `false` and the console remains uninitialized. + +The console supports: ```text command registration @@ -520,6 +527,9 @@ argument preservation multiple interactive line interceptors interceptor removal Stream polling +bounded Console input capacity reservation +capacity retention across polling and line clearing +over-length discard handling without buffer growth runtime Event listing Event schema description allow-list enforcement @@ -880,7 +890,7 @@ config.MaximumHexPayloadBytes ESPressio Event Transport serializes Event payloads using ESPressio Serializable's BinaryArchive ESPB v2 representation. -Beginning with Serial 0.5.1, EventMonitor does **not** decode that payload into a second `SerializationNode` tree merely for presentation. Instead, it uses Serializable 0.10.1's `TraverseBinaryArchive()` API to validate and stream the existing ESPB bytes directly to the selected Arduino `Print` destination. +Beginning with Serial 0.5.1, EventMonitor does **not** decode that payload into a second `SerializationNode` tree merely for presentation. Instead, it uses Serializable 0.10.2's `TraverseBinaryArchive()` API to validate and stream the existing ESPB bytes directly to the selected Arduino `Print` destination. This keeps human-readable structured diagnostics independent of the concrete C++ Event type and avoids ArduinoJson, while removing duplicate payload-tree allocations from the synchronous Event Transport observer path. @@ -981,16 +991,16 @@ A project using only the core Serial library: ```ini lib_deps = - flowduino/ESPressio-Serial@^0.5.1 + flowduino/ESPressio-Serial@^0.5.2 ``` An application using Event Monitor requires: ```ini lib_deps = - flowduino/ESPressio-Serial@^0.5.1 - flowduino/ESPressio-Event@^5.8.1 - flowduino/ESPressio-Serializable@^0.10.1 + flowduino/ESPressio-Serial@^0.5.2 + flowduino/ESPressio-Event@^5.8.2 + flowduino/ESPressio-Serializable@^0.10.2 ``` The Event/Serializable dependencies are intentionally not declared as mandatory package dependencies of ESPressio Serial because they are required only by the opt-in Event Monitor feature. @@ -1004,16 +1014,16 @@ The generic console requires only ESPressio Serial: ```ini lib_deps = - flowduino/ESPressio-Serial@^0.5.1 + flowduino/ESPressio-Serial@^0.5.2 ``` The Event Console additionally requires the runtime Event and JSON stacks: ```ini lib_deps = - flowduino/ESPressio-Serial@^0.5.1 - flowduino/ESPressio-Event@^5.8.1 - flowduino/ESPressio-Serializable@^0.10.1 + flowduino/ESPressio-Serial@^0.5.2 + flowduino/ESPressio-Event@^5.8.2 + flowduino/ESPressio-Serializable@^0.10.2 bblanchon/ArduinoJson ``` From 196c0493ad651a1a7025d2a216742a13e8ad929d Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Fri, 21 Aug 2026 10:52:08 +0200 Subject: [PATCH 10/11] ci: validate Serial 0.5.2 heap-pressure fix on bugfix branch (#10) --- .github/workflows/host-tests.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/host-tests.yml b/.github/workflows/host-tests.yml index 8a89fc1..c216287 100644 --- a/.github/workflows/host-tests.yml +++ b/.github/workflows/host-tests.yml @@ -2,7 +2,7 @@ name: Host Tests on: push: - branches: [main, bugfix/7-event-monitor-structured-payload-safety, feature/8-dependency-refresh-0.5.1] + branches: [main, bugfix/10-console-poll-heap-pressure] pull_request: jobs: @@ -144,6 +144,7 @@ jobs: cat > project/compile/src/main.cpp <<'EOF' #include #include + #include #include #include #include @@ -152,8 +153,15 @@ jobs: #include #include - void setup() {} + ESPressio::Serial::Console console; + + void setup() { + ESPressio::Serial::ConsoleConfig config; + config.ShowPrompt = false; + config.MaximumLineLength = 128; + console.Initialize(Serial, Serial, config); + } void loop() {} EOF - - name: Compile ESP32 Observable and Event monitors + - name: Compile ESP32 Console and Observable/Event monitors run: pio run -d project/compile From fdcf967e28579861a4e59cce450e5bc892622ca1 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Fri, 21 Aug 2026 10:53:28 +0200 Subject: [PATCH 11/11] test: verify clean initialization failure for impossible console capacity (#10) --- tests/test_console.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_console.cpp b/tests/test_console.cpp index cef9e04..46c2f95 100644 --- a/tests/test_console.cpp +++ b/tests/test_console.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -52,6 +53,26 @@ int main() { TestStream stream; + { + Console impossibleConsole; + ConsoleConfig impossibleConfig; + impossibleConfig.ShowPrompt = false; + impossibleConfig.MaximumLineLength = + std::numeric_limits::max(); + + assert( + !impossibleConsole.Initialize( + stream, + stream, + impossibleConfig + ) + ); + + assert( + !impossibleConsole.GetIsInitialized() + ); + } + Console console; ConsoleConfig config;