Merge upstream develop into packet authentication policy

This commit is contained in:
Benjamin Faershtein
2026-07-20 11:48:59 -07:00
74 changed files with 836 additions and 939 deletions
-38
View File
@@ -30,44 +30,6 @@ SPIClass SPI_HSPI(HSPI);
#endif // HAS_SDCARD #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. * Renames a file from pathFrom to pathTo.
* *
-1
View File
@@ -57,7 +57,6 @@ using namespace Adafruit_LittleFS_Namespace;
#endif #endif
void fsInit(); void fsInit();
bool copyFile(const char *from, const char *to);
bool renameFile(const char *pathFrom, const char *pathTo); bool renameFile(const char *pathFrom, const char *pathTo);
bool fsFormat(); bool fsFormat();
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels, size_t maxCount = 64, bool *wasLimited = nullptr); std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels, size_t maxCount = 64, bool *wasLimited = nullptr);
-35
View File
@@ -7,7 +7,6 @@
#include "SafeFile.h" #include "SafeFile.h"
#include "gps/RTC.h" #include "gps/RTC.h"
#include "memory/MemAudit.h" #include "memory/MemAudit.h"
#include <cassert>
#include <cstring> // memcpy #include <cstring> // memcpy
#ifndef MESSAGE_TEXT_POOL_SIZE #ifndef MESSAGE_TEXT_POOL_SIZE
@@ -244,40 +243,6 @@ const StoredMessage *MessageStore::tryAddFromPacket(const meshtastic_MeshPacket
return &liveMessages.back(); 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 #if ENABLE_MESSAGE_PERSISTENCE
// Compact, fixed-size on-flash representation using offset + length // Compact, fixed-size on-flash representation using offset + length
+1 -3
View File
@@ -93,10 +93,8 @@ class MessageStore
void addLiveMessage(StoredMessage &&msg); void addLiveMessage(StoredMessage &&msg);
void addLiveMessage(const StoredMessage &msg); // convenience overload void addLiveMessage(const StoredMessage &msg); // convenience overload
const std::deque<StoredMessage> &getLiveMessages() const { return liveMessages; } const std::deque<StoredMessage> &getLiveMessages() const { return liveMessages; }
// Add new messages from packets. Returns nullptr if the packet is filtered out.
const StoredMessage *tryAddFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing -> RAM only 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) // Persistence methods (used only on boot/shutdown)
void saveToFlash(); // Save messages to flash void saveToFlash(); // Save messages to flash
-10
View File
@@ -57,16 +57,6 @@ void consoleInit()
DEBUG_PORT.rpInit(); // Simply sets up semaphore 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. /// Initialize console, protobuf transport, serial port, and worker thread state.
SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), concurrency::OSThread("SerialConsole") SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), concurrency::OSThread("SerialConsole")
{ {
-1
View File
@@ -67,7 +67,6 @@ class SerialConsole : public StreamAPI, public RedirectablePrint, private concur
}; };
// A simple wrapper to allow non class aware code write to the console // A simple wrapper to allow non class aware code write to the console
void consolePrintf(const char *format, ...);
void consoleInit(); void consoleInit();
extern SerialConsole *console; extern SerialConsole *console;
-12
View File
@@ -194,18 +194,6 @@ void playBoop()
playTones(melody, sizeof(melody) / sizeof(ToneDuration)); 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 state for progressive lead-up notes
static int leadUpNoteIndex = 0; static int leadUpNoteIndex = 0;
static const ToneDuration leadUpNotes[] = { static const ToneDuration leadUpNotes[] = {
-1
View File
@@ -12,6 +12,5 @@ void play4ClickUp();
void playBoop(); void playBoop();
void playChirp(); void playChirp();
void playClick(); void playClick();
void playLongPressLeadUp();
bool playNextLeadUpNote(); // Play the next note in the lead-up sequence bool playNextLeadUpNote(); // Play the next note in the lead-up sequence
void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning
-5
View File
@@ -2201,11 +2201,6 @@ bool GPS::hasLock()
return false; return false;
} }
bool GPS::hasFlow()
{
return reader.passedChecksum() > 0;
}
bool GPS::whileActive() bool GPS::whileActive()
{ {
unsigned int charsInBuf = 0; unsigned int charsInBuf = 0;
-3
View File
@@ -111,9 +111,6 @@ class GPS : private concurrency::OSThread
/// Returns true if we have acquired GPS lock. /// Returns true if we have acquired GPS lock.
virtual bool hasLock(); 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 /// Return true if we are connected to a GPS
bool isConnected() const { return hasGPS; } bool isConnected() const { return hasGPS; }
-28
View File
@@ -496,34 +496,6 @@ float GeoCoord::rangeMetersToRadians(double range_meters)
return (PI / (180 * 60)) * distance_nm; 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 * Create a new point based on the passed-in point
* Ported from http://www.edwilliams.org/avform147.htm#LL * Ported from http://www.edwilliams.org/avform147.htm#LL
-3
View File
@@ -103,7 +103,6 @@ class GeoCoord
static void convertWGS84ToOSGB36(const double lat, const double lon, double &osgb_Latitude, double &osgb_Longitude); static 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 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 bearing(double lat1, double lon1, double lat2, double lon2);
static float rangeRadiansToMeters(double range_radians);
static float rangeMetersToRadians(double range_meters); static float rangeMetersToRadians(double range_meters);
static unsigned int bearingToDegrees(const char *bearing); static unsigned int bearingToDegrees(const char *bearing);
static const char *degreesToBearing(unsigned int degrees); static const char *degreesToBearing(unsigned int degrees);
@@ -114,8 +113,6 @@ class GeoCoord
static double toDegrees(double r); static double toDegrees(double r);
// Point to point conversions // Point to point conversions
int32_t distanceTo(const GeoCoord &pointB);
int32_t bearingTo(const GeoCoord &pointB);
std::shared_ptr<GeoCoord> pointAtDistance(double bearing, double range); std::shared_ptr<GeoCoord> pointAtDistance(double bearing, double range);
// Lat lon alt getters // Lat lon alt getters
+22 -2
View File
@@ -77,12 +77,13 @@ EInkParallelDisplay::~EInkParallelDisplay()
bool EInkParallelDisplay::connect() bool EInkParallelDisplay::connect()
{ {
LOG_INFO("Do EPD init"); LOG_INFO("Do EPD init");
int initRc = BBEP_SUCCESS;
if (!epaper) { if (!epaper) {
epaper = new FASTEPD; epaper = new FASTEPD;
#if defined(T5_S3_EPAPER_PRO_V1) #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) #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 // initialize all port 0 pins (0-7) as outputs / HIGH
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
epaper->ioPinMode(i, OUTPUT); epaper->ioPinMode(i, OUTPUT);
@@ -93,6 +94,16 @@ bool EInkParallelDisplay::connect()
#endif #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->setRotation(rotation); // does not work, messes up width/height
epaper->setMode(BB_MODE_1BPP); epaper->setMode(BB_MODE_1BPP);
epaper->clearWhite(); epaper->clearWhite();
@@ -103,6 +114,7 @@ bool EInkParallelDisplay::connect()
resetGhostPixelTracking(); resetGhostPixelTracking();
#endif #endif
displayReady = true;
return true; return true;
} }
@@ -187,6 +199,9 @@ void EInkParallelDisplay::asyncFullUpdateTask(void *pvParameters)
*/ */
void EInkParallelDisplay::display(void) 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 w = this->displayWidth;
const uint16_t h = this->displayHeight; const uint16_t h = this->displayHeight;
@@ -400,6 +415,9 @@ void EInkParallelDisplay::resetGhostPixelTracking()
*/ */
bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit) bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit)
{ {
if (!displayReady)
return false;
uint32_t now = millis(); uint32_t now = millis();
if (lastDrawMsec == 0 || (now - lastDrawMsec) > msecLimit) { if (lastDrawMsec == 0 || (now - lastDrawMsec) > msecLimit) {
display(); display();
@@ -410,6 +428,8 @@ bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit)
void EInkParallelDisplay::endUpdate() void EInkParallelDisplay::endUpdate()
{ {
if (!displayReady)
return;
{ {
// ensure any async full update is started/completed // ensure any async full update is started/completed
if (asyncFullRunning.load()) { if (asyncFullRunning.load()) {
+3
View File
@@ -40,6 +40,9 @@ class EInkParallelDisplay : public OLEDDisplay
uint32_t lastDrawMsec = 0; uint32_t lastDrawMsec = 0;
FASTEPD *epaper; FASTEPD *epaper;
// Set only when connect() fully succeeds; framebuffer-touching methods no-op while false.
bool displayReady = false;
private: private:
// Async full-refresh support // Async full-refresh support
std::atomic<bool> asyncFullRunning{false}; std::atomic<bool> asyncFullRunning{false};
-34
View File
@@ -1846,40 +1846,6 @@ void Screen::handleStartFirmwareUpdateScreen()
setFrameImmediateDraw(frames); 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() void Screen::increaseBrightness()
{ {
brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62); brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62);
-2
View File
@@ -310,8 +310,6 @@ class Screen : public concurrency::OSThread
*/ */
void doDeepSleep(); void doDeepSleep();
void blink();
// Draw north // Draw north
float estimatedHeading(double lat, double lon); float estimatedHeading(double lat, double lon);
-5
View File
@@ -718,11 +718,6 @@ void VirtualKeyboard::setInputText(const std::string &text)
inputText = text; inputText = text;
} }
std::string VirtualKeyboard::getInputText() const
{
return inputText;
}
void VirtualKeyboard::setHeader(const std::string &header) void VirtualKeyboard::setHeader(const std::string &header)
{ {
headerText = header; headerText = header;
-1
View File
@@ -27,7 +27,6 @@ class VirtualKeyboard
void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY); void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
void setInputText(const std::string &text); void setInputText(const std::string &text);
std::string getInputText() const;
void setHeader(const std::string &header); void setHeader(const std::string &header);
void setCallback(std::function<void(const std::string &)> callback); void setCallback(std::function<void(const std::string &)> callback);
-124
View File
@@ -230,131 +230,7 @@ void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i
#endif #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 // 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) void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
drawFrameWiFi(display, state, x, y); drawFrameWiFi(display, state, x, y);
-3
View File
@@ -20,12 +20,9 @@ namespace DebugRenderer
{ {
// Debug frame functions // Debug frame functions
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); 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); void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Trampoline functions for framework callback compatibility // 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); void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// LoRa information display // LoRa information display
-34
View File
@@ -17,12 +17,6 @@
#include "meshUtils.h" #include "meshUtils.h"
#include <algorithm> #include <algorithm>
// Forward declarations for functions defined in Screen.cpp
namespace graphics
{
extern bool haveGlyphs(const char *str);
} // namespace graphics
// Global screen instance // Global screen instance
extern graphics::Screen *screen; extern graphics::Screen *screen;
@@ -180,11 +174,6 @@ unsigned long getModeCycleIntervalMs()
return 3000; 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) 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; 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); 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 NodeListRenderer
} // namespace graphics } // namespace graphics
#endif #endif
-1
View File
@@ -58,7 +58,6 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
const char *getCurrentModeTitle_Nodes(int screenWidth); const char *getCurrentModeTitle_Nodes(int screenWidth);
const char *getCurrentModeTitle_Location(int screenWidth); const char *getCurrentModeTitle_Location(int screenWidth);
std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth); 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 // Scrolling controls
void scrollUp(); void scrollUp();
@@ -207,8 +207,6 @@ void NotificationRenderer::resetBanner()
alertBannerMessage[0] = '\0'; alertBannerMessage[0] = '\0';
current_notification_type = notificationTypeEnum::none; current_notification_type = notificationTypeEnum::none;
OnScreenKeyboardModule::instance().clearPopup();
inEvent.inputEvent = INPUT_BROKER_NONE; inEvent.inputEvent = INPUT_BROKER_NONE;
inEvent.kbchar = 0; inEvent.kbchar = 0;
curSelected = 0; curSelected = 0;
@@ -1187,9 +1185,6 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
display->setColor(WHITE); display->setColor(WHITE);
// Draw the virtual keyboard // Draw the virtual keyboard
virtualKeyboard->draw(display, 0, 0); virtualKeyboard->draw(display, 0, 0);
// Draw transient popup overlay (if any) managed by OnScreenKeyboardModule
OnScreenKeyboardModule::instance().drawPopupOverlay(display);
} else { } else {
// If virtualKeyboard is null, reset the banner to avoid getting stuck // If virtualKeyboard is null, reset the banner to avoid getting stuck
LOG_INFO("Virtual keyboard is null - resetting banner"); LOG_INFO("Virtual keyboard is null - resetting banner");
@@ -1202,12 +1197,5 @@ bool NotificationRenderer::isOverlayBannerShowing()
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); 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 } // namespace graphics
#endif #endif
-1
View File
@@ -39,7 +39,6 @@ class NotificationRenderer
static BannerFont alertBannerLineFonts[MAX_LINES + 1]; static BannerFont alertBannerLineFonts[MAX_LINES + 1];
static void parseBannerMessageWithFonts(const char *message); static void parseBannerMessageWithFonts(const char *message);
static void resetBanner(); static void resetBanner();
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
-24
View File
@@ -1386,30 +1386,6 @@ int UIRenderer::formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLED
return display->getStringWidth(buf); 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 #ifdef USE_EINK
/// Used on eink displays while in deep sleep /// Used on eink displays while in deep sleep
void UIRenderer::drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void UIRenderer::drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
-3
View File
@@ -104,9 +104,6 @@ class UIRenderer
{ {
drawStringWithEmotes(display, x, y, line.c_str(), fontHeight, emoteSpacing, fauxBold); 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 UIRenderer
} // namespace graphics } // namespace graphics
-24
View File
@@ -649,30 +649,6 @@ std::string InkHUD::Applet::getTimeString()
return getTimeString(getValidTime(RTCQuality::RTCQualityDevice, true)); 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 // Get an abbreviated, human readable, distance string
// Honors config.display.units, to offer both metric and imperial // Honors config.display.units, to offer both metric and imperial
std::string InkHUD::Applet::localizeDistance(uint32_t meters) std::string InkHUD::Applet::localizeDistance(uint32_t meters)
-1
View File
@@ -183,7 +183,6 @@ class Applet : public GFX
SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value 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(uint32_t epochSeconds); // Human readable
std::string getTimeString(); // Current time, 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 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 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 std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars
@@ -217,13 +217,6 @@ bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n)
return true; 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. // Messages are loaded once by InkHUD::begin() before applets start.
// Nothing to do here at per-applet activation time. // Nothing to do here at per-applet activation time.
void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash() void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash()
@@ -46,7 +46,6 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule
bool approveNotification(Notification &n) override; // Which notifications to suppress bool approveNotification(Notification &n) override; // Which notifications to suppress
protected: protected:
void saveMessagesToFlash();
void loadMessagesFromFlash(); void loadMessagesFromFlash();
uint8_t channelIndex = 0; uint8_t channelIndex = 0;
-38
View File
@@ -326,44 +326,6 @@ void InkHUD::InkHUD::touchNavDown()
} }
} }
// Call this when touch input needs joystick-like left navigation independent of joystick-enabled mode
void InkHUD::InkHUD::touchNavLeft()
{
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
case 1: // 90 deg
events->onTouchNavDown();
break;
case 2: // 180 deg
events->onTouchNavRight();
break;
case 3: // 270 deg
events->onTouchNavUp();
break;
default: // 0 deg
events->onTouchNavLeft();
break;
}
}
// Call this when touch input needs joystick-like right navigation independent of joystick-enabled mode
void InkHUD::InkHUD::touchNavRight()
{
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
case 1: // 90 deg
events->onTouchNavUp();
break;
case 2: // 180 deg
events->onTouchNavLeft();
break;
case 3: // 270 deg
events->onTouchNavDown();
break;
default: // 0 deg
events->onTouchNavRight();
break;
}
}
void InkHUD::InkHUD::touchTap(uint16_t x, uint16_t y) void InkHUD::InkHUD::touchTap(uint16_t x, uint16_t y)
{ {
events->onTouchTap(x, y, false); events->onTouchTap(x, y, false);
-2
View File
@@ -71,8 +71,6 @@ class InkHUD
void navRight(); void navRight();
void touchNavUp(); void touchNavUp();
void touchNavDown(); void touchNavDown();
void touchNavLeft();
void touchNavRight();
void touchTap(uint16_t x, uint16_t y); void touchTap(uint16_t x, uint16_t y);
void touchLongPress(uint16_t x, uint16_t y); void touchLongPress(uint16_t x, uint16_t y);
+4 -2
View File
@@ -496,8 +496,10 @@ We keep this separate latest-message cache for this purpose, because:
Broadcasts and DMs take different paths into `messageStore`: Broadcasts 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`. - **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.addFromPacket()` directly and stores the result in `latestMessage.dm`. - **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 #### Saving / Loading
-8
View File
@@ -117,14 +117,6 @@ void TwoButton::setHandlerDown(uint8_t whichButton, Callback onDown)
buttons[whichButton].onDown = 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 // Set what should happen when a "short press" event has occurred
void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress) void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress)
{ {
-1
View File
@@ -38,7 +38,6 @@ class TwoButton : protected concurrency::OSThread
void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false);
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
void setHandlerDown(uint8_t whichButton, Callback onDown); void setHandlerDown(uint8_t whichButton, Callback onDown);
void setHandlerUp(uint8_t whichButton, Callback onUp);
void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); void setHandlerShortPress(uint8_t whichButton, Callback onShortPress);
void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); void setHandlerLongPress(uint8_t whichButton, Callback onLongPress);
@@ -194,14 +194,6 @@ void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown)
buttons[whichButton].onDown = 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 // Set what should happen when a "short press" event has occurred
void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress) void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress)
{ {
@@ -217,26 +209,6 @@ void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLong
buttons[whichButton].onLongPress = onLongPress; 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 // Set what should happen when a "press" event has fired
// Note: this will occur while the joystick button is still held // Note: this will occur while the joystick button is still held
void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress) void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress)
@@ -49,11 +49,8 @@ class TwoButtonExtended : protected concurrency::OSThread
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
void setJoystickDebounce(uint32_t debounceMs); void setJoystickDebounce(uint32_t debounceMs);
void setHandlerDown(uint8_t whichButton, Callback onDown); void setHandlerDown(uint8_t whichButton, Callback onDown);
void setHandlerUp(uint8_t whichButton, Callback onUp);
void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); void setHandlerShortPress(uint8_t whichButton, Callback onShortPress);
void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); 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 setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress);
void setTwoWayRockerPressHandlers(Callback lPress, Callback rPress); void setTwoWayRockerPressHandlers(Callback lPress, Callback rPress);
+1 -1
View File
@@ -1222,7 +1222,7 @@ bool runASAP;
// TODO find better home than main.cpp // TODO find better home than main.cpp
extern meshtastic_DeviceMetadata getDeviceMetadata() 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)); strncpy(deviceMetadata.firmware_version, optstr(APP_VERSION), sizeof(deviceMetadata.firmware_version));
deviceMetadata.device_state_version = DEVICESTATE_CUR_VER; deviceMetadata.device_state_version = DEVICESTATE_CUR_VER;
deviceMetadata.canShutdown = pmu_found || HAS_CPU_SHUTDOWN; deviceMetadata.canShutdown = pmu_found || HAS_CPU_SHUTDOWN;
-14
View File
@@ -9,7 +9,6 @@
*/ */
#include "memGet.h" #include "memGet.h"
#include "configuration.h" #include "configuration.h"
#include "memory/MemAudit.h"
#if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP) #if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP)
#include <malloc.h> #include <malloc.h>
@@ -108,16 +107,3 @@ uint32_t MemGet::getPsramSize()
return 0; return 0;
#endif #endif
} }
void displayPercentHeapFree()
{
uint32_t freeHeap = memGet.getFreeHeap();
uint32_t totalHeap = memGet.getHeapSize();
if (totalHeap == 0 || totalHeap == UINT32_MAX) {
LOG_INFO("Heap size unavailable");
return;
}
int percent = (int)((freeHeap * 100) / totalHeap);
LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap);
memaudit::logBreakdown("heap"); // per-subsystem breakdown rides along with the periodic heap log
}
+27 -15
View File
@@ -177,6 +177,9 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
} }
#endif #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) // 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 (!isToUs(p) && !isFromUs(p) && (p->hop_limit > 0 || exhaustHops)) {
if (p->id != 0) { if (p->id != 0) {
@@ -210,11 +213,10 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
} }
#endif #endif
if (p->next_hop == NO_NEXT_HOP_PREFERENCE) { ErrorCode res =
FloodingRouter::send(tosend); (p->next_hop == NO_NEXT_HOP_PREFERENCE) ? FloodingRouter::send(tosend) : NextHopRouter::send(tosend);
} else { if (res == ERRNO_SHOULD_RELEASE)
NextHopRouter::send(tosend); packetPool.release(tosend);
}
return true; return true;
} }
@@ -419,8 +421,10 @@ int32_t NextHopRouter::doRetransmissions()
trafficManagementModule->clearNextHop(p.packet->to); trafficManagementModule->clearNextHop(p.packet->to);
} }
#endif #endif
if (auto *copy = packetPool.allocCopy(*p.packet)) if (auto *copy = packetPool.allocCopy(*p.packet)) {
FloodingRouter::send(copy); if (FloodingRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
} else { } else {
#if NEXTHOP_EARLY_FLOOD_ON_UNVERIFIED #if NEXTHOP_EARLY_FLOOD_ON_UNVERIFIED
// M4 (gated): if the route isn't proven healthy, don't spend a second directed // 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); meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to);
if (sentTo) if (sentTo)
sentTo->next_hop = NO_NEXT_HOP_PREFERENCE; sentTo->next_hop = NO_NEXT_HOP_PREFERENCE;
if (auto *copy = packetPool.allocCopy(*p.packet)) if (auto *copy = packetPool.allocCopy(*p.packet)) {
FloodingRouter::send(copy); if (FloodingRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
} else { } else {
if (auto *copy = packetPool.allocCopy(*p.packet)) if (auto *copy = packetPool.allocCopy(*p.packet)) {
NextHopRouter::send(copy); if (NextHopRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
} }
#else #else
if (auto *copy = packetPool.allocCopy(*p.packet)) if (auto *copy = packetPool.allocCopy(*p.packet)) {
NextHopRouter::send(copy); if (NextHopRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
#endif #endif
} }
} else { } else {
// Note: we call the superclass version because we don't want to have our version of send() add a new // Note: we call the superclass version because we don't want to have our version of send() add a new
// retransmission record // retransmission record
if (auto *copy = packetPool.allocCopy(*p.packet)) if (auto *copy = packetPool.allocCopy(*p.packet)) {
FloodingRouter::send(copy); if (FloodingRouter::send(copy) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
} }
// Queue again // Queue again
+4
View File
@@ -206,7 +206,11 @@ class NextHopRouter : public FloodingRouter
*/ */
std::optional<uint8_t> getNextHop(NodeNum to, uint8_t relay_node); std::optional<uint8_t> getNextHop(NodeNum to, uint8_t relay_node);
#ifdef PIO_UNIT_TESTING
public: // expose perhapsRebroadcast to the test shim
#else
private: private:
#endif
/** Check if we should be rebroadcasting this packet if so, do so. /** Check if we should be rebroadcasting this packet if so, do so.
* @return true if we did rebroadcast */ * @return true if we did rebroadcast */
bool perhapsRebroadcast(const meshtastic_MeshPacket *p) override; bool perhapsRebroadcast(const meshtastic_MeshPacket *p) override;
+8 -1
View File
@@ -3312,13 +3312,20 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
/** Update user info and channel for this node based on received user data /** 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); meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
if (!info) { if (!info) {
return false; 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 !(MESHTASTIC_EXCLUDE_PKI)
if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) { if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) {
printBytes("Incoming Pubkey: ", p.public_key.bytes, 32); printBytes("Incoming Pubkey: ", p.public_key.bytes, 32);
+3 -3
View File
@@ -255,9 +255,9 @@ class NodeDB
*/ */
void updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src = RX_SRC_RADIO); 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 /** 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 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 * Sets a node either favorite or unfavorite. Returns true if the node ends
+65 -19
View File
@@ -359,15 +359,6 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src)
return send(p); 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 * Send a packet on a suitable interface. This routine will
* later free() the packet to pool. This routine is not allowed to stall. * 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) #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 }; enum class NodeInfoBootstrapResult { NOT_APPLICABLE, VERIFIED, INVALID };
static NodeInfoBootstrapResult verifyFirstContactNodeInfo(meshtastic_MeshPacket *p) static NodeInfoBootstrapResult verifyFirstContactNodeInfo(meshtastic_MeshPacket *p)
@@ -557,7 +604,7 @@ static NodeInfoBootstrapResult verifyFirstContactNodeInfo(meshtastic_MeshPacket
return NodeInfoBootstrapResult::VERIFIED; 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 auto policy = config.security.packet_signature_policy;
const bool strict = policy == meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT; 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; return true;
// In Balanced, preserve legacy unsigned-unicast compatibility and only reject a signable // In Balanced, preserve legacy unsigned-unicast compatibility and only reject a signable
// unsigned broadcast from a known signer. encodedDataSize is // unsigned broadcast from a known signer. Canonical sizing removes unknown protobuf
// the size of the encoded Data exactly as the sender built it (or 0 to size p->decoded // fields before mirroring the sender-side signedDataFits() gate. Oversized broadcasts
// canonically); with no signature field present it is the unsigned base, and adding // remain compatible.
// XEDDSA_SIGNATURE_FIELD_BYTES mirrors the sender-side signedDataFits() gate per packet,
// whatever fields the Data carried. Oversized broadcasts remain compatible.
if (nodeDB->hasSeenXeddsaSigner(p->from) && isBroadcast(p->to)) { 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 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); LOG_WARN("Dropping unsigned broadcast from 0x%08x that previously signed", p->from);
return false; return false;
} }
@@ -813,8 +859,8 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
p->channel = chIndex; // change to store the index instead of the hash p->channel = chIndex; // change to store the index instead of the hash
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA) #if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
// Use the encoded Data size before merging local-only bitfield state. // Run before merging local-only bitfield state into the decoded Data.
if (!checkXeddsaReceivePolicy(p, rawSize)) if (!checkXeddsaReceivePolicy(p))
return DecodeState::DECODE_POLICY_REJECT; return DecodeState::DECODE_POLICY_REJECT;
#endif #endif
+3 -4
View File
@@ -94,7 +94,6 @@ class Router : protected concurrency::OSThread, protected PacketHistory
* NOTE: This method will free the provided packet (even if we return an error code) * NOTE: This method will free the provided packet (even if we return an error code)
*/ */
virtual ErrorCode send(meshtastic_MeshPacket *p); 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 /* Statistics for the amount of duplicate received packets and the amount of times we cancel a relay because someone did it
before us */ before us */
@@ -187,9 +186,9 @@ void resetRoutingAuthEvaluationCount();
meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p); meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p);
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA) #if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
/** Enforce the configured XEdDSA receive policy; zero encodedDataSize derives it canonically. /** Enforce the configured XEdDSA receive policy. The caller must hold cryptLock.
* The caller must hold cryptLock. Returns false when the packet must be dropped. */ * Returns false when the packet must be dropped. */
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p, size_t encodedDataSize = 0); bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p);
#endif #endif
extern Router *router; extern Router *router;
-137
View File
@@ -74,15 +74,7 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
ResourceNode *nodeAPIv1FromRadioOptions = new ResourceNode("/api/v1/fromradio", "OPTIONS", &handleAPIv1FromRadio); ResourceNode *nodeAPIv1FromRadioOptions = new ResourceNode("/api/v1/fromradio", "OPTIONS", &handleAPIv1FromRadio);
ResourceNode *nodeAPIv1FromRadio = new ResourceNode("/api/v1/fromradio", "GET", &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 *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 *nodeRestart = new ResourceNode("/restart", "POST", &handleRestart);
ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload); ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload);
@@ -100,8 +92,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
secureServer->registerNode(nodeAPIv1ToRadio); secureServer->registerNode(nodeAPIv1ToRadio);
secureServer->registerNode(nodeAPIv1FromRadioOptions); secureServer->registerNode(nodeAPIv1FromRadioOptions);
secureServer->registerNode(nodeAPIv1FromRadio); secureServer->registerNode(nodeAPIv1FromRadio);
// secureServer->registerNode(nodeHotspotApple);
// secureServer->registerNode(nodeHotspotAndroid);
secureServer->registerNode(nodeRestart); secureServer->registerNode(nodeRestart);
secureServer->registerNode(nodeFormUpload); secureServer->registerNode(nodeFormUpload);
secureServer->registerNode(nodeJsonScanNetworks); secureServer->registerNode(nodeJsonScanNetworks);
@@ -109,12 +99,7 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
secureServer->registerNode(nodeJsonDelete); secureServer->registerNode(nodeJsonDelete);
secureServer->registerNode(nodeJsonReport); secureServer->registerNode(nodeJsonReport);
secureServer->registerNode(nodeJsonNodes); secureServer->registerNode(nodeJsonNodes);
// secureServer->registerNode(nodeUpdateFs);
// secureServer->registerNode(nodeDeleteFs);
secureServer->registerNode(nodeAdmin); secureServer->registerNode(nodeAdmin);
// secureServer->registerNode(nodeAdminFs);
// secureServer->registerNode(nodeAdminSettings);
// secureServer->registerNode(nodeAdminSettingsApply);
secureServer->registerNode(nodeRoot); // This has to be last secureServer->registerNode(nodeRoot); // This has to be last
// Insecure nodes // Insecure nodes
@@ -122,20 +107,13 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
insecureServer->registerNode(nodeAPIv1ToRadio); insecureServer->registerNode(nodeAPIv1ToRadio);
insecureServer->registerNode(nodeAPIv1FromRadioOptions); insecureServer->registerNode(nodeAPIv1FromRadioOptions);
insecureServer->registerNode(nodeAPIv1FromRadio); insecureServer->registerNode(nodeAPIv1FromRadio);
// insecureServer->registerNode(nodeHotspotApple);
// insecureServer->registerNode(nodeHotspotAndroid);
insecureServer->registerNode(nodeRestart); insecureServer->registerNode(nodeRestart);
insecureServer->registerNode(nodeFormUpload); insecureServer->registerNode(nodeFormUpload);
insecureServer->registerNode(nodeJsonScanNetworks); insecureServer->registerNode(nodeJsonScanNetworks);
insecureServer->registerNode(nodeJsonFsBrowseStatic); insecureServer->registerNode(nodeJsonFsBrowseStatic);
insecureServer->registerNode(nodeJsonDelete); insecureServer->registerNode(nodeJsonDelete);
insecureServer->registerNode(nodeJsonReport); insecureServer->registerNode(nodeJsonReport);
// insecureServer->registerNode(nodeUpdateFs);
// insecureServer->registerNode(nodeDeleteFs);
insecureServer->registerNode(nodeAdmin); insecureServer->registerNode(nodeAdmin);
// insecureServer->registerNode(nodeAdminFs);
// insecureServer->registerNode(nodeAdminSettings);
// insecureServer->registerNode(nodeAdminSettingsApply);
insecureServer->registerNode(nodeRoot); // This has to be last insecureServer->registerNode(nodeRoot); // This has to be last
} }
@@ -230,36 +208,6 @@ void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res)
LOG_DEBUG("webAPI handleAPIv1ToRadio"); 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 // Escape a string into a JSON double-quoted literal. Matches the previous
// SimpleJSON StringifyString behavior (0x00-0x1F and 0x7F -> \u00xx lowercase, // SimpleJSON StringifyString behavior (0x00-0x1F and 0x7F -> \u00xx lowercase,
// escapes " \ / \b \f \n \r \t, UTF-8 passes through unchanged). // 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()); res->print(out.c_str());
} }
/*
This supports the Apple Captive Network Assistant (CNA) Portal
*/
void handleHotspot(HTTPRequest *req, HTTPResponse *res)
{
LOG_INFO("Hotspot Request");
/*
If we don't do a redirect, be sure to return a "Success" message
otherwise iOS will have trouble detecting that the connection to the SoftAP worked.
*/
// Status code is 200 OK by default.
// We want to deliver a simple HTML page, so we send a corresponding content type:
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET");
// res->println("<!DOCTYPE html>");
res->println("<meta http-equiv=\"refresh\" content=\"0;url=/\" />");
}
void handleDeleteFsContent(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET");
res->println("<h1>Meshtastic</h1>");
res->println("Delete Content in /static/*");
LOG_INFO("Delete files from /static/* : ");
concurrency::LockGuard g(spiLock);
htmlDeleteDir("/static");
res->println("<p><hr><p><a href=/admin>Back to admin</a>");
}
void handleAdmin(HTTPRequest *req, HTTPResponse *res) void handleAdmin(HTTPRequest *req, HTTPResponse *res)
{ {
res->setHeader("Content-Type", "text/html"); res->setHeader("Content-Type", "text/html");
@@ -909,52 +818,6 @@ void handleAdmin(HTTPRequest *req, HTTPResponse *res)
res->println("<a href=/json/report>Device Report</a><br>"); res->println("<a href=/json/report>Device Report</a><br>");
} }
void handleAdminSettings(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET");
res->println("<h1>Meshtastic</h1>");
res->println("This isn't done.");
res->println("<form action=/admin/settings/apply method=post>");
res->println("<table border=1>");
res->println("<tr><td>Set?</td><td>Setting</td><td>current value</td><td>new value</td></tr>");
res->println("<tr><td><input type=checkbox></td><td>WiFi SSID</td><td>false</td><td><input type=radio></td></tr>");
res->println("<tr><td><input type=checkbox></td><td>WiFi Password</td><td>false</td><td><input type=radio></td></tr>");
res->println(
"<tr><td><input type=checkbox></td><td>Smart Position Update</td><td>false</td><td><input type=radio></td></tr>");
res->println("</table>");
res->println("<table>");
res->println("<input type=submit value=Apply New Settings>");
res->println("<form>");
res->println("<p><hr><p><a href=/admin>Back to admin</a>");
}
void handleAdminSettingsApply(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "POST");
res->println("<h1>Meshtastic</h1>");
res->println(
"<html><head><meta http-equiv=\"refresh\" content=\"1;url=/admin/settings\" /><title>Settings Applied. </title>");
res->println("Settings Applied. Please wait.");
}
void handleFs(HTTPRequest *req, HTTPResponse *res)
{
res->setHeader("Content-Type", "text/html");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET");
res->println("<h1>Meshtastic</h1>");
res->println("<a href=/admin/fs/delete>Delete Web Content</a><p><form action=/admin/fs/update "
"method=post><input type=submit value=UPDATE_WEB_CONTENT></form>Be patient!");
res->println("<p><hr><p><a href=/admin>Back to admin</a>");
}
void handleRestart(HTTPRequest *req, HTTPResponse *res) void handleRestart(HTTPRequest *req, HTTPResponse *res)
{ {
res->setHeader("Content-Type", "text/html"); res->setHeader("Content-Type", "text/html");
-6
View File
@@ -4,7 +4,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer);
// Declare some handler functions for the various URLs on the server // Declare some handler functions for the various URLs on the server
void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res); void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res);
void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res); void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res);
void handleHotspot(HTTPRequest *req, HTTPResponse *res);
void handleStatic(HTTPRequest *req, HTTPResponse *res); void handleStatic(HTTPRequest *req, HTTPResponse *res);
void handleRestart(HTTPRequest *req, HTTPResponse *res); void handleRestart(HTTPRequest *req, HTTPResponse *res);
void handleFormUpload(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 handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res);
void handleReport(HTTPRequest *req, HTTPResponse *res); void handleReport(HTTPRequest *req, HTTPResponse *res);
void handleNodes(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 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 // Interface to the PhoneAPI to access the protobufs with messages
class HttpAPI : public PhoneAPI class HttpAPI : public PhoneAPI
-11
View File
@@ -1,14 +1,3 @@
#include "mesh/http/ContentHelper.h" #include "mesh/http/ContentHelper.h"
// #include <Arduino.h> // #include <Arduino.h>
// #include "main.h" // #include "main.h"
void replaceAll(std::string &str, const std::string &from, const std::string &to)
{
if (from.empty())
return;
size_t start_pos = 0;
while ((start_pos = str.find(from, start_pos)) != std::string::npos) {
str.replace(start_pos, from.length(), to);
start_pos += to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx'
}
}
-2
View File
@@ -3,5 +3,3 @@
#include <string> #include <string>
#define BoolToString(x) ((x) ? "true" : "false") #define BoolToString(x) ((x) ? "true" : "false")
void replaceAll(std::string &str, const std::string &from, const std::string &to);
+27
View File
@@ -815,6 +815,23 @@ static void reconcileAccelerometerThread(bool wasOn, bool nowOn, bool otherFeatu
} }
#endif #endif
// A "regenerate keys" client sends a blank SecurityConfig holding only the new private key, rather than the
// config it read from us. Detect that shape - new private key, every other field at its proto default - so it
// isn't mistaken for "and clear everything else".
static bool isBareKeypairRotation(const meshtastic_Config_SecurityConfig &incoming,
const meshtastic_Config_SecurityConfig &current)
{
if (incoming.private_key.size != 32)
return false;
if (current.private_key.size == 32 && memcmp(incoming.private_key.bytes, current.private_key.bytes, 32) == 0)
return false;
return incoming.admin_key_count == 0 && !incoming.is_managed && !incoming.serial_enabled && !incoming.debug_log_api_enabled &&
!incoming.admin_channel_enabled &&
incoming.packet_signature_policy ==
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_COMPATIBLE;
}
void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers) void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
{ {
auto changes = SEGMENT_CONFIG; 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.private_key = config.security.private_key;
incoming.public_key = config.security.public_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 MESHTASTIC_EXCLUDE_PKI || MESHTASTIC_EXCLUDE_XEDDSA
if (incoming.packet_signature_policy != if (incoming.packet_signature_policy !=
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_BALANCED) { meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_BALANCED) {
-24
View File
@@ -82,7 +82,6 @@ CannedMessageModule::CannedMessageModule()
void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel) void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel)
{ {
// Do NOT override explicit broadcast replies // Do NOT override explicit broadcast replies
// Only reuse lastDest in LaunchRepeatDestination()
if (newDest == 0) { if (newDest == 0) {
dest = NODENUM_BROADCAST; 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); 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) void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel)
{ {
// Do NOT override explicit broadcast replies // Do NOT override explicit broadcast replies
// Only reuse lastDest in LaunchRepeatDestination()
if (newDest == 0) { if (newDest == 0) {
dest = NODENUM_BROADCAST; 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) static int getRowHeightForEmoteText(const char *text, int minimumHeight, int emoteSpacing = 2)
{ {
// Grow the row only when an emote is taller than the font. // 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); 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() int CannedMessageModule::getNextIndex()
{ {
if (this->currentMessageIndex >= (this->messagesCount - 1)) { if (this->currentMessageIndex >= (this->messagesCount - 1)) {
-3
View File
@@ -55,7 +55,6 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
CannedMessageModule(); CannedMessageModule();
void LaunchWithDestination(NodeNum, uint8_t newChannel = 0); void LaunchWithDestination(NodeNum, uint8_t newChannel = 0);
void LaunchRepeatDestination();
void LaunchFreetextWithDestination(NodeNum, uint8_t newChannel = 0); void LaunchFreetextWithDestination(NodeNum, uint8_t newChannel = 0);
// === Emote Picker navigation === // === Emote Picker navigation ===
@@ -70,11 +69,9 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
// === State/UI === // === State/UI ===
bool shouldDraw(); bool shouldDraw();
bool hasMessages();
void resetSearch(); void resetSearch();
void updateDestinationSelectionList(); void updateDestinationSelectionList();
void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
bool isCharInputAllowed() const;
String drawWithCursor(String text, int cursor); String drawWithCursor(String text, int cursor);
// === Emote Picker === // === Emote Picker ===
+12 -3
View File
@@ -46,6 +46,16 @@ const static DetectionSensorTriggerHandler handlers[_meshtastic_ModuleConfig_Det
[meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_HIGH] = detection_trigger_either_edge, [meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_HIGH] = detection_trigger_either_edge,
}; };
// The configured trigger type arrives as an unvalidated protobuf enum, so a value outside the
// generated range would index past the handler table. Fall back to the schema default instead.
static meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType configuredTriggerType()
{
const uint32_t configured = (uint32_t)moduleConfig.detection_sensor.detection_trigger_type;
if (configured > (uint32_t)_meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MAX)
return _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN;
return (meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType)configured;
}
int32_t DetectionSensorModule::runOnce() int32_t DetectionSensorModule::runOnce()
{ {
/* /*
@@ -89,8 +99,7 @@ int32_t DetectionSensorModule::runOnce()
if (!Throttle::isWithinTimespanMs(lastSentToMesh, if (!Throttle::isWithinTimespanMs(lastSentToMesh,
Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs))) { Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs))) {
bool isDetected = hasDetectionEvent(); bool isDetected = hasDetectionEvent();
DetectionSensorTriggerVerdict verdict = DetectionSensorTriggerVerdict verdict = handlers[configuredTriggerType()](wasDetected, isDetected);
handlers[moduleConfig.detection_sensor.detection_trigger_type](wasDetected, isDetected);
wasDetected = isDetected; wasDetected = isDetected;
switch (verdict) { switch (verdict) {
case DetectionSensorVerdictDetected: case DetectionSensorVerdictDetected:
@@ -160,5 +169,5 @@ bool DetectionSensorModule::hasDetectionEvent()
{ {
bool currentState = digitalRead(moduleConfig.detection_sensor.monitor_pin); bool currentState = digitalRead(moduleConfig.detection_sensor.monitor_pin);
// LOG_DEBUG("Detection Sensor Module: Current state: %i", currentState); // LOG_DEBUG("Detection Sensor Module: Current state: %i", currentState);
return (moduleConfig.detection_sensor.detection_trigger_type & 1) ? currentState : !currentState; return (configuredTriggerType() & 1) ? currentState : !currentState;
} }
+12 -1
View File
@@ -49,10 +49,21 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!"); LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
return true; 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 // Coerce user.id to be derived from the node number
snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp)); 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); bool wasBroadcast = isBroadcast(mp.to);
-121
View File
@@ -65,7 +65,6 @@ void OnScreenKeyboardModule::stop(bool callEmptyCallback)
// Keep NotificationRenderer legacy pointers in sync // Keep NotificationRenderer legacy pointers in sync
NotificationRenderer::virtualKeyboard = nullptr; NotificationRenderer::virtualKeyboard = nullptr;
NotificationRenderer::textInputCallback = nullptr; NotificationRenderer::textInputCallback = nullptr;
clearPopup();
if (callEmptyCallback && cb) if (callEmptyCallback && cb)
cb(""); cb("");
} }
@@ -131,9 +130,6 @@ bool OnScreenKeyboardModule::draw(OLEDDisplay *display)
display->fillRect(0, 0, display->getWidth(), display->getHeight()); display->fillRect(0, 0, display->getWidth(), display->getHeight());
display->setColor(WHITE); display->setColor(WHITE);
keyboard->draw(display, 0, 0); keyboard->draw(display, 0, 0);
// Draw popup overlay if needed
drawPopup(display);
return true; return true;
} }
@@ -150,123 +146,6 @@ void OnScreenKeyboardModule::onCancel()
stop(true); stop(true);
} }
void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs)
{
if (!title || !content)
return;
strncpy(popupTitle, title, sizeof(popupTitle) - 1);
popupTitle[sizeof(popupTitle) - 1] = '\0';
strncpy(popupMessage, content, sizeof(popupMessage) - 1);
popupMessage[sizeof(popupMessage) - 1] = '\0';
popupUntil = millis() + durationMs;
popupVisible = true;
}
void OnScreenKeyboardModule::clearPopup()
{
popupTitle[0] = '\0';
popupMessage[0] = '\0';
popupUntil = 0;
popupVisible = false;
}
void OnScreenKeyboardModule::drawPopupOverlay(OLEDDisplay *display)
{
// Only render the popup overlay (without drawing the keyboard)
drawPopup(display);
}
void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display)
{
if (!popupVisible)
return;
if (millis() > popupUntil || popupMessage[0] == '\0') {
popupVisible = false;
return;
}
// Build lines and leverage NotificationRenderer inverted box drawing for consistent style
constexpr uint16_t maxContentLines = 3;
const bool hasTitle = popupTitle[0] != '\0';
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
const uint16_t maxWrapWidth = display->width() - 40;
auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector<std::string> {
std::vector<std::string> wrapped;
std::string current;
std::string word;
const char *p = text;
while (*p && wrapped.size() < maxContentLines) {
while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
if (*p == '\n') {
if (!current.empty()) {
wrapped.push_back(current);
current.clear();
if (wrapped.size() >= maxContentLines)
break;
}
}
++p;
}
if (!*p || wrapped.size() >= maxContentLines)
break;
word.clear();
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r')
word += *p++;
if (word.empty())
continue;
std::string test = current.empty() ? word : (current + " " + word);
uint16_t w = display->getStringWidth(test.c_str(), test.length(), true);
if (w <= availableWidth)
current = test;
else {
if (!current.empty()) {
wrapped.push_back(current);
current = word;
if (wrapped.size() >= maxContentLines)
break;
} else {
current = word;
while (current.size() > 1 &&
display->getStringWidth(current.c_str(), current.length(), true) > availableWidth)
current.pop_back();
}
}
}
if (!current.empty() && wrapped.size() < maxContentLines)
wrapped.push_back(current);
return wrapped;
};
std::vector<std::string> allLines;
if (hasTitle)
allLines.emplace_back(popupTitle);
char buf[sizeof(popupMessage)];
strncpy(buf, popupMessage, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *paragraph = strtok(buf, "\n");
while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) {
auto wrapped = wrapText(paragraph, maxWrapWidth);
for (const auto &ln : wrapped) {
if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0))
break;
allLines.push_back(ln);
}
paragraph = strtok(nullptr, "\n");
}
std::vector<const char *> ptrs;
for (const auto &ln : allLines)
ptrs.push_back(ln.c_str());
ptrs.push_back(nullptr);
// Use the standard notification box drawing from NotificationRenderer
NotificationRenderer::drawNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0);
}
} // namespace graphics } // namespace graphics
#endif // HAS_SCREEN #endif // HAS_SCREEN
-12
View File
@@ -25,11 +25,6 @@ class OnScreenKeyboardModule
static bool processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *keyboard); static bool processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *keyboard);
bool draw(OLEDDisplay *display); 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: private:
OnScreenKeyboardModule() = default; OnScreenKeyboardModule() = default;
~OnScreenKeyboardModule(); ~OnScreenKeyboardModule();
@@ -39,15 +34,8 @@ class OnScreenKeyboardModule
void onSubmit(const std::string &text); void onSubmit(const std::string &text);
void onCancel(); void onCancel();
void drawPopup(OLEDDisplay *display);
VirtualKeyboard *keyboard = nullptr; VirtualKeyboard *keyboard = nullptr;
std::function<void(const std::string &)> callback; std::function<void(const std::string &)> callback;
char popupTitle[64] = {0};
char popupMessage[256] = {0};
uint32_t popupUntil = 0;
bool popupVisible = false;
}; };
} // namespace graphics } // namespace graphics
+2 -1
View File
@@ -55,7 +55,8 @@ void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketI
// Allow the caller to set want_ack on this ACK packet if it's important that the ACK be delivered reliably // 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; 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) uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp)
@@ -10,11 +10,6 @@ float UnitConversions::MetersPerSecondToKnots(float metersPerSecond)
return metersPerSecond * 1.94384; return metersPerSecond * 1.94384;
} }
float UnitConversions::MetersPerSecondToMilesPerHour(float metersPerSecond)
{
return metersPerSecond * 2.23694;
}
float UnitConversions::HectoPascalToInchesOfMercury(float hectoPascal) float UnitConversions::HectoPascalToInchesOfMercury(float hectoPascal)
{ {
return hectoPascal * 0.029529983071445; return hectoPascal * 0.029529983071445;
-1
View File
@@ -7,7 +7,6 @@ class UnitConversions
public: public:
static float CelsiusToFahrenheit(float celsius); static float CelsiusToFahrenheit(float celsius);
static float MetersPerSecondToKnots(float metersPerSecond); static float MetersPerSecondToKnots(float metersPerSecond);
static float MetersPerSecondToMilesPerHour(float metersPerSecond);
static float HectoPascalToInchesOfMercury(float hectoPascal); static float HectoPascalToInchesOfMercury(float hectoPascal);
// Bound a float before Arduino String(float) renders it: its fixed char[33] + dtostrf overflow // Bound a float before Arduino String(float) renders it: its fixed char[33] + dtostrf overflow
+1 -24
View File
@@ -78,16 +78,6 @@ uint8_t sanitizePositionPrecision(uint8_t precision)
return 32; 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. * Return a short human-readable name for common port numbers.
* Falls back to "port:<N>" for unknown ports. * Falls back to "port:<N>" for unknown ports.
@@ -224,19 +214,6 @@ meshtastic_TrafficManagementStats TrafficManagementModule::getStats() const
return stats; 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) void TrafficManagementModule::incrementStat(uint32_t *field)
{ {
concurrency::LockGuard guard(&cacheLock); concurrency::LockGuard guard(&cacheLock);
@@ -757,7 +734,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
meshtastic_User requester = meshtastic_User_init_zero; meshtastic_User requester = meshtastic_User_init_zero;
if (!unauthenticatedSigner && if (!unauthenticatedSigner &&
pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &requester)) { 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"); logAction("respond", &mp, "nodeinfo-cache");
incrementStat(&stats.nodeinfo_cache_hits); incrementStat(&stats.nodeinfo_cache_hits);
-2
View File
@@ -38,8 +38,6 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
TrafficManagementModule &operator=(const TrafficManagementModule &) = delete; TrafficManagementModule &operator=(const TrafficManagementModule &) = delete;
meshtastic_TrafficManagementStats getStats() const; meshtastic_TrafficManagementStats getStats() const;
void resetStats();
void recordRouterHopPreserved();
// Next-hop overflow cache (routing hint). // Next-hop overflow cache (routing hint).
// setNextHop: store a confirmed last-byte next hop for `dest`. Called by // setNextHop: store a confirmed last-byte next hop for `dest`. Called by
-10
View File
@@ -444,16 +444,6 @@ bool BMI270Sensor::writeRegister(uint8_t reg, uint8_t value)
return wire->endTransmission() == 0; 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) uint8_t BMI270Sensor::readRegister(uint8_t reg)
{ {
wire->beginTransmission(deviceAddress()); wire->beginTransmission(deviceAddress());
-1
View File
@@ -18,7 +18,6 @@ class BMI270Sensor : public MotionSensor
// BMI270 register access // BMI270 register access
bool writeRegister(uint8_t reg, uint8_t value); 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); uint8_t readRegister(uint8_t reg);
bool readRegisters(uint8_t reg, uint8_t *data, size_t len); bool readRegisters(uint8_t reg, uint8_t *data, size_t len);
+2 -1
View File
@@ -122,7 +122,8 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
if (isFromUs(e.packet)) { if (isFromUs(e.packet)) {
auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index);
pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT;
router->sendLocal(pAck); if (router->sendLocal(pAck) == ERRNO_SHOULD_RELEASE)
packetPool.release(pAck);
} else { } else {
LOG_INFO("Ignore downlink message we originally sent"); LOG_INFO("Ignore downlink message we originally sent");
} }
+111
View File
@@ -0,0 +1,111 @@
// Weak software __atomic_*_{1,2,4} for ESP32-S2/S3. -mdisable-hardware-atomics makes
// GCC emit these libcalls, but the precompiled libnewlib ships only the _8 variants and
// some toolchains' libgcc ships none, so S3 links fail on macOS. Weak = a real libgcc
// definition wins with no clash. Spinlock/critical-section like IDF's stdatomic.c.
#include "sdkconfig.h"
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2)
#include <stdbool.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
// These names are GCC builtins; declaring them trips -Wbuiltin-declaration-mismatch
// (uint32_t vs GCC's unsigned int, same ABI). Suppressed as IDF's stdatomic.c does.
#pragma GCC diagnostic ignored "-Wbuiltin-declaration-mismatch"
static portMUX_TYPE s_swatomic_mux = portMUX_INITIALIZER_UNLOCKED;
#define SWATOMIC_ENTER() portENTER_CRITICAL_SAFE(&s_swatomic_mux)
#define SWATOMIC_EXIT() portEXIT_CRITICAL_SAFE(&s_swatomic_mux)
// load / store / exchange / compare_exchange for one width.
#define GEN_SWATOMIC_CORE(N, TYPE) \
__attribute__((weak, used)) TYPE __atomic_load_##N(const volatile void *ptr, int memorder) \
{ \
(void)memorder; \
SWATOMIC_ENTER(); \
TYPE ret = *(const volatile TYPE *)ptr; \
SWATOMIC_EXIT(); \
return ret; \
} \
__attribute__((weak, used)) void __atomic_store_##N(volatile void *ptr, TYPE val, int memorder) \
{ \
(void)memorder; \
SWATOMIC_ENTER(); \
*(volatile TYPE *)ptr = val; \
SWATOMIC_EXIT(); \
} \
__attribute__((weak, used)) TYPE __atomic_exchange_##N(volatile void *ptr, TYPE val, int memorder) \
{ \
(void)memorder; \
SWATOMIC_ENTER(); \
TYPE old = *(volatile TYPE *)ptr; \
*(volatile TYPE *)ptr = val; \
SWATOMIC_EXIT(); \
return old; \
} \
__attribute__((weak, used)) bool __atomic_compare_exchange_##N(volatile void *ptr, void *expected, TYPE desired, \
bool is_weak, int success, int failure) \
{ \
(void)is_weak; \
(void)success; \
(void)failure; \
bool ok; \
SWATOMIC_ENTER(); \
TYPE cur = *(volatile TYPE *)ptr; \
if (cur == *(TYPE *)expected) { \
*(volatile TYPE *)ptr = desired; \
ok = true; \
} else { \
*(TYPE *)expected = cur; \
ok = false; \
} \
SWATOMIC_EXIT(); \
return ok; \
}
// A read-modify-write pair: fetch_<name> returns the old value, <name>_fetch the
// new. EXPR is evaluated over `old` and `val`.
#define GEN_SWATOMIC_RMW(N, TYPE, NAME, EXPR) \
__attribute__((weak, used)) TYPE __atomic_fetch_##NAME##_##N(volatile void *ptr, TYPE val, int memorder) \
{ \
(void)memorder; \
SWATOMIC_ENTER(); \
TYPE old = *(volatile TYPE *)ptr; \
*(volatile TYPE *)ptr = (TYPE)(EXPR); \
SWATOMIC_EXIT(); \
return old; \
} \
__attribute__((weak, used)) TYPE __atomic_##NAME##_fetch_##N(volatile void *ptr, TYPE val, int memorder) \
{ \
(void)memorder; \
SWATOMIC_ENTER(); \
TYPE old = *(volatile TYPE *)ptr; \
TYPE nv = (TYPE)(EXPR); \
*(volatile TYPE *)ptr = nv; \
SWATOMIC_EXIT(); \
return nv; \
}
#define GEN_SWATOMIC_ALL(N, TYPE) \
GEN_SWATOMIC_CORE(N, TYPE) \
GEN_SWATOMIC_RMW(N, TYPE, add, old + val) \
GEN_SWATOMIC_RMW(N, TYPE, sub, old - val) \
GEN_SWATOMIC_RMW(N, TYPE, and, old &val) \
GEN_SWATOMIC_RMW(N, TYPE, or, old | val) \
GEN_SWATOMIC_RMW(N, TYPE, xor, old ^ val) \
GEN_SWATOMIC_RMW(N, TYPE, nand, ~(old & val))
GEN_SWATOMIC_ALL(1, uint8_t)
GEN_SWATOMIC_ALL(2, uint16_t)
GEN_SWATOMIC_ALL(4, uint32_t)
#else
// Keep this a non-empty translation unit on targets that inline hardware atomics.
typedef int swatomic_translation_unit_not_empty;
#endif // CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32S2
+43
View File
@@ -120,6 +120,44 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state)
return 0; 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() void portduinoCustomInit()
{ {
static struct argp_option options[] = {{"port", 'p', "PORT", 0, "The TCP port to use."}, 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 // 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}; uint8_t dmac[6] = {0};
if (portduino_config.lora_spi_dev == "ch341") { if (portduino_config.lora_spi_dev == "ch341") {
-10
View File
@@ -1078,16 +1078,6 @@ bool isSessionExpired()
return (millis() - s_sessionStartedMs) > s_sessionMaxMs; 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() uint8_t consumeSessionBoot()
{ {
if (s_bootsRemaining == 0) { if (s_bootsRemaining == 0) {
-4
View File
@@ -243,10 +243,6 @@ void setSession(uint32_t maxSeconds);
/// from the main loop on a low-frequency tick. /// from the main loop on a low-frequency tick.
bool isSessionExpired(); 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 /// 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 /// re-arm the session timer in place - no reboot. Called from the main
/// loop when a session expires AND there is still budget. Decrements /// loop when a session expires AND there is still budget. Decrements
+4 -1
View File
@@ -62,10 +62,13 @@ bool XModemAdapter::isValidFilename(const char *name)
{ {
if (!name || name[0] == '\0') if (!name || name[0] == '\0')
return false; 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 // Reject any ".." path component. Absolute paths and subdirectories are fine; they stay within
// the filesystem root, so only traversal out of it needs blocking. // the filesystem root, so only traversal out of it needs blocking.
for (const char *seg = name; *seg;) { 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); const size_t len = slash ? (size_t)(slash - seg) : strlen(seg);
if (len == 2 && seg[0] == '.' && seg[1] == '.') if (len == 2 && seg[0] == '.' && seg[1] == '.')
return false; return false;
+76
View File
@@ -1197,6 +1197,80 @@ static void test_handleSetConfig_security_acceptsSuppliedKeypair()
TEST_ASSERT_EQUAL_MEMORY(expectedPub, config.security.public_key.bytes, 32); 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() static void test_regionInfo_supportsPreset()
{ {
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); 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_fromLocal_customBandwidthNonZeroPreserved);
RUN_TEST(test_handleSetConfig_security_preservesKeypairWhenPrivateOmitted); RUN_TEST(test_handleSetConfig_security_preservesKeypairWhenPrivateOmitted);
RUN_TEST(test_handleSetConfig_security_acceptsSuppliedKeypair); 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_regionInfo_supportsPreset);
RUN_TEST(test_checkConfigRegion_quietCheckReportsReason); RUN_TEST(test_checkConfigRegion_quietCheckReportsReason);
RUN_TEST(test_handleSetConfig_fromOthers_siblingLockedPresetSwapsRegion); RUN_TEST(test_handleSetConfig_fromOthers_siblingLockedPresetSwapsRegion);
+94
View File
@@ -15,6 +15,7 @@
#include "gps/RTC.h" #include "gps/RTC.h"
#include "mesh/NextHopRouter.h" #include "mesh/NextHopRouter.h"
#include "mesh/NodeDB.h" #include "mesh/NodeDB.h"
#include "mesh/RadioInterface.h"
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
@@ -87,6 +88,7 @@ class NextHopRouterTestShim : public NextHopRouter
using NextHopRouter::noteRouteFailure; using NextHopRouter::noteRouteFailure;
using NextHopRouter::noteRouteLearned; using NextHopRouter::noteRouteLearned;
using NextHopRouter::noteRouteSuccess; using NextHopRouter::noteRouteSuccess;
using NextHopRouter::perhapsRebroadcast;
using Router::shouldDecrementHopLimit; // protected in Router using Router::shouldDecrementHopLimit; // protected in Router
void resetRouteHealthForTest() 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 MockNodeDB *mockNodeDB = nullptr;
static NextHopRouterTestShim *shim = 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 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<RadioInterface>(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() void setup()
@@ -459,6 +548,11 @@ void setup()
RUN_TEST(test_hoplimit_decrement_on_colliding_favorites); RUN_TEST(test_hoplimit_decrement_on_colliding_favorites);
RUN_TEST(test_hoplimit_decrement_when_resolved_not_favorite); 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()); exit(UNITY_END());
} }
+282 -1
View File
@@ -27,6 +27,7 @@
#include "mesh/ReliableRouter.h" #include "mesh/ReliableRouter.h"
#include "mesh/Router.h" #include "mesh/Router.h"
#include "mesh/SinglePortModule.h" #include "mesh/SinglePortModule.h"
#include "modules/NodeInfoModule.h"
#include "modules/RoutingModule.h" #include "modules/RoutingModule.h"
#include "mqtt/MQTT.h" #include "mqtt/MQTT.h"
#include <ErriezCRC32.h> #include <ErriezCRC32.h>
@@ -87,6 +88,21 @@ class MockNodeDB : public NodeDB
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, value); 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<meshtastic_NodeInfoLite> testNodes; std::vector<meshtastic_NodeInfoLite> 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"); "Balanced downgrade memory must survive repeated hot-store eviction");
} }
#endif #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) // 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) static void preparePipelineSigner(NodeNum sender)
{ {
uint8_t pub[32], priv[32]; 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"); 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 // 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"); 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() void setup()
{ {
initializeTestEnvironment(); initializeTestEnvironment();
@@ -1371,6 +1638,8 @@ void setup()
#if WARM_NODE_COUNT > 0 #if WARM_NODE_COUNT > 0
RUN_TEST(test_A17_strict_verifies_signer_from_warm_key_store); RUN_TEST(test_A17_strict_verifies_signer_from_warm_key_store);
#endif #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"); printf("\n=== Group B: send-side signing policy ===\n");
RUN_TEST(test_B1_local_broadcast_is_signed); 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_C10_legacy_channel_dm_failure_has_no_pipeline_effects);
RUN_TEST(test_C11_malformed_pki_plaintext_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); 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"); printf("\n=== Group D: encoding invariants ===\n");
RUN_TEST(test_D1_signature_field_overhead_exact); 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_E6_decoded_unsigned_unicast_from_signer_accepted);
RUN_TEST(test_E8_decoded_partial_signature_from_signer_dropped); RUN_TEST(test_E8_decoded_partial_signature_from_signer_dropped);
RUN_TEST(test_E9_decoded_partial_signature_from_nonsigner_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()); exit(UNITY_END());
} }
+21
View File
@@ -21,6 +21,22 @@ void test_xmodem_rejects_dotdot_traversal(void)
TEST_ASSERT_FALSE(XModemAdapter::isValidFilename("/..")); 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) void test_xmodem_rejects_empty(void)
{ {
TEST_ASSERT_FALSE(XModemAdapter::isValidFilename("")); 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. // ".." 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("my..file"));
TEST_ASSERT_TRUE(XModemAdapter::isValidFilename("...")); 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 #endif // FSCom
@@ -46,6 +65,8 @@ void setup()
UNITY_BEGIN(); UNITY_BEGIN();
#ifdef FSCom #ifdef FSCom
RUN_TEST(test_xmodem_rejects_dotdot_traversal); 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_rejects_empty);
RUN_TEST(test_xmodem_allows_legit_paths); RUN_TEST(test_xmodem_allows_legit_paths);
#endif #endif
@@ -28,6 +28,14 @@ lib_deps =
https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip
https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.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] [env:t5s3_epaper_inkhud]
extends = t5s3_epaper_base, inkhud extends = t5s3_epaper_base, inkhud
board_level = extra board_level = extra