AIvoices/firmware-arduino/src/WifiManager.cpp

1248 lines
No EOL
40 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Wifi Manager
* (c) 2022-2024 Martin Verges
*
* Licensed under CC BY-NC-SA 4.0
* (Attribution-NonCommercial-ShareAlike 4.0 International)
**/
#include "WifiManager.h"
#include "OTA.h"
#include "AsyncJson.h"
#include "ArduinoJson.h"
#include <WiFi.h>
#include <Preferences.h>
#include <Config.h>
bool isDeviceRegistered() {
if (!authTokenGlobal.isEmpty()) {
return true;
}
HTTPClient http;
#ifdef DEV_MODE
http.begin("http://" + String(backend_server) + ":" + String(backend_port) +
"/api/generate_auth_token?macAddress=" + WiFi.macAddress());
#else
WiFiClientSecure client;
client.setCACert(Vercel_CA_cert);
http.begin(client, "https://" + String(backend_server) +
"/api/generate_auth_token?macAddress=" + WiFi.macAddress());
#endif
http.setTimeout(10000);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
JsonDocument doc;
DeserializationError error = deserializeJson(doc, payload);
if (error) {
Serial.print("JSON parsing failed: ");
Serial.println(error.c_str());
http.end();
return false;
}
String authToken = doc["token"];
if (!authToken.isEmpty()) {
// Store the auth token in NVS
preferences.begin("auth", false);
preferences.putString("auth_token", authToken);
preferences.end();
authTokenGlobal = String(authToken);
http.end();
return true;
}
}
// If we get here, either the request failed or no token was found
http.end();
return false;
}
void connectCb() {
Serial.println("On connecting to Wifi");
if (isDeviceRegistered()) {
if (otaState == OTA_IN_PROGRESS) {
performOTAUpdate();
} else if (otaState == OTA_COMPLETE) {
markOTAUpdateComplete();
ESP.restart();
} else {
websocketSetup(ws_server, ws_port, ws_path);
}
}
}
/**
* @brief Write a message to the Serial interface
* @param msg The message to be written
*
* This function is a simple wrapper around Serial.print() to write a message
* to the serial console. It can be overwritten by a custom implementation for
* enhanced logging.
*/
void WIFIMANAGER::logMessage(String msg) {
Serial.print(msg);
}
/**
* @brief Background Task running as a loop forever
* @param param needs to be a valid WIFIMANAGER instance
*/
void wifiTask(void* param) {
yield();
delay(500); // wait a short time until everything is setup before executing the loop forever
yield();
const TickType_t xDelay = 10000 / portTICK_PERIOD_MS;
WIFIMANAGER * wifimanager = (WIFIMANAGER *) param;
for(;;) {
yield();
wifimanager->loop();
yield();
vTaskDelay(xDelay);
}
}
/**
* @brief Start the background task, which will take care of the Wifi connection
*
* This method will load the configuration from NVS, try to connect to the configured WIFI(s)
* and then start a background task, which will keep monitoring and trying to reconnect
* to the configured WIFI(s) in case the connection drops.
*/
void WIFIMANAGER::startBackgroundTask(String softApName, String softApPass) {
if (softApName.length()) this->softApName = softApName;
if (softApPass.length()) this->softApPass = softApPass;
WiFi.setSleep(false);
loadFromNVS();
tryConnect();
BaseType_t taskCreated = xTaskCreatePinnedToCore(
wifiTask,
"WifiManager",
4096, // Stack size in words
this, // Task input parameter
1, // Priority of the task
&WifiCheckTask, // Task handle.
0 // Core where the task should run
);
if (taskCreated != pdPASS) {
logMessage("[ERROR] WifiManager: Error creating background task\n");
}
}
/**
* @brief Construct a new WIFIMANAGER::WIFIMANAGER object
* @details Puts the Wifi mode to AP+STA and registers Wifi Events
* @param ns Namespace for the preferences non volatile storage (NVS)
*/
WIFIMANAGER::WIFIMANAGER(const char * ns) {
NVS = (char *)ns;
// AP on/off
WiFi.onEvent([&](WiFiEvent_t event, WiFiEventInfo_t info) {
logMessage("[WIFI] onEvent() AP mode started!\n");
softApRunning = true;
deviceState = SOFT_AP;
#if ESP_ARDUINO_VERSION_MAJOR >= 2
}, ARDUINO_EVENT_WIFI_AP_START); // arduino-esp32 2.0.0 and later
#else
}, SYSTEM_EVENT_AP_START); // arduino-esp32 1.0.6
#endif
WiFi.onEvent([&](WiFiEvent_t event, WiFiEventInfo_t info) {
logMessage("[WIFI] onEvent() AP mode stopped!\n");
softApRunning = false;
deviceState = IDLE;
#if ESP_ARDUINO_VERSION_MAJOR >= 2
}, ARDUINO_EVENT_WIFI_AP_STOP); // arduino-esp32 2.0.0 and later
#else
}, SYSTEM_EVENT_AP_STOP); // arduino-esp32 1.0.6
#endif
// AP client join/leave
WiFi.onEvent([&](WiFiEvent_t event, WiFiEventInfo_t info) {
logMessage("[WIFI] onEvent() new client connected to softAP!\n");
#if ESP_ARDUINO_VERSION_MAJOR >= 2
}, ARDUINO_EVENT_WIFI_AP_STACONNECTED); // arduino-esp32 2.0.0 and later
#else
}, SYSTEM_EVENT_AP_STACONNECTED); // arduino-esp32 1.0.6
#endif
WiFi.onEvent([&](WiFiEvent_t event, WiFiEventInfo_t info) {
logMessage("[WIFI] onEvent() Client disconnected from softAP!\n");
#if ESP_ARDUINO_VERSION_MAJOR >= 2
}, ARDUINO_EVENT_WIFI_AP_STADISCONNECTED); // arduino-esp32 2.0.0 and later
#else
}, SYSTEM_EVENT_AP_STADISCONNECTED); // arduino-esp32 1.0.6
#endif
}
/**
* @brief Destroy the WIFIMANAGER::WIFIMANAGER object
* @details will stop the background task as well but not cleanup the AsyncWebserver
*/
WIFIMANAGER::~WIFIMANAGER() {
vTaskDelete(WifiCheckTask);
// FIXME: get rid of the registered Webserver AsyncCallbackWebHandlers
}
/**
* @brief If no WIFI is available, fallback to create an AP on the ESP32
* @param state boolean true (create AP) or false (don't create an AP)
*/
void WIFIMANAGER::fallbackToSoftAp(const bool state) {
createFallbackAP = state;
}
/**
* @brief Get the current configured fallback state
* @return true
* @return false
*/
bool WIFIMANAGER::getFallbackState() const {
return createFallbackAP;
}
/**
* @brief Remove all entries from the current known and configured Wifi list
* @details This only affects memory, not the storage!
* @details If you wan't to persist this, you need to call writeToNVS()
*/
void WIFIMANAGER::clearApList() {
for(uint8_t i=0; i<WIFIMANAGER_MAX_APS; i++) {
apList[i].apName = "";
apList[i].apPass = "";
}
}
/**
* @brief Load last saved configuration from the NVS into the memory
* @return true on success
* @return false on error
*/
bool WIFIMANAGER::loadFromNVS() {
configuredSSIDs = 0;
if (preferences.begin(NVS, true)) {
clearApList();
char tmpKey[10] = { 0 };
for(uint8_t i=0; i<WIFIMANAGER_MAX_APS; i++) {
sprintf(tmpKey, "apName%d", i);
if (preferences.getType(tmpKey) == PT_STR) {
String apName = preferences.getString(tmpKey, "");
if (apName.length() > 0) {
sprintf(tmpKey, "apPass%d", i);
String apPass = preferences.getString(tmpKey);
logMessage(String("[WIFI] Load SSID '") + apName + "' to " + String(i+1) + ". slot.\n");
apList[i].apName = apName;
apList[i].apPass = apPass;
configuredSSIDs++;
}
}
}
preferences.end();
return true;
}
logMessage("[WIFI] Unable to load data from NVS, giving up...\n");
return false;
}
/**
* @brief Write the current in memory configuration to the non volatile storage
* @return true on success
* @return false on error with the NVS
*/
bool WIFIMANAGER::writeToNVS() {
if (!preferences.begin(NVS, false)) {
logMessage("[WIFI] Unable to write data to NVS, giving up...");
return false;
}
preferences.clear();
char tmpKey[10];
for(uint8_t i = 0; i < WIFIMANAGER_MAX_APS; i++) {
if (apList[i].apName.isEmpty()) continue;
snprintf(tmpKey, sizeof(tmpKey), "apName%d", i);
preferences.putString(tmpKey, apList[i].apName);
snprintf(tmpKey, sizeof(tmpKey), "apPass%d", i);
preferences.putString(tmpKey, apList[i].apPass);
}
preferences.end();
return true;
}
/**
* @brief Add a new WIFI SSID to the known credentials list
* @param apName Name of the SSID to connect to
* @param apPass Password (or empty) to connect to the SSID
* @param updateNVS Write the new entry directly to NVS
* @return true on success
* @return false on failure
*/
bool WIFIMANAGER::addWifi(String apName, String apPass, bool updateNVS) {
if(apName.length() < 1 || apName.length() > 31) {
logMessage("[WIFI] No SSID given or ssid too long");
return false;
}
if(apPass.length() > 63) {
logMessage("[WIFI] Passphrase too long");
return false;
}
for(uint8_t i=0; i<WIFIMANAGER_MAX_APS; i++) {
if (apList[i].apName == "") {
logMessage(String("[WIFI] Found unused slot Nr. ") + String(i) + " to store the new SSID '" + apName + "' credentials.\n");
apList[i].apName = apName;
apList[i].apPass = apPass;
configuredSSIDs++;
if (updateNVS) return writeToNVS();
else return true;
}
}
logMessage("[WIFI] No slot available to store SSID credentials");
return false; // max entries reached
}
/**
* @brief Drop a known SSID entry ID from the known list and write change to NVS
* @param apId ID of the SSID within the array
* @return true on success
* @return false on error
*/
bool WIFIMANAGER::delWifi(uint8_t apId) {
if (apId < WIFIMANAGER_MAX_APS) {
apList[apId].apName.clear();
apList[apId].apPass.clear();
return writeToNVS();
}
return false;
}
/**
* @brief Drop a known SSID name from the known list and write change to NVS
* @param apName SSID name
* @return true on success
* @return false on error
*/
bool WIFIMANAGER::delWifi(String apName) {
int num = 0;
for(uint8_t i=0; i<WIFIMANAGER_MAX_APS; i++) {
if (apList[i].apName == apName) {
if (delWifi(i)) num++;
}
}
return num > 0;
}
/**
* @brief Provides information about the current configuration state
* @details When at least 1 SSID is configured, the return value will be true, otherwise false
* @return true if one or more SSIDs stored
* @return false if no configuration is available
*/
bool WIFIMANAGER::configAvailable() const {
return configuredSSIDs != 0;
}
/**
* @brief Provides the apList element id of the first configured slot
* @details It's used to speed up connection by getting the first available configuration
* @note only call this function when you have configuredSSIDs > 0, otherwise it will return 0 as well and fail!
* @return uint8_t apList element id
*/
uint8_t WIFIMANAGER::getApEntry() {
for(uint8_t i=0; i<WIFIMANAGER_MAX_APS; i++) {
if (apList[i].apName.length()) return i;
}
logMessage("[WIFI][ERROR] We did not find a valid entry!\n");
logMessage("[WIFI][ERROR] Make sure to not call this function if configuredSSIDs != 1.\n");
return 0;
}
/**
* @brief Background loop function running inside the task
* @details regulary check if the connection is up&running, try to reconnect or create a fallback AP
*/
void WIFIMANAGER::loop() {
if (millis() - lastWifiCheckMillis < intervalWifiCheckMillis) return;
lastWifiCheckMillis = millis();
if(WiFi.waitForConnectResult() == WL_CONNECTED) {
// Check if we are connected to a well known SSID
for(uint8_t i=0; i<WIFIMANAGER_MAX_APS; i++) {
if (WiFi.SSID() == apList[i].apName) {
logMessage(String("[WIFI][STATUS] Connected to known SSID: '") + WiFi.SSID() + "' with IP " + WiFi.localIP().toString() + "\n");
return;
}
}
// looks like we are connected to something else, strange!?
logMessage("[WIFI] We are connected to an unknown SSID ignoring. Connected to: " + WiFi.SSID() + "\n");
} else {
if (softApRunning) {
logMessage("[WIFI] Not trying to connect to a known SSID. SoftAP has " + String(WiFi.softAPgetStationNum()) + " clients connected!\n");
} else {
// let's try to connect to some WiFi in Range
if (!tryConnect()) {
if (createFallbackAP) runSoftAP();
else logMessage("[WIFI] Auto creation of SoftAP is disabled, no starting AP!\n");
}
}
}
if (softApRunning && millis() - startApTimeMillis > timeoutApMillis) {
if (WiFi.softAPgetStationNum() > 0) {
logMessage("[WIFI] SoftAP has " + String(WiFi.softAPgetStationNum()) + " clients connected!\n");
startApTimeMillis = millis(); // reset timeout as someone is connected
return;
}
logMessage("[WIFI] Running in AP mode but timeout reached. Closing AP!\n");
stopSoftAP();
delay(100);
}
}
/**
* @brief Try to connect to one of the configured SSIDs (if available).
* @details If more than 2 SSIDs configured, scan for available WIFIs and connect to the strongest
* @return true on success
* @return false on error or no configuration
*/
bool WIFIMANAGER::tryConnect() {
if (!configAvailable()) {
logMessage("[WIFI] No SSIDs configured in NVS, unable to connect\n");
if (createFallbackAP) runSoftAP();
return false;
}
if (softApRunning) {
logMessage("[WIFI] Not trying to connect. SoftAP has " + String(WiFi.softAPgetStationNum()) + " clients connected!\n");
return false;
}
int choosenAp = INT_MIN;
if (configuredSSIDs == 1) {
// only one configured SSID, skip scanning and try to connect to this specific one.
choosenAp = getApEntry();
} else {
WiFi.mode(WIFI_STA);
int8_t scanResult = WiFi.scanNetworks(false, true);
if(scanResult <= 0) {
logMessage("[WIFI] Unable to find WIFI networks in range to this device!\n");
return false;
}
logMessage(String("[WIFI] Found ") + String(scanResult) + " networks in range\n");
int choosenRssi = INT_MIN; // we want to select the strongest signal with the highest priority if we have multiple SSIDs available
for(int8_t x = 0; x < scanResult; ++x) {
String ssid;
uint8_t encryptionType;
int32_t rssi;
uint8_t* bssid;
int32_t channel;
WiFi.getNetworkInfo(x, ssid, encryptionType, rssi, bssid, channel);
for(uint8_t i=0; i<WIFIMANAGER_MAX_APS; i++) {
if (apList[i].apName.length() == 0 || apList[i].apName != ssid) continue;
if (rssi > choosenRssi) {
if(encryptionType == WIFI_AUTH_OPEN || apList[i].apPass.length() > 0) { // open wifi or we do know a password
choosenAp = i;
choosenRssi = rssi;
}
} // else lower wifi signal
}
}
WiFi.scanDelete();
}
if (choosenAp == INT_MIN) {
logMessage("[WIFI] Unable to find an SSID to connect to!\n");
return false;
} else {
logMessage(String("[WIFI] Trying to connect to SSID ") + apList[choosenAp].apName
+ " with password " + (apList[choosenAp].apPass.length() > 0 ? "'***'" : "''") + "\n"
);
WiFi.begin(apList[choosenAp].apName.c_str(), apList[choosenAp].apPass.c_str());
wl_status_t status = (wl_status_t)WiFi.waitForConnectResult(5000UL);
auto startTime = millis();
// wait for connection, fail, or timeout
while(status != WL_CONNECTED && status != WL_NO_SSID_AVAIL && status != WL_CONNECT_FAILED && (millis() - startTime) <= 10000) {
delay(10);
status = (wl_status_t)WiFi.waitForConnectResult(5000UL);
}
switch(status) {
case WL_IDLE_STATUS:
logMessage("[WIFI] Connecting failed (0): Idle\n");
break;
case WL_NO_SSID_AVAIL:
logMessage("[WIFI] Connecting failed (1): The AP can't be found\n");
break;
case WL_SCAN_COMPLETED:
logMessage("[WIFI] Connecting failed (2): Scan completed\n");
break;
case WL_CONNECTED: // 3
logMessage("[WIFI] Connection successful\n");
logMessage("[WIFI] SSID : " + WiFi.SSID() + "\n");
logMessage("[WIFI] IP : " + WiFi.localIP().toString() + "\n");
connectCb();
stopSoftAP();
return true;
break;
case WL_CONNECT_FAILED:
logMessage("[WIFI] Connecting failed (4): Unknown reason\n");
break;
case WL_CONNECTION_LOST:
logMessage("[WIFI] Connecting failed (5): Connection lost\n");
break;
case WL_DISCONNECTED:
logMessage("[WIFI] Connecting failed (6): Disconnected\n");
break;
case WL_NO_SHIELD:
logMessage("[WIFI] Connecting failed (7): No Wifi shield found\n");
break;
default:
logMessage("[WIFI] Connecting failed (" + String(status) + "): Unknown status code\n");
break;
}
}
return false;
}
void WIFIMANAGER::configueSoftAp(String apName, String apPass) {
this->softApName = apName;
this->softApPass = apPass;
}
/**
* @brief Start a SoftAP for direct client access
* @param apName name of the AP to create (default is ESP_XXXXXXXX)
* @return true on success
* @return false o error or if a SoftAP already runs
*/
bool WIFIMANAGER::runSoftAP(String apName, String apPass) {
if (apName.length()) this->softApName = apName;
if (apPass.length()) this->softApPass = apPass;
if (softApRunning) return true;
startApTimeMillis = millis();
if (this->softApName == "") this->softApName = "ESP_" + String((uint32_t)ESP.getEfuseMac());
logMessage("[WIFI] Starting configuration portal on AP SSID " + this->softApName + "\n");
WiFi.mode(WIFI_AP);
bool state = WiFi.softAP(this->softApName.c_str(), (this->softApPass.length() ? this->softApPass.c_str() : NULL));
if (state) {
IPAddress IP = WiFi.softAPIP();
logMessage("[WIFI] AP created. My IP is: " + String(IP) + "\n");
return true;
} else {
logMessage("[WIFI] Unable to create SoftAP!\n");
return false;
}
}
/**
* @brief Stop/Disconnect a current running SoftAP
*/
void WIFIMANAGER::stopSoftAP() {
WiFi.softAPdisconnect();
WiFi.mode(WIFI_STA);
}
/**
* @brief Stop/Disconnect a current wifi connection
*/
void WIFIMANAGER::stopClient() {
WiFi.disconnect();
}
/**
* @brief Stop/Disconnect all running wifi activies and optionally kill the background task as well
* @param killTask true to kill the background task to prevent reconnects
*/
void WIFIMANAGER::stopWifi(bool killTask) {
if (killTask) vTaskDelete(WifiCheckTask);
stopSoftAP();
stopClient();
}
/**
* @brief Attach the WebServer to the WifiManager to register the RESTful API
* @param srv WebServer object
*/
#if ASYNC_WEBSERVER == true
void WIFIMANAGER::attachWebServer(AsyncWebServer * srv) {
#else
void WIFIMANAGER::attachWebServer(WebServer * srv) {
#endif
webServer = srv; // store it in the class for later use
#if ASYNC_WEBSERVER == true
// not required
#else
// just for debugging
webServer->onNotFound([&]() {
String uri = WebServer::urlDecode(webServer->uri()); // required to read paths with blanks
// Dump debug data
String message;
message.reserve(100);
message = F("Error: File not found\n\nURI: ");
message += uri;
message += F("\nMethod: ");
message += (webServer->method() == HTTP_GET) ? "GET" : "POST";
message += F("\nArguments: ");
message += webServer->args();
message += '\n';
for (uint8_t i = 0; i < webServer->args(); i++) {
message += F(" NAME:");
message += webServer->argName(i);
message += F("\n VALUE:");
message += webServer->arg(i);
message += '\n';
}
message += "path=";
message += webServer->arg("path");
message += '\n';
logMessage(message);
});
#endif
#if ASYNC_WEBSERVER == true
webServer->on((apiPrefix + "/softap/start").c_str(), HTTP_POST, [&](AsyncWebServerRequest * request) {
request->send(200, "application/json", "{\"message\":\"Soft AP stopped\"}");
#else
webServer->on((apiPrefix + "/softap/start").c_str(), HTTP_POST, [&]() {
webServer->send(200, "application/json", "{\"message\":\"Soft AP stopped\"}");
#endif
yield();
delay(250);
runSoftAP();
});
#if ASYNC_WEBSERVER == true
webServer->on((apiPrefix + "/softap/stop").c_str(), HTTP_POST, [&](AsyncWebServerRequest * request) {
request->send(200, "application/json", "{\"message\":\"Soft AP stopped\"}");
#else
webServer->on((apiPrefix + "/softap/stop").c_str(), HTTP_POST, [&]() {
webServer->send(200, "application/json", "{\"message\":\"Soft AP stopped\"}");
#endif
yield();
delay(250); // It's likely that this message won't go trough, but we give it a short time
stopSoftAP();
});
#if ASYNC_WEBSERVER == true
webServer->on((apiPrefix + "/client/stop").c_str(), HTTP_POST, [&](AsyncWebServerRequest * request) {
request->send(200, "application/json", "{\"message\":\"Terminating current Wifi connection\"}");
#else
webServer->on((apiPrefix + "/client/stop").c_str(), HTTP_POST, [&]() {
webServer->send(200, "application/json", "{\"message\":\"Terminating current Wifi connection\"}");
#endif
yield();
delay(500); // It's likely that this message won't go trough, but we give it a short time
stopClient();
});
#if ASYNC_WEBSERVER == true
webServer->on((apiPrefix + "/add").c_str(), HTTP_POST, [&](AsyncWebServerRequest * request){}, NULL,
[&](AsyncWebServerRequest * request, uint8_t *data, size_t len, size_t index, size_t total) {
JsonDocument jsonBuffer;
deserializeJson(jsonBuffer, (const char*)data);
auto resp = request;
#else
webServer->on((apiPrefix + "/add").c_str(), HTTP_POST, [&]() {
if (webServer->args() != 1) {
webServer->send(400, "application/json", "{\"message\":\"Bad Request. Only accepting one json body in request!\"}");
}
JsonDocument jsonBuffer;
deserializeJson(jsonBuffer, webServer->arg(0));
auto resp = webServer;
#endif
if (!jsonBuffer["apName"].is<String>() || !jsonBuffer["apPass"].is<String>()) {
resp->send(422, "application/json", "{\"message\":\"Invalid data\"}");
return;
}
if (!addWifi(jsonBuffer["apName"].as<String>(), jsonBuffer["apPass"].as<String>())) {
resp->send(500, "application/json", "{\"message\":\"Unable to process data\"}");
} else {resp->send(200, "application/json", "{\"message\":\"New network added\"");}
});
#if ASYNC_WEBSERVER == true
webServer->on((apiPrefix + "/id").c_str(), HTTP_DELETE, [&](AsyncWebServerRequest * request){}, NULL,
[&](AsyncWebServerRequest * request, uint8_t *data, size_t len, size_t index, size_t total) {
JsonDocument jsonBuffer;
deserializeJson(jsonBuffer, (const char*)data);
auto resp = request;
#else
webServer->on((apiPrefix + "/id").c_str(), HTTP_DELETE, [&]() {
if (webServer->args() != 1) {
webServer->send(400, "application/json", "{\"message\":\"Bad Request. Only accepting one json body in request!\"}");
}
JsonDocument jsonBuffer;
deserializeJson(jsonBuffer, webServer->arg(0));
auto resp = webServer;
#endif
int idValue = jsonBuffer["id"].as<String>().toInt();
if (idValue < 0 || idValue >= WIFIMANAGER_MAX_APS) {
resp->send(422, "application/json", "{\"message\":\"Invalid data\"}");
return;
}
if (!delWifi(jsonBuffer["id"].as<uint8_t>())) {
resp->send(500, "application/json", "{\"message\":\"Unable to delete entry\"}");
} else resp->send(200, "application/json", "{\"message\":\"AP deleted\"}");
});
#if ASYNC_WEBSERVER == true
webServer->on((apiPrefix + "/apName").c_str(), HTTP_DELETE, [&](AsyncWebServerRequest * request){}, NULL,
[&](AsyncWebServerRequest * request, uint8_t *data, size_t len, size_t index, size_t total) {
JsonDocument jsonBuffer;
deserializeJson(jsonBuffer, (const char*)data);
auto resp = request;
#else
webServer->on((apiPrefix + "/apName").c_str(), HTTP_DELETE, [&]() {
if (webServer->args() != 1) {
webServer->send(400, "application/json", "{\"message\":\"Bad Request. Only accepting one json body in request!\"}");
}
JsonDocument jsonBuffer;
deserializeJson(jsonBuffer, webServer->arg(0));
auto resp = webServer;
#endif
if (!jsonBuffer["apName"].is<String>()) {
resp->send(422, "application/json", "{\"message\":\"Invalid data\"}");
return;
}
if (!delWifi(jsonBuffer["apName"].as<String>())) {
resp->send(500, "application/json", "{\"message\":\"Unable to delete entry\"}");
} else resp->send(200, "application/json", "{\"message\":\"AP deleted\"}");
});
#if ASYNC_WEBSERVER == true
webServer->on((apiPrefix + "/configlist").c_str(), HTTP_GET, [&](AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("application/json");
#else
webServer->on((apiPrefix + "/configlist").c_str(), HTTP_GET, [&]() {
String buffer;
#endif
JsonDocument jsonDoc;
auto jsonArray = jsonDoc.to<JsonArray>();
for(uint8_t i=0; i<WIFIMANAGER_MAX_APS; i++) {
if (apList[i].apName.length() > 0) {
JsonObject wifiNet = jsonArray.createNestedObject();
wifiNet["id"] = i;
wifiNet["apName"] = apList[i].apName;
wifiNet["apPass"] = apList[i].apPass.length() > 0 ? true : false;
}
}
#if ASYNC_WEBSERVER == true
serializeJson(jsonArray, *response);
response->setCode(200);
response->setContentLength(measureJson(jsonDoc));
request->send(response);
#else
// Improve me: not that efficient without the stream response
serializeJson(jsonArray, buffer);
webServer->send(200, "application/json", (buffer.equals("null") ? "{}" : buffer));
#endif
});
#if ASYNC_WEBSERVER == true
webServer->on((apiPrefix + "/scan").c_str(), HTTP_GET, [&](AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("application/json");
#else
webServer->on((apiPrefix + "/scan").c_str(), HTTP_GET, [&]() {
String buffer;
#endif
JsonDocument jsonDoc;
int scanResult;
String ssid;
uint8_t encryptionType;
int32_t rssi;
uint8_t* bssid;
int32_t channel;
scanResult = WiFi.scanComplete();
if (scanResult == WIFI_SCAN_FAILED) {
scanResult = WiFi.scanNetworks(true, true); // FIXME: scanNetworks is disconnecting clients!
jsonDoc["status"] = "scanning";
} else if (scanResult > 0) {
for (int8_t i = 0; i < scanResult; i++) {
WiFi.getNetworkInfo(i, ssid, encryptionType, rssi, bssid, channel);
JsonObject wifiNet = jsonDoc.createNestedObject();
wifiNet["ssid"] = ssid;
wifiNet["encryptionType"] = encryptionType;
wifiNet["rssi"] = rssi;
wifiNet["channel"] = channel;
yield();
}
WiFi.scanDelete();
}
#if ASYNC_WEBSERVER == true
serializeJson(jsonDoc, *response);
response->setCode(200);
response->setContentLength(measureJson(jsonDoc));
request->send(response);
#else
// Improve me: not that efficient without the stream response
serializeJson(jsonDoc, buffer);
webServer->send(200, "application/json", buffer);
#endif
});
#if ASYNC_WEBSERVER == true
webServer->on((apiPrefix + "/status").c_str(), HTTP_GET, [&](AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("application/json");
#else
webServer->on((apiPrefix + "/status").c_str(), HTTP_GET, [&]() {
String buffer;
#endif
JsonDocument jsonDoc;
jsonDoc["ssid"] = WiFi.SSID();
jsonDoc["signalStrengh"] = WiFi.RSSI();
jsonDoc["ip"] = WiFi.localIP().toString();
jsonDoc["gw"] = WiFi.gatewayIP().toString();
jsonDoc["nm"] = WiFi.subnetMask().toString();
jsonDoc["hostname"] = WiFi.getHostname();
jsonDoc["chipModel"] = ESP.getChipModel();
jsonDoc["chipRevision"] = ESP.getChipRevision();
jsonDoc["chipCores"] = ESP.getChipCores();
jsonDoc["getHeapSize"] = ESP.getHeapSize();
jsonDoc["freeHeap"] = ESP.getFreeHeap();
#if ASYNC_WEBSERVER == true
serializeJson(jsonDoc, *response);
response->setCode(200);
response->setContentLength(measureJson(jsonDoc));
request->send(response);
#else
// Improve me: not that efficient without the stream response
serializeJson(jsonDoc, buffer);
webServer->send(200, "application/json", buffer);
#endif
});
}
/**
* @brief Attach the WebServer to the WifiManager to register the RESTful API
* @param srv WebServer object
*/
void WIFIMANAGER::attachUI() {
#if ASYNC_WEBSERVER == true
webServer->on((uiPrefix).c_str(), HTTP_GET, [](AsyncWebServerRequest* request) {
#else
webServer->on((uiPrefix).c_str(), HTTP_GET, [&]() {
#endif
String html = R"html(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>YOUR ELATO 😊</title>
<style>
:root {
--primary-color: #2563eb;
--bg-color: #f8fafc;
--card-bg: #ffffff;
--text-color: #1e293b;
--border-color: #e2e8f0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 16px;
line-height: 1.5;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.card {
background: var(--card-bg);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
border: 1px solid var(--border-color);
}
h1, h2 {
margin: 0 0 16px 0;
color: var(--text-color);
}
.network-list {
list-style: none;
padding: 0;
margin: 0;
}
.network-item {
display: flex;
align-items: center;
padding: 12px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: background-color 0.2s;
}
.network-item:last-child {
border-bottom: none;
}
.network-item:hover {
background-color: var(--bg-color);
}
.network-info {
flex-grow: 1;
}
.network-info div {
float: left;
width: 70%;
}
.network-info button {
float: right;
width: 30%;
}
.ssid {
font-weight: 500;
margin-bottom: 4px;
}
.signal {
font-size: 0.875rem;
color: #64748b;
}
button {
background: var(--primary-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
transition: opacity 0.2s;
}
button:hover {
opacity: 0.9;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status {
padding: 8px;
border-radius: 4px;
margin-bottom: 16px;
display: none;
}
.status.error {
background: #fee2e2;
color: #991b1b;
display: block;
}
.status.success {
background: #dcfce7;
color: #166534;
display: block;
}
.status.info {
background: #e0f2fe;
color: #075985;
display: block;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
align-items: center;
justify-content: center;
}
.modal-content {
background: var(--card-bg);
padding: 24px;
border-radius: 8px;
width: 90%;
max-width: 400px;
}
input {
width: 100%;
padding: 8px;
margin: 8px 0 16px;
border: 1px solid var(--border-color);
border-radius: 4px;
box-sizing: border-box;
font-size: 16px;
}
.button-group {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.button-secondary {
background: var(--bg-color);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.saved-networks {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border-color);
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<h1>YOUR ELATO DEVICE 😊</h1>
<div id="status"></div>
<button onclick="scanNetworks()">Scan for Networks</button>
<button onclick="showConnectModal()">Manual Connect</button>
</div>
<div class="card">
<h2> Saved Networks</h2>
<div id="savedNetworks" class="network-list"></div>
</div>
<div class="card">
<h2>🛜 Available Networks</h2>
<div id="networkList" class="network-list"></div>
</div>
</div>
<div id="connectModal" class="modal">
<div class="modal-content">
<h2>Connect to Network</h2>
<form id="connectForm" onsubmit="connectToNetwork(event)">
<label for="apName">Network Name:</label>
<input type="text" id="apName" required>
<label for="apPass">Password:</label>
<input type="password" id="apPass" required>
<div class="button-group">
<button type="button" class="button-secondary" onclick="closeModal()">Cancel</button>
<button type="submit">Connect</button>
</div>
</form>
</div>
</div>
<script>
const API_BASE = '/api';
let networks = {};
// Load saved networks when page loads
window.addEventListener('load', () => {
loadSavedNetworks();
scanNetworks();
});
async function loadSavedNetworks() {
try {
const response = await fetch(`${API_BASE}/wifi/configlist`);
if (!response.ok) throw new Error('Failed to fetch saved networks');
const savedNetworks = await response.json();
displaySavedNetworks(savedNetworks);
} catch (error) {
showStatus('Failed to load saved networks: ' + error.message, 'error');
}
}
function displaySavedNetworks(networks) {
const networkList = document.getElementById('savedNetworks');
const networkArray = Object.values(networks);
if (networkArray.length === 0) {
networkList.innerHTML = '<div class="network-item">No saved networks</div>';
return;
}
networkList.innerHTML = networkArray.map(network => `
<div class="network-item">
<div class="network-info">
<div class="ssid">${network.apName}</div>
<button onclick="deleteNetwork('${network.id}')">🗑 Remove</button>
</div>
</div>
`).join('');
}
async function scanNetworks() {
try {
showStatus('Scanning for networks...', 'info');
const response = await fetch(`${API_BASE}/wifi/scan`);
if (!response.ok) throw new Error('Network scan failed');
// Wait 5 seconds for the scan to complete
await new Promise(resolve => setTimeout(resolve, 5000));
const scanResponse = await fetch(`${API_BASE}/wifi/scan/results`);
if (!scanResponse.ok) throw new Error('Failed to fetch scan results');
networks = await scanResponse.json();
displayNetworks(networks);
showStatus('Networks found', 'success');
} catch (error) {
showStatus(error.message, 'error');
}
}
function displayNetworks(networks) {
const networkList = document.getElementById('networkList');
const networkArray = Object.values(networks || {})
.filter(network => network && network.ssid && network.ssid.length > 0);
if (networkArray.length === 0) {
networkList.innerHTML = '<div class="network-item">No networks found</div>';
return;
}
// Sort networks by RSSI
networkArray.sort((a, b) => b.rssi - a.rssi);
networkList.innerHTML = networkArray
.map(network => `
<div class="network-item" onclick="showConnectModal('${network.ssid}')">
<div class="network-info">
<div class="ssid">${network.ssid}</div>
<div class="signal">
Signal: ${getSignalStrength(network.rssi)}
${network.encryptionType > 0 ? '🔒' : ''}
</div>
</div>
</div>
`).join('');
}
function getSignalStrength(rssi) {
if (rssi >= -50) return 'Excellent';
if (rssi >= -60) return 'Very Good';
if (rssi >= -70) return 'Good';
if (rssi >= -80) return 'Fair';
return 'Poor';
}
function showConnectModal(apName = '') {
document.getElementById('apName').value = apName;
document.getElementById('apName').readOnly = !!apName;
document.getElementById('apPass').value = '';
document.getElementById('connectModal').style.display = 'flex';
}
function closeModal() {
document.getElementById('connectModal').style.display = 'none';
}
async function deleteNetwork(deleteId) {
try {
const response = await fetch(`${API_BASE}/wifi/id`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: deleteId }),
});
if (!response.ok) throw new Error(JSON.stringify(response));
showStatus('Network deleted successfully', 'success');
await loadSavedNetworks(); // Refresh the list
} catch (error) {
showStatus('Failed to delete network: ' + error.message, 'error');
}
}
async function connectToNetwork(event) {
event.preventDefault();
const apName = document.getElementById('apName').value;
const apPass = document.getElementById('apPass').value;
try {
showStatus('Connecting to network...', 'info');
const response = await fetch(`${API_BASE}/wifi/add`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ apName, apPass }),
});
if (!response.ok) throw new Error('Connection failed');
closeModal();
showStatus('Successfully connected!', 'success');
// Refresh saved networks list
await loadSavedNetworks();
} catch (error) {
showStatus(error.message, 'error');
}
}
function showStatus(message, type) {
const statusElement = document.getElementById('status');
statusElement.innerHTML = message;
statusElement.className = `status ${type}`;
}
</script>
</body>
</html>
)html";
#if ASYNC_WEBSERVER == true
request->send(200, "text/html", html);
#else
webServer->send(200, "text/html", html);
#endif
});
}