new wifi companions with wifi setup in onboarding; update firmware version and date accordingly

This commit is contained in:
pelgraine
2026-03-03 21:59:30 +11:00
parent b444a664c5
commit 31db349305
6 changed files with 475 additions and 199 deletions
+2 -2
View File
@@ -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)
+54 -5
View File
@@ -1,5 +1,7 @@
#include <Arduino.h> // needed for PlatformIO
#include <esp_bt.h> // for esp_bt_controller_mem_release (web reader WiFi)
#ifdef BLE_PIN_CODE
#include <esp_bt.h> // for esp_bt_controller_mem_release (web reader WiFi)
#endif
#include <Mesh.h>
#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 <WiFi.h>
#include <helpers/esp32/SerialWifiInterface.h>
SerialWifiInterface serial_interface;
#ifndef TCP_PORT
#define TCP_PORT 5000
#endif
#elif defined(BLE_PIN_CODE)
#include <helpers/esp32/SerialBLEInterface.h>
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);
}
@@ -10,6 +10,11 @@
#include "ModemManager.h"
#endif
#ifdef MECK_WIFI_COMPANION
#include <WiFi.h>
#include <SD.h>
#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;
+16 -166
View File
@@ -1072,12 +1072,9 @@ private:
// Bookmarks & History
std::vector<String> _bookmarks;
std::vector<String> _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) ||
+50 -16
View File
@@ -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 <WiFi.h>
#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);
}