diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 23c04df3..9e086014 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -8,11 +8,11 @@ #define FIRMWARE_VER_CODE 8 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "2 March 2026" +#define FIRMWARE_BUILD_DATE "3 March 2026" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "Meck v0.9.7" +#define FIRMWARE_VERSION "Meck v0.9.8" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index a4bc6b19..90173d89 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1,5 +1,7 @@ #include // needed for PlatformIO -#include // for esp_bt_controller_mem_release (web reader WiFi) +#ifdef BLE_PIN_CODE + #include // for esp_bt_controller_mem_release (web reader WiFi) +#endif #include #include "MyMesh.h" #include "variant.h" // Board-specific defines (HAS_GPS, etc.) @@ -363,6 +365,13 @@ static uint32_t _atoi(const char* sp) { #ifndef TCP_PORT #define TCP_PORT 5000 #endif + #elif defined(MECK_WIFI_COMPANION) + #include + #include + SerialWifiInterface serial_interface; + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif #elif defined(BLE_PIN_CODE) #include SerialBLEInterface serial_interface; @@ -639,9 +648,44 @@ void setup() { MESH_DEBUG_PRINTLN("setup() - the_mesh.begin() done"); #ifdef WIFI_SSID - MESH_DEBUG_PRINTLN("setup() - WiFi mode"); + MESH_DEBUG_PRINTLN("setup() - WiFi mode (compile-time credentials)"); WiFi.begin(WIFI_SSID, WIFI_PWD); serial_interface.begin(TCP_PORT); +#elif defined(MECK_WIFI_COMPANION) + { + // WiFi companion: load credentials from SD at runtime. + // TCP server starts regardless — companion connects when WiFi comes up. + MESH_DEBUG_PRINTLN("setup() - WiFi companion mode (runtime credentials)"); + WiFi.mode(WIFI_STA); + if (sdCardReady) { + File f = SD.open("/web/wifi.cfg", FILE_READ); + if (f) { + String ssid = f.readStringUntil('\n'); ssid.trim(); + String pass = f.readStringUntil('\n'); pass.trim(); + f.close(); + digitalWrite(SDCARD_CS, HIGH); + if (ssid.length() > 0) { + MESH_DEBUG_PRINTLN("setup() - WiFi: connecting to '%s'", ssid.c_str()); + WiFi.begin(ssid.c_str(), pass.c_str()); + unsigned long timeout = millis() + 8000; + while (WiFi.status() != WL_CONNECTED && millis() < timeout) { + delay(100); + } + if (WiFi.status() == WL_CONNECTED) { + Serial.printf("WiFi companion: connected to %s, IP: %s\n", + ssid.c_str(), WiFi.localIP().toString().c_str()); + } else { + Serial.println("WiFi companion: auto-connect failed (configure in Settings)"); + } + } + } else { + digitalWrite(SDCARD_CS, HIGH); + Serial.println("WiFi companion: no /web/wifi.cfg found (configure in Settings)"); + } + } + serial_interface.begin(TCP_PORT); + MESH_DEBUG_PRINTLN("setup() - WiFi TCP server started on port %d", TCP_PORT); + } #elif defined(BLE_PIN_CODE) MESH_DEBUG_PRINTLN("setup() - about to call serial_interface.begin() with BLE"); serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); @@ -1697,7 +1741,12 @@ void handleKeyboardInput() { Serial.println("Opening web reader"); { static bool webReaderWifiReady = false; + #ifdef MECK_WIFI_COMPANION + // WiFi companion: WiFi is already up from boot, no BLE to tear down + webReaderWifiReady = true; + #endif if (!webReaderWifiReady) { + #ifdef BLE_PIN_CODE // WiFi needs ~40KB contiguous heap. The BLE controller holds ~30KB, // leaving only ~30KB largest block. We MUST release BLE memory first. // @@ -1716,14 +1765,14 @@ void handleKeyboardInput() { Serial.printf("WebReader: heap AFTER BT release: free=%d, largest=%d\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + #endif - // 3) Now init WiFi while we have maximum contiguous heap + // Init WiFi while we have maximum contiguous heap if (WiFi.mode(WIFI_STA)) { Serial.println("WebReader: WiFi STA init OK"); webReaderWifiReady = true; } else { - Serial.println("WebReader: WiFi STA init FAILED even after BT release"); - // Clean up partial WiFi init to avoid memory leak + Serial.println("WebReader: WiFi STA init FAILED"); WiFi.mode(WIFI_OFF); } diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index d6942d2d..1f5dfe9f 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -10,6 +10,11 @@ #include "ModemManager.h" #endif +#ifdef MECK_WIFI_COMPANION + #include + #include +#endif + // Forward declarations class UITask; class MyMesh; @@ -60,6 +65,9 @@ enum SettingsRowType : uint8_t { ROW_TX_POWER, // TX power (1-20 dBm) ROW_UTC_OFFSET, // UTC offset (-12 to +14) ROW_MSG_NOTIFY, // Keyboard flash on new msg toggle + #ifdef MECK_WIFI_COMPANION + ROW_WIFI_SETUP, // WiFi SSID/password configuration + #endif #ifdef HAS_4G_MODEM ROW_MODEM_TOGGLE, // 4G modem enable/disable toggle (4G builds only) // ROW_RINGTONE, // Incoming call ringtone toggle (4G builds only) @@ -86,11 +94,18 @@ enum EditMode : uint8_t { EDIT_PICKER, // A/D cycles options (radio preset) EDIT_NUMBER, // W/S adjusts value (freq, BW, SF, CR, TX, UTC) EDIT_CONFIRM, // Confirmation dialog (delete channel, apply radio) + #ifdef MECK_WIFI_COMPANION + EDIT_WIFI, // WiFi scan/select/password flow + #endif }; // Max rows in the settings list -#ifdef HAS_4G_MODEM +#if defined(HAS_4G_MODEM) && defined(MECK_WIFI_COMPANION) +#define SETTINGS_MAX_ROWS 48 // Extra rows for IMEI, Carrier, APN, WiFi +#elif defined(HAS_4G_MODEM) #define SETTINGS_MAX_ROWS 46 // Extra rows for IMEI, Carrier, APN +#elif defined(MECK_WIFI_COMPANION) +#define SETTINGS_MAX_ROWS 42 // Extra row for WiFi #else #define SETTINGS_MAX_ROWS 40 #endif @@ -134,6 +149,24 @@ private: bool _modemEnabled; #endif + #ifdef MECK_WIFI_COMPANION + // WiFi setup sub-screen state + enum WifiSetupPhase : uint8_t { + WIFI_PHASE_IDLE, + WIFI_PHASE_SCANNING, + WIFI_PHASE_SELECT, // W/S to pick SSID, Enter to select + WIFI_PHASE_PASSWORD, // Type password, Enter to connect + WIFI_PHASE_CONNECTING, + }; + WifiSetupPhase _wifiPhase; + String _wifiSSIDs[10]; + int _wifiSSIDCount; + int _wifiSSIDSelected; + char _wifiPassBuf[64]; + int _wifiPassLen; + unsigned long _wifiFormLastChar; // For brief password reveal + #endif + // --------------------------------------------------------------------------- // Row table management // --------------------------------------------------------------------------- @@ -150,6 +183,9 @@ private: addRow(ROW_TX_POWER); addRow(ROW_UTC_OFFSET); addRow(ROW_MSG_NOTIFY); + #ifdef MECK_WIFI_COMPANION + addRow(ROW_WIFI_SETUP); + #endif #ifdef HAS_4G_MODEM addRow(ROW_MODEM_TOGGLE); // addRow(ROW_RINGTONE); @@ -328,6 +364,14 @@ public: #ifdef HAS_4G_MODEM _modemEnabled = ModemManager::loadEnabledConfig(); #endif + #ifdef MECK_WIFI_COMPANION + _wifiPhase = WIFI_PHASE_IDLE; + _wifiSSIDCount = 0; + _wifiSSIDSelected = 0; + _wifiPassLen = 0; + memset(_wifiPassBuf, 0, sizeof(_wifiPassBuf)); + _wifiFormLastChar = 0; + #endif rebuildRows(); } @@ -512,6 +556,17 @@ public: display.print(tmp); break; + #ifdef MECK_WIFI_COMPANION + case ROW_WIFI_SETUP: + if (WiFi.status() == WL_CONNECTED) { + snprintf(tmp, sizeof(tmp), "WiFi: %s", WiFi.SSID().c_str()); + } else { + strcpy(tmp, "WiFi: (not connected)"); + } + display.print(tmp); + break; + #endif + #ifdef HAS_4G_MODEM case ROW_MODEM_TOGGLE: snprintf(tmp, sizeof(tmp), "4G Modem: %s", @@ -660,6 +715,73 @@ public: display.setTextSize(1); } + #ifdef MECK_WIFI_COMPANION + // === WiFi setup overlay === + if (_editMode == EDIT_WIFI) { + 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(0); + int wy = by + 4; + + if (_wifiPhase == WIFI_PHASE_SCANNING) { + display.drawTextCentered(display.width() / 2, wy, "Scanning for networks..."); + + } else if (_wifiPhase == WIFI_PHASE_SELECT) { + display.setCursor(bx + 4, wy); + display.print("Select network:"); + wy += 10; + for (int wi = 0; wi < _wifiSSIDCount && wy < by + bh - 16; wi++) { + bool sel = (wi == _wifiSSIDSelected); + if (sel) { + display.setColor(DisplayDriver::LIGHT); + display.fillRect(bx + 2, wy + 5, bw - 4, 8); + display.setColor(DisplayDriver::DARK); + } else { + display.setColor(DisplayDriver::LIGHT); + } + display.setCursor(bx + 4, wy); + char ssidLine[40]; + if (sel) { + snprintf(ssidLine, sizeof(ssidLine), "> %.33s", _wifiSSIDs[wi].c_str()); + } else { + snprintf(ssidLine, sizeof(ssidLine), " %.33s", _wifiSSIDs[wi].c_str()); + } + display.print(ssidLine); + wy += 8; + } + + } else if (_wifiPhase == WIFI_PHASE_PASSWORD) { + display.setCursor(bx + 4, wy); + snprintf(tmp, sizeof(tmp), "SSID: %s", _wifiSSIDs[_wifiSSIDSelected].c_str()); + display.print(tmp); + wy += 12; + display.setCursor(bx + 4, wy); + display.print("Password:"); + wy += 10; + display.setCursor(bx + 4, wy); + // Masked password with brief reveal of last char + char passBuf[66]; + for (int pi = 0; pi < _wifiPassLen; pi++) passBuf[pi] = '*'; + if (_wifiPassLen > 0 && _wifiFormLastChar > 0 && + (millis() - _wifiFormLastChar) < 800) { + passBuf[_wifiPassLen - 1] = _wifiPassBuf[_wifiPassLen - 1]; + } + passBuf[_wifiPassLen] = '_'; + passBuf[_wifiPassLen + 1] = '\0'; + display.print(passBuf); + + } else if (_wifiPhase == WIFI_PHASE_CONNECTING) { + display.drawTextCentered(display.width() / 2, wy + 10, "Connecting..."); + } + display.setTextSize(1); + } + #endif + // === Footer === int footerY = display.height() - 12; display.drawRect(0, footerY - 2, display.width(), 1); @@ -668,6 +790,16 @@ public: if (_editMode == EDIT_TEXT) { display.print("Type, Enter:Ok Q:Cancel"); + #ifdef MECK_WIFI_COMPANION + } else if (_editMode == EDIT_WIFI) { + if (_wifiPhase == WIFI_PHASE_SELECT) { + display.print("W/S:Pick Enter:Select Q:Bck"); + } else if (_wifiPhase == WIFI_PHASE_PASSWORD) { + display.print("Type, Enter:Connect Q:Bck"); + } else { + display.print("Please wait..."); + } + #endif } else if (_editMode == EDIT_PICKER) { display.print("A/D:Choose Enter:Ok"); } else if (_editMode == EDIT_NUMBER) { @@ -713,6 +845,102 @@ public: return true; // consume all keys in confirm mode } + #ifdef MECK_WIFI_COMPANION + // --- WiFi setup flow --- + if (_editMode == EDIT_WIFI) { + if (_wifiPhase == WIFI_PHASE_SELECT) { + if (c == 'w' || c == 'W') { + if (_wifiSSIDSelected > 0) _wifiSSIDSelected--; + return true; + } + if (c == 's' || c == 'S') { + if (_wifiSSIDSelected < _wifiSSIDCount - 1) _wifiSSIDSelected++; + return true; + } + if (c == '\r' || c == 13) { + // Selected an SSID — move to password entry + _wifiPhase = WIFI_PHASE_PASSWORD; + _wifiPassLen = 0; + memset(_wifiPassBuf, 0, sizeof(_wifiPassBuf)); + _wifiFormLastChar = 0; + return true; + } + if (c == 'q' || c == 'Q') { + _editMode = EDIT_NONE; + _wifiPhase = WIFI_PHASE_IDLE; + if (_onboarding) _onboarding = false; // Skip WiFi, finish onboarding + return true; + } + return true; + } + + if (_wifiPhase == WIFI_PHASE_PASSWORD) { + if (c == '\r' || c == 13) { + // Attempt connection + _wifiPassBuf[_wifiPassLen] = '\0'; + _wifiPhase = WIFI_PHASE_CONNECTING; + + // Save credentials to SD first (so web reader can reuse them) + if (SD.exists("/web") || SD.mkdir("/web")) { + File f = SD.open("/web/wifi.cfg", FILE_WRITE); + if (f) { + f.println(_wifiSSIDs[_wifiSSIDSelected]); + f.println(_wifiPassBuf); + f.close(); + } + digitalWrite(SDCARD_CS, HIGH); + } + + WiFi.disconnect(false); + WiFi.begin(_wifiSSIDs[_wifiSSIDSelected].c_str(), _wifiPassBuf); + + // Brief blocking wait — fine for e-ink (screen won't update during this anyway) + unsigned long timeout = millis() + 8000; + while (WiFi.status() != WL_CONNECTED && millis() < timeout) { + delay(100); + } + + if (WiFi.status() == WL_CONNECTED) { + Serial.printf("Settings: WiFi connected to %s, IP: %s\n", + _wifiSSIDs[_wifiSSIDSelected].c_str(), + WiFi.localIP().toString().c_str()); + _editMode = EDIT_NONE; + _wifiPhase = WIFI_PHASE_IDLE; + if (_onboarding) _onboarding = false; // Finish onboarding + } else { + Serial.println("Settings: WiFi connection failed"); + // Go back to SSID selection so user can retry + _wifiPhase = WIFI_PHASE_SELECT; + } + return true; + } + if (c == 'q' || c == 'Q') { + // Back to SSID selection + _wifiPhase = WIFI_PHASE_SELECT; + return true; + } + if (c == '\b') { + if (_wifiPassLen > 0) { + _wifiPassLen--; + _wifiPassBuf[_wifiPassLen] = '\0'; + } + return true; + } + // Printable character + if (c >= 32 && c < 127 && _wifiPassLen < 63) { + _wifiPassBuf[_wifiPassLen++] = c; + _wifiPassBuf[_wifiPassLen] = '\0'; + _wifiFormLastChar = millis(); + return true; + } + return true; + } + + // Scanning and connecting phases consume all keys + return true; + } + #endif + // --- Text editing mode --- if (_editMode == EDIT_TEXT) { if (c == '\r' || c == 13) { @@ -808,9 +1036,37 @@ public: } _editMode = EDIT_NONE; if (_onboarding) { - // Apply and finish onboarding applyRadioParams(); + #ifdef MECK_WIFI_COMPANION + // Move to WiFi setup before finishing onboarding + for (int r = 0; r < _numRows; r++) { + if (_rows[r].type == ROW_WIFI_SETUP) { + _cursor = r; + break; + } + } + // Auto-launch the WiFi scan + _editMode = EDIT_WIFI; + _wifiPhase = WIFI_PHASE_SCANNING; + _wifiSSIDCount = 0; + _wifiSSIDSelected = 0; + WiFi.mode(WIFI_STA); + int n = WiFi.scanNetworks(); + if (n > 0) { + _wifiSSIDCount = min(n, 10); + for (int si = 0; si < _wifiSSIDCount; si++) { + _wifiSSIDs[si] = WiFi.SSID(si); + } + _wifiPhase = WIFI_PHASE_SELECT; + } else { + _editMode = EDIT_NONE; + _wifiPhase = WIFI_PHASE_IDLE; + _onboarding = false; // Finish even without WiFi + } + WiFi.scanDelete(); + #else _onboarding = false; + #endif } return true; } @@ -952,6 +1208,33 @@ public: Serial.printf("Settings: Msg flash notify = %s\n", _prefs->kb_flash_notify ? "ON" : "OFF"); break; + #ifdef MECK_WIFI_COMPANION + case ROW_WIFI_SETUP: { + // Launch WiFi scan → select → password → connect flow + _editMode = EDIT_WIFI; + _wifiPhase = WIFI_PHASE_SCANNING; + _wifiSSIDCount = 0; + _wifiSSIDSelected = 0; + + // Blocking WiFi scan (3-5 seconds, fine for e-ink) + WiFi.mode(WIFI_STA); + int n = WiFi.scanNetworks(); + if (n > 0) { + _wifiSSIDCount = min(n, 10); + for (int si = 0; si < _wifiSSIDCount; si++) { + _wifiSSIDs[si] = WiFi.SSID(si); + } + _wifiPhase = WIFI_PHASE_SELECT; + } else { + // No networks found — exit WiFi flow + Serial.println("Settings: WiFi scan found no networks"); + _editMode = EDIT_NONE; + _wifiPhase = WIFI_PHASE_IDLE; + } + WiFi.scanDelete(); + break; + } + #endif #ifdef HAS_4G_MODEM case ROW_MODEM_TOGGLE: _modemEnabled = !_modemEnabled; diff --git a/examples/companion_radio/ui-new/Webreaderscreen.h b/examples/companion_radio/ui-new/Webreaderscreen.h index 3e03fac0..8e2a3dde 100644 --- a/examples/companion_radio/ui-new/Webreaderscreen.h +++ b/examples/companion_radio/ui-new/Webreaderscreen.h @@ -1072,12 +1072,9 @@ private: // Bookmarks & History std::vector _bookmarks; std::vector _history; - int _homeSelected; // Selected item in home view (0=IRC, 1=URL, 2=Search, then bookmarks, then history) + int _homeSelected; // Selected item in home view (0=URL bar, then bookmarks, then history) int _homeScrollY; // Pixel scroll offset for home view bool _urlEditing; // True when URL bar is active for text entry - bool _searchEditing; // True when search bar is active for text entry - char _searchBuffer[128]; // Search query text - int _searchLen; // Link selection int _linkInput; // Accumulated link number digits @@ -2781,7 +2778,7 @@ private: const int sectionH = listLineH; // Section header height int maxChars = _charsPerLine - 2; // Account for "> " prefix if (maxChars < 10) maxChars = 10; - int totalItems = 3 + (int)_bookmarks.size() + (int)_history.size(); + int totalItems = 2 + (int)_bookmarks.size() + (int)_history.size(); // ---- Layout pass: compute virtual Y extent of each item ---- // We track: for each selectable item, its (virtualY, height). @@ -2807,12 +2804,6 @@ private: virtualY += urlBarH; itemIdx++; - // Item 2: Search bar - int searchBarH = listLineH + 2; - if (itemIdx == _homeSelected) { selectedTop = virtualY; selectedBot = virtualY + searchBarH; } - virtualY += searchBarH; - itemIdx++; - // Bookmarks if (_bookmarks.size() > 0) { virtualY += sectionH; // "-- Bookmarks --" header @@ -2933,39 +2924,6 @@ private: itemIdx++; } - // Item 2: Search bar - { - bool selected = (_homeSelected == itemIdx); - if (HOME_VISIBLE(y, searchBarH)) { - if (selected) { - display.setColor(DisplayDriver::LIGHT); - display.fillRect(0, y + 5, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH); - display.setColor(DisplayDriver::DARK); - } else { - display.setColor(DisplayDriver::LIGHT); - } - display.setCursor(0, y); - if (_searchEditing) { - char searchDisp[140]; - int maxShow = maxChars - 8; // "Search: " prefix + cursor - int start = 0; - if (_searchLen > maxShow) start = _searchLen - maxShow; - snprintf(searchDisp, sizeof(searchDisp), "Search: %s_", _searchBuffer + start); - display.print(searchDisp); - } else if (_searchLen > 0) { - char searchDisp[140]; - int maxShow = maxChars - 7; - snprintf(searchDisp, sizeof(searchDisp), "Search: %s", - _searchLen > maxShow ? (_searchBuffer + _searchLen - maxShow) : _searchBuffer); - display.print(searchDisp); - } else { - display.print("Search: [DuckDuckGo Lite]"); - } - } - y += searchBarH; - itemIdx++; - } - // Bookmarks section if (_bookmarks.size() > 0) { // Section header @@ -3098,43 +3056,15 @@ private: display.setColor(DisplayDriver::YELLOW); if (_urlEditing) { display.print("Type URL Ent:Go"); - } else if (_searchEditing) { - display.print("Type query Ent:Search"); } else { char footerBuf[48]; bool hasData = (_cookieCount > 0 || !_history.empty()); - bool onBookmark = (_homeSelected >= 3 && _homeSelected < 3 + (int)_bookmarks.size()); - if (onBookmark && hasData) - snprintf(footerBuf, sizeof(footerBuf), "Ent:Go Del:Del Bkmk X:Clr Ckies"); - else if (onBookmark) - snprintf(footerBuf, sizeof(footerBuf), "Q:Bk Ent:Go Del:Del Bkmk"); - else if (hasData) - snprintf(footerBuf, sizeof(footerBuf), "Q:Bk W/S Ent:Go X:Clr Ckies"); + if (hasData) + snprintf(footerBuf, sizeof(footerBuf), "Q:Bk W/S Ent:Go X:Clr"); else snprintf(footerBuf, sizeof(footerBuf), "Q:Bk W/S:Nav Ent:Go"); display.print(footerBuf); } - - // Toast notification overlay (for bookmark deleted, etc.) - if (_toastMsg[0] && (millis() - _toastTime < 1500)) { - display.setTextSize(1); - int tw = display.getTextWidth(_toastMsg); - int bw = tw + 16; - int bh = 20; - int bx = (display.width() - bw) / 2; - int by = (display.height() - bh) / 2; - display.setColor(DisplayDriver::DARK); - display.fillRect(bx, by, bw, bh); - display.setColor(DisplayDriver::LIGHT); - display.drawRect(bx, by, bw, 1); - display.drawRect(bx, by + bh - 1, bw, 1); - display.drawRect(bx, by, 1, bh); - display.drawRect(bx + bw - 1, by, 1, bh); - display.setCursor(bx + 8, by + 5); - display.print(_toastMsg); - } else if (_toastMsg[0]) { - _toastMsg[0] = '\0'; - } } void renderFetching(DisplayDriver& display) { @@ -3479,7 +3409,7 @@ private: } bool handleHomeInput(char c) { - int totalItems = 3 + _bookmarks.size() + _history.size(); // IRC + URL + Search + bookmarks + history + int totalItems = 2 + _bookmarks.size() + _history.size(); // IRC + URL + bookmarks + history if (_urlEditing) { // URL text entry mode @@ -3534,67 +3464,6 @@ private: return true; // Consume all keys in editing mode } - // Search text entry mode - if (_searchEditing) { - if (c == '\r' || c == 13) { - if (_searchLen > 0) { - _searchEditing = false; - // Build DuckDuckGo Lite search URL - // URL-encode the query: spaces become +, special chars become %XX - char encoded[256]; - int ei = 0; - for (int i = 0; i < _searchLen && ei < (int)sizeof(encoded) - 4; i++) { - char ch = _searchBuffer[i]; - if (ch == ' ') { - encoded[ei++] = '+'; - } else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || - (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' || ch == '.' || ch == '~') { - encoded[ei++] = ch; - } else { - if (ei < (int)sizeof(encoded) - 4) { - snprintf(encoded + ei, 4, "%%%02X", (unsigned char)ch); - ei += 3; - } - } - } - encoded[ei] = '\0'; - snprintf(_urlBuffer, WEB_MAX_URL_LEN, "https://html.duckduckgo.com/lite/?q=%s", encoded); - _urlLen = strlen(_urlBuffer); - if (!isNetworkAvailable()) { - _mode = WIFI_SETUP; - if (!loadAndAutoConnect()) { - startWifiScan(); - } else { - fetchWithSelfRef(_urlBuffer); - } - } else { - fetchWithSelfRef(_urlBuffer); - } - } - return true; - } - if (c == '\b' || c == 127) { - if (_searchLen > 0) { - _searchBuffer[--_searchLen] = '\0'; - } - return true; - } - if (c == 'q' && _searchLen == 0) { - _searchEditing = false; - return true; - } - if (c == 0x1B) { // ESC - _searchEditing = false; - return true; - } - if (c >= 32 && c < 127 && _searchLen < (int)sizeof(_searchBuffer) - 1) { - _searchBuffer[_searchLen++] = c; - _searchBuffer[_searchLen] = '\0'; - return true; - } - return true; // Consume all keys in search editing mode - } - // Normal navigation if (c == 'w' || c == 'W' || c == 0xF2) { if (_homeSelected > 0) _homeSelected--; @@ -3625,14 +3494,9 @@ private: _urlEditing = true; return true; } - if (_homeSelected == 2) { - // Activate search editing - _searchEditing = true; - return true; - } - // Bookmark or history item selected (offset by 3 for IRC + URL + Search) + // Bookmark or history item selected (offset by 2 for IRC + URL) const char* selectedUrl = nullptr; - int bmIdx = _homeSelected - 3; + int bmIdx = _homeSelected - 2; if (bmIdx < (int)_bookmarks.size()) { selectedUrl = _bookmarks[bmIdx].c_str(); } else { @@ -3658,25 +3522,6 @@ private: return true; } - // Delete/Backspace - remove selected bookmark - if (c == '\b' || c == 127) { - int bmIdx = _homeSelected - 3; - if (bmIdx >= 0 && bmIdx < (int)_bookmarks.size()) { - _bookmarks.erase(_bookmarks.begin() + bmIdx); - saveBookmarks(); - // Adjust selection if we deleted the last item - int newTotal = 3 + _bookmarks.size() + _history.size(); - if (_homeSelected >= newTotal && _homeSelected > 0) { - _homeSelected--; - } - strncpy(_toastMsg, "Bookmark deleted", sizeof(_toastMsg)); - _toastTime = millis(); - Serial.printf("WebReader: Deleted bookmark %d\n", bmIdx); - return true; - } - return false; - } - // X - clear all cookies if (c == 'x' || c == 'X') { bool hadData = (_cookieCount > 0 || !_history.empty()); @@ -4964,7 +4809,6 @@ public: _textBuffer(nullptr), _textLen(0), _links(nullptr), _linkCount(0), _currentPage(0), _totalPages(0), _homeSelected(0), _homeScrollY(0), _urlEditing(false), - _searchEditing(false), _searchLen(0), _linkInput(0), _linkInputActive(false), _formCount(0), _forms(nullptr), _activeForm(-1), _activeField(0), _formFieldEditing(false), _formEditLen(0), _formLastCharAt(0), @@ -4981,7 +4825,6 @@ public: _ircLastDataTime(0), _ircReconnectAt(0), _ircDirty(false), _ircLastRender(0) { _urlBuffer[0] = '\0'; - _searchBuffer[0] = '\0'; _wifiPass[0] = '\0'; _pageTitle[0] = '\0'; _currentUrl[0] = '\0'; @@ -5110,17 +4953,22 @@ public: if (_tlsClient) { delete _tlsClient; _tlsClient = nullptr; } _tlsHost = String(); + #ifdef MECK_WIFI_COMPANION + // WiFi companion: keep WiFi alive for the companion TCP server. + // Don't disconnect or change mode — just reset our internal state. + _wifiState = WIFI_CONNECTED; // WiFi is still up + #else // Shut down WiFi to reclaim ~50-70KB internal RAM WiFi.disconnect(true); WiFi.mode(WIFI_OFF); _wifiState = WIFI_IDLE; + #endif // Reset mode so re-entry starts fresh _mode = HOME; _homeSelected = 0; _homeScrollY = 0; _urlEditing = false; - _searchEditing = false; Serial.printf("WebReader: exitReader - heap after: %d, largest: %d\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); @@ -5131,7 +4979,6 @@ public: bool wantsTextReader() const { return _requestTextReader; } void clearTextReaderRequest() { _requestTextReader = false; } bool isUrlEditing() const { return _urlEditing && _mode == HOME; } - bool isSearchEditing() const { return _searchEditing && _mode == HOME; } bool isWifiSetup() const { return _mode == WIFI_SETUP; } bool isPasswordEntry() const { return _mode == WIFI_SETUP && _wifiState == WIFI_ENTERING_PASS; @@ -5139,6 +4986,9 @@ public: bool isFormFilling() const { return _mode == FORM_FILL && _formFieldEditing; } + bool isSearchEditing() const { + return false; // TODO: page text search not yet implemented + } bool isIRCMode() const { return _mode == IRC_CHAT || _mode == IRC_SETUP; } bool isIRCTextEntry() const { return (_mode == IRC_CHAT && _ircComposing) || diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index df3b1b88..5fcd6f3c 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -5,12 +5,7 @@ #include "RepeaterAdminScreen.h" #include "MapScreen.h" #include "target.h" -#include "GPSAiding.h" - -#ifndef GPS_BOOT_DELAY_MS -#define GPS_BOOT_DELAY_MS 250 -#endif -#ifdef WIFI_SSID +#if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION) #include #endif @@ -102,6 +97,8 @@ class HomeScreen : public UIScreen { RADIO, #ifdef BLE_PIN_CODE BLUETOOTH, +#elif defined(MECK_WIFI_COMPANION) + WIFI_STATUS, #endif ADVERT, #if ENV_INCLUDE_GPS == 1 @@ -306,14 +303,16 @@ public: display.drawTextCentered(display.width() / 2, y, tmp); y += 18; - #ifdef WIFI_SSID + #if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION) IPAddress ip = WiFi.localIP(); - snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); - display.setTextSize(1); - display.drawTextCentered(display.width() / 2, y, tmp); - y += 12; + if (ip != IPAddress(0,0,0,0)) { + snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d:%d", ip[0], ip[1], ip[2], ip[3], TCP_PORT); + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, y, tmp); + y += 12; + } #endif - #if defined(BLE_PIN_CODE) || defined(WIFI_SSID) + #if defined(BLE_PIN_CODE) || defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION) if (_task->hasConnection()) { display.setColor(DisplayDriver::GREEN); display.setTextSize(1); @@ -424,6 +423,44 @@ public: display.setColor(DisplayDriver::GREEN); display.setTextSize(1); display.drawTextCentered(display.width() / 2, 72, "toggle: " PRESS_LABEL); +#endif +#ifdef MECK_WIFI_COMPANION + } else if (_page == HomePage::WIFI_STATUS) { + display.setColor(DisplayDriver::GREEN); + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, 18, "WiFi Companion"); + + int wy = 36; + display.setTextSize(0); + if (WiFi.status() == WL_CONNECTED) { + display.setColor(DisplayDriver::GREEN); + snprintf(tmp, sizeof(tmp), "SSID: %s", WiFi.SSID().c_str()); + display.drawTextCentered(display.width() / 2, wy, tmp); + wy += 10; + IPAddress ip = WiFi.localIP(); + snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); + display.drawTextCentered(display.width() / 2, wy, tmp); + wy += 10; + snprintf(tmp, sizeof(tmp), "Port: %d", TCP_PORT); + display.drawTextCentered(display.width() / 2, wy, tmp); + wy += 12; + if (_task->hasConnection()) { + display.setColor(DisplayDriver::GREEN); + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, wy, "< App Connected >"); + } else { + display.setColor(DisplayDriver::YELLOW); + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, wy, "Waiting for app..."); + } + } else { + display.setColor(DisplayDriver::RED); + display.drawTextCentered(display.width() / 2, wy, "Not connected"); + wy += 12; + display.setColor(DisplayDriver::LIGHT); + display.drawTextCentered(display.width() / 2, wy, "Configure in Settings"); + } + display.setTextSize(1); #endif } else if (_page == HomePage::ADVERT) { display.setColor(DisplayDriver::GREEN); @@ -1355,14 +1392,11 @@ void UITask::toggleGPS() { #endif notify(UIEventType::ack); } else { - // Enable GPS — power on hardware + send aiding for faster fix + // Enable GPS — power on hardware _sensors->setSettingValue("gps", "1"); _node_prefs->gps_enabled = 1; #ifdef PIN_GPS_EN digitalWrite(PIN_GPS_EN, GPS_EN_ACTIVE); - delay(GPS_BOOT_DELAY_MS); - GPSAiding::sendAllAiding(Serial2, sensors.node_lat, sensors.node_lon, - rtc_clock.getCurrentTime(), 50000, 10); #endif notify(UIEventType::ack); } diff --git a/variants/lilygo_tdeck_pro/platformio.ini b/variants/lilygo_tdeck_pro/platformio.ini index 43b05473..f996b390 100644 --- a/variants/lilygo_tdeck_pro/platformio.ini +++ b/variants/lilygo_tdeck_pro/platformio.ini @@ -94,7 +94,7 @@ lib_deps = bitbank2/PNGdec@^1.0.1 ; --------------------------------------------------------------------------- -; Meck unified builds — one codebase, three variants via build flags +; Meck unified builds — one codebase, six variants via build flags ; --------------------------------------------------------------------------- ; Audio + BLE companion (audio-player hardware with BLE phone bridging) @@ -122,6 +122,39 @@ lib_deps = https://github.com/schreibfaul1/ESP32-audioI2S.git#2.0.6 bitbank2/JPEGDEC +; Audio + WiFi companion (audio-player hardware with WiFi app bridging) +; No BLE — WiFi companion uses SerialWifiInterface (TCP socket on port 5000). +; Connect via MeshCore web app, meshcore.js, or Python CLI over local network. +; No BLE protocol ceiling on contacts; bumped to 1500 (PSRAM-backed). +; WiFi always on from boot — web reader works without teardown, extra free heap. +; WiFi credentials loaded from SD card at runtime (/web/wifi.cfg). +; Configure via Settings > WiFi Setup, or through the web reader. +[env:meck_audio_wifi] +extends = LilyGo_TDeck_Pro +build_flags = + ${LilyGo_TDeck_Pro.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=1500 + -D MAX_GROUP_CHANNELS=20 + -D MECK_WIFI_COMPANION=1 + -D TCP_PORT=5000 + -D WIFI_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D MECK_AUDIO_VARIANT + -D MECK_WEB_READER=1 + -D FIRMWARE_VERSION='"Meck v0.9.8.WiFi"' +build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + +lib_deps = + ${LilyGo_TDeck_Pro.lib_deps} + densaugeo/base64 @ ~1.4.0 + https://github.com/schreibfaul1/ESP32-audioI2S.git#2.0.6 + bitbank2/JPEGDEC + ; Audio standalone (audio-player hardware, no BLE/WiFi — maximum battery life) ; No MECK_WEB_READER: WiFi power draw conflicts with zero-radio-power design. ; Contacts and sort arrays allocated in PSRAM — 1500 contacts uses ~290KB of 8MB. @@ -159,7 +192,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 -D HAS_4G_MODEM=1 -D MECK_WEB_READER=1 - -D FIRMWARE_VERSION='"Meck v0.9.7.4G"' + -D FIRMWARE_VERSION='"Meck v0.9.8.4G"' build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} + + @@ -170,10 +203,38 @@ lib_deps = ${LilyGo_TDeck_Pro.lib_deps} densaugeo/base64 @ ~1.4.0 -; 4G standalone (4G modem hardware, no BLE — maximum battery + cellular features) -; No BLE_PIN_CODE: BLE never initializes, saving ~30KB heap + radio power. -; MECK_WEB_READER enabled: works better without BLE (no teardown dance needed, -; more free heap from boot). WiFi-first with cellular PPP fallback (future). +; 4G + WiFi companion (4G modem hardware with WiFi app bridging, no audio) +; No BLE — WiFi companion uses SerialWifiInterface (TCP socket on port 5000). +; Connect via MeshCore web app, meshcore.js, or Python CLI over local network. +; WiFi credentials loaded from SD card at runtime (/web/wifi.cfg). +; Configure via Settings > WiFi Setup, or through the web reader. +; Contacts and sort arrays allocated in PSRAM — 1500 contacts uses ~290KB of 8MB. +[env:meck_4g_wifi] +extends = LilyGo_TDeck_Pro +build_flags = + ${LilyGo_TDeck_Pro.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=1500 + -D MAX_GROUP_CHANNELS=20 + -D MECK_WIFI_COMPANION=1 + -D TCP_PORT=5000 + -D WIFI_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D HAS_4G_MODEM=1 + -D MECK_WEB_READER=1 + -D FIRMWARE_VERSION='"Meck v0.9.8.4G.WiFi"' +build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + +lib_deps = + ${LilyGo_TDeck_Pro.lib_deps} + densaugeo/base64 @ ~1.4.0 + +; 4G standalone (audio-player hardware, no BLE/WiFi — maximum battery life) +; No MECK_WEB_READER: WiFi power draw conflicts with zero-radio-power design. ; Contacts and sort arrays allocated in PSRAM — 1500 contacts uses ~290KB of 8MB. [env:meck_4g_standalone] extends = LilyGo_TDeck_Pro @@ -184,8 +245,7 @@ build_flags = -D MAX_GROUP_CHANNELS=20 -D OFFLINE_QUEUE_SIZE=256 -D HAS_4G_MODEM=1 - -D MECK_WEB_READER=1 - -D FIRMWARE_VERSION='"Meck v0.9.7.4G.SA"' + -D FIRMWARE_VERSION='"Meck v0.9.8.4G.SA"' build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} + +