From 40bea065a7526246bf5379bdbb6e2aaa95e6b0e9 Mon Sep 17 00:00:00 2001 From: Brent Perteet Date: Mon, 19 Jan 2026 17:42:48 -0600 Subject: [PATCH] adding save and battery voltage reading --- main/CMakeLists.txt | 2 +- main/battery.c | 204 ++++++++++++++++++++++++++++++++++++++++++++ main/battery.h | 56 ++++++++++++ main/gui.c | 26 ++++-- main/main.c | 8 +- main/system.c | 119 ++++++++++++++++++++++++++ main/system.h | 16 ++++ 7 files changed, 424 insertions(+), 7 deletions(-) create mode 100644 main/battery.c create mode 100644 main/battery.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 3ba20b1..855897c 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,4 +1,4 @@ -idf_component_register(SRCS "bt_app.c" "system.c" "bubble.c" "keypad.c" "main.c" +idf_component_register(SRCS "battery.c" "bt_app.c" "system.c" "bubble.c" "keypad.c" "main.c" "gui.c" "lsm6dsv.c" INCLUDE_DIRS "." diff --git a/main/battery.c b/main/battery.c new file mode 100644 index 0000000..a071d33 --- /dev/null +++ b/main/battery.c @@ -0,0 +1,204 @@ +#include "battery.h" +#include "system.h" +#include "gpio.h" +#include "esp_log.h" +#include "esp_adc/adc_oneshot.h" +#include "esp_adc/adc_cali.h" +#include "esp_adc/adc_cali_scheme.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "battery"; + +static adc_oneshot_unit_handle_t adc1_handle = NULL; +static adc_cali_handle_t adc1_cali_handle = NULL; +static bool adc_calibration_enabled = false; + +// ADC Calibration initialization +static bool adc_calibration_init(adc_unit_t unit, adc_channel_t channel, adc_atten_t atten, adc_cali_handle_t *out_handle) +{ + adc_cali_handle_t handle = NULL; + esp_err_t ret = ESP_FAIL; + bool calibrated = false; + +#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED + if (!calibrated) { + ESP_LOGI(TAG, "Calibration scheme: Curve Fitting"); + adc_cali_curve_fitting_config_t cali_config = { + .unit_id = unit, + .chan = channel, + .atten = atten, + .bitwidth = BATTERY_ADC_WIDTH, + }; + ret = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); + if (ret == ESP_OK) { + calibrated = true; + } + } +#endif + +#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED + if (!calibrated) { + ESP_LOGI(TAG, "Calibration scheme: Line Fitting"); + adc_cali_line_fitting_config_t cali_config = { + .unit_id = unit, + .atten = atten, + .bitwidth = BATTERY_ADC_WIDTH, + }; + ret = adc_cali_create_scheme_line_fitting(&cali_config, &handle); + if (ret == ESP_OK) { + calibrated = true; + } + } +#endif + + *out_handle = handle; + if (ret == ESP_OK) { + ESP_LOGI(TAG, "ADC calibration successful"); + } else { + ESP_LOGW(TAG, "ADC calibration failed: %s", esp_err_to_name(ret)); + } + return calibrated; +} + +esp_err_t battery_init(void) +{ + esp_err_t ret; + + // Configure ADC1 oneshot mode + adc_oneshot_unit_init_cfg_t init_config = { + .unit_id = ADC_UNIT_1, + }; + ret = adc_oneshot_new_unit(&init_config, &adc1_handle); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize ADC unit: %s", esp_err_to_name(ret)); + return ret; + } + + // Configure ADC channel + adc_oneshot_chan_cfg_t config = { + .bitwidth = BATTERY_ADC_WIDTH, + .atten = BATTERY_ADC_ATTEN, + }; + ret = adc_oneshot_config_channel(adc1_handle, BATTERY_ADC_CHANNEL, &config); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to configure ADC channel: %s", esp_err_to_name(ret)); + return ret; + } + + // Initialize calibration + adc_calibration_enabled = adc_calibration_init(ADC_UNIT_1, BATTERY_ADC_CHANNEL, + BATTERY_ADC_ATTEN, &adc1_cali_handle); + + ESP_LOGI(TAG, "Battery monitoring initialized on GPIO34 (ADC1_CH6)"); + return ESP_OK; +} + +int battery_read_raw(void) +{ + int adc_raw = 0; + esp_err_t ret = adc_oneshot_read(adc1_handle, BATTERY_ADC_CHANNEL, &adc_raw); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to read ADC: %s", esp_err_to_name(ret)); + return -1; + } + return adc_raw; +} + +int battery_read_voltage_mv(void) +{ + int voltage_mv = 0; + int adc_sum = 0; + int valid_samples = 0; + + // Take multiple samples and average + for (int i = 0; i < BATTERY_SAMPLES; i++) { + int adc_raw = battery_read_raw(); + if (adc_raw >= 0) { + adc_sum += adc_raw; + valid_samples++; + } + vTaskDelay(pdMS_TO_TICKS(1)); // Small delay between samples + } + + if (valid_samples == 0) { + ESP_LOGE(TAG, "No valid ADC samples"); + return -1; + } + + int adc_avg = adc_sum / valid_samples; + + // Convert to voltage using calibration if available + if (adc_calibration_enabled) { + esp_err_t ret = adc_cali_raw_to_voltage(adc1_cali_handle, adc_avg, &voltage_mv); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "Calibration conversion failed, using raw calculation"); + adc_calibration_enabled = false; // Disable for future reads + } + } + + // Fallback to manual calculation if calibration not available + if (!adc_calibration_enabled) { + // Simple linear conversion for 12-bit ADC with 12dB attenuation + // Approximate range: 0-3300mV for 0-4095 raw values + voltage_mv = (adc_avg * 3300) / 4095; + } + + // Apply voltage divider ratio to get actual battery voltage + voltage_mv = (int)(voltage_mv * BATTERY_VOLTAGE_DIVIDER_RATIO); + + return voltage_mv; +} + +int battery_get_percentage(void) +{ + int voltage_mv = battery_read_voltage_mv(); + + if (voltage_mv < 0) { + return -1; + } + + // Clamp to min/max range + if (voltage_mv >= BATTERY_VOLTAGE_MAX) { + return 100; + } + if (voltage_mv <= BATTERY_VOLTAGE_MIN) { + return 0; + } + + // Linear interpolation between min and max + int percentage = ((voltage_mv - BATTERY_VOLTAGE_MIN) * 100) / + (BATTERY_VOLTAGE_MAX - BATTERY_VOLTAGE_MIN); + + return percentage; +} + +// Battery monitoring task +static void battery_monitoring_task(void *pvParameters) +{ + ESP_LOGI(TAG, "Battery monitoring task started"); + + while (1) { + int voltage_mv = battery_read_voltage_mv(); + int percentage = battery_get_percentage(); + + if (voltage_mv >= 0 && percentage >= 0) { + // Update system state with battery info + system_setBatteryVoltage(voltage_mv); + system_setBatteryPercentage(percentage); + + ESP_LOGI(TAG, "Battery: %d mV (%d%%)", voltage_mv, percentage); + } else { + ESP_LOGW(TAG, "Failed to read battery voltage"); + } + + // Read battery every 30 seconds + vTaskDelay(pdMS_TO_TICKS(30000)); + } +} + +void battery_start_monitoring_task(void) +{ + xTaskCreate(battery_monitoring_task, "battery_task", 3072, NULL, 5, NULL); + ESP_LOGI(TAG, "Battery monitoring task created"); +} diff --git a/main/battery.h b/main/battery.h new file mode 100644 index 0000000..dcd4c88 --- /dev/null +++ b/main/battery.h @@ -0,0 +1,56 @@ +#ifndef BATTERY_H +#define BATTERY_H + +#include +#include "esp_err.h" + +// Battery monitoring configuration +#define BATTERY_ADC_CHANNEL ADC_CHANNEL_6 // GPIO34 (ADC1_CH6) +#define BATTERY_ADC_ATTEN ADC_ATTEN_DB_12 // Full range ~3.9V (0-3300mV with attenuation) +#define BATTERY_ADC_WIDTH ADC_WIDTH_BIT_12 // 12-bit resolution (0-4095) + +// Battery voltage calculation constants +// Adjust these based on your voltage divider circuit +#define BATTERY_VOLTAGE_DIVIDER_RATIO 2.0f // Example: R1=R2, adjust for your circuit +#define BATTERY_SAMPLES 16 // Number of samples to average + +// Battery percentage thresholds (in millivolts at battery) +#define BATTERY_VOLTAGE_MAX 4200 // Fully charged Li-Ion +#define BATTERY_VOLTAGE_MIN 3000 // Empty Li-Ion (cutoff) + +/** + * @brief Initialize battery monitoring ADC + * + * @return esp_err_t ESP_OK on success + */ +esp_err_t battery_init(void); + +/** + * @brief Read raw ADC value from battery pin + * + * @return int Raw ADC value (0-4095) + */ +int battery_read_raw(void); + +/** + * @brief Read battery voltage in millivolts (averaged) + * + * @return int Battery voltage in mV + */ +int battery_read_voltage_mv(void); + +/** + * @brief Get battery percentage (0-100) + * + * @return int Battery percentage + */ +int battery_get_percentage(void); + +/** + * @brief Start battery monitoring task + * + * This task periodically reads battery voltage and updates system state + */ +void battery_start_monitoring_task(void); + +#endif // BATTERY_H diff --git a/main/gui.c b/main/gui.c index da41b8a..6cfe0a4 100644 --- a/main/gui.c +++ b/main/gui.c @@ -1395,28 +1395,32 @@ static lv_obj_t* create_volume_page(void) { static void show_volume_control(void) { ESP_LOGI(TAG, "Showing volume control"); - + + // Load saved volume from system + _current_volume = system_getVolume(); + ESP_LOGI(TAG, "Loaded volume from system: %d", _current_volume); + lv_obj_t* menu = create_menu_container(); if (!menu) { ESP_LOGE(TAG, "Failed to create menu container for volume control"); return; } - + lv_obj_t* volume_page = create_volume_page(); if (!volume_page) { ESP_LOGE(TAG, "Failed to create volume page"); return; } - + lv_menu_set_page(menu, volume_page); _currentPage = volume_page; _mode = GUI_MENU; // Keep in menu mode menu_hide_headers(menu); - + lv_obj_remove_flag(menu, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(_bubble, LV_OBJ_FLAG_HIDDEN); - + ESP_LOGI(TAG, "Volume control displayed"); } @@ -1452,6 +1456,17 @@ static bool menu_stack_is_empty(void) { static void menu_go_back(void) { ESP_LOGI(TAG, "Menu go back requested"); + // Save volume if exiting from volume page + if (_currentPage == _volume_page) { + system_setVolume(_current_volume); + esp_err_t ret = system_saveVolume(); + if (ret == ESP_OK) { + ESP_LOGI(TAG, "Volume %d saved when exiting volume menu", _current_volume); + } else { + ESP_LOGE(TAG, "Failed to save volume: %s", esp_err_to_name(ret)); + } + } + if (menu_stack_is_empty()) { ESP_LOGI(TAG, "Menu stack empty, returning to bubble mode"); _mode = GUI_BUBBLE; @@ -1497,6 +1512,7 @@ static void update_volume_display(int volume) { UNLOCK(); _current_volume = volume; + system_setVolume(volume); // Update system state in real-time ESP_LOGI(TAG, "Volume display updated to %d%%", volume); } } diff --git a/main/main.c b/main/main.c index e6ba36c..058b362 100644 --- a/main/main.c +++ b/main/main.c @@ -28,6 +28,7 @@ #include "gpio.h" #include "keypad.h" #include "system.h" +#include "battery.h" @@ -313,10 +314,15 @@ void app_main(void) bt_app_init(); print_heap_info("POST_BLUETOOTH"); + // Initialize battery monitoring + ESP_ERROR_CHECK(battery_init()); + battery_start_monitoring_task(); + print_heap_info("POST_BATTERY"); + gpio_set_level(PIN_NUM_LED_0, 1); gpio_set_level(PIN_NUM_LED_1, 1); gpio_set_level(PIN_NUM_LED_2, 1); - + gui_start(); print_heap_info("POST_GUI"); gpio_set_level(PIN_NUM_LED_2, 1); diff --git a/main/system.c b/main/system.c index e60faef..50a9c7e 100644 --- a/main/system.c +++ b/main/system.c @@ -13,6 +13,8 @@ static EventManager_t _eventManager; static QueueHandle_t _nvsRequestQueue; static const char* NVS_NAMESPACE = "bt_devices"; static const char* NVS_KEY_COUNT = "count"; +static const char* NVS_NAMESPACE_SETTINGS = "settings"; +static const char* NVS_KEY_VOLUME = "volume"; static esp_err_t nvs_load_devices_internal(paired_device_t *devices, size_t *count); static esp_err_t nvs_save_devices_internal(const paired_device_t *devices, size_t count); @@ -24,6 +26,9 @@ void system_init(void) _systemState.pairedDeviceCount = 0; _systemState.isCharging = false; _systemState.swapLR = false; + _systemState.volume = 50; // Default volume + _systemState.batteryVoltage_mv = 0; + _systemState.batteryPercentage = 0; _systemEvent = xEventGroupCreate(); @@ -31,6 +36,9 @@ void system_init(void) _eventManager.mutex = xSemaphoreCreateMutex(); system_initNvsService(); + + // Load saved volume from NVS + system_loadVolume(); } int system_getPrimaryAxis(void) @@ -75,6 +83,38 @@ bool system_getChargeStatus(void) return charging; } +void system_setBatteryVoltage(int voltage_mv) +{ + xSemaphoreTake(_eventManager.mutex, portMAX_DELAY); + _systemState.batteryVoltage_mv = voltage_mv; + xSemaphoreGive(_eventManager.mutex); +} + +int system_getBatteryVoltage(void) +{ + int voltage; + xSemaphoreTake(_eventManager.mutex, portMAX_DELAY); + voltage = _systemState.batteryVoltage_mv; + xSemaphoreGive(_eventManager.mutex); + return voltage; +} + +void system_setBatteryPercentage(int percentage) +{ + xSemaphoreTake(_eventManager.mutex, portMAX_DELAY); + _systemState.batteryPercentage = percentage; + xSemaphoreGive(_eventManager.mutex); +} + +int system_getBatteryPercentage(void) +{ + int percentage; + xSemaphoreTake(_eventManager.mutex, portMAX_DELAY); + percentage = _systemState.batteryPercentage; + xSemaphoreGive(_eventManager.mutex); + return percentage; +} + void system_setSwapLR(bool swap) { xSemaphoreTake(_eventManager.mutex, portMAX_DELAY); @@ -257,6 +297,85 @@ void system_requestVolumeDown(void) { system_notifyAll(EM_EVENT_VOLUME_DOWN); } +void system_setVolume(int volume) { + if (volume < 0) volume = 0; + if (volume > 100) volume = 100; + + xSemaphoreTake(_eventManager.mutex, portMAX_DELAY); + _systemState.volume = volume; + xSemaphoreGive(_eventManager.mutex); + + ESP_LOGI("system", "Volume set to %d", volume); +} + +int system_getVolume(void) { + int volume; + xSemaphoreTake(_eventManager.mutex, portMAX_DELAY); + volume = _systemState.volume; + xSemaphoreGive(_eventManager.mutex); + return volume; +} + +esp_err_t system_saveVolume(void) { + nvs_handle_t nvs_handle; + esp_err_t ret; + + int volume = system_getVolume(); + + ret = nvs_open(NVS_NAMESPACE_SETTINGS, NVS_READWRITE, &nvs_handle); + if (ret != ESP_OK) { + ESP_LOGE("system", "Failed to open NVS namespace for volume write: %s", esp_err_to_name(ret)); + return ret; + } + + ret = nvs_set_i32(nvs_handle, NVS_KEY_VOLUME, volume); + if (ret != ESP_OK) { + ESP_LOGE("system", "Failed to write volume to NVS: %s", esp_err_to_name(ret)); + nvs_close(nvs_handle); + return ret; + } + + ret = nvs_commit(nvs_handle); + nvs_close(nvs_handle); + + if (ret == ESP_OK) { + ESP_LOGI("system", "Volume %d saved to NVS", volume); + } else { + ESP_LOGE("system", "Failed to commit volume to NVS: %s", esp_err_to_name(ret)); + } + + return ret; +} + +esp_err_t system_loadVolume(void) { + nvs_handle_t nvs_handle; + esp_err_t ret; + int32_t volume = 50; // Default value + + ret = nvs_open(NVS_NAMESPACE_SETTINGS, NVS_READONLY, &nvs_handle); + if (ret != ESP_OK) { + ESP_LOGI("system", "No saved volume found, using default: %d", volume); + system_setVolume(volume); + return ESP_OK; // Not an error, just means no saved value + } + + ret = nvs_get_i32(nvs_handle, NVS_KEY_VOLUME, &volume); + nvs_close(nvs_handle); + + if (ret == ESP_OK) { + ESP_LOGI("system", "Loaded volume from NVS: %d", volume); + system_setVolume(volume); + } else if (ret == ESP_ERR_NVS_NOT_FOUND) { + ESP_LOGI("system", "No saved volume found, using default: %d", volume); + system_setVolume(volume); + ret = ESP_OK; // Not an error + } else { + ESP_LOGE("system", "Failed to read volume from NVS: %s", esp_err_to_name(ret)); + } + + return ret; +} + void system_initNvsService(void) { _nvsRequestQueue = xQueueCreate(10, sizeof(nvs_request_t)); if (_nvsRequestQueue == NULL) { diff --git a/main/system.h b/main/system.h index a21d251..12f39da 100644 --- a/main/system.h +++ b/main/system.h @@ -58,6 +58,13 @@ typedef struct SystemState_s // Swap L/R audio channels bool swapLR; + // Volume setting (0-100) + int volume; + + // Battery monitoring + int batteryVoltage_mv; + int batteryPercentage; + } SystemState_t; @@ -93,6 +100,11 @@ float system_getAngle(void); void system_setChargeStatus(bool charging); bool system_getChargeStatus(void); +void system_setBatteryVoltage(int voltage_mv); +int system_getBatteryVoltage(void); +void system_setBatteryPercentage(int percentage); +int system_getBatteryPercentage(void); + void system_setSwapLR(bool swap); bool system_getSwapLR(void); void system_toggleSwapLR(void); @@ -118,6 +130,10 @@ int system_getBtDeviceIndex(void); // Volume control functions void system_requestVolumeUp(void); void system_requestVolumeDown(void); +void system_setVolume(int volume); +int system_getVolume(void); +esp_err_t system_saveVolume(void); +esp_err_t system_loadVolume(void); // NVS Service typedef enum {