Detect under-voltage on GPIO1/11, persist UV state in NVS, enter a minimal power mode without ESP-NOW, and recover after charge; expose LED mode 6 battery-low for host testing via goTool. Co-authored-by: Cursor <cursoragent@cursor.com>
248 lines
6.1 KiB
C
248 lines
6.1 KiB
C
#include "board_input.h"
|
|
#include "powerpod.h"
|
|
#include "client_registry.h"
|
|
#include "driver/gpio.h"
|
|
#if POWERPOD_BATTERY_UV_ENABLE
|
|
#include "battery_uv.h"
|
|
#endif
|
|
#include "esp_adc/adc_oneshot.h"
|
|
#include "esp_log.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/idf_additions.h"
|
|
#include "freertos/queue.h"
|
|
#include <string.h>
|
|
|
|
static const char *TAG_BTN = "[BTN]";
|
|
static const char *TAG_LIPO = "[LIPO]";
|
|
|
|
#define LIPO_SAMPLE_INTERVAL_MS 10000
|
|
#define BUTTON_QUEUE_LEN 4
|
|
#define BUTTON_DEBOUNCE_MS 80
|
|
#define LIPO_ADC_FULL_SCALE_MV 3300
|
|
#define LIPO_ADC_MAX_RAW 4095
|
|
|
|
static QueueHandle_t s_button_queue;
|
|
|
|
typedef struct {
|
|
adc_oneshot_unit_handle_t unit;
|
|
adc_channel_t ch;
|
|
bool ok;
|
|
} lipo_adc_t;
|
|
|
|
static lipo_adc_t s_lipo1;
|
|
static lipo_adc_t s_lipo2;
|
|
|
|
static esp_err_t adc_init_gpio(int gpio, lipo_adc_t *out) {
|
|
out->unit = NULL;
|
|
out->ok = false;
|
|
|
|
adc_unit_t unit_id;
|
|
esp_err_t err = adc_oneshot_io_to_channel(gpio, &unit_id, &out->ch);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGW(TAG_LIPO, "GPIO%d not an ADC channel: %s", gpio, esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
adc_oneshot_unit_init_cfg_t init_cfg = {
|
|
.unit_id = unit_id,
|
|
};
|
|
err = adc_oneshot_new_unit(&init_cfg, &out->unit);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGW(TAG_LIPO, "ADC unit %d init GPIO%d failed: %s", (int)unit_id, gpio,
|
|
esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
adc_oneshot_chan_cfg_t chan_cfg = {
|
|
.atten = ADC_ATTEN_DB_12,
|
|
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
|
};
|
|
err = adc_oneshot_config_channel(out->unit, out->ch, &chan_cfg);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGW(TAG_LIPO, "ADC config GPIO%d failed: %s", gpio, esp_err_to_name(err));
|
|
adc_oneshot_del_unit(out->unit);
|
|
out->unit = NULL;
|
|
return err;
|
|
}
|
|
|
|
out->ok = true;
|
|
ESP_LOGI(TAG_LIPO, "GPIO%d ready (ADC unit %d)", gpio, (int)unit_id);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static uint32_t raw_to_mv(int raw) {
|
|
if (raw < 0) {
|
|
return 0;
|
|
}
|
|
return (uint32_t)((raw * LIPO_ADC_FULL_SCALE_MV) / LIPO_ADC_MAX_RAW);
|
|
}
|
|
|
|
static void sample_one_channel(const lipo_adc_t *adc, uint32_t *mv_out,
|
|
bool *valid_out) {
|
|
*valid_out = false;
|
|
*mv_out = 0;
|
|
if (adc == NULL || !adc->ok || adc->unit == NULL) {
|
|
return;
|
|
}
|
|
int raw = 0;
|
|
if (adc_oneshot_read(adc->unit, adc->ch, &raw) == ESP_OK) {
|
|
*valid_out = true;
|
|
*mv_out = raw_to_mv(raw);
|
|
}
|
|
}
|
|
|
|
void board_input_read_lipo(board_lipo_reading_t *out) {
|
|
if (out == NULL) {
|
|
return;
|
|
}
|
|
memset(out, 0, sizeof(*out));
|
|
sample_one_channel(&s_lipo1, &out->lipo1_mv, &out->lipo1_valid);
|
|
sample_one_channel(&s_lipo2, &out->lipo2_mv, &out->lipo2_valid);
|
|
}
|
|
|
|
static void lipo_monitor_task(void *param) {
|
|
(void)param;
|
|
|
|
ESP_LOGI(TAG_LIPO, "monitor task (interval %d ms)", LIPO_SAMPLE_INTERVAL_MS);
|
|
|
|
while (1) {
|
|
board_lipo_reading_t reading;
|
|
board_input_read_lipo(&reading);
|
|
client_registry_set_master_battery(&reading);
|
|
|
|
ESP_LOGI(TAG_LIPO,
|
|
"LIPO1 GPIO%d %s %lu mV LIPO2 GPIO%d %s %lu mV",
|
|
V_LIPO_1_GPIO, reading.lipo1_valid ? "ok" : "n/a",
|
|
(unsigned long)reading.lipo1_mv, V_LIPO_2_GPIO,
|
|
reading.lipo2_valid ? "ok" : "n/a",
|
|
(unsigned long)reading.lipo2_mv);
|
|
|
|
#if POWERPOD_BATTERY_UV_ENABLE
|
|
battery_uv_poll(&reading);
|
|
#endif
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(LIPO_SAMPLE_INTERVAL_MS));
|
|
}
|
|
}
|
|
|
|
static void IRAM_ATTR button_isr(void *arg) {
|
|
(void)arg;
|
|
uint8_t one = 1;
|
|
BaseType_t wake = pdFALSE;
|
|
if (s_button_queue != NULL) {
|
|
xQueueSendFromISR(s_button_queue, &one, &wake);
|
|
if (wake) {
|
|
portYIELD_FROM_ISR();
|
|
}
|
|
}
|
|
}
|
|
|
|
static void button_task(void *param) {
|
|
(void)param;
|
|
uint8_t evt;
|
|
|
|
while (1) {
|
|
if (xQueueReceive(s_button_queue, &evt, portMAX_DELAY) != pdTRUE) {
|
|
continue;
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(BUTTON_DEBOUNCE_MS));
|
|
if (gpio_get_level(TASTER_GPIO) == 0) {
|
|
ESP_LOGI(TAG_BTN, "pressed (GPIO%d)", TASTER_GPIO);
|
|
}
|
|
}
|
|
}
|
|
|
|
static esp_err_t init_button(void) {
|
|
if (V_LIPO_2_GPIO == TASTER_GPIO) {
|
|
ESP_LOGW(TAG_BTN,
|
|
"GPIO%d shared with V_LIPO_2 — button only, no ADC on that pin",
|
|
TASTER_GPIO);
|
|
}
|
|
|
|
s_button_queue = xQueueCreate(BUTTON_QUEUE_LEN, sizeof(uint8_t));
|
|
if (s_button_queue == NULL) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
gpio_config_t cfg = {
|
|
.pin_bit_mask = 1ULL << TASTER_GPIO,
|
|
.mode = GPIO_MODE_INPUT,
|
|
.pull_up_en = GPIO_PULLUP_ENABLE,
|
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
|
.intr_type = GPIO_INTR_NEGEDGE,
|
|
};
|
|
ESP_ERROR_CHECK(gpio_config(&cfg));
|
|
|
|
esp_err_t err = gpio_install_isr_service(0);
|
|
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
|
return err;
|
|
}
|
|
|
|
err = gpio_isr_handler_add(TASTER_GPIO, button_isr, NULL);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
|
|
if (xTaskCreate(button_task, "btn", 2048, NULL, 2, NULL) != pdPASS) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
ESP_LOGI(TAG_BTN, "ready GPIO%d (active low)", TASTER_GPIO);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t init_lipo_adc_hw(void) {
|
|
memset(&s_lipo1, 0, sizeof(s_lipo1));
|
|
memset(&s_lipo2, 0, sizeof(s_lipo2));
|
|
|
|
adc_init_gpio(V_LIPO_1_GPIO, &s_lipo1);
|
|
|
|
if (V_LIPO_2_GPIO == TASTER_GPIO) {
|
|
ESP_LOGW(TAG_LIPO, "LIPO2 on GPIO%d skipped (button uses same pin)",
|
|
V_LIPO_2_GPIO);
|
|
} else {
|
|
adc_init_gpio(V_LIPO_2_GPIO, &s_lipo2);
|
|
}
|
|
|
|
if (!s_lipo1.ok && !s_lipo2.ok) {
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t board_input_init_adc_only(void) { return init_lipo_adc_hw(); }
|
|
|
|
esp_err_t board_input_start_lipo_monitor(void) {
|
|
if (!s_lipo1.ok && !s_lipo2.ok) {
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
if (xTaskCreate(lipo_monitor_task, "lipo_mon", 3072, NULL, 1, NULL) != pdPASS) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t board_input_init_button(void) { return init_button(); }
|
|
|
|
esp_err_t board_input_init(void) {
|
|
esp_err_t err = board_input_init_adc_only();
|
|
if (err != ESP_OK) {
|
|
ESP_LOGW(TAG_LIPO, "ADC init failed: %s", esp_err_to_name(err));
|
|
} else {
|
|
err = board_input_start_lipo_monitor();
|
|
if (err != ESP_OK) {
|
|
ESP_LOGW(TAG_LIPO, "monitor task failed: %s", esp_err_to_name(err));
|
|
}
|
|
}
|
|
|
|
err = board_input_init_button();
|
|
if (err != ESP_OK) {
|
|
ESP_LOGW(TAG_BTN, "init failed: %s", esp_err_to_name(err));
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|