- 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:
- Unify configuration management - Consolidate all getter/setter operations into a single C++ class
- C++ class-based approach - Replace C function API with object-oriented design
- DeviceResponseBuilder integration - Prepare for Phase 2 of unified device response protocol
- Conditional compilation - Use
ENABLE_DEVICE_RESPONSEflag: - When
ENABLE_DEVICE_RESPONSE=1: Use newCommandclass - When
ENABLE_DEVICE_RESPONSE=0: Fallback to existingruntime_config(C functions) - 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¶
- text_command_manager.cpp - Already has
ENABLE_DEVICE_RESPONSEbranching logic - Each handler manually validates arguments
- Calls
config_set_*()andconfig_get_*() -
Returns either
DeviceResponseBuilderor legacyresponse_*()format -
DeviceResponseBuilder - Requires configuration getters
- Used in handlers when
ENABLE_DEVICE_RESPONSE=1 -
Needs clean API to read current config values
-
dac_manager.h - DAC threshold encoding
dac_encode_threshold()- Encodes 12-bit threshold to 2-byte protocoldac_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_*()returningbool(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 whenENABLE_DEVICE_RESPONSE=1)src/command.cpp- Implementation with singleton patterninclude/config.h- AddENABLE_DEVICE_RESPONSEflag (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_tfields - 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:
getInstance()static method call- Cost: Single pointer dereference (negligible)
- Compiler typically inlines this automatically
-
xtensa-esp32-gcc with
-O2optimization: ~2-4 CPU cycles -
Getter/Setter Method Calls
- Without inline: Function prologue/epilogue (~10-20 cycles)
- With inline: Direct member access (1-2 cycles)
-
Recommendation: Use
inlinefor all getters and simple setters -
Code Size Impact
- Singleton storage: 10-12 bytes (single instance on data segment)
- Method bodies (inlined): No additional code at call site
- Method bodies (not inlined): ~10-20 bytes per method
-
Total impact with inlining: Negligible (< 50 bytes)
-
Memory Impact
- Instance data: 12 bytes (6 × uint16_t + 1 × bool = 13 bytes with padding)
- Current system RAM: 320KB; Impact: < 0.004%
- 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¶
- Singleton Pattern
- Single global instance accessible anywhere
- Thread-safe for ESP32 single-core execution
-
Prevents multiple command objects
-
Static Methods vs Instance Methods
- Use instance methods for actual operations
- Static method
getInstance()for accessor -
Clearer intent than all-static class
-
Return Values
- Setters return
boolfor validation feedback - Getters return actual values (or sentinel 0xFFFF for errors)
-
Consistent with existing
runtime_configAPI -
DAC Synchronization
- Kept in
set_threshold()like current implementation - Single point of update
-
No deferred DAC updates
-
No Persistent Storage
- RAM-only, reset on power cycle (like current system)
- 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 sinceENABLE_WIFIis 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¶
- Verify ENABLE_DEVICE_RESPONSE flag - Check if already defined in config.h
- Create include/command.h - Full class definition with conditional guards
- Create src/command.cpp - Singleton implementation with initialization
- Test compilation - Both paths (
ENABLE_DEVICE_RESPONSE=0and=1) - Integrate with text_command_manager.cpp - Verify handlers work unchanged
- Planning for Phase 2 - CommandParser class design