Ack on messages sent

This commit is contained in:
HarukiToreda
2025-09-26 16:51:09 -04:00
parent 07d3726cde
commit 28502c93c9
4 changed files with 93 additions and 1 deletions

View File

@@ -59,6 +59,9 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
sm.dest = packet.decoded.dest; sm.dest = packet.decoded.dest;
sm.type = MessageType::DM_TO_US; sm.type = MessageType::DM_TO_US;
} }
// Outgoing messages start as UNKNOWN until ACK/NACK arrives
sm.ackStatus = AckStatus::UNKNOWN;
} else { } else {
// Normal incoming // Normal incoming
sm.sender = packet.from; sm.sender = packet.from;
@@ -72,6 +75,9 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
sm.dest = NODENUM_BROADCAST; // fallback sm.dest = NODENUM_BROADCAST; // fallback
sm.type = MessageType::BROADCAST; sm.type = MessageType::BROADCAST;
} }
// Received messages dont wait for ACK mark as ACKED
sm.ackStatus = AckStatus::ACKED;
} }
addLiveMessage(sm); addLiveMessage(sm);
@@ -103,6 +109,9 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st
sm.dest = NODENUM_BROADCAST; sm.dest = NODENUM_BROADCAST;
sm.type = MessageType::BROADCAST; sm.type = MessageType::BROADCAST;
// Manual/outgoing messages start as UNKNOWN until ACK/NACK arrives
sm.ackStatus = AckStatus::UNKNOWN;
addLiveMessage(sm); addLiveMessage(sm);
} }
@@ -131,8 +140,12 @@ void MessageStore::saveToFlash()
f.write((uint8_t *)&m.dest, sizeof(m.dest)); f.write((uint8_t *)&m.dest, sizeof(m.dest));
f.write((uint8_t *)m.text.c_str(), std::min(static_cast<size_t>(MAX_MESSAGE_SIZE), m.text.size())); f.write((uint8_t *)m.text.c_str(), std::min(static_cast<size_t>(MAX_MESSAGE_SIZE), m.text.size()));
f.write('\0'); // null terminator f.write('\0'); // null terminator
uint8_t bootFlag = m.isBootRelative ? 1 : 0; uint8_t bootFlag = m.isBootRelative ? 1 : 0;
f.write(&bootFlag, 1); // persist boot-relative flag f.write(&bootFlag, 1); // persist boot-relative flag
uint8_t statusByte = static_cast<uint8_t>(m.ackStatus);
f.write(&statusByte, 1); // persist ackStatus
} }
spiLock->unlock(); spiLock->unlock();
@@ -189,6 +202,18 @@ void MessageStore::loadFromFlash()
m.isBootRelative = (m.timestamp < 60u * 60u * 24u * 7u); m.isBootRelative = (m.timestamp < 60u * 60u * 24u * 7u);
} }
// Try to read ackStatus (newer format)
if (f.available() > 0) {
uint8_t statusByte = 0;
if (f.readBytes((char *)&statusByte, 1) == 1) {
m.ackStatus = static_cast<AckStatus>(statusByte);
} else {
m.ackStatus = AckStatus::UNKNOWN; // fallback
}
} else {
m.ackStatus = AckStatus::UNKNOWN; // legacy files
}
// Recompute type from dest // Recompute type from dest
if (m.dest == NODENUM_BROADCAST) { if (m.dest == NODENUM_BROADCAST) {
m.type = MessageType::BROADCAST; m.type = MessageType::BROADCAST;

View File

@@ -11,6 +11,14 @@ constexpr size_t MAX_MESSAGE_SIZE = 220; // safe bound for text payload
// Explicit message classification // Explicit message classification
enum class MessageType : uint8_t { BROADCAST = 0, DM_TO_US = 1 }; enum class MessageType : uint8_t { BROADCAST = 0, DM_TO_US = 1 };
// Delivery status for messages we sent
enum class AckStatus : uint8_t {
UNKNOWN = 0, // default (not yet resolved)
ACKED = 1, // got a valid ACK
NACKED = 2, // explicitly failed
TIMEOUT = 3 // no ACK after retry window
};
struct StoredMessage { struct StoredMessage {
uint32_t timestamp; // When message was created (secs since boot/RTC) uint32_t timestamp; // When message was created (secs since boot/RTC)
uint32_t sender; // NodeNum of sender uint32_t sender; // NodeNum of sender
@@ -29,10 +37,13 @@ struct StoredMessage {
// (true = millis()/1000 fallback, false = epoch/RTC absolute) // (true = millis()/1000 fallback, false = epoch/RTC absolute)
bool isBootRelative; bool isBootRelative;
// Delivery status (only meaningful for our own sent messages)
AckStatus ackStatus;
// Default constructor to initialize all fields safely // Default constructor to initialize all fields safely
StoredMessage() StoredMessage()
: timestamp(0), sender(0), channelIndex(0), text(""), dest(0xffffffff), type(MessageType::BROADCAST), : timestamp(0), sender(0), channelIndex(0), text(""), dest(0xffffffff), type(MessageType::BROADCAST),
isBootRelative(false) isBootRelative(false), ackStatus(AckStatus::UNKNOWN)
{ {
} }
}; };

View File

@@ -233,6 +233,33 @@ const std::vector<uint32_t> &getSeenPeers()
return seenPeers; return seenPeers;
} }
// Helpers for drawing status marks
void drawCheckMark(OLEDDisplay *display, int x, int y, int size = 8)
{
int h = size;
int w = size;
// Center mark vertically with the text row
int midY = y + (FONT_HEIGHT_SMALL / 2);
int topY = midY - (h / 2);
display->drawLine(x, topY + h / 2, x + w / 3, topY + h);
display->drawLine(x + w / 3, topY + h, x + w, topY);
}
void drawXMark(OLEDDisplay *display, int x, int y, int size = 8)
{
int h = size;
int w = size;
// Center mark vertically with the text row
int midY = y + (FONT_HEIGHT_SMALL / 2);
int topY = midY - (h / 2);
display->drawLine(x, topY, x + w, topY + h);
display->drawLine(x + w, topY, x, topY + h);
}
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
// Ensure any boot-relative timestamps are upgraded if RTC is valid // Ensure any boot-relative timestamps are upgraded if RTC is valid
@@ -329,6 +356,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
std::vector<std::string> allLines; std::vector<std::string> allLines;
std::vector<bool> isMine; // track alignment std::vector<bool> isMine; // track alignment
std::vector<bool> isHeader; // track header lines std::vector<bool> isHeader; // track header lines
std::vector<AckStatus> ackForLine;
for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) { for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) {
const auto &m = *it; const auto &m = *it;
@@ -417,6 +445,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
allLines.push_back(std::string(headerStr)); allLines.push_back(std::string(headerStr));
isMine.push_back(mine); isMine.push_back(mine);
isHeader.push_back(true); isHeader.push_back(true);
ackForLine.push_back(m.ackStatus);
// Split message text into wrapped lines // Split message text into wrapped lines
std::vector<std::string> wrapped = generateLines(display, "", m.text.c_str(), textWidth); std::vector<std::string> wrapped = generateLines(display, "", m.text.c_str(), textWidth);
@@ -424,6 +453,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
allLines.push_back(ln); allLines.push_back(ln);
isMine.push_back(mine); isMine.push_back(mine);
isHeader.push_back(false); isHeader.push_back(false);
ackForLine.push_back(AckStatus::UNKNOWN);
} }
} }
@@ -484,6 +514,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int headerX = isMine[i] ? (SCREEN_WIDTH - w - 2) : x; int headerX = isMine[i] ? (SCREEN_WIDTH - w - 2) : x;
display->drawString(headerX, lineY, cachedLines[i].c_str()); display->drawString(headerX, lineY, cachedLines[i].c_str());
// Draw ACK/NACK mark for our own messages
if (isMine[i]) {
int markX = headerX - 10;
int markY = lineY;
if (ackForLine[i] == AckStatus::ACKED) {
drawCheckMark(display, markX, markY, 8);
} else if (ackForLine[i] == AckStatus::NACKED || ackForLine[i] == AckStatus::TIMEOUT) {
drawXMark(display, markX, markY, 8);
}
}
// Draw underline just under header text // Draw underline just under header text
int underlineY = lineY + FONT_HEIGHT_SMALL; int underlineY = lineY + FONT_HEIGHT_SMALL;
for (int px = 0; px < w; ++px) { for (int px = 0; px < w; ++px) {

View File

@@ -1008,6 +1008,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
sm.dest = dest; sm.dest = dest;
sm.type = MessageType::DM_TO_US; sm.type = MessageType::DM_TO_US;
} }
sm.ackStatus = AckStatus::UNKNOWN;
messageStore.addLiveMessage(sm); messageStore.addLiveMessage(sm);
@@ -2185,6 +2186,20 @@ ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &
waitingForAck = false; waitingForAck = false;
// Update last sent StoredMessage with ACK/NACK result
if (!messageStore.getMessages().empty()) {
StoredMessage &last = const_cast<StoredMessage &>(messageStore.getMessages().back());
if (last.sender == nodeDB->getNodeNum()) { // only update our own messages
if (this->ack) {
last.ackStatus = AckStatus::ACKED;
} else {
// If error_reason was explicit, you can map to NACKED; otherwise TIMEOUT
last.ackStatus =
(decoded.error_reason == meshtastic_Routing_Error_NONE) ? AckStatus::ACKED : AckStatus::NACKED;
}
}
}
// Capture radio metrics from this ACK/NACK packet // Capture radio metrics from this ACK/NACK packet
this->lastRxRssi = mp.rx_rssi; this->lastRxRssi = mp.rx_rssi;
this->lastRxSnr = mp.rx_snr; this->lastRxSnr = mp.rx_snr;