2025-11-28 18:26:32 -06:00
|
|
|
#include "BLE.hpp"
|
|
|
|
|
#include "NimBLEDevice.h"
|
|
|
|
|
#include "WiFi.hpp"
|
2025-12-18 15:01:25 -06:00
|
|
|
#include "nvs_flash.h"
|
|
|
|
|
#include "defines.h"
|
2025-12-18 22:08:07 -06:00
|
|
|
#include <mutex>
|
|
|
|
|
#include "bmHTTP.hpp"
|
2025-11-28 18:26:32 -06:00
|
|
|
|
2025-12-17 21:52:00 -06:00
|
|
|
std::atomic<bool> flag_scan_requested{false};
|
|
|
|
|
std::atomic<bool> credsGiven{false};
|
2025-12-18 22:08:07 -06:00
|
|
|
std::atomic<bool> tokenGiven{false};
|
|
|
|
|
std::atomic<bool> isBLEClientConnected{false};
|
|
|
|
|
std::atomic<bool> scanBlock{false};
|
2025-12-17 21:52:00 -06:00
|
|
|
std::mutex dataMutex;
|
2025-11-28 18:26:32 -06:00
|
|
|
|
2025-12-17 21:52:00 -06:00
|
|
|
wifi_auth_mode_t auth;
|
|
|
|
|
static std::string SSID = "";
|
|
|
|
|
static std::string TOKEN = "";
|
|
|
|
|
static std::string PASS = "";
|
|
|
|
|
static std::string UNAME = "";
|
2025-11-28 18:26:32 -06:00
|
|
|
|
|
|
|
|
// Global pointers to characteristics for notification support
|
2025-12-18 22:28:55 -06:00
|
|
|
std::atomic<NimBLECharacteristic*> ssidListChar = nullptr;
|
|
|
|
|
std::atomic<NimBLECharacteristic*> connectConfirmChar = nullptr;
|
|
|
|
|
std::atomic<NimBLECharacteristic*> authConfirmChar = nullptr;
|
|
|
|
|
std::atomic<NimBLECharacteristic*> credsChar = nullptr;
|
|
|
|
|
std::atomic<NimBLECharacteristic*> tokenChar = nullptr;
|
|
|
|
|
std::atomic<NimBLECharacteristic*> ssidRefreshChar = nullptr;
|
2025-11-28 18:26:32 -06:00
|
|
|
|
|
|
|
|
NimBLEAdvertising* initBLE() {
|
|
|
|
|
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 + NOTIFY)
|
|
|
|
|
ssidListChar = pService->createCharacteristic(
|
|
|
|
|
"0000",
|
2025-12-18 22:08:07 -06:00
|
|
|
NIMBLE_PROPERTY::READ
|
2025-11-28 18:26:32 -06:00
|
|
|
);
|
2025-12-18 22:28:55 -06:00
|
|
|
ssidListChar.load()->createDescriptor("2902"); // Add BLE2902 descriptor for notifications
|
2025-11-28 18:26:32 -06:00
|
|
|
|
2025-12-18 22:08:07 -06:00
|
|
|
// 0x0001 - Credentials JSON (WRITE) - Replaces separate SSID/Password/Uname
|
|
|
|
|
// Expected JSON format: {"ssid":"network","password":"pass"}
|
|
|
|
|
credsChar = pService->createCharacteristic(
|
2025-11-28 18:26:32 -06:00
|
|
|
"0001",
|
|
|
|
|
NIMBLE_PROPERTY::WRITE
|
|
|
|
|
);
|
2025-12-18 22:28:55 -06:00
|
|
|
credsChar.load()->setCallbacks(charCallbacks);
|
2025-12-18 22:08:07 -06:00
|
|
|
|
|
|
|
|
// 0x0002 - Token (WRITE)
|
|
|
|
|
tokenChar = pService->createCharacteristic(
|
|
|
|
|
"0002",
|
|
|
|
|
NIMBLE_PROPERTY::WRITE
|
|
|
|
|
);
|
2025-12-18 22:28:55 -06:00
|
|
|
tokenChar.load()->setCallbacks(charCallbacks);
|
2025-12-18 22:08:07 -06:00
|
|
|
|
|
|
|
|
// 0x0003 - Auth Confirmation (READ + NOTIFY)
|
|
|
|
|
authConfirmChar = pService->createCharacteristic(
|
|
|
|
|
"0003",
|
|
|
|
|
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
|
|
|
|
|
);
|
2025-12-18 22:28:55 -06:00
|
|
|
authConfirmChar.load()->createDescriptor("2902"); // Add BLE2902 descriptor for notifications
|
2025-11-28 18:26:32 -06:00
|
|
|
|
|
|
|
|
// 0x0004 - SSID Refresh (WRITE)
|
2025-12-18 22:08:07 -06:00
|
|
|
ssidRefreshChar = pService->createCharacteristic(
|
2025-11-28 18:26:32 -06:00
|
|
|
"0004",
|
2025-12-18 22:08:07 -06:00
|
|
|
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
|
2025-11-28 18:26:32 -06:00
|
|
|
);
|
2025-12-18 22:28:55 -06:00
|
|
|
ssidRefreshChar.load()->setCallbacks(charCallbacks);
|
2025-11-28 18:26:32 -06:00
|
|
|
|
|
|
|
|
// 0x0005 - Connect Confirmation (READ + NOTIFY)
|
|
|
|
|
connectConfirmChar = pService->createCharacteristic(
|
|
|
|
|
"0005",
|
|
|
|
|
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
|
|
|
|
|
);
|
2025-12-18 22:28:55 -06:00
|
|
|
connectConfirmChar.load()->createDescriptor("2902"); // Add BLE2902 descriptor for notifications
|
2025-11-28 18:26:32 -06:00
|
|
|
|
|
|
|
|
// 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");
|
|
|
|
|
flag_scan_requested = true;
|
|
|
|
|
|
|
|
|
|
return pAdvertising;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 22:08:07 -06:00
|
|
|
void notifyConnectionStatus(bool success) {
|
2025-12-18 22:28:55 -06:00
|
|
|
NimBLECharacteristic* tmpConfChar = connectConfirmChar.load();
|
|
|
|
|
tmpConfChar->setValue(success ? "Connected" : "Error");
|
|
|
|
|
tmpConfChar->notify();
|
|
|
|
|
tmpConfChar->setValue(""); // Clear value after notify
|
2025-12-18 22:08:07 -06:00
|
|
|
}
|
|
|
|
|
void notifyAuthStatus(bool success) {
|
2025-12-18 22:28:55 -06:00
|
|
|
NimBLECharacteristic* tmpConfChar = authConfirmChar.load();
|
|
|
|
|
tmpConfChar->setValue(success ? "Authenticated" : "Error");
|
|
|
|
|
tmpConfChar->notify();
|
|
|
|
|
tmpConfChar->setValue(""); // Clear value after notify
|
2025-12-18 22:08:07 -06:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 21:52:00 -06:00
|
|
|
bool BLEtick(NimBLEAdvertising* pAdvertising) {
|
2025-11-28 18:26:32 -06:00
|
|
|
if(flag_scan_requested) {
|
|
|
|
|
flag_scan_requested = false;
|
2025-12-18 22:08:07 -06:00
|
|
|
if (!scanBlock) {
|
|
|
|
|
scanBlock = true;
|
|
|
|
|
printf("Scanning WiFi...\n");
|
2025-12-18 22:19:54 -06:00
|
|
|
bmWiFi.scanAndUpdateSSIDList();
|
2025-12-18 22:08:07 -06:00
|
|
|
}
|
|
|
|
|
else printf("Duplicate scan request\n");
|
2025-11-28 18:26:32 -06:00
|
|
|
}
|
2025-12-17 21:52:00 -06:00
|
|
|
else if (credsGiven) {
|
|
|
|
|
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;
|
2025-12-18 22:08:07 -06:00
|
|
|
credsGiven = false;
|
2025-12-17 21:52:00 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool wifiConnect;
|
|
|
|
|
if (tmpAUTH == WIFI_AUTH_WPA2_ENTERPRISE || tmpAUTH == WIFI_AUTH_WPA3_ENTERPRISE)
|
|
|
|
|
wifiConnect = bmWiFi.attemptConnect(tmpSSID.c_str(), tmpUNAME.c_str(), tmpPASS.c_str(), tmpAUTH);
|
|
|
|
|
else wifiConnect = bmWiFi.attemptConnect(tmpSSID.c_str(), tmpPASS.c_str(), tmpAUTH);
|
2025-12-18 22:08:07 -06:00
|
|
|
if (!wifiConnect) {
|
|
|
|
|
// notify errored
|
|
|
|
|
notifyConnectionStatus(false);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-17 21:52:00 -06:00
|
|
|
|
2025-12-18 15:01:25 -06:00
|
|
|
nvs_handle_t WiFiHandle;
|
|
|
|
|
esp_err_t err = nvs_open(nvsWiFi, NVS_READWRITE, &WiFiHandle);
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
printf("ERROR Saving Credentials\n");
|
2025-12-18 22:08:07 -06:00
|
|
|
// notify errored
|
|
|
|
|
notifyConnectionStatus(false);
|
2025-12-18 15:01:25 -06:00
|
|
|
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);
|
|
|
|
|
}
|
2025-12-18 22:08:07 -06:00
|
|
|
if (err == ESP_OK) {
|
|
|
|
|
// notify connected
|
|
|
|
|
notifyConnectionStatus(true);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
// notify connected
|
|
|
|
|
notifyConnectionStatus(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (tokenGiven) {
|
|
|
|
|
tokenGiven = false;
|
|
|
|
|
if (!bmWiFi.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;
|
|
|
|
|
}
|
2025-12-18 15:01:25 -06:00
|
|
|
|
2025-12-18 22:08:07 -06:00
|
|
|
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");
|
|
|
|
|
|
|
|
|
|
notifyAuthStatus(success);
|
|
|
|
|
return success;
|
2025-12-17 21:52:00 -06:00
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 22:08:07 -06:00
|
|
|
void reset() {
|
|
|
|
|
esp_wifi_scan_stop();
|
|
|
|
|
scanBlock = false;
|
|
|
|
|
flag_scan_requested = false;
|
|
|
|
|
credsGiven = false;
|
|
|
|
|
tokenGiven = false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 22:19:54 -06:00
|
|
|
void MyServerCallbacks::onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) {
|
2025-12-18 22:08:07 -06:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 22:19:54 -06:00
|
|
|
void MyCharCallbacks::onRead(NimBLECharacteristic* pChar, NimBLEConnInfo& connInfo) {
|
|
|
|
|
printf("Characteristic Read\n");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 21:52:00 -06:00
|
|
|
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());
|
|
|
|
|
|
|
|
|
|
// Check which characteristic was written to
|
2025-12-18 22:08:07 -06:00
|
|
|
if (pChar == credsChar) {
|
2025-12-17 21:52:00 -06:00
|
|
|
// 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");
|
|
|
|
|
credsGiven = false;
|
|
|
|
|
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;
|
2025-12-18 22:08:07 -06:00
|
|
|
bool tempCredsGiven = ssidPresent && (passPresent || open) &&
|
2025-12-17 21:52:00 -06:00
|
|
|
(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 : "";
|
|
|
|
|
credsGiven = tempCredsGiven; // update the global flag.
|
|
|
|
|
}
|
|
|
|
|
else printf("ERROR: Did not receive necessary credentials.\n");
|
|
|
|
|
cJSON_Delete(root);
|
|
|
|
|
} else {
|
|
|
|
|
printf("Failed to parse JSON\n");
|
|
|
|
|
credsGiven = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 22:08:07 -06:00
|
|
|
else if (pChar == tokenChar) {
|
|
|
|
|
if (val.length() > 0) {
|
|
|
|
|
printf("Received Token: %s\n", val.c_str());
|
|
|
|
|
std::lock_guard<std::mutex> lock(dataMutex);
|
|
|
|
|
TOKEN = val;
|
|
|
|
|
tokenGiven = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (pChar == ssidRefreshChar) {
|
|
|
|
|
if (val == "Start") {
|
|
|
|
|
// Refresh characteristic
|
|
|
|
|
printf("Refresh Requested\n");
|
|
|
|
|
flag_scan_requested = true;
|
|
|
|
|
}
|
|
|
|
|
else if (val == "Done") {
|
|
|
|
|
printf("Data read complete\n");
|
|
|
|
|
scanBlock = false;
|
|
|
|
|
}
|
2025-12-17 21:52:00 -06:00
|
|
|
}
|
|
|
|
|
else printf("Unknown UUID: %s\n", uuidStr.c_str());
|
|
|
|
|
}
|