diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index 246cf0022..680ec4878 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -34,6 +34,9 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: return useShortName ? "LongM" : "LongMod"; break; + case meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST: + return useShortName ? "LiteF" : "LiteFast"; + break; default: return useShortName ? "Custom" : "Invalid"; break; diff --git a/src/airtime.cpp b/src/airtime.cpp index a7736d667..0e0d72e20 100644 --- a/src/airtime.cpp +++ b/src/airtime.cpp @@ -133,11 +133,12 @@ bool AirTime::isTxAllowedChannelUtil(bool polite) bool AirTime::isTxAllowedAirUtil() { - if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) { - if (utilizationTXPercent() < myRegion->dutyCycle * polite_duty_cycle_percent / 100) { + float effectiveDutyCycle = getEffectiveDutyCycle(); + if (!config.lora.override_duty_cycle && effectiveDutyCycle < 100) { + if (utilizationTXPercent() < effectiveDutyCycle * polite_duty_cycle_percent / 100) { return true; } else { - LOG_WARN("TX air util. >%f%%. Skip send", myRegion->dutyCycle * polite_duty_cycle_percent / 100); + LOG_WARN("TX air util. >%f%%. Skip send", effectiveDutyCycle * polite_duty_cycle_percent / 100); return false; } } diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 1ac75c4d9..04a17a554 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -141,7 +141,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration) } config.lora.tx_enabled = true; initRegion(); - if (myRegion->dutyCycle < 100) { + if (getEffectiveDutyCycle() < 100) { config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit } diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index f2514eea1..1eed7559b 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -22,4 +22,11 @@ struct RegionInfo { extern const RegionInfo regions[]; extern const RegionInfo *myRegion; -extern void initRegion(); \ No newline at end of file +extern void initRegion(); + +/** + * Get the effective duty cycle for the current region based on device role. + * For EU_866, returns 10% for fixed devices (ROUTER, ROUTER_LATE) and 2.5% for mobile devices. + * For other regions, returns the standard duty cycle. + */ +extern float getEffectiveDutyCycle(); \ No newline at end of file diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 3c0da4494..78eab91d3 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -187,6 +187,13 @@ const RegionInfo regions[] = { */ RDEF(BR_902, 902.0f, 907.5f, 100, 0, 30, true, false, false), + /* + EU 866MHz RFID band (ETSI EN 302 208): 4 channels at 865.7/866.3/866.9/867.5 MHz + 475 kHz gap between channels, 27 dBm, duty cycle 2.5% (mobile) or 10% (fixed) + https://www.etsi.org/deliver/etsi_en/302200_302299/302208/03.04.01_60/en_302208v030401p.pdf + */ + RDEF(EU_866, 865.6375f, 867.5625f, 2.5, 0.475, 27, true, false, false), + /* 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. */ @@ -219,6 +226,23 @@ void initRegion() myRegion = r; } +/** + * Get duty cycle for current region. EU_866: 10% for routers, 2.5% for mobile. + */ +float getEffectiveDutyCycle() +{ + if (myRegion->code == meshtastic_Config_LoRaConfig_RegionCode_EU_866) { + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + return 10.0f; + } else { + return 2.5f; + } + } + // For all other regions, return the standard duty cycle + return myRegion->dutyCycle; +} + /** * ## LoRaWAN for North America @@ -518,6 +542,11 @@ void RadioInterface::applyModemConfig() cr = 8; sf = 12; break; + case meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST: + bw = 125; + cr = 5; + sf = 9; + break; } } else { sf = loraConfig.spread_factor; @@ -551,6 +580,19 @@ void RadioInterface::applyModemConfig() // Set to default modem preset loraConfig.use_preset = true; loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + } else if (myRegion->code == meshtastic_Config_LoRaConfig_RegionCode_EU_866 && bw != 125) { + static const char *err_string = "EU_866 requires 125kHz bandwidth. Fall back to LiteFast preset"; + LOG_ERROR(err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + sprintf(cn->message, err_string); + service->sendClientNotification(cn); + + // Set to LiteFast preset which is compliant + loraConfig.use_preset = true; + loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST; } else { validConfig = true; } @@ -569,8 +611,9 @@ void RadioInterface::applyModemConfig() // 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))); + // Calculate number of channels: spacing = gap between channels (0 for continuous spectrum) + float channelSpacing = myRegion->spacing + (bw / 1000); + uint32_t numChannels = round((myRegion->freqEnd - myRegion->freqStart + myRegion->spacing) / channelSpacing); // 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()); @@ -582,11 +625,8 @@ void RadioInterface::applyModemConfig() 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 frequency: freqStart is band edge, add half bandwidth to get first channel center + float freq = myRegion->freqStart + (bw / 2000) + (channel_num * channelSpacing); // override if we have a verbatim frequency if (loraConfig.override_frequency) { diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 05f47d7f4..59826d0a5 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -294,10 +294,11 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) } // should have already been handled by sendLocal // Abort sending if we are violating the duty cycle - if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) { + float effectiveDutyCycle = getEffectiveDutyCycle(); + if (!config.lora.override_duty_cycle && effectiveDutyCycle < 100) { float hourlyTxPercent = airTime->utilizationTXPercent(); - if (hourlyTxPercent > myRegion->dutyCycle) { - uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, myRegion->dutyCycle); + if (hourlyTxPercent > effectiveDutyCycle) { + uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, effectiveDutyCycle); LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d mins", silentMinutes); diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index a98515059..c4895bffc 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -793,7 +793,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) } config.lora.tx_enabled = true; initRegion(); - if (myRegion->dutyCycle < 100) { + if (getEffectiveDutyCycle() < 100) { config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit } // Compare the entire string, we are sure of the length as a topic has never been set