mirror of
https://github.com/pelgraine/Meck.git
synced 2026-07-07 02:01:19 +02:00
preliminary html web reader app stage 1
This commit is contained in:
@@ -8,11 +8,11 @@
|
||||
#define FIRMWARE_VER_CODE 8
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "20 Feb 2026"
|
||||
#define FIRMWARE_BUILD_DATE "22 Feb 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "Meck v0.9.2"
|
||||
#define FIRMWARE_VERSION "Meck v0.9.3"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <esp_bt.h> // for esp_bt_controller_mem_release (web reader WiFi)
|
||||
#include <Mesh.h>
|
||||
#include "MyMesh.h"
|
||||
#include "variant.h" // Board-specific defines (HAS_GPS, etc.)
|
||||
@@ -16,6 +17,9 @@
|
||||
#include "ChannelScreen.h"
|
||||
#include "SettingsScreen.h"
|
||||
#include "RepeaterAdminScreen.h"
|
||||
#ifdef MECK_WEB_READER
|
||||
#include "WebReaderScreen.h"
|
||||
#endif
|
||||
extern SPIClass displaySpi; // From GxEPDDisplay.cpp, shared SPI bus
|
||||
|
||||
TCA8418Keyboard keyboard(I2C_ADDR_KEYBOARD, &Wire);
|
||||
@@ -33,6 +37,11 @@
|
||||
static bool composeDM = false;
|
||||
static int composeDMContactIdx = -1;
|
||||
static char composeDMName[32];
|
||||
#ifdef MECK_WEB_READER
|
||||
static unsigned long lastWebReaderRefresh = 0;
|
||||
static bool webReaderNeedsRefresh = false;
|
||||
static bool webReaderTextEntry = false; // True when URL/password entry active
|
||||
#endif
|
||||
// AGC reset - periodically re-assert RX boosted gain to prevent sensitivity drift
|
||||
#define AGC_RESET_INTERVAL_MS 500
|
||||
static unsigned long lastAGCReset = 0;
|
||||
@@ -691,10 +700,21 @@ void loop() {
|
||||
#else
|
||||
bool smsSuppressLoop = false;
|
||||
#endif
|
||||
if (!composeMode && !notesSuppressLoop && !smsSuppressLoop) {
|
||||
#ifdef MECK_WEB_READER
|
||||
// Safety: clear web reader text entry flag if we're no longer on the web reader
|
||||
if (webReaderTextEntry && !ui_task.isOnWebReader()) {
|
||||
webReaderTextEntry = false;
|
||||
webReaderNeedsRefresh = false;
|
||||
}
|
||||
#endif
|
||||
if (!composeMode && !notesSuppressLoop && !smsSuppressLoop
|
||||
#ifdef MECK_WEB_READER
|
||||
&& !webReaderTextEntry
|
||||
#endif
|
||||
) {
|
||||
ui_task.loop();
|
||||
} else {
|
||||
// Handle debounced screen refresh (compose, emoji picker, or notes editor)
|
||||
// Handle debounced screen refresh (compose, emoji picker, notes, or web reader text entry)
|
||||
if (composeNeedsRefresh && (millis() - lastComposeRefresh) >= COMPOSE_REFRESH_INTERVAL) {
|
||||
if (composeMode) {
|
||||
if (emojiPickerMode) {
|
||||
@@ -708,7 +728,7 @@ void loop() {
|
||||
ui_task.loop();
|
||||
} else if (smsSuppressLoop) {
|
||||
// SMS compose: render directly to display, same as mesh compose
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(DISPLAY_CLASS) && defined(HAS_4G_MODEM)
|
||||
display.startFrame();
|
||||
((SMSScreen*)ui_task.getSMSScreen())->render(display);
|
||||
display.endFrame();
|
||||
@@ -717,6 +737,28 @@ void loop() {
|
||||
lastComposeRefresh = millis();
|
||||
composeNeedsRefresh = false;
|
||||
}
|
||||
#ifdef MECK_WEB_READER
|
||||
if (webReaderNeedsRefresh && (millis() - lastWebReaderRefresh) >= COMPOSE_REFRESH_INTERVAL) {
|
||||
WebReaderScreen* wr2 = (WebReaderScreen*)ui_task.getWebReaderScreen();
|
||||
if (wr2) {
|
||||
display.startFrame();
|
||||
wr2->render(display);
|
||||
display.endFrame();
|
||||
}
|
||||
lastWebReaderRefresh = millis();
|
||||
webReaderNeedsRefresh = false;
|
||||
}
|
||||
// Password reveal expiry: re-render to mask character after 800ms
|
||||
if (webReaderTextEntry && !webReaderNeedsRefresh) {
|
||||
WebReaderScreen* wr3 = (WebReaderScreen*)ui_task.getWebReaderScreen();
|
||||
if (wr3 && wr3->needsRevealRefresh() && (millis() - lastWebReaderRefresh) >= 850) {
|
||||
display.startFrame();
|
||||
wr3->render(display);
|
||||
display.endFrame();
|
||||
lastWebReaderRefresh = millis();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
// Track reader/notes/audiobook mode state for key routing
|
||||
readerMode = ui_task.isOnTextReader();
|
||||
@@ -1214,6 +1256,51 @@ void handleKeyboardInput() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// *** WEB READER TEXT INPUT MODE ***
|
||||
// Match compose mode pattern: key handler sets a flag and returns instantly.
|
||||
// Main loop renders with 100ms debounce (same as COMPOSE_REFRESH_INTERVAL).
|
||||
// This way the key handler never blocks for 648ms during a render.
|
||||
#ifdef MECK_WEB_READER
|
||||
if (ui_task.isOnWebReader()) {
|
||||
WebReaderScreen* wr = (WebReaderScreen*)ui_task.getWebReaderScreen();
|
||||
bool urlEdit = wr ? wr->isUrlEditing() : false;
|
||||
bool passEdit = wr ? wr->isPasswordEntry() : false;
|
||||
bool formEdit = wr ? wr->isFormFilling() : false;
|
||||
if (wr && (urlEdit || passEdit || formEdit)) {
|
||||
webReaderTextEntry = true; // Suppress ui_task.loop() in main loop
|
||||
wr->handleInput(key); // Updates buffer instantly, no render
|
||||
|
||||
// Check if text entry ended (submitted, cancelled, etc.)
|
||||
if (!wr->isUrlEditing() && !wr->isPasswordEntry() && !wr->isFormFilling()) {
|
||||
// Text entry ended
|
||||
webReaderTextEntry = false;
|
||||
webReaderNeedsRefresh = false;
|
||||
// fetchPage()/submitForm() handle their own rendering, or mode changed —
|
||||
// let ui_task.loop() resume on next iteration
|
||||
} else {
|
||||
// Still typing — request debounced refresh
|
||||
webReaderNeedsRefresh = true;
|
||||
lastWebReaderRefresh = millis();
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// Not in text entry — clear flag so ui_task.loop() resumes
|
||||
webReaderTextEntry = false;
|
||||
|
||||
// Q from HOME mode exits the web reader entirely (like text reader)
|
||||
if ((key == 'q' || key == 'Q') && wr && wr->isHome() && !wr->isUrlEditing()) {
|
||||
Serial.println("Exiting web reader");
|
||||
ui_task.gotoHomeScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
// Route keys through normal UITask for navigation/scrolling
|
||||
ui_task.injectKey(key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Normal mode - not composing
|
||||
switch (key) {
|
||||
case 'c':
|
||||
@@ -1258,6 +1345,50 @@ void handleKeyboardInput() {
|
||||
ui_task.gotoSMSScreen();
|
||||
break;
|
||||
#endif
|
||||
|
||||
#ifdef MECK_WEB_READER
|
||||
case 'b':
|
||||
// Open web reader (browser)
|
||||
Serial.println("Opening web reader");
|
||||
{
|
||||
static bool webReaderWifiReady = false;
|
||||
if (!webReaderWifiReady) {
|
||||
// WiFi needs ~40KB contiguous heap. The BLE controller holds ~30KB,
|
||||
// leaving only ~30KB largest block. We MUST release BLE memory first.
|
||||
//
|
||||
// This disables BLE for the duration of the session.
|
||||
// BLE comes back on reboot.
|
||||
Serial.printf("WebReader: heap BEFORE BT release: free=%d, largest=%d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
|
||||
// 1) Stop BLE controller (disable + deinit)
|
||||
btStop();
|
||||
delay(50);
|
||||
|
||||
// 2) Release the BT controller's reserved memory region back to heap
|
||||
esp_bt_controller_mem_release(ESP_BT_MODE_BTDM);
|
||||
delay(50);
|
||||
|
||||
Serial.printf("WebReader: heap AFTER BT release: free=%d, largest=%d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
|
||||
// 3) Now 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
|
||||
WiFi.mode(WIFI_OFF);
|
||||
}
|
||||
|
||||
Serial.printf("WebReader: heap after WiFi init: free=%d, largest=%d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
}
|
||||
}
|
||||
ui_task.gotoWebReader();
|
||||
break;
|
||||
#endif
|
||||
|
||||
case 'n':
|
||||
// Open notes
|
||||
@@ -1274,9 +1405,13 @@ void handleKeyboardInput() {
|
||||
break;
|
||||
|
||||
case 's':
|
||||
// Open settings (from home), or navigate down on channel/contacts/admin
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()) {
|
||||
ui_task.injectKey('s'); // Pass directly for channel/contacts scrolling
|
||||
// Open settings (from home), or navigate down on channel/contacts/admin/web
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()
|
||||
#ifdef MECK_WEB_READER
|
||||
|| ui_task.isOnWebReader()
|
||||
#endif
|
||||
) {
|
||||
ui_task.injectKey('s'); // Pass directly for scrolling
|
||||
} else {
|
||||
Serial.println("Opening settings");
|
||||
ui_task.gotoSettingsScreen();
|
||||
@@ -1285,8 +1420,12 @@ void handleKeyboardInput() {
|
||||
|
||||
case 'w':
|
||||
// Navigate up/previous (scroll on channel screen)
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()) {
|
||||
ui_task.injectKey('w'); // Pass directly for channel/contacts switching
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()
|
||||
#ifdef MECK_WEB_READER
|
||||
|| ui_task.isOnWebReader()
|
||||
#endif
|
||||
) {
|
||||
ui_task.injectKey('w'); // Pass directly for scrolling
|
||||
} else {
|
||||
Serial.println("Nav: Previous");
|
||||
ui_task.injectKey(0xF2); // KEY_PREV
|
||||
@@ -1371,6 +1510,17 @@ void handleKeyboardInput() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
#ifdef MECK_WEB_READER
|
||||
// If web reader is in reading/link/wifi mode, inject q for internal navigation
|
||||
// (reading→home, wifi→home). Only exit to firmware home if already on web home.
|
||||
if (ui_task.isOnWebReader()) {
|
||||
WebReaderScreen* wr = (WebReaderScreen*)ui_task.getWebReaderScreen();
|
||||
if (wr && !wr->isHome()) {
|
||||
ui_task.injectKey('q');
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Go back to home screen (admin mode handled above)
|
||||
Serial.println("Nav: Back to home");
|
||||
ui_task.gotoHomeScreen();
|
||||
@@ -1395,6 +1545,13 @@ void handleKeyboardInput() {
|
||||
break;
|
||||
|
||||
default:
|
||||
#ifdef MECK_WEB_READER
|
||||
// Pass unhandled keys to web reader (l=link, g=go, k=bookmark, 0-9=link#)
|
||||
if (ui_task.isOnWebReader()) {
|
||||
ui_task.injectKey(key);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
Serial.printf("Unhandled key in normal mode: '%c' (0x%02X)\n", key, key);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -340,6 +340,10 @@ public:
|
||||
display.drawTextCentered(display.width() / 2, y, "[E] Reader [P] Audiobooks");
|
||||
#else
|
||||
display.drawTextCentered(display.width() / 2, y, "[E] Reader ");
|
||||
#endif
|
||||
#ifdef MECK_WEB_READER
|
||||
y += 10;
|
||||
display.drawTextCentered(display.width() / 2, y, "[B] Browser ");
|
||||
#endif
|
||||
y += 14;
|
||||
|
||||
@@ -1446,6 +1450,28 @@ void UITask::gotoRepeaterAdmin(int contactIdx) {
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
#ifdef MECK_WEB_READER
|
||||
void UITask::gotoWebReader() {
|
||||
// Lazy-initialize on first use (same pattern as audiobook player)
|
||||
if (web_reader == nullptr) {
|
||||
Serial.printf("WebReader: lazy init - free heap: %d, largest block: %d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
web_reader = new WebReaderScreen(this);
|
||||
Serial.printf("WebReader: init complete - free heap: %d\n", ESP.getFreeHeap());
|
||||
}
|
||||
WebReaderScreen* wr = (WebReaderScreen*)web_reader;
|
||||
if (_display != NULL) {
|
||||
wr->enter(*_display);
|
||||
}
|
||||
setCurrScreen(web_reader);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
#endif
|
||||
|
||||
void UITask::onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) {
|
||||
if (repeater_admin && isOnRepeaterAdmin()) {
|
||||
((RepeaterAdminScreen*)repeater_admin)->onLoginResult(success, permissions, server_time);
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
#include "SMSScreen.h"
|
||||
#endif
|
||||
|
||||
#ifdef MECK_WEB_READER
|
||||
#include "WebReaderScreen.h"
|
||||
#endif
|
||||
|
||||
class UITask : public AbstractUITask {
|
||||
DisplayDriver* _display;
|
||||
SensorManager* _sensors;
|
||||
@@ -66,6 +70,9 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* sms_screen; // SMS messaging screen (4G variant only)
|
||||
#endif
|
||||
UIScreen* repeater_admin; // Repeater admin screen
|
||||
#ifdef MECK_WEB_READER
|
||||
UIScreen* web_reader; // Web reader screen (lazy-init, WiFi required)
|
||||
#endif
|
||||
UIScreen* curr;
|
||||
|
||||
void userLedHandler();
|
||||
@@ -97,6 +104,9 @@ public:
|
||||
void gotoOnboarding(); // Navigate to settings in onboarding mode
|
||||
void gotoAudiobookPlayer(); // Navigate to audiobook player
|
||||
void gotoRepeaterAdmin(int contactIdx); // Navigate to repeater admin
|
||||
#ifdef MECK_WEB_READER
|
||||
void gotoWebReader(); // Navigate to web reader (browser)
|
||||
#endif
|
||||
#ifdef HAS_4G_MODEM
|
||||
void gotoSMSScreen();
|
||||
bool isOnSMSScreen() const { return curr == sms_screen; }
|
||||
@@ -114,6 +124,9 @@ public:
|
||||
bool isOnSettingsScreen() const { return curr == settings_screen; }
|
||||
bool isOnAudiobookPlayer() const { return curr == audiobook_screen; }
|
||||
bool isOnRepeaterAdmin() const { return curr == repeater_admin; }
|
||||
#ifdef MECK_WEB_READER
|
||||
bool isOnWebReader() const { return curr == web_reader; }
|
||||
#endif
|
||||
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
// Check if audio is playing/paused in the background (for status indicators)
|
||||
@@ -150,6 +163,9 @@ public:
|
||||
UIScreen* getAudiobookScreen() const { return audiobook_screen; }
|
||||
void setAudiobookScreen(UIScreen* s) { audiobook_screen = s; }
|
||||
UIScreen* getRepeaterAdminScreen() const { return repeater_admin; }
|
||||
#ifdef MECK_WEB_READER
|
||||
UIScreen* getWebReaderScreen() const { return web_reader; }
|
||||
#endif
|
||||
|
||||
// from AbstractUITask
|
||||
void msgRead(int msgcount) override;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
// WebReaderDeps.cpp
|
||||
// -----------------------------------------------------------------------
|
||||
// PlatformIO library dependency finder (LDF) hint file.
|
||||
//
|
||||
// The web reader's WiFi/HTTP includes live in WebReaderScreen.h (header-only),
|
||||
// but PlatformIO's LDF can't always trace framework library dependencies
|
||||
// through conditional #include chains in headers. This .cpp file exposes
|
||||
// the includes at the top level where the scanner reliably finds them.
|
||||
//
|
||||
// No actual code here — just #include directives for the dependency finder.
|
||||
// -----------------------------------------------------------------------
|
||||
#ifdef MECK_WEB_READER
|
||||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#endif
|
||||
Reference in New Issue
Block a user