Skip to content

Commit 567d3eb

Browse files
authored
Merge pull request #28 from m-RNA/feature/add-battery-monitor
Add AdcSampler & BatteryMonitor
2 parents 347d1d6 + 80599b4 commit 567d3eb

23 files changed

Lines changed: 1141 additions & 347 deletions

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Firmware and tools for OpenIris — Wi‑Fi, UVC streaming, and a Python setup C
1515
- `tools/setup_openiris.py` — interactive CLI for Wi‑Fi, MDNS/Name, Mode, LED PWM, Logs, and a Settings Summary
1616
- Composite USB (UVC + CDC) when UVC mode is enabled (`GENERAL_INCLUDE_UVC_MODE`) for simultaneous video streaming and command channel
1717
- LED current monitoring (if enabled via `MONITORING_LED_CURRENT`) with filtered mA readings
18+
- Battery voltage monitoring (if enabled via `MONITORING_BATTERY_ENABLE`) with Li-ion SOC percentage calculation
1819
- Configurable debug LED + external IR LED control with optional error mirroring (`LED_DEBUG_ENABLE`, `LED_EXTERNAL_AS_DEBUG`)
1920
- Auto‑discovered per‑board configuration overlays under `boards/`
2021
- 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
158159
`{"commands":[{"command":"switch_mode","data":{"mode":"uvc"}}]}` then reboot.
159160
- Read filtered LED current (if enabled):
160161
`{"commands":[{"command":"get_led_current"}]}`
162+
- Read battery status (if enabled):
163+
`{"commands":[{"command":"get_battery_status"}]}`
161164

162165
---
163166

@@ -241,10 +244,26 @@ Responses are JSON blobs flushed immediately.
241244

242245
---
243246

244-
### Monitoring (LED Current)
247+
### Monitoring (LED Current & Battery)
248+
249+
**LED Current Monitoring**
245250

246251
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.
247252

253+
**Battery Monitoring**
254+
255+
Enabled with `MONITORING_BATTERY_ENABLE=y`. Supports voltage divider configuration for measuring Li-ion/Li-Po battery voltage:
256+
257+
| Kconfig | Description |
258+
|---------|-------------|
259+
| MONITORING_BATTERY_ADC_GPIO | GPIO pin connected to voltage divider output |
260+
| MONITORING_BATTERY_DIVIDER_R_TOP_OHM | Top resistor value (battery side) |
261+
| MONITORING_BATTERY_DIVIDER_R_BOTTOM_OHM | Bottom resistor value (GND side) |
262+
| MONITORING_BATTERY_INTERVAL_MS | Sampling interval in milliseconds |
263+
| MONITORING_BATTERY_SAMPLES | Moving average window size |
264+
265+
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 (%).
266+
248267
### Debug & External LED Configuration
249268

250269
| Kconfig | Effect |

components/CommandManager/CommandManager/CommandManager.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ std::unordered_map<std::string, CommandType> commandTypeMap = {
2626
{"get_led_duty_cycle", CommandType::GET_LED_DUTY_CYCLE},
2727
{"get_serial", CommandType::GET_SERIAL},
2828
{"get_led_current", CommandType::GET_LED_CURRENT},
29+
{"get_battery_status", CommandType::GET_BATTERY_STATUS},
2930
{"get_who_am_i", CommandType::GET_WHO_AM_I},
3031
};
3132

@@ -102,6 +103,9 @@ std::function<CommandResult()> CommandManager::createCommand(const CommandType t
102103
case CommandType::GET_LED_CURRENT:
103104
return [this]
104105
{ return getLEDCurrentCommand(this->registry); };
106+
case CommandType::GET_BATTERY_STATUS:
107+
return [this]
108+
{ return getBatteryStatusCommand(this->registry); };
105109
case CommandType::GET_WHO_AM_I:
106110
return [this]
107111
{ return getInfoCommand(this->registry); };

components/CommandManager/CommandManager/CommandManager.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ enum class CommandType
4747
GET_LED_DUTY_CYCLE,
4848
GET_SERIAL,
4949
GET_LED_CURRENT,
50+
GET_BATTERY_STATUS,
5051
GET_WHO_AM_I,
5152
};
5253

components/CommandManager/CommandManager/commands/device_commands.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,31 @@ CommandResult getLEDCurrentCommand(std::shared_ptr<DependencyRegistry> registry)
219219
#endif
220220
}
221221

222+
CommandResult getBatteryStatusCommand(std::shared_ptr<DependencyRegistry> registry)
223+
{
224+
#if CONFIG_MONITORING_BATTERY_ENABLE
225+
auto mon = registry->resolve<MonitoringManager>(DependencyType::monitoring_manager);
226+
if (!mon)
227+
{
228+
return CommandResult::getErrorResult("MonitoringManager unavailable");
229+
}
230+
231+
const auto status = mon->getBatteryStatus();
232+
if (!status.valid)
233+
{
234+
return CommandResult::getErrorResult("Battery voltage unavailable");
235+
}
236+
237+
const auto json = nlohmann::json{
238+
{"voltage_mv", std::format("{:.2f}", static_cast<double>(status.voltage_mv))},
239+
{"percentage", std::format("{:.1f}", static_cast<double>(status.percentage))},
240+
};
241+
return CommandResult::getSuccessResult(json);
242+
#else
243+
return CommandResult::getErrorResult("Battery monitor disabled");
244+
#endif
245+
}
246+
222247
CommandResult getInfoCommand(std::shared_ptr<DependencyRegistry> /*registry*/)
223248
{
224249
const char *who = CONFIG_GENERAL_BOARD;

components/CommandManager/CommandManager/commands/device_commands.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ CommandResult getSerialNumberCommand(std::shared_ptr<DependencyRegistry> registr
2626

2727
// Monitoring
2828
CommandResult getLEDCurrentCommand(std::shared_ptr<DependencyRegistry> registry);
29+
CommandResult getBatteryStatusCommand(std::shared_ptr<DependencyRegistry> registry);
2930

3031
// General info
3132
CommandResult getInfoCommand(std::shared_ptr<DependencyRegistry> registry);

components/Monitoring/CMakeLists.txt

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,53 @@
1+
# Architecture:
2+
# +-----------------------+
3+
# | MonitoringManager | ← High-level coordinator
4+
# +-----------------------+
5+
# | BatteryMonitor | ← Battery logic (platform-independent)
6+
# | CurrentMonitor | ← Current logic (platform-independent)
7+
# +-----------------------+
8+
# | AdcSampler | ← BSP: Unified ADC sampling interface
9+
# +-----------------------+
10+
# | ESP-IDF ADC HAL | ← Espressif official driver
11+
# +-----------------------+
12+
113
set(
214
requires
315
Helpers
416
)
517

6-
if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3")
18+
# Platform-specific dependencies
19+
if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3" OR "$ENV{IDF_TARGET}" STREQUAL "esp32")
720
list(APPEND requires
821
driver
922
esp_adc
1023
)
1124
endif()
1225

26+
# Common source files (platform-independent business logic)
1327
set(
1428
source_files
15-
""
29+
"Monitoring/MonitoringManager.cpp"
30+
"Monitoring/BatteryMonitor.cpp"
31+
"Monitoring/CurrentMonitor.cpp"
1632
)
1733

18-
if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3")
19-
list(APPEND source_files
20-
"Monitoring/CurrentMonitor_esp32s3.cpp"
21-
"Monitoring/MonitoringManager_esp32s3.cpp"
22-
)
23-
else()
34+
# BSP Layer: ADC sampler implementation
35+
if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3" OR "$ENV{IDF_TARGET}" STREQUAL "esp32")
36+
# Common ADC implementation
2437
list(APPEND source_files
25-
"Monitoring/CurrentMonitor_esp32.cpp"
26-
"Monitoring/MonitoringManager_esp32.cpp"
27-
)
38+
"Monitoring/AdcSampler.cpp"
39+
)
40+
41+
# Platform-specific GPIO-to-channel mapping
42+
if ("$ENV{IDF_TARGET}" STREQUAL "esp32s3")
43+
list(APPEND source_files
44+
"Monitoring/AdcSampler_esp32s3.cpp"
45+
)
46+
elseif ("$ENV{IDF_TARGET}" STREQUAL "esp32")
47+
list(APPEND source_files
48+
"Monitoring/AdcSampler_esp32.cpp"
49+
)
50+
endif()
2851
endif()
2952

3053

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/**
2+
* @file AdcSampler.cpp
3+
* @brief BSP Layer - Common ADC sampling implementation
4+
*
5+
* This file contains platform-independent ADC sampling logic.
6+
* Platform-specific GPIO-to-channel mapping is in separate files:
7+
* - AdcSampler_esp32.cpp
8+
* - AdcSampler_esp32s3.cpp
9+
*/
10+
11+
#include "AdcSampler.hpp"
12+
13+
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32)
14+
#include <esp_log.h>
15+
16+
static const char *TAG = "[AdcSampler]";
17+
18+
// Static member initialization
19+
adc_oneshot_unit_handle_t AdcSampler::shared_unit_ = nullptr;
20+
21+
AdcSampler::~AdcSampler()
22+
{
23+
if (cali_handle_)
24+
{
25+
#if defined(CONFIG_IDF_TARGET_ESP32S3)
26+
adc_cali_delete_scheme_curve_fitting(cali_handle_);
27+
#elif defined(CONFIG_IDF_TARGET_ESP32)
28+
adc_cali_delete_scheme_line_fitting(cali_handle_);
29+
#endif
30+
cali_handle_ = nullptr;
31+
}
32+
}
33+
34+
bool AdcSampler::init(int gpio, adc_atten_t atten, adc_bitwidth_t bitwidth, size_t window_size)
35+
{
36+
// Initialize moving average filter
37+
if (window_size == 0)
38+
{
39+
window_size = 1;
40+
}
41+
samples_.assign(window_size, 0);
42+
sample_sum_ = 0;
43+
sample_idx_ = 0;
44+
sample_count_ = 0;
45+
46+
atten_ = atten;
47+
bitwidth_ = bitwidth;
48+
49+
// Map GPIO to ADC channel (platform-specific)
50+
if (!map_gpio_to_channel(gpio, unit_, channel_))
51+
{
52+
ESP_LOGW(TAG, "GPIO %d is not a valid ADC1 pin on this chip", gpio);
53+
return false;
54+
}
55+
56+
// Initialize shared ADC unit
57+
if (!ensure_unit())
58+
{
59+
return false;
60+
}
61+
62+
// Configure the ADC channel
63+
if (!configure_channel(gpio, atten, bitwidth))
64+
{
65+
return false;
66+
}
67+
68+
// Try calibration (requires eFuse data)
69+
// ESP32-S3 uses curve-fitting, ESP32 uses line-fitting
70+
esp_err_t cal_err = ESP_FAIL;
71+
72+
#if defined(CONFIG_IDF_TARGET_ESP32S3)
73+
// ESP32-S3 curve fitting calibration
74+
adc_cali_curve_fitting_config_t cal_cfg = {
75+
.unit_id = unit_,
76+
.chan = channel_,
77+
.atten = atten_,
78+
.bitwidth = bitwidth_,
79+
};
80+
cal_err = adc_cali_create_scheme_curve_fitting(&cal_cfg, &cali_handle_);
81+
#elif defined(CONFIG_IDF_TARGET_ESP32)
82+
// ESP32 line-fitting calibration is per-unit, not per-channel
83+
adc_cali_line_fitting_config_t cal_cfg = {
84+
.unit_id = unit_,
85+
.atten = atten_,
86+
.bitwidth = bitwidth_,
87+
};
88+
cal_err = adc_cali_create_scheme_line_fitting(&cal_cfg, &cali_handle_);
89+
#endif
90+
91+
if (cal_err == ESP_OK)
92+
{
93+
cali_inited_ = true;
94+
ESP_LOGI(TAG, "ADC calibration initialized");
95+
}
96+
else
97+
{
98+
cali_inited_ = false;
99+
ESP_LOGW(TAG, "ADC calibration not available; using raw-to-mV approximation");
100+
}
101+
102+
return true;
103+
}
104+
105+
bool AdcSampler::sampleOnce()
106+
{
107+
if (!shared_unit_)
108+
{
109+
return false;
110+
}
111+
112+
int raw = 0;
113+
esp_err_t err = adc_oneshot_read(shared_unit_, channel_, &raw);
114+
if (err != ESP_OK)
115+
{
116+
ESP_LOGE(TAG, "adc_oneshot_read failed: %s", esp_err_to_name(err));
117+
return false;
118+
}
119+
120+
int mv = 0;
121+
if (cali_inited_)
122+
{
123+
if (adc_cali_raw_to_voltage(cali_handle_, raw, &mv) != ESP_OK)
124+
{
125+
mv = 0;
126+
}
127+
}
128+
else
129+
{
130+
// Approximate conversion for 12dB attenuation (~0–3600 mV range)
131+
// Full-scale raw = (1 << bitwidth_) - 1
132+
// For 12-bit: max raw = 4095 → ~3600 mV
133+
int full_scale_mv = 3600;
134+
int max_raw = (1 << bitwidth_) - 1;
135+
if (max_raw > 0)
136+
{
137+
mv = (raw * full_scale_mv) / max_raw;
138+
}
139+
else
140+
{
141+
mv = 0;
142+
}
143+
}
144+
145+
// Update moving average filter
146+
sample_sum_ -= samples_[sample_idx_];
147+
samples_[sample_idx_] = mv;
148+
sample_sum_ += mv;
149+
sample_idx_ = (sample_idx_ + 1) % samples_.size();
150+
if (sample_count_ < samples_.size())
151+
{
152+
sample_count_++;
153+
}
154+
filtered_mv_ = sample_sum_ / static_cast<int>(sample_count_ > 0 ? sample_count_ : 1);
155+
156+
return true;
157+
}
158+
159+
bool AdcSampler::ensure_unit()
160+
{
161+
if (shared_unit_)
162+
{
163+
return true;
164+
}
165+
166+
adc_oneshot_unit_init_cfg_t unit_cfg = {
167+
.unit_id = ADC_UNIT_1,
168+
.clk_src = ADC_RTC_CLK_SRC_DEFAULT,
169+
.ulp_mode = ADC_ULP_MODE_DISABLE,
170+
};
171+
esp_err_t err = adc_oneshot_new_unit(&unit_cfg, &shared_unit_);
172+
if (err != ESP_OK)
173+
{
174+
ESP_LOGE(TAG, "adc_oneshot_new_unit failed: %s", esp_err_to_name(err));
175+
shared_unit_ = nullptr;
176+
return false;
177+
}
178+
return true;
179+
}
180+
181+
bool AdcSampler::configure_channel(int gpio, adc_atten_t atten, adc_bitwidth_t bitwidth)
182+
{
183+
adc_oneshot_chan_cfg_t chan_cfg = {
184+
.atten = atten,
185+
.bitwidth = bitwidth,
186+
};
187+
esp_err_t err = adc_oneshot_config_channel(shared_unit_, channel_, &chan_cfg);
188+
if (err != ESP_OK)
189+
{
190+
ESP_LOGE(TAG, "adc_oneshot_config_channel failed (GPIO %d, CH %d): %s",
191+
gpio, static_cast<int>(channel_), esp_err_to_name(err));
192+
return false;
193+
}
194+
return true;
195+
}
196+
197+
#endif // CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32

0 commit comments

Comments
 (0)