* Add software LoRa IRQ polling and an MCP23017 expander HAL for radios behind an I2C expander
Some boards route the SX126x control lines (NRESET/DIO1/BUSY) through an I2C
GPIO expander instead of native GPIOs, and don't wire the expander's /INT to
the MCU, so RadioLib cannot take a hardware DIO1 interrupt.
Two reusable pieces, both gated behind USE_MCP23017 / LORA_DIO1_SOFTWARE_POLL
so builds that don't define them are unaffected:
- ExtensionIOMCP23017 + MCP23017LockingArduinoHal: a RadioLib HAL that maps
virtual pins (MCP23017_VPIN_BASE .. +15) onto an MCP23017's GPIO, so
RESET/BUSY/DIO1 reads and writes become I2C transactions while the real SPI
GPIOs (SCK/MOSI/MISO/CS) pass straight through. A failed I2C read skips the
read-modify-write rather than clobbering the rest of the bank.
- LORA_DIO1_SOFTWARE_POLL: with no DIO1 interrupt available, the SX126x
interface polls the radio IRQ status register from the radio thread and
synthesizes the ISR_TX/ISR_RX events, filtering noisy preamble/header IRQs so
they can't starve TX. A 1 ms ISR_POLL_TICK drives the poll; TX timers may
overwrite the pending tick (pollMissedIrqs() is the backup that bounds the
latency of the busy-Rx contention window). Behaviour is unchanged on all
boards that keep the hardware interrupt.
* Add Meshnology W10 AIOT Dev Kit variant (SX1262 via MCP23017 expander)
ESP32-S3R8 + EBYTE E22-900MM22S (SX1262) + AXP2101 PMIC + Quectel L76KB GPS +
SPI TFT, with the radio's RESET/DIO1/BUSY and the LCD reset routed through an
MCP23017 I2C expander at 0x20. The expander's /INT is not wired to the ESP32,
so DIO1 uses the software poll. Pins come from the board schematic and the
vendor firmware; the expander is an MCP23017 despite the V1.1 schematic's stale
'TCA9555' label (the V1.2 placement diagram and all vendor code use the MCP23017
register map).
A local pins_arduino.h shadows the generic esp32s3 variant to drop RGB_BUILTIN,
which otherwise pulls the Arduino RMT HAL into the link and fails against this
build's trimmed FreeRTOS config.
Reports HardwareModel 140 (MESHNOLOGY_W10, already present in the protobufs).
Hardware-verified on a real W10 AIOT Dev Kit: AXP2101 PMU, MCP23017, SX1262
init + a full over-the-air TX/RX round trip through the expander, PCF85063 RTC,
SHT41 and QMI8658 auto-detected, and an L76KB GPS fix.
* meshnology-w10: enable ES8311 speaker for notification tones
Bring up the ES8311 codec (I2C 0x18) -> NS4150 amp -> speaker so the board can
play notification tones / ringtones over the I2S buzzer path (the
use_i2s_as_buzzer external-notification option). Codec2 voice stays out of
scope: it is SX1280-only and this is a sub-GHz board.
- variant.h: HAS_I2S + DAC_I2S pins (MCLK=1, BCK=2, WS=4, DOUT=5, DIN=3)
- platformio.ini: arduino-audio-driver + ESP8266Audio + ESP8266SAM
- extra_variants/meshnology_w10/variant.cpp: lateInitVariant() configures the
ES8311, mirroring the other Meshtastic ES8311 boards. Kept codec-only (no
main.h) so the audio driver's 'using namespace audio_driver' does not pull a
conflicting GpioPin into scope alongside Meshtastic's class GpioPin.
- AudioThread.h: toggle the NS4150 amp (MCP23017 EXIO_PA_CTRL) around playback.
Verified on hardware: a test tune played audibly through the speaker.
* meshnology-w10: address review feedback on the MCP23017 driver
- ExtensionIOMCP23017: serialize register access with a mutex, since the radio
HAL (BUSY/DIO1/RESET) and AudioThread (amp enable) now reach the expander from
different threads and the read-modify-write paths are not atomic.
- digitalRead: return a fail-safe HIGH on a failed read instead of LOW, so a
transient I2C error on the LoRa BUSY line can't look like 'ready' and let
RadioLib start an SPI transaction early. (DIO1 is polled via the radio IRQ
register, not this pin.)
- enablePinChangeInterrupt: use the checked readReg() overload and skip on a
failed read, matching the other read-modify-write helpers.
- meshnology_w10 variant.cpp: log a warning if an ES8311 register write NACKs
instead of silently leaving the codec half-configured.
- RadioInterface.cpp: guard the MCP23017 HAL branch with ARCH_ESP32 to match the
include and driver-file guards.
* Replace board-model audio/sleep ifdefs with reusable variant capability macros
Two shared-code spots keyed off specific hardware models; move the board-specific
detail into opt-in macros the variants define, so the core code stays generic and
new boards can opt into the behavior class without touching shared files.
- AudioThread amp control: drop the T_LORA_PAGER / MESHNOLOGY_W10 ifdefs. A board
with an I2S amp now defines AUDIO_AMP_ENABLE(on) in its variant.h (T-LoRa Pager
and Meshnology W10 do), and AudioThread just calls it around playback. The
expander includes are keyed on USE_XL9555 / USE_MCP23017 (capabilities) instead
of the model name.
- Light-sleep / DIO1 wakeup: drop the SENSECAP_INDICATOR check. Boards whose
LORA_DIO1 is an expander pin (not an ESP32 GPIO) define LORA_DIO1_EXTENDED_IO
(SenseCAP Indicator and Meshnology W10), and sleep.cpp keys off that for both
the light-sleep skip and the DIO1 GPIO-wakeup skip. This also fixes a latent
issue on the SenseCAP: it previously reached the GPIO-wakeup path and called
gpio_pulldown_en() on a virtual expander pin; it now skips that cleanly.
Builds verified on meshnology_w10, tlora-pager, and seeed-sensecap-indicator.
* meshnology-w10: silence cppcheck constParameterPointer on the ISR-callback helper
isIsrTxCallback() takes a function pointer it only compares; cppcheck's
constParameterPointer wants it 'pointer to const', which is meaningless for a
function pointer (the codebase already globally suppresses the sibling
constParameterCallback). Inline-suppress it to keep the check green.
* HopScaling: qualify the member assignment as this->count
cppcheck (CI's version) flags 'count = newCount;' at the end of trimIfNeeded()
as uselessAssignmentArg ('assignment of function parameter has no effect') - a
false positive, since count is a member that outlives the call. Write it as
this->count (matching the Step-1 assignment above) so the analyzer sees a member
write. This finding comes in via the develop merge, not this board; fixing it
here to unblock the PR's cppcheck check.
1470 lines
63 KiB
C++
1470 lines
63 KiB
C++
#include "RadioInterface.h"
|
||
#include "Channels.h"
|
||
#include "DisplayFormatters.h"
|
||
#include "LLCC68Interface.h"
|
||
#include "LR1110Interface.h"
|
||
#include "LR1120Interface.h"
|
||
#include "LR1121Interface.h"
|
||
#include "LR2021Interface.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
|
||
|
||
#if defined(ARCH_ESP32) && defined(USE_MCP23017)
|
||
#include "platform/esp32/MCP23017LockingArduinoHal.h"
|
||
#endif
|
||
|
||
#ifdef ARCH_STM32WL
|
||
#include "STM32WLE5JCInterface.h"
|
||
#endif
|
||
|
||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_STD[] = {
|
||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW),
|
||
PRESET(SHORT_FAST), PRESET(LONG_MODERATE), PRESET(SHORT_TURBO), PRESET(LONG_TURBO), MODEM_PRESET_END};
|
||
|
||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_868[] = {
|
||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST),
|
||
PRESET(SHORT_SLOW), PRESET(SHORT_FAST), PRESET(LONG_MODERATE), MODEM_PRESET_END};
|
||
|
||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_UNDEF[] = {PRESET(LONG_FAST), MODEM_PRESET_END};
|
||
|
||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_LITE[] = {PRESET(LITE_FAST), PRESET(LITE_SLOW), MODEM_PRESET_END};
|
||
|
||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_NARROW[] = {PRESET(NARROW_FAST), PRESET(NARROW_SLOW),
|
||
MODEM_PRESET_END};
|
||
|
||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_TINY[] = {PRESET(TINY_FAST), PRESET(TINY_SLOW), MODEM_PRESET_END};
|
||
|
||
// Region profiles: bundle preset list + regulatory parameters shared across regions
|
||
// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle
|
||
const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 1, 1};
|
||
const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 1, 1};
|
||
const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 1, 1};
|
||
const RegionProfile PROFILE_LITE = {PRESETS_LITE, 0.4, 0.0375f, false, false, 0, 10, 10};
|
||
const RegionProfile PROFILE_NARROW = {PRESETS_NARROW, 0, 0.0104f, true, false, 0, 1, 1};
|
||
// Ham '20kHz' profile. 15.6kHz bandwidth coerced to 20kHz via padding.
|
||
const RegionProfile PROFILE_HAM_20KHZ = {PRESETS_TINY, 0, 0.0022f, false, true, 0, 2, 2};
|
||
// Ham '100kHz' profile. 62.5kHz bandwidth coerced to 100kHz via padding.
|
||
const RegionProfile PROFILE_HAM_100KHZ = {PRESETS_NARROW, 0, 0.01875f, false, true, 0, 1, 1};
|
||
|
||
Observable<uint32_t> RadioInterface::loraRxPacketObservable;
|
||
|
||
#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr, default_preset, \
|
||
override_slot) \
|
||
{ \
|
||
meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, \
|
||
wide_lora, &profile_ptr, default_preset, override_slot, #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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
/*
|
||
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
|
||
|
||
EU 866MHz band (Band no. 46b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD)
|
||
Gives 4 channels at 865.7/866.3/866.9/867.5 MHz, 400 kHz gap plus 37.5 kHz padding between channels, 27 dBm,
|
||
duty cycle 2.5% (mobile) or 10% (fixed) https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:02006D0771(01)-20250123
|
||
|
||
EU 868MHz band: 3 channels at 869.410/869.4625/869.577 MHz
|
||
Channel centres at 869.442/869.525/869.608 MHz,
|
||
10.4 kHz padding on channels, 27 dBm, duty cycle 10%
|
||
*/
|
||
RDEF(EU_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_EU868, PRESET(LONG_FAST), 0),
|
||
RDEF(EU_866, 865.6f, 867.6f, 2.5, 27, false, false, PROFILE_LITE, PRESET(LITE_FAST), 0),
|
||
RDEF(EU_N_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_NARROW, PRESET(NARROW_SLOW), 1),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
RDEF(PH_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||
RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
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, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
ITU Region 1 (Europe, Africa, Middle East, former USSR) amateur 2m allocation: 144.000 - 146.000 MHz.
|
||
Power limit is the regulatory ceiling (1 W / 30 dBm) - individual hardware will cap below this
|
||
via its own PA curve; the field here is just the legal upper bound.
|
||
|
||
Default slot: 26 (144.510 MHz)
|
||
https://www.iaru-r1.org/wp-content/uploads/2020/12/VHF-Bandplan.pdf
|
||
*/
|
||
RDEF(ITU1_2M, 144.0f, 146.0f, 100, 30, false, false, PROFILE_HAM_20KHZ, PRESET(TINY_FAST), 26),
|
||
|
||
/*
|
||
ITU Region 2 (Americas) amateur 2m allocation: 144.000 - 148.000 MHz.
|
||
Typical admin rules (e.g. US FCC Part 97) allow well above 30 dBm for licensed operators.
|
||
|
||
Default slot: 51 (145.010 MHz)
|
||
https://www.arrl.org/band-plan
|
||
*/
|
||
RDEF(ITU2_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM_20KHZ, PRESET(TINY_FAST), 51),
|
||
|
||
/*
|
||
ITU Region 3 (Asia/Pacific) amateur 2m allocation: 144.000 - 148.000 MHz.
|
||
Typical admin rules allow well above 30 dBm for licensed operators.
|
||
|
||
Default slot: 33 (144.650 MHz)
|
||
https://www.iaru.org/wp-content/uploads/2020/01/R3-004-IARU-Region-3-Bandplan-rev.2.pdf
|
||
https://www.wia.org.au/members/bandplans/data/documents/WIA%20Australian%20Band%20Plan%202026.pdf
|
||
*/
|
||
RDEF(ITU3_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM_20KHZ, PRESET(TINY_FAST), 33),
|
||
|
||
/*
|
||
ITU Region 2 (Americas) amateur 1.25m '125cm' allocation: 220.000 - 225.000 MHz.
|
||
Typical admin rules (e.g. US FCC Part 97) allow well above 30 dBm for licensed operators.
|
||
Note: Some countries do not allocate 220-222 MHz (e.g. USA, Canada). Check local law!
|
||
|
||
Default slot: 37 (223.650 MHz)
|
||
https://www.arrl.org/band-plan
|
||
*/
|
||
RDEF(ITU2_125CM, 220.0f, 225.0f, 100, 30, false, false, PROFILE_HAM_100KHZ, PRESET(NARROW_SLOW), 37),
|
||
|
||
/*
|
||
ITU Region 1 (Europe, Africa, Middle East, former USSR) amateur 70cm allocation: 430.000 - 440.000 MHz.
|
||
Power limit is the regulatory ceiling (1 W / 30 dBm) - individual hardware will cap below this
|
||
via its own PA curve; the field here is just the legal upper bound.
|
||
|
||
Default slot: 37 (433.650 MHz)
|
||
*/
|
||
RDEF(ITU1_70CM, 430.0f, 440.0f, 100, 30, false, false, PROFILE_HAM_100KHZ, PRESET(NARROW_SLOW), 37),
|
||
|
||
/*
|
||
ITU Region 2 (Americas) amateur 70cm allocation: 420.000 - 450.000 MHz.
|
||
Typical admin rules (e.g. US FCC Part 97) allow well above 30 dBm for licensed operators.
|
||
Note: Some countries do not allocate 420-430 MHz or 440-450 MHz. Check local law!
|
||
|
||
Default slot: 137 (433.650 MHz)
|
||
*/
|
||
RDEF(ITU2_70CM, 420.0f, 450.0f, 100, 30, false, false, PROFILE_HAM_100KHZ, PRESET(NARROW_SLOW), 137),
|
||
|
||
/*
|
||
ITU Region 3 (Asia/Pacific) amateur 70cm allocation: 430.000 - 450.000 MHz.
|
||
Typical admin rules allow well above 30 dBm for licensed operators.
|
||
Note: Some countries do not allocate 440-450 MHz. Check local law!
|
||
|
||
Default slot: 37 (433.650 MHz)
|
||
*/
|
||
RDEF(ITU3_70CM, 430.0f, 450.0f, 100, 30, false, false, PROFILE_HAM_100KHZ, PRESET(NARROW_SLOW), 37),
|
||
|
||
/*
|
||
2.4 GHZ WLAN Band equivalent. Only for SX128x chips.
|
||
*/
|
||
RDEF(LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||
|
||
/*
|
||
This needs to be last. Same as US.
|
||
*/
|
||
RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF, PRESET(LONG_FAST), 0),
|
||
|
||
};
|
||
|
||
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;
|
||
#elif defined(ARCH_ESP32) && defined(USE_MCP23017)
|
||
// Radio control lines (RESET/DIO1/BUSY) are virtual pins on an MCP23017 I2C expander
|
||
LockingArduinoHal *loraHal = new MCP23017LockingArduinoHal(SPI, loraSpiSettings, mcpIoExpander);
|
||
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_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
|
||
if (!rIf) {
|
||
rIf = std::unique_ptr<LR2021Interface>(
|
||
new LR2021Interface(loraHal, LR2021_SPI_NSS_PIN, LR2021_IRQ_PIN, LR2021_NRESET_PIN, LR2021_BUSY_PIN));
|
||
if (!rIf->init()) {
|
||
LOG_WARN("No LR2021 radio");
|
||
rIf = nullptr;
|
||
} else {
|
||
LOG_INFO("LR2021 init success");
|
||
radioType = LR2021_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;
|
||
}
|
||
|
||
void getRegionPresetMap(meshtastic_LoRaRegionPresetMap &map)
|
||
{
|
||
map = meshtastic_LoRaRegionPresetMap_init_zero;
|
||
|
||
const size_t maxGroups = sizeof(map.groups) / sizeof(map.groups[0]);
|
||
const size_t maxRegions = sizeof(map.region_groups) / sizeof(map.region_groups[0]);
|
||
const size_t maxPresets = sizeof(map.groups[0].presets) / sizeof(map.groups[0].presets[0]);
|
||
|
||
// Coalesce regions that share an identical preset list into one group. Two
|
||
// regions belong to the same group when they share the same RegionProfile
|
||
// (which owns the preset list + licensing) AND the same default preset.
|
||
// Keyed by profile pointer, not the preset-array pointer: PROFILE_NARROW and
|
||
// PROFILE_HAM_100KHZ share PRESETS_NARROW but differ in licensedOnly.
|
||
const RegionProfile *groupProfile[sizeof(map.groups) / sizeof(map.groups[0])] = {};
|
||
|
||
for (const RegionInfo *r = regions; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET; r++) {
|
||
// No room left to map any further region; once full we can't add more, so
|
||
// log once and stop. An incomplete map means clients won't constrain the
|
||
// omitted regions, so this must be discoverable rather than silent.
|
||
if (map.region_groups_count >= maxRegions) {
|
||
LOG_ERROR("Region preset map full at %u regions; remaining regions omitted", (unsigned)maxRegions);
|
||
break;
|
||
}
|
||
|
||
// Find the group this region belongs to, or create it.
|
||
int gi = -1;
|
||
for (pb_size_t g = 0; g < map.groups_count; g++) {
|
||
if (groupProfile[g] == r->profile && map.groups[g].default_preset == r->getDefaultPreset()) {
|
||
gi = g;
|
||
break;
|
||
}
|
||
}
|
||
if (gi < 0) {
|
||
if (map.groups_count >= maxGroups) {
|
||
// Out of group slots (should not happen for the current table). The
|
||
// region can't be advertised; skip it but make the gap visible.
|
||
LOG_ERROR("Region preset map out of group slots (%u); region %d omitted", (unsigned)maxGroups, r->code);
|
||
continue;
|
||
}
|
||
gi = map.groups_count++;
|
||
groupProfile[gi] = r->profile;
|
||
meshtastic_LoRaPresetGroup &grp = map.groups[gi];
|
||
grp.default_preset = r->getDefaultPreset();
|
||
grp.licensed_only = r->profile->licensedOnly;
|
||
grp.presets_count = 0;
|
||
for (size_t i = 0; r->profile->presets[i] != MODEM_PRESET_END && grp.presets_count < maxPresets; i++)
|
||
grp.presets[grp.presets_count++] = r->profile->presets[i];
|
||
}
|
||
|
||
// Map this region to its group (capacity checked at the top of the loop).
|
||
meshtastic_LoRaRegionPresets &rg = map.region_groups[map.region_groups_count++];
|
||
rg.region = r->code;
|
||
rg.group_index = (uint8_t)gi;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// Node IDs and packet IDs are formatted as 0x%08x in logs, and !%08x in user-facing display.
|
||
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=%d", 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=0x%08x", s.source);
|
||
|
||
if (s.dest != 0)
|
||
out += DEBUG_PORT.mt_sprintf(" dest=0x%08x", s.dest);
|
||
|
||
if (s.request_id)
|
||
out += DEBUG_PORT.mt_sprintf(" requestId=0x%08x", 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 a client notification (error level unless specified). Safe to call when service is null (e.g. in tests).
|
||
*/
|
||
static void sendErrorNotification(const char *msg, meshtastic_LogRecord_Level level = meshtastic_LogRecord_Level_ERROR)
|
||
{
|
||
if (!service)
|
||
return;
|
||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||
if (!cn)
|
||
return;
|
||
cn->level = level;
|
||
snprintf(cn->message, sizeof(cn->message), "%s", msg);
|
||
service->sendClientNotification(cn);
|
||
}
|
||
|
||
// The EU_868/EU_866/EU_N_868 trio own mutually exclusive preset lists. Selecting a preset
|
||
// locked to a sibling means the user wants that sibling region, not the default preset.
|
||
static const meshtastic_Config_LoRaConfig_RegionCode SWAPPABLE_EU_REGIONS[] = {
|
||
meshtastic_Config_LoRaConfig_RegionCode_EU_868,
|
||
meshtastic_Config_LoRaConfig_RegionCode_EU_866,
|
||
meshtastic_Config_LoRaConfig_RegionCode_EU_N_868,
|
||
};
|
||
|
||
/**
|
||
* If currentRegion is one of the swappable EU regions and preset belongs to a sibling in
|
||
* that trio, return the sibling region that owns the preset. Returns nullptr otherwise.
|
||
*/
|
||
const RegionInfo *RadioInterface::regionSwapForPreset(meshtastic_Config_LoRaConfig_RegionCode currentRegion,
|
||
meshtastic_Config_LoRaConfig_ModemPreset preset)
|
||
{
|
||
bool currentIsSwappable = false;
|
||
for (auto code : SWAPPABLE_EU_REGIONS) {
|
||
if (code == currentRegion)
|
||
currentIsSwappable = true;
|
||
}
|
||
if (!currentIsSwappable)
|
||
return nullptr;
|
||
|
||
for (auto code : SWAPPABLE_EU_REGIONS) {
|
||
if (code == currentRegion)
|
||
continue;
|
||
const RegionInfo *sibling = getRegion(code);
|
||
if (sibling->supportsPreset(preset))
|
||
return sibling;
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
/**
|
||
* Checks if a region is valid for the current settings, with no side effects.
|
||
* Safe to call speculatively (e.g. from UI pickers). When errBuf is given, it
|
||
* receives the human-readable failure reason.
|
||
* Returns false if not compatible.
|
||
*/
|
||
bool RadioInterface::checkConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig, char *errBuf, size_t errLen)
|
||
{
|
||
const RegionInfo *newRegion = getRegion(loraConfig.region);
|
||
|
||
// Reject unrecognized region codes (getRegion returns UNSET sentinel for unknown codes)
|
||
if (newRegion->code != loraConfig.region) {
|
||
if (errBuf)
|
||
snprintf(errBuf, errLen, "Region code %d is not recognized", loraConfig.region);
|
||
return false;
|
||
}
|
||
|
||
// If you are not licensed, you can't use ham regions.
|
||
if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed) {
|
||
if (errBuf)
|
||
snprintf(errBuf, errLen, "Region %s requires licensed mode", newRegion->name);
|
||
return false;
|
||
}
|
||
|
||
// Hardware compatibility: wide-LoRa (2.4 GHz) regions need a wide-capable radio, and
|
||
// sub-GHz regions need a radio that can tune below 2.4 GHz (SX128x cannot). UNSET is
|
||
// always allowed since it is the "no region" state.
|
||
if (newRegion->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && RadioLibInterface::instance) {
|
||
const char *unsupported = nullptr;
|
||
if (newRegion->wideLora && !RadioLibInterface::instance->wideLora()) {
|
||
unsupported = "2.4 GHz";
|
||
} else if (!newRegion->wideLora && !RadioLibInterface::instance->supportsSubGhz()) {
|
||
unsupported = "sub-GHz";
|
||
}
|
||
if (unsupported) {
|
||
if (errBuf)
|
||
snprintf(errBuf, errLen, "Region %s needs %s, which this radio does not support", newRegion->name, unsupported);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Checks if a region is valid for the current settings. On failure, logs at ERROR,
|
||
* records a critical error, and sends a client notification.
|
||
* Returns false if not compatible.
|
||
*/
|
||
bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig)
|
||
{
|
||
char err_string[160];
|
||
if (checkConfigRegion(loraConfig, err_string, sizeof(err_string)))
|
||
return true;
|
||
|
||
LOG_ERROR("%s", err_string);
|
||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||
sendErrorNotification(err_string);
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Internal helper: check 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 = newRegion->supportsPreset(loraConfig.modem_preset);
|
||
if (!preset_valid) {
|
||
// A preset locked to a sibling of the swappable EU regions swaps the region instead
|
||
// of clamping the preset, as long as the previous region was itself one of the trio.
|
||
const RegionInfo *swapRegion = regionSwapForPreset(loraConfig.region, loraConfig.modem_preset);
|
||
if (swapRegion) {
|
||
if (!clamp) {
|
||
// Validation must still fail so callers route into the clamp, but quietly:
|
||
// the clamp will accept this config by swapping regions, so don't record a
|
||
// critical error or alarm the user over a change that is about to succeed.
|
||
LOG_INFO("Preset %s implies region swap %s to %s, deferring to clamp", presetName, newRegion->name,
|
||
swapRegion->name);
|
||
return false;
|
||
}
|
||
snprintf(err_string, sizeof(err_string), "Preset %s swaps region %s to %s", presetName, newRegion->name,
|
||
swapRegion->name);
|
||
LOG_INFO("%s", err_string);
|
||
sendErrorNotification(err_string, meshtastic_LogRecord_Level_INFO);
|
||
|
||
loraConfig.region = swapRegion->code;
|
||
newRegion = swapRegion;
|
||
check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora);
|
||
preset_valid = true;
|
||
}
|
||
}
|
||
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 {
|
||
// Clamp at the source so numFreqSlots below can never be 0 (bandwidth 0 is reachable from a crafted set_config)
|
||
check_bw = clampBandwidthKHz(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);
|
||
// numFreqSlots can still be 0 for an UNSET/degenerate region, and % 0 is a SIGFPE
|
||
uint32_t channelNameHashSlot = numFreqSlots ? (hash(channelName) % numFreqSlots) : 0;
|
||
uint32_t presetNameHashSlot = numFreqSlots ? (hash(presetNameDisplay) % numFreqSlots) : 0;
|
||
|
||
if (loraConfig.override_frequency == 0) {
|
||
|
||
// Check if we use the default frequency slot
|
||
// overrideSlot: 0 = channel hash, -1 = preset hash, >0 = explicit slot
|
||
uses_default_frequency_slot =
|
||
(loraConfig.channel_num == 0) || // user choice unset, no frequency override, so use default
|
||
(newRegion->overrideSlot > 0 &&
|
||
loraConfig.channel_num == newRegion->overrideSlot) || // user setting matches explicit override slot
|
||
((newRegion->overrideSlot == OVERRIDE_SLOT_DEFAULT_CHANNEL_HASH) &&
|
||
((uint32_t)(loraConfig.channel_num - 1) == channelNameHashSlot)) || // user setting matches channel name hash
|
||
((newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) &&
|
||
((uint32_t)(loraConfig.channel_num - 1) == presetNameHashSlot)); // user setting matches preset name hash
|
||
|
||
// 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 (newRegion->overrideSlot > 0) { // clamp to explicit override slot
|
||
loraConfig.channel_num = newRegion->overrideSlot; // use the explicit override slot defined for this region
|
||
uses_default_frequency_slot = true;
|
||
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH && loraConfig.use_preset) {
|
||
// clamp to preset name hash
|
||
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 (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);
|
||
}
|
||
// Clamp at the source so numFreqSlots below can never be 0 (a bandwidth-0 config may already be persisted)
|
||
bw = clampBandwidthKHz(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).
|
||
const char *channelName = channels.getName(channels.getPrimaryIndex());
|
||
// Guard the modulo: numFreqSlots can be 0 for an UNSET/degenerate region, and % 0 is a SIGFPE
|
||
uint32_t channelNameHashSlot = numFreqSlots ? (hash(channelName) % numFreqSlots) : 0;
|
||
uint32_t presetNameHashSlot =
|
||
numFreqSlots
|
||
? (hash(DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset)) %
|
||
numFreqSlots)
|
||
: 0;
|
||
|
||
// 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) {
|
||
// Handle three override slot cases: explicit slot (>0), preset hash (-1), or channel hash (0)
|
||
if (newRegion->overrideSlot > 0) {
|
||
channel_num = newRegion->overrideSlot - 1; // explicit override slot (1-based to 0-based)
|
||
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) {
|
||
channel_num = presetNameHashSlot; // use preset name hash
|
||
} else {
|
||
channel_num = channelNameHashSlot; // use channel name hash (default case)
|
||
}
|
||
} 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);
|
||
|
||
if (newRegion->wideLora) { // clamp if wide freq range
|
||
preambleLength = wideLoraPreambleLengthDefault; // 12 is the default for operation above 2GHz
|
||
} else {
|
||
preambleLength =
|
||
preambleLengthDefault; // 8 is default, but we use longer to increase the amount of sleep time when receiving
|
||
}
|
||
|
||
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: %u x %.3fkHz", numFreqSlots, bw);
|
||
if (newRegion->overrideSlot > 0) {
|
||
LOG_INFO("Using region explicit override slot: %d", newRegion->overrideSlot);
|
||
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) {
|
||
LOG_INFO("Using region preset name hash for slot selection");
|
||
}
|
||
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);
|
||
}
|