Close

Demystifying the communication protocol (3)

A project log for DJI FPV - 6S Battery Compatibility Mod [Part 1]

I'm trying to mod/recreate the DJI FPV battery to use regular 6S lipo batteries. In a first attempt I try to replace the lipo cells.

AirCruiserAirCruiser 06/01/2022 at 09:530 Comments

Finally, coming to the point: The important commands...

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, 55797, 12675, 8202, 4753, 792, 30631, 26158, 21685, 17724, 48587, 44098, 40665, 36688, 64495, 60006, 55549, 51572, 16900, 21389, 24854, 28831, 1056, 5545, 10034, 14011, 52812, 57285, 60766, 64727, 34920, 39393, 43898, 47859, 21125, 17164, 29079, 24606, 5281, 1320, 14259, 9786, 57037, 53060, 64991, 60502, 39145, 35168, 48123, 43634, 25350, 29327, 16404, 20893, 9506, 13483, 1584, 6073, 61262, 65223, 52316, 56789, 43370, 47331, 35448, 39921, 29575, 25102, 20629, 16668, 13731, 9258, 5809, 1848, 65487, 60998, 56541, 52564, 47595, 43106, 39673, 35696, 33800, 38273, 42778, 46739, 49708, 54181, 57662, 61623, 2112, 6601, 11090, 15067, 20068, 24557, 28022, 31999, 38025, 34048, 47003, 42514, 53933, 49956, 61887, 57398, 6337, 2376, 15315, 10842, 24293, 20332, 32247, 27774, 42250, 46211, 34328, 38801, 58158, 62119, 49212, 53685, 10562, 14539, 2640, 7129, 28518, 32495, 19572, 24061, 46475, 41986, 38553, 34576, 62383, 57894, 53437, 49460, 14787, 10314, 6865, 2904, 32743, 28270, 23797, 19836, 50700, 55173, 58654, 62615, 32808, 37281, 41786, 45747, 19012, 23501, 26966, 30943, 3168, 7657, 12146, 16123, 54925, 50948, 62879, 58390, 37033, 33056, 46011, 41522, 23237, 19276, 31191, 26718, 7393, 3432, 16371, 11898, 59150, 63111, 50204, 54677, 41258, 45219, 33336, 37809, 27462, 31439, 18516, 23005, 11618, 15595, 3696, 8185, 63375, 58886, 54429, 50452, 45483, 40994, 37561, 33584, 31687, 27214, 22741, 18780, 15843, 11370, 7921, 3960};
        
  for (int i=0; i<CalcLength(buff)-2; i++){
    value = LUTCRC16[(value ^ buff[i]) & 255] ^ (value >> 8);
  }
        
  crc16[0] = value & 255;
  crc16[1] = (65280 & value) >> 8;
}

Battery data decoding

GetPushDynamicData [CmdSet 13, CmdID 2]

Now we can read, check and create duml messages. Nice! But we still don't know what is encoded in most data fields and which ones are relevant. In my experience, only one duml message really counts. It's GetPushDynamicData [CmdSet 13, CmdID 2]. Both the FLYC and the gimbal ask for battery information with this command. The FLYC with the data field "00000000" and the gimbal with "a0810620". The battery response data field might look like this: 00a0875f0000d4fcffff8c070000f4060000f300065d00000000030000001301640000bc031b040000
What does that mean? The following interpretation is based on my expectations and the values I have observed under (more or less well) defined conditions. So it may not be 100% correct and/or complete.

Byte Description Example Comment
0 + 1 Identifier 0x00A0 Gimbal: 0x00A0
FLYC: 0x0000
2 + 3 Voltage [mV] 0x875F 16 bit integer (little endian)
C/C++: V = (data[3] << 8) | (data[2] & 255);

0x875F -> 24455 [mV] (24.455 V)
4 + 5 Delimiter (?) 0x0000 Did not observe anything else than 0x0000.
6 + 7 Current [mA] 0xD4FC 16 bit integer (little endian)
C/C++: I = (data[7] << 8) | (data[6] & 255);

0xD4FC -> -812 [mA] (negative sign because
the battery is being discharged)
8 + 9 Delimiter (?) 0xffff Did not observe anything else than 0xffff.
10 + 11 Design Capacity [mA] 0x8C07 16 bit integer (little endian)
C/C++: C_D = (data[11] << 8) | (data[10] & 255);

0x8C07 -> 1932 [mA]
12 + 13 Delimiter (?) 0x0000 Did not observe anything else than 0x0000.
14 + 15 Remaining Capacity [mA] 0xF406 16 bit integer (little endian)
C/C++: C_rm = (data[15] << 8) | (data[14] & 255);

0xF406 -> 1780 [mA]
16 + 17 Battery Temperature [°C/10] 0xF300 16 bit integer (little endian)
C/C++: T = (data[17] << 8) | (data[16] & 255);

0xF300 -> 243 [°C/10] (24.3 °C)
18 Number of Cells 0x06 8 bit integer
C/C++: N = data[18];

0x06 -> 6 Lipo Cells ;)
120 Delimiter (?) 0x0000 Did not observe anything else than 0x0000.
21 + 22 Battery left [%] 0x5D00 16 bit integer (little endian)
C/C++: P = (data[22] << 8) | (data[21] & 255);

0x5D00 -> 93 [%]
23 - 30 ??? 0x0000000
300000013
Always seems the same. Decoding it the same
way as before, we can find 768 and 4864.
Couldn't find meaning in it.
31 + 32 Max. Voltage [mV] ??? 0x0164 16 bit integer (little endian)
C/C++: V_max = (data[32] << 8) | (data[31] & 255);

0x0164 -> 25601 [mV] (25.601 V)
33 + 34 Delimiter (?) 0x0000 Did not observe anything else than 0x0000.
35 + 36 ??? 0xBC03 16 bit integer (little endian)
C/C++: V = (data[36] << 8) | (data[35] & 255);

0xBC03 -> 956
37 + 38 ??? 0x1B04 16 bit integer (little endian)
C/C++: V = (data[38] << 8) | (data[37] & 255);

0x1B04 -> 1051
39 + 40 Delimiter (?) 0x0000 Did not observe anything else than 0x0000.



But why is this data field so important? As I said, an error occurs when I use a arbitrary LiPo without charging it with the smart battery control board after connecting it to the board. I assumed the reason for this is that the control board's BMS needs to be calibrated by loading the cells. This seems to be true. In this case, both data fields for the target capacity and the remaining capacity are 0x0000. 

And even if I charge a LiPo with a capacity greater than 2000 mAh, the design capacity data field will not go above 2000 mAh. So when I connect a 3000 mAh to the smart battery control board and charge it, the battery thinks only 2000 mAh capacity are possible. Therefore, it will not supply more than 2000 mAh and the drone will initiate an emergency landing when I get to that region.

So the trick is to change these values to the correct ones. For the design capacity, this is easy. You just need to measure (or estimate) the capacity of the LiPo you intend to use. And for the remaining capacity? The easiest way I could think of was to only connect fully charged batteries. Then I set the initial value to the design capacity. After that I multiplied the current measured current by the time since the last current measurement and subtracted this from the initial value. And so on and so on. The percentage can be calculated on the basis of the remaining capacity and the design capacity and written into the corresponding data field. This kind of simple coulomb counter works surprisingly well. But what to do if the battery is not fully charged? This can lead to the unpleasant situation that you think you can fly a few more rounds, but in fact the drone will die in a few seconds.

In a future log I will show a possible workaround for this problem. But maybe you have a better/great idea. Please feel free to let me know!


What's the matter with data fields 35 - 38?

Short answer: I don't know!
And now the longer one...
I captured the data during a test run and plotted it against time.

This appeared to be some sort of multiple of the percentage and the temperature.

After some "manual curve fits" I've come up with a formula that results in a curve that falls between the two curves. But I have no idea if that has any meaning. Maybe this is some kind of temperature corrected battery fitness indicator? What do you think?


GetPushCellVoltage [CmdSet 13, CmdID 3]

The interpretation of the data field of [CmdSet 13, CmdID 3] was quite easy as it should contain the voltages of the six battery cells. Typically it looks like this: 000006B70FB20FB50FC30FA80FB10F. So we have 4 zeros, 06 and six similar looking 2 byte numbers. Thus, the following decoding scheme suggested itself:

Byte Description Example Comment
0 + 1 Delimiter (?) 0x0000 Did not observe anything else than 0x0000.
2 Number of Cells 0x06 The number of LiPo/LiIon cells within the battery.
3 + 4 Voltage Cell 1 [mV] 0xB70F 16 bit integer (little endian)
C/C++: CV1 = (data[4] << 8) | (data[3] & 255);

0xB70F -> 4023 [mV]
5 + 6 Voltage Cell 2 [mV] 0xB20F 16 bit integer (little endian)
C/C++: CV2 = (data[6] << 8) | (data[5] & 255);

0xB20F -> 4018 [mV]
7 + 8 Voltage Cell 3 [mV] 0xB50F 16 bit integer (little endian)
C/C++: CV3 = (data[8] << 8) | (data[7] & 255);

0xB50F -> 4021 [mV]
9 + 10 Voltage Cell 4 [mV] 0xC30F 16 bit integer (little endian)
C/C++: CV4 = (data[10] << 8) | (data[9] & 255);

0xC50F -> 4037 [mV]
11 + 12 Voltage Cell 5 [mV] 0xA80F 16 bit integer (little endian)
C/C++: CV5 = (data[12] << 8) | (data[11] & 255);

0xA80F -> 4008 [mV]
13 + 14 Voltage Cell 6 [mV] 0xB10F 16 bit integer (little endian)
C/C++: CV6 = (data[14] << 8) | (data[13] & 255);

0xB10F -> 4017 [mV]

DLog Battery Data [CmdSet 14, CmdID 34]

More interesting and not yet fully understood is [CmdSet 14, CmdID 34]. As already mentioned, it comes in 2 variations that seem to alternate. Here I would like to discuss the one whose data field starts with 8913 and is acknowledged by the FLYC with 00008913. With 122 bytes, the data field is quite large compared to most other duml messages (e.g. 558704FB0B035033200E22891377030300000E048C078007F400521051105110551050105210020030FDFFFFC861D2610000EB6100000001380800000300030000004001400046004E003100380046003600370036003A00450053009A004C013600000000000000000024FDFFFF20FDFFFF20FDFFFF28FDFFFF24FDFFFF18FDFFFF018C07DCCD). And unlike most other communications, DLog Battery Data is initialized by the battery, which sends the data to the drone as a request. When I started to decode the data field it was a bit confusing as I had no idea what would be encoded within it. I realized that six very similar numbers are encoded (cell voltages) and that there are other similarities to GetPushDynamicData [CmdSet 13, CmdID 2]. And the rest?

Funnily enough, the answer to this question comes from the battery itself. As described in the last part of this log series, the battery sends the following information to the drone with [CmdSet 14, CmdID 33]: "bat_monitor,bfsm,bmos,cflag,cstatus, cfc,crm,gtem,cc1,cc2,cc3,cc4,cc5,cc6,ccyc,hIv,ca_p,dm_p,dbat,bfly_u,bfly_g,cqmax,cdt,cdpq,cqpq,cqt,cr0,cr1,cr2,cr3, cr4,cr5,cr6,cr7,cr8, cr9,crA,crB,crC,crD,crE,cst, ddl ,ddh,hi1,hi2,hi3,hi4,hi5,hi6,bgd,ctf". 
This seems to be the decoding scheme for the data field of [CmdSet 14, CmdID 34]. Assuming that, only two questions remain.
1. How many bytes does the values seize?
2. What does these funny characters want to tell us?

Based on the observation that most of the other values were encoded as 16 bit integers and the knowledge about some of the values (e.g. in which range current, voltage and capacity were expected), I would suggest the following decoding scheme:

Discussions