# PilotVault Fly Agent Android companion app that runs **next to DJI Fly**. DJI Fly flies the aircraft; the Agent listens to the aircraft's **Remote ID** broadcast, streams the paired aircraft's position to PilotVault, and files the flight in the logbook. It uses no DJI SDK and never sends a command, so it works with every DJI Fly aircraft (Mavic 4 Pro, Air 3S, Mini 5 Pro, …) as long as Remote ID is broadcasting. Plan and research: `REA/DJI Fly/PilotVault_FlyAgentApp_Plan.md` (outside this repo). Built so far: phases **A0–A3** (login, Remote ID receiver, live uplink, flight session → logbook). Not yet: RTMP video (A4), flight-record import (A5), media metadata (A6), missions to DJI Fly (A7). ## Build ```bash flutter build apk --debug ``` `build/app/outputs/flutter-apk/app-debug.apk`, package `com.pilotvault.flyagent`. - `permission_handler` is pinned to 12.x: 13.x needs compileSdk 37. - `kotlin.incremental=false` in `android/gradle.properties`: the project and the pub cache are on different drives, which breaks Kotlin's incremental caches. ## How it fits | Piece | Where | |---|---| | Remote ID scanners (BLE legacy + BT5, Wi-Fi NaN, Wi-Fi Beacon) in a foreground service | `android/.../flyagent/*.kt` | | OpenDroneID decoder (ASTM F3411 / EN 4709-002) | `lib/rid/odid_decoder.dart` | | Frames → aircraft (merged by serial across transports), pairing | `lib/rid/rid_aircraft.dart`, `rid_service.dart` | | Live uplink to the hub, `/ws/device?id=agent-` | `lib/uplink/uplink.dart` | | Session state machine (armed → active → landed), auto start/stop | `lib/session/flight_session.dart` | | Filing to `POST /api/flights`, offline queue | `lib/session/session_controller.dart` | The scanners only strip transport framing; all decoding is in Dart and unit tested against byte-built messages (`test/`). **API Server contract.** The only server change is the logging-path split: - Live map: the hub already ingests `telemetry` events (`latitude`, `longitude`, `altitude` = height above take-off, `velocityX/Y/Z` = north/east/down, `isFlying`, `phoneLatitude/Longitude`). The Agent adds `source: "remote-id"`, `ridSerial`, `pilotLatitude/Longitude`, `uaClass`, which ride along in the raw event. It never sends `flightControllerSerial`, so the Web App's fleet auto-add does not mint a drone from a Remote ID serial. - Logbook: entries are filed with `loggingPath: "automatic_flyagentapp"` (migration `1720301200_split_logging_path.js`; the paths are `manual`, `automatic_flyapp`, `automatic_flyagentapp`). Remote ID is not the aircraft's own flight recorder, so the API Server still requires every § 5 field on this path, and the session fills them all in. - Pairing lives on the phone (SharedPreferences), not on the drone record; a `remote_id_serial` drone field is a later server change. **Privacy.** Every aircraft in range is decoded, but only the paired one is uploaded or logged. Other operators' broadcasts stay in memory on the phone. ## Limits worth knowing - Wi-Fi Beacon Remote ID needs Android 11+ and Android throttles Wi-Fi scans to 4 per 2 minutes (turn off "Wi-Fi scan throttling" in Developer options). An aircraft that only broadcasts over Beacon updates every ~30 s. - Not yet checked on a real aircraft: which transport each DJI Fly aircraft uses (research R1), and NaN on the SHT-AL09 tablet (Android 9: no Beacon IEs).