From d739827db298da980e66888b1aecbcac311ff2af Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Mon, 15 Jun 2026 16:12:07 -0400 Subject: [PATCH] Add touch-down and long-press input detection Implement touch-down event detection to provide immediate visual feedback when touching list items, mirroring button navigation behavior. Add long-press detection (500ms threshold) to distinguish between tap and hold gestures. Apply touch-down selection updates to file browser, home menu, and recent books activities. --- lib/hal/HalGPIO.cpp | 4 ++ lib/hal/HalGPIO.h | 8 ++++ src/MappedInputManager.cpp | 18 +++++++++ src/MappedInputManager.h | 9 +++++ src/activities/home/FileBrowserActivity.cpp | 8 ++++ src/activities/home/HomeActivity.cpp | 8 ++++ src/activities/home/RecentBooksActivity.cpp | 6 +++ .../network/CalibreConnectActivity.cpp | 6 +-- .../network/CrossPointWebServerActivity.cpp | 6 +-- .../network/NetworkModeSelectionActivity.cpp | 6 +++ .../network/WifiSelectionActivity.cpp | 13 ++++++ .../reader/EpubReaderBookmarksActivity.cpp | 8 ++++ .../EpubReaderChapterSelectionActivity.cpp | 6 +++ .../reader/EpubReaderMenuActivity.cpp | 8 ++++ .../settings/FontSelectionActivity.cpp | 6 +++ .../settings/LanguageSelectActivity.cpp | 6 +++ .../settings/OpdsServerListActivity.cpp | 6 +++ src/activities/settings/SettingsActivity.cpp | 9 +++++ src/activities/util/ConfirmationActivity.cpp | 29 ++++++++++++++ src/activities/util/KeyboardEntryActivity.cpp | 17 ++++++-- src/network/CrossPointWebServer.cpp | 40 +++++++++---------- src/network/TaskWatchdog.h | 21 ++++++++++ src/network/WebDAVHandler.cpp | 8 ++-- 23 files changed, 223 insertions(+), 33 deletions(-) create mode 100644 src/network/TaskWatchdog.h diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index 43023d45..6b952c30 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -245,6 +245,10 @@ unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPower bool HalGPIO::wasTouchTap(float& nx, float& ny) const { return inputMgr.wasTouchTap(nx, ny); } +bool HalGPIO::wasTouchDown(float& nx, float& ny) const { return inputMgr.wasTouchPressedAt(nx, ny); } + +unsigned long HalGPIO::lastTouchHeldMs() const { return inputMgr.lastTouchHeldMs(); } + bool HalGPIO::hasTouch() const { return inputMgr.hasTouch(); } void HalGPIO::startDeepSleep() { diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h index 94d86177..fb446fbe 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -77,6 +77,14 @@ class HalGPIO { // primitive; see MappedInputManager for the top-left = Back mapping.) bool wasTouchTap(float& nx, float& ny) const; + // Press-edge of a touch: true on touch-down with the down position normalized + // 0..1 (panel native). For showing the pressed/selected element before release. + bool wasTouchDown(float& nx, float& ny) const; + + // Duration (ms) of the last touch contact, latched on release. Valid on the + // release frame (alongside wasTouchTap). For tap-vs-long-press decisions. + unsigned long lastTouchHeldMs() const; + // True if a touch controller is present/active (runtime gate; false on the C3). bool hasTouch() const; diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index 9fbb1bb7..abde5d48 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -82,6 +82,24 @@ bool MappedInputManager::wasItemTapped(int& id) const { return TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Item, id); } +bool MappedInputManager::wasItemTouchedDown(int& id) const { + float nx = 0.0f, ny = 0.0f; + if (!gpio.wasTouchDown(nx, ny)) return false; + int lx = 0, ly = 0; + renderer.tapToLogical(nx, ny, lx, ly); + return TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Item, id); +} + +bool MappedInputManager::wasItemLongPressed(int& id) const { + static constexpr unsigned long TOUCH_LONG_PRESS_MS = 500; + float nx = 0.0f, ny = 0.0f; + if (!gpio.wasTouchTap(nx, ny)) return false; // release frame + if (gpio.lastTouchHeldMs() < TOUCH_LONG_PRESS_MS) return false; + int lx = 0, ly = 0; + renderer.tapToLogical(nx, ny, lx, ly); + return TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Item, id); +} + bool MappedInputManager::wasTabTapped(int& id) const { float nx = 0.0f, ny = 0.0f; if (!gpio.wasTouchTap(nx, ny)) return false; diff --git a/src/MappedInputManager.h b/src/MappedInputManager.h index 1655a46f..1c5be260 100644 --- a/src/MappedInputManager.h +++ b/src/MappedInputManager.h @@ -31,6 +31,15 @@ class MappedInputManager { // element's id. Activities treat the id as "select + activate". False on // non-touch devices or when the tap missed every target. bool wasItemTapped(int& id) const; + // Press-edge analogue of wasItemTapped: fires on touch-DOWN over an item, so the + // activity can move its selection to that item (showing the selected state) before + // release. Release still activates via wasItemTapped. Mirrors button nav (move + // selection, then confirm). + bool wasItemTouchedDown(int& id) const; + // Long-press variant of wasItemTapped: true on release of a touch over an item + // held past the long-press threshold (a subset of wasItemTapped's releases, so + // check this first). Lets a screen distinguish tap vs press-and-hold on touch. + bool wasItemLongPressed(int& id) const; // Like wasItemTapped, but for tab-bar tabs (id = tab index) and cover/card // targets (id = item index). Distinct kinds so screens with both a list and a // tab bar / cover (Home, Settings) don't confuse them. diff --git a/src/activities/home/FileBrowserActivity.cpp b/src/activities/home/FileBrowserActivity.cpp index 7ee4ef17..9c6d0896 100644 --- a/src/activities/home/FileBrowserActivity.cpp +++ b/src/activities/home/FileBrowserActivity.cpp @@ -206,6 +206,14 @@ void FileBrowserActivity::loop() { const int pathReserved = renderer.getLineHeight(SMALL_FONT_ID) + UITheme::getInstance().getMetrics().verticalSpacing; const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false, pathReserved); + // Touch-down moves the selector to the pressed entry (shows selected state); release + // opens it below. + int downId = -1; + if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast(files.size())) { + selectorIndex = downId; + requestUpdate(); + } + // A tap opens the tapped entry (held-time is 0 on a tap, so it takes the short-press // open path below, never the long-press delete). int tappedId = -1; diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 09ab4ad4..bcb6b613 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -183,6 +183,14 @@ void HomeActivity::loop() { // menu-local ids (it is drawn with selectorIndex offset by recentBooks.size()), // so map the tapped id back into the global selector space. (The recent-book // cover is a separate, single-item draw path — tappable in a later phase.) + // Touch-down moves the selector to the pressed menu button (shows selected state), + // like Up/Down; release opens it below. + int downId = -1; + if (mappedInput.wasItemTouchedDown(downId)) { + selectorIndex = static_cast(recentBooks.size()) + downId; + requestUpdate(); + } + int tappedId = -1; const bool tapped = mappedInput.wasItemTapped(tappedId); if (tapped) { diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 97a0a974..59d3a9fc 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -63,6 +63,12 @@ void RecentBooksActivity::loop() { return; } + int downId = -1; + if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast(recentBooks.size())) { + selectorIndex = downId; + requestUpdate(); + } + int tappedId = -1; const bool tapped = mappedInput.wasItemTapped(tappedId); if (tapped && tappedId >= 0 && tappedId < static_cast(recentBooks.size())) selectorIndex = tappedId; diff --git a/src/activities/network/CalibreConnectActivity.cpp b/src/activities/network/CalibreConnectActivity.cpp index ff89cf27..4c431805 100644 --- a/src/activities/network/CalibreConnectActivity.cpp +++ b/src/activities/network/CalibreConnectActivity.cpp @@ -4,13 +4,13 @@ #include #include #include -#include #include "MappedInputManager.h" #include "SilentRestart.h" #include "WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "network/TaskWatchdog.h" namespace { constexpr const char* HOSTNAME = "crosspoint"; @@ -110,12 +110,12 @@ void CalibreConnectActivity::loop() { LOG_DBG("CAL", "WARNING: %lu ms gap since last handleClient", timeSinceLastHandleClient); } - esp_task_wdt_reset(); + feedTaskWatchdog(); constexpr int MAX_ITERATIONS = 80; for (int i = 0; i < MAX_ITERATIONS && webServer->isRunning(); i++) { webServer->handleClient(); if ((i & 0x07) == 0x07) { - esp_task_wdt_reset(); + feedTaskWatchdog(); } if ((i & 0x0F) == 0x0F) { yield(); diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 29737a5a..760345f1 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include @@ -16,6 +15,7 @@ #include "activities/network/CalibreConnectActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "network/TaskWatchdog.h" #include "util/QrUtils.h" namespace { @@ -328,7 +328,7 @@ void CrossPointWebServerActivity::loop() { } // Reset watchdog BEFORE processing - HTTP header parsing can be slow - esp_task_wdt_reset(); + feedTaskWatchdog(); // Process HTTP requests in tight loop for maximum throughput // More iterations = more data processed per main loop cycle @@ -337,7 +337,7 @@ void CrossPointWebServerActivity::loop() { webServer->handleClient(); // Reset watchdog every 32 iterations if ((i & 0x1F) == 0x1F) { - esp_task_wdt_reset(); + feedTaskWatchdog(); } // Yield and check for exit button every 64 iterations if ((i & 0x3F) == 0x3F) { diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index c2f80414..bf70ad06 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -30,6 +30,12 @@ void NetworkModeSelectionActivity::loop() { return; } + int downId = -1; + if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast(MENU_ITEM_COUNT)) { + selectedIndex = downId; + requestUpdate(); + } + // Handle confirm button (or a tap) - select current option int tappedId = -1; const bool tapped = mappedInput.wasItemTapped(tappedId); diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 9d473699..2393da4b 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -409,6 +409,19 @@ void WifiSelectionActivity::loop() { // Handle network list state if (state == WifiSelectionState::NETWORK_LIST) { + // Touch: down-select highlights the pressed network, tap selects it (like Confirm). + int downId = -1; + if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast(networks.size())) { + selectedNetworkIndex = downId; + requestUpdate(); + } + int tappedId = -1; + if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < static_cast(networks.size())) { + selectedNetworkIndex = tappedId; + selectNetwork(selectedNetworkIndex); + return; + } + // Check for Back button to exit (cancel) if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { onComplete(false); diff --git a/src/activities/reader/EpubReaderBookmarksActivity.cpp b/src/activities/reader/EpubReaderBookmarksActivity.cpp index 79345c3a..0a25c243 100644 --- a/src/activities/reader/EpubReaderBookmarksActivity.cpp +++ b/src/activities/reader/EpubReaderBookmarksActivity.cpp @@ -103,6 +103,14 @@ void EpubReaderBookmarksActivity::loop() { } } + if (confirmingDelete < DELETE_MODE_DISPLAY) { + int downId = -1; + if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast(bookmarks.size())) { + selectorIndex = downId; + requestUpdate(); + } + } + int tappedId = -1; const bool tapped = (confirmingDelete < DELETE_MODE_DISPLAY) && mappedInput.wasItemTapped(tappedId); if (tapped && tappedId >= 0 && tappedId < static_cast(bookmarks.size())) selectorIndex = tappedId; diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 8dddd247..853b47d8 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -31,6 +31,12 @@ void EpubReaderChapterSelectionActivity::loop() { const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); const int totalItems = getTotalItems(); + int downId = -1; + if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < totalItems) { + selectorIndex = downId; + requestUpdate(); + } + int tappedId = -1; const bool tapped = mappedInput.wasItemTapped(tappedId); if (tapped && tappedId >= 0 && tappedId < totalItems) selectorIndex = tappedId; diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index a36a1ba5..08c7d18c 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -57,6 +57,14 @@ void EpubReaderMenuActivity::loop() { requestUpdate(); }); + // Touch-down moves the selection to the pressed item (shows selected state), like + // moving with Up/Down; release activates it below. + int downId = -1; + if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast(menuItems.size())) { + selectedIndex = downId; + requestUpdate(); + } + // A tap selects the item and activates it in one gesture (falls into Confirm below). int tappedId = -1; const bool tapped = mappedInput.wasItemTapped(tappedId); diff --git a/src/activities/settings/FontSelectionActivity.cpp b/src/activities/settings/FontSelectionActivity.cpp index a75b5a97..f5b0b0c7 100644 --- a/src/activities/settings/FontSelectionActivity.cpp +++ b/src/activities/settings/FontSelectionActivity.cpp @@ -54,6 +54,12 @@ void FontSelectionActivity::loop() { return; } + int downId = -1; + if (mappedInput.wasItemTouchedDown(downId) && downId >= 0) { + selectedIndex_ = downId; + requestUpdate(); + } + int tappedId = -1; if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0) { selectedIndex_ = tappedId; diff --git a/src/activities/settings/LanguageSelectActivity.cpp b/src/activities/settings/LanguageSelectActivity.cpp index b6e63870..ff0be608 100644 --- a/src/activities/settings/LanguageSelectActivity.cpp +++ b/src/activities/settings/LanguageSelectActivity.cpp @@ -32,6 +32,12 @@ void LanguageSelectActivity::loop() { return; } + int downId = -1; + if (mappedInput.wasItemTouchedDown(downId) && downId >= 0) { + selectedIndex = downId; + requestUpdate(); + } + int tappedId = -1; if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0) { selectedIndex = tappedId; diff --git a/src/activities/settings/OpdsServerListActivity.cpp b/src/activities/settings/OpdsServerListActivity.cpp index c6381850..4a0c367f 100644 --- a/src/activities/settings/OpdsServerListActivity.cpp +++ b/src/activities/settings/OpdsServerListActivity.cpp @@ -41,6 +41,12 @@ void OpdsServerListActivity::loop() { return; } + int downId = -1; + if (mappedInput.wasItemTouchedDown(downId) && downId >= 0) { + selectedIndex = downId; + requestUpdate(); + } + int tappedId = -1; if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0) { selectedIndex = tappedId; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 4295b143..42a429ac 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -118,6 +118,14 @@ void SettingsActivity::loop() { // A tap on a settings row selects + activates it in one gesture. The list is drawn // with selectedIndex = selectedSettingIndex - 1 (row 0 is the category tab), so map // the tapped 0-based row back by +1. (Category tab bar is tappable in a later phase.) + // Touch-down moves the selection to the pressed row (shows selected state); release + // toggles/activates it below. (Row 0 is the tab bar, so settings list id 0 -> index 1.) + int downId = -1; + if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < settingsCount) { + selectedSettingIndex = downId + 1; + requestUpdate(); + } + int tappedId = -1; if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < settingsCount) { selectedSettingIndex = tappedId + 1; @@ -133,6 +141,7 @@ void SettingsActivity::loop() { selectedCategoryIndex = tabId; selectedSettingIndex = 0; hasChangedCategory = true; + requestUpdate(); } // Handle actions with early return diff --git a/src/activities/util/ConfirmationActivity.cpp b/src/activities/util/ConfirmationActivity.cpp index f37cfde8..49fce078 100644 --- a/src/activities/util/ConfirmationActivity.cpp +++ b/src/activities/util/ConfirmationActivity.cpp @@ -3,6 +3,7 @@ #include #include "HalDisplay.h" +#include "components/TouchRegistry.h" #include "components/UITheme.h" ConfirmationActivity::ConfirmationActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, @@ -46,8 +47,26 @@ void ConfirmationActivity::render(RenderLock&& lock) { // Draw Body if (!safeBody.empty()) { renderer.drawCenteredText(fontId, currentY, safeBody.c_str(), true, EpdFontFamily::REGULAR); + currentY += lineHeight; } + // On-screen Cancel / Confirm buttons (also tappable). The footer hints below map + // the same actions to Left/Right for button devices, but on touch-only devices + // the hints are hidden, so these are the only affordance. + const int btnH = lineHeight + 20; + const int totalW = renderer.getScreenWidth() - margin * 2; + const int btnW = (totalW - spacing) / 2; + const int btnY = currentY + spacing * 3; + const Rect cancelRect{margin, btnY, btnW, btnH}; + const Rect confirmRect{margin + btnW + spacing, btnY, btnW, btnH}; + renderer.drawRect(cancelRect.x, cancelRect.y, cancelRect.width, cancelRect.height); + renderer.drawRect(confirmRect.x, confirmRect.y, confirmRect.width, confirmRect.height); + const int btnTextY = btnY + (btnH - lineHeight) / 2; + UITheme::drawCenteredText(renderer, cancelRect, fontId, btnTextY, I18N.get(StrId::STR_CANCEL)); + UITheme::drawCenteredText(renderer, confirmRect, fontId, btnTextY, I18N.get(StrId::STR_CONFIRM)); + TouchRegistry::getInstance().add(cancelRect, 0, TouchRegistry::Item); + TouchRegistry::getInstance().add(confirmRect, 1, TouchRegistry::Item); + // Draw UI Elements const auto labels = mappedInput.mapLabels("", "", I18N.get(StrId::STR_CANCEL), I18N.get(StrId::STR_CONFIRM)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); @@ -56,6 +75,16 @@ void ConfirmationActivity::render(RenderLock&& lock) { } void ConfirmationActivity::loop() { + // Tap the on-screen buttons: id 1 = Confirm, id 0 = Cancel. + int tappedId = -1; + if (mappedInput.wasItemTapped(tappedId)) { + ActivityResult res; + res.isCancelled = (tappedId != 1); + setResult(std::move(res)); + finish(); + return; + } + if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { ActivityResult res; res.isCancelled = false; diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index c5ebac16..53ebe92d 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -337,13 +337,24 @@ void KeyboardEntryActivity::loop() { } } - // A tap selects the key and presses it in one gesture. Encoded id = row*100+col - // (bottom function row = getContentRowCount()). Skipped in cursor mode, where a - // tap on a key would be ambiguous with cursor editing. + // A tap selects the key and presses it. Encoded id = row*100+col (bottom function + // row = getContentRowCount()). A touch-and-hold inserts the alternate character + // (numbers/symbols on a letter), mirroring the button long-press — wasItemLongPressed + // is checked first since it's a subset of wasItemTapped's releases. Skipped in + // cursor mode, where a tap on a key would be ambiguous with cursor editing. int tappedKey = -1; if (!cursorMode && mappedInput.wasItemTapped(tappedKey)) { selectedRow = tappedKey / 100; selectedCol = tappedKey % 100; + int longKey = -1; + if (mappedInput.wasItemLongPressed(longKey)) { + const char alt = getAlternativeChar(); + if (alt != '\0') { + insertChar(alt); + requestUpdate(); + return; + } + } if (handleKeyPress()) { requestUpdate(); } diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index dc54dd84..4a640719 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include @@ -15,6 +14,7 @@ #include "OpdsServerStore.h" #include "SdCardFontSystem.h" #include "SettingsList.h" +#include "TaskWatchdog.h" #include "WebDAVHandler.h" #include "WifiCredentialStore.h" #include "html/FilesPageHtml.generated.h" @@ -424,8 +424,8 @@ void CrossPointWebServer::scanFiles(const char* path, const std::function(result); size_t totalWritten = 0; while (totalWritten < bytesRead) { - esp_task_wdt_reset(); + feedTaskWatchdog(); size_t wrote = client.write(buffer + totalWritten, bytesRead - totalWritten); if (wrote == 0) { downloadOk = false; @@ -576,12 +576,12 @@ static size_t writeCount = 0; static bool flushUploadBuffer(CrossPointWebServer::UploadState& state) { if (state.bufferPos > 0 && state.file) { - esp_task_wdt_reset(); // Reset watchdog before potentially slow SD write + feedTaskWatchdog(); // Reset watchdog before potentially slow SD write const unsigned long writeStart = millis(); const size_t written = state.file.write(state.buffer.data(), state.bufferPos); totalWriteTime += millis() - writeStart; writeCount++; - esp_task_wdt_reset(); // Reset watchdog after SD write + feedTaskWatchdog(); // Reset watchdog after SD write if (written != state.bufferPos) { LOG_DBG("WEB", "[UPLOAD] Buffer flush failed: expected %d, wrote %d", state.bufferPos, written); @@ -597,7 +597,7 @@ void CrossPointWebServer::handleUpload(UploadState& state) const { static size_t lastLoggedSize = 0; // Reset watchdog at start of every upload callback - HTTP parsing can be slow - esp_task_wdt_reset(); + feedTaskWatchdog(); // Safety check: ensure server is still valid if (!running || !server) { @@ -609,7 +609,7 @@ void CrossPointWebServer::handleUpload(UploadState& state) const { if (upload.status == UPLOAD_FILE_START) { // Reset watchdog - this is the critical 1% crash point - esp_task_wdt_reset(); + feedTaskWatchdog(); state.fileName = upload.filename; state.size = 0; @@ -647,21 +647,21 @@ void CrossPointWebServer::handleUpload(UploadState& state) const { filePath += state.fileName; // Check if file already exists - SD operations can be slow - esp_task_wdt_reset(); + feedTaskWatchdog(); if (Storage.exists(filePath.c_str())) { LOG_DBG("WEB", "[UPLOAD] Overwriting existing file: %s", filePath.c_str()); - esp_task_wdt_reset(); + feedTaskWatchdog(); Storage.remove(filePath.c_str()); } // Open file for writing - this can be slow due to FAT cluster allocation - esp_task_wdt_reset(); + feedTaskWatchdog(); if (!Storage.openFileForWrite("WEB", filePath, state.file)) { state.error = "Failed to create file on SD card"; LOG_DBG("WEB", "[UPLOAD] FAILED to create file: %s", filePath.c_str()); return; } - esp_task_wdt_reset(); + feedTaskWatchdog(); LOG_DBG("WEB", "[UPLOAD] File created successfully: %s", filePath.c_str()); } else if (upload.status == UPLOAD_FILE_WRITE) { @@ -1603,20 +1603,20 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* filePath.c_str()); // Check if file exists and remove it - esp_task_wdt_reset(); + feedTaskWatchdog(); if (Storage.exists(filePath.c_str())) { Storage.remove(filePath.c_str()); } // Open file for writing - esp_task_wdt_reset(); + feedTaskWatchdog(); if (!Storage.openFileForWrite("WS", filePath, wsUploadFile)) { wsServer->sendTXT(num, "ERROR:Failed to create file"); wsUploadInProgress = false; wsUploadClientNum = 255; return; } - esp_task_wdt_reset(); + feedTaskWatchdog(); // Zero-byte upload: complete immediately without waiting for BIN frames if (wsUploadSize == 0) { @@ -1655,9 +1655,9 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* wsServer->sendTXT(num, "ERROR:Upload overflow"); return; } - esp_task_wdt_reset(); + feedTaskWatchdog(); size_t written = wsUploadFile.write(payload, length); - esp_task_wdt_reset(); + feedTaskWatchdog(); if (written != length) { abortWsUpload("WS"); @@ -1761,7 +1761,7 @@ void CrossPointWebServer::handleFontUploadData() { switch (upload.status) { case UPLOAD_FILE_START: { - esp_task_wdt_reset(); + feedTaskWatchdog(); String family = server->arg("family"); fontUpload.file = HalFile(); fontUpload.familyName.clear(); @@ -1812,7 +1812,7 @@ void CrossPointWebServer::handleFontUploadData() { case UPLOAD_FILE_WRITE: { if (!fontUpload.valid) break; - esp_task_wdt_reset(); + feedTaskWatchdog(); // Validate magic bytes on first chunk only if (!fontUpload.magicChecked && upload.currentSize >= 8) { @@ -1839,7 +1839,7 @@ void CrossPointWebServer::handleFontUploadData() { fontUpload.file.write(fontUpload.buffer.data(), fontUpload.bufferPos); fontUpload.bytesWritten += fontUpload.bufferPos; fontUpload.bufferPos = 0; - esp_task_wdt_reset(); + feedTaskWatchdog(); } } break; diff --git a/src/network/TaskWatchdog.h b/src/network/TaskWatchdog.h new file mode 100644 index 00000000..0494a7e6 --- /dev/null +++ b/src/network/TaskWatchdog.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +// Feed the Task Watchdog Timer only when the calling task is actually +// subscribed to it. esp_task_wdt_reset() logs +// "task_wdt: esp_task_wdt_reset(...): task not found" +// on every call when the current task was never registered via +// esp_task_wdt_add(). Whether the Arduino loopTask is auto-subscribed depends +// on the chip target's framework sdkconfig: the ESP32-C3 (X4) build subscribes +// it, the classic-ESP32 (m5paper) build does not, so the unguarded resets in +// the web server / WiFi paths spammed the log there. +// +// esp_task_wdt_status(nullptr) returns ESP_OK only when the current task is +// subscribed, so this guard makes the reset a no-op on builds where the loop +// task is not watchdog-monitored, while preserving the reset where it is. +static inline void feedTaskWatchdog() { + if (esp_task_wdt_status(nullptr) == ESP_OK) { + esp_task_wdt_reset(); + } +} diff --git a/src/network/WebDAVHandler.cpp b/src/network/WebDAVHandler.cpp index 64fa3d09..b7f63688 100644 --- a/src/network/WebDAVHandler.cpp +++ b/src/network/WebDAVHandler.cpp @@ -3,8 +3,8 @@ #include #include #include -#include +#include "TaskWatchdog.h" #include "util/BookCacheUtils.h" namespace { @@ -84,7 +84,7 @@ void WebDAVHandler::raw(WebServer& server, const String& uri, HTTPRaw& raw) { } else if (raw.status == RAW_WRITE) { if (_putFile && _putOk) { - esp_task_wdt_reset(); + feedTaskWatchdog(); size_t written = _putFile.write(raw.buf, raw.currentSize); if (written != raw.currentSize) { _putOk = false; @@ -252,7 +252,7 @@ void WebDAVHandler::handlePropfind(WebServer& s) { file.close(); yield(); - esp_task_wdt_reset(); + feedTaskWatchdog(); file = root.openNextFile(); } } @@ -628,7 +628,7 @@ void WebDAVHandler::handleCopy(WebServer& s) { uint8_t buf[4096]; bool copyOk = true; while (srcFile.available()) { - esp_task_wdt_reset(); + feedTaskWatchdog(); int bytesRead = srcFile.read(buf, sizeof(buf)); if (bytesRead <= 0) break; size_t written = dstFile.write(buf, bytesRead);