T-Watch - update settingscreen touch so rxlog displays on watch with auto-scroll updates.

Added persistent channel message history with swipe to scroll up and down.
This commit is contained in:
pelgraine
2026-07-17 22:02:16 +10:00
parent 619e8f44a5
commit 2cf7945136
5 changed files with 159 additions and 2 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
#define FIRMWARE_VER_CODE 11
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "15 July 2026"
#define FIRMWARE_BUILD_DATE "17 July 2026"
#endif
#ifndef FIRMWARE_VERSION
+28
View File
@@ -2164,6 +2164,11 @@ static void lastHeardToggleContact() {
if (ss->isEditing()) {
return 0; // Consume — don't interfere with active edit mode
}
#if defined(MECK_TWATCH)
// Aim activation at the row under the finger, not just the current
// cursor, so a single long-press opens/edits the pressed row.
{ int vx, vy; touchToVirtual(x, y, vx, vy); ss->selectRowAtVY(vy); }
#endif
if (ss->isOnDeletableChannel()) {
return 'x'; // Long press on channel row → delete
}
@@ -2853,6 +2858,15 @@ void setup() {
Serial.println("setup() - SD features initialized");
}
#endif
#if defined(MECK_TWATCH)
// Watch: load persisted channel-message history from LittleFS (no SD).
{
ChannelScreen* chanScr = (ChannelScreen*)ui_task.getChannelScreen();
if (chanScr && chanScr->loadFromLittleFS()) {
MESH_DEBUG_PRINTLN("setup() - Message history loaded from LittleFS");
}
}
#endif
// Check if node name is still the default hex prefix (first 4 bytes of pub key)
// If so, launch onboarding wizard to set name and radio preset
// ---------------------------------------------------------------------------
@@ -2974,6 +2988,14 @@ void otaResumeRadio() {
#endif
void loop() {
#if defined(MECK_TWATCH)
// Watch: flush pending channel-message history to LittleFS on a 30s debounce
// (saveToSD marks it dirty on each message; at most one ~57 KB write / 30s).
{
ChannelScreen* chanScr = (ChannelScreen*)ui_task.getChannelScreen();
if (chanScr) chanScr->flushMessagesIfDue(30000UL);
}
#endif
// T-Echo Card: lazy Codec2 init from shallow stack context.
// codec2_create needs ~3KB stack for FFT/trig init. The loop task
// has only 4KB total. Calling from render() (deep call chain) overflows.
@@ -3963,6 +3985,12 @@ void loop() {
// Channel screen: horizontal swipe opens picker instead of cycling
if (ui_task.isOnChannelScreen() && (c == 'a' || c == 'd')) {
ui_task.gotoChannelPickerScreen();
#if defined(MECK_TWATCH)
} else if (ui_task.isOnChannelScreen() && (c == 'w' || c == 's')) {
// Watch: a vertical swipe pages the message view (one page/swipe).
ChannelScreen* cs = (ChannelScreen*)ui_task.getChannelScreen();
if (cs) { cs->pageScroll(c == 'w'); ui_task.forceRefresh(); }
#endif
} else {
ui_task.injectKey(c);
}
@@ -11,6 +11,9 @@
#if defined(HAS_SDCARD) && defined(ESP32)
#include <SD.h>
#endif
#if defined(MECK_TWATCH)
#include <LittleFS.h> // watch has no SD -- message history persists to LittleFS
#endif
// DM send status codes (session only). Guarded so AbstractUITask.h can carry
// the same definitions without a clash.
@@ -103,6 +106,9 @@ private:
int _msgsPerPage; // Messages that fit on screen
uint8_t _viewChannelIdx; // Which channel we're currently viewing
bool _sdReady; // SD card is available for persistence
#if defined(MECK_TWATCH)
unsigned long _msgDirtySince = 0; // watch: pending LittleFS write (0 = clean)
#endif
bool _showPathOverlay; // Show path detail overlay for last received msg
int _pathScrollPos; // Scroll offset within path overlay hop list
int _pathHopsVisible; // Hops that fit on screen (set during render)
@@ -554,6 +560,13 @@ public:
// Save the entire message buffer to SD card.
// File: /meshcore/messages.bin (~50 KB for 300 messages)
void saveToSD() {
#if defined(MECK_TWATCH)
// Watch: defer to the LittleFS flush cadence -- a ~57 KB write on every
// message would wear the flash. Timestamp the pending write; the main loop
// flushes it (see flushMessagesIfDue), and shutdown flushes directly.
if (_msgDirtySince == 0) _msgDirtySince = millis();
return;
#endif
#if defined(HAS_SDCARD) && defined(ESP32)
if (!_sdReady) return;
@@ -596,6 +609,94 @@ public:
#endif
}
#if defined(MECK_TWATCH)
// --- Watch message-history persistence (LittleFS; the watch has no SD) ------
// Flush the ring to LittleFS. Called on an interval from the main loop and
// directly on shutdown, so traffic produces at most one write per interval.
void flushMessagesNow() {
if (!LittleFS.exists("/meshcore")) {
LittleFS.mkdir("/meshcore");
}
File f = LittleFS.open(MSG_FILE_PATH, "w", true);
if (!f) {
Serial.println("ChannelScreen: LittleFS save failed - can't open file");
return;
}
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));
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.snr = _messages[i].snr;
rec.dm_peer_hash = _messages[i].dm_peer_hash;
memcpy(rec.path, _messages[i].path, MSG_PATH_MAX);
memcpy(rec.text, _messages[i].text, CHANNEL_MSG_TEXT_LEN);
f.write((uint8_t*)&rec, sizeof(rec));
}
f.close();
_msgDirtySince = 0;
}
// Flush only if a write is pending and the debounce interval has elapsed.
void flushMessagesIfDue(unsigned long intervalMs) {
if (_msgDirtySince && millis() - _msgDirtySince >= intervalMs) flushMessagesNow();
}
// Load the ring from LittleFS at boot. Returns true if any messages loaded.
bool loadFromLittleFS() {
if (!LittleFS.exists(MSG_FILE_PATH)) {
Serial.println("ChannelScreen: no saved messages on LittleFS");
return false;
}
File f = LittleFS.open(MSG_FILE_PATH, "r");
if (!f) {
Serial.println("ChannelScreen: LittleFS load failed - can't open file");
return false;
}
MsgFileHeader hdr;
if (f.read((uint8_t*)&hdr, sizeof(hdr)) != sizeof(hdr)) { f.close(); return false; }
if (hdr.magic != MSG_FILE_MAGIC) { f.close(); return false; }
if (hdr.version != MSG_FILE_VERSION) { f.close(); return false; }
if (hdr.capacity != CHANNEL_MSG_HISTORY_SIZE) { f.close(); return false; }
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)) 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);
_messages[i].snr = rec.snr;
_messages[i].dm_peer_hash = rec.dm_peer_hash;
memcpy(_messages[i].path, rec.path, MSG_PATH_MAX);
memcpy(_messages[i].text, rec.text, CHANNEL_MSG_TEXT_LEN);
_messages[i].scope_idx = 0xFF;
_messages[i].send_ref = 0;
_messages[i].dm_status = DM_SEND_NONE;
_messages[i].dm_attempt = 0;
_messages[i].dm_total = 0;
if (_messages[i].valid) loaded++;
}
_msgCount = (int)hdr.count;
_newestIdx = (int)hdr.newestIdx;
_scrollPos = 0;
if (_newestIdx < -1 || _newestIdx >= CHANNEL_MSG_HISTORY_SIZE) _newestIdx = -1;
if (_msgCount < 0 || _msgCount > CHANNEL_MSG_HISTORY_SIZE) _msgCount = loaded;
f.close();
Serial.printf("ChannelScreen: Loaded %d messages from LittleFS (count=%d, newest=%d)\n",
loaded, _msgCount, _newestIdx);
return loaded > 0;
}
#endif
// Load message buffer from SD card. Returns true if messages were loaded.
bool loadFromSD() {
#if defined(HAS_SDCARD) && defined(ESP32)
@@ -1685,6 +1786,16 @@ public:
#endif
}
// Vertical-swipe scroll (watch). Pages the normal message view a screenful at
// a time; keeps single-step in reply-select / path-overlay / DM-inbox so
// precise selection isn't lost. Bounds clamping is handled by handleInput.
void pageScroll(bool older) {
char k = older ? 'w' : 's';
bool normalView = !_replySelectMode && !_showPathOverlay && !_dmInboxMode;
int steps = (normalView && _msgsPerPage > 1) ? _msgsPerPage : 1;
for (int i = 0; i < steps; i++) handleInput(k);
}
bool handleInput(char c) override {
// If overlay is showing, handle scroll and dismiss
if (_showPathOverlay) {
@@ -140,6 +140,10 @@ public:
int render(DisplayDriver& display) override {
int count = the_mesh.getRxLogCount();
#if defined(MECK_TWATCH)
// Watch: no keyboard or touch scroll, so cycle through entries automatically.
if (count > 1) { _scrollPos++; if (_scrollPos >= count) _scrollPos = 0; }
#endif
if (_scrollPos < 0) _scrollPos = 0;
if (_scrollPos > count - 1) _scrollPos = (count > 0) ? count - 1 : 0;
@@ -153,7 +157,11 @@ public:
display.drawRect(0, 11, display.width(), 1);
int headerHeight = 14;
#if defined(MECK_TWATCH)
int footerHeight = 2; // no footer on the watch
#else
int footerHeight = 14;
#endif
int maxY = display.height() - footerHeight;
int y = headerHeight;
@@ -180,6 +188,7 @@ public:
display.setTextSize(1);
// === Footer ===
#if !defined(MECK_TWATCH)
int footerY = display.height() - 12;
display.drawRect(0, footerY - 2, display.width(), 1);
display.setColor(DisplayDriver::YELLOW);
@@ -189,8 +198,13 @@ public:
#else
display.print("Sh+Del:Bk W/S:Scroll");
#endif
#endif
#if defined(MECK_TWATCH)
return 3000; // watch: advance to the next packet every 3s
#else
return 5000; // refresh every 5s to pick up newly received packets
#endif
}
bool handleInput(char c) override {
@@ -3028,7 +3028,11 @@ public:
}
#endif
#if defined(MECK_TWATCH)
if (_tickerActive) return 60; // marquee running -- match the channel screen
// Marquee running, or a wide row was just selected (its width was measured
// this frame): refresh fast so scrolling starts promptly instead of after
// the 1s idle interval.
if (_tickerActive ||
(_tickerRow == _cursor && _tickerTextW > display.width() - 10)) return 60;
#endif
return _editMode != EDIT_NONE ? 700 : 1000;
}