Cleaned up code and added ESP32 support for AdcSampler-based BatteryMonitor and current monitoring

This commit is contained in:
m-RNA
2026-01-01 14:44:06 +08:00
parent 4bc682b4f2
commit a2cce45d78
14 changed files with 611 additions and 260 deletions

View File

@@ -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()

View File

@@ -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 <esp_log.h>
#include <cmath>
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 (~03600 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<int>(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<adc_channel_t>(gpio - 1);
return true;
}
channel = ADC_CHANNEL_0;
return false;
}
#endif // CONFIG_IDF_TARGET_ESP32S3
#endif // CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32

View File

@@ -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 <cstddef>
#include <cstdint>
#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 <vector>
/**
* @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<int> 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

View File

@@ -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

View File

@@ -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 GPIO110 → CH0CH9
if (gpio >= 1 && gpio <= 10)
{
channel = static_cast<adc_channel_t>(gpio - 1);
return true;
}
channel = ADC_CHANNEL_0;
return false;
}
#endif // CONFIG_IDF_TARGET_ESP32S3

View File

@@ -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 <esp_log.h>
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<float>(CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM) / static_cast<float>(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<float>(CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM) / static_cast<float>(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<float>(mv_at_adc) * scale_;
return static_cast<int>(std::lround(battery_mv));
#else
return 0;

View File

@@ -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 <cmath>
/**
* @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)
};

View File

@@ -1,25 +1,45 @@
#include <esp_log.h>
#include <cmath>
#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 <esp_log.h>
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

View File

@@ -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 <cstdint>
#include <memory>
#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

View File

@@ -1,14 +0,0 @@
#include "CurrentMonitor.hpp"
#include <esp_log.h>
static const char *TAG_CM = "[CurrentMonitor]";
void CurrentMonitor::setup()
{
ESP_LOGI(TAG_CM, "LED current monitoring disabled");
}
float CurrentMonitor::getCurrentMilliAmps() const
{
return 0.0f;
}

View File

@@ -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 <esp_log.h>
#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<MonitoringManager *>(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<float>(last_battery_mv_.load());
#endif
return 0.0f;
}

View File

@@ -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 <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <atomic>
#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<float> last_current_ma_{0.0f};
std::atomic<int> last_battery_mv_{0};
CurrentMonitor cm_;
BatteryMonitor bm_;
};

View File

@@ -1,34 +0,0 @@
#include "MonitoringManager.hpp"
#include <esp_log.h>
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;
}

View File

@@ -1,134 +0,0 @@
#include "MonitoringManager.hpp"
#include <cmath>
#include <esp_log.h>
#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<MonitoringManager *>(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<float>(last_battery_mv_.load());
#else
return 0.0f;
#endif
}