Merge upstream develop into packet authentication policy

This commit is contained in:
Benjamin Faershtein
2026-07-20 11:48:59 -07:00
74 changed files with 836 additions and 939 deletions
-38
View File
@@ -30,44 +30,6 @@ SPIClass SPI_HSPI(HSPI);
#endif // HAS_SDCARD
/**
* @brief Copies a file from one location to another.
*
* @param from The path of the source file.
* @param to The path of the destination file.
* @return true if the file was successfully copied, false otherwise.
*/
bool copyFile(const char *from, const char *to)
{
#ifdef FSCom
// take SPI Lock
concurrency::LockGuard g(spiLock);
unsigned char cbuffer[16];
File f1 = FSCom.open(from, FILE_O_READ);
if (!f1) {
LOG_ERROR("Failed to open source file %s", from);
return false;
}
File f2 = FSCom.open(to, FILE_O_WRITE);
if (!f2) {
LOG_ERROR("Failed to open destination file %s", to);
return false;
}
while (f1.available() > 0) {
byte i = f1.read(cbuffer, 16);
f2.write(cbuffer, i);
}
f2.flush();
f2.close();
f1.close();
return true;
#endif
}
/**
* Renames a file from pathFrom to pathTo.
*
-1
View File
@@ -57,7 +57,6 @@ using namespace Adafruit_LittleFS_Namespace;
#endif
void fsInit();
bool copyFile(const char *from, const char *to);
bool renameFile(const char *pathFrom, const char *pathTo);
bool fsFormat();
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels, size_t maxCount = 64, bool *wasLimited = nullptr);
-35
View File
@@ -7,7 +7,6 @@
#include "SafeFile.h"
#include "gps/RTC.h"
#include "memory/MemAudit.h"
#include <cassert>
#include <cstring> // memcpy
#ifndef MESSAGE_TEXT_POOL_SIZE
@@ -244,40 +243,6 @@ const StoredMessage *MessageStore::tryAddFromPacket(const meshtastic_MeshPacket
return &liveMessages.back();
}
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
{
const StoredMessage *stored = tryAddFromPacket(packet);
assert(stored);
return *stored;
}
// Outgoing/manual message
void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text)
{
StoredMessage sm;
// Always use our local time (helper handles RTC vs boot time)
assignTimestamp(sm);
sm.sender = sender;
sm.channelIndex = channelIndex;
sm.textOffset = storeTextInPool(text.c_str(), text.size());
sm.textLength = text.size();
// Use the provided destination
sm.dest = sender;
sm.type = MessageType::DM_TO_US;
// Outgoing messages always start with unknown ack status
sm.ackStatus = AckStatus::NONE;
addLiveMessage(sm);
#if ENABLE_MESSAGE_PERSISTENCE
markMessageStoreUnsaved();
#endif
}
#if ENABLE_MESSAGE_PERSISTENCE
// Compact, fixed-size on-flash representation using offset + length
+1 -3
View File
@@ -93,10 +93,8 @@ class MessageStore
void addLiveMessage(StoredMessage &&msg);
void addLiveMessage(const StoredMessage &msg); // convenience overload
const std::deque<StoredMessage> &getLiveMessages() const { return liveMessages; }
// Add new messages from packets. Returns nullptr if the packet is filtered out.
const StoredMessage *tryAddFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing -> RAM only
// Add new messages from packets or manual input
const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only
void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add
// Persistence methods (used only on boot/shutdown)
void saveToFlash(); // Save messages to flash
-10
View File
@@ -57,16 +57,6 @@ void consoleInit()
DEBUG_PORT.rpInit(); // Simply sets up semaphore
}
/// Print and flush an unclassified formatted console message.
void consolePrintf(const char *format, ...)
{
va_list arg;
va_start(arg, format);
console->vprintf(nullptr, format, arg);
va_end(arg);
console->flush();
}
/// Initialize console, protobuf transport, serial port, and worker thread state.
SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), concurrency::OSThread("SerialConsole")
{
-1
View File
@@ -67,7 +67,6 @@ class SerialConsole : public StreamAPI, public RedirectablePrint, private concur
};
// A simple wrapper to allow non class aware code write to the console
void consolePrintf(const char *format, ...);
void consoleInit();
extern SerialConsole *console;
-12
View File
@@ -194,18 +194,6 @@ void playBoop()
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
}
void playLongPressLeadUp()
{
// An ascending lead-up sequence for long press - builds anticipation
ToneDuration melody[] = {
{NOTE_C3, 100}, // Start low
{NOTE_E3, 100}, // Step up
{NOTE_G3, 100}, // Keep climbing
{NOTE_B3, 150} // Peak with longer note for emphasis
};
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
}
// Static state for progressive lead-up notes
static int leadUpNoteIndex = 0;
static const ToneDuration leadUpNotes[] = {
-1
View File
@@ -12,6 +12,5 @@ void play4ClickUp();
void playBoop();
void playChirp();
void playClick();
void playLongPressLeadUp();
bool playNextLeadUpNote(); // Play the next note in the lead-up sequence
void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning
-5
View File
@@ -2201,11 +2201,6 @@ bool GPS::hasLock()
return false;
}
bool GPS::hasFlow()
{
return reader.passedChecksum() > 0;
}
bool GPS::whileActive()
{
unsigned int charsInBuf = 0;
-3
View File
@@ -111,9 +111,6 @@ class GPS : private concurrency::OSThread
/// Returns true if we have acquired GPS lock.
virtual bool hasLock();
/// Returns true if there's valid data flow with the chip.
virtual bool hasFlow();
/// Return true if we are connected to a GPS
bool isConnected() const { return hasGPS; }
-28
View File
@@ -496,34 +496,6 @@ float GeoCoord::rangeMetersToRadians(double range_meters)
return (PI / (180 * 60)) * distance_nm;
}
/**
* Ported from http://www.edwilliams.org/avform147.htm#Intro
* @brief Convert from radians to range in meters on a great circle
* @param range_radians
* The range in radians
* @return Range in meters on a great circle
*/
float GeoCoord::rangeRadiansToMeters(double range_radians)
{
double distance_nm = ((180 * 60) / PI) * range_radians;
// 1 meter is 0.000539957 nm
return distance_nm * 0.000539957;
}
// Find distance from point to passed in point
int32_t GeoCoord::distanceTo(const GeoCoord &pointB)
{
return latLongToMeter(this->getLatitude() * 1e-7, this->getLongitude() * 1e-7, pointB.getLatitude() * 1e-7,
pointB.getLongitude() * 1e-7);
}
// Find bearing from point to passed in point
int32_t GeoCoord::bearingTo(const GeoCoord &pointB)
{
return bearing(this->getLatitude() * 1e-7, this->getLongitude() * 1e-7, pointB.getLatitude() * 1e-7,
pointB.getLongitude() * 1e-7);
}
/**
* Create a new point based on the passed-in point
* Ported from http://www.edwilliams.org/avform147.htm#LL
-3
View File
@@ -103,7 +103,6 @@ class GeoCoord
static void convertWGS84ToOSGB36(const double lat, const double lon, double &osgb_Latitude, double &osgb_Longitude);
static float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b);
static float bearing(double lat1, double lon1, double lat2, double lon2);
static float rangeRadiansToMeters(double range_radians);
static float rangeMetersToRadians(double range_meters);
static unsigned int bearingToDegrees(const char *bearing);
static const char *degreesToBearing(unsigned int degrees);
@@ -114,8 +113,6 @@ class GeoCoord
static double toDegrees(double r);
// Point to point conversions
int32_t distanceTo(const GeoCoord &pointB);
int32_t bearingTo(const GeoCoord &pointB);
std::shared_ptr<GeoCoord> pointAtDistance(double bearing, double range);
// Lat lon alt getters
+22 -2
View File
@@ -77,12 +77,13 @@ EInkParallelDisplay::~EInkParallelDisplay()
bool EInkParallelDisplay::connect()
{
LOG_INFO("Do EPD init");
int initRc = BBEP_SUCCESS;
if (!epaper) {
epaper = new FASTEPD;
#if defined(T5_S3_EPAPER_PRO_V1)
epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
#elif defined(T5_S3_EPAPER_PRO_V2)
epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
// initialize all port 0 pins (0-7) as outputs / HIGH
for (int i = 0; i < 8; i++) {
epaper->ioPinMode(i, OUTPUT);
@@ -93,6 +94,16 @@ bool EInkParallelDisplay::connect()
#endif
}
// FastEPD allocates its framebuffer only from PSRAM; if PSRAM init failed the alloc returns
// NULL and initPanel() returns an error, so clearWhite() below would memset(NULL). Skip EInk
// bring-up but return true so OLEDDisplay still allocates its base buffer (base draw ops stay
// safe); displayReady stays false so the FastEPD push paths no-op -> node runs headless.
if (initRc != BBEP_SUCCESS || epaper->currentBuffer() == nullptr) {
LOG_ERROR("EPD framebuffer unavailable (initPanel rc=%d, PSRAM=%u); running headless", initRc,
(unsigned)ESP.getPsramSize());
return true;
}
// epaper->setRotation(rotation); // does not work, messes up width/height
epaper->setMode(BB_MODE_1BPP);
epaper->clearWhite();
@@ -103,6 +114,7 @@ bool EInkParallelDisplay::connect()
resetGhostPixelTracking();
#endif
displayReady = true;
return true;
}
@@ -187,6 +199,9 @@ void EInkParallelDisplay::asyncFullUpdateTask(void *pvParameters)
*/
void EInkParallelDisplay::display(void)
{
if (!displayReady) // no framebuffer (PSRAM absent / init failed) -> nothing to push
return;
const uint16_t w = this->displayWidth;
const uint16_t h = this->displayHeight;
@@ -400,6 +415,9 @@ void EInkParallelDisplay::resetGhostPixelTracking()
*/
bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit)
{
if (!displayReady)
return false;
uint32_t now = millis();
if (lastDrawMsec == 0 || (now - lastDrawMsec) > msecLimit) {
display();
@@ -410,6 +428,8 @@ bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit)
void EInkParallelDisplay::endUpdate()
{
if (!displayReady)
return;
{
// ensure any async full update is started/completed
if (asyncFullRunning.load()) {
+3
View File
@@ -40,6 +40,9 @@ class EInkParallelDisplay : public OLEDDisplay
uint32_t lastDrawMsec = 0;
FASTEPD *epaper;
// Set only when connect() fully succeeds; framebuffer-touching methods no-op while false.
bool displayReady = false;
private:
// Async full-refresh support
std::atomic<bool> asyncFullRunning{false};
-34
View File
@@ -1846,40 +1846,6 @@ void Screen::handleStartFirmwareUpdateScreen()
setFrameImmediateDraw(frames);
}
void Screen::blink()
{
#ifdef MESHTASTIC_LOCKDOWN
// L4: defensive guard. blink() paints arbitrary geometry, not node
// data, so it doesn't actually leak today. But it bypasses the normal
// ui->update() path that the lockdown short-circuit gates, so any
// future change that puts content into blink would silently leak past
// redaction. Refuse to draw when the redaction latch is set.
if (meshtastic_security::shouldRedactDisplay())
return;
#endif
setFastFramerate();
uint8_t count = 10;
dispdev->setBrightness(254);
while (count > 0) {
dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight());
#if GRAPHICS_TFT_COLORING_ENABLED
prepareFrameColorRegions();
#endif
dispdev->display();
delay(50);
dispdev->clear();
#if GRAPHICS_TFT_COLORING_ENABLED
prepareFrameColorRegions();
#endif
dispdev->display();
delay(50);
count = count - 1;
}
// The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in
// OLEDDisplay.
dispdev->setBrightness(brightness);
}
void Screen::increaseBrightness()
{
brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62);
-2
View File
@@ -310,8 +310,6 @@ class Screen : public concurrency::OSThread
*/
void doDeepSleep();
void blink();
// Draw north
float estimatedHeading(double lat, double lon);
-5
View File
@@ -718,11 +718,6 @@ void VirtualKeyboard::setInputText(const std::string &text)
inputText = text;
}
std::string VirtualKeyboard::getInputText() const
{
return inputText;
}
void VirtualKeyboard::setHeader(const std::string &header)
{
headerText = header;
-1
View File
@@ -27,7 +27,6 @@ class VirtualKeyboard
void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
void setInputText(const std::string &text);
std::string getInputText() const;
void setHeader(const std::string &header);
void setCallback(std::function<void(const std::string &)> callback);
-124
View File
@@ -230,131 +230,7 @@ void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i
#endif
}
void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setFont(FONT_SMALL);
// The coordinates define the left starting point of the text
display->setTextAlignment(TEXT_ALIGN_LEFT);
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
display->setColor(BLACK);
}
char batStr[20];
if (powerStatus->getHasBattery()) {
int batV = powerStatus->getBatteryVoltageMv() / 1000;
int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10;
snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(),
powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' ');
// Line 1
display->drawString(x, y, batStr);
if (config.display.heading_bold)
display->drawString(x + 1, y, batStr);
} else {
// Line 1
display->drawString(x, y, "USB");
if (config.display.heading_bold)
display->drawString(x + 1, y, "USB");
}
uint32_t currentMillis = millis();
uint32_t seconds = currentMillis / 1000;
uint32_t minutes = seconds / 60;
uint32_t hours = minutes / 60;
uint32_t days = hours / 24;
// currentMillis %= 1000;
// seconds %= 60;
// minutes %= 60;
// hours %= 24;
// Show uptime as days, hours, minutes OR seconds
std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds);
// Line 1 (Still)
if (currentResolution != graphics::ScreenResolution::UltraLow) {
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
if (config.display.heading_bold)
display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
display->setColor(WHITE);
}
// Setup string to assemble analogClock string
std::string analogClock = "";
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
if (rtc_sec > 0) {
long hms = rtc_sec % SEC_PER_DAY;
// hms += tz.tz_dsttime * SEC_PER_HOUR;
// hms -= tz.tz_minuteswest * SEC_PER_MIN;
// mod `hms` to ensure in positive range of [0...SEC_PER_DAY)
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
// Tear apart hms into h:m:s
int hour, min, sec;
graphics::decomposeTime(rtc_sec, hour, min, sec);
char timebuf[12];
if (config.display.use_12h_clock) {
std::string meridiem = "am";
if (hour >= 12) {
if (hour > 12)
hour -= 12;
meridiem = "pm";
}
if (hour == 00) {
hour = 12;
}
snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str());
} else {
snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec);
}
analogClock += timebuf;
}
// Line 2
display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str());
// Display Channel Utilization
char chUtil[13];
snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent());
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil);
#if HAS_GPS
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
// Line 3
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude
UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
// Line 4
UIRenderer::drawGpsCoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus);
} else {
UIRenderer::drawGpsPowerStatus(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
}
#endif
/* Display a heartbeat pixel that blinks every time the frame is redrawn */
#ifdef SHOW_REDRAWS
if (heartbeat)
display->setPixel(0, 0);
heartbeat = !heartbeat;
#endif
}
// Trampoline functions for DebugInfo class access
void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
drawFrame(display, state, x, y);
}
void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
drawFrameSettings(display, state, x, y);
}
void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
drawFrameWiFi(display, state, x, y);
-3
View File
@@ -20,12 +20,9 @@ namespace DebugRenderer
{
// Debug frame functions
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Trampoline functions for framework callback compatibility
void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// LoRa information display
-34
View File
@@ -17,12 +17,6 @@
#include "meshUtils.h"
#include <algorithm>
// Forward declarations for functions defined in Screen.cpp
namespace graphics
{
extern bool haveGlyphs(const char *str);
} // namespace graphics
// Global screen instance
extern graphics::Screen *screen;
@@ -180,11 +174,6 @@ unsigned long getModeCycleIntervalMs()
return 3000;
}
int calculateMaxScroll(int totalEntries, int visibleRows)
{
return max(0, (totalEntries - 1) / (visibleRows * 2));
}
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
{
x = (currentResolution == ScreenResolution::High) ? x - 2 : (currentResolution == ScreenResolution::Low) ? x - 1 : x;
@@ -910,29 +899,6 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, headingRadian, lat, lon);
}
/// Draw a series of fields in a column, wrapping to multiple columns if needed
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields)
{
// The coordinates define the left starting point of the text
display->setTextAlignment(TEXT_ALIGN_LEFT);
const char **f = fields;
int xo = x, yo = y;
while (*f) {
display->drawString(xo, yo, *f);
if ((display->getColor() == BLACK) && config.display.heading_bold)
display->drawString(xo + 1, yo, *f);
display->setColor(WHITE);
yo += FONT_HEIGHT_SMALL;
if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) {
xo += SCREEN_WIDTH / 2;
yo = 0;
}
f++;
}
}
} // namespace NodeListRenderer
} // namespace graphics
#endif
-1
View File
@@ -58,7 +58,6 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
const char *getCurrentModeTitle_Nodes(int screenWidth);
const char *getCurrentModeTitle_Location(int screenWidth);
std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth);
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
// Scrolling controls
void scrollUp();
@@ -207,8 +207,6 @@ void NotificationRenderer::resetBanner()
alertBannerMessage[0] = '\0';
current_notification_type = notificationTypeEnum::none;
OnScreenKeyboardModule::instance().clearPopup();
inEvent.inputEvent = INPUT_BROKER_NONE;
inEvent.kbchar = 0;
curSelected = 0;
@@ -1187,9 +1185,6 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
display->setColor(WHITE);
// Draw the virtual keyboard
virtualKeyboard->draw(display, 0, 0);
// Draw transient popup overlay (if any) managed by OnScreenKeyboardModule
OnScreenKeyboardModule::instance().drawPopupOverlay(display);
} else {
// If virtualKeyboard is null, reset the banner to avoid getting stuck
LOG_INFO("Virtual keyboard is null - resetting banner");
@@ -1202,12 +1197,5 @@ bool NotificationRenderer::isOverlayBannerShowing()
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
}
void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs)
{
if (!title || !content || current_notification_type != notificationTypeEnum::text_input)
return;
OnScreenKeyboardModule::instance().showPopup(title, content, durationMs);
}
} // namespace graphics
#endif
-1
View File
@@ -39,7 +39,6 @@ class NotificationRenderer
static BannerFont alertBannerLineFonts[MAX_LINES + 1];
static void parseBannerMessageWithFonts(const char *message);
static void resetBanner();
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
-24
View File
@@ -1386,30 +1386,6 @@ int UIRenderer::formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLED
return display->getStringWidth(buf);
}
// Check if the display can render a string (detect special chars; emoji)
bool UIRenderer::haveGlyphs(const char *str)
{
#if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) || defined(OLED_CS)
// Don't want to make any assumptions about custom language support
return true;
#endif
// Check each character with the lookup function for the OLED library
// We're not really meant to use this directly..
bool have = true;
for (uint16_t i = 0; i < strlen(str); i++) {
uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]);
// If font doesn't support a character, it is substituted for ¿
if (result == 191 && (uint8_t)str[i] != 191) {
have = false;
break;
}
}
// LOG_DEBUG("haveGlyphs=%d", have);
return have;
}
#ifdef USE_EINK
/// Used on eink displays while in deep sleep
void UIRenderer::drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
-3
View File
@@ -104,9 +104,6 @@ class UIRenderer
{
drawStringWithEmotes(display, x, y, line.c_str(), fontHeight, emoteSpacing, fauxBold);
}
// Check if the display can render a string (detect special chars; emoji)
static bool haveGlyphs(const char *str);
}; // namespace UIRenderer
} // namespace graphics
-24
View File
@@ -649,30 +649,6 @@ std::string InkHUD::Applet::getTimeString()
return getTimeString(getValidTime(RTCQuality::RTCQualityDevice, true));
}
// Calculate how many nodes have been seen within our preferred window of activity
// This period is set by user, via the menu
// Todo: optimize to calculate once only per WindowManager::render
uint16_t InkHUD::Applet::getActiveNodeCount()
{
// Don't even try to count nodes if RTC isn't set
// The last heard values in nodedb will be incomprehensible
if (getRTCQuality() == RTCQualityNone)
return 0;
uint16_t count = 0;
// For each node in db
for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Check if heard recently, and not our own node
if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum())
count++;
}
return count;
}
// Get an abbreviated, human readable, distance string
// Honors config.display.units, to offer both metric and imperial
std::string InkHUD::Applet::localizeDistance(uint32_t meters)
-1
View File
@@ -183,7 +183,6 @@ class Applet : public GFX
SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value
std::string getTimeString(uint32_t epochSeconds); // Human readable
std::string getTimeString(); // Current time, human readable
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
std::string parse(const std::string &text); // Handle text which might contain special chars
std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars
@@ -217,13 +217,6 @@ bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n)
return true;
}
// Save messages to flash via the global messageStore.
// The global store holds messages for all channels; no per-channel file is needed.
void InkHUD::ThreadedMessageApplet::saveMessagesToFlash()
{
messageStore.saveToFlash();
}
// Messages are loaded once by InkHUD::begin() before applets start.
// Nothing to do here at per-applet activation time.
void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash()
@@ -46,7 +46,6 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule
bool approveNotification(Notification &n) override; // Which notifications to suppress
protected:
void saveMessagesToFlash();
void loadMessagesFromFlash();
uint8_t channelIndex = 0;
-38
View File
@@ -326,44 +326,6 @@ void InkHUD::InkHUD::touchNavDown()
}
}
// Call this when touch input needs joystick-like left navigation independent of joystick-enabled mode
void InkHUD::InkHUD::touchNavLeft()
{
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
case 1: // 90 deg
events->onTouchNavDown();
break;
case 2: // 180 deg
events->onTouchNavRight();
break;
case 3: // 270 deg
events->onTouchNavUp();
break;
default: // 0 deg
events->onTouchNavLeft();
break;
}
}
// Call this when touch input needs joystick-like right navigation independent of joystick-enabled mode
void InkHUD::InkHUD::touchNavRight()
{
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
case 1: // 90 deg
events->onTouchNavUp();
break;
case 2: // 180 deg
events->onTouchNavLeft();
break;
case 3: // 270 deg
events->onTouchNavDown();
break;
default: // 0 deg
events->onTouchNavRight();
break;
}
}
void InkHUD::InkHUD::touchTap(uint16_t x, uint16_t y)
{
events->onTouchTap(x, y, false);
-2
View File
@@ -71,8 +71,6 @@ class InkHUD
void navRight();
void touchNavUp();
void touchNavDown();
void touchNavLeft();
void touchNavRight();
void touchTap(uint16_t x, uint16_t y);
void touchLongPress(uint16_t x, uint16_t y);
+4 -2
View File
@@ -496,8 +496,10 @@ We keep this separate latest-message cache for this purpose, because:
Broadcasts and DMs take different paths into `messageStore`:
- **Broadcasts** - `ThreadedMessageApplet::handleReceived()` calls `messageStore.addFromPacket()`. `Events::onReceiveTextMessage()` then updates `latestMessage.broadcast` separately for fast access by `AllMessageApplet` and `NotificationApplet`.
- **DMs** - `ThreadedMessageApplet` skips DMs entirely. `Events::onReceiveTextMessage()` calls `messageStore.addFromPacket()` directly and stores the result in `latestMessage.dm`.
- **Broadcasts** - `ThreadedMessageApplet::handleReceived()` calls `messageStore.tryAddFromPacket()`. `Events::onReceiveTextMessage()` then updates `latestMessage.broadcast` separately for fast access by `AllMessageApplet` and `NotificationApplet`.
- **DMs** - `ThreadedMessageApplet` skips DMs entirely. `Events::onReceiveTextMessage()` calls `messageStore.tryAddFromPacket()` directly and stores the result in `latestMessage.dm`.
`tryAddFromPacket()` returns `nullptr` when the packet is filtered out (see `shouldStorePacket()`), so both paths must null-check before use.
#### Saving / Loading
-8
View File
@@ -117,14 +117,6 @@ void TwoButton::setHandlerDown(uint8_t whichButton, Callback onDown)
buttons[whichButton].onDown = onDown;
}
// Set what should happen when a button becomes unpressed
// Use this to implement a "While held" behavior
void TwoButton::setHandlerUp(uint8_t whichButton, Callback onUp)
{
assert(whichButton < 2);
buttons[whichButton].onUp = onUp;
}
// Set what should happen when a "short press" event has occurred
void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress)
{
-1
View File
@@ -38,7 +38,6 @@ class TwoButton : protected concurrency::OSThread
void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false);
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
void setHandlerDown(uint8_t whichButton, Callback onDown);
void setHandlerUp(uint8_t whichButton, Callback onUp);
void setHandlerShortPress(uint8_t whichButton, Callback onShortPress);
void setHandlerLongPress(uint8_t whichButton, Callback onLongPress);
@@ -194,14 +194,6 @@ void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown)
buttons[whichButton].onDown = onDown;
}
// Set what should happen when a button becomes unpressed
// Use this to implement a "While held" behavior
void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp)
{
assert(whichButton < 2);
buttons[whichButton].onUp = onUp;
}
// Set what should happen when a "short press" event has occurred
void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress)
{
@@ -217,26 +209,6 @@ void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLong
buttons[whichButton].onLongPress = onLongPress;
}
// Set what should happen when a joystick button becomes pressed
// Use this to implement a "while held" behavior
void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown)
{
joystick[Direction::UP].onDown = uDown;
joystick[Direction::DOWN].onDown = dDown;
joystick[Direction::LEFT].onDown = lDown;
joystick[Direction::RIGHT].onDown = rDown;
}
// Set what should happen when a joystick button becomes unpressed
// Use this to implement a "while held" behavior
void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp)
{
joystick[Direction::UP].onUp = uUp;
joystick[Direction::DOWN].onUp = dUp;
joystick[Direction::LEFT].onUp = lUp;
joystick[Direction::RIGHT].onUp = rUp;
}
// Set what should happen when a "press" event has fired
// Note: this will occur while the joystick button is still held
void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress)
@@ -49,11 +49,8 @@ class TwoButtonExtended : protected concurrency::OSThread
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
void setJoystickDebounce(uint32_t debounceMs);
void setHandlerDown(uint8_t whichButton, Callback onDown);
void setHandlerUp(uint8_t whichButton, Callback onUp);
void setHandlerShortPress(uint8_t whichButton, Callback onShortPress);
void setHandlerLongPress(uint8_t whichButton, Callback onLongPress);
void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown);
void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp);
void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress);
void setTwoWayRockerPressHandlers(Callback lPress, Callback rPress);
+1 -1
View File
@@ -1222,7 +1222,7 @@ bool runASAP;
// TODO find better home than main.cpp
extern meshtastic_DeviceMetadata getDeviceMetadata()
{
meshtastic_DeviceMetadata deviceMetadata = meshtastic_DeviceMetadata_init_zero;
meshtastic_DeviceMetadata deviceMetadata = meshtastic_DeviceMetadata_init_default;
strncpy(deviceMetadata.firmware_version, optstr(APP_VERSION), sizeof(deviceMetadata.firmware_version));
deviceMetadata.device_state_version = DEVICESTATE_CUR_VER;
deviceMetadata.canShutdown = pmu_found || HAS_CPU_SHUTDOWN;
-14
View File
@@ -9,7 +9,6 @@
*/
#include "memGet.h"
#include "configuration.h"
#include "memory/MemAudit.h"
#if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP)
#include <malloc.h>
@@ -108,16 +107,3 @@ uint32_t MemGet::getPsramSize()
return 0;
#endif
}
void displayPercentHeapFree()
{
uint32_t freeHeap = memGet.getFreeHeap();
uint32_t totalHeap = memGet.getHeapSize();
if (totalHeap == 0 || totalHeap == UINT32_MAX) {
LOG_INFO("Heap size unavailable");
return;
}
int percent = (int)((freeHeap * 100) / totalHeap);
LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap);
memaudit::logBreakdown("heap"); // per-subsystem breakdown rides along with the periodic heap log
}
+27 -15
View File
@@ -177,6 +177,9 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
}
#endif
if (p->to == NODENUM_BROADCAST_NO_LORA)
return false;
// Allow rebroadcast if hop_limit > 0 OR if we're exhausting hops (which sets hop_limit = 0 but still needs one relay)
if (!isToUs(p) && !isFromUs(p) && (p->hop_limit > 0 || exhaustHops)) {
if (p->id != 0) {
@@ -210,11 +213,10 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
}
#endif
if (p->next_hop == NO_NEXT_HOP_PREFERENCE) {
FloodingRouter::send(tosend);
} else {
NextHopRouter::send(tosend);
}
ErrorCode res =
(p->next_hop == NO_NEXT_HOP_PREFERENCE) ? FloodingRouter::send(tosend) : NextHopRouter::send(tosend);
if (res == ERRNO_SHOULD_RELEASE)
packetPool.release(tosend);
return true;
}
@@ -419,8 +421,10 @@ int32_t NextHopRouter::doRetransmissions()
trafficManagementModule->clearNextHop(p.packet->to);
}
#endif
if (auto *copy = packetPool.allocCopy(*p.packet))
FloodingRouter::send(copy);
if (auto *copy = packetPool.allocCopy(*p.packet)) {
if (FloodingRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
} else {
#if NEXTHOP_EARLY_FLOOD_ON_UNVERIFIED
// M4 (gated): if the route isn't proven healthy, don't spend a second directed
@@ -434,22 +438,30 @@ int32_t NextHopRouter::doRetransmissions()
meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to);
if (sentTo)
sentTo->next_hop = NO_NEXT_HOP_PREFERENCE;
if (auto *copy = packetPool.allocCopy(*p.packet))
FloodingRouter::send(copy);
if (auto *copy = packetPool.allocCopy(*p.packet)) {
if (FloodingRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
} else {
if (auto *copy = packetPool.allocCopy(*p.packet))
NextHopRouter::send(copy);
if (auto *copy = packetPool.allocCopy(*p.packet)) {
if (NextHopRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
}
#else
if (auto *copy = packetPool.allocCopy(*p.packet))
NextHopRouter::send(copy);
if (auto *copy = packetPool.allocCopy(*p.packet)) {
if (NextHopRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
#endif
}
} else {
// Note: we call the superclass version because we don't want to have our version of send() add a new
// retransmission record
if (auto *copy = packetPool.allocCopy(*p.packet))
FloodingRouter::send(copy);
if (auto *copy = packetPool.allocCopy(*p.packet)) {
if (FloodingRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
}
// Queue again
+4
View File
@@ -206,7 +206,11 @@ class NextHopRouter : public FloodingRouter
*/
std::optional<uint8_t> getNextHop(NodeNum to, uint8_t relay_node);
#ifdef PIO_UNIT_TESTING
public: // expose perhapsRebroadcast to the test shim
#else
private:
#endif
/** Check if we should be rebroadcasting this packet if so, do so.
* @return true if we did rebroadcast */
bool perhapsRebroadcast(const meshtastic_MeshPacket *p) override;
+8 -1
View File
@@ -3312,13 +3312,20 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
/** Update user info and channel for this node based on received user data
*/
bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex)
bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex, bool xeddsaSigned)
{
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
if (!info) {
return false;
}
// Once a node has proven it signs, only a signed update may change its identity. The public-key guard
// below is no help - an attacker can replay the victim's real (public) key. Our own record is exempt.
if (nodeId != getNodeNum() && nodeInfoLiteHasXeddsaSigned(info) && !xeddsaSigned) {
LOG_WARN("Refusing unsigned identity update for node 0x%08x that previously signed", nodeId);
return false;
}
#if !(MESHTASTIC_EXCLUDE_PKI)
if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) {
printBytes("Incoming Pubkey: ", p.public_key.bytes, 32);
+3 -3
View File
@@ -255,9 +255,9 @@ class NodeDB
*/
void updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src = RX_SRC_RADIO);
/** Update user info and channel for this node based on received user data
*/
bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0);
/** Update user info and channel for this node based on received user data.
* A known signer's identity is only learned when xeddsaSigned; defaults false so callers fail closed. */
bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0, bool xeddsaSigned = false);
/*
* Sets a node either favorite or unfavorite. Returns true if the node ends
+65 -19
View File
@@ -359,15 +359,6 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src)
return send(p);
}
}
/**
* Send a packet on a suitable interface.
*/
ErrorCode Router::rawSend(meshtastic_MeshPacket *p)
{
assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside)
return iface->send(p);
}
/**
* Send a packet on a suitable interface. This routine will
* later free() the packet to pool. This routine is not allowed to stall.
@@ -531,6 +522,62 @@ void Router::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Rout
}
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
/** Size a decoded Data as the sender's signedDataFits() gate would have, with padding stripped:
* unknown fields inside Data.payload survive in payload.size and would otherwise let a forger
* inflate an unsigned broadcast past the signable budget. Returns false only if sizing failed.
* Sizing only what this build's schema decodes, so a signable type that later grows needs its
* legitimate maximum re-checked against the budget or honest unsigned broadcasts get dropped. */
static bool canonicalSignableSize(meshtastic_Data *d, size_t *size)
{
const pb_msgdesc_t *fields = nullptr;
switch (d->portnum) {
case meshtastic_PortNum_POSITION_APP:
fields = &meshtastic_Position_msg;
break;
case meshtastic_PortNum_TELEMETRY_APP:
fields = &meshtastic_Telemetry_msg;
break;
case meshtastic_PortNum_WAYPOINT_APP:
fields = &meshtastic_Waypoint_msg;
break;
case meshtastic_PortNum_NODEINFO_APP:
fields = &meshtastic_User_msg;
break;
default:
break;
}
if (fields) {
// Scratch kept off the stack: these decoded structs are large for the smaller MCU targets.
// Safe as file-static state because both callers of checkXeddsaReceivePolicy hold cryptLock.
static union {
// cppcheck-suppress unusedStructMember ; written by pb_decode through &inner
meshtastic_Position position;
// cppcheck-suppress unusedStructMember ; written by pb_decode through &inner
meshtastic_Telemetry telemetry;
// cppcheck-suppress unusedStructMember ; written by pb_decode through &inner
meshtastic_Waypoint waypoint;
// cppcheck-suppress unusedStructMember ; written by pb_decode through &inner
meshtastic_User user;
} inner;
memset(&inner, 0, sizeof(inner));
size_t canonicalPayload;
if (pb_decode_from_bytes(d->payload.bytes, d->payload.size, fields, &inner) &&
pb_get_encoded_size(&canonicalPayload, fields, &inner) && canonicalPayload <= d->payload.size) {
// Only the length matters when sizing a bytes field, so swap it in place instead of
// copying the whole Data; restored below because modules still need the real payload.
const pb_size_t prevSize = d->payload.size;
d->payload.size = (pb_size_t)canonicalPayload;
const bool sized = pb_get_encoded_size(size, &meshtastic_Data_msg, d);
d->payload.size = prevSize;
return sized;
}
}
return pb_get_encoded_size(size, &meshtastic_Data_msg, d);
}
enum class NodeInfoBootstrapResult { NOT_APPLICABLE, VERIFIED, INVALID };
static NodeInfoBootstrapResult verifyFirstContactNodeInfo(meshtastic_MeshPacket *p)
@@ -557,7 +604,7 @@ static NodeInfoBootstrapResult verifyFirstContactNodeInfo(meshtastic_MeshPacket
return NodeInfoBootstrapResult::VERIFIED;
}
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p, size_t encodedDataSize)
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
{
const auto policy = config.security.packet_signature_policy;
const bool strict = policy == meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT;
@@ -617,15 +664,14 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p, size_t encodedDataSize)
return true;
// In Balanced, preserve legacy unsigned-unicast compatibility and only reject a signable
// unsigned broadcast from a known signer. encodedDataSize is
// the size of the encoded Data exactly as the sender built it (or 0 to size p->decoded
// canonically); with no signature field present it is the unsigned base, and adding
// XEDDSA_SIGNATURE_FIELD_BYTES mirrors the sender-side signedDataFits() gate per packet,
// whatever fields the Data carried. Oversized broadcasts remain compatible.
// unsigned broadcast from a known signer. Canonical sizing removes unknown protobuf
// fields before mirroring the sender-side signedDataFits() gate. Oversized broadcasts
// remain compatible.
if (nodeDB->hasSeenXeddsaSigner(p->from) && isBroadcast(p->to)) {
if (encodedDataSize == 0 && !pb_get_encoded_size(&encodedDataSize, &meshtastic_Data_msg, &p->decoded))
size_t canonicalSize;
if (!canonicalSignableSize(&p->decoded, &canonicalSize))
return true; // can't size it; never drop on a sizing failure
if (encodedDataSize + XEDDSA_SIGNATURE_FIELD_BYTES + MESHTASTIC_HEADER_LENGTH <= MAX_LORA_PAYLOAD_LEN) {
if (canonicalSize + XEDDSA_SIGNATURE_FIELD_BYTES + MESHTASTIC_HEADER_LENGTH <= MAX_LORA_PAYLOAD_LEN) {
LOG_WARN("Dropping unsigned broadcast from 0x%08x that previously signed", p->from);
return false;
}
@@ -813,8 +859,8 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
p->channel = chIndex; // change to store the index instead of the hash
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
// Use the encoded Data size before merging local-only bitfield state.
if (!checkXeddsaReceivePolicy(p, rawSize))
// Run before merging local-only bitfield state into the decoded Data.
if (!checkXeddsaReceivePolicy(p))
return DecodeState::DECODE_POLICY_REJECT;
#endif
+3 -4
View File
@@ -94,7 +94,6 @@ class Router : protected concurrency::OSThread, protected PacketHistory
* NOTE: This method will free the provided packet (even if we return an error code)
*/
virtual ErrorCode send(meshtastic_MeshPacket *p);
virtual ErrorCode rawSend(meshtastic_MeshPacket *p);
/* Statistics for the amount of duplicate received packets and the amount of times we cancel a relay because someone did it
before us */
@@ -187,9 +186,9 @@ void resetRoutingAuthEvaluationCount();
meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p);
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
/** Enforce the configured XEdDSA receive policy; zero encodedDataSize derives it canonically.
* The caller must hold cryptLock. Returns false when the packet must be dropped. */
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p, size_t encodedDataSize = 0);
/** Enforce the configured XEdDSA receive policy. The caller must hold cryptLock.
* Returns false when the packet must be dropped. */
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p);
#endif
extern Router *router;
-137
View File
@@ -74,15 +74,7 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
ResourceNode *nodeAPIv1FromRadioOptions = new ResourceNode("/api/v1/fromradio", "OPTIONS", &handleAPIv1FromRadio);
ResourceNode *nodeAPIv1FromRadio = new ResourceNode("/api/v1/fromradio", "GET", &handleAPIv1FromRadio);
// ResourceNode *nodeHotspotApple = new ResourceNode("/hotspot-detect.html", "GET", &handleHotspot);
// ResourceNode *nodeHotspotAndroid = new ResourceNode("/generate_204", "GET", &handleHotspot);
ResourceNode *nodeAdmin = new ResourceNode("/admin", "GET", &handleAdmin);
// ResourceNode *nodeAdminSettings = new ResourceNode("/admin/settings", "GET", &handleAdminSettings);
// ResourceNode *nodeAdminSettingsApply = new ResourceNode("/admin/settings/apply", "POST", &handleAdminSettingsApply);
// ResourceNode *nodeAdminFs = new ResourceNode("/admin/fs", "GET", &handleFs);
// ResourceNode *nodeUpdateFs = new ResourceNode("/admin/fs/update", "POST", &handleUpdateFs);
// ResourceNode *nodeDeleteFs = new ResourceNode("/admin/fs/delete", "GET", &handleDeleteFsContent);
ResourceNode *nodeRestart = new ResourceNode("/restart", "POST", &handleRestart);
ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload);
@@ -100,8 +92,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
secureServer->registerNode(nodeAPIv1ToRadio);
secureServer->registerNode(nodeAPIv1FromRadioOptions);
secureServer->registerNode(nodeAPIv1FromRadio);
// secureServer->registerNode(nodeHotspotApple);
// secureServer->registerNode(nodeHotspotAndroid);
secureServer->registerNode(nodeRestart);
secureServer->registerNode(nodeFormUpload);
secureServer->registerNode(nodeJsonScanNetworks);
@@ -109,12 +99,7 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
secureServer->registerNode(nodeJsonDelete);
secureServer->registerNode(nodeJsonReport);
secureServer->registerNode(nodeJsonNodes);
// secureServer->registerNode(nodeUpdateFs);
// secureServer->registerNode(nodeDeleteFs);
secureServer->registerNode(nodeAdmin);
// secureServer->registerNode(nodeAdminFs);
// secureServer->registerNode(nodeAdminSettings);
// secureServer->registerNode(nodeAdminSettingsApply);
secureServer->registerNode(nodeRoot); // This has to be last
// Insecure nodes
@@ -122,20 +107,13 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
insecureServer->registerNode(nodeAPIv1ToRadio);
insecureServer->registerNode(nodeAPIv1FromRadioOptions);
insecureServer->registerNode(nodeAPIv1FromRadio);
// insecureServer->registerNode(nodeHotspotApple);
// insecureServer->registerNode(nodeHotspotAndroid);
insecureServer->registerNode(nodeRestart);
insecureServer->registerNode(nodeFormUpload);
insecureServer->registerNode(nodeJsonScanNetworks);
insecureServer->registerNode(nodeJsonFsBrowseStatic);
insecureServer->registerNode(nodeJsonDelete);
insecureServer->registerNode(nodeJsonReport);
// insecureServer->registerNode(nodeUpdateFs);
// insecureServer->registerNode(nodeDeleteFs);
insecureServer->registerNode(nodeAdmin);
// insecureServer->registerNode(nodeAdminFs);
// insecureServer->registerNode(nodeAdminSettings);
// insecureServer->registerNode(nodeAdminSettingsApply);
insecureServer->registerNode(nodeRoot); // This has to be last
}
@@ -230,36 +208,6 @@ void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res)
LOG_DEBUG("webAPI handleAPIv1ToRadio");
}
void htmlDeleteDir(const char *dirname)
{
File root = FSCom.open(dirname);
if (!root) {
return;
}
if (!root.isDirectory()) {
return;
}
File file = root.openNextFile();
while (file) {
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
htmlDeleteDir(file.name());
file.flush();
file.close();
} else {
String fileName = String(file.name());
file.flush();
file.close();
LOG_DEBUG(" %s", fileName.c_str());
FSCom.remove(fileName);
}
file = root.openNextFile();
}
root.flush();
root.close();
}
// Escape a string into a JSON double-quoted literal. Matches the previous
// SimpleJSON StringifyString behavior (0x00-0x1F and 0x7F -> \u00xx lowercase,
// escapes " \ / \b \f \n \r \t, UTF-8 passes through unchanged).
@@ -858,45 +806,6 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
res->print(out.c_str());
}
/*
This supports the Apple Captive Network Assistant (CNA) Portal
*/
void handleHotspot(HTTPRequest *req, HTTPResponse *res)
{
LOG_INFO("Hotspot Request");
/*
If we don't do a redirect, be sure to return a "Success" message
otherwise iOS will have trouble detecting that the connection to the SoftAP worked.
*/
// Status code is 200 OK by default.
// We want to deliver a simple HTML page, so we send a corresponding content type:
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET");
// res->println("<!DOCTYPE html>");
res->println("<meta http-equiv=\"refresh\" content=\"0;url=/\" />");
}
void handleDeleteFsContent(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET");
res->println("<h1>Meshtastic</h1>");
res->println("Delete Content in /static/*");
LOG_INFO("Delete files from /static/* : ");
concurrency::LockGuard g(spiLock);
htmlDeleteDir("/static");
res->println("<p><hr><p><a href=/admin>Back to admin</a>");
}
void handleAdmin(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
@@ -909,52 +818,6 @@ void handleAdmin(HTTPRequest *req, HTTPResponse *res)
res->println("<a href=/json/report>Device Report</a><br>");
}
void handleAdminSettings(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET");
res->println("<h1>Meshtastic</h1>");
res->println("This isn't done.");
res->println("<form action=/admin/settings/apply method=post>");
res->println("<table border=1>");
res->println("<tr><td>Set?</td><td>Setting</td><td>current value</td><td>new value</td></tr>");
res->println("<tr><td><input type=checkbox></td><td>WiFi SSID</td><td>false</td><td><input type=radio></td></tr>");
res->println("<tr><td><input type=checkbox></td><td>WiFi Password</td><td>false</td><td><input type=radio></td></tr>");
res->println(
"<tr><td><input type=checkbox></td><td>Smart Position Update</td><td>false</td><td><input type=radio></td></tr>");
res->println("</table>");
res->println("<table>");
res->println("<input type=submit value=Apply New Settings>");
res->println("<form>");
res->println("<p><hr><p><a href=/admin>Back to admin</a>");
}
void handleAdminSettingsApply(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "POST");
res->println("<h1>Meshtastic</h1>");
res->println(
"<html><head><meta http-equiv=\"refresh\" content=\"1;url=/admin/settings\" /><title>Settings Applied. </title>");
res->println("Settings Applied. Please wait.");
}
void handleFs(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET");
res->println("<h1>Meshtastic</h1>");
res->println("<a href=/admin/fs/delete>Delete Web Content</a><p><form action=/admin/fs/update "
"method=post><input type=submit value=UPDATE_WEB_CONTENT></form>Be patient!");
res->println("<p><hr><p><a href=/admin>Back to admin</a>");
}
void handleRestart(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
-6
View File
@@ -4,7 +4,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer);
// Declare some handler functions for the various URLs on the server
void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res);
void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res);
void handleHotspot(HTTPRequest *req, HTTPResponse *res);
void handleStatic(HTTPRequest *req, HTTPResponse *res);
void handleRestart(HTTPRequest *req, HTTPResponse *res);
void handleFormUpload(HTTPRequest *req, HTTPResponse *res);
@@ -13,12 +12,7 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res);
void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res);
void handleReport(HTTPRequest *req, HTTPResponse *res);
void handleNodes(HTTPRequest *req, HTTPResponse *res);
void handleUpdateFs(HTTPRequest *req, HTTPResponse *res);
void handleDeleteFsContent(HTTPRequest *req, HTTPResponse *res);
void handleFs(HTTPRequest *req, HTTPResponse *res);
void handleAdmin(HTTPRequest *req, HTTPResponse *res);
void handleAdminSettings(HTTPRequest *req, HTTPResponse *res);
void handleAdminSettingsApply(HTTPRequest *req, HTTPResponse *res);
// Interface to the PhoneAPI to access the protobufs with messages
class HttpAPI : public PhoneAPI
-11
View File
@@ -1,14 +1,3 @@
#include "mesh/http/ContentHelper.h"
// #include <Arduino.h>
// #include "main.h"
void replaceAll(std::string &str, const std::string &from, const std::string &to)
{
if (from.empty())
return;
size_t start_pos = 0;
while ((start_pos = str.find(from, start_pos)) != std::string::npos) {
str.replace(start_pos, from.length(), to);
start_pos += to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx'
}
}
-2
View File
@@ -3,5 +3,3 @@
#include <string>
#define BoolToString(x) ((x) ? "true" : "false")
void replaceAll(std::string &str, const std::string &from, const std::string &to);
+27
View File
@@ -815,6 +815,23 @@ static void reconcileAccelerometerThread(bool wasOn, bool nowOn, bool otherFeatu
}
#endif
// A "regenerate keys" client sends a blank SecurityConfig holding only the new private key, rather than the
// config it read from us. Detect that shape - new private key, every other field at its proto default - so it
// isn't mistaken for "and clear everything else".
static bool isBareKeypairRotation(const meshtastic_Config_SecurityConfig &incoming,
const meshtastic_Config_SecurityConfig &current)
{
if (incoming.private_key.size != 32)
return false;
if (current.private_key.size == 32 && memcmp(incoming.private_key.bytes, current.private_key.bytes, 32) == 0)
return false;
return incoming.admin_key_count == 0 && !incoming.is_managed && !incoming.serial_enabled && !incoming.debug_log_api_enabled &&
!incoming.admin_channel_enabled &&
incoming.packet_signature_policy ==
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_COMPATIBLE;
}
void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
{
auto changes = SEGMENT_CONFIG;
@@ -1104,6 +1121,16 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
incoming.private_key = config.security.private_key;
incoming.public_key = config.security.public_key;
}
// Rotating the keypair must not drop the admin keys - that locks the owner out of remote admin with no
// recourse but a physical connection. Clearing admin keys still works via a SET that leaves the private
// key alone and sends an empty list.
if (isBareKeypairRotation(incoming, config.security)) {
LOG_INFO("Security set is a bare keypair rotation; preserving remaining security config");
meshtastic_Config_SecurityConfig rotated = config.security;
rotated.public_key = incoming.public_key; // usually empty; derived from the private key below
rotated.private_key = incoming.private_key;
incoming = rotated;
}
#if MESHTASTIC_EXCLUDE_PKI || MESHTASTIC_EXCLUDE_XEDDSA
if (incoming.packet_signature_policy !=
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_BALANCED) {
-24
View File
@@ -82,7 +82,6 @@ CannedMessageModule::CannedMessageModule()
void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel)
{
// Do NOT override explicit broadcast replies
// Only reuse lastDest in LaunchRepeatDestination()
if (newDest == 0) {
dest = NODENUM_BROADCAST;
@@ -114,19 +113,9 @@ void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChan
LOG_DEBUG("[CannedMessage] LaunchWithDestination dest=0x%08x ch=%d", dest, channel);
}
void CannedMessageModule::LaunchRepeatDestination()
{
if (!lastDestSet) {
LaunchWithDestination(NODENUM_BROADCAST, 0);
} else {
LaunchWithDestination(lastDest, lastChannel);
}
}
void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel)
{
// Do NOT override explicit broadcast replies
// Only reuse lastDest in LaunchRepeatDestination()
if (newDest == 0) {
dest = NODENUM_BROADCAST;
@@ -308,12 +297,6 @@ void CannedMessageModule::updateDestinationSelectionList()
}
}
// Returns true if character input is currently allowed (used for search/freetext states)
bool CannedMessageModule::isCharInputAllowed() const
{
return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION;
}
static int getRowHeightForEmoteText(const char *text, int minimumHeight, int emoteSpacing = 2)
{
// Grow the row only when an emote is taller than the font.
@@ -1437,13 +1420,6 @@ bool CannedMessageModule::shouldDraw()
this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER);
}
// Has the user defined any canned messages?
// Expose publicly whether canned message module is ready for use
bool CannedMessageModule::hasMessages()
{
return (this->messagesCount > 0);
}
int CannedMessageModule::getNextIndex()
{
if (this->currentMessageIndex >= (this->messagesCount - 1)) {
-3
View File
@@ -55,7 +55,6 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
CannedMessageModule();
void LaunchWithDestination(NodeNum, uint8_t newChannel = 0);
void LaunchRepeatDestination();
void LaunchFreetextWithDestination(NodeNum, uint8_t newChannel = 0);
// === Emote Picker navigation ===
@@ -70,11 +69,9 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
// === State/UI ===
bool shouldDraw();
bool hasMessages();
void resetSearch();
void updateDestinationSelectionList();
void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
bool isCharInputAllowed() const;
String drawWithCursor(String text, int cursor);
// === Emote Picker ===
+12 -3
View File
@@ -46,6 +46,16 @@ const static DetectionSensorTriggerHandler handlers[_meshtastic_ModuleConfig_Det
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_HIGH] = detection_trigger_either_edge,
};
// The configured trigger type arrives as an unvalidated protobuf enum, so a value outside the
// generated range would index past the handler table. Fall back to the schema default instead.
static meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType configuredTriggerType()
{
const uint32_t configured = (uint32_t)moduleConfig.detection_sensor.detection_trigger_type;
if (configured > (uint32_t)_meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MAX)
return _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN;
return (meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType)configured;
}
int32_t DetectionSensorModule::runOnce()
{
/*
@@ -89,8 +99,7 @@ int32_t DetectionSensorModule::runOnce()
if (!Throttle::isWithinTimespanMs(lastSentToMesh,
Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs))) {
bool isDetected = hasDetectionEvent();
DetectionSensorTriggerVerdict verdict =
handlers[moduleConfig.detection_sensor.detection_trigger_type](wasDetected, isDetected);
DetectionSensorTriggerVerdict verdict = handlers[configuredTriggerType()](wasDetected, isDetected);
wasDetected = isDetected;
switch (verdict) {
case DetectionSensorVerdictDetected:
@@ -160,5 +169,5 @@ bool DetectionSensorModule::hasDetectionEvent()
{
bool currentState = digitalRead(moduleConfig.detection_sensor.monitor_pin);
// LOG_DEBUG("Detection Sensor Module: Current state: %i", currentState);
return (moduleConfig.detection_sensor.detection_trigger_type & 1) ? currentState : !currentState;
return (configuredTriggerType() & 1) ? currentState : !currentState;
}
+12 -1
View File
@@ -49,10 +49,21 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
return true;
}
NodeNum sourceNum = getFrom(&mp);
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(sourceNum);
// Broadcasts only: senders never sign unicast NodeInfo, so dropping it would break exchanges
// with signer nodes. Backstops ingress that skips Router's downgrade drop (e.g. decoded MQTT).
if (node && nodeInfoLiteHasXeddsaSigned(node) && !mp.xeddsa_signed && isBroadcast(mp.to)) {
LOG_WARN("Dropping unsigned NodeInfo broadcast from node 0x%08x that previously signed", sourceNum);
return true;
}
// Coerce user.id to be derived from the node number
snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp));
bool hasChanged = nodeDB->updateUser(getFrom(&mp), p, mp.channel);
// updateUser() refuses the identity write for a known signer sending unsigned (all unicast
// NodeInfo), so the exchange above still proceeds but cannot spoof the stored name.
bool hasChanged = nodeDB->updateUser(getFrom(&mp), p, mp.channel, mp.xeddsa_signed);
bool wasBroadcast = isBroadcast(mp.to);
-121
View File
@@ -65,7 +65,6 @@ void OnScreenKeyboardModule::stop(bool callEmptyCallback)
// Keep NotificationRenderer legacy pointers in sync
NotificationRenderer::virtualKeyboard = nullptr;
NotificationRenderer::textInputCallback = nullptr;
clearPopup();
if (callEmptyCallback && cb)
cb("");
}
@@ -131,9 +130,6 @@ bool OnScreenKeyboardModule::draw(OLEDDisplay *display)
display->fillRect(0, 0, display->getWidth(), display->getHeight());
display->setColor(WHITE);
keyboard->draw(display, 0, 0);
// Draw popup overlay if needed
drawPopup(display);
return true;
}
@@ -150,123 +146,6 @@ void OnScreenKeyboardModule::onCancel()
stop(true);
}
void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs)
{
if (!title || !content)
return;
strncpy(popupTitle, title, sizeof(popupTitle) - 1);
popupTitle[sizeof(popupTitle) - 1] = '\0';
strncpy(popupMessage, content, sizeof(popupMessage) - 1);
popupMessage[sizeof(popupMessage) - 1] = '\0';
popupUntil = millis() + durationMs;
popupVisible = true;
}
void OnScreenKeyboardModule::clearPopup()
{
popupTitle[0] = '\0';
popupMessage[0] = '\0';
popupUntil = 0;
popupVisible = false;
}
void OnScreenKeyboardModule::drawPopupOverlay(OLEDDisplay *display)
{
// Only render the popup overlay (without drawing the keyboard)
drawPopup(display);
}
void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display)
{
if (!popupVisible)
return;
if (millis() > popupUntil || popupMessage[0] == '\0') {
popupVisible = false;
return;
}
// Build lines and leverage NotificationRenderer inverted box drawing for consistent style
constexpr uint16_t maxContentLines = 3;
const bool hasTitle = popupTitle[0] != '\0';
display->setFont(FONT_SMALL);
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;
std::string current;
std::string word;
const char *p = text;
while (*p && wrapped.size() < maxContentLines) {
while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
if (*p == '\n') {
if (!current.empty()) {
wrapped.push_back(current);
current.clear();
if (wrapped.size() >= maxContentLines)
break;
}
}
++p;
}
if (!*p || wrapped.size() >= maxContentLines)
break;
word.clear();
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r')
word += *p++;
if (word.empty())
continue;
std::string test = current.empty() ? word : (current + " " + word);
uint16_t w = display->getStringWidth(test.c_str(), test.length(), true);
if (w <= availableWidth)
current = test;
else {
if (!current.empty()) {
wrapped.push_back(current);
current = word;
if (wrapped.size() >= maxContentLines)
break;
} else {
current = word;
while (current.size() > 1 &&
display->getStringWidth(current.c_str(), current.length(), true) > availableWidth)
current.pop_back();
}
}
}
if (!current.empty() && wrapped.size() < maxContentLines)
wrapped.push_back(current);
return wrapped;
};
std::vector<std::string> allLines;
if (hasTitle)
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 && 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");
}
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, ptrs.data(), allLines.size(), 0, 0);
}
} // namespace graphics
#endif // HAS_SCREEN
-12
View File
@@ -25,11 +25,6 @@ class OnScreenKeyboardModule
static bool processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *keyboard);
bool draw(OLEDDisplay *display);
void showPopup(const char *title, const char *content, uint32_t durationMs);
void clearPopup();
// Draw only the popup overlay (used when legacy virtualKeyboard draws the keyboard)
void drawPopupOverlay(OLEDDisplay *display);
private:
OnScreenKeyboardModule() = default;
~OnScreenKeyboardModule();
@@ -39,15 +34,8 @@ class OnScreenKeyboardModule
void onSubmit(const std::string &text);
void onCancel();
void drawPopup(OLEDDisplay *display);
VirtualKeyboard *keyboard = nullptr;
std::function<void(const std::string &)> callback;
char popupTitle[64] = {0};
char popupMessage[256] = {0};
uint32_t popupUntil = 0;
bool popupVisible = false;
};
} // namespace graphics
+2 -1
View File
@@ -55,7 +55,8 @@ void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketI
// Allow the caller to set want_ack on this ACK packet if it's important that the ACK be delivered reliably
p->want_ack = ackWantsAck;
router->sendLocal(p); // we sometimes send directly to the local node
if (router->sendLocal(p) == ERRNO_SHOULD_RELEASE) // we sometimes send directly to the local node
packetPool.release(p);
}
uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp)
@@ -10,11 +10,6 @@ float UnitConversions::MetersPerSecondToKnots(float metersPerSecond)
return metersPerSecond * 1.94384;
}
float UnitConversions::MetersPerSecondToMilesPerHour(float metersPerSecond)
{
return metersPerSecond * 2.23694;
}
float UnitConversions::HectoPascalToInchesOfMercury(float hectoPascal)
{
return hectoPascal * 0.029529983071445;
-1
View File
@@ -7,7 +7,6 @@ class UnitConversions
public:
static float CelsiusToFahrenheit(float celsius);
static float MetersPerSecondToKnots(float metersPerSecond);
static float MetersPerSecondToMilesPerHour(float metersPerSecond);
static float HectoPascalToInchesOfMercury(float hectoPascal);
// Bound a float before Arduino String(float) renders it: its fixed char[33] + dtostrf overflow
+1 -24
View File
@@ -78,16 +78,6 @@ uint8_t sanitizePositionPrecision(uint8_t precision)
return 32;
}
/**
* Saturating increment for uint8_t counters.
* Prevents overflow by capping at UINT8_MAX (255).
*/
inline void saturatingIncrement(uint8_t &counter)
{
if (counter < UINT8_MAX)
counter++;
}
/**
* Return a short human-readable name for common port numbers.
* Falls back to "port:<N>" for unknown ports.
@@ -224,19 +214,6 @@ meshtastic_TrafficManagementStats TrafficManagementModule::getStats() const
return stats;
}
void TrafficManagementModule::resetStats()
{
concurrency::LockGuard guard(&cacheLock);
stats = meshtastic_TrafficManagementStats_init_zero;
}
void TrafficManagementModule::recordRouterHopPreserved()
{
// router_preserve_hops: not suitable right now - removed from config until
// the right heuristic for when to preserve vs. exhaust is clearer.
(void)stats.router_hops_preserved;
}
void TrafficManagementModule::incrementStat(uint32_t *field)
{
concurrency::LockGuard guard(&cacheLock);
@@ -757,7 +734,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
meshtastic_User requester = meshtastic_User_init_zero;
if (!unauthenticatedSigner &&
pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &requester)) {
nodeDB->updateUser(getFrom(&mp), requester, mp.channel);
nodeDB->updateUser(getFrom(&mp), requester, mp.channel, mp.xeddsa_signed);
}
logAction("respond", &mp, "nodeinfo-cache");
incrementStat(&stats.nodeinfo_cache_hits);
-2
View File
@@ -38,8 +38,6 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
TrafficManagementModule &operator=(const TrafficManagementModule &) = delete;
meshtastic_TrafficManagementStats getStats() const;
void resetStats();
void recordRouterHopPreserved();
// Next-hop overflow cache (routing hint).
// setNextHop: store a confirmed last-byte next hop for `dest`. Called by
-10
View File
@@ -444,16 +444,6 @@ bool BMI270Sensor::writeRegister(uint8_t reg, uint8_t value)
return wire->endTransmission() == 0;
}
bool BMI270Sensor::writeRegisters(uint8_t reg, const uint8_t *data, size_t len)
{
wire->beginTransmission(deviceAddress());
wire->write(reg);
for (size_t i = 0; i < len; i++) {
wire->write(data[i]);
}
return wire->endTransmission() == 0;
}
uint8_t BMI270Sensor::readRegister(uint8_t reg)
{
wire->beginTransmission(deviceAddress());
-1
View File
@@ -18,7 +18,6 @@ class BMI270Sensor : public MotionSensor
// BMI270 register access
bool writeRegister(uint8_t reg, uint8_t value);
bool writeRegisters(uint8_t reg, const uint8_t *data, size_t len);
uint8_t readRegister(uint8_t reg);
bool readRegisters(uint8_t reg, uint8_t *data, size_t len);
+2 -1
View File
@@ -122,7 +122,8 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
if (isFromUs(e.packet)) {
auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index);
pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT;
router->sendLocal(pAck);
if (router->sendLocal(pAck) == ERRNO_SHOULD_RELEASE)
packetPool.release(pAck);
} else {
LOG_INFO("Ignore downlink message we originally sent");
}
+111
View File
@@ -0,0 +1,111 @@
// Weak software __atomic_*_{1,2,4} for ESP32-S2/S3. -mdisable-hardware-atomics makes
// GCC emit these libcalls, but the precompiled libnewlib ships only the _8 variants and
// some toolchains' libgcc ships none, so S3 links fail on macOS. Weak = a real libgcc
// definition wins with no clash. Spinlock/critical-section like IDF's stdatomic.c.
#include "sdkconfig.h"
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2)
#include <stdbool.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
// These names are GCC builtins; declaring them trips -Wbuiltin-declaration-mismatch
// (uint32_t vs GCC's unsigned int, same ABI). Suppressed as IDF's stdatomic.c does.
#pragma GCC diagnostic ignored "-Wbuiltin-declaration-mismatch"
static portMUX_TYPE s_swatomic_mux = portMUX_INITIALIZER_UNLOCKED;
#define SWATOMIC_ENTER() portENTER_CRITICAL_SAFE(&s_swatomic_mux)
#define SWATOMIC_EXIT() portEXIT_CRITICAL_SAFE(&s_swatomic_mux)
// load / store / exchange / compare_exchange for one width.
#define GEN_SWATOMIC_CORE(N, TYPE) \
__attribute__((weak, used)) TYPE __atomic_load_##N(const volatile void *ptr, int memorder) \
{ \
(void)memorder; \
SWATOMIC_ENTER(); \
TYPE ret = *(const volatile TYPE *)ptr; \
SWATOMIC_EXIT(); \
return ret; \
} \
__attribute__((weak, used)) void __atomic_store_##N(volatile void *ptr, TYPE val, int memorder) \
{ \
(void)memorder; \
SWATOMIC_ENTER(); \
*(volatile TYPE *)ptr = val; \
SWATOMIC_EXIT(); \
} \
__attribute__((weak, used)) TYPE __atomic_exchange_##N(volatile void *ptr, TYPE val, int memorder) \
{ \
(void)memorder; \
SWATOMIC_ENTER(); \
TYPE old = *(volatile TYPE *)ptr; \
*(volatile TYPE *)ptr = val; \
SWATOMIC_EXIT(); \
return old; \
} \
__attribute__((weak, used)) bool __atomic_compare_exchange_##N(volatile void *ptr, void *expected, TYPE desired, \
bool is_weak, int success, int failure) \
{ \
(void)is_weak; \
(void)success; \
(void)failure; \
bool ok; \
SWATOMIC_ENTER(); \
TYPE cur = *(volatile TYPE *)ptr; \
if (cur == *(TYPE *)expected) { \
*(volatile TYPE *)ptr = desired; \
ok = true; \
} else { \
*(TYPE *)expected = cur; \
ok = false; \
} \
SWATOMIC_EXIT(); \
return ok; \
}
// A read-modify-write pair: fetch_<name> returns the old value, <name>_fetch the
// new. EXPR is evaluated over `old` and `val`.
#define GEN_SWATOMIC_RMW(N, TYPE, NAME, EXPR) \
__attribute__((weak, used)) TYPE __atomic_fetch_##NAME##_##N(volatile void *ptr, TYPE val, int memorder) \
{ \
(void)memorder; \
SWATOMIC_ENTER(); \
TYPE old = *(volatile TYPE *)ptr; \
*(volatile TYPE *)ptr = (TYPE)(EXPR); \
SWATOMIC_EXIT(); \
return old; \
} \
__attribute__((weak, used)) TYPE __atomic_##NAME##_fetch_##N(volatile void *ptr, TYPE val, int memorder) \
{ \
(void)memorder; \
SWATOMIC_ENTER(); \
TYPE old = *(volatile TYPE *)ptr; \
TYPE nv = (TYPE)(EXPR); \
*(volatile TYPE *)ptr = nv; \
SWATOMIC_EXIT(); \
return nv; \
}
#define GEN_SWATOMIC_ALL(N, TYPE) \
GEN_SWATOMIC_CORE(N, TYPE) \
GEN_SWATOMIC_RMW(N, TYPE, add, old + val) \
GEN_SWATOMIC_RMW(N, TYPE, sub, old - val) \
GEN_SWATOMIC_RMW(N, TYPE, and, old &val) \
GEN_SWATOMIC_RMW(N, TYPE, or, old | val) \
GEN_SWATOMIC_RMW(N, TYPE, xor, old ^ val) \
GEN_SWATOMIC_RMW(N, TYPE, nand, ~(old & val))
GEN_SWATOMIC_ALL(1, uint8_t)
GEN_SWATOMIC_ALL(2, uint16_t)
GEN_SWATOMIC_ALL(4, uint32_t)
#else
// Keep this a non-empty translation unit on targets that inline hardware atomics.
typedef int swatomic_translation_unit_not_empty;
#endif // CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32S2
+43
View File
@@ -120,6 +120,44 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state)
return 0;
}
// A kernel SPI transfer is capped by the spidev module's `bufsiz` parameter (4096 by default).
// LovyanGFX pushes the framebuffer in large chunks, so a display bigger than that budget fails
// deep inside the driver with a bare -EMSGSIZE. Check up front so the user gets told what to fix.
static void checkSpidevBufsiz()
{
if (portduino_config.display_spi_dev == "" || portduino_config.displayWidth == 0 || portduino_config.displayHeight == 0) {
return;
}
switch (portduino_config.displayPanel) {
case no_screen:
case x11:
case fb:
case hub75:
return; // not driven over spidev
default:
break;
}
const long required = (long)portduino_config.displayWidth * portduino_config.displayHeight / 2 * 3;
std::ifstream bufsizFile("/sys/module/spidev/parameters/bufsiz");
long bufsiz = 0;
if (!bufsizFile.is_open() || !(bufsizFile >> bufsiz)) {
// spidev may be built into the kernel without exposing the parameter; nothing to check.
return;
}
if (bufsiz < required) {
std::cerr << "SPI display " << portduino_config.displayWidth << "x" << portduino_config.displayHeight
<< " needs a spidev buffer of at least " << required << " bytes, but "
<< "/sys/module/spidev/parameters/bufsiz is " << bufsiz << "." << std::endl;
std::cerr << "Add 'spidev.bufsiz=" << required << "' to your kernel command line "
<< "(/boot/firmware/cmdline.txt on Raspberry Pi OS) and reboot." << std::endl;
std::cerr << "Or echo that value into /etc/modprobe.d/spidev.conf and reload the spidev module" << std::endl;
exit(EXIT_FAILURE);
}
}
void portduinoCustomInit()
{
static struct argp_option options[] = {{"port", 'p', "PORT", 0, "The TCP port to use."},
@@ -559,6 +597,11 @@ void portduinoSetup()
}
}
// if we have s SPI display, check /sys/module/spidev/parameters/bufsiz
// It needs to be at least width * height / 2 * 3
// fail with a more useful error message.
checkSpidevBufsiz();
// if we're using a usermode driver, we need to initialize it here, to get a serial number back for mac address
uint8_t dmac[6] = {0};
if (portduino_config.lora_spi_dev == "ch341") {
-10
View File
@@ -1078,16 +1078,6 @@ bool isSessionExpired()
return (millis() - s_sessionStartedMs) > s_sessionMaxMs;
}
uint32_t getSessionRemainingSeconds()
{
if (s_sessionMaxMs == 0)
return 0;
uint32_t elapsedMs = millis() - s_sessionStartedMs;
if (elapsedMs >= s_sessionMaxMs)
return 0;
return (s_sessionMaxMs - elapsedMs) / 1000UL;
}
uint8_t consumeSessionBoot()
{
if (s_bootsRemaining == 0) {
-4
View File
@@ -243,10 +243,6 @@ void setSession(uint32_t maxSeconds);
/// from the main loop on a low-frequency tick.
bool isSessionExpired();
/// Seconds remaining in the current session. 0 if no timer is set, or if
/// the timer has expired (use isSessionExpired() to distinguish).
uint32_t getSessionRemainingSeconds();
/// Consume one boot from the on-flash token (the rollback ledger) and
/// re-arm the session timer in place - no reboot. Called from the main
/// loop when a session expires AND there is still budget. Decrements
+4 -1
View File
@@ -62,10 +62,13 @@ bool XModemAdapter::isValidFilename(const char *name)
{
if (!name || name[0] == '\0')
return false;
const bool driveLetter = (name[0] >= 'A' && name[0] <= 'Z') || (name[0] >= 'a' && name[0] <= 'z');
if (driveLetter && name[1] == ':')
return false;
// Reject any ".." path component. Absolute paths and subdirectories are fine; they stay within
// the filesystem root, so only traversal out of it needs blocking.
for (const char *seg = name; *seg;) {
const char *slash = strchr(seg, '/');
const char *slash = strpbrk(seg, "/\\");
const size_t len = slash ? (size_t)(slash - seg) : strlen(seg);
if (len == 2 && seg[0] == '.' && seg[1] == '.')
return false;