Skip to content
  • 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:

  1. Compare EventQueue and CommandQueue structural design
  2. Verify 4-layer architecture (Layer 0 domain β†’ Layer 1 validation β†’ Layer 2 conversion β†’ Layer 3 serialization)
  3. Identify symmetric vs. asymmetric patterns
  4. 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:

  1. 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
  2. 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_t with 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* payload in command_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

  1. High Structural Symmetry: Type-level mirroring ensures consistency
  2. Clear Layering: 4-layer architecture with distinct responsibilities
  3. Flexible Payload Handling: Phase 6 pointer pattern supports any response structure
  4. Unified Serialization: Single send() function for both types
  5. Deterministic: No dynamic allocation, predictable behavior
  6. Smart Asymmetry: Operational differences justified by context

πŸ” Considerations

  1. Event Flattening vs. Pointer:
  2. Commands use pointer (flexible, no type restrictions)
  3. Events flatten fields (optimized for space in struct)
  4. Both serialize identically β†’ No user-facing difference
  5. Trade-off: Event struct larger but creation simpler

  6. enqueue() External API:

  7. Caller must populate all event fields before queueing
  8. Enables detection loop control but requires discipline
  9. Could add event builder helper if complexity grows

  10. Queue Size Asymmetry:

  11. Commands: 10 items (fast processing)
  12. Events: 200 items (high-frequency bursts)
  13. Sizes empirically justified by workload

Next Steps

  1. Documentation: Add flow diagrams to REFACTORING_ROADMAP.md or create design guide
  2. Code Review: Update code comments to reference this symmetry analysis
  3. Testing: Verify queue behavior under load (burst events, command backlog)
  4. Event Builder: Consider adding helper function to construct event_t with sensor reads
  5. Asymmetry Documentation: Add comments explaining why receive/execute are internal vs. enqueue external