Die Mondphasenuhr       

von Günther Zöppel                       
 
                      Elektronik-Labor  Bastelecke  Projekte  Mikrocontoller                        



Mit meiner "besseren Hälfte" hatte ich kürzlich einen Disput darüber, ob wir nun zu- oder abnehmenden Mond hätten. Ich habe mir das so gemerkt anhand einer Eselsbrücke: Wenn man die helle Seite des Mondes sieht wie eine Klammer Auf ( , dann ist Abnehmender Mond, und umgekehrt, wenn die helle Seite des Mondes aussieht wie eine Klammer Zu ), dann ist Zunehmender Mond. Um dies noch eindrucksvoller zu visualisieren, habe ich mir mit einem ESP32 und einem GC9A01A-Display eine Monduhr gebaut, die diesen Sachverhalt etwas verständlicher demonstriert. Die Uhr hat zwei Anzeigeebenen, die mittels Taster an einem GPIO-Pin des ESP hin-und hergeschaltet werden können. Die erste Ebene stellt 8 Mondsymbole dar, die stilisiert die Mondphasen repräsentieren. Ein roter Zeiger läuft rechtsläufig mit der gerade aktuellen  Mondphase im Mondmonat mit und zeigt auf oder zwischen die entsprechenden Symbole. Zusätzlich wird verbal der Text "Monduhr" in grüner Farbe und der Name der gerade aktuellen Mondphase in weiß eingeblendet.



In der 2. Ebene wird in 5 Textzeilen untereinander folgendes dargestellt:
- aktuelle Uhrzeit (MESZ) über NTP-Server aus dem WLAN geholt und in RTC gespeichert
- aktuelles Datum aus RTC
- Mondaufgangszeit
- Monduntergangszeit
- Beleuchtungsstärke der Mondoberfläche in %



Die letzten 3 Werte habe ich über eine API-Schnittstelle von "ipgeolocation" geholt. Dort kann man sich kostenlos einen Account einrichten und die entsprechenden Daten abrufen, indem man einen entsprechend erhaltenen API-Code in den Sketch einbindet. Da wären auch noch weitere kostenlos über die API mitgelieferte Daten einbindbar, z.B. die Mondentfernung.

Der Sketch selbst ist übersichtlich gestaltet -  alle relevanten Daten sind über Variablen anpassbar und dokumentiert. Für die Mondauf- und Untergangszeiten ist auch die geographische Lage zeitbestimmend, deshalb wird im Sketch auch Längen- und Breitengrad für meinen Wohnort definiert – das kann jederzeit angepasst werden .Zugegeben - bei den trigonometrischen Funktionen habe ich mir etwas Hilfe von ChatGPT geholt, das ist meist effektiver als langwieriges Probieren.  Der Aufbau der Uhr erfolgte erstmal über eine Realisierung auf  Steckboards, der Einbau in ein formschönes Gehäuse kam zum Schluss, ähnlich wie bei meinem Projekt „Wetterstation“.





Für potentielle Nachnutzer hier noch Schaltbild und Sketch :



//Monduhr
// Daten von API-Key "API-Key" von ipgeolocation
//weiter unten in Funktion holeMonddatenVonAPI diesen Key einfügen und Breiten/Längengrad des Standortes
// 0x0000: Schwarz - Farben für Mondsymbole
// 0xFFFF: Weiß
// 0xF800: Rot
// 0x07E0: Grün
// 0x001F: Blau
// 0x000F: Dunkelblau
// 0xA660: Dunkelgelb
#include <Adafruit_GC9A01A.h>
#include <Adafruit_GFX.h>
#include <SPI.h>
#include <Wire.h>
#include <RTClib.h>
#include <WiFi.h>
#include <time.h>
#include <math.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

#define TFT_CS 5
#define TFT_DC 2
#define TFT_RST 4
#define BUTTON_PIN 15

Adafruit_GC9A01A tft(TFT_CS, TFT_DC, TFT_RST);
RTC_DS3231 rtc;

const int centerX = 120;
const int centerY = 120;
const int kreisRadius = 90;
const int mondRadius = 14;
const uint16_t mondfarbe = 0xA660; // Dunkelgelb, hier definieren!

uint32_t letzteStunde = 99;
int letzteSekunde = -1;
bool showPage1 = true;
bool lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 200;

DateTime lastMoonCalcDate;

const char* mondnamen[8] = {
" Neumond",
" zun. Sichel",
" zun. Halbmond",
" zun. Dreiviertel",
" Vollmond",
" abn. Dreiviertel",
" abn. Halbmond",
" abn. Sichel"
};

const char* ssid = "Mein_WLAN";
const char* password = "Mein_Passwort";

const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 7200;
const int daylightOffset_sec = 0;

const double latitude = 50.65;
const double longitude = 13.20;

String moonriseTime, moonsetTime, moonIllumination;

double deg2rad(double deg) { return deg * M_PI / 180.0; }
double rad2deg(double rad) { return rad * 180.0 / M_PI; }

double julianDate(int y, int m, int d, double utHours = 0.0) {
if (m <= 2) { y -= 1; m += 12; }
int A = y / 100;
int B = 2 - A + A / 4;
return floor(365.25 * (y + 4716)) + floor(30.6001 * (m + 1)) + d + B - 1524.5 + utHours / 24.0;
}

void moonPos(double jd, double &ra, double &dec) {
double D = jd - 2451545.0;
double N = fmod(125.1228 - 0.0529538083 * D, 360.0);
double i = 5.1454;
double w = fmod(318.0634 + 0.1643573223 * D, 360.0);
double a = 60.2666;
double e = 0.054900;
double M = fmod(115.3654 + 13.0649929509 * D, 360.0);

double E = M + e * rad2deg(sin(deg2rad(M))) * (1.0 + e * cos(deg2rad(M)));
double xv = a * (cos(deg2rad(E)) - e);
double yv = a * sqrt(1.0 - e * e) * sin(deg2rad(E));
double v = rad2deg(atan2(yv, xv));
double r = sqrt(xv * xv + yv * yv);

double xeclip = r * (cos(deg2rad(N)) * cos(deg2rad(v + w)) - sin(deg2rad(N)) * sin(deg2rad(v + w)) * cos(deg2rad(i)));
double yeclip = r * (sin(deg2rad(N)) * cos(deg2rad(v + w)) + cos(deg2rad(N)) * sin(deg2rad(v + w)) * cos(deg2rad(i)));
double zeclip = r * (sin(deg2rad(v + w)) * sin(deg2rad(i)));

double lon = rad2deg(atan2(yeclip, xeclip));
double lat = rad2deg(atan2(zeclip, sqrt(xeclip * xeclip + yeclip * yeclip)));

double obl_ecl = 23.4393 - 3.563E-7 * D;
double xeq = cos(deg2rad(lon));
double yeq = cos(deg2rad(obl_ecl)) * sin(deg2rad(lon));
double zeq = sin(deg2rad(obl_ecl)) * sin(deg2rad(lon));

ra = rad2deg(atan2(yeq, xeq));
if (ra < 0) ra += 360.0;
dec = rad2deg(asin(zeq));
}

double moonAltitude(DateTime dt) {
double jd = julianDate(dt.year(), dt.month(), dt.day(),
dt.hour() + dt.minute() / 60.0 + dt.second() / 3600.0);
double ra, dec;
moonPos(jd, ra, dec);

double latRad = deg2rad(latitude);
double decRad = deg2rad(dec);

double lst = (280.46061837 + 360.98564736629 * (jd - 2451545.0) + longitude) / 15.0;
lst = fmod(lst, 24.0);
if (lst < 0) lst += 24.0;

double ha = deg2rad((lst - ra / 15.0) * 15.0);

return rad2deg(asin(sin(latRad) * sin(decRad) + cos(latRad) * cos(decRad) * cos(ha)));
}

void berechneMondaufUntergang(DateTime date) {
}

float berechneMondphasenanteil(int jahr, int monat, int tag, int stunde, int minute) {
if (monat < 3) { jahr--; monat += 12; }
++monat;
long c = 365.25 * jahr;
long e = 30.6 * monat;
double jd = c + e + tag - 694039.09;
jd += (stunde + minute / 60.0) / 24.0;
jd /= 29.5305882;
return jd - floor(jd);
}

void zeichneHalbmond(int x, int y, bool rechtsHell) {
tft.fillCircle(x, y, mondRadius, mondfarbe);
for (int dx = -mondRadius; dx <= mondRadius; dx++) {
for (int dy = -mondRadius; dy <= mondRadius; dy++) {
if (dx * dx + dy * dy <= mondRadius * mondRadius) {
if ((rechtsHell && dx > 0) || (!rechtsHell && dx < 0)) {
tft.drawPixel(x + dx, y + dy, GC9A01A_WHITE);
}
}
}
}
}

void zeichneMondphasenKreis() {
for (int i = 0; i < 8; i++) {
float winkel = i * (2 * PI / 8) - PI / 2;
int x = centerX + cos(winkel) * kreisRadius;
int y = centerY + sin(winkel) * kreisRadius;
switch (i) {
case 0: tft.fillCircle(x, y, mondRadius, mondfarbe); break;
case 1: tft.fillCircle(x, y, mondRadius, GC9A01A_WHITE);
tft.fillCircle(x - 6, y, mondRadius, mondfarbe); break;
case 2: zeichneHalbmond(x, y, true); break;
case 3: tft.fillCircle(x, y, mondRadius, mondfarbe);
tft.fillCircle(x + 6, y, mondRadius, GC9A01A_WHITE); break;
case 4: tft.fillCircle(x, y, mondRadius, GC9A01A_WHITE); break;
case 5: tft.fillCircle(x, y, mondRadius, mondfarbe);
tft.fillCircle(x - 6, y, mondRadius, GC9A01A_WHITE); break;
case 6: zeichneHalbmond(x, y, false); break;
case 7: tft.fillCircle(x, y, mondRadius, GC9A01A_WHITE);
tft.fillCircle(x + 6, y, mondRadius, mondfarbe); break;
}
}
}

void zeichneZeiger(float phase, uint16_t farbe) {
float winkel = phase * 2 * PI - PI / 2;
float cosW = cos(winkel);
float sinW = sin(winkel);
int spitzeX = centerX + cosW * (kreisRadius - 20);
int spitzeY = centerY + sinW * (kreisRadius - 20);
for (int i = -1; i <= 1; i++) {
tft.drawLine(centerX + i * sinW, centerY - i * cosW, spitzeX + i * sinW, spitzeY + i * cosW, farbe);
}
tft.fillTriangle(
spitzeX,
spitzeY,
spitzeX - sinW * 6 - cosW * 3,
spitzeY + cosW * 6 - sinW * 3,
spitzeX + sinW * 6 - cosW * 3,
spitzeY - cosW * 6 - sinW * 3,
farbe
);
tft.fillCircle(centerX, centerY, 4, farbe);
}

void aktualisiereSeite1() {
tft.fillScreen(GC9A01A_BLACK);
zeichneMondphasenKreis();
tft.setTextColor(GC9A01A_GREEN);
tft.setTextSize(2);
tft.setCursor(80, 45);
tft.print("Monduhr");
for (int r = 116; r <= 119; r++) tft.drawCircle(120, 120, r, GC9A01A_GREEN);
DateTime jetzt = rtc.now();
float phase = berechneMondphasenanteil(jetzt.year(), jetzt.month(), jetzt.day(), jetzt.hour(), jetzt.minute());
int phaseIndex = (int)(phase * 8) % 8;
tft.fillRect(15, 80, 210, 20, GC9A01A_BLACK);
tft.setTextColor(GC9A01A_WHITE);
tft.setCursor(15, 80);
tft.print(mondnamen[phaseIndex]);
zeichneZeiger(phase, GC9A01A_RED);
}

void zeichneSeite2Static() {
tft.fillScreen(GC9A01A_BLACK);
DateTime jetzt = rtc.now();
char buf[32];
tft.setTextColor(GC9A01A_WHITE);
tft.setTextSize(2);

sprintf(buf, "%02d:%02d:%02d", jetzt.hour(), jetzt.minute(), jetzt.second());
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
tft.setCursor(120 - w / 2, 40);
tft.print(buf);

sprintf(buf, "%02d.%02d.%04d", jetzt.day(), jetzt.month(), jetzt.year());
tft.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
tft.setCursor(120 - w / 2, 70);
tft.print(buf);

tft.getTextBounds("Auf: " + moonriseTime, 0, 0, &x1, &y1, &w, &h);
tft.setCursor(120 - w / 2, 100);
tft.print("Auf: " + moonriseTime);

tft.getTextBounds("Unter: " + moonsetTime, 0, 0, &x1, &y1, &w, &h);
tft.setCursor(120 - w / 2, 130);
tft.print("Unter: " + moonsetTime);

sprintf(buf, " Licht: %s%%", moonIllumination.c_str());
tft.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
tft.setCursor(120 - w / 2, 160);
tft.print(buf);

for (int r = 116; r <= 119; r++) {
tft.drawCircle(120, 120, r, GC9A01A_RED);
}
}

void aktualisiereSekunden() {
DateTime jetzt = rtc.now();
if (jetzt.second() != letzteSekunde) {
letzteSekunde = jetzt.second();
char buf[16];
sprintf(buf, "%02d:%02d:%02d", jetzt.hour(), jetzt.minute(), jetzt.second());
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
tft.fillRect(120 - w / 2 - 5, 35, w + 10, 20, GC9A01A_BLACK);
tft.setCursor(120 - w / 2, 40);
tft.setTextColor(GC9A01A_WHITE);
tft.setTextSize(2);
tft.print(buf);
}
}

void zeigeStartText(const char* text, uint16_t farbe) {
tft.fillScreen(GC9A01A_BLACK);
tft.setTextColor(farbe);
tft.setTextSize(2);
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
tft.setCursor((240 - w) / 2, 120 - h / 2);
tft.print(text);
}

void holeZeitVonNTP() {
zeigeStartText("WLAN verbinden...", GC9A01A_YELLOW);
WiFi.begin(ssid, password);
unsigned long startAttemptTime = millis();
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 5000) {
delay(100);
}
if (WiFi.status() == WL_CONNECTED) {
zeigeStartText("Zeit holen...", GC9A01A_YELLOW);
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
struct tm timeinfo;
if (getLocalTime(&timeinfo, 5000)) {
rtc.adjust(DateTime(
timeinfo.tm_year + 1900,
timeinfo.tm_mon + 1,
timeinfo.tm_mday,
timeinfo.tm_hour,
timeinfo.tm_min,
timeinfo.tm_sec
));
zeigeStartText("Zeit OK", GC9A01A_GREEN);
delay(1000);
} else {
zeigeStartText("NTP Timeout", GC9A01A_RED);
delay(1000);
}
WiFi.disconnect(true);
} else {
zeigeStartText("WLAN Timeout", GC9A01A_RED);
delay(1000);
}
}

void holeMondDatenVonAPI() {
WiFi.begin(ssid, password);
unsigned long startAttemptTime = millis();
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 5000) {
delay(100);
}
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
http.begin("https://api.ipgeolocation.io/astronomy?apiKey=API_KEY&lat=50.65&long=13.2");
int httpCode = http.GET();
if (httpCode > 0) {
String payload = http.getString();
StaticJsonDocument<1024> doc;
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
moonriseTime = doc["moonrise"].as<String>();
moonsetTime = doc["moonset"].as<String>();
moonIllumination = doc["moon_illumination_percentage"].as<String>();
}
}
http.end();
WiFi.disconnect(true);
}
}

void setup() {
Serial.begin(115200);
tft.begin();
rtc.begin();
pinMode(BUTTON_PIN, INPUT_PULLUP);

holeZeitVonNTP();
holeMondDatenVonAPI();
DateTime jetzt = rtc.now();
lastMoonCalcDate = jetzt;
aktualisiereSeite1();
}

void loop() {
bool buttonState = digitalRead(BUTTON_PIN);
if (buttonState == LOW && lastButtonState == HIGH && (millis() - lastDebounceTime) > debounceDelay) {
showPage1 = !showPage1;
if (showPage1) aktualisiereSeite1(); else zeichneSeite2Static();
lastDebounceTime = millis();
}
lastButtonState = buttonState;

DateTime jetzt = rtc.now();

if (showPage1 && jetzt.hour() != letzteStunde) {
letzteStunde = jetzt.hour();
aktualisiereSeite1();
}

if (!showPage1) {
aktualisiereSekunden();
}
}




Elektronik-Labor  Bastelecke  Projekte  Mikrocontoller