#include <SoftwareSerial.h>

// Define the pins for RS485 communication
#define RO 2
#define DI 3
#define RE 8
#define DE 7

#define RESPONSE_FRAME_SIZE 21

char sensorDataTextBuffer[200];

struct SoilSensorData
{
  bool isSensorTimeout {false};
  bool isValid {false};
  float temperature {-1.0};
  float humidity {-1.0};
  unsigned int conductivity {-1};
  float ph {-1.0};
  unsigned int nitrogen {-1};
  unsigned int phosphorus {-1};
  unsigned int potassium {-1};
  unsigned int salinity {-1};
};

class SoilSensor
{
  public:
    SoilSensor()
    {
      modbus = new SoftwareSerial(RO, DI);
    }

    void initialise()
    {
      Serial.begin(9600); // Initialize serial communication for debugging
      modbus->begin(9600);    // Initialize software serial communication at 9600 baud rate

      pinMode(RE, OUTPUT); // Set RE pin as output
      pinMode(DE, OUTPUT); // Set DE pi
    }

    void sendDataRequest()
    {
      digitalWrite(DE, HIGH);
      digitalWrite(RE, HIGH);
      delay(10);

      // Send the request frame to the soil sensor
       modbus->write(soilSensorRequest, sizeof(soilSensorRequest));
    }
    
    SoilSensorData read()
    {
      SoilSensorData soilSensorData;

       // End the transmission mode and set to receive mode for RS485
      digitalWrite(DE, LOW);
      digitalWrite(RE, LOW);
      delay(10); 
      
      //Wait for the response from the sensor or timeout after 1 second
      unsigned long startTime = millis();
      while (modbus->available() < RESPONSE_FRAME_SIZE && millis() - startTime < 1000)
      {
        delay(1);
      }

      if (modbus->available() >= RESPONSE_FRAME_SIZE) // If valid response received
      {
       // Read the response from the sensor
        byte index = 0;
        while (modbus->available() && index < RESPONSE_FRAME_SIZE)
        {
          soilSensorResponse[index] = modbus->read();
          index++;
        }
        
        soilSensorData = computeData();
      }
      else
      {
        soilSensorData.isSensorTimeout = true; 
      }
 
      return soilSensorData;
    } 

    ~SoilSensor()
    {
      if (modbus != nullptr) delete modbus;
      delete[] soilSensorResponse;
    }
  
  private:
    const byte soilSensorRequest[8] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x08, 0x44, 0x0C};
    SoftwareSerial* modbus = nullptr;
    byte soilSensorResponse[RESPONSE_FRAME_SIZE];

   
    int convertBytesToDecimal(unsigned int offset)
    {
      return soilSensorResponse[offset] * 256 + soilSensorResponse[offset + 1];
    }

    SoilSensorData computeData() const
    {
      SoilSensorData soilSensorData;

      if (soilSensorResponse[0] != 1 || soilSensorResponse[1] != 3 || soilSensorResponse[2] != 16)
      {
        soilSensorData.isValid = false;
        return soilSensorData;
      }

      soilSensorData.isValid = true;
      soilSensorData.temperature = convertBytesToDecimal(3) / 10.0; //degrees celcius
      soilSensorData.humidity = convertBytesToDecimal(5) / 10.0; // percent
      soilSensorData.conductivity = convertBytesToDecimal(7); // microSiemens per centimetre
      soilSensorData.ph = convertBytesToDecimal(9) / 100.0;
      soilSensorData.nitrogen = convertBytesToDecimal(11); // miligram per Kilogram
      soilSensorData.phosphorus = convertBytesToDecimal(13); // miligram per Kilogram
      soilSensorData.potassium = convertBytesToDecimal(15); // miligram per Kilogram
      soilSensorData.salinity = convertBytesToDecimal(17); // miligram per Kilogram

      return soilSensorData;
    }
 };


SoilSensor soilSensor;

void writeSensorDataToString(SoilSensorData & sensorData)
{
  if (sensorData.isSensorTimeout)
  {
      sprintf(sensorDataTextBuffer, "Incomplete data or sensor time out");  
  }
  else
  {
    if (sensorData.isValid)  
    {
      char* tempStr = malloc(6 * sizeof(char));
      char* humidityStr = malloc(6 * sizeof(char));
      char* phStr = malloc(6 * sizeof(char));

      dtostrf(sensorData.temperature, 4, 1, tempStr);
      dtostrf(sensorData.humidity, 4, 1, humidityStr);
      dtostrf(sensorData.ph, 4, 2, phStr);

      auto conductivity = sensorData.conductivity;
      auto n = sensorData.nitrogen;
      auto p = sensorData.phosphorus;
      auto k = sensorData.potassium;
      auto salinity = sensorData.salinity;

      sprintf(sensorDataTextBuffer, "Temperature: %s; Humidity: %s; Conductivity: %u; PH: %s; N: %u; P: %u; K: %u; Salinity: %u", tempStr, humidityStr, conductivity, phStr, n, p, k, salinity);  

      if (tempStr != NULL) free(tempStr);
      if (humidityStr != NULL) free(humidityStr);
      if (phStr != NULL) free(phStr);
    }
    else
    {
      sprintf(sensorDataTextBuffer, "Data read from sensor is invalid");  
    }
  }
}


void setup() 
{
 soilSensor.initialise();
}

void loop() 
{
  soilSensor.sendDataRequest();
  SoilSensorData sensorData = soilSensor.read();
  writeSensorDataToString(sensorData);
  Serial.println(sensorDataTextBuffer);

  delay(2000); // Wait for a second before the next loop iteration
}