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
+1
View File
@@ -144,6 +144,7 @@ correctly whatever the keyboard is set to.
| Mark all read | Clears its unread count |
| Notif, Melody | Its own notification and sound, instead of the global ones |
| Fav | Marks it as a favourite (shared with the app) |
| Save | Wio Tracker L1, with Keep messages on: whether its new messages go on the flash. Off for a busy channel you only read live, so it leaves the room to the others; what's already saved stays until the limit or Delete after removes it |
| Scope | The region its messages are tagged with: **Default** follows the list's default, `*` sends none; the list is in Settings › Radio › Scope |
| Pin to dial | Puts it on the Favourites page |
| Edit, Delete | Renames it or changes its secret; removes it |
+3 -2
View File
@@ -39,8 +39,9 @@ A few notes:
- **Keep messages** (Wio Tracker L1) saves messages on the flash, across a
power-off and past what fits in memory (see
[How much is kept](./messages.md#how-much-is-kept)): **Off**, 500, 1000 or
2500 in total. When the limit is reached the oldest go, in batches. Setting
it to Off deletes what was saved.
2500 in total. When the limit is reached the oldest go, in batches. **Off**
stops saving at once and deletes what was saved at the next start, so turn it
back on before then to keep it. **Delete history** deletes right away.
- **Delete after** (Wio Tracker L1) removes saved messages older than 7, 30 or
90 days, or **Never**. They go in batches too, so some stay a little longer,
and nothing goes while the clock isn't set.
+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");