mirror of
https://github.com/meshtastic/firmware.git
synced 2026-06-18 05:31:40 -04:00
1170 lines
48 KiB
C++
1170 lines
48 KiB
C++
#include "RadioInterface.h"
|
||
#include "Channels.h"
|
||
#include "DisplayFormatters.h"
|
||
#include "LLCC68Interface.h"
|
||
#include "LR1110Interface.h"
|
||
#include "LR1120Interface.h"
|
||
#include "LR1121Interface.h"
|
||
#include "MeshRadio.h"
|
||
#include "MeshService.h"
|
||
#include "NodeDB.h"
|
||
#include "RF95Interface.h"
|
||
#include "Router.h"
|
||
#include "SX1262Interface.h"
|
||
#include "SX1268Interface.h"
|
||
#include "SX1280Interface.h"
|
||
#include "configuration.h"
|
||
#include "detect/LoRaRadioType.h"
|
||
#include "main.h"
|
||
#include "meshUtils.h" // for pow_of_2
|
||
#include "sleep.h"
|
||
#include <assert.h>
|
||
#include <pb_decode.h>
|
||
#include <pb_encode.h>
|
||
#include <string.h>
|
||
|
||
#ifdef ARCH_PORTDUINO
|
||
#include "platform/portduino/PortduinoGlue.h"
|
||
#include "platform/portduino/SimRadio.h"
|
||
#include "platform/portduino/USBHal.h"
|
||
#endif
|
||
|
||
#ifdef ARCH_STM32WL
|
||
#include "STM32WLE5JCInterface.h"
|
||
#endif
|
||
|
||
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, power_limit, frequency_switching, \
|
||
wide_lora, &profile_ptr, #name \
|
||
}
|
||
|
||
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, 30, false, false, PROFILE_STD),
|
||
|
||
/*
|
||
EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21]
|
||
|
||
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, 10, false, false, PROFILE_STD),
|
||
/*
|
||
https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/
|
||
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
|
||
https://www.legislation.gov.uk/uksi/1999/930/schedule/6/part/III/made/data.xht?view=snippet&wrap=true
|
||
|
||
audio_permitted = false per regulation
|
||
|
||
Special Note:
|
||
The link above describes LoRaWAN's band plan, stating a power limit of 16 dBm. This is their own suggested specification,
|
||
we do not need to follow it. The European Union regulations clearly state that the power limit for this frequency range is
|
||
500 mW, or 27 dBm. It also states that we can use interference avoidance and spectrum access techniques (such as LBT +
|
||
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, 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, 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, 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, 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, 14, false, false, PROFILE_STD),
|
||
|
||
/*
|
||
https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf
|
||
|
||
Note:
|
||
- We do LBT, so 100% is allowed.
|
||
*/
|
||
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, 23, false, false, PROFILE_STD),
|
||
|
||
/*
|
||
Taiwan, 920-925Mhz, limited to 0.5W indoor or coastal, 1.0W outdoor.
|
||
5.8.1 in the Low-power Radio-frequency Devices Technical Regulations
|
||
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, 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, 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, 36, false, false, PROFILE_STD),
|
||
|
||
/*
|
||
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||
https://standard.nbtc.go.th/getattachment/Standards/%E0%B8%A1%E0%B8%B2%E0%B8%95%E0%B8%A3%E0%B8%90%E0%B8%B2%E0%B8%99%E0%B8%97%E0%B8%B2%E0%B8%87%E0%B9%80%E0%B8%97%E0%B8%84%E0%B8%99%E0%B8%B4%E0%B8%84%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B9%80%E0%B8%84%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B9%82%E0%B8%97%E0%B8%A3%E0%B8%84%E0%B8%A1%E0%B8%99%E0%B8%B2%E0%B8%84%E0%B8%A1/1033-2565.pdf.aspx?lang=th-TH
|
||
Thailand 920–925 MHz set max TX power to 27 dBm and enforce 10% duty cycle, aligned with NBTC regulations.
|
||
*/
|
||
RDEF(TH, 920.0f, 925.0f, 10, 27, false, false, PROFILE_STD),
|
||
|
||
/*
|
||
433,05-434,7 Mhz 10 mW
|
||
868,0-868,6 Mhz 25 mW
|
||
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
|
||
*/
|
||
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, 20, false, false, PROFILE_STD),
|
||
|
||
/*
|
||
Malaysia
|
||
919 - 923 Mhz at 500mW, no restrictions.
|
||
923 - 924 MHz at 500mW with 1% duty cycle OR frequency hopping.
|
||
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, 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, 20, false, false, PROFILE_STD),
|
||
|
||
/*
|
||
Philippines
|
||
433 - 434.7 MHz <10 mW erp, NTC approved device required
|
||
868 - 869.4 MHz <25 mW erp, NTC approved device required
|
||
915 - 918 MHz <250 mW EIRP, no external antenna allowed
|
||
https://github.com/meshtastic/firmware/issues/4948#issuecomment-2394926135
|
||
*/
|
||
|
||
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
|
||
433.075 - 434.775 MHz <10 mW EIRP, Low Powered Devices (LPD)
|
||
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, 10, false, false, PROFILE_STD),
|
||
RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD),
|
||
|
||
/*
|
||
Nepal
|
||
865 MHz to 868 MHz 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, 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, 30, false, false, PROFILE_STD),
|
||
|
||
/*
|
||
2.4 GHZ WLAN Band equivalent. Only for SX128x chips.
|
||
*/
|
||
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, 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];
|
||
|
||
// Global LoRa radio type
|
||
LoRaRadioType radioType = NO_RADIO;
|
||
|
||
extern RadioLibHal *RadioLibHAL;
|
||
#if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32)
|
||
extern SPIClass SPI1;
|
||
#endif
|
||
|
||
std::unique_ptr<RadioInterface> initLoRa()
|
||
{
|
||
std::unique_ptr<RadioInterface> rIf = nullptr;
|
||
|
||
#if ARCH_PORTDUINO
|
||
SPISettings loraSpiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
|
||
#else
|
||
SPISettings loraSpiSettings(4000000, MSBFIRST, SPI_MODE0);
|
||
#endif
|
||
|
||
#ifdef ARCH_PORTDUINO
|
||
// as one can't use a function pointer to the class constructor:
|
||
auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
||
RADIOLIB_PIN_TYPE busy) {
|
||
switch (portduino_config.lora_module) {
|
||
case use_rf95:
|
||
return std::unique_ptr<RadioInterface>(new RF95Interface(hal, cs, irq, rst, busy));
|
||
case use_sx1262:
|
||
return std::unique_ptr<RadioInterface>(new SX1262Interface(hal, cs, irq, rst, busy));
|
||
case use_sx1268:
|
||
return std::unique_ptr<RadioInterface>(new SX1268Interface(hal, cs, irq, rst, busy));
|
||
case use_sx1280:
|
||
return std::unique_ptr<RadioInterface>(new SX1280Interface(hal, cs, irq, rst, busy));
|
||
case use_lr1110:
|
||
return std::unique_ptr<RadioInterface>(new LR1110Interface(hal, cs, irq, rst, busy));
|
||
case use_lr1120:
|
||
return std::unique_ptr<RadioInterface>(new LR1120Interface(hal, cs, irq, rst, busy));
|
||
case use_lr1121:
|
||
return std::unique_ptr<RadioInterface>(new LR1121Interface(hal, cs, irq, rst, busy));
|
||
case use_llcc68:
|
||
return std::unique_ptr<RadioInterface>(new LLCC68Interface(hal, cs, irq, rst, busy));
|
||
case use_simradio:
|
||
return std::unique_ptr<RadioInterface>(new SimRadio);
|
||
default:
|
||
assert(0); // shouldn't happen
|
||
return std::unique_ptr<RadioInterface>(nullptr);
|
||
}
|
||
};
|
||
|
||
LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(),
|
||
portduino_config.lora_spi_dev.c_str());
|
||
if (portduino_config.lora_spi_dev == "ch341") {
|
||
RadioLibHAL = ch341Hal;
|
||
} else {
|
||
if (RadioLibHAL != nullptr) {
|
||
delete RadioLibHAL;
|
||
RadioLibHAL = nullptr;
|
||
}
|
||
RadioLibHAL = new LockingArduinoHal(SPI, loraSpiSettings);
|
||
}
|
||
rIf =
|
||
loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin,
|
||
portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin);
|
||
|
||
if (!rIf->init()) {
|
||
LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str());
|
||
rIf = nullptr;
|
||
exit(EXIT_FAILURE);
|
||
} else {
|
||
LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str());
|
||
}
|
||
|
||
#elif defined(HW_SPI1_DEVICE)
|
||
LockingArduinoHal *loraHal = new LockingArduinoHal(SPI1, loraSpiSettings);
|
||
RadioLibHAL = loraHal;
|
||
#else // HW_SPI1_DEVICE
|
||
LockingArduinoHal *loraHal = new LockingArduinoHal(SPI, loraSpiSettings);
|
||
RadioLibHAL = loraHal;
|
||
#endif
|
||
|
||
// radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init)
|
||
#if defined(USE_STM32WLx)
|
||
if (!rIf) {
|
||
rIf = std::unique_ptr<STM32WLE5JCInterface>(
|
||
new STM32WLE5JCInterface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||
if (!rIf->init()) {
|
||
LOG_WARN("No STM32WL radio");
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("STM32WL init success");
|
||
radioType = STM32WLx_RADIO;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1
|
||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||
rIf = std::unique_ptr<RF95Interface>(new RF95Interface(loraHal, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1));
|
||
if (!rIf->init()) {
|
||
LOG_WARN("No RF95 radio");
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("RF95 init success");
|
||
radioType = RF95_RADIO;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1
|
||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||
auto sxIf =
|
||
std::unique_ptr<SX1262Interface>(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||
#ifdef SX126X_DIO3_TCXO_VOLTAGE
|
||
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
|
||
#endif
|
||
if (!sxIf->init()) {
|
||
LOG_WARN("No SX1262 radio");
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("SX1262 init success");
|
||
rIf = std::move(sxIf);
|
||
radioType = SX1262_RADIO;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL)
|
||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||
// try using the specified TCXO voltage
|
||
auto sxIf =
|
||
std::unique_ptr<SX1262Interface>(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
|
||
if (!sxIf->init()) {
|
||
LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
||
rIf = std::move(sxIf);
|
||
radioType = SX1262_RADIO;
|
||
}
|
||
}
|
||
|
||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||
// If specified TCXO voltage fails, attempt to use DIO3 as a reference instead
|
||
rIf = std::unique_ptr<SX1262Interface>(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||
if (!rIf->init()) {
|
||
LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V");
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("SX1262 init success, XTAL, Vref 0.0V");
|
||
radioType = SX1262_RADIO;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if defined(USE_SX1268)
|
||
#if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL)
|
||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||
// try using the specified TCXO voltage
|
||
auto sxIf =
|
||
std::unique_ptr<SX1268Interface>(new SX1268Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
|
||
if (!sxIf->init()) {
|
||
LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
|
||
rIf = std::move(sxIf);
|
||
radioType = SX1268_RADIO;
|
||
}
|
||
}
|
||
#endif
|
||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||
rIf = std::unique_ptr<SX1268Interface>(new SX1268Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||
if (!rIf->init()) {
|
||
LOG_WARN("No SX1268 radio");
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("SX1268 init success");
|
||
radioType = SX1268_RADIO;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if defined(USE_LLCC68)
|
||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||
rIf = std::unique_ptr<LLCC68Interface>(new LLCC68Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
|
||
if (!rIf->init()) {
|
||
LOG_WARN("No LLCC68 radio");
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("LLCC68 init success");
|
||
radioType = LLCC68_RADIO;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1
|
||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||
rIf = std::unique_ptr<LR1110Interface>(
|
||
new LR1110Interface(loraHal, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN));
|
||
if (!rIf->init()) {
|
||
LOG_WARN("No LR1110 radio");
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("LR1110 init success");
|
||
radioType = LR1110_RADIO;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1
|
||
if (!rIf) {
|
||
rIf = std::unique_ptr<LR1120Interface>(
|
||
new LR1120Interface(loraHal, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN));
|
||
if (!rIf->init()) {
|
||
LOG_WARN("No LR1120 radio");
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("LR1120 init success");
|
||
radioType = LR1120_RADIO;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1
|
||
if (!rIf) {
|
||
rIf = std::unique_ptr<LR1121Interface>(
|
||
new LR1121Interface(loraHal, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN));
|
||
if (!rIf->init()) {
|
||
LOG_WARN("No LR1121 radio");
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("LR1121 init success");
|
||
radioType = LR1121_RADIO;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1
|
||
if (!rIf) {
|
||
rIf = std::unique_ptr<SX1280Interface>(new SX1280Interface(loraHal, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY));
|
||
if (!rIf->init()) {
|
||
LOG_WARN("No SX1280 radio");
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("SX1280 init success");
|
||
radioType = SX1280_RADIO;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
// check if the radio chip matches the selected region
|
||
if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) {
|
||
LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset");
|
||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||
nodeDB->saveToDisk(SEGMENT_CONFIG);
|
||
|
||
if (rIf && !rIf->reconfigure()) {
|
||
LOG_WARN("Reconfigure failed, rebooting");
|
||
if (screen) {
|
||
screen->showSimpleBanner("Rebooting...");
|
||
}
|
||
rebootAtMsec = millis() + 5000;
|
||
}
|
||
}
|
||
return rIf;
|
||
}
|
||
|
||
void initRegion()
|
||
{
|
||
const RegionInfo *r = regions;
|
||
#ifdef REGULATORY_LORA_REGIONCODE
|
||
for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != REGULATORY_LORA_REGIONCODE; r++)
|
||
;
|
||
LOG_INFO("Wanted region %d, regulatory override to %s", config.lora.region, r->name);
|
||
#else
|
||
for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != config.lora.region; r++)
|
||
;
|
||
LOG_INFO("Wanted region %d, using %s", config.lora.region, r->name);
|
||
#endif
|
||
myRegion = r;
|
||
}
|
||
|
||
const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code)
|
||
{
|
||
const RegionInfo *r = regions;
|
||
for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != code; r++)
|
||
;
|
||
return r;
|
||
}
|
||
|
||
uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received)
|
||
{
|
||
uint32_t pl = 0;
|
||
if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) {
|
||
pl = p->encrypted.size + sizeof(PacketHeader);
|
||
} else {
|
||
size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded);
|
||
pl = numbytes + sizeof(PacketHeader);
|
||
}
|
||
return getPacketTime(pl, received);
|
||
}
|
||
|
||
/** The delay to use for retransmitting dropped packets */
|
||
uint32_t RadioInterface::getRetransmissionMsec(const meshtastic_MeshPacket *p)
|
||
{
|
||
size_t numbytes = p->which_payload_variant == meshtastic_MeshPacket_decoded_tag
|
||
? pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded)
|
||
: p->encrypted.size + MESHTASTIC_HEADER_LENGTH;
|
||
uint32_t packetAirtime = getPacketTime(numbytes + sizeof(PacketHeader));
|
||
// Make sure enough time has elapsed for this packet to be sent and an ACK is received.
|
||
// LOG_DEBUG("Waiting for flooding message with airtime %d and slotTime is %d", packetAirtime, slotTimeMsec);
|
||
float channelUtil = airTime->channelUtilizationPercent();
|
||
uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax);
|
||
// Assuming we pick max. of CWsize and there will be a client with SNR at half the range
|
||
return 2 * packetAirtime + (pow_of_2(CWsize) + 2 * CWmax + pow_of_2(int((CWmax + CWmin) / 2))) * slotTimeMsec +
|
||
PROCESSING_TIME_MSEC;
|
||
}
|
||
|
||
/** The delay to use when we want to send something */
|
||
uint32_t RadioInterface::getTxDelayMsec()
|
||
{
|
||
/** We wait a random multiple of 'slotTimes' (see definition in header file) in order to avoid collisions.
|
||
The pool to take a random multiple from is the contention window (CW), which size depends on the
|
||
current channel utilization. */
|
||
float channelUtil = airTime->channelUtilizationPercent();
|
||
uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax);
|
||
// LOG_DEBUG("Current channel utilization is %f so setting CWsize to %d", channelUtil, CWsize);
|
||
return random(0, pow_of_2(CWsize)) * slotTimeMsec;
|
||
}
|
||
|
||
/** The CW size to use when calculating SNR_based delays */
|
||
uint8_t RadioInterface::getCWsize(float snr)
|
||
{
|
||
// The minimum value for a LoRa SNR
|
||
const int32_t SNR_MIN = -20;
|
||
|
||
// The maximum value for a LoRa SNR
|
||
const int32_t SNR_MAX = 10;
|
||
|
||
return map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax);
|
||
}
|
||
|
||
/** The worst-case SNR_based packet delay */
|
||
uint32_t RadioInterface::getTxDelayMsecWeightedWorst(float snr)
|
||
{
|
||
uint8_t CWsize = getCWsize(snr);
|
||
// offset the maximum delay for routers: (2 * CWmax * slotTimeMsec)
|
||
return (2 * CWmax * slotTimeMsec) + pow_of_2(CWsize) * slotTimeMsec;
|
||
}
|
||
|
||
/** Returns true if we should rebroadcast early like a ROUTER */
|
||
bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p)
|
||
{
|
||
// If we are a ROUTER, we always rebroadcast early
|
||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/** The delay to use when we want to flood a message */
|
||
uint32_t RadioInterface::getTxDelayMsecWeighted(meshtastic_MeshPacket *p)
|
||
{
|
||
// high SNR = large CW size (Long Delay)
|
||
// low SNR = small CW size (Short Delay)
|
||
float snr = p->rx_snr;
|
||
uint32_t delay = 0;
|
||
uint8_t CWsize = getCWsize(snr);
|
||
// LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize);
|
||
if (shouldRebroadcastEarlyLikeRouter(p)) {
|
||
delay = random(0, 2 * CWsize) * slotTimeMsec;
|
||
LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay);
|
||
} else {
|
||
// offset the maximum delay for routers: (2 * CWmax * slotTimeMsec)
|
||
delay = (2 * CWmax * slotTimeMsec) + random(0, pow_of_2(CWsize)) * slotTimeMsec;
|
||
LOG_DEBUG("rx_snr found in packet. Setting tx delay:%d", delay);
|
||
}
|
||
|
||
return delay;
|
||
}
|
||
|
||
void printPacket(const char *prefix, const meshtastic_MeshPacket *p)
|
||
{
|
||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||
std::string out =
|
||
DEBUG_PORT.mt_sprintf("%s (id=0x%08x fr=0x%08x to=0x%08x, transport = %u, WantAck=%d, HopLim=%d Ch=0x%x", prefix, p->id,
|
||
p->from, p->to, p->transport_mechanism, p->want_ack, p->hop_limit, p->channel);
|
||
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||
auto &s = p->decoded;
|
||
|
||
out += DEBUG_PORT.mt_sprintf(" Portnum=%d", s.portnum);
|
||
|
||
if (s.want_response)
|
||
out += DEBUG_PORT.mt_sprintf(" WANTRESP");
|
||
|
||
if (p->pki_encrypted)
|
||
out += DEBUG_PORT.mt_sprintf(" PKI");
|
||
|
||
if (s.source != 0)
|
||
out += DEBUG_PORT.mt_sprintf(" source=%08x", s.source);
|
||
|
||
if (s.dest != 0)
|
||
out += DEBUG_PORT.mt_sprintf(" dest=%08x", s.dest);
|
||
|
||
if (s.request_id)
|
||
out += DEBUG_PORT.mt_sprintf(" requestId=%0x", s.request_id);
|
||
|
||
/* now inside Data and therefore kinda opaque
|
||
if (s.which_ackVariant == SubPacket_success_id_tag)
|
||
out += DEBUG_PORT.mt_sprintf(" successId=%08x", s.ackVariant.success_id);
|
||
else if (s.which_ackVariant == SubPacket_fail_id_tag)
|
||
out += DEBUG_PORT.mt_sprintf(" failId=%08x", s.ackVariant.fail_id); */
|
||
} else {
|
||
out += " encrypted";
|
||
out += DEBUG_PORT.mt_sprintf(" len=%d", p->encrypted.size + sizeof(PacketHeader));
|
||
}
|
||
|
||
if (p->rx_time != 0)
|
||
out += DEBUG_PORT.mt_sprintf(" rxtime=%u", p->rx_time);
|
||
if (p->rx_snr != 0.0)
|
||
out += DEBUG_PORT.mt_sprintf(" rxSNR=%g", p->rx_snr);
|
||
if (p->rx_rssi != 0)
|
||
out += DEBUG_PORT.mt_sprintf(" rxRSSI=%i", p->rx_rssi);
|
||
if (p->via_mqtt != 0)
|
||
out += DEBUG_PORT.mt_sprintf(" via MQTT");
|
||
if (p->hop_start != 0)
|
||
out += DEBUG_PORT.mt_sprintf(" hopStart=%d", p->hop_start);
|
||
if (p->next_hop != 0)
|
||
out += DEBUG_PORT.mt_sprintf(" nextHop=0x%x", p->next_hop);
|
||
if (p->relay_node != 0)
|
||
out += DEBUG_PORT.mt_sprintf(" relay=0x%x", p->relay_node);
|
||
if (p->priority != 0)
|
||
out += DEBUG_PORT.mt_sprintf(" priority=%d", p->priority);
|
||
|
||
out += ")";
|
||
LOG_DEBUG("%s", out.c_str());
|
||
#endif
|
||
}
|
||
|
||
RadioInterface::RadioInterface()
|
||
{
|
||
assert(sizeof(PacketHeader) == MESHTASTIC_HEADER_LENGTH); // make sure the compiler did what we expected
|
||
}
|
||
|
||
bool RadioInterface::reconfigure()
|
||
{
|
||
applyModemConfig();
|
||
return true;
|
||
}
|
||
|
||
bool RadioInterface::init()
|
||
{
|
||
LOG_INFO("Start meshradio init");
|
||
|
||
configChangedObserver.observe(&service->configChanged);
|
||
preflightSleepObserver.observe(&preflightSleep);
|
||
notifyDeepSleepObserver.observe(¬ifyDeepSleep);
|
||
|
||
// we now expect interfaces to operate in promiscuous mode
|
||
// radioIf.setThisAddress(nodeDB->getNodeNum()); // Note: we must do this here, because the nodenum isn't inited at
|
||
// constructor time.
|
||
|
||
applyModemConfig();
|
||
|
||
return true;
|
||
}
|
||
|
||
int RadioInterface::notifyDeepSleepCb(void *unused)
|
||
{
|
||
sleep();
|
||
return 0;
|
||
}
|
||
|
||
/** hash a string into an integer
|
||
*
|
||
* djb2 by Dan Bernstein.
|
||
* http://www.cse.yorku.ca/~oz/hash.html
|
||
*/
|
||
uint32_t hash(const char *str)
|
||
{
|
||
uint32_t hash = 5381;
|
||
int c;
|
||
|
||
while ((c = *str++) != 0)
|
||
hash = ((hash << 5) + hash) + (unsigned char)c; /* hash * 33 + c */
|
||
|
||
return hash;
|
||
}
|
||
|
||
/**
|
||
* Save our frequency for later reuse.
|
||
*/
|
||
void RadioInterface::saveFreq(float freq)
|
||
{
|
||
savedFreq = freq;
|
||
}
|
||
|
||
/**
|
||
* Save our frequency slot (aka channel) for later reuse.
|
||
*/
|
||
void RadioInterface::saveChannelNum(uint32_t channel_num)
|
||
{
|
||
savedChannelNum = channel_num;
|
||
}
|
||
|
||
/**
|
||
* Save our frequency for later reuse.
|
||
*/
|
||
float RadioInterface::getFreq()
|
||
{
|
||
return savedFreq;
|
||
}
|
||
|
||
/**
|
||
* Save our channel for later reuse.
|
||
*/
|
||
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;
|
||
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;
|
||
LOG_INFO("Using custom Coding Rate %u", cr);
|
||
} else {
|
||
cr = newcr;
|
||
}
|
||
|
||
} 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 > 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 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
|
||
|
||
uint32_t channel_num;
|
||
float freq;
|
||
|
||
// 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);
|
||
|
||
// 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", newRegion->name, channelName, loraConfig.modem_preset,
|
||
channel_num, power);
|
||
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;
|
||
- roundtrip air propagation time (assuming max. 30km between nodes);
|
||
- Tx/Rx turnaround time (maximum of SX126x and SX127x);
|
||
- MAC processing time (measured on T-beam) */
|
||
uint32_t RadioInterface::computeSlotTimeMsec()
|
||
{
|
||
float sumPropagationTurnaroundMACTime = 0.2 + 0.4 + 7; // in milliseconds
|
||
float symbolTime = pow_of_2(sf) / bw; // in milliseconds
|
||
|
||
if (myRegion->wideLora) {
|
||
// CAD duration derived from AN1200.22 of SX1280
|
||
return (NUM_SYM_CAD_24GHZ + (2 * sf + 3) / 32) * symbolTime + sumPropagationTurnaroundMACTime;
|
||
} else {
|
||
// CAD duration for SX127x is max. 2.25 symbols, for SX126x it is number of symbols + 0.5 symbol
|
||
return max(2.25, NUM_SYM_CAD + 0.5) * symbolTime + sumPropagationTurnaroundMACTime;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Some regulatory regions limit xmit power.
|
||
* This function should be called by subclasses after setting their desired power. It might lower it
|
||
*/
|
||
void RadioInterface::limitPower(int8_t loraMaxPower)
|
||
{
|
||
uint8_t maxPower = 255; // No limit
|
||
|
||
if (myRegion->powerLimit)
|
||
maxPower = myRegion->powerLimit;
|
||
|
||
if ((power > maxPower) && !devicestate.owner.is_licensed) {
|
||
LOG_INFO("Lower transmit power because of regulatory limits");
|
||
power = maxPower;
|
||
}
|
||
|
||
#if HAS_LORA_FEM
|
||
if (!devicestate.owner.is_licensed) {
|
||
power = loraFEMInterface.powerConversion(power);
|
||
}
|
||
#else
|
||
// todo:All entries containing "lora fem" are grouped together above.
|
||
#ifdef ARCH_PORTDUINO
|
||
size_t num_pa_points = portduino_config.num_pa_points;
|
||
const uint16_t *tx_gain = portduino_config.tx_gain_lora;
|
||
#else
|
||
size_t num_pa_points = NUM_PA_POINTS;
|
||
const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA};
|
||
#endif
|
||
|
||
if (num_pa_points == 1) {
|
||
if (tx_gain[0] > 0 && !devicestate.owner.is_licensed) {
|
||
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[0]);
|
||
power -= tx_gain[0];
|
||
}
|
||
} else if (!devicestate.owner.is_licensed) {
|
||
// we have an array of PA gain values. Find the highest power setting that works.
|
||
for (int radio_dbm = 0; radio_dbm < (int)num_pa_points; radio_dbm++) {
|
||
if (((radio_dbm + tx_gain[radio_dbm]) > power) ||
|
||
((radio_dbm == (int)(num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) {
|
||
// we've exceeded the power limit, or hit the max we can do
|
||
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]);
|
||
power -= tx_gain[radio_dbm];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
if (power > loraMaxPower) // Clamp power to maximum defined level
|
||
power = loraMaxPower;
|
||
|
||
LOG_INFO("Final Tx power: %d dBm", power);
|
||
}
|
||
|
||
void RadioInterface::deliverToReceiver(meshtastic_MeshPacket *p)
|
||
{
|
||
if (router) {
|
||
p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||
router->enqueueReceivedMessage(p);
|
||
}
|
||
}
|
||
|
||
/***
|
||
* given a packet set sendingPacket and decode the protobufs into radiobuf. Returns # of payload bytes to send
|
||
*/
|
||
size_t RadioInterface::beginSending(meshtastic_MeshPacket *p)
|
||
{
|
||
assert(!sendingPacket);
|
||
|
||
// LOG_DEBUG("Send queued packet on mesh (txGood=%d,rxGood=%d,rxBad=%d)", rf95.txGood(), rf95.rxGood(), rf95.rxBad());
|
||
assert(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag); // It should have already been encoded by now
|
||
|
||
radioBuffer.header.from = p->from;
|
||
radioBuffer.header.to = p->to;
|
||
radioBuffer.header.id = p->id;
|
||
radioBuffer.header.channel = p->channel;
|
||
radioBuffer.header.next_hop = p->next_hop;
|
||
radioBuffer.header.relay_node = p->relay_node;
|
||
if (p->hop_limit > HOP_MAX) {
|
||
LOG_WARN("hop limit %d is too high, setting to %d", p->hop_limit, HOP_RELIABLE);
|
||
p->hop_limit = HOP_RELIABLE;
|
||
}
|
||
radioBuffer.header.flags =
|
||
p->hop_limit | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) | (p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0);
|
||
radioBuffer.header.flags |= (p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK;
|
||
|
||
// if the sender nodenum is zero, that means uninitialized
|
||
assert(radioBuffer.header.from);
|
||
assert(p->encrypted.size <= sizeof(radioBuffer.payload));
|
||
memcpy(radioBuffer.payload, p->encrypted.bytes, p->encrypted.size);
|
||
|
||
sendingPacket = p;
|
||
return p->encrypted.size + sizeof(PacketHeader);
|
||
} |