Skip to content

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

  1. Single Responsibility: EventQueue handles queuing, DeviceResponse handles conversion
  2. Testability: from_event() can be unit tested in isolation
  3. Maintainability: Payload structure changes only affect DeviceResponse, not EventQueue
  4. Consistency: All Layer 1→2 transformations go through DeviceResponse methods
  5. 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()