diff --git a/.github/workflows/host-tests.yml b/.github/workflows/host-tests.yml index de55e1b..8a89fc1 100644 --- a/.github/workflows/host-tests.yml +++ b/.github/workflows/host-tests.yml @@ -2,7 +2,7 @@ name: Host Tests on: push: - branches: [main, feature/observable-callback-coverage] + branches: [main, bugfix/7-event-monitor-structured-payload-safety, feature/8-dependency-refresh-0.5.1] pull_request: jobs: @@ -23,11 +23,18 @@ jobs: repository: Flowduino/ESPressio-Observable ref: 3.0.1 path: deps/ESPressio-Observable + - name: Checkout ESPressio Serializable 0.10.2 + uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Serializable + ref: 0.10.2 + path: deps/ESPressio-Serializable - name: Configure run: >- cmake -S tests -B build -DESPRESSIO_COMMAND_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Command/src" -DESPRESSIO_OBSERVABLE_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Observable/src" + -DESPRESSIO_SERIALIZABLE_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Serializable/src" - name: Build run: cmake --build build --parallel - name: Test @@ -46,18 +53,36 @@ jobs: repository: Flowduino/ESPressio-Observable ref: 3.0.1 path: project/dependencies/ESPressio-Observable - - name: Checkout ESPressio Units 0.2.1 + - name: Checkout ESPressio Units 0.2.3 uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-Units - ref: 0.2.1 + ref: 0.2.3 path: project/dependencies/ESPressio-Units - - name: Checkout ESPressio Timing 2.2.2 + - name: Checkout ESPressio Timing 2.2.4 uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-Timing - ref: 2.2.2 + ref: 2.2.4 path: project/dependencies/ESPressio-Timing + - name: Checkout ESPressio Threads 3.1.4 + uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Threads + ref: 3.1.4 + path: project/dependencies/ESPressio-Threads + - name: Checkout ESPressio Serializable 0.10.2 + uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Serializable + ref: 0.10.2 + path: project/dependencies/ESPressio-Serializable + - name: Checkout ESPressio Event 5.8.2 + uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Event + ref: 5.8.2 + path: project/dependencies/ESPressio-Event - name: Checkout ESPressio Command 0.3.0 uses: actions/checkout@v4 with: @@ -76,11 +101,11 @@ jobs: repository: Flowduino/ESPressio-Sockets ref: 0.5.0 path: project/dependencies/ESPressio-Sockets - - name: Checkout ESPressio ESP-Now 0.5.0 + - name: Checkout ESPressio ESP-Now 0.5.2 uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-ESP-Now - ref: 0.5.0 + ref: 0.5.2 path: project/dependencies/ESPressio-ESP-Now - uses: actions/setup-python@v5 with: @@ -111,6 +136,9 @@ jobs: ../dependencies/ESPressio-Observable ../dependencies/ESPressio-Units ../dependencies/ESPressio-Timing + ../dependencies/ESPressio-Threads + ../dependencies/ESPressio-Serializable + ../dependencies/ESPressio-Event ../ESPressio-Serial EOF cat > project/compile/src/main.cpp <<'EOF' @@ -121,9 +149,11 @@ jobs: #include #include #include + #include + #include void setup() {} void loop() {} EOF - - name: Compile ESP32 Observable monitors + - name: Compile ESP32 Observable and Event monitors run: pio run -d project/compile diff --git a/CHANGELOG.md b/CHANGELOG.md index f0aae0e..ff1bf76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## 0.5.1 + +### Fixed + +- Replaced EventMonitor's tree-building `BinaryArchive::Load()` diagnostic path with bounded, allocation-free ESPB traversal from ESPressio Serializable 0.10.2, preventing valid payload diagnostics from requiring a second heap-backed `SerializationNode` tree. +- Hardened structured `EventMonitor` payload diagnostics so malformed, truncated, excessively nested, or otherwise unreasonable Event Transport payloads are rejected under monitor-specific decode limits. +- Added fail-safe fallback from `Structured` to bounded `Hex` output whenever a payload fails structured validation or exceeds the configured monitor limits. +- Updated Event Monitor's optional Serializable baseline to ESPressio Serializable >= 0.10.2 < 1.0.0, consuming the bounded/allocation-free BinaryArchive facilities introduced for Flowduino/ESPressio-Serializable#2. + +### Added + +- Added `MaximumStructuredNodes` to `EventMonitorConfig` alongside the existing collection, string, and nesting limits. +- Added deterministic malformed/deep/random payload regression and stress coverage for the EventMonitor structured-payload validation path. +- Added ESP32 compile validation for `EventMonitor` against the coordinated dependency-refresh candidates: Units 0.2.3, Timing 2.2.4, Threads 3.1.4, ESP-Now 0.5.2, Event 5.8.2, and released Serializable 0.10.2. + +### Changed + +- Raised the current optional Event integration baseline to ESPressio Event >= 5.8.2 < 6.0.0. +- Raised the current optional ESP-Now monitor baseline to ESPressio ESP-Now >= 0.5.2 < 1.0.0. +- Updated current documentation and CI to consume the completed Serializable 0.10.2 dependency cascade rather than intermediate bug-fix commits. +- Serializable 0.10.2 also resolves the strict-build `-Wmisleading-indentation` warning exposed by Serial's warnings-as-errors host validation. + +### Compatibility + +- Core Serial remains dependency-free. +- EventMonitor remains opt-in. +- Existing structured output remains JSON-like and source-compatible for payloads that validate successfully. + ## 0.5.0 - Added opt-in `CommandMonitor` for ESPressio Command 0.3.x registry lifecycle observation. @@ -144,4 +172,4 @@ The structure follows the principles of [Keep a Changelog](https://keepachangelo ### Dependency model - Core ESPressio Serial has no mandatory ESPressio library dependencies. -- Event Monitor is opt-in and requires ESPressio Event 5.5.0 or newer plus ESPressio Serializable 0.9.0 or newer. +- Event Monitor is opt-in and requires ESPressio Event 5.5.0 or newer plus ESPressio Serializable 0.9.0 or newer. \ No newline at end of file diff --git a/ESPRESSIO_DEPENDENCY_CHART.md b/ESPRESSIO_DEPENDENCY_CHART.md index df06c04..491818b 100644 --- a/ESPRESSIO_DEPENDENCY_CHART.md +++ b/ESPRESSIO_DEPENDENCY_CHART.md @@ -1,125 +1,129 @@ -# ESPressio Dependency Chart +# ESPressio Dependency Chart — Serial 0.5.1 ![ESPressio Library Dependency Chart](ESPRESSIO_DEPENDENCY_CHART.png) -## Purpose +## ESPressio Serial 0.5.1 -This document describes the current dependency relationships between ESPressio libraries relevant to ESPressio Serial 0.5.0. +The Serial core and generic `Console` have no mandatory ESPressio dependencies. +All ESPressio integrations remain opt-in. -The chart is hierarchical: libraries with no **required** ESPressio dependencies appear at the top, while libraries that build on progressively more of the ecosystem appear lower. - -- **Solid arrow** — required ESPressio dependency. -- **Dashed arrow** — opt-in dependency activated only by the associated feature/header. -- Arrows point from the dependent library to the library it consumes. - -## ESPressio Serial 0.5.0 - -The ESPressio Serial core and generic `Console` have no required ESPressio dependency. - -All integrations remain opt-in. - -### CommandConsole and CommandMonitor - -`CommandConsole` and `CommandMonitor` consume: +### Current integration baselines ```text -ESPressio Command >= 0.3.0 < 1.0.0 -``` +CommandConsole / CommandMonitor + - - -> ESPressio Command >= 0.3.0 < 1.0.0 -Command supplies the transport-neutral typed Command registry, parsing, validation, invocation, help/completion metadata, scoped command registration, and Observable registry lifecycle used by Serial's Command integrations. +SecurityMonitor + - - -> ESPressio Security >= 0.2.0 < 1.0.0 -### SecurityMonitor +SocketWorkerMonitor + - - -> ESPressio Sockets >= 0.5.0 < 1.0.0 -`SecurityMonitor` consumes: +SocketSecuritySessionMonitor + - - -> ESPressio Sockets >= 0.5.0 < 1.0.0 + - - -> ESPressio Security >= 0.2.0 < 1.0.0 -```text -ESPressio Security >= 0.2.0 < 1.0.0 -``` +ESPNowTransportMonitor + - - -> ESPressio ESP-Now >= 0.5.2 < 1.0.0 -Security supplies the Observable configuration, secure-session, replay-protection, and failure lifecycle observed directly by the monitor. +SystemClockMonitor + - - -> ESPressio Timing >= 2.2.4 < 3.0.0 -### Socket monitors +ThreadMonitor + - - -> ESPressio Threads >= 3.1.4 < 4.0.0 -`SocketWorkerMonitor` consumes: - -```text -ESPressio Sockets >= 0.5.0 < 1.0.0 +EventMonitor / EventConsole + - - -> ESPressio Event >= 5.8.2 < 6.0.0 + - - -> ESPressio Serializable >= 0.10.2 < 1.0.0 ``` -`SocketSecuritySessionMonitor` consumes: - -```text -ESPressio Sockets >= 0.5.0 < 1.0.0 -ESPressio Security >= 0.2.0 < 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. -Sockets supplies the Observable socket worker and secure-session lifecycle contracts. Security is only relevant to the secure-session integration. - -### ESPNowTransportMonitor - -`ESPNowTransportMonitor` consumes: +## Current coordinated ecosystem ```text -ESPressio ESP-Now >= 0.5.0 < 1.0.0 +FOUNDATIONAL +├── Observable 3.0.1 +├── Serializable 0.10.2 +├── Units 0.2.3 +├── Security 0.2.0 +└── Command 0.3.0 + +RUNTIME +└── Timing 2.2.4 + ├── Units >= 0.2.3 < 1.0.0 + └── Observable >= 3.0.1 < 4.0.0 + +EXECUTION +└── Threads 3.1.4 + ├── Timing >= 2.2.4 < 3.0.0 + └── Observable >= 3.0.1 < 4.0.0 + +TRANSPORT / INTEGRATION +├── Sockets 0.5.0 +└── ESP-Now 0.5.2 + +EVENT +└── Event 5.8.2 + ├── Threads >= 3.1.4 < 4.0.0 + ├── Timing >= 2.2.4 < 3.0.0 + ├── Observable >= 3.0.1 < 4.0.0 + └── Serializable >= 0.10.2 < 1.0.0 [optional] + +DIAGNOSTICS / OPERATOR +└── Serial 0.5.1 ``` -ESP-Now supplies the Observable transport, peer, and send lifecycle contract. +## Dependency-direction rule -### EventMonitor and EventConsole +Serial is deliberately a terminal/downstream integration layer. It may observe +or operate against Command, Security, Sockets, ESP-Now, Timing, Threads, Event, +and Serializable, but none of those libraries should acquire a Serial +dependency. -`EventMonitor` consumes: +The wider ecosystem should follow the same rule: dependency edges cascade +downstream and integration code belongs with the component that introduces the +additional dependency. -```text -ESPressio Event >= 5.8.0 < 6.0.0 -ESPressio Serializable >= 0.10.0 < 1.0.0 -``` +### Known circular optional relationships -Event supplies the Event Transport Transaction Observation stream. Serializable supplies structured payload decoding used for human-readable diagnostic output. - -The legacy EventConsole initialization path remains supported for compatibility. The recommended Command-backed EventConsole integration consumes: +Two existing Event bridge placements violate that preferred direction: ```text -ESPressio Command >= 0.3.0 < 1.0.0 -ESPressio Event >= 5.8.0 < 6.0.0 -ESPressio Serializable >= 0.10.0 < 1.0.0 -``` - -Command supplies the shared `event`/`events` command tree and scoped registration lifetime. Event supplies runtime Serializable Event discovery, descriptors, construction and dispatch. Serializable supplies `JsonArchive` and validation diagnostics. +Sockets - - -> Event + concrete socket Event transports -The external ArduinoJson dependency is required only by the optional Serializable `JsonArchive`; it is outside this ESPressio-to-ESPressio dependency chart. - -### Timing and Threads monitors - -`SystemClockMonitor` optionally consumes: - -```text -ESPressio Timing >= 2.2.2 < 3.0.0 +Event - - -> Sockets + SocketWorkerEventBridge + SocketSecuritySessionEventBridge ``` -`ThreadMonitor` optionally consumes: +and: ```text -ESPressio Threads >= 3.1.2 < 4.0.0 -``` +ESP-Now - - -> Event + ESPNowEventTransport -### Event bridges versus Serial monitors +Event - - -> ESP-Now + ESPNowTransportEventBridge +``` -The 0.5.0 Observable monitors subscribe directly to the originating subsystem. They do not require ESPressio Event. +The optimal resolution is to keep Event transport-neutral and relocate the +transport-specific Observer-to-Event bridges downstream into the corresponding +Sockets/ESP-Now Event integration, or into dedicated integration packages. -ESPressio Event 5.8.0 separately supplies optional Event bridges for Command, Security, Sockets, and ESP-Now when asynchronous Event conversion is desired. Serial diagnostics therefore remain usable without introducing Event as an intermediary. +Generic Event bridges for upstream libraries that do not themselves consume +Event—such as Timing, Threads, Command, and Security—do not create this cycle. -## Current ecosystem relationships +## Why ESP-Now is not pinned to Event 5.8.2 -- Observable 3.0.1 has no mandatory ESPressio dependencies. -- Serializable 0.10.0 has no mandatory ESPressio dependencies. -- Units 0.2.1 optionally consumes Serializable for Serializable Unit counterparts. -- Timing 2.2.2 requires Units and Observable. -- Threads 3.1.2 requires Timing and Observable. -- Security 0.2.0 requires Observable; Event conversion is opt-in downstream through Event 5.8.0. -- Command 0.3.0 requires Observable; Event conversion is opt-in downstream through Event 5.8.0. -- Sockets 0.5.0 consumes Observable for lifecycle observation and optionally integrates Command and Security. -- ESP-Now 0.5.0 requires Timing and Observable and optionally integrates Command, Security, and Event transport functionality. -- Event 5.8.0 requires Threads, Timing, and Observable and optionally bridges Security, Command, Sockets, and ESP-Now observer contracts. -- Serial 0.5.0 has no mandatory ESPressio dependencies; Command, Security, Sockets, ESP-Now, Event, Serializable, Timing, and Threads integrations are all opt-in. +ESP-Now 0.5.2's **required** dependency refresh is Timing 2.2.4. Its Event +transport is optional and can consume a compatible Event 5.x release. Requiring +ESP-Now 0.5.2 to consume Event 5.8.2 while Event also contains an ESP-Now bridge +would strengthen the reciprocal edge and produce unnecessary release churn. -Applications using only the core ESPressio Serial layer acquire none of these optional ESPressio dependencies. +Serial is different: Serial sits downstream of both and therefore validates its +Event integration against Event 5.8.2 and its ESP-Now monitor against ESP-Now +0.5.2. diff --git a/README.md b/README.md index a2b9561..07a84ab 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ Serial and console-oriented components for the Flowduino ESPressio Development Platform. -Version 0.5.0 expands ESPressio Serial diagnostics with opt-in Observable-backed monitors for ESPressio Command, Security, Sockets, and ESP-Now while preserving the dependency-free Serial core and existing Console/Event integrations. +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. -## Current Version — 0.5.0 +## Current Version — 0.5.1 -Version **0.5.0** extends Serial's diagnostics layer to consume the Observable lifecycle contracts provided by ESPressio Command, Security, Sockets, and ESP-Now. +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. -Core ESPressio Serial remains free of mandatory ESPressio-library dependencies. The new monitors are selected only when their corresponding upstream headers are available: +The Observable-backed monitor integrations introduced in 0.5.0 remain available unchanged: ```text CommandMonitor @@ -25,15 +25,17 @@ SocketSecuritySessionMonitor - - -> ESPressio Security >= 0.2.0 < 1.0.0 ESPNowTransportMonitor - - - -> ESPressio ESP-Now >= 0.5.0 < 1.0.0 + - - -> ESPressio ESP-Now >= 0.5.2 < 1.0.0 ``` `DiagnosticMonitor` can additionally compose `CommandMonitor` and `ESPNowTransportMonitor` when those dependencies are present. Security and Socket monitors remain instance-oriented because the application must choose the specific `TransportSecurity`, `SocketWorker`, or `SocketSecuritySession` object to observe. -These monitors subscribe directly to the originating library's Observable contract. They do not invent parallel Serial lifecycle semantics and do not require ESPressio Event. Event-backed observation remains a separate opt-in integration in ESPressio Event 5.8.0. +These monitors subscribe directly to the originating library's Observable contract. They do not invent parallel Serial lifecycle semantics and do not require ESPressio Event. Event-backed observation remains a separate opt-in integration in ESPressio Event 5.8.2. 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. + ## ESPressio Development Platform ESPressio is a collection of discrete, composable component libraries designed around a common development ethos: @@ -75,11 +77,11 @@ The **core ESPressio Serial library has no required ESPressio library dependenci The Event Monitor is deliberately opt-in and requires: ```text -ESPressio Event >= 5.8.0 < 6.0.0 -ESPressio Serializable >= 0.10.0 < 1.0.0 +ESPressio Event >= 5.8.2 < 6.0.0 +ESPressio Serializable >= 0.10.2 < 1.0.0 ``` -The additional opt-in monitoring dependencies for 0.5.0 are listed above. Historical sections below retain older release-specific baselines where those versions are part of the documented history. +The additional opt-in monitoring dependencies for the 0.5.x line are listed above. Historical sections below retain older release-specific baselines where those versions are part of the documented history. For the complete ecosystem hierarchy, see: @@ -672,7 +674,7 @@ ThreadMonitor EventMonitor - - -> ESPressio Event >= 5.8.0 < 6.0.0 - - - -> ESPressio Serializable >= 0.10.0 < 1.0.0 + - - -> ESPressio Serializable >= 0.10.1 < 1.0.0 ``` All ESPressio relationships remain opt-in. The 0.5.0 observer monitors add the additional optional relationships documented near the top of this README. @@ -876,16 +878,13 @@ config.MaximumHexPayloadBytes `Structured` is the default. -ESPressio Event Transport serializes Event payloads using ESPressio Serializable's `BinaryArchive`. - -The monitor decodes that Binary Archive into Serializable's generic `SerializationNode` tree and renders it directly as JSON-like structured text. +ESPressio Event Transport serializes Event payloads using ESPressio Serializable's BinaryArchive ESPB v2 representation. -This has two important advantages: +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. -1. the monitor does not need to know the concrete C++ Event type; -2. it does not require ArduinoJson merely to present human-readable diagnostics. +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. -The monitor is therefore able to inspect arbitrary transported Serializable Event payloads using the schema already encoded in the Binary Archive. +If the payload is malformed or exceeds the configured diagnostic limits, EventMonitor prints a bounded hexadecimal fallback rather than attempting structured tree construction. --- @@ -898,12 +897,13 @@ Configuration includes: ```cpp MaximumCollectionItems MaximumStringLength +MaximumStructuredNodes MaximumStructuredDepth IndentSpaces PrettyStructuredPayload ``` -These provide deterministic limits when monitoring large or deeply nested Event payloads. +These limits are applied while validating/traversing ESPB bytes before structured output is emitted. `MaximumStructuredNodes` bounds aggregate payload-tree breadth as well as the existing collection/string/depth controls. --- @@ -932,11 +932,11 @@ Inbound and outbound monitoring can also be enabled independently. # Borrowed Event Transport data -ESPressio Event 5.5 transaction snapshots expose borrowed Event/payload references valid only during the Observer callback. +ESPressio Event transaction snapshots expose borrowed Event/payload references valid only during the Observer callback. `EventMonitor` consumes those values synchronously and does not retain borrowed transaction pointers after the callback returns. -Structured decoding is therefore performed while the payload is valid. +Structured traversal is therefore performed while the payload is valid, without copying it into a second tree. --- @@ -971,7 +971,7 @@ examples/ The example uses a small local `LoopbackEventTransport` so both outbound and inbound transactions can be demonstrated on a single ESP32 without networking or additional hardware. -It defines a Serializable counter Event, transports it through Event 5.5, and renders the Binary payload as structured text. +It defines a Serializable counter Event, transports it through Event, and renders the Binary payload as structured text. --- @@ -981,16 +981,16 @@ A project using only the core Serial library: ```ini lib_deps = - flowduino/ESPressio-Serial@^0.5.0 + flowduino/ESPressio-Serial@^0.5.1 ``` An application using Event Monitor requires: ```ini lib_deps = - flowduino/ESPressio-Serial@^0.5.0 - flowduino/ESPressio-Event@^5.8.0 - flowduino/ESPressio-Serializable@^0.10.0 + flowduino/ESPressio-Serial@^0.5.1 + flowduino/ESPressio-Event@^5.8.1 + flowduino/ESPressio-Serializable@^0.10.1 ``` 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 +1004,16 @@ The generic console requires only ESPressio Serial: ```ini lib_deps = - flowduino/ESPressio-Serial@^0.5.0 + flowduino/ESPressio-Serial@^0.5.1 ``` The Event Console additionally requires the runtime Event and JSON stacks: ```ini lib_deps = - flowduino/ESPressio-Serial@^0.5.0 - flowduino/ESPressio-Event@^5.8.0 - flowduino/ESPressio-Serializable@^0.10.0 + flowduino/ESPressio-Serial@^0.5.1 + flowduino/ESPressio-Event@^5.8.1 + flowduino/ESPressio-Serializable@^0.10.1 bblanchon/ArduinoJson ``` @@ -1045,7 +1045,7 @@ Hardware-radio implementations belong in the planned **ESPressio Radio** library # Summary -ESPressio Serial 0.3.0 provides three complementary layers: +ESPressio Serial provides three complementary layers: ```text CORE @@ -1060,6 +1060,7 @@ DIAGNOSTICS / LOGGING SystemClockMonitor [opt-in Timing] ThreadMonitor [opt-in Threads] EventMonitor [opt-in Event + Serializable] + Command/Security/Sockets/ESP-Now monitors [opt-in] DiagnosticMonitor OPERATOR CONSOLE @@ -1068,7 +1069,7 @@ OPERATOR CONSOLE Print output extensible commands - EventConsole [opt-in Event 5.6 + Serializable JSON] + EventConsole [opt-in Event + Serializable JSON] runtime Event discovery schema description JSON composition diff --git a/library.json b/library.json index bea5822..67f6bb2 100644 --- a/library.json +++ b/library.json @@ -16,7 +16,7 @@ "type": "git", "url": "https://github.com/Flowduino/ESPressio-Serial.git" }, - "version": "0.5.0", + "version": "0.5.1", "license": "Apache-2.0", "frameworks": "arduino", "platforms": "espressif32" diff --git a/library.properties b/library.properties index c1af497..6c52610 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ESPressio-Serial -version=0.5.0 +version=0.5.1 author=Flowduino maintainer=Flowduino sentence=Serial console, diagnostics, logging and operator tooling for the ESPressio ecosystem. diff --git a/src/ESPressio_SerialTypes.hpp b/src/ESPressio_SerialTypes.hpp index ed06b91..d44e549 100644 --- a/src/ESPressio_SerialTypes.hpp +++ b/src/ESPressio_SerialTypes.hpp @@ -43,6 +43,7 @@ struct EventMonitorConfig { std::size_t MaximumHexPayloadBytes = 256; std::size_t MaximumCollectionItems = 64; std::size_t MaximumStringLength = 512; + std::size_t MaximumStructuredNodes = 1024; uint8_t MaximumStructuredDepth = 12; uint8_t IndentSpaces = 2; }; diff --git a/src/event/ESPressio_EventMonitor.hpp b/src/event/ESPressio_EventMonitor.hpp index 67c8545..caf84b9 100644 --- a/src/event/ESPressio_EventMonitor.hpp +++ b/src/event/ESPressio_EventMonitor.hpp @@ -4,8 +4,8 @@ #error "ESPressio EventMonitor requires ESPressio Event >= 5.6.2 < 6.0.0." #endif -#if !__has_include() -#error "ESPressio EventMonitor requires ESPressio Serializable >= 0.9.0 < 1.0.0." +#if !__has_include() +#error "ESPressio EventMonitor requires ESPressio Serializable >= 0.10.1 < 1.0.0." #endif #include @@ -15,6 +15,7 @@ #include "../ESPressio_SerialTypes.hpp" #include "ESPressio_EventMonitorFormatter.hpp" +#include "ESPressio_EventMonitorPayloadSafety.hpp" namespace ESPressio::Serial { @@ -163,12 +164,59 @@ class EventMonitor final : return; } + if ( + _config.PayloadFormat != + EventMonitorPayloadFormat::Structured + ) { + EventMonitorFormatter:: + PrintTransaction( + *_output, + transaction, + _config + ); + return; + } + + // Print the transaction metadata through the established formatter, but + // suppress its legacy tree-building structured payload path. The + // payload itself is then traversed directly from ESPB bytes without + // constructing a second SerializationNode tree. + EventMonitorConfig metadataConfig = + _config; + metadataConfig.PayloadFormat = + EventMonitorPayloadFormat::None; + EventMonitorFormatter:: PrintTransaction( *_output, transaction, - _config + metadataConfig ); + + _output->print(" payload: "); + + if ( + PrintStructuredEventPayload( + *_output, + transaction.Payload, + transaction.PayloadSize, + _config + ) + ) { + _output->println(); + return; + } + + _output->print( + " " + ); + PrintEventPayloadHexFallback( + *_output, + transaction.Payload, + transaction.PayloadSize, + _config + ); + _output->println(); } }; diff --git a/src/event/ESPressio_EventMonitorPayloadSafety.hpp b/src/event/ESPressio_EventMonitorPayloadSafety.hpp new file mode 100644 index 0000000..f42e4f0 --- /dev/null +++ b/src/event/ESPressio_EventMonitorPayloadSafety.hpp @@ -0,0 +1,448 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../ESPressio_SerialTypes.hpp" + +namespace ESPressio::Serial { + +inline Serializable::BinaryArchiveDecodeLimits +BuildEventMonitorDecodeLimits( + const EventMonitorConfig& config +) noexcept { + Serializable::BinaryArchiveDecodeLimits limits; + + limits.MaximumDepth = + config.MaximumStructuredDepth; + + limits.MaximumTotalNodes = + std::max( + config.MaximumStructuredNodes, + 1 + ); + + const auto maximumCollectionItems = + std::max( + config.MaximumCollectionItems, + 1 + ); + + limits.MaximumObjectMembers = + static_cast( + std::min( + maximumCollectionItems, + UINT32_MAX + ) + ); + + limits.MaximumArrayElements = + static_cast( + std::min( + maximumCollectionItems, + UINT32_MAX + ) + ); + + limits.MaximumNameLength = + std::max( + config.MaximumStringLength, + 1 + ); + + limits.MaximumStringLength = + std::max( + config.MaximumStringLength, + 1 + ); + + return limits; +} + + +inline bool ValidateStructuredEventPayload( + const uint8_t* payload, + std::size_t size, + const EventMonitorConfig& config +) noexcept { + return Serializable::ValidateBinaryArchive( + payload, + size, + BuildEventMonitorDecodeLimits(config) + ); +} + + +namespace EventMonitorPayloadSafetyDetail { + +inline void PrintIndent( + Print& output, + std::size_t depth, + uint8_t spaces +) noexcept { + const std::size_t count = + depth * static_cast(spaces); + + for (std::size_t index = 0; index < count; ++index) { + output.write(static_cast(' ')); + } +} + + +inline void PrintEscapedString( + Print& output, + std::string_view value, + std::size_t maximumLength +) noexcept { + output.write(static_cast('"')); + + const std::size_t length = + std::min(value.size(), maximumLength); + + for (std::size_t index = 0; index < length; ++index) { + const unsigned char character = + static_cast(value[index]); + + switch (character) { + case '"': output.print("\\\""); break; + case '\\': output.print("\\\\"); break; + case '\b': output.print("\\b"); break; + case '\f': output.print("\\f"); break; + case '\n': output.print("\\n"); break; + case '\r': output.print("\\r"); break; + case '\t': output.print("\\t"); break; + + default: + if (character < 0x20u) { + char escaped[7]; + std::snprintf( + escaped, + sizeof(escaped), + "\\u%04X", + static_cast(character) + ); + output.print(escaped); + } else { + output.write(static_cast(character)); + } + break; + } + } + + if (value.size() > maximumLength) { + output.print("..."); + } + + output.write(static_cast('"')); +} + + +class StructuredPayloadPrinter final : + public Serializable::BinaryArchiveVisitor { +private: + Print& _output; + const EventMonitorConfig& _config; + + void BeforeCollectionItem( + uint32_t index, + std::size_t depth + ) noexcept { + if (index > 0) { + _output.write(static_cast(',')); + } + + if (_config.PrettyStructuredPayload) { + _output.println(); + PrintIndent( + _output, + depth + 1, + _config.IndentSpaces + ); + } + } + + void CloseCollection( + uint32_t count, + std::size_t depth, + char close + ) noexcept { + if ( + _config.PrettyStructuredPayload && + count > 0 + ) { + _output.println(); + PrintIndent( + _output, + depth, + _config.IndentSpaces + ); + } + + _output.write(static_cast(close)); + } + +public: + StructuredPayloadPrinter( + Print& output, + const EventMonitorConfig& config + ) noexcept : + _output(output), + _config(config) { + } + + bool OnObjectBegin( + uint32_t, + std::size_t + ) noexcept override { + _output.write(static_cast('{')); + return true; + } + + bool OnObjectProperty( + std::string_view name, + uint32_t index, + uint32_t, + std::size_t depth + ) noexcept override { + BeforeCollectionItem(index, depth); + PrintEscapedString( + _output, + name, + _config.MaximumStringLength + ); + _output.print( + _config.PrettyStructuredPayload + ? ": " + : ":" + ); + return true; + } + + bool OnObjectEnd( + uint32_t count, + std::size_t depth + ) noexcept override { + CloseCollection(count, depth, '}'); + return true; + } + + bool OnArrayBegin( + uint32_t, + std::size_t + ) noexcept override { + _output.write(static_cast('[')); + return true; + } + + bool OnArrayElement( + uint32_t index, + uint32_t, + std::size_t depth + ) noexcept override { + BeforeCollectionItem(index, depth); + return true; + } + + bool OnArrayEnd( + uint32_t count, + std::size_t depth + ) noexcept override { + CloseCollection(count, depth, ']'); + return true; + } + + bool OnNull( + std::size_t + ) noexcept override { + _output.print("null"); + return true; + } + + bool OnBoolean( + bool value, + std::size_t + ) noexcept override { + _output.print(value ? "true" : "false"); + return true; + } + + bool OnSignedInteger( + int64_t value, + std::size_t + ) noexcept override { + char buffer[32]; + std::snprintf( + buffer, + sizeof(buffer), + "%" PRId64, + value + ); + _output.print(buffer); + return true; + } + + bool OnUnsignedInteger( + uint64_t value, + std::size_t + ) noexcept override { + char buffer[32]; + std::snprintf( + buffer, + sizeof(buffer), + "%" PRIu64, + value + ); + _output.print(buffer); + return true; + } + + bool OnFloat32( + float value, + std::size_t + ) noexcept override { + char buffer[32]; + std::snprintf( + buffer, + sizeof(buffer), + "%.7g", + static_cast(value) + ); + _output.print(buffer); + return true; + } + + bool OnFloat64( + double value, + std::size_t + ) noexcept override { + char buffer[48]; + std::snprintf( + buffer, + sizeof(buffer), + "%.15g", + value + ); + _output.print(buffer); + return true; + } + + bool OnString( + std::string_view value, + std::size_t + ) noexcept override { + PrintEscapedString( + _output, + value, + _config.MaximumStringLength + ); + return true; + } +}; + + +inline void PrintHexPayload( + Print& output, + const uint8_t* payload, + std::size_t size, + std::size_t maximumBytes +) noexcept { + if ( + payload == nullptr || + size == 0 + ) { + output.print(""); + return; + } + + const std::size_t count = + std::min(size, maximumBytes); + + for (std::size_t index = 0; index < count; ++index) { + if (index > 0) { + output.write(static_cast(' ')); + } + + char byte[3]; + std::snprintf( + byte, + sizeof(byte), + "%02X", + static_cast(payload[index]) + ); + output.print(byte); + } + + if (count < size) { + output.print(" ..."); + } +} + +} // namespace EventMonitorPayloadSafetyDetail + + +inline bool PrintStructuredEventPayload( + Print& output, + const uint8_t* payload, + std::size_t size, + const EventMonitorConfig& config +) noexcept { + if ( + payload == nullptr || + size == 0 + ) { + output.print(""); + return true; + } + + const auto limits = + BuildEventMonitorDecodeLimits(config); + + // Validate first so malformed input never leaves a partially rendered + // structured diagnostic line. Both passes are allocation-free. + if ( + !Serializable::ValidateBinaryArchive( + payload, + size, + limits + ) + ) { + return false; + } + + EventMonitorPayloadSafetyDetail:: + StructuredPayloadPrinter visitor( + output, + config + ); + + return Serializable::TraverseBinaryArchive( + payload, + size, + visitor, + limits + ); +} + + +inline void PrintEventPayloadHexFallback( + Print& output, + const uint8_t* payload, + std::size_t size, + const EventMonitorConfig& config +) noexcept { + EventMonitorPayloadSafetyDetail::PrintHexPayload( + output, + payload, + size, + config.MaximumHexPayloadBytes + ); +} + +} // namespace ESPressio::Serial diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 29beb05..10eddfc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,7 @@ enable_testing() set(ESPRESSIO_COMMAND_INCLUDE_DIR "" CACHE PATH "Path containing ESPressio_Command.hpp") set(ESPRESSIO_OBSERVABLE_INCLUDE_DIR "" CACHE PATH "Path containing ESPressio_Observable.hpp") +set(ESPRESSIO_SERIALIZABLE_INCLUDE_DIR "" CACHE PATH "Path containing ESPressio_BinaryArchive.hpp") add_executable( test_console @@ -35,3 +36,19 @@ target_compile_features(test_command_console PRIVATE cxx_std_17) target_compile_options(test_command_console PRIVATE -Wall -Wextra -Wpedantic -Werror) target_include_directories(test_command_console PRIVATE stubs ../src ${ESPRESSIO_COMMAND_INCLUDE_DIR} ${ESPRESSIO_OBSERVABLE_INCLUDE_DIR}) add_test(NAME CommandConsoleContract COMMAND test_command_console) + +add_executable( + test_event_monitor_payload_safety + test_event_monitor_payload_safety.cpp +) + +target_compile_features(test_event_monitor_payload_safety PRIVATE cxx_std_17) +target_compile_options(test_event_monitor_payload_safety PRIVATE -Wall -Wextra -Wpedantic -Werror) +target_include_directories( + test_event_monitor_payload_safety + PRIVATE + stubs + ../src + ${ESPRESSIO_SERIALIZABLE_INCLUDE_DIR} +) +add_test(NAME EventMonitorPayloadSafety COMMAND test_event_monitor_payload_safety) diff --git a/tests/test_event_monitor_payload_safety.cpp b/tests/test_event_monitor_payload_safety.cpp new file mode 100644 index 0000000..8df9389 --- /dev/null +++ b/tests/test_event_monitor_payload_safety.cpp @@ -0,0 +1,114 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace ESPressio; + +class BufferPrint final : public Print { +public: + std::string Data; + + std::size_t write(uint8_t value) override { + Data.push_back(static_cast(value)); + return 1; + } +}; + +static void AppendU16(std::vector& data, uint16_t value) { + data.push_back(static_cast(value & 0xffu)); + data.push_back(static_cast((value >> 8u) & 0xffu)); +} + +static std::vector DeepPayload(unsigned depth) { + std::vector data = {'E', 'S', 'P', 'B', 2u}; + for (unsigned level = 0; level < depth; ++level) { + data.push_back(static_cast( + Serializable::SerializationNodeType::Object + )); + AppendU16(data, 1); + AppendU16(data, 1); + data.push_back('x'); + } + data.push_back(static_cast( + Serializable::SerializationNodeType::Null + )); + return data; +} + +int main() { + Serial::EventMonitorConfig config; + config.MaximumStructuredDepth = 8; + config.MaximumStructuredNodes = 128; + config.MaximumCollectionItems = 32; + config.MaximumStringLength = 128; + + Serializable::BinaryArchive validArchive; + validArchive.Write("value", uint32_t(42)); + const auto valid = validArchive.GetData(); + assert(Serial::ValidateStructuredEventPayload( + valid.data(), valid.size(), config + )); + + // Structured output is rendered directly from ESPB bytes and does not need + // a second SerializationNode tree. + { + BufferPrint output; + assert(Serial::PrintStructuredEventPayload( + output, + valid.data(), + valid.size(), + config + )); + assert(output.Data.find("\"value\"") != std::string::npos); + assert(output.Data.find("42") != std::string::npos); + } + + const auto deep = DeepPayload(16); + assert(!Serial::ValidateStructuredEventPayload( + deep.data(), deep.size(), config + )); + { + BufferPrint output; + assert(!Serial::PrintStructuredEventPayload( + output, + deep.data(), + deep.size(), + config + )); + // Validation runs before presentation, so rejected input never emits a + // half-rendered structured payload. + assert(output.Data.empty()); + } + + const std::vector truncated = { + 'E', 'S', 'P', 'B', 2u, + static_cast(Serializable::SerializationNodeType::Object), + 1u, 0u + }; + assert(!Serial::ValidateStructuredEventPayload( + truncated.data(), truncated.size(), config + )); + + // Stress the allocation-free diagnostic guard with deterministic arbitrary + // byte sequences. The contract is bounded rejection without constructing + // tree state or destabilizing the caller. + std::mt19937 rng(0x45564D4Fu); + std::vector bytes(512); + for (unsigned iteration = 0; iteration < 5000; ++iteration) { + const std::size_t size = 1 + (rng() % bytes.size()); + for (std::size_t index = 0; index < size; ++index) { + bytes[index] = static_cast(rng()); + } + (void)Serial::ValidateStructuredEventPayload( + bytes.data(), size, config + ); + } + + return 0; +}