Initial commit
commit
81d92887f6
|
@ -0,0 +1,104 @@
|
|||
## Description
|
||||
|
||||
Convert QGroundControl survey waypoints to DJI GO 4 waypoints 2.0.
|
||||
This is spefically intended to work _only_ with the "survey" mode from
|
||||
QGroundControl.
|
||||
|
||||
This will likely only work if your smartphone is rooted, as you will need `su`.
|
||||
My personal recommendation is to get `Magisk` installed.
|
||||
|
||||
This script is largely very naiive. It works as expected with `qgroundcontrol
|
||||
v4.0.11-1` and `DJI GO 4 v??` as of date 06-11-2020.
|
||||
|
||||
There is no guarantee it will continue to work with future updates of either
|
||||
application. You have been warned.
|
||||
|
||||
By default, a backup of the original DJI GO 4 database is made, so even if
|
||||
the resulting db is broken, you can always recover the original.
|
||||
|
||||
Has only been tested using `omnirom beryllium` - your mileage may vary.
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
For the script:
|
||||
- Python 3
|
||||
- all required modules are I believe part of python core
|
||||
- sqlite3
|
||||
|
||||
Obviously:
|
||||
- QGroundControl
|
||||
- DJI GO 4
|
||||
|
||||
|
||||
## Prerequisite
|
||||
|
||||
- Open QGroundControl
|
||||
- either with the app on your phone, or more easily with the
|
||||
desktop version. Tested with Linux and the App, but should be the same for Mac
|
||||
and Windoze
|
||||
- Create a survey waypoint mission
|
||||
- Make sure the camera lens settings are all correct
|
||||
- Use your common sense to determine
|
||||
- suitable overlap
|
||||
- cruising flight speed
|
||||
- suitable altitude
|
||||
- other?
|
||||
|
||||
Some settings can be passed directly to the script from the commandline, and
|
||||
will overwrite the equivalent values from QGroundControl.
|
||||
|
||||
*n.b.* This script currently ignores takeoff and landing points, and solely focusses
|
||||
on the survey mission waypoints, at a single altitude.
|
||||
|
||||
|
||||
## Preparation
|
||||
|
||||
It is assumed that you have a terminal open on your "host" in a suitable working
|
||||
directory, and that `qgc2dji.py` is in that directory.
|
||||
When copying the `.db` you can store it wherever you like - it just has to be a
|
||||
location that `adb pull` can retrieve from.
|
||||
Once your phone is rooted and reasonably under your own control (developer
|
||||
options enabled, USB debugging enabled):
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Use your own judgement when it comes to `chown` and `chmod` to match the original
|
||||
`.db`
|
||||
|
||||
```
|
||||
[host]$ adb shell
|
||||
|
||||
[phone]$ su
|
||||
[phone]# cp /data/data/dji.go.v4/databases/way_point_2.db /sdcard/
|
||||
[phone]# <ctrl-d>
|
||||
[phone]$ <ctrl-d>
|
||||
|
||||
[host]$ adb pull /sdcard/way_point_2.db ./
|
||||
[host]$ python ./qgc2dji.py -i <qgroundcontrol savefile> -d ./way_point_2.db
|
||||
[host]$ adb push ./way_point_2.db /sdcard/
|
||||
[host]$ adb shell
|
||||
|
||||
[phone]$ su
|
||||
[phone]# cd /data/data/dji.go.v4/databases
|
||||
[phone]# mv /sdcard/way_point_2.db ./
|
||||
[phone]# ls -ld
|
||||
drwxrwx--x 2 <user> <group> 4096 2020-11-06 11:48 .
|
||||
|
||||
[phone]# chown <user>:<group> ./way_point_2.db
|
||||
[phone]# chmod 660 ./way_point_2.db
|
||||
```
|
||||
|
||||
|
||||
## Future
|
||||
|
||||
- Also update 'way_point_2_cache.db'
|
||||
- Choice of "action upon mission completion"
|
||||
- Currently defaults to hover
|
||||
- Altitude per waypoint
|
||||
- Takeoff and landing points?
|
||||
- Calculate appropriate crusing flight speed?
|
||||
- Dependent on required overlap and limitation of camera regarding photo
|
||||
interval
|
||||
- Waypoint triggers (start/stop recording, take photo, etc)
|
|
@ -0,0 +1,255 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# Convert QGroundControl survey waypoints to DJI GO 4 waypoints 2.0
|
||||
#
|
||||
# Solely for the creation of terrain survey mapping
|
||||
# at a fixed altitude.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import argparse
|
||||
import time
|
||||
import math
|
||||
import random
|
||||
import sqlite3
|
||||
import json
|
||||
|
||||
|
||||
####### Globals ################################################################
|
||||
|
||||
mission_table = 'dji_pilot_dji_groundstation_waypoint2_model_WayPoint2MissionDBModel'
|
||||
waypoint_table = 'dji_pilot_dji_groundstation_waypoint2_model_WayPoint2MissionDBModel$WayPoint2DBPoint'
|
||||
|
||||
|
||||
####### Helper Function(s) #####################################################
|
||||
|
||||
def haversine(coord1, coord2):
|
||||
R = 6371000 #6372800 # Earth radius in meters [looks like DJI GO 4 uses 6371000]
|
||||
lat1, lon1 = coord1
|
||||
lat2, lon2 = coord2
|
||||
|
||||
phi1, phi2 = math.radians(lat1), math.radians(lat2)
|
||||
dphi = math.radians(lat2 - lat1)
|
||||
dlambda = math.radians(lon2 - lon1)
|
||||
|
||||
a = math.sin(dphi/2)**2 + \
|
||||
math.cos(phi1) * math.cos(phi2) * math.sin(dlambda/2)**2
|
||||
|
||||
return 2 * R * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||
|
||||
|
||||
################################################################################
|
||||
|
||||
|
||||
# TODO: Confirm that 'complexItemType' = "survey"
|
||||
# TODO: Also update 'way_point_2_cache.db'
|
||||
# - delete existing cached mission, and its' waypoints
|
||||
# - insert the same new data as with 'way_point_2.db'
|
||||
|
||||
parser = argparse.ArgumentParser(description='Convert QGroundControl survey waypoints to DJI GO 4 waypoints 2.0')
|
||||
|
||||
parser.add_argument('-d', '--dbfile', required=True, type=str,
|
||||
help='DJI GO 4 database file')
|
||||
parser.add_argument('-i', '--infile', required=True, type=argparse.FileType('r'),
|
||||
help='JSON input file')
|
||||
parser.add_argument('-a', '--altitude', type=float,
|
||||
help='Altitude above ground, in meters')
|
||||
parser.add_argument('-f', '--finishaction', type=int, default=4,
|
||||
help='Act to perform upon mission completion [0, 1, 2, 3 or 4 - hover] (default: %(default)s)')
|
||||
parser.add_argument('-n', '--name', type=str,
|
||||
help='Name for the mission (qgc2dji-survey_<date-time>)')
|
||||
parser.add_argument('-s', '--speed', type=float,
|
||||
help='Flight speed')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.isfile(args.dbfile):
|
||||
print("[ERROR] Database file does not exist!", file=sys.stderr)
|
||||
exit(2)
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(args.dbfile)
|
||||
except sqlite3.Error as e:
|
||||
print("[ERROR] Could not connect to database!\n" + e.args[0], file=sys.stderr)
|
||||
exit(2)
|
||||
|
||||
|
||||
# The only values we care about are inside "TransectStyleComlexItem"
|
||||
items = json.loads(args.infile.read())['mission']['items']
|
||||
|
||||
main_data = None
|
||||
for item in items:
|
||||
if 'TransectStyleComplexItem' in item:
|
||||
main_data = item["TransectStyleComplexItem"]
|
||||
break
|
||||
if not main_data:
|
||||
print("[ERROR] Required data not present in json!", file=sys.stderr)
|
||||
exit(2)
|
||||
|
||||
altitude = args.altitude
|
||||
if not altitude:
|
||||
altitude = main_data["CameraCalc"]["DistanceToSurface"]
|
||||
|
||||
finishaction = args.finishaction
|
||||
|
||||
# If this is no set, it will be later on during parsing
|
||||
flight_speed = args.speed
|
||||
if not flight_speed:
|
||||
# First check if a value was set in the json
|
||||
for item in items:
|
||||
if 'command' in item and item['command'] == 178:
|
||||
# Reverse-engineered this location - could easily change in future QGroundControl releases
|
||||
flight_speed = item['params'][1]
|
||||
break
|
||||
# Set an acceptable, but slow default if speed still hasn't been set
|
||||
# TODO: Calculate this based on mission distance / photo count / 5s to get one photo per 5seconds ???
|
||||
if not flight_speed:
|
||||
flight_speed = 3.5
|
||||
|
||||
mission_name = args.name
|
||||
if not mission_name:
|
||||
timestr = time.strftime('%d%m%Y-%H%M%S')
|
||||
mission_name = "qgc2dji-survey_" + timestr
|
||||
|
||||
# DJI GO database stores time in milliseconds since epoch
|
||||
update_time = int(round(time.time() * 1000))
|
||||
|
||||
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute(f'SELECT missionId FROM {mission_table}')
|
||||
existing_ids = cur.fetchall()
|
||||
|
||||
# Looking at existing database entries, it appears that 'missionId' is an arbitrary value
|
||||
mission_id = random.randrange(10000,99999)
|
||||
while (mission_id,) in existing_ids:
|
||||
print("[INFO] ID collision. Generating new ID.")
|
||||
mission_id = random.randrange(10000,99999)
|
||||
|
||||
waypoint_queue = []
|
||||
|
||||
# Set default values for DJI GO database waypoint table
|
||||
heading_type = 0 # Free ?
|
||||
poi_index = -1 # None ?
|
||||
heading = 0 # Look-ahead ?
|
||||
action = 0 # Do nothing (as opposed to take photo, etc)
|
||||
pitch = -90.0 # Point camera toward ground
|
||||
radius = 2.0 # Some default ? Could be realted to 'arc' vs 'polyline'
|
||||
speed = 0.0 # Use 'autoFlightSpeed' from 'mission_table'
|
||||
|
||||
# Prepare each waypoint for insert
|
||||
waypoints = main_data['VisualTransectPoints']
|
||||
last_coord = ()
|
||||
curr_coord = ()
|
||||
accum_distance = 0
|
||||
|
||||
# TEST DATA - Expected accumulated result: 1825.33557128906
|
||||
# Result initially was 1825.8512451494107 which should be close enough
|
||||
# Result with "standard" Earth radius is 1825.3355327088402 which is closer still
|
||||
# TODO: Test geopy module ? [see: https://janakiev.com/blog/gps-points-distance-python/]
|
||||
#waypoints = [(-19.9914615912953,57.6010317688705),
|
||||
# (-19.9906124706645,57.6018561685789),
|
||||
# (-19.9907586941667,57.6021400677547),
|
||||
# (-19.9917566018806,57.6012092059048),
|
||||
# (-19.9920208284113,57.6013183979067),
|
||||
# (-19.9908972216132,57.6023884794603),
|
||||
# (-19.9910049650248,57.6026669190904),
|
||||
# (-19.9923184032966,57.601438509184),
|
||||
# (-19.9925544106552,57.6017169485915),
|
||||
# (-19.9911050125569,57.6029944951315),
|
||||
# (-19.9912281477626,57.6033111519851),
|
||||
# (-19.9927904174512,57.6019353328111),
|
||||
# (-19.9931793793255,57.6022154786425),
|
||||
# (-19.9915212340617,57.6036721688515),
|
||||
# (-19.9915956280736,57.6039779066684),
|
||||
# (-19.9934298150556,57.6024819755698)]
|
||||
|
||||
count = 0
|
||||
for waypoint in waypoints:
|
||||
latitude = waypoint[0]
|
||||
longitude = waypoint[1]
|
||||
waypoint_id = count
|
||||
count = count + 1
|
||||
|
||||
waypoint_queue.append((heading_type, altitude, mission_id, poi_index, heading, latitude, action, pitch, radius, speed, longitude, waypoint_id))
|
||||
|
||||
if count > 1:
|
||||
curr_coord = (latitude, longitude)
|
||||
accum_distance = accum_distance + haversine(last_coord, curr_coord)
|
||||
last_coord = curr_coord
|
||||
else:
|
||||
last_coord = (latitude, longitude)
|
||||
|
||||
|
||||
# Set default values for DJI GO database mission table
|
||||
is_use_custom_direction = 0
|
||||
first_lon = 0.0
|
||||
first_lat = 0.0
|
||||
local = ''#None # becomes NULL in sqlite db
|
||||
exit_mission_on_rc_lost = 0 # Keep flying
|
||||
flight_path_mode = 0 # 'polyline' vs 'arc' ?
|
||||
is_cache = 0 # ?
|
||||
rotate_gimbal_pitch = 1 # ?
|
||||
goto_first_waypoint_mode = 0 # ?
|
||||
repeat_times = 1 # Only fly the mission once
|
||||
max_flight_speed = 8.3 # (m/s) Based on ~30km/h being a good maximum
|
||||
heading_mode = 0 # ? Different from 'heading_type' ?
|
||||
is_enable_multi_poi = 0
|
||||
route_distance = accum_distance #'' #None # Can we get away with leaving this blank !?
|
||||
point_count = len(waypoints)
|
||||
|
||||
|
||||
print("\n======= MISSION DATA =======\n")
|
||||
print(f"Mission name:\t\t{mission_name}")
|
||||
print(f"Mission id:\t\t{mission_id}")
|
||||
print(f"Max flight speed:\t{max_flight_speed} m/s")
|
||||
print(f"Cruise speed:\t\t{flight_speed} m/s")
|
||||
print(f"Altitude:\t\t{altitude} m")
|
||||
print(f"Route distance:\t\t{route_distance} m")
|
||||
print(f"Finished action:\t{finishaction}")
|
||||
print("")
|
||||
|
||||
# TODO: Implement more robust regex version of the following
|
||||
proceed = ''
|
||||
while proceed not in ['y', 'Y', 'n', 'N']:
|
||||
proceed = input("Write out to database? [y/n] ")
|
||||
|
||||
if proceed == 'n' or proceed == 'N':
|
||||
print("[INFO] Exiting without updating database.")
|
||||
exit(1)
|
||||
|
||||
|
||||
# Generate SQL INSERT for the mission
|
||||
sql = (f"INSERT INTO {mission_table}"
|
||||
"(missionId, isUseCustomDirection, updateTime, finishedAction, firstLng, local, "
|
||||
"exitMissionOnRCSignalLost, flightPathMode, isCache, rotateGimbalPitch, "
|
||||
"gotoFirstWaypointMode, pointCount, repeatTimes, routDistance, firstLat, " # n.b. 'routDistance' is not a spelling mistake
|
||||
"missionName, maxFlightSpeed, headingMode, autoFlightSpeed, isEnableMultiPOI) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)")
|
||||
cur.execute(sql, (mission_id, is_use_custom_direction, update_time, finishaction,
|
||||
first_lon, local, exit_mission_on_rc_lost, flight_path_mode,
|
||||
is_cache, rotate_gimbal_pitch, goto_first_waypoint_mode, point_count,
|
||||
repeat_times, route_distance, first_lat, mission_name, max_flight_speed,
|
||||
heading_mode, flight_speed, is_enable_multi_poi))
|
||||
|
||||
# SQL INSERT for all waypoints from QGroundControl
|
||||
sql = (f"INSERT INTO {waypoint_table}"
|
||||
"(headingType, altitude, missionId, poiIndex, heading, latitude, action, "
|
||||
"pitch, radius, speed, longitude, myIndex) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)")
|
||||
cur.executemany(sql, waypoint_queue)
|
||||
|
||||
|
||||
# TODO: Implement database backup as a commandline option
|
||||
shutil.copy2(args.dbfile, args.dbfile + ".bak")
|
||||
|
||||
|
||||
# Write changes back to the database
|
||||
conn.commit()
|
||||
|
||||
|
||||
# TODO: Determine if there is any other clean up
|
||||
|
||||
|
||||
conn.close()
|
Loading…
Reference in New Issue