Merge pull request #28 from m-RNA/feature/add-battery-monitor

Add AdcSampler & BatteryMonitor
This commit is contained in:
Lorow
2026-01-06 19:12:58 +01:00
committed by GitHub
23 changed files with 1142 additions and 348 deletions

View File

@@ -15,6 +15,7 @@ Firmware and tools for OpenIris — WiFi, UVC streaming, and a Python setup C
- `tools/setup_openiris.py` — interactive CLI for WiFi, 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`)
- Autodiscovered perboard configuration overlays under `boards/`
- Command framework (JSON over serial / CDC / REST) for mode switching, WiFi 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 |

View File

@@ -26,6 +26,7 @@ std::unordered_map<std::string, CommandType> 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<CommandResult()> 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); };

View File

@@ -47,6 +47,7 @@ enum class CommandType
GET_LED_DUTY_CYCLE,
GET_SERIAL,
GET_LED_CURRENT,
GET_BATTERY_STATUS,
GET_WHO_AM_I,
};

View File

@@ -219,6 +219,31 @@ CommandResult getLEDCurrentCommand(std::shared_ptr<DependencyRegistry> registry)
#endif
}
CommandResult getBatteryStatusCommand(std::shared_ptr<DependencyRegistry> registry)
{
#if CONFIG_MONITORING_BATTERY_ENABLE
auto mon = registry->resolve<MonitoringManager>(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<double>(status.voltage_mv))},
{"percentage", std::format("{:.1f}", static_cast<double>(status.percentage))},
};
return CommandResult::getSuccessResult(json);
#else
return CommandResult::getErrorResult("Battery monitor disabled");
#endif
}
CommandResult getInfoCommand(std::shared_ptr<DependencyRegistry> /*registry*/)
{
const char *who = CONFIG_GENERAL_BOARD;

View File

@@ -26,6 +26,7 @@ CommandResult getSerialNumberCommand(std::shared_ptr<DependencyRegistry> registr
// Monitoring
CommandResult getLEDCurrentCommand(std::shared_ptr<DependencyRegistry> registry);
CommandResult getBatteryStatusCommand(std::shared_ptr<DependencyRegistry> registry);
// General info
CommandResult getInfoCommand(std::shared_ptr<DependencyRegistry> registry);

View File

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

View File

@@ -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 <esp_log.h>
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 (~03600 mV range)
// Full-scale raw = (1 << bitwidth_) - 1
// For 12-bit: max raw = 4095 → ~3600 mV
int full_scale_mv = 3600;
int max_raw = (1 << bitwidth_) - 1;
if (max_raw > 0)
{
mv = (raw * full_scale_mv) / max_raw;
}
else
{
mv = 0;
}
}
// 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<int>(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<int>(channel_), esp_err_to_name(err));
return false;
}
return true;
}
#endif // CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32

View File

@@ -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 <cstddef>
#include <cstdint>
#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 <vector>
/**
* @class AdcSampler
* @brief Hardware abstraction layer for ADC sampling with moving average filter
*
* This class provides a unified interface for ADC sampling across different
* ESP32 variants. Platform-specific GPIO-to-channel mapping is handled internally.
*/
class AdcSampler
{
public:
AdcSampler() = default;
~AdcSampler();
// 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<int> 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

View File

@@ -0,0 +1,59 @@
/**
* @file AdcSampler_esp32.cpp
* @brief BSP Layer - ESP32 specific GPIO to ADC channel mapping
*
* ESP32 ADC1 GPIO mapping:
* - GPIO32 → ADC1_CH4
* - GPIO33 → ADC1_CH5
* - GPIO34 → ADC1_CH6
* - GPIO35 → ADC1_CH7
* - GPIO36 → ADC1_CH0
* - GPIO37 → ADC1_CH1
* - GPIO38 → ADC1_CH2
* - GPIO39 → ADC1_CH3
*
* Note: ADC2 is not used to avoid conflicts with Wi-Fi.
*/
#include "AdcSampler.hpp"
#if defined(CONFIG_IDF_TARGET_ESP32)
bool AdcSampler::map_gpio_to_channel(int gpio, adc_unit_t &unit, adc_channel_t &channel)
{
unit = ADC_UNIT_1; // Only use ADC1 to avoid Wi-Fi conflict
// ESP32: ADC1 GPIO mapping (GPIO32-39)
switch (gpio)
{
case 36:
channel = ADC_CHANNEL_0;
return true;
case 37:
channel = ADC_CHANNEL_1;
return true;
case 38:
channel = ADC_CHANNEL_2;
return true;
case 39:
channel = ADC_CHANNEL_3;
return true;
case 32:
channel = ADC_CHANNEL_4;
return true;
case 33:
channel = ADC_CHANNEL_5;
return true;
case 34:
channel = ADC_CHANNEL_6;
return true;
case 35:
channel = ADC_CHANNEL_7;
return true;
default:
channel = ADC_CHANNEL_0;
return false;
}
}
#endif // CONFIG_IDF_TARGET_ESP32

View File

@@ -0,0 +1,39 @@
/**
* @file AdcSampler_esp32s3.cpp
* @brief BSP Layer - ESP32-S3 specific GPIO to ADC channel mapping
*
* ESP32-S3 ADC1 GPIO mapping:
* - GPIO1 → ADC1_CH0
* - GPIO2 → ADC1_CH1
* - GPIO3 → ADC1_CH2
* - GPIO4 → ADC1_CH3
* - GPIO5 → ADC1_CH4
* - GPIO6 → ADC1_CH5
* - GPIO7 → ADC1_CH6
* - GPIO8 → ADC1_CH7
* - GPIO9 → ADC1_CH8
* - GPIO10 → ADC1_CH9
*
* Note: ADC2 is not used to avoid conflicts with Wi-Fi.
*/
#include "AdcSampler.hpp"
#if defined(CONFIG_IDF_TARGET_ESP32S3)
bool AdcSampler::map_gpio_to_channel(int gpio, adc_unit_t &unit, adc_channel_t &channel)
{
unit = ADC_UNIT_1; // Only use ADC1 to avoid Wi-Fi conflict
// ESP32-S3: ADC1 on GPIO110 → CH0CH9
if (gpio >= 1 && gpio <= 10)
{
channel = static_cast<adc_channel_t>(gpio - 1);
return true;
}
channel = ADC_CHANNEL_0;
return false;
}
#endif // CONFIG_IDF_TARGET_ESP32S3

View File

@@ -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 <esp_log.h>
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<float>(CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM) / static_cast<float>(CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM);
}
// Initialize ADC sampler (BSP layer)
if (!adc_.init(CONFIG_MONITORING_BATTERY_ADC_GPIO, ADC_ATTEN_DB_12, ADC_BITWIDTH_DEFAULT, CONFIG_MONITORING_BATTERY_SAMPLES))
{
ESP_LOGE(TAG, "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<float>(mv_at_adc) * scale_;
return static_cast<int>(std::lround(battery_mv));
#else
return 0;
#endif
}
float BatteryMonitor::voltageToPercentage(int voltage_mv)
{
const float volts = static_cast<float>(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;
}

View File

@@ -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 <algorithm>
#include <array>
#include <cmath>
#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<VoltageSOC, 12> 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)
};

View File

@@ -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 <esp_log.h>
static const char *TAG = "[CurrentMonitor]";
void CurrentMonitor::setup()
{
#ifdef CONFIG_MONITORING_LED_CURRENT
if (!AdcSampler::isSupported())
{
ESP_LOGI(TAG, "LED current monitoring not supported on this target");
return;
}
const bool ok = adc_.init(CONFIG_MONITORING_LED_ADC_GPIO, ADC_ATTEN_DB_12, ADC_BITWIDTH_DEFAULT, CONFIG_MONITORING_LED_SAMPLES);
if (!ok)
{
ESP_LOGE(TAG, "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<float>(filtered_mv)) / static_cast<float>(shunt_milliohm);
#else
return 0.0f;
#endif
}

View File

@@ -1,50 +1,61 @@
#ifndef CURRENT_MONITOR_HPP
#define CURRENT_MONITOR_HPP
#pragma once
#include <cstdint>
#include <memory>
#include <vector>
#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 <cstdint>
#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<int> samples_;
size_t sample_idx_ = 0;
size_t sample_count_ = 0;
mutable AdcSampler adc_; // ADC sampler instance (BSP layer)
};
#endif

View File

@@ -1,42 +0,0 @@
#include "CurrentMonitor.hpp"
#include <esp_log.h>
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

View File

@@ -1,179 +0,0 @@
#include "CurrentMonitor.hpp"
#include <esp_log.h>
#include <cmath>
#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<float>(filtered_mv_)) / static_cast<float>(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<int>(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<adc_channel_t>(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

View File

@@ -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 <esp_log.h>
#include "sdkconfig.h"
static const char *TAG = "[MonitoringManager]";
void MonitoringManager::setup()
{
#if CONFIG_MONITORING_LED_CURRENT
if (CurrentMonitor::isEnabled())
{
cm_.setup();
ESP_LOGI(TAG, "LED current monitoring enabled. Interval=%dms, Samples=%d, Gain=%d, R=%dmΩ",
CONFIG_MONITORING_LED_INTERVAL_MS,
CONFIG_MONITORING_LED_SAMPLES,
CONFIG_MONITORING_LED_GAIN,
CONFIG_MONITORING_LED_SHUNT_MILLIOHM);
}
else
{
ESP_LOGI(TAG, "LED current monitoring not supported on this target");
}
#else
ESP_LOGI(TAG, "LED current monitoring disabled by Kconfig");
#endif
#if CONFIG_MONITORING_BATTERY_ENABLE
if (BatteryMonitor::isEnabled())
{
bm_.setup();
ESP_LOGI(TAG, "Battery monitoring enabled. Interval=%dms, Samples=%d, R-Top=%dΩ, R-Bottom=%dΩ",
CONFIG_MONITORING_BATTERY_INTERVAL_MS,
CONFIG_MONITORING_BATTERY_SAMPLES,
CONFIG_MONITORING_BATTERY_DIVIDER_R_TOP_OHM,
CONFIG_MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM);
}
else
{
ESP_LOGI(TAG, "Battery monitoring not supported on this target");
}
#else
ESP_LOGI(TAG, "Battery monitoring disabled by Kconfig");
#endif
}
void MonitoringManager::start()
{
if (!isEnabled())
{
ESP_LOGI(TAG, "No monitoring features enabled, task not started");
return;
}
if (task_ == nullptr)
{
xTaskCreate(&MonitoringManager::taskEntry, "MonitoringTask", 2048, this, 1, &task_);
ESP_LOGI(TAG, "Monitoring task started");
}
}
void MonitoringManager::stop()
{
if (task_)
{
TaskHandle_t toDelete = task_;
task_ = nullptr;
vTaskDelete(toDelete);
ESP_LOGI(TAG, "Monitoring task stopped");
}
}
void MonitoringManager::taskEntry(void *arg)
{
static_cast<MonitoringManager *>(arg)->run();
}
void MonitoringManager::run()
{
if (!isEnabled())
{
vTaskDelete(nullptr);
return;
}
TickType_t now_tick = xTaskGetTickCount();
#if CONFIG_MONITORING_LED_CURRENT
TickType_t next_tick_led = now_tick;
const TickType_t led_period = pdMS_TO_TICKS(CONFIG_MONITORING_LED_INTERVAL_MS);
#endif
#if CONFIG_MONITORING_BATTERY_ENABLE
TickType_t next_tick_bat = now_tick;
const TickType_t batt_period = pdMS_TO_TICKS(CONFIG_MONITORING_BATTERY_INTERVAL_MS);
#endif
while (true)
{
now_tick = xTaskGetTickCount();
TickType_t wait_ticks = pdMS_TO_TICKS(50); // Default wait time
#if CONFIG_MONITORING_LED_CURRENT
if (CurrentMonitor::isEnabled() && now_tick >= next_tick_led)
{
float ma = cm_.getCurrentMilliAmps();
last_current_ma_.store(ma);
next_tick_led = now_tick + led_period;
}
if (CurrentMonitor::isEnabled())
{
TickType_t to_led = (next_tick_led > now_tick) ? (next_tick_led - now_tick) : 1;
if (to_led < wait_ticks)
{
wait_ticks = to_led;
}
}
#endif
#if CONFIG_MONITORING_BATTERY_ENABLE
if (BatteryMonitor::isEnabled() && now_tick >= next_tick_bat)
{
const auto status = bm_.getBatteryStatus();
if (status.valid)
{
std::lock_guard<std::mutex> 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<std::mutex> lock(battery_mutex_);
return last_battery_status_;
}
#endif
return {0, 0.0f, false};
}

View File

@@ -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 <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <atomic>
#include <mutex>
#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<float> 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_;
};

View File

@@ -1,25 +0,0 @@
#include "MonitoringManager.hpp"
#include <esp_log.h>
static const char *TAG_MM = "[MonitoringManager]";
void MonitoringManager::setup()
{
ESP_LOGI(TAG_MM, "Monitoring disabled by Kconfig");
}
void MonitoringManager::start()
{
}
void MonitoringManager::stop()
{
}
void MonitoringManager::taskEntry(void *arg)
{
}
void MonitoringManager::run()
{
}

View File

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

View File

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

View File

@@ -23,7 +23,7 @@
#include <RestAPI.hpp>
#include <main_globals.hpp>
#ifdef CONFIG_MONITORING_LED_CURRENT
#if CONFIG_MONITORING_LED_CURRENT || CONFIG_MONITORING_BATTERY_ENABLE
#include <MonitoringManager.hpp>
#endif
@@ -72,7 +72,7 @@ UVCStreamManager uvcStream;
auto ledManager = std::make_shared<LEDManager>(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> monitoringManager = std::make_shared<MonitoringManager>();
#endif
@@ -273,7 +273,7 @@ extern "C" void app_main(void)
#endif
dependencyRegistry->registerService<LEDManager>(DependencyType::led_manager, ledManager);
#ifdef CONFIG_MONITORING_LED_CURRENT
#if CONFIG_MONITORING_LED_CURRENT || CONFIG_MONITORING_BATTERY_ENABLE
dependencyRegistry->registerService<MonitoringManager>(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

View File

@@ -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}")