feat(ui): message delivery status (DM ACK + channel relay)

End-to-end delivery indicators on the device UI, drawn next to outgoing
messages and auto-scaled to the font (legible on landscape e-ink).

DM (and room servers, which share the DM path):
- Pending / delivered / failed marker driven by the real end-to-end ACK.
  Pending shows a row of dots — one per send — so auto-resend progress is
  visible before it resolves to ✓ / ✗.
- Auto-resend: new pref dm_resend_count (0-5, default 2), Settings ›
  Messages › Resend. A pending DM whose ACK times out is re-sent (reusing
  the original timestamp) until resends run out, then ✗. Driven from
  UITask::loop so it completes in the background, independent of screen.
- Incoming dedup: a retry reuses the sender timestamp + text but carries a
  fresh packet hash, so addDMMsg drops copies matching prefix+ts+text.

Channels (flood, no recipient ACK):
- ✓ only once a repeater echo confirms the send was relayed into the mesh;
  no echo is normal, not a failure (no pending/fail shown). A small ring
  tracks a burst of sends so each matches its echo. Receive-path hashing is
  gated so the hot flood path is untouched when idle.

Shared:
- Markers shown in both the history list and the fullscreen message view.
- Reusable scalable mini-icon facility in icons.h (bitmap + auto-scale);
  adding a new status icon is a bitmap plus one draw call.

No changes to the upstream mesh library (src/).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-14 23:33:16 +02:00
co-authored by Claude Opus 4.8
parent 8545454f5b
commit bcf8774962
10 changed files with 379 additions and 19 deletions
@@ -65,6 +65,7 @@ class SettingsScreen : public UIScreen {
SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER,
// Messages section
SECTION_MESSAGES,
DM_RESEND,
MSG_SLOT_0, MSG_SLOT_1, MSG_SLOT_2, MSG_SLOT_3, MSG_SLOT_4,
MSG_SLOT_5, MSG_SLOT_6, MSG_SLOT_7, MSG_SLOT_8, MSG_SLOT_9,
Count
@@ -570,6 +571,12 @@ class SettingsScreen : public UIScreen {
display.print("Rooms");
display.setCursor(display.valCol(), y);
display.print((p && p->room_fav_only) ? "fav" : "all");
} else if (item == DM_RESEND) {
display.print("Resend");
display.setCursor(display.valCol(), y);
uint8_t n = p ? p->dm_resend_count : 0;
if (n == 0) display.print("OFF");
else { char buf[6]; snprintf(buf, sizeof(buf), "%ux", (unsigned)n); display.print(buf); }
} else if (isMsgSlot(item)) {
int slot = msgSlotIndex(item);
char label[5];
@@ -777,6 +784,12 @@ public:
_dirty = true;
return true;
}
if (_selected == DM_RESEND && p) {
int n = p->dm_resend_count;
if (right || enter) n = (n + 1) % 6; // 0..5, wraps
else if (left) n = (n + 5) % 6;
if (left || right || enter) { p->dm_resend_count = (uint8_t)n; _dirty = true; return true; }
}
if (_selected == BATT_DISPLAY && p) {
int idx = p->batt_display_mode < BATT_DISPLAY_COUNT ? p->batt_display_mode : 0;
if (right) idx = (idx + 1) % BATT_DISPLAY_COUNT;