This commit is contained in:
pelgraine
2026-03-22 16:11:37 +11:00
parent bad821ac4b
commit b208af83f6
3 changed files with 169 additions and 15 deletions
+133
View File
@@ -367,6 +367,87 @@
static bool gt911Ready = false;
static bool sdCardReady = false; // T5S3 SD card state
// ---------------------------------------------------------------------------
// SD Settings Backup / Restore (T5S3)
// ---------------------------------------------------------------------------
static bool copyFile(fs::FS& srcFS, const char* srcPath,
fs::FS& dstFS, const char* dstPath) {
File src = srcFS.open(srcPath, "r");
if (!src) return false;
File dst = dstFS.open(dstPath, "w", true);
if (!dst) { src.close(); return false; }
uint8_t buf[128];
while (src.available()) {
int n = src.read(buf, sizeof(buf));
if (n > 0) dst.write(buf, n);
}
src.close();
dst.close();
return true;
}
void backupSettingsToSD() {
if (!sdCardReady) return;
if (!SD.exists("/meshcore")) SD.mkdir("/meshcore");
if (SPIFFS.exists("/new_prefs")) {
copyFile(SPIFFS, "/new_prefs", SD, "/meshcore/prefs.bin");
}
if (SPIFFS.exists("/channels2")) {
copyFile(SPIFFS, "/channels2", SD, "/meshcore/channels.bin");
}
if (SPIFFS.exists("/identity/_main.id")) {
if (!SD.exists("/meshcore/identity")) SD.mkdir("/meshcore/identity");
copyFile(SPIFFS, "/identity/_main.id", SD, "/meshcore/identity/_main.id");
}
if (SPIFFS.exists("/contacts3")) {
copyFile(SPIFFS, "/contacts3", SD, "/meshcore/contacts.bin");
}
digitalWrite(SDCARD_CS, HIGH);
Serial.println("Settings backed up to SD");
}
bool restoreSettingsFromSD() {
if (!sdCardReady) return false;
bool restored = false;
if (!SPIFFS.exists("/new_prefs") && SD.exists("/meshcore/prefs.bin")) {
if (copyFile(SD, "/meshcore/prefs.bin", SPIFFS, "/new_prefs")) {
Serial.println("Restored prefs from SD");
restored = true;
}
}
if (!SPIFFS.exists("/channels2") && SD.exists("/meshcore/channels.bin")) {
if (copyFile(SD, "/meshcore/channels.bin", SPIFFS, "/channels2")) {
Serial.println("Restored channels from SD");
restored = true;
}
}
if (!SPIFFS.exists("/identity/_main.id") && SD.exists("/meshcore/identity/_main.id")) {
SPIFFS.mkdir("/identity");
if (copyFile(SD, "/meshcore/identity/_main.id", SPIFFS, "/identity/_main.id")) {
Serial.println("Restored identity from SD");
restored = true;
}
}
if (!SPIFFS.exists("/contacts3") && SD.exists("/meshcore/contacts.bin")) {
if (copyFile(SD, "/meshcore/contacts.bin", SPIFFS, "/contacts3")) {
Serial.println("Restored contacts from SD");
restored = true;
}
}
if (restored) {
Serial.println("=== Settings restored from SD card backup ===");
}
digitalWrite(SDCARD_CS, HIGH);
return restored;
}
#ifdef MECK_CARDKB
#include "CardKBKeyboard.h"
static CardKBKeyboard cardkb;
@@ -1322,6 +1403,11 @@ void setup() {
if (mounted) {
sdCardReady = true;
Serial.println("setup() - SD card initialized");
// If SPIFFS was wiped (fresh flash), restore settings from SD backup
if (restoreSettingsFromSD()) {
Serial.println("setup() - T5S3: Settings restored from SD backup");
}
} else {
Serial.println("setup() - SD card not available");
}
@@ -1682,8 +1768,52 @@ void setup() {
MESH_DEBUG_PRINTLN("=== setup() - COMPLETE ===");
}
// ---------------------------------------------------------------------------
// OTA radio control — pause LoRa during firmware updates to prevent SPI
// bus contention (SD and LoRa share the same SPI bus on both platforms).
// Also pauses the mesh loop to prevent radio state confusion while standby.
// ---------------------------------------------------------------------------
#ifdef MECK_OTA_UPDATE
extern RADIO_CLASS radio; // Defined in target.cpp
static bool otaRadioPaused = false;
void otaPauseRadio() {
otaRadioPaused = true;
radio.standby();
Serial.println("OTA: Radio standby, mesh loop paused");
}
void otaResumeRadio() {
radio.startReceive();
otaRadioPaused = false;
Serial.println("OTA: Radio receive resumed, mesh loop active");
}
#endif
void loop() {
#ifdef MECK_OTA_UPDATE
if (!otaRadioPaused) {
#endif
the_mesh.loop();
#ifdef MECK_OTA_UPDATE
} else {
// OTA 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.
if (ui_task.isOnSettingsScreen()) {
SettingsScreen* ss = (SettingsScreen*)ui_task.getSettingsScreen();
if (ss) {
ss->pollOTAServer();
// Detect upload completion and trigger verify → flash → reboot.
// Must happen here (not in render) because T5S3 e-ink refresh blocks
// for 500ms+ and the render-based check never fires reliably.
ss->checkOTAComplete(display);
}
}
}
#endif
sensors.loop();
@@ -1955,6 +2085,9 @@ void loop() {
#endif
rtc_clock.tick();
// Periodic AGC reset - re-assert boosted RX gain to prevent sensitivity drift
#ifdef MECK_OTA_UPDATE
if (!otaRadioPaused)
#endif
if ((millis() - lastAGCReset) >= AGC_RESET_INTERVAL_MS) {
radio_reset_agc();
lastAGCReset = millis();
@@ -794,9 +794,18 @@ public:
WiFi.macAddress(mac);
snprintf(_otaApName, sizeof(_otaApName), "Meck-Update-%02X%02X", mac[4], mac[5]);
// Tear down existing WiFi and start AP
// Pause LoRa radio — SD and LoRa share the same SPI bus on both
// platforms. Incoming packets during SD writes cause bus contention
// that stalls the upload.
extern void otaPauseRadio();
otaPauseRadio();
// Clean WiFi init from any state (including never-initialised on
// standalone builds where WiFi.mode() was never called during boot).
// OFF→AP sequence ensures the WiFi peripheral starts fresh.
WiFi.disconnect(true);
delay(100);
WiFi.mode(WIFI_OFF);
delay(200);
WiFi.mode(WIFI_AP);
WiFi.softAP(_otaApName);
delay(500); // Let AP stabilise
@@ -878,12 +887,15 @@ public:
WiFi.mode(WIFI_OFF);
delay(100);
_editMode = EDIT_NONE;
// Resume LoRa radio
extern void otaResumeRadio();
otaResumeRadio();
// Try to restore STA WiFi from saved credentials
#ifdef MECK_WIFI_COMPANION
WiFi.mode(WIFI_STA);
wifiReconnectSaved();
#endif
Serial.println("OTA: Stopped, AP down");
Serial.println("OTA: Stopped, AP down, radio resumed");
}
bool verifyFirmwareFile() {
@@ -975,13 +987,25 @@ public:
return true;
}
// Called from render loop to poll the web server
// Called from render loop AND main loop to poll the web server
void pollOTAServer() {
if (_otaServer && (_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) {
_otaServer->handleClient();
}
}
// Called from main loop — detect upload completion and trigger flash.
// Must be called from the main loop (not render) because T5S3 FastEPD
// blocks for 500ms+ per frame, making render-only detection unreliable.
void checkOTAComplete(DisplayDriver& display) {
if (_editMode != EDIT_OTA) return;
if (!_otaUploadOk) return;
if (_otaPhase != OTA_PHASE_RECEIVING && _otaPhase != OTA_PHASE_WAITING) return;
Serial.printf("OTA: Upload complete (%d bytes), starting flash sequence\n", _otaBytesReceived);
processOTAUpload(display);
}
// Run the verify → flash → reboot sequence after upload completes
void processOTAUpload(DisplayDriver& display) {
// Stop web server and AP first
@@ -1599,13 +1623,6 @@ public:
display.setTextSize(0);
int oy = by + 4;
// Detect upload completion — trigger verify + flash sequence
if (_otaUploadOk && (_otaPhase == OTA_PHASE_RECEIVING || _otaPhase == OTA_PHASE_WAITING)) {
display.endFrame(); // Flush current frame before blocking flash
processOTAUpload(display);
return 500; // Won't reach here if flash succeeds (reboots)
}
if (_otaPhase == OTA_PHASE_CONFIRM) {
display.drawTextCentered(display.width() / 2, oy, "Firmware Update");
oy += 14;
@@ -1853,11 +1870,8 @@ public:
return true;
}
} else if (_otaPhase == OTA_PHASE_WAITING) {
// Check if upload just completed
// Upload completed — main loop will detect and trigger flash
if (_otaUploadOk) {
// Upload finished — run verify + flash (blocking)
// The display reference isn't available here, so we set a flag
// and the render loop will call processOTAUpload()
return true;
}
if (c == 'q' || c == 'Q') {
@@ -63,10 +63,13 @@ build_src_filter = ${esp32_base.build_src_filter}
+<../variants/LilyGo_T5S3_EPaper_Pro>
lib_deps =
${esp32_base.lib_deps}
WebServer
Update
; ---------------------------------------------------------------------------
; T5S3 standalone — touch UI (stub), verify display rendering
; Uses FastEPD for parallel e-ink, Adafruit GFX for drawing
; OTA enabled: WiFi AP activates only during user-initiated firmware updates.
; ---------------------------------------------------------------------------
[env:meck_t5s3_standalone]
extends = LilyGo_T5S3_EPaper_Pro
@@ -80,6 +83,7 @@ build_flags =
-D DISPLAY_CLASS=FastEPDDisplay
-D USE_EINK
-D MECK_CARDKB
-D MECK_OTA_UPDATE=1
; -D MECK_SERIF_FONT ; FreeSerif (Times New Roman-like)
; ; Default (no flag): FreeSans (Arial-like)
build_src_filter = ${LilyGo_T5S3_EPaper_Pro.build_src_filter}
@@ -98,6 +102,7 @@ lib_deps =
; ---------------------------------------------------------------------------
; T5S3 BLE companion — touch UI, BLE phone bridging
; Connect via MeshCore iOS/Android app over Bluetooth
; OTA enabled: WiFi AP activates only during user-initiated firmware updates.
; Flash: pio run -e meck_t5s3_ble -t upload
; ---------------------------------------------------------------------------
[env:meck_t5s3_ble]
@@ -112,6 +117,7 @@ build_flags =
-D DISPLAY_CLASS=FastEPDDisplay
-D USE_EINK
-D MECK_CARDKB
-D MECK_OTA_UPDATE=1
; -D MECK_SERIF_FONT
build_src_filter = ${LilyGo_T5S3_EPaper_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
@@ -141,6 +147,7 @@ build_flags =
-D MAX_GROUP_CHANNELS=20
-D MECK_WIFI_COMPANION=1
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D TCP_PORT=5000
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=FastEPDDisplay