Complete browser OTA responses before reboot and allow HTTP restarts

This commit is contained in:
mikecarper committed 2026-10-05 11:52:24 -07:00
1 parent 8fbf948d68
commit e95cf6d618
10 files changed
+449 -19

No files matched your search

+3
View File
@@ -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;
+1
View File
@@ -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
+53
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+32 -2
View File
@@ -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;
+74
View File
@@ -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()
+7 -1
View File
@@ -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 {