DJI Virtual Flight RC radio interface

A project log for A poor man's FPV journey

If you want to enter FPV world in a cheap way, read these pages !

JP GleyzesJP Gleyzes 12/24/2022 at 14:000 Comments

Principle of operations

I got a friend's remote controller2 and here is what it gives when connected to a PC

It's an HID Joystick with X, Y, Z, Rx and Ry "analog sticks" and 7 active buttons. 

Here is the law table to map the actions on the remote2 to the corresponding joystick events.

This being known it is easy to reproduce a Bluetooth Low Energy Joystick!


Once again the heart of the system is an ESP32 MCU. The radio receiver or the trainer port output of my radio being 5V devices, I needed a level shifter to accomodate with the 3.3V IO of the ESP32.

And that's it for the schematics: one single pin used as input for the PPM train and the rest over the air via BLE!


As you may imagine the PCB is also very simple. My receiver is simply put on the side of the ESP32

The board and the receiver are powered by a 5V powerbank and no other connection is needed!


Most of the difficulty of this project was not in the hardware side but rather in the software one. 

It was really simple to emulate a BLE gamepad, but much more difficult to find the right combination of channels to be accepted by the DJI Virtual Flight. So after a lot of trial and errors I succeeded (see above) !

The code is mostly using the excellent LemingDev BLEgamepad library.

I have only added an interrupt routine to decommutate the PPM train. Only a few lines of code:

void IRAM_ATTR ppmISR() {
  // Remember the current micros() and calculate the time since the last pulseReceived()
  unsigned long previousMicros = microsAtLastPulse;
  microsAtLastPulse = micros();
  unsigned long pulseDuration = microsAtLastPulse - previousMicros;

  if (pulseDuration < MIN_TIME)
    microsAtLastPulse = previousMicros; //cancel the pulse
  else if (pulseDuration > BLANK_TIME)
    currentChannel = 0; // Blank detected: restart from channel 1
    digitalWrite (LED_PIN, !digitalRead(LED_PIN));
    // Store times between pulses as channel values
    if (currentChannel < NB_CHANNELS)
      if ((pulseDuration > 900) && (pulseDuration < 2500))
        rawValues[currentChannel] = pulseDuration;
    currentChannel++ ;

The rawValues of the Tx are stored in microsecond reprensenting the duration of each pulse.

then the conversion into gamepad events is also straightforward:


      //; //C1 button       //; //Sart button       //; //Pause/home button       //; //photo button       //; //gimbal Up       //; //gimbal Down. And buttons 5 and 6 released = gimbal center       //bleGamepad.pressHome(); //exit App releaseHome();       if ((rawValues[5] - 1500) > 250) //trainer button toggle       {;     //toggle Manual to S       }       else bleGamepad.release(BUTTON_7);       //map(value, fromLow, fromHigh, toLow, toHigh)       bleGamepad.setX(map(rawValues[3],1000, 2000, -660, 660)); //Right Stick horizontal #ifdef MODE_1       bleGamepad.setY(-map(rawValues[2],1000, 2000, -660, 660));  //Right Stick vertical       bleGamepad.setZ(map(rawValues[1],1000, 2000, -660, 660));   //Left Stick vertical #else       bleGamepad.setY(map(rawValues[2],1000, 2000, -660, 660));       bleGamepad.setZ(-map(rawValues[1],1000, 2000, -660, 660)); #endif       bleGamepad.setRX(map(rawValues[0],1000, 2000, -660, 660));  //Left Stick horizontal       bleGamepad.setRY(-map(rawValues[4],1000, 2000, -660, 660)); //gimbal on 3 states switch          bleGamepad.sendReport();

A you can see I can use:

Source code is on my Github page here :