mirror of
https://github.com/pelgraine/Meck.git
synced 2026-07-03 16:21:17 +02:00
t5s3 ota
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user