From 813ebf971289a2e85882f85afc8bafe307b34d65 Mon Sep 17 00:00:00 2001 From: m-RNA Date: Wed, 24 Dec 2025 03:39:42 +0800 Subject: [PATCH 01/12] Add AdcSampler class to support ADC monitoring function --- .../Monitoring/Monitoring/AdcSampler.cpp | 160 ++++++++++++++++++ .../Monitoring/Monitoring/AdcSampler.hpp | 58 +++++++ 2 files changed, 218 insertions(+) create mode 100644 components/Monitoring/Monitoring/AdcSampler.cpp create mode 100644 components/Monitoring/Monitoring/AdcSampler.hpp diff --git a/components/Monitoring/Monitoring/AdcSampler.cpp b/components/Monitoring/Monitoring/AdcSampler.cpp new file mode 100644 index 0000000..3696b5b --- /dev/null +++ b/components/Monitoring/Monitoring/AdcSampler.cpp @@ -0,0 +1,160 @@ +#include "AdcSampler.hpp" + +#if defined(CONFIG_IDF_TARGET_ESP32S3) +#include +#include + +static const char *TAG_ADC = "[AdcSampler]"; + +adc_oneshot_unit_handle_t AdcSampler::shared_unit_ = nullptr; + +AdcSampler::~AdcSampler() +{ + if (cali_handle_) + { + adc_cali_delete_scheme_curve_fitting(cali_handle_); + cali_handle_ = nullptr; + } +} + +bool AdcSampler::init(int gpio, adc_atten_t atten, adc_bitwidth_t bitwidth, size_t window_size) +{ + if (window_size == 0) + { + window_size = 1; + } + samples_.assign(window_size, 0); + sample_sum_ = 0; + sample_idx_ = 0; + sample_count_ = 0; + + atten_ = atten; + bitwidth_ = bitwidth; + + if (!map_gpio_to_channel(gpio, unit_, channel_)) + { + ESP_LOGW(TAG_ADC, "GPIO %d may not be ADC-capable on ESP32-S3", gpio); + } + + if (!ensure_unit()) + { + return false; + } + + if (!configure_channel(gpio, atten, bitwidth)) + { + return false; + } + + // Calibration using curve fitting if available + 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) + { + cali_inited_ = true; + ESP_LOGI(TAG_ADC, "ADC calibration initialized (curve fitting)"); + } + else + { + cali_inited_ = false; + ESP_LOGW(TAG_ADC, "ADC calibration not available; using raw-to-mV approximation"); + } + + return true; +} + +bool AdcSampler::sampleOnce() +{ + if (!shared_unit_) + { + return false; + } + + int raw = 0; + if (adc_oneshot_read(shared_unit_, channel_, &raw) != ESP_OK) + { + return false; + } + + int mv = 0; + if (cali_inited_) + { + if (adc_cali_raw_to_voltage(cali_handle_, raw, &mv) != ESP_OK) + { + mv = 0; + } + } + else + { + // Approximation for 11dB attenuation + mv = (raw * 2450) / 4095; + } + + // Moving average + sample_sum_ -= samples_[sample_idx_]; + samples_[sample_idx_] = mv; + sample_sum_ += mv; + sample_idx_ = (sample_idx_ + 1) % samples_.size(); + if (sample_count_ < samples_.size()) + { + sample_count_++; + } + filtered_mv_ = sample_sum_ / static_cast(sample_count_ > 0 ? sample_count_ : 1); + + return true; +} + +bool AdcSampler::ensure_unit() +{ + if (shared_unit_) + { + return true; + } + + adc_oneshot_unit_init_cfg_t unit_cfg = { + .unit_id = ADC_UNIT_1, + .clk_src = ADC_RTC_CLK_SRC_DEFAULT, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + 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)); + shared_unit_ = nullptr; + return false; + } + return true; +} + +bool AdcSampler::configure_channel(int gpio, adc_atten_t atten, adc_bitwidth_t bitwidth) +{ + adc_oneshot_chan_cfg_t chan_cfg = { + .atten = atten, + .bitwidth = bitwidth, + }; + 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)); + 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 diff --git a/components/Monitoring/Monitoring/AdcSampler.hpp b/components/Monitoring/Monitoring/AdcSampler.hpp new file mode 100644 index 0000000..e68763f --- /dev/null +++ b/components/Monitoring/Monitoring/AdcSampler.hpp @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#include "sdkconfig.h" + +#if defined(CONFIG_IDF_TARGET_ESP32S3) +#include "esp_adc/adc_oneshot.h" +#include "esp_adc/adc_cali.h" +#include "esp_adc/adc_cali_scheme.h" +#include + +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); + + // Perform one conversion, update filtered value. Returns false on failure. + bool sampleOnce(); + + int getFilteredMilliVolts() const { return filtered_mv_; } + +private: + 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). + static adc_oneshot_unit_handle_t shared_unit_; + + adc_cali_handle_t cali_handle_{nullptr}; + bool cali_inited_{false}; + adc_channel_t channel_{ADC_CHANNEL_0}; + adc_unit_t unit_{ADC_UNIT_1}; + adc_atten_t atten_{ADC_ATTEN_DB_12}; + adc_bitwidth_t bitwidth_{ADC_BITWIDTH_DEFAULT}; + + std::vector samples_{}; + int sample_sum_{0}; + size_t sample_idx_{0}; + size_t sample_count_{0}; + int filtered_mv_{0}; +}; + +#else +// Stub for non-ESP32-S3 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; } +}; +#endif From b739bd277544f8195909bd3599476cb99c27955f Mon Sep 17 00:00:00 2001 From: m-RNA Date: Wed, 24 Dec 2025 03:42:00 +0800 Subject: [PATCH 02/12] Add BatteryMonitor class to support battery monitoring --- .../Monitoring/Monitoring/BatteryMonitor.cpp | 44 +++++++++++++++++++ .../Monitoring/Monitoring/BatteryMonitor.hpp | 29 ++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 components/Monitoring/Monitoring/BatteryMonitor.cpp create mode 100644 components/Monitoring/Monitoring/BatteryMonitor.hpp diff --git a/components/Monitoring/Monitoring/BatteryMonitor.cpp b/components/Monitoring/Monitoring/BatteryMonitor.cpp new file mode 100644 index 0000000..0be7d32 --- /dev/null +++ b/components/Monitoring/Monitoring/BatteryMonitor.cpp @@ -0,0 +1,44 @@ +#include "BatteryMonitor.hpp" + +#include + +static const char *TAG_BAT = "[BatteryMonitor]"; + +bool BatteryMonitor::setup() +{ +#if defined(CONFIG_IDF_TARGET_ESP32S3) + if (CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM <= 0) + { + ESP_LOGE(TAG_BAT, "Invalid divider bottom resistor: %d", CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM); + return false; + } + scale_ = 1.0f + static_cast(CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM) / static_cast(CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM); + 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"); + return false; + } + ESP_LOGI(TAG_BAT, "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"); + return false; +#endif +} + +int BatteryMonitor::getBatteryMilliVolts() const +{ +#if defined(CONFIG_IDF_TARGET_ESP32S3) + if (!adc_.sampleOnce()) + return 0; + + const int mv_at_adc = adc_.getFilteredMilliVolts(); + if (mv_at_adc <= 0) + return 0; + + const float battery_mv = mv_at_adc * scale_; + return static_cast(std::lround(battery_mv)); +#else + return 0; +#endif +} diff --git a/components/Monitoring/Monitoring/BatteryMonitor.hpp b/components/Monitoring/Monitoring/BatteryMonitor.hpp new file mode 100644 index 0000000..fd552eb --- /dev/null +++ b/components/Monitoring/Monitoring/BatteryMonitor.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "AdcSampler.hpp" +#include + +class BatteryMonitor +{ +public: + BatteryMonitor() = default; + ~BatteryMonitor() = default; + + bool setup(); + + // Read once, update filter, and return battery voltage in mV (after divider compensation). + int getBatteryMilliVolts() const; + + // Whether monitoring is enabled by Kconfig + static constexpr bool isEnabled() + { +#ifdef CONFIG_MONITORING_BATTERY_ENABLE + return true; +#else + return false; +#endif + } + +private: + float scale_{1.0f}; + mutable AdcSampler adc_; +}; From 973afba994747ae0b84bff9d121248afb12a767d Mon Sep 17 00:00:00 2001 From: m-RNA Date: Wed, 24 Dec 2025 03:42:18 +0800 Subject: [PATCH 03/12] Reconstruct the CurrentMonitor class, remove unused members and methods, and simplify ADC initialization logic --- .../Monitoring/Monitoring/CurrentMonitor.hpp | 24 +-- .../Monitoring/CurrentMonitor_esp32s3.cpp | 171 ++---------------- 2 files changed, 20 insertions(+), 175 deletions(-) diff --git a/components/Monitoring/Monitoring/CurrentMonitor.hpp b/components/Monitoring/Monitoring/CurrentMonitor.hpp index f61fa01..6cfdea0 100644 --- a/components/Monitoring/Monitoring/CurrentMonitor.hpp +++ b/components/Monitoring/Monitoring/CurrentMonitor.hpp @@ -3,25 +3,19 @@ #pragma once #include #include -#include #include "sdkconfig.h" +#include "AdcSampler.hpp" class CurrentMonitor { public: - CurrentMonitor(); + CurrentMonitor() = default; ~CurrentMonitor() = default; void setup(); - void sampleOnce(); - - // Returns filtered voltage in millivolts at shunt (after dividing by gain) - int getFilteredMillivolts() const { return filtered_mv_; } - // Returns current in milliamps computed as Vshunt[mV] / R[mΩ] - float getCurrentMilliAmps() const; // convenience: combined sampling and compute; returns mA - float pollAndGetMilliAmps(); + float getCurrentMilliAmps() const; // Whether monitoring is enabled by Kconfig static constexpr bool isEnabled() @@ -34,17 +28,7 @@ public: } private: -#ifdef CONFIG_MONITORING_LED_CURRENT - void init_adc(); - int read_mv_once(); - int gpio_to_adc_channel(int gpio); -#endif - - int filtered_mv_ = 0; - int sample_sum_ = 0; - std::vector samples_; - size_t sample_idx_ = 0; - size_t sample_count_ = 0; + mutable AdcSampler adc_; }; #endif diff --git a/components/Monitoring/Monitoring/CurrentMonitor_esp32s3.cpp b/components/Monitoring/Monitoring/CurrentMonitor_esp32s3.cpp index 478a7ce..c041a01 100644 --- a/components/Monitoring/Monitoring/CurrentMonitor_esp32s3.cpp +++ b/components/Monitoring/Monitoring/CurrentMonitor_esp32s3.cpp @@ -1,26 +1,17 @@ -#include "CurrentMonitor.hpp" #include #include - -#ifdef CONFIG_MONITORING_LED_CURRENT -#include "esp_adc/adc_oneshot.h" -#include "esp_adc/adc_cali.h" -#include "esp_adc/adc_cali_scheme.h" -#endif +#include "CurrentMonitor.hpp" static const char *TAG_CM = "[CurrentMonitor]"; -CurrentMonitor::CurrentMonitor() -{ -#ifdef CONFIG_MONITORING_LED_CURRENT - samples_.assign(CONFIG_MONITORING_LED_SAMPLES, 0); -#endif -} - void CurrentMonitor::setup() { #ifdef CONFIG_MONITORING_LED_CURRENT - init_adc(); + 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"); + } #else ESP_LOGI(TAG_CM, "LED current monitoring disabled"); #endif @@ -32,148 +23,18 @@ float CurrentMonitor::getCurrentMilliAmps() const const int shunt_milliohm = CONFIG_MONITORING_LED_SHUNT_MILLIOHM; // mΩ if (shunt_milliohm <= 0) return 0.0f; + + if (!adc_.sampleOnce()) + return 0.0f; + + int filtered_mv = adc_.getFilteredMilliVolts(); + if (CONFIG_MONITORING_LED_GAIN > 0) + filtered_mv = filtered_mv / CONFIG_MONITORING_LED_GAIN; // convert back to shunt voltage + // Physically correct scaling: // I[mA] = 1000 * Vshunt[mV] / R[mΩ] - return (1000.0f * static_cast(filtered_mv_)) / static_cast(shunt_milliohm); + return (1000.0f * static_cast(filtered_mv)) / static_cast(shunt_milliohm); #else return 0.0f; #endif -} - -float CurrentMonitor::pollAndGetMilliAmps() -{ - sampleOnce(); - return getCurrentMilliAmps(); -} - -void CurrentMonitor::sampleOnce() -{ -#ifdef CONFIG_MONITORING_LED_CURRENT - int mv = read_mv_once(); - // Divide by analog gain/divider factor to get shunt voltage - if (CONFIG_MONITORING_LED_GAIN > 0) - mv = mv / CONFIG_MONITORING_LED_GAIN; - - // Moving average over N samples - if (samples_.empty()) - { - samples_.assign(CONFIG_MONITORING_LED_SAMPLES, 0); - sample_sum_ = 0; - sample_idx_ = 0; - sample_count_ = 0; - } - - sample_sum_ -= samples_[sample_idx_]; - samples_[sample_idx_] = mv; - sample_sum_ += mv; - sample_idx_ = (sample_idx_ + 1) % samples_.size(); - if (sample_count_ < samples_.size()) - sample_count_++; - - filtered_mv_ = sample_sum_ / static_cast(sample_count_ > 0 ? sample_count_ : 1); -#else - (void)0; -#endif -} - -#ifdef CONFIG_MONITORING_LED_CURRENT - -static adc_oneshot_unit_handle_t s_adc_handle = nullptr; -static adc_cali_handle_t s_cali_handle = nullptr; -static bool s_cali_inited = false; -static adc_channel_t s_channel; -static adc_unit_t s_unit; - -void CurrentMonitor::init_adc() -{ - // Derive ADC unit/channel from GPIO - int gpio = CONFIG_MONITORING_LED_ADC_GPIO; - - esp_err_t err; - adc_oneshot_unit_init_cfg_t unit_cfg = { - .unit_id = ADC_UNIT_1, - }; - err = adc_oneshot_new_unit(&unit_cfg, &s_adc_handle); - if (err != ESP_OK) - { - ESP_LOGE(TAG_CM, "adc_oneshot_new_unit failed: %s", esp_err_to_name(err)); - return; - } - - // Try to map GPIO to ADC channel automatically if helper exists; otherwise guess for ESP32-S3 ADC1 -#ifdef ADC1_GPIO1_CHANNEL - (void)0; // placeholder for potential future macros -#endif - - // Use IO-to-channel helper where available -#ifdef CONFIG_IDF_TARGET_ESP32S3 - // ESP32-S3: ADC1 channels on GPIO1..GPIO10 map to CH0..CH9 - if (gpio >= 1 && gpio <= 10) - { - s_unit = ADC_UNIT_1; - s_channel = static_cast(gpio - 1); - } - else - { - ESP_LOGW(TAG_CM, "Configured GPIO %d may not be ADC-capable on ESP32-S3", gpio); - s_unit = ADC_UNIT_1; - s_channel = ADC_CHANNEL_0; - } -#else - // Fallback: assume ADC1 CH0 - s_unit = ADC_UNIT_1; - s_channel = ADC_CHANNEL_0; -#endif - - adc_oneshot_chan_cfg_t chan_cfg = { - .atten = ADC_ATTEN_DB_11, - .bitwidth = ADC_BITWIDTH_DEFAULT, - }; - err = adc_oneshot_config_channel(s_adc_handle, s_channel, &chan_cfg); - if (err != ESP_OK) - { - ESP_LOGE(TAG_CM, "adc_oneshot_config_channel failed: %s", esp_err_to_name(err)); - } - - // Calibration using curve fitting if available - adc_cali_curve_fitting_config_t cal_cfg = { - .unit_id = s_unit, - .atten = chan_cfg.atten, - .bitwidth = chan_cfg.bitwidth, - }; - if (adc_cali_create_scheme_curve_fitting(&cal_cfg, &s_cali_handle) == ESP_OK) - { - s_cali_inited = true; - ESP_LOGI(TAG_CM, "ADC calibration initialized (curve fitting)"); - } - else - { - s_cali_inited = false; - ESP_LOGW(TAG_CM, "ADC calibration not available; using raw-to-mV approximation"); - } -} - -int CurrentMonitor::read_mv_once() -{ - if (!s_adc_handle) - return 0; - int raw = 0; - if (adc_oneshot_read(s_adc_handle, s_channel, &raw) != ESP_OK) - return 0; - - int mv = 0; - if (s_cali_inited) - { - if (adc_cali_raw_to_voltage(s_cali_handle, raw, &mv) != ESP_OK) - mv = 0; - } - else - { - // Very rough fallback for 11dB attenuation - // Typical full-scale ~2450mV at raw max 4095 (12-bit). IDF defaults may vary. - mv = (raw * 2450) / 4095; - } - return mv; -} - -#endif // CONFIG_MONITORING_LED_CURRENT +} \ No newline at end of file From 2a3f99b10c2e768a203a58f0aed61e89b3eea713 Mon Sep 17 00:00:00 2001 From: m-RNA Date: Wed, 24 Dec 2025 03:47:38 +0800 Subject: [PATCH 04/12] Add battery monitoring to the monitoring manager, update relevant methods and log output --- .../Monitoring/MonitoringManager.hpp | 7 +- .../Monitoring/MonitoringManager_esp32s3.cpp | 94 +++++++++++++++++-- main/openiris_main.cpp | 8 +- 3 files changed, 95 insertions(+), 14 deletions(-) diff --git a/components/Monitoring/Monitoring/MonitoringManager.hpp b/components/Monitoring/Monitoring/MonitoringManager.hpp index 7fb6e78..fe8ee6e 100644 --- a/components/Monitoring/Monitoring/MonitoringManager.hpp +++ b/components/Monitoring/Monitoring/MonitoringManager.hpp @@ -3,6 +3,7 @@ #include #include #include "CurrentMonitor.hpp" +#include "BatteryMonitor.hpp" class MonitoringManager { @@ -12,7 +13,9 @@ public: void stop(); // Latest filtered current in mA - float getCurrentMilliAmps() const { return last_current_ma_.load(); } + float getCurrentMilliAmps() const; + // Latest battery voltage in mV + float getBatteryVoltageMilliVolts() const; private: static void taskEntry(void *arg); @@ -20,5 +23,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_esp32s3.cpp b/components/Monitoring/Monitoring/MonitoringManager_esp32s3.cpp index 2eec518..32f321a 100644 --- a/components/Monitoring/Monitoring/MonitoringManager_esp32s3.cpp +++ b/components/Monitoring/Monitoring/MonitoringManager_esp32s3.cpp @@ -1,26 +1,37 @@ #include "MonitoringManager.hpp" +#include #include #include "sdkconfig.h" static const char *TAG_MM = "[MonitoringManager]"; - void MonitoringManager::setup() { -#ifdef CONFIG_MONITORING_LED_CURRENT +#if CONFIG_MONITORING_LED_CURRENT cm_.setup(); - ESP_LOGI(TAG_MM, "Monitoring enabled. Interval=%dms, Samples=%d, Gain=%d, R=%dmΩ", + 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, "Monitoring disabled by Kconfig"); + 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() { -#ifdef CONFIG_MONITORING_LED_CURRENT +#if CONFIG_MONITORING_LED_CURRENT || CONFIG_MONITORING_BATTERY_ENABLE if (task_ == nullptr) { xTaskCreate(&MonitoringManager::taskEntry, "MonitoringTask", 2048, this, 1, &task_); @@ -45,14 +56,79 @@ void MonitoringManager::taskEntry(void *arg) void MonitoringManager::run() { -#ifdef CONFIG_MONITORING_LED_CURRENT +#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) { - float ma = cm_.pollAndGetMilliAmps(); - last_current_ma_.store(ma); - vTaskDelay(pdMS_TO_TICKS(CONFIG_MONITORING_LED_INTERVAL_MS)); + 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 +} diff --git a/main/openiris_main.cpp b/main/openiris_main.cpp index 95fae32..fb9a945 100644 --- a/main/openiris_main.cpp +++ b/main/openiris_main.cpp @@ -23,7 +23,7 @@ #include #include -#ifdef CONFIG_MONITORING_LED_CURRENT +#if CONFIG_MONITORING_LED_CURRENT || CONFIG_MONITORING_BATTERY_ENABLE #include #endif @@ -72,7 +72,7 @@ UVCStreamManager uvcStream; auto ledManager = std::make_shared(BLINK_GPIO, CONFIG_LED_C_PIN_GPIO, ledStateQueue, deviceConfig); -#ifdef CONFIG_MONITORING_LED_CURRENT +#if CONFIG_MONITORING_LED_CURRENT || CONFIG_MONITORING_BATTERY_ENABLE std::shared_ptr monitoringManager = std::make_shared(); #endif @@ -273,7 +273,7 @@ extern "C" void app_main(void) #endif dependencyRegistry->registerService(DependencyType::led_manager, ledManager); -#ifdef CONFIG_MONITORING_LED_CURRENT +#if CONFIG_MONITORING_LED_CURRENT || CONFIG_MONITORING_BATTERY_ENABLE dependencyRegistry->registerService(DependencyType::monitoring_manager, monitoringManager); #endif @@ -285,7 +285,7 @@ extern "C" void app_main(void) deviceConfig->load(); ledManager->setup(); -#ifdef CONFIG_MONITORING_LED_CURRENT +#if CONFIG_MONITORING_LED_CURRENT || CONFIG_MONITORING_BATTERY_ENABLE monitoringManager->setup(); monitoringManager->start(); #endif From 02cc939d322dd189692310a2b238cf5b8c1c95cb Mon Sep 17 00:00:00 2001 From: m-RNA Date: Wed, 24 Dec 2025 03:49:49 +0800 Subject: [PATCH 05/12] Add command support for obtaining battery status --- .../CommandManager/CommandManager.cpp | 4 ++ .../CommandManager/CommandManager.hpp | 1 + .../commands/device_commands.cpp | 65 +++++++++++++++++++ .../commands/device_commands.hpp | 1 + tools/setup_openiris.py | 19 ++++++ 5 files changed, 90 insertions(+) diff --git a/components/CommandManager/CommandManager/CommandManager.cpp b/components/CommandManager/CommandManager/CommandManager.cpp index f2bb174..61dc9bf 100644 --- a/components/CommandManager/CommandManager/CommandManager.cpp +++ b/components/CommandManager/CommandManager/CommandManager.cpp @@ -26,6 +26,7 @@ std::unordered_map commandTypeMap = { {"get_led_duty_cycle", CommandType::GET_LED_DUTY_CYCLE}, {"get_serial", CommandType::GET_SERIAL}, {"get_led_current", CommandType::GET_LED_CURRENT}, + {"get_battery_status", CommandType::GET_BATTERY_STATUS}, {"get_who_am_i", CommandType::GET_WHO_AM_I}, }; @@ -102,6 +103,9 @@ std::function CommandManager::createCommand(const CommandType t case CommandType::GET_LED_CURRENT: return [this] { return getLEDCurrentCommand(this->registry); }; + case CommandType::GET_BATTERY_STATUS: + return [this] + { return getBatteryStatusCommand(this->registry); }; case CommandType::GET_WHO_AM_I: return [this] { return getInfoCommand(this->registry); }; diff --git a/components/CommandManager/CommandManager/CommandManager.hpp b/components/CommandManager/CommandManager/CommandManager.hpp index 5563570..1ddc5ad 100644 --- a/components/CommandManager/CommandManager/CommandManager.hpp +++ b/components/CommandManager/CommandManager/CommandManager.hpp @@ -47,6 +47,7 @@ enum class CommandType GET_LED_DUTY_CYCLE, GET_SERIAL, GET_LED_CURRENT, + GET_BATTERY_STATUS, GET_WHO_AM_I, }; diff --git a/components/CommandManager/CommandManager/commands/device_commands.cpp b/components/CommandManager/CommandManager/commands/device_commands.cpp index d0dfed8..ccc80f4 100644 --- a/components/CommandManager/CommandManager/commands/device_commands.cpp +++ b/components/CommandManager/CommandManager/commands/device_commands.cpp @@ -1,4 +1,6 @@ #include "device_commands.hpp" +#include +#include #include "LEDManager.hpp" #include "MonitoringManager.hpp" #include "esp_mac.h" @@ -219,6 +221,69 @@ CommandResult getLEDCurrentCommand(std::shared_ptr registry) #endif } +CommandResult getBatteryStatusCommand(std::shared_ptr registry) +{ +#if CONFIG_MONITORING_BATTERY_ENABLE + auto mon = registry->resolve(DependencyType::monitoring_manager); + if (!mon) + { + return CommandResult::getErrorResult("MonitoringManager unavailable"); + } + + const float volts = mon->getBatteryVoltageMilliVolts(); + if (volts <= 0.0f) + { + return CommandResult::getErrorResult("Battery voltage unavailable"); + } + + struct VoltageSOC + { + float voltage_mv; + float soc; + }; + + constexpr std::array lookup = { + VoltageSOC{4200.0f, 100.0f}, VoltageSOC{4060.0f, 90.0f}, VoltageSOC{3980.0f, 80.0f}, VoltageSOC{3920.0f, 70.0f}, + VoltageSOC{3870.0f, 60.0f}, VoltageSOC{3820.0f, 50.0f}, VoltageSOC{3790.0f, 40.0f}, VoltageSOC{3770.0f, 30.0f}, + VoltageSOC{3740.0f, 20.0f}, VoltageSOC{3680.0f, 10.0f}, VoltageSOC{3450.0f, 5.0f}, VoltageSOC{3300.0f, 0.0f}, + }; + + float percent = 0.0f; + if (volts >= lookup.front().voltage_mv) + { + percent = lookup.front().soc; + } + else if (volts <= lookup.back().voltage_mv) + { + percent = lookup.back().soc; + } + else + { + for (size_t index = 0; index < lookup.size() - 1; ++index) + { + const auto high = lookup[index]; + const auto low = lookup[index + 1]; + if (volts <= high.voltage_mv && volts >= low.voltage_mv) + { + const float span = high.voltage_mv - low.voltage_mv; + const float ratio = (volts - low.voltage_mv) / (span > 0.0f ? span : 1.0f); + percent = low.soc + ratio * (high.soc - low.soc); + break; + } + } + } + percent = std::clamp(percent, 0.0f, 100.0f); + + const auto json = nlohmann::json{ + {"voltage_mv", std::format("{:.2f}", static_cast(volts))}, + {"percentage", std::format("{:.1f}", static_cast(percent))}, + }; + return CommandResult::getSuccessResult(json); +#else + return CommandResult::getErrorResult("Battery monitor disabled"); +#endif +} + CommandResult getInfoCommand(std::shared_ptr /*registry*/) { const char *who = CONFIG_GENERAL_BOARD; diff --git a/components/CommandManager/CommandManager/commands/device_commands.hpp b/components/CommandManager/CommandManager/commands/device_commands.hpp index 496aa8f..4a71a78 100644 --- a/components/CommandManager/CommandManager/commands/device_commands.hpp +++ b/components/CommandManager/CommandManager/commands/device_commands.hpp @@ -26,6 +26,7 @@ CommandResult getSerialNumberCommand(std::shared_ptr registr // Monitoring CommandResult getLEDCurrentCommand(std::shared_ptr registry); +CommandResult getBatteryStatusCommand(std::shared_ptr registry); // General info CommandResult getInfoCommand(std::shared_ptr registry); \ No newline at end of file diff --git a/tools/setup_openiris.py b/tools/setup_openiris.py index 3e40872..da22d54 100644 --- a/tools/setup_openiris.py +++ b/tools/setup_openiris.py @@ -232,6 +232,19 @@ def get_led_current(device: OpenIrisDevice) -> dict: } +def get_battery_status(device: OpenIrisDevice) -> dict: + response = device.send_command("get_battery_status") + if has_command_failed(response): + print(f"❌ Failed to get battery status: {response}") + return {"voltage_mv": "unknown", "percentage": "unknown"} + + data = response["results"][0]["result"]["data"] + return { + "voltage_mv": data.get("voltage_mv", "unknown"), + "percentage": data.get("percentage", "unknown"), + } + + def configure_device_name(device: OpenIrisDevice, *args, **kwargs): current_name = get_mdns_name(device) print(f"\n📍 Current device name: {current_name['name']} \n") @@ -340,6 +353,7 @@ def get_settings_summary(device: OpenIrisDevice, *args, **kwargs): ("Info", get_device_info), ("LED", get_led_duty_cycle), ("Current", get_led_current), + ("Battery", get_battery_status), ("Mode", get_device_mode), ("WiFi", get_wifi_status), ] @@ -357,6 +371,11 @@ def get_settings_summary(device: OpenIrisDevice, *args, **kwargs): led_current_ma = current_section.get("led_current_ma") print(f"🔌 LED Current: {led_current_ma} mA") + battery = summary.get("Battery", {}) + voltage_mv = battery.get("voltage_mv") + percentage = battery.get("percentage") + print(f"🔋 Battery: {voltage_mv} mV | {percentage} %") + advertised_name_data = summary.get("AdvertisedName", {}) advertised_name = advertised_name_data.get("name") print(f"📛 Name: {advertised_name}") From 7118593f52bac3c2544d920c8e7d20edb2833476 Mon Sep 17 00:00:00 2001 From: m-RNA Date: Wed, 24 Dec 2025 03:50:28 +0800 Subject: [PATCH 06/12] Add AdcSampler and BatteryMonitor source file support for esp32s3 target --- components/Monitoring/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/Monitoring/CMakeLists.txt b/components/Monitoring/CMakeLists.txt index bd394ff..e030357 100644 --- a/components/Monitoring/CMakeLists.txt +++ b/components/Monitoring/CMakeLists.txt @@ -17,6 +17,8 @@ set( if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3") list(APPEND source_files + "Monitoring/AdcSampler.cpp" + "Monitoring/BatteryMonitor.cpp" "Monitoring/CurrentMonitor_esp32s3.cpp" "Monitoring/MonitoringManager_esp32s3.cpp" ) From c1b15dce5880405084492e541a2a9feb81bb7086 Mon Sep 17 00:00:00 2001 From: m-RNA Date: Wed, 24 Dec 2025 03:56:25 +0800 Subject: [PATCH 07/12] Add battery monitoring sdkconfig options, including voltage monitoring enable, ADC pin, voltage divider and sampling interval settings --- boards/sdkconfig.base_defaults | 17 ++++++++++++ main/Kconfig.projbuild | 51 ++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/boards/sdkconfig.base_defaults b/boards/sdkconfig.base_defaults index a624d2a..fe9db53 100644 --- a/boards/sdkconfig.base_defaults +++ b/boards/sdkconfig.base_defaults @@ -603,6 +603,23 @@ CONFIG_LED_EXTERNAL_PWM_FREQ=5000 CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=100 # end of OpenIris: LED Configuration +# +# OpenIris: Monitoring +# +CONFIG_MONITORING_LED_CURRENT=y +CONFIG_MONITORING_LED_ADC_GPIO=3 +CONFIG_MONITORING_LED_GAIN=11 +CONFIG_MONITORING_LED_SHUNT_MILLIOHM=22000 +CONFIG_MONITORING_LED_SAMPLES=10 +CONFIG_MONITORING_LED_INTERVAL_MS=500 +CONFIG_MONITORING_BATTERY_ENABLE=y +CONFIG_MONITORING_BATTERY_ADC_GPIO=1 +CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM=10000 +CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM=10000 +CONFIG_MONITORING_BATTERY_SAMPLES=10 +CONFIG_MONITORING_BATTERY_INTERVAL_MS=1000 +# end of OpenIris: Monitoring + # # Camera sensor pinout configuration # diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index be12760..d5cfb2a 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -238,4 +238,55 @@ menu "OpenIris: Monitoring" help Period between samples when background monitoring is active. + config MONITORING_BATTERY_ENABLE + bool "Enable battery voltage monitoring" + default y + help + Enables an ADC-based readout of the lithium battery voltage so the software + can report remaining charge in commands or REST endpoints. + + config MONITORING_BATTERY_ADC_GPIO + int "ADC GPIO for battery voltage" + depends on MONITORING_BATTERY_ENABLE + range 0 48 + default 1 + help + GPIO that is wired to the battery sense divider. On ESP32-S3 ADC1 channels + 1..10 map to GPIO1..10; adjust if your board uses a different pin. + + config MONITORING_BATTERY_DIVIDER_R_TOP_OHM + int "Battery divider top resistor (ohms)" + depends on MONITORING_BATTERY_ENABLE + range 1 10000000 + default 10000 + help + The resistor from battery positive to ADC input in the voltage divider. + Set together with MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM to match your + board's sense network. Effective scale = 1 + R_top / R_bottom. + + config MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM + int "Battery divider bottom resistor (ohms)" + depends on MONITORING_BATTERY_ENABLE + range 1 10000000 + default 10000 + help + The resistor from ADC input to ground in the voltage divider. For a 1:1 + divider with 10k/10k, leave the defaults (10 kΩ each). + + config MONITORING_BATTERY_SAMPLES + int "Battery filter window size (samples)" + depends on MONITORING_BATTERY_ENABLE + range 1 200 + default 10 + help + Moving-average window length for battery voltage filtering. + + config MONITORING_BATTERY_INTERVAL_MS + int "Battery sampling interval (ms)" + depends on MONITORING_BATTERY_ENABLE + range 10 60000 + default 1000 + help + Period between background battery voltage samples. + endmenu \ No newline at end of file From 4bc682b4f23f92e49c38361818ba0926c515aaa0 Mon Sep 17 00:00:00 2001 From: m-RNA Date: Thu, 1 Jan 2026 11:49:29 +0800 Subject: [PATCH 08/12] Fix: Let ESP32 compile successfully --- .../Monitoring/Monitoring/CurrentMonitor.hpp | 2 ++ .../Monitoring/CurrentMonitor_esp32.cpp | 28 ------------------- .../Monitoring/MonitoringManager_esp32.cpp | 9 ++++++ 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/components/Monitoring/Monitoring/CurrentMonitor.hpp b/components/Monitoring/Monitoring/CurrentMonitor.hpp index 6cfdea0..d099fa3 100644 --- a/components/Monitoring/Monitoring/CurrentMonitor.hpp +++ b/components/Monitoring/Monitoring/CurrentMonitor.hpp @@ -16,6 +16,8 @@ public: // convenience: combined sampling and compute; returns mA float getCurrentMilliAmps() const; + // + float getBatteryVoltageMilliVolts() const; // Whether monitoring is enabled by Kconfig static constexpr bool isEnabled() diff --git a/components/Monitoring/Monitoring/CurrentMonitor_esp32.cpp b/components/Monitoring/Monitoring/CurrentMonitor_esp32.cpp index be2d090..a571ac6 100644 --- a/components/Monitoring/Monitoring/CurrentMonitor_esp32.cpp +++ b/components/Monitoring/Monitoring/CurrentMonitor_esp32.cpp @@ -3,12 +3,6 @@ static const char *TAG_CM = "[CurrentMonitor]"; -CurrentMonitor::CurrentMonitor() -{ - // empty as esp32 doesn't support this - // but without a separate implementation, the linker will complain :c -} - void CurrentMonitor::setup() { ESP_LOGI(TAG_CM, "LED current monitoring disabled"); @@ -18,25 +12,3 @@ float CurrentMonitor::getCurrentMilliAmps() const { return 0.0f; } - -float CurrentMonitor::pollAndGetMilliAmps() -{ - sampleOnce(); - return getCurrentMilliAmps(); -} - -void CurrentMonitor::sampleOnce() -{ - (void)0; -} - -#ifdef CONFIG_MONITORING_LED_CURRENT -void CurrentMonitor::init_adc() -{ -} - -int CurrentMonitor::read_mv_once() -{ - return 0; -} -#endif \ No newline at end of file diff --git a/components/Monitoring/Monitoring/MonitoringManager_esp32.cpp b/components/Monitoring/Monitoring/MonitoringManager_esp32.cpp index e9c4adf..58f7170 100644 --- a/components/Monitoring/Monitoring/MonitoringManager_esp32.cpp +++ b/components/Monitoring/Monitoring/MonitoringManager_esp32.cpp @@ -23,3 +23,12 @@ 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 From a2cce45d789cb49584be942af726c4798f6d16ac Mon Sep 17 00:00:00 2001 From: m-RNA Date: Thu, 1 Jan 2026 14:44:06 +0800 Subject: [PATCH 09/12] Cleaned up code and added ESP32 support for AdcSampler-based BatteryMonitor and current monitoring --- components/Monitoring/CMakeLists.txt | 47 +++-- .../Monitoring/Monitoring/AdcSampler.cpp | 91 +++++++--- .../Monitoring/Monitoring/AdcSampler.hpp | 77 +++++++- .../Monitoring/AdcSampler_esp32.cpp | 59 ++++++ .../Monitoring/AdcSampler_esp32s3.cpp | 39 ++++ .../Monitoring/Monitoring/BatteryMonitor.cpp | 51 ++++-- .../Monitoring/Monitoring/BatteryMonitor.hpp | 39 +++- ...Monitor_esp32s3.cpp => CurrentMonitor.cpp} | 34 +++- .../Monitoring/Monitoring/CurrentMonitor.hpp | 39 +++- .../Monitoring/CurrentMonitor_esp32.cpp | 14 -- .../Monitoring/MonitoringManager.cpp | 171 ++++++++++++++++++ .../Monitoring/MonitoringManager.hpp | 42 ++++- .../Monitoring/MonitoringManager_esp32.cpp | 34 ---- .../Monitoring/MonitoringManager_esp32s3.cpp | 134 -------------- 14 files changed, 611 insertions(+), 260 deletions(-) create mode 100644 components/Monitoring/Monitoring/AdcSampler_esp32.cpp create mode 100644 components/Monitoring/Monitoring/AdcSampler_esp32s3.cpp rename components/Monitoring/Monitoring/{CurrentMonitor_esp32s3.cpp => CurrentMonitor.cpp} (52%) delete mode 100644 components/Monitoring/Monitoring/CurrentMonitor_esp32.cpp create mode 100644 components/Monitoring/Monitoring/MonitoringManager.cpp delete mode 100644 components/Monitoring/Monitoring/MonitoringManager_esp32.cpp delete mode 100644 components/Monitoring/Monitoring/MonitoringManager_esp32s3.cpp 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 -} From 017474746f630042470109c50d15147c20439ae4 Mon Sep 17 00:00:00 2001 From: m-RNA Date: Thu, 1 Jan 2026 16:35:49 +0800 Subject: [PATCH 10/12] config: disable battery monitoring by default and clean up ADC settings - Set CONFIG_MONITORING_BATTERY_ENABLE to n by default - Update help text for ADC pin configuration in Kconfig - Remove unused config options --- boards/sdkconfig.base_defaults | 17 ----------------- main/Kconfig.projbuild | 9 ++++++--- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/boards/sdkconfig.base_defaults b/boards/sdkconfig.base_defaults index fe9db53..a624d2a 100644 --- a/boards/sdkconfig.base_defaults +++ b/boards/sdkconfig.base_defaults @@ -603,23 +603,6 @@ CONFIG_LED_EXTERNAL_PWM_FREQ=5000 CONFIG_LED_EXTERNAL_PWM_DUTY_CYCLE=100 # end of OpenIris: LED Configuration -# -# OpenIris: Monitoring -# -CONFIG_MONITORING_LED_CURRENT=y -CONFIG_MONITORING_LED_ADC_GPIO=3 -CONFIG_MONITORING_LED_GAIN=11 -CONFIG_MONITORING_LED_SHUNT_MILLIOHM=22000 -CONFIG_MONITORING_LED_SAMPLES=10 -CONFIG_MONITORING_LED_INTERVAL_MS=500 -CONFIG_MONITORING_BATTERY_ENABLE=y -CONFIG_MONITORING_BATTERY_ADC_GPIO=1 -CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM=10000 -CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM=10000 -CONFIG_MONITORING_BATTERY_SAMPLES=10 -CONFIG_MONITORING_BATTERY_INTERVAL_MS=1000 -# end of OpenIris: Monitoring - # # Camera sensor pinout configuration # diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index d5cfb2a..1a81688 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -204,7 +204,9 @@ menu "OpenIris: Monitoring" range 0 48 default 3 help - GPIO connected to the current sense input (ADC1 on ESP32-S3: 1..10 supported). + GPIO connected to the current sense input. On ESP32-S3 ADC1 channels + 0..9 map to GPIO1..10; On ESP32 ADC1 channels 0..3 map to GPIO36..39, + channels 4..7 map to GPIO32..35; adjust if your board uses a different pin. config MONITORING_LED_GAIN int "Analog front-end gain/divider" @@ -240,7 +242,7 @@ menu "OpenIris: Monitoring" config MONITORING_BATTERY_ENABLE bool "Enable battery voltage monitoring" - default y + default n help Enables an ADC-based readout of the lithium battery voltage so the software can report remaining charge in commands or REST endpoints. @@ -252,7 +254,8 @@ menu "OpenIris: Monitoring" default 1 help GPIO that is wired to the battery sense divider. On ESP32-S3 ADC1 channels - 1..10 map to GPIO1..10; adjust if your board uses a different pin. + 0..9 map to GPIO1..10; On ESP32 ADC1 channels 0..3 map to GPIO36..39; + channels 4..7 map to GPIO32..35; adjust if your board uses a different pin. config MONITORING_BATTERY_DIVIDER_R_TOP_OHM int "Battery divider top resistor (ohms)" From 97b910a38cfcbe1d5a2905f9b4574cd553dc10f4 Mon Sep 17 00:00:00 2001 From: m-RNA Date: Thu, 1 Jan 2026 17:14:53 +0800 Subject: [PATCH 11/12] Move the battery power calculation part to the BatteryMonitor class --- .../commands/device_commands.cpp | 48 +----------- .../Monitoring/Monitoring/BatteryMonitor.cpp | 49 +++++++++++++ .../Monitoring/Monitoring/BatteryMonitor.hpp | 73 +++++++++++++++++-- .../Monitoring/MonitoringManager.cpp | 16 ++-- .../Monitoring/MonitoringManager.hpp | 8 +- 5 files changed, 136 insertions(+), 58 deletions(-) diff --git a/components/CommandManager/CommandManager/commands/device_commands.cpp b/components/CommandManager/CommandManager/commands/device_commands.cpp index ccc80f4..5dbee0d 100644 --- a/components/CommandManager/CommandManager/commands/device_commands.cpp +++ b/components/CommandManager/CommandManager/commands/device_commands.cpp @@ -1,6 +1,4 @@ #include "device_commands.hpp" -#include -#include #include "LEDManager.hpp" #include "MonitoringManager.hpp" #include "esp_mac.h" @@ -230,53 +228,15 @@ CommandResult getBatteryStatusCommand(std::shared_ptr regist return CommandResult::getErrorResult("MonitoringManager unavailable"); } - const float volts = mon->getBatteryVoltageMilliVolts(); - if (volts <= 0.0f) + const auto status = mon->getBatteryStatus(); + if (!status.valid) { return CommandResult::getErrorResult("Battery voltage unavailable"); } - struct VoltageSOC - { - float voltage_mv; - float soc; - }; - - constexpr std::array lookup = { - VoltageSOC{4200.0f, 100.0f}, VoltageSOC{4060.0f, 90.0f}, VoltageSOC{3980.0f, 80.0f}, VoltageSOC{3920.0f, 70.0f}, - VoltageSOC{3870.0f, 60.0f}, VoltageSOC{3820.0f, 50.0f}, VoltageSOC{3790.0f, 40.0f}, VoltageSOC{3770.0f, 30.0f}, - VoltageSOC{3740.0f, 20.0f}, VoltageSOC{3680.0f, 10.0f}, VoltageSOC{3450.0f, 5.0f}, VoltageSOC{3300.0f, 0.0f}, - }; - - float percent = 0.0f; - if (volts >= lookup.front().voltage_mv) - { - percent = lookup.front().soc; - } - else if (volts <= lookup.back().voltage_mv) - { - percent = lookup.back().soc; - } - else - { - for (size_t index = 0; index < lookup.size() - 1; ++index) - { - const auto high = lookup[index]; - const auto low = lookup[index + 1]; - if (volts <= high.voltage_mv && volts >= low.voltage_mv) - { - const float span = high.voltage_mv - low.voltage_mv; - const float ratio = (volts - low.voltage_mv) / (span > 0.0f ? span : 1.0f); - percent = low.soc + ratio * (high.soc - low.soc); - break; - } - } - } - percent = std::clamp(percent, 0.0f, 100.0f); - const auto json = nlohmann::json{ - {"voltage_mv", std::format("{:.2f}", static_cast(volts))}, - {"percentage", std::format("{:.1f}", static_cast(percent))}, + {"voltage_mv", std::format("{:.2f}", static_cast(status.voltage_mv))}, + {"percentage", std::format("{:.1f}", static_cast(status.percentage))}, }; return CommandResult::getSuccessResult(json); #else diff --git a/components/Monitoring/Monitoring/BatteryMonitor.cpp b/components/Monitoring/Monitoring/BatteryMonitor.cpp index 9b08031..97d2f95 100644 --- a/components/Monitoring/Monitoring/BatteryMonitor.cpp +++ b/components/Monitoring/Monitoring/BatteryMonitor.cpp @@ -71,3 +71,52 @@ int BatteryMonitor::getBatteryMilliVolts() const return 0; #endif } + +float BatteryMonitor::voltageToPercentage(int voltage_mv) +{ + const float volts = static_cast(voltage_mv); + + // Handle boundary conditions + if (volts >= soc_lookup_.front().voltage_mv) + return soc_lookup_.front().soc; + + if (volts <= soc_lookup_.back().voltage_mv) + return soc_lookup_.back().soc; + + // Linear interpolation between lookup table points + for (size_t i = 0; i < soc_lookup_.size() - 1; ++i) + { + const auto &high = soc_lookup_[i]; + const auto &low = soc_lookup_[i + 1]; + + if (volts <= high.voltage_mv && volts >= low.voltage_mv) + { + const float voltage_span = high.voltage_mv - low.voltage_mv; + if (voltage_span <= 0.0f) + { + return low.soc; + } + const float ratio = (volts - low.voltage_mv) / voltage_span; + return low.soc + ratio * (high.soc - low.soc); + } + } + + return 0.0f; +} + +BatteryStatus BatteryMonitor::getBatteryStatus() const +{ + BatteryStatus status = {0, 0.0f, false}; + +#if CONFIG_MONITORING_BATTERY_ENABLE + const int mv = getBatteryMilliVolts(); + if (mv <= 0) + return status; + + status.voltage_mv = mv; + status.percentage = std::clamp(voltageToPercentage(mv), 0.0f, 100.0f); + status.valid = true; +#endif + + return status; +} diff --git a/components/Monitoring/Monitoring/BatteryMonitor.hpp b/components/Monitoring/Monitoring/BatteryMonitor.hpp index 1f70c82..defdf63 100644 --- a/components/Monitoring/Monitoring/BatteryMonitor.hpp +++ b/components/Monitoring/Monitoring/BatteryMonitor.hpp @@ -14,15 +14,31 @@ * +-----------------------+ */ +#include +#include +#include #include "AdcSampler.hpp" #include "sdkconfig.h" -#include + + +/** + * @struct BatteryStatus + * @brief Battery status information + */ +struct BatteryStatus +{ + int voltage_mv; // Battery voltage in millivolts + float percentage; // State of charge percentage (0-100%) + bool valid; // Whether the reading is valid +}; /** * @class BatteryMonitor - * @brief Monitors battery voltage through a resistor divider + * @brief Monitors battery voltage and calculates state of charge for Li-ion batteries * * Uses AdcSampler (BSP layer) for hardware abstraction. + * Includes voltage-to-SOC lookup table for typical Li-ion/Li-Po batteries. + * * Configuration is done via Kconfig options: * - CONFIG_MONITORING_BATTERY_ENABLE * - CONFIG_MONITORING_BATTERY_ADC_GPIO @@ -39,10 +55,29 @@ public: // Initialize battery monitoring hardware bool setup(); - // Read once, update filter, and return battery voltage in mV (after divider compensation), 0 on failure + /** + * @brief Read battery voltage (with divider compensation) + * @return Battery voltage in millivolts, 0 on failure + */ int getBatteryMilliVolts() const; - // Whether monitoring is enabled by Kconfig and supported by BSP + /** + * @brief Calculate battery state of charge from voltage + * @param voltage_mv Battery voltage in millivolts + * @return State of charge percentage (0-100%) + */ + static float voltageToPercentage(int voltage_mv); + + /** + * @brief Get complete battery status (voltage + percentage) + * @return BatteryStatus struct with voltage, percentage, and validity + */ + BatteryStatus getBatteryStatus() const; + + /** + * @brief Check if battery monitoring is enabled and supported + * @return true if enabled and ADC is supported + */ static constexpr bool isEnabled() { #ifdef CONFIG_MONITORING_BATTERY_ENABLE @@ -53,6 +88,34 @@ public: } private: - float scale_{1.0f}; // Voltage divider scaling factor + /** + * @brief Li-ion/Li-Po voltage to SOC lookup table entry + */ + struct VoltageSOC + { + float voltage_mv; + float soc; + }; + + /** + * @brief Typical Li-ion single cell discharge curve lookup table + * Based on typical 3.7V nominal Li-ion/Li-Po cell characteristics + */ + static constexpr std::array soc_lookup_ = {{ + {4200.0f, 100.0f}, // Fully charged + {4060.0f, 90.0f}, + {3980.0f, 80.0f}, + {3920.0f, 70.0f}, + {3870.0f, 60.0f}, + {3820.0f, 50.0f}, + {3790.0f, 40.0f}, + {3770.0f, 30.0f}, + {3740.0f, 20.0f}, + {3680.0f, 10.0f}, + {3450.0f, 5.0f}, // Low battery warning + {3300.0f, 0.0f}, // Empty / cutoff voltage + }}; + + float scale_{1.0f}; // Voltage divider scaling factor mutable AdcSampler adc_; // ADC sampler instance (BSP layer) }; diff --git a/components/Monitoring/Monitoring/MonitoringManager.cpp b/components/Monitoring/Monitoring/MonitoringManager.cpp index d5d29d8..1f265a3 100644 --- a/components/Monitoring/Monitoring/MonitoringManager.cpp +++ b/components/Monitoring/Monitoring/MonitoringManager.cpp @@ -127,10 +127,11 @@ void MonitoringManager::run() #if CONFIG_MONITORING_BATTERY_ENABLE if (BatteryMonitor::isEnabled() && now_tick >= next_tick_bat) { - const int mv = bm_.getBatteryMilliVolts(); - if (mv > 0) + const auto status = bm_.getBatteryStatus(); + if (status.valid) { - last_battery_mv_.store(mv); + std::lock_guard lock(battery_mutex_); + last_battery_status_ = status; } next_tick_bat = now_tick + batt_period; } @@ -161,11 +162,14 @@ float MonitoringManager::getCurrentMilliAmps() const return 0.0f; } -float MonitoringManager::getBatteryVoltageMilliVolts() const +BatteryStatus MonitoringManager::getBatteryStatus() const { #if CONFIG_MONITORING_BATTERY_ENABLE if (BatteryMonitor::isEnabled()) - return static_cast(last_battery_mv_.load()); + { + std::lock_guard lock(battery_mutex_); + return last_battery_status_; + } #endif - return 0.0f; + return {0, 0.0f, false}; } diff --git a/components/Monitoring/Monitoring/MonitoringManager.hpp b/components/Monitoring/Monitoring/MonitoringManager.hpp index 2db44b8..36069e7 100644 --- a/components/Monitoring/Monitoring/MonitoringManager.hpp +++ b/components/Monitoring/Monitoring/MonitoringManager.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include "BatteryMonitor.hpp" #include "CurrentMonitor.hpp" @@ -47,8 +48,8 @@ public: // Latest filtered current in mA float getCurrentMilliAmps() const; - // Latest battery voltage in mV - float getBatteryVoltageMilliVolts() const; + // Get complete battery status (voltage + percentage + validity) + BatteryStatus getBatteryStatus() const; // Check if any monitoring feature is enabled static constexpr bool isEnabled() @@ -62,7 +63,8 @@ private: TaskHandle_t task_{nullptr}; std::atomic last_current_ma_{0.0f}; - std::atomic last_battery_mv_{0}; + BatteryStatus last_battery_status_{0, 0.0f, false}; + mutable std::mutex battery_mutex_; // Protect non-atomic BatteryStatus CurrentMonitor cm_; BatteryMonitor bm_; From 80599b48ee301cf88e38e53d794e3233199a1c7b Mon Sep 17 00:00:00 2001 From: m-RNA Date: Thu, 1 Jan 2026 21:46:29 +0800 Subject: [PATCH 12/12] docs: update README to include battery monitoring features and commands --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 310600f..2f54b4c 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Firmware and tools for OpenIris — Wi‑Fi, UVC streaming, and a Python setup C - `tools/setup_openiris.py` — interactive CLI for Wi‑Fi, MDNS/Name, Mode, LED PWM, Logs, and a Settings Summary - Composite USB (UVC + CDC) when UVC mode is enabled (`GENERAL_INCLUDE_UVC_MODE`) for simultaneous video streaming and command channel - LED current monitoring (if enabled via `MONITORING_LED_CURRENT`) with filtered mA readings +- Battery voltage monitoring (if enabled via `MONITORING_BATTERY_ENABLE`) with Li-ion SOC percentage calculation - Configurable debug LED + external IR LED control with optional error mirroring (`LED_DEBUG_ENABLE`, `LED_EXTERNAL_AS_DEBUG`) - Auto‑discovered per‑board configuration overlays under `boards/` - Command framework (JSON over serial / CDC / REST) for mode switching, Wi‑Fi config, OTA credentials, LED brightness, info & monitoring @@ -158,6 +159,8 @@ Runtime override: If the setup CLI (or a JSON command) provides a new device nam `{"commands":[{"command":"switch_mode","data":{"mode":"uvc"}}]}` then reboot. - Read filtered LED current (if enabled): `{"commands":[{"command":"get_led_current"}]}` +- Read battery status (if enabled): + `{"commands":[{"command":"get_battery_status"}]}` --- @@ -241,10 +244,26 @@ Responses are JSON blobs flushed immediately. --- -### Monitoring (LED Current) +### Monitoring (LED Current & Battery) + +**LED Current Monitoring** Enabled with `MONITORING_LED_CURRENT=y` plus shunt/gain settings. The task samples every `CONFIG_MONITORING_LED_INTERVAL_MS` ms and maintains a filtered moving average over `CONFIG_MONITORING_LED_SAMPLES` samples. Use `get_led_current` command to query. +**Battery Monitoring** + +Enabled with `MONITORING_BATTERY_ENABLE=y`. Supports voltage divider configuration for measuring Li-ion/Li-Po battery voltage: + +| Kconfig | Description | +|---------|-------------| +| MONITORING_BATTERY_ADC_GPIO | GPIO pin connected to voltage divider output | +| MONITORING_BATTERY_DIVIDER_R_TOP_OHM | Top resistor value (battery side) | +| MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM | Bottom resistor value (GND side) | +| MONITORING_BATTERY_INTERVAL_MS | Sampling interval in milliseconds | +| MONITORING_BATTERY_SAMPLES | Moving average window size | + +The firmware includes a Li-ion discharge curve lookup table for SOC (State of Charge) percentage calculation with linear interpolation. Use `get_battery_status` command to query voltage (mV) and percentage (%). + ### Debug & External LED Configuration | Kconfig | Effect |