Channel message view retains history after reboot

This commit is contained in:
pelgraine
2026-02-10 18:56:53 +11:00
parent 33c2758a87
commit 8e98132506
3 changed files with 326 additions and 40 deletions
+155 -38
View File
@@ -10,6 +10,7 @@
#include <SD.h>
#include "TextReaderScreen.h"
#include "ContactsScreen.h"
#include "ChannelScreen.h"
extern SPIClass displaySpi; // From GxEPDDisplay.cpp, shared SPI bus
TCA8418Keyboard keyboard(I2C_ADDR_KEYBOARD, &Wire);
@@ -44,6 +45,105 @@
void drawComposeScreen();
void drawEmojiPicker();
void sendComposedMessage();
// SD-backed persistence state
static bool sdCardReady = false;
// ---------------------------------------------------------------------------
// SD Settings Backup / Restore
// ---------------------------------------------------------------------------
// Copies a file byte-for-byte between two filesystem objects.
// Works across SPIFFS <-> SD because both use the Arduino File API.
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;
}
// Backup prefs, channels, and identity from SPIFFS to SD card.
// Called after any savePrefs() to keep the SD mirror current.
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");
}
// Channels may live on SPIFFS or ExtraFS - on ESP32 they are on SPIFFS
if (SPIFFS.exists("/channels2")) {
copyFile(SPIFFS, "/channels2", SD, "/meshcore/channels.bin");
}
// Identity
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");
}
// Contacts
if (SPIFFS.exists("/contacts3")) {
copyFile(SPIFFS, "/contacts3", SD, "/meshcore/contacts.bin");
}
digitalWrite(SDCARD_CS, HIGH); // Release SD CS
Serial.println("Settings backed up to SD");
}
// Restore prefs, channels, and identity from SD card to SPIFFS.
// Called at boot if SPIFFS prefs file is missing (e.g. after a fresh flash).
// Returns true if anything was restored.
bool restoreSettingsFromSD() {
if (!sdCardReady) return false;
bool restored = false;
// Only restore if SPIFFS is missing the prefs file (fresh flash)
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;
}
}
// Identity - most critical; keeps the same device pub key across reflashes
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;
}
#endif
// Believe it or not, this std C function is busted on some platforms!
@@ -289,7 +389,31 @@ void setup() {
MESH_DEBUG_PRINTLN("setup() - ESP32 filesystem init - calling SPIFFS.begin()");
SPIFFS.begin(true);
MESH_DEBUG_PRINTLN("setup() - SPIFFS.begin() done");
// ---------------------------------------------------------------------------
// Early SD card init — needed BEFORE the_mesh.begin() so we can restore
// settings from a previous firmware flash. The display SPI bus is already
// up (display.begin() ran earlier), so SD can share it now.
// ---------------------------------------------------------------------------
#if defined(LilyGo_TDeck_Pro) && defined(HAS_SDCARD)
{
pinMode(SDCARD_CS, OUTPUT);
digitalWrite(SDCARD_CS, HIGH); // Deselect SD initially
if (SD.begin(SDCARD_CS, displaySpi, 4000000)) {
sdCardReady = true;
MESH_DEBUG_PRINTLN("setup() - SD card initialized (early)");
// If SPIFFS was wiped (fresh flash), restore settings from SD backup
if (restoreSettingsFromSD()) {
MESH_DEBUG_PRINTLN("setup() - Settings restored from SD backup");
}
} else {
MESH_DEBUG_PRINTLN("setup() - SD card not available");
}
}
#endif
MESH_DEBUG_PRINTLN("setup() - about to call store.begin()");
store.begin();
MESH_DEBUG_PRINTLN("setup() - store.begin() done");
@@ -350,23 +474,32 @@ void setup() {
initKeyboard();
#endif
// Initialize SD card for text reader
// ---------------------------------------------------------------------------
// SD card is already initialized (early init above).
// Now set up SD-dependent features: message history + text reader.
// ---------------------------------------------------------------------------
#if defined(LilyGo_TDeck_Pro) && defined(HAS_SDCARD)
{
pinMode(SDCARD_CS, OUTPUT);
digitalWrite(SDCARD_CS, HIGH); // Deselect SD initially
if (SD.begin(SDCARD_CS, displaySpi, 4000000)) {
MESH_DEBUG_PRINTLN("setup() - SD card initialized");
// Tell the text reader that SD is ready, then pre-index books at boot
TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen();
if (reader) {
reader->setSDReady(true);
if (disp) {
reader->bootIndex(*disp);
}
if (sdCardReady) {
// Load persisted channel messages from SD
ChannelScreen* chanScr = (ChannelScreen*)ui_task.getChannelScreen();
if (chanScr) {
chanScr->setSDReady(true);
if (chanScr->loadFromSD()) {
MESH_DEBUG_PRINTLN("setup() - Message history loaded from SD");
}
}
}
// Tell the text reader that SD is ready, then pre-index books at boot
TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen();
if (reader) {
reader->setSDReady(true);
if (disp) {
reader->bootIndex(*disp);
}
}
// Do an initial settings backup to SD (captures any first-boot defaults)
backupSettingsToSD();
}
#endif
@@ -376,16 +509,12 @@ void setup() {
sensors.setSettingValue("gps", "1");
the_mesh.getNodePrefs()->gps_enabled = 1;
the_mesh.savePrefs();
#if defined(LilyGo_TDeck_Pro) && defined(HAS_SDCARD)
backupSettingsToSD();
#endif
MESH_DEBUG_PRINTLN("setup() - GPS enabled by default");
#endif
// T-Deck Pro: BLE starts disabled for standalone-first operation
// User can toggle it on from the Bluetooth home page (Enter or long-press)
#if defined(LilyGo_TDeck_Pro) && defined(BLE_PIN_CODE)
serial_interface.disable();
MESH_DEBUG_PRINTLN("setup() - BLE disabled at boot (standalone mode)");
#endif
MESH_DEBUG_PRINTLN("=== setup() - COMPLETE ===");
}
@@ -767,21 +896,9 @@ void handleKeyboardInput() {
case 'q':
case 'Q':
case '\b':
// If editing UTC offset on GPS page, pass through to cancel
if (ui_task.isEditingHomeScreen()) {
ui_task.injectKey('q');
} else {
// Go back to home screen
Serial.println("Nav: Back to home");
ui_task.gotoHomeScreen();
}
break;
case 'u':
case 'U':
// UTC offset editing (on GPS home page)
Serial.println("Nav: UTC offset");
ui_task.injectKey('u');
// Go back to home screen
Serial.println("Nav: Back to home");
ui_task.gotoHomeScreen();
break;
case ' ':
+167 -1
View File
@@ -6,6 +6,11 @@
#include <MeshCore.h>
#include "EmojiSprites.h"
// SD card message persistence
#if defined(HAS_SDCARD) && defined(ESP32)
#include <SD.h>
#endif
// Maximum messages to store in history
#define CHANNEL_MSG_HISTORY_SIZE 20
#define CHANNEL_MSG_TEXT_LEN 160
@@ -14,6 +19,32 @@
#define MAX_GROUP_CHANNELS 20
#endif
// ---------------------------------------------------------------------------
// On-disk format for message persistence (SD card)
// ---------------------------------------------------------------------------
#define MSG_FILE_MAGIC 0x4D434853 // "MCHS" - MeshCore History Store
#define MSG_FILE_VERSION 1
#define MSG_FILE_PATH "/meshcore/messages.bin"
struct __attribute__((packed)) MsgFileHeader {
uint32_t magic;
uint16_t version;
uint16_t capacity;
uint16_t count;
int16_t newestIdx;
// 12 bytes total
};
struct __attribute__((packed)) MsgFileRecord {
uint32_t timestamp;
uint8_t path_len;
uint8_t channel_idx;
uint8_t valid;
uint8_t reserved;
char text[CHANNEL_MSG_TEXT_LEN];
// 168 bytes total
};
class UITask; // Forward declaration
class MyMesh; // Forward declaration
extern MyMesh the_mesh;
@@ -38,17 +69,20 @@ private:
int _scrollPos; // Current scroll position (0 = newest)
int _msgsPerPage; // Messages that fit on screen
uint8_t _viewChannelIdx; // Which channel we're currently viewing
bool _sdReady; // SD card is available for persistence
public:
ChannelScreen(UITask* task, mesh::RTCClock* rtc)
: _task(task), _rtc(rtc), _msgCount(0), _newestIdx(-1), _scrollPos(0),
_msgsPerPage(3), _viewChannelIdx(0) {
_msgsPerPage(3), _viewChannelIdx(0), _sdReady(false) {
// Initialize all messages as invalid
for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
_messages[i].valid = false;
}
}
void setSDReady(bool ready) { _sdReady = ready; }
// Add a new message to the history
void addMessage(uint8_t channel_idx, uint8_t path_len, const char* sender, const char* text) {
// Move to next slot in circular buffer
@@ -70,6 +104,9 @@ public:
// Reset scroll to show newest message
_scrollPos = 0;
// Persist to SD card
saveToSD();
}
// Get count of messages for the currently viewed channel
@@ -88,6 +125,135 @@ public:
uint8_t getViewChannelIdx() const { return _viewChannelIdx; }
void setViewChannelIdx(uint8_t idx) { _viewChannelIdx = idx; _scrollPos = 0; }
// -----------------------------------------------------------------------
// SD card persistence
// -----------------------------------------------------------------------
// Save the entire message buffer to SD card.
// File: /meshcore/messages.bin (~3.4 KB for 20 messages)
void saveToSD() {
#if defined(HAS_SDCARD) && defined(ESP32)
if (!_sdReady) return;
// Ensure directory exists
if (!SD.exists("/meshcore")) {
SD.mkdir("/meshcore");
}
File f = SD.open(MSG_FILE_PATH, "w", true);
if (!f) {
Serial.println("ChannelScreen: SD save failed - can't open file");
return;
}
// Write header
MsgFileHeader hdr;
hdr.magic = MSG_FILE_MAGIC;
hdr.version = MSG_FILE_VERSION;
hdr.capacity = CHANNEL_MSG_HISTORY_SIZE;
hdr.count = (uint16_t)_msgCount;
hdr.newestIdx = (int16_t)_newestIdx;
f.write((uint8_t*)&hdr, sizeof(hdr));
// Write all message slots (including invalid ones - preserves circular buffer layout)
for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
MsgFileRecord rec;
rec.timestamp = _messages[i].timestamp;
rec.path_len = _messages[i].path_len;
rec.channel_idx = _messages[i].channel_idx;
rec.valid = _messages[i].valid ? 1 : 0;
rec.reserved = 0;
memcpy(rec.text, _messages[i].text, CHANNEL_MSG_TEXT_LEN);
f.write((uint8_t*)&rec, sizeof(rec));
}
f.close();
digitalWrite(SDCARD_CS, HIGH); // Release SD CS
#endif
}
// Load message buffer from SD card. Returns true if messages were loaded.
bool loadFromSD() {
#if defined(HAS_SDCARD) && defined(ESP32)
if (!_sdReady) return false;
if (!SD.exists(MSG_FILE_PATH)) {
Serial.println("ChannelScreen: No saved messages on SD");
return false;
}
File f = SD.open(MSG_FILE_PATH, "r");
if (!f) {
Serial.println("ChannelScreen: SD load failed - can't open file");
return false;
}
// Read and validate header
MsgFileHeader hdr;
if (f.read((uint8_t*)&hdr, sizeof(hdr)) != sizeof(hdr)) {
Serial.println("ChannelScreen: SD load failed - short header");
f.close();
return false;
}
if (hdr.magic != MSG_FILE_MAGIC) {
Serial.printf("ChannelScreen: SD load failed - bad magic 0x%08X\n", hdr.magic);
f.close();
return false;
}
if (hdr.version != MSG_FILE_VERSION) {
Serial.printf("ChannelScreen: SD load failed - version %d (expected %d)\n",
hdr.version, MSG_FILE_VERSION);
f.close();
return false;
}
if (hdr.capacity != CHANNEL_MSG_HISTORY_SIZE) {
Serial.printf("ChannelScreen: SD load failed - capacity %d (expected %d)\n",
hdr.capacity, CHANNEL_MSG_HISTORY_SIZE);
f.close();
return false;
}
// Read message records
int loaded = 0;
for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
MsgFileRecord rec;
if (f.read((uint8_t*)&rec, sizeof(rec)) != sizeof(rec)) {
Serial.printf("ChannelScreen: SD load - short read at record %d\n", i);
break;
}
_messages[i].timestamp = rec.timestamp;
_messages[i].path_len = rec.path_len;
_messages[i].channel_idx = rec.channel_idx;
_messages[i].valid = (rec.valid != 0);
memcpy(_messages[i].text, rec.text, CHANNEL_MSG_TEXT_LEN);
if (_messages[i].valid) loaded++;
}
_msgCount = (int)hdr.count;
_newestIdx = (int)hdr.newestIdx;
_scrollPos = 0;
// Sanity-check restored state
if (_newestIdx < -1 || _newestIdx >= CHANNEL_MSG_HISTORY_SIZE) _newestIdx = -1;
if (_msgCount < 0 || _msgCount > CHANNEL_MSG_HISTORY_SIZE) _msgCount = loaded;
f.close();
digitalWrite(SDCARD_CS, HIGH); // Release SD CS
Serial.printf("ChannelScreen: Loaded %d messages from SD (count=%d, newest=%d)\n",
loaded, _msgCount, _newestIdx);
return loaded > 0;
#else
return false;
#endif
}
// -----------------------------------------------------------------------
// Rendering
// -----------------------------------------------------------------------
int render(DisplayDriver& display) override {
char tmp[40];
+4 -1
View File
@@ -87,13 +87,15 @@ public:
bool isOnChannelScreen() const { return curr == channel_screen; }
bool isOnContactsScreen() const { return curr == contacts_screen; }
bool isOnTextReader() const { return curr == text_reader; } // *** NEW ***
bool isEditingHomeScreen() const; // UTC offset editing on GPS page
uint8_t getChannelScreenViewIdx() const;
void toggleBuzzer();
bool getGPSState();
void toggleGPS();
// Check if home screen is in an editing mode (e.g. UTC offset editor)
bool isEditingHomeScreen() const;
// Inject a key press from external source (e.g., keyboard)
void injectKey(char c);
@@ -105,6 +107,7 @@ public:
UIScreen* getMsgPreviewScreen() const { return msg_preview; }
UIScreen* getTextReaderScreen() const { return text_reader; } // *** NEW ***
UIScreen* getContactsScreen() const { return contacts_screen; }
UIScreen* getChannelScreen() const { return channel_screen; }
// from AbstractUITask
void msgRead(int msgcount) override;