diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index c62d80888..a98bdc011 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -30,7 +30,16 @@ jobs: strategy: fail-fast: false matrix: - arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32, check] + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + - check runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -56,6 +65,7 @@ jobs: esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }} nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }} rp2040: ${{ steps.jsonStep.outputs.rp2040 }} + rp2350: ${{ steps.jsonStep.outputs.rp2350 }} stm32: ${{ steps.jsonStep.outputs.stm32 }} check: ${{ steps.jsonStep.outputs.check }} @@ -145,7 +155,7 @@ jobs: pio_env: ${{ matrix.board }} platform: nrf52840 - build-rpi2040: + build-rp2040: needs: [setup, version] strategy: fail-fast: false @@ -156,6 +166,17 @@ jobs: pio_env: ${{ matrix.board }} platform: rp2040 + build-rp2350: + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.rp2350) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: rp2350 + build-stm32: needs: [setup, version] strategy: @@ -243,7 +264,15 @@ jobs: strategy: fail-fast: false matrix: - arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32] + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 runs-on: ubuntu-latest needs: [ @@ -253,7 +282,8 @@ jobs: build-esp32c3, build-esp32c6, build-nrf52840, - build-rpi2040, + build-rp2040, + build-rp2350, build-stm32, ] steps: @@ -392,7 +422,15 @@ jobs: strategy: fail-fast: false matrix: - arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32] + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' }} needs: [release-artifacts, version] @@ -449,7 +487,8 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' }} needs: [release-firmware, version] env: - targets: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,stm32 + targets: |- + esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/bin/build-firmware.sh b/bin/build-firmware.sh index c53f1b660..fdd7caa11 100644 --- a/bin/build-firmware.sh +++ b/bin/build-firmware.sh @@ -11,7 +11,7 @@ elif (echo $2 | grep -q "nrf52"); then elif (echo $2 | grep -q "stm32"); then bin/build-stm32.sh $1 elif (echo $2 | grep -q "rpi2040"); then - bin/build-rpi2040.sh $1 + bin/build-rp2xx0.sh $1 else echo "Unknown target $2" exit 1 diff --git a/protobufs b/protobufs index d31cd890d..9bac2886f 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit d31cd890d58ffa7e3524e0685a8617bbd181a1c6 +Subproject commit 9bac2886f9344f25716921467a82e8b0326107cd diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 5420d1b4b..5d9b5a33b 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -412,9 +412,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, float freq = RadioLibInterface::instance->getFreq(); snprintf(freqStr, sizeof(freqStr), "%.3f", freq); if (config.lora.channel_num == 0) { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %smhz", freqStr); + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); } else { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %smhz (%d)", freqStr, config.lora.channel_num); + snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); } size_t len = strlen(frequencyslot); if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 5eaa2c6bf..cf19c4825 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -14,6 +14,7 @@ #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" #include "modules/KeyVerificationModule.h" +#include "modules/TraceRouteModule.h" extern uint16_t TFT_MESH; @@ -153,6 +154,7 @@ void menuHandler::TZPicker() "US/Mountain", "US/Central", "US/Eastern", + "BR/Brasilia", "UTC", "EU/Western", "EU/" @@ -167,7 +169,7 @@ void menuHandler::TZPicker() BannerOverlayOptions bannerOptions; bannerOptions.message = "Pick Timezone"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 17; + bannerOptions.optionsCount = 19; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { menuHandler::menuQueue = menuHandler::clock_menu; @@ -186,25 +188,27 @@ void menuHandler::TZPicker() strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); } else if (selected == 7) { // Eastern strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 8) { // UTC - strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef)); - } else if (selected == 9) { // EU/Western + } else if (selected == 8) { // Brazil + strncpy(config.device.tzdef, "BRT3", sizeof(config.device.tzdef)); + } else if (selected == 9) { // UTC + strncpy(config.device.tzdef, "UTC0", sizeof(config.device.tzdef)); + } else if (selected == 10) { // EU/Western strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef)); - } else if (selected == 10) { // EU/Central + } else if (selected == 11) { // EU/Central strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef)); - } else if (selected == 11) { // EU/Eastern + } else if (selected == 12) { // EU/Eastern strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef)); - } else if (selected == 12) { // Asia/Kolkata + } else if (selected == 13) { // Asia/Kolkata strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef)); - } else if (selected == 13) { // China + } else if (selected == 14) { // China strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef)); - } else if (selected == 14) { // AU/AWST + } else if (selected == 15) { // AU/AWST strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef)); - } else if (selected == 15) { // AU/ACST + } else if (selected == 16) { // AU/ACST strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } else if (selected == 16) { // AU/AEST + } else if (selected == 17) { // AU/AEST strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } else if (selected == 17) { // NZ + } else if (selected == 18) { // NZ strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef)); } if (selected != 0) { @@ -428,7 +432,7 @@ void menuHandler::systemBaseMenu() void menuHandler::favoriteBaseMenu() { - enum optionsNumbers { Back, Preset, Freetext, Remove, enumEnd }; + enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd }; static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"}; static int optionsEnumArray[enumEnd] = {Back, Preset}; int options = 2; @@ -437,6 +441,8 @@ void menuHandler::favoriteBaseMenu() optionsArray[options] = "New Freetext Msg"; optionsEnumArray[options++] = Freetext; } + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; optionsArray[options] = "Remove Favorite"; optionsEnumArray[options++] = Remove; @@ -453,6 +459,10 @@ void menuHandler::favoriteBaseMenu() } else if (selected == Remove) { menuHandler::menuQueue = menuHandler::remove_favorite; screen->runNow(); + } else if (selected == TraceRoute) { + if (traceRouteModule) { + traceRouteModule->launch(graphics::UIRenderer::currentFavoriteNodeNum); + } } }; screen->showOverlayBanner(bannerOptions); @@ -491,12 +501,12 @@ void menuHandler::positionBaseMenu() void menuHandler::nodeListMenu() { - enum optionsNumbers { Back, Favorite, Verify, Reset }; - static const char *optionsArray[] = {"Back", "Add Favorite", "Key Verification", "Reset NodeDB"}; + enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd }; + static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Node Action"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; + bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Favorite) { menuQueue = add_favorite; @@ -507,6 +517,9 @@ void menuHandler::nodeListMenu() } else if (selected == Reset) { menuQueue = reset_node_db_menu; screen->runNow(); + } else if (selected == TraceRoute) { + menuQueue = trace_route_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -859,6 +872,16 @@ void menuHandler::removeFavoriteMenu() screen->showOverlayBanner(bannerOptions); } +void menuHandler::traceRouteMenu() +{ + screen->showNodePicker("Node to Trace", 30000, [](uint32_t nodenum) -> void { + LOG_INFO("Menu: Node picker selected node 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule); + if (traceRouteModule) { + traceRouteModule->startTraceRoute(nodenum); + } + }); +} + void menuHandler::testMenu() { @@ -1131,6 +1154,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case remove_favorite: removeFavoriteMenu(); break; + case trace_route_menu: + traceRouteMenu(); + break; case test_menu: testMenu(); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 1f989be79..2e4923241 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -36,7 +36,8 @@ class menuHandler system_base_menu, key_verification_init, key_verification_final_prompt, - throttle_message + trace_route_menu, + throttle_message, }; static screenMenus menuQueue; @@ -64,6 +65,7 @@ class menuHandler static void shutdownMenu(); static void addFavoriteMenu(); static void removeFavoriteMenu(); + static void traceRouteMenu(); static void testMenu(); static void numberTest(); static void wifiBaseMenu(); diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h index 89d3b15d0..f7d79366e 100644 --- a/src/mesh/MeshService.h +++ b/src/mesh/MeshService.h @@ -146,7 +146,7 @@ class MeshService virtual void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m); /// Send a ClientNotification to the phone - void sendClientNotification(meshtastic_ClientNotification *cn); + virtual void sendClientNotification(meshtastic_ClientNotification *cn); /// Send an error response to the phone void sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp); diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index b7120a064..881dc6ab7 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -406,6 +406,9 @@ NodeDB::NodeDB() config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; config.position.gps_enabled = 0; } +#ifdef USERPREFS_FIRMWARE_EDITION + myNodeInfo.firmware_edition = USERPREFS_FIRMWARE_EDITION; +#endif #ifdef USERPREFS_FIXED_GPS if (myNodeInfo.reboot_count == 1) { // Check if First boot ever or after Factory Reset. meshtastic_Position fixedGPS = meshtastic_Position_init_default; diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index e709db6c4..ba1a52b27 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -362,7 +362,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size #define meshtastic_BackupPreferences_size 2271 #define meshtastic_ChannelFile_size 718 -#define meshtastic_DeviceState_size 1724 +#define meshtastic_DeviceState_size 1728 #define meshtastic_NodeInfoLite_size 196 #define meshtastic_PositionLite_size 28 #define meshtastic_UserLite_size 98 diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index abc06e635..8e6524042 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -935,6 +935,9 @@ typedef struct _meshtastic_MyNodeInfo { char pio_env[40]; /* The indicator for whether this device is running event firmware and which */ meshtastic_FirmwareEdition firmware_edition; + /* The number of nodes in the nodedb. + This is used by the phone to know how many NodeInfo packets to expect on want_config */ + uint16_t nodedb_count; } meshtastic_MyNodeInfo; /* Debug output from the device. @@ -1322,7 +1325,7 @@ extern "C" { #define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0} #define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0} -#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN} +#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0} #define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_default {0, 0, 0, 0} #define meshtastic_FromRadio_init_default {0, 0, {meshtastic_MeshPacket_init_default}} @@ -1353,7 +1356,7 @@ extern "C" { #define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0} #define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0} -#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN} +#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0} #define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_zero {0, 0, 0, 0} #define meshtastic_FromRadio_init_zero {0, 0, {meshtastic_MeshPacket_init_zero}} @@ -1477,6 +1480,7 @@ extern "C" { #define meshtastic_MyNodeInfo_device_id_tag 12 #define meshtastic_MyNodeInfo_pio_env_tag 13 #define meshtastic_MyNodeInfo_firmware_edition_tag 14 +#define meshtastic_MyNodeInfo_nodedb_count_tag 15 #define meshtastic_LogRecord_message_tag 1 #define meshtastic_LogRecord_time_tag 2 #define meshtastic_LogRecord_source_tag 3 @@ -1710,7 +1714,8 @@ X(a, STATIC, SINGULAR, UINT32, reboot_count, 8) \ X(a, STATIC, SINGULAR, UINT32, min_app_version, 11) \ X(a, STATIC, SINGULAR, BYTES, device_id, 12) \ X(a, STATIC, SINGULAR, STRING, pio_env, 13) \ -X(a, STATIC, SINGULAR, UENUM, firmware_edition, 14) +X(a, STATIC, SINGULAR, UENUM, firmware_edition, 14) \ +X(a, STATIC, SINGULAR, UINT32, nodedb_count, 15) #define meshtastic_MyNodeInfo_CALLBACK NULL #define meshtastic_MyNodeInfo_DEFAULT NULL @@ -1993,7 +1998,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_LowEntropyKey_size 0 #define meshtastic_MeshPacket_size 378 #define meshtastic_MqttClientProxyMessage_size 501 -#define meshtastic_MyNodeInfo_size 79 +#define meshtastic_MyNodeInfo_size 83 #define meshtastic_NeighborInfo_size 258 #define meshtastic_Neighbor_size 22 #define meshtastic_NodeInfo_size 323 diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 33d5e1016..1a9c3a7a7 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -43,6 +43,10 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C #include "motion/AccelerometerThread.h" #endif +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) +#include "SerialModule.h" +#endif AdminModule *adminModule; bool hasOpenEditTransaction; @@ -638,7 +642,16 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) case meshtastic_Config_position_tag: LOG_INFO("Set config: Position"); config.has_position = true; + // If we have turned off the GPS (disabled or not present) and we're not using fixed position, + // clear the stored position since it may not get updated + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && + c.payload_variant.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED && + config.position.fixed_position == false && c.payload_variant.position.fixed_position == false) { + nodeDB->clearLocalPosition(); + saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); + } config.position = c.payload_variant.position; + // Save nodedb as well in case we got a fixed position packet break; case meshtastic_Config_power_tag: @@ -798,8 +811,13 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { - if (!hasOpenEditTransaction) + // If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth + // Otherwise, disable Bluetooth to prevent the phone from interfering with the config + if (!hasOpenEditTransaction && + !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) { disableBluetooth(); + } + switch (c.which_payload_variant) { case meshtastic_ModuleConfig_mqtt_tag: #if MESHTASTIC_EXCLUDE_MQTT @@ -810,12 +828,22 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) if (!MQTT::isValidConfig(c.payload_variant.mqtt)) { return false; } + // Disable Bluetooth to prevent interference during MQTT configuration + disableBluetooth(); moduleConfig.has_mqtt = true; moduleConfig.mqtt = c.payload_variant.mqtt; #endif break; case meshtastic_ModuleConfig_serial_tag: LOG_INFO("Set module config: Serial"); +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) + if (!SerialModule::isValidConfig(c.payload_variant.serial)) { + LOG_ERROR("Invalid serial config"); + return false; + } + disableBluetooth(); // Disable Bluetooth to prevent interference during Serial configuration +#endif moduleConfig.has_serial = true; moduleConfig.serial = c.payload_variant.serial; break; @@ -971,9 +999,10 @@ void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32 // So even if we internally use 0 to represent 'use default' we still need to send the value we are // using to the app (so that even old phone apps work with new device loads). // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); - // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally private - // and useful for users to know current provisioning) hideSecret(r.get_radio_response.preferences.wifi_password); - // r.get_config_response.which_payloadVariant = Config_ModuleConfig_telemetry_tag; + // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally + // private and useful for users to know current provisioning) + // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = + // Config_ModuleConfig_telemetry_tag; res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); @@ -1057,9 +1086,10 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const // So even if we internally use 0 to represent 'use default' we still need to send the value we are // using to the app (so that even old phone apps work with new device loads). // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); - // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally private - // and useful for users to know current provisioning) hideSecret(r.get_radio_response.preferences.wifi_password); - // r.get_config_response.which_payloadVariant = Config_ModuleConfig_telemetry_tag; + // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally + // private and useful for users to know current provisioning) + // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = + // Config_ModuleConfig_telemetry_tag; res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index f3921ef19..f3091e5bf 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -74,6 +74,26 @@ static Print *serialPrint = &Serial2; char serialBytes[512]; size_t serialPayloadSize; +bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config) +{ + if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA, + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO)) { + const char *warning = + "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes."; + LOG_ERROR(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + snprintf(cn->message, sizeof(cn->message), "%s", warning); + service->sendClientNotification(cn); +#endif + return false; + } + + return true; +} + SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio") { switch (moduleConfig.serial.mode) { diff --git a/src/modules/SerialModule.h b/src/modules/SerialModule.h index fa86db28f..1c74c927c 100644 --- a/src/modules/SerialModule.h +++ b/src/modules/SerialModule.h @@ -20,6 +20,8 @@ class SerialModule : public StreamAPI, private concurrency::OSThread public: SerialModule(); + static bool isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config); + protected: virtual int32_t runOnce() override; diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp index 0a6e1b4c4..72ac99118 100644 --- a/src/modules/StoreForwardModule.cpp +++ b/src/modules/StoreForwardModule.cpp @@ -202,6 +202,8 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp) this->packetHistory[this->packetHistoryTotalCount].reply_id = p.reply_id; this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji; this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size; + this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi; + this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr; memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN); this->packetHistoryTotalCount++; @@ -252,6 +254,8 @@ meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t p->decoded.reply_id = this->packetHistory[i].reply_id; p->rx_time = this->packetHistory[i].time; p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji; + p->rx_rssi = this->packetHistory[i].rx_rssi; + p->rx_snr = this->packetHistory[i].rx_snr; // Let's assume that if the server received the S&F request that the client is in range. // TODO: Make this configurable. @@ -623,4 +627,4 @@ StoreForwardModule::StoreForwardModule() disable(); } #endif -} \ No newline at end of file +} diff --git a/src/modules/StoreForwardModule.h b/src/modules/StoreForwardModule.h index 30db1625c..25836eded 100644 --- a/src/modules/StoreForwardModule.h +++ b/src/modules/StoreForwardModule.h @@ -19,6 +19,8 @@ struct PacketHistoryStruct { bool emoji; uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; pb_size_t payload_size; + int32_t rx_rssi; + float rx_snr; }; class StoreForwardModule : private concurrency::OSThread, public ProtobufModule @@ -108,4 +110,4 @@ class StoreForwardModule : private concurrency::OSThread, public ProtobufModule< virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p); }; -extern StoreForwardModule *storeForwardModule; \ No newline at end of file +extern StoreForwardModule *storeForwardModule; diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp index 2d534bd67..74b9678f4 100644 --- a/src/modules/SystemCommandsModule.cpp +++ b/src/modules/SystemCommandsModule.cpp @@ -89,6 +89,11 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) #if !MESHTASTIC_EXCLUDE_GPS if (gps) { LOG_WARN("GPS Toggle2"); + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && + config.position.fixed_position == false) { + nodeDB->clearLocalPosition(); + nodeDB->saveToDisk(); + } gps->toggleGpsMode(); const char *msg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled"; diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp index 41cb35649..bd75c6983 100644 --- a/src/modules/TraceRouteModule.cpp +++ b/src/modules/TraceRouteModule.cpp @@ -1,6 +1,13 @@ #include "TraceRouteModule.h" #include "MeshService.h" +#include "graphics/Screen.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "mesh/Router.h" #include "meshUtils.h" +#include + +extern graphics::Screen *screen; TraceRouteModule *traceRouteModule; @@ -27,6 +34,123 @@ void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtasti // Set updated route to the payload of the to be flooded packet p.decoded.payload.size = pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, r); + + if (tracingNode != 0) { + // check isResponseFromTarget + bool isResponseFromTarget = (incoming.request_id != 0 && p.from == tracingNode); + bool isRequestToUs = (incoming.request_id == 0 && p.to == nodeDB->getNodeNum() && tracingNode != 0); + + // Check if this is a trace route response containing our target node + bool containsTargetNode = false; + for (uint8_t i = 0; i < r->route_count; i++) { + if (r->route[i] == tracingNode) { + containsTargetNode = true; + break; + } + } + for (uint8_t i = 0; i < r->route_back_count; i++) { + if (r->route_back[i] == tracingNode) { + containsTargetNode = true; + break; + } + } + + // Check if this response contains a complete route to our target + bool hasCompleteRoute = (r->route_count > 0 && r->route_back_count > 0) || + (containsTargetNode && (r->route_count > 0 || r->route_back_count > 0)); + + LOG_INFO("TracRoute packet analysis: tracingNode=0x%08x, p.from=0x%08x, p.to=0x%08x, request_id=0x%08x", tracingNode, + p.from, p.to, incoming.request_id); + LOG_INFO("TracRoute conditions: isResponseFromTarget=%d, isRequestToUs=%d, containsTargetNode=%d, hasCompleteRoute=%d", + isResponseFromTarget, isRequestToUs, containsTargetNode, hasCompleteRoute); + + if (isResponseFromTarget || isRequestToUs || (containsTargetNode && hasCompleteRoute)) { + LOG_INFO("TracRoute result detected: isResponseFromTarget=%d, isRequestToUs=%d", isResponseFromTarget, isRequestToUs); + + LOG_INFO("SNR arrays - towards_count=%d, back_count=%d", r->snr_towards_count, r->snr_back_count); + for (int i = 0; i < r->snr_towards_count; i++) { + LOG_INFO("SNR towards[%d] = %d (%.1fdB)", i, r->snr_towards[i], (float)r->snr_towards[i] / 4.0f); + } + for (int i = 0; i < r->snr_back_count; i++) { + LOG_INFO("SNR back[%d] = %d (%.1fdB)", i, r->snr_back[i], (float)r->snr_back[i] / 4.0f); + } + + String result = ""; + + // Show request path (from initiator to target) + if (r->route_count > 0) { + result += getNodeName(nodeDB->getNodeNum()); + for (uint8_t i = 0; i < r->route_count; i++) { + result += " > "; + const char *name = getNodeName(r->route[i]); + float snr = + (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) ? ((float)r->snr_towards[i] / 4.0f) : 0.0f; + result += name; + if (snr != 0.0f) { + result += "("; + result += String(snr, 1); + result += "dB)"; + } + } + result += " > "; + result += getNodeName(tracingNode); + if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) { + result += "("; + result += String((float)r->snr_towards[r->snr_towards_count - 1] / 4.0f, 1); + result += "dB)"; + } + result += "\n"; + } else { + // Direct connection (no intermediate hops) + result += getNodeName(nodeDB->getNodeNum()); + result += " > "; + result += getNodeName(tracingNode); + if (r->snr_towards_count > 0 && r->snr_towards[0] != INT8_MIN) { + result += "("; + result += String((float)r->snr_towards[0] / 4.0f, 1); + result += "dB)"; + } + result += "\n"; + } + + // Show response path (from target back to initiator) + if (r->route_back_count > 0) { + result += getNodeName(tracingNode); + for (int8_t i = r->route_back_count - 1; i >= 0; i--) { + result += " > "; + const char *name = getNodeName(r->route_back[i]); + float snr = (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) ? ((float)r->snr_back[i] / 4.0f) : 0.0f; + result += name; + if (snr != 0.0f) { + result += "("; + result += String(snr, 1); + result += "dB)"; + } + } + // add initiator node + result += " > "; + result += getNodeName(nodeDB->getNodeNum()); + if (r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN) { + result += "("; + result += String((float)r->snr_back[r->snr_back_count - 1] / 4.0f, 1); + result += "dB)"; + } + } else { + // Direct return path (no intermediate hops) + result += getNodeName(tracingNode); + result += " > "; + result += getNodeName(nodeDB->getNodeNum()); + if (r->snr_back_count > 0 && r->snr_back[0] != INT8_MIN) { + result += "("; + result += String((float)r->snr_back[0] / 4.0f, 1); + result += "dB)"; + } + } + + LOG_INFO("Trace route result: %s", result.c_str()); + handleTraceRouteResult(result); + } + } } void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination) @@ -173,8 +297,467 @@ meshtastic_MeshPacket *TraceRouteModule::allocReply() } TraceRouteModule::TraceRouteModule() - : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg) + : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg), OSThread("TraceRoute") { ourPortNum = meshtastic_PortNum_TRACEROUTE_APP; isPromiscuous = true; // We need to update the route even if it is not destined to us +} + +const char *TraceRouteModule::getNodeName(NodeNum node) +{ + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); + if (info && info->has_user) { + if (strlen(info->user.short_name) > 0) { + return info->user.short_name; + } + if (strlen(info->user.long_name) > 0) { + return info->user.long_name; + } + } + + static char fallback[12]; + snprintf(fallback, sizeof(fallback), "0x%08x", node); + return fallback; +} + +bool TraceRouteModule::startTraceRoute(NodeNum node) +{ + LOG_INFO("=== TraceRoute startTraceRoute CALLED: node=0x%08x ===", node); + unsigned long now = millis(); + + if (node == 0 || node == NODENUM_BROADCAST) { + LOG_ERROR("Invalid node number for trace route: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Invalid node"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return false; + } + + if (node == nodeDB->getNodeNum()) { + LOG_ERROR("Cannot trace route to self: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Cannot trace self"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return false; + } + + if (!initialized) { + lastTraceRouteTime = 0; + initialized = true; + LOG_INFO("TraceRoute initialized for first time"); + } + + if (runState == TRACEROUTE_STATE_TRACKING) { + LOG_INFO("TraceRoute already in progress"); + return false; + } + + if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { + // Cooldown + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + bannerText = String("Wait for ") + String(wait) + String("s"); + runState = TRACEROUTE_STATE_COOLDOWN; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); + return false; + } + + tracingNode = node; + lastTraceRouteTime = now; + runState = TRACEROUTE_STATE_TRACKING; + bannerText = String("Tracing ") + getNodeName(node); + + LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node); + + // 请求焦点,然后触发UI更新事件 + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + // 设置定时器来处理超时检查 + setIntervalFromNow(1000); // 每秒检查一次状态 + + meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; + LOG_INFO("Creating RouteDiscovery protobuf..."); + + // Allocate a packet directly from router like the reference code + meshtastic_MeshPacket *p = router->allocForSending(); + if (p) { + // Set destination and port + p->to = node; + p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; + p->decoded.want_response = true; + + // Manually encode the RouteDiscovery payload + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); + + LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, + p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size); + LOG_INFO("About to call service->sendToMesh..."); + + if (service) { + LOG_INFO("MeshService is available, sending packet..."); + service->sendToMesh(p, RX_SRC_USER); + LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); + } else { + LOG_ERROR("MeshService is NULL!"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Service unavailable"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e2; + e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e2); + return false; + } + } else { + LOG_ERROR("Failed to allocate TraceRoute packet from router"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Failed to send"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e2; + e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e2); + return false; + } + return true; +} + +void TraceRouteModule::launch(NodeNum node) +{ + if (node == 0 || node == NODENUM_BROADCAST) { + LOG_ERROR("Invalid node number for trace route: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Invalid node"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return; + } + + if (node == nodeDB->getNodeNum()) { + LOG_ERROR("Cannot trace route to self: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Cannot trace self"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return; + } + + if (!initialized) { + lastTraceRouteTime = 0; + initialized = true; + LOG_INFO("TraceRoute initialized for first time"); + } + + unsigned long now = millis(); + if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + bannerText = String("Wait for ") + String(wait) + String("s"); + runState = TRACEROUTE_STATE_COOLDOWN; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); + return; + } + + runState = TRACEROUTE_STATE_TRACKING; + tracingNode = node; + lastTraceRouteTime = now; + bannerText = String("Tracing ") + getNodeName(node); + + requestFocus(); + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + setIntervalFromNow(1000); + + meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; + LOG_INFO("Creating RouteDiscovery protobuf..."); + + meshtastic_MeshPacket *p = router->allocForSending(); + if (p) { + p->to = node; + p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; + p->decoded.want_response = true; + + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); + + LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, + p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size); + + if (service) { + service->sendToMesh(p, RX_SRC_USER); + LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); + } else { + LOG_ERROR("MeshService is NULL!"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Service unavailable"; + resultShowTime = millis(); + tracingNode = 0; + } + } else { + LOG_ERROR("Failed to allocate TraceRoute packet from router"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Failed to send"; + resultShowTime = millis(); + tracingNode = 0; + } +} + +void TraceRouteModule::handleTraceRouteResult(const String &result) +{ + resultText = result; + runState = TRACEROUTE_STATE_RESULT; + resultShowTime = millis(); + tracingNode = 0; + + LOG_INFO("TraceRoute result ready, requesting focus. Result: %s", result.c_str()); + + setIntervalFromNow(1000); + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + LOG_INFO("=== TraceRoute handleTraceRouteResult END ==="); +} + +bool TraceRouteModule::shouldDraw() +{ + bool draw = (runState != TRACEROUTE_STATE_IDLE); + static TraceRouteRunState lastLoggedState = TRACEROUTE_STATE_IDLE; + if (runState != lastLoggedState) { + LOG_INFO("TraceRoute shouldDraw: runState=%d, draw=%d", runState, draw); + lastLoggedState = runState; + } + return draw; +} +#if HAS_SCREEN +void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + LOG_DEBUG("TraceRoute drawFrame called: runState=%d", runState); + + display->setTextAlignment(TEXT_ALIGN_CENTER); + + if (runState == TRACEROUTE_STATE_TRACKING) { + display->setFont(FONT_MEDIUM); + int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); + display->drawString(display->getWidth() / 2 + x, centerY, bannerText); + + } else if (runState == TRACEROUTE_STATE_RESULT) { + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + display->drawString(x, y, "Route Result"); + + int contentStartY = y + FONT_HEIGHT_MEDIUM + 2; // Add more spacing after title + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + if (resultText.length() > 0) { + std::vector lines; + String currentLine = ""; + int maxWidth = display->getWidth() - 4; + + int start = 0; + int newlinePos = resultText.indexOf('\n', start); + + while (newlinePos != -1 || start < resultText.length()) { + String segment; + if (newlinePos != -1) { + segment = resultText.substring(start, newlinePos); + start = newlinePos + 1; + newlinePos = resultText.indexOf('\n', start); + } else { + segment = resultText.substring(start); + start = resultText.length(); + } + + if (display->getStringWidth(segment) <= maxWidth) { + lines.push_back(segment); + } else { + // Try to break at better positions (space, >, <, -) + String remaining = segment; + + while (remaining.length() > 0) { + String tempLine = ""; + int lastGoodBreak = -1; + bool lineComplete = false; + + for (int i = 0; i < remaining.length(); i++) { + char ch = remaining.charAt(i); + String testLine = tempLine + ch; + + if (display->getStringWidth(testLine) > maxWidth) { + if (lastGoodBreak >= 0) { + // Break at the last good position + lines.push_back(remaining.substring(0, lastGoodBreak + 1)); + remaining = remaining.substring(lastGoodBreak + 1); + lineComplete = true; + break; + } else if (tempLine.length() > 0) { + lines.push_back(tempLine); + remaining = remaining.substring(i); + lineComplete = true; + break; + } else { + // Single character exceeds width + lines.push_back(String(ch)); + remaining = remaining.substring(i + 1); + lineComplete = true; + break; + } + } else { + tempLine = testLine; + // Mark good break positions + if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')') { + lastGoodBreak = i; + } + } + } + + if (!lineComplete) { + // Reached end of remaining text + if (tempLine.length() > 0) { + lines.push_back(tempLine); + } + break; + } + } + } + } + + int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing + for (size_t i = 0; i < lines.size(); i++) { + int lineY = contentStartY + (i * lineHeight); + if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) { + display->drawString(x + 2, lineY, lines[i]); + } + } + } + + } else if (runState == TRACEROUTE_STATE_COOLDOWN) { + display->setFont(FONT_MEDIUM); + int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); + display->drawString(display->getWidth() / 2 + x, centerY, bannerText); + } +} +#endif // HAS_SCREEN +int32_t TraceRouteModule::runOnce() +{ + unsigned long now = millis(); + + if (runState == TRACEROUTE_STATE_IDLE) { + return INT32_MAX; + } + + // Check for tracking timeout + if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) { + LOG_INFO("TraceRoute timeout, no response received"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "No response received"; + resultShowTime = now; + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + setIntervalFromNow(resultDisplayMs); + return resultDisplayMs; + } + + // Update cooldown display every second + if (runState == TRACEROUTE_STATE_COOLDOWN) { + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + if (wait > 0) { + String newBannerText = String("Wait for ") + String(wait) + String("s"); + bannerText = newBannerText; + LOG_INFO("TraceRoute cooldown: updating banner to %s", bannerText.c_str()); + + // Force flash UI + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + if (screen) { + screen->forceDisplay(); + } + + return 1000; + } else { + // Cooldown finished + LOG_INFO("TraceRoute cooldown finished, returning to IDLE"); + runState = TRACEROUTE_STATE_IDLE; + bannerText = ""; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return INT32_MAX; + } + } + + if (runState == TRACEROUTE_STATE_RESULT) { + if (now - resultShowTime >= resultDisplayMs) { + LOG_INFO("TraceRoute result display timeout, returning to IDLE"); + runState = TRACEROUTE_STATE_IDLE; + resultText = ""; + bannerText = ""; + tracingNode = 0; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return INT32_MAX; + } else { + return 1000; + } + } + + if (runState == TRACEROUTE_STATE_TRACKING) { + return 1000; + } + + return INT32_MAX; } \ No newline at end of file diff --git a/src/modules/TraceRouteModule.h b/src/modules/TraceRouteModule.h index afe2b3871..51d98826e 100644 --- a/src/modules/TraceRouteModule.h +++ b/src/modules/TraceRouteModule.h @@ -1,16 +1,40 @@ #pragma once #include "ProtobufModule.h" +#include "concurrency/OSThread.h" +#include "graphics/Screen.h" +#include "graphics/SharedUIDisplay.h" +#include "input/InputBroker.h" +#if HAS_SCREEN +#include "OLEDDisplayUi.h" +#endif #define ROUTE_SIZE sizeof(((meshtastic_RouteDiscovery *)0)->route) / sizeof(((meshtastic_RouteDiscovery *)0)->route[0]) /** * A module that traces the route to a certain destination node */ -class TraceRouteModule : public ProtobufModule +enum TraceRouteRunState { TRACEROUTE_STATE_IDLE, TRACEROUTE_STATE_TRACKING, TRACEROUTE_STATE_RESULT, TRACEROUTE_STATE_COOLDOWN }; + +class TraceRouteModule : public ProtobufModule, + public Observable, + private concurrency::OSThread { public: TraceRouteModule(); + bool startTraceRoute(NodeNum node); + void launch(NodeNum node); + void handleTraceRouteResult(const String &result); + bool shouldDraw(); +#if HAS_SCREEN + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; +#endif + + const char *getNodeName(NodeNum node); + + virtual bool wantUIFrame() override { return shouldDraw(); } + virtual Observable *getUIFrameObservable() override { return this; } + protected: bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) override; @@ -20,6 +44,8 @@ class TraceRouteModule : public ProtobufModule the route array containing the IDs of nodes this packet went through */ void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) override; + virtual int32_t runOnce() override; + private: // Call to add unknown hops (e.g. when a node couldn't decrypt it) to the route based on hopStart and current hopLimit void insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination); @@ -31,6 +57,17 @@ class TraceRouteModule : public ProtobufModule Set origin to where the request came from. Set dest to the ID of its destination, or NODENUM_BROADCAST if it has not yet arrived there. */ void printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination); + + TraceRouteRunState runState = TRACEROUTE_STATE_IDLE; + unsigned long lastTraceRouteTime = 0; + unsigned long resultShowTime = 0; + unsigned long cooldownMs = 30000; + unsigned long resultDisplayMs = 10000; + unsigned long trackingTimeoutMs = 10000; + String bannerText; + String resultText; + NodeNum tracingNode = 0; + bool initialized = false; }; extern TraceRouteModule *traceRouteModule; \ No newline at end of file diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 091612827..21d4a8fa0 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -39,6 +39,7 @@ #include #define ntohl __ntohl #endif +#include MQTT *mqtt; @@ -624,18 +625,32 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC 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"); + const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"; + LOG_ERROR(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination + service->sendClientNotification(cn); +#endif 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 && parsed.serverPort != PubSubConfig::defaultPort) { - LOG_ERROR("Invalid MQTT config: Unsupported port '%d' for the default MQTT server", parsed.serverPort); + const char *warning = "Invalid MQTT config: default server address must not have a port specified"; + LOG_ERROR(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination + service->sendClientNotification(cn); +#endif return false; } return true; diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index 8047079ba..32d81f6b4 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -27,6 +27,12 @@ #include #include +#if defined(UNIT_TEST) +#define IS_RUNNING_TESTS 1 +#else +#define IS_RUNNING_TESTS 0 +#endif + namespace { // Minimal router needed to receive messages from MQTT. @@ -56,7 +62,13 @@ class MockMeshService : public MeshService messages_.emplace_back(*m); releaseMqttClientProxyMessageToPool(m); } - std::list messages_; // Messages received from the MeshService. + void sendClientNotification(meshtastic_ClientNotification *n) override + { + notifications_.emplace_back(*n); + releaseClientNotificationToPool(n); + } + std::list messages_; // Messages received from the MeshService. + std::list notifications_; // Notifications received from the MeshService. }; // Minimal NodeDB needed to return values from getMeshNode. @@ -823,14 +835,6 @@ void test_configWithDefaultServerAndInvalidPort(void) 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)); -} - // isValidConfig connects to a custom host and port. void test_configCustomHostAndPort(void) { @@ -911,7 +915,6 @@ void setup() 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); diff --git a/test/test_serial/SerialModule.cpp b/test/test_serial/SerialModule.cpp new file mode 100644 index 000000000..1bccf04a7 --- /dev/null +++ b/test/test_serial/SerialModule.cpp @@ -0,0 +1,156 @@ +#include "DebugConfiguration.h" +#include "TestUtil.h" +#include + +#ifdef ARCH_PORTDUINO +#include "configuration.h" + +#if defined(UNIT_TEST) +#define IS_RUNNING_TESTS 1 +#else +#define IS_RUNNING_TESTS 0 +#endif + +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) +#include "modules/SerialModule.h" +#endif + +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) + +// Test that empty configuration is valid. +void test_serialConfigEmptyIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = {}; + + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +// Test that basic enabled configuration is valid. +void test_serialConfigEnabledIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = {.enabled = true}; + + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and NMEA mode is valid. +void test_serialConfigWithOverrideConsoleNmeaModeIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA}; + + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and CalTopo mode is valid. +void test_serialConfigWithOverrideConsoleCalTopoModeIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO}; + + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and DEFAULT mode is invalid. +void test_serialConfigWithOverrideConsoleDefaultModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT}; + + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and SIMPLE mode is invalid. +void test_serialConfigWithOverrideConsoleSimpleModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE}; + + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and TEXTMSG mode is invalid. +void test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG}; + + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and PROTO mode is invalid. +void test_serialConfigWithOverrideConsoleProtoModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO}; + + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); +} + +// Test that various modes work without override_console_serial_port. +void test_serialConfigVariousModesWithoutOverrideAreValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = {.enabled = true, .override_console_serial_port = false}; + + // Test DEFAULT mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test SIMPLE mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test TEXTMSG mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test PROTO mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test NMEA mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test CALTOPO mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +#endif // Architecture check + +void setup() +{ + initializeTestEnvironment(); + +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) + UNITY_BEGIN(); + RUN_TEST(test_serialConfigEmptyIsValid); + RUN_TEST(test_serialConfigEnabledIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleNmeaModeIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleCalTopoModeIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleDefaultModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleSimpleModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleProtoModeIsInvalid); + RUN_TEST(test_serialConfigVariousModesWithoutOverrideAreValid); + exit(UNITY_END()); +#else + LOG_WARN("This test requires ESP32, NRF52, or RP2040 architecture"); + UNITY_BEGIN(); + UNITY_END(); +#endif +} +#else +void setup() +{ + initializeTestEnvironment(); + LOG_WARN("This test requires the ARCH_PORTDUINO variant"); + UNITY_BEGIN(); + UNITY_END(); +} +#endif +void loop() {} diff --git a/userPrefs.jsonc b/userPrefs.jsonc index 3da8e7ba6..f6f3ef995 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -23,6 +23,7 @@ // "USERPREFS_CONFIG_OWNER_SHORT_NAME": "MLN", // "USERPREFS_CONFIG_DEVICE_ROLE": "meshtastic_Config_DeviceConfig_Role_CLIENT", // Defaults to CLIENT. ROUTER*, LOST AND FOUND, and REPEATER roles are restricted. // "USERPREFS_EVENT_MODE": "1", + // "USERPREFS_FIRMWARE_EDITION": "meshtastic_FirmwareEdition_BURNING_MAN", // "USERPREFS_FIXED_BLUETOOTH": "121212", // "USERPREFS_FIXED_GPS": "", // "USERPREFS_FIXED_GPS_ALT": "0", diff --git a/variants/nrf52840/diy/WashTastic/platformio.ini b/variants/nrf52840/diy/WashTastic/platformio.ini new file mode 100644 index 000000000..881b961e1 --- /dev/null +++ b/variants/nrf52840/diy/WashTastic/platformio.ini @@ -0,0 +1,13 @@ +; Promicro + E22900M30S +[env:WashTastic] +extends = nrf52840_base +board = promicro-nrf52840 +board_level = extra +build_flags = ${nrf52840_base.build_flags} + -I variants/nrf52840/diy/nrf52_promicro_diy_tcxo + -D PRIVATE_HW + -D EBYTE_E22_900M30S +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> +lib_deps = + ${nrf52840_base.lib_deps} +debug_tool = jlink diff --git a/variants/rp2040/rpipicow/platformio.ini b/variants/rp2040/rpipicow/platformio.ini index 658d113d7..60845ba39 100644 --- a/variants/rp2040/rpipicow/platformio.ini +++ b/variants/rp2040/rpipicow/platformio.ini @@ -2,6 +2,7 @@ extends = rp2040_base board = rpipicow board_level = pr +board_check = true upload_protocol = picotool # add our variants files to the include and src paths build_flags = diff --git a/variants/rp2350/rpipico2w/platformio.ini b/variants/rp2350/rpipico2w/platformio.ini index 59b1354b4..5dbce533b 100644 --- a/variants/rp2350/rpipico2w/platformio.ini +++ b/variants/rp2350/rpipico2w/platformio.ini @@ -2,6 +2,7 @@ extends = rp2350_base board = rpipico2w board_level = pr +board_check = true upload_protocol = jlink # debug settings for external openocd with RP2040 support (custom build) debug_tool = custom