From 426db506d47f01a453be36a3c90f572838ab663f Mon Sep 17 00:00:00 2001 From: gdampf Date: Sun, 20 Dec 2020 23:43:52 +0100 Subject: [PATCH] Updated for Arduino Json 6 --- Basecamp.cpp | 814 +++++++++++++++++++++++----------------------- Basecamp.hpp | 29 +- Configuration.cpp | 304 ++++++++--------- Configuration.hpp | 63 +--- WebServer.cpp | 403 +++++++++++------------ 5 files changed, 765 insertions(+), 848 deletions(-) diff --git a/Basecamp.cpp b/Basecamp.cpp index 7c6b0ec..4a488c3 100644 --- a/Basecamp.cpp +++ b/Basecamp.cpp @@ -1,6 +1,7 @@ /* Basecamp - ESP32 library to simplify the basics of IoT projects - Written by Merlin Schumacher (mls@ct.de) for c't magazin für computer technik (https://www.ct.de) + Written by Merlin Schumacher (mls@ct.de) for c't magazin für computer technik + (https://www.ct.de) - updated by Guido Dampf (guido@dampf.de) Licensed under GPLv3. See LICENSE for details. */ @@ -9,438 +10,453 @@ #include "debug.hpp" namespace { - const constexpr uint16_t defaultThreadStackSize = 3072; - const constexpr UBaseType_t defaultThreadPriority = 0; - // Default length for access point mode password - const constexpr unsigned defaultApSecretLength = 8; + const constexpr uint16_t defaultThreadStackSize = 3072; + const constexpr UBaseType_t defaultThreadPriority = 0; + // Default length for access point mode password + const constexpr unsigned defaultApSecretLength = 8; } -Basecamp::Basecamp(SetupModeWifiEncryption setupModeWifiEncryption, ConfigurationUI configurationUi) - : MqttGuardInterface(mqtt) - , configuration(String{"/basecamp.json"}) - , setupModeWifiEncryption_(setupModeWifiEncryption) - , configurationUi_(configurationUi) -{ -} +Basecamp::Basecamp(SetupModeWifiEncryption setupModeWifiEncryption, + ConfigurationUI configurationUi) : + configuration(String {"/basecamp.json"}), + setupModeWifiEncryption_(setupModeWifiEncryption), + configurationUi_(configurationUi) {} /** * This function generates a cleaned string from the device name set by the user. */ -String Basecamp::_cleanHostname() -{ - String clean_hostname = configuration.get(ConfigurationKey::deviceName); // Get device name from configuration - - // If hostname is not set, return default - if (clean_hostname == "") { - return "basecamp-device"; - } - - // Transform device name to lower case - clean_hostname.toLowerCase(); - - // Replace all non-alphanumeric characters in hostname to minus symbols - for (int i = 0; i <= clean_hostname.length(); i++) { - if (!isalnum(clean_hostname.charAt(i))) { - clean_hostname.setCharAt(i,'-'); - }; - }; - DEBUG_PRINTLN(clean_hostname); - - // return cleaned hostname String - return clean_hostname; -}; - -/** - * Returns true if a secret for the setup WiFi AP is set - */ -bool Basecamp::isSetupModeWifiEncrypted(){ - return (setupModeWifiEncryption_ == SetupModeWifiEncryption::secured); -} - -/** - * Returns the SSID of the setup WiFi network - */ -String Basecamp::getSetupModeWifiName(){ - return wifi.getAPName(); -} +String Basecamp::_cleanHostname() { + // Get device name from configuration + String clean_hostname = configuration.get("DeviceName"); -/** - * Returns the secret of the setup WiFi network - */ -String Basecamp::getSetupModeWifiSecret(){ - return configuration.get(ConfigurationKey::accessPointSecret); -} + // If hostname is not set, return default + if (clean_hostname == "") { + return "basecamp-device"; + } -/** - * This is the initialisation function for the Basecamp class. - */ -bool Basecamp::begin(String fixedWiFiApEncryptionPassword) -{ - // Make sure we only accept valid passwords for ap - if (fixedWiFiApEncryptionPassword.length() != 0) { - if (fixedWiFiApEncryptionPassword.length() >= wifi.getMinimumSecretLength()) { - setupModeWifiEncryption_ = SetupModeWifiEncryption::secured; - } else { - Serial.println("Error: Given fixed ap secret is too short. Refusing."); - } - } - - // Enable serial output - Serial.begin(115200); - // Display a simple lifesign - Serial.println(""); - Serial.println("Basecamp Startup"); - - // Load configuration from internal flash storage. - // If configuration.load() fails, reset the configuration - if (!configuration.load()) { - DEBUG_PRINTLN("Configuration is broken. Resetting."); - configuration.reset(); - }; - - // Get a cleaned version of the device name. - // It is used as a hostname for DHCP and ArduinoOTA. - hostname = _cleanHostname(); - DEBUG_PRINTLN(hostname); - - // Have checkResetReason() control if the device configuration - // should be reset or not. - checkResetReason(); - -#ifndef BASECAMP_NOWIFI - - // If there is no access point secret set yet, generate one and save it. - // It will survive the default config reset. - if (!configuration.isKeySet(ConfigurationKey::accessPointSecret) || - fixedWiFiApEncryptionPassword.length() >= wifi.getMinimumSecretLength()) - { - String apSecret = fixedWiFiApEncryptionPassword; - if (apSecret.length() < wifi.getMinimumSecretLength()) { - // Not set or too short. Generate a random one. - Serial.println("Generating access point secret."); - apSecret = wifi.generateRandomSecret(defaultApSecretLength); - } else { - Serial.println("Using fixed access point secret."); - } - configuration.set(ConfigurationKey::accessPointSecret, apSecret); - configuration.save(); - } - - DEBUG_PRINTF("Secret: %s\n", configuration.get(ConfigurationKey::accessPointSecret).c_str()); - - // Initialize Wifi with the stored configuration data. - wifi.begin( - configuration.get(ConfigurationKey::wifiEssid), // The (E)SSID or WiFi-Name - configuration.get(ConfigurationKey::wifiPassword), // The WiFi password - configuration.get(ConfigurationKey::wifiConfigured), // Has the WiFi been configured - hostname, // The system hostname to use for DHCP - (setupModeWifiEncryption_ == SetupModeWifiEncryption::none)?"":configuration.get(ConfigurationKey::accessPointSecret) - ); - - // Get WiFi MAC - mac = wifi.getSoftwareMacAddress(":"); -#endif -#ifndef BASECAMP_NOMQTT - // Check if MQTT has been disabled by the user - if (!configuration.get(ConfigurationKey::mqttActive).equalsIgnoreCase("false")) { - // Setting up variables for the MQTT client. This is necessary due to - // the nature of the library. It won't work properly with Arduino Strings. - const auto &mqtthost = configuration.get(ConfigurationKey::mqttHost); - const auto &mqttuser = configuration.get(ConfigurationKey::mqttUser); - const auto &mqttpass = configuration.get(ConfigurationKey::mqttPass); - // INFO: that library just copies the pointer to the hostname. As long as nobody - // modifies the config, this may work. - mqtt.setClientId(hostname.c_str()); - auto mqttport = configuration.get(ConfigurationKey::mqttPort).toInt(); - if (mqttport == 0) mqttport = 1883; - // INFO: that library just copies the pointer to the hostname. As long as nobody - // modifies the config, this may work. - // Define the hostname and port of the MQTT broker. - mqtt.setServer(mqtthost.c_str(), mqttport); - // If MQTT credentials are stored, set them. - if (mqttuser.length() != 0) { - mqtt.setCredentials(mqttuser.c_str(), mqttpass.c_str()); - }; - // Create a timer and register a "onDisconnect" callback function that manages the (re)connection of the MQTT client - // It will be called by the Asyc-MQTT-Client KeepAlive function if a connection loss is detected - // The timer is then started and will start a function to reconnect MQTT after 2 seconds - mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)&mqtt, reinterpret_cast(connectToMqtt)); - mqtt.onDisconnect(onMqttDisconnect); - // Do not connect MQTT directly but only start the timer to give the main setup() time to register all MQTT callbacks before - // Especially a "onConnect" callback should be in place to get informed about a successful MQTT connection - // setup() can optionally call mqtt.connect() by itself if MQTT is needed before timer elapses - xTimerStart(mqttReconnectTimer, 0); - }; -#endif + // Transform device name to lower case + clean_hostname.toLowerCase(); -#ifndef BASECAMP_NOOTA - // Set up Over-the-Air-Updates (OTA) if it hasn't been disabled. - if (!configuration.get(ConfigurationKey::otaActive).equalsIgnoreCase("false")) { - - // Set OTA password - String otaPass = configuration.get(ConfigurationKey::otaPass); - if (otaPass.length() != 0) { - ArduinoOTA.setPassword(otaPass.c_str()); - } - - // Set OTA hostname - ArduinoOTA.setHostname(hostname.c_str()); - - // The following code is copied verbatim from the ESP32 BasicOTA.ino example - // This is the callback for the beginning of the OTA process - ArduinoOTA - .onStart([]() { - String type; - if (ArduinoOTA.getCommand() == U_FLASH) - type = "sketch"; - else // U_SPIFFS - type = "filesystem"; - SPIFFS.end(); - - Serial.println("Start updating " + type); - }) - // When the update ends print it to serial - .onEnd([]() { - Serial.println("\nEnd"); - }) - // Show the progress of the update - .onProgress([](unsigned int progress, unsigned int total) { - Serial.printf("Progress: %u%%\r", (progress / (total / 100))); - }) - // Error handling for the update - .onError([](ota_error_t error) { - Serial.printf("Error[%u]: ", error); - if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); - else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); - else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); - else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); - else if (error == OTA_END_ERROR) Serial.println("End Failed"); - }); - - // Start the OTA service - ArduinoOTA.begin(); - - } -#endif + // Replace all non-alphanumeric characters in hostname to minus symbols + for (int i = 0; i <= clean_hostname.length(); i++) { + if (!isalnum(clean_hostname.charAt(i))) { + clean_hostname.setCharAt(i, '-'); + }; + }; + DEBUG_PRINTLN(clean_hostname); -#ifndef BASECAMP_NOWEB - if (shouldEnableConfigWebserver()) - { - // Add a webinterface element for the h1 that contains the device name. It is a child of the #wrapper-element. - web.addInterfaceElement("heading", "h1", "","#wrapper"); - web.setInterfaceElementAttribute("heading", "class", "fat-border"); - web.addInterfaceElement("logo", "img", "", "#heading"); - web.setInterfaceElementAttribute("logo", "src", "/logo.svg"); - String DeviceName = configuration.get(ConfigurationKey::deviceName); - if (DeviceName == "") { - DeviceName = "Unconfigured Basecamp Device"; - } - web.addInterfaceElement("title", "title", DeviceName,"head"); - web.addInterfaceElement("devicename", "span", DeviceName,"#heading"); - // Set the class attribute of the element to fat-border. - web.setInterfaceElementAttribute("heading", "class", "fat-border"); - // Add a paragraph with some basic information - web.addInterfaceElement("infotext1", "p", "Configure your device with the following options:","#wrapper"); - - // Add the configuration form, that will include all inputs for config data - web.addInterfaceElement("configform", "form", "","#wrapper"); - web.setInterfaceElementAttribute("configform", "action", "#"); - web.setInterfaceElementAttribute("configform", "onsubmit", "collectConfiguration()"); - - web.addInterfaceElement("DeviceName", "input", "Device name","#configform" , "DeviceName"); - - // Add an input field for the WIFI data and link it to the corresponding configuration data - web.addInterfaceElement("WifiEssid", "input", "WIFI SSID:","#configform" , "WifiEssid"); - web.addInterfaceElement("WifiPassword", "input", "WIFI Password:", "#configform", "WifiPassword"); - web.setInterfaceElementAttribute("WifiPassword", "type", "password"); - web.addInterfaceElement("WifiConfigured", "input", "", "#configform", "WifiConfigured"); - web.setInterfaceElementAttribute("WifiConfigured", "type", "hidden"); - web.setInterfaceElementAttribute("WifiConfigured", "value", "true"); - - // Add input fields for MQTT configurations if it hasn't been disabled - if (!configuration.get(ConfigurationKey::mqttActive).equalsIgnoreCase("false")) { - web.addInterfaceElement("MQTTHost", "input", "MQTT Host:","#configform" , "MQTTHost"); - web.addInterfaceElement("MQTTPort", "input", "MQTT Port:","#configform" , "MQTTPort"); - web.setInterfaceElementAttribute("MQTTPort", "type", "number"); - web.setInterfaceElementAttribute("MQTTPort", "min", "0"); - web.setInterfaceElementAttribute("MQTTPort", "max", "65535"); - web.addInterfaceElement("MQTTUser", "input", "MQTT Username:","#configform" , "MQTTUser"); - web.addInterfaceElement("MQTTPass", "input", "MQTT Password:","#configform" , "MQTTPass"); - web.setInterfaceElementAttribute("MQTTPass", "type", "password"); - } - // Add a save button that calls the JavaScript function collectConfiguration() on click - web.addInterfaceElement("saveform", "button", "Save","#configform"); - web.setInterfaceElementAttribute("saveform", "type", "submit"); - - // Show the devices MAC in the Webinterface - String infotext2 = "This device has the MAC-Address: " + mac; - web.addInterfaceElement("infotext2", "p", infotext2,"#wrapper"); - - web.addInterfaceElement("footer", "footer", "Powered by ", "body"); - web.addInterfaceElement("footerlink", "a", "Basecamp", "footer"); - web.setInterfaceElementAttribute("footerlink", "href", "https://github.com/merlinschumacher/Basecamp"); - web.setInterfaceElementAttribute("footerlink", "target", "_blank"); - #ifdef BASECAMP_USEDNS - #ifdef DNSServer_h - if (!configuration.get(ConfigurationKey::wifiConfigured).equalsIgnoreCase("true")) { - dnsServer.start(53, "*", wifi.getSoftAPIP()); - xTaskCreatePinnedToCore(&DnsHandling, "DNSTask", 4096, (void*) &dnsServer, 5, NULL,0); - } - #endif - #endif - // Start webserver and pass the configuration object to it - // Also pass a Lambda-function that restarts the device after the configuration has been saved. - web.begin(configuration, [](){ - delay(2000); - ESP.restart(); - }); - } - #endif - Serial.println(showSystemInfo()); - - // TODO: only return true if everything setup up correctly - return true; -} + // return cleaned hostname String + return clean_hostname; +}; /** - * This is the background task function for the Basecamp class. To be called from Arduino loop. + * This is the initialisation function for the Basecamp class. */ -void Basecamp::handle (void) -{ - #ifndef BASECAMP_NOOTA - // This call takes care of the ArduinoOTA function provided by Basecamp - ArduinoOTA.handle(); - #endif -} +bool Basecamp::begin(String fixedWiFiApEncryptionPassword) { + // Make sure we only accept valid passwords for ap + if (fixedWiFiApEncryptionPassword.length() != 0) { + if (fixedWiFiApEncryptionPassword.length() >= wifi.getMinimumSecretLength()) { + setupModeWifiEncryption_ = SetupModeWifiEncryption::secured; + } else { + Serial.println("Error: Given fixed ap secret is too short. Refusing."); + } + } + // Enable serial output + Serial.begin(115200); + // Display a simple lifesign + Serial.println(""); + Serial.println("Basecamp V.0.1.9 GD"); + + // Load configuration from internal flash storage. + // If configuration.load() fails, reset the configuration + if (!configuration.load()) { + DEBUG_PRINTLN("Configuration is broken. Resetting."); + configuration.reset(); + }; + + // Get a cleaned version of the device name. + // It is used as a hostname for DHCP and ArduinoOTA. + hostname = _cleanHostname(); + DEBUG_PRINTLN(hostname); + + // Have checkResetReason() control if the device configuration + // should be reset or not. + checkResetReason(); + + #ifndef BASECAMP_NOWIFI + + // If there is no access point secret set yet, generate one and save it. + // It will survive the default config reset. + if (!configuration.isKeySet(ConfigurationKey::accessPointSecret) || + fixedWiFiApEncryptionPassword.length() >= wifi.getMinimumSecretLength()) { + String apSecret = fixedWiFiApEncryptionPassword; + if (apSecret.length() < wifi.getMinimumSecretLength()) { + // Not set or too short. Generate a random one. + Serial.println("Generating access point secret."); + apSecret = wifi.generateRandomSecret(defaultApSecretLength); + } else { + Serial.println("Using fixed access point secret."); + } + configuration.set(ConfigurationKey::accessPointSecret, apSecret); + configuration.save(); + } -#ifndef BASECAMP_NOMQTT + DEBUG_PRINTF("Secret: %s\n", + configuration.get(ConfigurationKey::accessPointSecret).c_str()); + + // Initialize Wifi with the stored configuration data. + wifi.begin( + configuration.get("WifiEssid"), // The (E)SSID or WiFi-Name + configuration.get("WifiPassword"), // The WiFi password + configuration.get("WifiConfigured"), // Has the WiFi been configured + hostname, // The system hostname to use for DHCP + (setupModeWifiEncryption_ == SetupModeWifiEncryption::none) ? "" : + configuration.get(ConfigurationKey::accessPointSecret) + ); + + // Get WiFi MAC + mac = wifi.getSoftwareMacAddress(":"); + #endif + #ifndef BASECAMP_NOMQTT + // Check if MQTT has been disabled by the user + if (configuration.get("MQTTActive") != "false") { + // Setting up variables for the MQTT client. This is necessary due to + // the nature of the library. It won't work properly with Arduino Strings. + const auto & mqtthost = configuration.get("MQTTHost"); + const auto & mqttuser = configuration.get("MQTTUser"); + const auto & mqttpass = configuration.get("MQTTPass"); + // INFO: that library just copies the pointer to the hostname. As long as nobody + // modifies the config, this may work. + mqtt.setClientId(hostname.c_str()); + auto mqttport = configuration.get("MQTTPort").toInt(); + if (mqttport == 0) mqttport = 1883; + // INFO: that library just copies the pointer to the hostname. As long as nobody + // modifies the config, this may work. + // Define the hostname and port of the MQTT broker. + mqtt.setServer(mqtthost.c_str(), mqttport); + // If MQTT credentials are stored, set them. + if (mqttuser.length() != 0) { + mqtt.setCredentials(mqttuser.c_str(), mqttpass.c_str()); + }; + // Start a task that manages the (re)connection of the MQTT client + // It's pinned to the same core (0) as FreeRTOS so the Arduino code inside setup() + // and loop() will not be interrupted, as they are pinned to core 1. + xTaskCreatePinnedToCore( & MqttHandling, "MqttTask", defaultThreadStackSize, + (void * ) & mqtt, defaultThreadPriority, NULL, 0); + }; + #endif + + #ifndef BASECAMP_NOOTA + // Set up Over-the-Air-Updates (OTA) if it hasn't been disabled. + if (configuration.get("OTAActive") != "false") { + // Create struct that stores the parameters for the OTA task + struct taskParms OTAParams[1]; + // TODO: How long do these params have to be living? + // Set OTA password + OTAParams[0].parm1 = configuration.get("OTAPass").c_str(); + // Set OTA hostname + OTAParams[0].parm2 = hostname.c_str(); + + // Create a task that takes care of OTA update handling. + // It's pinned to the same core (0) as FreeRTOS so the Arduino code inside setup() + // and loop() will not be interrupted, as they are pinned to core 1. + xTaskCreatePinnedToCore( & OTAHandling, "ArduinoOTATask", defaultThreadStackSize, + (void * ) & OTAParams[0], defaultThreadPriority, NULL, 0); + } + #endif + + #ifndef BASECAMP_NOWEB + if (shouldEnableConfigWebserver()) { + // Start webserver and pass the configuration object to it + web.begin(configuration); + // Add a webinterface element for the h1 that contains the device name. + // It is a child of the #wrapper-element. + web.addInterfaceElement("heading", "h1", "", "#wrapper"); + web.setInterfaceElementAttribute("heading", "class", "fat-border"); + web.addInterfaceElement("logo", "img", "", "#heading"); + web.setInterfaceElementAttribute("logo", "src", "/logo.svg"); + String DeviceName = configuration.get("DeviceName"); + if (DeviceName == "") { + DeviceName = "Unconfigured Basecamp Device"; + } + web.addInterfaceElement("title", "title", DeviceName, "head"); + web.addInterfaceElement("devicename", "span", DeviceName, "#heading"); + // Set the class attribute of the element to fat-border. + web.setInterfaceElementAttribute("heading", "class", "fat-border"); + // Add a paragraph with some basic information + web.addInterfaceElement("infotext1", "p", + "Configure your device with the following options:", "#wrapper"); + + // Add the configuration form, that will include all inputs for config data + web.addInterfaceElement("configform", "form", "", "#wrapper"); + web.setInterfaceElementAttribute("configform", "action", "saveConfig"); + + web.addInterfaceElement("DeviceName", "input", "Device Name", "#configform", + "DeviceName"); + + // Add an input field for the WIFI data and link it to the corresponding + // configuration data + web.addInterfaceElement("WifiEssid", "input", "WIFI SSID:", "#configform", + "WifiEssid"); + web.addInterfaceElement("WifiPassword", "input", "WIFI Password:", + "#configform", "WifiPassword"); + web.setInterfaceElementAttribute("WifiPassword", "type", "password"); + web.addInterfaceElement("WifiConfigured", "input", "", "#configform", + "WifiConfigured"); + web.setInterfaceElementAttribute("WifiConfigured", "type", "hidden"); + web.setInterfaceElementAttribute("WifiConfigured", "value", "true"); + + #ifndef BASECAMP_NOMQTT + // Add input fields for MQTT configurations if it hasn't been disabled + if (configuration.get("MQTTActive") != "false") { + web.addInterfaceElement("MQTTHost", "input", "MQTT Host:", "#configform", + "MQTTHost"); + web.addInterfaceElement("MQTTPort", "input", "MQTT Port:", "#configform", + "MQTTPort"); + web.setInterfaceElementAttribute("MQTTPort", "type", "number"); + web.setInterfaceElementAttribute("MQTTPort", "min", "0"); + web.setInterfaceElementAttribute("MQTTPort", "max", "65535"); + web.addInterfaceElement("MQTTUser", "input", "MQTT Username:", "#configform", + "MQTTUser"); + web.addInterfaceElement("MQTTPass", "input", "MQTT Password:", "#configform", + "MQTTPass"); + web.setInterfaceElementAttribute("MQTTPass", "type", "password"); + } + #endif + + // Add a save button that calls the JavaScript function collectConfiguration() + // on click + web.addInterfaceElement("saveform", "input", " ", "#configform"); + web.setInterfaceElementAttribute("saveform", "type", "button"); + web.setInterfaceElementAttribute("saveform", "value", "Save"); + web.setInterfaceElementAttribute("saveform", "onclick", "collectConfiguration()"); + + // Show the devices MAC in the Webinterface + String infotext2 = "This device has the MAC-Address: " + mac; + web.addInterfaceElement("infotext2", "p", infotext2, "#wrapper"); + + web.addInterfaceElement("footer", "footer", "Powered by ", "body"); + web.addInterfaceElement("footerlink", "a", "Basecamp", "footer"); + web.setInterfaceElementAttribute("footerlink", "href", + "https://github.com/merlinschumacher/Basecamp"); + web.setInterfaceElementAttribute("footerlink", "target", "_blank"); + #ifdef BASECAMP_USEDNS + #ifdef DNSServer_h + if (configuration.get("WifiConfigured") != "True") { + dnsServer.start(53, "*", wifi.getSoftAPIP()); + xTaskCreatePinnedToCore( & DnsHandling, "DNSTask", 4096, + (void * ) & dnsServer, 5, NULL, 0); + } + #endif + #endif + } + #endif + Serial.println(showSystemInfo()); -bool Basecamp::shouldEnableConfigWebserver() const -{ - return (configurationUi_ == ConfigurationUI::always || - (configurationUi_ == ConfigurationUI::accessPoint && wifi.getOperationMode() == WifiControl::Mode::accessPoint)); + // TODO: only return true if everything setup up correctly + return true; } -// This is a task that is called if MQTT client has lost connection. After 2 seconds it automatically trys to reconnect. - -TimerHandle_t Basecamp::mqttReconnectTimer; - -void Basecamp::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) -{ - Serial.print("MQTT Disconnected. Reason: "); Serial.println((int)reason, DEC); - xTimerStart(mqttReconnectTimer, 0); +bool Basecamp::shouldEnableConfigWebserver() const { + return (configurationUi_ == ConfigurationUI::always || + (configurationUi_ == ConfigurationUI::accessPoint && + wifi.getOperationMode() == WifiControl::Mode::accessPoint)); } -void Basecamp::connectToMqtt(TimerHandle_t xTimer) -{ - AsyncMqttClient *mqtt = (AsyncMqttClient *) pvTimerGetTimerID(xTimer); +#ifndef BASECAMP_NOMQTT - if (WiFi.status() == WL_CONNECTED) { - Serial.println("Trying to connect ..."); - mqtt->connect(); // has no effect if already connected ( if (_connected) return;) +// This is a task that checks if the MQTT client is still connected or not. +// If not it automatically reconnect. +// TODO: Think about making void* the real corresponding type +void Basecamp::MqttHandling(void * mqttPointer) { + // is set to true, when a connection attempt is already running. + // Parallel connection attempts + // seem to mess up the async-mqtt-client library. + bool mqttIsConnecting = false; + AsyncMqttClient * mqtt = (AsyncMqttClient * ) mqttPointer; + while (1) { + // TODO: What is the sense behind these magics? + // If the MQTT client is not connected force a disconnect. + if (mqtt -> connected() != 1) { + mqttIsConnecting = false; + mqtt -> disconnect(true); + } + // If the MQTT client is not connecting, not already connected and the WiFi has a + // connection, try to connect + if (!mqttIsConnecting) { + if (mqtt -> connected() != 1) { + if (WiFi.status() == WL_CONNECTED) { + mqtt -> connect(); + mqttIsConnecting = true; + } else { + mqtt -> disconnect(); + } + } + } + vTaskDelay(100); } - else { - Serial.println("Waiting for WiFi ..."); - xTimerStart(xTimer, 0); - } -} - +}; #endif #ifdef BASECAMP_USEDNS #ifdef DNSServer_h // This is a task that handles DNS requests from clients -void Basecamp::DnsHandling(void * dnsServerPointer) -{ - DNSServer * dnsServer = (DNSServer *) dnsServerPointer; - while(1) { - // handle each request - dnsServer->processNextRequest(); - vTaskDelay(1000); - } +void Basecamp::DnsHandling(void * dnsServerPointer) { + DNSServer * dnsServer = (DNSServer * ) dnsServerPointer; + while (1) { + // handle each request + dnsServer -> processNextRequest(); + vTaskDelay(100); + } }; #endif #endif -// This function checks the reset reason returned by the ESP and resets the configuration if neccessary. -// It counts all system reboots that occured by power cycles or button resets. -// If the ESP32 receives an IP the boot counts as successful and the counter will be reset by Basecamps -// WiFi management. -void Basecamp::checkResetReason() -{ - // Instead of the internal flash it uses the somewhat limited, but sufficient preferences storage - preferences.begin("basecamp", false); - // Get the reset reason for the current boot - int reason = rtc_get_reset_reason(0); - DEBUG_PRINT("Reset reason: "); - DEBUG_PRINTLN(reason); - // If the reason is caused by a power cycle (1) or a RTC reset / button press(16) evaluate the current - // bootcount and act accordingly. - if (reason == 1 || reason == 16) { - // Get the current number of unsuccessful boots stored - unsigned int bootCounter = preferences.getUInt("bootcounter", 0); - // increment it - bootCounter++; - DEBUG_PRINT("Unsuccessful boots: "); - DEBUG_PRINTLN(bootCounter); - - // If the counter is bigger than 3 it will be the fifths consecutive unsucessful reboot. - // This forces a reset of the WiFi configuration and the AP will be opened again - if (bootCounter > 3){ - DEBUG_PRINTLN("Configuration forcibly reset."); - // Mark the WiFi configuration as invalid - configuration.set(ConfigurationKey::wifiConfigured, "False"); - // Save the configuration immediately - configuration.save(); - // Reset the boot counter - preferences.putUInt("bootcounter", 0); - // Call the destructor for preferences so that all data is safely stored befor rebooting - preferences.end(); - Serial.println("Resetting the WiFi configuration."); - // Reboot - ESP.restart(); - - // If the WiFi is unconfigured and the device is rebooted twice format the internal flash storage - } else if (bootCounter > 2 && configuration.get(ConfigurationKey::wifiConfigured).equalsIgnoreCase("false")) { - Serial.println("Factory reset was forced."); - // Format the flash storage - SPIFFS.format(); - // Reset the boot counter - preferences.putUInt("bootcounter", 0); - // Call the destructor for preferences so that all data is safely stored befor rebooting - preferences.end(); - Serial.println("Rebooting."); - // Reboot - ESP.restart(); - - // In every other case: store the current boot count - } else { - preferences.putUInt("bootcounter", bootCounter); - }; - - // if the reset has been for any other cause, reset the counter - } else { - preferences.putUInt("bootcounter", 0); - }; - // Call the destructor for preferences so that all data is safely stored - preferences.end(); +// This function checks the reset reason returned by the ESP and resets the +// configuration if neccessary. It counts all system reboots that occured +// by power cycles or button resets. If the ESP32 receives an IP the boot +// counts as successful and the counter will be reset by Basecamps WiFi management. +void Basecamp::checkResetReason() { + // Instead of the internal flash it uses the somewhat limited, but sufficient + // preferences storage + preferences.begin("basecamp", false); + // Get the reset reason for the current boot + int reason = rtc_get_reset_reason(0); + DEBUG_PRINT("Reset reason: "); + DEBUG_PRINTLN(reason); + // If the reason is caused by a power cycle (1) or a RTC reset / button press(16) + // evaluate the current bootcount and act accordingly. + if (reason == 1 || reason == 16) { + // Get the current number of unsuccessful boots stored + unsigned int bootCounter = preferences.getUInt("bootcounter", 0); + // increment it + bootCounter++; + DEBUG_PRINT("Unsuccessful boots: "); + DEBUG_PRINTLN(bootCounter); + + // If the counter is bigger than 3 it will be the fifths consecutive unsucessful + // reboot. + // This forces a reset of the WiFi configuration and the AP will be opened again + if (bootCounter > 3) { + DEBUG_PRINTLN("Configuration forcibly reset."); + // Mark the WiFi configuration as invalid + configuration.set("WifiConfigured", "False"); + // Save the configuration immediately + configuration.save(); + // Reset the boot counter + preferences.putUInt("bootcounter", 0); + // Call the destructor for preferences so that all data is safely stored befor + // rebooting + preferences.end(); + Serial.println("Resetting the WiFi configuration."); + // Reboot + ESP.restart(); + + // If the WiFi is unconfigured and the device is rebooted twice format the + // internal flash storage + } else if (bootCounter > 2 && configuration.get("WifiConfigured") == "False") { + Serial.println("Factory reset was forced."); + // Format the flash storage + SPIFFS.format(); + // Reset the boot counter + preferences.putUInt("bootcounter", 0); + // Call the destructor for preferences so that all data is safely stored befor + // rebooting + preferences.end(); + Serial.println("Rebooting."); + // Reboot + ESP.restart(); + + // In every other case: store the current boot count + } else { + preferences.putUInt("bootcounter", bootCounter); + }; + + // if the reset has been for any other cause, reset the counter + } else { + preferences.putUInt("bootcounter", 0); + }; + // Call the destructor for preferences so that all data is safely stored + preferences.end(); }; +#ifndef BASECAMP_NOOTA +// This tasks takes care of the ArduinoOTA function provided by Basecamp +void Basecamp::OTAHandling(void * OTAParams) { + + // Create a struct to store the given parameters + struct taskParms * params; + // Cast the void type pointer given to the task into a struct + params = (struct taskParms * ) OTAParams; + + // The first parameter is assumed to be the password for the OTA process + // If it's set, require a password for upgrades + if (strlen(params -> parm1) != 0) { + ArduinoOTA.setPassword(params -> parm1); + } + // The second parameter is assumed to be the hostname of the esp + // It is set to be distinctive in the Arduino IDE + ArduinoOTA.setHostname(params -> parm2); + // The following code is copied verbatim from the ESP32 BasicOTA.ino example + // This is the callback for the beginning of the OTA process + ArduinoOTA + .onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + type = "sketch"; + else // U_SPIFFS + type = "filesystem"; + SPIFFS.end(); + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using + // SPIFFS.end() + Serial.println("Start updating " + type); + }) + // When the update ends print it to serial + .onEnd([]() { + Serial.println("\nEnd"); + }) + // Show the progress of the update + .onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }) + // Error handling for the update + .onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); + else if (error == OTA_END_ERROR) Serial.println("End Failed"); + }); + // Start the OTA service + ArduinoOTA.begin(); + // The while loop checks if OTA requests are received and sleeps for a bit if not + while (1) { + ArduinoOTA.handle(); + + vTaskDelay(100); + + } +}; +#endif + // This shows basic information about the system. Currently only the mac // TODO: expand infos String Basecamp::showSystemInfo() { - std::ostringstream info; - info << "MAC-Address: " << mac.c_str(); - info << ", Hardware MAC: " << wifi.getHardwareMacAddress(":").c_str() << std::endl; - - if (configuration.isKeySet(ConfigurationKey::accessPointSecret)) { - info << "*******************************************" << std::endl; - info << "* ACCESS POINT PASSWORD: "; - info << configuration.get(ConfigurationKey::accessPointSecret).c_str() << std::endl; - info << "*******************************************" << std::endl; - } - - return {info.str().c_str()}; -} + std::ostringstream info; + info << "MAC-Address: " << mac.c_str(); + info << ", Hardware MAC: " << wifi.getHardwareMacAddress(":").c_str() << std::endl; + + if (configuration.isKeySet(ConfigurationKey::accessPointSecret)) { + info << "*******************************************" << std::endl; + info << "* ACCESS POINT PASSWORD: "; + info << configuration.get(ConfigurationKey::accessPointSecret).c_str() + << std::endl; + info << "*******************************************" << std::endl; + } + return { + info.str().c_str() + }; +} \ No newline at end of file diff --git a/Basecamp.hpp b/Basecamp.hpp index 869c079..ecdbcc3 100644 --- a/Basecamp.hpp +++ b/Basecamp.hpp @@ -25,19 +25,13 @@ #ifndef BASECAMP_NOMQTT #include -#include "mqttGuardInterface.hpp" -#include "freertos/timers.h" #endif #ifndef BASECAMP_NOOTA #include #endif -class Basecamp -#ifndef BASECAMP_NOMQTT - : public MqttGuardInterface -#endif -{ +class Basecamp { public: // How to handle encryption in setup mode (AP mode) enum class SetupModeWifiEncryption @@ -68,38 +62,37 @@ class Basecamp * SetupModeWifiEncryption will be overriden to SetupModeWifiEncryption::secure. */ bool begin(String fixedWiFiApEncryptionPassword = {}); - void handle(); - void checkResetReason(); String showSystemInfo(); - bool isSetupModeWifiEncrypted(); - String getSetupModeWifiName(); - String getSetupModeWifiSecret(); String hostname; - + struct taskParms { + const char* parm1; + const char* parm2; + }; #ifndef BASECAMP_NOWIFI String mac; WifiControl wifi; #endif #ifndef BASECAMP_NOMQTT - AsyncMqttClient mqtt; - static TimerHandle_t mqttReconnectTimer; - static void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); - static void connectToMqtt(TimerHandle_t xTimer); + AsyncMqttClient mqtt; + static void MqttHandling(void *); #endif #ifndef BASECAMP_NOWEB #ifdef BASECAMP_USEDNS #ifdef DNSServer_h - DNSServer dnsServer; + DNSServer dnsServer; static void DnsHandling(void *); #endif #endif WebServer web; #endif +#ifndef BASECAMP_NOOTA + static void OTAHandling(void *); +#endif private: String _cleanHostname(); bool shouldEnableConfigWebserver() const; diff --git a/Configuration.cpp b/Configuration.cpp index 9c9d814..5a6aee9 100644 --- a/Configuration.cpp +++ b/Configuration.cpp @@ -1,208 +1,182 @@ /* Basecamp - ESP32 library to simplify the basics of IoT projects - Written by Merlin Schumacher (mls@ct.de) for c't magazin für computer technik (https://www.ct.de) + Written by Merlin Schumacher (mls@ct.de) for c't magazin für computer technik + (https://www.ct.de) - updated by Guido Dampf (guido@dampf.de) Licensed under GPLv3. See LICENSE for details. */ #include "Configuration.hpp" -Configuration::Configuration() - : _memOnlyConfig( true ), - _jsonFile() -{ -} - -Configuration::Configuration(String filename) - : _memOnlyConfig( false ), - _jsonFile(std::move(filename)) -{ -} - -void Configuration::setMemOnly() { - _memOnlyConfig = true; - _jsonFile = ""; -} - -void Configuration::setFileName(const String& filename) { - _memOnlyConfig = false; - _jsonFile = filename; -} +Configuration::Configuration(String filename): _jsonFile(std::move(filename)) {} bool Configuration::load() { - DEBUG_PRINTLN("Loading config file "); - - if (_memOnlyConfig) { - DEBUG_PRINTLN("Memory-only configuration: Nothing loaded!"); - return false; - } - - DEBUG_PRINTLN(_jsonFile); - if (!SPIFFS.begin(true)) { - Serial.println("Could not access SPIFFS."); - return false; - } - - File configFile = SPIFFS.open(_jsonFile, "r"); - - if (!configFile || configFile.isDirectory()) { - Serial.println("Failed to open config file"); - return false; - } - - DynamicJsonBuffer _jsonBuffer; - JsonObject& _jsonData = _jsonBuffer.parseObject(configFile); - - if (!_jsonData.success()) { - Serial.println("Failed to parse config file."); - return false; - } - - for (const auto& configItem : _jsonData) { - set(configItem.key, configItem.value); - } - - configFile.close(); - return true; + DEBUG_PRINTLN("Loading config file "); + DEBUG_PRINTLN(_jsonFile); + if (!SPIFFS.begin(true)) { + Serial.println("Could not access SPIFFS."); + return false; + } + + File configFile = SPIFFS.open(_jsonFile, "r"); + + if (!configFile || configFile.isDirectory()) { + Serial.println("Failed to open config file"); + return false; + } + + DynamicJsonDocument _jsonDocument(JSON_SIZE); + + DeserializationError error = deserializeJson(_jsonDocument, configFile); + if (error) { + Serial.println("Failed to parse config file."); + return false; + } + + for (JsonPair configItem: _jsonDocument.as < JsonObject > ()) { + const char * key = configItem.key().c_str(); + JsonVariant value = configItem.value(); + set(String(key), value); + } + + #ifdef DEBUG + serializeJsonPretty(_jsonDocument, Serial); + Serial.println(); + #endif + configFile.close(); + #if DEBUG == 2 + esp_deep_sleep_start(); + #endif + return true; } bool Configuration::save() { - DEBUG_PRINTLN("Saving config file"); - - if (_memOnlyConfig) { - DEBUG_PRINTLN("Memory-only configuration: Nothing saved!"); - return false; - } - - File configFile = SPIFFS.open(_jsonFile, "w"); - if (!configFile) { - Serial.println("Failed to open config file for writing"); - return false; - } - - if (configuration.empty()) - { - Serial.println("Configuration empty"); - } - - DynamicJsonBuffer _jsonBuffer; - JsonObject &_jsonData = _jsonBuffer.createObject(); - - for (const auto& x : configuration) - { - _jsonData.set(x.first, String{x.second}); - } - - _jsonData.printTo(configFile); -#ifdef DEBUG - _jsonData.prettyPrintTo(Serial); -#endif - configFile.close(); - _configurationTainted = false; - return true; + DEBUG_PRINTLN("Saving config file"); + + File configFile = SPIFFS.open(_jsonFile, "w"); + if (!configFile) { + Serial.println("Failed to open config file for writing"); + return false; + } + + if (configuration.empty()) { + Serial.println("Configuration empty"); + } + + DynamicJsonDocument _jsonDocument(JSON_FILE_SIZE); + + for (const auto & x: configuration) { + _jsonDocument[x.first] = String { + x.second + }; + } + + if (serializeJson(_jsonDocument, configFile) == 0) { + Serial.println("Failed to write Config-File"); + } + #ifdef DEBUG + serializeJsonPretty(_jsonDocument, Serial); + Serial.println(); + #endif + configFile.close(); + _configurationTainted = false; + return true; } void Configuration::set(String key, String value) { - std::ostringstream debug; - debug << "Settting " << key.c_str() << " to " << value.c_str() << "(was " << get(key).c_str() << ")"; - DEBUG_PRINTLN(debug.str().c_str()); - - if (get(key) != value) { - _configurationTainted = true; - configuration[key] = value; - } else { - DEBUG_PRINTLN("Cowardly refusing to overwrite existing key with the same value"); - } + std::ostringstream debug; + debug << "Settting " << key.c_str() << " to " << value.c_str() << "(was " + << get(key).c_str() << ")"; + DEBUG_PRINTLN(debug.str().c_str()); + + if (get(key) != value) { + _configurationTainted = true; + configuration[key] = value; + } else { + DEBUG_PRINTLN("Cowardly refusing to overwrite existing key with the same value"); + } } -void Configuration::set(ConfigurationKey key, String value) -{ - set(getKeyName(key), std::move(value)); +void Configuration::set(ConfigurationKey key, String value) { + set(getKeyName(key), std::move(value)); } -const String &Configuration::get(String key) const -{ - auto found = configuration.find(key); - if (found != configuration.end()) { - std::ostringstream debug; - debug << "Config value for " << key.c_str() << ": " << found->second.c_str(); - DEBUG_PRINTLN(debug.str().c_str()); +const String & Configuration::get(String key) const { + auto found = configuration.find(key); + if (found != configuration.end()) { + std::ostringstream debug; + debug << "Config value for " << key.c_str() << ": " << found -> second.c_str(); + DEBUG_PRINTLN(debug.str().c_str()); - return found->second; - } + return found -> second; + } - // Default: if not set, we just return an empty String. TODO: Throw? - return noResult_; + // Default: if not set, we just return an empty String. TODO: Throw? + return noResult_; } -const String &Configuration::get(ConfigurationKey key) const -{ - return get(getKeyName(key)); +const String & Configuration::get(ConfigurationKey key) const { + return get(getKeyName(key)); } // return a char* instead of a Arduino String to maintain backwards compatibility // with printed examples -[[deprecated("getCString() is deprecated. Use get() instead")]] -char* Configuration::getCString(String key) -{ - char *newCString = (char*) malloc(configuration[key].length()+1); - strcpy(newCString,get(key).c_str()); - return newCString; +[ + [deprecated("getCString() is deprecated. Use get() instead")] +] char * Configuration::getCString(String key) { + char * newCString = (char * ) malloc(configuration[key].length() + 1); + strcpy(newCString, get(key).c_str()); + return newCString; } -bool Configuration::keyExists(const String& key) const -{ - return (configuration.find(key) != configuration.end()); +bool Configuration::keyExists(const String & key) const { + return (configuration.find(key) != configuration.end()); } -bool Configuration::keyExists(ConfigurationKey key) const -{ - return (configuration.find(getKeyName(key)) != configuration.end()); +bool Configuration::keyExists(ConfigurationKey key) const { + return (configuration.find(getKeyName(key)) != configuration.end()); } -bool Configuration::isKeySet(ConfigurationKey key) const -{ - auto found = configuration.find(getKeyName(key)); - if (found == configuration.end()) - { - return false; - } +bool Configuration::isKeySet(ConfigurationKey key) const { + auto found = configuration.find(getKeyName(key)); + if (found == configuration.end()) { + return false; + } - return (found->second.length() > 0); + return (found -> second.length() > 0); } -void Configuration::reset() -{ - configuration.clear(); - this->save(); - this->load(); +void Configuration::reset() { + configuration.clear(); + this -> save(); + this -> load(); } -void Configuration::resetExcept(const std::list &keysToPreserve) -{ - std::map preservedKeys; - for (const auto &key : keysToPreserve) { - if (keyExists(key)) { - // Make a copy of the old value - preservedKeys[key] = get(key); - } - } +void Configuration::resetExcept( + const std::list < ConfigurationKey > & keysToPreserve) { + std::map < ConfigurationKey, String > preservedKeys; + for (const auto & key: keysToPreserve) { + if (keyExists(key)) { + // Make a copy of the old value + preservedKeys[key] = get(key); + } + } - configuration.clear(); + configuration.clear(); - for (const auto &key : preservedKeys) { - set(key.first, key.second); - } + for (const auto & key: preservedKeys) { + set(key.first, key.second); + } - this->save(); - this->load(); + this -> save(); + this -> load(); } void Configuration::dump() { -#ifdef DEBUG - for (const auto &p : configuration) { - Serial.print( "configuration["); - Serial.print(p.first); - Serial.print("] = "); - Serial.println(p.second); - } -#endif -} + #ifdef DEBUG + for (const auto & p: configuration) { + Serial.print("configuration["); + Serial.print(p.first); + Serial.print("] = "); + Serial.println(p.second); + } + #endif +} \ No newline at end of file diff --git a/Configuration.hpp b/Configuration.hpp index aa7ab1d..c0cbb73 100644 --- a/Configuration.hpp +++ b/Configuration.hpp @@ -15,20 +15,13 @@ #include #include +#ifndef JSON_SIZE + #define JSON_SIZE 2048 +#endif + // TODO: Extend with all known keys enum class ConfigurationKey { - deviceName, accessPointSecret, - wifiConfigured, - wifiEssid, - wifiPassword, - mqttActive, - mqttHost, - mqttPort, - mqttUser, - mqttPass, - otaActive, - otaPass, }; // TODO: Extend with all known keys @@ -38,66 +31,22 @@ static const String getKeyName(ConfigurationKey key) // (if the warnings are turned on exactly...) switch (key) { - case ConfigurationKey::deviceName: - return "DeviceName"; - case ConfigurationKey::accessPointSecret: return "APSecret"; - - case ConfigurationKey::wifiConfigured: - return "WifiConfigured"; - - case ConfigurationKey::wifiEssid: - return "WifiEssid"; - - case ConfigurationKey::wifiPassword: - return "WifiPassword"; - - case ConfigurationKey::mqttActive: - return "MQTTActive"; - - case ConfigurationKey::mqttHost: - return "MQTTHost"; - - case ConfigurationKey::mqttPort: - return "MQTTPort"; - - case ConfigurationKey::mqttUser: - return "MQTTUser"; - - case ConfigurationKey::mqttPass: - return "MQTTPass"; - - case ConfigurationKey::otaActive: - return "OTAActive"; - - case ConfigurationKey::otaPass: - return "OTAPass"; + break; } return ""; } class Configuration { public: - // Default constructor: Memory-only configuration (NO EEPROM read/writes - Configuration(); - // Constructor with filename: Can be read from and written to EEPROM explicit Configuration(String filename); ~Configuration() = default; - - // Switched configuration to memory-only and empties filename - void setMemOnly(); - // Sets new filename and removes memory-only tag - void setFileName(const String& filename); - // Returns memory-only state of configuration - bool isMemOnly() {return _memOnlyConfig;} const String& getKey(ConfigurationKey configKey) const; - // Both functions return true on successful load or save. Return false on any failure. Also return false for memory-only configurations. bool load(); bool save(); - void dump(); // Returns true if the key 'key' exists @@ -142,8 +91,6 @@ class Configuration { String _jsonFile; bool _configurationTainted = false; String noResult_ = {}; - // Set to true if configuration is memory-only - bool _memOnlyConfig; }; #endif diff --git a/WebServer.cpp b/WebServer.cpp index bdcae68..69f8912 100644 --- a/WebServer.cpp +++ b/WebServer.cpp @@ -1,222 +1,209 @@ /* Basecamp - ESP32 library to simplify the basics of IoT projects - Written by Merlin Schumacher (mls@ct.de) for c't magazin für computer technik (https://www.ct.de) + Written by Merlin Schumacher (mls@ct.de) for c't magazin für computer technik + (https://www.ct.de) - updated by Guido Dampf (guido@dampf.de) Licensed under GPLv3. See LICENSE for details. */ #include "WebServer.hpp" namespace { - template - void debugPrint(std::ostream &stream, NAMEVALUETYPE &nameAndValue) - { - stream << nameAndValue->name().c_str() << ": " << nameAndValue->value().c_str(); - } + template < typename NAMEVALUETYPE > + void debugPrint(std::ostream & stream, NAMEVALUETYPE & nameAndValue) { + stream << nameAndValue -> name().c_str() << ": " + << nameAndValue -> value().c_str(); + } +} // namespace + +WebServer::WebServer(): events("/events"), server(80) { + server.addHandler( & events); + #ifdef BASECAMP_USEDNS + #ifdef DNSServer_h + server.addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER); + #endif + #endif } -WebServer::WebServer() - : events("/events") - , server(80) -{ - server.addHandler(&events); -#ifdef BASECAMP_USEDNS -#ifdef DNSServer_h - server.addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER); -#endif -#endif +void WebServer::begin(Configuration & configuration) { + SPIFFS.begin(); + server.begin(); + + server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = + request -> beginResponse_P(200, "text/html", index_htm_gz, index_htm_gz_len); + response -> addHeader("Content-Encoding", "gzip"); + request -> send(response); + }); + + server.on("/basecamp.css", HTTP_GET, [](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = + request -> beginResponse_P(200, "text/css", basecamp_css_gz, + basecamp_css_gz_len); + response -> addHeader("Content-Encoding", "gzip"); + request -> send(response); + }); + + server.on("/basecamp.js", HTTP_GET, [](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = + request -> beginResponse_P(200, "text/js", basecamp_js_gz, basecamp_js_gz_len); + response -> addHeader("Content-Encoding", "gzip"); + request -> send(response); + }); + server.on("/logo.svg", HTTP_GET, [](AsyncWebServerRequest * request) { + AsyncWebServerResponse * response = + request -> beginResponse_P(200, "image/svg+xml", logo_svg_gz, logo_svg_gz_len); + response -> addHeader("Content-Encoding", "gzip"); + request -> send(response); + }); + + server.on("/data.json", HTTP_GET, + [ & configuration, this](AsyncWebServerRequest * request) { + AsyncJsonResponse * response = new AsyncJsonResponse(false, JSON_RESPONSE_SIZE); + + JsonVariant _jsonData = response -> getRoot(); + JsonArray elements = _jsonData.createNestedArray("elements"); + + for (const auto & interfaceElement: interfaceElements) { + JsonObject element = elements.createNestedObject(); + JsonObject attributes = element.createNestedObject("attributes"); + element["element"] = interfaceElement.element; + element["id"] = interfaceElement.id; + element["content"] = interfaceElement.content; + element["parent"] = interfaceElement.parent; + + for (const auto & attribute: interfaceElement.attributes) { + attributes[attribute.first] = String { + attribute.second + }; + } + + if (interfaceElement.getAttribute("data-config").length() != 0) { + if (interfaceElement.getAttribute("type") == "password") { + attributes["placeholder"] = "Password unchanged"; + attributes["value"] = ""; + } else { + attributes["value"] = String { + configuration.get(interfaceElement.getAttribute("data-config")) + }; + } + } + } + #ifdef DEBUG + serializeJsonPretty(_jsonData, Serial); + #endif + response -> setLength(); + // NOTE: AsyncServer.send(ptr* foo) deletes `response` after async send. + // As this is not documented in the header there: thanks for nothing. + request -> send(response); + }); + + server.on("/submitconfig", HTTP_POST, + [ & configuration, this](AsyncWebServerRequest * request) { + if (request -> params() == 0) { + DEBUG_PRINTLN("Refusing to take over an empty configuration submission."); + request -> send(500); + return; + } + debugPrintRequest(request); + + for (int i = 0; i < request -> params(); i++) { + AsyncWebParameter * webParameter = request -> getParam(i); + if (webParameter -> isPost() && webParameter -> value().length() != 0) { + configuration.set(webParameter -> name().c_str(), + webParameter -> value().c_str()); + } + } + + configuration.save(); + request -> send(201); + + // Why? What is this magic value for? + delay(2000); + esp_restart(); + }); + + server.onNotFound([this](AsyncWebServerRequest * request) { + #ifdef DEBUG + DEBUG_PRINTLN("WebServer request not found: "); + debugPrintRequest(request); + #endif + request -> send(404); + }); } -void WebServer::begin(Configuration &configuration, std::function submitFunc) { - SPIFFS.begin(); - - server.on("/" , HTTP_GET, [](AsyncWebServerRequest * request) - { - AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", index_htm_gz, index_htm_gz_len); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); - }); - - server.on("/basecamp.css" , HTTP_GET, [](AsyncWebServerRequest * request) - { - AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", basecamp_css_gz, basecamp_css_gz_len); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); - }); - - server.on("/basecamp.js" , HTTP_GET, [](AsyncWebServerRequest * request) - { - AsyncWebServerResponse *response = request->beginResponse_P(200, "text/js", basecamp_js_gz, basecamp_js_gz_len); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); - }); - server.on("/logo.svg" , HTTP_GET, [](AsyncWebServerRequest * request) - { - AsyncWebServerResponse *response = request->beginResponse_P(200, "image/svg+xml", logo_svg_gz, logo_svg_gz_len); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); - }); - - server.on("/data.json" , HTTP_GET, [&configuration, this](AsyncWebServerRequest * request) - { - AsyncJsonResponse *response = new AsyncJsonResponse(); - DynamicJsonBuffer _jsonBuffer; - - JsonObject &_jsonData = response->getRoot(); - JsonArray &elements = _jsonData.createNestedArray("elements"); - - for (const auto &interfaceElement : interfaceElements) - { - JsonObject &element = elements.createNestedObject(); - JsonObject &attributes = element.createNestedObject("attributes"); - element["element"] = _jsonBuffer.strdup(interfaceElement.element); - element["id"] = _jsonBuffer.strdup(interfaceElement.id); - element["content"] = _jsonBuffer.strdup(interfaceElement.content); - element["parent"] = _jsonBuffer.strdup(interfaceElement.parent); - - for (const auto &attribute : interfaceElement.attributes) - { - attributes[attribute.first] = String{attribute.second}; - } - - if (interfaceElement.getAttribute("data-config").length() != 0) - { - if (interfaceElement.getAttribute("type")=="password") - { - attributes["placeholder"] = "Password unchanged"; - attributes["value"] = ""; - } else { - attributes["value"] = String{configuration.get(interfaceElement.getAttribute("data-config"))}; - } - } - } -#ifdef DEBUG - _jsonData.prettyPrintTo(Serial); -#endif - response->setLength(); - // NOTE: AsyncServer.send(ptr* foo) deletes `response` after async send. - // As this is not documented in the header there: thanks for nothing. - request->send(response); - }); - - server.on("/submitconfig", HTTP_POST, [&configuration, submitFunc, this](AsyncWebServerRequest *request) - { - if (request->params() == 0) { - DEBUG_PRINTLN("Refusing to take over an empty configuration submission."); - request->send(500); - return; - } - debugPrintRequest(request); - - for (int i = 0; i < request->params(); i++) - { - AsyncWebParameter *webParameter = request->getParam(i); - if (webParameter->isPost() && webParameter->value().length() != 0) - { - configuration.set(webParameter->name().c_str(), webParameter->value().c_str()); - } - } - - configuration.save(); - request->send(201); - - // Only call submitFunc when it has been set to something useful - if( submitFunc ) submitFunc(); - }); - - server.onNotFound([this](AsyncWebServerRequest *request) - { -#ifdef DEBUG - DEBUG_PRINTLN("WebServer request not found: "); - debugPrintRequest(request); -#endif - request->send(404); - }); - - server.begin(); +void WebServer::debugPrintRequest(AsyncWebServerRequest * request) { + #ifdef DEBUG + /** + That AsyncWebServer code uses some strange bit-consstructs instead of enum + class. Also no const getter. As I refuse to bring that code to 21st century, + we have to live with it until someone brave fixes it. + */ + const std::map < WebRequestMethodComposite, std::string > + requestMethods { + {HTTP_GET, "GET"}, + {HTTP_POST, "POST"}, + {HTTP_DELETE, "DELETE"}, + {HTTP_PUT, "PUT"}, + {HTTP_PATCH, "PATCH"}, + {HTTP_HEAD, "HEAD"}, + {HTTP_OPTIONS, "OPTIONS"}, + }; + + std::ostringstream output; + + output << "Method: "; + auto found = requestMethods.find(request -> method()); + if (found != requestMethods.end()) { + output << found -> second; + } else { + output << "Unknown (" << static_cast < unsigned int > (request -> method()) + << ")"; + } + + output << std::endl; + output << "URL: " << request -> url().c_str() << std::endl; + output << "Content-Length: " << request -> contentLength() << std::endl; + output << "Content-Type: " << request -> contentType().c_str() << std::endl; + + output << "Headers: " << std::endl; + for (int i = 0; i < request -> headers(); i++) { + auto * header = request -> getHeader(i); + output << "\t"; + debugPrint(output, header); + output << std::endl; + } + + output << "Parameters: " << std::endl; + for (int i = 0; i < request -> params(); i++) { + auto * parameter = request -> getParam(i); + output << "\t"; + if (parameter -> isFile()) { + output << "This is a file. FileSize: " << parameter -> size() << std::endl << + "\t\t"; + } + debugPrint(output, parameter); + output << std::endl; + } + + Serial.println(output.str().c_str()); + #endif } -void WebServer::debugPrintRequest(AsyncWebServerRequest *request) -{ -#ifdef DEBUG - /** - That AsyncWebServer code uses some strange bit-consstructs instead of enum - class. Also no const getter. As I refuse to bring that code to 21st century, - we have to live with it until someone brave fixes it. - */ - const std::map requestMethods{ - { HTTP_GET, "GET" }, - { HTTP_POST, "POST" }, - { HTTP_DELETE, "DELETE" }, - { HTTP_PUT, "PUT" }, - { HTTP_PATCH, "PATCH" }, - { HTTP_HEAD, "HEAD" }, - { HTTP_OPTIONS, "OPTIONS" }, - }; - - std::ostringstream output; - - output << "Method: "; - auto found = requestMethods.find(request->method()); - if (found != requestMethods.end()) { - output << found->second; - } else { - output << "Unknown (" << static_cast(request->method()) << ")"; - } - - output << std::endl; - output << "URL: " << request->url().c_str() << std::endl; - output << "Content-Length: " << request->contentLength() << std::endl; - output << "Content-Type: " << request->contentType().c_str() << std::endl; - - output << "Headers: " << std::endl; - for (int i = 0; i < request->headers(); i++) { - auto *header = request->getHeader(i); - output << "\t"; - debugPrint(output, header); - output << std::endl; - } - - output << "Parameters: " << std::endl; - for (int i = 0; i < request->params(); i++) { - auto *parameter = request->getParam(i); - output << "\t"; - if (parameter->isFile()) { - output << "This is a file. FileSize: " << parameter->size() << std::endl << "\t\t"; - } - debugPrint(output, parameter); - output << std::endl; - } - - Serial.println(output.str().c_str()); -#endif -} - -void WebServer::addInterfaceElement(const String &id, String element, String content, String parent, String configvariable) { - interfaceElements.emplace_back(id, std::move(element), std::move(content), std::move(parent)); - if (configvariable.length() != 0) { - setInterfaceElementAttribute(id, "data-config", std::move(configvariable)); - } -} - -void WebServer::setInterfaceElementAttribute(const String &id, const String &key, String value) -{ - for (auto &element : interfaceElements) { - if (element.getId() == id) { - element.setAttribute(key, std::move(value)); - return; - } - } -} - -void WebServer::reset() { - interfaceElements.clear(); - // We should also reset the server itself, according to documentation, but it will cause a crash. - // It works without reset, if you only configure one server after a reboot. Not sure what happens if you want to reconfigure during runtime. - //server.reset(); - //server.addHandler(&events); -//#ifdef BASECAMP_USEDNS -//#ifdef DNSServer_h - //server.addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER); -//#endif -//#endif -} +void WebServer::addInterfaceElement(const String & id, String element, + String content, String parent, String configvariable) { + interfaceElements.emplace_back(id, std::move(element), std::move(content), + std::move(parent)); + if (configvariable.length() != 0) { + setInterfaceElementAttribute(id, "data-config", std::move(configvariable)); + } + } + +void WebServer::setInterfaceElementAttribute(const String & id, + const String & key, String value) { + for (auto & element: interfaceElements) { + if (element.getId() == id) { + element.setAttribute(key, std::move(value)); + return; + } + } +} \ No newline at end of file