mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-10 18:52:44 +02:00
Channel message view retains history after reboot
This commit is contained in:
@@ -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 ' ':
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user