/*
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;
}