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.type = MessageType::DM_TO_US;
}
// Outgoing messages start as UNKNOWN until ACK/NACK arrives
sm.ackStatus = AckStatus::UNKNOWN;
} else {
// Normal incoming
sm.sender = packet.from;
@ -72,6 +75,9 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
sm.dest = NODENUM_BROADCAST; // fallback
sm.type = MessageType::BROADCAST;
}
// Received messages dont wait for ACK mark as ACKED
sm.ackStatus = AckStatus::ACKED;
}
addLiveMessage(sm);
@ -103,6 +109,9 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st
sm.dest = NODENUM_BROADCAST;
sm.type = MessageType::BROADCAST;
// Manual/outgoing messages start as UNKNOWN until ACK/NACK arrives
sm.ackStatus = AckStatus::UNKNOWN;
addLiveMessage(sm);
}
@ -131,8 +140,12 @@ void MessageStore::saveToFlash()
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('\0'); // null terminator
uint8_t bootFlag = m.isBootRelative ? 1 : 0;
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();
@ -189,6 +202,18 @@ void MessageStore::loadFromFlash()
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
if (m.dest == NODENUM_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
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 {
uint32_t timestamp; // When message was created (secs since boot/RTC)
uint32_t sender; // NodeNum of sender
@ -29,10 +37,13 @@ struct StoredMessage {
// (true = millis()/1000 fallback, false = epoch/RTC absolute)
bool isBootRelative;
// Delivery status (only meaningful for our own sent messages)
AckStatus ackStatus;
// Default constructor to initialize all fields safely
StoredMessage()
: 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;
}
// 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)
{
// 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<bool> isMine; // track alignment
std::vector<bool> isHeader; // track header lines
std::vector<AckStatus> ackForLine;
for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) {
const auto &m = *it;
@ -417,6 +445,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
allLines.push_back(std::string(headerStr));
isMine.push_back(mine);
isHeader.push_back(true);
ackForLine.push_back(m.ackStatus);
// Split message text into wrapped lines
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);
isMine.push_back(mine);
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;
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
int underlineY = lineY + FONT_HEIGHT_SMALL;
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.type = MessageType::DM_TO_US;
}
sm.ackStatus = AckStatus::UNKNOWN;
messageStore.addLiveMessage(sm);
@ -2185,6 +2186,20 @@ ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &
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
this->lastRxRssi = mp.rx_rssi;
this->lastRxSnr = mp.rx_snr;