Merge upstream develop into packet authentication policy
This commit is contained in:
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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[] = {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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 ¤t)
|
||||||
|
{
|
||||||
|
if (incoming.private_key.size != 32)
|
||||||
|
return false;
|
||||||
|
if (current.private_key.size == 32 && memcmp(incoming.private_key.bytes, current.private_key.bytes, 32) == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return incoming.admin_key_count == 0 && !incoming.is_managed && !incoming.serial_enabled && !incoming.debug_log_api_enabled &&
|
||||||
|
!incoming.admin_channel_enabled &&
|
||||||
|
incoming.packet_signature_policy ==
|
||||||
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_COMPATIBLE;
|
||||||
|
}
|
||||||
|
|
||||||
void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
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) {
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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 ===
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
Reference in New Issue
Block a user