Просмотр исходного кода

Merge branch 'rolling' into master

CaCO3 2 лет назад
Родитель
Сommit
98d85b2c6c
60 измененных файлов с 9487 добавлено и 1374 удалено
  1. 14 5
      .github/workflows/build.yaml
  2. 2 0
      .gitignore
  3. 44 2
      Changelog.md
  4. 12 9
      code/components/jomjol_controlcamera/ClassControllCamera.cpp
  5. 40 39
      code/components/jomjol_fileserver_ota/server_file.cpp
  6. 41 32
      code/components/jomjol_fileserver_ota/server_ota.cpp
  7. 34 18
      code/components/jomjol_flowcontroll/ClassFlowControll.cpp
  8. 2 0
      code/components/jomjol_flowcontroll/ClassFlowControll.h
  9. 21 14
      code/components/jomjol_flowcontroll/ClassFlowMQTT.cpp
  10. 32 1
      code/components/jomjol_helper/Helper.cpp
  11. 6 0
      code/components/jomjol_helper/Helper.h
  12. 166 0
      code/components/jomjol_helper/sdcard_check.cpp
  13. 11 0
      code/components/jomjol_helper/sdcard_check.h
  14. 148 0
      code/components/jomjol_helper/statusled.cpp
  15. 34 0
      code/components/jomjol_helper/statusled.h
  16. 17 11
      code/components/jomjol_logfile/ClassLogFile.cpp
  17. 1 1
      code/components/jomjol_logfile/ClassLogFile.h
  18. 19 7
      code/components/jomjol_mqtt/interface_mqtt.cpp
  19. 105 51
      code/components/jomjol_mqtt/server_mqtt.cpp
  20. 1 1
      code/components/jomjol_mqtt/server_mqtt.h
  21. 114 61
      code/components/jomjol_tfliteclass/server_tflite.cpp
  22. 1 0
      code/components/jomjol_tfliteclass/server_tflite.h
  23. 190 225
      code/components/jomjol_wlan/connect_wlan.cpp
  24. 1 8
      code/components/jomjol_wlan/connect_wlan.h
  25. 191 167
      code/components/jomjol_wlan/read_wlanini.cpp
  26. 13 1
      code/components/jomjol_wlan/read_wlanini.h
  27. 2 7
      code/include/defines.h
  28. 325 282
      code/main/main.cpp
  29. 9 5
      code/main/server_main.cpp
  30. 77 39
      code/main/softAP.cpp
  31. 2 0
      code/sdkconfig.defaults
  32. BIN
      sd-card/config/ana-class100_0154_s1_q.tflite
  33. BIN
      sd-card/config/ana-class100_0157_s1_q.tflite
  34. BIN
      sd-card/config/ana-cont_11.3.1_s2.tflite
  35. BIN
      sd-card/config/ana-cont_1105_s2_q.tflite
  36. BIN
      sd-card/config/dig-class100-0150_s2_q.tflite
  37. BIN
      sd-card/config/dig-class100_0160_s2_q.tflite
  38. 12 8
      sd-card/html/data.html
  39. 1 1
      sd-card/html/edit_alignment.html
  40. 1 1
      sd-card/html/edit_analog.html
  41. 167 265
      sd-card/html/edit_config_param.html
  42. 1 1
      sd-card/html/edit_digits.html
  43. 1 1
      sd-card/html/edit_reference.html
  44. 1 0
      sd-card/html/github.min.css
  45. 148 64
      sd-card/html/graph.html
  46. BIN
      sd-card/html/help.png
  47. 13 4
      sd-card/html/index.html
  48. 6883 0
      sd-card/html/mkdocs_theme.css
  49. 187 0
      sd-card/html/mkdocs_theme_extra.css
  50. 0 6
      sd-card/html/plotly-2.14.0.min.js
  51. 7 0
      sd-card/html/plotly-basic-2.18.2.min.js
  52. 24 12
      sd-card/html/prevalue_set.html
  53. 32 13
      sd-card/html/readconfigparam.js
  54. 38 12
      sd-card/wlan.ini
  55. 79 0
      tools/parameter-tooltip-generator/generate-param-doc-tooltips.py
  56. 15 0
      tools/parameter-tooltip-generator/generate-param-doc-tooltips.sh
  57. 1 0
      tools/parameter-tooltip-generator/html/css/github.min.css
  58. 1 0
      tools/parameter-tooltip-generator/html/css/readme.md
  59. 9 0
      tools/parameter-tooltip-generator/html/css/theme.css
  60. 191 0
      tools/parameter-tooltip-generator/html/css/theme_extra.css

+ 14 - 5
.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

+ 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

+ 44 - 2
Changelog.md

@@ -1,3 +1,44 @@
+## [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**
@@ -37,7 +78,8 @@ If you want to revert back to `v14` or earlier, you will have to revert the migr
 
 -   n.a.
 
-## [14.0.3] - 2023-02-05
+
+## [14.0.3] -2023-02-05
 
 **Stabilization and Improved User Experience**
 
@@ -841,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

+ 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>"

+ 34 - 18
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,6 +274,8 @@ 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
     //#endif //ENABLE_MQTT
@@ -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, false);
             #endif //ENABLE_MQTT
 
             FlowControll[i]->doFlow(time);
@@ -371,11 +382,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, false);
         #endif //ENABLE_MQTT
 
         #ifdef DEBUG_DETAIL_ON
@@ -405,11 +416,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, false);
     #endif //ENABLE_MQTT
 
     return result;
@@ -589,6 +600,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))
         {
@@ -597,18 +612,21 @@ bool ClassFlowControll::ReadParameter(FILE* pfile, string& aktparamgraph)
 
         /* TimeServer and TimeZone got already read from the config, see setupTime () */
 
+        #ifdef WLAN_USE_MESH_ROAMING
         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 +634,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);
 

+ 21 - 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;  
@@ -165,7 +166,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 +174,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 +211,7 @@ bool ClassFlowMQTT::Start(float AutoInterval)
 
 bool ClassFlowMQTT::doFlow(string zwtime)
 {
+    bool success;
     std::string result;
     std::string resulterror = "";
     std::string resultraw = "";
@@ -220,7 +223,7 @@ bool ClassFlowMQTT::doFlow(string zwtime)
     string zw = "";
     string namenumber = "";
 
-    publishSystemData();
+    success = publishSystemData();
 
     if (flowpostprocessing && getMQTTisConnected())
     {
@@ -246,13 +249,13 @@ bool ClassFlowMQTT::doFlow(string zwtime)
 
 
             if (result.length() > 0)   
-                MQTTPublish(namenumber + "value", result, SetRetainFlag);
+                success |= MQTTPublish(namenumber + "value", result, SetRetainFlag);
 
             if (resulterror.length() > 0)  
-                MQTTPublish(namenumber + "error", resulterror, SetRetainFlag);
+                success |= MQTTPublish(namenumber + "error", resulterror, SetRetainFlag);
 
             if (resultrate.length() > 0) {
-                MQTTPublish(namenumber + "rate", resultrate, SetRetainFlag);
+                success |= MQTTPublish(namenumber + "rate", resultrate, SetRetainFlag);
                 
                 std::string resultRatePerTimeUnit;
                 if (getTimeUnit() == "h") { // Need conversion to be per hour
@@ -261,22 +264,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, 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, SetRetainFlag); // Legacy API
+                success |= MQTTPublish(namenumber + "rate_per_digitalization_round", resultchangabs, SetRetainFlag);
             }
 
             if (resultraw.length() > 0)   
-                MQTTPublish(namenumber + "raw", resultraw, SetRetainFlag);
+                success |= MQTTPublish(namenumber + "raw", resultraw, SetRetainFlag);
 
             if (resulttimestamp.length() > 0)
-                MQTTPublish(namenumber + "timestamp", resulttimestamp, SetRetainFlag);
+                success |= MQTTPublish(namenumber + "timestamp", resulttimestamp, SetRetainFlag);
 
             std::string json = flowpostprocessing->getJsonFromNumber(i, "\n");
-            MQTTPublish(namenumber + "json", json, SetRetainFlag);
+            success |= MQTTPublish(namenumber + "json", json, SetRetainFlag);
         }
     }
     
@@ -294,10 +297,14 @@ bool ClassFlowMQTT::doFlow(string zwtime)
     //                 result = result + "\t" + zw;
     //         }
     //     }
-    //     MQTTPublish(topic, result, SetRetainFlag);
+    //     success |= MQTTPublish(topic, result, 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

+ 17 - 11
code/components/jomjol_logfile/ClassLogFile.cpp

@@ -78,11 +78,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 +95,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");
@@ -386,14 +389,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();
 

+ 19 - 7
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
@@ -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;
@@ -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();

+ 105 - 51
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"
@@ -46,7 +47,7 @@ void mqttServer_setMeterType(std::string _meterType, std::string _valueUnit, std
     rateUnit = _rateUnit;
 }
 
-void sendHomeAssistantDiscoveryTopic(std::string group, std::string field,
+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) {
     std::string version = std::string(libfive_git_version());
 
@@ -130,26 +131,29 @@ void sendHomeAssistantDiscoveryTopic(std::string group, std::string field,
     "}"  +
     "}";
 
-    MQTTPublish(topicFull, payload, true);
+    return MQTTPublish(topicFull, payload, true);
 }
 
-void MQTThomeassistantDiscovery() {  
-    if (!getMQTTisConnected()) 
-        return;
+bool MQTThomeassistantDiscovery() {  
+    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;
+    }
 
-    //                              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");
+    LogFile.WriteToFile(ESP_LOG_INFO, TAG, "MQTT - Sending 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
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "uptime",          "Uptime",            "clock-time-eight-outline", "s",   "",                "",            "diagnostic");
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "MAC",             "MAC Address",       "network-outline",          "",    "",                "",            "diagnostic");
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "hostname",        "Hostname",          "network-outline",          "",    "",                "",            "diagnostic");
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "freeMem",         "Free Memory",       "memory",                   "B",   "",                "measurement", "diagnostic");
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "wifiRSSI",        "Wi-Fi RSSI",        "wifi",                     "dBm", "signal_strength", "",            "diagnostic");
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "CPUtemp",         "CPU Temperature",   "thermometer",              "°C",  "temperature",     "measurement", "diagnostic");
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "interval",        "Interval",          "clock-time-eight-outline", "min",  ""           ,    "measurement", "diagnostic");
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "IP",              "IP",                "network-outline",           "",    "",               "",            "diagnostic");
+    allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("",     "status",          "Status",            "list-status",               "",    "",               "",            "diagnostic");
 
 
     for (int i = 0; i < (*NUMBERS).size(); ++i) {
@@ -158,76 +162,126 @@ 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", "");
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "raw",                "Raw Value",                        "raw",                   "",        "",            "",                 "diagnostic");
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "error",              "Error",                            "alert-circle-outline",  "",        "",            "",                 "diagnostic");
         /* 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,  "",            "",                 "");        
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "rate_per_digitalization_round",  "Change since last digitalization round", "arrow-expand-vertical", valueUnit, "",            "measurement",      ""); // correctly the Unit is Uint/Interval!
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "timestamp",          "Timestamp",                  "clock-time-eight-outline", "",        "timestamp",   "",                "diagnostic");
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "json",               "JSON",                       "code-json",                "",        "",            "",                 "diagnostic");
+        allSendsSuccessed |= sendHomeAssistantDiscoveryTopic(group,   "problem",            "Problem",                    "alert-outline",            "",        "problem",            "",                 ""); // Special binary sensor which is based on error topic
     }
+
+    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Successfully published all Homeassistant Discovery MQTT topics");
+    return allSendsSuccessed;
 }
 
-void publishSystemData() {
-    if (!getMQTTisConnected()) 
-        return;
+bool publishSystemData() {
+    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...");
 
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + std::string(LWT_TOPIC), LWT_CONNECTED, 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), retainFlag);
     
     sprintf(tmp_char, "%lu", (long) getESPHeapSize());
-    MQTTPublish(maintopic + "/" + "freeMem", std::string(tmp_char), retainFlag);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "freeMem", std::string(tmp_char), retainFlag);
 
     sprintf(tmp_char, "%d", get_WIFI_RSSI());
-    MQTTPublish(maintopic + "/" + "wifiRSSI", std::string(tmp_char), retainFlag);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "wifiRSSI", std::string(tmp_char), retainFlag);
 
     sprintf(tmp_char, "%d", (int)temperatureRead());
-    MQTTPublish(maintopic + "/" + "CPUtemp", std::string(tmp_char), retainFlag);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "CPUtemp", std::string(tmp_char), retainFlag);
+
+    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Successfully published all System MQTT topics");
+    return allSendsSuccessed;
 }
 
 
-void publishStaticData() {
-    if (!getMQTTisConnected()) 
-        return;
+bool publishStaticData() {
+    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);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "MAC", getMac(), retainFlag);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "IP", *getIPAddress(), retainFlag);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "hostname", wlan_config.hostname, retainFlag);
 
     std::stringstream stream;
     stream << std::fixed << std::setprecision(1) << roundInterval; // minutes
-    MQTTPublish(maintopic + "/" + "interval", stream.str(), retainFlag);
+    allSendsSuccessed |= MQTTPublish(maintopic + "/" + "interval", stream.str(), retainFlag);
+
+    LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Successfully published all Static MQTT topics");
+    return allSendsSuccessed;
 }
 
 esp_err_t sendDiscovery_and_static_Topics(httpd_req_t *req) {
+    bool success = false;
+
     if (HomeassistantDiscovery) {
-        MQTThomeassistantDiscovery();
+        success = MQTThomeassistantDiscovery();
     }
 
-    publishStaticData();
-
-    const char* resp_str = (const char*) req->user_ctx;
-    httpd_resp_send(req, resp_str, strlen(resp_str));  
+    success |= publishStaticData();
 
-    return ESP_OK;
+    if (success) {
+        char msg[] = "MQTT Homeassistant Discovery and Static Topics sent!";
+        httpd_resp_send(req, msg, strlen(msg));  
+        return ESP_OK;
+    }
+    else {
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "One or more MQTT topics failed to be published!");
+        char msg[] = "Failed to send MQTT topics!";
+        httpd_resp_send(req, msg, strlen(msg)); 
+        return ESP_FAIL;
+    }
 }
 
 void GotConnected(std::string maintopic, bool retainFlag) {
-    if (HomeassistantDiscovery) {
-        MQTThomeassistantDiscovery();
+    static bool initialStaticOrHomeassistantDiscoveryTopicsGotSent = false;
+    bool success = false;
+
+    /* Only send Homeassistant Discovery and Static topics on the first time connecting */
+    if (!initialStaticOrHomeassistantDiscoveryTopicsGotSent) {
+        if (HomeassistantDiscovery) {
+            success = MQTThomeassistantDiscovery();
+        }
+
+        success |= publishStaticData();
+
+        if (success) {
+            /* Sending of all Homeassistant Discovery and Static Topics was successfull.
+             * Will no no longer send it on a re-connect!
+             * (But it is still possible to trigger sending through the REST API). */
+            initialStaticOrHomeassistantDiscoveryTopicsGotSent = true;
+        }
+        else {
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "One or more static or Homeassistant Discovery MQTT topics failed to be published! Will try again on the next round.");
+        }
     }
 
-    publishStaticData();
-    publishSystemData();
+    /* The System Data changes at runtime, therefore we always send it after a re-connect */
+    success |= publishSystemData();
+
+    if (!success) {
+        LogFile.WriteToFile(ESP_LOG_WARN, TAG, "One or more MQTT topics failed to be published!");
+    }
 }
 
 void register_server_mqtt_uri(httpd_handle_t server) {
@@ -236,7 +290,7 @@ void register_server_mqtt_uri(httpd_handle_t server) {
 
     uri.uri       = "/mqtt_publish_discovery";
     uri.handler   = sendDiscovery_and_static_Topics;
-    uri.user_ctx  = (void*) "MQTT Discovery and Static Topics sent";    
+    uri.user_ctx  = (void*) "";    
     httpd_register_uri_handler(server, &uri); 
 }
 

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

@@ -16,7 +16,7 @@ std::string mqttServer_getMainTopic();
 
 void register_server_mqtt_uri(httpd_handle_t server);
 
-void publishSystemData();
+bool publishSystemData();
 
 std::string getTimeUnit(void);
 void GotConnected(std::string maintopic, bool SetRetainFlag);

+ 114 - 61
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,16 +908,22 @@ 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
         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. Check configuration");
+            StatusLED(TIME_CHECK, 1, false);
+        }
 
         fr_delta_ms = (esp_timer_get_time() - fr_start) / 1000;
         if (auto_interval > fr_delta_ms)

+ 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);

+ 190 - 225
code/components/jomjol_wlan/connect_wlan.cpp

@@ -1,72 +1,48 @@
 #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 "../../include/defines.h"
+#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;
-
-///////////////////////////////////////////////////////////
-
-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;
+bool WIFIConnected = false;
+int WIFIReconnectCnt = 0;
 
-/////////////////////////////////
-/////////////////////////////////
 
 #ifdef WLAN_USE_MESH_ROAMING
 
@@ -301,97 +277,87 @@ static void esp_bss_rssi_low_handler(void* arg, esp_event_base_t event_base,
 
 std::string* getIPAddress()
 {
-    return &ipadress;
+    return &wlan_config.ipaddress;
 }
 
 
 std::string* getSSID()
 {
-    return &ssid;
+    return &wlan_config.ssid;
 }
 
 
-void task_doBlink(void *pvParameter)
+static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
 {
-    ESP_LOGI("BLINK", "Flash - start");
-    while (BlinkIsRunning)
-    {
-//        ESP_LOGI("BLINK", "Blinken - wait");
-        vTaskDelay(100 / portTICK_PERIOD_MS);
+    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) 
+	{
+        WIFIConnected = false;
+        esp_wifi_connect();
     }
-
-    BlinkIsRunning = true;
-
-	// 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);
+	
+	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)");
+			// --> 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
 		}
-        gpio_set_level(BLINK_GPIO, 0);      
-        vTaskDelay(BlinkDauer / portTICK_PERIOD_MS);
-    }
-
-    if (BlinkOff)
-        gpio_set_level(BLINK_GPIO, 1);
-
-    ESP_LOGI("BLINK", "Flash - done");
-    BlinkIsRunning = false;
-
-    vTaskDelete(NULL); //Delete this task if it exits from the loop above
-}
-
-
-void LEDBlinkTask(int _dauer, int _anz, bool _off)
-{
-	BlinkDauer = _dauer;
-	BlinkAnzahl = _anz;
-	BlinkOff = _off;
-
-    xTaskCreate(&task_doBlink, "task_doBlink", 4 * 1024, NULL, tskIDLE_PRIORITY+1, NULL);
-}
-/////////////////////////////////////////////////////////
 
+		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()));
+	}
+	
+	else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) 
+	{
+        WIFIConnected = true;
+		WIFIReconnectCnt = 0;
 
-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) {
-        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);
+		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, "Got IP: " + wlan_config.ipaddress);
 
-        WIFIConnected = true;
-        #ifdef ENABLE_MQTT
+		#ifdef ENABLE_MQTT
             if (getMQTTisEnabled()) {
                 vTaskDelay(5000 / portTICK_PERIOD_MS); 
                 MQTT_Init();    // Init when WIFI is getting connected    
             }
-        #endif //ENABLE_MQTT
-    }
+        #endif //ENABLE_MQTT   
+	}
 }
 
 
@@ -403,148 +369,148 @@ void strinttoip4(const char *ip, int &a, int &b, int &c, int &d) {
 }
 
 
-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);
+    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;
+	}
 
-    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() );
+    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 +522,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();
 }

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

@@ -5,18 +5,11 @@
 
 #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;
-
 #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);
+            #ifdef WLAN_USE_MESH_ROAMING
+            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;
 }
 
-
+#ifdef WLAN_USE_MESH_ROAMING
 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);
 

+ 2 - 7
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
@@ -164,12 +164,7 @@
     //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
+
 
     //ClassFlowCNNGeneral
     #define Analog_error 3

+ 325 - 282
code/main/main.cpp

@@ -40,7 +40,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 +86,12 @@ 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);
 
 static const char *TAG = "MAIN";
 
+
 bool Init_NVS_SDCard()
 {
     esp_err_t ret = nvs_flash_init();
@@ -100,9 +99,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 +108,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 +132,324 @@ 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();
+    // SD card: Create log directories (if not already existing)
+    // ********************************************
+    LogFile.CreateLogDirectories(); // mandatory for logging + image saving
 
-    setupTime();
-
-    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
+    // 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
 
-#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
-
-    CheckIsPlannedReboot();
+    // 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 frequency: " + std::to_string(CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ) + 
+                                           "Mhz, 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 +466,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 +508,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 +523,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 +608,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
         }
     }
 
@@ -623,33 +696,3 @@ std::vector<std::string> splitString(const std::string& str) {
 
     return found;
 }*/
-
-
-bool replace(std::string& s, std::string const& toReplace, std::string const& replaceWith) {
-    return replace(s, toReplace, replaceWith, true);
-}
-
-bool replace(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;
-}

+ 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

+ 2 - 0
code/sdkconfig.defaults

@@ -133,6 +133,8 @@ CONFIG_GC032A_SUPPORT=n
 CONFIG_GC0308_SUPPORT=n
 CONFIG_BF3005_SUPPORT=n
 
+CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=4864
+
 #only necessary for task analysis (include/defines -> TASK_ANALYSIS_ON)
 #set in [env:esp32cam-dev-task-analysis]
 #CONFIG_FREERTOS_USE_TRACE_FACILITY=1

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


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


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


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


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


BIN
sd-card/config/dig-class100_0160_s2_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;

Разница между файлами не показана из-за своего большого размера
+ 167 - 265
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
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;
+}

Разница между файлами не показана из-за своего большого размера
+ 0 - 6
sd-card/html/plotly-2.14.0.min.js


Разница между файлами не показана из-за своего большого размера
+ 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>

+ 32 - 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);
@@ -283,7 +283,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 +315,23 @@ function ParseConfig() {
           param["DataLogging"]["DataFilesRetention"]["value1"] = "3";
      }
 
+     // Downward compatiblity: 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 +770,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. 

Разница между файлами не показана из-за своего большого размера
+ 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;
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов