From 5679cda38e250eadec91a3db2948a005132bcb7b Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:43:06 +1100 Subject: [PATCH] tdpro touch paches - dialpad touch system conflict fix and longpress changed to 750ms --- examples/companion_radio/main.cpp | 31 +- .../companion_radio/ui-new/Settingsscreen.h | 527 +++++++++++++++++- variants/lilygo_tdeck_pro/TDeckBoard.h | 2 +- 3 files changed, 548 insertions(+), 12 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index b3c3753..02e5e4e 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -489,7 +489,7 @@ static int16_t touchLastX = 0; static int16_t touchLastY = 0; static unsigned long lastTouchSeenMs = 0; - #define TOUCH_LONG_PRESS_MS 500 + #define TOUCH_LONG_PRESS_MS 750 #if defined(LilyGo_T5S3_EPaper_Pro) #define TOUCH_SWIPE_THRESHOLD 60 // T5S3: 960×540 — 60px ≈ 6% of width #else @@ -931,6 +931,12 @@ static void lastHeardToggleContact() { return KEY_ENTER; // Editing mode or header/footer tap } + // SMS screen: dedicated dialer/touch handler runs separately (HAS_4G_MODEM block) + // Return 0 so the general handler doesn't inject spurious keys + #ifdef HAS_4G_MODEM + if (ui_task.isOnSMSScreen()) return 0; + #endif + // All other screens: tap = select return KEY_ENTER; } @@ -939,6 +945,11 @@ static void lastHeardToggleContact() { static char mapTouchSwipe(int16_t dx, int16_t dy) { bool horizontal = abs(dx) > abs(dy); + // SMS screen: dedicated touch handler covers all interaction + #ifdef HAS_4G_MODEM + if (ui_task.isOnSMSScreen()) return 0; + #endif + // Reader (reading mode): swipe left/right for page turn if (ui_task.isOnTextReader()) { TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen(); @@ -1002,6 +1013,11 @@ static void lastHeardToggleContact() { // Map a long press to a key static char mapTouchLongPress(int16_t x, int16_t y) { + // SMS screen: dedicated touch handler covers all interaction + #ifdef HAS_4G_MODEM + if (ui_task.isOnSMSScreen()) return 0; + #endif + // Home screen: long press = activate current page action // (BLE toggle, send advert, hibernate, GPS toggle, etc.) if (ui_task.isOnHomeScreen()) { @@ -1819,7 +1835,7 @@ void loop() { the_mesh.loop(); #ifdef MECK_OTA_UPDATE } else { - // OTA active — poll the web server from the main loop for fast response. + // OTA/File Manager active — poll the web server from the main loop for fast response. // The render cycle on T5S3 (960×540 FastEPD) can block for 500ms+ during // e-ink refresh, causing the browser to timeout before handleClient() runs. // Polling here gives us ~1-5ms response time instead. @@ -2178,7 +2194,7 @@ void loop() { // Gestures: // Tap = finger down + up with minimal movement → select/open // Swipe = finger drag > threshold → scroll/page turn - // Long press = finger held > 500ms without moving → edit/enter + // Long press = finger held > 750ms without moving → edit/enter // After processing an event, cooldown waits for finger lift before next event. // Touch is disabled while lock screen is active. // When virtual keyboard is active (T5S3), taps route to keyboard. @@ -2190,6 +2206,15 @@ void loop() { #if defined(LilyGo_T5S3_EPaper_Pro) touchBlocked = touchBlocked || ui_task.isVKBActive(); #endif +#ifdef HAS_4G_MODEM + // SMS dialer has its own dedicated touch handler — don't consume touch data here + if (smsMode) { + SMSScreen* smsScr = (SMSScreen*)ui_task.getSMSScreen(); + if (smsScr && smsScr->getSubView() == SMSScreen::PHONE_DIALER) { + touchBlocked = true; + } + } +#endif if (!touchBlocked) { diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index 7acc3d6..d5a808b 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -143,7 +143,9 @@ enum SettingsRowType : uint8_t { ROW_ADD_CHANNEL, // "+ Add Hashtag Channel" ROW_INFO_HEADER, // "--- Info ---" separator #ifdef MECK_OTA_UPDATE + ROW_OTA_TOOLS_SUBMENU, // Folder row → enters OTA Tools sub-screen ROW_FW_UPDATE, // "Firmware Update" — WiFi upload + flash + ROW_SD_FILE_MGR, // "SD File Manager" — WiFi file browser #endif ROW_PUB_KEY, // Public key display ROW_FIRMWARE, // Firmware version @@ -168,6 +170,7 @@ enum EditMode : uint8_t { #endif #ifdef MECK_OTA_UPDATE EDIT_OTA, // OTA firmware update flow (multi-phase overlay) + EDIT_FILEMGR, // SD file manager flow (WiFi file browser) #endif }; @@ -178,6 +181,9 @@ enum SubScreen : uint8_t { SUB_NONE, // Top-level settings list SUB_CONTACTS, // Contacts settings sub-screen SUB_CHANNELS, // Channels management sub-screen + #ifdef MECK_OTA_UPDATE + SUB_OTA_TOOLS, // OTA Tools sub-screen (FW update + File Manager) + #endif }; #ifdef MECK_OTA_UPDATE @@ -192,6 +198,13 @@ enum OtaPhase : uint8_t { OTA_PHASE_DONE, // Success, rebooting OTA_PHASE_ERROR, // Error with message }; + +// File manager phases +enum FmPhase : uint8_t { + FM_PHASE_CONFIRM, // "Start SD file manager? Enter:Yes Q:No" + FM_PHASE_WAITING, // AP up, file browser active + FM_PHASE_ERROR, // Error with message +}; #endif // Max rows in the settings list (increased for contact sub-toggles + WiFi) @@ -281,6 +294,9 @@ private: bool _otaUploadOk; char _otaApName[24]; const char* _otaError; + // File manager state + FmPhase _fmPhase; + const char* _fmError; #endif // --------------------------------------------------------------------------- @@ -362,6 +378,12 @@ private: } } addRow(ROW_ADD_CHANNEL); + #ifdef MECK_OTA_UPDATE + } else if (_subScreen == SUB_OTA_TOOLS) { + // --- OTA Tools sub-screen --- + addRow(ROW_FW_UPDATE); + addRow(ROW_SD_FILE_MGR); + #endif } else { // --- Top-level settings list --- addRow(ROW_NAME); @@ -398,7 +420,7 @@ private: // Info section (stays at top level) addRow(ROW_INFO_HEADER); #ifdef MECK_OTA_UPDATE - addRow(ROW_FW_UPDATE); + addRow(ROW_OTA_TOOLS_SUBMENU); #endif addRow(ROW_PUB_KEY); addRow(ROW_FIRMWARE); @@ -556,6 +578,8 @@ public: _otaBytesReceived = 0; _otaUploadOk = false; _otaError = nullptr; + _fmPhase = FM_PHASE_CONFIRM; + _fmError = nullptr; #endif } @@ -1003,10 +1027,14 @@ public: return true; } - // Called from render loop AND main loop to poll the web server + // Called from render loop AND main loop to poll the web server. + // Handles both OTA firmware upload and SD file manager modes. void pollOTAServer() { - if (_otaServer && (_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) { - _otaServer->handleClient(); + if (_otaServer) { + if ((_editMode == EDIT_OTA && (_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) || + (_editMode == EDIT_FILEMGR && _fmPhase == FM_PHASE_WAITING)) { + _otaServer->handleClient(); + } } } @@ -1073,6 +1101,341 @@ public: ESP.restart(); } + // --------------------------------------------------------------------------- + // SD File Manager — WiFi file browser, upload, download, delete + // --------------------------------------------------------------------------- + + void startFileMgr() { + _editMode = EDIT_FILEMGR; + _fmPhase = FM_PHASE_CONFIRM; + _fmError = nullptr; + } + + void startFileMgrServer() { + // Build AP name with last 4 of MAC for uniqueness + uint8_t mac[6]; + WiFi.macAddress(mac); + snprintf(_otaApName, sizeof(_otaApName), "Meck-Files-%02X%02X", mac[4], mac[5]); + + // Pause LoRa radio — SD and LoRa share the same SPI bus on both + // platforms. Incoming packets during SD writes cause bus contention. + extern void otaPauseRadio(); + otaPauseRadio(); + + // Clean WiFi init from any state + WiFi.disconnect(true); + WiFi.mode(WIFI_OFF); + delay(200); + WiFi.mode(WIFI_AP); + WiFi.softAP(_otaApName); + delay(500); + Serial.printf("FM: AP '%s' started, IP: %s\n", + _otaApName, WiFi.softAPIP().toString().c_str()); + + // Start web server + if (_otaServer) { _otaServer->stop(); delete _otaServer; } + _otaServer = new WebServer(80); + + // --- Serve the file manager SPA --- + _otaServer->on("/", HTTP_GET, [this]() { + _otaServer->send(200, "text/html", fileMgrPageHTML()); + }); + + // --- Directory listing: GET /api/ls?path=/ --- + _otaServer->on("/api/ls", HTTP_GET, [this]() { + String path = _otaServer->arg("path"); + if (path.isEmpty()) path = "/"; + File dir = SD.open(path); + if (!dir || !dir.isDirectory()) { + dir.close(); digitalWrite(SDCARD_CS, HIGH); + _otaServer->send(404, "application/json", "{\"error\":\"Not found\"}"); + return; + } + String json = "["; + bool first = true; + File entry; + while ((entry = dir.openNextFile())) { + if (!first) json += ","; + first = false; + // Extract basename — entry.name() may return full path on some ESP32 cores + const char* fullName = entry.name(); + const char* baseName = strrchr(fullName, '/'); + baseName = baseName ? baseName + 1 : fullName; + json += "{\"n\":\""; + json += baseName; + json += "\",\"s\":"; + json += String((unsigned long)entry.size()); + json += ",\"d\":"; + json += entry.isDirectory() ? "1" : "0"; + json += "}"; + entry.close(); + } + dir.close(); + digitalWrite(SDCARD_CS, HIGH); + json += "]"; + _otaServer->send(200, "application/json", json); + }); + + // --- File download: GET /api/dl?path=/file.txt --- + _otaServer->on("/api/dl", HTTP_GET, [this]() { + String path = _otaServer->arg("path"); + File f = SD.open(path, FILE_READ); + if (!f || f.isDirectory()) { + if (f) f.close(); + digitalWrite(SDCARD_CS, HIGH); + _otaServer->send(404, "text/plain", "Not found"); + return; + } + // Extract filename for Content-Disposition + String name = path; + int lastSlash = name.lastIndexOf('/'); + if (lastSlash >= 0) name = name.substring(lastSlash + 1); + _otaServer->sendHeader("Content-Disposition", + "attachment; filename=\"" + name + "\""); + + // Stream file in chunks — no full-file RAM allocation + size_t fileSize = f.size(); + _otaServer->setContentLength(fileSize); + _otaServer->send(200, "application/octet-stream", ""); + uint8_t* buf = (uint8_t*)ps_malloc(4096); + if (!buf) buf = (uint8_t*)malloc(4096); + if (buf) { + while (f.available()) { + int n = f.read(buf, 4096); + if (n > 0) _otaServer->sendContent((const char*)buf, n); + } + free(buf); + } + f.close(); + digitalWrite(SDCARD_CS, HIGH); + }); + + // --- File upload: POST /api/upload?dir=/ --- + _otaServer->on("/api/upload", HTTP_POST, + [this]() { + _otaServer->send(200, "application/json", "{\"ok\":true}"); + }, + [this]() { + HTTPUpload& upload = _otaServer->upload(); + static File fmUploadFile; + + if (upload.status == UPLOAD_FILE_START) { + String dir = _otaServer->arg("dir"); + if (dir.isEmpty()) dir = "/"; + if (!dir.endsWith("/")) dir += "/"; + String fullPath = dir + upload.filename; + Serial.printf("FM: Upload start: %s\n", fullPath.c_str()); + fmUploadFile = SD.open(fullPath, FILE_WRITE); + if (!fmUploadFile) { + Serial.println("FM: Failed to open file for write"); + } + + } else if (upload.status == UPLOAD_FILE_WRITE) { + if (fmUploadFile) { + fmUploadFile.write(upload.buf, upload.currentSize); + } + + } else if (upload.status == UPLOAD_FILE_END) { + if (fmUploadFile) { + fmUploadFile.close(); + digitalWrite(SDCARD_CS, HIGH); + Serial.printf("FM: Upload done: %s (%d bytes)\n", + upload.filename.c_str(), upload.totalSize); + } + + } else if (upload.status == UPLOAD_FILE_ABORTED) { + if (fmUploadFile) fmUploadFile.close(); + digitalWrite(SDCARD_CS, HIGH); + Serial.println("FM: Upload aborted"); + } + } + ); + + // --- Create directory: GET /api/mkdir?path=/newfolder --- + _otaServer->on("/api/mkdir", HTTP_GET, [this]() { + String path = _otaServer->arg("path"); + if (path.isEmpty()) { + _otaServer->send(400, "application/json", "{\"error\":\"No path\"}"); + return; + } + bool ok = SD.mkdir(path); + digitalWrite(SDCARD_CS, HIGH); + _otaServer->send(ok ? 200 : 500, "application/json", + ok ? "{\"ok\":true}" : "{\"error\":\"mkdir failed\"}"); + }); + + // --- Delete file/folder: GET /api/rm?path=/file.txt --- + _otaServer->on("/api/rm", HTTP_GET, [this]() { + String path = _otaServer->arg("path"); + if (path.isEmpty() || path == "/") { + _otaServer->send(400, "application/json", "{\"error\":\"Bad path\"}"); + return; + } + File f = SD.open(path); + bool ok = false; + if (f) { + bool isDir = f.isDirectory(); + f.close(); + ok = isDir ? SD.rmdir(path) : SD.remove(path); + } + digitalWrite(SDCARD_CS, HIGH); + _otaServer->send(ok ? 200 : 500, "application/json", + ok ? "{\"ok\":true}" : "{\"error\":\"Delete failed\"}"); + }); + + _otaServer->begin(); + Serial.println("FM: Web server started on port 80"); + _fmPhase = FM_PHASE_WAITING; + } + + void stopFileMgr() { + if (_otaServer) { _otaServer->stop(); delete _otaServer; _otaServer = nullptr; } + WiFi.softAPdisconnect(true); + WiFi.mode(WIFI_OFF); + delay(100); + _editMode = EDIT_NONE; + extern void otaResumeRadio(); + otaResumeRadio(); + #ifdef MECK_WIFI_COMPANION + WiFi.mode(WIFI_STA); + wifiReconnectSaved(); + #endif + Serial.println("FM: Stopped, AP down, radio resumed"); + } + + // --- File manager SPA HTML --- + static const char* fileMgrPageHTML() { + return + "" + "" + "Meck SD Files" + "" + "

Meck SD File Manager

" + "
/
" + "
" + "" + "" + "" + "
" + "" + "
" + "

Tap to select files or drag and drop

" + "" + "
" + "
" + "
" + "
" + ""; + } + #endif // --------------------------------------------------------------------------- @@ -1121,6 +1484,10 @@ public: display.print("Settings > Contacts"); } else if (_subScreen == SUB_CHANNELS) { display.print("Settings > Channels"); + #ifdef MECK_OTA_UPDATE + } else if (_subScreen == SUB_OTA_TOOLS) { + display.print("Settings > OTA Tools"); + #endif } else { display.print("Settings"); } @@ -1446,9 +1813,18 @@ public: break; #ifdef MECK_OTA_UPDATE + case ROW_OTA_TOOLS_SUBMENU: + display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN); + display.print("OTA Tools >>"); + break; + case ROW_FW_UPDATE: display.print("Firmware Update"); break; + + case ROW_SD_FILE_MGR: + display.print("SD File Manager"); + break; #endif case ROW_INFO_HEADER: @@ -1725,6 +2101,75 @@ public: display.setTextSize(1); } + + // === File Manager overlay === + if (_editMode == EDIT_FILEMGR) { + int bx = 2, by = 14, bw = display.width() - 4; + int bh = display.height() - 28; + display.setColor(DisplayDriver::DARK); + display.fillRect(bx, by, bw, bh); + display.setColor(DisplayDriver::LIGHT); + display.drawRect(bx, by, bw, bh); + + display.setTextSize(_prefs->smallTextSize()); + int oy = by + 4; + + if (_fmPhase == FM_PHASE_CONFIRM) { + display.drawTextCentered(display.width() / 2, oy, "SD File Manager"); + oy += 14; + display.setCursor(bx + 4, oy); + display.print("Start WiFi file server?"); + oy += 10; + display.setCursor(bx + 4, oy); + display.print("Browse, upload and download"); + oy += 8; + display.setCursor(bx + 4, oy); + display.print("SD card files via browser."); + oy += 10; + display.setCursor(bx + 4, oy); + display.setColor(DisplayDriver::YELLOW); + display.print("LoRa paused while active."); + display.setColor(DisplayDriver::LIGHT); + + } else if (_fmPhase == FM_PHASE_WAITING) { + display.drawTextCentered(display.width() / 2, oy, "SD File Manager"); + oy += 14; + display.setCursor(bx + 4, oy); + display.print("Connect to WiFi network:"); + oy += 10; + display.setColor(DisplayDriver::GREEN); + display.setCursor(bx + 4, oy); + display.print(_otaApName); + display.setColor(DisplayDriver::LIGHT); + oy += 12; + display.setCursor(bx + 4, oy); + display.print("Then open browser:"); + oy += 10; + display.setColor(DisplayDriver::GREEN); + display.setCursor(bx + 4, oy); + char ipBuf[32]; + snprintf(ipBuf, sizeof(ipBuf), "http://%s", WiFi.softAPIP().toString().c_str()); + display.print(ipBuf); + display.setColor(DisplayDriver::LIGHT); + oy += 12; + display.setCursor(bx + 4, oy); + display.print("File server active..."); + + pollOTAServer(); + + } else if (_fmPhase == FM_PHASE_ERROR) { + display.setColor(DisplayDriver::YELLOW); + display.drawTextCentered(display.width() / 2, oy, "File Manager Error"); + display.setColor(DisplayDriver::LIGHT); + oy += 14; + if (_fmError) { + display.setCursor(bx + 4, oy); + display.print(_fmError); + } + } + + display.setTextSize(1); + } #endif // === Footer === @@ -1737,7 +2182,12 @@ public: if (_editMode == EDIT_NONE) { if (_subScreen != SUB_NONE) { display.print("Boot:Back"); - const char* r = (_subScreen == SUB_CHANNELS) ? "Tap:Select Hold:Del" : "Tap:Toggle Hold:Edit"; + const char* r; + if (_subScreen == SUB_CHANNELS) r = "Tap:Select Hold:Del"; + #ifdef MECK_OTA_UPDATE + else if (_subScreen == SUB_OTA_TOOLS) r = "Tap:Select"; + #endif + else r = "Tap:Toggle Hold:Edit"; display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY); display.print(r); } else { @@ -1786,6 +2236,19 @@ public: } else { display.print("Please wait..."); } + } else if (_editMode == EDIT_FILEMGR) { + if (_fmPhase == FM_PHASE_CONFIRM) { + display.print("Boot:Cancel"); + const char* r = "Tap:Start"; + display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY); + display.print(r); + } else if (_fmPhase == FM_PHASE_WAITING) { + display.print("Boot:Stop"); + } else if (_fmPhase == FM_PHASE_ERROR) { + display.print("Boot:Back"); + } else { + display.print("Please wait..."); + } #endif } else if (_editMode == EDIT_TEXT) { display.print("Hold:Type"); @@ -1823,6 +2286,16 @@ public: } else { display.print("Please wait..."); } + } else if (_editMode == EDIT_FILEMGR) { + if (_fmPhase == FM_PHASE_CONFIRM) { + display.print("Enter:Start Q:Cancel"); + } else if (_fmPhase == FM_PHASE_WAITING) { + display.print("Q:Stop"); + } else if (_fmPhase == FM_PHASE_ERROR) { + display.print("Q:Back"); + } else { + display.print("Please wait..."); + } #endif } else if (_editMode == EDIT_PICKER) { display.print("A/D:Choose Enter:Ok"); @@ -1843,9 +2316,10 @@ public: #endif #ifdef MECK_OTA_UPDATE - // Poll web server frequently during OTA waiting/receiving phases - if (_editMode == EDIT_OTA && - (_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) { + // Poll web server frequently during OTA waiting/receiving or file manager phases + if ((_editMode == EDIT_OTA && + (_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) || + (_editMode == EDIT_FILEMGR && _fmPhase == FM_PHASE_WAITING)) { return 200; // 200ms — fast enough for web server responsiveness } #endif @@ -1912,6 +2386,32 @@ public: // Consume all keys during OTA return true; } + + // --- File Manager flow --- + if (_editMode == EDIT_FILEMGR) { + if (_fmPhase == FM_PHASE_CONFIRM) { + if (c == '\r' || c == 13) { + startFileMgrServer(); + return true; + } + if (c == 'q' || c == 'Q') { + _editMode = EDIT_NONE; + return true; + } + } else if (_fmPhase == FM_PHASE_WAITING) { + if (c == 'q' || c == 'Q') { + stopFileMgr(); + return true; + } + } else if (_fmPhase == FM_PHASE_ERROR) { + if (c == 'q' || c == 'Q') { + stopFileMgr(); + return true; + } + } + // Consume all keys during file manager + return true; + } #endif #ifdef MECK_WIFI_COMPANION @@ -2484,9 +2984,20 @@ public: startEditText(""); break; #ifdef MECK_OTA_UPDATE + case ROW_OTA_TOOLS_SUBMENU: + _savedTopCursor = _cursor; + _subScreen = SUB_OTA_TOOLS; + _cursor = 0; + _scrollTop = 0; + rebuildRows(); + Serial.println("Settings: entered OTA Tools sub-screen"); + break; case ROW_FW_UPDATE: startOTA(); break; + case ROW_SD_FILE_MGR: + startFileMgr(); + break; #endif case ROW_CHANNEL: case ROW_PUB_KEY: diff --git a/variants/lilygo_tdeck_pro/TDeckBoard.h b/variants/lilygo_tdeck_pro/TDeckBoard.h index c9a09b2..f7bee9f 100644 --- a/variants/lilygo_tdeck_pro/TDeckBoard.h +++ b/variants/lilygo_tdeck_pro/TDeckBoard.h @@ -22,7 +22,7 @@ // T-Deck Pro battery capacity (all variants use 1400 mAh cell) #ifndef BQ27220_DESIGN_CAPACITY_MAH -#define BQ27220_DESIGN_CAPACITY_MAH 1400 +#define BQ27220_DESIGN_CAPACITY_MAH 2000 #endif class TDeckBoard : public ESP32Board {