logging_path is now manual, automatic_flyapp or automatic_flyagentapp. The Fly App's entries are backed by the aircraft's flight recorder and keep the missing-FDR red flag; the Fly Agent App's come from Remote ID, which is not a flight recorder, so they must carry the § 5 fields like a manual entry. The API maps the old "automatic" onto automatic_flyapp so Fly App builds already on devices keep filing. The migration has been applied to the remote PocketBase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
flutter build apk --debug
build/app/outputs/flutter-apk/app-debug.apk, package com.pilotvault.flyagent.
permission_handleris pinned to 12.x: 13.x needs compileSdk 37.kotlin.incremental=falseinandroid/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-<serial> |
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
telemetryevents (latitude,longitude,altitude= height above take-off,velocityX/Y/Z= north/east/down,isFlying,phoneLatitude/Longitude). The Agent addssource: "remote-id",ridSerial,pilotLatitude/Longitude,uaClass, which ride along in the raw event. It never sendsflightControllerSerial, 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"(migration1720301200_split_logging_path.js; the paths aremanual,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_serialdrone 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).