From b03b0c0f43556eba2b067d23718336a5b23b4809 Mon Sep 17 00:00:00 2001 From: pulipakaa24 Date: Mon, 9 Mar 2026 02:29:03 -0500 Subject: [PATCH] Ready for test --- components/esp-nimble-cpp | 1 - dependencies.lock | 27 ++++- include/defines.h | 6 +- include/mainEventLoop.hpp | 31 +++-- include/max17048.h | 64 ++++++++--- sdkconfig.seeed_xiao_esp32c6 | 9 +- src/BLE.cpp | 2 +- src/batteryManagement.cpp | 14 +-- src/i2c.c | 2 +- src/idf_component.yml | 4 +- src/main.cpp | 4 + src/mainEventLoop.cpp | 215 +++++++++++++++++++++-------------- src/max17048.c | 120 +++++++++++++++---- src/servo.cpp | 3 +- src/setup.cpp | 1 + 15 files changed, 344 insertions(+), 159 deletions(-) delete mode 160000 components/esp-nimble-cpp diff --git a/components/esp-nimble-cpp b/components/esp-nimble-cpp deleted file mode 160000 index 25af28b..0000000 --- a/components/esp-nimble-cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 25af28bcad1d3c42f76bfc6e73bea3f833a4ef6d diff --git a/dependencies.lock b/dependencies.lock index 9347bb2..084b2ec 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -13,8 +13,28 @@ dependencies: registry_url: https://components.espressif.com/ type: service version: 1.0.0 + esp-nimble-cpp: + component_hash: 224980602c685130c426248ad9a0500686d4a5aff0ec3a10b6dfacf00c554a91 + dependencies: + - name: espressif/esp_hosted + rules: + - if: target in [esp32p4] + version: '*' + - name: espressif/esp_wifi_remote + rules: + - if: target in [esp32p4] + version: '>=0.5.3' + - name: idf + rules: + - if: target in [esp32p4] + version: '>=5.3.0' + source: + git: https://github.com/h2zero/esp-nimble-cpp.git + path: . + type: git + version: 002abf91e9779ea5646d75278ae52c6b848d3fa0 espressif/esp_websocket_client: - component_hash: 723aba370113196c66321442426cd6452c351eef31c85c83bd1446831ef9f8f4 + component_hash: c5a067a9fddea370c478017e66fac302f4b79c3d4027e9bdd42a019786cceb92 dependencies: - name: idf require: private @@ -22,13 +42,14 @@ dependencies: source: registry_url: https://components.espressif.com type: service - version: 1.6.0 + version: 1.6.1 idf: source: type: idf version: 5.5.1 direct_dependencies: - bubblesnake/esp_socketio_client -manifest_hash: d73c96c5d6ddd24707089a2953e50f36a12ebbc66b5458ada3d4f55c0987ccf1 +- esp-nimble-cpp +manifest_hash: 5f4bfc48b0eb389591b06fcf0a5c43c05e081427d594466829c6f7c545158221 target: esp32c6 version: 2.0.0 diff --git a/include/defines.h b/include/defines.h index 8f482e7..bf42e2e 100644 --- a/include/defines.h +++ b/include/defines.h @@ -31,11 +31,11 @@ // #define srvAddr "192.168.1.190:3000" #define srvAddr "wahwa.com" -#define ENCODER_PIN_A GPIO_NUM_23 // d5 +#define ENCODER_PIN_A GPIO_NUM_21 // d3 #define ENCODER_PIN_B GPIO_NUM_16 // d6 -#define InputEnc_PIN_A GPIO_NUM_1 // d1 -#define InputEnc_PIN_B GPIO_NUM_2 // d2 +#define InputEnc_PIN_A GPIO_NUM_0 // d0 +#define InputEnc_PIN_B GPIO_NUM_1 // d1 #define servoPin GPIO_NUM_20 #define servoLEDCChannel LEDC_CHANNEL_0 diff --git a/include/mainEventLoop.hpp b/include/mainEventLoop.hpp index a9696bb..45dd53f 100644 --- a/include/mainEventLoop.hpp +++ b/include/mainEventLoop.hpp @@ -1,21 +1,34 @@ #ifndef BM_EVENTS_H #define BM_EVENTS_H + #include "freertos/FreeRTOS.h" #include "freertos/queue.h" +#include "freertos/task.h" -// Event Types +#ifdef __cplusplus +extern "C" { +#endif + +// Shared with max17048.c (C) — only C-compatible types here typedef enum { - EVENT_CLEAR_CALIB, - EVENT_SAVE_POS, - EVENT_REQUEST_POS + EVENT_CLEAR_CALIB, + EVENT_SAVE_POS, + EVENT_REQUEST_POS, + EVENT_BATTERY_CRITICAL, // stop servo, save pos, alert server, deep sleep + EVENT_BATTERY_WARNING, // alert server (no shutdown) + EVENT_REPORT_SOC, // periodic SOC sync to server } main_event_type_t; -void mainEventLoop(); - extern QueueHandle_t main_event_queue; +extern TaskHandle_t wakeTaskHandle; +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +void mainEventLoop(); void wakeTimer(void* pvParameters); +#endif -extern TaskHandle_t wakeTaskHandle; - -#endif \ No newline at end of file +#endif diff --git a/include/max17048.h b/include/max17048.h index 763d4c9..c48b725 100644 --- a/include/max17048.h +++ b/include/max17048.h @@ -1,38 +1,68 @@ #ifndef MAX_17_H #define MAX_17_H -#define MAX17048_ADDR 0x36 -#define MAX17048_REG_VCELL 0x02 // Voltage -#define MAX17048_REG_SOC 0x04 // State of Charge (%) -#define MAX17048_REG_MODE 0x06 -#define MAX17048_REG_VERSION 0x08 -#define MAX17048_REG_CONFIG 0x0C -#define MAX17048_REG_CMD 0xFE // Command (Reset) -#define MAX17048_REG_STATUS 0x1A -#define MAX17048_REG_VALRT 0x14 -#define MAX17048_REG_VRST_ID 0x18 +#define MAX17048_ADDR 0x36 +#define MAX17048_REG_VCELL 0x02 +#define MAX17048_REG_SOC 0x04 +#define MAX17048_REG_MODE 0x06 +#define MAX17048_REG_VERSION 0x08 +#define MAX17048_REG_CONFIG 0x0C +#define MAX17048_REG_VALRT 0x14 +#define MAX17048_REG_VRST_ID 0x18 +#define MAX17048_REG_STATUS 0x1A +#define MAX17048_REG_CMD 0xFE -// Commands -#define MAX17048_CMD_POR 0x5400 // Power On Reset command -#define MAX17048_CMD_QSTRT 0x4000 // Quick Start command +#define MAX17048_CMD_POR 0x5400 +#define MAX17048_CMD_QSTRT 0x4000 +// NOTE: maxALRT (GPIO_NUM_2) conflicts with InputEnc_PIN_B in defines.h. +// Assign maxALRT to a free GPIO before enabling the interrupt. +#define maxALRT GPIO_NUM_2 + +// STATUS register MSB (addr 0x1A) bit masks +// [7:X] [6:EnVR] [5:SC] [4:HD] [3:VR] [2:VL] [1:VH] [0:RI] +#define SCbit (1 << 5) // SOC changed by 1% +#define HDbit (1 << 4) // SOC crossed low threshold (CONFIG.ATHD) +#define VRbit (1 << 3) // voltage reset alert +#define VLbit (1 << 2) // VCELL below VALRT.MIN +#define VHbit (1 << 1) // VCELL above VALRT.MAX +#define RIbit (1 << 0) // reset indicator (device just powered on) + +// SOC thresholds for user-facing push notifications / shutdown logic +#define SOC_WARN_20 20 // emit warning push at this level +#define SOC_WARN_10 10 // emit critical push at this level +#define SOC_CRITICAL_VL 5 // treat undervoltage as critical only below this SOC #ifdef __cplusplus extern "C" { #endif + #include "driver/i2c.h" +// Alert type carried from bms_checker_task to mainEventLoop via bms_pending_alert +typedef enum { + BATT_ALERT_OVERVOLTAGE, // VH: charging fault / damaged battery + BATT_ALERT_CRITICAL_LOW, // HD, or VL + SOC < SOC_CRITICAL_VL + BATT_ALERT_LOW_VOLTAGE_WARNING, // VL with SOC >= SOC_CRITICAL_VL (transient dip) + BATT_ALERT_SOC_LOW_20, // SOC just crossed 20% downward + BATT_ALERT_SOC_LOW_10, // SOC just crossed 10% downward +} batt_alert_type_t; + +extern uint8_t established_soc; +extern volatile batt_alert_type_t bms_pending_alert; + esp_err_t max17048_init(); -uint8_t bms_get_soc(); +uint8_t bms_get_soc(); esp_err_t bms_set_alert_bound_voltages(float min, float max); esp_err_t bms_set_reset_voltage(float vreset); -esp_err_t bms_clear_status(); +void bms_checker_task(void *pvParameters); -#define bms_set_alsc() max17048_friendly_write_reg(MAX17048_REG_CONFIG, 0, 1<<6, 0, 1<<6) +#define bms_set_alsc() max17048_friendly_write_reg(MAX17048_REG_CONFIG, 0, 1<<6, 0, 1<<6) #define bms_clear_status() max17048_write_reg(MAX17048_REG_STATUS, 0, 0) +#define bms_clear_alrt() max17048_friendly_write_reg(MAX17048_REG_CONFIG, 0, 0, 0, 1<<5) #ifdef __cplusplus } #endif -#endif \ No newline at end of file +#endif diff --git a/sdkconfig.seeed_xiao_esp32c6 b/sdkconfig.seeed_xiao_esp32c6 index f5eebb8..4c0813c 100644 --- a/sdkconfig.seeed_xiao_esp32c6 +++ b/sdkconfig.seeed_xiao_esp32c6 @@ -2568,6 +2568,10 @@ CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y # CONFIG_WIFI_PROV_STA_FAST_SCAN is not set # end of Wi-Fi Provisioning Manager +# +# ESP Socket.IO client +# + # # ESP-NimBLE-CPP configuration # @@ -2590,10 +2594,6 @@ CONFIG_NIMBLE_CPP_FREERTOS_TASK_BLOCK_BIT=31 CONFIG_NIMBLE_CPP_IDF=y # end of ESP-NimBLE-CPP configuration -# -# ESP Socket.IO client -# - # # ESP WebSocket client # @@ -2807,5 +2807,4 @@ CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y CONFIG_SUPPORT_TERMIOS=y CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1 -CONFIG_NIMBLE_ENABLED=y # End of deprecated options diff --git a/src/BLE.cpp b/src/BLE.cpp index d42ac98..acc6ed4 100644 --- a/src/BLE.cpp +++ b/src/BLE.cpp @@ -179,7 +179,7 @@ void MyServerCallbacks::onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInf void MyServerCallbacks::onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) { isBLEClientConnected = false; printf("Client disconnected - reason: %d\n", reason); - reset(); + if (!finalAuth) reset(); } void MyCharCallbacks::onRead(NimBLECharacteristic* pChar, NimBLEConnInfo& connInfo) { diff --git a/src/batteryManagement.cpp b/src/batteryManagement.cpp index 0596538..4ea25af 100644 --- a/src/batteryManagement.cpp +++ b/src/batteryManagement.cpp @@ -1,10 +1,10 @@ - // 3. Post Event to System Loop - battery_data_t data = { .soc = soc, .voltage = voltage }; + // // 3. Post Event to System Loop + // battery_data_t data = { .soc = soc, .voltage = voltage }; - esp_event_post(BATTERY_EVENTS, BATTERY_EVENT_UPDATE, &data, sizeof(data), 0); + // esp_event_post(BATTERY_EVENTS, BATTERY_EVENT_UPDATE, &data, sizeof(data), 0); - // Optional: Post warnings - if (soc < 20.0) { - esp_event_post(BATTERY_EVENTS, BATTERY_EVENT_LOW, NULL, 0, 0); - } \ No newline at end of file + // // Optional: Post warnings + // if (soc < 20.0) { + // esp_event_post(BATTERY_EVENTS, BATTERY_EVENT_LOW, NULL, 0, 0); + // } \ No newline at end of file diff --git a/src/i2c.c b/src/i2c.c index 00bcad6..0548349 100644 --- a/src/i2c.c +++ b/src/i2c.c @@ -66,7 +66,7 @@ esp_err_t max17048_write_reg(uint8_t reg_addr, uint8_t MSB, uint8_t LSB) { esp_err_t max17048_friendly_write_reg(uint8_t reg_addr, uint8_t MSB, uint8_t LSB, uint8_t MSBmask, uint8_t LSBmask) { uint8_t origMSB, origLSB; - esp_err_t err = max17048_read_reg(reg_addr, origMSB, origLSB); + esp_err_t err = max17048_read_reg(reg_addr, &origMSB, &origLSB); MSB &= MSBmask; LSB &= LSBmask; MSB |= origMSB & ~MSBmask; diff --git a/src/idf_component.yml b/src/idf_component.yml index 6492a6f..e53e69c 100644 --- a/src/idf_component.yml +++ b/src/idf_component.yml @@ -1,2 +1,4 @@ dependencies: - bubblesnake/esp_socketio_client: "^1.0.0" \ No newline at end of file + bubblesnake/esp_socketio_client: "^1.0.0" + esp-nimble-cpp: + git: https://github.com/h2zero/esp-nimble-cpp.git \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 75f242f..6a036d1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include "calibration.hpp" #include "esp_pm.h" #include "mainEventLoop.hpp" +#include "max17048.h" // Global encoder instances Encoder* topEnc = new Encoder(ENCODER_PIN_A, ENCODER_PIN_B); @@ -24,6 +25,8 @@ void mainApp() { } ESP_ERROR_CHECK(ret); + main_event_queue = xQueueCreate(10, sizeof(main_event_type_t)); + WiFi::init(); Calibration::init(); @@ -31,6 +34,7 @@ void mainApp() { topEnc->init(); bottomEnc->init(); servoInit(); + max17048_init(); setupAndCalibrate(); diff --git a/src/mainEventLoop.cpp b/src/mainEventLoop.cpp index 56d1343..e33c7af 100644 --- a/src/mainEventLoop.cpp +++ b/src/mainEventLoop.cpp @@ -7,52 +7,74 @@ #include "encoder.hpp" #include "WiFi.hpp" #include "socketIO.hpp" +#include "max17048.h" +#include "esp_sleep.h" -TaskHandle_t wakeTaskHandle = NULL; +// Give C linkage so max17048.c (a C translation unit) can link against these +extern "C" { + TaskHandle_t wakeTaskHandle = NULL; + QueueHandle_t main_event_queue = NULL; +} -void wakeTimer(void* pvParameters) { - while (1) { - vTaskDelay(pdMS_TO_TICKS(60000)); - // avoid accumulating events during re-setup or calibration - if (setupTaskHandle != NULL || socketIOactive - || uxQueueMessagesWaiting(main_event_queue) > 2) continue; - main_event_type_t evt = EVENT_REQUEST_POS; - xQueueSend(main_event_queue, &evt, portMAX_DELAY); +// ── Battery helpers ─────────────────────────────────────────────────────────── + +static const char* battAlertTypeStr(batt_alert_type_t type) { + switch (type) { + case BATT_ALERT_OVERVOLTAGE: return "overvoltage"; + case BATT_ALERT_CRITICAL_LOW: return "critical_low"; + case BATT_ALERT_LOW_VOLTAGE_WARNING: return "low_voltage_warning"; + case BATT_ALERT_SOC_LOW_20: return "low_20"; + case BATT_ALERT_SOC_LOW_10: return "low_10"; + default: return "unknown"; } } +static bool postBatteryAlert(batt_alert_type_t type, uint8_t soc) { + cJSON* payload = cJSON_CreateObject(); + cJSON_AddStringToObject(payload, "type", battAlertTypeStr(type)); + cJSON_AddNumberToObject(payload, "soc", soc); + cJSON* response = nullptr; + bool ok = httpPOST("battery_alert", webToken, payload, response); + cJSON_Delete(payload); + if (response) cJSON_Delete(response); + return ok; +} + +static bool postBatterySoc(uint8_t soc) { + cJSON* payload = cJSON_CreateObject(); + cJSON_AddNumberToObject(payload, "soc", soc); + cJSON* response = nullptr; + bool ok = httpPOST("battery_update", webToken, payload, response); + cJSON_Delete(payload); + if (response) cJSON_Delete(response); + return ok; +} + +// ── Position helpers ────────────────────────────────────────────────────────── + bool postServoPos(uint8_t currentAppPos) { - // Create POST data cJSON* posData = cJSON_CreateObject(); cJSON_AddNumberToObject(posData, "port", 1); - cJSON_AddNumberToObject(posData, "pos", currentAppPos); - - // Send position update + cJSON_AddNumberToObject(posData, "pos", currentAppPos); + cJSON* response = nullptr; bool success = httpPOST("position", webToken, posData, response); cJSON_Delete(posData); - + if (success && response != nullptr) { - // Parse await_calib from response cJSON* awaitCalibItem = cJSON_GetObjectItem(response, "await_calib"); - bool awaitCalib = false; - if (cJSON_IsBool(awaitCalibItem)) { - awaitCalib = awaitCalibItem->valueint != 0; - } - + bool awaitCalib = cJSON_IsBool(awaitCalibItem) && awaitCalibItem->valueint != 0; printf("Position update sent: %d, await_calib=%d\n", currentAppPos, awaitCalib); cJSON_Delete(response); - + if (awaitCalib) { Calibration::clearCalibrated(); if (!calibrate()) { if (!WiFi::attemptDHCPrenewal()) setupAndCalibrate(); - else { - if (!calibrate()) { - printf("ERROR OCCURED: EVEN AFTER SETUP, SOCKET OPENING FAIL\n"); - setupAndCalibrate(); - } + else if (!calibrate()) { + printf("ERROR: EVEN AFTER DHCP RENEWAL, SOCKET OPENING FAIL\n"); + setupAndCalibrate(); } } } @@ -65,21 +87,17 @@ bool getServoPos() { bool success = httpGET("position", webToken, response); if (success && response != NULL) { - // Check if response is an array if (cJSON_IsArray(response)) { int arraySize = cJSON_GetArraySize(response); - - // Condition 1: More than one object in array + if (arraySize > 1) { printf("Multiple peripherals detected, entering setup.\n"); cJSON_Delete(response); return false; - } - // Condition 2: Check peripheral_number in first object - else if (arraySize > 0) { - cJSON *firstObject = cJSON_GetArrayItem(response, 0); + } else if (arraySize > 0) { + cJSON* firstObject = cJSON_GetArrayItem(response, 0); if (firstObject != NULL) { - cJSON *peripheralNum = cJSON_GetObjectItem(firstObject, "peripheral_number"); + cJSON* peripheralNum = cJSON_GetObjectItem(firstObject, "peripheral_number"); if (cJSON_IsNumber(peripheralNum) && peripheralNum->valueint != 1) { printf("Peripheral number is not 1, entering setup.\n"); cJSON_Delete(response); @@ -87,22 +105,19 @@ bool getServoPos() { } printf("Verified new token!\n"); - cJSON *awaitCalib = cJSON_GetObjectItem(firstObject, "await_calib"); + cJSON* awaitCalib = cJSON_GetObjectItem(firstObject, "await_calib"); if (cJSON_IsBool(awaitCalib)) { if (awaitCalib->valueint) { Calibration::clearCalibrated(); if (!calibrate()) { if (!WiFi::attemptDHCPrenewal()) setupAndCalibrate(); - else { - if (!calibrate()) { - printf("ERROR OCCURED: EVEN AFTER SETUP, SOCKET OPENING FAIL\n"); - setupAndCalibrate(); - } + else if (!calibrate()) { + printf("ERROR: EVEN AFTER DHCP RENEWAL, SOCKET OPENING FAIL\n"); + setupAndCalibrate(); } } - } - else { + } else { cJSON* pos = cJSON_GetObjectItem(firstObject, "last_pos"); runToAppPos(pos->valueint); } @@ -115,62 +130,86 @@ bool getServoPos() { return success; } -QueueHandle_t main_event_queue = NULL; +// ── Wake timer (fires every 60 s) ──────────────────────────────────────────── + +void wakeTimer(void* pvParameters) { + while (1) { + vTaskDelay(pdMS_TO_TICKS(60000)); + if (setupTaskHandle != NULL || socketIOactive + || uxQueueMessagesWaiting(main_event_queue) > 2) continue; + + main_event_type_t evt = EVENT_REQUEST_POS; + xQueueSend(main_event_queue, &evt, portMAX_DELAY); + + evt = EVENT_REPORT_SOC; + xQueueSend(main_event_queue, &evt, portMAX_DELAY); + } +} + +// ── Main event loop ─────────────────────────────────────────────────────────── void mainEventLoop() { - main_event_queue = xQueueCreate(10, sizeof(main_event_type_t)); main_event_type_t received_event_type; while (true) { - if (xQueueReceive(main_event_queue, &received_event_type, portMAX_DELAY)) { - if (received_event_type == EVENT_CLEAR_CALIB) { - Calibration::clearCalibrated(); - if (!calibrate()) { - if (!WiFi::attemptDHCPrenewal()) - setupAndCalibrate(); - else { - if (!calibrate()) { - printf("ERROR OCCURED: EVEN AFTER SETUP, SOCKET OPENING FAIL\n"); - setupAndCalibrate(); - } - } - } - } - else if (received_event_type == EVENT_SAVE_POS) { - servoSavePos(); + if (!xQueueReceive(main_event_queue, &received_event_type, portMAX_DELAY)) continue; - uint8_t currentAppPos = Calibration::convertToAppPos(topEnc->getCount()); - - if (!postServoPos(currentAppPos)) { - printf("Failed to send position update\n"); - if (!WiFi::attemptDHCPrenewal()) { - setupAndCalibrate(); - postServoPos(currentAppPos); - } - else { - if (!postServoPos(currentAppPos)) { - printf("renewed dhcp successfully, but still failed to post\n"); - setupAndCalibrate(); - } - } + if (received_event_type == EVENT_CLEAR_CALIB) { + Calibration::clearCalibrated(); + if (!calibrate()) { + if (!WiFi::attemptDHCPrenewal()) + setupAndCalibrate(); + else if (!calibrate()) { + printf("ERROR: EVEN AFTER DHCP RENEWAL, SOCKET OPENING FAIL\n"); + setupAndCalibrate(); } } - else if (received_event_type == EVENT_REQUEST_POS) { - if (!getServoPos()) { - printf("Failed to send position update\n"); - if (!WiFi::attemptDHCPrenewal()) { - setupAndCalibrate(); - getServoPos(); - } - else { - if (!getServoPos()) { - printf("renewed dhcp successfully, but still failed to post\n"); - setupAndCalibrate(); - } - } + + } else if (received_event_type == EVENT_SAVE_POS) { + servoSavePos(); + uint8_t currentAppPos = Calibration::convertToAppPos(topEnc->getCount()); + if (!postServoPos(currentAppPos)) { + printf("Failed to send position update\n"); + if (!WiFi::attemptDHCPrenewal()) { + setupAndCalibrate(); + postServoPos(currentAppPos); + } else if (!postServoPos(currentAppPos)) { + printf("Renewed DHCP but still failed to post position\n"); + setupAndCalibrate(); } } + + } else if (received_event_type == EVENT_REQUEST_POS) { + if (!getServoPos()) { + printf("Failed to get position\n"); + if (!WiFi::attemptDHCPrenewal()) { + setupAndCalibrate(); + getServoPos(); + } else if (!getServoPos()) { + printf("Renewed DHCP but still failed to get position\n"); + setupAndCalibrate(); + } + } + + } else if (received_event_type == EVENT_BATTERY_CRITICAL) { + // Stop the motor immediately, persist position, notify server, then hibernate. + // esp_deep_sleep_start() with no wakeup source = indefinite sleep until reset. + // The MAX17048 VRESET comparator handles detection of battery recovery. + servoOff(); + servoSavePos(); + batt_alert_type_t alertType = bms_pending_alert; // snapshot volatile + postBatteryAlert(alertType, established_soc); + printf("CRITICAL BATTERY EVENT (%s, SOC=%d%%). Entering deep sleep.\n", + battAlertTypeStr(alertType), established_soc); + esp_deep_sleep_start(); + + } else if (received_event_type == EVENT_BATTERY_WARNING) { + postBatteryAlert((batt_alert_type_t)bms_pending_alert, established_soc); + + } else if (received_event_type == EVENT_REPORT_SOC) { + postBatterySoc(established_soc); } } + vTaskDelete(NULL); -} \ No newline at end of file +} diff --git a/src/max17048.c b/src/max17048.c index 7a43ecf..6477c8e 100644 --- a/src/max17048.c +++ b/src/max17048.c @@ -1,44 +1,120 @@ #include "max17048.h" -#include "esp_err.h" +#include "mainEventLoop.hpp" #include "i2c.h" -#include "esp_timer.h" +#include "esp_err.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "freertos/task.h" static const char *TAG = "BATTERY"; -uint8_t established_soc; + +uint8_t established_soc = 100; +volatile batt_alert_type_t bms_pending_alert = BATT_ALERT_CRITICAL_LOW; + +static TaskHandle_t bms_event_handler = NULL; + +void IRAM_ATTR alrt_ISR(void* arg) { + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + vTaskNotifyGiveFromISR(bms_event_handler, &xHigherPriorityTaskWoken); + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); +} esp_err_t max17048_init() { esp_err_t err = ESP_OK; - uint8_t status, _; err |= i2c_init(); - err |= max17048_read_reg(MAX17048_REG_STATUS, &status, &_); - err |= bms_set_alert_bound_voltages(3.3, 4.2); - err |= bms_set_reset_voltage(3.25); + err |= bms_set_alert_bound_voltages(3.3f, 4.2f); + err |= bms_set_reset_voltage(3.25f); err |= bms_set_alsc(); - err |= bms_clear_status(); + xTaskCreate(bms_checker_task, "BMS", 4096, NULL, 20, &bms_event_handler); + + gpio_config_t io_conf = { + .pin_bit_mask = (1ULL << maxALRT), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_NEGEDGE, + }; + + gpio_config(&io_conf); + gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1); + gpio_isr_handler_add(maxALRT, alrt_ISR, NULL); + + ESP_LOGI(TAG, "MAX17048 initialized, ALRT interrupt on GPIO %d", maxALRT); + + err |= bms_clear_status(); + err |= bms_clear_alrt(); + return err; } -// Helper: Function reading MAX17048 SOC register, returning battery percentage uint8_t bms_get_soc() { - // uint16_t raw_soc, raw_vcell; uint16_t raw_soc; - - // Read SOC (Register 0x04) - if (max17048_read_reg(MAX17048_REG_SOC, ((uint8_t*)&raw_soc)+1, (uint8_t*)&raw_soc) == ESP_OK) { - return (raw_soc >> 8) + raw_soc & 0x80; // round to the nearest percent - } else { - ESP_LOGE(TAG, "Failed to read MAX17048"); - return 0; + if (max17048_read_reg(MAX17048_REG_SOC, ((uint8_t*)&raw_soc) + 1, (uint8_t*)&raw_soc) == ESP_OK) { + // upper byte = whole percent; lower byte msb = 0.5%; round to nearest + return (uint8_t)(raw_soc >> 8) + ((raw_soc & 0x80) ? 1 : 0); } + ESP_LOGE(TAG, "Failed to read SOC register"); + return 0; } esp_err_t bms_set_alert_bound_voltages(float min, float max) { - uint8_t minVal = (uint16_t)((float)min * 1000.0) / 20; - uint8_t maxVal = (uint16_t)((float)max * 1000.0) / 20; + uint8_t minVal = (uint8_t)((uint16_t)(min * 1000.0f) / 20); + uint8_t maxVal = (uint8_t)((uint16_t)(max * 1000.0f) / 20); return max17048_write_reg(MAX17048_REG_VALRT, minVal, maxVal); } esp_err_t bms_set_reset_voltage(float vreset) { - uint8_t maxVal = (uint16_t)((float)vreset * 1000.0) / 40; - max17048_write_reg(MAX17048_REG_VRST_ID, vreset, 0); -} \ No newline at end of file + uint8_t val = (uint8_t)((uint16_t)(vreset * 1000.0f) / 40); + return max17048_write_reg(MAX17048_REG_VRST_ID, val, 0); +} + +void bms_checker_task(void *pvParameters) { + uint8_t prev_soc = 100; + + while (true) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + uint8_t status, _; + if (max17048_read_reg(MAX17048_REG_STATUS, &status, &_) != ESP_OK) { + ESP_LOGE(TAG, "STATUS read failed"); + bms_clear_alrt(); + continue; + } + + uint8_t soc = bms_get_soc(); + established_soc = soc; + + // Clear before posting to queue so ALRT pin is deasserted promptly + bms_clear_status(); + bms_clear_alrt(); + + main_event_type_t evt; + + if ((status & VHbit) || (status & HDbit) || ((status & VLbit) && soc < SOC_CRITICAL_VL)) { + // Critical: overvoltage (hardware fault) or battery truly empty + bms_pending_alert = (status & VHbit) ? BATT_ALERT_OVERVOLTAGE : BATT_ALERT_CRITICAL_LOW; + evt = EVENT_BATTERY_CRITICAL; + xQueueSend(main_event_queue, &evt, portMAX_DELAY); + + } else if (status & VLbit) { + // Undervoltage but SOC still healthy — likely a transient load spike + bms_pending_alert = BATT_ALERT_LOW_VOLTAGE_WARNING; + evt = EVENT_BATTERY_WARNING; + xQueueSend(main_event_queue, &evt, portMAX_DELAY); + + } else if (status & SCbit) { + // 1% SOC change: check downward threshold crossings for user notifications + if (soc <= SOC_WARN_10 && prev_soc > SOC_WARN_10) { + bms_pending_alert = BATT_ALERT_SOC_LOW_10; + evt = EVENT_BATTERY_WARNING; + xQueueSend(main_event_queue, &evt, portMAX_DELAY); + } else if (soc <= SOC_WARN_20 && prev_soc > SOC_WARN_20) { + bms_pending_alert = BATT_ALERT_SOC_LOW_20; + evt = EVENT_BATTERY_WARNING; + xQueueSend(main_event_queue, &evt, portMAX_DELAY); + } + prev_soc = soc; + } + } +} diff --git a/src/servo.cpp b/src/servo.cpp index 82a8604..bc88551 100644 --- a/src/servo.cpp +++ b/src/servo.cpp @@ -40,11 +40,12 @@ void servoInit() { gpio_sleep_sel_dis(servoPin); // Configure servo power switch pin as output + gpio_reset_pin(servoSwitch); gpio_set_direction(servoSwitch, GPIO_MODE_OUTPUT); gpio_set_level(servoSwitch, 0); // Start with servo power off // Configure debug LED pin as output - gpio_reset_pin(GPIO_NUM_22); + gpio_reset_pin(debugLED); gpio_set_direction(debugLED, GPIO_MODE_OUTPUT); gpio_set_level(debugLED, 0); // Start with LED off diff --git a/src/setup.cpp b/src/setup.cpp index 7d7ad0b..1aed2ec 100644 --- a/src/setup.cpp +++ b/src/setup.cpp @@ -156,6 +156,7 @@ void setupLoop() { } else { printf("Token read unsuccessful, entering setup.\n"); + nvs_close(authHandle); initialSetup(); } }