From 950341d1f980369b58cc3d63dac6b036effc0270 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 18 Jan 2025 08:15:06 -0600 Subject: [PATCH 01/71] Alert app messages should be treated as text (#5878) --- src/mesh/MeshService.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h index 175d8a595..42f701d5c 100644 --- a/src/mesh/MeshService.h +++ b/src/mesh/MeshService.h @@ -64,7 +64,8 @@ class MeshService return true; } return p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || - p->decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP; + p->decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP || + p->decoded.portnum == meshtastic_PortNum_ALERT_APP; } /// Called when some new packets have arrived from one of the radios Observable fromNumChanged; From 973b453d43ba5d9d4239dabff56cab676e578e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 20 Jan 2025 09:34:54 +0100 Subject: [PATCH 02/71] Update RAK2560 code (#5844) * * Update RAK9154 sensor to tx remote power telemetry * remove uf2 script, pio run does that inline * move sensor module to correct position * disable LED and Accelerometer code on rak2560 * trunk fmt * mention epaper variant * attention, revert, revert * Enable Environment Telemetry of these values * fix float values --- src/Power.cpp | 10 +- .../Telemetry/EnvironmentTelemetry.cpp | 10 ++ src/modules/Telemetry/PowerTelemetry.cpp | 16 +-- .../Telemetry/Sensor}/RAK9154Sensor.cpp | 48 ++++++-- .../modules/Telemetry/Sensor}/RAK9154Sensor.h | 15 ++- src/motion/BMX160Sensor.cpp | 2 +- src/motion/BMX160Sensor.h | 2 +- src/power.h | 5 +- variants/rak2560/create_uf2.py | 113 ------------------ variants/rak2560/platformio.ini | 2 - 10 files changed, 76 insertions(+), 147 deletions(-) rename {variants/rak2560 => src/modules/Telemetry/Sensor}/RAK9154Sensor.cpp (76%) rename {variants/rak2560 => src/modules/Telemetry/Sensor}/RAK9154Sensor.h (55%) delete mode 100644 variants/rak2560/create_uf2.py diff --git a/src/Power.cpp b/src/Power.cpp index ae0908ec6..8d5fe1c32 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -87,7 +87,7 @@ MAX17048Sensor max17048Sensor; #endif #endif -#if HAS_RAKPROT && !defined(ARCH_PORTDUINO) +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT && !defined(ARCH_PORTDUINO) RAK9154Sensor rak9154Sensor; #endif @@ -243,7 +243,8 @@ class AnalogBatteryLevel : public HasBatteryLevel virtual uint16_t getBattVoltage() override { -#if defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && !defined(HAS_PMU) +#if HAS_TELEMETRY && defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && !defined(HAS_PMU) && \ + !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (hasRAK()) { return getRAKVoltage(); } @@ -406,7 +407,8 @@ class AnalogBatteryLevel : public HasBatteryLevel /// we can't be smart enough to say 'full'? virtual bool isCharging() override { -#if defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && !defined(HAS_PMU) +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && \ + !defined(HAS_PMU) if (hasRAK()) { return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; } @@ -447,7 +449,7 @@ class AnalogBatteryLevel : public HasBatteryLevel float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS); uint32_t last_read_time_ms = 0; -#if defined(HAS_RAKPROT) +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) uint16_t getRAKVoltage() { return rak9154Sensor.getBusVoltageMv(); } diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 1af6347f2..6a5e8376d 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -163,6 +163,12 @@ int32_t EnvironmentTelemetryModule::runOnce() result = max17048Sensor.runOnce(); if (cgRadSens.hasSensor()) result = cgRadSens.runOnce(); + // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the + // sensormap here. +#ifdef HAS_RAKPROT + + result = rak9154Sensor.runOnce(); +#endif #endif } return result; @@ -480,6 +486,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m valid = valid && cgRadSens.getMetrics(m); hasSensor = true; } +#ifdef HAS_RAKPROT + valid = valid && rak9154Sensor.getMetrics(m); + hasSensor = true; +#endif #endif return valid && hasSensor; } diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 9c794e31e..38a5c6f11 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -100,7 +100,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s { display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - + if (lastMeasurementPacket == nullptr) { // In case of no valid packet, display "Power Telemetry", "No measurement" display->drawString(x, y, "Power Telemetry"); @@ -121,23 +121,23 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s } // Display "Pow. From: ..." - display->drawString(x, y, "Pow. From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); + display->drawString(x, y, "Pow. From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags if (lastMeasurement.variant.power_metrics.has_ch1_voltage || lastMeasurement.variant.power_metrics.has_ch1_current) { display->drawString(x, y += _fontHeight(FONT_SMALL), - "Ch1: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) + - "V " + String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA"); + "Ch1: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) + "V " + + String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA"); } if (lastMeasurement.variant.power_metrics.has_ch2_voltage || lastMeasurement.variant.power_metrics.has_ch2_current) { display->drawString(x, y += _fontHeight(FONT_SMALL), - "Ch2: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) + - "V " + String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA"); + "Ch2: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) + "V " + + String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA"); } if (lastMeasurement.variant.power_metrics.has_ch3_voltage || lastMeasurement.variant.power_metrics.has_ch3_current) { display->drawString(x, y += _fontHeight(FONT_SMALL), - "Ch3: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) + - "V " + String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA"); + "Ch3: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) + "V " + + String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA"); } } diff --git a/variants/rak2560/RAK9154Sensor.cpp b/src/modules/Telemetry/Sensor/RAK9154Sensor.cpp similarity index 76% rename from variants/rak2560/RAK9154Sensor.cpp rename to src/modules/Telemetry/Sensor/RAK9154Sensor.cpp index 43affe581..ad3925f08 100644 --- a/variants/rak2560/RAK9154Sensor.cpp +++ b/src/modules/Telemetry/Sensor/RAK9154Sensor.cpp @@ -1,9 +1,10 @@ -#ifdef HAS_RAKPROT -#include "../variants/rak2560/RAK9154Sensor.h" -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "../modules/Telemetry/Sensor/TelemetrySensor.h" #include "configuration.h" +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RAK9154Sensor.h" +#include "TelemetrySensor.h" #include "concurrency/Periodic.h" #include @@ -25,6 +26,8 @@ static uint16_t dc_vol = 0; static uint8_t dc_prec = 0; static uint8_t provision = 0; +extern RAK9154Sensor rak9154Sensor; + static void onewire_evt(const uint8_t pid, const uint8_t sid, const SNHUBAPI_EVT_E eid, uint8_t *msg, uint16_t len) { switch (eid) { @@ -78,6 +81,7 @@ static void onewire_evt(const uint8_t pid, const uint8_t sid, const SNHUBAPI_EVT default: break; } + rak9154Sensor.setLastRead(millis()); break; case SNHUBAPI_EVT_REPORT: @@ -106,6 +110,7 @@ static void onewire_evt(const uint8_t pid, const uint8_t sid, const SNHUBAPI_EVT default: break; } + rak9154Sensor.setLastRead(millis()); break; @@ -145,15 +150,18 @@ static int32_t onewireHandle() int32_t RAK9154Sensor::runOnce() { - onewirePeriodic = new Periodic("onewireHandle", onewireHandle); + if (!rak9154Sensor.isInitialized()) { + onewirePeriodic = new Periodic("onewireHandle", onewireHandle); - mySerial.begin(9600); + mySerial.begin(9600); - RakSNHub_Protocl_API.init(onewire_evt); + RakSNHub_Protocl_API.init(onewire_evt); - status = true; - initialized = true; - return 0; + status = true; + initialized = true; + } + + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } void RAK9154Sensor::setup() @@ -163,7 +171,16 @@ void RAK9154Sensor::setup() bool RAK9154Sensor::getMetrics(meshtastic_Telemetry *measurement) { - return true; + if (getBusVoltageMv() > 0) { + measurement->variant.environment_metrics.has_voltage = true; + measurement->variant.environment_metrics.has_current = true; + + measurement->variant.environment_metrics.voltage = (float)getBusVoltageMv() / 1000; + measurement->variant.environment_metrics.current = (float)getCurrentMa() / 1000; + return true; + } else { + return false; + } } uint16_t RAK9154Sensor::getBusVoltageMv() @@ -171,6 +188,11 @@ uint16_t RAK9154Sensor::getBusVoltageMv() return dc_vol; } +int16_t RAK9154Sensor::getCurrentMa() +{ + return dc_cur; +} + int RAK9154Sensor::getBusBatteryPercent() { return (int)dc_prec; @@ -180,4 +202,8 @@ bool RAK9154Sensor::isCharging() { return (dc_cur > 0) ? true : false; } +void RAK9154Sensor::setLastRead(uint32_t lastRead) +{ + this->lastRead = lastRead; +} #endif // HAS_RAKPROT diff --git a/variants/rak2560/RAK9154Sensor.h b/src/modules/Telemetry/Sensor/RAK9154Sensor.h similarity index 55% rename from variants/rak2560/RAK9154Sensor.h rename to src/modules/Telemetry/Sensor/RAK9154Sensor.h index 6c6f304d6..c96139f9c 100644 --- a/variants/rak2560/RAK9154Sensor.h +++ b/src/modules/Telemetry/Sensor/RAK9154Sensor.h @@ -1,23 +1,30 @@ -#ifdef HAS_RAKPROT +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) + #ifndef _RAK9154SENSOR_H #define _RAK9154SENSOR_H 1 #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "../modules/Telemetry/Sensor/TelemetrySensor.h" -#include "../modules/Telemetry/Sensor/VoltageSensor.h" +#include "CurrentSensor.h" +#include "TelemetrySensor.h" +#include "VoltageSensor.h" -class RAK9154Sensor : public TelemetrySensor, VoltageSensor +class RAK9154Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { private: protected: virtual void setup() override; + uint32_t lastRead = 0; public: RAK9154Sensor(); virtual int32_t runOnce() override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual uint16_t getBusVoltageMv() override; + virtual int16_t getCurrentMa() override; int getBusBatteryPercent(); bool isCharging(); + void setLastRead(uint32_t lastRead); }; #endif // _RAK9154SENSOR_H #endif // HAS_RAKPROT \ No newline at end of file diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp index 06cea3229..3ddbe46ea 100755 --- a/src/motion/BMX160Sensor.cpp +++ b/src/motion/BMX160Sensor.cpp @@ -4,7 +4,7 @@ BMX160Sensor::BMX160Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -#ifdef RAK_4631 +#if defined(RAK_4631) && !defined(RAK2560) #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // screen is defined in main.cpp diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h index 9031b4504..fc5a48aa4 100755 --- a/src/motion/BMX160Sensor.h +++ b/src/motion/BMX160Sensor.h @@ -7,7 +7,7 @@ #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C -#ifdef RAK_4631 +#if defined(RAK_4631) && !defined(RAK2560) #include "Fusion/Fusion.h" #include diff --git a/src/power.h b/src/power.h index ab55fc7e1..176e16ee5 100644 --- a/src/power.h +++ b/src/power.h @@ -1,5 +1,4 @@ #pragma once -#include "../variants/rak2560/RAK9154Sensor.h" #include "PowerStatus.h" #include "concurrency/OSThread.h" #include "configuration.h" @@ -56,8 +55,8 @@ extern INA3221Sensor ina3221Sensor; extern MAX17048Sensor max17048Sensor; #endif -#if HAS_RAKPROT && !defined(ARCH_PORTDUINO) -#include "../variants/rak2560/RAK9154Sensor.h" +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT && !defined(ARCH_PORTDUINO) +#include "modules/Telemetry/Sensor/RAK9154Sensor.h" extern RAK9154Sensor rak9154Sensor; #endif diff --git a/variants/rak2560/create_uf2.py b/variants/rak2560/create_uf2.py deleted file mode 100644 index af78f3e09..000000000 --- a/variants/rak2560/create_uf2.py +++ /dev/null @@ -1,113 +0,0 @@ -import struct - -Import("env") # noqa: F821 - - -# Parse input and create UF2 file -def create_uf2(source, target, env): - # source_hex = target[0].get_abspath() - source_hex = target[0].get_string(False) - source_hex = ".\\" + source_hex - print("#########################################################") - print("Create UF2 from " + source_hex) - print("#########################################################") - # print("Source: " + source_hex) - target = source_hex.replace(".hex", "") - target = target + ".uf2" - # print("Target: " + target) - - with open(source_hex, mode="rb") as f: - inpbuf = f.read() - - outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8")) - - write_file(target, outbuf) - print("#########################################################") - print(target + " is ready to flash to target device") - print("#########################################################") - - -# Add callback after .hex file was created -env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", create_uf2) # noqa: F821 - -# UF2 creation taken from uf2conv.py -UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" -UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected -UF2_MAGIC_END = 0x0AB16F30 # Ditto - -familyid = 0xADA52840 - - -class Block: - def __init__(self, addr): - self.addr = addr - self.bytes = bytearray(256) - - def encode(self, blockno, numblocks): - global familyid - flags = 0x0 - if familyid: - flags |= 0x2000 - hd = struct.pack( - " Date: Mon, 20 Jan 2025 13:20:59 +0200 Subject: [PATCH 03/71] Create BananaPi-BPI-R4-sx1262.yaml (#5897) --- bin/config.d/BananaPi-BPI-R4-sx1262.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 bin/config.d/BananaPi-BPI-R4-sx1262.yaml diff --git a/bin/config.d/BananaPi-BPI-R4-sx1262.yaml b/bin/config.d/BananaPi-BPI-R4-sx1262.yaml new file mode 100644 index 000000000..825ab2699 --- /dev/null +++ b/bin/config.d/BananaPi-BPI-R4-sx1262.yaml @@ -0,0 +1,9 @@ +Lora: + Module: sx1262 # BananaPi-BPI-R4 SPI via 26p GPIO Header +## CS: 28 + IRQ: 50 + Busy: 62 + Reset: 51 + spidev: spidev1.0 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true From 0f981153ebbad06675f04bd9b0dd6170c7f08352 Mon Sep 17 00:00:00 2001 From: isseysandei Date: Mon, 20 Jan 2025 17:47:47 +0100 Subject: [PATCH 04/71] No focus on new messages if auto-carousel is off (#5881) * no focus on messages if screen carousel is disabled * trunk + comment * compacted the nested if using ternary operator * trunk --- src/graphics/Screen.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 198dcc235..b7253ca17 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2662,14 +2662,13 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg) int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) { - if (showingNormalScreen) { - // Outgoing message - if (packet->from == 0) - setFrames(FOCUS_PRESERVE); // Return to same frame (quietly hiding the rx text message frame) + // If auto carousel is disabled -> return 0 and skip new messages handling + if (config.display.auto_screen_carousel_secs == 0) + return 0; - // Incoming message - else - setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message + // Handle focus change based on message type + if (showingNormalScreen) { + setFrames(packet->from == 0 ? FOCUS_PRESERVE : FOCUS_TEXTMESSAGE); } return 0; @@ -2756,4 +2755,4 @@ int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg) } // namespace graphics #else graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} -#endif // HAS_SCREEN +#endif // HAS_SCREEN \ No newline at end of file From c4fcbad3723d75a98a28501c3354cae5a424e20b Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 20 Jan 2025 09:43:35 -0800 Subject: [PATCH 05/71] Reboot before formatting LittleFS (#5900) Co-authored-by: Ben Meadors --- src/FSCommon.cpp | 29 +++++------------ src/FSCommon.h | 5 +-- src/SafeFile.cpp | 6 +--- src/platform/nrf52/main-nrf52.cpp | 52 ++++++++++++++++++++++++++++++- 4 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index 1f2994b29..461c72c26 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -49,24 +49,6 @@ void OSFS::writeNBytes(uint16_t address, unsigned int num, const byte *input) } #endif -bool lfs_assert_failed = - false; // Note: we use this global on all platforms, though it can only be set true on nrf52 (in our modified lfs_util.h) - -extern "C" void lfs_assert(const char *reason) -{ - LOG_ERROR("LFS assert: %s", reason); - lfs_assert_failed = true; - -#ifndef ARCH_PORTDUINO -#ifdef FSCom - // CORRUPTED FILESYSTEM. This causes bootloop so - // might as well try formatting now. - LOG_ERROR("Trying FSCom.format()"); - FSCom.format(); -#endif -#endif -} - /** * @brief Copies a file from one location to another. * @@ -348,10 +330,16 @@ void rmDir(const char *dirname) #endif } +/** + * Some platforms (nrf52) might need to do an extra step before FSBegin(). + */ +__attribute__((weak, noinline)) void preFSBegin() {} + void fsInit() { #ifdef FSCom - spiLock->lock(); + concurrency::LockGuard g(spiLock); + preFSBegin(); if (!FSBegin()) { LOG_ERROR("Filesystem mount failed"); // assert(0); This auto-formats the partition, so no need to fail here. @@ -362,7 +350,6 @@ void fsInit() LOG_DEBUG("Filesystem files:"); #endif listDir("/", 10); - spiLock->unlock(); #endif } @@ -400,4 +387,4 @@ void setupSDCard() LOG_DEBUG("Total space: %lu MB", (uint32_t)(SD.totalBytes() / (1024 * 1024))); LOG_DEBUG("Used space: %lu MB", (uint32_t)(SD.usedBytes() / (1024 * 1024))); #endif -} +} \ No newline at end of file diff --git a/src/FSCommon.h b/src/FSCommon.h index 254245b29..10ce4aeec 100644 --- a/src/FSCommon.h +++ b/src/FSCommon.h @@ -57,7 +57,4 @@ bool renameFile(const char *pathFrom, const char *pathTo); std::vector getFiles(const char *dirname, uint8_t levels); void listDir(const char *dirname, uint8_t levels, bool del = false); void rmDir(const char *dirname); -void setupSDCard(); - -extern bool lfs_assert_failed; // Note: we use this global on all platforms, though it can only be set true on nrf52 (in our - // modified lfs_util.h) +void setupSDCard(); \ No newline at end of file diff --git a/src/SafeFile.cpp b/src/SafeFile.cpp index f874164ae..94232e81d 100644 --- a/src/SafeFile.cpp +++ b/src/SafeFile.cpp @@ -8,7 +8,6 @@ static File openFile(const char *filename, bool fullAtomic) concurrency::LockGuard g(spiLock); LOG_DEBUG("Opening %s, fullAtomic=%d", filename, fullAtomic); #ifdef ARCH_NRF52 - lfs_assert_failed = false; File file = FSCom.open(filename, FILE_O_WRITE); file.seek(0); return file; @@ -20,7 +19,6 @@ static File openFile(const char *filename, bool fullAtomic) filenameTmp += ".tmp"; // clear any previous LFS errors - lfs_assert_failed = false; return FSCom.open(filenameTmp.c_str(), FILE_O_WRITE); } @@ -96,8 +94,6 @@ bool SafeFile::close() bool SafeFile::testReadback() { concurrency::LockGuard g(spiLock); - bool lfs_failed = lfs_assert_failed; - lfs_assert_failed = false; String filenameTmp = filename; filenameTmp += ".tmp"; @@ -119,7 +115,7 @@ bool SafeFile::testReadback() return false; } - return !lfs_failed; + return true; } #endif \ No newline at end of file diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 7ca047654..ad4d7a881 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -1,6 +1,7 @@ #include "configuration.h" #include #include +#include #include #include #include @@ -130,6 +131,54 @@ int printf(const char *fmt, ...) return res; } +namespace +{ +constexpr uint8_t NRF52_MAGIC_LFS_IS_CORRUPT = 0xF5; +constexpr uint32_t MULTIPLE_CORRUPTION_DELAY_MILLIS = 20 * 60 * 1000; +static unsigned long millis_until_formatting_again = 0; + +// Report the critical error from loop(), giving a chance for the screen to be initialized first. +inline void reportLittleFSCorruptionOnce() +{ + static bool report_corruption = !!millis_until_formatting_again; + if (report_corruption) { + report_corruption = false; + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); + } +} +} // namespace + +void preFSBegin() +{ + // The GPREGRET register keeps its value across warm boots. Check that this is a warm boot and, if GPREGRET + // is set to NRF52_MAGIC_LFS_IS_CORRUPT, format LittleFS. + if (!(NRF_POWER->RESETREAS == 0 && NRF_POWER->GPREGRET == NRF52_MAGIC_LFS_IS_CORRUPT)) + return; + NRF_POWER->GPREGRET = 0; + millis_until_formatting_again = millis() + MULTIPLE_CORRUPTION_DELAY_MILLIS; + InternalFS.format(); + LOG_INFO("LittleFS format complete; restoring default settings"); +} + +extern "C" void lfs_assert(const char *reason) +{ + LOG_ERROR("LittleFS corruption detected: %s", reason); + if (millis_until_formatting_again > millis()) { + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); + const long millis_remain = millis_until_formatting_again - millis(); + LOG_WARN("Pausing %d seconds to avoid wear on flash storage", millis_remain / 1000); + delay(millis_remain); + } + LOG_INFO("Rebooting to format LittleFS"); + delay(500); // Give the serial port a bit of time to output that last message. + // Try setting GPREGRET with the SoftDevice first. If that fails (perhaps because the SD hasn't been initialize yet) then set + // NRF_POWER->GPREGRET directly. + if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { + NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; + } + NVIC_SystemReset(); +} + void checkSDEvents() { if (useSoftDevice) { @@ -154,6 +203,7 @@ void checkSDEvents() void nrf52Loop() { checkSDEvents(); + reportLittleFSCorruptionOnce(); } #ifdef USE_SEMIHOSTING @@ -309,4 +359,4 @@ void enterDfuMode() #else enterUf2Dfu(); #endif -} +} \ No newline at end of file From f87c37012386cc0eab10a100cf161be5dd5f5613 Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Tue, 21 Jan 2025 18:11:37 +0100 Subject: [PATCH 06/71] Fix possible memory leak for `ROUTER_LATE` (#5901) --- src/mesh/RadioLibInterface.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index e31f0b3e2..69809b7a4 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -340,8 +340,11 @@ void RadioLibInterface::clampToLateRebroadcastWindow(NodeNum from, PacketId id) meshtastic_MeshPacket *p = txQueue.remove(from, id, true, false); if (p) { p->tx_after = millis() + getTxDelayMsecWeightedWorst(p->rx_snr); - txQueue.enqueue(p); - LOG_DEBUG("Move existing queued packet to the late rebroadcast window %dms from now", p->tx_after - millis()); + if (txQueue.enqueue(p)) { + LOG_DEBUG("Move existing queued packet to the late rebroadcast window %dms from now", p->tx_after - millis()); + } else { + packetPool.release(p); + } } } From 9041af365de649e6c782c6ebedd19a61ea9fb2b6 Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 21 Jan 2025 17:18:40 -0500 Subject: [PATCH 07/71] Move OpenWRT configs to subdir (#5902) --- bin/config.d/{ => OpenWRT}/BananaPi-BPI-R4-sx1262.yaml | 0 bin/config.d/{ => OpenWRT}/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml | 0 bin/config.d/{ => OpenWRT}/OpenWRT_One_mikroBUS_sx1262.yaml | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename bin/config.d/{ => OpenWRT}/BananaPi-BPI-R4-sx1262.yaml (100%) rename bin/config.d/{ => OpenWRT}/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml (100%) rename bin/config.d/{ => OpenWRT}/OpenWRT_One_mikroBUS_sx1262.yaml (100%) diff --git a/bin/config.d/BananaPi-BPI-R4-sx1262.yaml b/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml similarity index 100% rename from bin/config.d/BananaPi-BPI-R4-sx1262.yaml rename to bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml diff --git a/bin/config.d/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml b/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml similarity index 100% rename from bin/config.d/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml rename to bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml diff --git a/bin/config.d/OpenWRT_One_mikroBUS_sx1262.yaml b/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml similarity index 100% rename from bin/config.d/OpenWRT_One_mikroBUS_sx1262.yaml rename to bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml From 71591fb06a5d4018bd6ed3ab4c9673c2a03287ac Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 21 Jan 2025 19:53:32 -0500 Subject: [PATCH 08/71] Build docker images with other linux (#5837) --- .github/workflows/build_docker.yml | 51 -------- .github/workflows/daily_packaging.yml | 6 + .github/workflows/docker_build.yml | 70 +++++++++++ .github/workflows/docker_manifest.yml | 167 +++++++++++++++++++++++++ .github/workflows/main_matrix.yml | 35 +++++- .github/workflows/release_channels.yml | 7 ++ 6 files changed, 281 insertions(+), 55 deletions(-) delete mode 100644 .github/workflows/build_docker.yml create mode 100644 .github/workflows/docker_build.yml create mode 100644 .github/workflows/docker_manifest.yml diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml deleted file mode 100644 index 18787f16a..000000000 --- a/.github/workflows/build_docker.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Build Docker - -on: workflow_call - -permissions: - contents: write - packages: write - -jobs: - build-native: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Get release version string - run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT - id: version - - - name: Docker login - if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }} - uses: docker/login-action@v3 - with: - username: meshtastic - password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }} - - - name: Docker setup - if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }} - uses: docker/setup-buildx-action@v3 - - - name: Docker build and push tagged versions - if: ${{ github.event_name == 'workflow_dispatch' }} - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - push: true - tags: meshtastic/meshtasticd:${{ steps.version.outputs.long }} - - - name: Docker build and push - if: ${{ github.ref == 'refs/heads/master' && github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }} - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - push: true - tags: meshtastic/meshtasticd:latest diff --git a/.github/workflows/daily_packaging.yml b/.github/workflows/daily_packaging.yml index 14daae74d..cb8f866c6 100644 --- a/.github/workflows/daily_packaging.yml +++ b/.github/workflows/daily_packaging.yml @@ -20,6 +20,12 @@ permissions: packages: write jobs: + docker-multiarch: + uses: ./.github/workflows/docker_manifest.yml + with: + release_channel: daily + secrets: inherit + package-ppa: strategy: fail-fast: false diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml new file mode 100644 index 000000000..83c67bb32 --- /dev/null +++ b/.github/workflows/docker_build.yml @@ -0,0 +1,70 @@ +name: Build Docker + +# Build Docker image, push untagged (digest-only) + +on: + workflow_call: + inputs: + distro: + description: Distro to target + required: true + type: string + # choices: [debian, alpine] + platform: + description: Platform to target + required: true + type: string + runs-on: + description: Runner to use + required: true + type: string + push: + description: Push images to registry + required: false + type: boolean + default: false + outputs: + digest: + description: Digest of built image + value: ${{ jobs.docker-build.outputs.digest }} + +permissions: + contents: write + packages: write + +jobs: + docker-build: + outputs: + digest: ${{ steps.docker_variant.outputs.digest }} + runs-on: ${{ inputs.runs-on }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: Docker login + if: ${{ inputs.push }} + uses: docker/login-action@v3 + with: + username: meshtastic + password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Docker setup + uses: docker/setup-buildx-action@v3 + + - name: Docker build and push + uses: docker/build-push-action@v6 + id: docker_variant + with: + context: . + file: | + ${{ contains(inputs.distro, 'debian') && './Dockerfile' || contains(inputs.distro, 'alpine') && './alpine.Dockerfile' }} + push: ${{ inputs.push }} + tags: "" # Intentionally empty, push with digest only + platforms: ${{ inputs.platform }} diff --git a/.github/workflows/docker_manifest.yml b/.github/workflows/docker_manifest.yml new file mode 100644 index 000000000..30dcfb067 --- /dev/null +++ b/.github/workflows/docker_manifest.yml @@ -0,0 +1,167 @@ +name: Build Docker Multi-Arch Manifest + +on: + workflow_call: + inputs: + release_channel: + description: Release channel to target + required: true + type: string + +permissions: + contents: write + packages: write + +jobs: + docker-debian-amd64: + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: true + + docker-debian-arm64: + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm64 + runs-on: ubuntu-24.04-arm + push: true + + docker-debian-armv7: + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm/v7 + runs-on: ubuntu-24.04-arm + push: true + + docker-alpine-amd64: + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: true + + docker-alpine-arm64: + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/arm64 + runs-on: ubuntu-24.04-arm + push: true + + docker-alpine-armv7: + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/arm/v7 + runs-on: ubuntu-24.04-arm + push: true + + docker-manifest: + needs: + # Debian + - docker-debian-amd64 + - docker-debian-arm64 + - docker-debian-armv7 + # Alpine + - docker-alpine-amd64 + - docker-alpine-arm64 + - docker-alpine-armv7 + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: Get release version string + run: | + echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + echo "short=$(./bin/buildinfo.py short)" >> $GITHUB_OUTPUT + id: version + + - name: Enumerate tags + shell: python + run: | + import os + + short = "${{ steps.version.outputs.short }}" + long = "${{ steps.version.outputs.long }}" + release_channel = "${{ inputs.release_channel }}" + tags = { + "beta": { + "debian": [ + f"{short}", f"{long}", f"{short}-beta", f"{long}-beta", "beta", "latest", + f"{short}-debian", f"{long}-debian", f"{short}-beta-debian", f"{long}-beta-debian", "beta-debian" + ], + "alpine": [ + f"{short}-alpine", f"{long}-alpine", f"{short}-beta-alpine", f"{long}-beta-alpine", "beta-alpine" + ] + }, + "alpha": { + "debian": [ + f"{short}-alpha", f"{long}-alpha", "alpha", + f"{short}-alpha-debian", f"{long}-alpha-debian", "alpha-debian" + ], + "alpine": [ + f"{short}-alpha-alpine", f"{long}-alpha-alpine", "alpha-alpine" + ] + }, + "daily": { + "debian": ["daily", "daily-debian"], + "alpine": ["daily-alpine"] + } + } + + with open(os.environ['GITHUB_OUTPUT'], 'a') as fh: + fh.write(f"debian={','.join(tags[release_channel]['debian'])}\n") + fh.write(f"alpine={','.join(tags[release_channel]['alpine'])}\n") + id: tags + + - name: Docker login + uses: docker/login-action@v3 + with: + username: meshtastic + password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }} + + - name: Docker meta (Debian) + id: meta_debian + uses: docker/metadata-action@v5 + with: + images: meshtastic/meshtasticd + tags: ${{ steps.tags.outputs.debian }} + + - name: Create Docker manifest (Debian) + id: manifest_debian + uses: int128/docker-manifest-create-action@v2 + with: + tags: ${{ steps.meta_debian.outputs.tags }} + push: true + sources: | + meshtastic/meshtasticd@${{ needs.docker-debian-amd64.outputs.digest }} + meshtastic/meshtasticd@${{ needs.docker-debian-arm64.outputs.digest }} + meshtastic/meshtasticd@${{ needs.docker-debian-armv7.outputs.digest }} + + - name: Docker meta (Alpine) + id: meta_alpine + uses: docker/metadata-action@v5 + with: + images: meshtastic/meshtasticd + tags: ${{ steps.tags.outputs.alpine }} + + - name: Create Docker manifest (Alpine) + id: manifest_alpine + uses: int128/docker-manifest-create-action@v2 + with: + tags: ${{ steps.meta_alpine.outputs.tags }} + push: true + sources: | + meshtastic/meshtasticd@${{ needs.docker-alpine-amd64.outputs.digest }} + meshtastic/meshtasticd@${{ needs.docker-alpine-arm64.outputs.digest }} + meshtastic/meshtasticd@${{ needs.docker-alpine-armv7.outputs.digest }} diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 0a0ea9954..a9678f4fc 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -147,10 +147,37 @@ jobs: test-native: uses: ./.github/workflows/test_native.yml - build-docker: - if: ${{ github.event_name == 'workflow_dispatch' }} - uses: ./.github/workflows/build_docker.yml - secrets: inherit + docker-debian-amd64: + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + + docker-alpine-amd64: + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + + docker-debian-arm64: + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm64 + runs-on: ubuntu-24.04-arm + push: false + + docker-debian-armv7: + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm/v7 + runs-on: ubuntu-24.04-arm + push: false after-checks: runs-on: ubuntu-latest diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index afb7319ed..b59a0316c 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -9,6 +9,13 @@ permissions: packages: write jobs: + build-docker: + uses: ./.github/workflows/docker_manifest.yml + with: + release_channel: |- + ${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} + secrets: inherit + package-ppa: strategy: fail-fast: false From 0fdbf70452158d292d71bdd8b02499d8bd4b2c2a Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 21 Jan 2025 22:26:10 -0500 Subject: [PATCH 09/71] Small fix: Correctly pass secrets in Docker builds (#5905) --- .github/workflows/docker_build.yml | 3 +++ .github/workflows/docker_manifest.yml | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 83c67bb32..43072b777 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -4,6 +4,9 @@ name: Build Docker on: workflow_call: + secrets: + DOCKER_FIRMWARE_TOKEN: + required: false # Only required for push inputs: distro: description: Distro to target diff --git a/.github/workflows/docker_manifest.yml b/.github/workflows/docker_manifest.yml index 30dcfb067..9183dfd6c 100644 --- a/.github/workflows/docker_manifest.yml +++ b/.github/workflows/docker_manifest.yml @@ -2,6 +2,9 @@ name: Build Docker Multi-Arch Manifest on: workflow_call: + secrets: + DOCKER_FIRMWARE_TOKEN: + required: true inputs: release_channel: description: Release channel to target @@ -20,6 +23,7 @@ jobs: platform: linux/amd64 runs-on: ubuntu-24.04 push: true + secrets: inherit docker-debian-arm64: uses: ./.github/workflows/docker_build.yml @@ -28,6 +32,7 @@ jobs: platform: linux/arm64 runs-on: ubuntu-24.04-arm push: true + secrets: inherit docker-debian-armv7: uses: ./.github/workflows/docker_build.yml @@ -36,6 +41,7 @@ jobs: platform: linux/arm/v7 runs-on: ubuntu-24.04-arm push: true + secrets: inherit docker-alpine-amd64: uses: ./.github/workflows/docker_build.yml @@ -44,6 +50,7 @@ jobs: platform: linux/amd64 runs-on: ubuntu-24.04 push: true + secrets: inherit docker-alpine-arm64: uses: ./.github/workflows/docker_build.yml @@ -52,6 +59,7 @@ jobs: platform: linux/arm64 runs-on: ubuntu-24.04-arm push: true + secrets: inherit docker-alpine-armv7: uses: ./.github/workflows/docker_build.yml @@ -60,6 +68,7 @@ jobs: platform: linux/arm/v7 runs-on: ubuntu-24.04-arm push: true + secrets: inherit docker-manifest: needs: From fdc87d492c0bc164a50f02da2f1ab2806013704e Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 22 Jan 2025 00:45:34 -0800 Subject: [PATCH 10/71] Add quotes around ${platformio.build_dir} (#5906) Fixes #5898 (hopefully) --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index ea4de4db1..1c51e53b4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,7 +20,7 @@ extra_scripts = bin/platformio-custom.py build_flags = -Wno-missing-field-initializers -Wno-format - -Isrc -Isrc/mesh -Isrc/mesh/generated -Isrc/gps -Isrc/buzz -Wl,-Map,${platformio.build_dir}/output.map + -Isrc -Isrc/mesh -Isrc/mesh/generated -Isrc/gps -Isrc/buzz -Wl,-Map,"${platformio.build_dir}"/output.map -DUSE_THREAD_NAMES -DTINYGPS_OPTION_NO_CUSTOM_FIELDS -DPB_ENABLE_MALLOC=1 From 7fb22cf678d0e40d7e44e3e9ecd8cb10f734e8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Wed, 22 Jan 2025 14:11:58 +0100 Subject: [PATCH 11/71] ignore platformio core files when building in place --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 803aee139..b63f431d1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ web.tar *.code-workspace .idea +.platformio +.local +.cache .DS_Store Thumbs.db From 01892cbd1eaa824628315f3c79078763f3e8cc91 Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 22 Jan 2025 09:55:57 -0500 Subject: [PATCH 12/71] Docker: tag intermediate containers (#5910) --- .github/workflows/docker_build.yml | 21 ++++++++++++++++++++- .github/workflows/docker_manifest.yml | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 43072b777..eec0785c0 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -48,6 +48,11 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} + - name: Get release version string + run: | + echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + id: version + - name: Docker login if: ${{ inputs.push }} uses: docker/login-action@v3 @@ -61,6 +66,20 @@ jobs: - name: Docker setup uses: docker/setup-buildx-action@v3 + - name: Sanitize platform string + id: sanitize_platform + # Replace slashes with underscores + run: echo "cleaned_platform=${{ inputs.platform }}" | sed 's/\//_/g' >> $GITHUB_OUTPUT + + - name: Docker tag + id: meta + uses: docker/metadata-action@v5 + with: + images: meshtastic/meshtasticd + tags: | + GHA-${{ steps.version.outputs.long }}-${{ inputs.distro }}-${{ steps.sanitize_platform.outputs.cleaned_platform }} + flavor: latest=false + - name: Docker build and push uses: docker/build-push-action@v6 id: docker_variant @@ -69,5 +88,5 @@ jobs: file: | ${{ contains(inputs.distro, 'debian') && './Dockerfile' || contains(inputs.distro, 'alpine') && './alpine.Dockerfile' }} push: ${{ inputs.push }} - tags: "" # Intentionally empty, push with digest only + tags: ${{ steps.meta.outputs.tags }} # Tag is only meant to be consumed by the "manifest" job platforms: ${{ inputs.platform }} diff --git a/.github/workflows/docker_manifest.yml b/.github/workflows/docker_manifest.yml index 9183dfd6c..28dbf8c21 100644 --- a/.github/workflows/docker_manifest.yml +++ b/.github/workflows/docker_manifest.yml @@ -145,6 +145,7 @@ jobs: with: images: meshtastic/meshtasticd tags: ${{ steps.tags.outputs.debian }} + flavor: latest=false - name: Create Docker manifest (Debian) id: manifest_debian From 8e8b22edb0ae0aa56f92bbf49967b667a9acc747 Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 22 Jan 2025 12:09:29 -0500 Subject: [PATCH 13/71] Debian: Switch OBS repo to `network:Meshtastic` (#5912) --- .github/workflows/daily_packaging.yml | 2 +- .github/workflows/release_channels.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/daily_packaging.yml b/.github/workflows/daily_packaging.yml index cb8f866c6..11fe2043a 100644 --- a/.github/workflows/daily_packaging.yml +++ b/.github/workflows/daily_packaging.yml @@ -40,7 +40,7 @@ jobs: package-obs: uses: ./.github/workflows/package_obs.yml with: - obs_project: home:meshtastic:daily + obs_project: network:Meshtastic:daily series: unstable secrets: inherit diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index b59a0316c..a3a105d6d 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -32,7 +32,7 @@ jobs: uses: ./.github/workflows/package_obs.yml with: obs_project: |- - home:meshtastic:${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} + network:Meshtastic:${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} series: |- ${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} secrets: inherit From 3b40fe9805041a69381442bce1b2273bdd91b99d Mon Sep 17 00:00:00 2001 From: Austin Date: Thu, 23 Jan 2025 17:03:03 -0500 Subject: [PATCH 14/71] Docker: Switch tags to newline-seperated (#5919) --- .github/workflows/docker_manifest.yml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker_manifest.yml b/.github/workflows/docker_manifest.yml index 28dbf8c21..d1d1a5634 100644 --- a/.github/workflows/docker_manifest.yml +++ b/.github/workflows/docker_manifest.yml @@ -128,9 +128,14 @@ jobs: } } - with open(os.environ['GITHUB_OUTPUT'], 'a') as fh: - fh.write(f"debian={','.join(tags[release_channel]['debian'])}\n") - fh.write(f"alpine={','.join(tags[release_channel]['alpine'])}\n") + with open(os.environ["GITHUB_OUTPUT"], "a") as fh: + fh.write("debian< Date: Thu, 23 Jan 2025 19:12:20 -0600 Subject: [PATCH 15/71] NRF52 - Remove file totally before opening write (#5916) * Remove prefs first * Remove file first * Remove truncate * No longer needed * Missed a param * That wasn't supposed to be there * Remove vestigal lfs assert * Durr --- src/SafeFile.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/SafeFile.cpp b/src/SafeFile.cpp index 94232e81d..c942aa0ee 100644 --- a/src/SafeFile.cpp +++ b/src/SafeFile.cpp @@ -8,9 +8,8 @@ static File openFile(const char *filename, bool fullAtomic) concurrency::LockGuard g(spiLock); LOG_DEBUG("Opening %s, fullAtomic=%d", filename, fullAtomic); #ifdef ARCH_NRF52 - File file = FSCom.open(filename, FILE_O_WRITE); - file.seek(0); - return file; + FSCom.remove(filename); + return FSCom.open(filename, FILE_O_WRITE); #endif if (!fullAtomic) FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists) @@ -59,9 +58,6 @@ bool SafeFile::close() return false; spiLock->lock(); -#ifdef ARCH_NRF52 - f.truncate(); -#endif f.close(); spiLock->unlock(); From d1f7739bbea229049ac92d32022888fbd8b8e382 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 23 Jan 2025 19:56:59 -0600 Subject: [PATCH 16/71] Peg NRF52 arduino to meshtastic fork with LFE bluetooth fix (#5924) --- arch/nrf52/nrf52.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/nrf52/nrf52.ini b/arch/nrf52/nrf52.ini index 57b276978..b68977c78 100644 --- a/arch/nrf52/nrf52.ini +++ b/arch/nrf52/nrf52.ini @@ -4,7 +4,7 @@ platform = platformio/nordicnrf52@^10.7.0 extends = arduino_base platform_packages = ; our custom Git version until they merge our PR - framework-arduinoadafruitnrf52 @ https://github.com/geeksville/Adafruit_nRF52_Arduino.git + framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino.git#e13f5820002a4fb2a5e6754b42ace185277e5adf toolchain-gccarmnoneeabi@~1.90301.0 build_type = debug From 3298df953a75d70471ca77abe78dff8cca2eb6bc Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Sat, 25 Jan 2025 00:30:18 +1100 Subject: [PATCH 17/71] Fixed the issue that the wifi configuration saved to RAM did not take effect. (#5925) Co-authored-by: virgil --- src/mesh/wifi/WiFiAPClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index dcfcdc047..41de89794 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -225,7 +225,7 @@ bool initWifi() #if !MESHTASTIC_EXCLUDE_WEBSERVER createSSLCert(); // For WebServer #endif - esp_wifi_set_storage(WIFI_STORAGE_RAM); // Disable flash storage for WiFi credentials + WiFi.persistent(false); // Disable flash storage for WiFi credentials #endif if (!*wifiPsw) // Treat empty password as no password wifiPsw = NULL; From 4c97351187c80f38d680f2ef3fde18e427d29d50 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 18:52:17 -0600 Subject: [PATCH 18/71] [create-pull-request] automated change (#5926) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/mesh.pb.h | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/protobufs b/protobufs index fde27e4ef..7f13df0e5 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit fde27e4ef0fcee967063ba353422ed5f9a1c4790 +Subproject commit 7f13df0e5f7cbb07f0e6f3a57c0d86ad448738db diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 5cd23c8e3..3353a020f 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -223,6 +223,9 @@ typedef enum _meshtastic_HardwareModel { /* Mesh-Tab, esp32 based https://github.com/valzzu/Mesh-Tab */ meshtastic_HardwareModel_MESH_TAB = 86, + /* MeshLink board developed by LoraItalia. NRF52840, eByte E22900M22S (Will also come with other frequencies), 25w MPPT solar charger (5v,12v,18v selectable), support for gps, buzzer, oled or e-ink display, 10 gpios, hardware watchdog + https://www.loraitalia.it */ + meshtastic_HardwareModel_MESHLINK = 87, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ From fd56995764c3ea003a989e85965e197617854e3c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Jan 2025 07:53:24 -0600 Subject: [PATCH 19/71] [create-pull-request] automated change (#5928) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- debian/changelog | 5 +++-- version.properties | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/debian/changelog b/debian/changelog index a1a359cfb..1b371296b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,7 +1,8 @@ -meshtasticd (2.5.20.0) UNRELEASED; urgency=medium +meshtasticd (2.5.21.0) UNRELEASED; urgency=medium * Initial packaging * GitHub Actions Automatic version bump * GitHub Actions Automatic version bump + * GitHub Actions Automatic version bump - -- Austin Lane Wed, 15 Jan 2025 14:08:54 +0000 + -- Austin Lane Sat, 25 Jan 2025 01:39:16 +0000 diff --git a/version.properties b/version.properties index 4312ae59a..efc42428c 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 5 -build = 20 +build = 21 From a14346bc4f862f8f713e9d2e21704b14ae5c99b1 Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Sat, 25 Jan 2025 16:24:24 +0100 Subject: [PATCH 20/71] Rate limit position replies to three minutes (#5932) --- src/modules/PositionModule.cpp | 20 ++++++++++++++++---- src/modules/PositionModule.h | 2 ++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index 6285d7aa5..95a47f0a1 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -160,7 +160,8 @@ bool PositionModule::hasGPS() #endif } -meshtastic_MeshPacket *PositionModule::allocReply() +// Allocate a packet with our position data if we have one +meshtastic_MeshPacket *PositionModule::allocPositionPacket() { if (precision == 0) { LOG_DEBUG("Skip location send because precision is set to 0!"); @@ -262,7 +263,8 @@ meshtastic_MeshPacket *PositionModule::allocReply() p.has_ground_speed = true; } - LOG_INFO("Position reply: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i); + LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i); + lastSentToMesh = millis(); // TAK Tracker devices should send their position in a TAK packet over the ATAK port if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) @@ -271,6 +273,16 @@ meshtastic_MeshPacket *PositionModule::allocReply() return allocDataProtobuf(p); } +meshtastic_MeshPacket *PositionModule::allocReply() +{ + if (lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 3 * 60 * 1000)) { + LOG_DEBUG("Skip Position reply since we sent it <3min ago"); + ignoreRequest = true; // Mark it as ignored for MeshModule + return nullptr; + } + return allocPositionPacket(); +} + meshtastic_MeshPacket *PositionModule::allocAtakPli() { LOG_INFO("Send TAK PLI packet"); @@ -333,9 +345,9 @@ void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t cha precision = 0; } - meshtastic_MeshPacket *p = allocReply(); + meshtastic_MeshPacket *p = allocPositionPacket(); if (p == nullptr) { - LOG_DEBUG("allocReply returned a nullptr"); + LOG_DEBUG("allocPositionPacket returned a nullptr"); return; } diff --git a/src/modules/PositionModule.h b/src/modules/PositionModule.h index 1e4aa5d29..dc732a3db 100644 --- a/src/modules/PositionModule.h +++ b/src/modules/PositionModule.h @@ -55,6 +55,7 @@ class PositionModule : public ProtobufModule, private concu virtual int32_t runOnce() override; private: + meshtastic_MeshPacket *allocPositionPacket(); struct SmartPosition getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition); meshtastic_MeshPacket *allocAtakPli(); void trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate = false); @@ -62,6 +63,7 @@ class PositionModule : public ProtobufModule, private concu void sendLostAndFoundText(); bool hasQualityTimesource(); bool hasGPS(); + uint32_t lastSentToMesh = 0; // Last time we sent our position to the mesh const uint32_t minimumTimeThreshold = Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30); From 7649e70585ab1fa9a67924c7e5ab4a85b4bf702a Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 25 Jan 2025 12:01:25 -0600 Subject: [PATCH 21/71] Revert "No focus on new messages if auto-carousel is off (#5881)" (#5936) This reverts commit 0f981153ebbad06675f04bd9b0dd6170c7f08352. --- src/graphics/Screen.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index b7253ca17..198dcc235 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2662,13 +2662,14 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg) int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) { - // If auto carousel is disabled -> return 0 and skip new messages handling - if (config.display.auto_screen_carousel_secs == 0) - return 0; - - // Handle focus change based on message type if (showingNormalScreen) { - setFrames(packet->from == 0 ? FOCUS_PRESERVE : FOCUS_TEXTMESSAGE); + // Outgoing message + if (packet->from == 0) + setFrames(FOCUS_PRESERVE); // Return to same frame (quietly hiding the rx text message frame) + + // Incoming message + else + setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message } return 0; @@ -2755,4 +2756,4 @@ int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg) } // namespace graphics #else graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} -#endif // HAS_SCREEN \ No newline at end of file +#endif // HAS_SCREEN From 10d553087c4d8158b961a5b9ab8b6a1ff413ca38 Mon Sep 17 00:00:00 2001 From: Aleksey Vasilenko Date: Sun, 26 Jan 2025 10:54:26 +0200 Subject: [PATCH 22/71] Add missing build_unflags (#5941) Fixes 'undefined reference to app_main' build error for my_esp32s3_diy_eink and my_esp32s3_diy_oled variants. --- variants/my_esp32s3_diy_eink/platformio.ini | 4 +++- variants/my_esp32s3_diy_oled/platformio.ini | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/variants/my_esp32s3_diy_eink/platformio.ini b/variants/my_esp32s3_diy_eink/platformio.ini index e81f2c1ab..b2404566f 100644 --- a/variants/my_esp32s3_diy_eink/platformio.ini +++ b/variants/my_esp32s3_diy_eink/platformio.ini @@ -14,7 +14,9 @@ lib_deps = ${esp32_base.lib_deps} zinggjm/GxEPD2@^1.5.1 adafruit/Adafruit NeoPixel @ ^1.12.0 -build_unflags = -DARDUINO_USB_MODE=1 +build_unflags = + ${esp32s3_base.build_unflags} + -DARDUINO_USB_MODE=1 build_flags = ;${esp32_base.build_flags} -D MY_ESP32S3_DIY -I variants/my_esp32s3_diy_eink ${esp32_base.build_flags} -D PRIVATE_HW -I variants/my_esp32s3_diy_eink diff --git a/variants/my_esp32s3_diy_oled/platformio.ini b/variants/my_esp32s3_diy_oled/platformio.ini index 2d7a5cd91..0fbbaa899 100644 --- a/variants/my_esp32s3_diy_oled/platformio.ini +++ b/variants/my_esp32s3_diy_oled/platformio.ini @@ -13,7 +13,9 @@ platform_packages = lib_deps = ${esp32_base.lib_deps} adafruit/Adafruit NeoPixel @ ^1.12.0 -build_unflags = -DARDUINO_USB_MODE=1 +build_unflags = + ${esp32s3_base.build_unflags} + -DARDUINO_USB_MODE=1 build_flags = ;${esp32_base.build_flags} -D MY_ESP32S3_DIY -I variants/my_esp32s3_diy_oled ${esp32_base.build_flags} -D PRIVATE_HW -I variants/my_esp32s3_diy_oled From 4747e73f37e8f6d14aaa3288841fa0f7d8f1c177 Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Sun, 26 Jan 2025 20:59:59 +0100 Subject: [PATCH 23/71] Space out periodic broadcasts of modules automatically (#5931) * Space out periodic broadcasts of modules automatically * Add warning for function usage --------- Co-authored-by: Ben Meadors --- src/mesh/MeshModule.cpp | 10 ++++++++++ src/mesh/MeshModule.h | 9 +++++++++ src/modules/DetectionSensorModule.cpp | 4 ++-- src/modules/NodeInfoModule.cpp | 7 ++++--- src/modules/PositionModule.cpp | 5 +++-- src/modules/Telemetry/AirQualityTelemetry.cpp | 4 ++-- src/modules/Telemetry/DeviceTelemetry.h | 4 ++-- src/modules/Telemetry/EnvironmentTelemetry.cpp | 6 +++--- src/modules/Telemetry/HealthTelemetry.cpp | 2 +- src/modules/Telemetry/PowerTelemetry.cpp | 2 +- 10 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index 2f2863fa5..62d3c82bc 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -10,6 +10,7 @@ std::vector *MeshModule::modules; const meshtastic_MeshPacket *MeshModule::currentRequest; +uint8_t MeshModule::numPeriodicModules = 0; /** * If any of the current chain of modules has already sent a reply, it will be here. This is useful to allow @@ -35,6 +36,15 @@ MeshModule::~MeshModule() modules->erase(it); } +// âš ï¸ **Only call once** to set the initial delay before a module starts broadcasting periodically +int32_t MeshModule::setStartDelay() +{ + int32_t startDelay = MESHMODULE_MIN_BROADCAST_DELAY_MS + numPeriodicModules * MESHMODULE_BROADCAST_SPACING_MS; + numPeriodicModules++; + + return startDelay; +} + meshtastic_MeshPacket *MeshModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit) { diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index a88f1e6ff..f08b8f49c 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -9,6 +9,9 @@ #include #endif +#define MESHMODULE_MIN_BROADCAST_DELAY_MS 30 * 1000 // Min. delay after boot before sending first broadcast by any module +#define MESHMODULE_BROADCAST_SPACING_MS 15 * 1000 // Initial spacing between broadcasts of different modules + /** handleReceived return enumeration * * Use ProcessMessage::CONTINUE to allows other modules to process a message. @@ -119,6 +122,12 @@ class MeshModule */ static const meshtastic_MeshPacket *currentRequest; + // We keep track of the number of modules that send a periodic broadcast to schedule them spaced out over time + static uint8_t numPeriodicModules; + + // Set the start delay for module that broadcasts periodically + int32_t setStartDelay(); + /** * If your handler wants to send a response, simply set currentReply and it will be sent at the end of response handling. */ diff --git a/src/modules/DetectionSensorModule.cpp b/src/modules/DetectionSensorModule.cpp index c479867fc..ca682b772 100644 --- a/src/modules/DetectionSensorModule.cpp +++ b/src/modules/DetectionSensorModule.cpp @@ -81,7 +81,7 @@ int32_t DetectionSensorModule::runOnce() } LOG_INFO("Detection Sensor Module: init"); - return DELAYED_INTERVAL; + return setStartDelay(); } // LOG_DEBUG("Detection Sensor Module: Current pin state: %i", digitalRead(moduleConfig.detection_sensor.monitor_pin)); @@ -161,4 +161,4 @@ 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; -} +} \ No newline at end of file diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index b55d47d5b..ce4a6bd06 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -97,8 +97,9 @@ NodeInfoModule::NodeInfoModule() : ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo") { isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others - setIntervalFromNow(30 * - 1000); // Send our initial owner announcement 30 seconds after we start (to give network time to setup) + + setIntervalFromNow(setStartDelay()); // Send our initial owner announcement 30 seconds + // after we start (to give network time to setup) } int32_t NodeInfoModule::runOnce() @@ -112,4 +113,4 @@ int32_t NodeInfoModule::runOnce() sendOurNodeInfo(NODENUM_BROADCAST, requestReplies); // Send our info (don't request replies) } return Default::getConfiguredOrDefaultMs(config.device.node_info_broadcast_secs, default_node_info_broadcast_secs); -} +} \ No newline at end of file diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index 95a47f0a1..e0f5b513f 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -28,8 +28,9 @@ PositionModule::PositionModule() nodeStatusObserver.observe(&nodeStatus->onNewStatus); if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && - config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) - setIntervalFromNow(60 * 1000); + config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { + setIntervalFromNow(setStartDelay()); + } // Power saving trackers should clear their position on startup to avoid waking up and sending a stale position if ((config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 6a8077f03..392bd6148 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -50,12 +50,12 @@ int32_t AirQualityTelemetryModule::runOnce() nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = i2cScanner->fetchI2CBus(found.address); - return 1000; + return setStartDelay(); } #endif return disable(); } - return 1000; + return setStartDelay(); } return disable(); } else { diff --git a/src/modules/Telemetry/DeviceTelemetry.h b/src/modules/Telemetry/DeviceTelemetry.h index 19b7d5b01..a1d55a596 100644 --- a/src/modules/Telemetry/DeviceTelemetry.h +++ b/src/modules/Telemetry/DeviceTelemetry.h @@ -18,7 +18,7 @@ class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModu uptimeWrapCount = 0; uptimeLastMs = millis(); nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(45 * 1000); // Wait until NodeInfo is sent + setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent } virtual bool wantUIFrame() { return false; } @@ -62,4 +62,4 @@ class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModu uint32_t uptimeWrapCount; uint32_t uptimeLastMs; -}; +}; \ No newline at end of file diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 6a5e8376d..3fa3e848a 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -107,8 +107,6 @@ int32_t EnvironmentTelemetryModule::runOnce() if (moduleConfig.telemetry.environment_measurement_enabled) { LOG_INFO("Environment Telemetry: init"); - // it's possible to have this module enabled, only for displaying values on the screen. - // therefore, we should only enable the sensor loop if measurement is also enabled #ifdef SENSECAP_INDICATOR result = indicatorSensor.runOnce(); #endif @@ -171,7 +169,9 @@ int32_t EnvironmentTelemetryModule::runOnce() #endif #endif } - return result; + // it's possible to have this module enabled, only for displaying values on the screen. + // therefore, we should only enable the sensor loop if measurement is also enabled + return result == UINT32_MAX ? disable() : setStartDelay(); } else { // if we somehow got to a second run of this module with measurement disabled, then just wait forever if (!moduleConfig.telemetry.environment_measurement_enabled) { diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp index 1b9b49813..a2a18ba03 100644 --- a/src/modules/Telemetry/HealthTelemetry.cpp +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -62,7 +62,7 @@ int32_t HealthTelemetryModule::runOnce() if (max30102Sensor.hasSensor()) result = max30102Sensor.runOnce(); } - return result; + return result == UINT32_MAX ? disable() : setStartDelay(); } else { // if we somehow got to a second run of this module with measurement disabled, then just wait forever if (!moduleConfig.telemetry.health_measurement_enabled) { diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 38a5c6f11..04bcbe200 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -65,7 +65,7 @@ int32_t PowerTelemetryModule::runOnce() if (max17048Sensor.hasSensor() && !max17048Sensor.isInitialized()) result = max17048Sensor.runOnce(); } - return result; + return result == UINT32_MAX ? disable() : setStartDelay(); #else return disable(); #endif From 2d42e1b2bcd85ca80ec1c3ed5ba9600a21a75182 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:00:12 +0100 Subject: [PATCH 24/71] fix: TCXO_OPTIONAL featuring SenseCAP Indicator (V1/V2) (#5948) * fix TCXO_OPTIONAL * fix LOG_WARN * fix lora.begin() returns -707 * trunk fmt --- src/main.cpp | 38 +++++++++---------- src/mesh/SX126xInterface.cpp | 15 ++------ src/mesh/SX126xInterface.h | 3 ++ .../diy/nrf52_promicro_diy_tcxo/variant.h | 3 +- variants/seeed-sensecap-indicator/variant.h | 3 ++ 5 files changed, 28 insertions(+), 34 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 24fc71749..f4599e0e3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -115,10 +115,6 @@ AccelerometerThread *accelerometerThread = nullptr; AudioThread *audioThread = nullptr; #endif -#if defined(TCXO_OPTIONAL) -float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if TCXO is optional, put this here so it can be changed further down. -#endif - using namespace concurrency; volatile static const char slipstreamTZString[] = USERPREFS_TZ_STRING; @@ -928,13 +924,16 @@ void setup() #if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1 if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { + auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); +#ifdef SX126X_DIO3_TCXO_VOLTAGE + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); +#endif + if (!sxIf->init()) { LOG_WARN("No SX1262 radio"); - delete rIf; - rIf = NULL; + delete sxIf; } else { LOG_INFO("SX1262 init success"); + rIf = sxIf; radioType = SX1262_RADIO; } } @@ -942,29 +941,28 @@ void setup() #if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL) if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // Try using the specified TCXO voltage - rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1262 radio with TCXO, Vref %f V", tcxoVoltage); - delete rIf; - rIf = NULL; - tcxoVoltage = 0; // if it fails, set the TCXO voltage to zero for the next attempt + // try using the specified TCXO voltage + auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); + if (!sxIf->init()) { + LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + delete sxIf; } else { - LOG_WARN("SX1262 init success, TCXO, Vref %f V", tcxoVoltage); + LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + rIf = sxIf; radioType = SX1262_RADIO; } } if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // If specified TCXO voltage fails, attempt to use DIO3 as a reference instea + // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); if (!rIf->init()) { - LOG_WARN("No SX1262 radio with XTAL, Vref %f V", tcxoVoltage); + LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V"); delete rIf; rIf = NULL; - tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if it fails, set the TCXO voltage back for the next radio search } else { - LOG_INFO("SX1262 init success, XTAL, Vref %f V", tcxoVoltage); + LOG_INFO("SX1262 init success, XTAL, Vref 0.0V"); radioType = SX1262_RADIO; } } diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 8a7bc7670..5710de7ea 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -50,22 +50,13 @@ template bool SX126xInterface::init() #endif #if ARCH_PORTDUINO - float tcxoVoltage = (float)settingsMap[dio3_tcxo_voltage] / 1000; + tcxoVoltage = (float)settingsMap[dio3_tcxo_voltage] / 1000; if (settingsMap[sx126x_ant_sw_pin] != RADIOLIB_NC) { digitalWrite(settingsMap[sx126x_ant_sw_pin], HIGH); pinMode(settingsMap[sx126x_ant_sw_pin], OUTPUT); } -// FIXME: correct logic to default to not using TCXO if no voltage is specified for SX126X_DIO3_TCXO_VOLTAGE -#elif !defined(SX126X_DIO3_TCXO_VOLTAGE) - float tcxoVoltage = - 0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per - // https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/SX126x/SX126x.h#L471C26-L471C104 - // (DIO3 is free to be used as an IRQ) -#elif !defined(TCXO_OPTIONAL) - float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; - // (DIO3 is not free to be used as an IRQ) #endif - if (tcxoVoltage == 0) + if (tcxoVoltage == 0.0) LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); else LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", tcxoVoltage); @@ -83,7 +74,7 @@ template bool SX126xInterface::init() int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO); // \todo Display actual typename of the adapter, not just `SX126x` LOG_INFO("SX126x init result %d", res); - if (res == RADIOLIB_ERR_CHIP_NOT_FOUND) + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) return false; LOG_INFO("Frequency set to %f", getFreq()); diff --git a/src/mesh/SX126xInterface.h b/src/mesh/SX126xInterface.h index 45b39a68a..47b07c284 100644 --- a/src/mesh/SX126xInterface.h +++ b/src/mesh/SX126xInterface.h @@ -28,8 +28,11 @@ template class SX126xInterface : public RadioLibInterface bool isIRQPending() override { return lora.getIrqFlags() != 0; } + void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; } + protected: float currentLimit = 140; // Higher OCP limit for SX126x PA + float tcxoVoltage = 0.0; /** * Specific module instance diff --git a/variants/diy/nrf52_promicro_diy_tcxo/variant.h b/variants/diy/nrf52_promicro_diy_tcxo/variant.h index 6ffb86cff..5e939c023 100644 --- a/variants/diy/nrf52_promicro_diy_tcxo/variant.h +++ b/variants/diy/nrf52_promicro_diy_tcxo/variant.h @@ -183,8 +183,7 @@ settings. */ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL -extern float tcxoVoltage; // make this available everywhere +#define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL #ifdef __cplusplus } diff --git a/variants/seeed-sensecap-indicator/variant.h b/variants/seeed-sensecap-indicator/variant.h index 29d547be3..c5fc685cd 100644 --- a/variants/seeed-sensecap-indicator/variant.h +++ b/variants/seeed-sensecap-indicator/variant.h @@ -70,5 +70,8 @@ #define SX126X_RESET LORA_RESET #define SX126X_DIO2_AS_RF_SWITCH +#define TCXO_OPTIONAL // handle Indicator V1 and V2 +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + #define USE_VIRTUAL_KEYBOARD 1 #define DISPLAY_CLOCK_FRAME 1 From 30a31a3a13caec27fa1d09e119e97fdb2881b8a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Tue, 28 Jan 2025 15:38:22 +0100 Subject: [PATCH 25/71] Oem logo (#5939) * reinstate oemlogo, add to userPrefs.jsonc * disable from default build --- src/graphics/Screen.cpp | 76 ++++++++++++++++++++++++++++++++++++++--- userPrefs.jsonc | 10 ++++-- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 198dcc235..c9004432f 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -123,7 +123,7 @@ static bool heartbeat = false; #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) -/// Check if the display can render a string (detect special chars; emoji) +// Check if the display can render a string (detect special chars; emoji) static bool haveGlyphs(const char *str) { #if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) || defined(OLED_CS) @@ -162,11 +162,7 @@ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDispl display->setFont(FONT_MEDIUM); display->setTextAlignment(TEXT_ALIGN_LEFT); -#ifdef USERPREFS_SPLASH_TITLE - const char *title = USERPREFS_SPLASH_TITLE; -#else const char *title = "meshtastic.org"; -#endif display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); display->setFont(FONT_SMALL); @@ -185,6 +181,56 @@ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDispl display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code } +#ifdef USERPREFS_OEM_TEXT + +static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; + display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, + USERPREFS_OEM_IMAGE_HEIGHT, xbm); + + switch (USERPREFS_OEM_FONT_SIZE) { + case 0: + display->setFont(FONT_SMALL); + break; + case 2: + display->setFont(FONT_LARGE); + break; + default: + display->setFont(FONT_MEDIUM); + break; + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *title = USERPREFS_OEM_TEXT; + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + display->setFont(FONT_SMALL); + + // Draw region in upper left + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); + + // Draw version and shortname in upper right + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); + + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); + screen->forceDisplay(); + + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code +} + +static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Draw region in upper left + const char *region = myRegion ? myRegion->name : NULL; + drawOEMIconScreen(region, display, state, x, y); +} + +#endif + void Screen::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) { uint16_t x_offset = display->width() / 2; @@ -1658,6 +1704,10 @@ void Screen::setup() // Set the utf8 conversion function dispdev->setFontTableLookupFunction(customFontTableLookup); +#ifdef USERPREFS_OEM_TEXT + logo_timeout *= 2; // Double the time if we have a custom logo +#endif + // Add frames. EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { @@ -1803,6 +1853,22 @@ int32_t Screen::runOnce() showingBootScreen = false; } +#ifdef USERPREFS_OEM_TEXT + static bool showingOEMBootScreen = true; + if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) { + LOG_INFO("Switch to OEM screen..."); + // Change frames. + static FrameCallback bootOEMFrames[] = {drawOEMBootScreen}; + static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]); + ui->setFrames(bootOEMFrames, bootOEMFrameCount); + ui->update(); +#ifndef USE_EINK + ui->update(); +#endif + showingOEMBootScreen = false; + } +#endif + #ifndef DISABLE_WELCOME_UNSET if (showingNormalScreen && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { setWelcomeFrames(); diff --git a/userPrefs.jsonc b/userPrefs.jsonc index 055f59273..de610464d 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -29,9 +29,13 @@ // "USERPREFS_FIXED_GPS_LON": "2.294508368", // "USERPREFS_LORACONFIG_CHANNEL_NUM": "31", // "USERPREFS_LORACONFIG_MODEM_PRESET": "meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST", - // "USERPREFS_SPLASH_TITLE": "DEFCONtastic", "USERPREFS_TZ_STRING": "tzplaceholder " // "USERPREFS_USE_ADMIN_KEY_0": "{ 0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c }", // "USERPREFS_USE_ADMIN_KEY_1": "{}", - // "USERPREFS_USE_ADMIN_KEY_2": "{}" -} \ No newline at end of file + // "USERPREFS_USE_ADMIN_KEY_2": "{}", + // "USERPREFS_OEM_TEXT": "Caterham Car Club", + // "USERPREFS_OEM_FONT_SIZE": "0", + // "USERPREFS_OEM_IMAGE_WIDTH": "50", + // "USERPREFS_OEM_IMAGE_HEIGHT": "28", + // "USERPREFS_OEM_IMAGE_DATA": "{ 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0x61, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0x67, 0x00, 0x00, 0x00, 0x18, 0x1F, 0xF0, 0x67, 0x00, 0x00, 0x00, 0x30, 0x1F, 0xF8, 0x33, 0x00, 0x00, 0x00, 0x30, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x00, 0x60, 0x00, 0xFE, 0x18, 0x00, 0x00, 0x00, 0x60, 0x00, 0x7E, 0x18, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x3F, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x80, 0x1F, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x81, 0x1F, 0x06, 0x00, 0x00, 0x00, 0x80, 0xC1, 0x0F, 0x06, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x0F, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x0F, 0x03, 0x00, 0x00, 0x00, 0x00, 0xE6, 0x8F, 0x01, 0x00, 0x00, 0x00, 0x00, 0xEE, 0xC7, 0x01, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00}" +} From 6a12760c3d07805b5af614820af8732751be6037 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Wed, 29 Jan 2025 09:57:52 +0800 Subject: [PATCH 26/71] Fix off-by-one error with log writes (#5959) As reported by @jstockdale, when writing coloured logs we were writing the full string, including a null terminator. This caused issues for programs consuming our logs. The fix as identified is not to write the null. Fixes https://github.com/meshtastic/firmware/issues/5945 --- src/RedirectablePrint.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 57f53019d..07f873864 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -79,17 +79,17 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l } if (color && logLevel != nullptr) { if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) - Print::write("\u001b[34m", 6); + Print::write("\u001b[34m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) - Print::write("\u001b[32m", 6); + Print::write("\u001b[32m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) - Print::write("\u001b[33m", 6); + Print::write("\u001b[33m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) - Print::write("\u001b[31m", 6); + Print::write("\u001b[31m", 5); } len = Print::write(printBuf, len); if (color && logLevel != nullptr) { - Print::write("\u001b[0m", 5); + Print::write("\u001b[0m", 4); } return len; } @@ -107,15 +107,15 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, // include the header if (color) { if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) - Print::write("\u001b[34m", 6); + Print::write("\u001b[34m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) - Print::write("\u001b[32m", 6); + Print::write("\u001b[32m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) - Print::write("\u001b[33m", 6); + Print::write("\u001b[33m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) - Print::write("\u001b[31m", 6); + Print::write("\u001b[31m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) - Print::write("\u001b[35m", 6); + Print::write("\u001b[35m", 5); } uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // display local time on logfile @@ -393,4 +393,4 @@ std::string RedirectablePrint::mt_sprintf(const std::string fmt_str, ...) break; } return std::string(formatted.get()); -} +} \ No newline at end of file From 78da8f6fc43591d9fbd5822fb2a885d9e8a93258 Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 29 Jan 2025 06:51:26 -0500 Subject: [PATCH 27/71] Portduino: Allow limiting TX Power from yaml (#5954) --- bin/config-dist.yaml | 4 +++- bin/config.d/lora-MeshAdv-900M30S.yaml | 3 +++ src/mesh/LR11x0Interface.cpp | 6 ++++++ src/mesh/RF95Interface.cpp | 5 ++++- src/mesh/SX126xInterface.cpp | 5 ++++- src/mesh/SX128xInterface.cpp | 5 ++++- src/platform/portduino/PortduinoGlue.cpp | 8 +++++++- src/platform/portduino/PortduinoGlue.h | 7 ++++++- 8 files changed, 37 insertions(+), 6 deletions(-) diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml index c8f181308..1bf52fda2 100644 --- a/bin/config-dist.yaml +++ b/bin/config-dist.yaml @@ -78,6 +78,8 @@ Lora: # TXen: x # TX and RX enable pins # RXen: x +# SX126X_MAX_POWER: 8 # Limit the output power to 8 dBm, useful for amped nodes + # spiSpeed: 2000000 ### Set default/fallback gpio chip to use in /dev/. Defaults to 0. @@ -188,4 +190,4 @@ General: MaxMessageQueue: 100 ConfigDirectory: /etc/meshtasticd/config.d/ # MACAddress: AA:BB:CC:DD:EE:FF -# MACAddressSource: eth0 +# MACAddressSource: eth0 \ No newline at end of file diff --git a/bin/config.d/lora-MeshAdv-900M30S.yaml b/bin/config.d/lora-MeshAdv-900M30S.yaml index 07dada620..113901d5e 100644 --- a/bin/config.d/lora-MeshAdv-900M30S.yaml +++ b/bin/config.d/lora-MeshAdv-900M30S.yaml @@ -7,3 +7,6 @@ Lora: TXen: 13 RXen: 12 DIO3_TCXO_VOLTAGE: true + # Only for E22-900M33S: + # Limit the output power to 8 dBm + # SX126X_MAX_POWER: 8 \ No newline at end of file diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index ce4f912ba..5a9a53d2d 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -20,12 +20,18 @@ static const Module::RfSwitchMode_t rfswitch_table[] = { // Particular boards might define a different max power based on what their hardware can do, default to max power output if not // specified (may be dangerous if using external PA and LR11x0 power config forgotten) +#if ARCH_PORTDUINO +#define LR1110_MAX_POWER settingsMap[lr1110_max_power] +#endif #ifndef LR1110_MAX_POWER #define LR1110_MAX_POWER 22 #endif // the 2.4G part maxes at 13dBm +#if ARCH_PORTDUINO +#define LR1120_MAX_POWER settingsMap[lr1120_max_power] +#endif #ifndef LR1120_MAX_POWER #define LR1120_MAX_POWER 13 #endif diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index d4d9ad23c..1dfc72708 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -9,6 +9,9 @@ #include "PortduinoGlue.h" #endif +#if ARCH_PORTDUINO +#define RF95_MAX_POWER settingsMap[rf95_max_power] +#endif #ifndef RF95_MAX_POWER #define RF95_MAX_POWER 20 #endif @@ -337,4 +340,4 @@ bool RF95Interface::sleep() return true; } -#endif +#endif \ No newline at end of file diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 5710de7ea..7c950bc8e 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -11,6 +11,9 @@ // Particular boards might define a different max power based on what their hardware can do, default to max power output if not // specified (may be dangerous if using external PA and SX126x power config forgotten) +#if ARCH_PORTDUINO +#define SX126X_MAX_POWER settingsMap[sx126x_max_power] +#endif #ifndef SX126X_MAX_POWER #define SX126X_MAX_POWER 22 #endif @@ -333,4 +336,4 @@ template bool SX126xInterface::sleep() return true; } -#endif +#endif \ No newline at end of file diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index ee3408456..1032934b8 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -10,6 +10,9 @@ #endif // Particular boards might define a different max power based on what their hardware can do +#if ARCH_PORTDUINO +#define SX128X_MAX_POWER settingsMap[sx128x_max_power] +#endif #ifndef SX128X_MAX_POWER #define SX128X_MAX_POWER 13 #endif @@ -315,4 +318,4 @@ template bool SX128xInterface::sleep() return true; } -#endif +#endif \ No newline at end of file diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index ab78baa1a..d7ff4fc65 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -369,6 +369,12 @@ bool loadConfig(const char *configPath) } } + settingsMap[sx126x_max_power] = yamlConfig["Lora"]["SX126X_MAX_POWER"].as(22); + settingsMap[sx128x_max_power] = yamlConfig["Lora"]["SX128X_MAX_POWER"].as(13); + settingsMap[lr1110_max_power] = yamlConfig["Lora"]["LR1110_MAX_POWER"].as(22); + settingsMap[lr1120_max_power] = yamlConfig["Lora"]["LR1120_MAX_POWER"].as(13); + settingsMap[rf95_max_power] = yamlConfig["Lora"]["RF95_MAX_POWER"].as(20); + settingsMap[dio2_as_rf_switch] = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as(false); settingsMap[dio3_tcxo_voltage] = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(0) * 1000; if (settingsMap[dio3_tcxo_voltage] == 0 && yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(false)) { @@ -569,4 +575,4 @@ bool MAC_from_string(std::string mac_str, uint8_t *dmac) } else { return false; } -} +} \ No newline at end of file diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index d1e91956d..c6b5f8b41 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -27,6 +27,11 @@ enum configNames { sx126x_ant_sw_pin, sx126x_ant_sw_line, sx126x_ant_sw_gpiochip, + sx126x_max_power, + sx128x_max_power, + lr1110_max_power, + lr1120_max_power, + rf95_max_power, dio2_as_rf_switch, dio3_tcxo_voltage, use_rf95, @@ -94,4 +99,4 @@ int initGPIOPin(int pinNum, std::string gpioChipname, int line); bool loadConfig(const char *configPath); static bool ends_with(std::string_view str, std::string_view suffix); void getMacAddr(uint8_t *dmac); -bool MAC_from_string(std::string mac_str, uint8_t *dmac); +bool MAC_from_string(std::string mac_str, uint8_t *dmac); \ No newline at end of file From cd8592ef4accb3e758e8bae80744eacc93df7015 Mon Sep 17 00:00:00 2001 From: Jason P Date: Wed, 29 Jan 2025 06:14:43 -0600 Subject: [PATCH 28/71] Fixes #5766 Updated MQTT privateCidrRanges to add Tailscale (#5957) --- src/mqtt/MQTT.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index f642af231..f808a66ef 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -217,6 +217,7 @@ bool isPrivateIpAddress(const IPAddress &ip) {.network = 169u << 24 | 254 << 16, .mask = 0xffff0000}, // 169.254.0.0/16 {.network = 10u << 24, .mask = 0xff000000}, // 10.0.0.0/8 {.network = 127u << 24 | 1, .mask = 0xffffffff}, // 127.0.0.1/32 + {.network = 100u << 24 | 64 << 16, .mask = 0xffc00000}, // 100.64.0.0/10 }; const uint32_t addr = ntohl(ip); for (const auto &cidrRange : privateCidrRanges) { From b5cad2b65e934efac1bbf1d92a6616c12e680a39 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 29 Jan 2025 20:52:24 -0600 Subject: [PATCH 29/71] Fix negative decimal value detection in userPrefs (#5963) --- bin/platformio-custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py index acfeae10c..09e8e6d83 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -102,7 +102,7 @@ pref_flags = [] for pref in userPrefs: if userPrefs[pref].startswith("{"): pref_flags.append("-D" + pref + "=" + userPrefs[pref]) - elif userPrefs[pref].replace(".", "").isdigit(): + elif userPrefs[pref].lstrip("-").replace(".", "").isdigit(): pref_flags.append("-D" + pref + "=" + userPrefs[pref]) elif userPrefs[pref] == "true" or userPrefs[pref] == "false": pref_flags.append("-D" + pref + "=" + userPrefs[pref]) From 4c0e0b84712e08371d84400187e42996bb5bc254 Mon Sep 17 00:00:00 2001 From: Austin Date: Sat, 1 Feb 2025 03:58:58 -0500 Subject: [PATCH 30/71] Portduino: Set Web SSL Cert / Key paths from yaml (#5961) --- Dockerfile | 3 ++- alpine.Dockerfile | 3 ++- bin/config-dist.yaml | 2 ++ debian/meshtasticd.dirs | 3 ++- meshtasticd.spec.rpkg | 3 +++ src/mesh/raspihttp/PiWebServer.cpp | 18 +++++++++++------- src/platform/portduino/PortduinoGlue.cpp | 7 ++++++- src/platform/portduino/PortduinoGlue.h | 2 ++ 8 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index f3b294a5b..f9a3b9962 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,8 @@ USER root RUN apt-get update && apt-get --no-install-recommends -y install libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libulfius2.7 libusb-1.0-0-dev liborcania2.3 libssl3 && \ apt-get clean && rm -rf /var/lib/apt/lists/* \ && mkdir -p /var/lib/meshtasticd \ - && mkdir -p /etc/meshtasticd/config.d + && mkdir -p /etc/meshtasticd/config.d \ + && mkdir -p /etc/meshtasticd/ssl # Fetch compiled binary from the builder COPY --from=builder /tmp/firmware/release/meshtasticd /usr/sbin/ diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 115602b3b..8b48eeca3 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -29,7 +29,8 @@ USER root RUN apk add libstdc++ libgpiod yaml-cpp libusb i2c-tools \ && mkdir -p /var/lib/meshtasticd \ - && mkdir -p /etc/meshtasticd/config.d + && mkdir -p /etc/meshtasticd/config.d \ + && mkdir -p /etc/meshtasticd/ssl COPY --from=builder /tmp/firmware/release/meshtasticd /usr/sbin/ WORKDIR /var/lib/meshtasticd diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml index 1bf52fda2..da4c192c7 100644 --- a/bin/config-dist.yaml +++ b/bin/config-dist.yaml @@ -184,6 +184,8 @@ Logging: Webserver: # Port: 443 # Port for Webserver & Webservices # RootPath: /usr/share/meshtasticd/web # Root Dir of WebServer +# SSLKey: /etc/meshtasticd/ssl/private_key.pem # Path to SSL Key, generated if not present +# SSLCert: /etc/meshtasticd/ssl/certificate.pem # Path to SSL Certificate, generated if not present General: MaxNodes: 200 diff --git a/debian/meshtasticd.dirs b/debian/meshtasticd.dirs index 5f57ff7be..45a1ca3db 100644 --- a/debian/meshtasticd.dirs +++ b/debian/meshtasticd.dirs @@ -1,4 +1,5 @@ etc/meshtasticd etc/meshtasticd/config.d etc/meshtasticd/available.d -usr/share/meshtasticd/web \ No newline at end of file +usr/share/meshtasticd/web +etc/meshtasticd/ssl \ No newline at end of file diff --git a/meshtasticd.spec.rpkg b/meshtasticd.spec.rpkg index 1819897b0..720e94408 100644 --- a/meshtasticd.spec.rpkg +++ b/meshtasticd.spec.rpkg @@ -72,6 +72,8 @@ install -D -m 0644 bin/meshtasticd.service %{buildroot}%{_unitdir}/meshtasticd.s # Install the web files under /usr/share/meshtasticd/web mkdir -p %{buildroot}%{_datadir}/meshtasticd/web cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web +# Install default SSL storage directory (for web) +mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd/ssl %files %license LICENSE @@ -86,6 +88,7 @@ cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web %dir %{_datadir}/meshtasticd %dir %{_datadir}/meshtasticd/web %{_datadir}/meshtasticd/web/* +%dir %{_sysconfdir}/meshtasticd/ssl %changelog %autochangelog \ No newline at end of file diff --git a/src/mesh/raspihttp/PiWebServer.cpp b/src/mesh/raspihttp/PiWebServer.cpp index 9d2625410..4fae0bc3d 100644 --- a/src/mesh/raspihttp/PiWebServer.cpp +++ b/src/mesh/raspihttp/PiWebServer.cpp @@ -65,6 +65,9 @@ mail: marchammermann@googlemail.com #define DEFAULT_REALM "default_realm" #define PREFIX "" +#define KEY_PATH settingsStrings[websslkeypath].c_str() +#define CERT_PATH settingsStrings[websslcertpath].c_str() + struct _file_config configWeb; // We need to specify some content-type mapping, so the resources get delivered with the @@ -384,13 +387,13 @@ char *read_file_into_string(const char *filename) int PiWebServerThread::CheckSSLandLoad() { // read certificate - cert_pem = read_file_into_string("certificate.pem"); + cert_pem = read_file_into_string(CERT_PATH); if (cert_pem == NULL) { LOG_ERROR("ERROR SSL Certificate File can't be loaded or is missing"); return 1; } // read private key - key_pem = read_file_into_string("private_key.pem"); + key_pem = read_file_into_string(KEY_PATH); if (key_pem == NULL) { LOG_ERROR("ERROR file private_key can't be loaded or is missing"); return 2; @@ -415,8 +418,8 @@ int PiWebServerThread::CreateSSLCertificate() return 2; } - // Ope file to write private key file - FILE *pkey_file = fopen("private_key.pem", "wb"); + // Open file to write private key file + FILE *pkey_file = fopen(KEY_PATH, "wb"); if (!pkey_file) { LOG_ERROR("Error opening private key file"); return 3; @@ -426,18 +429,19 @@ int PiWebServerThread::CreateSSLCertificate() fclose(pkey_file); // open Certificate file - FILE *x509_file = fopen("certificate.pem", "wb"); + FILE *x509_file = fopen(CERT_PATH, "wb"); if (!x509_file) { LOG_ERROR("Error opening cert"); return 4; } - // write cirtificate + // write certificate PEM_write_X509(x509_file, x509); fclose(x509_file); EVP_PKEY_free(pkey); + LOG_INFO("Create SSL Key %s successful", KEY_PATH); X509_free(x509); - LOG_INFO("Create SSL Cert -certificate.pem- succesfull "); + LOG_INFO("Create SSL Cert %s successful", CERT_PATH); return 0; } diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index d7ff4fc65..9da65c92c 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -524,7 +524,12 @@ bool loadConfig(const char *configPath) if (yamlConfig["Webserver"]) { settingsMap[webserverport] = (yamlConfig["Webserver"]["Port"]).as(-1); - settingsStrings[webserverrootpath] = (yamlConfig["Webserver"]["RootPath"]).as(""); + settingsStrings[webserverrootpath] = + (yamlConfig["Webserver"]["RootPath"]).as("/usr/share/meshtasticd/web"); + settingsStrings[websslkeypath] = + (yamlConfig["Webserver"]["SSLKey"]).as("/etc/meshtasticd/ssl/private_key.pem"); + settingsStrings[websslcertpath] = + (yamlConfig["Webserver"]["SSLCert"]).as("/etc/meshtasticd/ssl/certificate.pem"); } if (yamlConfig["General"]) { diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index c6b5f8b41..a52ca88f8 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -81,6 +81,8 @@ enum configNames { webserver, webserverport, webserverrootpath, + websslkeypath, + websslcertpath, maxtophone, maxnodes, ascii_logs, From d9534cfc9d487f3c4e3b0a92f9b7244e599813db Mon Sep 17 00:00:00 2001 From: Chloe Bethel Date: Mon, 3 Feb 2025 03:31:54 +0000 Subject: [PATCH 31/71] Remove unused usages of #include to save Flash (#5978) Saves ~100KB on wio-e5, which was previously at 99% Flash usage --- src/mesh/NodeDB.cpp | 1 - src/serialization/JSONValue.cpp | 1 - 2 files changed, 2 deletions(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 762982287..4a01e0d41 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -23,7 +23,6 @@ #include "modules/NeighborInfoModule.h" #include #include -#include #include #include #include diff --git a/src/serialization/JSONValue.cpp b/src/serialization/JSONValue.cpp index b2e9575bf..64dc10abe 100644 --- a/src/serialization/JSONValue.cpp +++ b/src/serialization/JSONValue.cpp @@ -22,7 +22,6 @@ * THE SOFTWARE. */ -#include #include #include #include From b370717dcd50d3961d6c94565df6a86919688af7 Mon Sep 17 00:00:00 2001 From: Woutvstk <119763111+Woutvstk@users.noreply.github.com> Date: Mon, 3 Feb 2025 06:43:32 +0100 Subject: [PATCH 32/71] Add bearing to other node on device screen in text (#5968) * Merge branch 'store-and-forward' of https://github.com/Woutvstk/meshtastic_firmware into store-and-forward * also show bearing to a waypoint in text on screen --- src/graphics/Screen.cpp | 33 +++++++++++++++++------------- src/modules/WaypointModule.cpp | 37 +++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index c9004432f..4ee49e3c0 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1446,9 +1446,9 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ static char distStr[20]; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - strncpy(distStr, "? mi", sizeof(distStr)); // might not have location data + strncpy(distStr, "? mi ?°", sizeof(distStr)); // might not have location data } else { - strncpy(distStr, "? km", sizeof(distStr)); + strncpy(distStr, "? km ?°", sizeof(distStr)); } meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const char *fields[] = {username, lastStr, signalStr, distStr, NULL}; @@ -1481,18 +1481,6 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ float d = GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - if (d < (2 * MILES_TO_FEET)) - snprintf(distStr, sizeof(distStr), "%.0f ft", d * METERS_TO_FEET); - else - snprintf(distStr, sizeof(distStr), "%.1f mi", d * METERS_TO_FEET / MILES_TO_FEET); - } else { - if (d < 2000) - snprintf(distStr, sizeof(distStr), "%.0f m", d); - else - snprintf(distStr, sizeof(distStr), "%.1f km", d / 1000); - } - float bearingToOther = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly @@ -1500,6 +1488,23 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ if (!config.display.compass_north_top) bearingToOther -= myHeading; screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); + + float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2*PI : bearingToOther; + bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; + + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + if (d < (2 * MILES_TO_FEET)) + snprintf(distStr, sizeof(distStr), "%.0fft %.0f°", d * METERS_TO_FEET, bearingToOtherDegrees); + else + snprintf(distStr, sizeof(distStr), "%.1fmi %.0f°", d * METERS_TO_FEET / MILES_TO_FEET, bearingToOtherDegrees); + } else { + if (d < 2000) + snprintf(distStr, sizeof(distStr), "%.0fm %.0f°", d, bearingToOtherDegrees); + else + snprintf(distStr, sizeof(distStr), "%.1fkm %.0f°", d / 1000, bearingToOtherDegrees); + } + + } } if (!hasNodeHeading) { diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index b8b738309..08b48b682 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -135,20 +135,6 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); screen->drawCompassNorth(display, compassX, compassY, myHeading); - // Distance to Waypoint - float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - if (d < (2 * MILES_TO_FEET)) - snprintf(distStr, sizeof(distStr), "%.0f ft", d * METERS_TO_FEET); - else - snprintf(distStr, sizeof(distStr), "%.1f mi", d * METERS_TO_FEET / MILES_TO_FEET); - } else { - if (d < 2000) - snprintf(distStr, sizeof(distStr), "%.0f m", d); - else - snprintf(distStr, sizeof(distStr), "%.1f km", d / 1000); - } - // Compass bearing to waypoint float bearingToOther = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); @@ -157,6 +143,25 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, if (!config.display.compass_north_top) bearingToOther -= myHeading; screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); + + float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2*PI : bearingToOther; + bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; + + // Distance to Waypoint + float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + if (d < (2 * MILES_TO_FEET)) + snprintf(distStr, sizeof(distStr), "%.0fft %.0f°", d * METERS_TO_FEET, bearingToOtherDegrees); + else + snprintf(distStr, sizeof(distStr), "%.1fmi %.0f°", d * METERS_TO_FEET / MILES_TO_FEET, bearingToOtherDegrees); + } else { + if (d < 2000) + snprintf(distStr, sizeof(distStr), "%.0fm %.0f°", d, bearingToOtherDegrees); + else + snprintf(distStr, sizeof(distStr), "%.1fkm %.0f°", d / 1000, bearingToOtherDegrees); + } + + } // If our node doesn't have position @@ -166,9 +171,9 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // ? in the distance field if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - strncpy(distStr, "? mi", sizeof(distStr)); + strncpy(distStr, "? mi ?°", sizeof(distStr)); else - strncpy(distStr, "? km", sizeof(distStr)); + strncpy(distStr, "? km ?°", sizeof(distStr)); } // Draw compass circle From d7409342786f8c60b10e012b03810454ebb292c8 Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Mon, 3 Feb 2025 12:24:47 +0100 Subject: [PATCH 33/71] Don't rate-limit position requests for Lost and Found role (#5981) --- src/modules/PositionModule.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index e0f5b513f..acbc3143d 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -276,7 +276,8 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket() meshtastic_MeshPacket *PositionModule::allocReply() { - if (lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 3 * 60 * 1000)) { + if (config.device.role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && lastSentToMesh && + Throttle::isWithinTimespanMs(lastSentToMesh, 3 * 60 * 1000)) { LOG_DEBUG("Skip Position reply since we sent it <3min ago"); ignoreRequest = true; // Mark it as ignored for MeshModule return nullptr; From 3a34f8beaff09f494e3996f059737d10d3af75f0 Mon Sep 17 00:00:00 2001 From: Tom <116762865+Nestpebble@users.noreply.github.com> Date: Mon, 3 Feb 2025 12:16:35 +0000 Subject: [PATCH 34/71] E80 promicro update (#5967) * add readme and update rfswitch * Updated readme to include all data from Ebyte * Added results from switch testing & notes thereon * fixed picture * Whoops! Forgot to uncomment some settings from test. * Update readme.md * Delete variants/diy/nrf52_promicro_diy_tcxo/E80_RSSI_per_case.png * Add webp image to appease trunk * Update readme.md * Trunky trunk trunk * Clang and the trunk is done --- .../E80_RSSI_per_case.webp | Bin 0 -> 10512 bytes .../diy/nrf52_promicro_diy_tcxo/readme.md | 107 ++++++++++++++++++ .../diy/nrf52_promicro_diy_tcxo/rfswitch.h | 11 +- .../diy/nrf52_promicro_diy_tcxo/variant.h | 2 +- 4 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 variants/diy/nrf52_promicro_diy_tcxo/E80_RSSI_per_case.webp create mode 100644 variants/diy/nrf52_promicro_diy_tcxo/readme.md diff --git a/variants/diy/nrf52_promicro_diy_tcxo/E80_RSSI_per_case.webp b/variants/diy/nrf52_promicro_diy_tcxo/E80_RSSI_per_case.webp new file mode 100644 index 0000000000000000000000000000000000000000..825c3cbc04ac6bc3f08cb824750e8fa171bd06b3 GIT binary patch literal 10512 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%0ACt7_&<2f9OIt%BE zd6wxz0ZNWw^0*&m{o{Z6);^Po{;ZSp_FR8qpSWvkCCwKSq?}a}y?gplU^_y0SDe1< z!!HF%D%#NPn~T3U@|#JlYc9?LlAwy9PVwH>>6fm9k*zFT<*wvvPOzBJ$=`FQ zvg7&vA`-UsFbJ&vySUaDwp)DEZBNa}ikh148p4zbd<(!bSx&7R%r`j{^`4;zs!H8g zu`>`cL^bK*w#5-kxk20T4~bz(H!Cv>yCx0|@&z_!{^<@HN6Uu9{Hry8CyL{d-a;-`0fSn|1_!5VD7COfkdrT z(BEgQY(xEeX~UZMIuQ$~kxe#z-ms7<50!Q1iJ3#fn?P;XExA#IE6{qTimt2sOPh&MR^gzmpm_#YcTPxpC^6h z8GXOcjjVrm;pVRoidlLX~fU|%DE-|{ghjtcuc!W~AU@Ck(lF9=~c5|#uvzC;CS4#5Xo8!RC@ubk%va+BJ zEWi2)PTokM#9N`;eM^kKegKfJI#Mwb8QN+tjfxUZprL?&18E<%?4>e8!|Y^8pdEW6 zU_1#tpSK!bYxoU>c+B94cqEntuF)FFAS|_(#U#kZL=6sM{7gg5hv7Sc_E#YRp2xD0 zKQg;c@^hD%)yo2d&=yHs|Ye zoki`SeK8z&zbV<^X0odR7QphBtug15QV5{K#q-viNG>!4>c)^QPiR`w92K60?+`JC zCwJD4f;+&Amnf(97O?gkP8p9MUs%_r%qj?2H8pNCH9MZ%I~BagT#lPDXw%VMth%!e z5t_3oY4e^*-jmkrvTUz(2<0uF-xM##z1ihUEdm38t2 zoAB?9jd-~YA{4F6u8vpl@N{X|aDJ1m{ zZ2Y|6iejQM6^XggZni9E1=>*@W9Nw-ZtNwgbAThtfIlqvwYV`k4;xE(38kl$BZhdI z%a7MOzzxRC6s|idRx0eG>q^3*RWr~CeC#94|D=2T zH8Z%*^`7R8TyRfmYZC4CAq0Yn9619eN705wruT&y0Lar-qack0yBv&W7yHSv;kmP#Vn>Y(Y9CaF_TtijPAV;${2u_v$ zypPl{WOmh2MfQmrYWb-yA~z{ql}oHzGG;>OL@jYAm!3ywllncHg=`HqDCV-xQb;-F zNU5R17X|dAPzz5s8@-)o)Hy>;Jbdw(Q?~<@@wS7y)>K3MAI(kB zKCAWtf41?Ef-|Qj9Qr=I@T#z7M7ExMM@C)kQQMod`PAIuA3{8HuJ1s77wy_&=ie8%Y>8Ha53 zt4Hep5$M2wqjNObbC0tx1(Gb~L3JM+R{d*}Jx5u8Vse^8ziqpXK_te{G%xNa{ob8j%;S*phz|T`B0ZB|2w- zX{x9ud^@eF88^UFWZCPd*iNC5xi~*|%fT1K(68V*NXQk*1r@{chcnUZtT`1L(cg}Q1)lZ+vLo6LaQ^N{75f_R??}k1-+ePX5)TIQ1!*Mi3?ep! zy6x8N{A44<+$U;{f)h{-Hh|kLG^m^wy>pzi=-E$=e1@75OQQk)jq|~`?*xkslZ6!& z;T`-$rpar_7#XoDLgnOKV>3gblV^5*QRXSA&C9=Foe#^AF_gq!;ZEmU(n^Wbe-YUa6=Z87lPyS#21BzO7oTW}=`#|Gs zwGk2cxe7#Ao@rbHN&`#v?6Z1TN3Yscg;(w%zf1l@l#(oHS9<{f8SV2Ls z)*H2&gB?@W|Bwwn5^KF7VSO-jpl*Rv4DdwZ{`hB!?l^ZyveCK}nxz(!VzF`6#waDn=37WSQ1 z8k)>J`&{queTTU98A;BK_CDmz22*B)>t#*jAsbW;{{?NG^sbBrYc!*%pUDqqfHLW;Lp>^AmGq@^Mlqcv~VttW7S8w`oPSR(HHCnEKSJ`qeXl4;ahe~H0*M_D6DJM3~G zE=fQ1+-kRagL~T^A)_W@X?OV6QJkS2o~86$z~I;oRV00%WCNI06ZmMuZxSuZ!&<&} z$O|nGf5`2+Vf)0y+8~ri{+?YH4||zq6kI$3?A-C;+TI4)7#8KXQ?ah&{8}%Lz}y_g z2?^5+6M}FEf6ikUOxVR8clUwoczS|8uM>>F%wz0!4RxiH&9W>V(=U)mZ;y=oRK$2~ zF-6_jqPc?d0`g`&$J4Nz6AZh&s(I5dLa_={t;kc-kZ!pBtxhcMup95Ahp#(6Sht=; zejGeU9LV#6Z+bbv}3 z-D9ynIugZULY#_JGLRtls-UG}|C%jV^y<_f(GaeEA&0Gi>gg`=dV$$iy%=GVY@?(X zEIMz^c>4`%JjsMS1e(Mw(4L93u{G^5Ez-bwuFE4^6d& zx24U#0{CV(8dL8~7 zfd)bbG+`>rUSN2PS|oG)kXVPwH2*;-NFr3xyZ$*EeUcBVeQFx_i8}z@?K-!Qt^~~eF|%EZ&rZl z=T8MfDL)T&mFhDes+@NTL)j<=Tkd=)lGJPV5^6(i0YP2T`hc;?VewT@=zwgR(M-{v zC(Aa-eOeb$-ZDeQ?uE;Mwz}4mfsn_+gAZYmoQT>XU$JjWmAyf`$Al_-SPHPQf+D)5 zIX;?C=NTRnji)?@MB&-f7vJlmZ?mTOYWiQtNmTyqzh5K}xjy=un!zWYc|t?VBlqv( ziCjan*xK6UF>Z$bN6AW zZZIigi6}#er$^5-M`vvIdiMnea8><{ur`YZwe(y+PSyt!CQH$2`XPXF!+8sVm9V#z z+qzt{9Sv$;dA}-rvuD5#8Et?;-v;e$S&&SNImYJ$+aS9-gu+*W;#JcV8Mo@tk|3dV zb$z;xm3sl+gd+PHLG7^LUDvrYCGg#0 z`;6^`CV_e~y!H6)L(+}5eY1PzND59uB)YHfseggYvB{Y=#HT2I=PK*UmY3m+yOLV> zS*Fyxk;2KWtRi~yB#Q|<{QIAz=^=I430J4r5Ao3Z`Lo?1;&hJ*Xqyp+5}}zAHB0EX z;MSv)^e>ih0(1tWBw4)BO>7>S1F)Yn?}X_6$8BCv5_F|?-o*Z$$+R!U* zVh;Yic$&XjYa6fWx&R9nl+xz@3_7aASY6dUQ26bwf1jszQNUQ;6Z=spk}2mRS05yM z7k|_9DUqA9{AA+EvhAhCbdyIz8Bg@2pkO)0-_eto@%^Sn;2!_i>GjY1)&}%|SH`_6 zwu!B_l%I9NDk>GF!)?AY?mthp8EE?GZk*eLAtE#Z5l9j)W5775aG^O1;Y9aY*#?jv z+8jnFlVA|GDi2LY3FSgcS0<%G)#4>(Pb)i%V{uh8`URO*Z&q2!4vmgfi+t_#D)@Q6 z4Jh;ik33-i9)^&DdY1Z~F_@FtUB2E250y6-sO&1Ofxn*zBR zshKEQ!*#J*l4~3<|-o>1WhS^$OPB8TZCR-e10AjKqSTDKjWIA-Cj&5qd`^(#W z%~6_*^n49wfqMN`c;!k4|!BVQb9f>(h3|I{F z-VvTa6I-inNa<+C(iGCKNECQwDj-^B$-5__}o z>?pBuE0T2M9ev(kN?qG^Bz9#Hh@pFVP#$to$x%mBDwXSrRMn4)@1|1=6peFM^O;zx zY@BY|ICaZfW|4$JRDwnI$TZMTsQ5z4V)D^rThUCB=+EkS@3gf=U z7bj^wY2(keE#4cI4JQ(BV3{tb6-Y_xwgsVXE+*4X7<)77s z;m!OC;#&TVG|J)R(p1NAvjMzU z5m_1}d?~{GE&ow%p4S6Yz9h^~7JnC5MvRhDqtLa{hoT_?li=j+$%GzL_Klbe^q6$0 z;3wpO?lZcn_EA*kb1&ZgP}RS6m}|RNj?@mSwiKmkBpi>J@CfUAip=3P-}r6J&25Zy z9Gd7C7-PT>IOLQ4Gy6=&?Q9sWo|NC;rIcytya*K?WBD!QtM+RSE30{uCH8~x&m^yJ z;w@Ff=@%lSV64Y01U;FRt#;WMIL$G!_DZQtcFw-b&tj;BnxqvfzbbMAKhtjLd)gMb zqN#q`qbtQ=w?LM9YHzSm2HkdGwVpdhDLAV+BADf7DOi0`6$=a)W6Csy|6o^(_4x&f zwnLd%4p@3QR`^$hDMzqKIwBijoyH##4Js_bOjT z$m+=&#AolZ+DJLU&$y~8#Un<^mwy26;0wm^aQiNj5AZMtHsLo0gK7|;JbT`#rbiK% z+e5JIOgthyJ221J>MI#t#6k+h%Li~ku-Qt7RbHC}861L_mgOz3ieHKZ2PSd2?uYfn zsn^RMSe-eF+ktH;@|%)lOntaYLs3EN9?(<7&=drNp}6-!WSqA;KZ`-XLBY^U4F-L) zll5y9EyrMmXRR)moK0Gg9pJTsx>X_G2YM!nQI>r?h_{Z`Hc?TbxZoQ6^I5mIBYR$Z zA;VKGW7hp`TughFzL+~G16M8Ga+vml9v(6uVLl5q3C|`jk^VP}O!(^jolQ-slBY6X zb)ttJ#1fCCD+SYrM~5jzr#JV`8F83srAldw4q*m^07M=XXPxv)v9?QinPj!fM2-?# zj?IC$V19a-^vsN>HADvou}ta_OCzgv2jUDDW#GJ;d*DU-Q2KA!xvK|gOw%5yD{dAh zQm~1D3VMNgR?+Rt^8rKYpUb(2_reTw;S&V`KIcm#y73_=QjFf!lwqa)U^GStJvUcbE`BPSOGN0_?hy{ zRZsI?pX-dwl?pA>BBgi)y!PMMCUzMjslgUzNl1BGdPDqLn zv#EP4G5W7M@E8K8i*kSVyMM6~o zg-N$AKe9*xcN0yHdi?Ps#HFvjMcju!2JxX`!OO8(8qC>Eq;Vn`z$1bux44r-Mc|yw zgGB*MxAT7UIac0yQ2z|0N3!6J6=6#AOshQ<%fu`2F}w zBJw=|3Hf&Dx21*})@oT%iMR7~c9emoWXkHq!htoIa{@jR8a} ztt&mQYPWwG>}-et`KCCy6FDN-!qWI z#9z(~TuMA!#z;`zn4)8~L~6fYw|CgF&D2W;h?((zL|VTyPRlVT+J5>VItJ)OZ$tF2 zAC>bmk^A-3?c?V^rV@`>dU~i13vnl?To{dcD!$VL>2lk&?EG!>Ua~YaK3*xMP-I3k z{(1J*(NR26qNAR@=Uqj#QkB4{15PTrBKM(kMS_PLzv^h9ZC2>9t$@aZ&uz*b>kFpV zfK|w)_5p&cA?z0!=V=j!+NW@yrpbtFVq$J{G%GEtl91-RV`5USP;}ZFf%=v6MV#cf6f~h(7OhcZNZfKF_d#X#DV$Z0e z2Z+O8$mLlQ$FJ&wyN7;IO7W7qR*EXxE%~z&jN!Tw*uTfN|&M63-A9mR!S3fhGG!z|Pf9qYjl?w-3#{ z{tVM#-R2Vt7k1Hh*JCYr9VHx|`bCsfK)nC9v*PRN8BuNk78m`!n8g?oQz8fTDqt9H z<9ZLhxIHkkH`x1<4{)B>^!8YpZ7w`3buzg17JBC6uN%Hh5WSkDoC+G*9i`ybO6Fw9 z9Cfsb;WJh@ZK3u&aeuvHk_#P=W~J!HZ?tkE{vG){ja26s-+L;+UOMlyrol_5v*q|J zN89zzYSdNH`Wf#8*pm2QZm?w-!=m1lv^P3u-Qh_v=*i(b92fRb*1^XXlQxqQca#lh z6L(}kDlGN+Sh?(j{~B5*ZqHr0!&fn#Vg(q;M0#sOdVN8I&u=>95Kw-xyy0bhuD{FFr+I{GxW#@3a-9cS9QYE2O+kJI5u_`USK9T_Q9`4xU=f37c5;wNg3cIu{7 zY)MjfRurT|`dEEiDnS3^0RqP>b))XkSUI@&pV#_=OKPr_jQ||0vjt5Qzx@vvf(^s} t$Pa9hoC85TET5}#-W>pdanbnq*bV@Q1EnR9{)@`X18Dy + + The table of known modules is at the bottom of the variant.h, and reproduced here for convenience. + +| Mfr | Module | TCXO | RF Switch | Notes | +| ------------ | ---------------- | ---- | --------- | ------------------------------------- | +| Ebyte | E22-900M22S | Yes | Ext | | +| Ebyte | E22-900MM22S | No | Ext | | +| Ebyte | E22-900M30S | Yes | Ext | | +| Ebyte | E22-900M33S | Yes | Ext | MAX_POWER must be set to 8 for this | +| Ebyte | E220-900M22S | No | Ext | LLCC68, looks like DIO3 not connected | +| AI-Thinker | RA-01SH | No | Int | SX1262 | +| Heltec | HT-RA62 | Yes | Int | | +| NiceRF | Lora1262 | yes | Int | | +| Waveshare | Core1262-HF | yes | Ext | | +| Waveshare | LoRa Node Module | yes | Int | | +| Seeed | Wio-SX1262 | yes | Int | Sooooo cute! | +| AI-Thinker | RA-02 | No | Int | SX1278 **433mhz band only** | +| RF Solutions | RFM95 | No | Int | Untested | +| Ebyte | E80-900M2213S | Yes | Int | LR1121 radio | + + + +## LR1121 modules - E80 is the default + +The E80 from CDEbyte is the most obtainable module at present, and has been selected as the default option. + +Naturally, CDEbyte have chosen to ignore the generic Semtech impelementation of the RF switching logic and have supplied confusing and contradictory documentation, which is explained below. + +tl;dr: The E80 is chosen as the default. **If you wish to use another module, the table in `rfswitch.h` must be adjusted accordingly.** + +### E80 switching - the saga + +The CDEbyte implementation of the LR1121 is contained in their E80 module. As stated above, CDEbyte have chosen to ignore the generic Semtech implementation of the RF switching logic and have their own table, which is located at the bottom of the page [here](https://www.cdebyte.com/products/E80-900M2213S/2#Pin), and reflected on page 6 of their user manual, and reproduced below: + +| DIO5/RFSW0 | DIO6/RFSW1 | RF status | +| ---------- | ---------- | ----------------------------- | +| 0 | 0 | RX | +| 0 | 1 | TX (Sub-1GHz low power mode) | +| 1 | 0 | TX (Sub-1GHz high power mode) | +| 1 | 1 | TX(2.4GHz) | + +However, looking at the sample code they provide on page 9, the values would be: + +| DIO5/RFSW0 | DIO6/RFSW1 | RF status | +| ---------- | ---------- | ----------------------------- | +| 0 | 1 | RX | +| 1 | 1 | TX (Sub-1GHz low power mode) | +| 1 | 0 | TX (Sub-1GHz high power mode) | +| 0 | 0 | TX(2.4GHz) | + +The Semtech default, the values are (taken from [here](https://github.com/Lora-net/SWSD006/blob/v2.6.1/lib/app_subGHz_config_lr11xx.c#L145-L154)): + +
+ +```cpp + .rfswitch = { + .enable = LR11XX_SYSTEM_RFSW0_HIGH | LR11XX_SYSTEM_RFSW1_HIGH | LR11XX_SYSTEM_RFSW2_HIGH, + .standby = 0, + .rx = LR11XX_SYSTEM_RFSW0_HIGH, + .tx = LR11XX_SYSTEM_RFSW0_HIGH | LR11XX_SYSTEM_RFSW1_HIGH, + .tx_hp = LR11XX_SYSTEM_RFSW1_HIGH, + .tx_hf = 0, + .gnss = LR11XX_SYSTEM_RFSW2_HIGH, + .wifi = 0, + }, +``` + +
+ +| DIO5/RFSW0 | DIO6/RFSW1 | RF status | +| ---------- | ---------- | ----------------------------- | +| 1 | 0 | RX | +| 1 | 1 | TX (Sub-1GHz low power mode) | +| 0 | 1 | TX (Sub-1GHz high power mode) | +| 0 | 0 | TX(2.4GHz) | + +It is evident from the tables above that there is no real consistency to those provided by Ebyte. + +#### An experiment + +Tests were conducted in each of the three configurations between a known-good SX1262 and an E80, passing packets in both directions and recording the reported RSSI. The E80 was set at 22db and 14db to activate the high and low power settings respectively. The results are shown in the chart below. + +![Chart showing RSSI readings in each configuration and setting](./E80_RSSI_per_case.webp) + +## Conclusion + +The RF switching is based on the code example given. Logically, this shows the DIO5 and DIO6 are swapped compared to the reference design. + +If future DIYers wish to use an alternative module, the table in `rfswitch.h` must be adjusted accordingly. diff --git a/variants/diy/nrf52_promicro_diy_tcxo/rfswitch.h b/variants/diy/nrf52_promicro_diy_tcxo/rfswitch.h index 2258c3135..71508c037 100644 --- a/variants/diy/nrf52_promicro_diy_tcxo/rfswitch.h +++ b/variants/diy/nrf52_promicro_diy_tcxo/rfswitch.h @@ -1,17 +1,20 @@ #include "RadioLib.h" +// This is rewritten to match the requirements of the E80-900M2213S +// The E80 does not conform to the reference Semtech switches(!) and therefore needs a custom matrix. +// See footnote #3 in "https://www.cdebyte.com/products/E80-900M2213S/2#Pin" // RF Switch Matrix SubG RFO_HP_LF / RFO_LP_LF / RFI_[NP]_LF0 // DIO5 -> RFSW0_V1 // DIO6 -> RFSW1_V2 -// DIO7 -> ANT_CTRL_ON + ESP_IO9/LR_GPS_ANT_DC_EN -> RFI_GPS (Bias-T GPS) (LR11x0 only) +// DIO7 -> not connected on E80 module - note that GNSS and Wifi scanning are not possible. static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 DIO7 - {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW, LOW}}, - {LR11x0::MODE_TX, {LOW, HIGH, LOW}}, {LR11x0::MODE_TX_HP, {LOW, HIGH, LOW}}, + {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, END_OF_MODE_TABLE, -}; \ No newline at end of file +}; diff --git a/variants/diy/nrf52_promicro_diy_tcxo/variant.h b/variants/diy/nrf52_promicro_diy_tcxo/variant.h index 5e939c023..b74b100a3 100644 --- a/variants/diy/nrf52_promicro_diy_tcxo/variant.h +++ b/variants/diy/nrf52_promicro_diy_tcxo/variant.h @@ -193,4 +193,4 @@ settings. * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif From 8cacdb65d6fe9ce943824e54cc741b2785ec3014 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Mon, 3 Feb 2025 22:39:42 +0800 Subject: [PATCH 35/71] Fix INA226 Sensor Voltage Readings (#5972) They were off by a factor of 1000 due to the difference between Volts and MilliVolts, as reported by @morcant . Fixes https://github.com/meshtastic/firmware/issues/5969 --- src/modules/Telemetry/Sensor/INA226Sensor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/Telemetry/Sensor/INA226Sensor.cpp b/src/modules/Telemetry/Sensor/INA226Sensor.cpp index 1ee7cd92e..8b1cded60 100644 --- a/src/modules/Telemetry/Sensor/INA226Sensor.cpp +++ b/src/modules/Telemetry/Sensor/INA226Sensor.cpp @@ -40,14 +40,14 @@ bool INA226Sensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.environment_metrics.has_current = true; // mV conversion to V - measurement->variant.environment_metrics.voltage = ina226.getBusVoltage() / 1000; + measurement->variant.environment_metrics.voltage = ina226.getBusVoltage(); measurement->variant.environment_metrics.current = ina226.getCurrent_mA(); return true; } uint16_t INA226Sensor::getBusVoltageMv() { - return lround(ina226.getBusVoltage()); + return lround(ina226.getBusVoltage() * 1000); } int16_t INA226Sensor::getCurrentMa() From 5c17afb2ac6b40f6cf4a20b8c86f1f906df47273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 3 Feb 2025 16:36:05 +0100 Subject: [PATCH 36/71] Clean up some legacy macro definitions (#5983) --- src/graphics/EInkDynamicDisplay.cpp | 4 ++-- variants/heltec_vision_master_e213/platformio.ini | 4 ---- variants/heltec_vision_master_e290/platformio.ini | 6 +----- variants/heltec_wireless_paper/platformio.ini | 4 ---- variants/heltec_wireless_paper_v1/platformio.ini | 4 ---- variants/t-echo/platformio.ini | 3 --- variants/tlora_t3s3_epaper/platformio.ini | 5 ----- 7 files changed, 3 insertions(+), 27 deletions(-) diff --git a/src/graphics/EInkDynamicDisplay.cpp b/src/graphics/EInkDynamicDisplay.cpp index 6664646b9..47012ca47 100644 --- a/src/graphics/EInkDynamicDisplay.cpp +++ b/src/graphics/EInkDynamicDisplay.cpp @@ -238,7 +238,7 @@ void EInkDynamicDisplay::checkRateLimiting() // Skip update: too soon for BACKGROUND if (frameFlags == BACKGROUND) { - if (Throttle::isWithinTimespanMs(previousRunMs, EINK_LIMIT_RATE_BACKGROUND_SEC * 1000)) { + if (Throttle::isWithinTimespanMs(previousRunMs, 30000)) { refresh = SKIPPED; reason = EXCEEDED_RATELIMIT_FULL; return; @@ -251,7 +251,7 @@ void EInkDynamicDisplay::checkRateLimiting() // Skip update: too soon for RESPONSIVE if (frameFlags & RESPONSIVE) { - if (Throttle::isWithinTimespanMs(previousRunMs, EINK_LIMIT_RATE_RESPONSIVE_SEC * 1000)) { + if (Throttle::isWithinTimespanMs(previousRunMs, 1000)) { refresh = SKIPPED; reason = EXCEEDED_RATELIMIT_FAST; LOG_DEBUG("refresh=SKIPPED, reason=EXCEEDED_RATELIMIT_FAST, frameFlags=0x%x", frameFlags); diff --git a/variants/heltec_vision_master_e213/platformio.ini b/variants/heltec_vision_master_e213/platformio.ini index 709ae321f..cc6f283b5 100644 --- a/variants/heltec_vision_master_e213/platformio.ini +++ b/variants/heltec_vision_master_e213/platformio.ini @@ -10,12 +10,8 @@ build_flags = -DEINK_HEIGHT=122 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted - -DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates - -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates -; -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. -DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" - -DEINK_HASQUIRK_WEAKFASTREFRESH ; Pixels set with fast-refresh are easy to clear, disrupted by sunlight lib_deps = ${esp32s3_base.lib_deps} https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d diff --git a/variants/heltec_vision_master_e290/platformio.ini b/variants/heltec_vision_master_e290/platformio.ini index e1ba100ae..06804e4f2 100644 --- a/variants/heltec_vision_master_e290/platformio.ini +++ b/variants/heltec_vision_master_e290/platformio.ini @@ -11,12 +11,8 @@ build_flags = -D EINK_HEIGHT=128 -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -D EINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted - -D EINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates - -D EINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates -D EINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" - -D EINK_HASQUIRK_WEAKFASTREFRESH ; Pixels set with fast-refresh are easy to clear, disrupted by sunlight -; -D EINK_LIMIT_GHOSTING_PX=2000 ; How much image ghosting is tolerated -; -D EINK_BACKGROUND_USES_FAST ; (If enabled) don't redraw RESPONSIVE frames at next BACKGROUND update + lib_deps = ${esp32s3_base.lib_deps} diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index afbbd8be9..a7045b182 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -10,12 +10,8 @@ build_flags = -D EINK_HEIGHT=122 -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -D EINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted - -D EINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates - -D EINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates -; -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated -D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. -D EINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" - -D EINK_HASQUIRK_WEAKFASTREFRESH ; Pixels set with fast-refresh are easy to clear, disrupted by sunlight lib_deps = ${esp32s3_base.lib_deps} https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d diff --git a/variants/heltec_wireless_paper_v1/platformio.ini b/variants/heltec_wireless_paper_v1/platformio.ini index c94bcacca..2ce7559f9 100644 --- a/variants/heltec_wireless_paper_v1/platformio.ini +++ b/variants/heltec_wireless_paper_v1/platformio.ini @@ -11,11 +11,7 @@ build_flags = -D EINK_HEIGHT=122 -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -D EINK_LIMIT_FASTREFRESH=5 ; How many consecutive fast-refreshes are permitted - -D EINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates - -D EINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated - ;-D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. - -D EINK_HASQUIRK_VICIOUSFASTREFRESH ; Identify that pixels drawn by fast-refresh are harder to clear lib_deps = ${esp32s3_base.lib_deps} https://github.com/meshtastic/GxEPD2#55f618961db45a23eff0233546430f1e5a80f63a diff --git a/variants/t-echo/platformio.ini b/variants/t-echo/platformio.ini index 5b295c96a..ce58c0b88 100644 --- a/variants/t-echo/platformio.ini +++ b/variants/t-echo/platformio.ini @@ -14,9 +14,6 @@ build_flags = ${nrf52840_base.build_flags} -Ivariants/t-echo -DEINK_HEIGHT=200 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted - -DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates - -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates -; -DEINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. build_src_filter = ${nrf52_base.build_src_filter} +<../variants/t-echo> diff --git a/variants/tlora_t3s3_epaper/platformio.ini b/variants/tlora_t3s3_epaper/platformio.ini index ceb4fbaf5..3f3b3fe50 100644 --- a/variants/tlora_t3s3_epaper/platformio.ini +++ b/variants/tlora_t3s3_epaper/platformio.ini @@ -12,11 +12,6 @@ build_flags = -DEINK_HEIGHT=122 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted - -DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates - -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates - -DEINK_HASQUIRK_VICIOUSFASTREFRESH ; Identify that pixels drawn by fast-refresh are harder to clear - ;-DEINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated - ;-DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. lib_deps = ${esp32s3_base.lib_deps} From a3a295488c5f6092494f139fab93ac4926981949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 3 Feb 2025 16:48:10 +0100 Subject: [PATCH 37/71] add firmware build script for use with docker --- bin/build-firmware.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 bin/build-firmware.sh diff --git a/bin/build-firmware.sh b/bin/build-firmware.sh new file mode 100644 index 000000000..c53f1b660 --- /dev/null +++ b/bin/build-firmware.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini + +export PIP_BREAK_SYSTEM_PACKAGES=1 + +if (echo $2 | grep -q "esp32"); then + bin/build-esp32.sh $1 +elif (echo $2 | grep -q "nrf52"); then + bin/build-nrf52.sh $1 +elif (echo $2 | grep -q "stm32"); then + bin/build-stm32.sh $1 +elif (echo $2 | grep -q "rpi2040"); then + bin/build-rpi2040.sh $1 +else + echo "Unknown target $2" + exit 1 +fi \ No newline at end of file From 1b457bcfbb36a4032f853c06512f2ac7936fad3a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 20:47:51 -0600 Subject: [PATCH 38/71] [create-pull-request] automated change (#5985) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/config.pb.h | 13 +++++++++---- src/mesh/generated/meshtastic/localonly.pb.h | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/protobufs b/protobufs index 7f13df0e5..b80785b16 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 7f13df0e5f7cbb07f0e6f3a57c0d86ad448738db +Subproject commit b80785b16bc0d243b97917998706e7bf209cd9d0 diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 14aed9dfe..4747ddb5a 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -468,6 +468,9 @@ typedef struct _meshtastic_Config_DisplayConfig { bool wake_on_tap_or_motion; /* Indicates how to rotate or invert the compass output to accurate display on the display. */ meshtastic_Config_DisplayConfig_CompassOrientation compass_orientation; + /* If false (default), the device will display the time in 24-hour format on screen. + If true, the device will display the time in 12-hour format on screen. */ + bool use_12h_clock; } meshtastic_Config_DisplayConfig; /* Lora Config */ @@ -690,7 +693,7 @@ extern "C" { #define meshtastic_Config_PowerConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0} -#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN} +#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0} #define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} #define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0} #define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0} @@ -701,7 +704,7 @@ extern "C" { #define meshtastic_Config_PowerConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0} -#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN} +#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0} #define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} #define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0} #define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0} @@ -765,6 +768,7 @@ extern "C" { #define meshtastic_Config_DisplayConfig_heading_bold_tag 9 #define meshtastic_Config_DisplayConfig_wake_on_tap_or_motion_tag 10 #define meshtastic_Config_DisplayConfig_compass_orientation_tag 11 +#define meshtastic_Config_DisplayConfig_use_12h_clock_tag 12 #define meshtastic_Config_LoRaConfig_use_preset_tag 1 #define meshtastic_Config_LoRaConfig_modem_preset_tag 2 #define meshtastic_Config_LoRaConfig_bandwidth_tag 3 @@ -907,7 +911,8 @@ X(a, STATIC, SINGULAR, UENUM, oled, 7) \ X(a, STATIC, SINGULAR, UENUM, displaymode, 8) \ X(a, STATIC, SINGULAR, BOOL, heading_bold, 9) \ X(a, STATIC, SINGULAR, BOOL, wake_on_tap_or_motion, 10) \ -X(a, STATIC, SINGULAR, UENUM, compass_orientation, 11) +X(a, STATIC, SINGULAR, UENUM, compass_orientation, 11) \ +X(a, STATIC, SINGULAR, BOOL, use_12h_clock, 12) #define meshtastic_Config_DisplayConfig_CALLBACK NULL #define meshtastic_Config_DisplayConfig_DEFAULT NULL @@ -985,7 +990,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg; #define MESHTASTIC_MESHTASTIC_CONFIG_PB_H_MAX_SIZE meshtastic_Config_size #define meshtastic_Config_BluetoothConfig_size 10 #define meshtastic_Config_DeviceConfig_size 98 -#define meshtastic_Config_DisplayConfig_size 30 +#define meshtastic_Config_DisplayConfig_size 32 #define meshtastic_Config_LoRaConfig_size 85 #define meshtastic_Config_NetworkConfig_IpV4Config_size 20 #define meshtastic_Config_NetworkConfig_size 202 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index dc0f507c9..7a6712bf0 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -187,7 +187,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size -#define meshtastic_LocalConfig_size 741 +#define meshtastic_LocalConfig_size 743 #define meshtastic_LocalModuleConfig_size 699 #ifdef __cplusplus From 447533aae5b82398e8dcd648745a4ff1a3c0a8ac Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 4 Feb 2025 07:38:54 -0500 Subject: [PATCH 39/71] meshtasticd-debian: Remove existing deb builds (#5792) Replaced with OpenSUSE Build Service https://build.opensuse.org/project/show/network:Meshtastic --- .github/workflows/build_native.yml | 38 -------- .github/workflows/build_raspbian.yml | 52 ----------- .github/workflows/build_raspbian_armv7l.yml | 52 ----------- .github/workflows/main_matrix.yml | 32 +------ .github/workflows/package_amd64.yml | 90 ------------------- .github/workflows/package_raspbian.yml | 90 ------------------- .github/workflows/package_raspbian_armv7l.yml | 90 ------------------- .github/workflows/release_channels.yml | 12 +-- 8 files changed, 9 insertions(+), 447 deletions(-) delete mode 100644 .github/workflows/build_native.yml delete mode 100644 .github/workflows/build_raspbian.yml delete mode 100644 .github/workflows/build_raspbian_armv7l.yml delete mode 100644 .github/workflows/package_amd64.yml delete mode 100644 .github/workflows/package_raspbian.yml delete mode 100644 .github/workflows/package_raspbian_armv7l.yml diff --git a/.github/workflows/build_native.yml b/.github/workflows/build_native.yml deleted file mode 100644 index cca839328..000000000 --- a/.github/workflows/build_native.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Build Native - -on: workflow_call - -permissions: - contents: write - packages: write - -jobs: - build-native: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Setup native build - id: base - uses: ./.github/actions/setup-native - - - name: Build Native - run: bin/build-native.sh - - - name: Get release version string - run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT - id: version - - - name: Store binaries as an artifact - uses: actions/upload-artifact@v4 - with: - name: firmware-native-${{ steps.version.outputs.long }}.zip - overwrite: true - path: | - release/meshtasticd_linux_x86_64 - bin/config-dist.yaml diff --git a/.github/workflows/build_raspbian.yml b/.github/workflows/build_raspbian.yml deleted file mode 100644 index 646c6c9f3..000000000 --- a/.github/workflows/build_raspbian.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Build Raspbian - -on: workflow_call - -permissions: - contents: write - packages: write - -jobs: - build-raspbian: - runs-on: [self-hosted, linux, ARM64] - steps: - - name: Install libbluetooth - shell: bash - run: | - sudo apt-get update -y --fix-missing - sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev - - - name: Checkout code - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Upgrade python tools - shell: bash - run: | - python -m pip install --upgrade pip - pip install -U platformio adafruit-nrfutil - pip install -U meshtastic --pre - - - name: Upgrade platformio - shell: bash - run: | - pio upgrade - - - name: Build Raspbian - run: bin/build-native.sh - - - name: Get release version string - run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT - id: version - - - name: Store binaries as an artifact - uses: actions/upload-artifact@v4 - with: - name: firmware-raspbian-${{ steps.version.outputs.long }}.zip - overwrite: true - path: | - release/meshtasticd_linux_aarch64 - bin/config-dist.yaml diff --git a/.github/workflows/build_raspbian_armv7l.yml b/.github/workflows/build_raspbian_armv7l.yml deleted file mode 100644 index 21b1aea79..000000000 --- a/.github/workflows/build_raspbian_armv7l.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Build Raspbian Arm - -on: workflow_call - -permissions: - contents: write - packages: write - -jobs: - build-raspbian-armv7l: - runs-on: [self-hosted, linux, ARM] - steps: - - name: Install libbluetooth - shell: bash - run: | - sudo apt-get update -y --fix-missing - sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev - - - name: Checkout code - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Upgrade python tools - shell: bash - run: | - python -m pip install --upgrade pip - pip install -U platformio adafruit-nrfutil - pip install -U meshtastic --pre - - - name: Upgrade platformio - shell: bash - run: | - pio upgrade - - - name: Build Raspbian - run: bin/build-native.sh - - - name: Get release version string - run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT - id: version - - - name: Store binaries as an artifact - uses: actions/upload-artifact@v4 - with: - name: firmware-raspbian-armv7l-${{ steps.version.outputs.long }}.zip - overwrite: true - path: | - release/meshtasticd_linux_armv7l - bin/config-dist.yaml diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index a9678f4fc..b13866435 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -128,15 +128,6 @@ jobs: with: board: ${{ matrix.board }} - package-raspbian: - uses: ./.github/workflows/package_raspbian.yml - - package-raspbian-armv7l: - uses: ./.github/workflows/package_raspbian_armv7l.yml - - package-native: - uses: ./.github/workflows/package_amd64.yml - build-debian-src: uses: ./.github/workflows/build_debian_src.yml with: @@ -158,7 +149,7 @@ jobs: docker-alpine-amd64: uses: ./.github/workflows/docker_build.yml with: - distro: debian + distro: alpine platform: linux/amd64 runs-on: ubuntu-24.04 push: false @@ -288,14 +279,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' }} outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} - needs: - [ - gather-artifacts, - package-raspbian, - package-raspbian-armv7l, - package-native, - build-debian-src, - ] + needs: [gather-artifacts, build-debian-src] steps: - name: Checkout uses: actions/checkout@v4 @@ -324,13 +308,6 @@ jobs: body: | Autogenerated by github action, developer should edit as required before publishing... - - name: Download deb files - uses: actions/download-artifact@v4 - with: - pattern: meshtasticd_${{ steps.version.outputs.long }}_*.deb - merge-multiple: true - path: ./output - - name: Download source deb uses: actions/download-artifact@v4 with: @@ -346,11 +323,8 @@ jobs: - name: Display structure of downloaded files run: ls -lR - - name: Add deb files to release + - name: Add source deb to release run: | - gh release upload v${{ steps.version.outputs.long }} ./output/meshtasticd_${{ steps.version.outputs.long }}_arm64.deb - gh release upload v${{ steps.version.outputs.long }} ./output/meshtasticd_${{ steps.version.outputs.long }}_armhf.deb - gh release upload v${{ steps.version.outputs.long }} ./output/meshtasticd_${{ steps.version.outputs.long }}_amd64.deb gh release upload v${{ steps.version.outputs.long }} ./output/meshtasticd-${{ steps.version.outputs.deb }}-src.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/package_amd64.yml b/.github/workflows/package_amd64.yml deleted file mode 100644 index d9f041736..000000000 --- a/.github/workflows/package_amd64.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: Package Native - -on: - workflow_call: - workflow_dispatch: - -permissions: - contents: write - packages: write - -jobs: - build-native: - uses: ./.github/workflows/build_native.yml - - package-native: - runs-on: ubuntu-22.04 - needs: build-native - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Pull web ui - uses: dsaltares/fetch-gh-release-asset@master - with: - repo: meshtastic/web - file: build.tar - target: build.tar - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Get release version string - run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT - id: version - - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - name: firmware-native-${{ steps.version.outputs.long }}.zip - merge-multiple: true - - - name: Display structure of downloaded files - run: ls -R - - - name: build .debpkg - run: | - mkdir -p .debpkg/DEBIAN - mkdir -p .debpkg/usr/share/meshtasticd/web - mkdir -p .debpkg/usr/sbin - mkdir -p .debpkg/etc/meshtasticd - mkdir -p .debpkg/etc/meshtasticd/config.d - mkdir -p .debpkg/etc/meshtasticd/available.d - mkdir -p .debpkg/usr/lib/systemd/system/ - tar -xf build.tar -C .debpkg/usr/share/meshtasticd/web - shopt -s dotglob nullglob - if [ -d .debpkg/usr/share/meshtasticd/web/build ]; then mv .debpkg/usr/share/meshtasticd/web/build/* .debpkg/usr/share/meshtasticd/web/; fi - if [ -d .debpkg/usr/share/meshtasticd/web/build ]; then rmdir .debpkg/usr/share/meshtasticd/web/build; fi - if [ -d .debpkg/usr/share/meshtasticd/web/.DS_Store ]; then rm -f .debpkg/usr/share/meshtasticd/web/.DS_Store; fi - gunzip .debpkg/usr/share/meshtasticd/web/ -r - cp release/meshtasticd_linux_x86_64 .debpkg/usr/sbin/meshtasticd - cp bin/config-dist.yaml .debpkg/etc/meshtasticd/config.yaml - cp bin/config.d/* .debpkg/etc/meshtasticd/available.d/ -r - chmod +x .debpkg/usr/sbin/meshtasticd - cp bin/meshtasticd.service .debpkg/usr/lib/systemd/system/meshtasticd.service - echo "/etc/meshtasticd/config.yaml" > .debpkg/DEBIAN/conffiles - chmod +x .debpkg/DEBIAN/conffiles - # Transition /usr/share/doc/meshtasticd to /usr/share/meshtasticd - echo "rm -rf /usr/share/doc/meshtasticd" > .debpkg/DEBIAN/preinst - chmod +x .debpkg/DEBIAN/preinst - echo "ln -sf /usr/share/meshtasticd /usr/share/doc/meshtasticd" > .debpkg/DEBIAN/postinst - chmod +x .debpkg/DEBIAN/postinst - - - uses: jiro4989/build-deb-action@v3 - with: - package: meshtasticd - package_root: .debpkg - maintainer: Jonathan Bennett - version: ${{ steps.version.outputs.long }} # refs/tags/v*.*.* - arch: amd64 - depends: libyaml-cpp0.7, openssl, libulfius2.7, libi2c0 - desc: Native Linux Meshtastic binary. - - - uses: actions/upload-artifact@v4 - with: - name: meshtasticd_${{ steps.version.outputs.long }}_amd64.deb - overwrite: true - path: | - ./*.deb diff --git a/.github/workflows/package_raspbian.yml b/.github/workflows/package_raspbian.yml deleted file mode 100644 index 62613f85f..000000000 --- a/.github/workflows/package_raspbian.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: Package Raspbian - -on: - workflow_call: - workflow_dispatch: - -permissions: - contents: write - packages: write - -jobs: - build-raspbian: - uses: ./.github/workflows/build_raspbian.yml - - package-raspbian: - runs-on: ubuntu-22.04 - needs: build-raspbian - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Pull web ui - uses: dsaltares/fetch-gh-release-asset@master - with: - repo: meshtastic/web - file: build.tar - target: build.tar - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Get release version string - run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT - id: version - - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - name: firmware-raspbian-${{ steps.version.outputs.long }}.zip - merge-multiple: true - - - name: Display structure of downloaded files - run: ls -R - - - name: build .debpkg - run: | - mkdir -p .debpkg/DEBIAN - mkdir -p .debpkg/usr/share/meshtasticd/web - mkdir -p .debpkg/usr/sbin - mkdir -p .debpkg/etc/meshtasticd - mkdir -p .debpkg/etc/meshtasticd/config.d - mkdir -p .debpkg/etc/meshtasticd/available.d - mkdir -p .debpkg/usr/lib/systemd/system/ - tar -xf build.tar -C .debpkg/usr/share/meshtasticd/web - shopt -s dotglob nullglob - if [ -d .debpkg/usr/share/meshtasticd/web/build ]; then mv .debpkg/usr/share/meshtasticd/web/build/* .debpkg/usr/share/meshtasticd/web/; fi - if [ -d .debpkg/usr/share/meshtasticd/web/build ]; then rmdir .debpkg/usr/share/meshtasticd/web/build; fi - if [ -d .debpkg/usr/share/meshtasticd/web/.DS_Store ]; then rm -f .debpkg/usr/share/meshtasticd/web/.DS_Store; fi - gunzip .debpkg/usr/share/meshtasticd/web/ -r - cp release/meshtasticd_linux_aarch64 .debpkg/usr/sbin/meshtasticd - cp bin/config-dist.yaml .debpkg/etc/meshtasticd/config.yaml - cp bin/config.d/* .debpkg/etc/meshtasticd/available.d/ -r - chmod +x .debpkg/usr/sbin/meshtasticd - cp bin/meshtasticd.service .debpkg/usr/lib/systemd/system/meshtasticd.service - echo "/etc/meshtasticd/config.yaml" > .debpkg/DEBIAN/conffiles - chmod +x .debpkg/DEBIAN/conffiles - # Transition /usr/share/doc/meshtasticd to /usr/share/meshtasticd - echo "rm -rf /usr/share/doc/meshtasticd" > .debpkg/DEBIAN/preinst - chmod +x .debpkg/DEBIAN/preinst - echo "ln -sf /usr/share/meshtasticd /usr/share/doc/meshtasticd" > .debpkg/DEBIAN/postinst - chmod +x .debpkg/DEBIAN/postinst - - - uses: jiro4989/build-deb-action@v3 - with: - package: meshtasticd - package_root: .debpkg - maintainer: Jonathan Bennett - version: ${{ steps.version.outputs.long }} # refs/tags/v*.*.* - arch: arm64 - depends: libyaml-cpp0.7, openssl, libulfius2.7, libi2c0 - desc: Native Linux Meshtastic binary. - - - uses: actions/upload-artifact@v4 - with: - name: meshtasticd_${{ steps.version.outputs.long }}_arm64.deb - overwrite: true - path: | - ./*.deb diff --git a/.github/workflows/package_raspbian_armv7l.yml b/.github/workflows/package_raspbian_armv7l.yml deleted file mode 100644 index 8a9df1710..000000000 --- a/.github/workflows/package_raspbian_armv7l.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: Package Raspbian - -on: - workflow_call: - workflow_dispatch: - -permissions: - contents: write - packages: write - -jobs: - build-raspbian_armv7l: - uses: ./.github/workflows/build_raspbian_armv7l.yml - - package-raspbian_armv7l: - runs-on: ubuntu-22.04 - needs: build-raspbian_armv7l - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Pull web ui - uses: dsaltares/fetch-gh-release-asset@master - with: - repo: meshtastic/web - file: build.tar - target: build.tar - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Get release version string - run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT - id: version - - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - name: firmware-raspbian-armv7l-${{ steps.version.outputs.long }}.zip - merge-multiple: true - - - name: Display structure of downloaded files - run: ls -R - - - name: build .debpkg - run: | - mkdir -p .debpkg/DEBIAN - mkdir -p .debpkg/usr/share/meshtasticd/web - mkdir -p .debpkg/usr/sbin - mkdir -p .debpkg/etc/meshtasticd - mkdir -p .debpkg/etc/meshtasticd/config.d - mkdir -p .debpkg/etc/meshtasticd/available.d - mkdir -p .debpkg/usr/lib/systemd/system/ - tar -xf build.tar -C .debpkg/usr/share/meshtasticd/web - shopt -s dotglob nullglob - if [ -d .debpkg/usr/share/meshtasticd/web/build ]; then mv .debpkg/usr/share/meshtasticd/web/build/* .debpkg/usr/share/meshtasticd/web/; fi - if [ -d .debpkg/usr/share/meshtasticd/web/build ]; then rmdir .debpkg/usr/share/meshtasticd/web/build; fi - if [ -d .debpkg/usr/share/meshtasticd/web/.DS_Store ]; then rm -f .debpkg/usr/share/meshtasticd/web/.DS_Store; fi - gunzip .debpkg/usr/share/meshtasticd/web/ -r - cp release/meshtasticd_linux_armv7l .debpkg/usr/sbin/meshtasticd - cp bin/config-dist.yaml .debpkg/etc/meshtasticd/config.yaml - cp bin/config.d/* .debpkg/etc/meshtasticd/available.d/ -r - chmod +x .debpkg/usr/sbin/meshtasticd - cp bin/meshtasticd.service .debpkg/usr/lib/systemd/system/meshtasticd.service - echo "/etc/meshtasticd/config.yaml" > .debpkg/DEBIAN/conffiles - chmod +x .debpkg/DEBIAN/conffiles - # Transition /usr/share/doc/meshtasticd to /usr/share/meshtasticd - echo "rm -rf /usr/share/doc/meshtasticd" > .debpkg/DEBIAN/preinst - chmod +x .debpkg/DEBIAN/preinst - echo "ln -sf /usr/share/meshtasticd /usr/share/doc/meshtasticd" > .debpkg/DEBIAN/postinst - chmod +x .debpkg/DEBIAN/postinst - - - uses: jiro4989/build-deb-action@v3 - with: - package: meshtasticd - package_root: .debpkg - maintainer: Jonathan Bennett - version: ${{ steps.version.outputs.long }} # refs/tags/v*.*.* - arch: armhf - depends: libyaml-cpp0.7, openssl, libulfius2.7, libi2c0 - desc: Native Linux Meshtastic binary. - - - uses: actions/upload-artifact@v4 - with: - name: meshtasticd_${{ steps.version.outputs.long }}_armhf.deb - overwrite: true - path: | - ./*.deb diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index a3a105d6d..9cdabde9e 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -37,9 +37,9 @@ jobs: ${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} secrets: inherit - # hook-copr: - # uses: ./.github/workflows/hook_copr.yml - # with: - # copr_project: |- - # ${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} - # secrets: inherit + hook-copr: + uses: ./.github/workflows/hook_copr.yml + with: + copr_project: |- + ${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} + secrets: inherit From 1c8eb7ece3895f608a2da84b6e8e47050e639991 Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 5 Feb 2025 16:19:22 -0500 Subject: [PATCH 40/71] meshtasticd: Fix web download location (#5993) --- debian/ci_pack_sdeb.sh | 2 +- meshtasticd.spec.rpkg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/ci_pack_sdeb.sh b/debian/ci_pack_sdeb.sh index 1f311af93..a8b2252ae 100755 --- a/debian/ci_pack_sdeb.sh +++ b/debian/ci_pack_sdeb.sh @@ -11,7 +11,7 @@ platformio pkg install -e native -t platformio/tool-scons@4.40502.0 tar -cf pio.tar pio/ rm -rf pio # Download the latest meshtastic/web release build.tar to `web.tar` -curl -L https://github.com/meshtastic/web/releases/download/latest/build.tar -o web.tar +curl -L https://github.com/meshtastic/web/releases/latest/download/build.tar -o web.tar package=$(dpkg-parsechangelog --show-field Source) diff --git a/meshtasticd.spec.rpkg b/meshtasticd.spec.rpkg index 720e94408..0a0f03557 100644 --- a/meshtasticd.spec.rpkg +++ b/meshtasticd.spec.rpkg @@ -21,7 +21,7 @@ Summary: Meshtastic daemon for communicating with Meshtastic devices License: GPL-3.0 URL: https://github.com/meshtastic/firmware Source0: {{{ git_dir_pack }}} -Source1: https://github.com/meshtastic/web/releases/download/latest/build.tar +Source1: https://github.com/meshtastic/web/releases/latest/download/build.tar BuildRequires: systemd-rpm-macros BuildRequires: python3-devel From 64def246eeeee4f9139e4c576da92a9faced8853 Mon Sep 17 00:00:00 2001 From: Tom <116762865+NomDeTom@users.noreply.github.com> Date: Thu, 6 Feb 2025 03:36:04 +0000 Subject: [PATCH 41/71] Corrected some misinformation (#5995) Change the module text too soon , before it had chance to reach a final conclusion. Co-authored-by: Tom <116762865+Nestpebble@users.noreply.github.com> --- .../diy/nrf52_promicro_diy_tcxo/readme.md | 2 +- .../diy/nrf52_promicro_diy_tcxo/variant.h | 40 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/variants/diy/nrf52_promicro_diy_tcxo/readme.md b/variants/diy/nrf52_promicro_diy_tcxo/readme.md index 4da6566ec..585ac36de 100644 --- a/variants/diy/nrf52_promicro_diy_tcxo/readme.md +++ b/variants/diy/nrf52_promicro_diy_tcxo/readme.md @@ -31,7 +31,7 @@ Also worth noting that the Seeed WIO SX1262 in particular only has RXEN exposed | NiceRF | Lora1262 | yes | Int | | | Waveshare | Core1262-HF | yes | Ext | | | Waveshare | LoRa Node Module | yes | Int | | -| Seeed | Wio-SX1262 | yes | Int | Sooooo cute! | +| Seeed | Wio-SX1262 | yes | Ext | Cute! DIO2/TXEN are not exposed | | AI-Thinker | RA-02 | No | Int | SX1278 **433mhz band only** | | RF Solutions | RFM95 | No | Int | Untested | | Ebyte | E80-900M2213S | Yes | Int | LR1121 radio | diff --git a/variants/diy/nrf52_promicro_diy_tcxo/variant.h b/variants/diy/nrf52_promicro_diy_tcxo/variant.h index b74b100a3..de49018f4 100644 --- a/variants/diy/nrf52_promicro_diy_tcxo/variant.h +++ b/variants/diy/nrf52_promicro_diy_tcxo/variant.h @@ -22,26 +22,26 @@ extern "C" { /* NRF52 PRO MICRO PIN ASSIGNMENT -| Pin   | Function   |   | Pin     | Function     | RF95 | +| Pin   | Function   |   | Pin     | Function     | RF95 | | ----- | ----------- | --- | -------- | ------------ | ----- | -| Gnd   |             |   | vbat     |             | | -| P0.06 | Serial2 RX |   | vbat     |             | | -| P0.08 | Serial2 TX |   | Gnd     |             | | -| Gnd   |             |   | reset   |             | | -| Gnd   |             |   | ext_vcc | *see 0.13   | | -| P0.17 | RXEN       |   | P0.31   | BATTERY_PIN | | -| P0.20 | GPS_RX     |   | P0.29   | BUSY         | DIO0 | -| P0.22 | GPS_TX     |   | P0.02   | MISO | MISO | -| P0.24 | GPS_EN     |   | P1.15   | MOSI         | MOSI | -| P1.00 | BUTTON_PIN |   | P1.13   | CS           | CS   | -| P0.11 | SCL         |   | P1.11   | SCK         | SCK | -| P1.04 | SDA         |   | P0.10   | DIO1/IRQ     | DIO1 | -| P1.06 | Free pin   |   | P0.09   | RESET       | RST | -|       |             |   |         |             | | -|       | Mid board   |   |         | Internal     | | -| P1.01 | Free pin   |   | 0.15     | LED         | | -| P1.02 | Free pin   |   | 0.13     | 3V3_EN       | | -| P1.07 | Free pin   |   |         |             | | +| Gnd   |             |   | vbat     |             | | +| P0.06 | Serial2 RX |   | vbat     |             | | +| P0.08 | Serial2 TX |   | Gnd     |             | | +| Gnd   |             |   | reset   |             | | +| Gnd   |             |   | ext_vcc | *see 0.13   | | +| P0.17 | RXEN       |   | P0.31   | BATTERY_PIN | | +| P0.20 | GPS_RX     |   | P0.29   | BUSY         | DIO0 | +| P0.22 | GPS_TX     |   | P0.02   | MISO | MISO | +| P0.24 | GPS_EN     |   | P1.15   | MOSI         | MOSI | +| P1.00 | BUTTON_PIN |   | P1.13   | CS           | CS   | +| P0.11 | SCL         |   | P1.11   | SCK         | SCK | +| P1.04 | SDA         |   | P0.10   | DIO1/IRQ     | DIO1 | +| P1.06 | Free pin   |   | P0.09   | RESET       | RST | +|       |             |   |         |             | | +|       | Mid board   |   |         | Internal     | | +| P1.01 | Free pin   |   | 0.15     | LED         | | +| P1.02 | Free pin   |   | 0.13     | 3V3_EN       | | +| P1.07 | Free pin   |   |         |             | | */ // Number of pins defined in PinDescription array @@ -175,7 +175,7 @@ settings. | NiceRF | Lora1262 | yes | Int | | | Waveshare | Core1262-HF | yes | Ext | | | Waveshare | LoRa Node Module | yes | Int | | -| Seeed | Wio-SX1262 | yes | Int | Sooooo cute! | +| Seeed | Wio-SX1262 | yes | Ext | Cute! DIO2/TXEN are not exposed | | AI-Thinker | RA-02 | No | Int | SX1278 **433mhz band only** | | RF Solutions | RFM95 | No | Int | Untested | | Ebyte | E80-900M2213S | Yes | Int | LR1121 radio | From 9db51a72a4459357af28d112baaebe91c0e0c39f Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Thu, 6 Feb 2025 21:11:17 +0100 Subject: [PATCH 42/71] Fix T-Deck/T-Watch no BT (#5998) fixes #5997 --- src/mesh/NodeDB.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 4a01e0d41..9caa03928 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -407,7 +407,7 @@ bool NodeDB::resetRadioConfig(bool factory_reset) rebootAtMsec = millis() + (5 * 1000); } -#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3)) && defined(HAS_TFT) +#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3)) && HAS_TFT // as long as PhoneAPI shares BT and TFT app switch BT off config.bluetooth.enabled = false; if (moduleConfig.external_notification.nag_timeout == 60) @@ -1528,4 +1528,4 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co LOG_ERROR("A critical failure occurred, portduino is exiting"); exit(2); #endif -} \ No newline at end of file +} From cb0519dd9ce9b19945a791f67ac6b81bf88567b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 14:11:32 -0600 Subject: [PATCH 43/71] [create-pull-request] automated change (#5989) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- debian/changelog | 5 +++-- version.properties | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/debian/changelog b/debian/changelog index 1b371296b..3ec57b805 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,9 @@ -meshtasticd (2.5.21.0) UNRELEASED; urgency=medium +meshtasticd (2.5.22.0) UNRELEASED; urgency=medium * Initial packaging * GitHub Actions Automatic version bump * GitHub Actions Automatic version bump * GitHub Actions Automatic version bump + * GitHub Actions Automatic version bump - -- Austin Lane Sat, 25 Jan 2025 01:39:16 +0000 + -- Austin Lane Wed, 05 Feb 2025 01:10:33 +0000 diff --git a/version.properties b/version.properties index efc42428c..2e207e21e 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 5 -build = 21 +build = 22 From 4a6a0efcfd286bc0a2c19e91b2167c6b8dd8577f Mon Sep 17 00:00:00 2001 From: lizthedeveloper <915684+lizTheDeveloper@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:29:48 -0800 Subject: [PATCH 44/71] log the nonce value at DEBUG instead of INFO (#6001) you're leaking the nonce to stdout, if your logs are routed to a folder, this logs the nonce every time, leading to replay attack surface area being higher. Changed to debug. --- src/mesh/CryptoEngine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/CryptoEngine.cpp b/src/mesh/CryptoEngine.cpp index 1624ab0d5..4613a6218 100644 --- a/src/mesh/CryptoEngine.cpp +++ b/src/mesh/CryptoEngine.cpp @@ -74,7 +74,7 @@ bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtas auth = bytesOut + numBytes; memcpy((uint8_t *)(auth + 8), &extraNonceTmp, sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp; - LOG_INFO("Random nonce value: %d", extraNonceTmp); + LOG_DEBUG("Random nonce value: %d", extraNonceTmp); if (remotePublic.size == 0) { LOG_DEBUG("Node %d or their public_key not found", toNode); return false; From 4e8c4f0d558e8b9ed852f9fbb60fc5ab0c89864a Mon Sep 17 00:00:00 2001 From: dylanli Date: Fri, 7 Feb 2025 16:02:56 +0800 Subject: [PATCH 45/71] T1000-E hardware updates and GPS positioning accuracy optimisation (#6003) * T1000-E button setting update * T1000-E GNSS lock error fix * T1000-E GNSS improve detection success --------- Co-authored-by: WayenWeng --- src/gps/GPS.cpp | 22 ++++++++++++++++++++++ variants/tracker-t1000-e/variant.h | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 863f956cf..c2aae0381 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -449,7 +449,22 @@ bool GPS::setup() if (!didSerialInit) { int msglen = 0; if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) { +#ifdef TRACKER_T1000_E + // add power up/down strategy, improve ag3335 detection success + digitalWrite(PIN_GPS_EN, LOW); + delay(500); + digitalWrite(GPS_VRTC_EN, LOW); + delay(1000); + digitalWrite(GPS_VRTC_EN, HIGH); + delay(500); + digitalWrite(PIN_GPS_EN, HIGH); + delay(1000); +#endif +#ifdef TRACKER_T1000_E + if (probeTries < 5) { +#else if (probeTries < 2) { +#endif LOG_DEBUG("Probe for GPS at %d", serialSpeeds[speedSelect]); gnssModel = probe(serialSpeeds[speedSelect]); if (gnssModel == GNSS_MODEL_UNKNOWN) { @@ -460,7 +475,11 @@ bool GPS::setup() } } // Rare Serial Speeds +#ifdef TRACKER_T1000_E + if (probeTries == 5) { +#else if (probeTries == 2) { +#endif LOG_DEBUG("Probe for GPS at %d", rareSerialSpeeds[speedSelect]); gnssModel = probe(rareSerialSpeeds[speedSelect]); if (gnssModel == GNSS_MODEL_UNKNOWN) { @@ -772,6 +791,9 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) setPowerPMU(true); // Power (PMU): on writePinStandby(false); // Standby (pin): awake (not standby) setPowerUBLOX(true); // Standby (UBLOX): awake +#ifdef GNSS_AIROHA + lastFixStartMsec = 0; +#endif break; case GPS_SOFTSLEEP: diff --git a/variants/tracker-t1000-e/variant.h b/variants/tracker-t1000-e/variant.h index 6a1f99600..e65f26c93 100644 --- a/variants/tracker-t1000-e/variant.h +++ b/variants/tracker-t1000-e/variant.h @@ -55,7 +55,7 @@ extern "C" { #define BUTTON_PIN (0 + 6) // P0.06 #define BUTTON_ACTIVE_LOW false #define BUTTON_ACTIVE_PULLUP false -#define BUTTON_SENSE_TYPE 0x6 +#define BUTTON_SENSE_TYPE 0x5 // enable input pull-down #define HAS_WIRE 1 From d70a9392affb9ace3c3c32af9acc8129ab6976b7 Mon Sep 17 00:00:00 2001 From: ChangYanChu Date: Sat, 8 Feb 2025 20:03:44 +0800 Subject: [PATCH 46/71] improve UTF-8 string handling in JSONValue (#6011) ```text feat(json): improve UTF-8 string handling in JSONValue - Add proper UTF-8 multi-byte character sequence handling - Add boundary checks for UTF-8 sequences - Keep original code structure and flow - Add detailed comments for UTF-8 processing logic This change improves the robustness of JSON string handling while maintaining compatibility with existing code. --- src/serialization/JSONValue.cpp | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/serialization/JSONValue.cpp b/src/serialization/JSONValue.cpp index 64dc10abe..20cd90373 100644 --- a/src/serialization/JSONValue.cpp +++ b/src/serialization/JSONValue.cpp @@ -850,18 +850,26 @@ std::string JSONValue::StringifyString(const std::string &str) str_out += "\\r"; } else if (chr == '\t') { str_out += "\\t"; - } else if (chr < ' ' || chr > 126) { - str_out += "\\u"; - for (int i = 0; i < 4; i++) { - int value = (chr >> 12) & 0xf; - if (value >= 0 && value <= 9) - str_out += (char)('0' + value); - else if (value >= 10 && value <= 15) - str_out += (char)('A' + (value - 10)); - chr <<= 4; - } + } else if (chr < 0x20 || chr == 0x7F) { + char buf[7]; + snprintf(buf, sizeof(buf), "\\u%04x", chr); + str_out += buf; + } else if (chr < 0x80) { + str_out += chr; } else { str_out += chr; + size_t remain = str.end() - iter - 1; + if ((chr & 0xE0) == 0xC0 && remain >= 1) { + ++iter; + str_out += *iter; + } else if ((chr & 0xF0) == 0xE0 && remain >= 2) { + str_out += *(++iter); + str_out += *(++iter); + } else if ((chr & 0xF8) == 0xF0 && remain >= 3) { + str_out += *(++iter); + str_out += *(++iter); + str_out += *(++iter); + } } ++iter; From 39e45d90e16db0352660a912247e27620515ab91 Mon Sep 17 00:00:00 2001 From: Mark Trevor Birss Date: Mon, 10 Feb 2025 09:59:13 +0200 Subject: [PATCH 47/71] Create display-x11.yaml (#6021) --- bin/config.d/display-x11.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 bin/config.d/display-x11.yaml diff --git a/bin/config.d/display-x11.yaml b/bin/config.d/display-x11.yaml new file mode 100644 index 000000000..b22df1cb0 --- /dev/null +++ b/bin/config.d/display-x11.yaml @@ -0,0 +1,4 @@ +Display: + Panel: X11 + Width: 480 + Height: 480 From 96262b106c579cccf1fde24a4e8aa771e5f34ae1 Mon Sep 17 00:00:00 2001 From: Mark Trevor Birss Date: Mon, 10 Feb 2025 11:53:58 +0200 Subject: [PATCH 48/71] Revert "Create display-x11.yaml (#6021)" (#6022) This reverts commit 39e45d90e16db0352660a912247e27620515ab91. --- bin/config.d/display-x11.yaml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 bin/config.d/display-x11.yaml diff --git a/bin/config.d/display-x11.yaml b/bin/config.d/display-x11.yaml deleted file mode 100644 index b22df1cb0..000000000 --- a/bin/config.d/display-x11.yaml +++ /dev/null @@ -1,4 +0,0 @@ -Display: - Panel: X11 - Width: 480 - Height: 480 From 7c4bf38647691aa04aaea88f22b1b96d018b3ca2 Mon Sep 17 00:00:00 2001 From: Austin Date: Mon, 10 Feb 2025 15:29:16 -0500 Subject: [PATCH 49/71] meshtasticd flatpak: Include pio deps with release (#6025) --- .github/workflows/main_matrix.yml | 27 +++++++++-- .github/workflows/package_pio_deps.yml | 64 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/package_pio_deps.yml diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index b13866435..1dc177cc6 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -135,6 +135,12 @@ jobs: build_location: local secrets: inherit + package-pio-deps-native: + uses: ./.github/workflows/package_pio_deps.yml + with: + pio_env: native + secrets: inherit + test-native: uses: ./.github/workflows/test_native.yml @@ -279,7 +285,10 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' }} outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} - needs: [gather-artifacts, build-debian-src] + needs: + - gather-artifacts + - build-debian-src + - package-pio-deps-native steps: - name: Checkout uses: actions/checkout@v4 @@ -315,17 +324,27 @@ jobs: merge-multiple: true path: ./output/debian-src - - name: Zip source deb + - name: Download native pio deps + uses: actions/download-artifact@v4 + with: + pattern: platformio-deps-native-${{ steps.version.outputs.long }} + merge-multiple: true + path: ./output/pio-deps-native + + - name: Zip linux sources working-directory: output - run: zip -j -9 -r ./meshtasticd-${{ steps.version.outputs.deb }}-src.zip ./debian-src + run: | + zip -j -9 -r ./meshtasticd-${{ steps.version.outputs.deb }}-src.zip ./debian-src + zip -j -9 -r ./platformio-deps-native-${{ steps.version.outputs.long }}.zip ./pio-deps-native # For diagnostics - name: Display structure of downloaded files run: ls -lR - - name: Add source deb to release + - name: Add linux sources to release run: | gh release upload v${{ steps.version.outputs.long }} ./output/meshtasticd-${{ steps.version.outputs.deb }}-src.zip + gh release upload v${{ steps.version.outputs.long }} ./output/platformio-deps-native-${{ steps.version.outputs.long }}.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml new file mode 100644 index 000000000..38c0e8104 --- /dev/null +++ b/.github/workflows/package_pio_deps.yml @@ -0,0 +1,64 @@ +name: Package PlatformIO Library Dependencies +# trunk-ignore-all(checkov/CKV_GHA_7): Allow workflow_dispatch inputs for testing + +on: + workflow_call: + inputs: + pio_env: + description: PlatformIO environment to target + required: true + type: string + workflow_dispatch: + inputs: + pio_env: + description: PlatformIO environment to target + required: true + type: string + +permissions: + contents: write + packages: write + +jobs: + pkg-pio-libdeps: + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - name: Install deps + shell: bash + run: | + pip install platformio + + - name: Get release version string + run: | + echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + id: version + + - name: Fetch libdeps + shell: bash + run: |- + platformio pkg install -e ${{ inputs.pio_env }} + platformio pkg install -e ${{ inputs.pio_env }} -t platformio/tool-scons@4.40502.0 + env: + PLATFORMIO_LIBDEPS_DIR: pio/libdeps + PLATFORMIO_PACKAGES_DIR: pio/packages + PLATFORMIO_CORE_DIR: pio/core + + - name: Store binaries as an artifact + uses: actions/upload-artifact@v4 + with: + name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }} + overwrite: true + path: | + pio/* From da1d78c8822a084e9473ff03091fccb3b5ea5228 Mon Sep 17 00:00:00 2001 From: Jason P Date: Mon, 10 Feb 2025 14:30:43 -0600 Subject: [PATCH 50/71] Add support for 12- and 24-hour clock, Minor Settings Frame Adjustment (#5988) * 12- or 24-hour clock work in progress * 12- and 24-hour added to Settings Frame. Also some adjustments to screen layout. * Updated Uptime wording to be "Up" to fit within screen real estate * Removed label from uptime to conserve additional space --------- Co-authored-by: Ben Meadors --- src/graphics/Screen.cpp | 48 +++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 4ee49e3c0..0c18f3287 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1489,22 +1489,21 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ bearingToOther -= myHeading; screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); - float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2*PI : bearingToOther; + float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { if (d < (2 * MILES_TO_FEET)) snprintf(distStr, sizeof(distStr), "%.0fft %.0f°", d * METERS_TO_FEET, bearingToOtherDegrees); else - snprintf(distStr, sizeof(distStr), "%.1fmi %.0f°", d * METERS_TO_FEET / MILES_TO_FEET, bearingToOtherDegrees); + snprintf(distStr, sizeof(distStr), "%.1fmi %.0f°", d * METERS_TO_FEET / MILES_TO_FEET, + bearingToOtherDegrees); } else { if (d < 2000) snprintf(distStr, sizeof(distStr), "%.0fm %.0f°", d, bearingToOtherDegrees); else snprintf(distStr, sizeof(distStr), "%.1fkm %.0f°", d / 1000, bearingToOtherDegrees); } - - } } if (!hasNodeHeading) { @@ -2649,13 +2648,12 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat display->drawString(x + 1, y, String("USB")); } - auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true); + // auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode); - if (config.display.heading_bold) - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode); + // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode); + // if (config.display.heading_bold) + // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode); - // Line 2 uint32_t currentMillis = millis(); uint32_t seconds = currentMillis / 1000; uint32_t minutes = seconds / 60; @@ -2668,6 +2666,9 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat display->setColor(WHITE); + // Setup string to assemble analogClock string + std::string analogClock = ""; + // Show uptime as days, hours, minutes OR seconds std::string uptime = screen->drawTimeDelta(days, hours, minutes, seconds); @@ -2684,17 +2685,36 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - char timebuf[10]; - snprintf(timebuf, sizeof(timebuf), " %02d:%02d:%02d", hour, min, sec); - uptime += timebuf; + 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; } - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, uptime.c_str()); + // Line 1 + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + + // 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 @@ -2827,4 +2847,4 @@ int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg) } // namespace graphics #else graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} -#endif // HAS_SCREEN +#endif // HAS_SCREEN \ No newline at end of file From 4e2b47cc67d33d399350a2947c4fe61329801cb5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:02:42 -0600 Subject: [PATCH 51/71] [create-pull-request] automated change (#6027) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/module_config.pb.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/protobufs b/protobufs index b80785b16..068646653 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit b80785b16bc0d243b97917998706e7bf209cd9d0 +Subproject commit 068646653e8375fc145988026ad242a3cf70f7ab diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index 697b965c5..848b010d3 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.h +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -347,7 +347,7 @@ typedef struct _meshtastic_ModuleConfig_TelemetryConfig { bool health_screen_enabled; } meshtastic_ModuleConfig_TelemetryConfig; -/* TODO: REPLACE */ +/* Canned Messages Module Config */ typedef struct _meshtastic_ModuleConfig_CannedMessageConfig { /* Enable the rotary encoder #1. This is a 'dumb' encoder sending pulses on both A and B pins while rotating. */ bool rotary1_enabled; From 8427072d791b5547ec029534e615da1e45b740f3 Mon Sep 17 00:00:00 2001 From: Austin Date: Mon, 10 Feb 2025 18:58:02 -0500 Subject: [PATCH 52/71] meshtasticd: include `.hidden` (.git) dirs in pio-deps (#6028) --- .github/workflows/package_pio_deps.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml index 38c0e8104..9f535b7b1 100644 --- a/.github/workflows/package_pio_deps.yml +++ b/.github/workflows/package_pio_deps.yml @@ -60,5 +60,6 @@ jobs: with: name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }} overwrite: true + include-hidden-files: true path: | pio/* From d1fa27d3537d5b78d0af96fdbcbec580dee36b6a Mon Sep 17 00:00:00 2001 From: Austin Date: Mon, 10 Feb 2025 21:35:06 -0500 Subject: [PATCH 53/71] small fix: don't junk the zip for pio-deps (#6029) --- .github/workflows/main_matrix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 1dc177cc6..7062ef525 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -335,7 +335,7 @@ jobs: working-directory: output run: | zip -j -9 -r ./meshtasticd-${{ steps.version.outputs.deb }}-src.zip ./debian-src - zip -j -9 -r ./platformio-deps-native-${{ steps.version.outputs.long }}.zip ./pio-deps-native + zip -9 -r ./platformio-deps-native-${{ steps.version.outputs.long }}.zip ./pio-deps-native # For diagnostics - name: Display structure of downloaded files From 7fdd262d55bd694fe9a347ab8c946bd080bb5bce Mon Sep 17 00:00:00 2001 From: nwilde1590 Date: Tue, 11 Feb 2025 00:02:21 -0600 Subject: [PATCH 54/71] Added custom OCV array values for T1000-E (#6031) --- src/power.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/power.h b/src/power.h index 176e16ee5..e9c0deb7c 100644 --- a/src/power.h +++ b/src/power.h @@ -24,6 +24,8 @@ #define OCV_ARRAY 1400, 1300, 1280, 1270, 1260, 1250, 1240, 1230, 1210, 1150, 1000 #elif defined(CELL_TYPE_LTO) #define OCV_ARRAY 2700, 2560, 2540, 2520, 2500, 2460, 2420, 2400, 2380, 2320, 1500 +#elif defined(TRACKER_T1000_E) +#define OCV_ARRAY 4190, 4078, 4017, 3969, 3887, 3818, 3798, 3791, 3766, 3712, 3100 #else // LiIon #define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100 #endif From eb650a6adbc4a83ca1df909cec80dd6a56dfd2a9 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Tue, 11 Feb 2025 17:34:37 +0100 Subject: [PATCH 55/71] set TCXO to 2.4V (#6036) --- variants/seeed-sensecap-indicator/variant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/seeed-sensecap-indicator/variant.h b/variants/seeed-sensecap-indicator/variant.h index c5fc685cd..58eed7d96 100644 --- a/variants/seeed-sensecap-indicator/variant.h +++ b/variants/seeed-sensecap-indicator/variant.h @@ -71,7 +71,7 @@ #define SX126X_DIO2_AS_RF_SWITCH #define TCXO_OPTIONAL // handle Indicator V1 and V2 -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define SX126X_DIO3_TCXO_VOLTAGE 2.4 #define USE_VIRTUAL_KEYBOARD 1 #define DISPLAY_CLOCK_FRAME 1 From 495f69cf907d03619289e8f6b8563a91fcc9a5e7 Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 11 Feb 2025 19:57:23 -0500 Subject: [PATCH 56/71] Trunk: Trailing commas begone! (#6038) --- .trunk/configs/.prettierrc | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .trunk/configs/.prettierrc diff --git a/.trunk/configs/.prettierrc b/.trunk/configs/.prettierrc new file mode 100644 index 000000000..edf9dbc9c --- /dev/null +++ b/.trunk/configs/.prettierrc @@ -0,0 +1,10 @@ +{ + "overrides": [ + { + "files": "userPrefs.jsonc", + "options": { + "trailingComma": "none" + } + } + ] +} From 01935ea35e1c2c1bcdb51757e63751e06a5d1899 Mon Sep 17 00:00:00 2001 From: porkcube Date: Fri, 14 Feb 2025 07:50:28 -0500 Subject: [PATCH 57/71] Add XIAO nRF52840 + Wio SX1262 DIY Variant (#5976) * added xiao nRF52840 + xiao wio sx1262 DIY variant * fix path / make buildy buildy * pcf cruft from personal hw --------- Co-authored-by: Ben Meadors --- variants/diy/platformio.ini | 16 +- .../seeed-xiao-nrf52840-wio-sx1262/README.md | 43 ++++ .../variant.cpp | 55 +++++ .../seeed-xiao-nrf52840-wio-sx1262/variant.h | 188 ++++++++++++++++++ 4 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 variants/diy/seeed-xiao-nrf52840-wio-sx1262/README.md create mode 100644 variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp create mode 100644 variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h diff --git a/variants/diy/platformio.ini b/variants/diy/platformio.ini index b7f3f6a92..229f48bbf 100644 --- a/variants/diy/platformio.ini +++ b/variants/diy/platformio.ini @@ -70,6 +70,20 @@ lib_deps = ${nrf52840_base.lib_deps} debug_tool = jlink +; Seeed XIAO nRF52840 + XIAO Wio SX1262 DIY +[env:seeed-xiao-nrf52840-wio-sx1262] +board = xiao_ble_sense +extends = nrf52840_base +board_level = extra +build_flags = ${nrf52840_base.build_flags} -Ivariants/diy/seeed-xiao-nrf52840-wio-sx1262 -D PRIVATE_HW + -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/diy/seeed-xiao-nrf52840-wio-sx1262> +lib_deps = + ${nrf52840_base.lib_deps} +debug_tool = jlink + ; NanoVHF T-Energy-S3 + E22(0)-xxxM - DIY [env:t-energy-s3_e22] extends = esp32s3_base @@ -86,4 +100,4 @@ build_flags = -D BOARD_HAS_PSRAM -D ARDUINO_USB_MODE=0 -D ARDUINO_USB_CDC_ON_BOOT=1 - -I variants/diy/t-energy-s3_e22 + -I variants/diy/t-energy-s3_e22 \ No newline at end of file diff --git a/variants/diy/seeed-xiao-nrf52840-wio-sx1262/README.md b/variants/diy/seeed-xiao-nrf52840-wio-sx1262/README.md new file mode 100644 index 000000000..194c53434 --- /dev/null +++ b/variants/diy/seeed-xiao-nrf52840-wio-sx1262/README.md @@ -0,0 +1,43 @@ +# XIAO nRF52840 + XIAO Wio SX1262 + +For a mere doubling in price you too can swap out the XIAO ESP32C3 for a XIAO nRF52840, stack the Wio SX1262 radio board either above or underneath the nRF52840, solder the pins, and achieve a massive improvement in battery life! + +I'm not really sure why else you would want to as the ESP32C3 is perfectly cromulent, easily connects to the Wio SX1262 via the B2B connector and has an onboard IPEX connector for the included Bluetooth antenna. So you'll also lose BT range, but you will also have working ADC for the battery in Meshtastic and also have an ESP32C3 to use for something else! + +If you're still reading you are clearly gonna do it anyway, so...mount the Wio SX1262 either on top or underneath depending on your preference. The `variant.h` will work with either configuration though it does map the Wio SX1262's button to nRF52840 Pin `D5` as it can still be used as a user button and it's nice to be able to gracefully shutdown a node by holding it down for 5 seconds. + +If you do decide to wire up the button, orient it so looking straight-down at the Wio SX1262 the radio chip is at the bottom, button in the middle and the hole is at the top - the **left** side of the button should be soldered to `GND` (e.g. the 2nd pin down the top on the **right** row of pins) and the **right** side of the button should be soldered to `D5` (e.g. the 2nd pin up from the button on the **left** row of pins.). This mirrors the original wiring and wiring it in reverse could end up connecting GND to voltage and that's no beuno. + +Serial Pins remain available on `D6` (TX) and `D7` (RX) should you want to use them, The same pins could be repurposed for `i2c` if you would like to have that instead of serial, in `variant.h` you would just need to change: + +```c++ +// RX and TX pins +#define PIN_SERIAL1_RX (6) +#define PIN_SERIAL1_TX (7) +``` + +to + +```c++ +// RX and TX pins +#define PIN_SERIAL1_RX (-1) +#define PIN_SERIAL1_TX (-1) +``` + +and + +```c++ +#define PIN_WIRE_SDA (-1) +#define PIN_WIRE_SCL (-1) +// #define PIN_WIRE_SDA (6) +// #define PIN_WIRE_SCL (7) +``` + +to + +```c++ +#define PIN_WIRE_SDA (6) +#define PIN_WIRE_SCL (7) +``` + +If you wanted both serial and i2c you could even go so far as to use the pads for the PDM mic which is missing on the non-sense board (`P1.00` / `P0.16`)... or move up to the nRF52840 Plus which has even more pins available but hasn't been checked/confirmed if it follows the same pin mapping as the non-plus. diff --git a/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp b/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp new file mode 100644 index 000000000..2c6c3e539 --- /dev/null +++ b/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp @@ -0,0 +1,55 @@ +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D13 + 2, // D0 is P0.02 (A0) + 3, // D1 is P0.03 (A1) + 28, // D2 is P0.28 (A2) + 29, // D3 is P0.29 (A3) + 4, // D4 is P0.04 (A4,SDA) + 5, // D5 is P0.05 (A5,SCL) + 43, // D6 is P1.11 (TX) + 44, // D7 is P1.12 (RX) + 45, // D8 is P1.13 (SCK) + 46, // D9 is P1.14 (MISO) + 47, // D10 is P1.15 (MOSI) + + // LEDs + 26, // D11 is P0.26 (LED RED) + 6, // D12 is P0.06 (LED BLUE) + 30, // D13 is P0.30 (LED GREEN) + 14, // D14 is P0.14 (READ_BAT) + + // LSM6DS3TR + 40, // D15 is P1.08 (6D_PWR) + 27, // D16 is P0.27 (6D_I2C_SCL) + 7, // D17 is P0.07 (6D_I2C_SDA) + 11, // D18 is P0.11 (6D_INT1) + + // MIC + 42, // 17,//42, // D19 is P1.10 (MIC_PWR) + 32, // 26,//32, // D20 is P1.00 (PDM_CLK) + 16, // 25,//16, // D21 is P0.16 (PDM_DATA) + + // BQ25100 + 13, // D22 is P0.13 (HICHG) + 17, // D23 is P0.17 (~CHG) + + // + 21, // D24 is P0.21 (QSPI_SCK) + 25, // D25 is P0.25 (QSPI_CSN) + 20, // D26 is P0.20 (QSPI_SIO_0 DI) + 24, // D27 is P0.24 (QSPI_SIO_1 DO) + 22, // D28 is P0.22 (QSPI_SIO_2 WP) + 23, // D29 is P0.23 (QSPI_SIO_3 HOLD) + + // NFC + 9, // D30 is P0.09 (NFC1) + 10, // D31 is P0.10 (NFC2) + + // VBAT + 31, // D32 is P0.10 (VBAT) +}; \ No newline at end of file diff --git a/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h b/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h new file mode 100644 index 000000000..d5dfc3fab --- /dev/null +++ b/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h @@ -0,0 +1,188 @@ +// basically xiao_ble with pins remapped for: +// Seeed XIAO nRF52840 : https://www.seeedstudio.com/Seeed-XIAO-BLE-nRF52840-p-5201.html +// Seeed Wio SX1626 : https://www.seeedstudio.com/Wio-SX1262-with-XIAO-ESP32S3-p-5982.html + +#ifndef _SEEED_XIAO_NRF52840_SENSE_H_ +#define _SEEED_XIAO_NRF52840_SENSE_H_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define PINS_COUNT (33) +#define NUM_DIGITAL_PINS (33) +#define NUM_ANALOG_INPUTS (8) // A6 is used for battery, A7 is analog reference +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +// ---- +#define LED_RED 11 +#define LED_BLUE 12 +#define LED_GREEN 13 + +#define PIN_LED1 LED_GREEN +#define PIN_LED2 LED_BLUE +#define PIN_LED3 LED_RED + +#define PIN_LED PIN_LED1 +#define LED_PWR (PINS_COUNT) + +#define LED_BUILTIN PIN_LED +#define LED_STATE_ON 1 // State when LED is lit + +// XIAO Wio-SX1262 Shield User button +#define PIN_BUTTON1 5 +#define BUTTON_NEED_PULLUP + +// Digital Pins +// ------------ +#define D0 (0ul) +#define D1 (1ul) +#define D2 (2ul) +#define D3 (3ul) +#define D4 (4ul) +#define D5 (5ul) +#define D6 (6ul) +#define D7 (7ul) +#define D8 (8ul) +#define D9 (9ul) +#define D10 (10ul) + +// Analog Pins +// ----------- +#define PIN_A0 (0) +#define PIN_A1 (1) +#define PIN_A2 (2) +#define PIN_A3 (3) +#define PIN_A4 (4) +#define PIN_A5 (5) +#define PIN_VBAT (32) +#define VBAT_ENABLE (14) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +#define ADC_RESOLUTION 12 + +// Other Pins +// ---------- +#define PIN_NFC1 (30) +#define PIN_NFC2 (31) + +// RX and TX pins +#define PIN_SERIAL1_RX (6) +#define PIN_SERIAL1_TX (7) +// complains if not defined +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +// 4 is used as RF_SW and 5 for USR button so... +#define PIN_WIRE_SDA (-1) +#define PIN_WIRE_SCL (-1) +// #define PIN_WIRE_SDA (6) +// #define PIN_WIRE_SCL (7) + +static const uint8_t SDA = PIN_WIRE_SDA; +static const uint8_t SCL = PIN_WIRE_SCL; + +// SPI SX1262 +// ---------- +#define SPI_SX1262 +#ifdef SPI_SX1262 +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (9) +#define PIN_SPI_MOSI (10) +#define PIN_SPI_SCK (8) + +static const uint8_t SS = D3; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +// supported modules list +#define USE_SX1262 + +// common pinouts for SX126X modules +#define SX126X_CS D3 +#define SX126X_DIO1 D0 +#define SX126X_BUSY D1 +#define SX126X_RESET D2 + +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_RXEN 38 +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif + +// Wire Interfaces +// ------------------- +#define WIRE_INTERFACES_COUNT 1 // 2 + +// Sense version has IMU and PDM Mic +// #define XIAO_SENSE +#ifndef XIAO_SENSE +// 6 DoF IMU +#define PIN_LSM6DS3TR_C_POWER (15) +#define PIN_LSM6DS3TR_C_INT1 (18) +// PDM Interfaces +// --------------- +#define PIN_PDM_PWR (19) +#define PIN_PDM_CLK (20) +#define PIN_PDM_DIN (21) +#endif + +// QSPI Pins +// --------- +#define PIN_QSPI_SCK (24) +#define PIN_QSPI_CS (25) +#define PIN_QSPI_IO0 (26) +#define PIN_QSPI_IO1 (27) +#define PIN_QSPI_IO2 (28) +#define PIN_QSPI_IO3 (29) + +// On-board QSPI Flash +// ------------------- +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +// Battery +// ------- +// P0_14 = 14 Reads battery voltage from divider on signal board. +// PIN_VBAT is reading voltage divider on XIAO and is program pin 32 / or P0.31 +#define BAT_READ 14 +#define BATTERY_SENSE_RESOLUTION_BITS 10 +#define CHARGE_LED 23 // P0_17 = 17 D23 YELLOW CHARGE LED +#define HICHG 22 // P0_13 = 13 D22 Charge-select pin for Lipo for 100 mA instead of default 50mA charge + +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_VBAT // PIN_A0 + +// ratio of voltage divider = 3.0 (R17=1M, R18=510k) +#define ADC_MULTIPLIER 3 // 3.0 + a bit for being optimistic + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file From 9b46cb4ef08688a2f424c76d8425561e4f5db844 Mon Sep 17 00:00:00 2001 From: Woutvstk <119763111+Woutvstk@users.noreply.github.com> Date: Fri, 14 Feb 2025 19:53:22 +0100 Subject: [PATCH 58/71] Rak4631 remove spi1 (#6042) * Removed non-existant SPI1 interface on rak4631 * trunk fmt --- src/detect/einkScan.h | 16 ++++++++-------- variants/rak4631/variant.h | 18 +++++++----------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/detect/einkScan.h b/src/detect/einkScan.h index d20c7b6e5..5bc218d00 100644 --- a/src/detect/einkScan.h +++ b/src/detect/einkScan.h @@ -6,28 +6,28 @@ void d_writeCommand(uint8_t c) { - SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); + SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); if (PIN_EINK_DC >= 0) digitalWrite(PIN_EINK_DC, LOW); if (PIN_EINK_CS >= 0) digitalWrite(PIN_EINK_CS, LOW); - SPI1.transfer(c); + SPI.transfer(c); if (PIN_EINK_CS >= 0) digitalWrite(PIN_EINK_CS, HIGH); if (PIN_EINK_DC >= 0) digitalWrite(PIN_EINK_DC, HIGH); - SPI1.endTransaction(); + SPI.endTransaction(); } void d_writeData(uint8_t d) { - SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); + SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); if (PIN_EINK_CS >= 0) digitalWrite(PIN_EINK_CS, LOW); - SPI1.transfer(d); + SPI.transfer(d); if (PIN_EINK_CS >= 0) digitalWrite(PIN_EINK_CS, HIGH); - SPI1.endTransaction(); + SPI.endTransaction(); } unsigned long d_waitWhileBusy(uint16_t busy_time) @@ -53,7 +53,7 @@ unsigned long d_waitWhileBusy(uint16_t busy_time) void scanEInkDevice(void) { - SPI1.begin(); + SPI.begin(); d_writeCommand(0x22); d_writeData(0x83); d_writeCommand(0x20); @@ -62,6 +62,6 @@ void scanEInkDevice(void) LOG_DEBUG("EInk display found"); else LOG_DEBUG("EInk display not found"); - SPI1.end(); + SPI.end(); } #endif \ No newline at end of file diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h index bc5541336..f50f3b880 100644 --- a/variants/rak4631/variant.h +++ b/variants/rak4631/variant.h @@ -107,15 +107,11 @@ static const uint8_t AREF = PIN_AREF; /* * SPI Interfaces */ -#define SPI_INTERFACES_COUNT 2 +#define SPI_INTERFACES_COUNT 1 -#define PIN_SPI_MISO (45) -#define PIN_SPI_MOSI (44) -#define PIN_SPI_SCK (43) - -#define PIN_SPI1_MISO (29) // (0 + 29) -#define PIN_SPI1_MOSI (30) // (0 + 30) -#define PIN_SPI1_SCK (3) // (0 + 3) +#define PIN_SPI_MISO (29) +#define PIN_SPI_MOSI (30) +#define PIN_SPI_SCK (3) static const uint8_t SS = 42; static const uint8_t MOSI = PIN_SPI_MOSI; @@ -130,8 +126,8 @@ static const uint8_t SCK = PIN_SPI_SCK; #define PIN_EINK_BUSY (0 + 4) #define PIN_EINK_DC (0 + 17) #define PIN_EINK_RES (-1) -#define PIN_EINK_SCLK (0 + 3) -#define PIN_EINK_MOSI (0 + 30) // also called SDI +#define PIN_EINK_SCLK PIN_SPI_SCK +#define PIN_EINK_MOSI PIN_SPI_MOSI // also called SDI // #define USE_EINK @@ -259,7 +255,7 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define PIN_ETHERNET_RESET 21 #define PIN_ETHERNET_SS PIN_EINK_CS -#define ETH_SPI_PORT SPI1 +#define ETH_SPI_PORT SPI #define AQ_SET_PIN 10 #ifdef __cplusplus From c83ffd4911f1edb8cb437db97346c40ac630e245 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Fri, 14 Feb 2025 17:19:50 -0800 Subject: [PATCH 59/71] Consider the MQTT TLS remote IP when enabled. (#6058) --- src/mqtt/MQTT.cpp | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index f808a66ef..6043daa34 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -412,36 +412,28 @@ void MQTT::reconnect() const char *serverAddr = default_mqtt_address; const char *mqttUsername = default_mqtt_username; const char *mqttPassword = default_mqtt_password; + MQTTClient *clientConnection = mqttClient.get(); if (*moduleConfig.mqtt.address) { serverAddr = moduleConfig.mqtt.address; mqttUsername = moduleConfig.mqtt.username; mqttPassword = moduleConfig.mqtt.password; } -#if HAS_WIFI && !defined(ARCH_PORTDUINO) -#if !defined(CONFIG_IDF_TARGET_ESP32C6) +#if HAS_WIFI && !defined(ARCH_PORTDUINO) && !defined(CONFIG_IDF_TARGET_ESP32C6) if (moduleConfig.mqtt.tls_enabled) { // change default for encrypted to 8883 try { serverPort = 8883; wifiSecureClient.setInsecure(); - - pubSub.setClient(wifiSecureClient); LOG_INFO("Use TLS-encrypted session"); + clientConnection = &wifiSecureClient; } catch (const std::exception &e) { LOG_ERROR("MQTT ERROR: %s", e.what()); } } else { LOG_INFO("Use non-TLS-encrypted session"); - pubSub.setClient(*mqttClient); } -#else - pubSub.setClient(*mqttClient); #endif -#elif HAS_NETWORKING - pubSub.setClient(*mqttClient); -#endif - std::pair hostAndPort = parseHostAndPort(serverAddr, serverPort); serverAddr = hostAndPort.first.c_str(); serverPort = hostAndPort.second; @@ -451,13 +443,14 @@ void MQTT::reconnect() LOG_INFO("Connect directly to MQTT server %s, port: %d, username: %s, password: %s", serverAddr, serverPort, mqttUsername, mqttPassword); + pubSub.setClient(*clientConnection); bool connected = pubSub.connect(owner.id, mqttUsername, mqttPassword); if (connected) { LOG_INFO("MQTT connected"); enabled = true; // Start running background process again runASAP = true; reconnectCount = 0; - isMqttServerAddressPrivate = isPrivateIpAddress(mqttClient->remoteIP()); + isMqttServerAddressPrivate = isPrivateIpAddress(clientConnection->remoteIP()); publishNodeInfo(); sendSubscriptions(); From 50b7d6a0f7e7bac8766a8c467a9a5afad332b072 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Fri, 14 Feb 2025 18:32:41 -0800 Subject: [PATCH 60/71] Establish MQTT connection only from MQTT::runOnce (#6057) Co-authored-by: Ben Meadors --- src/mesh/eth/ethClient.cpp | 9 --------- src/mesh/wifi/WiFiAPClient.cpp | 9 --------- src/mqtt/MQTT.h | 8 ++++---- test/test_mqtt/MQTT.cpp | 3 ++- 4 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 24c4f0db1..70c6e3fe4 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -5,9 +5,6 @@ #include "configuration.h" #include "main.h" #include "mesh/api/ethServerAPI.h" -#if !MESHTASTIC_EXCLUDE_MQTT -#include "mqtt/MQTT.h" -#endif #include "target_specific.h" #include #include @@ -72,12 +69,6 @@ static int32_t reconnectETH() ethStartupComplete = true; } -#if !MESHTASTIC_EXCLUDE_MQTT - // FIXME this is kinda yucky, instead we should just have an observable for 'wifireconnected' - if (mqtt && !moduleConfig.mqtt.proxy_to_client_enabled && !mqtt->isConnectedDirectly()) { - mqtt->reconnect(); - } -#endif } #ifndef DISABLE_NTP diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index 41de89794..d4a5dbf94 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -7,9 +7,6 @@ #include "main.h" #include "mesh/api/WiFiServerAPI.h" -#if !MESHTASTIC_EXCLUDE_MQTT -#include "mqtt/MQTT.h" -#endif #include "target_specific.h" #include #include @@ -111,12 +108,6 @@ static void onNetworkConnected() #endif APStartupComplete = true; } - - // FIXME this is kinda yucky, instead we should just have an observable for 'wifireconnected' -#ifndef MESHTASTIC_EXCLUDE_MQTT - if (mqtt) - mqtt->reconnect(); -#endif } static int32_t reconnectWiFi() diff --git a/src/mqtt/MQTT.h b/src/mqtt/MQTT.h index cf52ad877..42157fda9 100644 --- a/src/mqtt/MQTT.h +++ b/src/mqtt/MQTT.h @@ -47,10 +47,6 @@ class MQTT : private concurrency::OSThread */ void onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex); - /** Attempt to connect to server if necessary - */ - void reconnect(); - bool isConnectedDirectly(); bool publish(const char *topic, const char *payload, bool retained); @@ -115,6 +111,10 @@ class MQTT : private concurrency::OSThread */ bool wantsLink() const; + /** Attempt to connect to server if necessary + */ + void reconnect(); + /** Tell the server what subscriptions we want (based on channels.downlink_enabled) */ void sendSubscriptions(); diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index 55ba479e2..3a4625aed 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -242,6 +242,7 @@ class MQTTUnitTest : public MQTT mqttClient.release(); delete pubsub; } + using MQTT::reconnect; int queueSize() { return mqttQueue.numUsed(); } void reportToMap(std::optional precision = std::nullopt) { @@ -488,7 +489,7 @@ void test_reconnectProxyDoesNotReconnectMqtt(void) moduleConfig.mqtt.proxy_to_client_enabled = true; MQTTUnitTest::restart(); - mqtt->reconnect(); + unitTest->reconnect(); TEST_ASSERT_FALSE(pubsub->connected_); } From 8c9947b05c00e8137200a2b9093cbb19edd2aaa9 Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:55:51 +0100 Subject: [PATCH 61/71] Allow NeighborInfo on non-default frequency slot (#6061) --- src/modules/NeighborInfoModule.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp index fb658421d..eebf428a4 100644 --- a/src/modules/NeighborInfoModule.cpp +++ b/src/modules/NeighborInfoModule.cpp @@ -121,7 +121,8 @@ Will be used for broadcast. */ int32_t NeighborInfoModule::runOnce() { - if (moduleConfig.neighbor_info.transmit_over_lora && !channels.isDefaultChannel(channels.getPrimaryIndex()) && + if (moduleConfig.neighbor_info.transmit_over_lora && + (!channels.isDefaultChannel(channels.getPrimaryIndex()) || !RadioInterface::uses_default_frequency_slot) && airTime->isTxAllowedChannelUtil(true) && airTime->isTxAllowedAirUtil()) { sendNeighborInfo(NODENUM_BROADCAST, false); } else { From 2f6cd021115d0e63975cf8248bb550c76a7497a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Gjels=C3=B8?= <36234524+gjelsoe@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:06:41 +0100 Subject: [PATCH 62/71] Typo for Bandit button LEDs (#6053) Changed Button 2 LED index define from BUTTON1_COLOR_INDEX to correct BUTTON2_COLOR_INDEX --- src/AmbientLightingThread.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AmbientLightingThread.h b/src/AmbientLightingThread.h index 600583348..c487f9d53 100644 --- a/src/AmbientLightingThread.h +++ b/src/AmbientLightingThread.h @@ -153,7 +153,7 @@ class AmbientLightingThread : public concurrency::OSThread pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1); #endif #if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX) - pixels.fill(BUTTON2_COLOR, BUTTON1_COLOR_INDEX, 1); + pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1); #endif #endif pixels.show(); From 27fea5fc0724bbd1d6142f684b529807a91211d2 Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:06:10 +0100 Subject: [PATCH 63/71] Fix STM32WL TCXO setting; enable logs and modules (#6063) Co-authored-by: Ben Meadors --- arch/stm32/stm32.ini | 10 ++++++++-- src/mesh/STM32WLE5JCInterface.cpp | 5 ++++- src/mesh/STM32WLE5JCInterface.h | 3 --- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/arch/stm32/stm32.ini b/arch/stm32/stm32.ini index 7e211496d..46f41db3a 100644 --- a/arch/stm32/stm32.ini +++ b/arch/stm32/stm32.ini @@ -11,9 +11,15 @@ build_flags = ${arduino_base.build_flags} -flto -Isrc/platform/stm32wl -g - -DMESHTASTIC_MINIMIZE_BUILD + -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + -DMESHTASTIC_EXCLUDE_INPUTBROKER + -DMESHTASTIC_EXCLUDE_I2C + -DMESHTASTIC_EXCLUDE_POWERMON + -DMESHTASTIC_EXCLUDE_SCREEN + -DMESHTASTIC_EXCLUDE_MQTT + -DMESHTASTIC_EXCLUDE_BLUETOOTH + -DMESHTASTIC_EXCLUDE_PKI -DMESHTASTIC_EXCLUDE_GPS - -DDEBUG_MUTE ; -DVECT_TAB_OFFSET=0x08000000 -DconfigUSE_CMSIS_RTOS_V2=1 ; -DSPI_MODE_0=SPI_MODE0 diff --git a/src/mesh/STM32WLE5JCInterface.cpp b/src/mesh/STM32WLE5JCInterface.cpp index 499db9176..ad1f675b6 100644 --- a/src/mesh/STM32WLE5JCInterface.cpp +++ b/src/mesh/STM32WLE5JCInterface.cpp @@ -18,6 +18,9 @@ bool STM32WLE5JCInterface::init() { RadioLibInterface::init(); + // https://github.com/Seeed-Studio/LoRaWan-E5-Node/blob/main/Middlewares/Third_Party/SubGHz_Phy/stm32_radio_driver/radio_driver.c + setTCXOVoltage(1.7); + lora.setRfSwitchTable(rfswitch_pins, rfswitch_table); if (power > STM32WLx_MAX_POWER) // This chip has lower power limits than some @@ -39,4 +42,4 @@ bool STM32WLE5JCInterface::init() return res == RADIOLIB_ERR_NONE; } -#endif // ARCH_STM32WL +#endif // ARCH_STM32WL \ No newline at end of file diff --git a/src/mesh/STM32WLE5JCInterface.h b/src/mesh/STM32WLE5JCInterface.h index fad793332..0c8140290 100644 --- a/src/mesh/STM32WLE5JCInterface.h +++ b/src/mesh/STM32WLE5JCInterface.h @@ -16,9 +16,6 @@ class STM32WLE5JCInterface : public SX126xInterface virtual bool init() override; }; -// https://github.com/Seeed-Studio/LoRaWan-E5-Node/blob/main/Middlewares/Third_Party/SubGHz_Phy/stm32_radio_driver/radio_driver.c -static const float tcxoVoltage = 1.7; - /* https://wiki.seeedstudio.com/LoRa-E5_STM32WLE5JC_Module/ * Wio-E5 module ONLY transmits through RFO_HP * Receive: PA4=1, PA5=0 From 4407d9e04023a693205aefa36c754d11002c1f0e Mon Sep 17 00:00:00 2001 From: porkcube Date: Sun, 16 Feb 2025 07:39:48 -0500 Subject: [PATCH 64/71] assigning SDA/SCL so it actually works 8| (#6065) --- variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h b/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h index d5dfc3fab..7a76727f2 100644 --- a/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h +++ b/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h @@ -84,17 +84,15 @@ static const uint8_t A5 = PIN_A5; #define PIN_NFC2 (31) // RX and TX pins -#define PIN_SERIAL1_RX (6) -#define PIN_SERIAL1_TX (7) +#define PIN_SERIAL1_RX (-1) +#define PIN_SERIAL1_TX (-1) // complains if not defined #define PIN_SERIAL2_RX (-1) #define PIN_SERIAL2_TX (-1) // 4 is used as RF_SW and 5 for USR button so... -#define PIN_WIRE_SDA (-1) -#define PIN_WIRE_SCL (-1) -// #define PIN_WIRE_SDA (6) -// #define PIN_WIRE_SCL (7) +#define PIN_WIRE_SDA (6) +#define PIN_WIRE_SCL (7) static const uint8_t SDA = PIN_WIRE_SDA; static const uint8_t SCL = PIN_WIRE_SCL; From 7648391f91f2b84e367ae2b38220b30936fb45b1 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sun, 16 Feb 2025 05:15:30 -0800 Subject: [PATCH 65/71] Reject invalid configuration for the default MQTT server (#6066) * Sanity check configuration for the default MQTT server * Skip for MESHTASTIC_EXCLUDE_MQTT --------- Co-authored-by: Ben Meadors --- src/modules/AdminModule.cpp | 17 ++++++++++++++--- src/modules/AdminModule.h | 2 +- src/mqtt/MQTT.cpp | 27 +++++++++++++++++++++++++-- src/mqtt/MQTT.h | 2 ++ test/test_mqtt/MQTT.cpp | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 6 deletions(-) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 7906b410b..530d0b82e 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -162,7 +162,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta case meshtastic_AdminMessage_set_module_config_tag: LOG_INFO("Client set module config"); - handleSetModuleConfig(r->set_module_config); + if (!handleSetModuleConfig(r->set_module_config)) { + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + } break; case meshtastic_AdminMessage_set_channel_tag: @@ -648,15 +650,23 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) saveChanges(changes, requiresReboot); } -void AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) +bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { if (!hasOpenEditTransaction) disableBluetooth(); switch (c.which_payload_variant) { case meshtastic_ModuleConfig_mqtt_tag: +#if MESHTASTIC_EXCLUDE_MQTT + LOG_WARN("Set module config: MESHTASTIC_EXCLUDE_MQTT is defined. Not setting MQTT config"); + return false; +#else LOG_INFO("Set module config: MQTT"); + if (!MQTT::isValidConfig(c.payload_variant.mqtt)) { + return false; + } moduleConfig.has_mqtt = true; moduleConfig.mqtt = c.payload_variant.mqtt; +#endif break; case meshtastic_ModuleConfig_serial_tag: LOG_INFO("Set module config: Serial"); @@ -724,6 +734,7 @@ void AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) break; } saveChanges(SEGMENT_MODULECONFIG); + return true; } void AdminModule::handleSetChannel(const meshtastic_Channel &cc) @@ -1160,4 +1171,4 @@ void disableBluetooth() nrf52Bluetooth->shutdown(); #endif #endif -} +} \ No newline at end of file diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index ee2ebfd96..12c857e04 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -50,7 +50,7 @@ class AdminModule : public ProtobufModule, public Obser void handleSetOwner(const meshtastic_User &o); void handleSetChannel(const meshtastic_Channel &cc); void handleSetConfig(const meshtastic_Config &c); - void handleSetModuleConfig(const meshtastic_ModuleConfig &c); + bool handleSetModuleConfig(const meshtastic_ModuleConfig &c); void handleSetChannel(); void handleSetHamMode(const meshtastic_HamParameters &req); void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg); diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 6043daa34..67eba82a6 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -41,6 +41,7 @@ MQTT *mqtt; namespace { constexpr int reconnectMax = 5; +constexpr uint16_t mqttPort = 1883; // FIXME - this size calculation is super sloppy, but it will go away once we dynamically alloc meshpackets static uint8_t bytes[meshtastic_MqttClientProxyMessage_size + 30]; // 12 for channel name and 16 for nodeid @@ -245,6 +246,11 @@ std::pair parseHostAndPort(String server, uint16_t port = 0) } return std::make_pair(std::move(server), port); } + +bool isDefaultServer(const String &host) +{ + return host.length() == 0 || host == default_mqtt_address; +} } // namespace void MQTT::mqttCallback(char *topic, byte *payload, unsigned int length) @@ -324,7 +330,7 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) } String host = parseHostAndPort(moduleConfig.mqtt.address).first; - isConfiguredForDefaultServer = host.length() == 0 || host == default_mqtt_address; + isConfiguredForDefaultServer = isDefaultServer(host); IPAddress ip; isMqttServerAddressPrivate = ip.fromString(host.c_str()) && isPrivateIpAddress(ip); @@ -408,7 +414,7 @@ void MQTT::reconnect() } #if HAS_NETWORKING // Defaults - int serverPort = 1883; + int serverPort = mqttPort; const char *serverAddr = default_mqtt_address; const char *mqttUsername = default_mqtt_username; const char *mqttPassword = default_mqtt_password; @@ -561,6 +567,23 @@ int32_t MQTT::runOnce() return 30000; } +bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config) +{ + String host; + uint16_t port; + std::tie(host, port) = parseHostAndPort(config.address, mqttPort); + const bool defaultServer = isDefaultServer(host); + if (defaultServer && config.tls_enabled) { + LOG_ERROR("Invalid MQTT config: TLS was enabled, but the default server does not support TLS"); + return false; + } + if (defaultServer && port != mqttPort) { + LOG_ERROR("Invalid MQTT config: Unsupported port '%d' for the default MQTT server", port); + return false; + } + return true; +} + void MQTT::publishNodeInfo() { // TODO: NodeInfo broadcast over MQTT only (NODENUM_BROADCAST_NO_LORA) diff --git a/src/mqtt/MQTT.h b/src/mqtt/MQTT.h index 42157fda9..f7e3864f8 100644 --- a/src/mqtt/MQTT.h +++ b/src/mqtt/MQTT.h @@ -61,6 +61,8 @@ class MQTT : private concurrency::OSThread bool isUsingDefaultServer() { return isConfiguredForDefaultServer; } + static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config); + protected: struct QueueEntry { std::string topic; diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index 3a4625aed..c00922548 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -800,6 +800,38 @@ void test_customMqttRoot(void) [] { return pubsub->subscriptions_.count("custom/2/e/test/+") && pubsub->subscriptions_.count("custom/2/e/PKI/+"); })); } +// Empty configuration is valid. +void test_configurationEmptyIsValid(void) +{ + meshtastic_ModuleConfig_MQTTConfig config; + + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); +} + +// Configuration with the default server is valid. +void test_configWithDefaultServer(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address}; + + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); +} + +// Configuration with the default server and port 8888 is invalid. +void test_configWithDefaultServerAndInvalidPort(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address ":8888"}; + + TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); +} + +// Configuration with the default server and tls_enabled = true is invalid. +void test_configWithDefaultServerAndInvalidTLSEnabled(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.tls_enabled = true}; + + TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); +} + void setup() { initializeTestEnvironment(); @@ -843,6 +875,10 @@ void setup() RUN_TEST(test_enabled); RUN_TEST(test_disabled); RUN_TEST(test_customMqttRoot); + RUN_TEST(test_configurationEmptyIsValid); + RUN_TEST(test_configWithDefaultServer); + RUN_TEST(test_configWithDefaultServerAndInvalidPort); + RUN_TEST(test_configWithDefaultServerAndInvalidTLSEnabled); exit(UNITY_END()); } #else From 7eb77276cdeca95b4a04360333b357921a6ca141 Mon Sep 17 00:00:00 2001 From: A_Ponzano Date: Mon, 17 Feb 2025 02:49:17 +0100 Subject: [PATCH 66/71] Add support for new NRF52 board, MeshLink (#5736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for MeshLink * Updated, enabled watchdog and added button definition * added eink variant and removed some compile errors * Small board json file edit * Finally got trunk working (somehow?), this is just cleanup with trunk fmt * Various improvements and cleanup. Removed the use of PIN_3V3_En and defined a specific WD_EN pin instead for better clarity. Will do a bit more testing asap to make sure everything still works as intended :) * Enable on-board QSPI Flash * run trunk fmt with clang-format --------- Co-authored-by: Ben Meadors Co-authored-by: Thomas Göttgens Co-authored-by: Austin --- boards/meshlink.json | 52 ++++++++ src/graphics/EInkDisplay2.cpp | 9 ++ src/mesh/generated/meshtastic/mesh.pb.h | 2 +- src/modules/SerialModule.cpp | 8 +- src/platform/nrf52/architecture.h | 2 +- src/platform/nrf52/main-nrf52.cpp | 5 + src/sleep.cpp | 5 +- variants/meshlink/platformio.ini | 30 +++++ variants/meshlink/variant.cpp | 23 ++++ variants/meshlink/variant.h | 153 ++++++++++++++++++++++++ variants/meshlink_eink/platformio.ini | 30 +++++ variants/meshlink_eink/variant.cpp | 23 ++++ variants/meshlink_eink/variant.h | 153 ++++++++++++++++++++++++ 13 files changed, 488 insertions(+), 7 deletions(-) create mode 100644 boards/meshlink.json create mode 100644 variants/meshlink/platformio.ini create mode 100644 variants/meshlink/variant.cpp create mode 100644 variants/meshlink/variant.h create mode 100644 variants/meshlink_eink/platformio.ini create mode 100644 variants/meshlink_eink/variant.cpp create mode 100644 variants/meshlink_eink/variant.h diff --git a/boards/meshlink.json b/boards/meshlink.json new file mode 100644 index 000000000..a608de88a --- /dev/null +++ b/boards/meshlink.json @@ -0,0 +1,52 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DMESHLINK -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x00B3"], + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "MeshLink", + "mcu": "nrf52840", + "variant": "meshlink", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd" + }, + "frameworks": ["arduino"], + "name": "MeshLink", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["nrfutil", "jlink", "nrfjprog", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.loraitalia.it", + "vendor": "LoraItalia" +} diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 6c85582c0..9702b0086 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -140,6 +140,15 @@ bool EInkDisplay::connect() adafruitDisplay->setRotation(3); adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); } +#elif defined(MESHLINK) + { + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); + + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(); + adafruitDisplay->setRotation(3); + adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); + } #elif defined(RAK4630) || defined(MAKERPYTHON) { if (eink_found) { diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 3353a020f..de8a1a353 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -1775,4 +1775,4 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; } /* extern "C" */ #endif -#endif +#endif \ No newline at end of file diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index bf53b1748..c6a95912b 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -60,7 +60,7 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; -#if defined(TTGO_T_ECHO) || defined(CANARYONE) +#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") {} static Print *serialPrint = &Serial; #elif defined(CONFIG_IDF_TARGET_ESP32C6) @@ -158,7 +158,7 @@ int32_t SerialModule::runOnce() Serial.begin(baud); Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); } -#elif !defined(TTGO_T_ECHO) && !defined(CANARYONE) +#elif !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(MESHLINK) if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { #ifdef ARCH_RP2040 Serial2.setFIFOSize(RX_BUFFER); @@ -214,7 +214,7 @@ int32_t SerialModule::runOnce() } } -#if !defined(TTGO_T_ECHO) && !defined(CANARYONE) +#if !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(MESHLINK) else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { processWXSerial(); @@ -416,7 +416,7 @@ uint32_t SerialModule::getBaudRate() */ void SerialModule::processWXSerial() { -#if !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) +#if !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) static unsigned int lastAveraged = 0; static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. static double dir_sum_sin = 0; diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index ce99244ba..3e4397686 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -127,4 +127,4 @@ #if !defined(PIN_SERIAL_RX) && !defined(NRF52840_XXAA) // No serial ports on this board - ONLY use segger in memory console #define USE_SEGGER -#endif +#endif \ No newline at end of file diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index ad4d7a881..8483d21c6 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -304,6 +304,11 @@ void cpuDeepSleep(uint32_t msecToWake) nrf_gpio_cfg_default(WB_I2C1_SDA); #endif #endif +#ifdef MESHLINK +#ifdef PIN_WD_EN + digitalWrite(PIN_WD_EN, LOW); +#endif +#endif #ifdef HELTEC_MESH_NODE_T114 nrf_gpio_cfg_default(PIN_GPS_PPS); diff --git a/src/sleep.cpp b/src/sleep.cpp index 161b6e107..437d7b88b 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -245,6 +245,9 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN #ifdef PIN_3V3_EN digitalWrite(PIN_3V3_EN, LOW); #endif +#ifdef PIN_WD_EN + digitalWrite(PIN_WD_EN, LOW); +#endif #endif ledBlink.set(false); @@ -530,4 +533,4 @@ void enableLoraInterrupt() } #endif } -#endif +#endif \ No newline at end of file diff --git a/variants/meshlink/platformio.ini b/variants/meshlink/platformio.ini new file mode 100644 index 000000000..180dddd49 --- /dev/null +++ b/variants/meshlink/platformio.ini @@ -0,0 +1,30 @@ +; MeshLink board developed by LoraItalia. NRF52840, eByte E22900M22S (Will also come with other frequencies), 25w MPPT solar charger (5v,12v,18v selectable), support for gps, buzzer, oled or e-ink display, 10 gpios, hardware watchdog +; https://www.loraitalia.it +; firmware for boards with or without oled display +[env:meshlink] +extends = nrf52840_base +board = meshlink +;board_check = true +build_flags = ${nrf52840_base.build_flags} -I variants/meshlink -D MESHLINK + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -D GPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -D EINK_DISPLAY_MODEL=GxEPD2_213_B74 + -D EINK_WIDTH=250 + -D EINK_HEIGHT=122 + -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -D EINK_LIMIT_FASTREFRESH=5 ; How many consecutive fast-refreshes are permitted + -D EINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates + -D EINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates + -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated + -D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. + -D EINK_HASQUIRK_VICIOUSFASTREFRESH ; Identify that pixels drawn by fast-refresh are harder to clear + + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/meshlink> +lib_deps = + ${nrf52840_base.lib_deps} + https://github.com/meshtastic/GxEPD2#55f618961db45a23eff0233546430f1e5a80f63a +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds +;upload_protocol = jlink \ No newline at end of file diff --git a/variants/meshlink/variant.cpp b/variants/meshlink/variant.cpp new file mode 100644 index 000000000..81a5097c4 --- /dev/null +++ b/variants/meshlink/variant.cpp @@ -0,0 +1,23 @@ +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, HIGH); // turn off the white led while booting + // otherwise it will stay lit for several seconds (could be annoying) + +#ifdef PIN_WD_EN + pinMode(PIN_WD_EN, OUTPUT); + digitalWrite(PIN_WD_EN, HIGH); // Enable the Watchdog at boot +#endif +} \ No newline at end of file diff --git a/variants/meshlink/variant.h b/variants/meshlink/variant.h new file mode 100644 index 000000000..54df03691 --- /dev/null +++ b/variants/meshlink/variant.h @@ -0,0 +1,153 @@ +#ifndef _VARIANT_MESHLINK_ +#define _VARIANT_MESHLINK_ +#ifndef MESHLINK +#define MESHLINK +#endif +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +// #define USE_LFXO // Board uses 32khz crystal for LF +#define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (2) +#define NUM_ANALOG_OUTPUTS (0) + +#define BUTTON_PIN (-1) // If defined, this will be used for user button presses, +#define BUTTON_NEED_PULLUP + +// LEDs +#define PIN_LED1 (24) // Built in white led for status +#define LED_BLUE PIN_LED1 +#define LED_BUILTIN PIN_LED1 + +#define LED_STATE_ON 0 // State when LED is litted +#define LED_INVERTED 1 + +// Testing USB detection +// #define NRF_APM + +/* + * Analog pins + */ +#define PIN_A1 (3) // P0.03/AIN1 +#define ADC_RESOLUTION 14 + +// Other pins +// #define PIN_AREF (2) +// static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (32 + 8) +#define PIN_SERIAL1_TX (7) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (8) +#define PIN_SPI_MOSI (32 + 9) +#define PIN_SPI_SCK (11) + +#define PIN_SPI1_MISO (23) +#define PIN_SPI1_MOSI (21) +#define PIN_SPI1_SCK (19) + +static const uint8_t SS = 12; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ +// #define USE_EINK + +#define PIN_EINK_CS (15) +#define PIN_EINK_BUSY (16) +#define PIN_EINK_DC (14) +#define PIN_EINK_RES (17) +#define PIN_EINK_SCLK (19) +#define PIN_EINK_MOSI (21) // also called SDI + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (1) +#define PIN_WIRE_SCL (27) + +// QSPI Pins +#define PIN_QSPI_SCK 19 +#define PIN_QSPI_CS 22 +#define PIN_QSPI_IO0 21 +#define PIN_QSPI_IO1 23 +#define PIN_QSPI_IO2 32 +#define PIN_QSPI_IO3 20 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES W25Q16JVUXIQ +#define EXTERNAL_FLASH_USE_QSPI + +#define USE_SX1262 +#define SX126X_CS (12) +#define SX126X_DIO1 (32 + 1) +#define SX126X_BUSY (32 + 3) +#define SX126X_RESET (6) +// #define SX126X_RXEN (13) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// pin 25 is used to enable or disable the watchdog. This pin has to be disabled when cpu is put to sleep +// otherwise the timer will expire and wd will reboot the cpu +#define PIN_WD_EN (25) + +#define PIN_GPS_PPS (26) // Pulse per second input from the GPS + +#define GPS_TX_PIN PIN_SERIAL1_RX // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN PIN_SERIAL1_TX // This is for bits going TOWARDS the GPS + +// #define GPS_THREAD_INTERVAL 50 + +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press +#define PIN_GPS_EN (0) +#define GPS_EN_ACTIVE LOW + +#define PIN_BUZZER (31) // P0.31/AIN7 + +// Battery +// The battery sense is hooked to pin A0 (2) +#define BATTERY_PIN (2) +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.42 // fine tuning of voltage + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ +#endif \ No newline at end of file diff --git a/variants/meshlink_eink/platformio.ini b/variants/meshlink_eink/platformio.ini new file mode 100644 index 000000000..db3647e73 --- /dev/null +++ b/variants/meshlink_eink/platformio.ini @@ -0,0 +1,30 @@ +; MeshLink board developed by LoraItalia. NRF52840, eByte E22900M22S (Will also come with other frequencies), 25w MPPT solar charger (5v,12v,18v selectable), support for gps, buzzer, oled or e-ink display, 10 gpios, hardware watchdog +; https://www.loraitalia.it +; firmware for boards with a 250x122 e-ink display +[env:meshlink_eink] +extends = nrf52840_base +board = meshlink +;board_check = true +build_flags = ${nrf52840_base.build_flags} -I variants/meshlink_eink -D MESHLINK + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + -D GPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -D EINK_DISPLAY_MODEL=GxEPD2_213_B74 + -D EINK_WIDTH=250 + -D EINK_HEIGHT=122 + -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -D EINK_LIMIT_FASTREFRESH=5 ; How many consecutive fast-refreshes are permitted + -D EINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates + -D EINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates + -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated + -D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. + -D EINK_HASQUIRK_VICIOUSFASTREFRESH ; Identify that pixels drawn by fast-refresh are harder to clear + + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/meshlink_eink> +lib_deps = + ${nrf52840_base.lib_deps} + https://github.com/meshtastic/GxEPD2#55f618961db45a23eff0233546430f1e5a80f63a +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds +;upload_protocol = jlink \ No newline at end of file diff --git a/variants/meshlink_eink/variant.cpp b/variants/meshlink_eink/variant.cpp new file mode 100644 index 000000000..81a5097c4 --- /dev/null +++ b/variants/meshlink_eink/variant.cpp @@ -0,0 +1,23 @@ +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, HIGH); // turn off the white led while booting + // otherwise it will stay lit for several seconds (could be annoying) + +#ifdef PIN_WD_EN + pinMode(PIN_WD_EN, OUTPUT); + digitalWrite(PIN_WD_EN, HIGH); // Enable the Watchdog at boot +#endif +} \ No newline at end of file diff --git a/variants/meshlink_eink/variant.h b/variants/meshlink_eink/variant.h new file mode 100644 index 000000000..b605d7082 --- /dev/null +++ b/variants/meshlink_eink/variant.h @@ -0,0 +1,153 @@ +#ifndef _VARIANT_MESHLINK_ +#define _VARIANT_MESHLINK_ +#ifndef MESHLINK +#define MESHLINK +#endif +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +// #define USE_LFXO // Board uses 32khz crystal for LF +#define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (2) +#define NUM_ANALOG_OUTPUTS (0) + +#define BUTTON_PIN (-1) // If defined, this will be used for user button presses, +#define BUTTON_NEED_PULLUP + +// LEDs +#define PIN_LED1 (24) // Built in white led for status +#define LED_BLUE PIN_LED1 +#define LED_BUILTIN PIN_LED1 + +#define LED_STATE_ON 0 // State when LED is litted +#define LED_INVERTED 1 + +// Testing USB detection +// #define NRF_APM + +/* + * Analog pins + */ +#define PIN_A1 (3) // P0.03/AIN1 +#define ADC_RESOLUTION 14 + +// Other pins +// #define PIN_AREF (2) +// static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (32 + 8) +#define PIN_SERIAL1_TX (7) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (8) +#define PIN_SPI_MOSI (32 + 9) +#define PIN_SPI_SCK (11) + +#define PIN_SPI1_MISO (23) +#define PIN_SPI1_MOSI (21) +#define PIN_SPI1_SCK (19) + +static const uint8_t SS = 12; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ +#define USE_EINK + +#define PIN_EINK_CS (15) +#define PIN_EINK_BUSY (16) +#define PIN_EINK_DC (14) +#define PIN_EINK_RES (17) +#define PIN_EINK_SCLK (19) +#define PIN_EINK_MOSI (21) // also called SDI + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (1) +#define PIN_WIRE_SCL (27) + +// QSPI Pins +#define PIN_QSPI_SCK 19 +#define PIN_QSPI_CS 22 +#define PIN_QSPI_IO0 21 +#define PIN_QSPI_IO1 23 +#define PIN_QSPI_IO2 32 +#define PIN_QSPI_IO3 20 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES W25Q16JVUXIQ +#define EXTERNAL_FLASH_USE_QSPI + +#define USE_SX1262 +#define SX126X_CS (12) +#define SX126X_DIO1 (32 + 1) +#define SX126X_BUSY (32 + 3) +#define SX126X_RESET (6) +// #define SX126X_RXEN (13) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// pin 25 is used to enable or disable the watchdog. This pin has to be disabled when cpu is put to sleep +// otherwise the timer will expire and wd will reboot the cpu +#define PIN_WD_EN (25) + +#define PIN_GPS_PPS (26) // Pulse per second input from the GPS + +#define GPS_TX_PIN PIN_SERIAL1_RX // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN PIN_SERIAL1_TX // This is for bits going TOWARDS the GPS + +// #define GPS_THREAD_INTERVAL 50 + +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press +#define PIN_GPS_EN (0) +#define GPS_EN_ACTIVE LOW + +#define PIN_BUZZER (31) // P0.31/AIN7 + +// Battery +// The battery sense is hooked to pin A0 (2) +#define BATTERY_PIN (2) +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.42 // fine tuning of voltage + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ +#endif \ No newline at end of file From 3b0232de1b6282eacfbff6e50b68fca7e67b8511 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 17 Feb 2025 13:03:44 -0800 Subject: [PATCH 67/71] Validate MQTT config by testing a connection (#6076) --- src/mqtt/MQTT.cpp | 158 +++++++++++++++++++++++++--------------- src/mqtt/MQTT.h | 30 ++++---- test/test_mqtt/MQTT.cpp | 63 +++++++++++++++- 3 files changed, 173 insertions(+), 78 deletions(-) diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 67eba82a6..5f16f909f 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -41,7 +41,6 @@ MQTT *mqtt; namespace { constexpr int reconnectMax = 5; -constexpr uint16_t mqttPort = 1883; // FIXME - this size calculation is super sloppy, but it will go away once we dynamically alloc meshpackets static uint8_t bytes[meshtastic_MqttClientProxyMessage_size + 30]; // 12 for channel name and 16 for nodeid @@ -251,6 +250,68 @@ bool isDefaultServer(const String &host) { return host.length() == 0 || host == default_mqtt_address; } + +struct PubSubConfig { + explicit PubSubConfig(const meshtastic_ModuleConfig_MQTTConfig &config) + { + if (*config.address) { + serverAddr = config.address; + mqttUsername = config.username; + mqttPassword = config.password; + } + if (config.tls_enabled) { + serverPort = 8883; + } + std::tie(serverAddr, serverPort) = parseHostAndPort(serverAddr.c_str(), serverPort); + } + + // Defaults + static constexpr uint16_t defaultPort = 1883; + uint16_t serverPort = defaultPort; + String serverAddr = default_mqtt_address; + const char *mqttUsername = default_mqtt_username; + const char *mqttPassword = default_mqtt_password; +}; + +#if HAS_NETWORKING +bool connectPubSub(const PubSubConfig &config, PubSubClient &pubSub, Client &client) +{ + pubSub.setBufferSize(1024); + pubSub.setClient(client); + pubSub.setServer(config.serverAddr.c_str(), config.serverPort); + + LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: %s", config.serverAddr.c_str(), + config.serverPort, config.mqttUsername, config.mqttPassword); + + const bool connected = pubSub.connect(owner.id, config.mqttUsername, config.mqttPassword); + if (connected) { + LOG_INFO("MQTT connected"); + } else { + LOG_WARN("Failed to connect to MQTT server"); + } + return connected; +} +#endif + +inline bool isConnectedToNetwork() +{ +#if HAS_WIFI + return WiFi.isConnected(); +#elif HAS_ETHERNET + return Ethernet.linkStatus() == LinkON; +#else + return false; +#endif +} + +/** return true if we have a channel that wants uplink/downlink or map reporting is enabled + */ +bool wantsLink() +{ + const bool hasChannelorMapReport = + moduleConfig.mqtt.enabled && (moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled()); + return hasChannelorMapReport && (moduleConfig.mqtt.proxy_to_client_enabled || isConnectedToNetwork()); +} } // namespace void MQTT::mqttCallback(char *topic, byte *payload, unsigned int length) @@ -413,46 +474,18 @@ void MQTT::reconnect() return; // Don't try to connect directly to the server } #if HAS_NETWORKING - // Defaults - int serverPort = mqttPort; - const char *serverAddr = default_mqtt_address; - const char *mqttUsername = default_mqtt_username; - const char *mqttPassword = default_mqtt_password; + const PubSubConfig config(moduleConfig.mqtt); MQTTClient *clientConnection = mqttClient.get(); - - if (*moduleConfig.mqtt.address) { - serverAddr = moduleConfig.mqtt.address; - mqttUsername = moduleConfig.mqtt.username; - mqttPassword = moduleConfig.mqtt.password; - } -#if HAS_WIFI && !defined(ARCH_PORTDUINO) && !defined(CONFIG_IDF_TARGET_ESP32C6) +#if MQTT_SUPPORTS_TLS if (moduleConfig.mqtt.tls_enabled) { - // change default for encrypted to 8883 - try { - serverPort = 8883; - wifiSecureClient.setInsecure(); - LOG_INFO("Use TLS-encrypted session"); - clientConnection = &wifiSecureClient; - } catch (const std::exception &e) { - LOG_ERROR("MQTT ERROR: %s", e.what()); - } + mqttClientTLS.setInsecure(); + LOG_INFO("Use TLS-encrypted session"); + clientConnection = &mqttClientTLS; } else { LOG_INFO("Use non-TLS-encrypted session"); } #endif - std::pair hostAndPort = parseHostAndPort(serverAddr, serverPort); - serverAddr = hostAndPort.first.c_str(); - serverPort = hostAndPort.second; - pubSub.setServer(serverAddr, serverPort); - pubSub.setBufferSize(1024); - - LOG_INFO("Connect directly to MQTT server %s, port: %d, username: %s, password: %s", serverAddr, serverPort, mqttUsername, - mqttPassword); - - pubSub.setClient(*clientConnection); - bool connected = pubSub.connect(owner.id, mqttUsername, mqttPassword); - if (connected) { - LOG_INFO("MQTT connected"); + if (connectPubSub(config, pubSub, *clientConnection)) { enabled = true; // Start running background process again runASAP = true; reconnectCount = 0; @@ -507,23 +540,6 @@ void MQTT::sendSubscriptions() #endif } -bool MQTT::wantsLink() const -{ - bool hasChannelorMapReport = - moduleConfig.mqtt.enabled && (moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled()); - - if (hasChannelorMapReport && moduleConfig.mqtt.proxy_to_client_enabled) - return true; - -#if HAS_WIFI - return hasChannelorMapReport && WiFi.isConnected(); -#endif -#if HAS_ETHERNET - return hasChannelorMapReport && Ethernet.linkStatus() == LinkON; -#endif - return false; -} - int32_t MQTT::runOnce() { #if HAS_NETWORKING @@ -567,18 +583,42 @@ int32_t MQTT::runOnce() return 30000; } -bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config) +bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client) { - String host; - uint16_t port; - std::tie(host, port) = parseHostAndPort(config.address, mqttPort); - const bool defaultServer = isDefaultServer(host); + const PubSubConfig parsed(config); + + if (config.enabled && !config.proxy_to_client_enabled) { +#if HAS_NETWORKING + std::unique_ptr clientConnection; + if (config.tls_enabled) { +#if MQTT_SUPPORTS_TLS + MQTTClientTLS *tlsClient = new MQTTClientTLS; + clientConnection.reset(tlsClient); + tlsClient->setInsecure(); +#else + LOG_ERROR("Invalid MQTT config: tls_enabled is not supported on this node"); + return false; +#endif + } else { + clientConnection.reset(new MQTTClient); + } + std::unique_ptr pubSub(new PubSubClient); + if (isConnectedToNetwork()) { + return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection); + } +#else + LOG_ERROR("Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"); + return false; +#endif + } + + const bool defaultServer = isDefaultServer(parsed.serverAddr); if (defaultServer && config.tls_enabled) { LOG_ERROR("Invalid MQTT config: TLS was enabled, but the default server does not support TLS"); return false; } - if (defaultServer && port != mqttPort) { - LOG_ERROR("Invalid MQTT config: Unsupported port '%d' for the default MQTT server", port); + if (defaultServer && parsed.serverPort != PubSubConfig::defaultPort) { + LOG_ERROR("Invalid MQTT config: Unsupported port '%d' for the default MQTT server", parsed.serverPort); return false; } return true; diff --git a/src/mqtt/MQTT.h b/src/mqtt/MQTT.h index f7e3864f8..5cda90218 100644 --- a/src/mqtt/MQTT.h +++ b/src/mqtt/MQTT.h @@ -10,12 +10,10 @@ #endif #if HAS_WIFI #include -#if !defined(ARCH_PORTDUINO) -#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR < 3 +#if __has_include() #include #endif #endif -#endif #if HAS_ETHERNET #include #endif @@ -61,7 +59,8 @@ class MQTT : private concurrency::OSThread bool isUsingDefaultServer() { return isConfiguredForDefaultServer; } - static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config); + /// Validate the meshtastic_ModuleConfig_MQTTConfig. + static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config) { return isValidConfig(config, nullptr); } protected: struct QueueEntry { @@ -78,22 +77,23 @@ class MQTT : private concurrency::OSThread #ifndef PIO_UNIT_TESTING private: #endif - // supposedly the current version is busted: - // http://www.iotsharing.com/2017/08/how-to-use-esp32-mqtts-with-mqtts-mosquitto-broker-tls-ssl.html #if HAS_WIFI using MQTTClient = WiFiClient; -#if !defined(ARCH_PORTDUINO) -#if (defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR < 3) || defined(RPI_PICO) - WiFiClientSecure wifiSecureClient; +#if __has_include() + using MQTTClientTLS = WiFiClientSecure; +#define MQTT_SUPPORTS_TLS 1 #endif -#endif -#endif -#if HAS_ETHERNET +#elif HAS_ETHERNET using MQTTClient = EthernetClient; +#else + using MQTTClient = void; #endif #if HAS_NETWORKING std::unique_ptr mqttClient; +#if MQTT_SUPPORTS_TLS + MQTTClientTLS mqttClientTLS; +#endif PubSubClient pubSub; explicit MQTT(std::unique_ptr mqttClient); #endif @@ -109,10 +109,6 @@ class MQTT : private concurrency::OSThread uint32_t map_position_precision = default_map_position_precision; uint32_t map_publish_interval_msecs = default_map_publish_interval_secs * 1000; - /** return true if we have a channel that wants uplink/downlink or map reporting is enabled - */ - bool wantsLink() const; - /** Attempt to connect to server if necessary */ void reconnect(); @@ -124,6 +120,8 @@ class MQTT : private concurrency::OSThread /// Callback for direct mqtt subscription messages static void mqttCallback(char *topic, byte *payload, unsigned int length); + static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client); + /// Called when a new publish arrives from the MQTT server void onReceive(char *topic, byte *payload, size_t length); diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index c00922548..50a98001a 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -94,6 +94,7 @@ class MockPubSubServer : public WiFiClient int connect(IPAddress ip, uint16_t port) override { + port_ = port; if (refuseConnection_) return 0; connected_ = true; @@ -101,6 +102,8 @@ class MockPubSubServer : public WiFiClient } int connect(const char *host, uint16_t port) override { + host_ = host; + port_ = port; if (refuseConnection_) return 0; connected_ = true; @@ -197,6 +200,8 @@ class MockPubSubServer : public WiFiClient bool connected_ = false; bool refuseConnection_ = false; // Simulate a failed connection. uint32_t ipAddress_ = 0x01010101; // IP address of the MQTT server. + std::string host_; // Requested host. + uint16_t port_; // Requested port. std::list buffer_; // Buffer of messages for the pubSub client to receive. std::string command_; // Current command received from the pubSub client. std::set subscriptions_; // Topics that the pubSub client has subscribed to. @@ -242,6 +247,7 @@ class MQTTUnitTest : public MQTT mqttClient.release(); delete pubsub; } + using MQTT::isValidConfig; using MQTT::reconnect; int queueSize() { return mqttQueue.numUsed(); } void reportToMap(std::optional precision = std::nullopt) @@ -801,13 +807,25 @@ void test_customMqttRoot(void) } // Empty configuration is valid. -void test_configurationEmptyIsValid(void) +void test_configEmptyIsValid(void) { - meshtastic_ModuleConfig_MQTTConfig config; + meshtastic_ModuleConfig_MQTTConfig config = {}; TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } +// Empty 'enabled' configuration is valid. +void test_configEnabledEmptyIsValid(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true}; + MockPubSubServer client; + + TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(client.connected_); + TEST_ASSERT_EQUAL_STRING(default_mqtt_address, client.host_.c_str()); + TEST_ASSERT_EQUAL(1883, client.port_); +} + // Configuration with the default server is valid. void test_configWithDefaultServer(void) { @@ -832,6 +850,41 @@ void test_configWithDefaultServerAndInvalidTLSEnabled(void) TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); } +// isValidConfig connects to a custom host and port. +void test_configCustomHostAndPort(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server:1234"}; + MockPubSubServer client; + + TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(client.connected_); + TEST_ASSERT_EQUAL_STRING("server", client.host_.c_str()); + TEST_ASSERT_EQUAL(1234, client.port_); +} + +// isValidConfig returns false if a connection cannot be established. +void test_configWithConnectionFailure(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server"}; + MockPubSubServer client; + client.refuseConnection_ = true; + + TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); +} + +// isValidConfig returns true when tls_enabled is supported, or false otherwise. +void test_configWithTLSEnabled(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server", .tls_enabled = true}; + MockPubSubServer client; + +#if MQTT_SUPPORTS_TLS + TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); +#else + TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); +#endif +} + void setup() { initializeTestEnvironment(); @@ -875,10 +928,14 @@ void setup() RUN_TEST(test_enabled); RUN_TEST(test_disabled); RUN_TEST(test_customMqttRoot); - RUN_TEST(test_configurationEmptyIsValid); + RUN_TEST(test_configEmptyIsValid); + RUN_TEST(test_configEnabledEmptyIsValid); RUN_TEST(test_configWithDefaultServer); RUN_TEST(test_configWithDefaultServerAndInvalidPort); RUN_TEST(test_configWithDefaultServerAndInvalidTLSEnabled); + RUN_TEST(test_configCustomHostAndPort); + RUN_TEST(test_configWithConnectionFailure); + RUN_TEST(test_configWithTLSEnabled); exit(UNITY_END()); } #else From c67aa25d19aaa4773fc3ee457ad421c9daf2c01d Mon Sep 17 00:00:00 2001 From: noahhaon <170715+noahhaon@users.noreply.github.com> Date: Tue, 18 Feb 2025 20:25:55 +0100 Subject: [PATCH 68/71] Add missing traceroute fields to serialized JSON output (#6087) --- src/serialization/MeshPacketSerializer.cpp | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index 2f0d881f2..2c1dc0ca7 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -220,7 +220,11 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg, &scratch)) { decoded = &scratch; - JSONArray route; // Route this message took + JSONArray route; // Route this message took + JSONArray routeBack; // Route this message took back + JSONArray snrTowards; // Snr for forward route + JSONArray snrBack; // Snr for reverse route + // Lambda function for adding a long name to the route auto addToRoute = [](JSONArray *route, NodeNum num) { char long_name[40] = "Unknown"; @@ -236,7 +240,24 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, } addToRoute(&route, mp->from); // Ended at the original destination (source of response) + addToRoute(&routeBack, mp->from); // Started at the original destination (source of response) + for (uint8_t i = 0; i < decoded->route_back_count; i++) { + addToRoute(&routeBack, decoded->route_back[i]); + } + addToRoute(&routeBack, mp->to); // Ended at the original transmitter (destination of response) + + for (uint8_t i = 0; i < decoded->snr_back_count; i++) { + snrBack.push_back(new JSONValue((float)decoded->snr_back[i] / 4)); + } + + for (uint8_t i = 0; i < decoded->snr_towards_count; i++) { + snrTowards.push_back(new JSONValue((float)decoded->snr_towards[i] / 4)); + } + msgPayload["route"] = new JSONValue(route); + msgPayload["route_back"] = new JSONValue(routeBack); + msgPayload["snr_back"] = new JSONValue(snrBack); + msgPayload["snr_towards"] = new JSONValue(snrTowards); jsonObj["payload"] = new JSONValue(msgPayload); } else if (shouldLog) { LOG_ERROR(errStr, msgType.c_str()); From 191ca8ce124d9adbc9843ec5de0a3edf3f2c0af9 Mon Sep 17 00:00:00 2001 From: rcarteraz Date: Tue, 18 Feb 2025 14:56:13 -0700 Subject: [PATCH 69/71] update readme add logo image (#6088) --- .github/meshtastic_logo.png | Bin 0 -> 91300 bytes README.md | 31 ++++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 .github/meshtastic_logo.png diff --git a/.github/meshtastic_logo.png b/.github/meshtastic_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..11c5db18c71249b1ac78d30ca2ffeafd5629912b GIT binary patch literal 91300 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 K{aMp7;(q|(7(V|1 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index ca8a924fd..f34bf1839 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# Meshtastic Firmware + + + + + + ## Overview -This repository contains the device firmware for the Meshtastic project. +This repository contains the official device firmware for Meshtastic, an open-source LoRa mesh networking project designed for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware supports various hardware platforms, including ESP32, nRF52, RP2040/RP2350, and Linux-based devices. -- **[Building Instructions](https://meshtastic.org/docs/development/firmware/build)** -- **[Flashing Instructions](https://meshtastic.org/docs/getting-started/flashing-firmware/)** +Meshtastic enables text messaging, location sharing, and telemetry over a decentralized mesh network, making it ideal for outdoor adventures, emergency preparedness, and remote operations. + +### Get Started + +- 🔧 **[Building Instructions](https://meshtastic.org/docs/development/firmware/build)** – Learn how to compile the firmware from source. +- âš¡ **[Flashing Instructions](https://meshtastic.org/docs/getting-started/flashing-firmware/)** – Install or update the firmware on your device. + +Join our community and help improve Meshtastic! 🚀 ## Stats -![Alt](https://repobeats.axiom.co/api/embed/a92f097d9197ae853e780ec53d7d126e545629ab.svg "Repobeats analytics image") +![Alt](https://repobeats.axiom.co/api/embed/8025e56c482ec63541593cc5bd322c19d5c0bdcf.svg "Repobeats analytics image") From 79b3a1e60e6080d6b27ea43e98012a9cf32fa9a3 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Wed, 19 Feb 2025 20:58:41 +0900 Subject: [PATCH 70/71] chore: update unishox2.h (#6092) occuring -> occurring --- src/mesh/compression/unishox2.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mesh/compression/unishox2.h b/src/mesh/compression/unishox2.h index 5e2cc8b4c..823128f02 100644 --- a/src/mesh/compression/unishox2.h +++ b/src/mesh/compression/unishox2.h @@ -291,8 +291,8 @@ extern int unishox2_decompress_simple(const char *in, int len, char *out); * @param[in] olen length of 'out' buffer in bytes. Can be omitted if sufficient buffer is provided * @param[in] usx_hcodes Horizontal codes (array of bytes). See macro section for samples. * @param[in] usx_hcode_lens Length of each element in usx_hcodes array - * @param[in] usx_freq_seq Frequently occuring sequences. See USX_FREQ_SEQ_* macros for samples - * @param[in] usx_templates Templates of frequently occuring patterns. See USX_TEMPLATES macro. + * @param[in] usx_freq_seq Frequently occurring sequences. See USX_FREQ_SEQ_* macros for samples + * @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro. */ extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], @@ -310,8 +310,8 @@ extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(ch * @param[in] olen length of 'out' buffer in bytes. Can be omitted if sufficient buffer is provided * @param[in] usx_hcodes Horizontal codes (array of bytes). See macro section for samples. * @param[in] usx_hcode_lens Length of each element in usx_hcodes array - * @param[in] usx_freq_seq Frequently occuring sequences. See USX_FREQ_SEQ_* macros for samples - * @param[in] usx_templates Templates of frequently occuring patterns. See USX_TEMPLATES macro. + * @param[in] usx_freq_seq Frequently occurring sequences. See USX_FREQ_SEQ_* macros for samples + * @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro. */ extern int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], @@ -344,4 +344,4 @@ extern int unishox2_decompress_lines(const char *in, int len, UNISHOX_API_OUT_AN const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[], struct us_lnk_lst *prev_lines); -#endif \ No newline at end of file +#endif From 5da5803c4c99d2fbce6ef8cf45ce1b1f6656c721 Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 19 Feb 2025 07:14:46 -0500 Subject: [PATCH 71/71] Trunk: Annotate PRs and Auto-Upgrade (#6091) --- .github/workflows/nightly.yml | 20 ++++++++++++-- .github/workflows/trunk_annotate.pr.yml | 26 +++++++++++++++++++ .../{trunk-check.yml => trunk_check.yml} | 0 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/trunk_annotate.pr.yml rename .github/workflows/{trunk-check.yml => trunk_check.yml} (100%) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index e249823a7..7a35e2b99 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -4,9 +4,11 @@ on: - cron: 0 8 * * 1-5 workflow_dispatch: {} +permissions: read-all + jobs: trunk_check: - name: Trunk Check Upload + name: Trunk Check and Upload runs-on: ubuntu-latest steps: @@ -14,6 +16,20 @@ jobs: uses: actions/checkout@v4 - name: Trunk Check - uses: trunk-io/trunk-action@782e83f803ca6e369f035d64c6ba2768174ba61b + uses: trunk-io/trunk-action@v1 with: trunk-token: ${{ secrets.TRUNK_TOKEN }} + + trunk_upgrade: + name: Trunk Upgrade (PR) + runs-on: ubuntu-latest + permissions: + contents: write # For trunk to create PRs + pull-requests: write # For trunk to create PRs + steps: + - name: Checkout + uses: actions/checkout@v4 + + # See https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades + - name: Trunk Upgrade + uses: trunk-io/trunk-action/upgrade@v1 diff --git a/.github/workflows/trunk_annotate.pr.yml b/.github/workflows/trunk_annotate.pr.yml new file mode 100644 index 000000000..ac5cdc0d5 --- /dev/null +++ b/.github/workflows/trunk_annotate.pr.yml @@ -0,0 +1,26 @@ +name: Annotate PR with trunk issues +# See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#getting-inline-annotations-for-fork-prs + +on: + workflow_run: + workflows: [Pull Request] # Name from `trunk_check.yml` + types: [completed] + +permissions: read-all + +jobs: + trunk_check: + name: Trunk Code Quality Annotate + runs-on: ubuntu-latest + permissions: + checks: write # For trunk to post annotations + contents: read # For repo checkout + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Trunk Check + uses: trunk-io/trunk-action@v1 + with: + post-annotations: true diff --git a/.github/workflows/trunk-check.yml b/.github/workflows/trunk_check.yml similarity index 100% rename from .github/workflows/trunk-check.yml rename to .github/workflows/trunk_check.yml