mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
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:
@@ -28,9 +28,11 @@ class QuickMsgScreen : public UIScreen {
|
||||
int _sel_channel_idx;
|
||||
bool _sending_to_channel;
|
||||
|
||||
// Carries the just-sent DM's ACK tag + deadline from sendText() to afterSend().
|
||||
// Carries the just-sent DM's ACK tag + deadline + send timestamp from
|
||||
// sendText() to afterSend().
|
||||
uint32_t _last_ack_tag = 0;
|
||||
uint32_t _last_ack_deadline_ms = 0;
|
||||
uint32_t _last_send_ts = 0;
|
||||
|
||||
// MSG_PICK (shared)
|
||||
int _msg_sel, _msg_scroll;
|
||||
@@ -113,6 +115,12 @@ class QuickMsgScreen : public UIScreen {
|
||||
uint8_t ack_status; // AckState; meaningful only when outgoing
|
||||
uint32_t ack_tag; // expected_ack CRC to match against onMsgAck()
|
||||
uint32_t ack_deadline_ms; // millis() by which a pending ACK must arrive
|
||||
// Sender-perspective message timestamp: the send timestamp for outgoing
|
||||
// (reused verbatim on resend so the recipient treats it as a retry), or the
|
||||
// sender_timestamp for incoming (used to dedup retried copies). 0 = unknown.
|
||||
uint32_t msg_ts;
|
||||
uint8_t attempt; // last attempt number sent (outgoing); next resend = attempt+1
|
||||
uint8_t resends_left; // remaining auto-resends before the marker shows ✗
|
||||
};
|
||||
static const int DM_HIST_MAX = 64;
|
||||
DmHistEntry _dm_hist[DM_HIST_MAX];
|
||||
@@ -304,8 +312,11 @@ class QuickMsgScreen : public UIScreen {
|
||||
|
||||
// ack_tag != 0 marks an outgoing DM as awaiting an end-to-end ACK by
|
||||
// ack_deadline_ms; 0 means "sent, no confirmation possible" (no path / incoming).
|
||||
// msg_ts = sender-perspective timestamp (send ts for outgoing / sender_timestamp
|
||||
// for incoming); resends = remaining auto-resends for an outgoing pending DM.
|
||||
void storeDMMsg(const uint8_t* pub_key, bool outgoing, const char* text,
|
||||
uint32_t ack_tag = 0, uint32_t ack_deadline_ms = 0) {
|
||||
uint32_t ack_tag = 0, uint32_t ack_deadline_ms = 0,
|
||||
uint32_t msg_ts = 0, uint8_t resends = 0) {
|
||||
int pos;
|
||||
if (_dm_hist_count < DM_HIST_MAX) {
|
||||
pos = (_dm_hist_head + _dm_hist_count) % DM_HIST_MAX;
|
||||
@@ -322,12 +333,18 @@ class QuickMsgScreen : public UIScreen {
|
||||
_dm_hist[pos].ack_status = (outgoing && ack_tag) ? ACK_PENDING : ACK_NONE;
|
||||
_dm_hist[pos].ack_tag = ack_tag;
|
||||
_dm_hist[pos].ack_deadline_ms = ack_deadline_ms;
|
||||
_dm_hist[pos].msg_ts = msg_ts;
|
||||
_dm_hist[pos].attempt = 0;
|
||||
_dm_hist[pos].resends_left = (outgoing && ack_tag) ? resends : 0;
|
||||
}
|
||||
|
||||
// Effective status for display: a pending ACK past its deadline reads as
|
||||
// failed (no separate timer needed — evaluated lazily at render time).
|
||||
// Effective status for display. A pending ACK only reads as failed once its
|
||||
// deadline has passed AND no auto-resends remain — while resends_left > 0 the
|
||||
// entry stays pending (tickDmResends() retries / finalises it). Safety net for
|
||||
// when the tick hasn't run yet; the tick is the authority that writes ACK_FAIL.
|
||||
AckState dmEffectiveStatus(const DmHistEntry& e) const {
|
||||
if (e.ack_status == ACK_PENDING && (int32_t)(millis() - e.ack_deadline_ms) >= 0)
|
||||
if (e.ack_status == ACK_PENDING && e.resends_left == 0 &&
|
||||
(int32_t)(millis() - e.ack_deadline_ms) >= 0)
|
||||
return ACK_FAIL;
|
||||
return (AckState)e.ack_status;
|
||||
}
|
||||
@@ -374,7 +391,10 @@ class QuickMsgScreen : public UIScreen {
|
||||
_viewing_max_seen = 0;
|
||||
_task->showAlert("Sent!", 600);
|
||||
} else if (ok) {
|
||||
storeDMMsg(_sel_contact.id.pub_key, true, msg, _last_ack_tag, _last_ack_deadline_ms);
|
||||
NodePrefs* np = _task->getNodePrefs();
|
||||
uint8_t resends = np ? np->dm_resend_count : 0;
|
||||
storeDMMsg(_sel_contact.id.pub_key, true, msg, _last_ack_tag,
|
||||
_last_ack_deadline_ms, _last_send_ts, resends);
|
||||
_dm_hist_sel = 0;
|
||||
_dm_hist_scroll = 0;
|
||||
_phase = DM_HIST;
|
||||
@@ -388,16 +408,19 @@ class QuickMsgScreen : public UIScreen {
|
||||
bool sendText(const char* msg) {
|
||||
_last_ack_tag = 0;
|
||||
_last_ack_deadline_ms = 0;
|
||||
_last_send_ts = 0;
|
||||
if (_sending_to_channel) {
|
||||
ChannelDetails ch;
|
||||
if (!the_mesh.getChannel(_sel_channel_idx, ch)) return false;
|
||||
return the_mesh.sendGroupMessage(rtc_clock.getCurrentTime(), ch.channel,
|
||||
the_mesh.getNodeName(), msg, strlen(msg));
|
||||
} else {
|
||||
uint32_t send_ts = rtc_clock.getCurrentTime();
|
||||
uint32_t expected_ack = 0, est_timeout = 0;
|
||||
bool ok = the_mesh.sendMessage(_sel_contact, rtc_clock.getCurrentTime(), 0,
|
||||
bool ok = the_mesh.sendMessage(_sel_contact, send_ts, 0,
|
||||
msg, expected_ack, est_timeout) > 0;
|
||||
if (ok && expected_ack) {
|
||||
_last_send_ts = send_ts;
|
||||
_last_ack_tag = expected_ack;
|
||||
// Generous margin over the base estimate so a slow multi-hop ACK isn't
|
||||
// prematurely shown as failed.
|
||||
@@ -627,8 +650,20 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) {
|
||||
storeDMMsg(pub_key, outgoing, text);
|
||||
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text,
|
||||
uint32_t sender_timestamp = 0) {
|
||||
// Drop retried copies of an incoming DM: a resend reuses the sender's
|
||||
// timestamp and text but carries a fresh packet hash, so the mesh dup-filter
|
||||
// lets it through. Match on prefix + sender_timestamp + text to suppress it.
|
||||
if (!outgoing && sender_timestamp != 0) {
|
||||
for (int i = 0; i < _dm_hist_count; i++) {
|
||||
const DmHistEntry& e = _dm_hist[(_dm_hist_head + i) % DM_HIST_MAX];
|
||||
if (!e.outgoing && e.msg_ts == sender_timestamp &&
|
||||
memcmp(e.prefix, pub_key, 4) == 0 && strcmp(e.text, text) == 0)
|
||||
return; // duplicate retry — already in history
|
||||
}
|
||||
}
|
||||
storeDMMsg(pub_key, outgoing, text, 0, 0, outgoing ? 0 : sender_timestamp, 0);
|
||||
}
|
||||
|
||||
// Called when an end-to-end ACK arrives (routed from MyMesh::onAckRecv).
|
||||
@@ -644,6 +679,46 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// Look up a contact by 4-byte pub_key prefix (as stored in DmHistEntry).
|
||||
bool contactByPrefix(const uint8_t* prefix, ContactInfo& out) const {
|
||||
int total = the_mesh.getNumContacts();
|
||||
for (int i = 0; i < total; i++) {
|
||||
ContactInfo c;
|
||||
if (the_mesh.getContactByIdx(i, c) && memcmp(c.id.pub_key, prefix, 4) == 0) {
|
||||
out = c;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Background tick (called every UI loop, regardless of the active screen) that
|
||||
// drives auto-resend of on-device DMs. When a pending DM passes its deadline
|
||||
// with no ACK: resend with the next attempt# (reusing the original timestamp so
|
||||
// the recipient dedups) while resends remain, else mark it failed (✗).
|
||||
void tickDmResends() {
|
||||
uint32_t now = millis();
|
||||
for (int i = 0; i < _dm_hist_count; i++) {
|
||||
DmHistEntry& e = _dm_hist[(_dm_hist_head + i) % DM_HIST_MAX];
|
||||
if (!e.outgoing || e.ack_status != ACK_PENDING) continue;
|
||||
if ((int32_t)(now - e.ack_deadline_ms) < 0) continue; // still waiting
|
||||
if (e.resends_left == 0) { e.ack_status = ACK_FAIL; continue; }
|
||||
ContactInfo c;
|
||||
if (!contactByPrefix(e.prefix, c)) { e.ack_status = ACK_FAIL; continue; }
|
||||
uint32_t expected_ack = 0, est_timeout = 0;
|
||||
uint8_t next_attempt = e.attempt + 1;
|
||||
if (the_mesh.sendMessage(c, e.msg_ts, next_attempt, e.text,
|
||||
expected_ack, est_timeout) > 0 && expected_ack) {
|
||||
e.attempt = next_attempt;
|
||||
e.ack_tag = expected_ack; // each attempt has a distinct ACK CRC
|
||||
e.ack_deadline_ms = now + est_timeout + 4000;
|
||||
e.resends_left--;
|
||||
} else {
|
||||
e.ack_status = ACK_FAIL; // couldn't compose/send — give up
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int getDMUnreadTotal() const {
|
||||
return _task->getDMUnreadTotal();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user