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

Progress Log: Command Class Refactoring Design

Task Description

Design and implement a Command class to replace runtime_config.h/cpp with the following goals:

  1. Unify configuration management - Consolidate all getter/setter operations into a single C++ class
  2. C++ class-based approach - Replace C function API with object-oriented design
  3. DeviceResponseBuilder integration - Prepare for Phase 2 of unified device response protocol
  4. Conditional compilation - Use ENABLE_DEVICE_RESPONSE flag:
  5. When ENABLE_DEVICE_RESPONSE=1: Use new Command class
  6. When ENABLE_DEVICE_RESPONSE=0: Fallback to existing runtime_config (C functions)
  7. Foundation for CommandParser - Create stable API that CommandParser can depend on

Analysis

Current State (runtime_config pattern)

Strengths:

  • Simple C function API
  • Direct configuration access
  • Minimal overhead

Weaknesses:

  • Configuration scattered across C functions
  • No unified interface for get/set operations
  • DAC synchronization logic coupled with setters
  • Difficult to integrate with DeviceResponseBuilder (C++ class)
  • No natural place for validation logic

Existing Integration Points

  1. text_command_manager.cpp - Already has ENABLE_DEVICE_RESPONSE branching logic
  2. Each handler manually validates arguments
  3. Calls config_set_*() and config_get_*()
  4. Returns either DeviceResponseBuilder or legacy response_*() format

  5. DeviceResponseBuilder - Requires configuration getters

  6. Used in handlers when ENABLE_DEVICE_RESPONSE=1
  7. Needs clean API to read current config values

  8. dac_manager.h - DAC threshold encoding

  9. dac_encode_threshold() - Encodes 12-bit threshold to 2-byte protocol
  10. dac_send() - Sends encoded bytes to DAC via SPI

Design Strategy

Singleton Pattern with Static Access:

Command& cmd = Command::getInstance();
cmd.set_poll_count(200);
uint16_t count = cmd.get_poll_count();

Getter/Setter Naming Convention:

  • All getters: get_*()
  • All setters: set_*() returning bool (validation success/failure)
  • Status methods: is_*() for boolean queries

Conditional Compilation Wrapper:

#if ENABLE_DEVICE_RESPONSE
  // Use Command class
  Command::getInstance().set_poll_count(count);
#else
  // Use legacy runtime_config
  config_set_poll_count(count);
#endif

Implementation Plan

Phase 1: Command Class Structure (Base Implementation)

File Structure:

  • include/command.h - Class definition (only when ENABLE_DEVICE_RESPONSE=1)
  • src/command.cpp - Implementation with singleton pattern
  • include/config.h - Add ENABLE_DEVICE_RESPONSE flag (if not present)

Core API:

class Command {
 public:
  static Command& getInstance();  // Singleton accessor

  // Configuration Getters
  uint16_t get_poll_count() const;
  uint16_t get_threshold(uint8_t ch) const;
  const char* get_version() const;
  uint16_t get_deadtime() const;
  bool get_stream_enabled() const;

  // Configuration Setters (return bool for validation)
  bool set_poll_count(uint16_t count);
  bool set_threshold(uint8_t ch, uint16_t val);
  bool set_deadtime(uint16_t deadtime_ms);
  void set_stream_enabled(bool enable);

  // Utility Methods
  bool format_status(char* buffer, size_t buffer_size) const;

 private:
  // Singleton instance
  static Command* g_instance;

  // Configuration data (mirroring runtime_config_t)
  uint16_t poll_count_;
  uint16_t threshold_ch1_;
  uint16_t threshold_ch2_;
  uint16_t threshold_ch3_;
  uint16_t deadtime_ms_;
  bool stream_enabled_;
  const char* firmware_version_;

  // Private constructor/destructor
  Command();
  ~Command() = delete;  // Non-copyable, non-movable
  Command(const Command&) = delete;
  Command& operator=(const Command&) = delete;
};

Initialization Pattern:

// In setup():
Command::getInstance().init();  // Optional explicit init

Implementation Details

Data Storage:

  • Member variables match runtime_config_t fields
  • Static instance in command.cpp
  • Default values same as current runtime_config.cpp

Setter Validation:

bool Command::set_poll_count(uint16_t count) {
  if (count < 1 || count > 65535) {
    return false;
  }
  poll_count_ = count;
  return true;
}

bool Command::set_threshold(uint8_t ch, uint16_t val) {
  if (ch < 1 || ch > 3 || val > 4095) {
    return false;
  }

  // Update config
  switch (ch) {
    case 1: threshold_ch1_ = val; break;
    case 2: threshold_ch2_ = val; break;
    case 3: threshold_ch3_ = val; break;
    default: return false;
  }

  // Sync with DAC hardware
  uint8_t byte1, byte2;
  if (dac_encode_threshold(val, &byte1, &byte2)) {
    dac_send(ch, byte1, byte2);
  }

  return true;
}

Getter Implementation:

uint16_t Command::get_threshold(uint8_t ch) const {
  if (ch < 1 || ch > 3) {
    return 0xFFFF;  // Invalid indicator
  }

  switch (ch) {
    case 1: return threshold_ch1_;
    case 2: return threshold_ch2_;
    case 3: return threshold_ch3_;
    default: return 0xFFFF;
  }
}

Stream Output Control:

bool Command::get_stream_enabled() const {
  return stream_enabled_;
}

void Command::set_stream_enabled(bool enable) {
  stream_enabled_ = enable;
}

Integration with DeviceResponseBuilder

In text_command_manager.cpp handlers:

#if ENABLE_DEVICE_RESPONSE
  // Use Command class with builder
  JsonDocument doc = DeviceResponseBuilder::simple(
    "poll_count",
    (int32_t)Command::getInstance().get_poll_count()
  );
  serializeJson(doc, Serial);
  Serial.println();
#else
  // Fallback to legacy runtime_config
  send_response(response_int("poll_count", config_get_poll_count()));
#endif

Performance & Overhead Analysis

Singleton Pattern Overhead:

  1. getInstance() static method call
  2. Cost: Single pointer dereference (negligible)
  3. Compiler typically inlines this automatically
  4. xtensa-esp32-gcc with -O2 optimization: ~2-4 CPU cycles

  5. Getter/Setter Method Calls

  6. Without inline: Function prologue/epilogue (~10-20 cycles)
  7. With inline: Direct member access (1-2 cycles)
  8. Recommendation: Use inline for all getters and simple setters

  9. Code Size Impact

  10. Singleton storage: 10-12 bytes (single instance on data segment)
  11. Method bodies (inlined): No additional code at call site
  12. Method bodies (not inlined): ~10-20 bytes per method
  13. Total impact with inlining: Negligible (< 50 bytes)

  14. Memory Impact

  15. Instance data: 12 bytes (6 × uint16_t + 1 × bool = 13 bytes with padding)
  16. Current system RAM: 320KB; Impact: < 0.004%
  17. No dynamic allocation (static instance)

Optimization Strategy: Header-only Inline Getters

All getter methods should be declared inline in header file:

// In command.h
class Command {
 public:
  static Command& getInstance();  // Non-inline, defined in .cpp

  // Inline getters (hot path - zero overhead)
  inline uint16_t get_poll_count() const { return poll_count_; }
  inline uint16_t get_threshold(uint8_t ch) const {
    if (ch < 1 || ch > 3) return 0xFFFF;
    return (ch == 1) ? threshold_ch1_ : (ch == 2) ? threshold_ch2_ : threshold_ch3_;
  }
  inline const char* get_version() const { return firmware_version_; }
  inline uint16_t get_deadtime() const { return deadtime_ms_; }
  inline bool get_stream_enabled() const { return stream_enabled_; }

  // Setters with validation (non-inline due to complexity)
  bool set_poll_count(uint16_t count);
  bool set_threshold(uint8_t ch, uint16_t val);
  bool set_deadtime(uint16_t deadtime_ms);
  inline void set_stream_enabled(bool enable) { stream_enabled_ = enable; }

  bool format_status(char* buffer, size_t buffer_size) const;

 private:
  // ... data members ...
};

Why This Approach:

  • Getters are frequently called, inlining eliminates all overhead
  • Complex setters stay in .cpp with validation logic
  • getInstance() called rarely (once per handler)
  • Result: Zero or better performance vs C function approach

No Backward Compatibility Wrappers

  • Command class is a complete replacement for runtime_config
  • No legacy C-style function wrappers
  • Full C++ class-based refactoring when ENABLE_DEVICE_RESPONSE=1
  • Clean, modern API with no transitional code

Key Design Decisions

  1. Singleton Pattern
  2. Single global instance accessible anywhere
  3. Thread-safe for ESP32 single-core execution
  4. Prevents multiple command objects

  5. Static Methods vs Instance Methods

  6. Use instance methods for actual operations
  7. Static method getInstance() for accessor
  8. Clearer intent than all-static class

  9. Return Values

  10. Setters return bool for validation feedback
  11. Getters return actual values (or sentinel 0xFFFF for errors)
  12. Consistent with existing runtime_config API

  13. DAC Synchronization

  14. Kept in set_threshold() like current implementation
  15. Single point of update
  16. No deferred DAC updates

  17. No Persistent Storage

  18. RAM-only, reset on power cycle (like current system)
  19. Same semantics as runtime_config_t

List of commands (minimal implementation)

Information

  • USAGE: cmd.usage() (returns list of available commands; similar to current GET_HELP but simpler)
  • GET_VERSION : cmd.get_version() (returns firmware version)
  • GET_MAC_ADDRESS: cmd.get_mac_address() (returns MAC addres of ESP32 WiFI)
  • GET_UPTIME: cmd.get_uptime() (returns uptime in milliseconds since ESP32 started)

Detection

  • GET_STREAM: cmd.get_stream()
  • SET_STREAM <0|1>: cmd.set_stream(mode)
  • GET_DEADTIME: cmd.get_deadtime()
  • SET_DEADTIME <ms>: cmd.set_deadtime(ms)
  • GET_POLL_COUNT: cmd.get_poll_count(count)
  • SET_POLL_COUNT <count>: cmd.set_poll_count(count)

Threshold

  • GET_THRESHOLD <ch> : cmd.get_threshold(ch)
  • SET_THRESHOLD <ch> <vth> : cmd.set_threshold(ch, vth) (write value to DAC)

RTC

  • GET_RTC_TIME: cmd.get_rtc_time() (returns unixtime of ESP32 RTC time)
  • SET_RTC_TIME <unixtime>: cmd.set_rtc_time(unixtime) (update unixtime of ESP32 RTC time)

GNSS

  • GET_GNSS_TIME: cmd.get_gnss_time() (returns unixtime of GNSS module)
  • GET_GNSS_LONGITUDE: cmd.get_gnss_longitude()
  • GET_GNSS_LATITUDE: cmd.get_gnss_latitude()
  • GET_GNSS_ALTITUDE: cmd.get_gnss_altitude()
  • GET_GNSS_SATELLITES: cmd.get_gnss_satellites()
  • GET_GNSS_QUALITY: cmd.get_gnss_quality()
  • GET_GNSS_VALID: cmd.get_gnss_valid()
  • GET_GNSS_HDOP: cmd.get_gnss_hdop()
  • GET_GNSS: cmd.gnss() (returns status of GNSS: unixtime, latitude, longitude, altitude, satellites, fix_quality, hdop, fix_valid, and else?)

BME280

  • GET_BME_TMP: cmd.get_bme280_temperature() (returns temperature of BME280 module)
  • GET_BME_ATM: cmd.get_bme280_pressure() (returns pressure of BME280 module)
  • GET_BME_HMD: cmd.get_bme280_humidity() (returns humidity of BME280 module)
  • GET_BME280: cmd.get_bme280() (returns status of BME280: tmp_c, atm_pa, and hmd_pct, and else?)

WiFi (defer implementation since WiFi feature is not ready yet)

  • GET_WIFI_MODE: cmd.get_wifi_mode()
  • SET_WIFI_MODE <mode>: cmd.set_wifi_mode(mode)
  • SET_WIFI_AP <ssid> <password>: cmd.set_wifi_ap(ssid, password)
  • GET_WIFI_AP: cmd.get_wifi_ap() (returns ssid and password of AP)
  • GET_WIFI: cmd.wifi() (returns status of WiFI: mode, SSID, password, rssid, and else?) -> defer implementation since ENABLE_WIFI is not ready yet.

Header (include/command.h)

// include/command.h
class Command {
 public:
  static Command& getInstance();

  // === Information ===
  const char* get_version() const;
  const char* get_mac_address() const;
  uint32_t get_uptime() const;
  // usage() はハンドラー側で実装

  // === Detection ===
  inline bool get_stream() const { return stream_enabled_; }
  inline void set_stream(bool enable) { stream_enabled_ = enable; }
  inline uint16_t get_deadtime() const { return deadtime_ms_; }
  bool set_deadtime(uint16_t ms);
  inline uint16_t get_poll_count() const { return poll_count_; }
  bool set_poll_count(uint16_t count);  // 0 <= count <= 65535

  // === Threshold ===
  uint16_t get_threshold(uint8_t channel) const;  // 1 <= ch <= 3
  bool set_threshold(uint8_t channel, uint16_t threshold);  // 0 <= threshold < 1024

  // === RTC (conditional) ===
#if ENABLE_RTC
  uint32_t get_rtc_time() const;
  bool set_rtc_time(uint32_t unixtime);
#endif

  // === GNSS (conditional) ===
#if ENABLE_GNSS
  uint32_t get_gnss_time() const;  // returns in unixtime
  float get_gnss_latitude() const;
  float get_gnss_longitude() const;
  float get_gnss_altitude() const;
  uint8_t get_gnss_satellites() const;
  uint8_t get_gnss_quality() const;
  bool get_gnss_valid() const;
  float get_gnss_hdop() const;
#endif

  // === BME280 (conditional) ===
#if ENABLE_BME280
  float get_bme280_temperature() const;
  float get_bme280_pressure() const;
  float get_bme280_humidity() const;
#endif

private:
  // singleton (static instance)
  static Command g_command;

  // data members (private)
  uint16_t poll_count_;
  uint16_t threshold1_;
  uint16_t threshold2_;
  uint16_t threshold3_;
  uint16_t deadtime_ms_;
  bool stream_enabled_;
  const char* firmware_version_;

public:
  // Static accessor
  static Command& getInstance() {
    return g_command;
  }

private:
  Command();
  ~Command() = delete;
  Command(const Command&) = delete;
  Command& operator=(const Command&) = delete;
};

Next Steps

  1. Verify ENABLE_DEVICE_RESPONSE flag - Check if already defined in config.h
  2. Create include/command.h - Full class definition with conditional guards
  3. Create src/command.cpp - Singleton implementation with initialization
  4. Test compilation - Both paths (ENABLE_DEVICE_RESPONSE=0 and =1)
  5. Integrate with text_command_manager.cpp - Verify handlers work unchanged
  6. Planning for Phase 2 - CommandParser class design