Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"}]}`

---

Expand Down Expand Up @@ -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 |
Expand Down
4 changes: 4 additions & 0 deletions components/CommandManager/CommandManager/CommandManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};

Expand Down Expand Up @@ -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); };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ enum class CommandType
GET_LED_DUTY_CYCLE,
GET_SERIAL,
GET_LED_CURRENT,
GET_BATTERY_STATUS,
GET_WHO_AM_I,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
45 changes: 34 additions & 11 deletions components/Monitoring/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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"
)

if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3")
list(APPEND source_files
"Monitoring/CurrentMonitor_esp32s3.cpp"
"Monitoring/MonitoringManager_esp32s3.cpp"
)
else()
# 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/CurrentMonitor_esp32.cpp"
"Monitoring/MonitoringManager_esp32.cpp"
)
"Monitoring/AdcSampler.cpp"
)

# Platform-specific GPIO-to-channel mapping
if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3")
list(APPEND source_files
"Monitoring/AdcSampler_esp32s3.cpp"
)
elseif ("$ENV{IDF_TARGET}" STREQUAL "esp32")
list(APPEND source_files
"Monitoring/AdcSampler_esp32.cpp"
)
endif()
endif()


Expand Down
197 changes: 197 additions & 0 deletions components/Monitoring/Monitoring/AdcSampler.cpp
Original file line number Diff line number Diff line change
@@ -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 (~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<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
Loading