0.前言

我们在使用DJI无人机进行某种应用的实验的时候,很容易出现的一个场景是:不仅仅需要无人机记录的摄像机拍摄的视频,还需要某次飞行的具体数据,例如在飞行的采样时刻时的飞行速度、高度、经纬度、偏航,滚转,俯仰角等信息。这些信息通常并不是直接显式地、简单地能够通过DJI提供的简单用户界面,例如通过DJI FLY APP就能够获取的,需要我们通过开发者套件去得到具体的飞行数据。

0.1 实验环境

本文实验的环境:

  • 无人机型号:Mavic 3
  • 手机信息:Android 13安装的最新版本DJI FLY APP*

本文以下的行文皆默认在此实验环境下讨论。

*注:使用第三方或者iOS端的DJI遥控飞行APP会产生与本文介绍的飞行记录数据格式§1.1 手机端DJI TXT logs不同,如果读者的环境与本文不符,请参考本节§1.3 本节其他参考文献 末尾的的参考文献。

1.飞行数据类型

DJI Drone在飞行过程中,在不同的设备载体中可能会存在不同的飞行记录(Flight log)。

值得注意的是,DJI Drone产品的推出持续了一定的年份。在讨论DJI的飞行数据时,不得不考虑实验设备差异的问题,即不同型号和不同固件版本的无人机与遥控器、不同手机操作系统的手机、不同的飞行APP(DJI推出的用于操控无人机的手机APP有数次变更,同时DJI允许第三方APP通过DJI SDK开发飞行APP)等也会在不同的时间阶段产生不同的飞行数据,针对它们的解密方法五花八门。在这里我们只讨论Mavic3之后的较新款的DJI Drone的飞行数据类型。其实对于旧款的无人机相对来说解密的办法是更加简单的,不必过于担心本文方法的普适性不足的问题。

DJI Drone在某次飞行结束后产生的飞行记录的类型是较多的,但是我们主要关注如下几个:

  1. 手机端DJI TXT logs;
  2. 飞行器DAT文件(黑匣子数据)。

1.1 手机端DJI TXT logs

这个是我们唯一能够解密的飞行数据信息。

关于该TXT的信息简介如下:

  • 数据格式:加密.txt文件
  • 创建方式:DJI SDK
  • 记录范围:电机启动到停止(或丢失信号的最后时刻)
  • 采样频率:0.2s/次,10Hz
  • 传输链路:无人机到遥控器到手机
  • 数据类型:IMU计算后的数据,非传感器原始数据
  • 信息类型:采样时间、设备,电池信息、飞行距离、飞行速度、飞行高度、经纬度、偏航,滚转,俯仰角等
  • 缺点:采样频率较低,无传感器原始数据
  • 空间占用:数MB

1.1.1 获取方法

按照本文的实验环境,安卓端的DJI FLY APP的飞行记录保存在 Android\data\dji.go.v5\files\FlightRecord

LOG的命名具有规律,例如:DJIFlightRecord_2023-09-21_[15-21-40].txt,包含有飞行起始时间。

如需了解其他版本或手机的更多获取方法,请参考网址:

1.1.2 具体信息

通过DJI SDK解密或者通过PhantomHelp or AirData网站上传飞行记录可获取到飞行记录的明文。

这里简单节选个别重要参数介绍一下飞行记录包含的项目。

飞行记录明文在结构上主要分为两个一级项目:

  • summary:飞行器的硬件信息以及本次飞行总览和起始信息;
  • info:以0.2s/次为采样频率采集的当前采样时刻的具体数据。

1.1.2.1 summary:

summary下二级项目 三级项目1 三级项目2 备注
batteriesInformation firmwareVersion serialNumber
camerasInformation firmwareVersion serialNumber
gimbalsInformation firmwareVersion serialNumber
remoteControllerInformation firmwareVersion serialNumber
flightControllerInformation firmwareVersion serialNumber
startTime \ \ Unix 时间戳
startCoordinate latitude longitude
totalDistance \ \ 英尺
totalTime \ \
samplingRate \ \ 采样率=10
maxHeight \ \
maxHorizontalSpeed \ \ 米每秒
maxVirticalSpeed \ \ 米每秒

其中第一行指的是summary这个一级项目下的部分二级项目,表格的列代表当前二级项目下包含的三级项目。

为了方便理解,以电池信息这个二级项目为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

"batteriesInformation": {
"0": {
"index": 0,
"firmwareVersion": [
8,
75,
2,
9
],
"serialNumber": "4********09V3"
}
}

*注:为简单起见,只列出了固件版本和序列号信息,并且把电池编号“0”去掉了,把原本是四级项目的两个项目"firmwareVersion"和"serialNumber"当做三级项目了。

1.1.2.2 info:

该一级项目的分项目极多,取决于你的飞行时间。我们取某次采样时刻的json信息节选部分如下:

某采样时刻下的二级项目 备注
flightControllerState attitude \ attitude包括pitch, yaw, roll
homeLocationCoordinate latitude longitude 精度足够,参考见表格下方json参考
takeoffLocationAltitude \ \
aircraftLocation latitude longitude 精度足够,参考见表格下方json参考
altitude \ \
flightMode \ \
GPSSignalLevel \ \
satelliteCount \ \ 卫星数量
isFlying \ \
areMotorsOn \ \
velocity \ \ 通过IMU计算的速度,包括X, Y, Z速度
flightTimeInSeconds \ \ 飞行时间,单位秒
cumulativeFlightDistance \ \
cameraState mode remainingSpaceInMB
gimbalState atitude \ attitude包括pitch, yaw, roll

为了方便理解,以某个采样时刻的json信息为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210

"flightControllerState": {
"attitude": {
"pitch": 4,
"roll": 1,
"yaw": 407
},
"homeLocationCoordinate": {
"latitude": 12.026330289426021,
"longitude": 47.76282816459477
},
"takeoffLocationAltitude": 4164.42188,
"aircraftLocation": {
"latitude": 12.026340733207853,
"longitude": 47.7628277001368
},
"altitude": 9.8,
"flightMode": "GPSAtti",
"GPSSignalLevel": "Level5",
"satelliteCount": 22,
"remainingFlightTime": 0,
"batteryPercentageNeededToLandFromCurrentHeight": 0,
"batteryPercentageNeededToGoHome": 0,
"smartRTHState": "Unknown",
"behavior": "GoHome",
"isFailsafeEnabled": false,
"areMotorsOn": true,
"isHomeLocationSet": true,
"isLandingConfirmationNeeded": false,
"hasReachedMaxFlightHeight": false,
"hasReachedMaxFlightRadius": false,
"windWarning": "Level0",
"countOfFlights": 1,
"flightLogIndex": 102,
"isFlying": true,
"smartRTHCountdown": 0,
"velocity": {
"velocityX": 0,
"velocityY": 0,
"velocityZ": 0
},
"isGPSBeingUsed": true,
"flightTimeInSeconds": 28.399999618530273,
"cumulativeFlightDistance": 7.3180433909594313
},
"cameraState": {
"isRecording": false,
"isShootingSinglePhoto": false,
"isInserted": true,
"isInitializing": false,
"hasError": false,
"isVerified": true,
"isFull": true,
"isFormatted": true,
"isFormatting": false,
"isInvalidFormat": false,
"isReadOnly": false,
"totalSpaceInMB": 121660,
"remainingSpaceInMB": 17492,
"availableCaptureCount": 0,
"availableRecordingTimeInSeconds": 1358,
"index": 0,
"mode": "RecordVideo"
},
"gimbalState": {
"atitude": {
"pitch": -32.400001525878906,
"roll": 0,
"yaw": 38.600002288818359
},
"fineTunedRoll": 0,
"fineTunedPitch": 0,
"fineTunedYaw": 0,
"isRollAtStop": false,
"isYawAtStop": false,
"isPitchAtStop": false,
"yawRelativeToAircraftHeading": 0,
"mode": "YawFollow",
"index": 0
},
"rcHardwareState": {
"leftStick": {
"horizontalPosition": 0,
"verticalPosition": 0
},
"rightStick": {
"horizontalPosition": 0,
"verticalPosition": 0
},
"leftWheel": 0,
"rightWheel": {
"isPresent": true,
"isTurned": false,
"isClicked": false,
"value": 0
},
"flightModeSwitch": "Two",
"goHomeButton": {
"isPresent": true,
"isClicked": false
},
"recordButton": {
"isPresent": true,
"isClicked": false
},
"shutterButton": {
"isPresent": true,
"isClicked": false
},
"style": "Unknown"
},
"batteryState": {
"voltage": 15650,
"current": -7437,
"temperature": 35.1,
"cellVoltages": [
3917,
3918,
3914,
3902
],
"chargeRemainingInPercent": 63,
"lowBatteryWarningThreshold": -1,
"seriousLowBatteryWarningThreshold": -1,
"index": -1,
"lifetimeRemaining": 0,
"designCapacity": 1280000,
"numberOfDischarges": 3840,
"isInSingleBatteryMode": false,
"fullChargeCapacity": 4717,
"chargeRemaining": 2941
},
"airLinkState": {
"downlinkSignalQuality": 0,
"hasDownlinkSignalQuality": false,
"uplinkSignalQuality": 0,
"hasUplinkSignalQuality": false
},
"visionState": {
"collisionAvoidanceEnabled": true,
"controlState": {
"isAscentLimitedByObstacle": false,
"isAvoidingActiveObstacleCollision": false,
"isBraking": false,
"isPerformingPrecisionLanding": false
},
"detectionStateMap": {}
},
"gimbalsState": {
"0": {
"atitude": {
"pitch": -32.400001525878906,
"roll": 0,
"yaw": 38.600002288818359
},
"fineTunedRoll": 0,
"fineTunedPitch": 0,
"fineTunedYaw": 0,
"isRollAtStop": false,
"isYawAtStop": false,
"isPitchAtStop": false,
"yawRelativeToAircraftHeading": 0,
"mode": "YawFollow",
"index": 0
}
},
"camerasState": {
"0": {
"isRecording": false,
"isShootingSinglePhoto": false,
"isInserted": true,
"isInitializing": false,
"hasError": false,
"isVerified": true,
"isFull": true,
"isFormatted": true,
"isFormatting": false,
"isInvalidFormat": false,
"isReadOnly": false,
"totalSpaceInMB": 121660,
"remainingSpaceInMB": 17492,
"availableCaptureCount": 0,
"availableRecordingTimeInSeconds": 1358,
"index": 0,
"mode": "RecordVideo"
}
},
"batteriesState": {
"0": {
"voltage": 15650,
"current": -7437,
"temperature": 35.1,
"cellVoltages": [
3917,
3918,
3914,
3902
],
"chargeRemainingInPercent": 63,
"lowBatteryWarningThreshold": -1,
"seriousLowBatteryWarningThreshold": -1,
"index": -1,
"lifetimeRemaining": 0,
"designCapacity": 1280000,
"numberOfDischarges": 3840,
"isInSingleBatteryMode": false,
"fullChargeCapacity": 4717,
"chargeRemaining": 2941
}
}

可见经纬度和时间信息可以满足精度要求。

*注:为保护隐私,举例的信息数值中的经纬度系随机伪造,非真实信息。

1.2 飞行器DAT文件(黑匣子数据)

飞行器DAT文件包含有包括传感器的原始数据的完整的信息,文件很大,大概在几GB左右,但是较新的机型无法解密。唯一能够解密的是DJI,因包含一些机密信息,DJI不可能提供解密方法。

因此总结一句话,该文件对我们没有任何帮助。除非你的飞行器发生了意外需要DJI协助的时候。

1.3 本节其他参考文献

2.解密方法

实际的解密方法有很多,也很简单,如果不担心飞行记录泄密,大可在PhantomHelpAirData 上传你的飞行记录。如果你希望利用DJI SDK离线解密,可以参考如下方法。

本文实验环境:

  • PC操作系统:Ubuntu 22.04
  • 材料:准备好某次飞行log:DJIFlightRecord_2023-09-21_[15-21-40].txt

2.1 DJI SDK:FlightRecordParsingLib

利用DJI SDK中的FlightRecordParsingLib示例程序即可轻松解密。

仓库地址:

2.2 解密步骤

主要介绍在Ubuntu下解密的步骤,docker的方法其实就是创建一个Ubuntu镜像运行该脚本,异曲同工。

  1. 注册DJI开发者账号并获取APP KEY

    DJI开发者注册
    DJI开发者注册

    🔗开发者注册链接->点击此处

  2. 克隆项目:

  3. 安装相关依赖:

    *注:缺乏相关依赖会无法编译,按照官方库的参考文档,会因缺少 `libcurl4-openssl-dev` 和 `zlib1g-dev` 而无法编译。
  4. 编译

    1
    2
    3
    cd dji-flightrecord-kit/build/Ubuntu/FRSample
    sh generate.sh
    ./FRSample
  5. 导出
    首先在终端导入你的APP KEY:

    1
    export SDK_KEY=your_APP_KEY

    导出解密明文:

    1
    ./FRSample DJIFlightRecord_2023-09-21_[15-21-40].txt > output.json

最后检查导出的文件即可。

2.3 本节参考文献

  1. https://github.com/dji-sdk/FlightRecordParsingLib/issues/8
  2. https://github.com/dji-sdk/FlightRecordParsingLib/issues/13

3. 小结

本文简单介绍了DJI无人机飞行记录的类型和形式,罗列了关键的部分项目,并提供了部分解释和例子。另外,介绍了使用DJI SDK离线解密DJI飞行记录的方法,修正了官方SDK仓库使用文档的问题,能够在Ubuntu和Docker下运行解密。


本站由 @JasonYip 使用 Stellar 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。