From 953fcca304ed37c94a0c20d19dcc8df86a853b00 Mon Sep 17 00:00:00 2001 From: Jason P Date: Thu, 28 Aug 2025 11:23:24 -0500 Subject: [PATCH 01/10] BaseUI Show/Hide Frame Functionality (#7382) * Rename System Frame (from Memory) in code base * Create menu options to Show/Hide frames: Node Lists, Bearings, Position, LoRa, Clock and Favorites frames * Move Region Picker into submenu * Tweak wording for Send Position vs Node Info if the device has GPS --- src/graphics/Screen.cpp | 223 +++++++++++++++++++--------- src/graphics/Screen.h | 30 +++- src/graphics/draw/DebugRenderer.cpp | 2 +- src/graphics/draw/DebugRenderer.h | 4 +- src/graphics/draw/MenuHandler.cpp | 149 ++++++++++++++++++- src/graphics/draw/MenuHandler.h | 3 + src/graphics/images.h | 4 +- 7 files changed, 331 insertions(+), 84 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0a2229d0e..5bddb6cc8 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -901,75 +901,90 @@ void Screen::setFrames(FrameFocus focus) } #if defined(DISPLAY_CLOCK_FRAME) - fsi.positions.clock = numframes; + if (!hiddenFrames.clock) { + fsi.positions.clock = numframes; #if defined(M5STACK_UNITC6L) - normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; + normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; #else - normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame - : graphics::ClockRenderer::drawDigitalClockFrame; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; #endif - indicatorIcons.push_back(digital_icon_clock); + indicatorIcons.push_back(digital_icon_clock); + } #endif // Declare this early so it’s available in FOCUS_PRESERVE block bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); - fsi.positions.home = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; - indicatorIcons.push_back(icon_home); + if (!hiddenFrames.home) { + fsi.positions.home = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; + indicatorIcons.push_back(icon_home); + } fsi.positions.textMessage = numframes; normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; indicatorIcons.push_back(icon_mail); #ifndef USE_EINK - fsi.positions.nodelist = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; - indicatorIcons.push_back(icon_nodes); + if (!hiddenFrames.nodelist) { + fsi.positions.nodelist = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; + indicatorIcons.push_back(icon_nodes); + } #endif // Show detailed node views only on E-Ink builds #ifdef USE_EINK - fsi.positions.nodelist_lastheard = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; - indicatorIcons.push_back(icon_nodes); - - fsi.positions.nodelist_hopsignal = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; - indicatorIcons.push_back(icon_signal); - - fsi.positions.nodelist_distance = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; - indicatorIcons.push_back(icon_distance); + if (!hiddenFrames.nodelist_lastheard) { + fsi.positions.nodelist_lastheard = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; + indicatorIcons.push_back(icon_nodes); + } + if (!hiddenFrames.nodelist_hopsignal) { + fsi.positions.nodelist_hopsignal = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; + indicatorIcons.push_back(icon_signal); + } + if (!hiddenFrames.nodelist_distance) { + fsi.positions.nodelist_distance = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; + indicatorIcons.push_back(icon_distance); + } #endif #if HAS_GPS - fsi.positions.nodelist_bearings = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; - indicatorIcons.push_back(icon_list); - - fsi.positions.gps = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; - indicatorIcons.push_back(icon_compass); + if (!hiddenFrames.nodelist_bearings) { + fsi.positions.nodelist_bearings = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; + indicatorIcons.push_back(icon_list); + } + if (!hiddenFrames.gps) { + fsi.positions.gps = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; + indicatorIcons.push_back(icon_compass); + } #endif - if (RadioLibInterface::instance) { + if (RadioLibInterface::instance && !hiddenFrames.lora) { fsi.positions.lora = numframes; normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; indicatorIcons.push_back(icon_radio); } - if (!dismissedFrames.memory) { - fsi.positions.memory = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage; - indicatorIcons.push_back(icon_memory); + if (!hiddenFrames.system) { + fsi.positions.system = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; + indicatorIcons.push_back(icon_system); } #if !defined(DISPLAY_CLOCK_FRAME) - fsi.positions.clock = numframes; - normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame - : graphics::ClockRenderer::drawDigitalClockFrame; - indicatorIcons.push_back(digital_icon_clock); + if (!hiddenFrames.clock) { + fsi.positions.clock = numframes; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; + indicatorIcons.push_back(digital_icon_clock); + } #endif #if HAS_WIFI && !defined(ARCH_PORTDUINO) - if (!dismissedFrames.wifi && isWifiAvailable()) { + if (!hiddenFrames.wifi && isWifiAvailable()) { fsi.positions.wifi = numframes; normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; indicatorIcons.push_back(icon_wifi); @@ -1011,27 +1026,29 @@ void Screen::setFrames(FrameFocus focus) if (numMeshNodes > 0) numMeshNodes--; - // Temporary array to hold favorite node frames - std::vector favoriteFrames; + if (!hiddenFrames.show_favorites) { + // Temporary array to hold favorite node frames + std::vector favoriteFrames; - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { - favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { + favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); + } } - } - // Insert favorite frames *after* collecting them all - if (!favoriteFrames.empty()) { - fsi.positions.firstFavorite = numframes; - for (const auto &f : favoriteFrames) { - normalFrames[numframes++] = f; - indicatorIcons.push_back(icon_node); + // Insert favorite frames *after* collecting them all + if (!favoriteFrames.empty()) { + fsi.positions.firstFavorite = numframes; + for (const auto &f : favoriteFrames) { + normalFrames[numframes++] = f; + indicatorIcons.push_back(icon_node); + } + fsi.positions.lastFavorite = numframes - 1; + } else { + fsi.positions.firstFavorite = 255; + fsi.positions.lastFavorite = 255; } - fsi.positions.lastFavorite = numframes - 1; - } else { - fsi.positions.firstFavorite = 255; - fsi.positions.lastFavorite = 255; } fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE @@ -1070,7 +1087,7 @@ void Screen::setFrames(FrameFocus focus) ui->switchToFrame(fsi.positions.clock); break; case FOCUS_SYSTEM: - ui->switchToFrame(fsi.positions.memory); + ui->switchToFrame(fsi.positions.system); break; case FOCUS_PRESERVE: @@ -1098,30 +1115,96 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) setFastFramerate(); } +void Screen::toggleFrameVisibility(const std::string &frameName) +{ +#ifndef USE_EINK + if (frameName == "nodelist") { + hiddenFrames.nodelist = !hiddenFrames.nodelist; + } +#endif +#ifdef USE_EINK + if (frameName == "nodelist_lastheard") { + hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard; + } + if (frameName == "nodelist_hopsignal") { + hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal; + } + if (frameName == "nodelist_distance") { + hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance; + } +#endif +#if HAS_GPS + if (frameName == "nodelist_bearings") { + hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; + } + if (frameName == "gps") { + hiddenFrames.gps = !hiddenFrames.gps; + } +#endif + if (frameName == "lora") { + hiddenFrames.lora = !hiddenFrames.lora; + } + if (frameName == "clock") { + hiddenFrames.clock = !hiddenFrames.clock; + } + if (frameName == "show_favorites") { + hiddenFrames.show_favorites = !hiddenFrames.show_favorites; + } +} + +bool Screen::isFrameHidden(const std::string &frameName) const +{ +#ifndef USE_EINK + if (frameName == "nodelist") + return hiddenFrames.nodelist; +#endif +#ifdef USE_EINK + if (frameName == "nodelist_lastheard") + return hiddenFrames.nodelist_lastheard; + if (frameName == "nodelist_hopsignal") + return hiddenFrames.nodelist_hopsignal; + if (frameName == "nodelist_distance") + return hiddenFrames.nodelist_distance; +#endif +#if HAS_GPS + if (frameName == "nodelist_bearings") + return hiddenFrames.nodelist_bearings; + if (frameName == "gps") + return hiddenFrames.gps; +#endif + if (frameName == "lora") + return hiddenFrames.lora; + if (frameName == "clock") + return hiddenFrames.clock; + if (frameName == "show_favorites") + return hiddenFrames.show_favorites; + + return false; +} + // Dismisses the currently displayed screen frame, if possible // Relevant for text message, waypoint, others in future? // Triggered with a CardKB keycombo -void Screen::dismissCurrentFrame() +void Screen::hideCurrentFrame() { uint8_t currentFrame = ui->getUiState()->currentFrame; bool dismissed = false; - if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { - LOG_INFO("Dismiss Text Message"); + LOG_INFO("Hide Text Message"); devicestate.has_rx_text_message = false; memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { - LOG_DEBUG("Dismiss Waypoint"); + LOG_DEBUG("Hide Waypoint"); devicestate.has_rx_waypoint = false; - dismissedFrames.waypoint = true; + hiddenFrames.waypoint = true; dismissed = true; } else if (currentFrame == framesetInfo.positions.wifi) { - LOG_DEBUG("Dismiss WiFi Screen"); - dismissedFrames.wifi = true; + LOG_DEBUG("Hide WiFi Screen"); + hiddenFrames.wifi = true; dismissed = true; - } else if (currentFrame == framesetInfo.positions.memory) { - LOG_INFO("Dismiss Memory"); - dismissedFrames.memory = true; + } else if (currentFrame == framesetInfo.positions.lora) { + LOG_INFO("Hide LoRa"); + hiddenFrames.lora = true; dismissed = true; } @@ -1277,7 +1360,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) // Outgoing message (likely sent from phone) devicestate.has_rx_text_message = false; memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); - dismissedFrames.textMessage = true; + hiddenFrames.textMessage = true; hasUnreadMessage = false; // Clear unread state when user replies setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list @@ -1402,7 +1485,7 @@ int Screen::handleInputEvent(const InputEvent *event) } else if (event->inputEvent == INPUT_BROKER_SELECT) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { menuHandler::homeBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { menuHandler::systemBaseMenu(); #if HAS_GPS } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { @@ -1411,7 +1494,7 @@ int Screen::handleInputEvent(const InputEvent *event) } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { menuHandler::clockMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { - menuHandler::LoraRegionPicker(); + menuHandler::loraMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { if (devicestate.rx_text_message.from) { menuHandler::messageResponseMenu(); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index ecc39ac60..85dcfaf69 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -593,7 +593,11 @@ class Screen : public concurrency::OSThread void setSSLFrames(); // Dismiss the currently focussed frame, if possible (e.g. text message, waypoint) - void dismissCurrentFrame(); + void hideCurrentFrame(); + + // Menu-driven Show / Hide Toggle + void toggleFrameVisibility(const std::string &frameName); + bool isFrameHidden(const std::string &frameName) const; #ifdef USE_EINK /// Draw an image to remain on E-Ink display after screen off @@ -655,7 +659,7 @@ class Screen : public concurrency::OSThread uint8_t settings = 255; uint8_t wifi = 255; uint8_t deviceFocused = 255; - uint8_t memory = 255; + uint8_t system = 255; uint8_t gps = 255; uint8_t home = 255; uint8_t textMessage = 255; @@ -673,12 +677,28 @@ class Screen : public concurrency::OSThread uint8_t frameCount = 0; } framesetInfo; - struct DismissedFrames { + struct hiddenFrames { bool textMessage = false; bool waypoint = false; bool wifi = false; - bool memory = false; - } dismissedFrames; + bool system = false; + bool home = false; + bool clock = false; +#ifndef USE_EINK + bool nodelist = false; +#endif +#ifdef USE_EINK + bool nodelist_lastheard = false; + bool nodelist_hopsignal = false; + bool nodelist_distance = false; +#endif +#if HAS_GPS + bool nodelist_bearings = false; + bool gps = false; +#endif + bool lora = false; + bool show_favorites = false; + } hiddenFrames; /// Try to start drawing ASAP void setFastFramerate(); diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 6137ddef8..61e919208 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -499,7 +499,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // **************************** // * System Screen * // **************************** -void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setFont(FONT_SMALL); diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h index f4d484f58..3382e931d 100644 --- a/src/graphics/draw/DebugRenderer.h +++ b/src/graphics/draw/DebugRenderer.h @@ -31,8 +31,8 @@ void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state // LoRa information display void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); -// Memory screen display -void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +// System screen display +void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); } // namespace DebugRenderer } // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index ba554dbd6..48e7e808b 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -26,6 +26,24 @@ menuHandler::screenMenus menuHandler::menuQueue = menu_none; bool test_enabled = false; uint8_t test_count = 0; +void menuHandler::loraMenu() +{ + static const char *optionsArray[] = {"Back", "Region Picker"}; + enum optionsNumbers { Back = 0, lora_picker = 1 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "LoRa Actions"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + // No action + } else if (selected == lora_picker) { + menuHandler::menuQueue = menuHandler::lora_picker; + } + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::OnboardMessage() { static const char *optionsArray[] = {"OK", "Got it!"}; @@ -320,7 +338,7 @@ void menuHandler::messageResponseMenu() bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Dismiss) { - screen->dismissCurrentFrame(); + screen->hideCurrentFrame(); } else if (selected == Preset) { if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); @@ -361,8 +379,11 @@ void menuHandler::homeBaseMenu() optionsArray[options] = "Sleep Screen"; optionsEnumArray[options++] = Sleep; #endif - - optionsArray[options] = "Send Position"; + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + optionsArray[options] = "Send Position"; + } else { + optionsArray[options] = "Send Node Info"; + } optionsEnumArray[options++] = Position; #if defined(M5STACK_UNITC6L) optionsArray[options] = "New Preset"; @@ -455,7 +476,7 @@ void menuHandler::textMessageBaseMenu() void menuHandler::systemBaseMenu() { - enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, Test, enumEnd }; + enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, FrameToggles, Test, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; @@ -467,6 +488,9 @@ void menuHandler::systemBaseMenu() optionsArray[options] = "Screen Options"; optionsEnumArray[options++] = ScreenOptions; #endif + + optionsArray[options] = "Frame Visiblity Toggle"; + optionsEnumArray[options++] = FrameToggles; #if defined(M5STACK_UNITC6L) optionsArray[options] = "Bluetooth"; #else @@ -504,6 +528,9 @@ void menuHandler::systemBaseMenu() } else if (selected == PowerMenu) { menuHandler::menuQueue = menuHandler::power_menu; screen->runNow(); + } else if (selected == FrameToggles) { + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); } else if (selected == Test) { menuHandler::menuQueue = menuHandler::test_menu; screen->runNow(); @@ -580,6 +607,7 @@ void menuHandler::positionBaseMenu() optionsArray[options] = "Compass Calibrate"; optionsEnumArray[options++] = CompassCalibrate; } + BannerOverlayOptions bannerOptions; bannerOptions.message = "Position Action"; bannerOptions.optionsArrayPtr = optionsArray; @@ -1220,6 +1248,116 @@ void menuHandler::keyVerificationFinalPrompt() } } +void menuHandler::FrameToggles_menu() +{ + enum optionsNumbers { + Finish, + nodelist, + nodelist_lastheard, + nodelist_hopsignal, + nodelist_distance, + nodelist_bearings, + gps, + lora, + clock, + show_favorites, + enumEnd + }; + static const char *optionsArray[enumEnd] = {"Finish"}; + static int optionsEnumArray[enumEnd] = {Finish}; + int options = 1; + + // Track last selected index (not enum value!) + static int lastSelectedIndex = 0; + +#ifndef USE_EINK + optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List"; + optionsEnumArray[options++] = nodelist; +#endif +#ifdef USE_EINK + optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; + optionsEnumArray[options++] = nodelist_lastheard; + optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; + optionsEnumArray[options++] = nodelist_hopsignal; + optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; + optionsEnumArray[options++] = nodelist_distance; +#endif +#if HAS_GPS + optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings"; + optionsEnumArray[options++] = nodelist_bearings; + + optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; + optionsEnumArray[options++] = gps; +#endif + + optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa"; + optionsEnumArray[options++] = lora; + + optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock"; + optionsEnumArray[options++] = clock; + + optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; + optionsEnumArray[options++] = show_favorites; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Show/Hide Frames"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value + + bannerOptions.bannerCallback = [optionsEnumArray, options](int selected) mutable -> void { + // Find the index of selected in optionsEnumArray + int idx = 0; + for (; idx < options; ++idx) { + if (optionsEnumArray[idx] == selected) + break; + } + lastSelectedIndex = idx; + + if (selected == Finish) { + screen->setFrames(Screen::FOCUS_DEFAULT); + } else if (selected == nodelist) { + screen->toggleFrameVisibility("nodelist"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_lastheard) { + screen->toggleFrameVisibility("nodelist_lastheard"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_hopsignal) { + screen->toggleFrameVisibility("nodelist_hopsignal"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_distance) { + screen->toggleFrameVisibility("nodelist_distance"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_bearings) { + screen->toggleFrameVisibility("nodelist_bearings"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == gps) { + screen->toggleFrameVisibility("gps"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == lora) { + screen->toggleFrameVisibility("lora"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == clock) { + screen->toggleFrameVisibility("clock"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == show_favorites) { + screen->toggleFrameVisibility("show_favorites"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::handleMenuSwitch(OLEDDisplay *display) { if (menuQueue != menu_none) @@ -1316,6 +1454,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case power_menu: powerMenu(); break; + case FrameToggles: + FrameToggles_menu(); + break; case throttle_message: screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index ed49a89fb..4e7e02173 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -39,11 +39,13 @@ class menuHandler key_verification_final_prompt, trace_route_menu, throttle_message, + FrameToggles }; static screenMenus menuQueue; static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); + static void loraMenu(); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm); static void clockMenu(); @@ -77,6 +79,7 @@ class menuHandler static void screenOptionsMenu(); static void powerMenu(); static void textMessageMenu(); + static void FrameToggles_menu(); private: static void saveUIConfig(); diff --git a/src/graphics/images.h b/src/graphics/images.h index 4a58edb3b..fd9a2db0f 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -118,8 +118,8 @@ const uint8_t icon_radio[] PROGMEM = { 0xA9 // Row 7: #..#.#.# }; -// 🪙 Memory Icon -const uint8_t icon_memory[] PROGMEM = { +// 🪙 System Icon +const uint8_t icon_system[] PROGMEM = { 0x24, // Row 0: ..#..#.. 0x3C, // Row 1: ..####.. 0xC3, // Row 2: ##....## From 8e1da8561e76f0645d25a3761bd04506a52640e7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 19:56:47 -0500 Subject: [PATCH 02/10] Update actions/checkout action to v5 (#8031) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build_one_arch.yml | 12 ++++++------ .github/workflows/build_one_target.yml | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_one_arch.yml b/.github/workflows/build_one_arch.yml index 5901a335c..204130711 100644 --- a/.github/workflows/build_one_arch.yml +++ b/.github/workflows/build_one_arch.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: 3.x @@ -52,7 +52,7 @@ jobs: version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT @@ -266,7 +266,7 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} @@ -345,7 +345,7 @@ jobs: - package-pio-deps-native-tft steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 @@ -414,7 +414,7 @@ jobs: needs: [release-artifacts, version] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 @@ -469,7 +469,7 @@ jobs: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index 07478ff93..f5831ed52 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: 3.x @@ -63,7 +63,7 @@ jobs: if: ${{ inputs.target != '' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT @@ -171,7 +171,7 @@ jobs: needs: [version, build-arch] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} @@ -250,7 +250,7 @@ jobs: - package-pio-deps-native-tft steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 @@ -309,7 +309,7 @@ jobs: needs: [release-artifacts, version] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 @@ -364,7 +364,7 @@ jobs: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 From f083864f1fbb20b04df2fd646bf25bd27d0775fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 19:56:57 -0500 Subject: [PATCH 03/10] Update actions/download-artifact action to v5 (#8032) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build_one_arch.yml | 14 +++++++------- .github/workflows/build_one_target.yml | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build_one_arch.yml b/.github/workflows/build_one_arch.yml index 204130711..3b11a9758 100644 --- a/.github/workflows/build_one_arch.yml +++ b/.github/workflows/build_one_arch.yml @@ -271,7 +271,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: path: ./ pattern: firmware-${{inputs.arch}}-* @@ -300,7 +300,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -364,14 +364,14 @@ jobs: Autogenerated by github action, developer should edit as required before publishing... - name: Download source deb - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -421,7 +421,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -438,7 +438,7 @@ jobs: - name: Zip firmware run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip merge-multiple: true @@ -476,7 +476,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index f5831ed52..4bf5dfe74 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -176,7 +176,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: path: ./ pattern: firmware-*-* @@ -205,7 +205,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-*-${{ needs.version.outputs.long }} merge-multiple: true @@ -269,14 +269,14 @@ jobs: Autogenerated by github action, developer should edit as required before publishing... - name: Download source deb - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -316,7 +316,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-*-${{ needs.version.outputs.long }} merge-multiple: true @@ -333,7 +333,7 @@ jobs: - name: Zip firmware run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: debug-elfs-*-${{ needs.version.outputs.long }}.zip merge-multiple: true @@ -371,7 +371,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true From 7821919faea62d751352d8859531d5a35caa17a3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 20:12:28 -0500 Subject: [PATCH 04/10] Update actions/setup-python action to v6 (#8033) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build_one_arch.yml | 8 ++++---- .github/workflows/build_one_target.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build_one_arch.yml b/.github/workflows/build_one_arch.yml index 3b11a9758..f5352b3c4 100644 --- a/.github/workflows/build_one_arch.yml +++ b/.github/workflows/build_one_arch.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x cache: pip @@ -348,7 +348,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -417,7 +417,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -472,7 +472,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index 4bf5dfe74..3c83ce960 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x cache: pip @@ -253,7 +253,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -312,7 +312,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -367,7 +367,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x From dcd53eb7cb61a751903851215551b037e28c3301 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 5 Sep 2025 23:06:58 -0400 Subject: [PATCH 05/10] Phone GPS display on Position Screen for BaseUI (#7875) * Phone GPS display on Position Screen This is a PR to show when a phone shares GPS location with the node so you can reliably know what coordinate is being shared with the Mesh. --- src/graphics/draw/UIRenderer.cpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index e00b19b2f..cd88d2f3d 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -937,7 +937,26 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU config.display.heading_bold = false; const char *displayLine = ""; // Initialize to empty string by default - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + + bool usePhoneGPS = (ourNode && nodeDB->hasValidPosition(ourNode) && + config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED); + + if (usePhoneGPS) { + // Phone-provided GPS is active + displayLine = "Phone GPS"; + int yOffset = (isHighResolution) ? 3 : 1; + if (isHighResolution) { + NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, + imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, + imgSatellite); + } + int xOffset = (isHighResolution) ? 6 : 0; + display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); + } else if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + // GPS disabled / not present if (config.position.fixed_position) { displayLine = "Fixed GPS"; } else { @@ -954,6 +973,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU int xOffset = (isHighResolution) ? 6 : 0; display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); } else { + // Onboard GPS UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); } @@ -1013,6 +1033,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // === Fifth Row: Altitude === char DisplayLineTwo[32] = {0}; + int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode)) + ? ourNode->position.altitude + : geoCoord.getAltitude(); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); } else { From 39648e609a6229e89571d1b1495d1c8632fd06f5 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 16 Sep 2025 13:11:18 -0500 Subject: [PATCH 06/10] Merge pull request #8004 from compumike/compumike/debug-heap-add-free-heap-debugging-to-all-log-lines When `DEBUG_HEAP` is defined, add free heap bytes to every log line in `RedirectablePrint::log_to_serial` --- platformio.ini | 1 + src/RedirectablePrint.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/platformio.ini b/platformio.ini index 42453a33e..941e33beb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -55,6 +55,7 @@ build_flags = -Wno-missing-field-initializers -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-D OLED_PL=1 + #-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs monitor_speed = 115200 monitor_filters = direct diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 7c8d77651..1a5fd6e6f 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -4,6 +4,7 @@ #include "concurrency/OSThread.h" #include "configuration.h" #include "main.h" +#include "memGet.h" #include "mesh/generated/meshtastic/mesh.pb.h" #include #include @@ -166,6 +167,16 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, print(thread->ThreadName); print("] "); } + +#ifdef DEBUG_HEAP + // Add heap free space bytes prefix before every log message +#ifdef ARCH_PORTDUINO + ::printf("[heap %u] ", memGet.getFreeHeap()); +#else + printf("[heap %u] ", memGet.getFreeHeap()); +#endif +#endif // DEBUG_HEAP + r += vprintf(logLevel, format, arg); } From 2bafac242efe0257b7dae5d07f1143a6d07e587b Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Sep 2025 02:29:47 +0200 Subject: [PATCH 07/10] Feature: Seamless Cross-Preset Communication via UDP Multicast Bridging (#7753) * Added compatibility between nodes on different Presets through `Mesh via UDP` * Optimize multicast handling and channel mapping - FloodingRouter: remove redundant UDP-encrypted rebroadcast suppression. - Router: guard multicast fallback with HAS_UDP_MULTICAST and map fallback-decoded packets to the local default channel via isDefaultChannel() - UdpMulticastHandler: set transport_mechanism only after successful decode * trunk fmt * Move setting transport mechanism. --------- Co-authored-by: GUVWAF --- src/mesh/Channels.cpp | 26 +++++++++++++++++++++++ src/mesh/Channels.h | 2 ++ src/mesh/Router.cpp | 30 +++++++++++++++++++++++++++ src/mesh/udp/UdpMulticastHandler.h | 2 +- variants/esp32s3/t-deck-pro/variant.h | 15 +++++++------- 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 4ef41ddfb..4c0a0edad 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -423,6 +423,32 @@ bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash) } } +bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) +{ + // Iterate all known presets + for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; + ++preset) { + const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false); + if (!name) + continue; + if (strcmp(name, "Invalid") == 0) + continue; // skip invalid placeholder + uint8_t h = xorHash((const uint8_t *)name, strlen(name)); + // Expand default PSK alias 1 to actual bytes and xor into hash + uint8_t tmp = h ^ xorHash(defaultpsk, sizeof(defaultpsk)); + if (tmp == channelHash) { + // Set crypto to defaultpsk and report success + CryptoKey k; + memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); + k.length = sizeof(defaultpsk); + crypto->setKey(k); + LOG_INFO("Matched default preset '%s' for hash 0x%x; set default PSK", name, channelHash); + return true; + } + } + return false; +} + /** Given a channel index setup crypto for encoding that channel (or the primary channel if that channel is unsecured) * * This method is called before encoding outbound packets diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h index 7873a306a..b53f552fa 100644 --- a/src/mesh/Channels.h +++ b/src/mesh/Channels.h @@ -94,6 +94,8 @@ class Channels bool ensureLicensedOperation(); + bool setDefaultPresetCryptoForHash(ChannelHash channelHash); + private: /** Given a channel index, change to use the crypto key specified by that index * diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 6c5d08a93..8bfa7ae9f 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -430,6 +430,36 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) } } } + +#if HAS_UDP_MULTICAST + // Fallback: for UDP multicast, try default preset names with default PSK if normal channel match failed + if (!decrypted && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP) { + if (channels.setDefaultPresetCryptoForHash(p->channel)) { + memcpy(bytes, p->encrypted.bytes, rawSize); + crypto->decrypt(p->from, p->id, rawSize, bytes); + + meshtastic_Data decodedtmp; + memset(&decodedtmp, 0, sizeof(decodedtmp)); + if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) && + decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) { + p->decoded = decodedtmp; + p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; + // Map to our local default channel index (name+PSK default), not necessarily primary + ChannelIndex defaultIndex = channels.getPrimaryIndex(); + for (ChannelIndex i = 0; i < channels.getNumChannels(); ++i) { + if (channels.isDefaultChannel(i)) { + defaultIndex = i; + break; + } + } + chIndex = defaultIndex; + decrypted = true; + } else { + LOG_WARN("UDP fallback decode attempted but failed for hash 0x%x", p->channel); + } + } + } +#endif if (decrypted) { // parsing was successful p->channel = chIndex; // change to store the index instead of the hash diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 9650668a8..2df8686a3 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -50,10 +50,10 @@ class UdpMulticastHandler final LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength); #endif meshtastic_MeshPacket mp; - mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength); bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp); if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; mp.pki_encrypted = false; mp.public_key.size = 0; memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes)); diff --git a/variants/esp32s3/t-deck-pro/variant.h b/variants/esp32s3/t-deck-pro/variant.h index abe0a772a..35cb99435 100644 --- a/variants/esp32s3/t-deck-pro/variant.h +++ b/variants/esp32s3/t-deck-pro/variant.h @@ -93,11 +93,10 @@ // Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface // code) -#define MODEM_POWER_EN 41 -#define MODEM_PWRKEY 40 -#define MODEM_RST 9 -#define MODEM_RI 7 -#define MODEM_DTR 8 -#define MODEM_RX 10 -#define MODEM_TX 11 - +#define MODEM_POWER_EN 41 +#define MODEM_PWRKEY 40 +#define MODEM_RST 9 +#define MODEM_RI 7 +#define MODEM_DTR 8 +#define MODEM_RX 10 +#define MODEM_TX 11 From 68ba3b315c5d7e5b6b35764330b865d1343a1fa1 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 17 Sep 2025 08:37:51 -0500 Subject: [PATCH 08/10] Auto-favorite remote admin node --- src/modules/AdminModule.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 407003f7e..79ea7bc0c 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -104,6 +104,10 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta (config.security.admin_key[2].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) { LOG_INFO("PKC admin payload with authorized sender key"); + auto remoteNode = nodeDB->getMeshNode(mp.from); + if (remoteNode && !remoteNode->is_favorite) { + remoteNode->is_favorite = true; + } } else { myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED, &mp); LOG_INFO("Received PKC admin payload, but the sender public key does not match the admin authorized key!"); From b14e5770d540512f46cfa93fc046f9d2cdd8f804 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 13:11:53 -0500 Subject: [PATCH 09/10] Merge pull request #7873 from compumike/compumike/client-base-role Add `CLIENT_BASE` role: `ROUTER` for favorites, `CLIENT` otherwise (for attic/roof nodes!) --- src/DisplayFormatters.cpp | 45 ++++++++++++++++++++++ src/graphics/Screen.cpp | 8 ++-- src/mesh/FloodingRouter.cpp | 26 +++++++++++-- src/mesh/FloodingRouter.h | 4 ++ src/mesh/NextHopRouter.cpp | 13 +++++-- src/mesh/NextHopRouter.h | 3 ++ src/mesh/NodeDB.cpp | 59 +++++++++++++++++++++++++++++ src/mesh/NodeDB.h | 10 +++++ src/mesh/RadioInterface.cpp | 23 +++++++++-- src/mesh/RadioInterface.h | 5 ++- src/mesh/RadioLibInterface.cpp | 8 ++-- src/mesh/RadioLibInterface.h | 2 +- src/platform/portduino/SimRadio.cpp | 6 +-- src/platform/portduino/SimRadio.h | 2 +- 14 files changed, 191 insertions(+), 23 deletions(-) diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index d367aa661..5193e1cb4 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -38,4 +38,49 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC return useShortName ? "Custom" : "Invalid"; break; } +} + +const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role) +{ + switch (role) { + case meshtastic_Config_DeviceConfig_Role_CLIENT: + return "Client"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE: + return "Client Mute"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN: + return "Client Hidden"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE: + return "Client Base"; + break; + case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND: + return "Lost and Found"; + break; + case meshtastic_Config_DeviceConfig_Role_TRACKER: + return "Tracker"; + break; + case meshtastic_Config_DeviceConfig_Role_SENSOR: + return "Sensor"; + break; + case meshtastic_Config_DeviceConfig_Role_TAK: + return "TAK"; + break; + case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER: + return "TAK Tracker"; + break; + case meshtastic_Config_DeviceConfig_Role_ROUTER: + return "Router"; + break; + case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE: + return "Router Late"; + break; + case meshtastic_Config_DeviceConfig_Role_REPEATER: + return "Repeater"; + break; + default: + return "Unknown"; + break; + } } \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 5bddb6cc8..807cb5867 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -25,6 +25,7 @@ along with this program. If not, see . #include "PowerMon.h" #include "Throttle.h" #include "configuration.h" +#include "meshUtils.h" #if HAS_SCREEN #include @@ -58,7 +59,6 @@ along with this program. If not, see . #include "mesh-pb-constants.h" #include "mesh/Channels.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" -#include "meshUtils.h" #include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "modules/WaypointModule.h" @@ -1562,13 +1562,15 @@ bool shouldWakeOnReceivedMessage() /* The goal here is to determine when we do NOT wake up the screen on message received: - Any ext. notifications are turned on - - If role is not client / client_mute + - If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE - If the battery level is very low */ if (moduleConfig.external_notification.enabled) { return false; } - if (!meshtastic_Config_DeviceConfig_Role_CLIENT && !meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE) { + if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, + meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { return false; } if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index dbd458b61..f805055c8 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -43,12 +43,30 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) return Router::shouldFilterReceived(p); } +bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p) +{ + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + // ROUTER, REPEATER, ROUTER_LATE should never cancel relaying a packet (i.e. we should always rebroadcast), + // even if we've heard another station rebroadcast it already. + return false; + } + + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + // CLIENT_BASE: if the packet is from or to a favorited node, + // we should act like a ROUTER and should never cancel a rebroadcast (i.e. we should always rebroadcast), + // even if we've heard another station rebroadcast it already. + return !nodeDB->isFromOrToFavoritedNode(*p); + } + + // All other roles (such as CLIENT) should cancel a rebroadcast if they hear another station's rebroadcast. + return true; +} + void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) { - if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && - config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER && - config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && - p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { + if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && roleAllowsCancelingDupe(p)) { // cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater! // But only LoRa packets should be able to trigger this. if (Router::cancelSending(p->from, p->id)) diff --git a/src/mesh/FloodingRouter.h b/src/mesh/FloodingRouter.h index 36c6ad8aa..30ad5945b 100644 --- a/src/mesh/FloodingRouter.h +++ b/src/mesh/FloodingRouter.h @@ -59,6 +59,10 @@ class FloodingRouter : public Router */ virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; + // Return false for roles like ROUTER or REPEATER which should always rebroadcast even when we've heard another rebroadcast of + // the same packet + bool roleAllowsCancelingDupe(const meshtastic_MeshPacket *p); + /* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */ void perhapsCancelDupe(const meshtastic_MeshPacket *p); diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index db3d62038..9bb8b240c 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -161,6 +161,15 @@ bool NextHopRouter::stopRetransmission(NodeNum from, PacketId id) return stopRetransmission(key); } +bool NextHopRouter::roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p) +{ + // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) + + // Return false for roles like ROUTER, REPEATER, ROUTER_LATE which should always transmit the packet at least once. + + return roleAllowsCancelingDupe(p); // same logic as FloodingRouter::roleAllowsCancelingDupe +} + bool NextHopRouter::stopRetransmission(GlobalPacketId key) { auto old = findPendingPacket(key); @@ -170,9 +179,7 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key) to avoid canceling a transmission if it was ACKed super fast via MQTT */ if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) { // We only cancel it if we are the original sender or if we're not a router(_late)/repeater - if (isFromUs(p) || (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && - config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER && - config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { + if (isFromUs(p) || roleAllowsCancelingFromTxQueue(p)) { // remove the 'original' (identified by originator and packet->id) from the txqueue and free it cancelSending(getFrom(p), p->id); } diff --git a/src/mesh/NextHopRouter.h b/src/mesh/NextHopRouter.h index 6c2764aff..0022644e9 100644 --- a/src/mesh/NextHopRouter.h +++ b/src/mesh/NextHopRouter.h @@ -121,6 +121,9 @@ class NextHopRouter : public FloodingRouter */ PendingPacket *startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx = NUM_INTERMEDIATE_RETX); + // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) + bool roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p); + /** * Stop any retransmissions we are doing of the specified node/packet ID pair * diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 6473722d7..65d2f4d1c 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1750,6 +1750,65 @@ void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) } } +bool NodeDB::isFavorite(uint32_t nodeId) +{ + // returns true if nodeId is_favorite; false if not or not found + + // NODENUM_BROADCAST will never be in the DB + if (nodeId == NODENUM_BROADCAST) + return false; + + meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); + + if (lite) { + return lite->is_favorite; + } + return false; +} + +bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) +{ + // This method is logically equivalent to: + // return isFavorite(p.from) || isFavorite(p.to); + // but is more efficient by: + // 1. doing only one pass through the database, instead of two + // 2. exiting early when a favorite is found, or if both from and to have been seen + + if (p.to == NODENUM_BROADCAST) + return isFavorite(p.from); // we never store NODENUM_BROADCAST in the DB, so we only need to check p.from + + meshtastic_NodeInfoLite *lite = NULL; + + bool seenFrom = false; + bool seenTo = false; + + for (int i = 0; i < numMeshNodes; i++) { + lite = &meshNodes->at(i); + + if (lite->num == p.from) { + if (lite->is_favorite) + return true; + + seenFrom = true; + } + + if (lite->num == p.to) { + if (lite->is_favorite) + return true; + + seenTo = true; + } + + if (seenFrom && seenTo) + return false; // we've seen both, and neither is a favorite, so we can stop searching early + + // Note: if we knew that sortMeshDB was always called after any change to is_favorite, we could exit early after searching + // all favorited nodes first. + } + + return false; +} + void NodeDB::pause_sort(bool paused) { sortingIsPaused = paused; diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 167dc1337..f73f64f92 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -185,6 +185,16 @@ class NodeDB */ void set_favorite(bool is_favorite, uint32_t nodeId); + /* + * Returns true if the node is in the NodeDB and marked as favorite + */ + bool isFavorite(uint32_t nodeId); + + /* + * Returns true if p->from or p->to is a favorited node + */ + bool isFromOrToFavoritedNode(const meshtastic_MeshPacket &p); + /** * Other functions like the node picker can request a pause in the node sorting */ diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index a5c293868..71fcf1e74 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -311,16 +311,33 @@ uint32_t RadioInterface::getTxDelayMsecWeightedWorst(float snr) return (2 * CWmax * slotTimeMsec) + pow_of_2(CWsize) * slotTimeMsec; } +/** Returns true if we should rebroadcast early like a ROUTER */ +bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p) +{ + // If we are a ROUTER or REPEATER, we always rebroadcast early + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { + return true; + } + + // If we are a CLIENT_BASE and the packet is from or to a favorited node, we should rebroadcast early + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + return nodeDB->isFromOrToFavoritedNode(*p); + } + + return false; +} + /** The delay to use when we want to flood a message */ -uint32_t RadioInterface::getTxDelayMsecWeighted(float snr) +uint32_t RadioInterface::getTxDelayMsecWeighted(meshtastic_MeshPacket *p) { // high SNR = large CW size (Long Delay) // low SNR = small CW size (Short Delay) + float snr = p->rx_snr; uint32_t delay = 0; uint8_t CWsize = getCWsize(snr); // LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize); - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || - config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { + if (shouldRebroadcastEarlyLikeRouter(p)) { delay = random(0, 2 * CWsize) * slotTimeMsec; LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay); } else { diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index c9e71cfa8..eff284747 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -180,8 +180,11 @@ class RadioInterface /** The worst-case SNR_based packet delay */ uint32_t getTxDelayMsecWeightedWorst(float snr); + /** Returns true if we should rebroadcast early like a ROUTER */ + bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p); + /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */ - uint32_t getTxDelayMsecWeighted(float snr); + uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p); /** If the packet is not already in the late rebroadcast window, move it there */ virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; } diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index e3ef58f14..56341adf9 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -310,7 +310,7 @@ void RadioLibInterface::setTransmitDelay() // So we want to make sure the other side has had a chance to reconfigure its radio. if (p->tx_after) { - unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p->rx_snr) : getTxDelayMsec(); + unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p) : getTxDelayMsec(); unsigned long now = millis(); p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false); @@ -323,7 +323,7 @@ void RadioLibInterface::setTransmitDelay() } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerSNR(p->rx_snr); + startTransmitTimerRebroadcast(p); } } @@ -336,11 +336,11 @@ void RadioLibInterface::startTransmitTimer(bool withDelay) } } -void RadioLibInterface::startTransmitTimerSNR(float snr) +void RadioLibInterface::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { - uint32_t delay = getTxDelayMsecWeighted(snr); + uint32_t delay = getTxDelayMsecWeighted(p); notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable } } diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 2ab2679c0..9f497812f 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -161,7 +161,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified * timer scaled to SNR of to be flooded packet * @return Timestamp after which the packet may be sent */ - void startTransmitTimerSNR(float snr); + void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); void handleTransmitInterrupt(); void handleReceiveInterrupt(); diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index 4e748c5f9..cea1eab3a 100644 --- a/src/platform/portduino/SimRadio.cpp +++ b/src/platform/portduino/SimRadio.cpp @@ -43,7 +43,7 @@ void SimRadio::setTransmitDelay() } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerSNR(p->rx_snr); + startTransmitTimerRebroadcast(p); } } @@ -57,11 +57,11 @@ void SimRadio::startTransmitTimer(bool withDelay) } } -void SimRadio::startTransmitTimerSNR(float snr) +void SimRadio::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { - uint32_t delayMsec = getTxDelayMsecWeighted(snr); + uint32_t delayMsec = getTxDelayMsecWeighted(p); // LOG_DEBUG("xmit timer %d", delay); notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); } diff --git a/src/platform/portduino/SimRadio.h b/src/platform/portduino/SimRadio.h index ea534bd65..d8b53739f 100644 --- a/src/platform/portduino/SimRadio.h +++ b/src/platform/portduino/SimRadio.h @@ -64,7 +64,7 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr void startTransmitTimer(bool withDelay = true); /** timer scaled to SNR of to be flooded packet */ - void startTransmitTimerSNR(float snr); + void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); void handleTransmitInterrupt(); void handleReceiveInterrupt(); From e2ce36978274b7d5661ee77ff34934de2b3932cc Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 19 Sep 2025 06:29:18 -0500 Subject: [PATCH 10/10] Fixes --- src/DisplayFormatters.h | 1 + src/mesh/Channels.cpp | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/DisplayFormatters.h b/src/DisplayFormatters.h index 2d7a3e8db..981010b33 100644 --- a/src/DisplayFormatters.h +++ b/src/DisplayFormatters.h @@ -6,4 +6,5 @@ class DisplayFormatters public: static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, bool usePreset); + static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role); }; diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 4c0a0edad..aec112a3e 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -428,7 +428,8 @@ bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) // Iterate all known presets for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; ++preset) { - const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false); + const char *name = + DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false, false); if (!name) continue; if (strcmp(name, "Invalid") == 0)