feat(ui): channel "relayed into mesh" marker (✓ on repeater echo)

Channels have no recipient ACK, so true delivery can't be shown. Instead reuse
the heard-repeater-echo idea (same as APC's flood feedback) to mark an outgoing
channel message ✓ once a repeater rebroadcast of it is heard — i.e. it was
relayed into the mesh. No echo is NOT a failure (direct/0-hop neighbours never
echo), so unconfirmed sends simply show no marker.

- MyMesh: single-slot relay tracker independent of APC (trackRelaySend on every
  channel flood; hash-match in filterRecvFloodPacket gated on _relay_pending so
  the hot recv path is untouched otherwise; window expiry in loop just drops it).
  lastChannelRelaySeq() hands the seq to the UI.
- AbstractUITask::onChannelRelayed() -> UITask -> QuickMsgScreen::markChannelRelayed().
- ChHistEntry gains relay_status / relay_seq; afterSend arms the marker on the
  exact stored entry; render shows ✓ only on ACK_OK for our own ("Me:") rows.

Verified: GAT562 (SSD1306) and WioTrackerL1Eink (GxEPD) solo builds compile clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-14 14:18:51 +02:00
co-authored by Claude Sonnet 4.6
parent 219a2bba5c
commit d064a617e0
6 changed files with 94 additions and 4 deletions
+31
View File
@@ -499,6 +499,16 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
apcSampleSnr(packet->getSNR());
}
}
// UI relayed-into-mesh marker: same heard-echo idea, runs regardless of APC.
// Gated on _relay_pending so the hash is only computed inside the send window.
if (_relay_pending && packet->payload_len == _relay_len) {
uint8_t h[MAX_HASH_SIZE];
packet->calculatePacketHash(h);
if (memcmp(h, _relay_hash, MAX_HASH_SIZE) == 0) {
_relay_pending = false;
if (_ui) _ui->onChannelRelayed(_relay_seq);
}
}
// REVISIT: try to determine which Region (from transport_codes[1]) that Sender is indicating for replies/responses
// if unknown, fallback to finding Region from transport_codes[0], the 'scope' used by Sender
return false;
@@ -534,6 +544,7 @@ void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, ui
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
// TODO: have per-channel send_scope
if (_prefs.tx_apc) apcTrackFloodSend(pkt); // listen for a repeater echo to drive APC (channels have no ACK)
trackRelaySend(pkt); // and for the UI "relayed" marker
if (send_unscoped) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped
} else {
@@ -1152,6 +1163,18 @@ void MyMesh::apcTrackFloodSend(const mesh::Packet* pkt) {
_apc_flood_pending = true;
}
// Arm the UI "relayed into mesh" tracker for a channel send (independent of APC).
// A repeater rebroadcast heard within the window = relayed; no echo = simply not
// shown as relayed (NOT a failure — direct/0-hop neighbours never echo).
void MyMesh::trackRelaySend(const mesh::Packet* pkt) {
pkt->calculatePacketHash(_relay_hash);
_relay_len = pkt->payload_len;
_relay_deadline = futureMillis(APC_FLOOD_ECHO_WINDOW_MS);
_relay_seq = (_relay_seq == 0xFFFFFFFFu) ? 1 : _relay_seq + 1; // never 0 (0 = "no relay")
_last_relay_seq = _relay_seq;
_relay_pending = true;
}
void MyMesh::onSendTimeout() {
if (_prefs.tx_apc) apcOnFailure();
}
@@ -1171,6 +1194,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) {
_iter_started = false;
_cli_rescue = false;
_relay_pending = false;
_relay_seq = 0;
_last_relay_seq = 0;
offline_queue_len = 0;
app_target_ver = 0;
_bot_last_dm_reply_ms = 0;
@@ -2651,6 +2677,11 @@ void MyMesh::loop() {
_apc_flood_pending = false;
if (_prefs.tx_apc) apcOnFailure();
}
// UI relay window expired with no echo — just drop it (no echo is not a
// failure for channels; the marker simply stays "sent").
if (_relay_pending && millisHasNowPassed(_relay_deadline)) {
_relay_pending = false;
}
if (_cli_rescue) {
checkCLIRescueCmd();