Loop issue with ezbutton library

I'm using my Mega to communicate with my ESP32 via nRF24L01. I'm using the ezbutton library. I have 3 buttons on my transmitter and when I press each button, I want an LED to light up at the transmitter while a certain action is performed at the receiver's end. I am able to see joystick data come through to my receiver but it shows my joystick's analog values at the time the button was released which means for new joystick data to appear, I have to press ("release" technically) the button each time my joystick is moved. Can someone please assist? I've tried having a while loop nested within my if loop and while that seems to work for my transmitter, I see nothing come through to the receiver. Thanks in advance!

P.S. Yes, functions would help to clean up my code a whole lot, just laying it all out for now since I am still relatively new to functions in C.

// Transmitter

/*
 * Created by ArduinoGetStarted.com
 *
 * This example code is in the public domain
 *
 * Tutorial page: https://arduinogetstarted.com/tutorials/arduino-button-library
 *
 * This example:
 *   + uses debounce for multiple buttons.
 *   + reads state of multiple buttons
 *   + detects the pressed and released events of multiple buttons
 */

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

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

struct __attribute__((packed)) payload {
  uint16_t mode;
  uint16_t joystick[2];
  uint16_t heading;
} data;

ezButton button1(11);  // create ezButton object that attach to pin 11;
ezButton button2(30);  // create ezButton object that attach to pin 30;
ezButton button3(40);  // create ezButton object that attach to pin 40;
const byte led1 = 12;
const byte led2 = 5;
const byte led3 = 4;

void setup() {
  Serial.begin(9600);
  radio.begin();
  radio.openWritingPipe(address);
  radio.setPALevel(RF24_PA_MIN);
  radio.stopListening();
  // add manual mode here - should start at initial setup
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  pinMode(led3, OUTPUT);
  button1.setDebounceTime(50); // set debounce time to 50 milliseconds
  button2.setDebounceTime(50); // set debounce time to 50 milliseconds
  button3.setDebounceTime(50); // set debounce time to 50 milliseconds
}

void loop() {
  button1.loop(); // MUST call the loop() function first
  button2.loop(); // MUST call the loop() function first
  button3.loop(); // MUST call the loop() function first

  int btn1State = button1.getState();
  int btn2State = button2.getState();
  int btn3State = button3.getState();

  radio.write(&data, sizeof(data));

  if(button1.isReleased()){
    digitalWrite(led1, HIGH);
    digitalWrite(led2, LOW);
    digitalWrite(led3, LOW);
    data.mode = 0;
    int rudder, throttle;
    rudder = analogRead(A0);
    throttle = 1023 - analogRead(A1);
    data.joystick[0] = rudder;
    data.joystick[1] = throttle;
    Serial.print("Manual mode is active");
    Serial.print(rudder); Serial.print("|"); Serial.print(throttle); Serial.println();
  }
  else if (button2.isReleased()){
    digitalWrite(led1, LOW);
    digitalWrite(led2, HIGH);
    digitalWrite(led3, LOW);
    data.mode = 1;
    Serial.println("Heading hold mode is active");
  }
  else if (button3.isReleased()){
    digitalWrite(led1, LOW);
    digitalWrite(led2, LOW);
    digitalWrite(led3, HIGH);
    data.mode = 2;
    Serial.println("Waypoint navigation mode is active");
  }
}

// Receiver

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

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

const int IN1 = 32;   // Motor direction pin 1
const int IN2 = 33;   // Motor direction pin 2
const int PWMA = 25;  // Motor speed control (PWM)
const int STBY = 26; // Motor stby pin
const int servo1_pin = 14; // Servo pin

Servo servo1;

struct __attribute__((packed)) payload {
  uint16_t mode; // 0: Manual, 1: Heading Hold, 2: Blink Mode
  uint16_t joystick[2];
  uint16_t heading;
} data;

void setup() {
  Serial.begin(9600);
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(PWMA, OUTPUT);
  pinMode(STBY, OUTPUT);
  servo1.attach(servo1_pin);
  pinMode(LED_BUILTIN, 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) {
    Serial.print("Mode 0 received");
    int rudder = data.joystick[0];
    int throttle = data.joystick[1];
    Serial.print(rudder); Serial.print("|"); Serial.print(throttle); Serial.println();
    
    if (throttle > 562) { // Forward; 50 added as deadzone to center position of 512 for stability
      digitalWrite(IN1, HIGH);
      digitalWrite(IN2, LOW);
      digitalWrite(STBY, HIGH);
      analogWrite(PWMA, map(throttle, 562, 1023, 0, 255));
  } else if (throttle < 462) { // Reverse
      digitalWrite(IN1, LOW);
      digitalWrite(IN2, HIGH);
      digitalWrite(STBY, HIGH);
      analogWrite(PWMA, map(throttle, 0, 462, 255, 0));
  } else { // Stop
      digitalWrite(STBY, LOW);
  }

    int servo_angle = map(rudder, 0, 1023, 0, 180);
    servo1.write(servo_angle);
    }
   else if (data.mode == 1) {
    // Heading Hold Mode
    Serial.print("Mode 1 received");
    digitalWrite(LED_BUILTIN, HIGH); // Test code
  }
  else if (data.mode == 2) {
    // Blink Mode
    Serial.println("Mode 2 received");
    digitalWrite(LED_BUILTIN, HIGH); // Test code
    delay(1000);
    digitalWrite(LED_BUILTIN, LOW);
    delay(1000);
  }
}
}

Quick idea, gotta jet but why not do this

    int rudder, throttle;
    rudder = analogRead(A0);
    throttle = 1023 - analogRead(A1);
    data.joystick[0] = rudder;
    data.joystick[1] = throttle;
    Serial.print("Manual mode is active");
    Serial.print(rudder); Serial.print("|"); Serial.print(throttle); Serial.println();

not in the same code that responds to button1, but outside the button stuff always, or maybe just if the data.mode is 0?

a7

Reverse the buttons, use INPUT_PULLUP and a press is equal to LOW.

Thanks for that, @alto777. You mean like in a switch case format? I modified my transmitter code to implement a switch case based on which LED was on. Serial monitor shows me what I want so it's working how I want it. However, I believe I will have to do a similar switch case on the receiver side as well. Feels like I'm getting close but also not getting close haha. Attached my transmitter code here to see if it's along the lines of what you're saying.

/*
 * Created by ArduinoGetStarted.com
 *
 * This example code is in the public domain
 *
 * Tutorial page: https://arduinogetstarted.com/tutorials/arduino-button-library
 *
 * This example:
 *   + uses debounce for multiple buttons.
 *   + reads state of multiple buttons
 *   + detects the pressed and released events of multiple buttons
 */

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

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

struct __attribute__((packed)) payload {
  uint16_t mode;
  uint16_t joystick[2];
  uint16_t heading;
} data;

ezButton button1(11);  // create ezButton object that attach to pin 11;
ezButton button2(30);  // create ezButton object that attach to pin 30;
ezButton button3(40);  // create ezButton object that attach to pin 40;
const byte led1 = 12;
const byte led2 = 5;
const byte led3 = 4;

void setup() {
  Serial.begin(9600);
  radio.begin();
  radio.openWritingPipe(address);
  radio.setPALevel(RF24_PA_MIN);
  radio.stopListening();
  // add manual mode here - should start at initial setup
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  pinMode(led3, OUTPUT);
  button1.setDebounceTime(50); // set debounce time to 50 milliseconds
  button2.setDebounceTime(50); // set debounce time to 50 milliseconds
  button3.setDebounceTime(50); // set debounce time to 50 milliseconds
}

void loop() {
  button1.loop(); // MUST call the loop() function first
  button2.loop(); // MUST call the loop() function first
  button3.loop(); // MUST call the loop() function first

  int btn1State = button1.getState();
  int btn2State = button2.getState();
  int btn3State = button3.getState();

  radio.write(&data, sizeof(data));

  // if(button1.isReleased()){
  //   digitalWrite(led1, HIGH);
  //   digitalWrite(led2, LOW);
  //   digitalWrite(led3, LOW);

    switch (chkButtons()){
      case 0:
      data.mode = 0;
    int rudder, throttle;
    rudder = analogRead(A0);
    throttle = 1023 - analogRead(A1);
    data.joystick[0] = rudder;
    data.joystick[1] = throttle;
    Serial.print("Manual mode is active");
    Serial.print(rudder); Serial.print("|"); Serial.print(throttle); Serial.println();
      break;

      case 1:
      data.mode = 1;
    Serial.println("Heading hold mode is active");
      break;
    }
    // data.mode = 0;
    // int rudder, throttle;
    // rudder = analogRead(A0);
    // throttle = 1023 - analogRead(A1);
    // data.joystick[0] = rudder;
    // data.joystick[1] = throttle;
    // Serial.print("Manual mode is active");
    // Serial.print(rudder); Serial.print("|"); Serial.print(throttle); Serial.println();
  // }
  // else if (button2.isReleased()){
  //   digitalWrite(led1, LOW);
  //   digitalWrite(led2, HIGH);
  //   digitalWrite(led3, LOW);
    // data.mode = 1;
    // Serial.println("Heading hold mode is active");
  // }
  // else if (button3.isReleased()){
  //   digitalWrite(led1, LOW);
  //   digitalWrite(led2, LOW);
  //   digitalWrite(led3, HIGH);
  //   data.mode = 2;
  //   Serial.println("Waypoint navigation mode is active");
  // }
}

int chkButtons(){
  if(button1.isPressed()){
    digitalWrite(led1, HIGH);
    digitalWrite(led2, LOW);
    digitalWrite(led3, LOW);
    return 0;
  }
  else if (button2.isPressed()){
    digitalWrite(led1, LOW);
    digitalWrite(led2, HIGH);
    digitalWrite(led3, LOW);
    return 1;
  }
  else if (button3.isPressed()){
    digitalWrite(led1, LOW);
    digitalWrite(led2, LOW);
    digitalWrite(led3, HIGH);
    return 2;
}
}

Thanks for the input, @sonofcy. The ezbutton library implements buttons as INPUT_PULLUP according to the source code. Could you please elaborate?

Great. So did you write your code so that a LOW is the Pressed or ON. The way you wrote your problem statement it sounds like the buttons are being interpreted as Pressed or ON in the HIGH (PULLUP) position.
Perhaps if you restate the problem?

So my main goal here was to use the buttons as toggle switches where pressing one button turns on the corresponding LED which stays on as long as another button isn't pressed. The issue I was facing when I first posted this was that the action to be performed when a button is pressed or released wasn't continuing in a loop. Now I've implemented a switch case within the loop and that seems to be sending data in a loop like I want (updated sketch posted above as my reply to alto777).

That can work and you know more about what you doing, but I literally meant just move those lines, and possibly test whether to execute:

void loop() {
  button1.loop(); // MUST call the loop() function first
  button2.loop(); // MUST call the loop() function first
  button3.loop(); // MUST call the loop() function first

  int btn1State = button1.getState();
  int btn2State = button2.getState();
  int btn3State = button3.getState();

  radio.write(&data, sizeof(data));

  if(button1.isReleased()){
    digitalWrite(led1, HIGH);
    digitalWrite(led2, LOW);
    digitalWrite(led3, LOW);
    data.mode = 0;
// not here ...
//   int rudder, throttle;
//    rudder = analogRead(A0);
//    throttle = 1023 - analogRead(A1);
//    data.joystick[0] = rudder;
//    data.joystick[1] = throttle;
    Serial.print("Manual mode is active");
    Serial.print(rudder); Serial.print("|"); Serial.print(throttle); Serial.println();
  }
  else if (button2.isReleased()){
    digitalWrite(led1, LOW);
    digitalWrite(led2, HIGH);
    digitalWrite(led3, LOW);
    data.mode = 1;
    Serial.println("Heading hold mode is active");
  }
  else if (button3.isReleased()){
    digitalWrite(led1, LOW);
    digitalWrite(led2, LOW);
    digitalWrite(led3, HIGH);
    data.mode = 2;
    Serial.println("Waypoint navigation mode is active");
  }
 
//... here either with the test of data.mode or not
  if (data.mode == 0) {
    int rudder, throttle;
    rudder = analogRead(A0);
    throttle = 1023 - analogRead(A1);
    data.joystick[0] = rudder;
    data.joystick[1] = throttle;
  }
}

But you are on a good track, so.

a7

1 Like

I didn't think the OP was complaining, presumably @theswaggyd knows that

  if (button1.isReleased()) {

can simply be replaced by

  if (button1.isPressed()) {

and I hope the buttons are wired as ezButton expects, between the input pin and ground. It uses INPUT_PULLUP mode, presence of an external pullup resistor would not be necessary nor would it hurt.

a7

1 Like

Apologies for the delayed response.

That is exactly what I was trying to do! I just couldn't express it in code. I come from a MATLAB background, still getting used to C code. So now I have the basic layout done and things seem to be working fine. Thanks @alto777, I will mark down your suggestion as the solution. I am also reposting my code here in case anyone find any of this useful.

// Transmitter

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

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

struct __attribute__((packed)) payload {
  uint16_t mode;
  uint16_t joystick[2];
  uint16_t heading;
} data;

ezButton button1(11);  // create ezButton object that attach to pin 11;
ezButton button2(30);  // create ezButton object that attach to pin 30;
ezButton button3(40);  // create ezButton object that attach to pin 40;
const byte led1 = 12;
const byte led2 = 5;
const byte led3 = 4;

void setup() {
  Serial.begin(9600);
  radio.begin();
  radio.openWritingPipe(address);
  radio.setPALevel(RF24_PA_MIN);
  radio.stopListening();
  data.mode = 0; // Manual mode at startup
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  pinMode(led3, OUTPUT);
  button1.setDebounceTime(50); // set debounce time to 50 milliseconds
  button2.setDebounceTime(50); // set debounce time to 50 milliseconds
  button3.setDebounceTime(50); // set debounce time to 50 milliseconds
}

void loop() {
  button1.loop(); // MUST call the loop() function first
  button2.loop(); // MUST call the loop() function first
  button3.loop(); // MUST call the loop() function first

  int btn1State = button1.getState();
  int btn2State = button2.getState();
  int btn3State = button3.getState();

  radio.write(&data, sizeof(data));

  if(button1.isReleased()){
    data.mode = 0;
    Serial.print("Manual mode is active");
  }
  else if (button2.isReleased()){
    data.mode = 1;
    Serial.println("Heading hold mode is active");
  }
  else if (button3.isReleased()){
    data.mode = 2;
    Serial.println("Waypoint navigation mode is active");
  }

 if (data.mode == 0) {
    digitalWrite(led1, HIGH);
    digitalWrite(led2, LOW);
    digitalWrite(led3, LOW);
    int rudder, throttle;
    rudder = analogRead(A0);
    throttle = 1023 - analogRead(A1);
    data.joystick[0] = rudder;
    data.joystick[1] = throttle;
    Serial.print("Manual mode is active");
    Serial.print(rudder); Serial.print("|"); Serial.print(throttle); Serial.println();
 }
  else if (data.mode == 1) {
    digitalWrite(led1, LOW);
    digitalWrite(led2, HIGH);
    digitalWrite(led3, LOW);
    Serial.println("Heading hold mode is active");
  }
  else if (data.mode == 2) {
    digitalWrite(led1, LOW);
    digitalWrite(led2, LOW);
    digitalWrite(led3, HIGH);
    Serial.println("WP nav mode is active");
    }
}
// Receiver

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

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

const int IN1 = 32;   // Motor direction pin 1
const int IN2 = 33;   // Motor direction pin 2
const int PWMA = 25;  // Motor speed control (PWM)
const int STBY = 26; // Motor stby pin
const int servo1_pin = 14; // Servo pin

int ledState = LOW;
unsigned long previousMillis = 0;
const long interval = 500;

Servo servo1;

struct __attribute__((packed)) payload {
  uint16_t mode; // 0: Manual, 1: Heading Hold, 2: Blink Mode
  uint16_t joystick[2];
  uint16_t heading;
} data;

void setup() {
  Serial.begin(9600);
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(PWMA, OUTPUT);
  pinMode(STBY, OUTPUT);
  servo1.attach(servo1_pin);
  pinMode(LED_BUILTIN, 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) {
    doManualMode();
    }
   else if (data.mode == 1) {
    // Heading Hold Mode - TBD
    //  Serial.println("Mode 1 received");
     digitalWrite(LED_BUILTIN, HIGH);
  }
    else if (data.mode == 2) {
      // Add something here
      // Serial.println("Mode 2 received");
    blinkLED();
    }
  }
}

void blinkLED(){
    unsigned long currentMillis = millis();
      if (currentMillis - previousMillis >= interval){
        previousMillis = currentMillis;
         if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }
      digitalWrite(LED_BUILTIN, ledState);
  }
}

void doManualMode(){
  // Serial.print("Mode 0 received");
    digitalWrite(LED_BUILTIN, LOW);
    int rudder = data.joystick[0];
    int throttle = data.joystick[1];
    Serial.print(rudder); Serial.print("|"); Serial.print(throttle); Serial.println();
    
    if (throttle > 562) { // Forward; 50 added as deadzone to center position of 512 for stability
      digitalWrite(IN1, HIGH);
      digitalWrite(IN2, LOW);
      digitalWrite(STBY, HIGH);
      analogWrite(PWMA, map(throttle, 562, 1023, 0, 255));
  } else if (throttle < 462) { // Reverse
      digitalWrite(IN1, LOW);
      digitalWrite(IN2, HIGH);
      digitalWrite(STBY, HIGH);
      analogWrite(PWMA, map(throttle, 462, 0, 0, 255));
  } else { // Stop
      digitalWrite(STBY, LOW);
  }

    int servo_angle = map(rudder, 0, 1023, 0, 180);
    servo1.write(servo_angle);
}

On a slightly separate issue, wondering if you could point me in the right direction. I want to add a heading hold mode when button 2 is pressed (this project is for my RC boat). Essentially, when the button is pressed, I want to set up a PID loop to hold heading but till a heading is received via the transmitter's serial monitor, I want the boat to hold the current heading. I have some sense of how this should be done - roughly translating to "if heading hold mode active, get current heading (I will have GPS+IMU) and hold that, else hold the new heading that comes through via RF." Any advice would be greatly appreciated! And again, thank you very much!

I sure wasn't complaining there haha. And yes, @alto777, I did notice that the two could be replaced which is why my original post had released while my follow up code last time had pressed. Going through the ezbutton code, I could tell I could replace one with the other for my needs.

@alto777 sorry to bombard you with another message here but on the receiver end, I'm guessing something along the lines of "if data.heading available, use that as the setpoint, else use current heading as setpoint" should work (going to try this in the next few days). On the transmitter side, I need to figure out the code to send numbers through the serial monitor but I've been more concerned with how the receiver should interpret things.

Please let me know if this warrants a new topic altogether. Thanks again!

It's just a matter of designing when you set/update the setpoint-- If the PID was running in the transmitter, I would think you should update the setpoint when the hold heading button is triggered. If you were using a Arduino PID library-like variable names, it might fit into your #10 code like:

  else if (button2.isReleased()){
    data.mode = 1;
    Serial.println("Heading hold mode is active");
    Setpoint = currentHeading;
  }

But if you are running the PID in the receiver, the trick is to detect the change into "hold heading mode" and copy the currentHeading to the Setpoint:

   else if (data.mode == 1) {
    // Heading Hold Mode - TBD
    if( lastMode != data.mode) { // mode changed, initialize
        Setpoint = currentHeading;
        //  Serial.println("Mode 1 received");
        digitalWrite(LED_BUILTIN, HIGH);
    }
    // do the work
    Input = currentHeading;
    myPID.compute();
    adjustRudderPerPID(Output);
  }

Hi @DaveX, apologies for the delayed response, been traveling over the last week.

Thanks a lot for the feedback, appreciate it all! Detecting the change on the receiver side is a brilliant way of doing it! I was struggling to think about it like that. I modified it a little bit to get it to do what I want (using 0 for currentHeading for now) but I am unable to "reset" the heading hold function once I switch out of it and get back to it again. As in, it'll hold on to the last demanded value and won't reset to the current heading when I enter the heading hold mode from manual mode.

// Transmitter

else if (data.mode == 1) {
    digitalWrite(led1, LOW);
    digitalWrite(led2, HIGH);
    digitalWrite(led3, LOW);
    if (Serial.available() > 0) {
    String input = Serial.readStringUntil('\n'); // Read input
    input.trim(); // Remove extra spaces

    // Convert input to double
    data.heading = input.toDouble();
    }
    // Serial.println("Heading hold mode is active");
  }
// Receiver

 else if (data.mode == 1) {
    // Heading Hold Mode - TBD
    //  Serial.println("Mode 1 received");
     if (lastMode != data.mode){
       if(!data.heading){
       double hdg_dmd = 0; // Dummy - replace with current heading
         Serial.println(hdg_dmd);
       } else {
         double hdg_dmd = data.heading;
         Serial.println(hdg_dmd);
       }
     }
   }

There might be an issue with how lastmode is being handled. How does the whole code look? The change detection trick depends on lastmode tracking data.mode and there being one iteration through loop() where they differ. In the posted snippet I can’t see how lastMode is updated.

I'm fairly sure it's exactly that. I have attached my entire code here.

// Transmitter

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

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

struct __attribute__((packed)) payload {
  uint16_t mode;
  uint16_t joystick[2];
  uint16_t heading;
  float waypoints[2];
} data;

ezButton button1(11);  // create ezButton object that attach to pin 11;
ezButton button2(30);  // create ezButton object that attach to pin 30;
ezButton button3(40);  // create ezButton object that attach to pin 40;
const byte led1 = 12;
const byte led2 = 5;
const byte led3 = 4;

void setup() {
  Serial.begin(9600);
  radio.begin();
  radio.openWritingPipe(address);
  radio.setPALevel(RF24_PA_MIN);
  radio.stopListening();
  data.mode = 0; // Manual mode at startup
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  pinMode(led3, OUTPUT);
  button1.setDebounceTime(50); // set debounce time to 50 milliseconds
  button2.setDebounceTime(50); // set debounce time to 50 milliseconds
  button3.setDebounceTime(50); // set debounce time to 50 milliseconds
}

void loop() {
  button1.loop(); // MUST call the loop() function first
  button2.loop(); // MUST call the loop() function first
  button3.loop(); // MUST call the loop() function first

  int btn1State = button1.getState();
  int btn2State = button2.getState();
  int btn3State = button3.getState();

  radio.write(&data, sizeof(data));

  if(button1.isReleased()){
    data.mode = 0;
    Serial.println("Manual mode is active");
  }
  else if (button2.isReleased()){
    data.mode = 1;
    Serial.println("Heading hold mode is active");
  }
  else if (button3.isReleased()){
    data.mode = 2;
    Serial.println("Waypoint navigation mode is active");
  }

 if (data.mode == 0) {
    digitalWrite(led1, HIGH);
    digitalWrite(led2, LOW);
    digitalWrite(led3, LOW);
    int rudder, throttle;
    rudder = analogRead(A0);
    throttle = 1023 - analogRead(A1);
    data.joystick[0] = rudder;
    data.joystick[1] = throttle;
    Serial.print(rudder); Serial.print("|"); Serial.print(throttle); Serial.println();
 }
  else if (data.mode == 1) {
    digitalWrite(led1, LOW);
    digitalWrite(led2, HIGH);
    digitalWrite(led3, LOW);
    if (Serial.available() > 0) {
    String input = Serial.readStringUntil('\n'); // Read input
    input.trim(); // Remove extra spaces

    // Convert input to double
    data.heading = input.toDouble();
    }
    // Serial.println("Heading hold mode is active");
  }
  else if (data.mode == 2) {
    digitalWrite(led1, LOW);
    digitalWrite(led2, LOW);
    digitalWrite(led3, HIGH);
    // Serial.print("WP nav mode is active");
    float targetLat, targetLon;
    if (Serial.available() > 0) {
    String input = Serial.readStringUntil('\n'); // Read the input line
    input.trim(); // Remove extra spaces

    // Parse latitude and longitude
    int spaceIndex = input.indexOf(',');
    if (spaceIndex != -1) {
      targetLat = input.substring(0, spaceIndex).toFloat();
      targetLon = input.substring(spaceIndex + 1).toFloat();
      data.waypoints[0] = targetLat;
      data.waypoints[1] = targetLon;
  }
}
  }
}
  
// Receiver

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

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

const int IN1 = 25;   // Motor direction pin 1
const int IN2 = 33;   // Motor direction pin 2
const int PWMA = 32;  // Motor speed control (PWM)
const int STBY = 26; // Motor stby pin
const int servo1_pin = 14; // Servo pin

int ledState = LOW;
unsigned long previousMillis = 0;
const long interval = 500;

int lastMode = 0; // Tracking operation mode

Servo servo1;

bool received = false;

struct __attribute__((packed)) payload {
  uint16_t mode; // 0: Manual, 1: Heading Hold, 2: Blink Mode
  uint16_t joystick[2];
  uint16_t heading;
  float waypoints[2];
} data;

void setup() {
  Serial.begin(9600);
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(PWMA, OUTPUT);
  pinMode(STBY, OUTPUT);
  servo1.attach(servo1_pin);
  pinMode(LED_BUILTIN, 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) {
    doManualMode();
    }
   else if (data.mode == 1) {
    // Heading Hold Mode - TBD
    //  Serial.println("Mode 1 received");
     if (lastMode != data.mode){
       if(!data.heading){
       double hdg_dmd = 0; // Dummy - replace with current heading
         Serial.println(hdg_dmd);
       } else {
         double hdg_dmd = data.heading;
         Serial.println(hdg_dmd);
       }
     }
   }
    else if (data.mode == 2) {
      float targetLat = data.waypoints[0];
      float targetLon = data.waypoints[1];
      Serial.print(targetLat,5); Serial.print(","); Serial.print(targetLon,5); Serial.println();
    blinkLED();
    }
  }
}

void blinkLED(){
    unsigned long currentMillis = millis();
      if (currentMillis - previousMillis >= interval){
        previousMillis = currentMillis;
         if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }
      digitalWrite(LED_BUILTIN, ledState);
  }
}

void doManualMode(){
  // Serial.print("Mode 0 received");
    digitalWrite(LED_BUILTIN, LOW);
    int rudder = data.joystick[0];
    int throttle = data.joystick[1];
    Serial.print(rudder); Serial.print("|"); Serial.print(throttle); Serial.println();
    
    if (throttle > 562) { // Forward; 50 added as deadzone to center position of 512 for stability
      digitalWrite(IN1, HIGH);
      digitalWrite(IN2, LOW);
      digitalWrite(STBY, HIGH);
      analogWrite(PWMA, map(throttle, 562, 1023, 0, 255));
  } else if (throttle < 462) { // Reverse
      digitalWrite(IN1, LOW);
      digitalWrite(IN2, HIGH);
      digitalWrite(STBY, HIGH);
      analogWrite(PWMA, map(throttle, 462, 0, 0, 255));
  } else { // Stop
      digitalWrite(STBY, LOW);
  }

    int servo_angle = map(rudder, 0, 1023, 0, 180);
    servo1.write(servo_angle);
}

Try saving lastMode with a line like this at the end of the loop:

lastMode = data.mode;

like:

void loop() {
  if (radio.available()) {
    radio.read(&data, sizeof(data));
    if (data.mode == 0) {
      doManualMode();
    }
    else if (data.mode == 1) {
      // Heading Hold Mode - TBD
      //  Serial.println("Mode 1 received");
      if (lastMode != data.mode) {
        if (!data.heading) {
          double hdg_dmd = 0; // Dummy - replace with current heading
          Serial.println(hdg_dmd);
        } else {
          double hdg_dmd = data.heading;
          Serial.println(hdg_dmd);
        }
      }
    }
    else if (data.mode == 2) {
      float targetLat = data.waypoints[0];
      float targetLon = data.waypoints[1];
      Serial.print(targetLat, 5); Serial.print(","); Serial.print(targetLon, 5); Serial.println();
      blinkLED();
    }
  }
  lastMode = data.mode;  // save the mode for comparison next iteration.
}

You could also do it a bit in the I.P.O. model style and set a flag for a oneshot at the top of the loop and then use it for the processing part:

void loop() {
  if (radio.available()) {
    radio.read(&data, sizeof(data));
    bool modeChanged = (data.mode != lastMode );
    if (data.mode == 0) {
      doManualMode();
    }
    else if (data.mode == 1) {
      // Heading Hold Mode - TBD
      //  Serial.println("Mode 1 received");
      if (modeChanged) {
        if (!data.heading) {
          double hdg_dmd = 0; // Dummy - replace with current heading
          Serial.println(hdg_dmd);
        } else {
          double hdg_dmd = data.heading;
          Serial.println(hdg_dmd);
        }
      }
    }
    else if (data.mode == 2) {
      float targetLat = data.waypoints[0];
      float targetLon = data.waypoints[1];
      Serial.print(targetLat, 5); Serial.print(","); Serial.print(targetLon, 5); Serial.println();
      blinkLED();
    }
    if(modeChanged){  // report and update for next iteration
      Serial.print("Mode changed from ");
      Serial.print(lastMode);
      Serial.print(" to ");
      Serial.println(data.mode);
      lastMode = data.mode;
    }
  }
}

Thanks a lot for your suggestions, @DaveX!

I'm still not able to get the values to reset after a mode change and I think the "if (!data.heading)" might have something to do with that as in, "if no data received" might not be the same in code as no data.heading. I'll keep tinkering with this and try to figure it out. Appreciate all the assistance!

The 'data' struct in the transmitting program will persist the last assigned data.heading value, and the whole "data" struct will be transmitted and received, so there really isn't an "no data received" option--it will always re-receive whatever the last data.heading that was assigned in the transmitting code.

Well, (!data.heading) is true if data.heading is 0, so essentially either branch prints out data.heading, and you could then replace the whole if-else structure with:

 Serial.println((double)data.heading); 

...and get the same results.

You might also auto-reformat the code to make the indenting match the structure. Proper indenting sometimes helps identify unexpected behavior or logic.

Does the printing show what you expect? With the change-of-mode-detection you could write a richer set of print statements and see what messages are being received/transmitted.

1 Like

Understood, and thanks for the explanation. Yes, I realized fairly quickly that "no data received" wasn't quite happening since the last transmission was still being sent.

I have since replaced the if-else structure in a similar manner like you have.

I'm cleaning up my code, including indentation, right now. Agreed - much easier looking through code when it's neatly indented.

The change of mode doesn't keep the heading hold loop going. The loop stops but takes a value sent over RF. The next time I get into this mode, it shows the last heading demand value once. So somewhere the loop stops. At least now I see which direction to go in so I'll keep at it for a bit. It's not too bad if I don't make this work for now since technically, I can still send data and make the receiver act on it.

I'm relatively new to C code so might take me some time as I pick up syntax and get better at things haha but I've seen this before so I know it's possible. Enjoying learning as I go so hopefully I'll crack it at some point.

Again, truly appreciate the inputs!