From 8928ff8ae5fab1448ea101c868135c3ff4edbc46 Mon Sep 17 00:00:00 2001 From: Randall Hand Date: Sun, 5 Jul 2026 09:51:04 -0400 Subject: [PATCH] feat(lora): warn when selecting a licensed amateur-radio region (#3924) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a compliance warning to the LoRa configuration UI (both the device Configuration tab and the Remote Admin radio-config panel) when the selected RegionCode is one of the ITU 2m amateur-radio bands (ITU1_2M=27, ITU23_2M=28). Operating on these regions legally requires a valid amateur radio license, so the banner reminds the operator of their compliance responsibility — mirroring the amateur-band warning in the official Meshtastic mobile apps. New `isAmateurRadioRegion()` helper + `AMATEUR_RADIO_REGIONS` map in configuration/constants.ts, unit-tested in LoRaConfigSection.test.tsx. Note: firmware 2.8's region->preset legality map (used by the mobile apps to filter the preset picker) cannot be consumed yet because the vendored protobufs are at v2.7.26 and predate it. Per-preset legality filtering is deferred until the protobuf submodule is bumped to a 2.8 release; the helper's doc comment records the follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n --- public/locales/en.json | 1 + .../RadioConfigurationSection.tsx | 19 +++++++++- .../configuration/LoRaConfigSection.test.tsx | 36 ++++++++++++++++++- .../configuration/LoRaConfigSection.tsx | 19 +++++++++- src/components/configuration/constants.ts | 36 +++++++++++++++++++ 5 files changed, 108 insertions(+), 3 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index 4caf4d9f7..113490e76 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -3189,6 +3189,7 @@ "lora_config.override_frequency_description": "HAM/Advanced: Override channel calculation and use this frequency. Frequency offset still applies. Value of 0 = disabled", "lora_config.region": "Region Code", "lora_config.region_description": "Select your regulatory region for frequency/power limits", + "lora_config.amateur_band_warning": "This region operates in a licensed amateur (ham) radio band. A valid amateur radio license is required, and you are responsible for complying with local band-plan, power, and station identification rules.", "lora_config.hop_limit": "Hop Limit", "lora_config.hop_limit_description": "Maximum number of hops for mesh packets. Range: 1-7 (default: 3)", "lora_config.tx_power": "Transmit Power (dBm)", diff --git a/src/components/admin-commands/RadioConfigurationSection.tsx b/src/components/admin-commands/RadioConfigurationSection.tsx index 2a86b164f..33428a5d7 100644 --- a/src/components/admin-commands/RadioConfigurationSection.tsx +++ b/src/components/admin-commands/RadioConfigurationSection.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { MODEM_PRESET_OPTIONS, REGION_OPTIONS } from '../configuration/constants'; +import { MODEM_PRESET_OPTIONS, REGION_OPTIONS, isAmateurRadioRegion } from '../configuration/constants'; import type { Channel } from '../../types/device'; interface RadioConfigurationSectionProps { @@ -214,6 +214,23 @@ export const RadioConfigurationSection: React.FC ))} + {isAmateurRadioRegion(region) && ( +
+ ⚠️ {t('lora_config.amateur_band_warning')} +
+ )}
diff --git a/src/components/configuration/LoRaConfigSection.test.tsx b/src/components/configuration/LoRaConfigSection.test.tsx index a4f6e8218..ae9b71c68 100644 --- a/src/components/configuration/LoRaConfigSection.test.tsx +++ b/src/components/configuration/LoRaConfigSection.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { MODEM_PRESET_OPTIONS, FEM_LNA_MODE_OPTIONS } from './constants'; +import { MODEM_PRESET_OPTIONS, FEM_LNA_MODE_OPTIONS, AMATEUR_RADIO_REGIONS, isAmateurRadioRegion, REGION_MAP } from './constants'; /** * LoRaConfigSection Tests @@ -137,6 +137,40 @@ describe('LoRaConfigSection', () => { }); }); + describe('Amateur Radio Region Warning (#3924)', () => { + it('flags the ITU 2m amateur regions (27, 28) as amateur bands', () => { + expect(isAmateurRadioRegion(27)).toBe(true); // ITU1_2M + expect(isAmateurRadioRegion(28)).toBe(true); // ITU23_2M + }); + + it('keeps the amateur region set in sync with REGION_MAP values', () => { + expect(AMATEUR_RADIO_REGIONS[27]).toBe('ITU1_2M'); + expect(AMATEUR_RADIO_REGIONS[28]).toBe('ITU23_2M'); + expect(REGION_MAP['ITU1_2M']).toBe(27); + expect(REGION_MAP['ITU23_2M']).toBe(28); + }); + + it('does not flag standard ISM/SRD regions as amateur bands', () => { + expect(isAmateurRadioRegion(1)).toBe(false); // US + expect(isAmateurRadioRegion(3)).toBe(false); // EU_868 + expect(isAmateurRadioRegion(6)).toBe(false); // ANZ + expect(isAmateurRadioRegion(32)).toBe(false); // EU_N_868 + }); + + it('does not flag UNSET (0) as an amateur band', () => { + expect(isAmateurRadioRegion(0)).toBe(false); + }); + + it('handles null/undefined region without throwing', () => { + expect(isAmateurRadioRegion(null)).toBe(false); + expect(isAmateurRadioRegion(undefined)).toBe(false); + }); + + it('does not flag an out-of-range region value', () => { + expect(isAmateurRadioRegion(999)).toBe(false); + }); + }); + describe('Save Configuration Logic', () => { it('includes femLnaMode in the LoRa save payload', () => { // Mirrors the Device Configuration save (ConfigurationTab) and Remote Admin diff --git a/src/components/configuration/LoRaConfigSection.tsx b/src/components/configuration/LoRaConfigSection.tsx index 47dd2ea1b..f47fadcb8 100644 --- a/src/components/configuration/LoRaConfigSection.tsx +++ b/src/components/configuration/LoRaConfigSection.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useMemo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { MODEM_PRESET_OPTIONS, REGION_OPTIONS, FEM_LNA_MODE_OPTIONS } from './constants'; +import { MODEM_PRESET_OPTIONS, REGION_OPTIONS, FEM_LNA_MODE_OPTIONS, isAmateurRadioRegion } from './constants'; import { useSaveBar } from '../../hooks/useSaveBar'; interface LoRaConfigSectionProps { @@ -387,6 +387,23 @@ const LoRaConfigSection: React.FC = ({ ))} + {isAmateurRadioRegion(region) && ( +
+ ⚠️ {t('lora_config.amateur_band_warning')} +
+ )}