groundwork for BMS

This commit is contained in:
2026-02-22 23:00:10 -06:00
parent 0179b767e9
commit 4481a22f6b
17 changed files with 194 additions and 10 deletions

419
src/BLE.cpp Normal file
View File

@@ -0,0 +1,419 @@
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "BLE.hpp"
#include "WiFi.hpp"
#include "nvs_flash.h"
#include "socketIO.hpp"
#include "defines.h"
#include <mutex>
#include "bmHTTP.hpp"
#include "setup.hpp"
#include "esp_mac.h"
std::atomic<bool> flag_scan_requested{false};
std::atomic<bool> isBLEClientConnected{false};
std::atomic<bool> scanBlock{false};
std::atomic<bool> finalAuth{false};
std::mutex dataMutex;
wifi_auth_mode_t auth;
static std::string SSID = "";
static std::string TOKEN = "";
static std::string PASS = "";
static std::string UNAME = "";
// Global pointers to characteristics for notification support
std::atomic<NimBLECharacteristic*> ssidListChar = nullptr;
std::atomic<NimBLECharacteristic*> connectConfirmChar = nullptr;
// Forward declarations
bool attemptUseWiFiCreds();
bool tokenCheck();
std::atomic<NimBLECharacteristic*> authConfirmChar = nullptr;
std::atomic<NimBLECharacteristic*> credsChar = nullptr;
std::atomic<NimBLECharacteristic*> tokenChar = nullptr;
std::atomic<NimBLECharacteristic*> ssidRefreshChar = nullptr;
std::atomic<NimBLECharacteristic*> deviceInfoChar = nullptr;
static QueueHandle_t BLE_event_queue = NULL;
static TaskHandle_t BLE_manager_task_handle = NULL;
SemaphoreHandle_t BLE_Queue_Shutdown_Semaphore = NULL;
NimBLEAdvertising* initBLE() {
BLE_event_queue = xQueueCreate(10, sizeof(BLE_event_type_t));
xTaskCreate(BLE_manager_task, "BLE", 8192, NULL, 5, &BLE_manager_task_handle);
finalAuth = false;
NimBLEDevice::init("BlindMaster-C6");
// Optional: Boost power for better range (ESP32-C6 supports up to +20dBm)
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
// Set security
NimBLEDevice::setSecurityAuth(false, false, true); // bonding=false, mitm=false, sc=true (Secure Connections)
NimBLEDevice::setSecurityIOCap(BLE_HS_IO_NO_INPUT_OUTPUT); // No input/output capability
NimBLEServer *pServer = NimBLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
pServer->advertiseOnDisconnect(true); // Automatically restart advertising on disconnect
NimBLEService *pService = pServer->createService("181C");
// Create all characteristics with callbacks
MyCharCallbacks* charCallbacks = new MyCharCallbacks();
// 0x0000 - SSID List (READ)
ssidListChar = pService->createCharacteristic(
"0000",
NIMBLE_PROPERTY::READ
);
ssidListChar.load()->createDescriptor("2902"); // Add BLE2902 descriptor for notifications
// 0x0001 - Credentials JSON (WRITE) - Replaces separate SSID/Password/Uname
// Expected JSON format: {"ssid":"network","password":"pass"}
credsChar = pService->createCharacteristic(
"0001",
NIMBLE_PROPERTY::WRITE
);
credsChar.load()->setCallbacks(charCallbacks);
// 0x0002 - Token (WRITE)
tokenChar = pService->createCharacteristic(
"0002",
NIMBLE_PROPERTY::WRITE
);
tokenChar.load()->setCallbacks(charCallbacks);
// 0x0003 - Auth Confirmation (READ + NOTIFY)
authConfirmChar = pService->createCharacteristic(
"0003",
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
);
authConfirmChar.load()->createDescriptor("2902"); // Add BLE2902 descriptor for notifications
// 0x0004 - SSID Refresh
ssidRefreshChar = pService->createCharacteristic(
"0004",
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
);
ssidRefreshChar.load()->setCallbacks(charCallbacks);
// 0x0005 - Connect Confirmation (READ + NOTIFY)
connectConfirmChar = pService->createCharacteristic(
"0005",
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
);
connectConfirmChar.load()->createDescriptor("2902"); // Add BLE2902 descriptor for notifications
// 0x0006 - Device Info (READ) - MAC address and other device details
deviceInfoChar = pService->createCharacteristic(
"0006",
NIMBLE_PROPERTY::READ
);
// Build device info JSON with MAC address
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
cJSON *infoRoot = cJSON_CreateObject();
cJSON_AddStringToObject(infoRoot, "mac", macStr);
cJSON_AddStringToObject(infoRoot, "firmware", "1.0.0");
cJSON_AddStringToObject(infoRoot, "model", "BlindMaster-C6");
char *infoJson = cJSON_PrintUnformatted(infoRoot);
deviceInfoChar.load()->setValue(std::string(infoJson));
cJSON_Delete(infoRoot);
free(infoJson);
// Start
pService->start();
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->addServiceUUID("181C");
pAdvertising->setName("BlindMaster-C6");
pAdvertising->enableScanResponse(true);
pAdvertising->setPreferredParams(0x06, 0x12); // Connection interval preferences
pAdvertising->start();
printf("BLE Started. Waiting...\n");
return pAdvertising;
}
void notifyConnectionStatus(bool success) {
NimBLECharacteristic* tmpConfChar = connectConfirmChar.load();
tmpConfChar->setValue(success ? "Connected" : "Error");
tmpConfChar->notify();
tmpConfChar->setValue(""); // Clear value after notify
}
void notifyAuthStatus(bool success) {
NimBLECharacteristic* tmpConfChar = authConfirmChar.load();
tmpConfChar->setValue(success ? "Authenticated" : "Error");
tmpConfChar->notify();
tmpConfChar->setValue(""); // Clear value after notify
}
void reset() {
BLE_Queue_Shutdown_Semaphore = xSemaphoreCreateBinary();
BLE_event_type_t event_type = EVENT_SHUTDOWN;
xQueueSend(BLE_event_queue, &event_type, portMAX_DELAY);
xSemaphoreTake(BLE_Queue_Shutdown_Semaphore, portMAX_DELAY);
vQueueDelete(BLE_event_queue);
esp_wifi_scan_stop();
if (!finalAuth) esp_wifi_disconnect();
scanBlock = false;
vSemaphoreDelete(BLE_Queue_Shutdown_Semaphore);
BLE_event_queue = xQueueCreate(10, sizeof(BLE_event_type_t));
xTaskCreate(BLE_manager_task, "BLE", 8192, NULL, 5, &BLE_manager_task_handle);
}
void MyServerCallbacks::onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) {
isBLEClientConnected = true;
printf("Client connected\n");
reset();
};
void MyServerCallbacks::onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) {
isBLEClientConnected = false;
printf("Client disconnected - reason: %d\n", reason);
reset();
}
void MyCharCallbacks::onRead(NimBLECharacteristic* pChar, NimBLEConnInfo& connInfo) {
printf("Characteristic Read\n");
}
void MyCharCallbacks::onWrite(NimBLECharacteristic* pChar, NimBLEConnInfo& connInfo) {
std::string val = pChar->getValue();
std::string uuidStr = pChar->getUUID().toString();
printf("onWrite called! UUID: %s, Value length: %d\n", uuidStr.c_str(), val.length());
// Load atomic pointers for comparison
NimBLECharacteristic* currentCredsChar = credsChar.load();
NimBLECharacteristic* currentTokenChar = tokenChar.load();
NimBLECharacteristic* currentRefreshChar = ssidRefreshChar.load();
// Check which characteristic was written to
if (pChar == currentCredsChar) {
// Credentials JSON characteristic
if (val.length() > 0) {
printf("Received JSON: %s\n", val.c_str());
// Parse JSON using cJSON
cJSON *root = cJSON_Parse(val.c_str());
if (root != NULL) {
cJSON *ssid = cJSON_GetObjectItem(root, "ssid");
cJSON *password = cJSON_GetObjectItem(root, "password");
cJSON *authType = cJSON_GetObjectItem(root, "auth");
cJSON *uname = cJSON_GetObjectItem(root, "uname");
bool enterprise = false;
bool open = false;
bool error = false;
if (cJSON_IsNumber(authType)) {
enterprise = authType->valueint == WIFI_AUTH_WPA2_ENTERPRISE ||
authType->valueint == WIFI_AUTH_WPA3_ENTERPRISE;
open = authType->valueint == WIFI_AUTH_OPEN;
error = authType->valueint < 0 || authType->valueint >= WIFI_AUTH_MAX;
}
else error = true;
if (error) {
printf("ERROR: Invalid Auth mode passed in with JSON.\n");
cJSON_Delete(root);
return;
}
bool ssidPresent = cJSON_IsString(ssid) && ssid->valuestring != NULL;
bool passPresent = cJSON_IsString(password) && password->valuestring != NULL;
bool unamePresent = cJSON_IsString(uname) && uname->valuestring != NULL;
bool tempCredsGiven = ssidPresent && (passPresent || open) &&
(unamePresent || !enterprise);
if (tempCredsGiven) {
printf("Received credentials, will attempt connection\n");
{
std::lock_guard<std::mutex> lock(dataMutex);
auth = (wifi_auth_mode_t)(authType->valueint);
SSID = ssid->valuestring;
PASS = passPresent ? password->valuestring : "";
UNAME = unamePresent ? uname->valuestring : "";
}
BLE_event_type_t event_type = EVENT_CREDS_GIVEN;
if (xQueueSend(BLE_event_queue, &event_type, pdMS_TO_TICKS(10)) == pdPASS)
printf("Successfully added credsGiven to event queue\n");
else printf("CredsGiven event queue addition failed\n");
}
else printf("ERROR: Did not receive necessary credentials.\n");
cJSON_Delete(root);
} else {
printf("Failed to parse JSON\n");
}
}
}
else if (pChar == currentTokenChar) {
if (val.length() > 0) {
printf("Received Token: %s\n", val.c_str());
{
std::lock_guard<std::mutex> lock(dataMutex);
TOKEN = val;
}
BLE_event_type_t event_type = EVENT_TOKEN_GIVEN;
if (xQueueSend(BLE_event_queue, &event_type, pdMS_TO_TICKS(10)) == pdPASS)
printf("Successfully added tokenGiven to event queue\n");
else printf("TokenGiven event queue addition failed\n");
}
}
else if (pChar == currentRefreshChar) {
if (val == "Start") {
// Refresh characteristic
BLE_event_type_t event_type = EVENT_SCAN_REQUESTED;
if (xQueueSend(BLE_event_queue, &event_type, pdMS_TO_TICKS(10)) == pdPASS) {
printf("Event queue addition success for scan start\n");
}
else printf("Scan start event queue addition failed\n");
}
else if (val == "Done") {
printf("Data read complete\n");
scanBlock = false;
}
}
else printf("Unknown UUID: %s\n", uuidStr.c_str());
}
void BLE_manager_task(void *pvParameters) {
BLE_event_type_t received_event_type;
while (true) {
if (xQueueReceive(BLE_event_queue, &received_event_type, portMAX_DELAY)) {
if (received_event_type == EVENT_SCAN_REQUESTED) {
printf("Refresh Requested\n");
if (!scanBlock) {
scanBlock = true;
printf("Scanning WiFi...\n");
WiFi::scanAndUpdateSSIDList();
}
else printf("Duplicate scan request\n");
}
else if (received_event_type == EVENT_TOKEN_GIVEN) {
if (tokenCheck()) {
vQueueDelete(BLE_event_queue);
if (setupTaskHandle != NULL) {
xTaskNotifyGive(setupTaskHandle);
printf("Setup complete.\n");
}
break;
}
}
else if (received_event_type == EVENT_CREDS_GIVEN)
attemptUseWiFiCreds();
else if (received_event_type == EVENT_SHUTDOWN) break;
}
}
xSemaphoreGive(BLE_Queue_Shutdown_Semaphore); // this is null-safe
vTaskDelete(NULL);
}
bool tokenCheck() {
if (!WiFi::isConnected()) {
printf("ERROR: token given without WiFi connection\n");
notifyAuthStatus(false);
return false;
}
// HTTP request to verify device with token
std::string tmpTOKEN;
{
std::lock_guard<std::mutex> lock(dataMutex);
tmpTOKEN = TOKEN;
}
cJSON *responseRoot;
bool success = httpGET("verify_device", tmpTOKEN, responseRoot);
if (!success) return false;
success = false;
if (responseRoot != NULL) {
cJSON *tokenItem = cJSON_GetObjectItem(responseRoot, "token");
if (cJSON_IsString(tokenItem) && tokenItem->valuestring != NULL) {
printf("New token received: %s\n", tokenItem->valuestring);
// Save token to NVS
nvs_handle_t AuthHandle;
esp_err_t nvs_err = nvs_open(nvsAuth, NVS_READWRITE, &AuthHandle);
if (nvs_err == ESP_OK) {
nvs_err = nvs_set_str(AuthHandle, tokenTag, tokenItem->valuestring);
if (nvs_err == ESP_OK) {
nvs_commit(AuthHandle);
success = true;
webToken = tokenItem->valuestring;
}
else printf("ERROR: could not save webToken to NVS\n");
nvs_close(AuthHandle);
}
else printf("ERROR: Couldn't open NVS for auth token\n");
}
cJSON_Delete(responseRoot);
}
else printf("Failed to parse JSON response\n");
finalAuth = true;
notifyAuthStatus(success);
if (success) NimBLEDevice::deinit(true); // deinitialize BLE
return success;
}
bool attemptUseWiFiCreds() {
std::string tmpSSID;
std::string tmpUNAME;
std::string tmpPASS;
wifi_auth_mode_t tmpAUTH;
{
std::lock_guard<std::mutex> lock(dataMutex);
tmpSSID = SSID;
tmpUNAME = UNAME;
tmpPASS = PASS;
tmpAUTH = auth;
}
bool wifiConnect;
if (tmpAUTH == WIFI_AUTH_WPA2_ENTERPRISE || tmpAUTH == WIFI_AUTH_WPA3_ENTERPRISE)
wifiConnect = WiFi::attemptConnect(tmpSSID.c_str(), tmpUNAME.c_str(), tmpPASS.c_str(), tmpAUTH);
else wifiConnect = WiFi::attemptConnect(tmpSSID.c_str(), tmpPASS.c_str(), tmpAUTH);
if (!wifiConnect) {
// notify errored
notifyConnectionStatus(false);
return false;
}
nvs_handle_t WiFiHandle;
esp_err_t err = nvs_open(nvsWiFi, NVS_READWRITE, &WiFiHandle);
if (err != ESP_OK) {
printf("ERROR Saving Credentials\n");
// notify errored
notifyConnectionStatus(false);
return false;
}
else {
err = nvs_set_str(WiFiHandle, ssidTag, tmpSSID.c_str());
if (err == ESP_OK) err = nvs_set_str(WiFiHandle, passTag, tmpPASS.c_str());
if (err == ESP_OK) err = nvs_set_str(WiFiHandle, unameTag, tmpUNAME.c_str());
if (err == ESP_OK) err = nvs_set_u8(WiFiHandle, authTag, (uint8_t)tmpAUTH);
if (err == ESP_OK) nvs_commit(WiFiHandle);
nvs_close(WiFiHandle);
}
if (err == ESP_OK) {
// notify connected
notifyConnectionStatus(true);
return true;
}
else {
// notify errored
notifyConnectionStatus(false);
return false;
}
}

View File

@@ -1,7 +1,7 @@
# This file was automatically generated for projects
# without default 'CMakeLists.txt' file.
FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.* ${CMAKE_SOURCE_DIR}/include/*.cpp)
FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*)
idf_component_register(SRCS ${app_sources}
INCLUDE_DIRS "."

320
src/WiFi.cpp Normal file
View File

@@ -0,0 +1,320 @@
#include "WiFi.hpp"
#include "esp_eap_client.h"
#include "cJSON.h" // Native replacement for ArduinoJson
#include "BLE.hpp"
#include "esp_wifi_he.h"
TaskHandle_t WiFi::awaitConnectHandle = NULL;
EventGroupHandle_t WiFi::s_wifi_event_group = NULL;
esp_netif_t* WiFi::netif = NULL;
esp_event_handler_instance_t WiFi::instance_any_id = NULL;
esp_event_handler_instance_t WiFi::instance_got_ip = NULL;
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_STARTED_BIT BIT1
// The Event Handler (The engine room)
void WiFi::event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data) {
// WiFi driver has finished initialization
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
printf("WiFi initialized and ready\n");
xEventGroupSetBits(s_wifi_event_group, WIFI_STARTED_BIT);
}
// We got disconnected -> Retry automatically
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
// 1. Cast the data to the correct struct
wifi_event_sta_disconnected_t* event = (wifi_event_sta_disconnected_t*) event_data;
printf("WiFi Disconnected. Reason Code: %d\n", event->reason);
// 2. Check specific Reason Codes
switch (event->reason) {
case WIFI_REASON_AUTH_EXPIRE: // Reason 2
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: // Reason 15 (Most Common for Wrong Pass)
case WIFI_REASON_BEACON_TIMEOUT: // Reason 200
case WIFI_REASON_AUTH_FAIL: // Reason 202
case WIFI_REASON_HANDSHAKE_TIMEOUT: // Reason 204
printf("ERROR: Likely Wrong Password!\n");
if (awaitConnectHandle != NULL) {
xTaskNotify(awaitConnectHandle, false, eSetValueWithOverwrite);
}
break;
case WIFI_REASON_NO_AP_FOUND: // Reason 201
printf("ERROR: SSID Not Found\n");
if (awaitConnectHandle != NULL) {
xTaskNotify(awaitConnectHandle, false, eSetValueWithOverwrite);
}
break;
case WIFI_REASON_ASSOC_LEAVE: // Reason 8 - Manual disconnect
printf("Manual disconnect, not retrying\n");
break;
case WIFI_REASON_ASSOC_FAIL: // Reason 203 (Can be AP busy/rate limiting)
printf("Association failed, will retry...\n");
vTaskDelay(pdMS_TO_TICKS(1000)); // Wait 1 second before retry to avoid rate limiting
esp_netif_dhcpc_start(netif);
esp_wifi_connect();
break;
default:
printf("Retrying...\n");
esp_netif_dhcpc_start(netif);
esp_wifi_connect();
break;
}
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) {
// This is triggered when the scan finishes!
printf("Scan complete, processing results...\n");
// Call a function to process results and notify BLE
processScanResults();
}
// 3. We got an IP Address -> Success!
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
printf("Got IP: " IPSTR "\n", IP2STR(&event->ip_info.ip));
esp_netif_dhcpc_stop(netif);
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
if (awaitConnectHandle != NULL) {
xTaskNotify(awaitConnectHandle, true, eSetValueWithOverwrite);
}
}
}
void WiFi::init() {
s_wifi_event_group = xEventGroupCreate();
// 1. Init Network Interface
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
netif = esp_netif_create_default_wifi_sta();
// 2. Init WiFi Driver
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// 3. Set Mode to Station (Client)
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_event_handler_instance_register(
WIFI_EVENT, // The "Topic" (Base)
ESP_EVENT_ANY_ID, // The specific "Subject" (Any ID)
&event_handler, // The Function to call
NULL, // Argument to pass to the function (optional)
&instance_any_id // Where to store the registration handle
));
ESP_ERROR_CHECK(esp_event_handler_instance_register(
IP_EVENT,
IP_EVENT_STA_GOT_IP,
&event_handler,
NULL,
&instance_got_ip
));
ESP_ERROR_CHECK(esp_wifi_start());
xEventGroupWaitBits(s_wifi_event_group, WIFI_STARTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
awaitConnectHandle = NULL;
}
// --- CHECK STATUS ---
bool WiFi::isConnected() {
if (s_wifi_event_group == NULL) return false;
EventBits_t bits = xEventGroupGetBits(s_wifi_event_group);
return (bits & WIFI_CONNECTED_BIT);
}
// --- GET IP AS STRING ---
std::string WiFi::getIP() {
esp_netif_ip_info_t ip_info;
esp_netif_get_ip_info(netif, &ip_info);
char buf[20];
sprintf(buf, IPSTR, IP2STR(&ip_info.ip));
return std::string(buf);
}
bool WiFi::attemptConnect(const std::string ssid, const std::string password,
const wifi_auth_mode_t authMode) {
esp_wifi_sta_enterprise_disable();
esp_wifi_disconnect();
wifi_config_t wifi_config = {};
snprintf((char*)wifi_config.sta.ssid, sizeof(wifi_config.sta.ssid), "%s", ssid.c_str());
snprintf((char*)wifi_config.sta.password, sizeof(wifi_config.sta.password), "%s", password.c_str());
wifi_config.sta.threshold.authmode = authMode;
wifi_config.sta.pmf_cfg.capable = true;
wifi_config.sta.pmf_cfg.required = false;
esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
return awaitConnected();
}
bool WiFi::attemptConnect(const std::string ssid, const std::string uname,
const std::string password, const wifi_auth_mode_t authMode) {
esp_wifi_disconnect();
// 1. Auto-generate the Identity
std::string identity = "anonymous";
size_t atPos = uname.find('@');
if (atPos != std::string::npos) identity += uname.substr(atPos);
printf("Real User: %s\n", uname.c_str());
printf("Outer ID : %s (Privacy Safe)\n", identity.c_str());
wifi_config_t wifi_config = {};
snprintf((char*)wifi_config.sta.ssid, sizeof(wifi_config.sta.ssid), "%s", ssid.c_str());
wifi_config.sta.threshold.authmode = authMode;
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
// 3. Set the calculated identity (using new ESP-IDF v5.x API)
esp_wifi_sta_enterprise_enable();
esp_eap_client_set_identity((uint8_t *)identity.c_str(), identity.length());
esp_eap_client_set_username((uint8_t *)uname.c_str(), uname.length());
esp_eap_client_set_password((uint8_t *)password.c_str(), password.length());
return awaitConnected();
}
bool WiFi::awaitConnected() {
awaitConnectHandle = xTaskGetCurrentTaskHandle();
if (esp_wifi_connect() != ESP_OK) {
awaitConnectHandle = NULL;
return false;
}
uint32_t status;
uint8_t MAX_TIMEOUT = 10; //seconds
if (xTaskNotifyWait(0, ULONG_MAX, &status, pdMS_TO_TICKS(MAX_TIMEOUT * 1000)) == pdTRUE) {
awaitConnectHandle = NULL;
if (!status) {
printf("SSID/Password was wrong! Aborting connection attempt.\n");
return false;
}
} else {
// Timeout - check if connected anyway
awaitConnectHandle = NULL;
}
if (isConnected()) {
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
uint8_t protocol_bitmap = 0;
esp_err_t err = esp_wifi_get_protocol(WIFI_IF_STA, &protocol_bitmap);
if (err == ESP_OK && (protocol_bitmap & WIFI_PROTOCOL_11AX)) {
// WiFi 6 (802.11ax) - Use Target Wake Time (TWT) for power saving
wifi_twt_setup_config_t twt_config = {
.setup_cmd = TWT_REQUEST,
.trigger = true,
.flow_type = 0, // Announced
.flow_id = 0,
.wake_invl_expn = 12, // Exponent for interval
.min_wake_dura = 255, // ~65ms (unit is 256 microseconds)
.wake_invl_mant = 14648, // Mantissa (mant * 2^exp = 60,000,000 us = 60s)
.timeout_time_ms = 5000,
};
esp_wifi_sta_itwt_setup(&twt_config);
}
return true;
}
return false;
}
// ------------- non-class --------------
void WiFi::scanAndUpdateSSIDList() {
printf("Starting WiFi Scan...\n");
esp_wifi_sta_enterprise_disable();
esp_wifi_disconnect();
wifi_scan_config_t scan_config = {
.ssid = NULL,
.bssid = NULL,
.channel = 0,
.show_hidden = false
};
esp_err_t err = esp_wifi_scan_start(&scan_config, false);
if (err != ESP_OK) {
printf("Scan failed!\n");
return;
}
}
void WiFi::processScanResults() {
// 2. Get the results
uint16_t ap_count = 0;
esp_wifi_scan_get_ap_num(&ap_count);
// Limit to 10 networks to save RAM/BLE MTU space
if (ap_count > 10) ap_count = 10;
wifi_ap_record_t *ap_list = (wifi_ap_record_t *)malloc(sizeof(wifi_ap_record_t) * ap_count);
if (ap_list == NULL) {
printf("Heap allocation error in processScanResults\n");
return;
}
esp_err_t err = esp_wifi_scan_get_ap_records(&ap_count, ap_list);
if (err != ESP_OK) {
printf("Failed to get scan records\n");
free(ap_list);
return;
}
// 3. Build JSON using cJSON
cJSON *root = cJSON_CreateArray();
for (int i = 0; i < ap_count; i++) {
cJSON *item = cJSON_CreateObject();
// ESP-IDF stores SSID as uint8_t, cast to char*
cJSON_AddStringToObject(item, "ssid", (char *)ap_list[i].ssid);
cJSON_AddNumberToObject(item, "rssi", ap_list[i].rssi);
// Add encryption type if you want (optional)
cJSON_AddNumberToObject(item, "auth", ap_list[i].authmode);
cJSON_AddItemToArray(root, item);
}
// 4. Convert to String
char *json_string = cJSON_PrintUnformatted(root); // Compact JSON
printf("JSON: %s\n", json_string);
// 5. Update BLE
if (ssidListChar != nullptr) {
ssidListChar.load()->setValue(std::string(json_string));
NimBLECharacteristic *tmpRefreshChar = ssidRefreshChar.load();
tmpRefreshChar->setValue("Ready");
tmpRefreshChar->notify();
}
// 6. Cleanup Memory
free(ap_list);
cJSON_Delete(root); // This deletes all children (items) too
free(json_string); // cJSON_Print allocates memory, you must free it
}
bool WiFi::attemptDHCPrenewal() {
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
esp_netif_dhcpc_start(netif);
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT,
pdFALSE,
pdFALSE,
pdMS_TO_TICKS(4000));
if (bits & WIFI_CONNECTED_BIT) {
printf("renewal success");
// Stop the client again to save power.
esp_netif_dhcpc_stop(netif);
return true;
}
printf("DHCP Renewal failed. Reconnecting Wi-Fi...\n");
esp_wifi_disconnect();
return awaitConnected();
}

10
src/batteryManagement.cpp Normal file
View File

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

141
src/bmHTTP.cpp Normal file
View File

@@ -0,0 +1,141 @@
#include "bmHTTP.hpp"
#include "esp_http_client.h"
#include "nvs_flash.h"
#include "defines.h"
#include "esp_crt_bundle.h"
std::string webToken;
const std::string urlBase = std::string("http") + (secureSrv ? "s" : "") + "://" + srvAddr + "/";
esp_err_t _http_event_handler(esp_http_client_event_t *evt) {
switch(evt->event_id) {
case HTTP_EVENT_ON_DATA: {
// Append received data to buffer (handles both chunked and non-chunked)
if (evt->data_len > 0 && evt->user_data != NULL) {
std::string* rxBuffer = (std::string*)evt->user_data;
rxBuffer->append((char*)evt->data, evt->data_len);
}
break;
}
default:
break;
}
return ESP_OK;
}
bool httpGET(std::string endpoint, std::string token, cJSON* &JSONresponse) {
std::string url = urlBase + endpoint;
std::string responseBuffer = "";
esp_http_client_config_t config = {};
config.url = url.c_str();
config.event_handler = _http_event_handler; // Attach the bucket
config.user_data = &responseBuffer; // Pass pointer to our string so the handler can write to it
if (secureSrv) {
config.transport_type = HTTP_TRANSPORT_OVER_SSL;
config.crt_bundle_attach = esp_crt_bundle_attach;
}
esp_http_client_handle_t client = esp_http_client_init(&config);
// Add authorization header
std::string authHeader = "Bearer " + token;
esp_http_client_set_header(client, "Authorization", authHeader.c_str());
// Open connection and fetch headers
esp_err_t err = esp_http_client_perform(client);
bool success = false;
if (err == ESP_OK) {
int status_code = esp_http_client_get_status_code(client);
printf("Status = %d, Content Length = %d\n", status_code, esp_http_client_get_content_length(client));
if (status_code == 200) {
printf("Response: %s\n", responseBuffer.c_str());
JSONresponse = cJSON_Parse(responseBuffer.c_str());
if (JSONresponse) success = true;
}
} else {
printf("HTTP GET failed: %s\n", esp_err_to_name(err));
}
esp_http_client_cleanup(client);
return success;
}
bool httpPOST(std::string endpoint, std::string token, cJSON* postData, cJSON* &JSONresponse) {
std::string url = urlBase + endpoint;
std::string responseBuffer = "";
// Convert JSON object to string
char* postString = cJSON_PrintUnformatted(postData);
if (postString == NULL) {
printf("Failed to serialize JSON for POST\n");
return false;
}
esp_http_client_config_t config = {};
config.url = url.c_str();
config.method = HTTP_METHOD_POST;
config.event_handler = _http_event_handler;
config.user_data = &responseBuffer;
if (secureSrv) {
config.transport_type = HTTP_TRANSPORT_OVER_SSL;
config.crt_bundle_attach = esp_crt_bundle_attach;
}
esp_http_client_handle_t client = esp_http_client_init(&config);
// Set headers
std::string authHeader = "Bearer " + token;
esp_http_client_set_header(client, "Authorization", authHeader.c_str());
esp_http_client_set_header(client, "Content-Type", "application/json");
// Set POST data
esp_http_client_set_post_field(client, postString, strlen(postString));
// Perform request
esp_err_t err = esp_http_client_perform(client);
bool success = false;
if (err == ESP_OK) {
int status_code = esp_http_client_get_status_code(client);
printf("Status = %d, Content Length = %d\n", status_code, esp_http_client_get_content_length(client));
if (status_code == 200 || status_code == 201) {
printf("Response: %s\n", responseBuffer.c_str());
JSONresponse = cJSON_Parse(responseBuffer.c_str());
if (JSONresponse) success = true;
}
} else {
printf("HTTP POST failed: %s\n", esp_err_to_name(err));
}
free(postString);
esp_http_client_cleanup(client);
return success;
}
void deleteWiFiAndTokenDetails() {
nvs_handle_t wifiHandle;
if (nvs_open(nvsWiFi, NVS_READWRITE, &wifiHandle) == ESP_OK) {
if (nvs_erase_all(wifiHandle) == ESP_OK) {
printf("Successfully erased WiFi details\n");
nvs_commit(wifiHandle);
}
else printf("ERROR: Erase wifi failed\n");
nvs_close(wifiHandle);
}
else printf("ERROR: Failed to open WiFi section for deletion\n");
nvs_handle_t authHandle;
if (nvs_open(nvsAuth, NVS_READWRITE, &authHandle) == ESP_OK) {
if (nvs_erase_all(authHandle) == ESP_OK) {
printf("Successfully erased Auth details\n");
nvs_commit(authHandle);
}
else printf("ERROR: Erase auth failed\n");
nvs_close(authHandle);
}
else printf("ERROR: Failed to open Auth section for deletion\n");
}

164
src/calibration.cpp Normal file
View File

@@ -0,0 +1,164 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "calibration.hpp"
#include "defines.h"
#include "nvs_flash.h"
#include "socketIO.hpp"
#include <limits.h>
// Static member definitions
std::atomic<bool> Calibration::calibrated{false};
std::atomic<int32_t> Calibration::UpTicks{0};
std::atomic<int32_t> Calibration::DownTicks{0};
TaskHandle_t calibTaskHandle = NULL;
void Calibration::init() {
nvs_handle_t calibHandle;
if (nvs_open(nvsCalib, NVS_READONLY, &calibHandle) == ESP_OK) {
int32_t tempUpTicks;
int32_t tempDownTicks;
uint8_t tempCalib;
esp_err_t err = ESP_OK;
err |= nvs_get_i32(calibHandle, UpTicksTag, &tempUpTicks);
err |= nvs_get_i32(calibHandle, DownTicksTag, &tempDownTicks);
err |= nvs_get_u8(calibHandle, statusTag, &tempCalib);
if (err == ESP_OK) {
UpTicks = tempUpTicks;
DownTicks = tempDownTicks;
calibrated = tempCalib;
printf("Range: %d - %d\n", tempUpTicks, tempDownTicks);
}
else {
printf("Data missing from NVS\n");
calibrated = false;
}
nvs_close(calibHandle);
}
else {
printf("CALIBINIT: failed to open NVS - not created?\n");
calibrated = false;
}
}
bool Calibration::clearCalibrated() {
if (!calibrated) return true;
// clear variable and NVS
calibrated = false;
nvs_handle_t calibHandle;
if (nvs_open(nvsCalib, NVS_READWRITE, &calibHandle) == ESP_OK) {
if (nvs_set_u8(calibHandle, statusTag, false) != ESP_OK) {
printf("Error saving calibration status as false.\n");
return false;
}
nvs_commit(calibHandle);
nvs_close(calibHandle);
}
else {
printf("Error opening calibration NVS segment.\n");
return false;
}
return true;
}
bool Calibration::beginDownwardCalib(Encoder& topEnc) {
int32_t tempUpTicks = topEnc.getCount();
nvs_handle_t calibHandle;
if (nvs_open(nvsCalib, NVS_READWRITE, &calibHandle) == ESP_OK) {
if (nvs_set_i32(calibHandle, UpTicksTag, tempUpTicks) == ESP_OK) {
printf("Saved UpTicks to NVS\n");
UpTicks = tempUpTicks;
nvs_commit(calibHandle);
}
else {
printf("Error saving UpTicks.\n");
return false;
}
nvs_close(calibHandle);
}
else {
printf("Error opening NVS to save UpTicks\n");
return false;
}
return true;
}
bool Calibration::completeCalib(Encoder& topEnc) {
int32_t tempDownTicks = topEnc.getCount();
if (tempDownTicks == UpTicks) {
printf("ERROR: NO RANGE\n");
return false;
}
nvs_handle_t calibHandle;
if (nvs_open(nvsCalib, NVS_READWRITE, &calibHandle) == ESP_OK) {
esp_err_t err = ESP_OK;
err |= nvs_set_i32(calibHandle, DownTicksTag, tempDownTicks);
err |= nvs_set_u8(calibHandle, statusTag, true);
if (err != ESP_OK) {
printf("Error saving calibration data.\n");
return false;
}
DownTicks = tempDownTicks;
calibrated = true;
printf("Range: %d - %d\n", UpTicks.load(), tempDownTicks);
nvs_commit(calibHandle);
nvs_close(calibHandle);
}
else {
printf("Error opening calibration NVS segment.\n");
return false;
}
return true;
}
int32_t Calibration::convertToTicks(uint8_t appPos) {
// appPos between 0 and 10, convert to target encoder ticks.
return (((int32_t)appPos * (UpTicks - DownTicks)) / 10) + DownTicks;
}
uint8_t Calibration::convertToAppPos(int32_t ticks) {
// appPos between 0 and 10, convert to target encoder ticks.
int8_t retVal = (ticks - DownTicks) * 10 / (UpTicks - DownTicks);
return (retVal < 0) ? 0 : ((retVal > 10) ? 10 : retVal);
}
bool calibrate() {
calibTaskHandle = xTaskGetCurrentTaskHandle();
printf("Connecting to Socket.IO server for calibration...\n");
initSocketIO();
// Wait for device_init message from server with timeout
int timeout_count = 0;
const int MAX_TIMEOUT = 60; // seconds
uint32_t status;
// Wait for notification with timeout
if (xTaskNotifyWait(0, ULONG_MAX, &status, pdMS_TO_TICKS(MAX_TIMEOUT * 1000)) == pdTRUE) {
// Notification received within timeout
if (status) {
printf("Connected successfully, awaiting destroy command\n");
xTaskNotifyWait(0, ULONG_MAX, &status, portMAX_DELAY);
calibTaskHandle = NULL;
if (status == 2) { // calibration complete
printf("Calibration process complete\n");
stopSocketIO();
return true;
}
else { // unexpected disconnect
printf("Disconnected unexpectedly!\n");
stopSocketIO();
return false;
}
} else {
calibTaskHandle = NULL;
printf("Connection failed! Returning to setup.\n");
stopSocketIO();
return false;
}
} else {
// Timeout reached
calibTaskHandle = NULL;
printf("Timeout waiting for device_init - connection failed\n");
stopSocketIO();
return false;
}
}

130
src/encoder.cpp Normal file
View File

@@ -0,0 +1,130 @@
#include "encoder.hpp"
#include "driver/gpio.h"
#include "esp_log.h"
#include "soc/gpio_struct.h"
#include "servo.hpp"
static const char *TAG = "ENCODER";
// Constructor
Encoder::Encoder(gpio_num_t pinA, gpio_num_t pinB)
: pin_a(pinA), pin_b(pinB), count(0),
last_state_a(0), last_state_b(0), last_count_base(0),
watchdog_handle(nullptr) {}
// Static ISR - receives Encoder instance via arg
void IRAM_ATTR Encoder::isr_handler(void* arg)
{
Encoder* encoder = static_cast<Encoder*>(arg);
// Read GPIO levels directly from hardware
uint32_t gpio_levels = GPIO.in.val;
uint8_t current_a = (gpio_levels >> encoder->pin_a) & 0x1;
uint8_t current_b = (gpio_levels >> encoder->pin_b) & 0x1;
// Quadrature decoding logic
if (current_a != encoder->last_state_a) {
if (!current_a) {
if (current_b) encoder->last_count_base++;
else encoder->last_count_base--;
}
else {
if (current_b) encoder->last_count_base--;
else encoder->last_count_base++;
}
}
else if (current_b != encoder->last_state_b) {
if (!current_b) {
if (current_a) encoder->last_count_base--;
else encoder->last_count_base++;
}
else {
if (current_a) encoder->last_count_base++;
else encoder->last_count_base--;
}
}
// Accumulate to full detent count
if (encoder->last_count_base > 3) {
encoder->count += 1;
encoder->last_count_base -= 4;
if (calibListen) servoCalibListen();
if (encoder->feedWDog) {
esp_timer_stop(encoder->watchdog_handle);
esp_timer_start_once(encoder->watchdog_handle, 500000);
debugLEDTgl();
}
if (encoder->wandListen) servoWandListen();
if (encoder->serverListen) servoServerListen();
}
else if (encoder->last_count_base < 0) {
encoder->count -= 1;
encoder->last_count_base += 4;
if (calibListen) servoCalibListen();
if (encoder->feedWDog) {
esp_timer_stop(encoder->watchdog_handle);
esp_timer_start_once(encoder->watchdog_handle, 500000);
debugLEDTgl();
}
if (encoder->wandListen) servoWandListen();
if (encoder->serverListen) servoServerListen();
}
encoder->last_state_a = current_a;
encoder->last_state_b = current_b;
}
void Encoder::init()
{
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_ANYEDGE;
io_conf.pin_bit_mask = (1ULL << pin_a) | (1ULL << pin_b);
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_config(&io_conf);
// Install ISR service if not already installed
gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1);
// Attach ISR with THIS instance as argument
gpio_isr_handler_add(pin_a, Encoder::isr_handler, this);
gpio_isr_handler_add(pin_b, Encoder::isr_handler, this);
ESP_LOGI(TAG, "Encoder initialized on pins %d and %d", pin_a, pin_b);
}
void Encoder::deinit()
{
gpio_isr_handler_remove(pin_a);
gpio_isr_handler_remove(pin_b);
ESP_LOGI(TAG, "Encoder deinitialized");
}
void Encoder::setupWatchdog() {
if (watchdog_handle == NULL) {
const esp_timer_create_args_t enc_watchdog_args = {
.callback = &watchdogCallback,
.dispatch_method = ESP_TIMER_ISR,
.name = "encoder_wdt",
};
ESP_ERROR_CHECK(esp_timer_create(&enc_watchdog_args, &watchdog_handle));
}
ESP_ERROR_CHECK(esp_timer_start_once(watchdog_handle, 500000));
feedWDog = true;
}
void IRAM_ATTR Encoder::pauseWatchdog() {
if (watchdog_handle != nullptr) esp_timer_stop(watchdog_handle);
feedWDog = false;
}
Encoder::~Encoder() {
if (watchdog_handle != NULL) {
esp_timer_stop(watchdog_handle);
esp_timer_delete(watchdog_handle);
watchdog_handle = NULL;
}
}

75
src/i2c.c Normal file
View File

@@ -0,0 +1,75 @@
#include "i2c.h"
#include "driver/i2c.h"
#include "esp_log.h"
#include "max17048.h"
// Helper: Initializes I2C controller at 100kHz, returns 0=success
esp_err_t i2c_init() {
// 1. Initialize I2C
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
return (i2c_param_config(I2C_MASTER_NUM, &conf) ||
i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0));
}
// Helper: Read 16-bit register to 2-byte array (MSB first big endian)
esp_err_t max17048_read_reg(uint8_t reg_addr, uint8_t *MSB, uint8_t *LSB) {
// this is better than converting to little endian for my application
// since I usually need to handle bytes individually.
uint8_t data[2];
// Write register address
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (MAX17048_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg_addr, true);
// Restart and Read 2 bytes
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (MAX17048_ADDR << 1) | I2C_MASTER_READ, true);
i2c_master_read(cmd, data, 2, I2C_MASTER_LAST_NACK);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(I2C_MASTER_TIMEOUT_MS));
i2c_cmd_link_delete(cmd);
if (ret == ESP_OK) {
*MSB = data[0];
*LSB = data[1];
}
return ret;
}
// Write big endian 2-byte array to a 16-bit register
esp_err_t max17048_write_reg(uint8_t reg_addr, uint8_t MSB, uint8_t LSB) {
// Write register address
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (MAX17048_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg_addr, true);
i2c_master_write_byte(cmd, MSB, true);
i2c_master_write_byte(cmd, LSB, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(I2C_MASTER_TIMEOUT_MS));
i2c_cmd_link_delete(cmd);
return ret;
}
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);
MSB &= MSBmask;
LSB &= LSBmask;
MSB |= origMSB & ~MSBmask;
LSB |= origLSB & ~LSBmask;
return err | max17048_write_reg(reg_addr, MSB, LSB);
}

176
src/mainEventLoop.cpp Normal file
View File

@@ -0,0 +1,176 @@
#include "mainEventLoop.hpp"
#include "calibration.hpp"
#include "setup.hpp"
#include "servo.hpp"
#include "bmHTTP.hpp"
#include "cJSON.h"
#include "encoder.hpp"
#include "WiFi.hpp"
#include "socketIO.hpp"
TaskHandle_t wakeTaskHandle = 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);
}
}
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* 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;
}
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();
}
}
}
}
}
return success;
}
bool getServoPos() {
cJSON* response = nullptr;
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);
if (firstObject != NULL) {
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);
return false;
}
printf("Verified new token!\n");
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 {
cJSON* pos = cJSON_GetObjectItem(firstObject, "last_pos");
runToAppPos(pos->valueint);
}
}
cJSON_Delete(response);
}
}
}
}
return success;
}
QueueHandle_t main_event_queue = NULL;
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();
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();
}
}
}
}
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();
}
}
}
}
}
}
vTaskDelete(NULL);
}

44
src/max17048.c Normal file
View File

@@ -0,0 +1,44 @@
#include "max17048.h"
#include "esp_err.h"
#include "i2c.h"
#include "esp_timer.h"
static const char *TAG = "BATTERY";
uint8_t established_soc;
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_alsc();
err |= bms_clear_status();
}
// 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;
}
}
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;
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);
}

282
src/servo.cpp Normal file
View File

@@ -0,0 +1,282 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "servo.hpp"
#include "driver/ledc.h"
#include "defines.h"
#include "esp_log.h"
#include "socketIO.hpp"
#include "nvs_flash.h"
#include "mainEventLoop.hpp"
std::atomic<bool> calibListen{false};
std::atomic<int32_t> baseDiff{0};
std::atomic<int32_t> target{0};
std::atomic<bool> runningManual{false};
std::atomic<bool> runningServer{false};
std::atomic<bool> startLess{false};
void servoInit() {
// LEDC timer configuration (C++ aggregate initialization)
ledc_timer_config_t ledc_timer = {};
ledc_timer.speed_mode = LEDC_LOW_SPEED_MODE;
ledc_timer.timer_num = LEDC_TIMER_0;
ledc_timer.duty_resolution = LEDC_TIMER_16_BIT;
ledc_timer.freq_hz = 50;
ledc_timer.clk_cfg = LEDC_USE_RC_FAST_CLK;
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
// LEDC channel configuration
ledc_channel_config_t ledc_channel = {};
ledc_channel.speed_mode = LEDC_LOW_SPEED_MODE;
ledc_channel.channel = servoLEDCChannel;
ledc_channel.timer_sel = LEDC_TIMER_0;
ledc_channel.intr_type = LEDC_INTR_DISABLE;
ledc_channel.gpio_num = servoPin;
ledc_channel.duty = offSpeed; // Start off
ledc_channel.hpoint = 0;
ledc_channel.sleep_mode = LEDC_SLEEP_MODE_KEEP_ALIVE;
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
gpio_sleep_sel_dis(servoPin);
// Configure servo power switch pin as output
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_set_direction(debugLED, GPIO_MODE_OUTPUT);
gpio_set_level(debugLED, 0); // Start with LED off
topEnc->count = servoReadPos();
if (Calibration::getCalibrated()) initMainLoop();
debugLEDSwitch(1);
}
void servoOn(uint8_t dir, uint8_t manOrServer) {
servoMainSwitch(1);
ledc_set_duty(LEDC_LOW_SPEED_MODE, servoLEDCChannel, (dir ? ccwSpeed : cwSpeed));
ledc_update_duty(LEDC_LOW_SPEED_MODE, servoLEDCChannel);
runningManual = !manOrServer;
runningServer = manOrServer;
}
void servoOff() {
ledc_set_duty(LEDC_LOW_SPEED_MODE, servoLEDCChannel, offSpeed);
ledc_update_duty(LEDC_LOW_SPEED_MODE, servoLEDCChannel);
runningManual = false;
runningServer = false;
servoMainSwitch(0);
}
void servoMainSwitch(uint8_t onOff) {
gpio_set_level(servoSwitch, onOff ? 1 : 0);
}
void debugLEDSwitch(uint8_t onOff) {
gpio_set_level(debugLED, onOff ? 1 : 0);
}
void debugLEDTgl() {
static bool onOff = false;
gpio_set_level(debugLED, onOff);
onOff = !onOff;
}
bool servoInitCalib() {
topEnc->pauseWatchdog();
// get ready for calibration by clearing all these listeners
bottomEnc->wandListen.store(false, std::memory_order_release);
topEnc->wandListen.store(false, std::memory_order_release);
topEnc->serverListen.store(false, std::memory_order_release);
if (!Calibration::clearCalibrated()) return false;
if (topEnc == nullptr || bottomEnc == nullptr) {
printf("ERROR: CALIBRATION STARTED BEFORE SERVO INITIALIZATION\n");
return false;
}
baseDiff = bottomEnc->getCount() - topEnc->getCount();
calibListen = true;
return true;
}
void servoCancelCalib() {
calibListen = false;
servoOff();
}
void servoCalibListen() {
int32_t effDiff = (bottomEnc->getCount() - topEnc->getCount()) - baseDiff;
if (effDiff > 1) servoOn(CCW, manual);
else if (effDiff < -1) {
servoOn(CW, manual);
}
else {
servoOff();
}
}
bool servoBeginDownwardCalib() {
calibListen = false;
servoOff();
vTaskDelay(pdMS_TO_TICKS(1000));
if (!Calibration::beginDownwardCalib(*topEnc)) return false;
baseDiff = bottomEnc->getCount() - topEnc->getCount();
calibListen = true;
return true;
}
bool servoCompleteCalib() {
calibListen = false;
servoOff();
vTaskDelay(pdMS_TO_TICKS(1000));
if (!Calibration::completeCalib(*topEnc)) return false;
initMainLoop();
return true;
}
void initMainLoop() {
topEnc->setupWatchdog();
servoSavePos();
bottomEnc->wandListen.store(true, std::memory_order_release);
}
void IRAM_ATTR watchdogCallback(void* arg) {
if (runningManual || runningServer) {
topEnc->pauseWatchdog();
// get ready for recalibration by clearing all these listeners
bottomEnc->wandListen.store(false, std::memory_order_release);
topEnc->wandListen.store(false, std::memory_order_release);
topEnc->serverListen.store(false, std::memory_order_release);
servoOff();
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
main_event_type_t evt = EVENT_CLEAR_CALIB;
BaseType_t result = xQueueSendFromISR(main_event_queue, &evt, &xHigherPriorityTaskWoken);
if (result == pdPASS) portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
else {
// if no movement is running, we're fine
// save current servo-encoder position for reinitialization
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
main_event_type_t evt = EVENT_SAVE_POS;
BaseType_t result = xQueueSendFromISR(main_event_queue, &evt, &xHigherPriorityTaskWoken);
if (result == pdPASS) portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// clear running flags
runningManual = false;
runningServer = false;
}
void servoSavePos() {
// save current servo-encoder position for use on reinitialization
nvs_handle_t servoHandle;
if (nvs_open(nvsServo, NVS_READWRITE, &servoHandle) == ESP_OK) {
int32_t topCount = topEnc->getCount();
if (nvs_set_i32(servoHandle, posTag, topCount) != ESP_OK)
printf("Error saving current position\n");
else printf("Success - Current position saved as: %d\n", topCount);
nvs_commit(servoHandle);
nvs_close(servoHandle);
}
else {
printf("Error opening servoPos NVS segment.\n");
}
}
int32_t servoReadPos() {
// save current servo-encoder position for use on reinitialization
int32_t val = 0;
nvs_handle_t servoHandle;
if (nvs_open(nvsServo, NVS_READONLY, &servoHandle) == ESP_OK) {
if (nvs_get_i32(servoHandle, posTag, &val) != ESP_OK)
printf("Error reading current position\n");
else printf("Success - Current position read as: %d\n", val);
nvs_close(servoHandle);
}
else {
printf("Error opening servoPos NVS segment.\n");
}
return val;
}
void stopServerRun() {
// stop listener and stop running if serverRun is still active.
topEnc->serverListen.store(false, std::memory_order_release);
if (runningServer) servoOff();
}
void servoWandListen() {
// stop any remote-initiated movement
stopServerRun();
// freeze atomic values
int32_t upBound = Calibration::UpTicks;
int32_t downBound = Calibration::DownTicks;
int32_t bottomCount = bottomEnc->getCount();
int32_t topCount = topEnc->getCount();
// ensure the baseDiff doesn't wait on wand to turn all the way back to original range.
if ((upBound > downBound && bottomCount - baseDiff > upBound)
|| (upBound < downBound && bottomCount - baseDiff < upBound))
baseDiff = bottomCount - upBound;
else if ((upBound > downBound && bottomCount - baseDiff < downBound)
|| (upBound < downBound && bottomCount - baseDiff > downBound))
baseDiff = bottomCount - downBound;
// calculate the difference between wand and top servo
int32_t effDiff = (bottomCount - topCount) - baseDiff;
// if we are at either bound, stop servo and servo-listener
// if effective difference is 0, stop servo and servo-listener
// otherwise, run servo in whichever direction necessary and
// ensure servo-listener is active.
if (topCount >= (MAX(upBound, downBound) - 1)
&& effDiff > 1) { // TODO: see whether these margins need to be removed.
servoOff();
topEnc->wandListen.store(false, std::memory_order_release);
}
else if (topCount <= (MIN(upBound, downBound) + 1)
&& effDiff < -1) {
servoOff();
topEnc->wandListen.store(false, std::memory_order_release);
}
else if (effDiff > 1) {
topEnc->wandListen.store(true, std::memory_order_release);
servoOn(CCW, manual);
}
else if (effDiff < -1) {
topEnc->wandListen.store(true, std::memory_order_release);
servoOn(CW, manual);
}
else {
servoOff();
topEnc->wandListen.store(false, std::memory_order_release);
}
}
void servoServerListen() {
// If we have reached or passed our goal, stop running and stop listener.
if (topEnc->getCount() >= target && startLess) stopServerRun();
else if (topEnc->getCount() <= target && !startLess) stopServerRun();
baseDiff = bottomEnc->getCount() - topEnc->getCount();
}
void runToAppPos(uint8_t appPos) {
// manual control takes precedence over remote control, always.
// also do not begin operation if not calibrated;
if (runningManual || !Calibration::getCalibrated()) return;
servoOff();
target = Calibration::convertToTicks(appPos); // calculate target encoder position
printf("runToAppPos Called, running to %d from %d", target.load(), topEnc->getCount());
// allow servo position to settle
vTaskDelay(pdMS_TO_TICKS(500));
int32_t topCount = topEnc->getCount();
if (abs(topCount - target) <= 1) return;
startLess = topCount < target;
if (runningManual) return; // check again before starting remote control
if (startLess) servoOn(CCW, server); // begin servo movement
else servoOn(CW, server);
topEnc->serverListen.store(true, std::memory_order_release); // start listening for shutoff point
}

180
src/setup.cpp Normal file
View File

@@ -0,0 +1,180 @@
#include "freertos/FreeRTOS.h"
#include "setup.hpp"
#include "BLE.hpp"
#include "WiFi.hpp"
#include "nvs_flash.h"
#include "defines.h"
#include "bmHTTP.hpp"
#include "socketIO.hpp"
#include "calibration.hpp"
TaskHandle_t setupTaskHandle = NULL;
std::atomic<bool> awaitCalibration{false};
void initialSetup() {
printf("Entered Setup\n");
initBLE();
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
setupTaskHandle = NULL;
}
void setupAndCalibrate() {
while (1) {
setupLoop();
if (awaitCalibration) {
if (calibrate()) break;
}
else break;
}
}
void setupLoop() {
setupTaskHandle = xTaskGetCurrentTaskHandle();
bool initSuccess = false;
while(!initSuccess) {
nvs_handle_t WiFiHandle;
if (nvs_open(nvsWiFi, NVS_READONLY, &WiFiHandle) == ESP_OK) {
size_t ssidSize;
esp_err_t WiFiPrefsError = nvs_get_str(WiFiHandle, ssidTag, NULL, &ssidSize);
size_t pwSize;
WiFiPrefsError |= nvs_get_str(WiFiHandle, passTag, NULL, &pwSize);
uint8_t authMode;
WiFiPrefsError |= nvs_get_u8(WiFiHandle, authTag, &authMode);
if (WiFiPrefsError == ESP_ERR_NVS_NOT_FOUND) {
printf("Didn't find creds\n");
// Make the RGB LED a certain color (Blue?)
nvs_close(WiFiHandle);
initialSetup();
continue;
}
else if (WiFiPrefsError == ESP_OK) {
char ssid[ssidSize];
nvs_get_str(WiFiHandle, ssidTag, ssid, &ssidSize);
char pw[pwSize];
nvs_get_str(WiFiHandle, passTag, pw, &pwSize);
nvs_close(WiFiHandle);
if (!WiFi::attemptConnect(ssid, pw, (wifi_auth_mode_t)authMode)) {
// Make RGB LED certain color (Blue?)
printf("Found credentials, failed to connect.\n");
initialSetup();
continue;
}
else {
printf("Connected to WiFi from NVS credentials\n");
nvs_handle_t authHandle;
if (nvs_open(nvsAuth, NVS_READONLY, &authHandle) == ESP_OK) {
size_t tokenSize;
if (nvs_get_str(authHandle, tokenTag, NULL, &tokenSize) == ESP_OK) {
char token[tokenSize];
nvs_get_str(authHandle, tokenTag, token, &tokenSize);
nvs_close(authHandle);
// Use permanent device token to connect to Socket.IO
// The server will verify the token during connection handshake
webToken = std::string(token);
cJSON* response = nullptr;
initSuccess = httpGET("position", webToken, response);
if (!initSuccess) {
initialSetup();
continue;
}
initSuccess = false;
if (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);
initialSetup();
continue;
}
// Condition 2: Check peripheral_number in first object
else if (arraySize > 0) {
cJSON *firstObject = cJSON_GetArrayItem(response, 0);
if (firstObject != NULL) {
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);
initialSetup();
continue;
}
// Valid single peripheral with number 1, continue with normal flow
initSuccess = true;
printf("Verified new token!\n");
cJSON *awaitCalib = cJSON_GetObjectItem(firstObject, "await_calib");
if (cJSON_IsBool(awaitCalib)) awaitCalibration = awaitCalib->valueint;
cJSON_Delete(response);
if (!awaitCalibration) {
// Create calibration status object
cJSON* calibPostObj = cJSON_CreateObject();
cJSON_AddNumberToObject(calibPostObj, "port", 1);
cJSON_AddBoolToObject(calibPostObj, "calibrated", Calibration::getCalibrated());
// Send calibration status to server
cJSON* calibResponse = nullptr;
bool calibSuccess = httpPOST("report_calib_status", webToken, calibPostObj, calibResponse);
if (calibSuccess && calibResponse != NULL) {
printf("Calibration status reported successfully\n");
cJSON_Delete(calibResponse);
} else {
printf("Failed to report calibration status\n");
}
cJSON_Delete(calibPostObj);
if (!Calibration::getCalibrated()) awaitCalibration = true;
}
}
else {
printf("null object\n");
cJSON_Delete(response);
initialSetup();
continue;
}
}
else {
printf("no items in array\n");
cJSON_Delete(response);
initialSetup();
continue;
}
}
else {
printf("Response not array\n");
cJSON_Delete(response);
initialSetup();
continue;
}
}
else printf("Failed to parse JSON response\n");
}
else {
printf("Token read unsuccessful, entering setup.\n");
initialSetup();
}
}
else {
printf("Auth NVS segment doesn't exist, entering setup.\n");
initialSetup();
}
}
} else {
// Make RGB LED certain color (Blue?)
nvs_close(WiFiHandle);
printf("Program error in Wifi Connection\n");
initialSetup();
}
}
else {
printf("WiFi NVS segment doesn't exist, entering setup.\n");
initialSetup();
}
}
setupTaskHandle = NULL; // Clear handle on function exit (safety)
}

381
src/socketIO.cpp Normal file
View File

@@ -0,0 +1,381 @@
#include "socketIO.hpp"
#include "esp_socketio_client.h"
#include "bmHTTP.hpp" // To access webToken
#include "WiFi.hpp"
#include "setup.hpp"
#include "cJSON.h"
#include "calibration.hpp"
#include "servo.hpp"
#include "defines.h"
#include "esp_crt_bundle.h"
static esp_socketio_client_handle_t io_client;
static esp_socketio_packet_handle_t tx_packet = NULL;
static bool stopSocketFlag = false;
std::atomic<bool> socketIOactive{false};
// Event handler for Socket.IO events
static void socketio_event_handler(void *handler_args, esp_event_base_t base,
int32_t event_id, void *event_data) {
esp_socketio_event_data_t *data = (esp_socketio_event_data_t *)event_data;
esp_socketio_packet_handle_t packet = data->socketio_packet;
switch (event_id) {
case SOCKETIO_EVENT_OPENED:
printf("Socket.IO Received OPEN packet\n");
// Connect to default namespace "/"
esp_socketio_client_connect_nsp(data->client, NULL, NULL);
break;
case SOCKETIO_EVENT_NS_CONNECTED: {
printf("Socket.IO Connected to namespace!\n");
// Check if connected to default namespace
char *nsp = esp_socketio_packet_get_nsp(packet);
if (strcmp(nsp, "/") == 0) {
printf("Connected to default namespace - waiting for device_init...\n");
}
// Don't set connected yet - wait for device_init message from server
break;
}
case SOCKETIO_EVENT_DATA: {
printf("Received Socket.IO data\n");
// Parse the received packet
cJSON *json = esp_socketio_packet_get_json(packet);
if (json) {
char *json_str = cJSON_Print(json);
printf("Data: %s\n", json_str);
// Check if this is an array event
if (cJSON_IsArray(json) && cJSON_GetArraySize(json) >= 2) {
cJSON *eventName = cJSON_GetArrayItem(json, 0);
if (cJSON_IsString(eventName)) {
// Handle error event
if (strcmp(eventName->valuestring, "error") == 0) {
printf("Received error message from server\n");
cJSON *data = cJSON_GetArrayItem(json, 1);
if (data) {
cJSON *message = cJSON_GetObjectItem(data, "message");
if (message && cJSON_IsString(message)) {
printf("Server error: %s\n", message->valuestring);
}
}
// Mark connection as failed
if (calibTaskHandle != NULL)
xTaskNotify(calibTaskHandle, false, eSetValueWithOverwrite);
}
// Handle device_init event
else if (strcmp(eventName->valuestring, "device_init") == 0) {
printf("Received device_init message\n");
cJSON *data = cJSON_GetArrayItem(json, 1);
if (data) {
cJSON *type = cJSON_GetObjectItem(data, "type");
if (type && strcmp(type->valuestring, "success") == 0) {
printf("Device authenticated successfully\n");
// Parse device state
cJSON *deviceState = cJSON_GetObjectItem(data, "deviceState");
if (cJSON_IsArray(deviceState)) {
int stateCount = cJSON_GetArraySize(deviceState);
printf("Device has %d peripheral(s):\n", stateCount);
for (int i = 0; i < stateCount; i++) {
cJSON *periph = cJSON_GetArrayItem(deviceState, i);
int port = cJSON_GetObjectItem(periph, "port")->valueint;
int lastPos = cJSON_GetObjectItem(periph, "lastPos")->valueint;
// TODO: UPDATE MOTOR/ENCODER STATES BASED ON THIS, as well as the successive websocket updates.
printf(" Port %d: pos=%d\n", port, lastPos);
if (port != 1) printf("ERROR: NON-1 PORT RECEIVED\n");
}
}
// Now mark as connected
if (calibTaskHandle != NULL)
xTaskNotify(calibTaskHandle, true, eSetValueWithOverwrite);
} else {
printf("Device authentication failed\n");
Calibration::clearCalibrated();
deleteWiFiAndTokenDetails();
if (calibTaskHandle != NULL)
xTaskNotify(calibTaskHandle, false, eSetValueWithOverwrite);
}
}
}
// Handle device_deleted event
else if (strcmp(eventName->valuestring, "device_deleted") == 0) {
printf("Device has been deleted from account - disconnecting\n");
cJSON *data = cJSON_GetArrayItem(json, 1);
if (data) {
cJSON *message = cJSON_GetObjectItem(data, "message");
if (message && cJSON_IsString(message)) {
printf("Server message: %s\n", message->valuestring);
}
}
Calibration::clearCalibrated();
deleteWiFiAndTokenDetails();
if (calibTaskHandle != NULL)
xTaskNotify(calibTaskHandle, false, eSetValueWithOverwrite);
}
// Handle calib_start event
else if (strcmp(eventName->valuestring, "calib_start") == 0) {
printf("Device calibration begun, setting up...\n");
cJSON *data = cJSON_GetArrayItem(json, 1);
if (data) {
cJSON *port = cJSON_GetObjectItem(data, "port");
if (port && cJSON_IsNumber(port)) {
if (port->valueint != 1) {
printf("Error, non-1 port received for calibration\n");
emitCalibError("Non-1 Port");
}
else {
printf("Running initCalib...\n");
if (!servoInitCalib()) {
printf("initCalib returned False\n");
emitCalibError("Initialization failed");
}
else {
printf("Ready to calibrate\n");
emitCalibStage1Ready();
}
}
}
}
}
// Handle user_stage1_complete event
else if (strcmp(eventName->valuestring, "user_stage1_complete") == 0) {
printf("User completed stage 1 (tilt up), switching direction...\n");
cJSON *data = cJSON_GetArrayItem(json, 1);
if (data) {
cJSON *port = cJSON_GetObjectItem(data, "port");
if (port && cJSON_IsNumber(port)) {
if (port->valueint != 1) {
printf("Error, non-1 port received for calibration\n");
emitCalibError("Non-1 Port");
}
else {
if (!servoBeginDownwardCalib())
emitCalibError("Direction Switch Failed");
else emitCalibStage2Ready();
}
}
}
}
// Handle user_stage2_complete event
else if (strcmp(eventName->valuestring, "user_stage2_complete") == 0) {
printf("User completed stage 2 (tilt down), finalizing calibration...\n");
cJSON *data = cJSON_GetArrayItem(json, 1);
if (data) {
cJSON *port = cJSON_GetObjectItem(data, "port");
if (port && cJSON_IsNumber(port)) {
if (port->valueint != 1) {
printf("Error, non-1 port received for calibration\n");
emitCalibError("Non-1 port");
}
else {
if (!servoCompleteCalib()) emitCalibError("Completion failed");
else {
emitCalibDone();
}
}
}
}
}
// Handle calib_done_ack event
else if (strcmp(eventName->valuestring, "calib_done_ack") == 0) {
printf("Server acknowledged calibration completion - safe to disconnect\n");
stopSocketFlag = true;
}
// Handle cancel_calib event
else if (strcmp(eventName->valuestring, "cancel_calib") == 0) {
printf("Canceling calibration process...\n");
cJSON *data = cJSON_GetArrayItem(json, 1);
if (data) {
cJSON *port = cJSON_GetObjectItem(data, "port");
if (port && cJSON_IsNumber(port)) {
if (port->valueint != 1) {
printf("Error, non-1 port received for calibration\n");
emitCalibError("Non-1 Port");
}
else {
servoCancelCalib();
}
}
}
}
}
}
free(json_str);
}
break;
}
case SOCKETIO_EVENT_ERROR: {
printf("Socket.IO Error!\n");
servoCancelCalib();
esp_websocket_event_data_t *ws_event = data->websocket_event;
if (ws_event) {
// 1. Check for TLS/SSL specific errors (Certificate issues)
if (ws_event->error_handle.error_type == WEBSOCKET_ERROR_TYPE_TCP_TRANSPORT) {
// This prints the "MbedTLS" error code (The low-level crypto library)
// Common codes: -0x2700 (CRT verify failed), -0x7200 (SSL handshake failed)
if (ws_event->error_handle.esp_tls_stack_err != 0) {
printf("TLS/SSL Stack Error: -0x%x\n", -ws_event->error_handle.esp_tls_stack_err);
}
// 2. Check the Certificate Verification Flags
// If this is non-zero, the certificate was rejected.
if (ws_event->error_handle.esp_tls_cert_verify_flags != 0) {
uint32_t flags = ws_event->error_handle.esp_tls_cert_verify_flags;
printf("Certificate Verification FAILED. Flags: 0x%lx\n", flags);
// Simple decoder for common flags:
if (flags & (1 << 0)) printf(" - CRT_NOT_TRUSTED (Root CA not found in bundle)\n");
if (flags & (1 << 1)) printf(" - CRT_BAD_KEY_USAGE\n");
if (flags & (1 << 2)) printf(" - CRT_EXPIRED (Check your ESP32 system time!)\n");
if (flags & (1 << 3)) printf(" - CRT_CN_MISMATCH (Domain name doesn't match cert)\n");
}
}
// 3. Check for HTTP Handshake errors (401/403/404)
// This happens if SSL worked, but the server rejected your path or token
else if (ws_event->error_handle.error_type == WEBSOCKET_ERROR_TYPE_HANDSHAKE) {
int status = ws_event->error_handle.esp_ws_handshake_status_code;
printf("HTTP Handshake Error: %d\n", status);
if (status == 401 || status == 403) {
printf("Authentication failed - invalid token\n");
} else if (status == 404) {
printf("404 Not Found - Check your URI/Path\n");
}
}
}
if (calibTaskHandle != NULL)
xTaskNotify(calibTaskHandle, false, eSetValueWithOverwrite);
break;
}
}
// Handle WebSocket-level disconnections
if (data->websocket_event_id == WEBSOCKET_EVENT_DISCONNECTED) {
printf("WebSocket disconnected\n");
if (calibTaskHandle != NULL)
xTaskNotify(calibTaskHandle, false, eSetValueWithOverwrite);
}
if (stopSocketFlag) {
if (calibTaskHandle != NULL)
xTaskNotify(calibTaskHandle, 2, eSetValueWithOverwrite);
stopSocketFlag = false; // Clear flag after notifying once
}
}
const std::string uriString = std::string("ws") + (secureSrv ? "s" : "") + "://" + srvAddr + "/socket.io/?EIO=4&transport=websocket";
void initSocketIO() {
stopSocketFlag = false; // Reset flag for new connection
// Prepare the Authorization Header (Bearer format)
std::string authHeader = "Authorization: Bearer " + webToken + "\r\n";
esp_socketio_client_config_t config = {};
config.websocket_config.uri = uriString.c_str();
config.websocket_config.headers = authHeader.c_str();
if (secureSrv) {
config.websocket_config.transport = WEBSOCKET_TRANSPORT_OVER_SSL;
config.websocket_config.crt_bundle_attach = esp_crt_bundle_attach;
}
io_client = esp_socketio_client_init(&config);
tx_packet = esp_socketio_client_get_tx_packet(io_client);
esp_socketio_register_events(io_client, SOCKETIO_EVENT_ANY, socketio_event_handler, NULL);
esp_socketio_client_start(io_client);
socketIOactive = true;
}
void stopSocketIO() {
if (io_client != NULL) {
printf("Stopping Socket.IO client...\n");
esp_socketio_client_close(io_client, pdMS_TO_TICKS(1000));
esp_socketio_client_destroy(io_client);
io_client = NULL;
tx_packet = NULL;
}
socketIOactive = false;
}
// Helper function to emit Socket.IO event with data
static void emitSocketEvent(const char* eventName, cJSON* data) {
if (esp_socketio_packet_set_header(tx_packet, EIO_PACKET_TYPE_MESSAGE,
SIO_PACKET_TYPE_EVENT, NULL, -1) == ESP_OK) {
cJSON *array = cJSON_CreateArray();
cJSON_AddItemToArray(array, cJSON_CreateString(eventName));
cJSON_AddItemToArray(array, data);
esp_socketio_packet_set_json(tx_packet, array);
esp_socketio_client_send_data(io_client, tx_packet);
esp_socketio_packet_reset(tx_packet);
cJSON_Delete(array);
} else {
// If packet header setup failed, clean up the data object
cJSON_Delete(data);
}
}
// Function to emit 'calib_done' as expected by your server
void emitCalibDone(int port) {
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "port", port);
emitSocketEvent("calib_done", data);
}
// Function to emit 'calib_stage1_ready' to notify server device is ready for tilt up
void emitCalibStage1Ready(int port) {
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "port", port);
emitSocketEvent("calib_stage1_ready", data);
}
// Function to emit 'calib_stage2_ready' to notify server device is ready for tilt down
void emitCalibStage2Ready(int port) {
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "port", port);
emitSocketEvent("calib_stage2_ready", data);
}
// Function to emit 'report_calib_status' to tell server device's actual calibration state
void emitCalibStatus(bool calibrated, int port) {
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "port", port);
cJSON_AddBoolToObject(data, "calibrated", calibrated);
emitSocketEvent("report_calib_status", data);
}
// Function to emit 'device_calib_error' to notify server of calibration failure
void emitCalibError(const char* errorMessage, int port) {
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "port", port);
cJSON_AddStringToObject(data, "message", errorMessage);
emitSocketEvent("device_calib_error", data);
}
// Function to emit 'pos_hit' to notify server of position change
void emitPosHit(int pos, int port) {
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "port", port);
cJSON_AddNumberToObject(data, "pos", pos);
emitSocketEvent("pos_hit", data);
}