diff --git a/.github/workflows/first_time_contributor.yml b/.github/workflows/first_time_contributor.yml new file mode 100644 index 000000000..1ebc9a602 --- /dev/null +++ b/.github/workflows/first_time_contributor.yml @@ -0,0 +1,47 @@ +name: Welcome First-Time Contributor + +on: + issues: + types: opened + pull_request_target: + types: opened + +permissions: {} + +jobs: + welcome: + runs-on: ubuntu-latest + permissions: + issues: write # Required to post comments and labels on issues + pull-requests: write # Required to post comments and labels on PRs + steps: + - uses: plbstl/first-contribution@v4 + with: + labels: first-contribution + issue-opened-msg: | + ### @{fc-author}, Welcome to Meshtastic! :wave: + + Thanks for opening your first issue. If it's helpful, an easy way + to get logs is the "Open Serial Monitor" button on the [Web Flasher](https://flasher.meshtastic.org). + + If you have ideas for features, note that we often debate big ideas + in the [discussions tab](https://github.com/meshtastic/firmware/discussions/categories/ideas) + first. This tracker tends to be for ideas that have community + consensus and a clear implementation. + + We're very active [on discord](https://discord.com/invite/meshtastic), + especially the \#firmware and \#alphanauts-testing channels. If you'll + be around for a while, we'd love to see you there! + + Welcome to the community! :heart: + + pr-opened-msg: | + ### @{fc-author}, Welcome to Meshtastic! + + Thanks for opening your first pull request. We really appreciate it. + + We discuss work as a team in discord, please join us in the [#firmware channel](https://discord.com/invite/meshtastic). + There's a big backlog of patches at the moment. If you have time, + please help us with some code review and testing of [other PRs](https://github.com/meshtastic/firmware/pulls)! + + Welcome to the team :smile: diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index cabe0dd97..40fcd7109 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -143,7 +143,7 @@ jobs: merge-multiple: true - name: Test Report - uses: dorny/test-reporter@v2.3.0 + uses: dorny/test-reporter@v2.4.0 with: name: PlatformIO Tests path: testreport.xml diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 3656ae32c..705f0177c 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -8,8 +8,8 @@ plugins: uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.495 - - renovate@42.66.8 + - checkov@3.2.496 + - renovate@42.66.14 - prettier@3.7.4 - trufflehog@3.92.4 - yamllint@1.37.1 diff --git a/SECURITY.md b/SECURITY.md index 5092595e1..a77a4d028 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Firmware Version | Supported | | ---------------- | ------------------ | -| 2.6.x | :white_check_mark: | -| <= 2.5.x | :x: | +| 2.7.x | :white_check_mark: | +| <= 2.6.x | :x: | ## Reporting a Vulnerability diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 140ac3e2a..cb8985ee6 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,9 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.18 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.17 diff --git a/debian/changelog b/debian/changelog index b9212c1be..5f25d53ad 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +meshtasticd (2.7.18.0) unstable; urgency=medium + + * Version 2.7.18 + + -- GitHub Actions Fri, 02 Jan 2026 12:45:36 +0000 + meshtasticd (2.7.17.0) unstable; urgency=medium * Version 2.7.17 diff --git a/platformio.ini b/platformio.ini index 768dd5dc4..a8589b006 100644 --- a/platformio.ini +++ b/platformio.ini @@ -119,7 +119,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/862ed040c4ab44f0dfbbe492691f144886102588.zip + https://github.com/meshtastic/device-ui/archive/a8e2f947f7abaf0c5ac8e6dd189a22156335beaa.zip ; Common libs for environmental measurements in telemetry module [environmental_base] diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index 7de6c0740..0716dedd0 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -24,6 +24,9 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) switch (event->inputEvent) { case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_ALT_PRESS: + playClick(); // Low delay feedback + break; + case INPUT_BROKER_SELECT: case INPUT_BROKER_SELECT_LONG: playBeep(); // Confirmation feedback @@ -58,4 +61,4 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) } return 0; // Allow other handlers to process the event -} \ No newline at end of file +} diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index aa8346585..00ad71031 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -113,7 +113,14 @@ void playShutdownMelody() void playChirp() { // A short, friendly "chirp" sound for key presses - ToneDuration melody[] = {{NOTE_AS3, 20}}; // Very short AS3 note + ToneDuration melody[] = {{NOTE_AS3, 20}}; // Short AS3 note + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playClick() +{ + // A very short "click" sound with minimum delay; ideal for rotary encoder events + ToneDuration melody[] = {{NOTE_AS3, 1}}; // Very Short AS3 playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } diff --git a/src/buzz/buzz.h b/src/buzz/buzz.h index c25a54a5b..fa0c8c496 100644 --- a/src/buzz/buzz.h +++ b/src/buzz/buzz.h @@ -9,6 +9,7 @@ void playGPSDisableBeep(); void playComboTune(); void playBoop(); void playChirp(); +void playClick(); void playLongPressLeadUp(); bool playNextLeadUpNote(); // Play the next note in the lead-up sequence void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning \ No newline at end of file diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index f5ca6ed03..8f06fcf9f 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -8,6 +8,7 @@ #include "graphics/draw/UIRenderer.h" #include "main.h" #include "meshtastic/config.pb.h" +#include "modules/ExternalNotificationModule.h" #include "power.h" #include #include @@ -56,7 +57,6 @@ void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second) // === Shared External State === bool hasUnreadMessage = false; -bool isMuted = false; ScreenResolution currentResolution = ScreenResolution::Low; // === Internal State === @@ -306,7 +306,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } - } else if (isMuted) { + } else if (externalNotificationModule->getMute()) { if (currentResolution == ScreenResolution::High) { int iconX = iconRightEdge - mute_symbol_big_width; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; @@ -325,7 +325,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int iconX = iconRightEdge - mute_symbol_width; int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - if (isInverted) { + if (isInverted && !force_no_invert) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); display->setColor(BLACK); @@ -383,7 +383,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } - } else if (isMuted) { + } else if (externalNotificationModule->getMute()) { if (currentResolution == ScreenResolution::High) { int iconX = iconRightEdge - mute_symbol_big_width; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index af0d8dac1..a8ecdfada 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -41,7 +41,6 @@ namespace graphics // Shared state (declare inside namespace) extern bool hasUnreadMessage; -extern bool isMuted; enum class ScreenResolution : uint8_t { UltraLow = 0, Low = 1, High = 2 }; extern ScreenResolution currentResolution; ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth); diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index d9db84c35..5687c8620 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -20,8 +20,8 @@ #include "mesh/MeshTypes.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" +#include "modules/ExternalNotificationModule.h" #include "modules/KeyVerificationModule.h" - #include "modules/TraceRouteModule.h" #include #include @@ -107,50 +107,60 @@ void menuHandler::OnboardMessage() void menuHandler::LoraRegionPicker(uint32_t duration) { - static const char *optionsArray[] = {"Back", - "US", - "EU_433", - "EU_868", - "CN", - "JP", - "ANZ", - "KR", - "TW", - "RU", - "IN", - "NZ_865", - "TH", - "LORA_24", - "UA_433", - "UA_868", - "MY_433", - "MY_" - "919", - "SG_" - "923", - "PH_433", - "PH_868", - "PH_915", - "ANZ_433", - "KZ_433", - "KZ_863", - "NP_865", - "BR_902"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Set the LoRa region"; + static const LoraRegionOption regionOptions[] = { + {"Back", OptionsAction::Back}, + {"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US}, + {"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433}, + {"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868}, + {"CN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_CN}, + {"JP", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_JP}, + {"ANZ", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ}, + {"KR", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KR}, + {"TW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TW}, + {"RU", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_RU}, + {"IN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_IN}, + {"NZ_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NZ_865}, + {"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH}, + {"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24}, + {"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433}, + {"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868}, + {"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433}, + {"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919}, + {"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923}, + {"PH_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_433}, + {"PH_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_868}, + {"PH_915", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_915}, + {"ANZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ_433}, + {"KZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_433}, + {"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863}, + {"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865}, + {"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902}, + }; + + constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]); + static std::array regionLabels{}; + + const char *bannerMessage = "Set the LoRa region"; if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "LoRa Region"; + bannerMessage = "LoRa Region"; } - bannerOptions.durationMs = duration; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 27; - bannerOptions.InitialSelected = 0; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { - config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected); + + auto bannerOptions = + createStaticBannerOptions(bannerMessage, regionOptions, regionLabels, [](const LoraRegionOption &option, int) -> void { + if (!option.hasValue) { + return; + } + + auto selectedRegion = option.value; + if (config.lora.region == selectedRegion) { + return; + } + + config.lora.region = selectedRegion; auto changes = SEGMENT_CONFIG; - // This is needed as we wait til picking the LoRa region to generate keys for the first time. + // FIXME: This should be a method consolidated with the same logic in the admin message as well + // This is needed as we wait til picking the LoRa region to generate keys for the first time. #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) if (!owner.is_licensed) { bool keygenSuccess = false; @@ -187,8 +197,19 @@ void menuHandler::LoraRegionPicker(uint32_t duration) service->reloadConfig(changes); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }); + + bannerOptions.durationMs = duration; + + int initialSelection = 0; + for (size_t i = 0; i < regionCount; ++i) { + if (regionOptions[i].hasValue && regionOptions[i].value == config.lora.region) { + initialSelection = static_cast(i); + break; } - }; + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } @@ -303,102 +324,100 @@ void menuHandler::showConfirmationBanner(const char *message, std::function void { - if (selected == Back) { - menuHandler::menuQueue = menuHandler::clock_menu; - screen->runNow(); - } else if (selected == Digital) { - uiconfig.is_clockface_analog = false; - saveUIConfig(); - screen->setFrames(Screen::FOCUS_CLOCK); - } else { - uiconfig.is_clockface_analog = true; - saveUIConfig(); - screen->setFrames(Screen::FOCUS_CLOCK); - } + static const ClockFaceOption clockFaceOptions[] = { + {"Back", OptionsAction::Back}, + {"Digital", OptionsAction::Select, false}, + {"Analog", OptionsAction::Select, true}, }; + + constexpr size_t clockFaceCount = sizeof(clockFaceOptions) / sizeof(clockFaceOptions[0]); + static std::array clockFaceLabels{}; + + auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels, + [](const ClockFaceOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (uiconfig.is_clockface_analog == option.value) { + return; + } + + uiconfig.is_clockface_analog = option.value; + saveUIConfig(); + screen->setFrames(Screen::FOCUS_CLOCK); + }); + bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1; screen->showOverlayBanner(bannerOptions); } void menuHandler::TZPicker() { - static const char *optionsArray[] = {"Back", - "US/Hawaii", - "US/Alaska", - "US/Pacific", - "US/Arizona", - "US/Mountain", - "US/Central", - "US/Eastern", - "BR/Brasilia", - "UTC", - "EU/Western", - "EU/" - "Central", - "EU/Eastern", - "Asia/Kolkata", - "Asia/Hong_Kong", - "AU/AWST", - "AU/ACST", - "AU/AEST", - "Pacific/NZ"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Pick Timezone"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 19; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 0) { - menuHandler::menuQueue = menuHandler::clock_menu; - screen->runNow(); - } else if (selected == 1) { // Hawaii - strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef)); - } else if (selected == 2) { // Alaska - strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 3) { // Pacific - strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 4) { // Arizona - strncpy(config.device.tzdef, "MST7", sizeof(config.device.tzdef)); - } else if (selected == 5) { // Mountain - strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 6) { // Central - 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) { // 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 == 11) { // EU/Central - strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef)); - } 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 == 13) { // Asia/Kolkata - strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef)); - } else if (selected == 14) { // China - strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef)); - } else if (selected == 15) { // AU/AWST - strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef)); - } 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 == 17) { // AU/AEST - strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } 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) { + static const TimezoneOption timezoneOptions[] = { + {"Back", OptionsAction::Back}, + {"US/Hawaii", OptionsAction::Select, "HST10"}, + {"US/Alaska", OptionsAction::Select, "AKST9AKDT,M3.2.0,M11.1.0"}, + {"US/Pacific", OptionsAction::Select, "PST8PDT,M3.2.0,M11.1.0"}, + {"US/Arizona", OptionsAction::Select, "MST7"}, + {"US/Mountain", OptionsAction::Select, "MST7MDT,M3.2.0,M11.1.0"}, + {"US/Central", OptionsAction::Select, "CST6CDT,M3.2.0,M11.1.0"}, + {"US/Eastern", OptionsAction::Select, "EST5EDT,M3.2.0,M11.1.0"}, + {"BR/Brasilia", OptionsAction::Select, "BRT3"}, + {"UTC", OptionsAction::Select, "UTC0"}, + {"EU/Western", OptionsAction::Select, "GMT0BST,M3.5.0/1,M10.5.0"}, + {"EU/Central", OptionsAction::Select, "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"EU/Eastern", OptionsAction::Select, "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Asia/Kolkata", OptionsAction::Select, "IST-5:30"}, + {"Asia/Hong_Kong", OptionsAction::Select, "HKT-8"}, + {"AU/AWST", OptionsAction::Select, "AWST-8"}, + {"AU/ACST", OptionsAction::Select, "ACST-9:30ACDT,M10.1.0,M4.1.0/3"}, + {"AU/AEST", OptionsAction::Select, "AEST-10AEDT,M10.1.0,M4.1.0/3"}, + {"Pacific/NZ", OptionsAction::Select, "NZST-12NZDT,M9.5.0,M4.1.0/3"}, + }; + + constexpr size_t timezoneCount = sizeof(timezoneOptions) / sizeof(timezoneOptions[0]); + static std::array timezoneLabels{}; + + auto bannerOptions = createStaticBannerOptions( + "Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (strncmp(config.device.tzdef, option.value, sizeof(config.device.tzdef)) == 0) { + return; + } + + strncpy(config.device.tzdef, option.value, sizeof(config.device.tzdef)); + config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0'; + setenv("TZ", config.device.tzdef, 1); service->reloadConfig(SEGMENT_CONFIG); + }); + + int initialSelection = 0; + for (size_t i = 0; i < timezoneCount; ++i) { + if (timezoneOptions[i].hasValue && + strncmp(config.device.tzdef, timezoneOptions[i].value, sizeof(config.device.tzdef)) == 0) { + initialSelection = static_cast(i); + break; } - }; + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } @@ -458,10 +477,9 @@ void menuHandler::messageResponseMenu() #endif BannerOverlayOptions bannerOptions; + bannerOptions.message = "Message Action"; if (currentResolution == ScreenResolution::UltraLow) { bannerOptions.message = "Message"; - } else { - bannerOptions.message = "Message Action"; } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; @@ -843,12 +861,21 @@ void menuHandler::messageViewModeMenu() void menuHandler::homeBaseMenu() { - enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Sleep, enumEnd }; + enum optionsNumbers { Back, Mute, Backlight, Position, Preset, Freetext, Sleep, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; + if (moduleConfig.external_notification.enabled && externalNotificationModule && + config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED) { + if (!externalNotificationModule->getMute()) { + optionsArray[options] = "Temporarily Mute"; + } else { + optionsArray[options] = "Unmute"; + } + optionsEnumArray[options++] = Mute; + } #if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN) optionsArray[options] = "Toggle Backlight"; optionsEnumArray[options++] = Backlight; @@ -872,7 +899,13 @@ void menuHandler::homeBaseMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Backlight) { + if (selected == Mute) { + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + externalNotificationModule->setMute(!externalNotificationModule->getMute()); + IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow();) + } + } else if (selected == Backlight) { + screen->setOn(false); #if defined(PIN_EINK_EN) if (uiconfig.screen_brightness == 1) { uiconfig.screen_brightness = 0; @@ -895,8 +928,12 @@ void menuHandler::homeBaseMenu() } else if (selected == Sleep) { screen->setOn(false); } else if (selected == Position) { - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SEND_PING, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); + service->refreshLocalMeshNode(); + if (service->trySendPosition(NODENUM_BROADCAST, true)) { + IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); + } else { + IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); + } } else if (selected == Preset) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } else if (selected == Freetext) { @@ -949,6 +986,7 @@ void menuHandler::systemBaseMenu() optionsArray[options] = "Notifications"; optionsEnumArray[options++] = Notifications; + optionsArray[options] = "Display Options"; optionsEnumArray[options++] = ScreenOptions; @@ -985,7 +1023,7 @@ void menuHandler::systemBaseMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Notifications) { - menuHandler::menuQueue = menuHandler::notifications_menu; + menuHandler::menuQueue = menuHandler::buzzermodemenupicker; screen->runNow(); } else if (selected == ScreenOptions) { menuHandler::menuQueue = menuHandler::screen_options_menu; @@ -1092,57 +1130,92 @@ void menuHandler::favoriteBaseMenu() void menuHandler::positionBaseMenu() { - enum optionsNumbers { - Back, - GPSToggle, - GPSFormat, + enum class PositionAction { + GpsToggle, + GpsFormat, CompassMenu, CompassCalibrate, GPSSmartPosition, GPSUpdateInterval, - GPSPositionBroadcast, - enumEnd + GPSPositionBroadcast }; - static const char *optionsArray[enumEnd] = { - "Back", "On/Off Toggle", "Format", "Smart Position", "Update Interval", "Broadcast Interval", "Compass"}; - static int optionsEnumArray[enumEnd] = { - Back, GPSToggle, GPSFormat, GPSSmartPosition, GPSUpdateInterval, GPSPositionBroadcast, CompassMenu}; - int options = 7; + static const PositionMenuOption baseOptions[] = { + {"Back", OptionsAction::Back}, + {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, + {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, + {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, + {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, + {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, + {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, + }; - if (accelerometerThread) { - optionsArray[options] = "Compass Calibrate"; - optionsEnumArray[options++] = CompassCalibrate; - } + static const PositionMenuOption calibrateOptions[] = { + {"Back", OptionsAction::Back}, + {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, + {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, + {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, + {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, + {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, + {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, + {"Compass Calibrate", OptionsAction::Select, static_cast(PositionAction::CompassCalibrate)}, + }; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "GPS Action"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == GPSToggle) { + constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]); + constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); + static std::array baseLabels{}; + static std::array calibrateLabels{}; + + auto onSelection = [](const PositionMenuOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + return; + } + + if (!option.hasValue) { + return; + } + + auto action = static_cast(option.value); + switch (action) { + case PositionAction::GpsToggle: menuQueue = gps_toggle_menu; screen->runNow(); - } else if (selected == GPSFormat) { + break; + case PositionAction::GpsFormat: menuQueue = gps_format_menu; screen->runNow(); - } else if (selected == CompassMenu) { + break; + case PositionAction::CompassMenu: menuQueue = compass_point_north_menu; screen->runNow(); - } else if (selected == CompassCalibrate) { - accelerometerThread->calibrate(30); - } else if (selected == GPSSmartPosition) { + break; + case PositionAction::CompassCalibrate: + if (accelerometerThread) { + accelerometerThread->calibrate(30); + } + break; + case PositionAction::GPSSmartPosition: menuQueue = gps_smart_position_menu; screen->runNow(); - } else if (selected == GPSUpdateInterval) { + break; + case PositionAction::GPSUpdateInterval: menuQueue = gps_update_interval_menu; screen->runNow(); - } else if (selected == GPSPositionBroadcast) { + break; + case PositionAction::GPSPositionBroadcast: menuQueue = gps_position_broadcast_menu; screen->runNow(); + break; } }; + + BannerOverlayOptions bannerOptions; + if (accelerometerThread) { + bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection); + } else { + bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); + } + screen->showOverlayBanner(bannerOptions); } @@ -1198,27 +1271,38 @@ void menuHandler::nodeListMenu() void menuHandler::nodeNameLengthMenu() { - enum OptionsNumbers { Back, Long, Short }; - static const char *optionsArray[] = {"Back", "Long", "Short"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Node Name Length"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Long) { - // Set names to long - LOG_INFO("Setting names to long"); - config.display.use_long_node_name = true; - } else if (selected == Short) { - // Set names to short - LOG_INFO("Setting names to short"); - config.display.use_long_node_name = false; - } else if (selected == Back) { - menuQueue = node_base_menu; - screen->runNow(); - } + static const NodeNameOption nodeNameOptions[] = { + {"Back", OptionsAction::Back}, + {"Long", OptionsAction::Select, true}, + {"Short", OptionsAction::Select, false}, }; - bannerOptions.InitialSelected = config.display.use_long_node_name == true ? 1 : 2; + + constexpr size_t nodeNameCount = sizeof(nodeNameOptions) / sizeof(nodeNameOptions[0]); + static std::array nodeNameLabels{}; + + auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels, + [](const NodeNameOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = node_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (config.display.use_long_node_name == option.value) { + return; + } + + config.display.use_long_node_name = option.value; + LOG_INFO("Setting names to %s", option.value ? "long" : "short"); + }); + + int initialSelection = config.display.use_long_node_name ? 1 : 2; + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } @@ -1252,119 +1336,169 @@ void menuHandler::resetNodeDBMenu() void menuHandler::compassNorthMenu() { - enum optionsNumbers { Back, Dynamic, Fixed, Freeze }; - static const char *optionsArray[] = {"Back", "Dynamic", "Fixed Ring", "Freeze Heading"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "North Directions?"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; - bannerOptions.InitialSelected = uiconfig.compass_mode + 1; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Dynamic) { - if (uiconfig.compass_mode != meshtastic_CompassMode_DYNAMIC) { - uiconfig.compass_mode = meshtastic_CompassMode_DYNAMIC; - saveUIConfig(); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } else if (selected == Fixed) { - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) { - uiconfig.compass_mode = meshtastic_CompassMode_FIXED_RING; - saveUIConfig(); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } else if (selected == Freeze) { - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { - uiconfig.compass_mode = meshtastic_CompassMode_FREEZE_HEADING; - saveUIConfig(); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } else if (selected == Back) { - menuQueue = position_base_menu; - screen->runNow(); - } + static const CompassOption compassOptions[] = { + {"Back", OptionsAction::Back}, + {"Dynamic", OptionsAction::Select, meshtastic_CompassMode_DYNAMIC}, + {"Fixed Ring", OptionsAction::Select, meshtastic_CompassMode_FIXED_RING}, + {"Freeze Heading", OptionsAction::Select, meshtastic_CompassMode_FREEZE_HEADING}, }; + + constexpr size_t compassCount = sizeof(compassOptions) / sizeof(compassOptions[0]); + static std::array compassLabels{}; + + auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels, + [](const CompassOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = position_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (uiconfig.compass_mode == option.value) { + return; + } + + uiconfig.compass_mode = option.value; + saveUIConfig(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + }); + + int initialSelection = 0; + for (size_t i = 0; i < compassCount; ++i) { + if (compassOptions[i].hasValue && uiconfig.compass_mode == compassOptions[i].value) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } #if !MESHTASTIC_EXCLUDE_GPS void menuHandler::GPSToggleMenu() { - - static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Toggle GPS"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; - playGPSEnableBeep(); - gps->enable(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 2) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; - playGPSDisableBeep(); - gps->disable(); - service->reloadConfig(SEGMENT_CONFIG); - } else { - menuQueue = position_base_menu; - screen->runNow(); - } + static const GPSToggleOption gpsToggleOptions[] = { + {"Back", OptionsAction::Back}, + {"Enabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_ENABLED}, + {"Disabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_DISABLED}, }; - bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2; + + constexpr size_t toggleCount = sizeof(gpsToggleOptions) / sizeof(gpsToggleOptions[0]); + static std::array toggleLabels{}; + + auto bannerOptions = + createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = position_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (config.position.gps_mode == option.value) { + return; + } + + config.position.gps_mode = option.value; + if (option.value == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + playGPSEnableBeep(); + gps->enable(); + } else { + playGPSDisableBeep(); + gps->disable(); + } + service->reloadConfig(SEGMENT_CONFIG); + }); + + int initialSelection = 0; + for (size_t i = 0; i < toggleCount; ++i) { + if (gpsToggleOptions[i].hasValue && config.position.gps_mode == gpsToggleOptions[i].value) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } void menuHandler::GPSFormatMenu() { + static const GPSFormatOption formatOptionsHigh[] = { + {"Back", OptionsAction::Back}, + {"Decimal Degrees", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, + {"Degrees Minutes Seconds", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, + {"Universal Transverse Mercator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, + {"Military Grid Reference System", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, + {"Open Location Code", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, + {"Ordnance Survey Grid Ref", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, + {"Maidenhead Locator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, + }; - static const char *optionsArray[] = {"Back", - (currentResolution == ScreenResolution::High) ? "Decimal Degrees" : "DEC", - (currentResolution == ScreenResolution::High) ? "Degrees Minutes Seconds" : "DMS", - (currentResolution == ScreenResolution::High) ? "Universal Transverse Mercator" : "UTM", - (currentResolution == ScreenResolution::High) ? "Military Grid Reference System" - : "MGRS", - (currentResolution == ScreenResolution::High) ? "Open Location Code" : "OLC", - (currentResolution == ScreenResolution::High) ? "Ordnance Survey Grid Ref" : "OSGR", - (currentResolution == ScreenResolution::High) ? "Maidenhead Locator" : "MLS"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "GPS Format"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 8; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 2) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 3) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 4) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 5) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 6) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 7) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else { + static const GPSFormatOption formatOptionsLow[] = { + {"Back", OptionsAction::Back}, + {"DEC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, + {"DMS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, + {"UTM", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, + {"MGRS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, + {"OLC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, + {"OSGR", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, + {"MLS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, + }; + + constexpr size_t formatCount = sizeof(formatOptionsHigh) / sizeof(formatOptionsHigh[0]); + static std::array formatLabelsHigh{}; + static std::array formatLabelsLow{}; + + auto onSelection = [](const GPSFormatOption &option, int) -> void { + if (option.action == OptionsAction::Back) { menuQueue = position_base_menu; screen->runNow(); + return; } + + if (!option.hasValue) { + return; + } + + if (uiconfig.gps_format == option.value) { + return; + } + + uiconfig.gps_format = option.value; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); }; - bannerOptions.InitialSelected = uiconfig.gps_format + 1; + + BannerOverlayOptions bannerOptions; + int initialSelection = 0; + + if (currentResolution == ScreenResolution::High) { + bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsHigh, formatLabelsHigh, onSelection); + for (size_t i = 0; i < formatCount; ++i) { + if (formatOptionsHigh[i].hasValue && uiconfig.gps_format == formatOptionsHigh[i].value) { + initialSelection = static_cast(i); + break; + } + } + } else { + bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsLow, formatLabelsLow, onSelection); + for (size_t i = 0; i < formatCount; ++i) { + if (formatOptionsLow[i].hasValue && uiconfig.gps_format == formatOptionsLow[i].value) { + initialSelection = static_cast(i); + break; + } + } + } + + bannerOptions.InitialSelected = initialSelection; screen->showOverlayBanner(bannerOptions); } @@ -1604,9 +1738,9 @@ void menuHandler::BluetoothToggleMenu() void menuHandler::BuzzerModeMenu() { - static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only", "DMs Only"}; + static const char *optionsArray[] = {"All Enabled", "All Disabled", "Notifications", "System Only", "DMs Only"}; BannerOverlayOptions bannerOptions; - bannerOptions.message = "Buzzer Mode"; + bannerOptions.message = "Notification Sounds"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { @@ -1685,100 +1819,63 @@ void menuHandler::switchToMUIMenu() void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) { - static const char *optionsArray[] = { - "Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Blue", "Teal", "Cyan", "Ice", "Pink", - "White", "Gray"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Select Screen Color"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 14; - bannerOptions.bannerCallback = [display](int selected) -> void { + static const ScreenColorOption colorOptions[] = { + {"Back", OptionsAction::Back}, + {"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)}, + {"Meshtastic Green", OptionsAction::Select, ScreenColor(103, 234, 148)}, + {"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)}, + {"Red", OptionsAction::Select, ScreenColor(255, 64, 64)}, + {"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)}, + {"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)}, + {"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)}, + {"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)}, + {"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)}, + {"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)}, + {"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)}, + {"White", OptionsAction::Select, ScreenColor(255, 255, 255)}, + {"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)}, + }; + + constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]); + static std::array colorLabels{}; + + auto bannerOptions = createStaticBannerOptions( + "Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = system_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ HAS_TFT || defined(HACKADAY_COMMUNICATOR) - uint8_t TFT_MESH_r = 0; - uint8_t TFT_MESH_g = 0; - uint8_t TFT_MESH_b = 0; - if (selected == 1) { - LOG_INFO("Setting color to system default or defined variant"); - // Given just before we set all these to zero, we will allow this to go through - } else if (selected == 2) { - LOG_INFO("Setting color to Meshtastic Green"); - TFT_MESH_r = 103; - TFT_MESH_g = 234; - TFT_MESH_b = 148; - } else if (selected == 3) { - LOG_INFO("Setting color to Yellow"); - TFT_MESH_r = 255; - TFT_MESH_g = 255; - TFT_MESH_b = 128; - } else if (selected == 4) { - LOG_INFO("Setting color to Red"); - TFT_MESH_r = 255; - TFT_MESH_g = 64; - TFT_MESH_b = 64; - } else if (selected == 5) { - LOG_INFO("Setting color to Orange"); - TFT_MESH_r = 255; - TFT_MESH_g = 160; - TFT_MESH_b = 20; - } else if (selected == 6) { - LOG_INFO("Setting color to Purple"); - TFT_MESH_r = 204; - TFT_MESH_g = 153; - TFT_MESH_b = 255; - } else if (selected == 7) { - LOG_INFO("Setting color to Blue"); - TFT_MESH_r = 0; - TFT_MESH_g = 0; - TFT_MESH_b = 255; - } else if (selected == 8) { - LOG_INFO("Setting color to Teal"); - TFT_MESH_r = 16; - TFT_MESH_g = 102; - TFT_MESH_b = 102; - } else if (selected == 9) { - LOG_INFO("Setting color to Cyan"); - TFT_MESH_r = 0; - TFT_MESH_g = 255; - TFT_MESH_b = 255; - } else if (selected == 10) { - LOG_INFO("Setting color to Ice"); - TFT_MESH_r = 173; - TFT_MESH_g = 216; - TFT_MESH_b = 230; - } else if (selected == 11) { - LOG_INFO("Setting color to Pink"); - TFT_MESH_r = 255; - TFT_MESH_g = 105; - TFT_MESH_b = 180; - } else if (selected == 12) { - LOG_INFO("Setting color to White"); - TFT_MESH_r = 255; - TFT_MESH_g = 255; - TFT_MESH_b = 255; - } else if (selected == 13) { - LOG_INFO("Setting color to Gray"); - TFT_MESH_r = 128; - TFT_MESH_g = 128; - TFT_MESH_b = 128; - } else { - menuQueue = system_base_menu; - screen->runNow(); - } + const ScreenColor &color = option.value; + if (color.useVariant) { + LOG_INFO("Setting color to system default or defined variant"); + } else { + LOG_INFO("Setting color to %s", option.label); + } + + uint8_t r = color.r; + uint8_t g = color.g; + uint8_t b = color.b; - if (selected != 0) { display->setColor(BLACK); display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); display->setColor(WHITE); - if (TFT_MESH_r == 0 && TFT_MESH_g == 0 && TFT_MESH_b == 0) { + if (color.useVariant || (r == 0 && g == 0 && b == 0)) { #ifdef TFT_MESH_OVERRIDE TFT_MESH = TFT_MESH_OVERRIDE; #else TFT_MESH = COLOR565(0x67, 0xEA, 0x94); #endif } else { - TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); + TFT_MESH = COLOR565(r, g, b); } #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) @@ -1786,16 +1883,40 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) #endif screen->setFrames(graphics::Screen::FOCUS_SYSTEM); - if (TFT_MESH_r == 0 && TFT_MESH_g == 0 && TFT_MESH_b == 0) { + if (color.useVariant || (r == 0 && g == 0 && b == 0)) { uiconfig.screen_rgb_color = 0; } else { - uiconfig.screen_rgb_color = (TFT_MESH_r << 16) | (TFT_MESH_g << 8) | TFT_MESH_b; + uiconfig.screen_rgb_color = + (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); } LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); saveUIConfig(); - } #endif - }; + }); + + int initialSelection = 0; + if (uiconfig.screen_rgb_color == 0) { + initialSelection = 1; + } else { + uint32_t currentColor = uiconfig.screen_rgb_color; + for (size_t i = 0; i < colorCount; ++i) { + if (!colorOptions[i].hasValue) { + continue; + } + const ScreenColor &color = colorOptions[i].value; + if (color.useVariant) { + continue; + } + uint32_t encoded = + (static_cast(color.r) << 16) | (static_cast(color.g) << 8) | static_cast(color.b); + if (encoded == currentColor) { + initialSelection = static_cast(i); + break; + } + } + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } @@ -1981,30 +2102,6 @@ void menuHandler::wifiToggleMenu() screen->showOverlayBanner(bannerOptions); } -void menuHandler::notificationsMenu() -{ - enum optionsNumbers { Back, BuzzerActions }; - static const char *optionsArray[] = {"Back", "Buzzer Actions"}; - static int optionsEnumArray[] = {Back, BuzzerActions}; - int options = 2; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Notifications"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == BuzzerActions) { - menuHandler::menuQueue = menuHandler::buzzermodemenupicker; - screen->runNow(); - } else { - menuQueue = system_base_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); -} - void menuHandler::screenOptionsMenu() { // Check if brightness is supported @@ -2422,9 +2519,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case bluetooth_toggle_menu: BluetoothToggleMenu(); break; - case notifications_menu: - notificationsMenu(); - break; case screen_options_menu: screenOptionsMenu(); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index bda744a66..445513e25 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -39,7 +39,6 @@ class menuHandler number_test, wifi_toggle_menu, bluetooth_toggle_menu, - notifications_menu, screen_options_menu, power_menu, system_base_menu, @@ -98,7 +97,6 @@ class menuHandler static void numberTest(); static void wifiBaseMenu(); static void wifiToggleMenu(); - static void notificationsMenu(); static void screenOptionsMenu(); static void powerMenu(); static void nodeNameLengthMenu(); @@ -130,7 +128,28 @@ template struct MenuOption { MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {} }; +struct ScreenColor { + uint8_t r; + uint8_t g; + uint8_t b; + bool useVariant; + + ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) + : r(rIn), g(gIn), b(bIn), useVariant(variantIn) + { + } +}; + using RadioPresetOption = MenuOption; +using LoraRegionOption = MenuOption; +using TimezoneOption = MenuOption; +using CompassOption = MenuOption; +using ScreenColorOption = MenuOption; +using GPSToggleOption = MenuOption; +using GPSFormatOption = MenuOption; +using NodeNameOption = MenuOption; +using PositionMenuOption = MenuOption; +using ClockFaceOption = MenuOption; } // namespace graphics #endif diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp index 88bed998d..ad0f9fc47 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp @@ -1,6 +1,7 @@ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./PositionsApplet.h" +#include "NodeDB.h" using namespace NicheGraphics; @@ -49,8 +50,8 @@ ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPack if (!hasPosition) return ProcessMessage::CONTINUE; - bool hasHopsAway = (mp.hop_start != 0 && mp.hop_limit <= mp.hop_start); // From NodeDB::updateFrom - uint8_t hopsAway = mp.hop_start - mp.hop_limit; + const int8_t hopsAway = getHopsAway(mp); + const bool hasHopsAway = hopsAway >= 0; // Determine if the position packet would change anything on-screen // ----------------------------------------------------------------- diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index c5748a560..83b64a873 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -195,7 +195,7 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) // but opted NOT TO. Because it is not a good idea to let remote nodes 'probe' to find out which PSKs were "good" vs // bad. routingModule->sendAckNak(meshtastic_Routing_Error_NO_RESPONSE, getFrom(&mp), mp.id, mp.channel, - routingModule->getHopLimitForResponse(mp.hop_start, mp.hop_limit)); + routingModule->getHopLimitForResponse(mp)); } } @@ -235,7 +235,7 @@ void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to) assert(p->which_payload_variant == meshtastic_MeshPacket_decoded_tag); // Should already be set by now p->to = getFrom(&to); // Make sure that if we are sending to the local node, we use our local node addr, not 0 p->channel = to.channel; // Use the same channel that the request came in on - p->hop_limit = routingModule->getHopLimitForResponse(to.hop_start, to.hop_limit); + p->hop_limit = routingModule->getHopLimitForResponse(to); // No need for an ack if we are just delivering locally (it just generates an ignored ack) p->want_ack = (to.from != 0) ? to.want_ack : false; diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index e1037f789..c1b3839bb 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -95,11 +95,8 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp) } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user && nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) { if (airTime->isTxAllowedChannelUtil(true)) { - // Hops used by the request. If somebody in between running modified firmware modified it, ignore it - auto hopStart = mp->hop_start; - auto hopLimit = mp->hop_limit; - uint8_t hopsUsed = hopStart < hopLimit ? config.lora.hop_limit : hopStart - hopLimit; - if (hopsUsed > config.lora.hop_limit + 2) { + const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit); + if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) { LOG_DEBUG("Skip send NodeInfo: %d hops away is too far away", hopsUsed); } else { LOG_INFO("Heard new node on ch. %d, send NodeInfo and ask for response", mp->channel); diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index afdb4d096..5230e5b85 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -64,7 +64,7 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) perhapsRebroadcast(p); } } else { - bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit; + bool isRepeated = getHopsAway(*p) == 0; // If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again if (isRepeated) { if (!findInTxQueue(p->from, p->id)) { @@ -101,8 +101,7 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to); bool weWereSoleRelayer = false; bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer); - if ((weWereRelayer && wasAlreadyRelayer) || - (p->hop_start != 0 && p->hop_start == p->hop_limit && weWereSoleRelayer)) { + if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) { 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 (was relayer %d we were sole %d)", p->from, p->relay_node, wasAlreadyRelayer, weWereSoleRelayer); diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 9052ee17c..3c408f01f 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1549,6 +1549,23 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p) return delta; } +int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown) +{ + // Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a + // bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware + // version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as + // the bitfield is encrypted under the channel encryption key. For encrypted packets, this returns + // defaultIfUnknown when hop_start is 0. + if (p.hop_start == 0 && !(p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield)) + return defaultIfUnknown; // Cannot reliably determine the number of hops. + + // Guard against invalid values. + if (p.hop_start < p.hop_limit) + return defaultIfUnknown; + + return p.hop_start - p.hop_limit; +} + #define NUM_ONLINE_SECS (60 * 60 * 2) // 2 hrs to consider someone offline size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) @@ -1801,9 +1818,10 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT // If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway - if (mp.hop_start != 0 && mp.hop_limit <= mp.hop_start) { + const int8_t hopsAway = getHopsAway(mp); + if (hopsAway >= 0) { info->has_hops_away = true; - info->hops_away = mp.hop_start - mp.hop_limit; + info->hops_away = hopsAway; } sortMeshDB(); } diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 6fd8deb87..817e31617 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -110,6 +110,10 @@ uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n); /// Given a packet, return how many seconds in the past (vs now) it was received uint32_t sinceReceived(const meshtastic_MeshPacket *p); +/// Given a packet, return the number of hops used to reach this node. +/// Returns defaultIfUnknown if the number of hops couldn't be determined. +int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown = -1); + enum LoadFileResult { // Successfully opened the file LOAD_SUCCESS = 1, diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index f7daf1122..5ee513e89 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -171,7 +171,8 @@ const RegionInfo regions[] = { 863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields https://github.com/meshtastic/firmware/issues/7204 */ - RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, true), + RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), + RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, false), /* Nepal @@ -519,6 +520,10 @@ void RadioInterface::applyModemConfig() sf = 12; break; } + if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) { + cr = loraConfig.coding_rate; + LOG_INFO("Using custom Coding Rate %u", cr); + } } else { sf = loraConfig.spread_factor; cr = loraConfig.coding_rate; diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 7619fc106..2b9b17183 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -1,6 +1,7 @@ #include "ReliableRouter.h" #include "Default.h" #include "MeshTypes.h" +#include "NodeDB.h" #include "configuration.h" #include "memGet.h" #include "mesh-pb-constants.h" @@ -108,12 +109,12 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas // If this packet should always be ACKed reliably with want_ack back to the original sender, make sure we // do that unconditionally. sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit), true); + routingModule->getHopLimitForResponse(*p), true); } else if (!p->decoded.request_id && !p->decoded.reply_id) { // If it's not an ACK or a reply, send an ACK. sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); - } else if ((p->hop_start > 0 && p->hop_start == p->hop_limit) || p->next_hop != NO_NEXT_HOP_PREFERENCE) { + routingModule->getHopLimitForResponse(*p)); + } else if ((getHopsAway(*p) == 0) || p->next_hop != NO_NEXT_HOP_PREFERENCE) { // If we received the packet directly from the original sender, send a 0-hop ACK since the original sender // won't overhear any implicit ACKs. If we received the packet via NextHopRouter, also send a 0-hop ACK to // stop the immediate relayer's retransmissions. @@ -123,11 +124,11 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) { LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY"); sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); + routingModule->getHopLimitForResponse(*p)); } else { // Send a 'NO_CHANNEL' error on the primary channel if want_ack packet destined for us cannot be decoded sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(), - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); + routingModule->getHopLimitForResponse(*p)); } } else if (p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum()) && p->hop_limit > 0) { // No wantAck, but we need to ACK with hop limit of 0 if we were the next hop to stop their retransmissions diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 47ed0c85a..a3861521a 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -81,8 +81,7 @@ Router::Router() : concurrency::OSThread("Router"), fromRadioQueue(MAX_RX_FROMRA bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) { // First hop MUST always decrement to prevent retry issues - bool isFirstHop = (p->hop_start != 0 && p->hop_start == p->hop_limit); - if (isFirstHop) { + if (getHopsAway(*p) == 0) { return true; // Always decrement on first hop } @@ -114,7 +113,7 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) // Check 3: role check (moderate cost - multiple comparisons) if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { continue; } @@ -730,7 +729,8 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_NODEINFO_APP, meshtastic_PortNum_ROUTING_APP, meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_ADMIN_APP, meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_KEY_VERIFICATION_APP, meshtastic_PortNum_WAYPOINT_APP, - meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP)) { + meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP, + meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP)) { LOG_DEBUG("Ignore packet on non-standard portnum for CORE_PORTNUMS_ONLY"); cancelSending(p->from, p->id); skipHandle = true; @@ -745,15 +745,19 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) MeshModule::callModules(*p, src); #if !MESHTASTIC_EXCLUDE_MQTT - // Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not to - // us (because we would be able to decrypt it) - if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 && - !isBroadcast(p->to) && !isToUs(p)) - p_encrypted->pki_encrypted = true; - // After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet - if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled && - !isFromUs(p) && mqtt) - mqtt->onSend(*p_encrypted, *p, p->channel); + if (p_encrypted == nullptr) { + LOG_WARN("p_encrypted is null, skipping MQTT publish"); + } else { + // Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not + // to us (because we would be able to decrypt it) + if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 && + !isBroadcast(p->to) && !isToUs(p)) + p_encrypted->pki_encrypted = true; + // After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet + if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled && + !isFromUs(p) && mqtt) + mqtt->onSend(*p_encrypted, *p, p->channel); + } #endif } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 5af9ebaac..8d1ba6346 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -115,7 +115,6 @@ namespace graphics extern int bannerSignalBars; } extern ScanI2C::DeviceAddress cardkb_found; -extern bool graphics::isMuted; extern bool osk_found; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp index 936a7b44a..2cd8ec5ed 100644 --- a/src/modules/NeighborInfoModule.cpp +++ b/src/modules/NeighborInfoModule.cpp @@ -170,7 +170,7 @@ bool NeighborInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, } else { LOG_DEBUG(" Ignoring dummy neighbor info packet (single neighbor with nodeId 0, snr 0)"); } - } else if (mp.hop_start != 0 && mp.hop_start == mp.hop_limit) { + } else if (getHopsAway(mp) == 0) { LOG_DEBUG("Get or create neighbor: %u with snr %f", mp.from, mp.rx_snr); // If the hopLimit is the same as hopStart, then it is a neighbor getOrCreateNeighbor(mp.from, mp.from, 0, diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index 662f5379a..e9e1fc786 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -58,12 +58,11 @@ void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketI router->sendLocal(p); // we sometimes send directly to the local node } -uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit) +uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp) { - if (hopStart != 0) { - // Hops used by the request. If somebody in between running modified firmware modified it, ignore it - uint8_t hopsUsed = hopStart < hopLimit ? config.lora.hop_limit : hopStart - hopLimit; - if (hopsUsed > config.lora.hop_limit) { + const int8_t hopsUsed = getHopsAway(mp); + if (hopsUsed >= 0) { + if (hopsUsed > (int32_t)(config.lora.hop_limit)) { // In event mode, we never want to send packets with more than our default 3 hops. #if !(EVENTMODE) // This falls through to the default. return hopsUsed; // If the request used more hops than the limit, use the same amount of hops diff --git a/src/modules/RoutingModule.h b/src/modules/RoutingModule.h index 5d4b9596f..2ac42f447 100644 --- a/src/modules/RoutingModule.h +++ b/src/modules/RoutingModule.h @@ -20,7 +20,7 @@ class RoutingModule : public ProtobufModule uint8_t hopLimit = 0); // Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response - uint8_t getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit); + uint8_t getHopLimitForResponse(const meshtastic_MeshPacket &mp); protected: friend class Router; diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp index b347b2f5d..51543eab6 100644 --- a/src/modules/SystemCommandsModule.cpp +++ b/src/modules/SystemCommandsModule.cpp @@ -45,10 +45,9 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) // Mute case INPUT_BROKER_MSG_MUTE_TOGGLE: if (moduleConfig.external_notification.enabled && externalNotificationModule) { - bool isMuted = externalNotificationModule->getMute(); - externalNotificationModule->setMute(!isMuted); - IF_SCREEN(graphics::isMuted = !isMuted; if (!isMuted) externalNotificationModule->stopNow(); - screen->showSimpleBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);) + externalNotificationModule->setMute(externalNotificationModule->getMute()); + IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow(); screen->showSimpleBanner( + externalNotificationModule->getMute() ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);) } return 0; // Bluetooth diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp index 215e49c7a..572f0281a 100644 --- a/src/modules/Telemetry/HealthTelemetry.cpp +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -136,12 +136,12 @@ void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState * display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr); if (lastMeasurement.variant.health_metrics.has_heart_bpm) { char heartStr[32]; - snprintf(heartStr, sizeof(heartStr), "Heart Rate: %.0f bpm", lastMeasurement.variant.health_metrics.heart_bpm); + snprintf(heartStr, sizeof(heartStr), "Heart Rate: %u bpm", lastMeasurement.variant.health_metrics.heart_bpm); display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr); } if (lastMeasurement.variant.health_metrics.has_spO2) { char spo2Str[32]; - snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2); + snprintf(spo2Str, sizeof(spo2Str), "spO2: %u %%", lastMeasurement.variant.health_metrics.spO2); display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str); } } diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp index 87a2f1bd2..41dc02cd1 100644 --- a/src/modules/TraceRouteModule.cpp +++ b/src/modules/TraceRouteModule.cpp @@ -1,5 +1,6 @@ #include "TraceRouteModule.h" #include "MeshService.h" +#include "NodeDB.h" #include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" @@ -359,10 +360,10 @@ void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_Ro } // Only insert unknown hops if hop_start is valid - if (p.hop_start != 0 && p.hop_limit <= p.hop_start) { - uint8_t hopsTaken = p.hop_start - p.hop_limit; + const int8_t hopsTaken = getHopsAway(p); + if (hopsTaken >= 0) { int8_t diff = hopsTaken - *route_count; - for (uint8_t i = 0; i < diff; i++) { + for (int8_t i = 0; i < diff; i++) { if (*route_count < ROUTE_SIZE) { route[*route_count] = NODENUM_BROADCAST; // This will represent an unknown hop *route_count += 1; @@ -370,7 +371,7 @@ void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_Ro } // Add unknown SNR values if necessary diff = *route_count - *snr_count; - for (uint8_t i = 0; i < diff; i++) { + for (int8_t i = 0; i < diff; i++) { if (*snr_count < ROUTE_SIZE) { snr_list[*snr_count] = INT8_MIN; // This will represent an unknown SNR *snr_count += 1; diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index b6533fc6a..3b98eca3d 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -14,11 +14,11 @@ #include #include +#ifdef NIMBLE_TWO #include "NimBLEAdvertising.h" -#ifdef CONFIG_BT_NIMBLE_EXT_ADV #include "NimBLEExtAdvertising.h" -#endif #include "PowerStatus.h" +#endif #if defined(CONFIG_NIMBLE_CPP_IDF) #include "host/ble_gap.h" @@ -26,12 +26,15 @@ #include "nimble/nimble/host/include/host/ble_gap.h" #endif +#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) + namespace { constexpr uint16_t kPreferredBleMtu = 517; constexpr uint16_t kPreferredBleTxOctets = 251; constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8; } // namespace +#endif // Debugging options: careful, they slow things down quite a bit! // #define DEBUG_NIMBLE_ON_READ_TIMING // uncomment to time onRead duration @@ -310,9 +313,11 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread { PhoneAPI::onNowHasData(fromRadioNum); -#ifdef DEBUG_NIMBLE_NOTIFY int currentNotifyCount = notifyCount.fetch_add(1); + uint8_t cc = bleServer->getConnectedCount(); + +#ifdef DEBUG_NIMBLE_NOTIFY // This logging slows things down when there are lots of packets going to the phone, like initial connection: LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc); #endif @@ -321,7 +326,13 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread put_le32(val, fromRadioNum); fromNumCharacteristic->setValue(val, sizeof(val)); +#ifdef NIMBLE_TWO + // NOTE: I don't have any NIMBLE_TWO devices, but this line makes me suspicious, and I suspect it needs to just be + // notify(). fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE); +#else + fromNumCharacteristic->notify(); +#endif } /// Check the current underlying physical link to see if the client is currently connected @@ -386,7 +397,12 @@ static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks { - void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &) override +#ifdef NIMBLE_TWO + virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) +#else + virtual void onWrite(NimBLECharacteristic *pCharacteristic) + +#endif { // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. // Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent onWrite calls. @@ -433,7 +449,11 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks { - void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &) override +#ifdef NIMBLE_TWO + virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) +#else + virtual void onRead(NimBLECharacteristic *pCharacteristic) +#endif { // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. @@ -541,27 +561,32 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { +#ifdef NIMBLE_TWO public: - explicit NimbleBluetoothServerCallback(NimbleBluetooth *ble) : ble(ble) {} + NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; } private: NimbleBluetooth *ble; - uint32_t onPassKeyDisplay() override + virtual uint32_t onPassKeyDisplay() +#else + virtual uint32_t onPassKeyRequest() +#endif { uint32_t passkey = config.bluetooth.fixed_pin; if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) { LOG_INFO("Use random passkey"); + // This is the passkey to be entered on peer - we pick a number >100,000 to ensure 6 digits passkey = random(100000, 999999); } - LOG_INFO("*** Enter passkey %06u on the peer side ***", passkey); + LOG_INFO("*** Enter passkey %d on the peer side ***", passkey); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); meshtastic::BluetoothStatus newStatus(std::to_string(passkey)); bluetoothStatus->updateStatus(&newStatus); -#if HAS_SCREEN +#if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (screen) { screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { char btPIN[16] = "888888"; @@ -590,29 +615,39 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks }); } #endif - passkeyShowing = true; + return passkey; } - void onAuthenticationComplete(NimBLEConnInfo &connInfo) override +#ifdef NIMBLE_TWO + virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) +#else + virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) +#endif { LOG_INFO("BLE authentication complete"); meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); bluetoothStatus->updateStatus(&newStatus); + // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (passkeyShowing) { passkeyShowing = false; - if (screen) { + if (screen) screen->endAlert(); - } } + // Store the connection handle for future use +#ifdef NIMBLE_TWO nimbleBluetoothConnHandle = connInfo.getConnHandle(); +#else + nimbleBluetoothConnHandle = desc->conn_handle; +#endif } - void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override +#ifdef NIMBLE_TWO + virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) { LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str()); @@ -637,12 +672,21 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu); pServer->updateConnParams(connHandle, 6, 12, 0, 200); } +#endif - void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override +#ifdef NIMBLE_TWO + virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) { LOG_INFO("BLE disconnect reason: %d", reason); +#else + virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) + { + LOG_INFO("BLE disconnect"); +#endif +#ifdef NIMBLE_TWO if (ble->isDeInit) return; +#endif meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newStatus); @@ -666,69 +710,35 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks bluetoothPhoneAPI->writeCount = 0; } + // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection memset(lastToRadio, 0, sizeof(lastToRadio)); - nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; + nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; // BLE_HS_CONN_HANDLE_NONE means "no connection" +#ifdef NIMBLE_TWO + // Restart Advertising ble->startAdvertising(); +#else + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + if (!pAdvertising->start(0)) { + if (pAdvertising->isAdvertising()) { + LOG_DEBUG("BLE advertising already running"); + } else { + LOG_ERROR("BLE failed to restart advertising"); + } + } +#endif } }; static NimbleBluetoothToRadioCallback *toRadioCallbacks; static NimbleBluetoothFromRadioCallback *fromRadioCallbacks; -void NimbleBluetooth::startAdvertising() -{ -#if defined(CONFIG_BT_NIMBLE_EXT_ADV) - NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - NimBLEExtAdvertisement legacyAdvertising; - - legacyAdvertising.setLegacyAdvertising(true); - legacyAdvertising.setScannable(true); - legacyAdvertising.setConnectable(true); - legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN); - if (powerStatus->getHasBattery() == 1) { - legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f)); - } - legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID)); - legacyAdvertising.setMinInterval(500); - legacyAdvertising.setMaxInterval(1000); - - NimBLEExtAdvertisement legacyScanResponse; - legacyScanResponse.setLegacyAdvertising(true); - legacyScanResponse.setConnectable(true); - legacyScanResponse.setName(getDeviceName()); - - if (!pAdvertising->setInstanceData(0, legacyAdvertising)) { - LOG_ERROR("BLE failed to set legacyAdvertising"); - } else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) { - LOG_ERROR("BLE failed to set legacyScanResponse"); - } else if (!pAdvertising->start(0, 0, 0)) { - LOG_ERROR("BLE failed to start legacyAdvertising"); - } -#else - NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - pAdvertising->reset(); - pAdvertising->addServiceUUID(MESH_SERVICE_UUID); - if (powerStatus->getHasBattery() == 1) { - pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); - } - - NimBLEAdvertisementData scan; - scan.setName(getDeviceName()); - pAdvertising->setScanResponseData(scan); - pAdvertising->enableScanResponse(true); - - if (!pAdvertising->start(0)) { - LOG_ERROR("BLE failed to start advertising"); - } -#endif - LOG_DEBUG("BLE Advertising started"); -} - void NimbleBluetooth::shutdown() { + // No measurable power saving for ESP32 during light-sleep(?) #ifndef ARCH_ESP32 + // Shutdown bluetooth for minimum power draw LOG_INFO("Disable bluetooth"); NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); pAdvertising->reset(); @@ -736,6 +746,7 @@ void NimbleBluetooth::shutdown() #endif } +// Proper shutdown for ESP32. Needs reboot to reverse. void NimbleBluetooth::deinit() { #ifdef ARCH_ESP32 @@ -749,17 +760,21 @@ void NimbleBluetooth::deinit() digitalWrite(BLE_LED, LOW); #endif #endif +#ifndef NIMBLE_TWO + NimBLEDevice::deinit(); +#endif #endif } +// Has initial setup been completed bool NimbleBluetooth::isActive() { - return bleServer != nullptr; + return bleServer; } bool NimbleBluetooth::isConnected() { - return bleServer && bleServer->getConnectedCount() > 0; + return bleServer->getConnectedCount() > 0; } int NimbleBluetooth::getRssi() @@ -803,7 +818,7 @@ void NimbleBluetooth::setup() LOG_INFO("Init the NimBLE bluetooth module"); NimBLEDevice::init(getDeviceName()); - NimBLEDevice::setPower(9); + NimBLEDevice::setPower(ESP_PWR_LVL_P9); #if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)) int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu); @@ -836,7 +851,11 @@ void NimbleBluetooth::setup() NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); } bleServer = NimBLEDevice::createServer(); - auto *serverCallbacks = new NimbleBluetoothServerCallback(this); +#ifdef NIMBLE_TWO + NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this); +#else + NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(); +#endif bleServer->setCallbacks(serverCallbacks, true); setupService(); startAdvertising(); @@ -881,7 +900,11 @@ void NimbleBluetooth::setupService() NimBLEService *batteryService = bleServer->createService(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic) (uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1); +#ifdef NIMBLE_TWO NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904(); +#else + NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904); +#endif batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8); batteryLevelDescriptor->setNamespace(1); batteryLevelDescriptor->setUnit(0x27ad); @@ -889,12 +912,54 @@ void NimbleBluetooth::setupService() batteryService->start(); } +void NimbleBluetooth::startAdvertising() +{ +#ifdef NIMBLE_TWO + NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + NimBLEExtAdvertisement legacyAdvertising; + + legacyAdvertising.setLegacyAdvertising(true); + legacyAdvertising.setScannable(true); + legacyAdvertising.setConnectable(true); + legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN); + if (powerStatus->getHasBattery() == 1) { + legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f)); + } + legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID)); + legacyAdvertising.setMinInterval(500); + legacyAdvertising.setMaxInterval(1000); + + NimBLEExtAdvertisement legacyScanResponse; + legacyScanResponse.setLegacyAdvertising(true); + legacyScanResponse.setConnectable(true); + legacyScanResponse.setName(getDeviceName()); + + if (!pAdvertising->setInstanceData(0, legacyAdvertising)) { + LOG_ERROR("BLE failed to set legacyAdvertising"); + } else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) { + LOG_ERROR("BLE failed to set legacyScanResponse"); + } else if (!pAdvertising->start(0, 0, 0)) { + LOG_ERROR("BLE failed to start legacyAdvertising"); + } +#else + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->reset(); + pAdvertising->addServiceUUID(MESH_SERVICE_UUID); + pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service + pAdvertising->start(0); +#endif +} + /// Given a level between 0-100, update the BLE attribute void updateBatteryLevel(uint8_t level) { if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) { BatteryCharacteristic->setValue(&level, 1); +#ifdef NIMBLE_TWO BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE); +#else + BatteryCharacteristic->notify(); +#endif } } @@ -909,7 +974,11 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length) if (!bleServer || !isConnected() || length > 512) { return; } +#ifdef NIMBLE_TWO logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE); +#else + logRadioCharacteristic->notify(logMessage, length, true); +#endif } void clearNVS() diff --git a/src/nimble/NimbleBluetooth.h b/src/nimble/NimbleBluetooth.h index 2956fe6d0..458fa4a67 100644 --- a/src/nimble/NimbleBluetooth.h +++ b/src/nimble/NimbleBluetooth.h @@ -12,11 +12,16 @@ class NimbleBluetooth : BluetoothApi bool isConnected(); int getRssi(); void sendLog(const uint8_t *logMessage, size_t length); +#if defined(NIMBLE_TWO) void startAdvertising(); +#endif bool isDeInit = false; private: void setupService(); +#if !defined(NIMBLE_TWO) + void startAdvertising(); +#endif }; void setBluetoothEnable(bool enable); diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index b31d2dc2e..a12972cb0 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -418,8 +418,9 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); if (mp->rx_snr != 0) jsonObj["snr"] = new JSONValue((float)mp->rx_snr); - if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { - jsonObj["hops_away"] = new JSONValue((unsigned int)(mp->hop_start - mp->hop_limit)); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); } @@ -450,8 +451,9 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); if (mp->rx_snr != 0) jsonObj["snr"] = new JSONValue((float)mp->rx_snr); - if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { - jsonObj["hops_away"] = new JSONValue((unsigned int)(mp->hop_start - mp->hop_limit)); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); } jsonObj["size"] = new JSONValue((unsigned int)mp->encrypted.size); diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index 353c710a1..41f505b94 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -358,8 +358,9 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, jsonObj["rssi"] = (int)mp->rx_rssi; if (mp->rx_snr != 0) jsonObj["snr"] = (float)mp->rx_snr; - if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { - jsonObj["hops_away"] = (unsigned int)(mp->hop_start - mp->hop_limit); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = (unsigned int)(hopsAway); jsonObj["hop_start"] = (unsigned int)(mp->hop_start); } @@ -393,8 +394,9 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa jsonObj["rssi"] = (int)mp->rx_rssi; if (mp->rx_snr != 0) jsonObj["snr"] = (float)mp->rx_snr; - if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { - jsonObj["hops_away"] = (unsigned int)(mp->hop_start - mp->hop_limit); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = (unsigned int)(hopsAway); jsonObj["hop_start"] = (unsigned int)(mp->hop_start); } jsonObj["size"] = (unsigned int)mp->encrypted.size; diff --git a/variants/esp32/tbeam/platformio.ini b/variants/esp32/tbeam/platformio.ini index ed2bfb1a1..655d48c1d 100644 --- a/variants/esp32/tbeam/platformio.ini +++ b/variants/esp32/tbeam/platformio.ini @@ -2,7 +2,7 @@ [env:tbeam] extends = esp32_base board = ttgo-t-beam -board_level = extra + board_check = true build_flags = ${esp32_base.build_flags} -D TBEAM_V10 @@ -13,7 +13,7 @@ upload_speed = 921600 [env:tbeam-displayshield] extends = env:tbeam - +board_level = extra build_flags = ${env:tbeam.build_flags} -D USE_ST7796 diff --git a/variants/esp32s3/mesh-tab/variant.h b/variants/esp32s3/mesh-tab/variant.h index 92ee951bb..67522a63b 100644 --- a/variants/esp32s3/mesh-tab/variant.h +++ b/variants/esp32s3/mesh-tab/variant.h @@ -55,5 +55,6 @@ #define SX126X_RESET 14 #define SX126X_RXEN 47 #define SX126X_TXEN RADIOLIB_NC // Assuming that DIO2 is connected to TXEN pin +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif diff --git a/variants/esp32s3/rak3312/pins_arduino.h b/variants/esp32s3/rak3312/pins_arduino.h index 15a26e991..dc5d30d61 100644 --- a/variants/esp32s3/rak3312/pins_arduino.h +++ b/variants/esp32s3/rak3312/pins_arduino.h @@ -17,6 +17,8 @@ static const uint8_t MOSI = 11; static const uint8_t MISO = 10; static const uint8_t SCK = 13; +#define SPI_INTERFACES_COUNT 1 + #define SPI_MOSI (11) #define SPI_SCK (13) #define SPI_MISO (10) @@ -25,4 +27,51 @@ static const uint8_t SCK = 13; // LEDs #define LED_BUILTIN LED_GREEN +#ifdef _VARIANT_RAK3112_ +/* + * Serial interfaces + */ +// TXD1 RXD1 on Base Board +#define PIN_SERIAL1_RX (44) +#define PIN_SERIAL1_TX (43) + +/* + * Internal SPI to LoRa transceiver + */ +#define LORA_SX126X_SCK 5 +#define LORA_SX126X_MISO 3 +#define LORA_SX126X_MOSI 6 + +/* + * Analog pins + */ +#define PIN_A0 (21) +#define PIN_A1 (14) + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 2 + +#define PIN_WIRE1_SDA (17) +#define PIN_WIRE1_SCL (18) + +/* + * GPIO's + */ +#define WB_IO1 21 +#define WB_IO2 2 +// #define WB_IO2 14 +#define WB_IO3 41 +#define WB_IO4 42 +#define WB_IO5 38 +#define WB_IO6 39 +// #define WB_SW1 35 NC +#define WB_A0 1 +#define WB_A1 2 +#define WB_CS 12 +#define WB_LED1 46 +#define WB_LED2 45 +#endif + #endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/rak3312/platformio.ini b/variants/esp32s3/rak3312/platformio.ini index 65eba93e6..ddac65d77 100644 --- a/variants/esp32s3/rak3312/platformio.ini +++ b/variants/esp32s3/rak3312/platformio.ini @@ -9,3 +9,15 @@ build_flags = ${esp32s3_base.build_flags} -D RAK3312 -I variants/esp32s3/rak3312 + +[env:rak3112] +extends = esp32s3_base +board = wiscore_rak3312 +board_level = extra +upload_protocol = esptool + +build_flags = + ${esp32_base.build_flags} + -D RAK3312 + -D _VARIANT_RAK3112_ + -I variants/esp32s3/rak3312 diff --git a/variants/esp32s3/rak3312/variant.h b/variants/esp32s3/rak3312/variant.h index b672eb9cd..a9cec8f2e 100644 --- a/variants/esp32s3/rak3312/variant.h +++ b/variants/esp32s3/rak3312/variant.h @@ -18,11 +18,6 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif -#define SX126X_POWER_EN (4) - -#define PIN_POWER_EN PIN_3V3_EN -#define PIN_3V3_EN (14) - #define LED_GREEN 46 #define LED_BLUE 45 @@ -35,10 +30,30 @@ #define LED_STATE_ON 1 // State when LED is litted +#define BATTERY_PIN 1 +#define ADC_CHANNEL ADC_CHANNEL_0 + +#ifdef _VARIANT_RAK3112_ // Modular variant (stamp) +#define ADC_MULTIPLIER 2.11 + +#define BUTTON_NEED_PULLUP + +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +#define SDCARD_CS SPI_CS + +#define I2C_SDA1 PIN_WIRE1_SDA +#define I2C_SCL1 PIN_WIRE1_SCL +#else // Generic 3312 variant (40-pin standard connector) +#define ADC_MULTIPLIER 1.667 + +#define SX126X_POWER_EN (4) + +#define PIN_POWER_EN PIN_3V3_EN +#define PIN_3V3_EN (14) + #define HAS_GPS 1 #define GPS_TX_PIN 43 #define GPS_RX_PIN 44 -#define BATTERY_PIN 1 -#define ADC_CHANNEL ADC_CHANNEL_0 -#define ADC_MULTIPLIER 1.667 \ No newline at end of file +#endif diff --git a/version.properties b/version.properties index 8e40687e9..0a028eff0 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 17 +build = 18