Skip to main content

Python parser for DJI drone flight log files

Project description

pydjirecord

Python parser for DJI drone flight log files (.txt binary format).

Supports all log format versions 1 through 14, including XOR encoding (v7-12) and AES-256-CBC encryption (v13-14) with per-feature-point keys fetched from the DJI API.

Documentation | Changelog | PyPI

Acknowledgments

This project is a Python rewrite of dji-log-parser by Luc Vauvillier. The Rust implementation is the authoritative reference for parsing logic, binary layouts, and encryption details. Thank you for the excellent work and for making it open source.

Binary struct layouts and feature-point mappings are cross-referenced against the official DJI C++ parsing library: dji-sdk/FlightRecordParsingLib.

Requirements

  • Python 3.10+

Installation

pip install pydjirecord

To also parse VirtualStick (type 33) records, install the optional protobuf extra:

pip install 'pydjirecord[proto]'

Or from source:

git clone https://github.com/rembish/pydjirecord.git
cd pydjirecord
make install

CLI Usage

The package installs a djirecord command:

djirecord FILE [--json | --raw | --geojson | --kml | --csv | --hardware] [-o FILE] [--api-key KEY] [--no-cache] [--no-verify]

Flight info (default)

With no format flag, prints a human-readable summary. When an API key is available (or the log doesn't need one), frames are decrypted automatically and corrected values are shown for coordinates, distance, photos, and video time:

djirecord flight.txt                    # header-only for v13+
djirecord flight.txt --api-key KEY      # decrypts frames, shows corrected values
Log version:  14

Aircraft:     Mavic Air 2
Product type: MAVIC_AIR2
Aircraft SN:  ABC123
...

Flight stats:
  Distance:   4523.1 m
  Duration:   8m 42s
  Max height: 119.8 m
  Frames:     4362

Photos:       62
Video time:   1m 13s

Export formats

# JSON to stdout (details-only for v13+ without API key)
djirecord flight.txt --json

# JSON with frames to file
djirecord flight.txt --json -o flight.json --api-key YOUR_KEY

# Raw records as JSON
djirecord flight.txt --raw --api-key YOUR_KEY

# GeoJSON track
djirecord flight.txt --geojson -o track.geojson --api-key YOUR_KEY

# KML track
djirecord flight.txt --kml -o track.kml --api-key YOUR_KEY

# CSV telemetry
djirecord flight.txt --csv -o telemetry.csv --api-key YOUR_KEY

Format flags are mutually exclusive. Output defaults to stdout (-o -).

Hardware report

djirecord flight.txt --hardware --api-key YOUR_KEY
AIRCRAFT
  Model:          DJI Mini 4 Pro
  Product type:   MINI4_PRO
  Serial:         1581F6Z9C23CP003

CAMERA
  Serial:         6TVQLBJ0M209BS
  SD card:        inserted

REMOTE CONTROLLER
  Serial:         6UZBLCN021016H
  Downlink:       min 0%, avg 80%
  Uplink:         min 1%, avg 85%

BATTERY
  Serial:         7BVPLBVDA104J3
  Design cap:     2590 mAh
  Charge cycles:  3
  Charge:         99% -> 81% (used 18%)
  Temperature:    29.5 - 39.2 C
  Cells:          2, deviation 4 mV

FLIGHT CONTROLLER
  Failsafe:       GO_HOME
  Obstacle avd:   ON

Shows aircraft, camera, RC (signal quality, pilot GPS if available), battery health (design capacity, charge cycles, voltage range, cell deviation), firmware versions, flight controller settings, and component serials. Works without an API key (header-only mode) but shows more with decrypted frames.

API key

Logs version 13 and above use AES-256-CBC encryption. To decrypt them you need a DJI API key. To obtain one:

  1. Visit DJI Developer Technologies and log in.
  2. Click CREATE APP, choose Open API as the App Type, and fill in the required details (App Name, Category, Description).
  3. Activate the app through the confirmation link sent to your email.
  4. On your developer user page, find your app's details — the ApiKey is labeled as the SDK key.

Provide the key via:

  • --api-key KEY argument
  • DJI_API_KEY environment variable
  • .env file in the current directory
# .env file
DJI_API_KEY=your_key_here

--no-cache skips the local keychain cache and always makes a fresh API call.

--no-verify disables TLS certificate verification for the DJI API request. Use this if the request fails with a certificate error on your system (e.g. corporate proxies or custom CA stores):

djirecord flight.txt --api-key KEY --no-verify

Keychains are cached locally after the first successful fetch, so --no-verify is only needed once per unique log file.

Library Usage

from pydjirecord import DJILog

# Parse a flight log
data = open("flight.txt", "rb").read()
log = DJILog.from_bytes(data)

# Access flight metadata (no decryption needed)
print(log.version)
print(log.details.aircraft_name)
print(log.details.total_distance)

# Decrypt and iterate frames (v13+ needs keychains from the DJI API)
keychains = log.fetch_keychains("YOUR_API_KEY") if log.version >= 13 else None
# Pass verify=False if you get a TLS certificate error:
# keychains = log.fetch_keychains("YOUR_API_KEY", verify=False) if log.version >= 13 else None
frames = log.frames(keychains)

for frame in frames:
    print(frame.osd.latitude, frame.osd.longitude, frame.osd.altitude)
    print(frame.battery.voltage, frame.battery.charge_level, frame.battery.lifetime_remaining)
    print(frame.gimbal.pitch, frame.gimbal.yaw)

# Raw records
records = log.records(keychains)

Accurate flight statistics

Several header fields (capture_num, video_time, total_distance, latitude/longitude) are unreliable. FrameDetails corrects them automatically when you pass decoded frames:

from pydjirecord import DJILog
from pydjirecord.frame.details import FrameDetails

data = open("flight.txt", "rb").read()
log = DJILog.from_bytes(data)
keychains = log.fetch_keychains("YOUR_API_KEY") if log.version >= 13 else None
frames = log.frames(keychains)

# FrameDetails computes all corrected values from frames automatically
details = FrameDetails.from_details(log.details, frames)

print(details.latitude)       # from header, or first valid OSD GPS fix if header is 0,0
print(details.longitude)      # same
print(details.total_distance) # cumulative GPS track length from frames
print(details.photo_num)      # computed from Camera remain_photo_num delta
print(details.video_time)     # computed from Camera record_time segments

The individual compute_* functions are also available if you need them directly:

from pydjirecord.frame.builder import compute_coordinates, compute_photo_num, compute_video_time

lat, lon = compute_coordinates(frames)                            # first valid GPS fix
distance = frames[-1].osd.cumulative_distance if frames else 0.0  # GPS track length
photos = compute_photo_num(frames)                                # remain_photo_num delta
video_seconds = compute_video_time(frames)                        # sum of record_time segments

Flight anomaly detection

FrameDetails automatically classifies flight anomalies when frames are provided:

details = FrameDetails.from_details(log.details, frames)

if details.anomaly and details.anomaly.severity != FlightSeverity.GREEN:
    print(f"Severity: {details.anomaly.severity.name}")
    print(f"Actions:  {[a.name for a in details.anomaly.actions]}")
    print(f"Motor blocked: {details.anomaly.motor_blocked}")
    print(f"Max descent:   {details.anomaly.max_descent_speed:.1f} m/s")

Or call the function directly:

from pydjirecord.frame.builder import compute_flight_anomalies
from pydjirecord.frame.anomaly import FlightSeverity

anomaly = compute_flight_anomalies(frames)
if anomaly.severity == FlightSeverity.RED:
    print("Critical flight anomaly detected")

Severity levels: RED (loss of control, forced landing, motor failure, freefall), AMBER (low battery RTH, GPS degradation, negative final altitude), GREEN (normal flight).

Known Limitations

Header field caveats

The Details header block is readable without decryption. Most fields are reliable, but some are not (verified across 585 real flight logs):

Field Status Notes
details.latitude / details.longitude Unreliable Zero in ~20 % of flights (116 of 585 tested logs) despite being real outdoor flights with GPS. The DJI app fails to write takeoff coordinates to the header. When frames are available, FrameDetails falls back to the first valid OSD GPS fix.
details.total_distance Approximate Stored in the binary as kilometres; converted to metres on parse. Matches frame-computed distance within float32 precision in 95%+ of logs. A small number carry stale values from prior flights. The DJI C++ library ignores this field and recomputes from the GPS track. Prefer frames[-1].osd.cumulative_distance when decrypted frames are available.
details.max_height Reliable Matches frame-computed maximum within 1-2 m in all tested logs.
details.max_horizontal_speed Reliable Matches frame-computed maximum in all tested logs.
details.capture_num Broken Always 0 for DJI Fly app logs. When frames are available, FrameDetails.photo_num is computed from Camera remain_photo_num delta and is accurate.
details.video_time Unreliable Not per-flight recording duration. The ratio to actual in-frame recording time ranges from 1x to over 100x with no consistent unit. When frames are available, FrameDetails.video_time is computed from Camera record_time segments and is accurate.

Network access required for decryption

Version 13 and 14 logs use AES-256-CBC encryption. Decryption requires fetching per-flight keys from the DJI API over HTTPS:

https://dev.dji.com/...

In environments with certificate validation issues (corporate proxies, custom CA stores), log.fetch_keychains() may raise a TLS error. Pass verify=False or use the --no-verify CLI flag to bypass certificate checking. Keychains are cached after the first successful fetch so you only need this once per log file.

In air-gapped or network-restricted environments (no outbound HTTPS), log.fetch_keychains() will raise a network error. In that case:

  • log.details (the unencrypted header) is still fully readable.
  • log.version, log.details.aircraft_name, log.details.start_time, etc. work without a network call.
  • djirecord flight.txt --json works without a key and returns a details-only JSON object (no frame data).
  • Frame-level telemetry and the frame-bearing export formats (--raw, --csv, --geojson, --kml, and --json with frames) require the decryption keys and will fail without network access.

Encryption

Version Encryption
1-6 None
7-12 XOR (CRC64-derived key)
13-14 XOR + AES-256-CBC (per-feature-point keys from DJI API)

Status

The core parsing pipeline, frame builder, and all export formats work. Record types not covered by the upstream binary spec are returned as raw bytes.

Testing coverage:

The author has format version 14 logs from Mavic Air 2 and Mini 4 Pro (RC2). Older format versions (v1-12) are tested only through crafted binary data in unit tests, not with real flight logs. If you have DJI flight logs from older drones or older DJI app versions (format versions 1 through 12), please consider contributing them — even a single short flight per version would help verify the parsing and decryption paths end-to-end.

Development

make install        # create venv and install with dev deps
make check          # format + lint + typecheck + test
make format         # ruff format + autofix
make lint           # ruff check
make typecheck      # mypy strict
make integration    # integration + mutation-regression tests, no coverage floor
make test           # pytest with coverage
make docs           # build documentation site
make docs-serve     # serve documentation locally
make build          # build sdist and wheel into dist/

Run tests across all supported Python versions with tox (requires the interpreters to be installed):

.venv/bin/tox                  # all versions
.venv/bin/tox -e py310,py312   # specific versions

Run a single test:

.venv/bin/pytest tests/test_cli.py::TestJsonOutput -xvs

Run integration and mutation-regression tests against a private local corpus without committing logs into the repo:

make integration DJI_LOGS_DIR=/path/to/your/logs

The integration target passes --no-cov so partial runs don't trip the coverage floor. Equivalent manual invocation:

DJI_LOGS_DIR=/path/to/your/logs .venv/bin/pytest -m integration --no-cov -xvs tests/test_djilog.py tests/test_mutation_regression.py

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydjirecord-1.3.0.tar.gz (85.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydjirecord-1.3.0-py3-none-any.whl (70.8 kB view details)

Uploaded Python 3

File details

Details for the file pydjirecord-1.3.0.tar.gz.

File metadata

  • Download URL: pydjirecord-1.3.0.tar.gz
  • Upload date:
  • Size: 85.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pydjirecord-1.3.0.tar.gz
Algorithm Hash digest
SHA256 268bc25f9cf4aefa9bfc1b7d1be8d39a561e06abc80dbf72064368ad9461a202
MD5 22f580b1f7730cb4b993d49f9736e868
BLAKE2b-256 062d5f12db0e73baa52527bef1177e465a96bcfd81add3574545ec06b6863ea9

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydjirecord-1.3.0.tar.gz:

Publisher: ci.yml on rembish/pydjirecord

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pydjirecord-1.3.0-py3-none-any.whl.

File metadata

  • Download URL: pydjirecord-1.3.0-py3-none-any.whl
  • Upload date:
  • Size: 70.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pydjirecord-1.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 21b184d1ed32e872fbc15be173987975f6233cbfb344d83534c7e26d3bf22cc2
MD5 d8593160d827265a5da4a82734c20591
BLAKE2b-256 cd2f9011e9fbc8db4954d35161a90ecb8746d8dda6336148b84ab1f8463db3b7

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydjirecord-1.3.0-py3-none-any.whl:

Publisher: ci.yml on rembish/pydjirecord

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page