From c75d8e007b7b9300bf4bd64b2a1ca8921baad40b Mon Sep 17 00:00:00 2001 From: sib Date: Tue, 14 Jul 2026 20:10:05 +0200 Subject: [PATCH] feat: OTA firmware and config updates over wifi Adds ArduinoOTA to the NerdminerV2 env so the firmware and the SPIFFS config can be flashed over wifi: pio run -e NerdminerV2-OTA -t upload --upload-port pio run -e NerdminerV2-OTA -t uploadfs --upload-port - 16MB dual-app partition table (default_16MB.csv) for A/B OTA slots. - mDNS hostname nerdminer-, advertising fw_version in its TXT record. - OTA password taken from the NERDMINER_OTA_PWD env var at build time; the build fails (static_assert) if it is unset, so no password is committed. - During a transfer the miner tasks idle at a safe point via an ota_active flag and RELEASE the SHA hardware lock first: otherwise suspending a hw miner mid-hash keeps that lock and Update.end()'s image SHA-256 check deadlocks (transfer reaches 100%, device hangs). The monitor and stratum tasks (which don't touch the SHA engine) are simply suspended. - A watchdog task reboots the device if a transfer stalls >90s, since ArduinoOTA.handle() blocks inside loop(). --- platformio.ini | 23 +++++--- src/NerdMinerV2.ino.cpp | 113 +++++++++++++++++++++++++++++++++++++--- src/mining.cpp | 10 ++++ src/mining.h | 3 ++ 4 files changed, 135 insertions(+), 14 deletions(-) diff --git a/platformio.ini b/platformio.ini index 6fff6673..4ecdceea 100644 --- a/platformio.ini +++ b/platformio.ini @@ -611,12 +611,10 @@ monitor_filters = board_build.arduino.memory_type = qio_opi monitor_speed = 115200 upload_speed = 115200 -# 2 x 4.5MB app, 6.875MB SPIFFS -;board_build.partitions = large_spiffs_16MB.csv -;board_build.partitions = default_8MB.csv -board_build.partitions = huge_app.csv -;board_build.partitions = default.csv -build_flags = +; T-Display-S3 = 16MB flash: dual app slots (OTA) + SPIFFS +board_upload.flash_size = 16MB +board_build.partitions = default_16MB.csv +build_flags = -D LV_LVGL_H_INCLUDE_SIMPLE -D BOARD_HAS_PSRAM -D ARDUINO_USB_MODE=1 @@ -624,17 +622,26 @@ build_flags = -D NERDMINERV2=1 -D CORE_DEBUG_LEVEL=0 -D CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL=0 + ; mot de passe OTA hors dépôt : exporter NERDMINER_OTA_PWD avant le build + -D OTA_PASSWORD='"${sysenv.NERDMINER_OTA_PWD}"' ;-D DEBUG_MINING=1 ;To enable I2C mining in future: -D ENABLE_I2C_MINING=1 -lib_deps = +lib_deps = https://github.com/takkaO/OpenFontRender#v1.2 bblanchon/ArduinoJson@^6.21.5 https://github.com/tzapu/WiFiManager.git#v2.0.17 mathertel/oneButton@^2.6.1 arduino-libraries/NTPClient@^3.2.1 -lib_ignore = +lib_ignore = HANSOLOminerv2 +; Flash par WiFi : pio run -e NerdminerV2-OTA -t upload --upload-port +; pio run -e NerdminerV2-OTA -t uploadfs --upload-port (config SPIFFS) +[env:NerdminerV2-OTA] +extends = env:NerdminerV2 +upload_protocol = espota +upload_flags = --auth=${sysenv.NERDMINER_OTA_PWD} + ;-------------------------------------------------------------------- [env:Lilygo-T-Embed] diff --git a/src/NerdMinerV2.ino.cpp b/src/NerdMinerV2.ino.cpp index e5833c40..f17212e1 100644 --- a/src/NerdMinerV2.ino.cpp +++ b/src/NerdMinerV2.ino.cpp @@ -21,6 +21,16 @@ #endif #include +#include +#include +#include +#include "version.h" + +#ifndef OTA_PASSWORD +#define OTA_PASSWORD "" +#endif +//Refuse un firmware OTA sans mot de passe (NERDMINER_OTA_PWD absent au build) +static_assert(sizeof(OTA_PASSWORD) > 1, "OTA password vide : exporter NERDMINER_OTA_PWD avant le build"); //#define HW_SHA256_TEST //3 seconds WDT @@ -53,6 +63,10 @@ extern monitor_data mMonitor; unsigned long start = millis(); const char* ntpServer = "pool.ntp.org"; +//Task handles, global so the OTA hooks can suspend everything during a flash +TaskHandle_t minerTask1 = NULL, minerTask2 = NULL; +TaskHandle_t monitorTask = NULL, stratumTask = NULL; + //void runMonitor(void *name); @@ -126,6 +140,13 @@ void setup() /******** INIT WIFI ************/ init_WifiManager(); +#ifdef OTA_ONLY_TEST + //Diagnostic build: bring up WiFi + OTA only, no miner/monitor/stratum tasks. + Serial.println("OTA_ONLY_TEST: no mining tasks started"); + vTaskPrioritySet(NULL, 4); + return; +#endif + /******** CREATE TASK TO PRINT SCREEN *****/ //tft.pushImage(0, 0, MinerWidth, MinerHeight, MinerScreen); // Higher prio monitor task @@ -134,21 +155,21 @@ void setup() static const char monitor_name[] = "(Monitor)"; #if defined(CONFIG_IDF_TARGET_ESP32) // Increased stack for ESP32 classic due to NVS operations - BaseType_t res1 = xTaskCreatePinnedToCore(runMonitor, "Monitor", 9500, (void*)monitor_name, 5, NULL,1); + BaseType_t res1 = xTaskCreatePinnedToCore(runMonitor, "Monitor", 9500, (void*)monitor_name, 5, &monitorTask,1); #else - BaseType_t res1 = xTaskCreatePinnedToCore(runMonitor, "Monitor", 10000, (void*)monitor_name, 5, NULL,1); + BaseType_t res1 = xTaskCreatePinnedToCore(runMonitor, "Monitor", 10000, (void*)monitor_name, 5, &monitorTask,1); #endif /******** CREATE STRATUM TASK *****/ static const char stratum_name[] = "(Stratum)"; #if defined(CONFIG_IDF_TARGET_ESP32) && !defined(ESP32_2432S028R) && !defined(ESP32_2432S028_2USB) // Reduced stack for ESP32 classic to save memory - BaseType_t res2 = xTaskCreatePinnedToCore(runStratumWorker, "Stratum", 12000, (void*)stratum_name, 4, NULL,1); + BaseType_t res2 = xTaskCreatePinnedToCore(runStratumWorker, "Stratum", 12000, (void*)stratum_name, 4, &stratumTask,1); #elif defined(ESP32_2432S028R) || defined(ESP32_2432S028_2USB) // Free a little bit of the heap to the screen - BaseType_t res2 = xTaskCreatePinnedToCore(runStratumWorker, "Stratum", 13500, (void*)stratum_name, 4, NULL,1); + BaseType_t res2 = xTaskCreatePinnedToCore(runStratumWorker, "Stratum", 13500, (void*)stratum_name, 4, &stratumTask,1); #else - BaseType_t res2 = xTaskCreatePinnedToCore(runStratumWorker, "Stratum", 15000, (void*)stratum_name, 4, NULL,1); + BaseType_t res2 = xTaskCreatePinnedToCore(runStratumWorker, "Stratum", 15000, (void*)stratum_name, 4, &stratumTask,1); #endif /******** CREATE MINER TASKS *****/ @@ -158,7 +179,6 @@ void setup() // Start mining tasks //BaseType_t res = xTaskCreate(runWorker, name, 35000, (void*)name, 1, NULL); - TaskHandle_t minerTask1, minerTask2 = NULL; #ifdef HARDWARE_SHA265 #if defined(CONFIG_IDF_TARGET_ESP32) xTaskCreate(minerWorkerHw, "MinerHw-0", 3584, (void*)0, 3, &minerTask1); // Reduced for ESP32 classic @@ -201,6 +221,80 @@ void app_error_fault_handler(void *arg) { esp_restart(); } +//OTA over WiFi: firmware (upload) and SPIFFS config (uploadfs) via espota. +//Init deferred until WiFi is up; handle() below is a non-blocking UDP poll. +static bool s_ota_ready = false; +static volatile bool s_ota_in_progress = false; +static volatile uint32_t s_ota_last_progress_ms = 0; + +//ArduinoOTA.handle() blocks inside loop() for the whole transfer, so a stalled +//transfer (dead sender, wifi drop) can never be detected from loop() itself: +//without this task the miners would stay suspended forever. +static void otaStallWatchdog(void *unused) { + while (true) { + vTaskDelay(5000 / portTICK_PERIOD_MS); + if (s_ota_in_progress && (millis() - s_ota_last_progress_ms) > 90000) { + Serial.println("OTA: stalled >90s, restarting"); + ESP.restart(); + } + } +} + +static void setupOTA() { + uint8_t mac[6]; + WiFi.macAddress(mac); + static char host[24]; + snprintf(host, sizeof(host), "nerdminer-%02x%02x", mac[4], mac[5]); + ArduinoOTA.setHostname(host); + ArduinoOTA.setPassword(OTA_PASSWORD); + ArduinoOTA.onStart([]() { + //Ask the miners to idle at a safe point and release the SHA engine lock, + //then give in-flight jobs a moment to finish. Suspending them mid-hash would + //keep that lock and deadlock Update.end()'s image SHA-256 verification. + ota_active = true; + vTaskDelay(300 / portTICK_PERIOD_MS); + if (minerTask1) esp_task_wdt_delete(minerTask1); + if (minerTask2) esp_task_wdt_delete(minerTask2); + //Suspend the monitor (screen redraw, SPI) and stratum (pool socket): they + //don't touch the SHA engine, so a plain suspend is safe and frees CPU/SPI. + if (monitorTask) vTaskSuspend(monitorTask); + if (stratumTask) vTaskSuspend(stratumTask); + if (ArduinoOTA.getCommand() == U_SPIFFS) + SPIFFS.end(); + s_ota_last_progress_ms = millis(); + s_ota_in_progress = true; + Serial.println("OTA: transfer started, mining suspended"); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + s_ota_last_progress_ms = millis(); + static unsigned int last = 0; + if (progress < last) last = 0; //new transfer + if (progress - last >= 131072 || progress == total) { + last = progress; + Serial.printf("OTA: %u/%u\n", progress, total); + } + }); + ArduinoOTA.onEnd([]() { + s_ota_in_progress = false; //device reboots right after + }); + ArduinoOTA.onError([](ota_error_t error) { + //No ESP.restart() here: ArduinoOTA reports the error detail to the sender + //AFTER this callback (Update.printError). Rearm the stall watchdog so the + //device reboots ~10s later instead, once the error left the socket. + Serial.printf("OTA: error %u, reboot in ~10s\n", error); + s_ota_in_progress = true; + s_ota_last_progress_ms = millis() - 20000; + }); + ArduinoOTA.begin(); + //Deployed version readable from the LAN: avahi-browse -r _arduino._tcp + MDNS.addServiceTxt("arduino", "tcp", "fw_version", CURRENT_VERSION); +#ifdef AUTO_VERSION + MDNS.addServiceTxt("arduino", "tcp", "fw_build", AUTO_VERSION); +#endif + xTaskCreate(otaStallWatchdog, "OTAdog", 2048, NULL, 1, NULL); + Serial.printf("OTA: ready as %s.local\n", host); +} + void loop() { // keep watching the push buttons: #ifdef PIN_BUTTON_1 @@ -216,5 +310,12 @@ void loop() { #endif wifiManagerProcess(); // avoid delays() in loop when non-blocking and other long running code + if (!s_ota_ready && WiFi.status() == WL_CONNECTED) { + setupOTA(); + s_ota_ready = true; + } + if (s_ota_ready) + ArduinoOTA.handle(); + vTaskDelay(50 / portTICK_PERIOD_MS); } diff --git a/src/mining.cpp b/src/mining.cpp index 8fd73163..07cb0e1f 100644 --- a/src/mining.cpp +++ b/src/mining.cpp @@ -61,6 +61,12 @@ extern TSettings Settings; IPAddress serverIP(1, 1, 1, 1); //Temporally save poolIPaddres +//Set by the OTA onStart hook. The hw miner holds the SHA engine lock +//(esp_sha_acquire_hardware) while hashing; suspending it mid-job would keep +//that lock and deadlock Update.end(), which verifies the image SHA-256. +//Instead the miners watch this flag and idle at a safe point, lock released. +volatile bool ota_active = false; + //Global work data static WiFiClient client; static miner_data mMiner; //Global miner data (Create a miner class TODO) @@ -594,6 +600,7 @@ void minerWorkerSw(void * task_id) uint32_t wdt_counter = 0; while (1) { + if (ota_active) { vTaskDelay(100 / portTICK_PERIOD_MS); continue; } //idle during OTA { std::lock_guard lock(s_job_mutex); if (result) @@ -800,6 +807,9 @@ void minerWorkerHw(void * task_id) while (1) { + //Idle during OTA at a safe point: never suspended while holding the SHA + //engine lock, so Update.end()'s image SHA-256 verification can proceed. + if (ota_active) { vTaskDelay(100 / portTICK_PERIOD_MS); continue; } { std::lock_guard lock(s_job_mutex); if (result) diff --git a/src/mining.h b/src/mining.h index 86c010fe..7adc5a78 100644 --- a/src/mining.h +++ b/src/mining.h @@ -19,6 +19,9 @@ void runMonitor(void *name); void runStratumWorker(void *name); + +//Set true by the OTA onStart hook so the miners idle and release the SHA engine. +extern volatile bool ota_active; void runMiner(void *name); void minerWorkerSw(void * task_id);