3 buttons resulting in 3 different actions over nRF24L01

Hello,

For my RC boat, I am using an Uno as my transmitter and ESP32 as my receiver. The idea is to have different modes of operation for the boat. On the transmitter side, I have 3 buttons, each connected to an LED to indicate what mode is active. I am using nRF24L01 for RF comms between the two. I'm struggling with the programming part. When the first button is pressed, I want to activate manual mode where joystick commands (x and y) are transmitted over to the receiver to influence a motor and a servo connected to the rudder. For the second mode, I'd like to have something like a heading hold mode where I can send a heading value through the serial monitor. I haven't yet decided what I want to do with the 3rd button but once I have an idea of how to successfully transmit and code this correctly, I'd like to have a 3rd functionality. The overall idea is that each LED will stay on as long as another button for a different mode is not selected. As of now, I can make the LEDs stay on but I am unable to command an output on the receiver's side. For the RF part, I have been going through tutorials and have been reading about sending data wirelessly using a struct format but I am relatively new to this so any guidance on that front would also be much appreciated. Thank you!

// Transmitter

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

#define BUTTON1 3
#define BUTTON2 4
#define BUTTON3 5
#define LED1 10
#define LED2 9
#define LED3 6

RF24 radio(7, 8); // CE, CSN pins for nRF24L01
const byte address[6] = "00001"; // Address for communication

bool button1State = false, button2State = false, button3State = false;
bool lastButton1 = false, lastButton2 = false, lastButton3 = false;
unsigned long debounceDelay = 50;
unsigned long lastDebounceTime1 = 0, lastDebounceTime2 = 0, lastDebounceTime3 = 0;

struct Payload {
  uint16_t mode; // 0: Manual, 1: Heading Hold, 2: Blink Mode
  uint16_t joystickX;
  uint16_t joystickY;
  uint16_t heading;
};

Payload data = {0, 0, 0, 0}; // Initial payload

void setup() {
  pinMode(BUTTON1, INPUT_PULLUP);
  pinMode(BUTTON2, INPUT_PULLUP);
  pinMode(BUTTON3, INPUT_PULLUP);
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);

  radio.begin();
  radio.openWritingPipe(address);
  radio.setPALevel(RF24_PA_LOW);
  radio.stopListening();
}

void loop() {
  // Button 1
  if (digitalRead(BUTTON1) == LOW) {
    if (millis() - lastDebounceTime1 > debounceDelay) {
      button1State = !button1State;
      button2State = false;
      button3State = false;
      lastDebounceTime1 = millis();
    }
  }
  lastButton1 = digitalRead(BUTTON1);

  // Button 2
  if (digitalRead(BUTTON2) == LOW) {
    if (millis() - lastDebounceTime2 > debounceDelay) {
      button2State = !button2State;
      button1State = false;
      button3State = false;
      lastDebounceTime2 = millis();
    }
  }
  lastButton2 = digitalRead(BUTTON2);

  // Button 3
  if (digitalRead(BUTTON3) == LOW) {
    if (millis() - lastDebounceTime3 > debounceDelay) {
      button3State = !button3State;
      button1State = false;
      button2State = false;
      lastDebounceTime3 = millis();
    }
  }
  lastButton3 = digitalRead(BUTTON3);

  // Set LEDs and mode
  if (button1State) {
    digitalWrite(LED1, HIGH);
    digitalWrite(LED2, LOW);
    digitalWrite(LED3, LOW);
    data.mode = 0; // Manual Mode
    data.joystickX = analogRead(A0);
    data.joystickY = analogRead(A1);
  } else if (button2State) {
    digitalWrite(LED1, LOW);
    digitalWrite(LED2, HIGH);
    digitalWrite(LED3, LOW);
    data.mode = 1; // Heading Hold
    if (Serial.available()) {
      data.heading = Serial.parseInt(); // Read heading from Serial Monitor
    }
  } else if (button3State) {
    digitalWrite(LED1, LOW);
    digitalWrite(LED2, LOW);
    digitalWrite(LED3, HIGH);
    data.mode = 2; // Blink Mode
  }

  // Send data to receiver
  radio.write(&data, sizeof(data));
  delay(50);
}


// Receiver

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

#define BUILTIN_LED 2

const int servo1_pin = 14; // Servo pin

Servo servo1;

RF24 radio(4, 5); // CE, CSN pins for nRF24L01
const byte address[6] = "00001";

struct Payload {
  uint16_t mode; // 0: Manual, 1: Heading Hold, 2: Blink Mode
  uint16_t joystickX;
  uint16_t joystickY;
  uint16_t heading;
};

Payload data;

unsigned long lastBlinkTime = 0;
bool ledState = false;

void setup() {
  pinMode(BUILTIN_LED, OUTPUT);
  radio.begin();
  radio.openReadingPipe(0, address);
  radio.setPALevel(RF24_PA_LOW);
  radio.startListening();
}

void loop() {
  if (radio.available()) {
    radio.read(&data, sizeof(data));
  }

  if (data.mode == 0) {
    // Manual Control
    servo1.write(map(data.joystickX, 0, 1023, 0, 180)); // Control rudder
    analogWrite(16, map(data.joystickY, 0, 1023, 0, 255)); // Control motor
    digitalWrite(BUILTIN_LED, LOW);
  } else if (data.mode == 1) {
    // Heading Hold Mode
    digitalWrite(BUILTIN_LED, LOW);
    int headingError = data.heading - getCurrentHeading(); // Dummy stuff - replace with PID logic
    int correction = map(headingError, 0, 180, 0, 255);
    analogWrite(15, correction); // Adjust motor/servo
  } else if (data.mode == 2) {
    // Blink Mode
    if (millis() - lastBlinkTime >= 1000) {
      ledState = !ledState;
      digitalWrite(BUILTIN_LED, ledState);
      lastBlinkTime = millis();
    }
  }
}

int getCurrentHeading() {
  // Dummy function for now
  return 0;
}

Update: So I've managed to get the built in LED on the ESP32 to blink when I press the 3rd button on the Uno. Some success!

look over the code below for dealing with buttons,

as for sending data, hoe do you know which byte if the first byte? do you need a preamble to delimit the start of a msg that is synchonized to

// check multiple buttons and toggle LEDs

enum { Off = HIGH, On = LOW };

byte pinsLed [] = { 10, 11, 12 };
byte pinsBut [] = { A1, A2, A3 };
#define N_BUT   sizeof(pinsBut)

byte butState [N_BUT];

// -----------------------------------------------------------------------------
int
chkButtons ()
{
    for (unsigned n = 0; n < sizeof(pinsBut); n++)  {
        byte but = digitalRead (pinsBut [n]);

        if (butState [n] != but)  {
            butState [n] = but;

            delay (10);     // debounce

            if (On == but)
                return n;
        }
    }
    return -1;
}

// -----------------------------------------------------------------------------
void
loop ()
{
    switch (chkButtons ())  {
    case 2:
        digitalWrite (pinsLed [2], ! digitalRead (pinsLed [2]));
        break;

    case 1:
        digitalWrite (pinsLed [1], ! digitalRead (pinsLed [1]));
        break;

    case 0:
        digitalWrite (pinsLed [0], ! digitalRead (pinsLed [0]));
        break;
    }
}

// -----------------------------------------------------------------------------
void
setup ()
{
    Serial.begin (9600);

    for (unsigned n = 0; n < sizeof(pinsBut); n++)  {
        pinMode (pinsBut [n], INPUT_PULLUP);
        butState [n] = digitalRead (pinsBut [n]);
    }

    for (unsigned n = 0; n < sizeof(pinsLed); n++)  {
        digitalWrite (pinsLed [n], Off);
        pinMode      (pinsLed [n], OUTPUT);
    }
}