- Date Created: 2025-12-11
- Last Modified: 2025-12-11
Progress Log: EventQueue ↔ CommandQueue Symmetry Analysis¶
Task Description¶
Completed CommandQueue implementation revealed fundamental architectural asymmetry between command and event handling paths:
- CommandQueue follows unified 3-layer pipeline:
- Layer 1 (Handler):
command_response_twith typed fields - Layer 2 (Dispatcher):
device_response_tconversion viafrom_command() -
Layer 3 (Serialization): JSON output via
send() -
EventQueue bypasses Layer 1/2:
- Direct enqueue of
device_response_tenvelope +event_tpayload - Single-stage serialization via
send_event_response_full() - No typed
event_response_tLayer 1 structure
This asymmetry creates maintenance burden:
- Duplicate type definitions (
event_tin both stream_data.h and event_queue.h) - Inconsistent error handling patterns (command validation in Layer 1, event validation scattered)
- Different response construction paths (typed struct vs. envelope+payload tuple)
Outcome¶
Identified 5 major asymmetries:
1. Input Data Types (CRITICAL)¶
CommandQueue: Receives command_t, internally queues after parsing
EventQueue: Receives event_queue_entry_t (pre-assembled by caller)
2. Pipeline Architecture¶
CommandQueue: 4-layer pipeline (Serial → Parse → Queue → Dispatch → Convert → Serialize)
EventQueue: 2-layer pipeline (Enqueue → Serialize) [Skips Layer 1/2 validation]
3. Layer 1 Structure¶
CommandQueue: Layer 1 = command_response_t (typed fields + payload pointer)
EventQueue: No Layer 1 = device_response_t directly (already transport format)
4. Response Construction¶
CommandQueue: Handler builds typed response via success_response()/error_response()
EventQueue: Caller builds event_queue_entry_t (response envelope + event data)
5. Utility Methods¶
CommandQueue: has_pending()
EventQueue: has_pending() missing; instead: get_queued_count(), is_full(), get_overflow_count(), clear()
Learnings¶
Why This Matters¶
- Developer Cognitive Load: Two different patterns for similar buffering systems
- Testing Complexity: Different validation entry points (Layer 1 vs. caller)
- Maintenance Risk: Changes to one require careful consideration of the other
- Evolution Path: If we later add event validation/error handling, where does it live?
Root Cause Analysis¶
EventQueue was designed around high-frequency event streaming use case:
- No command-like validation needed (detector produces valid sensor data)
- No handler dispatch table required
- Minimal processing overhead desired
- Direct serialization sufficient
CommandQueue follows command-request-response pattern:
- Validation critical (user input untrusted)
- Handler dispatch required
- Multiple response types possible
- 3-layer architecture necessary
Design Trade-offs¶
| Aspect | CommandQueue | EventQueue | Impact |
|---|---|---|---|
| Flexibility | High (typed Layer 1) | Low (direct enqueue) | Event validation hard to add |
| Performance | Slight overhead | Optimal (~1-2μs) | Acceptable; validation >> queue overhead |
| Consistency | Strict (3-layer) | Loose (2-layer) | Maintenance burden |
| Memory | Fixed overhead | Minimal | Both acceptable on ESP32 |
Unified Symmetry Proposal¶
Option A: CommandQueue Style (Recommended)¶
Introduce event_response_t Layer 1 struct:
typedef struct {
bool is_ok;
device_response_code_t error_code;
char error_message[256];
event_t event;
uint32_t sent_at;
} event_response_t;
class EventQueue {
// Enqueue Layer 1 response (like CommandQueue)
static bool enqueue(const event_response_t* response);
// execute() method (parallel to CommandQueue::execute())
static bool execute(void);
};
Benefits:
- Perfect symmetry with CommandQueue
- Unified Layer 1 → Layer 2 → Layer 3 path
- Event validation can be added to Layer 1
- Single response construction pattern
- Easier to understand for developers familiar with CommandQueue
Costs:
- Requires refactoring EventQueue enqueue/flush logic
- Slightly more code (~50-100 lines)
- Imperceptible performance impact
Option B: Keep Current Design (Status Quo)¶
Document asymmetry explicitly and maintain separate code paths.
Benefits:
- Zero refactoring
- Minimal code
- Optimal performance maintained
Costs:
- Technical debt accumulates
- Future validation features must work around asymmetry
- Onboarding complexity for new developers
Option C: Hybrid (Middle Ground)¶
Keep EventQueue efficient but add event_response_t type for consistency:
- EventQueue uses simplified validation-free
event_response_t - CommandQueue uses full
command_response_twith error handling - Parallel structures but distinct purposes
Benefits:
- Some symmetry without full refactor
- Validates events at creation, not enqueue
- Partial developer alignment
Costs:
- Still two patterns
- Incomplete solution
Next Steps¶
-
Decision Required: Choose Option A, B, or C above
-
If Option A:
- Design event validation layer (where should event_response_t be created?)
- Refactor EventQueue::enqueue() signature
- Add EventQueue::execute() parallel to CommandQueue::execute()
- Update main.cpp call pattern
-
Add unit tests for both queues
-
If Option B:
- Document asymmetry in CLAUDE.md (Implementation Details section)
- Add architectural rationale comments to event_queue.h/cpp
-
Mark as "intentional design" not a bug
-
If Option C:
- Define lightweight event_response_t
- Update event creation caller pattern
- Document dual-response-type system
Recommendation¶
Option A (CommandQueue Style) is recommended because:
- Future-proofs event validation additions
- Single mental model for all queue operations
- Enables consistent error handling across device
- Aligns with unified device_response_t philosophy
- Maintenance burden lighter long-term despite refactor costs
The ~50-100 lines of refactoring is negligible compared to lifetime maintenance benefit.
Processing Flow Comparison: CommandQueue vs EventQueue (Option A)¶
CommandQueue: Complete 4-Layer Processing Pipeline¶
Serial Input → Receive → Parse → Queue → Dispatch → Layer 1 Convert → Layer 2 Convert → Layer 3 Serialize → Serial Output
Detailed Flow:
| # | Stage | Function | Input Type | Output Type | Description |
|---|---|---|---|---|---|
| 1 | Reception | CommandQueue::receive() |
Serial bytes (char[]) |
command_t (parsed) |
Reads line from serial (timeout 500ms), validates length, null-terminates |
| 2 | Parsing | command_parse() (internal) |
char[] (raw command line) |
command_t (structured) |
Splits "SET_THRESHOLD 1 512" → name="SET_THRESHOLD", args[0]="1", args[1]="512" |
| 3 | Queuing | CommandQueue::enqueue() (implicit via receive) |
command_t |
FreeRTOS Queue | Stores to static FreeRTOS queue (10-item buffer, 650 bytes) |
| 4 | Dequeuing | CommandQueue::execute() |
FreeRTOS Queue | command_t (dequeued) |
Retrieves next command from queue |
| 5 | Dispatch | CommandQueue::dispatch() (internal) |
command_t |
command_response_t (Layer 1) |
Looks up command in dispatch table, calls handler function (e.g., handle_set_threshold()) |
| 6 | Layer 1 Response | handle_*() (handler function) |
command_t (validated args) |
command_response_t (typed fields) |
Handler executes business logic, returns typed Layer 1 response (e.g., status, threshold value, error code) |
| 7 | Layer 2 Conversion | DeviceResponse::from_command() |
command_response_t (Layer 1) |
device_response_t (Layer 2) |
Wraps Layer 1 in transport envelope: type, status, sent_at, error_code, payload pointer |
| 8 | Layer 3 Serialization | DeviceResponse::send() |
device_response_t (Layer 2) |
JSON string | Merges payload fields with envelope, serializes to JSONL: {"type":"response","status":"ok",...payload...} |
| 9 | Transmission | Serial.println() |
JSON string | Serial port | Writes complete JSON line to serial (115200 baud) |
Total Processing Path: 9 sequential stages
Queue Depth: 1 item (receives then dequeues within same execute() call)
Responsibility Model:
- Layer 1 (Handler): Validation, business logic, typed response
- Layer 2 (Converter): Transport envelope wrapping
- Layer 3 (Serializer): JSON formatting
EventQueue: Current 2-Layer Pipeline (BEFORE Option A)¶
Detection → Sensor Read → Build event_t → Convert to device_response_t → Queue → Flush → Serialize → Serial Output
Current Flow:
| # | Stage | Function | Input Type | Output Type | Description |
|---|---|---|---|---|---|
| 1 | Detection | cosmic_detector_read() |
GPIO pin state | cosmic_detection_t (detected flag) |
Polls detection pins GPIO12/19/27 (100x per cycle) |
| 2 | Sensor Read | bme280_read(), adc_read(), rtc_read() |
I2C/ADC/RTC buses | Sensor values (temp, pressure, humidity, adc, timestamp) | Reads environmental sensors only if detected=true |
| 3 | Event Build | (inline in main.cpp) | Sensor values | event_t (struct) |
Populates hit1, hit2, hit3, adc, temp, pressure, humidity, timestamp fields |
| 4 | Layer 1 Response | event_to_device_response() |
event_t (raw data) |
device_response_t (envelope only, no Layer 1!) |
Bypasses Layer 1 validation - directly creates Layer 2 envelope |
| 5 | Queue Assembly | (inline in main.cpp) | device_response_t + event_t |
event_queue_entry_t (tuple) |
Manually assembles queue entry (envelope + payload) before enqueue |
| 6 | Queuing | EventQueue::enqueue(event_queue_entry_t*) |
event_queue_entry_t |
FreeRTOS Queue | Stores to static FreeRTOS queue (200-item buffer, 50 KB) |
| 7 | Flushing | EventQueue::flush() |
FreeRTOS Queue (all items) | Drained queue | Dequeues ALL pending events in tight loop |
| 8 | Layer 3 Serialization | send_event_response_full(entry) |
event_queue_entry_t |
JSON string | Serializes: type, status, hit1, hit2, hit3, adc, optional fields |
| 9 | Transmission | Serial.println() |
JSON string | Serial port | Writes complete JSON line to serial (115200 baud) |
Issues with Current Flow:
- ❌ Missing Layer 1 (event_response_t) - no validation/error structure
- ❌ Queue carries both envelope AND payload (inefficient)
- ❌ Asymmetric with CommandQueue pattern
- ❌ Main.cpp responsible for envelope + payload assembly (caller burden)
EventQueue: Target 4-Layer Pipeline (AFTER Option A)¶
Detection → Sensor Read → Build event_t → Layer 1 Response → Queue → Execute → Layer 2 Convert → Layer 3 Serialize → Serial Output
Target Flow (Symmetric with CommandQueue):
| # | Stage | Function | Input Type | Output Type | Description |
|---|---|---|---|---|---|
| 1 | Detection | cosmic_detector_read() |
GPIO pin state | cosmic_detection_t (detected flag) |
Polls detection pins GPIO12/19/27 (100x per cycle) |
| 2 | Sensor Read | bme280_read(), adc_read(), rtc_read() |
I2C/ADC/RTC buses | Sensor values (temp, pressure, humidity, adc, timestamp) | Reads environmental sensors only if detected=true |
| 3 | Event Build | (inline in main.cpp) | Sensor values | event_t (struct) |
Populates hit1, hit2, hit3, adc, temp, pressure, humidity, timestamp fields |
| 4 | Layer 1 Response | event_response_ok(&event) |
event_t (raw data) |
event_response_t (Layer 1 with validation) |
NEW: Creates Layer 1 response with is_ok=true, error_code=OK, sent_at=timestamp |
| 5 | Queuing | EventQueue::enqueue(&event_resp) |
event_response_t (Layer 1 only) |
FreeRTOS Queue | Stores Layer 1 response to static FreeRTOS queue (200-item buffer, 30 KB) |
| 6 | Dequeuing/Execute | EventQueue::execute() |
FreeRTOS Queue | event_response_t (dequeued) |
NEW: Dequeues one event_response_t and processes it (parallel to CommandQueue::execute()) |
| 7 | Layer 2 Conversion | DeviceResponse::from_event(&event_resp) |
event_response_t (Layer 1) |
device_response_t (Layer 2) |
NEW: Wraps Layer 1 in transport envelope: type=DEVICE_TYPE_EVENT, status based on is_ok, sent_at |
| 8 | Layer 3 Serialization | DeviceResponse::send(&response) |
device_response_t (Layer 2 with payload pointer) |
JSON string | UNIFIED: Merges Layer 2 envelope with event payload fields via pointer, serializes to JSONL |
| 9 | Transmission | Serial.println() |
JSON string | Serial port | Writes complete JSON line to serial (115200 baud) |
Benefits of Target Flow:
- ✅ Symmetric with CommandQueue: Same 4-stage pipeline (Reception→Parsing→Queueing→Dispatching) maps to (Detection→Sensor Read→Queuing→Execute)
- ✅ Complete 3-layer architecture: Layer 1 (Domain) → Layer 2 (Transport) → Layer 3 (Serialization)
- ✅ Queue carries only Layer 1 (lighter, 20 KB savings)
- ✅ Main.cpp simplified: Just call
event_response_ok()+enqueue()(caller obligation reduced) - ✅ Validation layer exists for future error handling
- ✅ Single mental model for all queue operations
Comparison Matrix: CommandQueue vs EventQueue (Target)¶
| Aspect | CommandQueue (Current) | EventQueue (Target) | Status |
|---|---|---|---|
| Input Source | Serial: char[] line |
Detection: event_t struct |
Different sources, same pattern |
| Stage 1 | receive() + parse() → command_t |
Sensor read + build → event_t |
Functionally equivalent |
| Stage 2 | enqueue() → FreeRTOS queue |
event_response_ok() → Layer 1 struct |
Different structures, same Layer 1 role |
| Stage 3 | execute() → dequeue + dispatch |
execute() → dequeue + convert |
SAME METHOD NAME ✅ |
| Layer 1 Type | command_response_t |
event_response_t |
Parallel structures ✅ |
| Layer 1 Builder | success_response() / error_response() |
event_response_ok() / event_response_error() |
Parallel helpers ✅ |
| Layer 2 Conversion | DeviceResponse::from_command() |
DeviceResponse::from_event() |
Parallel converters ✅ |
| Layer 3 Serialization | DeviceResponse::send() |
DeviceResponse::send() |
UNIFIED ✅ |
| Queue Item Type | command_t |
event_response_t |
Both Layer 1 ✅ |
| Queue Size | 10 items, 650 bytes | 200 items, 30 KB | EventQueue larger (high-frequency bursts) |
| Asymmetry | N/A (CommandQueue is reference) | 🟡 RESOLVED in Target | Design goal met ✅ |
Processing Flow Diagram¶
BEFORE (Asymmetric):
┌─────────────────────────────────────────────────────────────┐
│ COMMAND PATH (4 layers) │
├─────────────────────────────────────────────────────────────┤
│ Serial → Receive → Parse → Queue → Dispatch → Layer1 → Layer2 → Layer3 → JSON
│ (command_t) (handler) (typed) (envelope) (serialize)
└─────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ EVENT PATH (2 layers) ❌ │
├──────────────────────────────────────────────────────┤
│ Detect → Read → Build → Queue → Serialize → JSON
│ (event_t) (envelope+payload) (no Layer1!)
└──────────────────────────────────────────────────────┘
ASYMMETRIC ❌
AFTER (Option A - Symmetric):
┌──────────────────────────────────────────────────────────────┐
│ COMMAND PATH (4 layers) │
├──────────────────────────────────────────────────────────────┤
│ Serial → Receive → Parse → Queue → Execute → Layer1 → Layer2 → Layer3 → JSON
│ (command_t) (dispatch) (typed) (envelope) (serialize)
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ EVENT PATH (4 layers) ✅ │
├──────────────────────────────────────────────────────────────┤
│ Detect → Read → Build → Layer1 → Queue → Execute → Layer2 → Layer3 → JSON
│ (event_t) (event_response_t) (convert) (envelope) (serialize)
└──────────────────────────────────────────────────────────────┘
SYMMETRIC ✅
Implementation Path: Option A (CommandQueue Style)¶
Data Flow Target¶
event_t (detection)
↓
EventQueue::enqueue(event_response_t)
↓ (Layer 2 conversion)
DeviceResponse::from_event(event_response_t)
↓ (Layer 2 envelope)
device_response_t
↓ (Layer 3 serialization)
send_event_response() → JSON output
Include Dependencies¶
event_queue.h¶
Add to includes section:
#include "config.h"
#if ENABLE_DEVICE_RESPONSE
#include "device_response_types.h" // For device_response_code_t enum
#endif
Rationale: event_response_t struct uses device_response_code_t enum.
Follows same pattern as command_queue.h for circular dependency avoidance.
Separates types (device_response_types.h) from implementations to prevent circular includes:
event_queue.h→ includesdevice_response_types.h(types only)device_response.h→ includesevent_queue.h(safe, no circular dependency)
event_response.h¶
Add to includes section:
#include <ArduinoJson.h>
#include "device_response_types.h" // For enums (DEVICE_TYPE_EVENT, etc.)
#include "event_queue.h" // For event_response_t type
#include "device_response.h" // For device_response_t type
Rationale: Needs types from device_response_types.h before event_queue.h is included.
Type Definitions (Additions)¶
1. event_response_t (NEW Layer 1 type)¶
Add to event_queue.h:
/**
* @brief Event response (Layer 1 - domain layer)
*
* Represents detection event with validation, error handling, and metadata.
* Symmetric with command_response_t for unified 3-layer architecture.
*
* Data flow:
* event_t → event_response_t (Layer 1) → device_response_t (Layer 2) → JSON (Layer 3)
*/
typedef struct {
/**
* Validation status: true for valid event, false for error
* Set to false if event_t data fails validation
*/
bool is_ok;
/**
* Error code (present when is_ok=false)
* Maps to device_response_code_t (1-5)
*/
device_response_code_t error_code;
/**
* Error message (present when is_ok=false, max 255 chars)
* Human-readable description of validation failure
*/
char error_message[256];
/**
* Raw detection event data (timestamp captured at detection time)
* Only valid when is_ok=true
*/
event_t event;
/**
* Timestamp of event capture (seconds)
* Unix time if ENABLE_RTC=1, or device uptime if ENABLE_RTC=0
* Captured at detection moment, not at serialization time
*/
uint32_t sent_at;
} event_response_t;
Method Signatures¶
NEW Methods (to add to EventQueue class)¶
| Method | Signature | Purpose | Location |
|---|---|---|---|
has_pending() |
static bool has_pending(void) |
Check if events awaiting execution (parallel to CommandQueue) | event_queue.h line ~XXX |
execute() |
static bool execute(void) |
Dequeue and execute (process one event) | event_queue.h line ~XXX |
MODIFIED Methods (signature changes required)¶
| Old Signature | New Signature | Change | Rationale |
|---|---|---|---|
enqueue(event_queue_entry_t*) |
enqueue(event_response_t*) |
Accept Layer 1, not pre-assembled Layer 2 | Symmetric with CommandQueue::receive() pattern |
flush() |
(delete) | Remove 2-layer draining | Replaced by execute() pattern |
DELETED Methods¶
| Method | Reason |
|---|---|
get_queued_count() |
Use has_pending() consistency check instead |
is_full() |
Not needed (non-blocking enqueue returns false) |
get_overflow_count() |
Can be added later if needed for monitoring |
clear() |
Use internal queue reset, not public API |
flush() |
Replaced by execute() for single-item processing |
Internal Variables (Implementation)¶
NEW Static Variables (in event_queue.cpp)¶
/**
* @brief Queue item counter for Layer 1 responses (event_response_t items)
* Maps to: EventQueue::has_pending(), EventQueue::execute()
*/
static uint8_t g_event_queue_pending_count;
/**
* @brief Single-item dequeue buffer for Layer 1 responses
* Holds event_response_t for execute() processing
*/
static event_response_t g_event_dequeue_buffer;
MODIFIED Static Variables¶
| Variable | Old Type | New Type | Change | Reason |
|---|---|---|---|---|
g_event_queue_buffer |
event_queue_entry_t[] |
event_response_t[] |
Item type changes from Layer 2 to Layer 1 | Deferred envelope assembly until Layer 2 |
g_event_queue_size |
sizeof(event_queue_entry_t) * 200 |
sizeof(event_response_t) * 200 |
Size calculation updates | Layer 1 struct is smaller (no device_response_t overhead) |
Function Implementations¶
NEW Functions (add to event_queue.cpp)¶
/**
* @brief Create success event response (Layer 1 helper)
* Parallel to CommandQueue's success_response()
*/
inline event_response_t event_response_ok(const event_t* event) {
event_response_t resp = {};
resp.is_ok = true;
resp.error_code = DEVICE_CODE_OK;
resp.sent_at = device_get_timestamp();
if (event) {
memcpy(&resp.event, event, sizeof(event_t));
}
return resp;
}
/**
* @brief Create error event response (Layer 1 helper)
* Parallel to CommandQueue's error_response()
*/
inline event_response_t event_response_error(
device_response_code_t code, const char* message) {
event_response_t resp = {};
resp.is_ok = false;
resp.error_code = code;
resp.sent_at = device_get_timestamp();
if (message) {
strncpy(resp.error_message, message, sizeof(resp.error_message) - 1);
resp.error_message[sizeof(resp.error_message) - 1] = '\0';
}
return resp;
}
REFACTORED Functions (signature change)¶
| Function | Old | New | Change |
|---|---|---|---|
EventQueue::enqueue() |
(event_queue_entry_t*) |
(event_response_t*) |
Accept Layer 1 typed response |
EventQueue::execute() |
N/A (new) | Dequeue → dispatch (not needed) → Layer 2 convert → Layer 3 serialize | Process one event |
DELETED Functions (remove from event_queue.cpp)¶
| Function | Reason |
|---|---|
flush() |
Replaced by execute() (process one event per call, not drain all) |
get_queued_count() |
Use has_pending() instead |
is_full() |
Check return value of enqueue() |
get_overflow_count() |
Remove monitoring API (can add as DEBUG build only later) |
clear() |
Make internal only (not public API) |
Conversion Functions¶
NEW: DeviceResponse::from_event() (Layer 2 converter)¶
Add to device_response.h:
/**
* @brief Convert event_response_t (Layer 1) to device_response_t (Layer 2)
*
* Layer 2 converter: Bridges Layer 1 validation results to Layer 2 transport envelope.
* Parallel to DeviceResponse::from_command().
*
* Processing:
* - Sets device_response_t.type = DEVICE_TYPE_EVENT
* - Sets status based on event_response_t.is_ok
* - Copies error_code and error_message if present
* - Copies sent_at timestamp
* - Event payload stored separately (passed to send_event_response_full)
*
* @param event_resp Layer 1 event response
* @return device_response_t Transport envelope ready for serialization
*/
inline device_response_t DeviceResponse::from_event(const event_response_t* event_resp) {
device_response_t resp = {};
resp.type = DEVICE_TYPE_EVENT;
resp.status = event_resp->is_ok ? DEVICE_STATUS_OK : DEVICE_STATUS_ERROR;
resp.error_code = event_resp->error_code;
resp.sent_at = event_resp->sent_at;
if (!event_resp->is_ok && event_resp->error_message[0]) {
strncpy(resp.error_message, event_resp->error_message,
sizeof(resp.error_message) - 1);
resp.error_message[sizeof(resp.error_message) - 1] = '\0';
}
return resp;
}
UNIFIED Layer 3 Serializer: DeviceResponse::send() (Complete Symmetry - Option 1)¶
For complete Layer 3 symmetry, both CommandQueue and EventQueue use the same unified DeviceResponse::send() function.
CURRENT Implementation (Separate Serializers):
// CommandQueue path: Uses unified DeviceResponse::send()
// in device_response.cpp
void DeviceResponse::send(const device_response_t* response) {
JsonDocument doc;
doc["type"] = (response->type == DEVICE_TYPE_RESPONSE) ? "response" : "event";
doc["status"] = (response->status == DEVICE_STATUS_OK) ? "ok" : "error";
doc["sent_at"] = response->sent_at;
if (response->payload) {
for (JsonPairConst pair : JsonObjectConst(response->payload->as<JsonObject>())) {
doc[pair.key()] = pair.value();
}
}
serializeJson(doc, Serial);
Serial.println();
}
// EventQueue path: Uses separate send_event_response_full()
// in event_response.cpp
void send_event_response_full(const event_queue_entry_t* entry) {
JsonDocument doc;
doc["type"] = "event";
doc["status"] = (entry->response.status == DEVICE_STATUS_OK) ? "ok" : "error";
doc["sent_at"] = entry->response.sent_at;
// ... manually populate hit1, hit2, hit3, adc, optional fields ...
serializeJson(doc, Serial);
Serial.println();
}
// ❌ PROBLEM: Two separate paths for similar work
IMPROVED Implementation (Unified Serializer):
// BOTH CommandQueue AND EventQueue use unified DeviceResponse::send()
// in device_response.cpp
void DeviceResponse::send(const device_response_t* response) {
JsonDocument doc;
// Common envelope (all message types)
doc["type"] = (response->type == DEVICE_TYPE_EVENT) ? "event" : "response";
doc["status"] = (response->status == DEVICE_STATUS_OK) ? "ok" : "error";
doc["sent_at"] = response->sent_at;
// Error fields (if present)
if (response->status == DEVICE_STATUS_ERROR) {
doc["error_code"] = response->error_code;
doc["error_message"] = response->error_message;
}
// Payload fields (merged from response->payload pointer)
// Works for BOTH command responses and event responses
if (response->payload) {
for (JsonPairConst pair : JsonObjectConst(response->payload->as<JsonObject>())) {
doc[pair.key()] = pair.value();
}
}
// Serialize and transmit
serializeJson(doc, Serial);
Serial.println();
}
// ✅ SOLUTION: Single path for all message types
EventQueue::execute() Implementation (Layer 2 → Layer 3):
bool EventQueue::execute(void) {
if (!has_pending()) return false;
// Layer 1: Dequeue event_response_t
event_response_t event_resp = g_event_dequeue_buffer; // already dequeued
// Layer 2: Convert to device_response_t
device_response_t response = DeviceResponse::from_event(&event_resp);
// Layer 3: Populate payload and serialize
// Create static JsonDocument with event payload fields
static JsonDocument event_payload;
event_payload.clear();
event_payload["hit1"] = event_resp.event.hit1;
event_payload["hit2"] = event_resp.event.hit2;
event_payload["hit3"] = event_resp.event.hit3;
event_payload["adc"] = event_resp.event.adc;
#if ENABLE_HITTYPE
event_payload["hit_type"] = event_resp.event.hit_type;
#endif
#if ENABLE_BME280
event_payload["tmp_c"] = event_resp.event.tmp_c;
event_payload["atm_pa"] = event_resp.event.atm_pa;
event_payload["hmd_pct"] = event_resp.event.hmd_pct;
#endif
// ... other optional fields ...
// Set payload pointer in Layer 2 envelope
response.payload = &event_payload;
// Layer 3: Use unified serializer
DeviceResponse::send(&response);
return true;
}
Benefits of Unified Layer 3 (Option 1):
- ✅ One serialization function for all message types
- ✅ Consistent JSON output format (commands + events)
- ✅ Easier to maintain and test
- ✅ Perfect architectural symmetry
- ✅ Extensible to future message types
- ✅ Remove separate
send_event_response_full()function (code consolidation)
Main Loop Integration¶
BEFORE (current 2-layer pattern)¶
void loop() {
// Detection
if (cosmic_read().detected) {
event_t event = build_event_from_sensors();
device_response_t resp = event_to_device_response(&event);
// Queue carries: device_response_t + event_t as event_queue_entry_t
EventQueue::enqueue(&event_queue_entry);
}
// Transmission (drains all)
EventQueue::flush();
}
AFTER (new 4-layer pattern)¶
void loop() {
// Detection: Create Layer 1 response
if (cosmic_read().detected) {
event_t event = build_event_from_sensors();
event_response_t event_resp = event_response_ok(&event);
// Queue carries: event_response_t only
EventQueue::enqueue(&event_resp);
}
// Transmission: Process one event per loop iteration
EventQueue::execute();
}
File Changes Summary¶
| File | Changes | Type |
|---|---|---|
| include/event_queue.h | Add event_response_t typedef, new has_pending(), new execute(), modify enqueue() sig, remove flush(), get_queued_count(), is_full(), get_overflow_count(), clear() |
Header |
| src/event_queue.cpp | Rewrite queue mechanics (Layer 1 items), add has_pending(), add execute(), refactor enqueue(), delete flush(), delete utility methods, update buffer types |
Implementation |
| include/event_response.h | Add event_response_ok(), event_response_error() helpers, DELETE send_event_response_full() signature (replaced by unified DeviceResponse::send()) |
Header |
| src/event_response.cpp | Implement new helpers, DELETE send_event_response_full() implementation (replaced by unified DeviceResponse::send()) |
Implementation |
| include/device_response.h | Add DeviceResponse::from_event() converter (parallel to from_command()) |
Header |
| src/device_response.cpp | Implement from_event() converter, verify DeviceResponse::send() already handles both command and event types |
Implementation |
| src/main.cpp | Update detection loop: event_response_ok() + enqueue(), replace flush() with execute() |
Implementation |
Memory Impact Analysis¶
| Metric | Before (2-layer) | After (4-layer) | Delta | Note |
|---|---|---|---|---|
| Queue item size | event_queue_entry_t (~250B) |
event_response_t (~150B) |
-100B | No Layer 2 in queue |
| Queue total (200 items) | 50 KB | 30 KB | -20 KB | Significant savings |
| Stack (enqueue) | ~250B temporary | ~150B temporary | -100B | Smaller copy |
| Heap | 0 | 0 | 0 | No change |
| Flash (code) | ~2 KB | ~3 KB | +1 KB | New helpers, converters |
Conclusion: 20 KB RAM savings, 1 KB code growth = net +19 KB available RAM
Validation Checkpoints¶
After each phase, verify:
- Phase 1: Type definitions compile without errors
- Phase 2: Helper functions (event_response_ok/error) callable
- Phase 3: Queue accepts event_response_t items
- Phase 4: Conversion DeviceResponse::from_event() produces correct device_response_t
- Phase 5: Serialization send_event_response_full() outputs complete JSON
- Phase 6: Main loop integrates without compilation errors
- Phase 7: Runtime test detects cosmic rays → queue → output → JSON validation
- Phase 8: Symmetry check: CommandQueue and EventQueue patterns identical