Fotogrametría asistida por IA
En el artículo anterior de esta serie describí cómo se construyó la Fase 1 de la aplicación: desde la verificación del entorno hasta tener una app funcional para planificar misiones de fotogrametría sobre un mapa. El resultado era ya útil como herramienta de planificación, pero le faltaba lo más importante: ser capaz de hacer volar al dron. Eso es lo que vino después, y fue bastante más accidentado de lo que esperaba.
Integrando el DJI Mobile SDK v5
El primer paso de la Fase 2 fue integrar el DJI Mobile SDK v5.17.0, que es el que da acceso al control del Mini 3 Pro desde una aplicación Android propia. La integración no es trivial: requiere añadir las dependencias específicas de DJI en el fichero de Gradle, configurar su repositorio Maven en settings.gradle.kts, y gestionar una API Key de desarrollador que se inyecta en el manifest en tiempo de compilación a través de local.properties. Hasta aquí, trabajo de fontanería habitual.

Lo más interesante de esta fase fue la arquitectura que se diseñó para encapsular toda la interacción con el SDK en un paquete propio (dji/), con responsabilidades bien separadas:
DjiSdkManager: gestiona la inicialización del SDK y el estado de conexión con el dron, expuesto como unStateFlow<Boolean>.DroneTelemetry: se suscribe a todos los datos en tiempo real del dron —GPS, heading, batería, señal, estado de vuelo— usando los listeners delKeyManagerdel SDK.MissionExecutor: el motor de navegación autónoma. Aquí es donde ocurre lo más interesante, y también donde llegaron los mayores problemas.CameraConfigurator: control en vivo de los parámetros de cámara (ISO, velocidad de obturación, balance de blancos).FlightLogger: logger que escribe a un fichero en el almacenamiento del dispositivo, pensado para diagnóstico en campo donde no hay acceso a un PC con ADB.
La inicialización del SDK merece una mención especial porque tiene un orden concreto que no se puede alterar: primero SDKManager.init() con su callback, y solo dentro del callback de éxito se llama a registerApp(). Invertirlo produce errores silenciosos que son difíciles de diagnosticar.
Para completar la integración también fue necesario añadir un fichero accessory_filter.xml que permite que la app se abra automáticamente al conectar el mando DJI por USB, y un network_security_config.xml requerido por el SDK para sus comunicaciones internas.
La pantalla de vuelo: FPV y telemetría
Con el SDK integrado, el siguiente paso fue construir la pantalla de ejecución de misiones. El diseño final es un layout horizontal a dos columnas, pensado para usarse en modo landscape con el teléfono conectado al mando:
- Columna izquierda (60%): imagen FPV en vivo a pantalla casi completa, con un botón de retorno superpuesto como overlay.
- Columna derecha (40%): mini-mapa con el plan de vuelo y la posición del dron, telemetría en tiempo real, estado de la misión y controles de vuelo (iniciar, pausar, parada de emergencia).
El FPV se implementa con un SurfaceView que registra su superficie en el SDK de DJI para recibir el stream de vídeo. La integración con Jetpack Compose se hace, igual que con MapLibre, mediante AndroidView.
El diseño de la zona de cámara pasó por varias iteraciones antes de llegar a algo satisfactorio. El primer intento fue un panel lateral con los controles; ocupaba demasiado espacio y dejaba la imagen FPV demasiado pequeña para ser útil. Después se probó con un aspecto fijo 16:9, y luego 21:9, sin que ninguno funcionara bien en landscape. La solución final fue la más obvia en retrospectiva: FPV a pantalla completa con controles translúcidos superpuestos, al estilo de cualquier app de cámara moderna. Los valores actuales de ISO, obturación y balance de blancos aparecen como chips en la parte inferior; al tocar uno se despliega un selector.
En cuanto a las capas cartográficas, en esta fase se añadieron dos específicas para España que son muy útiles en campo:
- Ortofotos PNOA del IGN: imágenes aéreas de alta resolución via WMTS. Mucho más útiles que OSM para identificar el terreno antes de volar.
- Catastro: parcelas y edificios superpuestos sobre las ortofotos via WMS INSPIRE. Imprescindible para saber exactamente sobre qué propiedad estás volando.
Ambas capas se seleccionan desde un botón en la esquina del mapa, y la selección se persiste en el ViewModel para que no se pierda al navegar entre pantallas.
El descubrimiento que lo cambió todo: el Mini 3 Pro no soporta misiones onboard. Una prueba para Claude Code
Llegados aquí es donde el proyecto se encontró con su mayor obstáculo, y también donde la experiencia se volvió más instructiva.
El mecanismo estándar de DJI para ejecutar misiones de waypoints es subir al dron un archivo KMZ con la ruta definida y dejarlo volar de forma autónoma usando su propio hardware de navegación. Es el equivalente a programar un GPS y decirle «llega tú solo». La primera implementación del motor de ejecución usaba exactamente este sistema: WaypointMissionManager.pushKMZFileToAircraft().
El resultado fue un escueto error: «Upload: null».

Después de descartar problemas de configuración, el diagnóstico fue demoledor: el DJI Mini 3 Pro no soporta misiones onboard. Esta funcionalidad está reservada para la gama enterprise de DJI (Matrice, Mavic 3E y similares). El Mini 3 Pro, a pesar de ser un dron muy capaz, no tiene el hardware de navegación necesario para ejecutar misiones de forma autónoma sin intervención continua de la app.
Estrictamente hablando, esto era algo que ya sabía. Lo que quería averiguar era si Claude Code iba a ser lo suficientemente listo como para detectarlo, y tomar las acciones en consecuencia. Así, y de manera completamente intencionada, no había dicho nada durante el proceso de preparación de la aplicación, y había omitido este detalle. Y Claude Code no fue capaz de averiguarlo desde el inicio. Había optado por la implementación convencional, sin tener en cuenta las especificidades del modelo de dron con el que estábamos trabajando.
Una vez que Claude Code fue consciente de esto, no lo quedó más remedio que replantear completamente el motor de ejecución, e ir a la alternativa que sí está soportada para el Mini 3 Pro, que no es otra que Virtual Stick: un modo del SDK que simula los sticks del mando, enviando comandos de vuelo continuos desde la app a 20 Hz. En lugar de subir una misión y dejar que el dron la ejecute, la app es quien navega activamente en todo momento, calculando en cada ciclo hacia dónde debe moverse el dron y enviando ese comando.
Es más complejo, más exigente computacionalmente y más sensible a la latencia, pero funciona en todos los drones DJI. Y hay que decir que el resultado final es bastante más controlable que las misiones onboard, porque la lógica de navegación está completamente en tus manos.
Virtual Stick: cómo se navega sin misiones onboard
El modo Virtual Stick que se implementó usa el Advanced Mode, que ofrece bastante más control que el modo básico:
- Velocidad expresada en coordenadas absolutas norte/este (sistema GROUND), no relativas al dron.
- Altitud en modo POSITION: el SDK mantiene la altura objetivo sin que haya que controlarla constantemente.
- Yaw en modo ANGLE: se especifica un ángulo absoluto (0° = norte, sentido horario) en lugar de una velocidad de rotación.
El algoritmo de navegación para cada waypoint funciona así: leer la posición GPS actual del dron, calcular el bearing y la distancia hasta el objetivo, decidir el yaw apropiado, y enviar un comando de velocidad descompuesto en sus componentes norte y este. Todo esto se repite a 20 Hz hasta que la distancia al waypoint cae por debajo de 1,5 metros.
Para evitar movimientos bruscos, especialmente problemáticos con la latencia inherente a cualquier comunicación inalámbrica, se implementó un perfil de velocidad trapezoidal: el dron acelera gradualmente al inicio de cada tramo, vuela a velocidad de crucero en la parte central y decelera al acercarse al waypoint, con una velocidad mínima de 0,3 m/s para mantener el movimiento incluso muy cerca del objetivo.
En los waypoints donde hay que tomar foto, la secuencia es: hover de 1,5 segundos → disparo → espera de 1 segundo. Antes de iniciar la navegación, la app verifica que la cámara está en modo foto (no en vídeo), un paso que parece obvio pero que si se omite provoca que los disparos fallen silenciosamente.
Tres bugs de vuelo real, y cómo se resolvieron
La depuración de un sistema de navegación autónoma tiene una dificultad añadida: los bugs se manifiestan en campo, con el dron en el aire, y no siempre hay acceso a herramientas de diagnóstico. Por eso se implementó desde el principio el FlightLogger, que escribe un log detallado a un fichero recuperable después mediante adb pull. Fue una decisión que resultó fundamental.
Bug 1: el dron volaba en dirección opuesta a los waypoints. El primer síntoma en campo fue que, tras despegar y alcanzar la altitud de misión, el dron se alejaba del área planificada en lugar de dirigirse al primer waypoint. La primera sospecha fue GPS inválido (coordenadas en 0,0), así que se añadió una función waitForValidGps() que espera hasta 15 segundos a que el GPS sea fiable antes de empezar a navegar. El problema persistió.
Con los logs del segundo vuelo de prueba, el diagnóstico fue mucho más concreto. El GPS era correcto. Los bearings calculados eran correctos. Pero el movimiento real del dron no correspondía con las velocidades que se enviaban. Y ahí estaba el problema: en el DJI MSDK v5 con el sistema de coordenadas GROUND, los ejes pitch y roll están intercambiados respecto a la convención aeronáutica habitual.
// Lo que dice la intuición (y la documentación):
param.pitch = northVelocity // adelante/atrás
param.roll = eastVelocity // izquierda/derecha
// Lo que hace realmente el SDK v5 en modo GROUND:
param.pitch = eastVelocity // este-oeste
param.roll = northVelocity // norte-sur
Intercambiar los dos valores en el código hizo que el dron empezara a seguir los waypoints correctamente. Es el tipo de bug que no aparece en ninguna documentación y que solo se descubre volando y tomando logs.
Bug 2: Virtual Stick falla al habilitarse. En algunos arranques, enableVirtualStick() devolvía un error con descripción nula, lo que hacía imposible iniciar la misión. El problema era de orden: el Advanced Mode se intentaba habilitar antes de que Virtual Stick estuviera activo. La solución fue invertir el orden de las llamadas y añadir un sistema de reintentos (tres intentos con dos segundos de espera entre cada uno), que es también lo recomendable para cualquier operación que dependa de la comunicación con el dron.

Bug 3: las fotos no se guardaban en el dron. El dron completaba la ruta correctamente —se podía ver en los logs que alcanzaba cada waypoint y ejecutaba el comando de disparo— pero al revisar la memoria no había fotos. La causa: la cámara estaba en modo vídeo. KeyStartShootPhoto del SDK falla silenciosamente si la cámara no está previamente configurada en modo foto. La solución fue añadir una llamada explícita a KeyCameraMode = PHOTO_NORMAL antes de iniciar cualquier navegación.

Optimizando el orden de los waypoints
Uno de los últimos refinamientos de esta fase fue la optimización del orden de recorrido de los waypoints, especialmente relevante en misiones de modelo 3D con doble rejilla (cross-hatch).
El problema es que las líneas de vuelo se generan en dos grupos: primero todas las pasadas en una dirección, luego todas las perpendiculares. Recorrerlas en ese orden provoca un salto largo entre el último punto del primer grupo y el primero del segundo, lo que se traduce en distancia extra y tiempo de vuelo innecesario. Visualmente, la trayectoria parecía caótica.
La solución fue implementar un algoritmo de vecino más cercano: al terminar cada línea de vuelo, se elige como siguiente la línea pendiente cuyo extremo más cercano esté a menor distancia, independientemente de si es una línea primaria o perpendicular. Esto intercala naturalmente ambos grupos y minimiza las transiciones. El coste computacional para el tamaño típico de una misión (menos de 100 líneas) es completamente despreciable.
Ajustes de UX para uso en campo
Además del motor de navegación, esta fase incluyó una serie de ajustes de experiencia de usuario pensados específicamente para el uso en campo, con el sol en la cara y el dron en el aire:
- Landscape forzado: la app no tiene ningún sentido en modo vertical cuando el teléfono está montado en el mando DJI.
- Modo inmersivo: las barras del sistema Android se ocultan para maximizar el espacio de la imagen FPV.
- Centrado de mapa en GPS real: en lugar de centrar en Madrid al arrancar, el mapa se posiciona en la ubicación actual del dispositivo. Útil cuando llegas a una parcela y abres la app.
- Persistencia de la capa de mapa: la selección entre OSM, PNOA y Catastro se persiste en el ViewModel, así no se resetea al navegar entre pantallas.
- Restauración de misión al volver al mapa: cuando se navega de la pantalla de vuelo al mapa y de vuelta, el polígono y las líneas de vuelo se restauran correctamente porque el MapView se reinicializa.
El resultado de la Fase 2
Al cierre de esta fase, la aplicación era ya capaz de ejecutar misiones de fotogrametría reales de principio a fin:
- Integración completa con DJI MSDK v5.17.0.
- Conexión USB con el mando y estado del dron en tiempo real (GPS, batería, señal, heading, altitud).
- Vista FPV integrada.
- Navegación autónoma via Virtual Stick con perfil de velocidad trapezoidal.
- Disparo automático de cámara en cada waypoint fotográfico.
- Control en vivo de parámetros de cámara (ISO, obturación, EV, balance de blancos).
- Mini-mapa con la posición del dron en tiempo real.
- Retorno automático a casa al finalizar la misión.
- Parada de emergencia.
- Validación de GPS antes de iniciar la navegación.
- Logger a fichero para diagnóstico post-vuelo.
- Capas de mapa PNOA (IGN) y Catastro.
- Optimización de orden de waypoints por vecino más cercano.
No fue un camino sin sobresaltos. El descubrimiento por parte de Claude Code de que el Mini 3 Pro no soporta misiones onboard le obligó a reescribir el motor de ejecución desde cero. Los bugs de pitch/roll intercambiados y de Virtual Stick fallando silenciosamente solo se pudieron diagnosticar con vuelos reales y logs detallados. Pero el resultado es una app que vuela misiones de fotogrametría de forma autónoma y que ha demostrado funcionar en campo.
En el próximo artículo hablaré de la Fase 3: la corrección del heading del dron durante el vuelo, la visualización en tiempo real de la trayectoria sobre el mini-mapa, y una serie de mejoras de UX que marcaron la diferencia entre una app que funciona y una app que es cómoda de usar en campo.