A standalone astronomy display for the LILYGO T-Display S3 Pro (ESP32-S3). Tracks the sun, moon, and light conditions — sunrise, sunset, golden hour, blue hour, twilight phases, moon phase and illumination — across four touch-navigated screens. Data fetched from ipgeolocation.io and cached locally for offline use.

screen1 screen1 screen1 screen1


Screens

# Screen Content
0 Moon Detail Phase image, illumination, rise/set, altitude, azimuth, distance
1 Day / Sun Sunrise, sunset, solar noon, day length, altitude, azimuth, night begin/end
2 Morning Twilight Astronomical, nautical, civil twilight · blue hour · golden hour (AM)
3 Evening Twilight Golden hour · blue hour · civil, nautical, astronomical twilight (PM)

All data is fetched from the ipgeolocation.io Astronomy API every 15 minutes and cached to LittleFS for offline boot.

Every screen shares a common status bar at the bottom:

Position Content
Bottom left Date of last API fetch (2026-05-11)
Bottom centre Navigation dots (active screen highlighted)
Bottom right Time of last API fetch (08:34:11.738)

Hardware

Component Detail
Board LilyGO T-Display S3 Pro
MCU ESP32-S3R8, 240 MHz dual-core LX7
Display 2.33" IPS TFT, 480×222, ST7796, SPI
Touch CST226SE capacitive (I2C)
Light / Proximity LTR-553ALS-01 (I2C, shared bus)
Flash 16 MB (6 MB app + 8 MB LittleFS)
PSRAM 8 MB OPI (Octal)
Battery Built-in 3.8 V 470 mAh + SY6970 PMIC
Connectivity WiFi 802.11 b/g/n

GPIO Pin Map

Function Pin
Display MOSI 17
Display SCLK 18
Display MISO 8
Display CS 39
Display DC 9
Display RST 47
Backlight PWM 48
I2C SDA (touch + sensor) 5
I2C SCL (touch + sensor) 6
Touch RST 13
Touch IRQ 21
Brightness UP button 16 (active LOW)
Brightness DOWN button 12 (active LOW)

Quick Start

1. Clone the repo

git clone https://github.com/stephanef/astro-micro-station.git
cd astro-micro-station

Open the folder in VSCode with the PlatformIO extension installed — dependencies are resolved automatically on first build.

2. Configure secrets

Copy include/secrets_template.h to include/secrets.h and fill in your credentials:

// Required
#define WIFI_SSID       "your-network"
#define WIFI_PASSWORD   "your-password"
#define ASTRO_API_KEY   "your-key"        // free at ipgeolocation.io

// Required — your coordinates
#define OBSERVER_LAT    43.7001f          // decimal degrees, positive = North
#define OBSERVER_LON    -79.4163f         // decimal degrees, negative = West
#define OBSERVER_TZ     "America/Toronto"
#define UTC_OFFSET_SEC  -18000            // EDT = -14400  |  EST = -18000

// Optional — only needed if MQTT_ENABLED is defined in config.h
#define MQTT_BROKER     "192.168.1.x"     // your Mosquitto broker IP
#define MQTT_USER       ""                // leave empty if broker has no auth
#define MQTT_PASS       ""

Get a free API key at ipgeolocation.io/signup. Free tier: 1000 req/day — at 15-minute refresh this app uses ~96 req/day.

To disable Home Assistant integration entirely, comment out #define MQTT_ENABLED in config.h — the MQTT credentials in secrets.h are then ignored.

secrets.h is gitignored and never committed. config.h (non-sensitive settings) is safe to commit as-is or copy from config.example.h.

3. Build and flash

pio run --target upload
pio device monitor        # 115200 baud

If the board does not auto-enter flash mode:

  1. Hold BOOT
  2. Press RESET
  3. Release BOOT
  4. Run pio run --target upload

4. First boot — LittleFS

On the very first boot the LittleFS partition is blank and will be formatted automatically. You will see [LittleFS] OK in the monitor. From that point the last successful API response is cached at /astro_cache.json and loaded on any boot where WiFi is unavailable.


Input Action
Tap anywhere Next screen (cycles 0 → 1 → 2 → 3 → 0)
Long press (> 0.8 s) Toggle red night-vision mode
Wave hand at top sensor Wake display from dim
Brightness UP button (IO16) Increase backlight, hold to repeat
Brightness DOWN button (IO12) Decrease backlight, hold to repeat

Backlight dims automatically after DIM_TIMEOUT_SEC seconds of inactivity (default 30 s). Brightness level is preserved across dim/wake cycles.


Night-Vision (Red Mode)

Long-press anywhere to toggle. In red mode:

  • Background, cards, and text shift to deep red tones
  • Moon phase image is re-rendered as red-luminance (shading preserved)
  • Backlight target drops to BACKLIGHT_RED_MODE (default 60/255)

Home Assistant Integration (optional)

Enable by defining MQTT_ENABLED in config.h (already set by default). Set MQTT_TOPIC_STATUS to your preferred base topic (default: astronomy).

Auto-discovery — no YAML required

The device uses MQTT Discovery. On every MQTT connection it publishes 19 sensor config messages to homeassistant/sensor/astro_<name>/config. Home Assistant picks these up automatically and creates a single Astro Micro Station device with all sensors grouped under it — no configuration.yaml editing needed, exactly like other auto-discovered devices.

Sensors registered automatically:

Sensor Source topic
Moon Phase, Moon Illumination, Moonrise, Moonset, Moon Altitude, Moon Azimuth, Moon Distance astronomy/moon
Sunrise, Sunset, Solar Noon, Day Length, Sun Altitude, Sun Azimuth astronomy/sun
Golden Hour AM, Blue Hour AM, Civil Twilight AM astronomy/morning
Golden Hour PM, Blue Hour PM, Civil Twilight PM astronomy/evening

MQTT topics

After every successful API fetch (every 15 minutes) the device publishes 4 retained JSON topics:

Topic Content
astronomy online / offline (LWT — broker publishes offline automatically if device disconnects)
astronomy/sun date, time, sunrise, sunset, solar noon, day length, altitude, azimuth, distance, night begin/end, midnight
astronomy/moon phase, phase name, illumination %, rise, set, altitude, azimuth, angle, distance
astronomy/morning astronomical, nautical, civil twilight · blue hour · golden hour times
astronomy/evening golden hour · blue hour · civil, nautical, astronomical twilight times

Troubleshooting — device not appearing in Home Assistant

  1. Go to Settings → Devices & Services → MQTT → Configure, subscribe to homeassistant/sensor/astro_sunrise/config and click START LISTENING. You should see a JSON config payload immediately (it is retained).
  2. If nothing appears, check the serial monitor for [MQTT] Discovery: lines at boot.
  3. If discovery messages are present but no device appears in HA, restart the HA MQTT integration: Settings → Devices & Services → MQTT → Reload.
  4. Verify broker IP and credentials match MQTT_BROKER / MQTT_USER in config.h.

To disable MQTT entirely, comment out #define MQTT_ENABLED in config.h.


Serial Monitor Output

Key log prefixes for diagnostics:

Prefix Meaning
[Boot] Startup sequence
[WiFi] Connection status and RSSI
[NTP] Time sync
[LittleFS] Filesystem mount result
[AstroAPI] HTTP fetch, parse result, cache save/load
[fetchAstro] Heap + PSRAM snapshot after each fetch
[Display] Init, sprite buffer, heap + PSRAM at boot
[Mem] Hourly heap + PSRAM trend (watch min for leaks)
[Nav] Screen change events
[MQTT] → Data topic published (sun / moon / morning / evening)
[MQTT] Discovery: Auto-discovery config published for one sensor

Example memory line:

[Mem]  heap=186432  min=164208  maxBlock=163840  psram=7823104

The min value is the all-time low — a steadily decreasing min indicates a heap leak.


API Data — Refresh Behaviour

All fields are fetched from the API and replaced on every 15-minute refresh — nothing is cached selectively. The distinction below is purely about how fast the API values move, not about how often the device fetches them.

Stable throughout the day (API returns the same value all 24 h)

Field Notes
Sunrise / Sunset Set once at midnight for the date
Solar noon Set once for the date
Day length Set once for the date
Night begin / end Set once for the date
Astronomical / Nautical / Civil twilight All begin/end times set for the date
Blue hour begin / end (AM & PM) Set for the date
Golden hour begin / end (AM & PM) Set for the date
Moonrise / Moonset Set for the date
Moon phase Changes once every ~3.7 days
Moon illumination Changes by < 1 % per hour

Continuously changing (visibly different each 15-minute fetch)

Field Rate of change
Sun altitude ~15° per hour near transit
Sun azimuth ~15° per hour
Moon altitude ~0.5° per hour
Moon azimuth ~0.5° per hour
Moon distance Tens of km per hour (perigee/apogee drift)
Fetch date / time (fetchDate / currentTime) Timestamp of the API call — updates every fetch

Adjusting for Your Location

In include/secrets.h:

#define OBSERVER_LAT    43.7001f          // decimal degrees, positive = North
#define OBSERVER_LON    -79.4163f         // decimal degrees, negative = West
#define OBSERVER_TZ     "America/Toronto"
#define UTC_OFFSET_SEC  -18000            // EDT = -14400  |  EST = -18000

File Structure

astro-micro-station/
├── platformio.ini              # Build config, libraries, pin definitions
├── partitions_custom.csv       # 6 MB app / 8 MB LittleFS
├── include/
│   ├── secrets.h               # ⚠ gitignored — WiFi, API key, MQTT creds, coordinates
│   ├── secrets_template.h      # Safe template — copy to secrets.h and fill in
│   ├── config.h                # Non-sensitive settings (safe to commit)
│   ├── config.example.h        # Template for config.h
│   ├── AstroAPI.h              # ipgeolocation.io data structures + class
│   ├── DisplayManager.h        # Colour palette, screen class declaration
│   ├── MQTTManager.h           # HA status publisher (stub when disabled)
│   ├── icons.h                 # ICON_ASTRO — 22×22 RGB565 PROGMEM
│   └── moon_icons.h            # 8 moon phase images — 155×155 RGB565 PROGMEM
├── src/
│   ├── main.cpp                # Setup, loop, WiFi, NTP, touch FSM, brightness
│   ├── AstroAPI.cpp            # HTTP fetch, JSON parse, LittleFS cache
│   ├── DisplayManager.cpp      # All screen rendering
│   └── MQTTManager.cpp         # MQTT connection + discovery + data publish
└── assets/
    ├── moon-phases/            # Source SVGs for moon phase images (8 files)
    ├── icons/                  # Source PNG for header icon
    └── tools/
        └── svg_to_h.py         # Converts SVGs → moon_icons.h (run in venv)

Regenerating Moon Phase Images

The moon phase header file is generated from SVG sources using a Python script:

cd assets/tools
python3 -m venv venv && source venv/bin/activate
pip install cairosvg pillow
python3 svg_to_h.py
# output written to include/moon_icons.h

Source files are in assets/moon-phases/. The script renders each SVG to 155×155 greyscale RGB565, alpha-composited on black, with 0xFFFF as the chroma key for transparency.