7 Commits

13 changed files with 896 additions and 27 deletions

View File

@@ -274,12 +274,20 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
if (file.read((uint8_t *)&_prefs.large_font, sizeof(_prefs.large_font)) != sizeof(_prefs.large_font)) {
_prefs.large_font = 0; // default: tiny font
}
if (file.read((uint8_t *)&_prefs.tx_fail_reset_threshold, sizeof(_prefs.tx_fail_reset_threshold)) != sizeof(_prefs.tx_fail_reset_threshold)) {
_prefs.tx_fail_reset_threshold = 3; // default: 3
}
if (file.read((uint8_t *)&_prefs.rx_fail_reboot_threshold, sizeof(_prefs.rx_fail_reboot_threshold)) != sizeof(_prefs.rx_fail_reboot_threshold)) {
_prefs.rx_fail_reboot_threshold = 3; // default: 3
}
// Clamp to valid ranges
if (_prefs.dark_mode > 1) _prefs.dark_mode = 0;
if (_prefs.portrait_mode > 1) _prefs.portrait_mode = 0;
if (_prefs.hint_shown > 1) _prefs.hint_shown = 0;
if (_prefs.large_font > 1) _prefs.large_font = 0;
if (_prefs.tx_fail_reset_threshold > 10) _prefs.tx_fail_reset_threshold = 3;
if (_prefs.rx_fail_reboot_threshold > 10) _prefs.rx_fail_reboot_threshold = 3;
// auto_lock_minutes: only accept known options (0, 2, 5, 10, 15, 30)
{
uint8_t alm = _prefs.auto_lock_minutes;
@@ -334,6 +342,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.auto_lock_minutes, sizeof(_prefs.auto_lock_minutes)); // 100
file.write((uint8_t *)&_prefs.hint_shown, sizeof(_prefs.hint_shown)); // 101
file.write((uint8_t *)&_prefs.large_font, sizeof(_prefs.large_font)); // 102
file.write((uint8_t *)&_prefs.tx_fail_reset_threshold, sizeof(_prefs.tx_fail_reset_threshold)); // 103
file.write((uint8_t *)&_prefs.rx_fail_reboot_threshold, sizeof(_prefs.rx_fail_reboot_threshold)); // 104
file.close();
}

View File

@@ -264,6 +264,16 @@ int MyMesh::getInterferenceThreshold() const {
return _prefs.interference_threshold;
}
uint8_t MyMesh::getTxFailResetThreshold() const {
return _prefs.tx_fail_reset_threshold;
}
uint8_t MyMesh::getRxFailRebootThreshold() const {
return _prefs.rx_fail_reboot_threshold;
}
void MyMesh::onRxUnrecoverable() {
board.reboot();
}
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
@@ -2255,6 +2265,10 @@ void MyMesh::checkCLIRescueCmd() {
Serial.printf(" > %d\n", _prefs.multi_acks);
} else if (strcmp(key, "int.thresh") == 0) {
Serial.printf(" > %d\n", _prefs.interference_threshold);
} else if (strcmp(key, "tx.fail.threshold") == 0) {
Serial.printf(" > %d\n", _prefs.tx_fail_reset_threshold);
} else if (strcmp(key, "rx.fail.threshold") == 0) {
Serial.printf(" > %d\n", _prefs.rx_fail_reboot_threshold);
} else if (strcmp(key, "gps.baud") == 0) {
uint32_t effective = _prefs.gps_baudrate ? _prefs.gps_baudrate : GPS_BAUDRATE;
Serial.printf(" > %lu (effective: %lu)\n",
@@ -2315,6 +2329,8 @@ void MyMesh::checkCLIRescueCmd() {
Serial.printf(" af: %.1f\n", _prefs.airtime_factor);
Serial.printf(" multi.acks: %d\n", _prefs.multi_acks);
Serial.printf(" int.thresh: %d\n", _prefs.interference_threshold);
Serial.printf(" tx.fail: %d\n", _prefs.tx_fail_reset_threshold);
Serial.printf(" rx.fail: %d\n", _prefs.rx_fail_reboot_threshold);
{
uint32_t eff_baud = _prefs.gps_baudrate ? _prefs.gps_baudrate : GPS_BAUDRATE;
Serial.printf(" gps.baud: %lu\n", (unsigned long)eff_baud);
@@ -2710,6 +2726,30 @@ void MyMesh::checkCLIRescueCmd() {
Serial.println(" Error: use 0 (disabled) or 14+ (typical: 14)");
}
} else if (memcmp(config, "tx.fail.threshold ", 18) == 0) {
int val = atoi(&config[18]);
if (val < 0) val = 0;
if (val > 10) val = 10;
_prefs.tx_fail_reset_threshold = (uint8_t)val;
savePrefs();
if (val == 0) {
Serial.println(" > tx fail reset disabled");
} else {
Serial.printf(" > tx fail reset after %d failures\n", val);
}
} else if (memcmp(config, "rx.fail.threshold ", 18) == 0) {
int val = atoi(&config[18]);
if (val < 0) val = 0;
if (val > 10) val = 10;
_prefs.rx_fail_reboot_threshold = (uint8_t)val;
savePrefs();
if (val == 0) {
Serial.println(" > rx fail reboot disabled");
} else {
Serial.printf(" > reboot after %d rx recovery failures\n", val);
}
} else if (memcmp(config, "gps.baud ", 9) == 0) {
uint32_t val = (uint32_t)atol(&config[9]);
if (val == 0 || val == 4800 || val == 9600 || val == 19200 ||
@@ -2807,6 +2847,8 @@ void MyMesh::checkCLIRescueCmd() {
Serial.println(" af <0-9> Airtime factor");
Serial.println(" multi.acks <0|1> Redundant ACKs (default: 1)");
Serial.println(" int.thresh <0|14+> Interference threshold dB (0=off, 14=typical)");
Serial.println(" tx.fail.threshold <0-10> TX fail radio reset (0=off, default 3)");
Serial.println(" rx.fail.threshold <0-10> RX stuck reboot (0=off, default 3)");
Serial.println(" gps.baud <rate> GPS baud (0=default, reboot to apply)");
Serial.println("");
Serial.println(" Clock:");

View File

@@ -8,11 +8,11 @@
#define FIRMWARE_VER_CODE 10
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "26 March 2026"
#define FIRMWARE_BUILD_DATE "27 March 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v1.4"
#define FIRMWARE_VERSION "Meck v1.5"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -143,6 +143,9 @@ public:
protected:
float getAirtimeBudgetFactor() const override;
int getInterferenceThreshold() const override;
uint8_t getTxFailResetThreshold() const override;
uint8_t getRxFailRebootThreshold() const override;
void onRxUnrecoverable() override;
int calcRxDelay(float score, uint32_t air_time) const override;
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;

View File

@@ -40,6 +40,8 @@ struct NodePrefs { // persisted to file
uint8_t auto_lock_minutes; // 0=disabled, 2/5/10/15/30=auto-lock after idle
uint8_t hint_shown; // 0=show nav hint on boot, 1=already shown (dismiss permanently)
uint8_t large_font; // 0=tiny (built-in 6x8), 1=larger (FreeSans9pt) — T-Deck Pro only
uint8_t tx_fail_reset_threshold; // 0=disabled, 1-10, default 3
uint8_t rx_fail_reboot_threshold; // 0=disabled, 1-10, default 3
// --- Font helpers (inline, no overhead) ---
// Returns the DisplayDriver text-size index for "small/body" text.

View File

@@ -489,7 +489,7 @@
static int16_t touchLastX = 0;
static int16_t touchLastY = 0;
static unsigned long lastTouchSeenMs = 0;
#define TOUCH_LONG_PRESS_MS 500
#define TOUCH_LONG_PRESS_MS 750
#if defined(LilyGo_T5S3_EPaper_Pro)
#define TOUCH_SWIPE_THRESHOLD 60 // T5S3: 960×540 — 60px ≈ 6% of width
#else
@@ -931,6 +931,12 @@ static void lastHeardToggleContact() {
return KEY_ENTER; // Editing mode or header/footer tap
}
// SMS screen: dedicated dialer/touch handler runs separately (HAS_4G_MODEM block)
// Return 0 so the general handler doesn't inject spurious keys
#ifdef HAS_4G_MODEM
if (ui_task.isOnSMSScreen()) return 0;
#endif
// All other screens: tap = select
return KEY_ENTER;
}
@@ -939,6 +945,11 @@ static void lastHeardToggleContact() {
static char mapTouchSwipe(int16_t dx, int16_t dy) {
bool horizontal = abs(dx) > abs(dy);
// SMS screen: dedicated touch handler covers all interaction
#ifdef HAS_4G_MODEM
if (ui_task.isOnSMSScreen()) return 0;
#endif
// Reader (reading mode): swipe left/right for page turn
if (ui_task.isOnTextReader()) {
TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen();
@@ -1002,6 +1013,11 @@ static void lastHeardToggleContact() {
// Map a long press to a key
static char mapTouchLongPress(int16_t x, int16_t y) {
// SMS screen: dedicated touch handler covers all interaction
#ifdef HAS_4G_MODEM
if (ui_task.isOnSMSScreen()) return 0;
#endif
// Home screen: long press = activate current page action
// (BLE toggle, send advert, hibernate, GPS toggle, etc.)
if (ui_task.isOnHomeScreen()) {
@@ -1819,7 +1835,7 @@ void loop() {
the_mesh.loop();
#ifdef MECK_OTA_UPDATE
} else {
// OTA active — poll the web server from the main loop for fast response.
// OTA/File Manager active — poll the web server from the main loop for fast response.
// The render cycle on T5S3 (960×540 FastEPD) can block for 500ms+ during
// e-ink refresh, causing the browser to timeout before handleClient() runs.
// Polling here gives us ~1-5ms response time instead.
@@ -2178,7 +2194,7 @@ void loop() {
// Gestures:
// Tap = finger down + up with minimal movement → select/open
// Swipe = finger drag > threshold → scroll/page turn
// Long press = finger held > 500ms without moving → edit/enter
// Long press = finger held > 750ms without moving → edit/enter
// After processing an event, cooldown waits for finger lift before next event.
// Touch is disabled while lock screen is active.
// When virtual keyboard is active (T5S3), taps route to keyboard.
@@ -2190,6 +2206,15 @@ void loop() {
#if defined(LilyGo_T5S3_EPaper_Pro)
touchBlocked = touchBlocked || ui_task.isVKBActive();
#endif
#ifdef HAS_4G_MODEM
// SMS dialer has its own dedicated touch handler — don't consume touch data here
if (smsMode) {
SMSScreen* smsScr = (SMSScreen*)ui_task.getSMSScreen();
if (smsScr && smsScr->getSubView() == SMSScreen::PHONE_DIALER) {
touchBlocked = true;
}
}
#endif
if (!touchBlocked)
{

View File

@@ -28,6 +28,7 @@
#include <SD.h>
#endif
#include <WebServer.h>
#include <DNSServer.h>
#include <Update.h>
#include <esp_ota_ops.h>
#endif
@@ -143,7 +144,9 @@ enum SettingsRowType : uint8_t {
ROW_ADD_CHANNEL, // "+ Add Hashtag Channel"
ROW_INFO_HEADER, // "--- Info ---" separator
#ifdef MECK_OTA_UPDATE
ROW_OTA_TOOLS_SUBMENU, // Folder row → enters OTA Tools sub-screen
ROW_FW_UPDATE, // "Firmware Update" — WiFi upload + flash
ROW_SD_FILE_MGR, // "SD File Manager" — WiFi file browser
#endif
ROW_PUB_KEY, // Public key display
ROW_FIRMWARE, // Firmware version
@@ -168,6 +171,7 @@ enum EditMode : uint8_t {
#endif
#ifdef MECK_OTA_UPDATE
EDIT_OTA, // OTA firmware update flow (multi-phase overlay)
EDIT_FILEMGR, // SD file manager flow (WiFi file browser)
#endif
};
@@ -178,6 +182,9 @@ enum SubScreen : uint8_t {
SUB_NONE, // Top-level settings list
SUB_CONTACTS, // Contacts settings sub-screen
SUB_CHANNELS, // Channels management sub-screen
#ifdef MECK_OTA_UPDATE
SUB_OTA_TOOLS, // OTA Tools sub-screen (FW update + File Manager)
#endif
};
#ifdef MECK_OTA_UPDATE
@@ -192,6 +199,13 @@ enum OtaPhase : uint8_t {
OTA_PHASE_DONE, // Success, rebooting
OTA_PHASE_ERROR, // Error with message
};
// File manager phases
enum FmPhase : uint8_t {
FM_PHASE_CONFIRM, // "Start SD file manager? Enter:Yes Q:No"
FM_PHASE_WAITING, // AP up, file browser active
FM_PHASE_ERROR, // Error with message
};
#endif
// Max rows in the settings list (increased for contact sub-toggles + WiFi)
@@ -281,6 +295,10 @@ private:
bool _otaUploadOk;
char _otaApName[24];
const char* _otaError;
// File manager state
FmPhase _fmPhase;
const char* _fmError;
DNSServer* _dnsServer;
#endif
// ---------------------------------------------------------------------------
@@ -362,6 +380,12 @@ private:
}
}
addRow(ROW_ADD_CHANNEL);
#ifdef MECK_OTA_UPDATE
} else if (_subScreen == SUB_OTA_TOOLS) {
// --- OTA Tools sub-screen ---
addRow(ROW_FW_UPDATE);
addRow(ROW_SD_FILE_MGR);
#endif
} else {
// --- Top-level settings list ---
addRow(ROW_NAME);
@@ -394,12 +418,12 @@ private:
// Folder rows for sub-screens
addRow(ROW_CONTACTS_SUBMENU);
addRow(ROW_CHANNELS_SUBMENU);
#ifdef MECK_OTA_UPDATE
addRow(ROW_OTA_TOOLS_SUBMENU);
#endif
// Info section (stays at top level)
addRow(ROW_INFO_HEADER);
#ifdef MECK_OTA_UPDATE
addRow(ROW_FW_UPDATE);
#endif
addRow(ROW_PUB_KEY);
addRow(ROW_FIRMWARE);
@@ -556,6 +580,9 @@ public:
_otaBytesReceived = 0;
_otaUploadOk = false;
_otaError = nullptr;
_fmPhase = FM_PHASE_CONFIRM;
_fmError = nullptr;
_dnsServer = nullptr;
#endif
}
@@ -1003,10 +1030,18 @@ public:
return true;
}
// Called from render loop AND main loop to poll the web server
// Called from render loop AND main loop to poll the web server.
// Handles both OTA firmware upload and SD file manager modes.
void pollOTAServer() {
if (_otaServer && (_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) {
_otaServer->handleClient();
if (_otaServer) {
if ((_editMode == EDIT_OTA && (_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) ||
(_editMode == EDIT_FILEMGR && _fmPhase == FM_PHASE_WAITING)) {
_otaServer->handleClient();
}
}
// Process DNS for captive portal redirect (file manager only)
if (_dnsServer && _editMode == EDIT_FILEMGR && _fmPhase == FM_PHASE_WAITING) {
_dnsServer->processNextRequest();
}
}
@@ -1073,6 +1108,443 @@ public:
ESP.restart();
}
// ---------------------------------------------------------------------------
// SD File Manager — WiFi file browser, upload, download, delete
// ---------------------------------------------------------------------------
void startFileMgr() {
_editMode = EDIT_FILEMGR;
_fmPhase = FM_PHASE_CONFIRM;
_fmError = nullptr;
}
void startFileMgrServer() {
// Build AP name with last 4 of MAC for uniqueness
uint8_t mac[6];
WiFi.macAddress(mac);
snprintf(_otaApName, sizeof(_otaApName), "Meck-Files-%02X%02X", mac[4], mac[5]);
// Pause LoRa radio — SD and LoRa share the same SPI bus on both
// platforms. Incoming packets during SD writes cause bus contention.
extern void otaPauseRadio();
otaPauseRadio();
// Clean WiFi init from any state
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
delay(200);
WiFi.mode(WIFI_AP);
WiFi.softAP(_otaApName);
delay(500);
Serial.printf("FM: AP '%s' started, IP: %s\n",
_otaApName, WiFi.softAPIP().toString().c_str());
// Start DNS server — redirect ALL DNS lookups to our AP IP.
// This triggers captive portal detection on phones, which opens the
// page in a real browser instead of the restricted captive webview.
if (_dnsServer) { delete _dnsServer; }
_dnsServer = new DNSServer();
_dnsServer->start(53, "*", WiFi.softAPIP());
Serial.println("FM: DNS captive portal started");
// Start web server
if (_otaServer) { _otaServer->stop(); delete _otaServer; }
_otaServer = new WebServer(80);
// --- Captive portal detection handlers ---
// Phones/OS probe these URLs to detect captive portals. Redirecting
// them to our page causes the OS to open a real browser.
// iOS / macOS
_otaServer->on("/hotspot-detect.html", HTTP_GET, [this]() {
Serial.println("FM: captive probe (Apple)");
_otaServer->sendHeader("Location", "http://192.168.4.1/");
_otaServer->send(302, "text/plain", "");
});
// Android
_otaServer->on("/generate_204", HTTP_GET, [this]() {
Serial.println("FM: captive probe (Android)");
_otaServer->sendHeader("Location", "http://192.168.4.1/");
_otaServer->send(302, "text/plain", "");
});
_otaServer->on("/gen_204", HTTP_GET, [this]() {
_otaServer->sendHeader("Location", "http://192.168.4.1/");
_otaServer->send(302, "text/plain", "");
});
// Windows
_otaServer->on("/connecttest.txt", HTTP_GET, [this]() {
_otaServer->sendHeader("Location", "http://192.168.4.1/");
_otaServer->send(302, "text/plain", "");
});
_otaServer->on("/redirect", HTTP_GET, [this]() {
_otaServer->sendHeader("Location", "http://192.168.4.1/");
_otaServer->send(302, "text/plain", "");
});
// Firefox
_otaServer->on("/canonical.html", HTTP_GET, [this]() {
_otaServer->sendHeader("Location", "http://192.168.4.1/");
_otaServer->send(302, "text/plain", "");
});
_otaServer->on("/success.txt", HTTP_GET, [this]() {
_otaServer->send(200, "text/plain", "success");
});
// --- Main page: server-rendered directory listing (no JS needed) ---
_otaServer->on("/", HTTP_GET, [this]() {
String path = _otaServer->arg("path");
if (path.isEmpty()) path = "/";
String msg = _otaServer->arg("msg");
Serial.printf("FM: page request path='%s'\n", path.c_str());
String html = fmBuildPage(path, msg);
_otaServer->send(200, "text/html", html);
});
// --- File download: GET /dl?path=/file.txt ---
_otaServer->on("/dl", HTTP_GET, [this]() {
String path = _otaServer->arg("path");
File f = SD.open(path, FILE_READ);
if (!f || f.isDirectory()) {
if (f) f.close();
digitalWrite(SDCARD_CS, HIGH);
_otaServer->send(404, "text/plain", "Not found");
return;
}
String name = path;
int lastSlash = name.lastIndexOf('/');
if (lastSlash >= 0) name = name.substring(lastSlash + 1);
_otaServer->sendHeader("Content-Disposition",
"attachment; filename=\"" + name + "\"");
size_t fileSize = f.size();
_otaServer->setContentLength(fileSize);
_otaServer->send(200, "application/octet-stream", "");
uint8_t* buf = (uint8_t*)ps_malloc(4096);
if (!buf) buf = (uint8_t*)malloc(4096);
if (buf) {
while (f.available()) {
int n = f.read(buf, 4096);
if (n > 0) _otaServer->sendContent((const char*)buf, n);
}
free(buf);
}
f.close();
digitalWrite(SDCARD_CS, HIGH);
});
// --- File upload: POST /upload?dir=/ → redirect back to listing ---
_otaServer->on("/upload", HTTP_POST,
[this]() {
String dir = _otaServer->arg("dir");
if (dir.isEmpty()) dir = "/";
_otaServer->sendHeader("Location", "/?path=" + dir + "&msg=Upload+complete");
_otaServer->send(303, "text/plain", "Redirecting...");
},
[this]() {
HTTPUpload& upload = _otaServer->upload();
static File fmUploadFile;
if (upload.status == UPLOAD_FILE_START) {
String dir = _otaServer->arg("dir");
if (dir.isEmpty()) dir = "/";
if (!dir.endsWith("/")) dir += "/";
String fullPath = dir + upload.filename;
Serial.printf("FM: Upload start: %s\n", fullPath.c_str());
fmUploadFile = SD.open(fullPath, FILE_WRITE);
if (!fmUploadFile) Serial.println("FM: Failed to open file for write");
} else if (upload.status == UPLOAD_FILE_WRITE) {
if (fmUploadFile) fmUploadFile.write(upload.buf, upload.currentSize);
} else if (upload.status == UPLOAD_FILE_END) {
if (fmUploadFile) {
fmUploadFile.close();
digitalWrite(SDCARD_CS, HIGH);
Serial.printf("FM: Upload done: %s (%d bytes)\n",
upload.filename.c_str(), upload.totalSize);
}
} else if (upload.status == UPLOAD_FILE_ABORTED) {
if (fmUploadFile) fmUploadFile.close();
digitalWrite(SDCARD_CS, HIGH);
Serial.println("FM: Upload aborted");
}
}
);
// --- Create directory: GET /mkdir?name=xxx&dir=/path ---
_otaServer->on("/mkdir", HTTP_GET, [this]() {
String dir = _otaServer->arg("dir");
String name = _otaServer->arg("name");
if (dir.isEmpty()) dir = "/";
if (name.isEmpty()) {
_otaServer->sendHeader("Location", "/?path=" + dir + "&msg=No+name");
_otaServer->send(303);
return;
}
String full = dir + (dir.endsWith("/") ? "" : "/") + name;
bool ok = SD.mkdir(full);
digitalWrite(SDCARD_CS, HIGH);
Serial.printf("FM: mkdir '%s' %s\n", full.c_str(), ok ? "OK" : "FAIL");
_otaServer->sendHeader("Location",
"/?path=" + dir + "&msg=" + (ok ? "Folder+created" : "mkdir+failed"));
_otaServer->send(303);
});
// --- Delete file/folder: GET /rm?path=/file&ret=/parent ---
_otaServer->on("/rm", HTTP_GET, [this]() {
String path = _otaServer->arg("path");
String ret = _otaServer->arg("ret");
if (ret.isEmpty()) ret = "/";
if (path.isEmpty() || path == "/") {
_otaServer->sendHeader("Location", "/?path=" + ret + "&msg=Bad+path");
_otaServer->send(303);
return;
}
File f = SD.open(path);
bool ok = false;
if (f) {
bool isDir = f.isDirectory();
f.close();
ok = isDir ? SD.rmdir(path) : SD.remove(path);
}
digitalWrite(SDCARD_CS, HIGH);
Serial.printf("FM: rm '%s' %s\n", path.c_str(), ok ? "OK" : "FAIL");
_otaServer->sendHeader("Location",
"/?path=" + ret + "&msg=" + (ok ? "Deleted" : "Delete+failed"));
_otaServer->send(303);
});
// --- Confirm delete page: GET /confirm-rm?path=/file&ret=/parent ---
_otaServer->on("/confirm-rm", HTTP_GET, [this]() {
String path = _otaServer->arg("path");
String ret = _otaServer->arg("ret");
if (ret.isEmpty()) ret = "/";
String name = path;
int sl = name.lastIndexOf('/');
if (sl >= 0) name = name.substring(sl + 1);
String html = "<!DOCTYPE html><html><head>"
"<meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>Confirm Delete</title>"
"<style>"
"body{font-family:-apple-system,sans-serif;max-width:480px;margin:40px auto;"
"padding:0 20px;background:#1a1a2e;color:#e0e0e0;text-align:center}"
".b{display:inline-block;padding:10px 24px;border-radius:6px;text-decoration:none;"
"font-weight:bold;margin:8px;font-size:1em}"
".br{background:#e74c3c;color:#fff}.bg{background:#4ecca3;color:#1a1a2e}"
"</style></head><body>"
"<h2 style='color:#e74c3c'>Delete?</h2>"
"<p style='font-size:1.1em'>" + fmHtmlEscape(name) + "</p>"
"<a class='b br' href='/rm?path=" + fmUrlEncode(path) + "&ret=" + fmUrlEncode(ret) + "'>Delete</a>"
"<a class='b bg' href='/?path=" + fmUrlEncode(ret) + "'>Cancel</a>"
"</body></html>";
_otaServer->send(200, "text/html", html);
});
// Catch-all: redirect unknown URLs to file manager (catches captive portal probes)
_otaServer->onNotFound([this]() {
Serial.printf("FM: redirect %s -> /\n", _otaServer->uri().c_str());
_otaServer->sendHeader("Location", "http://192.168.4.1/");
_otaServer->send(302, "text/plain", "");
});
_otaServer->begin();
Serial.println("FM: Web server started on port 80");
_fmPhase = FM_PHASE_WAITING;
}
void stopFileMgr() {
if (_otaServer) { _otaServer->stop(); delete _otaServer; _otaServer = nullptr; }
if (_dnsServer) { _dnsServer->stop(); delete _dnsServer; _dnsServer = nullptr; }
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_OFF);
delay(100);
_editMode = EDIT_NONE;
extern void otaResumeRadio();
otaResumeRadio();
#ifdef MECK_WIFI_COMPANION
WiFi.mode(WIFI_STA);
wifiReconnectSaved();
#endif
Serial.println("FM: Stopped, AP down, radio resumed");
}
// --- Helpers for server-rendered HTML ---
static String fmHtmlEscape(const String& s) {
String r;
r.reserve(s.length());
for (unsigned int i = 0; i < s.length(); i++) {
char c = s[i];
if (c == '&') r += "&amp;";
else if (c == '<') r += "&lt;";
else if (c == '>') r += "&gt;";
else if (c == '"') r += "&quot;";
else r += c;
}
return r;
}
static String fmUrlEncode(const String& s) {
String r;
for (unsigned int i = 0; i < s.length(); i++) {
char c = s[i];
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '/' || c == '~') {
r += c;
} else {
char hex[4];
snprintf(hex, sizeof(hex), "%%%02X", (uint8_t)c);
r += hex;
}
}
return r;
}
static String fmFormatSize(size_t bytes) {
if (bytes < 1024) return String(bytes) + " B";
if (bytes < 1048576) return String(bytes / 1024) + " KB";
return String(bytes / 1048576) + "." + String((bytes % 1048576) * 10 / 1048576) + " MB";
}
// Build the complete HTML page with inline directory listing
String fmBuildPage(const String& path, const String& msg) {
String html;
html.reserve(4096);
// --- Head + CSS ---
html += "<!DOCTYPE html><html><head>"
"<meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>Meck SD Files</title>"
"<style>"
"body{font-family:-apple-system,sans-serif;max-width:600px;margin:20px auto;"
"padding:0 16px;background:#1a1a2e;color:#e0e0e0}"
"h1{color:#4ecca3;font-size:1.3em;margin:8px 0}"
".pa{background:#16213e;padding:8px 12px;border-radius:6px;margin:8px 0;"
"font-family:monospace;font-size:0.9em;word-break:break-all}"
".tb{display:flex;gap:6px;margin:8px 0;flex-wrap:wrap}"
".b{background:#4ecca3;color:#1a1a2e;border:none;padding:7px 14px;"
"border-radius:5px;font-size:0.85em;font-weight:bold;cursor:pointer;"
"text-decoration:none;display:inline-block}"
".b:active{background:#3ba88f}"
".br{background:#e74c3c;color:#fff;padding:3px 8px;font-size:0.75em}.br:active{background:#c0392b}"
".it{display:flex;align-items:center;padding:8px 4px;border-bottom:1px solid #16213e;gap:6px}"
".ic{font-size:1.1em;width:22px;text-align:center}"
".nm{flex:1;word-break:break-all;color:#e0e0e0;text-decoration:none}"
".nm:hover{color:#4ecca3}"
".sz{color:#888;font-size:0.8em;min-width:54px;text-align:right;margin-right:4px}"
".up{background:#16213e;border:2px dashed #4ecca3;border-radius:8px;"
"padding:14px;margin:10px 0;text-align:center}"
".em{color:#888;text-align:center;padding:20px}"
".ms{background:#16213e;padding:8px 12px;border-radius:6px;margin:8px 0;"
"border-left:3px solid #4ecca3;font-size:0.9em}"
"</style></head><body>";
// --- Title + path ---
html += "<h1>Meck SD File Manager</h1>";
html += "<div class='pa'>" + fmHtmlEscape(path) + "</div>";
// --- Status message (from redirects) ---
if (msg.length() > 0) {
html += "<div class='ms'>" + fmHtmlEscape(msg) + "</div>";
}
// --- Navigation buttons ---
html += "<div class='tb'>";
if (path != "/") {
// Compute parent
String parent = path;
if (parent.endsWith("/")) parent = parent.substring(0, parent.length() - 1);
int sl = parent.lastIndexOf('/');
parent = (sl <= 0) ? "/" : parent.substring(0, sl);
html += "<a class='b' href='/?path=" + fmUrlEncode(parent) + "'>.. Up</a>";
}
html += "<a class='b' href='/?path=" + fmUrlEncode(path) + "'>Refresh</a>";
html += "</div>";
// --- Directory listing (server-rendered) ---
File dir = SD.open(path, FILE_READ);
if (!dir || !dir.isDirectory()) {
if (dir) dir.close();
digitalWrite(SDCARD_CS, HIGH);
html += "<div class='em'>Cannot open directory</div>";
} else {
// Collect entries into arrays for sorting (dirs first, then alpha)
struct FmEntry { String name; size_t size; bool isDir; };
FmEntry entries[128]; // max entries to display
int count = 0;
File entry = dir.openNextFile();
while (entry && count < 128) {
const char* fullName = entry.name();
const char* baseName = strrchr(fullName, '/');
baseName = baseName ? baseName + 1 : fullName;
entries[count].name = baseName;
entries[count].size = entry.size();
entries[count].isDir = entry.isDirectory();
count++;
entry.close();
entry = dir.openNextFile();
}
dir.close();
digitalWrite(SDCARD_CS, HIGH);
Serial.printf("FM: listing %d entries for '%s'\n", count, path.c_str());
// Sort: dirs first, then alphabetical
for (int i = 0; i < count - 1; i++) {
for (int j = i + 1; j < count; j++) {
bool swap = false;
if (entries[i].isDir != entries[j].isDir) {
swap = !entries[i].isDir && entries[j].isDir;
} else {
swap = entries[i].name.compareTo(entries[j].name) > 0;
}
if (swap) {
FmEntry tmp = entries[i];
entries[i] = entries[j];
entries[j] = tmp;
}
}
}
if (count == 0) {
html += "<div class='em'>Empty folder</div>";
} else {
for (int i = 0; i < count; i++) {
String fp = path + (path.endsWith("/") ? "" : "/") + entries[i].name;
html += "<div class='it'>";
html += "<span class='ic'>" + String(entries[i].isDir ? "\xF0\x9F\x93\x81" : "\xF0\x9F\x93\x84") + "</span>";
if (entries[i].isDir) {
html += "<a class='nm' href='/?path=" + fmUrlEncode(fp) + "'>" + fmHtmlEscape(entries[i].name) + "</a>";
} else {
html += "<a class='nm' href='/dl?path=" + fmUrlEncode(fp) + "'>" + fmHtmlEscape(entries[i].name) + "</a>";
html += "<span class='sz'>" + fmFormatSize(entries[i].size) + "</span>";
}
html += "<a class='b br' href='/confirm-rm?path=" + fmUrlEncode(fp) + "&ret=" + fmUrlEncode(path) + "'>Del</a>";
html += "</div>";
}
}
}
// --- Upload form (standard HTML form, no JS needed) ---
html += "<div class='up'>"
"<form method='POST' action='/upload?dir=" + fmUrlEncode(path) + "' enctype='multipart/form-data'>"
"<p>Select files to upload</p>"
"<input type='file' name='file' multiple><br><br>"
"<button class='b' type='submit'>Upload</button>"
"</form></div>";
// --- New folder (tiny inline form) ---
html += "<form action='/mkdir' method='GET' style='margin:8px 0;display:flex;gap:6px'>"
"<input type='hidden' name='dir' value='" + fmHtmlEscape(path) + "'>"
"<input type='text' name='name' placeholder='New folder name' "
"style='flex:1;padding:7px;border-radius:5px;border:1px solid #4ecca3;"
"background:#16213e;color:#e0e0e0'>"
"<button class='b' type='submit'>Create</button>"
"</form>";
html += "</body></html>";
return html;
}
#endif
// ---------------------------------------------------------------------------
@@ -1121,6 +1593,10 @@ public:
display.print("Settings > Contacts");
} else if (_subScreen == SUB_CHANNELS) {
display.print("Settings > Channels");
#ifdef MECK_OTA_UPDATE
} else if (_subScreen == SUB_OTA_TOOLS) {
display.print("Settings > OTA Tools");
#endif
} else {
display.print("Settings");
}
@@ -1446,9 +1922,18 @@ public:
break;
#ifdef MECK_OTA_UPDATE
case ROW_OTA_TOOLS_SUBMENU:
display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN);
display.print("OTA Tools >>");
break;
case ROW_FW_UPDATE:
display.print("Firmware Update");
break;
case ROW_SD_FILE_MGR:
display.print("SD File Manager");
break;
#endif
case ROW_INFO_HEADER:
@@ -1725,6 +2210,75 @@ public:
display.setTextSize(1);
}
// === File Manager overlay ===
if (_editMode == EDIT_FILEMGR) {
int bx = 2, by = 14, bw = display.width() - 4;
int bh = display.height() - 28;
display.setColor(DisplayDriver::DARK);
display.fillRect(bx, by, bw, bh);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(bx, by, bw, bh);
display.setTextSize(_prefs->smallTextSize());
int oy = by + 4;
if (_fmPhase == FM_PHASE_CONFIRM) {
display.drawTextCentered(display.width() / 2, oy, "SD File Manager");
oy += 14;
display.setCursor(bx + 4, oy);
display.print("Start WiFi file server?");
oy += 10;
display.setCursor(bx + 4, oy);
display.print("Upload and download files");
oy += 8;
display.setCursor(bx + 4, oy);
display.print("on SD card via browser.");
oy += 10;
display.setCursor(bx + 4, oy);
display.setColor(DisplayDriver::YELLOW);
display.print("LoRa paused while active.");
display.setColor(DisplayDriver::LIGHT);
} else if (_fmPhase == FM_PHASE_WAITING) {
display.drawTextCentered(display.width() / 2, oy, "SD File Manager");
oy += 14;
display.setCursor(bx + 4, oy);
display.print("Connect to WiFi network:");
oy += 10;
display.setColor(DisplayDriver::GREEN);
display.setCursor(bx + 4, oy);
display.print(_otaApName);
display.setColor(DisplayDriver::LIGHT);
oy += 12;
display.setCursor(bx + 4, oy);
display.print("Then open browser:");
oy += 10;
display.setColor(DisplayDriver::GREEN);
display.setCursor(bx + 4, oy);
char ipBuf[32];
snprintf(ipBuf, sizeof(ipBuf), "http://%s", WiFi.softAPIP().toString().c_str());
display.print(ipBuf);
display.setColor(DisplayDriver::LIGHT);
oy += 12;
display.setCursor(bx + 4, oy);
display.print("File server active...");
pollOTAServer();
} else if (_fmPhase == FM_PHASE_ERROR) {
display.setColor(DisplayDriver::YELLOW);
display.drawTextCentered(display.width() / 2, oy, "File Manager Error");
display.setColor(DisplayDriver::LIGHT);
oy += 14;
if (_fmError) {
display.setCursor(bx + 4, oy);
display.print(_fmError);
}
}
display.setTextSize(1);
}
#endif
// === Footer ===
@@ -1737,7 +2291,12 @@ public:
if (_editMode == EDIT_NONE) {
if (_subScreen != SUB_NONE) {
display.print("Boot:Back");
const char* r = (_subScreen == SUB_CHANNELS) ? "Tap:Select Hold:Del" : "Tap:Toggle Hold:Edit";
const char* r;
if (_subScreen == SUB_CHANNELS) r = "Tap:Select Hold:Del";
#ifdef MECK_OTA_UPDATE
else if (_subScreen == SUB_OTA_TOOLS) r = "Tap:Select";
#endif
else r = "Tap:Toggle Hold:Edit";
display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY);
display.print(r);
} else {
@@ -1786,6 +2345,19 @@ public:
} else {
display.print("Please wait...");
}
} else if (_editMode == EDIT_FILEMGR) {
if (_fmPhase == FM_PHASE_CONFIRM) {
display.print("Boot:Cancel");
const char* r = "Tap:Start";
display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY);
display.print(r);
} else if (_fmPhase == FM_PHASE_WAITING) {
display.print("Boot:Stop");
} else if (_fmPhase == FM_PHASE_ERROR) {
display.print("Boot:Back");
} else {
display.print("Please wait...");
}
#endif
} else if (_editMode == EDIT_TEXT) {
display.print("Hold:Type");
@@ -1823,6 +2395,16 @@ public:
} else {
display.print("Please wait...");
}
} else if (_editMode == EDIT_FILEMGR) {
if (_fmPhase == FM_PHASE_CONFIRM) {
display.print("Enter:Start Q:Cancel");
} else if (_fmPhase == FM_PHASE_WAITING) {
display.print("Q:Stop");
} else if (_fmPhase == FM_PHASE_ERROR) {
display.print("Q:Back");
} else {
display.print("Please wait...");
}
#endif
} else if (_editMode == EDIT_PICKER) {
display.print("A/D:Choose Enter:Ok");
@@ -1843,9 +2425,10 @@ public:
#endif
#ifdef MECK_OTA_UPDATE
// Poll web server frequently during OTA waiting/receiving phases
if (_editMode == EDIT_OTA &&
(_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) {
// Poll web server frequently during OTA waiting/receiving or file manager phases
if ((_editMode == EDIT_OTA &&
(_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) ||
(_editMode == EDIT_FILEMGR && _fmPhase == FM_PHASE_WAITING)) {
return 200; // 200ms — fast enough for web server responsiveness
}
#endif
@@ -1912,6 +2495,32 @@ public:
// Consume all keys during OTA
return true;
}
// --- File Manager flow ---
if (_editMode == EDIT_FILEMGR) {
if (_fmPhase == FM_PHASE_CONFIRM) {
if (c == '\r' || c == 13) {
startFileMgrServer();
return true;
}
if (c == 'q' || c == 'Q') {
_editMode = EDIT_NONE;
return true;
}
} else if (_fmPhase == FM_PHASE_WAITING) {
if (c == 'q' || c == 'Q') {
stopFileMgr();
return true;
}
} else if (_fmPhase == FM_PHASE_ERROR) {
if (c == 'q' || c == 'Q') {
stopFileMgr();
return true;
}
}
// Consume all keys during file manager
return true;
}
#endif
#ifdef MECK_WIFI_COMPANION
@@ -2484,9 +3093,20 @@ public:
startEditText("");
break;
#ifdef MECK_OTA_UPDATE
case ROW_OTA_TOOLS_SUBMENU:
_savedTopCursor = _cursor;
_subScreen = SUB_OTA_TOOLS;
_cursor = 0;
_scrollTop = 0;
rebuildRows();
Serial.println("Settings: entered OTA Tools sub-screen");
break;
case ROW_FW_UPDATE:
startOTA();
break;
case ROW_SD_FILE_MGR:
startFileMgr();
break;
#endif
case ROW_CHANNEL:
case ROW_PUB_KEY:

View File

@@ -1403,6 +1403,107 @@ public:
// Called from setup() after SD card init. Scans files, pre-indexes first
// 100 pages of each, and shows progress on the e-ink display.
// Pre-index files inside one level of subdirectories so navigating
// into them later is instant (idx files already on SD).
void bootIndexSubfolders() {
// Work from the root-level _dirList that scanFiles() already populated.
// Copy it -- scanFiles() will overwrite _dirList when we scan each subfolder.
std::vector<String> subDirs = _dirList;
if (subDirs.empty()) return;
Serial.printf("TextReader: Pre-indexing %d subfolders\n", (int)subDirs.size());
int totalSubFiles = 0;
int cachedSubFiles = 0;
int indexedSubFiles = 0;
for (int d = 0; d < (int)subDirs.size(); d++) {
String subPath = String(BOOKS_FOLDER) + "/" + subDirs[d];
_currentPath = subPath;
scanFiles(); // populates _fileList for this subfolder
// Also pick up previously converted EPUB cache files for this subfolder
String epubCachePath = subPath + "/.epub_cache";
if (SD.exists(epubCachePath.c_str())) {
File cacheDir = SD.open(epubCachePath.c_str());
if (cacheDir && cacheDir.isDirectory()) {
File cf = cacheDir.openNextFile();
while (cf && _fileList.size() < READER_MAX_FILES) {
if (!cf.isDirectory()) {
String cname = String(cf.name());
int cslash = cname.lastIndexOf('/');
if (cslash >= 0) cname = cname.substring(cslash + 1);
if (cname.endsWith(".txt") || cname.endsWith(".TXT")) {
bool dup = false;
for (int k = 0; k < (int)_fileList.size(); k++) {
if (_fileList[k] == cname) { dup = true; break; }
}
if (!dup) _fileList.push_back(cname);
}
}
cf = cacheDir.openNextFile();
}
cacheDir.close();
}
}
for (int i = 0; i < (int)_fileList.size(); i++) {
totalSubFiles++;
// Try loading existing .idx cache -- if hit, skip
FileCache tempCache;
if (loadIndex(_fileList[i], tempCache)) {
cachedSubFiles++;
continue;
}
// Skip .epub files (converted on first open)
if (_fileList[i].endsWith(".epub") || _fileList[i].endsWith(".EPUB")) continue;
// Index this .txt file
String fullPath = _currentPath + "/" + _fileList[i];
File file = SD.open(fullPath.c_str(), FILE_READ);
if (!file) {
// Try epub cache fallback
String cacheFallback = epubCachePath + "/" + _fileList[i];
file = SD.open(cacheFallback.c_str(), FILE_READ);
}
if (!file) continue;
indexedSubFiles++;
String displayName = subDirs[d] + "/" + _fileList[i];
drawBootSplash(indexedSubFiles, 0, displayName);
FileCache cache;
cache.filename = _fileList[i];
cache.fileSize = file.size();
cache.fullyIndexed = false;
cache.lastReadPage = 0;
cache.pagePositions.clear();
cache.pagePositions.push_back(0);
indexPagesWordWrap(file, 0, cache.pagePositions,
_linesPerPage, _charsPerLine,
PREINDEX_PAGES - 1,
_textAreaHeight, _lineHeight);
cache.fullyIndexed = !file.available();
file.close();
saveIndex(cache.filename, cache.pagePositions, cache.fileSize,
cache.fullyIndexed, 0);
Serial.printf("TextReader: %s/%s - indexed %d pages%s\n",
subDirs[d].c_str(), _fileList[i].c_str(),
(int)cache.pagePositions.size(),
cache.fullyIndexed ? " (complete)" : "");
yield(); // Feed WDT between files
}
}
Serial.printf("TextReader: Subfolder pre-index: %d files (%d cached, %d newly indexed)\n",
totalSubFiles, cachedSubFiles, indexedSubFiles);
}
void bootIndex(DisplayDriver& display) {
if (!_sdReady) return;
@@ -1444,20 +1545,24 @@ public:
}
}
if (_fileList.size() == 0) {
Serial.println("TextReader: No files to index");
if (_fileList.size() == 0 && _dirList.size() == 0) {
Serial.println("TextReader: No files or folders to index");
_bootIndexed = true;
return;
}
int cachedCount = 0;
int needsIndexCount = 0;
// --- Pass 1 & 2: Index root-level files ---
if (_fileList.size() > 0) {
// --- Pass 1: Fast cache load (no per-file splash screens) ---
// Try to load existing .idx files from SD for every file.
// This is just SD reads — no indexing, no e-ink refreshes.
_fileCache.clear();
_fileCache.resize(_fileList.size()); // Pre-allocate slots to maintain alignment with _fileList
int cachedCount = 0;
int needsIndexCount = 0;
for (int i = 0; i < (int)_fileList.size(); i++) {
if (loadIndex(_fileList[i], _fileCache[i])) {
@@ -1523,6 +1628,26 @@ public:
}
}
} // end if (_fileList.size() > 0)
// --- Pass 3: Pre-index files inside subfolders (one level deep) ---
// Save root state -- bootIndexSubfolders() will overwrite _fileList/_dirList
// via scanFiles() as it iterates each subdirectory.
if (_dirList.size() > 0) {
std::vector<String> savedFileList = _fileList;
std::vector<String> savedDirList = _dirList;
std::vector<FileCache> savedFileCache = _fileCache;
bootIndexSubfolders();
// Restore root state
_currentPath = String(BOOKS_FOLDER);
_fileList = savedFileList;
_dirList = savedDirList;
_fileCache = savedFileCache;
}
// Deselect SD to free SPI bus
digitalWrite(SDCARD_CS, HIGH);

View File

@@ -52,10 +52,28 @@ void Dispatcher::loop() {
prev_isrecv_mode = is_recv;
if (!is_recv) {
radio_nonrx_start = _ms->getMillis();
} else {
rx_stuck_count = 0; // radio recovered — reset counter
}
}
if (!is_recv && _ms->getMillis() - radio_nonrx_start > 8000) { // radio has not been in Rx mode for 8 seconds!
_err_flags |= ERR_EVENT_STARTRX_TIMEOUT;
rx_stuck_count++;
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): RX stuck (attempt %d), calling onRxStuck()", getLogDateTime(), rx_stuck_count);
onRxStuck();
uint8_t reboot_threshold = getRxFailRebootThreshold();
if (reboot_threshold > 0 && rx_stuck_count >= reboot_threshold) {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): RX unrecoverable after %d attempts", getLogDateTime(), rx_stuck_count);
onRxUnrecoverable();
}
// Reset state to give recovery the full 8s window before re-triggering
radio_nonrx_start = _ms->getMillis();
prev_isrecv_mode = true;
cad_busy_start = 0;
next_agc_reset_time = futureMillis(getAGCResetInterval());
}
if (outbound) { // waiting for outbound send to be completed
@@ -277,14 +295,27 @@ void Dispatcher::checkSend() {
logTxFail(outbound, outbound->getRawLength());
// re-queue instead of dropping so the packet gets another chance
// re-queue packet for retry instead of dropping it
int retry_delay = getCADFailRetryDelay();
unsigned long retry_time = futureMillis(retry_delay);
_mgr->queueOutbound(outbound, 0, retry_time);
outbound = NULL;
next_tx_time = retry_time;
// count consecutive failures and reset radio if stuck
uint8_t threshold = getTxFailResetThreshold();
if (threshold > 0) {
tx_fail_count++;
if (tx_fail_count >= threshold) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): TX stuck (%d failures), resetting radio", getLogDateTime(), tx_fail_count);
onTxStuck();
tx_fail_count = 0;
next_tx_time = futureMillis(2000);
}
}
return;
}
tx_fail_count = 0; // clear counter on successful TX start
outbound_expiry = futureMillis(max_airtime);
#if MESH_PACKET_LOGGING

View File

@@ -122,6 +122,8 @@ class Dispatcher {
bool prev_isrecv_mode;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
uint8_t tx_fail_count;
uint8_t rx_stuck_count;
void processRecvPacket(Packet* pkt);
@@ -142,6 +144,8 @@ protected:
_err_flags = 0;
radio_nonrx_start = 0;
prev_isrecv_mode = true;
tx_fail_count = 0;
rx_stuck_count = 0;
}
virtual DispatcherAction onRecvPacket(Packet* pkt) = 0;
@@ -159,6 +163,11 @@ protected:
virtual uint32_t getCADFailMaxDuration() const;
virtual int getInterferenceThreshold() const { return 0; } // disabled by default
virtual int getAGCResetInterval() const { return 0; } // disabled by default
virtual uint8_t getTxFailResetThreshold() const { return 3; } // reset radio after N consecutive TX failures; 0=disabled
virtual void onTxStuck() { _radio->resetAGC(); } // override to use doFullRadioReset() when available
virtual uint8_t getRxFailRebootThreshold() const { return 3; } // reboot after N failed RX recovery attempts; 0=disabled
virtual void onRxStuck() { _radio->resetAGC(); } // called each time RX stuck for 8s; override for deeper reset
virtual void onRxUnrecoverable() { } // called when reboot threshold exceeded; override to call _board->reboot()
public:
void begin();
@@ -188,4 +197,4 @@ private:
void checkSend();
};
}
}

View File

@@ -185,7 +185,7 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
return 0;
}
#define BLE_WRITE_MIN_INTERVAL 60
#define BLE_WRITE_MIN_INTERVAL 30
bool SerialBLEInterface::isWriteBusy() const {
return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write?

View File

@@ -23,7 +23,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE
uint8_t buf[MAX_FRAME_SIZE];
};
#define FRAME_QUEUE_SIZE 4
#define FRAME_QUEUE_SIZE 8
int recv_queue_len;
Frame recv_queue[FRAME_QUEUE_SIZE];
int send_queue_len;

View File

@@ -64,6 +64,7 @@ build_src_filter = ${esp32_base.build_src_filter}
lib_deps =
${esp32_base.lib_deps}
WebServer
DNSServer
Update
; ---------------------------------------------------------------------------

View File

@@ -97,6 +97,7 @@ lib_deps =
adafruit/Adafruit GFX Library@^1.11.0
bitbank2/PNGdec@^1.0.1
WebServer
DNSServer
Update
; ---------------------------------------------------------------------------
@@ -150,7 +151,7 @@ build_flags =
-D MECK_AUDIO_VARIANT
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D FIRMWARE_VERSION='"Meck v1.3.WiFi"'
-D FIRMWARE_VERSION='"Meck v1.5.WiFi"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
@@ -203,7 +204,7 @@ build_flags =
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D FIRMWARE_VERSION='"Meck v1.3.4G"'
-D FIRMWARE_VERSION='"Meck v1.5.4G"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
@@ -234,7 +235,7 @@ build_flags =
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D FIRMWARE_VERSION='"Meck v1.3.4G.WiFi"'
-D FIRMWARE_VERSION='"Meck v1.5.4G.WiFi"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
@@ -261,7 +262,7 @@ build_flags =
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D FIRMWARE_VERSION='"Meck v1.3.4G.SA"'
-D FIRMWARE_VERSION='"Meck v1.5.4G.SA"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>