intermediate: OTA update

This commit is contained in:
wellenvogel 2021-12-14 18:01:38 +01:00
parent 8b9fabe9e4
commit 428f55e87c
6 changed files with 80 additions and 17 deletions

View File

@ -9,6 +9,7 @@ class UpdateParam{
public: public:
String error; String error;
size_t uplodaded=0; size_t uplodaded=0;
bool otherUpdate=false;
bool hasError(){ bool hasError(){
return ! error.isEmpty(); return ! error.isEmpty();
} }
@ -23,12 +24,12 @@ bool GwUpdate::delayedRestart(){
vTaskDelete(NULL); vTaskDelete(NULL);
},"reset",2000,logger,0,NULL) == pdPASS; },"reset",2000,logger,0,NULL) == pdPASS;
} }
GwUpdate::GwUpdate(GwLog *log, GwWebServer *webserver, PasswordChecker checker) GwUpdate::GwUpdate(GwLog *log, GwWebServer *webserver, PasswordChecker ckr)
{ {
this->checker=checker; this->checker=ckr;
this->logger = log; this->logger = log;
this->server = webserver->getServer(); this->server = webserver->getServer();
server->on("/update", HTTP_POST, [&](AsyncWebServerRequest *request) { server->on("/api/update", HTTP_POST, [&](AsyncWebServerRequest *request) {
// the request handler is triggered after the upload has finished... // the request handler is triggered after the upload has finished...
// create the response, add header, and send response // create the response, add header, and send response
String result="{\"status\":\"OK\"}"; String result="{\"status\":\"OK\"}";
@ -50,11 +51,13 @@ GwUpdate::GwUpdate(GwLog *log, GwWebServer *webserver, PasswordChecker checker)
if (!updateOk){ if (!updateOk){
result=jsonError(String("Update failed: ")+param->error); result=jsonError(String("Update failed: ")+param->error);
} }
if (! param->otherUpdate) {
updateRunning=0;
}
} }
AsyncWebServerResponse *response = request->beginResponse(200, "application/json",result); AsyncWebServerResponse *response = request->beginResponse(200, "application/json",result);
response->addHeader("Connection", "close"); response->addHeader("Connection", "close");
request->send(response); request->send(response);
updateRunning=false;
if (updateOk) delayedRestart(); if (updateOk) delayedRestart();
}, [&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { }, [&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
//Upload handler chunks in data //Upload handler chunks in data
@ -67,29 +70,37 @@ GwUpdate::GwUpdate(GwLog *log, GwWebServer *webserver, PasswordChecker checker)
param=new UpdateParam(); param=new UpdateParam();
request->_tempObject=param; request->_tempObject=param;
} }
if (updateRunning){ if (updateRunning > 0 && (updateRunning + 60000) >= millis()){
param->error="another update is running"; param->error="another update is running";
param->otherUpdate=true;
} }
else{ else{
updateRunning=true; updateRunning=millis();
} }
if (!param->hasError()) if (!param->hasError())
{ {
if (!request->hasParam("_hash", true)) AsyncWebParameter *hash=request->getParam("_hash");
if (! hash){
hash=request->getParam("_hash",true);
}
if (!hash)
{ {
LOG_DEBUG(GwLog::ERROR, "missing _hash in update"); LOG_DEBUG(GwLog::ERROR, "missing _hash in update");
param->error = "missing _hash"; param->error = "missing _hash";
} }
else else
{ {
if (!checker(request->getParam("_hash")->value())) String shash=hash->value();
LOG_DEBUG(GwLog::DEBUG,"checking hash %s",shash.c_str());
if (!checker(shash))
{ {
LOG_DEBUG(GwLog::ERROR, "invalid _hash in update"); LOG_DEBUG(GwLog::ERROR, "invalid _hash in update %s",shash.c_str());
param->error = "invalid password"; param->error = "invalid password";
} }
} }
} }
if (! param->hasError()){ if (! param->hasError()){
Update.abort(); //abort any dangling update
int cmd=U_FLASH; int cmd=U_FLASH;
if (!Update.begin(UPDATE_SIZE_UNKNOWN, cmd)) { // Start with max available size if (!Update.begin(UPDATE_SIZE_UNKNOWN, cmd)) { // Start with max available size
LOG_DEBUG(GwLog::ERROR,"unable to start update %s",Update.errorString()); LOG_DEBUG(GwLog::ERROR,"unable to start update %s",Update.errorString());
@ -107,6 +118,7 @@ GwUpdate::GwUpdate(GwLog *log, GwWebServer *webserver, PasswordChecker checker)
{ {
LOG_DEBUG(GwLog::ERROR, "invalid write, expected %d got %d", (int)len, (int)wr); LOG_DEBUG(GwLog::ERROR, "invalid write, expected %d got %d", (int)len, (int)wr);
param->error="unable to write"; param->error="unable to write";
Update.abort();
} }
else{ else{
param->uplodaded+=wr; param->uplodaded+=wr;
@ -115,10 +127,12 @@ GwUpdate::GwUpdate(GwLog *log, GwWebServer *webserver, PasswordChecker checker)
if (final && ! param->hasError()) if (final && ! param->hasError())
{ // if the final flag is set then this is the last frame of data { // if the final flag is set then this is the last frame of data
LOG_DEBUG(GwLog::DEBUG,"finalizing update");
if (!Update.end(true)) if (!Update.end(true))
{ //true to set the size to the current progress { //true to set the size to the current progress
LOG_DEBUG(GwLog::ERROR, "unable to end update %s", Update.errorString()); LOG_DEBUG(GwLog::ERROR, "unable to end update %s", Update.errorString());
param->error=String("unable to end update:") + String(Update.errorString()); param->error=String("unable to end update:") + String(Update.errorString());
Update.abort();
} }
} }
else else

View File

@ -7,7 +7,7 @@ class GwUpdate{
AsyncWebServer *server; AsyncWebServer *server;
GwLog *logger; GwLog *logger;
PasswordChecker checker; PasswordChecker checker;
bool updateRunning=false; unsigned long updateRunning=0;
public: public:
bool delayedRestart(); bool delayedRestart();
GwUpdate(GwLog *log,GwWebServer *webserver, PasswordChecker checker); GwUpdate(GwLog *log,GwWebServer *webserver, PasswordChecker checker);

View File

@ -169,7 +169,7 @@ bool checkPass(String hash){
return false; return false;
} }
GwUpdate updater(&logger,&webserver,checkPass); GwUpdate updater(&logger,&webserver,&checkPass);
void updateNMEACounter(int id,const char *msg,bool incoming,bool fail=false){ void updateNMEACounter(int id,const char *msg,bool incoming,bool fail=false){
//we rely on the msg being long enough //we rely on the msg being long enough

View File

@ -5,31 +5,57 @@ import sys
import http import http
import http.server import http.server
import urllib.request import urllib.request
import urllib.parse
import posixpath import posixpath
import os import os
import traceback
class RequestHandler(http.server.SimpleHTTPRequestHandler): class RequestHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self): def do_proxy(self):
p=self.path p=self.path
print("path=%s"%p) print("path=%s"%p)
if p.startswith("/api/"): if p.startswith("/api/"):
apiurl=self.server.proxyUrl apiurl=self.server.proxyUrl
url=apiurl+p.replace("/api","") url=apiurl+p.replace("/api","")
hostname=urllib.parse.urlparse(url).netloc
print("proxy to %s"%url) print("proxy to %s"%url)
try: try:
with urllib.request.urlopen(url,timeout=10) as response: body = None
if self.headers.get('content-length') is not None:
content_len = int(self.headers.get('content-length'))
print("reading %d bytes body"%content_len)
body = self.rfile.read(content_len)
# set new headers
new_headers = {}
for item in self.headers.items():
new_headers[item[0]] = item[1]
new_headers['host'] = hostname
try:
del new_headers['accept-encoding']
except KeyError:
pass
req=urllib.request.Request(url,headers=new_headers,data=body)
with urllib.request.urlopen(req,timeout=50) as response:
self.send_response(http.HTTPStatus.OK) self.send_response(http.HTTPStatus.OK)
self.send_header("Content-type", response.getheader("Content-type")) for h in response.getheaders():
self.send_header(h[0],h[1])
self.end_headers() self.end_headers()
shutil.copyfileobj(response,self.wfile) shutil.copyfileobj(response,self.wfile)
return True
return None return None
self.send_error(http.HTTPStatus.NOT_FOUND, "api not found") self.send_error(http.HTTPStatus.NOT_FOUND, "api not found")
return None return None
except Exception as e: except Exception as e:
print("Exception: %s"%traceback.format_exc())
self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, "api error %s"%str(e)) self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, "api error %s"%str(e))
return None return None
super().do_GET() def do_GET(self):
if not self.do_proxy():
super().do_GET()
def do_POST(self):
if not self.do_proxy():
super().do_POST()
def translate_path(self, path): def translate_path(self, path):
"""Translate a /-separated PATH to the local filename syntax. """Translate a /-separated PATH to the local filename syntax.

View File

@ -20,6 +20,7 @@
<div class="tab" data-page="configPage">Config</div> <div class="tab" data-page="configPage">Config</div>
<div class="tab" data-page="xdrPage">XDR</div> <div class="tab" data-page="xdrPage">XDR</div>
<div class="tab" data-page="dashboardPage">Data</div> <div class="tab" data-page="dashboardPage">Data</div>
<div class="tab" data-page="updatePage">Update</div>
</div> </div>
<div id="statusPage" class="tabPage"> <div id="statusPage" class="tabPage">
<div id="statusPageContent"> <div id="statusPageContent">
@ -74,6 +75,12 @@
<div class="tabPage hidden" id="dashboardPage"> <div class="tabPage hidden" id="dashboardPage">
</div> </div>
<div class="tabPage hidden" id="updatePage">
<form action="api/update" method="post" id="uploadForm">
<input type="file" name="file1" id="uploadFile">
</form>
<button id="uploadBin">Upload</button>
</div>
</div> </div>
<div class="overlayContainer hidden" id="overlayContainer"> <div class="overlayContainer hidden" id="overlayContainer">

View File

@ -1445,7 +1445,23 @@ function updateDashboard(data) {
}); });
} }
} }
function uploadBin(){
let el=document.getElementById("uploadFile");
if (! el) return;
if ( el.files.length < 1) return;
ensurePass()
.then (function(hash){
let req = new XMLHttpRequest();
req.onloadend=function(){
alert("upload complete");
}
let formData = new FormData();
formData.append("file1", el.files[0]);
req.open("POST", '/api/update?_hash='+encodeURIComponent(hash));
req.send(formData);
})
.catch(function(e){});
}
window.setInterval(update, 1000); window.setInterval(update, 1000);
window.setInterval(function () { window.setInterval(function () {
let dp = document.getElementById('dashboardPage'); let dp = document.getElementById('dashboardPage');