Compare commits

..
Author SHA1 Message Date
vidplace7 db2b334ee8 Short circuit invalid nodenum check in ham mode.
Until signing is implemented, this will prevent the nodenum from being regenerated upon each reboot.
2026-06-09 19:53:08 -04:00
17 changed files with 273 additions and 368 deletions
-5
View File
@@ -14,7 +14,6 @@
* For more information, see: https://meshtastic.org/
*/
#include "power.h"
#include "BluetoothCommon.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "PowerFSM.h"
@@ -963,10 +962,6 @@ void Power::readPowerStatus()
lastLogTime = millis();
}
newStatus.notifyObservers(&powerStatus2);
// Mirror battery level to the BLE Battery Service (0x2A19); the platform layer clamps and dedupes.
if (hasBattery == OptTrue)
updateBatteryLevel(powerStatus2.getBatteryChargePercent());
#ifdef DEBUG_HEAP
if (lastheap != memGet.getFreeHeap()) {
// Use stack-allocated buffer to avoid heap allocations in monitoring code
+109 -64
View File
@@ -102,8 +102,8 @@ namespace graphics
// if defined a pixel will blink to show redraws
// #define SHOW_REDRAWS
#define ASCII_BELL '\x07'
// Base frames plus active module/favorite frames; grows only when the actual frameset needs it.
static std::vector<FrameCallback> normalFrames;
// A text message frame + debug frame + all the node infos
FrameCallback *normalFrames;
static uint32_t targetFramerate = IDLE_FRAMERATE;
#if GRAPHICS_TFT_COLORING_ENABLED
static inline void prepareFrameColorRegions()
@@ -136,7 +136,6 @@ 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
@@ -334,14 +333,8 @@ 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;
}
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);
MeshModule &pi = *moduleFrames.at(module_frame);
pi.drawFrame(display, state, x, y);
}
/**
@@ -409,11 +402,9 @@ 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(16)
: concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32)
{
normalFrames.reserve(24);
indicatorIcons.reserve(24);
moduleFrames.reserve(8);
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
dispdev = new SH1106Wire(address.address, -1, -1, geometry,
@@ -497,6 +488,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
Screen::~Screen()
{
delete[] graphics::normalFrames;
}
/**
@@ -1150,130 +1142,183 @@ void Screen::setFrames(FrameFocus focus)
LOG_DEBUG("Show standard frames");
showingNormalScreen = true;
normalFrames.clear();
indicatorIcons.clear();
auto appendFrame = [this](FrameCallback frame, const uint8_t *icon) -> uint8_t {
normalFrames.emplace_back(frame);
indicatorIcons.push_back(icon);
return normalFrames.size() - 1;
};
size_t numframes = 0;
// If we have a critical fault, show it first
fsi.positions.fault = normalFrames.size();
fsi.positions.fault = numframes;
if (error_code) {
fsi.positions.fault = appendFrame(NotificationRenderer::drawCriticalFaultFrame, icon_error);
normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame;
indicatorIcons.push_back(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)
fsi.positions.clock = appendFrame(graphics::ClockRenderer::drawAnalogClockFrame, digital_icon_clock);
normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame;
#else
fsi.positions.clock = appendFrame(uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame,
digital_icon_clock);
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame;
#endif
indicatorIcons.push_back(digital_icon_clock);
}
#endif
if (!hiddenFrames.home) {
fsi.positions.home = appendFrame(graphics::UIRenderer::drawDeviceFocused, icon_home);
fsi.positions.home = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
indicatorIcons.push_back(icon_home);
}
fsi.positions.textMessage = appendFrame(graphics::MessageRenderer::drawTextMessageFrame, icon_mail);
fsi.positions.textMessage = numframes;
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
indicatorIcons.push_back(icon_mail);
#ifndef USE_EINK
if (!hiddenFrames.nodelist_nodes) {
fsi.positions.nodelist_nodes = appendFrame(graphics::NodeListRenderer::drawDynamicListScreen_Nodes, icon_nodes);
fsi.positions.nodelist_nodes = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes;
indicatorIcons.push_back(icon_nodes);
}
if (!hiddenFrames.nodelist_location) {
fsi.positions.nodelist_location = appendFrame(graphics::NodeListRenderer::drawDynamicListScreen_Location, icon_list);
fsi.positions.nodelist_location = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location;
indicatorIcons.push_back(icon_list);
}
#endif
// Show detailed node views only on E-Ink builds
#ifdef USE_EINK
if (!hiddenFrames.nodelist_lastheard) {
fsi.positions.nodelist_lastheard = appendFrame(graphics::NodeListRenderer::drawLastHeardScreen, icon_nodes);
fsi.positions.nodelist_lastheard = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
indicatorIcons.push_back(icon_nodes);
}
if (!hiddenFrames.nodelist_hopsignal) {
fsi.positions.nodelist_hopsignal = appendFrame(graphics::NodeListRenderer::drawHopSignalScreen, icon_signal);
fsi.positions.nodelist_hopsignal = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
indicatorIcons.push_back(icon_signal);
}
if (!hiddenFrames.nodelist_distance) {
fsi.positions.nodelist_distance = appendFrame(graphics::NodeListRenderer::drawDistanceScreen, icon_distance);
fsi.positions.nodelist_distance = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
indicatorIcons.push_back(icon_distance);
}
#endif
#if HAS_GPS
#ifdef USE_EINK
if (!hiddenFrames.nodelist_bearings) {
fsi.positions.nodelist_bearings = appendFrame(graphics::NodeListRenderer::drawNodeListWithCompasses, icon_list);
fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list);
}
#endif
if (!hiddenFrames.gps) {
fsi.positions.gps = appendFrame(graphics::UIRenderer::drawCompassAndLocationScreen, icon_compass);
fsi.positions.gps = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
indicatorIcons.push_back(icon_compass);
}
#endif
if (RadioLibInterface::instance && !hiddenFrames.lora) {
fsi.positions.lora = appendFrame(graphics::DebugRenderer::drawLoRaFocused, icon_radio);
fsi.positions.lora = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused;
indicatorIcons.push_back(icon_radio);
}
if (!hiddenFrames.system) {
fsi.positions.system = appendFrame(graphics::DebugRenderer::drawSystemScreen, icon_system);
fsi.positions.system = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen;
indicatorIcons.push_back(icon_system);
}
#if !defined(DISPLAY_CLOCK_FRAME)
if (!hiddenFrames.clock) {
fsi.positions.clock = appendFrame(uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame,
digital_icon_clock);
fsi.positions.clock = numframes;
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame;
indicatorIcons.push_back(digital_icon_clock);
}
#endif
if (!hiddenFrames.chirpy) {
fsi.positions.chirpy = appendFrame(graphics::DebugRenderer::drawChirpy, chirpy_small);
fsi.positions.chirpy = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy;
indicatorIcons.push_back(chirpy_small);
}
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
if (!hiddenFrames.wifi && isWifiAvailable()) {
fsi.positions.wifi = appendFrame(graphics::DebugRenderer::drawDebugInfoWiFiTrampoline, icon_wifi);
fsi.positions.wifi = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline;
indicatorIcons.push_back(icon_wifi);
}
#endif
moduleFrameStart = normalFrames.size();
moduleFrames = MeshModule::GetMeshModulesWithUIFrames();
// 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);
LOG_DEBUG("Show %d module frames", moduleFrames.size());
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;
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++;
}
}
LOG_DEBUG("Added modules. numframes: %d", normalFrames.size());
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--;
if (!hiddenFrames.show_favorites) {
uint8_t firstFavorite = 255;
uint8_t lastFavorite = 255;
// Temporary array to hold favorite node frames
std::vector<FrameCallback> favoriteFrames;
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (n && n->num != nodeDB->getNodeNum() && nodeInfoLiteIsFavorite(n)) {
const uint8_t frameIndex = appendFrame(graphics::UIRenderer::drawFavoriteNode, icon_node);
if (firstFavorite == 255)
firstFavorite = frameIndex;
lastFavorite = frameIndex;
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
}
}
fsi.positions.firstFavorite = firstFavorite;
fsi.positions.lastFavorite = lastFavorite;
// 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.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());
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);
ui->setFrames(normalFrames.data(), fsi.frameCount);
ui->setFrames(normalFrames, numframes);
ui->disableAllIndicators();
// Add overlays: frame icons and alert banner)
+18 -69
View File
@@ -154,8 +154,8 @@ static std::vector<uint32_t> seenPeers;
// Public helper so menus / store can clear stale registries
void clearThreadRegistries()
{
std::vector<int>().swap(seenChannels);
std::vector<uint32_t>().swap(seenPeers);
seenChannels.clear();
seenPeers.clear();
}
// Setter so other code can switch threads
@@ -387,58 +387,6 @@ 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
@@ -698,15 +646,19 @@ 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;
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) {
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));
isMine.push_back(mine);
isHeader.push_back(false);
ackForLine.push_back(AckStatus::NONE);
++wrappedCount;
}
}
@@ -1056,23 +1008,20 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
std::vector<int> rowHeights;
rowHeights.reserve(lines.size());
auto currentMetrics = graphics::EmoteRenderer::LineMetrics{};
auto nextMetrics = lines.empty()
? graphics::EmoteRenderer::LineMetrics{}
: graphics::EmoteRenderer::analyzeLine(nullptr, lines[0], FONT_HEIGHT_SMALL, emotes, numEmotes);
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));
}
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 = currentMetrics.tallestHeight;
const bool hasEmote = currentMetrics.hasEmote;
const bool nextHasEmote = (idx + 1 < lines.size()) && nextMetrics.hasEmote;
const int tallestEmote = lineMetrics[idx].tallestHeight;
const bool hasEmote = lineMetrics[idx].hasEmote;
const bool nextHasEmote = (idx + 1 < lines.size()) && lineMetrics[idx + 1].hasEmote;
if (isHeaderVec[idx]) {
// Header line spacing
+5 -2
View File
@@ -244,10 +244,13 @@ void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to)
p->decoded.request_id = to.id;
}
std::vector<MeshModule *> MeshModule::GetMeshModulesWithUIFrames()
std::vector<MeshModule *> MeshModule::GetMeshModulesWithUIFrames(int startIndex)
{
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;
@@ -308,4 +311,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();
static std::vector<MeshModule *> GetMeshModulesWithUIFrames(int startIndex);
static void observeUIEvents(Observer<const UIFrameEvent *> *observer);
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
+12 -7
View File
@@ -1542,6 +1542,8 @@ void NodeDB::pickNewNodeNum()
// Identity check via public key (or "empty slot?" when no keys yet);
// macaddr no longer lives on the slim header.
// This check does not work when is_licensed=true since we don't store a public key.
// Revisit with XEdDSA signing.
auto isOurOwnEntry = [&](const meshtastic_NodeInfoLite *n) -> bool {
if (!n)
return false;
@@ -1550,13 +1552,16 @@ void NodeDB::pickNewNodeNum()
return !nodeInfoLiteHasUser(n);
};
meshtastic_NodeInfoLite *found;
while (((found = getMeshNode(nodeNum)) && !isOurOwnEntry(found)) ||
(nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) {
NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice
if (found)
LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, picking 0x%x", nodeNum, candidate);
nodeNum = candidate;
// Short circuit the check for licensed devices since they do not have public keys to compare against the nodeDB.
if (!owner.is_licensed) {
meshtastic_NodeInfoLite *found;
while (((found = getMeshNode(nodeNum)) && !isOurOwnEntry(found)) ||
(nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) {
NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice
if (found)
LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, picking 0x%x", nodeNum, candidate);
nodeNum = candidate;
}
}
LOG_DEBUG("Use nodenum 0x%x ", nodeNum);
+91 -145
View File
@@ -44,8 +44,6 @@ 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
@@ -146,36 +144,6 @@ 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
@@ -187,9 +155,10 @@ int CannedMessageModule::splitConfiguredMessages()
{
int i = 0;
String canned_messages = cannedMessageModuleConfig.messages;
// Copy all message parts into the buffer
strncpy(this->messageBuffer, cannedMessageModuleConfig.messages, sizeof(this->messageBuffer) - 1);
this->messageBuffer[sizeof(this->messageBuffer) - 1] = '\0';
strncpy(this->messageBuffer, canned_messages.c_str(), sizeof(this->messageBuffer));
// Temporary array to allow for insertion
const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0};
@@ -253,7 +222,7 @@ void CannedMessageModule::resetSearch()
{
int previousDestIndex = destIndex;
searchQuery[0] = '\0';
searchQuery = "";
updateDestinationSelectionList();
// Adjust scrollIndex so previousDestIndex is still visible
@@ -269,21 +238,26 @@ void CannedMessageModule::resetSearch()
}
void CannedMessageModule::updateDestinationSelectionList()
{
static size_t lastNumMeshNodes = 0;
static String lastSearchQuery = "";
size_t numMeshNodes = nodeDB->getNumMeshNodes();
bool nodesChanged = (numMeshNodes != cachedDestinationNodeCount);
cachedDestinationNodeCount = numMeshNodes;
bool nodesChanged = (numMeshNodes != lastNumMeshNodes);
lastNumMeshNodes = numMeshNodes;
// Early exit if nothing changed
if (destinationSelectionCacheValid && strcmp(searchQuery, cachedDestinationSearchQuery) == 0 && !nodesChanged)
if (searchQuery == lastSearchQuery && !nodesChanged)
return;
strncpy(cachedDestinationSearchQuery, searchQuery, sizeof(cachedDestinationSearchQuery) - 1);
cachedDestinationSearchQuery[sizeof(cachedDestinationSearchQuery) - 1] = '\0';
lastSearchQuery = searchQuery;
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);
@@ -292,27 +266,38 @@ void CannedMessageModule::updateDestinationSelectionList()
if (!node || node->num == myNodeNum || !nodeInfoLiteHasUser(node) || node->public_key.size != 32)
continue;
const char *nodeName = node->long_name;
const char *shortName = node->short_name;
const String &nodeName = node->long_name;
if (searchQuery[0] == '\0') {
this->filteredNodes.push_back({node, sinceLastSeen(node)});
} else if (containsIgnoreCase(nodeName, searchQuery) || containsIgnoreCase(shortName, searchQuery)) {
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)});
}
}
}
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) {
const char *name = channels.getName(i);
if (name && name[0] && !activeChannelNameSeen(activeChannelIndices, name)) {
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(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();
@@ -495,11 +480,7 @@ 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
@@ -511,26 +492,6 @@ 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 ||
@@ -586,15 +547,11 @@ 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) {
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();
}
this->searchQuery += (char)event->kbchar;
needsUpdate = true;
if ((millis() - lastFilterUpdate) > filterDebounceMs) {
runOnce(); // update filter immediately
lastFilterUpdate = millis();
}
return 1;
}
@@ -608,13 +565,12 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
// Handle backspace
if (event->inputEvent == INPUT_BROKER_BACK) {
size_t queryLen = strlen(searchQuery);
if (queryLen > 0) {
searchQuery[queryLen - 1] = '\0';
if (searchQuery.length() > 0) {
searchQuery.remove(searchQuery.length() - 1);
needsUpdate = true;
runOnce();
}
if (searchQuery[0] == '\0') {
if (searchQuery.length() == 0) {
resetSearch();
needsUpdate = false;
}
@@ -685,7 +641,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[0] = '\0';
searchQuery = "";
// UIFrameEvent e;
// e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
@@ -715,7 +671,8 @@ 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);
releaseFreetext();
freetext = "";
cursor = 0;
payload = 0;
currentMessageIndex = -1;
@@ -805,7 +762,8 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
// Return to inactive state
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
this->currentMessageIndex = -1;
this->releaseFreetext();
this->freetext = "";
this->cursor = 0;
// Force display update to show normal screen
UIFrameEvent e;
@@ -864,7 +822,8 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
// Cancel (dismiss freetext screen)
if (event->inputEvent == INPUT_BROKER_LEFT) {
updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
releaseFreetext();
freetext = "";
cursor = 0;
payload = 0;
currentMessageIndex = -1;
@@ -987,7 +946,8 @@ 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);
releaseFreetext();
freetext = "";
cursor = 0;
payload = 0;
currentMessageIndex = -1;
@@ -1227,7 +1187,8 @@ int32_t CannedMessageModule::runOnce()
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->currentMessageIndex = -1;
this->releaseFreetext();
this->freetext = "";
this->cursor = 0;
this->notifyObservers(&e);
return 2000;
}
@@ -1240,21 +1201,24 @@ int32_t CannedMessageModule::runOnce()
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->currentMessageIndex = -1;
this->releaseFreetext();
this->freetext = "";
this->cursor = 0;
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->releaseFreetext();
this->freetext = "";
this->cursor = 0;
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->releaseFreetext();
this->freetext = "";
this->cursor = 0;
this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE);
// Clean up virtual keyboard if it exists during timeout
@@ -1276,7 +1240,8 @@ int32_t CannedMessageModule::runOnce()
// Clean up state but *dont* deactivate yet
this->currentMessageIndex = -1;
this->releaseFreetext();
this->freetext = "";
this->cursor = 0;
// Tell Screen to jump straight to the TextMessage frame
e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
@@ -1302,7 +1267,8 @@ int32_t CannedMessageModule::runOnce()
// Clean up state
this->currentMessageIndex = -1;
this->releaseFreetext();
this->freetext = "";
this->cursor = 0;
// Tell Screen to jump straight to the TextMessage frame
e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
@@ -1319,7 +1285,8 @@ int32_t CannedMessageModule::runOnce()
}
// fallback clean-up if nothing above returned
this->currentMessageIndex = -1;
this->releaseFreetext();
this->freetext = "";
this->cursor = 0;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->notifyObservers(&e);
@@ -1345,13 +1312,15 @@ int32_t CannedMessageModule::runOnce()
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) {
if (this->messagesCount > 0) {
this->currentMessageIndex = getPrevIndex();
this->releaseFreetext();
this->freetext = "";
this->cursor = 0;
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->releaseFreetext();
this->freetext = "";
this->cursor = 0;
this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE);
}
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) {
@@ -1525,10 +1494,8 @@ void CannedMessageModule::drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState
display->setColor(OLEDDISPLAY_COLOR::WHITE);
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->drawStringMaxWidth(0, 0, display->getWidth(),
cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor));
display->setFont(FONT_MEDIUM);
@@ -1714,11 +1681,8 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
// Header
int titleY = 2;
char titleText[64];
if (searchQuery[0])
snprintf(titleText, sizeof(titleText), "Select Destination [%s]", searchQuery);
else
snprintf(titleText, sizeof(titleText), "Select Destination [ ]");
String titleText = "Select Destination";
titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]";
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->drawString(display->getWidth() / 2, titleY, titleText);
display->setTextAlignment(TEXT_ALIGN_LEFT);
@@ -1745,27 +1709,26 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
int xOffset = 0;
int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset;
char entryText[96] = "";
std::string entryText;
// Draw Channels First
if (itemIndex < numActiveChannels) {
uint8_t channelIndex = this->activeChannelIndices[itemIndex];
const char *channelName = channels.getName(channelIndex);
snprintf(entryText, sizeof(entryText), "#%s", channelName ? channelName : "?");
entryText = std::string("#") + (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) {
nodeName = node->short_name;
entryText = node->short_name;
} else if (node->long_name[0]) {
nodeName = node->long_name;
entryText = node->long_name;
} else {
nodeName = node->short_name;
entryText = node->short_name;
}
}
@@ -1775,21 +1738,22 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
if (availWidth < 0)
availWidth = 0;
char truncatedEntry[96];
graphics::UIRenderer::truncateStringWithEmotes(display, nodeName ? nodeName : "", truncatedEntry, sizeof(truncatedEntry),
graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry),
availWidth);
snprintf(entryText, sizeof(entryText), "%s", truncatedEntry);
entryText = truncatedEntry;
// Prepend "* " if this is a favorite
if (nodeInfoLiteIsFavorite(node)) {
snprintf(entryText, sizeof(entryText), "* %s", truncatedEntry);
entryText = "* " + entryText;
}
graphics::UIRenderer::truncateStringWithEmotes(display, entryText, truncatedEntry, sizeof(truncatedEntry), availWidth);
snprintf(entryText, sizeof(entryText), "%s", truncatedEntry);
graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry),
availWidth);
entryText = truncatedEntry;
}
}
if (entryText[0] == '\0' || strcmp(entryText, "Unknown") == 0)
snprintf(entryText, sizeof(entryText), "?");
if (entryText.empty() || entryText == "Unknown")
entryText = "?";
// Highlight background (if selected)
if (itemIndex == destIndex) {
@@ -1799,7 +1763,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
}
// Draw entry text
graphics::UIRenderer::drawStringWithEmotes(display, xOffset + 2, yOffset, entryText, FONT_HEIGHT_SMALL, 1, false);
graphics::UIRenderer::drawStringWithEmotes(display, xOffset + 2, yOffset, entryText.c_str(), FONT_HEIGHT_SMALL, 1, false);
display->setColor(WHITE);
// Draw key icon (after highlight)
@@ -2058,9 +2022,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
display->setColor(WHITE);
{
int inputY = 0 + y + 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);
String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor);
drawWrappedEmoteText(display, x, inputY, msgWithCursor.c_str(), display->getWidth() - x, FONT_HEIGHT_SMALL);
}
#endif
return;
@@ -2405,27 +2368,10 @@ void CannedMessageModule::handleSetCannedMessageModuleMessages(const char *from_
}
}
void CannedMessageModule::drawWithCursor(char *buffer, size_t bufferSize, const char *text, size_t cursor)
String CannedMessageModule::drawWithCursor(String text, int cursor)
{
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';
String result = text.substring(0, cursor) + "_" + text.substring(cursor);
return result;
}
#endif
+2 -8
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;
void drawWithCursor(char *buffer, size_t bufferSize, const char *text, size_t cursor);
String drawWithCursor(String text, int cursor);
// === Emote Picker ===
int handleEmotePickerInput(const InputEvent *event);
@@ -146,8 +146,7 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
int visibleRows = 0;
bool needsUpdate = true;
unsigned long lastUpdateMillis = 0;
static constexpr size_t searchQuerySize = 33;
char searchQuery[searchQuerySize] = {0};
String searchQuery;
String freetext;
// === Message Storage ===
@@ -176,9 +175,6 @@ 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;
@@ -188,8 +184,6 @@ 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; // deliberate copy: the licensed strip below must not clobber the global owner state
ignoreRequest = false; // Don't ignore requests anymore
meshtastic_User &u = owner;
// Strip the public key if the user is licensed
if (u.is_licensed && u.public_key.size > 0) {
+25 -27
View File
@@ -193,36 +193,24 @@ void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display)
display->setTextAlignment(TEXT_ALIGN_LEFT);
const uint16_t maxWrapWidth = display->width() - 40;
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) {
auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector<std::string> {
std::vector<std::string> wrapped;
std::string current;
std::string word;
const char *p = text;
while (*p && lineCount < maxContentLines + (hasTitle ? 1 : 0)) {
while (*p && wrapped.size() < maxContentLines) {
while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
if (*p == '\n') {
if (!current.empty()) {
appendLine(current);
wrapped.push_back(current);
current.clear();
if (lineCount >= maxContentLines + (hasTitle ? 1 : 0))
if (wrapped.size() >= maxContentLines)
break;
}
}
++p;
}
if (!*p || lineCount >= maxContentLines + (hasTitle ? 1 : 0))
if (!*p || wrapped.size() >= maxContentLines)
break;
word.clear();
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r')
@@ -235,9 +223,9 @@ void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display)
current = test;
else {
if (!current.empty()) {
appendLine(current);
wrapped.push_back(current);
current = word;
if (lineCount >= maxContentLines + (hasTitle ? 1 : 0))
if (wrapped.size() >= maxContentLines)
break;
} else {
current = word;
@@ -247,26 +235,36 @@ void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display)
}
}
}
if (!current.empty() && lineCount < maxContentLines + (hasTitle ? 1 : 0))
appendLine(current);
if (!current.empty() && wrapped.size() < maxContentLines)
wrapped.push_back(current);
return wrapped;
};
std::vector<std::string> allLines;
if (hasTitle)
appendLine(popupTitle);
allLines.emplace_back(popupTitle);
char buf[sizeof(popupMessage)];
strncpy(buf, popupMessage, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *paragraph = strtok(buf, "\n");
while (paragraph && lineCount < maxContentLines + (hasTitle ? 1 : 0)) {
wrapText(paragraph, maxWrapWidth);
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);
}
paragraph = strtok(nullptr, "\n");
}
linePtrs[lineCount] = nullptr;
std::vector<const char *> ptrs;
for (const auto &ln : allLines)
ptrs.push_back(ln.c_str());
ptrs.push_back(nullptr);
// Use the standard notification box drawing from NotificationRenderer
NotificationRenderer::drawNotificationBox(display, nullptr, linePtrs, lineCount, 0, 0);
NotificationRenderer::drawNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0);
}
} // namespace graphics
+1 -1
View File
@@ -21,7 +21,7 @@ void TraceRouteModule::setResultText(const String &text)
void TraceRouteModule::clearResultLines()
{
std::vector<String>().swap(resultLines);
resultLines.clear();
resultLinesDirty = false;
}
#if HAS_SCREEN
+1 -2
View File
@@ -5,8 +5,7 @@
// 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);
// const-qualified so std::variant instantiation works on Apple libc++ (copying stays ill-formed either way)
DecodedServiceEnvelope(const DecodedServiceEnvelope &) = delete;
DecodedServiceEnvelope(DecodedServiceEnvelope &) = delete;
DecodedServiceEnvelope(DecodedServiceEnvelope &&);
~DecodedServiceEnvelope();
// Clients must check that this is true before using.
+3 -21
View File
@@ -40,7 +40,6 @@ constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
BLECharacteristic *fromNumCharacteristic;
BLECharacteristic *BatteryCharacteristic;
static int lastBatteryLevel = -1; // last value written to 0x2A19, to skip redundant writes/notifies
BLECharacteristic *logRadioCharacteristic;
BLEServer *bleServer;
@@ -719,8 +718,6 @@ void NimbleBluetooth::deinit()
#endif
BLEDevice::deinit(true);
BatteryCharacteristic = nullptr; // freed by deinit; clear so updateBatteryLevel() won't touch it
lastBatteryLevel = -1;
#endif
}
@@ -859,31 +856,16 @@ void NimbleBluetooth::setupService()
BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
(uint16_t)0x2a19, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
BatteryCharacteristic->addDescriptor(batteryLevelDescriptor);
// Seed an initial 0-100 level so an early read of 0x2A19 returns a valid value.
uint8_t initialLevel = (powerStatus && powerStatus->getHasBattery()) ? powerStatus->getBatteryChargePercent() : 0;
if (initialLevel > 100)
initialLevel = 100;
BatteryCharacteristic->setValue(&initialLevel, 1);
lastBatteryLevel = initialLevel;
batteryService->start();
}
/// Given a level between 0-100, update the BLE attribute
void updateBatteryLevel(uint8_t level)
{
if (!config.bluetooth.enabled || !BatteryCharacteristic)
return;
if (level > 100) // 0x2A19 must stay within the BAS 0-100 range
level = 100;
if (level == lastBatteryLevel)
return;
lastBatteryLevel = level;
// Cache the value so a READ works without a subscriber; notify only when connected.
BatteryCharacteristic->setValue(&level, 1);
if (nimbleBluetooth && nimbleBluetooth->isConnected())
if ((config.bluetooth.enabled == true) && nimbleBluetooth && nimbleBluetooth->isConnected()) {
BatteryCharacteristic->setValue(&level, 1);
BatteryCharacteristic->notify();
}
}
void NimbleBluetooth::clearBonds()
+2 -12
View File
@@ -15,9 +15,8 @@ static BLECharacteristic fromRadio = BLECharacteristic(BLEUuid(FROMRADIO_UUID_16
static BLECharacteristic toRadio = BLECharacteristic(BLEUuid(TORADIO_UUID_16));
static BLECharacteristic logRadio = BLECharacteristic(BLEUuid(LOGRADIO_UUID_16));
static BLEDis bledis; // DIS (Device Information Service) helper class instance
static BLEBas blebas; // BAS (Battery Service) helper class instance
static int lastBatteryLevel = -1; // last value written to BAS, to skip redundant writes/notifies
static BLEDis bledis; // DIS (Device Information Service) helper class instance
static BLEBas blebas; // BAS (Battery Service) helper class instance
#ifndef BLE_DFU_SECURE
static BLEDfu bledfu; // DFU software update helper service
#else
@@ -337,7 +336,6 @@ void NRF52Bluetooth::setup()
LOG_INFO("Init the Battery Service");
blebas.begin();
blebas.write(0); // Unknown battery level for now
lastBatteryLevel = 0;
// Setup the Heart Rate Monitor service using
// BLEService and BLECharacteristic classes
LOG_INFO("Init the Mesh bluetooth service");
@@ -357,14 +355,6 @@ void NRF52Bluetooth::resumeAdvertising()
/// Given a level between 0-100, update the BLE attribute
void updateBatteryLevel(uint8_t level)
{
if (!nrf52Bluetooth) // skip until the Battery Service has been begun in setup()
return;
if (level > 100) // BAS battery level must stay within 0-100
level = 100;
if (level == lastBatteryLevel)
return;
lastBatteryLevel = level;
blebas.write(level);
}
void NRF52Bluetooth::clearBonds()
@@ -1,5 +1,4 @@
#include "TestUtil.h"
#include <cstdlib>
#include <unity.h>
static void test_placeholder()
@@ -1,5 +1,4 @@
#include "TestUtil.h"
#include <cstdlib>
#include <unity.h>
#if defined(ARCH_PORTDUINO)
+1
View File
@@ -55,6 +55,7 @@ build_flags_common =
-lyaml-cpp
-ljsoncpp
!pkg-config --cflags jsoncpp --silence-errors || :
-li2c
-luv
-std=gnu17
-std=gnu++17