Help needed: PS4 Controller (Bluepad32) → ESP32 → nRF24L01 RC system

Hi everyone,
I'm building a long-range RC system (boat/plane project) and I need help connecting everything properly.
:wrench: Setup:
TX: PS4 Controller → ESP32 (Bluepad32 library) → nRF24L01
RX: ESP8266 / ESP32 → nRF24L01 → Servos + ESC
:bullseye: What I want to achieve:
Read PS4 joystick values using Bluepad32
Map joystick values (-512 to 512) into 0–255 or PWM-friendly range
Send the data using nRF24L01 (RF24 library) in a structured packet
Receive and convert it into:
writeMicroseconds() for ESC
Servo control signals
:red_question_mark: Main issue:
I’m not sure about the best way to structure the data packet between ESP32 and ESP8266/ESP32, especially keeping it stable for RC control (latency + reliability).
If anyone has:
Example code
GitHub project
Or tips for Bluepad32 + RF24 integration
I’d really appreciate it :folded_hands:
Thanks in advance!

I'd write

RX: nRF24L01 → ESP8266 / ESP32 → Servos + ESC

The basic packet is simple, just a number and a pointer to a bunch of bytes. No interpretation or meaning beyond N * 8 1s and 0s.

Typically a struct holding all the values you want to send is defined.

Then when you load it up with the readings or the mapped readings, you can nRF24L01 transmit the entire structure as a number of bytes.

If you wanna get fancy, an extra few bytes can be used to pass a CRC code, then the receiver can test the packet for integrity.

But that's overkill here, and maybe for any nRF24L01 link, as it may do more or less the same thing behind the scenes so to speak.

a7

Thanks alto777 for the insight! You're right, I'm already using a struct to package the axes (ch1 to ch7). My main concern now is the 'timing' between the Bluepad32 task and the RF24 transmission on the ESP32. Should I run the RF24 transmission inside the Bluepad32 callback or in the main loop to minimize latency? Also, if you have a known-stable 'Struct' example for 6-7 channels, that would be awesome."

I don't know enough to say!

There's probably not much to be gained by doing the nRF24L01 inside the callback to do that.

I would set a flag and check it in the free-running loop (no blocking, checks very frequently), send a packet.

But either way is not that much different or that much code, so getting empirical may be necessary.

You may find it to be unsatisfactory end-to-end no matter what you do.

There's some magic here you've glossed over

nRF24L01 → Servos

where I assume you will be using the transmitted values to synthesize the servo signals, which for simple servos run at 50 Hz pulses in the range of 1 .. 2 milliseconds.

Modern r/c protocols make that look positively 20th Century level. :-|

Transmit the full -512 to 512 range: I would just add to make it all positive 0 .. 1023.

Those 14 bytes (7 16 bit integers) don't need to be in a struct, just contiguous in memory as they woukd be in a simple array. You will pass the address of the array (or struct) and the byte count.

I understand the need for and roll of the bluepad32, but I am curious what informed your decision to use the nRF24L01. Nothing wrong with it, I've used it for this kind of thing, they work well. But later radio sets meant for r/c may do better.

Post any code, preferably a working version of something and let's see what it looks like.

a7

Welcome!

That sounds like a verry interesting project that should be a lot of fun! However, Please keep in mind that we are not a free design or code-writing service. We’re more than happy to help with your design or code, but we need you to make an initial attempt. Please design and write your code, then post it along with an annotated schematic and an explanation of what’s not working properly. There is also a for hire section if you want to pay for it.

Many will suggest you do it in sections and that is the correct way of doing it. We have no idea of your skill set or what resources you have available. If you are new to Arduino I highly recommend getting a copy of the Arduino Cookbook, A lot of what you will want to do is in there.

  1. Show Your Work First: Before asking for assistance, make an attempt to design or write the code yourself. Share your work along with details about what isn’t working. That includes a preliminary schematic that you drew.
  2. Provide Clear Documentation: Since we can’t see your project, share an annotated schematic (best) or a clear drawing of your setup. Clear Pictures are welcome, but avoid using Fritzing diagrams as they are wiring diagrams, not schematics, and are not ideal for troubleshooting.
  3. Include Technical Details: If there is specific hardware involved, include links to technical information. There are often many versions of similar components, so precise details are essential. I will assume the processor is a UNO. Many times the processor is stated as ESP32, please state which one and post a link to it.
  4. Reference Resources: For additional help, check out useful links and tutorials: Useful Links on Arduino Forum. Of course one of the better sources of information (in my opinion) is the Arduino Cookbook. Skim it cover to cover and stop on any project that interests you.

ESP32 > NRF24L01 tests transmitting a structure

//  ESP32 > NRF24L01 transmitter test using a structure

// https://nrf24.github.io/RF24/

// UNO connections
// arduino SCK pin 11 goes to NRF24L10_pin SCK
// arduino MISO pin 12 goes to NRF24L10_pin MI
// arduino MOSI pin 13 goes to NRF24L10_pin MO
// NRF24L10 CE to arduino pin 9
// NRF24L10 CSN to arduino pin10

// ESP32 connections
// ESP32 SCK pin GPIO18 goes to NRF24L10_pin SCK
// ESP32 MISO pin GPIO19 goes to NRF24L10_pin MI
// ESP32 MOSI pin GPIO23 goes to NRF24L10_pin MO
// NRF24L10 CE to ESP32 pin GPIO4
// NRF24L10 CSN to ESP32 pin GPIO 5

// RP2040 connections
// RP2040 SPIO_SCK pin GP18 goes to NRF24L10_pin SCK
// RP2040 SPIO_RX pin GP16 goes to NRF24L10_pin MISO
// RP2040 SPIO_TX pin GP19 goes to NRF24L10_pin MOSI
// RP2040 pin SPIO_CSn GP17 to NRF24L10 CSN 
// RP2040 pin GP20 to NRF24L10 CE 
// RP2040 GND and 3.3V to NRF24L10  GND and VCC

#include <SPI.h>
#include "RF24.h"

struct __attribute__((packed)) Struct1 {  // test data structure
  uint8_t seq;                            // sequence number
  uint8_t id;                             // node ID if multiple transmitters
  uint16_t temperature;
  float voltage;
  uint8_t crc;                      // CRC check
} data = { 0, 1, 10, 3.14159, 0 };  // initial test data

// for this test using a simple checksum - replace with CRC check
unsigned char checksum(unsigned char *data, uint8_t *crc) {
  unsigned char sum = 0;
  while (data != crc) {
    sum += *data;
    data++;
  }
  return sum;
}

bool radioNumber = 0;
RF24 radio(4, 5);  //CE and CSN

byte addresses[][6] = { "1Node", "2Node" };

void setup() {
  Serial.begin(115200);
  Serial.println("\n\nESP32 > NRF24L01 transmit structure");
  radio.begin();
  if (radio.isChipConnected())
    Serial.println("\n\nTransmitter NF24 connected to SPI");
  else Serial.println("\n\nNF24 is NOT connected to SPI");
  radio.setChannel(125);
  radio.setPALevel(RF24_PA_MIN);
  radio.powerUp();
  radio.setDataRate(RF24_1MBPS);
  //radio.setDataRate(RF24_250KBPS);
  if (radioNumber) {
    radio.openWritingPipe(addresses[1]);
    radio.openReadingPipe(1, addresses[0]);
  } else {
    radio.openWritingPipe(addresses[0]);
    radio.openReadingPipe(1, addresses[1]);
  }
  radio.stopListening();
  Serial.print("data sizeof (bytes):  ");  // print size of data structure in bytes
  Serial.println(sizeof(Struct1));
  radio.setPayloadSize(sizeof(Struct1));
}

// loop transmiting data packet
void loop() {
  static int errors = 0, frames = 0;
  data.crc = checksum((unsigned char *)&data, &data.crc);
  Serial.print("Tx Frame: ");
  Serial.print(++frames);
  Serial.print(" seq:   ");
  Serial.print(data.seq);
  Serial.print("  temperature:   ");
  Serial.print(data.temperature);
  Serial.print("  voltage:   ");
  Serial.print(data.voltage);
  Serial.print("  crc: 0x");
  Serial.print(data.crc, HEX);
  if (!radio.write(&data, sizeof(data))) {  // transmit packet
    Serial.print(F(" Tx failed "));
    Serial.println(++errors);
  } else {
    Serial.print(" Tx OK! - errorrs ");
    Serial.println(errors);
  }
  delay(1000);
  data.seq++;  // update test data
  data.temperature += 5;
  data.voltage += 5.0f;
}

receiver

//  NRF24L01 receiver test using a structure

// UNO connections
// arduino SCK pin 11 goes to NRF24L10_pin SCK
// arduino MISO pin 12 goes to NRF24L10_pin MI
// arduino MOSI pin 13 goes to NRF24L10_pin MO
// NRF24L10 CE to arduino pin 9
// NRF24L10 CSN to arduino pin10

// ESP32 connections
// ESP32 SCK pin GPIO18 goes to NRF24L10_pin SCK
// ESP32 MISO pin GPIO19 goes to NRF24L10_pin MI
// ESP32 MOSI pin GPIO23 goes to NRF24L10_pin MO
// NRF24L10 CE to ESP32 pin GPIO4
// NRF24L10 CSN to ESP32 pin GPIO 5

#include <SPI.h>
#include <RF24.h>

struct __attribute__((packed)) Struct1 {  // test data structure
  uint8_t seq;                            // sequence number
  uint8_t id;                             // node ID if multiple transmitters
  uint16_t temperature;
  float voltage;
  uint8_t crc;  // CRC check
} data;

// for this test using a simple checksum - replace with CRC check
unsigned char checksum(unsigned char *data, uint8_t *crc) {
  unsigned char sum = 0;
  while (data != crc) {
    sum += *data;
    data++;
  }
  return sum;
}

#define CE_PIN 4
#define CSN_PIN 5

bool radioNumber = 1;
const uint8_t pipes[][6] = { "1Node", "2Node" };

RF24 radio(CE_PIN, CSN_PIN);

char dataReceived[10];  // this must match dataToSend in the TX
bool newData = false;

//===========

void setup() {
  Serial.begin(115200);
  radio.begin();
  if (radio.isChipConnected())
    Serial.println("\n\nReceiver NF24 connected to SPI");
  else {
    Serial.println("\n\nNF24 is NOT connected to SPI");
    while (1)
      ;
  }
  radio.setChannel(125);
  // radio.setDataRate(RF24_1MBPS);
  radio.setDataRate(RF24_250KBPS);
  radio.printDetails();
  if (!radioNumber) {
    radio.openWritingPipe(pipes[0]);
    radio.openReadingPipe(1, pipes[1]);
  } else {
    radio.openWritingPipe(pipes[1]);
    radio.openReadingPipe(1, pipes[0]);
  }
  radio.startListening();
  Serial.print("data sizeof (bytes):  ");  // print size of data structure in bytes
  Serial.println(sizeof(Struct1));
  radio.setPayloadSize(sizeof(Struct1));
}

//=============

void loop() {
  static uint8_t seq_check = 0;
  static int crcErrors = 0, seqErrors = 0, frames = 0;
  if (radio.available()) {
    while (radio.available()) {
      int length = radio.getPayloadSize();
      Serial.print("received ");
      Serial.print(length);
      if (length != sizeof(data)) {
        Serial.println(" bytes - invalid packet size!");
        return;
      }
      Serial.print(" bytes Frame No: ");
      Serial.print(++frames);
      radio.read(&data, sizeof(data));
      Serial.print("  temperature:   ");
      Serial.print(data.temperature);
      Serial.print("  voltage:   ");
      Serial.print(data.voltage);
      Serial.print(" crc: 0x");
      Serial.print(data.crc, HEX);
      if (checksum((unsigned char *)&data, &data.crc) == data.crc)
        Serial.print(" CRC OK  ");
      else {
        Serial.print(" CRC ERROR!  ");
        crcErrors++;
      }
      Serial.print(" errors ");
      Serial.print(crcErrors);
      Serial.print(" seq:   ");
      Serial.print(data.seq);
      if (seq_check == data.seq) Serial.print(" seq OK  - ");
      else {
        Serial.print(" seq error! expected ");
        Serial.print(seq_check);
        seqErrors++;
      }
      Serial.print(" - seq errors ");
      Serial.println(seqErrors);
      seq_check = ++data.seq;
    }
  }
}

transmitter serial monitor output

Transmitter NF24 connected to SPI
data sizeof (bytes):  9
Tx Frame: 1 seq:   0  temperature:   10  voltage:   3.14  crc: 0x73 Tx OK! - errorrs 0
Tx Frame: 2 seq:   1  temperature:   15  voltage:   8.14  crc: 0x8B Tx OK! - errorrs 0
Tx Frame: 3 seq:   2  temperature:   20  voltage:   13.14  crc: 0xE1 Tx OK! - errorrs 0
Tx Frame: 4 seq:   3  temperature:   25  voltage:   18.14  crc: 0xA Tx OK! - errorrs 0
Tx Frame: 5 seq:   4  temperature:   30  voltage:   23.14  crc: 0x38 Tx OK! - errorrs 0
Tx Frame: 6 seq:   5  temperature:   35  voltage:   28.14  crc: 0x66 Tx OK! - errorrs 0
Tx Frame: 7 seq:   6  temperature:   40  voltage:   33.14  crc: 0x2 Tx OK! - errorrs 0
Tx Frame: 8 seq:   7  temperature:   45  voltage:   38.14  crc: 0x1C Tx OK! - errorrs 0
Tx Frame: 9 seq:   8  temperature:   50  voltage:   43.14  crc: 0x36 Tx OK! - errorrs 0
Tx Frame: 10 seq:   9  temperature:   55  voltage:   48.14  crc: 0x50 Tx OK! - errorrs 0
Tx Frame: 11 seq:   10  temperature:   60  voltage:   53.14  crc: 0x6A Tx OK! - errorrs 0
Tx Frame: 12 seq:   11  temperature:   65  voltage:   58.14  crc: 0x84 Tx OK! - errorrs 0
Tx Frame: 13 seq:   12  temperature:   70  voltage:   63.14  crc: 0x9E Tx OK! - errorrs 0
Tx Frame: 14 seq:   13  temperature:   75  voltage:   68.14  crc: 0xE9 Tx OK! - errorrs 0
Tx Frame: 15 seq:   14  temperature:   80  voltage:   73.14  crc: 0xF9 Tx OK! - errorrs 0
Tx Frame: 16 seq:   15  temperature:   85  voltage:   78.14  crc: 0x9 Tx OK! - errorrs 0
Tx Frame: 17 seq:   16  temperature:   90  voltage:   83.14  crc: 0x19 Tx OK! - errorrs 0
Tx Frame: 18 seq:   17  temperature:   95  voltage:   88.14  crc: 0x29 Tx OK! - errorrs 0
......
Tx Frame: 1345 seq:   64  temperature:   6730  voltage:   6723.14  crc: 0xF7 Tx OK! - errorrs 0
Tx Frame: 1346 seq:   65  temperature:   6735  voltage:   6728.14  crc: 0x25 Tx OK! - errorrs 0
Tx Frame: 1347 seq:   66  temperature:   6740  voltage:   6733.14  crc: 0x53 Tx OK! - errorrs 0
Tx Frame: 1348 seq:   67  temperature:   6745  voltage:   6738.14  crc: 0x81 Tx OK! - errorrs 0
Tx Frame: 1349 seq:   68  temperature:   6750  voltage:   6743.14  crc: 0xAF Tx OK! - errorrs 0
Tx Frame: 1350 seq:   69  temperature:   6755  voltage:   6748.14  crc: 0xDD Tx OK! - errorrs 0
Tx Frame: 1351 seq:   70  temperature:   6760  voltage:   6753.14  crc: 0xC Tx OK! - errorrs 0
Tx Frame: 1352 seq:   71  temperature:   6765  voltage:   6758.14  crc: 0x3A Tx OK! - errorrs 0
Tx Frame: 1353 seq:   72  temperature:   6770  voltage:   6763.14  crc: 0x68 Tx OK! - errorrs 0
....
Tx Frame: 22779 seq:   250  temperature:   48364  voltage:   113893.14  crc: 0xCC Tx OK! - errorrs 0
Tx Frame: 22780 seq:   251  temperature:   48369  voltage:   113898.14  crc: 0x55 Tx OK! - errorrs 0
Tx Frame: 22781 seq:   252  temperature:   48374  voltage:   113903.14  crc: 0xDD Tx OK! - errorrs 0
Tx Frame: 22782 seq:   253  temperature:   48379  voltage:   113908.14  crc: 0x66 Tx OK! - errorrs 0
Tx Frame: 22783 seq:   254  temperature:   48384  voltage:   113913.14  crc: 0xEF Tx OK! - errorrs 0
Tx Frame: 22784 seq:   255  temperature:   48389  voltage:   113918.14  crc: 0x78 Tx OK! - errorrs 0
Tx Frame: 22785 seq:   0  temperature:   48394  voltage:   113923.14  crc: 0x0 Tx OK! - errorrs 0
Tx Frame: 22786 seq:   1  temperature:   48399  voltage:   113928.14  crc: 0x89 Tx OK! - errorrs 0
Tx Frame: 22787 seq:   2  temperature:   48404  voltage:   113933.14  crc: 0x11 Tx OK! - errorrs 0

receiver serial monitor output

ESP32 RECEIVER
Receiver NF24 connected to SPI
SPI Speedz	= 10 Mhz
STATUS		= 0x0e RX_DR=0 TX_DS=0 MAX_RT=0 RX_P_NO=7 TX_FULL=0
RX_ADDR_P0-1	= 0x65646f4e32 0x65646f4e31
RX_ADDR_P2-5	= 0xc3 0xc4 0xc5 0xc6
TX_ADDR		= 0x65646f4e32
RX_PW_P0-6	= 0x20 0x20 0x20 0x20 0x20 0x20
EN_AA		= 0x3f
EN_RXADDR	= 0x03
RF_CH		= 0x7d
RF_SETUP	= 0x27
CONFIG		= 0x0e
DYNPD/FEATURE	= 0x00 0x00
Data Rate	= 250 KBPS
Model		= nRF24L01+
CRC Length	= 16 bits
PA Power	= PA_MAX
ARC		= 0
data sizeof (bytes):  9
received 9 bytes Frame No: 1  temperature:   10  voltage:   3.14 crc: 0x73 CRC OK   errors 0 seq:   0 seq OK  -  - seq errors 0
received 9 bytes Frame No: 2  temperature:   15  voltage:   8.14 crc: 0x8B CRC OK   errors 0 seq:   1 seq OK  -  - seq errors 0
received 9 bytes Frame No: 3  temperature:   20  voltage:   13.14 crc: 0xE1 CRC OK   errors 0 seq:   2 seq OK  -  - seq errors 0
received 9 bytes Frame No: 4  temperature:   25  voltage:   18.14 crc: 0xA CRC OK   errors 0 seq:   3 seq OK  -  - seq errors 0
received 9 bytes Frame No: 5  temperature:   30  voltage:   23.14 crc: 0x38 CRC OK   errors 0 seq:   4 seq OK  -  - seq errors 0
received 9 bytes Frame No: 6  temperature:   35  voltage:   28.14 crc: 0x66 CRC OK   errors 0 seq:   5 seq OK  -  - seq errors 0
received 9 bytes Frame No: 7  temperature:   40  voltage:   33.14 crc: 0x2 CRC OK   errors 0 seq:   6 seq OK  -  - seq errors 0
received 9 bytes Frame No: 8  temperature:   45  voltage:   38.14 crc: 0x1C CRC OK   errors 0 seq:   7 seq OK  -  - seq errors 0
.....
received 9 bytes Frame No: 1349  temperature:   6750  voltage:   6743.14 crc: 0xAF CRC OK   errors 0 seq:   68 seq OK  -  - seq errors 0
received 9 bytes Frame No: 1350  temperature:   6755  voltage:   6748.14 crc: 0xDD CRC OK   errors 0 seq:   69 seq OK  -  - seq errors 0
received 9 bytes Frame No: 1351  temperature:   6760  voltage:   6753.14 crc: 0xC CRC OK   errors 0 seq:   70 seq OK  -  - seq errors 0
received 9 bytes Frame No: 1352  temperature:   6765  voltage:   6758.14 crc: 0x3A CRC OK   errors 0 seq:   71 seq OK  -  - seq errors 0
received 9 bytes Frame No: 1353  temperature:   6770  voltage:   6763.14 crc: 0x68 CRC OK   errors 0 seq:   72 seq OK  -  - seq errors 0
received 9 bytes Frame No: 1354  temperature:   6775  voltage:   6768.14 crc: 0x96 CRC OK   errors 0 seq:   73 seq OK  -  - seq errors 0
received 9 bytes Frame No: 1355  temperature:   6780  voltage:   6773.14 crc: 0xC4 CRC OK   errors 0 seq:   74 seq OK  -  - seq errors 0
received 9 bytes Frame No: 1356  temperature:   6785  voltage:   6778.14 crc: 0xF2 CRC OK   errors 0 seq:   75 seq OK  -  - seq errors 0
received 9 bytes Frame No: 1357  temperature:   6790  voltage:   6783.14 crc: 0x20 CRC OK   errors 0 seq:   76 seq OK  -  - seq errors 0
received 9 bytes Frame No: 1358  temperature:   6795  voltage:   6788.14 crc: 0x4F CRC OK   errors 0 seq:   77 seq OK  -  - seq errors 0
.....
received 9 bytes Frame No: 22692  temperature:   47929  voltage:   113458.14 crc: 0x67 CRC OK   errors 0 seq:   163 seq OK  -  - seq errors 0
received 9 bytes Frame No: 22693  temperature:   47934  voltage:   113463.14 crc: 0xEF CRC OK   errors 0 seq:   164 seq OK  -  - seq errors 0
received 9 bytes Frame No: 22694  temperature:   47939  voltage:   113468.14 crc: 0x78 CRC OK   errors 0 seq:   165 seq OK  -  - seq errors 0
received 9 bytes Frame No: 22695  temperature:   47944  voltage:   113473.14 crc: 0x0 CRC OK   errors 0 seq:   166 seq OK  -  - seq errors 0
received 9 bytes Frame No: 22696  temperature:   47949  voltage:   113478.14 crc: 0x89 CRC OK   errors 0 seq:   167 seq OK  -  - seq errors 0
received 9 bytes Frame No: 22697  temperature:   47954  voltage:   113483.14 crc: 0x11 CRC OK   errors 0 seq:   168 seq OK  -  - seq errors 0
received 9 bytes Frame No: 22698  temperature:   47959  voltage:   113488.14 crc: 0x9A CRC OK   errors 0 seq:   169 seq OK  -  - seq errors 0
received 9 bytes Frame No: 22699  temperature:   47964  voltage:   113493.14 crc: 0x22 CRC OK   errors 0 seq:   170 seq OK  -  - seq errors 0

NOTE: if transmitter and receiver are the same processor (same type lengths, padding, etc) the structure

__attribute__((packed))

is not requred

Yeth. But it is overkill and would not be needed at the low level,of the radio packets.

These are small packets with trivial parsing, and the radio already uses CRC for link-level integrity.

In its standard configuration with auto-ack enabled, it also handles retries and rejection of bad packets, so additional error checking is unnecessary.

A CRC might be useful in the higher level logic for some other reasons. I don't see it, TBH, if you are writing the code on both sides of the link.

a7