A browser-native flight log analysis platform for DJI drones. Parses encrypted and unencrypted telemetry files, GNSS observation data, and photogrammetry survey markers entirely on-device. No data leaves the user's browser.
- Overview
- Architecture
- Supported File Formats
- Parsing Pipeline
- Data Storage
- Auto-Linking Engine
- Analysis Modules
- Survey Analysis Workflow
- Security Model
- Getting Started
- Deployment
- Tech Stack
LogAnalyzer ingests DJI flight logs (.txt, .dat, .bin), subtitle telemetry (.srt), RINEX GNSS observations (.obs, .nav), and survey geotag markers (.mrk) to produce interactive dashboards covering flight telemetry, battery health, anomaly detection, satellite visibility, and photogrammetric accuracy. Every computation runs inside the browser using Web Workers. Data is persisted locally in IndexedDB.
The application follows a layered architecture with strict separation between parsing, storage, analysis, and presentation.
┌─────────────────────────────────────────────────────────────┐
│ UI Layer │
│ Pages: Upload │ History │ Detail │ Compare │ Survey │ Fleet│
│ Components: MapPanel │ DataTable │ Toast │ Modal │ Sidebar │
├─────────────────────────────────────────────────────────────┤
│ Analysis Engine │
│ flight-summary │ anomaly-detector │ battery-health │
│ flight-comparator │ survey-accuracy │
├─────────────────────────────────────────────────────────────┤
│ Persistence Layer │
│ Dexie.js (IndexedDB) │ schema.js │ flight-store.js │
│ rinex-store.js │ query-engine.js │
├─────────────────────────────────────────────────────────────┤
│ Parser Suite │
│ dji-txt.parser │ dji-dat.parser │ srt.parser │
│ rinex.parser │ mrk.parser │ dji-txt.crypto │
├─────────────────────────────────────────────────────────────┤
│ Runtime Environment │
│ Browser │ Web Workers │ Web Crypto API │ IndexedDB │
└─────────────────────────────────────────────────────────────┘
src/
├── main.js # Router, page lifecycle, error boundary
├── components/ # Reusable UI components
│ ├── nav-sidebar.js # Navigation sidebar with hash-based routing
│ ├── map-panel.js # Leaflet map with trajectory & geotag layers
│ ├── file-dropzone.js # Drag-and-drop file upload zone
│ ├── toast.js # Notification system
│ └── modal.js # Confirmation dialogs
├── pages/ # Route-level page controllers
│ ├── upload.js # File ingestion and processing queue
│ ├── flight-history.js # Flight list with grid/table views
│ ├── flight-detail.js # Single-flight telemetry dashboard
│ ├── survey-analysis.js # RINEX + MRK survey quality dashboard
│ └── settings.js # API key, storage, export preferences
├── core/
│ ├── parsers/ # File format parsers (see below)
│ ├── database/ # IndexedDB schema and store modules
│ └── analysis/ # Offline analysis engines
├── styles/ # Global CSS design system
└── utils/ # Binary helpers, geo math, time formatting
| Extension | Format | Source | Content |
|---|---|---|---|
.txt |
DJI TXT Flight Log | DJI Fly / DJI GO / DJI Pilot app | Full telemetry: GPS, altitude, speed, battery, gimbal, RC inputs, motor status, warnings |
.dat / .bin |
DJI DAT Binary Log | Drone internal storage / DJI Assistant | High-resolution sensor data: raw OSD, battery cells, gimbal angles, RC channels |
.srt |
DJI SRT Subtitle | DJI camera video recordings | Per-frame GPS, altitude, ISO, shutter speed, focal length, color temperature |
.obs |
RINEX Observation | GNSS receiver / Base station | Satellite pseudorange, carrier phase, Doppler, signal strength per epoch |
.nav |
RINEX Navigation | GNSS receiver / Base station | Satellite ephemeris and orbit parameters |
.mrk |
DJI Survey Geotags | DJI Phantom 4 RTK / Matrice 300 RTK / PPK solutions | Per-image WGS84 coordinates, ellipsoidal height, attitude, positional standard deviations |
The TXT parser operates on raw ArrayBuffer input. The file layout is:
- Header (100 bytes): Contains the record area end offset, details area length, and a version byte at offset 10.
- Records Area (offset 100 to
recordAreaEnd): Sequential binary records, each structured as[type:u8][length:u8][payload][0xFF]. - Details Area: JSON metadata appended after the records area.
Each record payload is XOR-descrambled using a CRC-64/ECMA-182 lookup table. The scramble seed is derived from payload[0] ^ recordType. An 8-byte key is generated from the CRC-64 table entry at that seed index, and every byte in the payload is XOR'd against the repeating key.
Parsed record types:
| Type Code | Category | Fields Extracted |
|---|---|---|
0x01 |
OSD | lat, lng, altitude, height, speed, heading, satellites, fly state, flight action |
0x02 |
Home | home lat/lng/altitude |
0x03 |
Gimbal | pitch, roll, yaw |
0x04 |
RC Input | aileron, elevator, throttle, rudder |
0x06 |
Battery | voltage, current, capacity %, temperature, cell voltages |
0x07 |
App Tip | warning messages |
0x0A |
Motor | per-motor speed, status |
0x0D |
App GPS | app-reported GPS data |
After parsing, timestamps are attached at 100ms intervals based on OSD record sequence position.
Logs from firmware version 13 onward use AES-128-CBC encryption on the entire records area. The decryption workflow is:
- The parser detects
version >= 13from the header byte. - The file's fingerprint (SHA-256 hash) is computed and sent to the DJI Open Platform API (
https://openapi.dji.com/api/v1/flight-records/keychains). - The API returns an AES key and initialization vector.
- The Web Crypto API decrypts the records area using
AES-CBC. - The decrypted buffer is reassembled (header + decrypted records + remainder) and fed back into the standard parser.
Keychains are cached locally in IndexedDB to avoid repeated API calls for the same file.
DAT files use a simpler structure: length-prefixed records with 16-bit type identifiers and 16-bit length fields. The parser skips a 128-byte file header if the magic bytes match (0x0755 or 0x0306). Record types are:
0x0001— OSD (lat/lng stored asfloat64radians, converted to degrees)0x0002— Battery (voltage in centivolt units, current in centiamp units)0x0003— Gimbal (angles in decidegrees)0x0004— RC (raw stick values asint16)
The SRT parser splits the file by double-newlines into subtitle blocks. Each block contains:
- A sequence index
- A timecode line (
HH:MM:SS,mmm --> HH:MM:SS,mmm) - One or more telemetry lines embedded in
<font>tags
Telemetry is extracted using regex patterns for keys like latitude, longitude, altitude, iso, shutter, fnum, ev, ct, and focal_len.
The RINEX parser supports both v2.x and v3.x formats:
- Header parsing: Extracts version, file type, marker name, antenna type, approximate position, observation types, and first observation time.
- v2 epoch parsing: Reads epoch headers with 2-digit years (80+ = 1900s, <80 = 2000s), satellite lists packed 12 per line, and observation data at 5 values per line (16 characters each).
- v3 epoch parsing: Reads epoch headers prefixed with
>, 4-digit years, and per-satellite observation data on individual lines. - Derived metrics:
countConstellations()classifies satellites by SV prefix (G=GPS, R=GLONASS, E=Galileo, C=BeiDou, J=QZSS).estimatePDOP()computes a simplified geometric dilution estimate.
The MRK parser handles two distinct formats:
- Standard DJI format: Space-delimited columns —
index timestamp_gps longitude latitude altitude_ell altitude_baro roll pitch yaw filename [std_lng std_lat std_alt] - PPK solution exports: Detected by the presence of
,Lat,,Lon, and,Ellhmarkers in the data. The parser searches for these text markers to locate coordinate fields regardless of column order.
Additional robustness features:
- UTF-16 detection: Strips null bytes from improperly decoded files.
- Header skipping: Ignores lines starting with
#,//,photo, orindex. - GPS time conversion: Converts GPS seconds-of-week to UTC using the current GPS week estimate and a leap-second offset of 18s.
- CEP50 computation: Calculates horizontal position accuracy as
0.5887 * sqrt(sigma_lng^2 + sigma_lat^2).
All data is stored in the browser's IndexedDB via the Dexie.js ORM. The database is named LogAnalyzer and uses the following schema:
| Table | Primary Key | Indexes | Purpose |
|---|---|---|---|
flights |
flight_id |
drone_sn, start_time, end_time, *tags |
Core flight records with summary statistics |
telemetry |
auto-increment | flight_id, timestamp_ms, source_type |
OSD telemetry samples (100ms intervals) |
battery |
auto-increment | flight_id, timestamp_ms |
Battery voltage, current, cell data |
motors |
auto-increment | flight_id, timestamp_ms |
Per-motor RPM and status |
gimbal |
auto-increment | flight_id, timestamp_ms |
Gimbal pitch/roll/yaw |
rc_input |
auto-increment | flight_id, timestamp_ms |
RC stick positions |
warnings |
auto-increment | flight_id, timestamp_ms |
App warnings and tips |
survey_marks |
auto-increment | flight_id, image_num |
MRK geotag positions and accuracy |
rinex_files |
filename |
type, start_time, end_time, epoch_count |
RINEX file metadata and headers |
gnss_epochs |
auto-increment | flight_id, rinex_filename, time |
Per-epoch satellite counts, constellations, PDOP |
analysis_cache |
flight_id |
analysis_type |
Cached analysis results |
keychains |
log_hash |
— | Cached AES decryption keychains |
drones |
serial_number |
model, firmware |
Fleet inventory |
- Capacity: Browser-managed, typically 50–100 GB depending on available disk space (the UI displays a conservative 500 MB estimate).
- Privacy: All data remains on-device. No telemetry, flight paths, or survey coordinates are transmitted to any server.
- Deletion: The "Clear All Flight Data" function in Settings calls
db.delete()which removes the entire IndexedDB database file, followed bydb.open()to reinitialize the schema. - Storage estimate: Uses
navigator.storage.estimate()to report actual disk usage.
When flight logs and RINEX/survey files are uploaded independently, the system automatically correlates them using temporal overlap detection.
Upload Flight Log (.txt) Upload RINEX (.obs)
│ │
▼ ▼
Parse → Extract Parse → Extract
start_time, end_time start_time, end_time
│ │
▼ ▼
Store in `flights` Store in `rinex_files`
table + `gnss_epochs` tables
│ │
└──────────┬──────────────────────┘
▼
Auto-Link: Time Window Intersection
WHERE flight.start_time < rinex.end_time
AND flight.end_time > rinex.start_time
│
▼
Update gnss_epochs.flight_id
for all epochs within the flight's time window
The auto-linker runs in both directions:
- Flight uploaded first (
autoLinkFlightToRinex): Queriesrinex_filesfor any files whose time window overlaps the flight. Updates matchinggnss_epochsrecords with theflight_id. - RINEX uploaded first (
autoLinkRinexToFlights): Queriesflightsfor any flights whose time window overlaps the RINEX file. Updates matchinggnss_epochsrecords with theflight_id.
This means files can be uploaded in any order and the correlation is established automatically.
Computes aggregate statistics from parsed OSD and battery records:
- Duration, total distance (Haversine), max altitude (MSL and AGL)
- Max horizontal/vertical speed, max distance from home point
- Battery start/end percentage, minimum voltage, consumption rate
- Drone type identification from OSD record fields
- Path point extraction for map visualization
Scans telemetry streams against configurable thresholds:
| Anomaly | Threshold | Severity |
|---|---|---|
| Rapid voltage drop | > 0.5V in 5 seconds | Critical |
| Battery overtemperature | > 50°C | Warning |
| Cell imbalance | > 200mV delta | Warning |
| Sustained high current | > 25A for 10+ seconds | Warning |
| Low satellite count | < 6 satellites | Warning |
| Position jump | > 10m between samples | Warning |
| Altitude violation | > 120m AGL | Regulatory |
| Distance violation | > 5000m from home | Warning |
| Excessive descent rate | > 5 m/s | Warning |
Evaluates long-term battery condition from voltage curves, cell balance, internal resistance estimates, and charge cycle patterns.
Side-by-side analysis of two flight logs — compares altitude profiles, speed patterns, battery consumption, and distance metrics.
Generates a quality report from MRK geotags and RINEX epochs:
- Per-tag CEP50: Horizontal circular error probable at the 50th percentile.
- Aggregate statistics: Mean, P95, min/max for both horizontal and vertical accuracy.
- PDOP analysis: Min, max, mean, and P95 of position dilution of precision.
- Constellation timeline: Time-series satellite counts broken down by GPS, GLONASS, Galileo, BeiDou, and QZSS.
- Quality grading: A (CEP P95 <= 5cm), B (<= 10cm), C (single issue), D (multiple issues).
-
Upload RINEX files (
.obs/.nav) via the Upload page or the Survey page's "Load RINEX" button. Epochs are parsed, constellation counts and PDOP are computed per-epoch, and everything is stored in IndexedDB. -
Upload MRK file (
.mrk) via the Upload page or the Survey page's "Load MRK File" button. Geotags are parsed (supporting both standard DJI and PPK solution formats), stored in thesurvey_markstable, and displayed on the map. -
View the Survey dashboard. The page loads all available RINEX files and survey marks from the database. Clicking a RINEX file loads its epochs and computes the full survey report:
- KPI cards showing image count, quality grade, mean CEP50, and mean PDOP
- Interactive map with auto-zoom to the mission area
- Satellite count per constellation stacked area chart (D3.js)
- PDOP over time line chart with a threshold reference line
- Sortable geotag table with coordinates, altitude, and accuracy metrics
-
Auto-zoom: The map automatically calculates the bounding box of all geotag markers and fits the view to the mission area using
L.featureGroup().getBounds().
- Local-only data: All parsing, analysis, and storage happens in the browser. No flight data, GPS coordinates, or survey results are ever sent to a remote server.
- DJI API key: The only external API call is to the DJI Open Platform for fetching AES decryption keychains (v13+ logs only). The API key can be configured in two ways:
- Environment variable (
VITE_DJI_API_KEY): Set at build time or in the deployment platform. The Settings page shows "Using Global System Key" and disables manual input. - Per-user (localStorage): Users can enter their own key in Settings. Stored in
localStorage, never transmitted.
- Environment variable (
- Node.js 18+ with npm 9+
# Install dependencies
npm install
# Start development server
npm run devThe app will be available at http://localhost:5173.
| Variable | Required | Description |
|---|---|---|
VITE_DJI_API_KEY |
No | DJI Open Platform API key for v13+ log decryption. If not set, users can enter their own key in Settings. |
Create a .env file in the project root:
VITE_DJI_API_KEY=your_dji_open_api_key_here- Push the repository to GitHub.
- Import the project in Vercel.
- Add
VITE_DJI_API_KEYas an environment variable in the Vercel dashboard. - Deploy. The build command (
vite build) and output directory (dist/) are detected automatically.
npm run build # Produces optimized bundle in dist/
npm run preview # Serves the production build locally| Layer | Technology | Purpose |
|---|---|---|
| Build | Vite 5 | Development server, HMR, production bundling |
| UI | Vanilla JS | Zero-framework SPA with hash-based routing |
| Charts | Plotly.js | Interactive telemetry time-series charts |
| Charts | D3.js | Satellite constellation stacked area charts |
| Maps | Leaflet | Interactive map with trajectory and geotag layers |
| Database | Dexie.js | IndexedDB ORM with transactions and indexing |
| Crypto | Web Crypto API | AES-128-CBC decryption for v13+ logs |
| Testing | Vitest | Unit test runner |
| Command | Description |
|---|---|
npm run dev |
Start the Vite development server |
npm run build |
Build the production bundle to dist/ |
npm run preview |
Serve the production build locally |
npm run test |
Run the Vitest test suite |
This project is proprietary. All rights reserved.