¿Alguna vez intentaste fotografiar un avión cruzando justo por delante de la Luna? Es una de esas tomas que quedan espectaculares... y que casi siempre se pierden por segundos. Este proyecto le quita la suerte a la ecuación: un pequeño dispositivo con pantalla táctil que sigue en vivo los aviones que vuelan sobre tu zona, calcula dónde está la Luna en el cielo y te avisa con anticipación cuándo uno va a pasar cerca. o directamente por encima. del disco lunar.
Al final de este tutorial vas a entender cómo se combinan datos de vuelos en tiempo real (la red abierta ADS-B) con un cálculo de la posición de la Luna hecho dentro del mismo microcontrolador, sin API ni llaves. Y, sobre todo, vas a saber portar la idea original de Adafruit. pensada para su placa Qualia ESP32-S3 con pantalla redonda. a un ESP32 genérico y una pantalla TFT común de las que consigues en Chile.
Qué hace, en concreto
El dispositivo consulta la red abierta adsb.fi para traer los aviones dentro de un radio de 15 millas náuticas. Cada avión llega con su latitud, longitud y altitud, y el firmware las convierte a coordenadas de azimut y elevación. las mismas que usa para la Luna. . Después "vuela" cada avión hacia adelante en software, a pasos de 5 segundos durante los próximos 5 minutos, y registra la menor separación angular respecto a la Luna.
Cuando el acercamiento más cercano cae dentro de los umbrales de alerta y falta menos de 90 segundos, la pantalla levanta un banner: cian para un paso cercano, rosado para un tránsito sobre el disco. Son dos niveles de aviso:
- NEAR (cercano): el avión y la Luna van a compartir el encuadre de un teleobjetivo.
- TRANSIT (tránsito): el avión cruza literalmente por delante del disco lunar.

Las cuatro pantallas
En vez de seguir un orden lineal paso a paso, conviene entender el proyecto por bloques. La interfaz se organiza en cuatro pantallas táctiles:
- RADAR: vista en planta del tráfico a tu alrededor, con el rumbo de la Luna marcado, los banners de alerta y el indicativo y la altitud de cada avión.
- LIST: todos los aviones seguidos, ordenados por qué tan cerca van a pasar de la Luna, con la separación prevista y el tiempo hasta el acercamiento máximo. Tocas cualquier fila y ves los detalles del vuelo.
- MOON: la fase actual dibujada a escala, el porcentaje de iluminación, la altitud y el azimut, y las horas de salida y puesta de la Luna en tu zona horaria.
- LOCATION: un teclado táctil para tu ubicación y la rotación de pantalla. Ambos se guardan en memoria no volátil, así que los configuras una sola vez.


Hardware: del combo Adafruit al ESP32 de acá
El proyecto original corre sobre una placa Adafruit Qualia ESP32-S3 manejando una pantalla redonda de 2,1" 480×480 con touch capacitivo. El chip ESP32-S3 es clave: sus 16 MB de flash y 8 MB de PSRAM alcanzan para mantener un framebuffer a todo color, correr la matemática de trayectorias y hablar con la red al mismo tiempo. Sin placas extra, sin módulo GPS y sin sensores. El único cableado es un cable plano (ribbon) hacia la pantalla y un USB-C para alimentar.
Por qué importa la PSRAM (algo que el original no detalla): un framebuffer de 480×480 píxeles a 16 bits ocupa unos 450 KB. Eso no cabe en la RAM interna del ESP32, y por eso se necesita PSRAM externa. Si portas el proyecto a un ESP32 sin PSRAM, vas a tener que bajar la resolución o usar una pantalla SPI más chica que se refresque por regiones.
Un detalle fino de ingeniería: el firmware baja a propósito el reloj de píxel (DISPLAY_FREQ) desde los 16 MHz recomendados hasta 8 MHz. La pantalla RGB paralela se escanea continuamente desde la PSRAM, y las ráfagas de WiFi/SSL compiten por ese ancho de banda, lo que provoca un "corrimiento" horizontal de la imagen (screen drift). Bajar el pixel clock. y también limitar la potencia de transmisión WiFi a 8 dBm. deja margen y elimina el temblor.
Los parámetros que vas a querer tocar
Como el código está escrito en CircuitPython, cada valor ajustable es una constante simple al inicio de code.py. Puedes editarlas por USB sin instalar ninguna cadena de compilación:
CX, CY = 240, 240
PX_PER_MI = 42 # 5 mi -> 210 px ring
FETCH_S = 15 # aircraft refresh
MOON_S = 60 # ephemeris refresh
LOOKAHEAD_S = 300 # transit prediction window
SEP_HIT = 0.5 # pink: plane crosses right over the moon disc
SEP_NEAR = 2.0 # cyan: close pass — moon still in a 300mm frame
NM_RADIUS = 15 # adsb.fi query radius (nm)
DEBUG_TOUCH = False # print every tap/release to serial — enable when tuning
DISPLAY_FREQ = 8_000_000 # pclk; guide default is 16 MHz — lowered to fight
# ESP32-S3 RGB "screen drift" during WiFi bursts
Baja SEP_HIT si quieres alertas solo cuando el avión realmente tape la Luna; sube NM_RADIUS si vives cerca de un aeropuerto con mucho tráfico. LOOKAHEAD_S define cuántos segundos hacia el futuro se proyecta cada avión.
El corazón del cálculo
Dos funciones geométricas hacen el trabajo pesado. az_el convierte la posición de un avión (en millas al este/norte y su altitud en pies) a azimut y elevación. ang_sep calcula la separación angular entre dos puntos del cielo con la fórmula del coseno de la distancia esférica. Y predict proyecta cada avión hacia adelante buscando su acercamiento mínimo a la Luna:
def az_el(x_mi, y_mi, alt_ft):
gd = math.sqrt(x_mi * x_mi + y_mi * y_mi) + 1e-6
el = math.degrees(math.atan2(alt_ft / 5280.0, gd))
az = math.degrees(math.atan2(x_mi, y_mi)) % 360
return az, el
def ang_sep(az1, el1, az2, el2):
a1, e1, a2, e2 = map(math.radians, (az1, el1, az2, el2))
c = (math.sin(e1) * math.sin(e2)
+ math.cos(e1) * math.cos(e2) * math.cos(a1 - a2))
return math.degrees(math.acos(max(-1, min(1, c))))
def predict(ac, m_az, m_el):
"""Return (eta_s, min_sep_deg). Always evaluates the CURRENT position
(t=0), so aircraft with missing/low ground speed — common for GA
transponders on adsb.fi — still register their live separation instead
of being silently excluded from the alert logic."""
az, el = az_el(ac["x"], ac["y"], ac["alt"])
best_t, best_sep = 0, ang_sep(az, el, m_az, m_el)
if ac["gs"] >= 40:
v = ac["gs"] * 1.15078 / 3600.0 # mi/s
tr = math.radians(ac["track"])
vx, vy = v * math.sin(tr), v * math.cos(tr)
for t in range(5, LOOKAHEAD_S + 1, 5):
az, el = az_el(ac["x"] + vx * t, ac["y"] + vy * t, ac["alt"])
s = ang_sep(az, el, m_az, m_el)
if s < best_sep:
best_sep, best_t = s, t
return best_t, best_sep
Un detalle bien pensado: predict siempre evalúa primero la posición actual (t=0). Así, los aviones con velocidad baja o ausente. común en avionetas con transponders GA en adsb.fi. igual registran su separación real, en vez de quedar excluidos de la lógica de alerta.
Trayendo los aviones
La consulta a adsb.fi es directa. Fíjate en el timeout corto de 4 segundos: como esta llamada bloquea todo el bucle (incluido el touch), es preferible saltarse un ciclo de 15 s antes que congelar la pantalla 10 s esperando una respuesta lenta.
def fetch_aircraft():
"""adsb.fi open data — planes within NM_RADIUS of observer."""
out = []
url = "https://opendata.adsb.fi/api/v2/lat/{:.4f}/lon/{:.4f}/dist/{}".format(
LAT, LON, NM_RADIUS)
try:
# short timeout: this call blocks the whole loop (touch included) —
# better to skip one 15s cycle than freeze for 10s on a slow response
r = requests.get(url, timeout=4)
data = r.json()
r.close()
except Exception as e:
print("adsb fetch failed:", e)
return None
for ac in data.get("aircraft", []):
try:
la, lo = ac["lat"], ac["lon"]
alt = ac.get("alt_baro", 0)
if alt == "ground" or not isinstance(alt, (int, float)) or alt < 500:
continue
dx = (lo - LON) * 69.17 * math.cos(math.radians(LAT)) # mi east
dy = (la - LAT) * 69.17 # mi north
out.append({
"cs": (ac.get("flight") or ac.get("hex", "?")).strip(),
"x": dx, "y": dy, "alt": alt,
"gs": ac.get("gs", 0) or 0,
"track": ac.get("track", 0) or 0,
"type": ac.get("t", "?") or "?",
})
except (KeyError, TypeError):
continue
return out

Puesta en marcha
- Flashea la última versión de CircuitPython 9.x en tu placa.
- Copia las librerías del bundle de Adafruit a la carpeta
lib(el proyecto usaadafruit_requests,adafruit_ntp,adafruit_display_text,adafruit_display_shapesyadafruit_cst8xxpara el touch, más los módulostl021wvc02ymoon_ephempropios del proyecto). - Copia
code.pyy los assets a la unidadCIRCUITPY, respetando la estructura de carpetas. - Configura tu red WiFi en las variables de entorno y tu ubicación desde la pantalla LOCATION.
Variantes y mejoras
- Adáptalo a Chile de verdad: el código original geocodifica por código ZIP de EE.UU. (
api.zippopotam.us/us/...). Para usarlo acá, reemplaza esa geocodificación por coordenadas fijas de tu ciudad. por ejemplo, Santiago: latitud, 33,45, longitud, 70,66. directamente en las variablesLATyLON, o cambia el endpoint por uno que soporte direcciones chilenas. - Guarda los tránsitos en microSD: agrega un módulo microSD por SPI y registra cada evento NEAR/TRANSIT con su timestamp. Después puedes revisar qué noches tuvieron más pasos y planificar tus sesiones fotográficas.
- Dispara la cámara sola: conecta un optoacoplador al cable disparador de tu cámara y, cuando
predictdetecte un TRANSIT a menos de X segundos, activa el obturador. Conviertes el detector en un intervalómetro inteligente. - Suma un aviso sonoro: un buzzer te permite mirar por el visor sin despegar la vista de la pantalla.
Personalización para Chile
El proyecto original depende de dos piezas específicas de Adafruit que no se venden en Chile: la placa Qualia ESP32-S3 y la pantalla redonda RGB666 de 2,1". La buena noticia es que la idea se porta bien a componentes que sí tienes en stock local en MechatronicStore:
- Seeed Studio XIAO ESP32-S3 (SKU GS1-3). $13.670 CLP. Mismo chip ESP32-S3 con PSRAM, en formato compacto. Es el reemplazo más fiel de la placa Qualia para correr CircuitPython y sostener el framebuffer más la matemática de trayectorias.
- Placa TZT ESP32 WROOM con pantalla táctil LCD 240×320 LVGL (SKU A-301). $21.990 CLP. Si prefieres una solución todo en-uno, esta placa integra ESP32 + pantalla táctil. La pantalla es rectangular (no redonda) y de menor resolución, así que hay que adaptar el dibujado, pero te ahorra el cableado del cable plano.
- Cable USB Tipo C a USB Tipo A 1 m (SKU B-101). $2.190 CLP. Para alimentar y programar la placa. Asegúrate de que sea de datos, no solo de carga.
Piezas del proyecto original que no necesitas comprar acá (o que consigues en una tienda de fotografía): el tornillo adaptador 3/8" a 1/4", la rótula mini y el soporte cold shoe para montar el aparato sobre la cámara, además de la carcasa impresa en 3D (los STL están en la guía original).
Si el tutorial original usa la placa Qualia ESP32-S3, el XIAO ESP32-S3 cumple la misma función. mismo silicio. a un costo local mucho menor.
Recursos
- Tutorial original: Moon Flight Tracker (Adafruit Learning System)
- CircuitPython para tu placa: https://circuitpython.org/downloads
- Bundle de librerías CircuitPython de Adafruit: https://circuitpython.org/libraries
- Red de datos de vuelos usada: adsb.fi (API abierta, sin llave)
Inspirado en el proyecto de los Ruiz Brothers para Adafruit. Versión chilena con componentes en stock local en MechatronicStore.









