mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-10 02:32:44 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user