Compare commits

...
16 changed files with 325 additions and 276 deletions
@@ -87,12 +87,6 @@
</screenshots>
<releases>
<release version="2.7.26" date="2026-06-10">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.26</url>
</release>
<release version="2.7.25" date="2026-05-23">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.25</url>
</release>
<release version="2.7.24" date="2026-05-08">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.24</url>
</release>
-12
View File
@@ -1,15 +1,3 @@
meshtasticd (2.7.26.0) unstable; urgency=medium
* Version 2.7.26
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 10 Jun 2026 00:19:23 +0000
meshtasticd (2.7.25.0) unstable; urgency=medium
* Version 2.7.25
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Sat, 23 May 2026 01:16:20 +0000
meshtasticd (2.7.24.0) unstable; urgency=medium
* Version 2.7.24
+64 -109
View File
@@ -102,8 +102,8 @@ namespace graphics
// if defined a pixel will blink to show redraws
// #define SHOW_REDRAWS
#define ASCII_BELL '\x07'
// A text message frame + debug frame + all the node infos
FrameCallback *normalFrames;
// Base frames plus active module/favorite frames; grows only when the actual frameset needs it.
static std::vector<FrameCallback> normalFrames;
static uint32_t targetFramerate = IDLE_FRAMERATE;
#if GRAPHICS_TFT_COLORING_ENABLED
static inline void prepareFrameColorRegions()
@@ -136,6 +136,7 @@ uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100};
// At some point, we're going to ask all of the modules if they would like to display a screen frame
// we'll need to hold onto pointers for the modules that can draw a frame.
std::vector<MeshModule *> moduleFrames;
static uint8_t moduleFrameStart = 0;
#if HAS_GPS
// GeoCoord object for the screen
@@ -333,8 +334,14 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
// otherwise, just display the module frame that's aligned with the current frame
module_frame = state->currentFrame;
}
MeshModule &pi = *moduleFrames.at(module_frame);
pi.drawFrame(display, state, x, y);
if (module_frame < moduleFrameStart)
return;
const uint8_t moduleIndex = module_frame - moduleFrameStart;
if (moduleIndex >= moduleFrames.size() || moduleFrames[moduleIndex] == nullptr)
return;
moduleFrames[moduleIndex]->drawFrame(display, state, x, y);
}
/**
@@ -402,9 +409,11 @@ SPIClass SPI1(HSPI);
#endif
Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_OledType screenType, OLEDDISPLAY_GEOMETRY geometry)
: concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32)
: concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(16)
{
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
normalFrames.reserve(24);
indicatorIcons.reserve(24);
moduleFrames.reserve(8);
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
dispdev = new SH1106Wire(address.address, -1, -1, geometry,
@@ -488,7 +497,6 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
Screen::~Screen()
{
delete[] graphics::normalFrames;
}
/**
@@ -1142,183 +1150,130 @@ void Screen::setFrames(FrameFocus focus)
LOG_DEBUG("Show standard frames");
showingNormalScreen = true;
normalFrames.clear();
indicatorIcons.clear();
size_t numframes = 0;
auto appendFrame = [this](FrameCallback frame, const uint8_t *icon) -> uint8_t {
normalFrames.emplace_back(frame);
indicatorIcons.push_back(icon);
return normalFrames.size() - 1;
};
// If we have a critical fault, show it first
fsi.positions.fault = numframes;
fsi.positions.fault = normalFrames.size();
if (error_code) {
normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame;
indicatorIcons.push_back(icon_error);
fsi.positions.fault = appendFrame(NotificationRenderer::drawCriticalFaultFrame, icon_error);
focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame
}
#if defined(DISPLAY_CLOCK_FRAME)
if (!hiddenFrames.clock) {
fsi.positions.clock = numframes;
#if defined(OLED_TINY)
normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame;
fsi.positions.clock = appendFrame(graphics::ClockRenderer::drawAnalogClockFrame, digital_icon_clock);
#else
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame;
fsi.positions.clock = appendFrame(uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame,
digital_icon_clock);
#endif
indicatorIcons.push_back(digital_icon_clock);
}
#endif
if (!hiddenFrames.home) {
fsi.positions.home = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
indicatorIcons.push_back(icon_home);
fsi.positions.home = appendFrame(graphics::UIRenderer::drawDeviceFocused, icon_home);
}
fsi.positions.textMessage = numframes;
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
indicatorIcons.push_back(icon_mail);
fsi.positions.textMessage = appendFrame(graphics::MessageRenderer::drawTextMessageFrame, icon_mail);
#ifndef USE_EINK
if (!hiddenFrames.nodelist_nodes) {
fsi.positions.nodelist_nodes = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes;
indicatorIcons.push_back(icon_nodes);
fsi.positions.nodelist_nodes = appendFrame(graphics::NodeListRenderer::drawDynamicListScreen_Nodes, icon_nodes);
}
if (!hiddenFrames.nodelist_location) {
fsi.positions.nodelist_location = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location;
indicatorIcons.push_back(icon_list);
fsi.positions.nodelist_location = appendFrame(graphics::NodeListRenderer::drawDynamicListScreen_Location, icon_list);
}
#endif
// Show detailed node views only on E-Ink builds
#ifdef USE_EINK
if (!hiddenFrames.nodelist_lastheard) {
fsi.positions.nodelist_lastheard = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
indicatorIcons.push_back(icon_nodes);
fsi.positions.nodelist_lastheard = appendFrame(graphics::NodeListRenderer::drawLastHeardScreen, icon_nodes);
}
if (!hiddenFrames.nodelist_hopsignal) {
fsi.positions.nodelist_hopsignal = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
indicatorIcons.push_back(icon_signal);
fsi.positions.nodelist_hopsignal = appendFrame(graphics::NodeListRenderer::drawHopSignalScreen, icon_signal);
}
if (!hiddenFrames.nodelist_distance) {
fsi.positions.nodelist_distance = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
indicatorIcons.push_back(icon_distance);
fsi.positions.nodelist_distance = appendFrame(graphics::NodeListRenderer::drawDistanceScreen, icon_distance);
}
#endif
#if HAS_GPS
#ifdef USE_EINK
if (!hiddenFrames.nodelist_bearings) {
fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list);
fsi.positions.nodelist_bearings = appendFrame(graphics::NodeListRenderer::drawNodeListWithCompasses, icon_list);
}
#endif
if (!hiddenFrames.gps) {
fsi.positions.gps = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
indicatorIcons.push_back(icon_compass);
fsi.positions.gps = appendFrame(graphics::UIRenderer::drawCompassAndLocationScreen, icon_compass);
}
#endif
if (RadioLibInterface::instance && !hiddenFrames.lora) {
fsi.positions.lora = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused;
indicatorIcons.push_back(icon_radio);
fsi.positions.lora = appendFrame(graphics::DebugRenderer::drawLoRaFocused, icon_radio);
}
if (!hiddenFrames.system) {
fsi.positions.system = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen;
indicatorIcons.push_back(icon_system);
fsi.positions.system = appendFrame(graphics::DebugRenderer::drawSystemScreen, icon_system);
}
#if !defined(DISPLAY_CLOCK_FRAME)
if (!hiddenFrames.clock) {
fsi.positions.clock = numframes;
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame;
indicatorIcons.push_back(digital_icon_clock);
fsi.positions.clock = appendFrame(uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame,
digital_icon_clock);
}
#endif
if (!hiddenFrames.chirpy) {
fsi.positions.chirpy = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy;
indicatorIcons.push_back(chirpy_small);
fsi.positions.chirpy = appendFrame(graphics::DebugRenderer::drawChirpy, chirpy_small);
}
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
if (!hiddenFrames.wifi && isWifiAvailable()) {
fsi.positions.wifi = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline;
indicatorIcons.push_back(icon_wifi);
fsi.positions.wifi = appendFrame(graphics::DebugRenderer::drawDebugInfoWiFiTrampoline, icon_wifi);
}
#endif
// Beware of what changes you make in this code!
// We pass numframes into GetMeshModulesWithUIFrames() which is highly important!
// Inside of that callback, goes over to MeshModule.cpp and we run
// modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr
// entries until we're ready to start building the matching entries.
// We are doing our best to keep the normalFrames vector
// and the moduleFrames vector in lock step.
moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes);
moduleFrameStart = normalFrames.size();
moduleFrames = MeshModule::GetMeshModulesWithUIFrames();
LOG_DEBUG("Show %d module frames", moduleFrames.size());
for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) {
// Draw the module frame, using the hack described above
if (*i != nullptr) {
normalFrames[numframes] = drawModuleFrame;
// Check if the module being drawn has requested focus
// We will honor this request later, if setFrames was triggered by a UIFrameEvent
MeshModule *m = *i;
if (m && m->isRequestingFocus())
fsi.positions.focusedModule = numframes;
if (m && m == waypointModule)
fsi.positions.waypoint = numframes;
indicatorIcons.push_back(icon_module);
numframes++;
}
for (MeshModule *m : moduleFrames) {
const uint8_t frameIndex = appendFrame(drawModuleFrame, icon_module);
if (m && m->isRequestingFocus())
fsi.positions.focusedModule = frameIndex;
if (m && m == waypointModule)
fsi.positions.waypoint = frameIndex;
}
LOG_DEBUG("Added modules. numframes: %d", numframes);
// We don't show the node info of our node (if we have it yet - we should)
size_t numMeshNodes = nodeDB->getNumMeshNodes();
if (numMeshNodes > 0)
numMeshNodes--;
LOG_DEBUG("Added modules. numframes: %d", normalFrames.size());
if (!hiddenFrames.show_favorites) {
// Temporary array to hold favorite node frames
std::vector<FrameCallback> favoriteFrames;
uint8_t firstFavorite = 255;
uint8_t lastFavorite = 255;
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (n && n->num != nodeDB->getNodeNum() && nodeInfoLiteIsFavorite(n)) {
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
const uint8_t frameIndex = appendFrame(graphics::UIRenderer::drawFavoriteNode, icon_node);
if (firstFavorite == 255)
firstFavorite = frameIndex;
lastFavorite = frameIndex;
}
}
// Insert favorite frames *after* collecting them all
if (!favoriteFrames.empty()) {
fsi.positions.firstFavorite = numframes;
for (const auto &f : favoriteFrames) {
normalFrames[numframes++] = f;
indicatorIcons.push_back(icon_node);
}
fsi.positions.lastFavorite = numframes - 1;
} else {
fsi.positions.firstFavorite = 255;
fsi.positions.lastFavorite = 255;
}
fsi.positions.firstFavorite = firstFavorite;
fsi.positions.lastFavorite = lastFavorite;
}
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
this->frameCount = numframes; // Save frame count for use in custom overlay
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
fsi.frameCount = normalFrames.size(); // Total framecount is used to apply FOCUS_PRESERVE
this->frameCount = normalFrames.size(); // Save frame count for use in custom overlay
LOG_DEBUG("Finished build frames. numframes: %d", normalFrames.size());
ui->setFrames(normalFrames, numframes);
ui->setFrames(normalFrames.data(), fsi.frameCount);
ui->disableAllIndicators();
// Add overlays: frame icons and alert banner)
+69 -18
View File
@@ -154,8 +154,8 @@ static std::vector<uint32_t> seenPeers;
// Public helper so menus / store can clear stale registries
void clearThreadRegistries()
{
seenChannels.clear();
seenPeers.clear();
std::vector<int>().swap(seenChannels);
std::vector<uint32_t>().swap(seenPeers);
}
// Setter so other code can switch threads
@@ -387,6 +387,58 @@ static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int to
}
}
static void appendWrappedLines(OLEDDisplay *display, const char *messageBuf, int textWidth, std::vector<std::string> &allLines,
size_t maxTotalLines, size_t maxWrappedLines)
{
std::string line;
std::string word;
size_t wrappedCount = 0;
auto appendLine = [&](std::string &&newLine) -> bool {
if (newLine.empty())
return true;
if (allLines.size() >= maxTotalLines || wrappedCount >= maxWrappedLines)
return false;
allLines.emplace_back(std::move(newLine));
++wrappedCount;
return true;
};
for (int i = 0; messageBuf[i]; ++i) {
char ch = messageBuf[i];
if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 &&
(unsigned char)messageBuf[i + 2] == 0x99) {
ch = '\''; // plain apostrophe
i += 2; // skip over the extra UTF-8 bytes
}
if (ch == '\n') {
if (!word.empty())
line += word;
if (!appendLine(std::move(line)))
return;
line.clear();
word.clear();
} else if (ch == ' ') {
line += word + ' ';
word.clear();
} else {
word += ch;
std::string test = line + word;
uint16_t strWidth = graphics::UIRenderer::measureStringWithEmotes(display, test.c_str());
if (strWidth > textWidth) {
if (!line.empty() && !appendLine(std::move(line)))
return;
line = word;
word.clear();
}
}
}
if (!word.empty())
line += word;
appendLine(std::move(line));
}
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
// Ensure any boot-relative timestamps are upgraded if RTC is valid
@@ -646,19 +698,15 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
const char *msgText = MessageStore::getText(m);
int wrapWidth = mine ? rightTextWidth : leftTextWidth;
std::vector<std::string> wrapped = generateLines(display, "", msgText, wrapWidth);
// Per-message wrap-line limit: even if wrapping produces many lines, cap them to prevent
// a single long message from consuming most or all of the cache.
constexpr size_t MAX_WRAPPED_LINES_PER_MSG = 20U;
size_t wrappedCount = 0;
for (auto &ln : wrapped) {
if (allLines.size() >= MAX_CACHED_LINES || wrappedCount >= MAX_WRAPPED_LINES_PER_MSG)
break; // Cache limit or per-message limit reached; stop adding lines from this message
allLines.emplace_back(std::move(ln));
const size_t previousLineCount = allLines.size();
appendWrappedLines(display, msgText, wrapWidth, allLines, MAX_CACHED_LINES, MAX_WRAPPED_LINES_PER_MSG);
for (size_t i = previousLineCount; i < allLines.size(); ++i) {
isMine.push_back(mine);
isHeader.push_back(false);
ackForLine.push_back(AckStatus::NONE);
++wrappedCount;
}
}
@@ -1008,20 +1056,23 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
std::vector<int> rowHeights;
rowHeights.reserve(lines.size());
std::vector<graphics::EmoteRenderer::LineMetrics> lineMetrics;
lineMetrics.reserve(lines.size());
for (const auto &line : lines) {
lineMetrics.push_back(graphics::EmoteRenderer::analyzeLine(nullptr, line, FONT_HEIGHT_SMALL, emotes, numEmotes));
}
auto currentMetrics = graphics::EmoteRenderer::LineMetrics{};
auto nextMetrics = lines.empty()
? graphics::EmoteRenderer::LineMetrics{}
: graphics::EmoteRenderer::analyzeLine(nullptr, lines[0], FONT_HEIGHT_SMALL, emotes, numEmotes);
for (size_t idx = 0; idx < lines.size(); ++idx) {
currentMetrics = nextMetrics;
nextMetrics = (idx + 1 < lines.size())
? graphics::EmoteRenderer::analyzeLine(nullptr, lines[idx + 1], FONT_HEIGHT_SMALL, emotes, numEmotes)
: graphics::EmoteRenderer::LineMetrics{};
const int baseHeight = FONT_HEIGHT_SMALL;
int lineHeight = baseHeight;
const int tallestEmote = lineMetrics[idx].tallestHeight;
const bool hasEmote = lineMetrics[idx].hasEmote;
const bool nextHasEmote = (idx + 1 < lines.size()) && lineMetrics[idx + 1].hasEmote;
const int tallestEmote = currentMetrics.tallestHeight;
const bool hasEmote = currentMetrics.hasEmote;
const bool nextHasEmote = (idx + 1 < lines.size()) && nextMetrics.hasEmote;
if (isHeaderVec[idx]) {
// Header line spacing
+2 -5
View File
@@ -244,13 +244,10 @@ void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to)
p->decoded.request_id = to.id;
}
std::vector<MeshModule *> MeshModule::GetMeshModulesWithUIFrames(int startIndex)
std::vector<MeshModule *> MeshModule::GetMeshModulesWithUIFrames()
{
std::vector<MeshModule *> modulesWithUIFrames;
// Fill with nullptr up to startIndex
modulesWithUIFrames.resize(startIndex, nullptr);
if (modules) {
for (auto i = modules->begin(); i != modules->end(); ++i) {
auto &pi = **i;
@@ -311,4 +308,4 @@ bool MeshModule::isRequestingFocus()
} else
return false;
}
#endif
#endif
+1 -1
View File
@@ -76,7 +76,7 @@ class MeshModule
*/
static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO);
static std::vector<MeshModule *> GetMeshModulesWithUIFrames(int startIndex);
static std::vector<MeshModule *> GetMeshModulesWithUIFrames();
static void observeUIEvents(Observer<const UIFrameEvent *> *observer);
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
+145 -91
View File
@@ -44,6 +44,8 @@ extern MessageStore messageStore;
#include "graphics/ScreenFonts.h"
#include <Throttle.h>
#include <cctype>
#include <new>
// Remove Canned message screen if no action is taken for some milliseconds
#define INACTIVATE_AFTER_MS 20000
@@ -144,6 +146,36 @@ bool hasKeyForNode(const meshtastic_NodeInfoLite *node)
{
return nodeInfoLiteHasUser(node) && node->public_key.size > 0;
}
static bool containsIgnoreCase(const char *text, const char *query)
{
if (!query || !*query)
return true;
if (!text || !*text)
return false;
const size_t queryLen = strlen(query);
for (const char *p = text; *p; ++p) {
size_t i = 0;
while (i < queryLen && p[i] &&
std::tolower(static_cast<unsigned char>(p[i])) == std::tolower(static_cast<unsigned char>(query[i]))) {
++i;
}
if (i == queryLen)
return true;
}
return false;
}
static bool activeChannelNameSeen(const std::vector<uint8_t> &activeChannelIndices, const char *name)
{
for (uint8_t channelIndex : activeChannelIndices) {
const char *existingName = channels.getName(channelIndex);
if (existingName && strcmp(existingName, name) == 0)
return true;
}
return false;
}
/**
* @brief Items in array this->messages will be set to be pointing on the right
* starting points of the string this->messageStore
@@ -155,10 +187,9 @@ int CannedMessageModule::splitConfiguredMessages()
{
int i = 0;
String canned_messages = cannedMessageModuleConfig.messages;
// Copy all message parts into the buffer
strncpy(this->messageBuffer, canned_messages.c_str(), sizeof(this->messageBuffer));
strncpy(this->messageBuffer, cannedMessageModuleConfig.messages, sizeof(this->messageBuffer) - 1);
this->messageBuffer[sizeof(this->messageBuffer) - 1] = '\0';
// Temporary array to allow for insertion
const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0};
@@ -222,7 +253,7 @@ void CannedMessageModule::resetSearch()
{
int previousDestIndex = destIndex;
searchQuery = "";
searchQuery[0] = '\0';
updateDestinationSelectionList();
// Adjust scrollIndex so previousDestIndex is still visible
@@ -238,26 +269,21 @@ void CannedMessageModule::resetSearch()
}
void CannedMessageModule::updateDestinationSelectionList()
{
static size_t lastNumMeshNodes = 0;
static String lastSearchQuery = "";
size_t numMeshNodes = nodeDB->getNumMeshNodes();
bool nodesChanged = (numMeshNodes != lastNumMeshNodes);
lastNumMeshNodes = numMeshNodes;
bool nodesChanged = (numMeshNodes != cachedDestinationNodeCount);
cachedDestinationNodeCount = numMeshNodes;
// Early exit if nothing changed
if (searchQuery == lastSearchQuery && !nodesChanged)
if (destinationSelectionCacheValid && strcmp(searchQuery, cachedDestinationSearchQuery) == 0 && !nodesChanged)
return;
lastSearchQuery = searchQuery;
strncpy(cachedDestinationSearchQuery, searchQuery, sizeof(cachedDestinationSearchQuery) - 1);
cachedDestinationSearchQuery[sizeof(cachedDestinationSearchQuery) - 1] = '\0';
needsUpdate = false;
this->filteredNodes.clear();
this->activeChannelIndices.clear();
NodeNum myNodeNum = nodeDB->getNodeNum();
String lowerSearchQuery = searchQuery;
lowerSearchQuery.toLowerCase();
// Preallocate space to reduce reallocation
this->filteredNodes.reserve(numMeshNodes);
@@ -266,38 +292,27 @@ void CannedMessageModule::updateDestinationSelectionList()
if (!node || node->num == myNodeNum || !nodeInfoLiteHasUser(node) || node->public_key.size != 32)
continue;
const String &nodeName = node->long_name;
const char *nodeName = node->long_name;
const char *shortName = node->short_name;
if (searchQuery.length() == 0) {
if (searchQuery[0] == '\0') {
this->filteredNodes.push_back({node, sinceLastSeen(node)});
} else if (containsIgnoreCase(nodeName, searchQuery) || containsIgnoreCase(shortName, searchQuery)) {
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)});
}
}
}
meshtastic_MeshPacket *p = allocDataPacket();
p->pki_encrypted = true;
p->channel = 0;
// Populate active channels
std::vector<String> seenChannels;
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()) {
const char *name = channels.getName(i);
if (name && name[0] && !activeChannelNameSeen(activeChannelIndices, name)) {
this->activeChannelIndices.push_back(i);
seenChannels.push_back(name);
}
}
scrollIndex = 0; // Show first result at the top
destIndex = 0; // Highlight the first entry
destinationSelectionCacheValid = true;
if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) {
LOG_INFO("Nodes changed, forcing UI refresh.");
screen->forceDisplay();
@@ -480,7 +495,11 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
void CannedMessageModule::updateState(cannedMessageModuleRunState newState, bool shouldRequestFocus)
{
cannedMessageModuleRunState oldState = runState;
runState = newState;
if (oldState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION && newState != CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) {
releaseDestinationSelectionCache();
}
if (runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
inputBroker->menuMode =
false; // Allow any key input to be sent to the message composer instead of being interpreted as menu navigation
@@ -492,6 +511,26 @@ void CannedMessageModule::updateState(cannedMessageModuleRunState newState, bool
}
}
void CannedMessageModule::releaseDestinationSelectionCache()
{
searchQuery[0] = '\0';
cachedDestinationSearchQuery[0] = '\0';
cachedDestinationNodeCount = 0;
destinationSelectionCacheValid = false;
destIndex = 0;
scrollIndex = 0;
std::vector<uint8_t>().swap(activeChannelIndices);
std::vector<NodeEntry>().swap(filteredNodes);
}
void CannedMessageModule::releaseFreetext()
{
freetext.~String();
new (&freetext) String();
cursor = 0;
}
bool CannedMessageModule::isUpEvent(const InputEvent *event)
{
return event->inputEvent == INPUT_BROKER_UP ||
@@ -547,11 +586,15 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown && event->inputEvent != INPUT_BROKER_LEFT &&
event->inputEvent != INPUT_BROKER_RIGHT && event->inputEvent != INPUT_BROKER_SELECT) {
this->searchQuery += (char)event->kbchar;
needsUpdate = true;
if ((millis() - lastFilterUpdate) > filterDebounceMs) {
runOnce(); // update filter immediately
lastFilterUpdate = millis();
size_t queryLen = strlen(searchQuery);
if (queryLen + 1 < sizeof(searchQuery)) {
searchQuery[queryLen] = static_cast<char>(event->kbchar);
searchQuery[queryLen + 1] = '\0';
needsUpdate = true;
if ((millis() - lastFilterUpdate) > filterDebounceMs) {
runOnce(); // update filter immediately
lastFilterUpdate = millis();
}
}
return 1;
}
@@ -565,12 +608,13 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
// Handle backspace
if (event->inputEvent == INPUT_BROKER_BACK) {
if (searchQuery.length() > 0) {
searchQuery.remove(searchQuery.length() - 1);
size_t queryLen = strlen(searchQuery);
if (queryLen > 0) {
searchQuery[queryLen - 1] = '\0';
needsUpdate = true;
runOnce();
}
if (searchQuery.length() == 0) {
if (searchQuery[0] == '\0') {
resetSearch();
needsUpdate = false;
}
@@ -641,7 +685,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) {
updateState(returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT, true);
returnToCannedList = false;
searchQuery = "";
searchQuery[0] = '\0';
// UIFrameEvent e;
// e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
@@ -671,8 +715,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
if (runState != CANNED_MESSAGE_RUN_STATE_INACTIVE &&
(event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG)) {
updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
freetext = "";
cursor = 0;
releaseFreetext();
payload = 0;
currentMessageIndex = -1;
@@ -762,8 +805,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
// Return to inactive state
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
this->currentMessageIndex = -1;
this->freetext = "";
this->cursor = 0;
this->releaseFreetext();
// Force display update to show normal screen
UIFrameEvent e;
@@ -822,8 +864,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
// Cancel (dismiss freetext screen)
if (event->inputEvent == INPUT_BROKER_LEFT) {
updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
freetext = "";
cursor = 0;
releaseFreetext();
payload = 0;
currentMessageIndex = -1;
@@ -946,8 +987,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG ||
(event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() == 0)) {
updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
freetext = "";
cursor = 0;
releaseFreetext();
payload = 0;
currentMessageIndex = -1;
@@ -1187,8 +1227,7 @@ int32_t CannedMessageModule::runOnce()
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->currentMessageIndex = -1;
this->freetext = "";
this->cursor = 0;
this->releaseFreetext();
this->notifyObservers(&e);
return 2000;
}
@@ -1201,24 +1240,21 @@ int32_t CannedMessageModule::runOnce()
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->currentMessageIndex = -1;
this->freetext = "";
this->cursor = 0;
this->releaseFreetext();
this->notifyObservers(&e);
}
// Handle SENDING_ACTIVE state transition after virtual keyboard message
else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) {
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
this->currentMessageIndex = -1;
this->freetext = "";
this->cursor = 0;
this->releaseFreetext();
return INT32_MAX;
} else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) &&
!Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) {
// Reset module on inactivity
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->currentMessageIndex = -1;
this->freetext = "";
this->cursor = 0;
this->releaseFreetext();
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
// Clean up virtual keyboard if it exists during timeout
@@ -1240,8 +1276,7 @@ int32_t CannedMessageModule::runOnce()
// Clean up state but *dont* deactivate yet
this->currentMessageIndex = -1;
this->freetext = "";
this->cursor = 0;
this->releaseFreetext();
// Tell Screen to jump straight to the TextMessage frame
e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
@@ -1267,8 +1302,7 @@ int32_t CannedMessageModule::runOnce()
// Clean up state
this->currentMessageIndex = -1;
this->freetext = "";
this->cursor = 0;
this->releaseFreetext();
// Tell Screen to jump straight to the TextMessage frame
e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
@@ -1285,8 +1319,7 @@ int32_t CannedMessageModule::runOnce()
}
// fallback clean-up if nothing above returned
this->currentMessageIndex = -1;
this->freetext = "";
this->cursor = 0;
this->releaseFreetext();
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->notifyObservers(&e);
@@ -1312,15 +1345,13 @@ int32_t CannedMessageModule::runOnce()
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) {
if (this->messagesCount > 0) {
this->currentMessageIndex = getPrevIndex();
this->freetext = "";
this->cursor = 0;
this->releaseFreetext();
this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE);
}
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) {
if (this->messagesCount > 0) {
this->currentMessageIndex = this->getNextIndex();
this->freetext = "";
this->cursor = 0;
this->releaseFreetext();
this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE);
}
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) {
@@ -1494,8 +1525,10 @@ void CannedMessageModule::drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState
display->setColor(OLEDDISPLAY_COLOR::WHITE);
display->drawStringMaxWidth(0, 0, display->getWidth(),
cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor));
char msgWithCursor[meshtastic_Constants_DATA_PAYLOAD_LEN + 2];
cannedMessageModule->drawWithCursor(msgWithCursor, sizeof(msgWithCursor), cannedMessageModule->freetext.c_str(),
cannedMessageModule->cursor);
display->drawStringMaxWidth(0, 0, display->getWidth(), msgWithCursor);
display->setFont(FONT_MEDIUM);
@@ -1681,8 +1714,11 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
// Header
int titleY = 2;
String titleText = "Select Destination";
titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]";
char titleText[64];
if (searchQuery[0])
snprintf(titleText, sizeof(titleText), "Select Destination [%s]", searchQuery);
else
snprintf(titleText, sizeof(titleText), "Select Destination [ ]");
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->drawString(display->getWidth() / 2, titleY, titleText);
display->setTextAlignment(TEXT_ALIGN_LEFT);
@@ -1709,26 +1745,27 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
int xOffset = 0;
int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset;
std::string entryText;
char entryText[96] = "";
// Draw Channels First
if (itemIndex < numActiveChannels) {
uint8_t channelIndex = this->activeChannelIndices[itemIndex];
const char *channelName = channels.getName(channelIndex);
entryText = std::string("#") + (channelName ? channelName : "?");
snprintf(entryText, sizeof(entryText), "#%s", channelName ? channelName : "?");
}
// Then Draw Nodes
else {
int nodeIndex = itemIndex - numActiveChannels;
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(this->filteredNodes.size())) {
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
const char *nodeName = nullptr;
if (node) {
if (display->getWidth() <= 64) {
entryText = node->short_name;
nodeName = node->short_name;
} else if (node->long_name[0]) {
entryText = node->long_name;
nodeName = node->long_name;
} else {
entryText = node->short_name;
nodeName = node->short_name;
}
}
@@ -1738,22 +1775,21 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
if (availWidth < 0)
availWidth = 0;
char truncatedEntry[96];
graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry),
graphics::UIRenderer::truncateStringWithEmotes(display, nodeName ? nodeName : "", truncatedEntry, sizeof(truncatedEntry),
availWidth);
entryText = truncatedEntry;
snprintf(entryText, sizeof(entryText), "%s", truncatedEntry);
// Prepend "* " if this is a favorite
if (nodeInfoLiteIsFavorite(node)) {
entryText = "* " + entryText;
snprintf(entryText, sizeof(entryText), "* %s", truncatedEntry);
}
graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry),
availWidth);
entryText = truncatedEntry;
graphics::UIRenderer::truncateStringWithEmotes(display, entryText, truncatedEntry, sizeof(truncatedEntry), availWidth);
snprintf(entryText, sizeof(entryText), "%s", truncatedEntry);
}
}
if (entryText.empty() || entryText == "Unknown")
entryText = "?";
if (entryText[0] == '\0' || strcmp(entryText, "Unknown") == 0)
snprintf(entryText, sizeof(entryText), "?");
// Highlight background (if selected)
if (itemIndex == destIndex) {
@@ -1763,7 +1799,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
}
// Draw entry text
graphics::UIRenderer::drawStringWithEmotes(display, xOffset + 2, yOffset, entryText.c_str(), FONT_HEIGHT_SMALL, 1, false);
graphics::UIRenderer::drawStringWithEmotes(display, xOffset + 2, yOffset, entryText, FONT_HEIGHT_SMALL, 1, false);
display->setColor(WHITE);
// Draw key icon (after highlight)
@@ -2022,8 +2058,9 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
display->setColor(WHITE);
{
int inputY = 0 + y + FONT_HEIGHT_SMALL;
String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor);
drawWrappedEmoteText(display, x, inputY, msgWithCursor.c_str(), display->getWidth() - x, FONT_HEIGHT_SMALL);
char msgWithCursor[meshtastic_Constants_DATA_PAYLOAD_LEN + 2];
this->drawWithCursor(msgWithCursor, sizeof(msgWithCursor), this->freetext.c_str(), this->cursor);
drawWrappedEmoteText(display, x, inputY, msgWithCursor, display->getWidth() - x, FONT_HEIGHT_SMALL);
}
#endif
return;
@@ -2368,10 +2405,27 @@ void CannedMessageModule::handleSetCannedMessageModuleMessages(const char *from_
}
}
String CannedMessageModule::drawWithCursor(String text, int cursor)
void CannedMessageModule::drawWithCursor(char *buffer, size_t bufferSize, const char *text, size_t cursor)
{
String result = text.substring(0, cursor) + "_" + text.substring(cursor);
return result;
if (!buffer || bufferSize == 0)
return;
const char *source = text ? text : "";
const size_t sourceLen = strlen(source);
const size_t cursorIndex = std::min(cursor, sourceLen);
const size_t beforeLen = std::min(cursorIndex, bufferSize - 1);
memcpy(buffer, source, beforeLen);
size_t outLen = beforeLen;
if (outLen < bufferSize - 1)
buffer[outLen++] = '_';
const size_t remaining = bufferSize - 1 - outLen;
const size_t afterLen = std::min(sourceLen - cursorIndex, remaining);
memcpy(buffer + outLen, source + cursorIndex, afterLen);
outLen += afterLen;
buffer[outLen] = '\0';
}
#endif
+8 -2
View File
@@ -75,7 +75,7 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
void updateDestinationSelectionList();
void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
bool isCharInputAllowed() const;
String drawWithCursor(String text, int cursor);
void drawWithCursor(char *buffer, size_t bufferSize, const char *text, size_t cursor);
// === Emote Picker ===
int handleEmotePickerInput(const InputEvent *event);
@@ -146,7 +146,8 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
int visibleRows = 0;
bool needsUpdate = true;
unsigned long lastUpdateMillis = 0;
String searchQuery;
static constexpr size_t searchQuerySize = 33;
char searchQuery[searchQuerySize] = {0};
String freetext;
// === Message Storage ===
@@ -175,6 +176,9 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
unsigned long lastTouchMillis = 0;
uint32_t lastFilterUpdate = 0;
static constexpr uint32_t filterDebounceMs = 30;
size_t cachedDestinationNodeCount = 0;
char cachedDestinationSearchQuery[searchQuerySize] = {0};
bool destinationSelectionCacheValid = false;
std::vector<uint8_t> activeChannelIndices;
std::vector<NodeEntry> filteredNodes;
@@ -184,6 +188,8 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
#endif
void updateState(cannedMessageModuleRunState, bool shouldRequestFocus = false);
void releaseDestinationSelectionCache();
void releaseFreetext();
bool isUpEvent(const InputEvent *event);
bool isDownEvent(const InputEvent *event);
+2 -2
View File
@@ -158,8 +158,8 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
ignoreRequest = true;
return NULL;
} else {
ignoreRequest = false; // Don't ignore requests anymore
meshtastic_User &u = owner;
ignoreRequest = false; // Don't ignore requests anymore
meshtastic_User u = owner; // deliberate copy: the licensed strip below must not clobber the global owner state
// Strip the public key if the user is licensed
if (u.is_licensed && u.public_key.size > 0) {
+27 -25
View File
@@ -193,24 +193,36 @@ void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display)
display->setTextAlignment(TEXT_ALIGN_LEFT);
const uint16_t maxWrapWidth = display->width() - 40;
auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector<std::string> {
std::vector<std::string> wrapped;
char lineStorage[maxContentLines + 1][64] = {};
const char *linePtrs[maxContentLines + 2] = {};
uint8_t lineCount = 0;
auto appendLine = [&](const std::string &line) {
if (line.empty() || lineCount >= maxContentLines + (hasTitle ? 1 : 0))
return;
strncpy(lineStorage[lineCount], line.c_str(), sizeof(lineStorage[lineCount]) - 1);
lineStorage[lineCount][sizeof(lineStorage[lineCount]) - 1] = '\0';
linePtrs[lineCount] = lineStorage[lineCount];
++lineCount;
};
auto wrapText = [&](const char *text, uint16_t availableWidth) {
std::string current;
std::string word;
const char *p = text;
while (*p && wrapped.size() < maxContentLines) {
while (*p && lineCount < maxContentLines + (hasTitle ? 1 : 0)) {
while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
if (*p == '\n') {
if (!current.empty()) {
wrapped.push_back(current);
appendLine(current);
current.clear();
if (wrapped.size() >= maxContentLines)
if (lineCount >= maxContentLines + (hasTitle ? 1 : 0))
break;
}
}
++p;
}
if (!*p || wrapped.size() >= maxContentLines)
if (!*p || lineCount >= maxContentLines + (hasTitle ? 1 : 0))
break;
word.clear();
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r')
@@ -223,9 +235,9 @@ void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display)
current = test;
else {
if (!current.empty()) {
wrapped.push_back(current);
appendLine(current);
current = word;
if (wrapped.size() >= maxContentLines)
if (lineCount >= maxContentLines + (hasTitle ? 1 : 0))
break;
} else {
current = word;
@@ -235,36 +247,26 @@ void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display)
}
}
}
if (!current.empty() && wrapped.size() < maxContentLines)
wrapped.push_back(current);
return wrapped;
if (!current.empty() && lineCount < maxContentLines + (hasTitle ? 1 : 0))
appendLine(current);
};
std::vector<std::string> allLines;
if (hasTitle)
allLines.emplace_back(popupTitle);
appendLine(popupTitle);
char buf[sizeof(popupMessage)];
strncpy(buf, popupMessage, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *paragraph = strtok(buf, "\n");
while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) {
auto wrapped = wrapText(paragraph, maxWrapWidth);
for (const auto &ln : wrapped) {
if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0))
break;
allLines.push_back(ln);
}
while (paragraph && lineCount < maxContentLines + (hasTitle ? 1 : 0)) {
wrapText(paragraph, maxWrapWidth);
paragraph = strtok(nullptr, "\n");
}
std::vector<const char *> ptrs;
for (const auto &ln : allLines)
ptrs.push_back(ln.c_str());
ptrs.push_back(nullptr);
linePtrs[lineCount] = nullptr;
// Use the standard notification box drawing from NotificationRenderer
NotificationRenderer::drawNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0);
NotificationRenderer::drawNotificationBox(display, nullptr, linePtrs, lineCount, 0, 0);
}
} // namespace graphics
+1 -1
View File
@@ -21,7 +21,7 @@ void TraceRouteModule::setResultText(const String &text)
void TraceRouteModule::clearResultLines()
{
resultLines.clear();
std::vector<String>().swap(resultLines);
resultLinesDirty = false;
}
#if HAS_SCREEN
+2 -1
View File
@@ -5,7 +5,8 @@
// meshtastic_ServiceEnvelope that automatically releases dynamically allocated memory when it goes out of scope.
struct DecodedServiceEnvelope : public meshtastic_ServiceEnvelope {
DecodedServiceEnvelope(const uint8_t *payload, size_t length);
DecodedServiceEnvelope(DecodedServiceEnvelope &) = delete;
// const-qualified so std::variant instantiation works on Apple libc++ (copying stays ill-formed either way)
DecodedServiceEnvelope(const DecodedServiceEnvelope &) = delete;
DecodedServiceEnvelope(DecodedServiceEnvelope &&);
~DecodedServiceEnvelope();
// Clients must check that this is true before using.
@@ -1,4 +1,5 @@
#include "TestUtil.h"
#include <cstdlib>
#include <unity.h>
static void test_placeholder()
@@ -1,4 +1,5 @@
#include "TestUtil.h"
#include <cstdlib>
#include <unity.h>
#if defined(ARCH_PORTDUINO)
-1
View File
@@ -55,7 +55,6 @@ build_flags_common =
-lyaml-cpp
-ljsoncpp
!pkg-config --cflags jsoncpp --silence-errors || :
-li2c
-luv
-std=gnu17
-std=gnu++17
+2 -2
View File
@@ -1,4 +1,4 @@
[VERSION]
major = 2
minor = 7
build = 26
minor = 8
build = 0