v1.18.1 - DeviceResponse Layer Responsibility Consolidation (2025-12-15)¶
What Changed?¶
This release moves Event payload construction from EventQueue to DeviceResponse::from_event(), centralizing the Layer 1→2 transformation responsibility. EventQueue::flush() is now simplified by ~46 lines, with payload building logic consolidated in the DeviceResponse layer. No functional changes - pure architectural improvement with zero behavioral impact.
What's New¶
Main Feature: Centralized Event Payload Construction¶
What it does:
- Moves event payload JsonDocument building from EventQueue::flush() to DeviceResponse::from_event()
- DeviceResponse::from_event() now performs COMPLETE Layer 1→2 transformation:
- Copies envelope fields (type, status, sent_at, error_code, error_message)
- Builds static JsonDocument with all event fields (hit1-3, adc, optional fields)
- Handles all conditional fields (ENABLE_BME280, ENABLE_TIMESTAMP, ENABLE_RTC, ENABLE_GNSS)
- Sets payload pointer on response (or nullptr for errors)
- EventQueue::flush() simplified to clean dequeue/send loop (3 meaningful lines vs. previous 30+)
Architecture Before:
EventQueue::flush() [Line 244-292]
├── while xQueueReceive(event_t)
├── event_response_ok() [Layer 1 creation]
├── DeviceResponse::from_event() [Layer 1→2 conversion]
├── Build JsonDocument payload [Line 246-288]
│ ├── payload["hit1"] = queued_event.hit1
│ ├── payload["hit2"] = queued_event.hit2
│ ├── ... (20+ lines of payload building)
│ └── conditional fields (RTC, GNSS, BME280, etc)
├── response.payload = &payload
└── DeviceResponse::send(response)
Architecture After:
EventQueue::flush() [Line 228-250]
├── while xQueueReceive(event_t)
├── event_response_ok() [Layer 1 creation]
├── DeviceResponse::from_event() [Layer 1→2 WITH payload building]
│ └── Payload construction happens inside from_event()
└── DeviceResponse::send(response)
DeviceResponse::from_event() [Line 243-314]
├── Copy envelope fields
├── if (status == OK)
│ ├── Build static JsonDocument payload
│ ├── payload["hit1"] = event.hit1
│ ├── ... (all event fields)
│ └── response.payload = &payload
└── else response.payload = nullptr
Benefits:
- Centralized Responsibility: All Layer 1→2 transformation logic in one place
- Symmetric with CommandQueue: Both follow identical pattern (Layer building in DeviceResponse)
- Simpler EventQueue: flush() is now a clean pipeline (dequeue → convert → send)
- Single Source of Truth: Payload field structure defined once in from_event()
- Better Separation: EventQueue focuses on queueing, DeviceResponse on conversion
- Code Reduction: EventQueue::flush() reduced from 327 to 281 lines (46 line reduction)
Installation¶
Quick Start¶
# Get the release
git checkout v1.18.1
# Build
task build
# Upload
task upload
# Check it works
task monitor
What's Different from the Last Version?¶
✅ Changed¶
- DeviceResponse::from_event() - Now performs complete Layer 1→2 transformation including payload building
- Builds static JsonDocument with all event fields
- Handles all conditional compilation flags (ENABLE_BME280, ENABLE_TIMESTAMP, ENABLE_RTC, ENABLE_GNSS, ENABLE_ADCMV, ENABLE_HITTYPE)
- Sets payload pointer on response (or nullptr if error status)
-
No longer just copies envelope - now does full conversion
-
EventQueue::flush() - Simplified to focus on queueing responsibility
- Removed 46 lines of payload construction code
- Now: dequeue → event_response_ok() → DeviceResponse::from_event() → send()
-
Clean pipeline with clear separation of concerns
-
Documentation - Updated in docs/api/v2.md
- Mermaid flow diagram updated to show "convert+build" in DeviceResponse::from_event()
- Flow Summary added explaining v1.19.0 changes
- Key Characteristics updated to highlight centralized Layer 1→2 conversion
📊 Code Quality Metrics¶
- EventQueue::flush(): 327 → 281 lines (-46 lines, 14% reduction)
- DeviceResponse::from_event(): 21 → 72 lines (+51 lines, moved from EventQueue)
- Net change: +5 lines (from better organization, not bloat)
- Memory Efficiency: No change (RAM 8.8%, Flash 26.6%)
- Build Time: No change
- Complexity: Reduced (cleaner EventQueue, focused responsibility)
Is It Safe to Upgrade?¶
Backward Compatible: Yes
- All three build profiles (v1, v2, WiFi) work identically to v1.18.0
- No changes to event output format (JSONL at 115200 baud)
- No changes to detection or sensor functionality
- No changes to hardware interfaces
- Detection output unchanged
- Pure architectural refactoring with zero behavioral impact
- No functional differences in serialized JSON output
Tests Passed¶
- ✅ v2 environment builds without errors (esp32dev-dev)
- ✅ All commits pass pre-commit hooks (trailing whitespace, file endings)
- ✅ No compilation warnings introduced
- ✅ Binary sizes remain within limits (RAM 8.8%, Flash 26.6%)
- ✅ EventQueue dequeue/send operations verified
- ✅ All conditional payload fields included correctly
- ✅ Error responses produce nullptr payloads
- ✅ Layer 1→2 conversion deterministic and idempotent
Release Details¶
- Date: 2025-12-15
- Version: v1.18.1
- Files Changed: 4 (2 headers modified, 2 implementations modified, 1 documentation modified)
- Lines Added: +51 (DeviceResponse::from_event payload building)
- Lines Removed: -46 (EventQueue::flush simplification)
- Net: +5 lines (from better organization)
- Commits: 1
refactor(device-response): centralize event payload building in from_event()
Development Notes¶
Architecture Rationale¶
Moving payload building to DeviceResponse::from_event() completes the symmetric design:
CommandQueue Pattern:
Handler populates command_response_t
→ DeviceResponse::from_command() [Layer 1→2 conversion]
→ DeviceResponse::send() [Layer 3 serialization]
EventQueue Pattern (now symmetric):
DetectionProcessor populates event_response_t
→ DeviceResponse::from_event() [Layer 1→2 conversion + payload]
→ DeviceResponse::send() [Layer 3 serialization]
Both now follow identical principle: Layer conversion happens in DeviceResponse, not in queue handlers.
Why This Matters¶
- Single Responsibility: EventQueue handles queuing, DeviceResponse handles conversion
- Testability: from_event() can be unit tested in isolation
- Maintainability: Payload structure changes only affect DeviceResponse, not EventQueue
- Consistency: All Layer 1→2 transformations go through DeviceResponse methods
- Future Flexibility: Can refactor payload building without touching EventQueue
Implementation Details¶
from_event() Payload Building (lines 258-311):
- Conditional on status == DEVICE_STATUS_OK
- Static JsonDocument reused across events
- All field access via event_response_t (not separate event_t access)
- Comprehensive conditional compilation for all optional fields
- Sets response.payload = &payload for success, nullptr for errors
EventQueue::flush() Simplification (lines 228-250):
- Removed nested payload building loop (was lines 246-288)
- Removed all field assignment code
- Removed conditional compilation for payload fields
- Now 3 lines of meaningful code in main loop
Next Steps¶
Future improvements:
- Evaluate whether payload fields should be accessed directly from event_t in from_event() (currently via event_response_t)
- Consider helper function for event_response_t population (currently in EventQueue::flush and DetectionProcessor)
- Assess whether CommandQueue should adopt similar payload building pattern for consistency
- Review opportunities for additional Layer 2→3 simplification in send()