mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-10 02:32:44 +02:00
new wifi companions with wifi setup in onboarding; update firmware version and date accordingly
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user