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.

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, provide a DJI API 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 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.2.1.tar.gz (84.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.2.1-py3-none-any.whl (70.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pydjirecord-1.2.1.tar.gz
  • Upload date:
  • Size: 84.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.2.1.tar.gz
Algorithm Hash digest
SHA256 4092ec256b0843ff04b43d4ad8e69e0e42d274bc07eb50a538e989dc61b1894e
MD5 66d0f898936f0b35792df7e0774ad6e5
BLAKE2b-256 f9ac824f315d3f34f95d53b57319595533917861ea8501308968f33e9896d3f1

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydjirecord-1.2.1.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.2.1-py3-none-any.whl.

File metadata

  • Download URL: pydjirecord-1.2.1-py3-none-any.whl
  • Upload date:
  • Size: 70.0 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.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2437303cb9ccd1d61a9040569a47d756e5f808f2809c61f7c771aee8b06d7183
MD5 13cf1d582b1384487e804c71e00cefe2
BLAKE2b-256 f474ba76405473d90abbd00b24784d31e44491552b036bf25fe091c6f8d85c1b

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydjirecord-1.2.1-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