From e02866365856de8c623c645b69cf7b37e9417fe0 Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 9 Jun 2026 18:52:29 -0500 Subject: [PATCH] BaseUI: First attempt at Ham Mode implementation (#10663) * First attempt at Ham Mode implementation * Simplify licensedOnly check * Move related code closer together * TX Disabled if N0CALL, enabled if properly set * Only disable if callsign is N0CALL, don't enable at this stage. * Allow users back to Normal mode if they don't pick an ITU region --- src/graphics/draw/MenuHandler.cpp | 102 ++++++++++++++++++++++++------ src/graphics/draw/MenuHandler.h | 7 +- src/graphics/draw/UIRenderer.cpp | 13 ++-- src/modules/AdminModule.cpp | 4 ++ src/modules/AdminModule.h | 4 ++ 5 files changed, 105 insertions(+), 25 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 8403b3b7c..9157dc31d 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -126,6 +126,7 @@ void launchReplyForMessage(const StoredMessage &message, bool freetext) menuHandler::screenMenus menuHandler::menuQueue = MenuNone; uint32_t menuHandler::pickedNodeNum = 0; +meshtastic_Config_LoRaConfig_RegionCode menuHandler::pendingRegion = meshtastic_Config_LoRaConfig_RegionCode_UNSET; bool test_enabled = false; uint8_t test_count = 0; @@ -174,6 +175,36 @@ void menuHandler::OnboardMessage() screen->showOverlayBanner(bannerOptions); } +static void applyLoraRegion(meshtastic_Config_LoRaConfig_RegionCode region, bool isHam) +{ + config.lora.region = region; + config.lora.channel_num = 0; // Reset to default channel + + if (isHam && adminModule) { + meshtastic_HamParameters hamParams = meshtastic_HamParameters_init_zero; + strncpy(hamParams.call_sign, "N0CALL", sizeof(hamParams.call_sign) - 1); + strncpy(hamParams.short_name, "N0CL", sizeof(hamParams.short_name)); + hamParams.tx_power = config.lora.tx_power; + hamParams.frequency = config.lora.override_frequency; + adminModule->handleSetHamMode(hamParams); + } + auto changes = SEGMENT_CONFIG; +#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) + if (crypto) { + crypto->ensurePkiKeys(config.security, owner); + } +#endif + initRegion(); + if (getEffectiveDutyCycle() < 100) { + config.lora.ignore_mqtt = true; + } + if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { + sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); + changes |= SEGMENT_MODULECONFIG; + } + service->reloadConfig(changes); +} + void menuHandler::LoraRegionPicker(uint32_t duration) { static const LoraRegionOption regionOptions[] = { @@ -242,27 +273,20 @@ void menuHandler::LoraRegionPicker(uint32_t duration) return; } - config.lora.region = selectedRegion; - auto changes = SEGMENT_CONFIG; - -#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (crypto) { - crypto->ensurePkiKeys(config.security, owner); + bool hamMode = getRegion(selectedRegion)->profile->licensedOnly; + if (hamMode) { + LOG_INFO("User chose an amateur radio mode region"); + pendingRegion = selectedRegion; + menuQueue = HamModeConfirm; + screen->runNow(); + } else if (owner.is_licensed) { + LOG_INFO("Licensed user chose a non-ham region; prompting to revert licensed mode"); + pendingRegion = selectedRegion; + menuQueue = LicensedToNormalConfirm; + screen->runNow(); + } else { + applyLoraRegion(selectedRegion, false); } -#endif - config.lora.tx_enabled = true; - initRegion(); - if (getEffectiveDutyCycle() < 100) { - config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit - } - - if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { - // Default broker is in use, so subscribe to the appropriate MQTT root topic for this region - sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); - changes |= SEGMENT_MODULECONFIG; - } - - service->reloadConfig(changes); }); bannerOptions.durationMs = duration; @@ -279,6 +303,38 @@ void menuHandler::LoraRegionPicker(uint32_t duration) screen->showOverlayBanner(bannerOptions); } +void menuHandler::hamModeConfirmMenu() +{ + static const char *confirmOptions[] = {"No", "Yes"}; + BannerOverlayOptions confirmBanner; + confirmBanner.message = "I confirm I am a\nlicensed amateur\nradio operator"; + confirmBanner.optionsArrayPtr = confirmOptions; + confirmBanner.optionsCount = 2; + confirmBanner.bannerCallback = [](int selected) { + if (selected == 1) + applyLoraRegion(pendingRegion, true); + }; + screen->showOverlayBanner(confirmBanner); +} + +void menuHandler::licensedToNormalConfirmMenu() +{ + static const char *confirmOptions[] = {"Keep licensed", "Revert to Normal"}; + BannerOverlayOptions confirmBanner; + confirmBanner.message = "Revert licensed\nmode? This will\nre-enable encryption."; + confirmBanner.optionsArrayPtr = confirmOptions; + confirmBanner.optionsCount = 2; + confirmBanner.bannerCallback = [](int selected) { + if (selected == 1) { + owner.is_licensed = false; + config.lora.override_duty_cycle = false; + service->reloadOwner(false); + } + applyLoraRegion(pendingRegion, false); + }; + screen->showOverlayBanner(confirmBanner); +} + void menuHandler::deviceRolePicker() { static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"}; @@ -2822,6 +2878,12 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case ThemeMenu: themeMenu(); break; + case HamModeConfirm: + hamModeConfirmMenu(); + break; + case LicensedToNormalConfirm: + licensedToNormalConfirmMenu(); + break; } menuQueue = MenuNone; } diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 3ac9e606e..b924c9197 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -55,10 +55,13 @@ class menuHandler FrameToggles, DisplayUnits, MessageBubblesMenu, - ThemeMenu + ThemeMenu, + HamModeConfirm, + LicensedToNormalConfirm }; static screenMenus menuQueue; static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu + static meshtastic_Config_LoRaConfig_RegionCode pendingRegion; static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); @@ -111,6 +114,8 @@ class menuHandler static void messageBubblesMenu(); static void themeMenu(); static void textMessageMenu(); + static void hamModeConfirmMenu(); + static void licensedToNormalConfirmMenu(); private: static void saveUIConfig(); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 6f499483f..27bd8f8c2 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -1144,11 +1144,16 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta bool origBold = config.display.heading_bold; config.display.heading_bold = false; - // Display Region and Channel Utilization - if (currentResolution == ScreenResolution::UltraLow) { - drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + if (!config.lora.tx_enabled) { + const char *txdisabled = "Transmit Disabled"; + display->drawString(x, getTextPositions(display)[line], txdisabled); } else { - drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + // Display Region and Channel Utilization + if (currentResolution == ScreenResolution::UltraLow) { + drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + } else { + drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + } } char uptimeStr[32] = ""; if (currentResolution != ScreenResolution::UltraLow) { diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index ff72aad42..2ca5100d0 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -1463,6 +1463,10 @@ void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) } channels.onConfigChanged(); + if (strcmp(p.call_sign, "N0CALL") == 0) { + config.lora.tx_enabled = false; + } + service->reloadOwner(false); saveChanges(SEGMENT_CONFIG | SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); } diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index 5c690abbd..aeb41471c 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -67,7 +67,11 @@ class AdminModule : public ProtobufModule, public Obser private: bool handleSetModuleConfig(const meshtastic_ModuleConfig &c); void handleSetChannel(); + + public: void handleSetHamMode(const meshtastic_HamParameters &req); + + private: void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg); void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent); void reboot(int32_t seconds);