feat(ui): DM auto-resend + incoming dedup + channel relay ring

DM auto-resend (delivery status follow-up):
- New pref dm_resend_count (0-5, default 2), Settings › Messages › Resend.
  Schema sentinel 0xC0DE0009 → 0xC0DE000A; old files clamp to the default.
- When a pending on-device DM passes its ACK deadline, tickDmResends()
  (driven from UITask::loop, so it runs in the background regardless of the
  active screen) re-sends with the next attempt# reusing the original
  timestamp, refreshing ack_tag/deadline, until resends run out → then ✗.
- dmEffectiveStatus keeps the entry pending while resends remain.

Incoming DM dedup:
- A retry reuses the sender timestamp + text but has a fresh packet hash, so
  the mesh dup-filter passes it. addDMMsg now takes sender_timestamp and drops
  copies matching prefix+timestamp+text. sender_timestamp plumbed from
  MyMesh::queueMessage through AbstractUITask/UITask.

Channel relay ring:
- Replace the single-slot relayed-into-mesh tracker with a 4-slot ring so a
  quick burst of channel sends are each matched to their repeater echo.
  Receive-path hashing still gated (now on _relay_active) so the hot flood
  path is untouched when idle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-14 15:13:29 +02:00
parent 54b53f7f0f
commit aa4bb490d0
9 changed files with 166 additions and 38 deletions

View File

@@ -324,6 +324,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
rd(&_prefs.advert_sound_scope, sizeof(_prefs.advert_sound_scope));
rd(&_prefs.rx_powersave, sizeof(_prefs.rx_powersave));
rd(&_prefs.tx_apc, sizeof(_prefs.tx_apc));
rd(&_prefs.dm_resend_count, sizeof(_prefs.dm_resend_count));
// These fields were appended over successive schema bumps; an older file
// can leave stray bytes here, so clamp out-of-range values back to defaults.
// Values for notif_melody_ad: 0=built-in, 1=melody1, 2=melody2, 3=none.
@@ -333,6 +334,9 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
if (_prefs.advert_sound_scope > 1) _prefs.advert_sound_scope = ADVERT_SOUND_SCOPE_ALL;
if (_prefs.rx_powersave > 1) _prefs.rx_powersave = 0;
if (_prefs.tx_apc > 1) _prefs.tx_apc = 0;
// An old (0xC0DE0009) file leaves the low byte of its sentinel here (0x09),
// which is out of range — fall back to the default of 2 resends.
if (_prefs.dm_resend_count > 5) _prefs.dm_resend_count = 2;
// Schema sentinel: bumped on layout changes. Mismatch means an older file
// (or a different schema); rd() already zero-inits any fields not present,
@@ -456,6 +460,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.advert_sound_scope, sizeof(_prefs.advert_sound_scope));
file.write((uint8_t *)&_prefs.rx_powersave, sizeof(_prefs.rx_powersave));
file.write((uint8_t *)&_prefs.tx_apc, sizeof(_prefs.tx_apc));
file.write((uint8_t *)&_prefs.dm_resend_count, sizeof(_prefs.dm_resend_count));
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL.
uint32_t sentinel = NodePrefs::SCHEMA_SENTINEL;