From d5bb566276606a2f43cdf31e36b002a793d84538 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Mon, 8 Sep 2025 20:53:49 +1000 Subject: [PATCH 1/5] Only log good times. (It's not always a good time then) (#7904) Further to https://github.com/meshtastic/firmware/pull/7897 , there was another log line that was triggering indiscriminantly on GPS_INTERVAL_THRESHOLD . Rather than logging a bad time 4000 times, let's just log one good time when it is set. --- src/gps/GPS.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 9ae7ae97d..88984a890 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1532,10 +1532,9 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s t.tm_year = d.year() - 1900; t.tm_isdst = false; if (t.tm_mon > -1) { - LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min, - t.tm_sec, ti.age()); if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { - LOG_DEBUG("Time set."); + LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, + t.tm_min, t.tm_sec, ti.age()); return true; } else { return false; From 6c697806154258e2ec590e4709b3cba0d5263003 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:52:21 -0500 Subject: [PATCH 2/5] chore(deps): update meshtastic/device-ui digest to 3677476 (#7925) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 16bb0eb96..81f95a7e3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/233d18ef42e9d189f90fdfe621f0cd7edff2d221.zip + https://github.com/meshtastic/device-ui/archive/3677476c8a823ee85056b5fb1d146a3e193f8276.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 803e96800e9d348d7003170c7c65c0ccdae30b33 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 8 Sep 2025 17:21:55 -0500 Subject: [PATCH 3/5] ATAK module should be disabled for non-TAK roles (#7928) --- src/modules/Modules.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index b9b4dd3e5..85d183aef 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -141,7 +141,10 @@ void setupModules() detectionSensorModule = new DetectionSensorModule(); #endif #if !MESHTASTIC_EXCLUDE_ATAK - atakPluginModule = new AtakPluginModule(); + if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TAK, + meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)) { + atakPluginModule = new AtakPluginModule(); + } #endif #if !MESHTASTIC_EXCLUDE_PKI keyVerificationModule = new KeyVerificationModule(); From c8afbe68b535b5f76201b0f78320336682f8edd3 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 9 Sep 2025 06:34:38 -0500 Subject: [PATCH 4/5] Use char buffer for probeResponse (#7870) * Use char buffer for probeResponse * \Update src/gps/GPS.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Revert "\Update src/gps/GPS.cpp" This reverts commit 54d64e19f710c2971347507bff5e506b2209602f. * Remove string --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/gps/GPS.cpp | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 88984a890..d4e9076d9 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1,5 +1,4 @@ #include // Include for strstr -#include #include #include "configuration.h" @@ -1370,34 +1369,42 @@ GnssModel_t GPS::probe(int serialSpeed) GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap) { - String response = ""; + char response[256] = {0}; // Fixed buffer instead of String + uint16_t responseLen = 0; unsigned long start = millis(); while (millis() - start < timeout) { if (_serial_gps->available()) { - response += (char)_serial_gps->read(); + char c = _serial_gps->read(); - if (response.endsWith(",") || response.endsWith("\r\n")) { + // Add char to buffer if there's space + if (responseLen < sizeof(response) - 1) { + response[responseLen++] = c; + response[responseLen] = '\0'; + } + + if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) { #ifdef GPS_DEBUG - LOG_DEBUG(response.c_str()); + LOG_DEBUG(response); #endif // check if we can see our chips for (const auto &chipInfo : responseMap) { - if (strstr(response.c_str(), chipInfo.detectionString.c_str()) != nullptr) { + if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) { LOG_INFO("%s detected", chipInfo.chipName.c_str()); return chipInfo.driver; } } } - if (response.endsWith("\r\n")) { - response.trim(); - response = ""; // Reset the response string for the next potential message + if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') { + // Reset the response buffer for the next potential message + responseLen = 0; + response[0] = '\0'; } } } #ifdef GPS_DEBUG - LOG_DEBUG(response.c_str()); + LOG_DEBUG(response); #endif - return GNSS_MODEL_UNKNOWN; // Return empty string on timeout + return GNSS_MODEL_UNKNOWN; // Return unknown on timeout } GPS *GPS::createGps() From d1d16fc25f0d58027c151733b75772041511b580 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 9 Sep 2025 08:21:46 -0500 Subject: [PATCH 5/5] Make phone queues use a static pointer queue (#7919) * Make phone queues use a static pointer queue * Static init * Compile time constants now * Instead, lets just use the normal pointerqueue for linux native builds and static for IoT platforms * Add missing method * Missing methods * Update variant.h --- src/mesh/MeshService.cpp | 6 +- src/mesh/MeshService.h | 21 +++ src/mesh/StaticPointerQueue.h | 77 +++++++++++ src/mesh/mesh-pb-constants.h | 15 +++ .../ports/test_text_message.cpp | 127 +++++++++++++----- variants/native/portduino-buildroot/variant.h | 2 +- 6 files changed, 213 insertions(+), 35 deletions(-) create mode 100644 src/mesh/StaticPointerQueue.h diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 2cc4197c1..7e35fccd8 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -61,8 +61,10 @@ Allocator &queueStatusPool = staticQueueStatusPool; #include "Router.h" MeshService::MeshService() - : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_TOPHONE), toPhoneMqttProxyQueue(MAX_RX_TOPHONE), - toPhoneClientNotificationQueue(MAX_RX_TOPHONE / 2) +#ifdef ARCH_PORTDUINO + : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_QUEUESTATUS_TOPHONE), + toPhoneMqttProxyQueue(MAX_RX_MQTTPROXY_TOPHONE), toPhoneClientNotificationQueue(MAX_RX_NOTIFICATION_TOPHONE) +#endif { lastQueueStatus = {0, 0, 16, 0}; } diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h index f7d79366e..5d074368f 100644 --- a/src/mesh/MeshService.h +++ b/src/mesh/MeshService.h @@ -9,7 +9,12 @@ #include "MeshRadio.h" #include "MeshTypes.h" #include "Observer.h" +#ifdef ARCH_PORTDUINO #include "PointerQueue.h" +#else +#include "StaticPointerQueue.h" +#endif +#include "mesh-pb-constants.h" #if defined(ARCH_PORTDUINO) #include "../platform/portduino/SimRadio.h" #endif @@ -37,16 +42,32 @@ class MeshService /// FIXME, change to a DropOldestQueue and keep a count of the number of dropped packets to ensure /// we never hang because android hasn't been there in a while /// FIXME - save this to flash on deep sleep +#ifdef ARCH_PORTDUINO PointerQueue toPhoneQueue; +#else + StaticPointerQueue toPhoneQueue; +#endif // keep list of QueueStatus packets to be send to the phone +#ifdef ARCH_PORTDUINO PointerQueue toPhoneQueueStatusQueue; +#else + StaticPointerQueue toPhoneQueueStatusQueue; +#endif // keep list of MqttClientProxyMessages to be send to the client for delivery +#ifdef ARCH_PORTDUINO PointerQueue toPhoneMqttProxyQueue; +#else + StaticPointerQueue toPhoneMqttProxyQueue; +#endif // keep list of ClientNotifications to be send to the client (phone) +#ifdef ARCH_PORTDUINO PointerQueue toPhoneClientNotificationQueue; +#else + StaticPointerQueue toPhoneClientNotificationQueue; +#endif // This holds the last QueueStatus send meshtastic_QueueStatus lastQueueStatus; diff --git a/src/mesh/StaticPointerQueue.h b/src/mesh/StaticPointerQueue.h new file mode 100644 index 000000000..398ee450c --- /dev/null +++ b/src/mesh/StaticPointerQueue.h @@ -0,0 +1,77 @@ +#pragma once + +#include "concurrency/OSThread.h" +#include "freertosinc.h" +#include + +/** + * A static circular buffer queue for pointers. + * This provides the same interface as PointerQueue but uses a statically allocated + * buffer instead of dynamic allocation. + */ +template class StaticPointerQueue +{ + static_assert(MaxElements > 0, "MaxElements must be greater than 0"); + + T *buffer[MaxElements]; + int head = 0; + int tail = 0; + int count = 0; + concurrency::OSThread *reader = nullptr; + + public: + StaticPointerQueue() + { + // Initialize all buffer elements to nullptr to silence warnings and ensure clean state + for (int i = 0; i < MaxElements; i++) { + buffer[i] = nullptr; + } + } + + int numFree() const { return MaxElements - count; } + + bool isEmpty() const { return count == 0; } + + int numUsed() const { return count; } + + bool enqueue(T *x, TickType_t maxWait = portMAX_DELAY) + { + if (count >= MaxElements) { + return false; // Queue is full + } + + if (reader) { + reader->setInterval(0); + concurrency::mainDelay.interrupt(); + } + + buffer[tail] = x; + tail = (tail + 1) % MaxElements; + count++; + return true; + } + + bool dequeue(T **p, TickType_t maxWait = portMAX_DELAY) + { + if (count == 0) { + return false; // Queue is empty + } + + *p = buffer[head]; + head = (head + 1) % MaxElements; + count--; + return true; + } + + // returns a ptr or null if the queue was empty + T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) + { + T *p; + return dequeue(&p, maxWait) ? p : nullptr; + } + + void setReader(concurrency::OSThread *t) { reader = t; } + + // For compatibility with PointerQueue interface + int getMaxLen() const { return MaxElements; } +}; diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index 08c03dc6b..224f45de2 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -18,6 +18,21 @@ #define MAX_RX_TOPHONE 32 #endif +/// max number of QueueStatus packets which can be waiting for delivery to phone +#ifndef MAX_RX_QUEUESTATUS_TOPHONE +#define MAX_RX_QUEUESTATUS_TOPHONE 4 +#endif + +/// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone +#ifndef MAX_RX_MQTTPROXY_TOPHONE +#define MAX_RX_MQTTPROXY_TOPHONE 32 +#endif + +/// max number of ClientNotification packets which can be waiting for delivery to phone +#ifndef MAX_RX_NOTIFICATION_TOPHONE +#define MAX_RX_NOTIFICATION_TOPHONE 4 +#endif + /// Verify baseline assumption of node size. If it increases, we need to reevaluate /// the impact of its memory footprint, notably on MAX_NUM_NODES. static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES."); diff --git a/test/test_meshpacket_serializer/ports/test_text_message.cpp b/test/test_meshpacket_serializer/ports/test_text_message.cpp index de3f34541..0f3b0bc6d 100644 --- a/test/test_meshpacket_serializer/ports/test_text_message.cpp +++ b/test/test_meshpacket_serializer/ports/test_text_message.cpp @@ -1,42 +1,105 @@ #include "../test_helpers.h" +#include + +// Helper function to test common packet fields and structure +void verify_text_message_packet_structure(const std::string &json, const char *expected_text) +{ + TEST_ASSERT_TRUE(json.length() > 0); + + // Use smart pointer for automatic memory management + std::unique_ptr root(JSON::Parse(json.c_str())); + TEST_ASSERT_NOT_NULL(root.get()); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check basic packet fields - use helper function to reduce duplication + auto check_field = [&](const char *field, uint32_t expected_value) { + auto it = jsonObj.find(field); + TEST_ASSERT_TRUE(it != jsonObj.end()); + TEST_ASSERT_EQUAL(expected_value, (uint32_t)it->second->AsNumber()); + }; + + check_field("from", 0x11223344); + check_field("to", 0x55667788); + check_field("id", 0x9999); + + // Check message type + auto type_it = jsonObj.find("type"); + TEST_ASSERT_TRUE(type_it != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("text", type_it->second->AsString().c_str()); + + // Check payload + auto payload_it = jsonObj.find("payload"); + TEST_ASSERT_TRUE(payload_it != jsonObj.end()); + TEST_ASSERT_TRUE(payload_it->second->IsObject()); + + JSONObject payload = payload_it->second->AsObject(); + auto text_it = payload.find("text"); + TEST_ASSERT_TRUE(text_it != payload.end()); + TEST_ASSERT_EQUAL_STRING(expected_text, text_it->second->AsString().c_str()); + + // No need for manual delete with smart pointer +} // Test TEXT_MESSAGE_APP port void test_text_message_serialization() { const char *test_text = "Hello Meshtastic!"; meshtastic_MeshPacket packet = - create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, (const uint8_t *)test_text, strlen(test_text)); + create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(test_text), strlen(test_text)); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); - - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); - - JSONObject jsonObj = root->AsObject(); - - // Check basic packet fields - TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end()); - TEST_ASSERT_EQUAL(0x11223344, (uint32_t)jsonObj["from"]->AsNumber()); - - TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end()); - TEST_ASSERT_EQUAL(0x55667788, (uint32_t)jsonObj["to"]->AsNumber()); - - TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end()); - TEST_ASSERT_EQUAL(0x9999, (uint32_t)jsonObj["id"]->AsNumber()); - - // Check message type - TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); - TEST_ASSERT_EQUAL_STRING("text", jsonObj["type"]->AsString().c_str()); - - // Check payload - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - - JSONObject payload = jsonObj["payload"]->AsObject(); - TEST_ASSERT_TRUE(payload.find("text") != payload.end()); - TEST_ASSERT_EQUAL_STRING("Hello Meshtastic!", payload["text"]->AsString().c_str()); - - delete root; + verify_text_message_packet_structure(json, test_text); } + +// Test with nullptr to check robustness +void test_text_message_serialization_null() +{ + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + verify_text_message_packet_structure(json, ""); +} + +// Test TEXT_MESSAGE_APP port with very long message (boundary testing) +void test_text_message_serialization_long_text() +{ + // Test with actual message size limits + constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit + std::string long_text(MAX_MESSAGE_SIZE, 'A'); + + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, + reinterpret_cast(long_text.c_str()), long_text.length()); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + verify_text_message_packet_structure(json, long_text.c_str()); +} + +// Test with message over size limit (should fail) +void test_text_message_serialization_oversized() +{ + constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit + std::string oversized_text(OVERSIZED_MESSAGE, 'B'); + + meshtastic_MeshPacket packet = create_test_packet( + meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(oversized_text.c_str()), oversized_text.length()); + + // Should fail or return empty/error + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + // Should only verify first 234 characters for oversized messages + std::string expected_text = oversized_text.substr(0, 234); + verify_text_message_packet_structure(json, expected_text.c_str()); +} + +// Add test for malformed UTF-8 sequences +void test_text_message_serialization_invalid_utf8() +{ + const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8 + meshtastic_MeshPacket packet = + create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, invalid_utf8, sizeof(invalid_utf8) - 1); + + // Should not crash, may produce replacement characters + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); +} \ No newline at end of file diff --git a/variants/native/portduino-buildroot/variant.h b/variants/native/portduino-buildroot/variant.h index b7b39d6e8..11a6c0bd3 100644 --- a/variants/native/portduino-buildroot/variant.h +++ b/variants/native/portduino-buildroot/variant.h @@ -2,4 +2,4 @@ #define CANNED_MESSAGE_MODULE_ENABLE 1 #define HAS_GPS 1 #define MAX_RX_TOPHONE settingsMap[maxtophone] -#define MAX_NUM_NODES settingsMap[maxnodes] \ No newline at end of file +#define MAX_NUM_NODES settingsMap[maxnodes]