// Serial LoRa/FSK modem for Heltec V3/V4 with a RAK-style P2P AT command surface.
#include <Arduino.h>
#include <Preferences.h>
#include <SPI.h>
#include <RadioLib.h>
#include <mbedtls/aes.h>
#include <esp_system.h>
#include <esp_mac.h>
#include <esp_sleep.h>
#include <esp_chip_info.h>
#include <driver/uart.h>

#if __has_include("rom/usb/usb_uart_bridge.h")
#include <rom/usb/usb_uart_bridge.h>
#define HAS_USB_BOOTLOADER_RESET 1
#else
#define HAS_USB_BOOTLOADER_RESET 0
#endif

#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
class DualConsoleSerial {
 public:
  void begin(uint32_t baud) {
    usb_serial.begin(baud);
    uart0.begin(baud, SERIAL_8N1, 44, 43);
  }

  // Forward to both underlying transports. On Heltec V3/V4 the host
  // typically opens the USB CDC port, so growing that buffer is what
  // prevents long AT+PSEND hex lines (~400 chars) from being dropped
  // while the loop is busy in handleReceive / radio.transmit. The
  // hardware UART gets the same treatment for the alt console.
  void setRxBufferSize(size_t size) {
    usb_serial.setRxBufferSize(size);
    uart0.setRxBufferSize(size);
  }

  void end() {
    usb_serial.end();
    uart0.end();
  }

  void flush() {
    usb_serial.flush();
    uart0.flush();
  }

  operator bool() const {
    return static_cast<bool>(usb_serial);
  }

  int available() {
    return usb_serial.available() + uart0.available();
  }

  int read() {
    if (usb_serial.available() > 0) {
      return usb_serial.read();
    }
    return uart0.read();
  }

  template <typename T>
  size_t print(const T& value) {
    const size_t usb_count = usb_serial.print(value);
    const size_t uart_count = uart0.print(value);
    return usb_count > uart_count ? usb_count : uart_count;
  }

  template <typename T>
  size_t print(const T& value, int format) {
    const size_t usb_count = usb_serial.print(value, format);
    const size_t uart_count = uart0.print(value, format);
    return usb_count > uart_count ? usb_count : uart_count;
  }

  size_t print(const __FlashStringHelper* value) {
    const size_t usb_count = usb_serial.print(value);
    const size_t uart_count = uart0.print(value);
    return usb_count > uart_count ? usb_count : uart_count;
  }

  template <typename T>
  size_t println(const T& value) {
    const size_t usb_count = usb_serial.println(value);
    const size_t uart_count = uart0.println(value);
    return usb_count > uart_count ? usb_count : uart_count;
  }

  template <typename T>
  size_t println(const T& value, int format) {
    const size_t usb_count = usb_serial.println(value, format);
    const size_t uart_count = uart0.println(value, format);
    return usb_count > uart_count ? usb_count : uart_count;
  }

  size_t println(const __FlashStringHelper* value) {
    const size_t usb_count = usb_serial.println(value);
    const size_t uart_count = uart0.println(value);
    return usb_count > uart_count ? usb_count : uart_count;
  }

  size_t println() {
    const size_t usb_count = usb_serial.println();
    const size_t uart_count = uart0.println();
    return usb_count > uart_count ? usb_count : uart_count;
  }

 private:
  decltype(::Serial)& usb_serial = ::Serial;
  HardwareSerial uart0 = HardwareSerial(0);
};

static DualConsoleSerial ConsoleSerial;
#define Serial ConsoleSerial
#endif

// Heltec WiFi LoRa 32 V3/V4 SX1262 wiring.
static constexpr uint8_t LORA_NSS_PIN = 8;
static constexpr uint8_t LORA_DIO1_PIN = 14;
static constexpr uint8_t LORA_NRST_PIN = 12;
static constexpr uint8_t LORA_BUSY_PIN = 13;
static constexpr uint8_t LORA_SCK_PIN = 9;
static constexpr uint8_t LORA_MISO_PIN = 11;
static constexpr uint8_t LORA_MOSI_PIN = 10;

static constexpr unsigned long SERIAL_BAUD = 115200;
static constexpr size_t MAX_FRAME_SIZE = 255;
static constexpr float HELTEC_TCXO_VOLTAGE = 1.8f;
static constexpr uint8_t SETTINGS_VERSION = 4;
static constexpr uint8_t BATTERY_ADC_PIN = 1;
static constexpr uint8_t SYSV_MODE_PIN = 37;
static constexpr uint8_t ACTIVITY_LED_PIN = 35;

#if defined(HELTEC_V4) || defined(HELTEC_V4_OLED) || defined(HELTEC_V4_TFT) || defined(ARDUINO_HELTEC_WIFI_LORA_32_V4)
static constexpr uint8_t BATTERY_ADC_ENABLE_LEVEL = HIGH;
static constexpr uint8_t BATTERY_ADC_DISABLE_LEVEL = LOW;
static constexpr char MODEM_VERSION[] = "HeltecV4-LoRaModem RUI3-Emu 1.1";
static constexpr char HW_MODEL[] = "Heltec V4 SX1262";
#else
static constexpr uint8_t BATTERY_ADC_ENABLE_LEVEL = LOW;
static constexpr uint8_t BATTERY_ADC_DISABLE_LEVEL = HIGH;
static constexpr char MODEM_VERSION[] = "HeltecV3-LoRaModem RUI3-Emu 1.1";
static constexpr char HW_MODEL[] = "Heltec V3 SX1262";
#endif

static constexpr float BATTERY_VOLTAGE_MULTIPLIER = 4.9f * 1.045f;
static constexpr unsigned long LPM_IDLE_DELAY_MS = 25;

static constexpr char CLI_VERSION[] = "1.1.4";
static constexpr char API_VERSION[] = "1.1.4";

enum class ModulationMode : uint8_t {
  Fsk = 0,
  LoRa = 1,
};

enum class RakReceivePolicy : uint8_t {
  Disabled = 0,
  ContinuousTx,
  Continuous,
  OneShot,
  Timed,
};

enum class RakError : uint8_t {
  Generic,
  Parameter,
  Busy,
  Rx,
  TooLong,
};

struct ModemSettings {
  ModulationMode modulation = ModulationMode::LoRa;
  uint32_t rfFrequencyHz = 915000000UL;
  int8_t outputPower = 14;

  uint8_t loraSpreadingFactor = 7;
  uint8_t loraBandwidthIndex = 7;  // 0..9 => 7.8kHz..500kHz, 7/8/9 are 125/250/500kHz.
  uint8_t loraCodingRate = 1;      // 1..4 => 4/5..4/8
  uint16_t loraPreambleLength = 8;
  uint16_t loraSymbolTimeout = 0;  // 0 = keep library default/max.

  uint32_t fskBitRate = 50000;        // bps
  uint32_t fskFrequencyDeviation = 25000;  // Hz
  // Keep defaults spec-valid: bitRate + 2*freqDev = 100 kHz <= 117.3 kHz.
  uint32_t fskRxBandwidth = 117300;   // Hz, valid SX1262 GFSK RX bandwidth.
  uint16_t fskPreambleLength = 16;    // bits
  uint8_t fskDataShaping = RADIOLIB_SHAPING_NONE;
  uint8_t fskEncoding = RADIOLIB_ENCODING_NRZ;
  bool fixedLengthPayload = false;
  uint8_t fixedPayloadLength = MAX_FRAME_SIZE;

  uint8_t syncWord[8] = {0x14, 0x24};
  uint8_t syncWordLength = 2;

  bool payloadCryptEnabled = false;
  uint8_t payloadKey[16] = {0};
  uint8_t cryptoIv[16] = {0};
  uint32_t baudRate = SERIAL_BAUD;
  bool lowPowerEnabled = false;
  bool cadEnabled = false;
};

Preferences prefs;
ModemSettings settings;
int lastRadioState = RADIOLIB_ERR_NONE;

enum class FskInitStage : uint8_t {
  None = 0,
  ValidateProfile,
  BeginFsk,
  SetFrequency,
  SetOutputPower,
  SetDataShaping,
  SetEncoding,
  SetSyncWord,
  SetPacketMode,
  SetPacketReceivedAction,
  ApplyReceiveState,
};

struct FskInitDiagnostics {
  FskInitStage stage = FskInitStage::None;
  int state = RADIOLIB_ERR_NONE;
  uint32_t bitRate = 0;
  uint32_t frequencyDeviation = 0;
  uint32_t rxBandwidth = 0;
  uint32_t selectedRxBandwidth = 0;
  uint8_t beginVariant = 0;
  uint8_t resetAttempts = 0;
  uint32_t rfFrequencyHz = 0;
  int8_t outputPower = 0;
  uint8_t syncWordLength = 0;
  int shapingState = RADIOLIB_ERR_NONE;
  int encodingState = RADIOLIB_ERR_NONE;
  int syncWordState = RADIOLIB_ERR_NONE;
  int packetModeState = RADIOLIB_ERR_NONE;
  int applyReceiveState = RADIOLIB_ERR_NONE;
};

FskInitDiagnostics lastFskInitDiagnostics;

SX1262 radio = new Module(LORA_NSS_PIN, LORA_DIO1_PIN, LORA_NRST_PIN, LORA_BUSY_PIN);

volatile bool packetReceived = false;
volatile uint32_t rxIrqCount = 0;
uint32_t rxIrqCountReported = 0;
bool rxDiagVerbose = false;
String commandBuffer;
bool pendingCommandCr = false;

bool receiveActive = false;
RakReceivePolicy receivePolicy = RakReceivePolicy::Disabled;
uint32_t receiveWindowMs = 0;
unsigned long receiveDeadline = 0;
unsigned long lastCommandAt = 0;

uint8_t buildMonthFromDate(const char* date) {
  if (strncmp(date, "Jan", 3) == 0) return 1;
  if (strncmp(date, "Feb", 3) == 0) return 2;
  if (strncmp(date, "Mar", 3) == 0) return 3;
  if (strncmp(date, "Apr", 3) == 0) return 4;
  if (strncmp(date, "May", 3) == 0) return 5;
  if (strncmp(date, "Jun", 3) == 0) return 6;
  if (strncmp(date, "Jul", 3) == 0) return 7;
  if (strncmp(date, "Aug", 3) == 0) return 8;
  if (strncmp(date, "Sep", 3) == 0) return 9;
  if (strncmp(date, "Oct", 3) == 0) return 10;
  if (strncmp(date, "Nov", 3) == 0) return 11;
  if (strncmp(date, "Dec", 3) == 0) return 12;
  return 0;
}

String getBuildTimeString() {
  const char* buildDate = __DATE__;
  const char* buildClock = __TIME__;

  const uint16_t year = static_cast<uint16_t>((buildDate[7] - '0') * 1000 + (buildDate[8] - '0') * 100 +
                                              (buildDate[9] - '0') * 10 + (buildDate[10] - '0'));
  const uint8_t month = buildMonthFromDate(buildDate);
  const uint8_t day = static_cast<uint8_t>(((buildDate[4] == ' ') ? 0 : (buildDate[4] - '0')) * 10 + (buildDate[5] - '0'));

  char formatted[17];
  snprintf(formatted, sizeof(formatted), "%04u%02u%02u-%c%c%c%c%c%c", year, month, day, buildClock[0], buildClock[1],
           buildClock[3], buildClock[4], buildClock[6], buildClock[7]);
  return String(formatted);
}

void setPacketReceivedFlag() {
  packetReceived = true;
  rxIrqCount++;
}

void setLoraActivityLed(bool enabled) {
  digitalWrite(ACTIVITY_LED_PIN, enabled ? HIGH : LOW);
}

const char* modulationName(ModulationMode mode) {
  return (mode == ModulationMode::LoRa) ? "LORA" : "FSK";
}

float bandwidthIndexToKHz(uint8_t index) {
  switch (index) {
    case 0:
      return 7.8f;
    case 1:
      return 10.4f;
    case 2:
      return 15.6f;
    case 3:
      return 20.8f;
    case 4:
      return 31.25f;
    case 5:
      return 41.7f;
    case 6:
      return 62.5f;
    case 7:
      return 125.0f;
    case 8:
      return 250.0f;
    case 9:
      return 500.0f;
    default:
      return 0.0f;
  }
}

String loraBandwidthLabel(uint8_t index) {
  switch (index) {
    case 0:
      return String(7);
    case 1:
      return String(10);
    case 2:
      return String(15);
    case 3:
      return String(20);
    case 4:
      return String(31);
    case 5:
      return String(41);
    case 6:
      return String(62);
    case 7:
      return String(125);
    case 8:
      return String(250);
    case 9:
      return String(500);
    default:
      return String();
  }
}

String loraBandwidthCode(uint8_t index) {
  switch (index) {
    case 7:
      return String(0);
    case 8:
      return String(1);
    case 9:
      return String(2);
    case 0:
      return String(3);
    case 1:
      return String(4);
    case 2:
      return String(5);
    case 3:
      return String(6);
    case 4:
      return String(7);
    case 5:
      return String(8);
    case 6:
      return String(9);
    default:
      return String();
  }
}

bool isHexChar(char c) {
  return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}

uint8_t hexValue(char c) {
  if (c >= '0' && c <= '9') {
    return static_cast<uint8_t>(c - '0');
  }
  if (c >= 'A' && c <= 'F') {
    return static_cast<uint8_t>(c - 'A' + 10);
  }
  return static_cast<uint8_t>(c - 'a' + 10);
}

String bytesToHex(const uint8_t* data, size_t length) {
  static constexpr char HEX_DIGITS[] = "0123456789ABCDEF";
  String output;
  output.reserve(length * 2);
  for (size_t index = 0; index < length; ++index) {
    output += HEX_DIGITS[(data[index] >> 4) & 0x0F];
    output += HEX_DIGITS[data[index] & 0x0F];
  }
  return output;
}

String bytesToHexLower(const uint8_t* data, size_t length) {
  static constexpr char HEX_DIGITS[] = "0123456789abcdef";
  String output;
  output.reserve(length * 2);
  for (size_t index = 0; index < length; ++index) {
    output += HEX_DIGITS[(data[index] >> 4) & 0x0F];
    output += HEX_DIGITS[data[index] & 0x0F];
  }
  return output;
}

String formatMacAddress(const uint8_t* mac) {
  static constexpr char HEX_DIGITS[] = "0123456789abcdef";
  String output;
  output.reserve(17);
  for (size_t index = 0; index < 6; ++index) {
    if (index != 0) {
      output += ':';
    }
    output += HEX_DIGITS[(mac[index] >> 4) & 0x0F];
    output += HEX_DIGITS[mac[index] & 0x0F];
  }
  return output;
}

void readBleMac(uint8_t* mac) {
#if defined(ESP_MAC_BT)
  esp_read_mac(mac, ESP_MAC_BT);
#else
  esp_read_mac(mac, ESP_MAC_WIFI_STA);
#endif
}

String getBleMacString() {
  uint8_t mac[6];
  readBleMac(mac);
  return formatMacAddress(mac);
}

String getHardwareId() {
  uint8_t mac[6];
  readBleMac(mac);
  return bytesToHexLower(mac + 2, 4);
}

String getSerialNumber() {
  uint8_t mac[6];
  readBleMac(mac);

  uint64_t serial = 0;
  for (size_t index = 0; index < 6; ++index) {
    serial = (serial << 8) | mac[index];
  }

  return String(static_cast<unsigned long long>(serial));
}

String getBootVersionString() {
  esp_chip_info_t chipInfo;
  esp_chip_info(&chipInfo);
  String value = ESP.getChipModel();
  value += F(" rev ");
  value += String(chipInfo.revision);
  return value;
}

float readSystemVoltage() {
  pinMode(SYSV_MODE_PIN, OUTPUT);
  digitalWrite(SYSV_MODE_PIN, BATTERY_ADC_ENABLE_LEVEL);
  delay(10);
  analogSetPinAttenuation(BATTERY_ADC_PIN, ADC_2_5db);
  const float voltage = (static_cast<float>(analogReadMilliVolts(BATTERY_ADC_PIN)) * BATTERY_VOLTAGE_MULTIPLIER) / 1000.0f;
  digitalWrite(SYSV_MODE_PIN, BATTERY_ADC_DISABLE_LEVEL);
  pinMode(SYSV_MODE_PIN, ANALOG);
  return voltage;
}

bool parseHexBytes(const String& text, uint8_t* output, size_t expectedLength) {
  if (text.length() != static_cast<int>(expectedLength * 2)) {
    return false;
  }

  for (size_t index = 0; index < expectedLength; ++index) {
    const char high = text[index * 2];
    const char low = text[index * 2 + 1];
    if (!isHexChar(high) || !isHexChar(low)) {
      return false;
    }
    output[index] = static_cast<uint8_t>((hexValue(high) << 4) | hexValue(low));
  }

  return true;
}

bool parseHexPayload(const String& text, uint8_t* output, size_t& outputLength) {
  if ((text.length() % 2) != 0) {
    return false;
  }

  outputLength = text.length() / 2;
  if (outputLength > MAX_FRAME_SIZE) {
    return false;
  }

  for (size_t index = 0; index < outputLength; ++index) {
    const char high = text[index * 2];
    const char low = text[index * 2 + 1];
    if (!isHexChar(high) || !isHexChar(low)) {
      return false;
    }
    output[index] = static_cast<uint8_t>((hexValue(high) << 4) | hexValue(low));
  }

  return true;
}

bool parseLongStrict(const String& text, long& value, int base = 10) {
  if (text.length() == 0) {
    return false;
  }

  char buffer[32];
  if (text.length() >= static_cast<int>(sizeof(buffer))) {
    return false;
  }

  text.toCharArray(buffer, sizeof(buffer));
  char* endPtr = nullptr;
  value = strtol(buffer, &endPtr, base);
  return endPtr != buffer && *endPtr == '\0';
}

bool parseUnsignedLongFlexible(const String& text, unsigned long& value) {
  if (text.length() == 0) {
    return false;
  }

  char buffer[32];
  if (text.length() >= static_cast<int>(sizeof(buffer))) {
    return false;
  }

  text.toCharArray(buffer, sizeof(buffer));
  char* endPtr = nullptr;
  value = strtoul(buffer, &endPtr, 0);
  return endPtr != buffer && *endPtr == '\0';
}

String nextToken(const String& input, int& cursor, char separator) {
  if (cursor < 0 || cursor >= static_cast<int>(input.length())) {
    return String();
  }

  const int next = input.indexOf(separator, cursor);
  if (next < 0) {
    String token = input.substring(cursor);
    cursor = input.length();
    return token;
  }

  String token = input.substring(cursor, next);
  cursor = next + 1;
  return token;
}

bool isValidFrequency(uint32_t hz) {
  return hz >= 150000000UL && hz <= 960000000UL;
}

bool isValidLoRaSpreadingFactor(long value) {
  return value >= 5 && value <= 12;
}

bool isValidLoRaCodingRate(long value) {
  return value >= 0 && value <= 3;
}

bool isValidLoRaPreamble(long value) {
  return value >= 5 && value <= 65535;
}

bool isValidOutputPower(long value) {
  return value >= 5 && value <= 22;
}

bool isValidLoRaSymbolTimeout(long value) {
  return value >= 0 && value <= 248;
}

bool isValidReceiveValue(long value) {
  return value >= 0 && value <= 65535;
}

bool isValidFskBitRate(unsigned long value) {
  return value >= 600UL && value <= 300000UL;
}

bool isValidFskFrequencyDeviation(unsigned long value) {
  return value >= 600UL && value <= 200000UL;
}

static const uint32_t SX1262_GFSK_BANDWIDTHS_HZ[] = {
    4800UL, 5800UL, 7300UL, 9700UL, 11700UL, 14600UL, 19500UL,
    23400UL, 29300UL, 39000UL, 46900UL, 58600UL, 78200UL, 93800UL,
    117300UL, 156200UL, 187200UL, 234300UL, 312000UL, 373600UL, 467000UL};

bool isValidSx1262FskBandwidth(uint32_t value) {
  for (size_t i = 0; i < (sizeof(SX1262_GFSK_BANDWIDTHS_HZ) / sizeof(SX1262_GFSK_BANDWIDTHS_HZ[0])); ++i) {
    if (SX1262_GFSK_BANDWIDTHS_HZ[i] == value) {
      return true;
    }
  }
  return false;
}

bool isValidFskBandwidth(unsigned long value) {
  return isValidSx1262FskBandwidth(value);
}

bool isValidFskPreamble(long value) {
  return value >= 5 && value <= 65535;
}

bool isValidSx1262FskProfile(const ModemSettings& candidate) {
  if (!isValidFskBitRate(candidate.fskBitRate) ||
      !isValidFskFrequencyDeviation(candidate.fskFrequencyDeviation) ||
      !isValidFskBandwidth(candidate.fskRxBandwidth)) {
    return false;
  }

  // SX1262 GFSK occupied bandwidth should fit within selected RX bandwidth.
  const uint64_t occupiedBandwidth =
      static_cast<uint64_t>(candidate.fskBitRate) +
      (2ULL * static_cast<uint64_t>(candidate.fskFrequencyDeviation));
  return occupiedBandwidth <= static_cast<uint64_t>(candidate.fskRxBandwidth);
}

const char* fskInitStageName(FskInitStage stage) {
  switch (stage) {
    case FskInitStage::None:
      return "NONE";
    case FskInitStage::ValidateProfile:
      return "VALIDATE";
    case FskInitStage::BeginFsk:
      return "BEGIN_FSK";
    case FskInitStage::SetFrequency:
      return "SET_FREQ";
    case FskInitStage::SetOutputPower:
      return "SET_POWER";
    case FskInitStage::SetDataShaping:
      return "SET_SHAPING";
    case FskInitStage::SetEncoding:
      return "SET_ENCODING";
    case FskInitStage::SetSyncWord:
      return "SET_SYNC";
    case FskInitStage::SetPacketMode:
      return "SET_PKT_MODE";
    case FskInitStage::SetPacketReceivedAction:
      return "SET_RX_ACTION";
    case FskInitStage::ApplyReceiveState:
      return "APPLY_RX";
  }
  return "UNKNOWN";
}

void printLastFskInitDiagnostics() {
  Serial.print(F("+ERR=FSKDIAG stage="));
  Serial.print(fskInitStageName(lastFskInitDiagnostics.stage));
  Serial.print(F(" state="));
  Serial.print(lastFskInitDiagnostics.state);
  Serial.print(F(" br="));
  Serial.print(lastFskInitDiagnostics.bitRate);
  Serial.print(F(" fdev="));
  Serial.print(lastFskInitDiagnostics.frequencyDeviation);
  Serial.print(F(" bw="));
  Serial.print(lastFskInitDiagnostics.rxBandwidth);
  Serial.print(F(" selBw="));
  Serial.print(lastFskInitDiagnostics.selectedRxBandwidth);
  Serial.print(F(" beginVar="));
  Serial.print(lastFskInitDiagnostics.beginVariant);
  Serial.print(F(" resets="));
  Serial.print(lastFskInitDiagnostics.resetAttempts);
  Serial.print(F(" freq="));
  Serial.print(lastFskInitDiagnostics.rfFrequencyHz);
  Serial.print(F(" pwr="));
  Serial.print(lastFskInitDiagnostics.outputPower);
  Serial.print(F(" swlen="));
  Serial.print(lastFskInitDiagnostics.syncWordLength);
  Serial.print(F(" optShape="));
  Serial.print(lastFskInitDiagnostics.shapingState);
  Serial.print(F(" optEnc="));
  Serial.print(lastFskInitDiagnostics.encodingState);
  Serial.print(F(" optSync="));
  Serial.print(lastFskInitDiagnostics.syncWordState);
  Serial.print(F(" optPkt="));
  Serial.print(lastFskInitDiagnostics.packetModeState);
  Serial.print(F(" rxState="));
  Serial.println(lastFskInitDiagnostics.applyReceiveState);
}

void sanitizeSettings(ModemSettings& candidate) {
  const ModemSettings defaults;

  if (!isValidFrequency(candidate.rfFrequencyHz)) {
    candidate.rfFrequencyHz = defaults.rfFrequencyHz;
  }
  if (!isValidOutputPower(candidate.outputPower)) {
    candidate.outputPower = defaults.outputPower;
  }

  if (candidate.modulation == ModulationMode::LoRa) {
    if (!isValidLoRaSpreadingFactor(candidate.loraSpreadingFactor)) {
      candidate.loraSpreadingFactor = defaults.loraSpreadingFactor;
    }
    if (candidate.loraBandwidthIndex > 9) {
      candidate.loraBandwidthIndex = defaults.loraBandwidthIndex;
    }
    if (candidate.loraCodingRate < 1 || candidate.loraCodingRate > 4) {
      candidate.loraCodingRate = defaults.loraCodingRate;
    }
    if (!isValidLoRaPreamble(candidate.loraPreambleLength)) {
      candidate.loraPreambleLength = defaults.loraPreambleLength;
    }
    if (!isValidLoRaSymbolTimeout(candidate.loraSymbolTimeout)) {
      candidate.loraSymbolTimeout = defaults.loraSymbolTimeout;
    }
    candidate.syncWordLength = 2;
  } else {
    if (!isValidFskBitRate(candidate.fskBitRate)) {
      candidate.fskBitRate = defaults.fskBitRate;
    }
    if (!isValidFskFrequencyDeviation(candidate.fskFrequencyDeviation)) {
      candidate.fskFrequencyDeviation = defaults.fskFrequencyDeviation;
    }
    if (!isValidFskBandwidth(candidate.fskRxBandwidth)) {
      candidate.fskRxBandwidth = defaults.fskRxBandwidth;
    }
    if (!isValidFskPreamble(candidate.fskPreambleLength)) {
      candidate.fskPreambleLength = defaults.fskPreambleLength;
    }
    if (!isValidSx1262FskProfile(candidate)) {
      candidate.fskBitRate = defaults.fskBitRate;
      candidate.fskFrequencyDeviation = defaults.fskFrequencyDeviation;
      candidate.fskRxBandwidth = defaults.fskRxBandwidth;
    }
    if (candidate.fskDataShaping != RADIOLIB_SHAPING_NONE &&
        candidate.fskDataShaping != RADIOLIB_SHAPING_0_3 &&
        candidate.fskDataShaping != RADIOLIB_SHAPING_0_5 &&
        candidate.fskDataShaping != RADIOLIB_SHAPING_0_7 &&
        candidate.fskDataShaping != RADIOLIB_SHAPING_1_0) {
      candidate.fskDataShaping = defaults.fskDataShaping;
    }
    if (candidate.fskEncoding != RADIOLIB_ENCODING_NRZ && candidate.fskEncoding != 2) {
      candidate.fskEncoding = defaults.fskEncoding;
    }
    if (candidate.syncWordLength < 2 || candidate.syncWordLength > sizeof(candidate.syncWord)) {
      candidate.syncWordLength = defaults.syncWordLength;
    }
  }

  if (candidate.fixedPayloadLength == 0 || candidate.fixedPayloadLength > MAX_FRAME_SIZE) {
    candidate.fixedPayloadLength = MAX_FRAME_SIZE;
  }
}

void applyKnownGoodFskProfile(ModemSettings& candidate) {
  const ModemSettings defaults;
  candidate.modulation = ModulationMode::Fsk;
  candidate.fskBitRate = defaults.fskBitRate;
  candidate.fskFrequencyDeviation = defaults.fskFrequencyDeviation;
  candidate.fskRxBandwidth = defaults.fskRxBandwidth;
  candidate.fskPreambleLength = defaults.fskPreambleLength;
  candidate.fskDataShaping = defaults.fskDataShaping;
  candidate.fskEncoding = defaults.fskEncoding;
  candidate.fixedLengthPayload = false;
  candidate.fixedPayloadLength = MAX_FRAME_SIZE;
  candidate.syncWord[0] = defaults.syncWord[0];
  candidate.syncWord[1] = defaults.syncWord[1];
  candidate.syncWordLength = 2;
}

bool parseLoRaBandwidthValue(const String& token, uint8_t& bandwidthIndex) {
  long value = 0;
  if (!parseLongStrict(token, value)) {
    return false;
  }

  switch (value) {
    case 0:
      bandwidthIndex = 7;
      return true;
    case 1:
      bandwidthIndex = 8;
      return true;
    case 2:
      bandwidthIndex = 9;
      return true;
    case 3:
      bandwidthIndex = 0;
      return true;
    case 4:
      bandwidthIndex = 1;
      return true;
    case 5:
      bandwidthIndex = 2;
      return true;
    case 6:
      bandwidthIndex = 3;
      return true;
    case 7:
      bandwidthIndex = 4;
      return true;
    case 8:
      bandwidthIndex = 5;
      return true;
    case 9:
      bandwidthIndex = 6;
      return true;
    default:
      return false;
  }
}

bool parseSyncWordValue(const String& text, uint8_t* buffer, uint8_t& length, ModulationMode mode) {
  String normalized = text;
  if (normalized.startsWith(F("0x")) || normalized.startsWith(F("0X"))) {
    normalized = normalized.substring(2);
  }

  if (mode == ModulationMode::LoRa) {
    if (normalized.length() == 2) {
      normalized = String("00") + normalized;
    }
    if (normalized.length() != 4 || !parseHexBytes(normalized, buffer, 2)) {
      return false;
    }
    length = 2;
    return true;
  }

  if ((normalized.length() % 2) != 0 || normalized.length() == 0 || normalized.length() > 16) {
    return false;
  }

  length = static_cast<uint8_t>(normalized.length() / 2);
  return parseHexBytes(normalized, buffer, length);
}

bool parseKeyOrIv(const String& text, uint8_t* buffer) {
  return parseHexBytes(text, buffer, 16);
}

bool parsePKey(const String& text, uint8_t* buffer) {
  if (!parseHexBytes(text, buffer, 8)) {
    return false;
  }
  memset(buffer + 8, 0, 8);
  return true;
}

bool parsePayloadCryptValue(const String& text, bool& enabled) {
  if (text == F("1") || text.equalsIgnoreCase("ON")) {
    enabled = true;
    return true;
  }
  if (text == F("0") || text.equalsIgnoreCase("OFF")) {
    enabled = false;
    return true;
  }
  return false;
}

bool parseModulationValue(const String& text, ModulationMode& mode) {
  if (text == F("1") || text.equalsIgnoreCase("LORA")) {
    mode = ModulationMode::LoRa;
    return true;
  }
  if (text == F("0") || text.equalsIgnoreCase("FSK")) {
    mode = ModulationMode::Fsk;
    return true;
  }
  return false;
}

bool parseEncodingValue(const String& text, uint8_t& encoding) {
  if (text == F("0") || text.equalsIgnoreCase("NRZ")) {
    encoding = RADIOLIB_ENCODING_NRZ;
    return true;
  }
  if (text == F("2") || text.equalsIgnoreCase("WHITENING")) {
    encoding = 2;
    return true;
  }
  return false;
}

String encodingLabel(uint8_t encoding) {
  switch (encoding) {
    case RADIOLIB_ENCODING_NRZ:
      return F("NRZ");
    case 2:
      return F("WHITENING");
    default:
      return String(encoding);
  }
}

bool parseShapingValue(const String& text, uint8_t& shaping) {
  if (text == F("0") || text.equalsIgnoreCase("NONE")) {
    shaping = RADIOLIB_SHAPING_NONE;
    return true;
  }
  if (text == F("0.3") || text == F("3")) {
    shaping = RADIOLIB_SHAPING_0_3;
    return true;
  }
  if (text == F("0.5") || text == F("5")) {
    shaping = RADIOLIB_SHAPING_0_5;
    return true;
  }
  if (text == F("0.7") || text == F("7")) {
    shaping = RADIOLIB_SHAPING_0_7;
    return true;
  }
  if (text == F("1.0") || text == F("10") || text == F("1")) {
    shaping = RADIOLIB_SHAPING_1_0;
    return true;
  }
  return false;
}

String shapingLabel(uint8_t shaping) {
  switch (shaping) {
    case RADIOLIB_SHAPING_NONE:
      return F("NONE");
    case RADIOLIB_SHAPING_0_3:
      return F("0.3");
    case RADIOLIB_SHAPING_0_5:
      return F("0.5");
    case RADIOLIB_SHAPING_0_7:
      return F("0.7");
    case RADIOLIB_SHAPING_1_0:
      return F("1.0");
    default:
      return String(shaping);
  }
}

void printStatusLine(const __FlashStringHelper* status) {
  Serial.print(F("\r\n"));
  Serial.print(status);
  Serial.print(F("\r\n"));
}

void replyOk() {
  printStatusLine(F("OK"));
}

void replyError(RakError error) {
  switch (error) {
    case RakError::Generic:
      printStatusLine(F("AT_ERROR"));
      return;
    case RakError::Parameter:
      printStatusLine(F("AT_PARAM_ERROR"));
      return;
    case RakError::Busy:
      printStatusLine(F("AT_BUSY_ERROR"));
      return;
    case RakError::Rx:
      printStatusLine(F("AT_RX_ERROR"));
      return;
    case RakError::TooLong:
      printStatusLine(F("AT_TEST_PARAM_OVERFLOW"));
      return;
  }
}

void printCommandHelp(const char* command, const String& value) {
  Serial.print(command);
  Serial.print(F(": "));
  Serial.print(value);
  Serial.print(F("\r\n"));
  replyOk();
}

void printCommandHelp(const char* command, long value) {
  printCommandHelp(command, String(value));
}

void printCommandValue(const char* command, const String& value) {
  Serial.print(command);
  Serial.print('=');
  Serial.print(value);
  Serial.print(F("\r\n"));
  replyOk();
}

void printCommandValue(const char* command, long value) {
  printCommandValue(command, String(value));
}

bool configChangesBlocked() {
  return receiveActive && receivePolicy != RakReceivePolicy::Disabled;
}

bool txBlocked() {
  return receiveActive && receivePolicy != RakReceivePolicy::Disabled && receivePolicy != RakReceivePolicy::ContinuousTx;
}

void saveSettings() {
  prefs.putUChar("cfgver", SETTINGS_VERSION);
  prefs.putUChar("mod", static_cast<uint8_t>(settings.modulation));
  prefs.putULong("freq", settings.rfFrequencyHz);
  prefs.putChar("pwr", settings.outputPower);

  prefs.putUChar("lsf", settings.loraSpreadingFactor);
  prefs.putUChar("lbw", settings.loraBandwidthIndex);
  prefs.putUChar("lcr", settings.loraCodingRate);
  prefs.putUShort("lpre", settings.loraPreambleLength);
  prefs.putUShort("lsym", settings.loraSymbolTimeout);

  prefs.putULong("fbr", settings.fskBitRate);
  prefs.putULong("fdev", settings.fskFrequencyDeviation);
  prefs.putULong("fbw", settings.fskRxBandwidth);
  prefs.putUShort("fpre", settings.fskPreambleLength);
  prefs.putUChar("fshape", settings.fskDataShaping);
  prefs.putUChar("fenc", settings.fskEncoding);
  prefs.putBool("ffix", settings.fixedLengthPayload);
  prefs.putUChar("ffixlen", settings.fixedPayloadLength);

  prefs.putBytes("sync", settings.syncWord, sizeof(settings.syncWord));
  prefs.putUChar("synclen", settings.syncWordLength);

  prefs.putBool("crypt", settings.payloadCryptEnabled);
  prefs.putBytes("pkey", settings.payloadKey, sizeof(settings.payloadKey));
  prefs.putBytes("civ", settings.cryptoIv, sizeof(settings.cryptoIv));
  prefs.putULong("baud", settings.baudRate);
  prefs.putBool("lpm", settings.lowPowerEnabled);
  prefs.putBool("cad", settings.cadEnabled);
}

void loadSettings() {
  settings.modulation = static_cast<ModulationMode>(prefs.getUChar("mod", static_cast<uint8_t>(settings.modulation)));
  if (settings.modulation != ModulationMode::LoRa && settings.modulation != ModulationMode::Fsk) {
    settings.modulation = ModulationMode::LoRa;
  }

  settings.rfFrequencyHz = prefs.getULong("freq", settings.rfFrequencyHz);
  settings.outputPower = prefs.getChar("pwr", settings.outputPower);

  settings.loraSpreadingFactor = prefs.getUChar("lsf", settings.loraSpreadingFactor);
  settings.loraBandwidthIndex = prefs.getUChar("lbw", settings.loraBandwidthIndex);
  settings.loraCodingRate = prefs.getUChar("lcr", settings.loraCodingRate);
  settings.loraPreambleLength = prefs.getUShort("lpre", settings.loraPreambleLength);
  settings.loraSymbolTimeout = prefs.getUShort("lsym", settings.loraSymbolTimeout);

  settings.fskBitRate = prefs.getULong("fbr", settings.fskBitRate);
  settings.fskFrequencyDeviation = prefs.getULong("fdev", settings.fskFrequencyDeviation);
  settings.fskRxBandwidth = prefs.getULong("fbw", settings.fskRxBandwidth);
  settings.fskPreambleLength = prefs.getUShort("fpre", settings.fskPreambleLength);
  settings.fskDataShaping = prefs.getUChar("fshape", settings.fskDataShaping);
  settings.fskEncoding = prefs.getUChar("fenc", settings.fskEncoding);
  settings.fixedLengthPayload = prefs.getBool("ffix", settings.fixedLengthPayload);
  settings.fixedPayloadLength = prefs.getUChar("ffixlen", settings.fixedPayloadLength);

  if (prefs.getBytesLength("sync") == sizeof(settings.syncWord)) {
    prefs.getBytes("sync", settings.syncWord, sizeof(settings.syncWord));
  }
  settings.syncWordLength = prefs.getUChar("synclen", settings.syncWordLength);
  if (settings.syncWordLength == 0 || settings.syncWordLength > sizeof(settings.syncWord)) {
    settings.syncWordLength = 2;
  }
  if (settings.modulation == ModulationMode::LoRa) {
    settings.syncWordLength = 2;
  } else if (settings.syncWordLength < 2) {
    settings.syncWordLength = 2;
  }

  settings.payloadCryptEnabled = prefs.getBool("crypt", settings.payloadCryptEnabled);
  if (prefs.getBytesLength("pkey") == sizeof(settings.payloadKey)) {
    prefs.getBytes("pkey", settings.payloadKey, sizeof(settings.payloadKey));
  }
  if (prefs.getBytesLength("civ") == sizeof(settings.cryptoIv)) {
    prefs.getBytes("civ", settings.cryptoIv, sizeof(settings.cryptoIv));
  }
  settings.baudRate = prefs.getULong("baud", settings.baudRate);
  if (settings.baudRate < 1200UL || settings.baudRate > 921600UL) {
    settings.baudRate = SERIAL_BAUD;
  }
  settings.lowPowerEnabled = prefs.getBool("lpm", settings.lowPowerEnabled);
  settings.cadEnabled = prefs.getBool("cad", settings.cadEnabled);

  sanitizeSettings(settings);
}

int stopReceiveMode() {
  receiveActive = false;
  packetReceived = false;
  setLoraActivityLed(false);
  return radio.standby();
}

int startReceiveMode() {
  packetReceived = false;
  setLoraActivityLed(false);
  const int state = radio.startReceive();
  receiveActive = (state == RADIOLIB_ERR_NONE);
  return state;
}

uint32_t currentReceiveValue() {
  switch (receivePolicy) {
    case RakReceivePolicy::Disabled:
      return 0;
    case RakReceivePolicy::ContinuousTx:
      return 65533UL;
    case RakReceivePolicy::Continuous:
      return 65534UL;
    case RakReceivePolicy::OneShot:
      return 65535UL;
    case RakReceivePolicy::Timed:
      return receiveWindowMs;
  }
  return 0;
}

int applyReceiveState() {
  switch (receivePolicy) {
    case RakReceivePolicy::Disabled:
      receiveWindowMs = 0;
      return stopReceiveMode();
    case RakReceivePolicy::ContinuousTx:
      receiveWindowMs = 65533UL;
      return startReceiveMode();
    case RakReceivePolicy::Continuous:
      receiveWindowMs = 65534UL;
      return startReceiveMode();
    case RakReceivePolicy::OneShot:
      receiveWindowMs = 65535UL;
      return startReceiveMode();
    case RakReceivePolicy::Timed:
      receiveDeadline = millis() + receiveWindowMs;
      return startReceiveMode();
  }
  return RADIOLIB_ERR_INVALID_BANDWIDTH;
}

int initializeRadio(const ModemSettings& candidate) {
  int state = RADIOLIB_ERR_NONE;

  lastFskInitDiagnostics = FskInitDiagnostics();
  lastFskInitDiagnostics.bitRate = candidate.fskBitRate;
  lastFskInitDiagnostics.frequencyDeviation = candidate.fskFrequencyDeviation;
  lastFskInitDiagnostics.rxBandwidth = candidate.fskRxBandwidth;
  lastFskInitDiagnostics.selectedRxBandwidth = 0;
  lastFskInitDiagnostics.rfFrequencyHz = candidate.rfFrequencyHz;
  lastFskInitDiagnostics.outputPower = candidate.outputPower;
  lastFskInitDiagnostics.syncWordLength = candidate.syncWordLength;

  if (candidate.modulation == ModulationMode::LoRa) {
    state = radio.begin(
        static_cast<float>(candidate.rfFrequencyHz) / 1000000.0f,
        bandwidthIndexToKHz(candidate.loraBandwidthIndex),
        candidate.loraSpreadingFactor,
        candidate.loraCodingRate + 4,
        candidate.syncWord[1],
        candidate.outputPower,
        candidate.loraPreambleLength,
        HELTEC_TCXO_VOLTAGE);
    if (state != RADIOLIB_ERR_NONE) {
      return state;
    }
    // Heltec V3/V4 routes the RF switch through DIO2.
    radio.setDio2AsRfSwitch(true);

  } else {
    lastFskInitDiagnostics.stage = FskInitStage::ValidateProfile;
    if (!isValidSx1262FskProfile(candidate)) {
      lastFskInitDiagnostics.state = RADIOLIB_ERR_INVALID_BANDWIDTH;
      return RADIOLIB_ERR_INVALID_BANDWIDTH;
    }

    // Use the SX1262 beginFSK overload that accepts (freq, br, freqDev, rxBw,
    // power, preamble, tcxoVoltage, useRegulatorLDO). The ConfigFSK_t struct
    // has no TCXO field, and on Heltec V3/V4 (TCXO on DIO3 @ 1.8 V) failing
    // to configure TCXO during init causes BUSY to hang and beginFSK to
    // return RADIOLIB_ERR_SPI_CMD_TIMEOUT (-707). Previously the call was
    // mis-ordered (bit rate passed as frequency); fix that here.
    lastFskInitDiagnostics.stage = FskInitStage::BeginFsk;
    const float freqMhz = static_cast<float>(candidate.rfFrequencyHz) / 1000000.0f;
    const float brKbps = static_cast<float>(candidate.fskBitRate) / 1000.0f;
    const float fdevKhz = static_cast<float>(candidate.fskFrequencyDeviation) / 1000.0f;
    const float rxBwKhz = static_cast<float>(candidate.fskRxBandwidth) / 1000.0f;
    state = radio.beginFSK(
        freqMhz,
        brKbps,
        fdevKhz,
        rxBwKhz,
        candidate.outputPower,
        candidate.fskPreambleLength,
        HELTEC_TCXO_VOLTAGE,
        false);
    lastFskInitDiagnostics.state = state;
    lastFskInitDiagnostics.selectedRxBandwidth = candidate.fskRxBandwidth;
    if (state != RADIOLIB_ERR_NONE) {
      return state;
    }

    // Heltec V3/V4 routes the RF switch through DIO2.
    radio.setDio2AsRfSwitch(true);

    // Configure FSK modulation parameters via runtime setters (per example).
    lastFskInitDiagnostics.stage = FskInitStage::SetFrequency;
    state = radio.setBitRate(static_cast<float>(candidate.fskBitRate) / 1000.0f);
    lastFskInitDiagnostics.state = state;
    if (state != RADIOLIB_ERR_NONE) {
      return state;
    }

    state = radio.setFrequencyDeviation(static_cast<float>(candidate.fskFrequencyDeviation) / 1000.0f);
    lastFskInitDiagnostics.state = state;
    if (state != RADIOLIB_ERR_NONE) {
      return state;
    }

    state = radio.setRxBandwidth(static_cast<float>(candidate.fskRxBandwidth) / 1000.0f);
    lastFskInitDiagnostics.state = state;
    if (state != RADIOLIB_ERR_NONE) {
      return state;
    }

    state = radio.setOutputPower(candidate.outputPower);
    lastFskInitDiagnostics.state = state;
    if (state != RADIOLIB_ERR_NONE) {
      return state;
    }

    state = radio.setPreambleLength(candidate.fskPreambleLength);
    lastFskInitDiagnostics.state = state;
    if (state != RADIOLIB_ERR_NONE) {
      return state;
    }

    // Some RadioLib/SX1262 combinations report unsupported for these optional FSK features.
    // Keep mode switch resilient and treat them as best-effort.
    lastFskInitDiagnostics.stage = FskInitStage::SetDataShaping;
    lastFskInitDiagnostics.shapingState = radio.setDataShaping(candidate.fskDataShaping);

    lastFskInitDiagnostics.stage = FskInitStage::SetEncoding;
    lastFskInitDiagnostics.encodingState = radio.setEncoding(candidate.fskEncoding);

    uint8_t syncWord[8];
    memcpy(syncWord, candidate.syncWord, candidate.syncWordLength);
    lastFskInitDiagnostics.stage = FskInitStage::SetSyncWord;
    lastFskInitDiagnostics.syncWordState = radio.setSyncWord(syncWord, candidate.syncWordLength);

    lastFskInitDiagnostics.stage = FskInitStage::SetPacketMode;
    if (candidate.fixedLengthPayload) {
      lastFskInitDiagnostics.packetModeState = radio.fixedPacketLengthMode(candidate.fixedPayloadLength);
    } else {
      lastFskInitDiagnostics.packetModeState = radio.variablePacketLengthMode(MAX_FRAME_SIZE);
    }
  }

  lastFskInitDiagnostics.stage = FskInitStage::SetPacketReceivedAction;
  radio.setPacketReceivedAction(setPacketReceivedFlag);
  lastFskInitDiagnostics.stage = FskInitStage::ApplyReceiveState;
  lastFskInitDiagnostics.applyReceiveState = applyReceiveState();
  lastFskInitDiagnostics.state = lastFskInitDiagnostics.applyReceiveState;
  return lastFskInitDiagnostics.applyReceiveState;
}

bool applyAndSaveSettings() {
  const ModemSettings previousSettings = settings;
  int state = initializeRadio(settings);
  lastRadioState = state;
  if (state != RADIOLIB_ERR_NONE && settings.modulation == ModulationMode::Fsk) {
    // Retry once with a known-good FSK profile to recover from stale settings.
    ModemSettings recovered = settings;
    applyKnownGoodFskProfile(recovered);
    sanitizeSettings(recovered);
    state = initializeRadio(recovered);
    lastRadioState = state;
    if (state == RADIOLIB_ERR_NONE) {
      settings = recovered;
      saveSettings();
      return true;
    }
  }

  if (state != RADIOLIB_ERR_NONE) {
    settings = previousSettings;
    lastRadioState = initializeRadio(settings);
    return false;
  }

  saveSettings();
  return true;
}

bool restoreDefaults() {
  const ModemSettings previousSettings = settings;
  settings = ModemSettings();
  receivePolicy = RakReceivePolicy::Disabled;
  receiveWindowMs = 0;
  const int state = initializeRadio(settings);
  if (state != RADIOLIB_ERR_NONE) {
    settings = previousSettings;
    initializeRadio(settings);
    return false;
  }
  prefs.clear();
  saveSettings();
  return true;
}

void enterLightSleep(uint32_t durationMs) {
  const RakReceivePolicy previousPolicy = receivePolicy;
  const uint32_t previousWindow = receiveWindowMs;
  if (receiveActive) {
    stopReceiveMode();
  }

  Serial.flush();
  uart_set_wakeup_threshold(UART_NUM_0, 3);
  esp_sleep_enable_uart_wakeup(UART_NUM_0);
  if (durationMs > 0) {
    esp_sleep_enable_timer_wakeup(static_cast<uint64_t>(durationMs) * 1000ULL);
  }
  esp_light_sleep_start();
  if (durationMs > 0) {
    esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_TIMER);
  }
  esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_UART);

  receivePolicy = previousPolicy;
  receiveWindowMs = previousWindow;
  if (previousPolicy != RakReceivePolicy::Disabled) {
    applyReceiveState();
  }
}

bool encryptPayload(const uint8_t* input, size_t inputLength, uint8_t* output, size_t& outputLength) {
  if (!settings.payloadCryptEnabled) {
    if (inputLength > MAX_FRAME_SIZE) {
      return false;
    }
    memcpy(output, input, inputLength);
    outputLength = inputLength;
    return true;
  }

  const size_t paddedLength = ((inputLength / 16) + 1) * 16;
  if (paddedLength > MAX_FRAME_SIZE) {
    return false;
  }

  uint8_t plaintext[MAX_FRAME_SIZE];
  memset(plaintext, 0, sizeof(plaintext));
  memcpy(plaintext, input, inputLength);
  const uint8_t pad = static_cast<uint8_t>(paddedLength - inputLength);
  for (size_t index = inputLength; index < paddedLength; ++index) {
    plaintext[index] = pad;
  }

  uint8_t iv[16];
  memcpy(iv, settings.cryptoIv, sizeof(iv));

  mbedtls_aes_context aes;
  mbedtls_aes_init(&aes);
  if (mbedtls_aes_setkey_enc(&aes, settings.payloadKey, 128) != 0 ||
      mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, paddedLength, iv, plaintext, output) != 0) {
    mbedtls_aes_free(&aes);
    return false;
  }
  mbedtls_aes_free(&aes);

  outputLength = paddedLength;
  return true;
}

bool decryptPayload(const uint8_t* input, size_t inputLength, uint8_t* output, size_t& outputLength) {
  if (!settings.payloadCryptEnabled) {
    if (inputLength > MAX_FRAME_SIZE) {
      return false;
    }
    memcpy(output, input, inputLength);
    outputLength = inputLength;
    return true;
  }

  if (inputLength == 0 || (inputLength % 16) != 0) {
    return false;
  }

  uint8_t ciphertext[MAX_FRAME_SIZE];
  memcpy(ciphertext, input, inputLength);

  uint8_t iv[16];
  memcpy(iv, settings.cryptoIv, sizeof(iv));

  mbedtls_aes_context aes;
  mbedtls_aes_init(&aes);
  if (mbedtls_aes_setkey_dec(&aes, settings.payloadKey, 128) != 0 ||
      mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT, inputLength, iv, ciphertext, output) != 0) {
    mbedtls_aes_free(&aes);
    return false;
  }
  mbedtls_aes_free(&aes);

  const uint8_t pad = output[inputLength - 1];
  if (pad == 0 || pad > 16 || pad > inputLength) {
    return false;
  }
  for (size_t index = inputLength - pad; index < inputLength; ++index) {
    if (output[index] != pad) {
      return false;
    }
  }

  outputLength = inputLength - pad;
  return true;
}

void printHelp() {
  Serial.println(F("+++++++++++++++"));
  Serial.println(F("AT command list"));
  Serial.println(F("+++++++++++++++"));
  Serial.println(F("AT?                   AT commands"));
  Serial.println(F("ATR                   Restore default"));
  Serial.println(F("ATZ                   MCU reset"));
  Serial.println(F("AT+BUILDTIME          Firmware build time"));
  Serial.println(F("AT+BOOTVER            ESP32 boot ROM info"));
  Serial.println(F("AT+HWMODEL            Hardware model"));
  Serial.println(F("AT+NWM                P2P work mode"));
  Serial.println(F("AT+CLIVER             AT subset revision"));
  Serial.println(F("AT+APIVER             Same as CLIVER"));
  Serial.println(F("AT+HWID               Unique hardware ID"));
  Serial.println(F("AT+SN                 Device serial number"));
  Serial.println(F("AT+SYSV               System/battery voltage"));
  Serial.println(F("AT+BAT                Alias of AT+SYSV"));
  Serial.println(F("AT+BLEMAC             BLE MAC address"));
  Serial.println(F("AT+SLEEP              Timed light sleep"));
  Serial.println(F("AT+LPM                Auto light sleep"));
  Serial.println(F("AT+BAUD               UART baud rate"));
  Serial.println(F("AT+BOOT               Reboot to bootloader"));
  Serial.println(F("AT+CAD                Channel activity detection"));
  Serial.println(F("AT+RFFREQUENCY        Get/set RF frequency"));
  Serial.println(F("AT+PFREQ              Alias of AT+RFFREQUENCY"));
  Serial.println(F("AT+SPREADINGFACTOR    Get/set spreading factor"));
  Serial.println(F("AT+PSF                Alias of AT+SPREADINGFACTOR"));
  Serial.println(F("AT+BANDWIDTH          Get/set bandwidth"));
  Serial.println(F("AT+PBW                Alias of AT+BANDWIDTH"));
  Serial.println(F("AT+CODINGRATE         Get/set coding rate"));
  Serial.println(F("AT+PCR                Alias of AT+CODINGRATE"));
  Serial.println(F("AT+PREAMBLELENGTH     Get/set preamble length"));
  Serial.println(F("AT+PPL                Alias of AT+PREAMBLELENGTH"));
  Serial.println(F("AT+TXOUTPUTPOWER      Get/set TX power"));
  Serial.println(F("AT+PTP                Alias of AT+TXOUTPUTPOWER"));
  Serial.println(F("AT+POWER              Alias of AT+TXOUTPUTPOWER"));
  Serial.println(F("AT+SYNCWORD           Set sync word"));
  Serial.println(F("AT+SYMBOLTIMEOUT      Set symbol timeout"));
  Serial.println(F("AT+PCRYPT             Enable payload crypto"));
  Serial.println(F("AT+PKEY               Set payload crypto key"));
  Serial.println(F("AT+CRYPIV             Set payload crypto IV"));
  Serial.println(F("AT+PBR                Set FSK bitrate"));
  Serial.println(F("AT+BITRATE            Alias of AT+PBR"));
  Serial.println(F("AT+PFDEV              Set FSK frequency deviation"));
  Serial.println(F("AT+FDEV               Alias of AT+PFDEV"));
  Serial.println(F("AT+ENCODING           Vendor extension: FSK encoding"));
  Serial.println(F("AT+SHAPING            Vendor extension: FSK shaping"));
  Serial.println(F("AT+FIXLENGTHPAYLOAD   Set fixed payload mode"));
  Serial.println(F("AT+P2P                Set P2P configuration"));
  Serial.println(F("AT+PSEND              P2P send hex data"));
  Serial.println(F("AT+SEND               Alias of AT+PSEND"));
  Serial.println(F("AT+PRECV              P2P receive mode"));
  Serial.println(F("AT+VER                Get SW version"));
  Serial.println(F("+++++++++++++++"));
  Serial.println(F("OK"));
}

bool transmitBytes(uint8_t* data, size_t length) {
  if (txBlocked()) {
    replyError(RakError::Busy);
    return false;
  }

  int state = radio.standby();
  receiveActive = false;
  if (state != RADIOLIB_ERR_NONE) {
    replyError(RakError::Generic);
    return false;
  }

  if (settings.cadEnabled && settings.modulation == ModulationMode::LoRa) {
    while (true) {
      state = radio.scanChannel();
      if (state == RADIOLIB_CHANNEL_FREE) {
        break;
      }
      if (state == RADIOLIB_LORA_DETECTED) {
        delay(10);
        continue;
      }
      applyReceiveState();
      replyError(RakError::Generic);
      return false;
    }
  }

  setLoraActivityLed(true);
  state = radio.transmit(data, length);
  setLoraActivityLed(false);
  if (state != RADIOLIB_ERR_NONE) {
    applyReceiveState();
    replyError((state == RADIOLIB_ERR_PACKET_TOO_LONG) ? RakError::TooLong : RakError::Generic);
    return false;
  }

  state = applyReceiveState();
  if (state != RADIOLIB_ERR_NONE) {
    replyError(RakError::Generic);
    return false;
  }

  replyOk();
  Serial.println(F("+EVT:TXP2P DONE"));
  return true;
}

void finishReceiveAfterPacket() {
  switch (receivePolicy) {
    case RakReceivePolicy::Disabled:
      stopReceiveMode();
      return;
    case RakReceivePolicy::ContinuousTx:
      startReceiveMode();
      return;
    case RakReceivePolicy::Continuous:
      startReceiveMode();
      return;
    case RakReceivePolicy::OneShot:
      receivePolicy = RakReceivePolicy::Disabled;
      receiveWindowMs = 0;
      stopReceiveMode();
      return;
    case RakReceivePolicy::Timed:
      if (static_cast<long>(millis() - receiveDeadline) >= 0) {
        receivePolicy = RakReceivePolicy::Disabled;
        receiveWindowMs = 0;
        stopReceiveMode();
      } else {
        startReceiveMode();
      }
      return;
  }
}

void handleReceive() {
  if (rxDiagVerbose && rxIrqCount != rxIrqCountReported) {
    rxIrqCountReported = rxIrqCount;
    Serial.print(F("+RX:IRQ irq="));
    Serial.print(static_cast<unsigned long>(rxIrqCount));
    Serial.print(F(" active="));
    Serial.println(receiveActive ? 1 : 0);
  }

  if (!packetReceived || !receiveActive) {
    return;
  }

  packetReceived = false;
  setLoraActivityLed(true);

  uint8_t frame[MAX_FRAME_SIZE];
  const size_t frameLength = radio.getPacketLength();
  if (frameLength == 0 || frameLength > sizeof(frame)) {
    Serial.print(F("+EVT:RXP2P RECEIVE ERROR LEN="));
    Serial.println(static_cast<unsigned long>(frameLength));
    finishReceiveAfterPacket();
    return;
  }

  if (radio.readData(frame, frameLength) != RADIOLIB_ERR_NONE) {
    Serial.println(F("+EVT:RXP2P RECEIVE ERROR READ"));
    finishReceiveAfterPacket();
    return;
  }

  uint8_t payload[MAX_FRAME_SIZE];
  size_t payloadLength = 0;
  if (!decryptPayload(frame, frameLength, payload, payloadLength)) {
    Serial.print(F("+EVT:RXP2P RECEIVE ERROR DECRYPT len="));
    Serial.print(static_cast<unsigned long>(frameLength));
    Serial.print(F(" raw="));
    Serial.println(bytesToHex(frame, frameLength));
    finishReceiveAfterPacket();
    return;
  }

  Serial.print(F("+EVT:RXP2P:"));
  Serial.print(static_cast<int>(radio.getRSSI()));
  Serial.print(':');
  if (settings.modulation == ModulationMode::LoRa) {
    Serial.print(radio.getSNR(), 1);
  } else {
    Serial.print(0.0f, 1);
  }
  Serial.print(':');
  Serial.println(bytesToHex(payload, payloadLength));

  finishReceiveAfterPacket();
}

void handleReceiveTimeout() {
  if (receivePolicy != RakReceivePolicy::Timed || !receiveActive) {
    return;
  }

  if (static_cast<long>(millis() - receiveDeadline) >= 0) {
    receivePolicy = RakReceivePolicy::Disabled;
    receiveWindowMs = 0;
    stopReceiveMode();
    Serial.println(F("+EVT:RXP2P RECEIVE TIMEOUT"));
  }
}

void handleBuildTimeCommand(const String& command) {
  if (command == F("AT+BUILDTIME?")) {
    printCommandHelp("AT+BUILDTIME", F("get firmware build time"));
    return;
  }
  if (command == F("AT+BUILDTIME=?")) {
    printCommandValue("AT+BUILDTIME", getBuildTimeString());
    return;
  }
  replyError(RakError::Generic);
}

void handleBootVersionCommand(const String& command) {
  if (command == F("AT+BOOTVER?")) {
    printCommandHelp("AT+BOOTVER", F("best-effort ROM boot information"));
    return;
  }
  if (command == F("AT+BOOTVER=?")) {
    printCommandValue("AT+BOOTVER", getBootVersionString());
    return;
  }
  replyError(RakError::Generic);
}

void handleHardwareModelCommand(const String& command) {
  if (command == F("AT+HWMODEL?")) {
    printCommandHelp("AT+HWMODEL", F("get hardware model"));
    return;
  }
  if (command == F("AT+HWMODEL=?")) {
    printCommandValue("AT+HWMODEL", HW_MODEL);
    return;
  }
  replyError(RakError::Generic);
}

void handleCliVersionCommand(const String& command, const __FlashStringHelper* commandPrefix) {
  const String prefix(commandPrefix);
  if (command == prefix + '?') {
    printCommandHelp(prefix.c_str(), F("get AT subset revision"));
    return;
  }
  if (command == prefix + "=?") {
    printCommandValue(prefix.c_str(), prefix.equalsIgnoreCase("AT+APIVER") ? API_VERSION : CLI_VERSION);
    return;
  }
  replyError(RakError::Generic);
}

void handleHardwareIdCommand(const String& command) {
  if (command == F("AT+HWID?")) {
    printCommandHelp("AT+HWID", F("get unique hardware id from BLE MAC"));
    return;
  }
  if (command == F("AT+HWID=?")) {
    printCommandValue("AT+HWID", getHardwareId());
    return;
  }
  replyError(RakError::Generic);
}

void handleSerialNumberCommand(const String& command) {
  if (command == F("AT+SN?")) {
    printCommandHelp("AT+SN", F("get device serial number"));
    return;
  }
  if (command == F("AT+SN=?")) {
    printCommandValue("AT+SN", getSerialNumber());
    return;
  }
  replyError(RakError::Generic);
}

void handleSystemVoltageCommand(const String& command, const __FlashStringHelper* commandPrefix) {
  const String prefix(commandPrefix);
  if (command == prefix + '?') {
    printCommandHelp(prefix.c_str(), F("get system voltage in volts"));
    return;
  }
  if (command == prefix + "=?") {
    printCommandValue(prefix.c_str(), String(readSystemVoltage(), 3));
    return;
  }
  replyError(RakError::Generic);
}

void handleBleMacCommand(const String& command) {
  if (command == F("AT+BLEMAC?")) {
    printCommandHelp("AT+BLEMAC", F("get BLE MAC address"));
    return;
  }
  if (command == F("AT+BLEMAC=?")) {
    printCommandValue("AT+BLEMAC", getBleMacString());
    return;
  }
  replyError(RakError::Generic);
}

void handleSleepCommand(const String& command) {
  if (command == F("AT+SLEEP?")) {
    printCommandHelp("AT+SLEEP", F("enter timed light sleep in milliseconds"));
    return;
  }
  if (!command.startsWith(F("AT+SLEEP="))) {
    replyError(RakError::Generic);
    return;
  }

  unsigned long durationMs = 0;
  if (!parseUnsignedLongFlexible(command.substring(9), durationMs) || durationMs == 0) {
    replyError(RakError::Parameter);
    return;
  }

  replyOk();
  enterLightSleep(durationMs);
}

void handleLowPowerModeCommand(const String& command) {
  if (command == F("AT+LPM?")) {
    printCommandHelp("AT+LPM", F("auto light sleep after commands finish (0 = OFF, 1 = ON)"));
    return;
  }
  if (command == F("AT+LPM=?")) {
    printCommandValue("AT+LPM", settings.lowPowerEnabled ? 1 : 0);
    return;
  }
  if (!command.startsWith(F("AT+LPM="))) {
    replyError(RakError::Generic);
    return;
  }

  bool enabled = false;
  if (!parsePayloadCryptValue(command.substring(7), enabled)) {
    replyError(RakError::Parameter);
    return;
  }

  settings.lowPowerEnabled = enabled;
  saveSettings();
  replyOk();
}

void restartSerialAtBaud(uint32_t baud, bool waitForPort = false) {
  Serial.flush();
  Serial.end();
  delay(20);
  Serial.setRxBufferSize(2048);
  Serial.begin(baud);
  if (waitForPort) {
    const unsigned long waitStart = millis();
    while (!Serial && (millis() - waitStart) < 3000UL) {
      delay(10);
    }
  }
}

void handleBaudCommand(const String& command) {
  if (command == F("AT+BAUD?")) {
    printCommandHelp("AT+BAUD", F("get or set UART baud rate"));
    return;
  }
  if (command == F("AT+BAUD=?")) {
    printCommandValue("AT+BAUD", static_cast<long>(settings.baudRate));
    return;
  }
  if (!command.startsWith(F("AT+BAUD="))) {
    replyError(RakError::Generic);
    return;
  }

  unsigned long baud = 0;
  if (!parseUnsignedLongFlexible(command.substring(8), baud) || baud < 1200UL || baud > 921600UL) {
    replyError(RakError::Parameter);
    return;
  }

  settings.baudRate = baud;
  saveSettings();
  replyOk();
  restartSerialAtBaud(settings.baudRate);
}

void handleBootCommand(const String& command) {
  if (command == F("AT+BOOT?")) {
    printCommandHelp("AT+BOOT", F("reboot into ROM USB/UART bootloader when supported"));
    return;
  }
  if (command != F("AT+BOOT")) {
    replyError(RakError::Generic);
    return;
  }

#if HAS_USB_BOOTLOADER_RESET
  replyOk();
  Serial.flush();
  usb_uart_bridge_reset_to_bootloader();
#else
  replyError(RakError::Generic);
#endif
}

void handleCadCommand(const String& command) {
  if (command == F("AT+CAD?")) {
    printCommandHelp("AT+CAD", F("get or set channel activity detection (1 = ON, 0 = OFF)"));
    return;
  }
  if (command == F("AT+CAD=?")) {
    printCommandValue("AT+CAD", settings.cadEnabled ? 1 : 0);
    return;
  }
  if (!command.startsWith(F("AT+CAD="))) {
    replyError(RakError::Generic);
    return;
  }

  bool enabled = false;
  if (!parsePayloadCryptValue(command.substring(7), enabled)) {
    replyError(RakError::Parameter);
    return;
  }

  settings.cadEnabled = enabled;
  saveSettings();
  replyOk();
}

void handleNwmCommand(const String& command) {
  if (command == F("AT+NWM?")) {
    printCommandHelp("AT+NWM", F("get or set the network working mode (0 = P2P_LORA, 1 = LoRaWAN, 2 = P2P_FSK)"));
    return;
  }
  if (command == F("AT+NWM=?")) {
    printCommandValue("AT+NWM", (settings.modulation == ModulationMode::LoRa) ? 0 : 2);
    return;
  }
  if (!command.startsWith(F("AT+NWM="))) {
    replyError(RakError::Generic);
    return;
  }

  long value = 0;
  if (!parseLongStrict(command.substring(7), value)) {
    replyError(RakError::Parameter);
    return;
  }
  if (value != 0 && value != 2) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  const ModulationMode requestedMode = (value == 0) ? ModulationMode::LoRa : ModulationMode::Fsk;
  if (requestedMode == settings.modulation) {
    // No change: keep current runtime state, just acknowledge.
    replyOk();
    return;
  }

  settings.modulation = requestedMode;
  if (settings.modulation == ModulationMode::Fsk) {
    applyKnownGoodFskProfile(settings);
  } else {
    settings.syncWordLength = 2;
  }
  sanitizeSettings(settings);

  // RUI3 behavior: persist the new working mode and reboot so the radio is
  // initialized from a clean power-on state for the selected modulation.
  // Switching modulations at runtime on the SX1262 is unreliable because the
  // chip retains command/IRQ/TCXO context from the previously running mode.
  saveSettings();
  replyOk();
  Serial.flush();
  delay(50);
  ESP.restart();
}

void handleModulationCommand(const String& command) {
  if (command == F("AT+MOD?")) {
    printCommandHelp("AT+MOD", F("vendor alias for P2P modulation (0 = FSK, 1 = LORA)"));
    return;
  }
  if (command == F("AT+MOD=?")) {
    printCommandValue("AT+MOD", (settings.modulation == ModulationMode::LoRa) ? 1 : 0);
    return;
  }
  if (!command.startsWith(F("AT+MOD="))) {
    replyError(RakError::Generic);
    return;
  }

  ModulationMode mode;
  if (!parseModulationValue(command.substring(7), mode)) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  if (mode == settings.modulation) {
    // No change: keep current runtime state, just acknowledge.
    replyOk();
    return;
  }

  settings.modulation = mode;
  if (settings.modulation == ModulationMode::Fsk) {
    applyKnownGoodFskProfile(settings);
  } else {
    settings.syncWordLength = 2;
  }
  sanitizeSettings(settings);

  // RUI3 behavior: persist the new modulation and reboot for a clean radio init.
  saveSettings();
  replyOk();
  Serial.flush();
  delay(50);
  ESP.restart();
}

void handleRfFrequencyCommand(const String& command, const __FlashStringHelper* commandPrefix) {
  const String prefix(commandPrefix);
  const String setPrefix = prefix + '=';

  if (command == prefix + '?') {
    printCommandHelp(prefix.c_str(), F("get or set P2P Frequency"));
    return;
  }
  if (command == prefix + "=?") {
    printCommandValue(prefix.c_str(), static_cast<long>(settings.rfFrequencyHz));
    return;
  }
  if (!command.startsWith(setPrefix)) {
    replyError(RakError::Generic);
    return;
  }

  long value = 0;
  if (!parseLongStrict(command.substring(setPrefix.length()), value) || !isValidFrequency(static_cast<uint32_t>(value))) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  settings.rfFrequencyHz = static_cast<uint32_t>(value);
  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handleSpreadingFactorCommand(const String& command, const __FlashStringHelper* commandPrefix) {
  if (settings.modulation != ModulationMode::LoRa) {
    replyError(RakError::Parameter);
    return;
  }

  const String prefix(commandPrefix);
  const String setPrefix = prefix + '=';
  if (command == prefix + '?') {
    printCommandHelp(prefix.c_str(), F("get or set P2P Spreading Factor (5-12)"));
    return;
  }
  if (command == prefix + "=?") {
    printCommandValue(prefix.c_str(), settings.loraSpreadingFactor);
    return;
  }
  if (!command.startsWith(setPrefix)) {
    replyError(RakError::Generic);
    return;
  }

  long value = 0;
  if (!parseLongStrict(command.substring(setPrefix.length()), value) || !isValidLoRaSpreadingFactor(value)) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  settings.loraSpreadingFactor = static_cast<uint8_t>(value);
  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handleBandwidthCommand(const String& command, const __FlashStringHelper* commandPrefix) {
  const String prefix(commandPrefix);
  const String setPrefix = prefix + '=';

  if (command == prefix + '?') {
    printCommandHelp(prefix.c_str(), F("get or set P2P Bandwidth"));
    return;
  }
  if (command == prefix + "=?") {
    if (settings.modulation == ModulationMode::LoRa) {
      printCommandValue(prefix.c_str(), loraBandwidthCode(settings.loraBandwidthIndex));
    } else {
      printCommandValue(prefix.c_str(), static_cast<long>(settings.fskRxBandwidth));
    }
    return;
  }
  if (!command.startsWith(setPrefix)) {
    replyError(RakError::Generic);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  if (settings.modulation == ModulationMode::LoRa) {
    uint8_t bandwidthIndex = 0;
    if (!parseLoRaBandwidthValue(command.substring(setPrefix.length()), bandwidthIndex)) {
      replyError(RakError::Parameter);
      return;
    }
    settings.loraBandwidthIndex = bandwidthIndex;
  } else {
    unsigned long value = 0;
    if (!parseUnsignedLongFlexible(command.substring(setPrefix.length()), value) || !isValidFskBandwidth(value)) {
      replyError(RakError::Parameter);
      return;
    }
    settings.fskRxBandwidth = value;
  }

  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handleCodingRateCommand(const String& command, const __FlashStringHelper* commandPrefix) {
  if (settings.modulation != ModulationMode::LoRa) {
    replyError(RakError::Parameter);
    return;
  }

  const String prefix(commandPrefix);
  const String setPrefix = prefix + '=';
  if (command == prefix + '?') {
    printCommandHelp(prefix.c_str(), F("get or set P2P Code Rate(0=4/5, 1=4/6, 2=4/7, 3=4/8)"));
    return;
  }
  if (command == prefix + "=?") {
    printCommandValue(prefix.c_str(), static_cast<long>(settings.loraCodingRate - 1));
    return;
  }
  if (!command.startsWith(setPrefix)) {
    replyError(RakError::Generic);
    return;
  }

  long value = 0;
  if (!parseLongStrict(command.substring(setPrefix.length()), value) || !isValidLoRaCodingRate(value)) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  settings.loraCodingRate = static_cast<uint8_t>(value + 1);
  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handlePreambleLengthCommand(const String& command, const __FlashStringHelper* commandPrefix) {
  const String prefix(commandPrefix);
  const String setPrefix = prefix + '=';

  if (command == prefix + '?') {
    printCommandHelp(prefix.c_str(), F("get or set P2P Preamble Length (5-65535)"));
    return;
  }
  if (command == prefix + "=?") {
    if (settings.modulation == ModulationMode::LoRa) {
      printCommandValue(prefix.c_str(), static_cast<long>(settings.loraPreambleLength));
    } else {
      printCommandValue(prefix.c_str(), static_cast<long>(settings.fskPreambleLength));
    }
    return;
  }
  if (!command.startsWith(setPrefix)) {
    replyError(RakError::Generic);
    return;
  }

  long value = 0;
  if (!parseLongStrict(command.substring(setPrefix.length()), value)) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  if (settings.modulation == ModulationMode::LoRa) {
    if (!isValidLoRaPreamble(value)) {
      replyError(RakError::Parameter);
      return;
    }
    settings.loraPreambleLength = static_cast<uint16_t>(value);
  } else {
    if (!isValidFskPreamble(value)) {
      replyError(RakError::Parameter);
      return;
    }
    settings.fskPreambleLength = static_cast<uint16_t>(value);
  }

  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handlePowerCommand(const String& command, const __FlashStringHelper* commandPrefix) {
  const String prefix(commandPrefix);
  const String setPrefix = prefix + '=';
  if (command == prefix + '?') {
    printCommandHelp(prefix.c_str(), F("get or set P2P Tx Power(5-22)"));
    return;
  }
  if (command == prefix + "=?") {
    printCommandValue(prefix.c_str(), settings.outputPower);
    return;
  }
  if (!command.startsWith(setPrefix)) {
    replyError(RakError::Generic);
    return;
  }

  long value = 0;
  if (!parseLongStrict(command.substring(setPrefix.length()), value) || !isValidOutputPower(value)) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  settings.outputPower = static_cast<int8_t>(value);
  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handleSyncWordCommand(const String& command) {
  if (command == F("AT+SYNCWORD?")) {
    printCommandHelp("AT+SYNCWORD", F("get or set P2P syncword (0x0000 - 0xffff)"));
    return;
  }
  if (command == F("AT+SYNCWORD=?")) {
    printCommandValue("AT+SYNCWORD", bytesToHex(settings.syncWord, 2));
    return;
  }
  if (!command.startsWith(F("AT+SYNCWORD="))) {
    replyError(RakError::Generic);
    return;
  }

  uint8_t syncWord[8];
  uint8_t syncLength = 0;
  const String syncToken = command.substring(12);
  if (!parseSyncWordValue(syncToken, syncWord, syncLength, settings.modulation)) {
    replyError((settings.modulation == ModulationMode::Fsk && syncToken.length() > 16) ? RakError::TooLong : RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  memcpy(settings.syncWord, syncWord, sizeof(syncWord));
  settings.syncWordLength = syncLength;
  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handleSymbolTimeoutCommand(const String& command) {
  if (settings.modulation != ModulationMode::LoRa) {
    replyError(RakError::Parameter);
    return;
  }

  if (command == F("AT+SYMBOLTIMEOUT?")) {
    printCommandHelp("AT+SYMBOLTIMEOUT", F("get or set P2P symbolTimeout (0-248)"));
    return;
  }
  if (command == F("AT+SYMBOLTIMEOUT=?")) {
    printCommandValue("AT+SYMBOLTIMEOUT", static_cast<long>(settings.loraSymbolTimeout));
    return;
  }
  if (!command.startsWith(F("AT+SYMBOLTIMEOUT="))) {
    replyError(RakError::Generic);
    return;
  }

  long value = 0;
  if (!parseLongStrict(command.substring(17), value) || !isValidLoRaSymbolTimeout(value)) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  settings.loraSymbolTimeout = static_cast<uint16_t>(value);
  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handlePayloadCryptCommand(const String& command, const __FlashStringHelper* commandPrefix) {
  const String prefix(commandPrefix);
  if (command == prefix + '?') {
    printCommandHelp(prefix.c_str(), F("get or set the encryption status of P2P mode"));
    return;
  }
  if (command == prefix + "=?") {
    printCommandValue(prefix.c_str(), settings.payloadCryptEnabled ? 1 : 0);
    return;
  }

  if (!command.startsWith(prefix)) {
    replyError(RakError::Generic);
    return;
  }

  int cursor = prefix.length();
  while (cursor < static_cast<int>(command.length()) && command[cursor] == ' ') {
    ++cursor;
  }
  if (cursor >= static_cast<int>(command.length()) || command[cursor] != '=') {
    replyError(RakError::Generic);
    return;
  }

  String valueToken = command.substring(cursor + 1);
  valueToken.trim();

  bool enabled = false;
  if (!parsePayloadCryptValue(valueToken, enabled)) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  settings.payloadCryptEnabled = enabled;
  saveSettings();
  replyOk();
}

void handlePayloadKeyCommand(const String& command, const __FlashStringHelper* commandPrefix) {
  const String prefix(commandPrefix);
  const String setPrefix = prefix + '=';
  if (command == prefix + '?') {
    printCommandHelp(prefix.c_str(), F("get or set the encryption key of P2P mode (8 bytes in hex)"));
    return;
  }
  if (command == prefix + "=?") {
    printCommandValue(prefix.c_str(), bytesToHex(settings.payloadKey, 8));
    return;
  }
  if (!command.startsWith(setPrefix)) {
    replyError(RakError::Generic);
    return;
  }

  uint8_t newKey[16];
  const String keyToken = command.substring(setPrefix.length());
  if (!parsePKey(keyToken, newKey)) {
    replyError(keyToken.length() > 16 ? RakError::TooLong : RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  memcpy(settings.payloadKey, newKey, sizeof(settings.payloadKey));
  saveSettings();
  replyOk();
}

void handleCryptoIvCommand(const String& command) {
  if (command == F("AT+CRYPIV?")) {
    printCommandHelp("AT+CRYPIV", F("get or set the encryption IV of P2P mode (16 bytes in hex)"));
    return;
  }
  if (command == F("AT+CRYPIV=?")) {
    printCommandValue("AT+CRYPIV", bytesToHex(settings.cryptoIv, sizeof(settings.cryptoIv)));
    return;
  }
  if (!command.startsWith(F("AT+CRYPIV="))) {
    replyError(RakError::Generic);
    return;
  }

  uint8_t newIv[16];
  const String ivToken = command.substring(10);
  if (!parseKeyOrIv(ivToken, newIv)) {
    replyError(ivToken.length() > 32 ? RakError::TooLong : RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  memcpy(settings.cryptoIv, newIv, sizeof(settings.cryptoIv));
  saveSettings();
  replyOk();
}

void handleBitRateCommand(const String& command, const __FlashStringHelper* commandPrefix) {
  if (settings.modulation != ModulationMode::Fsk) {
    replyError(RakError::Parameter);
    return;
  }

  const String prefix(commandPrefix);
  const String setPrefix = prefix + '=';
  if (command == prefix + '?') {
    printCommandHelp(prefix.c_str(), F("get or set the P2P FSK modem bitrate (600-300000 b/s)"));
    return;
  }
  if (command == prefix + "=?") {
    printCommandValue(prefix.c_str(), static_cast<long>(settings.fskBitRate));
    return;
  }
  if (!command.startsWith(setPrefix)) {
    replyError(RakError::Generic);
    return;
  }

  unsigned long value = 0;
  if (!parseUnsignedLongFlexible(command.substring(setPrefix.length()), value) || !isValidFskBitRate(value)) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  settings.fskBitRate = value;
  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handleFrequencyDeviationCommand(const String& command, const __FlashStringHelper* commandPrefix) {
  if (settings.modulation != ModulationMode::Fsk) {
    replyError(RakError::Parameter);
    return;
  }

  const String prefix(commandPrefix);
  const String setPrefix = prefix + '=';
  if (command == prefix + '?') {
    printCommandHelp(prefix.c_str(), F("get or set the P2P FSK modem frequency deviation (600-200000 Hz)"));
    return;
  }
  if (command == prefix + "=?") {
    printCommandValue(prefix.c_str(), static_cast<long>(settings.fskFrequencyDeviation));
    return;
  }
  if (!command.startsWith(setPrefix)) {
    replyError(RakError::Generic);
    return;
  }

  unsigned long value = 0;
  if (!parseUnsignedLongFlexible(command.substring(setPrefix.length()), value) || !isValidFskFrequencyDeviation(value)) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  settings.fskFrequencyDeviation = value;
  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handleEncodingCommand(const String& command) {
  if (settings.modulation != ModulationMode::Fsk) {
    replyError(RakError::Parameter);
    return;
  }

  if (command == F("AT+ENCODING?")) {
    printCommandHelp("AT+ENCODING", F("vendor extension: get or set FSK encoding"));
    return;
  }
  if (command == F("AT+ENCODING=?")) {
    printCommandValue("AT+ENCODING", encodingLabel(settings.fskEncoding));
    return;
  }
  if (!command.startsWith(F("AT+ENCODING="))) {
    replyError(RakError::Generic);
    return;
  }

  uint8_t encoding = 0;
  if (!parseEncodingValue(command.substring(12), encoding)) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  settings.fskEncoding = encoding;
  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handleShapingCommand(const String& command) {
  if (settings.modulation != ModulationMode::Fsk) {
    replyError(RakError::Parameter);
    return;
  }

  if (command == F("AT+SHAPING?")) {
    printCommandHelp("AT+SHAPING", F("vendor extension: get or set FSK data shaping"));
    return;
  }
  if (command == F("AT+SHAPING=?")) {
    printCommandValue("AT+SHAPING", shapingLabel(settings.fskDataShaping));
    return;
  }
  if (!command.startsWith(F("AT+SHAPING="))) {
    replyError(RakError::Generic);
    return;
  }

  uint8_t shaping = 0;
  if (!parseShapingValue(command.substring(11), shaping)) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  settings.fskDataShaping = shaping;
  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handleFixedLengthPayloadCommand(const String& command) {
  if (command == F("AT+FIXLENGTHPAYLOAD?")) {
    printCommandHelp("AT+FIXLENGTHPAYLOAD", F("get or set P2P fix length payload on/off (1 = ON, 0 = OFF)"));
    return;
  }
  if (command == F("AT+FIXLENGTHPAYLOAD=?")) {
    printCommandValue("AT+FIXLENGTHPAYLOAD", settings.fixedLengthPayload ? 1 : 0);
    return;
  }
  if (!command.startsWith(F("AT+FIXLENGTHPAYLOAD="))) {
    replyError(RakError::Generic);
    return;
  }

  const String enabledToken = command.substring(20);
  bool enabled = false;
  if (!parsePayloadCryptValue(enabledToken, enabled)) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  settings.fixedLengthPayload = enabled;
  if (!enabled) {
    settings.fixedPayloadLength = MAX_FRAME_SIZE;
  }
  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handleP2PCommand(const String& command) {
  if (command == F("AT+P2P?")) {
    printCommandHelp("AT+P2P", F("get or set all P2P parameters"));
    return;
  }

  if (command == F("AT+P2P=?")) {
    if (configChangesBlocked()) {
      replyError(RakError::Busy);
      return;
    }

    String value = String(settings.rfFrequencyHz);
    if (settings.modulation == ModulationMode::LoRa) {
      value += ':';
      value += String(static_cast<unsigned int>(settings.loraSpreadingFactor));
      value += ':';
      value += loraBandwidthCode(settings.loraBandwidthIndex);
      value += ':';
      value += String(static_cast<unsigned int>(settings.loraCodingRate - 1));
      value += ':';
      value += String(static_cast<unsigned long>(settings.loraPreambleLength));
      value += ':';
      value += String(static_cast<long>(settings.outputPower));
    } else {
      value += ':';
      value += String(settings.fskBitRate);
      value += ':';
      value += String(settings.fskRxBandwidth);
      value += ':';
      value += String(settings.fskFrequencyDeviation);
      value += ':';
      value += String(static_cast<unsigned long>(settings.fskPreambleLength));
      value += ':';
      value += String(static_cast<long>(settings.outputPower));
    }
    printCommandValue("AT+P2P", value);
    return;
  }

  if (!command.startsWith(F("AT+P2P="))) {
    replyError(RakError::Generic);
    return;
  }

  const String args = command.substring(7);
  int cursor = 0;
  const String freqToken = nextToken(args, cursor, ':');

  long frequency = 0;
  if (!parseLongStrict(freqToken, frequency) || !isValidFrequency(static_cast<uint32_t>(frequency))) {
    replyError(RakError::Parameter);
    return;
  }

  if (configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  ModemSettings updated = settings;
  updated.rfFrequencyHz = static_cast<uint32_t>(frequency);

  if (settings.modulation == ModulationMode::LoRa) {
    const String sfToken = nextToken(args, cursor, ':');
    const String bandwidthToken = nextToken(args, cursor, ':');
    const String codingRateToken = nextToken(args, cursor, ':');
    const String preambleToken = nextToken(args, cursor, ':');
    const String powerToken = nextToken(args, cursor, ':');

    long sf = 0;
    long codingRate = 0;
    long preamble = 0;
    long power = 0;
    uint8_t bandwidthIndex = 0;

    if (!parseLongStrict(sfToken, sf) ||
        !parseLoRaBandwidthValue(bandwidthToken, bandwidthIndex) ||
        !parseLongStrict(codingRateToken, codingRate) ||
        !parseLongStrict(preambleToken, preamble) ||
        !parseLongStrict(powerToken, power) ||
        cursor != static_cast<int>(args.length())) {
      replyError(RakError::Parameter);
      return;
    }

    if (!isValidLoRaSpreadingFactor(sf) || !isValidLoRaCodingRate(codingRate) ||
        !isValidLoRaPreamble(preamble) || !isValidOutputPower(power)) {
      replyError(RakError::Parameter);
      return;
    }

    updated.loraSpreadingFactor = static_cast<uint8_t>(sf);
    updated.loraBandwidthIndex = bandwidthIndex;
    updated.loraCodingRate = static_cast<uint8_t>(codingRate + 1);
    updated.loraPreambleLength = static_cast<uint16_t>(preamble);
    updated.outputPower = static_cast<int8_t>(power);
  } else {
    const String bitrateToken = nextToken(args, cursor, ':');
    const String bandwidthToken = nextToken(args, cursor, ':');
    const String fdevToken = nextToken(args, cursor, ':');
    const String preambleToken = nextToken(args, cursor, ':');
    const String powerToken = nextToken(args, cursor, ':');

    unsigned long bitrate = 0;
    unsigned long bandwidth = 0;
    unsigned long fdev = 0;
    long preamble = 0;
    long power = 0;

    if (!parseUnsignedLongFlexible(bitrateToken, bitrate) ||
        !parseUnsignedLongFlexible(bandwidthToken, bandwidth) ||
        !parseUnsignedLongFlexible(fdevToken, fdev) ||
        !parseLongStrict(preambleToken, preamble) ||
        !parseLongStrict(powerToken, power) ||
        cursor != static_cast<int>(args.length())) {
      replyError(RakError::Parameter);
      return;
    }

    if (!isValidFskBitRate(bitrate) || !isValidFskBandwidth(bandwidth) ||
        !isValidFskFrequencyDeviation(fdev) || !isValidFskPreamble(preamble) ||
        !isValidOutputPower(power)) {
      replyError(RakError::Parameter);
      return;
    }

    ModemSettings profileCheck = updated;
    profileCheck.fskBitRate = bitrate;
    profileCheck.fskRxBandwidth = bandwidth;
    profileCheck.fskFrequencyDeviation = fdev;
    if (!isValidSx1262FskProfile(profileCheck)) {
      replyError(RakError::Parameter);
      return;
    }

    updated.fskBitRate = bitrate;
    updated.fskRxBandwidth = bandwidth;
    updated.fskFrequencyDeviation = fdev;
    updated.fskPreambleLength = static_cast<uint16_t>(preamble);
    updated.outputPower = static_cast<int8_t>(power);
  }

  settings = updated;
  if (!applyAndSaveSettings()) {
    replyError(RakError::Generic);
    return;
  }
  replyOk();
}

void handleSendCommand(const String& command) {
  if (command == F("AT+PSEND?") || command == F("AT+SEND?")) {
    const char* response = command.startsWith(F("AT+PSEND")) ? "AT+PSEND" : "AT+SEND";
    printCommandHelp(response, F("send data in P2P mode"));
    return;
  }
  const bool isAlias = command.startsWith(F("AT+SEND="));
  if (!command.startsWith(F("AT+PSEND=")) && !isAlias) {
    replyError(RakError::Generic);
    return;
  }

  uint8_t payload[MAX_FRAME_SIZE];
  size_t payloadLength = 0;
  const String payloadToken = command.substring(isAlias ? 8 : 9);
  if (!parseHexPayload(payloadToken, payload, payloadLength)) {
    replyError(payloadToken.length() > (MAX_FRAME_SIZE * 2) ? RakError::TooLong : RakError::Parameter);
    return;
  }

  uint8_t frame[MAX_FRAME_SIZE];
  size_t frameLength = 0;
  if (!encryptPayload(payload, payloadLength, frame, frameLength)) {
    replyError(RakError::TooLong);
    return;
  }

  if (settings.modulation == ModulationMode::Fsk && settings.fixedLengthPayload && frameLength != settings.fixedPayloadLength) {
    replyError(RakError::Parameter);
    return;
  }

  transmitBytes(frame, frameLength);
}

void handleReceiveCommand(const String& command) {
  if (command == F("AT+PRECV?")) {
    printCommandHelp("AT+PRECV", F("enter P2P RX mode for a period of time (ms)"));
    return;
  }
  if (command == F("AT+PRECV=?")) {
    printCommandValue("AT+PRECV", static_cast<long>(currentReceiveValue()));
    return;
  }
  if (!command.startsWith(F("AT+PRECV="))) {
    replyError(RakError::Generic);
    return;
  }

  long value = 0;
  if (!parseLongStrict(command.substring(9), value) || !isValidReceiveValue(value)) {
    replyError(RakError::Parameter);
    return;
  }

  if (value != 0 && configChangesBlocked()) {
    replyError(RakError::Busy);
    return;
  }

  if (value == 0) {
    receivePolicy = RakReceivePolicy::Disabled;
    receiveWindowMs = 0;
  } else if (value == 65533) {
    receivePolicy = RakReceivePolicy::ContinuousTx;
    receiveWindowMs = 65533UL;
  } else if (value == 65534) {
    receivePolicy = RakReceivePolicy::Continuous;
    receiveWindowMs = 65534UL;
  } else if (value == 65535) {
    receivePolicy = RakReceivePolicy::OneShot;
    receiveWindowMs = 65535UL;
  } else {
    receivePolicy = RakReceivePolicy::Timed;
    receiveWindowMs = static_cast<uint32_t>(value);
  }

  if (applyReceiveState() != RADIOLIB_ERR_NONE) {
    replyError(RakError::Rx);
    return;
  }
  replyOk();
}

void handleCommand(String command) {
  command.trim();
  if (command.length() == 0) {
    return;
  }

  if (command == F("AT")) {
    replyOk();
    return;
  }
  if (command == F("AT?")) {
    printHelp();
    return;
  }
  if (command == F("ATR")) {
    if (!restoreDefaults()) {
      replyError(RakError::Generic);
      return;
    }
    replyOk();
    return;
  }
  if (command == F("ATZ")) {
    replyOk();
    delay(50);
    ESP.restart();
    return;
  }
  if (command.startsWith(F("AT+BUILDTIME"))) {
    handleBuildTimeCommand(command);
    return;
  }
  if (command.startsWith(F("AT+BOOTVER"))) {
    handleBootVersionCommand(command);
    return;
  }
  if (command.startsWith(F("AT+HWMODEL"))) {
    handleHardwareModelCommand(command);
    return;
  }
  if (command == F("AT+VER?") || command == F("AT+VER=?")) {
    printCommandValue("AT+VER", MODEM_VERSION);
    return;
  }
  if (command.startsWith(F("AT+CLIVER"))) {
    handleCliVersionCommand(command, F("AT+CLIVER"));
    return;
  }
  if (command.startsWith(F("AT+APIVER"))) {
    handleCliVersionCommand(command, F("AT+APIVER"));
    return;
  }
  if (command.startsWith(F("AT+HWID"))) {
    handleHardwareIdCommand(command);
    return;
  }
  if (command.startsWith(F("AT+SN"))) {
    handleSerialNumberCommand(command);
    return;
  }
  if (command.startsWith(F("AT+BAT"))) {
    handleSystemVoltageCommand(command, F("AT+BAT"));
    return;
  }
  if (command.startsWith(F("AT+SYSV"))) {
    handleSystemVoltageCommand(command, F("AT+SYSV"));
    return;
  }
  if (command.startsWith(F("AT+BLEMAC"))) {
    handleBleMacCommand(command);
    return;
  }
  if (command.startsWith(F("AT+SLEEP"))) {
    handleSleepCommand(command);
    return;
  }
  if (command.startsWith(F("AT+LPM"))) {
    handleLowPowerModeCommand(command);
    return;
  }
  if (command.startsWith(F("AT+BAUD"))) {
    handleBaudCommand(command);
    return;
  }
  if (command.startsWith(F("AT+BOOT"))) {
    handleBootCommand(command);
    return;
  }
  if (command.startsWith(F("AT+CAD"))) {
    handleCadCommand(command);
    return;
  }

  if (command.startsWith(F("AT+NWM"))) {
    handleNwmCommand(command);
    return;
  }
  if (command.startsWith(F("AT+MOD"))) {
    handleModulationCommand(command);
    return;
  }
  if (command.startsWith(F("AT+RFFREQUENCY"))) {
    handleRfFrequencyCommand(command, F("AT+RFFREQUENCY"));
    return;
  }
  if (command.startsWith(F("AT+PFREQ"))) {
    handleRfFrequencyCommand(command, F("AT+PFREQ"));
    return;
  }
  if (command.startsWith(F("AT+FREQUENCY"))) {
    handleRfFrequencyCommand(command, F("AT+FREQUENCY"));
    return;
  }
  if (command.startsWith(F("AT+SPREADINGFACTOR"))) {
    handleSpreadingFactorCommand(command, F("AT+SPREADINGFACTOR"));
    return;
  }
  if (command.startsWith(F("AT+PSF"))) {
    handleSpreadingFactorCommand(command, F("AT+PSF"));
    return;
  }
  if (command.startsWith(F("AT+BANDWIDTH"))) {
    handleBandwidthCommand(command, F("AT+BANDWIDTH"));
    return;
  }
  if (command.startsWith(F("AT+PBW"))) {
    handleBandwidthCommand(command, F("AT+PBW"));
    return;
  }
  if (command.startsWith(F("AT+CODINGRATE"))) {
    handleCodingRateCommand(command, F("AT+CODINGRATE"));
    return;
  }
  // NOTE: AT+PCR is a prefix of AT+PCRYPT, so route PCRYPT/PKEY first to
  // avoid mis-dispatching encryption commands into the coding-rate handler.
  if (command.startsWith(F("AT+PCRYPT"))) {
    handlePayloadCryptCommand(command, F("AT+PCRYPT"));
    return;
  }
  if (command.startsWith(F("AT+PKEY"))) {
    handlePayloadKeyCommand(command, F("AT+PKEY"));
    return;
  }
  if (command.startsWith(F("AT+PCR"))) {
    handleCodingRateCommand(command, F("AT+PCR"));
    return;
  }
  if (command.startsWith(F("AT+PREAMBLELENGTH"))) {
    handlePreambleLengthCommand(command, F("AT+PREAMBLELENGTH"));
    return;
  }
  if (command.startsWith(F("AT+PPL"))) {
    handlePreambleLengthCommand(command, F("AT+PPL"));
    return;
  }
  if (command.startsWith(F("AT+PREAMBLE"))) {
    handlePreambleLengthCommand(command, F("AT+PREAMBLE"));
    return;
  }
  if (command.startsWith(F("AT+TXOUTPUTPOWER"))) {
    handlePowerCommand(command, F("AT+TXOUTPUTPOWER"));
    return;
  }
  if (command.startsWith(F("AT+PTP"))) {
    handlePowerCommand(command, F("AT+PTP"));
    return;
  }
  if (command.startsWith(F("AT+POWER"))) {
    handlePowerCommand(command, F("AT+POWER"));
    return;
  }
  if (command.startsWith(F("AT+SYNCWORD"))) {
    handleSyncWordCommand(command);
    return;
  }
  if (command.startsWith(F("AT+SYMBOLTIMEOUT"))) {
    handleSymbolTimeoutCommand(command);
    return;
  }
  if (command.startsWith(F("AT+ENCRY"))) {
    handlePayloadCryptCommand(command, F("AT+ENCRY"));
    return;
  }
  if (command.startsWith(F("AT+ENCKEY"))) {
    handlePayloadKeyCommand(command, F("AT+ENCKEY"));
    return;
  }
  if (command.startsWith(F("AT+CRYPIV"))) {
    handleCryptoIvCommand(command);
    return;
  }
  if (command.startsWith(F("AT+PBR"))) {
    handleBitRateCommand(command, F("AT+PBR"));
    return;
  }
  if (command.startsWith(F("AT+BITRATE"))) {
    handleBitRateCommand(command, F("AT+BITRATE"));
    return;
  }
  if (command.startsWith(F("AT+PFDEV"))) {
    handleFrequencyDeviationCommand(command, F("AT+PFDEV"));
    return;
  }
  if (command.startsWith(F("AT+FDEV"))) {
    handleFrequencyDeviationCommand(command, F("AT+FDEV"));
    return;
  }
  if (command.startsWith(F("AT+ENCODING"))) {
    handleEncodingCommand(command);
    return;
  }
  if (command.startsWith(F("AT+SHAPING"))) {
    handleShapingCommand(command);
    return;
  }
  if (command.startsWith(F("AT+FIXLENGTHPAYLOAD"))) {
    handleFixedLengthPayloadCommand(command);
    return;
  }
  if (command.startsWith(F("AT+P2P"))) {
    handleP2PCommand(command);
    return;
  }
  if (command.startsWith(F("AT+PSEND")) || command.startsWith(F("AT+SEND"))) {
    handleSendCommand(command);
    return;
  }
  if (command.startsWith(F("AT+PRECV"))) {
    handleReceiveCommand(command);
    return;
  }
  if (command == F("AT+RXSTATS") || command == F("AT+RXSTATS=?")) {
    Serial.print(F("AT+RXSTATS:irq="));
    Serial.print(static_cast<unsigned long>(rxIrqCount));
    Serial.print(F(",active="));
    Serial.print(receiveActive ? 1 : 0);
    Serial.print(F(",policy="));
    Serial.print(static_cast<unsigned int>(static_cast<uint8_t>(receivePolicy)));
    Serial.print(F(",rssi="));
    Serial.println(static_cast<int>(radio.getRSSI(false)));
    replyOk();
    return;
  }
  if (command == F("AT+RXDIAG?") || command == F("AT+RXDIAG=?")) {
    printCommandValue("AT+RXDIAG", rxDiagVerbose ? 1 : 0);
    return;
  }
  if (command.startsWith(F("AT+RXDIAG="))) {
    const String value = command.substring(10);
    if (value == F("0")) {
      rxDiagVerbose = false;
      replyOk();
      return;
    }
    if (value == F("1")) {
      rxDiagVerbose = true;
      replyOk();
      return;
    }
    replyError(RakError::Parameter);
    return;
  }

  replyError(RakError::Generic);
}

void setup() {
  prefs.begin("loramodem", false);
  loadSettings();
  pinMode(ACTIVITY_LED_PIN, OUTPUT);
  setLoraActivityLed(false);

  const uint32_t configuredBaud = settings.baudRate;
  // Large RX buffer so long AT+PSEND hex payloads (a 200-byte block becomes
  // a ~410 char line after framing + AES padding + hex encoding) do not get
  // dropped while the loop is busy in handleReceive / radio.transmit.
  Serial.setRxBufferSize(2048);
  Serial.begin(SERIAL_BAUD);
  while (!Serial && millis() < 3000) {
    delay(10);
  }

  Serial.print(F("+BOOT:baud_default="));
  Serial.print(SERIAL_BAUD);
  Serial.print(F(",baud_config="));
  Serial.println(configuredBaud);

  if (configuredBaud != SERIAL_BAUD) {
    Serial.println(F("+BOOT:Switching to configured baud"));
    restartSerialAtBaud(configuredBaud);
    delay(50);
  }

  Serial.println(F("+BOOT:Initializing SPI"));
  SPI.begin(LORA_SCK_PIN, LORA_MISO_PIN, LORA_MOSI_PIN, LORA_NSS_PIN);

  Serial.println(F("+BOOT:Initializing radio"));
  const int state = initializeRadio(settings);
  if (state != RADIOLIB_ERR_NONE) {
    Serial.print(F("+ERR=BOOT "));
    Serial.println(state);
    return;
  }

  Serial.print(F("+READY:"));
  Serial.print(MODEM_VERSION);
  Serial.print(',');
  Serial.println(modulationName(settings.modulation));
  lastCommandAt = millis();
}

void loop() {
  while (Serial.available() > 0) {
    const char c = static_cast<char>(Serial.read());
    if (c == '\r') {
      pendingCommandCr = true;
      continue;
    }
    if (c == '\n') {
      if (pendingCommandCr) {
        handleCommand(commandBuffer);
        lastCommandAt = millis();
        commandBuffer = "";
      }
      pendingCommandCr = false;
      continue;
    }
    pendingCommandCr = false;
    if (commandBuffer.length() >= 512) {
      commandBuffer = "";
      replyError(RakError::TooLong);
      continue;
    }
    commandBuffer += c;
  }

  handleReceiveTimeout();
  handleReceive();
  if (settings.lowPowerEnabled && !receiveActive && commandBuffer.length() == 0 && Serial.available() == 0) {
    if (static_cast<long>(millis() - lastCommandAt) >= static_cast<long>(LPM_IDLE_DELAY_MS)) {
      enterLightSleep(0);
    }
  }
}
