- Diseño y desarrollo de un entorno de fotogrametría con drones asistido por IA
- Aplicación Android para fotogrametría con IA. Verificación del entorno y Fase 1 de desarrollo inicial
- Aplicación Android para fotogrametría con IA. Fase 2: Primera versión capaz de volar
- Aplicación Android para fotogrametría con IA. Fase 3: Corrección de heading, visualización en vuelo y ajustes de UX
- Aplicación Android para fotogrametría con IA. La plataforma de procesado: OpenDroneMap desplegado con Claude Code
- Aplicación Android para fotogrametría con IA. Fase 4: Integración cloud, exportación KMZ y cierre del ciclo completo
- Fotogrametría con IA: Detección de hallazgos en tiempo real durante el vuelo. Módulo de auditoría IA con YOLOv8 y RTMP sobre el stack ODM
- Aplicación Android para fotogrametría con IA. Fase 5: Streaming RTMP y telemetría en tiempo real — cómo se coordina un sistema entre dos agentes de IA
- Aplicación Android para fotogrametría con IA. Fase 6: Resultados en el mapa, gestión multi-batería y reanudación de misiones
- Aplicación Android para fotogrametría con IA. Fase 7: Edición de polígono, estimación de batería corregida y caché offline
- Aplicación Android para fotogrametría con IA. Fase 8: Condiciones de vuelo — meteorología AEMET, índice Kp y GPS en tiempo real
Hay una pregunta que cualquier piloto de drones se hace antes de salir al campo: ¿se puede volar hoy? Para responderla, hasta ahora había que abrir la app de AEMET, mirar el viento, consultar otra fuente para el índice geomagnético, ver en la pantalla del teléfono cuántos satélites GPS tienen fix y cruzar mentalmente todos esos datos para tomar la decisión. La Fase 8 integra esa información directamente en la app, en un panel accesible desde el mapa sin interrumpir ninguno de los flujos existentes. El resultado es concreto: abrir el panel, ver los semáforos, saber si se puede volar y cuándo es el mejor momento en las próximas 48 horas.

Tres fuentes de datos, un panel
El panel integra tres fuentes independientes. La meteorología viene de AEMET OpenData con la API key del usuario: predicción horaria por municipio con viento, racha, probabilidad de lluvia, nubosidad, temperatura, sensación térmica, humedad, visibilidad y UV, con cobertura de 48 horas. La actividad geomagnética viene de NOAA SWPC, la agencia espacial estadounidense: el índice Kp en tiempo real y su previsión para las próximas 72 horas en ventanas de tres horas, sin ningún tipo de autenticación. Y el estado del GPS viene del propio teléfono: el GnssStatusCallback de Android expone en tiempo real los satélites visibles, los que están en fix y las constelaciones detectadas (GPS, GLONASS, Galileo, BeiDou).
El índice Kp merece una explicación para quien no lo conozca. Es una medida de la actividad geomagnética global: valores entre 0 y 3 indican condiciones normales, Kp 4 es una perturbación menor que puede degradar ligeramente la precisión GPS, y Kp 5 o superior indica tormenta geomagnética con impacto real en la señal de los satélites. Para una misión de fotogrametría donde la precisión del GPS determina directamente la calidad del georreferenciado de las imágenes, este indicador es relevante.
El problema de AEMET: doble salto y Latin-1
La API de AEMET OpenData tiene una particularidad que no está especialmente documentada pero que se descubre rápidamente al trabajar con ella: usa un patrón de doble salto. La primera petición al endpoint de predicción no devuelve los datos directamente, sino una URL a otro JSON donde están los datos reales. El WeatherRepository hace los dos saltos en serie:
GET /api/prediccion/especifica/municipio/horaria/{cod_ine}?api_key=XXX
→ { "datos": "https://opendata.aemet.es/opendata/sh/...json" }
GET https://opendata.aemet.es/opendata/sh/...json
→ [array con la predicción por horas]
El segundo salto tiene otra particularidad: la respuesta no viene en UTF-8 sino en ISO-8859-1 (Latin-1). Esto no es un problema para los datos numéricos, pero sí para los campos de texto —el nombre del municipio, la descripción del estado del cielo— que contienen caracteres como tildes o la ñ. Si se parsea el JSON como UTF-8, esos caracteres aparecen corruptos. La solución fue leer los bytes brutos de la respuesta y decodificarlos explícitamente:
val bytes = response.body.bytes()
val json = String(bytes, Charsets.ISO_8859_1)
Este hallazgo se confirmó con file sobre un fichero de respuesta guardado en disco: ISO-8859 text. No hay forma de saberlo mirando la documentación de AEMET, solo probando.
Resolver la ubicación: de coordenadas a código INE
AEMET no trabaja con coordenadas geográficas sino con códigos INE de municipio. Para obtener la predicción de una ubicación hay que saber a qué municipio pertenece, y eso requiere resolver lat/lng a un código INE.
El plan original contemplaba incluir una tabla CSV embebida en los assets de la app con los ~8.000 municipios de España. La solución implementada en la prácitca fue diferente: descargar la tabla de municipios de la propia API de AEMET y cachearla en disco. La ventaja es que la tabla siempre está actualizada y el APK no se engrosa con 200 KB de datos estáticos que pueden cambiar. La desventaja es que requiere red en el primer uso, que es un escenario perfectamente asumible.
La resolución de municipio más cercano a unas coordenadas se hace por distancia Haversine sobre la tabla cacheada: para cada municipio se calcula la distancia al punto y se devuelve el más próximo. O(n) sobre 8.000 elementos es perfectamente rápido en un dispositivo moderno.
Las coordenadas de los municipios en la tabla de AEMET vienen en dos formatos: grados/minutos/segundos con símbolos (que sufren el problema de la codificación que indicábamos antes) y decimal como string en los campos latitud_dec/longitud_dec. Se usan los decimales para evitar el parsing DMS, que requeriría lidiar con los símbolos de grado corruptos en Latin-1.
El panel: slider de 48 horas y semáforos configurables
El panel de condiciones es un ModalBottomSheet que se abre desde el selector de capas del mapa, sin ocupar espacio permanente en la interfaz. El elemento central es un slider de 0 a 48 horas: al deslizarlo, las tarjetas de meteorología se actualizan con los datos de esa hora concreta de la predicción AEMET.
Encima del slider hay una barra de resumen de 48 bloques, uno por hora, coloreados según el semáforo de esa hora. El bloque correspondiente a la hora seleccionada se dibuja más alto que el resto para actuar como indicador visual de posición. Tocar cualquier bloque salta el slider a esa hora. El color de cada bloque es el peor indicador entre todos los parámetros evaluados: si la meteorología está en verde pero el Kp está en ámbar, el bloque es ámbar.
Los semáforos evalúan cada parámetro contra umbrales configurables en los ajustes de la app:
- Viento máximo (m/s)
- Racha máxima (m/s)
- Lluvia máxima (mm/h)
- Visibilidad mínima (km)
- Kp máximo para verde
- Satélites mínimos en fix para verde
Un botón «Mejor ventana de vuelo» recorre las 48 horas buscando el bloque contiguo más largo en verde y salta el slider al inicio de ese intervalo, mostrando un mensaje con el rango horario encontrado.
La tarjeta GPS merece una aclaración de diseño: los satélites visibles no tienen predicción futura, porque calcularla requeriría TLE orbitales y la posición del observador —una complejidad injustificada cuando el índice Kp ya es el indicador real de calidad GPS en una ventana temporal. El panel siempre muestra los satélites actuales del teléfono, con una etiqueta que indica que es el valor «ahora» aunque el slider esté en el futuro. Las tarjetas de meteorología y GPS son independientes: la barra de resumen y la tarjeta Meteo evalúan solo parámetros meteorológicos, y la tarjeta GPS tiene su propio semáforo.
Las pruebas: primero las APIs, luego el dispositivo
La verificación de la Fase 8 siguió el mismo protocolo de fases anteriores: primero validar las APIs externas con curl antes de escribir ningún código de integración, luego tests de compilación, y finalmente pruebas en el dispositivo real.
La validación previa con curl confirmó el comportamiento del doble salto de AEMET, la codificación ISO-8859-1 y la estructura del JSON de predicción horaria. También confirmó que la API de NOAA SWPC devuelve un array bidimensional con cabecera, donde las filas tienen la forma [time_tag, kp, observed, noaa_scale], lo que determinó el modelo de datos antes de escribir el parser.
Las pruebas en el Motorola Moto G73 5G el 12 de abril revelaron cuatro bugs que se corrigieron en el momento:
- URL base incorrecta:
WeatherRepositoryusabahttps://opendata.aemet.escomo base, pero todas las rutas de la API requieren el prefijo/opendata. Resultado: 404 en todas las peticiones. La URL correcta eshttps://opendata.aemet.es/opendata. - Endpoint de validación inexistente: el botón «Probar AEMET» llamaba a
/api/maestro/provincias, que devuelve 404 incluso con una clave válida. Cambiado a/api/maestro/municipios, que devuelve 200 con key válida y 401 sin ella. - Fallback de ubicación demasiado agresivo: sin polígono dibujado, el panel usaba siempre Madrid como ubicación por defecto. Se añadió una consulta a
LocationManagerantes de caer al hardcode. Verificado en campo: con el dispositivo en Pontevedra, el panel resolvió correctamente «Forcarei (36018)». - Semáforo de la tarjeta Meteo inconsistente con la barra de resumen: la barra usa solo parámetros meteorológicos para colorear cada bloque, pero la tarjeta Meteo usaba la evaluación completa (meteorología + GPS + Kp). Con 0/0 satélites en fix —situación normal en interior— la tarjeta Meteo mostraba «Desaconsejado» aunque la meteorología estuviera perfectamente en verde. Se separó la evaluación: la tarjeta Meteo usa solo parámetros meteorológicos, coherente con la barra; la tarjeta GPS tiene su propio semáforo independiente.
La prueba con datos reales de AEMET —12 de abril, Galicia, lluvia previsible— devolvió exactamente lo esperado: «Viento 3,6 m/s N · Racha 9,2 m/s», «Lluvia Prob 80% · 0.1 mm/h», «Temp 13°C · ST 13°C», semáforo general en rojo por probabilidad de lluvia superior al umbral. La tarjeta GPS mostraba 0/0 satélites en fix, esperado en interior.
Queda pendiente de verificar en exterior la tarjeta GNSS con fix real. En interior el comportamiento es el correcto —cero satélites, semáforo rojo— pero la actualización en tiempo real del StateFlow al adquirir fix necesita confirmarse en campo abierto con el GPS activo.
Degradación elegante
Como en todas las fases de integración con servicios externos, el sistema está diseñado para degradarse sin bloquear la app. Sin API key de AEMET, el panel muestra un mensaje de configuración. Sin red pero con caché válida (menos de una hora), muestra los datos cacheados con el timestamp de última actualización. Sin red y sin caché, el panel queda vacío sin errores ni fallos. Si NOAA SWPC no responde, la tarjeta GPS sigue mostrando los satélites en tiempo real, simplemente sin el dato de Kp. Si el GPS del teléfono está desactivado, la tarjeta lo indica.
El estado del proyecto: ocho fases, un ciclo completo
Con la Fase 8 completada, la aplicación cubre ahora todo el proceso de una misión de fotogrametría con drones: consultar si las condiciones son adecuadas antes de salir, planificar el área de vuelo, ejecutar la misión con el dron de forma autónoma, procesar las imágenes en el servidor, ver los resultados sobre el mapa y consultar los hallazgos detectados por la IA durante el vuelo. Ocho fases que empezaron con un mapa vacío y un slider de altura.
Lo que queda en el horizonte —soporte para otros modelos de drones DJI, descarga activa de tiles de mapa, drag directo de vértices del polígono— está documentado como Fase 9. Cuando haya algo concreto que contar, habrá artículo. 