From f06f808391baa4caf76ff3b96ca26205ce6725cd Mon Sep 17 00:00:00 2001 From: Jason P Date: Wed, 12 Aug 2026 21:59:26 +0000 Subject: [PATCH] Add Transmit Enabled menu to LoRa frame (#11442) * Add Transmit Enabled menu to LoRa frame * Fix outer variable shadows --- src/graphics/draw/DebugRenderer.cpp | 134 +++++++++++++++------------- src/graphics/draw/MenuHandler.cpp | 40 ++++++++- src/graphics/draw/MenuHandler.h | 2 + 3 files changed, 110 insertions(+), 66 deletions(-) diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index b50c7081c..3227ed604 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -224,75 +224,83 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, #if !defined(OLED_TINY) // === Fifth Row: Channel Utilization === - const char *chUtil = "ChUtil:"; - char chUtilPercentage[10]; - snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - - int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 - : display->getStringWidth(chUtil) + 5; - int chUtil_y = getTextPositions(display)[line] + 3; - - int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; - int chutil_bar_max_fill = chutil_bar_width - 2; // Account for border - int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; - int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; - int chutil_percent = airTime->channelUtilizationPercent(); - const int raw_chutil_percent = chutil_percent; - - int centerofscreen = SCREEN_WIDTH / 2; - int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; - int starting_position = centerofscreen - total_line_content_width; - - display->drawString(starting_position, getTextPositions(display)[line], chUtil); - - // Force 61% or higher to show a full 100% bar, text would still show related percent. - if (chutil_percent >= 61) { - chutil_percent = 100; - } - - // Weighting for nonlinear segments - float milestone1 = 25; - float milestone2 = 40; - float weight1 = 0.45; // Weight for 0-25% - float weight2 = 0.35; // Weight for 25-40% - float weight3 = 0.20; // Weight for 40-100% - float totalWeight = weight1 + weight2 + weight3; - - int seg1 = chutil_bar_max_fill * (weight1 / totalWeight); - int seg2 = chutil_bar_max_fill * (weight2 / totalWeight); - int seg3 = chutil_bar_max_fill - seg1 - seg2; // Remainder absorbs rounding errors - - int fillRight = 0; - - if (chutil_percent <= milestone1) { - fillRight = (seg1 * (chutil_percent / milestone1)); - } else if (chutil_percent <= milestone2) { - fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); + if (!config.lora.tx_enabled) { + const char *txdisabled = "Transmit Disabled"; + textWidth = display->getStringWidth(txdisabled); + display->drawString((SCREEN_WIDTH - textWidth) / 2, getTextPositions(display)[line], txdisabled); } else { - fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); - } - // Draw outline - display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + const char *chUtil = "ChUtil:"; + char chUtilPercentage[10]; + snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - // Fill progress - if (fillRight > 0) { -#if GRAPHICS_TFT_COLORING_ENABLED - uint16_t UtilizationFillColor = TFTPalette::Good; - if (raw_chutil_percent >= 60) { - UtilizationFillColor = TFTPalette::Bad; - } else if (raw_chutil_percent >= 35) { - UtilizationFillColor = TFTPalette::Medium; + int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 + : display->getStringWidth(chUtil) + 5; + int chUtil_y = getTextPositions(display)[line] + 3; + + int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; + int chutil_bar_max_fill = chutil_bar_width - 2; // Account for border + int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; + int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; + int chutil_percent = airTime->channelUtilizationPercent(); + const int raw_chutil_percent = chutil_percent; + + int centerofscreen = SCREEN_WIDTH / 2; + int total_line_content_width = + (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; + int starting_position = centerofscreen - total_line_content_width; + + display->drawString(starting_position, getTextPositions(display)[line], chUtil); + + // Force 61% or higher to show a full 100% bar, text would still show related percent. + if (chutil_percent >= 61) { + chutil_percent = 100; } - setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black, - starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2); -#endif - display->fillRect(starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2); - } - display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++], - chUtilPercentage); + // Weighting for nonlinear segments + float milestone1 = 25; + float milestone2 = 40; + float weight1 = 0.45; // Weight for 0-25% + float weight2 = 0.35; // Weight for 25-40% + float weight3 = 0.20; // Weight for 40-100% + float totalWeight = weight1 + weight2 + weight3; + + int seg1 = chutil_bar_max_fill * (weight1 / totalWeight); + int seg2 = chutil_bar_max_fill * (weight2 / totalWeight); + int seg3 = chutil_bar_max_fill - seg1 - seg2; // Remainder absorbs rounding errors + + int fillRight = 0; + + if (chutil_percent <= milestone1) { + fillRight = (seg1 * (chutil_percent / milestone1)); + } else if (chutil_percent <= milestone2) { + fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); + } else { + fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); + } + + // Draw outline + display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + + // Fill progress + if (fillRight > 0) { +#if GRAPHICS_TFT_COLORING_ENABLED + uint16_t UtilizationFillColor = TFTPalette::Good; + if (raw_chutil_percent >= 60) { + UtilizationFillColor = TFTPalette::Bad; + } else if (raw_chutil_percent >= 35) { + UtilizationFillColor = TFTPalette::Medium; + } + setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black, + starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2); #endif + display->fillRect(starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2); + } + + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++], + chUtilPercentage); +#endif + } graphics::drawCommonFooter(display, x, y); } diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index f5cd21e1a..6471d70c6 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -148,27 +148,31 @@ void menuHandler::loraMenu() "Radio Preset", "Frequency Slot", "LoRa Region", + "Transmit Enabled", #if HAS_LORA_FEM "FEM LNA", #endif }; + // NOTE: "FEM LNA" must stay last; it is the only entry that can be hidden at runtime by + // trimming optionsCount, which only works for a trailing option. enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4, + TxEnabled = 5, #if HAS_LORA_FEM - LoraFemLna = 5 + LoraFemLna = 6 #endif }; BannerOverlayOptions bannerOptions; bannerOptions.message = "LoRa Actions"; bannerOptions.optionsArrayPtr = optionsArray; #if HAS_LORA_FEM - bannerOptions.optionsCount = loraFEMInterface.isLnaCanControl() ? 6 : 5; + bannerOptions.optionsCount = loraFEMInterface.isLnaCanControl() ? 7 : 6; #else - bannerOptions.optionsCount = 5; + bannerOptions.optionsCount = 6; #endif bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { @@ -181,6 +185,8 @@ void menuHandler::loraMenu() menuHandler::menuQueue = menuHandler::FrequencySlot; } else if (selected == LoraPicker) { menuHandler::menuQueue = menuHandler::LoraPicker; + } else if (selected == TxEnabled) { + menuHandler::menuQueue = menuHandler::TXEnabledMenu; } #if HAS_LORA_FEM else if (selected == LoraFemLna) { @@ -571,6 +577,31 @@ void menuHandler::radioPresetPicker() screen->showOverlayBanner(buildRegionPresetBanner()); } +void menuHandler::txEnabledMenu() +{ + static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; + enum optionsNumbers { Back = 0, Enabled = 1, Disabled = 2 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Transmit Enabled"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.InitialSelected = config.lora.tx_enabled ? Enabled : Disabled; + bannerOptions.bannerCallback = [](int selected) -> void { + // -1 is the timeout/dismiss case; treat it like Back so we never write config. + if (selected <= Back) { + menuHandler::menuQueue = menuHandler::LoraMenu; + screen->runNow(); + return; + } + bool wanted = (selected == Enabled); + if (config.lora.tx_enabled == wanted) + return; + config.lora.tx_enabled = wanted; + service->reloadConfig(SEGMENT_CONFIG); + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::twelveHourPicker() { static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; @@ -2943,6 +2974,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case RadioPresetPicker: radioPresetPicker(); break; + case TXEnabledMenu: + txEnabledMenu(); + break; case FrequencySlot: FrequencySlotPicker(); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 311205e0a..965093223 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -13,6 +13,7 @@ class menuHandler LoraPicker, DeviceRolePicker, RadioPresetPicker, + TXEnabledMenu, FrequencySlot, NoTimeoutLoraPicker, TzPicker, @@ -73,6 +74,7 @@ class menuHandler static void loraMenu(); static void deviceRolePicker(); static void radioPresetPicker(); + static void txEnabledMenu(); static void FrequencySlotPicker(); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm);