Serial and console-oriented components for the Flowduino ESPressio Development Platform.
Version 0.3.0 adds a reusable Stream/Print command console and an opt-in operator-facing Event Console capable of discovering, describing, composing as JSON, validating, and dispatching runtime-registered Serializable Events through ESPressio Event 5.6.1.
The latest stable version is 0.4.0.
ESPressio is a collection of discrete, composable component libraries designed around a common development ethos:
- Light-weight
- Ease of use
- Object-oriented design
- SOLID design principles
- Pay only for the functionality an application selects
Licensed under the Apache License 2.0. See LICENSE.
The public API resides beneath:
ESPressio::SerialBecause Arduino exposes a global object named Serial, fully qualified ESPressio Serial names are recommended:
ESPressio::Serial::EventMonitor monitor;while the Arduino serial port remains:
::SerialThe core ESPressio Serial library has no required ESPressio library dependencies.
The Event Monitor is deliberately opt-in and requires:
ESPressio Event >= 5.7.1 < 6.0.0
ESPressio Serializable >= 0.10.0 < 1.0.0
For the complete ecosystem hierarchy, see:
ESPressio Library Dependency Chart
In the dependency chart:
- Solid relationships represent required dependencies.
- Dashed relationships represent opt-in dependencies introduced only when the associated feature/header is used.
Version 0.3.1 updates the optional EventConsole integration baseline to ESPressio Event 5.6.1.
Event 5.6.1 corrects EventDispatchContext equality semantics required by ESPressio Threads 3.1 ReadWriteMutex<T> change detection. No ESPressio Serial console, monitoring, logging, or EventConsole public API changes are required.
Applications using EventConsole should therefore target:
flowduino/ESPressio-Event@^5.7.1The core Serial library and generic Console remain independent of ESPressio Event.
Version 0.3.0 adds the interactive operator/service-console layer.
The architecture deliberately preserves library ownership:
operator
|
v
ESPressio Serial Console
|
| JSON
v
ESPressio Serializable JsonArchive
|
| SerializationNode
v
ESPressio Event 5.6 runtime registry/factory
|
v
concrete Serializable Event
|
v
normal Queue / Stack dispatch
|
+--> local listeners
|
+--> EventTransportManager
|
+--> any configured outbound transport
Serial does not create a second Event registry or remote-dispatch mechanism.
The generic console is available independently of Event:
#include <ESPressio_Console.hpp>
ESPressio::Serial::Console console;
void setup() {
::Serial.begin(115200);
ESPressio::Serial::ConsoleConfig config;
config.Prompt = "espressio> ";
console.Initialize(
::Serial,
::Serial,
config
);
console.RegisterCommand(
"hello",
"Print a greeting",
[](const auto& context) {
// Handle context.Arguments.
}
);
}
void loop() {
console.Poll();
}Input uses Arduino Stream; output uses Arduino Print.
The console therefore works with Hardware Serial, USB CDC, or another compatible implementation.
The line buffer is bounded through:
ConsoleConfig::MaximumLineLengthand the console supports:
command registration
command unregistration
help
arguments
prompt configuration
optional input echo
multiple interactive line interceptors
backspace/delete handling
CR/LF handling
Multiple line interceptors are intentional: future console extensions can maintain independent interactive states without replacing one global input handler.
The Event Console is opt-in:
#include <ESPressio_EventConsole.hpp>and requires:
ESPressio Event >= 5.7.1
ESPressio Serializable >= 0.10.0 < 1.0.0
ArduinoJson (through the optional Serializable JsonArchive)
Initialize it over an existing Console:
ESPressio::Serial::Console console;
ESPressio::Serial::EventConsole eventConsole;
console.Initialize(
::Serial,
::Serial
);
eventConsole.Initialize(
console
);Runtime Event discovery does not imply permission to dispatch an Event.
The default access policy is:
EventConsoleAccessPolicy::AllowListedOnlyAllow specific Event types:
eventConsole.AllowEvent<
CameraShutterEvent
>();
eventConsole.AllowEvent(
"flowduino.motor.move.v1"
);For a controlled development environment, explicitly enable all registered types:
eventConsole.SetAccessPolicy(
ESPressio::Serial::
EventConsoleAccessPolicy::
AllRegistered
);Deny-list entries override allow-all:
eventConsole.DenyEvent<
FactoryResetEvent
>();This prevents a registered administrative/destructive Event from becoming operator-dispatchable merely because a console is enabled.
List runtime-registered Serializable Events:
espressio> events
Registered Serializable Events:
flowduino.camera.shutter.v1 [constructible] [allowed] schema=1 defaultRouting=Outbound
flowduino.motor.move.v1 [constructible] [allowed] schema=2 defaultRouting=Bidirectional
flowduino.system.factory-reset.v1 [constructible] [denied] schema=1 defaultRouting=None
The equivalent command is:
event list
espressio> event describe flowduino.motor.move.v1
uses Event 5.6's runtime descriptor and Serializable schema metadata to report:
stable Event type name
stable Event type ID
schema version
runtime constructibility
operator access
default Event Transport direction
property names
property types
required state
read-only state
sensitive metadata
default-value availability
aliases
Per-transport route names are not fabricated: Event 5.6 currently exposes the default routing direction through the public runtime descriptor.
Queue:
event queue flowduino.motor.move.v1 {"axis":"pan","position":45,"speed":20}
Stack:
event stack flowduino.motor.move.v1 {"axis":"pan","position":45,"speed":20}
event dispatch is a Queue alias.
JSON is parsed through ESPressio Serializable's JsonArchive, converted to a representation-neutral SerializationNode, and passed to Event 5.6's runtime factory.
event compose flowduino.motor.move.v1
or:
event compose flowduino.motor.move.v1 stack
prompts for a one-line JSON object:
Enter one-line JSON object for flowduino.motor.move.v1 (or 'cancel'):
{"axis":"pan","position":45,"speed":20}
Runtime-created Events use the normal ESPressio Serializable validation path.
Validation errors are presented to the operator with:
property path
serialization error code
diagnostic message
For example:
Event payload validation failed with 2 issue(s):
speed: NumericOutOfRange - Property failed its numeric range constraint
axis: UnknownEnumValue - Value is not a registered enum mapping
No separate Serial-specific Event validation system exists.
Confirmation is enabled by default:
Dispatch Event 'flowduino.motor.move.v1' via Queue priority=Normal? [y/N]
Only y or yes proceeds; any other response cancels the dispatch.
It can be disabled explicitly:
EventConsoleConfig config;
config.RequireConfirmation = false;Event Console uses Event 5.6's ownership-safe runtime dispatch API.
Once dispatched, the Event follows the normal Event system:
runtime-created Event
|
v
Queue / Stack
|
v
local Event dispatch
|
v
EventTransportManager
|
v
existing per-transport outbound routing
Event Console therefore knows nothing about ESP-NOW, UDP, TCP, WebSocket, MQTT, or another concrete Event transport.
EventConsole can optionally send security/operation audit records to any existing:
ILoggerSinkusing:
eventConsole.SetAuditSink(
&history
);Useful audit conditions include:
successful operator dispatch
denied dispatch
unregistered type
malformed JSON
oversized JSON
construction/validation failure
dispatch failure
The Event payload itself is deliberately not copied into the audit message by default, avoiding accidental logging of sensitive properties.
Console-created Events naturally flow through the ordinary Event Transport pipeline.
If EventMonitor is enabled, the same operator-created Event appears in its normal outbound/inbound transaction diagnostics without any special integration code.
Operator JSON is bounded by:
EventConsoleConfig::MaximumJsonLengthand the enclosing generic Console independently bounds total input line length.
Queue and Stack dispatch can be independently disabled:
config.AllowQueue = true;
config.AllowStack = false;Version 0.3.0 adds:
examples/
├── Console/
│ └── Console.ino
│
├── EventConsole/
│ └── EventConsole.ino
│
└── EventConsoleLoopback/
└── EventConsoleLoopback.ino
EventConsoleLoopback combines the operator console, Event Console, Event Monitor, Serializable Event, and a local loopback IEventTransport to demonstrate the complete:
Serial JSON
-> runtime Event
-> local dispatch
-> Event Transport
-> inbound reconstruction
-> Serial Event Monitor
pipeline on one ESP32.
The repository includes host-side tests for:
generic Console command dispatch
argument preservation
multiple interactive line interceptors
interceptor removal
Stream polling
runtime Event listing
Event schema description
allow-list enforcement
JSON command processing
pre-dispatch confirmation
type-erased dispatch
The Event Console contract test uses narrow test doubles for Event 5.6 and the Serializable JSON adapter, while release preparation verifies compatibility with the real public API surface.
Version 0.2.0 adds a general diagnostics foundation alongside the existing Event Monitor.
#include <ESPressio_Logging.hpp>
ESPressio::Serial::Logger<> logger;
ESPressio::Serial::SerialLogSink serialSink(::Serial);
ESPressio::Serial::DiagnosticRingBuffer<64> history;
void setup() {
::Serial.begin(115200);
logger.AddSink(serialSink);
logger.AddSink(history);
logger.SetMinimumLevel(
ESPressio::Serial::LogLevel::Debug
);
logger.Info("Application", "Boot complete");
}Supported levels are:
Trace
Debug
Info
Warning
Error
Critical
Off
Logger supports multiple simultaneous ILoggerSink implementations. Logging data is therefore separated from its output destination: Serial is one sink, not the logging architecture itself.
ESPRESSIO_SERIAL_COMPILETIME_LOG_LEVEL may be defined to remove lower-severity calls from runtime delivery, while SetMinimumLevel() provides runtime filtering.
DiagnosticRingBuffer<Capacity> is both an ILoggerSink and a bounded in-memory history.
ESPressio::Serial::DiagnosticRingBuffer<64> history;
logger.AddSink(history);
// Later, after a fault:
history.Dump(::Serial);Entries are copied into fixed-size storage; the oldest entry is overwritten when capacity is exhausted. This makes it suitable for retaining the diagnostic events immediately preceding a failure without unbounded heap growth.
#include <ESPressio_SystemClockMonitor.hpp>
ESPressio::Serial::SystemClockMonitor<> clockMonitor;
clockMonitor.Initialize(::Serial);This integration directly consumes ESPressio Timing 2.2.2's ISystemClockObserver notifications. It reports time-setting, synchronization acceptance/rejection, synchronization state changes, resets/configuration changes, and callback scheduling/execution.
Synchronization output includes the clock value before correction, the value after correction, and the immediate nanosecond difference.
This is an opt-in Timing dependency; ESPressio Event is not involved.
#include <ESPressio_ThreadMonitor.hpp>
ESPressio::Serial::ThreadMonitor threadMonitor;
threadMonitor.Initialize(::Serial);ThreadMonitor directly observes the process-wide ESPressio Threads 3.1.2 infrastructure:
ThreadManager
ThreadGarbageCollector
ThreadTerminationDispatcher
It reports registration, cleanup, garbage collection, termination dispatch, initialization, and failure lifecycle notifications.
This is an opt-in Threads dependency; Event bridges are not required merely to display Thread diagnostics.
When the corresponding dependency headers are available, the convenience monitor can compose all supported subsystem monitors:
#include <ESPressio_DiagnosticMonitor.hpp>
ESPressio::Serial::DiagnosticMonitor diagnostics;
void setup() {
::Serial.begin(115200);
ESPressio::Serial::DiagnosticMonitorConfig config;
config.SystemClock = true;
config.Threads = true;
config.Events = true;
diagnostics.Initialize(
::Serial,
config
);
}The aggregate uses compile-time feature detection. It does not itself make Timing, Threads, Event, or Serializable mandatory package dependencies.
ESPressio Serial core
-> no mandatory ESPressio dependency
Logging
-> no additional ESPressio dependency
SystemClockMonitor
- - -> ESPressio Timing >= 2.2.2 < 3.0.0
ThreadMonitor
- - -> ESPressio Threads >= 3.1.2 < 4.0.0
EventMonitor
- - -> ESPressio Event >= 5.7.1 < 6.0.0
- - -> ESPressio Serializable >= 0.10.0 < 1.0.0
All ESPressio relationships remain opt-in.
#include <ESPressio_Serial.hpp>The core header contains common ESPressio Serial types only.
It does not include ESPressio Event or ESPressio Serializable.
Event monitoring is selected explicitly:
#include <ESPressio_EventMonitor.hpp>or through the feature batch header:
#include <ESPressio_SerialEventMonitoring.hpp>EventMonitor consumes the Event Transport Transaction Observation API introduced by ESPressio Event 5.5.0.
It does not implement an Event Transport and does not alter Event routing.
Conceptually:
Serializable Event
|
v
EventTransportManager
|
+-----------------------> concrete transport
|
+--> transaction Observer
|
v
EventMonitor
|
v
Arduino Print
|
+--------+--------+
| |
v v
Serial USB CDC
Any Print implementation may be used. The monitor is therefore not tied specifically to HardwareSerial.
#include <ESPressio_EventMonitor.hpp>
ESPressio::Serial::EventMonitor
monitor;
void setup() {
::Serial.begin(115200);
ESPressio::Serial::
EventMonitorConfig
config;
monitor.Initialize(
::Serial,
config
);
}Initialize() registers the monitor with the selected EventTransportManager.
It does not initialize the Event Transport Manager itself. The application remains responsible for its normal Event Transport setup and initialization.
The monitor unregisters automatically when destroyed or when:
monitor.Shutdown();is called.
The default mode is:
EventMonitorMode::EventsThis is intended to provide one useful record per logical transported Event rather than printing every internal lifecycle transition.
It reports:
outbound Event after concrete transport handoff
inbound Event after successful deserialization
inbound rejection
transport processing failure
For example:
[ESPressio Event] [OUT] [OutboundHandedToTransport] type=flowduino.example.serial.monitored-counter.v1 typeId=0x... schema=1 message=3 transport=0x... dispatch=Queue priority=Normal origin=Local hops=0 accepted=true payloadBytes=...
payload: {
"__schemaVersion": 1,
"counter": 3,
"source": "local"
}
and the looped-back inbound Event may then appear as:
[ESPressio Event] [IN] [InboundDeserialized] type=flowduino.example.serial.monitored-counter.v1 typeId=0x... schema=1 message=3 transport=0x... dispatch=Queue priority=Normal origin=Remote hops=0 payloadBytes=...
payload: {
"__schemaVersion": 1,
"counter": 3,
"source": "local"
}
For deeper diagnostics:
config.Mode =
ESPressio::Serial::
EventMonitorMode::Lifecycle;prints every Event 5.5 transaction stage exposed by EventTransportManager:
OutboundAccepted
OutboundSerialized
OutboundHandedToTransport
InboundAccepted
InboundRejected
InboundDeserialized
InboundDispatched
Failed
Lifecycle mode is intentionally verbose and is most useful while debugging the transport pipeline itself.
The Event Monitor supports:
EventMonitorPayloadFormat::None
EventMonitorPayloadFormat::Summary
EventMonitorPayloadFormat::Hex
EventMonitorPayloadFormat::StructuredOnly transaction metadata is printed.
Reports payload size without printing payload contents.
Prints the Serializable Binary Archive bytes in hexadecimal.
The maximum number of bytes is controlled by:
config.MaximumHexPayloadBytesStructured 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.
This has two important advantages:
- the monitor does not need to know the concrete C++ Event type;
- it does not require ArduinoJson merely to present human-readable diagnostics.
The monitor is therefore able to inspect arbitrary transported Serializable Event payloads using the schema already encoded in the Binary Archive.
Diagnostic output should not be allowed to grow without bound.
Configuration includes:
MaximumCollectionItems
MaximumStringLength
MaximumStructuredDepth
IndentSpaces
PrettyStructuredPayloadThese provide deterministic limits when monitoring large or deeply nested Event payloads.
The monitor can independently enable or disable:
stable Event type name
stable Event type ID
schema version
message ID
transport address
dispatch method
priority
origin
hop count
transport acceptance result
using EventMonitorConfig.
Inbound and outbound monitoring can also be enabled independently.
ESPressio Event 5.5 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.
Event Transport transaction observation is synchronous.
Serial/USB output can be comparatively slow.
Enabling Event Monitor—particularly Lifecycle mode or large structured payload output—can therefore add diagnostic latency to the Event Transport execution path.
This is intentional for a developer-facing monitor, but applications with tight real-time requirements should:
- disable monitoring in production;
- use
SummaryorNonepayload modes; - use an appropriately fast
Printdestination; - avoid Lifecycle mode except during diagnosis.
The Event Monitor changes observation only; it does not change Event routing or transport semantics.
The repository includes:
examples/
└── EventMonitor/
└── EventMonitor.ino
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.
A project using only the core Serial library:
lib_deps =
flowduino/ESPressio-Serial@^0.4.0An application using Event Monitor requires:
lib_deps =
flowduino/ESPressio-Serial@^0.4.0
flowduino/ESPressio-Event@^5.7.1
flowduino/ESPressio-Serializable@^0.10.0The 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.
The generic console requires only ESPressio Serial:
lib_deps =
flowduino/ESPressio-Serial@^0.4.0The Event Console additionally requires the runtime Event and JSON stacks:
lib_deps =
flowduino/ESPressio-Serial@^0.4.0
flowduino/ESPressio-Event@^5.7.1
flowduino/ESPressio-Serializable@^0.10.0
bblanchon/ArduinoJsonArduinoJson is required only because EventConsole selects ESPressio Serializable's optional JsonArchive; it remains unnecessary for core Serial, logging, diagnostics, and the generic Console.
ESPressio Serial is intended to contain Serial/console-oriented ESPressio integrations rather than becoming a general communications catch-all.
Potential future components include:
Serial Event Transport
structured Event-based remote log sinks
persistent diagnostic sinks
additional subsystem console commands
serial configuration interfaces
serial protocol adapters
operator authentication/session policy where appropriate
Network/socket implementations belong in ESPressio Sockets.
ESP-NOW implementations belong in ESPressio ESP-Now.
Hardware-radio implementations belong in the planned ESPressio Radio library.
ESPressio Serial 0.3.0 provides three complementary layers:
CORE
ESPressio_Serial.hpp
diagnostic types
no mandatory ESPressio dependency
DIAGNOSTICS / LOGGING
Logger
SerialLogSink
DiagnosticRingBuffer
SystemClockMonitor [opt-in Timing]
ThreadMonitor [opt-in Threads]
EventMonitor [opt-in Event + Serializable]
DiagnosticMonitor
OPERATOR CONSOLE
Console
Stream input
Print output
extensible commands
EventConsole [opt-in Event 5.6 + Serializable JSON]
runtime Event discovery
schema description
JSON composition
validation diagnostics
allow/deny policy
confirmation
Queue / Stack dispatch
The central rule remains unchanged:
ESPressio Serial owns human/operator interaction and presentation; the upstream ESPressio libraries continue to own their underlying runtime semantics.
Serial 0.4.0 adds an opt-in bridge to ESPressio Command >= 0.2.0 < 1.0.0. Core Serial remains usable without Command. Include ESPressio_CommandConsole.hpp only when the integration is required.
lib_deps =
flowduino/ESPressio-Serial@^0.4.0
flowduino/ESPressio-Command@^0.2.0#include <ESPressio_Console.hpp>
#include <ESPressio_CommandConsole.hpp>
#include <ESPressio_Commands.hpp>
ESPressio::Serial::Console console;
ESPressio::Serial::CommandConsole commandConsole;
void setup() {
console.Initialize(Serial, Serial);
commandConsole.Initialize(console);
auto& commands = ESPressio::Command::CommandRegistry::GetInstance();
commands.Command("system").Command("status")
.OnExecute([](const ESPressio::Command::CommandContext&) {
return ESPressio::Command::CommandResult::Ok("System OK");
});
}
void loop() { console.Poll(); }CommandConsole reuses Serial's existing input/prompt handling and forwards resolvable lines into the shared transport-neutral Command registry. Unknown roots fall through so other Console interceptors and legacy commands can continue to coexist.
When Command integration is selected, initialize EventConsole with CommandConsole:
ESPressio::Serial::EventConsole eventConsole;
eventConsole.Initialize(commandConsole);EventConsole then registers the following shared Command tree with ownership-safe registration handles:
event list
event describe <type>
event compose <type> [queue|stack]
event queue <type> <json>
event stack <type> <json>
event dispatch <type> <json>
event cancel
events
Shutdown removes the registered subtrees, preventing callbacks from outliving the EventConsole instance. The previous Initialize(Console&, ...) overload remains available for compatibility with existing applications.