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