- Date Created: 2025-12-19
- Last Modified: 2025-12-19
Progress Log: V2 Queue Symmetry Design & Analysis¶
Task Description¶
Analyzed the EventQueue and CommandQueue implementations to verify architectural symmetry in the unified device response protocol (v1.11.3+). The goal was to confirm that both input (commands) and output (events) paths follow mirrored designs for consistency and maintainability.
Scope:
- Compare EventQueue and CommandQueue structural design
- Verify 4-layer architecture (Layer 0 domain β Layer 1 validation β Layer 2 conversion β Layer 3 serialization)
- Identify symmetric vs. asymmetric patterns
- Document flow diagrams and design findings
Outcome¶
β Verification Complete: High Symmetry Achieved¶
Both queues implement a clean, mirrored architecture with excellent separation of concerns. The design successfully maintains symmetry at the type and abstraction level while allowing operational asymmetry where it makes sense.
Key Findings¶
1. Layer 0 (Domain Data) - FULLY SYMMETRIC¶
// Command Path
typedef struct {
char name[32]; // Command name
uint8_t arg_count;
char args[MAX_COMMAND_ARGS][MAX_ARG_LENGTH];
} command_t;
// Event Path
typedef struct {
uint16_t hit1, hit2, hit3; // Detection counts
int16_t adc; // ADC reading
// ... optional fields controlled by ENABLE_* flags
} event_t;
Symmetry:
- Both are lightweight, structured representations of domain data
- Both can carry optional fields via conditional compilation
- Both are queued "as-is" (Layer 0 in queue)
- FreeRTOS queue holds the raw type (command_t or event_t)
Design Benefit: Minimal copying, zero conversion overhead at queue level.
2. Layer 1 (Validation/Response) - FULLY SYMMETRIC¶
// Command Response (Layer 1)
typedef struct {
device_response_type_t type;
device_response_status_t status;
device_response_code_t error_code;
char error_message[256];
uint64_t sent_us;
const JsonDocument* payload; // Payload Pointer
} command_response_t;
// Event Response (Layer 1)
typedef struct {
device_response_type_t type;
device_response_status_t status;
device_response_code_t error_code;
char error_message[256];
uint64_t sent_us;
// ... flattened event fields (hit1, hit2, hit3, adc, etc.)
} event_response_t;
Symmetry:
- Both have identical envelope (
type,status,error_code,error_message,sent_us) - Both carry payload data (command responses via pointer, events via flattened fields)
- Both have mirror helper functions:
command_response_ok()βevent_response_ok()command_response_error()βevent_response_error()- Both encode errors with semantic codes (
DEVICE_CODE_INVALID_ARG,DEVICE_CODE_OUT_OF_RANGE, etc.)
Design Benefit: Consistent error handling and validation semantics across both paths.
3. FreeRTOS Queue - FULLY SYMMETRIC¶
| Property | CommandQueue | EventQueue | Symmetry |
|---|---|---|---|
| Static Allocation | xQueueCreateStatic() |
xQueueCreateStatic() |
β Identical |
| Queue Size | 10 items | 200 items | β Sized for use case (commands sparse, events frequent) |
| Item Type | command_t |
event_t |
β Both Layer 0 types |
| Non-Blocking | xQueueSend(..., 0) |
xQueueSend(..., 0) |
β Identical |
| Overflow Handling | Drops oldest (DOS safety) | Increments counter | β Both safe |
| Stream Control | Command::getInstance().get_stream() check |
Same | β Identical |
Design Benefit: Deterministic behavior, no dynamic allocation, safe overflow handling.
4. Processing Flow - ASYMMETRIC (BY DESIGN)¶
CommandQueue (Input Path):
receive() β parse() β xQueueSend(command_t) β execute() β dispatch() β handler β send()
Ownership: CommandQueue manages lifecycle (receive, queue, execute)
Responsibility: Parse, validate args, route to handler
EventQueue (Output Path):
enqueue(event_t from caller) β xQueueSend(event_t) β flush() β serialize β send()
Ownership: Caller manages detection & sensors, EventQueue manages queueing & serialization
Responsibility: Buffer high-frequency events, serialize to JSON
Analysis:
This asymmetry is intentional and correct:
-
CommandQueue::receive() must be internal because:
- Serial reception happens in main loop
- Parsing is domain-specific (requires command table)
- Error recovery (buffer overflow, malformed input) must be atomic
-
EventQueue::enqueue() is external because:
- Detection happens in detection loop (different context)
- Caller has raw sensor data (temperature, ADC, hits)
- Separation: detection logic β queueing logic
Why This is Good:
- CommandQueue encapsulates serial I/O (implementation detail)
- EventQueue is a pure queue service (open to any caller)
- Both avoid mixing domain logic with infrastructure
5. Layer 2 (Type Conversion) - FULLY SYMMETRIC¶
// Command path
device_response_t DeviceResponse::from_command(const command_response_t& cmd);
// Event path
device_response_t DeviceResponse::from_event(const event_response_t& event);
Symmetry:
- Both convert Layer 1 β Layer 2 (typed struct β transport envelope)
- Both create
device_response_twith identical envelope fields - Both copy payload (command via pointer, event via JsonDocument pointer set in from_event)
- Both deterministic, side-effect-free
Design Benefit: Clean, predictable transformation layer.
6. Layer 3 (Serialization) - FULLY SYMMETRIC¶
// Single unified serialization for both paths
DeviceResponse::send(const device_response_t& response); // Outputs JSONL
Symmetry:
- Both command and event responses use identical send() function
- No type-specific branching (type is a field in device_response_t, not behavior discriminator)
- Handles all optional fields via conditional compilation
Design Benefit: Unified output format, single serialization path.
Flow Diagram: Complete Symmetry¶
ββββ COMMAND INPUT PATH βββββββββββββββββββββββββ ββββ EVENT OUTPUT PATH βββββββββββββββββββββββ
β β β β
β Layer 0 (Domain) β β Layer 0 (Domain) β
β ββββββββββββββββββββββββββ β β ββββββββββββββββββββββββββββ β
β β Serial Input Stream β β β β Detection Loop β β
β β β β β β (cosmic_read()) β β
β β "SET_POLL_COUNT 200\n"β β β β β hit1=95, hit2=87,... β β
β βββββββββββββ¬βββββββββββββ β β ββββββββββββββ¬ββββββββββββββ β
β β β β β β
β ββββββββββββvβββββββββββββ β β βββββββββββββvβββββββββββββββ β
β β CommandQueue::receive()β β β β EventQueue::enqueue() β β
β β - Parse line β β β β - Check stream enabled β β
β β - Split by whitespace β β β β - Queue event_t β β
β β - Resolve aliases β β β β - Track overflows β β
β βββββββββββββ¬βββββββββββββ β β ββββββββββββββ¬βββββββββββββββ β
β β β β β β
β Layer 0βLayer 1 β β Layer 0βLayer 1 β
β ββββββββββββvβββββββββββββ β β βββββββββββββvβββββββββββββββ β
β β command_t β β β β event_t (Layer 0) β β
β β ββ name="SET_PC" β β β β ββ hit1=95 β β
β β ββ arg_count=1 β β β β ββ hit2=87 β β
β β ββ args[0]="200" β β β β ββ hit3=92 β β
β β β β β β ββ adc=2048 β β
β β [QUEUED in FreeRTOS] β β β β ββ ... (sensor data) β β
β βββββββββββββ¬βββββββββββββ β β β β β
β β β β β [QUEUED in FreeRTOS] β β
β ββββββββββββvβββββββββββββ β β ββββββββββββββ¬βββββββββββββββ β
β β CommandQueue::execute()β β β β β
β β - Dequeue command_t β β β βββββββββββββvβββββββββββββββ β
β β - Call dispatch() β β β β EventQueue::flush() β β
β β - Find handler β β β β - Check stream enabled β β
β β - Call handler(cmd) β β β β - Dequeue all events β β
β βββββββββββββ¬βββββββββββββ β β β - For each event: β β
β β β β β - Create response β β
β Layer 1: Handler generates response β β β - Convert to DR β β
β ββββββββββββvβββββββββββββ β β ββββββββββββββ¬βββββββββββββββ β
β βcommand_response_t β β β β β
β β ββ type=RESPONSE β β β Layer 1: Validation β
β β ββ status=OK β β β βββββββββββββvβββββββββββββββ β
β β ββ payloadβ{...} β β β βevent_response_t β β
β β ββ sent_us=123456 β β β β ββ type=EVENT β β
β β β β β β ββ status=OK β β
β β [from handler] β β β β ββ hit1, hit2, hit3, adc β β
β βββββββββββββ¬βββββββββββββ β β β ββ sent_us=123456 β β
β β β β ββββββββββββββ¬βββββββββββββββ β
β Layer 1βLayer 2 β β β β
β ββββββββββββvβββββββββββββββββββββββββββββ β β Layer 1βLayer 2 β
β β DeviceResponse::from_command() β β β βββββββββββββvβββββββββββββββ β
β β - Copy type, status, error fields β β β β DeviceResponse::from_event() β
β β - Copy payload pointer β β β β - Copy envelope fields β β
β β - Return device_response_t β β β β - Build JsonDocument β β
β βββββββββββββ¬βββββββββββββββββββββββββββββ β β β - Flatten event fields β β
β β β β β - Set payload pointer β β
β ββββββββββββvβββββββββββββ β β ββββββββββββββ¬βββββββββββββββ β
β β device_response_t β β β β β
β β ββ type=RESPONSE β β β βββββββββββββvβββββββββββββββ β
β β ββ status=OK β β β β device_response_t β β
β β ββ sent_us=123456 β β β β ββ type=EVENT β β
β β ββ payloadβ{...} β β β β ββ status=OK β β
β β β β β β ββ payloadβ{ hit1,hit2..}β β
β β [Layer 2: Transport] β β β β ββ sent_us=123456 β β
β βββββββββββββ¬βββββββββββββ β β β β β
β β β β β [Layer 2: Transport] β β
β β β β ββββββββββββββ¬βββββββββββββββ β
β β β β β β
β β Layer 3: Unified Serializationβ β β β
β β βββββββββββββββββββββββββββββββ΄βββ΄ββββββββββββββββ΄βββββββββ β
β β β DeviceResponse::send(device_response_t) β β
β β β - Serialize to JSON Lines format β β
β β β - Merge payload fields with envelope β β
β β β - Output to Serial (115200 baud) β β
β β ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ β
β β β β
β βββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β Serial Output (JSONL, one per line) β
β β β
β β {"type":"response","status":"ok","sent_us":123456, ...} β
β β {"type":"event","status":"ok","sent_us":123457, ...} β
β β {"type":"event","status":"ok","sent_us":123458, ...} β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Learnings¶
1. Architectural Consistency Achieved¶
The v2 protocol successfully implements mirrored architecture for input and output:
- Layer 0 (Domain): Lightweight, structured representation of domain concepts
- Layer 1 (Validation): Typed response with error handling semantics
- Layer 2 (Conversion): Deterministic translation to transport format
- Layer 3 (Serialization): Unified output handler
This 4-layer pattern enables clean separation and testability.
2. Smart Operational Asymmetry¶
While structurally symmetric, the operational flow differs by context:
- CommandQueue: Input from serial β parsing β queueing β execution β output
- EventQueue: Input from sensors β queueing β serialization β output
This asymmetry reflects different operational constraints:
- Serial input requires atomic receive/parse for error recovery
- Sensor output decouples detection from transmission for performance
The key insight: Structural symmetry + Operational asymmetry = Flexibility + Clarity
3. Phase 6 Payload Pointer Pattern Benefits¶
Both command and event responses use the same payload approach:
- Command:
const JsonDocument* payloadincommand_response_t - Event: Fields are flattened but serialized identically
This unifies the serialization layer completely.
4. Stream Control Unification¶
Both paths respect the same flag:
bool stream_enabled = Command::getInstance().get_stream();
This enables runtime control of both command responses and event output via single SET_STREAM command.
5. Memory Efficiency¶
- CommandQueue: 10 items Γ ~65 bytes = ~650 bytes
- EventQueue: 200 items Γ ~160 bytes = ~32 KB
- Total: ~32.65 KB (9.9% of ESP32's 320KB RAM)
- All static allocation (no heap fragmentation)
Design Summary¶
β Strengths¶
- High Structural Symmetry: Type-level mirroring ensures consistency
- Clear Layering: 4-layer architecture with distinct responsibilities
- Flexible Payload Handling: Phase 6 pointer pattern supports any response structure
- Unified Serialization: Single
send()function for both types - Deterministic: No dynamic allocation, predictable behavior
- Smart Asymmetry: Operational differences justified by context
π Considerations¶
- Event Flattening vs. Pointer:
- Commands use pointer (flexible, no type restrictions)
- Events flatten fields (optimized for space in struct)
- Both serialize identically β No user-facing difference
-
Trade-off: Event struct larger but creation simpler
-
enqueue() External API:
- Caller must populate all event fields before queueing
- Enables detection loop control but requires discipline
-
Could add event builder helper if complexity grows
-
Queue Size Asymmetry:
- Commands: 10 items (fast processing)
- Events: 200 items (high-frequency bursts)
- Sizes empirically justified by workload
Next Steps¶
- Documentation: Add flow diagrams to REFACTORING_ROADMAP.md or create design guide
- Code Review: Update code comments to reference this symmetry analysis
- Testing: Verify queue behavior under load (burst events, command backlog)
- Event Builder: Consider adding helper function to construct event_t with sensor reads
- Asymmetry Documentation: Add comments explaining why receive/execute are internal vs. enqueue external