Lora settings expansion and validation logic improvement (#9878)

* Enhance LoRa configuration with modem presets and validation logic

* Rename bootstrapLoRaConfigFromPreset tests to validateModemConfig for clarity and consistency

* additional tidy-ups to the validateModemConfig - still fundamentally broken at this point

* Enhance region validation by adding numPresets to RegionInfo and implementing validateRegionConfig in RadioInterface

* Add validation for modem configuration in applyModemConfig

* Fix region unset handling and improve modem config validation in handleSetConfig

* Refactor LoRa configuration validation methods and introduce clamping method for invalid settings

* Update handleSetConfig to use fromOthers parameter to either correct or reject invalid settings

* Fix some of the copilot review comments for LoRa configuration validation and clamping methods; add tests for region and preset handling

* Redid the slot default checking and calculation. Should resolve the outstanding comments.

* Add bandwidth calculation for LoRa modem preset fallback in clampConfigLora

* Remove unused preset name variable in validateConfigLora and fix default frequency slot check in applyModemConfig

* update tests for region handling

* Got the synthetic colleague to add mock service for testing

* Flash savings... hopefully

* Refactor modem preset handling to use sentinel values and improve default preset retrieval

* Refactor region handling to use profile structures for modem presets and channel calculations

* added comments for clarity on parameters

* Add shadow table tests and validateConfigLora enhancements for region presets

* Add isFromUs tests for preset validation in AdminModule

* Respond to copilot github review

* address copilot comments

* address null poointers

* fix build errors

* Fix the fix, undo the silly suggestions from synthetic reviewer.

* we all float here

* Fix include path for AdminModule in test_main.cpp

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* More suggestion fixes

* admin module merge conflicts

* admin module fixes from merge hell

* fix: initialize default frequency slot and custom channel name; update LNA mode handling

* save some bytes...

* fix: simplify error logging for bandwidth checks in LoRa configuration

* Update src/mesh/MeshRadio.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Tom
2026-03-20 07:43:47 -05:00
committed by GitHub
co-authored by GitHub Copilot Autofix powered by AI
parent fc030d2d19
commit 22d63fa69c
11 changed files with 1355 additions and 252 deletions
+307 -134
View File
@@ -21,6 +21,7 @@
#include <assert.h>
#include <pb_decode.h>
#include <pb_encode.h>
#include <string.h>
#ifdef ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h"
@@ -32,10 +33,32 @@
#include "STM32WLE5JCInterface.h"
#endif
#define RDEF(name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, wide_lora) \
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_STD[] = {
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, MODEM_PRESET_END};
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_868[] = {
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, MODEM_PRESET_END};
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_UNDEF[] = {meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
MODEM_PRESET_END};
// Region profiles: bundle preset list + regulatory parameters shared across regions
// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle, override slot
const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 0, 0, 0};
const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 0, 0, 0};
const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 0, 0, 0};
#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr) \
{ \
meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, \
frequency_switching, wide_lora, #name \
meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, \
wide_lora, &profile_ptr, #name \
}
const RegionInfo regions[] = {
@@ -43,7 +66,7 @@ const RegionInfo regions[] = {
https://link.springer.com/content/pdf/bbm%3A978-1-4842-4357-2%2F1.pdf
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
*/
RDEF(US, 902.0f, 928.0f, 100, 0, 30, true, false, false),
RDEF(US, 902.0f, 928.0f, 100, 30, false, false, PROFILE_STD),
/*
EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21]
@@ -51,8 +74,7 @@ const RegionInfo regions[] = {
https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.02.01_60/en_30022002v030201p.pdf
FIXME: https://github.com/meshtastic/firmware/issues/3371
*/
RDEF(EU_433, 433.0f, 434.0f, 10, 0, 10, true, false, false),
RDEF(EU_433, 433.0f, 434.0f, 10, 10, false, false, PROFILE_STD),
/*
https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
@@ -67,33 +89,33 @@ const RegionInfo regions[] = {
AFA) to avoid a duty cycle. (Please refer to line P page 22 of this document.)
https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.01.01_60/en_30022002v030101p.pdf
*/
RDEF(EU_868, 869.4f, 869.65f, 10, 0, 27, false, false, false),
RDEF(EU_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_EU868),
/*
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
*/
RDEF(CN, 470.0f, 510.0f, 100, 0, 19, true, false, false),
RDEF(CN, 470.0f, 510.0f, 100, 19, false, false, PROFILE_STD),
/*
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
https://www.arib.or.jp/english/html/overview/doc/5-STD-T108v1_5-E1.pdf
https://qiita.com/ammo0613/items/d952154f1195b64dc29f
*/
RDEF(JP, 920.5f, 923.5f, 100, 0, 13, true, false, false),
RDEF(JP, 920.5f, 923.5f, 100, 13, false, false, PROFILE_STD),
/*
https://www.iot.org.au/wp/wp-content/uploads/2016/12/IoTSpectrumFactSheet.pdf
https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf
Also used in Brazil.
*/
RDEF(ANZ, 915.0f, 928.0f, 100, 0, 30, true, false, false),
RDEF(ANZ, 915.0f, 928.0f, 100, 30, false, false, PROFILE_STD),
/*
433.05 - 434.79 MHz, 25mW EIRP max, No duty cycle restrictions
AU Low Interference Potential https://www.acma.gov.au/licences/low-interference-potential-devices-lipd-class-licence
NZ General User Radio Licence for Short Range Devices https://gazette.govt.nz/notice/id/2022-go3100
*/
RDEF(ANZ_433, 433.05f, 434.79f, 100, 0, 14, true, false, false),
RDEF(ANZ_433, 433.05f, 434.79f, 100, 14, false, false, PROFILE_STD),
/*
https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf
@@ -101,13 +123,13 @@ const RegionInfo regions[] = {
Note:
- We do LBT, so 100% is allowed.
*/
RDEF(RU, 868.7f, 869.2f, 100, 0, 20, true, false, false),
RDEF(RU, 868.7f, 869.2f, 100, 20, false, false, PROFILE_STD),
/*
https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulId=53943&efYd=0
https://resources.lora-alliance.org/technical-specifications/rp002-1-0-4-regional-parameters
*/
RDEF(KR, 920.0f, 923.0f, 100, 0, 23, true, false, false),
RDEF(KR, 920.0f, 923.0f, 100, 23, false, false, PROFILE_STD),
/*
Taiwan, 920-925Mhz, limited to 0.5W indoor or coastal, 1.0W outdoor.
@@ -115,42 +137,38 @@ const RegionInfo regions[] = {
https://www.ncc.gov.tw/english/files/23070/102_5190_230703_1_doc_C.PDF
https://gazette.nat.gov.tw/egFront/e_detail.do?metaid=147283
*/
RDEF(TW, 920.0f, 925.0f, 100, 0, 27, true, false, false),
RDEF(TW, 920.0f, 925.0f, 100, 27, false, false, PROFILE_STD),
/*
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
*/
RDEF(IN, 865.0f, 867.0f, 100, 0, 30, true, false, false),
RDEF(IN, 865.0f, 867.0f, 100, 30, false, false, PROFILE_STD),
/*
https://rrf.rsm.govt.nz/smart-web/smart/page/-smart/domain/licence/LicenceSummary.wdk?id=219752
https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf
*/
RDEF(NZ_865, 864.0f, 868.0f, 100, 0, 36, true, false, false),
RDEF(NZ_865, 864.0f, 868.0f, 100, 36, false, false, PROFILE_STD),
/*
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
*/
RDEF(TH, 920.0f, 925.0f, 100, 0, 16, true, false, false),
RDEF(TH, 920.0f, 925.0f, 100, 16, false, false, PROFILE_STD),
/*
433,05-434,7 Mhz 10 mW
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
*/
RDEF(UA_433, 433.0f, 434.7f, 10, 0, 10, true, false, false),
/*
868,0-868,6 Mhz 25 mW
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
*/
RDEF(UA_868, 868.0f, 868.6f, 1, 0, 14, true, false, false),
RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD),
RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD),
/*
Malaysia
433 - 435 MHz at 100mW, no restrictions.
https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf
*/
RDEF(MY_433, 433.0f, 435.0f, 100, 0, 20, true, false, false),
RDEF(MY_433, 433.0f, 435.0f, 100, 20, false, false, PROFILE_STD),
/*
Malaysia
@@ -159,14 +177,14 @@ const RegionInfo regions[] = {
Frequency hopping is used for 919 - 923 MHz.
https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf
*/
RDEF(MY_919, 919.0f, 924.0f, 100, 0, 27, true, true, false),
RDEF(MY_919, 919.0f, 924.0f, 100, 27, true, false, PROFILE_STD),
/*
Singapore
SG_923 Band 30d: 917 - 925 MHz at 100mW, no restrictions.
https://www.imda.gov.sg/-/media/imda/files/regulation-licensing-and-consultations/ict-standards/telecommunication-standards/radio-comms/imdatssrd.pdf
*/
RDEF(SG_923, 917.0f, 925.0f, 100, 0, 20, true, false, false),
RDEF(SG_923, 917.0f, 925.0f, 100, 20, false, false, PROFILE_STD),
/*
Philippines
@@ -176,8 +194,9 @@ const RegionInfo regions[] = {
https://github.com/meshtastic/firmware/issues/4948#issuecomment-2394926135
*/
RDEF(PH_433, 433.0f, 434.7f, 100, 0, 10, true, false, false), RDEF(PH_868, 868.0f, 869.4f, 100, 0, 14, true, false, false),
RDEF(PH_915, 915.0f, 918.0f, 100, 0, 24, true, false, false),
RDEF(PH_433, 433.0f, 434.7f, 100, 10, false, false, PROFILE_STD),
RDEF(PH_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD),
RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD),
/*
Kazakhstan
@@ -185,37 +204,38 @@ const RegionInfo regions[] = {
863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields
https://github.com/meshtastic/firmware/issues/7204
*/
RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false),
RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, false),
RDEF(KZ_433, 433.075f, 434.775f, 100, 10, false, false, PROFILE_STD),
RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD),
/*
Nepal
865MHz to 868MHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use,
specifically in non-cellular mode. https://www.nta.gov.np/uploads/contents/Radio-Frequency-Policy-2080-English.pdf
*/
RDEF(NP_865, 865.0f, 868.0f, 100, 0, 30, true, false, false),
RDEF(NP_865, 865.0f, 868.0f, 100, 30, false, false, PROFILE_STD),
/*
Brazil
902 - 907.5 MHz , 1W power limit, no duty cycle restrictions
https://github.com/meshtastic/firmware/issues/3741
*/
RDEF(BR_902, 902.0f, 907.5f, 100, 0, 30, true, false, false),
RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD),
/*
2.4 GHZ WLAN Band equivalent. Only for SX128x chips.
*/
RDEF(LORA_24, 2400.0f, 2483.5f, 100, 0, 10, true, false, true),
RDEF(LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, PROFILE_STD),
/*
This needs to be last. Same as US.
*/
RDEF(UNSET, 902.0f, 928.0f, 100, 0, 30, true, false, false)
RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF)
};
const RegionInfo *myRegion;
bool RadioInterface::uses_default_frequency_slot = true;
bool RadioInterface::uses_custom_channel_name = false;
static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1];
@@ -501,45 +521,14 @@ void initRegion()
myRegion = r;
}
void RadioInterface::bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig)
const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code)
{
if (!loraConfig.use_preset) {
return;
}
// Find region info to determine whether "wide" LoRa is permitted (2.4 GHz uses wider bandwidth codes).
const RegionInfo *r = regions;
for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != loraConfig.region; r++)
for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != code; r++)
;
const bool regionWideLora = r->wideLora;
float bwKHz = 0;
uint8_t sf = 0;
uint8_t cr = 0;
modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr);
// If selected preset requests a bandwidth larger than the region span, fall back to LONG_FAST.
if (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && (r->freqEnd - r->freqStart) < (bwKHz / 1000.0f)) {
loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr);
}
loraConfig.bandwidth = bwKHzToCode(bwKHz);
loraConfig.spread_factor = sf;
return r;
}
/**
* ## LoRaWAN for North America
LoRaWAN defines 64, 125 kHz channels from 902.3 to 914.9 MHz increments.
The maximum output power for North America is +30 dBM.
The band is from 902 to 928 MHz. It mentions channel number and its respective channel frequency. All the 13 channels are
separated by 2.16 MHz with respect to the adjacent channels. Channel zero starts at 903.08 MHz center frequency.
*/
uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received)
{
uint32_t pl = 0;
@@ -749,7 +738,7 @@ void RadioInterface::saveFreq(float freq)
}
/**
* Save our channel for later reuse.
* Save our frequency slot (aka channel) for later reuse.
*/
void RadioInterface::saveChannelNum(uint32_t channel_num)
{
@@ -772,113 +761,297 @@ uint32_t RadioInterface::getChannelNum()
return savedChannelNum;
}
/**
* Send an error-level client notification. Safe to call when service is null (e.g. in tests).
*/
static void sendErrorNotification(const char *msg)
{
if (!service)
return;
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
if (!cn)
return;
cn->level = meshtastic_LogRecord_Level_ERROR;
snprintf(cn->message, sizeof(cn->message), "%s", msg);
service->sendClientNotification(cn);
}
/**
* Checks if a region is valid for the current settings.
* Returns false if not compatible.
*/
bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig)
{
const RegionInfo *newRegion = getRegion(loraConfig.region);
// If you are not licensed, you can't use ham regions.
if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed) {
char err_string[160];
snprintf(err_string, sizeof(err_string), "Region %s requires licensed mode", newRegion->name);
LOG_ERROR("%s", err_string);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
sendErrorNotification(err_string);
return false;
}
return true;
}
/**
* Internal helper: validate or clamp a LoRa config against its region.
* When clamp==false, returns false on first error (pure validation).
* When clamp==true, fixes invalid settings in-place and returns true.
*/
bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp)
{
char err_string[160];
float check_bw;
const RegionInfo *newRegion = getRegion(loraConfig.region);
const char *presetName = DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset);
// Check preset validity (only when use_preset is true)
if (loraConfig.use_preset) {
check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora);
bool preset_valid = false;
for (size_t i = 0; i < newRegion->getNumPresets(); i++) {
if (loraConfig.modem_preset == newRegion->getAvailablePresets()[i]) {
preset_valid = true;
break;
}
}
if (!preset_valid) {
const char *defaultName = DisplayFormatters::getModemPresetDisplayName(newRegion->getDefaultPreset(), false, true);
if (clamp) {
snprintf(err_string, sizeof(err_string), "Preset %s invalid for %s, using %s", presetName, newRegion->name,
defaultName);
} else {
snprintf(err_string, sizeof(err_string), "Preset %s invalid for %s", presetName, newRegion->name);
}
LOG_ERROR("%s", err_string);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
sendErrorNotification(err_string);
if (clamp) {
loraConfig.modem_preset = newRegion->getDefaultPreset();
check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora);
} else {
return false;
}
}
} else {
check_bw = bwCodeToKHz(loraConfig.bandwidth);
}
// Calculate width of slots (aka channels) based on bandwidth and any spacing or padding required by the region:
// spacing = gap between slots (0 for continuous spectrum) and at the beginning of the band
// padding = gap at the beginning and end of the slots (0 for no padding)
float freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (check_bw / 1000); // in MHz
uint32_t numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth);
// Check if the region supports the requested bandwidth
if ((newRegion->freqEnd - newRegion->freqStart) < freqSlotWidth) {
const float regionSpanKHz = (newRegion->freqEnd - newRegion->freqStart) * 1000.0f;
snprintf(err_string, sizeof(err_string), "%s span %.0fkHz < requested %.0fkHz", newRegion->name, regionSpanKHz, check_bw);
LOG_ERROR("%s", err_string);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
sendErrorNotification(err_string);
if (clamp) {
loraConfig.bandwidth = bwKHzToCode(modemPresetToBwKHz(newRegion->getDefaultPreset(), newRegion->wideLora));
check_bw = bwCodeToKHz(loraConfig.bandwidth);
// Recompute slot width and number of slots based on the new bandwidth
freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (check_bw / 1000); // in MHz
numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth);
} else {
return false;
}
}
const char *channelName = channels.getName(channels.getPrimaryIndex());
const char *presetNameDisplay =
DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset);
uint32_t channelNameHashSlot = hash(channelName) % numFreqSlots;
uint32_t presetNameHashSlot = hash(presetNameDisplay) % numFreqSlots;
if (loraConfig.override_frequency == 0) {
// Check if we use the default frequency slot
uses_default_frequency_slot =
(loraConfig.channel_num == 0) || // user choice unset, no frequency override, so use default
(newRegion->profile->overrideSlot != 0 &&
loraConfig.channel_num == newRegion->profile->overrideSlot) || // user setting matches override
((newRegion->profile->overrideSlot == 0) &&
((uint32_t)(loraConfig.channel_num - 1) == presetNameHashSlot)); // user setting matches preset hash, no override
// check if user setting different to preset name
uses_custom_channel_name = (strcmp(channelName, presetNameDisplay) != 0);
if (loraConfig.channel_num > numFreqSlots) {
snprintf(err_string, sizeof(err_string), "Channel number %u invalid for %s, max is %u", loraConfig.channel_num,
newRegion->name, numFreqSlots);
LOG_ERROR("%s", err_string);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
sendErrorNotification(err_string);
if (clamp) {
if (uses_custom_channel_name) { // clamp to channel name hash
loraConfig.channel_num =
channelNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
} else if ((loraConfig.use_preset) && (newRegion->profile->overrideSlot != 0)) { // clamp to preset override slot
loraConfig.channel_num =
newRegion->profile->overrideSlot; // use the override slot specified by the region profile
uses_default_frequency_slot = true;
} else if (loraConfig.use_preset) { // clamp to preset slot
loraConfig.channel_num = presetNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
uses_default_frequency_slot = true;
} else { // if not using preset, and no custom channel name, just clamp to default anyway
uses_default_frequency_slot = true;
};
} else {
return false;
}
} // end of channel number check
} else {
// if we have a frequency override, we ignore the channel number and just use the override frequency
snprintf(err_string, sizeof(err_string), "Frequency override in place, using %.3f", loraConfig.override_frequency);
}
return true;
}
bool RadioInterface::validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig)
{
auto copy = loraConfig;
return checkOrClampConfigLora(copy, false);
}
void RadioInterface::clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig)
{
checkOrClampConfigLora(loraConfig, true);
}
/**
* Pull our channel settings etc... from protobufs to the dumb interface settings
* Note: this must be given only settings which have been validated or clamped!
*/
void RadioInterface::applyModemConfig()
{
// Set up default configuration
// No Sync Words in LORA mode
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
bool validConfig = false; // We need to check for a valid configuration
while (!validConfig) {
if (loraConfig.use_preset) {
modemPresetToParams(loraConfig.modem_preset, myRegion->wideLora, bw, sf, cr);
if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) {
cr = loraConfig.coding_rate;
LOG_INFO("Using custom Coding Rate %u", cr);
}
} else {
sf = loraConfig.spread_factor;
const RegionInfo *newRegion = getRegion(loraConfig.region);
myRegion = newRegion;
if (loraConfig.use_preset) {
if (!validateConfigLora(loraConfig)) {
loraConfig.modem_preset = newRegion->getDefaultPreset();
}
uint8_t newcr;
modemPresetToParams(loraConfig.modem_preset, newRegion->wideLora, bw, sf, newcr);
// If custom CR is being used already, check if the new preset is higher
if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate < newcr) {
cr = newcr;
LOG_INFO("Default Coding Rate is higher than custom setting, using %u", cr);
}
// If the custom CR is higher than the preset, use it
else if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate > newcr) {
cr = loraConfig.coding_rate;
bw = bwCodeToKHz(loraConfig.bandwidth);
}
if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) {
const float regionSpanKHz = (myRegion->freqEnd - myRegion->freqStart) * 1000.0f;
const float requestedBwKHz = bw;
const bool isWideRequest = requestedBwKHz >= 499.5f; // treat as 500 kHz preset
const char *presetName =
DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset);
char err_string[160];
if (isWideRequest) {
snprintf(err_string, sizeof(err_string), "%s region too narrow for 500kHz preset (%s). Falling back to LongFast.",
myRegion->name, presetName);
} else {
snprintf(err_string, sizeof(err_string), "%s region span %.0fkHz < requested %.0fkHz. Falling back to LongFast.",
myRegion->name, regionSpanKHz, requestedBwKHz);
}
LOG_ERROR("%s", err_string);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_ERROR;
snprintf(cn->message, sizeof(cn->message), "%s", err_string);
service->sendClientNotification(cn);
// Set to default modem preset
loraConfig.use_preset = true;
loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
LOG_INFO("Using custom Coding Rate %u", cr);
} else {
validConfig = true;
cr = loraConfig.coding_rate;
}
} else { // if not using preset, then just use the custom settings
if (validateConfigLora(loraConfig)) {
} else {
LOG_WARN("Invalid LoRa config settings, cannot apply requested modem config - falling back to %s defaults",
newRegion->name);
clampConfigLora(loraConfig);
}
bw = bwCodeToKHz(loraConfig.bandwidth);
sf = loraConfig.spread_factor;
cr = loraConfig.coding_rate;
}
power = loraConfig.tx_power;
if ((power == 0) || ((power > myRegion->powerLimit) && !devicestate.owner.is_licensed))
power = myRegion->powerLimit;
if ((power == 0) || ((power > newRegion->powerLimit) && !devicestate.owner.is_licensed))
power = newRegion->powerLimit;
if (power == 0)
power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of myRegion defaults
power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of newRegion defaults
// to 0, currently no region has an actual power limit of 0 [dBm] so we can assume regions which have this
// variable set to 0 don't have a valid power limit)
// Set final tx_power back onto config
loraConfig.tx_power = (int8_t)power; // cppcheck-suppress assignmentAddressToInteger
// Calculate the number of channels
uint32_t numChannels = floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000)));
uint32_t channel_num;
float freq;
// If user has manually specified a channel num, then use that, otherwise generate one by hashing the name
const char *channelName = channels.getName(channels.getPrimaryIndex());
// channel_num is actually (channel_num - 1), since modulus (%) returns values from 0 to (numChannels - 1)
uint32_t channel_num = (loraConfig.channel_num ? loraConfig.channel_num - 1 : hash(channelName)) % numChannels;
// Calculate number of frequency slots (aka Channels):
// spacing = gap between channels (0 for continuous spectrum) and at the beginning of the band
// padding = gap at the beginning and end of the channel (0 for no padding)
float freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (bw / 1000); // in MHz
uint32_t numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth);
// Check if we use the default frequency slot
RadioInterface::uses_default_frequency_slot =
channel_num ==
hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels;
// Old frequency selection formula
// float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num);
// New frequency selection formula
float freq = myRegion->freqStart + (bw / 2000) + (channel_num * (bw / 1000));
// Calculate hash of channel name and preset name to pick a default frequency slot if user has not specified one.
// Note that channel_num is actually (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to
// (numFreqSlots - 1).
uint32_t presetNameHashSlot =
hash(DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset)) % numFreqSlots;
// override if we have a verbatim frequency
if (loraConfig.override_frequency) {
freq = loraConfig.override_frequency;
channel_num = -1;
uses_default_frequency_slot = false;
} else {
// If user has not manually specified a frequency slot, or has not specified one that is different than the default or the
// override for the new region, then use the default or override. If the user has not specified one, but has specified a
// custom channel name, then use the hash of that channel name to pick a frequency slot. Note that channel_num is actually
// (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to (numFreqSlots - 1).
// NB: channel_num is also know as frequency slot but it's too late to fix now.
if (uses_default_frequency_slot) {
// if there's an override slot, use that
if (newRegion->profile->overrideSlot != 0) {
channel_num = newRegion->profile->overrideSlot - 1;
} else {
channel_num = presetNameHashSlot;
}
} else { // use the manually defined one
channel_num = loraConfig.channel_num - 1;
}
// Calculate frequency: freqStart is band edge, add half bandwidth (plus optional padding) to get middle of first channel
// subsequent channels are spaced by freqSlotWidth
freq = newRegion->freqStart + (bw / 2000) + newRegion->profile->padding + (channel_num * freqSlotWidth); // in MHz
}
saveChannelNum(channel_num);
saveFreq(freq + loraConfig.frequency_offset);
const char *channelName = channels.getName(channels.getPrimaryIndex());
slotTimeMsec = computeSlotTimeMsec();
preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw);
LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset);
LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", myRegion->name, channelName, loraConfig.modem_preset,
LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", newRegion->name, channelName, loraConfig.modem_preset,
channel_num, power);
LOG_INFO("myRegion->freqStart -> myRegion->freqEnd: %f -> %f (%f MHz)", myRegion->freqStart, myRegion->freqEnd,
myRegion->freqEnd - myRegion->freqStart);
LOG_INFO("numChannels: %d x %.3fkHz", numChannels, bw);
LOG_INFO("newRegion->freqStart -> newRegion->freqEnd: %f -> %f (%f MHz)", newRegion->freqStart, newRegion->freqEnd,
newRegion->freqEnd - newRegion->freqStart);
LOG_INFO("numFreqSlots: %d x %.3fkHz", numFreqSlots, bw);
if (newRegion->profile->overrideSlot != 0) {
LOG_INFO("Using region override slot: %d", newRegion->profile->overrideSlot);
}
LOG_INFO("channel_num: %d", channel_num + 1);
LOG_INFO("frequency: %f", getFreq());
LOG_INFO("Slot time: %u msec, preamble time: %u msec", slotTimeMsec, preambleTimeMsec);
}
} // end of applyModemConfig
/** Slottime is the time to detect a transmission has started, consisting of:
- CAD duration;
@@ -992,4 +1165,4 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p)
sendingPacket = p;
return p->encrypted.size + sizeof(PacketHeader);
}
}