new OTA page with progressbar (#1756)

* new OTA page with progress bar

* improve error message on missing demo files

* .

* Implemented Reboot for "firmware.bin" as well

* Update feature.yaml

* cache static files (#1755)

Co-authored-by: CaCO3 <caco@ruinelli.ch>

* .

* .

* added filename validation

* .

* .

* .

* move

* added missing dash to regex

* restrict file type

* .

* .

* .

* .

* cleanup no longer needed mode

* only start restart counter if restart is required

Co-authored-by: CaCO3 <caco@ruinelli.ch>
Co-authored-by: jomjol <30766535+jomjol@users.noreply.github.com>
This commit is contained in:
CaCO3
2023-01-07 20:21:35 +01:00
committed by GitHub
parent b1a38e0a6d
commit 26897ccb15
5 changed files with 276 additions and 272 deletions

View File

@@ -682,7 +682,8 @@ void CCamera::useDemoMode()
FILE *fd = fopen("/sdcard/demo/files.txt", "r"); FILE *fd = fopen("/sdcard/demo/files.txt", "r");
if (!fd) { if (!fd) {
LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Please provide the demo files first!"); LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Can not start Demo mode, the folder '/sdcard/demo/' does not contain the needed files!");
LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "See Details on https://github.com/jomjol/AI-on-the-edge-device/wiki/Demo-Mode!");
return; return;
} }

View File

@@ -645,40 +645,17 @@ esp_err_t handler_reboot(httpd_req_t *req)
LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "handler_reboot"); LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "handler_reboot");
ESP_LOGI(TAG, "!!! System will restart within 5 sec!!!"); ESP_LOGI(TAG, "!!! System will restart within 5 sec!!!");
char _query[200];
char _valuechar[30];
std::string _task;
std::string response = std::string response =
"<html><head><script>" "<html><head><script>"
"function m(h) {" "function m(h) {"
"document.getElementById('t').innerHTML=h;" "document.getElementById('t').innerHTML=h;"
"setInterval(function (){h +='.'; document.getElementById('t').innerHTML=h;" "setInterval(function (){h +='.'; document.getElementById('t').innerHTML=h;"
"fetch('reboot_page.html',{mode: 'no-cors'}).then(r=>{parent.location.href=('index.html');})}, 1000);" "fetch('reboot_page.html',{mode: 'no-cors'}).then(r=>{parent.location.href=('index.html');})}, 1000);"
"}</script></head></html><body style='font-family: arial'><h3 id=t></h3>"; "}</script></head></html><body style='font-family: arial'><h3 id=t></h3>"
"<script>m('Rebooting!<br>The page will automatically reload in around 25..60s.<br><br>');</script>"
if (httpd_req_get_url_query_str(req, _query, 200) == ESP_OK) "</body></html>";
{
ESP_LOGD(TAG, "Query: %s", _query);
if (httpd_query_key_value(_query, "task", _valuechar, 30) == ESP_OK)
{
ESP_LOGD(TAG, "task is found: %s", _valuechar);
_task = std::string(_valuechar);
}
}
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
if (_task.compare("OTA") == 0) { // Reboot after OTA upload
response.append("<script>m('The upload completed successfully.<br>Rebooting and installing it now...<br><br>"
"The page will automatically reload after the update completed.<br>"
"This can take several minutes!<br><br>');</script>");
}
else { // Normal reboot
response.append("<script>m('Rebooting!<br>The page will automatically reload in around 25..60s.<br><br>');</script>");
}
response.append("</body></html>");
httpd_resp_send(req, response.c_str(), strlen(response.c_str())); httpd_resp_send(req, response.c_str(), strlen(response.c_str()));
doReboot(); doReboot();

View File

@@ -839,11 +839,11 @@ void task_autodoFlow(void *pvParameter)
if (!isPlannedReboot) if (!isPlannedReboot)
{ {
if (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 or to fetch the log!"); 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.WriteToFile(ESP_LOG_WARN, TAG, "Setting logfile level to DEBUG until the next reboot!");
LogFile.setLogLevel(ESP_LOG_DEBUG); LogFile.setLogLevel(ESP_LOG_DEBUG);
//MQTTPublish(GetMQTTMainTopic() + "/" + "status", "Postponing first round", false); //MQTTPublish(GetMQTTMainTopic() + "/" + "status", "Postponing first round", false);
vTaskDelay(60*5000 / portTICK_RATE_MS); // Wait 5 minutes to give time to do an OTA or fetch the log vTaskDelay(60*5000 / portTICK_RATE_MS); // Wait 5 minutes to give time to do an OTA Update or fetch the log
} }
} }

View File

@@ -11,6 +11,10 @@
<script type="text/javascript" src="readconfigcommon.js"></script> <script type="text/javascript" src="readconfigcommon.js"></script>
<script type="text/javascript" src="readconfigparam.js"></script> <script type="text/javascript" src="readconfigparam.js"></script>
<script type="text/javascript" src="jquery-3.6.0.min.js"></script>
<script type="text/javascript" src="common.js"></script>
<script type="text/javascript" src="firework.js"></script>
<script> <script>
async function loadPage(page) { async function loadPage(page) {
console.log("loadPage(" + page + ")"); console.log("loadPage(" + page + ")");

View File

@@ -1,271 +1,293 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link rel="icon" href="favicon.ico" type="image/x-icon"> <link rel="icon" href="favicon.ico" type="image/x-icon">
<title>OTA Update</title> <title>OTA Update</title>
<meta charset="utf-8"> <meta charset="utf-8">
<script type="text/javascript" src="common.js"></script> <style>
<style> h1 {font-size: 2em;}
h1 {font-size: 2em;} h2 {font-size: 1.5em;}
h2 {font-size: 1.5em;} h3 {font-size: 1.2em;}
h3 {font-size: 1.2em;} p {font-size: 1em;}
p {font-size: 1em;}
input[type=number] {
width: 138px;
padding: 10px 5px;
display: inline-block;
border: 1px solid #ccc;
font-size: 16px;
}
.button {
padding: 10px 20px;
width: 211px;
font-size: 16px;
}
</style>
input[type=number] { <link href="firework.css" rel="stylesheet">
width: 138px; <script type="text/javascript" src="jquery-3.6.0.min.js"></script>
padding: 10px 5px; <script type="text/javascript" src="common.js"></script>
display: inline-block; <script type="text/javascript" src="firework.js"></script>
border: 1px solid #ccc;
font-size: 16px;
}
.button {
padding: 10px 20px;
width: 211px;
font-size: 16px;
}
</style>
<link href="firework.css" rel="stylesheet">
<script type="text/javascript" src="jquery-3.6.0.min.js"></script>
<script type="text/javascript" src="firework.js"></script>
</head> </head>
<body style="font-family: arial; padding: 0px 10px;"> <body style="font-family: arial; padding: 0px 10px;">
<p>Check the <a href="https://github.com/jomjol/AI-on-the-edge-device/releases" target=_blank>Release Page</a> to see if there is an update available.</p> <h2>OTA Update</h2>
<p>Normally, the overall update package (<i><span style="font-family:monospace">update__*.zip</span></i>) is your best choice!<br> <p>Check the <a href="https://github.com/jomjol/AI-on-the-edge-device/releases" target=_blank>Release Page</a> to see if there is an update available. <br>
Alternatively you can use the old style <i><span style="font-family:monospace">firmware__*.bin</span></i> and Then pick the <i><span style="font-family:monospace">AI-on-the-edge-device__update__*.zip</span></i> file!</p>
web interface (<i><span style="font-family:monospace">html__*.zip</span></i>). How ever it is strongly recommended to update firmware and <p>Alternatively you can use a file in the following format:</p>
web interface at the same time!</p> <ul>
<hr> <li><span style="font-family:monospace">AI-on-the-edge-device__update__*.zip</span></li>
<h2>Update</h2> <li><span style="font-family:monospace">AI-on-the-edge-device__firmware__*.bin</span></li>
<b>Do not reload the page or switch to another page while the update is in progress!</b> <li><span style="font-family:monospace">*.tfl/tflite</span></li>
<table class="fixed" border="0"> </ul>
<tr> <p>Make sure the file extention is lower case.</p>
<p>
<label for="newfile">Select the file containig the update ( <p><br><b>Do not reload the page or switch to another page while the update is in progress!</b><br></p>
<i><span style="font-family:monospace">update__*.zip</span></i>,
<i><span style="font-family:monospace">firmware__*.bin</span></i>, <form id="upload_form" enctype="multipart/form-data" method="post">
<i><span style="font-family:monospace">html__*.zip</span></i>, <input type="file" accept=".bin,.zip,.tfl,.tflite" name="file_selector" id="file_selector" onchange="validate_file()"><br><br>
<i><span style="font-family:monospace">*.tfl/tflite</span></i>)
</p> <button class="button" style="width:300px" id="start_OTA_button" type="button" onclick="start_OTA()" disabled>Upload and install</button>
<br><br>
<progress id="progressBar" value="0" max="100" style="width:600px;"></progress>
<p id="loaded_n_total"></p>
<h3><span id="status">Status: idle</span></h3>
</form>
<script language="JavaScript">
var domainname = getDomainname();
</tr> var action_runtime = 0;
<tr>
<p> /* Max size of an individual file. Make sure this
<input id="newfile" type="file" onchange="setpath()" style="width:100%;"> * value is same as that set in server_file.c */
</p> var MAX_FILE_SIZE = 8000*1024;
</tr> var MAX_FILE_SIZE_STR = "8MB";
<tr>
<p> function validate_file() {
<button class="button" style="width:300px" id="doUpdate" type="button" onclick="prepareOnServer()">Upload and update<br>(incl. reboot - if needed)</button> document.getElementById("start_OTA_button").disabled = true;
</p>
</tr>
<tr>
<p>
<h3><span id="status">Status: idle</span> <span id="progress"></span></h3>
</p>
</tr>
</table>
var fileInput = document.getElementById("file_selector").files;
var filepath = document.getElementById("file_selector").value;
<script language="JavaScript"> console.log("filepath: " + filepath);
var domainname = getDomainname(); filename = filepath.split(/[\\\/]/).pop();
console.log("filename: " + filename);
/* Max size of an individual file. Make sure this /* Various checks on filename length and file size */
* value is same as that set in server_file.c */ if (fileInput.length == 0) {
var MAX_FILE_SIZE = 8000*1024; firework.launch('No file selected!', 'danger', 30000);
var MAX_FILE_SIZE_STR = "8MB"; return;
} else if (filename.length == 0) {
var action_runtime = 0; firework.launch('File path on server is not set!', 'danger', 30000);
var progressTimerHandle = null; return;
} else if (filename.length > 100) {
firework.launch('Filename is to long! Max 100 characters.', 'danger', 30000);
function init(){ return;
domainname = getDomainname(); } else if (filename.indexOf(' ') >= 0) {
firework.launch('Filename can not have spaces!', 'danger', 30000);
document.getElementById("doUpdate").disabled = true; return;
} } else if (filename[filename.length-1] == '/') {
firework.launch('File name not specified after path!', 'danger', 30000);
return;
function doRebootAfterUpdate() { } else if (fileInput[0].size > MAX_FILE_SIZE) {
/* if (confirm("Upload completed!\nThe device will reboot now and complete the update.\nThis will take up to 180s!")) {*/ firework.launch("File size must be less than " + MAX_FILE_SIZE_STR + "!", 'danger', 30000);
var stringota = "/reboot?task=OTA"; return;
window.location = stringota;
window.location.href = stringota;
window.location.assign(stringota);
window.location.replace(stringota);
/* }*/
}
function setpath() {
var nameneu = document.getElementById("newfile").value;
nameneu = nameneu.split(/[\\\/]/).pop();
document.getElementById("doUpdate").disabled = false;
document.getElementById("status").innerText = "Status: File selected";
}
function prepareOnServer() {
var fileInput = document.getElementById("newfile").files;
var nameneu = document.getElementById("newfile").value;
filePath = nameneu.split(/[\\\/]/).pop();
/* 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);
return;
} else if (filePath.length == 0) {
firework.launch('File path on server is not set!', 'danger', 30000);
return;
} else if (filePath.length > 100) {
firework.launch('Filename is to long! Max 100 characters.', 'danger', 30000);
return;
} else if (filePath.indexOf(' ') >= 0) {
firework.launch('File path on server cannot have spaces!', 'danger', 30000);
return;
} else if (filePath[filePath.length-1] == '/') {
firework.launch('File name not specified after path!', 'danger', 30000);
return;
} else if (fileInput[0].size > MAX_FILE_SIZE) {
firework.launch("File size must be less than " + MAX_FILE_SIZE_STR + "!", 'danger', 30000);
return;
}
document.getElementById("status").innerText = "Status: Preparations on device";
document.getElementById("doUpdate").disabled = true;
var xhttp = new XMLHttpRequest();
var nameneu = document.getElementById("newfile").value;
filePath = nameneu.split(/[\\\/]/).pop();
/* first delete the old firmware AND empty the /firmware directory*/
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4) {
stopProgressTimer();
if (xhttp.status == 200) {
/* keine Reaktion, damit sich das Dokument nicht ändert */
upload();
} else if (xhttp.status == 0) {
firework.launch('Server closed the connection abruptly!', 'danger', 30000);
document.getElementById("doUpdate").disabled = false;
} else {
firework.launch('An error occured: ' + xhttp.responseText, 'danger', 30000);
document.getElementById("doUpdate").disabled = false;
}
}
};
startProgressTimer("Server preparations...");
var _toDo = domainname + "/ota?task=emptyfirmwaredir";
xhttp.open("GET", _toDo, true);
xhttp.send();
}
function upload() {
document.getElementById("newfile").disabled = true;
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4) {
stopProgressTimer();
if (xhttp.status == 200) {
extract();
} else if (xhttp.status == 0) {
firework.launch('Server closed the connection abruptly!', 'danger', 30000);
document.getElementById("doUpdate").disabled = false;
} else {
firework.launch('An error occured: ' + xhttp.responseText, 'danger', 30000);
document.getElementById("doUpdate").disabled = false;
} }
}
};
startProgressTimer("Upload");
var fileInput = document.getElementById("newfile").files; /* Check if the fillename matches our expected pattern
var file = fileInput[0]; * - AI-on-the-edge-device__update__*.zip
var upload_path = "/upload/firmware/" + filePath; * - firmware__*.bin
* - *.ftl
xhttp.open("POST", upload_path, true); * - *.tflite */
document.getElementById("status").innerText = "Status: Uploading (takes up to 60s)..."; if ( /(^AI-on-the-edge-device__update__)[a-z0-9()_\-.]*(\.zip$)/.test(filename) || // OK
xhttp.send(file); ( /(^AI-on-the-edge-device__firmware)[a-z0-9()_\-.]*(\.bin$)/.test(filename)) ||
} ( /[a-z0-9()_\-.]*(\.tfl$)/.test(filename)) ||
( /[a-z0-9()_\-.]*(\.tflite$)/.test(filename))) {
firework.launch('Great, the filename matches our expectations. You can now press "Upload and update".', 'success', 5000);
function extract() {
document.getElementById("status").innerText = "Status: Processing on device (takes up to 3 minutes)...";
var xhttp = new XMLHttpRequest();
/* first delete the old firmware */
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4) {
stopProgressTimer();
if (xhttp.status == 200) {
document.getElementById("status").innerText = "Status: Update completed!";
document.getElementById("doUpdate").disabled = true;
document.getElementById("newfile").disabled = false;
document.cookie = "page=overview.html"; // Make sure after the reboot we go to the overview page
if (xhttp.responseText.startsWith("reboot"))
{
doRebootAfterUpdate();
}
else
{
firework.launch('Processing done', 'success', 5000);
}
} else if (xhttp.status == 0) {
firework.launch('Server closed the connection abruptly!', 'danger', 30000);
UpdatePage();
} else {
firework.launch('An error occured: ' + xhttp.responseText, 'danger', 30000);
UpdatePage();
} }
/* Following filenames are acceptiod but not prefered:
* - *.bin
* - *.zip */
else if (filename.endsWith(".zip") || filename.endsWith(".bin")) { // Warning but still accepted
firework.launch('The filename does not match the suggested file name pattern, but is nevertheless accepted. You can now press "Upload and install', 'warning', 10000);
}
/* Any other file name format is not accepted */
else { // invalid
firework.launch('The filename does not match our expectations!', 'danger', 30000);
return;
}
document.getElementById("start_OTA_button").disabled = false;
} }
};
startProgressTimer("Extraction");
var nameneu = document.getElementById("newfile").value; function start_OTA() {
filePath = nameneu.split(/[\\\/]/).pop(); document.getElementById("start_OTA_button").disabled = true;
var _toDo = domainname + "/ota?task=update&file=" + filePath;
xhttp.open("GET", _toDo, true); var file_name = document.getElementById("file_selector").value;
xhttp.send(); document.getElementById("file_selector").disabled = true;
} file_name = file_name.split(/[\\\/]/).pop();
document.getElementById("status").innerText = "Status: File selected";
prepareOnServer();
}
function startProgressTimer(step) { function doRebootAfterUpdate() {
console.log(step + "..."); var xhttp = new XMLHttpRequest();
document.getElementById('progress').innerHTML = "(0s)"; xhttp.open("GET", "/reboot", true);
action_runtime = 0; xhttp.send();
progressTimerHandle = setInterval(function() { }
action_runtime += 1;
console.log("Progress: " + action_runtime + "s");
document.getElementById('progress').innerHTML = "(" + action_runtime + "s)";
}, 1000);
}
function stopProgressTimer() { function prepareOnServer() {
clearInterval(progressTimerHandle); document.getElementById("status").innerText = "Status: Preparing device...";
document.getElementById('progress').innerHTML = "";
} var xhttp = new XMLHttpRequest();
var file_name = document.getElementById("file_selector").value;
filePath = file_name.split(/[\\\/]/).pop();
/* first delete the old firmware AND empty the /firmware directory*/
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4) {
if (xhttp.status == 200) {
/* keine Reaktion, damit sich das Dokument nicht ändert */
upload();
} else if (xhttp.status == 0) {
firework.launch('Server closed the connection abruptly!', 'danger', 30000);
} else {
firework.launch('An error occured: ' + xhttp.responseText, 'danger', 30000);
}
}
};
var _toDo = domainname + "/ota?task=emptyfirmwaredir";
xhttp.open("GET", _toDo, true);
xhttp.send();
}
init(); function extract() {
document.getElementById("status").innerText = "Status: Processing on device...";
</script> var xhttp = new XMLHttpRequest();
/* first delete the old firmware */
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4) {
if (xhttp.status == 200) {
document.cookie = "page=overview.html"; // Make sure after the reboot we go to the overview page
if (xhttp.responseText.startsWith("reboot")) { // Reboot required
console.log("Upload completed, the device will now restart and install the update!");
document.getElementById("status").innerText = "Status: Installing...";
firework.launch('Upload completed, the device will now restart and install the update', 'success', 5000);
/* Tell it to reboot */
doRebootAfterUpdate();
action_runtime = 0;
updateTimer = setInterval(function() {
action_runtime += 1;
console.log("Waiting: " + action_runtime + "s");
_("progressBar").value = Math.round(action_runtime);
if (action_runtime > 10) { // After 10 seconds, start to check if we are up again
/* Check if the device is up again and forward to index page if so */
fetch('reboot_page.html?' + Math.random(), {mode: 'no-cors'}).then(
r=>{parent.location.href=('index.html');}
)
}
if (action_runtime > 100) { // We reached 300 seconds but device is not ready yet
firework.launch("The device seems not do be up again, or maybe we missed it. Try to reload this page or reset the device!", 'danger', 30000);
clearInterval(updateTimer);
}
}, 3000);
}
else // No reboot required
{
document.getElementById("status").innerText = "Status: Update completed";
firework.launch('Update completed!', 'success', 5000);
document.getElementById("file_selector").disabled = false;
}
} else if (xhttp.status == 0) {
firework.launch('Server closed the connection abruptly!', 'danger', 30000);
} else {
firework.launch('An error occured: ' + xhttp.responseText, 'danger', 30000);
}
}
};
var file_name = document.getElementById("file_selector").value;
filePath = file_name.split(/[\\\/]/).pop();
var _toDo = domainname + "/ota?task=update&file=" + filePath;
xhttp.open("GET", _toDo, true);
xhttp.send();
}
function _(el) {
return document.getElementById(el);
}
function upload() {
document.getElementById("status").innerText = "Status: Uploading...";
var upload_path = "/upload/firmware/" + filePath;
var file = _("file_selector").files[0];
var formdata = new FormData();
formdata.append("file_selector", file);
var ajax = new XMLHttpRequest();
ajax.upload.addEventListener("progress", progressHandler, false);
ajax.addEventListener("load", completeHandler, false);
ajax.addEventListener("error", errorHandler, false);
ajax.addEventListener("abort", abortHandler, false);
ajax.open("POST", upload_path);
ajax.send(file);
}
function progressHandler(event) {
_("loaded_n_total").innerHTML = "Uploaded " + (event.loaded / 1024 / 1024).toFixed(2) +
" MBytes of " + (event.total / 1024/ 1024).toFixed(2) + " MBytes.";
var percent = (event.loaded / event.total) * 100;
_("progressBar").value = Math.round(percent);
_("status").innerHTML = "Status: " + Math.round(percent) + "% uploaded... please wait";
}
function completeHandler(event) {
_("status").innerHTML = "Status: " + event.target.responseText;
_("progressBar").value = 0; //will clear progress bar after successful upload
_("loaded_n_total").innerHTML = "";
extract();
}
function errorHandler(event) {
_("status").innerHTML = "Status: Upload Failed";
firework.launch('Upload failed!', 'danger', 30000);
document.getElementById("file_selector").disabled = false;
}
function abortHandler(event) {
_("status").innerHTML = "Status: Upload Aborted";
firework.launch('Upload aborted!', 'danger', 30000);
document.getElementById("file_selector").disabled = false;
}
</script>
</body> </body>
</html> </html>