#include "RadioLibInterface.h" #include "MeshTypes.h" #include "NodeDB.h" #include "PowerMon.h" #include "SPILock.h" #include "Throttle.h" #include "configuration.h" #include "error.h" #include "main.h" #include "mesh-pb-constants.h" #if !MESHTASTIC_EXCLUDE_BEACON #include "modules/MeshBeaconModule.h" #endif #include #include #if ARCH_PORTDUINO #include "PortduinoGlue.h" #include "meshUtils.h" #endif void LockingArduinoHal::spiBeginTransaction() { spiLock->lock(); ArduinoHal::spiBeginTransaction(); } void LockingArduinoHal::spiEndTransaction() { ArduinoHal::spiEndTransaction(); spiLock->unlock(); } #if ARCH_PORTDUINO void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in) { spi->transfer(out, in, len); } #endif RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy, PhysicalLayer *_iface) : NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface) { instance = this; // Initialize unused sample slots to a sane default; sample count controls averaging. for (uint8_t i = 0; i < NOISE_FLOOR_SAMPLES; i++) { noiseFloorSamples[i] = NOISE_FLOOR_DEFAULT; } #if defined(ARCH_STM32WL) && defined(USE_SX1262) module.setCb_digitalWrite(stm32wl_emulate_digitalWrite); module.setCb_digitalRead(stm32wl_emulate_digitalRead); #endif } #ifdef ARCH_ESP32 // ESP32 doesn't use that flag #define YIELD_FROM_ISR(x) portYIELD_FROM_ISR() #else #define YIELD_FROM_ISR(x) portYIELD_FROM_ISR(x) #endif void INTERRUPT_ATTR RadioLibInterface::isrLevel0Common(PendingISR cause) { instance->disableInterrupt(); BaseType_t xHigherPriorityTaskWoken; instance->notifyFromISR(&xHigherPriorityTaskWoken, cause, true); /* Force a context switch if xHigherPriorityTaskWoken is now set to pdTRUE. The macro used to do this is dependent on the port and may be called portEND_SWITCHING_ISR. */ YIELD_FROM_ISR(xHigherPriorityTaskWoken); } void INTERRUPT_ATTR RadioLibInterface::isrRxLevel0() { isrLevel0Common(ISR_RX); } void INTERRUPT_ATTR RadioLibInterface::isrTxLevel0() { isrLevel0Common(ISR_TX); } /** Our ISR code currently needs this to find our active instance */ RadioLibInterface *RadioLibInterface::instance; /** Could we send right now (i.e. either not actively receiving or transmitting)? */ bool RadioLibInterface::canSendImmediately() { // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, // we almost certainly guarantee no one outside will like the packet we are sending. bool busyTx = sendingPacket != NULL; bool busyRx = isReceiving && isActivelyReceiving(); if (busyTx || busyRx) { if (busyTx) { LOG_WARN("Can not send yet, busyTx"); } // If we've been trying to send the same packet more than one minute and we haven't gotten a // TX IRQ from the radio, the radio is probably broken. if (busyTx && !Throttle::isWithinTimespanMs(lastTxStart, 60000)) { LOG_ERROR("Hardware Failure! busyTx for more than 60s"); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_TRANSMIT_FAILED); // reboot in 5 seconds when this condition occurs. rebootAtMsec = lastTxStart + 65000; } if (busyRx) { LOG_WARN("Can not send yet, busyRx"); } return false; } else return true; } bool RadioLibInterface::receiveDetected(uint16_t irq, unsigned long syncWordHeaderValidFlag, unsigned long preambleDetectedFlag) { bool detected = (irq & (syncWordHeaderValidFlag | preambleDetectedFlag)); // Handle false detections if (detected) { if (!activeReceiveStart) { activeReceiveStart = millis(); } else if (!Throttle::isWithinTimespanMs(activeReceiveStart, 2 * preambleTimeMsec)) { if (!(irq & syncWordHeaderValidFlag)) { // The HEADER_VALID flag should be set by now if it was really a packet, so ignore PREAMBLE_DETECTED flag activeReceiveStart = 0; LOG_DEBUG("Ignore false preamble detection"); return false; } else { uint32_t maxPacketTimeMsec = getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN + sizeof(PacketHeader)); if (!Throttle::isWithinTimespanMs(activeReceiveStart, maxPacketTimeMsec)) { // We should have gotten an RX_DONE IRQ by now if it was really a packet, so ignore HEADER_VALID flag activeReceiveStart = 0; LOG_DEBUG("Ignore false header detection"); return false; } } } } return detected; } /// Send a packet (possibly by enquing in a private fifo). This routine will /// later free() the packet to pool. This routine is not allowed to stall because it is called from /// bluetooth comms code. If the txmit queue is empty it might return an error ErrorCode RadioLibInterface::send(meshtastic_MeshPacket *p) { #ifndef DISABLE_WELCOME_UNSET if (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { if (disabled || !config.lora.tx_enabled) { LOG_WARN("send - !config.lora.tx_enabled"); packetPool.release(p); return ERRNO_DISABLED; } } else { LOG_WARN("send - lora tx disabled: Region unset"); packetPool.release(p); return ERRNO_DISABLED; } #else if (disabled || !config.lora.tx_enabled) { LOG_WARN("send - !config.lora.tx_enabled"); packetPool.release(p); return ERRNO_DISABLED; } #endif if (p->to == NODENUM_BROADCAST_NO_LORA) { LOG_DEBUG("Drop no-LoRa pkt"); return ERRNO_SHOULD_RELEASE; } // Sometimes when testing it is useful to be able to never turn on the xmitter #ifndef LORA_DISABLE_SENDING printPacket("enqueue for send", p); LOG_DEBUG("txGood=%d,txRelay=%d,rxGood=%d,rxBad=%d", txGood, txRelay, rxGood, rxBad); bool dropped = false; ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN; if (dropped) { txDrop++; } if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks packetPool.release(p); return res; } // set (random) transmit delay to let others reconfigure their radio, // to avoid collisions and implement timing-based flooding setTransmitDelay(); return res; #else packetPool.release(p); return ERRNO_DISABLED; #endif } meshtastic_QueueStatus RadioLibInterface::getQueueStatus() { meshtastic_QueueStatus qs; qs.res = qs.mesh_packet_id = 0; qs.free = txQueue.getFree(); qs.maxlen = txQueue.getMaxLen(); return qs; } bool RadioLibInterface::canSleep() { bool res = txQueue.empty(); if (!res) { // only print debug messages if we are vetoing sleep LOG_DEBUG("Radio wait to sleep, txEmpty=%d", res); } return res; } /** Allow other firmware components to ask whether we are currently sending a packet Initially implemented to protect T-Echo's capacitive touch button from spurious presses during tx */ bool RadioLibInterface::isSending() { return sendingPacket != NULL; } /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ bool RadioLibInterface::cancelSending(NodeNum from, PacketId id) { auto p = txQueue.remove(from, id); if (p) packetPool.release(p); // free the packet we just removed bool result = (p != NULL); LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result); return result; } /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id) { return txQueue.find(from, id); } void RadioLibInterface::updateNoiseFloor() { // Only sample from idle receive mode. TX/RX-critical paths must return to radio work quickly. if (!isReceiving || sendingPacket != NULL || isActivelyReceiving() || isIRQPending()) { return; } uint32_t now = millis(); if (now - lastNoiseFloorUpdate < NOISE_FLOOR_UPDATE_INTERVAL_MS) { return; } lastNoiseFloorUpdate = now; int16_t rssi = getCurrentRSSI(); if (rssi == NOISE_FLOOR_INVALID || rssi >= 0 || rssi < NOISE_FLOOR_VALID_MIN) { LOG_DEBUG("Skipping invalid RSSI reading: %d", rssi); return; } noiseFloorSamples[currentSampleIndex] = (int32_t)rssi; currentSampleIndex++; if (currentSampleIndex >= NOISE_FLOOR_SAMPLES) { currentSampleIndex = 0; isNoiseFloorBufferFull = true; } currentNoiseFloor = getAverageNoiseFloorInternal(); LOG_DEBUG("Noise floor: %d dBm (samples: %d, latest: %d dBm)", currentNoiseFloor, getNoiseFloorSampleCountInternal(), rssi); } uint8_t RadioLibInterface::getNoiseFloorSampleCountInternal() const { return isNoiseFloorBufferFull ? NOISE_FLOOR_SAMPLES : currentSampleIndex; } int32_t RadioLibInterface::getAverageNoiseFloorInternal() const { uint8_t sampleCount = getNoiseFloorSampleCountInternal(); if (sampleCount == 0) { return NOISE_FLOOR_DEFAULT; } int32_t sum = 0; for (uint8_t i = 0; i < sampleCount; i++) { sum += noiseFloorSamples[i]; } return sum / sampleCount; } int32_t RadioLibInterface::getAverageNoiseFloor() { return getAverageNoiseFloorInternal(); } int32_t RadioLibInterface::getNoiseFloor() { return currentNoiseFloor; } bool RadioLibInterface::hasNoiseFloorSamples() { return getNoiseFloorSampleCountInternal() > 0; } uint8_t RadioLibInterface::getNoiseFloorSampleCount() { return getNoiseFloorSampleCountInternal(); } void RadioLibInterface::resetNoiseFloor() { currentSampleIndex = 0; isNoiseFloorBufferFull = false; currentNoiseFloor = NOISE_FLOOR_DEFAULT; LOG_INFO("Noise floor reset - rolling window collection will restart"); } bool RadioLibInterface::randomBytes(uint8_t *buffer, size_t length) { if (!buffer || length == 0 || !iface) { return false; } // Older RadioLib versions only expose random(min, max), so fill the buffer byte-by-byte. for (size_t i = 0; i < length; ++i) { int32_t value = iface->random(0, 255); if (value < 0) { return false; } buffer[i] = static_cast(value & 0xFF); } return true; } /** radio helper thread callback. We never immediately transmit after any operation (either Rx or Tx). Instead we should wait a random multiple of 'slotTimes' (see definition in RadioInterface.h) taken from a contention window (CW) to lower the chance of collision. The CW size is determined by setTransmitDelay() and depends either on the current channel utilization or SNR in case of a flooding message. After this, we perform channel activity detection (CAD) and reset the transmit delay if it is currently active. */ void RadioLibInterface::onNotify(uint32_t notification) { switch (notification) { case ISR_TX: handleTransmitInterrupt(); // completeSending() already restored the radio to the home config #if !MESHTASTIC_EXCLUDE_BEACON // Pre-switch the radio to the NEXT queued packet's beacon config (no-op for normal traffic). // Not required for correctness — TRANSMIT_DELAY_COMPLETED would switch before CAD anyway — but // doing it here lets the next beacon skip the switch-only delay cycle and, more importantly, // keeps the post-TX listen window (and the CAD/LBT that follows) on the channel we're about to // transmit on. Only engages when the next packet is itself a beacon — exactly when we want it. MeshBeaconModule::reconfigureForBeaconTX(this, txQueue.getFront()); #endif startReceive(); setTransmitDelay(); break; case ISR_RX: handleReceiveInterrupt(); startReceive(); setTransmitDelay(); break; case TRANSMIT_DELAY_COMPLETED: // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread // has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode? if (!txQueue.empty()) { if (!canSendImmediately()) { setTransmitDelay(); // currently Rx/Tx-ing: reset random delay } else { meshtastic_MeshPacket *txp = txQueue.getFront(); assert(txp); long delay_remaining = txp->tx_after ? txp->tx_after - millis() : 0; if (delay_remaining > 0) { // There's still some delay pending on this packet, so resume waiting for it to elapse notifyLater(delay_remaining, TRANSMIT_DELAY_COMPLETED, false); #if !MESHTASTIC_EXCLUDE_BEACON } else if (MeshBeaconModule::beaconTxConfigInvalid(txp)) { // The beacon's target radio config is invalid (bad preset/region, or an // unlicensed node keying up on a ham-only region). Drop the packet — never // transmit it on the current (home) config — and move on to the next queued packet. LOG_DEBUG("Beacon: invalid TX radio config, dropping packet 0x%08x", txp->id); meshtastic_MeshPacket *bad = txQueue.dequeue(); MeshBeaconModule::clearTargetRadioSettings(bad); packetPool.release(bad); setTransmitDelay(); } else if (MeshBeaconModule::reconfigureForBeaconTX(this, txp)) { setTransmitDelay(); #endif } else { if (isChannelActive()) { // check if there is currently a LoRa packet on the channel #if !MESHTASTIC_EXCLUDE_BEACON if (!MeshBeaconModule::hasTargetRadioSettings(txp)) #endif { startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again } setTransmitDelay(); } else { // Send any outgoing packets we have ready as fast as possible to keep the time between channel scan and // actual transmission as short as possible txp = txQueue.dequeue(); assert(txp); startSend(txp); LOG_DEBUG("%d packets remain in the TX queue", txQueue.getMaxLen() - txQueue.getFree()); } } } } else { // Do nothing, because the queue is empty } break; default: assert(0); // We expected to receive a valid notification from the ISR } } void RadioLibInterface::setTransmitDelay() { meshtastic_MeshPacket *p = txQueue.getFront(); if (!p) { return; // noop if there's nothing in the queue } // We want all sending/receiving to be done by our daemon thread. // We use a delay here because this packet might have been sent in response to a packet we just received. // So we want to make sure the other side has had a chance to reconfigure its radio. if (p->tx_after) { unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p) : getTxDelayMsec(); unsigned long now = millis(); p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false); } else if (p->rx_snr == 0 && p->rx_rssi == 0) { /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. * This assumption is valid because of the offset generated by the radio to account for the noise * floor. */ startTransmitTimer(true); } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); startTransmitTimerRebroadcast(p); } } void RadioLibInterface::startTransmitTimer(bool withDelay) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { uint32_t delay = !withDelay ? 1 : getTxDelayMsec(); notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable } } void RadioLibInterface::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { uint32_t delay = getTxDelayMsecWeighted(p); notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable } } /** * If the packet is not already in the late rebroadcast window, move it there */ void RadioLibInterface::clampToLateRebroadcastWindow(NodeNum from, PacketId id) { // Look for non-late packets only, so we don't do this twice! meshtastic_MeshPacket *p = txQueue.remove(from, id, true, false); if (p) { p->tx_after = millis() + getTxDelayMsecWeightedWorst(p->rx_snr); bool dropped = false; if (txQueue.enqueue(p, &dropped)) { LOG_DEBUG("Move existing queued packet to the late rebroadcast window %dms from now", p->tx_after - millis()); } else { packetPool.release(p); } if (dropped) { txDrop++; } } } /** * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version * @return Whether a pending packet was removed */ bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) { meshtastic_MeshPacket *p = txQueue.remove(from, id, true, true, hop_limit_lt); if (p) { LOG_DEBUG("Dropping pending-TX packet 0x%08x with hop limit %d", p->id, p->hop_limit); packetPool.release(p); return true; } return false; } void RadioLibInterface::handleTransmitInterrupt() { // This can be null if we forced the device to enter standby mode. In that case // ignore the transmit interrupt if (sendingPacket) completeSending(); powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // But our transmitter is definitely off now } void RadioLibInterface::completeSending() { // We are careful to clear sending packet before calling printPacket because // that can take a long time auto p = sendingPacket; sendingPacket = NULL; #ifdef LED_LORA digitalWrite(LED_LORA, LED_STATE_OFF); #endif if (p) { // Packet has been sent, count it toward our TX airtime utilization. uint32_t xmitMsec = getPacketTime(p); airTime->logAirtime(TX_LOG, xmitMsec); txGood++; if (!isFromUs(p)) txRelay++; printPacket("Completed sending", p); #if !MESHTASTIC_EXCLUDE_BEACON MeshBeaconModule::clearTargetRadioSettings(p); MeshBeaconModule::reconfigureForBeaconTX(this, nullptr); #endif // We are done sending that packet, release it packetPool.release(p); } } void RadioLibInterface::handleReceiveInterrupt() { // when this is called, we should be in receive mode - if we are not, just jump out instead of bombing. Possible Race // Condition? if (!isReceiving) { LOG_ERROR("handleReceiveInterrupt called when not in rx mode, which shouldn't happen"); return; } isReceiving = false; // read the number of actually received bytes size_t length = iface->getPacketLength(); uint32_t rxMsec = getPacketTime(length, true); #ifndef DISABLE_WELCOME_UNSET if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { LOG_WARN("lora rx disabled: Region unset"); airTime->logAirtime(RX_ALL_LOG, rxMsec); return; } #endif int state = iface->readData((uint8_t *)&radioBuffer, length); #if ARCH_PORTDUINO if (portduino_config.logoutputlevel == level_trace) { printBytes("Raw incoming packet: ", (uint8_t *)&radioBuffer, length); } #endif if (state != RADIOLIB_ERR_NONE) { // Log PacketHeader similar to RadioInterface::printPacket so we can try to match RX errors to other packets in the logs. LOG_ERROR("Ignore received packet due to error=%d (maybe id=0x%08x fr=0x%08x to=0x%08x flags=0x%02x rxSNR=%g rxRSSI=%i " "nextHop=0x%x relay=0x%x)", state, radioBuffer.header.id, radioBuffer.header.from, radioBuffer.header.to, radioBuffer.header.flags, iface->getSNR(), lround(iface->getRSSI()), radioBuffer.header.next_hop, radioBuffer.header.relay_node); rxBad++; airTime->logAirtime(RX_ALL_LOG, rxMsec); } else { // Skip the 4 headers that are at the beginning of the rxBuf int32_t payloadLen = length - sizeof(PacketHeader); // check for short packets if (payloadLen < 0) { LOG_WARN("Ignore received packet too short"); rxBad++; airTime->logAirtime(RX_ALL_LOG, rxMsec); } else { rxGood++; // altered packet with "from == 0" can do Remote Node Administration without permission if (radioBuffer.header.from == 0) { LOG_WARN("Ignore received packet without sender"); return; } // Note: we deliver _all_ packets to our router (i.e. our interface is intentionally promiscuous). // This allows the router and other apps on our node to sniff packets (usually routing) between other // nodes. meshtastic_MeshPacket *mp = packetPool.allocZeroed(); // Keep the assigned fields in sync with src/mqtt/MQTT.cpp:onReceiveProto mp->from = radioBuffer.header.from; mp->to = radioBuffer.header.to; mp->id = radioBuffer.header.id; mp->channel = radioBuffer.header.channel; assert(HOP_MAX <= PACKET_FLAGS_HOP_LIMIT_MASK); // If hopmax changes, carefully check this code mp->hop_limit = radioBuffer.header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; mp->hop_start = (radioBuffer.header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; mp->want_ack = !!(radioBuffer.header.flags & PACKET_FLAGS_WANT_ACK_MASK); mp->via_mqtt = !!(radioBuffer.header.flags & PACKET_FLAGS_VIA_MQTT_MASK); // If hop_start is not set, next_hop and relay_node are invalid (firmware <2.3) mp->next_hop = mp->hop_start == 0 ? NO_NEXT_HOP_PREFERENCE : radioBuffer.header.next_hop; mp->relay_node = mp->hop_start == 0 ? NO_RELAY_NODE : radioBuffer.header.relay_node; addReceiveMetadata(mp); mp->which_payload_variant = meshtastic_MeshPacket_encrypted_tag; // Mark that the payload is still encrypted at this point assert(((uint32_t)payloadLen) <= sizeof(mp->encrypted.bytes)); memcpy(mp->encrypted.bytes, radioBuffer.payload, payloadLen); mp->encrypted.size = payloadLen; printPacket("Lora RX", mp); #ifdef LED_LORA loraRxPacketObservable.notifyObservers(mp->from); #endif airTime->logAirtime(RX_LOG, rxMsec); deliverToReceiver(mp); } } } void RadioLibInterface::startReceive() { isReceiving = true; powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn); } void RadioLibInterface::pollMissedIrqs() { // RadioLibInterface::enableInterrupt uses EDGE-TRIGGERED interrupts. Poll as a backup to catch missed edges. if (isReceiving) { checkRxDoneIrqFlag(); } if (sendingPacket) { checkTxDoneIrqFlag(); } } void RadioLibInterface::resetAGC() { // Base implementation: no-op. Override in chip-specific subclasses. } void RadioLibInterface::checkRxDoneIrqFlag() { if (iface->checkIrq(RADIOLIB_IRQ_RX_DONE)) { LOG_WARN("caught missed RX_DONE"); notify(ISR_RX, true); } } void RadioLibInterface::checkTxDoneIrqFlag() { if (iface->checkIrq(RADIOLIB_IRQ_TX_DONE)) { LOG_WARN("caught missed TX_DONE"); notify(ISR_TX, true); } } void RadioLibInterface::configHardwareForSend() { powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn); } void RadioLibInterface::setStandby() { // neither sending nor receiving powerMon->clearState(meshtastic_PowerMon_State_Lora_RXOn); powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); } /** start an immediate transmit */ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp) { /* NOTE: Minimize the actions before startTransmit() to keep the time between channel scan and actual transmit as low as possible to avoid collisions. */ if (disabled || !config.lora.tx_enabled) { LOG_WARN("Drop Tx packet because LoRa Tx disabled"); #if !MESHTASTIC_EXCLUDE_BEACON // This packet may have already triggered a beacon radio switch in TRANSMIT_DELAY_COMPLETED; // since it never reaches completeSending() here, restore the radio so it isn't left on the // beacon config (which would also break RX on the home channel). MeshBeaconModule::clearTargetRadioSettings(txp); MeshBeaconModule::reconfigureForBeaconTX(this, nullptr); #endif packetPool.release(txp); return false; } else { configHardwareForSend(); // must be after setStandby size_t numbytes = beginSending(txp); int res = iface->startTransmit((uint8_t *)&radioBuffer, numbytes); if (res != RADIOLIB_ERR_NONE) { LOG_ERROR("startTransmit failed, error=%d", res); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_RADIO_SPI_BUG); // This send failed, but make sure to 'complete' it properly completeSending(); powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // Transmitter off now startReceive(); // Restart receive mode (because startTransmit failed to put us in xmit mode) } else { // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register // bits enableInterrupt(isrTxLevel0); lastTxStart = millis(); printPacket("Started Tx", txp); #ifdef LED_LORA digitalWrite(LED_LORA, LED_STATE_ON); #endif } return res == RADIOLIB_ERR_NONE; } }