diff --git a/components/Monitoring/CMakeLists.txt b/components/Monitoring/CMakeLists.txt index e030357..d2a5703 100644 --- a/components/Monitoring/CMakeLists.txt +++ b/components/Monitoring/CMakeLists.txt @@ -1,32 +1,53 @@ +# Architecture: +# +-----------------------+ +# | MonitoringManager | ← High-level coordinator +# +-----------------------+ +# | BatteryMonitor | ← Battery logic (platform-independent) +# | CurrentMonitor | ← Current logic (platform-independent) +# +-----------------------+ +# | AdcSampler | ← BSP: Unified ADC sampling interface +# +-----------------------+ +# | ESP-IDF ADC HAL | ← Espressif official driver +# +-----------------------+ + set( requires Helpers ) -if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3") +# Platform-specific dependencies +if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3" OR "$ENV{IDF_TARGET}" STREQUAL "esp32") list(APPEND requires driver esp_adc ) endif() +# Common source files (platform-independent business logic) set( source_files - "" + "Monitoring/MonitoringManager.cpp" + "Monitoring/BatteryMonitor.cpp" + "Monitoring/CurrentMonitor.cpp" ) +# BSP Layer: ADC sampler implementation +if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3" OR "$ENV{IDF_TARGET}" STREQUAL "esp32") +# Common ADC implementation +list(APPEND source_files + "Monitoring/AdcSampler.cpp" + ) + +# Platform-specific GPIO-to-channel mapping if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3") -list(APPEND source_files - "Monitoring/AdcSampler.cpp" - "Monitoring/BatteryMonitor.cpp" - "Monitoring/CurrentMonitor_esp32s3.cpp" - "Monitoring/MonitoringManager_esp32s3.cpp" -) -else() -list(APPEND source_files - "Monitoring/CurrentMonitor_esp32.cpp" - "Monitoring/MonitoringManager_esp32.cpp" -) + list(APPEND source_files + "Monitoring/AdcSampler_esp32s3.cpp" + ) +elseif ("$ENV{IDF_TARGET}" STREQUAL "esp32") + list(APPEND source_files + "Monitoring/AdcSampler_esp32.cpp" + ) +endif() endif() diff --git a/components/Monitoring/Monitoring/AdcSampler.cpp b/components/Monitoring/Monitoring/AdcSampler.cpp index 3696b5b..387a7ba 100644 --- a/components/Monitoring/Monitoring/AdcSampler.cpp +++ b/components/Monitoring/Monitoring/AdcSampler.cpp @@ -1,24 +1,39 @@ +/** + * @file AdcSampler.cpp + * @brief BSP Layer - Common ADC sampling implementation + * + * This file contains platform-independent ADC sampling logic. + * Platform-specific GPIO-to-channel mapping is in separate files: + * - AdcSampler_esp32.cpp + * - AdcSampler_esp32s3.cpp + */ + #include "AdcSampler.hpp" -#if defined(CONFIG_IDF_TARGET_ESP32S3) +#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) #include -#include -static const char *TAG_ADC = "[AdcSampler]"; +static const char *TAG = "[AdcSampler]"; +// Static member initialization adc_oneshot_unit_handle_t AdcSampler::shared_unit_ = nullptr; AdcSampler::~AdcSampler() { if (cali_handle_) { + #if defined(CONFIG_IDF_TARGET_ESP32S3) adc_cali_delete_scheme_curve_fitting(cali_handle_); + #elif defined(CONFIG_IDF_TARGET_ESP32) + adc_cali_delete_scheme_line_fitting(cali_handle_); + #endif cali_handle_ = nullptr; } } bool AdcSampler::init(int gpio, adc_atten_t atten, adc_bitwidth_t bitwidth, size_t window_size) { + // Initialize moving average filter if (window_size == 0) { window_size = 1; @@ -31,37 +46,57 @@ bool AdcSampler::init(int gpio, adc_atten_t atten, adc_bitwidth_t bitwidth, size atten_ = atten; bitwidth_ = bitwidth; + // Map GPIO to ADC channel (platform-specific) if (!map_gpio_to_channel(gpio, unit_, channel_)) { - ESP_LOGW(TAG_ADC, "GPIO %d may not be ADC-capable on ESP32-S3", gpio); + ESP_LOGW(TAG, "GPIO %d is not a valid ADC1 pin on this chip", gpio); + return false; } + // Initialize shared ADC unit if (!ensure_unit()) { return false; } + // Configure the ADC channel if (!configure_channel(gpio, atten, bitwidth)) { return false; } - // Calibration using curve fitting if available + // Try calibration (requires eFuse data) + // ESP32-S3 uses curve-fitting, ESP32 uses line-fitting + esp_err_t cal_err = ESP_FAIL; + + #if defined(CONFIG_IDF_TARGET_ESP32S3) + // ESP32-S3 curve fitting calibration adc_cali_curve_fitting_config_t cal_cfg = { .unit_id = unit_, .chan = channel_, .atten = atten_, .bitwidth = bitwidth_, }; - if (adc_cali_create_scheme_curve_fitting(&cal_cfg, &cali_handle_) == ESP_OK) + cal_err = adc_cali_create_scheme_curve_fitting(&cal_cfg, &cali_handle_); + #elif defined(CONFIG_IDF_TARGET_ESP32) + // ESP32 line-fitting calibration is per-unit, not per-channel + adc_cali_line_fitting_config_t cal_cfg = { + .unit_id = unit_, + .atten = atten_, + .bitwidth = bitwidth_, + }; + cal_err = adc_cali_create_scheme_line_fitting(&cal_cfg, &cali_handle_); + #endif + + if (cal_err == ESP_OK) { cali_inited_ = true; - ESP_LOGI(TAG_ADC, "ADC calibration initialized (curve fitting)"); + ESP_LOGI(TAG, "ADC calibration initialized"); } else { cali_inited_ = false; - ESP_LOGW(TAG_ADC, "ADC calibration not available; using raw-to-mV approximation"); + ESP_LOGW(TAG, "ADC calibration not available; using raw-to-mV approximation"); } return true; @@ -75,8 +110,10 @@ bool AdcSampler::sampleOnce() } int raw = 0; - if (adc_oneshot_read(shared_unit_, channel_, &raw) != ESP_OK) + esp_err_t err = adc_oneshot_read(shared_unit_, channel_, &raw); + if (err != ESP_OK) { + ESP_LOGE(TAG, "adc_oneshot_read failed: %s", esp_err_to_name(err)); return false; } @@ -90,11 +127,22 @@ bool AdcSampler::sampleOnce() } else { - // Approximation for 11dB attenuation - mv = (raw * 2450) / 4095; + // Approximate conversion for 12dB attenuation (~0–3600 mV range) + // Full-scale raw = (1 << bitwidth_) - 1 + // For 12-bit: max raw = 4095 → ~3600 mV + int full_scale_mv = 3600; + int max_raw = (1 << bitwidth_) - 1; + if (max_raw > 0) + { + mv = (raw * full_scale_mv) / max_raw; + } + else + { + mv = 0; + } } - // Moving average + // Update moving average filter sample_sum_ -= samples_[sample_idx_]; samples_[sample_idx_] = mv; sample_sum_ += mv; @@ -123,7 +171,7 @@ bool AdcSampler::ensure_unit() esp_err_t err = adc_oneshot_new_unit(&unit_cfg, &shared_unit_); if (err != ESP_OK) { - ESP_LOGE(TAG_ADC, "adc_oneshot_new_unit failed: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "adc_oneshot_new_unit failed: %s", esp_err_to_name(err)); shared_unit_ = nullptr; return false; } @@ -139,22 +187,11 @@ bool AdcSampler::configure_channel(int gpio, adc_atten_t atten, adc_bitwidth_t b esp_err_t err = adc_oneshot_config_channel(shared_unit_, channel_, &chan_cfg); if (err != ESP_OK) { - ESP_LOGE(TAG_ADC, "adc_oneshot_config_channel failed (GPIO %d, CH %d): %s", gpio, channel_, esp_err_to_name(err)); + ESP_LOGE(TAG, "adc_oneshot_config_channel failed (GPIO %d, CH %d): %s", + gpio, static_cast(channel_), esp_err_to_name(err)); return false; } return true; } -bool AdcSampler::map_gpio_to_channel(int gpio, adc_unit_t &unit, adc_channel_t &channel) -{ - unit = ADC_UNIT_1; - if (gpio >= 1 && gpio <= 10) - { - channel = static_cast(gpio - 1); - return true; - } - channel = ADC_CHANNEL_0; - return false; -} - -#endif // CONFIG_IDF_TARGET_ESP32S3 +#endif // CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32 diff --git a/components/Monitoring/Monitoring/AdcSampler.hpp b/components/Monitoring/Monitoring/AdcSampler.hpp index e68763f..a98235c 100644 --- a/components/Monitoring/Monitoring/AdcSampler.hpp +++ b/components/Monitoring/Monitoring/AdcSampler.hpp @@ -1,37 +1,96 @@ #pragma once +/** + * @file AdcSampler.hpp + * @brief BSP Layer - Unified ADC sampling interface (Hardware Abstraction) + * + * Architecture: + * +-----------------------+ + * | MonitoringManager | ← High-level coordinator + * +-----------------------+ + * | BatteryMonitor | ← Battery logic: voltage, capacity, health + * | CurrentMonitor | ← Current logic: power, instantaneous current + * +-----------------------+ + * | AdcSampler | ← BSP: Unified ADC sampling interface (this file) + * +-----------------------+ + * | ESP-IDF ADC HAL | ← Espressif official driver + * +-----------------------+ + */ + #include #include #include "sdkconfig.h" -#if defined(CONFIG_IDF_TARGET_ESP32S3) +#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) #include "esp_adc/adc_oneshot.h" #include "esp_adc/adc_cali.h" #include "esp_adc/adc_cali_scheme.h" #include +/** + * @class AdcSampler + * @brief Hardware abstraction layer for ADC sampling with moving average filter + * + * This class provides a unified interface for ADC sampling across different + * ESP32 variants. Platform-specific GPIO-to-channel mapping is handled internally. + */ class AdcSampler { public: AdcSampler() = default; ~AdcSampler(); - // Initialize the ADC channel on the shared ADC1 oneshot unit. - // window_size: moving-average window (>=1). - bool init(int gpio, adc_atten_t atten = ADC_ATTEN_DB_12, adc_bitwidth_t bitwidth = ADC_BITWIDTH_DEFAULT, size_t window_size = 1); + // Non-copyable, non-movable (owns hardware resources) + AdcSampler(const AdcSampler &) = delete; + AdcSampler &operator=(const AdcSampler &) = delete; + AdcSampler(AdcSampler &&) = delete; + AdcSampler &operator=(AdcSampler &&) = delete; - // Perform one conversion, update filtered value. Returns false on failure. + /** + * @brief Initialize the ADC channel on the shared ADC1 oneshot unit + * @param gpio GPIO pin number for ADC input + * @param atten ADC attenuation setting (default: 12dB for ~0-3.3V range) + * @param bitwidth ADC resolution (default: 12-bit) + * @param window_size Moving average window size (>=1) + * @return true on success, false on failure + */ + bool init(int gpio, + adc_atten_t atten = ADC_ATTEN_DB_12, + adc_bitwidth_t bitwidth = ADC_BITWIDTH_DEFAULT, + size_t window_size = 1); + + /** + * @brief Perform one ADC conversion and update filtered value + * @return true on success, false on failure + */ bool sampleOnce(); + /** + * @brief Get the filtered ADC reading in millivolts + * @return Filtered voltage in mV + */ int getFilteredMilliVolts() const { return filtered_mv_; } + /** + * @brief Check if ADC sampling is supported on current platform + * @return true if supported + */ + static constexpr bool isSupported() { return true; } + private: + // Hardware initialization helpers bool ensure_unit(); bool configure_channel(int gpio, adc_atten_t atten, adc_bitwidth_t bitwidth); - bool map_gpio_to_channel(int gpio, adc_unit_t &unit, adc_channel_t &channel); - // Shared ADC1 oneshot handle and calibration mutex-less state (single-threaded use here). + /** + * @brief Platform-specific GPIO to ADC channel mapping + * @note Implemented separately in AdcSampler_esp32.cpp and AdcSampler_esp32s3.cpp + */ + static bool map_gpio_to_channel(int gpio, adc_unit_t &unit, adc_channel_t &channel); + + // Shared ADC1 oneshot handle (single instance for all AdcSampler objects) static adc_oneshot_unit_handle_t shared_unit_; + // Per-instance state adc_cali_handle_t cali_handle_{nullptr}; bool cali_inited_{false}; adc_channel_t channel_{ADC_CHANNEL_0}; @@ -39,6 +98,7 @@ private: adc_atten_t atten_{ADC_ATTEN_DB_12}; adc_bitwidth_t bitwidth_{ADC_BITWIDTH_DEFAULT}; + // Moving average filter state std::vector samples_{}; int sample_sum_{0}; size_t sample_idx_{0}; @@ -47,12 +107,13 @@ private: }; #else -// Stub for non-ESP32-S3 targets to keep interfaces consistent. +// Stub for unsupported targets to keep interfaces consistent class AdcSampler { public: bool init(int /*gpio*/, int /*atten*/ = 0, int /*bitwidth*/ = 0, size_t /*window_size*/ = 1) { return false; } bool sampleOnce() { return false; } int getFilteredMilliVolts() const { return 0; } + static constexpr bool isSupported() { return false; } }; #endif diff --git a/components/Monitoring/Monitoring/AdcSampler_esp32.cpp b/components/Monitoring/Monitoring/AdcSampler_esp32.cpp new file mode 100644 index 0000000..936afe2 --- /dev/null +++ b/components/Monitoring/Monitoring/AdcSampler_esp32.cpp @@ -0,0 +1,59 @@ +/** + * @file AdcSampler_esp32.cpp + * @brief BSP Layer - ESP32 specific GPIO to ADC channel mapping + * + * ESP32 ADC1 GPIO mapping: + * - GPIO32 → ADC1_CH4 + * - GPIO33 → ADC1_CH5 + * - GPIO34 → ADC1_CH6 + * - GPIO35 → ADC1_CH7 + * - GPIO36 → ADC1_CH0 + * - GPIO37 → ADC1_CH1 + * - GPIO38 → ADC1_CH2 + * - GPIO39 → ADC1_CH3 + * + * Note: ADC2 is not used to avoid conflicts with Wi-Fi. + */ + +#include "AdcSampler.hpp" + +#if defined(CONFIG_IDF_TARGET_ESP32) + +bool AdcSampler::map_gpio_to_channel(int gpio, adc_unit_t &unit, adc_channel_t &channel) +{ + unit = ADC_UNIT_1; // Only use ADC1 to avoid Wi-Fi conflict + + // ESP32: ADC1 GPIO mapping (GPIO32-39) + switch (gpio) + { + case 36: + channel = ADC_CHANNEL_0; + return true; + case 37: + channel = ADC_CHANNEL_1; + return true; + case 38: + channel = ADC_CHANNEL_2; + return true; + case 39: + channel = ADC_CHANNEL_3; + return true; + case 32: + channel = ADC_CHANNEL_4; + return true; + case 33: + channel = ADC_CHANNEL_5; + return true; + case 34: + channel = ADC_CHANNEL_6; + return true; + case 35: + channel = ADC_CHANNEL_7; + return true; + default: + channel = ADC_CHANNEL_0; + return false; + } +} + +#endif // CONFIG_IDF_TARGET_ESP32 diff --git a/components/Monitoring/Monitoring/AdcSampler_esp32s3.cpp b/components/Monitoring/Monitoring/AdcSampler_esp32s3.cpp new file mode 100644 index 0000000..a9cc976 --- /dev/null +++ b/components/Monitoring/Monitoring/AdcSampler_esp32s3.cpp @@ -0,0 +1,39 @@ +/** + * @file AdcSampler_esp32s3.cpp + * @brief BSP Layer - ESP32-S3 specific GPIO to ADC channel mapping + * + * ESP32-S3 ADC1 GPIO mapping: + * - GPIO1 → ADC1_CH0 + * - GPIO2 → ADC1_CH1 + * - GPIO3 → ADC1_CH2 + * - GPIO4 → ADC1_CH3 + * - GPIO5 → ADC1_CH4 + * - GPIO6 → ADC1_CH5 + * - GPIO7 → ADC1_CH6 + * - GPIO8 → ADC1_CH7 + * - GPIO9 → ADC1_CH8 + * - GPIO10 → ADC1_CH9 + * + * Note: ADC2 is not used to avoid conflicts with Wi-Fi. + */ + +#include "AdcSampler.hpp" + +#if defined(CONFIG_IDF_TARGET_ESP32S3) + +bool AdcSampler::map_gpio_to_channel(int gpio, adc_unit_t &unit, adc_channel_t &channel) +{ + unit = ADC_UNIT_1; // Only use ADC1 to avoid Wi-Fi conflict + + // ESP32-S3: ADC1 on GPIO1–10 → CH0–CH9 + if (gpio >= 1 && gpio <= 10) + { + channel = static_cast(gpio - 1); + return true; + } + + channel = ADC_CHANNEL_0; + return false; +} + +#endif // CONFIG_IDF_TARGET_ESP32S3 diff --git a/components/Monitoring/Monitoring/BatteryMonitor.cpp b/components/Monitoring/Monitoring/BatteryMonitor.cpp index 0be7d32..9b08031 100644 --- a/components/Monitoring/Monitoring/BatteryMonitor.cpp +++ b/components/Monitoring/Monitoring/BatteryMonitor.cpp @@ -1,34 +1,62 @@ -#include "BatteryMonitor.hpp" +/** + * @file BatteryMonitor.cpp + * @brief Business Logic Layer - Battery monitoring implementation + * + * Platform-independent battery monitoring logic. + * Uses AdcSampler (BSP layer) for hardware abstraction. + */ +#include "BatteryMonitor.hpp" #include -static const char *TAG_BAT = "[BatteryMonitor]"; +static const char *TAG = "[BatteryMonitor]"; bool BatteryMonitor::setup() { -#if defined(CONFIG_IDF_TARGET_ESP32S3) - if (CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM <= 0) +#if CONFIG_MONITORING_BATTERY_ENABLE + if (!AdcSampler::isSupported()) { - ESP_LOGE(TAG_BAT, "Invalid divider bottom resistor: %d", CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM); + ESP_LOGI(TAG, "Battery monitoring not supported on this target"); return false; } - scale_ = 1.0f + static_cast(CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM) / static_cast(CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM); + + // Validate divider resistor configuration + if (CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM == 0) + { + ESP_LOGE(TAG, "Invalid divider bottom resistor: %d", CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM); + return false; + } + if (CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM <= 0 || CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM < 0) + { + scale_ = 1.0f; + } + else + { + // Calculate voltage divider scaling factor + // Vbat = Vadc * (R_TOP + R_BOTTOM) / R_BOTTOM + scale_ = 1.0f + static_cast(CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM) / static_cast(CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM); + } + + // Initialize ADC sampler (BSP layer) if (!adc_.init(CONFIG_MONITORING_BATTERY_ADC_GPIO, ADC_ATTEN_DB_12, ADC_BITWIDTH_DEFAULT, CONFIG_MONITORING_BATTERY_SAMPLES)) { - ESP_LOGE(TAG_BAT, "Battery ADC init failed"); + ESP_LOGE(TAG, "Battery ADC init failed"); return false; } - ESP_LOGI(TAG_BAT, "Battery monitor enabled (GPIO=%d, scale=%.3f)", CONFIG_MONITORING_BATTERY_ADC_GPIO, scale_); + ESP_LOGI(TAG, "Battery monitor enabled (GPIO=%d, scale=%.3f)", CONFIG_MONITORING_BATTERY_ADC_GPIO, scale_); return true; #else - ESP_LOGI(TAG_BAT, "Battery monitoring not supported on this target"); + ESP_LOGI(TAG, "Battery monitoring disabled by Kconfig"); return false; #endif } int BatteryMonitor::getBatteryMilliVolts() const { -#if defined(CONFIG_IDF_TARGET_ESP32S3) +#if CONFIG_MONITORING_BATTERY_ENABLE + if (!AdcSampler::isSupported()) + return 0; + if (!adc_.sampleOnce()) return 0; @@ -36,7 +64,8 @@ int BatteryMonitor::getBatteryMilliVolts() const if (mv_at_adc <= 0) return 0; - const float battery_mv = mv_at_adc * scale_; + // Apply voltage divider scaling + const float battery_mv = static_cast(mv_at_adc) * scale_; return static_cast(std::lround(battery_mv)); #else return 0; diff --git a/components/Monitoring/Monitoring/BatteryMonitor.hpp b/components/Monitoring/Monitoring/BatteryMonitor.hpp index fd552eb..1f70c82 100644 --- a/components/Monitoring/Monitoring/BatteryMonitor.hpp +++ b/components/Monitoring/Monitoring/BatteryMonitor.hpp @@ -1,29 +1,58 @@ #pragma once +/** + * @file BatteryMonitor.hpp + * @brief Business Logic Layer - Battery monitoring (voltage, capacity, health) + * + * Architecture: + * +-----------------------+ + * | MonitoringManager | ← High-level coordinator + * +-----------------------+ + * | BatteryMonitor | ← Battery logic (this file) + * | CurrentMonitor | ← Current logic + * +-----------------------+ + * | AdcSampler | ← BSP: Unified ADC sampling interface + * +-----------------------+ + */ + #include "AdcSampler.hpp" +#include "sdkconfig.h" #include +/** + * @class BatteryMonitor + * @brief Monitors battery voltage through a resistor divider + * + * Uses AdcSampler (BSP layer) for hardware abstraction. + * Configuration is done via Kconfig options: + * - CONFIG_MONITORING_BATTERY_ENABLE + * - CONFIG_MONITORING_BATTERY_ADC_GPIO + * - CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM + * - CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM + * - CONFIG_MONITORING_BATTERY_SAMPLES + */ class BatteryMonitor { public: BatteryMonitor() = default; ~BatteryMonitor() = default; + // Initialize battery monitoring hardware bool setup(); - // Read once, update filter, and return battery voltage in mV (after divider compensation). + // Read once, update filter, and return battery voltage in mV (after divider compensation), 0 on failure int getBatteryMilliVolts() const; - // Whether monitoring is enabled by Kconfig + // Whether monitoring is enabled by Kconfig and supported by BSP static constexpr bool isEnabled() { #ifdef CONFIG_MONITORING_BATTERY_ENABLE - return true; + return AdcSampler::isSupported(); #else return false; #endif } private: - float scale_{1.0f}; - mutable AdcSampler adc_; + float scale_{1.0f}; // Voltage divider scaling factor + mutable AdcSampler adc_; // ADC sampler instance (BSP layer) }; diff --git a/components/Monitoring/Monitoring/CurrentMonitor_esp32s3.cpp b/components/Monitoring/Monitoring/CurrentMonitor.cpp similarity index 52% rename from components/Monitoring/Monitoring/CurrentMonitor_esp32s3.cpp rename to components/Monitoring/Monitoring/CurrentMonitor.cpp index c041a01..792b575 100644 --- a/components/Monitoring/Monitoring/CurrentMonitor_esp32s3.cpp +++ b/components/Monitoring/Monitoring/CurrentMonitor.cpp @@ -1,25 +1,45 @@ -#include -#include -#include "CurrentMonitor.hpp" +/** + * @file CurrentMonitor.cpp + * @brief Business Logic Layer - Current monitoring implementation + * + * Platform-independent current monitoring logic. + * Uses AdcSampler (BSP layer) for hardware abstraction. + */ -static const char *TAG_CM = "[CurrentMonitor]"; +#include "CurrentMonitor.hpp" +#include + +static const char *TAG = "[CurrentMonitor]"; void CurrentMonitor::setup() { #ifdef CONFIG_MONITORING_LED_CURRENT + if (!AdcSampler::isSupported()) + { + ESP_LOGI(TAG, "LED current monitoring not supported on this target"); + return; + } + const bool ok = adc_.init(CONFIG_MONITORING_LED_ADC_GPIO, ADC_ATTEN_DB_12, ADC_BITWIDTH_DEFAULT, CONFIG_MONITORING_LED_SAMPLES); if (!ok) { - ESP_LOGE(TAG_CM, "ADC init failed for LED current monitor"); + ESP_LOGE(TAG, "ADC init failed for LED current monitor"); + return; } + + ESP_LOGI(TAG, "LED current monitor enabled (GPIO=%d, Shunt=%dmΩ, Gain=%d)", CONFIG_MONITORING_LED_ADC_GPIO, CONFIG_MONITORING_LED_SHUNT_MILLIOHM, + CONFIG_MONITORING_LED_GAIN); #else - ESP_LOGI(TAG_CM, "LED current monitoring disabled"); + ESP_LOGI(TAG, "LED current monitoring disabled by Kconfig"); #endif } float CurrentMonitor::getCurrentMilliAmps() const { #ifdef CONFIG_MONITORING_LED_CURRENT + if (!AdcSampler::isSupported()) + return 0.0f; + const int shunt_milliohm = CONFIG_MONITORING_LED_SHUNT_MILLIOHM; // mΩ if (shunt_milliohm <= 0) return 0.0f; @@ -28,6 +48,8 @@ float CurrentMonitor::getCurrentMilliAmps() const return 0.0f; int filtered_mv = adc_.getFilteredMilliVolts(); + + // Apply gain compensation if using current sense amplifier if (CONFIG_MONITORING_LED_GAIN > 0) filtered_mv = filtered_mv / CONFIG_MONITORING_LED_GAIN; // convert back to shunt voltage diff --git a/components/Monitoring/Monitoring/CurrentMonitor.hpp b/components/Monitoring/Monitoring/CurrentMonitor.hpp index d099fa3..b474196 100644 --- a/components/Monitoring/Monitoring/CurrentMonitor.hpp +++ b/components/Monitoring/Monitoring/CurrentMonitor.hpp @@ -1,36 +1,61 @@ #ifndef CURRENT_MONITOR_HPP #define CURRENT_MONITOR_HPP #pragma once +/** + * @file CurrentMonitor.hpp + * @brief Business Logic Layer - Current monitoring (power, instantaneous current) + * + * Architecture: + * +-----------------------+ + * | MonitoringManager | ← High-level coordinator + * +-----------------------+ + * | BatteryMonitor | ← Battery logic + * | CurrentMonitor | ← Current logic (this file) + * +-----------------------+ + * | AdcSampler | ← BSP: Unified ADC sampling interface + * +-----------------------+ + */ + #include -#include #include "sdkconfig.h" #include "AdcSampler.hpp" +/** + * @class CurrentMonitor + * @brief Monitors LED current through a shunt resistor + * + * Uses AdcSampler (BSP layer) for hardware abstraction. + * Configuration is done via Kconfig options: + * - CONFIG_MONITORING_LED_CURRENT + * - CONFIG_MONITORING_LED_ADC_GPIO + * - CONFIG_MONITORING_LED_SHUNT_MILLIOHM + * - CONFIG_MONITORING_LED_GAIN + * - CONFIG_MONITORING_LED_SAMPLES + */ class CurrentMonitor { public: CurrentMonitor() = default; ~CurrentMonitor() = default; + // Initialize current monitoring hardware void setup(); // convenience: combined sampling and compute; returns mA float getCurrentMilliAmps() const; - // - float getBatteryVoltageMilliVolts() const; - // Whether monitoring is enabled by Kconfig + // Whether monitoring is enabled by Kconfig and supported by BSP static constexpr bool isEnabled() { -#ifdef CONFIG_MONITORING_LED_CURRENT - return true; +#if CONFIG_MONITORING_LED_CURRENT + return AdcSampler::isSupported(); #else return false; #endif } private: - mutable AdcSampler adc_; + mutable AdcSampler adc_; // ADC sampler instance (BSP layer) }; #endif diff --git a/components/Monitoring/Monitoring/CurrentMonitor_esp32.cpp b/components/Monitoring/Monitoring/CurrentMonitor_esp32.cpp deleted file mode 100644 index a571ac6..0000000 --- a/components/Monitoring/Monitoring/CurrentMonitor_esp32.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "CurrentMonitor.hpp" -#include - -static const char *TAG_CM = "[CurrentMonitor]"; - -void CurrentMonitor::setup() -{ - ESP_LOGI(TAG_CM, "LED current monitoring disabled"); -} - -float CurrentMonitor::getCurrentMilliAmps() const -{ - return 0.0f; -} diff --git a/components/Monitoring/Monitoring/MonitoringManager.cpp b/components/Monitoring/Monitoring/MonitoringManager.cpp new file mode 100644 index 0000000..d5d29d8 --- /dev/null +++ b/components/Monitoring/Monitoring/MonitoringManager.cpp @@ -0,0 +1,171 @@ +/** + * @file MonitoringManager.cpp + * @brief High-level Coordinator - Monitoring manager implementation + * + * Platform-independent monitoring coordination logic. + * Manages BatteryMonitor and CurrentMonitor subsystems. + */ + +#include "MonitoringManager.hpp" +#include +#include "sdkconfig.h" + +static const char *TAG = "[MonitoringManager]"; + +void MonitoringManager::setup() +{ +#if CONFIG_MONITORING_LED_CURRENT + if (CurrentMonitor::isEnabled()) + { + cm_.setup(); + ESP_LOGI(TAG, "LED current monitoring enabled. Interval=%dms, Samples=%d, Gain=%d, R=%dmΩ", + CONFIG_MONITORING_LED_INTERVAL_MS, + CONFIG_MONITORING_LED_SAMPLES, + CONFIG_MONITORING_LED_GAIN, + CONFIG_MONITORING_LED_SHUNT_MILLIOHM); + } + else + { + ESP_LOGI(TAG, "LED current monitoring not supported on this target"); + } +#else + ESP_LOGI(TAG, "LED current monitoring disabled by Kconfig"); +#endif + +#if CONFIG_MONITORING_BATTERY_ENABLE + if (BatteryMonitor::isEnabled()) + { + bm_.setup(); + ESP_LOGI(TAG, "Battery monitoring enabled. Interval=%dms, Samples=%d, R-Top=%dΩ, R-Bottom=%dΩ", + CONFIG_MONITORING_BATTERY_INTERVAL_MS, + CONFIG_MONITORING_BATTERY_SAMPLES, + CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM, + CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM); + } + else + { + ESP_LOGI(TAG, "Battery monitoring not supported on this target"); + } +#else + ESP_LOGI(TAG, "Battery monitoring disabled by Kconfig"); +#endif +} + +void MonitoringManager::start() +{ + if (!isEnabled()) + { + ESP_LOGI(TAG, "No monitoring features enabled, task not started"); + return; + } + + if (task_ == nullptr) + { + xTaskCreate(&MonitoringManager::taskEntry, "MonitoringTask", 2048, this, 1, &task_); + ESP_LOGI(TAG, "Monitoring task started"); + } +} + +void MonitoringManager::stop() +{ + if (task_) + { + TaskHandle_t toDelete = task_; + task_ = nullptr; + vTaskDelete(toDelete); + ESP_LOGI(TAG, "Monitoring task stopped"); + } +} + +void MonitoringManager::taskEntry(void *arg) +{ + static_cast(arg)->run(); +} + +void MonitoringManager::run() +{ + if (!isEnabled()) + { + vTaskDelete(nullptr); + return; + } + + TickType_t now_tick = xTaskGetTickCount(); + +#if CONFIG_MONITORING_LED_CURRENT + TickType_t next_tick_led = now_tick; + const TickType_t led_period = pdMS_TO_TICKS(CONFIG_MONITORING_LED_INTERVAL_MS); +#endif + +#if CONFIG_MONITORING_BATTERY_ENABLE + TickType_t next_tick_bat = now_tick; + const TickType_t batt_period = pdMS_TO_TICKS(CONFIG_MONITORING_BATTERY_INTERVAL_MS); +#endif + + while (true) + { + now_tick = xTaskGetTickCount(); + TickType_t wait_ticks = pdMS_TO_TICKS(50); // Default wait time + +#if CONFIG_MONITORING_LED_CURRENT + if (CurrentMonitor::isEnabled() && now_tick >= next_tick_led) + { + float ma = cm_.getCurrentMilliAmps(); + last_current_ma_.store(ma); + next_tick_led = now_tick + led_period; + } + if (CurrentMonitor::isEnabled()) + { + TickType_t to_led = (next_tick_led > now_tick) ? (next_tick_led - now_tick) : 1; + if (to_led < wait_ticks) + { + wait_ticks = to_led; + } + } +#endif + +#if CONFIG_MONITORING_BATTERY_ENABLE + if (BatteryMonitor::isEnabled() && now_tick >= next_tick_bat) + { + const int mv = bm_.getBatteryMilliVolts(); + if (mv > 0) + { + last_battery_mv_.store(mv); + } + next_tick_bat = now_tick + batt_period; + } + if (BatteryMonitor::isEnabled()) + { + TickType_t to_batt = (next_tick_bat > now_tick) ? (next_tick_bat - now_tick) : 1; + if (to_batt < wait_ticks) + { + wait_ticks = to_batt; + } + } +#endif + + if (wait_ticks == 0) + { + wait_ticks = 1; + } + vTaskDelay(wait_ticks); + } +} + +float MonitoringManager::getCurrentMilliAmps() const +{ +#if CONFIG_MONITORING_LED_CURRENT + if (CurrentMonitor::isEnabled()) + return last_current_ma_.load(); +#endif + return 0.0f; +} + +float MonitoringManager::getBatteryVoltageMilliVolts() const +{ +#if CONFIG_MONITORING_BATTERY_ENABLE + if (BatteryMonitor::isEnabled()) + return static_cast(last_battery_mv_.load()); +#endif + return 0.0f; +} diff --git a/components/Monitoring/Monitoring/MonitoringManager.hpp b/components/Monitoring/Monitoring/MonitoringManager.hpp index fe8ee6e..2db44b8 100644 --- a/components/Monitoring/Monitoring/MonitoringManager.hpp +++ b/components/Monitoring/Monitoring/MonitoringManager.hpp @@ -1,15 +1,48 @@ #pragma once +/** + * @file MonitoringManager.hpp + * @brief High-level Coordinator - Combines Battery and Current monitoring + * + * Architecture: + * +-----------------------+ + * | MonitoringManager | ← High-level coordinator (this file) + * +-----------------------+ + * | BatteryMonitor | ← Battery logic: voltage, capacity, health + * | CurrentMonitor | ← Current logic: power, instantaneous current + * +-----------------------+ + * | AdcSampler | ← BSP: Unified ADC sampling interface + * +-----------------------+ + * | ESP-IDF ADC HAL | ← Espressif official driver + * +-----------------------+ + */ + #include #include #include -#include "CurrentMonitor.hpp" #include "BatteryMonitor.hpp" +#include "CurrentMonitor.hpp" +/** + * @class MonitoringManager + * @brief Coordinates battery and current monitoring subsystems + * + * This class manages the lifecycle and periodic sampling of both + * BatteryMonitor and CurrentMonitor. It runs a background FreeRTOS task + * to perform periodic measurements based on Kconfig intervals. + * + * Thread-safety: Uses atomic variables for cross-thread data access. + */ class MonitoringManager { public: + MonitoringManager() = default; + ~MonitoringManager() = default; + + // Initialize monitoring subsystems based on Kconfig settings void setup(); + // Start the background monitoring task void start(); + // Stop the background monitoring task void stop(); // Latest filtered current in mA @@ -17,6 +50,12 @@ public: // Latest battery voltage in mV float getBatteryVoltageMilliVolts() const; + // Check if any monitoring feature is enabled + static constexpr bool isEnabled() + { + return CurrentMonitor::isEnabled() || BatteryMonitor::isEnabled(); + } + private: static void taskEntry(void *arg); void run(); @@ -24,6 +63,7 @@ private: TaskHandle_t task_{nullptr}; std::atomic last_current_ma_{0.0f}; std::atomic last_battery_mv_{0}; + CurrentMonitor cm_; BatteryMonitor bm_; }; diff --git a/components/Monitoring/Monitoring/MonitoringManager_esp32.cpp b/components/Monitoring/Monitoring/MonitoringManager_esp32.cpp deleted file mode 100644 index 58f7170..0000000 --- a/components/Monitoring/Monitoring/MonitoringManager_esp32.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "MonitoringManager.hpp" -#include - -static const char *TAG_MM = "[MonitoringManager]"; - -void MonitoringManager::setup() -{ - ESP_LOGI(TAG_MM, "Monitoring disabled by Kconfig"); -} - -void MonitoringManager::start() -{ -} - -void MonitoringManager::stop() -{ -} - -void MonitoringManager::taskEntry(void *arg) -{ -} - -void MonitoringManager::run() -{ -} - -float MonitoringManager::getCurrentMilliAmps() const { - return 0.0f; -} - -float MonitoringManager::getBatteryVoltageMilliVolts() const -{ - return 0.0f; -} \ No newline at end of file diff --git a/components/Monitoring/Monitoring/MonitoringManager_esp32s3.cpp b/components/Monitoring/Monitoring/MonitoringManager_esp32s3.cpp deleted file mode 100644 index 32f321a..0000000 --- a/components/Monitoring/Monitoring/MonitoringManager_esp32s3.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include "MonitoringManager.hpp" -#include -#include -#include "sdkconfig.h" - -static const char *TAG_MM = "[MonitoringManager]"; -void MonitoringManager::setup() -{ -#if CONFIG_MONITORING_LED_CURRENT - cm_.setup(); - ESP_LOGI(TAG_MM, "LED current monitoring enabled. Interval=%dms, Samples=%d, Gain=%d, R=%dmΩ", - CONFIG_MONITORING_LED_INTERVAL_MS, - CONFIG_MONITORING_LED_SAMPLES, - CONFIG_MONITORING_LED_GAIN, - CONFIG_MONITORING_LED_SHUNT_MILLIOHM); -#else - ESP_LOGI(TAG_MM, "LED current monitoring disabled by Kconfig"); -#endif - -#if CONFIG_MONITORING_BATTERY_ENABLE - bm_.setup(); - ESP_LOGI(TAG_MM, "Battery monitoring enabled. Interval=%dms, Samples=%d, R-Top=%dΩ, R-Bottom=%dΩ", - CONFIG_MONITORING_BATTERY_INTERVAL_MS, - CONFIG_MONITORING_BATTERY_SAMPLES, - CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM, - CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM); -#else - ESP_LOGI(TAG_MM, "Battery monitoring disabled by Kconfig"); -#endif -} - -void MonitoringManager::start() -{ -#if CONFIG_MONITORING_LED_CURRENT || CONFIG_MONITORING_BATTERY_ENABLE - if (task_ == nullptr) - { - xTaskCreate(&MonitoringManager::taskEntry, "MonitoringTask", 2048, this, 1, &task_); - } -#endif -} - -void MonitoringManager::stop() -{ - if (task_) - { - TaskHandle_t toDelete = task_; - task_ = nullptr; - vTaskDelete(toDelete); - } -} - -void MonitoringManager::taskEntry(void *arg) -{ - static_cast(arg)->run(); -} - -void MonitoringManager::run() -{ -#if CONFIG_MONITORING_LED_CURRENT || CONFIG_MONITORING_BATTERY_ENABLE - TickType_t now_tick = xTaskGetTickCount(); -#if CONFIG_MONITORING_LED_CURRENT - TickType_t next_tick_led = now_tick; - const TickType_t led_period = pdMS_TO_TICKS(CONFIG_MONITORING_LED_INTERVAL_MS); -#endif -#if CONFIG_MONITORING_BATTERY_ENABLE - TickType_t next_tick_bat = now_tick; - const TickType_t batt_period = pdMS_TO_TICKS(CONFIG_MONITORING_BATTERY_INTERVAL_MS); -#endif - - - while (true) - { - now_tick = xTaskGetTickCount(); - TickType_t wait_ticks = pdMS_TO_TICKS(50); - -#if CONFIG_MONITORING_LED_CURRENT - if (now_tick >= next_tick_led) - { - float ma = cm_.getCurrentMilliAmps(); - last_current_ma_.store(ma); - next_tick_led = now_tick + led_period; - } - TickType_t to_led = (next_tick_led > now_tick) ? (next_tick_led - now_tick) : 1; - if (to_led < wait_ticks) - { - wait_ticks = to_led; - } -#endif - -#if CONFIG_MONITORING_BATTERY_ENABLE - if (now_tick >= next_tick_bat) - { - const int mv = bm_.getBatteryMilliVolts(); - if (mv > 0) - { - last_battery_mv_.store(mv); - } - next_tick_bat = now_tick + batt_period; - } - TickType_t to_batt = (next_tick_bat > now_tick) ? (next_tick_bat - now_tick) : 1; - if (to_batt < wait_ticks) - { - wait_ticks = to_batt; - } -#endif - - if (wait_ticks == 0) - { - wait_ticks = 1; - } - vTaskDelay(wait_ticks); - } -#else - vTaskDelete(nullptr); -#endif -} - -float MonitoringManager::getCurrentMilliAmps() const -{ -#if CONFIG_MONITORING_LED_CURRENT - return last_current_ma_.load(); -#else - return 0.0f; -#endif -} - -float MonitoringManager::getBatteryVoltageMilliVolts() const -{ -#if CONFIG_MONITORING_BATTERY_ENABLE - return static_cast(last_battery_mv_.load()); -#else - return 0.0f; -#endif -}