Skip to content
  • Date Created: 2025-12-09
  • Last Modified: 2025-12-09

Progress Log: CommandQueue Class Design (Phase 2)

Task Description

Following successful v1.13.0 Command class implementation, design Phase 2: CommandQueue class for unified command reception, parsing, and queuing - replacing fragmented text_command.cpp functionality.

Current State Analysis:

  • Text command reception and parsing scattered across text_command.cpp (7 functions)
  • Serial reception: text_receive_line(), timeout handling, overflow detection
  • Queueing: FreeRTOS Queue with 10-item capacity
  • Parsing: text_parse() with alias resolution, character validation
  • Dispatch: text_dispatch() with command table lookup
  • No unified interface: main.cpp must call 3 separate functions per cycle

Design Goals:

  1. Unified Reception Pipeline: Single class handles serial I/O → parse → queue
  2. Simplified Interface: main.cpp calls only command_queue_receive() and command_queue_execute()
  3. Type-Safe Commands: Leverage Command class for argument validation
  4. Foundation for Handlers: Standardized command format for all handlers
  5. Integration Ready: Works seamlessly with Command class for configuration updates

Proposed Architecture

CommandQueue Class Structure

class CommandQueue {
private:
  // Queue storage (FreeRTOS static allocation)
  static QueueHandle_t g_queue;
  static StaticQueue_t g_queue_storage;
  static uint8_t g_queue_buffer[COMMAND_QUEUE_SIZE * sizeof(command_t)];

  // Helper methods
  static bool receive_line(char* buffer, size_t buffer_size);
  static command_t parse(const char* line);
  static void discard_input(void);

public:
  // Initialization
  static void init(void);

  // Reception and queueing (main entry point)
  static bool receive(void);

  // Query and execution
  static bool has_pending(void) const;
  static bool execute(void);
};

Data Structure: command_t

Simplified and optimized compared to current text_command_t:

typedef struct {
  char name[32];              // Command name (uppercase, null-terminated)
  uint8_t arg_count;          // Number of arguments (0-2)
  char args[2][16];           // Argument strings (null-terminated)
} command_t;

Improvements over text_command_t: - Removed raw_buffer[64] (error context not needed - errors handled at reception) - Kept essential fields: command_name, arg_count, args - More memory-efficient for queue storage

Handler Signature Standardization

Current (in text_command_manager.cpp):

typedef command_response_t (*command_handler_t)(text_command_t cmd);

// Handler validation logic duplicated in each handler
command_response_t handle_set_poll_count(text_command_t cmd) {
  if (cmd.arg_count != 1) { return error; }
  uint16_t count = atoi(cmd.args[0]);
  if (count < 1 || count > 65535) { return error; }
  // ... set and execute
}

Proposed (CommandQueue with type-safe parsing):

typedef command_response_t (*command_handler_t)(const command_t& cmd);

// Validation moved to CommandQueue argument parsing
// Handler receives pre-validated values
command_response_t handle_set_poll_count(const command_t& cmd) {
  // args[0] guaranteed to be valid uint16_t [1, 65535]
  uint16_t count = atoi(cmd.args[0]);  // Safe - already validated
  Command::getInstance().set_poll_count(count);
  return device_response_ok(RESPONSE_TYPE_OK);
}

Integration with Existing Architecture

Current Flow (3 separate calls):

main.cpp loop():
  ├─ text_receive()        // Serial → parse → queue
  ├─ text_execute()        // Dequeue → dispatch → respond
  └─ (repeat)

CommandQueue Flow (2 calls, unified):

main.cpp loop():
  ├─ CommandQueue::receive()   // Serial → parse → queue
  ├─ CommandQueue::execute()   // Dequeue → dispatch → respond
  └─ (repeat)

Compilation Strategy

When ENABLE_DEVICE_RESPONSE=1 (new path):

#if ENABLE_DEVICE_RESPONSE
  // Phase 2: CommandQueue + Command class
  // New implementation in command_queue.h/cpp
  // Handlers in text_command_manager.cpp (adapted)
#else
  // Legacy path: text_command.cpp
  // Original text_receive(), text_parse(), text_execute()
  // Unchanged and fully backward compatible
#endif

Key Point: CommandQueue is conditional on ENABLE_DEVICE_RESPONSE=1, same as Command class. They form an integrated subsystem.

Implementation Details

Reception Flow

  1. Availability Check: Serial.available() > 0
  2. Line Reception: Serial.readBytesUntil('\n', buffer, size)
  3. Validation:
  4. Overflow detection (buffer full without newline)
  5. Empty command check
  6. Trailing whitespace trimming
  7. Return: true if valid line received, false if timeout/error
bool CommandQueue::receive_line(char* buffer, size_t buffer_size) {
  Serial.setTimeout(COMMAND_RECEPTION_TIMEOUT_MS);  // 500ms
  size_t bytes = Serial.readBytesUntil('\n', buffer, buffer_size - 1);

  // Check for overflow (buffer full without newline)
  if (bytes == (buffer_size - 1)) {
    discard_input();
    return false;
  }

  // Check for empty command
  if (bytes == 0) return false;

  // Trim trailing whitespace
  while (bytes > 0 && isspace(buffer[bytes - 1])) bytes--;

  // Null terminate
  buffer[bytes] = '\0';

  return bytes > 0;
}

Parsing Flow

  1. Command Name Extraction: First whitespace-delimited token
  2. Uppercase Conversion: Normalize input
  3. Alias Resolution: Map single-char shortcuts (T, G, C, etc.)
  4. Argument Splitting: Subsequent tokens up to MAX_ARGS
  5. Return: Populated command_t structure
command_t CommandQueue::parse(const char* line) {
  command_t cmd = {0};

  // Copy to working buffer (strtok modifies it)
  char work[MAX_COMMAND_LENGTH];
  strncpy(work, line, sizeof(work) - 1);
  work[sizeof(work) - 1] = '\0';

  // Extract command name
  char* token = strtok(work, " ");
  if (!token) return cmd;

  strncpy(cmd.name, token, sizeof(cmd.name) - 1);
  cmd.name[sizeof(cmd.name) - 1] = '\0';
  to_uppercase(cmd.name);

  // Resolve aliases (T → SET_THRESHOLD, etc.)
  resolve_alias(cmd.name);

  // Extract arguments
  while ((token = strtok(NULL, " ")) && cmd.arg_count < 2) {
    strncpy(cmd.args[cmd.arg_count], token, sizeof(cmd.args[0]) - 1);
    cmd.args[cmd.arg_count][sizeof(cmd.args[0]) - 1] = '\0';
    cmd.arg_count++;
  }

  return cmd;
}

Queuing with FreeRTOS

Uses static allocation (same as current implementation):

static QueueHandle_t g_queue = NULL;
static StaticQueue_t g_queue_storage;
static uint8_t g_queue_buffer[COMMAND_QUEUE_SIZE * sizeof(command_t)];

void CommandQueue::init(void) {
  g_queue = xQueueCreateStatic(
    COMMAND_QUEUE_SIZE,
    sizeof(command_t),
    g_queue_buffer,
    &g_queue_storage
  );
}

bool CommandQueue::receive(void) {
  if (!Serial.available()) return false;

  char buffer[MAX_COMMAND_LENGTH];
  if (!receive_line(buffer, sizeof(buffer))) {
    discard_input();
    return true;  // We processed the input (even if invalid)
  }

  command_t cmd = parse(buffer);

  BaseType_t result = xQueueSend(g_queue, &cmd, 0);
  if (result != pdTRUE) {
    Serial.println("ERROR: Command queue full");
  }

  return true;
}

bool CommandQueue::has_pending(void) const {
  return uxQueueMessagesWaiting(g_queue) > 0;
}

bool CommandQueue::execute(void) {
  if (!has_pending()) return false;

  command_t cmd = {0};
  if (xQueueReceive(g_queue, &cmd, 0) != pdTRUE) return false;

  // Dispatch to handler (via command_table)
  command_response_t response = dispatch(cmd);

  // Send response
  if (strlen(response.message) > 0) {
    Serial.print(response.message);
  }

  return true;
}

Implementation Phases

Phase 2A: CommandQueue Core (v1.14.0)

Files Created:

  • include/command_queue.h (200 lines) - Public interface, data structures
  • src/command_queue.cpp (300 lines) - Implementation, FreeRTOS integration

Tasks:

  1. Define command_t structure (simplified vs current text_command_t)
  2. Implement receive_line() with timeout and overflow detection
  3. Implement parse() with alias resolution
  4. Integrate FreeRTOS Queue (static allocation)
  5. Implement receive() (unified reception pipeline)
  6. Implement execute() (dequeue + dispatch)
  7. Test with serial monitor (simple commands)
  8. Verify memory usage (Q: stack vs heap?)

Compatibility: Conditional on ENABLE_DEVICE_RESPONSE=1

Phase 2B: Handler Refactoring (v1.14.1)

Tasks:

  1. Update handler signatures to use command_t instead of text_command_t
  2. Migrate key handlers (GET_STATUS, GET_VERSION, SET_POLL_COUNT, SET_THRESHOLD, SET_DEADTIME)
  3. Move handler implementations to use Command class (v1.13.0)
  4. Update dispatch table (command_entry_t) to reference new handlers
  5. Verify backward compatibility with text command protocol
  6. Test all commands with serial monitor

Handlers to Update (priority order):

  • GET_VERSION (no args) → uses Command::get_version()
  • GET_STATUS (no args) → uses Command::format_status()
  • SET_POLL_COUNT (1 arg: count) → uses Command::set_poll_count()
  • SET_THRESHOLD (2 args: ch, val) → uses Command::set_threshold()
  • SET_DEADTIME (1 arg: ms) → uses Command::set_deadtime()

Phase 2C: Complete Migration (v1.14.2)

Tasks:

  1. Migrate remaining commands (GET_THRESHOLD, GET_UPTIME, TEST_LED, RESET, GET_HELP, GET_STREAM, SET_STREAM, etc.)
  2. Migrate conditional commands (RTC, GNSS, WiFi, BME280 when available)
  3. Move all handler implementations into text_command_manager.cpp
  4. Verify command_table dispatch works for all 20+ commands
  5. Remove legacy text_command.cpp (only when ENABLE_DEVICE_RESPONSE=0 not used)
  6. Performance verification: measure parse latency, queue overhead
  7. Create release notes for v1.14.2

Key Design Decisions

  1. Static command_t Structure: No dynamic allocation, fixed-size args
  2. Rationale: Embedded system, FreeRTOS Queue requires fixed item size

  3. Removed raw_buffer: Error context not needed in queue

  4. Rationale: Errors handled at reception, handler receives validated commands
  5. Saves 64 bytes per queued command

  6. Conditional on ENABLE_DEVICE_RESPONSE=1: Integrated with Command class

  7. Rationale: Both form unified configuration management subsystem
  8. Legacy text_command.cpp remains available when flag disabled

  9. FreeRTOS Static Allocation: No heap, no fragmentation

  10. Rationale: Matches current implementation, safe for embedded
  11. Queue size remains 10 items (configurable)

  12. No Type Conversion in Handlers: Move uint16_t validation to reception

  13. Rationale: Simplify handlers, centralize validation logic
  14. Handler receives pre-validated arguments

  15. Unified command_response_t: Single response type for all commands

  16. Rationale: Integrates with device_response_builder from v1.12.0
  17. JSONL format for all responses

Risks & Mitigations

Risk Impact Mitigation
Handler migration errors Broken commands Incremental migration (Phase 2B), test each command before moving next
Queue overflow under load Lost commands Monitor with GET_QUEUE_STATS, increase COMMAND_QUEUE_SIZE if needed
Parser performance Latency increase Benchmark parse time vs current text_parse(), optimize if needed
FreeRTOS integration Crashes on queue ops Use static allocation (proven), test with task priority scenarios
Backward compatibility Legacy code breaks Keep conditional on ENABLE_DEVICE_RESPONSE=1, test both paths
Memory fragmentation Heap issues No dynamic allocation in CommandQueue, stack-based parsing

Architecture Diagram

Serial Input (raw bytes)
  ↓
CommandQueue::receive()
  ├─ receive_line()      ← Handles Serial I/O, timeouts, overflow
  ├─ parse()             ← Tokenizes, alias resolution
  ├─ Queueing            ← FreeRTOS static queue
  └─ Return true (consumed input)
  ↓
[Main loop continues]
  ↓
CommandQueue::execute()
  ├─ has_pending()       ← Check queue
  ├─ Dequeue            ← Get next command_t
  ├─ dispatch()          ← Table-driven lookup
  │  └─ Handler         ← Execute command logic
  └─ Send response      ← Serial output
  ↓
Response sent to serial

File Organization After Phase 2C

Complete Separation via ENABLE_DEVICE_RESPONSE flag:

Path 1: ENABLE_DEVICE_RESPONSE=1 (CommandQueue + Command class)

include/
  ├─ command_queue.h              (NEW - 200 lines)
  ├─ command.h                    (v1.13.0 - Command class singleton)

src/
  ├─ command_queue.cpp            (NEW - 300 lines, core queue + reception + parsing)
  ├─ command_manager.cpp          (NEW - dispatch table + utilities only, ~100 lines)
  ├─ command.cpp                  (v1.13.0 - Command class implementation)
  │
  └─ command/                     (NEW - subdirectory for individual command handlers)
      ├─ version.cpp              (handle_get_version)
      ├─ status.cpp               (handle_get_status)
      ├─ mac_address.cpp          (handle_get_mac_address)
      ├─ poll_count.cpp           (handle_set_poll_count)
      ├─ threshold.cpp            (handle_set_threshold, handle_get_threshold)
      ├─ deadtime.cpp             (handle_set_deadtime)
      ├─ stream.cpp               (handle_set_stream, handle_get_stream)
      ├─ test_led.cpp             (handle_test_led)
      ├─ uptime.cpp               (handle_get_uptime)
      ├─ help.cpp                 (handle_get_help)
      ├─ bme280.cpp               (handle_get_bme280)
      ├─ reset.cpp                (handle_reset)
      ├─ rtc.cpp                  (handle_set_rtc_time, handle_get_rtc_time - conditional ENABLE_RTC)
      ├─ gnss.cpp                 (handle_sync_time, handle_get_gnss_* - conditional ENABLE_GNSS)
      └─ wifi.cpp                 (handle_set_wifi_ssid, handle_set_wifi_enable, handle_get_wifi_status - conditional ENABLE_WIFI)

Key Design Points:

  • core files at src/ root: command_queue.cpp (300 lines) and command_manager.cpp (~100 lines)
  • handler files in src/command/: Each file 20-50 lines, focused on 1-2 related commands
  • Direct filename-to-command mapping: version.cpp has GET_VERSION, threshold.cpp has SET_THRESHOLD + GET_THRESHOLD
  • Conditional compilation: Files with #if ENABLE_RTC etc. guards included based on build flags
  • Zero overhead at file level: Only needed handlers compiled in, others excluded by src_filter

Path 2: ENABLE_DEVICE_RESPONSE=0 (Legacy text_command)

include/
  ├─ text_command.h               (LEGACY - unchanged)
  ├─ runtime_config.h             (LEGACY - unchanged)

src/
  ├─ text_command.cpp             (LEGACY - unchanged)
  ├─ text_command_manager.cpp     (LEGACY - unchanged)
  ├─ runtime_config.cpp           (LEGACY - unchanged)

Key Point: Complete mutual exclusivity via conditional compilation:

#if ENABLE_DEVICE_RESPONSE
  // CommandQueue path (NEW)
  #include "command_queue.h"
  CommandQueue::receive();
  CommandQueue::execute();
#else
  // Legacy text_command path (UNCHANGED)
  #include "text_command.h"
  text_receive();
  text_execute();
#endif

Integration with v1.13.0 (Command Class)

CommandQueue forms the reception and parsing layer above Command class:

┌─────────────────────────────────────────────┐
│ Application Layer (main.cpp)                │
│ - Detection loop                            │
│ - Sensor reading                            │
└─────────────────────────────────────────────┘
           ↓
┌─────────────────────────────────────────────┐
│ Command Handling (CommandQueue + Handlers)  │
│ - text_receive()  ← CommandQueue            │
│ - text_execute()  ← CommandQueue            │
└─────────────────────────────────────────────┘
           ↓
┌─────────────────────────────────────────────┐
│ Configuration Management (Command Class)    │
│ - get_poll_count(), set_poll_count()        │
│ - get_threshold(ch), set_threshold(ch, val) │
│ - get_deadtime(), set_deadtime()            │
│ - And RTC, BME280, GNSS conditionals        │
└─────────────────────────────────────────────┘

Complete Separation Strategy (ENABLE_DEVICE_RESPONSE)

Design Philosophy: Two Independent Worlds

When user sets ENABLE_DEVICE_RESPONSE=1 in build config:

  1. Legacy code NEVER compiles: text_command.cpp, text_command_manager.cpp, runtime_config.* are completely excluded
  2. New code ALWAYS compiles: command_queue.*, command_queue_handlers.cpp, command.cpp
  3. No conditional logic inside files: Use #if at file/directory level, not inside functions

File-Level Conditional Compilation

main.cpp (the only file with #if conditionals):

#if ENABLE_DEVICE_RESPONSE
  #include "command_queue.h"
  #include "command.h"

  void setup() {
    CommandQueue::init();
  }

  void loop() {
    CommandQueue::receive();   // NEW: unified reception + parse + queue
    CommandQueue::execute();   // NEW: dequeue + dispatch + respond
  }
#else
  #include "text_command.h"
  #include "runtime_config.h"

  void setup() {
    text_command_init();
  }

  void loop() {
    text_receive();   // LEGACY: unchanged
    text_execute();   // LEGACY: unchanged
  }
#endif

platformio.ini (build-time exclusion):

[env:esp32dev-dev]
build_flags = -DENABLE_DEVICE_RESPONSE=1
lib_ignore =
src_filter =
  +<*>
  -<text_command.cpp>
  -<text_command_manager.cpp>
  -<runtime_config.cpp>

[env:esp32dev-release]
build_flags = -DENABLE_DEVICE_RESPONSE=0
lib_ignore =
src_filter =
  +<*>
  -<command_queue.cpp>
  -<command_manager.cpp>
  -<command.cpp>

Next Steps

  1. Finalize CommandQueue header (include/command_queue.h):
  2. Define command_t structure
  3. Declare public static methods
  4. Add configuration macros (COMMAND_QUEUE_SIZE, TIMEOUT_MS)
  5. Guard with #if ENABLE_DEVICE_RESPONSE

  6. Implement CommandQueue core (src/command_queue.cpp):

  7. FreeRTOS Queue initialization
  8. Reception, parsing, queuing logic
  9. Error handling
  10. Guard entire file with #if ENABLE_DEVICE_RESPONSE

  11. Create command manager (src/command_manager.cpp):

  12. Implement all 48 handlers using Command class
  13. Full command dispatch table
  14. Integration with DeviceResponseBuilder
  15. Guard entire file with #if ENABLE_DEVICE_RESPONSE

  16. Update main.cpp for conditional flow:

  17. Add #if ENABLE_DEVICE_RESPONSE block
  18. Keep legacy path unchanged

  19. Create release notes (v1.14.0):

  20. Document new CommandQueue class
  21. Explain unified reception pipeline
  22. Show usage examples for developers
  23. Document ENABLE_DEVICE_RESPONSE=1 requirement

Detailed Integration: CommandQueue + Command Class

Layer Architecture

┌──────────────────────────────────────────────────────┐
│ Serial I/O (main.cpp)                                │
│ - CommandQueue::receive()  ← read from serial         │
│ - CommandQueue::execute()  ← write to serial          │
└──────────────────┬───────────────────────────────────┘
                   ↓
┌──────────────────────────────────────────────────────┐
│ CommandQueue (command_queue.cpp/h)                   │
│ - Receive line from serial                           │
│ - Parse → command_t {name, args[]}                   │
│ - Queue with FreeRTOS                                │
│ - Dispatch to handler                                │
└──────────────────┬───────────────────────────────────┘
                   ↓
┌──────────────────────────────────────────────────────┐
│ Command Handlers (command_queue_handlers.cpp)        │
│ - 48 handler functions taking const command_t&       │
│ - Access arguments: cmd.args[0], cmd.args[1], etc.   │
│ - Call Command class methods                         │
│ - Generate responses via DeviceResponseBuilder       │
└──────────────────┬───────────────────────────────────┘
                   ↓
┌──────────────────────────────────────────────────────┐
│ Command Class (command.cpp/h - v1.13.0)              │
│ - get_poll_count(), set_poll_count()                 │
│ - get_threshold(ch), set_threshold(ch, val)          │
│ - get_deadtime(), set_deadtime()                     │
│ - get_stream(), set_stream()                         │
│ - DAC synchronization (automatic in setters)         │
│ - RTC, GNSS, BME280 (conditional)                    │
└──────────────────┬───────────────────────────────────┘
                   ↓
┌──────────────────────────────────────────────────────┐
│ Hardware (DAC, GPIO, sensors)                        │
└──────────────────────────────────────────────────────┘

Implementation Architecture Note

⚠️ Important: command_manager.cpp provides the dispatch table and utilities, while individual handlers are implemented in src/command/*.cpp files.

File Roles

File Role
command_queue.cpp Core: FreeRTOS queue, reception, parsing
command_manager.cpp Dispatch table, utility functions, handler forward declarations
command/*.cpp Individual handler implementations (~15 files)

Subdirectory Structure Rationale

The command-name-based subdirectory structure provides:

  • Scalability: 48 handlers across 15 files (avg 3 handlers/file) is maintainable
  • Discoverability: Filename directly maps to command name (version.cpp → GET_VERSION)
  • Isolation: Each handler/command group is self-contained
  • Modularity: Adding new commands = adding new files (no monolithic file growth)
  • Build flexibility: Can selectively exclude files via src_filter (conditional compilation)

Implementation Pattern

// command_manager.cpp - provides dispatch table and utilities

// Forward declarations (implicit private)
static command_response_t handle_get_version(const command_t& cmd);
static command_response_t handle_get_status(const command_t& cmd);
// ... 48 handlers

// Static dispatch table (const, zero runtime cost)
extern const command_entry_t command_table[] = {
  {"GET_VERSION", {"V", NULL}, handle_get_version, "System", "..."},
  // ... 48 entries
  {NULL, {NULL}, NULL, NULL, NULL}
};

// Handler implementations
static command_response_t handle_get_version(const command_t& cmd) { ... }
static command_response_t handle_get_status(const command_t& cmd) { ... }
// ... 48 implementations

File Naming Convention

The name command_manager reflects its role (managing command dispatch/execution), not its structure (C functions, not classes).

Handler Implementation Pattern

Example 1: Simple getter (GET_VERSION)

// command_manager.cpp
static command_response_t handle_get_version(const command_t& cmd) {
  if (cmd.arg_count != 0) {
    return error_response("GET_VERSION", "No arguments expected", 1);
  }

  // Use Command class (v1.13.0)
  const char* version = Command::getInstance().get_version();

  // Build response via DeviceResponseBuilder
  JsonDocument doc = DeviceResponseBuilder::simple("version", version);
  serializeJson(doc, Serial);
  Serial.println();

  return success_response();
}

Example 2: Setter with validation (SET_POLL_COUNT)

// command_manager.cpp
static command_response_t handle_set_poll_count(const command_t& cmd) {
  if (cmd.arg_count != 1) {
    return error_response("SET_POLL_COUNT", "Missing argument: <count>", 1);
  }

  // Parse argument (CommandQueue guarantees valid format)
  uint16_t count = atoi(cmd.args[0]);

  // Call Command class setter (includes validation)
  if (!Command::getInstance().set_poll_count(count)) {
    return error_response("SET_POLL_COUNT", "Out of range [1, 65535]", 2);
  }

  // Build response
  JsonDocument doc = DeviceResponseBuilder::simple("poll_count", count);
  serializeJson(doc, Serial);
  Serial.println();

  return success_response();
}

Example 3: Complex handler (SET_THRESHOLD with DAC sync)

// command_manager.cpp
static command_response_t handle_set_threshold(const command_t& cmd) {
  if (cmd.arg_count != 2) {
    return error_response("SET_THRESHOLD", "Missing arguments: <ch> <val>", 1);
  }

  // Parse arguments
  uint8_t ch = atoi(cmd.args[0]);
  uint16_t val = atoi(cmd.args[1]);

  // Call Command class setter (automatically syncs DAC)
  if (!Command::getInstance().set_threshold(ch, val)) {
    return error_response("SET_THRESHOLD", "Invalid channel [1,3] or value [0,1023]", 2);
  }

  // Build response (DAC sync already happened in Command::set_threshold)
  JsonDocument doc;
  JsonObject threshold = doc.createNestedObject("threshold");
  threshold["channel"] = ch;
  threshold["value"] = val;
  serializeJson(doc, Serial);
  Serial.println();

  return success_response();
}

Dispatch Table Pattern

// command_manager.cpp
typedef struct {
  const char* name;
  const char* aliases[4];
  command_response_t (*handler)(const command_t&);
  const char* category;
  const char* description;
} command_entry_t;

extern const command_entry_t command_table[] = {
  // System Info
  {"GET_VERSION",     {"V", NULL},             handle_get_version,     "System",   "Firmware version"},
  {"GET_STATUS",      {"S", "STATUS", NULL},  handle_get_status,      "System",   "System status"},
  {"GET_MAC_ADDRESS", {NULL},                 handle_get_mac_address, "System",   "MAC address"},

  // Detection
  {"SET_POLL_COUNT",  {"C", NULL},            handle_set_poll_count,  "Detection", "Set poll count"},
  {"SET_THRESHOLD",   {"T", NULL},            handle_set_threshold,   "Detection", "Set threshold"},
  {"GET_THRESHOLD",   {"G", NULL},            handle_get_threshold,   "Detection", "Get threshold"},
  {"SET_DEADTIME",    {"D", NULL},            handle_set_deadtime,    "Detection", "Set deadtime"},

  // ... 41 more handlers ...

  {NULL, {NULL}, NULL, NULL, NULL}  // Terminator
};

Success Criteria

  • ✅ CommandQueue and dependencies compile only when ENABLE_DEVICE_RESPONSE=1
  • ✅ Legacy text_command path compiles only when ENABLE_DEVICE_RESPONSE=0
  • ✅ No shared code between paths (complete independence)
  • ✅ All 48 handlers migrated to new pattern
  • ✅ Each handler uses Command class (v1.13.0) directly
  • ✅ DAC synchronization automatic (no duplication)
  • ✅ Threshold range unified to [0, 1023]
  • ✅ All responses use DeviceResponseBuilder
  • ✅ Serial protocol unchanged (backward compatible)
  • ✅ Memory usage ≤ current (stack-based, no heap)
  • ✅ Performance measured and acceptable

Learnings from Command Class (v1.13.0)

Apply these proven patterns to CommandQueue:

  • Conditional Compilation: Use #if ENABLE_DEVICE_RESPONSE consistently
  • JSDoc Documentation: Comprehensive function/method documentation
  • Static Allocation: No heap for embedded reliability
  • Simple API: Few public methods, clear responsibility
  • Type Safety: Minimize casting, use explicit types
  • Integration: Works with existing modules (Command class, handlers, responses)