// SPDX-License-Identifier: Apache-2.0 #include "sx1262_radio.h" #include "sx126x_radiolib_hal.h" #include #include #include #include #include #include #include #define TAG "sx1262" namespace { // TX-done wait is derived from the packet's time-on-air: fixed timeouts either // false-time-out on slow configs (high SF / narrow BW, airtime up to seconds) or // wait needlessly long on fast ones. The margin covers PA ramp and command latency; // the fallback is used only when RadioLib can't compute airtime for the modem config. constexpr auto SX1262_TX_TIMEOUT_MARGIN_MILLIS = 1000; constexpr auto SX1262_TX_TIMEOUT_FALLBACK_MILLIS = 2000; constexpr uint32_t SX1262_INTERRUPT_BIT = (1 << 0); constexpr uint32_t SX1262_DIO1_EVENT_BIT = (1 << 1); constexpr uint32_t SX1262_QUEUED_TX_BIT = (1 << 2); constexpr auto SX1262_IRQ_FLAGS = RADIOLIB_IRQ_RX_DEFAULT_FLAGS; // RX callbacks run on the radio thread and may do non-trivial work (e.g. packet decryption) constexpr size_t SX1262_THREAD_STACK_SIZE = 8192; const char* toString(enum LoraRadioState state) { switch (state) { case LORA_RADIO_STATE_OFF: return "off"; case LORA_RADIO_STATE_ON_PENDING: return "on-pending"; case LORA_RADIO_STATE_ON: return "on"; case LORA_RADIO_STATE_OFF_PENDING: return "off-pending"; case LORA_RADIO_STATE_ERROR: return "error"; default: return "unknown"; } } const char* toString(enum LoraModulation modulation) { switch (modulation) { case LORA_MODULATION_NONE: return "none"; case LORA_MODULATION_FSK: return "FSK"; case LORA_MODULATION_LORA: return "LoRa"; case LORA_MODULATION_LR_FHSS: return "LR-FHSS"; default: return "unknown"; } } const char* toString(enum LoraParameter parameter) { switch (parameter) { case LORA_PARAMETER_POWER: return "power"; case LORA_PARAMETER_BOOSTED_GAIN: return "boosted gain"; case LORA_PARAMETER_FREQUENCY: return "frequency"; case LORA_PARAMETER_BANDWIDTH: return "bandwidth"; case LORA_PARAMETER_SPREADING_FACTOR: return "spreading factor"; case LORA_PARAMETER_CODING_RATE: return "coding rate"; case LORA_PARAMETER_SYNC_WORD: return "sync word"; case LORA_PARAMETER_PREAMBLE_LENGTH: return "preamble length"; case LORA_PARAMETER_FREQUENCY_DEVIATION: return "frequency deviation"; case LORA_PARAMETER_DATA_RATE: return "data rate"; case LORA_PARAMETER_NARROW_GRID: return "narrow grid"; case LORA_PARAMETER_CURRENT_LIMIT: return "current limit"; default: return "unknown"; } } template constexpr error_t checkLimitsAndApply(T& target, const int32_t value, const int32_t lower, const int32_t upper, const int32_t step = 0) { if ((value >= lower) && (value <= upper)) { if ((step != 0) && ((value % step) != 0)) { return ERROR_OUT_OF_RANGE; } target = static_cast(value); return ERROR_NONE; } return ERROR_OUT_OF_RANGE; } template constexpr error_t checkValuesAndApply(T& target, const int32_t value, std::initializer_list valids) { for (int32_t valid : valids) { if (value == valid) { target = static_cast(value); return ERROR_NONE; } } return ERROR_OUT_OF_RANGE; } } // namespace struct Sx1262Radio::RadioParts { Sx126xRadiolibHal hal; Module radioModule; SX1262 radio; explicit RadioParts(const Settings& settings) : hal(settings.spi_host, settings.spi_frequency_hz, settings.spi_controller) , radioModule(&hal, settings.pin_cs, RADIOLIB_NC, settings.pin_reset, settings.pin_busy) , radio(&radioModule) {} }; Sx1262Radio::Sx1262Radio(const Settings& settings) : settings(settings) { recursive_mutex_construct(&mutex); event_group_construct(&events); parts = new RadioParts(settings); } Sx1262Radio::~Sx1262Radio() { setEnabled(false); delete parts; event_group_destruct(&events); recursive_mutex_destruct(&mutex); } error_t Sx1262Radio::probe() const { // NRESET output, idle high. BUSY input with a pull-up: an absent or unpowered // module leaves BUSY floating, and the pull-up parks it high so the ready // check below can't false-pass on a floating line. gpio_config_t reset_conf = { .pin_bit_mask = (1ULL << settings.pin_reset), .mode = GPIO_MODE_OUTPUT, .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; gpio_config(&reset_conf); gpio_set_level(settings.pin_reset, 1); gpio_config_t busy_conf = { .pin_bit_mask = (1ULL << settings.pin_busy), .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; gpio_config(&busy_conf); // Reset pulse (datasheet: NRESET low for >= 100 us triggers a full reset) gpio_set_level(settings.pin_reset, 0); delay_millis(2); gpio_set_level(settings.pin_reset, 1); // After reset the chip boots and calibrates with BUSY high, then drives BUSY // low once it reaches STDBY_RC (datasheet: ~3.5 ms max). Allow a generous // margin; a line stuck high means no chip is answering. constexpr auto PROBE_TIMEOUT_MILLIS = 20; int elapsed = 0; while (gpio_get_level(settings.pin_busy) != 0) { if (elapsed >= PROBE_TIMEOUT_MILLIS) { LOG_E(TAG, "Probe failed: BUSY (GPIO %d) stuck high after reset — module absent or unpowered?", settings.pin_busy); return ERROR_RESOURCE; } delay_millis(1); elapsed++; } // Drop the probe pull-up again: the chip actively drives BUSY when powered, // and RadioLib reconfigures the pin at begin() anyway. busy_conf.pull_up_en = GPIO_PULLUP_DISABLE; gpio_config(&busy_conf); LOG_I(TAG, "Probe OK: SX1262 answered reset in ~%d ms (BUSY low)", elapsed); return ERROR_NONE; } // region Thread lifecycle void Sx1262Radio::dio1Isr(void* context) { auto* self = static_cast(context); // DIO1 is armed as a HIGH_LEVEL interrupt (edge types are unreliable on the // ESP32 per erratum 3.11). A level interrupt re-fires for as long as the line // is asserted, so mask it here and let the radio thread re-arm once it has // cleared the modem IRQ (which drops DIO1 low again). gpio_descriptor_disable_interrupt(self->settings.dio1); event_group_set(self->events, SX1262_DIO1_EVENT_BIT); } int32_t Sx1262Radio::threadMainStatic(void* context) { return static_cast(context)->threadMain(); } bool Sx1262Radio::isThreadInterrupted() const { lock(); const bool interrupted = threadInterrupted; unlock(); return interrupted; } int32_t Sx1262Radio::threadMain() { int rc = doBegin(getModulation()); bool hasRx = false; if (rc != 0) { return rc; } setState(LORA_RADIO_STATE_ON); while (!isThreadInterrupted()) { // Re-arm DIO1: the ISR masks the HIGH_LEVEL interrupt on each fire, so the // modem's next RX/TX-done needs it enabled again. DIO1 is low here (the // previous IRQ was cleared by doReceive()/doTransmit()); re-arming while it // were still asserted would just self-fire once and be absorbed by the // empty-read guard in doReceive(). gpio_descriptor_enable_interrupt(settings.dio1); hasRx = doListen(); // Thread might've been interrupted in the meanwhile if (isThreadInterrupted()) { break; } // Service a received packet before deciding to transmit: an RX-done and a // queued TX can coincide in the same iteration, and dropping the RX here would // lose the packet outright. if (hasRx) { doReceive(); } if (getTxQueueSize() > 0) { doTransmit(); } } doEnd(); return 0; } error_t Sx1262Radio::setEnabled(bool enabled) { lock(); if (enabled) { if ((thread != nullptr) && (thread_get_state(thread) != THREAD_STATE_STOPPED)) { LOG_W(TAG, "Already started"); unlock(); return ERROR_NONE; } if (modulation == LORA_MODULATION_NONE) { LOG_E(TAG, "Cannot enable without a modulation set"); unlock(); return ERROR_INVALID_STATE; } if (thread != nullptr) { thread_free(thread); thread = nullptr; } threadInterrupted = false; setState(LORA_RADIO_STATE_ON_PENDING); thread = thread_alloc_full("SX1262", SX1262_THREAD_STACK_SIZE, threadMainStatic, this, tskNO_AFFINITY); if (thread == nullptr) { setState(LORA_RADIO_STATE_ERROR); unlock(); return ERROR_OUT_OF_MEMORY; } thread_set_priority(thread, THREAD_PRIORITY_HIGH); if (thread_start(thread) != ERROR_NONE) { thread_free(thread); thread = nullptr; setState(LORA_RADIO_STATE_ERROR); unlock(); return ERROR_UNDEFINED; } unlock(); return ERROR_NONE; } else { setState(LORA_RADIO_STATE_OFF_PENDING); if (thread != nullptr) { threadInterrupted = true; event_group_set(events, SX1262_INTERRUPT_BIT); Thread* oldThread = thread; thread = nullptr; if (thread_get_state(oldThread) != THREAD_STATE_STOPPED) { // Unlock so the thread can lock unlock(); // Wait for the thread to finish thread_join(oldThread, portMAX_DELAY, pdMS_TO_TICKS(10)); // Re-lock to continue logic below lock(); } thread_free(oldThread); } setState(LORA_RADIO_STATE_OFF); unlock(); return ERROR_NONE; } } // endregion // region State, modulation and callbacks enum LoraRadioState Sx1262Radio::getState() const { lock(); const auto result = state; unlock(); return result; } void Sx1262Radio::setState(enum LoraRadioState newState) { lock(); if (state == newState) { unlock(); return; } LOG_I(TAG, "State: %s -> %s", toString(state), toString(newState)); state = newState; auto callbacks = stateCallbacks; unlock(); for (const auto& entry : callbacks) { entry.callback(settings.device, entry.context, newState); } } error_t Sx1262Radio::setModulation(enum LoraModulation newModulation) { const auto currentState = getState(); if ((currentState == LORA_RADIO_STATE_ON_PENDING) || (currentState == LORA_RADIO_STATE_ON)) { return ERROR_INVALID_STATE; } if (!((newModulation == LORA_MODULATION_NONE) || canTransmit(newModulation) || canReceive(newModulation))) { return ERROR_NOT_SUPPORTED; } lock(); LOG_I(TAG, "Modulation set to %s", toString(newModulation)); modulation = newModulation; unlock(); return ERROR_NONE; } enum LoraModulation Sx1262Radio::getModulation() const { lock(); const auto result = modulation; unlock(); return result; } // Callbacks are invoked on a snapshot of the list, with the radio mutex released: // consumers take their own locks in callbacks and also call into this API while // holding those locks, so invoking under the radio mutex would set up an AB-BA // deadlock between the radio thread and any consumer thread. void Sx1262Radio::publishRx(const struct LoraRxPacket& packet) { lock(); auto callbacks = rxCallbacks; unlock(); for (const auto& entry : callbacks) { entry.callback(settings.device, entry.context, &packet); } } void Sx1262Radio::publishTx(LoraTxId id, enum LoraTransmissionState txState) { lock(); auto callbacks = txCallbacks; unlock(); for (const auto& entry : callbacks) { entry.callback(settings.device, entry.context, id, txState); } } error_t Sx1262Radio::addRxCallback(void* context, LoraRxCallback callback) { lock(); rxCallbacks.push_back({context, callback}); unlock(); return ERROR_NONE; } error_t Sx1262Radio::removeRxCallback(LoraRxCallback callback) { lock(); const auto old_size = rxCallbacks.size(); std::erase_if(rxCallbacks, [callback](const auto& entry) { return entry.callback == callback; }); const auto result = (rxCallbacks.size() == old_size) ? ERROR_NOT_FOUND : ERROR_NONE; unlock(); return result; } error_t Sx1262Radio::addStateCallback(void* context, LoraStateCallback callback) { lock(); stateCallbacks.push_back({context, callback}); unlock(); return ERROR_NONE; } error_t Sx1262Radio::removeStateCallback(LoraStateCallback callback) { lock(); const auto old_size = stateCallbacks.size(); std::erase_if(stateCallbacks, [callback](const auto& entry) { return entry.callback == callback; }); const auto result = (stateCallbacks.size() == old_size) ? ERROR_NOT_FOUND : ERROR_NONE; unlock(); return result; } error_t Sx1262Radio::addTxCallback(void* context, LoraTxCallback callback) { lock(); txCallbacks.push_back({context, callback}); unlock(); return ERROR_NONE; } error_t Sx1262Radio::removeTxCallback(LoraTxCallback callback) { lock(); const auto old_size = txCallbacks.size(); std::erase_if(txCallbacks, [callback](const auto& entry) { return entry.callback == callback; }); const auto result = (txCallbacks.size() == old_size) ? ERROR_NOT_FOUND : ERROR_NONE; unlock(); return result; } // endregion // region TX queue size_t Sx1262Radio::getTxQueueSize() const { lock(); const auto size = txQueue.size(); unlock(); return size; } Sx1262Radio::TxItem Sx1262Radio::popNextQueuedTx() { lock(); auto tx = std::move(txQueue.front()); txQueue.pop_front(); unlock(); return tx; } error_t Sx1262Radio::transmit(const uint8_t* data, size_t length, LoraTxId* id) { lock(); const auto txId = lastTxId; lastTxId++; txQueue.push_back(TxItem {.id = txId, .data = std::vector(data, data + length)}); LOG_D(TAG, "TX id=%d queued: %u bytes (queue depth %u)", (int)txId, (unsigned)length, (unsigned)txQueue.size()); unlock(); publishTx(txId, LORA_TRANSMISSION_STATE_QUEUED); event_group_set(events, SX1262_QUEUED_TX_BIT); if (id != nullptr) { *id = txId; } return ERROR_NONE; } // endregion // region Parameters error_t Sx1262Radio::setBaseParameter(enum LoraParameter parameter, int32_t value) { switch (parameter) { case LORA_PARAMETER_POWER: return checkLimitsAndApply(power, value, -9, 22); case LORA_PARAMETER_BOOSTED_GAIN: return checkLimitsAndApply(boostedGain, value, 0, 1, 1); case LORA_PARAMETER_CURRENT_LIMIT: // SX1262 OCP range is 0..140 mA (RadioLib clamps to a 2.5 mA step internally). return checkLimitsAndApply(currentLimit, value, 0, 140); default: return ERROR_NOT_SUPPORTED; } } error_t Sx1262Radio::setLoraParameter(enum LoraParameter parameter, int32_t value) { switch (parameter) { // Frequency in Hz (150..960 MHz) case LORA_PARAMETER_FREQUENCY: return checkLimitsAndApply(frequency, value, 150000000, 960000000); // Bandwidth in Hz (RadioLib's supported LoRa bandwidths, expressed in Hz) case LORA_PARAMETER_BANDWIDTH: return checkValuesAndApply(bandwidth, value, {7800, 10400, 15600, 20800, 31250, 41700, 62500, 125000, 250000, 500000}); case LORA_PARAMETER_SPREADING_FACTOR: return checkLimitsAndApply(spreadingFactor, value, 7, 12, 1); case LORA_PARAMETER_CODING_RATE: return checkLimitsAndApply(codingRate, value, 5, 8, 1); case LORA_PARAMETER_SYNC_WORD: return checkLimitsAndApply(syncWord, value, 0, 255); case LORA_PARAMETER_PREAMBLE_LENGTH: return checkLimitsAndApply(preambleLength, value, 0, 65535); default: break; } LOG_W(TAG, "Tried to set unsupported LoRa parameter \"%s\" to %d", toString(parameter), (int)value); return ERROR_NOT_SUPPORTED; } error_t Sx1262Radio::setFskParameter(enum LoraParameter parameter, int32_t value) { switch (parameter) { // Frequency in Hz (150..960 MHz) case LORA_PARAMETER_FREQUENCY: return checkLimitsAndApply(frequency, value, 150000000, 960000000); // RX bandwidth in Hz (RadioLib's supported FSK bandwidths, expressed in Hz) case LORA_PARAMETER_BANDWIDTH: return checkValuesAndApply(bandwidth, value, {4800, 5800, 7300, 9700, 11700, 14600, 19500, 23400, 29300, 39000, 46900, 58600, 78200}); case LORA_PARAMETER_PREAMBLE_LENGTH: return checkLimitsAndApply(preambleLength, value, 0, 65535); // Bit rate in bit/s (0.6..300 kbps) case LORA_PARAMETER_DATA_RATE: return checkLimitsAndApply(bitRate, value, 600, 300000); // Frequency deviation in Hz (0..200 kHz) case LORA_PARAMETER_FREQUENCY_DEVIATION: return checkLimitsAndApply(frequencyDeviation, value, 0, 200000); default: break; } LOG_W(TAG, "Tried to set unsupported FSK parameter \"%s\" to %d", toString(parameter), (int)value); return ERROR_NOT_SUPPORTED; } error_t Sx1262Radio::setLrFhssParameter(enum LoraParameter parameter, int32_t value) { switch (parameter) { // Bandwidth in Hz (RadioLib's supported LR-FHSS bandwidths, expressed in Hz) case LORA_PARAMETER_BANDWIDTH: return checkValuesAndApply(bandwidth, value, {39060, 85940, 136720, 183590, 335940, 386720, 722660, 773440, 1523400, 1574200}); case LORA_PARAMETER_CODING_RATE: return checkValuesAndApply(codingRate, value, {RADIOLIB_SX126X_LR_FHSS_CR_5_6, RADIOLIB_SX126X_LR_FHSS_CR_2_3, RADIOLIB_SX126X_LR_FHSS_CR_1_2, RADIOLIB_SX126X_LR_FHSS_CR_1_3}); case LORA_PARAMETER_NARROW_GRID: return checkLimitsAndApply(narrowGrid, value, 0, 1, 1); default: break; } LOG_W(TAG, "Tried to set unsupported LR-FHSS parameter \"%s\" to %d", toString(parameter), (int)value); return ERROR_NOT_SUPPORTED; } error_t Sx1262Radio::setParameter(enum LoraParameter parameter, int32_t value) { lock(); error_t result = setBaseParameter(parameter, value); if (result == ERROR_NOT_SUPPORTED) { switch (modulation) { case LORA_MODULATION_LORA: result = setLoraParameter(parameter, value); break; case LORA_MODULATION_FSK: result = setFskParameter(parameter, value); break; case LORA_MODULATION_LR_FHSS: result = setLrFhssParameter(parameter, value); break; default: break; } } if (result == ERROR_NONE) { LOG_D(TAG, "Parameter %s = %d", toString(parameter), (int)value); } unlock(); return result; } error_t Sx1262Radio::getBaseParameter(enum LoraParameter parameter, int32_t* value) const { switch (parameter) { case LORA_PARAMETER_POWER: *value = power; return ERROR_NONE; case LORA_PARAMETER_BOOSTED_GAIN: *value = boostedGain; return ERROR_NONE; case LORA_PARAMETER_CURRENT_LIMIT: *value = currentLimit; return ERROR_NONE; default: return ERROR_NOT_SUPPORTED; } } error_t Sx1262Radio::getLoraParameter(enum LoraParameter parameter, int32_t* value) const { switch (parameter) { case LORA_PARAMETER_FREQUENCY: *value = frequency; return ERROR_NONE; case LORA_PARAMETER_BANDWIDTH: *value = bandwidth; return ERROR_NONE; case LORA_PARAMETER_SPREADING_FACTOR: *value = spreadingFactor; return ERROR_NONE; case LORA_PARAMETER_CODING_RATE: *value = codingRate; return ERROR_NONE; case LORA_PARAMETER_SYNC_WORD: *value = syncWord; return ERROR_NONE; case LORA_PARAMETER_PREAMBLE_LENGTH: *value = preambleLength; return ERROR_NONE; default: return ERROR_NOT_SUPPORTED; } } error_t Sx1262Radio::getFskParameter(enum LoraParameter parameter, int32_t* value) const { switch (parameter) { case LORA_PARAMETER_FREQUENCY: *value = frequency; return ERROR_NONE; case LORA_PARAMETER_BANDWIDTH: *value = bandwidth; return ERROR_NONE; case LORA_PARAMETER_DATA_RATE: *value = bitRate; return ERROR_NONE; case LORA_PARAMETER_FREQUENCY_DEVIATION: *value = frequencyDeviation; return ERROR_NONE; default: return ERROR_NOT_SUPPORTED; } } error_t Sx1262Radio::getLrFhssParameter(enum LoraParameter parameter, int32_t* value) const { switch (parameter) { case LORA_PARAMETER_BANDWIDTH: *value = bandwidth; return ERROR_NONE; case LORA_PARAMETER_CODING_RATE: *value = codingRate; return ERROR_NONE; case LORA_PARAMETER_NARROW_GRID: *value = narrowGrid; return ERROR_NONE; default: return ERROR_NOT_SUPPORTED; } } error_t Sx1262Radio::getParameter(enum LoraParameter parameter, int32_t* value) const { lock(); // No warnings are emitted to be able to discover parameters by return status error_t result = getBaseParameter(parameter, value); if (result == ERROR_NOT_SUPPORTED) { switch (modulation) { case LORA_MODULATION_LORA: result = getLoraParameter(parameter, value); break; case LORA_MODULATION_FSK: result = getFskParameter(parameter, value); break; case LORA_MODULATION_LR_FHSS: result = getLrFhssParameter(parameter, value); break; default: break; } } unlock(); return result; } // endregion // region Radio operations (radio thread only) // DIO1 uses the GPIO descriptor callback API in HIGH_LEVEL mode. The interrupt is // armed (enabled) per cycle by the radio thread loop and masked by the ISR on each // fire; the modem asserts DIO1 for RX/TX-done, which the driver clears by reading // the packet or finishing the transmission. Edge-triggered interrupts are avoided // on purpose (ESP32 erratum 3.11: subsequent edge interrupts may be missed, which // for a radio would drop an RX/TX-done and stall the thread). void Sx1262Radio::registerDio1Isr() { gpio_flags_t flags = GPIO_FLAG_DIRECTION_INPUT; flags = GPIO_FLAG_INTERRUPT_TO_OPTIONS(flags, GPIO_INTERRUPT_HIGH_LEVEL); if (gpio_descriptor_set_flags(settings.dio1, flags) != ERROR_NONE || gpio_descriptor_add_callback(settings.dio1, dio1Isr, this) != ERROR_NONE) { LOG_E(TAG, "Failed to install DIO1 interrupt"); } } void Sx1262Radio::unregisterDio1Isr() { gpio_descriptor_disable_interrupt(settings.dio1); gpio_descriptor_remove_callback(settings.dio1); } int Sx1262Radio::doBegin(enum LoraModulation beginModulation) { int16_t rc = RADIOLIB_ERR_NONE; auto& radio = parts->radio; // RadioLib takes MHz/kHz/kbps floats; the driver stores Hz/bit/s integers. const float frequencyMhz = static_cast(frequency) / 1000000.0f; const float bandwidthKhz = static_cast(bandwidth) / 1000.0f; if (beginModulation == LORA_MODULATION_LORA) { LOG_I( TAG, "Starting LoRa: %.3f MHz, BW %.2f kHz, SF%u, CR 4/%u, sync 0x%02X, preamble %u, %d dBm, TCXO %.1f V", frequencyMhz, bandwidthKhz, spreadingFactor, codingRate, syncWord, preambleLength, power, settings.tcxo_voltage ); rc = radio.begin( frequencyMhz, bandwidthKhz, spreadingFactor, codingRate, syncWord, power, preambleLength, settings.tcxo_voltage, settings.use_regulator_ldo ); } else if (beginModulation == LORA_MODULATION_FSK) { const float bitRateKbps = static_cast(bitRate) / 1000.0f; const float frequencyDeviationKhz = static_cast(frequencyDeviation) / 1000.0f; LOG_I( TAG, "Starting FSK: %.3f MHz, %.2f kbps, deviation %.1f kHz, BW %.1f kHz, preamble %u, %d dBm", frequencyMhz, bitRateKbps, frequencyDeviationKhz, bandwidthKhz, preambleLength, power ); rc = radio.beginFSK( frequencyMhz, bitRateKbps, frequencyDeviationKhz, bandwidthKhz, power, preambleLength, settings.tcxo_voltage, settings.use_regulator_ldo ); } else if (beginModulation == LORA_MODULATION_LR_FHSS) { // NOTE: LR-FHSS is unvalidated. RadioLib's beginLRFHSS() takes // (freq, bw-index, cr, narrowGrid, ...) where bw is a RADIOLIB_SX126X_LR_FHSS_BW_* // index, not a frequency; this call passes the stored bandwidth into the freq slot // and is known to be incomplete. Left as-is pending a dedicated LR-FHSS bring-up — // the LoRa and FSK paths above are the hardware-validated ones. LOG_I(TAG, "Starting LR-FHSS: BW %d Hz, CR %u, %s grid", (int)bandwidth, codingRate, narrowGrid ? "narrow" : "wide"); rc = radio.beginLRFHSS( bandwidth, codingRate, narrowGrid, settings.tcxo_voltage, settings.use_regulator_ldo ); } else { LOG_E(TAG, "SX1262 not capable of modulation \"%s\"", toString(beginModulation)); setState(LORA_RADIO_STATE_ERROR); return -1; } if (rc != RADIOLIB_ERR_NONE) { LOG_E(TAG, "RadioLib initialization failed with code %hi", rc); setState(LORA_RADIO_STATE_ERROR); return -1; } // Apply the PA over-current protection limit. RadioLib's begin() already set its // fail-safe default (60 mA), so this is only meaningful when a consumer raised it // via LORA_PARAMETER_CURRENT_LIMIT to reach higher output power. rc = radio.setCurrentLimit(static_cast(currentLimit)); if (rc != RADIOLIB_ERR_NONE) { LOG_E(TAG, "Setting current limit to %d mA failed with code %hi", (int)currentLimit, rc); setState(LORA_RADIO_STATE_ERROR); return -1; } // Modules that wire the antenna TX/RX switch to DIO2 (e.g. LilyGO T-Deck Max) // must enable this or the RF path stays disconnected and no TX/RX gets through. if (settings.dio2_rf_switch) { rc = radio.setDio2AsRfSwitch(true); if (rc != RADIOLIB_ERR_NONE) { LOG_E(TAG, "Setting DIO2 as RF switch failed with code %hi", rc); setState(LORA_RADIO_STATE_ERROR); return -1; } } rc = radio.setRxBoostedGainMode(boostedGain, true); if (rc != RADIOLIB_ERR_NONE) { LOG_E(TAG, "Setting RX boosted gain to %s failed with code %hi", boostedGain ? "true" : "false", rc); setState(LORA_RADIO_STATE_ERROR); return -1; } LOG_I(TAG, "Modem initialized (chip verified by RadioLib)"); registerDio1Isr(); return 0; } void Sx1262Radio::doEnd() { unregisterDio1Isr(); // Leave the modem in its lowest-power state; the next enable runs a full begin() const int16_t rc = parts->radio.sleep(); if (rc != RADIOLIB_ERR_NONE) { LOG_W(TAG, "Putting modem to sleep failed with code %hi", rc); } else { LOG_I(TAG, "Modem put to sleep"); } } void Sx1262Radio::doTransmit() { currentTx = popNextQueuedTx(); auto& radio = parts->radio; int16_t rc = radio.standby(); if (rc != RADIOLIB_ERR_NONE) { LOG_W(TAG, "RadioLib returned %hi on TX standby", rc); } LOG_I(TAG, "TX id=%d: %u bytes (%u more queued)", (int)currentTx.id, (unsigned)currentTx.data.size(), (unsigned)getTxQueueSize()); rc = radio.startTransmit(currentTx.data.data(), currentTx.data.size()); if (rc == RADIOLIB_ERR_NONE) { publishTx(currentTx.id, LORA_TRANSMISSION_STATE_TRANSMIT_PENDING); // Time-on-air (microseconds) for the current modem config; 0 if RadioLib can't // compute it, in which case fall back to a fixed timeout. const uint32_t airtimeMillis = radio.getTimeOnAir(currentTx.data.size()) / 1000; const uint32_t txTimeoutMillis = (airtimeMillis > 0) ? (airtimeMillis + SX1262_TX_TIMEOUT_MARGIN_MILLIS) : SX1262_TX_TIMEOUT_FALLBACK_MILLIS; // outFlags stays 0 on timeout, which routes to the Timeout branch below uint32_t txEventFlags = 0; event_group_wait( events, SX1262_INTERRUPT_BIT | SX1262_DIO1_EVENT_BIT, false, true, &txEventFlags, pdMS_TO_TICKS(txTimeoutMillis) ); // Clean up after transmission radio.finishTransmit(); // Thread might've been interrupted in the meanwhile. Publish a terminal state so a // caller that queued this TX still gets a final callback for its id when // setEnabled(false) races with an in-flight transmit. if (isThreadInterrupted()) { publishTx(currentTx.id, LORA_TRANSMISSION_STATE_ERROR); return; } // If the DIO1 bit is unset, this means the wait timed out if (txEventFlags & SX1262_DIO1_EVENT_BIT) { LOG_I(TAG, "TX id=%d: done", (int)currentTx.id); publishTx(currentTx.id, LORA_TRANSMISSION_STATE_TRANSMITTED); } else { LOG_W(TAG, "TX id=%d: no TX-done IRQ within %u ms", (int)currentTx.id, (unsigned)txTimeoutMillis); publishTx(currentTx.id, LORA_TRANSMISSION_STATE_TIMEOUT); } } else { LOG_E(TAG, "Error transmitting id=%d, rc=%hi", (int)currentTx.id, rc); publishTx(currentTx.id, LORA_TRANSMISSION_STATE_ERROR); } } bool Sx1262Radio::doListen() { auto& radio = parts->radio; if (getModulation() != LORA_MODULATION_LR_FHSS) { int16_t rc = radio.startReceiveDutyCycleAuto(preambleLength, 0, SX1262_IRQ_FLAGS); if (rc == RADIOLIB_ERR_NONE) { uint32_t flags = 0; event_group_wait( events, SX1262_INTERRUPT_BIT | SX1262_DIO1_EVENT_BIT | SX1262_QUEUED_TX_BIT, false, true, &flags, portMAX_DELAY ); return (flags & SX1262_DIO1_EVENT_BIT) != 0; } else { LOG_E(TAG, "Error setting dutycycle RX, RadioLib returned %hi", rc); } return false; } else { // LR-FHSS modem only supports TX event_group_wait( events, SX1262_INTERRUPT_BIT | SX1262_QUEUED_TX_BIT, false, true, nullptr, portMAX_DELAY ); return false; } } void Sx1262Radio::doReceive() { // LR-FHSS modem only supports TX if (getModulation() == LORA_MODULATION_LR_FHSS) return; auto& radio = parts->radio; uint16_t rxSize = radio.getPacketLength(true); std::vector data(rxSize); int16_t rc = radio.readData(data.data(), rxSize); if (rc != RADIOLIB_ERR_NONE) { LOG_E(TAG, "Error receiving data, RadioLib returned %hi", rc); } else if (rxSize == 0) { // Empty read: skip silently to avoid log flooding on spurious IRQs. } else { const struct LoraRxPacket packet = { .data = data.data(), .length = data.size(), .rssi = radio.getRSSI(), .snr = radio.getSNR(), }; LOG_I(TAG, "RX: %u bytes, RSSI %.1f dBm, SNR %.1f dB", (unsigned)packet.length, packet.rssi, packet.snr); publishRx(packet); radio.finishReceive(); } } // endregion