Moon Phase Display Based on Observer's Location on Earth

by pgeschwi in Circuits > Arduino

30 Views, 1 Favorites, 0 Comments

Moon Phase Display Based on Observer's Location on Earth

20250418_144157.jpg

I'm a hobby stargazer and in some discussions with friends, I found that some did not notice the moon changing its angle of the illuminated limb throughout its path. Since Minnesota is not a friendly state for us astronomy fans, I thought about visualizing moon phases as they appear in the sky, adjusting for the observer's position on Earth. Usually, moon phases are shown universally, progressing horizontal, with the axis of the moon always north/south. So, here is one which corrects this deficiency:

Features

  1. Displays 40 moon phase images (at 9-degree increments) with realistic progression over time.
  2. Takes the observer’s location into account to tilt the illuminated limb correctly.
  3. Uses formulas from Jean Meeus' Astronomical Algorithms (1998).
  4. Just for the giggles, I included Black/Blue Moon date calculations—added for fun since some of the necessary data was already present for moon phase calculations.

The Sketch is written for ESP32. A traditional Arduino does not have the storage/RAM and also lacks the required precision (double float). The ESP32 does not have a hardware floating point unit capable calculating double float either, but the math library will emulate double precision. This of course is at the expense of speed, but we're not in a hurry and the calculation is done only once a minute.

When calculating moon phases, using the long term average of 29.53 days per moon cycle, single float precision is OK, but this would prevent us from getting reliable results for the blue moon date/time calculation.

Astronomically irrelevant, but a fun fact to know when the next black/blue moon appears:

Black Moon = The second new moon in the same month

Blue Moon = The second full moon in the same month

Seasonal Blue Moon = The third full moon in a season with four full moons. A season is the time (quarter) between equinox and solstice and vice versa.

BTW, blue moons appear about every 18 months to 2.5 years, but there are exceptions. In 2018, January and March had a blue moon the same year.

The code is modular and can be dissected/reduced to the level you'd like to implement. Most of the routines can be used outside this project as long the right parameters are supplied. Whoever worked with Arduino/ESP32 should be able to pull parts out and use for their own purpose. Some routines I got from open sources in the astronomy forums and adjusted for my purposes. Special credits go to

  1. Greg Miller - www.celestialprogramming.com
  2. Giuseppe Cardillo - www.mathworks.com/matlabcentral/fileexchange/17977-equinoxes-and-solstices
  3. Dan Cross - the most comprehensive library of astronomical open source libraries I know, also available for multiple programming languages. github.com/cosinekitty/astronomy

Supplies

ESP32.png
Display.png
SD-Card.png
RotaryEncoder.png
Original Battery holder.png
20250418_140050.jpg

The parts list is pretty short. We'd need a display (I used two which I had in the junk drawer), use any of the commonly used ones and adjust the driver reference in the TFT_eSPI library if needed for a different type. More below on how this is done. I used 2x ILI9341 with a resolution of 240x320, which gives us plenty room. I used one for the text info and the other for graphics. If you like to use a single, bigger display, your choice may be one with a resolution of 480x320, saving you the hassle driving two individual displays. A matching link is in the parts list below.

If the display (like the one I used) has a SD-Card reader, you don't need one extra, otherwise a SD card adapter or micro SD-Card reader is needed. The SD-Card will carry the 40 PNG-Images of the moon phases. These are traditional moon phase images, north/south oriented which will be rotated to show the actual illuminated limb.

To adjust settings we'll use a rotary encoder with push-button.

Finally we'd need a RTC (Real-Time-Clock) to keep time while the system is switched off. Clock precision is not of utmost importance, there is no big change if the clock is a couple of minutes off. The RTC is running on UTC, all calculations are based on UTC anyways. The time zone settings are affecting displayed dates/times only.

Parts list:

  1. ESP32 Dev-Board or similar (minimum is a version with 30 or more pins):
  2. https://www.amazon.com/Development-AYWHP-ESP-WROOM-32-Bluetooth-Compatible/dp/B0DG8JFY3C/ref=sr_1_10
  3. Display: (it looks like that non-touch-sensitive versions are hard to get nowadays. I guess one can take the challenge, drop the encoder and make setting changes via the touch screen):
  4. https://www.amazon.com/XIITIA-Display-ILI9341-240x320-Screen/dp/B0CJHPRFGF/ref=sr_1_11
  5. https://www.amazon.com/DEVMO-ILI9341-Display-240x320-Raspberry/dp/B07WR48MVD/ref=sr_1_3
  6. https://www.amazon.com/Hosyond-Display-Compatible-Mega2560-Development/dp/B0BWJHK4M6/ref=sr_1_7_sspa (4in + 3.5in are using the higher resolution)
  7. RTC (this is the one I used, but this has a design flaw. It is intended to use a LIR2032 which is continuously being recharged. The problem is that the cell is constantly overcharged and killed after a couple of months. I replaced the battery holder by a 1.5F super-cap which is the better solution, picture above):
  8. https://www.amazon.com/AITRIP-Precision-AT24C32-Arduino-Raspberry/dp/B09KPC8JZQ/ref=sr_1_1
  9. Encoder:
  10. https://www.amazon.com/Cylewet-Encoder-Digital-Potentiometer-Arduino/dp/B07DM2YMT4/ref=sr_1_1_pp
  11. SD-Card Reader (if needed):
  12. https://www.amazon.com/HiLetgo-Adater-Interface-Conversion-Arduino/dp/B07BJ2P6X6/ref=sr_1_1

Wiring

Fritzing.png

Wiring is straight forward as shown above, detailed pin assignments are also shown in the sketch. Driving two displays using the same library can be done by using different CS (Chip Select) pins independent from the library. The library in use is 'TFT_eSPI' which is an universal approach for many different display modules, but it's getting finicky if you want to drive two different types of displays.

The two displays in use are wired in parallel, only the CS pins are separated. In the sketch, I select the correct display by writing a '0' (CS is low active) to the matching pin.Using a larger screen avoids these added hoops, but you would also need to adjust the position references for graphics shown on the secondary display.

It should only take a couple of minutes to put everything together on a breadboard. I created a little case for those who are interested.

Libraries and Settings, Code Upload

Before we can get the code uploaded, we need the required libraries installed. All required libraries are easily installable via the search window in the Arduino IDE. Those who never worked with ESP32 before, need the ESP board + package as well:

  1. Boards manager:
  2. search for 'esp32' and install 'esp32 by Espressif Systems'
  3. Library manager
  4. search for 'tft_espi' and install 'TFT_eSPI by Bodmer'
  5. search for 'pngdec' and install 'PNGdec by Larry Bank'
  6. search for 'rtclib' and install 'RTClib by Adafruit'
  7. search for 'esp32rotary' and install 'ESP32RotaryEncoder by Matthew Clark'
  8. SPI, SD and Wire library should be already installed

The only Library requiring some special attention is TFT_eSPI. Unlike most others, the settings for pins and hw-chipsets are done in 'User_Setup.h' inside the library folder. The two sections which may need changing are the chipset and the corresponding pins. The first part is the driver section, beginning around line 44. Only one type should be active (uncommented) and should look like this:

// Only define one driver, the other ones must be commented out
#define ILI9341_DRIVER // Generic driver for common displays
//#define ILI9341_2_DRIVER // Alternative ILI9341 driver, see https://github.com/Bodmer/TFT_eSPI/issues/1172
//#define ST7735_DRIVER // Define additional parameters below for this display
//#define ILI9163_DRIVER // Define additional parameters below for this display
//#define S6D02A1_DRIVER
//#define RPI_ILI9486_DRIVER // 20MHz maximum SPI
//#define HX8357D_DRIVER
//#define ILI9481_DRIVER
//#define ILI9486_DRIVER
//#define ILI9488_DRIVER // WARNING: Do not connect ILI9488 display SDO to MISO if other devices share the SPI bus (TFT SDO does NOT tristate when CS is high)
//#define ST7789_DRIVER // Full configuration option, define additional parameters below for this display
//#define ST7789_2_DRIVER // Minimal configuration option, define additional parameters below for this display
//#define R61581_DRIVER
//#define RM68140_DRIVER
//#define ST7796_DRIVER
//#define SSD1351_DRIVER
//#define SSD1963_480_DRIVER
//#define SSD1963_800_DRIVER
//#define SSD1963_800ALT_DRIVER
//#define ILI9225_DRIVER
//#define GC9A01_DRIVER

The second section defining the pins starts around line 209 for the ESP32 world. There are plenty other MCUs listed. This makes the library very flexible but admittingly somewhat confusing to setup, making sure you are manipulating the right category..

Below are the pins I used.

#define TFT_MISO 19
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS 35 // Chip select control pin, put on 35 for multi display setups, managing CS separately
#define TFT_DC 2 // Data Command control pin
#define TFT_RST 4 // Reset pin (could connect to RST pin)
//#define TFT_RST -1 // Set TFT_RST to -1 if display RESET is connected to ESP32 board RST

Everybody who has worked with ESP32 will probably stumble over the CS-Definition. This is set to pin 35, which is an input-only pin and would be unable to drive the display. In this particular case I am driving two displays and use a single instance to drive both. Selecting the correct display is done in the actual code by manipulating the CS-pins accordingly.

The two pins in use to drive the displays in my sketch are:

#define DispText_CS 5 // manual CS switch outside Library, defined CS Pin in library as 35 to take manual control (User_Setup.h)
#define DispMoon_CS 16 // display for moon phase and relative position of Sun,Moon,Earth

This is all we need to prepare for compiling the sketch and upload. One display should show text information and the other the moon and below the relative position of sun, moon and earth.

All required files, sketch and SD-Card images are stored in my Github repository at:

https://github.com/pgeschwi/Moon-view-based-on-observer-location

If you get an error while compiling the sketch about a missing "bb_spi_lcd", just use the library manager to install this library with the same name as well, no need to change anything in the sketch.

Operation and Settings

World Coordinate System.png

If the RTC has not been in use with this sketch, initial default values will be written to NVRAM. The three items stored in NVRAM are

  1. Time zone
  2. Observer's Latitude
  3. Observer's Longitude

The initial time zone setting is US-Central Time, and the location of Minneapolis. Lat/Lon is stored as full degrees, there is no need for higher precision when it comes to viewing angles or such. If the RTC derived time happens to be outside the year 2000-2099, it will reset to 1/1/2000. This allows us to use two digits for displaying the year.

When you google for your city's location, Lat/Lon coordinates are usually expressed using degrees with an orientation identifier (N/S/W/E). To make adjustments easier, I use +/- sign according the above chart. For S and W use negative, for N and E use positive values and round to the next full degree.

E.g. a Google search for "lat lon Minneapolis" results in 44.9778° N, 93.2650° W. Adjust settings to "Lat 45" and "Lon -93".

To change settings, press the encoder button until a box appears at the current time display for the month, adjust by turning the encoder. Use a short press to advance to the next setting. Every change in settings triggers a full calculation so you can observe how Alt/Az values and the position of sun and moon are changing while making adjustments.

I added time zones for the US and the UK/European main land as well as plain UTC. I did not include a setting for the date format, this is always MM/DD/YY. Maybe adding this in a later version. Not sure if a time format matching the timezone or simply a universal format would work better.

If you like to add your own timezone, change the TZdata array. You can remove the ones you don't need or add as many you want to be listed. The array is dynamic and does not need any changes to references. The format of one entry is as follows:

{ "Central", "CST6CDT,M3.2.0/2:00:00,M11.1.0/2:00:00", "CST", "CDT" },

The parameters from left to right:

  1. Display name: "Central" (just a text)
  2. String for switching SDT/DST, POSIX format (see below).
  3. Display text for standard time
  4. Display text for daylight saving time / summer time

The POSIX time format in short form is like this: Mm.n.d/time

  1. Mm: The month (1-12).
  2. n: The week number (1-5) (use 5 for last week).
  3. d: The day of the week (0 for Sunday, 6 for Saturday).
  4. time: The time in 24-hour format (e.g., 2:00:00, can be abbreviated).

I do hope that at some point this pesky DST switching will be obsolete, but I guess, there won't be an easy consensus... oh well. If this may still happen, you can still keep the time zones, simply remove the switch dates and define just the zone.

Sample Sequence of Images

20250418_144219.jpg
20250418_144234.jpg
20250418_144304.jpg
20250418_144318.jpg
20250418_144329.jpg
20250418_144349.jpg
20250418_144405.jpg
20250418_144421.jpg
20250418_144437.jpg
20250418_144456.jpg
20250418_144510.jpg
20250418_144522.jpg
20250418_145858.jpg

The images sequence starts at 11:39 Central time, briefly before the 1st Quarter moon at 21:20 (top of the list of next moon events). The bright limb is at 44°, counted CW from the top.

The upper part also showing the upcoming black/blue moon dates:

  1. Blk - black moon
  2. Bl-M - monthly blue moon
  3. Bl-S - seasonal blue moon

The position display below the moon does only show the Azimuth, not taking Altitude into account. At that time, the moon was just above the horizon (Alt 6°, probably not visible unless you'd be at a shore line) and rising ENE (Az 56°, East would be 90°, counting from north, CW). For folks on the northern hemisphere (Lat >= 0), the ellipsis is showing south on top. We are used to look south for sun and moon on their path.For folks on the southern half on this ball (Lat < 0), the ellipse is rotated 180° as they are looking north.

The screenshots are in one hour steps during the day and following night. At 14:40 the moon image changes to 1st Quarter. The illuminated limb angle has reduced to 32° and will start increasing again during the next hours. At 19:41, while the sun is setting, the limb's angle has increased to 93°. In the last image, short before midnight, the limb's angle is at 135° and will reduce until next day about 11am again. As everybody knows, this will shift over the moon's cycle of about 29.53 days.

During the sun or moon are below the horizon (Alt < 0°), the display will still show the moon, purposefully ignoring that the Earth is getting in the line of sight.

Of course there are times in which the limb's angle is somewhat irrelevant, new and full moon are special cases. The angle of the illuminated limb will still be calculated but will change very rapidly due to the small angles in the constellation of sun, moon and earth, almost being in line. If we want to be very precise (I mean actually hairsplitting nerdy) everybody knows that a real full moon can only be observed during a moon eclipse, when sun, earth and moon are in one line. Same is true for an exact new moon, which happens only during a solar eclipse.

One other thing needs to be noted when you explore various locations on earth. Settings allow you to go over Latitude 90 (north-pole) which will switch you to -90 (south-pole) causing a 180° flip. The same is true for Longitude above 180° rolling over to -180, but this does not cause a flip, because this is just going around the globe in the same direction.

Final Build

20250420_225316.jpg

As mentioned above, I made a simple box to carry the two displays. Since this is a work in progress and I am not sure where this goes, I just needed a holder, easier to handle than a flying breadboard build.