From 4f28d22217a143d4bcf1a1aaad9eb95c0a4eb3b8 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:11:06 +1000 Subject: [PATCH] Add device-side DM retries with Sending x/N counter, delivered tick and Failed status in conversation view. Tracked DM sends now retry like the app: 3 flood attempts with no path set, or 5 with a path (4 direct, then path reset and a final flood). Each attempt waits its own hop-aware est_timeout. Delivered resolves on the recipient's ack for any attempt and draws a tick after the message prefix; Failed shows once all attempts are exhausted. Status is session-only: SD message format unchanged at v4. Wired for keyboard, touch, CardKB and T-Watch compose plus channel share. BLE app sends and the voice envelope path are untouched. --- examples/companion_radio/AbstractUITask.h | 13 ++ examples/companion_radio/MyMesh.cpp | 124 +++++++++++++++++- examples/companion_radio/MyMesh.h | 29 +++- examples/companion_radio/main.cpp | 21 ++- .../companion_radio/ui-new/ChannelScreen.h | 80 ++++++++++- examples/companion_radio/ui-new/UITask.cpp | 28 +++- examples/companion_radio/ui-new/UITask.h | 5 +- 7 files changed, 284 insertions(+), 16 deletions(-) diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 18d2c703..21020c0b 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -22,6 +22,15 @@ enum class UIEventType { ack }; +// DM send status codes (device-side tracked sends, pushed via dmSendStatus()). +// Guarded so ChannelScreen.h can carry the same definitions without a clash. +#ifndef DM_SEND_NONE +#define DM_SEND_NONE 0 +#define DM_SEND_SENDING 1 +#define DM_SEND_DELIVERED 2 +#define DM_SEND_FAILED 3 +#endif + class AbstractUITask { protected: mesh::MainBoard* _board; @@ -50,6 +59,10 @@ public: virtual void forceRefresh() {} virtual void addSentChannelMessage(uint8_t channel_idx, const char* sender, const char* text) {} + // Device-side tracked DM send status (from MyMesh retry engine). + // status is one of DM_SEND_*; attempt/total describe the retry counter. + virtual void dmSendStatus(uint32_t send_ref, uint8_t status, uint8_t attempt, uint8_t total) {} + // Mark a channel as read when BLE companion app syncs a message virtual void markChannelReadFromBLE(uint8_t channel_idx) {} virtual void markAllChannelsRead() {} // Companion builds: zero all unread on app connect diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 4a03b313..987e6fdf 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -510,7 +510,31 @@ void MyMesh::onContactPathUpdated(const ContactInfo &contact) { dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); } +// Resolve a device-side tracked DM send when any of its attempts' acks +// arrives. Checked independently of expected_ack_table so a circular-table +// overwrite under load cannot strand a pending send. +void MyMesh::resolvePendingDMSend(uint32_t ack) { + if (ack == 0) return; + for (int i = 0; i < MAX_PENDING_DM_SENDS; i++) { + PendingDMSend& p = pending_dm[i]; + if (!p.active) continue; + for (int a = 0; a < p.attempt && a < 5; a++) { + if (p.acks[a] != ack) continue; + p.active = false; + MESH_DEBUG_PRINTLN("UI: DM delivered (attempt %d/%d), ref=0x%08X", + p.attempt, p.total, p.send_ref); + if (_ui) _ui->dmSendStatus(p.send_ref, DM_SEND_DELIVERED, p.attempt, p.total); + break; + } + } +} + ContactInfo* MyMesh::processAck(const uint8_t *data) { + { + uint32_t ack; + memcpy(&ack, data, 4); + resolvePendingDMSend(ack); + } // see if matches any in a table for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) { if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient @@ -946,13 +970,21 @@ void MyMesh::queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, co } } -bool MyMesh::uiSendDirectMessage(uint32_t contact_idx, const char* text) { +bool MyMesh::uiSendDirectMessage(uint32_t contact_idx, const char* text, + uint32_t* out_send_ref, uint8_t* out_total) { + if (out_send_ref) *out_send_ref = 0; + if (out_total) *out_total = 0; + ContactInfo contact; if (!getContactByIdx(contact_idx, contact)) return false; ContactInfo* recipient = lookupContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE); if (!recipient) return false; + // Plan the attempt count from the path state at first transmit: + // no path -> 3 flood attempts, path set -> 4 direct then 1 flood after reset + uint8_t total = (recipient->out_path_len == OUT_PATH_UNKNOWN) ? 3 : 5; + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); uint32_t expected_ack, est_timeout; int result = sendMessage(*recipient, timestamp, 0, text, expected_ack, est_timeout); @@ -970,6 +1002,29 @@ bool MyMesh::uiSendDirectMessage(uint32_t contact_idx, const char* text) { next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE; } + // Tracked send: allocate a retry slot so the message gets app-style + // retries and DM_SEND_* status pushes. If the table is full the send + // stays single-shot and the caller shows no status (send_ref stays 0). + if (out_send_ref) { + for (int i = 0; i < MAX_PENDING_DM_SENDS; i++) { + if (pending_dm[i].active) continue; + PendingDMSend& p = pending_dm[i]; + memset(&p, 0, sizeof(p)); + p.active = true; + p.attempt = 1; + p.total = total; + memcpy(p.contact_pub, recipient->id.pub_key, PUB_KEY_SIZE); + p.timestamp = timestamp; + p.send_ref = expected_ack ? expected_ack : 1; + p.acks[0] = expected_ack; + p.deadline = futureMillis(est_timeout); + StrHelper::strncpy(p.text, text, sizeof(p.text)); + *out_send_ref = p.send_ref; + if (out_total) *out_total = p.total; + break; + } + } + MESH_DEBUG_PRINTLN("UI: DM sent to %s (%s), ack=0x%08X timeout=%dms", recipient->name, result == MSG_SEND_SENT_FLOOD ? "flood" : "direct", expected_ack, est_timeout); @@ -1506,6 +1561,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe app_target_ver = 0; clearPendingReqs(); next_ack_idx = 0; + memset(pending_dm, 0, sizeof(pending_dm)); sign_data = NULL; dirty_contacts_expiry = 0; _nextContactSaveDue = 0; @@ -3701,9 +3757,75 @@ void MyMesh::checkSerialInterface() { } } +// Retry engine for device-side tracked DM sends. Each pass: any pending send +// whose current attempt window (per-attempt est_timeout, hop-aware for direct) +// has elapsed either resends with the attempt counter bumped, or is marked +// failed once its planned attempts are exhausted. Before the final attempt of +// a 5-attempt (path set) plan, the path is reset so the last try goes flood. +void MyMesh::sweepPendingDMSends() { + for (int i = 0; i < MAX_PENDING_DM_SENDS; i++) { + PendingDMSend& p = pending_dm[i]; + if (!p.active) continue; + if (!millisHasNowPassed(p.deadline)) continue; + + if (p.attempt >= p.total) { + // All attempts exhausted without an ack + p.active = false; + MESH_DEBUG_PRINTLN("UI: DM failed after %d attempts, ref=0x%08X", p.attempt, p.send_ref); + if (_ui) _ui->dmSendStatus(p.send_ref, DM_SEND_FAILED, p.attempt, p.total); + continue; + } + + ContactInfo* recipient = lookupContactByPubKey(p.contact_pub, PUB_KEY_SIZE); + if (recipient == NULL) { + // Contact removed mid-flight -- nothing left to send to + p.active = false; + if (_ui) _ui->dmSendStatus(p.send_ref, DM_SEND_FAILED, p.attempt, p.total); + continue; + } + + uint8_t next_attempt = p.attempt + 1; + if (p.total == 5 && next_attempt == p.total) { + resetPathTo(*recipient); // final attempt: drop the set path, go flood + } + + uint32_t expected_ack, est_timeout; + int result = sendMessage(*recipient, p.timestamp, next_attempt - 1, p.text, + expected_ack, est_timeout); + if (result == MSG_SEND_FAILED) { + if (next_attempt >= p.total) { + // Final attempt never left the radio -- give up now + p.active = false; + if (_ui) _ui->dmSendStatus(p.send_ref, DM_SEND_FAILED, p.attempt, p.total); + } else { + p.deadline = futureMillis(2000); // packet pool busy -- retry this attempt shortly + } + continue; + } + + p.attempt = next_attempt; + p.acks[next_attempt - 1] = expected_ack; + p.deadline = futureMillis(est_timeout); + + if (expected_ack) { + expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); + expected_ack_table[next_ack_idx].ack = expected_ack; + expected_ack_table[next_ack_idx].contact = recipient; + next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE; + } + + MESH_DEBUG_PRINTLN("UI: DM retry %d/%d to %s (%s)", p.attempt, p.total, recipient->name, + result == MSG_SEND_SENT_FLOOD ? "flood" : "direct"); + if (_ui) _ui->dmSendStatus(p.send_ref, DM_SEND_SENDING, p.attempt, p.total); + } +} + void MyMesh::loop() { BaseChatMesh::loop(); + // Device-side DM retry engine + sweepPendingDMSends(); + // Always check USB serial for text CLI commands (independent of BLE) checkCLIRescueCmd(); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index c064bd42..6606379a 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -178,7 +178,11 @@ public: void queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* sender, const char* text); // Send a direct message from the UI (no BLE dependency) - bool uiSendDirectMessage(uint32_t contact_idx, const char* text); + // Pass out_send_ref to opt into a tracked send with app-style retries and + // dmSendStatus() pushes; out_total receives the planned attempt count. + // Callers that leave both NULL get the original single-transmit behaviour. + bool uiSendDirectMessage(uint32_t contact_idx, const char* text, + uint32_t* out_send_ref = NULL, uint8_t* out_total = NULL); // Send raw binary data to a contact (PAYLOAD_TYPE_RAW_CUSTOM, direct route only) // Used for dz0ny VE3 voice protocol: voice packets (0x56) and fetch requests (0x72) @@ -384,6 +388,29 @@ private: AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table int next_ack_idx; + // Device-side tracked DM sends with app-style retries: + // no path set -> 3 attempts, all flood + // path set -> 5 attempts: 4 direct on the set path, then path reset + // and a final flood attempt + // Each attempt is a real transmit with the attempt counter bumped, so each + // has its own expected ack (the attempt bits are inside the ack hash). + // Session only -- a reboot abandons any in-flight sequence. + #define MAX_PENDING_DM_SENDS 4 + struct PendingDMSend { + bool active; + uint8_t attempt; // attempts sent so far (1-based) + uint8_t total; // planned attempts (3 or 5) + uint8_t contact_pub[PUB_KEY_SIZE]; // recipient (idx can shift, pub key cannot) + uint32_t timestamp; // original msg timestamp, reused per attempt + uint32_t send_ref; // UI handle (= attempt-1 expected ack) + uint32_t acks[5]; // expected ack per attempt + unsigned long deadline; // futureMillis() for the current attempt + char text[MAX_TEXT_LEN + 1]; // raw text kept for resends + }; + PendingDMSend pending_dm[MAX_PENDING_DM_SENDS]; + void sweepPendingDMSends(); + void resolvePendingDMSend(uint32_t ack); + #ifndef ADVERT_PATH_TABLE_SIZE #define ADVERT_PATH_TABLE_SIZE 1000 #endif diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index bb978f53..7b967b50 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -5139,11 +5139,14 @@ void handleKeyboardInput() { snprintf(shareMsg, sizeof(shareMsg), "%s%s|%s", MECK_CH_PREFIX, ch.name, hexSecret); - if (the_mesh.uiSendDirectMessage((uint32_t)contactIdx, shareMsg)) { + uint32_t sendRef = 0; + uint8_t sendTotal = 0; + if (the_mesh.uiSendDirectMessage((uint32_t)contactIdx, shareMsg, &sendRef, &sendTotal)) { // Add sanitised version to DM conversation view char displayMsg[64]; snprintf(displayMsg, sizeof(displayMsg), "Shared channel: %s", ch.name); - ui_task.addSentDM(contact.name, the_mesh.getNodePrefs()->node_name, displayMsg); + ui_task.addSentDM(contact.name, the_mesh.getNodePrefs()->node_name, displayMsg, + sendRef, sendTotal); char alertBuf[48]; snprintf(alertBuf, sizeof(alertBuf), "Shared with %s", contact.name); @@ -6355,9 +6358,12 @@ void sendComposedMessage() { if (composeDM) { // Direct message to a specific contact if (composeDMContactIdx >= 0) { - if (the_mesh.uiSendDirectMessage((uint32_t)composeDMContactIdx, utf8Buf)) { + uint32_t sendRef = 0; + uint8_t sendTotal = 0; + if (the_mesh.uiSendDirectMessage((uint32_t)composeDMContactIdx, utf8Buf, &sendRef, &sendTotal)) { // Add to channel screen so sent DM appears in conversation view - ui_task.addSentDM(composeDMName, the_mesh.getNodePrefs()->node_name, utf8Buf); + ui_task.addSentDM(composeDMName, the_mesh.getNodePrefs()->node_name, utf8Buf, + sendRef, sendTotal); ui_task.showAlert("DM sent!", 1500); } else { ui_task.showAlert("DM failed!", 1500); @@ -6539,8 +6545,11 @@ void sendCardKBMessage() { if (ckbComposeDM) { // Direct message if (ckbComposeDMIdx >= 0) { - if (the_mesh.uiSendDirectMessage((uint32_t)ckbComposeDMIdx, ckbComposeBuf)) { - ui_task.addSentDM(ckbComposeDMName, the_mesh.getNodePrefs()->node_name, ckbComposeBuf); + uint32_t sendRef = 0; + uint8_t sendTotal = 0; + if (the_mesh.uiSendDirectMessage((uint32_t)ckbComposeDMIdx, ckbComposeBuf, &sendRef, &sendTotal)) { + ui_task.addSentDM(ckbComposeDMName, the_mesh.getNodePrefs()->node_name, ckbComposeBuf, + sendRef, sendTotal); ui_task.showAlert("DM sent!", 1500); } else { ui_task.showAlert("DM failed!", 1500); diff --git a/examples/companion_radio/ui-new/ChannelScreen.h b/examples/companion_radio/ui-new/ChannelScreen.h index 8d0107e1..2d96f872 100644 --- a/examples/companion_radio/ui-new/ChannelScreen.h +++ b/examples/companion_radio/ui-new/ChannelScreen.h @@ -12,6 +12,15 @@ #include #endif +// DM send status codes (session only). Guarded so AbstractUITask.h can carry +// the same definitions without a clash. +#ifndef DM_SEND_NONE +#define DM_SEND_NONE 0 +#define DM_SEND_SENDING 1 +#define DM_SEND_DELIVERED 2 +#define DM_SEND_FAILED 3 +#endif + // Maximum messages to store in history #ifndef CHANNEL_MSG_HISTORY_SIZE #define CHANNEL_MSG_HISTORY_SIZE 300 @@ -69,6 +78,11 @@ public: char text[CHANNEL_MSG_TEXT_LEN]; bool valid; uint8_t scope_idx; // Region scope index for display (session only, 0xFF = unscoped). Not persisted. + // --- DM send status (session only). Not persisted to SD. --- + uint32_t send_ref; // Tracked-send handle from MyMesh (0 = untracked) + uint8_t dm_status; // DM_SEND_NONE / SENDING / DELIVERED / FAILED + uint8_t dm_attempt; // attempts sent so far (for "Sending x/N") + uint8_t dm_total; // planned attempts (3 or 5) }; // Simple hash for DM peer matching @@ -78,6 +92,23 @@ public: return h; } + // Draw a small tick (check mark) for delivered DMs using fillRect steps. + // DisplayDriver has no line primitive and the GFX fonts carry no tick + // glyph (they cover ASCII 0x20-0x7E only), so the glyph is drawn by hand. + // Inherits the current draw colour. Sized from the line height so it + // matches both the 6x8 built-in font and the 9pt faces. + static void drawDeliveredTick(DisplayDriver& display, int x, int y, int lineH) { + int s = (lineH >= 11) ? 2 : 1; // stroke thickness by font size + int baseY = y + lineH - 2 * s; // near the text baseline + // short down-stroke + display.fillRect(x, baseY - s, s, s); + display.fillRect(x + s, baseY, s, s); + // long up-stroke + display.fillRect(x + 2 * s, baseY - s, s, s); + display.fillRect(x + 3 * s, baseY - 2 * s, s, s); + display.fillRect(x + 4 * s, baseY - 3 * s, s, s); + } + private: UITask* _task; mesh::RTCClock* _rtc; @@ -141,6 +172,10 @@ public: _messages[i].dm_peer_hash = 0; memset(_messages[i].path, 0, MSG_PATH_MAX); _messages[i].scope_idx = 0xFF; + _messages[i].send_ref = 0; + _messages[i].dm_status = DM_SEND_NONE; + _messages[i].dm_attempt = 0; + _messages[i].dm_total = 0; } // Initialize unread counts memset(_unread, 0, sizeof(_unread)); @@ -153,7 +188,8 @@ public: // suppressUnread: if true, do not increment the unread counter for this message void addMessage(uint8_t channel_idx, uint8_t path_len, const char* sender, const char* text, const uint8_t* path_bytes = nullptr, int8_t snr = 0, const char* peer_name = nullptr, - bool suppressUnread = false, uint8_t scope_idx = 0xFF) { + bool suppressUnread = false, uint8_t scope_idx = 0xFF, + uint32_t send_ref = 0, uint8_t send_total = 0) { // Move to next slot in circular buffer _newestIdx = (_newestIdx + 1) % CHANNEL_MSG_HISTORY_SIZE; @@ -164,6 +200,12 @@ public: msg->snr = snr; msg->valid = true; msg->scope_idx = scope_idx; + // Tracked send: attempt 1 has already been transmitted by the time the + // local echo is added, so the initial state is "Sending 1/N" + msg->send_ref = send_ref; + msg->dm_status = send_ref ? DM_SEND_SENDING : DM_SEND_NONE; + msg->dm_attempt = send_ref ? 1 : 0; + msg->dm_total = send_total; // Set DM peer hash for conversation filtering if (channel_idx == 0xFF) { @@ -472,6 +514,21 @@ public: } // ----------------------------------------------------------------------- + // Update the send status of a tracked sent DM (from MyMesh via UITask). + // Returns true if a message with this send_ref was found and updated. + bool setSendStatus(uint32_t send_ref, uint8_t status, uint8_t attempt, uint8_t total) { + if (send_ref == 0) return false; + for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) { + ChannelMessage* m = &_messages[i]; + if (!m->valid || m->send_ref != send_ref) continue; + m->dm_status = status; + m->dm_attempt = attempt; + m->dm_total = total; + return true; + } + return false; + } + // Per-channel history deletion // ----------------------------------------------------------------------- @@ -617,6 +674,11 @@ public: memcpy(_messages[i].path, rec.path, MSG_PATH_MAX); memcpy(_messages[i].text, rec.text, CHANNEL_MSG_TEXT_LEN); _messages[i].scope_idx = 0xFF; // region scope is session-only, not stored on SD + // DM send status is session-only, not stored on SD + _messages[i].send_ref = 0; + _messages[i].dm_status = DM_SEND_NONE; + _messages[i].dm_attempt = 0; + _messages[i].dm_total = 0; if (_messages[i].valid) loaded++; } @@ -1326,6 +1388,12 @@ public: } else { sprintf(tmp, ">%dd ", age / 86400); } + } else if (msg->dm_status == DM_SEND_SENDING) { + // Tracked sent DM still in flight -- show the attempt counter + sprintf(tmp, "Sending %d/%d ", msg->dm_attempt, msg->dm_total); + } else if (msg->dm_status == DM_SEND_FAILED) { + // All attempts exhausted without an ack from the recipient + sprintf(tmp, "Failed "); } else { int hopsDisp = (msg->path_len == 0xFF) ? 0 : (msg->path_len & 63); // Byte mode: flood packets encode it in the upper bits of path_len. @@ -1344,7 +1412,17 @@ public: sprintf(tmp, "(%dh)(%db) %dd ", hopsDisp, bphDisp, age / 86400); } } + // Delivered: keep the standard prefix and draw a tick in a reserved + // gap straight after it (before the message text) + int statusTickX = -1; + if (!isSelected && msg->dm_status == DM_SEND_DELIVERED) { + statusTickX = display.getTextWidth(tmp); + strcat(tmp, " "); + } display.print(tmp); + if (statusTickX >= 0) { + drawDeliveredTick(display, statusTickX + 1, y, lineHeight); + } // DO NOT advance y - message text continues on the same line // Message text with character wrapping and inline emoji support diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 90bb4681..b8a15cfd 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2586,10 +2586,14 @@ if (curr) curr->poll(); setCurrScreen(tw_channel); } else if (purpose == TWatchKeyboardScreen::TWKB_DM) { bool dmSuccess = false; - if (strlen(sendText) > 0 && the_mesh.uiSendDirectMessage((uint32_t)ctxIdx, sendText)) { + uint32_t sendRef = 0; + uint8_t sendTotal = 0; + if (strlen(sendText) > 0 && + the_mesh.uiSendDirectMessage((uint32_t)ctxIdx, sendText, &sendRef, &sendTotal)) { ContactInfo dmRecipient; if (the_mesh.getContactByIdx(ctxIdx, dmRecipient)) { - addSentDM(dmRecipient.name, the_mesh.getNodePrefs()->node_name, sendText); + addSentDM(dmRecipient.name, the_mesh.getNodePrefs()->node_name, sendText, + sendRef, sendTotal); } dmSuccess = true; } @@ -3172,11 +3176,14 @@ void UITask::onVKBSubmit() { if (strlen(text) == 0) break; bool dmSuccess = false; - if (the_mesh.uiSendDirectMessage((uint32_t)idx, text)) { + uint32_t sendRef = 0; + uint8_t sendTotal = 0; + if (the_mesh.uiSendDirectMessage((uint32_t)idx, text, &sendRef, &sendTotal)) { // Add to channel screen so sent DM appears in conversation view ContactInfo dmRecipient; if (the_mesh.getContactByIdx(idx, dmRecipient)) { - addSentDM(dmRecipient.name, the_mesh.getNodePrefs()->node_name, text); + addSentDM(dmRecipient.name, the_mesh.getNodePrefs()->node_name, text, + sendRef, sendTotal); } dmSuccess = true; } @@ -3736,12 +3743,21 @@ void UITask::addSentChannelMessage(uint8_t channel_idx, const char* sender, cons ((ChannelScreen *) channel_screen)->addMessage(channel_idx, 0, sender, formattedMsg); } -void UITask::addSentDM(const char* recipientName, const char* sender, const char* text) { +void UITask::addSentDM(const char* recipientName, const char* sender, const char* text, + uint32_t send_ref, uint8_t send_total) { // Format as "Sender: message" and tag with recipient's peer hash char formattedMsg[CHANNEL_MSG_TEXT_LEN]; snprintf(formattedMsg, sizeof(formattedMsg), "%s: %s", sender, text); ((ChannelScreen *) channel_screen)->addMessage(0xFF, 0, sender, formattedMsg, - nullptr, 0, recipientName); + nullptr, 0, recipientName, + false, 0xFF, send_ref, send_total); +} + +void UITask::dmSendStatus(uint32_t send_ref, uint8_t status, uint8_t attempt, uint8_t total) { + if (((ChannelScreen *) channel_screen)->setSendStatus(send_ref, status, attempt, total)) { + // Repaint promptly if the user is looking at the conversation + if (isOnChannelScreen()) forceRefresh(); + } } void UITask::markChannelReadFromBLE(uint8_t channel_idx) { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index e5f911da..66aa025c 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -374,7 +374,10 @@ public: // Add a sent message to the channel screen history void addSentChannelMessage(uint8_t channel_idx, const char* sender, const char* text) override; - void addSentDM(const char* recipientName, const char* sender, const char* text); + void addSentDM(const char* recipientName, const char* sender, const char* text, + uint32_t send_ref = 0, uint8_t send_total = 0); + // DM send status push from the MyMesh retry engine + void dmSendStatus(uint32_t send_ref, uint8_t status, uint8_t attempt, uint8_t total) override; // Mark channel as read when BLE companion app syncs messages void markChannelReadFromBLE(uint8_t channel_idx) override;