Solartracker_prototyp.ino


/*
  Sonnenstandsberechnung und Solartracking
  - Prototyp von 2018 -
  Positionsangaben für Ort: Rönkhausen
  Version 16.0, M. Schulte, 23. September 2023
  - mit hochpräziser DS3231RTC Echtzeituhr
  - Mit LCD-Ausgabe
  - mit Ausgabe Spannungswert PV-Modul
  - mit Hall-Drehwinkelsensor
  - AzS, AzI neu angepasst
  - Wenn AzI == AzS dann rote und grüne LED an
  - Funktion Windrichtung modifiziert
  - Neu: Grenzwert-Definition mit switch und case
  - Neu: mit RTClib.h statt DS3231.h:
  - damit kompatibel zu "Arduino UNO R4 Minima" Board
  - Neu: Temperatur auslesen von DS3231 RTC, Anzeige statt MOZ
*/

#include "RTClib.h"                       // für die Zeitsteuerung
#include <DTutils.h>                      // für die Berechnung des Jahrestages
#include <Servo.h>                        // für die Servo-Höhenwinkelverstellung
#include <AFMotor.h>                      // für die Schrittmotor-Azimutverstellung
#include <LiquidCrystal_I2C.h>            // für die Datenausgabe auf dem LCD-Display

RTC_DS3231 rtc;                           // DS3231 ist RealTimeClock
AF_Stepper stepper(200, 2);               // Stepper 200 steps/U, shield-Anschluss M2
LiquidCrystal_I2C lcd(0x26, 20, 4);       // I2C Adresse des Displays von 0x27 auf 0x26 geändert
Servo servo2;                             // Servomotor für Elevation

float pi = 3.14159265;
float kwert = pi / 180;
float latitude = 51.222191;               // Ortskoordinate Breitengrad
float longitude = 7.954169;               // Ortskoordinate Längengrad
float Elevation; float elevation;
float Aufgang; float Untergang;
float AzS; int AzI;                       // Gradwerte Soll- und Istwert (Hall-Sensor)
char buffer[80];                          // buffer für DateTime-Display-Ausgabe
int zeitschritt = 4000;                   // Zeitschritt loop = 4 sec


void setup() {

  if (! rtc.begin())
  {
    lcd.setCursor(1, 1);
    lcd.print("Couldn't find RTC");
    while (1)
    {
      delay(10);
    }
  }

  // rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));    // Hier Datum und aktuelle Uhrzeit (MEZ) setzen,
  // rtc.adjust(DateTime(2023, 9, 23, 15, 54, 00));     // danach diese Zeile(n) auskommentieren und sketch neu laden

  pinMode(A3, INPUT);                                   // setze Hall-Sensor Pin A3
  pinMode(A2, INPUT);                                   // setze Spannung PV-Panel Pin A2
  pinMode(14, OUTPUT);                                  // setze grüne LED Pin 14 (A0)
  pinMode(15, OUTPUT);                                  // setze rote LED Pin 15 (A1)

  servo2.attach(9);                                     // initialisiere Servo2 Pin 9

  stepper.setSpeed(10);                                 // setze Stepper auf 10 U/min
  stepper.release();                                    // schalte Stepper stromlos
  lcd.init();                                           // initialisiere 20x4 LCD
  lcd.backlight();                                      // initialisiere LCD Hintergrundlicht an
  lcd.clear();                                          // lösche Bildschirm

  lcd.setCursor(0, 0);
  lcd.print("Solartracker v.16.0");
  lcd.setCursor(0, 1);
  lcd.print("Version vom 23.09.23");
  lcd.setCursor(0, 2);
  lcd.print("www.mi-schu.de");
  lcd.setCursor(0, 3);
  lcd.print("/solartracker.htm");
  delay(zeitschritt);
  lcd.clear();
}

// Beginn der Schleife


void loop() {

  float Temp = float(rtc.getTemperature());             // Temperatur holen von DS3231 RTC
  DateTime now = rtc.now();                             // Zeitwert holen von RTC Echtzeituhr
  sprintf(buffer, "%02d.%02d.%04d  %02d:%02d:%02d", now.day(), now.month(), now.year(), now.hour(), now.minute(), now.second());

  int Jahr = now.year();
  int Monat = now.month();
  int Tag = now.day();
  float Stunde = now.hour();
  float Minute = now.minute();
  float Sekunde = now.second();

  float Deklination;
  float Zeitgleichung;
  float Zeitdifferenz;
  float AufgangOrtszeit;
  float UntergangOrtszeit;
  float Tageslaenge;
  float MEZ;  float MOZ;  float WOZ;                                                   // Mitteleuropäische Zeit, Mittlere Ortszeit, Wahre Ortszeit
  float ZeitSeitMittag;
  float BreiteR = latitude * kwert;                                                    // geogr. Breite in Radians
  float hoeheSM = -(50.0 / 60.0) * kwert;                                              // Höhe des Sonnenmittelpunkts bei Aufgang
  int Grenzwert;
  int Zeitzone = 1;                                                                    // Zeitzone Greenwich + 1 (z.B. Berlin)
  int sensorValue;                                                                     // Analoger Ist-Wert (Hall-Sensor)
  int yearday;
  String wr;

  MEZ = Stunde + Minute / 60 + Sekunde / 3600;                                         // Berechne MEZ
  yearday = DayOfYear(now.year(), now.month(), now.day());                             // Aufruf der Funktion: yearday
  Zeitgleichung = BerechneZeitgleichung (yearday);                                     // Aufruf der Funktion: BerechneZeitgleichung
  Deklination = BerechneDeklination (yearday);                                         // Aufruf der Funktion: BerechneDeklination
  Zeitdifferenz = BerechneZeitdifferenz (hoeheSM, latitude, kwert, Deklination, pi);   // Aufruf der Funktion: BerechneZeitdifferenz
  ZeitSeitMittag = MEZ + longitude / 15.0 - Zeitzone - 12 + Zeitgleichung;             // Berechne ZeitSeitMittag
  AzS = SunAngles(Deklination, BreiteR, ZeitSeitMittag, Elevation);                    // Azimut-Soll: Aufruf der Funktion: SunAngles
  Elevation = Elevation / kwert;
  Tageslaenge = BerechneTageslaenge (Zeitdifferenz, Zeitgleichung, longitude, Zeitzone, Aufgang, Untergang); // Aufruf der Funktion: BerechneTageslaenge

  MOZ = MEZ - (-longitude / 15) - 1;                                                   // Berechne Mittlere Ortszeit aus MEZ und Längengrad
  WOZ = MOZ + Zeitgleichung;                                                           // Berechne Wahre Ortszeit

  int WOZdez = (int)WOZ;
  int WOZmin = (WOZ - WOZdez) * 60;
  int MOZdez = (int)MOZ;
  int MOZmin = (MOZ - MOZdez) * 60;
  int StdL = (int)Tageslaenge;
  int MinL = (int)((Tageslaenge - StdL) * 60);
  int StdA = (int)Aufgang;
  int MinA = (int)((Aufgang - StdA) * 60);
  int StdU = (int)Untergang;
  int MinU = (int)((Untergang - StdU) * 60);

  switch (StdL) {
    case 7 ... 8:
      Grenzwert = 135; break;
    case 9 ... 10:
      Grenzwert = 115; break;
    case 11 ... 12:
      Grenzwert = 95; break;
    case 13 ... 14:
      Grenzwert = 75; break;
    case 15 ... 16:
      Grenzwert = 55; break;
    case 17 ... 18:
      Grenzwert = 35; break;
  }

  // Steuerung Servomotor für elevation / Servoposition

  elevation = map(Elevation, 90, 0, 96, 3);                   // Kalibrierung der Servoposition (elevation)
  constrain(elevation, 3, 96);                                // Begrenzung des Servo-Wertebereichs

  // Steuerung Steppermotor für Azimutposition

  sensorValue = analogRead(A3);                               // Auslesen Hall-Sensorwert an Pin A3

  if (sensorValue >= 121 && sensorValue < 500) {              // 2-stufige Skalierung der Hall-Sensorwerte
    AzI = map(sensorValue, 248, 503, 90, 180);                // 1.Stufe: Skalierung der Hall-Sensorwerte in Gradwerte 45-180 Grad
  }                                                           // Neu kalibriert: 30.08.23
  if (sensorValue >= 500 && sensorValue <= 883) {
    AzI = map(sensorValue, 503, 752, 180, 270);               // 2. Stufe: Skalierung der Hall-Sensorwerte in Gradwerte 180-315 Grad
  }
  constrain(AzI, 40, 320);                                    // Begrenzung des Azimut-Ist-Wertebereichs
  float AzSoll = AzS;                                         // AzSoll float-Variable für Displayausgabe
  AzS = int(AzS);
  stepper.release();                                          // Stepper stromlos schalten

  if (Elevation > -1) {                                       // Solange Elevation > -1 --> Sonne steht über dem Horizont
    servo2.write(elevation);                                  // Schreiben der Servoposition (Nachführung)
    if (AzI < AzS) {                                          // Nachführung: Wenn Hall-Sensorwert kleiner als Soll, dann:
      digitalWrite(14, HIGH);                                 // LED grün an
      stepper.step(32, FORWARD, MICROSTEP);                   // Steppermotor dreht 32 steps vorwärts (entspricht +1 Grad des Drehkranzes)
      digitalWrite(15, LOW);                                  // LED rot aus
      stepper.release();                                      // Stepper stromlos schalten
    }
    if (AzI > AzS) {                                          // Nachführung: Wenn Hall-Sensorwert grösser als Soll, dann:
      digitalWrite(15, HIGH);                                 // LED rot an
      stepper.step(32, BACKWARD, MICROSTEP);                  // Steppermotor dreht 32 steps rückwärts (entspricht -1 Grad des Drehkranzes)
      digitalWrite(14, LOW);                                  // LED grün aus
      stepper.release();                                      // Stepper stromlos schalten
    }
    if (AzI == AzS) {                                         // Nachführung: Wenn Hall-Sensorwert gleich dem Sollwert, dann:
      digitalWrite(15, HIGH);                                 // LED rot an
      digitalWrite(14, HIGH);                                 // LED grün an
    }
  }
  else {                                                      // Sonne ist untergegangen --> Rückführung auf Anfangsposition
    if (Elevation < -1 && AzI >= Grenzwert) {                 // Rückführung: Wenn Elevation < -1 (Sonne ist untergegangen), dann:
      digitalWrite(15, HIGH);                                 // LED rot an: Stepper dreht rückwärts
      stepper.setSpeed(30);                                   // Stepper 30 U/min
      stepper.step(100, BACKWARD, MICROSTEP);                 // Schneller Rücklauf mit 100 steps/loop solange Hall-Sensorwert >= Grenzwert
      digitalWrite(14, LOW);                                  // LED grün aus
      stepper.release();                                      // Stepper stromlos schalten
    }
  }

  // Abfrage 3-Volt-Solarpanel

  int analogPin = A2;                                         // Solarpanel Pluspol an A3
  float pvmod;
  float volt;
  pvmod = analogRead(analogPin);                              // Abfrage Spannung Solar Panel
  volt = pvmod / 1023 * 4.75;                                 // Umrechnung Analogwert in Spannungswert (Volt)

  // für Anzeige: Stunden, Minuten, Deklination

  int Std = (int)Tageslaenge;
  int Min = (int)((Tageslaenge - Std) * 60);
  Deklination = Deklination / kwert;                          // Umwandlung Deklination in Grad
  wr = Windrichtung(AzI);                                     // Aufruf Funktion: Windrichtung


  // Ausgabe der Werte auf 20x4 LCD-Display

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(buffer);

  lcd.setCursor(0, 1);
  lcd.print("AzS");
  lcd.setCursor(4, 1);
  lcd.print(AzSoll, 2);
  lcd.write(0xDF);
  lcd.print(" ");

  lcd.setCursor(12, 1);
  
  if (WOZdez < 10) lcd.print("0");
  lcd.print(WOZdez);
  lcd.print(":");
  if (WOZmin < 10) lcd.print("0");
  lcd.print(WOZmin);
  lcd.print(" WO");

  lcd.setCursor(0, 2);
  lcd.print("AzI ");
  lcd.print(AzI);
  lcd.write(0xDF);
  lcd.print(wr);
  lcd.print(" ");

  lcd.setCursor(0, 3);
  lcd.print("Elv");
  lcd.setCursor(12, 3);
  
  lcd.setCursor(3, 3);
  lcd.print(" ");
  lcd.setCursor(4, 3);
  lcd.print(Elevation, 2);
  lcd.write(0xDF);
  
  lcd.setCursor(12, 2);
  lcd.print("TM ");
  lcd.print(Temp, 1);
  lcd.write(0xDF);
  
  lcd.setCursor(12, 3);
  lcd.print("PV ");
  lcd.print(volt, 1);
  lcd.print(" V");

  delay(zeitschritt);  // Programmpause
  lcd.clear();

  lcd.setCursor(0, 0);
  lcd.print("Tageslaenge  ");
  if (Std < 10) lcd.print("0");
  lcd.print(Std);
  lcd.print(":");
  if (Min < 10) lcd.print("0");
  lcd.print(Min);
  lcd.print(" h");

  lcd.setCursor(0, 1);
  lcd.print("So.Aufgang   ");
  if (StdA < 10) lcd.print("0");
  lcd.print(StdA);
  lcd.print(":");
  if (MinA < 10) lcd.print("0");
  lcd.print(MinA);
  lcd.print(" h");

  lcd.setCursor(0, 2);
  lcd.print("So.Untergang ");
  if (StdU < 10) lcd.print("0");
  lcd.print(StdU);
  lcd.print(":");
  if (MinU < 10) lcd.print("0");
  lcd.print(MinU);
  lcd.print(" h");

  lcd.setCursor(0, 3);
  lcd.print("Deklination ");
  if (Deklination >= 0 && Deklination < 10) {
    lcd.print(" ");
    lcd.print(Deklination, 2);
  }
  if (Deklination >= 10 || Deklination < 0 && Deklination > -10) {
    lcd.print(" ");
    lcd.print(Deklination, 3);
  }
  if (Deklination <= -10) lcd.print(Deklination, 3);
  lcd.write(0xDF);

  delay(zeitschritt);  // Programmpause
}

// Funktion: Windrichtung

String Windrichtung (int AzI) {
  int i;

  switch (AzI) {
    case 0 ... 11:
      i = 0; break;
    case 12 ... 34:
      i = 1; break;
    case 35 ... 56:
      i = 2; break;
    case 57 ... 79:
      i = 3; break;
    case 80 ... 101:
      i = 4; break;
    case 102 ... 124:
      i = 5; break;
    case 125 ... 146:
      i = 6; break;
    case 147 ... 169:
      i = 7; break;
    case 170 ... 191:
      i = 8; break;
    case 192 ... 214:
      i = 9; break;
    case 215 ... 236:
      i = 10; break;
    case 237 ... 259:
      i = 11; break;
    case 260 ... 281:
      i = 12; break;
    case 282 ... 304:
      i = 13; break;
    case 305 ... 326:
      i = 14; break;
    case 327 ... 349:
      i = 15; break;
  }
  const char* windrichtung[16] = {"N ", "NNO", "NO ", "ONO", "O ", "OSO", "SO ", "SSO", "S ", "SSW", "SW ", "WSW", "W ", "WNW", "NW ", "NNW"};
  String wr = windrichtung[i];
  return wr;
}

// Funktion: SunAngles für die Berechnung von Soll und Elevation

float SunAngles (float Deklination, float BreiteR, float ZeitSeitMittag, float & Elevation) {
  float cosdec = cos(Deklination);
  float sindec = sin(Deklination);
  float lha = ZeitSeitMittag * (1.0027379 - 1 / 365.25) * 15 * kwert;
  float coslha = cos(lha);
  float sinlha = sin(lha);
  float coslat = cos(BreiteR);
  float sinlat = sin(BreiteR);
  float N = -cosdec * sinlha;
  float D = sindec * coslat - cosdec * coslha * sinlat;
  Elevation = asin(sindec * sinlat + cosdec * coslha * coslat);    // Höhe des Sonnenmittelpunktes über dem Horizont
  float AzS = atan2(N, D);
  if (AzS < 0) {
    AzS += 2 * pi;
  }
  AzS = AzS / kwert;
  return AzS;
}

// Funktion: BerechneZeitgleichung

float BerechneZeitgleichung (int yearday) {
  float Zeitgleichung = -0.170869921174742 * sin(0.0336997028793971 * yearday + 0.465419984181394) - 0.129890681040717 * sin(0.0178674832556871 * yearday - 0.167936777524864);
  return Zeitgleichung;
}

// Funktion: BerechneDeklination

float BerechneDeklination(int yearday) {
  float Deklination = 0.409526325277017 * sin(0.0169060504029192 * (yearday - 80.0856919827619));
  return Deklination;
}

// Funktion: BerechneZeitdifferenz

float BerechneZeitdifferenz(float hoeheSM, float latitude, float kwert, float Deklination, float pi) {
  float Zeitdifferenz;
  Zeitdifferenz = 12 * acos((sin(hoeheSM) - sin(latitude * kwert) * sin(Deklination)) / (cos(latitude * kwert) * cos(Deklination))) / pi;
  return Zeitdifferenz;
}

// Funktion: BerechneTageslaenge

float BerechneTageslaenge (float Zeitdifferenz, float Zeitgleichung, float longitude, int Zeitzone, float & Aufgang, float & Untergang) {
  float AufgangOrtszeit = 12 - Zeitdifferenz - Zeitgleichung;
  float UntergangOrtszeit = 12 + Zeitdifferenz - Zeitgleichung;
  Aufgang = AufgangOrtszeit - longitude / 15 + Zeitzone;
  Untergang = UntergangOrtszeit - longitude / 15 + Zeitzone;
  float Tageslaenge = Untergang - Aufgang;
  return Tageslaenge;
}