Close
0%
0%

DJI FPV - Battery Breakout Mod [2]: Communication

Demystifying the DJI communication protocol
DUML - DJI Universal Markup Language

Similar projects worth following
I learned a lot in the first part of this project. Above all, things are often not as simple as they first seem. It became clear that simply separating the battery cells from the smart control board and making them changeable would not be enough. It became apparent that the communication between the drone and the battery has to be manipulated. Here I present and discuss some insights to this communication.



YouTube: https://www.youtube.com/@AirCruiserFPV
Discord: https://discord.gg/Yfan8F3epY
Mail: aircruiserfpv+hackaday@gmail.com

The communication interface

As said, there were some rumors that the CAN bus is used for the communication between the battery and the drone. So I ordered a cheap logic analyzer and a suitable bridge circuit (CAN <-> serial) to analyze the communication and maybe mimic it. While waiting, I got my hands on an oscilloscope and wanted to check if these rumors are really true. But the measured signals made me a bit suspicious...

The CAN bus is a new topic for me, but all the schematics I've seen so far looked a little different. In these schemes, CAN HIGH never went to ground. In addition, the low maximum voltage of approx. 3.2 V made me a little nervous, since the bridge circuit I ordered had an operating voltage of 5 V. As I was told that the CAN bus should be tolerant to 5V and what matters is that the difference between CAN HIGH and CAN LOW is at least 2V in the dominant state.

But I was still afraid of frying my drone/battery so I decided to take a closer look at the smart battery control board. Unfortunately, most ICs were covered with a coating that made the labeling very difficult to read. After a while I could figure out all but one of the ICs. And none of them were capable of CAN bus communication. So it had to be the last one. I was able to gently scrape off the coating with a scalpel without damaging the label. After this I could recognize a SN65HVD75 which is a RS-485 communication IC. Ok, RS-485 instead of CAN. Very similar but not quite the same.

With the help of the manual (https://www.ti.com/lit/ds/symlink/sn65hvd78.pdf) the communication pins A and B could be identified. As assumed, these pins are connected directly to the drone <-> battery connector.

Using my logic analyzer I was able to easily measure the signal on pin A. Interpreted as asynchronous serial communication (115.2 kbit/s, 8bits, 1 stop bit, no parity, inverted/non-inverted depending if you use pin A or B) it turned out to be DJI's duml protocol.



And now comes the hard part

With that I could read all communication between the smart battery and the drone. So far so good... But the goal was to mimic the communication so that I can tell the drone what I want and the drone will accept that. To do this, I needed to be able to read and understand the drone's requests and to respond accordingly. Also, I needed to find out if there are any requests from the battery to the drone that the drone is expecting but don't ask for, "pings", etc.

Sounds like routine work, but the problem was that the duml protocol is widely undocumented (no official documentation). It is mostly reverse engineered knowledge that is not complete. To dig a little deeper, I recorded 12 minutes of drone <> battery communication (16209 duml messages, downloadable in the files section of this project). Wireshark which was suggested to me is a great tool to help decoding the messages. But there are still many unknown CMDIDs, data fields that I can't be decoded, etc.

One example: A request from FLYC to Battery with the CMDID GetPushDynamicData (55110492030bb126400d02000000004b03) followd by ACK from Battery to FLYC with the datafield 0000765300006cfcffff7c070000cc0100004801061900000000030000001301640000470281020000" (5536043d0b03b126800d020000765300006cfcffff7c070000cc010000480106190000000003000000130164000047028102000064e4).

So the question is: What does 0000765300006cfcffff7c070000cc0100004801061900000000030000001301640000470281020000 mean? Battery status? Battery voltage? LiPo cell voltages?

Wireshark tries to decode the data field, but returns unrealistic values. The warning is another indication that something is out of date...


Due to the limited space I have to split up here. So check out the logs for the rest of the project :)

plain - 1.19 MB - 11/11/2022 at 21:23

Download

plain - 909.08 kB - 11/11/2022 at 21:23

Download

  • Log 2: Finally, coming to the point. The important commands...

    AirCruiser11/11/2022 at 22:00 0 comments

    As I mentioned before, the authentication process is a big and still unsolved problem. Without proper authentication, the drone will not start the motors. But there is a workaround that I will discuss in detail in the second part of this project. So let's focus on the other parts of the communication. Sure, all messages are important in some way. But it seems most of them don't need to be understood. Using recorded data fields seems to be enough to respond to requests from the drone without understanding all the details (or even a bit). Of course, replying the entire recorded duml message is not enough. Why? The sequence numbers of most drone requests change, but it is important that the request and reply sequence numbers are the same, so they need to be adjusted. And even if a single bit of the message is changed, the checksum must also be recalculated. 


    Hashing the duml

    Since it is important to implement hashing correctly, I will say a few words about it. As mentioned in part 2, the hashes of the header and the entire duml message are CRC8 and CRC16 checksums with custom initial values and non-standard hexadecimal lookup tables. So most of the standard libraries won't work (I guess).

    Based on the sources mentioned, I wrote the following C++ code to calculate the checksum of the header. As you can see, the custom initial value is 119 and the non-default hexadecimal lookup table is {0, 94, ... , 53}. The duml message is passed to this function through the array of integers buff. CRC8 is only used to calculate the checksum of the header, so only the first 3 bytes are hashed. 

    int CalcCRC8(int buff[]){
      int value     = 119;
      int crc8      = 0;
      int LUTCRC8[] = {0, 94, -68, -30, 97, 63, -35, -125, -62, -100, 126, 32, -93, -3, 31, 65, -99, -61, 33, 127, -4, -94, 64, 30, 95, 1, -29, -67, 62, 96, -126, -36, 35, 125, -97, -63, 66, 28, -2, -96, -31, -65, 93, 3, -128, -34, 60, 98, -66, -32, 2, 92, -33, -127, 99, 61, 124, 34, -64, -98, 29, 67, -95, -1, 70, 24, -6, -92, 39, 121, -101, -59, -124, -38, 56, 102, -27, -69, 89, 7, -37, -123, 103, 57, -70, -28, 6, 88, 25, 71, -91, -5, 120, 38, -60, -102, 101, 59, -39, -121, 4, 90, -72, -26, -89, -7, 27, 69, -58, -104, 122, 36, -8, -90, 68, 26, -103, -57, 37, 123, 58, 100, -122, -40, 91, 5, -25, -71, -116, -46, 48, 110, -19, -77, 81, 15, 78, 16, -14, -84, 47, 113, -109, -51, 17, 79, -83, -13, 112, 46, -52, -110, -45, -115, 111, 49, -78, -20, 14, 80, -81, -15, 19, 77, -50, -112, 114, 44, 109, 51, -47, -113, 12, 82, -80, -18, 50, 108, -114, -48, 83, 13, -17, -79, -16, -82, 76, 18, -111, -49, 45, 115, -54, -108, 118, 40, -85, -11, 23, 73, 8, 86, -76, -22, 105, 55, -43, -117, 87, 9, -21, -75, 54, 104, -118, -44, -107, -53, 41, 119, -12, -86, 72, 22, -23, -73, 85, 11, -120, -42, 52, 106, 43, 117, -105, -55, 74, 20, -10, -88, 116, 42, -56, -106, 21, 75, -87, -9, -74, -24, 10, 84, -41, -119, 107, 53};
      
      for (int i=0;i<3;i++){
        value = LUTCRC8[(value^buff[i]) & 255];
      }
      
      crc8 = value & 0xff;
      
      return crc8;
    }

    My version of the code to calculate the CRC16 checksum of the entire duml looks similar to the CRC8 code but is slightly different. We also have a custom initial value (13970) and a non-default hexadecimal lookup table ({0, 4489, ... , 3960}). But now the checksum is calculated over the whole duml. Except for the last two bytes which represent the CRC16 checksum encoded in the duml. Since the transmitted checksum is split into two bytes, I also split the resulting calculated checksum into two bytes to simplify the comparison. But that is a matter of taste and could also be done differently.

    void CalcCRC16(int buff[], int crc16[]){
      int value      = 13970;
      int LUTCRC16[] = {0, 4489, 8978, 12955, 17956, 22445, 25910, 29887, 35912, 40385, 44890, 48851, 51820, 56293, 59774, 63735, 4225, 264, 13203, 8730, 22181, 18220, 30135, 25662, 40137, 36160, 49115, 44626, 56045, 52068, 63999, 59510, 8450, 12427, 528, 5017, 26406, 30383, 17460, 21949, 44362, 48323, 36440, 40913, 60270, 64231, 51324...
    Read more »

  • Log 1: Learn to speak dronish

    AirCruiser11/11/2022 at 21:55 0 comments

    I'v recorded many drone <> battery communications, trying to decode the vocabulary and grammar. As mentioned, https://b3yond.d3vl.com/duml/ is a good place to start to decode the DUML (DJI Universal Markup Language) messages sent between the drone and the battery. A major disadvantage of this website is that the data fields cannot be decoded. So you are told what the subject of the message is (e.g. battery request), but not the exact information that is being transmitted. Furthermore, since there is no official documentation of DUML and all knowledge about it is reverse engineered, this website is unable to decrypt every message. But this is a common problem of all sources I found.

    In this log, I don't want to discuss every aspect of DUML in detail. If you want to learn more about it, there are some really great resources. For example, a master's thesis by Thomas Christof and a related blog: https://epub.jku.at/obvulihs/download/pdf/6966648?originalFilename=true

    A great collection of different tools for DJI firmware that also covers DUML:
    https://github.com/o-gs/dji-firmware-tools/

    This includes the Comm Dissector, which can be used to analyze communication with the DJI drone via Wireshark:
    https://github.com/o-gs/dji-firmware-tools/tree/master/comm_dissector

    Also helpful if you want to learn how to decode and encode DUML messages:
    https://github.com/fpv-wtf/margerine/blob/master/src/packer.js

    With this sources and some great hints and advice (thx to Joonas and bri3d from discord) I learned to decode and encode DUML messages by my own.



    Structure of a DUML message

    DUML messages have a well-defined structure from which follows that they are at least 13 bytes long. There is a header, an address part and a data part. The hash of the header and the hash of the entire message are also transmitted (CRC8 and CRC16).

    Byte Bits Description Comment
    0 8 Delimiter Fixed value: 0x55
    1 + 2 10 Packet Length Length of the entire DUML [bytes] (little endian)
    C/C++: Length = ((duml[2] << 8) | (duml[1] & 255)) & 1023;
    2 6 Protocol Version Fixed value: 0x01
    C/C++: Version = (duml[2] & 252) >> 2;
    3 8 Header CRC8 Custom initial value (0x77) and non-standard
    hexadecimal lookup table
    4 3 Sender ID C/C++: SenderID = duml[4] >> 5;
    4 5 Sender Type C/C++: SenderType = duml[4] & 31;
    5 3 Receiver ID C/C++: ReceiverID = duml[5] >> 5;
    5 3 Receiver Type C/C++: ReceiverType = duml[5] & 31;
    6 + 7 16 Sequence Number 16 bit integer (little endian)
    C/C++: SeqNum= (duml[8] << 8) | (duml[7] & 255);
    8 1 Command Type Request: 0x00
    Response: 0x01
    C/C++: CmdType = duml[8] >> 7;
    8 3 Acknowledgement No ACK: 0x00
    Before Exec: 0x02
    After Exec: 0x03
    C/C++: ACK = (duml[8] >> 4) & 7;
    8 4 Encryption None: 0x00
    AES 128: 0x01
    Self Def: 0x02
    Xor: 0x03
    DES 56: 0x04
    DES 112: 0x05
    AES 192: 0x06
    AES 256: 0x07

    (DJI FPV only uses None (0x00))

    C/C++: Encryption = duml[8] & 15;
    9 8 Command Set Defines set of commands between sender and receiver
    10 8 Command ID Command to be executed
    11 : 10+n 8*n  Payload Data transmitted
    11+n : 12+n 16 Packet CRC16 Custom initial value (0x3692) and non-standard
    hexadecimal lookup table

    C/C++: CRC16= (duml[12+n] << 8) | (duml[11+n] & 255);


    As I know, byte decomposition, bitwise operations and things like that can be a bit confusing the first time. So I tried to show the translation of a duml massage into the values we are interested in a little bit more grafically. For this example we take the short duml massage "550E0466030BE322400D1900E0FE". In the upper row you find the byte and hex value information, below that the representation in bits. The background color of the bits indicates the information they represent. I tried to give each piece of information its own color. For example, the bits that encode the length of the duml message are blue, while the version bits are yellow. Below that are boxes...

    Read more »

View all 2 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates