mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-01 18:55:28 -04:00
* fix(radio): put the beacon restore back inside completeSending's if (p) Reverts the RadioLibInterface and RadioInterface changes from #11573 (ac330e6a6). Hoisting MeshBeaconModule::reconfigureForBeaconTX() out of the if (p) block changed its meaning from "a send completed" to "the radio went to standby, for any reason" - and every driver's setStandby() calls completeSending() unconditionally: on the pre-TX LBT scan, on startReceive(), and inside reconfigure(). Two shipping faults followed, both confirmed on hardware the next day. Every beacon transmitted on the wrong preset. isChannelActive() standbys the radio immediately before each transmit, so the restore ran between the switch and the key-up. The packet went out carrying the beacon channel hash with home modem settings - inaudible to listeners on the target preset, an unknown hash to listeners on the home one. Inert in both directions. And unbounded recursion: the restore calls iface->reconfigure(), which standbys, which calls completeSending(), which restores again, each level running a full applyModemConfig(). It terminated in a HardFault and a silent reboot (Reset reason 0x4 on nRF52, no panic output). The crash masked the misdirection - the node died before Started Tx, so the wrong preset was invisible until the recursion was fixed. completeSending() clears sendingPacket at the top, so any nested call sees p == NULL. The if (p) block was an accidental re-entrancy guard, and nothing named it as such; removing it created both faults at once. Name it now. This also reverts the beginSending() failure return that motivated the move, and the startSend() scaffolding built to reach the restore on that path. The payload bounds check it replaced is reinstated in the next commit, at a point where refusing a packet is already a supported outcome. * fix(radio): bound the payload at the radio queue, not mid-transmit #11573 replaced beginSending()'s assert with a runtime check that logged, released the packet and returned 0. beginSending() had never returned 0 before, so startSend() gained a failure path it had to unwind - and the release moved ownership of the packet out of the caller that held it. That new return value is what made hoisting the beacon restore look necessary. The check itself is worth keeping. MeshPacket.encrypted has a nanopb maximum of 256 bytes against a 240-byte radio buffer, and beginSending() is on the path for relayed frames and phone-sourced packets, neither under our control. Asserts are commonly compiled out in release builds, so what shipped was an unchecked 256-into-240 memcpy driven by remote input. Move it to Router::send(), immediately before iface->send(p) - the single funnel for every over-the-air transmit. Refusing a packet there is already a supported outcome: it returns TOO_LARGE, which is what perhapsEncode() already returns for the same condition on the decoded path, and releases or NAKs exactly as the duty-cycle limit above it does. Nothing radio-side has happened at that point, so there is no half-started transmit to tear back down. perhapsEncode()'s existing check does not cover this case: relayed and phone-sourced frames arrive already encrypted and never reach it. beginSending() keeps a last line of defence, but clamps rather than failing, so it stays a call that always succeeds. Adds MAX_RADIO_PAYLOAD_LEN so both sites name the same number instead of recomputing it. Nothing about a beacon can trigger any of this - broadcast_message is admin-truncated to 100 bytes, the whole MeshBeacon protobuf tops out at 180, and observed beacons run to 106 - which is why this is separated from the beacon changes rather than carried with them. Tests: Router::send() refuses an oversized payload and still sends one that exactly fills the buffer; beginSending() clamps instead of rejecting, and leaves ordinary traffic whole. * fix(beacon): guard the radio switch/restore against re-entry and early restore Two checks in reconfigureForBeaconTX(), both independent of radio state, so the switch/restore state machine no longer rests on sendingPacket's lifetime - which is exactly the implicit coupling that let #11573 through. A re-entrancy guard. Both branches end in iface->reconfigure(), whose setStandby() runs completeSending(), which calls straight back in here. While one call is applying a config, a nested call returns false and leaves it alone. This covers the switch branch too, which had the same exposure with a quieter symptom: a second switch before the restore would take the re-entrant call as a restore and undo the switch still being applied, sending the beacon on the home channel instead of its target. A restore gate. The restore now waits for the packet that armed the switch to actually finish, tracked by id against our own target table rather than by asking the radio. Every caller that completes or abandons a beacon clears that packet's target settings first, so a live entry means the TX has not happened yet. cancelSending() now clears too, which is what keeps a cancelled beacon from pinning the radio on the beacon config. Together these make explicit the invariant completeSending()'s if (p) block was carrying by accident: a future hoist of that call gets a logged no-op instead of a crash and a misdirected beacon. Also sets radioSwitched before reconfigure() rather than after, in both branches, so the flag never describes a radio state that is not yet true. Diagnostics, because every step of this dance was previously silent about its own state. Count consecutive switches with no restore between them and log the depth on both sides, so a change-change-change-restore run reads off the log; switch #2 onwards prints the held home snapshot, which is the value that has to survive a second switch. The restore names the config it is restoring to, so a stale snapshot is visible directly. The re-entrancy guard logs when it fires - expected exactly twice per beacon, so a burst means something new is re-entering rather than a silent reboot. And setTargetRadioSettings() now warns on the slot eviction that previously left a packet to key up on whatever config was running - no crash, no log, wrong channel. Reachable only with beacon broadcast enabled (the default flags are LISTEN_ENABLED | LEGACY_SPLIT, so broadcast is off) and a target differing from the running config; an identical target takes the early return and never switches. Tests: three re-entrancy cases against a RadioInterface whose reconfigure() re-enters exactly as completeSending() does - bounded, so a regression fails an assertion instead of overflowing the stack and taking the runner with it - plus a restore that must defer until the beacon it switched for completes. * fix(beacon,radio): address review findings on #11596 Payload ceiling was one byte too generous. RadioBuffer::payload is 240 bytes because the buffer reserves MAX_LORA_PAYLOAD_LEN + 1, but the PHY caps a whole frame at 255 and beginSending() adds a 16-byte header - so a 240-byte payload produced a 256-byte frame. Define the ceiling as MAX_LORA_PAYLOAD_LEN - sizeof(PacketHeader), matching what perhapsEncode() already enforces, with a static_assert that it still fits the buffer. Target-table eviction could unblock the restore gate. With every slot live, setTargetRadioSettings() overwrote slot 0 - and if that slot held the packet the outstanding switch is gated on, the restore came unblocked and put the home config back under a beacon that had not keyed up. Skip that entry when choosing a victim, and refuse the target outright if every slot is in flight. Needs radioSwitched/switchedForId at file scope so the setter can see them. Restore on every abandon path, not just the clear. cancelSending() dropped a queued packet's target without restoring, so a beacon pre-switched by onNotify() and then cancelled left the radio receiving on the beacon config; removePendingTXPacket() did neither. Both now route through abandonBeaconTarget(), as does startSend()'s tx-disabled branch. The restore gate makes it a no-op when the abandoned packet is not the one we switched for. No NAK on the oversize drop. p->channel is a wire hash by that point, not an index, and Channels::getIndexByHash() is declared but never defined. Only already-encrypted ingress can reach the gate anyway - perhapsEncode() bounds everything it encodes - and those carry no index to answer on. Release and log. Tests clear sendingPacket before releasing their packet, and assert against the payload ceiling rather than the buffer size. * fix(beacon): route the invalid-target drop through abandonBeaconTarget onNotify()'s invalid-config drop was the one packet-abandonment path still clearing the target directly instead of going through abandonBeaconTarget(), so a packet that armed the radio switch and then failed validation would be released with the radio left on the beacon config and nothing to restore it. The helper's restore gate (targetRadioSettingsLive(switchedForId)) makes the call a no-op for any packet that did not arm the switch, so this closes the gap without risking a premature restore. Also trims the switch-state comment to the two-line limit. * fix(radio): take the abandoned packet as a pointer to const cppcheck's constParameterPointer failed the check matrix on every board: abandonBeaconTarget() only forwards the packet to clearTargetRadioSettings(), which already takes a const pointer, so the parameter should be const too. * refactor(radio): drive the beacon radio switch through TX hooks RadioLibInterface named MeshBeaconModule at six call sites behind MESHTASTIC_EXCLUDE_BEACON guards, so the driver carried per-packet beacon state: when to switch preset, when a target config was invalid mid-transmit, and when not to listen on a busy channel. Review on #11596 asked for the module dependency to come out. RadioTxHook is what the driver knows instead - beforeTransmit() returning send/defer/drop, holdsRadio(), packetReleased() - on a self-registering intrusive list, so nothing is allocated and a build without the beacon module registers nothing and every call is a no-op. The four abandon paths (cancel, remove-pending, TX disabled, completeSending) collapse onto one packetReleased(), and the tri-state means the driver no longer has to know why a packet wanted a re-delay or a drop. MeshBeaconTxHook wraps the existing statics; the switch/restore logic, its re-entrancy guard and its restore gate are untouched. It is created in Modules.cpp inside the existing exclusion guard, so MESHTASTIC_EXCLUDE_BEACON now works by nothing registering rather than by #ifdefs in the driver. Behaviour is unchanged. The invalid-config LOG_DEBUG moves into the module and the driver logs a generic refusal. Four tests cover the send/defer/drop mapping and that an empty hook list is a no-op; native:test_mesh_beacon is 59/59. Also notes in sendBeaconPacket that beacons uplink to MQTT on the primary slot's uplink_enabled, and that the topic follows the beacon channel under the crypto-override swap - both intentional. * fix(beacon): restore the home config for a packet that jumps the queue The restore gate added in9cb7b96c9refused to put the home config back while the beacon that armed the switch was still live. That is right for a release - completeSending() runs on every setStandby(), and restoring there would undo the switch before the beacon had keyed up - but it also caught the case where the driver is asking about a different packet it is about to transmit. MeshPacketQueue::enqueue() inserts by priority (std::upper_bound over CompareMeshPacketFunc), so an ACK or routing packet queued during the beacon's deferred transmit delay lands ahead of it. beforeTransmit() then saw an untagged packet, found the beacon still queued, skipped the restore and returned PRETX_SEND - and the packet transmitted on the beacon's preset, slot and region. It was encrypted and hashed for the home channel, so no receiver on either preset could use it. Apply the gate only to a null p. A non-null untagged packet is the driver about to key up, which always restores; the restore returns PRETX_DEFER, so the driver re-runs the delay and the channel scan on the config it will actually transmit on. beforeTransmit() is the only caller that passes a non-null untagged packet, so nothing else changes. Found by CodeRabbit on #11596. native:test_mesh_beacon 60/60, including a regression test for the queue transition; the four re-entrancy tests still cover the null-p gate. --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
1535 lines
67 KiB
C++
1535 lines
67 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), PRESET(MEDIUM_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};
|
||
|
||
// The EU_868/EU_866/EU_N_868 trio share the 868 band but own mutually exclusive preset
|
||
// profiles. Selecting a preset locked to a sibling swaps the region to that sibling (see
|
||
// regionSwapForPreset), so from any region in the trio every one of these presets is
|
||
// selectable. This union is what we advertise to clients as the trio's legal list. It is a
|
||
// display-only superset: on-device enforcement still uses each region's own disjoint
|
||
// profile->presets, so this must never be assigned to a RegionProfile (that would make
|
||
// supportsPreset() accept the preset in place and defeat the swap). Keep in sync with the
|
||
// EU_868/EU_866/EU_N_868 profile lists below. Sized to the 11-preset wire cap.
|
||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_SUPERSET[] = {
|
||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW), PRESET(SHORT_FAST),
|
||
PRESET(LONG_MODERATE), PRESET(LITE_FAST), PRESET(LITE_SLOW), PRESET(NARROW_FAST), PRESET(NARROW_SLOW), MODEM_PRESET_END};
|
||
|
||
// 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,
|
||
};
|
||
|
||
static bool isSwappableEuRegion(meshtastic_Config_LoRaConfig_RegionCode code)
|
||
{
|
||
for (auto c : SWAPPABLE_EU_REGIONS)
|
||
if (c == code)
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
// 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
|
||
https://zakon.rada.gov.ua/laws/show/262-2026-п
|
||
*/
|
||
RDEF(UA_433, 433.0f, 434.7f, 10, 10, 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)
|
||
#if defined(HAS_SDCARD) && defined(SDCARD_USE_SPI1)
|
||
extern SPIClass &SPI1; // alias for SPI_HSPI; both on SPI2_HOST
|
||
#else
|
||
extern SPIClass SPI1;
|
||
#endif
|
||
#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_lr2021:
|
||
return std::unique_ptr<RadioInterface>(new LR2021Interface(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.get(); // non-owning: the ch341 HAL stays owned by the global unique_ptr
|
||
} 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;
|
||
}
|
||
|
||
bool isKnownModemPreset(meshtastic_Config_LoRaConfig_ModemPreset preset)
|
||
{
|
||
// Walks profile->presets directly rather than RegionInfo::supportsPreset(), which calls
|
||
// back here for the UNSET entry. UNSET terminates the table, so it is checked last.
|
||
for (const RegionInfo *r = regions;; r++) {
|
||
for (size_t i = 0; r->profile->presets[i] != MODEM_PRESET_END; i++)
|
||
if (r->profile->presets[i] == preset)
|
||
return true;
|
||
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
||
return false;
|
||
}
|
||
}
|
||
|
||
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; rest 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;
|
||
// EU 86x siblings advertise the trio's superset - any of those presets is
|
||
// reachable from here via an automatic region swap. Every other region
|
||
// advertises exactly its own enforced profile list.
|
||
const meshtastic_Config_LoRaConfig_ModemPreset *advertised =
|
||
isSwappableEuRegion(r->code) ? PRESETS_EU_SUPERSET : r->profile->presets;
|
||
for (size_t i = 0; advertised[i] != MODEM_PRESET_END && grp.presets_count < maxPresets; i++)
|
||
grp.presets[grp.presets_count++] = advertised[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;
|
||
}
|
||
|
||
#ifdef USERPREFS_LORACONFIG_MODEM_PRESET
|
||
// A pinned preset is a statement of intent, not enforcement: supportsPreset() still accepts any
|
||
// known preset while unset. Stock builds emit no UNSET entry, which clients read as unconstrained.
|
||
if (map.groups_count < maxGroups && map.region_groups_count < maxRegions) {
|
||
const RegionInfo *unset = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
||
meshtastic_LoRaPresetGroup &grp = map.groups[map.groups_count];
|
||
grp.presets_count = 1;
|
||
grp.presets[0] = USERPREFS_LORACONFIG_MODEM_PRESET;
|
||
grp.default_preset = USERPREFS_LORACONFIG_MODEM_PRESET;
|
||
grp.licensed_only = unset->profile->licensedOnly;
|
||
|
||
meshtastic_LoRaRegionPresets &rg = map.region_groups[map.region_groups_count++];
|
||
rg.region = unset->code;
|
||
rg.group_index = (uint8_t)map.groups_count++;
|
||
} else {
|
||
// Costs only the intent signal - clients fall back to unconstrained - but must not be silent.
|
||
LOG_ERROR("Region preset map full; UNSET intent omitted");
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* 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 in packet. Router: 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 in packet. 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->has_rx_time) // rx_time has explicit presence; a millis() placeholder isn't a real reading to print
|
||
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->has_rx_rssi) // rx_rssi has explicit presence; a != 0 check would hide a genuine 0 dBm reading
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* 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)
|
||
{
|
||
if (!isSwappableEuRegion(currentRegion))
|
||
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,
|
||
bool prospectiveLicensedOwner)
|
||
{
|
||
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 && !prospectiveLicensedOwner) {
|
||
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, defer 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 above custom setting, use %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, can't apply modem config - fall 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("Region explicit override slot: %d", newRegion->overrideSlot);
|
||
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) {
|
||
LOG_INFO("Use region preset name hash for slot");
|
||
}
|
||
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 Tx power: 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 too high, set 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);
|
||
|
||
// Oversize is rejected at the radio queue in Router::send(); clamp rather than fail here so this
|
||
// stays a call that always succeeds, with no failure return for startSend() to unwind.
|
||
size_t payloadLen = p->encrypted.size;
|
||
if (payloadLen > MAX_RADIO_PAYLOAD_LEN) {
|
||
LOG_ERROR("Payload %u exceeds radioBuffer capacity %u, truncate", (unsigned)payloadLen, (unsigned)MAX_RADIO_PAYLOAD_LEN);
|
||
payloadLen = MAX_RADIO_PAYLOAD_LEN;
|
||
}
|
||
|
||
memcpy(radioBuffer.payload, p->encrypted.bytes, payloadLen);
|
||
|
||
sendingPacket = p;
|
||
return payloadLen + sizeof(PacketHeader);
|
||
}
|