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 | 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..5dbee0d 100644 --- a/components/CommandManager/CommandManager/commands/device_commands.cpp +++ b/components/CommandManager/CommandManager/commands/device_commands.cpp @@ -219,6 +219,31 @@ 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 auto status = mon->getBatteryStatus(); + if (!status.valid) + { + return CommandResult::getErrorResult("Battery voltage unavailable"); + } + + const auto json = nlohmann::json{ + {"voltage_mv", std::format("{:.2f}", static_cast(status.voltage_mv))}, + {"percentage", std::format("{:.1f}", static_cast(status.percentage))}, + }; + 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/components/Monitoring/CMakeLists.txt b/components/Monitoring/CMakeLists.txt index bd394ff..d2a5703 100644 --- a/components/Monitoring/CMakeLists.txt +++ b/components/Monitoring/CMakeLists.txt @@ -1,30 +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/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 new file mode 100644 index 0000000..387a7ba --- /dev/null +++ b/components/Monitoring/Monitoring/AdcSampler.cpp @@ -0,0 +1,197 @@ +/** + * @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) || defined(CONFIG_IDF_TARGET_ESP32) +#include + +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; + } + samples_.assign(window_size, 0); + sample_sum_ = 0; + sample_idx_ = 0; + sample_count_ = 0; + + atten_ = atten; + bitwidth_ = bitwidth; + + // Map GPIO to ADC channel (platform-specific) + if (!map_gpio_to_channel(gpio, unit_, channel_)) + { + 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; + } + + // 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_, + }; + 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 calibration initialized"); + } + else + { + cali_inited_ = false; + ESP_LOGW(TAG, "ADC calibration not available; using raw-to-mV approximation"); + } + + return true; +} + +bool AdcSampler::sampleOnce() +{ + if (!shared_unit_) + { + return false; + } + + int raw = 0; + 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; + } + + int mv = 0; + if (cali_inited_) + { + if (adc_cali_raw_to_voltage(cali_handle_, raw, &mv) != ESP_OK) + { + mv = 0; + } + } + else + { + // 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; + } + } + + // Update moving average filter + 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_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_oneshot_config_channel failed (GPIO %d, CH %d): %s", + gpio, static_cast(channel_), esp_err_to_name(err)); + return false; + } + return true; +} + +#endif // CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32 diff --git a/components/Monitoring/Monitoring/AdcSampler.hpp b/components/Monitoring/Monitoring/AdcSampler.hpp new file mode 100644 index 0000000..a98235c --- /dev/null +++ b/components/Monitoring/Monitoring/AdcSampler.hpp @@ -0,0 +1,119 @@ +#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) || 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(); + + // Non-copyable, non-movable (owns hardware resources) + AdcSampler(const AdcSampler &) = delete; + AdcSampler &operator=(const AdcSampler &) = delete; + AdcSampler(AdcSampler &&) = delete; + AdcSampler &operator=(AdcSampler &&) = delete; + + /** + * @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); + + /** + * @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}; + adc_unit_t unit_{ADC_UNIT_1}; + 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}; + size_t sample_count_{0}; + int filtered_mv_{0}; +}; + +#else +// 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 new file mode 100644 index 0000000..97d2f95 --- /dev/null +++ b/components/Monitoring/Monitoring/BatteryMonitor.cpp @@ -0,0 +1,122 @@ +/** + * @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 = "[BatteryMonitor]"; + +bool BatteryMonitor::setup() +{ +#if CONFIG_MONITORING_BATTERY_ENABLE + if (!AdcSampler::isSupported()) + { + ESP_LOGI(TAG, "Battery monitoring not supported on this target"); + return false; + } + + // 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, "Battery ADC init failed"); + return false; + } + ESP_LOGI(TAG, "Battery monitor enabled (GPIO=%d, scale=%.3f)", CONFIG_MONITORING_BATTERY_ADC_GPIO, scale_); + return true; +#else + ESP_LOGI(TAG, "Battery monitoring disabled by Kconfig"); + return false; +#endif +} + +int BatteryMonitor::getBatteryMilliVolts() const +{ +#if CONFIG_MONITORING_BATTERY_ENABLE + if (!AdcSampler::isSupported()) + return 0; + + if (!adc_.sampleOnce()) + return 0; + + const int mv_at_adc = adc_.getFilteredMilliVolts(); + if (mv_at_adc <= 0) + return 0; + + // Apply voltage divider scaling + const float battery_mv = static_cast(mv_at_adc) * scale_; + return static_cast(std::lround(battery_mv)); +#else + 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 new file mode 100644 index 0000000..defdf63 --- /dev/null +++ b/components/Monitoring/Monitoring/BatteryMonitor.hpp @@ -0,0 +1,121 @@ +#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 +#include +#include +#include "AdcSampler.hpp" +#include "sdkconfig.h" + + +/** + * @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 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 + * - 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(); + + /** + * @brief Read battery voltage (with divider compensation) + * @return Battery voltage in millivolts, 0 on failure + */ + int getBatteryMilliVolts() const; + + /** + * @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 + return AdcSampler::isSupported(); +#else + return false; +#endif + } + +private: + /** + * @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/CurrentMonitor.cpp b/components/Monitoring/Monitoring/CurrentMonitor.cpp new file mode 100644 index 0000000..792b575 --- /dev/null +++ b/components/Monitoring/Monitoring/CurrentMonitor.cpp @@ -0,0 +1,62 @@ +/** + * @file CurrentMonitor.cpp + * @brief Business Logic Layer - Current monitoring implementation + * + * Platform-independent current monitoring logic. + * Uses AdcSampler (BSP layer) for hardware abstraction. + */ + +#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, "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, "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; + + if (!adc_.sampleOnce()) + 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 + + // Physically correct scaling: + // I[mA] = 1000 * Vshunt[mV] / R[mΩ] + return (1000.0f * static_cast(filtered_mv)) / static_cast(shunt_milliohm); +#else + return 0.0f; +#endif +} \ No newline at end of file diff --git a/components/Monitoring/Monitoring/CurrentMonitor.hpp b/components/Monitoring/Monitoring/CurrentMonitor.hpp index f61fa01..b474196 100644 --- a/components/Monitoring/Monitoring/CurrentMonitor.hpp +++ b/components/Monitoring/Monitoring/CurrentMonitor.hpp @@ -1,50 +1,61 @@ #ifndef CURRENT_MONITOR_HPP #define CURRENT_MONITOR_HPP #pragma once -#include -#include -#include -#include "sdkconfig.h" +/** + * @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 "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(); + CurrentMonitor() = default; ~CurrentMonitor() = default; + // Initialize current monitoring hardware 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 + // 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: -#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_; // 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 be2d090..0000000 --- a/components/Monitoring/Monitoring/CurrentMonitor_esp32.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "CurrentMonitor.hpp" -#include - -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"); -} - -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/CurrentMonitor_esp32s3.cpp b/components/Monitoring/Monitoring/CurrentMonitor_esp32s3.cpp deleted file mode 100644 index 478a7ce..0000000 --- a/components/Monitoring/Monitoring/CurrentMonitor_esp32s3.cpp +++ /dev/null @@ -1,179 +0,0 @@ -#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 - -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(); -#else - ESP_LOGI(TAG_CM, "LED current monitoring disabled"); -#endif -} - -float CurrentMonitor::getCurrentMilliAmps() const -{ -#ifdef CONFIG_MONITORING_LED_CURRENT - const int shunt_milliohm = CONFIG_MONITORING_LED_SHUNT_MILLIOHM; // mΩ - if (shunt_milliohm <= 0) - return 0.0f; - // Physically correct scaling: - // I[mA] = 1000 * Vshunt[mV] / R[mΩ] - 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 diff --git a/components/Monitoring/Monitoring/MonitoringManager.cpp b/components/Monitoring/Monitoring/MonitoringManager.cpp new file mode 100644 index 0000000..1f265a3 --- /dev/null +++ b/components/Monitoring/Monitoring/MonitoringManager.cpp @@ -0,0 +1,175 @@ +/** + * @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 auto status = bm_.getBatteryStatus(); + if (status.valid) + { + std::lock_guard lock(battery_mutex_); + last_battery_status_ = status; + } + 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; +} + +BatteryStatus MonitoringManager::getBatteryStatus() const +{ +#if CONFIG_MONITORING_BATTERY_ENABLE + if (BatteryMonitor::isEnabled()) + { + std::lock_guard lock(battery_mutex_); + return last_battery_status_; + } +#endif + return {0, 0.0f, false}; +} diff --git a/components/Monitoring/Monitoring/MonitoringManager.hpp b/components/Monitoring/Monitoring/MonitoringManager.hpp index 7fb6e78..36069e7 100644 --- a/components/Monitoring/Monitoring/MonitoringManager.hpp +++ b/components/Monitoring/Monitoring/MonitoringManager.hpp @@ -1,18 +1,61 @@ #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 +#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 - float getCurrentMilliAmps() const { return last_current_ma_.load(); } + float getCurrentMilliAmps() const; + // Get complete battery status (voltage + percentage + validity) + BatteryStatus getBatteryStatus() const; + + // Check if any monitoring feature is enabled + static constexpr bool isEnabled() + { + return CurrentMonitor::isEnabled() || BatteryMonitor::isEnabled(); + } private: static void taskEntry(void *arg); @@ -20,5 +63,9 @@ private: TaskHandle_t task_{nullptr}; std::atomic last_current_ma_{0.0f}; + BatteryStatus last_battery_status_{0, 0.0f, false}; + mutable std::mutex battery_mutex_; // Protect non-atomic BatteryStatus + 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 e9c4adf..0000000 --- a/components/Monitoring/Monitoring/MonitoringManager_esp32.cpp +++ /dev/null @@ -1,25 +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() -{ -} diff --git a/components/Monitoring/Monitoring/MonitoringManager_esp32s3.cpp b/components/Monitoring/Monitoring/MonitoringManager_esp32s3.cpp deleted file mode 100644 index 2eec518..0000000 --- a/components/Monitoring/Monitoring/MonitoringManager_esp32s3.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "MonitoringManager.hpp" -#include -#include "sdkconfig.h" - -static const char *TAG_MM = "[MonitoringManager]"; - -void MonitoringManager::setup() -{ -#ifdef CONFIG_MONITORING_LED_CURRENT - cm_.setup(); - ESP_LOGI(TAG_MM, "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"); -#endif -} - -void MonitoringManager::start() -{ -#ifdef CONFIG_MONITORING_LED_CURRENT - 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() -{ -#ifdef CONFIG_MONITORING_LED_CURRENT - while (true) - { - float ma = cm_.pollAndGetMilliAmps(); - last_current_ma_.store(ma); - vTaskDelay(pdMS_TO_TICKS(CONFIG_MONITORING_LED_INTERVAL_MS)); - } -#else - vTaskDelete(nullptr); -#endif -} diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index be12760..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" @@ -238,4 +240,56 @@ menu "OpenIris: Monitoring" help Period between samples when background monitoring is active. + config MONITORING_BATTERY_ENABLE + bool "Enable battery voltage monitoring" + 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. + + 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 + 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)" + 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 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 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}")