#include "RadioLibInterface.h"
#include "MeshTypes.h"
#include "NodeDB.h"
#include "PowerMon.h"
#include "SPILock.h"
#include "Throttle.h"
#include "UptimeClock.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 <pb_decode.h>
#include <pb_encode.h>

#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 >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_TRACE("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_TRACE("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_TRACE("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 deepSleep)
{
    // A packet being actively transmitted has already left the TX queue (sendingPacket), so
    // check it separately. It only vetoes deep sleep: light sleep keeps the radio powered and
    // the TX finishes on its own, but deep sleep powers the radio down and would truncate the
    // packet on air.
    bool res = txQueue.empty() && !(deepSleep && isSending());
    if (!res) { // only print debug messages if we are vetoing sleep
        LOG_DEBUG("Radio wait to sleep, txEmpty=%d, txInFlight=%d", txQueue.empty(), isSending());
    }
    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%08x, 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_TRACE("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 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<uint8_t>(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.
*/
// In software-IRQ-poll mode (LORA_DIO1_SOFTWARE_POLL) a 1ms poll tick is almost always pending, so
// TX timers must be allowed to overwrite the pending notification or TX scheduling starves. On all
// other targets keep the historical non-overwriting behavior.
#ifdef LORA_DIO1_SOFTWARE_POLL
static constexpr bool txTimerOverwrite = true;
#else
static constexpr bool txTimerOverwrite = false;
#endif

// cppcheck-suppress constParameterPointer ; a function pointer can't meaningfully point to const
bool RadioLibInterface::isIsrTxCallback(void (*callback)())
{
    return callback == isrTxLevel0;
}

void RadioLibInterface::scheduleIrqPollTick()
{
    // Never overwrite a pending notification (especially TRANSMIT_DELAY_COMPLETED),
    // otherwise poll ticks would starve TX scheduling.
    //
    // There is a single notification slot, so while a TX is queued and the radio is busy receiving,
    // the self-rescheduling TRANSMIT_DELAY_COMPLETED timer (which does overwrite, see txTimerOverwrite)
    // can keep the slot and prevent a poll tick from being scheduled. In that window a completing
    // RX/TX is not seen by the poll; RadioInterface's pollMissedIrqs() (~1s) is the backup that
    // recovers it, so the effect is bounded added latency under heavy contention, not a lost event.
    notifyLater(1, ISR_POLL_TICK, false);
}

void RadioLibInterface::deliverPendingIrqFromPoll(PendingISR cause)
{
    disableInterrupt(); // stop polling; this is the poll-path equivalent of isrLevel0Common()
    notify(cause, true);
}

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 ISR_POLL_TICK:
        handleSoftwareLoraIrqPoll();
        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);
                const uint32_t now = Time::getMillis();
                // Not `long remaining = tx_after - millis()`: that uint32_t subtraction widens to
                // ~4.29e9 where long is 64-bit (portduino), rescheduling a due packet ~49.7 days out.
                if (txp->tx_after && !Throttle::deadlinePassedAt(now, txp->tx_after)) {
                    // There's still some delay pending on this packet, so resume waiting for it to elapse
                    notifyLater(txp->tx_after - now, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite);
#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, drop 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_TRACE("%d packets in 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, txTimerOverwrite);
    } 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_TRACE("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, txTimerOverwrite); // 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, txTimerOverwrite); // 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_TRACE("Move queued packet to late rebroadcast window %ums from now", (uint32_t)(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("Drop pending-TX packet 0x%08x, 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 while not in rx mode");
        return;
    }

    isReceiving = false;

    // read the number of actually received bytes
    size_t length = iface->getPacketLength();

    // Some drivers report this as a 16 bit value, so a bad readback can overrun radioBuffer in readData()
    if (length > sizeof(radioBuffer)) {
        LOG_ERROR("Ignore rx packet, bad length %u", (unsigned int)length);
        rxBad++;
        return;
    }

    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 rx packet, 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();
            if (!mp) {
                airTime->logAirtime(RX_LOG, rxMsec);
                return;
            }

            // 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: 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;
    }
}
