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
This commit is contained in:
Jason P
2026-06-09 19:52:29 -04:00
committed by GitHub
co-authored by GitHub
parent bf68b9e597
commit e028663658
5 changed files with 105 additions and 25 deletions
+82 -20
View File
@@ -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;
}
+6 -1
View File
@@ -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();
+9 -4
View File
@@ -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) {
+4
View File
@@ -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);
}
+4
View File
@@ -67,7 +67,11 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, 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);