mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-08 17:52:44 +02:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -12,6 +12,15 @@
|
||||
#include <SD.h>
|
||||
#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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user