Node selection optimization for encoder and fix for ACK messages.

This commit is contained in:
HarukiToreda 2025-05-21 21:13:46 -04:00
parent 53d5801790
commit b35fb886e4
3 changed files with 320 additions and 250 deletions

View File

@ -433,6 +433,21 @@ const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0
const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010,
0b01000010, 0b01000010, 0b11111111, 0b00011000};
#define key_symbol_width 8
#define key_symbol_height 8
const uint8_t key_symbol[] PROGMEM = {
0b00000000,
0b00000000,
0b00000110,
0b11111001,
0b10101001,
0b10000110,
0b00000000,
0b00000000
};
#endif
#include "img/icon.xbm"

View File

@ -13,9 +13,9 @@
#include "detect/ScanI2C.h"
#include "input/ScanAndSelect.h"
#include "mesh/generated/meshtastic/cannedmessages.pb.h"
#include "graphics/images.h"
#include "modules/AdminModule.h"
#include "graphics/SharedUIDisplay.h"
#include "main.h" // for cardkb_found
#include "modules/ExternalNotificationModule.h" // for buzzer control
#if !MESHTASTIC_EXCLUDE_GPS
@ -70,6 +70,9 @@ CannedMessageModule::CannedMessageModule()
}
}
bool hasKeyForNode(const meshtastic_NodeInfoLite* node) {
return node && node->has_user && node->user.public_key.size > 0;
}
/**
* @brief Items in array this->messages will be set to be pointing on the right
* starting points of the string this->messageStore
@ -125,22 +128,32 @@ int CannedMessageModule::splitConfiguredMessages()
}
void CannedMessageModule::resetSearch() {
LOG_INFO("Resetting search, restoring full destination list");
updateFilteredNodes(); // Reload all nodes and channels
int previousDestIndex = destIndex;
searchQuery = "";
updateFilteredNodes();
// Adjust scrollIndex so previousDestIndex is still visible
int totalEntries = activeChannelIndices.size() + filteredNodes.size();
this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL;
if (this->visibleRows < 1) this->visibleRows = 1;
int maxScrollIndex = std::max(0, totalEntries - visibleRows);
scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex);
lastUpdateMillis = millis();
requestFocus();
}
void CannedMessageModule::updateFilteredNodes() {
static size_t lastNumMeshNodes = 0; // Track the last known node count
static String lastSearchQuery = ""; // Track last search query
static size_t lastNumMeshNodes = 0;
static String lastSearchQuery = "";
size_t numMeshNodes = nodeDB->getNumMeshNodes();
// If the number of nodes has changed, force an update
bool nodesChanged = (numMeshNodes != lastNumMeshNodes);
lastNumMeshNodes = numMeshNodes;
// Also check if search query changed
// Early exit if nothing changed
if (searchQuery == lastSearchQuery && !nodesChanged) return;
lastSearchQuery = searchQuery;
needsUpdate = false;
@ -148,43 +161,50 @@ void CannedMessageModule::updateFilteredNodes() {
this->activeChannelIndices.clear();
NodeNum myNodeNum = nodeDB->getNodeNum();
String lowerSearchQuery = searchQuery;
lowerSearchQuery.toLowerCase();
for (size_t i = 0; i < numMeshNodes; i++) {
// Preallocate space to reduce reallocation
this->filteredNodes.reserve(numMeshNodes);
for (size_t i = 0; i < numMeshNodes; ++i) {
meshtastic_NodeInfoLite* node = nodeDB->getMeshNodeByIndex(i);
if (!node || node->num == myNodeNum) continue;
String nodeName = node->user.long_name;
String lowerNodeName = nodeName;
String lowerSearchQuery = searchQuery;
const String& nodeName = node->user.long_name;
lowerNodeName.toLowerCase();
lowerSearchQuery.toLowerCase();
if (searchQuery.length() == 0 || lowerNodeName.indexOf(lowerSearchQuery) != -1) {
if (searchQuery.length() == 0) {
this->filteredNodes.push_back({node, sinceLastSeen(node)});
} else {
// Avoid unnecessary lowercase conversion if already matched
String lowerNodeName = nodeName;
lowerNodeName.toLowerCase();
if (lowerNodeName.indexOf(lowerSearchQuery) != -1) {
this->filteredNodes.push_back({node, sinceLastSeen(node)});
}
}
}
// Populate active channels
this->activeChannelIndices.clear();
std::vector<String> seenChannels;
for (uint8_t i = 0; i < channels.getNumChannels(); i++) {
String channelName = channels.getName(i);
if (channelName.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), channelName) == seenChannels.end()) {
seenChannels.reserve(channels.getNumChannels());
for (uint8_t i = 0; i < channels.getNumChannels(); ++i) {
String name = channels.getName(i);
if (name.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), name) == seenChannels.end()) {
this->activeChannelIndices.push_back(i);
seenChannels.push_back(channelName);
seenChannels.push_back(name);
}
}
// Sort nodes by favorite status and last seen time
// Sort by favorite, then last heard
std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry& a, const NodeEntry& b) {
if (a.node->is_favorite != b.node->is_favorite) {
return a.node->is_favorite > b.node->is_favorite; // Favorited nodes first
}
return a.lastHeard < b.lastHeard; // Otherwise, sort by last heard (oldest first)
if (a.node->is_favorite != b.node->is_favorite)
return a.node->is_favorite > b.node->is_favorite;
return a.lastHeard < b.lastHeard;
});
// 🔹 If nodes have changed, refresh the screen
scrollIndex = 0; // Show first result at the top
destIndex = 0; // Highlight the first entry
if (nodesChanged) {
LOG_INFO("Nodes changed, forcing UI refresh.");
screen->forceDisplay();
@ -229,85 +249,63 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
}
static int lastDestIndex = -1; // Cache the last index
bool selectionChanged = false; // Track if UI needs redrawing
bool isUp = event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP);
bool isDown = event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN);
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
if (event->kbchar >= 32 && event->kbchar <= 126) {
this->searchQuery += event->kbchar;
needsUpdate = true;
runOnce(); // <=== Force filtering immediately
return 0;
}
size_t numMeshNodes = this->filteredNodes.size();
int totalEntries = numMeshNodes + this->activeChannelIndices.size();
int columns = 2;
int totalRows = (totalEntries + columns - 1) / columns;
int columns = 1;
int totalRows = totalEntries; // one entry per row now
int maxScrollIndex = std::max(0, totalRows - this->visibleRows);
scrollIndex = std::max(0, std::min(scrollIndex, maxScrollIndex));
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) {
if (this->searchQuery.length() > 0) {
this->searchQuery.remove(this->searchQuery.length() - 1);
needsUpdate = true;
runOnce(); // <=== Ensure filter updates after backspace
}
if (this->searchQuery.length() == 0) {
resetSearch(); // Function to restore all destinations
needsUpdate = false;
}
return 0;
}
bool needsRedraw = false;
// 🔼 UP Navigation in Node Selection
if (isUp) {
if ((this->destIndex / columns) <= scrollIndex) {
if (scrollIndex > 0) {
scrollIndex--;
needsRedraw = true;
if (this->destIndex > 0) {
this->destIndex--;
if ((this->destIndex / columns) < scrollIndex) {
scrollIndex = this->destIndex / columns;
shouldRedraw = true;
} else if ((this->destIndex / columns) >= (scrollIndex + visibleRows)) {
scrollIndex = (this->destIndex / columns) - visibleRows + 1;
shouldRedraw = true;
} else {
shouldRedraw = true; // ✅ allow redraw only once below
}
} else if (this->destIndex >= columns) {
this->destIndex -= columns;
}
}
// 🔽 DOWN Navigation in Node Selection
if (isDown) {
if ((this->destIndex / columns) >= (scrollIndex + this->visibleRows - 1)) {
if (scrollIndex < maxScrollIndex) {
scrollIndex++;
needsRedraw = true;
}
} else if (this->destIndex + columns < totalEntries) {
this->destIndex += columns;
}
}
// ◀ LEFT Navigation (Wrap to previous row OR last row)
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) {
if (this->destIndex % columns == 0) {
if (this->destIndex >= columns) {
this->destIndex = this->destIndex - columns + (columns - 1);
} else {
int lastRowStart = ((totalEntries - 1) / columns) * columns;
this->destIndex = std::min(lastRowStart + (columns - 1), totalEntries - 1);
}
} else {
this->destIndex--;
}
}
// ▶ RIGHT Navigation (Wrap to next row OR first row)
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) {
int nextIndex = this->destIndex + 1;
if ((this->destIndex + 1) % columns == 0 || nextIndex >= totalEntries) {
if (this->destIndex + columns < totalEntries) {
this->destIndex = this->destIndex + columns - (columns - 1);
} else {
this->destIndex = 0;
}
} else {
if (this->destIndex + 1 < totalEntries) {
this->destIndex++;
if ((this->destIndex / columns) >= (scrollIndex + visibleRows)) {
scrollIndex = (this->destIndex / columns) - visibleRows + 1;
shouldRedraw = true;
} else {
shouldRedraw = true;
}
}
}
@ -322,8 +320,9 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
}
}
// Only refresh UI when needed
if (needsRedraw) {
if (shouldRedraw) {
screen->forceDisplay();
shouldRedraw = false;
}
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) {
if (this->destIndex < static_cast<int>(this->activeChannelIndices.size())) {
@ -690,17 +689,24 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
// Prevents the canned message module from regenerating the screen's frameset at unexpected times,
// or raising a UIFrameEvent before another module has the chance
this->waitingForAck = true;
this->lastSentNode = dest;
LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes);
service->sendToMesh(
p, RX_SRC_LOCAL,
true); // send to mesh, cc to phone. Even if there's no phone connected, this stores the message to match ACKs
}
unsigned long lastUpdateMillis = 0;
int32_t CannedMessageModule::runOnce()
{
#define NODE_UPDATE_IDLE_MS 100
#define NODE_UPDATE_ACTIVE_MS 80
unsigned long updateThreshold = (searchQuery.length() > 0) ? NODE_UPDATE_ACTIVE_MS : NODE_UPDATE_IDLE_MS;
if (needsUpdate && millis() - lastUpdateMillis > updateThreshold) {
updateFilteredNodes();
lastUpdateMillis = millis();
}
if (((!moduleConfig.canned_message.enabled) && !CANNED_MESSAGE_MODULE_ENABLE) ||
(this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) {
temporaryMessage = "";
@ -953,7 +959,10 @@ int32_t CannedMessageModule::runOnce()
this->notifyObservers(&e);
return INACTIVATE_AFTER_MS;
}
if (shouldRedraw) {
screen->forceDisplay();
shouldRedraw = false;
}
return INT32_MAX;
}
@ -1249,7 +1258,6 @@ bool CannedMessageModule::interceptingKeyboardInput()
return true;
}
}
#if !HAS_TFT
void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
@ -1258,104 +1266,49 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
// === Draw temporary message if available ===
if (temporaryMessage.length() != 0) {
requestFocus(); // Tell Screen::setFrames to move to our module's frame
LOG_DEBUG("Draw temporary message: %s", temporaryMessage.c_str());
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setFont(FONT_MEDIUM);
display->drawString(display->getWidth() / 2 + x, 0 + y + 12, temporaryMessage);
} else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) {
requestFocus(); // Tell Screen::setFrames to move to our module's frame
EINK_ADD_FRAMEFLAG(display, COSMETIC); // Clean after this popup. Layout makes ghosting particularly obvious
display->setTextAlignment(TEXT_ALIGN_CENTER);
#ifdef USE_EINK
display->setFont(FONT_SMALL); // No chunky text
#else
display->setFont(FONT_MEDIUM); // Chunky text
#endif
String displayString;
display->setTextAlignment(TEXT_ALIGN_CENTER);
if (this->ack) {
displayString = "Delivered to\n%s";
} else {
displayString = "Delivery failed\nto %s";
}
display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, displayString,
cannedMessageModule->getNodeName(this->incoming));
display->setFont(FONT_SMALL);
String snrString = "Last Rx SNR: %f";
String rssiString = "Last Rx RSSI: %d";
// Don't bother drawing snr and rssi for tiny displays
if (display->getHeight() > 100) {
// Original implementation used constants of y = 100 and y = 130. Shrink this if screen is *slightly* small
int16_t snrY = 100;
int16_t rssiY = 130;
// If dislay is *slighly* too small for the original consants, squish up a bit
if (display->getHeight() < rssiY + FONT_HEIGHT_SMALL) {
snrY = display->getHeight() - ((1.5) * FONT_HEIGHT_SMALL);
rssiY = display->getHeight() - ((2.5) * FONT_HEIGHT_SMALL);
return;
}
if (this->ack) {
display->drawStringf(display->getWidth() / 2 + x, snrY + y, buffer, snrString, this->lastRxSnr);
display->drawStringf(display->getWidth() / 2 + x, rssiY + y, buffer, rssiString, this->lastRxRssi);
}
}
} else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) {
// E-Ink: clean the screen *after* this pop-up
EINK_ADD_FRAMEFLAG(display, COSMETIC);
requestFocus(); // Tell Screen::setFrames to move to our module's frame
#ifdef USE_EINK
display->setFont(FONT_SMALL); // No chunky text
#else
display->setFont(FONT_MEDIUM); // Chunky text
#endif
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->drawString(display->getWidth() / 2 + x, 0 + y + 12, "Sending...");
} else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) {
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled.");
} else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
// === Destination Selection ===
if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
requestFocus();
updateFilteredNodes();
display->clear();
display->setColor(WHITE); // Always draw cleanly
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
// === Header ===
int titleY = 2;
String titleText = "Select Destination";
titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]";
display->drawString(display->getWidth() / 2 - display->getStringWidth(titleText) / 2, titleY, titleText);
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->drawString(display->getWidth() / 2, titleY, titleText);
display->setTextAlignment(TEXT_ALIGN_LEFT);
int rowYOffset = titleY + FONT_HEIGHT_SMALL; // Adjusted for search box spacing
// === List Items ===
int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4);
int numActiveChannels = this->activeChannelIndices.size();
int totalEntries = numActiveChannels + this->filteredNodes.size();
int columns = 2;
this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / FONT_HEIGHT_SMALL;
int columns = 1;
this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4);
if (this->visibleRows < 1) this->visibleRows = 1;
// Ensure scrolling within bounds
// === Clamp scrolling ===
if (scrollIndex > totalEntries / columns) scrollIndex = totalEntries / columns;
if (scrollIndex < 0) scrollIndex = 0;
for (int row = 0; row < visibleRows; row++) {
int itemIndex = (scrollIndex + row) * columns;
for (int col = 0; col < columns; col++) {
int itemIndex = scrollIndex + row;
if (itemIndex >= totalEntries) break;
int xOffset = col * (display->getWidth() / columns);
int yOffset = row * FONT_HEIGHT_SMALL + rowYOffset;
int xOffset = 0;
int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset;
String entryText;
// Draw Channels First
@ -1368,103 +1321,184 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
int nodeIndex = itemIndex - numActiveChannels;
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(this->filteredNodes.size())) {
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
entryText = node ? (node->is_favorite ? "* " + String(node->user.long_name) : String(node->user.long_name)) : "?";
if (node) {
entryText = node->is_favorite ? "* " + String(node->user.long_name) : String(node->user.long_name);
bool hasKey = hasKeyForNode(node);
}
}
}
// Prevent Empty Names
if (entryText.length() == 0 || entryText == "Unknown") entryText = "?";
// Trim if Too Long
while (display->getStringWidth(entryText + "-") > (display->getWidth() / columns - 4)) {
entryText = entryText.substring(0, entryText.length() - 1);
}
// Highlight Selection
// === Highlight background (if selected) ===
if (itemIndex == destIndex) {
display->fillRect(xOffset, yOffset, display->getStringWidth(entryText) + 4, FONT_HEIGHT_SMALL + 2);
int scrollPadding = 8; // Reserve space for scrollbar
display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5);
display->setColor(BLACK);
}
// === Draw entry text ===
display->drawString(xOffset + 2, yOffset, entryText);
display->setColor(WHITE);
itemIndex++;
// === Draw key icon (after highlight) ===
if (itemIndex >= numActiveChannels) {
int nodeIndex = itemIndex - numActiveChannels;
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(this->filteredNodes.size())) {
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
if (node && hasKeyForNode(node)) {
int iconX = display->getWidth() - key_symbol_width - 15;
int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2;
if (itemIndex == destIndex) {
display->setColor(INVERSE);
} else {
display->setColor(WHITE);
}
display->drawXbm(iconX, iconY, key_symbol_width, key_symbol_height, key_symbol);
}
}
if (totalEntries > visibleRows * columns) {
display->drawRect(display->getWidth() - 6, rowYOffset, 4, visibleRows * FONT_HEIGHT_SMALL);
int totalPages = (totalEntries + columns - 1) / columns;
int scrollHeight = (visibleRows * FONT_HEIGHT_SMALL * visibleRows) / (totalPages);
int scrollPos = rowYOffset + ((visibleRows * FONT_HEIGHT_SMALL) * scrollIndex) / totalPages;
display->fillRect(display->getWidth() - 6, scrollPos, 4, scrollHeight);
}
screen->forceDisplay();
} else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
requestFocus(); // Tell Screen::setFrames to move to our module's frame
}
// Scrollbar
if (totalEntries > visibleRows) {
int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4);
int totalScrollable = totalEntries;
int scrollTrackX = display->getWidth() - 6;
display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight);
int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable;
int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable;
display->fillRect(scrollTrackX, scrollPos, 4, scrollHeight);
}
return;
}
// === ACK/NACK Screen ===
if (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) {
requestFocus();
EINK_ADD_FRAMEFLAG(display, COSMETIC);
display->setTextAlignment(TEXT_ALIGN_CENTER);
#ifdef USE_EINK
display->setFont(FONT_SMALL);
#else
display->setFont(FONT_MEDIUM);
#endif
if (this->ack) {
if (this->lastSentNode == NODENUM_BROADCAST) {
snprintf(buffer, sizeof(buffer), "Relayed to %s", channels.getName(this->channel));
} else {
snprintf(buffer, sizeof(buffer), "%s\nto %s",
this->lastAckWasRelayed ? "Delivered (Relayed)" : "Delivered (Direct)",
getNodeName(this->incoming));
}
} else {
snprintf(buffer, sizeof(buffer), "Delivery failed\nto %s", getNodeName(this->incoming));
}
display->drawString(display->getWidth() / 2 + x, 0 + y + 12, buffer);
display->setFont(FONT_SMALL);
// SNR/RSSI
if (display->getHeight() > 100) {
int16_t snrY = 100;
int16_t rssiY = 130;
if (display->getHeight() < rssiY + FONT_HEIGHT_SMALL) {
snrY = display->getHeight() - ((1.5) * FONT_HEIGHT_SMALL);
rssiY = display->getHeight() - ((2.5) * FONT_HEIGHT_SMALL);
}
if (this->ack) {
display->drawStringf(display->getWidth() / 2 + x, snrY + y, buffer, "Last Rx SNR: %f", this->lastRxSnr);
display->drawStringf(display->getWidth() / 2 + x, rssiY + y, buffer, "Last Rx RSSI: %d", this->lastRxRssi);
}
}
return;
}
// === Sending Screen ===
if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) {
EINK_ADD_FRAMEFLAG(display, COSMETIC);
requestFocus();
#ifdef USE_EINK
display->setFont(FONT_SMALL);
#else
display->setFont(FONT_MEDIUM);
#endif
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->drawString(display->getWidth() / 2 + x, 0 + y + 12, "Sending...");
return;
}
// === Disabled Screen ===
if (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) {
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled.");
return;
}
// === Free Text Input Screen ===
if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
requestFocus();
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
EInkDynamicDisplay* einkDisplay = static_cast<EInkDynamicDisplay*>(display);
einkDisplay->enableUnlimitedFastMode(); // Enable unlimited fast refresh while typing
einkDisplay->enableUnlimitedFastMode();
#endif
#if defined(USE_VIRTUAL_KEYBOARD)
drawKeyboard(display, state, 0, 0);
#else
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NONE) {
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
display->setColor(BLACK);
}
switch (this->destSelect) {
case CANNED_MESSAGE_DESTINATION_TYPE_NODE:
display->drawStringf(1 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest),
channels.getName(this->channel));
LOG_INFO("Displaying recipient: Node=%s (ID=%d)", cannedMessageModule->getNodeName(this->dest), this->dest);
display->drawStringf(0 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest),
channels.getName(this->channel));
display->drawStringf(0 + x, 0 + y, buffer, "To: >%s<@%s", getNodeName(this->dest), channels.getName(this->channel));
break;
case CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL:
display->drawStringf(1 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest),
channels.getName(this->channel));
display->drawStringf(0 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest),
channels.getName(this->channel));
display->drawStringf(0 + x, 0 + y, buffer, "To: %s@>%s<", getNodeName(this->dest), channels.getName(this->channel));
break;
default:
if (display->getWidth() > 128) {
display->drawStringf(0 + x, 0 + y, buffer, "To: %s@%s", cannedMessageModule->getNodeName(this->dest),
channels.getName(this->channel));
display->drawStringf(0 + x, 0 + y, buffer, "To: %s@%s", getNodeName(this->dest), channels.getName(this->channel));
} else {
display->drawStringf(0 + x, 0 + y, buffer, "To: %.5s@%.5s", cannedMessageModule->getNodeName(this->dest),
channels.getName(this->channel));
display->drawStringf(0 + x, 0 + y, buffer, "To: %.5s@%.5s", getNodeName(this->dest), channels.getName(this->channel));
}
break;
}
// used chars right aligned, only when not editing the destination
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) {
uint16_t charsLeft =
meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0);
uint16_t charsLeft = meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0);
snprintf(buffer, sizeof(buffer), "%d left", charsLeft);
display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer);
}
display->setColor(WHITE);
display->drawStringMaxWidth(
0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(),
cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor));
display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(),
drawWithCursor(this->freetext, this->cursor));
#endif
} else {
return;
}
// === Canned Messages List ===
if (this->messagesCount > 0) {
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawStringf(0 + x, 0 + y, buffer, "To: %s", cannedMessageModule->getNodeName(this->dest));
display->drawStringf(0 + x, 0 + y, buffer, "To: %s", getNodeName(this->dest));
int lines = (display->getHeight() / FONT_HEIGHT_SMALL) - 1;
if (lines == 3) {
display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
display->setColor(BLACK);
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, cannedMessageModule->getCurrentMessage());
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, getCurrentMessage());
display->setColor(WHITE);
if (this->messagesCount > 1) {
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL, cannedMessageModule->getPrevMessage());
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 3, cannedMessageModule->getNextMessage());
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL, getPrevMessage());
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 3, getNextMessage());
}
} else {
int topMsg = (messagesCount > lines && currentMessageIndex >= lines - 1) ? currentMessageIndex - lines + 2 : 0;
@ -1472,19 +1506,15 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
if (i == currentMessageIndex - topMsg) {
#ifdef USE_EINK
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), ">");
display->drawString(12 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1),
cannedMessageModule->getCurrentMessage());
display->drawString(12 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), getCurrentMessage());
#else
display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), x + display->getWidth(),
y + FONT_HEIGHT_SMALL);
display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), x + display->getWidth(), y + FONT_HEIGHT_SMALL);
display->setColor(BLACK);
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), cannedMessageModule->getCurrentMessage());
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), getCurrentMessage());
display->setColor(WHITE);
#endif
} else if (messagesCount > 1) { // Only draw others if there are multiple messages
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1),
cannedMessageModule->getMessageByIndex(topMsg + i));
}
} else {
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), getMessageByIndex(topMsg + i));
}
}
}
@ -1495,17 +1525,38 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
{
if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck) {
// look for a request_id
if (mp.decoded.request_id != 0) {
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen
requestFocus(); // Tell Screen::setFrames that our module's frame should be shown, even if not "first" in the frameset
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
requestFocus();
this->runState = CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED;
this->incoming = service->getNodenumFromRequestId(mp.decoded.request_id);
// Decode the Routing payload to check for errors
meshtastic_Routing decoded = meshtastic_Routing_init_default;
pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded);
this->ack = decoded.error_reason == meshtastic_Routing_Error_NONE;
waitingForAck = false; // No longer want routing packets
// === Relay Detection ===
uint8_t relayByte = mp.relay_node;
uint8_t senderLastByte = mp.from & 0xFF;
this->lastAckWasRelayed = (relayByte != senderLastByte);
// === Accept ACK if no error AND:
// - Broadcast (allow any ACK)
// - OR matches exact destination
bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE);
bool isFromDest = (mp.from == this->lastSentNode);
bool isBroadcast = (this->lastSentNode == NODENUM_BROADCAST);
this->ack = isAck && (isBroadcast || isFromDest);
// === Set .incoming to the node who ACK'd (even if it was broadcast)
if (isBroadcast && mp.from != nodeDB->getNodeNum()) {
this->incoming = mp.from;
} else {
this->incoming = this->lastSentNode;
}
waitingForAck = false;
this->notifyObservers(&e);
// run the next time 2 seconds later
setIntervalFromNow(2000);

View File

@ -61,6 +61,8 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
bool needsUpdate = true;
String searchQuery;
std::vector<uint8_t> activeChannelIndices;
bool shouldRedraw = false;
unsigned long lastUpdateMillis = 0;
public:
CannedMessageModule();
@ -164,8 +166,10 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
uint8_t numChannels = 0;
ChannelIndex indexChannels[MAX_NUM_CHANNELS] = {0};
NodeNum incoming = NODENUM_BROADCAST;
NodeNum lastSentNode = 0; // Tracks who the message was sent to (for ACK screen)
bool ack = false; // True means ACK, false means NAK (error_reason != NONE)
bool waitingForAck = false; // Are currently interested in routing packets?
bool lastAckWasRelayed = false;
float lastRxSnr = 0;
int32_t lastRxRssi = 0;