From 2b2bc3200f04125e62cedd85a8e4e1a6bcd8e399 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:22:56 +0200 Subject: [PATCH 01/16] feat: add socket worker observer contract --- src/ESPressio_ISocketWorkerObserver.hpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/ESPressio_ISocketWorkerObserver.hpp diff --git a/src/ESPressio_ISocketWorkerObserver.hpp b/src/ESPressio_ISocketWorkerObserver.hpp new file mode 100644 index 0000000..963db23 --- /dev/null +++ b/src/ESPressio_ISocketWorkerObserver.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include + +namespace ESPressio::Sockets { + +class ISocketWorkerObserver : + public virtual Observable::IObserver { +public: + virtual ~ISocketWorkerObserver() = default; + + virtual void OnSocketWorkerStarted( + const char* + ) {} + + virtual void OnSocketWorkerStartFailed( + const char* + ) {} + + virtual void OnSocketWorkerStopped() {} +}; + +} // namespace ESPressio::Sockets From e65d4e011fbc7e6276e8da77daa78bb3b378d0d6 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:23:02 +0200 Subject: [PATCH 02/16] feat: add socket security session observer contract --- ...Pressio_ISocketSecuritySessionObserver.hpp | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/ESPressio_ISocketSecuritySessionObserver.hpp diff --git a/src/ESPressio_ISocketSecuritySessionObserver.hpp b/src/ESPressio_ISocketSecuritySessionObserver.hpp new file mode 100644 index 0000000..2a074ee --- /dev/null +++ b/src/ESPressio_ISocketSecuritySessionObserver.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace ESPressio::Sockets { + +class ISocketSecuritySessionObserver : + public virtual Observable::IObserver { +public: + virtual ~ISocketSecuritySessionObserver() = default; + + virtual void OnSocketSecuritySessionFaulted( + const Security::SecurityResult& + ) {} + + virtual void OnSocketSecuritySessionReset() {} +}; + +} // namespace ESPressio::Sockets From 90d190a39aa3b0964e84e068ccb6d7e9f9e81996 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:23:16 +0200 Subject: [PATCH 03/16] feat: make socket worker lifecycle observable --- src/ESPressio_SocketWorker.hpp | 120 +++++++++++++++------------------ 1 file changed, 55 insertions(+), 65 deletions(-) diff --git a/src/ESPressio_SocketWorker.hpp b/src/ESPressio_SocketWorker.hpp index 074834c..d5ea5f3 100644 --- a/src/ESPressio_SocketWorker.hpp +++ b/src/ESPressio_SocketWorker.hpp @@ -1,120 +1,110 @@ #pragma once #include +#include +#include + +#include "ESPressio_ISocketWorkerObserver.hpp" #include "ESPressio_SocketTypes.hpp" namespace ESPressio::Sockets { class SocketWorker { private: + class WorkerObservable final : public Observable::ThreadSafeObservable { + private: + template + void Notify(Callback&& callback) { + ExecuteNotification([&](NotificationContext& notification) { + notification.WithObservers([&](ISocketWorkerObserver* observer) { + try { callback(observer); } catch (...) {} + }); + }); + } + public: + void Started(const char* name) { Notify([&](ISocketWorkerObserver* observer){ observer->OnSocketWorkerStarted(name); }); } + void StartFailed(const char* name) { Notify([&](ISocketWorkerObserver* observer){ observer->OnSocketWorkerStartFailed(name); }); } + void Stopped() { Notify([](ISocketWorkerObserver* observer){ observer->OnSocketWorkerStopped(); }); } + }; + TaskHandle_t _taskHandle = nullptr; std::atomic _running{false}; SocketWorkerConfig _config; + std::shared_ptr _observable = std::make_shared(); - static void TaskEntry( - void* parameter - ) { - auto* worker = - static_cast( - parameter - ); - - if (worker != nullptr) { - worker->Run(); - } - + static void TaskEntry(void* parameter) { + auto* worker = static_cast(parameter); + if (worker != nullptr) worker->Run(); vTaskDelete(nullptr); } void Run() { while (_running.load()) { OnWorkerIteration(); - - if ( - _config.IdleDelayMilliseconds > - 0 - ) { - vTaskDelay( - pdMS_TO_TICKS( - _config. - IdleDelayMilliseconds - ) - ); + if (_config.IdleDelayMilliseconds > 0) { + vTaskDelay(pdMS_TO_TICKS(_config.IdleDelayMilliseconds)); } else { taskYIELD(); } } - _taskHandle = nullptr; } protected: virtual void OnWorkerIteration() = 0; - bool StartWorker( - const char* name, - const SocketWorkerConfig& config - ) { - if (_running.load()) { - return true; - } + bool StartWorker(const char* name, const SocketWorkerConfig& config) { + if (_running.load()) return true; _config = config; _running.store(true); - const BaseType_t result = - xTaskCreatePinnedToCore( - TaskEntry, - name, - config.StackSize, - this, - config.Priority, - &_taskHandle, - config.Core - ); + const BaseType_t result = xTaskCreatePinnedToCore( + TaskEntry, + name, + config.StackSize, + this, + config.Priority, + &_taskHandle, + config.Core + ); if (result != pdPASS) { _running.store(false); _taskHandle = nullptr; + _observable->StartFailed(name); return false; } + _observable->Started(name); return true; } void StopWorker() { - _running.store(false); - - if ( - _taskHandle == nullptr || - xTaskGetCurrentTaskHandle() == - _taskHandle - ) { - return; - } + const bool wasRunning = _running.exchange(false); - /* - * Worker loops are deliberately non-blocking or use short timeouts. - * Wait for natural exit before derived classes destroy their socket - * resources. - */ - while (_taskHandle != nullptr) { - vTaskDelay( - pdMS_TO_TICKS(1) - ); + if (_taskHandle != nullptr && xTaskGetCurrentTaskHandle() != _taskHandle) { + while (_taskHandle != nullptr) { + vTaskDelay(pdMS_TO_TICKS(1)); + } } + + if (wasRunning) _observable->Stopped(); } public: - virtual ~SocketWorker() { - StopWorker(); + virtual ~SocketWorker() { StopWorker(); } + + Observable::ObserverHandlePtr RegisterObserver(ISocketWorkerObserver* observer) { + return _observable->RegisterObserver(observer); } - bool GetWorkerIsRunning() const - noexcept { - return _running.load(); + void UnregisterObserver(ISocketWorkerObserver* observer) { + _observable->UnregisterObserver(observer); } + + bool GetWorkerIsRunning() const noexcept { return _running.load(); } }; -} +} // namespace ESPressio::Sockets From 326b644882844459aa40de3fa992b6dab14751b6 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:23:33 +0200 Subject: [PATCH 04/16] feat: make socket security session observable --- src/ESPressio_SocketSecuritySession.hpp | 112 ++++++++++++++++++------ 1 file changed, 84 insertions(+), 28 deletions(-) diff --git a/src/ESPressio_SocketSecuritySession.hpp b/src/ESPressio_SocketSecuritySession.hpp index aae97b4..168d41a 100644 --- a/src/ESPressio_SocketSecuritySession.hpp +++ b/src/ESPressio_SocketSecuritySession.hpp @@ -3,11 +3,15 @@ #include #include #include +#include #include #include +#include #include +#include "ESPressio_ISocketSecuritySessionObserver.hpp" + namespace ESPressio::Sockets { struct SocketSecuritySessionConfig { @@ -20,18 +24,88 @@ class SocketSecuritySession final { using ReceiveCallback = std::function; using FailureCallback = std::function; +private: + class SessionObservable final : public Observable::ThreadSafeObservable { + private: + template + void Notify(Callback&& callback) { + ExecuteNotification([&](NotificationContext& notification) { + notification.WithObservers([&](ISocketSecuritySessionObserver* observer) { + try { callback(observer); } catch (...) {} + }); + }); + } + public: + void Faulted(const Security::SecurityResult& result) { + Notify([&](ISocketSecuritySessionObserver* observer){ observer->OnSocketSecuritySessionFaulted(result); }); + } + void Reset() { + Notify([](ISocketSecuritySessionObserver* observer){ observer->OnSocketSecuritySessionReset(); }); + } + }; + + Security::TransportSecurity& _security; + WriteCallback _writer; + SocketSecuritySessionConfig _config; + ReceiveCallback _receive; + FailureCallback _failure; + std::vector _buffer; + bool _discarding = false; + std::shared_ptr _observable = std::make_shared(); + + void PublishFailure(const Security::SecurityResult& failure) { + if (_failure) _failure(failure); + _observable->Faulted(failure); + } + + void ProcessEnvelope(uint8_t protocol, const uint8_t* envelope, std::size_t size) { + Security::UnprotectedPayload opened; + auto result = _security.Unprotect(protocol, envelope, size, opened); + if (!result.Success) { + PublishFailure(result); + return; + } + if (_receive) _receive(opened); + } + + static void Append32(std::vector& out, uint32_t value) { + for (int i=0;i<4;++i) out.push_back(static_cast(value >> (i*8))); + } + static uint32_t Read32(const uint8_t* p) { + return static_cast(p[0]) | (static_cast(p[1])<<8) | + (static_cast(p[2])<<16) | (static_cast(p[3])<<24); + } + +public: SocketSecuritySession(Security::TransportSecurity& security, WriteCallback writer, SocketSecuritySessionConfig config = {}) : _security(security), _writer(std::move(writer)), _config(config) {} void SetReceiveCallback(ReceiveCallback callback) { _receive = std::move(callback); } void SetFailureCallback(FailureCallback callback) { _failure = std::move(callback); } + Observable::ObserverHandlePtr RegisterObserver(ISocketSecuritySessionObserver* observer) { + return _observable->RegisterObserver(observer); + } + + void UnregisterObserver(ISocketSecuritySessionObserver* observer) { + _observable->UnregisterObserver(observer); + } + bool Send(uint8_t protocol, const void* payload, std::size_t payloadLength, Security::SecurityResult* resultOut = nullptr) { if (!_writer || (payload == nullptr && payloadLength != 0)) return false; std::vector protectedBytes; auto result = _security.Protect(protocol, static_cast(payload), payloadLength, protectedBytes); if (resultOut) *resultOut = result; - if (!result.Success || protectedBytes.empty() || protectedBytes.size() > _config.MaximumProtectedFrameBytes || protectedBytes.size() > 0xFFFFFFFFu) return false; + if (!result.Success) { + PublishFailure(result); + return false; + } + if (protectedBytes.empty() || protectedBytes.size() > _config.MaximumProtectedFrameBytes || protectedBytes.size() > 0xFFFFFFFFu) { + auto failure = Security::SecurityResult::Fail(Security::SecurityError::BufferLimitExceeded, "Protected secure socket frame is empty or exceeds configured limit"); + if (resultOut) *resultOut = failure; + PublishFailure(failure); + return false; + } std::vector frame; frame.reserve(5 + protectedBytes.size()); Append32(frame, static_cast(protectedBytes.size())); @@ -47,9 +121,10 @@ class SocketSecuritySession final { if (_buffer.size() < 5) return true; const uint32_t length = Read32(_buffer.data()); if (length == 0 || length > _config.MaximumProtectedFrameBytes) { - _buffer.clear(); _discarding = true; + _buffer.clear(); + _discarding = true; auto failure = Security::SecurityResult::Fail(Security::SecurityError::BufferLimitExceeded, "Secure socket frame length is invalid or exceeds configured limit"); - if (_failure) _failure(failure); + PublishFailure(failure); return false; } if (_buffer.size() < 5 + static_cast(length)) return true; @@ -59,32 +134,13 @@ class SocketSecuritySession final { } } - void Reset() { _buffer.clear(); _discarding = false; } - std::size_t BufferedBytes() const noexcept { return _buffer.size(); } - -private: - Security::TransportSecurity& _security; - WriteCallback _writer; - SocketSecuritySessionConfig _config; - ReceiveCallback _receive; - FailureCallback _failure; - std::vector _buffer; - bool _discarding = false; - - void ProcessEnvelope(uint8_t protocol, const uint8_t* envelope, std::size_t size) { - Security::UnprotectedPayload opened; - auto result = _security.Unprotect(protocol, envelope, size, opened); - if (!result.Success) { if (_failure) _failure(result); return; } - if (_receive) _receive(opened); + void Reset() { + _buffer.clear(); + _discarding = false; + _observable->Reset(); } - static void Append32(std::vector& out, uint32_t value) { - for (int i=0;i<4;++i) out.push_back(static_cast(value >> (i*8))); - } - static uint32_t Read32(const uint8_t* p) { - return static_cast(p[0]) | (static_cast(p[1])<<8) | - (static_cast(p[2])<<16) | (static_cast(p[3])<<24); - } + std::size_t BufferedBytes() const noexcept { return _buffer.size(); } }; -} +} // namespace ESPressio::Sockets From 00a4db2c8877cea21f164256d194a0cac3957c9e Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:23:53 +0200 Subject: [PATCH 05/16] chore: bump Sockets to 0.5.0 --- library.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/library.json b/library.json index 427a219..9faa89c 100644 --- a/library.json +++ b/library.json @@ -1,7 +1,7 @@ { "name": "ESPressio-Sockets", "description": "Socket-based ESPressio transports, Command invocation adapters, Security sessions, and Timing synchronization providers for ESP32.", - "keywords": "esp32,sockets,udp,tcp,tls,websocket,mqtt,event,command,security,encryption,transport,network,timing,clock,synchronization,espressio", + "keywords": "esp32,sockets,udp,tcp,tls,websocket,mqtt,event,command,security,encryption,transport,network,timing,clock,synchronization,observable,espressio", "authors": { "name": "Flowduino", "maintainer": true, @@ -16,11 +16,16 @@ "type": "git", "url": "https://github.com/Flowduino/ESPressio-Sockets.git" }, - "version": "0.4.0", + "version": "0.5.0", "license": "Apache-2.0", "frameworks": "arduino", "platforms": "espressif32", "dependencies": [ + { + "name": "Flowduino ESPressio-Observable", + "version": ">=3.0.1 <4.0.0", + "url": "https://github.com/Flowduino/ESPressio-Observable.git" + }, { "owner": "links2004", "name": "WebSockets", From fae310f6ee622961c2e1995c03cb088939713363 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:24:01 +0200 Subject: [PATCH 06/16] chore: update Sockets Arduino metadata --- library.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library.properties b/library.properties index 3ab0034..bd4d6f7 100644 --- a/library.properties +++ b/library.properties @@ -1,11 +1,11 @@ name=ESPressio-Sockets -version=0.4.0 +version=0.5.0 author=Flowduino maintainer=Flowduino sentence=Socket-based Event Transport, Command invocation, Security sessions, and System Clock synchronization for the ESPressio ecosystem. -paragraph=Provides UDP, TCP, TLS, WebSocket and MQTT Event transports, opt-in TCP ESPressio Command invocation, opt-in ESPressio Security stream/datagram protection, and opt-in Timing synchronization. +paragraph=Provides UDP, TCP, TLS, WebSocket and MQTT Event transports, opt-in TCP ESPressio Command invocation, opt-in ESPressio Security stream/datagram protection, observable worker/session lifecycle notifications, and opt-in Timing synchronization. category=Communication url=https://github.com/Flowduino/ESPressio-Sockets architectures=esp32 includes=ESPressio_Sockets.hpp -depends=WebSockets,PubSubClient +depends=Flowduino ESPressio-Observable (>=3.0.1),WebSockets,PubSubClient From 062d18d487b968d41480e584ad7b5fb1da03c5e0 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:24:07 +0200 Subject: [PATCH 07/16] chore: update Sockets component version --- component.mk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/component.mk b/component.mk index d4fe052..51daf11 100644 --- a/component.mk +++ b/component.mk @@ -4,6 +4,6 @@ CXXFLAGS += -std=gnu++17 CPPFLAGS += \ -DESPRESSIO_SOCKETS \ -DESPRESSIO_SOCKETS_VERSION_MAJOR=0 \ - -DESPRESSIO_SOCKETS_VERSION_MINOR=4 \ + -DESPRESSIO_SOCKETS_VERSION_MINOR=5 \ -DESPRESSIO_SOCKETS_VERSION_PATCH=0 \ - -DESPRESSIO_SOCKETS_VERSION_STRING=\"0.4.0\" + -DESPRESSIO_SOCKETS_VERSION_STRING=\"0.5.0\" From 3109891785934bb57ddd71a1f406a2a8d0a5c1ba Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:24:14 +0200 Subject: [PATCH 08/16] chore: expose Sockets 0.5.0 version --- src/ESPressio_Sockets.hpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/ESPressio_Sockets.hpp b/src/ESPressio_Sockets.hpp index 2fb81ce..bb18db5 100644 --- a/src/ESPressio_Sockets.hpp +++ b/src/ESPressio_Sockets.hpp @@ -4,13 +4,13 @@ #define ESPRESSIO_SOCKETS_VERSION_MAJOR 0 #endif #ifndef ESPRESSIO_SOCKETS_VERSION_MINOR -#define ESPRESSIO_SOCKETS_VERSION_MINOR 4 +#define ESPRESSIO_SOCKETS_VERSION_MINOR 5 #endif #ifndef ESPRESSIO_SOCKETS_VERSION_PATCH #define ESPRESSIO_SOCKETS_VERSION_PATCH 0 #endif #ifndef ESPRESSIO_SOCKETS_VERSION_STRING -#define ESPRESSIO_SOCKETS_VERSION_STRING "0.4.0" +#define ESPRESSIO_SOCKETS_VERSION_STRING "0.5.0" #endif #include "ESPressio_SocketTypes.hpp" @@ -20,6 +20,12 @@ * Dependency-bearing integrations are deliberately NOT batch-included here. * Include only the facilities required by the project. * + * Observable lifecycle: + * ESPressio_SocketWorker.hpp + * ESPressio_ISocketWorkerObserver.hpp + * ESPressio_SocketSecuritySession.hpp + * ESPressio_ISocketSecuritySessionObserver.hpp + * * Event transports: * ESPressio_UDPEventTransport.hpp * ESPressio_TCPClientEventTransport.hpp @@ -38,10 +44,11 @@ * ESPressio_SocketCommandSession.hpp * ESPressio_TCPCommandServer.hpp * - * Security: + * Security (validated against ESPressio Security >=0.2.0 <1.0.0): * ESPressio_SocketSecuritySession.hpp * ESPressio_SocketSecurityDatagram.hpp * - * This keeps Event/Serializable, Timing, Command, and Security dependencies - * opt-in at the consuming-code level. + * Event, Timing, Command, and Security integrations remain opt-in at the + * consuming-code level. Observable is the common lifecycle-notification + * dependency used by socket workers and secure sessions. */ From 9bc0f421f0ab4bd14bc31dcc2a09c65c03f8c2a1 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:24:31 +0200 Subject: [PATCH 09/16] docs: add Sockets 0.5.0 changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 031adc6..293a1c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.5.0 — 2026-08-20 + +### Added +- Added `ISocketWorkerObserver` and observable socket-worker lifecycle notifications for start, start failure, and stop transitions. +- Added `ISocketSecuritySessionObserver` and observable secure-session fault/reset notifications. +- Added ESPressio Observable >= 3.0.1 < 4.0.0 as the common lifecycle-observer dependency. +- Added optional ESPressio Event bridge support through ESPressio Event 5.8.0. + +### Changed +- Updated the validated optional ESPressio Security baseline to Security >= 0.2.0 < 1.0.0. +- Security session send/unprotect/frame-limit failures now publish lifecycle observations while preserving existing result and callback behavior. +- Bumped package/component/public version metadata to 0.5.0. +- Event, Timing, Command and Security integrations remain opt-in. + ## 0.4.0 — 2026-08-20 ### Added From 6e449952a0c440ab62cb209110d5977295881fa1 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:34:34 +0200 Subject: [PATCH 10/16] ci: validate Sockets against observable dependency generation --- .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 3b5b400..9387e80 100644 --- a/.github/workflows/host-tests.yml +++ b/.github/workflows/host-tests.yml @@ -4,7 +4,7 @@ on: push: branches: - main - - feature/command-socket-integration + - feature/observable-callback-coverage pull_request: jobs: @@ -14,13 +14,20 @@ jobs: - name: Checkout Sockets uses: actions/checkout@v4 - - name: Checkout ESPressio Command 0.2.0 + - name: Checkout ESPressio Command 0.3.0 feature source uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-Command - ref: 0.2.0 + ref: 74ea89f9c8895cfaa272bae44d0eba5711a3dbc1 path: deps/ESPressio-Command + - name: Checkout ESPressio Security 0.2.0 feature source + uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Security + ref: cb5224b3f98174f8b521ff87d92a373d2e97dcf7 + path: deps/ESPressio-Security + - name: Checkout ESPressio Timing 2.2.2 uses: actions/checkout@v4 with: @@ -46,6 +53,7 @@ jobs: run: >- cmake -S tests -B build -DESPRESSIO_COMMAND_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Command/src" + -DESPRESSIO_SECURITY_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Security/src" -DESPRESSIO_TIMING_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Timing/src" -DESPRESSIO_UNITS_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Units/src" -DESPRESSIO_OBSERVABLE_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Observable/src" From 9ab24f6a7067bdd72c876c179538d62d2358ee5d Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:34:45 +0200 Subject: [PATCH 11/16] test: add Sockets observable contract coverage --- tests/CMakeLists.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b925192..7a850e0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -13,10 +13,14 @@ if(NOT ESPRESSIO_COMMAND_INCLUDE_DIR) message(FATAL_ERROR "ESPRESSIO_COMMAND_INCLUDE_DIR is required for SocketCommand tests") endif() +if(NOT ESPRESSIO_OBSERVABLE_INCLUDE_DIR) + message(FATAL_ERROR "ESPRESSIO_OBSERVABLE_INCLUDE_DIR is required for observable Sockets tests") +endif() + add_executable(test_socket_command test_socket_command.cpp) target_compile_features(test_socket_command PRIVATE cxx_std_17) target_compile_options(test_socket_command PRIVATE -Wall -Wextra -Wpedantic -Werror) -target_include_directories(test_socket_command PRIVATE ../src ${ESPRESSIO_COMMAND_INCLUDE_DIR}) +target_include_directories(test_socket_command PRIVATE ../src ${ESPRESSIO_COMMAND_INCLUDE_DIR} ${ESPRESSIO_OBSERVABLE_INCLUDE_DIR}) add_test(NAME SocketCommand COMMAND test_socket_command) if(NOT ESPRESSIO_SECURITY_INCLUDE_DIR) @@ -26,9 +30,15 @@ endif() add_executable(test_socket_security test_socket_security.cpp) target_compile_features(test_socket_security PRIVATE cxx_std_17) target_compile_options(test_socket_security PRIVATE -Wall -Wextra -Wpedantic -Werror) -target_include_directories(test_socket_security PRIVATE ../src ${ESPRESSIO_SECURITY_INCLUDE_DIR}) +target_include_directories(test_socket_security PRIVATE ../src ${ESPRESSIO_SECURITY_INCLUDE_DIR} ${ESPRESSIO_OBSERVABLE_INCLUDE_DIR}) add_test(NAME SocketSecurity COMMAND test_socket_security) +add_executable(test_socket_observable test_socket_observable.cpp) +target_compile_features(test_socket_observable PRIVATE cxx_std_17) +target_compile_options(test_socket_observable PRIVATE -Wall -Wextra -Wpedantic -Werror) +target_include_directories(test_socket_observable PRIVATE ../src ${ESPRESSIO_SECURITY_INCLUDE_DIR} ${ESPRESSIO_OBSERVABLE_INCLUDE_DIR}) +add_test(NAME SocketObservable COMMAND test_socket_observable) + if(ESPRESSIO_TIMING_INCLUDE_DIR AND ESPRESSIO_UNITS_INCLUDE_DIR AND ESPRESSIO_OBSERVABLE_INCLUDE_DIR) add_executable(test_clock_sync_protocol test_clock_sync_protocol.cpp) target_compile_features(test_clock_sync_protocol PRIVATE cxx_std_17) From 1ecc989968766a40cb4543bee780ac0bcd14a197 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:34:54 +0200 Subject: [PATCH 12/16] test: cover socket security session observations --- tests/test_socket_observable.cpp | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/test_socket_observable.cpp diff --git a/tests/test_socket_observable.cpp b/tests/test_socket_observable.cpp new file mode 100644 index 0000000..d815ecd --- /dev/null +++ b/tests/test_socket_observable.cpp @@ -0,0 +1,51 @@ +#include +#include +#include + +#include + +using namespace ESPressio; + +class SessionObserver final : public Sockets::ISocketSecuritySessionObserver { +public: + int Faulted = 0; + int Reset = 0; + + void OnSocketSecuritySessionFaulted(const Security::SecurityResult&) override { + ++Faulted; + } + + void OnSocketSecuritySessionReset() override { + ++Reset; + } +}; + +int main() { + Security::AeadCipherRegistry ciphers; + Security::StaticKeyProvider keys; + Security::StandardRandomSource random; + Security::TransportSecurityConfig config; + config.Policy = Security::TransportSecurityPolicy::Disabled; + Security::TransportSecurity security(ciphers, keys, random, config); + + Sockets::SocketSecuritySession session( + security, + [](const uint8_t*, std::size_t) { return true; } + ); + + SessionObserver observer; + auto handle = session.RegisterObserver(&observer); + assert(handle); + + const uint8_t malformedLength[5] = {0, 0, 0, 0, 1}; + assert(!session.Feed(malformedLength, sizeof(malformedLength))); + assert(observer.Faulted == 1); + + session.Reset(); + assert(observer.Reset == 1); + + handle.reset(); + session.Reset(); + assert(observer.Reset == 1); + return 0; +} From 2476a895d51cf1a84608fc9280365ed363e13507 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:37:11 +0200 Subject: [PATCH 13/16] docs: document Sockets 0.5.0 observable lifecycle --- README.md | 277 ++++++++++++++++++------------------------------------ 1 file changed, 92 insertions(+), 185 deletions(-) diff --git a/README.md b/README.md index c0401d5..2b2dd77 100644 --- a/README.md +++ b/README.md @@ -1,257 +1,164 @@ # ESPressio Sockets -Socket-based ESPressio transports, Command adapters, transport-security sessions, and System Clock synchronization providers for the Flowduino ESPressio Development Platform. +Socket-based ESPressio transports, Command adapters, transport-security sessions, System Clock synchronization providers, and observable transport lifecycle surfaces for the Flowduino ESPressio Development Platform. -## Latest Stable Version +## Current Development Version -The latest Stable Version is **0.4.0**. +This branch targets **ESPressio Sockets 0.5.0**. -## Compatibility - -ESPressio Sockets `0.4.0` targets ESP32/Arduino-ESP32 and C++17. Individual facilities may depend on Arduino networking classes, WebSockets, MQTT, or optional ESPressio libraries according to the adapter selected. +0.5.0 adds native Observable lifecycle coverage for socket workers and secure stream sessions, and refreshes the optional Security baseline to ESPressio Security 0.2.x. -## ESPressio Development Platform +See [CHANGELOG.md](CHANGELOG.md) for release history. -ESPressio libraries are discrete, composable components with explicit responsibility boundaries. Sockets owns IP/socket transport mechanics; Event owns Event semantics, Command owns Command parsing/execution, Timing owns clock discipline, and Security owns encryption/authentication/replay protection. +## Dependency model -## License +ESPressio Sockets keeps feature dependencies as narrow as possible. -Apache License 2.0. See [LICENSE](LICENSE). +### Core lifecycle dependency -## ESPressio Library Dependencies +Sockets 0.5.0 requires: -Core ESPressio Sockets has no mandatory ESPressio dependency. +- **ESPressio Observable >= 3.0.1 and < 4.0.0** for native worker/session lifecycle observation. -Optional integrations: +The Arduino networking integrations continue to use the existing WebSockets and PubSubClient dependencies where those adapters are selected. -```text -Event transports - ESPressio Event >= 5.7.1 < 6.0.0 +### Optional ESPressio integrations -Command integration - ESPressio Command >= 0.2.0 < 1.0.0 +- **ESPressio Timing >= 2.2.2 and < 3.0.0** — clock synchronization providers. +- **ESPressio Command >= 0.3.0 and < 1.0.0** — socket Command invocation/session facilities. +- **ESPressio Security >= 0.2.0 and < 1.0.0** — protected stream/datagram sessions. +- **ESPressio Event >= 5.8.0 and < 6.0.0** — Event transports and optional observer-to-Event bridges. -Clock synchronization - ESPressio Timing >= 2.2.2 < 3.0.0 +Security, Command, Timing and Event are deliberately not batch-included by `ESPressio_Sockets.hpp`; consuming code includes only the integration headers it needs. -Transport Security - ESPressio Security >= 0.1.0 < 1.0.0 +```text +Observable -----------------------> Sockets lifecycle +Timing -------- optional ---------> Sockets synchronization +Command ------- optional ---------> Sockets command sessions +Security ------ optional ---------> Sockets secure sessions +Event --------- optional ---------> socket Event transports / Event bridges ``` -External socket adapters continue to use WebSockets/PubSubClient where applicable. - -See [ESPRESSIO_DEPENDENCY_CHART.md](ESPRESSIO_DEPENDENCY_CHART.md), [COMMAND_INTEGRATION.md](COMMAND_INTEGRATION.md), and [SECURITY_INTEGRATION.md](SECURITY_INTEGRATION.md). - -## Namespace - -```cpp -ESPressio::Sockets -``` +See [ESPRESSIO_DEPENDENCY_CHART.md](ESPRESSIO_DEPENDENCY_CHART.md) for the broader ecosystem view. ## PlatformIO -Core Sockets: +For core Sockets 0.5.0: ```ini lib_deps = - https://github.com/Flowduino/ESPressio-Sockets@^0.4.0 - -build_flags = - -std=gnu++17 - -build_unflags = - -std=gnu++11 - -fno-rtti + flowduino/ESPressio-Sockets@^0.5.0 + flowduino/ESPressio-Observable@^3.0.1 ``` -Security integration: +Add only the optional ESPressio libraries needed by the selected integration. For example, a secure stream session additionally consumes Security 0.2.x. -```ini -lib_deps = - https://github.com/Flowduino/ESPressio-Sockets@^0.4.0 - https://github.com/Flowduino/ESPressio-Security@^0.1.0 -``` +Before tags are published, coordinated feature-branch testing can use the Git sources explicitly. -Add Event, Command, or Timing only when selecting those integrations. +## Core headers -## Header Structure +`ESPressio_Sockets.hpp` exposes lightweight core socket types and documents the available opt-in integration headers. -The normal umbrella is: +Important integration headers include: -```cpp -#include -``` +- `ESPressio_SocketWorker.hpp` +- `ESPressio_ISocketWorkerObserver.hpp` +- `ESPressio_SocketSecuritySession.hpp` +- `ESPressio_ISocketSecuritySessionObserver.hpp` +- `ESPressio_SocketSecurityDatagram.hpp` +- `ESPressio_SocketCommandSession.hpp` +- `ESPressio_TCPCommandServer.hpp` +- `ESPressio_SocketClockSynchronization.hpp` +- the UDP/TCP/TLS/WebSocket/MQTT Event Transport headers. -Dependency-bearing integrations are deliberately opt-in and are not included automatically. +## Socket worker lifecycle observation -Security headers: +`SocketWorker` owns a FreeRTOS worker task and now exposes lifecycle notifications through `ISocketWorkerObserver`: ```cpp -#include -#include -``` - -## Event Transports +class WorkerObserver final : + public ESPressio::Sockets::ISocketWorkerObserver { +public: + void OnSocketWorkerStarted(const char* name) override { + // Worker task successfully created. + } -ESPressio Sockets provides socket Event Transport adapters for: + void OnSocketWorkerStartFailed(const char* name) override { + // Worker task could not be created. + } -```text -UDP -TCP client -TCP server -TLS -WebSocket client/server -MQTT + void OnSocketWorkerStopped() override { + // Worker transitioned out of the running state. + } +}; ``` -Event routing/type semantics remain owned by ESPressio Event rather than being embedded in Sockets. +The observer surface is passive. It does not replace the worker implementation's own iteration logic or transport callbacks. -## Command Integration +## Secure stream sessions -`SocketCommandSession` and `TCPCommandServer` allow ESPressio Command trees to be invoked over socket connections. The integration supports line-oriented and structured-binary requests, correlation IDs, per-client state, policy hooks, bounded request handling, metadata, result observation, and error handling. - -See [COMMAND_INTEGRATION.md](COMMAND_INTEGRATION.md). - -## Clock Synchronization - -Optional Timing integration supplies UDP, TCP, and WebSocket synchronization mechanisms and external SNTP/NTP reference facilities while keeping clock-discipline policy inside ESPressio Timing. - -## Transport Security - -0.4.0 introduces optional ESPressio Security integration at the socket transport boundary. - -```text -Event / Command / application protocol - | - v - TransportSecurity - | - +------+------+ - | | - v v -SocketSecurity SocketSecurity -Session Datagram - | | - v v -stream socket datagram socket -``` +`SocketSecuritySession` applies ESPressio Security to stream-oriented carriers such as TCP/TLS/WebSocket byte streams while retaining explicit frame length boundaries. -Sockets does not implement AES, ChaCha, keys, nonces, or replay logic. It delegates those concerns to ESPressio Security and adapts socket framing/message boundaries. +The existing primary callbacks remain unchanged: -### `SocketSecuritySession` +- `WriteCallback` — strategy/dependency used to write bytes to the carrier. +- `ReceiveCallback` — primary authenticated payload delivery path. +- `FailureCallback` — existing direct failure notification. -For TCP/TLS/WebSocket-style byte streams: +0.5.0 adds `ISocketSecuritySessionObserver` for passive lifecycle/diagnostic consumers: ```cpp -Sockets::SocketSecuritySession session( - security, - [&](const uint8_t* data, std::size_t size) { - return client.write(data, size) == size; +class SessionObserver final : + public ESPressio::Sockets::ISocketSecuritySessionObserver { +public: + void OnSocketSecuritySessionFaulted( + const ESPressio::Security::SecurityResult& result + ) override { + // Metrics / diagnostics / audit reporting. } -); -``` -Each protected envelope is prefixed by a four-byte little-endian length. `Feed()` accepts arbitrary stream chunks: - -```cpp -session.Feed(receivedData, receivedLength); + void OnSocketSecuritySessionReset() override { + // Session framing state was explicitly reset. + } +}; ``` -It supports frames split across many reads and multiple frames arriving in one read. Declared frame lengths are bounded by `SocketSecuritySessionConfig::MaximumProtectedFrameBytes`. +Security protection/authentication failures and invalid protected-frame limits are observable without changing the existing return-value and callback semantics. -### `SocketSecurityDatagram` +## Datagram security -UDP/message-oriented sockets already preserve boundaries, so one ESPressio Security envelope is sent per datagram: +`SocketSecurityDatagram` remains the message-oriented companion for UDP-style carriers, with one Security envelope per datagram. Cryptographic session establishment, replay protection and authentication failures originate in ESPressio Security; applications that need to observe those underlying state transitions can subscribe to the `TransportSecurity` instance directly. -```cpp -Sockets::SocketSecurityDatagram datagram( - security, - sendDatagramCallback -); -``` +## Command integration -The incoming datagram is passed to `Receive()` and is delivered upward only after Security authentication/decryption and replay validation succeed. +Socket Command facilities remain opt-in and consume ESPressio Command. The 0.5.0 dependency generation expects Command 0.3.x, which includes Observable command-registry lifecycle support. Sockets does not duplicate those registry notifications. -### Security Guarantees +## Event integration -The adapters inherit Security 0.1.x semantics: +Socket Event transports remain opt-in. Additionally, ESPressio Event 5.8.0 supplies observer-to-Event bridges for the new Sockets lifecycle surfaces: -```text -AEAD encryption/authentication -protocol binding -key IDs and rotation -sender identity -authenticated session epoch -64-bit sequence numbers -sliding replay window -Disabled / Preferred / Required policies +```cpp +#include +#include ``` -`Required` is recommended for network-exposed Command/control traffic when plaintext must never be accepted. - -## TLS vs ESPressio Security - -TLS and ESPressio Security operate at different boundaries. - -TLS protects a connection/session. ESPressio Security protects the application transport payload with ESPressio-specific protocol binding, sender/session identity and replay semantics. - -Applications may use Security over plaintext TCP/UDP, or combine it with TLS/WSS as defense-in-depth. - -## Securing Commands - -The structured bytes used for Command invocation can be routed through `SocketSecuritySession`. Authentication/decryption therefore completes before the resulting Command invocation is passed to Command processing. - -Command does not need a direct Security dependency. +`SocketWorkerEventBridge` binds to a specific `SocketWorker`; `SocketSecuritySessionEventBridge` binds to a specific secure session. This keeps ownership explicit and prevents ESPressio Event from becoming a dependency of Sockets itself. -The same architecture applies to Event or future application protocols. +## Timing integration -## Failure Observation +Existing UDP/TCP/WebSocket/SNTP clock synchronization remains delegated to ESPressio Timing. Sockets provides transport mechanics; Timing owns synchronization policy and clock discipline. -Both Security adapters expose a failure callback carrying `SecurityResult`. This provides error classification for authentication, replay, key, algorithm, protocol, and frame-limit failures without exposing secret key material. +## Event transports -## Examples - -The repository includes examples for Event transports, socket clock synchronization, TCP Command serving, and: - -```text -examples/SecureTCPClient/SecureTCPClient.ino -``` - -The secure TCP example adapts `WiFiClient` to `SocketSecuritySession` using AES-256-GCM. Example credentials and key material are placeholders only. +Existing UDP, TCP, TLS, WebSocket and MQTT Event transports continue to use ESPressio Event's transport abstractions and routing policy. 0.5.0 does not change their wire protocol. ## Testing -The host suite covers existing functionality and the new Security integration: - -```text -CoreWithoutCommandOrSecurity -SocketCommand -SocketSecurity -ClockSynchronizationProtocol -``` - -`SocketSecurity` covers fragmented stream input, coalesced stream frames, declared-size limits, stream reset behavior, datagram protection and replay rejection. - -Permanent CI checks out released ESPressio Security 0.1.0 and compiles the real ESP32 secure TCP example in addition to host tests. +The host suite validates core inclusion, Command integration, Security integration, clock synchronization and the new secure-session observer contract. CI now validates against the coordinated Command 0.3 / Security 0.2 / Observable 3.0.1 dependency generation. ## Compatibility -Sockets 0.4.0 is a backward-compatible minor release: - -- existing Event Transport APIs remain unchanged; -- existing Command APIs remain unchanged; -- existing Timing synchronization remains unchanged; -- existing TLS/WSS behavior remains available; -- Security integration is opt-in; -- the normal umbrella remains independent of Security. - -## Contributing - -Issues and contributions are welcome through GitHub. New socket mechanisms should keep application semantics and cryptography outside their concrete I/O responsibility wherever possible. - -## Changelog - -See [CHANGELOG.md](CHANGELOG.md). +0.5.0 is intended as a backward-compatible extension of 0.4.x. Existing receive/write callbacks and transport integration APIs remain supported. Applications using the newly observable worker/session classes now need Observable 3.x available to the build. ## License From c9b53ef8e2e7196de17ee22b36fea0c0a0a250aa Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:49:06 +0200 Subject: [PATCH 14/16] docs: restore full README and document observable coverage --- README.md | 308 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 217 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 2b2dd77..1e99f9d 100644 --- a/README.md +++ b/README.md @@ -1,164 +1,290 @@ # ESPressio Sockets -Socket-based ESPressio transports, Command adapters, transport-security sessions, System Clock synchronization providers, and observable transport lifecycle surfaces for the Flowduino ESPressio Development Platform. +Socket-based ESPressio transports, Command adapters, transport-security sessions, and System Clock synchronization providers for the Flowduino ESPressio Development Platform. -## Current Development Version +## Latest Stable Version -This branch targets **ESPressio Sockets 0.5.0**. +The latest Stable Version is **0.4.0**. -0.5.0 adds native Observable lifecycle coverage for socket workers and secure stream sessions, and refreshes the optional Security baseline to ESPressio Security 0.2.x. +## Current Development Version — 0.5.0 -See [CHANGELOG.md](CHANGELOG.md) for release history. +The `feature/observable-callback-coverage` branch targets **0.5.0** and adds native Observable lifecycle coverage while preserving the existing socket/data callback model. -## Dependency model +For this development branch, the dependency model is: -ESPressio Sockets keeps feature dependencies as narrow as possible. +```text +Required + ESPressio Observable >= 3.0.1 < 4.0.0 + +Optional Command integration + ESPressio Command >= 0.3.0 < 1.0.0 + +Optional Transport Security + ESPressio Security >= 0.2.0 < 1.0.0 + +Optional Event integration + ESPressio Event >= 5.8.0 < 6.0.0 + +Optional Timing synchronization + ESPressio Timing >= 2.2.2 < 3.0.0 +``` -### Core lifecycle dependency +Observable coverage is owned by Sockets itself. `SocketWorker` exposes start/start-failure/stop lifecycle observation, and `SocketSecuritySession` exposes secure-session fault/reset observation. Existing receive, write, Command, Event Transport, and security-processing callbacks remain authoritative for their original responsibilities. -Sockets 0.5.0 requires: +ESPressio Event remains **opt-in**. Event 5.8 provides `SocketWorkerEventBridge` and `SocketSecuritySessionEventBridge`; Sockets does not depend upward on Event. ESPressio Serial 0.5 can consume the same observer contracts directly for diagnostics without requiring Event. -- **ESPressio Observable >= 3.0.1 and < 4.0.0** for native worker/session lifecycle observation. +The stable-release documentation below remains intact so existing 0.4.0 users retain accurate historical guidance. -The Arduino networking integrations continue to use the existing WebSockets and PubSubClient dependencies where those adapters are selected. +## Compatibility -### Optional ESPressio integrations +ESPressio Sockets `0.4.0` targets ESP32/Arduino-ESP32 and C++17. Individual facilities may depend on Arduino networking classes, WebSockets, MQTT, or optional ESPressio libraries according to the adapter selected. -- **ESPressio Timing >= 2.2.2 and < 3.0.0** — clock synchronization providers. -- **ESPressio Command >= 0.3.0 and < 1.0.0** — socket Command invocation/session facilities. -- **ESPressio Security >= 0.2.0 and < 1.0.0** — protected stream/datagram sessions. -- **ESPressio Event >= 5.8.0 and < 6.0.0** — Event transports and optional observer-to-Event bridges. +## ESPressio Development Platform -Security, Command, Timing and Event are deliberately not batch-included by `ESPressio_Sockets.hpp`; consuming code includes only the integration headers it needs. +ESPressio libraries are discrete, composable components with explicit responsibility boundaries. Sockets owns IP/socket transport mechanics; Event owns Event semantics, Command owns Command parsing/execution, Timing owns clock discipline, and Security owns encryption/authentication/replay protection. + +## License + +Apache License 2.0. See [LICENSE](LICENSE). + +## ESPressio Library Dependencies + +Core ESPressio Sockets has no mandatory ESPressio dependency in the stable 0.4.0 release. The 0.5.0 development branch adds the required Observable dependency documented above. + +Optional integrations for stable 0.4.0: ```text -Observable -----------------------> Sockets lifecycle -Timing -------- optional ---------> Sockets synchronization -Command ------- optional ---------> Sockets command sessions -Security ------ optional ---------> Sockets secure sessions -Event --------- optional ---------> socket Event transports / Event bridges +Event transports + ESPressio Event >= 5.7.1 < 6.0.0 + +Command integration + ESPressio Command >= 0.2.0 < 1.0.0 + +Clock synchronization + ESPressio Timing >= 2.2.2 < 3.0.0 + +Transport Security + ESPressio Security >= 0.1.0 < 1.0.0 ``` -See [ESPRESSIO_DEPENDENCY_CHART.md](ESPRESSIO_DEPENDENCY_CHART.md) for the broader ecosystem view. +External socket adapters continue to use WebSockets/PubSubClient where applicable. + +See [ESPRESSIO_DEPENDENCY_CHART.md](ESPRESSIO_DEPENDENCY_CHART.md), [COMMAND_INTEGRATION.md](COMMAND_INTEGRATION.md), and [SECURITY_INTEGRATION.md](SECURITY_INTEGRATION.md). + +## Namespace + +```cpp +ESPressio::Sockets +``` ## PlatformIO -For core Sockets 0.5.0: +Core Sockets: ```ini lib_deps = - flowduino/ESPressio-Sockets@^0.5.0 - flowduino/ESPressio-Observable@^3.0.1 + https://github.com/Flowduino/ESPressio-Sockets@^0.4.0 + +build_flags = + -std=gnu++17 + +build_unflags = + -std=gnu++11 + -fno-rtti ``` -Add only the optional ESPressio libraries needed by the selected integration. For example, a secure stream session additionally consumes Security 0.2.x. +Security integration: -Before tags are published, coordinated feature-branch testing can use the Git sources explicitly. +```ini +lib_deps = + https://github.com/Flowduino/ESPressio-Sockets@^0.4.0 + https://github.com/Flowduino/ESPressio-Security@^0.1.0 +``` -## Core headers +Add Event, Command, or Timing only when selecting those integrations. -`ESPressio_Sockets.hpp` exposes lightweight core socket types and documents the available opt-in integration headers. +For the 0.5.0 development branch, also include ESPressio Observable 3.0.1 or newer within the 3.x line, and use the Command/Security floors listed in the development-version section above. -Important integration headers include: +## Header Structure -- `ESPressio_SocketWorker.hpp` -- `ESPressio_ISocketWorkerObserver.hpp` -- `ESPressio_SocketSecuritySession.hpp` -- `ESPressio_ISocketSecuritySessionObserver.hpp` -- `ESPressio_SocketSecurityDatagram.hpp` -- `ESPressio_SocketCommandSession.hpp` -- `ESPressio_TCPCommandServer.hpp` -- `ESPressio_SocketClockSynchronization.hpp` -- the UDP/TCP/TLS/WebSocket/MQTT Event Transport headers. +The normal umbrella is: -## Socket worker lifecycle observation +```cpp +#include +``` -`SocketWorker` owns a FreeRTOS worker task and now exposes lifecycle notifications through `ISocketWorkerObserver`: +Dependency-bearing integrations are deliberately opt-in and are not included automatically. + +Security headers: ```cpp -class WorkerObserver final : - public ESPressio::Sockets::ISocketWorkerObserver { -public: - void OnSocketWorkerStarted(const char* name) override { - // Worker task successfully created. - } +#include +#include +``` - void OnSocketWorkerStartFailed(const char* name) override { - // Worker task could not be created. - } +## Event Transports - void OnSocketWorkerStopped() override { - // Worker transitioned out of the running state. - } -}; +ESPressio Sockets provides socket Event Transport adapters for: + +```text +UDP +TCP client +TCP server +TLS +WebSocket client/server +MQTT ``` -The observer surface is passive. It does not replace the worker implementation's own iteration logic or transport callbacks. +Event routing/type semantics remain owned by ESPressio Event rather than being embedded in Sockets. + +## Command Integration + +`SocketCommandSession` and `TCPCommandServer` allow ESPressio Command trees to be invoked over socket connections. The integration supports line-oriented and structured-binary requests, correlation IDs, per-client state, policy hooks, bounded request handling, metadata, result observation, and error handling. -## Secure stream sessions +See [COMMAND_INTEGRATION.md](COMMAND_INTEGRATION.md). -`SocketSecuritySession` applies ESPressio Security to stream-oriented carriers such as TCP/TLS/WebSocket byte streams while retaining explicit frame length boundaries. +## Clock Synchronization -The existing primary callbacks remain unchanged: +Optional Timing integration supplies UDP, TCP, and WebSocket synchronization mechanisms and external SNTP/NTP reference facilities while keeping clock-discipline policy inside ESPressio Timing. -- `WriteCallback` — strategy/dependency used to write bytes to the carrier. -- `ReceiveCallback` — primary authenticated payload delivery path. -- `FailureCallback` — existing direct failure notification. +## Transport Security -0.5.0 adds `ISocketSecuritySessionObserver` for passive lifecycle/diagnostic consumers: +0.4.0 introduces optional ESPressio Security integration at the socket transport boundary. + +```text +Event / Command / application protocol + | + v + TransportSecurity + | + +------+------+ + | | + v v +SocketSecurity SocketSecurity +Session Datagram + | | + v v +stream socket datagram socket +``` + +Sockets does not implement AES, ChaCha, keys, nonces, or replay logic. It delegates those concerns to ESPressio Security and adapts socket framing/message boundaries. + +### `SocketSecuritySession` + +For TCP/TLS/WebSocket-style byte streams: ```cpp -class SessionObserver final : - public ESPressio::Sockets::ISocketSecuritySessionObserver { -public: - void OnSocketSecuritySessionFaulted( - const ESPressio::Security::SecurityResult& result - ) override { - // Metrics / diagnostics / audit reporting. +Sockets::SocketSecuritySession session( + security, + [&](const uint8_t* data, std::size_t size) { + return client.write(data, size) == size; } +); +``` - void OnSocketSecuritySessionReset() override { - // Session framing state was explicitly reset. - } -}; +Each protected envelope is prefixed by a four-byte little-endian length. `Feed()` accepts arbitrary stream chunks: + +```cpp +session.Feed(receivedData, receivedLength); ``` -Security protection/authentication failures and invalid protected-frame limits are observable without changing the existing return-value and callback semantics. +It supports frames split across many reads and multiple frames arriving in one read. Declared frame lengths are bounded by `SocketSecuritySessionConfig::MaximumProtectedFrameBytes`. -## Datagram security +### `SocketSecurityDatagram` -`SocketSecurityDatagram` remains the message-oriented companion for UDP-style carriers, with one Security envelope per datagram. Cryptographic session establishment, replay protection and authentication failures originate in ESPressio Security; applications that need to observe those underlying state transitions can subscribe to the `TransportSecurity` instance directly. +UDP/message-oriented sockets already preserve boundaries, so one ESPressio Security envelope is sent per datagram: -## Command integration +```cpp +Sockets::SocketSecurityDatagram datagram( + security, + sendDatagramCallback +); +``` -Socket Command facilities remain opt-in and consume ESPressio Command. The 0.5.0 dependency generation expects Command 0.3.x, which includes Observable command-registry lifecycle support. Sockets does not duplicate those registry notifications. +The incoming datagram is passed to `Receive()` and is delivered upward only after Security authentication/decryption and replay validation succeed. -## Event integration +### Security Guarantees -Socket Event transports remain opt-in. Additionally, ESPressio Event 5.8.0 supplies observer-to-Event bridges for the new Sockets lifecycle surfaces: +The stable 0.4.0 adapters inherit Security 0.1.x semantics: -```cpp -#include -#include +```text +AEAD encryption/authentication +protocol binding +key IDs and rotation +sender identity +authenticated session epoch +64-bit sequence numbers +sliding replay window +Disabled / Preferred / Required policies ``` -`SocketWorkerEventBridge` binds to a specific `SocketWorker`; `SocketSecuritySessionEventBridge` binds to a specific secure session. This keeps ownership explicit and prevents ESPressio Event from becoming a dependency of Sockets itself. +`Required` is recommended for network-exposed Command/control traffic when plaintext must never be accepted. + +## TLS vs ESPressio Security + +TLS and ESPressio Security operate at different boundaries. + +TLS protects a connection/session. ESPressio Security protects the application transport payload with ESPressio-specific protocol binding, sender/session identity and replay semantics. + +Applications may use Security over plaintext TCP/UDP, or combine it with TLS/WSS as defense-in-depth. + +## Securing Commands -## Timing integration +The structured bytes used for Command invocation can be routed through `SocketSecuritySession`. Authentication/decryption therefore completes before the resulting Command invocation is passed to Command processing. -Existing UDP/TCP/WebSocket/SNTP clock synchronization remains delegated to ESPressio Timing. Sockets provides transport mechanics; Timing owns synchronization policy and clock discipline. +Command does not need a direct Security dependency. -## Event transports +The same architecture applies to Event or future application protocols. -Existing UDP, TCP, TLS, WebSocket and MQTT Event transports continue to use ESPressio Event's transport abstractions and routing policy. 0.5.0 does not change their wire protocol. +## Failure Observation + +Stable 0.4.0 Security adapters expose a failure callback carrying `SecurityResult`. This provides error classification for authentication, replay, key, algorithm, protocol, and frame-limit failures without exposing secret key material. + +The 0.5.0 development branch additionally exposes the corresponding `ISocketSecuritySessionObserver` lifecycle contract and the general `ISocketWorkerObserver` lifecycle contract. These observer notifications complement rather than replace existing operational callbacks. + +## Examples + +The repository includes examples for Event transports, socket clock synchronization, TCP Command serving, and: + +```text +examples/SecureTCPClient/SecureTCPClient.ino +``` + +The secure TCP example adapts `WiFiClient` to `SocketSecuritySession` using AES-256-GCM. Example credentials and key material are placeholders only. ## Testing -The host suite validates core inclusion, Command integration, Security integration, clock synchronization and the new secure-session observer contract. CI now validates against the coordinated Command 0.3 / Security 0.2 / Observable 3.0.1 dependency generation. +The host suite covers existing functionality and the new Security integration: + +```text +CoreWithoutCommandOrSecurity +SocketCommand +SocketSecurity +ClockSynchronizationProtocol +``` + +`SocketSecurity` covers fragmented stream input, coalesced stream frames, declared-size limits, stream reset behavior, datagram protection and replay rejection. The 0.5.0 development branch extends host validation to the new observer lifecycle surface and tests against the refreshed Command/Security/Observable dependency generation. ## Compatibility -0.5.0 is intended as a backward-compatible extension of 0.4.x. Existing receive/write callbacks and transport integration APIs remain supported. Applications using the newly observable worker/session classes now need Observable 3.x available to the build. +Sockets 0.4.0 is a backward-compatible minor release: + +- existing Event Transport APIs remain unchanged; +- existing Command APIs remain unchanged; +- existing Timing synchronization remains unchanged; +- existing TLS/WSS behavior remains available; +- Security integration is opt-in; +- the normal umbrella remains independent of Security. + +The 0.5.0 development branch is also designed as a backward-compatible minor extension. Observable becomes a core dependency because core worker lifecycle is now observable; Event itself remains optional. + +## Contributing + +Issues and contributions are welcome through GitHub. New socket mechanisms should keep application semantics and cryptography outside their concrete I/O responsibility wherever possible. + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md). ## License From 228da490ece6f6d8a95b3ac398b39cc1675c7b54 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 20:34:22 +0200 Subject: [PATCH 15/16] ci: validate released Command and Security baselines --- .github/workflows/host-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/host-tests.yml b/.github/workflows/host-tests.yml index 9387e80..ffd89d9 100644 --- a/.github/workflows/host-tests.yml +++ b/.github/workflows/host-tests.yml @@ -14,18 +14,18 @@ jobs: - name: Checkout Sockets uses: actions/checkout@v4 - - name: Checkout ESPressio Command 0.3.0 feature source + - name: Checkout ESPressio Command 0.3.0 uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-Command - ref: 74ea89f9c8895cfaa272bae44d0eba5711a3dbc1 + ref: 0.3.0 path: deps/ESPressio-Command - - name: Checkout ESPressio Security 0.2.0 feature source + - name: Checkout ESPressio Security 0.2.0 uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-Security - ref: cb5224b3f98174f8b521ff87d92a373d2e97dcf7 + ref: 0.2.0 path: deps/ESPressio-Security - name: Checkout ESPressio Timing 2.2.2 From 048bb41bcd7b5f4984dffc26ff95193216e01cee Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 20:34:33 +0200 Subject: [PATCH 16/16] ci: validate Security 0.2.0 and Command 0.3.0 integration --- .../workflows/security-integration-tests.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/security-integration-tests.yml b/.github/workflows/security-integration-tests.yml index d7412bc..2b653e8 100644 --- a/.github/workflows/security-integration-tests.yml +++ b/.github/workflows/security-integration-tests.yml @@ -2,7 +2,7 @@ name: Security Integration on: push: - branches: [feature/security-integration, main] + branches: [feature/security-integration, feature/observable-callback-coverage, main] pull_request: branches: [main] @@ -14,12 +14,12 @@ jobs: - uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-Command - ref: 0.2.0 + ref: 0.3.0 path: deps/ESPressio-Command - uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-Security - ref: 0.1.0 + ref: 0.2.0 path: deps/ESPressio-Security - uses: actions/checkout@v4 with: @@ -56,13 +56,18 @@ jobs: - uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-Security - ref: 0.1.0 + ref: 0.2.0 path: deps/ESPressio-Security + - uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Observable + ref: 3.0.1 + path: deps/ESPressio-Observable - name: Install PlatformIO run: pip install platformio - name: Compile SecureTCPClient run: >- pio ci examples/SecureTCPClient/SecureTCPClient.ino - --board esp32dev --lib . --lib deps/ESPressio-Security - --project-option="build_flags=-std=gnu++17" + --board esp32dev --lib . --lib deps/ESPressio-Security --lib deps/ESPressio-Observable + --project-option="build_flags=-std=gnu++17 -frtti" --project-option="build_unflags=-std=gnu++11 -fno-rtti"