浏览代码

Merge branch 'main' of https://github.com/jomjol/AI-on-the-edge-device

jomjol 11 月之前
父节点
当前提交
063c4827d0

+ 47 - 33
README.md

@@ -251,17 +251,17 @@ There are some ideas and feature requests which are not currently being pursued
                 </a>
             </td>
             <td align="center">
-                <a href="https://github.com/Slider0007">
-                    <img src="https://avatars.githubusercontent.com/u/115730895?v=4" width="100;" alt="Slider0007"/>
+                <a href="https://github.com/SybexX">
+                    <img src="https://avatars.githubusercontent.com/u/587201?v=4" width="100;" alt="SybexX"/>
                     <br />
-                    <sub><b>Slider0007</b></sub>
+                    <sub><b>michael</b></sub>
                 </a>
             </td>
             <td align="center">
-                <a href="https://github.com/SybexX">
-                    <img src="https://avatars.githubusercontent.com/u/587201?v=4" width="100;" alt="SybexX"/>
+                <a href="https://github.com/Slider0007">
+                    <img src="https://avatars.githubusercontent.com/u/115730895?v=4" width="100;" alt="Slider0007"/>
                     <br />
-                    <sub><b>michael</b></sub>
+                    <sub><b>Slider0007</b></sub>
                 </a>
             </td>
             <td align="center">
@@ -368,6 +368,13 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>parhedberg</b></sub>
                 </a>
             </td>
+            <td align="center">
+                <a href="https://github.com/fsck-block">
+                    <img src="https://avatars.githubusercontent.com/u/58307481?v=4" width="100;" alt="fsck-block"/>
+                    <br />
+                    <sub><b>fsck-block</b></sub>
+                </a>
+            </td>
             <td align="center">
                 <a href="https://github.com/slovdahl">
                     <img src="https://avatars.githubusercontent.com/u/1417619?v=4" width="100;" alt="slovdahl"/>
@@ -389,13 +396,6 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>LordGuilly</b></sub>
                 </a>
             </td>
-            <td align="center">
-                <a href="https://github.com/bilalmirza74">
-                    <img src="https://avatars.githubusercontent.com/u/84387676?v=4" width="100;" alt="bilalmirza74"/>
-                    <br />
-                    <sub><b>Bilal Mirza</b></sub>
-                </a>
-            </td>
             <td align="center">
                 <a href="https://github.com/muggenhor">
                     <img src="https://avatars.githubusercontent.com/u/484066?v=4" width="100;" alt="muggenhor"/>
@@ -406,10 +406,17 @@ There are some ideas and feature requests which are not currently being pursued
 		</tr>
 		<tr>
             <td align="center">
-                <a href="https://github.com/ppisljar">
-                    <img src="https://avatars.githubusercontent.com/u/13629809?v=4" width="100;" alt="ppisljar"/>
+                <a href="https://github.com/bilalmirza74">
+                    <img src="https://avatars.githubusercontent.com/u/84387676?v=4" width="100;" alt="bilalmirza74"/>
                     <br />
-                    <sub><b>Peter Pisljar</b></sub>
+                    <sub><b>Bilal Mirza</b></sub>
+                </a>
+            </td>
+            <td align="center">
+                <a href="https://github.com/AngryApostrophe">
+                    <img src="https://avatars.githubusercontent.com/u/89547888?v=4" width="100;" alt="AngryApostrophe"/>
+                    <br />
+                    <sub><b>AngryApostrophe</b></sub>
                 </a>
             </td>
             <td align="center">
@@ -426,6 +433,13 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>Ranjana761</b></sub>
                 </a>
             </td>
+            <td align="center">
+                <a href="https://github.com/SURYANSH-RAI">
+                    <img src="https://avatars.githubusercontent.com/u/79277130?v=4" width="100;" alt="SURYANSH-RAI"/>
+                    <br />
+                    <sub><b>SURYANSH RAI</b></sub>
+                </a>
+            </td>
             <td align="center">
                 <a href="https://github.com/SkylightXD">
                     <img src="https://avatars.githubusercontent.com/u/16561545?v=4" width="100;" alt="SkylightXD"/>
@@ -433,6 +447,8 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>SkylightXD</b></sub>
                 </a>
             </td>
+		</tr>
+		<tr>
             <td align="center">
                 <a href="https://github.com/ottk3">
                     <img src="https://avatars.githubusercontent.com/u/5236802?v=4" width="100;" alt="ottk3"/>
@@ -447,8 +463,6 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>Tobias Bieniek</b></sub>
                 </a>
             </td>
-		</tr>
-		<tr>
             <td align="center">
                 <a href="https://github.com/tkopczuk">
                     <img src="https://avatars.githubusercontent.com/u/101632?v=4" width="100;" alt="tkopczuk"/>
@@ -477,6 +491,8 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>flox_x</b></sub>
                 </a>
             </td>
+		</tr>
+		<tr>
             <td align="center">
                 <a href="https://github.com/gneluka">
                     <img src="https://avatars.githubusercontent.com/u/32097881?v=4" width="100;" alt="gneluka"/>
@@ -491,8 +507,6 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>kalwados</b></sub>
                 </a>
             </td>
-		</tr>
-		<tr>
             <td align="center">
                 <a href="https://github.com/kub3let">
                     <img src="https://avatars.githubusercontent.com/u/95883234?v=4" width="100;" alt="kub3let"/>
@@ -521,13 +535,8 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>smartboart</b></sub>
                 </a>
             </td>
-            <td align="center">
-                <a href="https://github.com/AngryApostrophe">
-                    <img src="https://avatars.githubusercontent.com/u/89547888?v=4" width="100;" alt="AngryApostrophe"/>
-                    <br />
-                    <sub><b>AngryApostrophe</b></sub>
-                </a>
-            </td>
+		</tr>
+		<tr>
             <td align="center">
                 <a href="https://github.com/wetneb">
                     <img src="https://avatars.githubusercontent.com/u/309908?v=4" width="100;" alt="wetneb"/>
@@ -535,8 +544,6 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>Antonin Delpeuch</b></sub>
                 </a>
             </td>
-		</tr>
-		<tr>
             <td align="center">
                 <a href="https://github.com/adarazs">
                     <img src="https://avatars.githubusercontent.com/u/6269603?v=4" width="100;" alt="adarazs"/>
@@ -572,6 +579,8 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>Dave</b></sub>
                 </a>
             </td>
+		</tr>
+		<tr>
             <td align="center">
                 <a href="https://github.com/FarukhS52">
                     <img src="https://avatars.githubusercontent.com/u/129654632?v=4" width="100;" alt="FarukhS52"/>
@@ -579,8 +588,6 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>Farookh Zaheer Siddiqui</b></sub>
                 </a>
             </td>
-		</tr>
-		<tr>
             <td align="center">
                 <a href="https://github.com/hex7c0">
                     <img src="https://avatars.githubusercontent.com/u/4419146?v=4" width="100;" alt="hex7c0"/>
@@ -616,6 +623,8 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>Joerg Rosenkranz</b></sub>
                 </a>
             </td>
+		</tr>
+		<tr>
             <td align="center">
                 <a href="https://github.com/Innovatorcloudy">
                     <img src="https://avatars.githubusercontent.com/u/183274513?v=4" width="100;" alt="Innovatorcloudy"/>
@@ -623,8 +632,6 @@ There are some ideas and feature requests which are not currently being pursued
                     <sub><b>KrishCode</b></sub>
                 </a>
             </td>
-		</tr>
-		<tr>
             <td align="center">
                 <a href="https://github.com/myxor">
                     <img src="https://avatars.githubusercontent.com/u/1397377?v=4" width="100;" alt="myxor"/>
@@ -652,6 +659,13 @@ There are some ideas and feature requests which are not currently being pursued
                     <br />
                     <sub><b>Michael Geissler</b></sub>
                 </a>
+            </td>
+            <td align="center">
+                <a href="https://github.com/ppisljar">
+                    <img src="https://avatars.githubusercontent.com/u/13629809?v=4" width="100;" alt="ppisljar"/>
+                    <br />
+                    <sub><b>Peter Pisljar</b></sub>
+                </a>
             </td>
 		</tr>
 	<tbody>

+ 61 - 84
code/components/jomjol_fileserver_ota/server_file.cpp

@@ -6,11 +6,8 @@
    software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
    CONDITIONS OF ANY KIND, either express or implied.
 */
-
-
 #include "server_file.h"
 
-
 #include <stdio.h>
 #include <string.h>
 #include <string>
@@ -58,7 +55,6 @@ struct file_server_data {
     char scratch[SERVER_FILER_SCRATCH_BUFSIZE];
 };
 
-
 #include <iostream>
 #include <sys/types.h>
 #include <dirent.h>
@@ -67,11 +63,9 @@ using namespace std;
 
 string SUFFIX_ZW = "_0xge";
 
-
 static esp_err_t send_logfile(httpd_req_t *req, bool send_full_file);
 static esp_err_t send_datafile(httpd_req_t *req, bool send_full_file);
 
-
 esp_err_t get_numbers_file_handler(httpd_req_t *req)
 {
     std::string ret = flowctrl.getNumbersName();
@@ -87,7 +81,6 @@ esp_err_t get_numbers_file_handler(httpd_req_t *req)
     return ESP_OK;
 }
 
-
 esp_err_t get_data_file_handler(httpd_req_t *req)
 {
     struct dirent *entry;
@@ -131,7 +124,6 @@ esp_err_t get_data_file_handler(httpd_req_t *req)
     return ESP_OK;
 }
 
-
 esp_err_t get_tflite_file_handler(httpd_req_t *req)
 {
     struct dirent *entry;
@@ -175,12 +167,11 @@ esp_err_t get_tflite_file_handler(httpd_req_t *req)
     return ESP_OK;
 }
 
-
 /* Send HTTP response with a run-time generated html consisting of
  * a list of all files and folders under the requested path.
  * In case of SPIFFS this returns empty list when path is any
  * string other than '/', since SPIFFS doesn't support directories */
-static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath, const char* uripath, bool readonly)
+static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath, const char *uripath, bool readonly)
 {
     char entrypath[FILE_PATH_MAX];
     char entrysize[16];
@@ -192,82 +183,85 @@ static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath, const
     char dirpath_corrected[FILE_PATH_MAX];
     strcpy(dirpath_corrected, dirpath);
 
-    file_server_data * server_data = (file_server_data *) req->user_ctx;
-    if ((strlen(dirpath_corrected)-1) > strlen(server_data->base_path))      // if dirpath is not mountpoint, the last "\" needs to be removed
-        dirpath_corrected[strlen(dirpath_corrected)-1] = '\0';
+    file_server_data *server_data = (file_server_data *)req->user_ctx;
+
+    if ((strlen(dirpath_corrected) - 1) > strlen(server_data->base_path)) {
+        // if dirpath is not mountpoint, the last "\" needs to be removed
+        dirpath_corrected[strlen(dirpath_corrected) - 1] = '\0';
+    }
 
-    DIR *dir = opendir(dirpath_corrected);
+    DIR *pdir = opendir(dirpath_corrected);
 
     const size_t dirpath_len = strlen(dirpath);
     ESP_LOGD(TAG, "Dirpath: <%s>, Pathlength: %d", dirpath, dirpath_len);
 
-    /* Retrieve the base path of file storage to construct the full path */
+    // Retrieve the base path of file storage to construct the full path
     strlcpy(entrypath, dirpath, sizeof(entrypath));
     ESP_LOGD(TAG, "entrypath: <%s>", entrypath);
 
-    if (!dir) {
+    if (!pdir) {
         LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to stat dir: " + std::string(dirpath) + "!");
-        /* Respond with 404 Not Found */
+        // Respond with 404 Not Found
         httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, get404());
         return ESP_FAIL;
     }
 
     httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
 
-    /* Send HTML file header */
-    httpd_resp_sendstr_chunk(req, "<!DOCTYPE html><html><body>");
-
-/////////////////////////////////////////////////
-    if (!readonly) {
-        FILE *fd = fopen("/sdcard/html/file_server.html", "r");
-        char *chunk = ((struct file_server_data *)req->user_ctx)->scratch;
-        size_t chunksize;
-        do {
-            chunksize = fread(chunk, 1, SERVER_FILER_SCRATCH_BUFSIZE, fd);
-            //        ESP_LOGD(TAG, "Chunksize %d", chunksize);
-            if (chunksize > 0){
-                if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
-                fclose(fd);
-                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "File sending failed!");
-                return ESP_FAIL;
-                }
-            }
-        } while (chunksize != 0);
-        fclose(fd);
-        //    ESP_LOGI(TAG, "File sending complete");
-    }
-///////////////////////////////
+    // Send HTML file header
+    httpd_resp_sendstr_chunk(req, "<!DOCTYPE html><html lang=\"en\" xml:lang=\"en\"><head>");
+    httpd_resp_sendstr_chunk(req, "<link href=\"/file_server.css\" rel=\"stylesheet\">");
+    httpd_resp_sendstr_chunk(req, "<link href=\"/firework.css\" rel=\"stylesheet\">");
+    httpd_resp_sendstr_chunk(req, "<script type=\"text/javascript\" src=\"/jquery-3.6.0.min.js\"></script>");
+    httpd_resp_sendstr_chunk(req, "<script type=\"text/javascript\" src=\"/firework.js\"></script></head>");
+
+    httpd_resp_sendstr_chunk(req, "<body>");
+
+    httpd_resp_sendstr_chunk(req, "<table class=\"fixed\" border=\"0\" width=100% style=\"font-family: arial\">");
+    httpd_resp_sendstr_chunk(req, "<tr><td style=\"vertical-align: top;width: 300px;\"><h2>Fileserver</h2></td>"
+                                  "<td rowspan=\"2\"><table border=\"0\" style=\"width:100%\"><tr><td style=\"width:80px\">"
+                                  "<label for=\"newfile\">Source</label></td><td colspan=\"2\">"
+                                  "<input id=\"newfile\" type=\"file\" onchange=\"setpath()\" style=\"width:100%;\"></td></tr>"
+                                  "<tr><td><label for=\"filepath\">Destination</label></td><td>"
+                                  "<input id=\"filepath\" type=\"text\" style=\"width:94%;\"></td><td>"
+                                  "<button id=\"upload\" type=\"button\" class=\"button\" onclick=\"upload()\">Upload</button></td></tr>"
+                                  "</table></td></tr><tr></tr><tr><td colspan=\"2\">"
+                                  "<button style=\"font-size:16px; padding: 5px 10px\" id=\"dirup\" type=\"button\" onclick=\"dirup()\""
+                                  "disabled>&#129145; Directory up</button><span style=\"padding-left:15px\" id=\"currentpath\">"
+                                  "</span></td></tr>");
+    httpd_resp_sendstr_chunk(req, "</table>");
+
+    httpd_resp_sendstr_chunk(req, "<script type=\"text/javascript\" src=\"/file_server.js\"></script>");
+    httpd_resp_sendstr_chunk(req, "<script type=\"text/javascript\">initFileServer();</script>");
 
     std::string _zw = std::string(dirpath);
     _zw = _zw.substr(8, _zw.length() - 8);
-    _zw = "/delete/" + _zw + "?task=deldircontent"; 
+    _zw = "/delete/" + _zw + "?task=deldircontent";
 
+    // Send file-list table definition and column labels
+    httpd_resp_sendstr_chunk(req, "<table id=\"files_table\">"
+                                  "<col width=\"800px\"><col width=\"300px\"><col width=\"300px\"><col width=\"100px\">"
+                                  "<thead><tr><th>Name</th><th>Type</th><th>Size</th>");
 
-    /* Send file-list table definition and column labels */
-    httpd_resp_sendstr_chunk(req,
-        "<table id=\"files_table\">"
-        "<col width=\"800px\" /><col width=\"300px\" /><col width=\"300px\" /><col width=\"100px\" />"
-        "<thead><tr><th>Name</th><th>Type</th><th>Size</th>");
     if (!readonly) {
-        httpd_resp_sendstr_chunk(req, "<th>"
-            "<form method=\"post\" action=\"");
+        httpd_resp_sendstr_chunk(req, "<th><form method=\"post\" action=\"");
         httpd_resp_sendstr_chunk(req, _zw.c_str());
-        httpd_resp_sendstr_chunk(req,
-            "\"><button type=\"submit\">DELETE ALL!</button></form>"
-            "</th></tr>");
+        httpd_resp_sendstr_chunk(req, "\"><button type=\"submit\">DELETE ALL!</button></form></th></tr>");
     }
+
     httpd_resp_sendstr_chunk(req, "</thead><tbody>\n");
 
-    /* Iterate over all files / folders and fetch their names and sizes */
-    while ((entry = readdir(dir)) != NULL) {
-        if (strcmp("wlan.ini", entry->d_name) != 0 )        // wlan.ini soll nicht angezeigt werden!
-        {
+    // Iterate over all files / folders and fetch their names and sizes
+    while ((entry = readdir(pdir)) != NULL) {
+        // wlan.ini soll nicht angezeigt werden!
+        if (strcmp("wlan.ini", entry->d_name) != 0) {
             entrytype = (entry->d_type == DT_DIR ? "directory" : "file");
 
             strlcpy(entrypath + dirpath_len, entry->d_name, sizeof(entrypath) - dirpath_len);
             ESP_LOGD(TAG, "Entrypath: %s", entrypath);
+
             if (stat(entrypath, &entry_stat) == -1) {
-                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to stat " + string(entrytype) + ": " + string(entry->d_name));
+                LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Failed to stat " + std::string(entrytype) + ": " + std::string(entry->d_name));
                 continue;
             }
 
@@ -283,22 +277,25 @@ static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath, const
                 }
             }
 
-            ESP_LOGI(TAG, "Found %s: %s (%s bytes)", entrytype, entry->d_name, entrysize);
+            ESP_LOGD(TAG, "Found %s: %s (%s bytes)", entrytype, entry->d_name, entrysize);
 
-            /* Send chunk of HTML file containing table entries with file name and size */
+            // Send chunk of HTML file containing table entries with file name and size
             httpd_resp_sendstr_chunk(req, "<tr><td><a href=\"");
             httpd_resp_sendstr_chunk(req, "/fileserver");
             httpd_resp_sendstr_chunk(req, uripath);
             httpd_resp_sendstr_chunk(req, entry->d_name);
+
             if (entry->d_type == DT_DIR) {
                 httpd_resp_sendstr_chunk(req, "/");
             }
+
             httpd_resp_sendstr_chunk(req, "\">");
             httpd_resp_sendstr_chunk(req, entry->d_name);
             httpd_resp_sendstr_chunk(req, "</a></td><td>");
             httpd_resp_sendstr_chunk(req, entrytype);
             httpd_resp_sendstr_chunk(req, "</td><td>");
             httpd_resp_sendstr_chunk(req, entrysize);
+
             if (!readonly) {
                 httpd_resp_sendstr_chunk(req, "</td><td>");
                 httpd_resp_sendstr_chunk(req, "<form method=\"post\" action=\"/delete");
@@ -306,31 +303,28 @@ static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath, const
                 httpd_resp_sendstr_chunk(req, entry->d_name);
                 httpd_resp_sendstr_chunk(req, "\"><button type=\"submit\">Delete</button></form>");
             }
+
             httpd_resp_sendstr_chunk(req, "</td></tr>\n");
         }
     }
-    closedir(dir);
 
-    /* Finish the file list table */
+    closedir(pdir);
+
+    // Finish the file list table
     httpd_resp_sendstr_chunk(req, "</tbody></table>");
 
-    /* Send remaining chunk of HTML file to complete it */
+    // Send remaining chunk of HTML file to complete it
     httpd_resp_sendstr_chunk(req, "</body></html>");
 
-    /* Send empty chunk to signal HTTP response completion */
+    // Send empty chunk to signal HTTP response completion
     httpd_resp_sendstr_chunk(req, NULL);
     return ESP_OK;
 }
-/*
-#define IS_FILE_EXT(filename, ext) \
-    (strcasecmp(&filename[strlen(filename) - sizeof(ext) + 1], ext) == 0)
-*/
 
 static esp_err_t logfileact_get_full_handler(httpd_req_t *req) {
     return send_logfile(req, true);
 }
 
-
 static esp_err_t logfileact_get_last_part_handler(httpd_req_t *req) {
     return send_logfile(req, false);
 }
@@ -339,7 +333,6 @@ static esp_err_t datafileact_get_full_handler(httpd_req_t *req) {
     return send_datafile(req, true);
 }
 
-
 static esp_err_t datafileact_get_last_part_handler(httpd_req_t *req) {
     return send_datafile(req, false);
 }
@@ -424,7 +417,6 @@ static esp_err_t send_datafile(httpd_req_t *req, bool send_full_file)
     return ESP_OK;
 }
 
-
 static esp_err_t send_logfile(httpd_req_t *req, bool send_full_file)
 {
     LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "log_get_last_part_handler");
@@ -510,7 +502,6 @@ static esp_err_t send_logfile(httpd_req_t *req, bool send_full_file)
     return ESP_OK;
 }
 
-
 /* Handler to download a file kept on the server */
 static esp_err_t download_get_handler(httpd_req_t *req)
 {
@@ -528,7 +519,6 @@ static esp_err_t download_get_handler(httpd_req_t *req)
 //    filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
 //                                             req->uri, sizeof(filepath));
 
-
     if (!filename) {
         LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Filename is too long");
         /* Respond with 414 Error */
@@ -759,7 +749,6 @@ static esp_err_t upload_post_handler(httpd_req_t *req)
     httpd_resp_set_hdr(req, "Location", directory.c_str());
     httpd_resp_sendstr(req, "File uploaded successfully");
 
-
     return ESP_OK;
 }
 
@@ -770,7 +759,6 @@ static esp_err_t delete_post_handler(httpd_req_t *req)
     char filepath[FILE_PATH_MAX];
     struct stat file_stat;
 
-
 //////////////////////////////////////////////////////////////
     char _query[200];
     char _valuechar[30];    
@@ -893,13 +881,11 @@ static esp_err_t delete_post_handler(httpd_req_t *req)
         }
     }
 
-
     httpd_resp_set_hdr(req, "Location", directory.c_str());
     httpd_resp_sendstr(req, "File successfully deleted");
     return ESP_OK;
 }
 
-
 void delete_all_in_directory(std::string _directory)
 {
     struct dirent *entry;
@@ -1137,8 +1123,6 @@ void unzip(std::string _in_zip_file, std::string _target_directory){
     ESP_LOGD(TAG, "Success.");
 }
 
-
-
 void register_server_file_uri(httpd_handle_t server, const char *base_path)
 {
     static struct file_server_data *server_data = NULL;
@@ -1164,8 +1148,6 @@ void register_server_file_uri(httpd_handle_t server, const char *base_path)
     strlcpy(server_data->base_path, base_path,
             sizeof(server_data->base_path));
 
-
-
     /* URI handler for getting uploaded files */
 //    char zw[sizeof(serverprefix)+1];
 //    strcpy(zw, serverprefix);
@@ -1180,7 +1162,6 @@ void register_server_file_uri(httpd_handle_t server, const char *base_path)
     };
     httpd_register_uri_handler(server, &file_download);
 
-
     httpd_uri_t file_datafileact = {
         .uri       = "/datafileact",  // Match all URIs of type /path/to/file
         .method    = HTTP_GET,
@@ -1189,7 +1170,6 @@ void register_server_file_uri(httpd_handle_t server, const char *base_path)
     };
     httpd_register_uri_handler(server, &file_datafileact);
 
-
     httpd_uri_t file_datafile_last_part_handle = {
         .uri       = "/data",  // Match all URIs of type /path/to/file
         .method    = HTTP_GET,
@@ -1206,7 +1186,6 @@ void register_server_file_uri(httpd_handle_t server, const char *base_path)
     };
     httpd_register_uri_handler(server, &file_logfileact);
 
-
     httpd_uri_t file_logfile_last_part_handle = {
         .uri       = "/log",  // Match all URIs of type /path/to/file
         .method    = HTTP_GET,
@@ -1215,7 +1194,6 @@ void register_server_file_uri(httpd_handle_t server, const char *base_path)
     };
     httpd_register_uri_handler(server, &file_logfile_last_part_handle);
 
-
     /* URI handler for uploading files to server */
     httpd_uri_t file_upload = {
         .uri       = "/upload/*",   // Match all URIs of type /upload/path/to/file
@@ -1233,5 +1211,4 @@ void register_server_file_uri(httpd_handle_t server, const char *base_path)
         .user_ctx  = server_data    // Pass server data as context
     };
     httpd_register_uri_handler(server, &file_delete);
-
 }

+ 59 - 0
code/components/openmetrics/openmetrics.cpp

@@ -1,4 +1,6 @@
 #include "openmetrics.h"
+#include "functional"
+#include "esp_log.h"
 
 /**
  * create a singe metric from the given input
@@ -10,10 +12,66 @@ std::string createMetric(const std::string &metricName, const std::string &help,
            metricName + " " + value + "\n";
 }
 
+typedef struct sequence_metric {
+    const char *name;
+    const char *help;
+    const char *type;
+    std::function<std::string(NumberPost *number)> valueFunc;
+} sequence_metric_t;
+
+
+sequence_metric_t sequenceMetrics[4] = {
+    { "flow_value",     "current value of meter readout",     "gauge", [](NumberPost *number)-> std::string {return number->ReturnValue;} },
+    { "flow_raw_value", "current raw value of meter readout", "gauge", [](NumberPost *number)-> std::string {return number->ReturnRawValue;} },
+    { "flow_pre_value", "previous value of meter readout",    "gauge", [](NumberPost *number)-> std::string {return number->ReturnPreValue;} },
+    { "flow_error",     "Error message text != 'no error'",   "gauge", [](NumberPost *number)-> std::string {return number->ErrorMessageText.compare("no error") == 0 ? "0" : "1";} },
+};
+
+std::string createSequenceMetrics(std::string prefix, const std::vector<NumberPost *> &numbers)
+{
+    std::string result;
+    for (int i = 0; i<sizeof(sequenceMetrics)/sizeof(sequence_metric_t);i++) 
+    {
+        std::string res;
+        for (const auto &number : numbers)
+        {
+            std::string value = sequenceMetrics[i].valueFunc(number); 
+            if (value.find("N") != std::string::npos) {
+                value = "NaN";
+            }
+            ESP_LOGD("METRICS", "metric=%s, name=%s, value = %s ",sequenceMetrics[i].name,number->name.c_str(), value.c_str());
+
+            // only valid data is reported (https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#missing-data)
+            if (value.length() > 0)
+            {
+                auto label = number->name;
+                // except newline, double quote, and backslash (https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#abnf)
+                // to keep it simple, these characters are just removed from the label
+                replaceAll(label, "\\", "");
+                replaceAll(label, "\"", "");
+                replaceAll(label, "\n", "");
+
+                res += prefix + "_" + sequenceMetrics[i].name + "{sequence=\"" + label + "\"} " + value + "\n";
+            }
+        }
+        // prepend metadata if a valid metric was created
+        if (res.length() > 0)
+        {
+            res = "# HELP " + prefix + "_" + sequenceMetrics[i].name + " " + sequenceMetrics[i].help + "\n"
+                + "# TYPE " + prefix + "_" + sequenceMetrics[i].name + " " + sequenceMetrics[i].type + "\n"
+                + res;
+        }
+        result += res;
+    }
+
+    return result;
+}
+
 /**
  * Generate the MetricFamily from all available sequences
  * @returns the string containing the text wire format of the MetricFamily
  **/
+/*
 std::string createSequenceMetrics(std::string prefix, const std::vector<NumberPost *> &numbers)
 {
     std::string res;
@@ -41,3 +99,4 @@ std::string createSequenceMetrics(std::string prefix, const std::vector<NumberPo
     }
     return res;
 }
+*/

+ 41 - 6
code/test/components/openmetrics/test_openmetrics.cpp

@@ -37,23 +37,58 @@ void test_createSequenceMetrics()
     NumberPost *number_1 = new NumberPost;
     number_1->name = "main";
     number_1->ReturnValue = "123.456";
+    number_1->ReturnRawValue = "N23.456";
+    number_1->ReturnPreValue = "986.543";
+    number_1->ErrorMessageText = "";
     NUMBERS.push_back(number_1);
 
     const std::string metricNamePrefix = "ai_on_the_edge_device";
-    const std::string metricName = metricNamePrefix + "_flow_value";
+    const std::string metricName1 = metricNamePrefix + "_flow_value";
+    const std::string metricName2 = metricNamePrefix + "_flow_raw_value";
+    const std::string metricName3 = metricNamePrefix + "_flow_pre_value";
+    const std::string metricName4 = metricNamePrefix + "_flow_error";
+
+    std::string expected1 ;
+    expected1 = "# HELP " + metricName1 + " current value of meter readout\n# TYPE " + metricName1 + " gauge\n" +
+                metricName1 + "{sequence=\"" + number_1->name + "\"} " + number_1->ReturnValue + "\n";
+    
+    expected1 += "# HELP " + metricName2 + " current raw value of meter readout\n# TYPE " + metricName2 + " gauge\n" +
+                metricName2 + "{sequence=\"" + number_1->name + "\"} " + "NaN" + "\n";
+
+    expected1 += "# HELP " + metricName3 + " previous value of meter readout\n# TYPE " + metricName3 + " gauge\n" +
+                metricName3 + "{sequence=\"" + number_1->name + "\"} " + number_1->ReturnPreValue + "\n";
+    
+    expected1 += "# HELP " + metricName4 + " Error message text != 'no error'\n# TYPE " + metricName4 + " gauge\n" +
+                metricName4 + "{sequence=\"" + number_1->name + "\"} " + "1" + "\n";
 
-    std::string expected1 = "# HELP " + metricName + " current value of meter readout\n# TYPE " + metricName + " gauge\n" +
-                             metricName + "{sequence=\"" + number_1->name + "\"} " + number_1->ReturnValue + "\n";
     TEST_ASSERT_EQUAL_STRING(expected1.c_str(), createSequenceMetrics(metricNamePrefix, NUMBERS).c_str());
 
     NumberPost *number_2 = new NumberPost;
     number_2->name = "secondary";
     number_2->ReturnValue = "1.0";
+    number_2->ReturnRawValue = "01.000";
+    number_2->ReturnPreValue = "0.987";
+    number_2->ErrorMessageText = "no error";
     NUMBERS.push_back(number_2);
 
-    std::string expected2 = "# HELP " + metricName + " current value of meter readout\n# TYPE " + metricName + " gauge\n" +
-                             metricName + "{sequence=\"" + number_1->name + "\"} " + number_1->ReturnValue + "\n" +
-                             metricName + "{sequence=\"" + number_2->name + "\"} " + number_2->ReturnValue + "\n";
+    std::string expected2 ;
+    expected2 = "# HELP " + metricName1 + " current value of meter readout\n# TYPE " + metricName1 + " gauge\n" +
+                metricName1 + "{sequence=\"" + number_1->name + "\"} " + number_1->ReturnValue + "\n" +
+                metricName1 + "{sequence=\"" + number_2->name + "\"} " + number_2->ReturnValue + "\n";
+    
+    expected2 += "# HELP " + metricName2 + " current raw value of meter readout\n# TYPE " + metricName2 + " gauge\n" +
+                metricName2 + "{sequence=\"" + number_1->name + "\"} " + "NaN" + "\n" +
+                metricName2 + "{sequence=\"" + number_2->name + "\"} " + number_2->ReturnRawValue + "\n";
+
+    expected2 += "# HELP " + metricName3 + " previous value of meter readout\n# TYPE " + metricName3 + " gauge\n" +
+                metricName3 + "{sequence=\"" + number_1->name + "\"} " + number_1->ReturnPreValue + "\n" +
+                metricName3 + "{sequence=\"" + number_2->name + "\"} " + number_2->ReturnPreValue + "\n";
+
+    expected2 += "# HELP " + metricName4 + " Error message text != 'no error'\n# TYPE " + metricName4 + " gauge\n" +
+                metricName4 + "{sequence=\"" + number_1->name + "\"} " + "1" + "\n" +
+                metricName4 + "{sequence=\"" + number_2->name + "\"} " + "0" + "\n";
+
+    
     TEST_ASSERT_EQUAL_STRING(expected2.c_str(), createSequenceMetrics(metricNamePrefix, NUMBERS).c_str());
 }
 

+ 222 - 222
sd-card/html/backup.html

@@ -1,222 +1,222 @@
-<!DOCTYPE html>
-<html lang="en" xml:lang="en"> 
-<head>
-    <title>Backup/Restore Configuration</title>
-    <meta charset="UTF-8" />
-
-    <style>
-        h1 {font-size: 2em;}
-        h2 {font-size: 1.5em; margin-block-start: 0.0em; margin-block-end: 0.2em;}
-        h3 {font-size: 1.2em;}
-        p {font-size: 1em;}
-
-        input[type=number] {
-            width: 138px;
-            padding: 10px 5px;
-            display: inline-block;
-            border: 1px solid #ccc;
-            font-size: 16px; 
-        }
-
-        .button {
-            padding: 5px 10px;
-            width: 205px;
-            font-size: 16px;
-        }
-    </style>
-
-</head>
-
-<body style="font-family: arial; padding: 0px 10px;">
-    <h2>Backup Configuration</h2>
-    <p>With the following action the <a href="/fileserver/config/" target="_self">config</a> folder on the SD-card gets zipped and provided as a download.</p>
-
-    <button class="button" id="startBackup" type="button" onclick="startBackup()">Create Backup</button>
-    <p id=progress></p>
-    <hr>
-    <h2>Restore Configuration</h2>
-    <p>Use the <a href="/fileserver/config/" target="_self">File Server</a> to upload individual files.</p>
-</body>
-
-
-<script type="text/javascript" src="common.js?v=$COMMIT_HASH"></script>
-<script type="text/javascript" src="jszip.min.js?v=$COMMIT_HASH"></script>
-<script type="text/javascript" src="FileSaver.min.js?v=$COMMIT_HASH"></script>
-<script>
-
-function startBackup() {  
-    document.getElementById("progress").innerHTML = "Creating backup...<br>\n";
-    
-    // Get hostname
-    try {
-        var xhttp = new XMLHttpRequest();
-        xhttp.open("GET", getDomainname() + "/info?type=Hostname", false);
-        xhttp.send();
-        hostname = xhttp.responseText;
-    }
-    catch(err) {
-        setStatus("<span style=\"color: red\">Failed to fetch hostname: " + err.message + "!</span>");
-        return;
-    }
-    
-    // get date/time
-    var dateTime = new Date().toJSON().slice(0,10) + "_" + new Date().toJSON().slice(11,19).replaceAll(":", "-");
-    
-    zipFilename = hostname + "_" + dateTime + ".zip";
-    console.log(zipFilename);
-
-    // Get files list
-    setStatus("Fetching File List...");
-    try {
-        var xhttp = new XMLHttpRequest();
-        xhttp.open("GET", getDomainname() + "/fileserver/config/", false);
-        xhttp.send();
-        
-        var parser = new DOMParser();
-        var content = parser.parseFromString(xhttp.responseText, 'text/html');    }
-    catch(err) {
-        setStatus("Failed to fetch files list: " + err.message);
-        return;
-    }
-    
-    const list = content.querySelectorAll("a");
-    
-    var urls = [];
-    
-    for (a of list) {
-        url = a.getAttribute("href");
-        urls.push(getDomainname() + url);
-    }
-    
-    // Pack as zip and download
-    try {
-        backup(urls, zipFilename);
-        }
-    catch(err) {
-        setStatus("<span style=\"color: red\">Failed to zip files: " + err.message + "!</span>");
-        return;
-    }
-}
-
-
-function fetchFiles(urls, filesData, index, retry, zipFilename) {
-    url = urls[index];
-
-//    console.log(url + " started (" + index + "/" + urls.length + ")");
-    if (retry == 0) {
-        setStatus("&nbsp;- " + getFilenameFromUrl(urls[index]) + " (" + (index+1) + "/" + urls.length + ")...");
-    }
-    else {
-        setStatus("<span style=\"color: gray\">&nbsp;&nbsp;&nbsp;Retrying (" + retry + ")...</span>");
-    }
-
-    const xhr = new XMLHttpRequest();
-    xhr.open('GET', url, true);
-    xhr.responseType = "blob";
-
-    if (retry == 0) { // Short timeout on first retry
-        xhr.timeout = 2000; // time in milliseconds
-    }
-    else if (retry == 1) { // longer timeout
-        xhr.timeout = 5000; // time in milliseconds
-    }
-    else if (retry == 2) { // longer timeout
-        xhr.timeout = 10000; // time in milliseconds
-    }
-    else if (retry == 3) { // longer timeout
-        xhr.timeout = 20000; // time in milliseconds
-    }
-    else { // very long timeout
-        xhr.timeout = 30000; // time in milliseconds
-    }
-
-    xhr.onload = () => { // Request finished
-        //console.log(url + " done");
-
-        filesData[index] = xhr.response;
-
-        if (index == urls.length - 1) {
-            setStatus("Fetched all files");
-            generateZipFile(urls, filesData, zipFilename);
-            return;
-        }
-        else { // Next file
-            fetchFiles(urls, filesData, index+1, 0, zipFilename);
-        }
-    };
-
-    xhr.onprogress = (e) => { // XMLHttpRequest progress ... extend timeout
-        xhr.timeout = xhr.timeout + 500;
-    };
-
-    xhr.onerror = (e) => { // XMLHttpRequest error loading
-        console.log("Error on fetching " + url + "!");
-        if (retry > 5) {
-            setStatus("<span style=\"color: red\">Backup failed, please restart the device and try again!</span>");
-        }
-        else {
-            fetchFiles(urls, filesData, index, retry+1, zipFilename);
-        }
-    };
-
-    xhr.ontimeout = (e) => { // XMLHttpRequest timed out
-        console.log("Timeout on fetching " + url + "!");
-        if (retry > 5) {
-            setStatus("<span style=\"color: red\">Backup failed, please restart the device and try again!</span>");
-        }
-        else {
-            fetchFiles(urls, filesData, index, retry+1, zipFilename);
-        }
-    };
-
-    xhr.send(null);
-}
-
-
-function generateZipFile(urls, filesData, zipFilename) {
-    setStatus("Creating Zip File...");
-
-    var zip = new JSZip();
-
-    for (var i = 0; i < urls.length; i++) {        
-        zip.file(getFilenameFromUrl(urls[i]), filesData[i]);
-    }
-
-    zip.generateAsync({type:"blob"})
-    .then(function(content) {
-        saveAs(content, zipFilename);
-    });
-
-    setStatus("Backup completed");
-}
-
-
-const backup = (urls, zipFilename) => {
-    if(!urls) return;
-
-    /* Testing */
-    /*len = urls.length;
-    for (i = 0; i < len - 3; i++) {
-        urls.pop();
-    }*/
-
-    console.log(urls);
-
-    urlIndex = 0;
-    setStatus("Fetching files...");
-    fetchFiles(urls, [], 0, 0, zipFilename);
-};
-
-
-function setStatus(status) {
-    console.log(status);
-    document.getElementById("progress").innerHTML += status + "<br>\n";
-}
-
-function getFilenameFromUrl(url) {
-    return filename = url.substring(url.lastIndexOf('/')+1);
-}
-
-</script>
-
-</html>
+<!DOCTYPE html>
+<html lang="en" xml:lang="en"> 
+<head>
+    <title>Backup/Restore Configuration</title>
+    <meta charset="UTF-8" />
+
+    <style>
+        h1 {font-size: 2em;}
+        h2 {font-size: 1.5em; margin-block-start: 0.0em; margin-block-end: 0.2em;}
+        h3 {font-size: 1.2em;}
+        p {font-size: 1em;}
+
+        input[type=number] {
+            width: 138px;
+            padding: 10px 5px;
+            display: inline-block;
+            border: 1px solid #ccc;
+            font-size: 16px; 
+        }
+
+        .button {
+            padding: 5px 10px;
+            width: 205px;
+            font-size: 16px;
+        }
+    </style>
+
+</head>
+
+<body style="font-family: arial; padding: 0px 10px;">
+    <h2>Backup Configuration</h2>
+    <p>With the following action the <a href="/fileserver/config/" target="_self">config</a> folder on the SD-card gets zipped and provided as a download.</p>
+
+    <button class="button" id="startBackup" type="button" onclick="startBackup()">Create Backup</button>
+    <p id=progress></p>
+    <hr>
+    <h2>Restore Configuration</h2>
+    <p>Use the <a href="/fileserver/config/" target="_self">File Server</a> to upload individual files.</p>
+</body>
+
+
+<script type="text/javascript" src="common.js?v=$COMMIT_HASH"></script>
+<script type="text/javascript" src="jszip.min.js?v=$COMMIT_HASH"></script>
+<script type="text/javascript" src="FileSaver.min.js?v=$COMMIT_HASH"></script>
+<script>
+
+function startBackup() {  
+    document.getElementById("progress").innerHTML = "Creating backup...<br>\n";
+    
+    // Get hostname
+    try {
+        var xhttp = new XMLHttpRequest();
+        xhttp.open("GET", getDomainname() + "/info?type=Hostname", false);
+        xhttp.send();
+        hostname = xhttp.responseText;
+    }
+    catch(err) {
+        setStatus("<span style=\"color: red\">Failed to fetch hostname: " + err.message + "!</span>");
+        return;
+    }
+    
+    // get date/time
+    var dateTime = new Date().toJSON().slice(0,10) + "_" + new Date().toJSON().slice(11,19).replaceAll(":", "-");
+    
+    zipFilename = hostname + "_" + dateTime + ".zip";
+    console.log(zipFilename);
+
+    // Get files list
+    setStatus("Fetching File List...");
+    try {
+        var xhttp = new XMLHttpRequest();
+        xhttp.open("GET", getDomainname() + "/fileserver/config/", false);
+        xhttp.send();
+        
+        var parser = new DOMParser();
+        var content = parser.parseFromString(xhttp.responseText, 'text/html');    }
+    catch(err) {
+        setStatus("Failed to fetch files list: " + err.message);
+        return;
+    }
+    
+    const list = content.querySelectorAll("a");
+    
+    var urls = [];
+    
+    for (a of list) {
+        url = a.getAttribute("href");
+        urls.push(getDomainname() + url);
+    }
+    
+    // Pack as zip and download
+    try {
+        backup(urls, zipFilename);
+        }
+    catch(err) {
+        setStatus("<span style=\"color: red\">Failed to zip files: " + err.message + "!</span>");
+        return;
+    }
+}
+
+
+function fetchFiles(urls, filesData, index, retry, zipFilename) {
+    url = urls[index];
+
+//    console.log(url + " started (" + index + "/" + urls.length + ")");
+    if (retry == 0) {
+        setStatus("&nbsp;- " + getFilenameFromUrl(urls[index]) + " (" + (index+1) + "/" + urls.length + ")...");
+    }
+    else {
+        setStatus("<span style=\"color: gray\">&nbsp;&nbsp;&nbsp;Retrying (" + retry + ")...</span>");
+    }
+
+    const xhr = new XMLHttpRequest();
+    xhr.open('GET', url, true);
+    xhr.responseType = "blob";
+
+    if (retry == 0) { // Short timeout on first retry
+        xhr.timeout = 2000; // time in milliseconds
+    }
+    else if (retry == 1) { // longer timeout
+        xhr.timeout = 5000; // time in milliseconds
+    }
+    else if (retry == 2) { // longer timeout
+        xhr.timeout = 10000; // time in milliseconds
+    }
+    else if (retry == 3) { // longer timeout
+        xhr.timeout = 20000; // time in milliseconds
+    }
+    else { // very long timeout
+        xhr.timeout = 30000; // time in milliseconds
+    }
+
+    xhr.onload = () => { // Request finished
+        //console.log(url + " done");
+
+        filesData[index] = xhr.response;
+
+        if (index == urls.length - 1) {
+            setStatus("Fetched all files");
+            generateZipFile(urls, filesData, zipFilename);
+            return;
+        }
+        else { // Next file
+            fetchFiles(urls, filesData, index+1, 0, zipFilename);
+        }
+    };
+
+    xhr.onprogress = (e) => { // XMLHttpRequest progress ... extend timeout
+        xhr.timeout = xhr.timeout + 500;
+    };
+
+    xhr.onerror = (e) => { // XMLHttpRequest error loading
+        console.log("Error on fetching " + url + "!");
+        if (retry > 5) {
+            setStatus("<span style=\"color: red\">Backup failed, please restart the device and try again!</span>");
+        }
+        else {
+            fetchFiles(urls, filesData, index, retry+1, zipFilename);
+        }
+    };
+
+    xhr.ontimeout = (e) => { // XMLHttpRequest timed out
+        console.log("Timeout on fetching " + url + "!");
+        if (retry > 5) {
+            setStatus("<span style=\"color: red\">Backup failed, please restart the device and try again!</span>");
+        }
+        else {
+            fetchFiles(urls, filesData, index, retry+1, zipFilename);
+        }
+    };
+
+    xhr.send(null);
+}
+
+
+function generateZipFile(urls, filesData, zipFilename) {
+    setStatus("Creating Zip File...");
+
+    var zip = new JSZip();
+
+    for (var i = 0; i < urls.length; i++) {        
+        zip.file(getFilenameFromUrl(urls[i]), filesData[i]);
+    }
+
+    zip.generateAsync({type:"blob"})
+    .then(function(content) {
+        saveAs(content, zipFilename);
+    });
+
+    setStatus("Backup completed");
+}
+
+
+const backup = (urls, zipFilename) => {
+    if(!urls) return;
+
+    /* Testing */
+    /*len = urls.length;
+    for (i = 0; i < len - 3; i++) {
+        urls.pop();
+    }*/
+
+    console.log(urls);
+
+    urlIndex = 0;
+    setStatus("Fetching files...");
+    fetchFiles(urls, [], 0, 0, zipFilename);
+};
+
+
+function setStatus(status) {
+    console.log(status);
+    document.getElementById("progress").innerHTML += status + "<br>\n";
+}
+
+function getFilenameFromUrl(url) {
+    return filename = url.substring(url.lastIndexOf('/')+1);
+}
+
+</script>
+
+</html>

+ 207 - 0
sd-card/html/data_export.html

@@ -0,0 +1,207 @@
+<!DOCTYPE html>
+<html lang="en" xml:lang="en"> 
+<head>
+    <title>Data Export (CSV)</title>
+    <meta charset="UTF-8"/>
+
+    <style>
+        h1 {font-size: 2em;}
+        h2 {font-size: 1.5em; margin-block-start: 0.0em; margin-block-end: 0.2em;}
+        h3 {font-size: 1.2em;}
+        p {font-size: 1em;}
+
+        input[type=number] {
+            width: 138px;
+            padding: 10px 5px;
+            display: inline-block;
+            border: 1px solid #ccc;
+            font-size: 16px; 
+        }
+
+        .button {
+            padding: 5px 10px;
+            width: 300px;
+            font-size: 16px;
+        }
+    </style>
+</head>
+
+<body style="font-family: arial; padding: 0px 10px;">
+    <h2>Data Export(CSV)</h2>
+    <p>With the following action the <a href="/fileserver/log/data/" target="_self">data</a> folder on the SD-card gets zipped and provided as a download.</p>
+    <button class="button" id="startExportData" type="button" onclick="startExportData()">Export Data</button>
+    <p></p>
+    <hr>
+    <p id=progress></p>
+
+<script type="text/javascript" src="common.js?v=$COMMIT_HASH"></script>
+<script type="text/javascript" src="jszip.min.js?v=$COMMIT_HASH"></script>
+<script type="text/javascript" src="FileSaver.min.js?v=$COMMIT_HASH"></script>
+
+<script type="text/javascript">
+function startExportData() {  
+    document.getElementById("progress").innerHTML = "Creating Export Data...<br>\n";
+    
+    // Get hostname
+    try {
+        var xhttp = new XMLHttpRequest();
+        xhttp.open("GET", getDomainname() + "/info?type=Hostname", false);
+        xhttp.send();
+        hostname = xhttp.responseText;
+    }
+    catch(err) {
+        setStatus("<span style=\"color: red\">Failed to fetch hostname: " + err.message + "!</span>");
+        return;
+    }
+    
+    // get date/time
+    var dateTime = new Date().toJSON().slice(0,10) + "_" + new Date().toJSON().slice(11,19).replaceAll(":", "-");
+    
+    zipFilename = hostname + "_" + dateTime + "_csv" + ".zip";
+    console.log(zipFilename);
+
+    // Get files list
+    setStatus("Fetching File List...");
+    try {
+        var xhttp = new XMLHttpRequest();
+        xhttp.open("GET", getDomainname() + "/fileserver/log/data/", false);
+        xhttp.send();
+        
+        var parser = new DOMParser();
+        var content = parser.parseFromString(xhttp.responseText, 'text/html');
+    }
+    catch(err) {
+        setStatus("Failed to fetch files list: " + err.message);
+        return;
+    }
+    
+    const list = content.querySelectorAll("a");
+    
+    var urls = [];
+    
+    for (a of list) {
+        url = a.getAttribute("href");
+        urls.push(getDomainname() + url);
+    }
+    
+    // Pack as zip and download
+    try {
+        startExportZip(urls, zipFilename);
+        }
+    catch(err) {
+        setStatus("<span style=\"color: red\">Failed to zip files: " + err.message + "!</span>");
+        return;
+    }
+}
+
+function fetchFiles(urls, filesData, index, retry, zipFilename) {
+    url = urls[index];
+
+    if (retry == 0) {
+        setStatus("&nbsp;- " + getFilenameFromUrl(urls[index]) + " (" + (index+1) + "/" + urls.length + ")...");
+    }
+    else {
+        setStatus("<span style=\"color: gray\">&nbsp;&nbsp;&nbsp;Retrying (" + retry + ")...</span>");
+    }
+
+    const xhr = new XMLHttpRequest();
+    xhr.open('GET', url, true);
+    xhr.responseType = "blob";
+
+    if (retry == 0) { // Short timeout on first retry
+        xhr.timeout = 2000; // time in milliseconds
+    }
+    else if (retry == 1) { // longer timeout
+        xhr.timeout = 5000;
+    }
+    else if (retry == 2) { // longer timeout
+        xhr.timeout = 10000;
+    }
+    else if (retry == 3) { // longer timeout
+        xhr.timeout = 20000;
+    }
+    else { // very long timeout
+        xhr.timeout = 30000;
+    }
+
+    xhr.onload = () => { // Request finished
+        //console.log(url + " done");
+
+        filesData[index] = xhr.response;
+
+        if (index == urls.length - 1) {
+            setStatus("Fetched all files");
+            generateZipFile(urls, filesData, zipFilename);
+            return;
+        }
+        else { // Next file
+            fetchFiles(urls, filesData, index+1, 0, zipFilename);
+        }
+    };
+
+    xhr.onprogress = (e) => { // XMLHttpRequest progress ... extend timeout
+        xhr.timeout = xhr.timeout + 500;
+    };
+
+    xhr.onerror = (e) => { // XMLHttpRequest error loading
+        console.log("Error on fetching " + url + "!");
+        if (retry > 5) {
+            setStatus("<span style=\"color: red\">Data Export failed, please restart the device and try again!</span>");
+        }
+        else {
+            fetchFiles(urls, filesData, index, retry+1, zipFilename);
+        }
+    };
+
+    xhr.ontimeout = (e) => { // XMLHttpRequest timed out
+        console.log("Timeout on fetching " + url + "!");
+        if (retry > 5) {
+            setStatus("<span style=\"color: red\">Data Export failed, please restart the device and try again!</span>");
+        }
+        else {
+            fetchFiles(urls, filesData, index, retry+1, zipFilename);
+        }
+    };
+
+    xhr.send(null);
+}
+
+function generateZipFile(urls, filesData, zipFilename) {
+    setStatus("Creating Zip File...");
+
+    var zip = new JSZip();
+
+    for (var i = 0; i < urls.length; i++) {        
+        zip.file(getFilenameFromUrl(urls[i]), filesData[i]);
+    }
+
+    zip.generateAsync({type:"blob"})
+    .then(function(content) {
+        saveAs(content, zipFilename);
+    });
+
+    setStatus("Data Export completed");
+}
+
+const startExportZip = (urls, zipFilename) => {
+    if(!urls) { return; }
+
+    console.log(urls);
+
+    urlIndex = 0;
+    setStatus("Fetching files...");
+    fetchFiles(urls, [], 0, 0, zipFilename);
+};
+
+function setStatus(status) {
+    console.log(status);
+    document.getElementById("progress").innerHTML += status + "<br>\n";
+}
+
+function getFilenameFromUrl(url) {
+    return filename = url.substring(url.lastIndexOf('/')+1);
+}
+
+</script>
+</body>
+</html>

+ 50 - 45
sd-card/html/edit_alignment.html

@@ -79,6 +79,18 @@
             transform: translate(-50%,-50%);
             -ms-transform: translate(-50%,-50%);
         }
+        
+        #reboot_button {
+            float: none;
+            background-color: #f44336;
+            color: white;
+            padding: 5px;
+            border-radius:
+            5px; font-weight: bold;
+            text-align: center;
+            text-decoration: none;
+            display: inline-block;
+        }
     </style>
 
     <link href="firework.css?v=$COMMIT_HASH" rel="stylesheet">
@@ -188,13 +200,11 @@
             param;
     
         function doReboot() {
-            if (confirm("Are you sure you want to reboot? Did you save your changes?")) {
-                var stringota = domainname + "/reboot";
-                window.location = stringota;
-                window.location.href = stringota;
-                window.location.assign(stringota);
-                window.location.replace(stringota);
-            }
+            var stringota = domainname + "/reboot";
+            window.location = stringota;
+            window.location.href = stringota;
+            window.location.assign(stringota);
+            window.location.replace(stringota);
         }
     
         function ChangeSelection(){
@@ -215,50 +225,45 @@
             document.getElementById("overlay").style.display = "block";
             document.getElementById("overlaytext").innerHTML = "Save Alignment Marker...";
 
-            if (confirm("Are you sure you want to save the new alignment marker configuration?")) {
-                function sleep(ms) {
-                    return new Promise(resolve => setTimeout(resolve, ms));
-                }
+            function sleep(ms) {
+                return new Promise(resolve => setTimeout(resolve, ms));
+            }
 
-                async function task() {
-                    while (true) {
-                        WriteConfigININew();
-		
-                        if (neueref1 == 1 && neueref2 == 1) {
-                            UpdateConfigReferences(domainname);
-                        }
-                        else if (neueref1 == 1) {
-                            var anzneueref = 1;
-                            UpdateConfigReference(anzneueref, domainname);
-                        }
-                        else if (neueref2 == 1) {
-                            var anzneueref = 2;
-                            UpdateConfigReference(anzneueref, domainname);
-                        }
+            async function task() {
+                while (true) {
+                    WriteConfigININew();
+    
+                    if (neueref1 == 1 && neueref2 == 1) {
+                        UpdateConfigReferences(domainname);
+                    }
+                    else if (neueref1 == 1) {
+                        var anzneueref = 1;
+                        UpdateConfigReference(anzneueref, domainname);
+                    }
+                    else if (neueref2 == 1) {
+                        var anzneueref = 2;
+                        UpdateConfigReference(anzneueref, domainname);
+                    }
 
-                        SaveConfigToServer(domainname);
-				
-                        document.getElementById("updatemarker").disabled = false;
-                        // document.getElementById("savemarker").disabled = true;
-                        // document.getElementById("enhancecontrast").disabled = true;
+                    SaveConfigToServer(domainname);
+            
+                    document.getElementById("updatemarker").disabled = false;
+                    // document.getElementById("savemarker").disabled = true;
+                    // document.getElementById("enhancecontrast").disabled = true;
 
-                        EnDisableItem(false, "savemarker", true);
-                        EnDisableItem(false, "enhancecontrast", true);
+                    EnDisableItem(false, "savemarker", true);
+                    EnDisableItem(false, "enhancecontrast", true);
 
-                        document.getElementById("overlay").style.display = "none";
-                        firework.launch('Alignment marker saved. They will get applied after next reboot', 'success', 5000);
-                        return;
-                    }
+                    document.getElementById("overlay").style.display = "none";
+                    firework.launch('Alignment marker saved. They will get applied after the next reboot!<br><br>\n<a id="reboot_button" onclick="doReboot()">reboot now</a>', 'success', 5000);
+                    return;
                 }
-
-                setTimeout(function () {
-                    // Delay so the overlay gets shown
-                    task();
-                }, 1);
-            }
-            else {
-                document.getElementById("overlay").style.display = "none";
             }
+
+            setTimeout(function () {
+                // Delay so the overlay gets shown
+                task();
+            }, 1);
         }
 
         function EnhanceContrast() {

+ 26 - 16
sd-card/html/edit_analog.html

@@ -5,6 +5,20 @@
     <meta charset="UTF-8" />
     <title>Analog ROI</title>
 
+    <style>
+        #reboot_button {
+            float: none;
+            background-color: #f44336;
+            color: white;
+            padding: 5px;
+            border-radius:
+            5px; font-weight: bold;
+            text-align: center;
+            text-decoration: none;
+            display: inline-block;
+        }
+    </style>
+
     <link href="edit_style.css" rel="stylesheet">
     <link href="firework.css?v=$COMMIT_HASH" rel="stylesheet">
 
@@ -185,13 +199,11 @@ The following settings are only used for easier setup, they are <b>not</b> persi
         domainname = getDomainname();
 
     function doReboot() {
-        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;
-            window.location.assign(stringota);
-            window.location.replace(stringota);
-        }
+        var stringota = getDomainname() + "/reboot";
+        window.location = stringota;
+        window.location.href = stringota;
+        window.location.assign(stringota);
+        window.location.replace(stringota);
     }
     
     function EnDisableAnalog() {
@@ -331,16 +343,14 @@ The following settings are only used for easier setup, they are <b>not</b> persi
     }
 
     function SaveToConfig() {
-        if (confirm("Are you sure you want to save the new analog ROI configuration?")) {
-            //_zwcat = getConfigCategory();
-            cofcat["Analog"]["enabled"] = document.getElementById("Category_Analog_enabled").checked;
-            WriteConfigININew();
-            SaveConfigToServer(domainname);
-            UpdateROIs();
-            document.getElementById("saveroi").disabled = true;
+        //_zwcat = getConfigCategory();
+        cofcat["Analog"]["enabled"] = document.getElementById("Category_Analog_enabled").checked;
+        WriteConfigININew();
+        SaveConfigToServer(domainname);
+        UpdateROIs();
+        document.getElementById("saveroi").disabled = true;
 
-            firework.launch('Configuration saved. It will get applied after next reboot', 'success', 5000);
-        }
+        firework.launch('Configuration saved. It will get applied after the next reboot!<br><br>\n<a id="reboot_button" onclick="doReboot()">reboot now</a>', 'success', 5000);
     }
 
     function ShowMultiplier() {

+ 23 - 22
sd-card/html/edit_config_raw.html

@@ -20,6 +20,18 @@
 	textarea {
 		font-size: 15px;
 	}
+
+	#reboot_button {
+		float: none;
+		background-color: #f44336;
+		color: white;
+		padding: 5px;
+		border-radius:
+		5px; font-weight: bold;
+		text-align: center;
+		text-decoration: none;
+		display: inline-block;
+	}
 </style>
 
 <link href="firework.css?v=$COMMIT_HASH" rel="stylesheet">
@@ -38,15 +50,8 @@
 		</td>
 	</table>
 
-	<table>
-		<td>
-			<button class="button" onclick="saveTextAsFile()">Save Config</button>
-		</td>
-	    <td>
-	        <button class="button" id="reboot" type="button" onclick="doReboot()">Reboot to activate changes</button>
-		</td>
-	</table>
-
+	<hr>
+	<button class="button" onclick="saveTextAsFile()">Save Config</button>
 
 	<script type="text/javascript" src="readconfigparam.js?v=$COMMIT_HASH"></script>
 	<script type="text/javascript" src="readconfigcommon.js?v=$COMMIT_HASH"></script>
@@ -64,23 +69,19 @@
 
 	function saveTextAsFile()
 	{
-		if (confirm("Are you sure you want to save the configuration?")) {
-			FileDeleteOnServer("/config/config.ini", domainname);
-			var textToSave = document.getElementById("inputTextToSave").value;
-			FileSendContent(textToSave, "/config/config.ini", domainname);
+		FileDeleteOnServer("/config/config.ini", domainname);
+		var textToSave = document.getElementById("inputTextToSave").value;
+		FileSendContent(textToSave, "/config/config.ini", domainname);
 
-			firework.launch('Configuration saved. It will get applied after next reboot', 'success', 5000);
-		}
+		firework.launch('Configuration saved. It will get applied after the next reboot!<br><br>\n<a id="reboot_button" onclick="doReboot()">reboot now</a>', 'success', 5000);
 	}
 
 	function doReboot() {
-		if (confirm("Are you sure you want to reboot?")) {
-			var stringota = "/reboot";
-			window.location = stringota;
-			window.location.href = stringota;
-			window.location.assign(stringota);
-			window.location.replace(stringota);
-		}
+		var stringota = "/reboot";
+		window.location = stringota;
+		window.location.href = stringota;
+		window.location.assign(stringota);
+		window.location.replace(stringota);
 	}
 	
 	LoadConfigNeu();

+ 36 - 33
sd-card/html/edit_config_template.html

@@ -183,7 +183,19 @@
         color: white;
         transform: translate(-50%,-50%);
         -ms-transform: translate(-50%,-50%);
-    }	
+    }
+
+	#reboot_button {
+		float: none;
+		background-color: #f44336;
+		color: white;
+		padding: 5px;
+		border-radius:
+		5px; font-weight: bold;
+		text-align: center;
+		text-decoration: none;
+		display: inline-block;
+	}
 </style>
 
 <link rel="stylesheet" href="mkdocs_theme.css?v=$COMMIT_HASH" />
@@ -897,7 +909,7 @@
 
 		<tr style="margin-top:12px">
 			<td class="indent1" style="padding-top:25px" colspan="3">
-				<b>Parameter per number sequence:</b>
+				<b>The following parameters are configurable individually for each number sequence:</b>
                 <select 
 					style="font-weight: bold; margin-left:17px" id="Numbers_value1" onchange="numberChanged()">
 				</select>
@@ -1188,8 +1200,8 @@
 					<option value="energy_mwh">Energymeter (Value: MWh, Rate: MW)</option>
 					<option value="energy_gj">Energymeter (Value: GJ, Rate: GJ/h)</option>
 					<option value="temperature_c">Thermometer (Value: °C, Rate: °C/min)</option>
-					<option value="temperature_c">Thermometer (Value: °F, Rate: °F/min)</option>
-					<option value="temperature_c">Thermometer (Value: K, Rate: K/min)</option>
+					<option value="temperature_f">Thermometer (Value: °F, Rate: °F/min)</option>
+					<option value="temperature_k">Thermometer (Value: K, Rate: K/min)</option>
 				</select>
 			</td>
 			<td>$TOOLTIP_MQTT_MeterType</td>
@@ -1208,7 +1220,7 @@
 		
 		<tr class="MQTTItem" style="margin-top:12px">
 			<td class="indent1" style="padding-top:25px" colspan="3">
-				<b>Parameter per number sequence:</b>
+				<b>The following parameters are configurable individually for each number sequence:</b>
                 <select
 					style="font-weight: bold; margin-left:17px" id="NumbersMQTTIdx_value1" onchange="numberMQTTIdxChanged()">
                 </select>
@@ -1937,11 +1949,11 @@
 
 
 		<!------------- Autotimer ------------------>
-		<!--
 		<tr style="border-bottom: 2px solid lightgray;">
 			<td colspan="3" style="padding-left: 0px; padding-bottom: 3px;"><h4>Auto Timer</h4></td>
 		</tr>
-
+		
+		<!--
 		<tr class="expert" unused_id="ex13">
 			<td class="indent1">
 			    <class id="AutoTimer_AutoStart_text" style="color:black;">Automatic Round Start</class>
@@ -2110,14 +2122,9 @@
 		</tr>
 	</table>
 
-	<table style="padding-top:10px">
-		<td>
-			<button class="button" onclick="saveTextAsFile()">Save Config</button>
-		</td>
-	    <td>
-	        <button class="button" id="reboot" type="button" onclick="doReboot()">Reboot to activate changes</button>
-		</td>
-	</table>
+	<hr>
+	<button class="button" onclick="saveTextAsFile()">Save Config</button>
+
 </div>
 
 
@@ -2673,18 +2680,16 @@ function saveTextAsFile() {
         return;
     }
 
-    if (confirm("Are you sure you want to save the configuration?")) {
-        ReadParameterAll();
-        WriteConfigININew();
-        SaveConfigToServer(domainname);
+	ReadParameterAll();
+	WriteConfigININew();
+	SaveConfigToServer(domainname);
 
-        firework.launch('Configuration saved. It will get applied after the next reboot!', 'success', 5000);
+	firework.launch('Configuration saved. It will get applied after the next reboot!<br><br>\n<a id="reboot_button" onclick="doReboot()">reboot now</a>', 'success', 5000);
 
-        if (changeCamValue == 1) {
-            camSettingsSet();
-            firework.launch('You have changed the camera settings, so creating a new reference image and updating the alignment marks is mandatory!', 'success', 10000);		
-        }	    
-    }
+	if (changeCamValue == 1) {
+		camSettingsSet();
+		firework.launch('You have changed the camera settings, creating a new reference image and updating the alignment marks is mandatory!', 'success', 5000);
+	}
 }
 
 function camSettingsSet(){
@@ -2875,7 +2880,7 @@ function camSettingsSet(){
             
             if (xhttp.responseText == "CamSettingsSet") {
                 document.getElementById("overlay").style.display = "none";
-                firework.launch('Cam Settings saved', 'success', 2000);
+                firework.launch('Cam Settings saved', 'success', 5000);
                 return;
             }
             else {
@@ -2903,13 +2908,11 @@ function camSettingsSet(){
 }
 	
 function doReboot() {
-    if (confirm("Are you sure you want to reboot?")) {
-        var stringota = domainname + "/reboot";
-        window.location = stringota;
-        window.location.href = stringota;
-        window.location.assign(stringota);
-        window.location.replace(stringota);
-    }
+	var stringota = domainname + "/reboot";
+	window.location = stringota;
+	window.location.href = stringota;
+	window.location.assign(stringota);
+	window.location.replace(stringota);
 }
 
 function FormatDecimalValue(_param, _cat, _name) {

+ 25 - 16
sd-card/html/edit_digits.html

@@ -4,6 +4,19 @@
 <head>
     <meta charset="UTF-8" />
     <title>Digit ROI</title>
+    <style>
+        #reboot_button {
+            float: none;
+            background-color: #f44336;
+            color: white;
+            padding: 5px;
+            border-radius:
+            5px; font-weight: bold;
+            text-align: center;
+            text-decoration: none;
+            display: inline-block;
+        }
+    </style>
 
     <link href="edit_style.css" rel="stylesheet">
     <link href="firework.css?v=$COMMIT_HASH" rel="stylesheet">
@@ -204,13 +217,11 @@
         domainname = getDomainname();
 
     function doReboot() {
-        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;
-            window.location.assign(stringota);
-            window.location.replace(stringota);
-        }
+        var stringota = getDomainname() + "/reboot";
+        window.location = stringota;
+        window.location.href = stringota;
+        window.location.assign(stringota);
+        window.location.replace(stringota);
     }
 
     function EnDisableDigits() {
@@ -359,16 +370,14 @@
     }
 
     function SaveToConfig() {
-        if (confirm("Are you sure you want to save the new digit ROI configuration?")) {
-            // _zwcat = getConfigCategory();
-            cofcat["Digits"]["enabled"] = document.getElementById("Category_Digits_enabled").checked;
-            WriteConfigININew();
-            SaveConfigToServer(domainname);
-            UpdateROIs();
-            document.getElementById("saveroi").disabled = true;
+        // _zwcat = getConfigCategory();
+        cofcat["Digits"]["enabled"] = document.getElementById("Category_Digits_enabled").checked;
+        WriteConfigININew();
+        SaveConfigToServer(domainname);
+        UpdateROIs();
+        document.getElementById("saveroi").disabled = true;
 
-            firework.launch('Configuration saved. It will get applied after next reboot', 'success', 5000);
-        }
+        firework.launch('Configuration saved. It will get applied after the next reboot!<br><br>\n<a id="reboot_button" onclick="doReboot()">reboot now</a>', 'success', 5000);
     }
 
     function ShowMultiplier() {

+ 7 - 17
sd-card/html/edit_reference.html

@@ -395,22 +395,12 @@
 
     <script type="text/javascript">
         var canvas = document.getElementById('canvas'),
-            domainname = getDomainname(),
-            context = canvas.getContext('2d'),
-            imageObj = new Image(),
-            isActReference = false,
-            param,
-			category;
-
-        function doReboot() {
-            if (confirm("Are you sure you want to reboot? Did you save the config?")) {
-               var stringota = domainname + "/reboot";
-               window.location = stringota;
-               window.location.href = stringota;
-               window.location.assign(stringota);
-               window.location.replace(stringota);
-            }
-        }
+        domainname = getDomainname(),
+        context = canvas.getContext('2d'),
+        imageObj = new Image(),
+        isActReference = false,
+        param,
+        category;
 
         function cameraParameterChanged() {
             document.getElementById("savereferenceimage").disabled = true;
@@ -738,7 +728,7 @@
             
                     if (xhttp.responseText == "CamSettingsSet") {
 						document.getElementById("overlay").style.display = "none";
-                        firework.launch('Cam Settings saved', 'success', 2000);
+                        firework.launch('Cam Settings saved', 'success', 5000);
                         return;
                     }
                     else {

+ 50 - 0
sd-card/html/file_server.css

@@ -0,0 +1,50 @@
+h1 {font-size: 2em;}
+h2 {font-size: 1.5em; margin-block-start: 0.0em; margin-block-end: 0.2em;}
+h3 {font-size: 1.2em;}
+p {font-size: 1em;}
+
+#files_table {
+  font-family: Arial, Helvetica, sans-serif;
+  border-collapse: collapse;
+  width: 100%;
+}
+
+#files_table td, #files_table th {
+  border: 1px solid #ddd;
+  padding: 8px;
+}
+
+#files_table tr:nth-child(even){
+  background-color: #f2f2f2;
+}
+
+#files_table tr:hover {
+  background-color: #ddd;
+}
+
+#files_table th {
+  padding-top: 12px;
+  padding-bottom: 12px;
+  text-align: left;
+  background-color:lightgrey;
+  color: black;
+}
+
+input[type=file] {
+  padding: 5px 0px;
+  display: inline-block;
+  font-size: 16px; 
+}
+
+input[type=text] {
+  padding: 5px 10px;
+  display: inline-block;
+  border: 1px solid #ccc;
+  font-size: 16px; 
+}
+
+.button {
+  padding: 4px 10px;
+  width: 100px;
+  font-size: 16px;	
+}

+ 0 - 206
sd-card/html/file_server.html

@@ -1,206 +0,0 @@
-<!DOCTYPE html>
-<html lang="en" xml:lang="en">
-
-    <head>
-        <link href="/firework.css?v=$COMMIT_HASH" rel="stylesheet">
-        <script type="text/javascript" src="/jquery-3.6.0.min.js?v=$COMMIT_HASH"></script>
-        <script type="text/javascript" src="/firework.js?v=$COMMIT_HASH"></script>
-	
-        <style>
-            h1 {font-size: 2em;}
-            h2 {font-size: 1.5em; margin-block-start: 0.0em; margin-block-end: 0.2em;}
-            h3 {font-size: 1.2em;}
-            p {font-size: 1em;}
-
-            #files_table {
-                font-family: Arial, Helvetica, sans-serif;
-                border-collapse: collapse;
-                width: 100%;
-            }
-
-            #files_table td, #files_table th {
-                border: 1px solid #ddd;
-                padding: 8px;
-            }
-
-            #files_table tr:nth-child(even){
-                background-color: #f2f2f2;
-            }
-
-            #files_table tr:hover {
-                background-color: #ddd;
-            }
-
-            #files_table th {
-                padding-top: 12px;
-                padding-bottom: 12px;
-                text-align: left;
-                background-color:lightgrey;
-                color: black;
-            }
-
-            input[type=file] {
-                padding: 5px 0px;
-                display: inline-block;
-                font-size: 16px; 
-            }
-
-            input[type=text] {
-                padding: 5px 10px;
-                display: inline-block;
-                border: 1px solid #ccc;
-                font-size: 16px; 
-            }
-
-            .button {
-                padding: 4px 10px;
-                width: 100px;
-                font-size: 16px;	
-		    }
-        </style>
-    </head>
-    
-    </body>
-        <table class="fixed" border="0" width=100% style="font-family: arial">
-            <tr>
-                <td style="vertical-align: top;width: 300px;">
-                    <h2>Fileserver</h2>
-                </td>
-                <td rowspan="2">
-                    <table border="0" style="width:100%">
-                        <tr>
-                            <td style="width:80px">
-                                <label for="newfile">Source</label>
-                            </td>
-                            <td colspan="2">
-                                <input id="newfile" type="file" onchange="setpath()" style="width:100%;">
-                            </td>
-                        </tr>
-                        
-                        <tr>
-                            <td>
-                                <label for="filepath">Destination</label>
-                            </td>
-                            <td>
-                                <input id="filepath" type="text" style="width:94%;">
-                            </td>
-                            <td>
-                                <button id="upload" type="button" class="button" onclick="upload()">Upload</button>
-                            </td>
-                        </tr>
-                    </table>
-                </td>
-            </tr>
-            <tr></tr>
-            <tr>
-                <td colspan="2">
-                    <button style="font-size:16px; padding: 5px 10px" id="dirup" type="button" onclick="dirup()" disabled>&#129145; Directory up</button>
-                    <span style="padding-left:15px" id="currentpath"></span>
-                </td>
-            </tr>
-        </table>
-
-        <script type="text/javascript" src="/common.js?v=$COMMIT_HASH"></script>
-
-        <script type="text/javascript">
-        function setpath() {
-            var fileserverpraefix = "/fileserver";
-            var anz_zeichen_fileserver = fileserverpraefix.length;
-            var default_path = window.location.pathname.substring(anz_zeichen_fileserver) + document.getElementById("newfile").files[0].name;
-            document.getElementById("filepath").value = default_path;
-        }
-
-        function dirup() {
-            var str = window.location.href;
-            str = str.substring(0, str.length-1);
-            var zw = str.indexOf("/");
-            var found = zw;
-            while (zw >= 0)
-            {
-                zw = str.indexOf("/", found+1);  
-                if (zw >= 0)
-                    found = zw;
-            }
-            var res = str.substring(0, found+1);
-
-            window.location.href = res;	
-        }
-
-
-        function upload() {
-            var filePath = document.getElementById("filepath").value;
-            var upload_path = "/upload/" + filePath;
-            var fileInput = document.getElementById("newfile").files;
-
-            /* Max size of an individual file. Make sure this
-            * value is same as that set in file_server.c */
-            var MAX_FILE_SIZE = 8000*1024;
-            var MAX_FILE_SIZE_STR = "8000KB";
-
-            if (fileInput.length == 0) {
-                firework.launch('No file selected!', 'danger', 30000);
-            } else if (filePath.length == 0) {
-                firework.launch('File path on server is not set!', 'danger', 30000);
-            } else if (filePath.length > 100) {
-                firework.launch('Filename is to long! Max 100 characters.', 'danger', 30000);
-            } else if (filePath.indexOf(' ') >= 0) {
-                firework.launch('File path on server cannot have spaces!', 'danger', 30000);
-            } else if (filePath[filePath.length-1] == '/') {
-                firework.launch('File name not specified after path!', 'danger', 30000);
-            } else if (fileInput[0].size > MAX_FILE_SIZE) {
-                firework.launch("File size must be less than " + MAX_FILE_SIZE_STR + "!", 'danger', 30000);
-            } else {
-                document.getElementById("newfile").disabled = true;
-                document.getElementById("filepath").disabled = true;
-                document.getElementById("upload").disabled = true;
-
-                var file = fileInput[0];
-                var xhttp = new XMLHttpRequest();
-                xhttp.onreadystatechange = function() {
-                    if (xhttp.readyState == 4) {
-                        if (xhttp.status == 200) {
-                            document.open();
-                            document.write(xhttp.responseText);
-                            document.close();
-                            firework.launch('File upload completed', 'success', 5000);
-                        } else if (xhttp.status == 0) {
-                            firework.launch('Server closed the connection abruptly!', 'danger', 30000);
-                            UpdatePage(false);
-                        } else {
-                            firework.launch('An error occured: ' + xhttp.responseText, 'danger', 30000);
-                            UpdatePage(false);
-                        }
-                    }
-                };
-                xhttp.open("POST", upload_path, true);
-                xhttp.send(file);
-            }
-        }
-
-
-        function checkAtRootLevel(res) {
-            if (getPath() == "/fileserver/") { // Already at root level
-                document.getElementById("dirup").disabled = true;
-                console.log("Already on sd-card root level!");
-                return true;
-            }
-
-            document.getElementById("dirup").disabled = false;
-            return false;
-        }
-
-
-        function getPath() {
-            return window.location.pathname.replace(/\/+$/, '') + "/"
-        }
-
-        checkAtRootLevel();
-
-        console.log("Current path: " + getPath().replace("/fileserver", ""));
-        document.getElementById("currentpath").innerHTML = "Current path: <b>" + getPath().replace("/fileserver", "") + "</b>";
-
-        document.cookie = "page=" + getPath() + "; path=/";
-
-        </script>
-    </body>
-</html>

+ 93 - 0
sd-card/html/file_server.js

@@ -0,0 +1,93 @@
+function setpath() {
+  var fileserverpraefix = "/fileserver";
+  var anz_zeichen_fileserver = fileserverpraefix.length;
+  var default_path = window.location.pathname.substring(anz_zeichen_fileserver) + document.getElementById("newfile").files[0].name;
+  document.getElementById("filepath").value = default_path;
+}
+
+function dirup() {
+  var str = window.location.href;
+  str = str.substring(0, str.length-1);
+  var zw = str.indexOf("/");
+  var found = zw;
+  while (zw >= 0) {
+    zw = str.indexOf("/", found+1);  
+    if (zw >= 0) { 
+      found = zw; 
+    }
+  }
+  var res = str.substring(0, found+1);
+  window.location.href = res;	
+}
+
+function upload() {
+  var filePath = document.getElementById("filepath").value;
+  var upload_path = "/upload/" + filePath;
+  var fileInput = document.getElementById("newfile").files;
+
+  // Max size of an individual file. Make sure this value is same as that set in file_server.c
+  var MAX_FILE_SIZE = 8000*1024;
+  var MAX_FILE_SIZE_STR = "8000KB";
+
+  if (fileInput.length == 0) {
+    firework.launch('No file selected!', 'danger', 30000);
+  } else if (filePath.length == 0) {
+    firework.launch('File path on server is not set!', 'danger', 30000);
+  } else if (filePath.length > 100) {
+    firework.launch('Filename is to long! Max 100 characters.', 'danger', 30000);
+  } else if (filePath.indexOf(' ') >= 0) {
+    firework.launch('File path on server cannot have spaces!', 'danger', 30000);
+  } else if (filePath[filePath.length-1] == '/') {
+    firework.launch('File name not specified after path!', 'danger', 30000);
+  } else if (fileInput[0].size > MAX_FILE_SIZE) {
+    firework.launch("File size must be less than " + MAX_FILE_SIZE_STR + "!", 'danger', 30000);
+  } else {
+    document.getElementById("newfile").disabled = true;
+    document.getElementById("filepath").disabled = true;
+    document.getElementById("upload").disabled = true;
+
+    var file = fileInput[0];
+    var xhttp = new XMLHttpRequest();
+    xhttp.onreadystatechange = function() {
+      if (xhttp.readyState == 4) {
+        if (xhttp.status == 200) {
+          document.open();
+          document.write(xhttp.responseText);
+          document.close();
+          firework.launch('File upload completed', 'success', 5000);
+        } else if (xhttp.status == 0) {
+          firework.launch('Server closed the connection abruptly!', 'danger', 30000);
+          UpdatePage(false);
+        } else {
+          firework.launch('An error occured: ' + xhttp.responseText, 'danger', 30000);
+          UpdatePage(false);
+        }
+      }
+    };
+    xhttp.open("POST", upload_path, true);
+    xhttp.send(file);
+  }
+}
+
+function checkAtRootLevel(res) {
+  if (getPath() == "/fileserver/") { 
+    // Already at root level
+    document.getElementById("dirup").disabled = true;
+    console.log("Already on sd-card root level!");
+    return true;
+  }
+
+  document.getElementById("dirup").disabled = false;
+  return false;
+}
+
+function getPath() {
+  return window.location.pathname.replace(/\/+$/, '') + "/"
+}
+
+function initFileServer() {
+  checkAtRootLevel();
+  console.log("Current path: " + getPath().replace("/fileserver", ""));
+  document.getElementById("currentpath").innerHTML = "Current path: <b>" + getPath().replace("/fileserver", "") + "</b>";
+  document.cookie = "page=" + getPath() + "; path=/";
+}

+ 1 - 0
sd-card/html/index.html

@@ -142,6 +142,7 @@
             <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 Table</a></li>
             <li><a href="#" onclick="loadPage(getDomainname() + '/fileserver/log/data/');">Data Files</a></li>
+            <li><a href="#" onclick="loadPage('data_export.html?v=$COMMIT_HASH');">Data Export</a></li>
         </ul>
     </li>
 

+ 5 - 7
sd-card/html/reboot_page.html

@@ -34,13 +34,11 @@ p {font-size: 1em;}
 
 <script>
 function doReboot() {
-	// if (confirm("Are you sure you want to reboot the ESP32?")) {
-		var stringota = getDomainname() + "/reboot";
-		window.location = stringota;
-		window.location.href = stringota;
-		window.location.assign(stringota);
-		window.location.replace(stringota);
-	// }
+	var stringota = getDomainname() + "/reboot";
+	window.location = stringota;
+	window.location.href = stringota;
+	window.location.assign(stringota);
+	window.location.replace(stringota);
 }
 </script>
 

二进制
sd-card/html/searchicon.png


+ 2 - 2
sd-card/html/timezones.html

@@ -8,7 +8,7 @@
 }
 
 #myInput {
-  background-image: url('/css/searchicon.png');
+  background-image: url('searchicon.png');
   background-position: 10px 10px;
   background-repeat: no-repeat;
   width: 100%;
@@ -539,4 +539,4 @@
     
     </body>
     </html>
-    
+    

+ 2 - 2
sd-card/html/wlan_config.html

@@ -29,7 +29,7 @@
 //    var xhttp = new XMLHttpRequest();
 //    xhttp.onreadystatechange = function() {if (xhttp.readyState == 4) {if (xhttp.status == 200) {document.reload();}}};
 if (!file.name.includes("remote-setup")){
-    if (!confirm("The zip file name should contain \"...remote-setup...\". Are you sure that you have downloaded the correct file?"))
+    if (!confirm("The zip file name should contain \"...remote-setup...\". Are you sure you have downloaded the correct file?"))
         return;
 }
 
@@ -41,7 +41,7 @@ if (!file.name.includes("remote-setup")){
         var file = document.getElementById("newfile").files[0];
         if (!file.name.includes("remote-setup"))
         {
-            if (!confirm("The zip file name should contain \"...remote-setup...\". Are you sure that you have downloaded the correct file?"))
+            if (!confirm("The zip file name should contain \"...remote-setup...\". Are you sure you have downloaded the correct file?"))
                 return;
         }