Explorar o código

Merge branch 'rolling' into master

CaCO3 %!s(int64=2) %!d(string=hai) anos
pai
achega
6642f6f995
Modificáronse 72 ficheiros con 10249 adicións e 1439 borrados
  1. 21 12
      .github/workflows/build.yaml
  2. 2 0
      .gitignore
  3. 47 2
      Changelog.md
  4. 3 0
      FeatureRequest.md
  5. 9 8
      code/components/jomjol_controlGPIO/server_GPIO.cpp
  6. 12 9
      code/components/jomjol_controlcamera/ClassControllCamera.cpp
  7. 40 39
      code/components/jomjol_fileserver_ota/server_file.cpp
  8. 41 32
      code/components/jomjol_fileserver_ota/server_ota.cpp
  9. 37 20
      code/components/jomjol_flowcontroll/ClassFlowControll.cpp
  10. 2 0
      code/components/jomjol_flowcontroll/ClassFlowControll.h
  11. 28 14
      code/components/jomjol_flowcontroll/ClassFlowMQTT.cpp
  12. 32 1
      code/components/jomjol_helper/Helper.cpp
  13. 6 0
      code/components/jomjol_helper/Helper.h
  14. 166 0
      code/components/jomjol_helper/sdcard_check.cpp
  15. 11 0
      code/components/jomjol_helper/sdcard_check.h
  16. 148 0
      code/components/jomjol_helper/statusled.cpp
  17. 34 0
      code/components/jomjol_helper/statusled.h
  18. 34 19
      code/components/jomjol_logfile/ClassLogFile.cpp
  19. 1 1
      code/components/jomjol_logfile/ClassLogFile.h
  20. 24 12
      code/components/jomjol_mqtt/interface_mqtt.cpp
  21. 1 1
      code/components/jomjol_mqtt/interface_mqtt.h
  22. 301 0
      code/components/jomjol_mqtt/mqtt_outbox.c
  23. 66 0
      code/components/jomjol_mqtt/mqtt_outbox.h
  24. 129 56
      code/components/jomjol_mqtt/server_mqtt.cpp
  25. 2 1
      code/components/jomjol_mqtt/server_mqtt.h
  26. 129 65
      code/components/jomjol_tfliteclass/server_tflite.cpp
  27. 1 0
      code/components/jomjol_tfliteclass/server_tflite.h
  28. 17 2
      code/components/jomjol_time_sntp/time_sntp.cpp
  29. 1 0
      code/components/jomjol_time_sntp/time_sntp.h
  30. 368 257
      code/components/jomjol_wlan/connect_wlan.cpp
  31. 8 7
      code/components/jomjol_wlan/connect_wlan.h
  32. 191 167
      code/components/jomjol_wlan/read_wlanini.cpp
  33. 13 1
      code/components/jomjol_wlan/read_wlanini.h
  34. 18 10
      code/include/defines.h
  35. 385 267
      code/main/main.cpp
  36. 9 5
      code/main/server_main.cpp
  37. 77 39
      code/main/softAP.cpp
  38. 17 1
      code/sdkconfig.defaults
  39. BIN=BIN
      sd-card/config/ana-class100_0154_s1_q.tflite
  40. BIN=BIN
      sd-card/config/ana-class100_0157_s1_q.tflite
  41. BIN=BIN
      sd-card/config/ana-cont_11.3.1_s2.tflite
  42. BIN=BIN
      sd-card/config/ana-cont_1105_s2_q.tflite
  43. 3 2
      sd-card/config/config.ini
  44. BIN=BIN
      sd-card/config/dig-class100-0150_s2_q.tflite
  45. BIN=BIN
      sd-card/config/dig-cont_0600_s3.tflite
  46. BIN=BIN
      sd-card/config/dig-cont_0610_s3.tflite
  47. BIN=BIN
      sd-card/config/dig-cont_0610_s3_q.tflite
  48. BIN=BIN
      sd-card/config/dig-cont_0611_s3.tflite
  49. BIN=BIN
      sd-card/config/dig-cont_0611_s3_q.tflite
  50. 12 8
      sd-card/html/data.html
  51. 1 1
      sd-card/html/edit_alignment.html
  52. 1 1
      sd-card/html/edit_analog.html
  53. 169 266
      sd-card/html/edit_config_param.html
  54. 1 1
      sd-card/html/edit_digits.html
  55. 1 1
      sd-card/html/edit_reference.html
  56. 1 0
      sd-card/html/github.min.css
  57. 148 64
      sd-card/html/graph.html
  58. BIN=BIN
      sd-card/html/help.png
  59. 13 4
      sd-card/html/index.html
  60. 6883 0
      sd-card/html/mkdocs_theme.css
  61. 187 0
      sd-card/html/mkdocs_theme_extra.css
  62. 0 6
      sd-card/html/plotly-2.14.0.min.js
  63. 7 0
      sd-card/html/plotly-basic-2.18.2.min.js
  64. 24 12
      sd-card/html/prevalue_set.html
  65. 33 13
      sd-card/html/readconfigparam.js
  66. 38 12
      sd-card/wlan.ini
  67. 79 0
      tools/parameter-tooltip-generator/generate-param-doc-tooltips.py
  68. 15 0
      tools/parameter-tooltip-generator/generate-param-doc-tooltips.sh
  69. 1 0
      tools/parameter-tooltip-generator/html/css/github.min.css
  70. 1 0
      tools/parameter-tooltip-generator/html/css/readme.md
  71. 9 0
      tools/parameter-tooltip-generator/html/css/theme.css
  72. 191 0
      tools/parameter-tooltip-generator/html/css/theme_extra.css

+ 21 - 12
.github/workflows/build.yaml

@@ -70,15 +70,24 @@ jobs:
       #run: echo "Testing... ${{ github.ref_name }}, ${{ steps.vars.outputs.sha_short }}" > ./sd-card/html/version.txt; mkdir -p ./code/.pio/build/esp32cam/; cd ./code/.pio/build/esp32cam/; echo "${{ steps.vars.outputs.sha_short }}" > firmware.bin; cp firmware.bin partitions.bin; cp firmware.bin bootloader.bin # Testing
       run: cd code; platformio run --environment esp32cam
 
-    - name: Prepare Web UI (copy data from repo and update hashes in all files)
+    - name: Prepare Web UI (copy data from repo, generate tooltip pages and update hashes in all files)
       run: |
         rm -rf ./html
         mkdir html
-        cp ./sd-card/html/* ./html/
+        cp -r ./sd-card/html/* ./html/
+
+        python -m pip install markdown
+        mkdir html/param-tooltips
+        cd tools/parameter-tooltip-generator
+        bash generate-param-doc-tooltips.sh
+        cd ../..
+
+        cp -r ./sd-card/html/* ./html/
+
+        echo "Replacing variables..."
         cd html; find . -type f -exec sed -i 's/$COMMIT_HASH/${{ steps.vars.outputs.sha_short }}/g' {} \;
         
 
-
 #########################################################################################
 ## Pack for Update
 #########################################################################################
@@ -86,7 +95,7 @@ jobs:
     # New OTA concept
     # update__version.zip file with following content:
     #  - /firmware.bin
-    #  - (optional) /html/*
+    #  - (optional) /html/* (inkl. subfolders)
     #  - (optional) /config/*.tfl
     runs-on: ubuntu-latest
     needs: build
@@ -149,7 +158,7 @@ jobs:
     # New Remote Setup concept
     # remote_setup__version.zip file with following content:
     #  - /firmware.bin
-    #  - /html/*
+    #  - /html/* (inkl. subfolders)
     #  - /config/*
     runs-on: ubuntu-latest
     needs: build
@@ -334,13 +343,13 @@ jobs:
 #        changelogPath: Changelog.md
 #        version: ${{ steps.get_version.outputs.version-without-v }}
             
-    # the release notes will be extracted from changelog 
-    - name: Extract release notes
-      id: extract-release-notes
-      if: startsWith(github.ref, 'refs/tags/') 
-      uses: ffurrer2/extract-release-notes@v1
-      with:
-          changelog_file: Changelog.md
+#    # the release notes will be extracted from changelog 
+#    - name: Extract release notes
+#      id: extract-release-notes
+#      if: startsWith(github.ref, 'refs/tags/') 
+#      uses: ffurrer2/extract-release-notes@v1
+#      with:
+#          changelog_file: Changelog.md
 
     # Releases should only be created on master by tagging the last commit.
     # all artifacts in firmware folder pushed to the release

+ 2 - 0
.gitignore

@@ -23,3 +23,5 @@ CTestTestfile.cmake
 _deps
 code/edgeAI.code-workspace
 .DS_Store
+tools/parameter-tooltip-generator/html
+tools/parameter-tooltip-generator/AI-on-the-edge-device-docs

+ 47 - 2
Changelog.md

@@ -1,11 +1,54 @@
 ## [Unreleased]
 
+xxx
+
+### Update Procedure
+
+Update Procedure see [online documentation](https://jomjol.github.io/AI-on-the-edge-device-docs/Installation/#update-ota-over-the-air)
+
+:bangbang: Afterwards you should force-reload the Web Interface (usually Ctrl-F5 will do it).
+
+### Changes
+
+This release only migrates some parameters, see #2023 for details and a list of all parameter changes.
+The parameter migration happens automatically on the next startup. No user interaction is required.
+A backup of the config is stored on the SD-card as `config.bak`.
+
+Beside of the parameter change and the bugfix listed below, no changes are contained in this release!
+
+If you want to revert back to `v14` or earlier, you will have to revert the migration changes in `config.ini` manually!
+
+#### Added
+
+-   Additional interface to InfluxDB Version 2 upwards
+-   Updated the Hybrid CNN network to `dig-cont_0610_s3`
+-   :bangbang:  Update Camera driver: contrast, brightness and saturation now working
+   
+    :bangbang:  **Attention**: this can effect old version as well, because there not all settings were effective!
+
+#### Changed
+
+-   [#2023](https://github.com/jomjol/AI-on-the-edge-device/pull/2023) Migrated Parameters
+-   Removed old `Topic` parameter, it is not used anymore
+
+#### Fixed
+-  n.a.
+
+#### Removed
+
+-   n.a.
+
+
+## [15.0.3] - 2023-02-28
+
 **Parameter Migration**
 
 ### Update Procedure
 
 Update Procedure see [online documentation](https://jomjol.github.io/AI-on-the-edge-device-docs/Installation/#update-ota-over-the-air)
 
+:bangbang: Afterwards you should force-reload the Web Interface (usually Ctrl-F5 will do it).
+
 ### Changes
 
 This release only migrates some parameters, see #2023 for details and a list of all parameter changes.
@@ -29,12 +72,14 @@ If you want to revert back to `v14` or earlier, you will have to revert the migr
 
 -   [#2036](https://github.com/jomjol/AI-on-the-edge-device/issues/2036) Fix wrong url-encoding
 -   **NEW v15.0.2:**  [#1933](https://github.com/jomjol/AI-on-the-edge-device/issues/1933) Bugfix InfluxDB Timestamp
+-   **NEW v15.0.3:**  Re-added lost dropdownbox filling for Postprocessing Individual Parameters
 
 #### Removed
 
 -   n.a.
 
-## [14.0.3] - 2023-02-05
+
+## [14.0.3] -2023-02-05
 
 **Stabilization and Improved User Experience**
 
@@ -838,7 +883,7 @@ External Illumination
 -   Initial Version
 
 
-[15.0.1]: https://github.com/jomjol/AI-on-the-edge-device/compare/v14.0.3...v15.0.1
+[15.0.3]: https://github.com/jomjol/AI-on-the-edge-device/compare/v14.0.3...v15.0.3
 [14.0.3]: https://github.com/jomjol/AI-on-the-edge-device/compare/v13.0.8...v14.0.3
 [13.0.8]: https://github.com/jomjol/AI-on-the-edge-device/compare/v12.0.1...v13.0.8
 [13.0.7]: https://github.com/jomjol/AI-on-the-edge-device/compare/v12.0.1...v13.0.7

+ 3 - 0
FeatureRequest.md

@@ -10,6 +10,9 @@
    
 
 ____
+#### #36 Run demo without camera
+Demo mode requires a working camera (if not, one receives a 'Cam bad' error). Would be nice to demo or play around on other ESP32 boards (or on ESP32-CAM boards when you broke the camera cable...).
+
 #### #35 Use the same model, but provide the image from a Smartphone Camera
 as reading the Electricity or Water meter every few minutues only delivers apparent accuracy (DE: "Scheingenauigkeit") you could just as well take a picture with your Smartphone evey so often (e.g. once a week when you are in the Basement anyway), then with some "semi clever" tricks pass this image to the model developed here, and the values then on to who ever needs them e.g. via MQTT.
 IMO: It is not needed to have that many readings (datapoints) as our behaviour (Use of electricity or water) doesn't vary that much, say, over a weeks time. The interpolation between weekly readings will give sufficient information on the power and/or water usage. 

+ 9 - 8
code/components/jomjol_controlGPIO/server_GPIO.cpp

@@ -82,10 +82,10 @@ static void gpioHandlerTask(void *arg) {
 
 void GpioPin::gpioInterrupt(int value) {
 #ifdef ENABLE_MQTT    
-    if (_mqttTopic != "") {
+    if (_mqttTopic.compare("") != 0) {
         ESP_LOGD(TAG, "gpioInterrupt %s %d", _mqttTopic.c_str(), value);
 
-        MQTTPublish(_mqttTopic, value ? "true" : "false");        
+        MQTTPublish(_mqttTopic, value ? "true" : "false", 1);        
     }
 #endif //ENABLE_MQTT
     currentState = value;
@@ -115,7 +115,7 @@ void GpioPin::init()
     }
 
 #ifdef ENABLE_MQTT
-    if ((_mqttTopic != "") && ((_mode == GPIO_PIN_MODE_OUTPUT) || (_mode == GPIO_PIN_MODE_OUTPUT_PWM) || (_mode == GPIO_PIN_MODE_BUILT_IN_FLASH_LED))) {
+    if ((_mqttTopic.compare("") != 0) && ((_mode == GPIO_PIN_MODE_OUTPUT) || (_mode == GPIO_PIN_MODE_OUTPUT_PWM) || (_mode == GPIO_PIN_MODE_BUILT_IN_FLASH_LED))) {
         std::function<bool(std::string, char*, int)> f = std::bind(&GpioPin::handleMQTT, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
         MQTTregisterSubscribeFunction(_mqttTopic, f);
     }
@@ -141,8 +141,8 @@ void GpioPin::setValue(bool value, gpio_set_source setSource, std::string* error
         gpio_set_level(_gpio, value);
 
 #ifdef ENABLE_MQTT
-        if ((_mqttTopic != "") && (setSource != GPIO_SET_SOURCE_MQTT)) {
-            MQTTPublish(_mqttTopic, value ? "true" : "false");
+        if ((_mqttTopic.compare("") != 0) && (setSource != GPIO_SET_SOURCE_MQTT)) {
+            MQTTPublish(_mqttTopic, value ? "true" : "false", 1);
         }
 #endif //ENABLE_MQTT
     }
@@ -153,7 +153,8 @@ void GpioPin::publishState() {
     if (newState != currentState) {
         ESP_LOGD(TAG,"publish state of GPIO %d new state %d", _gpio, newState);
 #ifdef ENABLE_MQTT
-        MQTTPublish(_mqttTopic, newState ? "true" : "false");
+    if (_mqttTopic.compare("") != 0)
+        MQTTPublish(_mqttTopic, newState ? "true" : "false", 1);
 #endif //ENABLE_MQTT
         currentState = newState;
     }
@@ -359,9 +360,9 @@ bool GpioHandler::readConfig()
             gpio_int_type_t intType = resolveIntType(toLower(splitted[2]));
             uint16_t dutyResolution = (uint8_t)atoi(splitted[3].c_str());
 #ifdef ENABLE_MQTT 
-            bool mqttEnabled = toLower(splitted[4]) == "true";
+            bool mqttEnabled = (toLower(splitted[4]) == "true");
 #endif // ENABLE_MQTT
-            bool httpEnabled = toLower(splitted[5]) == "true";
+            bool httpEnabled = (toLower(splitted[5]) == "true");
             char gpioName[100];
             if (splitted.size() >= 7) {
                 strcpy(gpioName, trim(splitted[6]).c_str());

+ 12 - 9
code/components/jomjol_controlcamera/ClassControllCamera.cpp

@@ -7,6 +7,7 @@
 #include "esp_log.h"
 
 #include "Helper.h"
+#include "statusled.h"
 #include "CImageBasis.h"
 
 #include "server_ota.h"
@@ -557,15 +558,17 @@ void CCamera::LightOnOff(bool status)
 
 void CCamera::LEDOnOff(bool status)
 {
-	// Init the GPIO
-    gpio_pad_select_gpio(BLINK_GPIO);
-    /* Set the GPIO as a push/pull output */
-    gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT);  
-
-    if (!status)  
-        gpio_set_level(BLINK_GPIO, 1);
-    else
-        gpio_set_level(BLINK_GPIO, 0);      
+	if (xHandle_task_StatusLED == NULL) {
+        // Init the GPIO
+        gpio_pad_select_gpio(BLINK_GPIO);
+        /* Set the GPIO as a push/pull output */
+        gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT);  
+
+        if (!status)  
+            gpio_set_level(BLINK_GPIO, 1);
+        else
+            gpio_set_level(BLINK_GPIO, 0);   
+    }
 }
 
 

+ 40 - 39
code/components/jomjol_fileserver_ota/server_file.cpp

@@ -228,7 +228,7 @@ static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath, const
             if (chunksize > 0){
                 if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
                 fclose(fd);
-                ESP_LOGE(TAG, "File sending failed!");
+                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File sending failed!");
                 return ESP_FAIL;
                 }
             }
@@ -267,7 +267,7 @@ static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath, const
             strlcpy(entrypath + dirpath_len, entry->d_name, sizeof(entrypath) - dirpath_len);
             ESP_LOGD(TAG, "Entrypath: %s", entrypath);
             if (stat(entrypath, &entry_stat) == -1) {
-                ESP_LOGE(TAG, "Failed to stat %s: %s", entrytype, entry->d_name);
+                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to stat " + string(entrytype) + ": " + string(entry->d_name));
                 continue;
             }
 
@@ -357,7 +357,7 @@ static esp_err_t send_datafile(httpd_req_t *req, bool send_full_file)
 
     fd = fopen(currentfilename.c_str(), "r");
     if (!fd) {
-        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to read file: " + std::string(currentfilename) +"!");
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to read file: " + currentfilename + "!");
         /* Respond with 404 Error */
         httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, get404());
         return ESP_FAIL;
@@ -373,7 +373,7 @@ static esp_err_t send_datafile(httpd_req_t *req, bool send_full_file)
 
         /* Adapted from https://www.geeksforgeeks.org/implement-your-own-tail-read-last-n-lines-of-a-huge-file/ */
         if (fseek(fd, 0, SEEK_END)) {
-            ESP_LOGE(TAG, "Failed to get to end of file!");
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to get to end of file!");
             return ESP_FAIL;
         }
         else {
@@ -381,7 +381,7 @@ static esp_err_t send_datafile(httpd_req_t *req, bool send_full_file)
             ESP_LOGI(TAG, "File contains %ld bytes", pos);
 
             if (fseek(fd, pos - std::min((long)LOGFILE_LAST_PART_BYTES, pos), SEEK_SET)) { // Go LOGFILE_LAST_PART_BYTES bytes back from EOF
-                ESP_LOGE(TAG, "Failed to go back %ld bytes within the file!", std::min((long)LOGFILE_LAST_PART_BYTES, pos));
+                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to go back " + to_string(std::min((long)LOGFILE_LAST_PART_BYTES, pos)) + " bytes within the file!");
                 return ESP_FAIL;
             }
         }
@@ -404,7 +404,7 @@ static esp_err_t send_datafile(httpd_req_t *req, bool send_full_file)
         /* Send the buffer contents as HTTP response chunk */
         if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
             fclose(fd);
-            ESP_LOGE(TAG, "File sending failed!");
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File sending failed!");
             /* Abort sending file */
             httpd_resp_sendstr_chunk(req, NULL);
             /* Respond with 500 Internal Server Error */
@@ -443,7 +443,7 @@ static esp_err_t send_logfile(httpd_req_t *req, bool send_full_file)
 
     fd = fopen(currentfilename.c_str(), "r");
     if (!fd) {
-        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to read file: " + std::string(currentfilename.c_str()) +"!");
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to read file: " + currentfilename + "!");
         /* Respond with 404 Error */
         httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, get404());
         return ESP_FAIL;
@@ -459,7 +459,7 @@ static esp_err_t send_logfile(httpd_req_t *req, bool send_full_file)
 
         /* Adapted from https://www.geeksforgeeks.org/implement-your-own-tail-read-last-n-lines-of-a-huge-file/ */
         if (fseek(fd, 0, SEEK_END)) {
-            ESP_LOGE(TAG, "Failed to get to end of file!");
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to get to end of file!");
             return ESP_FAIL;
         }
         else {
@@ -467,7 +467,7 @@ static esp_err_t send_logfile(httpd_req_t *req, bool send_full_file)
             ESP_LOGI(TAG, "File contains %ld bytes", pos);
 
             if (fseek(fd, pos - std::min((long)LOGFILE_LAST_PART_BYTES, pos), SEEK_SET)) { // Go LOGFILE_LAST_PART_BYTES bytes back from EOF
-                ESP_LOGE(TAG, "Failed to go back %ld bytes within the file!", std::min((long)LOGFILE_LAST_PART_BYTES, pos));
+                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to go back " + to_string(std::min((long)LOGFILE_LAST_PART_BYTES, pos)) + " bytes within the file!");
                 return ESP_FAIL;
             }
         }
@@ -490,7 +490,7 @@ static esp_err_t send_logfile(httpd_req_t *req, bool send_full_file)
         /* Send the buffer contents as HTTP response chunk */
         if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
             fclose(fd);
-            ESP_LOGE(TAG, "File sending failed!");
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File sending failed!");
             /* Abort sending file */
             httpd_resp_sendstr_chunk(req, NULL);
             /* Respond with 500 Internal Server Error */
@@ -530,7 +530,7 @@ static esp_err_t download_get_handler(httpd_req_t *req)
 
 
     if (!filename) {
-        ESP_LOGE(TAG, "Filename is too long");
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Filename is too long");
         /* Respond with 414 Error */
         httpd_resp_send_err(req, HTTPD_414_URI_TOO_LONG, "Filename too long");
         return ESP_FAIL;
@@ -571,7 +571,7 @@ static esp_err_t download_get_handler(httpd_req_t *req)
 
     fd = fopen(filepath, "r");
     if (!fd) {
-        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to read file: " + std::string(filepath) +"!");
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to read file: " + std::string(filepath) + "!");
         /* Respond with 404 Error */
         httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, get404());
         return ESP_FAIL;
@@ -592,7 +592,7 @@ static esp_err_t download_get_handler(httpd_req_t *req)
         /* Send the buffer contents as HTTP response chunk */
         if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
             fclose(fd);
-            ESP_LOGE(TAG, "File sending failed!");
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File sending failed!");
             /* Abort sending file */
             httpd_resp_sendstr_chunk(req, NULL);
             /* Respond with 500 Internal Server Error */
@@ -634,14 +634,14 @@ static esp_err_t upload_post_handler(httpd_req_t *req)
 
     /* Filename cannot have a trailing '/' */
     if (filename[strlen(filename) - 1] == '/') {
-        ESP_LOGE(TAG, "Invalid filename: %s", filename);
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Invalid filename: " + string(filename));
         /* Respond with 400 Bad Request */
         httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid filename");
         return ESP_FAIL;
     }
 
     if (stat(filepath, &file_stat) == 0) {
-        ESP_LOGE(TAG, "File already exists: %s", filepath);
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File already exists: " + string(filepath));
         /* Respond with 400 Bad Request */
         httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "File already exists");
         return ESP_FAIL;
@@ -649,7 +649,7 @@ static esp_err_t upload_post_handler(httpd_req_t *req)
 
     /* File cannot be larger than a limit */
     if (req->content_len > MAX_FILE_SIZE) {
-        ESP_LOGE(TAG, "File too large: %d bytes", req->content_len);
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File too large: " + to_string(req->content_len) + " bytes");
         /* Respond with 400 Bad Request */
         httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
                             "File size must be less than "
@@ -661,7 +661,7 @@ static esp_err_t upload_post_handler(httpd_req_t *req)
 
     fd = fopen(filepath, "w");
     if (!fd) {
-        ESP_LOGE(TAG, "Failed to create file: %s", filepath);
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to create file: " + string(filepath));
         /* Respond with 500 Internal Server Error */
         httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create file");
         return ESP_FAIL;
@@ -692,7 +692,7 @@ static esp_err_t upload_post_handler(httpd_req_t *req)
             fclose(fd);
             unlink(filepath);
 
-            ESP_LOGE(TAG, "File reception failed!");
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File reception failed!");
             /* Respond with 500 Internal Server Error */
             httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file");
             return ESP_FAIL;
@@ -705,7 +705,7 @@ static esp_err_t upload_post_handler(httpd_req_t *req)
             fclose(fd);
             unlink(filepath);
 
-            ESP_LOGE(TAG, "File write failed!");
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File write failed!");
             /* Respond with 500 Internal Server Error */
             httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write file to storage");
             return ESP_FAIL;
@@ -780,7 +780,7 @@ static esp_err_t delete_post_handler(httpd_req_t *req)
         
         if (httpd_query_key_value(_query, "task", _valuechar, 30) == ESP_OK)
         {
-            ESP_LOGD(TAG, "task is found: %s", _valuechar);
+            LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "task is found: " + string(_valuechar));
             _task = std::string(_valuechar);
         }
     }
@@ -826,26 +826,26 @@ static esp_err_t delete_post_handler(httpd_req_t *req)
 
         /* Filename cannot have a trailing '/' */
         if (filename[strlen(filename) - 1] == '/') {
-            ESP_LOGE(TAG, "Invalid filename: %s", filename);
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Invalid filename: " + string(filename));
             /* Respond with 400 Bad Request */
             httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid filename");
             return ESP_FAIL;
         }
 
         if (strcmp(filename, "wlan.ini") == 0) {
-            ESP_LOGE(TAG, "Trying to delete protected file : %s", filename);
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to delete protected file : " + string(filename));
             httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "Not allowed to delete wlan.ini");
             return ESP_FAIL;
         }
 
         if (stat(filepath, &file_stat) == -1) {
-            ESP_LOGE(TAG, "File does not exist: %s", filename);
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File does not exist: " + string(filename));
             /* Respond with 400 Bad Request */
             httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "File does not exist");
             return ESP_FAIL;
         }
 
-        ESP_LOGI(TAG, "Deleting file: %s", filename);
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Deleting file: " + string(filename));
         /* Delete file */
         unlink(filepath);
 
@@ -887,7 +887,7 @@ void delete_all_in_directory(std::string _directory)
     std::string filename;
 
     if (!dir) {
-        ESP_LOGE(TAG, "Failed to stat dir: %s", _directory.c_str());
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to stat dir: " + _directory);
         return;
     }
 
@@ -896,7 +896,7 @@ void delete_all_in_directory(std::string _directory)
         if (!(entry->d_type == DT_DIR)){
             if (strcmp("wlan.ini", entry->d_name) != 0){                    // auf wlan.ini soll nicht zugegriffen werden !!!
                 filename = _directory + "/" + std::string(entry->d_name);
-                ESP_LOGI(TAG, "Deleting file: %s", filename.c_str());
+                LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Deleting file: " + filename);
                 /* Delete file */
                 unlink(filename.c_str());    
             }
@@ -930,7 +930,7 @@ std::string unzip_new(std::string _in_zip_file, std::string _target_zip, std::st
 
     // Get and print information about each file in the archive.
     int numberoffiles = (int)mz_zip_reader_get_num_files(&zip_archive);
-    ESP_LOGI(TAG, "Numbers of files to be extracted: %d", numberoffiles);
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Numbers of files to be extracted: " + to_string(numberoffiles));
 
     sort_iter = 0;
     {
@@ -953,7 +953,7 @@ std::string unzip_new(std::string _in_zip_file, std::string _target_zip, std::st
             p = mz_zip_reader_extract_file_to_heap(&zip_archive, archive_filename, &uncomp_size, 0);
                 if (!p)
                 {
-                    ESP_LOGE(TAG, "mz_zip_reader_extract_file_to_heap() failed on file %s", archive_filename);
+                    LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "mz_zip_reader_extract_file_to_heap() failed on file " + string(archive_filename));
                     mz_zip_reader_end(&zip_archive);
                     return ret;
                 }
@@ -1015,21 +1015,22 @@ std::string unzip_new(std::string _in_zip_file, std::string _target_zip, std::st
                 else
                 {
                     isokay = false;
-                    ESP_LOGD(TAG, "ERROR in writting extracted file (function fwrite) extracted file \"%s\", size %u", archive_filename, (uint)uncomp_size);
+                    LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "ERROR in writting extracted file (function fwrite) extracted file \"" +
+                            string(archive_filename) + "\", size " + to_string(uncomp_size));
                 }
 
                 DeleteFile(zw);
                 if (!isokay)
-                    ESP_LOGE(TAG, "ERROR in fwrite \"%s\", size %u", archive_filename, (uint)uncomp_size);
+                    LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "ERROR in fwrite \"" + string(archive_filename) + "\", size " + to_string(uncomp_size));
                 isokay = isokay && RenameFile(filename_zw, zw);
                 if (!isokay)
-                    ESP_LOGE(TAG, "ERROR in Rename \"%s\" to \"%s\"", filename_zw.c_str(), zw.c_str());
+                    LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "ERROR in Rename \"" + filename_zw + "\" to \"" + zw);
 
                 if (isokay)
-                    ESP_LOGI(TAG, "Successfully extracted file \"%s\", size %u", archive_filename, (uint)uncomp_size);
+                    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Successfully extracted file \"" + string(archive_filename) + "\", size " + to_string(uncomp_size));
                 else
                 {
-                    ESP_LOGE(TAG, "ERROR in extracting file \"%s\", size %u", archive_filename, (uint)uncomp_size);
+                    LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "ERROR in extracting file \"" + string(archive_filename) + "\", size " + to_string(uncomp_size));
                     ret = "ERROR";
                 }
                 mz_free(p);
@@ -1063,7 +1064,7 @@ void unzip(std::string _in_zip_file, std::string _target_directory){
     status = mz_zip_reader_init_file(&zip_archive, _in_zip_file.c_str(), 0);
     if (!status)
     {
-        ESP_LOGD(TAG, "mz_zip_reader_init_file() failed!");
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "mz_zip_reader_init_file() failed!");
         return;
     }
 
@@ -1075,7 +1076,7 @@ void unzip(std::string _in_zip_file, std::string _target_directory){
         status = mz_zip_reader_init_file(&zip_archive, _in_zip_file.c_str(), sort_iter ? MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY : 0);
         if (!status)
         {
-            ESP_LOGD(TAG, "mz_zip_reader_init_file() failed!");
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "mz_zip_reader_init_file() failed!");
             return;
         }
 
@@ -1089,7 +1090,7 @@ void unzip(std::string _in_zip_file, std::string _target_directory){
             p = mz_zip_reader_extract_file_to_heap(&zip_archive, archive_filename, &uncomp_size, 0);
             if (!p)
             {
-                ESP_LOGD(TAG, "mz_zip_reader_extract_file_to_heap() failed!");
+                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "mz_zip_reader_extract_file_to_heap() failed!");
                 mz_zip_reader_end(&zip_archive);
                 return;
             }
@@ -1125,19 +1126,19 @@ void register_server_file_uri(httpd_handle_t server, const char *base_path)
     /* Validate file storage base path */
     if (!base_path) {
 //    if (!base_path || strcmp(base_path, "/spiffs") != 0) {
-        ESP_LOGE(TAG, "File server base_path not set");
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File server base_path not set");
 //        return ESP_ERR_INVALID_ARG;
     }
 
     if (server_data) {
-        ESP_LOGE(TAG, "File server already started");
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File server already started");
 //        return ESP_ERR_INVALID_STATE;
     }
 
     /* Allocate memory for server data */
     server_data = (file_server_data *) calloc(1, sizeof(struct file_server_data));
     if (!server_data) {
-        ESP_LOGE(TAG, "Failed to allocate memory for server data");
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to allocate memory for server data");
 //        return ESP_ERR_NO_MEM;
     }
     strlcpy(server_data->base_path, base_path,

+ 41 - 32
code/components/jomjol_fileserver_ota/server_ota.cpp

@@ -39,6 +39,7 @@
 #include "ClassLogFile.h"
 
 #include "Helper.h"
+#include "statusled.h"
 #include "../../include/defines.h"
 
 /*an ota data write buffer ready to write to the flash*/
@@ -56,9 +57,9 @@ bool initial_setup = false;
 static void infinite_loop(void)
 {
     int i = 0;
-    ESP_LOGI(TAG, "When a new firmware is available on the server, press the reset button to download it");
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "When a new firmware is available on the server, press the reset button to download it");
     while(1) {
-        ESP_LOGI(TAG, "Waiting for a new firmware... %d", ++i);
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Waiting for a new firmware... (" + to_string(++i) + ")");
         vTaskDelay(1000 / portTICK_PERIOD_MS);
     }
 }
@@ -66,6 +67,8 @@ static void infinite_loop(void)
 
 void task_do_Update_ZIP(void *pvParameter)
 {
+    StatusLED(AP_OR_OTA, 1, true);  // Signaling an OTA update
+    
     std::string filetype = toUpper(getFileType(_file_name_update));
 
   	LogFile.WriteToFile(ESP_LOG_INFO, TAG, "File: " + _file_name_update + " Filetype: " + filetype);
@@ -87,13 +90,13 @@ void task_do_Update_ZIP(void *pvParameter)
             ota_update_task(retfirmware);
         }
 
-        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Trigger reboot due to firmware update.");
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Trigger reboot due to firmware update");
         doRebootOTA();
     } else if (filetype == "BIN")
     {
        	LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Do firmware update - file: " + _file_name_update);
         ota_update_task(_file_name_update);
-        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Trigger reboot due to firmware update.");
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Trigger reboot due to firmware update");
         doRebootOTA();
     }
     else
@@ -108,7 +111,7 @@ void CheckUpdate()
  	FILE *pfile;
     if ((pfile = fopen("/sdcard/update.txt", "r")) == NULL)
     {
-		LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "No update triggered.");
+		LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "No pending update");
         return;
 	}
 
@@ -120,13 +123,13 @@ void CheckUpdate()
 		std::string _szw = std::string(zw);
         if (_szw == "init")
         {
-       		LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Inital Setup triggered.");
-            initial_setup = true;        }
+       		LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Inital Setup triggered");
+        }
 	}
 
     fclose(pfile);
     DeleteFile("/sdcard/update.txt");   // Prevent Boot Loop!!!
-	LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Update during boot triggered - Update File: " + _file_name_update);
+	LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Start update process (" + _file_name_update + ")");
 
 
     xTaskCreate(&task_do_Update_ZIP, "task_do_Update_ZIP", configMINIMAL_STACK_SIZE * 35, NULL, tskIDLE_PRIORITY+1, NULL);
@@ -148,10 +151,10 @@ static bool ota_update_task(std::string fn)
     const esp_partition_t *configured = esp_ota_get_boot_partition();
     const esp_partition_t *running = esp_ota_get_running_partition();
 
-    if (configured != running) {
-        ESP_LOGW(TAG, "Configured OTA boot partition at offset 0x%08x, but running from offset 0x%08x",
-                 configured->address, running->address);
-        ESP_LOGW(TAG, "(This can happen if either the OTA boot data or preferred boot image become somehow corrupted.)");
+    if (configured != running) {        
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Configured OTA boot partition at offset " + to_string(configured->address) + 
+                ", but running from offset " + to_string(running->address));
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "(This can happen if either the OTA boot data or preferred boot image become somehow corrupted.)");
     }
     ESP_LOGI(TAG, "Running partition type %d subtype %d (offset 0x%08x)",
              running->type, running->subtype, running->address);
@@ -179,7 +182,7 @@ static bool ota_update_task(std::string fn)
 
     while (data_read > 0) {
         if (data_read < 0) {
-            ESP_LOGE(TAG, "Error: SSL data read error");
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Error: SSL data read error");
             return false;
         } else if (data_read > 0) {
             if (image_header_was_checked == false) {
@@ -203,16 +206,17 @@ static bool ota_update_task(std::string fn)
                     // check current version with last invalid partition
                     if (last_invalid_app != NULL) {
                         if (memcmp(invalid_app_info.version, new_app_info.version, sizeof(new_app_info.version)) == 0) {
-                            ESP_LOGW(TAG, "New version is the same as invalid version.");
-                            ESP_LOGW(TAG, "Previously, there was an attempt to launch the firmware with %s version, but it failed.", invalid_app_info.version);
-                            ESP_LOGW(TAG, "The firmware has been rolled back to the previous version.");
+                            LogFile.WriteToFile(ESP_LOG_WARN, TAG, "New version is the same as invalid version");
+                            LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Previously, there was an attempt to launch the firmware with " + 
+                                    string(invalid_app_info.version) + " version, but it failed");
+                            LogFile.WriteToFile(ESP_LOG_WARN, TAG, "The firmware has been rolled back to the previous version");
                             infinite_loop();
                         }
                     }
 
 /*
                     if (memcmp(new_app_info.version, running_app_info.version, sizeof(new_app_info.version)) == 0) {
-                        ESP_LOGW(TAG, "Current running version is the same as a new. We will not continue the update.");
+                        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Current running version is the same as a new. We will not continue the update");
                         infinite_loop();
                     }
 */
@@ -220,12 +224,12 @@ static bool ota_update_task(std::string fn)
 
                     err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
                     if (err != ESP_OK) {
-                        ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err));
+                        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_ota_begin failed (" + string(esp_err_to_name(err)) + ")");
                         return false;
                     }
                     ESP_LOGI(TAG, "esp_ota_begin succeeded");
                 } else {
-                    ESP_LOGE(TAG, "received package is not fit len");
+                    LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "received package is not fit len");
                     return false;
                 }
             }            
@@ -241,7 +245,7 @@ static bool ota_update_task(std::string fn)
            // * `errno` to check for underlying transport connectivity closure if any
            //
             if (errno == ECONNRESET || errno == ENOTCONN) {
-                ESP_LOGE(TAG, "Connection closed, errno = %d", errno);
+                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Connection closed, errno = " + to_string(errno));
                 break;
             }
         }
@@ -254,15 +258,15 @@ static bool ota_update_task(std::string fn)
     err = esp_ota_end(update_handle);
     if (err != ESP_OK) {
         if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
-            ESP_LOGE(TAG, "Image validation failed, image is corrupted");
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Image validation failed, image is corrupted");
         }
-        ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err));
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_ota_end failed (" + string(esp_err_to_name(err)) + ")!");
         return false;
     }
 
     err = esp_ota_set_boot_partition(update_partition);
     if (err != ESP_OK) {
-        ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (%s)!", esp_err_to_name(err));
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_ota_set_boot_partition failed (" + string(esp_err_to_name(err)) + ")!");
 
     }
 //    ESP_LOGI(TAG, "Prepare to restart system!");
@@ -329,7 +333,7 @@ void CheckOTAUpdate(void)
                         ESP_LOGI(TAG, "Diagnostics completed successfully! Continuing execution...");
                         esp_ota_mark_app_valid_cancel_rollback();
                     } else {
-                        ESP_LOGE(TAG, "Diagnostics failed! Start rollback to the previous version...");
+                        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Diagnostics failed! Start rollback to the previous version...");
                         esp_ota_mark_app_invalid_rollback_and_reboot();
                     }
                 }
@@ -353,7 +357,7 @@ void CheckOTAUpdate(void)
                 ESP_LOGI(TAG, "Diagnostics completed successfully! Continuing execution...");
                 esp_ota_mark_app_valid_cancel_rollback();
             } else {
-                ESP_LOGE(TAG, "Diagnostics failed! Start rollback to the previous version...");
+                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Diagnostics failed! Start rollback to the previous version...");
                 esp_ota_mark_app_invalid_rollback_and_reboot();
             }
         }
@@ -445,7 +449,7 @@ esp_err_t handler_ota_update(httpd_req_t *req)
         if ((filetype == "ZIP") || (filetype == "BIN"))
         {
            	FILE *pfile;
-            LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Update for reboot.");
+            LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Update for reboot");
             pfile = fopen("/sdcard/update.txt", "w");
             fwrite(fn.c_str(), fn.length(), 1, pfile);
             fclose(pfile);
@@ -522,7 +526,7 @@ esp_err_t handler_ota_update(httpd_req_t *req)
         }
         else
         {
-            ESP_LOGD(TAG, "File does not exist: %s", fn.c_str());
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File does not exist: " + fn);
         }
         /* Respond with an empty chunk to signal HTTP response completion */
         std::string zw = "file deleted\n";
@@ -537,7 +541,7 @@ esp_err_t handler_ota_update(httpd_req_t *req)
     httpd_resp_send(req, zw.c_str(), strlen(zw.c_str())); 
     httpd_resp_send_chunk(req, NULL, 0);  
 
-    ESP_LOGE(TAG, "ota without parameter - should not be the case!");
+    LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "ota without parameter - should not be the case!");
 
 /*  
     const char* resp_str;    
@@ -586,6 +590,9 @@ void task_reboot(void *KillAutoFlow)
         KillTFliteTasks();  // Kill autoflow task if executed in extra task, if not don't kill parent task
     }
 
+    Camera.LightOnOff(false);
+    StatusLEDOff();
+
     /* Stop service tasks */
     #ifdef ENABLE_MQTT
         MQTTdestroy_client(true);
@@ -600,20 +607,20 @@ void task_reboot(void *KillAutoFlow)
     vTaskDelay(5000 / portTICK_PERIOD_MS);
     hard_restart();     // Reset type: System reset (Triggered by watchdog), if esp_restart stalls (WDT needs to be activated)
 
-    ESP_LOGE(TAG, "Reboot failed!");
+    LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Reboot failed!");
     vTaskDelete(NULL); //Delete this task if it comes to this point
 }
 
 
 void doReboot()
 {
-    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Reboot triggered by Software (5s).");
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Reboot triggered by Software (5s)");
     LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Reboot in 5sec");
 
     BaseType_t xReturned = xTaskCreate(&task_reboot, "task_reboot", configMINIMAL_STACK_SIZE * 3, (void*) true, 10, NULL);
     if( xReturned != pdPASS )
     {
-        ESP_LOGE(TAG, "task_reboot not created -> force reboot without killing flow");
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "task_reboot not created -> force reboot without killing flow");
         task_reboot((void*) false);
     }
     vTaskDelay(10000 / portTICK_PERIOD_MS); // Prevent serving web client fetch response until system is shuting down
@@ -624,6 +631,8 @@ void doRebootOTA()
 {
     LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Reboot in 5sec");
 
+    Camera.LightOnOff(false);
+    StatusLEDOff();
     esp_camera_deinit();
 
     vTaskDelay(5000 / portTICK_PERIOD_MS);
@@ -641,7 +650,7 @@ esp_err_t handler_reboot(httpd_req_t *req)
     #endif    
 
     LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "handler_reboot");
-    ESP_LOGI(TAG, "!!! System will restart within 5 sec!!!");
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "!!! System will restart within 5 sec!!!");
 
     std::string response = 
         "<html><head><script>"

+ 37 - 20
code/components/jomjol_flowcontroll/ClassFlowControll.cpp

@@ -25,6 +25,7 @@ extern "C" {
 #endif //ENABLE_MQTT
 
 #include "server_help.h"
+#include "server_tflite.h"
 #include "../../include/defines.h"
 
 static const char* TAG = "CTRL";
@@ -195,6 +196,7 @@ void ClassFlowControll::SetInitialParameter(void)
     disabled = false;
     aktRunNr = 0;
     aktstatus = "Flow task not yet created";
+    aktstatusWithTime = aktstatus;
 }
 
 
@@ -272,8 +274,10 @@ ClassFlow* ClassFlowControll::CreateClassFlow(std::string _type)
 void ClassFlowControll::InitFlow(std::string config)
 {
     aktstatus = "Initialization";
+    aktstatusWithTime = aktstatus;
+
     //#ifdef ENABLE_MQTT
-        //MQTTPublish(mqttServer_getMainTopic() + "/" + "status", "Initialization", false); // Right now, not possible -> MQTT Service is going to be started later
+        //MQTTPublish(mqttServer_getMainTopic() + "/" + "status", "Initialization", 1, false); // Right now, not possible -> MQTT Service is going to be started later
     //#endif //ENABLE_MQTT
     
     string line;
@@ -318,6 +322,12 @@ void ClassFlowControll::InitFlow(std::string config)
 }
 
 
+std::string* ClassFlowControll::getActStatusWithTime()
+{
+    return &aktstatusWithTime;
+}
+
+
 std::string* ClassFlowControll::getActStatus()
 {
     return &aktstatus;
@@ -327,6 +337,7 @@ std::string* ClassFlowControll::getActStatus()
 void ClassFlowControll::setActStatus(std::string _aktstatus)
 {
     aktstatus = _aktstatus;
+    aktstatusWithTime = aktstatus;
 }
 
 
@@ -338,10 +349,10 @@ void ClassFlowControll::doFlowTakeImageOnly(string time)
     {
         if (FlowControll[i]->name() == "ClassFlowTakeImage") {
             zw_time = getCurrentTimeString("%H:%M:%S");
-            std::string flowStatus = TranslateAktstatus(FlowControll[i]->name());
-            aktstatus = flowStatus + " (" + zw_time + ")";
+            aktstatus = TranslateAktstatus(FlowControll[i]->name());
+            aktstatusWithTime = aktstatus + " (" + zw_time + ")";
             #ifdef ENABLE_MQTT
-                MQTTPublish(mqttServer_getMainTopic() + "/" + "status", flowStatus, false);
+                MQTTPublish(mqttServer_getMainTopic() + "/" + "status", aktstatus, 1, false);
             #endif //ENABLE_MQTT
 
             FlowControll[i]->doFlow(time);
@@ -355,6 +366,7 @@ bool ClassFlowControll::doFlow(string time)
     bool result = true;
     std::string zw_time;
     int repeat = 0;
+    int qos = 1;
 
     #ifdef DEBUG_DETAIL_ON 
         LogFile.WriteHeapInfo("ClassFlowControll::doFlow - Start");
@@ -371,11 +383,11 @@ bool ClassFlowControll::doFlow(string time)
     for (int i = 0; i < FlowControll.size(); ++i)
     {
         zw_time = getCurrentTimeString("%H:%M:%S");
-        std::string flowStatus = TranslateAktstatus(FlowControll[i]->name());
-        aktstatus = flowStatus + " (" + zw_time + ")";
-        //LogFile.WriteToFile(ESP_LOG_INFO, TAG, aktstatus);
+        aktstatus = TranslateAktstatus(FlowControll[i]->name());
+        aktstatusWithTime = aktstatus + " (" + zw_time + ")";
+        //LogFile.WriteToFile(ESP_LOG_INFO, TAG, aktstatusWithTime);
         #ifdef ENABLE_MQTT
-            MQTTPublish(mqttServer_getMainTopic() + "/" + "status", flowStatus, false);
+            MQTTPublish(mqttServer_getMainTopic() + "/" + "status", aktstatus, qos, false);
         #endif //ENABLE_MQTT
 
         #ifdef DEBUG_DETAIL_ON
@@ -405,11 +417,11 @@ bool ClassFlowControll::doFlow(string time)
     }
 
     zw_time = getCurrentTimeString("%H:%M:%S");
-    std::string flowStatus = "Flow finished";
-    aktstatus = flowStatus + " (" + zw_time + ")";
-    //LogFile.WriteToFile(ESP_LOG_INFO, TAG, aktstatus);
+    aktstatus = "Flow finished";
+    aktstatusWithTime = aktstatus + " (" + zw_time + ")";
+    //LogFile.WriteToFile(ESP_LOG_INFO, TAG, aktstatusWithTime);
     #ifdef ENABLE_MQTT
-        MQTTPublish(mqttServer_getMainTopic() + "/" + "status", flowStatus, false);
+        MQTTPublish(mqttServer_getMainTopic() + "/" + "status", aktstatus, qos, false);
     #endif //ENABLE_MQTT
 
     return result;
@@ -589,6 +601,10 @@ bool ClassFlowControll::ReadParameter(FILE* pfile, string& aktparamgraph)
             {
                 LogFile.setLogLevel(ESP_LOG_DEBUG);
             }
+
+            /* If system reboot was not triggered by user and reboot was caused by execption -> keep log level to DEBUG */
+            if (!getIsPlannedReboot() && (esp_reset_reason() == ESP_RST_PANIC))
+                LogFile.setLogLevel(ESP_LOG_DEBUG);
         }
         if ((toUpper(splitted[0]) == "LOGFILESRETENTION") && (splitted.size() > 1))
         {
@@ -596,19 +612,22 @@ bool ClassFlowControll::ReadParameter(FILE* pfile, string& aktparamgraph)
         }
 
         /* TimeServer and TimeZone got already read from the config, see setupTime () */
-
+        
+        #if (defined WLAN_USE_ROAMING_BY_SCANNING || (defined WLAN_USE_MESH_ROAMING && defined WLAN_USE_MESH_ROAMING_ACTIVATE_CLIENT_TRIGGERED_QUERIES))
         if ((toUpper(splitted[0]) == "RSSITHRESHOLD") && (splitted.size() > 1))
         {
-            if (ChangeRSSIThreshold(WLAN_CONFIG_FILE, atoi(splitted[1].c_str())))
+            int RSSIThresholdTMP = atoi(splitted[1].c_str());
+            RSSIThresholdTMP = min(0, max(-100, RSSIThresholdTMP)); // Verify input limits (-100 - 0)
+            
+            if (ChangeRSSIThreshold(WLAN_CONFIG_FILE, RSSIThresholdTMP))
             {
                 // reboot necessary so that the new wlan.ini is also used !!!
                 fclose(pfile);
-                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Rebooting to activate new RSSITHRESHOLD ...");
-                esp_restart();
-                hard_restart();                   
+                LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Rebooting to activate new RSSITHRESHOLD ...");
                 doReboot();
             }
         }
+        #endif
 
         if ((toUpper(splitted[0]) == "HOSTNAME") && (splitted.size() > 1))
         {
@@ -616,9 +635,7 @@ bool ClassFlowControll::ReadParameter(FILE* pfile, string& aktparamgraph)
             {
                 // reboot necessary so that the new wlan.ini is also used !!!
                 fclose(pfile);
-                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Rebooting to activate new HOSTNAME...");
-                esp_restart();
-                hard_restart();                   
+                LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Rebooting to activate new HOSTNAME...");             
                 doReboot();
             }
         }

+ 2 - 0
code/components/jomjol_flowcontroll/ClassFlowControll.h

@@ -37,6 +37,7 @@ protected:
 	float AutoInterval;
 	bool SetupModeActive;
 	void SetInitialParameter(void);	
+	std::string aktstatusWithTime;
 	std::string aktstatus;
 	int aktRunNr;
 
@@ -71,6 +72,7 @@ public:
 
 	bool isAutoStart(long &_interval);
 
+	std::string* getActStatusWithTime();
 	std::string* getActStatus();
 	void setActStatus(std::string _aktstatus);
 

+ 28 - 14
code/components/jomjol_flowcontroll/ClassFlowMQTT.cpp

@@ -5,6 +5,7 @@
 #include "ClassFlowMQTT.h"
 #include "Helper.h"
 #include "connect_wlan.h"
+#include "read_wlanini.h"
 #include "ClassLogFile.h"
 
 #include "time_sntp.h"
@@ -31,12 +32,12 @@ void ClassFlowMQTT::SetInitialParameter(void)
     topicError = "";
     topicRate = "";
     topicTimeStamp = "";
-    maintopic = hostname;
+    maintopic = wlan_config.hostname;
 
     topicUptime = "";
     topicFreeMem = "";
 
-    clientname = "AIOTED-" + getMac();
+    clientname = wlan_config.hostname;
 
     OldValue = "";
     flowpostprocessing = NULL;  
@@ -155,6 +156,9 @@ bool ClassFlowMQTT::ReadParameter(FILE* pfile, string& aktparamgraph)
             else if (toUpper(splitted[1]) == "ENERGY_MWH") {
                 mqttServer_setMeterType("energy", "MWh", "h", "MW");
             }
+            else if (toUpper(splitted[1]) == "ENERGY_GJ") {
+                mqttServer_setMeterType("energy", "GJ", "h", "GJ/h");
+            }
         }
 
         if ((toUpper(splitted[0]) == "CLIENTID") && (splitted.size() > 1))
@@ -165,7 +169,6 @@ bool ClassFlowMQTT::ReadParameter(FILE* pfile, string& aktparamgraph)
         if (((toUpper(splitted[0]) == "TOPIC") || (toUpper(splitted[0]) == "MAINTOPIC")) && (splitted.size() > 1))
         {
             maintopic = splitted[1];
-            mqttServer_setMainTopic(maintopic);
         }
     }
 
@@ -174,6 +177,8 @@ bool ClassFlowMQTT::ReadParameter(FILE* pfile, string& aktparamgraph)
      * How ever we need the interval parameter from the ClassFlowControll, but that only gets started later.
      * To work around this, we delay the start and trigger it from ClassFlowControll::ReadParameter() */
 
+    mqttServer_setMainTopic(maintopic);
+
     return true;
 }
 
@@ -209,6 +214,7 @@ bool ClassFlowMQTT::Start(float AutoInterval)
 
 bool ClassFlowMQTT::doFlow(string zwtime)
 {
+    bool success;
     std::string result;
     std::string resulterror = "";
     std::string resultraw = "";
@@ -219,8 +225,12 @@ bool ClassFlowMQTT::doFlow(string zwtime)
     std::string resultchangabs = "";
     string zw = "";
     string namenumber = "";
+    int qos = 1;
+
+    /* Send the the Homeassistant Discovery and the Static Topics in case they where scheduled */
+    sendDiscovery_and_static_Topics();
 
-    publishSystemData();
+    success = publishSystemData(qos);
 
     if (flowpostprocessing && getMQTTisConnected())
     {
@@ -246,13 +256,13 @@ bool ClassFlowMQTT::doFlow(string zwtime)
 
 
             if (result.length() > 0)   
-                MQTTPublish(namenumber + "value", result, SetRetainFlag);
+                success |= MQTTPublish(namenumber + "value", result, qos, SetRetainFlag);
 
             if (resulterror.length() > 0)  
-                MQTTPublish(namenumber + "error", resulterror, SetRetainFlag);
+                success |= MQTTPublish(namenumber + "error", resulterror, qos, SetRetainFlag);
 
             if (resultrate.length() > 0) {
-                MQTTPublish(namenumber + "rate", resultrate, SetRetainFlag);
+                success |= MQTTPublish(namenumber + "rate", resultrate, qos, SetRetainFlag);
                 
                 std::string resultRatePerTimeUnit;
                 if (getTimeUnit() == "h") { // Need conversion to be per hour
@@ -261,22 +271,22 @@ bool ClassFlowMQTT::doFlow(string zwtime)
                 else { // Keep per minute
                     resultRatePerTimeUnit = resultrate;
                 }
-                MQTTPublish(namenumber + "rate_per_time_unit", resultRatePerTimeUnit, SetRetainFlag);
+                success |= MQTTPublish(namenumber + "rate_per_time_unit", resultRatePerTimeUnit, qos, SetRetainFlag);
             }
 
             if (resultchangabs.length() > 0) {
-                MQTTPublish(namenumber + "changeabsolut", resultchangabs, SetRetainFlag); // Legacy API
-                MQTTPublish(namenumber + "rate_per_digitalization_round", resultchangabs, SetRetainFlag);
+                success |= MQTTPublish(namenumber + "changeabsolut", resultchangabs, qos, SetRetainFlag); // Legacy API
+                success |= MQTTPublish(namenumber + "rate_per_digitalization_round", resultchangabs, qos, SetRetainFlag);
             }
 
             if (resultraw.length() > 0)   
-                MQTTPublish(namenumber + "raw", resultraw, SetRetainFlag);
+                success |= MQTTPublish(namenumber + "raw", resultraw, qos, SetRetainFlag);
 
             if (resulttimestamp.length() > 0)
-                MQTTPublish(namenumber + "timestamp", resulttimestamp, SetRetainFlag);
+                success |= MQTTPublish(namenumber + "timestamp", resulttimestamp, qos, SetRetainFlag);
 
             std::string json = flowpostprocessing->getJsonFromNumber(i, "\n");
-            MQTTPublish(namenumber + "json", json, SetRetainFlag);
+            success |= MQTTPublish(namenumber + "json", json, qos, SetRetainFlag);
         }
     }
     
@@ -294,10 +304,14 @@ bool ClassFlowMQTT::doFlow(string zwtime)
     //                 result = result + "\t" + zw;
     //         }
     //     }
-    //     MQTTPublish(topic, result, SetRetainFlag);
+    //     success |= MQTTPublish(topic, result, qos, SetRetainFlag);
     // }
     
     OldValue = result;
+
+    if (!success) {
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "One or more MQTT topics failed to be published!");
+    }
     
     return true;
 }

+ 32 - 1
code/components/jomjol_helper/Helper.cpp

@@ -258,7 +258,7 @@ bool MakeDir(std::string path)
                 break;
 				
             default:
-				LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to create folder: " + path);
+				LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to create folder: " + path + " (errno: " + std::to_string(errno) + ")");
                 bSuccess = false;
                 break;
         }
@@ -963,3 +963,34 @@ std::string UrlDecode(const std::string& value)
 
     return result;
 }
+
+
+bool replaceString(std::string& s, std::string const& toReplace, std::string const& replaceWith) {
+    return replaceString(s, toReplace, replaceWith, true);
+}
+
+
+bool replaceString(std::string& s, std::string const& toReplace, std::string const& replaceWith, bool logIt) {
+    std::size_t pos = s.find(toReplace);
+
+    if (pos == std::string::npos) { // Not found
+        return false;
+    }
+
+    std::string old = s;
+    s.replace(pos, toReplace.length(), replaceWith);
+    if (logIt) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Migrated Configfile line '" + old + "' to '" + s + "'");
+    }
+    return true;
+}
+
+
+bool isInString(std::string& s, std::string const& toFind) {
+    std::size_t pos = s.find(toFind);
+
+    if (pos == std::string::npos) { // Not found
+        return false;
+    }
+    return true;
+}

+ 6 - 0
code/components/jomjol_helper/Helper.h

@@ -76,6 +76,8 @@ enum SystemStatusFlag_t {          // One bit per error
     SYSTEM_STATUS_PSRAM_BAD         = 1 << 0, //  1, Critical Error
     SYSTEM_STATUS_HEAP_TOO_SMALL    = 1 << 1, //  2, Critical Error
     SYSTEM_STATUS_CAM_BAD           = 1 << 2, //  4, Critical Error
+    SYSTEM_STATUS_SDCARD_CHECK_BAD  = 1 << 3, //  8, Critical Error
+    SYSTEM_STATUS_FOLDER_CHECK_BAD  = 1 << 4, //  16, Critical Error
 
     // Second Byte
     SYSTEM_STATUS_CAM_FB_BAD        = 1 << (0+8), //  8, Flow still might work
@@ -95,4 +97,8 @@ const char* get404(void);
 
 std::string UrlDecode(const std::string& value);
 
+bool replaceString(std::string& s, std::string const& toReplace, std::string const& replaceWith);
+bool replaceString(std::string& s, std::string const& toReplace, std::string const& replaceWith, bool logIt);
+bool isInString(std::string& s, std::string const& toFind);
+
 #endif //HELPER_H

+ 166 - 0
code/components/jomjol_helper/sdcard_check.cpp

@@ -0,0 +1,166 @@
+#include "sdcard_check.h"
+#include <string.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <sys/stat.h>
+
+#include "esp_rom_crc.h" 
+#include "ClassLogFile.h"
+
+static const char *TAG = "SDCARD";
+
+int SDCardCheckRW(void)
+{
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Basic R/W check started...");
+    FILE* pFile = NULL;
+    int iCRCMessage = 0;
+   
+    pFile = fopen("/sdcard/sdcheck.txt","w");
+    if (pFile == NULL) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Basic R/W check: (E1) No able to open file to write");
+        return -1;
+    } 
+    else {
+        std::string sMessage = "This message is used for a SD-Card basic check!";
+        iCRCMessage = esp_rom_crc16_le(0, (uint8_t*)sMessage.c_str(), sMessage.length());
+        if (fwrite(sMessage.c_str(), sMessage.length(), 1, pFile) == 0 ) {
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Basic R/W check: (E2) Not able to write file");
+            fclose(pFile);
+            unlink("/sdcard/sdcheck.txt");
+            return -2;
+        }
+        fclose(pFile); 
+    }
+
+    pFile = fopen("/sdcard/sdcheck.txt","r");
+    if (pFile == NULL) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Basic R/W check: (E3) Not able to open file to read back");
+        unlink("/sdcard/sdcheck.txt");
+        return -3;
+    } 
+    else {
+        char cReadBuf[50];
+        if (fgets(cReadBuf, sizeof(cReadBuf), pFile) == 0) {
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Basic R/W check: (E4) Not able to read file back");
+            fclose(pFile);
+            unlink("/sdcard/sdcheck.txt");
+            return -4;
+        }
+        else {
+            if (esp_rom_crc16_le(0, (uint8_t*)cReadBuf, strlen(cReadBuf)) != iCRCMessage) {                 
+                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Basic R/W check: (E5) Read back, but wrong CRC");
+                fclose(pFile);
+                unlink("/sdcard/sdcheck.txt");
+                return -5;
+            }
+        }      
+        fclose(pFile);
+    }
+
+    if (unlink("/sdcard/sdcheck.txt") != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Basic R/W check: (E6) Unable to delete the file");
+        return -6;
+    }
+
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Basic R/W check successful");
+    return 0;
+}
+
+
+bool SDCardCheckFolderFilePresence()
+{
+    struct stat sb;
+    bool bRetval = true;
+
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Folder/file presence check started...");
+    /* check if folder exists: config */
+    if (stat("/sdcard/config", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: Folder /config not found");
+        bRetval = false;
+    }
+
+    /* check if folder exists: html */
+    if (stat("/sdcard/html", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: Folder /html not found");
+        bRetval = false;
+    }
+
+    /* check if folder exists: firmware */
+    if (stat("/sdcard/firmware", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: Folder /firmware not found");
+        bRetval = false;
+    }
+
+    /* check if folder exists: img_tmp */
+    if (stat("/sdcard/img_tmp", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: Folder /img_tmp not found");
+        bRetval = false;
+    }
+
+    /* check if folder exists: log */
+    if (stat("/sdcard/log", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: Folder /log not found");
+        bRetval = false;
+    }
+
+    /* check if folder exists: demo */
+    if (stat("/sdcard/demo", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: Folder /demo not found");
+        bRetval = false;
+    }
+
+    /* check if file exists: wlan.ini */
+    if (stat("/sdcard/wlan.ini", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: File /wlan.ini not found");
+        bRetval = false;
+    }
+
+    /* check if file exists: config.ini */
+    if (stat("/sdcard/config/config.ini", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: File /config/config.ini not found");
+        bRetval = false;
+    }
+
+    /* check if file exists: index.html */
+    if (stat("/sdcard/html/index.html", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: File /html/index.html not found");
+        bRetval = false;
+    }
+
+    /* check if file exists: ota.html */
+    if (stat("/sdcard/html/ota_page.html", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: File /html/ota.html not found");
+        bRetval = false;
+    }
+
+    /* check if file exists: log.html */
+    if (stat("/sdcard/html/log.html", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: File /html/log.html not found");
+        bRetval = false;
+    }
+
+    /* check if file exists: common.js */
+    if (stat("/sdcard/html/common.js", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: File /html/common.js not found");
+        bRetval = false;
+    }
+
+    /* check if file exists: gethost.js */
+    if (stat("/sdcard/html/gethost.js", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: File /html/gethost.js not found");
+        bRetval = false;
+    }
+
+    /* check if file exists: version.txt */
+    if (stat("/sdcard/html/version.txt", &sb) != 0) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Folder/file check: File /html/version.txt not found");
+        bRetval = false;
+    }
+
+    if (bRetval)
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Folder/file presence check successful");
+    
+    return bRetval;
+}

+ 11 - 0
code/components/jomjol_helper/sdcard_check.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#ifndef COMPONENTS_HELPER_SDCARD_CHECK_H
+#define COMPONENTS_HELPER_SDCARD_CHECK_H
+
+#include "../../include/defines.h"
+
+int SDCardCheckRW(void);
+bool SDCardCheckFolderFilePresence(void);
+
+#endif /* COMPONENTS_HELPER_SDCARD_CHECK_H */

+ 148 - 0
code/components/jomjol_helper/statusled.cpp

@@ -0,0 +1,148 @@
+#include "statusled.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "driver/gpio.h"
+
+#include "ClassLogFile.h"
+#include "../../include/defines.h"
+
+
+static const char* TAG = "STATUSLED";
+
+TaskHandle_t xHandle_task_StatusLED = NULL;
+struct StatusLEDData StatusLEDData = {};
+
+
+void task_StatusLED(void *pvParameter)
+{
+    //ESP_LOGD(TAG, "task_StatusLED - create");
+	while (StatusLEDData.bProcessingRequest) 
+	{
+		//ESP_LOGD(TAG, "task_StatusLED - start");
+		struct StatusLEDData StatusLEDDataInt = StatusLEDData;
+
+		gpio_pad_select_gpio(BLINK_GPIO); // Init the GPIO
+		gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT); // Set the GPIO as a push/pull output
+		gpio_set_level(BLINK_GPIO, 1);// LED off
+
+		for (int i=0; i<2; ) // Default: repeat 2 times
+		{
+			if (!StatusLEDDataInt.bInfinite)
+				++i;
+
+			for (int j = 0; j < StatusLEDDataInt.iSourceBlinkCnt; ++j)
+			{
+				gpio_set_level(BLINK_GPIO, 0);
+				vTaskDelay(StatusLEDDataInt.iBlinkTime / portTICK_PERIOD_MS);
+				gpio_set_level(BLINK_GPIO, 1);      
+				vTaskDelay(StatusLEDDataInt.iBlinkTime / portTICK_PERIOD_MS);
+			}
+
+			vTaskDelay(500 / portTICK_PERIOD_MS);	// Delay between module code and error code
+
+			for (int j = 0; j < StatusLEDDataInt.iCodeBlinkCnt; ++j)
+			{
+				gpio_set_level(BLINK_GPIO, 0);      
+				vTaskDelay(StatusLEDDataInt.iBlinkTime / portTICK_PERIOD_MS);
+				gpio_set_level(BLINK_GPIO, 1);
+				vTaskDelay(StatusLEDDataInt.iBlinkTime / portTICK_PERIOD_MS);
+			}
+			vTaskDelay(1500 / portTICK_PERIOD_MS);	// Delay to signal new round
+		}
+
+		StatusLEDData.bProcessingRequest = false;
+		//ESP_LOGD(TAG, "task_StatusLED - done/wait");
+		vTaskDelay(10000 / portTICK_PERIOD_MS);	// Wait for an upcoming request otherwise continue and delete task to save memory
+	}
+	//ESP_LOGD(TAG, "task_StatusLED - delete");
+	xHandle_task_StatusLED = NULL;
+    vTaskDelete(NULL); // Delete this task due to no request
+}
+
+
+void StatusLED(StatusLedSource _eSource, int _iCode, bool _bInfinite)
+{
+	//ESP_LOGD(TAG, "StatusLED - start");
+
+    if (_eSource == WLAN_CONN) {
+		StatusLEDData.iSourceBlinkCnt = WLAN_CONN;
+		StatusLEDData.iCodeBlinkCnt = _iCode;
+		StatusLEDData.iBlinkTime = 250;
+		StatusLEDData.bInfinite = _bInfinite;
+	}
+	else if (_eSource == WLAN_INIT) {
+		StatusLEDData.iSourceBlinkCnt = WLAN_INIT;
+		StatusLEDData.iCodeBlinkCnt = _iCode;
+		StatusLEDData.iBlinkTime = 250;
+		StatusLEDData.bInfinite = _bInfinite;
+	}
+	else if (_eSource == SDCARD_INIT) {
+		StatusLEDData.iSourceBlinkCnt = SDCARD_INIT;
+		StatusLEDData.iCodeBlinkCnt = _iCode;
+		StatusLEDData.iBlinkTime = 250;
+		StatusLEDData.bInfinite = _bInfinite;
+	}
+	else if (_eSource == SDCARD_CHECK) {
+		StatusLEDData.iSourceBlinkCnt = SDCARD_CHECK;
+		StatusLEDData.iCodeBlinkCnt = _iCode;
+		StatusLEDData.iBlinkTime = 250;
+		StatusLEDData.bInfinite = _bInfinite;
+	}
+    else if (_eSource == CAM_INIT) {
+		StatusLEDData.iSourceBlinkCnt = CAM_INIT;
+		StatusLEDData.iCodeBlinkCnt = _iCode;
+		StatusLEDData.iBlinkTime = 250;
+		StatusLEDData.bInfinite = _bInfinite;
+	}
+    else if (_eSource == PSRAM_INIT) {
+		StatusLEDData.iSourceBlinkCnt = PSRAM_INIT;
+		StatusLEDData.iCodeBlinkCnt = _iCode;
+		StatusLEDData.iBlinkTime = 250;
+		StatusLEDData.bInfinite = _bInfinite;
+	}
+	else if (_eSource == TIME_CHECK) {
+		StatusLEDData.iSourceBlinkCnt = TIME_CHECK;
+		StatusLEDData.iCodeBlinkCnt = _iCode;
+		StatusLEDData.iBlinkTime = 250;
+		StatusLEDData.bInfinite = _bInfinite;
+	}
+	else if (_eSource == AP_OR_OTA) {
+		StatusLEDData.iSourceBlinkCnt = AP_OR_OTA;
+		StatusLEDData.iCodeBlinkCnt = _iCode;
+		StatusLEDData.iBlinkTime = 350;
+		StatusLEDData.bInfinite = _bInfinite;
+	}
+
+	if (xHandle_task_StatusLED && !StatusLEDData.bProcessingRequest) {
+		StatusLEDData.bProcessingRequest = true;
+		BaseType_t xReturned = xTaskAbortDelay(xHandle_task_StatusLED);	// Reuse still running status LED task
+		/*if (xReturned == pdPASS)
+			ESP_LOGD(TAG, "task_StatusLED - abort waiting delay");*/
+	}
+	else if (xHandle_task_StatusLED == NULL) {
+		StatusLEDData.bProcessingRequest = true;
+		BaseType_t xReturned = xTaskCreate(&task_StatusLED, "task_StatusLED", 1280, NULL, tskIDLE_PRIORITY+1, &xHandle_task_StatusLED);
+		if(xReturned != pdPASS)
+		{
+			xHandle_task_StatusLED = NULL;
+			LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "task_StatusLED failed to create");
+        	LogFile.WriteHeapInfo("task_StatusLED failed");
+		}
+	}
+	else {
+		ESP_LOGD(TAG, "task_StatusLED still processing, request skipped");	// Requests with high frequency could be skipped, but LED is only helpful for static states 
+	}
+	//ESP_LOGD(TAG, "StatusLED - done");
+}
+
+
+void StatusLEDOff(void)
+{
+	if (xHandle_task_StatusLED)
+		vTaskDelete(xHandle_task_StatusLED); // Delete task for StatusLED to force stop of blinking
+	
+	gpio_pad_select_gpio(BLINK_GPIO); // Init the GPIO
+	gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT); // Set the GPIO as a push/pull output
+	gpio_set_level(BLINK_GPIO, 1);// LED off
+}

+ 34 - 0
code/components/jomjol_helper/statusled.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#ifndef STATUSLED_H
+#define STATUSLED_H
+
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+
+
+extern TaskHandle_t xHandle_task_StatusLED;
+
+enum StatusLedSource {
+	WLAN_CONN = 1,
+    WLAN_INIT = 2,
+    SDCARD_INIT = 3,
+	SDCARD_CHECK = 4,
+    CAM_INIT = 5,
+    PSRAM_INIT = 6,
+    TIME_CHECK = 7,
+    AP_OR_OTA = 8
+};
+
+struct StatusLEDData {
+    int iSourceBlinkCnt = 1;
+    int iCodeBlinkCnt = 1;
+    int iBlinkTime = 250;
+    bool bInfinite = false;
+    bool bProcessingRequest = false;
+};
+
+void StatusLED(StatusLedSource _eSource, int _iCode, bool _bInfinite);
+void StatusLEDOff(void);
+
+#endif //STATUSLED_H

+ 34 - 19
code/components/jomjol_logfile/ClassLogFile.cpp

@@ -15,6 +15,7 @@ extern "C" {
 #endif
 
 #include "Helper.h"
+#include "time_sntp.h"
 #include "../../include/defines.h"
 
 static const char *TAG = "LOGFILE";
@@ -78,11 +79,11 @@ void ClassLogFile::WriteToData(std::string _timestamp, std::string _name, std::s
 }
 
 
-void ClassLogFile::setLogLevel(esp_log_level_t _logLevel){
-    loglevel = _logLevel;
-
+void ClassLogFile::setLogLevel(esp_log_level_t _logLevel)
+{
     std::string levelText;
 
+    // Print log level to log file
     switch(_logLevel) {            
         case ESP_LOG_WARN:
             levelText = "WARNING";
@@ -95,13 +96,16 @@ void ClassLogFile::setLogLevel(esp_log_level_t _logLevel){
         case ESP_LOG_DEBUG:
             levelText = "DEBUG";
             break;
+    
         case ESP_LOG_ERROR:
         default:
             levelText = "ERROR";
             break;
     }
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Set log level to " + levelText);
 
-    ESP_LOGI(TAG, "Log Level set to %s", levelText.c_str());
+    // set new log level
+    loglevel = _logLevel;
 
     /*
     LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Test");
@@ -318,15 +322,23 @@ void ClassLogFile::RemoveOldLogFile()
             //ESP_LOGD(TAG, "compare log file: %s to %s", entry->d_name, cmpfilename);
             if ((strlen(entry->d_name) == strlen(cmpfilename)) && (strcmp(entry->d_name, cmpfilename) < 0)) {
                 //ESP_LOGD(TAG, "delete log file: %s", entry->d_name);
-                std::string filepath = logroot + "/" + entry->d_name; 
-                if (unlink(filepath.c_str()) == 0) {
-                    deleted ++;
-                } else {
-                    ESP_LOGE(TAG, "can't delete file: %s", entry->d_name);
-                    notDeleted ++;
+                std::string filepath = logroot + "/" + entry->d_name;
+                if ((strcmp(entry->d_name, "log_1970-01-01.txt") == 0) && getTimeWasNotSetAtBoot()) { // keep logfile log_1970-01-01.txt if time was not set at boot (some boot logs are in there)
+                    //ESP_LOGD(TAG, "Skip deleting this file: %s", entry->d_name); 
+                    notDeleted++;
                 }
-            } else {
-                notDeleted ++;
+                else {          
+                    if (unlink(filepath.c_str()) == 0) {
+                        deleted++;
+                    } 
+                    else {
+                        ESP_LOGE(TAG, "can't delete file: %s", entry->d_name);
+                        notDeleted++;
+                    }
+                }
+            } 
+            else {
+                notDeleted++;
             }
         }
     }
@@ -386,14 +398,17 @@ void ClassLogFile::RemoveOldDataLog()
 }
 
 
-void ClassLogFile::CreateLogDirectories()
+bool ClassLogFile::CreateLogDirectories()
 {
-    MakeDir("/sdcard/log");
-    MakeDir("/sdcard/log/data");
-    MakeDir("/sdcard/log/analog");
-    MakeDir("/sdcard/log/digit");
-    MakeDir("/sdcard/log/message");
-    MakeDir("/sdcard/log/source");
+    bool bRetval = false;
+    bRetval = MakeDir("/sdcard/log");
+    bRetval = MakeDir("/sdcard/log/data");
+    bRetval = MakeDir("/sdcard/log/analog");
+    bRetval = MakeDir("/sdcard/log/digit");
+    bRetval = MakeDir("/sdcard/log/message");
+    bRetval = MakeDir("/sdcard/log/source");
+
+    return bRetval;
 }
 
 

+ 1 - 1
code/components/jomjol_logfile/ClassLogFile.h

@@ -35,7 +35,7 @@ public:
 
     void CloseLogFileAppendHandle();
 
-    void CreateLogDirectories();
+    bool CreateLogDirectories();
     void RemoveOldLogFile();
     void RemoveOldDataLog();
 

+ 24 - 12
code/components/jomjol_mqtt/interface_mqtt.cpp

@@ -14,6 +14,7 @@ std::map<std::string, std::function<void()>>* connectFunktionMap = NULL;
 std::map<std::string, std::function<bool(std::string, char*, int)>>* subscribeFunktionMap = NULL;
 
 int failedOnRound = -1;
+int MQTTReconnectCnt = 0;
  
 esp_mqtt_event_id_t esp_mqtt_ID = MQTT_EVENT_ANY;
 // ESP_EVENT_ANY_ID
@@ -30,7 +31,7 @@ bool SetRetainFlag;
 void (*callbackOnConnected)(std::string, bool) = NULL;
 
 
-bool MQTTPublish(std::string _key, std::string _content, bool retained_flag) 
+bool MQTTPublish(std::string _key, std::string _content, int qos, bool retained_flag) 
 {
     if (!mqtt_enabled) {                            // MQTT sevice not started / configured (MQTT_Init not called before)      
         return false;
@@ -50,7 +51,7 @@ bool MQTTPublish(std::string _key, std::string _content, bool retained_flag)
         #ifdef DEBUG_DETAIL_ON 
             long long int starttime = esp_timer_get_time();
         #endif
-        int msg_id = esp_mqtt_client_publish(client, _key.c_str(), _content.c_str(), 0, 1, retained_flag);
+        int msg_id = esp_mqtt_client_publish(client, _key.c_str(), _content.c_str(), 0, qos, retained_flag);
         #ifdef DEBUG_DETAIL_ON 
             ESP_LOGD(TAG, "Publish msg_id %d in %lld ms", msg_id, (esp_timer_get_time() - starttime)/1000);
         #endif
@@ -59,7 +60,7 @@ bool MQTTPublish(std::string _key, std::string _content, bool retained_flag)
             #ifdef DEBUG_DETAIL_ON 
                 starttime = esp_timer_get_time();
             #endif
-            msg_id = esp_mqtt_client_publish(client, _key.c_str(), _content.c_str(), 0, 1, retained_flag);
+            msg_id = esp_mqtt_client_publish(client, _key.c_str(), _content.c_str(), 0, qos, retained_flag);
             #ifdef DEBUG_DETAIL_ON 
                 ESP_LOGD(TAG, "Publish msg_id %d in %lld ms", msg_id, (esp_timer_get_time() - starttime)/1000);
             #endif
@@ -89,29 +90,39 @@ static esp_err_t mqtt_event_handler_cb(esp_mqtt_event_handle_t event) {
     std::string topic = "";
     switch (event->event_id) {
         case MQTT_EVENT_BEFORE_CONNECT:
-            ESP_LOGD(TAG, "MQTT_EVENT_BEFORE_CONNECT");
             mqtt_initialized = true;
             break;
+        
         case MQTT_EVENT_CONNECTED:
-            ESP_LOGD(TAG, "MQTT_EVENT_CONNECTED");
+            MQTTReconnectCnt = 0;
             mqtt_initialized = true;
             mqtt_connected = true;
             MQTTconnected();
             break;
+        
         case MQTT_EVENT_DISCONNECTED:
-            ESP_LOGD(TAG, "MQTT_EVENT_DISCONNECTED");
-            LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Disconnected from broker");
             mqtt_connected = false;
+            MQTTReconnectCnt++;
+            LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Disconnected, trying to reconnect");
+
+            if (MQTTReconnectCnt >= 5) {
+                MQTTReconnectCnt = 0;
+                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Disconnected, multiple reconnect attempts failed, still retrying...");
+            }
             break;
+        
         case MQTT_EVENT_SUBSCRIBED:
             ESP_LOGD(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
             break;
+        
         case MQTT_EVENT_UNSUBSCRIBED:
             ESP_LOGD(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
             break;
+        
         case MQTT_EVENT_PUBLISHED:
             ESP_LOGD(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
             break;
+        
         case MQTT_EVENT_DATA:
             ESP_LOGD(TAG, "MQTT_EVENT_DATA");
             ESP_LOGD(TAG, "TOPIC=%.*s", event->topic_len, event->topic);
@@ -126,6 +137,7 @@ static esp_err_t mqtt_event_handler_cb(esp_mqtt_event_handle_t event) {
                 ESP_LOGW(TAG, "no handler available\r\n");
             }
             break;
+        
         case MQTT_EVENT_ERROR:
             #ifdef DEBUG_DETAIL_ON 
                 ESP_LOGD(TAG, "MQTT_EVENT_ERROR - esp_mqtt_error_codes:");
@@ -136,8 +148,9 @@ static esp_err_t mqtt_event_handler_cb(esp_mqtt_event_handle_t event) {
                 ESP_LOGD(TAG, "esp_tls_stack_err:%d", event->error_handle->esp_tls_stack_err);
                 ESP_LOGD(TAG, "esp_tls_cert_verify_flags:%d", event->error_handle->esp_tls_cert_verify_flags);
             #endif
-            mqtt_connected = false;
+            //mqtt_connected = false;
             break;
+        
         default:
             ESP_LOGD(TAG, "Other event id:%d", event->event_id);
             break;
@@ -221,7 +234,7 @@ int MQTT_Init() {
         .buffer_size = 1536,                    // size of MQTT send/receive buffer (Default: 1024)
         .reconnect_timeout_ms = 15000,          // Try to reconnect to broker (Default: 10000ms)
         .network_timeout_ms = 20000,            // Network Timeout (Default: 10000ms)
-        .message_retransmit_timeout = 3000      // Tiem after message resent when broker not acknowledged (QoS1, QoS2)
+        .message_retransmit_timeout = 3000      // Time after message resent when broker not acknowledged (QoS1, QoS2)
 
     };
 
@@ -313,8 +326,7 @@ bool mqtt_handler_flow_start(std::string _topic, char* _data, int _data_len) {
 void MQTTconnected(){
     if (mqtt_connected) {
         LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Connected to broker");
-        MQTTPublish(lwt_topic, lwt_connected, true);                        // Publish "connected" to maintopic/connection
-
+        
         if (connectFunktionMap != NULL) {
             for(std::map<std::string, std::function<void()>>::iterator it = connectFunktionMap->begin(); it != connectFunktionMap->end(); ++it) {
                 it->second();
@@ -333,7 +345,7 @@ void MQTTconnected(){
             }
         }
 
-        vTaskDelay(10000 / portTICK_PERIOD_MS);                 // Delay execution of callback routine after connection got established   
+        /* Send Static Topics and Homeassistant Discovery */
         if (callbackOnConnected) {                              // Call onConnected callback routine --> mqtt_server
             callbackOnConnected(maintopic, SetRetainFlag);
         }

+ 1 - 1
code/components/jomjol_mqtt/interface_mqtt.h

@@ -15,7 +15,7 @@ bool MQTT_Configure(std::string _mqttURI, std::string _clientid, std::string _us
 int MQTT_Init();
 void MQTTdestroy_client(bool _disable);
 
-bool MQTTPublish(std::string _key, std::string _content, bool retained_flag = 1);            // retained Flag as Standart
+bool MQTTPublish(std::string _key, std::string _content, int qos, bool retained_flag = 1);            // retained Flag as Standart
 
 bool getMQTTisEnabled();
 bool getMQTTisConnected();

+ 301 - 0
code/components/jomjol_mqtt/mqtt_outbox.c

@@ -0,0 +1,301 @@
+/* This is a modification of https://github.com/espressif/esp-mqtt/blob/master/lib/mqtt_outbox.c
+ * to use the PSRAM instead of the internal heap.
+*/
+#include "mqtt_outbox.h"
+#include <stdlib.h>
+#include <string.h>
+#include "sys/queue.h"
+#include "esp_log.h"
+#include "esp_heap_caps.h"
+
+#define USE_PSRAM
+
+#ifdef CONFIG_MQTT_CUSTOM_OUTBOX
+static const char *TAG = "outbox";
+
+typedef struct outbox_item {
+    char *buffer;
+    int len;
+    int msg_id;
+    int msg_type;
+    int msg_qos;
+    outbox_tick_t tick;
+    pending_state_t pending;
+    STAILQ_ENTRY(outbox_item) next;
+} outbox_item_t;
+
+STAILQ_HEAD(outbox_list_t, outbox_item);
+
+
+outbox_handle_t outbox_init(void)
+{
+#ifdef USE_PSRAM
+    outbox_handle_t outbox = heap_caps_calloc(1, sizeof(struct outbox_list_t), MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
+#else
+    outbox_handle_t outbox = calloc(1, sizeof(struct outbox_list_t));
+#endif
+    //ESP_MEM_CHECK(TAG, outbox, return NULL);
+    STAILQ_INIT(outbox);
+    return outbox;
+}
+
+outbox_item_handle_t outbox_enqueue(outbox_handle_t outbox, outbox_message_handle_t message, outbox_tick_t tick)
+{
+#ifdef USE_PSRAM
+    outbox_item_handle_t item = heap_caps_calloc(1, sizeof(outbox_item_t), MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
+#else
+    outbox_item_handle_t item = calloc(1, sizeof(outbox_item_t));
+#endif
+    //ESP_MEM_CHECK(TAG, item, return NULL);
+    item->msg_id = message->msg_id;
+    item->msg_type = message->msg_type;
+    item->msg_qos = message->msg_qos;
+    item->tick = tick;
+    item->len =  message->len + message->remaining_len;
+    item->pending = QUEUED;
+#ifdef USE_PSRAM
+    item->buffer = heap_caps_malloc(message->len + message->remaining_len, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
+#else
+    item->buffer = malloc(message->len + message->remaining_len);
+#endif
+    /*ESP_MEM_CHECK(TAG, item->buffer, {
+        free(item);
+        return NULL;
+    });*/
+    memcpy(item->buffer, message->data, message->len);
+    if (message->remaining_data) {
+        memcpy(item->buffer + message->len, message->remaining_data, message->remaining_len);
+    }
+    STAILQ_INSERT_TAIL(outbox, item, next);
+    ESP_LOGD(TAG, "ENQUEUE msgid=%d, msg_type=%d, len=%d, size=%d", message->msg_id, message->msg_type, message->len + message->remaining_len, outbox_get_size(outbox));
+    return item;
+}
+
+outbox_item_handle_t outbox_get(outbox_handle_t outbox, int msg_id)
+{
+    outbox_item_handle_t item;
+    STAILQ_FOREACH(item, outbox, next) {
+        if (item->msg_id == msg_id) {
+            return item;
+        }
+    }
+    return NULL;
+}
+
+outbox_item_handle_t outbox_dequeue(outbox_handle_t outbox, pending_state_t pending, outbox_tick_t *tick)
+{
+    outbox_item_handle_t item;
+    STAILQ_FOREACH(item, outbox, next) {
+        if (item->pending == pending) {
+            if (tick) {
+                *tick = item->tick;
+            }
+            return item;
+        }
+    }
+    return NULL;
+}
+
+esp_err_t outbox_delete_item(outbox_handle_t outbox, outbox_item_handle_t item_to_delete)
+{
+    outbox_item_handle_t item;
+    STAILQ_FOREACH(item, outbox, next) {
+        if (item == item_to_delete) {
+            STAILQ_REMOVE(outbox, item, outbox_item, next);
+#ifdef USE_PSRAM
+            heap_caps_free(item->buffer);
+            heap_caps_free(item);
+#else
+            free(item->buffer);
+            free(item);
+#endif
+            return ESP_OK;
+        }
+    }
+    return ESP_FAIL;
+}
+
+uint8_t *outbox_item_get_data(outbox_item_handle_t item,  size_t *len, uint16_t *msg_id, int *msg_type, int *qos)
+{
+    if (item) {
+        *len = item->len;
+        *msg_id = item->msg_id;
+        *msg_type = item->msg_type;
+        *qos = item->msg_qos;
+        return (uint8_t *)item->buffer;
+    }
+    return NULL;
+}
+
+esp_err_t outbox_delete(outbox_handle_t outbox, int msg_id, int msg_type)
+{
+    outbox_item_handle_t item, tmp;
+    STAILQ_FOREACH_SAFE(item, outbox, next, tmp) {
+        if (item->msg_id == msg_id && (0xFF & (item->msg_type)) == msg_type) {
+            STAILQ_REMOVE(outbox, item, outbox_item, next);
+#ifdef USE_PSRAM
+            heap_caps_free(item->buffer);
+            heap_caps_free(item);
+#else
+            free(item->buffer);
+            free(item);
+#endif
+            ESP_LOGD(TAG, "DELETED msgid=%d, msg_type=%d, remain size=%d", msg_id, msg_type, outbox_get_size(outbox));
+            return ESP_OK;
+        }
+
+    }
+    return ESP_FAIL;
+}
+esp_err_t outbox_delete_msgid(outbox_handle_t outbox, int msg_id)
+{
+    outbox_item_handle_t item, tmp;
+    STAILQ_FOREACH_SAFE(item, outbox, next, tmp) {
+        if (item->msg_id == msg_id) {
+            STAILQ_REMOVE(outbox, item, outbox_item, next);
+#ifdef USE_PSRAM
+            heap_caps_free(item->buffer);
+            heap_caps_free(item);
+#else
+            free(item->buffer);
+            free(item);
+#endif
+        }
+
+    }
+    return ESP_OK;
+}
+esp_err_t outbox_set_pending(outbox_handle_t outbox, int msg_id, pending_state_t pending)
+{
+    outbox_item_handle_t item = outbox_get(outbox, msg_id);
+    if (item) {
+        item->pending = pending;
+        return ESP_OK;
+    }
+    return ESP_FAIL;
+}
+
+pending_state_t outbox_item_get_pending(outbox_item_handle_t item)
+{
+    if (item) {
+        return item->pending;
+    }
+    return QUEUED;
+}
+
+esp_err_t outbox_set_tick(outbox_handle_t outbox, int msg_id, outbox_tick_t tick)
+{
+    outbox_item_handle_t item = outbox_get(outbox, msg_id);
+    if (item) {
+        item->tick = tick;
+        return ESP_OK;
+    }
+    return ESP_FAIL;
+}
+
+esp_err_t outbox_delete_msgtype(outbox_handle_t outbox, int msg_type)
+{
+    outbox_item_handle_t item, tmp;
+    STAILQ_FOREACH_SAFE(item, outbox, next, tmp) {
+        if (item->msg_type == msg_type) {
+            STAILQ_REMOVE(outbox, item, outbox_item, next);
+#ifdef USE_PSRAM
+            heap_caps_free(item->buffer);
+            heap_caps_free(item);
+#else
+            free(item->buffer);
+            free(item);
+#endif
+        }
+
+    }
+    return ESP_OK;
+}
+int outbox_delete_single_expired(outbox_handle_t outbox, outbox_tick_t current_tick, outbox_tick_t timeout)
+{
+    int msg_id = -1;
+    outbox_item_handle_t item;
+    STAILQ_FOREACH(item, outbox, next) {
+        if (current_tick - item->tick > timeout) {
+            STAILQ_REMOVE(outbox, item, outbox_item, next);
+
+#ifdef USE_PSRAM
+            heap_caps_free(item->buffer);
+#else
+            free(item->buffer);
+#endif
+
+            msg_id = item->msg_id;
+
+#ifdef USE_PSRAM
+            heap_caps_free(item);
+#else
+            free(item);
+#endif
+
+            return msg_id;
+        }
+
+    }
+    return msg_id;
+}
+
+int outbox_delete_expired(outbox_handle_t outbox, outbox_tick_t current_tick, outbox_tick_t timeout)
+{
+    int deleted_items = 0;
+    outbox_item_handle_t item, tmp;
+    STAILQ_FOREACH_SAFE(item, outbox, next, tmp) {
+        if (current_tick - item->tick > timeout) {
+            STAILQ_REMOVE(outbox, item, outbox_item, next);
+#ifdef USE_PSRAM
+            heap_caps_free(item->buffer);
+            heap_caps_free(item);
+#else
+            free(item->buffer);
+            free(item);
+#endif
+            deleted_items ++;
+        }
+
+    }
+    return deleted_items;
+}
+
+int outbox_get_size(outbox_handle_t outbox)
+{
+    int siz = 0;
+    outbox_item_handle_t item;
+    STAILQ_FOREACH(item, outbox, next) {
+        // Suppressing "use after free" warning as this could happen only if queue is in inconsistent state
+        // which never happens if STAILQ interface used
+        siz += item->len; // NOLINT(clang-analyzer-unix.Malloc)
+    }
+    return siz;
+}
+
+void outbox_delete_all_items(outbox_handle_t outbox)
+{
+    outbox_item_handle_t item, tmp;
+    STAILQ_FOREACH_SAFE(item, outbox, next, tmp) {
+        STAILQ_REMOVE(outbox, item, outbox_item, next);
+#ifdef USE_PSRAM
+            heap_caps_free(item->buffer);
+            heap_caps_free(item);
+#else
+            free(item->buffer);
+            free(item);
+#endif
+    }
+}
+void outbox_destroy(outbox_handle_t outbox)
+{
+    outbox_delete_all_items(outbox);
+
+#ifdef USE_PSRAM
+    heap_caps_free(outbox);
+#else
+    free(outbox);
+#endif
+}
+
+#endif /* CONFIG_MQTT_CUSTOM_OUTBOX */

+ 66 - 0
code/components/jomjol_mqtt/mqtt_outbox.h

@@ -0,0 +1,66 @@
+/* This is an adaption of https://github.com/espressif/esp-mqtt/blob/master/lib/include/mqtt_outbox.h
+ * This file is subject to the terms and conditions defined in
+ * file 'LICENSE', which is part of this source code package.
+ * Tuan PM <tuanpm at live dot com>
+ */
+#ifndef _MQTT_OUTOBX_H_
+#define _MQTT_OUTOBX_H_
+//#include "platform.h"
+#include "esp_err.h"
+
+#ifdef  __cplusplus
+extern "C" {
+#endif
+
+struct outbox_item;
+
+typedef struct outbox_list_t *outbox_handle_t;
+typedef struct outbox_item *outbox_item_handle_t;
+typedef struct outbox_message *outbox_message_handle_t;
+typedef long long outbox_tick_t;
+
+typedef struct outbox_message {
+    uint8_t *data;
+    int len;
+    int msg_id;
+    int msg_qos;
+    int msg_type;
+    uint8_t *remaining_data;
+    int remaining_len;
+} outbox_message_t;
+
+typedef enum pending_state {
+    QUEUED,
+    TRANSMITTED,
+    ACKNOWLEDGED,
+    CONFIRMED
+} pending_state_t;
+
+outbox_handle_t outbox_init(void);
+outbox_item_handle_t outbox_enqueue(outbox_handle_t outbox, outbox_message_handle_t message, outbox_tick_t tick);
+outbox_item_handle_t outbox_dequeue(outbox_handle_t outbox, pending_state_t pending, outbox_tick_t *tick);
+outbox_item_handle_t outbox_get(outbox_handle_t outbox, int msg_id);
+uint8_t *outbox_item_get_data(outbox_item_handle_t item,  size_t *len, uint16_t *msg_id, int *msg_type, int *qos);
+esp_err_t outbox_delete(outbox_handle_t outbox, int msg_id, int msg_type);
+esp_err_t outbox_delete_msgid(outbox_handle_t outbox, int msg_id);
+esp_err_t outbox_delete_msgtype(outbox_handle_t outbox, int msg_type);
+esp_err_t outbox_delete_item(outbox_handle_t outbox, outbox_item_handle_t item);
+int outbox_delete_expired(outbox_handle_t outbox, outbox_tick_t current_tick, outbox_tick_t timeout);
+/**
+ * @brief Deletes single expired message returning it's message id
+ *
+ * @return msg id of the deleted message, -1 if no expired message in the outbox
+ */
+int outbox_delete_single_expired(outbox_handle_t outbox, outbox_tick_t current_tick, outbox_tick_t timeout);
+
+esp_err_t outbox_set_pending(outbox_handle_t outbox, int msg_id, pending_state_t pending);
+pending_state_t outbox_item_get_pending(outbox_item_handle_t item);
+esp_err_t outbox_set_tick(outbox_handle_t outbox, int msg_id, outbox_tick_t tick);
+int outbox_get_size(outbox_handle_t outbox);
+void outbox_destroy(outbox_handle_t outbox);
+void outbox_delete_all_items(outbox_handle_t outbox);
+
+#ifdef  __cplusplus
+}
+#endif
+#endif

+ 129 - 56
code/components/jomjol_mqtt/server_mqtt.cpp

@@ -7,6 +7,7 @@
 #include "esp_log.h"
 #include "ClassLogFile.h"
 #include "connect_wlan.h"
+#include "read_wlanini.h"
 #include "server_mqtt.h"
 #include "interface_mqtt.h"
 #include "time_sntp.h"
@@ -31,6 +32,7 @@ float roundInterval; // Minutes
 int keepAlive = 0; // Seconds
 bool retainFlag;
 static std::string maintopic;
+bool sendingOf_DiscoveryAndStaticTopics_scheduled = true; // Set it to true to make sure it gets sent at least once after startup
 
 
 void mqttServer_setParameter(std::vector<NumberPost*>* _NUMBERS, int _keepAlive, float _roundInterval) {
@@ -46,8 +48,9 @@ void mqttServer_setMeterType(std::string _meterType, std::string _valueUnit, std
     rateUnit = _rateUnit;
 }
 
-void sendHomeAssistantDiscoveryTopic(std::string group, std::string field,
-    std::string name, std::string icon, std::string unit, std::string deviceClass, std::string stateClass, std::string entityCategory) {
+bool sendHomeAssistantDiscoveryTopic(std::string group, std::string field,
+    std::string name, std::string icon, std::string unit, std::string deviceClass, std::string stateClass, std::string entityCategory,
+    int qos) {
     std::string version = std::string(libfive_git_version());
 
     if (version == "") {
@@ -130,26 +133,31 @@ void sendHomeAssistantDiscoveryTopic(std::string group, std::string field,
     "}"  +
     "}";
 
-    MQTTPublish(topicFull, payload, true);
+    return MQTTPublish(topicFull, payload, qos, true);
 }
 
-void MQTThomeassistantDiscovery() {  
-    if (!getMQTTisConnected()) 
-        return;
+bool MQTThomeassistantDiscovery(int qos) {  
+    bool allSendsSuccessed = false;
 
-    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "MQTT - Sending Homeassistant Discovery Topics (Meter Type: " + meterType + ", Value Unit: " + valueUnit + " , Rate Unit: " + rateUnit + ")...");
+    if (!getMQTTisConnected()) {
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Unable to send Homeassistant Discovery Topics, we are not connected to the MQTT broker!");
+        return false;
+    }
+
+    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Publishing Homeassistant Discovery topics (Meter Type: '" + meterType + "', Value Unit: '" + valueUnit + "' , Rate Unit: '" + rateUnit + "') ...");
 
-    //                              Group | Field            | User Friendly Name | Icon                      | Unit | Device Class     | State Class  | Entity Category
-    sendHomeAssistantDiscoveryTopic("",     "uptime",          "Uptime",            "clock-time-eight-outline", "s",   "",                "",            "diagnostic");
-    sendHomeAssistantDiscoveryTopic("",     "MAC",             "MAC Address",       "network-outline",          "",    "",                "",            "diagnostic");
-    sendHomeAssistantDiscoveryTopic("",     "hostname",        "Hostname",          "network-outline",          "",    "",                "",            "diagnostic");
-    sendHomeAssistantDiscoveryTopic("",     "freeMem",         "Free Memory",       "memory",                   "B",   "",                "measurement", "diagnostic");
-    sendHomeAssistantDiscoveryTopic("",     "wifiRSSI",        "Wi-Fi RSSI",        "wifi",                     "dBm", "signal_strength", "",            "diagnostic");
-    sendHomeAssistantDiscoveryTopic("",     "CPUtemp",         "CPU Temperature",   "thermometer",              "°C",  "temperature",     "measurement", "diagnostic");
-    sendHomeAssistantDiscoveryTopic("",     "interval",        "Interval",          "clock-time-eight-outline", "min",  ""           ,    "measurement", "diagnostic");
-    sendHomeAssistantDiscoveryTopic("",     "IP",              "IP",                "network-outline",           "",    "",               "",            "diagnostic");
-    sendHomeAssistantDiscoveryTopic("",     "status",          "Status",            "list-status",               "",    "",               "",            "diagnostic");
+	int aFreeInternalHeapSizeBefore = heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
 
+    //                                                   Group | Field            | User Friendly Name | Icon                      | Unit | Device Class     | State Class  | Entity Category
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "uptime",          "Uptime",            "clock-time-eight-outline", "s",   "",                "",            "diagnostic", qos);
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "MAC",             "MAC Address",       "network-outline",          "",    "",                "",            "diagnostic", qos);
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "hostname",        "Hostname",          "network-outline",          "",    "",                "",            "diagnostic", qos);
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "freeMem",         "Free Memory",       "memory",                   "B",   "",                "measurement", "diagnostic", qos);
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "wifiRSSI",        "Wi-Fi RSSI",        "wifi",                     "dBm", "signal_strength", "",            "diagnostic", qos);
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "CPUtemp",         "CPU Temperature",   "thermometer",              "°C",  "temperature",     "measurement", "diagnostic", qos);
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "interval",        "Interval",          "clock-time-eight-outline", "min",  ""           ,    "measurement", "diagnostic", qos);
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "IP",              "IP",                "network-outline",           "",    "",               "",            "diagnostic", qos);
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "status",          "Status",            "list-status",               "",    "",               "",            "diagnostic", qos);
 
 
     for (int i = 0; i < (*NUMBERS).size(); ++i) {
@@ -158,76 +166,141 @@ void MQTThomeassistantDiscovery() {
             group = "";
         }
 
-    //                                  Group | Field                 | User Friendly Name                | Icon                   | Unit     | Device Class | State Class       | Entity Category
-        sendHomeAssistantDiscoveryTopic(group,   "value",              "Value",                            "gauge",                 valueUnit, meterType,     "total_increasing", "");
-        sendHomeAssistantDiscoveryTopic(group,   "raw",                "Raw Value",                        "raw",                   valueUnit, "",            "total_increasing", "diagnostic");
-        sendHomeAssistantDiscoveryTopic(group,   "error",              "Error",                            "alert-circle-outline",  "",        "",            "",                 "diagnostic");
+    //                                                       Group | Field                          | User Friendly Name                | Icon                   | Unit                 | Device Class | State Class       | Entity Category
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "value",                      "Value",                            "gauge",                 valueUnit,             meterType,      "total_increasing", "", qos);
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "raw",                        "Raw Value",                        "raw",                   "",                    "",             "",                 "diagnostic", qos);
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "error",                      "Error",                            "alert-circle-outline",  "",                    "",             "",                 "diagnostic", qos);
         /* Not announcing "rate" as it is better to use rate_per_time_unit resp. rate_per_digitalization_round */
-        // sendHomeAssistantDiscoveryTopic(group,   "rate",               "Rate (Unit/Minute)",               "swap-vertical",         "",        "",            "",                 ""); // Legacy, always Unit per Minute
-        sendHomeAssistantDiscoveryTopic(group,   "rate_per_time_unit", "Rate (" + rateUnit + ")",          "swap-vertical",         rateUnit,  "",            "",                 "");        
-        sendHomeAssistantDiscoveryTopic(group,   "rate_per_digitalization_round",  "Change since last digitalization round", "arrow-expand-vertical", valueUnit, "",            "measurement",      ""); // correctly the Unit is Uint/Interval!
-        sendHomeAssistantDiscoveryTopic(group,   "timestamp",          "Timestamp",                  "clock-time-eight-outline", "",        "timestamp",   "",                "diagnostic");
-        sendHomeAssistantDiscoveryTopic(group,   "json",               "JSON",                       "code-json",                "",        "",            "",                 "diagnostic");
-        sendHomeAssistantDiscoveryTopic(group,   "problem",            "Problem",                    "alert-outline",            "",        "problem",            "",                 ""); // Special binary sensor which is based on error topic
+        // allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "rate",               "Rate (Unit/Minute)",               "swap-vertical",         "",        "",            "",                 ""); // Legacy, always Unit per Minute
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "rate_per_time_unit",         "Rate (" + rateUnit + ")",          "swap-vertical",         rateUnit,              "",             "measurement",      "", qos);
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "rate_per_digitalization_round",  "Change since last digitalization round",  "arrow-expand-vertical", valueUnit,  "",             "measurement",      "", qos); // correctly the Unit is Unit/Interval!
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "timestamp",                  "Timestamp",                     "clock-time-eight-outline", "",                    "timestamp",    "",                 "diagnostic", qos);
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "json",                       "JSON",                             "code-json",             "",                    "",             "",                 "diagnostic", qos);
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "problem",                    "Problem",                          "alert-outline",         "",                    "problem",      "",                 "", qos); // Special binary sensor which is based on error topic
     }
+
+    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Successfully published all Homeassistant Discovery MQTT topics");
+
+    int aFreeInternalHeapSizeAfter = heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
+    int aMinFreeInternalHeapSize =  heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
+
+    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Int. Heap Usage before Publishing Homeassistand Discovery Topics: " + 
+            to_string(aFreeInternalHeapSizeBefore) + ", after: " + to_string(aFreeInternalHeapSizeAfter) + ", delta: " + 
+            to_string(aFreeInternalHeapSizeBefore - aFreeInternalHeapSizeAfter) + ", lowest free: " + to_string(aMinFreeInternalHeapSize));
+
+    return allSendsSuccessed;
 }
 
-void publishSystemData() {
-    if (!getMQTTisConnected()) 
-        return;
+bool publishSystemData(int qos) {
+    bool allSendsSuccessed = false;
+
+    if (!getMQTTisConnected()) {
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Unable to send System Topics, we are not connected to the MQTT broker!");
+        return false;
+    }
 
     char tmp_char[50];
 
-    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Publishing system MQTT topics...");
+    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Publishing System MQTT topics...");
+
+	int aFreeInternalHeapSizeBefore = heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
+
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + std::string(LWT_TOPIC), LWT_CONNECTED, qos, retainFlag); // Publish "connected" to maintopic/connection
 
     sprintf(tmp_char, "%ld", (long)getUpTime());
-    MQTTPublish(maintopic + "/" + "uptime", std::string(tmp_char), retainFlag);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "uptime", std::string(tmp_char), qos, retainFlag);
     
     sprintf(tmp_char, "%lu", (long) getESPHeapSize());
-    MQTTPublish(maintopic + "/" + "freeMem", std::string(tmp_char), retainFlag);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "freeMem", std::string(tmp_char), qos, retainFlag);
 
     sprintf(tmp_char, "%d", get_WIFI_RSSI());
-    MQTTPublish(maintopic + "/" + "wifiRSSI", std::string(tmp_char), retainFlag);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "wifiRSSI", std::string(tmp_char), qos, retainFlag);
 
     sprintf(tmp_char, "%d", (int)temperatureRead());
-    MQTTPublish(maintopic + "/" + "CPUtemp", std::string(tmp_char), retainFlag);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "CPUtemp", std::string(tmp_char), qos, retainFlag);
+
+    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Successfully published all System MQTT topics");
+
+	int aFreeInternalHeapSizeAfter = heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
+	int aMinFreeInternalHeapSize =  heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
+
+    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Int. Heap Usage before publishing System Topics: " + 
+            to_string(aFreeInternalHeapSizeBefore) + ", after: " + to_string(aFreeInternalHeapSizeAfter) + ", delta: " + 
+            to_string(aFreeInternalHeapSizeBefore - aFreeInternalHeapSizeAfter) + ", lowest free: " + to_string(aMinFreeInternalHeapSize));
+
+    return allSendsSuccessed;
 }
 
 
-void publishStaticData() {
-    if (!getMQTTisConnected()) 
-        return;
+bool publishStaticData(int qos) {
+    bool allSendsSuccessed = false;
+
+    if (!getMQTTisConnected()) {
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Unable to send Static Topics, we are not connected to the MQTT broker!");
+        return false;
+    }
 
     LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Publishing static MQTT topics...");
-    MQTTPublish(maintopic + "/" + "MAC", getMac(), retainFlag);
-    MQTTPublish(maintopic + "/" + "IP", *getIPAddress(), retainFlag);
-    MQTTPublish(maintopic + "/" + "hostname", hostname, retainFlag);
+
+	int aFreeInternalHeapSizeBefore = heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
+
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "MAC", getMac(), qos, retainFlag);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "IP", *getIPAddress(), qos, retainFlag);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "hostname", wlan_config.hostname, qos, retainFlag);
 
     std::stringstream stream;
     stream << std::fixed << std::setprecision(1) << roundInterval; // minutes
-    MQTTPublish(maintopic + "/" + "interval", stream.str(), retainFlag);
-}
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "interval", stream.str(), qos, retainFlag);
 
-esp_err_t sendDiscovery_and_static_Topics(httpd_req_t *req) {
-    if (HomeassistantDiscovery) {
-        MQTThomeassistantDiscovery();
-    }
+    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Successfully published all Static MQTT topics");
+
+	int aFreeInternalHeapSizeAfter = heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
+	int aMinFreeInternalHeapSize =  heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
 
-    publishStaticData();
+    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Int. Heap Usage before Publishing Static Topics: " + 
+            to_string(aFreeInternalHeapSizeBefore) + ", after: " + to_string(aFreeInternalHeapSizeAfter) + ", delta: " + 
+            to_string(aFreeInternalHeapSizeBefore - aFreeInternalHeapSizeAfter) + ", lowest free: " + to_string(aMinFreeInternalHeapSize));
+
+    return allSendsSuccessed;
+}
 
-    const char* resp_str = (const char*) req->user_ctx;
-    httpd_resp_send(req, resp_str, strlen(resp_str));  
 
+esp_err_t scheduleSendingDiscovery_and_static_Topics(httpd_req_t *req) {
+    sendingOf_DiscoveryAndStaticTopics_scheduled = true;
+    char msg[] = "MQTT Homeassistant Discovery and Static Topics scheduled";
+    httpd_resp_send(req, msg, strlen(msg));  
     return ESP_OK;
 }
 
-void GotConnected(std::string maintopic, bool retainFlag) {
+
+esp_err_t sendDiscovery_and_static_Topics(void) {
+    bool success = false;
+
+    if (!sendingOf_DiscoveryAndStaticTopics_scheduled) {
+        // Flag not set, nothing to do
+        return ESP_OK;
+    }
+
     if (HomeassistantDiscovery) {
-        MQTThomeassistantDiscovery();
+        success = MQTThomeassistantDiscovery(1);
     }
 
-    publishStaticData();
-    publishSystemData();
+    success |= publishStaticData(1);
+
+    if (success) { // Success, clear the flag
+        sendingOf_DiscoveryAndStaticTopics_scheduled = false;
+        return ESP_OK;
+    }
+    else {
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "One or more MQTT topics failed to be published, will try sending them in the next round!");
+        /* Keep sendingOf_DiscoveryAndStaticTopics_scheduled set so we can retry after the next round */
+        return ESP_FAIL;
+    }
+}
+
+
+void GotConnected(std::string maintopic, bool retainFlag) {
+    // Nothing to do
 }
 
 void register_server_mqtt_uri(httpd_handle_t server) {
@@ -235,8 +308,8 @@ void register_server_mqtt_uri(httpd_handle_t server) {
     uri.method    = HTTP_GET;
 
     uri.uri       = "/mqtt_publish_discovery";
-    uri.handler   = sendDiscovery_and_static_Topics;
-    uri.user_ctx  = (void*) "MQTT Discovery and Static Topics sent";    
+    uri.handler   = scheduleSendingDiscovery_and_static_Topics;
+    uri.user_ctx  = (void*) "";    
     httpd_register_uri_handler(server, &uri); 
 }
 

+ 2 - 1
code/components/jomjol_mqtt/server_mqtt.h

@@ -16,10 +16,11 @@ std::string mqttServer_getMainTopic();
 
 void register_server_mqtt_uri(httpd_handle_t server);
 
-void publishSystemData();
+bool publishSystemData(int qos);
 
 std::string getTimeUnit(void);
 void GotConnected(std::string maintopic, bool SetRetainFlag);
+esp_err_t sendDiscovery_and_static_Topics(void);
 
 
 #endif //SERVERMQTT_H

+ 129 - 65
code/components/jomjol_tfliteclass/server_tflite.cpp

@@ -10,6 +10,7 @@
 
 #include "../../include/defines.h"
 #include "Helper.h"
+#include "statusled.h"
 
 #include "esp_camera.h"
 #include "time_sntp.h"
@@ -21,8 +22,11 @@
 #include "server_GPIO.h"
 
 #include "server_file.h"
+
+#include "read_wlanini.h"
 #include "connect_wlan.h"
 
+
 ClassFlowControll tfliteflow;
 
 TaskHandle_t xHandletask_autodoFlow = NULL;
@@ -44,20 +48,24 @@ static const char *TAG = "TFLITE SERVER";
 void CheckIsPlannedReboot()
 {
  	FILE *pfile;
-    if ((pfile = fopen("/sdcard/reboot.txt", "r")) == NULL)
-    {
-		LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Not a planned reboot.");
+    if ((pfile = fopen("/sdcard/reboot.txt", "r")) == NULL) {
+		//LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Initial boot or not a planned reboot");
         isPlannedReboot = false;
 	}
-    else
-    {
-		LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Planned reboot.");
+    else {
+		LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Planned reboot");
         DeleteFile("/sdcard/reboot.txt");   // Prevent Boot Loop!!!
         isPlannedReboot = true;
 	}
 }
 
 
+bool getIsPlannedReboot() 
+{
+    return isPlannedReboot;
+}
+
+
 int getCountFlowRounds() 
 {
     return countRounds;
@@ -370,64 +378,109 @@ esp_err_t handler_wasserzaehler(httpd_req_t *req)
             return ESP_OK;
         }
 
+
+        std::string *status = tfliteflow.getActStatus();
+        string query = std::string(_query);
+    //    ESP_LOGD(TAG, "Query: %s, query.c_str());
+        if (query.find("full") != std::string::npos)
+        {
+            string txt;
+            txt = "<body style=\"font-family: arial\">";
+
+            if ((countRounds <= 1) && (*status != std::string("Flow finished"))) { // First round not completed yet
+                txt += "<h3>Please wait for the first round to complete!</h3><h3>Current state: " + *status + "</h3>\n";
+            }
+            else {
+                txt += "<h3>Value</h3>";
+            }
+
+            httpd_resp_sendstr_chunk(req, txt.c_str());
+        }
+
+
         zw = tfliteflow.getReadout(_rawValue, _noerror);
         if (zw.length() > 0)
             httpd_resp_sendstr_chunk(req, zw.c_str()); 
 
-        string query = std::string(_query);
-    //    ESP_LOGD(TAG, "Query: %s, query.c_str());
+
         if (query.find("full") != std::string::npos)
         {
             string txt, zw;
-            
-            txt = "<p>Aligned Image: <p><img src=\"/img_tmp/alg_roi.jpg\"> <p>\n";
-            txt = txt + "Digital Counter: <p> ";
-            httpd_resp_sendstr_chunk(req, txt.c_str()); 
-            
-            std::vector<HTMLInfo*> htmlinfodig;
-            htmlinfodig = tfliteflow.GetAllDigital();  
 
-            for (int i = 0; i < htmlinfodig.size(); ++i)
-            {
-                if (tfliteflow.GetTypeDigital() == Digital)
+            if ((countRounds <= 1) && (*status != std::string("Flow finished"))) { // First round not completed yet
+                // Nothing to do
+            }
+            else {
+                /* Digital ROIs */
+                txt = "<body style=\"font-family: arial\">";
+                txt += "<h3>Recognized Digit ROIs (previous round)</h3>\n";
+                txt += "<table style=\"border-spacing: 5px\"><tr style=\"text-align: center; vertical-align: top;\">\n";
+
+                std::vector<HTMLInfo*> htmlinfodig;
+                htmlinfodig = tfliteflow.GetAllDigital(); 
+
+                for (int i = 0; i < htmlinfodig.size(); ++i)
                 {
-                    if (htmlinfodig[i]->val == 10)
-                        zw = "NaN";
+                    if (tfliteflow.GetTypeDigital() == Digital)
+                    {
+                        if (htmlinfodig[i]->val == 10)
+                            zw = "NaN";
+                        else
+                            zw = to_string((int) htmlinfodig[i]->val);
+
+                        txt += "<td style=\"width: 100px\"><h4>" + zw + "</h4><p><img src=\"/img_tmp/" +  htmlinfodig[i]->filename + "\"></p></td>\n";
+                    }
                     else
-                        zw = to_string((int) htmlinfodig[i]->val);
-
-                    txt = "<img src=\"/img_tmp/" +  htmlinfodig[i]->filename + "\"> " + zw;
+                    {
+                        std::stringstream stream;
+                        stream << std::fixed << std::setprecision(1) << htmlinfodig[i]->val;
+                        zw = stream.str();
+
+                        txt += "<td style=\"width: 100px\"><h4>" + zw + "</h4><p><img src=\"/img_tmp/" +  htmlinfodig[i]->filename + "\"></p></td>\n";
+                    }
+                    delete htmlinfodig[i];
                 }
-                else
+
+                htmlinfodig.clear();
+            
+                txt += "</tr></table>\n";
+                httpd_resp_sendstr_chunk(req, txt.c_str()); 
+
+
+                /* Analog ROIs */
+                txt = "<h3>Recognized Analog ROIs (previous round)</h3>\n";
+                txt += "<table style=\"border-spacing: 5px\"><tr style=\"text-align: center; vertical-align: top;\">\n";
+                
+                std::vector<HTMLInfo*> htmlinfoana;
+                htmlinfoana = tfliteflow.GetAllAnalog();
+                for (int i = 0; i < htmlinfoana.size(); ++i)
                 {
                     std::stringstream stream;
-                    stream << std::fixed << std::setprecision(1) << htmlinfodig[i]->val;
+                    stream << std::fixed << std::setprecision(1) << htmlinfoana[i]->val;
                     zw = stream.str();
 
-                    txt = "<img src=\"/img_tmp/" +  htmlinfodig[i]->filename + "\"> " + zw;
+                    txt += "<td style=\"width: 150px;\"><h4>" + zw + "</h4><p><img src=\"/img_tmp/" +  htmlinfoana[i]->filename + "\"></p></td>\n";
+                delete htmlinfoana[i];
                 }
+                htmlinfoana.clear();   
+
+                txt += "</tr>\n</table>\n";
                 httpd_resp_sendstr_chunk(req, txt.c_str()); 
-                delete htmlinfodig[i];
-            }
-            htmlinfodig.clear();
-        
-            txt = " <p> Analog Meter: <p> ";
-            httpd_resp_sendstr_chunk(req, txt.c_str()); 
-            
-            std::vector<HTMLInfo*> htmlinfoana;
-            htmlinfoana = tfliteflow.GetAllAnalog();
-            for (int i = 0; i < htmlinfoana.size(); ++i)
-            {
-                std::stringstream stream;
-                stream << std::fixed << std::setprecision(1) << htmlinfoana[i]->val;
-                zw = stream.str();
 
-                txt = "<img src=\"/img_tmp/" +  htmlinfoana[i]->filename + "\"> " + zw;
+
+                /* Full Image 
+                 * Only show it after the image got taken and aligned */
+                txt = "<h3>Aligned Image (current round)</h3>\n";
+                if ((*status == std::string("Initialization")) || 
+                    (*status == std::string("Initialization (delayed)")) || 
+                    (*status == std::string("Take Image"))) {
+                    txt += "<p>Current state: " + *status + "</p>\n";
+                }
+                else {
+                    txt += "<img src=\"/img_tmp/alg_roi.jpg\">\n";
+                }
                 httpd_resp_sendstr_chunk(req, txt.c_str()); 
-                delete htmlinfoana[i];
             }
-            htmlinfoana.clear();   
-
         }   
 
         /* Respond with an empty chunk to signal HTTP response completion */
@@ -664,7 +717,7 @@ esp_err_t handler_statusflow(httpd_req_t *req)
             ESP_LOGD(TAG, "handler_prevalue: %s", req->uri);
         #endif
 
-        string* zw = tfliteflow.getActStatus();
+        string* zw = tfliteflow.getActStatusWithTime();
         resp_str = zw->c_str();
 
         httpd_resp_send(req, resp_str, HTTPD_RESP_USE_STRLEN);   
@@ -807,21 +860,15 @@ void task_autodoFlow(void *pvParameter)
 
     bTaskAutoFlowCreated = true;
 
-    if (!isPlannedReboot)
+    if (!isPlannedReboot && (esp_reset_reason() == ESP_RST_PANIC))
     {
-        if (esp_reset_reason() == ESP_RST_PANIC) {
-            LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Restarted due to an Exception/panic! Postponing first round start by 5 minutes to allow for an OTA Update or to fetch the log!"); 
-            LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Setting logfile level to DEBUG until the next reboot!");
-            LogFile.setLogLevel(ESP_LOG_DEBUG);
-            tfliteflow.setActStatus("Initialization (delayed)");
-            //#ifdef ENABLE_MQTT
-                //MQTTPublish(mqttServer_getMainTopic() + "/" + "status", "Initialization (delayed)", false); // Right now, not possible -> MQTT Service is going to be started later
-            //#endif //ENABLE_MQTT
-            vTaskDelay(60*5000 / portTICK_RATE_MS); // Wait 5 minutes to give time to do an OTA Update or fetch the log
-        }
+        tfliteflow.setActStatus("Initialization (delayed)");
+        //#ifdef ENABLE_MQTT
+            //MQTTPublish(mqttServer_getMainTopic() + "/" + "status", "Initialization (delayed)", false); // Right now, not possible -> MQTT Service is going to be started later
+        //#endif //ENABLE_MQTT
+        vTaskDelay(60*5000 / portTICK_PERIOD_MS); // Wait 5 minutes to give time to do an OTA update or fetch the log
     }
 
-
     ESP_LOGD(TAG, "task_autodoFlow: start");
     doInit();
 
@@ -861,17 +908,33 @@ void task_autodoFlow(void *pvParameter)
             LogFile.RemoveOldLogFile();
             LogFile.RemoveOldDataLog();
         }
+
+        // Round finished -> Logfile
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Round #" + std::to_string(countRounds) + 
+                " completed (" + std::to_string(getUpTime() - roundStartTime) + " seconds)");
         
-        //CPU Temp -> Logfile
+        // CPU Temp -> Logfile
         LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "CPU Temperature: " + std::to_string((int)temperatureRead()) + "°C");
         
         // WIFI Signal Strength (RSSI) -> Logfile
         LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "WIFI Signal (RSSI): " + std::to_string(get_WIFI_RSSI()) + "dBm");
-        
-        //Round finished -> Logfile
-        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Round #" + std::to_string(countRounds) + 
-                " completed (" + std::to_string(getUpTime() - roundStartTime) + " seconds)");
 
+        // Check if time is synchronized (if NTP is configured)
+        if (getUseNtp() && !getTimeIsSet()) {
+            LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Time server is configured, but time is not yet set!");
+            StatusLED(TIME_CHECK, 1, false);
+        }
+
+        #if (defined WLAN_USE_MESH_ROAMING && defined WLAN_USE_MESH_ROAMING_ACTIVATE_CLIENT_TRIGGERED_QUERIES)
+            wifiRoamingQuery();
+        #endif
+        
+        // Scan channels and check if an AP with better RSSI is available, then disconnect and try to reconnect to AP with better RSSI
+        // NOTE: Keep this direct before the following task delay, because scan is done in blocking mode and this takes ca. 1,5 - 2s.
+        #ifdef WLAN_USE_ROAMING_BY_SCANNING
+            wifiRoamByScanning();
+        #endif
+        
         fr_delta_ms = (esp_timer_get_time() - fr_start) / 1000;
         if (auto_interval > fr_delta_ms)
         {
@@ -892,11 +955,12 @@ void TFliteDoAutoStart()
 
     ESP_LOGD(TAG, "getESPHeapInfo: %s", getESPHeapInfo().c_str());
 
-    xReturned = xTaskCreatePinnedToCore(&task_autodoFlow, "task_autodoFlow", 16 * 1024, NULL, tskIDLE_PRIORITY+2, &xHandletask_autodoFlow, 0);
-    //xReturned = xTaskCreate(&task_autodoFlow, "task_autodoFlow", 16 * 1024, NULL, tskIDLE_PRIORITY+2, &xHandletask_autodoFlow);
+    uint32_t stackSize = 16 * 1024;
+    xReturned = xTaskCreatePinnedToCore(&task_autodoFlow, "task_autodoFlow", stackSize, NULL, tskIDLE_PRIORITY+2, &xHandletask_autodoFlow, 0);
     if( xReturned != pdPASS )
     {
-       ESP_LOGD(TAG, "ERROR task_autodoFlow konnte nicht erzeugt werden!");
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Creation task_autodoFlow failed. Requested stack size:" + std::to_string(stackSize));
+        LogFile.WriteHeapInfo("Creation task_autodoFlow failed");
     }
     ESP_LOGD(TAG, "getESPHeapInfo: %s", getESPHeapInfo().c_str());
 }

+ 1 - 0
code/components/jomjol_tfliteclass/server_tflite.h

@@ -21,6 +21,7 @@ bool isSetupModusActive();
 int getCountFlowRounds();
 
 void CheckIsPlannedReboot();
+bool getIsPlannedReboot();
 
 esp_err_t GetJPG(std::string _filename, httpd_req_t *req);
 esp_err_t GetRawJPG(httpd_req_t *req);

+ 17 - 2
code/components/jomjol_time_sntp/time_sntp.cpp

@@ -25,6 +25,8 @@ static const char *TAG = "SNTP";
 static std::string timeZone = "";
 static std::string timeServer = "undefined";
 static bool useNtp = true;
+static bool timeWasNotSetAtBoot = false;
+static bool timeWasNotSetAtBoot_PrintStartBlock = false;
 
 std::string getNtpStatusText(sntp_sync_status_t status);
 static void setTimeZone(std::string _tzstring);
@@ -59,7 +61,13 @@ std::string getCurrentTimeString(const char * frm)
 
 void time_sync_notification_cb(struct timeval *tv)
 {
-    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Time is now successfully synced with NTP Server " +
+    if (timeWasNotSetAtBoot_PrintStartBlock) {
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "=================================================");
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "==================== Start ======================");
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "== Logs before time sync -> log_1970-01-01.txt ==");
+        timeWasNotSetAtBoot_PrintStartBlock = false;
+    }
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Time is synced with NTP Server " +
             getServerName() + ": " + getCurrentTimeString("%Y-%m-%d %H:%M:%S"));
 }
 
@@ -111,6 +119,11 @@ bool getUseNtp(void) {
     return useNtp;
 }
 
+bool getTimeWasNotSetAtBoot(void)
+{
+    return timeWasNotSetAtBoot;
+}
+
 
 std::string getServerName(void) {
     char buf[100];
@@ -140,7 +153,7 @@ bool setupTime() {
     ConfigFile configFile = ConfigFile(CONFIG_FILE); 
 
     if (!configFile.ConfigFileExists()){
-        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "No ConfigFile defined - exit setupTime() ");
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "No ConfigFile defined - exit setupTime()!");
         return false;
     }
 
@@ -225,6 +238,8 @@ bool setupTime() {
         LogFile.WriteToFile(ESP_LOG_INFO, TAG, "The local time is unknown, starting with " + std::string(strftime_buf));
         if (useNtp) {
             LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Once the NTP server provides a time, we will switch to that one");
+            timeWasNotSetAtBoot = true;
+            timeWasNotSetAtBoot_PrintStartBlock = true;
         }
     }
 

+ 1 - 0
code/components/jomjol_time_sntp/time_sntp.h

@@ -22,6 +22,7 @@ std::string ConvertTimeToString(time_t _time, const char * frm);
 
 
 bool getTimeIsSet(void);
+bool getTimeWasNotSetAtBoot(void);
 
 bool getUseNtp(void);
 bool setupTime();

+ 368 - 257
code/components/jomjol_wlan/connect_wlan.cpp

@@ -1,88 +1,69 @@
 #include "connect_wlan.h"
 
 #include <string.h>
-#include "freertos/FreeRTOS.h"
-#include "freertos/task.h"
-#include "freertos/event_groups.h"
-#include "driver/gpio.h"
-#include "esp_system.h"
-#include "esp_wifi.h"
-#include "esp_event.h"
-#include "esp_log.h"
-#include "nvs_flash.h"
-
-#include "lwip/err.h"
-#include "lwip/sys.h"
-#ifdef ENABLE_MQTT
-    #include "interface_mqtt.h"
-#endif //ENABLE_MQTT
-
+#include <stdlib.h>
 #include <fstream>
-#include <string>
 #include <vector>
 #include <sstream>
 #include <iostream>
 
-//////////////////////
-#include <string.h>
-#include <stdlib.h>
 #include "freertos/FreeRTOS.h"
 #include "freertos/task.h"
 #include "freertos/event_groups.h"
+
+#include "driver/gpio.h"
+#include "esp_system.h"
 #include "esp_wifi.h"
 #include "esp_wnm.h"
 #include "esp_rrm.h"
 #include "esp_mbo.h"
+#include "esp_mac.h"
+#include "esp_netif.h"
 #include "esp_event.h"
 #include "esp_log.h"
-#include "esp_mac.h"
 #include "nvs_flash.h"
-#include "esp_netif.h"
 
+#include "lwip/err.h"
+#include "lwip/sys.h"
+#ifdef ENABLE_MQTT
+    #include "interface_mqtt.h"
+#endif //ENABLE_MQTT
+
+#include "ClassLogFile.h"
+#include "read_wlanini.h"
+#include "Helper.h"
+#include "statusled.h"
 
-/////////////////////
 #include "../../include/defines.h"
 
 
-/* FreeRTOS event group to signal when we are connected*/
-static EventGroupHandle_t s_wifi_event_group;
-
 static const char *TAG = "WIFI";
 
-static int s_retry_num = 0;
-bool WIFIConnected = false;
+static bool APWithBetterRSSI = false;
+static bool WIFIConnected = false;
+static int WIFIReconnectCnt = 0;
 
-///////////////////////////////////////////////////////////
 
-int BlinkDauer;
-int BlinkAnzahl;
-bool BlinkOff;
-bool BlinkIsRunning = false;
 
-std::string hostname = "";
-std::string std_hostname = "watermeter";
-std::string ipadress = "";
-std::string ssid = "";
-int RSSIThreshold;
+void strinttoip4(const char *ip, int &a, int &b, int &c, int &d) {
+    std::string zw = std::string(ip);
+    std::stringstream s(zw);
+    char ch; //to temporarily store the '.'
+    s >> a >> ch >> b >> ch >> c >> ch >> d;
+}
 
-/////////////////////////////////
-/////////////////////////////////
 
-#ifdef WLAN_USE_MESH_ROAMING
+std::string BssidToString(const char* c) {
+	char cBssid[25];
+	sprintf(cBssid, "%02x:%02x:%02x:%02x:%02x:%02x", c[0], c[1], c[2], c[3], c[4], c[5]);
+	return std::string(cBssid);
+}
 
-int RSSI_Threshold = WLAN_WIFI_RSSI_THRESHOLD;
 
+#ifdef WLAN_USE_MESH_ROAMING
 /* rrm ctx */
 int rrm_ctx = 0;
 
-/* FreeRTOS event group to signal when we are connected & ready to make a request */
-static EventGroupHandle_t wifi_event_group;
-
-/* esp netif object representing the WIFI station */
-static esp_netif_t *sta_netif = NULL;
-
-//static const char *TAG = "roaming_example";
-
 static inline uint32_t WPA_GET_LE32(const uint8_t *a)
 {
 	return ((uint32_t) a[3] << 24) | (a[2] << 16) | (a[1] << 8) | a[0];
@@ -105,6 +86,7 @@ static inline uint32_t WPA_GET_LE32(const uint8_t *a)
 #define ETH_ALEN 6
 #endif
 
+
 #define MAX_NEIGHBOR_LEN 512
 static char * get_btm_neighbor_list(uint8_t *report, size_t report_len)
 {
@@ -121,10 +103,10 @@ static char * get_btm_neighbor_list(uint8_t *report, size_t report_len)
 	 * PHY Type[1]
 	 * Optional Subelements[variable]
 	 */
-#define NR_IE_MIN_LEN (ETH_ALEN + 4 + 1 + 1 + 1)
+	#define NR_IE_MIN_LEN (ETH_ALEN + 4 + 1 + 1 + 1)
 
 	if (!report || report_len == 0) {
-		ESP_LOGI(TAG, "RRM neighbor report is not valid");
+		ESP_LOGD(TAG, "Roaming: RRM neighbor report is not valid");
 		return NULL;
 	}
 
@@ -140,14 +122,14 @@ static char * get_btm_neighbor_list(uint8_t *report, size_t report_len)
 
 		if (pos[0] != WLAN_EID_NEIGHBOR_REPORT ||
 		    nr_len < NR_IE_MIN_LEN) {
-			ESP_LOGI(TAG, "CTRL: Invalid Neighbor Report element: id=%u len=%u",
+			ESP_LOGD(TAG, "Roaming CTRL: Invalid Neighbor Report element: id=%u len=%u",
 					data[0], nr_len);
 			ret = -1;
 			goto cleanup;
 		}
 
 		if (2U + nr_len > report_len) {
-			ESP_LOGI(TAG, "CTRL: Invalid Neighbor Report element: id=%u len=%zu nr_len=%u",
+			ESP_LOGD(TAG, "Roaming CTRL: Invalid Neighbor Report element: id=%u len=%zu nr_len=%u",
 					data[0], report_len, nr_len);
 			ret = -1;
 			goto cleanup;
@@ -190,8 +172,8 @@ static char * get_btm_neighbor_list(uint8_t *report, size_t report_len)
 
 			pos += s_len;
 		}
-
-		ESP_LOGI(TAG, "RMM neigbor report bssid=" MACSTR
+				
+		ESP_LOGI(TAG, "Roaming: RMM neigbor report bssid=" MACSTR
 				" info=0x%x op_class=%u chan=%u phy_type=%u%s%s%s%s",
 				MAC2STR(nr), WPA_GET_LE32(nr + ETH_ALEN),
 				nr[ETH_ALEN + 4], nr[ETH_ALEN + 5],
@@ -199,6 +181,10 @@ static char * get_btm_neighbor_list(uint8_t *report, size_t report_len)
 				lci[0] ? " lci=" : "", lci,
 				civic[0] ? " civic=" : "", civic);
 
+		
+		LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Roaming: RMM neigbor report BSSID: " + BssidToString((char*)nr) + 
+		                                        ", Channel: " + std::to_string(nr[ETH_ALEN + 5]));
+
 		/* neighbor start */
 		len += snprintf(buf + len, MAX_NEIGHBOR_LEN - len, " neighbor=");
 		/* bssid */
@@ -236,18 +222,19 @@ void neighbor_report_recv_cb(void *ctx, const uint8_t *report, size_t report_len
 	int *val = (int*) ctx;
 	uint8_t *pos = (uint8_t *)report;
 	int cand_list = 0;
+	int ret;
 
 	if (!report) {
-		ESP_LOGE(TAG, "report is null");
+		ESP_LOGD(TAG, "Roaming: Neighbor report is null");
 		return;
 	}
 	if (*val != rrm_ctx) {
-		ESP_LOGE(TAG, "rrm_ctx value didn't match, not initiated by us");
+		ESP_LOGE(TAG, "Roaming: rrm_ctx value didn't match, not initiated by us");
 		return;
 	}
 	/* dump report info */
-	ESP_LOGI(TAG, "rrm: neighbor report len=%d", report_len);
-	ESP_LOG_BUFFER_HEXDUMP(TAG, pos, report_len, ESP_LOG_INFO);
+	ESP_LOGD(TAG, "Roaming: RRM neighbor report len=%d", report_len);
+	ESP_LOG_BUFFER_HEXDUMP(TAG, pos, report_len, ESP_LOG_DEBUG);
 
 	/* create neighbor list */
 	char *neighbor_list = get_btm_neighbor_list(pos + 1, report_len - 1);
@@ -266,8 +253,10 @@ void neighbor_report_recv_cb(void *ctx, const uint8_t *report, size_t report_len
 		esp_wifi_scan_get_ap_records(&number, &ap_records);
 		cand_list = 1;
 	}
-	/* send AP btm query, this will cause STA to roam as well */
-	esp_wnm_send_bss_transition_mgmt_query(REASON_FRAME_LOSS, neighbor_list, cand_list);
+	/* send AP btm query requesting to roam depending on candidate list of AP */
+	// btm_query_reasons: https://github.com/espressif/esp-idf/blob/release/v4.4/components/wpa_supplicant/esp_supplicant/include/esp_wnm.h
+	ret = esp_wnm_send_bss_transition_mgmt_query(REASON_FRAME_LOSS, neighbor_list, cand_list);	// query reason 16 -> LOW RSSI --> (btm_query_reason)16
+	ESP_LOGD(TAG, "neighbor_report_recv_cb retval - bss_transisition_query: %d", ret);
 
 cleanup:
 	if (neighbor_list)
@@ -275,276 +264,399 @@ cleanup:
 }
 
 
-static void esp_bss_rssi_low_handler(void* arg, esp_event_base_t event_base,
-		int32_t event_id, void* event_data)
+static void esp_bss_rssi_low_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
 {
+	int retval = -1;
 	wifi_event_bss_rssi_low_t *event = (wifi_event_bss_rssi_low_t*) event_data;
 
-	ESP_LOGI(TAG, "%s:bss rssi is=%d", __func__, event->rssi);
-	/* Lets check channel conditions */
-	rrm_ctx++;
-	if (esp_rrm_send_neighbor_rep_request(neighbor_report_recv_cb, &rrm_ctx) < 0) {
-		/* failed to send neighbor report request */
-		ESP_LOGI(TAG, "failed to send neighbor report request");
-		if (esp_wnm_send_bss_transition_mgmt_query(REASON_FRAME_LOSS, NULL, 0) < 0) {
-			ESP_LOGI(TAG, "failed to send btm query");
-		}
+	LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Roaming Event: RSSI " + std::to_string(event->rssi) + 
+								" < RSSI_Threshold " + std::to_string(wlan_config.rssi_threshold));
+
+	/* If RRM is supported, call RRM and then send BTM query to AP */
+	if (esp_rrm_is_rrm_supported_connection() && esp_wnm_is_btm_supported_connection()) 
+	{
+		/* Lets check channel conditions */
+		rrm_ctx++;
+
+		retval = esp_rrm_send_neighbor_rep_request(neighbor_report_recv_cb, &rrm_ctx);
+		ESP_LOGD(TAG, "esp_rrm_send_neighbor_rep_request retval: %d", retval);
+		if (retval == 0)
+			LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Roaming: RRM + BTM query sent");
+		else
+			ESP_LOGD(TAG, "esp_rrm_send_neighbor_rep_request retval: %d", retval);
 	}
-}
 
+	/* If RRM is not supported or RRM request failed, send directly BTM query to AP */
+	if (retval < 0 && esp_wnm_is_btm_supported_connection()) 
+	{
+		// btm_query_reasons: https://github.com/espressif/esp-idf/blob/release/v4.4/components/wpa_supplicant/esp_supplicant/include/esp_wnm.h
+		retval = esp_wnm_send_bss_transition_mgmt_query(REASON_FRAME_LOSS, NULL, 0);	// query reason 16 -> LOW RSSI --> (btm_query_reason)16
+		ESP_LOGD(TAG, "esp_wnm_send_bss_transition_mgmt_query retval: %d", retval);
+		if (retval == 0)
+			LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Roaming: BTM query sent");
+		else
+			ESP_LOGD(TAG, "esp_wnm_send_bss_transition_mgmt_query retval: %d", retval);
+	}
+}
 
-#endif
 
-//////////////////////////////////
-//////////////////////////////////
+void printRoamingFeatureSupport(void) 
+{
+	if (esp_rrm_is_rrm_supported_connection())
+		LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Roaming: RRM (802.11k) supported by AP");
+	else
+		LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Roaming: RRM (802.11k) NOT supported by AP");
+
+	if (esp_wnm_is_btm_supported_connection())
+		LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Roaming: BTM (802.11v) supported by AP");
+	else
+		LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Roaming: BTM (802.11v) NOT supported by AP");
+}
 
 
-std::string* getIPAddress()
+#ifdef WLAN_USE_MESH_ROAMING_ACTIVATE_CLIENT_TRIGGERED_QUERIES
+void wifiRoamingQuery(void) 
 {
-    return &ipadress;
+	/* Query only if WIFI is connected and feature is supported by AP */
+	if (WIFIConnected && (esp_rrm_is_rrm_supported_connection() || esp_wnm_is_btm_supported_connection())) {
+		/* Client is allowed to send query to AP for roaming request if RSSI is lower than threshold */
+		/* Note 1: Set RSSI threshold funtion needs to be called to trigger WIFI_EVENT_STA_BSS_RSSI_LOW */
+		/* Note 2: Additional querys will be sent after flow round is finshed --> server_tflite.cpp - function "task_autodoFlow" */
+		/* Note 3: RSSI_Threshold = 0 --> Disable client query by application (WebUI parameter) */
+		
+		if (wlan_config.rssi_threshold != 0 && get_WIFI_RSSI() != -127 && (get_WIFI_RSSI() < wlan_config.rssi_threshold))
+			esp_wifi_set_rssi_threshold(wlan_config.rssi_threshold);
+	}
 }
+#endif // WLAN_USE_MESH_ROAMING_ACTIVATE_CLIENT_TRIGGERED_QUERIES
+#endif // WLAN_USE_MESH_ROAMING
 
 
-std::string* getSSID()
+#ifdef WLAN_USE_ROAMING_BY_SCANNING
+std::string getAuthModeName(const wifi_auth_mode_t auth_mode)
 {
-    return &ssid;
+	std::string AuthModeNames[] = {"OPEN", "WEP", "WPA PSK", "WPA2 PSK", "WPA WPA2 PSK", "WPA2 ENTERPRISE",
+                                   "WPA3 PSK", "WPA2 WPA3 PSK", "WAPI_PSK", "MAX"};
+    return AuthModeNames[auth_mode];
 }
 
 
-void task_doBlink(void *pvParameter)
+void wifi_scan(void)
 {
-    ESP_LOGI("BLINK", "Flash - start");
-    while (BlinkIsRunning)
-    {
-//        ESP_LOGI("BLINK", "Blinken - wait");
-        vTaskDelay(100 / portTICK_PERIOD_MS);
-    }
+    wifi_scan_config_t wifi_scan_config;
+    memset(&wifi_scan_config, 0, sizeof(wifi_scan_config));
+
+    wifi_scan_config.ssid = (uint8_t*)wlan_config.ssid.c_str(); // only scan for configured SSID
+    wifi_scan_config.show_hidden = true;            // scan also hidden SSIDs
+	wifi_scan_config.channel = 0;                   // scan all channels
+
+    esp_wifi_scan_start(&wifi_scan_config, true);   // not using event handler SCAN_DONE by purpose to keep SYS_EVENT heap smaller 
+                                                    // and the calling task task_autodoFlow is after scan is finish in wait state anyway
+                                                    // Scan duration: ca. (120ms + 30ms) * Number of channels -> ca. 1,5 - 2s
+
+    uint16_t max_number_of_ap_found = 10;           // max. number of APs, value will be updated by function "esp_wifi_scan_get_ap_num"
+	esp_wifi_scan_get_ap_num(&max_number_of_ap_found); // get actual found APs
+    wifi_ap_record_t* wifi_ap_records = new wifi_ap_record_t[max_number_of_ap_found]; // Allocate necessary record datasets
+	if (wifi_ap_records == NULL) {
+		esp_wifi_scan_get_ap_records(0, NULL); // free internal heap
+		LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "wifi_scan: Failed to allocate heap for wifi_ap_records");
+		return;
+	}
+	else {
+    	if (esp_wifi_scan_get_ap_records(&max_number_of_ap_found, wifi_ap_records) != ESP_OK) { // Retrieve results (and free internal heap)
+			LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "wifi_scan: esp_wifi_scan_get_ap_records: Error retrieving datasets");
+			free(wifi_ap_records);
+			return;
+		}
+	}
 
-    BlinkIsRunning = true;
+	wifi_ap_record_t currentAP;
+	esp_wifi_sta_get_ap_info(&currentAP);
+
+	LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Roaming: Current AP BSSID=" + BssidToString((char*)currentAP.bssid));
+    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Roaming: Scan completed, APs found with configured SSID: " + std::to_string(max_number_of_ap_found));
+    for (int i = 0; i < max_number_of_ap_found; i++) {
+        LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Roaming: " + std::to_string(i+1) +
+                                                ": SSID=" + std::string((char*)wifi_ap_records[i].ssid) +
+                                                ", BSSID=" + BssidToString((char*)wifi_ap_records[i].bssid) + 
+                                                ", RSSI=" + std::to_string(wifi_ap_records[i].rssi) + 
+                                                ", CH=" + std::to_string(wifi_ap_records[i].primary) + 
+                                                ", AUTH=" + getAuthModeName(wifi_ap_records[i].authmode));
+		if (wifi_ap_records[i].rssi > (currentAP.rssi + 5) && // RSSI is better than actual RSSI + 5 --> Avoid switching to AP with roughly same RSSI
+           (strcmp(BssidToString((char*)wifi_ap_records[i].bssid).c_str(), BssidToString((char*)currentAP.bssid).c_str()) != 0))
+        {
+			APWithBetterRSSI = true;
+        }
+	}
+    free(wifi_ap_records);
+}
 
-	// Init the GPIO
-    gpio_pad_select_gpio(BLINK_GPIO);
-    /* Set the GPIO as a push/pull output */
-    gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT);  
 
-    for (int i = 0; i < BlinkAnzahl; ++i)
-    {
-		if (BlinkAnzahl > 1)
-		{
-	        gpio_set_level(BLINK_GPIO, 1);
-    	    vTaskDelay(BlinkDauer / portTICK_PERIOD_MS);
+void wifiRoamByScanning(void)
+{
+	if (wlan_config.rssi_threshold != 0 && get_WIFI_RSSI() != -127 && (get_WIFI_RSSI() < wlan_config.rssi_threshold)) {
+		LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Roaming: Start scan of all channels for SSID " + wlan_config.ssid);
+		wifi_scan();
+
+		if (APWithBetterRSSI) {
+			APWithBetterRSSI = false;
+			LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Roaming: AP with better RSSI in range, disconnecting to switch AP...");
+			esp_wifi_disconnect();
+		} 
+		else {
+			LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Roaming: Scan completed, stay on current AP");
 		}
-        gpio_set_level(BLINK_GPIO, 0);      
-        vTaskDelay(BlinkDauer / portTICK_PERIOD_MS);
-    }
-
-    if (BlinkOff)
-        gpio_set_level(BLINK_GPIO, 1);
+	}
+}
+#endif // WLAN_USE_ROAMING_BY_SCANNING
 
-    ESP_LOGI("BLINK", "Flash - done");
-    BlinkIsRunning = false;
 
-    vTaskDelete(NULL); //Delete this task if it exits from the loop above
+std::string* getIPAddress()
+{
+    return &wlan_config.ipaddress;
 }
 
 
-void LEDBlinkTask(int _dauer, int _anz, bool _off)
+std::string* getSSID()
 {
-	BlinkDauer = _dauer;
-	BlinkAnzahl = _anz;
-	BlinkOff = _off;
-
-    xTaskCreate(&task_doBlink, "task_doBlink", 4 * 1024, NULL, tskIDLE_PRIORITY+1, NULL);
+    return &wlan_config.ssid;
 }
-/////////////////////////////////////////////////////////
 
 
-static void event_handler(void* arg, esp_event_base_t event_base,
-                                int32_t event_id, void* event_data)
+static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
 {
-    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
+    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) 
+	{
         WIFIConnected = false;
-        LEDBlinkTask(200, 1, true);
         esp_wifi_connect();
-    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
-        WIFIConnected = false;
-//        if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
-            esp_wifi_connect();
-            s_retry_num++;
-            ESP_LOGI(TAG, "retrying connection to the AP");
-//        } else {
-//            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
-//        }
-        ESP_LOGI(TAG,"connection to the AP failed");
-    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
-        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
-        ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
-        ipadress = std::string(ip4addr_ntoa((const ip4_addr*) &event->ip_info.ip));
-        s_retry_num = 0;
-        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
-        LEDBlinkTask(1000, 5, true);
+    }
+	else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) 
+	{
+		/* Disconnect reason: https://github.com/espressif/esp-idf/blob/d825753387c1a64463779bbd2369e177e5d59a79/components/esp_wifi/include/esp_wifi_types.h */
+		wifi_event_sta_disconnected_t *disconn = (wifi_event_sta_disconnected_t *)event_data;
+		if (disconn->reason == WIFI_REASON_ROAMING) {
+			LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Disconnected (" + std::to_string(disconn->reason) + ", Roaming 802.11kv)");
+			// --> no reconnect neccessary, it should automatically reconnect to new AP
+		}
+		else {
+			WIFIConnected = false;
+			if (disconn->reason == WIFI_REASON_NO_AP_FOUND) {
+				LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Disconnected (" + std::to_string(disconn->reason) + ", No AP)");
+				StatusLED(WLAN_CONN, 1, false);
+			}
+			else if (disconn->reason == WIFI_REASON_AUTH_EXPIRE ||
+					 disconn->reason == WIFI_REASON_AUTH_FAIL || 
+					 disconn->reason == WIFI_REASON_NOT_AUTHED ||
+					 disconn->reason == WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT || 
+					 disconn->reason == WIFI_REASON_HANDSHAKE_TIMEOUT) {
+				LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Disconnected (" + std::to_string(disconn->reason) + ", Auth fail)");
+				StatusLED(WLAN_CONN, 2, false);
+			}
+			else if (disconn->reason == WIFI_REASON_BEACON_TIMEOUT) {
+				LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Disconnected (" + std::to_string(disconn->reason) + ", Timeout)");
+				StatusLED(WLAN_CONN, 3, false);
+			}
+			else {
+				LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Disconnected (" + std::to_string(disconn->reason) + ")");
+				StatusLED(WLAN_CONN, 4, false);
+			}
+			WIFIReconnectCnt++;
+			esp_wifi_connect(); // Try to connect again
+		}
 
+		if (WIFIReconnectCnt >= 10) {
+			WIFIReconnectCnt = 0;
+			LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Disconnected, multiple reconnect attempts failed (" + 
+													 std::to_string(disconn->reason) + "), still retrying...");
+		}
+	}	
+	else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) 
+	{
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Connected to: " + wlan_config.ssid + ", RSSI: " + 
+												std::to_string(get_WIFI_RSSI()));
+
+		#ifdef WLAN_USE_MESH_ROAMING	
+			printRoamingFeatureSupport();
+
+			#ifdef WLAN_USE_MESH_ROAMING_ACTIVATE_CLIENT_TRIGGERED_QUERIES
+			// wifiRoamingQuery();	// Avoid client triggered query during processing flow (reduce risk of heap shortage). Request will be triggered at the end of every round anyway
+			#endif //WLAN_USE_MESH_ROAMING_ACTIVATE_CLIENT_TRIGGERED_QUERIES
+			
+		#endif //WLAN_USE_MESH_ROAMING
+	}	
+	else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) 
+	{
         WIFIConnected = true;
-        #ifdef ENABLE_MQTT
+		WIFIReconnectCnt = 0;
+
+		ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
+        wlan_config.ipaddress = std::string(ip4addr_ntoa((const ip4_addr*) &event->ip_info.ip));
+		LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Assigned IP: " + wlan_config.ipaddress);
+
+		#ifdef ENABLE_MQTT
             if (getMQTTisEnabled()) {
                 vTaskDelay(5000 / portTICK_PERIOD_MS); 
                 MQTT_Init();    // Init when WIFI is getting connected    
             }
-        #endif //ENABLE_MQTT
-    }
-}
-
-
-void strinttoip4(const char *ip, int &a, int &b, int &c, int &d) {
-    std::string zw = std::string(ip);
-    std::stringstream s(zw);
-    char ch; //to temporarily store the '.'
-    s >> a >> ch >> b >> ch >> c >> ch >> d;
+        #endif //ENABLE_MQTT   
+	}
 }
 
 
-void wifi_init_sta(const char *_ssid, const char *_password, const char *_hostname, const char *_ipadr, const char *_gw,  const char *_netmask, const char *_dns, int _rssithreshold)
+esp_err_t wifi_init_sta(void)
 {
-	RSSI_Threshold = _rssithreshold;
-    s_wifi_event_group = xEventGroupCreate();
-
-    ESP_ERROR_CHECK(esp_netif_init());
+	esp_err_t retval = esp_netif_init();
+	if (retval != ESP_OK) {
+		LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_netif_init: Error: "  + std::to_string(retval));
+		return retval;
+	}
 
-    ESP_ERROR_CHECK(esp_event_loop_create_default());
+    retval = esp_event_loop_create_default();
+	if (retval != ESP_OK) {
+		LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_event_loop_create_default: Error: "  + std::to_string(retval));
+		return retval;
+	}
+	
     esp_netif_t *my_sta = esp_netif_create_default_wifi_sta();
 
-    if ((_ipadr != NULL) && (_gw != NULL) && (_netmask != NULL))
+    if (!wlan_config.ipaddress.empty() && !wlan_config.gateway.empty() && !wlan_config.netmask.empty())
     {
-
-        ESP_LOGI(TAG, "set IP %s, GW %s, Netmask %s manual", _ipadr, _gw, _netmask);
-        esp_netif_dhcpc_stop(my_sta);
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Manual interface config -> IP: " + wlan_config.ipaddress + ", Gateway: " + 
+												std::string(wlan_config.gateway) + ", Netmask: " + std::string(wlan_config.netmask));
+		esp_netif_dhcpc_stop(my_sta);	// Stop DHCP service
 
         esp_netif_ip_info_t ip_info;
         int a, b, c, d;
-        strinttoip4(_ipadr, a, b, c, d);
-        IP4_ADDR(&ip_info.ip, a, b, c, d);
-        strinttoip4(_gw, a, b, c, d);
-        IP4_ADDR(&ip_info.gw, a, b, c, d);
-        strinttoip4(_netmask, a, b, c, d);
-        IP4_ADDR(&ip_info.netmask, a, b, c, d);
-
-        esp_netif_set_ip_info(my_sta, &ip_info);
+        strinttoip4(wlan_config.ipaddress.c_str(), a, b, c, d);
+        IP4_ADDR(&ip_info.ip, a, b, c, d);	// Set static IP address
+
+        strinttoip4(wlan_config.gateway.c_str(), a, b, c, d);
+        IP4_ADDR(&ip_info.gw, a, b, c, d);	// Set gateway
+
+        strinttoip4(wlan_config.netmask.c_str(), a, b, c, d);
+        IP4_ADDR(&ip_info.netmask, a, b, c, d);	// Set netmask
+
+        esp_netif_set_ip_info(my_sta, &ip_info);	// Set static IP configuration
     }
+	else {
+		LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Automatic interface config --> Use DHCP service");
+	}
 
-    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
-    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
+	wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
+    retval = esp_wifi_init(&cfg);
+	if (retval != ESP_OK) {
+		LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_wifi_init: Error: "  + std::to_string(retval));
+		return retval;
+	}
 
-    if ((_ipadr != NULL) && (_gw != NULL) && (_netmask != NULL))
+    if (!wlan_config.ipaddress.empty() && !wlan_config.gateway.empty() && !wlan_config.netmask.empty())
     {
-        if (_dns == NULL)
-            _dns = _gw;
-            
-        ESP_LOGI(TAG, "set DNS manual");
+        if (wlan_config.dns.empty()) {
+			LogFile.WriteToFile(ESP_LOG_INFO, TAG, "No DNS server, use gateway");
+			 wlan_config.dns = wlan_config.gateway;
+		} 
+		else {
+			LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Manual interface config -> DNS: " + wlan_config.dns);
+		}
+     
         esp_netif_dns_info_t dns_info;
         ip4_addr_t ip;
-        ip.addr = esp_ip4addr_aton(_dns);
+        ip.addr = esp_ip4addr_aton(wlan_config.dns.c_str());
         ip_addr_set_ip4_u32(&dns_info.ip, ip.addr);
-        ESP_ERROR_CHECK(esp_netif_set_dns_info(my_sta, ESP_NETIF_DNS_MAIN, &dns_info));
-    }
 
-    esp_event_handler_instance_t instance_any_id;
-    esp_event_handler_instance_t instance_got_ip;
-    esp_event_handler_instance_t instance_bss_rssi_low;
-    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
-                                                        ESP_EVENT_ANY_ID,
-                                                        &event_handler,
-                                                        NULL,
-                                                        &instance_any_id));
-    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
-                                                        IP_EVENT_STA_GOT_IP,
-                                                        &event_handler,
-                                                        NULL,
-                                                        &instance_got_ip));
+        retval = esp_netif_set_dns_info(my_sta, ESP_NETIF_DNS_MAIN, &dns_info);
+		if (retval != ESP_OK) {
+			LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_netif_set_dns_info: Error: "  + std::to_string(retval));
+			return retval;
+		}
+	}
+
+    retval = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
+                                                        &event_handler, NULL, NULL);
+	if (retval != ESP_OK) {
+		LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_event_handler_instance_register - WIFI_ANY: Error: "  + std::to_string(retval));
+		return retval;
+	}
+
+    retval = esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
+                                                        &event_handler, NULL, NULL);
+	if (retval != ESP_OK) {
+		LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_event_handler_instance_register - GOT_IP: Error: "  + std::to_string(retval));
+		return retval;
+	}
 
 	#ifdef WLAN_USE_MESH_ROAMING
-	ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, 
-                                                        WIFI_EVENT_STA_BSS_RSSI_LOW,
-                                                        &esp_bss_rssi_low_handler, 
-                                                        NULL,
-                                                        &instance_bss_rssi_low));
+	retval = esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_BSS_RSSI_LOW,
+                                                        &esp_bss_rssi_low_handler, NULL, NULL);
+	if (retval != ESP_OK) {
+		LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_event_handler_instance_register - BSS_RSSI_LOW: Error: "  + std::to_string(retval));
+		return retval;
+	}
 	#endif
 
     wifi_config_t wifi_config = { };
 
-    strcpy((char*)wifi_config.sta.ssid, (const char*)_ssid);
-    strcpy((char*)wifi_config.sta.password, (const char*)_password);
+	wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;		// Scan all channels instead of stopping after first match
+	wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;	// Sort by signal strength and keep up to 4 best APs
+	//wifi_config.sta.failure_retry_cnt = 3;					// IDF version 5.0 will support this
 
-    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
-    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
-    ESP_ERROR_CHECK(esp_wifi_start() );
+	#ifdef WLAN_USE_MESH_ROAMING
+	wifi_config.sta.rm_enabled = 1;		 // 802.11k (Radio Resource Management)
+	wifi_config.sta.btm_enabled = 1;	 // 802.11v (BSS Transition Management)
+	//wifi_config.sta.mbo_enabled = 1;	 // Multiband Operation (better use of Wi-Fi network resources in roaming decisions) -> not activated to save heap
+	wifi_config.sta.pmf_cfg.capable = 1; // 802.11w (Protected Management Frame, activated by default if other device also advertizes PMF capability)
+	//wifi_config.sta.ft_enabled = 1;	 // 802.11r (BSS Fast Transition) -> Upcoming IDF version 5.0 will support 11r
+	#endif
+
+    strcpy((char*)wifi_config.sta.ssid, (const char*)wlan_config.ssid.c_str());
+    strcpy((char*)wifi_config.sta.password, (const char*)wlan_config.password.c_str());
+
+    retval = esp_wifi_set_mode(WIFI_MODE_STA);
+	if (retval != ESP_OK) {
+		LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_wifi_set_mode: Error: "  + std::to_string(retval));
+		return retval;
+	}
+
+    retval = esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
+	if (retval != ESP_OK) {
+		if (retval == ESP_ERR_WIFI_PASSWORD) {
+			LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_wifi_set_config: SSID password invalid! Error: " + std::to_string(retval));
+		}
+		else {
+			LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_wifi_set_config: Error: "  + std::to_string(retval));
+		}
+		return retval;
+	}
 
-    if (_hostname != NULL)
+	retval = esp_wifi_start();
+	if (retval != ESP_OK) {
+		LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "esp_wifi_start: Error: "  + std::to_string(retval));
+		return retval;
+	}
+
+    if (!wlan_config.hostname.empty())
     {
-        esp_err_t ret = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA , _hostname);
-        hostname = std::string(_hostname);
-        if(ret != ESP_OK ){
-            ESP_LOGE(TAG,"Failed to set hostname: %d",ret);  
+        retval = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA , wlan_config.hostname.c_str());
+        if(retval != ESP_OK ) {
+			LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to set hostname! Error: " + std::to_string(retval));
         }
         else {
-            ESP_LOGI(TAG,"Set hostname to: %s", _hostname); 
+			LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Set hostname to: " + wlan_config.hostname);
         }
-
-    }
-
-    ESP_LOGI(TAG, "wifi_init_sta finished.");
-
-    /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
-     * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
-    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
-            WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
-            pdFALSE,
-            pdFALSE,
-            portMAX_DELAY);
-
-    /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually
-     * happened. */
-    if (bits & WIFI_CONNECTED_BIT) {
-        #ifdef __HIDE_PASSWORD
-            ESP_LOGI(TAG, "Connected with AP: %s, password: XXXXXXX", _ssid);
-        #else
-            ESP_LOGI(TAG, "Connected with AP: %s, password: %s", _ssid, _password);
-        #endif        
-    } else if (bits & WIFI_FAIL_BIT) {
-        #ifdef __HIDE_PASSWORD
-            ESP_LOGI(TAG, "Failed to connect with AP: %s, password: XXXXXXXX", _ssid);
-        #else
-            ESP_LOGI(TAG, "Failed to connect with AP: %s, password: %s", _ssid, _password);
-        #endif        
-    } else {
-        ESP_LOGE(TAG, "UNEXPECTED EVENT");
     }
-    ssid = std::string(_ssid);
 
-
-    /* The event will not be processed after unregister */
-//    ESP_ERROR_CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip));
-//    ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id));
-//    vEventGroupDelete(s_wifi_event_group);
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Init successful");
+	return ESP_OK;
 }
 
 
 int get_WIFI_RSSI()
 {
-    wifi_ap_record_t ap;
-    esp_wifi_sta_get_ap_info(&ap);
-    return ap.rssi;
-}
-
-
-void wifi_init_sta(const char *_ssid, const char *_password, const char *_hostname)
-{
-    wifi_init_sta(_ssid, _password, _hostname, NULL, NULL, NULL, NULL, 0);
-}
-
-
-void wifi_init_sta(const char *_ssid, const char *_password)
-{
-    wifi_init_sta(_ssid, _password, NULL, NULL, NULL, NULL, NULL, 0);
+	wifi_ap_record_t ap;
+	if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) 
+		return ap.rssi;
+	else
+		return -127;	// Return -127 if no info available e.g. not connected
 }
 
 
@@ -556,14 +668,13 @@ bool getWIFIisConnected()
 
 void WIFIDestroy() 
 {	
-    esp_wifi_disconnect();
-	
 	esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, event_handler);
     esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, event_handler);
 	#ifdef WLAN_USE_MESH_ROAMING
 	esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_BSS_RSSI_LOW, esp_bss_rssi_low_handler);
 	#endif
-	
+
+	esp_wifi_disconnect();
 	esp_wifi_stop();
 	esp_wifi_deinit();
 }

+ 8 - 7
code/components/jomjol_wlan/connect_wlan.h

@@ -5,18 +5,19 @@
 
 #include <string>
 
-void wifi_init_sta(const char *_ssid, const char *_password, const char *_hostname, const char *_ipadr, const char *_gw,  const char *_netmask, const char *_dns, int _rssithreshold);
-void wifi_init_sta(const char *_ssid, const char *_password, const char *_hostname);
-void wifi_init_sta(const char *_ssid, const char *_password);
-
+int wifi_init_sta(void);
 std::string* getIPAddress();
 std::string* getSSID();
 int get_WIFI_RSSI();
 bool getWIFIisConnected();
 void WIFIDestroy();
 
-extern std::string hostname;
-extern std::string std_hostname;
-extern int RSSIThreshold;
+#if (defined WLAN_USE_MESH_ROAMING && defined WLAN_USE_MESH_ROAMING_ACTIVATE_CLIENT_TRIGGERED_QUERIES)
+void wifiRoamingQuery(void);
+#endif
+
+#ifdef WLAN_USE_ROAMING_BY_SCANNING
+void wifiRoamByScanning(void);
+#endif
 
 #endif //CONNECT_WLAN_H

+ 191 - 167
code/components/jomjol_wlan/read_wlanini.cpp

@@ -11,9 +11,14 @@
 #include <iostream>
 #include <string.h>
 #include "esp_log.h"
+#include "ClassLogFile.h"
 #include "../../include/defines.h"
 
-static const char *TAG = "WLAN.INI";
+static const char *TAG = "WLANINI";
+
+
+struct wlan_config wlan_config = {};
+
 
 std::vector<string> ZerlegeZeileWLAN(std::string input, std::string _delimiter = "")
 {
@@ -40,207 +45,189 @@ std::vector<string> ZerlegeZeileWLAN(std::string input, std::string _delimiter =
 }
 
 
-
-bool LoadWlanFromFile(std::string fn, char *&_ssid, char *&_password, char *&_hostname, char *&_ipadr, char *&_gw,  char *&_netmask, char *&_dns, int &_rssithreshold)
+int LoadWlanFromFile(std::string fn)
 {
-    std::string ssid = "";
-    std::string passphrase = "";
-    std::string ipaddress = "";
-    std::string gw = "";
-    std::string netmask = "";
-    std::string dns = "";
-    int rssithreshold = 0;
-
     std::string line = "";
+    std::string tmp = "";
     std::vector<string> splitted;
-    hostname = std_hostname;
 
-    FILE* pFile;
     fn = FormatFileName(fn);
+    FILE* pFile = fopen(fn.c_str(), "r");
+    if (pFile == NULL) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Unable to open file (read). Device init aborted!"); 
+        return -1;
+    }
 
-    pFile = fopen(fn.c_str(), "r");
-    if (!pFile)
-        return false;
-
-    ESP_LOGD(TAG, "file loaded");
-
-    if (pFile == NULL)
-        return false;
+    ESP_LOGD(TAG, "LoadWlanFromFile: wlan.ini opened");
 
-    char zw[1024];
-    fgets(zw, 1024, pFile);
-    line = std::string(zw);
+    char zw[256];
+    if (fgets(zw, sizeof(zw), pFile) == NULL) {
+        line = "";
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "file opened, but empty or content not readable. Device init aborted!");
+        fclose(pFile);
+        return -1;
+    }
+    else {
+        line = std::string(zw);
+    }
 
     while ((line.size() > 0) || !(feof(pFile)))
     {
-//        ESP_LOGD(TAG, "%s", line.c_str());
-        splitted = ZerlegeZeileWLAN(line, "=");
-        splitted[0] = trim(splitted[0], " ");
-
-        if ((splitted.size() > 1) && (toUpper(splitted[0]) == "HOSTNAME")){
-            hostname = trim(splitted[1]);
-            if ((hostname[0] == '"') && (hostname[hostname.length()-1] == '"')){
-                hostname = hostname.substr(1, hostname.length()-2);
+        //ESP_LOGD(TAG, "line: %s", line.c_str());
+        if (line[0] != ';') {   // Skip lines which starts with ';'
+
+            splitted = ZerlegeZeileWLAN(line, "=");
+            splitted[0] = trim(splitted[0], " ");
+            
+            if ((splitted.size() > 1) && (toUpper(splitted[0]) == "SSID")){
+                tmp = trim(splitted[1]);
+                if ((tmp[0] == '"') && (tmp[tmp.length()-1] == '"')){
+                    tmp = tmp.substr(1, tmp.length()-2);
+                }
+                wlan_config.ssid = tmp;
+                LogFile.WriteToFile(ESP_LOG_INFO, TAG, "SSID: " + wlan_config.ssid);
             }
-        }
 
-        if ((splitted.size() > 1) && (toUpper(splitted[0]) == "SSID")){
-            ssid = trim(splitted[1]);
-            if ((ssid[0] == '"') && (ssid[ssid.length()-1] == '"')){
-                ssid = ssid.substr(1, ssid.length()-2);
+            else if ((splitted.size() > 1) && (toUpper(splitted[0]) == "PASSWORD")){
+                tmp = splitted[1];
+                if ((tmp[0] == '"') && (tmp[tmp.length()-1] == '"')){
+                    tmp = tmp.substr(1, tmp.length()-2);
+                }
+                wlan_config.password = tmp;
+                #ifndef __HIDE_PASSWORD
+                LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Password: " + wlan_config.password);
+                #else
+                LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Password: XXXXXXXX");
+                #endif
+            }   
+
+            else if ((splitted.size() > 1) && (toUpper(splitted[0]) == "HOSTNAME")){
+                tmp = trim(splitted[1]);
+                if ((tmp[0] == '"') && (tmp[tmp.length()-1] == '"')){
+                    tmp = tmp.substr(1, tmp.length()-2);
+                }
+                wlan_config.hostname = tmp;
+                LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Hostname: " + wlan_config.hostname);
             }
-        }
-
-        if ((splitted.size() > 1) && (toUpper(splitted[0]) == "RSSITHRESHOLD")){
-            string _s = trim(splitted[1]);
-            if ((_s[0] == '"') && (_s[_s.length()-1] == '"')){
-                _s = _s.substr(1, ssid.length()-2);
-            }
-            rssithreshold = atoi(_s.c_str());
-        }
 
-
-        if ((splitted.size() > 1) && (toUpper(splitted[0]) == "PASSWORD")){
-            passphrase = splitted[1];
-            if ((passphrase[0] == '"') && (passphrase[passphrase.length()-1] == '"')){
-                passphrase = passphrase.substr(1, passphrase.length()-2);
+            else if ((splitted.size() > 1) && (toUpper(splitted[0]) == "IP")){
+                tmp = splitted[1];
+                if ((tmp[0] == '"') && (tmp[tmp.length()-1] == '"')){
+                    tmp = tmp.substr(1, tmp.length()-2);
+                }
+                wlan_config.ipaddress = tmp;
+                LogFile.WriteToFile(ESP_LOG_INFO, TAG, "IP-Address: " + wlan_config.ipaddress);
             }
-        }
 
-        if ((splitted.size() > 1) && (toUpper(splitted[0]) == "IP")){
-            ipaddress = splitted[1];
-            if ((ipaddress[0] == '"') && (ipaddress[ipaddress.length()-1] == '"')){
-                ipaddress = ipaddress.substr(1, ipaddress.length()-2);
+            else if ((splitted.size() > 1) && (toUpper(splitted[0]) == "GATEWAY")){
+                tmp = splitted[1];
+                if ((tmp[0] == '"') && (tmp[tmp.length()-1] == '"')){
+                    tmp = tmp.substr(1, tmp.length()-2);
+                }
+                wlan_config.gateway = tmp;
+                LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Gateway: " + wlan_config.gateway);
             }
-        }
 
-        if ((splitted.size() > 1) && (toUpper(splitted[0]) == "GATEWAY")){
-            gw = splitted[1];
-            if ((gw[0] == '"') && (gw[gw.length()-1] == '"')){
-                gw = gw.substr(1, gw.length()-2);
+            else if ((splitted.size() > 1) && (toUpper(splitted[0]) == "NETMASK")){
+                tmp = splitted[1];
+                if ((tmp[0] == '"') && (tmp[tmp.length()-1] == '"')){
+                    tmp = tmp.substr(1, tmp.length()-2);
+                }
+                wlan_config.netmask = tmp;
+                LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Netmask: " + wlan_config.netmask);
             }
-        }
 
-        if ((splitted.size() > 1) && (toUpper(splitted[0]) == "NETMASK")){
-            netmask = splitted[1];
-            if ((netmask[0] == '"') && (netmask[netmask.length()-1] == '"')){
-                netmask = netmask.substr(1, netmask.length()-2);
+            else if ((splitted.size() > 1) && (toUpper(splitted[0]) == "DNS")){
+                tmp = splitted[1];
+                if ((tmp[0] == '"') && (tmp[tmp.length()-1] == '"')){
+                    tmp = tmp.substr(1, tmp.length()-2);
+                }
+                wlan_config.dns = tmp;
+                LogFile.WriteToFile(ESP_LOG_INFO, TAG, "DNS: " + wlan_config.dns);
             }
-        }
-
-        if ((splitted.size() > 1) && (toUpper(splitted[0]) == "DNS")){
-            dns = splitted[1];
-            if ((dns[0] == '"') && (dns[dns.length()-1] == '"')){
-                dns = dns.substr(1, dns.length()-2);
+            #if (defined WLAN_USE_ROAMING_BY_SCANNING || (defined WLAN_USE_MESH_ROAMING && defined WLAN_USE_MESH_ROAMING_ACTIVATE_CLIENT_TRIGGERED_QUERIES))
+            else if ((splitted.size() > 1) && (toUpper(splitted[0]) == "RSSITHRESHOLD")){
+                tmp = trim(splitted[1]);
+                if ((tmp[0] == '"') && (tmp[tmp.length()-1] == '"')){
+                    tmp = tmp.substr(1, tmp.length()-2);
+                }
+                wlan_config.rssi_threshold = atoi(tmp.c_str());
+                LogFile.WriteToFile(ESP_LOG_INFO, TAG, "RSSIThreshold: " + std::to_string(wlan_config.rssi_threshold));
             }
+            #endif
         }
 
-
-        if (fgets(zw, 1024, pFile) == NULL)
-        {
+        /* read next line */
+        if (fgets(zw, sizeof(zw), pFile) == NULL) {
             line = "";
         }
-        else
-        {
+        else {
             line = std::string(zw);
         }
     }
-
     fclose(pFile);
 
-    // Check if Hostname was empty in .ini if yes set to std_hostname
-    if(hostname.length() == 0){
-        hostname = std_hostname;
-    }
-
-    _hostname = new char[hostname.length() + 1];
-    strcpy(_hostname, hostname.c_str());
-
-    _ssid = new char[ssid.length() + 1];
-    strcpy(_ssid, ssid.c_str());
-
-    _password = new char[passphrase.length() + 1];
-    strcpy(_password, passphrase.c_str());
-
-    if (ipaddress.length() > 0)
-    {
-        _ipadr = new char[ipaddress.length() + 1];
-        strcpy(_ipadr, ipaddress.c_str());
-    }
-    else
-        _ipadr = NULL;
-
-    if (gw.length() > 0)
-    {
-        _gw = new char[gw.length() + 1];
-        strcpy(_gw, gw.c_str());
-    }
-    else
-        _gw = NULL;
-
-    if (netmask.length() > 0)
-    {
-        _netmask = new char[netmask.length() + 1];
-        strcpy(_netmask, netmask.c_str());
+    /* Check if SSID is empty (mandatory parameter) */
+    if (wlan_config.ssid.empty()) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "SSID empty. Device init aborted!");
+        return -2;
     }
-    else
-        _netmask = NULL;
 
-    if (dns.length() > 0)
-    {
-        _dns = new char[dns.length() + 1];
-        strcpy(_dns, dns.c_str());
+    /* Check if password is empty (mandatory parameter) */
+    if (wlan_config.password.empty()) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Password empty. Device init aborted!");
+        return -2;
     }
-    else
-        _dns = NULL;
 
-    _rssithreshold = rssithreshold;
-    RSSIThreshold = rssithreshold;
-    return true;
+    return 0;
 }
 
 
-
-
 bool ChangeHostName(std::string fn, std::string _newhostname)
 {
-    if (_newhostname == hostname)
+    if (_newhostname == wlan_config.hostname)
         return false;
 
-    string line = "";
+    std::string line = "";
     std::vector<string> splitted;
-
+    std::vector<string> neuesfile;
     bool found = false;
 
-    std::vector<string> neuesfile;
+    FILE* pFile = NULL;
 
-    FILE* pFile;
     fn = FormatFileName(fn);
     pFile = fopen(fn.c_str(), "r");
+    if (pFile == NULL) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "ChangeHostName: Unable to open file wlan.ini (read)"); 
+        return false;
+    }
 
-    ESP_LOGD(TAG, "file loaded\n");
+    ESP_LOGD(TAG, "ChangeHostName: wlan.ini opened");
 
-    if (pFile == NULL)
+    char zw[256];
+    if (fgets(zw, sizeof(zw), pFile) == NULL) {
+        line = "";
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "ChangeHostName: File opened, but empty or content not readable");
         return false;
-
-    char zw[1024];
-    fgets(zw, 1024, pFile);
-    line = std::string(zw);
+    }
+    else {
+        line = std::string(zw);
+    }
 
     while ((line.size() > 0) || !(feof(pFile)))
     {
-        ESP_LOGD(TAG, "%s", line.c_str());
+        //ESP_LOGD(TAG, "ChangeHostName: line: %s", line.c_str());
         splitted = ZerlegeZeileWLAN(line, "=");
         splitted[0] = trim(splitted[0], " ");
 
-        if ((splitted.size() > 1) && (toUpper(splitted[0]) == "HOSTNAME")){
+        if ((splitted.size() > 1) && ((toUpper(splitted[0]) == "HOSTNAME") || (toUpper(splitted[0]) == ";HOSTNAME"))){
             line = "hostname = \"" + _newhostname + "\"\n";
             found = true;
         }
 
         neuesfile.push_back(line);
 
-        if (fgets(zw, 1024, pFile) == NULL)
+        if (fgets(zw, sizeof(zw), pFile) == NULL)
         {
             line = "";
         }
@@ -252,52 +239,64 @@ bool ChangeHostName(std::string fn, std::string _newhostname)
 
     if (!found)
     {
-        line = "\nhostname = \"" + _newhostname + "\"\n";
+        line  = "\n;++++++++++++++++++++++++++++++++++\n";
+        line += "; Hostname: Name of device in network\n";
+        line += "; This parameter can be configured via WebUI configuration\n";
+        line += "; Default: \"watermeter\", if nothing is configured\n\n";
+        line = "hostname = \"" + _newhostname + "\"\n";
         neuesfile.push_back(line);        
     }
-
     fclose(pFile);
 
     pFile = fopen(fn.c_str(), "w+");
+    if (pFile == NULL) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "ChangeHostName: Unable to open file wlan.ini (write)"); 
+        return false;
+    }
 
     for (int i = 0; i < neuesfile.size(); ++i)
     {
-        ESP_LOGD(TAG, "%s", neuesfile[i].c_str());
+        //ESP_LOGD(TAG, "%s", neuesfile[i].c_str());
         fputs(neuesfile[i].c_str(), pFile);
     }
-
     fclose(pFile);
 
-    ESP_LOGD(TAG, "*** Hostname update done ***");
+    ESP_LOGD(TAG, "ChangeHostName done");
 
     return true;
 }
 
-
+#if (defined WLAN_USE_ROAMING_BY_SCANNING || (defined WLAN_USE_MESH_ROAMING && defined WLAN_USE_MESH_ROAMING_ACTIVATE_CLIENT_TRIGGERED_QUERIES))
 bool ChangeRSSIThreshold(std::string fn, int _newrssithreshold)
 {
-    if (RSSIThreshold == _newrssithreshold)
+    if (wlan_config.rssi_threshold == _newrssithreshold)
         return false;
 
-    string line = "";
+    std::string line = "";
     std::vector<string> splitted;
-
+    std::vector<string> neuesfile;
     bool found = false;
 
-    std::vector<string> neuesfile;
+    FILE* pFile = NULL;
 
-    FILE* pFile;
     fn = FormatFileName(fn);
     pFile = fopen(fn.c_str(), "r");
+    if (pFile == NULL) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "ChangeRSSIThreshold: Unable to open file wlan.ini (read)"); 
+        return false;
+    }
 
-    ESP_LOGD(TAG, "file loaded\n");
+    ESP_LOGD(TAG, "ChangeRSSIThreshold: wlan.ini opened");
 
-    if (pFile == NULL)
+    char zw[256];
+    if (fgets(zw, sizeof(zw), pFile) == NULL) {
+        line = "";
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "ChangeRSSIThreshold: File opened, but empty or content not readable");
         return false;
-
-    char zw[1024];
-    fgets(zw, 1024, pFile);
-    line = std::string(zw);
+    }
+    else {
+        line = std::string(zw);
+    }
 
     while ((line.size() > 0) || !(feof(pFile)))
     {
@@ -305,43 +304,68 @@ bool ChangeRSSIThreshold(std::string fn, int _newrssithreshold)
         splitted = ZerlegeZeileWLAN(line, "=");
         splitted[0] = trim(splitted[0], " ");
 
-        if ((splitted.size() > 1) && (toUpper(splitted[0]) == "RSSITHRESHOLD")){
+        /* Workaround to eliminate line with typo "RSSIThreashold" or "rssi" if existing */
+        if (((splitted.size() > 1) && (toUpper(splitted[0]) == "RSSITHREASHOLD")) ||
+            ((splitted.size() > 1) && (toUpper(splitted[0]) == ";RSSITHREASHOLD")) ||
+            ((splitted.size() > 1) && (toUpper(splitted[0]) == "RSSI")) ||
+            ((splitted.size() > 1) && (toUpper(splitted[0]) == ";RSSI"))) {
+            if (fgets(zw, sizeof(zw), pFile) == NULL) {
+                line = "";
+            }
+            else {
+                line = std::string(zw);
+            }
+            continue;
+        }
+
+        if ((splitted.size() > 1) && ((toUpper(splitted[0]) == "RSSITHRESHOLD") || (toUpper(splitted[0]) == ";RSSITHRESHOLD"))) {
             line = "RSSIThreshold = " + to_string(_newrssithreshold) + "\n";
             found = true;
         }
-
+    
         neuesfile.push_back(line);
-
-        if (fgets(zw, 1024, pFile) == NULL)
-        {
+        
+        if (fgets(zw, sizeof(zw), pFile) == NULL) {
             line = "";
         }
-        else
-        {
+        else {
             line = std::string(zw);
         }
     }
 
     if (!found)
     {
-        line = "RSSIThreshold = " + to_string(_newrssithreshold) + "\n";
+        line  = "\n;++++++++++++++++++++++++++++++++++\n";
+        line += "; WIFI Roaming:\n";
+        line += "; Network assisted roaming protocol is activated by default\n";
+        line += "; AP / mesh system needs to support roaming protocol 802.11k/v\n";
+        line += ";\n";
+        line += "; Optional feature (usually not neccessary):\n";
+        line += "; RSSI Threshold for client requested roaming query (RSSI < RSSIThreshold)\n";
+        line += "; Note: This parameter can be configured via WebUI configuration\n";
+        line += "; Default: 0 = Disable client requested roaming query\n\n";
+        line += "RSSIThreshold = " + to_string(_newrssithreshold) + "\n";
         neuesfile.push_back(line);        
     }
 
     fclose(pFile);
 
     pFile = fopen(fn.c_str(), "w+");
+    if (pFile == NULL) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "ChangeRSSIThreshold: Unable to open file wlan.ini (write)"); 
+        return false;
+    }
 
     for (int i = 0; i < neuesfile.size(); ++i)
     {
-        ESP_LOGD(TAG, "%s", neuesfile[i].c_str());
+        //ESP_LOGD(TAG, "%s", neuesfile[i].c_str());
         fputs(neuesfile[i].c_str(), pFile);
     }
 
     fclose(pFile);
 
-    ESP_LOGD(TAG, "*** RSSIThreshold update done ***");
+    ESP_LOGD(TAG, "ChangeRSSIThreshold done");
 
     return true;
 }
-
+#endif

+ 13 - 1
code/components/jomjol_wlan/read_wlanini.h

@@ -5,8 +5,20 @@
 
 #include <string>
 
-bool LoadWlanFromFile(std::string fn, char *&_ssid, char *&_password, char *&_hostname, char *&_ipadr, char *&_gw,  char *&_netmask, char *&_dns, int &_rssithreshold);
+struct wlan_config {
+    std::string ssid = "";
+    std::string password = "";
+    std::string hostname = "watermeter";    // Default: watermeter
+    std::string ipaddress = "";
+    std::string gateway = "";
+    std::string netmask = "";
+    std::string dns = "";
+    int rssi_threshold = 0;                 // Default: 0 -> ROAMING disabled
+};
+extern struct wlan_config wlan_config;
 
+
+int LoadWlanFromFile(std::string fn);
 bool ChangeHostName(std::string fn, std::string _newhostname);
 bool ChangeRSSIThreshold(std::string fn, int _newrssithreshold);
 

+ 18 - 10
code/include/defines.h

@@ -62,7 +62,7 @@
     #define FLASH_GPIO GPIO_NUM_4
     #define BLINK_GPIO GPIO_NUM_33
 
-    //ClassFlowMQTT + interface_mqtt + connect_wlan + main
+    //interface_mqtt + read_wlanini
     #define __HIDE_PASSWORD
 
     //ClassControllCamera
@@ -161,15 +161,23 @@
         }
     #define SUPRESS_TFLITE_ERRORS // use, to avoid error messages from TFLITE
 
-    //connect_wlan
-    #define WLAN_USE_MESH_ROAMING
-    #define WLAN_WIFI_RSSI_THRESHOLD -50
-    #define EXAMPLE_ESP_MAXIMUM_RETRY  1000
-    /* The event group allows multiple bits for each event, but we only care about two events:
-    * - we are connected to the AP with an IP
-    * - we failed to connect after the maximum amount of retries */
-    #define WIFI_CONNECTED_BIT BIT0
-    #define WIFI_FAIL_BIT      BIT1
+
+    // connect_wlan.cpp
+    //******************************
+    /* WIFI roaming functionalities 802.11k+v (uses ca. 6kB - 8kB internal RAM; if SCAN CACHE activated: + 1kB / beacon)
+    PLEASE BE AWARE: The following CONFIG parameters have to to be set in 
+    sdkconfig.defaults before use of this function is possible!!
+    CONFIG_WPA_11KV_SUPPORT=y
+    CONFIG_WPA_SCAN_CACHE=n
+    CONFIG_WPA_MBO_SUPPORT=n
+    CONFIG_WPA_11R_SUPPORT=n
+    */
+    //#define WLAN_USE_MESH_ROAMING   // 802.11v (BSS Transition Management) + 802.11k (Radio Resource Management) (ca. 6kB - 8kB internal RAM neccessary)
+    //#define WLAN_USE_MESH_ROAMING_ACTIVATE_CLIENT_TRIGGERED_QUERIES  // Client can send query to AP requesting to roam (if RSSI lower than RSSI threshold)
+
+    /* WIFI roaming only client triggered by scanning the channels after each round (only if RSSI < RSSIThreshold) and trigger a disconnect to switch AP */
+    #define WLAN_USE_ROAMING_BY_SCANNING
+
 
     //ClassFlowCNNGeneral
     #define Analog_error 3

+ 385 - 267
code/main/main.cpp

@@ -12,6 +12,7 @@
 //#include "esp_psram.h" // Comming in IDF 5.0, see https://docs.espressif.com/projects/esp-idf/en/v5.0-beta1/esp32/migration-guides/release-5.x/system.html?highlight=esp_psram_get_size
 //#include "spiram.h"
 #include "esp32/spiram.h"
+#include "esp_pm.h"
 
 
 // SD-Card ////////////////////
@@ -40,7 +41,10 @@
 #ifdef ENABLE_MQTT
     #include "server_mqtt.h"
 #endif //ENABLE_MQTT
-//#include "Helper.h"
+#include "Helper.h"
+#include "statusled.h"
+#include "sdcard_check.h"
+
 #include "../../include/defines.h"
 //#include "server_GPIO.h"
 
@@ -83,16 +87,13 @@ extern std::string getFwVersion(void);
 extern std::string getHTMLversion(void);
 extern std::string getHTMLcommit(void);
 
-
 std::vector<std::string> splitString(const std::string& str);
-bool replace(std::string& s, std::string const& toReplace, std::string const& replaceWith);
-bool replace(std::string& s, std::string const& toReplace, std::string const& replaceWith, bool logIt);
-//bool replace_all(std::string& s, std::string const& toReplace, std::string const& replaceWith);
-bool isInString(std::string& s, std::string const& toFind);
 void migrateConfiguration(void);
+bool setCpuFrequency(void);
 
 static const char *TAG = "MAIN";
 
+
 bool Init_NVS_SDCard()
 {
     esp_err_t ret = nvs_flash_init();
@@ -100,9 +101,8 @@ bool Init_NVS_SDCard()
         ESP_ERROR_CHECK(nvs_flash_erase());
         ret = nvs_flash_init();
     }
-////////////////////////////////////////////////
 
-    ESP_LOGI(TAG, "Using SDMMC peripheral");
+    ESP_LOGD(TAG, "Using SDMMC peripheral");
     sdmmc_host_t host = SDMMC_HOST_DEFAULT();
 
     // This initializes the slot without card detect (CD) and write protect (WP) signals.
@@ -110,20 +110,19 @@ bool Init_NVS_SDCard()
     sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
 
     // To use 1-line SD mode, uncomment the following line:
-
-#ifdef __SD_USE_ONE_LINE_MODE__
-    slot_config.width = 1;
-#endif
+    #ifdef __SD_USE_ONE_LINE_MODE__
+        slot_config.width = 1;
+    #endif
 
     // GPIOs 15, 2, 4, 12, 13 should have external 10k pull-ups.
     // Internal pull-ups are not sufficient. However, enabling internal pull-ups
     // does make a difference some boards, so we do that here.
     gpio_set_pull_mode(GPIO_NUM_15, GPIO_PULLUP_ONLY);   // CMD, needed in 4- and 1- line modes
     gpio_set_pull_mode(GPIO_NUM_2, GPIO_PULLUP_ONLY);    // D0, needed in 4- and 1-line modes
-#ifndef __SD_USE_ONE_LINE_MODE__
-    gpio_set_pull_mode(GPIO_NUM_4, GPIO_PULLUP_ONLY);    // D1, needed in 4-line mode only
-    gpio_set_pull_mode(GPIO_NUM_12, GPIO_PULLUP_ONLY);   // D2, needed in 4-line mode only
-#endif
+    #ifndef __SD_USE_ONE_LINE_MODE__
+        gpio_set_pull_mode(GPIO_NUM_4, GPIO_PULLUP_ONLY);    // D1, needed in 4-line mode only
+        gpio_set_pull_mode(GPIO_NUM_12, GPIO_PULLUP_ONLY);   // D2, needed in 4-line mode only
+    #endif
     gpio_set_pull_mode(GPIO_NUM_13, GPIO_PULLUP_ONLY);   // D3, needed in 4- and 1-line modes
 
     // Options for mounting the filesystem.
@@ -135,246 +134,330 @@ bool Init_NVS_SDCard()
         .allocation_unit_size = 16 * 1024
     };
 
+    sdmmc_card_t* card;
     // Use settings defined above to initialize SD card and mount FAT filesystem.
     // Note: esp_vfs_fat_sdmmc_mount is an all-in-one convenience function.
     // Please check its source code and implement error recovery when developing
     // production applications.
-    sdmmc_card_t* card;
     ret = esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card);
 
     if (ret != ESP_OK) {
         if (ret == ESP_FAIL) {
-            ESP_LOGE(TAG, "Failed to mount filesystem. "
-                "If you want the card to be formatted, set format_if_mount_failed = true.");
-        } else {
-            ESP_LOGE(TAG, "Failed to initialize the card (%s). "
-                "Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret));
+            ESP_LOGE(TAG, "Failed to mount FAT filesystem on SD card. Check SD card filesystem (only FAT supported) or try another card");
+            StatusLED(SDCARD_INIT, 1, true);
+        } 
+        else if (ret == 263) { // Error code: 0x107 --> usually: SD not found
+            ESP_LOGE(TAG, "SD card init failed. Check if SD card is properly inserted into SD card slot or try another card");
+            StatusLED(SDCARD_INIT, 2, true);
+        }
+        else {
+            ESP_LOGE(TAG, "SD card init failed. Check error code or try another card");
+            StatusLED(SDCARD_INIT, 3, true);
         }
         return false;
     }
 
-    sdmmc_card_print_info(stdout, card);
+    //sdmmc_card_print_info(stdout, card);  // With activated CONFIG_NEWLIB_NANO_FORMAT --> capacity not printed correctly anymore
     SaveSDCardInfo(card);
     return true;
 }
 
-void task_MainInitError_blink(void *pvParameter)
-{
-    gpio_pad_select_gpio(BLINK_GPIO);
-    gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT);  
-
-    
-    TickType_t xDelay;
-    xDelay = 100 / portTICK_PERIOD_MS;
-    ESP_LOGD(TAG, "SD-Card could not be inialized - STOP THE PROGRAMM HERE");
-
-    while (1)
-    {
-        gpio_set_level(BLINK_GPIO, 1);
-        vTaskDelay( xDelay );   
-        gpio_set_level(BLINK_GPIO, 0); 
-        vTaskDelay( xDelay );   
-
-    }
-    vTaskDelete(NULL); //Delete this task if it exits from the loop above
-}
-
 
 extern "C" void app_main(void)
 {
-   
-//#ifdef CONFIG_HEAP_TRACING_STANDALONE
-#if defined HEAP_TRACING_MAIN_WIFI || defined HEAP_TRACING_MAIN_START
-    //register a buffer to record the memory trace
-    ESP_ERROR_CHECK( heap_trace_init_standalone(trace_record, NUM_RECORDS) );
-#endif
-    
+    //#ifdef CONFIG_HEAP_TRACING_STANDALONE
+    #if defined HEAP_TRACING_MAIN_WIFI || defined HEAP_TRACING_MAIN_START
+        //register a buffer to record the memory trace
+        ESP_ERROR_CHECK( heap_trace_init_standalone(trace_record, NUM_RECORDS) );
+    #endif
+        
     TickType_t xDelay;
-    
-#ifdef DISABLE_BROWNOUT_DETECTOR
-    WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
-#endif
-    
+        
+    #ifdef DISABLE_BROWNOUT_DETECTOR
+        WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
+    #endif
 
-    ESP_LOGI(TAG, "\n\n\n\n\n"); // Add mark on log to see when it restarted
-    
+    #ifdef HEAP_TRACING_MAIN_START
+        ESP_ERROR_CHECK( heap_trace_start(HEAP_TRACE_LEAKS) );
+    #endif
+
+    // ********************************************
+    // Highlight start of app_main 
+    // ********************************************
+    ESP_LOGI(TAG, "\n\n\n\n================ Start app_main =================");
+ 
+    // Init camera
+    // ********************************************
     PowerResetCamera();
     esp_err_t camStatus = Camera.InitCam();
     Camera.LightOnOff(false);
+
     xDelay = 2000 / portTICK_PERIOD_MS;
-    ESP_LOGD(TAG, "After camera initialization: sleep for: %ldms", (long) xDelay);
-    vTaskDelay( xDelay );   
+    ESP_LOGD(TAG, "After camera initialization: sleep for: %ldms", (long) xDelay * CONFIG_FREERTOS_HZ/portTICK_PERIOD_MS);
+    vTaskDelay( xDelay );
 
+    // Init SD card
+    // ********************************************
     if (!Init_NVS_SDCard())
     {
-        xTaskCreate(&task_MainInitError_blink, "task_MainInitError_blink", configMINIMAL_STACK_SIZE * 64, NULL, tskIDLE_PRIORITY+1, NULL);
-        return; // No way to continue without SD-Card!
+        ESP_LOGE(TAG, "Device init aborted!");
+        return; // No way to continue without working SD card!
     }
 
-    migrateConfiguration();
-
-    setupTime();
+    // SD card: Create log directories (if not already existing)
+    // ********************************************
+    LogFile.CreateLogDirectories(); // mandatory for logging + image saving
 
-    string versionFormated = getFwVersion() + ", Date/Time: " + std::string(BUILD_TIME) + \
-        ", Web UI: " + getHTMLversion();
+    // ********************************************
+    // Highlight start of logfile logging
+    // Default Log Level: INFO -> Everything which needs to be logged during boot should be have level INFO, WARN OR ERROR
+    // ********************************************
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "=================================================");
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "==================== Start ======================");
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "=================================================");
 
-    if (std::string(GIT_TAG) != "") { // We are on a tag, add it as prefix
-        string versionFormated = "Tag: '" + std::string(GIT_TAG) + "', " + versionFormated;
+    // SD card: basic R/W check
+    // ********************************************
+    int iSDCardStatus = SDCardCheckRW();
+    if (iSDCardStatus < 0) {
+        if (iSDCardStatus <= -1 && iSDCardStatus >= -2) { // write error
+            StatusLED(SDCARD_CHECK, 1, true);
+        }
+        else if (iSDCardStatus <= -3 && iSDCardStatus >= -5) { // read error
+            StatusLED(SDCARD_CHECK, 2, true);
+        }
+        else if (iSDCardStatus == -6) { // delete error
+            StatusLED(SDCARD_CHECK, 3, true);
+        }
+        setSystemStatusFlag(SYSTEM_STATUS_SDCARD_CHECK_BAD); // reduced web interface going to be loaded
     }
 
-    LogFile.CreateLogDirectories();
-    MakeDir("/sdcard/demo");            // needed for demo mode
+    // Migrate parameter in config.ini to new naming (firmware 15.0 and newer)
+    // ********************************************
+    migrateConfiguration();
 
+    // Init time (as early as possible, but SD card needs to be initialized)
+    // ********************************************
+    setupTime();    // NTP time service: Status of time synchronization will be checked after every round (server_tflite.cpp)
 
-    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "=================================================");
-    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "==================== Startup ====================");
-    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "=================================================");
-    LogFile.WriteToFile(ESP_LOG_INFO, TAG, versionFormated);
-    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Reset reason: " + getResetReason());
-    
-#ifdef DEBUG_ENABLE_SYSINFO
-    #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL( 4, 0, 0 )
-        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Device Info : " + get_device_info() );
-        ESP_LOGD(TAG, "Device infos %s", get_device_info().c_str());
-    #endif
-#endif //DEBUG_ENABLE_SYSINFO
 
-#ifdef USE_HIMEM_IF_AVAILABLE
-    #ifdef DEBUG_HIMEM_MEMORY_CHECK
-        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Himem mem check : " + himem_memory_check() );
-        ESP_LOGD(TAG, "Himem mem check %s", himem_memory_check().c_str());
-    #endif
-#endif
+    // Set CPU Frequency
+    // ********************************************
+    setCpuFrequency();
 
-    CheckIsPlannedReboot();
+
+    // SD card: Create further mandatory directories (if not already existing)
+    // Correct creation of these folders will be checked with function "SDCardCheckFolderFilePresence"
+    // ********************************************
+    MakeDir("/sdcard/firmware");         // mandatory for OTA firmware update
+    MakeDir("/sdcard/img_tmp");          // mandatory for setting up alignment marks
+    MakeDir("/sdcard/demo");             // mandatory for demo mode
+
+    // Check for updates
+    // ********************************************
     CheckOTAUpdate();
     CheckUpdate();
+
+    // Start SoftAP for initial remote setup
+    // Note: Start AP if no wlan.ini and/or config.ini available, e.g. SD card empty; function does not exit anymore until reboot
+    // ********************************************
     #ifdef ENABLE_SOFTAP
-        CheckStartAPMode();          // if no wlan.ini and/or config.ini --> AP ist startet and this function does not exit anymore until reboot
+        CheckStartAPMode(); 
     #endif
 
-#ifdef HEAP_TRACING_MAIN_WIFI
-    ESP_ERROR_CHECK( heap_trace_start(HEAP_TRACE_LEAKS) );
-#endif
-    
-    char *ssid = NULL, *passwd = NULL, *hostname = NULL, *ip = NULL, *gateway = NULL, *netmask = NULL, *dns = NULL; int rssithreshold = 0;
-    LoadWlanFromFile(WLAN_CONFIG_FILE, ssid, passwd, hostname, ip, gateway, netmask, dns, rssithreshold);
-
-    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "WLAN-Settings - RSSI-Threshold: " + to_string(rssithreshold));
-
-    if (ssid != NULL && passwd != NULL)
-#ifdef __HIDE_PASSWORD
-        ESP_LOGD(TAG, "WLan: %s, XXXXXX", ssid);
-#else
-        ESP_LOGD(TAG, "WLan: %s, %s", ssid, passwd);
-#endif        
-
-    else
-        ESP_LOGD(TAG, "No SSID and PASSWORD set!!!");
-
-    if (hostname != NULL)
-        ESP_LOGD(TAG, "Hostname: %s", hostname);
-    else
-        ESP_LOGD(TAG, "Hostname not set");
+    // SD card: Check presence of some mandatory folders / files
+    // ********************************************
+    if (!SDCardCheckFolderFilePresence()) {
+        StatusLED(SDCARD_CHECK, 4, true);
+        setSystemStatusFlag(SYSTEM_STATUS_FOLDER_CHECK_BAD); // reduced web interface going to be loaded
+    }
 
-    if (ip != NULL && gateway != NULL && netmask != NULL)
-       ESP_LOGD(TAG, "Fixed IP: %s, Gateway %s, Netmask %s", ip, gateway, netmask);
-    if (dns != NULL)
-       ESP_LOGD(TAG, "DNS IP: %s", dns);
+    // Check version information
+    // ********************************************
+    std::string versionFormated = getFwVersion() + ", Date/Time: " + std::string(BUILD_TIME) + \
+        ", Web UI: " + getHTMLversion();
 
+    if (std::string(GIT_TAG) != "") { // We are on a tag, add it as prefix
+        versionFormated = "Tag: '" + std::string(GIT_TAG) + "', " + versionFormated;
+    }
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, versionFormated);
 
-    wifi_init_sta(ssid, passwd, hostname, ip, gateway, netmask, dns, rssithreshold);   
+    if (getHTMLcommit().substr(0, 7) == "?")
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, std::string("Failed to read file html/version.txt to parse Web UI version"));
+ 
+    if (getHTMLcommit().substr(0, 7) != std::string(GIT_REV).substr(0, 7)) { // Compare the first 7 characters of both hashes
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Web UI version (" + getHTMLcommit() + ") does not match firmware version (" + std::string(GIT_REV) + ")");
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Recommendation: Repeat installation using AI-on-the-edge-device__update__*.zip");    
+    }
 
+    // Check reboot reason
+    // ********************************************
+    CheckIsPlannedReboot();
+    if (!getIsPlannedReboot() && (esp_reset_reason() == ESP_RST_PANIC)) {  // If system reboot was not triggered by user and reboot was caused by execption 
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Reset reason: " + getResetReason());
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Device was rebooted due to a software exception! Log level is set to DEBUG until the next reboot. "
+                                               "Flow init is delayed by 5 minutes to check the logs or do an OTA update"); 
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Keep device running until crash occurs again and check logs after device is up again");
+        LogFile.setLogLevel(ESP_LOG_DEBUG);
+    }
+    else {
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Reset reason: " + getResetReason());
+    }
 
-    xDelay = 2000 / portTICK_PERIOD_MS;
-    ESP_LOGD(TAG, "main: sleep for: %ldms", (long) xDelay);
-    vTaskDelay( xDelay );   
+    #ifdef HEAP_TRACING_MAIN_START
+        ESP_ERROR_CHECK( heap_trace_stop() );
+        heap_trace_dump(); 
+    #endif
     
-#ifdef HEAP_TRACING_MAIN_WIFI
-    ESP_ERROR_CHECK( heap_trace_stop() );
-    heap_trace_dump(); 
-#endif   
-
-#ifdef HEAP_TRACING_MAIN_START
-    ESP_ERROR_CHECK( heap_trace_start(HEAP_TRACE_LEAKS) );
-#endif
-
-    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "=================================================");
-    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "================== Main Started =================");
-    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "=================================================");
+    #ifdef HEAP_TRACING_MAIN_WIFI
+        ESP_ERROR_CHECK( heap_trace_start(HEAP_TRACE_LEAKS) );
+    #endif
 
-    if (getHTMLcommit().substr(0, 7) != std::string(GIT_REV).substr(0, 7)) { // Compare the first 7 characters of both hashes
-        LogFile.WriteToFile(ESP_LOG_WARN, TAG, std::string("Web UI version (") + getHTMLcommit() + ") does not match firmware version (" + std::string(GIT_REV) + ") !");
-        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Please make sure to setup the SD-Card properly (check the documentation) or re-install using the AI-on-the-edge-device__update__*.zip!");    
+    // Read WLAN parameter and start WIFI
+    // ********************************************
+    int iWLANStatus = LoadWlanFromFile(WLAN_CONFIG_FILE);
+    if (iWLANStatus == 0) {
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "WLAN config loaded, init WIFI...");
+        if (wifi_init_sta() != ESP_OK) {
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "WIFI init failed. Device init aborted!");
+            StatusLED(WLAN_INIT, 3, true);
+            return;
+        }
+    }
+    else if (iWLANStatus == -1) {  // wlan.ini not available, potentially empty or content not readable
+        StatusLED(WLAN_INIT, 1, true);
+        return; // No way to continue without reading the wlan.ini
+    }
+    else if (iWLANStatus == -2) { // SSID or password not configured
+        StatusLED(WLAN_INIT, 2, true);
+        return; // No way to continue with empty SSID or password!
     }
 
-    std::string zw = getCurrentTimeString("%Y%m%d-%H%M%S");
-    ESP_LOGD(TAG, "time %s", zw.c_str());
-    
-#ifdef HEAP_TRACING_MAIN_START
-    ESP_ERROR_CHECK( heap_trace_stop() );
-    heap_trace_dump(); 
-#endif  
-
-    /* Check if PSRAM can be initalized */
-    esp_err_t ret;
-    ret = esp_spiram_init();
-    if (ret == ESP_FAIL) { // Failed to init PSRAM, most likely not available or broken
-        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to initialize PSRAM (" + std::to_string(ret) + ")!");
-        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Either your device misses the PSRAM chip or it is broken!");
+    xDelay = 2000 / portTICK_PERIOD_MS;
+    ESP_LOGD(TAG, "main: sleep for: %ldms", (long) xDelay * CONFIG_FREERTOS_HZ/portTICK_PERIOD_MS);
+    vTaskDelay( xDelay );
+
+    // Set log level for wifi component to WARN level (default: INFO; only relevant for serial console)
+    // ********************************************
+    esp_log_level_set("wifi", ESP_LOG_WARN);
+  
+    #ifdef HEAP_TRACING_MAIN_WIFI
+        ESP_ERROR_CHECK( heap_trace_stop() );
+        heap_trace_dump(); 
+    #endif   
+
+    #ifdef DEBUG_ENABLE_SYSINFO
+        #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL( 4, 0, 0 )
+            LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Device Info : " + get_device_info() );
+            ESP_LOGD(TAG, "Device infos %s", get_device_info().c_str());
+        #endif
+    #endif //DEBUG_ENABLE_SYSINFO
+
+    #ifdef USE_HIMEM_IF_AVAILABLE
+        #ifdef DEBUG_HIMEM_MEMORY_CHECK
+            LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Himem mem check : " + himem_memory_check() );
+            ESP_LOGD(TAG, "Himem mem check %s", himem_memory_check().c_str());
+        #endif
+    #endif
+   
+    // Init external PSRAM
+    // ********************************************
+    esp_err_t PSRAMStatus = esp_spiram_init();
+    if (PSRAMStatus != ESP_OK) {  // ESP_FAIL -> Failed to init PSRAM
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "PSRAM init failed (" + std::to_string(PSRAMStatus) + ")! PSRAM not found or defective");
         setSystemStatusFlag(SYSTEM_STATUS_PSRAM_BAD);
+        StatusLED(PSRAM_INIT, 1, true);
     }
-    else { // PSRAM init ok
-        /* Check if PSRAM provides at least 4 MB */
-        size_t psram_size = esp_spiram_get_size();        
-        // size_t psram_size = esp_psram_get_size(); // comming in IDF 5.0
-        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "The device has " + std::to_string(psram_size/1024/1024) + " MBytes of PSRAM");
-        if (psram_size < (4*1024*1024)) { // PSRAM is below 4 MBytes
-            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "At least 4 MBytes are required!");
-            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Does the device really have a 4 Mbytes PSRAM?");
+    else { // ESP_OK -> PSRAM init OK --> continue to check PSRAM size
+        size_t psram_size = esp_spiram_get_size(); // size_t psram_size = esp_psram_get_size(); // comming in IDF 5.0
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "PSRAM size: " + std::to_string(psram_size) + " byte (" + std::to_string(psram_size/1024/1024) + 
+                                               "MB / " + std::to_string(psram_size/1024/1024*8) + "MBit)");
+
+        // Check PSRAM size
+        // ********************************************
+        if (psram_size < (4*1024*1024)) { // PSRAM is below 4 MBytes (32Mbit)
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "PSRAM size >= 4MB (32Mbit) is mandatory to run this application");
             setSystemStatusFlag(SYSTEM_STATUS_PSRAM_BAD);
+            StatusLED(PSRAM_INIT, 2, true);
         }
-    }
-
-    /* Check available Heap memory */
-    size_t _hsize = getESPHeapSize();
-    if (_hsize < 4000000) { // Check available Heap memory for a bit less than 4 MB (a test on a good device showed 4187558 bytes to be available)
-        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Not enough Heap memory available. Expected around 4 MBytes, but only " + std::to_string(_hsize) + " Bytes are available! That is not enough for this firmware!");
-        setSystemStatusFlag(SYSTEM_STATUS_HEAP_TOO_SMALL);
-    } else { // Heap memory is ok
-        if (camStatus != ESP_OK) {
-            LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Failed to initialize camera module, retrying...");
-
-            PowerResetCamera();
-            esp_err_t camStatus = Camera.InitCam();
-            Camera.LightOnOff(false);
-            xDelay = 2000 / portTICK_PERIOD_MS;
-            ESP_LOGD(TAG, "After camera initialization: sleep for: %ldms", (long) xDelay);
-            vTaskDelay( xDelay ); 
-
-            if (camStatus != ESP_OK) {
-                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to initialize camera module!");
-                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Check that your camera module is working and connected properly!");
-                setSystemStatusFlag(SYSTEM_STATUS_CAM_BAD);
-            }
-        } else { // Test Camera    
-            if (!Camera.testCamera()) {
-                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Camera Framebuffer cannot be initialized!");
-                /* Easiest would be to simply restart here and try again,
-                   how ever there seem to be systems where it fails at startup but still work corectly later.
-                   Therefore we treat it still as successed! */
-                   setSystemStatusFlag(SYSTEM_STATUS_CAM_FB_BAD);
+        else { // PSRAM size OK --> continue to check heap size
+            size_t _hsize = getESPHeapSize();
+            LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Total heap: " + std::to_string(_hsize) + " byte");
+
+            // Check heap memory
+            // ********************************************
+            if (_hsize < 4000000) { // Check available Heap memory for a bit less than 4 MB (a test on a good device showed 4187558 bytes to be available)
+                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Total heap >= 4000000 byte is mandatory to run this application");
+                setSystemStatusFlag(SYSTEM_STATUS_HEAP_TOO_SMALL);
+                StatusLED(PSRAM_INIT, 3, true);
             }
-            else {
-                Camera.LightOnOff(false);
+            else { // HEAP size OK --> continue to check camera init
+                // Check camera init
+                // ********************************************
+                if (camStatus != ESP_OK) { // Camera init failed, retry to init
+                    char camStatusHex[33];
+                    sprintf(camStatusHex,"0x%02x", camStatus);
+                    LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Camera init failed (" + std::string(camStatusHex) + "), retrying...");
+
+                    PowerResetCamera();
+                    camStatus = Camera.InitCam();
+                    Camera.LightOnOff(false);
+
+                    xDelay = 2000 / portTICK_PERIOD_MS;
+                    ESP_LOGD(TAG, "After camera initialization: sleep for: %ldms", (long) xDelay * CONFIG_FREERTOS_HZ/portTICK_PERIOD_MS);
+                    vTaskDelay( xDelay ); 
+
+                    if (camStatus != ESP_OK) { // Camera init failed again
+                        sprintf(camStatusHex,"0x%02x", camStatus);
+                        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Camera init failed (" + std::string(camStatusHex) +
+                                                                ")! Check camera module and/or proper electrical connection");
+                        setSystemStatusFlag(SYSTEM_STATUS_CAM_BAD);
+                        StatusLED(CAM_INIT, 1, true);
+                    }
+                }
+                else { // ESP_OK -> Camera init OK --> continue to perform camera framebuffer check
+                    // Camera framebuffer check
+                    // ********************************************
+                    if (!Camera.testCamera()) {
+                        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Camera framebuffer check failed");
+                        // Easiest would be to simply restart here and try again,
+                        // how ever there seem to be systems where it fails at startup but still work correctly later.
+                        // Therefore we treat it still as successed! */
+                        setSystemStatusFlag(SYSTEM_STATUS_CAM_FB_BAD);
+                        StatusLED(CAM_INIT, 2, false);
+                    }
+                    Camera.LightOnOff(false);   // make sure flashlight is off before start of flow
+
+                    // Print camera infos
+                    // ********************************************
+                    char caminfo[50];
+                    sensor_t * s = esp_camera_sensor_get();
+                    sprintf(caminfo, "PID: 0x%02x, VER: 0x%02x, MIDL: 0x%02x, MIDH: 0x%02x", s->id.PID, s->id.VER, s->id.MIDH, s->id.MIDL);
+                    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Camera info: " + std::string(caminfo));
+                }
             }
         }
     }
 
+    // Print Device info
+    // ********************************************
+    esp_chip_info_t chipInfo;
+    esp_chip_info(&chipInfo);
+    
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Device info: CPU cores: " + std::to_string(chipInfo.cores) + 
+                                           ", Chip revision: " + std::to_string(chipInfo.revision));
+    
+    // Print SD-Card info
+    // ********************************************
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "SD card info: Name: " + getSDCardName() + ", Capacity: " + 
+                        getSDCardCapacity() + "MB, Free: " + getSDCardFreePartitionSpace() + "MB");
+
     xDelay = 2000 / portTICK_PERIOD_MS;
-    ESP_LOGD(TAG, "main: sleep for: %ldms", (long) xDelay*10);
+    ESP_LOGD(TAG, "main: sleep for: %ldms", (long) xDelay * CONFIG_FREERTOS_HZ/portTICK_PERIOD_MS);
     vTaskDelay( xDelay ); 
 
+    // Start webserver + register handler
+    // ********************************************
     ESP_LOGD(TAG, "starting servers");
 
     server = start_webserver();   
@@ -391,25 +474,23 @@ extern "C" void app_main(void)
     ESP_LOGD(TAG, "Before reg server main");
     register_server_main_uri(server, "/sdcard");
 
-
-    /* Testing */
+    // Only for testing purpose
     //setSystemStatusFlag(SYSTEM_STATUS_CAM_FB_BAD);
     //setSystemStatusFlag(SYSTEM_STATUS_PSRAM_BAD);
 
-    /* Main Init has successed or only an error which allows to continue operation */
+    // Check main init + start TFlite task
+    // ********************************************
     if (getSystemStatus() == 0) { // No error flag is set
-        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Initialization completed successfully!");
-        ESP_LOGD(TAG, "Before do autostart");
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Initialization completed successfully! Starting flow task ...");
         TFliteDoAutoStart();
     }
     else if (isSetSystemStatusFlag(SYSTEM_STATUS_CAM_FB_BAD) || // Non critical errors occured, we try to continue...
-        isSetSystemStatusFlag(SYSTEM_STATUS_NTP_BAD)) {
-        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Initialization completed with errors, but trying to continue...");
-        ESP_LOGD(TAG, "Before do autostart");
+             isSetSystemStatusFlag(SYSTEM_STATUS_NTP_BAD)) {
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "Initialization completed with errors! Starting flow task ...");
         TFliteDoAutoStart();
     }
-    else { // Any other error is critical and makes running the flow impossible.
-        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Initialization failed. Not starting flows!");
+    else { // Any other error is critical and makes running the flow impossible. Init is going to abort.
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Initialization failed. Flow task start aborted. Loading reduced web interface...");
     }
 }
 
@@ -435,7 +516,7 @@ void migrateConfiguration(void) {
 
         if (configLines[i].find("[") != std::string::npos) { // Start of new section
             section = configLines[i];
-            replace(section, ";", "", false); // Remove possible semicolon (just for the string comparison)
+            replaceString(section, ";", "", false); // Remove possible semicolon (just for the string comparison)
             //ESP_LOGI(TAG, "New section: %s", section.c_str());
         }
 
@@ -450,79 +531,79 @@ void migrateConfiguration(void) {
          *  - Only one whitespace before/after the equal sign
          */
         if (section == "[MakeImage]") {
-            migrated = migrated | replace(configLines[i], "[MakeImage]", "[TakeImage]"); // Rename the section itself
+            migrated = migrated | replaceString(configLines[i], "[MakeImage]", "[TakeImage]"); // Rename the section itself
         }
 
         if (section == "[MakeImage]" || section == "[TakeImage]") {
-            migrated = migrated | replace(configLines[i], "LogImageLocation", "RawImagesLocation");
-            migrated = migrated | replace(configLines[i], "LogfileRetentionInDays", "RawImagesRetention");
+            migrated = migrated | replaceString(configLines[i], "LogImageLocation", "RawImagesLocation");
+            migrated = migrated | replaceString(configLines[i], "LogfileRetentionInDays", "RawImagesRetention");
 
-            migrated = migrated | replace(configLines[i], ";Demo = true", ";Demo = false"); // Set it to its default value
-            migrated = migrated | replace(configLines[i], ";Demo", "Demo"); // Enable it
+            migrated = migrated | replaceString(configLines[i], ";Demo = true", ";Demo = false"); // Set it to its default value
+            migrated = migrated | replaceString(configLines[i], ";Demo", "Demo"); // Enable it
 
-            migrated = migrated | replace(configLines[i], ";FixedExposure = true", ";FixedExposure = false"); // Set it to its default value
-            migrated = migrated | replace(configLines[i], ";FixedExposure", "FixedExposure"); // Enable it
+            migrated = migrated | replaceString(configLines[i], ";FixedExposure = true", ";FixedExposure = false"); // Set it to its default value
+            migrated = migrated | replaceString(configLines[i], ";FixedExposure", "FixedExposure"); // Enable it
         }
 
         if (section == "[Alignment]") {
-            migrated = migrated | replace(configLines[i], ";InitialMirror = true", ";InitialMirror = false"); // Set it to its default value
-            migrated = migrated | replace(configLines[i], ";InitialMirror", "InitialMirror"); // Enable it
+            migrated = migrated | replaceString(configLines[i], ";InitialMirror = true", ";InitialMirror = false"); // Set it to its default value
+            migrated = migrated | replaceString(configLines[i], ";InitialMirror", "InitialMirror"); // Enable it
 
-            migrated = migrated | replace(configLines[i], ";FlipImageSize = true", ";FlipImageSize = false"); // Set it to its default value
-            migrated = migrated | replace(configLines[i], ";FlipImageSize", "FlipImageSize"); // Enable it
+            migrated = migrated | replaceString(configLines[i], ";FlipImageSize = true", ";FlipImageSize = false"); // Set it to its default value
+            migrated = migrated | replaceString(configLines[i], ";FlipImageSize", "FlipImageSize"); // Enable it
         }
 
         if (section == "[Digits]") {
-            migrated = migrated | replace(configLines[i], "LogImageLocation", "ROIImagesLocation");
-            migrated = migrated | replace(configLines[i], "LogfileRetentionInDays", "ROIImagesRetention");
+            migrated = migrated | replaceString(configLines[i], "LogImageLocation", "ROIImagesLocation");
+            migrated = migrated | replaceString(configLines[i], "LogfileRetentionInDays", "ROIImagesRetention");
         }
 
         if (section == "[Analog]") {
-            migrated = migrated | replace(configLines[i], "LogImageLocation", "ROIImagesLocation");
-            migrated = migrated | replace(configLines[i], "LogfileRetentionInDays", "ROIImagesRetention");
-            migrated = migrated | replace(configLines[i], "ExtendedResolution", ";UNUSED_PARAMETER"); // This parameter is no longer used
+            migrated = migrated | replaceString(configLines[i], "LogImageLocation", "ROIImagesLocation");
+            migrated = migrated | replaceString(configLines[i], "LogfileRetentionInDays", "ROIImagesRetention");
+            migrated = migrated | replaceString(configLines[i], "ExtendedResolution", ";UNUSED_PARAMETER"); // This parameter is no longer used
         }
 
         if (section == "[PostProcessing]") {
-            migrated = migrated | replace(configLines[i], ";PreValueUse = true", ";PreValueUse = false"); // Set it to its default value
-            migrated = migrated | replace(configLines[i], ";PreValueUse", "PreValueUse"); // Enable it
+            migrated = migrated | replaceString(configLines[i], ";PreValueUse = true", ";PreValueUse = false"); // Set it to its default value
+            migrated = migrated | replaceString(configLines[i], ";PreValueUse", "PreValueUse"); // Enable it
 
             /* AllowNegativeRates has a <NUMBER> as prefix! */
             if (isInString(configLines[i], "AllowNegativeRates") && isInString(configLines[i], ";")) { // It is the parameter "AllowNegativeRates" and it is commented out
-                migrated = migrated | replace(configLines[i], "true", "false"); // Set it to its default value
-                migrated = migrated | replace(configLines[i], ";", ""); // Enable it
+                migrated = migrated | replaceString(configLines[i], "true", "false"); // Set it to its default value
+                migrated = migrated | replaceString(configLines[i], ";", ""); // Enable it
             }
 
             /* IgnoreLeadingNaN has a <NUMBER> as prefix! */
             if (isInString(configLines[i], "IgnoreLeadingNaN") && isInString(configLines[i], ";")) { // It is the parameter "IgnoreLeadingNaN" and it is commented out
-                migrated = migrated | replace(configLines[i], "true", "false"); // Set it to its default value
-                migrated = migrated | replace(configLines[i], ";", ""); // Enable it
+                migrated = migrated | replaceString(configLines[i], "true", "false"); // Set it to its default value
+                migrated = migrated | replaceString(configLines[i], ";", ""); // Enable it
             }
 
             /* ExtendedResolution has a <NUMBER> as prefix! */
             if (isInString(configLines[i], "ExtendedResolution") && isInString(configLines[i], ";")) { // It is the parameter "ExtendedResolution" and it is commented out
-                migrated = migrated | replace(configLines[i], "true", "false"); // Set it to its default value
-                migrated = migrated | replace(configLines[i], ";", ""); // Enable it
+                migrated = migrated | replaceString(configLines[i], "true", "false"); // Set it to its default value
+                migrated = migrated | replaceString(configLines[i], ";", ""); // Enable it
             }
 
-            migrated = migrated | replace(configLines[i], ";ErrorMessage = true", ";ErrorMessage = false"); // Set it to its default value
-            migrated = migrated | replace(configLines[i], ";ErrorMessage", "ErrorMessage"); // Enable it
+            migrated = migrated | replaceString(configLines[i], ";ErrorMessage = true", ";ErrorMessage = false"); // Set it to its default value
+            migrated = migrated | replaceString(configLines[i], ";ErrorMessage", "ErrorMessage"); // Enable it
 
-            migrated = migrated | replace(configLines[i], ";CheckDigitIncreaseConsistency = true", ";CheckDigitIncreaseConsistency = false"); // Set it to its default value
-            migrated = migrated | replace(configLines[i], ";CheckDigitIncreaseConsistency", "CheckDigitIncreaseConsistency"); // Enable it
+            migrated = migrated | replaceString(configLines[i], ";CheckDigitIncreaseConsistency = true", ";CheckDigitIncreaseConsistency = false"); // Set it to its default value
+            migrated = migrated | replaceString(configLines[i], ";CheckDigitIncreaseConsistency", "CheckDigitIncreaseConsistency"); // Enable it
         }
 
         if (section == "[MQTT]") {
-            migrated = migrated | replace(configLines[i], "SetRetainFlag", "RetainMessages"); // First rename it, enable it with its default value
-            migrated = migrated | replace(configLines[i], ";RetainMessages = true", ";RetainMessages = false"); // Set it to its default value
-            migrated = migrated | replace(configLines[i], ";RetainMessages", "RetainMessages"); // Enable it
+            migrated = migrated | replaceString(configLines[i], "SetRetainFlag", "RetainMessages"); // First rename it, enable it with its default value
+            migrated = migrated | replaceString(configLines[i], ";RetainMessages = true", ";RetainMessages = false"); // Set it to its default value
+            migrated = migrated | replaceString(configLines[i], ";RetainMessages", "RetainMessages"); // Enable it
 
-            migrated = migrated | replace(configLines[i], ";HomeassistantDiscovery = true", ";HomeassistantDiscovery = false"); // Set it to its default value
-            migrated = migrated | replace(configLines[i], ";HomeassistantDiscovery", "HomeassistantDiscovery"); // Enable it
+            migrated = migrated | replaceString(configLines[i], ";HomeassistantDiscovery = true", ";HomeassistantDiscovery = false"); // Set it to its default value
+            migrated = migrated | replaceString(configLines[i], ";HomeassistantDiscovery", "HomeassistantDiscovery"); // Enable it
 
             if (configLines[i].rfind("Topic", 0) != std::string::npos)  // only if string starts with "Topic" (Was the naming in very old version)
             {
-                migrated = migrated | replace(configLines[i], "Topic", "MainTopic");
+                migrated = migrated | replaceString(configLines[i], "Topic", "MainTopic");
             }
         }
 
@@ -535,34 +616,34 @@ void migrateConfiguration(void) {
         }
 
         if (section == "[DataLogging]") {
-            migrated = migrated | replace(configLines[i], "DataLogRetentionInDays", "DataFilesRetention");
+            migrated = migrated | replaceString(configLines[i], "DataLogRetentionInDays", "DataFilesRetention");
             /* DataLogActive is true by default! */
-            migrated = migrated | replace(configLines[i], ";DataLogActive = false", ";DataLogActive = true"); // Set it to its default value
-            migrated = migrated | replace(configLines[i], ";DataLogActive", "DataLogActive"); // Enable it
+            migrated = migrated | replaceString(configLines[i], ";DataLogActive = false", ";DataLogActive = true"); // Set it to its default value
+            migrated = migrated | replaceString(configLines[i], ";DataLogActive", "DataLogActive"); // Enable it
         }
 
         if (section == "[AutoTimer]") {
-            migrated = migrated | replace(configLines[i], "Intervall", "Interval");
-            migrated = migrated | replace(configLines[i], ";AutoStart = true", ";AutoStart = false"); // Set it to its default value
-            migrated = migrated | replace(configLines[i], ";AutoStart", "AutoStart"); // Enable it
+            migrated = migrated | replaceString(configLines[i], "Intervall", "Interval");
+            migrated = migrated | replaceString(configLines[i], ";AutoStart = true", ";AutoStart = false"); // Set it to its default value
+            migrated = migrated | replaceString(configLines[i], ";AutoStart", "AutoStart"); // Enable it
 
         }
 
         if (section == "[Debug]") {
-            migrated = migrated | replace(configLines[i], "Logfile ", "LogLevel "); // Whitespace needed so it does not match `LogfileRetentionInDays`
+            migrated = migrated | replaceString(configLines[i], "Logfile ", "LogLevel "); // Whitespace needed so it does not match `LogfileRetentionInDays`
             /* LogLevel (resp. LogFile) was originally a boolean, but we switched it to an int
              * For both cases (true/false), we set it to level 2 (WARNING) */
-            migrated = migrated | replace(configLines[i], "LogLevel = true", "LogLevel = 2");
-            migrated = migrated | replace(configLines[i], "LogLevel = false", "LogLevel = 2");
-            migrated = migrated | replace(configLines[i], "LogfileRetentionInDays", "LogfilesRetention");
+            migrated = migrated | replaceString(configLines[i], "LogLevel = true", "LogLevel = 2");
+            migrated = migrated | replaceString(configLines[i], "LogLevel = false", "LogLevel = 2");
+            migrated = migrated | replaceString(configLines[i], "LogfileRetentionInDays", "LogfilesRetention");
         }
 
         if (section == "[System]") {
-            migrated = migrated | replace(configLines[i], "RSSIThreashold", "RSSIThreshold");
-            migrated = migrated | replace(configLines[i], "AutoAdjustSummertime", ";UNUSED_PARAMETER"); // This parameter is no longer used
+            migrated = migrated | replaceString(configLines[i], "RSSIThreashold", "RSSIThreshold");
+            migrated = migrated | replaceString(configLines[i], "AutoAdjustSummertime", ";UNUSED_PARAMETER"); // This parameter is no longer used
 
-            migrated = migrated | replace(configLines[i], ";SetupMode = true", ";SetupMode = false"); // Set it to its default value
-            migrated = migrated | replace(configLines[i], ";SetupMode", "SetupMode"); // Enable it
+            migrated = migrated | replaceString(configLines[i], ";SetupMode = true", ";SetupMode = false"); // Set it to its default value
+            migrated = migrated | replaceString(configLines[i], ";SetupMode", "SetupMode"); // Enable it
         }
     }
 
@@ -625,31 +706,68 @@ std::vector<std::string> splitString(const std::string& str) {
 }*/
 
 
-bool replace(std::string& s, std::string const& toReplace, std::string const& replaceWith) {
-    return replace(s, toReplace, replaceWith, true);
-}
+bool setCpuFrequency(void) {
+    ConfigFile configFile = ConfigFile(CONFIG_FILE); 
+    string cpuFrequency = "160";
+    esp_pm_config_esp32_t  pm_config; 
+
+    if (!configFile.ConfigFileExists()){
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "No ConfigFile defined - exit setCpuFrequency()!");
+        return false;
+    }
 
-bool replace(std::string& s, std::string const& toReplace, std::string const& replaceWith, bool logIt) {
-    std::size_t pos = s.find(toReplace);
+    std::vector<std::string> splitted;
+    std::string line = "";
+    bool disabledLine = false;
+    bool eof = false;
 
-    if (pos == std::string::npos) { // Not found
+
+    /* Load config from config file */
+    while ((!configFile.GetNextParagraph(line, disabledLine, eof) || 
+            (line.compare("[System]") != 0)) && !eof) {}
+    if (eof) {
         return false;
     }
 
-    std::string old = s;
-    s.replace(pos, toReplace.length(), replaceWith);
-    if (logIt) {
-        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Migrated Configfile line '" + old + "' to '" + s + "'");
+    if (disabledLine) {
+        return false;
     }
-    return true;
-}
 
+    while (configFile.getNextLine(&line, disabledLine, eof) && 
+            !configFile.isNewParagraph(line)) {
+        splitted = ZerlegeZeile(line);
 
-bool isInString(std::string& s, std::string const& toFind) {
-    std::size_t pos = s.find(toFind);
+        if (toUpper(splitted[0]) == "CPUFREQUENCY") {
+            cpuFrequency = splitted[1];
+            break;
+        }
+    }
 
-    if (pos == std::string::npos) { // Not found
+    if (esp_pm_get_configuration(&pm_config) != ESP_OK) {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to read CPU Frequency!");
         return false;
     }
+
+    if (cpuFrequency == "160") { // 160 is the default
+        // No change needed
+    }
+    else if (cpuFrequency == "240") {
+        pm_config.max_freq_mhz = 240;
+        pm_config.min_freq_mhz = pm_config.max_freq_mhz;
+        if (esp_pm_configure(&pm_config) != ESP_OK) {
+            LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to set new CPU frequency!");
+            return false;
+        }
+    }
+    else {
+        LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Unknown CPU frequency: " + cpuFrequency + "! "
+                "It must be 160 or 240!");
+        return false;
+    }
+
+    if (esp_pm_get_configuration(&pm_config) == ESP_OK) {
+        LogFile.WriteToFile(ESP_LOG_INFO, TAG, string("CPU frequency: ") + to_string(pm_config.max_freq_mhz) + " MHz");
+    }
+
     return true;
-}
+}

+ 9 - 5
code/main/server_main.cpp

@@ -8,6 +8,7 @@
 #include "time_sntp.h"
 
 #include "connect_wlan.h"
+#include "read_wlanini.h"
 
 #include "version.h"
 
@@ -83,7 +84,7 @@ esp_err_t info_get_handler(httpd_req_t *req)
     else if (_task.compare("Hostname") == 0)
     {
         std::string zw;
-        zw = std::string(hostname);
+        zw = std::string(wlan_config.hostname);
         httpd_resp_sendstr(req, zw.c_str());
         return ESP_OK;        
     }
@@ -214,7 +215,10 @@ esp_err_t hello_main_handler(httpd_req_t *req)
 
     if (filetosend == "/sdcard/html/index.html") {
         if (isSetSystemStatusFlag(SYSTEM_STATUS_PSRAM_BAD) || // Initialization failed with crritical errors!
-                isSetSystemStatusFlag(SYSTEM_STATUS_CAM_BAD)) {
+            isSetSystemStatusFlag(SYSTEM_STATUS_CAM_BAD) ||
+            isSetSystemStatusFlag(SYSTEM_STATUS_SDCARD_CHECK_BAD) ||
+            isSetSystemStatusFlag(SYSTEM_STATUS_FOLDER_CHECK_BAD)) 
+        {
             LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "We have a critical error, not serving main page!");
 
             char buf[20];
@@ -227,13 +231,13 @@ esp_err_t hello_main_handler(httpd_req_t *req)
                 }
             }
 
-            message += "<br>Please check <a href=\"https://jomjol.github.io/AI-on-the-edge-device-docs/Error-Codes\" target=_blank>jomjol.github.io/AI-on-the-edge-device-docs/Error-Codes</a> for more information!";
+            message += "<br>Please check logs with log viewer and/or <a href=\"https://jomjol.github.io/AI-on-the-edge-device-docs/Error-Codes\" target=_blank>jomjol.github.io/AI-on-the-edge-device-docs/Error-Codes</a> for more information!";
             message += "<br><br><button onclick=\"window.location.href='/reboot';\">Reboot</button>";
             message += "&nbsp;<button onclick=\"window.open('/ota_page.html');\">OTA Update</button>";
             message += "&nbsp;<button onclick=\"window.open('/log.html');\">Log Viewer</button>";
             message += "&nbsp;<button onclick=\"window.open('/info.html');\">Show System Info</button>";
-            httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, message.c_str());
-            return ESP_FAIL;
+            httpd_resp_send(req, message.c_str(), message.length());
+            return ESP_OK;
         }
         else if (isSetupModusActive()) {
             ESP_LOGD(TAG, "System is in setup mode --> index.html --> setup.html");

+ 77 - 39
code/main/softAP.cpp

@@ -27,6 +27,7 @@
 #include "server_help.h"
 #include "defines.h"
 #include "Helper.h"
+#include "statusled.h"
 #include "server_ota.h"
 
 #include "lwip/err.h"
@@ -40,22 +41,24 @@
 bool isConfigINI = false;
 bool isWlanINI = false;
 
-static const char *TAG = "wifi softAP";
+static const char *TAG = "WIFI AP";
+
 
 static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                                     int32_t event_id, void* event_data)
 {
     if (event_id == WIFI_EVENT_AP_STACONNECTED) {
         wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
-        ESP_LOGI(TAG, "station "MACSTR" join, AID=%d",
+        ESP_LOGI(TAG, "station " MACSTR " join, AID=%d",
                  MAC2STR(event->mac), event->aid);
     } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
         wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
-        ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d",
+        ESP_LOGI(TAG, "station " MACSTR " leave, AID=%d",
                  MAC2STR(event->mac), event->aid);
     }
 }
 
+
 void wifi_init_softAP(void)
 {
     ESP_ERROR_CHECK(esp_netif_init());
@@ -87,7 +90,7 @@ void wifi_init_softAP(void)
     ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
     ESP_ERROR_CHECK(esp_wifi_start());
 
-    ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s channel:%d",
+    ESP_LOGI(TAG, "started with SSID \"%s\", password: \"%s\", channel: %d. Connect to AP and open http://192.168.4.1",
              EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS, EXAMPLE_ESP_WIFI_CHANNEL);
 }
 
@@ -136,18 +139,18 @@ void SendHTTPResponse(httpd_req_t *req)
         httpd_resp_send_chunk(req, message.c_str(), strlen(message.c_str()));
 
 //        message = "</tr><tr><td> Hostname</td><td><input type=\"text\" name=\"hostname\" id=\"hostname\"></td><td></td>";
-//        message += "</tr><tr><td>Fixed IP</td><td><input type=\"text\" name=\"ip\" id=\"ip\"></td><td>Leave emtpy if set by router</td></tr>";
-//        message += "<tr><td>gateway</td><td><input type=\"text\" name=\"gateway\" id=\"gateway\"></td><td>Leave emtpy if set by router</td></tr>";
-//        message += "<tr><td>netmask</td><td><input type=\"text\" name=\"netmask\" id=\"netmask\"></td><td>Leave emtpy if set by router</td>";
-//        message += "</tr><tr><td>DNS</td><td><input type=\"text\" name=\"dns\" id=\"dns\"></td><td>Leave emtpy if set by router</td></tr>";
-//        message += "<tr><td>RSSI Threshold</td><td><input type=\"number\" name=\"name\" id=\"threshold\" min=\"-100\"  max=\"0\" step=\"1\" value = \"0\"></td><td>WLAN Mesh Parameter: Threshold for RSSI value to check for start switching access point in a mesh system.Possible values: -100 to 0, 0 = disabled - Value will be transfered to wlan.ini at next startup)</td></tr>";
+//        message += "</tr><tr><td>Fixed IP</td><td><input type=\"text\" name=\"ip\" id=\"ip\"></td><td>Leave emtpy if set by router (DHCP)</td></tr>";
+//        message += "<tr><td>Gateway</td><td><input type=\"text\" name=\"gateway\" id=\"gateway\"></td><td>Leave emtpy if set by router (DHCP)</td></tr>";
+//        message += "<tr><td>Netmask</td><td><input type=\"text\" name=\"netmask\" id=\"netmask\"></td><td>Leave emtpy if set by router (DHCP)</td>";
+//        message += "</tr><tr><td>DNS</td><td><input type=\"text\" name=\"dns\" id=\"dns\"></td><td>Leave emtpy if set by router (DHCP)</td></tr>";
+//        message += "<tr><td>RSSI Threshold</td><td><input type=\"number\" name=\"name\" id=\"threshold\" min=\"-100\"  max=\"0\" step=\"1\" value = \"0\"></td><td>WLAN Mesh Parameter: Threshold for RSSI value to check for start switching access point in a mesh system (if actual RSSI is lower). Possible values: -100 to 0, 0 = disabled - Value will be transfered to wlan.ini at next startup)</td></tr>";
 //        httpd_resp_send_chunk(req, message.c_str(), strlen(message.c_str()));
 
 
         message = "<button class=\"button\" type=\"button\" onclick=\"wr()\">Write wlan.ini</button>";
         message += "<script language=\"JavaScript\">async function wr(){";
         message += "api = \"/config?\"+\"ssid=\"+document.getElementById(\"ssid\").value+\"&pwd=\"+document.getElementById(\"password\").value;";
-//        message += "api = \"/config?\"+\"ssid=\"+document.getElementById(\"ssid\").value+\"&pwd=\"+document.getElementById(\"password\").value+\"&hn=\"+document.getElementById(\"hostname\").value+\"&ip=\"+document.getElementById(\"ip\").value+\"&gw=\"+document.getElementById(\"gateway\").value+\"&nm=\"+document.getElementById(\"netmask\").value+\"&dns=\"+document.getElementById(\"dns\").value+\"&rssi=\"+document.getElementById(\"threshold\").value;";
+//        message += "api = \"/config?\"+\"ssid=\"+document.getElementById(\"ssid\").value+\"&pwd=\"+document.getElementById(\"password\").value+\"&hn=\"+document.getElementById(\"hostname\").value+\"&ip=\"+document.getElementById(\"ip\").value+\"&gw=\"+document.getElementById(\"gateway\").value+\"&nm=\"+document.getElementById(\"netmask\").value+\"&dns=\"+document.getElementById(\"dns\").value+\"&rssithreshold=\"+document.getElementById(\"threshold\").value;";
         message += "fetch(api);await new Promise(resolve => setTimeout(resolve, 1000));location.reload();}</script>";
         httpd_resp_send_chunk(req, message.c_str(), strlen(message.c_str()));
         return;
@@ -164,8 +167,6 @@ void SendHTTPResponse(httpd_req_t *req)
 }
 
 
-
-
 esp_err_t test_handler(httpd_req_t *req)
 {
     SendHTTPResponse(req);
@@ -182,7 +183,7 @@ esp_err_t reboot_handlerAP(httpd_req_t *req)
     LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Trigger reboot due to firmware update.");
     doRebootOTA();
     return ESP_OK;
-};
+}
 
 
 esp_err_t config_ini_handler(httpd_req_t *req)
@@ -201,9 +202,10 @@ esp_err_t config_ini_handler(httpd_req_t *req)
     std::string hn = "";    // hostname
     std::string ip = "";
     std::string gw = "";    // gateway
-    std::string nm = "";    // nm
+    std::string nm = "";    // netmask
     std::string dns = "";
-    std::string rssi = "";
+    std::string rssithreshold = ""; //rssi threshold for WIFI roaming
+    std::string text = "";
 
 
     if (httpd_req_get_url_query_str(req, _query, 400) == ESP_OK)
@@ -258,77 +260,106 @@ esp_err_t config_ini_handler(httpd_req_t *req)
             dns = UrlDecode(std::string(_valuechar));
         }
 
-        if (httpd_query_key_value(_query, "rssi", _valuechar, 30) == ESP_OK)
+        if (httpd_query_key_value(_query, "rssithreshold", _valuechar, 30) == ESP_OK)
         {
-            ESP_LOGD(TAG, "rssi is found: %s", _valuechar);
-            rssi = UrlDecode(std::string(_valuechar));
+            ESP_LOGD(TAG, "rssithreshold is found: %s", _valuechar);
+            rssithreshold = UrlDecode(std::string(_valuechar));
         }
-    };
+    }
 
     FILE* configfilehandle = fopen(WLAN_CONFIG_FILE, "w");
+
+    text  = ";++++++++++++++++++++++++++++++++++\n";
+    text += "; AI on the edge - WLAN configuration\n";
+    text += "; ssid: Name of WLAN network (mandatory), e.g. \"WLAN-SSID\"\n";
+    text += "; password: Password of WLAN network (mandatory), e.g. \"PASSWORD\"\n\n";
+    fputs(text.c_str(), configfilehandle);
     
     if (ssid.length())
         ssid = "ssid = \"" + ssid + "\"\n";
     else
-        ssid = ";ssid = \"\"\n";
-
+        ssid = "ssid = \"\"\n";
     fputs(ssid.c_str(), configfilehandle);
 
     if (pwd.length())
         pwd = "password = \"" + pwd + "\"\n";
     else
-        pwd = ";password = \"\"\n";
+        pwd = "password = \"\"\n";
     fputs(pwd.c_str(), configfilehandle);
 
+    text  = "\n;++++++++++++++++++++++++++++++++++\n";
+    text += "; Hostname: Name of device in network\n";
+    text += "; This parameter can be configured via WebUI configuration\n";
+    text += "; Default: \"watermeter\", if nothing is configured\n\n";
+    fputs(text.c_str(), configfilehandle);
+
     if (hn.length())
         hn = "hostname = \"" + hn + "\"\n";
     else
-        hn = ";hostname = \"\"\n";
+        hn = ";hostname = \"watermeter\"\n";
     fputs(hn.c_str(), configfilehandle);
 
+    text  = "\n;++++++++++++++++++++++++++++++++++\n";
+    text += "; Fixed IP: If you like to use fixed IP instead of DHCP (default), the following\n";
+    text += "; parameters needs to be configured: ip, gateway, netmask are mandatory, dns optional\n\n";
+    fputs(text.c_str(), configfilehandle);
+
     if (ip.length())
         ip = "ip = \"" + ip + "\"\n";
     else
-        ip = ";ip = \"\"\n";
+        ip = ";ip = \"xxx.xxx.xxx.xxx\"\n";
     fputs(ip.c_str(), configfilehandle);
 
     if (gw.length())
         gw = "gateway = \"" + gw + "\"\n";
     else
-        gw = ";gateway = \"\"\n";
+        gw = ";gateway = \"xxx.xxx.xxx.xxx\"\n";
     fputs(gw.c_str(), configfilehandle);
 
     if (nm.length())
         nm = "netmask = \"" + nm + "\"\n";
     else
-        nm = ";netmask = \"\"\n";
+        nm = ";netmask = \"xxx.xxx.xxx.xxx\"\n";
     fputs(nm.c_str(), configfilehandle);
 
+    text  = "\n;++++++++++++++++++++++++++++++++++\n";
+    text += "; DNS server (optional, if no DNS is configured, gateway address will be used)\n\n";
+    fputs(text.c_str(), configfilehandle);
+
     if (dns.length())
         dns = "dns = \"" + dns + "\"\n";
     else
-        dns = ";dns = \"\"\n";
+        dns = ";dns = \"xxx.xxx.xxx.xxx\"\n";
     fputs(dns.c_str(), configfilehandle);
 
-    if (rssi.length())
-        rssi = "RSSIThreshold = \"" + rssi + "\"\n";
+    text  = "\n;++++++++++++++++++++++++++++++++++\n";
+    text += "; WIFI Roaming:\n";
+    text += "; Network assisted roaming protocol is activated by default\n";
+    text += "; AP / mesh system needs to support roaming protocol 802.11k/v\n";
+    text += ";\n";
+    text += "; Optional feature (usually not neccessary):\n";
+    text += "; RSSI Threshold for client requested roaming query (RSSI < RSSIThreshold)\n";
+    text += "; Note: This parameter can be configured via WebUI configuration\n";
+    text += "; Default: 0 = Disable client requested roaming query\n\n";
+    fputs(text.c_str(), configfilehandle);
+
+    if (rssithreshold.length())
+        rssithreshold = "RSSIThreshold = " + rssithreshold + "\n";
     else
-        rssi = ";rssi = \"\"\n";
-    fputs(rssi.c_str(), configfilehandle);
+        rssithreshold = "RSSIThreshold = 0\n";
+    fputs(rssithreshold.c_str(), configfilehandle);
 
     fflush(configfilehandle);
     fclose(configfilehandle);
 
     std::string zw = "ota without parameter - should not be the case!";
     httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
-    httpd_resp_send(req, zw.c_str(), strlen(zw.c_str())); 
-    httpd_resp_send_chunk(req, NULL, 0);  
-
-    ESP_LOGE(TAG, "end config.ini");
+    httpd_resp_send(req, zw.c_str(), zw.length()); 
 
+    ESP_LOGD(TAG, "end config.ini");
 
     return ESP_OK;
-};
+}
 
 
 esp_err_t upload_post_handlerAP(httpd_req_t *req)
@@ -470,21 +501,28 @@ httpd_handle_t start_webserverAP(void)
     return NULL;
 }
 
+
 void CheckStartAPMode()
 {
     isConfigINI = FileExists(CONFIG_FILE);
     isWlanINI = FileExists(WLAN_CONFIG_FILE);
 
-    if (!isConfigINI or !isWlanINI)
+    if (!isConfigINI)
+        ESP_LOGW(TAG, "config.ini not found!");
+
+    if (!isWlanINI)
+        ESP_LOGW(TAG, "wlan.ini not found!");
+
+    if (!isConfigINI || !isWlanINI)
     {
+        ESP_LOGI(TAG, "Starting access point for remote configuration");
+        StatusLED(AP_OR_OTA, 2, true);
         wifi_init_softAP();
         start_webserverAP();
         while(1) { // wait until reboot within task_do_Update_ZIP
             vTaskDelay(1000 / portTICK_PERIOD_MS);
         }
     }
-
-
 }
 
 #endif //#ifdef ENABLE_SOFTAP

+ 17 - 1
code/sdkconfig.defaults

@@ -99,6 +99,8 @@ CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384
 CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=40960
 CONFIG_SPIRAM_CACHE_WORKAROUND=y
 CONFIG_SPIRAM_IGNORE_NOTFOUND=y
+CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y
+CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y
 
 CONFIG_ESP_INT_WDT_TIMEOUT_MS=300
 
@@ -118,6 +120,9 @@ CONFIG_MQTT_MSG_ID_INCREMENTAL=y
 CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED=y
 CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED=y
 CONFIG_MQTT_USE_CORE_0=y
+CONFIG_MQTT_USE_CUSTOM_CONFIG=y
+#CONFIG_MQTT_OUTBOX_EXPIRED_TIMEOUT_MS=5000
+CONFIG_MQTT_CUSTOM_OUTBOX=y
 
 CONFIG_FREERTOS_TASK_FUNCTION_WRAPPER=n
 
@@ -133,7 +138,16 @@ CONFIG_GC032A_SUPPORT=n
 CONFIG_GC0308_SUPPORT=n
 CONFIG_BF3005_SUPPORT=n
 
-#only necessary for task analysis (include/defines -> TASK_ANALYSIS_ON)
+CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=4864
+
+#only necessary for WIFI mesh roaming (include/defines.h -> WLAN_USE_MESH_ROAMING)
+#CONFIG_WPA_11KV_SUPPORT=y
+#CONFIG_WPA_SCAN_CACHE=n
+#CONFIG_WPA_MBO_SUPPORT=n
+#CONFIG_WPA_11R_SUPPORT=n // Will be supported with ESP-IDF v5.0
+#CONFIG_WPA_DEBUG_PRINT=n
+
+#only necessary for task analysis (include/defines.h -> TASK_ANALYSIS_ON)
 #set in [env:esp32cam-dev-task-analysis]
 #CONFIG_FREERTOS_USE_TRACE_FACILITY=1
 #CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
@@ -144,3 +158,5 @@ CONFIG_BF3005_SUPPORT=n
 #I (2112) esp_himem: Initialized. Using last 8 32KB address blocks for bank switching on 4352 KB of physical memory.
 CONFIG_SPIRAM_BANKSWITCH_ENABLE=n
 #CONFIG_SPIRAM_BANKSWITCH_RESERVE is not set
+
+CONFIG_PM_ENABLE=y

BIN=BIN
sd-card/config/ana-class100_0154_s1_q.tflite


BIN=BIN
sd-card/config/ana-class100_0157_s1_q.tflite


BIN=BIN
sd-card/config/ana-cont_11.3.1_s2.tflite


BIN=BIN
sd-card/config/ana-cont_1105_s2_q.tflite


+ 3 - 2
sd-card/config/config.ini

@@ -22,7 +22,7 @@ FlipImageSize = false
 /config/ref1.jpg 442 142
 
 [Digits]
-Model = /config/dig-cont_0600_s3.tflite
+Model = /config/dig-cont_0611_s3_q.tflite
 CNNGoodThreshold = 0.5
 ;ROIImagesLocation = /log/digit
 ;ROIImagesRetention = 3
@@ -31,7 +31,7 @@ main.dig2 343 126 30 54 false
 main.dig3 391 126 30 54 false
 
 [Analog]
-Model = /config/ana-cont_11.3.1_s2.tflite
+Model = /config/ana-cont_1105_s2_q.tflite
 CNNGoodThreshold = 0.5
 ;ROIImagesLocation = /log/analog
 ;ROIImagesRetention = 3
@@ -107,4 +107,5 @@ TimeZone = CET-1CEST,M3.5.0,M10.5.0/3
 ;TimeServer = pool.ntp.org
 ;Hostname = undefined
 ;RSSIThreshold = 0
+CPUFrequency = 160
 SetupMode = true

BIN=BIN
sd-card/config/dig-class100-0150_s2_q.tflite


BIN=BIN
sd-card/config/dig-cont_0600_s3.tflite


BIN=BIN
sd-card/config/dig-cont_0610_s3.tflite


BIN=BIN
sd-card/config/dig-cont_0610_s3_q.tflite


BIN=BIN
sd-card/config/dig-cont_0611_s3.tflite


BIN=BIN
sd-card/config/dig-cont_0611_s3_q.tflite


+ 12 - 8
sd-card/html/data.html

@@ -5,6 +5,7 @@
             body {
                 height: 100%;
                 margin: 2px;
+                font-family: Arial, Helvetica, sans-serif;
             }
 
             .box {
@@ -33,19 +34,22 @@
         <script type="text/javascript" src="common.js?v=$COMMIT_HASH"></script> 
     </head>
     <body>
+            <h3>Todays Data</h3>
+            <h4>Last part of Todays Data</h4>
         <div class="box">
             <div class="row header">
-                <button onClick="reload();">Reload</button>
-                <button onClick="window.open(getDomainname() + '/datafileact');">Show full data</button>
-                <button onClick="window.location.href = getDomainname() + '/fileserver/log/data/'">Show older data files</button>
-                <button onClick="window.location.href = 'graph.html?v=$COMMIT_HASH'">Show graph</button>
+                <button onClick="reload();">Refresh</button>
+                <button onClick="window.open(getDomainname() + '/datafileact');">Show Full File</button>
+                <button onClick="window.location.href = getDomainname() + '/fileserver/log/data/'">Show Data Files</button>
+                <button onClick="window.location.href = 'graph.html?v=$COMMIT_HASH'">Show Graph</button>
             </div>
             <div class="row content" id="data"><br><br><br><b>Loading Data file, please wait...</b></div>
             <div class="row footer">
-                <button onClick="reload();">Reload</button>
-                <button onClick="window.open(getDomainname() + '/datafileact');">Show full data</button>
-                <button onClick="window.location.href = getDomainname() + '/fileserver/log/data/'">Show older data files</button>
-                <button onClick="window.location.href = 'graph.html?v=$COMMIT_HASH'">Show graph</button>
+                <button onClick="reload();">Refresh</button>
+                <button onClick="window.open(getDomainname() + '/datafileact');">Show Full File</button>
+                <button onClick="window.location.href = getDomainname() + '/fileserver/log/data/'">Show Data Files</button>
+                <button onClick="window.location.href = 'graph.html?v=$COMMIT_HASH'">Show Graph</button>
+                <p></p>
             </div>
           </div>
     </body>

+ 1 - 1
sd-card/html/edit_alignment.html

@@ -115,7 +115,7 @@ select {
             param;
     
 function doReboot() {
-    if (confirm("Are you sure you want to reboot? Did you save the config?")) {
+    if (confirm("Are you sure you want to reboot? Did you save your changes?")) {
             var stringota = getDomainname() + "/reboot";
             window.location = stringota;
             window.location.href = stringota;

+ 1 - 1
sd-card/html/edit_analog.html

@@ -165,7 +165,7 @@ th, td {
 
     
 function doReboot() {
-    if (confirm("Are you sure you want to reboot? Did you save the config?")) {
+    if (confirm("Are you sure you want to reboot? Did you save your changes?")) {
             var stringota = getDomainname() + "/reboot";
             window.location = stringota;
             window.location.href = stringota;

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 169 - 266
sd-card/html/edit_config_param.html


+ 1 - 1
sd-card/html/edit_digits.html

@@ -161,7 +161,7 @@ th, td {
             domainname = getDomainname();
 
     function doReboot() {
-        if (confirm("Are you sure you want to reboot? Did you save the config?")) {
+        if (confirm("Are you sure you want to reboot? Did you save your changes?")) {
                 var stringota = getDomainname() + "/reboot";
                 window.location = stringota;
                 window.location.href = stringota;

+ 1 - 1
sd-card/html/edit_reference.html

@@ -238,7 +238,7 @@ table {
         }			
                 
         function SaveReference(){
-            if (confirm("Are you sure you want to update the reference image?")) {
+            if (confirm("Are you sure you want to update the Reference Image?")) {
                 param["Alignment"]["InitialRotate"].value1 = document.getElementById("prerotateangle").value;
 
                 if ((param["Alignment"]["InitialMirror"].found == true) && (document.getElementById("mirror").checked))

+ 1 - 0
sd-card/html/github.min.css

@@ -0,0 +1 @@
+.hljs{display:block;overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector-id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font-weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet,.hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:700}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}

+ 148 - 64
sd-card/html/graph.html

@@ -1,21 +1,156 @@
 <html>
     <head>
-    <script type="text/javascript" src='plotly-2.14.0.min.js?v=$COMMIT_HASH'></script>
+    <script type="text/javascript" src='plotly-basic-2.18.2.min.js?v=$COMMIT_HASH'></script>
 
     <script type="text/javascript" src="common.js?v=$COMMIT_HASH"></script> 
     <script type="text/javascript" src="readconfigcommon.js?v=$COMMIT_HASH"></script>  
     <script type="text/javascript" src="readconfigparam.js?v=$COMMIT_HASH"></script>  
-
     <style>
-        textarea {
-            width: 600px;
-            height: 300px;
+        body {
+            font-family: Arial, Helvetica, sans-serif;
         }
     </style>
+  
     <script>
     function run() {
-      var el = document.getElementById('cnsl');
-      el && eval(el.value);
+        datefile = document.getElementById("datafiles").value;
+        numbername = document.getElementById("numbers").value;
+        showRrelativeValues = document.getElementById("showRrelativeValues").checked;
+        //alert("Auslesen: " + datefile + " " + numbername);
+
+        _domainname = getDomainname();
+        fetch(_domainname + '/fileserver/log/data/' + datefile)
+        .then(response => {
+            // handle the response
+            if (response.status == 404) {
+                firework.launch("No log data available for " + dateString, 'warning', 10000);
+            }
+            response.text()
+            .then( result => {
+                var lines = result.split("\n");
+                var traceValue =          { x: [], y: [], type: 'scatter', line: {width: 6}, name: 'Value'};
+                var tracePreValue =       { x: [], y: [], type: 'scatter', line: {width: 2}, name: 'Previous Value', visible: 'legendonly'};
+                var traceChangeRate =     { x: [], y: [], type: 'bar', yaxis: 'y2', opacity: 0.2, name: 'Change Rate'};
+                var traceChangeAbsolute = { x: [], y: [], type: 'bar', yaxis: 'y2', opacity: 0.2, name: 'Change Absolute', visible: 'legendonly'};
+
+                var timex = 1;
+                for (let line of lines) {
+                    {
+                        //console.log(line);
+                        if (line.split(",")[1] == numbername)
+                        {
+                            var value = line.split(",")[3];
+                            var preValue = line.split(",")[4];
+                            var changeRate = line.split(",")[5];
+                            var changeAbsolute = line.split(",")[6];
+                            var time  = line.split(",")[0];
+                            //console.log("> "+time+" "+value+"\n");
+
+                            traceValue.x.push(time);
+
+                            /* Catch empty fields */
+                            if (value == "" || isNaN(value)) {
+                                if (traceValue.y.length > 0) {
+                                    value = traceValue.y[traceValue.y.length-1];
+                                }
+                                else {
+                                    value = 0;
+                                }
+                            }
+
+                            if (preValue == "" || isNaN(preValue)) {
+                                if (tracePreValue.y.length > 0) {
+                                    preValue = tracePreValue.y[tracePreValue.y.length-1];
+                                }
+                                else {
+                                    preValue = 0;
+                                }
+                            }
+
+                            if (changeRate == "" || isNaN(changeRate)) {
+                                if (traceChangeRate.y.length > 0) {
+                                    changeRate = traceChangeRate.y[traceChangeRate.y.length-1];
+                                }
+                                else {
+                                    changeRate = 0;
+                                }
+                            }
+
+                            if (changeAbsolute == "" || isNaN(changeAbsolute)) {
+                                if (traceChangeAbsolute.y.length > 0) {
+                                    changeAbsolute = traceChangeAbsolute.y[traceChangeAbsolute.y.length-1];
+                                }
+                                else {
+                                    changeAbsolute = 0;
+                                }
+                            }
+
+                            traceValue.y.push(value);
+                            tracePreValue.y.push(preValue);
+                            traceChangeRate.y.push(changeRate);
+                            traceChangeAbsolute.y.push(changeAbsolute);
+                        }
+                    }
+                }
+                //console.log(trace);
+
+                // Copy time to all traces
+                tracePreValue.x = traceValue.x;
+                traceChangeRate.x = traceValue.x;
+                traceChangeAbsolute.x = traceValue.x;
+
+                //console.log(traceValue.y);
+
+                var offsetValue = traceValue.y[0];
+                var offsetPreValue = tracePreValue.y[0];
+
+                if (showRrelativeValues) {
+                    traceValue.y.forEach(function(part, index, arr) {
+                        arr[index] = arr[index] - offsetValue;
+                    });
+
+                    tracePreValue.y.forEach(function(part, index, arr) {
+                        arr[index] = arr[index] - offsetPreValue;
+                    });
+                }
+
+              //  console.log(traceValue.x)
+
+                var data = [traceValue, tracePreValue, traceChangeRate, traceChangeAbsolute];
+
+                var layout = {
+                    showlegend: true,
+                    colorway: ['green', 'black', 'blue', 'black'],
+
+                    yaxis: {title: 'Value'},
+                    yaxis2: {
+                        title: 'Change',
+                        overlaying: 'y',
+                        side: 'right'
+                    },
+
+                    margin: {
+                        l: 50,
+                        r: 50,
+                        b: 50,
+                        t: 50,
+                        pad: 4
+                    },
+
+                    legend: {
+                        x: 0.2,
+                        y: 0.9,
+                        xanchor: 'right'
+                    }
+                };
+
+                document.getElementById("chart").innerHTML = "";
+                Plotly.newPlot('chart', data, layout, {displayModeBar: true});
+            });
+        }).catch((error) => {
+            // handle the error
+            console.log(error);
+        });
     }
     </script>
     <link href="firework.css?v=$COMMIT_HASH" rel="stylesheet">
@@ -23,67 +158,16 @@
     <script type="text/javascript" src="firework.js?v=$COMMIT_HASH"></script>
     </head>
     <body>
-    <div id='chart'></div>
+        <h3>Data Graph</h3>
+        <div id='chart'><p>Loading...<br></p></div>
         <select id="datafiles" onchange="run();"></select>
         <select id="numbers" onchange="run();"></select>
-        <select id="datatype" onchange="run();">
-            <option value="3">Value</option>
-            <option value="4">PreValue</option>
-            <option value="5">Change-Rate</option>
-            <option value="6">Change-Absolut</option>
-        </select>
-        
+        <input type="checkbox" id="showRrelativeValues" onclick = 'run();' unchecked ><label for="showRrelativeValues">Show relative values</label>
+        <button onclick="run();">Refresh</button>
+        &nbsp;&nbsp;|&nbsp;&nbsp;
+        <button onClick="window.location.href = 'data.html?v=$COMMIT_HASH'">Show data</button>
         <button onClick="window.location.href = getDomainname() + '/fileserver/log/data/'">Show data files</button>
 
-  <!-- <button onclick="document.getElementById('editor').hidden = false; this.hidden = true;" >Editor</button> -->
-    <div id='editor' hidden='true'>
-    <textarea id="cnsl">
-datefile = document.getElementById("datafiles").value;
-numbername = document.getElementById("numbers").value;
-datatype = document.getElementById("datatype").value;
-//alert("Auslesen: " + datefile + " " + numbername);
-
-_domainname = getDomainname(); 
-fetch(_domainname + '/fileserver/log/data/' + datefile)
-.then(response => {
-    // handle the response
-    if (response.status == 404) {
-        firework.launch("No log data available for " + dateString, 'warning', 10000);
-    }
-    response.text()
-    .then( result => {
-        var lines = result.split("\n");
-        var trace = {
-        x: [],
-        y: [],
-        type: 'scatter'
-        };
-
-        var timex = 1;
-        for (let line of lines) {
-            {
-                console.log(line);
-                if (line.split(",")[1] == numbername)
-                {
-                    var value = line.split(",")[datatype];
-                    var time  = line.split(",")[0];
-                    console.log("> "+time+" "+value+"\n");
-                    trace.x.push(time);
-//                    timex += 1;
-                    trace.y.push(value);
-                }
-            }
-        }
-        console.log(trace);
-        var data = [trace];
-        Plotly.newPlot('chart', data);
-    });
-}).catch((error) => {
-    // handle the error
-    console.log(error);
-});        
-</textarea><br />
-<button onclick="run();">run</button>
 </div>
 
 <script>

BIN=BIN
sd-card/html/help.png


+ 13 - 4
sd-card/html/index.html

@@ -6,6 +6,7 @@
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <link rel="stylesheet" href="style.css?v=$COMMIT_HASH" type="text/css" >
+<link href="firework.css?v=$COMMIT_HASH" rel="stylesheet">
 
 <script type="text/javascript" src="common.js?v=$COMMIT_HASH"></script>
 <script type="text/javascript" src="readconfigcommon.js?v=$COMMIT_HASH"></script>
@@ -82,15 +83,23 @@
         </li>
       </ul>
 
-    <li><a href="#" onclick="loadPage(getDomainname() + '/value?full');">Recognition</a></li>
-    <li><a href="#" onclick="loadPage('graph.html?v=$COMMIT_HASH');">Data Graph</a></li>
-    <li><a href="#" onclick="loadPage(getDomainname() + '/fileserver/');">File Server</a></li>
+
+    <li><a>Data <i class="arrow down"></i></a>
+        <ul class="submenu">
+            <li><a href="#" onclick="loadPage(getDomainname() + '/value?full');">Recognition</a></li>
+            <li><a href="#" onclick="loadPage('graph.html?v=$COMMIT_HASH');">Data Graph</a></li>
+            <li><a href="#" onclick="loadPage('data.html?v=$COMMIT_HASH');">Data Viewer</a></li>
+            <li><a href="#" onclick="loadPage(getDomainname() + '/fileserver/log/data/');">Data Files</a></li>
+        </ul>
+    </li>
+
+
     <li><a>System <i class="arrow down"></i></a>
         <ul class="submenu">
             <li><a href="#" onclick="loadPage('backup.html?v=$COMMIT_HASH');">Backup/Restore</a></li>
             <li><a href="#" onclick="loadPage('ota_page.html?v=$COMMIT_HASH');">OTA Update</a></li>
             <li><a href="#" onclick="loadPage('log.html?v=$COMMIT_HASH');">Log Viewer</a></li>
-            <li><a href="#" onclick="loadPage('data.html?v=$COMMIT_HASH');">Data Viewer</a></li>
+            <li><a href="#" onclick="loadPage(getDomainname() + '/fileserver/');">File Server</a></li>
             <li><a href="#" onclick="loadPage('reboot_page.html?v=$COMMIT_HASH');">Reboot</a></li>
             <li><a href="#" onclick="loadPage('info.html?v=$COMMIT_HASH');">Info</a></li>
         </ul>

+ 6883 - 0
sd-card/html/mkdocs_theme.css

@@ -0,0 +1,6883 @@
+/*
+ * This file is copied from the upstream ReadTheDocs Sphinx
+ * theme. To aid upgradability this file should *not* be edited.
+ * modifications we need should be included in theme_extra.css.
+ *
+ * https://github.com/readthedocs/sphinx_rtd_theme
+ */
+
+ /* sphinx_rtd_theme version 1.0.0 | MIT license */
+ .chromeframe {
+    margin: .2em 0;
+    background: #ccc;
+    color: #000;
+    padding: .2em 0
+}
+
+.ir {
+    display: block;
+    border: 0;
+    text-indent: -999em;
+    overflow: hidden;
+    background-color: transparent;
+    background-repeat: no-repeat;
+    text-align: left;
+    direction: ltr;
+    *line-height: 0
+}
+
+.ir br {
+    display: none
+}
+
+.hidden {
+    display: none !important;
+    visibility: hidden
+}
+
+.visuallyhidden {
+    border: 0;
+    clip: rect(0 0 0 0);
+    height: 1px;
+    margin: -1px;
+    overflow: hidden;
+    padding: 0;
+    position: absolute;
+    width: 1px
+}
+
+.visuallyhidden.focusable:active,
+.visuallyhidden.focusable:focus {
+    clip: auto;
+    height: auto;
+    margin: 0;
+    overflow: visible;
+    position: static;
+    width: auto
+}
+
+.invisible {
+    visibility: hidden
+}
+
+.relative {
+    position: relative
+}
+
+.btn,
+.fa:before,
+.icon:before,
+.rst-content .admonition,
+.rst-content .admonition-title:before,
+.rst-content .admonition-todo,
+.rst-content .attention,
+.rst-content .caution,
+.rst-content .code-block-caption .headerlink:before,
+.rst-content .danger,
+.rst-content .eqno .headerlink:before,
+.rst-content .error,
+.rst-content .hint,
+.rst-content .important,
+.rst-content .note,
+.rst-content .seealso,
+.rst-content .tip,
+.rst-content .warning,
+.rst-content code.download span:first-child:before,
+.rst-content dl dt .headerlink:before,
+.rst-content h1 .headerlink:before,
+.rst-content h2 .headerlink:before,
+.rst-content h3 .headerlink:before,
+.rst-content h4 .headerlink:before,
+.rst-content h5 .headerlink:before,
+.rst-content h6 .headerlink:before,
+.rst-content p.caption .headerlink:before,
+.rst-content p .headerlink:before,
+.rst-content table>caption .headerlink:before,
+.rst-content tt.download span:first-child:before,
+.wy-alert,
+.wy-dropdown .caret:before,
+.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,
+.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,
+.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,
+.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,
+.wy-menu-vertical li.current>a,
+.wy-menu-vertical li.current>a button.toctree-expand:before,
+.wy-menu-vertical li.on a,
+.wy-menu-vertical li.on a button.toctree-expand:before,
+.wy-menu-vertical li button.toctree-expand:before,
+.wy-nav-top a,
+.wy-side-nav-search .wy-dropdown>a,
+.wy-side-nav-search>a,
+input[type=color],
+input[type=date],
+input[type=datetime-local],
+input[type=datetime],
+input[type=email],
+input[type=month],
+input[type=number],
+input[type=password],
+input[type=search],
+input[type=tel],
+input[type=text],
+input[type=time],
+input[type=url],
+input[type=week],
+select,
+textarea {
+    -webkit-font-smoothing: antialiased
+}
+
+.clearfix {
+    *zoom: 1
+}
+
+.clearfix:after,
+.clearfix:before {
+    display: table;
+    content: ""
+}
+
+.clearfix:after {
+    clear: both
+}
+
+.fa,
+.icon,
+.rst-content .admonition-title,
+.rst-content .code-block-caption .headerlink,
+.rst-content .eqno .headerlink,
+.rst-content code.download span:first-child,
+.rst-content dl dt .headerlink,
+.rst-content h1 .headerlink,
+.rst-content h2 .headerlink,
+.rst-content h3 .headerlink,
+.rst-content h4 .headerlink,
+.rst-content h5 .headerlink,
+.rst-content h6 .headerlink,
+.rst-content p.caption .headerlink,
+.rst-content p .headerlink,
+.rst-content table>caption .headerlink,
+.rst-content tt.download span:first-child,
+.wy-menu-vertical li.current>a button.toctree-expand,
+.wy-menu-vertical li.on a button.toctree-expand,
+.wy-menu-vertical li button.toctree-expand {
+    display: inline-block;
+    font: normal normal normal 14px/1 FontAwesome;
+    font-size: inherit;
+    text-rendering: auto;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale
+}
+
+.fa-lg {
+    font-size: 1.33333em;
+    line-height: .75em;
+    vertical-align: -15%
+}
+
+.fa-2x {
+    font-size: 2em
+}
+
+.fa-3x {
+    font-size: 3em
+}
+
+.fa-4x {
+    font-size: 4em
+}
+
+.fa-5x {
+    font-size: 5em
+}
+
+.fa-fw {
+    width: 1.28571em;
+    text-align: center
+}
+
+.fa-ul {
+    padding-left: 0;
+    margin-left: 2.14286em;
+    list-style-type: none
+}
+
+.fa-ul>li {
+    position: relative
+}
+
+.fa-li {
+    position: absolute;
+    left: -2.14286em;
+    width: 2.14286em;
+    top: .14286em;
+    text-align: center
+}
+
+.fa-li.fa-lg {
+    left: -1.85714em
+}
+
+.fa-border {
+    padding: .2em .25em .15em;
+    border: .08em solid #eee;
+    border-radius: .1em
+}
+
+.fa-pull-left {
+    float: left
+}
+
+.fa-pull-right {
+    float: right
+}
+
+.fa-pull-left.icon,
+.fa.fa-pull-left,
+.rst-content .code-block-caption .fa-pull-left.headerlink,
+.rst-content .eqno .fa-pull-left.headerlink,
+.rst-content .fa-pull-left.admonition-title,
+.rst-content code.download span.fa-pull-left:first-child,
+.rst-content dl dt .fa-pull-left.headerlink,
+.rst-content h1 .fa-pull-left.headerlink,
+.rst-content h2 .fa-pull-left.headerlink,
+.rst-content h3 .fa-pull-left.headerlink,
+.rst-content h4 .fa-pull-left.headerlink,
+.rst-content h5 .fa-pull-left.headerlink,
+.rst-content h6 .fa-pull-left.headerlink,
+.rst-content p .fa-pull-left.headerlink,
+.rst-content table>caption .fa-pull-left.headerlink,
+.rst-content tt.download span.fa-pull-left:first-child,
+.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,
+.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,
+.wy-menu-vertical li button.fa-pull-left.toctree-expand {
+    margin-right: .3em
+}
+
+.fa-pull-right.icon,
+.fa.fa-pull-right,
+.rst-content .code-block-caption .fa-pull-right.headerlink,
+.rst-content .eqno .fa-pull-right.headerlink,
+.rst-content .fa-pull-right.admonition-title,
+.rst-content code.download span.fa-pull-right:first-child,
+.rst-content dl dt .fa-pull-right.headerlink,
+.rst-content h1 .fa-pull-right.headerlink,
+.rst-content h2 .fa-pull-right.headerlink,
+.rst-content h3 .fa-pull-right.headerlink,
+.rst-content h4 .fa-pull-right.headerlink,
+.rst-content h5 .fa-pull-right.headerlink,
+.rst-content h6 .fa-pull-right.headerlink,
+.rst-content p .fa-pull-right.headerlink,
+.rst-content table>caption .fa-pull-right.headerlink,
+.rst-content tt.download span.fa-pull-right:first-child,
+.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,
+.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,
+.wy-menu-vertical li button.fa-pull-right.toctree-expand {
+    margin-left: .3em
+}
+
+.pull-right {
+    float: right
+}
+
+.pull-left {
+    float: left
+}
+
+.fa.pull-left,
+.pull-left.icon,
+.rst-content .code-block-caption .pull-left.headerlink,
+.rst-content .eqno .pull-left.headerlink,
+.rst-content .pull-left.admonition-title,
+.rst-content code.download span.pull-left:first-child,
+.rst-content dl dt .pull-left.headerlink,
+.rst-content h1 .pull-left.headerlink,
+.rst-content h2 .pull-left.headerlink,
+.rst-content h3 .pull-left.headerlink,
+.rst-content h4 .pull-left.headerlink,
+.rst-content h5 .pull-left.headerlink,
+.rst-content h6 .pull-left.headerlink,
+.rst-content p .pull-left.headerlink,
+.rst-content table>caption .pull-left.headerlink,
+.rst-content tt.download span.pull-left:first-child,
+.wy-menu-vertical li.current>a button.pull-left.toctree-expand,
+.wy-menu-vertical li.on a button.pull-left.toctree-expand,
+.wy-menu-vertical li button.pull-left.toctree-expand {
+    margin-right: .3em
+}
+
+.fa.pull-right,
+.pull-right.icon,
+.rst-content .code-block-caption .pull-right.headerlink,
+.rst-content .eqno .pull-right.headerlink,
+.rst-content .pull-right.admonition-title,
+.rst-content code.download span.pull-right:first-child,
+.rst-content dl dt .pull-right.headerlink,
+.rst-content h1 .pull-right.headerlink,
+.rst-content h2 .pull-right.headerlink,
+.rst-content h3 .pull-right.headerlink,
+.rst-content h4 .pull-right.headerlink,
+.rst-content h5 .pull-right.headerlink,
+.rst-content h6 .pull-right.headerlink,
+.rst-content p .pull-right.headerlink,
+.rst-content table>caption .pull-right.headerlink,
+.rst-content tt.download span.pull-right:first-child,
+.wy-menu-vertical li.current>a button.pull-right.toctree-expand,
+.wy-menu-vertical li.on a button.pull-right.toctree-expand,
+.wy-menu-vertical li button.pull-right.toctree-expand {
+    margin-left: .3em
+}
+
+.fa-spin {
+    -webkit-animation: fa-spin 2s linear infinite;
+    animation: fa-spin 2s linear infinite
+}
+
+.fa-pulse {
+    -webkit-animation: fa-spin 1s steps(8) infinite;
+    animation: fa-spin 1s steps(8) infinite
+}
+
+@-webkit-keyframes fa-spin {
+    0% {
+        -webkit-transform: rotate(0deg);
+        transform: rotate(0deg)
+    }
+    to {
+        -webkit-transform: rotate(359deg);
+        transform: rotate(359deg)
+    }
+}
+
+@keyframes fa-spin {
+    0% {
+        -webkit-transform: rotate(0deg);
+        transform: rotate(0deg)
+    }
+    to {
+        -webkit-transform: rotate(359deg);
+        transform: rotate(359deg)
+    }
+}
+
+.fa-rotate-90 {
+    -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
+    -webkit-transform: rotate(90deg);
+    -ms-transform: rotate(90deg);
+    transform: rotate(90deg)
+}
+
+.fa-rotate-180 {
+    -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
+    -webkit-transform: rotate(180deg);
+    -ms-transform: rotate(180deg);
+    transform: rotate(180deg)
+}
+
+.fa-rotate-270 {
+    -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
+    -webkit-transform: rotate(270deg);
+    -ms-transform: rotate(270deg);
+    transform: rotate(270deg)
+}
+
+.fa-flip-horizontal {
+    -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
+    -webkit-transform: scaleX(-1);
+    -ms-transform: scaleX(-1);
+    transform: scaleX(-1)
+}
+
+.fa-flip-vertical {
+    -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
+    -webkit-transform: scaleY(-1);
+    -ms-transform: scaleY(-1);
+    transform: scaleY(-1)
+}
+
+:root .fa-flip-horizontal,
+:root .fa-flip-vertical,
+:root .fa-rotate-90,
+:root .fa-rotate-180,
+:root .fa-rotate-270 {
+    filter: none
+}
+
+.fa-stack {
+    position: relative;
+    display: inline-block;
+    width: 2em;
+    height: 2em;
+    line-height: 2em;
+    vertical-align: middle
+}
+
+.fa-stack-1x,
+.fa-stack-2x {
+    position: absolute;
+    left: 0;
+    width: 100%;
+    text-align: center
+}
+
+.fa-stack-1x {
+    line-height: inherit
+}
+
+.fa-stack-2x {
+    font-size: 2em
+}
+
+.fa-inverse {
+    color: #fff
+}
+
+.fa-glass:before {
+    content: ""
+}
+
+.fa-music:before {
+    content: ""
+}
+
+.fa-search:before,
+.icon-search:before {
+    content: ""
+}
+
+.fa-envelope-o:before {
+    content: ""
+}
+
+.fa-heart:before {
+    content: ""
+}
+
+.fa-star:before {
+    content: ""
+}
+
+.fa-star-o:before {
+    content: ""
+}
+
+.fa-user:before {
+    content: ""
+}
+
+.fa-film:before {
+    content: ""
+}
+
+.fa-th-large:before {
+    content: ""
+}
+
+.fa-th:before {
+    content: ""
+}
+
+.fa-th-list:before {
+    content: ""
+}
+
+.fa-check:before {
+    content: ""
+}
+
+.fa-close:before,
+.fa-remove:before,
+.fa-times:before {
+    content: ""
+}
+
+.fa-search-plus:before {
+    content: ""
+}
+
+.fa-search-minus:before {
+    content: ""
+}
+
+.fa-power-off:before {
+    content: ""
+}
+
+.fa-signal:before {
+    content: ""
+}
+
+.fa-cog:before,
+.fa-gear:before {
+    content: ""
+}
+
+.fa-trash-o:before {
+    content: ""
+}
+
+.fa-home:before,
+.icon-home:before {
+    content: ""
+}
+
+.fa-file-o:before {
+    content: ""
+}
+
+.fa-clock-o:before {
+    content: ""
+}
+
+.fa-road:before {
+    content: ""
+}
+
+.fa-download:before,
+.rst-content code.download span:first-child:before,
+.rst-content tt.download span:first-child:before {
+    content: ""
+}
+
+.fa-arrow-circle-o-down:before {
+    content: ""
+}
+
+.fa-arrow-circle-o-up:before {
+    content: ""
+}
+
+.fa-inbox:before {
+    content: ""
+}
+
+.fa-play-circle-o:before {
+    content: ""
+}
+
+.fa-repeat:before,
+.fa-rotate-right:before {
+    content: ""
+}
+
+.fa-refresh:before {
+    content: ""
+}
+
+.fa-list-alt:before {
+    content: ""
+}
+
+.fa-lock:before {
+    content: ""
+}
+
+.fa-flag:before {
+    content: ""
+}
+
+.fa-headphones:before {
+    content: ""
+}
+
+.fa-volume-off:before {
+    content: ""
+}
+
+.fa-volume-down:before {
+    content: ""
+}
+
+.fa-volume-up:before {
+    content: ""
+}
+
+.fa-qrcode:before {
+    content: ""
+}
+
+.fa-barcode:before {
+    content: ""
+}
+
+.fa-tag:before {
+    content: ""
+}
+
+.fa-tags:before {
+    content: ""
+}
+
+.fa-book:before,
+.icon-book:before {
+    content: ""
+}
+
+.fa-bookmark:before {
+    content: ""
+}
+
+.fa-print:before {
+    content: ""
+}
+
+.fa-camera:before {
+    content: ""
+}
+
+.fa-font:before {
+    content: ""
+}
+
+.fa-bold:before {
+    content: ""
+}
+
+.fa-italic:before {
+    content: ""
+}
+
+.fa-text-height:before {
+    content: ""
+}
+
+.fa-text-width:before {
+    content: ""
+}
+
+.fa-align-left:before {
+    content: ""
+}
+
+.fa-align-center:before {
+    content: ""
+}
+
+.fa-align-right:before {
+    content: ""
+}
+
+.fa-align-justify:before {
+    content: ""
+}
+
+.fa-list:before {
+    content: ""
+}
+
+.fa-dedent:before,
+.fa-outdent:before {
+    content: ""
+}
+
+.fa-indent:before {
+    content: ""
+}
+
+.fa-video-camera:before {
+    content: ""
+}
+
+.fa-image:before,
+.fa-photo:before,
+.fa-picture-o:before {
+    content: ""
+}
+
+.fa-pencil:before {
+    content: ""
+}
+
+.fa-map-marker:before {
+    content: ""
+}
+
+.fa-adjust:before {
+    content: ""
+}
+
+.fa-tint:before {
+    content: ""
+}
+
+.fa-edit:before,
+.fa-pencil-square-o:before {
+    content: ""
+}
+
+.fa-share-square-o:before {
+    content: ""
+}
+
+.fa-check-square-o:before {
+    content: ""
+}
+
+.fa-arrows:before {
+    content: ""
+}
+
+.fa-step-backward:before {
+    content: ""
+}
+
+.fa-fast-backward:before {
+    content: ""
+}
+
+.fa-backward:before {
+    content: ""
+}
+
+.fa-play:before {
+    content: ""
+}
+
+.fa-pause:before {
+    content: ""
+}
+
+.fa-stop:before {
+    content: ""
+}
+
+.fa-forward:before {
+    content: ""
+}
+
+.fa-fast-forward:before {
+    content: ""
+}
+
+.fa-step-forward:before {
+    content: ""
+}
+
+.fa-eject:before {
+    content: ""
+}
+
+.fa-chevron-left:before {
+    content: ""
+}
+
+.fa-chevron-right:before {
+    content: ""
+}
+
+.fa-plus-circle:before {
+    content: ""
+}
+
+.fa-minus-circle:before {
+    content: ""
+}
+
+.fa-times-circle:before,
+.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before {
+    content: ""
+}
+
+.fa-check-circle:before,
+.wy-inline-validate.wy-inline-validate-success .wy-input-context:before {
+    content: ""
+}
+
+.fa-question-circle:before {
+    content: ""
+}
+
+.fa-info-circle:before {
+    content: ""
+}
+
+.fa-crosshairs:before {
+    content: ""
+}
+
+.fa-times-circle-o:before {
+    content: ""
+}
+
+.fa-check-circle-o:before {
+    content: ""
+}
+
+.fa-ban:before {
+    content: ""
+}
+
+.fa-arrow-left:before {
+    content: ""
+}
+
+.fa-arrow-right:before {
+    content: ""
+}
+
+.fa-arrow-up:before {
+    content: ""
+}
+
+.fa-arrow-down:before {
+    content: ""
+}
+
+.fa-mail-forward:before,
+.fa-share:before {
+    content: ""
+}
+
+.fa-expand:before {
+    content: ""
+}
+
+.fa-compress:before {
+    content: ""
+}
+
+.fa-plus:before {
+    content: ""
+}
+
+.fa-minus:before {
+    content: ""
+}
+
+.fa-asterisk:before {
+    content: ""
+}
+
+.fa-exclamation-circle:before,
+.rst-content .admonition-title:before,
+.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,
+.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before {
+    content: ""
+}
+
+.fa-gift:before {
+    content: ""
+}
+
+.fa-leaf:before {
+    content: ""
+}
+
+.fa-fire:before,
+.icon-fire:before {
+    content: ""
+}
+
+.fa-eye:before {
+    content: ""
+}
+
+.fa-eye-slash:before {
+    content: ""
+}
+
+.fa-exclamation-triangle:before,
+.fa-warning:before {
+    content: ""
+}
+
+.fa-plane:before {
+    content: ""
+}
+
+.fa-calendar:before {
+    content: ""
+}
+
+.fa-random:before {
+    content: ""
+}
+
+.fa-comment:before {
+    content: ""
+}
+
+.fa-magnet:before {
+    content: ""
+}
+
+.fa-chevron-up:before {
+    content: ""
+}
+
+.fa-chevron-down:before {
+    content: ""
+}
+
+.fa-retweet:before {
+    content: ""
+}
+
+.fa-shopping-cart:before {
+    content: ""
+}
+
+.fa-folder:before {
+    content: ""
+}
+
+.fa-folder-open:before {
+    content: ""
+}
+
+.fa-arrows-v:before {
+    content: ""
+}
+
+.fa-arrows-h:before {
+    content: ""
+}
+
+.fa-bar-chart-o:before,
+.fa-bar-chart:before {
+    content: ""
+}
+
+.fa-twitter-square:before {
+    content: ""
+}
+
+.fa-facebook-square:before {
+    content: ""
+}
+
+.fa-camera-retro:before {
+    content: ""
+}
+
+.fa-key:before {
+    content: ""
+}
+
+.fa-cogs:before,
+.fa-gears:before {
+    content: ""
+}
+
+.fa-comments:before {
+    content: ""
+}
+
+.fa-thumbs-o-up:before {
+    content: ""
+}
+
+.fa-thumbs-o-down:before {
+    content: ""
+}
+
+.fa-star-half:before {
+    content: ""
+}
+
+.fa-heart-o:before {
+    content: ""
+}
+
+.fa-sign-out:before {
+    content: ""
+}
+
+.fa-linkedin-square:before {
+    content: ""
+}
+
+.fa-thumb-tack:before {
+    content: ""
+}
+
+.fa-external-link:before {
+    content: ""
+}
+
+.fa-sign-in:before {
+    content: ""
+}
+
+.fa-trophy:before {
+    content: ""
+}
+
+.fa-github-square:before {
+    content: ""
+}
+
+.fa-upload:before {
+    content: ""
+}
+
+.fa-lemon-o:before {
+    content: ""
+}
+
+.fa-phone:before {
+    content: ""
+}
+
+.fa-square-o:before {
+    content: ""
+}
+
+.fa-bookmark-o:before {
+    content: ""
+}
+
+.fa-phone-square:before {
+    content: ""
+}
+
+.fa-twitter:before {
+    content: ""
+}
+
+.fa-facebook-f:before,
+.fa-facebook:before {
+    content: ""
+}
+
+.fa-github:before,
+.icon-github:before {
+    content: ""
+}
+
+.fa-unlock:before {
+    content: ""
+}
+
+.fa-credit-card:before {
+    content: ""
+}
+
+.fa-feed:before,
+.fa-rss:before {
+    content: ""
+}
+
+.fa-hdd-o:before {
+    content: ""
+}
+
+.fa-bullhorn:before {
+    content: ""
+}
+
+.fa-bell:before {
+    content: ""
+}
+
+.fa-certificate:before {
+    content: ""
+}
+
+.fa-hand-o-right:before {
+    content: ""
+}
+
+.fa-hand-o-left:before {
+    content: ""
+}
+
+.fa-hand-o-up:before {
+    content: ""
+}
+
+.fa-hand-o-down:before {
+    content: ""
+}
+
+.fa-arrow-circle-left:before,
+.icon-circle-arrow-left:before {
+    content: ""
+}
+
+.fa-arrow-circle-right:before,
+.icon-circle-arrow-right:before {
+    content: ""
+}
+
+.fa-arrow-circle-up:before {
+    content: ""
+}
+
+.fa-arrow-circle-down:before {
+    content: ""
+}
+
+.fa-globe:before {
+    content: ""
+}
+
+.fa-wrench:before {
+    content: ""
+}
+
+.fa-tasks:before {
+    content: ""
+}
+
+.fa-filter:before {
+    content: ""
+}
+
+.fa-briefcase:before {
+    content: ""
+}
+
+.fa-arrows-alt:before {
+    content: ""
+}
+
+.fa-group:before,
+.fa-users:before {
+    content: ""
+}
+
+.fa-chain:before,
+.fa-link:before,
+.icon-link:before {
+    content: ""
+}
+
+.fa-cloud:before {
+    content: ""
+}
+
+.fa-flask:before {
+    content: ""
+}
+
+.fa-cut:before,
+.fa-scissors:before {
+    content: ""
+}
+
+.fa-copy:before,
+.fa-files-o:before {
+    content: ""
+}
+
+.fa-paperclip:before {
+    content: ""
+}
+
+.fa-floppy-o:before,
+.fa-save:before {
+    content: ""
+}
+
+.fa-square:before {
+    content: ""
+}
+
+.fa-bars:before,
+.fa-navicon:before,
+.fa-reorder:before {
+    content: ""
+}
+
+.fa-list-ul:before {
+    content: ""
+}
+
+.fa-list-ol:before {
+    content: ""
+}
+
+.fa-strikethrough:before {
+    content: ""
+}
+
+.fa-underline:before {
+    content: ""
+}
+
+.fa-table:before {
+    content: ""
+}
+
+.fa-magic:before {
+    content: ""
+}
+
+.fa-truck:before {
+    content: ""
+}
+
+.fa-pinterest:before {
+    content: ""
+}
+
+.fa-pinterest-square:before {
+    content: ""
+}
+
+.fa-google-plus-square:before {
+    content: ""
+}
+
+.fa-google-plus:before {
+    content: ""
+}
+
+.fa-money:before {
+    content: ""
+}
+
+.fa-caret-down:before,
+.icon-caret-down:before,
+.wy-dropdown .caret:before {
+    content: ""
+}
+
+.fa-caret-up:before {
+    content: ""
+}
+
+.fa-caret-left:before {
+    content: ""
+}
+
+.fa-caret-right:before {
+    content: ""
+}
+
+.fa-columns:before {
+    content: ""
+}
+
+.fa-sort:before,
+.fa-unsorted:before {
+    content: ""
+}
+
+.fa-sort-desc:before,
+.fa-sort-down:before {
+    content: ""
+}
+
+.fa-sort-asc:before,
+.fa-sort-up:before {
+    content: ""
+}
+
+.fa-envelope:before {
+    content: ""
+}
+
+.fa-linkedin:before {
+    content: ""
+}
+
+.fa-rotate-left:before,
+.fa-undo:before {
+    content: ""
+}
+
+.fa-gavel:before,
+.fa-legal:before {
+    content: ""
+}
+
+.fa-dashboard:before,
+.fa-tachometer:before {
+    content: ""
+}
+
+.fa-comment-o:before {
+    content: ""
+}
+
+.fa-comments-o:before {
+    content: ""
+}
+
+.fa-bolt:before,
+.fa-flash:before {
+    content: ""
+}
+
+.fa-sitemap:before {
+    content: ""
+}
+
+.fa-umbrella:before {
+    content: ""
+}
+
+.fa-clipboard:before,
+.fa-paste:before {
+    content: ""
+}
+
+.fa-lightbulb-o:before {
+    content: ""
+}
+
+.fa-exchange:before {
+    content: ""
+}
+
+.fa-cloud-download:before {
+    content: ""
+}
+
+.fa-cloud-upload:before {
+    content: ""
+}
+
+.fa-user-md:before {
+    content: ""
+}
+
+.fa-stethoscope:before {
+    content: ""
+}
+
+.fa-suitcase:before {
+    content: ""
+}
+
+.fa-bell-o:before {
+    content: ""
+}
+
+.fa-coffee:before {
+    content: ""
+}
+
+.fa-cutlery:before {
+    content: ""
+}
+
+.fa-file-text-o:before {
+    content: ""
+}
+
+.fa-building-o:before {
+    content: ""
+}
+
+.fa-hospital-o:before {
+    content: ""
+}
+
+.fa-ambulance:before {
+    content: ""
+}
+
+.fa-medkit:before {
+    content: ""
+}
+
+.fa-fighter-jet:before {
+    content: ""
+}
+
+.fa-beer:before {
+    content: ""
+}
+
+.fa-h-square:before {
+    content: ""
+}
+
+.fa-plus-square:before {
+    content: ""
+}
+
+.fa-angle-double-left:before {
+    content: ""
+}
+
+.fa-angle-double-right:before {
+    content: ""
+}
+
+.fa-angle-double-up:before {
+    content: ""
+}
+
+.fa-angle-double-down:before {
+    content: ""
+}
+
+.fa-angle-left:before {
+    content: ""
+}
+
+.fa-angle-right:before {
+    content: ""
+}
+
+.fa-angle-up:before {
+    content: ""
+}
+
+.fa-angle-down:before {
+    content: ""
+}
+
+.fa-desktop:before {
+    content: ""
+}
+
+.fa-laptop:before {
+    content: ""
+}
+
+.fa-tablet:before {
+    content: ""
+}
+
+.fa-mobile-phone:before,
+.fa-mobile:before {
+    content: ""
+}
+
+.fa-circle-o:before {
+    content: ""
+}
+
+.fa-quote-left:before {
+    content: ""
+}
+
+.fa-quote-right:before {
+    content: ""
+}
+
+.fa-spinner:before {
+    content: ""
+}
+
+.fa-circle:before {
+    content: ""
+}
+
+.fa-mail-reply:before,
+.fa-reply:before {
+    content: ""
+}
+
+.fa-github-alt:before {
+    content: ""
+}
+
+.fa-folder-o:before {
+    content: ""
+}
+
+.fa-folder-open-o:before {
+    content: ""
+}
+
+.fa-smile-o:before {
+    content: ""
+}
+
+.fa-frown-o:before {
+    content: ""
+}
+
+.fa-meh-o:before {
+    content: ""
+}
+
+.fa-gamepad:before {
+    content: ""
+}
+
+.fa-keyboard-o:before {
+    content: ""
+}
+
+.fa-flag-o:before {
+    content: ""
+}
+
+.fa-flag-checkered:before {
+    content: ""
+}
+
+.fa-terminal:before {
+    content: ""
+}
+
+.fa-code:before {
+    content: ""
+}
+
+.fa-mail-reply-all:before,
+.fa-reply-all:before {
+    content: ""
+}
+
+.fa-star-half-empty:before,
+.fa-star-half-full:before,
+.fa-star-half-o:before {
+    content: ""
+}
+
+.fa-location-arrow:before {
+    content: ""
+}
+
+.fa-crop:before {
+    content: ""
+}
+
+.fa-code-fork:before {
+    content: ""
+}
+
+.fa-chain-broken:before,
+.fa-unlink:before {
+    content: ""
+}
+
+.fa-question:before {
+    content: ""
+}
+
+.fa-info:before {
+    content: ""
+}
+
+.fa-exclamation:before {
+    content: ""
+}
+
+.fa-superscript:before {
+    content: ""
+}
+
+.fa-subscript:before {
+    content: ""
+}
+
+.fa-eraser:before {
+    content: ""
+}
+
+.fa-puzzle-piece:before {
+    content: ""
+}
+
+.fa-microphone:before {
+    content: ""
+}
+
+.fa-microphone-slash:before {
+    content: ""
+}
+
+.fa-shield:before {
+    content: ""
+}
+
+.fa-calendar-o:before {
+    content: ""
+}
+
+.fa-fire-extinguisher:before {
+    content: ""
+}
+
+.fa-rocket:before {
+    content: ""
+}
+
+.fa-maxcdn:before {
+    content: ""
+}
+
+.fa-chevron-circle-left:before {
+    content: ""
+}
+
+.fa-chevron-circle-right:before {
+    content: ""
+}
+
+.fa-chevron-circle-up:before {
+    content: ""
+}
+
+.fa-chevron-circle-down:before {
+    content: ""
+}
+
+.fa-html5:before {
+    content: ""
+}
+
+.fa-css3:before {
+    content: ""
+}
+
+.fa-anchor:before {
+    content: ""
+}
+
+.fa-unlock-alt:before {
+    content: ""
+}
+
+.fa-bullseye:before {
+    content: ""
+}
+
+.fa-ellipsis-h:before {
+    content: ""
+}
+
+.fa-ellipsis-v:before {
+    content: ""
+}
+
+.fa-rss-square:before {
+    content: ""
+}
+
+.fa-play-circle:before {
+    content: ""
+}
+
+.fa-ticket:before {
+    content: ""
+}
+
+.fa-minus-square:before {
+    content: ""
+}
+
+.fa-minus-square-o:before,
+.wy-menu-vertical li.current>a button.toctree-expand:before,
+.wy-menu-vertical li.on a button.toctree-expand:before {
+    content: ""
+}
+
+.fa-level-up:before {
+    content: ""
+}
+
+.fa-level-down:before {
+    content: ""
+}
+
+.fa-check-square:before {
+    content: ""
+}
+
+.fa-pencil-square:before {
+    content: ""
+}
+
+.fa-external-link-square:before {
+    content: ""
+}
+
+.fa-share-square:before {
+    content: ""
+}
+
+.fa-compass:before {
+    content: ""
+}
+
+.fa-caret-square-o-down:before,
+.fa-toggle-down:before {
+    content: ""
+}
+
+.fa-caret-square-o-up:before,
+.fa-toggle-up:before {
+    content: ""
+}
+
+.fa-caret-square-o-right:before,
+.fa-toggle-right:before {
+    content: ""
+}
+
+.fa-eur:before,
+.fa-euro:before {
+    content: ""
+}
+
+.fa-gbp:before {
+    content: ""
+}
+
+.fa-dollar:before,
+.fa-usd:before {
+    content: ""
+}
+
+.fa-inr:before,
+.fa-rupee:before {
+    content: ""
+}
+
+.fa-cny:before,
+.fa-jpy:before,
+.fa-rmb:before,
+.fa-yen:before {
+    content: ""
+}
+
+.fa-rouble:before,
+.fa-rub:before,
+.fa-ruble:before {
+    content: ""
+}
+
+.fa-krw:before,
+.fa-won:before {
+    content: ""
+}
+
+.fa-bitcoin:before,
+.fa-btc:before {
+    content: ""
+}
+
+.fa-file:before {
+    content: ""
+}
+
+.fa-file-text:before {
+    content: ""
+}
+
+.fa-sort-alpha-asc:before {
+    content: ""
+}
+
+.fa-sort-alpha-desc:before {
+    content: ""
+}
+
+.fa-sort-amount-asc:before {
+    content: ""
+}
+
+.fa-sort-amount-desc:before {
+    content: ""
+}
+
+.fa-sort-numeric-asc:before {
+    content: ""
+}
+
+.fa-sort-numeric-desc:before {
+    content: ""
+}
+
+.fa-thumbs-up:before {
+    content: ""
+}
+
+.fa-thumbs-down:before {
+    content: ""
+}
+
+.fa-youtube-square:before {
+    content: ""
+}
+
+.fa-youtube:before {
+    content: ""
+}
+
+.fa-xing:before {
+    content: ""
+}
+
+.fa-xing-square:before {
+    content: ""
+}
+
+.fa-youtube-play:before {
+    content: ""
+}
+
+.fa-dropbox:before {
+    content: ""
+}
+
+.fa-stack-overflow:before {
+    content: ""
+}
+
+.fa-instagram:before {
+    content: ""
+}
+
+.fa-flickr:before {
+    content: ""
+}
+
+.fa-adn:before {
+    content: ""
+}
+
+.fa-bitbucket:before,
+.icon-bitbucket:before {
+    content: ""
+}
+
+.fa-bitbucket-square:before {
+    content: ""
+}
+
+.fa-tumblr:before {
+    content: ""
+}
+
+.fa-tumblr-square:before {
+    content: ""
+}
+
+.fa-long-arrow-down:before {
+    content: ""
+}
+
+.fa-long-arrow-up:before {
+    content: ""
+}
+
+.fa-long-arrow-left:before {
+    content: ""
+}
+
+.fa-long-arrow-right:before {
+    content: ""
+}
+
+.fa-apple:before {
+    content: ""
+}
+
+.fa-windows:before {
+    content: ""
+}
+
+.fa-android:before {
+    content: ""
+}
+
+.fa-linux:before {
+    content: ""
+}
+
+.fa-dribbble:before {
+    content: ""
+}
+
+.fa-skype:before {
+    content: ""
+}
+
+.fa-foursquare:before {
+    content: ""
+}
+
+.fa-trello:before {
+    content: ""
+}
+
+.fa-female:before {
+    content: ""
+}
+
+.fa-male:before {
+    content: ""
+}
+
+.fa-gittip:before,
+.fa-gratipay:before {
+    content: ""
+}
+
+.fa-sun-o:before {
+    content: ""
+}
+
+.fa-moon-o:before {
+    content: ""
+}
+
+.fa-archive:before {
+    content: ""
+}
+
+.fa-bug:before {
+    content: ""
+}
+
+.fa-vk:before {
+    content: ""
+}
+
+.fa-weibo:before {
+    content: ""
+}
+
+.fa-renren:before {
+    content: ""
+}
+
+.fa-pagelines:before {
+    content: ""
+}
+
+.fa-stack-exchange:before {
+    content: ""
+}
+
+.fa-arrow-circle-o-right:before {
+    content: ""
+}
+
+.fa-arrow-circle-o-left:before {
+    content: ""
+}
+
+.fa-caret-square-o-left:before,
+.fa-toggle-left:before {
+    content: ""
+}
+
+.fa-dot-circle-o:before {
+    content: ""
+}
+
+.fa-wheelchair:before {
+    content: ""
+}
+
+.fa-vimeo-square:before {
+    content: ""
+}
+
+.fa-try:before,
+.fa-turkish-lira:before {
+    content: ""
+}
+
+.fa-plus-square-o:before,
+.wy-menu-vertical li button.toctree-expand:before {
+    content: ""
+}
+
+.fa-space-shuttle:before {
+    content: ""
+}
+
+.fa-slack:before {
+    content: ""
+}
+
+.fa-envelope-square:before {
+    content: ""
+}
+
+.fa-wordpress:before {
+    content: ""
+}
+
+.fa-openid:before {
+    content: ""
+}
+
+.fa-bank:before,
+.fa-institution:before,
+.fa-university:before {
+    content: ""
+}
+
+.fa-graduation-cap:before,
+.fa-mortar-board:before {
+    content: ""
+}
+
+.fa-yahoo:before {
+    content: ""
+}
+
+.fa-google:before {
+    content: ""
+}
+
+.fa-reddit:before {
+    content: ""
+}
+
+.fa-reddit-square:before {
+    content: ""
+}
+
+.fa-stumbleupon-circle:before {
+    content: ""
+}
+
+.fa-stumbleupon:before {
+    content: ""
+}
+
+.fa-delicious:before {
+    content: ""
+}
+
+.fa-digg:before {
+    content: ""
+}
+
+.fa-pied-piper-pp:before {
+    content: ""
+}
+
+.fa-pied-piper-alt:before {
+    content: ""
+}
+
+.fa-drupal:before {
+    content: ""
+}
+
+.fa-joomla:before {
+    content: ""
+}
+
+.fa-language:before {
+    content: ""
+}
+
+.fa-fax:before {
+    content: ""
+}
+
+.fa-building:before {
+    content: ""
+}
+
+.fa-child:before {
+    content: ""
+}
+
+.fa-paw:before {
+    content: ""
+}
+
+.fa-spoon:before {
+    content: ""
+}
+
+.fa-cube:before {
+    content: ""
+}
+
+.fa-cubes:before {
+    content: ""
+}
+
+.fa-behance:before {
+    content: ""
+}
+
+.fa-behance-square:before {
+    content: ""
+}
+
+.fa-steam:before {
+    content: ""
+}
+
+.fa-steam-square:before {
+    content: ""
+}
+
+.fa-recycle:before {
+    content: ""
+}
+
+.fa-automobile:before,
+.fa-car:before {
+    content: ""
+}
+
+.fa-cab:before,
+.fa-taxi:before {
+    content: ""
+}
+
+.fa-tree:before {
+    content: ""
+}
+
+.fa-spotify:before {
+    content: ""
+}
+
+.fa-deviantart:before {
+    content: ""
+}
+
+.fa-soundcloud:before {
+    content: ""
+}
+
+.fa-database:before {
+    content: ""
+}
+
+.fa-file-pdf-o:before {
+    content: ""
+}
+
+.fa-file-word-o:before {
+    content: ""
+}
+
+.fa-file-excel-o:before {
+    content: ""
+}
+
+.fa-file-powerpoint-o:before {
+    content: ""
+}
+
+.fa-file-image-o:before,
+.fa-file-photo-o:before,
+.fa-file-picture-o:before {
+    content: ""
+}
+
+.fa-file-archive-o:before,
+.fa-file-zip-o:before {
+    content: ""
+}
+
+.fa-file-audio-o:before,
+.fa-file-sound-o:before {
+    content: ""
+}
+
+.fa-file-movie-o:before,
+.fa-file-video-o:before {
+    content: ""
+}
+
+.fa-file-code-o:before {
+    content: ""
+}
+
+.fa-vine:before {
+    content: ""
+}
+
+.fa-codepen:before {
+    content: ""
+}
+
+.fa-jsfiddle:before {
+    content: ""
+}
+
+.fa-life-bouy:before,
+.fa-life-buoy:before,
+.fa-life-ring:before,
+.fa-life-saver:before,
+.fa-support:before {
+    content: ""
+}
+
+.fa-circle-o-notch:before {
+    content: ""
+}
+
+.fa-ra:before,
+.fa-rebel:before,
+.fa-resistance:before {
+    content: ""
+}
+
+.fa-empire:before,
+.fa-ge:before {
+    content: ""
+}
+
+.fa-git-square:before {
+    content: ""
+}
+
+.fa-git:before {
+    content: ""
+}
+
+.fa-hacker-news:before,
+.fa-y-combinator-square:before,
+.fa-yc-square:before {
+    content: ""
+}
+
+.fa-tencent-weibo:before {
+    content: ""
+}
+
+.fa-qq:before {
+    content: ""
+}
+
+.fa-wechat:before,
+.fa-weixin:before {
+    content: ""
+}
+
+.fa-paper-plane:before,
+.fa-send:before {
+    content: ""
+}
+
+.fa-paper-plane-o:before,
+.fa-send-o:before {
+    content: ""
+}
+
+.fa-history:before {
+    content: ""
+}
+
+.fa-circle-thin:before {
+    content: ""
+}
+
+.fa-header:before {
+    content: ""
+}
+
+.fa-paragraph:before {
+    content: ""
+}
+
+.fa-sliders:before {
+    content: ""
+}
+
+.fa-share-alt:before {
+    content: ""
+}
+
+.fa-share-alt-square:before {
+    content: ""
+}
+
+.fa-bomb:before {
+    content: ""
+}
+
+.fa-futbol-o:before,
+.fa-soccer-ball-o:before {
+    content: ""
+}
+
+.fa-tty:before {
+    content: ""
+}
+
+.fa-binoculars:before {
+    content: ""
+}
+
+.fa-plug:before {
+    content: ""
+}
+
+.fa-slideshare:before {
+    content: ""
+}
+
+.fa-twitch:before {
+    content: ""
+}
+
+.fa-yelp:before {
+    content: ""
+}
+
+.fa-newspaper-o:before {
+    content: ""
+}
+
+.fa-wifi:before {
+    content: ""
+}
+
+.fa-calculator:before {
+    content: ""
+}
+
+.fa-paypal:before {
+    content: ""
+}
+
+.fa-google-wallet:before {
+    content: ""
+}
+
+.fa-cc-visa:before {
+    content: ""
+}
+
+.fa-cc-mastercard:before {
+    content: ""
+}
+
+.fa-cc-discover:before {
+    content: ""
+}
+
+.fa-cc-amex:before {
+    content: ""
+}
+
+.fa-cc-paypal:before {
+    content: ""
+}
+
+.fa-cc-stripe:before {
+    content: ""
+}
+
+.fa-bell-slash:before {
+    content: ""
+}
+
+.fa-bell-slash-o:before {
+    content: ""
+}
+
+.fa-trash:before {
+    content: ""
+}
+
+.fa-copyright:before {
+    content: ""
+}
+
+.fa-at:before {
+    content: ""
+}
+
+.fa-eyedropper:before {
+    content: ""
+}
+
+.fa-paint-brush:before {
+    content: ""
+}
+
+.fa-birthday-cake:before {
+    content: ""
+}
+
+.fa-area-chart:before {
+    content: ""
+}
+
+.fa-pie-chart:before {
+    content: ""
+}
+
+.fa-line-chart:before {
+    content: ""
+}
+
+.fa-lastfm:before {
+    content: ""
+}
+
+.fa-lastfm-square:before {
+    content: ""
+}
+
+.fa-toggle-off:before {
+    content: ""
+}
+
+.fa-toggle-on:before {
+    content: ""
+}
+
+.fa-bicycle:before {
+    content: ""
+}
+
+.fa-bus:before {
+    content: ""
+}
+
+.fa-ioxhost:before {
+    content: ""
+}
+
+.fa-angellist:before {
+    content: ""
+}
+
+.fa-cc:before {
+    content: ""
+}
+
+.fa-ils:before,
+.fa-shekel:before,
+.fa-sheqel:before {
+    content: ""
+}
+
+.fa-meanpath:before {
+    content: ""
+}
+
+.fa-buysellads:before {
+    content: ""
+}
+
+.fa-connectdevelop:before {
+    content: ""
+}
+
+.fa-dashcube:before {
+    content: ""
+}
+
+.fa-forumbee:before {
+    content: ""
+}
+
+.fa-leanpub:before {
+    content: ""
+}
+
+.fa-sellsy:before {
+    content: ""
+}
+
+.fa-shirtsinbulk:before {
+    content: ""
+}
+
+.fa-simplybuilt:before {
+    content: ""
+}
+
+.fa-skyatlas:before {
+    content: ""
+}
+
+.fa-cart-plus:before {
+    content: ""
+}
+
+.fa-cart-arrow-down:before {
+    content: ""
+}
+
+.fa-diamond:before {
+    content: ""
+}
+
+.fa-ship:before {
+    content: ""
+}
+
+.fa-user-secret:before {
+    content: ""
+}
+
+.fa-motorcycle:before {
+    content: ""
+}
+
+.fa-street-view:before {
+    content: ""
+}
+
+.fa-heartbeat:before {
+    content: ""
+}
+
+.fa-venus:before {
+    content: ""
+}
+
+.fa-mars:before {
+    content: ""
+}
+
+.fa-mercury:before {
+    content: ""
+}
+
+.fa-intersex:before,
+.fa-transgender:before {
+    content: ""
+}
+
+.fa-transgender-alt:before {
+    content: ""
+}
+
+.fa-venus-double:before {
+    content: ""
+}
+
+.fa-mars-double:before {
+    content: ""
+}
+
+.fa-venus-mars:before {
+    content: ""
+}
+
+.fa-mars-stroke:before {
+    content: ""
+}
+
+.fa-mars-stroke-v:before {
+    content: ""
+}
+
+.fa-mars-stroke-h:before {
+    content: ""
+}
+
+.fa-neuter:before {
+    content: ""
+}
+
+.fa-genderless:before {
+    content: ""
+}
+
+.fa-facebook-official:before {
+    content: ""
+}
+
+.fa-pinterest-p:before {
+    content: ""
+}
+
+.fa-whatsapp:before {
+    content: ""
+}
+
+.fa-server:before {
+    content: ""
+}
+
+.fa-user-plus:before {
+    content: ""
+}
+
+.fa-user-times:before {
+    content: ""
+}
+
+.fa-bed:before,
+.fa-hotel:before {
+    content: ""
+}
+
+.fa-viacoin:before {
+    content: ""
+}
+
+.fa-train:before {
+    content: ""
+}
+
+.fa-subway:before {
+    content: ""
+}
+
+.fa-medium:before {
+    content: ""
+}
+
+.fa-y-combinator:before,
+.fa-yc:before {
+    content: ""
+}
+
+.fa-optin-monster:before {
+    content: ""
+}
+
+.fa-opencart:before {
+    content: ""
+}
+
+.fa-expeditedssl:before {
+    content: ""
+}
+
+.fa-battery-4:before,
+.fa-battery-full:before,
+.fa-battery:before {
+    content: ""
+}
+
+.fa-battery-3:before,
+.fa-battery-three-quarters:before {
+    content: ""
+}
+
+.fa-battery-2:before,
+.fa-battery-half:before {
+    content: ""
+}
+
+.fa-battery-1:before,
+.fa-battery-quarter:before {
+    content: ""
+}
+
+.fa-battery-0:before,
+.fa-battery-empty:before {
+    content: ""
+}
+
+.fa-mouse-pointer:before {
+    content: ""
+}
+
+.fa-i-cursor:before {
+    content: ""
+}
+
+.fa-object-group:before {
+    content: ""
+}
+
+.fa-object-ungroup:before {
+    content: ""
+}
+
+.fa-sticky-note:before {
+    content: ""
+}
+
+.fa-sticky-note-o:before {
+    content: ""
+}
+
+.fa-cc-jcb:before {
+    content: ""
+}
+
+.fa-cc-diners-club:before {
+    content: ""
+}
+
+.fa-clone:before {
+    content: ""
+}
+
+.fa-balance-scale:before {
+    content: ""
+}
+
+.fa-hourglass-o:before {
+    content: ""
+}
+
+.fa-hourglass-1:before,
+.fa-hourglass-start:before {
+    content: ""
+}
+
+.fa-hourglass-2:before,
+.fa-hourglass-half:before {
+    content: ""
+}
+
+.fa-hourglass-3:before,
+.fa-hourglass-end:before {
+    content: ""
+}
+
+.fa-hourglass:before {
+    content: ""
+}
+
+.fa-hand-grab-o:before,
+.fa-hand-rock-o:before {
+    content: ""
+}
+
+.fa-hand-paper-o:before,
+.fa-hand-stop-o:before {
+    content: ""
+}
+
+.fa-hand-scissors-o:before {
+    content: ""
+}
+
+.fa-hand-lizard-o:before {
+    content: ""
+}
+
+.fa-hand-spock-o:before {
+    content: ""
+}
+
+.fa-hand-pointer-o:before {
+    content: ""
+}
+
+.fa-hand-peace-o:before {
+    content: ""
+}
+
+.fa-trademark:before {
+    content: ""
+}
+
+.fa-registered:before {
+    content: ""
+}
+
+.fa-creative-commons:before {
+    content: ""
+}
+
+.fa-gg:before {
+    content: ""
+}
+
+.fa-gg-circle:before {
+    content: ""
+}
+
+.fa-tripadvisor:before {
+    content: ""
+}
+
+.fa-odnoklassniki:before {
+    content: ""
+}
+
+.fa-odnoklassniki-square:before {
+    content: ""
+}
+
+.fa-get-pocket:before {
+    content: ""
+}
+
+.fa-wikipedia-w:before {
+    content: ""
+}
+
+.fa-safari:before {
+    content: ""
+}
+
+.fa-chrome:before {
+    content: ""
+}
+
+.fa-firefox:before {
+    content: ""
+}
+
+.fa-opera:before {
+    content: ""
+}
+
+.fa-internet-explorer:before {
+    content: ""
+}
+
+.fa-television:before,
+.fa-tv:before {
+    content: ""
+}
+
+.fa-contao:before {
+    content: ""
+}
+
+.fa-500px:before {
+    content: ""
+}
+
+.fa-amazon:before {
+    content: ""
+}
+
+.fa-calendar-plus-o:before {
+    content: ""
+}
+
+.fa-calendar-minus-o:before {
+    content: ""
+}
+
+.fa-calendar-times-o:before {
+    content: ""
+}
+
+.fa-calendar-check-o:before {
+    content: ""
+}
+
+.fa-industry:before {
+    content: ""
+}
+
+.fa-map-pin:before {
+    content: ""
+}
+
+.fa-map-signs:before {
+    content: ""
+}
+
+.fa-map-o:before {
+    content: ""
+}
+
+.fa-map:before {
+    content: ""
+}
+
+.fa-commenting:before {
+    content: ""
+}
+
+.fa-commenting-o:before {
+    content: ""
+}
+
+.fa-houzz:before {
+    content: ""
+}
+
+.fa-vimeo:before {
+    content: ""
+}
+
+.fa-black-tie:before {
+    content: ""
+}
+
+.fa-fonticons:before {
+    content: ""
+}
+
+.fa-reddit-alien:before {
+    content: ""
+}
+
+.fa-edge:before {
+    content: ""
+}
+
+.fa-credit-card-alt:before {
+    content: ""
+}
+
+.fa-codiepie:before {
+    content: ""
+}
+
+.fa-modx:before {
+    content: ""
+}
+
+.fa-fort-awesome:before {
+    content: ""
+}
+
+.fa-usb:before {
+    content: ""
+}
+
+.fa-product-hunt:before {
+    content: ""
+}
+
+.fa-mixcloud:before {
+    content: ""
+}
+
+.fa-scribd:before {
+    content: ""
+}
+
+.fa-pause-circle:before {
+    content: ""
+}
+
+.fa-pause-circle-o:before {
+    content: ""
+}
+
+.fa-stop-circle:before {
+    content: ""
+}
+
+.fa-stop-circle-o:before {
+    content: ""
+}
+
+.fa-shopping-bag:before {
+    content: ""
+}
+
+.fa-shopping-basket:before {
+    content: ""
+}
+
+.fa-hashtag:before {
+    content: ""
+}
+
+.fa-bluetooth:before {
+    content: ""
+}
+
+.fa-bluetooth-b:before {
+    content: ""
+}
+
+.fa-percent:before {
+    content: ""
+}
+
+.fa-gitlab:before,
+.icon-gitlab:before {
+    content: ""
+}
+
+.fa-wpbeginner:before {
+    content: ""
+}
+
+.fa-wpforms:before {
+    content: ""
+}
+
+.fa-envira:before {
+    content: ""
+}
+
+.fa-universal-access:before {
+    content: ""
+}
+
+.fa-wheelchair-alt:before {
+    content: ""
+}
+
+.fa-question-circle-o:before {
+    content: ""
+}
+
+.fa-blind:before {
+    content: ""
+}
+
+.fa-audio-description:before {
+    content: ""
+}
+
+.fa-volume-control-phone:before {
+    content: ""
+}
+
+.fa-braille:before {
+    content: ""
+}
+
+.fa-assistive-listening-systems:before {
+    content: ""
+}
+
+.fa-american-sign-language-interpreting:before,
+.fa-asl-interpreting:before {
+    content: ""
+}
+
+.fa-deaf:before,
+.fa-deafness:before,
+.fa-hard-of-hearing:before {
+    content: ""
+}
+
+.fa-glide:before {
+    content: ""
+}
+
+.fa-glide-g:before {
+    content: ""
+}
+
+.fa-sign-language:before,
+.fa-signing:before {
+    content: ""
+}
+
+.fa-low-vision:before {
+    content: ""
+}
+
+.fa-viadeo:before {
+    content: ""
+}
+
+.fa-viadeo-square:before {
+    content: ""
+}
+
+.fa-snapchat:before {
+    content: ""
+}
+
+.fa-snapchat-ghost:before {
+    content: ""
+}
+
+.fa-snapchat-square:before {
+    content: ""
+}
+
+.fa-pied-piper:before {
+    content: ""
+}
+
+.fa-first-order:before {
+    content: ""
+}
+
+.fa-yoast:before {
+    content: ""
+}
+
+.fa-themeisle:before {
+    content: ""
+}
+
+.fa-google-plus-circle:before,
+.fa-google-plus-official:before {
+    content: ""
+}
+
+.fa-fa:before,
+.fa-font-awesome:before {
+    content: ""
+}
+
+.fa-handshake-o:before {
+    content: ""
+}
+
+.fa-envelope-open:before {
+    content: ""
+}
+
+.fa-envelope-open-o:before {
+    content: ""
+}
+
+.fa-linode:before {
+    content: ""
+}
+
+.fa-address-book:before {
+    content: ""
+}
+
+.fa-address-book-o:before {
+    content: ""
+}
+
+.fa-address-card:before,
+.fa-vcard:before {
+    content: ""
+}
+
+.fa-address-card-o:before,
+.fa-vcard-o:before {
+    content: ""
+}
+
+.fa-user-circle:before {
+    content: ""
+}
+
+.fa-user-circle-o:before {
+    content: ""
+}
+
+.fa-user-o:before {
+    content: ""
+}
+
+.fa-id-badge:before {
+    content: ""
+}
+
+.fa-drivers-license:before,
+.fa-id-card:before {
+    content: ""
+}
+
+.fa-drivers-license-o:before,
+.fa-id-card-o:before {
+    content: ""
+}
+
+.fa-quora:before {
+    content: ""
+}
+
+.fa-free-code-camp:before {
+    content: ""
+}
+
+.fa-telegram:before {
+    content: ""
+}
+
+.fa-thermometer-4:before,
+.fa-thermometer-full:before,
+.fa-thermometer:before {
+    content: ""
+}
+
+.fa-thermometer-3:before,
+.fa-thermometer-three-quarters:before {
+    content: ""
+}
+
+.fa-thermometer-2:before,
+.fa-thermometer-half:before {
+    content: ""
+}
+
+.fa-thermometer-1:before,
+.fa-thermometer-quarter:before {
+    content: ""
+}
+
+.fa-thermometer-0:before,
+.fa-thermometer-empty:before {
+    content: ""
+}
+
+.fa-shower:before {
+    content: ""
+}
+
+.fa-bath:before,
+.fa-bathtub:before,
+.fa-s15:before {
+    content: ""
+}
+
+.fa-podcast:before {
+    content: ""
+}
+
+.fa-window-maximize:before {
+    content: ""
+}
+
+.fa-window-minimize:before {
+    content: ""
+}
+
+.fa-window-restore:before {
+    content: ""
+}
+
+.fa-times-rectangle:before,
+.fa-window-close:before {
+    content: ""
+}
+
+.fa-times-rectangle-o:before,
+.fa-window-close-o:before {
+    content: ""
+}
+
+.fa-bandcamp:before {
+    content: ""
+}
+
+.fa-grav:before {
+    content: ""
+}
+
+.fa-etsy:before {
+    content: ""
+}
+
+.fa-imdb:before {
+    content: ""
+}
+
+.fa-ravelry:before {
+    content: ""
+}
+
+.fa-eercast:before {
+    content: ""
+}
+
+.fa-microchip:before {
+    content: ""
+}
+
+.fa-snowflake-o:before {
+    content: ""
+}
+
+.fa-superpowers:before {
+    content: ""
+}
+
+.fa-wpexplorer:before {
+    content: ""
+}
+
+.fa-meetup:before {
+    content: ""
+}
+
+.sr-only {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    border: 0
+}
+
+.sr-only-focusable:active,
+.sr-only-focusable:focus {
+    position: static;
+    width: auto;
+    height: auto;
+    margin: 0;
+    overflow: visible;
+    clip: auto
+}
+
+.fa,
+.icon,
+.rst-content .admonition-title,
+.rst-content .code-block-caption .headerlink,
+.rst-content .eqno .headerlink,
+.rst-content code.download span:first-child,
+.rst-content dl dt .headerlink,
+.rst-content h1 .headerlink,
+.rst-content h2 .headerlink,
+.rst-content h3 .headerlink,
+.rst-content h4 .headerlink,
+.rst-content h5 .headerlink,
+.rst-content h6 .headerlink,
+.rst-content p.caption .headerlink,
+.rst-content p .headerlink,
+.rst-content table>caption .headerlink,
+.rst-content tt.download span:first-child,
+.wy-dropdown .caret,
+.wy-inline-validate.wy-inline-validate-danger .wy-input-context,
+.wy-inline-validate.wy-inline-validate-info .wy-input-context,
+.wy-inline-validate.wy-inline-validate-success .wy-input-context,
+.wy-inline-validate.wy-inline-validate-warning .wy-input-context,
+.wy-menu-vertical li.current>a button.toctree-expand,
+.wy-menu-vertical li.on a button.toctree-expand,
+.wy-menu-vertical li button.toctree-expand {
+    font-family: inherit
+}
+
+.fa:before,
+.icon:before,
+.rst-content .admonition-title:before,
+.rst-content .code-block-caption .headerlink:before,
+.rst-content .eqno .headerlink:before,
+.rst-content code.download span:first-child:before,
+.rst-content dl dt .headerlink:before,
+.rst-content h1 .headerlink:before,
+.rst-content h2 .headerlink:before,
+.rst-content h3 .headerlink:before,
+.rst-content h4 .headerlink:before,
+.rst-content h5 .headerlink:before,
+.rst-content h6 .headerlink:before,
+.rst-content p.caption .headerlink:before,
+.rst-content p .headerlink:before,
+.rst-content table>caption .headerlink:before,
+.rst-content tt.download span:first-child:before,
+.wy-dropdown .caret:before,
+.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,
+.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,
+.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,
+.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,
+.wy-menu-vertical li.current>a button.toctree-expand:before,
+.wy-menu-vertical li.on a button.toctree-expand:before,
+.wy-menu-vertical li button.toctree-expand:before {
+    font-family: FontAwesome;
+    display: inline-block;
+    font-style: normal;
+    font-weight: 400;
+    line-height: 1;
+    text-decoration: inherit
+}
+
+.rst-content .code-block-caption a .headerlink,
+.rst-content .eqno a .headerlink,
+.rst-content a .admonition-title,
+.rst-content code.download a span:first-child,
+.rst-content dl dt a .headerlink,
+.rst-content h1 a .headerlink,
+.rst-content h2 a .headerlink,
+.rst-content h3 a .headerlink,
+.rst-content h4 a .headerlink,
+.rst-content h5 a .headerlink,
+.rst-content h6 a .headerlink,
+.rst-content p.caption a .headerlink,
+.rst-content p a .headerlink,
+.rst-content table>caption a .headerlink,
+.rst-content tt.download a span:first-child,
+.wy-menu-vertical li.current>a button.toctree-expand,
+.wy-menu-vertical li.on a button.toctree-expand,
+.wy-menu-vertical li a button.toctree-expand,
+a .fa,
+a .icon,
+a .rst-content .admonition-title,
+a .rst-content .code-block-caption .headerlink,
+a .rst-content .eqno .headerlink,
+a .rst-content code.download span:first-child,
+a .rst-content dl dt .headerlink,
+a .rst-content h1 .headerlink,
+a .rst-content h2 .headerlink,
+a .rst-content h3 .headerlink,
+a .rst-content h4 .headerlink,
+a .rst-content h5 .headerlink,
+a .rst-content h6 .headerlink,
+a .rst-content p.caption .headerlink,
+a .rst-content p .headerlink,
+a .rst-content table>caption .headerlink,
+a .rst-content tt.download span:first-child,
+a .wy-menu-vertical li button.toctree-expand {
+    display: inline-block;
+    text-decoration: inherit
+}
+
+.btn .fa,
+.btn .icon,
+.btn .rst-content .admonition-title,
+.btn .rst-content .code-block-caption .headerlink,
+.btn .rst-content .eqno .headerlink,
+.btn .rst-content code.download span:first-child,
+.btn .rst-content dl dt .headerlink,
+.btn .rst-content h1 .headerlink,
+.btn .rst-content h2 .headerlink,
+.btn .rst-content h3 .headerlink,
+.btn .rst-content h4 .headerlink,
+.btn .rst-content h5 .headerlink,
+.btn .rst-content h6 .headerlink,
+.btn .rst-content p .headerlink,
+.btn .rst-content table>caption .headerlink,
+.btn .rst-content tt.download span:first-child,
+.btn .wy-menu-vertical li.current>a button.toctree-expand,
+.btn .wy-menu-vertical li.on a button.toctree-expand,
+.btn .wy-menu-vertical li button.toctree-expand,
+.nav .fa,
+.nav .icon,
+.nav .rst-content .admonition-title,
+.nav .rst-content .code-block-caption .headerlink,
+.nav .rst-content .eqno .headerlink,
+.nav .rst-content code.download span:first-child,
+.nav .rst-content dl dt .headerlink,
+.nav .rst-content h1 .headerlink,
+.nav .rst-content h2 .headerlink,
+.nav .rst-content h3 .headerlink,
+.nav .rst-content h4 .headerlink,
+.nav .rst-content h5 .headerlink,
+.nav .rst-content h6 .headerlink,
+.nav .rst-content p .headerlink,
+.nav .rst-content table>caption .headerlink,
+.nav .rst-content tt.download span:first-child,
+.nav .wy-menu-vertical li.current>a button.toctree-expand,
+.nav .wy-menu-vertical li.on a button.toctree-expand,
+.nav .wy-menu-vertical li button.toctree-expand,
+.rst-content .btn .admonition-title,
+.rst-content .code-block-caption .btn .headerlink,
+.rst-content .code-block-caption .nav .headerlink,
+.rst-content .eqno .btn .headerlink,
+.rst-content .eqno .nav .headerlink,
+.rst-content .nav .admonition-title,
+.rst-content code.download .btn span:first-child,
+.rst-content code.download .nav span:first-child,
+.rst-content dl dt .btn .headerlink,
+.rst-content dl dt .nav .headerlink,
+.rst-content h1 .btn .headerlink,
+.rst-content h1 .nav .headerlink,
+.rst-content h2 .btn .headerlink,
+.rst-content h2 .nav .headerlink,
+.rst-content h3 .btn .headerlink,
+.rst-content h3 .nav .headerlink,
+.rst-content h4 .btn .headerlink,
+.rst-content h4 .nav .headerlink,
+.rst-content h5 .btn .headerlink,
+.rst-content h5 .nav .headerlink,
+.rst-content h6 .btn .headerlink,
+.rst-content h6 .nav .headerlink,
+.rst-content p .btn .headerlink,
+.rst-content p .nav .headerlink,
+.rst-content table>caption .btn .headerlink,
+.rst-content table>caption .nav .headerlink,
+.rst-content tt.download .btn span:first-child,
+.rst-content tt.download .nav span:first-child,
+.wy-menu-vertical li .btn button.toctree-expand,
+.wy-menu-vertical li.current>a .btn button.toctree-expand,
+.wy-menu-vertical li.current>a .nav button.toctree-expand,
+.wy-menu-vertical li .nav button.toctree-expand,
+.wy-menu-vertical li.on a .btn button.toctree-expand,
+.wy-menu-vertical li.on a .nav button.toctree-expand {
+    display: inline
+}
+
+.btn .fa-large.icon,
+.btn .fa.fa-large,
+.btn .rst-content .code-block-caption .fa-large.headerlink,
+.btn .rst-content .eqno .fa-large.headerlink,
+.btn .rst-content .fa-large.admonition-title,
+.btn .rst-content code.download span.fa-large:first-child,
+.btn .rst-content dl dt .fa-large.headerlink,
+.btn .rst-content h1 .fa-large.headerlink,
+.btn .rst-content h2 .fa-large.headerlink,
+.btn .rst-content h3 .fa-large.headerlink,
+.btn .rst-content h4 .fa-large.headerlink,
+.btn .rst-content h5 .fa-large.headerlink,
+.btn .rst-content h6 .fa-large.headerlink,
+.btn .rst-content p .fa-large.headerlink,
+.btn .rst-content table>caption .fa-large.headerlink,
+.btn .rst-content tt.download span.fa-large:first-child,
+.btn .wy-menu-vertical li button.fa-large.toctree-expand,
+.nav .fa-large.icon,
+.nav .fa.fa-large,
+.nav .rst-content .code-block-caption .fa-large.headerlink,
+.nav .rst-content .eqno .fa-large.headerlink,
+.nav .rst-content .fa-large.admonition-title,
+.nav .rst-content code.download span.fa-large:first-child,
+.nav .rst-content dl dt .fa-large.headerlink,
+.nav .rst-content h1 .fa-large.headerlink,
+.nav .rst-content h2 .fa-large.headerlink,
+.nav .rst-content h3 .fa-large.headerlink,
+.nav .rst-content h4 .fa-large.headerlink,
+.nav .rst-content h5 .fa-large.headerlink,
+.nav .rst-content h6 .fa-large.headerlink,
+.nav .rst-content p .fa-large.headerlink,
+.nav .rst-content table>caption .fa-large.headerlink,
+.nav .rst-content tt.download span.fa-large:first-child,
+.nav .wy-menu-vertical li button.fa-large.toctree-expand,
+.rst-content .btn .fa-large.admonition-title,
+.rst-content .code-block-caption .btn .fa-large.headerlink,
+.rst-content .code-block-caption .nav .fa-large.headerlink,
+.rst-content .eqno .btn .fa-large.headerlink,
+.rst-content .eqno .nav .fa-large.headerlink,
+.rst-content .nav .fa-large.admonition-title,
+.rst-content code.download .btn span.fa-large:first-child,
+.rst-content code.download .nav span.fa-large:first-child,
+.rst-content dl dt .btn .fa-large.headerlink,
+.rst-content dl dt .nav .fa-large.headerlink,
+.rst-content h1 .btn .fa-large.headerlink,
+.rst-content h1 .nav .fa-large.headerlink,
+.rst-content h2 .btn .fa-large.headerlink,
+.rst-content h2 .nav .fa-large.headerlink,
+.rst-content h3 .btn .fa-large.headerlink,
+.rst-content h3 .nav .fa-large.headerlink,
+.rst-content h4 .btn .fa-large.headerlink,
+.rst-content h4 .nav .fa-large.headerlink,
+.rst-content h5 .btn .fa-large.headerlink,
+.rst-content h5 .nav .fa-large.headerlink,
+.rst-content h6 .btn .fa-large.headerlink,
+.rst-content h6 .nav .fa-large.headerlink,
+.rst-content p .btn .fa-large.headerlink,
+.rst-content p .nav .fa-large.headerlink,
+.rst-content table>caption .btn .fa-large.headerlink,
+.rst-content table>caption .nav .fa-large.headerlink,
+.rst-content tt.download .btn span.fa-large:first-child,
+.rst-content tt.download .nav span.fa-large:first-child,
+.wy-menu-vertical li .btn button.fa-large.toctree-expand,
+.wy-menu-vertical li .nav button.fa-large.toctree-expand {
+    line-height: .9em
+}
+
+.btn .fa-spin.icon,
+.btn .fa.fa-spin,
+.btn .rst-content .code-block-caption .fa-spin.headerlink,
+.btn .rst-content .eqno .fa-spin.headerlink,
+.btn .rst-content .fa-spin.admonition-title,
+.btn .rst-content code.download span.fa-spin:first-child,
+.btn .rst-content dl dt .fa-spin.headerlink,
+.btn .rst-content h1 .fa-spin.headerlink,
+.btn .rst-content h2 .fa-spin.headerlink,
+.btn .rst-content h3 .fa-spin.headerlink,
+.btn .rst-content h4 .fa-spin.headerlink,
+.btn .rst-content h5 .fa-spin.headerlink,
+.btn .rst-content h6 .fa-spin.headerlink,
+.btn .rst-content p .fa-spin.headerlink,
+.btn .rst-content table>caption .fa-spin.headerlink,
+.btn .rst-content tt.download span.fa-spin:first-child,
+.btn .wy-menu-vertical li button.fa-spin.toctree-expand,
+.nav .fa-spin.icon,
+.nav .fa.fa-spin,
+.nav .rst-content .code-block-caption .fa-spin.headerlink,
+.nav .rst-content .eqno .fa-spin.headerlink,
+.nav .rst-content .fa-spin.admonition-title,
+.nav .rst-content code.download span.fa-spin:first-child,
+.nav .rst-content dl dt .fa-spin.headerlink,
+.nav .rst-content h1 .fa-spin.headerlink,
+.nav .rst-content h2 .fa-spin.headerlink,
+.nav .rst-content h3 .fa-spin.headerlink,
+.nav .rst-content h4 .fa-spin.headerlink,
+.nav .rst-content h5 .fa-spin.headerlink,
+.nav .rst-content h6 .fa-spin.headerlink,
+.nav .rst-content p .fa-spin.headerlink,
+.nav .rst-content table>caption .fa-spin.headerlink,
+.nav .rst-content tt.download span.fa-spin:first-child,
+.nav .wy-menu-vertical li button.fa-spin.toctree-expand,
+.rst-content .btn .fa-spin.admonition-title,
+.rst-content .code-block-caption .btn .fa-spin.headerlink,
+.rst-content .code-block-caption .nav .fa-spin.headerlink,
+.rst-content .eqno .btn .fa-spin.headerlink,
+.rst-content .eqno .nav .fa-spin.headerlink,
+.rst-content .nav .fa-spin.admonition-title,
+.rst-content code.download .btn span.fa-spin:first-child,
+.rst-content code.download .nav span.fa-spin:first-child,
+.rst-content dl dt .btn .fa-spin.headerlink,
+.rst-content dl dt .nav .fa-spin.headerlink,
+.rst-content h1 .btn .fa-spin.headerlink,
+.rst-content h1 .nav .fa-spin.headerlink,
+.rst-content h2 .btn .fa-spin.headerlink,
+.rst-content h2 .nav .fa-spin.headerlink,
+.rst-content h3 .btn .fa-spin.headerlink,
+.rst-content h3 .nav .fa-spin.headerlink,
+.rst-content h4 .btn .fa-spin.headerlink,
+.rst-content h4 .nav .fa-spin.headerlink,
+.rst-content h5 .btn .fa-spin.headerlink,
+.rst-content h5 .nav .fa-spin.headerlink,
+.rst-content h6 .btn .fa-spin.headerlink,
+.rst-content h6 .nav .fa-spin.headerlink,
+.rst-content p .btn .fa-spin.headerlink,
+.rst-content p .nav .fa-spin.headerlink,
+.rst-content table>caption .btn .fa-spin.headerlink,
+.rst-content table>caption .nav .fa-spin.headerlink,
+.rst-content tt.download .btn span.fa-spin:first-child,
+.rst-content tt.download .nav span.fa-spin:first-child,
+.wy-menu-vertical li .btn button.fa-spin.toctree-expand,
+.wy-menu-vertical li .nav button.fa-spin.toctree-expand {
+    display: inline-block
+}
+
+.btn.fa:before,
+.btn.icon:before,
+.rst-content .btn.admonition-title:before,
+.rst-content .code-block-caption .btn.headerlink:before,
+.rst-content .eqno .btn.headerlink:before,
+.rst-content code.download span.btn:first-child:before,
+.rst-content dl dt .btn.headerlink:before,
+.rst-content h1 .btn.headerlink:before,
+.rst-content h2 .btn.headerlink:before,
+.rst-content h3 .btn.headerlink:before,
+.rst-content h4 .btn.headerlink:before,
+.rst-content h5 .btn.headerlink:before,
+.rst-content h6 .btn.headerlink:before,
+.rst-content p .btn.headerlink:before,
+.rst-content table>caption .btn.headerlink:before,
+.rst-content tt.download span.btn:first-child:before,
+.wy-menu-vertical li button.btn.toctree-expand:before {
+    opacity: .5;
+    -webkit-transition: opacity .05s ease-in;
+    -moz-transition: opacity .05s ease-in;
+    transition: opacity .05s ease-in
+}
+
+.btn.fa:hover:before,
+.btn.icon:hover:before,
+.rst-content .btn.admonition-title:hover:before,
+.rst-content .code-block-caption .btn.headerlink:hover:before,
+.rst-content .eqno .btn.headerlink:hover:before,
+.rst-content code.download span.btn:first-child:hover:before,
+.rst-content dl dt .btn.headerlink:hover:before,
+.rst-content h1 .btn.headerlink:hover:before,
+.rst-content h2 .btn.headerlink:hover:before,
+.rst-content h3 .btn.headerlink:hover:before,
+.rst-content h4 .btn.headerlink:hover:before,
+.rst-content h5 .btn.headerlink:hover:before,
+.rst-content h6 .btn.headerlink:hover:before,
+.rst-content p .btn.headerlink:hover:before,
+.rst-content table>caption .btn.headerlink:hover:before,
+.rst-content tt.download span.btn:first-child:hover:before,
+.wy-menu-vertical li button.btn.toctree-expand:hover:before {
+    opacity: 1
+}
+
+.btn-mini .fa:before,
+.btn-mini .icon:before,
+.btn-mini .rst-content .admonition-title:before,
+.btn-mini .rst-content .code-block-caption .headerlink:before,
+.btn-mini .rst-content .eqno .headerlink:before,
+.btn-mini .rst-content code.download span:first-child:before,
+.btn-mini .rst-content dl dt .headerlink:before,
+.btn-mini .rst-content h1 .headerlink:before,
+.btn-mini .rst-content h2 .headerlink:before,
+.btn-mini .rst-content h3 .headerlink:before,
+.btn-mini .rst-content h4 .headerlink:before,
+.btn-mini .rst-content h5 .headerlink:before,
+.btn-mini .rst-content h6 .headerlink:before,
+.btn-mini .rst-content p .headerlink:before,
+.btn-mini .rst-content table>caption .headerlink:before,
+.btn-mini .rst-content tt.download span:first-child:before,
+.btn-mini .wy-menu-vertical li button.toctree-expand:before,
+.rst-content .btn-mini .admonition-title:before,
+.rst-content .code-block-caption .btn-mini .headerlink:before,
+.rst-content .eqno .btn-mini .headerlink:before,
+.rst-content code.download .btn-mini span:first-child:before,
+.rst-content dl dt .btn-mini .headerlink:before,
+.rst-content h1 .btn-mini .headerlink:before,
+.rst-content h2 .btn-mini .headerlink:before,
+.rst-content h3 .btn-mini .headerlink:before,
+.rst-content h4 .btn-mini .headerlink:before,
+.rst-content h5 .btn-mini .headerlink:before,
+.rst-content h6 .btn-mini .headerlink:before,
+.rst-content p .btn-mini .headerlink:before,
+.rst-content table>caption .btn-mini .headerlink:before,
+.rst-content tt.download .btn-mini span:first-child:before,
+.wy-menu-vertical li .btn-mini button.toctree-expand:before {
+    font-size: 14px;
+    vertical-align: -15%
+}
+
+.rst-content .admonition,
+.rst-content .admonition-todo,
+.rst-content .attention,
+.rst-content .caution,
+.rst-content .danger,
+.rst-content .error,
+.rst-content .hint,
+.rst-content .important,
+.rst-content .note,
+.rst-content .seealso,
+.rst-content .tip,
+.rst-content .warning,
+.wy-alert {
+    padding: 12px;
+    line-height: 24px;
+    margin-bottom: 24px;
+    background: #e7f2fa
+}
+
+.rst-content .admonition-title,
+.wy-alert-title {
+    font-weight: 700;
+    display: block;
+    color: #fff;
+    background: #6ab0de;
+    padding: 6px 12px;
+    margin: -12px -12px 12px
+}
+
+.rst-content .danger,
+.rst-content .error,
+.rst-content .wy-alert-danger.admonition,
+.rst-content .wy-alert-danger.admonition-todo,
+.rst-content .wy-alert-danger.attention,
+.rst-content .wy-alert-danger.caution,
+.rst-content .wy-alert-danger.hint,
+.rst-content .wy-alert-danger.important,
+.rst-content .wy-alert-danger.note,
+.rst-content .wy-alert-danger.seealso,
+.rst-content .wy-alert-danger.tip,
+.rst-content .wy-alert-danger.warning,
+.wy-alert.wy-alert-danger {
+    background: #fdf3f2
+}
+
+.rst-content .danger .admonition-title,
+.rst-content .danger .wy-alert-title,
+.rst-content .error .admonition-title,
+.rst-content .error .wy-alert-title,
+.rst-content .wy-alert-danger.admonition-todo .admonition-title,
+.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,
+.rst-content .wy-alert-danger.admonition .admonition-title,
+.rst-content .wy-alert-danger.admonition .wy-alert-title,
+.rst-content .wy-alert-danger.attention .admonition-title,
+.rst-content .wy-alert-danger.attention .wy-alert-title,
+.rst-content .wy-alert-danger.caution .admonition-title,
+.rst-content .wy-alert-danger.caution .wy-alert-title,
+.rst-content .wy-alert-danger.hint .admonition-title,
+.rst-content .wy-alert-danger.hint .wy-alert-title,
+.rst-content .wy-alert-danger.important .admonition-title,
+.rst-content .wy-alert-danger.important .wy-alert-title,
+.rst-content .wy-alert-danger.note .admonition-title,
+.rst-content .wy-alert-danger.note .wy-alert-title,
+.rst-content .wy-alert-danger.seealso .admonition-title,
+.rst-content .wy-alert-danger.seealso .wy-alert-title,
+.rst-content .wy-alert-danger.tip .admonition-title,
+.rst-content .wy-alert-danger.tip .wy-alert-title,
+.rst-content .wy-alert-danger.warning .admonition-title,
+.rst-content .wy-alert-danger.warning .wy-alert-title,
+.rst-content .wy-alert.wy-alert-danger .admonition-title,
+.wy-alert.wy-alert-danger .rst-content .admonition-title,
+.wy-alert.wy-alert-danger .wy-alert-title {
+    background: #f29f97
+}
+
+.rst-content .admonition-todo,
+.rst-content .attention,
+.rst-content .caution,
+.rst-content .warning,
+.rst-content .wy-alert-warning.admonition,
+.rst-content .wy-alert-warning.danger,
+.rst-content .wy-alert-warning.error,
+.rst-content .wy-alert-warning.hint,
+.rst-content .wy-alert-warning.important,
+.rst-content .wy-alert-warning.note,
+.rst-content .wy-alert-warning.seealso,
+.rst-content .wy-alert-warning.tip,
+.wy-alert.wy-alert-warning {
+    background: #ffedcc
+}
+
+.rst-content .admonition-todo .admonition-title,
+.rst-content .admonition-todo .wy-alert-title,
+.rst-content .attention .admonition-title,
+.rst-content .attention .wy-alert-title,
+.rst-content .caution .admonition-title,
+.rst-content .caution .wy-alert-title,
+.rst-content .warning .admonition-title,
+.rst-content .warning .wy-alert-title,
+.rst-content .wy-alert-warning.admonition .admonition-title,
+.rst-content .wy-alert-warning.admonition .wy-alert-title,
+.rst-content .wy-alert-warning.danger .admonition-title,
+.rst-content .wy-alert-warning.danger .wy-alert-title,
+.rst-content .wy-alert-warning.error .admonition-title,
+.rst-content .wy-alert-warning.error .wy-alert-title,
+.rst-content .wy-alert-warning.hint .admonition-title,
+.rst-content .wy-alert-warning.hint .wy-alert-title,
+.rst-content .wy-alert-warning.important .admonition-title,
+.rst-content .wy-alert-warning.important .wy-alert-title,
+.rst-content .wy-alert-warning.note .admonition-title,
+.rst-content .wy-alert-warning.note .wy-alert-title,
+.rst-content .wy-alert-warning.seealso .admonition-title,
+.rst-content .wy-alert-warning.seealso .wy-alert-title,
+.rst-content .wy-alert-warning.tip .admonition-title,
+.rst-content .wy-alert-warning.tip .wy-alert-title,
+.rst-content .wy-alert.wy-alert-warning .admonition-title,
+.wy-alert.wy-alert-warning .rst-content .admonition-title,
+.wy-alert.wy-alert-warning .wy-alert-title {
+    background: #f0b37e
+}
+
+.rst-content .note,
+.rst-content .seealso,
+.rst-content .wy-alert-info.admonition,
+.rst-content .wy-alert-info.admonition-todo,
+.rst-content .wy-alert-info.attention,
+.rst-content .wy-alert-info.caution,
+.rst-content .wy-alert-info.danger,
+.rst-content .wy-alert-info.error,
+.rst-content .wy-alert-info.hint,
+.rst-content .wy-alert-info.important,
+.rst-content .wy-alert-info.tip,
+.rst-content .wy-alert-info.warning,
+.wy-alert.wy-alert-info {
+    background: #e7f2fa
+}
+
+.rst-content .note .admonition-title,
+.rst-content .note .wy-alert-title,
+.rst-content .seealso .admonition-title,
+.rst-content .seealso .wy-alert-title,
+.rst-content .wy-alert-info.admonition-todo .admonition-title,
+.rst-content .wy-alert-info.admonition-todo .wy-alert-title,
+.rst-content .wy-alert-info.admonition .admonition-title,
+.rst-content .wy-alert-info.admonition .wy-alert-title,
+.rst-content .wy-alert-info.attention .admonition-title,
+.rst-content .wy-alert-info.attention .wy-alert-title,
+.rst-content .wy-alert-info.caution .admonition-title,
+.rst-content .wy-alert-info.caution .wy-alert-title,
+.rst-content .wy-alert-info.danger .admonition-title,
+.rst-content .wy-alert-info.danger .wy-alert-title,
+.rst-content .wy-alert-info.error .admonition-title,
+.rst-content .wy-alert-info.error .wy-alert-title,
+.rst-content .wy-alert-info.hint .admonition-title,
+.rst-content .wy-alert-info.hint .wy-alert-title,
+.rst-content .wy-alert-info.important .admonition-title,
+.rst-content .wy-alert-info.important .wy-alert-title,
+.rst-content .wy-alert-info.tip .admonition-title,
+.rst-content .wy-alert-info.tip .wy-alert-title,
+.rst-content .wy-alert-info.warning .admonition-title,
+.rst-content .wy-alert-info.warning .wy-alert-title,
+.rst-content .wy-alert.wy-alert-info .admonition-title,
+.wy-alert.wy-alert-info .rst-content .admonition-title,
+.wy-alert.wy-alert-info .wy-alert-title {
+    background: #6ab0de
+}
+
+.rst-content .hint,
+.rst-content .important,
+.rst-content .tip,
+.rst-content .wy-alert-success.admonition,
+.rst-content .wy-alert-success.admonition-todo,
+.rst-content .wy-alert-success.attention,
+.rst-content .wy-alert-success.caution,
+.rst-content .wy-alert-success.danger,
+.rst-content .wy-alert-success.error,
+.rst-content .wy-alert-success.note,
+.rst-content .wy-alert-success.seealso,
+.rst-content .wy-alert-success.warning,
+.wy-alert.wy-alert-success {
+    background: #dbfaf4
+}
+
+.rst-content .hint .admonition-title,
+.rst-content .hint .wy-alert-title,
+.rst-content .important .admonition-title,
+.rst-content .important .wy-alert-title,
+.rst-content .tip .admonition-title,
+.rst-content .tip .wy-alert-title,
+.rst-content .wy-alert-success.admonition-todo .admonition-title,
+.rst-content .wy-alert-success.admonition-todo .wy-alert-title,
+.rst-content .wy-alert-success.admonition .admonition-title,
+.rst-content .wy-alert-success.admonition .wy-alert-title,
+.rst-content .wy-alert-success.attention .admonition-title,
+.rst-content .wy-alert-success.attention .wy-alert-title,
+.rst-content .wy-alert-success.caution .admonition-title,
+.rst-content .wy-alert-success.caution .wy-alert-title,
+.rst-content .wy-alert-success.danger .admonition-title,
+.rst-content .wy-alert-success.danger .wy-alert-title,
+.rst-content .wy-alert-success.error .admonition-title,
+.rst-content .wy-alert-success.error .wy-alert-title,
+.rst-content .wy-alert-success.note .admonition-title,
+.rst-content .wy-alert-success.note .wy-alert-title,
+.rst-content .wy-alert-success.seealso .admonition-title,
+.rst-content .wy-alert-success.seealso .wy-alert-title,
+.rst-content .wy-alert-success.warning .admonition-title,
+.rst-content .wy-alert-success.warning .wy-alert-title,
+.rst-content .wy-alert.wy-alert-success .admonition-title,
+.wy-alert.wy-alert-success .rst-content .admonition-title,
+.wy-alert.wy-alert-success .wy-alert-title {
+    background: #1abc9c
+}
+
+.rst-content .wy-alert-neutral.admonition,
+.rst-content .wy-alert-neutral.admonition-todo,
+.rst-content .wy-alert-neutral.attention,
+.rst-content .wy-alert-neutral.caution,
+.rst-content .wy-alert-neutral.danger,
+.rst-content .wy-alert-neutral.error,
+.rst-content .wy-alert-neutral.hint,
+.rst-content .wy-alert-neutral.important,
+.rst-content .wy-alert-neutral.note,
+.rst-content .wy-alert-neutral.seealso,
+.rst-content .wy-alert-neutral.tip,
+.rst-content .wy-alert-neutral.warning,
+.wy-alert.wy-alert-neutral {
+    background: #f3f6f6
+}
+
+.rst-content .wy-alert-neutral.admonition-todo .admonition-title,
+.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,
+.rst-content .wy-alert-neutral.admonition .admonition-title,
+.rst-content .wy-alert-neutral.admonition .wy-alert-title,
+.rst-content .wy-alert-neutral.attention .admonition-title,
+.rst-content .wy-alert-neutral.attention .wy-alert-title,
+.rst-content .wy-alert-neutral.caution .admonition-title,
+.rst-content .wy-alert-neutral.caution .wy-alert-title,
+.rst-content .wy-alert-neutral.danger .admonition-title,
+.rst-content .wy-alert-neutral.danger .wy-alert-title,
+.rst-content .wy-alert-neutral.error .admonition-title,
+.rst-content .wy-alert-neutral.error .wy-alert-title,
+.rst-content .wy-alert-neutral.hint .admonition-title,
+.rst-content .wy-alert-neutral.hint .wy-alert-title,
+.rst-content .wy-alert-neutral.important .admonition-title,
+.rst-content .wy-alert-neutral.important .wy-alert-title,
+.rst-content .wy-alert-neutral.note .admonition-title,
+.rst-content .wy-alert-neutral.note .wy-alert-title,
+.rst-content .wy-alert-neutral.seealso .admonition-title,
+.rst-content .wy-alert-neutral.seealso .wy-alert-title,
+.rst-content .wy-alert-neutral.tip .admonition-title,
+.rst-content .wy-alert-neutral.tip .wy-alert-title,
+.rst-content .wy-alert-neutral.warning .admonition-title,
+.rst-content .wy-alert-neutral.warning .wy-alert-title,
+.rst-content .wy-alert.wy-alert-neutral .admonition-title,
+.wy-alert.wy-alert-neutral .rst-content .admonition-title,
+.wy-alert.wy-alert-neutral .wy-alert-title {
+    color: #404040;
+    background: #e1e4e5
+}
+
+.rst-content .wy-alert-neutral.admonition-todo a,
+.rst-content .wy-alert-neutral.admonition a,
+.rst-content .wy-alert-neutral.attention a,
+.rst-content .wy-alert-neutral.caution a,
+.rst-content .wy-alert-neutral.danger a,
+.rst-content .wy-alert-neutral.error a,
+.rst-content .wy-alert-neutral.hint a,
+.rst-content .wy-alert-neutral.important a,
+.rst-content .wy-alert-neutral.note a,
+.rst-content .wy-alert-neutral.seealso a,
+.rst-content .wy-alert-neutral.tip a,
+.rst-content .wy-alert-neutral.warning a,
+.wy-alert.wy-alert-neutral a {
+    color: #2980b9
+}
+
+.rst-content .admonition-todo p:last-child,
+.rst-content .admonition p:last-child,
+.rst-content .attention p:last-child,
+.rst-content .caution p:last-child,
+.rst-content .danger p:last-child,
+.rst-content .error p:last-child,
+.rst-content .hint p:last-child,
+.rst-content .important p:last-child,
+.rst-content .note p:last-child,
+.rst-content .seealso p:last-child,
+.rst-content .tip p:last-child,
+.rst-content .warning p:last-child,
+.wy-alert p:last-child {
+    margin-bottom: 0
+}
+
+.wy-tray-container {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    z-index: 600
+}
+
+.wy-tray-container li {
+    display: block;
+    width: 300px;
+    background: transparent;
+    color: #fff;
+    text-align: center;
+    box-shadow: 0 5px 5px 0 rgba(0, 0, 0, .1);
+    padding: 0 24px;
+    min-width: 20%;
+    opacity: 0;
+    height: 0;
+    line-height: 56px;
+    overflow: hidden;
+    -webkit-transition: all .3s ease-in;
+    -moz-transition: all .3s ease-in;
+    transition: all .3s ease-in
+}
+
+.wy-tray-container li.wy-tray-item-success {
+    background: #27ae60
+}
+
+.wy-tray-container li.wy-tray-item-info {
+    background: #2980b9
+}
+
+.wy-tray-container li.wy-tray-item-warning {
+    background: #e67e22
+}
+
+.wy-tray-container li.wy-tray-item-danger {
+    background: #e74c3c
+}
+
+.wy-tray-container li.on {
+    opacity: 1;
+    height: 56px
+}
+
+@media screen and (max-width:768px) {
+    .wy-tray-container {
+        bottom: auto;
+        top: 0;
+        width: 100%
+    }
+    .wy-tray-container li {
+        width: 100%
+    }
+}
+
+button {
+    font-size: 100%;
+    margin: 0;
+    vertical-align: baseline;
+    *vertical-align: middle;
+    cursor: pointer;
+    line-height: normal;
+    -webkit-appearance: button;
+    *overflow: visible
+}
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+    border: 0;
+    padding: 0
+}
+
+button[disabled] {
+    cursor: default
+}
+
+.btn {
+    display: inline-block;
+    border-radius: 2px;
+    line-height: normal;
+    white-space: nowrap;
+    text-align: center;
+    cursor: pointer;
+    font-size: 100%;
+    padding: 6px 12px 8px;
+    color: #fff;
+    border: 1px solid rgba(0, 0, 0, .1);
+    background-color: #27ae60;
+    text-decoration: none;
+    font-weight: 400;
+    font-family: Lato, proxima-nova, Helvetica Neue, Arial, sans-serif;
+    box-shadow: inset 0 1px 2px -1px hsla(0, 0%, 100%, .5), inset 0 -2px 0 0 rgba(0, 0, 0, .1);
+    outline-none: false;
+    vertical-align: middle;
+    *display: inline;
+    zoom: 1;
+    -webkit-user-drag: none;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+    -webkit-transition: all .1s linear;
+    -moz-transition: all .1s linear;
+    transition: all .1s linear
+}
+
+.btn-hover {
+    background: #2e8ece;
+    color: #fff
+}
+
+.btn:hover {
+    background: #2cc36b;
+    color: #fff
+}
+
+.btn:focus {
+    background: #2cc36b;
+    outline: 0
+}
+
+.btn:active {
+    box-shadow: inset 0 -1px 0 0 rgba(0, 0, 0, .05), inset 0 2px 0 0 rgba(0, 0, 0, .1);
+    padding: 8px 12px 6px
+}
+
+.btn:visited {
+    color: #fff
+}
+
+.btn-disabled,
+.btn-disabled:active,
+.btn-disabled:focus,
+.btn-disabled:hover,
+.btn:disabled {
+    background-image: none;
+    filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+    filter: alpha(opacity=40);
+    opacity: .4;
+    cursor: not-allowed;
+    box-shadow: none
+}
+
+.btn::-moz-focus-inner {
+    padding: 0;
+    border: 0
+}
+
+.btn-small {
+    font-size: 80%
+}
+
+.btn-info {
+    background-color: #2980b9 !important
+}
+
+.btn-info:hover {
+    background-color: #2e8ece !important
+}
+
+.btn-neutral {
+    background-color: #f3f6f6 !important;
+    color: #404040 !important
+}
+
+.btn-neutral:hover {
+    background-color: #e5ebeb !important;
+    color: #404040
+}
+
+.btn-neutral:visited {
+    color: #404040 !important
+}
+
+.btn-success {
+    background-color: #27ae60 !important
+}
+
+.btn-success:hover {
+    background-color: #295 !important
+}
+
+.btn-danger {
+    background-color: #e74c3c !important
+}
+
+.btn-danger:hover {
+    background-color: #ea6153 !important
+}
+
+.btn-warning {
+    background-color: #e67e22 !important
+}
+
+.btn-warning:hover {
+    background-color: #e98b39 !important
+}
+
+.btn-invert {
+    background-color: #222
+}
+
+.btn-invert:hover {
+    background-color: #2f2f2f !important
+}
+
+.btn-link {
+    background-color: transparent !important;
+    color: #2980b9;
+    box-shadow: none;
+    border-color: transparent !important
+}
+
+.btn-link:active,
+.btn-link:hover {
+    background-color: transparent !important;
+    color: #409ad5 !important;
+    box-shadow: none
+}
+
+.btn-link:visited {
+    color: #9b59b6
+}
+
+.wy-btn-group .btn,
+.wy-control .btn {
+    vertical-align: middle
+}
+
+.wy-btn-group {
+    margin-bottom: 24px;
+    *zoom: 1
+}
+
+.wy-btn-group:after,
+.wy-btn-group:before {
+    display: table;
+    content: ""
+}
+
+.wy-btn-group:after {
+    clear: both
+}
+
+.wy-dropdown {
+    position: relative;
+    display: inline-block
+}
+
+.wy-dropdown-active .wy-dropdown-menu {
+    display: block
+}
+
+.wy-dropdown-menu {
+    position: absolute;
+    left: 0;
+    display: none;
+    float: left;
+    top: 100%;
+    min-width: 100%;
+    background: #fcfcfc;
+    z-index: 100;
+    border: 1px solid #cfd7dd;
+    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .1);
+    padding: 12px
+}
+
+.wy-dropdown-menu>dd>a {
+    display: block;
+    clear: both;
+    color: #404040;
+    white-space: nowrap;
+    font-size: 90%;
+    padding: 0 12px;
+    cursor: pointer
+}
+
+.wy-dropdown-menu>dd>a:hover {
+    background: #2980b9;
+    color: #fff
+}
+
+.wy-dropdown-menu>dd.divider {
+    border-top: 1px solid #cfd7dd;
+    margin: 6px 0
+}
+
+.wy-dropdown-menu>dd.search {
+    padding-bottom: 12px
+}
+
+.wy-dropdown-menu>dd.search input[type=search] {
+    width: 100%
+}
+
+.wy-dropdown-menu>dd.call-to-action {
+    background: #e3e3e3;
+    text-transform: uppercase;
+    font-weight: 500;
+    font-size: 80%
+}
+
+.wy-dropdown-menu>dd.call-to-action:hover {
+    background: #e3e3e3
+}
+
+.wy-dropdown-menu>dd.call-to-action .btn {
+    color: #fff
+}
+
+.wy-dropdown.wy-dropdown-up .wy-dropdown-menu {
+    bottom: 100%;
+    top: auto;
+    left: auto;
+    right: 0
+}
+
+.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu {
+    background: #fcfcfc;
+    margin-top: 2px
+}
+
+.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a {
+    padding: 6px 12px
+}
+
+.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover {
+    background: #2980b9;
+    color: #fff
+}
+
+.wy-dropdown.wy-dropdown-left .wy-dropdown-menu {
+    right: 0;
+    left: auto;
+    text-align: right
+}
+
+.wy-dropdown-arrow:before {
+    content: " ";
+    border-bottom: 5px solid #f5f5f5;
+    border-left: 5px solid transparent;
+    border-right: 5px solid transparent;
+    position: absolute;
+    display: block;
+    top: -4px;
+    left: 50%;
+    margin-left: -3px
+}
+
+.wy-dropdown-arrow.wy-dropdown-arrow-left:before {
+    left: 11px
+}
+
+.wy-form-stacked select {
+    display: block
+}
+
+.wy-form-aligned .wy-help-inline,
+.wy-form-aligned input,
+.wy-form-aligned label,
+.wy-form-aligned select,
+.wy-form-aligned textarea {
+    display: inline-block;
+    *display: inline;
+    *zoom: 1;
+    vertical-align: middle
+}
+
+.wy-form-aligned .wy-control-group>label {
+    display: inline-block;
+    vertical-align: middle;
+    width: 10em;
+    margin: 6px 12px 0 0;
+    float: left
+}
+
+.wy-form-aligned .wy-control {
+    float: left
+}
+
+.wy-form-aligned .wy-control label {
+    display: block
+}
+
+.wy-form-aligned .wy-control select {
+    margin-top: 6px
+}
+
+.wy-control-group {
+    margin-bottom: 24px;
+    max-width: 1200px;
+    margin-left: auto;
+    margin-right: auto;
+    *zoom: 1
+}
+
+.wy-control-group:after,
+.wy-control-group:before {
+    display: table;
+    content: ""
+}
+
+.wy-control-group:after {
+    clear: both
+}
+
+.wy-control-group.wy-control-group-required>label:after {
+    content: " *";
+    color: #e74c3c
+}
+
+.wy-control-group .wy-form-full,
+.wy-control-group .wy-form-halves,
+.wy-control-group .wy-form-thirds {
+    padding-bottom: 12px
+}
+
+.wy-control-group .wy-form-full input[type=color],
+.wy-control-group .wy-form-full input[type=date],
+.wy-control-group .wy-form-full input[type=datetime-local],
+.wy-control-group .wy-form-full input[type=datetime],
+.wy-control-group .wy-form-full input[type=email],
+.wy-control-group .wy-form-full input[type=month],
+.wy-control-group .wy-form-full input[type=number],
+.wy-control-group .wy-form-full input[type=password],
+.wy-control-group .wy-form-full input[type=search],
+.wy-control-group .wy-form-full input[type=tel],
+.wy-control-group .wy-form-full input[type=text],
+.wy-control-group .wy-form-full input[type=time],
+.wy-control-group .wy-form-full input[type=url],
+.wy-control-group .wy-form-full input[type=week],
+.wy-control-group .wy-form-full select,
+.wy-control-group .wy-form-halves input[type=color],
+.wy-control-group .wy-form-halves input[type=date],
+.wy-control-group .wy-form-halves input[type=datetime-local],
+.wy-control-group .wy-form-halves input[type=datetime],
+.wy-control-group .wy-form-halves input[type=email],
+.wy-control-group .wy-form-halves input[type=month],
+.wy-control-group .wy-form-halves input[type=number],
+.wy-control-group .wy-form-halves input[type=password],
+.wy-control-group .wy-form-halves input[type=search],
+.wy-control-group .wy-form-halves input[type=tel],
+.wy-control-group .wy-form-halves input[type=text],
+.wy-control-group .wy-form-halves input[type=time],
+.wy-control-group .wy-form-halves input[type=url],
+.wy-control-group .wy-form-halves input[type=week],
+.wy-control-group .wy-form-halves select,
+.wy-control-group .wy-form-thirds input[type=color],
+.wy-control-group .wy-form-thirds input[type=date],
+.wy-control-group .wy-form-thirds input[type=datetime-local],
+.wy-control-group .wy-form-thirds input[type=datetime],
+.wy-control-group .wy-form-thirds input[type=email],
+.wy-control-group .wy-form-thirds input[type=month],
+.wy-control-group .wy-form-thirds input[type=number],
+.wy-control-group .wy-form-thirds input[type=password],
+.wy-control-group .wy-form-thirds input[type=search],
+.wy-control-group .wy-form-thirds input[type=tel],
+.wy-control-group .wy-form-thirds input[type=text],
+.wy-control-group .wy-form-thirds input[type=time],
+.wy-control-group .wy-form-thirds input[type=url],
+.wy-control-group .wy-form-thirds input[type=week],
+.wy-control-group .wy-form-thirds select {
+    width: 100%
+}
+
+.wy-control-group .wy-form-full {
+    float: left;
+    display: block;
+    width: 100%;
+    margin-right: 0
+}
+
+.wy-control-group .wy-form-full:last-child {
+    margin-right: 0
+}
+
+.wy-control-group .wy-form-halves {
+    float: left;
+    display: block;
+    margin-right: 2.35765%;
+    width: 48.82117%
+}
+
+.wy-control-group .wy-form-halves:last-child,
+.wy-control-group .wy-form-halves:nth-of-type(2n) {
+    margin-right: 0
+}
+
+.wy-control-group .wy-form-halves:nth-of-type(odd) {
+    clear: left
+}
+
+.wy-control-group .wy-form-thirds {
+    float: left;
+    display: block;
+    margin-right: 2.35765%;
+    width: 31.76157%
+}
+
+.wy-control-group .wy-form-thirds:last-child,
+.wy-control-group .wy-form-thirds:nth-of-type(3n) {
+    margin-right: 0
+}
+
+.wy-control-group .wy-form-thirds:nth-of-type(3n+1) {
+    clear: left
+}
+
+.wy-control-group.wy-control-group-no-input .wy-control,
+.wy-control-no-input {
+    margin: 6px 0 0;
+    font-size: 90%
+}
+
+.wy-control-no-input {
+    display: inline-block
+}
+
+.wy-control-group.fluid-input input[type=color],
+.wy-control-group.fluid-input input[type=date],
+.wy-control-group.fluid-input input[type=datetime-local],
+.wy-control-group.fluid-input input[type=datetime],
+.wy-control-group.fluid-input input[type=email],
+.wy-control-group.fluid-input input[type=month],
+.wy-control-group.fluid-input input[type=number],
+.wy-control-group.fluid-input input[type=password],
+.wy-control-group.fluid-input input[type=search],
+.wy-control-group.fluid-input input[type=tel],
+.wy-control-group.fluid-input input[type=text],
+.wy-control-group.fluid-input input[type=time],
+.wy-control-group.fluid-input input[type=url],
+.wy-control-group.fluid-input input[type=week] {
+    width: 100%
+}
+
+.wy-form-message-inline {
+    padding-left: .3em;
+    color: #666;
+    font-size: 90%
+}
+
+.wy-form-message {
+    display: block;
+    color: #999;
+    font-size: 70%;
+    margin-top: .3125em;
+    font-style: italic
+}
+
+.wy-form-message p {
+    font-size: inherit;
+    font-style: italic;
+    margin-bottom: 6px
+}
+
+.wy-form-message p:last-child {
+    margin-bottom: 0
+}
+
+.wy-checkbox,
+.wy-radio {
+    margin: 6px 0;
+    color: #404040;
+    display: block
+}
+
+.wy-checkbox input,
+.wy-radio input {
+    vertical-align: baseline
+}
+
+.wy-form-message-inline {
+    display: inline-block;
+    *display: inline;
+    *zoom: 1;
+    vertical-align: middle
+}
+
+.wy-input-prefix,
+.wy-input-suffix {
+    white-space: nowrap;
+    padding: 6px
+}
+
+.wy-input-prefix .wy-input-context,
+.wy-input-suffix .wy-input-context {
+    line-height: 27px;
+    padding: 0 8px;
+    display: inline-block;
+    font-size: 80%;
+    background-color: #f3f6f6;
+    border: 1px solid #ccc;
+    color: #999
+}
+
+.wy-input-suffix .wy-input-context {
+    border-left: 0
+}
+
+.wy-input-prefix .wy-input-context {
+    border-right: 0
+}
+
+.wy-switch {
+    position: relative;
+    display: block;
+    height: 24px;
+    margin-top: 12px;
+    cursor: pointer
+}
+
+.wy-switch:before {
+    left: 0;
+    top: 0;
+    width: 36px;
+    height: 12px;
+    background: #ccc
+}
+
+.wy-switch:after,
+.wy-switch:before {
+    position: absolute;
+    content: "";
+    display: block;
+    border-radius: 4px;
+    -webkit-transition: all .2s ease-in-out;
+    -moz-transition: all .2s ease-in-out;
+    transition: all .2s ease-in-out
+}
+
+.wy-switch:after {
+    width: 18px;
+    height: 18px;
+    background: #999;
+    left: -3px;
+    top: -3px
+}
+
+.wy-switch span {
+    position: absolute;
+    left: 48px;
+    display: block;
+    font-size: 12px;
+    color: #ccc;
+    line-height: 1
+}
+
+.wy-switch.active:before {
+    background: #1e8449
+}
+
+.wy-switch.active:after {
+    left: 24px;
+    background: #27ae60
+}
+
+.wy-switch.disabled {
+    cursor: not-allowed;
+    opacity: .8
+}
+
+.wy-control-group.wy-control-group-error .wy-form-message,
+.wy-control-group.wy-control-group-error>label {
+    color: #e74c3c
+}
+
+.wy-control-group.wy-control-group-error input[type=color],
+.wy-control-group.wy-control-group-error input[type=date],
+.wy-control-group.wy-control-group-error input[type=datetime-local],
+.wy-control-group.wy-control-group-error input[type=datetime],
+.wy-control-group.wy-control-group-error input[type=email],
+.wy-control-group.wy-control-group-error input[type=month],
+.wy-control-group.wy-control-group-error input[type=number],
+.wy-control-group.wy-control-group-error input[type=password],
+.wy-control-group.wy-control-group-error input[type=search],
+.wy-control-group.wy-control-group-error input[type=tel],
+.wy-control-group.wy-control-group-error input[type=text],
+.wy-control-group.wy-control-group-error input[type=time],
+.wy-control-group.wy-control-group-error input[type=url],
+.wy-control-group.wy-control-group-error input[type=week],
+.wy-control-group.wy-control-group-error textarea {
+    border: 1px solid #e74c3c
+}
+
+.wy-inline-validate {
+    white-space: nowrap
+}
+
+.wy-inline-validate .wy-input-context {
+    padding: .5em .625em;
+    display: inline-block;
+    font-size: 80%
+}
+
+.wy-inline-validate.wy-inline-validate-success .wy-input-context {
+    color: #27ae60
+}
+
+.wy-inline-validate.wy-inline-validate-danger .wy-input-context {
+    color: #e74c3c
+}
+
+.wy-inline-validate.wy-inline-validate-warning .wy-input-context {
+    color: #e67e22
+}
+
+.wy-inline-validate.wy-inline-validate-info .wy-input-context {
+    color: #2980b9
+}
+
+.rotate-90 {
+    -webkit-transform: rotate(90deg);
+    -moz-transform: rotate(90deg);
+    -ms-transform: rotate(90deg);
+    -o-transform: rotate(90deg);
+    transform: rotate(90deg)
+}
+
+.rotate-180 {
+    -webkit-transform: rotate(180deg);
+    -moz-transform: rotate(180deg);
+    -ms-transform: rotate(180deg);
+    -o-transform: rotate(180deg);
+    transform: rotate(180deg)
+}
+
+.rotate-270 {
+    -webkit-transform: rotate(270deg);
+    -moz-transform: rotate(270deg);
+    -ms-transform: rotate(270deg);
+    -o-transform: rotate(270deg);
+    transform: rotate(270deg)
+}
+
+.mirror {
+    -webkit-transform: scaleX(-1);
+    -moz-transform: scaleX(-1);
+    -ms-transform: scaleX(-1);
+    -o-transform: scaleX(-1);
+    transform: scaleX(-1)
+}
+
+.mirror.rotate-90 {
+    -webkit-transform: scaleX(-1) rotate(90deg);
+    -moz-transform: scaleX(-1) rotate(90deg);
+    -ms-transform: scaleX(-1) rotate(90deg);
+    -o-transform: scaleX(-1) rotate(90deg);
+    transform: scaleX(-1) rotate(90deg)
+}
+
+.mirror.rotate-180 {
+    -webkit-transform: scaleX(-1) rotate(180deg);
+    -moz-transform: scaleX(-1) rotate(180deg);
+    -ms-transform: scaleX(-1) rotate(180deg);
+    -o-transform: scaleX(-1) rotate(180deg);
+    transform: scaleX(-1) rotate(180deg)
+}
+
+.mirror.rotate-270 {
+    -webkit-transform: scaleX(-1) rotate(270deg);
+    -moz-transform: scaleX(-1) rotate(270deg);
+    -ms-transform: scaleX(-1) rotate(270deg);
+    -o-transform: scaleX(-1) rotate(270deg);
+    transform: scaleX(-1) rotate(270deg)
+}
+
+@media only screen and (max-width:480px) {
+    .wy-form button[type=submit] {
+        margin: .7em 0 0
+    }
+    .wy-form input[type=color],
+    .wy-form input[type=date],
+    .wy-form input[type=datetime-local],
+    .wy-form input[type=datetime],
+    .wy-form input[type=email],
+    .wy-form input[type=month],
+    .wy-form input[type=number],
+    .wy-form input[type=password],
+    .wy-form input[type=search],
+    .wy-form input[type=tel],
+    .wy-form input[type=text],
+    .wy-form input[type=time],
+    .wy-form input[type=url],
+    .wy-form input[type=week],
+    .wy-form label {
+        margin-bottom: .3em;
+        display: block
+    }
+    .wy-form input[type=color],
+    .wy-form input[type=date],
+    .wy-form input[type=datetime-local],
+    .wy-form input[type=datetime],
+    .wy-form input[type=email],
+    .wy-form input[type=month],
+    .wy-form input[type=number],
+    .wy-form input[type=password],
+    .wy-form input[type=search],
+    .wy-form input[type=tel],
+    .wy-form input[type=time],
+    .wy-form input[type=url],
+    .wy-form input[type=week] {
+        margin-bottom: 0
+    }
+    .wy-form-aligned .wy-control-group label {
+        margin-bottom: .3em;
+        text-align: left;
+        display: block;
+        width: 100%
+    }
+    .wy-form-aligned .wy-control {
+        margin: 1.5em 0 0
+    }
+    .wy-form-message,
+    .wy-form-message-inline,
+    .wy-form .wy-help-inline {
+        display: block;
+        font-size: 80%;
+        padding: 6px 0
+    }
+}
+
+@media screen and (max-width:768px) {
+    .tablet-hide {
+        display: none
+    }
+}
+
+@media screen and (max-width:480px) {
+    .mobile-hide {
+        display: none
+    }
+}
+
+.float-left {
+    float: left
+}
+
+.float-right {
+    float: right
+}
+
+.full-width {
+    width: 100%
+}
+
+.rst-content table.docutils,
+.rst-content table.field-list,
+.wy-table {
+    border-collapse: collapse;
+    border-spacing: 0;
+    empty-cells: show;
+    margin-bottom: 24px
+}
+
+.rst-content table.docutils caption,
+.rst-content table.field-list caption,
+.wy-table caption {
+    color: #000;
+    font: italic 85%/1 arial, sans-serif;
+    padding: 1em 0;
+    text-align: center
+}
+
+.rst-content table.docutils td,
+.rst-content table.docutils th,
+.rst-content table.field-list td,
+.rst-content table.field-list th,
+.wy-table td,
+.wy-table th {
+    font-size: 90%;
+    margin: 0;
+    overflow: visible;
+    padding: 8px 16px
+}
+
+.rst-content table.docutils td:first-child,
+.rst-content table.docutils th:first-child,
+.rst-content table.field-list td:first-child,
+.rst-content table.field-list th:first-child,
+.wy-table td:first-child,
+.wy-table th:first-child {
+    border-left-width: 0
+}
+
+.rst-content table.docutils thead,
+.rst-content table.field-list thead,
+.wy-table thead {
+    color: #000;
+    text-align: left;
+    vertical-align: bottom;
+    white-space: nowrap
+}
+
+.rst-content table.docutils thead th,
+.rst-content table.field-list thead th,
+.wy-table thead th {
+    font-weight: 700;
+    border-bottom: 2px solid #e1e4e5
+}
+
+.rst-content table.docutils td,
+.rst-content table.field-list td,
+.wy-table td {
+    background-color: transparent;
+    vertical-align: middle
+}
+
+.rst-content table.docutils td p,
+.rst-content table.field-list td p,
+.wy-table td p {
+    line-height: 18px
+}
+
+.rst-content table.docutils td p:last-child,
+.rst-content table.field-list td p:last-child,
+.wy-table td p:last-child {
+    margin-bottom: 0
+}
+
+.rst-content table.docutils .wy-table-cell-min,
+.rst-content table.field-list .wy-table-cell-min,
+.wy-table .wy-table-cell-min {
+    width: 1%;
+    padding-right: 0
+}
+
+.rst-content table.docutils .wy-table-cell-min input[type=checkbox],
+.rst-content table.field-list .wy-table-cell-min input[type=checkbox],
+.wy-table .wy-table-cell-min input[type=checkbox] {
+    margin: 0
+}
+
+.wy-table-secondary {
+    color: grey;
+    font-size: 90%
+}
+
+.wy-table-tertiary {
+    color: grey;
+    font-size: 80%
+}
+
+.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,
+.wy-table-backed,
+.wy-table-odd td,
+.wy-table-striped tr:nth-child(2n-1) td {
+    background-color: #f3f6f6
+}
+
+.rst-content table.docutils,
+.wy-table-bordered-all {
+    border: 1px solid #e1e4e5
+}
+
+.rst-content table.docutils td,
+.wy-table-bordered-all td {
+    border-bottom: 1px solid #e1e4e5;
+    border-left: 1px solid #e1e4e5
+}
+
+.rst-content table.docutils tbody>tr:last-child td,
+.wy-table-bordered-all tbody>tr:last-child td {
+    border-bottom-width: 0
+}
+
+.wy-table-bordered {
+    border: 1px solid #e1e4e5
+}
+
+.wy-table-bordered-rows td {
+    border-bottom: 1px solid #e1e4e5
+}
+
+.wy-table-bordered-rows tbody>tr:last-child td {
+    border-bottom-width: 0
+}
+
+.wy-table-horizontal td,
+.wy-table-horizontal th {
+    border-width: 0 0 1px;
+    border-bottom: 1px solid #e1e4e5
+}
+
+.wy-table-horizontal tbody>tr:last-child td {
+    border-bottom-width: 0
+}
+
+.wy-table-responsive {
+    margin-bottom: 24px;
+    max-width: 100%;
+    overflow: auto
+}
+
+.wy-table-responsive table {
+    margin-bottom: 0 !important
+}
+
+.wy-table-responsive table td,
+.wy-table-responsive table th {
+    white-space: nowrap
+}
+
+.wy-text-left {
+    text-align: left
+}
+
+.wy-text-center {
+    text-align: center
+}
+
+.wy-text-right {
+    text-align: right
+}
+
+.wy-text-large {
+    font-size: 120%
+}
+
+.wy-text-normal {
+    font-size: 100%
+}
+
+.wy-text-small,
+small {
+    font-size: 80%
+}
+
+.wy-text-strike {
+    text-decoration: line-through
+}
+
+.wy-text-warning {
+    color: #e67e22 !important
+}
+
+a.wy-text-warning:hover {
+    color: #eb9950 !important
+}
+
+.wy-text-info {
+    color: #2980b9 !important
+}
+
+a.wy-text-info:hover {
+    color: #409ad5 !important
+}
+
+.wy-text-success {
+    color: #27ae60 !important
+}
+
+a.wy-text-success:hover {
+    color: #36d278 !important
+}
+
+.wy-text-danger {
+    color: #e74c3c !important
+}
+
+a.wy-text-danger:hover {
+    color: #ed7669 !important
+}
+
+.wy-text-neutral {
+    color: #404040 !important
+}
+
+a.wy-text-neutral:hover {
+    color: #595959 !important
+}
+
+
+.rst-content code,
+.rst-content tt
+ {
+    white-space: nowrap;
+    max-width: 100%;
+    background: #fff;
+    border: 1px solid #e1e4e5;
+    font-size: 75%;
+    padding: 0 5px;
+    font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, Courier, monospace;
+    color: #e74c3c;
+    overflow-x: auto
+}
+
+.rst-content tt.code-large,
+code.code-large {
+    font-size: 90%
+}
+
+.rst-content .section ul,
+.rst-content .toctree-wrapper ul,
+.rst-content section ul,
+.wy-plain-list-disc,
+article ul {
+    list-style: disc;
+    line-height: 24px;
+    margin-bottom: 24px
+}
+
+.rst-content .section ul li,
+.rst-content .toctree-wrapper ul li,
+.rst-content section ul li,
+.wy-plain-list-disc li,
+article ul li {
+    list-style: disc;
+    margin-left: 24px
+}
+
+.rst-content .section ul li p:last-child,
+.rst-content .section ul li ul,
+.rst-content .toctree-wrapper ul li p:last-child,
+.rst-content .toctree-wrapper ul li ul,
+.rst-content section ul li p:last-child,
+.rst-content section ul li ul,
+.wy-plain-list-disc li p:last-child,
+.wy-plain-list-disc li ul,
+article ul li p:last-child,
+article ul li ul {
+    margin-bottom: 0
+}
+
+.rst-content .section ul li li,
+.rst-content .toctree-wrapper ul li li,
+.rst-content section ul li li,
+.wy-plain-list-disc li li,
+article ul li li {
+    list-style: circle
+}
+
+.rst-content .section ul li li li,
+.rst-content .toctree-wrapper ul li li li,
+.rst-content section ul li li li,
+.wy-plain-list-disc li li li,
+article ul li li li {
+    list-style: square
+}
+
+.rst-content .section ul li ol li,
+.rst-content .toctree-wrapper ul li ol li,
+.rst-content section ul li ol li,
+.wy-plain-list-disc li ol li,
+article ul li ol li {
+    list-style: decimal
+}
+
+.rst-content .section ol,
+.rst-content .section ol.arabic,
+.rst-content .toctree-wrapper ol,
+.rst-content .toctree-wrapper ol.arabic,
+.rst-content section ol,
+.rst-content section ol.arabic,
+.wy-plain-list-decimal,
+article ol {
+    list-style: decimal;
+    line-height: 24px;
+    margin-bottom: 24px
+}
+
+.rst-content .section ol.arabic li,
+.rst-content .section ol li,
+.rst-content .toctree-wrapper ol.arabic li,
+.rst-content .toctree-wrapper ol li,
+.rst-content section ol.arabic li,
+.rst-content section ol li,
+.wy-plain-list-decimal li,
+article ol li {
+    list-style: decimal;
+    margin-left: 24px
+}
+
+.rst-content .section ol.arabic li ul,
+.rst-content .section ol li p:last-child,
+.rst-content .section ol li ul,
+.rst-content .toctree-wrapper ol.arabic li ul,
+.rst-content .toctree-wrapper ol li p:last-child,
+.rst-content .toctree-wrapper ol li ul,
+.rst-content section ol.arabic li ul,
+.rst-content section ol li p:last-child,
+.rst-content section ol li ul,
+.wy-plain-list-decimal li p:last-child,
+.wy-plain-list-decimal li ul,
+article ol li p:last-child,
+article ol li ul {
+    margin-bottom: 0
+}
+
+.rst-content .section ol.arabic li ul li,
+.rst-content .section ol li ul li,
+.rst-content .toctree-wrapper ol.arabic li ul li,
+.rst-content .toctree-wrapper ol li ul li,
+.rst-content section ol.arabic li ul li,
+.rst-content section ol li ul li,
+.wy-plain-list-decimal li ul li,
+article ol li ul li {
+    list-style: disc
+}
+
+.wy-breadcrumbs {
+    *zoom: 1
+}
+
+.wy-breadcrumbs:after,
+.wy-breadcrumbs:before {
+    display: table;
+    content: ""
+}
+
+.wy-breadcrumbs:after {
+    clear: both
+}
+
+.wy-breadcrumbs li {
+    display: inline-block
+}
+
+.wy-breadcrumbs li.wy-breadcrumbs-aside {
+    float: right
+}
+
+.wy-breadcrumbs li a {
+    display: inline-block;
+    padding: 5px
+}
+
+.wy-breadcrumbs li a:first-child {
+    padding-left: 0
+}
+
+.rst-content .wy-breadcrumbs li tt,
+.wy-breadcrumbs li .rst-content tt,
+.wy-breadcrumbs li code {
+    padding: 5px;
+    border: none;
+    background: none
+}
+
+.rst-content .wy-breadcrumbs li tt.literal,
+.wy-breadcrumbs li .rst-content tt.literal,
+.wy-breadcrumbs li code.literal {
+    color: #404040
+}
+
+.wy-breadcrumbs-extra {
+    margin-bottom: 0;
+    color: #b3b3b3;
+    font-size: 80%;
+    display: inline-block
+}
+
+@media screen and (max-width:480px) {
+    .wy-breadcrumbs-extra,
+    .wy-breadcrumbs li.wy-breadcrumbs-aside {
+        display: none
+    }
+}
+
+@media print {
+    .wy-breadcrumbs li.wy-breadcrumbs-aside {
+        display: none
+    }
+}
+
+html {
+    font-size: 16px
+}
+
+.wy-affix {
+    position: fixed;
+    top: 1.618em
+}
+
+.wy-menu a:hover {
+    text-decoration: none
+}
+
+.wy-menu-horiz {
+    *zoom: 1
+}
+
+.wy-menu-horiz:after,
+.wy-menu-horiz:before {
+    display: table;
+    content: ""
+}
+
+.wy-menu-horiz:after {
+    clear: both
+}
+
+.wy-menu-horiz li,
+.wy-menu-horiz ul {
+    display: inline-block
+}
+
+.wy-menu-horiz li:hover {
+    background: hsla(0, 0%, 100%, .1)
+}
+
+.wy-menu-horiz li.divide-left {
+    border-left: 1px solid #404040
+}
+
+.wy-menu-horiz li.divide-right {
+    border-right: 1px solid #404040
+}
+
+.wy-menu-horiz a {
+    height: 32px;
+    display: inline-block;
+    line-height: 32px;
+    padding: 0 16px
+}
+
+.wy-menu-vertical {
+    width: 300px
+}
+
+.wy-menu-vertical header,
+.wy-menu-vertical p.caption {
+    color: #55a5d9;
+    height: 32px;
+    line-height: 32px;
+    padding: 0 1.618em;
+    margin: 12px 0 0;
+    display: block;
+    font-weight: 700;
+    text-transform: uppercase;
+    font-size: 85%;
+    white-space: nowrap
+}
+
+.wy-menu-vertical ul {
+    margin-bottom: 0
+}
+
+.wy-menu-vertical li.divide-top {
+    border-top: 1px solid #404040
+}
+
+.wy-menu-vertical li.divide-bottom {
+    border-bottom: 1px solid #404040
+}
+
+.wy-menu-vertical li.current {
+    background: #e3e3e3
+}
+
+.wy-menu-vertical li.current a {
+    color: grey;
+    border-right: 1px solid #c9c9c9;
+    padding: .4045em 2.427em
+}
+
+.wy-menu-vertical li.current a:hover {
+    background: #d6d6d6
+}
+
+.rst-content .wy-menu-vertical li tt,
+.wy-menu-vertical li .rst-content tt,
+.wy-menu-vertical li code {
+    border: none;
+    background: inherit;
+    color: inherit;
+    padding-left: 0;
+    padding-right: 0
+}
+
+.wy-menu-vertical li button.toctree-expand {
+    display: block;
+    float: left;
+    margin-left: -1.2em;
+    line-height: 18px;
+    color: #4d4d4d;
+    border: none;
+    background: none;
+    padding: 0
+}
+
+.wy-menu-vertical li.current>a,
+.wy-menu-vertical li.on a {
+    color: #404040;
+    font-weight: 700;
+    position: relative;
+    background: #fcfcfc;
+    border: none;
+    padding: .4045em 1.618em
+}
+
+.wy-menu-vertical li.current>a:hover,
+.wy-menu-vertical li.on a:hover {
+    background: #fcfcfc
+}
+
+.wy-menu-vertical li.current>a:hover button.toctree-expand,
+.wy-menu-vertical li.on a:hover button.toctree-expand {
+    color: grey
+}
+
+.wy-menu-vertical li.current>a button.toctree-expand,
+.wy-menu-vertical li.on a button.toctree-expand {
+    display: block;
+    line-height: 18px;
+    color: #333
+}
+
+.wy-menu-vertical li.toctree-l1.current>a {
+    border-bottom: 1px solid #c9c9c9;
+    border-top: 1px solid #c9c9c9
+}
+
+.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,
+.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,
+.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,
+.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,
+.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,
+.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,
+.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,
+.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,
+.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,
+.wy-menu-vertical .toctree-l10.current .toctree-l11>ul {
+    display: none
+}
+
+.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,
+.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,
+.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,
+.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,
+.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,
+.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,
+.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,
+.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,
+.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,
+.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul {
+    display: block
+}
+
+.wy-menu-vertical li.toctree-l3,
+.wy-menu-vertical li.toctree-l4 {
+    font-size: .9em
+}
+
+.wy-menu-vertical li.toctree-l2 a,
+.wy-menu-vertical li.toctree-l3 a,
+.wy-menu-vertical li.toctree-l4 a,
+.wy-menu-vertical li.toctree-l5 a,
+.wy-menu-vertical li.toctree-l6 a,
+.wy-menu-vertical li.toctree-l7 a,
+.wy-menu-vertical li.toctree-l8 a,
+.wy-menu-vertical li.toctree-l9 a,
+.wy-menu-vertical li.toctree-l10 a {
+    color: #404040
+}
+
+.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,
+.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,
+.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,
+.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,
+.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,
+.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,
+.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,
+.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,
+.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand {
+    color: grey
+}
+
+.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,
+.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,
+.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,
+.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,
+.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,
+.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,
+.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,
+.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,
+.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a {
+    display: block
+}
+
+.wy-menu-vertical li.toctree-l2.current>a {
+    padding: .4045em 2.427em
+}
+
+.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a {
+    padding: .4045em 1.618em .4045em 4.045em
+}
+
+.wy-menu-vertical li.toctree-l3.current>a {
+    padding: .4045em 4.045em
+}
+
+.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a {
+    padding: .4045em 1.618em .4045em 5.663em
+}
+
+.wy-menu-vertical li.toctree-l4.current>a {
+    padding: .4045em 5.663em
+}
+
+.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a {
+    padding: .4045em 1.618em .4045em 7.281em
+}
+
+.wy-menu-vertical li.toctree-l5.current>a {
+    padding: .4045em 7.281em
+}
+
+.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a {
+    padding: .4045em 1.618em .4045em 8.899em
+}
+
+.wy-menu-vertical li.toctree-l6.current>a {
+    padding: .4045em 8.899em
+}
+
+.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a {
+    padding: .4045em 1.618em .4045em 10.517em
+}
+
+.wy-menu-vertical li.toctree-l7.current>a {
+    padding: .4045em 10.517em
+}
+
+.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a {
+    padding: .4045em 1.618em .4045em 12.135em
+}
+
+.wy-menu-vertical li.toctree-l8.current>a {
+    padding: .4045em 12.135em
+}
+
+.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a {
+    padding: .4045em 1.618em .4045em 13.753em
+}
+
+.wy-menu-vertical li.toctree-l9.current>a {
+    padding: .4045em 13.753em
+}
+
+.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a {
+    padding: .4045em 1.618em .4045em 15.371em
+}
+
+.wy-menu-vertical li.toctree-l10.current>a {
+    padding: .4045em 15.371em
+}
+
+.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a {
+    padding: .4045em 1.618em .4045em 16.989em
+}
+
+.wy-menu-vertical li.toctree-l2.current>a,
+.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a {
+    background: #c9c9c9
+}
+
+.wy-menu-vertical li.toctree-l2 button.toctree-expand {
+    color: #a3a3a3
+}
+
+.wy-menu-vertical li.toctree-l3.current>a,
+.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a {
+    background: #bdbdbd
+}
+
+.wy-menu-vertical li.toctree-l3 button.toctree-expand {
+    color: #969696
+}
+
+.wy-menu-vertical li.current ul {
+    display: block
+}
+
+.wy-menu-vertical li ul {
+    margin-bottom: 0;
+    display: none
+}
+
+.wy-menu-vertical li ul li a {
+    margin-bottom: 0;
+    color: #d9d9d9;
+    font-weight: 400
+}
+
+.wy-menu-vertical a {
+    line-height: 18px;
+    padding: .4045em 1.618em;
+    display: block;
+    position: relative;
+    font-size: 90%;
+    color: #d9d9d9
+}
+
+.wy-menu-vertical a:hover {
+    background-color: #4e4a4a;
+    cursor: pointer
+}
+
+.wy-menu-vertical a:hover button.toctree-expand {
+    color: #d9d9d9
+}
+
+.wy-menu-vertical a:active {
+    background-color: #2980b9;
+    cursor: pointer;
+    color: #fff
+}
+
+.wy-menu-vertical a:active button.toctree-expand {
+    color: #fff
+}
+
+.wy-side-nav-search {
+    display: block;
+    width: 300px;
+    padding: .809em;
+    margin-bottom: .809em;
+    z-index: 200;
+    background-color: #2980b9;
+    text-align: center;
+    color: #fcfcfc
+}
+
+.wy-side-nav-search input[type=text] {
+    width: 100%;
+    border-radius: 50px;
+    padding: 6px 12px;
+    border-color: #2472a4
+}
+
+.wy-side-nav-search img {
+    display: block;
+    margin: auto auto .809em;
+    height: 45px;
+    width: 45px;
+    background-color: #2980b9;
+    padding: 5px;
+    border-radius: 100%
+}
+
+.wy-side-nav-search .wy-dropdown>a,
+.wy-side-nav-search>a {
+    color: #fcfcfc;
+    font-size: 100%;
+    font-weight: 700;
+    display: inline-block;
+    padding: 4px 6px;
+    margin-bottom: .809em;
+    max-width: 100%
+}
+
+.wy-side-nav-search .wy-dropdown>a:hover,
+.wy-side-nav-search>a:hover {
+    background: hsla(0, 0%, 100%, .1)
+}
+
+.wy-side-nav-search .wy-dropdown>a img.logo,
+.wy-side-nav-search>a img.logo {
+    display: block;
+    margin: 0 auto;
+    height: auto;
+    width: auto;
+    border-radius: 0;
+    max-width: 100%;
+    background: transparent
+}
+
+.wy-side-nav-search .wy-dropdown>a.icon img.logo,
+.wy-side-nav-search>a.icon img.logo {
+    margin-top: .85em
+}
+
+.wy-side-nav-search>div.version {
+    margin-top: -.4045em;
+    margin-bottom: .809em;
+    font-weight: 400;
+    color: hsla(0, 0%, 100%, .3)
+}
+
+.wy-nav .wy-menu-vertical header {
+    color: #2980b9
+}
+
+.wy-nav .wy-menu-vertical a {
+    color: #b3b3b3
+}
+
+.wy-nav .wy-menu-vertical a:hover {
+    background-color: #2980b9;
+    color: #fff
+}
+
+[data-menu-wrap] {
+    -webkit-transition: all .2s ease-in;
+    -moz-transition: all .2s ease-in;
+    transition: all .2s ease-in;
+    position: absolute;
+    opacity: 1;
+    width: 100%;
+    opacity: 0
+}
+
+[data-menu-wrap].move-center {
+    left: 0;
+    right: auto;
+    opacity: 1
+}
+
+[data-menu-wrap].move-left {
+    right: auto;
+    left: -100%;
+    opacity: 0
+}
+
+[data-menu-wrap].move-right {
+    right: -100%;
+    left: auto;
+    opacity: 0
+}
+
+.wy-body-for-nav {
+    background: #fcfcfc
+}
+
+.wy-grid-for-nav {
+    position: absolute;
+    width: 100%;
+    height: 100%
+}
+
+.wy-nav-side {
+    position: fixed;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    padding-bottom: 2em;
+    width: 300px;
+    overflow-x: hidden;
+    overflow-y: hidden;
+    min-height: 100%;
+    color: #9b9b9b;
+    background: #343131;
+    z-index: 200
+}
+
+.wy-side-scroll {
+    width: 320px;
+    position: relative;
+    overflow-x: hidden;
+    overflow-y: scroll;
+    height: 100%
+}
+
+.wy-nav-top {
+    display: none;
+    background: #2980b9;
+    color: #fff;
+    padding: .4045em .809em;
+    position: relative;
+    line-height: 50px;
+    text-align: center;
+    font-size: 100%;
+    *zoom: 1
+}
+
+.wy-nav-top:after,
+.wy-nav-top:before {
+    display: table;
+    content: ""
+}
+
+.wy-nav-top:after {
+    clear: both
+}
+
+.wy-nav-top a {
+    color: #fff;
+    font-weight: 700
+}
+
+.wy-nav-top img {
+    margin-right: 12px;
+    height: 45px;
+    width: 45px;
+    background-color: #2980b9;
+    padding: 5px;
+    border-radius: 100%
+}
+
+.wy-nav-top i {
+    font-size: 30px;
+    float: left;
+    cursor: pointer;
+    padding-top: inherit
+}
+
+.wy-nav-content-wrap {
+    margin-left: 300px;
+    background: #fcfcfc;
+    min-height: 100%
+}
+
+.wy-nav-content {
+    padding: 1.618em 3.236em;
+    height: 100%;
+    max-width: 800px;
+    margin: auto
+}
+
+.wy-body-mask {
+    position: fixed;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, .2);
+    display: none;
+    z-index: 499
+}
+
+.wy-body-mask.on {
+    display: block
+}
+
+.rst-content footer span.commit tt,
+footer span.commit .rst-content tt,
+footer span.commit code {
+    padding: 0;
+    font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, Courier, monospace;
+    font-size: 1em;
+    background: none;
+    border: none;
+    color: grey
+}
+
+.rst-footer-buttons {
+    *zoom: 1
+}
+
+.rst-footer-buttons:after,
+.rst-footer-buttons:before {
+    width: 100%;
+    display: table;
+    content: ""
+}
+
+.rst-footer-buttons:after {
+    clear: both
+}
+
+.rst-breadcrumbs-buttons {
+    margin-top: 12px;
+    *zoom: 1
+}
+
+.rst-breadcrumbs-buttons:after,
+.rst-breadcrumbs-buttons:before {
+    display: table;
+    content: ""
+}
+
+.rst-breadcrumbs-buttons:after {
+    clear: both
+}
+
+#search-results .search li {
+    margin-bottom: 24px;
+    border-bottom: 1px solid #e1e4e5;
+    padding-bottom: 24px
+}
+
+#search-results .search li:first-child {
+    border-top: 1px solid #e1e4e5;
+    padding-top: 24px
+}
+
+#search-results .search li a {
+    font-size: 120%;
+    margin-bottom: 12px;
+    display: inline-block
+}
+
+#search-results .context {
+    color: grey;
+    font-size: 90%
+}
+
+.genindextable li>ul {
+    margin-left: 24px
+}
+
+
+.rst-versions {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    width: 300px;
+    color: #fcfcfc;
+    background: #1f1d1d;
+    font-family: Lato, proxima-nova, Helvetica Neue, Arial, sans-serif;
+    z-index: 400
+}
+
+.rst-versions a {
+    color: #2980b9;
+    text-decoration: none
+}
+
+.rst-versions .rst-badge-small {
+    display: none
+}
+
+.rst-versions .rst-current-version {
+    padding: 12px;
+    background-color: #272525;
+    display: block;
+    text-align: right;
+    font-size: 90%;
+    cursor: pointer;
+    color: #27ae60;
+    *zoom: 1
+}
+
+.rst-versions .rst-current-version:after,
+.rst-versions .rst-current-version:before {
+    display: table;
+    content: ""
+}
+
+.rst-versions .rst-current-version:after {
+    clear: both
+}
+
+.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,
+.rst-content .eqno .rst-versions .rst-current-version .headerlink,
+.rst-content .rst-versions .rst-current-version .admonition-title,
+.rst-content code.download .rst-versions .rst-current-version span:first-child,
+.rst-content dl dt .rst-versions .rst-current-version .headerlink,
+.rst-content h1 .rst-versions .rst-current-version .headerlink,
+.rst-content h2 .rst-versions .rst-current-version .headerlink,
+.rst-content h3 .rst-versions .rst-current-version .headerlink,
+.rst-content h4 .rst-versions .rst-current-version .headerlink,
+.rst-content h5 .rst-versions .rst-current-version .headerlink,
+.rst-content h6 .rst-versions .rst-current-version .headerlink,
+.rst-content p .rst-versions .rst-current-version .headerlink,
+.rst-content table>caption .rst-versions .rst-current-version .headerlink,
+.rst-content tt.download .rst-versions .rst-current-version span:first-child,
+.rst-versions .rst-current-version .fa,
+.rst-versions .rst-current-version .icon,
+.rst-versions .rst-current-version .rst-content .admonition-title,
+.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,
+.rst-versions .rst-current-version .rst-content .eqno .headerlink,
+.rst-versions .rst-current-version .rst-content code.download span:first-child,
+.rst-versions .rst-current-version .rst-content dl dt .headerlink,
+.rst-versions .rst-current-version .rst-content h1 .headerlink,
+.rst-versions .rst-current-version .rst-content h2 .headerlink,
+.rst-versions .rst-current-version .rst-content h3 .headerlink,
+.rst-versions .rst-current-version .rst-content h4 .headerlink,
+.rst-versions .rst-current-version .rst-content h5 .headerlink,
+.rst-versions .rst-current-version .rst-content h6 .headerlink,
+.rst-versions .rst-current-version .rst-content p .headerlink,
+.rst-versions .rst-current-version .rst-content table>caption .headerlink,
+.rst-versions .rst-current-version .rst-content tt.download span:first-child,
+.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,
+.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand {
+    color: #fcfcfc
+}
+
+.rst-versions .rst-current-version .fa-book,
+.rst-versions .rst-current-version .icon-book {
+    float: left
+}
+
+.rst-versions .rst-current-version.rst-out-of-date {
+    background-color: #e74c3c;
+    color: #fff
+}
+
+.rst-versions .rst-current-version.rst-active-old-version {
+    background-color: #f1c40f;
+    color: #000
+}
+
+.rst-versions.shift-up {
+    height: auto;
+    max-height: 100%;
+    overflow-y: scroll
+}
+
+.rst-versions.shift-up .rst-other-versions {
+    display: block
+}
+
+.rst-versions .rst-other-versions {
+    font-size: 90%;
+    padding: 12px;
+    color: grey;
+    display: none
+}
+
+.rst-versions .rst-other-versions hr {
+    display: block;
+    height: 1px;
+    border: 0;
+    margin: 20px 0;
+    padding: 0;
+    border-top: 1px solid #413d3d
+}
+
+.rst-versions .rst-other-versions dd {
+    display: inline-block;
+    margin: 0
+}
+
+.rst-versions .rst-other-versions dd a {
+    display: inline-block;
+    padding: 6px;
+    color: #fcfcfc
+}
+
+.rst-versions.rst-badge {
+    width: auto;
+    bottom: 20px;
+    right: 20px;
+    left: auto;
+    border: none;
+    max-width: 300px;
+    max-height: 90%
+}
+
+.rst-versions.rst-badge .fa-book,
+.rst-versions.rst-badge .icon-book {
+    float: none;
+    line-height: 30px
+}
+
+.rst-versions.rst-badge.shift-up .rst-current-version {
+    text-align: right
+}
+
+.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,
+.rst-versions.rst-badge.shift-up .rst-current-version .icon-book {
+    float: left
+}
+
+.rst-versions.rst-badge>.rst-current-version {
+    width: auto;
+    height: 30px;
+    line-height: 30px;
+    padding: 0 6px;
+    display: block;
+    text-align: center
+}
+
+@media screen and (max-width:768px) {
+    .rst-versions {
+        width: 85%;
+        display: none
+    }
+    .rst-versions.shift {
+        display: block
+    }
+}
+
+.rst-content .toctree-wrapper>p.caption,
+.rst-content h1,
+.rst-content h2,
+.rst-content h3,
+.rst-content h4,
+.rst-content h5,
+.rst-content h6 {
+    margin-bottom: 24px
+}
+
+.rst-content img {
+    max-width: 100%;
+    height: auto
+}
+
+.rst-content div.figure,
+.rst-content figure {
+    margin-bottom: 24px
+}
+
+.rst-content div.figure .caption-text,
+.rst-content figure .caption-text {
+    font-style: italic
+}
+
+.rst-content div.figure p:last-child.caption,
+.rst-content figure p:last-child.caption {
+    margin-bottom: 0
+}
+
+.rst-content div.figure.align-center,
+.rst-content figure.align-center {
+    text-align: center
+}
+
+.rst-content .section>a>img,
+.rst-content .section>img,
+.rst-content section>a>img,
+.rst-content section>img {
+    margin-bottom: 24px
+}
+
+.rst-content abbr[title] {
+    text-decoration: none
+}
+
+.rst-content.style-external-links a.reference.external:after {
+    font-family: FontAwesome;
+    content: "\f08e";
+    color: #b3b3b3;
+    vertical-align: super;
+    font-size: 60%;
+    margin: 0 .2em
+}
+
+.rst-content blockquote {
+    margin-left: 24px;
+    line-height: 24px;
+    margin-bottom: 24px
+}
+
+.rst-content pre.literal-block {
+    white-space: pre;
+    margin: 0;
+    padding: 12px;
+    font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, Courier, monospace;
+    display: block;
+    overflow: auto
+}
+
+.rst-content div[class^=highlight],
+.rst-content pre.literal-block {
+    border: 1px solid #e1e4e5;
+    overflow-x: auto;
+    margin: 1px 0 24px
+}
+
+.rst-content div[class^=highlight] div[class^=highlight],
+.rst-content pre.literal-block div[class^=highlight] {
+    padding: 0;
+    border: none;
+    margin: 0
+}
+
+.rst-content div[class^=highlight] td.code {
+    width: 100%
+}
+
+.rst-content .linenodiv pre {
+    border-right: 1px solid #e6e9ea;
+    margin: 0;
+    padding: 12px;
+    font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, Courier, monospace;
+    user-select: none;
+    pointer-events: none
+}
+
+.rst-content div[class^=highlight] pre {
+    white-space: pre;
+    margin: 0;
+    padding: 12px;
+    display: block;
+    overflow: auto
+}
+
+.rst-content div[class^=highlight] pre .hll {
+    display: block;
+    margin: 0 -12px;
+    padding: 0 12px
+}
+
+.rst-content .linenodiv pre,
+.rst-content div[class^=highlight] pre,
+.rst-content pre.literal-block {
+    font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, Courier, monospace;
+    font-size: 12px;
+    line-height: 1.4
+}
+
+.rst-content div.highlight .gp,
+.rst-content div.highlight span.linenos {
+    user-select: none;
+    pointer-events: none
+}
+
+.rst-content div.highlight span.linenos {
+    display: inline-block;
+    padding-left: 0;
+    padding-right: 12px;
+    margin-right: 12px;
+    border-right: 1px solid #e6e9ea
+}
+
+.rst-content .code-block-caption {
+    font-style: italic;
+    font-size: 85%;
+    line-height: 1;
+    padding: 1em 0;
+    text-align: center
+}
+
+@media print {
+    .rst-content .codeblock,
+    .rst-content div[class^=highlight],
+    .rst-content div[class^=highlight] pre {
+        white-space: pre-wrap
+    }
+}
+
+.rst-content .admonition,
+.rst-content .admonition-todo,
+.rst-content .attention,
+.rst-content .caution,
+.rst-content .danger,
+.rst-content .error,
+.rst-content .hint,
+.rst-content .important,
+.rst-content .note,
+.rst-content .seealso,
+.rst-content .tip,
+.rst-content .warning {
+    clear: both
+}
+
+.rst-content .admonition-todo .last,
+.rst-content .admonition-todo>:last-child,
+.rst-content .admonition .last,
+.rst-content .admonition>:last-child,
+.rst-content .attention .last,
+.rst-content .attention>:last-child,
+.rst-content .caution .last,
+.rst-content .caution>:last-child,
+.rst-content .danger .last,
+.rst-content .danger>:last-child,
+.rst-content .error .last,
+.rst-content .error>:last-child,
+.rst-content .hint .last,
+.rst-content .hint>:last-child,
+.rst-content .important .last,
+.rst-content .important>:last-child,
+.rst-content .note .last,
+.rst-content .note>:last-child,
+.rst-content .seealso .last,
+.rst-content .seealso>:last-child,
+.rst-content .tip .last,
+.rst-content .tip>:last-child,
+.rst-content .warning .last,
+.rst-content .warning>:last-child {
+    margin-bottom: 0
+}
+
+.rst-content .admonition-title:before {
+    margin-right: 4px
+}
+
+.rst-content .admonition table {
+    border-color: rgba(0, 0, 0, .1)
+}
+
+.rst-content .admonition table td,
+.rst-content .admonition table th {
+    background: transparent !important;
+    border-color: rgba(0, 0, 0, .1) !important
+}
+
+.rst-content .section ol.loweralpha,
+.rst-content .section ol.loweralpha>li,
+.rst-content .toctree-wrapper ol.loweralpha,
+.rst-content .toctree-wrapper ol.loweralpha>li,
+.rst-content section ol.loweralpha,
+.rst-content section ol.loweralpha>li {
+    list-style: lower-alpha
+}
+
+.rst-content .section ol.upperalpha,
+.rst-content .section ol.upperalpha>li,
+.rst-content .toctree-wrapper ol.upperalpha,
+.rst-content .toctree-wrapper ol.upperalpha>li,
+.rst-content section ol.upperalpha,
+.rst-content section ol.upperalpha>li {
+    list-style: upper-alpha
+}
+
+.rst-content .section ol li>*,
+.rst-content .section ul li>*,
+.rst-content .toctree-wrapper ol li>*,
+.rst-content .toctree-wrapper ul li>*,
+.rst-content section ol li>*,
+.rst-content section ul li>* {
+    margin-top: 12px;
+    margin-bottom: 12px
+}
+
+.rst-content .section ol li>:first-child,
+.rst-content .section ul li>:first-child,
+.rst-content .toctree-wrapper ol li>:first-child,
+.rst-content .toctree-wrapper ul li>:first-child,
+.rst-content section ol li>:first-child,
+.rst-content section ul li>:first-child {
+    margin-top: 0
+}
+
+.rst-content .section ol li>p,
+.rst-content .section ol li>p:last-child,
+.rst-content .section ul li>p,
+.rst-content .section ul li>p:last-child,
+.rst-content .toctree-wrapper ol li>p,
+.rst-content .toctree-wrapper ol li>p:last-child,
+.rst-content .toctree-wrapper ul li>p,
+.rst-content .toctree-wrapper ul li>p:last-child,
+.rst-content section ol li>p,
+.rst-content section ol li>p:last-child,
+.rst-content section ul li>p,
+.rst-content section ul li>p:last-child {
+    margin-bottom: 12px
+}
+
+.rst-content .section ol li>p:only-child,
+.rst-content .section ol li>p:only-child:last-child,
+.rst-content .section ul li>p:only-child,
+.rst-content .section ul li>p:only-child:last-child,
+.rst-content .toctree-wrapper ol li>p:only-child,
+.rst-content .toctree-wrapper ol li>p:only-child:last-child,
+.rst-content .toctree-wrapper ul li>p:only-child,
+.rst-content .toctree-wrapper ul li>p:only-child:last-child,
+.rst-content section ol li>p:only-child,
+.rst-content section ol li>p:only-child:last-child,
+.rst-content section ul li>p:only-child,
+.rst-content section ul li>p:only-child:last-child {
+    margin-bottom: 0
+}
+
+.rst-content .section ol li>ol,
+.rst-content .section ol li>ul,
+.rst-content .section ul li>ol,
+.rst-content .section ul li>ul,
+.rst-content .toctree-wrapper ol li>ol,
+.rst-content .toctree-wrapper ol li>ul,
+.rst-content .toctree-wrapper ul li>ol,
+.rst-content .toctree-wrapper ul li>ul,
+.rst-content section ol li>ol,
+.rst-content section ol li>ul,
+.rst-content section ul li>ol,
+.rst-content section ul li>ul {
+    margin-bottom: 12px
+}
+
+.rst-content .section ol.simple li>*,
+.rst-content .section ol.simple li ol,
+.rst-content .section ol.simple li ul,
+.rst-content .section ul.simple li>*,
+.rst-content .section ul.simple li ol,
+.rst-content .section ul.simple li ul,
+.rst-content .toctree-wrapper ol.simple li>*,
+.rst-content .toctree-wrapper ol.simple li ol,
+.rst-content .toctree-wrapper ol.simple li ul,
+.rst-content .toctree-wrapper ul.simple li>*,
+.rst-content .toctree-wrapper ul.simple li ol,
+.rst-content .toctree-wrapper ul.simple li ul,
+.rst-content section ol.simple li>*,
+.rst-content section ol.simple li ol,
+.rst-content section ol.simple li ul,
+.rst-content section ul.simple li>*,
+.rst-content section ul.simple li ol,
+.rst-content section ul.simple li ul {
+    margin-top: 0;
+    margin-bottom: 0
+}
+
+.rst-content .line-block {
+    margin-left: 0;
+    margin-bottom: 24px;
+    line-height: 24px
+}
+
+.rst-content .line-block .line-block {
+    margin-left: 24px;
+    margin-bottom: 0
+}
+
+.rst-content .topic-title {
+    font-weight: 700;
+    margin-bottom: 12px
+}
+
+.rst-content .toc-backref {
+    color: #404040
+}
+
+.rst-content .align-right {
+    float: right;
+    margin: 0 0 24px 24px
+}
+
+.rst-content .align-left {
+    float: left;
+    margin: 0 24px 24px 0
+}
+
+.rst-content .align-center {
+    margin: auto
+}
+
+.rst-content .align-center:not(table) {
+    display: block
+}
+
+.rst-content .code-block-caption .headerlink,
+.rst-content .eqno .headerlink,
+.rst-content .toctree-wrapper>p.caption .headerlink,
+.rst-content dl dt .headerlink,
+.rst-content h1 .headerlink,
+.rst-content h2 .headerlink,
+.rst-content h3 .headerlink,
+.rst-content h4 .headerlink,
+.rst-content h5 .headerlink,
+.rst-content h6 .headerlink,
+.rst-content p.caption .headerlink,
+.rst-content p .headerlink,
+.rst-content table>caption .headerlink {
+    opacity: 0;
+    font-size: 14px;
+    font-family: FontAwesome;
+    margin-left: .5em
+}
+
+.rst-content .code-block-caption .headerlink:focus,
+.rst-content .code-block-caption:hover .headerlink,
+.rst-content .eqno .headerlink:focus,
+.rst-content .eqno:hover .headerlink,
+.rst-content .toctree-wrapper>p.caption .headerlink:focus,
+.rst-content .toctree-wrapper>p.caption:hover .headerlink,
+.rst-content dl dt .headerlink:focus,
+.rst-content dl dt:hover .headerlink,
+.rst-content h1 .headerlink:focus,
+.rst-content h1:hover .headerlink,
+.rst-content h2 .headerlink:focus,
+.rst-content h2:hover .headerlink,
+.rst-content h3 .headerlink:focus,
+.rst-content h3:hover .headerlink,
+.rst-content h4 .headerlink:focus,
+.rst-content h4:hover .headerlink,
+.rst-content h5 .headerlink:focus,
+.rst-content h5:hover .headerlink,
+.rst-content h6 .headerlink:focus,
+.rst-content h6:hover .headerlink,
+.rst-content p.caption .headerlink:focus,
+.rst-content p.caption:hover .headerlink,
+.rst-content p .headerlink:focus,
+.rst-content p:hover .headerlink,
+.rst-content table>caption .headerlink:focus,
+.rst-content table>caption:hover .headerlink {
+    opacity: 1
+}
+
+.rst-content .btn:focus {
+    outline: 2px solid
+}
+
+.rst-content table>caption .headerlink:after {
+    font-size: 12px
+}
+
+.rst-content .centered {
+    text-align: center
+}
+
+.rst-content .sidebar {
+    float: right;
+    width: 40%;
+    display: block;
+    margin: 0 0 24px 24px;
+    padding: 24px;
+    background: #f3f6f6;
+    border: 1px solid #e1e4e5
+}
+
+.rst-content .sidebar dl,
+.rst-content .sidebar p,
+.rst-content .sidebar ul {
+    font-size: 90%
+}
+
+.rst-content .sidebar .last,
+.rst-content .sidebar>:last-child {
+    margin-bottom: 0
+}
+
+.rst-content .sidebar .sidebar-title {
+    display: block;
+    font-family: Roboto Slab, ff-tisa-web-pro, Georgia, Arial, sans-serif;
+    font-weight: 700;
+    background: #e1e4e5;
+    padding: 6px 12px;
+    margin: -24px -24px 24px;
+    font-size: 100%
+}
+
+.rst-content .highlighted {
+    background: #f1c40f;
+    box-shadow: 0 0 0 2px #f1c40f;
+    display: inline;
+    font-weight: 700
+}
+
+.rst-content .citation-reference,
+.rst-content .footnote-reference {
+    vertical-align: baseline;
+    position: relative;
+    top: -.4em;
+    line-height: 0;
+    font-size: 90%
+}
+
+.rst-content .hlist {
+    width: 100%
+}
+
+.rst-content dl dt span.classifier:before {
+    content: " : "
+}
+
+.rst-content dl dt span.classifier-delimiter {
+    display: none !important
+}
+
+html.writer-html4 .rst-content table.docutils.citation,
+html.writer-html4 .rst-content table.docutils.footnote {
+    background: none;
+    border: none
+}
+
+html.writer-html4 .rst-content table.docutils.citation td,
+html.writer-html4 .rst-content table.docutils.citation tr,
+html.writer-html4 .rst-content table.docutils.footnote td,
+html.writer-html4 .rst-content table.docutils.footnote tr {
+    border: none;
+    background-color: transparent !important;
+    white-space: normal
+}
+
+html.writer-html4 .rst-content table.docutils.citation td.label,
+html.writer-html4 .rst-content table.docutils.footnote td.label {
+    padding-left: 0;
+    padding-right: 0;
+    vertical-align: top
+}
+
+html.writer-html5 .rst-content dl.field-list,
+html.writer-html5 .rst-content dl.footnote {
+    display: grid;
+    grid-template-columns: max-content auto
+}
+
+html.writer-html5 .rst-content dl.field-list>dt,
+html.writer-html5 .rst-content dl.footnote>dt {
+    padding-left: 1rem
+}
+
+html.writer-html5 .rst-content dl.field-list>dt:after,
+html.writer-html5 .rst-content dl.footnote>dt:after {
+    content: ":"
+}
+
+html.writer-html5 .rst-content dl.field-list>dd,
+html.writer-html5 .rst-content dl.field-list>dt,
+html.writer-html5 .rst-content dl.footnote>dd,
+html.writer-html5 .rst-content dl.footnote>dt {
+    margin-bottom: 0
+}
+
+html.writer-html5 .rst-content dl.footnote {
+    font-size: .9rem
+}
+
+html.writer-html5 .rst-content dl.footnote>dt {
+    margin: 0 .5rem .5rem 0;
+    line-height: 1.2rem;
+    word-break: break-all;
+    font-weight: 400
+}
+
+html.writer-html5 .rst-content dl.footnote>dt>span.brackets {
+    margin-right: .5rem
+}
+
+html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before {
+    content: "["
+}
+
+html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after {
+    content: "]"
+}
+
+html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref {
+    font-style: italic
+}
+
+html.writer-html5 .rst-content dl.footnote>dd {
+    margin: 0 0 .5rem;
+    line-height: 1.2rem
+}
+
+html.writer-html5 .rst-content dl.footnote>dd p,
+html.writer-html5 .rst-content dl.option-list kbd {
+    font-size: .9rem
+}
+
+.rst-content table.docutils.footnote,
+html.writer-html4 .rst-content table.docutils.citation,
+html.writer-html5 .rst-content dl.footnote {
+    color: grey
+}
+
+.rst-content table.docutils.footnote code,
+.rst-content table.docutils.footnote tt,
+html.writer-html4 .rst-content table.docutils.citation code,
+html.writer-html4 .rst-content table.docutils.citation tt,
+html.writer-html5 .rst-content dl.footnote code,
+html.writer-html5 .rst-content dl.footnote tt {
+    color: #555
+}
+
+.rst-content .wy-table-responsive.citation,
+.rst-content .wy-table-responsive.footnote {
+    margin-bottom: 0
+}
+
+.rst-content .wy-table-responsive.citation+:not(.citation),
+.rst-content .wy-table-responsive.footnote+:not(.footnote) {
+    margin-top: 24px
+}
+
+.rst-content .wy-table-responsive.citation:last-child,
+.rst-content .wy-table-responsive.footnote:last-child {
+    margin-bottom: 24px
+}
+
+.rst-content table.docutils th {
+    border-color: #e1e4e5
+}
+
+html.writer-html5 .rst-content table.docutils th {
+    border: 1px solid #e1e4e5
+}
+
+html.writer-html5 .rst-content table.docutils td>p,
+html.writer-html5 .rst-content table.docutils th>p {
+    line-height: 1rem;
+    margin-bottom: 0;
+    font-size: .9rem
+}
+
+.rst-content table.docutils td .last,
+.rst-content table.docutils td .last>:last-child {
+    margin-bottom: 0
+}
+
+.rst-content table.field-list,
+.rst-content table.field-list td {
+    border: none
+}
+
+.rst-content table.field-list td p {
+    font-size: inherit;
+    line-height: inherit
+}
+
+.rst-content table.field-list td>strong {
+    display: inline-block
+}
+
+.rst-content table.field-list .field-name {
+    padding-right: 10px;
+    text-align: left;
+    white-space: nowrap
+}
+
+.rst-content table.field-list .field-body {
+    text-align: left
+}
+
+.rst-content code,
+.rst-content tt {
+    color: #000;
+    font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, Courier, monospace;
+    padding: 2px 5px
+}
+
+.rst-content code big,
+.rst-content code em,
+.rst-content tt big,
+.rst-content tt em {
+    font-size: 100% !important;
+    line-height: normal
+}
+
+.rst-content code.literal,
+.rst-content tt.literal {
+    color: #e74c3c;
+    white-space: normal
+}
+
+.rst-content code.xref,
+.rst-content tt.xref,
+a .rst-content code,
+a .rst-content tt {
+    font-weight: 700;
+    color: #404040
+}
+
+.rst-content kbd,
+.rst-content pre,
+.rst-content samp {
+    font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, Courier, monospace
+}
+
+.rst-content a code,
+.rst-content a tt {
+    color: #2980b9
+}
+
+.rst-content dl {
+    margin-bottom: 24px
+}
+
+.rst-content dl dt {
+    font-weight: 700;
+    margin-bottom: 12px
+}
+
+.rst-content dl ol,
+.rst-content dl p,
+.rst-content dl table,
+.rst-content dl ul {
+    margin-bottom: 12px
+}
+
+.rst-content dl dd {
+    margin: 0 0 12px 24px;
+    line-height: 24px
+}
+
+html.writer-html4 .rst-content dl:not(.docutils),
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) {
+    margin-bottom: 24px
+}
+
+html.writer-html4 .rst-content dl:not(.docutils)>dt,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt {
+    display: table;
+    margin: 6px 0;
+    font-size: 90%;
+    line-height: normal;
+    background: #e7f2fa;
+    color: #2980b9;
+    border-top: 3px solid #6ab0de;
+    padding: 6px;
+    position: relative
+}
+
+html.writer-html4 .rst-content dl:not(.docutils)>dt:before,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:before {
+    color: #6ab0de
+}
+
+html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt .headerlink {
+    color: #404040;
+    font-size: 100% !important
+}
+
+html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt {
+    margin-bottom: 6px;
+    border: none;
+    border-left: 3px solid #ccc;
+    background: #f0f0f0;
+    color: #555
+}
+
+html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt .headerlink,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt .headerlink {
+    color: #404040;
+    font-size: 100% !important
+}
+
+html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:first-child {
+    margin-top: 0
+}
+
+html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,
+html.writer-html4 .rst-content dl:not(.docutils) code.descname,
+html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,
+html.writer-html4 .rst-content dl:not(.docutils) tt.descname,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descclassname,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descclassname,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname {
+    background-color: transparent;
+    border: none;
+    padding: 0;
+    font-size: 100% !important
+}
+
+html.writer-html4 .rst-content dl:not(.docutils) code.descname,
+html.writer-html4 .rst-content dl:not(.docutils) tt.descname,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname {
+    font-weight: 700
+}
+
+html.writer-html4 .rst-content dl:not(.docutils) .optional,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .optional {
+    display: inline-block;
+    padding: 0 4px;
+    color: #000;
+    font-weight: 700
+}
+
+html.writer-html4 .rst-content dl:not(.docutils) .property,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .property {
+    display: inline-block;
+    padding-right: 8px;
+    max-width: 100%
+}
+
+html.writer-html4 .rst-content dl:not(.docutils) .k,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .k {
+    font-style: italic
+}
+
+html.writer-html4 .rst-content dl:not(.docutils) .descclassname,
+html.writer-html4 .rst-content dl:not(.docutils) .descname,
+html.writer-html4 .rst-content dl:not(.docutils) .sig-name,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .descclassname,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .descname,
+html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .sig-name {
+    font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, Courier, monospace;
+    color: #000
+}
+
+.rst-content .viewcode-back,
+.rst-content .viewcode-link {
+    display: inline-block;
+    color: #27ae60;
+    font-size: 80%;
+    padding-left: 24px
+}
+
+.rst-content .viewcode-back {
+    display: block;
+    float: right
+}
+
+.rst-content p.rubric {
+    margin-bottom: 12px;
+    font-weight: 700
+}
+
+.rst-content code.download,
+.rst-content tt.download {
+    background: inherit;
+    padding: inherit;
+    font-weight: 400;
+    font-family: inherit;
+    font-size: inherit;
+    color: inherit;
+    border: inherit;
+    white-space: inherit
+}
+
+.rst-content code.download span:first-child,
+.rst-content tt.download span:first-child {
+    -webkit-font-smoothing: subpixel-antialiased
+}
+
+.rst-content code.download span:first-child:before,
+.rst-content tt.download span:first-child:before {
+    margin-right: 4px
+}
+
+.rst-content .guilabel {
+    border: 1px solid #7fbbe3;
+    background: #e7f2fa;
+    font-size: 80%;
+    font-weight: 700;
+    border-radius: 4px;
+    padding: 2.4px 6px;
+    margin: auto 2px
+}
+
+.rst-content .versionmodified {
+    font-style: italic
+}
+
+@media screen and (max-width:480px) {
+    .rst-content .sidebar {
+        width: 100%
+    }
+}
+
+span[id*=MathJax-Span] {
+    color: #404040
+}
+
+.math {
+    text-align: center
+}

+ 187 - 0
sd-card/html/mkdocs_theme_extra.css

@@ -0,0 +1,187 @@
+/*
+ * Wrap inline code samples otherwise they shoot of the side and
+ * can't be read at all.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/313
+ * https://github.com/mkdocs/mkdocs/issues/233
+ * https://github.com/mkdocs/mkdocs/issues/834
+ */
+.rst-content code {
+    white-space: pre-wrap;
+    word-wrap: break-word;
+    padding: 2px 5px;
+}
+
+/**
+ * Make code blocks display as blocks and give them the appropriate
+ * font size and padding.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/855
+ * https://github.com/mkdocs/mkdocs/issues/834
+ * https://github.com/mkdocs/mkdocs/issues/233
+ */
+.rst-content pre code {
+    white-space: pre;
+    word-wrap: normal;
+    display: block;
+    padding: 12px;
+    font-size: 12px;
+}
+
+/**
+ * Fix code colors
+ *
+ * https://github.com/mkdocs/mkdocs/issues/2027
+ */
+.rst-content code {
+    color: #E74C3C;
+}
+
+.rst-content pre code {
+    color: #000;
+    background: #f8f8f8;
+}
+
+/*
+ * Fix link colors when the link text is inline code.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/718
+ */
+a code {
+    color: #2980B9;
+}
+a:hover code {
+    color: #3091d1;
+}
+a:visited code {
+    color: #9B59B6;
+}
+
+/*
+ * The CSS classes from highlight.js seem to clash with the
+ * ReadTheDocs theme causing some code to be incorrectly made
+ * bold and italic.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/411
+ */
+pre .cs, pre .c {
+    font-weight: inherit;
+    font-style: inherit;
+}
+
+/*
+ * Fix some issues with the theme and non-highlighted code
+ * samples. Without and highlighting styles attached the
+ * formatting is broken.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/319
+ */
+.rst-content .no-highlight {
+    display: block;
+    padding: 0.5em;
+    color: #333;
+}
+
+
+/*
+ * Additions specific to the search functionality provided by MkDocs
+ */
+
+.search-results {
+    margin-top: 23px;
+}
+
+.search-results article {
+    border-top: 1px solid #E1E4E5;
+    padding-top: 24px;
+}
+
+.search-results article:first-child {
+    border-top: none;
+}
+
+form .search-query {
+    width: 100%;
+    border-radius: 50px;
+    padding: 6px 12px;  /* csslint allow: box-model */
+    border-color: #D1D4D5;
+}
+
+/*
+ * Improve inline code blocks within admonitions.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/656
+ */
+ .rst-content .admonition code {
+    color: #404040;
+    border: 1px solid #c7c9cb;
+    border: 1px solid rgba(0, 0, 0, 0.2);
+    background: #f8fbfd;
+    background: rgba(255, 255, 255, 0.7);
+}
+
+/*
+ * Account for wide tables which go off the side.
+ * Override borders to avoid weirdness on narrow tables.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/834
+ * https://github.com/mkdocs/mkdocs/pull/1034
+ */
+.rst-content .section .docutils {
+    width: 100%;
+    overflow: auto;
+    display: block;
+    border: none;
+}
+
+
+/*
+ * Without the following amendments, the navigation in the theme will be
+ * slightly cut off. This is due to the fact that the .wy-nav-side has a
+ * padding-bottom of 2em, which must not necessarily align with the font-size of
+ * 90 % on the .rst-current-version container, combined with the padding of 12px
+ * above and below. These amendments fix this in two steps: First, make sure the
+ * .rst-current-version container has a fixed height of 40px, achieved using
+ * line-height, and then applying a padding-bottom of 40px to this container. In
+ * a second step, the items within that container are re-aligned using flexbox.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/2012
+ */
+ .wy-nav-side {
+    padding-bottom: 40px;
+}
+
+/*
+ * The second step of above amendment: Here we make sure the items are aligned
+ * correctly within the .rst-current-version container. Using flexbox, we
+ * achieve it in such a way that it will look like the following:
+ *
+ * [No repo_name]
+ *         Next >>                    // On the first page
+ * << Previous     Next >>            // On all subsequent pages
+ *
+ * [With repo_name]
+ *    <repo_name>        Next >>      // On the first page
+ * <repo_name>  << Previous  Next >>  // On all subsequent pages
+ *
+ * https://github.com/mkdocs/mkdocs/issues/2012
+ */
+.rst-versions .rst-current-version {
+    padding: 0 12px;
+    display: flex;
+    font-size: initial;
+    justify-content: space-between;
+    align-items: center;
+    line-height: 40px;
+}
+
+/*
+ * Please note that this amendment also involves removing certain inline-styles
+ * from the file ./mkdocs/themes/readthedocs/versions.html.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/2012
+ */
+.rst-current-version span {
+    flex: 1;
+    text-align: center;
+}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 6
sd-card/html/plotly-2.14.0.min.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 7 - 0
sd-card/html/plotly-basic-2.18.2.min.js


+ 24 - 12
sd-card/html/prevalue_set.html

@@ -48,20 +48,32 @@ input[type=number] {
 
 <table style="width:100%">
   <tr>
-    <h3>Current Value:</h3><p>
-	<div id="prevalue"></div>
-  <h3>Set Value:</h3><p>
-    Input (Format = 123.456):<p>
-	Previous Value: 
-	 <input type="number" id="myInput" name="myInput"
-           pattern="[0-9]+([\.,][0-9]+)?" step="0.001"
-            title="This should be a number with up to 4 decimal places.">
-	<p></p>
-	<button class="button" type="button" onclick="setprevalue()">Set Previous Value</button>
+    <td>
+      <h3>Current "Previous Value":</h3>
+    </td>
+    <td>
+      <div id="prevalue"></div>
+    </td>
+  </tr>
+  <tr>
+    <td>
+      <h3>New "Previous Value":<br>(Format = 123.456)</h3><p>&nbsp;</p>
+    </td>
+    <td>
+      <input type="number" id="myInput" name="myInput"
+              pattern="[0-9]+([\.,][0-9]+)?" step="0.001"
+                title="This should be a number with up to 4 decimal places.">
+      <button class="button" type="button" onclick="setprevalue()">Set Previous Value</button>
+      <p>(The current "Raw Value" got entered as the suggested new "Previous Value")</p>
+    </td>
   </tr>	
   <tr>
-    <h3>Result:</h3><p>
-	<div id="result" readonly></div>
+    <td>
+      <h3>Result:</h3>
+    </td>
+    <td>
+	    <div id="result" readonly></div>
+    </td>
   </tr>	 
 
 </table>

+ 33 - 13
sd-card/html/readconfigparam.js

@@ -162,11 +162,11 @@ function ParseConfig() {
      ParamAddValue(param, catname, "AnalogDigitalTransitionStart", 1, true);
      ParamAddValue(param, catname, "PreValueUse");
      ParamAddValue(param, catname, "PreValueAgeStartup");
-     ParamAddValue(param, catname, "AllowNegativeRates", 1, true);
+     ParamAddValue(param, catname, "AllowNegativeRates", 1, true, "true");
      ParamAddValue(param, catname, "MaxRateValue", 1, true);
      ParamAddValue(param, catname, "MaxRateType", 1, true);
-     ParamAddValue(param, catname, "ExtendedResolution", 1, true);
-     ParamAddValue(param, catname, "IgnoreLeadingNaN", 1, true);
+     ParamAddValue(param, catname, "ExtendedResolution", 1, true, "false");
+     ParamAddValue(param, catname, "IgnoreLeadingNaN", 1, true, "false");
      ParamAddValue(param, catname, "ErrorMessage");
      ParamAddValue(param, catname, "CheckDigitIncreaseConsistency");     
 
@@ -212,12 +212,12 @@ function ParseConfig() {
      category[catname]["enabled"] = false;
      category[catname]["found"] = false;
      param[catname] = new Object();
-     ParamAddValue(param, catname, "IO0", 6, false, [null, null, /^[0-9]*$/, null, null, /^[a-zA-Z0-9_-]*$/]);
-     ParamAddValue(param, catname, "IO1", 6, false, [null, null, /^[0-9]*$/, null, null, /^[a-zA-Z0-9_-]*$/]);
-     ParamAddValue(param, catname, "IO3", 6, false, [null, null, /^[0-9]*$/, null, null, /^[a-zA-Z0-9_-]*$/]);
-     ParamAddValue(param, catname, "IO4", 6, false, [null, null, /^[0-9]*$/, null, null, /^[a-zA-Z0-9_-]*$/]);
-     ParamAddValue(param, catname, "IO12", 6, false, [null, null, /^[0-9]*$/, null, null, /^[a-zA-Z0-9_-]*$/]);
-     ParamAddValue(param, catname, "IO13", 6, false, [null, null, /^[0-9]*$/, null, null, /^[a-zA-Z0-9_-]*$/]);
+     ParamAddValue(param, catname, "IO0", 6, false, "", [null, null, /^[0-9]*$/, null, null, /^[a-zA-Z0-9_-]*$/]);
+     ParamAddValue(param, catname, "IO1", 6, false, "",  [null, null, /^[0-9]*$/, null, null, /^[a-zA-Z0-9_-]*$/]);
+     ParamAddValue(param, catname, "IO3", 6, false, "",  [null, null, /^[0-9]*$/, null, null, /^[a-zA-Z0-9_-]*$/]);
+     ParamAddValue(param, catname, "IO4", 6, false, "",  [null, null, /^[0-9]*$/, null, null, /^[a-zA-Z0-9_-]*$/]);
+     ParamAddValue(param, catname, "IO12", 6, false, "",  [null, null, /^[0-9]*$/, null, null, /^[a-zA-Z0-9_-]*$/]);
+     ParamAddValue(param, catname, "IO13", 6, false, "",  [null, null, /^[0-9]*$/, null, null, /^[a-zA-Z0-9_-]*$/]);
      ParamAddValue(param, catname, "LEDType");
      ParamAddValue(param, catname, "LEDNumbers");
      ParamAddValue(param, catname, "LEDColor", 3);
@@ -262,6 +262,7 @@ function ParseConfig() {
      ParamAddValue(param, catname, "TimeServer");         
      ParamAddValue(param, catname, "Hostname");   
      ParamAddValue(param, catname, "RSSIThreshold");   
+     ParamAddValue(param, catname, "CPUFrequency");
      ParamAddValue(param, catname, "SetupMode"); 
      
      
@@ -283,7 +284,7 @@ function ParseConfig() {
           aktline++;
      }
 
-     // Make the downward compatiblity
+     // Make the downward compatiblity with DataLogging
      if (category["DataLogging"]["found"] == false)
      {
           category["DataLogging"]["found"] = true;
@@ -315,14 +316,23 @@ function ParseConfig() {
           param["DataLogging"]["DataFilesRetention"]["value1"] = "3";
      }
 
+     // Downward compatibility: Create RSSIThreshold if not available
+     if (param["System"]["RSSIThreshold"]["found"] == false)
+     {
+          param["System"]["RSSIThreshold"]["found"] = true;
+          param["System"]["RSSIThreshold"]["enabled"] = false;
+          param["System"]["RSSIThreshold"]["value1"] = "0";
+     }
 }
 
-function ParamAddValue(param, _cat, _param, _anzParam = 1, _isNUMBER = false, _checkRegExList = null){
+
+function ParamAddValue(param, _cat, _param, _anzParam = 1, _isNUMBER = false, _defaultValue = "", _checkRegExList = null){
      param[_cat][_param] = new Object(); 
      param[_cat][_param]["found"] = false;
      param[_cat][_param]["enabled"] = false;
      param[_cat][_param]["line"] = -1; 
      param[_cat][_param]["anzParam"] = _anzParam;
+     param[_cat][_param]["defaultValue"] = _defaultValue;
      param[_cat][_param]["Numbers"] = _isNUMBER;
      param[_cat][_param].checkRegExList = _checkRegExList;
 };
@@ -761,8 +771,18 @@ function CreateNUMBER(_numbernew){
                          _ret[_cat] = new Object();
                     }
                     _ret[_cat][_param] = new Object();
-                    _ret[_cat][_param]["found"] = false;
-                    _ret[_cat][_param]["enabled"] = false;
+                    if (param[_cat][_param]["defaultValue"] === "")
+                    {
+                         _ret[_cat][_param]["found"] = false;
+                         _ret[_cat][_param]["enabled"] = false;
+                    }
+                    else
+                    {
+                         _ret[_cat][_param]["found"] = true;
+                         _ret[_cat][_param]["enabled"] = true;
+                         _ret[_cat][_param]["value1"] = param[_cat][_param]["defaultValue"];
+
+                    }
                     _ret[_cat][_param]["anzParam"] = param[_cat][_param]["anzParam"]; 
 
                }

+ 38 - 12
sd-card/wlan.ini

@@ -1,12 +1,38 @@
-ssid = "SSID"
-password = "PASSWORD"
-hostname = "watermeter"
-;hostname is optional
-
-;if you want to use a fixed IP you need to specify the following 3 parameters (ip, gateway, netmask) with IP4-Addresses "123.456.789.012"
-;ip = "IP4-ADDRESS"
-;gateway = "IP4-ADDRESS"
-;netmask = "255.255.255.0"
-
-;in some cases you want to specify the DNS server as well (especially, if it is not identical to the gateway - this is optional for a fixed IP
-;dns = "IP4-ADDRESS"
+;++++++++++++++++++++++++++++++++++
+; AI on the edge - WLAN configuration
+;++++++++++++++++++++++++++++++++++
+; ssid: Name of WLAN network (mandatory), e.g. "WLAN-SSID"
+; password: Password of WLAN network (mandatory), e.g. "PASSWORD"
+
+ssid = ""
+password = ""
+
+;++++++++++++++++++++++++++++++++++
+; hostname: Name of device in network, e.g "watermeter"
+; This parameter can be configured via WebUI configuration
+; Default: "watermeter", if nothing is configured
+;hostname = "watermeter"
+
+;++++++++++++++++++++++++++++++++++
+; Fixed IP: If you like to use fixed IP instead of DHCP (default), the following
+; parameters needs to be configured: ip, gateway, netmask are mandatory, dns optional
+
+;ip = "xxx.xxx.xxx.xxx"
+;gateway = "xxx.xxx.xxx.xxx"
+;netmask = "xxx.xxx.xxx.xxx"
+
+; DNS server (optional, if no DNS is configured, gateway address will be used)
+
+;dns = "xxx.xxx.xxx.xxx"
+
+;++++++++++++++++++++++++++++++++++
+; WIFI Roaming:
+; Network assisted roaming protocol is activated by default
+; AP / mesh system needs to support roaming protocol 802.11k/v
+;
+; Optional feature (usually not neccessary):
+; RSSI Threshold for client requested roaming query (RSSI < RSSIThreshold)
+; Note: This parameter can be configured via WebUI configuration
+; Default: 0 = Disable client requested roaming query
+
+RSSIThreshold = 0

+ 79 - 0
tools/parameter-tooltip-generator/generate-param-doc-tooltips.py

@@ -0,0 +1,79 @@
+"""
+Grab all parameter files (markdown) and convert them to html files
+"""
+import os
+import glob
+import markdown
+
+
+parameterDocsFolder = "AI-on-the-edge-device-docs/param-docs/parameter-pages"
+docsMainFolder = "../../sd-card/html"
+configPage = "edit_config_param.html"
+
+htmlTooltipPrefix = """
+    <div class="rst-content"><div class="tooltip"><img src="help.png" width="32px"><span class="tooltiptext">
+"""
+
+
+htmlTooltipSuffix = """
+    </span></div></div>
+"""
+
+
+folders = sorted( filter( os.path.isdir, glob.glob(parameterDocsFolder + '/*') ) )
+
+
+def generateHtmlTooltip(section, parameter, markdownFile):
+    # print(section, parameter, markdownFile)
+
+    with open(markdownFile, 'r') as markdownFileHandle:
+        markdownFileContent = markdownFileHandle.read()
+
+    markdownFileContent = markdownFileContent.replace("# ", "### ") # Move all headings 2 level down
+
+    htmlTooltip = markdown.markdown(markdownFileContent, extensions=['admonition'])
+
+    # Make all links to be opened in a new page
+    htmlTooltip = htmlTooltip.replace("a href", "a target=_blank href")
+
+    # Add custom styles
+    htmlTooltip = htmlTooltip.replace("<h3>", "<h3 style=\"margin: 0\">")
+
+    # Update image paths and copy images to right folder
+    if "../img/" in htmlTooltip:
+        htmlTooltip = htmlTooltip.replace("../img/", "/")
+
+    htmlTooltip = htmlTooltipPrefix + htmlTooltip + htmlTooltipSuffix
+
+    # Add the tooltip to the config page
+    with open(docsMainFolder + "/" + configPage, 'r') as configPageHandle:
+        configPageContent = configPageHandle.read()
+
+    # print("replacing $TOOLTIP_" + section + "_" + parameter + " with the tooltip content...")
+    configPageContent = configPageContent.replace("<td>$TOOLTIP_" + section + "_" + parameter + "</td>", "<td>" + htmlTooltip + "</td>")
+
+    with open(docsMainFolder + "/" + configPage, 'w') as configPageHandle:
+        configPageHandle.write(configPageContent)
+
+
+print("Generating Tooltips...")
+
+"""
+Generate a HTML tooltip for each markdown page
+"""
+for folder in folders:
+    folder = folder.split("/")[-1]
+
+    files = sorted(filter(os.path.isfile, glob.glob(parameterDocsFolder + "/" + folder + '/*')))
+    for file in files:
+        if not ".md" in file: # Skip non-markdown files
+            continue
+
+        parameter = file.split("/")[-1].replace(".md", "")
+        parameter = parameter.replace("<", "").replace(">", "")
+        generateHtmlTooltip(folder, parameter, file)
+
+"""
+Copy images to main folder
+"""
+os.system("cp " + parameterDocsFolder + "/img/* " + docsMainFolder + "/")

+ 15 - 0
tools/parameter-tooltip-generator/generate-param-doc-tooltips.sh

@@ -0,0 +1,15 @@
+#!/bin/bash
+
+# Checkout the documentation reposo we can extract the parameter documentation
+if [ -d "AI-on-the-edge-device-docs" ] ; then
+    # Repo already checked out, pull it
+    cd AI-on-the-edge-device-docs
+    git checkout main
+    git pull
+    cd ..
+else
+    # Repos folde ris missing, clone it
+    git clone https://github.com/jomjol/AI-on-the-edge-device-docs.git
+fi
+
+python generate-param-doc-tooltips.py

+ 1 - 0
tools/parameter-tooltip-generator/html/css/github.min.css

@@ -0,0 +1 @@
+.hljs{display:block;overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector-id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font-weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet,.hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:700}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}

+ 1 - 0
tools/parameter-tooltip-generator/html/css/readme.md

@@ -0,0 +1 @@
+The files in this folder are directly copied from the generated mkdocs site folder. 

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 9 - 0
tools/parameter-tooltip-generator/html/css/theme.css


+ 191 - 0
tools/parameter-tooltip-generator/html/css/theme_extra.css

@@ -0,0 +1,191 @@
+/*
+ * Wrap inline code samples otherwise they shoot of the side and
+ * can't be read at all.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/313
+ * https://github.com/mkdocs/mkdocs/issues/233
+ * https://github.com/mkdocs/mkdocs/issues/834
+ */
+.rst-content code {
+    white-space: pre-wrap;
+    word-wrap: break-word;
+    padding: 2px 5px;
+}
+
+/**
+ * Make code blocks display as blocks and give them the appropriate
+ * font size and padding.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/855
+ * https://github.com/mkdocs/mkdocs/issues/834
+ * https://github.com/mkdocs/mkdocs/issues/233
+ */
+.rst-content pre code {
+    white-space: pre;
+    word-wrap: normal;
+    display: block;
+    padding: 12px;
+    font-size: 12px;
+}
+
+/**
+ * Fix code colors
+ *
+ * https://github.com/mkdocs/mkdocs/issues/2027
+ */
+.rst-content code {
+    color: #E74C3C;
+}
+
+.rst-content pre code {
+    color: #000;
+    background: #f8f8f8;
+}
+
+/*
+ * Fix link colors when the link text is inline code.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/718
+ */
+a code {
+    color: #2980B9;
+}
+a:hover code {
+    color: #3091d1;
+}
+a:visited code {
+    color: #9B59B6;
+}
+
+/*
+ * The CSS classes from highlight.js seem to clash with the
+ * ReadTheDocs theme causing some code to be incorrectly made
+ * bold and italic.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/411
+ */
+pre .cs, pre .c {
+    font-weight: inherit;
+    font-style: inherit;
+}
+
+/*
+ * Fix some issues with the theme and non-highlighted code
+ * samples. Without and highlighting styles attached the
+ * formatting is broken.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/319
+ */
+.rst-content .no-highlight {
+    display: block;
+    padding: 0.5em;
+    color: #333;
+}
+
+
+/*
+ * Additions specific to the search functionality provided by MkDocs
+ */
+
+.search-results {
+    margin-top: 23px;
+}
+
+.search-results article {
+    border-top: 1px solid #E1E4E5;
+    padding-top: 24px;
+}
+
+.search-results article:first-child {
+    border-top: none;
+}
+
+form .search-query {
+    width: 100%;
+    border-radius: 50px;
+    padding: 6px 12px;  /* csslint allow: box-model */
+    border-color: #D1D4D5;
+}
+
+/*
+ * Improve inline code blocks within admonitions.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/656
+ */
+ .rst-content .admonition code {
+    color: #404040;
+    border: 1px solid #c7c9cb;
+    border: 1px solid rgba(0, 0, 0, 0.2);
+    background: #f8fbfd;
+    background: rgba(255, 255, 255, 0.7);
+}
+
+/*
+ * Account for wide tables which go off the side.
+ * Override borders to avoid weirdness on narrow tables.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/834
+ * https://github.com/mkdocs/mkdocs/pull/1034
+ */
+.rst-content .section .docutils {
+    width: 100%;
+    overflow: auto;
+    display: block;
+    border: none;
+}
+
+td, th {
+    border: 1px solid #e1e4e5 !important; /* csslint allow: important */
+    border-collapse: collapse;
+}
+
+/*
+ * Without the following amendments, the navigation in the theme will be
+ * slightly cut off. This is due to the fact that the .wy-nav-side has a
+ * padding-bottom of 2em, which must not necessarily align with the font-size of
+ * 90 % on the .rst-current-version container, combined with the padding of 12px
+ * above and below. These amendments fix this in two steps: First, make sure the
+ * .rst-current-version container has a fixed height of 40px, achieved using
+ * line-height, and then applying a padding-bottom of 40px to this container. In
+ * a second step, the items within that container are re-aligned using flexbox.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/2012
+ */
+ .wy-nav-side {
+    padding-bottom: 40px;
+}
+
+/*
+ * The second step of above amendment: Here we make sure the items are aligned
+ * correctly within the .rst-current-version container. Using flexbox, we
+ * achieve it in such a way that it will look like the following:
+ *
+ * [No repo_name]
+ *         Next >>                    // On the first page
+ * << Previous     Next >>            // On all subsequent pages
+ *
+ * [With repo_name]
+ *    <repo_name>        Next >>      // On the first page
+ * <repo_name>  << Previous  Next >>  // On all subsequent pages
+ *
+ * https://github.com/mkdocs/mkdocs/issues/2012
+ */
+.rst-versions .rst-current-version {
+    padding: 0 12px;
+    display: flex;
+    font-size: initial;
+    justify-content: space-between;
+    align-items: center;
+    line-height: 40px;
+}
+
+/*
+ * Please note that this amendment also involves removing certain inline-styles
+ * from the file ./mkdocs/themes/readthedocs/versions.html.
+ *
+ * https://github.com/mkdocs/mkdocs/issues/2012
+ */
+.rst-current-version span {
+    flex: 1;
+    text-align: center;
+}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio