mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 09:52:23 +00:00
Complete browser OTA responses before reboot and allow HTTP restarts
This commit is contained in:
1 parent
8fbf948d68
commit
e95cf6d618
10 files changed
+449
-19
No files matched your search
@@ -255,6 +255,9 @@ jobs:
|
||||
- name: Verify WiFi OTA startup and WebConfig handoff
|
||||
run: python3 -B test/test_wifi_ota_start.py -v
|
||||
|
||||
- name: Verify browser OTA completion and immediate HTTP listener restart
|
||||
run: python3 -B test/test_async_web_lifecycle.py -v
|
||||
|
||||
- name: Verify GPS status distinguishes user settings from receiver sleep
|
||||
run: |
|
||||
python3 -B test/test_gps_status.py
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
#include <AsyncElegantOTA.h>
|
||||
#include <atomic>
|
||||
static std::atomic<bool> ota_uploads_enabled{false};
|
||||
static std::atomic<bool> ota_upload_busy{false};
|
||||
#if defined(ESP32)
|
||||
static std::atomic<bool> ota_reboot_pending{false};
|
||||
#endif
|
||||
// All upload callbacks run on AsyncTCP's task. Update is a global singleton;
|
||||
// retain one request as its owner until that request disconnects.
|
||||
static AsyncWebServerRequest* ota_upload_owner = nullptr;
|
||||
|
||||
AsyncElegantOtaClass AsyncElegantOTA;
|
||||
|
||||
@@ -8,6 +17,7 @@ void AsyncElegantOtaClass::setID(const char* id){
|
||||
|
||||
void AsyncElegantOtaClass::begin(AsyncWebServer *server, const char* username, const char* password){
|
||||
_server = server;
|
||||
setEnabled(true);
|
||||
|
||||
if(strlen(username) > 0){
|
||||
_authRequired = true;
|
||||
@@ -49,13 +59,13 @@ void AsyncElegantOtaClass::begin(AsyncWebServer *server, const char* username, c
|
||||
return request->requestAuthentication();
|
||||
}
|
||||
}
|
||||
// the request handler is triggered after the upload has finished...
|
||||
// create the response, add header, and send response
|
||||
AsyncWebServerResponse *response = request->beginResponse((Update.hasError())?500:200, "text/plain", (Update.hasError())?"FAIL":"OK");
|
||||
if (request->isSent()) return; // Preserve an earlier upload error.
|
||||
const bool complete = request->_tempObject
|
||||
&& *static_cast<bool*>(request->_tempObject) && !Update.hasError();
|
||||
AsyncWebServerResponse *response = request->beginResponse(complete?200:500, "text/plain", complete?"OK":"FAIL");
|
||||
response->addHeader("Connection", "close");
|
||||
response->addHeader("Access-Control-Allow-Origin", "*");
|
||||
request->send(response);
|
||||
restart();
|
||||
}, [&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
//Upload handler chunks in data
|
||||
if(_authRequired){
|
||||
@@ -65,11 +75,30 @@ void AsyncElegantOtaClass::begin(AsyncWebServer *server, const char* username, c
|
||||
}
|
||||
|
||||
if (!index) {
|
||||
if (!ota_uploads_enabled.load()) {
|
||||
return request->send(503, "text/plain", "OTA stopped");
|
||||
}
|
||||
bool expected = false;
|
||||
if (request->_tempObject
|
||||
#if defined(ESP32)
|
||||
|| ota_reboot_pending.load()
|
||||
#endif
|
||||
|| !ota_upload_busy.compare_exchange_strong(expected, true)
|
||||
) {
|
||||
return request->send(409, "text/plain", "OTA upload already active");
|
||||
}
|
||||
// A stop can race the first check before ownership was reserved.
|
||||
if (!ota_uploads_enabled.load()) {
|
||||
ota_upload_busy.store(false);
|
||||
return request->send(503, "text/plain", "OTA stopped");
|
||||
}
|
||||
if(!request->hasParam("MD5", true)) {
|
||||
ota_upload_busy.store(false);
|
||||
return request->send(400, "text/plain", "MD5 parameter missing");
|
||||
}
|
||||
|
||||
if(!Update.setMD5(request->getParam("MD5", true)->value().c_str())) {
|
||||
ota_upload_busy.store(false);
|
||||
return request->send(400, "text/plain", "MD5 parameter invalid");
|
||||
}
|
||||
|
||||
@@ -84,10 +113,34 @@ void AsyncElegantOtaClass::begin(AsyncWebServer *server, const char* username, c
|
||||
if (!Update.begin(UPDATE_SIZE_UNKNOWN, cmd)) { // Start with max available size
|
||||
#endif
|
||||
Update.printError(Serial);
|
||||
ota_upload_busy.store(false);
|
||||
return request->send(400, "text/plain", "OTA could not begin");
|
||||
}
|
||||
// AsyncWebServer frees this per-request allocation on disconnect.
|
||||
// hasError() alone also reports success for a POST with no upload.
|
||||
request->_tempObject = calloc(1, sizeof(bool));
|
||||
if (!request->_tempObject) {
|
||||
Update.abort();
|
||||
ota_upload_busy.store(false);
|
||||
return request->send(503, "text/plain", "OTA state allocation failed");
|
||||
}
|
||||
ota_upload_owner = request;
|
||||
request->onDisconnect([this, request]() {
|
||||
if (ota_upload_owner != request) return;
|
||||
ota_upload_owner = nullptr;
|
||||
if (*static_cast<bool*>(request->_tempObject)) {
|
||||
// Let the response drain and the network callback return
|
||||
// before rebooting from the separate restart task.
|
||||
restart();
|
||||
} else {
|
||||
Update.abort(); // Release a disconnected, partial upload.
|
||||
}
|
||||
ota_upload_busy.store(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (request->isSent() || ota_upload_owner != request) return;
|
||||
|
||||
// Write chunked data to the free sketch space
|
||||
if(len){
|
||||
if (Update.write(data, len) != len) {
|
||||
@@ -100,21 +153,53 @@ void AsyncElegantOtaClass::begin(AsyncWebServer *server, const char* username, c
|
||||
Update.printError(Serial);
|
||||
return request->send(400, "text/plain", "Could not end OTA");
|
||||
}
|
||||
*static_cast<bool*>(request->_tempObject) = true;
|
||||
}else{
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool AsyncElegantOtaClass::setEnabled(bool enabled) {
|
||||
if (enabled) {
|
||||
ota_uploads_enabled.store(true);
|
||||
return true;
|
||||
}
|
||||
const bool was_enabled = ota_uploads_enabled.exchange(false);
|
||||
if (ota_upload_busy.load()
|
||||
#if defined(ESP32)
|
||||
|| ota_reboot_pending.load()
|
||||
#endif
|
||||
) {
|
||||
ota_uploads_enabled.store(was_enabled);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// deprecated, keeping for backward compatibility
|
||||
void AsyncElegantOtaClass::loop() {
|
||||
}
|
||||
|
||||
void AsyncElegantOtaClass::restart() {
|
||||
#if defined(ESP32)
|
||||
bool expected = false;
|
||||
if (!ota_reboot_pending.compare_exchange_strong(expected, true)) return;
|
||||
// Even disconnect handlers run on the shared AsyncTCP task. Return to it
|
||||
// immediately, then reboot from a separate task after final TCP cleanup.
|
||||
if (xTaskCreate([](void*) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
ESP.restart();
|
||||
vTaskDelete(nullptr);
|
||||
}, "ota-reboot", 3072, nullptr, 1, nullptr) != pdPASS) {
|
||||
ota_reboot_pending.store(false);
|
||||
}
|
||||
#else
|
||||
yield();
|
||||
delay(1000);
|
||||
yield();
|
||||
ESP.restart();
|
||||
#endif
|
||||
}
|
||||
|
||||
String AsyncElegantOtaClass::getID(){
|
||||
|
||||
@@ -33,6 +33,10 @@ class AsyncElegantOtaClass{
|
||||
loop(),
|
||||
restart();
|
||||
|
||||
// Stop admission atomically with upload ownership. Existing GET
|
||||
// responses may drain while the containing listener is stopped.
|
||||
bool setEnabled(bool enabled);
|
||||
|
||||
private:
|
||||
AsyncWebServer *_server;
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ extra_scripts =
|
||||
pre:scripts/esp32_ota_heap_context.py
|
||||
pre:scripts/esp32_usb_session_fix.py
|
||||
pre:scripts/esp32_file_buffer_fix.py
|
||||
pre:scripts/esp32_async_web_fix.py
|
||||
merge-bin.py
|
||||
post:tools/mota/pio_endf.py
|
||||
post:scripts/check_esp32_dram.py
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Build private AsyncTCP fixes and the current vendored OTA implementation."""
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PINNED_ASYNCTCP_SHA256 = "27e3e22a6030cc5c9c9f32f28d0a3a36189b1ea6a5fa270777b2d2790a89c365"
|
||||
ORIGINAL_BIND = " msg->err = tcp_bind(pcb, msg->bind.addr, msg->bind.port);"
|
||||
REUSABLE_BIND = """ // A stopped HTTP listener may still have completed connections in
|
||||
// TIME_WAIT. Permit immediate rebinding during WebConfig/OTA handoffs.
|
||||
#if SO_REUSE
|
||||
ip_set_option(pcb, SOF_REUSEADDR);
|
||||
#endif
|
||||
""" + ORIGINAL_BIND
|
||||
|
||||
|
||||
def patched_async_tcp(source):
|
||||
source = source.replace("\r\n", "\n")
|
||||
if (hashlib.sha256(source.encode("ascii")).hexdigest() != PINNED_ASYNCTCP_SHA256
|
||||
or source.count(ORIGINAL_BIND) != 1):
|
||||
raise RuntimeError("AsyncTCP restart fix: changed pinned source; review library update")
|
||||
return source.replace(ORIGINAL_BIND, REUSABLE_BIND, 1)
|
||||
|
||||
|
||||
def replace_async_source(build_env, node):
|
||||
source = Path(node.srcnode().get_abspath())
|
||||
if source.name == "AsyncTCP.cpp":
|
||||
content = patched_async_tcp(source.read_text(encoding="utf-8"))
|
||||
include_dir = source.parent
|
||||
elif source.name == "AsyncElegantOTA.cpp":
|
||||
# file:// dependencies can retain an earlier copy in .pio/libdeps.
|
||||
# Compile this checkout's vendored source without editing that cache.
|
||||
include_dir = Path(build_env.subst("$PROJECT_DIR")) / "arch/esp32/AsyncElegantOTA/src"
|
||||
content = (include_dir / source.name).read_text(encoding="ascii")
|
||||
else:
|
||||
return node
|
||||
destination = Path(build_env.subst("$BUILD_DIR")) / "patched-esp32-web" / source.name
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not destination.exists() or destination.read_text(encoding="ascii") != content:
|
||||
destination.write_text(content, encoding="ascii")
|
||||
build_env.PrependUnique(CPPPATH=[str(include_dir)])
|
||||
return build_env.File(str(destination))
|
||||
|
||||
|
||||
def install(build_env):
|
||||
build_env.PrependUnique(CPPPATH=[str(Path(build_env.subst("$PROJECT_DIR"))
|
||||
/ "arch/esp32/AsyncElegantOTA/src")])
|
||||
build_env.AddBuildMiddleware(replace_async_source, "*AsyncTCP*src*AsyncTCP.cpp")
|
||||
build_env.AddBuildMiddleware(replace_async_source, "*AsyncElegantOTA*src*AsyncElegantOTA.cpp")
|
||||
|
||||
|
||||
if "Import" in globals():
|
||||
Import("env")
|
||||
install(env)
|
||||
+47
-12
@@ -366,9 +366,14 @@ bool ESP32Board::stopOTAUpdate(char reply[]) {
|
||||
#include <AsyncTCP.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncElegantOTA.h>
|
||||
#include <new>
|
||||
|
||||
#include <SPIFFS.h>
|
||||
|
||||
// Requests retain raw pointers to the AsyncWebServer and its route handlers.
|
||||
// Keep this one host alive across stops; end() only closes its listener.
|
||||
static AsyncWebServer* async_ota_host = nullptr;
|
||||
|
||||
bool ESP32Board::startOTAUpdate(const char* id, char reply[], bool force_ap) {
|
||||
if (!mesh::wireless::control().allowWiFiForOTA()) {
|
||||
strcpy(reply, "ERR: wireless change pending; retry start ota");
|
||||
@@ -413,25 +418,52 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[], bool force_ap) {
|
||||
|
||||
// Select the requested network even when the HTTP listener already exists.
|
||||
// In particular, `start ota ap` must work after an earlier LAN-mode start.
|
||||
if (ota_server != nullptr) return true;
|
||||
if (ota_server != nullptr) {
|
||||
if (ota_server->state() != LISTEN) ota_server->begin();
|
||||
if (ota_server->state() != LISTEN) {
|
||||
strcpy(reply, "ERR: OTA server failed; retry start ota");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static char id_buf[60];
|
||||
snprintf(id_buf, sizeof(id_buf), "%s (%s)", id, getManufacturerName());
|
||||
static char home_buf[90];
|
||||
snprintf(home_buf, sizeof(home_buf), "<H2>Hi! I am a MeshCore Repeater. ID: %s</H2>", id);
|
||||
|
||||
ota_server = new AsyncWebServer(80);
|
||||
|
||||
ota_server->on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/html", home_buf);
|
||||
});
|
||||
ota_server->on("/log", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(SPIFFS, "/packet_log", "text/plain");
|
||||
});
|
||||
|
||||
if (async_ota_host == nullptr) {
|
||||
async_ota_host = new (std::nothrow) AsyncWebServer(80);
|
||||
if (async_ota_host == nullptr) {
|
||||
if (ota_started_ap) WiFi.softAPdisconnect(true);
|
||||
ota_started_ap = false;
|
||||
inhibit_sleep = false;
|
||||
strcpy(reply, "ERR: OTA server allocation failed");
|
||||
return false;
|
||||
}
|
||||
async_ota_host->on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/html", home_buf);
|
||||
});
|
||||
async_ota_host->on("/log", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(SPIFFS, "/packet_log", "text/plain");
|
||||
});
|
||||
AsyncElegantOTA.begin(async_ota_host);
|
||||
}
|
||||
ota_server = async_ota_host;
|
||||
AsyncElegantOTA.setID(id_buf);
|
||||
AsyncElegantOTA.begin(ota_server); // Start ElegantOTA
|
||||
AsyncElegantOTA.setEnabled(true);
|
||||
ota_server->begin();
|
||||
if (ota_server->state() != LISTEN) {
|
||||
// begin() returns void even when the port could not be bound.
|
||||
AsyncElegantOTA.setEnabled(false);
|
||||
ota_server->end();
|
||||
ota_server = nullptr;
|
||||
if (ota_started_ap) WiFi.softAPdisconnect(true);
|
||||
ota_started_ap = false;
|
||||
inhibit_sleep = false;
|
||||
strcpy(reply, "ERR: OTA server failed; retry start ota");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -442,8 +474,11 @@ bool ESP32Board::stopOTAUpdate(char reply[]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!AsyncElegantOTA.setEnabled(false)) {
|
||||
strcpy(reply, "ERR: OTA upload active; retry stop ota after it finishes");
|
||||
return false;
|
||||
}
|
||||
ota_server->end();
|
||||
delete ota_server;
|
||||
ota_server = nullptr;
|
||||
if (ota_started_ap) WiFi.softAPdisconnect(true);
|
||||
ota_started_ap = false;
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
using String = std::string;
|
||||
constexpr int HTTP_GET=0, HTTP_POST=1, U_FLASH=0, U_SPIFFS=100;
|
||||
constexpr unsigned UPDATE_SIZE_UNKNOWN=~0U;
|
||||
constexpr int pdPASS=1;
|
||||
#define pdMS_TO_TICKS(value) (value)
|
||||
const char ELEGANT_HTML[] = "page";
|
||||
const unsigned ELEGANT_HTML_SIZE=sizeof(ELEGANT_HTML);
|
||||
int Serial=0;
|
||||
bool in_network_callback=false;
|
||||
struct Esp {
|
||||
int restarts=0;
|
||||
void restart() { assert(!in_network_callback); ++restarts; }
|
||||
} ESP;
|
||||
void yield() { assert(!in_network_callback); }
|
||||
void delay(unsigned) { assert(!in_network_callback); }
|
||||
void vTaskDelay(unsigned) { assert(!in_network_callback); }
|
||||
void vTaskDelete(void*) {}
|
||||
using Task = void (*)(void*);
|
||||
Task pending_task=nullptr;
|
||||
bool allow_task=true;
|
||||
int xTaskCreate(Task task, const char*, unsigned, void*, unsigned, void*) {
|
||||
if (!allow_task) return 0;
|
||||
assert(!pending_task); pending_task=task; return pdPASS;
|
||||
}
|
||||
@REBOOT_STATE@
|
||||
struct Parameter { String text="valid"; String value() { return text; } };
|
||||
struct Response {
|
||||
int code;
|
||||
void addHeader(const char*, const char*) {}
|
||||
};
|
||||
struct Request {
|
||||
void* _tempObject=nullptr;
|
||||
bool md5_present=true, authorized=true, sent=false;
|
||||
int code=0;
|
||||
std::function<void()> disconnected;
|
||||
Parameter md5;
|
||||
~Request() { free(_tempObject); }
|
||||
bool authenticate(const char*, const char*) { return authorized; }
|
||||
void requestAuthentication() { code=401; sent=true; }
|
||||
bool hasParam(const char*, bool) { return md5_present; }
|
||||
Parameter* getParam(const char*, bool) { return &md5; }
|
||||
Response* beginResponse(int code, const char*, const char*) { return new Response{code}; }
|
||||
Response* beginResponse_P(int code, const char*, const char*, unsigned) { return new Response{code}; }
|
||||
void send(Response* response) { code=response->code; sent=true; delete response; }
|
||||
void send(int status, const char*, const char*) { code=status; sent=true; }
|
||||
void send(int status, const char*, const String&) { code=status; sent=true; }
|
||||
bool isSent() const { return sent; }
|
||||
void onDisconnect(std::function<void()> fn) { disconnected=fn; }
|
||||
};
|
||||
using AsyncWebServerRequest=Request;
|
||||
static AsyncWebServerRequest* ota_upload_owner=nullptr;
|
||||
using AsyncWebServerResponse=Response;
|
||||
struct AsyncWebServer {
|
||||
std::function<void(Request*)> completion;
|
||||
std::function<void(Request*,String,size_t,uint8_t*,size_t,bool)> upload;
|
||||
template<class F> void on(const char*, int, F) {}
|
||||
template<class F,class U> void on(const char*, int method, F callback, U chunks) {
|
||||
assert(method==HTTP_POST); completion=callback; upload=chunks;
|
||||
}
|
||||
};
|
||||
struct UpdateFake {
|
||||
bool error=false, fail_end=false;
|
||||
int aborts=0;
|
||||
bool hasError() const { return error; }
|
||||
bool setMD5(const char* md5) { return String(md5)=="valid"; }
|
||||
bool begin(unsigned, int) { error=false; return true; }
|
||||
size_t write(uint8_t*, size_t size) { return size; }
|
||||
bool end(bool) { error=fail_end; return !error; }
|
||||
void abort() { error=true; ++aborts; }
|
||||
void printError(int) {}
|
||||
} Update;
|
||||
struct AsyncElegantOtaClass {
|
||||
AsyncWebServer* _server=nullptr;
|
||||
bool _authRequired=false;
|
||||
String _username, _password, _id="fixture";
|
||||
void begin(AsyncWebServer*, const char*, const char*);
|
||||
void restart();
|
||||
bool setEnabled(bool);
|
||||
};
|
||||
@BEGIN@
|
||||
@RESTART@
|
||||
|
||||
int main() {
|
||||
AsyncWebServer server;
|
||||
AsyncElegantOtaClass ota;
|
||||
ota.begin(&server,"","");
|
||||
auto complete=[&](Request& request) {
|
||||
in_network_callback=true; server.completion(&request); in_network_callback=false;
|
||||
};
|
||||
auto upload=[&](Request& request) {
|
||||
uint8_t data[8]={};
|
||||
in_network_callback=true;
|
||||
server.upload(&request,"firmware",0,data,sizeof(data),true);
|
||||
in_network_callback=false;
|
||||
};
|
||||
// A POST without an actual completed upload cannot report OK or reboot.
|
||||
Request empty;
|
||||
complete(empty);
|
||||
assert(empty.code>=400 && !pending_task && ESP.restarts==0);
|
||||
Request invalid;
|
||||
invalid.md5_present=false;
|
||||
upload(invalid); complete(invalid);
|
||||
assert(invalid.code==400 && !pending_task && ESP.restarts==0);
|
||||
Request corrupt;
|
||||
Update.fail_end=true;
|
||||
upload(corrupt); complete(corrupt);
|
||||
assert(corrupt.code==400 && !pending_task && ESP.restarts==0);
|
||||
in_network_callback=true; corrupt.disconnected(); in_network_callback=false;
|
||||
assert(Update.aborts==1 && ota_upload_owner==nullptr);
|
||||
Update.fail_end=false;
|
||||
Request partial;
|
||||
uint8_t chunk[4]={};
|
||||
server.upload(&partial,"firmware",0,chunk,sizeof(chunk),false);
|
||||
assert(!ota.setEnabled(false)); // An admitted upload keeps its listener alive.
|
||||
Request concurrent;
|
||||
upload(concurrent); complete(concurrent);
|
||||
assert(concurrent.code==409 && ota_upload_owner==&partial);
|
||||
in_network_callback=true; partial.disconnected(); in_network_callback=false;
|
||||
assert(Update.aborts==2 && ota_upload_owner==nullptr && ESP.restarts==0);
|
||||
assert(ota.setEnabled(false));
|
||||
Request stopped;
|
||||
upload(stopped); complete(stopped);
|
||||
assert(stopped.code==503 && !ota_upload_owner && !pending_task);
|
||||
assert(ota.setEnabled(true));
|
||||
Request valid;
|
||||
upload(valid); complete(valid);
|
||||
assert(valid.code==200 && valid.disconnected && !pending_task && ESP.restarts==0);
|
||||
// The response drains before disconnect; its callback only schedules work.
|
||||
in_network_callback=true; valid.disconnected(); in_network_callback=false;
|
||||
assert(pending_task && ESP.restarts==0);
|
||||
auto task=pending_task; pending_task=nullptr; task(nullptr);
|
||||
assert(ESP.restarts==1);
|
||||
}
|
||||
Vendored
+32
-2
@@ -12,6 +12,8 @@ constexpr int WL_CONNECTED = 3;
|
||||
constexpr int HTTP_GET = 0;
|
||||
constexpr int WIFI_AP = 2;
|
||||
constexpr int ESP_OK = 0, WIFI_IF_AP = 1;
|
||||
constexpr int LISTEN = 10;
|
||||
bool async_bind_allowed = true;
|
||||
constexpr uint8_t WIFI_PROTOCOL_11B = 1, WIFI_PROTOCOL_11G = 2;
|
||||
constexpr uint8_t WIFI_PROTOCOL_11N = 4, WIFI_PROTOCOL_LR = 8;
|
||||
using esp_err_t = int;
|
||||
@@ -60,15 +62,20 @@ struct AsyncWebServerRequest {
|
||||
void send(int, const char*, const char*, const char*) {}
|
||||
};
|
||||
struct AsyncWebServer {
|
||||
bool listening = false;
|
||||
explicit AsyncWebServer(int) {}
|
||||
template<class F> void on(const char*, int, F) {}
|
||||
void begin() { ++server_starts; }
|
||||
void end() {}
|
||||
void begin() { ++server_starts; listening = async_bind_allowed; }
|
||||
void end() { listening = false; }
|
||||
int state() const { return listening ? LISTEN : 0; }
|
||||
};
|
||||
struct Elegant {
|
||||
bool busy = false;
|
||||
void setID(const char*) {}
|
||||
void begin(AsyncWebServer*) {}
|
||||
bool setEnabled(bool enabled) { return enabled || !busy; }
|
||||
} AsyncElegantOTA;
|
||||
static AsyncWebServer* async_ota_host = nullptr;
|
||||
static int SPIFFS;
|
||||
struct ESP32Board {
|
||||
bool inhibit_sleep = false;
|
||||
@@ -141,6 +148,21 @@ int main() {
|
||||
WiFi.ap = false; // An external driver stop invalidates cached AP ownership.
|
||||
start(true, "192.168.4.1", "Join WiFi MeshCore-OTA");
|
||||
assert(WiFi.starts == ap_starts + 1);
|
||||
#ifndef LIGHTWEIGHT_WIFI_OTA
|
||||
auto* existing_server = board.ota_server;
|
||||
existing_server->listening = false;
|
||||
async_bind_allowed = false;
|
||||
assert(!board.startOTAUpdate("test node", reply.text, true));
|
||||
assert(strstr(reply.text, "OTA server failed") && board.inhibit_sleep);
|
||||
async_bind_allowed = true;
|
||||
start(true, "192.168.4.1", "Join WiFi MeshCore-OTA");
|
||||
assert(board.ota_server == existing_server && existing_server->listening);
|
||||
AsyncElegantOTA.busy = true;
|
||||
assert(!board.stopOTAUpdate(reply.text));
|
||||
assert(strstr(reply.text, "OTA upload active") && board.inhibit_sleep);
|
||||
assert(board.ota_server == existing_server && existing_server->listening);
|
||||
AsyncElegantOTA.busy = false;
|
||||
#endif
|
||||
#ifdef LIGHTWEIGHT_WIFI_OTA
|
||||
lightweight_ota_server.allow_stop = false;
|
||||
assert(!board.stopOTAUpdate(reply.text));
|
||||
@@ -165,6 +187,14 @@ int main() {
|
||||
assert(!board.startOTAUpdate("test node", reply.text, false));
|
||||
assert(strcmp(reply.text, "ERR: OTA WiFi failed") == 0);
|
||||
assert(!board.inhibit_sleep && !board.ota_server);
|
||||
#ifndef LIGHTWEIGHT_WIFI_OTA
|
||||
WiFi.allow_ap = true;
|
||||
async_bind_allowed = false;
|
||||
assert(!board.startOTAUpdate("test node", reply.text, false));
|
||||
assert(strstr(reply.text, "OTA server failed"));
|
||||
assert(!board.inhibit_sleep && !board.ota_server && !WiFi.ap);
|
||||
async_bind_allowed = true;
|
||||
#endif
|
||||
// A pending wireless shutdown must not race a newly started uploader.
|
||||
struct Backend : mesh::wireless::Backend {
|
||||
uint8_t active = mesh::wireless::All;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Execute the vendored uploader and pinned TCP listener restart path."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import urllib.request
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
import test_wifi_ota_start as wifi_start
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
spec=importlib.util.spec_from_file_location("async_web_fix", ROOT/"scripts/esp32_async_web_fix.py")
|
||||
fix=importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(fix)
|
||||
|
||||
|
||||
class AsyncWebLifecycleTest(unittest.TestCase):
|
||||
compile_and_run=wifi_start.WiFiOtaStartTest.compile_and_run
|
||||
|
||||
def test_upload_response_drains_before_deferred_reboot(self):
|
||||
source=(ROOT/"arch/esp32/AsyncElegantOTA/src/AsyncElegantOTA.cpp").read_text()
|
||||
fixture=(ROOT/"test/fixtures/async_ota_completion.cpp").read_text()
|
||||
state="""static std::atomic<bool> ota_reboot_pending{false};
|
||||
static std::atomic<bool> ota_uploads_enabled{false};
|
||||
static std::atomic<bool> ota_upload_busy{false};"""
|
||||
# The two preprocessor branches for Update.begin each open the same
|
||||
# logical block, so extract this method by its next declaration.
|
||||
begin=source[source.index("void AsyncElegantOtaClass::begin("):source.index("// deprecated")]
|
||||
self.compile_and_run(fixture.replace("@REBOOT_STATE@",state)
|
||||
.replace("@BEGIN@",begin)
|
||||
.replace("@RESTART@",extract_braced(source,"void AsyncElegantOtaClass::restart()")),"-DESP32=1")
|
||||
|
||||
def test_tcp_listener_can_rebind_with_completed_connections(self):
|
||||
cached=ROOT/".pio/libdeps/heltec_v4_repeater_observer_mqtt/AsyncTCP/src/AsyncTCP.cpp"
|
||||
source=(cached.read_text() if cached.exists() else urllib.request.urlopen(
|
||||
"https://raw.githubusercontent.com/ESP32Async/AsyncTCP/v3.5.0/src/AsyncTCP.cpp",
|
||||
timeout=30).read().decode("ascii"))
|
||||
patched=fix.patched_async_tcp(source)
|
||||
fixture=r'''
|
||||
#include <cassert>
|
||||
#define SO_REUSE 1
|
||||
constexpr int SOF_REUSEADDR=4,ERR_OK=0,ERR_USE=-8;
|
||||
using err_t=int;
|
||||
struct tcp_pcb { int flags=0; };
|
||||
struct tcpip_api_call_data {};
|
||||
struct tcp_api_call_t : tcpip_api_call_data {
|
||||
tcp_pcb** pcb;
|
||||
struct { void* addr=nullptr; unsigned short port=80; } bind;
|
||||
int err=0;
|
||||
};
|
||||
bool active_listener=false,time_wait=true;
|
||||
void ip_set_option(tcp_pcb* pcb,int flags) { pcb->flags|=flags; }
|
||||
int tcp_bind(tcp_pcb* pcb,void*,unsigned short) {
|
||||
return active_listener || (time_wait && !(pcb->flags&SOF_REUSEADDR)) ? ERR_USE : ERR_OK;
|
||||
}
|
||||
int tcp_close(tcp_pcb*) { return ERR_OK; }
|
||||
void tcp_abort(tcp_pcb*) { assert(false); }
|
||||
@BIND@
|
||||
int main() {
|
||||
tcp_pcb pcb,*pointer=&pcb;
|
||||
tcp_api_call_t message; message.pcb=&pointer;
|
||||
assert(_tcp_bind_api(&message)==ERR_OK && pointer==&pcb);
|
||||
active_listener=true;
|
||||
assert(_tcp_bind_api(&message)==ERR_USE && pointer==nullptr);
|
||||
}
|
||||
'''
|
||||
self.compile_and_run(fixture.replace("@BIND@",extract_braced(patched,"static err_t _tcp_bind_api(")))
|
||||
with self.assertRaises(AssertionError):
|
||||
self.compile_and_run(fixture.replace("@BIND@",extract_braced(source,"static err_t _tcp_bind_api(")))
|
||||
with self.assertRaisesRegex(RuntimeError,"changed pinned source"):
|
||||
fix.patched_async_tcp(source+"\n// unreviewed change\n")
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
unittest.main()
|
||||
@@ -39,6 +39,7 @@ STUBS = r'''
|
||||
constexpr int WL_CONNECTED = 3, WL_DISCONNECTED = 6, HTTP_GET = 0;
|
||||
constexpr int WIFI_AP = 2;
|
||||
constexpr int ESP_OK = 0;
|
||||
constexpr int LISTEN = 10;
|
||||
|
||||
struct IPAddress {
|
||||
uint32_t value = 0;
|
||||
@@ -102,14 +103,19 @@ struct AsyncWebServerRequest {
|
||||
template<class... Args> void send(Args...) {}
|
||||
};
|
||||
struct AsyncWebServer {
|
||||
bool listening = false;
|
||||
explicit AsyncWebServer(int port) { assert(port == 80); ++server_count; }
|
||||
template<class Handler> void on(const char*, int, Handler) {}
|
||||
void begin() { ++server_begin_calls; }
|
||||
void begin() { ++server_begin_calls; listening = true; }
|
||||
void end() { listening = false; }
|
||||
int state() const { return listening ? LISTEN : 0; }
|
||||
};
|
||||
struct MockElegantOTA {
|
||||
void setID(const char*) {}
|
||||
void begin(AsyncWebServer* server) { assert(server != nullptr); }
|
||||
bool setEnabled(bool) { return true; }
|
||||
} AsyncElegantOTA;
|
||||
static AsyncWebServer* async_ota_host = nullptr;
|
||||
struct MockSPIFFS {} SPIFFS;
|
||||
|
||||
class ESP32Board {
|
||||
|
||||
Reference in new issue
Block a user