/*
  ESP32 based Weather Station with a WiFi Server to provide sensor data for Prometheus to scrape  .
  Borrowed information from:
    Jeff Geerling - https://github.com/geerlingguy/airgradient-prometheus
                  - https://www.jeffgeerling.com/blog/2021/airgradient-diy-air-quality-monitor-co2-pm25
    AirGradient   - https://www.airgradient.com/diyshop/#!/DIY-AQ-Kit-1/p/447361353/category=128812596            
    ESP32 example "SimpleWiFiServer"
    https://randomnerdtutorials.com/esp32-websocket-server-arduino/
    Arduino BME280 library

  One day, in a future version, I plan to learn how to make all of the windvane code into a library.
  This will simplify the code overall.
  
  Written by Trevor Hone, 29 Mar 2022.
  Releasable to the general public.
 */

#include <WiFi.h>
#include <Arduino.h>
#include <WebServer.h>
#include <BME280I2C.h>
#include <Wire.h>


/* I found this on this forum thread https://forum.arduino.cc/t/fill-whole-array-with-the-same-value/66313/7 and I intend to use it to reset arrays.
  I plan on using an array to track position values instead of 128 separate variables.
  An alternative would be to use memset(yourArray,0,sizeof(yourArray));
*/


// Config ----------------------------------------------------------------------
// WiFi and IP connection info.
  const char* ssid     = "Your WIFI SSID here";
  const char* password = "Your WIFI password here";
  const int port = 9921;
  const char* deviceId = "Weather_station";

BME280I2C::Settings settings(
   BME280::OSR_X1,
   BME280::OSR_X1,
   BME280::OSR_X1,
   BME280::Mode_Forced,
   BME280::StandbyTime_1000ms,
   BME280::Filter_Off,
   BME280::SpiEnable_False,
   BME280I2C::I2CAddr_0x77 // I2C address. I2C specific.
);

BME280I2C bme(settings);


// Constants for calculating voltage and Windspeed. ESP32 assigns a 12 bit number (0-4095) for voltages from 0-3.3V
const float adc_constant = (3.3/4095);

// For calculating wind position and direction
  byte windPosition_raw = 0b00000000;


  const byte encoder_key[128] = {0b01111111, 0b00111111, 0b00111110, 0b00111010, 0b00111000, 0b10111000, 0b10011000, \
  0b00011000, 0b00001000, 0b01001000, 0b01001001, 0b01001101, 0b01001111, 0b00001111, 0b00101111, 0b10101111, 0b10111111, \
  0b10011111, 0b00011111, 0b00011101, 0b00011100, 0b01011100, 0b01001100, 0b00001100, 0b00000100, 0b00100100, 0b10100100, \
  0b10100110, 0b10100111, 0b10000111, 0b10010111, 0b11010111, 0b11011111, 0b11001111, 0b10001111, 0b10001110, 0b00001110, \
  0b00101110, 0b00100110, 0b00000110, 0b00000010, 0b00010010, 0b01010010, 0b01010011, 0b11010011, 0b11000011, 0b11001011, \
  0b11101011, 0b11101111, 0b11100111, 0b11000111, 0b01000111, 0b00000111, 0b00010111, 0b00010011, 0b00000011, 0b00000001, \
  0b00001001, 0b00101001, 0b10101001, 0b11101001, 0b11100001, 0b11100101, 0b11110101, 0b11110111, 0b11110011, 0b11100011, \
  0b10100011, 0b10000011, 0b10001011, 0b10001001, 0b10000001, 0b10000000, 0b10000100, 0b10010100, 0b11010100, 0b11110100, \
  0b11110000, 0b11110010, 0b11111010, 0b11111011, 0b11111001, 0b11110001, 0b11010001, 0b11000001, 0b11000101, 0b11000100, \
  0b11000000, 0b01000000, 0b01000010, 0b01001010, 0b01101010, 0b01111010, 0b01111000, 0b01111001, 0b01111101, 0b11111101, \
  0b11111100, 0b11111000, 0b11101000, 0b11100000, 0b11100010, 0b01100010, 0b01100000, 0b00100000, 0b00100001, 0b00100101, \
  0b00110101, 0b00111101, 0b00111100, 0b10111100, 0b10111110, 0b11111110, 0b01111110, 0b01111100, 0b01110100, 0b01110000, \
  0b01110001, 0b00110001, 0b00110000, 0b00010000, 0b10010000, 0b10010010, 0b10011010, 0b10011110, 0b00011110, 0b01011110, \
  0b01011111
  };
 
  int position_count[128];
  const float positionToDegree = 2.8515625; // this is the number that results from 365/128 and is used to convert position to degrees
  
  
// Config End ------------------------------------------------------------------

// Start the webserver and make gloabally available
WebServer server(port);

void setup() {
    analogSetAttenuation(ADC_11db);              // Sets the input attenuation for ALL ADC inputs, default is ADC_11db, range is ADC_0db, ADC_2_5db, ADC_6db, ADC_11db
    
    Serial.begin(9600);
    String hostname = deviceId;
    
    pinMode(5, OUTPUT);      // set the LED pin mode
    delay(10);
// start communication with BME280 sensor
  Wire.begin();

  while(!bme.begin())
  {
    Serial.println("Could not find BME280 sensor!");
    delay(1000);
  }

// Setting pins to read windvane encoder outputs
    pinMode(12, INPUT);
    pinMode(13, INPUT);
    pinMode(14, INPUT);
    pinMode(25, INPUT);
    pinMode(26, INPUT);
    pinMode(27, INPUT);
    pinMode(32, INPUT);
    pinMode(33, INPUT);


// Connecting to a WiFi network
    
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);

    WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
    WiFi.setHostname(hostname.c_str()); //define hostname
    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("");
    Serial.println("WiFi connected.");
    Serial.println("Signal Strength: " + WiFi.RSSI());
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
    Serial.print("MAC address: ");
    Serial.println(WiFi.macAddress());
    Serial.print("Hostname: ");
    Serial.println(WiFi.getHostname());
    server.on("/", HandleRoot);
    server.on("/metrics", HandleRoot);
    server.onNotFound(HandleNotFound);
    
    server.begin();
    Serial.println("HTTP server started at ip " + WiFi.localIP().toString() + ":" + String(port));
    
}

void loop() {
  /*
   * This section is used to sample the wind direction.
   * My prometheus server will query the device every 30sec which is where I reset the array.
   * If you don't have a similar setup, add a periodic memset(position_count,0,sizeof(position_count));  
   * in order to keep the data clean and consistent.
   */

  int current_position = -1;
  bitWrite(windPosition_raw, 7, digitalRead(13));
  bitWrite(windPosition_raw, 6, digitalRead(12));
  bitWrite(windPosition_raw, 5, digitalRead(14));
  bitWrite(windPosition_raw, 4, digitalRead(27));
  bitWrite(windPosition_raw, 3, digitalRead(26));
  bitWrite(windPosition_raw, 2, digitalRead(25));
  bitWrite(windPosition_raw, 1, digitalRead(33));
  bitWrite(windPosition_raw, 0, digitalRead(32));

  current_position = findPosition(windPosition_raw);
  if (current_position != -1) {
    position_count[current_position]++;
    }
  
  // looking for a call to the webserver
  server.handleClient();
  // delay for stability
  delay(20);
}



void HandleRoot() {
    server.send(200, "text/plain", GenerateMetrics() );
    //server.send(200, "text/plain", "hello");
}

void HandleNotFound() {
    String message = "File Not Found\n\n";
    message += "URI: ";
    message += server.uri();
    message += "\nMethod: ";
    message += (server.method() == HTTP_GET) ? "GET" : "POST";
    message += "\nArguments: ";
    message += server.args();
    message += "\n";
    for (uint i = 0; i < server.args(); i++) {
      message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
    }
    server.send(404, "text/html", message);
}

String GenerateMetrics() {
  // setup bme sensor calls (borrowed from the BME280 I2C Test.ino)
   float temp(NAN), hum(NAN), pres(NAN);

   BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
   BME280::PresUnit presUnit(BME280::PresUnit_Pa);

   bme.read(pres, temp, hum, tempUnit, presUnit);

   // Calculations for C to F and "standard" barometric pressure with temp changes
      float temp_f = (temp * 9/5) + 32;                                                //convert the temp to farenheight for display
      float baro = 1013.25*exp(-((0.02896*9.807)/(8.3143*(temp+273.15))*1292.96));
      
    // Setting up the string formatting for prometheus
      String message = "";
      String idString = "{id=\"" + String(deviceId) + "\",mac=\"" + WiFi.macAddress().c_str() + "\"} ";
 
 
  /* Calculating voltage & windspeed and adding to the message
   * Sensor output parameters
   * Voltage Min = 0.4
   * Voltage Max = 2
   * Wind Speed Min = 0
   * Wind Speed Max  = 32.4
   */
   
    analogSetAttenuation(ADC_11db); //setting attenuation to be able to handle the top voltage https://randomnerdtutorials.com/esp32-adc-analog-read-arduino-ide/
    float sensorValue = analogReadMilliVolts(39);  // get the ADC reading in millivolts
    //float anemometerVoltage = sensorValue * adc_constant;  //ESP32 12 bit reading to voltage
    float anemometerVoltage = sensorValue/1000;
    float wind_speed = (anemometerVoltage - 0.418) * (32.4 - 0) / (2 - 0.4); // (voltage_reading - voltage_min) * (windspeed_max - windspeed_min) / (voltage_max - voltage_min) + windspeed_min
    float speed_mph = ((wind_speed *3600)/1609.344);
/*   //For troubleshooting
    Serial.println(analogReadMilliVolts(39));
    Serial.println(anemometerVoltage);
    Serial.println(wind_speed);
    Serial.println(speed_mph);
*/    
    message += "# HELP wind_mph Anemometer windspeed\n";
    message += "wind_mph";
    message += idString;
    message += String(speed_mph);
    message += "\n";
  /*
   * Adding the wind direction to the message
   */

    //For troubleshooting
    //Serial.println(findPosition(windPosition_raw));
    int windMode = windPosition_mode();
    memset(position_count,0,sizeof(position_count));
    float windMode_degrees = windMode * positionToDegree;
        
 //   String cardinal_direction = cardinal(windMode_degrees);
    
    message += "# HELP wind_degrees 8-bit DIY windvane direction in degrees\n";
    message += "wind_degrees";
    message += idString;
    message += String(windMode_degrees);
    message += "\n";
/* //It appears that Prometheus does not allow for string values at this time (LAME!)
  // Having the cardinal direction separate for now.
    message += "# HELP 8-bit DIY windvane cardinal direction\n";
    message += "wind_direction_cardinal";
    message += idString;
    message += cardinal_direction;
    message += "\n";
*/
    
/*
 * Adding the BME280 data to the message stream for prometheus
 * Temp converted into F from C
 * Pressure in hPa 
 * Relative Humidity
 */

    message += "# HELP BME280 Temperature\n";
    message += "temp_f";
    message += idString;
    message += String(temp_f);
    message += "\n";
    message += "# HELP BME280 Pressure in hPa\n";
    message += "pressure_hpa";
    message += idString;
    message += String(pres/100);
    message += "\n";
    message += "# HELP BME280 Relative Humidity percent\n";
    message += "relative_hum";
    message += idString;
    message += String(hum);
    message += "\n";
    message += "# HELP Calculated 'average' barometric pressure based on tempurature and 4242ft (1292.96m) at home\n";
    message += "baro";
    message += idString;
    message += String(baro);
    message += "\n";

    return message;
}


int findPosition(byte desiredValue){
    int val_position;
      for (int i=0; i < 128; i++){
      if (desiredValue == encoder_key[i]){
        val_position = i;
        break;
      }
    }
    return val_position;
}


int windPosition_mode(){
    int max_v = -1;
    int max_i = -1;
      for ( int i = 0; i < 128; i++ )
      {
        if ( position_count[i] > max_v )
        {
          max_v = position_count[i];
          max_i = i;
        }
      }
    return max_i;
}
/* //This takes the degrees from the windvane and returns a string value of the cardinal direction
String cardinal (float deg){
  String cardinal_val;
  if(deg == 0){
    cardinal_val = "N";
  }
  
  if(deg > 348.75 || deg <= 11.25){
    cardinal_val = "N";
  }
  
  else if ((deg > 11.25) && (deg <= 33.75)){
    cardinal_val = "NNE";    
  }

  else if ((deg > 33.75) && (deg <= 56.25)){
    cardinal_val = "NE";    
  }
  
  else if ((deg > 56.25) && (deg <= 78.75)){
    cardinal_val = "ENE";    
  }
  
  else if ((deg > 78.75) && (deg <= 101.25)){
    cardinal_val = "E";    
  }
  
  else if ((deg > 101.25) && (deg <= 123.75)){
    cardinal_val = "ESE";    
  }
  
  else if ((deg > 123.75) && (deg <= 146.25)){
    cardinal_val = "SE";    
  }
  
  else if ((deg > 146.25) && (deg <= 168.75)){
    cardinal_val = "SSE";    
  }
  
  else if ((deg > 168.75) && (deg <= 191.25)){
    cardinal_val = "S";    
  }

  else if ((deg > 191.25) && (deg <= 213.75)){
    cardinal_val = "SSW";    
  }

  else if ((deg > 213.75) && (deg <= 236.25)){
    cardinal_val = "SW";    
  }

  else if ((deg > 236.25) && (deg <= 258.75)){
    cardinal_val = "WSW";    
  }

  else if ((deg > 258.75) && (deg <= 281.25)){
    cardinal_val = "W";    
  }

  else if ((deg > 281.25) && (deg <= 303.75)){
    cardinal_val = "WNW";    
  }

  else if ((deg > 303.75) && (deg <= 326.25)){
    cardinal_val = "NW";    
  }

  else if ((deg > 325.25) && (deg <= 348.75)){
    cardinal_val = "NNW";    
  }
  return cardinal_val;
}
*/
/*
 * this is unused for now
int arrayMax(int myArray[], int sizeOfArray){
    int max_v = INT_MIN;
    int max_i = -1;
      for ( int i = 0; i < sizeOfArray; i++ )
    {
      if ( myArray[i] > max_v )
      {
        max_v = myArray[i];
        max_i = i;
      }
    }
    return max_i;
}
*/
