mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 23:08:11 +00:00
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:
@@ -44,6 +44,9 @@ public:
|
||||
// An end-to-end ACK (CRC) arrived for one of our sent messages — drives the
|
||||
// DM delivery-status marker. Default no-op for UIs that don't track it.
|
||||
virtual void onMsgAck(uint32_t ack_crc) { (void)ack_crc; }
|
||||
// A repeater rebroadcast of one of our channel sends was heard (seq from
|
||||
// lastChannelRelaySeq()) — drives the channel "relayed into mesh" marker.
|
||||
virtual void onChannelRelayed(uint32_t seq) { (void)seq; }
|
||||
// True only when a BLE central is actually bonded/connected. On a dual
|
||||
// (BLE+USB) interface hasConnection() is always true (USB counts), so use
|
||||
// this for BLE-specific UI like the pairing-PIN prompt.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -201,6 +201,12 @@ protected:
|
||||
void apcSampleSnr(float snr);
|
||||
void apcOnFailure();
|
||||
void apcTrackFloodSend(const mesh::Packet* pkt);
|
||||
void trackRelaySend(const mesh::Packet* pkt); // arm the UI relayed-into-mesh tracker
|
||||
public:
|
||||
// Seq of the most recently tracked channel send — the UI records it on the
|
||||
// outgoing history entry so a heard echo (onChannelRelayed) can match it back.
|
||||
uint32_t lastChannelRelaySeq() const { return _last_relay_seq; }
|
||||
private:
|
||||
|
||||
// DataStoreHost methods
|
||||
bool onContactLoaded(const ContactInfo& contact) override { return addContact(contact); }
|
||||
@@ -291,6 +297,15 @@ private:
|
||||
uint16_t _apc_flood_len; // its payload length — cheap pre-filter before hashing
|
||||
uint32_t _apc_flood_deadline; // echo-wait deadline for that send
|
||||
bool _apc_flood_pending; // a tracked flood send is awaiting its echo
|
||||
// UI "relayed into mesh" tracker for channel sends — independent of APC, single
|
||||
// slot (only the latest channel send is tracked). Hashing on receive only runs
|
||||
// while a send is pending, so the hot flood-recv path is untouched otherwise.
|
||||
uint8_t _relay_hash[MAX_HASH_SIZE];
|
||||
uint16_t _relay_len;
|
||||
uint32_t _relay_deadline;
|
||||
uint32_t _relay_seq; // monotonic id of the pending tracked send
|
||||
uint32_t _last_relay_seq; // seq of the most recent tracked send (for the UI to record)
|
||||
bool _relay_pending;
|
||||
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
|
||||
char cli_command[80];
|
||||
uint8_t app_target_ver;
|
||||
|
||||
@@ -94,7 +94,13 @@ class QuickMsgScreen : public UIScreen {
|
||||
// recipient ACK exists for floods), so a missing echo is NOT shown as failure.
|
||||
enum AckState : uint8_t { ACK_NONE = 0, ACK_PENDING, ACK_OK, ACK_FAIL };
|
||||
|
||||
struct ChHistEntry { uint8_t ch_idx; char text[MSG_TEXT_BUF]; uint32_t timestamp; };
|
||||
struct ChHistEntry {
|
||||
uint8_t ch_idx;
|
||||
char text[MSG_TEXT_BUF];
|
||||
uint32_t timestamp;
|
||||
uint8_t relay_status; // AckState; only PENDING/OK used (no failure for floods)
|
||||
uint32_t relay_seq; // MyMesh relay seq to match against onChannelRelayed()
|
||||
};
|
||||
ChHistEntry _hist[CH_HIST_MAX];
|
||||
int _hist_head, _hist_count;
|
||||
|
||||
@@ -354,7 +360,13 @@ class QuickMsgScreen : public UIScreen {
|
||||
_phase = CHANNEL_HIST; // set before addChannelMsg so viewing=true, no unread bump
|
||||
char entry[sizeof(ChHistEntry::text)];
|
||||
snprintf(entry, sizeof(entry), "Me: %s", msg);
|
||||
addChannelMsg(_sel_channel_idx, entry);
|
||||
int pos = addChannelMsg(_sel_channel_idx, entry);
|
||||
// Arm the "relayed into mesh" marker on this exact entry — MyMesh tracked
|
||||
// the flood it just originated and reports a heard repeater echo by seq.
|
||||
if (pos >= 0) {
|
||||
_hist[pos].relay_status = ACK_PENDING;
|
||||
_hist[pos].relay_seq = the_mesh.lastChannelRelaySeq();
|
||||
}
|
||||
// After inserting sent msg at index 0, the unread index range is stale.
|
||||
// User is active in this channel — treat as fully read.
|
||||
_ch_unread[_sel_channel_idx] = 0;
|
||||
@@ -570,11 +582,13 @@ public:
|
||||
memset(_ch_unread, 0, sizeof(_ch_unread));
|
||||
}
|
||||
|
||||
void addChannelMsg(uint8_t ch_idx, const char* text) {
|
||||
// Returns the ring position the message was stored at, or -1 if rejected, so
|
||||
// the outgoing path can attach a relay seq to that exact entry.
|
||||
int addChannelMsg(uint8_t ch_idx, const char* text) {
|
||||
// Guard against bogus channel indices (e.g. findChannelIdx() returned -1
|
||||
// and was cast to uint8_t → 255). Storing such an entry would burn a ring
|
||||
// slot for a message that no visible channel can ever surface.
|
||||
if (ch_idx >= MAX_GROUP_CHANNELS) return;
|
||||
if (ch_idx >= MAX_GROUP_CHANNELS) return -1;
|
||||
int pos;
|
||||
if (_hist_count < CH_HIST_MAX) {
|
||||
pos = (_hist_head + _hist_count) % CH_HIST_MAX;
|
||||
@@ -593,9 +607,24 @@ public:
|
||||
_hist[pos].timestamp = rtc_clock.getCurrentTime();
|
||||
strncpy(_hist[pos].text, text, sizeof(_hist[pos].text) - 1);
|
||||
_hist[pos].text[sizeof(_hist[pos].text) - 1] = '\0';
|
||||
_hist[pos].relay_status = ACK_NONE;
|
||||
_hist[pos].relay_seq = 0;
|
||||
|
||||
bool viewing = (_phase == CHANNEL_HIST && _sel_channel_idx == (int)ch_idx);
|
||||
if (!viewing && _ch_unread[ch_idx] < 99) _ch_unread[ch_idx]++;
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Called when a repeater echo of one of our channel sends is heard.
|
||||
void markChannelRelayed(uint32_t seq) {
|
||||
if (seq == 0) return;
|
||||
for (int i = 0; i < _hist_count; i++) {
|
||||
ChHistEntry& e = _hist[(_hist_head + i) % CH_HIST_MAX];
|
||||
if (e.relay_status == ACK_PENDING && e.relay_seq == seq) {
|
||||
e.relay_status = ACK_OK;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) {
|
||||
@@ -1073,6 +1102,13 @@ public:
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
}
|
||||
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
|
||||
// Channels have no recipient ACK — only show ✓ once a repeater echo
|
||||
// confirms the send was relayed into the mesh; otherwise no marker
|
||||
// (absence is normal, not a failure).
|
||||
if (strcmp(sender, "Me") == 0 && _hist[ring_pos].relay_status == ACK_OK) {
|
||||
int gx = 3 + display.getTextWidth(sender) + 3;
|
||||
drawAckGlyph(display, gx, y + 1, ACK_OK);
|
||||
}
|
||||
if (age[0]) { display.setCursor(display.width() - age_w, y + 1); display.print(age); }
|
||||
if (!sel) display.setColor(DisplayDriver::LIGHT);
|
||||
if (portrait_expand) {
|
||||
|
||||
@@ -1376,6 +1376,10 @@ void UITask::onMsgAck(uint32_t ack_crc) {
|
||||
((QuickMsgScreen*)quick_msg)->markDmDelivered(ack_crc);
|
||||
}
|
||||
|
||||
void UITask::onChannelRelayed(uint32_t seq) {
|
||||
((QuickMsgScreen*)quick_msg)->markChannelRelayed(seq);
|
||||
}
|
||||
|
||||
void UITask::addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) {
|
||||
((QuickMsgScreen*)quick_msg)->addDMMsg(pub_key, outgoing, text);
|
||||
}
|
||||
|
||||
@@ -173,6 +173,7 @@ public:
|
||||
void addChannelMsg(uint8_t channel_idx, const char* text) override;
|
||||
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) override;
|
||||
void onMsgAck(uint32_t ack_crc) override;
|
||||
void onChannelRelayed(uint32_t seq) override;
|
||||
int getDMUnreadTotal() const;
|
||||
int getMsgCount() const { return _msgcount; }
|
||||
int getChannelUnreadCount() const;
|
||||
|
||||
Reference in New Issue
Block a user