diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index 7b0595aa9..b7dbd670c 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -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. * diff --git a/src/FSCommon.h b/src/FSCommon.h index 080ede691..03ad091be 100644 --- a/src/FSCommon.h +++ b/src/FSCommon.h @@ -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 getFiles(const char *dirname, uint8_t levels, size_t maxCount = 64, bool *wasLimited = nullptr); diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp index 6284ea007..8b6775eec 100644 --- a/src/MessageStore.cpp +++ b/src/MessageStore.cpp @@ -7,7 +7,6 @@ #include "SafeFile.h" #include "gps/RTC.h" #include "memory/MemAudit.h" -#include #include // 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 diff --git a/src/MessageStore.h b/src/MessageStore.h index 724e10bc6..366c1a37d 100644 --- a/src/MessageStore.h +++ b/src/MessageStore.h @@ -93,10 +93,8 @@ class MessageStore void addLiveMessage(StoredMessage &&msg); void addLiveMessage(const StoredMessage &msg); // convenience overload const std::deque &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 diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp index b32242212..93219d2fc 100644 --- a/src/SerialConsole.cpp +++ b/src/SerialConsole.cpp @@ -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") { diff --git a/src/SerialConsole.h b/src/SerialConsole.h index 29614e0bc..eeed25644 100644 --- a/src/SerialConsole.h +++ b/src/SerialConsole.h @@ -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; \ No newline at end of file diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 6692d996d..42b9900bf 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -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[] = { diff --git a/src/buzz/buzz.h b/src/buzz/buzz.h index 1b97e24de..2b6ac5022 100644 --- a/src/buzz/buzz.h +++ b/src/buzz/buzz.h @@ -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 \ No newline at end of file diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 0a942f10d..389fa6006 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -2201,11 +2201,6 @@ bool GPS::hasLock() return false; } -bool GPS::hasFlow() -{ - return reader.passedChecksum() > 0; -} - bool GPS::whileActive() { unsigned int charsInBuf = 0; diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 678cc2845..328a9a316 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -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; } diff --git a/src/gps/GeoCoord.cpp b/src/gps/GeoCoord.cpp index 8324bc3c0..4afae9394 100644 --- a/src/gps/GeoCoord.cpp +++ b/src/gps/GeoCoord.cpp @@ -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 diff --git a/src/gps/GeoCoord.h b/src/gps/GeoCoord.h index 3bb6606f2..5afa78430 100644 --- a/src/gps/GeoCoord.h +++ b/src/gps/GeoCoord.h @@ -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 pointAtDistance(double bearing, double range); // Lat lon alt getters diff --git a/src/graphics/EInkParallelDisplay.cpp b/src/graphics/EInkParallelDisplay.cpp index 293f57e81..552b11747 100644 --- a/src/graphics/EInkParallelDisplay.cpp +++ b/src/graphics/EInkParallelDisplay.cpp @@ -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()) { diff --git a/src/graphics/EInkParallelDisplay.h b/src/graphics/EInkParallelDisplay.h index 81189e400..60ef7f20c 100644 --- a/src/graphics/EInkParallelDisplay.h +++ b/src/graphics/EInkParallelDisplay.h @@ -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 asyncFullRunning{false}; diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 95883038b..7892cbdda 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -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); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 4aeec6ce8..ce9349408 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -310,8 +310,6 @@ class Screen : public concurrency::OSThread */ void doDeepSleep(); - void blink(); - // Draw north float estimatedHeading(double lat, double lon); diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp index 43b33e853..fd06e0def 100644 --- a/src/graphics/VirtualKeyboard.cpp +++ b/src/graphics/VirtualKeyboard.cpp @@ -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; diff --git a/src/graphics/VirtualKeyboard.h b/src/graphics/VirtualKeyboard.h index 169163b57..250ae61f8 100644 --- a/src/graphics/VirtualKeyboard.h +++ b/src/graphics/VirtualKeyboard.h @@ -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 callback); diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index b4dd5ab8a..5c755b927 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -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); diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h index 65fa74ca6..9d28a5e9c 100644 --- a/src/graphics/draw/DebugRenderer.h +++ b/src/graphics/draw/DebugRenderer.h @@ -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 diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index dfe6671c8..3a307398e 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -17,12 +17,6 @@ #include "meshUtils.h" #include -// 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 diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index 4aa217141..69c4bc067 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -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(); diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 98f229b83..74e26358f 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -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 diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index 08f6f74b0..360bfac3c 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -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); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 53f016645..1f5cccd65 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -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) diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 0aeace42e..1afddea8f 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -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 diff --git a/src/graphics/niche/InkHUD/Applet.cpp b/src/graphics/niche/InkHUD/Applet.cpp index d2fdc41f9..50efff8ff 100644 --- a/src/graphics/niche/InkHUD/Applet.cpp +++ b/src/graphics/niche/InkHUD/Applet.cpp @@ -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) diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index 2aa5bc640..84891b1fe 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -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 diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp index ef5902a24..a0e9e201a 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp @@ -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() diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h index 8ec7d48a5..df1a345d2 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h @@ -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; diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp index 2cdb9f507..fe5a32855 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -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); diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h index 0c1682637..538896792 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -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); diff --git a/src/graphics/niche/InkHUD/docs/README.md b/src/graphics/niche/InkHUD/docs/README.md index bea373b7a..7ff81c007 100644 --- a/src/graphics/niche/InkHUD/docs/README.md +++ b/src/graphics/niche/InkHUD/docs/README.md @@ -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 diff --git a/src/graphics/niche/Inputs/TwoButton.cpp b/src/graphics/niche/Inputs/TwoButton.cpp index 1a27e039b..4afe8d5b9 100644 --- a/src/graphics/niche/Inputs/TwoButton.cpp +++ b/src/graphics/niche/Inputs/TwoButton.cpp @@ -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) { diff --git a/src/graphics/niche/Inputs/TwoButton.h b/src/graphics/niche/Inputs/TwoButton.h index ae66adf96..fc2805ae5 100644 --- a/src/graphics/niche/Inputs/TwoButton.h +++ b/src/graphics/niche/Inputs/TwoButton.h @@ -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); diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.cpp b/src/graphics/niche/Inputs/TwoButtonExtended.cpp index f979faca9..bb5904cb6 100644 --- a/src/graphics/niche/Inputs/TwoButtonExtended.cpp +++ b/src/graphics/niche/Inputs/TwoButtonExtended.cpp @@ -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) diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.h b/src/graphics/niche/Inputs/TwoButtonExtended.h index eb536907d..f6b52b923 100644 --- a/src/graphics/niche/Inputs/TwoButtonExtended.h +++ b/src/graphics/niche/Inputs/TwoButtonExtended.h @@ -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); diff --git a/src/main.cpp b/src/main.cpp index 99bedd6f0..9e7361ae4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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; diff --git a/src/memGet.cpp b/src/memGet.cpp index cbba0f4e4..2da93bf12 100644 --- a/src/memGet.cpp +++ b/src/memGet.cpp @@ -9,7 +9,6 @@ */ #include "memGet.h" #include "configuration.h" -#include "memory/MemAudit.h" #if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP) #include @@ -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 -} \ No newline at end of file diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 3e7361f19..f1ed831f8 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -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 diff --git a/src/mesh/NextHopRouter.h b/src/mesh/NextHopRouter.h index d5e2a9621..3a19191fe 100644 --- a/src/mesh/NextHopRouter.h +++ b/src/mesh/NextHopRouter.h @@ -206,7 +206,11 @@ class NextHopRouter : public FloodingRouter */ std::optional 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; diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index c3fc2c0d3..b6bdf81f6 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -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); diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index fce250dbb..ac6780d77 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -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 diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 86f03379b..118d434d4 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -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 diff --git a/src/mesh/Router.h b/src/mesh/Router.h index 74824be0a..680d3ae8d 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -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; diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 598419c25..95712403e 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -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(""); - res->println(""); -} - -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("

Meshtastic

"); - res->println("Delete Content in /static/*"); - - LOG_INFO("Delete files from /static/* : "); - - concurrency::LockGuard g(spiLock); - htmlDeleteDir("/static"); - - res->println("


Back to admin"); -} - void handleAdmin(HTTPRequest *req, HTTPResponse *res) { res->setHeader("Content-Type", "text/html"); @@ -909,52 +818,6 @@ void handleAdmin(HTTPRequest *req, HTTPResponse *res) res->println("Device Report
"); } -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("

Meshtastic

"); - res->println("This isn't done."); - res->println("
"); - res->println(""); - res->println(""); - res->println(""); - res->println(""); - res->println( - ""); - res->println("
Set?Settingcurrent valuenew value
WiFi SSIDfalse
WiFi Passwordfalse
Smart Position Updatefalse
"); - res->println(""); - res->println(""); - res->println(""); - res->println("


Back to admin"); -} - -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("

Meshtastic

"); - res->println( - "Settings Applied. "); - - 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("

Meshtastic

"); - res->println("Delete Web Content

Be patient!"); - res->println("


Back to admin"); -} - void handleRestart(HTTPRequest *req, HTTPResponse *res) { res->setHeader("Content-Type", "text/html"); diff --git a/src/mesh/http/ContentHandler.h b/src/mesh/http/ContentHandler.h index ed182ad76..700ef78c8 100644 --- a/src/mesh/http/ContentHandler.h +++ b/src/mesh/http/ContentHandler.h @@ -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 diff --git a/src/mesh/http/ContentHelper.cpp b/src/mesh/http/ContentHelper.cpp index 8f283932b..b35a3fc92 100644 --- a/src/mesh/http/ContentHelper.cpp +++ b/src/mesh/http/ContentHelper.cpp @@ -1,14 +1,3 @@ #include "mesh/http/ContentHelper.h" // #include // #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' - } -} diff --git a/src/mesh/http/ContentHelper.h b/src/mesh/http/ContentHelper.h index e5d3a2f57..d4f744aba 100644 --- a/src/mesh/http/ContentHelper.h +++ b/src/mesh/http/ContentHelper.h @@ -3,5 +3,3 @@ #include #define BoolToString(x) ((x) ? "true" : "false") - -void replaceAll(std::string &str, const std::string &from, const std::string &to); diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 6a66a4533..9d723a32e 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -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 ¤t) +{ + 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) { diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 771066054..a10add057 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -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)) { diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index f6cb4d011..67334dd5e 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -55,7 +55,6 @@ class CannedMessageModule : public SinglePortModule, public Observable (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; } \ No newline at end of file diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index 6b8d7c5f0..01168114d 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -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); diff --git a/src/modules/OnScreenKeyboardModule.cpp b/src/modules/OnScreenKeyboardModule.cpp index e75d926bf..ae2707cfe 100644 --- a/src/modules/OnScreenKeyboardModule.cpp +++ b/src/modules/OnScreenKeyboardModule.cpp @@ -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::vector 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 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 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 diff --git a/src/modules/OnScreenKeyboardModule.h b/src/modules/OnScreenKeyboardModule.h index f86b71ec3..40dc23fae 100644 --- a/src/modules/OnScreenKeyboardModule.h +++ b/src/modules/OnScreenKeyboardModule.h @@ -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 callback; - - char popupTitle[64] = {0}; - char popupMessage[256] = {0}; - uint32_t popupUntil = 0; - bool popupVisible = false; }; } // namespace graphics diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index a46323ac6..aba0751f3 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -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) diff --git a/src/modules/Telemetry/UnitConversions.cpp b/src/modules/Telemetry/UnitConversions.cpp index fff1ee3d2..476ec7252 100644 --- a/src/modules/Telemetry/UnitConversions.cpp +++ b/src/modules/Telemetry/UnitConversions.cpp @@ -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; diff --git a/src/modules/Telemetry/UnitConversions.h b/src/modules/Telemetry/UnitConversions.h index 00d0bd51b..9819d284f 100644 --- a/src/modules/Telemetry/UnitConversions.h +++ b/src/modules/Telemetry/UnitConversions.h @@ -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 diff --git a/src/modules/TrafficManagementModule.cpp b/src/modules/TrafficManagementModule.cpp index 74e8de6c2..ea5054ded 100644 --- a/src/modules/TrafficManagementModule.cpp +++ b/src/modules/TrafficManagementModule.cpp @@ -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:" 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); diff --git a/src/modules/TrafficManagementModule.h b/src/modules/TrafficManagementModule.h index 864b542bf..f4a1d5954 100644 --- a/src/modules/TrafficManagementModule.h +++ b/src/modules/TrafficManagementModule.h @@ -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 diff --git a/src/motion/BMI270Sensor.cpp b/src/motion/BMI270Sensor.cpp index bc547529d..9f3911956 100644 --- a/src/motion/BMI270Sensor.cpp +++ b/src/motion/BMI270Sensor.cpp @@ -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()); diff --git a/src/motion/BMI270Sensor.h b/src/motion/BMI270Sensor.h index 7d6cdeaa9..3addc4541 100644 --- a/src/motion/BMI270Sensor.h +++ b/src/motion/BMI270Sensor.h @@ -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); diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 530ccf6af..e0b3a7bff 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -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"); } diff --git a/src/platform/esp32/xtensa_swatomic.c b/src/platform/esp32/xtensa_swatomic.c new file mode 100644 index 000000000..8e905a275 --- /dev/null +++ b/src/platform/esp32/xtensa_swatomic.c @@ -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 +#include + +#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_ returns the old value, _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 diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index c8e27eb8d..750396769 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -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") { diff --git a/src/security/EncryptedStorage.cpp b/src/security/EncryptedStorage.cpp index 206bb4ac2..d34906eaa 100644 --- a/src/security/EncryptedStorage.cpp +++ b/src/security/EncryptedStorage.cpp @@ -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) { diff --git a/src/security/EncryptedStorage.h b/src/security/EncryptedStorage.h index 06cba0061..3568867d0 100644 --- a/src/security/EncryptedStorage.h +++ b/src/security/EncryptedStorage.h @@ -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 diff --git a/src/xmodem.cpp b/src/xmodem.cpp index ce7ad8020..58339a369 100644 --- a/src/xmodem.cpp +++ b/src/xmodem.cpp @@ -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; diff --git a/test/test_admin_radio/test_main.cpp b/test/test_admin_radio/test_main.cpp index 3ce99fcb6..673d4590f 100644 --- a/test/test_admin_radio/test_main.cpp +++ b/test/test_admin_radio/test_main.cpp @@ -1197,6 +1197,80 @@ static void test_handleSetConfig_security_acceptsSuppliedKeypair() TEST_ASSERT_EQUAL_MEMORY(expectedPub, config.security.public_key.bytes, 32); } +// Issue #11073: "regenerate keys" sends a blank SecurityConfig holding only the new private key. Replacing +// the whole struct with it wiped the admin keys, locking the owner out of remote admin. +static void test_handleSetConfig_security_rotationPreservesAdminKeys() +{ + config.security = meshtastic_Config_SecurityConfig_init_zero; + config.security.private_key.size = 32; + memset(config.security.private_key.bytes, 0x11, 32); + config.security.public_key.size = 32; + memset(config.security.public_key.bytes, 0x22, 32); + config.security.admin_key_count = 2; + config.security.admin_key[0].size = 32; + memset(config.security.admin_key[0].bytes, 0xAA, 32); + config.security.admin_key[1].size = 32; + memset(config.security.admin_key[1].bytes, 0xBB, 32); + config.security.is_managed = true; + config.security.serial_enabled = true; + config.security.packet_signature_policy = + meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT; + + // Exactly what the regenerate dialog emits. + meshtastic_Config c = meshtastic_Config_init_zero; + c.which_payload_variant = meshtastic_Config_security_tag; + c.payload_variant.security.private_key.size = 32; + memset(c.payload_variant.security.private_key.bytes, 0x33, 32); + + testAdmin->deferSaves(); + testAdmin->handleSetConfig(c, false); + + uint8_t expectedPriv[32]; + memset(expectedPriv, 0x33, 32); + TEST_ASSERT_EQUAL_UINT(32, config.security.private_key.size); + TEST_ASSERT_EQUAL_MEMORY(expectedPriv, config.security.private_key.bytes, 32); + + uint8_t expectedAdmin0[32], expectedAdmin1[32]; + memset(expectedAdmin0, 0xAA, 32); + memset(expectedAdmin1, 0xBB, 32); + TEST_ASSERT_EQUAL_UINT(2, config.security.admin_key_count); + TEST_ASSERT_EQUAL_UINT(32, config.security.admin_key[0].size); + TEST_ASSERT_EQUAL_MEMORY(expectedAdmin0, config.security.admin_key[0].bytes, 32); + TEST_ASSERT_EQUAL_UINT(32, config.security.admin_key[1].size); + TEST_ASSERT_EQUAL_MEMORY(expectedAdmin1, config.security.admin_key[1].bytes, 32); + TEST_ASSERT_TRUE(config.security.is_managed); + TEST_ASSERT_TRUE(config.security.serial_enabled); + TEST_ASSERT_EQUAL(meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT, + config.security.packet_signature_policy); +} + +// The escape hatch: a SET that leaves the private key alone still clears admin keys. +static void test_handleSetConfig_security_clearsAdminKeysWhenKeypairUnchanged() +{ + config.security = meshtastic_Config_SecurityConfig_init_zero; + config.security.private_key.size = 32; + memset(config.security.private_key.bytes, 0x11, 32); + config.security.public_key.size = 32; + memset(config.security.public_key.bytes, 0x22, 32); + config.security.admin_key_count = 1; + config.security.admin_key[0].size = 32; + memset(config.security.admin_key[0].bytes, 0xAA, 32); + + // Same private key we already hold, empty admin key list. + meshtastic_Config c = meshtastic_Config_init_zero; + c.which_payload_variant = meshtastic_Config_security_tag; + c.payload_variant.security.private_key.size = 32; + memset(c.payload_variant.security.private_key.bytes, 0x11, 32); + c.payload_variant.security.public_key.size = 32; + memset(c.payload_variant.security.public_key.bytes, 0x22, 32); + + testAdmin->deferSaves(); + testAdmin->handleSetConfig(c, false); + + TEST_ASSERT_EQUAL_UINT(0, config.security.admin_key_count); + TEST_ASSERT_EQUAL_UINT(0, config.security.admin_key[0].size); +} + static void test_regionInfo_supportsPreset() { const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); @@ -1540,6 +1614,8 @@ void setup() RUN_TEST(test_handleSetConfig_fromLocal_customBandwidthNonZeroPreserved); RUN_TEST(test_handleSetConfig_security_preservesKeypairWhenPrivateOmitted); RUN_TEST(test_handleSetConfig_security_acceptsSuppliedKeypair); + RUN_TEST(test_handleSetConfig_security_rotationPreservesAdminKeys); + RUN_TEST(test_handleSetConfig_security_clearsAdminKeysWhenKeypairUnchanged); RUN_TEST(test_regionInfo_supportsPreset); RUN_TEST(test_checkConfigRegion_quietCheckReportsReason); RUN_TEST(test_handleSetConfig_fromOthers_siblingLockedPresetSwapsRegion); diff --git a/test/test_nexthop_routing/test_main.cpp b/test/test_nexthop_routing/test_main.cpp index b3964eb25..c7a27bf5c 100644 --- a/test/test_nexthop_routing/test_main.cpp +++ b/test/test_nexthop_routing/test_main.cpp @@ -15,6 +15,7 @@ #include "gps/RTC.h" #include "mesh/NextHopRouter.h" #include "mesh/NodeDB.h" +#include "mesh/RadioInterface.h" #include #include #include @@ -87,6 +88,7 @@ class NextHopRouterTestShim : public NextHopRouter using NextHopRouter::noteRouteFailure; using NextHopRouter::noteRouteLearned; using NextHopRouter::noteRouteSuccess; + using NextHopRouter::perhapsRebroadcast; using Router::shouldDecrementHopLimit; // protected in Router void resetRouteHealthForTest() @@ -96,6 +98,34 @@ class NextHopRouterTestShim : public NextHopRouter } }; +// --------------------------------------------------------------------------- +// MockRadioInterface - mirrors RadioLibInterface::send()'s NODENUM_BROADCAST_NO_LORA branch, which +// returns ERRNO_SHOULD_RELEASE without releasing. +// --------------------------------------------------------------------------- +class MockRadioInterface : public RadioInterface +{ + public: + ErrorCode send(meshtastic_MeshPacket *p) override + { + sendCount++; + if (declineAll || p->to == NODENUM_BROADCAST_NO_LORA) + return ERRNO_SHOULD_RELEASE; + + packetPool.release(p); + return ERRNO_OK; + } + + uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) override + { + (void)totalPacketLen; + (void)received; + return 0; + } + + int sendCount = 0; + bool declineAll = false; +}; + static MockNodeDB *mockNodeDB = nullptr; static NextHopRouterTestShim *shim = nullptr; @@ -409,6 +439,65 @@ void test_hoplimit_decrement_when_resolved_not_favorite(void) TEST_ASSERT_TRUE(shim->shouldDecrementHopLimit(&p)); // unique but not a favorite -> decrement } +// =========================================================================== +// Rebroadcast of NODENUM_BROADCAST_NO_LORA +// =========================================================================== + +static MockRadioInterface *installMockIface() +{ + MockRadioInterface *m = new MockRadioInterface(); + shim->addInterface(std::unique_ptr(m)); + return m; +} + +// Eligible for rebroadcast: not from/to us, hops left, nonzero id, no next-hop preference. +// Encrypted variant so Router::send() skips the encode path. +static meshtastic_MeshPacket makeRebroadcastCandidate(NodeNum to) +{ + meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero; + p.from = 0x22222222; // not us + p.to = to; + p.id = 0x0BADF00D; + p.hop_start = 3; + p.hop_limit = 3; + p.next_hop = NO_NEXT_HOP_PREFERENCE; + p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag; + p.encrypted.size = 8; + return p; +} + +// Control: proves the NO_LORA case below turns on the `to` field alone. +void test_rebroadcast_normal_broadcast_is_relayed(void) +{ + MockRadioInterface *mockIface = installMockIface(); + meshtastic_MeshPacket p = makeRebroadcastCandidate(NODENUM_BROADCAST); + + TEST_ASSERT_TRUE_MESSAGE(shim->perhapsRebroadcast(&p), "ordinary broadcast must be rebroadcast"); + TEST_ASSERT_EQUAL_MESSAGE(1, mockIface->sendCount, "exactly one packet should reach the radio"); +} + +void test_rebroadcast_no_lora_broadcast_is_not_relayed(void) +{ + MockRadioInterface *mockIface = installMockIface(); + meshtastic_MeshPacket p = makeRebroadcastCandidate(NODENUM_BROADCAST_NO_LORA); + + TEST_ASSERT_FALSE_MESSAGE(shim->perhapsRebroadcast(&p), "no-LoRa broadcast must not be rebroadcast"); + TEST_ASSERT_EQUAL_MESSAGE(0, mockIface->sendCount, "no packet should be handed to the radio at all"); +} + +// Declining mock bypasses the guard so send() is reached; the release itself is only observable as +// a sanitizer leak report, not an assertion. +void test_rebroadcast_declined_send_releases_packet(void) +{ + MockRadioInterface *mockIface = installMockIface(); + mockIface->declineAll = true; + + meshtastic_MeshPacket p = makeRebroadcastCandidate(NODENUM_BROADCAST); + + TEST_ASSERT_TRUE_MESSAGE(shim->perhapsRebroadcast(&p), "the rebroadcast must still be attempted"); + TEST_ASSERT_EQUAL_MESSAGE(1, mockIface->sendCount, "the copy must have reached the mock radio"); +} + // =========================================================================== void setup() @@ -459,6 +548,11 @@ void setup() RUN_TEST(test_hoplimit_decrement_on_colliding_favorites); RUN_TEST(test_hoplimit_decrement_when_resolved_not_favorite); + printf("\n=== rebroadcast of NODENUM_BROADCAST_NO_LORA ===\n"); + RUN_TEST(test_rebroadcast_normal_broadcast_is_relayed); + RUN_TEST(test_rebroadcast_no_lora_broadcast_is_not_relayed); + RUN_TEST(test_rebroadcast_declined_send_releases_packet); + exit(UNITY_END()); } diff --git a/test/test_packet_signing/test_main.cpp b/test/test_packet_signing/test_main.cpp index 4c3545a9c..0da6e9a20 100644 --- a/test/test_packet_signing/test_main.cpp +++ b/test/test_packet_signing/test_main.cpp @@ -27,6 +27,7 @@ #include "mesh/ReliableRouter.h" #include "mesh/Router.h" #include "mesh/SinglePortModule.h" +#include "modules/NodeInfoModule.h" #include "modules/RoutingModule.h" #include "mqtt/MQTT.h" #include @@ -87,6 +88,21 @@ class MockNodeDB : public NodeDB nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, value); } + void setLongName(NodeNum num, const char *name) + { + meshtastic_NodeInfoLite *n = getMeshNode(num); + TEST_ASSERT_NOT_NULL(n); + strncpy(n->long_name, name, sizeof(n->long_name) - 1); + n->long_name[sizeof(n->long_name) - 1] = '\0'; + } + + const char *longName(NodeNum num) + { + meshtastic_NodeInfoLite *n = getMeshNode(num); + TEST_ASSERT_NOT_NULL(n); + return n->long_name; + } + std::vector testNodes; }; @@ -723,6 +739,33 @@ void test_A17_strict_verifies_signer_from_warm_key_store(void) "Balanced downgrade memory must survive repeated hot-store eviction"); } #endif + +void test_A18_unsigned_broadcast_from_signer_with_unknown_fields_dropped(void) +{ + mockNodeDB->addNode(REMOTE_NODE); + mockNodeDB->setSignerBit(REMOTE_NODE, true); + + meshtastic_MeshPacket p = makeBroadcastWithUnknownFields(); + + TEST_ASSERT_EQUAL_MESSAGE(DECODE_POLICY_REJECT, perhapsDecode(&p), + "unsigned broadcast from a signer must be dropped despite unknown fields"); +} + +void test_A19_unsigned_broadcast_from_nonsigner_with_unknown_fields_accepted(void) +{ + mockNodeDB->addNode(REMOTE_NODE); + + meshtastic_MeshPacket p = makeBroadcastWithUnknownFields(); + const size_t rawSize = p.encrypted.size; + + TEST_ASSERT_EQUAL_MESSAGE(DECODE_SUCCESS, perhapsDecode(&p), "frame from a non-signer must still decode"); + TEST_ASSERT_EQUAL_MESSAGE(meshtastic_PortNum_POSITION_APP, p.decoded.portnum, "unknown fields must not disturb the portnum"); + TEST_ASSERT_EQUAL_MESSAGE(SMALL_PAYLOAD, p.decoded.payload.size, "payload must survive the unknown fields"); + TEST_ASSERT_FALSE(p.xeddsa_signed); + TEST_ASSERT_LESS_THAN_MESSAGE(rawSize, encodedDataSize(&p.decoded), + "unknown fields must drop at decode, leaving decoded size < raw"); +} + // =========================================================================== // Group B - send-side signing policy (perhapsEncode) // =========================================================================== @@ -878,9 +921,75 @@ void test_B7_infrastructure_port_signing_matrix(void) } // =========================================================================== -// Group C - routing pipeline authentication ordering +// Group C - routing pipeline and NodeInfo authentication ordering // =========================================================================== +class NodeInfoTestShim : public NodeInfoModule +{ + public: + using NodeInfoModule::handleReceivedProtobuf; +}; + +static meshtastic_MeshPacket makeNodeInfoPacket(bool signed_) +{ + meshtastic_MeshPacket mp = makeDecoded(REMOTE_NODE, NODENUM_BROADCAST, meshtastic_PortNum_NODEINFO_APP, SMALL_PAYLOAD); + mp.xeddsa_signed = signed_; + return mp; +} + +void test_N1_unsigned_nodeinfo_from_signer_dropped(void) +{ + mockNodeDB->addNode(REMOTE_NODE); + mockNodeDB->setSignerBit(REMOTE_NODE, true); + + NodeInfoTestShim shim; + meshtastic_MeshPacket mp = makeNodeInfoPacket(false); + meshtastic_User user = meshtastic_User_init_zero; + user.is_licensed = owner.is_licensed; + + TEST_ASSERT_TRUE_MESSAGE(shim.handleReceivedProtobuf(mp, &user), "unsigned NodeInfo from signer must be dropped"); +} + +void test_N2_signed_nodeinfo_from_signer_not_dropped(void) +{ + mockNodeDB->addNode(REMOTE_NODE); + mockNodeDB->setSignerBit(REMOTE_NODE, true); + + NodeInfoTestShim shim; + meshtastic_MeshPacket mp = makeNodeInfoPacket(true); + meshtastic_User user = meshtastic_User_init_zero; + user.is_licensed = owner.is_licensed; + + TEST_ASSERT_FALSE(shim.handleReceivedProtobuf(mp, &user)); +} + +void test_N3_unsigned_nodeinfo_from_nonsigner_not_dropped(void) +{ + mockNodeDB->addNode(REMOTE_NODE); + + NodeInfoTestShim shim; + meshtastic_MeshPacket mp = makeNodeInfoPacket(false); + meshtastic_User user = meshtastic_User_init_zero; + user.is_licensed = owner.is_licensed; + + TEST_ASSERT_FALSE(shim.handleReceivedProtobuf(mp, &user)); +} + +void test_N4_unsigned_unicast_nodeinfo_from_signer_accepted(void) +{ + mockNodeDB->addNode(REMOTE_NODE); + mockNodeDB->setSignerBit(REMOTE_NODE, true); + + NodeInfoTestShim shim; + meshtastic_MeshPacket mp = makeDecoded(REMOTE_NODE, LOCAL_NODE, meshtastic_PortNum_NODEINFO_APP, SMALL_PAYLOAD); + mp.xeddsa_signed = false; + meshtastic_User user = meshtastic_User_init_zero; + user.is_licensed = owner.is_licensed; + + TEST_ASSERT_FALSE_MESSAGE(shim.handleReceivedProtobuf(mp, &user), + "unsigned unicast NodeInfo from signer must not be dropped"); +} + static void preparePipelineSigner(NodeNum sender) { uint8_t pub[32], priv[32]; @@ -1192,6 +1301,67 @@ void test_C12_exact_authenticated_replay_reuses_verdict_without_collision_bypass TEST_ASSERT_EQUAL_MESSAGE(3, routingAuthEvaluationCount(), "same packet ID with different bytes must be reevaluated"); } +// C5: the packet survives (C4) but the identity claim inside it must not land - the pubkey guard +// can't tell a signer from an impersonator replaying its (public) key. Only the write is refused. +void test_N5_unsigned_unicast_nodeinfo_from_signer_does_not_change_name(void) +{ + mockNodeDB->addNode(REMOTE_NODE); + mockNodeDB->setSignerBit(REMOTE_NODE, true); + mockNodeDB->setLongName(REMOTE_NODE, "Genuine"); + + NodeInfoTestShim shim; + meshtastic_MeshPacket mp = makeDecoded(REMOTE_NODE, LOCAL_NODE, meshtastic_PortNum_NODEINFO_APP, SMALL_PAYLOAD); + mp.xeddsa_signed = false; + meshtastic_User user = meshtastic_User_init_zero; + user.is_licensed = owner.is_licensed; + strcpy(user.long_name, "Spoofed"); + strcpy(user.short_name, "SPF"); + + TEST_ASSERT_FALSE_MESSAGE(shim.handleReceivedProtobuf(mp, &user), "the packet itself must still be accepted"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("Genuine", mockNodeDB->longName(REMOTE_NODE), + "unsigned unicast NodeInfo from a signer must not rewrite its stored name"); +} + +// C6: the same exchange signed - the update is authenticated and must land, pinning C5 as a +// targeted refusal rather than a blanket block on unicast NodeInfo from signers. +void test_N6_signed_unicast_nodeinfo_from_signer_changes_name(void) +{ + mockNodeDB->addNode(REMOTE_NODE); + mockNodeDB->setSignerBit(REMOTE_NODE, true); + mockNodeDB->setLongName(REMOTE_NODE, "Genuine"); + + NodeInfoTestShim shim; + meshtastic_MeshPacket mp = makeDecoded(REMOTE_NODE, LOCAL_NODE, meshtastic_PortNum_NODEINFO_APP, SMALL_PAYLOAD); + mp.xeddsa_signed = true; + meshtastic_User user = meshtastic_User_init_zero; + user.is_licensed = owner.is_licensed; + strcpy(user.long_name, "Renamed"); + strcpy(user.short_name, "RNM"); + + TEST_ASSERT_FALSE(shim.handleReceivedProtobuf(mp, &user)); + TEST_ASSERT_EQUAL_STRING_MESSAGE("Renamed", mockNodeDB->longName(REMOTE_NODE), + "a signed update from a signer must still be learned"); +} + +// C7: a node that has never signed is unaffected - the ordinary case for most of the mesh. +void test_N7_unsigned_unicast_nodeinfo_from_nonsigner_changes_name(void) +{ + mockNodeDB->addNode(REMOTE_NODE); // signer bit clear + mockNodeDB->setLongName(REMOTE_NODE, "Genuine"); + + NodeInfoTestShim shim; + meshtastic_MeshPacket mp = makeDecoded(REMOTE_NODE, LOCAL_NODE, meshtastic_PortNum_NODEINFO_APP, SMALL_PAYLOAD); + mp.xeddsa_signed = false; + meshtastic_User user = meshtastic_User_init_zero; + user.is_licensed = owner.is_licensed; + strcpy(user.long_name, "Renamed"); + strcpy(user.short_name, "RNM"); + + TEST_ASSERT_FALSE(shim.handleReceivedProtobuf(mp, &user)); + TEST_ASSERT_EQUAL_STRING_MESSAGE("Renamed", mockNodeDB->longName(REMOTE_NODE), + "non-signer identity learning must be unaffected"); +} + // =========================================================================== // Group D - encoding invariants the routing gates depend on // =========================================================================== @@ -1332,6 +1502,103 @@ void test_E9_decoded_partial_signature_from_nonsigner_dropped(void) TEST_ASSERT_FALSE_MESSAGE(checkXeddsaReceivePolicy(&p), "partial signature must be dropped as malformed"); } +// Build an unsigned broadcast whose inner message is padded with an unknown field, and pin that the +// padding pushes the RAW size past the fit threshold - the exemption the attacker is buying - while +// the frame stays sendable. Without canonical inner sizing these packets are wrongly accepted. +static meshtastic_MeshPacket makePayloadPaddedBroadcast(meshtastic_PortNum port, const pb_msgdesc_t *fields, const void *inner, + size_t padLen) +{ + meshtastic_MeshPacket p = makeDecoded(REMOTE_NODE, NODENUM_BROADCAST, port, 0); + const size_t innerLen = pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), fields, inner); + TEST_ASSERT_GREATER_THAN_MESSAGE(0, innerLen, "failed to encode the spoofed inner message"); + p.decoded.payload.size = + innerLen + appendUnknownField(p.decoded.payload.bytes + innerLen, sizeof(p.decoded.payload.bytes) - innerLen, padLen); + + TEST_ASSERT_FALSE_MESSAGE(signedEncodingFits(&p.decoded), "padding must push the raw size past the fit threshold"); + TEST_ASSERT_LESS_OR_EQUAL_MESSAGE(MAX_LORA_PAYLOAD_LEN, encodedDataSize(&p.decoded) + MESHTASTIC_HEADER_LENGTH, + "padded frame must still be one a radio could send"); + return p; +} + +// E10: unknown fields buried inside Data.payload are discarded by the module's own pb_decode, so +// they must not sway the downgrade decision the way A10 already pins for Data-level unknown fields. +void test_E10_decoded_unsigned_position_padded_inside_payload_dropped(void) +{ + mockNodeDB->addNode(REMOTE_NODE); + mockNodeDB->setSignerBit(REMOTE_NODE, true); + + meshtastic_Position pos = meshtastic_Position_init_zero; + pos.has_latitude_i = pos.has_longitude_i = true; + pos.latitude_i = 371234567; + pos.longitude_i = -1221234567; + + meshtastic_MeshPacket p = makePayloadPaddedBroadcast(meshtastic_PortNum_POSITION_APP, &meshtastic_Position_msg, &pos, 163); + + TEST_ASSERT_FALSE_MESSAGE(checkXeddsaReceivePolicy(&p), "payload-padded unsigned Position from a signer must be dropped"); + TEST_ASSERT_FALSE(p.xeddsa_signed); +} + +// E11: over-correction guard. Telemetry (272 bytes max) and Waypoint (199) can legitimately exceed +// the signable budget, so canonical sizing must not shrink an honest one into the drop range. +void test_E11_decoded_unsigned_oversized_telemetry_from_signer_accepted(void) +{ + mockNodeDB->addNode(REMOTE_NODE); + mockNodeDB->setSignerBit(REMOTE_NODE, true); + + meshtastic_Telemetry t = meshtastic_Telemetry_init_zero; + t.which_variant = meshtastic_Telemetry_host_metrics_tag; + t.variant.host_metrics.uptime_seconds = 123456; + t.variant.host_metrics.has_user_string = true; + memset(t.variant.host_metrics.user_string, 'x', sizeof(t.variant.host_metrics.user_string) - 1); + + meshtastic_MeshPacket p = makeDecoded(REMOTE_NODE, NODENUM_BROADCAST, meshtastic_PortNum_TELEMETRY_APP, 0); + p.decoded.payload.size = + pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_Telemetry_msg, &t); + TEST_ASSERT_GREATER_THAN_MESSAGE(0, p.decoded.payload.size, "failed to encode the oversized Telemetry"); + + // Every byte here is a field this build understands, so canonical sizing must leave it alone. + TEST_ASSERT_FALSE_MESSAGE(signedEncodingFits(&p.decoded), "telemetry must be too big to sign, else the test is vacuous"); + + TEST_ASSERT_TRUE_MESSAGE(checkXeddsaReceivePolicy(&p), "honest oversized telemetry from a signer must not be dropped"); +} + +// E12: E10 for the Waypoint branch of the canonical-sizing switch. +void test_E12_decoded_unsigned_waypoint_padded_inside_payload_dropped(void) +{ + mockNodeDB->addNode(REMOTE_NODE); + mockNodeDB->setSignerBit(REMOTE_NODE, true); + + meshtastic_Waypoint w = meshtastic_Waypoint_init_zero; + w.id = 42; + w.has_latitude_i = w.has_longitude_i = true; + w.latitude_i = 371234567; + w.longitude_i = -1221234567; + strcpy(w.name, "spoofed"); + + meshtastic_MeshPacket p = makePayloadPaddedBroadcast(meshtastic_PortNum_WAYPOINT_APP, &meshtastic_Waypoint_msg, &w, 150); + + TEST_ASSERT_FALSE_MESSAGE(checkXeddsaReceivePolicy(&p), "payload-padded unsigned Waypoint from a signer must be dropped"); + TEST_ASSERT_FALSE(p.xeddsa_signed); +} + +// E13: E10 for the NodeInfo/User branch. Router drops it before rebroadcast; NodeInfoModule's own +// check (Group C) is receiver-local and would not stop the packet propagating. +void test_E13_decoded_unsigned_nodeinfo_padded_inside_payload_dropped(void) +{ + mockNodeDB->addNode(REMOTE_NODE); + mockNodeDB->setSignerBit(REMOTE_NODE, true); + + meshtastic_User u = meshtastic_User_init_zero; + strcpy(u.id, "!0b0b0b0b"); + strcpy(u.long_name, "spoofed node"); + strcpy(u.short_name, "SPF"); + + meshtastic_MeshPacket p = makePayloadPaddedBroadcast(meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg, &u, 150); + + TEST_ASSERT_FALSE_MESSAGE(checkXeddsaReceivePolicy(&p), "payload-padded unsigned NodeInfo from a signer must be dropped"); + TEST_ASSERT_FALSE(p.xeddsa_signed); +} + void setup() { initializeTestEnvironment(); @@ -1371,6 +1638,8 @@ void setup() #if WARM_NODE_COUNT > 0 RUN_TEST(test_A17_strict_verifies_signer_from_warm_key_store); #endif + RUN_TEST(test_A18_unsigned_broadcast_from_signer_with_unknown_fields_dropped); + RUN_TEST(test_A19_unsigned_broadcast_from_nonsigner_with_unknown_fields_accepted); printf("\n=== Group B: send-side signing policy ===\n"); RUN_TEST(test_B1_local_broadcast_is_signed); @@ -1394,6 +1663,14 @@ void setup() RUN_TEST(test_C10_legacy_channel_dm_failure_has_no_pipeline_effects); RUN_TEST(test_C11_malformed_pki_plaintext_has_no_pipeline_effects); RUN_TEST(test_C12_exact_authenticated_replay_reuses_verdict_without_collision_bypass); + printf("\n=== Group N: NodeInfoModule authentication ===\n"); + RUN_TEST(test_N1_unsigned_nodeinfo_from_signer_dropped); + RUN_TEST(test_N2_signed_nodeinfo_from_signer_not_dropped); + RUN_TEST(test_N3_unsigned_nodeinfo_from_nonsigner_not_dropped); + RUN_TEST(test_N4_unsigned_unicast_nodeinfo_from_signer_accepted); + RUN_TEST(test_N5_unsigned_unicast_nodeinfo_from_signer_does_not_change_name); + RUN_TEST(test_N6_signed_unicast_nodeinfo_from_signer_changes_name); + RUN_TEST(test_N7_unsigned_unicast_nodeinfo_from_nonsigner_changes_name); printf("\n=== Group D: encoding invariants ===\n"); RUN_TEST(test_D1_signature_field_overhead_exact); @@ -1407,6 +1684,10 @@ void setup() RUN_TEST(test_E6_decoded_unsigned_unicast_from_signer_accepted); RUN_TEST(test_E8_decoded_partial_signature_from_signer_dropped); RUN_TEST(test_E9_decoded_partial_signature_from_nonsigner_dropped); + RUN_TEST(test_E10_decoded_unsigned_position_padded_inside_payload_dropped); + RUN_TEST(test_E11_decoded_unsigned_oversized_telemetry_from_signer_accepted); + RUN_TEST(test_E12_decoded_unsigned_waypoint_padded_inside_payload_dropped); + RUN_TEST(test_E13_decoded_unsigned_nodeinfo_padded_inside_payload_dropped); exit(UNITY_END()); } diff --git a/test/test_xmodem/test_main.cpp b/test/test_xmodem/test_main.cpp index 816c78e9c..c6a20fdf0 100644 --- a/test/test_xmodem/test_main.cpp +++ b/test/test_xmodem/test_main.cpp @@ -21,6 +21,22 @@ void test_xmodem_rejects_dotdot_traversal(void) TEST_ASSERT_FALSE(XModemAdapter::isValidFilename("/..")); } +void test_xmodem_rejects_backslash_traversal(void) +{ + TEST_ASSERT_FALSE(XModemAdapter::isValidFilename("..\\secret")); + TEST_ASSERT_FALSE(XModemAdapter::isValidFilename("..\\..\\Windows\\System32\\drivers\\etc\\hosts")); + TEST_ASSERT_FALSE(XModemAdapter::isValidFilename("dir\\..\\..\\x")); + TEST_ASSERT_FALSE(XModemAdapter::isValidFilename("dir/..\\x")); + TEST_ASSERT_FALSE(XModemAdapter::isValidFilename("dir\\..")); +} + +void test_xmodem_rejects_drive_qualified(void) +{ + TEST_ASSERT_FALSE(XModemAdapter::isValidFilename("C:\\Windows\\System32\\x")); + TEST_ASSERT_FALSE(XModemAdapter::isValidFilename("C:/Windows/System32/x")); + TEST_ASSERT_FALSE(XModemAdapter::isValidFilename("c:relative.txt")); +} + void test_xmodem_rejects_empty(void) { TEST_ASSERT_FALSE(XModemAdapter::isValidFilename("")); @@ -36,6 +52,9 @@ void test_xmodem_allows_legit_paths(void) // ".." only inside a name (not a whole component) is a valid filename, not traversal. TEST_ASSERT_TRUE(XModemAdapter::isValidFilename("my..file")); TEST_ASSERT_TRUE(XModemAdapter::isValidFilename("...")); + // A colon that cannot form a drive qualifier is a legal filename on the POSIX daemon. + TEST_ASSERT_TRUE(XModemAdapter::isValidFilename("1:30pm.txt")); + TEST_ASSERT_TRUE(XModemAdapter::isValidFilename("dir/1:30pm.txt")); } #endif // FSCom @@ -46,6 +65,8 @@ void setup() UNITY_BEGIN(); #ifdef FSCom RUN_TEST(test_xmodem_rejects_dotdot_traversal); + RUN_TEST(test_xmodem_rejects_backslash_traversal); + RUN_TEST(test_xmodem_rejects_drive_qualified); RUN_TEST(test_xmodem_rejects_empty); RUN_TEST(test_xmodem_allows_legit_paths); #endif diff --git a/variants/esp32s3/t5s3_epaper/platformio.ini b/variants/esp32s3/t5s3_epaper/platformio.ini index 781b382b5..16997945f 100644 --- a/variants/esp32s3/t5s3_epaper/platformio.ini +++ b/variants/esp32s3/t5s3_epaper/platformio.ini @@ -28,6 +28,14 @@ lib_deps = https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip +; This board's 3.3V octal (AP_3v3) 8MB PSRAM is unreliable at the qio_opi default 80MHz +; (Total PSRAM reads 0); 40MHz makes it enumerate. Forces a from-source build as no +; precompiled 40MHz-octal libs exist; bandwidth is immaterial for an e-paper node. +custom_sdkconfig = + ${esp32s3_base.custom_sdkconfig} + CONFIG_SPIRAM_SPEED_40M=y + CONFIG_SPIRAM_SPEED=40 + [env:t5s3_epaper_inkhud] extends = t5s3_epaper_base, inkhud board_level = extra