diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml index 5c1c453dd..350ca290c 100644 --- a/.github/actions/setup-base/action.yml +++ b/.github/actions/setup-base/action.yml @@ -23,7 +23,7 @@ runs: sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x cache: pip diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index fa780aaf6..6ff12221b 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x cache: pip @@ -374,7 +374,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -443,7 +443,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -498,7 +498,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml index 13d3d1b4e..d8ff6e631 100644 --- a/.github/workflows/package_pio_deps.yml +++ b/.github/workflows/package_pio_deps.yml @@ -31,7 +31,7 @@ jobs: repository: ${{github.event.pull_request.head.repo.full_name}} - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml index 93114e2c7..5fca90961 100644 --- a/.github/workflows/pr_enforce_labels.yml +++ b/.github/workflows/pr_enforce_labels.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Check for PR labels - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const labels = context.payload.pull_request.labels.map(label => label.name); diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index 786feeced..4e285852d 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -177,7 +177,7 @@ jobs: - name: Comment test results on PR if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const fs = require('fs'); diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index ccd99e792..486f4b1a6 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -63,7 +63,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 5a11fdfa8..32e2c2c8b 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Stale PR+Issues - uses: actions/stale@v9.1.0 + uses: actions/stale@v10.0.0 with: days-before-stale: 45 exempt-issue-labels: pinned,3.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 52f180aa2..942659348 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,7 +47,7 @@ jobs: pio upgrade - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 22 diff --git a/.github/workflows/trunk_format_pr.yml b/.github/workflows/trunk_format_pr.yml index 2d191fc44..51082fc5f 100644 --- a/.github/workflows/trunk_format_pr.yml +++ b/.github/workflows/trunk_format_pr.yml @@ -39,7 +39,7 @@ jobs: git push - name: Comment on PR - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 874715638..e10e20a04 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,12 +9,12 @@ plugins: lint: enabled: - checkov@3.2.469 - - renovate@41.93.2 + - renovate@41.94.0 - prettier@3.6.2 - trufflehog@3.90.5 - yamllint@1.37.1 - bandit@1.8.6 - - trivy@0.65.0 + - trivy@0.66.0 - taplo@0.10.0 - ruff@0.12.11 - isort@6.0.1 @@ -23,7 +23,7 @@ lint: - svgo@4.0.0 - actionlint@1.7.7 - flake8@7.3.0 - - hadolint@2.12.1-beta + - hadolint@2.13.1 - shfmt@3.6.0 - shellcheck@0.11.0 - black@25.1.0 diff --git a/bin/device-install.bat b/bin/device-install.bat index 24c841e4b..56de4dc10 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -119,11 +119,10 @@ IF NOT "__%PYTHON%__"=="____" ( CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..." !ESPTOOL_CMD! >nul 2>&1 -IF %ERRORLEVEL% GEQ 2 ( - @REM esptool exits with code 1 if help is displayed. +IF %ERRORLEVEL% EQU 9009 ( + @REM 9009 = command not found on Windows CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!" EXIT /B 1 - GOTO eof ) IF %DEBUG% EQU 1 ( CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps." diff --git a/bin/device-install.sh b/bin/device-install.sh index c2ba7539a..98937f29a 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -5,6 +5,10 @@ BPS_RESET=false TFT_BUILD=false MCU="" +# Constants +RESET_BAUD=1200 +FIRMWARE_OFFSET=0x00 + # Variant groups BIGDB_8MB=( "picomputer-s3" @@ -121,7 +125,7 @@ while [ $# -gt 0 ]; do done if [[ $BPS_RESET == true ]]; then - $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status + $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status exit 0 fi @@ -202,7 +206,7 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then echo "Trying to flash ${FILENAME}, but first erasing and writing system information" $ESPTOOL_CMD erase-flash - $ESPTOOL_CMD write-flash 0x00 "${FILENAME}" + $ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}" echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" $ESPTOOL_CMD write-flash $OTA_OFFSET "${OTAFILE}" echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" diff --git a/bin/device-update.bat b/bin/device-update.bat index 9077ae5b9..a263da992 100755 --- a/bin/device-update.bat +++ b/bin/device-update.bat @@ -6,6 +6,8 @@ SET "SCRIPT_NAME=%~nx0" SET "DEBUG=0" SET "PYTHON=" SET "ESPTOOL_BAUD=115200" +SET "RESET_BAUD=1200" +SET "UPDATE_OFFSET=0x10000" SET "ESPTOOL_CMD=" SET "LOGCOUNTER=0" SET "CHANGE_MODE=0" @@ -85,14 +87,13 @@ IF "!FILENAME:update=!"=="!FILENAME!" ( ) :skip-filename -SET "ESPTOOL_BAUD=1200" CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( - SET "ESPTOOL_CMD=!PYTHON! -m esptool" + SET "ESPTOOL_CMD=""!PYTHON!"" -m esptool" CALL :LOG_MESSAGE DEBUG "Python interpreter supplied." ) ELSE ( - CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool... + CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..." WHERE esptool >nul 2>&1 IF %ERRORLEVEL% EQU 0 ( @REM WHERE exits with code 0 if esptool is found. @@ -105,11 +106,11 @@ IF NOT "__%PYTHON%__"=="____" ( CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..." !ESPTOOL_CMD! >nul 2>&1 -IF %ERRORLEVEL% GEQ 2 ( - @REM esptool exits with code 1 if help is displayed. +CALL :LOG_MESSAGE DEBUG "esptool exit code: %ERRORLEVEL%" +IF %ERRORLEVEL% EQU 9009 ( + @REM 9009 = command not found on Windows CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!" EXIT /B 1 - GOTO eof ) IF %DEBUG% EQU 1 ( CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps." @@ -127,13 +128,13 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." IF %CHANGE_MODE% EQU 1 ( @REM Attempt to change mode via 1200bps Reset. - CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status + CALL :RUN_ESPTOOL !RESET_BAUD! --after no_reset read_flash_status GOTO eof ) @REM Flashing operations. -CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash 0x10000 "!FILENAME!" || GOTO eof +CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET !UPDATE_OFFSET!..." +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !UPDATE_OFFSET! "!FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Script complete!." diff --git a/bin/device-update.sh b/bin/device-update.sh index 7f603e070..6f29496e9 100755 --- a/bin/device-update.sh +++ b/bin/device-update.sh @@ -3,6 +3,11 @@ PYTHON=${PYTHON:-$(which python3 python|head -n 1)} CHANGE_MODE=false +# Constants +FLASH_BAUD=115200 +RESET_BAUD=1200 +UPDATE_OFFSET=0x10000 + # Determine the correct esptool command to use if "$PYTHON" -m esptool version >/dev/null 2>&1; then ESPTOOL_CMD="$PYTHON -m esptool" @@ -64,7 +69,7 @@ done shift "$((OPTIND-1))" if [ "$CHANGE_MODE" = true ]; then - $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status + $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status exit 0 fi @@ -75,7 +80,7 @@ fi if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then echo "Trying to flash update ${FILENAME}" - $ESPTOOL_CMD --baud 115200 write-flash 0x10000 "${FILENAME}" + $ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}" else show_help echo "Invalid file: ${FILENAME}" diff --git a/src/Power.cpp b/src/Power.cpp index a123fe984..06c6a9089 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -833,16 +833,25 @@ void Power::readPowerStatus() newStatus.notifyObservers(&powerStatus2); #ifdef DEBUG_HEAP if (lastheap != memGet.getFreeHeap()) { - std::string threadlist = "Threads running:"; + // Use stack-allocated buffer to avoid heap allocations in monitoring code + char threadlist[256] = "Threads running:"; + int threadlistLen = strlen(threadlist); int running = 0; for (int i = 0; i < MAX_THREADS; i++) { auto thread = concurrency::mainController.get(i); if ((thread != nullptr) && (thread->enabled)) { - threadlist += vformat(" %s", thread->ThreadName.c_str()); + // Use snprintf to safely append to stack buffer without heap allocation + int remaining = sizeof(threadlist) - threadlistLen - 1; + if (remaining > 0) { + int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str()); + if (written > 0 && written < remaining) { + threadlistLen += written; + } + } running++; } } - LOG_DEBUG(threadlist.c_str()); + LOG_DEBUG(threadlist); LOG_DEBUG("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false)); lastheap = memGet.getFreeHeap(); @@ -856,15 +865,19 @@ void Power::readPowerStatus() sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]); auto newHeap = memGet.getFreeHeap(); - std::string heapTopic = - (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/heap/") + std::string(mac); - std::string heapString = std::to_string(newHeap); - mqtt->pubSub.publish(heapTopic.c_str(), heapString.c_str(), false); + // Use stack-allocated buffers to avoid heap allocations in monitoring code + char heapTopic[128]; + snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); + char heapString[16]; + snprintf(heapString, sizeof(heapString), "%u", newHeap); + mqtt->pubSub.publish(heapTopic, heapString, false); + auto wifiRSSI = WiFi.RSSI(); - std::string wifiTopic = - (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/wifi/") + std::string(mac); - std::string wifiString = std::to_string(wifiRSSI); - mqtt->pubSub.publish(wifiTopic.c_str(), wifiString.c_str(), false); + char wifiTopic[128]; + snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); + char wifiString[16]; + snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI); + mqtt->pubSub.publish(wifiTopic, wifiString, false); } #endif diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 08acc3e55..3eeb17ef0 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1128,6 +1128,15 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g #endif } +TFTDisplay::~TFTDisplay() +{ + // Clean up allocated line pixel buffer to prevent memory leak + if (linePixelBuffer != nullptr) { + free(linePixelBuffer); + linePixelBuffer = nullptr; + } +} + // Write the buffer to the display memory void TFTDisplay::display(bool fromBlank) { diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index 27672ad29..a64922d23 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -20,6 +20,9 @@ class TFTDisplay : public OLEDDisplay */ TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); + // Destructor to clean up allocated memory + ~TFTDisplay(); + // Write the buffer to the display memory virtual void display() override { display(false); }; virtual void display(bool fromBlank); diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp index d3fcbbf9d..e00c1cc6f 100644 --- a/src/input/RotaryEncoderImpl.cpp +++ b/src/input/RotaryEncoderImpl.cpp @@ -70,7 +70,7 @@ int32_t RotaryEncoderImpl::runOnce() this->notifyObservers(&e); } - return 20; + return 10; } #endif \ No newline at end of file diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 794b25aa6..7ceca2195 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -74,10 +74,11 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast if (p->from != 0) { meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from); if (origTx) { - // Either relayer of ACK was also a relayer of the packet, or we were the relayer and the ACK came directly from - // the destination + // Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came directly + // from the destination if (wasRelayer(p->relay_node, p->decoded.request_id, p->to) || - (wasRelayer(ourRelayID, p->decoded.request_id, p->to) && p->hop_start != 0 && p->hop_start == p->hop_limit)) { + (p->hop_start != 0 && p->hop_start == p->hop_limit && + wasSoleRelayer(ourRelayID, p->decoded.request_id, p->to))) { if (origTx->next_hop != p->relay_node) { // Not already set LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply", p->from, p->relay_node); origTx->next_hop = p->relay_node; diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index 3902c1057..735386d79 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -294,7 +294,7 @@ void PacketHistory::insert(const PacketRecord &r) /* Check if a certain node was a relayer of a packet in the history given an ID and sender * @return true if node was indeed a relayer, false if not */ -bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) +bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole) { if (!initOk()) { LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!"); @@ -322,27 +322,42 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const N found->sender, found->id, found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer); #endif - return wasRelayer(relayer, *found); + return wasRelayer(relayer, *found, wasSole); } /* Check if a certain node was a relayer of a packet in the history given iterator * @return true if node was indeed a relayer, false if not */ -bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r) +bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole) { - for (uint8_t i = 0; i < NUM_RELAYERS; i++) { + bool found = false; + bool other_present = false; + + for (uint8_t i = 0; i < NUM_RELAYERS; ++i) { if (r.relayed_by[i] == relayer) { -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? YES", r.sender, r.id, - r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], relayer); -#endif - return true; + found = true; + } else if (r.relayed_by[i] != 0) { + other_present = true; } } + + if (wasSole) { + *wasSole = (found && !other_present); + } + #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], relayer); #endif - return false; + + return found; +} + +// Check if a certain node was the *only* relayer of a packet in the history given an ID and sender +bool PacketHistory::wasSoleRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) +{ + bool wasSole = false; + wasRelayer(relayer, id, sender, &wasSole); + return wasSole; } // Remove a relayer from the list of relayers of a packet in the history given an ID and sender diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index 9f14a4cf0..4b53c8f6a 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -34,8 +34,9 @@ class PacketHistory void insert(const PacketRecord &r); // Insert or replace a packet record in the history /* Check if a certain node was a relayer of a packet in the history given iterator + * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet * @return true if node was indeed a relayer, false if not */ - bool wasRelayer(const uint8_t relayer, const PacketRecord &r); + bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr); PacketHistory(const PacketHistory &); // non construction-copyable PacketHistory &operator=(const PacketHistory &); // non copyable @@ -54,8 +55,12 @@ class PacketHistory bool *weWereNextHop = nullptr); /* Check if a certain node was a relayer of a packet in the history given an ID and sender + * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet * @return true if node was indeed a relayer, false if not */ - bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); + bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr); + + // Check if a certain node was the *only* relayer of a packet in the history given an ID and sender + bool wasSoleRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); // Remove a relayer from the list of relayers of a packet in the history given an ID and sender void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 67d461611..59e55db3f 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -64,7 +64,12 @@ typedef enum _meshtastic_Config_DeviceConfig_Role { in areas not already covered by other routers, or to bridge around problematic terrain, but should not be given priority over other routers in order to avoid unnecessaraily consuming hops. */ - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11 + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11, + /* Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT. + Technical Details: Used for stronger attic/roof nodes to distribute messages more widely + from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes + where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */ + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE = 12 } meshtastic_Config_DeviceConfig_Role; /* Defines the device's behavior for how messages are rebroadcast */ @@ -646,8 +651,8 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_Config_DeviceConfig_Role_MIN meshtastic_Config_DeviceConfig_Role_CLIENT -#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_ROUTER_LATE -#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_ROUTER_LATE+1)) +#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_CLIENT_BASE +#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_CLIENT_BASE+1)) #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN meshtastic_Config_DeviceConfig_RebroadcastMode_ALL #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MAX meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index ce3722aa7..2a4e77870 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -272,6 +272,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_HELTEC_MESH_SOLAR = 108, /* Lilygo T-Echo Lite */ meshtastic_HardwareModel_T_ECHO_LITE = 109, + /* New Heltec LoRA32 with ESP32-S3 CPU */ + meshtastic_HardwareModel_HELTEC_V4 = 110, /* ------------------------------------------------------------------------------------------------------------------------------------------ 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. ------------------------------------------------------------------------------------------------------------------------------------------ */ diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 42ebb8417..74953d8fc 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -342,6 +342,11 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) res->print(value->Stringify().c_str()); delete value; + + // Clean up the fileList to prevent memory leak + for (auto *val : fileList) { + delete val; + } } void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) @@ -610,33 +615,38 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) res->println("
");
     }
 
+    // Helper lambda to create JSON array and clean up memory properly
+    auto createJSONArrayFromLog = [](uint32_t *logArray, int count) -> JSONValue * {
+        JSONArray tempArray;
+        for (int i = 0; i < count; i++) {
+            tempArray.push_back(new JSONValue((int)logArray[i]));
+        }
+        JSONValue *result = new JSONValue(tempArray);
+        // Clean up original array to prevent memory leak
+        for (auto *val : tempArray) {
+            delete val;
+        }
+        return result;
+    };
+
     // data->airtime->tx_log
-    JSONArray txLogValues;
     uint32_t *logArray;
     logArray = airTime->airtimeReport(TX_LOG);
-    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
-        txLogValues.push_back(new JSONValue((int)logArray[i]));
-    }
+    JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
     // data->airtime->rx_log
-    JSONArray rxLogValues;
     logArray = airTime->airtimeReport(RX_LOG);
-    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
-        rxLogValues.push_back(new JSONValue((int)logArray[i]));
-    }
+    JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
     // data->airtime->rx_all_log
-    JSONArray rxAllLogValues;
     logArray = airTime->airtimeReport(RX_ALL_LOG);
-    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
-        rxAllLogValues.push_back(new JSONValue((int)logArray[i]));
-    }
+    JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
     // data->airtime
     JSONObject jsonObjAirtime;
-    jsonObjAirtime["tx_log"] = new JSONValue(txLogValues);
-    jsonObjAirtime["rx_log"] = new JSONValue(rxLogValues);
-    jsonObjAirtime["rx_all_log"] = new JSONValue(rxAllLogValues);
+    jsonObjAirtime["tx_log"] = txLogJsonValue;
+    jsonObjAirtime["rx_log"] = rxLogJsonValue;
+    jsonObjAirtime["rx_all_log"] = rxAllLogJsonValue;
     jsonObjAirtime["channel_utilization"] = new JSONValue(airTime->channelUtilizationPercent());
     jsonObjAirtime["utilization_tx"] = new JSONValue(airTime->utilizationTXPercent());
     jsonObjAirtime["seconds_since_boot"] = new JSONValue(int(airTime->getSecondsSinceBoot()));
@@ -765,6 +775,11 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
     JSONValue *value = new JSONValue(jsonObjOuter);
     res->print(value->Stringify().c_str());
     delete value;
+
+    // Clean up the nodesArray to prevent memory leak
+    for (auto *val : nodesArray) {
+        delete val;
+    }
 }
 
 /*
@@ -955,5 +970,10 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
     JSONValue *value = new JSONValue(jsonObjOuter);
     res->print(value->Stringify().c_str());
     delete value;
+
+    // Clean up the networkObjs to prevent memory leak
+    for (auto *val : networkObjs) {
+        delete val;
+    }
 }
 #endif
\ No newline at end of file
diff --git a/src/sleep.cpp b/src/sleep.cpp
index bff318900..83597e349 100644
--- a/src/sleep.cpp
+++ b/src/sleep.cpp
@@ -431,15 +431,6 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
         gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq
 #endif
 
-#ifdef T_LORA_PAGER
-    LOG_DEBUG("power down XL9555 io");
-    io.digitalWrite(EXPANDS_DRV_EN, LOW);
-    io.digitalWrite(EXPANDS_AMP_EN, LOW);
-    io.digitalWrite(EXPANDS_KB_EN, LOW);
-    io.digitalWrite(EXPANDS_SD_EN, LOW);
-    io.digitalWrite(EXPANDS_GPIO_EN, LOW);
-#endif
-
     auto res = esp_sleep_enable_gpio_wakeup();
     if (res != ESP_OK) {
         LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res);
@@ -480,14 +471,6 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
         gpio_wakeup_disable((gpio_num_t)RF95_IRQ);
     }
 #endif
-#ifdef T_LORA_PAGER
-    LOG_DEBUG("power up XL9555 io");
-    io.digitalWrite(EXPANDS_DRV_EN, HIGH);
-    io.digitalWrite(EXPANDS_AMP_EN, HIGH);
-    io.digitalWrite(EXPANDS_KB_EN, HIGH);
-    io.digitalWrite(EXPANDS_SD_EN, HIGH);
-    io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
-#endif
 
     esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
     notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here
diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini
new file mode 100644
index 000000000..809599212
--- /dev/null
+++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini
@@ -0,0 +1,12 @@
+; 9M2IBR APRS LoRa Tracker: ESP32-WROOM-32 + EBYTE E22-400M30S
+; https://shopee.com.my/product/1095224/21692283917
+[env:9m2ibr_aprs_lora_tracker]
+extends = esp32_base
+board = esp32doit-devkit-v1
+board_level = extra
+build_flags =
+  ${esp32_base.build_flags}
+  -D PRIVATE_HW
+  -D EBYTE_E22
+  -D EBYTE_E22_900M30S ; Assume Tx power curve is identical to 900M30S as there is no documentation
+  -I variants/esp32/diy/9m2ibr_aprs_lora_tracker
diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h
new file mode 100644
index 000000000..037933140
--- /dev/null
+++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h
@@ -0,0 +1,74 @@
+/*
+
+  9M2IBR APRS LoRa Tracker: ESP32-WROOM-32 + EBYTE E22-400M30S
+  https://shopee.com.my/product/1095224/21692283917
+
+  Originally developed for LoRa_APRS_iGate and GPIO is similar to
+  https://github.com/richonguzman/LoRa_APRS_iGate/blob/main/variants/ESP32_DIY_1W_LoRa_Mesh_V1_2/board_pinout.h
+
+*/
+
+// OLED (may be different controllers depending on screen size)
+#define I2C_SDA 21
+#define I2C_SCL 22
+#define HAS_SCREEN 1 // Generates randomized BLE pin
+
+// GNSS: Ai-Thinker GP-02 BDS/GNSS module
+#define GPS_RX_PIN 16
+#define GPS_TX_PIN 17
+
+// Button
+#define BUTTON_PIN 15 // Right side button - if not available, set device.button_gpio to 0 from Meshtastic client
+
+// LEDs
+#define LED_PIN 13 // Tx LED
+#define USER_LED 2 // Rx LED
+
+// Buzzer
+#define PIN_BUZZER 33
+
+// Battery sense
+#define BATTERY_PIN 35
+#define ADC_MULTIPLIER 2.01 // 100k + 100k, and add 1% tolerance
+#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
+#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION
+
+// SPI
+#define LORA_SCK 18
+#define LORA_MISO 19
+#define LORA_MOSI 23
+
+// LoRa
+#define LORA_CS 5
+#define LORA_DIO0 26          // a No connect on the SX1262/SX1268 module
+#define LORA_RESET 27         // RST for SX1276, and for SX1262/SX1268
+#define LORA_DIO1 12          // IRQ for SX1262/SX1268
+#define LORA_DIO2 RADIOLIB_NC // BUSY for SX1262/SX1268
+#define LORA_DIO3             // NC, but used as TCXO supply by E22 module
+#define LORA_RXEN 32          // RF switch RX (and E22 LNA) control by ESP32 GPIO
+#define LORA_TXEN 25          // RF switch TX (and E22 PA) control by ESP32 GPIO
+
+// RX/TX for RFM95/SX127x
+#define RF95_RXEN LORA_RXEN
+#define RF95_TXEN LORA_TXEN
+// #define RF95_TCXO 
+
+// common pinouts for SX126X modules
+#define SX126X_CS 5
+#define SX126X_DIO1 LORA_DIO1
+#define SX126X_BUSY LORA_DIO2
+#define SX126X_RESET LORA_RESET
+#define SX126X_RXEN LORA_RXEN
+#define SX126X_TXEN LORA_TXEN
+
+// Support alternative modules if soldered in place of E22
+#define USE_RF95 // RFM95/SX127x
+#define USE_SX1262
+#define USE_SX1268
+#define USE_LLCC68
+
+// E22 TCXO support
+#ifdef EBYTE_E22
+#define SX126X_DIO3_TCXO_VOLTAGE 1.8
+#define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL
+#endif
diff --git a/variants/esp32/heltec_wireless_bridge/platformio.ini b/variants/esp32/heltec_wireless_bridge/platformio.ini
index 60e686f9e..93c3e3394 100644
--- a/variants/esp32/heltec_wireless_bridge/platformio.ini
+++ b/variants/esp32/heltec_wireless_bridge/platformio.ini
@@ -1,6 +1,7 @@
 [env:heltec-wireless-bridge]
 ;build_type = debug ; to make it possible to step through our jtag debugger 
 extends = esp32_base
+board_level = extra
 board = heltec_wifi_lora_32
 build_flags = 
   ${esp32_base.build_flags}
diff --git a/variants/esp32/trackerd/platformio.ini b/variants/esp32/trackerd/platformio.ini
index 3c2726a3c..00c14fad2 100644
--- a/variants/esp32/trackerd/platformio.ini
+++ b/variants/esp32/trackerd/platformio.ini
@@ -1,5 +1,6 @@
 [env:trackerd]
 extends = esp32_base
+board_level = extra
 board = pico32
 board_build.f_flash = 80000000L
 
diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini
index b16e516a7..312d46259 100644
--- a/variants/esp32s3/tlora-pager/platformio.ini
+++ b/variants/esp32s3/tlora-pager/platformio.ini
@@ -26,7 +26,7 @@ lib_deps = ${esp32s3_base.lib_deps}
   lewisxhe/SensorLib@0.3.1
   https://github.com/pschatzmann/arduino-audio-driver/archive/refs/tags/v0.1.3.zip
   https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip
-  https://github.com/mverch67/RotaryEncoder
+  https://github.com/mverch67/RotaryEncoder/archive/25a59d5745a6645536f921427d80b08e78f886d4.zip
 
 [env:tlora-pager-tft]
 board_level = extra
diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini
index 72ac6320d..5c1047aae 100644
--- a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini
+++ b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini
@@ -1,6 +1,7 @@
 ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921
 [env:gat562_mesh_trial_tracker]
 extends = nrf52840_base
+board_level = extra
 board = gat562_mesh_trial_tracker
 board_check = true
 build_flags = ${nrf52840_base.build_flags}
diff --git a/variants/nrf52840/meshlink/platformio.ini b/variants/nrf52840/meshlink/platformio.ini
index 8216a704a..466362242 100644
--- a/variants/nrf52840/meshlink/platformio.ini
+++ b/variants/nrf52840/meshlink/platformio.ini
@@ -4,6 +4,7 @@
 [env:meshlink]
 extends = nrf52840_base
 board = meshlink
+board_level = extra
 ;board_check = true
 build_flags = ${nrf52840_base.build_flags}
   -I variants/nrf52840/meshlink
diff --git a/variants/nrf52840/meshlink_eink/platformio.ini b/variants/nrf52840/meshlink_eink/platformio.ini
index a48a9e695..af5a0040e 100644
--- a/variants/nrf52840/meshlink_eink/platformio.ini
+++ b/variants/nrf52840/meshlink_eink/platformio.ini
@@ -4,6 +4,7 @@
 [env:meshlink_eink]
 extends = nrf52840_base
 board = meshlink
+board_level = extra
 ;board_check = true
 build_flags = ${nrf52840_base.build_flags}
   -I variants/nrf52840/meshlink_eink
diff --git a/variants/rp2040/ec_catsniffer/platformio.ini b/variants/rp2040/ec_catsniffer/platformio.ini
index acf19d757..b70eff6d7 100644
--- a/variants/rp2040/ec_catsniffer/platformio.ini
+++ b/variants/rp2040/ec_catsniffer/platformio.ini
@@ -1,6 +1,7 @@
 [env:catsniffer]
 extends = rp2040_base
 board = rpipico
+board_level = extra
 upload_protocol = picotool
 build_flags = 
   ${rp2040_base.build_flags}