Ready for test

This commit is contained in:
2026-03-09 02:29:03 -05:00
parent 4481a22f6b
commit b03b0c0f43
15 changed files with 344 additions and 159 deletions

View File

@@ -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) {

View File

@@ -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);
}
// // Optional: Post warnings
// if (soc < 20.0) {
// esp_event_post(BATTERY_EVENTS, BATTERY_EVENT_LOW, NULL, 0, 0);
// }

View File

@@ -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;

View File

@@ -1,2 +1,4 @@
dependencies:
bubblesnake/esp_socketio_client: "^1.0.0"
bubblesnake/esp_socketio_client: "^1.0.0"
esp-nimble-cpp:
git: https://github.com/h2zero/esp-nimble-cpp.git

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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);
}
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;
}
}
}

View File

@@ -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

View File

@@ -156,6 +156,7 @@ void setupLoop() {
}
else {
printf("Token read unsuccessful, entering setup.\n");
nvs_close(authHandle);
initialSetup();
}
}