mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-07 06:44:33 -05:00
* fix(radio): recover from chip state loss in the RX/TX hot paths too * fix(radio): address CodeRabbit findings on the hot-path recovery PR (#11680) * fix(radio): address CodeRabbit findings on the hot-path recovery PR SX128x: startReceive() still called the old asserting setStandby() before the new trySetStandby(). The assert fired first, so the recovery path added below it could never run - the exact chip-state-loss crash this PR exists to fix was still live on SX128x. Remove the stale call. LR11x0: resolvedTcxoVoltage was set once after the primary begin() attempts, but two later paths - firmware recovery and the one-shot firmware update - call begin() again with tcxoVoltage and never updated it. On a TCXO_OPTIONAL board that only came up via one of those paths, reinitChip() would recover with the wrong oscillator setting. Update resolvedTcxoVoltage after each of those begin() calls too. LR20x0: reconfigure() discarded RadioLibInterface::reconfigure()'s result - the band-hop path always returned true regardless, and the same-band path reused the same flag for chip-programming errors, so a base-class failure could both mask itself as success and wrongly trigger a full re-init. Track the base-class result (reconfigureSuccess) separately from the chip result (standbySuccess), and return the former. Also shortens the recovery-rationale comments in RadioLibInterface.h and SX126xInterface.cpp to 1-2 lines per the repo's comment convention, the rationale now covered once in the base class. * fix(radio): finish the recovery ladder and stop recovery from rebooting Follow-up to the CodeRabbit findings, plus two gaps found auditing the branch against its own intent (never reboot on chip state loss; recover in place). RX left off was unrecoverable on an idle node. Every startReceive() call site is event-driven - RX/TX ISR, the CAD-busy branch, startSend()'s failure path, init(), reconfigure() - and a radio with RX off cannot raise an RX interrupt, so nothing re-arms it unless the node happens to transmit or the user changes config. A listen-only or quiet node stayed deaf for good, which is worse than the reboot this replaced. main.cpp's existing 60 s AGC tick now calls periodicRadioMaintenance(), which re-arms RX when rxOffline is set and otherwise does the AGC reset as before. In-place repair now gives up rather than retrying forever. After MAX_CHIP_RECOVERY_FAILURES consecutive failures - a throttle window apart, so minutes of a provably dead chip - schedule rebootAtMsec, the same deliberate reboot Portduino already uses for LoRa_in_error. A reboot re-runs init(), which redoes the power-enable GPIOs, settle delays and TCXO probing that begin() alone skips. Both counters reset in RadioLibInterface::startReceive(), the one point every driver reaches only once the chip accepts the RX start. SX128x: reconfigure()'s recovery reached reinitChip()'s region-mismatch branch, which rewrites config.lora.region, saves, and calls ESP.restart() / NVIC_SystemReset(). A runtime recovery must never reboot - that is the crash this path exists to prevent, and it would fire with a config save pending. Gated to the boot-time call via a fromInit parameter. LR20x0: a rejected setRxBoostedGainMode cleared the success flag and so forced a full fullBegin() chip reset. It is a warn-level cosmetic setting, treated as warn-only in LR11x0's equivalent, and not a lost-state signature. Also logs suppressed recovery attempts at debug level; previously a chip that stayed dead recorded one critical error and then went completely silent. * fix(radio): count RX re-arms, not re-inits, in the recovery ladder LR20x0's recoverChipStateLoss() is fullBegin(), which re-arms RX itself but reports success on begin() alone. A re-init that came back with RX still dead therefore reset chipRecoveryFailures, so a chip that could be re-inited forever while never receiving again held the ladder at zero and never reached the reboot. The other drivers had the same hole from the other side: the caller's retry startReceive() runs after the reset, so a retry that failed again left the count cleared. RadioLibInterface::startReceive() is now the only place the ladder clears, and it only runs once the chip actually accepted RX. The threshold is judged at the top of the next attempt - a throttle window later, after that attempt's retry (the caller's, or fullBegin's own) has had its chance to clear it. That also drops the old false positive where the reboot was armed before the retry that would have succeeded. RF95Interface::startReceive() set isReceiving directly instead of calling the base, so on RF95 nothing ever cleared rxOffline or the ladder: the first failed RX start left periodicRadioMaintenance() re-initing forever, and with the count now advancing it would have rebooted a working radio. --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> --------- Co-authored-by: Tom <116762865+NomDeTom@users.noreply.github.com>
590 lines
23 KiB
C++
590 lines
23 KiB
C++
#if RADIOLIB_EXCLUDE_LR11X0 != 1
|
|
#include "LR11x0Interface.h"
|
|
#include "Throttle.h"
|
|
#include "configuration.h"
|
|
#include "error.h"
|
|
#include "mesh/NodeDB.h"
|
|
|
|
// A variant may define LR11X0_UPDATE_FIRMWARE_TO to a Semtech transceiver firmware version (e.g. 0x0402) to
|
|
// bake that image in and update the radio on first boot. Every supported image is 61320 words, so this costs
|
|
// ~240 kB of flash regardless of the version chosen - only enable it on a variant with the headroom, and
|
|
// only for as long as it takes to update the affected units.
|
|
#ifdef LR11X0_UPDATE_FIRMWARE_TO
|
|
#if LR11X0_UPDATE_FIRMWARE_TO == 0x0402
|
|
#define RADIOLIB_LR1110_FIRMWARE_0402
|
|
#elif LR11X0_UPDATE_FIRMWARE_TO == 0x0401
|
|
#define RADIOLIB_LR1110_FIRMWARE_0401
|
|
#elif LR11X0_UPDATE_FIRMWARE_TO == 0x0307
|
|
#define RADIOLIB_LR1110_FIRMWARE_0307
|
|
#else
|
|
// Note: RadioLib ships lr1110_transceiver_0308.h but has no selector for it in LR11x0_firmware.h.
|
|
#error "LR11X0_UPDATE_FIRMWARE_TO must be one of 0x0307, 0x0401, 0x0402"
|
|
#endif
|
|
#include <modules/LR11x0/LR11x0_firmware.h>
|
|
#endif
|
|
|
|
#ifdef LR11X0_DIO_AS_RF_SWITCH
|
|
#include "rfswitch.h"
|
|
#elif ARCH_PORTDUINO
|
|
#include "PortduinoGlue.h"
|
|
#define rfswitch_dio_pins portduino_config.rfswitch_dio_pins
|
|
#define rfswitch_table portduino_config.rfswitch_table
|
|
#else
|
|
static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
|
|
static const Module::RfSwitchMode_t rfswitch_table[] = {
|
|
{LR11x0::MODE_STBY, {}}, {LR11x0::MODE_RX, {}}, {LR11x0::MODE_TX, {}}, {LR11x0::MODE_TX_HP, {}},
|
|
{LR11x0::MODE_TX_HF, {}}, {LR11x0::MODE_GNSS, {}}, {LR11x0::MODE_WIFI, {}}, END_OF_MODE_TABLE,
|
|
};
|
|
#endif
|
|
|
|
// Particular boards might define a different max power based on what their hardware can do, default to max power output if not
|
|
// specified (may be dangerous if using external PA and LR11x0 power config forgotten)
|
|
#if ARCH_PORTDUINO
|
|
#define LR1110_MAX_POWER portduino_config.lr1110_max_power
|
|
#endif
|
|
#ifndef LR1110_MAX_POWER
|
|
#define LR1110_MAX_POWER 22
|
|
#endif
|
|
|
|
// the 2.4G part maxes at 13dBm
|
|
#if ARCH_PORTDUINO
|
|
#define LR1120_MAX_POWER portduino_config.lr1120_max_power
|
|
#endif
|
|
#ifndef LR1120_MAX_POWER
|
|
#define LR1120_MAX_POWER 13
|
|
#endif
|
|
|
|
// Vref to assume for a board that declares a TCXO may be fitted without saying at what voltage.
|
|
// "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per
|
|
// https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104
|
|
#if defined(TCXO_OPTIONAL)
|
|
#define LR11X0_TCXO_DEFAULT_VOLTAGE 1.6f
|
|
#else
|
|
#define LR11X0_TCXO_DEFAULT_VOLTAGE 0
|
|
#endif
|
|
|
|
// A chip that never answers can surface either way depending on where RadioLib gave up: a bounded
|
|
// per-command BUSY wait in Module::SPItransferStream() reports SPI_CMD_TIMEOUT rather than
|
|
// SPI_CMD_FAILED, so both have to count as "the chip did not talk to us"
|
|
static inline bool lr11x0SpiFailed(int res)
|
|
{
|
|
return res == RADIOLIB_ERR_SPI_CMD_FAILED || res == RADIOLIB_ERR_SPI_CMD_TIMEOUT;
|
|
}
|
|
|
|
template <typename T>
|
|
LR11x0Interface<T>::LR11x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
|
RADIOLIB_PIN_TYPE busy)
|
|
: RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module)
|
|
{
|
|
LOG_WARN("LR11x0Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy);
|
|
}
|
|
|
|
/// Initialise the Driver transport hardware and software.
|
|
/// Make sure the Driver is properly configured before calling init().
|
|
/// \return true if initialisation succeeded.
|
|
template <typename T> bool LR11x0Interface<T>::init()
|
|
{
|
|
#ifdef LR11X0_POWER_EN
|
|
pinMode(LR11X0_POWER_EN, OUTPUT);
|
|
digitalWrite(LR11X0_POWER_EN, HIGH);
|
|
#endif
|
|
|
|
// An explicit Vref always wins; TCXO_OPTIONAL only supplies a default for boards that declare a
|
|
// TCXO may be fitted without saying at what voltage. Both may appear in the same variant file.
|
|
#if ARCH_PORTDUINO
|
|
// Portduino leaves dio3_tcxo_voltage at 0 whenever the YAML omits DIO3_TCXO_VOLTAGE, which is the
|
|
// "no explicit Vref" case, so the TCXO_OPTIONAL default still has to apply there
|
|
float tcxoVoltage =
|
|
portduino_config.dio3_tcxo_voltage > 0 ? (float)portduino_config.dio3_tcxo_voltage / 1000 : LR11X0_TCXO_DEFAULT_VOLTAGE;
|
|
#elif defined(LR11X0_DIO3_TCXO_VOLTAGE)
|
|
float tcxoVoltage = LR11X0_DIO3_TCXO_VOLTAGE;
|
|
#else
|
|
float tcxoVoltage = LR11X0_TCXO_DEFAULT_VOLTAGE;
|
|
#endif
|
|
|
|
// DIO3 is free to be used as an IRQ only while no TCXO Vref is driven on it
|
|
if (tcxoVoltage > 0)
|
|
LOG_DEBUG("LR11x0 TCXO Vref %f V on DIO3 (DIO3 unavailable as IRQ)", tcxoVoltage);
|
|
else
|
|
LOG_DEBUG("LR11x0 no TCXO Vref, XTAL only (DIO3 free as IRQ)");
|
|
#if defined(TCXO_OPTIONAL)
|
|
LOG_DEBUG("TCXO_OPTIONAL: osc type unknown, probe XTAL first, TCXO Vref as fallback");
|
|
#endif
|
|
|
|
RadioLibInterface::init();
|
|
|
|
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
|
|
limitPower(LR1120_MAX_POWER);
|
|
} else {
|
|
limitPower(LR1110_MAX_POWER); // default clamp for non-wide freq range
|
|
}
|
|
|
|
#ifdef LR11X0_RF_SWITCH_SUBGHZ
|
|
pinMode(LR11X0_RF_SWITCH_SUBGHZ, OUTPUT);
|
|
digitalWrite(LR11X0_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW);
|
|
LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
|
|
#endif
|
|
|
|
#ifdef LR11X0_RF_SWITCH_2_4GHZ
|
|
pinMode(LR11X0_RF_SWITCH_2_4GHZ, OUTPUT);
|
|
digitalWrite(LR11X0_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH);
|
|
LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
|
|
#endif
|
|
|
|
// Allow extra time for TCXO to stabilize after power-on
|
|
delay(10);
|
|
|
|
// Timestamped brackets so a hang inside RadioLib leaves a dangling "attempt" line in the boot log
|
|
auto tryBegin = [&](int attempt, float vref) {
|
|
uint32_t attemptStart = millis();
|
|
LOG_INFO("LR11x0 begin() attempt %d: tcxoVoltage=%.3fV at t=%ums", attempt, vref, attemptStart);
|
|
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, vref);
|
|
LOG_INFO("LR11x0 begin() attempt %d returned %d after %ums", attempt, res, millis() - attemptStart);
|
|
return res;
|
|
};
|
|
|
|
#if defined(TCXO_OPTIONAL)
|
|
// 1. XTAL, because a TCXO-first attempt hangs RadioLib's unbounded calibration wait on a module
|
|
// with no TCXO fitted, whereas XTAL fails fast and cleanly on a module that does have one
|
|
float attemptVoltage = 0;
|
|
#else
|
|
// 1. Whatever Vref the variant configured, which it declared unconditionally
|
|
float attemptVoltage = tcxoVoltage;
|
|
#endif
|
|
int res = tryBegin(1, attemptVoltage);
|
|
|
|
#if defined(TCXO_OPTIONAL)
|
|
// 2. XTAL failed with the chip present, so fall back to the TCXO if the variant configured one
|
|
if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) {
|
|
LOG_WARN("LR11x0 XTAL init failed (err %d), retry with TCXO Vref %f V", res, tcxoVoltage);
|
|
attemptVoltage = tcxoVoltage;
|
|
res = tryBegin(2, attemptVoltage);
|
|
if (res == RADIOLIB_ERR_NONE)
|
|
LOG_INFO("LR11x0 init success with TCXO Vref %f V", tcxoVoltage);
|
|
}
|
|
#endif
|
|
|
|
// 3. Some units need extra settling time, so give whichever oscillator we settled on one retry.
|
|
// After a step 2 fallback that is a second TCXO attempt, which is where settling actually matters.
|
|
if (lr11x0SpiFailed(res)) {
|
|
LOG_WARN("LR11x0 init failed with %d (SPI cmd failure), retry after delay", res);
|
|
delay(100);
|
|
res = tryBegin(3, attemptVoltage);
|
|
}
|
|
|
|
resolvedTcxoVoltage = attemptVoltage;
|
|
|
|
// \todo Display actual typename of the adapter, not just `LR11x0`
|
|
LOG_INFO("LR11x0 init result %d", res);
|
|
|
|
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || lr11x0SpiFailed(res)) {
|
|
#ifdef LR11X0_UPDATE_FIRMWARE_TO
|
|
// An interrupted update leaves the radio sitting in bootloader mode, where begin() fails. Retry the
|
|
// flash from here rather than giving up, otherwise the device could never recover on its own.
|
|
LOG_WARN("LR11x0 did not start; firmware recovery in case update was interrupted");
|
|
if (lora.updateFirmware(lr11xx_firmware_image, LR11XX_FIRMWARE_IMAGE_SIZE, true) == RADIOLIB_ERR_NONE) {
|
|
LOG_INFO("LR1110 firmware recovery OK, re-init radio");
|
|
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
|
if (res == RADIOLIB_ERR_NONE)
|
|
resolvedTcxoVoltage = tcxoVoltage;
|
|
}
|
|
#endif
|
|
if (res != RADIOLIB_ERR_NONE)
|
|
return false;
|
|
}
|
|
|
|
LR11x0VersionInfo_t version;
|
|
res = lora.getVersionInfo(&version);
|
|
if (res == RADIOLIB_ERR_NONE) {
|
|
LOG_DEBUG("LR11x0 Device %d, HW %d, FW %d.%d, WiFi %d.%d, GNSS %d.%d", version.device, version.hardware, version.fwMajor,
|
|
version.fwMinor, version.fwMajorWiFi, version.fwMinorWiFi, version.fwGNSS, version.almanacGNSS);
|
|
transceiverFw = ((uint16_t)version.fwMajor << 8) | version.fwMinor;
|
|
transceiverDevice = version.device;
|
|
}
|
|
|
|
#ifdef LR11X0_UPDATE_FIRMWARE_TO
|
|
// One-shot transceiver firmware update, opt-in per variant. Only runs when the part is an LR1110 running
|
|
// older firmware than the baked-in image, so once it has succeeded it is a no-op on subsequent boots.
|
|
if (transceiverDevice == RADIOLIB_LR11X0_DEVICE_LR1110 && transceiverFw != 0 && transceiverFw < LR11X0_UPDATE_FIRMWARE_TO) {
|
|
LOG_WARN("LR1110 transceiver FW %d.%d older than %d.%d - updating. DO NOT POWER OFF: "
|
|
"rewrites radio's own flash",
|
|
transceiverFw >> 8, transceiverFw & 0xFF, LR11X0_UPDATE_FIRMWARE_TO >> 8, LR11X0_UPDATE_FIRMWARE_TO & 0xFF);
|
|
|
|
int upd = lora.updateFirmware(lr11xx_firmware_image, LR11XX_FIRMWARE_IMAGE_SIZE, true);
|
|
if (upd != RADIOLIB_ERR_NONE) {
|
|
// The radio is likely sitting in bootloader mode. It is not bricked - the update is retried on
|
|
// the next boot because the version check above will still see old (or unreadable) firmware.
|
|
LOG_ERROR("LR1110 firmware update FAILED %s%d - power-cycle to retry", radioLibErr, upd);
|
|
return false;
|
|
}
|
|
|
|
LOG_INFO("LR1110 firmware update complete, re-init radio");
|
|
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
|
if (res != RADIOLIB_ERR_NONE) {
|
|
LOG_ERROR("LR11x0 re-init after firmware update failed %s%d", radioLibErr, res);
|
|
return false;
|
|
}
|
|
resolvedTcxoVoltage = tcxoVoltage;
|
|
|
|
if (lora.getVersionInfo(&version) == RADIOLIB_ERR_NONE) {
|
|
transceiverFw = ((uint16_t)version.fwMajor << 8) | version.fwMinor;
|
|
transceiverDevice = version.device;
|
|
LOG_INFO("LR1110 now running transceiver FW %d.%d", version.fwMajor, version.fwMinor);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
LOG_INFO("Frequency set to %f", getFreq());
|
|
LOG_INFO("Bandwidth set to %f", bw);
|
|
LOG_INFO("Power output set to %d", power);
|
|
|
|
if (res == RADIOLIB_ERR_NONE)
|
|
res = lora.setCRC(2);
|
|
|
|
// FIXME: May want to set depending on a definition, currently all LR1110 variant files use the DC-DC regulator option
|
|
if (res == RADIOLIB_ERR_NONE)
|
|
res = lora.setRegulatorDCDC();
|
|
|
|
#ifdef LR11X0_DIO_AS_RF_SWITCH
|
|
bool dioAsRfSwitch = true;
|
|
#elif defined(ARCH_PORTDUINO)
|
|
bool dioAsRfSwitch = portduino_config.has_rfswitch_table;
|
|
#else
|
|
bool dioAsRfSwitch = false;
|
|
#endif
|
|
|
|
if (dioAsRfSwitch) {
|
|
lora.setRfSwitchTable(rfswitch_dio_pins, rfswitch_table);
|
|
LOG_DEBUG("Set DIO RF switch");
|
|
}
|
|
|
|
if (res == RADIOLIB_ERR_NONE) {
|
|
if (config.lora.sx126x_rx_boosted_gain) { // the name is unfortunate but historically accurate
|
|
res = lora.setRxBoostedGainMode(true);
|
|
LOG_INFO("Set RX gain to boosted mode; result: %d", res);
|
|
} else {
|
|
res = lora.setRxBoostedGainMode(false);
|
|
LOG_INFO("Set RX gain to power saving mode; result: %d", res);
|
|
}
|
|
}
|
|
|
|
if (res == RADIOLIB_ERR_NONE)
|
|
startReceive(); // start receiving
|
|
|
|
return res == RADIOLIB_ERR_NONE;
|
|
}
|
|
|
|
template <typename T> int16_t LR11x0Interface<T>::programModemParams()
|
|
{
|
|
// configure publicly accessible settings
|
|
int16_t err = lora.setSpreadingFactor(sf);
|
|
if (err != RADIOLIB_ERR_NONE) {
|
|
LOG_ERROR("LR11x0 setSpreadingFactor(%u) %s%d", sf, radioLibErr, err);
|
|
return err;
|
|
}
|
|
|
|
err = lora.setBandwidth(bw, wideLora() && (getFreq() > 1000.0f));
|
|
if (err != RADIOLIB_ERR_NONE) {
|
|
LOG_ERROR("LR11x0 setBandwidth(%.1f) %s%d", bw, radioLibErr, err);
|
|
return err;
|
|
}
|
|
|
|
err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it
|
|
if (err != RADIOLIB_ERR_NONE) {
|
|
LOG_ERROR("LR11x0 setCodingRate(%u) %s%d", cr, radioLibErr, err);
|
|
return err;
|
|
}
|
|
|
|
err = lora.setSyncWord(syncWord);
|
|
if (err != RADIOLIB_ERR_NONE) {
|
|
LOG_ERROR("LR11x0 setSyncWord %s%d", radioLibErr, err);
|
|
return err;
|
|
}
|
|
|
|
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
|
|
limitPower(LR1120_MAX_POWER);
|
|
} else {
|
|
limitPower(LR1110_MAX_POWER); // default clamp for non-wide freq range
|
|
}
|
|
|
|
err = lora.setPreambleLength(preambleLength);
|
|
if (err != RADIOLIB_ERR_NONE) {
|
|
LOG_ERROR("LR11x0 setPreambleLength(%u) %s%d", preambleLength, radioLibErr, err);
|
|
return err;
|
|
}
|
|
|
|
err = lora.setFrequency(getFreq());
|
|
if (err != RADIOLIB_ERR_NONE) {
|
|
LOG_ERROR("LR11x0 setFrequency(%.3f) %s%d", getFreq(), radioLibErr, err);
|
|
return err;
|
|
}
|
|
|
|
err = lora.setOutputPower(power);
|
|
if (err != RADIOLIB_ERR_NONE) {
|
|
LOG_ERROR("LR11x0 setOutputPower(%d) %s%d", power, radioLibErr, err);
|
|
return err;
|
|
}
|
|
|
|
// Apply RX gain mode - valid in STDBY, matches resetAGC() pattern
|
|
err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
LOG_WARN("LR11x0 setRxBoostedGainMode %s%d", radioLibErr, err);
|
|
|
|
return RADIOLIB_ERR_NONE;
|
|
}
|
|
|
|
template <typename T> bool LR11x0Interface<T>::reinitChip()
|
|
{
|
|
// Clamp here, not just in programModemParams(): applyModemConfig() resets `power` to the raw
|
|
// config value, and the recovery path reaches begin() without passing through the params clamp
|
|
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
|
|
limitPower(LR1120_MAX_POWER);
|
|
} else {
|
|
limitPower(LR1110_MAX_POWER); // default clamp for non-wide freq range
|
|
}
|
|
|
|
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, resolvedTcxoVoltage);
|
|
if (res == RADIOLIB_ERR_NONE)
|
|
res = lora.setCRC(2);
|
|
if (res == RADIOLIB_ERR_NONE)
|
|
res = lora.setRegulatorDCDC();
|
|
|
|
#ifdef LR11X0_DIO_AS_RF_SWITCH
|
|
bool dioAsRfSwitch = true;
|
|
#elif defined(ARCH_PORTDUINO)
|
|
bool dioAsRfSwitch = portduino_config.has_rfswitch_table;
|
|
#else
|
|
bool dioAsRfSwitch = false;
|
|
#endif
|
|
|
|
// setRfSwitchTable() pushed the DIO switch config to the chip when init() called it; a reset chip has
|
|
// lost it and begin() does not restore it
|
|
if (res == RADIOLIB_ERR_NONE && dioAsRfSwitch)
|
|
lora.setRfSwitchTable(rfswitch_dio_pins, rfswitch_table);
|
|
|
|
if (res != RADIOLIB_ERR_NONE)
|
|
LOG_ERROR("LR11x0 re-init failed %s%d", radioLibErr, res);
|
|
return res == RADIOLIB_ERR_NONE;
|
|
}
|
|
|
|
template <typename T> bool LR11x0Interface<T>::reconfigure()
|
|
{
|
|
RadioLibInterface::reconfigure();
|
|
|
|
// set mode to standby - a chip that lost its state to a reset/brownout can time out here (-707),
|
|
// so don't let setStandby()'s assert fire before the recovery below gets a chance
|
|
int16_t err = trySetStandby();
|
|
if (err == RADIOLIB_ERR_NONE)
|
|
err = programModemParams();
|
|
|
|
if (err != RADIOLIB_ERR_NONE) {
|
|
// A chip that fails standby or rejects parameter programming (typically WRONG_MODEM, -20) has
|
|
// lost its runtime configuration - packet type included - to a chip-internal reset or brownout.
|
|
// Recover in place: begin() hardware-resets the chip and restores the LoRa packet type. Crashing
|
|
// here instead would reboot before MeshService persists the config change that triggered us.
|
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
|
LOG_ERROR("LR11x0 rejected modem params, chip state lost? Full re-init");
|
|
if (!reinitChip() || (err = programModemParams()) != RADIOLIB_ERR_NONE) {
|
|
LOG_ERROR("LR11x0 unrecoverable %s%d, radio down until reboot", radioLibErr, err);
|
|
return false;
|
|
}
|
|
LOG_INFO("LR11x0 recovered after re-init");
|
|
}
|
|
|
|
startReceive(); // restart receiving
|
|
|
|
return true;
|
|
}
|
|
|
|
template <typename T> void LR11x0Interface<T>::clearRadioIsr()
|
|
{
|
|
lora.clearIrqAction();
|
|
}
|
|
|
|
template <typename T> int16_t LR11x0Interface<T>::trySetStandby()
|
|
{
|
|
checkNotification(); // handle any pending interrupts before we force standby
|
|
|
|
int16_t err = lora.standby();
|
|
|
|
if (err != RADIOLIB_ERR_NONE) {
|
|
LOG_DEBUG("LR11x0 standby failed, err %d", err);
|
|
}
|
|
|
|
isReceiving = false; // If we were receiving, not any more
|
|
activeReceiveStart = 0;
|
|
disableInterrupt();
|
|
completeSending(); // If we were sending, not anymore
|
|
RadioLibInterface::setStandby();
|
|
return err;
|
|
}
|
|
|
|
template <typename T> void LR11x0Interface<T>::setStandby()
|
|
{
|
|
int16_t err = trySetStandby();
|
|
assert(err == RADIOLIB_ERR_NONE);
|
|
}
|
|
|
|
/**
|
|
* Add SNR data to received messages
|
|
*/
|
|
template <typename T> void LR11x0Interface<T>::addReceiveMetadata(meshtastic_MeshPacket *mp)
|
|
{
|
|
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
|
|
mp->rx_snr = lora.getSNR();
|
|
mp->rx_rssi = lround(lora.getRSSI());
|
|
mp->has_rx_rssi = true; // rx_rssi has explicit presence - a genuine reading must be marked present to survive encoding
|
|
LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError());
|
|
}
|
|
|
|
/** We override to turn on transmitter power as needed.
|
|
*/
|
|
template <typename T> void LR11x0Interface<T>::configHardwareForSend()
|
|
{
|
|
RadioLibInterface::configHardwareForSend();
|
|
}
|
|
|
|
// For power draw measurements, helpful to force radio to stay sleeping
|
|
// #define SLEEP_ONLY
|
|
|
|
template <typename T> void LR11x0Interface<T>::startReceive()
|
|
{
|
|
#ifdef SLEEP_ONLY
|
|
sleep();
|
|
#else
|
|
|
|
int16_t err = trySetStandby();
|
|
|
|
if (err == RADIOLIB_ERR_NONE) {
|
|
lora.setPreambleLength(preambleLength); // Solve RX ack fail after direct message sent. Not sure why this is needed.
|
|
|
|
// We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly.
|
|
err =
|
|
lora.startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
|
|
}
|
|
|
|
if (err != RADIOLIB_ERR_NONE) {
|
|
LOG_ERROR("StartReceive error: %d", err);
|
|
if (maybeRecoverChipStateLoss()) {
|
|
lora.setPreambleLength(preambleLength);
|
|
err = lora.startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS,
|
|
RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
|
|
}
|
|
}
|
|
|
|
if (err != RADIOLIB_ERR_NONE) {
|
|
// No assert: leave RX off rather than reboot; periodicRadioMaintenance() re-arms it, throttled
|
|
LOG_ERROR("LR11x0 RX offline %s%d", radioLibErr, err);
|
|
rxOffline = true;
|
|
return;
|
|
}
|
|
|
|
RadioLibInterface::startReceive();
|
|
|
|
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits
|
|
enableInterrupt(isrRxLevel0);
|
|
checkRxDoneIrqFlag();
|
|
#endif
|
|
}
|
|
|
|
/** Is the channel currently active? */
|
|
template <typename T> bool LR11x0Interface<T>::isChannelActive()
|
|
{
|
|
// check if we can detect a LoRa preamble on the current channel
|
|
ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD,
|
|
.detPeak = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT,
|
|
.detMin = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT,
|
|
.exitMode = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT,
|
|
.timeout = 0,
|
|
.irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS,
|
|
.irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}};
|
|
int16_t result = trySetStandby();
|
|
if (result == RADIOLIB_ERR_NONE) {
|
|
result = lora.scanChannel(cfg);
|
|
if (result == RADIOLIB_LORA_DETECTED)
|
|
return true;
|
|
if (result != RADIOLIB_ERR_WRONG_MODEM)
|
|
return false;
|
|
}
|
|
|
|
// standby failed or the LoRa modem type is gone - the chip lost its runtime state
|
|
maybeRecoverChipStateLoss();
|
|
return false; // report the channel free: a recovered chip can TX, a dead one fails startSend safely
|
|
}
|
|
|
|
/** Could we send right now (i.e. either not actively receiving or transmitting)? */
|
|
template <typename T> bool LR11x0Interface<T>::isActivelyReceiving()
|
|
{
|
|
// The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet
|
|
// received and handled the interrupt for reading the packet/handling errors.
|
|
return receiveDetected(lora.getIrqStatus(), RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID,
|
|
RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED);
|
|
}
|
|
|
|
#ifdef LR11X0_AGC_RESET
|
|
template <typename T> void LR11x0Interface<T>::resetAGC()
|
|
{
|
|
// Safety: don't reset mid-packet
|
|
if (sendingPacket != NULL || (isReceiving && isActivelyReceiving()))
|
|
return;
|
|
|
|
LOG_DEBUG("LR11x0 AGC reset: warm sleep + Calibrate(0x3F)");
|
|
|
|
// 1. Warm sleep - powers down the analog frontend, resetting AGC state
|
|
lora.sleep(true, 0);
|
|
|
|
// 2. Wake to RC standby for stable calibration
|
|
lora.standby(RADIOLIB_LR11X0_STANDBY_RC, true);
|
|
|
|
// 3. Calibrate all blocks (PLL, ADC, image, RC oscillators)
|
|
// calibrate() is protected on LR11x0, so use raw SPI (same as internal implementation)
|
|
uint8_t calData = RADIOLIB_LR11X0_CALIBRATE_ALL;
|
|
module.SPIwriteStream(RADIOLIB_LR11X0_CMD_CALIBRATE, &calData, 1, true, true);
|
|
|
|
// 4. Re-calibrate image rejection for actual operating frequency
|
|
// Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions.
|
|
lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f);
|
|
|
|
// 5. Re-apply RX boosted gain mode
|
|
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
|
|
|
// 6. Resume receiving
|
|
startReceive();
|
|
}
|
|
#endif
|
|
|
|
template <typename T> bool LR11x0Interface<T>::sleep()
|
|
{
|
|
// \todo Display actual typename of the adapter, not just `LR11x0`
|
|
LOG_DEBUG("LR11x0 entering sleep mode");
|
|
(void)trySetStandby(); // Stop any pending operations - the chip is being put to sleep, a failure must not crash
|
|
|
|
// turn off TCXO if it was powered
|
|
lora.setTCXO(0);
|
|
|
|
// put chipset into sleep mode (we've already disabled interrupts by now)
|
|
bool keepConfig = false;
|
|
lora.sleep(keepConfig, 0); // Note: we do not keep the config, full reinit will be needed
|
|
|
|
#ifdef LR11X0_POWER_EN
|
|
digitalWrite(LR11X0_POWER_EN, LOW);
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
template <typename T> int16_t LR11x0Interface<T>::getCurrentRSSI()
|
|
{
|
|
#ifdef ARCH_PORTDUINO_WASM
|
|
float rssi = lora.getRSSI(); // installed RadioLib's LR11x0 getRSSI() is 0-arg
|
|
#else
|
|
float rssi = lora.getRSSI(false, true);
|
|
#endif
|
|
return (int16_t)round(rssi);
|
|
}
|
|
|
|
// Don't leak the aliases into the files InterfacesTemplates.cpp includes after this one.
|
|
#undef rfswitch_dio_pins
|
|
#undef rfswitch_table
|
|
#endif
|