feat(l1): Save per channel, and Keep messages Off deletes the log at the next start

Channel options > Save keeps a busy channel's new messages off the flash, so
it leaves the room to the others; Keep messages stays the master switch.
Off now only stops the saving, so a value stepped past costs nothing and the
wait for the delete comes at a start. Delete history stays immediate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-30 12:35:22 +01:00
co-authored by Claude Opus 5.5
parent 2beb18b217
commit 2fd2c596e4
7 changed files with 54 additions and 9 deletions
+4
View File
@@ -336,6 +336,9 @@ struct NodePrefsFields {
// Settings > Messages > Delete after: messages older than this go, from the
// list at once (index into contactExpiryDays(); 0 = never).
uint8_t msg_expire;
// Channel options > Save: bit i = channel i isn't kept on flash, for one too
// busy to be worth the room [del→onChannelRemoved]
uint64_t ch_no_save;
// ui-new's new-message alert: the banner from the top, an envelope with a
// count in the corner, or nothing (the sound and the wake still happen).
static const uint8_t MSG_ALERT_NORMAL = 0, MSG_ALERT_COMPACT = 1, MSG_ALERT_OFF = 2;
@@ -904,6 +907,7 @@ class NodePrefs : public NodePrefsFields, public ConfigSerializer {
s.hex("ch_fav", ch_fav_bitmask); s.def("ch_fav_only", ch_fav_only);
s.hex("favs", favourite_contacts); s.hex("fav_kinds", favourite_kinds);
s.def("fav_sort", fav_sort); s.def("expiry", contact_expiry_idx); s.def("msg_keep", msg_keep); s.def("msg_expire", msg_expire);
s.hex("ch_no_save", ch_no_save);
}
void gpioSolo(Section& s) {
s.def("m1", gpio1_mode); s.def("m2", gpio2_mode); s.def("m3", gpio3_mode); s.def("m4", gpio4_mode);
@@ -259,6 +259,13 @@ static void setFavourite(NodePrefs* p, uint8_t idx, bool on) {
if (on) p->ch_fav_bitmask |= 1ULL << idx; else p->ch_fav_bitmask &= ~(1ULL << idx);
}
// Whether new messages of this channel go to the flash log (HistoryLog.h).
static bool saved(const NodePrefs* p, uint8_t idx) { return !p || !(p->ch_no_save & (1ULL << idx)); }
static void setSaved(NodePrefs* p, uint8_t idx, bool on) {
if (!p) return;
if (on) p->ch_no_save &= ~(1ULL << idx); else p->ch_no_save |= 1ULL << idx;
}
// Scope as a picker row: 0 = Default (follow the list's default), then the
// list itself from 1 ("*" = unscoped, then the named entries).
static uint8_t scope(const NodePrefs* p, uint8_t idx) {
@@ -274,8 +281,8 @@ static uint8_t scopeFromRow(int row) {
// here, so a channel re-added at a freed slot can't inherit the old one's
// settings. Adding such a field: clear it below (and mark it in NodePrefs.h).
// Covers bot_channel_idx, loc_share_channel_idx, ch_notif_melody_*,
// ch_notif_override / ch_notif_muted, ch_fav_bitmask, favourite dial slots,
// ch_scope_idx. True when something changed (caller saves).
// ch_notif_override / ch_notif_muted, ch_fav_bitmask, ch_no_save, favourite dial
// slots, ch_scope_idx. True when something changed (caller saves).
static bool onRemoved(NodePrefs* p, uint8_t idx) {
if (!p) return false;
bool changed = false;
@@ -286,6 +293,7 @@ static bool onRemoved(NodePrefs* p, uint8_t idx) {
if (p->ch_notif_melody_set & m) { p->ch_notif_melody_set &= ~m; p->ch_notif_melody_2 &= ~m; changed = true; }
if (p->ch_notif_override & m) { p->ch_notif_override &= ~m; p->ch_notif_muted &= ~m; changed = true; }
if (p->ch_fav_bitmask & m) { p->ch_fav_bitmask &= ~m; changed = true; }
if (p->ch_no_save & m) { p->ch_no_save &= ~m; changed = true; }
if (idx < NodePrefs::MAX_SCOPED_CHANNELS && p->ch_scope_idx[idx] != NodePrefs::CH_SCOPE_DEFAULT) {
p->ch_scope_idx[idx] = NodePrefs::CH_SCOPE_DEFAULT; // back to Default, as a new channel
changed = true;
+18 -5
View File
@@ -25,7 +25,9 @@
// A record: Rec, the path, the text, then its own total length as one byte, so
// the log can be walked from the end. restore() fills the RAM rings from there.
// A channel's messages carry channelKey(), so they follow the channel, not the
// slot; those of a channel since deleted aren't restored.
// slot; those of a channel since deleted aren't restored. A channel with Save
// off (chanctl::saved()) stays out of the log from then on; what it has there
// stays until it ages out.
//
// Older messages, past what the rings hold, are read a page at a time
// (pageBegin*(), pageOlder(), pageNewer()): the messages screen measures its
@@ -54,6 +56,8 @@ public:
if (_on) {
scan();
restore();
} else if (_fs && _fs->exists(ROOT)) {
forget(); // Keep messages off since the last start, see loop()
}
_next_prune = millis() + SAVE_DELAY_MS;
history->setArchive(this);
@@ -61,8 +65,12 @@ public:
// ── HistArchive ────────────────────────────────────────────────────────────
// arc_id orders the entries of this run; a restored one keeps 0, already saved.
void chAppend(ChHistEntry& e) override { e.arc_id = ++_seq; appended(); }
void chUpdate(const ChHistEntry&) override { touch(); }
void chAppend(ChHistEntry& e) override {
if (!chanctl::saved(_prefs, e.ch_idx)) { e.arc_id = NOT_SAVED; return; }
e.arc_id = ++_seq;
appended();
}
void chUpdate(const ChHistEntry& e) override { if (e.arc_id != NOT_SAVED) touch(); }
void dmAppend(DmHistEntry& e) override { e.arc_id = ++_seq; appended(); }
void dmUpdate(const DmHistEntry&) override { touch(); }
@@ -70,7 +78,9 @@ public:
bool on = _fs && _prefs && _prefs->msg_keep;
if (on != _on) {
_on = on;
if (on) touch(); else forget();
// Off only stops the saving; begin() deletes the log, so a value stepped
// past on the way to another costs nothing, and the wait comes at a start.
if (on) touch(); else _dirty = false;
}
if (_dirty && (int32_t)(millis() - _due) >= 0) flush(false);
if (_on && (int32_t)(millis() - _next_prune) >= 0) {
@@ -177,6 +187,9 @@ public:
private:
static constexpr const char* ROOT = "/h";
// arc_id of a message that stays in the ring: a channel with Save off. Above
// any _ch_done, so it never counts as in the log (pageBeginCh).
static const uint32_t NOT_SAVED = 0xFFFFFFFFUL;
struct __attribute__((packed)) Rec {
uint32_t key; // channelKey(), or a DM's 4-byte public key prefix
@@ -250,7 +263,7 @@ private:
uint32_t now = rtc_clock.getCurrentTime();
for (int j = _history->chHistCount() - 1; j >= 0; j--) {
const ChHistEntry& e = _history->chAtPos(_history->chHistPosNewest(j));
if (e.arc_id <= _ch_done) continue;
if (e.arc_id <= _ch_done || e.arc_id == NOT_SAVED) continue;
if (!all && e.relay_status == ACK_PENDING && now - e.timestamp < SETTLE_SECS) { more = true; break; }
ChannelDetails ch;
if (the_mesh.getChannel(e.ch_idx, ch) && ch.name[0] &&
@@ -15,6 +15,7 @@
#include "MessageHistory.h"
#ifdef HIST_FLASH
#include "ChannelControl.h" // chanctl::saved()
#include "HistoryLog.h"
#endif
#include "DmUnreadTable.h"
@@ -73,6 +73,8 @@ class MessagesScreen : public UIScreen {
char _ctx_pin_item[28]; // "Pin to dial" or "Unpin (slot N)"
char _ctx_fav_item[12]; // "Fav: ON" / "Fav: OFF" — shared by the channel,
// contact and room menus (never open at once)
char _ctx_save_item[12]; // "Save: ON" / "Save: OFF"
int _ctx_save_idx = -1; // its row in the channel menu, -1 with nothing to save
int _ctx_fav_idx = -1; // row the Fav toggle sits on, frozen at menu open
int _ctx_pin_idx = -1; // ...and the Pin row, in the room menu (it sits after Fav,
// whose own row depends on whether Logout is shown)
@@ -985,6 +987,11 @@ class MessagesScreen : public UIScreen {
snprintf(_ctx_fav_item, sizeof(_ctx_fav_item), is_fav ? "Fav: ON" : "Fav: OFF");
_ctx_dirty = true;
}
} else if (sel == _ctx_save_idx) {
bool on = !chanctl::saved(_task->getNodePrefs(), ch_idx);
chanctl::setSaved(_task->getNodePrefs(), ch_idx, on);
snprintf(_ctx_save_item, sizeof(_ctx_save_item), on ? "Save: ON" : "Save: OFF");
_ctx_dirty = true;
}
}
@@ -2365,6 +2372,7 @@ public:
if (res == PopupMenu::SELECTED && _num_channels > 0) {
uint8_t ch_idx = _ctx_ch_idx; // frozen at menu open — see declaration
int sel = _ctx_menu.selectedIndex();
if (_ctx_save_idx >= 0 && sel > _ctx_save_idx) sel--; // the rows below count without it
if (sel == 0) {
int cleared = (int)_history.chUnread(ch_idx);
_history.setChUnread(ch_idx, 0);
@@ -2465,6 +2473,15 @@ public:
_ctx_menu.addValueItem(_ctx_melody_item);
_ctx_fav_idx = 3;
_ctx_menu.addValueItem(_ctx_fav_item);
_ctx_save_idx = -1;
#ifdef HIST_FLASH
if (_task->getNodePrefs() && _task->getNodePrefs()->msg_keep) { // only with the log on
snprintf(_ctx_save_item, sizeof(_ctx_save_item),
chanctl::saved(_task->getNodePrefs(), ch_idx) ? "Save: ON" : "Save: OFF");
_ctx_save_idx = 4;
_ctx_menu.addValueItem(_ctx_save_item);
}
#endif
_ctx_menu.addItem(_ctx_scope_item);
_ctx_menu.addItem(_ctx_pin_item);
_ctx_menu.addItem("Edit");