/**
 * Works with SX126x-Arduino 2.0.32, BSP for RAKwireless nRF board 1.3.3 & bootloader 0.4.4 06/17/2026
 * @file Base LoRaWAN_OTAA_ABP_rak4631-1_v4.ino a.k.a. TTN end device rak4631-1 9-11-26
 * @author rakwireless.com - Rev. MichaelOboy
 * @brief LoRaWan node example with OTAA/ABP registration
 * @version 0.1 Revised for rak4631-1 eui and keys.
 * @version 0.2 Changed void waitForResponse to bool & and updated original sketch 
 * @date 2025-08-21  Rev. 9-11-2026.  
 * 
 * @copyright Copyright (c) 2020
 * 
 * @note RAK4631 GPIO mapping to nRF52840 GPIO ports
   RAK4631    <->  nRF52840
   WB_IO1     <->  P0.17 (GPIO 17)
   WB_IO2     <->  P1.02 (GPIO 34)
   WB_IO3     <->  P0.21 (GPIO 21)
   WB_IO4     <->  P0.04 (GPIO 4)
   WB_IO5     <->  P0.09 (GPIO 9)
   WB_IO6     <->  P0.10 (GPIO 10)
   WB_SW1     <->  P0.01 (GPIO 1)
   WB_A0      <->  P0.05/AIN3 (AnalogIn A3)
   WB_A1      <->  P0.31/AIN7 (AnalogIn A7)
 */

#include <Arduino.h>
#include <LoRaWan-RAK4630.h>  //http://librarymanager/All#SX126x
#include <SPI.h>
#include "RAK13010_SDI12.h"
#define TX_PIN WB_IO6      // SDI-12 data bus TX
#define RX_PIN WB_IO5      // SDI-12 data bus RX
#define OE_PIN WB_IO4      // Output enable (active low)
#define SENSOR_PWR WB_IO2  // 4.2V sensor rail
#define PIN_GRN WB_IO1
RAK_SDI12 mySDI12(RX_PIN, TX_PIN, OE_PIN);

int timer = 100;

// RAK4630 supply two LED
#ifndef LED_BUILTIN
#define LED_BUILTIN 35
#endif

#ifndef LED_BUILTIN2
#define LED_BUILTIN2 36
#endif


String sdiMsgStr;
bool sdiMsgReady = false;
volatile bool do_send = false;

// List of sensor addresses to poll
const char SENSOR_LIST[] = { '1', '2' };
const uint8_t SENSOR_COUNT = sizeof(SENSOR_LIST);
// Structure to hold converted sensor data
struct SensorData {
  float dielectric;
  float temperature;
  float vwc;
  bool valid;
};

SensorData sensorData[2];
float toppVWC(float e) {
  // Topp equation for VWC
  return -0.053 + 0.0292 * e - 0.00055 * e * e + 0.0000043 * e * e * e;
}

bool doOTAA = true;                                               // OTAA is used by default.
#define SCHED_MAX_EVENT_DATA_SIZE APP_TIMER_SCHED_EVENT_DATA_SIZE /**< Maximum size of scheduler events. */
#define SCHED_QUEUE_SIZE 60                                       /**< Maximum number of events in the scheduler queue. */
#define LORAWAN_DATERATE DR_0                                     /*LoRaMac datarates definition, from DR_0 to DR_5*/
#define LORAWAN_TX_POWER TX_POWER_5                               /*LoRaMac tx power definition, from TX_POWER_0 to TX_POWER_15*/
#define JOINREQ_NBTRIALS 3                                        /**< Number of trials for the join request. */
DeviceClass_t g_CurrentClass = CLASS_A;                           /* class definition*/
LoRaMacRegion_t g_CurrentRegion = LORAMAC_REGION_US915;           /* Region:US915*/
lmh_confirm g_CurrentConfirm = LMH_UNCONFIRMED_MSG;               /* confirm/unconfirm packet definition*/
uint8_t gAppPort = LORAWAN_APP_PORT;                              /* data port*/

/**@brief Structure containing LoRaWan parameters, needed for lmh_init()
*/
static lmh_param_t g_lora_param_init = { LORAWAN_ADR_OFF, LORAWAN_DATERATE, LORAWAN_PUBLIC_NETWORK, JOINREQ_NBTRIALS, LORAWAN_TX_POWER, LORAWAN_DUTYCYCLE_OFF };

// Foward declaration
static void lorawan_has_joined_handler(void);
static void lorawan_join_failed_handler(void);
static void lorawan_rx_handler(lmh_app_data_t *app_data);

static void lorawan_confirm_class_handler(DeviceClass_t Class);
static void send_lora_frame(void);

/**@brief Structure containing LoRaWan callback functions, needed for lmh_init()
*/
static lmh_callback_t g_lora_callbacks = {
  BoardGetBatteryLevel,
  BoardGetUniqueId,
  BoardGetRandomSeed,
  lorawan_rx_handler,
  lorawan_has_joined_handler,
  lorawan_confirm_class_handler,
  lorawan_join_failed_handler
};

/* void lorawan_tx_complete_handler(bool success) {
    Serial.print("TX complete: ");
    Serial.println(success);
}
*/

//OTAA keys !!!! KEYS ARE MSB !!!!
// Replace these with your TTN values
uint8_t nodeDeviceEUI[8] = { 0xAC, 0x1F, 0x09, 0xFF, 0xFE, 0x28, 0x5D, 0x26 };
// AC 1F 09 FF FE 28 5D 26
// AC1F09FFFE285D26

uint8_t nodeAppEUI[8] = { 0xAC, 0x1F, 0x09, 0xFF, 0xF8, 0x68, 0x46, 0x31 };
// AC 1F 09 FF F8 68 46 31
// AC1F09FFF8684631

uint8_t nodeAppKey[16] = { 0xBC, 0xAF, 0x7E, 0x83, 0x44, 0x21, 0xED, 0x99,
                           0x63, 0x3D, 0x33, 0x33, 0x25, 0xF6, 0xE2, 0x0D };
// BC AF 7E 83 44 21 ED 99 63 3D 33 33 25 F6 E2 0D
// BCAF7E834421ED99633D333325F6E20D

// ABP keys
uint32_t nodeDevAddr = 0x260CAA48;
// 26 0C AA 48
// 260CAA48

uint8_t nodeNwsKey[16] = { 0x33, 0xD0, 0x9E, 0x3D, 0x85, 0x2A, 0xA8, 0x54, 0xA6, 0xA4, 0x9E, 0x1D, 0xCF, 0x73, 0x17, 0xF0 };
// 33 D0 9E 3D 85 2A A8 54 A6 A4 9E 1D CF 73 17 F0
// 33D09E3D852AA854A6A49E1DCF7317F0

uint8_t nodeAppsKey[16] = { 0x55, 0x4B, 0xC2, 0x03, 0xCA, 0xEF, 0x11, 0x38, 0x3B, 0x61, 0xB7, 0x6D, 0x4C, 0xA5, 0xF3, 0xFD };
// 55 4B C2 03 CA EF 11 38 3B 61 B7 6D 4C A5 F3 FD
// 554BC203CAEF11383B61B76D4CA5F3FD

// Private definition
#define LORAWAN_APP_DATA_BUFF_SIZE 11                                                              /**< buffer size of the data to be transmitted. 64 orig.*/
#define LORAWAN_APP_INTERVAL 10000                                                                 /**< Defines for user timer, the application data transmission interval. 20s, value in [ms]. */
static uint8_t m_lora_app_data_buffer[LORAWAN_APP_DATA_BUFF_SIZE];                                 //< Lora user application data buffer.
static lmh_app_data_t m_lora_app_data = { m_lora_app_data_buffer, 0, 0, LMH_UNCONFIRMED_MSG, 0 };  //< Lora user application data structure.


TimerEvent_t appTimer;
static uint32_t timers_init(void);
static uint32_t count = 0;
static uint32_t count_fail = 0;

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);

  pinMode(SENSOR_PWR, OUTPUT);
  digitalWrite(SENSOR_PWR, HIGH);  // Power the sensors

  pinMode(PIN_GRN, OUTPUT);
  digitalWrite(PIN_GRN, HIGH);
  
  // Initialize Serial for debug output
  time_t timeout = millis();
  Serial.begin(115200);
  while (!Serial) {
    if ((millis() - timeout) < 5000) {
      delay(100);
    } else {
      break;
    }
  }
  Serial.println("Starting dual-sensor SDI-12 polling with payload conversion...");

  mySDI12.begin();
  delay(500);
  mySDI12.forceListen();

  // Initialize LoRa chip.
  lora_rak4630_init();

  Serial.println("=====================================");
  Serial.println("Welcome to RAK4630 LoRaWan!!!");
  if (doOTAA) {
    Serial.println("Type: OTAA");
  } else {
    Serial.println("Type: ABP");
  }
  switch (g_CurrentRegion) {
    case LORAMAC_REGION_AS923:
      Serial.println("Region: AS923");
      break;
    case LORAMAC_REGION_AU915:
      Serial.println("Region: AU915");
      break;
    case LORAMAC_REGION_CN470:
      Serial.println("Region: CN470");
      break;
    case LORAMAC_REGION_CN779:
      Serial.println("Region: CN779");
      break;
    case LORAMAC_REGION_EU433:
      Serial.println("Region: EU433");
      break;
    case LORAMAC_REGION_IN865:
      Serial.println("Region: IN865");
      break;
    case LORAMAC_REGION_EU868:
      Serial.println("Region: EU868");
      break;
    case LORAMAC_REGION_KR920:
      Serial.println("Region: KR920");
      break;
    case LORAMAC_REGION_US915:
      Serial.println("Region: US915");
      break;
    case LORAMAC_REGION_RU864:
      Serial.println("Region: RU864");
      break;
    case LORAMAC_REGION_AS923_2:
      Serial.println("Region: AS923-2");
      break;
    case LORAMAC_REGION_AS923_3:
      Serial.println("Region: AS923-3");
      break;
    case LORAMAC_REGION_AS923_4:
      Serial.println("Region: AS923-4");
      break;
  }
  Serial.println("=====================================");

  //create a user timer to send data to server period
  uint32_t err_code;
  err_code = timers_init();
  if (err_code != 0) {
    Serial.printf("timers_init failed - %d\n", err_code);
    return;
  }
  // Setup the EUIs and Keys
  if (doOTAA) {
    lmh_setDevEui(nodeDeviceEUI);
    lmh_setAppEui(nodeAppEUI);
    lmh_setAppKey(nodeAppKey);
  } else {
    lmh_setNwkSKey(nodeNwsKey);
    lmh_setAppSKey(nodeAppsKey);
    lmh_setDevAddr(nodeDevAddr);
  }

  // Initialize LoRaWan
  err_code = lmh_init(&g_lora_callbacks, g_lora_param_init, doOTAA, g_CurrentClass, g_CurrentRegion);

  // lmh_set_tx_done_cb(lorawan_tx_complete_handler);  // register the missing TX_handler required for BSP 0.21.20

  lmh_setSubBandChannels(2);  // // <-- FIX FOR US915 TTN: added to force radio to use Ch 8-15

  if (err_code != 0) {
    Serial.printf("lmh_init failed - %d\n", err_code);
    return;
  }
  // Start Join procedure
  
  lmh_join();
}


void loop() {

  digitalWrite(PIN_GRN, HIGH);
  delay(100);

  if (do_send) {

    // Give LoRaWAN stack time to finish RX2
    delay(7000);

    poll_sdi12_sensors();
    delay(100);  // small gap before send

    send_lora_frame();


    do_send = false;
     
  }

  digitalWrite(PIN_GRN, LOW);
  delay(100);
}

bool waitForResponse() {
  sdiMsgStr = "";
  sdiMsgReady = false;  // See version note 0.2

  uint32_t start = millis();
  while (millis() - start < 4000) {  // 4-second timeout
    int avail = mySDI12.available();

    if (avail < 0) {
      mySDI12.clearBuffer();
    } else if (avail > 0) {
      for (int a = 0; a < avail; a++) {
        char inByte = mySDI12.read();
        if (inByte == '\n') {
          sdiMsgReady = true;
        } else {
          sdiMsgStr += String(inByte);
        }
      }
    }
    if (sdiMsgReady) {
      Serial.println("R: " + sdiMsgStr);
      return true;
    }
  }

  Serial.println("R: (timeout)");
  return false;
}

void poll_sdi12_sensors() {
  // Poll each sensor sequentially
  for (uint8_t i = 0; i < SENSOR_COUNT; i++) {
    char addr = SENSOR_LIST[i];
    uint8_t idx = (addr == '1') ? 0 : 1;

    sensorData[idx].valid = false;

    // --- Send M! ---
    mySDI12.clearBuffer();
    String cmdM = String(addr) + "M!";
    Serial.println("T: " + cmdM);
    mySDI12.sendCommand(cmdM);

    // --- Wait for measurement time response ---
    waitForResponse();

    // --- Send D0! command ---
    delay(2000);  // TEROS22 returns early, but 2s is safe
    mySDI12.clearBuffer();
    String cmdD = String(addr) + "D0!";
    Serial.println("T: " + cmdD);
    mySDI12.sendCommand(cmdD);

    // --- Wait for data response ---
    if (waitForResponse()) {
      parseSensorData(idx, sdiMsgStr);
    }

    Serial.println("-----------------------------");
  }
  // Pause between full polling cycles
  printPayload();
  delay(5000);
}

void parseSensorData(uint8_t idx, String msg) {
  // Expected format: "1-88941.0+26.8"
  int dashPos = msg.indexOf('-');
  int plusPos = msg.indexOf('+');

  if (dashPos < 0 || plusPos < 0) return;

  float raw = msg.substring(dashPos + 1, plusPos).toFloat();
  float dielectric = raw / 1000.0;  // or /10000.0 depending on sensor
  float temperature = msg.substring(plusPos + 1).toFloat();
  float vwc = toppVWC(dielectric);

  sensorData[idx].dielectric = dielectric;
  sensorData[idx].temperature = temperature;
  sensorData[idx].vwc = vwc;
  sensorData[idx].valid = true;

  // 3. Print sensor values safely
  if (sensorData[0].valid) {
    Serial.print("S1 dielectric: ");
    Serial.println(sensorData[0].dielectric, 2);

    Serial.print("S1 temp: ");
    Serial.println(sensorData[0].temperature, 2);

    Serial.print("S1 VWC: ");
    Serial.println(sensorData[0].vwc, 2);
  }

  if (sensorData[1].valid) {
    Serial.print("S2 dielectric: ");
    Serial.println(sensorData[1].dielectric, 2);

    Serial.print("S2 temp: ");
    Serial.println(sensorData[1].temperature, 2);

    Serial.print("S2 VWC: ");
    Serial.println(sensorData[1].vwc, 2);
  }

  Serial.println("----------------------------------");
}

void printPayload() {
  Serial.println("Payload:");
  for (uint8_t i = 0; i < SENSOR_COUNT; i++) {
    char addr = SENSOR_LIST[i];
    uint8_t idx = (addr == '1') ? 0 : 1;

    if (sensorData[idx].valid) {
      Serial.print("Sensor ");
      Serial.print(addr);
      Serial.print(" -> Dielectric: ");
      Serial.print(sensorData[idx].dielectric);
      Serial.print("  Temp: ");
      Serial.print(sensorData[idx].temperature);
      Serial.print("C  VWC: ");
      Serial.println(sensorData[idx].vwc);
    } else {
      Serial.print("Sensor ");
      Serial.print(addr);
      Serial.println(" -> No data");
    }
  }

  Serial.println("=====================================");
}

/**@brief LoRa function for handling HasJoined event.
 */
void lorawan_has_joined_handler(void) {

  if (doOTAA == true) {
    Serial.println("OTAA Mode, Network Joined!");
  } else {
    Serial.println("ABP Mode");
  }

  // Force LMH to reset internal state
  lmh_init(&g_lora_callbacks, g_lora_param_init, doOTAA, g_CurrentClass, g_CurrentRegion);

  lmh_error_status ret = lmh_class_request(g_CurrentClass);

  if (ret == LMH_SUCCESS) {
    delay(1000);
    TimerSetValue(&appTimer, LORAWAN_APP_INTERVAL);
    TimerStart(&appTimer);
  }
}

/**@brief LoRa function for handling OTAA join failed
*/
static void lorawan_join_failed_handler(void) {
  Serial.println("OTAA join failed!");
  Serial.println("Check your EUI's and Keys's!");
  Serial.println("Check if a Gateway is in range!");
}

/**@brief Function for handling LoRaWan received data from Gateway
 *
 * @param[in] app_data  Pointer to rx data
 */
void lorawan_rx_handler(lmh_app_data_t *app_data) {
  Serial.printf("LoRa Packet received on port %d, size:%d, rssi:%d, snr:%d, data:%s\n",
                app_data->port, app_data->buffsize, app_data->rssi, app_data->snr, app_data->buffer);
}

void lorawan_confirm_class_handler(DeviceClass_t Class) {
  Serial.printf("switch to class %c done\n", "ABC"[Class]);
  // Informs the server that switch has occurred ASAP
  m_lora_app_data.buffsize = 0;
  m_lora_app_data.port = gAppPort;
  lmh_send(&m_lora_app_data, LMH_UNCONFIRMED_MSG);
}

void send_lora_frame(void) {
  Serial.print("Join status: ");
  Serial.println(lmh_join_status_get());

  if (lmh_join_status_get() == LMH_SET) {
    Serial.println("Join OK");
  }

  if (lmh_join_status_get() != LMH_SET) {
    Serial.println("Not joined, skipping send");  //Not joined, try again later
    return;
  }

  uint32_t i = 0;
  memset(m_lora_app_data.buffer, 0, LORAWAN_APP_DATA_BUFF_SIZE);

  m_lora_app_data.port = gAppPort;  //ok

  // -----------------------------
  // Sensor 1
  // -----------------------------
  Serial.print("Sensor1 valid = ");
  Serial.println(sensorData[0].valid);

  if (sensorData[0].valid) {
    uint16_t d1 = (uint16_t)sensorData[0].dielectric;          // 0-100
    uint16_t t1 = (uint16_t)(sensorData[0].temperature * 10);  // 0-500
    // uint16_t v1 = (uint16_t)(sensorData[0].vwc * 1000);       // 0-1000

    m_lora_app_data.buffer[i++] = highByte(d1);
    m_lora_app_data.buffer[i++] = lowByte(d1);

    m_lora_app_data.buffer[i++] = highByte(t1);
    m_lora_app_data.buffer[i++] = lowByte(t1);

    // m_lora_app_data.buffer[i++] = highByte(v1);
    // m_lora_app_data.buffer[i++] = lowByte(v1);
  } else {
    // Fill with 0xFF if no data
    for (int k = 0; k < 5; k++) m_lora_app_data.buffer[i++] = 0xFF;
  }

  // -----------------------------
  // Sensor 2
  // -----------------------------
  Serial.print("Sensor2 valid = ");
  Serial.println(sensorData[1].valid);

  if (sensorData[1].valid) {
    uint16_t d2 = (uint16_t)sensorData[1].dielectric;
    uint16_t t2 = (uint16_t)(sensorData[1].temperature * 10);
    // uint16_t v2 = (uint16_t)(sensorData[1].vwc * 1000);

    m_lora_app_data.buffer[i++] = highByte(d2);
    m_lora_app_data.buffer[i++] = lowByte(d2);

    m_lora_app_data.buffer[i++] = highByte(t2);
    m_lora_app_data.buffer[i++] = lowByte(t2);

    // m_lora_app_data.buffer[i++] = highByte(v2);
    // m_lora_app_data.buffer[i++] = lowByte(v2);
  } else {
    for (int k = 0; k < 5; k++) m_lora_app_data.buffer[i++] = 0xFF;
  }

  Serial.println("send_lora_frame() entered 1");

  m_lora_app_data.buffsize = i;

  Serial.print("i = ");
  Serial.println(i);

  Serial.print("buffsize = ");
  Serial.println(m_lora_app_data.buffsize);

  Serial.print("Payload bytes: ");
  for (int k = 0; k < i; k++) {
    Serial.print(m_lora_app_data.buffer[k], HEX);
    Serial.print(" ");
  }
  Serial.println();

  int rc = lmh_send(&m_lora_app_data, g_CurrentConfirm);
  Serial.print("lmh_send rc = ");
  Serial.println(rc);

  lmh_error_status error = lmh_send(&m_lora_app_data, LMH_UNCONFIRMED_MSG);  //ok
  // Debug error status
  Serial.print("lmh_send error = ");
  Serial.println(error);

  if (error == LMH_SUCCESS) {
    count++;
    Serial.printf("lmh_send ok count %d\n", count);
  } else {
    count_fail++;
    Serial.printf("lmh_send fail count %d\n", count_fail);
  }
}

/**@brief Function for handling user timerout event.
 */
void tx_lora_periodic_handler(void) {
  TimerSetValue(&appTimer, LORAWAN_APP_INTERVAL);
  TimerStart(&appTimer);
  digitalWrite(PIN_GRN, HIGH);
  do_send = true;
}

/**@brief Function for the Timer initialization.
 *
 * @details Initializes the timer module. This creates and starts application timers.
 */
uint32_t timers_init(void) {
  TimerInit(&appTimer, tx_lora_periodic_handler);
  digitalWrite(PIN_GRN, LOW);
  return 0;
}
