/*
 * CodeProven — nRF24 Packet Link Tester
 * Original educational sketch for two ESP32-WROOM-32 + standard nRF24L01+ nodes.
 * Install the RF24 library by TMRh20 separately through Arduino Library Manager.
 *
 * Set LINK_NODE to 0 and upload to the sender. Set it to 1 and upload to the
 * receiver. The sender sends one short packet per second at minimum power; the
 * receiver only listens. This is a paired-device link test, not a spectrum tool.
 */
#include <SPI.h>
#include <RF24.h>

#define LINK_NODE 0  // 0 = sender A; change to 1 for receiver B before second upload

#if LINK_NODE != 0 && LINK_NODE != 1
#error "LINK_NODE must be 0 (sender A) or 1 (receiver B)."
#endif

// ESP32-WROOM-32 default VSPI: SCK=18, MISO=19, MOSI=23.
// Use GPIO 26/27 for CE/CSN to avoid the ESP32 boot-strapping pins.
constexpr uint8_t RADIO_CE_PIN = 26;
constexpr uint8_t RADIO_CSN_PIN = 27;
constexpr uint8_t RADIO_CHANNEL = 76;  // fixed channel; check local regulations
constexpr uint32_t SEND_INTERVAL_MS = 1000;
constexpr uint32_t PACKET_MAGIC = 0x43505231UL;  // "CPR1"

RF24 radio(RADIO_CE_PIN, RADIO_CSN_PIN);
const uint8_t ADDRESS_A[6] = "CP_A1";
const uint8_t ADDRESS_B[6] = "CP_B1";

struct LinkPacket {
  uint32_t magic;
  uint32_t sequence;
  uint32_t sentAtMs;
};
static_assert(sizeof(LinkPacket) == 12, "LinkPacket must fit the documented 12-byte payload.");

const uint8_t* localAddress = LINK_NODE == 0 ? ADDRESS_A : ADDRESS_B;
const uint8_t* peerAddress = LINK_NODE == 0 ? ADDRESS_B : ADDRESS_A;
uint32_t nextSequence = 1;
uint32_t sentCount = 0;
uint32_t acknowledgedCount = 0;
uint32_t failedCount = 0;
uint32_t receivedCount = 0;
uint32_t estimatedMissingCount = 0;
uint32_t lastSequence = 0;
uint32_t lastSendAtMs = 0;

void stopForRadioFault() {
  Serial.println(F("ERROR: nRF24 did not initialize. Check 3.3 V, GND, SPI, CE, and CSN."));
  while (true) {
    delay(1000);
  }
}

void setup() {
  Serial.begin(115200);
  delay(250);
  Serial.println();
  Serial.println(F("CodeProven nRF24 Packet Link Tester"));
  Serial.print(F("Node role: "));
  Serial.println(LINK_NODE == 0 ? F("A / sender") : F("B / receiver"));
  Serial.println(F("Fixed channel 76; minimum radio power; one packet per second."));

  if (!radio.begin()) {
    stopForRadioFault();
  }

  radio.setAutoAck(true);
  radio.setPALevel(RF24_PA_MIN);
  radio.setDataRate(RF24_250KBPS);
  radio.setChannel(RADIO_CHANNEL);
  radio.setRetries(3, 5);
  radio.setCRCLength(RF24_CRC_16);
  radio.openReadingPipe(1, localAddress);
  radio.openWritingPipe(peerAddress);
  radio.startListening();

  Serial.println(F("Ready. Compare sender ACK results with receiver sequence numbers."));
}

void sendOneTestPacket() {
  LinkPacket packet = {PACKET_MAGIC, nextSequence++, millis()};
  radio.stopListening();
  const bool acknowledged = radio.write(&packet, sizeof(packet));
  radio.startListening();

  ++sentCount;
  if (acknowledged) {
    ++acknowledgedCount;
  } else {
    ++failedCount;
  }

  Serial.print(F("TX seq="));
  Serial.print(packet.sequence);
  Serial.print(acknowledged ? F(" ACK") : F(" NO_ACK"));
  Serial.print(F(" | sent="));
  Serial.print(sentCount);
  Serial.print(F(" acked="));
  Serial.print(acknowledgedCount);
  Serial.print(F(" failed="));
  Serial.println(failedCount);
}

void receiveAvailablePackets() {
  while (radio.available()) {
    LinkPacket packet = {};
    radio.read(&packet, sizeof(packet));
    if (packet.magic != PACKET_MAGIC) {
      continue;
    }

    ++receivedCount;
    if (lastSequence != 0 && packet.sequence > lastSequence + 1UL) {
      estimatedMissingCount += packet.sequence - lastSequence - 1UL;
    }
    lastSequence = packet.sequence;

    Serial.print(F("RX seq="));
    Serial.print(packet.sequence);
    Serial.print(F(" | received="));
    Serial.print(receivedCount);
    Serial.print(F(" estimated_gaps="));
    Serial.println(estimatedMissingCount);
  }
}

void loop() {
  if (LINK_NODE == 0) {
    const uint32_t now = millis();
    if (now - lastSendAtMs >= SEND_INTERVAL_MS) {
      lastSendAtMs = now;
      sendOneTestPacket();
    }
  } else {
    receiveAvailablePackets();
  }
}
