Android companion app for DroneOpsCommand — syncs DJI flight logs from smart controllers directly into the DroneOpsCommand Flight Management System.
Part of the DroneOps platform. No separate server required — DroneOpsCommand IS the backend.
DroneOpsSync runs on your DJI smart controller (sideloaded APK). It scans the controller's local storage for flight log files and uploads them in a single batch to your DroneOpsCommand instance over plain HTTP — either directly on your local network, or remotely via WireGuard VPN. Once confirmed on the server, logs can be deleted from the controller with a single tap.
- DroneOpsCommand deployed and running (see DroneOpsCommand)
- Device API key generated in DroneOpsCommand → Settings → Device Access
- Network access to the server — LAN (direct IP) or WireGuard VPN for remote
The controller connects directly to the server's local IP over HTTP. No tunnel, no TLS, no extra config. Just make sure both devices are on the same network and the server port is reachable.
DJI Controller → http://192.168.1.50:8080
Connect the controller to your WireGuard VPN before uploading. The app then talks to the server's VPN IP exactly like LAN — plain HTTP, no certificate issues.
DJI Controller → WireGuard VPN → http://10.8.0.1:8080
WireGuard has an official Android APK that runs on Android 8+, compatible with all DJI smart controllers.
In your DroneOpsCommand web UI:
- Go to Settings → Device Access
- Copy the auto-generated
Device API Key
cd android
./gradlew assembleRelease
# APK at: app/build/outputs/apk/release/app-release.apkSideload the APK onto each DJI smart controller via ADB or a file manager.
Open DroneOpsSync on the controller, tap the Settings (gear) icon, and enter:
| Field | Value |
|---|---|
| DroneOpsCommand URL | Local IP (e.g. http://192.168.1.50:8080) or WireGuard VPN IP |
| Device API Key | The key copied from DroneOpsCommand Settings |
Tap Save. The status indicator will turn green when the server is reachable.
On the DJI RC Pro 2 (stock-AOSP Android 11) and any other Android 11+ controller, the OS sandboxes Android/data/<other-pkg> directories — MANAGE_EXTERNAL_STORAGE does not grant access to them. DroneOpsSync uses the system Storage Access Framework (SAF) picker as a one-time grant per install.
On first launch, the home screen shows a "DJI Fly logs need a one-time folder grant" banner. Tap it, and the SAF picker opens pre-seeded at the DJI flight-record folder. Tap Use this folder to grant.
The grant is persisted across launches with both read and write access — read for scanning, write for delete-after-sync to actually remove the original from the controller. See ADR-0005 and ADR-0006.
If a delete ever reports failure, open Diagnostics and look at the [DELETE] channel — it now names the exact provider response.
- SCAN FOR LOGS — finds
.txt/.logfiles in all configured paths - SYNC ALL — uploads all pending logs to DroneOpsCommand. The server parses them, deduplicates by hash, and imports into the Flight Library. Each file is uploaded and tracked independently, so a slow or failed file never blocks the rest of the sortie.
- DELETE — after confirmation, removes synced files from the controller only
Files are never deleted automatically — explicit confirmation is always required.
The upload path is resilient to slow field connections and slow server-side log parses:
- Per-file isolation. A
SocketTimeoutExceptionon one file fails only that file and the batch continues. Only genuinely batch-wide conditions abort the run: an unreachable host (UnknownHostException) or a bad device key (HTTP 401/403). Failed files stay asERRORcards you can long-press or swipe to retry. - Async upload (202 + poll). When the server advertises
async_upload_availableon the device-health preflight, the client POSTs to…/device-upload/async, receives a202 Acceptedwith abatch_id(the connection is released as soon as the bytes are uploaded — the parse no longer happens in-request), then polls…/device-upload/status/{batch_id}until each file reaches a terminal state. SHA-256 dedup short-circuits files already on the server with no poll. - Graceful fallback.
async_upload_availabledefaults off, so a new APK against an older/legacy DroneOpsCommand transparently uses the unchanged synchronous upload path. Closing the app mid-poll is safe — the job completes server-side and dedup reconciles on the next launch.
Pairs with DroneOpsCommand v2.71.0. See ADR-0008 (client) and DroneOpsCommand ADR-0023 (the canonical cross-repo contract). Live progress is visible in Diagnostics → [UPLOAD] channel (async submit HTTP code, batch_id, poll status).
Pre-configured in the app; additional paths can be added in Settings.
| Controller / Device | DJI App | Path |
|---|---|---|
| RC Plus 2 (4TD), RC Plus (M30T) | DJI Pilot 2 | /storage/emulated/0/DJI/com.dji.industry.pilot/FlightRecord |
| RC Pro (M3P), RC 2 (M5P) | DJI GO 5 | /storage/emulated/0/Android/data/dji.go.v5/files/FlightRecord |
| Phone (DJI GO 5) | DJI GO 5 | /storage/emulated/0/Android/data/dji.go.v5/files/FlightRecord |
| Phone (DJI Fly) | DJI Fly | /storage/emulated/0/Android/data/com.dji.fly/files/FlightRecord |
| Badge | Meaning |
|---|---|
| PENDING | Found locally, not yet uploaded |
| SYNCING | Upload in progress |
| SYNCED | Confirmed in DroneOpsCommand Flight Library |
| ON SERVER | Already existed on server (deduplicated) |
| ERROR | Upload or parse failed — check connection/key |
| DELETED | Removed from controller after sync |
DroneOpsSync talks directly to DroneOpsCommand over plain HTTP. There is no middleware, relay server, or tunnel required. Endpoints used:
| Endpoint | Method | Purpose |
|---|---|---|
/api/flight-library/device-health |
GET | Preflight — reachability, device-key validity, key-rotation hint, and the async_upload_available capability flag |
/api/flight-library/device-upload |
POST | Legacy synchronous upload (parses in-request) — fallback path when the server does not advertise async support |
/api/flight-library/device-upload/async |
POST | Async upload — streams bytes, returns 202 + {batch_id}, parses off-request |
/api/flight-library/device-upload/status/{batch_id} |
GET | Status poll for an async batch (per-file state) |
The client capability-detects: if the preflight reports async_upload_available: true, it uses the async route; otherwise it falls back to the synchronous endpoint.
The DroneOpsCommand stack includes:
flight-parser— Rust service that parses DJI.txtlog format- PostgreSQL — persistent flight record storage
Place your logo PNG at:
android/app/src/main/res/drawable/droneops_sync_logo.png
Recommended size: 1024 × 410 px on a transparent background.
MIT