Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions .github/workflows/host-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -144,6 +144,7 @@ jobs:
cat > project/compile/src/main.cpp <<'EOF'
#include <Arduino.h>
#include <WiFi.h>
#include <ESPressio_Console.hpp>
#include <ESPressio_CommandMonitor.hpp>
#include <ESPressio_SecurityMonitor.hpp>
#include <ESPressio_SocketWorkerMonitor.hpp>
Expand All @@ -152,8 +153,15 @@ jobs:
#include <ESPressio_EventMonitor.hpp>
#include <event/ESPressio_EventMonitorPayloadSafety.hpp>

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
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.

Expand Down
13 changes: 7 additions & 6 deletions ESPRESSIO_DEPENDENCY_CHART.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
42 changes: 26 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand All @@ -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
```

Expand Down
4 changes: 2 additions & 2 deletions component.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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\"
2 changes: 1 addition & 1 deletion library.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion library.properties
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
23 changes: 20 additions & 3 deletions src/console/ESPressio_Console.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ class Console final {
static_cast<unsigned char>(
right[index]
)
)
) {
return false;
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
) {
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
72 changes: 72 additions & 0 deletions tests/test_console.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <cassert>
#include <limits>
#include <string>

#include <ESPressio_Console.hpp>
Expand Down Expand Up @@ -52,10 +53,31 @@ int main() {

TestStream stream;

{
Console impossibleConsole;
ConsoleConfig impossibleConfig;
impossibleConfig.ShowPrompt = false;
impossibleConfig.MaximumLineLength =
std::numeric_limits<std::size_t>::max();

assert(
!impossibleConsole.Initialize(
stream,
stream,
impossibleConfig
)
);

assert(
!impossibleConsole.GetIsInitialized()
);
}

Console console;

ConsoleConfig config;
config.ShowPrompt = false;
config.MaximumLineLength = 128;

assert(
console.Initialize(
Expand All @@ -65,6 +87,14 @@ int main() {
)
);

const auto reservedCapacity =
console.__GetInputBufferCapacityForTesting();

assert(
reservedCapacity >=
config.MaximumLineLength
);

bool called = false;
std::string lastArguments;

Expand Down Expand Up @@ -145,6 +175,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;
}
Loading