From 6050fb6956a800d83b9a60e7a5f694f1ab6fb0d4 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 14 Nov 2019 19:55:11 +0100 Subject: [PATCH] Migrated webserver to ESP Async Webserver * Integrate IotWebConf as local library * Add support for ESP Async Webserver to IotWebConf * Changed application to work with Async Webserver --- lib/IotWebConf/.clang-format | 5 + lib/IotWebConf/.gitignore | 1 + lib/IotWebConf/.library.json | 40 + lib/IotWebConf/.travis.yml | 41 + lib/IotWebConf/LICENSE.txt | 9 + lib/IotWebConf/README.md | 79 + .../IotWebConf01Minimal.ino | 86 ++ .../IotWebConf02StatusAndReset.ino | 96 ++ .../IotWebConf03CustomParameters.ino | 150 ++ .../IotWebConf04UpdateServer.ino | 99 ++ .../IotWebConf05Callbacks.ino | 142 ++ .../IotWebConf06MqttApp.ino | 265 ++++ .../IotWebConf07MqttRelay.ino | 299 ++++ .../IotWebConf08WebRelay.ino | 196 +++ .../IotWebConf09CustomConnection.ino | 179 +++ .../IotWebConf10CustomHtml.ino | 116 ++ lib/IotWebConf/keywords.txt | 43 + lib/IotWebConf/library.properties | 10 + lib/IotWebConf/src/IotWebConf.cpp | 1278 +++++++++++++++++ lib/IotWebConf/src/IotWebConf.h | 589 ++++++++ .../src/IotWebConfCompatibility.cpp | 120 ++ lib/IotWebConf/src/IotWebConfCompatibility.h | 94 ++ platformio.ini | 7 +- src/main.cpp | 30 +- 24 files changed, 3957 insertions(+), 17 deletions(-) create mode 100644 lib/IotWebConf/.clang-format create mode 100644 lib/IotWebConf/.gitignore create mode 100644 lib/IotWebConf/.library.json create mode 100644 lib/IotWebConf/.travis.yml create mode 100644 lib/IotWebConf/LICENSE.txt create mode 100644 lib/IotWebConf/README.md create mode 100644 lib/IotWebConf/examples/IotWebConf01Minimal/IotWebConf01Minimal.ino create mode 100644 lib/IotWebConf/examples/IotWebConf02StatusAndReset/IotWebConf02StatusAndReset.ino create mode 100644 lib/IotWebConf/examples/IotWebConf03CustomParameters/IotWebConf03CustomParameters.ino create mode 100644 lib/IotWebConf/examples/IotWebConf04UpdateServer/IotWebConf04UpdateServer.ino create mode 100644 lib/IotWebConf/examples/IotWebConf05Callbacks/IotWebConf05Callbacks.ino create mode 100644 lib/IotWebConf/examples/IotWebConf06MqttApp/IotWebConf06MqttApp.ino create mode 100644 lib/IotWebConf/examples/IotWebConf07MqttRelay/IotWebConf07MqttRelay.ino create mode 100644 lib/IotWebConf/examples/IotWebConf08WebRelay/IotWebConf08WebRelay.ino create mode 100644 lib/IotWebConf/examples/IotWebConf09CustomConnection/IotWebConf09CustomConnection.ino create mode 100644 lib/IotWebConf/examples/IotWebConf10CustomHtml/IotWebConf10CustomHtml.ino create mode 100644 lib/IotWebConf/keywords.txt create mode 100644 lib/IotWebConf/library.properties create mode 100644 lib/IotWebConf/src/IotWebConf.cpp create mode 100644 lib/IotWebConf/src/IotWebConf.h create mode 100644 lib/IotWebConf/src/IotWebConfCompatibility.cpp create mode 100644 lib/IotWebConf/src/IotWebConfCompatibility.h diff --git a/lib/IotWebConf/.clang-format b/lib/IotWebConf/.clang-format new file mode 100644 index 0000000..c2b03d8 --- /dev/null +++ b/lib/IotWebConf/.clang-format @@ -0,0 +1,5 @@ +BasedOnStyle: LLVM +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: InlineOnly +AlignAfterOpenBracket: AlwaysBreak +BreakBeforeBraces: Allman \ No newline at end of file diff --git a/lib/IotWebConf/.gitignore b/lib/IotWebConf/.gitignore new file mode 100644 index 0000000..722d5e7 --- /dev/null +++ b/lib/IotWebConf/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/lib/IotWebConf/.library.json b/lib/IotWebConf/.library.json new file mode 100644 index 0000000..914460a --- /dev/null +++ b/lib/IotWebConf/.library.json @@ -0,0 +1,40 @@ +{ + "name": "IotWebConf", + "version": "2.3.0", + "keywords": [ + "communication" + ], + "description": "ESP8266/ESP32 non-blocking WiFi/AP web configuration.", + "frameworks": [ + "arduino" + ], + "platforms": [ + "espressif8266", + "espressif32" + ], + "authors": [ + { + "email": "prampec+arduino@gmail.com", + "url": null, + "maintainer": true, + "name": "Balazs Kelemen" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/prampec/IotWebConf" + }, + "homepage": null, + "export": { + "include": null, + "exclude": [ + "extras", + "docs", + "tests", + "test", + "*.doxyfile", + "*.pdf" + ] + }, + "id": 5676 +} \ No newline at end of file diff --git a/lib/IotWebConf/.travis.yml b/lib/IotWebConf/.travis.yml new file mode 100644 index 0000000..2f4b379 --- /dev/null +++ b/lib/IotWebConf/.travis.yml @@ -0,0 +1,41 @@ +language: c +env: + global: + - ARDUINO_VERSION=1.8.9 + matrix: + - BOARD="esp8266:esp8266:d1_mini:xtal=80,eesz=4M3M,ip=lm2f,exception=disabled" + - BOARD="esp32:esp32:widora-air:FlashFreq=80" +before_install: + - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_1.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :1 -ac -screen 0 1280x1024x16" + - sleep 3 + - export DISPLAY=:1.0 + - wget http://downloads.arduino.cc/arduino-$ARDUINO_VERSION-linux64.tar.xz + - tar xf arduino-$ARDUINO_VERSION-linux64.tar.xz + - sudo mv arduino-$ARDUINO_VERSION /usr/local/share/arduino + - sudo ln -s /usr/local/share/arduino/arduino /usr/local/bin/arduino +install: + - ln -s $PWD /usr/local/share/arduino/libraries/IotWebConf + - arduino --install-library "MQTT" + - arduino --pref "boardsmanager.additional.urls=http://arduino.esp8266.com/stable/package_esp8266com_index.json,https://dl.espressif.com/dl/package_esp32_index.json" --save-prefs + - if [[ "$BOARD" =~ "esp8266:esp8266:" ]]; then + arduino --install-boards esp8266:esp8266; + fi + - if [[ "$BOARD" =~ "esp32:esp32:" ]]; then + arduino --install-boards esp32:esp32; + fi +script: + - arduino --verify --board $BOARD examples/IotWebConf01Minimal/IotWebConf01Minimal.ino + - arduino --verify --board $BOARD examples/IotWebConf02StatusAndReset/IotWebConf02StatusAndReset.ino + - arduino --verify --board $BOARD examples/IotWebConf03CustomParameters/IotWebConf03CustomParameters.ino + - arduino --verify --board $BOARD examples/IotWebConf04UpdateServer/IotWebConf04UpdateServer.ino + - arduino --verify --board $BOARD examples/IotWebConf05Callbacks/IotWebConf05Callbacks.ino + - arduino --verify --board $BOARD examples/IotWebConf06MqttApp/IotWebConf06MqttApp.ino + - arduino --verify --board $BOARD examples/IotWebConf07MqttRelay/IotWebConf07MqttRelay.ino + - arduino --verify --board $BOARD examples/IotWebConf08WebRelay/IotWebConf08WebRelay.ino + - arduino --verify --board $BOARD examples/IotWebConf09CustomConnection/IotWebConf09CustomConnection.ino +after_success: + - bash <(curl -s https://codecov.io/bash) +notifications: + email: + on_success: change + on_failure: change diff --git a/lib/IotWebConf/LICENSE.txt b/lib/IotWebConf/LICENSE.txt new file mode 100644 index 0000000..e129fc3 --- /dev/null +++ b/lib/IotWebConf/LICENSE.txt @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright 2018 Balazs Kelemen + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/IotWebConf/README.md b/lib/IotWebConf/README.md new file mode 100644 index 0000000..39eb61a --- /dev/null +++ b/lib/IotWebConf/README.md @@ -0,0 +1,79 @@ +# IotWebConf [![Build Status](https://travis-ci.org/prampec/IotWebConf.svg?branch=master)](https://travis-ci.org/prampec/IotWebConf) + +## Summary +IotWebConf is an Arduino library for ESP8266/ESP32 to provide a non-blocking standalone WiFi/AP web configuration portal. +**For ESP8266, IotWebConf requires the esp8266 board package version 2.4.2 or later!** + +Please subscribe to the [discussion forum](https://groups.google.com/forum/#!forum/iotwebconf), if you want to be informed on the latest news. + +## Highlights + + - Manages WiFi connection settings, + - Provides a config portal user interface, + - You can extend the configuration with your own property items, that are stored automatically, + - HTML customization, + - Validation support for the configuration property items, + - User code will be notified of status changes with callback methods, + - Configuration (including your custom items) stored in the EEPROM, + - Firmware OTA update support, + - Config portal remains available even after WiFi is connected, + - Automatic "Sign in to network" pop up in your browser (captive portal), + - Non-blocking - Your custom code will not be blocked in the whole process. + - Well documented header file, and examples from simple to complex levels. + +![Screenshot](https://sharedinventions.com/wp-content/uploads/2018/11/Screenshot_20181105-191748a.png) +![Screenshot](https://sharedinventions.com/wp-content/uploads/2019/02/Screenshot-from-2019-02-03-22-16-51b.png) + +## How it works +The idea is that the Thing will provide a web interface to allow modifying its configuration. E.g. for connecting to a local WiFi network, it needs the SSID and the password. + +When no WiFi is configured, or the configured network is unavailable it creates its own AP (access point), and lets clients connect to it directly to make the configuration. + +Furthermore there is a button (or let's say a Pin), that when pressed on startup will cause a default password to be used instead of the configured (forgotten) one. +You can find the default password in the sources. :) + +IotWebConf saves configuration in the "EEPROM". You can extend the config portal with your custom configuration items. Those items will be also maintained by IotWebConf. + +## Use cases + 1. **You turn on your IoT the first time** - It turns into AP (access point) mode, and waits for you on the 192.168.4.1 address with a web interface to set up your local network (and other configurations). For the first time a default password is used when you connect to the AP. When you connect to the AP, your device will likely automatically pop up the portal page. (We call this a Captive Portal.) When configuration is done, you must leave the AP. The device detects that no one is connected, and continues with normal operation. + 1. **WiFi configuration is changed, e.g. the Thing is moved to another location** - When the Thing cannot connect to the configured WiFi, it falls back to AP mode, and waits for you to change the network configuration. When no configuration was made, then it keeps trying to connect with the already configured settings. The Thing will not switch off the AP while anyone is connected to it, so you must leave the AP when finished with the configuration. + 1. **You want to connect to the AP, but have forgotten the configured AP WiFi password you set up previously** - Connect the appropriate pin on the Arduino to ground with a push button. Holding the button pressed while powering up the device causes the Thing to start the AP mode with the default password. (See Case 1. The pin is configured in the code.) + 1. **You want to change the configuration before the Thing connects to the Internet** - Fine! The Thing always starts up in AP mode and provides you a time frame to connect to it and make any modification to the configuration. Any time one is connected to the AP (provided by the device) the AP will stay on until the connection is closed. So take your time for the changes, the Thing will wait for you while you are connected to it. + 1. **You want to change the configuration at runtime** - No problem. IotWebConf keeps the config portal up and running even after the WiFi connection is finished. In this scenario you must enter username "admin" and password (already configured) to enter the config portal. Note, that the password provided for the authentication is not hidden from devices connected to the same WiFi network. You might want to force rebooting of the Thing to apply your changes. + +## IotWebConf vs. WiFiManager +tzapu's WiFiManager is a great library. The features of IotWebConf may appear very similar to WiFiManager. However, IotWebConf tries to be different. + - WiFiManager does not manages your custom properties. IotWebConf stores your configuration in "EEPROM". + - WiFiManager does not do validation. IotWebConf allow you to validate your property changes made in the config portal. + - With WiFiManager you cannot use both startup and on-demand configuration. With IotWebConf the config portal remains available via the connected local WiFi. + - WiFiManager provides list of available networks and also an information page, while these features are cool, IotWebConf tries to keep the code simple. So these features are not (yet) provided by IotWebConf. + - IotWebConf is fitted for more advanced users. You can keep control of the web server setup, configuration item input field behavior, and validation. + +## Security aspects + - The initial system password must be modified by the user, so there is no build-in password. + - When connecting in AP mode, the WiFi provides an encryption layer, so all you communication here is known to be safe. + - When connecting through a WiFi router (WiFi mode), the Thing will ask for authentication when someone requests the config portal. This is required as the Thing will be visible for all devices sharing the same network. But be warned by the following note... + - NOTE: **When connecting through a WiFi router (WiFi mode), your communication is not hidden from devices connecting to the same network.** So either: Do not allow ambiguous devices connecting to your WiFi router, or configure your Thing only in AP mode! + - However IotWebConf has a detailed debug output, passwords are not shown in this log by default. You have + to enable password visibility manually in the IotWebConf.h with the IOTWEBCONF_DEBUG_PWD_TO_SERIAL + if it is needed. + +## Compatibility +IotWebConf is primary built for ESP8266. But meanwhile it was discovered, that the code can be adopted +to ESP32. There are two major problems. + - ESP8266 uses specific naming for it's classes (e.g. ESP8266WebServer). However ESP32 uses a more generic naming (e.g. WebServer). The idea here is to use the generic naming hoping that ESP8266 will adopt these "standards" sooner or later. + - ESP32 does not provides an HTTPUpdateServer implementation. So in this project we have implemented one. Whenever ESP32 provides an official HTTPUpdateServer, this local implementation will be removed. + +## TODO / Feature requests + - We might want to add a "verify password" field. + - Possibility to organize blocks of config items to lists. (E.g. provide more SSIDs with passwords as a connection option.) + - Option the configure multiply WiFi connections. Try next when the last used one is just not available. + +## Known issues + - It is reported, that there might be unstable working with different lwIP variants. If you experiment serious problems, try to select another lwIP variant for your board in the Tools menu! (Tested with "v2 Lower Memory" version.) + +## Credits +Although IotWebConf started without being influenced by any other solutions, in the final code you can find some segments borrowed from the WiFiManager library. + - https://github.com/tzapu/WiFiManager + +Thanks to [all contributors](/prampec/IotWebConf/graphs/contributors) providing patches for the library! diff --git a/lib/IotWebConf/examples/IotWebConf01Minimal/IotWebConf01Minimal.ino b/lib/IotWebConf/examples/IotWebConf01Minimal/IotWebConf01Minimal.ino new file mode 100644 index 0000000..f72b0b7 --- /dev/null +++ b/lib/IotWebConf/examples/IotWebConf01Minimal/IotWebConf01Minimal.ino @@ -0,0 +1,86 @@ +/** + * IotWebConf01Minimal.ino -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Example: Minimal + * Description: + * This example will shows the bare minimum required for IotWeConf to start up. + * After starting up the thing, please search for WiFi access points e.g. with + * your phone. Use password provided in the code! + * After connecting to the access point the root page will automatically appears. + * We call this "captive portal". + * + * Please set a new password for the Thing (for the access point) as well as + * the SSID and password of your local WiFi. You cannot move on without these steps. + * + * You have to leave the access point before to let the Thing continue operation + * with connecting to configured WiFi. + * + * Note that you can find detailed debug information in the serial console depending + * on the settings IOTWEBCONF_DEBUG_TO_SERIAL, IOTWEBCONF_DEBUG_PWD_TO_SERIAL set + * in the IotWebConf.h . + */ + +#include + +// -- Initial name of the Thing. Used e.g. as SSID of the own Access Point. +const char thingName[] = "testThing"; + +// -- Initial password to connect to the Thing, when it creates an own Access Point. +const char wifiInitialApPassword[] = "smrtTHNG8266"; + +DNSServer dnsServer; +WebServer server(80); + +IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword); + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println("Starting up..."); + + // -- Initializing the configuration. + iotWebConf.init(); + + // -- Set up required URL handlers on the web server. + server.on("/", handleRoot); + server.on("/config", []{ iotWebConf.handleConfig(); }); + server.onNotFound([](){ iotWebConf.handleNotFound(); }); + + Serial.println("Ready."); +} + +void loop() +{ + // -- doLoop should be called as frequently as possible. + iotWebConf.doLoop(); +} + +/** + * Handle web requests to "/" path. + */ +void handleRoot() +{ + // -- Let IotWebConf test and handle captive portal requests. + if (iotWebConf.handleCaptivePortal()) + { + // -- Captive portal request were already served. + return; + } + String s = ""; + s += "IotWebConf 01 MinimalHello world!"; + s += "Go to configure page to change settings."; + s += "\n"; + + server.send(200, "text/html", s); +} + diff --git a/lib/IotWebConf/examples/IotWebConf02StatusAndReset/IotWebConf02StatusAndReset.ino b/lib/IotWebConf/examples/IotWebConf02StatusAndReset/IotWebConf02StatusAndReset.ino new file mode 100644 index 0000000..7c3a8f4 --- /dev/null +++ b/lib/IotWebConf/examples/IotWebConf02StatusAndReset/IotWebConf02StatusAndReset.ino @@ -0,0 +1,96 @@ +/** + * IotWebConf02StatusAndReset.ino -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Example: Status and Reset + * Description: + * This example is very similar to the "mininal" example. + * But here we provide a status indicator LED, to get feedback + * of the thing state. + * Further more we set up a push button. If push button is detected + * to be pressed at boot time, the thing will use the initial + * password for accepting connections in Access Point mode. This + * is usefull e.g. when custom password was lost. + * (See previous examples for more details!) + * + * Hardware setup for this example: + * - An LED is attached to LED_BUILTIN pin with setup On=LOW. + * This is hopefully already attached by default. + * - A push button is attached to pin D2, the other leg of the + * button should be attached to GND. + */ + +#include + +// -- Initial name of the Thing. Used e.g. as SSID of the own Access Point. +const char thingName[] = "testThing"; + +// -- Initial password to connect to the Thing, when it creates an own Access Point. +const char wifiInitialApPassword[] = "smrtTHNG8266"; + +// -- When CONFIG_PIN is pulled to ground on startup, the Thing will use the initial +// password to buld an AP. (E.g. in case of lost password) +#define CONFIG_PIN D2 + +// -- Status indicator pin. +// First it will light up (kept LOW), on Wifi connection it will blink, +// when connected to the Wifi it will turn off (kept HIGH). +#define STATUS_PIN LED_BUILTIN + +DNSServer dnsServer; +WebServer server(80); + +IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword); + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println("Starting up..."); + + // -- Initializing the configuration. + iotWebConf.setStatusPin(STATUS_PIN); + iotWebConf.setConfigPin(CONFIG_PIN); + iotWebConf.init(); + + // -- Set up required URL handlers on the web server. + server.on("/", handleRoot); + server.on("/config", []{ iotWebConf.handleConfig(); }); + server.onNotFound([](){ iotWebConf.handleNotFound(); }); + + Serial.println("Ready."); +} + +void loop() +{ + // -- doLoop should be called as frequently as possible. + iotWebConf.doLoop(); +} + +/** + * Handle web requests to "/" path. + */ +void handleRoot() +{ + // -- Let IotWebConf test and handle captive portal requests. + if (iotWebConf.handleCaptivePortal()) + { + // -- Captive portal request were already served. + return; + } + String s = ""; + s += "IotWebConf 02 Status and ResetHello world!"; + s += "Go to configure page to change settings."; + s += "\n"; + + server.send(200, "text/html", s); +} + diff --git a/lib/IotWebConf/examples/IotWebConf03CustomParameters/IotWebConf03CustomParameters.ino b/lib/IotWebConf/examples/IotWebConf03CustomParameters/IotWebConf03CustomParameters.ino new file mode 100644 index 0000000..afff8eb --- /dev/null +++ b/lib/IotWebConf/examples/IotWebConf03CustomParameters/IotWebConf03CustomParameters.ino @@ -0,0 +1,150 @@ +/** + * IotWebConf03CustomParameters.ino -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Example: Custom parameters + * Description: + * In this example it is shown how to attach your custom parameters + * to the config portal. Your parameters will be maintained by + * IotWebConf. This means, they will be loaded from/saved to EEPROM, + * and will appear in the config portal. + * Note the configSaved and formValidator callbacks! + * (See previous examples for more details!) + * + * Hardware setup for this example: + * - An LED is attached to LED_BUILTIN pin with setup On=LOW. + * - [Optional] A push button is attached to pin D2, the other leg of the + * button should be attached to GND. + */ + +#include + +// -- Initial name of the Thing. Used e.g. as SSID of the own Access Point. +const char thingName[] = "testThing"; + +// -- Initial password to connect to the Thing, when it creates an own Access Point. +const char wifiInitialApPassword[] = "smrtTHNG8266"; + +#define STRING_LEN 128 +#define NUMBER_LEN 32 + +// -- Configuration specific key. The value should be modified if config structure was changed. +#define CONFIG_VERSION "dem2" + +// -- When CONFIG_PIN is pulled to ground on startup, the Thing will use the initial +// password to buld an AP. (E.g. in case of lost password) +#define CONFIG_PIN D2 + +// -- Status indicator pin. +// First it will light up (kept LOW), on Wifi connection it will blink, +// when connected to the Wifi it will turn off (kept HIGH). +#define STATUS_PIN LED_BUILTIN + +// -- Callback method declarations. +void configSaved(); +boolean formValidator(); + +DNSServer dnsServer; +WebServer server(80); + +char stringParamValue[STRING_LEN]; +char intParamValue[NUMBER_LEN]; +char floatParamValue[NUMBER_LEN]; + +IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword, CONFIG_VERSION); +IotWebConfParameter stringParam = IotWebConfParameter("String param", "stringParam", stringParamValue, STRING_LEN); +IotWebConfSeparator separator1 = IotWebConfSeparator(); +IotWebConfParameter intParam = IotWebConfParameter("Int param", "intParam", intParamValue, NUMBER_LEN, "number", "1..100", NULL, "min='1' max='100' step='1'"); +// -- We can add a legend to the separator +IotWebConfSeparator separator2 = IotWebConfSeparator("Calibration factor"); +IotWebConfParameter floatParam = IotWebConfParameter("Float param", "floatParam", floatParamValue, NUMBER_LEN, "number", "e.g. 23.4", NULL, "step='0.1'"); + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println("Starting up..."); + + iotWebConf.setStatusPin(STATUS_PIN); + iotWebConf.setConfigPin(CONFIG_PIN); + iotWebConf.addParameter(&stringParam); + iotWebConf.addParameter(&separator1); + iotWebConf.addParameter(&intParam); + iotWebConf.addParameter(&separator2); + iotWebConf.addParameter(&floatParam); + iotWebConf.setConfigSavedCallback(&configSaved); + iotWebConf.setFormValidator(&formValidator); + iotWebConf.getApTimeoutParameter()->visible = true; + + // -- Initializing the configuration. + iotWebConf.init(); + + // -- Set up required URL handlers on the web server. + server.on("/", handleRoot); + server.on("/config", []{ iotWebConf.handleConfig(); }); + server.onNotFound([](){ iotWebConf.handleNotFound(); }); + + Serial.println("Ready."); +} + +void loop() +{ + // -- doLoop should be called as frequently as possible. + iotWebConf.doLoop(); +} + +/** + * Handle web requests to "/" path. + */ +void handleRoot() +{ + // -- Let IotWebConf test and handle captive portal requests. + if (iotWebConf.handleCaptivePortal()) + { + // -- Captive portal request were already served. + return; + } + String s = ""; + s += "IotWebConf 03 Custom ParametersHello world!"; + s += "
    "; + s += "
  • String param value: "; + s += stringParamValue; + s += "
  • Int param value: "; + s += atoi(intParamValue); + s += "
  • Float param value: "; + s += atof(floatParamValue); + s += "
"; + s += "Go to configure page to change values."; + s += "\n"; + + server.send(200, "text/html", s); +} + +void configSaved() +{ + Serial.println("Configuration was updated."); +} + +boolean formValidator() +{ + Serial.println("Validating form."); + boolean valid = true; + + int l = server.arg(stringParam.getId()).length(); + if (l < 3) + { + stringParam.errorMessage = "Please provide at least 3 characters for this test!"; + valid = false; + } + + return valid; +} + diff --git a/lib/IotWebConf/examples/IotWebConf04UpdateServer/IotWebConf04UpdateServer.ino b/lib/IotWebConf/examples/IotWebConf04UpdateServer/IotWebConf04UpdateServer.ino new file mode 100644 index 0000000..f6cf72a --- /dev/null +++ b/lib/IotWebConf/examples/IotWebConf04UpdateServer/IotWebConf04UpdateServer.ino @@ -0,0 +1,99 @@ +/** + * IotWebConf04UpdateServer.ino -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Example: Update Server + * Description: + * In this example we will provide a "firmware update" link in the + * config portal. + * (See ESP8266 ESP8266HTTPUpdateServer examples + * to understand UpdateServer!) + * (ESP32: HTTPUpdateServer library is ported for ESP32 in this project.) + * (See previous examples for more details!) + * + * Hardware setup for this example: + * - An LED is attached to LED_BUILTIN pin with setup On=LOW. + * - [Optional] A push button is attached to pin D2, the other leg of the + * button should be attached to GND. + */ + +#include + +// -- Initial name of the Thing. Used e.g. as SSID of the own Access Point. +const char thingName[] = "testThing"; + +// -- Initial password to connect to the Thing, when it creates an own Access Point. +const char wifiInitialApPassword[] = "smrtTHNG8266"; + +// -- Configuration specific key. The value should be modified if config structure was changed. +#define CONFIG_VERSION "dem1" + +// -- When CONFIG_PIN is pulled to ground on startup, the Thing will use the initial +// password to buld an AP. (E.g. in case of lost password) +#define CONFIG_PIN D2 + +// -- Status indicator pin. +// First it will light up (kept LOW), on Wifi connection it will blink, +// when connected to the Wifi it will turn off (kept HIGH). +#define STATUS_PIN LED_BUILTIN + +DNSServer dnsServer; +WebServer server(80); +HTTPUpdateServer httpUpdater; + +IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword, CONFIG_VERSION); + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println("Starting up..."); + + iotWebConf.setStatusPin(STATUS_PIN); + iotWebConf.setConfigPin(CONFIG_PIN); + iotWebConf.setupUpdateServer(&httpUpdater); + + // -- Initializing the configuration. + iotWebConf.init(); + + // -- Set up required URL handlers on the web server. + server.on("/", handleRoot); + server.on("/config", []{ iotWebConf.handleConfig(); }); + server.onNotFound([](){ iotWebConf.handleNotFound(); }); + + Serial.println("Ready."); +} + +void loop() +{ + // -- doLoop should be called as frequently as possible. + iotWebConf.doLoop(); +} + +/** + * Handle web requests to "/" path. + */ +void handleRoot() +{ + // -- Let IotWebConf test and handle captive portal requests. + if (iotWebConf.handleCaptivePortal()) + { + // -- Captive portal request were already served. + return; + } + String s = ""; + s += "IotWebConf 04 Update ServerHello world!"; + s += "Go to configure page to change values."; + s += "\n"; + + server.send(200, "text/html", s); +} + diff --git a/lib/IotWebConf/examples/IotWebConf05Callbacks/IotWebConf05Callbacks.ino b/lib/IotWebConf/examples/IotWebConf05Callbacks/IotWebConf05Callbacks.ino new file mode 100644 index 0000000..d4345f6 --- /dev/null +++ b/lib/IotWebConf/examples/IotWebConf05Callbacks/IotWebConf05Callbacks.ino @@ -0,0 +1,142 @@ +/** + * IotWebConf05Callbacks.ino -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Example: Callbacks + * Description: + * This example shows, what callbacks IotWebConf provides. + * (See previous examples for more details!) + * + * Hardware setup for this example: + * - An LED is attached to LED_BUILTIN pin with setup On=LOW. + * - [Optional] A push button is attached to pin D2, the other leg of the + * button should be attached to GND. + */ + +#include + +// -- Initial name of the Thing. Used e.g. as SSID of the own Access Point. +const char thingName[] = "testThing"; + +// -- Initial password to connect to the Thing, when it creates an own Access Point. +const char wifiInitialApPassword[] = "smrtTHNG8266"; + +#define STRING_LEN 128 + +// -- Configuration specific key. The value should be modified if config structure was changed. +#define CONFIG_VERSION "dem3" + +// -- When CONFIG_PIN is pulled to ground on startup, the Thing will use the initial +// password to buld an AP. (E.g. in case of lost password) +#define CONFIG_PIN D2 + +// -- Status indicator pin. +// First it will light up (kept LOW), on Wifi connection it will blink, +// when connected to the Wifi it will turn off (kept HIGH). +#define STATUS_PIN LED_BUILTIN + +// -- Callback method declarations. +void wifiConnected(); +void configSaved(); +boolean formValidator(); +void messageReceived(String &topic, String &payload); + +DNSServer dnsServer; +WebServer server(80); +HTTPUpdateServer httpUpdater; + +char stringParamValue[STRING_LEN]; + +IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword, CONFIG_VERSION); +IotWebConfParameter stringParam = IotWebConfParameter("String param", "stringParam", stringParamValue, STRING_LEN); + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println("Starting up..."); + + iotWebConf.setStatusPin(STATUS_PIN); + iotWebConf.setConfigPin(CONFIG_PIN); + iotWebConf.addParameter(&stringParam); + iotWebConf.setConfigSavedCallback(&configSaved); + iotWebConf.setFormValidator(&formValidator); + iotWebConf.setWifiConnectionCallback(&wifiConnected); + + // -- Initializing the configuration. + boolean validConfig = iotWebConf.init(); + if (!validConfig) + { + stringParamValue[0] = '\0'; + } + + // -- Set up required URL handlers on the web server. + server.on("/", handleRoot); + server.on("/config", []{ iotWebConf.handleConfig(); }); + server.onNotFound([](){ iotWebConf.handleNotFound(); }); + + Serial.println("Ready."); +} + +void loop() +{ + // -- doLoop should be called as frequently as possible. + iotWebConf.doLoop(); +} + +/** + * Handle web requests to "/" path. + */ +void handleRoot() +{ + // -- Let IotWebConf test and handle captive portal requests. + if (iotWebConf.handleCaptivePortal()) + { + // -- Captive portal request were already served. + return; + } + String s = ""; + s += "IotWebConf 05 CallbacksHello world!"; + s += "
    "; + s += "
  • String param value: "; + s += stringParamValue; + s += "
"; + s += "Go to configure page to change values."; + s += "\n"; + + server.send(200, "text/html", s); +} + +void wifiConnected() +{ + Serial.println("WiFi was connected."); +} + +void configSaved() +{ + Serial.println("Configuration was updated."); +} + +boolean formValidator() +{ + Serial.println("Validating form."); + boolean valid = true; + + int l = server.arg(stringParam.getId()).length(); + if (l < 3) + { + stringParam.errorMessage = "Please provide at least 3 characters for this test!"; + valid = false; + } + + return valid; +} + diff --git a/lib/IotWebConf/examples/IotWebConf06MqttApp/IotWebConf06MqttApp.ino b/lib/IotWebConf/examples/IotWebConf06MqttApp/IotWebConf06MqttApp.ino new file mode 100644 index 0000000..abc9767 --- /dev/null +++ b/lib/IotWebConf/examples/IotWebConf06MqttApp/IotWebConf06MqttApp.ino @@ -0,0 +1,265 @@ +/** + * IotWebConf06MqttApp.ino -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Example: MQTT Demo Application + * Description: + * All IotWebConf specific aspects of this example are described in + * previous examples, so please get familiar with IotWebConf before + * starting this example. So nothing new will be explained here, + * but a complete demo application will be build. + * It is also expected from the reader to have a basic knowledge over + * MQTT to understand this code. + * + * This example starts an MQTT client with the configured + * connection settings. + * Will post the status changes of the D2 pin in channel "/test/status". + * Receives messages appears in channel "/test/action", and writes them to serial. + * This example also provides the firmware update option. + * (See previous examples for more details!) + * + * Software setup for this example: + * This example utilizes Joel Gaehwiler's MQTT library. + * https://github.com/256dpi/arduino-mqtt + * + * Hardware setup for this example: + * - An LED is attached to LED_BUILTIN pin with setup On=LOW. + * - [Optional] A push button is attached to pin D2, the other leg of the + * button should be attached to GND. + */ + +#include +#include + +// -- Initial name of the Thing. Used e.g. as SSID of the own Access Point. +const char thingName[] = "testThing"; + +// -- Initial password to connect to the Thing, when it creates an own Access Point. +const char wifiInitialApPassword[] = "smrtTHNG8266"; + +#define STRING_LEN 128 + +// -- Configuration specific key. The value should be modified if config structure was changed. +#define CONFIG_VERSION "mqt1" + +// -- When CONFIG_PIN is pulled to ground on startup, the Thing will use the initial +// password to buld an AP. (E.g. in case of lost password) +#define CONFIG_PIN D2 + +// -- Status indicator pin. +// First it will light up (kept LOW), on Wifi connection it will blink, +// when connected to the Wifi it will turn off (kept HIGH). +#define STATUS_PIN LED_BUILTIN + +// -- Callback method declarations. +void wifiConnected(); +void configSaved(); +boolean formValidator(); +void mqttMessageReceived(String &topic, String &payload); + +DNSServer dnsServer; +WebServer server(80); +HTTPUpdateServer httpUpdater; +WiFiClient net; +MQTTClient mqttClient; + +char mqttServerValue[STRING_LEN]; +char mqttUserNameValue[STRING_LEN]; +char mqttUserPasswordValue[STRING_LEN]; + +IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword, CONFIG_VERSION); +IotWebConfParameter mqttServerParam = IotWebConfParameter("MQTT server", "mqttServer", mqttServerValue, STRING_LEN); +IotWebConfParameter mqttUserNameParam = IotWebConfParameter("MQTT user", "mqttUser", mqttUserNameValue, STRING_LEN); +IotWebConfParameter mqttUserPasswordParam = IotWebConfParameter("MQTT password", "mqttPass", mqttUserPasswordValue, STRING_LEN, "password"); + +boolean needMqttConnect = false; +boolean needReset = false; +int pinState = HIGH; +unsigned long lastReport = 0; +unsigned long lastMqttConnectionAttempt = 0; + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println("Starting up..."); + + iotWebConf.setStatusPin(STATUS_PIN); + iotWebConf.setConfigPin(CONFIG_PIN); + iotWebConf.addParameter(&mqttServerParam); + iotWebConf.addParameter(&mqttUserNameParam); + iotWebConf.addParameter(&mqttUserPasswordParam); + iotWebConf.setConfigSavedCallback(&configSaved); + iotWebConf.setFormValidator(&formValidator); + iotWebConf.setWifiConnectionCallback(&wifiConnected); + iotWebConf.setupUpdateServer(&httpUpdater); + + // -- Initializing the configuration. + boolean validConfig = iotWebConf.init(); + if (!validConfig) + { + mqttServerValue[0] = '\0'; + mqttUserNameValue[0] = '\0'; + mqttUserPasswordValue[0] = '\0'; + } + + // -- Set up required URL handlers on the web server. + server.on("/", handleRoot); + server.on("/config", []{ iotWebConf.handleConfig(); }); + server.onNotFound([](){ iotWebConf.handleNotFound(); }); + + mqttClient.begin(mqttServerValue, net); + mqttClient.onMessage(mqttMessageReceived); + + Serial.println("Ready."); +} + +void loop() +{ + // -- doLoop should be called as frequently as possible. + iotWebConf.doLoop(); + mqttClient.loop(); + + if (needMqttConnect) + { + if (connectMqtt()) + { + needMqttConnect = false; + } + } + else if ((iotWebConf.getState() == IOTWEBCONF_STATE_ONLINE) && (!mqttClient.connected())) + { + Serial.println("MQTT reconnect"); + connectMqtt(); + } + + if (needReset) + { + Serial.println("Rebooting after 1 second."); + iotWebConf.delay(1000); + ESP.restart(); + } + + unsigned long now = millis(); + if ((500 < now - lastReport) && (pinState != digitalRead(CONFIG_PIN))) + { + pinState = 1 - pinState; // invert pin state as it is changed + lastReport = now; + Serial.print("Sending on MQTT channel '/test/status' :"); + Serial.println(pinState == LOW ? "ON" : "OFF"); + mqttClient.publish("/test/status", pinState == LOW ? "ON" : "OFF"); + } +} + +/** + * Handle web requests to "/" path. + */ +void handleRoot() +{ + // -- Let IotWebConf test and handle captive portal requests. + if (iotWebConf.handleCaptivePortal()) + { + // -- Captive portal request were already served. + return; + } + String s = ""; + s += "IotWebConf 06 MQTT AppMQTT App demo"; + s += "
    "; + s += "
  • MQTT server: "; + s += mqttServerValue; + s += "
"; + s += "Go to configure page to change values."; + s += "\n"; + + server.send(200, "text/html", s); +} + +void wifiConnected() +{ + needMqttConnect = true; +} + +void configSaved() +{ + Serial.println("Configuration was updated."); + needReset = true; +} + +boolean formValidator() +{ + Serial.println("Validating form."); + boolean valid = true; + + int l = server.arg(mqttServerParam.getId()).length(); + if (l < 3) + { + mqttServerParam.errorMessage = "Please provide at least 3 characters!"; + valid = false; + } + + return valid; +} + +boolean connectMqtt() { + unsigned long now = millis(); + if (1000 > now - lastMqttConnectionAttempt) + { + // Do not repeat within 1 sec. + return false; + } + Serial.println("Connecting to MQTT server..."); + if (!connectMqttOptions()) { + lastMqttConnectionAttempt = now; + return false; + } + Serial.println("Connected!"); + + mqttClient.subscribe("/test/action"); + return true; +} + +/* +// -- This is an alternative MQTT connection method. +boolean connectMqtt() { + Serial.println("Connecting to MQTT server..."); + while (!connectMqttOptions()) { + iotWebConf.delay(1000); + } + Serial.println("Connected!"); + + mqttClient.subscribe("/test/action"); + return true; +} +*/ + +boolean connectMqttOptions() +{ + boolean result; + if (mqttUserPasswordValue[0] != '\0') + { + result = mqttClient.connect(iotWebConf.getThingName(), mqttUserNameValue, mqttUserPasswordValue); + } + else if (mqttUserNameValue[0] != '\0') + { + result = mqttClient.connect(iotWebConf.getThingName(), mqttUserNameValue); + } + else + { + result = mqttClient.connect(iotWebConf.getThingName()); + } + return result; +} + +void mqttMessageReceived(String &topic, String &payload) +{ + Serial.println("Incoming: " + topic + " - " + payload); +} + diff --git a/lib/IotWebConf/examples/IotWebConf07MqttRelay/IotWebConf07MqttRelay.ino b/lib/IotWebConf/examples/IotWebConf07MqttRelay/IotWebConf07MqttRelay.ino new file mode 100644 index 0000000..b360347 --- /dev/null +++ b/lib/IotWebConf/examples/IotWebConf07MqttRelay/IotWebConf07MqttRelay.ino @@ -0,0 +1,299 @@ +/** + * IotWebConf07MqttRelay.ino -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Example: MQTT Relay Demo + * Description: + * All IotWebConf specific aspects of this example are described in + * previous examples, so please get familiar with IotWebConf before + * starting this example. So nothing new will be explained here, + * but a complete demo application will be built. + * It is also expected from the reader to have a basic knowledge over + * MQTT to understand this code. + * + * This example starts an MQTT client with the configured + * connection settings. + * Will receives messages appears in channel "/devices/[thingName]/action" + * with payload ON/OFF, and reports current state in channel + * "/devices/[thingName]/status" (ON/OFF). Where the thingName can be + * configured in the portal. A relay will be switched on/off + * corresponding to the received action. The relay can be also controlled + * by the push button. + * The thing will delay actions arriving within 7 seconds. + * + * This example also provides the firmware update option. + * (See previous examples for more details!) + * + * Software setup for this example: + * This example utilizes Joel Gaehwiler's MQTT library. + * https://github.com/256dpi/arduino-mqtt + * + * Hardware setup for this example: + * - A Relay is attached to the D5 pin (On=HIGH). Note on relay pin! + * - An LED is attached to LED_BUILTIN pin with setup On=LOW. + * - A push button is attached to pin D2, the other leg of the + * button should be attached to GND. + * + * Note on relay pin + * Some people might want to use Wemos Relay Shield to test this example. + * Now Wemos Relay Shield connects the relay to pin D1. + * However, when using D1 as output, Serial communication will be blocked. + * So you will either keep on using D1 and miss the Serial monitor + * feedback, or connect your relay to another digital pin (e.g. D5). + * (You can modify your Wemos Relay Shield for that, as I show it in this + * video: https://youtu.be/GykA_7QmoXE) + */ + +#include +#include + +// -- Initial name of the Thing. Used e.g. as SSID of the own Access Point. +const char thingName[] = "testThing"; + +// -- Initial password to connect to the Thing, when it creates an own Access Point. +const char wifiInitialApPassword[] = "smrtTHNG8266"; + +#define STRING_LEN 128 + +// -- Configuration specific key. The value should be modified if config structure was changed. +#define CONFIG_VERSION "mqt2" + +// -- When BUTTON_PIN is pulled to ground on startup, the Thing will use the initial +// password to buld an AP. (E.g. in case of lost password) +#define BUTTON_PIN D2 + +// -- Status indicator pin. +// First it will light up (kept LOW), on Wifi connection it will blink, +// when connected to the Wifi it will turn off (kept HIGH). +#define STATUS_PIN LED_BUILTIN + +// -- Connected ouput pin. See "Note on relay pin"! +#define RELAY_PIN D5 + +#define MQTT_TOPIC_PREFIX "/devices/" + +// -- Ignore/limit status changes more frequent than the value below (milliseconds). +#define ACTION_FEQ_LIMIT 7000 +#define NO_ACTION -1 + +// -- Callback method declarations. +void wifiConnected(); +void configSaved(); +boolean formValidator(); +void mqttMessageReceived(String &topic, String &payload); + +DNSServer dnsServer; +WebServer server(80); +HTTPUpdateServer httpUpdater; +WiFiClient net; +MQTTClient mqttClient; + +char mqttServerValue[STRING_LEN]; + +IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword, CONFIG_VERSION); +IotWebConfParameter mqttServerParam = IotWebConfParameter("MQTT server", "mqttServer", mqttServerValue, STRING_LEN); + +boolean needMqttConnect = false; +boolean needReset = false; +unsigned long lastMqttConnectionAttempt = 0; +int needAction = NO_ACTION; +int state = LOW; +unsigned long lastAction = 0; +char mqttActionTopic[STRING_LEN]; +char mqttStatusTopic[STRING_LEN]; + +void setup() +{ + Serial.begin(115200); // See "Note on relay pin"! + Serial.println(); + Serial.println("Starting up..."); + + pinMode(RELAY_PIN, OUTPUT); + + iotWebConf.setStatusPin(STATUS_PIN); + iotWebConf.setConfigPin(BUTTON_PIN); + iotWebConf.addParameter(&mqttServerParam); + iotWebConf.setConfigSavedCallback(&configSaved); + iotWebConf.setFormValidator(&formValidator); + iotWebConf.setWifiConnectionCallback(&wifiConnected); + iotWebConf.setupUpdateServer(&httpUpdater); + + // -- Initializing the configuration. + boolean validConfig = iotWebConf.init(); + if (!validConfig) + { + mqttServerValue[0] = '\0'; + } + + // -- Set up required URL handlers on the web server. + server.on("/", handleRoot); + server.on("/config", []{ iotWebConf.handleConfig(); }); + server.onNotFound([](){ iotWebConf.handleNotFound(); }); + + // -- Prepare dynamic topic names + String temp = String(MQTT_TOPIC_PREFIX); + temp += iotWebConf.getThingName(); + temp += "/action"; + temp.toCharArray(mqttActionTopic, STRING_LEN); + temp = String(MQTT_TOPIC_PREFIX); + temp += iotWebConf.getThingName(); + temp += "/status"; + temp.toCharArray(mqttStatusTopic, STRING_LEN); + + mqttClient.begin(mqttServerValue, net); + mqttClient.onMessage(mqttMessageReceived); + + Serial.println("Ready."); +} + +void loop() +{ + // -- doLoop should be called as frequently as possible. + iotWebConf.doLoop(); + mqttClient.loop(); + + if (needMqttConnect) + { + if (connectMqtt()) + { + needMqttConnect = false; + } + } + else if ((iotWebConf.getState() == IOTWEBCONF_STATE_ONLINE) && (!mqttClient.connected())) + { + Serial.println("MQTT reconnect"); + connectMqtt(); + } + + if (needReset) + { + Serial.println("Rebooting after 1 second."); + iotWebConf.delay(1000); + ESP.restart(); + } + + unsigned long now = millis(); + + // -- Check for button push + if ((digitalRead(BUTTON_PIN) == LOW) + && ( ACTION_FEQ_LIMIT < now - lastAction)) + { + needAction = 1 - state; // -- Invert the state + } + + if ((needAction != NO_ACTION) + && ( ACTION_FEQ_LIMIT < now - lastAction)) + { + state = needAction; + digitalWrite(RELAY_PIN, state); + if (state == HIGH) + { + iotWebConf.blink(5000, 95); + } + else + { + iotWebConf.stopCustomBlink(); + } + mqttClient.publish(mqttStatusTopic, state == HIGH ? "ON" : "OFF", true, 1); + mqttClient.publish(mqttActionTopic, state == HIGH ? "ON" : "OFF", true, 1); + Serial.print("Switched "); + Serial.println(state == HIGH ? "ON" : "OFF"); + needAction = NO_ACTION; + lastAction = now; + } +} + +/** + * Handle web requests to "/" path. + */ +void handleRoot() +{ + // -- Let IotWebConf test and handle captive portal requests. + if (iotWebConf.handleCaptivePortal()) + { + // -- Captive portal request were already served. + return; + } + String s = F(""); + s += iotWebConf.getHtmlFormatProvider()->getStyle(); + s += "IotWebConf 07 MQTT Relay"; + s += iotWebConf.getThingName(); + s += "
State: "; + s += (state == HIGH ? "ON" : "OFF"); + s += "
"; + s += ""; + s += "
Go to configure page to change values.
"; + s += "\n"; + + server.send(200, "text/html", s); +} + +void wifiConnected() +{ + needMqttConnect = true; +} + +void configSaved() +{ + Serial.println("Configuration was updated."); + needReset = true; +} + +boolean formValidator() +{ + Serial.println("Validating form."); + boolean valid = true; + + int l = server.arg(mqttServerParam.getId()).length(); + if (l < 3) + { + mqttServerParam.errorMessage = "Please provide at least 3 characters!"; + valid = false; + } + + return valid; +} + +boolean connectMqtt() { + unsigned long now = millis(); + if (1000 > now - lastMqttConnectionAttempt) + { + // Do not repeat within 1 sec. + return false; + } + Serial.println("Connecting to MQTT server..."); + if (!mqttClient.connect(iotWebConf.getThingName())) { + lastMqttConnectionAttempt = now; + return false; + } + Serial.println("Connected!"); + + mqttClient.subscribe(mqttActionTopic); + mqttClient.publish(mqttStatusTopic, state == HIGH ? "ON" : "OFF", true, 1); + mqttClient.publish(mqttActionTopic, state == HIGH ? "ON" : "OFF", true, 1); + + return true; +} + +void mqttMessageReceived(String &topic, String &payload) +{ + Serial.println("Incoming: " + topic + " - " + payload); + + if (topic.endsWith("action")) + { + needAction = payload.equals("ON") ? HIGH : LOW; + if (needAction == state) + { + needAction = NO_ACTION; + } + } +} + diff --git a/lib/IotWebConf/examples/IotWebConf08WebRelay/IotWebConf08WebRelay.ino b/lib/IotWebConf/examples/IotWebConf08WebRelay/IotWebConf08WebRelay.ino new file mode 100644 index 0000000..b980d0a --- /dev/null +++ b/lib/IotWebConf/examples/IotWebConf08WebRelay/IotWebConf08WebRelay.ino @@ -0,0 +1,196 @@ +/** + * IotWebConf08WebRelay.ino -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Example: Web Relay Demo + * Description: + * All IotWebConf specific aspects of this example are described in + * previous examples, so please get familiar with IotWebConf before + * starting this example. So nothing new will be explained here, + * but a complete demo application will be built. + * It is also expected from the reader to have a basic knowledge over + * HTML to understand this code. + * + * This example stets up a web page, where switch action can take place. + * The repay can be also altered with the push button. + * The thing will delay actions arriving within 10 seconds. + * + * This example also provides the firmware update option. + * (See previous examples for more details!) + * + * Hardware setup for this example: + * - A Relay is attached to the D1 pin (On=HIGH) + * - An LED is attached to LED_BUILTIN pin with setup On=LOW. + * - A push button is attached to pin D2, the other leg of the + * button should be attached to GND. + */ + +#include + +// -- Initial name of the Thing. Used e.g. as SSID of the own Access Point. +const char thingName[] = "testThing"; + +// -- Initial password to connect to the Thing, when it creates an own Access Point. +const char wifiInitialApPassword[] = "smrtTHNG8266"; + +#define STRING_LEN 128 + +// -- Configuration specific key. The value should be modified if config structure was changed. +#define CONFIG_VERSION "web1" + +// -- When BUTTON_PIN is pulled to ground on startup, the Thing will use the initial +// password to buld an AP. (E.g. in case of lost password) +#define BUTTON_PIN D2 + +// -- Status indicator pin. +// First it will light up (kept LOW), on Wifi connection it will blink, +// when connected to the Wifi it will turn off (kept HIGH). +#define STATUS_PIN LED_BUILTIN + +// -- Connected ouput pin. +#define RELAY_PIN D1 + +#define ACTION_FEQ_LIMIT 10000 +#define NO_ACTION -1 + +// -- Callback method declarations. +void configSaved(); + +DNSServer dnsServer; +WebServer server(80); +HTTPUpdateServer httpUpdater; + +IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword, CONFIG_VERSION); + +boolean needReset = false; +int needAction = NO_ACTION; +int state = LOW; +unsigned long lastAction = 0; + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println("Starting up..."); + + pinMode(RELAY_PIN, OUTPUT); + + iotWebConf.setStatusPin(STATUS_PIN); + iotWebConf.setConfigPin(BUTTON_PIN); + iotWebConf.setConfigSavedCallback(&configSaved); + iotWebConf.setupUpdateServer(&httpUpdater); + + // -- Initializing the configuration. + iotWebConf.init(); + + // -- Set up required URL handlers on the web server. + server.on("/", handleRoot); + server.on("/config", []{ iotWebConf.handleConfig(); }); + server.onNotFound([](){ iotWebConf.handleNotFound(); }); + + Serial.println("Ready."); +} + +void loop() +{ + // -- doLoop should be called as frequently as possible. + iotWebConf.doLoop(); + + if (needReset) + { + Serial.println("Rebooting after 1 second."); + iotWebConf.delay(1000); + ESP.restart(); + } + + unsigned long now = millis(); + + // -- Check for button push + if ((digitalRead(BUTTON_PIN) == LOW) + && (ACTION_FEQ_LIMIT < now - lastAction)) + { + needAction = 1 - state; // -- Invert the state + } + + applyAction(now); +} + +void applyAction(unsigned long now) +{ + if ((needAction != NO_ACTION) + && (ACTION_FEQ_LIMIT < now - lastAction)) + { + state = needAction; + digitalWrite(RELAY_PIN, state); + if (state == HIGH) + { + iotWebConf.blink(5000, 95); + } + else + { + iotWebConf.stopCustomBlink(); + } + Serial.print("Switched "); + Serial.println(state == HIGH ? "ON" : "OFF"); + needAction = NO_ACTION; + lastAction = now; + } +} + +/** + * Handle web requests to "/" path. + */ +void handleRoot() +{ + // -- Let IotWebConf test and handle captive portal requests. + if (iotWebConf.handleCaptivePortal()) + { + // -- Captive portal request were already served. + return; + } + + if (server.hasArg("action")) + { + String action = server.arg("action"); + if (action.equals("on")) + { + needAction = HIGH; + } + else if (action.equals("off")) + { + needAction = LOW; + } + applyAction(millis()); + } + + String s = F(""); + s += iotWebConf.getHtmlFormatProvider()->getStyle(); + s += "IotWebConf 08 Web Relay"; + s += iotWebConf.getThingName(); + s += "
State: "; + s += (state == HIGH ? "ON" : "OFF"); + s += "
"; + s += "
"; + s += ""; + s += ""; + s += ""; + s += "
"; + s += "
Go to configure page to change values.
"; + s += "\n"; + + server.send(200, "text/html", s); +} + +void configSaved() +{ + Serial.println("Configuration was updated."); + needReset = true; +} diff --git a/lib/IotWebConf/examples/IotWebConf09CustomConnection/IotWebConf09CustomConnection.ino b/lib/IotWebConf/examples/IotWebConf09CustomConnection/IotWebConf09CustomConnection.ino new file mode 100644 index 0000000..20f0f94 --- /dev/null +++ b/lib/IotWebConf/examples/IotWebConf09CustomConnection/IotWebConf09CustomConnection.ino @@ -0,0 +1,179 @@ +/** + * IotWebConf09CustomConnection.ino -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Example: Custom connection + * Description: + * This example is for advanced users only! + * In this example custom connection handler methods are defined + * to override the default connecting behavior. + * Also, three custom parameters are introduced, that are used + * at the connection. + * (See previous examples for more details!) + * + * Hardware setup for this example: + * - An LED is attached to LED_BUILTIN pin with setup On=LOW. + * - [Optional] A push button is attached to pin D2, the other leg of the + * button should be attached to GND. + */ + +#include + +// -- Initial name of the Thing. Used e.g. as SSID of the own Access Point. +const char thingName[] = "testThing"; + +// -- Initial password to connect to the Thing, when it creates an own Access Point. +const char wifiInitialApPassword[] = "smrtTHNG8266"; + +#define STRING_LEN 128 +#define NUMBER_LEN 32 + +// -- Configuration specific key. The value should be modified if config structure was changed. +#define CONFIG_VERSION "dem9" + +// -- When CONFIG_PIN is pulled to ground on startup, the Thing will use the initial +// password to buld an AP. (E.g. in case of lost password) +#define CONFIG_PIN 2 + +// -- Status indicator pin. +// First it will light up (kept LOW), on Wifi connection it will blink, +// when connected to the Wifi it will turn off (kept HIGH). +#define STATUS_PIN LED_BUILTIN + +// -- Callback method declarations. +void configSaved(); +boolean formValidator(); +boolean connectAp(const char* apName, const char* password); +void connectWifi(const char* ssid, const char* password); + +DNSServer dnsServer; +WebServer server(80); + +char ipAddressValue[STRING_LEN]; +char gatewayValue[STRING_LEN]; +char netmaskValue[STRING_LEN]; + +IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword, CONFIG_VERSION); +IotWebConfParameter ipAddressParam = IotWebConfParameter("IP address", "ipAddress", ipAddressValue, STRING_LEN, "text", NULL, "192.168.3.222"); +IotWebConfParameter gatewayParam = IotWebConfParameter("Gateway", "gateway", gatewayValue, STRING_LEN, "text", NULL, "192.168.3.0"); +IotWebConfParameter netmaskParam = IotWebConfParameter("Subnet mask", "netmask", netmaskValue, STRING_LEN, "text", NULL, "255.255.255.0"); + +IPAddress ipAddress; +IPAddress gateway; +IPAddress netmask; + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println("Starting up..."); + + iotWebConf.setStatusPin(STATUS_PIN); + iotWebConf.setConfigPin(CONFIG_PIN); + iotWebConf.addParameter(&ipAddressParam); + iotWebConf.addParameter(&gatewayParam); + iotWebConf.addParameter(&netmaskParam); + iotWebConf.setConfigSavedCallback(&configSaved); + iotWebConf.setFormValidator(&formValidator); + iotWebConf.setApConnectionHandler(&connectAp); + iotWebConf.setWifiConnectionHandler(&connectWifi); + + // -- Initializing the configuration. + boolean validConfig = iotWebConf.init(); + + // -- Set up required URL handlers on the web server. + server.on("/", handleRoot); + server.on("/config", []{ iotWebConf.handleConfig(); }); + server.onNotFound([](){ iotWebConf.handleNotFound(); }); + + Serial.println("Ready."); +} + +void loop() +{ + // -- doLoop should be called as frequently as possible. + iotWebConf.doLoop(); +} + +/** + * Handle web requests to "/" path. + */ +void handleRoot() +{ + // -- Let IotWebConf test and handle captive portal requests. + if (iotWebConf.handleCaptivePortal()) + { + // -- Captive portal request were already served. + return; + } + String s = ""; + s += "IotWebConf 09 Custom ConnectionHello world!"; + s += "
    "; + s += "
  • IP address: "; + s += ipAddressValue; + s += "
"; + s += "Go to configure page to change values."; + s += "\n"; + + server.send(200, "text/html", s); +} + +void configSaved() +{ + Serial.println("Configuration was updated."); +} + +boolean formValidator() +{ + Serial.println("Validating form."); + boolean valid = true; + + if (!ipAddress.fromString(server.arg(ipAddressParam.getId()))) + { + ipAddressParam.errorMessage = "Please provide a valid IP address!"; + valid = false; + } + if (!netmask.fromString(server.arg(netmaskParam.getId()))) + { + netmaskParam.errorMessage = "Please provide a valid netmask!"; + valid = false; + } + if (!gateway.fromString(server.arg(gatewayParam.getId()))) + { + gatewayParam.errorMessage = "Please provide a valid gateway address!"; + valid = false; + } + + return valid; +} + +boolean connectAp(const char* apName, const char* password) +{ + // -- Custom AP settings + return WiFi.softAP(apName, password, 4); +} +void connectWifi(const char* ssid, const char* password) +{ + ipAddress.fromString(String(ipAddressValue)); + netmask.fromString(String(netmaskValue)); + gateway.fromString(String(gatewayValue)); + + if (!WiFi.config(ipAddress, gateway, netmask)) { + Serial.println("STA Failed to configure"); + } + Serial.print("ip: "); + Serial.println(ipAddress); + Serial.print("gw: "); + Serial.println(gateway); + Serial.print("net: "); + Serial.println(netmask); + WiFi.begin(ssid, password); +} diff --git a/lib/IotWebConf/examples/IotWebConf10CustomHtml/IotWebConf10CustomHtml.ino b/lib/IotWebConf/examples/IotWebConf10CustomHtml/IotWebConf10CustomHtml.ino new file mode 100644 index 0000000..6600787 --- /dev/null +++ b/lib/IotWebConf/examples/IotWebConf10CustomHtml/IotWebConf10CustomHtml.ino @@ -0,0 +1,116 @@ +/** + * IotWebConf01Minimal.ino -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Example: Custom HTML + * Description: + * This example demostrates how to override the default look and feel of the + * config portal. + * + * The first customalization is to add a logo to the config page. + * + * The second customalization is a special javascript, that detects all + * password fields, and adds a small button after each of them with a + * lock on it. By pressing the lock the password became visible/hidden. + */ + +#include + +// -- Initial name of the Thing. Used e.g. as SSID of the own Access Point. +const char thingName[] = "testThing"; + +// -- Initial password to connect to the Thing, when it creates an own Access Point. +const char wifiInitialApPassword[] = "smrtTHNG8266"; + +DNSServer dnsServer; +WebServer server(80); + +IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword); + +// -- Javascript block will be added to the header. +const char CUSTOMHTML_SCRIPT_INNER[] PROGMEM = "\n\ +document.addEventListener('DOMContentLoaded', function(event) {\n\ + let elements = document.querySelectorAll('input[type=\"password\"]');\n\ + for (let p of elements) {\n\ + let btn = document.createElement('INPUT'); btn.type = 'button'; btn.value = '🔓'; btn.style.width = 'auto'; p.style.width = '83%'; p.parentNode.insertBefore(btn,p.nextSibling);\n\ + btn.onclick = function() { if (p.type === 'password') { p.type = 'text'; btn.value = '🔒'; } else { p.type = 'password'; btn.value = '🔓'; } }\n\ + };\n\ +});\n"; +// -- HTML element will be added inside the body element. +const char CUSTOMHTML_BODY_INNER[] PROGMEM = "
\n"; + +// -- This is an OOP technique to override behaviour of the existing +// IotWebConfHtmlFormatProvider. Here two method are overriden from +// the original class. See IotWebConf.h for all potentially overridable +// methods of IotWebConfHtmlFormatProvider . +class CustomHtmlFormatProvider : public IotWebConfHtmlFormatProvider +{ +protected: + String getScriptInner() override + { + return + IotWebConfHtmlFormatProvider::getScriptInner() + + String(FPSTR(CUSTOMHTML_SCRIPT_INNER)); + } + String getBodyInner() override + { + return + String(FPSTR(CUSTOMHTML_BODY_INNER)) + + IotWebConfHtmlFormatProvider::getBodyInner(); + } +}; +// -- An instance must be created from the class defined above. +CustomHtmlFormatProvider customHtmlFormatProvider; + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println("Starting up..."); + + // -- Applying the new HTML format to IotWebConf. + iotWebConf.setHtmlFormatProvider(&customHtmlFormatProvider); + iotWebConf.init(); + + // -- Set up required URL handlers on the web server. + server.on("/", handleRoot); + server.on("/config", []{ iotWebConf.handleConfig(); }); + server.onNotFound([](){ iotWebConf.handleNotFound(); }); + + Serial.println("Ready."); +} + +void loop() +{ + // -- doLoop should be called as frequently as possible. + iotWebConf.doLoop(); +} + +/** + * Handle web requests to "/" path. + */ +void handleRoot() +{ + // -- Let IotWebConf test and handle captive portal requests. + if (iotWebConf.handleCaptivePortal()) + { + // -- Captive portal request were already served. + return; + } + String s = ""; + s += "IotWebConf 10 Custom HTML"; + s += FPSTR(CUSTOMHTML_BODY_INNER); + s += "Go to configure page to change settings."; + s += "\n"; + + server.send(200, "text/html; charset=UTF-8", s); +} + diff --git a/lib/IotWebConf/keywords.txt b/lib/IotWebConf/keywords.txt new file mode 100644 index 0000000..822f28c --- /dev/null +++ b/lib/IotWebConf/keywords.txt @@ -0,0 +1,43 @@ +# Datatypes (KEYWORD1) +# Methods and Functions (KEYWORD2) +# Constants (LITERAL1) + +IotWebConfParameter KEYWORD1 +label KEYWORD3 +id KEYWORD3 +valueBuffer KEYWORD3 +length KEYWORD3 +type KEYWORD3 +placeholder KEYWORD3 +defaultValue KEYWORD3 +customHtml KEYWORD3 +visible KEYWORD3 + +IotWebConfSeparator KEYWORD1 + +IotWebConf KEYWORD1 +setConfigPin KEYWORD2 +setStatusPin KEYWORD2 +setupUpdateServer KEYWORD2 +init KEYWORD2 +doLoop KEYWORD2 +handleCaptivePortal KEYWORD2 +handleConfig KEYWORD2 +handleNotFound KEYWORD2 +setWifiConnectionCallback KEYWORD2 +setConfigSavedCallback KEYWORD2 +setFormValidator KEYWORD2 +addParameter KEYWORD2 +getThingName KEYWORD2 +delay KEYWORD2 +setWifiConnectionTimeoutMs KEYWORD2 +blink KEYWORD2 +getState KEYWORD2 +setApTimeoutMs KEYWORD2 +getApTimeoutMs KEYWORD2 +getThingNameParameter KEYWORD2 +getApPasswordParameter KEYWORD2 +getWifiSsidParameter KEYWORD2 +getWifiPasswordParameter KEYWORD2 +getApTimeoutParameter KEYWORD2 +configSave KEYWORD2 diff --git a/lib/IotWebConf/library.properties b/lib/IotWebConf/library.properties new file mode 100644 index 0000000..d68fcbc --- /dev/null +++ b/lib/IotWebConf/library.properties @@ -0,0 +1,10 @@ +name=IotWebConf +version=2.3.0 +author=Balazs Kelemen +maintainer=Balazs Kelemen +sentence=ESP8266/ESP32 non-blocking WiFi/AP web configuration. +paragraph=IotWebConf will start up in AP (access point) mode, and provide a config portal for entering WiFi connection and other user-settings. The configuration is persisted in EEPROM. The config portal will stay available after WiFi connection was made. A WiFiManager alternative. +category=Communication +url=https://github.com/prampec/IotWebConf +architectures=esp8266,esp32 +includes=IotWebConf.h diff --git a/lib/IotWebConf/src/IotWebConf.cpp b/lib/IotWebConf/src/IotWebConf.cpp new file mode 100644 index 0000000..8780095 --- /dev/null +++ b/lib/IotWebConf/src/IotWebConf.cpp @@ -0,0 +1,1278 @@ +/** + * IotWebConf.cpp -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +#include + +#include "IotWebConf.h" + +#ifdef IOTWEBCONF_CONFIG_USE_MDNS +# ifdef ESP8266 +# include +# elif defined(ESP32) +# include +# endif +#endif + +#define IOTWEBCONF_STATUS_ENABLED (this->_statusPin >= 0) + +IotWebConfParameter::IotWebConfParameter() +{ +} +IotWebConfParameter::IotWebConfParameter( + const char* label, const char* id, char* valueBuffer, int length, + const char* type, const char* placeholder, const char* defaultValue, + const char* customHtml, boolean visible) +{ + this->label = label; + this->_id = id; + this->valueBuffer = valueBuffer; + this->_length = length; + this->type = type; + this->placeholder = placeholder; + this->defaultValue = defaultValue; + this->customHtml = customHtml; + this->visible = visible; +} +IotWebConfParameter::IotWebConfParameter( + const char* id, char* valueBuffer, int length, const char* customHtml, + const char* type) +{ + this->label = NULL; + this->_id = id; + this->valueBuffer = valueBuffer; + this->_length = length; + this->type = type; + this->customHtml = customHtml; + this->visible = true; + this->errorMessage = NULL; +} + +IotWebConfSeparator::IotWebConfSeparator() + : IotWebConfParameter(NULL, NULL, NULL, 0, NULL, NULL, NULL, NULL, true) +{ +} + +IotWebConfSeparator::IotWebConfSeparator(const char* label) + : IotWebConfParameter(label, NULL, NULL, 0, NULL, NULL, NULL, NULL, true) +{ +} + +//////////////////////////////////////////////////////////////// + +#ifndef USE_ESPASYNCWEBSERVER +IotWebConf::IotWebConf( + const char* defaultThingName, DNSServer* dnsServer, WebServer* server, + const char* initialApPassword, const char* configVersion) +#else +IotWebConf::IotWebConf( + const char* defaultThingName, DNSServer* dnsServer, AsyncWebServer* server, + const char* initialApPassword, const char* configVersion) +#endif +{ + strncpy(this->_thingName, defaultThingName, IOTWEBCONF_WORD_LEN); + this->_dnsServer = dnsServer; + this->_server = server; + this->_initialApPassword = initialApPassword; + this->_configVersion = configVersion; + itoa(this->_apTimeoutMs / 1000, this->_apTimeoutStr, 10); + + this->_thingNameParameter = IotWebConfParameter("Thing name", "iwcThingName", this->_thingName, IOTWEBCONF_WORD_LEN); + this->_apPasswordParameter = IotWebConfParameter("AP password", "iwcApPassword", this->_apPassword, IOTWEBCONF_WORD_LEN, "password"); + this->_wifiSsidParameter = IotWebConfParameter("WiFi SSID", "iwcWifiSsid", this->_wifiSsid, IOTWEBCONF_WORD_LEN); + this->_wifiPasswordParameter = IotWebConfParameter("WiFi password", "iwcWifiPassword", this->_wifiPassword, IOTWEBCONF_WORD_LEN, "password"); + this->_apTimeoutParameter = IotWebConfParameter("Startup delay (seconds)", "iwcApTimeout", this->_apTimeoutStr, IOTWEBCONF_WORD_LEN, "number", NULL, NULL, "min='1' max='600'", false); + this->addParameter(&this->_thingNameParameter); + this->addParameter(&this->_apPasswordParameter); + this->addParameter(&this->_wifiSsidParameter); + this->addParameter(&this->_wifiPasswordParameter); + this->addParameter(&this->_apTimeoutParameter); +} + +char* IotWebConf::getThingName() +{ + return this->_thingName; +} + +void IotWebConf::setConfigPin(int configPin) +{ + this->_configPin = configPin; +} + +void IotWebConf::setStatusPin(int statusPin) +{ + this->_statusPin = statusPin; +} + +#ifndef USE_ESPASYNCWEBSERVER +void IotWebConf::setupUpdateServer( + HTTPUpdateServer* updateServer, const char* updatePath) +{ + this->_updateServer = updateServer; + this->_updatePath = updatePath; +} +#endif + +boolean IotWebConf::init() +{ + // -- Setup pins. + if (this->_configPin >= 0) + { + pinMode(this->_configPin, INPUT_PULLUP); + this->_forceDefaultPassword = (digitalRead(this->_configPin) == LOW); + } + if (IOTWEBCONF_STATUS_ENABLED) + { + pinMode(this->_statusPin, OUTPUT); + digitalWrite(this->_statusPin, IOTWEBCONF_STATUS_ON); + } + + // -- Load configuration from EEPROM. + this->configInit(); + boolean validConfig = this->configLoad(); + if (!validConfig) + { + // -- No config + this->_apPassword[0] = '\0'; + this->_wifiSsid[0] = '\0'; + this->_wifiPassword[0] = '\0'; + this->_apTimeoutMs = IOTWEBCONF_DEFAULT_AP_MODE_TIMEOUT_MS; + } + else + { + this->_apTimeoutMs = atoi(this->_apTimeoutStr) * 1000; + } + + // -- Setup mdns +#ifdef ESP8266 + WiFi.hostname(this->_thingName); +#elif defined(ESP32) + WiFi.setHostname(this->_thingName); +#endif +#ifdef IOTWEBCONF_CONFIG_USE_MDNS + MDNS.begin(this->_thingName); + MDNS.addService("http", "tcp", 80); +#endif + + return validConfig; +} + +////////////////////////////////////////////////////////////////// + +bool IotWebConf::addParameter(IotWebConfParameter* parameter) +{ +/* +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("Adding parameter '"); + Serial.print(parameter->getId()); + Serial.println("'"); +#endif +*/ + if (this->_firstParameter == NULL) + { + this->_firstParameter = parameter; +// IOTWEBCONF_DEBUG_LINE(F("Adding as first")); + return true; + } + IotWebConfParameter* current = this->_firstParameter; + while (current->_nextParameter != NULL) + { + current = current->_nextParameter; + } + + current->_nextParameter = parameter; + return true; +} + +void IotWebConf::configInit() +{ + int size = 0; + IotWebConfParameter* current = this->_firstParameter; + while (current != NULL) + { + size += current->getLength(); + current = current->_nextParameter; + } +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("Config size: "); + Serial.println(size); +#endif + + EEPROM.begin( + IOTWEBCONF_CONFIG_START + IOTWEBCONF_CONFIG_VESION_LENGTH + size); +} + +/** + * Load the configuration from the eeprom. + */ +boolean IotWebConf::configLoad() +{ + if (this->configTestVersion()) + { + IotWebConfParameter* current = this->_firstParameter; + int start = IOTWEBCONF_CONFIG_START + IOTWEBCONF_CONFIG_VESION_LENGTH; + while (current != NULL) + { + if (current->getId() != NULL) + { + this->readEepromValue(start, current->valueBuffer, current->getLength()); +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + const char* defaultMarker = ""; +#endif + if ((strlen(current->valueBuffer) == 0) && (current->defaultValue != NULL)) + { + strncpy(current->valueBuffer, current->defaultValue, current->getLength()); +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + defaultMarker = " (using default)"; +#endif + } +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("Loaded config '"); + Serial.print(current->getId()); + Serial.print("'= "); +# ifdef IOTWEBCONF_DEBUG_PWD_TO_SERIAL + Serial.print("'"); + Serial.print(current->valueBuffer); + Serial.print("'"); + Serial.println(defaultMarker); +# else + if (strcmp("password", current->type) == 0) + { + Serial.print(F("")); + Serial.println(defaultMarker); + } + else + { + Serial.print("'"); + Serial.print(current->valueBuffer); + Serial.print("'"); + Serial.println(defaultMarker); + } +# endif +#endif + + start += current->getLength(); + } + current = current->_nextParameter; + } + return true; + } + else + { + IOTWEBCONF_DEBUG_LINE(F("Wrong config version.")); + return false; + } +} + +void IotWebConf::configSave() +{ + this->configSaveConfigVersion(); + IotWebConfParameter* current = this->_firstParameter; + int start = IOTWEBCONF_CONFIG_START + IOTWEBCONF_CONFIG_VESION_LENGTH; + while (current != NULL) + { + if (current->getId() != NULL) + { +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("Saving config '"); + Serial.print(current->getId()); + Serial.print("'= "); +# ifdef IOTWEBCONF_DEBUG_PWD_TO_SERIAL + Serial.print("'"); + Serial.print(current->valueBuffer); + Serial.println("'"); +# else + if (strcmp("password", current->type) == 0) + { + Serial.print(F("")); + } + else + { + Serial.print("'"); + Serial.print(current->valueBuffer); + Serial.println("'"); + } +# endif +#endif + + this->writeEepromValue(start, current->valueBuffer, current->getLength()); + start += current->getLength(); + } + current = current->_nextParameter; + } + EEPROM.commit(); + + this->_apTimeoutMs = atoi(this->_apTimeoutStr) * 1000; + + if (this->_configSavedCallback != NULL) + { + this->_configSavedCallback(); + } +} + +void IotWebConf::readEepromValue(int start, char* valueBuffer, int length) +{ + for (int t = 0; t < length; t++) + { + *((char*)valueBuffer + t) = EEPROM.read(start + t); + } +} +void IotWebConf::writeEepromValue(int start, char* valueBuffer, int length) +{ + for (int t = 0; t < length; t++) + { + EEPROM.write(start + t, *((char*)valueBuffer + t)); + } +} + +boolean IotWebConf::configTestVersion() +{ + for (byte t = 0; t < IOTWEBCONF_CONFIG_VESION_LENGTH; t++) + { + if (EEPROM.read(IOTWEBCONF_CONFIG_START + t) != this->_configVersion[t]) + { + return false; + } + } + return true; +} + +void IotWebConf::configSaveConfigVersion() +{ + for (byte t = 0; t < IOTWEBCONF_CONFIG_VESION_LENGTH; t++) + { + EEPROM.write(IOTWEBCONF_CONFIG_START + t, this->_configVersion[t]); + } +} + +void IotWebConf::setWifiConnectionCallback(std::function func) +{ + this->_wifiConnectionCallback = func; +} + +void IotWebConf::setConfigSavedCallback(std::function func) +{ + this->_configSavedCallback = func; +} + +void IotWebConf::setFormValidator(std::function func) +{ + this->_formValidator = func; +} + +void IotWebConf::setWifiConnectionTimeoutMs(unsigned long millis) +{ + this->_wifiConnectionTimeoutMs = millis; +} + +//////////////////////////////////////////////////////////////////////////////// + +#ifndef USE_ESPASYNCWEBSERVER +void IotWebConf::handleConfig() +#else +void IotWebConf::handleConfig(AsyncWebServerRequest *request) +#endif +{ + if (this->_state == IOTWEBCONF_STATE_ONLINE) + { + // -- Authenticate +#ifndef USE_ESPASYNCWEBSERVER + if (!this->_server->authenticate( +#else + if (!request->authenticate( +#endif + IOTWEBCONF_ADMIN_USER_NAME, this->_apPassword)) + { + IOTWEBCONF_DEBUG_LINE(F("Requesting authentication.")); +#ifndef USE_ESPASYNCWEBSERVER + this->_server->requestAuthentication(); +#else + request->requestAuthentication(); +#endif + return; + } + } + +#ifndef USE_ESPASYNCWEBSERVER + if (!this->_server->hasArg("iotSave") || !this->validateForm()) +#else + if (!request->hasArg("iotSave") || !this->validateForm(request)) +#endif + { + // -- Display config portal + IOTWEBCONF_DEBUG_LINE(F("Configuration page requested.")); + String page = htmlFormatProvider->getHead(); + page.replace("{v}", "Config ESP"); + page += htmlFormatProvider->getScript(); + page += htmlFormatProvider->getStyle(); + page += htmlFormatProvider->getHeadExtension(); + page += htmlFormatProvider->getHeadEnd(); + + page += htmlFormatProvider->getFormStart(); + char parLength[5]; + // -- Add parameters to the form + IotWebConfParameter* current = this->_firstParameter; + while (current != NULL) + { + if (current->getId() == NULL) + { +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.println("Rendering separator"); +#endif + page += "
"; + if (current->label != NULL) + { + page += ""; + page += current->label; + page += ""; + } + } + else if (current->visible) + { +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("Rendering '"); + Serial.print(current->getId()); + Serial.print("' with value: "); +# ifdef IOTWEBCONF_DEBUG_PWD_TO_SERIAL + Serial.println(current->valueBuffer); +# else + if (strcmp("password", current->type) == 0) + { + Serial.println(F("")); + } + else + { + Serial.println(current->valueBuffer); + } +# endif +#endif + + String pitem; + if (current->label != NULL) + { + pitem = htmlFormatProvider->getFormParam(current->type); + pitem.replace("{b}", current->label); + pitem.replace("{t}", current->type); + pitem.replace("{i}", current->getId()); + pitem.replace("{p}", current->placeholder == NULL ? "" : current->placeholder); + snprintf(parLength, 5, "%d", current->getLength()); + pitem.replace("{l}", parLength); + if (strcmp("password", current->type) == 0) + { + // -- Value of password is not rendered + pitem.replace("{v}", ""); + } +#ifndef USE_ESPASYNCWEBSERVER + else if (this->_server->hasArg(current->getId())) +#else + else if (request->hasArg(current->getId())) +#endif + { + // -- Value from previous submit +#ifndef USE_ESPASYNCWEBSERVER + pitem.replace("{v}", this->_server->arg(current->getId())); +#else + pitem.replace("{v}", request->arg(current->getId())); +#endif + } + else + { + // -- Value from config + pitem.replace("{v}", current->valueBuffer); + } + pitem.replace( + "{c}", current->customHtml == NULL ? "" : current->customHtml); + pitem.replace( + "{e}", + current->errorMessage == NULL ? "" : current->errorMessage); + pitem.replace( + "{s}", + current->errorMessage == NULL ? "" : "de"); // Div style class. + } + else + { + pitem = current->customHtml; + } + + page += pitem; + } + current = current->_nextParameter; + } + + page += htmlFormatProvider->getFormEnd(); + + if (this->_updatePath != NULL) + { + String pitem = htmlFormatProvider->getUpdate(); + pitem.replace("{u}", this->_updatePath); + page += pitem; + } + + // -- Fill config version string; + { + String pitem = htmlFormatProvider->getConfigVer(); + pitem.replace("{v}", this->_configVersion); + page += pitem; + } + + page += htmlFormatProvider->getEnd(); + +#ifndef USE_ESPASYNCWEBSERVER + this->_server->sendHeader("Content-Length", String(page.length())); + this->_server->send(200, "text/html; charset=UTF-8", page); +#else + AsyncWebServerResponse *response = request->beginResponse(200, "text/html; charset=UTF-8", page); + response->addHeader("Content-Length", String(page.length())); + request->send(response); +#endif + } + else + { + // -- Save config + IOTWEBCONF_DEBUG_LINE(F("Updating configuration")); + char temp[IOTWEBCONF_WORD_LEN]; + + IotWebConfParameter* current = this->_firstParameter; + while (current != NULL) + { + if ((current->getId() != NULL) && (current->visible)) + { + if ((strcmp("password", current->type) == 0) && + (current->getLength() <= IOTWEBCONF_WORD_LEN)) + { + // TODO: Passwords longer than IOTWEBCONF_WORD_LEN not supported. +#ifndef USE_ESPASYNCWEBSERVER + this->readParamValue(current->getId(), temp, current->getLength()); +#else + this->readParamValue(request, current->getId(), temp, current->getLength()); +#endif + if (temp[0] != '\0') + { + // -- Value was set. + strncpy(current->valueBuffer, temp, current->getLength()); +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print(current->getId()); + Serial.println(" was set"); +#endif + } + else + { +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print(current->getId()); + Serial.println(" was not changed"); +#endif + } + } + else + { +#ifndef USE_ESPASYNCWEBSERVER + this->readParamValue( + current->getId(), current->valueBuffer, current->getLength()); +#else + this->readParamValue( + request, current->getId(), current->valueBuffer, current->getLength()); +#endif +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print(current->getId()); + Serial.print("='"); +# ifdef IOTWEBCONF_DEBUG_PWD_TO_SERIAL + Serial.print(current->valueBuffer); +# else + if (strcmp("password", current->type) == 0) + { + Serial.print(F("")); + } + else + { + Serial.print(current->valueBuffer); + } +# endif + Serial.print(current->valueBuffer); + Serial.println("'"); +#endif + } + } + current = current->_nextParameter; + } + + this->configSave(); + + String page = htmlFormatProvider->getHead(); + page.replace("{v}", "Config ESP"); + page += htmlFormatProvider->getScript(); + page += htmlFormatProvider->getStyle(); +// page += _customHeadElement; + page += htmlFormatProvider->getHeadExtension(); + page += htmlFormatProvider->getHeadEnd(); + page += "Configuration saved. "; + if (this->_apPassword[0] == '\0') + { + page += F("You must change the default AP password to continue. Return " + "to configuration page."); + } + else if (this->_wifiSsid[0] == '\0') + { + page += F("You must provide the local wifi settings to continue. Return " + "to configuration page."); + } + else if (this->_state == IOTWEBCONF_STATE_NOT_CONFIGURED) + { + page += F("Please disconnect from WiFi AP to continue!"); + } + else + { + page += F("Return to home page."); + } + page += htmlFormatProvider->getHeadEnd(); + +#ifndef USE_ESPASYNCWEBSERVER + this->_server->sendHeader("Content-Length", String(page.length())); + this->_server->send(200, "text/html; charset=UTF-8", page); +#else + AsyncWebServerResponse *response = request->beginResponse(200, "text/html; charset=UTF-8", page); + response->addHeader("Content-Length", String(page.length())); + request->send(response); +#endif + } +} + +#ifndef USE_ESPASYNCWEBSERVER +void IotWebConf::readParamValue( + const char* paramName, char* target, unsigned int len) +#else +void IotWebConf::readParamValue( + AsyncWebServerRequest* request, const char* paramName, char* target, unsigned int len) +#endif +{ +#ifndef USE_ESPASYNCWEBSERVER + String value = this->_server->arg(paramName); +#else + String value = request->arg(paramName); +#endif +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("Value of arg '"); + Serial.print(paramName); + Serial.print("' is:"); + Serial.println(value); +#endif + value.toCharArray(target, len); +} + +#ifndef USE_ESPASYNCWEBSERVER +boolean IotWebConf::validateForm() +#else +boolean IotWebConf::validateForm(AsyncWebServerRequest* request) +#endif +{ + // -- Clean previous error messages. + IotWebConfParameter* current = this->_firstParameter; + while (current != NULL) + { + current->errorMessage = NULL; + current = current->_nextParameter; + } + + // -- Call external validator. + boolean valid = true; + if (this->_formValidator != NULL) + { + valid = this->_formValidator(); + } + + // -- Internal validation. +#ifndef USE_ESPASYNCWEBSERVER + int l = this->_server->arg(this->_thingNameParameter.getId()).length(); +#else + int l = request->arg(this->_thingNameParameter.getId()).length(); +#endif + if (3 > l) + { + this->_thingNameParameter.errorMessage = + "Give a name with at least 3 characters."; + valid = false; + } +#ifndef USE_ESPASYNCWEBSERVER + l = this->_server->arg(this->_apPasswordParameter.getId()).length(); +#else + l = request->arg(this->_apPasswordParameter.getId()).length(); +#endif + if ((0 < l) && (l < 8)) + { + this->_apPasswordParameter.errorMessage = + "Password length must be at least 8 characters."; + valid = false; + } +#ifndef USE_ESPASYNCWEBSERVER + l = this->_server->arg(this->_wifiPasswordParameter.getId()).length(); +#else + l = request->arg(this->_wifiPasswordParameter.getId()).length(); +#endif + if ((0 < l) && (l < 8)) + { + this->_wifiPasswordParameter.errorMessage = + "Password length must be at least 8 characters."; + valid = false; + } + + return valid; +} + +#ifndef USE_ESPASYNCWEBSERVER +void IotWebConf::handleNotFound() +#else +void IotWebConf::handleNotFound(AsyncWebServerRequest *request) +#endif +{ +#ifndef USE_ESPASYNCWEBSERVER + if (this->handleCaptivePortal()) +#else + if (this->handleCaptivePortal(request)) +#endif + { + // If captive portal redirect instead of displaying the error page. + return; + } +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("Requested non-existing page '"); +#ifndef USE_ESPASYNCWEBSERVER + Serial.print(this->_server->uri()); +#else + Serial.print(request->url()); +#endif + Serial.print("' arguments("); +#ifndef USE_ESPASYNCWEBSERVER + Serial.print(this->_server->method() == HTTP_GET ? "GET" : "POST"); +#else + Serial.print(request->method() == HTTP_GET ? "GET" : "POST"); +#endif + Serial.print("):"); +#ifndef USE_ESPASYNCWEBSERVER + Serial.println(this->_server->args()); +#else + Serial.println(request->args()); +#endif +#endif + String message = "File Not Found\n\n"; + message += "URI: "; +#ifndef USE_ESPASYNCWEBSERVER + message += this->_server->uri(); +#else + message += request->url(); +#endif + message += "\nMethod: "; +#ifndef USE_ESPASYNCWEBSERVER + message += (this->_server->method() == HTTP_GET) ? "GET" : "POST"; +#else + message += (request->method() == HTTP_GET) ? "GET" : "POST"; +#endif + message += "\nArguments: "; +#ifndef USE_ESPASYNCWEBSERVER + message += this->_server->args(); +#else + message += request->args(); +#endif + message += "\n"; + +#ifndef USE_ESPASYNCWEBSERVER + for (uint8_t i = 0; i < this->_server->args(); i++) +#else + for (uint8_t i = 0; i < request->args(); i++) +#endif + { + message += +#ifndef USE_ESPASYNCWEBSERVER + " " + this->_server->argName(i) + ": " + this->_server->arg(i) + "\n"; +#else + " " + request->argName(i) + ": " + request->arg(i) + "\n"; +#endif + } +#ifndef USE_ESPASYNCWEBSERVER + this->_server->sendHeader( + "Cache-Control", "no-cache, no-store, must-revalidate"); + this->_server->sendHeader("Pragma", "no-cache"); + this->_server->sendHeader("Expires", "-1"); + this->_server->sendHeader("Content-Length", String(message.length())); + this->_server->send(404, "text/plain", message); +#else + AsyncWebServerResponse *response = request->beginResponse(404, "text/plain", message); + response->addHeader( + "Cache-Control", "no-cache, no-store, must-revalidate"); + response->addHeader("Pragma", "no-cache"); + response->addHeader("Expires", "-1"); + response->addHeader("Content-Length", String(message.length())); + request->send(response); +#endif +} + +/** + * Redirect to captive portal if we got a request for another domain. + * Return true in that case so the page handler do not try to handle the request + * again. (Code from WifiManager project.) + */ +#ifndef USE_ESPASYNCWEBSERVER +boolean IotWebConf::handleCaptivePortal() +#else +boolean IotWebConf::handleCaptivePortal(AsyncWebServerRequest *request) +#endif +{ +#ifndef USE_ESPASYNCWEBSERVER + String host = this->_server->hostHeader(); +#else + String host = request->host().c_str(); +#endif + String thingName = String(this->_thingName); + thingName.toLowerCase(); + if (!isIp(host) && !host.startsWith(thingName)) + { +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("Request for "); + Serial.print(host); + Serial.print(" redirected to "); +#ifndef USE_ESPASYNCWEBSERVER + Serial.println(this->_server->client().localIP()); +#else + if (ON_STA_FILTER(request)) { + Serial.println(WiFi.localIP()); + } else { + Serial.println(WiFi.softAPIP()); + } +#endif +#endif +#ifndef USE_ESPASYNCWEBSERVER + this->_server->sendHeader( + "Location", String("http://") + toStringIp(this->_server->client().localIP()), true); + this->_server->send(302, "text/plain", ""); // Empty content inhibits Content-length header so we have to close the socket ourselves. + this->_server->client().stop(); // Stop is needed because we sent no content length + return true; +#else + String RedirectUrl = "http://"; + if (ON_STA_FILTER(request)) { + RedirectUrl += WiFi.localIP().toString(); + } else { + RedirectUrl += WiFi.softAPIP().toString(); + } + request->redirect(RedirectUrl); +#endif + } + return false; +} + +/** Is this an IP? */ +boolean IotWebConf::isIp(String str) +{ + for (size_t i = 0; i < str.length(); i++) + { + int c = str.charAt(i); + if (c != '.' && (c < '0' || c > '9')) + { + return false; + } + } + return true; +} + +/** IP to String? */ +String IotWebConf::toStringIp(IPAddress ip) +{ + String res = ""; + for (int i = 0; i < 3; i++) + { + res += String((ip >> (8 * i)) & 0xFF) + "."; + } + res += String(((ip >> 8 * 3)) & 0xFF); + return res; +} + +///////////////////////////////////////////////////////////////////////////////// + +void IotWebConf::delay(unsigned long m) +{ + unsigned long delayStart = millis(); + while (m > millis() - delayStart) + { + this->doLoop(); + delay(1); // -- Note: 1ms might not be enough to perform a full yield. So + // 'yeild' in 'doLoop' is eventually a good idea. + } +} + +void IotWebConf::doLoop() +{ + doBlink(); + yield(); // -- Yield should not be necessary, but cannot hurt eather. + if (this->_state == IOTWEBCONF_STATE_BOOT) + { + // -- After boot, fall immediately to AP mode. + byte startupState = IOTWEBCONF_STATE_AP_MODE; + if (this->_skipApStartup) + { + if (isWifiModePossible()) + { + IOTWEBCONF_DEBUG_LINE( + F("SkipApStartup is requested, but either no WiFi was set up, or " + "configButton was pressed.")); + } + else + { + // -- Startup state can be WiFi, if it is requested and also possible. + IOTWEBCONF_DEBUG_LINE(F("SkipApStartup mode was applied")); + startupState = IOTWEBCONF_STATE_CONNECTING; + } + } + this->changeState(startupState); + } + else if ( + (this->_state == IOTWEBCONF_STATE_NOT_CONFIGURED) || + (this->_state == IOTWEBCONF_STATE_AP_MODE)) + { + // -- We must only leave the AP mode, when no slaves are connected. + // -- Other than that AP mode has a timeout. E.g. after boot, or when retry + // connecting to WiFi + checkConnection(); + checkApTimeout(); + this->_dnsServer->processNextRequest(); +#ifndef USE_ESPASYNCWEBSERVER + this->_server->handleClient(); +#endif + } + else if (this->_state == IOTWEBCONF_STATE_CONNECTING) + { + if (checkWifiConnection()) + { + this->changeState(IOTWEBCONF_STATE_ONLINE); + return; + } + } + else if (this->_state == IOTWEBCONF_STATE_ONLINE) + { + // -- In server mode we provide web interface. And check whether it is time + // to run the client. +#ifndef USE_ESPASYNCWEBSERVER + this->_server->handleClient(); +#endif + if (WiFi.status() != WL_CONNECTED) + { + IOTWEBCONF_DEBUG_LINE(F("Not connected. Try reconnect...")); + this->changeState(IOTWEBCONF_STATE_CONNECTING); + return; + } + } +} + +/** + * What happens, when a state changed... + */ +void IotWebConf::changeState(byte newState) +{ + switch (newState) + { + case IOTWEBCONF_STATE_AP_MODE: + { + // -- In AP mode we must override the default AP password. Otherwise we stay + // in STATE_NOT_CONFIGURED. + if (isWifiModePossible()) + { +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + if (this->_forceDefaultPassword) + { + Serial.println("AP mode forced by reset pin"); + } + else + { + Serial.println("AP password was not set in configuration"); + } +#endif + newState = IOTWEBCONF_STATE_NOT_CONFIGURED; + } + break; + } + default: + break; + } +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("State changing from: "); + Serial.print(this->_state); + Serial.print(" to "); + Serial.println(newState); +#endif + byte oldState = this->_state; + this->_state = newState; + this->stateChanged(oldState, newState); +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("State changed from: "); + Serial.print(oldState); + Serial.print(" to "); + Serial.println(newState); +#endif +} + +/** + * What happens, when a state changed... + */ +void IotWebConf::stateChanged(byte oldState, byte newState) +{ +// updateOutput(); + switch (newState) + { + case IOTWEBCONF_STATE_AP_MODE: + case IOTWEBCONF_STATE_NOT_CONFIGURED: + if (newState == IOTWEBCONF_STATE_AP_MODE) + { + this->blinkInternal(300, 90); + } + else + { + this->blinkInternal(300, 50); + } + setupAp(); +#ifndef USE_ESPASYNCWEBSERVER + if (this->_updateServer != NULL) + { + this->_updateServer->setup(this->_server, this->_updatePath); + } +#endif + this->_server->begin(); + this->_apConnectionStatus = IOTWEBCONF_AP_CONNECTION_STATE_NC; + this->_apStartTimeMs = millis(); + break; + case IOTWEBCONF_STATE_CONNECTING: + if ((oldState == IOTWEBCONF_STATE_AP_MODE) || + (oldState == IOTWEBCONF_STATE_NOT_CONFIGURED)) + { + stopAp(); + } + this->blinkInternal(1000, 50); +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("Connecting to ["); + Serial.print(this->_wifiAuthInfo.ssid); +# ifdef IOTWEBCONF_DEBUG_PWD_TO_SERIAL + Serial.print("] with password ["); + Serial.print(this->_wifiAuthInfo.password); + Serial.println("]"); +# else + Serial.println(F("] (password is hidden)")); +# endif +#endif + this->_wifiConnectionStart = millis(); + this->_wifiConnectionHandler( + this->_wifiAuthInfo.ssid, this->_wifiAuthInfo.password); + break; + case IOTWEBCONF_STATE_ONLINE: + this->blinkInternal(8000, 2); +#ifndef USE_ESPASYNCWEBSERVER + if (this->_updateServer != NULL) + { + this->_updateServer->updateCredentials( + IOTWEBCONF_ADMIN_USER_NAME, this->_apPassword); + } +#endif + this->_server->begin(); + IOTWEBCONF_DEBUG_LINE(F("Accepting connection")); + if (this->_wifiConnectionCallback != NULL) + { + this->_wifiConnectionCallback(); + } + break; + default: + break; + } +} + +void IotWebConf::checkApTimeout() +{ + if ((this->_wifiSsid[0] != '\0') && (this->_apPassword[0] != '\0') && + (!this->_forceDefaultPassword)) + { + // -- Only move on, when we have a valid WifF and AP configured. + if ((this->_apConnectionStatus == IOTWEBCONF_AP_CONNECTION_STATE_DC) || + ((this->_apTimeoutMs < millis() - this->_apStartTimeMs) && + (this->_apConnectionStatus != IOTWEBCONF_AP_CONNECTION_STATE_C))) + { + this->changeState(IOTWEBCONF_STATE_CONNECTING); + } + } +} + +/** + * Checks whether we have anyone joined to our AP. + * If so, we must not change state. But when our guest leaved, we can + * immediately move on. + */ +void IotWebConf::checkConnection() +{ + if ((this->_apConnectionStatus == IOTWEBCONF_AP_CONNECTION_STATE_NC) && + (WiFi.softAPgetStationNum() > 0)) + { + this->_apConnectionStatus = IOTWEBCONF_AP_CONNECTION_STATE_C; + IOTWEBCONF_DEBUG_LINE(F("Connection to AP.")); + } + else if ( + (this->_apConnectionStatus == IOTWEBCONF_AP_CONNECTION_STATE_C) && + (WiFi.softAPgetStationNum() == 0)) + { + this->_apConnectionStatus = IOTWEBCONF_AP_CONNECTION_STATE_DC; + IOTWEBCONF_DEBUG_LINE(F("Disconnected from AP.")); + if (this->_forceDefaultPassword) + { + IOTWEBCONF_DEBUG_LINE(F("Releasing forced AP mode.")); + this->_forceDefaultPassword = false; + } + } +} + +boolean IotWebConf::checkWifiConnection() +{ + if (WiFi.status() != WL_CONNECTED) + { + if (this->_wifiConnectionTimeoutMs < millis() - this->_wifiConnectionStart) + { + // -- WiFi not available, fall back to AP mode. + IOTWEBCONF_DEBUG_LINE(F("Giving up.")); + WiFi.disconnect(true); + IotWebConfWifiAuthInfo* newWifiAuthInfo = _wifiConnectionFailureHandler(); + if (newWifiAuthInfo != NULL) + { + // -- Try connecting with another connection info. + this->_wifiAuthInfo.ssid = newWifiAuthInfo->ssid; + this->_wifiAuthInfo.password = newWifiAuthInfo->password; + this->changeState(IOTWEBCONF_STATE_CONNECTING); + } + else + { + this->changeState(IOTWEBCONF_STATE_AP_MODE); + } + } + return false; + } + + // -- Connected +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.println("WiFi connected"); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); +#endif + + return true; +} + +void IotWebConf::setupAp() +{ + WiFi.mode(WIFI_AP); + +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("Setting up AP: "); + Serial.println(this->_thingName); +#endif + if (this->_state == IOTWEBCONF_STATE_NOT_CONFIGURED) + { +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("With default password: "); +# ifdef IOTWEBCONF_DEBUG_PWD_TO_SERIAL + Serial.println(this->_initialApPassword); +# else + Serial.println(F("")); +# endif +#endif + this->_apConnectionHandler(this->_thingName, this->_initialApPassword); + } + else + { +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print("Use password: "); +# ifdef IOTWEBCONF_DEBUG_PWD_TO_SERIAL + Serial.println(this->_apPassword); +# else + Serial.println(F("")); +# endif +#endif + this->_apConnectionHandler(this->_thingName, this->_apPassword); + } + +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL + Serial.print(F("AP IP address: ")); + Serial.println(WiFi.softAPIP()); +#endif + // delay(500); // Without delay I've seen the IP address blank + // Serial.print(F("AP IP address: ")); + // Serial.println(WiFi.softAPIP()); + + /* Setup the DNS server redirecting all the domains to the apIP */ + this->_dnsServer->setErrorReplyCode(DNSReplyCode::NoError); + this->_dnsServer->start(IOTWEBCONF_DNS_PORT, "*", WiFi.softAPIP()); +} + +void IotWebConf::stopAp() +{ + WiFi.softAPdisconnect(true); + WiFi.mode(WIFI_STA); +} + +//////////////////////////////////////////////////////////////////// + +void IotWebConf::blink(unsigned long repeatMs, byte dutyCyclePercent) +{ + if (repeatMs == 0) + { + this->stopCustomBlink(); + } + else + { + this->_blinkOnMs = repeatMs * dutyCyclePercent / 100; + this->_blinkOffMs = repeatMs * (100 - dutyCyclePercent) / 100; + } +} + +void IotWebConf::fineBlink(unsigned long onMs, unsigned long offMs) +{ + this->_blinkOnMs = onMs; + this->_blinkOffMs = offMs; +} + +void IotWebConf::stopCustomBlink() +{ + this->_blinkOnMs = this->_internalBlinkOnMs; + this->_blinkOffMs = this->_internalBlinkOffMs; +} + +void IotWebConf::blinkInternal(unsigned long repeatMs, byte dutyCyclePercent) +{ + this->blink(repeatMs, dutyCyclePercent); + this->_internalBlinkOnMs = this->_blinkOnMs; + this->_internalBlinkOffMs = this->_blinkOffMs; +} + +void IotWebConf::doBlink() +{ + if (IOTWEBCONF_STATUS_ENABLED) + { + unsigned long now = millis(); + unsigned long delayMs = + this->_blinkState == LOW ? this->_blinkOnMs : this->_blinkOffMs; + if (delayMs < now - this->_lastBlinkTime) + { + this->_blinkState = 1 - this->_blinkState; + this->_lastBlinkTime = now; + digitalWrite(this->_statusPin, this->_blinkState); + } + } +} + +boolean IotWebConf::connectAp(const char* apName, const char* password) +{ + return WiFi.softAP(apName, password); +} +void IotWebConf::connectWifi(const char* ssid, const char* password) +{ + WiFi.begin(ssid, password); +} +IotWebConfWifiAuthInfo* IotWebConf::handleConnectWifiFailure() +{ + return NULL; +} diff --git a/lib/IotWebConf/src/IotWebConf.h b/lib/IotWebConf/src/IotWebConf.h new file mode 100644 index 0000000..424a4d5 --- /dev/null +++ b/lib/IotWebConf/src/IotWebConf.h @@ -0,0 +1,589 @@ +/** + * IotWebConf.h -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +#ifndef IotWebConf_h +#define IotWebConf_h + +#ifndef USE_ESPASYNCWEBSERVER +#include + +#ifdef ESP8266 +# include +# include +# include +#elif defined(ESP32) +# include +# include +#endif +#else +#include +#endif +#include // -- For captive portal + +// -- We might want to place the config in the EEPROM in an offset. +#define IOTWEBCONF_CONFIG_START 0 + +// -- Maximal length of any string used in IotWebConfig configuration (e.g. +// ssid, password). +#define IOTWEBCONF_WORD_LEN 33 + +// -- IotWebConf tries to connect to the local network for an amount of time +// before falling back to AP mode. +#define IOTWEBCONF_DEFAULT_WIFI_CONNECTION_TIMEOUT_MS 30000 + +// -- Thing will stay in AP mode for an amount of time on boot, before retrying +// to connect to a WiFi network. +#define IOTWEBCONF_DEFAULT_AP_MODE_TIMEOUT_MS 30000 + +// -- mDNS should allow you to connect to this device with a hostname provided +// by the device. E.g. mything.local +#define IOTWEBCONF_CONFIG_USE_MDNS + +// -- Logs progress information to Serial if enabled. +#define IOTWEBCONF_DEBUG_TO_SERIAL + +// -- Logs passwords to Serial if enabled. +//#define IOTWEBCONF_DEBUG_PWD_TO_SERIAL + +// -- Helper define for serial debug +#ifdef IOTWEBCONF_DEBUG_TO_SERIAL +# define IOTWEBCONF_DEBUG_LINE(MSG) Serial.println(MSG) +#else +# define IOTWEBCONF_DEBUG_LINE(MSG) +#endif + +// -- EEPROM config starts with a special prefix of length defined here. +#define IOTWEBCONF_CONFIG_VESION_LENGTH 4 +#define IOTWEBCONF_DNS_PORT 53 + +// -- HTML page fragments +const char IOTWEBCONF_HTML_HEAD[] PROGMEM = "{v}"; +const char IOTWEBCONF_HTML_STYLE_INNER[] PROGMEM = ".de{background-color:#ffaaaa;} .em{font-size:0.8em;color:#bb0000;padding-bottom:0px;} .c{text-align: center;} div,input{padding:5px;font-size:1em;} input{width:95%;} body{text-align: center;font-family:verdana;} button{border:0;border-radius:0.3rem;background-color:#16A1E7;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;} fieldset{border-radius:0.3rem;margin: 0px;}"; +const char IOTWEBCONF_HTML_SCRIPT_INNER[] PROGMEM = "function c(l){document.getElementById('s').value=l.innerText||l.textContent;document.getElementById('p').focus();}"; +const char IOTWEBCONF_HTML_HEAD_END[] PROGMEM = ""; +const char IOTWEBCONF_HTML_BODY_INNER[] PROGMEM = "
"; +const char IOTWEBCONF_HTML_FORM_START[] PROGMEM = "
"; +const char IOTWEBCONF_HTML_FORM_PARAM[] PROGMEM = "
{e}
"; +const char IOTWEBCONF_HTML_FORM_END[] PROGMEM = "
"; +const char IOTWEBCONF_HTML_SAVED[] PROGMEM = "
Condiguration saved
Return to home page.
"; +const char IOTWEBCONF_HTML_END[] PROGMEM = "
"; +const char IOTWEBCONF_HTML_UPDATE[] PROGMEM = ""; +const char IOTWEBCONF_HTML_CONFIG_VER[] PROGMEM = "
Firmware config version '{v}'
"; + +// -- State of the Thing +#define IOTWEBCONF_STATE_BOOT 0 +#define IOTWEBCONF_STATE_NOT_CONFIGURED 1 +#define IOTWEBCONF_STATE_AP_MODE 2 +#define IOTWEBCONF_STATE_CONNECTING 3 +#define IOTWEBCONF_STATE_ONLINE 4 + +// -- AP connection state +// -- No connection on AP. +#define IOTWEBCONF_AP_CONNECTION_STATE_NC 0 +// -- Has connection on AP. +#define IOTWEBCONF_AP_CONNECTION_STATE_C 1 +// -- All previous connection on AP was disconnected. +#define IOTWEBCONF_AP_CONNECTION_STATE_DC 2 + +// -- Status indicator output logical levels. +#define IOTWEBCONF_STATUS_ON LOW +#define IOTWEBCONF_STATUS_OFF HIGH + +// -- User name on login. +#define IOTWEBCONF_ADMIN_USER_NAME "admin" + +typedef struct IotWebConfWifiAuthInfo +{ + const char* ssid; + const char* password; +} IotWebConfWifiAuthInfo; + +/** + * IotWebConfParameters is a configuration item of the config portal. + * The parameter will have its input field on the configuration page, + * and the provided value will be saved to the EEPROM. + */ +class IotWebConfParameter +{ +public: + /** + * Create a parameter for the config portal. + * + * @label - Displayable label at the config portal. + * @id - Identifier used for HTTP queries and as configuration key. Must not contain spaces nor other special characters. + * @valueBuffer - Configuration value will be loaded to this buffer from the EEPROM. + * @length - The buffer should have a length provided here. + * @type (optional, default="text") - The type of the html input field. + * The type="password" has a special handling, as the value will be overwritten in the EEPROM + * only if value was provided on the config portal. Because of this logic, "password" type field with + * length more then IOTWEBCONF_WORD_LEN characters are not supported. + * @placeholder (optional) - Text appear in an empty input box. + * @defaultValue (optional) - Value should be pre-filled if none was specified before. + * @customHtml (optional) - The text of this parameter will be added into the HTML INPUT field. + */ + IotWebConfParameter( + const char* label, const char* id, char* valueBuffer, int length, + const char* type = "text", const char* placeholder = NULL, + const char* defaultValue = NULL, const char* customHtml = NULL, + boolean visible = true); + + /** + * Same as normal constructor, but config portal does not render a default + * input field for the item, instead uses the customHtml provided. Note the + * @type parameter description above! + */ + IotWebConfParameter( + const char* id, char* valueBuffer, int length, const char* customHtml, + const char* type = "text"); + + /** + * For internal use only. + */ + IotWebConfParameter(); + + const char* label; + char* valueBuffer; + const char* type; + const char* placeholder; + const char* defaultValue; + const char* customHtml; + boolean visible; + const char* errorMessage; + + // -- For internal use only + IotWebConfParameter* _nextParameter = NULL; + + const char* getId() { return this->_id; } + int getLength() { return this->_length; } + +private: + const char* _id = 0; + int _length; +}; + +/** + * A separator for separating field sets. + */ +class IotWebConfSeparator : public IotWebConfParameter +{ +public: + IotWebConfSeparator(); + + /** + * Create a seperator with a label (legend tag) + */ + IotWebConfSeparator(const char* label); +}; + +/** + * Class for providing HTML format segments. + */ +class IotWebConfHtmlFormatProvider +{ +public: + virtual String getHead() { return FPSTR(IOTWEBCONF_HTML_HEAD); } + virtual String getStyle() { return ""; } + virtual String getScript() { return ""; } + virtual String getHeadExtension() { return ""; } + virtual String getHeadEnd() { return String(FPSTR(IOTWEBCONF_HTML_HEAD_END)) + getBodyInner(); } + virtual String getFormStart() { return FPSTR(IOTWEBCONF_HTML_FORM_START); } + virtual String getFormParam(const char* type) { return FPSTR(IOTWEBCONF_HTML_FORM_PARAM); } + virtual String getFormEnd() { return FPSTR(IOTWEBCONF_HTML_FORM_END); } + virtual String getFormSaved() { return FPSTR(IOTWEBCONF_HTML_SAVED); } + virtual String getEnd() { return FPSTR(IOTWEBCONF_HTML_END); } + virtual String getUpdate() { return FPSTR(IOTWEBCONF_HTML_UPDATE); } + virtual String getConfigVer() { return FPSTR(IOTWEBCONF_HTML_CONFIG_VER); } +protected: + virtual String getStyleInner() { return FPSTR(IOTWEBCONF_HTML_STYLE_INNER); } + virtual String getScriptInner() { return FPSTR(IOTWEBCONF_HTML_SCRIPT_INNER); } + virtual String getBodyInner() { return FPSTR(IOTWEBCONF_HTML_BODY_INNER); } +}; + +/** + * Main class of the module. + */ +class IotWebConf +{ +public: + /** + * Create a new configuration handler. + * @thingName - Initial value for the thing name. Used in many places like AP name, can be changed by the user. + * @dnsServer - A created DNSServer, that can be configured for captive portal. + * @server - A created web server. Will be started upon connection success. + * @initialApPassword - Initial value for AP mode. Can be changed by the user. + * @configVersion - When the software is updated and the configuration is changing, this key should also be changed, + * so that the config portal will force the user to reenter all the configuration values. + */ +#ifndef USE_ESPASYNCWEBSERVER + IotWebConf( + const char* thingName, DNSServer* dnsServer, WebServer* server, + const char* initialApPassword, const char* configVersion = "init"); +#else + IotWebConf( + const char* thingName, DNSServer* dnsServer, AsyncWebServer* server, + const char* initialApPassword, const char* configVersion = "init"); +#endif + /** + * Provide an Arduino pin here, that has a button connected to it with the other end of the pin is connected to GND. + * The button pin is queried at for input on boot time (init time). + * If the button was pressed, the thing will enter AP mode with the initial password. + * Must be called before init()! + * @configPin - An Arduino pin. Will be configured as INPUT_PULLUP! + */ + void setConfigPin(int configPin); + + /** + * Provide an Arduino pin for status indicator (LOW = on). Blink codes: + * - Rapid blinks - The thing is in AP mode with default password. + * - Rapid blinks, but mostly on - AP mode, waiting for configuration changes. + * - Normal blinks - Connecting to WiFi. + * - Mostly off with rare rapid blinks - WiFi is connected performing normal operation. + * User can also apply custom blinks. See blink() method! + * Must be called before init()! + * @statusPin - An Arduino pin. Will be configured as OUTPUT! + */ + void setStatusPin(int statusPin); + + /** + * Add an UpdateServer instance to the system. The firmware update link will appear on the config portal. + * The UpdateServer will be added to the WebServer with the path provided here (or with "firmware", + * if none was provided). + * Login user will be IOTWEBCONF_ADMIN_USER_NAME, password is the password provided in the config portal. + * Should be called before init()! + * @updateServer - An uninitialized UpdateServer instance. + * @updatePath - (Optional) The path to set up the UpdateServer with. Will be also used in the config portal. + */ +#ifndef USE_ESPASYNCWEBSERVER + void setupUpdateServer( + HTTPUpdateServer* updateServer, const char* updatePath = "/firmware"); +#endif + + /** + * Start up the IotWebConf module. + * Loads all configuration from the EEPROM, and initialize the system. + * Will return false, if no configuration (with specified config version) was found in the EEPROM. + */ + boolean init(); + + /** + * IotWebConf is a non-blocking, state controlled system. Therefor it should be + * regularly triggered from the user code. + * So call this method any time you can. + */ + void doLoop(); + + /** + * Each WebServer URL handler method should start with calling this method. + * If this method return true, the request was already served by it. + */ +#ifndef USE_ESPASYNCWEBSERVER + boolean handleCaptivePortal(); +#else + boolean handleCaptivePortal(AsyncWebServerRequest *request); +#endif + + /** + * Config URL web request handler. Call this method to handle config request. + */ +#ifndef USE_ESPASYNCWEBSERVER + void handleConfig() +#else + void handleConfig(AsyncWebServerRequest *request); +#endif + + /** + * URL-not-found web request handler. Used for handling captive portal request. + */ +#ifndef USE_ESPASYNCWEBSERVER + void handleNotFound(); +#else + void handleNotFound(AsyncWebServerRequest *request); +#endif + + /** + * Specify a callback method, that will be called upon WiFi connection success. + * Should be called before init()! + */ + void setWifiConnectionCallback(std::function func); + + /** + * Specify a callback method, that will be called when settings have been changed. + * Should be called before init()! + */ + void setConfigSavedCallback(std::function func); + + /** + * Specify a callback method, that will be called when form validation is required. + * If the method will return false, the configuration will not be saved. + * Should be called before init()! + */ + void setFormValidator(std::function func); + + /** + * Specify your custom Access Point connection handler. Please use IotWebConf::connectAp() as + * reference when implementing your custom solution. + */ + void setApConnectionHandler( + std::function func) + { + _apConnectionHandler = func; + } + + /** + * Specify your custom WiFi connection handler. Please use IotWebConf::connectWifi() as + * reference when implementing your custom solution. + */ + void setWifiConnectionHandler( + std::function func) + { + _wifiConnectionHandler = func; + } + + /** + * With this method you can specify your custom WiFi timeout handler. + * This hander can manage what should happen, when WiFi connection timed out. + * By default the handler implementation returns with NULL, as seen on reference implementation + * IotWebConf::handleConnectWifiFailure(). This means we need to fall back to AP mode. + * If it method returns with a (new) WiFi settings, it is used as a next try. + * Note, that this feature is provided because of a possible future option of providing multiply + * WiFi settings. + */ + void setWifiConnectionFailedHandler( std::function func ) + { + _wifiConnectionFailureHandler = func; + } + + /** + * Add a custom parameter, that will be handled by the IotWebConf module. + * The parameter will be saved to/loaded from EEPROM automatically, + * and will appear on the config portal. + * Will return false, if adding was not successful. + * Must be called before init()! + */ + bool addParameter(IotWebConfParameter* parameter); + + /** + * Getter for the actually configured thing name. + */ + char* getThingName(); + + /** + * Use this delay, to prevent blocking IotWebConf. + */ + void delay(unsigned long millis); + + /** + * IotWebConf tries to connect to the local network for an amount of time before falling back to AP mode. + * The default amount can be updated with this setter. + * Should be called before init()! + */ + void setWifiConnectionTimeoutMs(unsigned long millis); + + /** + * Interrupts internal blinking cycle and applies new values for + * blinking the status LED (if one configured with setStatusPin() prior init() + * ). + * @repeatMs - Defines the the period of one on-off cycle in milliseconds. + * @dutyCyclePercent - LED on/off percent. 100 means always on, 0 means + * always off. When called with repeatMs = 0, then internal blink cycle will + * be continued. + */ + void blink(unsigned long repeatMs, byte dutyCyclePercent); + + /** + * Similar to blink, but here we define exact on and off times for more + * precise timings. + * @onMs - Milliseconds for the LED tudned on. + * @offMs - Milliseconds for the LED tudned off. + */ + void fineBlink(unsigned long onMs, unsigned long offMs); + + /** + * Stop custom blinking defined by blink() or fineBlink() and continues with + * the internal blink cycle. + */ + void stopCustomBlink(); + + /** + * Return the current state, that will be a value from the IOTWEBCONF_STATE_* constants. + */ + byte getState() { return this->_state; }; + + /** + * This method can be used to set the AP timeout directly without modifying the apTimeoutParameter. + * Note, that apTimeoutMs value will be reset to the value of apTimeoutParameter on init and on config save. + */ + void setApTimeoutMs(unsigned long apTimeoutMs) + { + this->_apTimeoutMs = apTimeoutMs; + }; + + /** + * Returns the actual value of the AP timeout in use. + */ + unsigned long getApTimeoutMs() { return this->_apTimeoutMs; }; + + /** + * Resets the authentication credentials for WiFi connection to the configured one. + * With the return value of setWifiConnectionFailedHandler() one can provide alternative connection settings, + * that can be reset with resetWifiAuthInfo(). + */ + void resetWifiAuthInfo() + { + _wifiAuthInfo = {this->_wifiSsid, this->_wifiPassword}; + }; + + /** + * By default IotWebConf starts up in AP mode. Calling this method before the init will force IotWebConf + * to connect immediatelly to the configured WiFi network. + * Note, this method only takes effect, when WiFi mode is enabled, thus when a valid WiFi connection is + * set up, and AP mode is not forced by ConfigPin (see setConfigPin() for details). + */ + void skipApStartup() { this->_skipApStartup = true; } + + /** + * Get internal parameters, for manual handling. + * Normally you don't need to access these parameters directly. + * Note, that changing valueBuffer of these parameters should be followed by configSave()! + */ + IotWebConfParameter* getThingNameParameter() + { + return &this->_thingNameParameter; + }; + IotWebConfParameter* getApPasswordParameter() + { + return &this->_apPasswordParameter; + }; + IotWebConfParameter* getWifiSsidParameter() + { + return &this->_wifiSsidParameter; + }; + IotWebConfParameter* getWifiPasswordParameter() + { + return &this->_wifiPasswordParameter; + }; + IotWebConfParameter* getApTimeoutParameter() + { + return &this->_apTimeoutParameter; + }; + + /** + * If config parameters are modified directly, the new values can be saved by this method. + * Note, that init() must pretend configSave()! + * Also note, that configSave writes to EEPROM, and EEPROM can be written only some thousand times + * in the lifetime of an ESP8266 module. + */ + void configSave(); + + /** + * With this method you can override the default HTML format provider to + * provide custom HTML segments. + */ + void + setHtmlFormatProvider(IotWebConfHtmlFormatProvider* customHtmlFormatProvider) + { + this->htmlFormatProvider = customHtmlFormatProvider; + } + IotWebConfHtmlFormatProvider* getHtmlFormatProvider() + { + return this->htmlFormatProvider; + } + +private: + const char* _initialApPassword = NULL; + const char* _configVersion; + DNSServer* _dnsServer; +#ifndef USE_ESPASYNCWEBSERVER + WebServer* _server; + HTTPUpdateServer* _updateServer = NULL; +#else + AsyncWebServer* _server; +#endif + int _configPin = -1; + int _statusPin = -1; + const char* _updatePath = NULL; + boolean _forceDefaultPassword = false; + boolean _skipApStartup = false; + IotWebConfParameter* _firstParameter = NULL; + IotWebConfParameter _thingNameParameter; + IotWebConfParameter _apPasswordParameter; + IotWebConfParameter _wifiSsidParameter; + IotWebConfParameter _wifiPasswordParameter; + IotWebConfParameter _apTimeoutParameter; + char _thingName[IOTWEBCONF_WORD_LEN]; + char _apPassword[IOTWEBCONF_WORD_LEN]; + char _wifiSsid[IOTWEBCONF_WORD_LEN]; + char _wifiPassword[IOTWEBCONF_WORD_LEN]; + char _apTimeoutStr[IOTWEBCONF_WORD_LEN]; + unsigned long _apTimeoutMs = IOTWEBCONF_DEFAULT_AP_MODE_TIMEOUT_MS; + unsigned long _wifiConnectionTimeoutMs = + IOTWEBCONF_DEFAULT_WIFI_CONNECTION_TIMEOUT_MS; + byte _state = IOTWEBCONF_STATE_BOOT; + unsigned long _apStartTimeMs = 0; + byte _apConnectionStatus = IOTWEBCONF_AP_CONNECTION_STATE_NC; + std::function _wifiConnectionCallback = NULL; + std::function _configSavedCallback = NULL; + std::function _formValidator = NULL; + std::function _apConnectionHandler = + &(IotWebConf::connectAp); + std::function _wifiConnectionHandler = + &(IotWebConf::connectWifi); + std::function _wifiConnectionFailureHandler = + &(IotWebConf::handleConnectWifiFailure); + unsigned long _internalBlinkOnMs = 500; + unsigned long _internalBlinkOffMs = 500; + unsigned long _blinkOnMs = 500; + unsigned long _blinkOffMs = 500; + byte _blinkState = IOTWEBCONF_STATUS_ON; + unsigned long _lastBlinkTime = 0; + unsigned long _wifiConnectionStart = 0; + IotWebConfWifiAuthInfo _wifiAuthInfo = {_wifiSsid, _wifiPassword}; + IotWebConfHtmlFormatProvider htmlFormatProviderInstance; + IotWebConfHtmlFormatProvider* htmlFormatProvider = &htmlFormatProviderInstance; + + void configInit(); + boolean configLoad(); + boolean configTestVersion(); + void configSaveConfigVersion(); + void readEepromValue(int start, char* valueBuffer, int length); + void writeEepromValue(int start, char* valueBuffer, int length); + +#ifndef USE_ESPASYNCWEBSERVER + void readParamValue(const char* paramName, char* target, unsigned int len); + boolean validateForm(); +#else + void readParamValue(AsyncWebServerRequest* request, const char* paramName, char* target, unsigned int len); + boolean validateForm(AsyncWebServerRequest* request); +#endif + + void changeState(byte newState); + void stateChanged(byte oldState, byte newState); + boolean isWifiModePossible() + { + return this->_forceDefaultPassword || (this->_apPassword[0] == '\0'); + } + boolean isIp(String str); + String toStringIp(IPAddress ip); + void doBlink(); + void blinkInternal(unsigned long repeatMs, byte dutyCyclePercent); + + void checkApTimeout(); + void checkConnection(); + boolean checkWifiConnection(); + void setupAp(); + void stopAp(); + + static boolean connectAp(const char* apName, const char* password); + static void connectWifi(const char* ssid, const char* password); + static IotWebConfWifiAuthInfo* handleConnectWifiFailure(); +}; + +#endif diff --git a/lib/IotWebConf/src/IotWebConfCompatibility.cpp b/lib/IotWebConf/src/IotWebConfCompatibility.cpp new file mode 100644 index 0000000..9d72989 --- /dev/null +++ b/lib/IotWebConf/src/IotWebConfCompatibility.cpp @@ -0,0 +1,120 @@ +/** + * IotWebConfCompatibility.cpp -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * Notes on IotWebConfCompatibility: + * This file contains workarounds, and borrowed codes from other projects, + * to keep IotWebConf code more simple and more general. See details + * in comments for code segments in IotWebConfCompatibility.h . + * + */ + +#ifdef ESP32 +/** + * NOTE: Lines starting with /// are changed by IotWebConf + */ +#include "IotWebConfCompatibility.h" + +static const char serverIndex[] PROGMEM = + R"(
+ + +
+ )"; +static const char successResponse[] PROGMEM = + "Update Success! Rebooting...\n"; + +HTTPUpdateServer::HTTPUpdateServer(bool serial_debug) +{ + _serial_output = serial_debug; + _server = NULL; + _username = emptyString; + _password = emptyString; + _authenticated = false; +} + +void HTTPUpdateServer::setup(WebServer *server, const String& path, const String& username, const String& password) +{ + _server = server; + _username = username; + _password = password; + + // handler for the /update form page + _server->on(path.c_str(), HTTP_GET, [&](){ + if(_username != emptyString && _password != emptyString && !_server->authenticate(_username.c_str(), _password.c_str())) + return _server->requestAuthentication(); + _server->send_P(200, PSTR("text/html"), serverIndex); + }); + + // handler for the /update form POST (once file upload finishes) + _server->on(path.c_str(), HTTP_POST, [&](){ + if(!_authenticated) + return _server->requestAuthentication(); + if (Update.hasError()) { + _server->send(200, F("text/html"), String(F("Update error: ")) + _updaterError); + } else { + _server->client().setNoDelay(true); + _server->send_P(200, PSTR("text/html"), successResponse); + delay(100); + _server->client().stop(); + ESP.restart(); + } + },[&](){ + // handler for the file upload, get's the sketch bytes, and writes + // them through the Update object + HTTPUpload& upload = _server->upload(); + + if(upload.status == UPLOAD_FILE_START){ + _updaterError = String(); + if (_serial_output) + Serial.setDebugOutput(true); + + _authenticated = (_username == emptyString || _password == emptyString || _server->authenticate(_username.c_str(), _password.c_str())); + if(!_authenticated){ + if (_serial_output) + Serial.printf("Unauthenticated Update\n"); + return; + } + +/// WiFiUDP::stopAll(); + if (_serial_output) + Serial.printf("Update: %s\n", upload.filename.c_str()); +/// uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; +/// if(!Update.begin(maxSketchSpace)){//start with max available size + if(!Update.begin(UPDATE_SIZE_UNKNOWN)){//start with max available size + _setUpdaterError(); + } + } else if(_authenticated && upload.status == UPLOAD_FILE_WRITE && !_updaterError.length()){ + if (_serial_output) Serial.printf("."); + if(Update.write(upload.buf, upload.currentSize) != upload.currentSize){ + _setUpdaterError(); + } + } else if(_authenticated && upload.status == UPLOAD_FILE_END && !_updaterError.length()){ + if(Update.end(true)){ //true to set the size to the current progress + if (_serial_output) Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize); + } else { + _setUpdaterError(); + } + if (_serial_output) Serial.setDebugOutput(false); + } else if(_authenticated && upload.status == UPLOAD_FILE_ABORTED){ + Update.end(); + if (_serial_output) Serial.println("Update was aborted"); + } + delay(0); + }); +} + +void HTTPUpdateServer::_setUpdaterError() +{ + if (_serial_output) Update.printError(Serial); + StreamString str; + Update.printError(str); + _updaterError = str.c_str(); +} +#endif \ No newline at end of file diff --git a/lib/IotWebConf/src/IotWebConfCompatibility.h b/lib/IotWebConf/src/IotWebConfCompatibility.h new file mode 100644 index 0000000..aa7e4cf --- /dev/null +++ b/lib/IotWebConf/src/IotWebConfCompatibility.h @@ -0,0 +1,94 @@ +/** + * IotWebConfCompatibility.h -- IotWebConf is an ESP8266/ESP32 + * non blocking WiFi/AP web configuration library for Arduino. + * https://github.com/prampec/IotWebConf + * + * Copyright (C) 2018 Balazs Kelemen + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + * + * Notes on IotWebConfCompatibility: + * This file contains workarounds, and borrowed codes from other projects, + * to keep IotWebConf code more simple and more general. See details + * in comments for code segments bellow this file. + * + */ + +/** + * ESP8266 still uses these special naming for their classes, while + * ESP32 uses a more general one. So for the time being, we are + * creating local aliases for those. + */ +#ifdef ESP8266 +# define WebServer ESP8266WebServer +# define HTTPUpdateServer ESP8266HTTPUpdateServer +#endif + + + +/** + * ESP32 doesn't implement a HTTPUpdateServer. However it seams, that to code + * from ESP8266 covers nearly all the same functionality. + * So we need to implement our own HTTPUpdateServer for ESP32, and code is + * reused from + * https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266HTTPUpdateServer/src/ + * version: 41de43a26381d7c9d29ce879dd5d7c027528371b + */ +#ifdef ESP32 + +#ifndef __HTTP_UPDATE_SERVER_H +#define __HTTP_UPDATE_SERVER_H + +#include +#include +#include +#include +#include + +#define emptyString F("") + +class WebServer; + +class HTTPUpdateServer +{ + public: + HTTPUpdateServer(bool serial_debug=false); + + void setup(WebServer *server) + { + setup(server, emptyString, emptyString); + } + + void setup(WebServer *server, const String& path) + { + setup(server, path, emptyString, emptyString); + } + + void setup(WebServer *server, const String& username, const String& password) + { + setup(server, "/update", username, password); + } + + void setup(WebServer *server, const String& path, const String& username, const String& password); + + void updateCredentials(const String& username, const String& password) + { + _username = username; + _password = password; + } + + protected: + void _setUpdaterError(); + + private: + bool _serial_output; + WebServer *_server; + String _username; + String _password; + bool _authenticated; + String _updaterError; +}; +#endif + +#endif \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index dd63bf5..b53f8a0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -16,8 +16,11 @@ upload_port = COM11 board_build.partitions = no_ota.csv lib_deps = - IotWebConf + ESPAsyncTCP + ESP Async WebServer ArduinoJson BH1750 Adafruit Unified Sensor - DHT sensor library \ No newline at end of file + DHT sensor library + +build_flags = -D USE_ESPASYNCWEBSERVER \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 7e5915f..f28695e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #define I2C_SDA 25 #define I2C_SCL 26 @@ -22,16 +23,13 @@ const char thingName[] = "hiGrow"; const char wifiInitialApPassword[] = "test1234"; DNSServer dnsServer; -WebServer server(80); +AsyncWebServer server(80); IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword); -BH1750 lightMeter(0x23); //0x23 - +BH1750 lightMeter; DHT_Unified dht(DHT12_PIN, DHT11); void wifiConnected(); -void handleRoot(); -void handleValues(); uint16_t soil = 0; uint32_t salt = 0; @@ -43,10 +41,9 @@ float lux = 0; /** * Handle web requests to "/" path. */ -void handleRoot() -{ +void handleRoot(AsyncWebServerRequest *request){ // -- Let IotWebConf test and handle captive portal requests. - if (iotWebConf.handleCaptivePortal()) { + if (iotWebConf.handleCaptivePortal(request)) { // -- Captive portal request were already served. return; } @@ -55,10 +52,10 @@ void handleRoot() s += F("Go to configure page to change settings."); s += F("\n"); - server.send(200, "text/html", s); + request->send(200, "text/html", s); } -void handleValues() +void handleValues(AsyncWebServerRequest *request) { StaticJsonDocument<200> doc; doc[F("temperature")] = temperature; @@ -71,7 +68,7 @@ void handleValues() String output; serializeJson(doc, output); - server.send(200, "application/json", output); + request->send(200, "application/json", output); } void wifiConnected() @@ -121,10 +118,13 @@ void setup() iotWebConf.setWifiConnectionCallback(&wifiConnected); iotWebConf.init(); - server.on("/", handleRoot); - server.on("/config", [] { iotWebConf.handleConfig(); }); - server.on("/values", handleValues); - server.onNotFound([]() { iotWebConf.handleNotFound(); }); + iotWebConf.doLoop(); + + server.on("/", HTTP_GET, handleRoot); + server.on("/config", HTTP_GET, [](AsyncWebServerRequest *request) { iotWebConf.handleConfig(request); }); + server.on("/values", HTTP_GET, handleValues); + server.onNotFound([](AsyncWebServerRequest *request) { iotWebConf.handleNotFound(request); }); + server.begin(); Wire.begin(I2C_SDA, I2C_SCL); dht.begin();