Nerf targets -NRF24, arduino nano and shock sensor

Hi everyone!

First of all, I would like to apologize in advance : english is not my first language therefore please be kind with me if my syntax is not perfect.

My project consists in connected targets, 11 in total (1 start target, 10 "real" targets), designed for NERF play at home.

3 game modes, selected prior to hitting the start target that launches a timer :

  • random (activates a random target, needs 10 targets hit to end),
  • speed (all 10 targets are activated, game ends when all have been hit)
  • infinite (randomly activated targets, game ends when the start target is hit again)

Hardware is as follows :
Start target (master) :

  • Arduino Nano,
  • NRF24 module for communication with the 10 other targets (slaves)
  • SW420 vibration module
  • JDY-31 bluetooth module for communication with the main system (a rasp Pi running Python code)

"Real" targets (slaves) :

  • Arduino Nano,
  • NRF24 module for communication with the master
  • SW420 vibration module

What I want to be able to do is as follows :

  1. Master (start target) receives a command from the python code with Serial (trouhgh the JDY-31)
  2. Depending on the command, a game mode is selected
  3. When the master is hit, it communicates with the other targets (using the NRF24 modules) to activate them and wait for their answer
  4. When an answer is received, the master sends it to the raspberry that either says "i need more" or "that's it, the end".

My python code is almost ready but at this stage I need to progress with the Arduino side of things, to test if everything is working properly one step at a time.

For the Arduino code, I'm an absolute beginner. I started with the blink example, added the condition "if serial input is this, turn on the LED. Then, if the shock sensor is triggered, turn the LED off".

I am starting to grasp how it works but I am currently struggling with the NRF24 communication thing.

The (great) examples provided by Robin2 in his Simple nRF24L01+ 2.4GHz transceiver demo work on my modules. SimpleTx, two way communication and other sketches are working with my modules.

However, they are still a bit too complicated for my understanding and even though I used his work to do some trial and error coding, and tried to deconstruct the code to understand it better, I'm stuck.

Here is where I'm at (as I said, I am a beginner and trying to go step by step) :

  • The master can receive a command from Serial, turn on the LED, and when the shock sensor is triggered it communicates with a slave (just one for now).
  • The slave is "activated", turns on the LED, waits for the shock sensor to be triggered to turn the LED off and to send a reply to the master.

Problem is : my master does not receive any reply and I don't know if it has been sent by the NRF24 module...

I have tried using radio.startListening() and radio.stopListening() but without good and consistent results.

Any help or guidance would be much appreciated. I am not asking you to write my code for me (i'm a stubborn guy and want to experiment myself, to get better at it, instead of just copy-pasting something).

Here is the current code for my master:

// Master

//RF24_250KBPS for 250kbs, RF24_1MBPS for 1Mbps, or RF24_2MBPS for 2Mbps

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


#define CE_PIN   9
#define CSN_PIN 10

const byte masterAddress[5] = {'T','X','a','a','a'};
const byte slaveAddress[5] = {'R','x','A','A','A'};

RF24 radio(CE_PIN, CSN_PIN);

int serialData;
int vit = 12;
int dataRecue; 

int shocksensor = 4; //pin de connection du capteur de choc
int LED = 5 ; // pin de la led

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

void setup() {

    Serial.begin(9600);

    pinMode(LED, OUTPUT);
    pinMode(shocksensor, INPUT);
    radio.begin();
    radio.setDataRate( RF24_1MBPS );
    
    radio.openWritingPipe(slaveAddress);
    radio.openReadingPipe(1, masterAddress);
}

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

void loop() {
 
 if(Serial.available() > 0){
    serialData = Serial.read();
     
    if (serialData == 'Z'){ // si python lance le mode vitesse
        digitalWrite(LED, HIGH);
        while (1){
            if (digitalRead(shocksensor)){
            digitalWrite(LED, LOW);
            Serial.println("u"); //démarre le timer côté Python
            radio.write( &vit, sizeof(vit) );
            radio.startListening();
              if ( radio.available() ) {
                  radio.read(&dataRecue, sizeof(dataRecue)); // réception de la réponse
                  Serial.println(dataRecue); // contrôle de la réponse
                  radio.stopListening();
              }
            break;
            }
        }
    } // fin du mode vitesse dans la boucle 
 
  } // fin ifserial available
 
} // fin de la boucle

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

And here is the code for my slave :

// Slave 1

//RF24_250KBPS for 250kbs, RF24_1MBPS for 1Mbps, or RF24_2MBPS for 2Mbps

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

#define CE_PIN   9
#define CSN_PIN 10

const byte masterAddress[5] = {'T','X','a','a','a'};
const byte thisSlaveAddress[5] = {'R','x','A','A','A'};

RF24 radio(CE_PIN, CSN_PIN);

int dataRecue;
int vit = 21;// valeur à renvoyer mode vitesse

int shocksensor = 4; // pin de connection du capteur de choc
int LED = 5; // pin de la led
//==============

void setup() {
    Serial.begin(9600);
    pinMode(LED, OUTPUT);
    pinMode(shocksensor, INPUT);

    radio.begin();
    radio.setDataRate( RF24_1MBPS ); 
    radio.openWritingPipe(masterAddress);
    radio.openReadingPipe(1, thisSlaveAddress);
    
    radio.startListening();

    
}

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

void loop() {
   recupdata();
   modevitesse();

} // fin boucle principale


//============
void recupdata () { // récupération des infos du master
     if ( radio.available() ) {
      radio.read( &dataRecue, sizeof(dataRecue) );
      Serial.println(dataRecue);
      radio.stopListening();
     }
}

//================
void modevitesse(){
    if (dataRecue == 12) {
      digitalWrite (LED, HIGH);
      while (1){
        if (digitalRead(shocksensor)){
          digitalWrite (LED, LOW);
          radio.write (&vit, sizeof(vit));
          Serial.println(vit); //juste pour le contôle
          radio.startListening();
          break;
        }
      }
    }
}

My comments are in french but it should be pretty obvious what they mean in english.

Side note : I have carefully read topics where Robin2 explains the concept of "pipes" and "addresses" and (please correct me if I'm wrong), I should be fine with just 1 master address and 1 slave address.
Slaves can listen all the time, waiting for their command, and when they need to reply, they will never do it all at once (they need to be hit first), so no collisions.

Thing is, when the master issues a command to a slave, it needs to wait for an answer that might take some time to come...

Thank you all for your help, I will be happy to share my progress with you here!

Well, I have made some progress.

It's not perfect but I'm almost with what I expect from the code. I tried another way of doing things, using this tutorial and got as far as having the master respond to a Serial command, send a signal to a slave once the shock sensor has been triggered, have the slave "activate" and send a response well received by the master once its own shoch sensor has been triggered.

The solution used is not perfect because it adds delay. I read on several occasions that this delay is necessary to give time to the NRF24 module to switch from Tx to Rx... However, I am trying to create a game where timing is important and where activating targets needs to be as fast as possible.

Any guidance or insight on this "issue" ?

Also, now I need to understand why the master cannot receive another command, unless I close the serial monitor and open it back again. I will probably need to try sending commands from my python code to see if it is a coding issue or just the arduino IDE causing this problem.

If anyone can tell me why I cannot "start" another game using the "Z" command in my code below, please tell ! :slight_smile:

Master code

    /*
    * MASTER
    */
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

#define LED 5
#define shocksensor 4
    
    RF24 radio(9, 10); // CE, CSN
    const byte addresses[][6] = {"00001", "00002"};
   int dataRecue;
   int reponse;
   char serialData;

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

    void setup() {
      Serial.begin(9600);
      pinMode(LED, OUTPUT);
      pinMode(shocksensor, INPUT);
      radio.begin();
      radio.openWritingPipe(addresses[1]); // 00002
      radio.openReadingPipe(1, addresses[0]); // 00001
      radio.setPALevel(RF24_PA_MIN);
    }

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

    void loop() {
      if(Serial.available() > 0) {
        serialData = Serial.read();

      delay(5);
      radio.stopListening();
      
      traitementData();

      delay(5);
      radio.startListening();
      while (!radio.available());
      radio.read(&dataRecue, sizeof(dataRecue));
      Serial.println(dataRecue);

              } // fin if serial available
    } // fin de la boucle

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

void traitementData() {
  if (serialData == 'Z') { // mode Vitesse
    digitalWrite(LED, HIGH);
    reponse = 15;
    while (1){
        if (digitalRead(shocksensor)){
        digitalWrite(LED, LOW);
        Serial.println("TOUCHE"); //pour contrôle
        radio.write( &reponse, sizeof(reponse) );
        Serial.println(reponse); // pour contrôle
        break;
            }
        } // fin du while
  } // fin du mode Vitesse
  
  else if (serialData == 'V') { // mode vérification
    reponse = 12;
    radio.write( &reponse, sizeof(reponse) );
    Serial.println("START = OK");
  }
}

Slave code

    /*
    * SLAVE
    */
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
    
#define LED 5
#define shocksensor 4
    
    RF24 radio(9, 10); // CE, CSN
    const byte addresses[][6] = {"00001", "00002"};
    int dataRecue;
    int reponse;
    int reception = 0;
    
//=========================================================    
    void setup() {
      Serial.begin(9600);
      pinMode(shocksensor, INPUT);
      pinMode(LED, OUTPUT);
      radio.begin();
      radio.openWritingPipe(addresses[0]); // 00001
      radio.openReadingPipe(1, addresses[1]); // 00002
      radio.setPALevel(RF24_PA_MIN);
    }

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

    void loop() {
      delay(5);
      radio.startListening();
      if ( radio.available()) {
        while (radio.available()) {
          radio.read(&dataRecue, sizeof(dataRecue));
          if (dataRecue > 0) {
            reception = 1;
          }
          Serial.println(dataRecue);
        }

        delay(5);
        radio.stopListening();
        traitementData();
        
      } // fin if radio available
    } // fin de la boucle

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

 void traitementData() {
  if (reception == 1) {
    if (dataRecue == 12) { // vérification
      reponse = 21;
      radio.write( &reponse, sizeof(reponse) );
      Serial.println(reponse); // pour contrôle
    }
    
    else if (dataRecue == 15) { // modes de jeu
      digitalWrite(LED, HIGH);
      reponse = 51;
      while (1){
         if (digitalRead(shocksensor)){
         digitalWrite(LED, LOW);
         Serial.println("TOUCHE"); //pour contrôle
         radio.write( &reponse, sizeof(reponse) );
         Serial.println(reponse); // pour contrôle
         reception = 0;
         break;
            }
        } // fin du while
    }
  }
 } // fin traitementData

In my slave's code, I added a "reception" variable. Receiving data from the master sets it to 1, allowing the traitementData code to be played. I'm not sure this is really necessary but it seems like it doesn't hurt to have it there. Any idea on that ?

Working with arduinos is so fun... I wish we had these in school to learn the basics of programming and building little toys and gadgets!

Robin2's tutorial shows you how to do that communication without having master/slave swap back and forth. It uses the response packet (or something like that - it is one of the examples). That would be faster

I tried modifying Robin2's examples (the Tx Rx swapping roles) but I could not get my own part of the code working with it.

As I said I am still a complete beginner with arduinos and Robin2's code is still above my understanding and knowledge. I have read this forum exchange as well to try and understand it better but as I understand it, the ackpayload needs to be loaded before anything else happens.

However, the master and slaves both have different commands or answers that need to be sent.

Would I need to update "ackdata" each time I want to send a command, or can I pre-set the commands and answers and use these variables with ackpayload ?

I have tried a few things to make my master's code loop correctly so I can send new commands through serial but I'm still stuck. I tried using "while" and placing snippets of code differently but to no avail.

It runs well for the first command sent with Serial (a "Z" in my code below), it does what I expect. But it does not register new commands. If I try to send another "Z", it doesnt pick it up... Any ideas why ?

It does register new serial commands when I close the serial monitor from the arduino IDE and open it up again though.

I would really appreciate guidance here :confused:

nalpak:
Master code

    /*

* MASTER
    */
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

#define LED 5
#define shocksensor 4
   
    RF24 radio(9, 10); // CE, CSN
    const byte addresses[][6] = {"00001", "00002"};
  int dataRecue;
  int reponse;
  char serialData;

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

void setup() {
      Serial.begin(9600);
      pinMode(LED, OUTPUT);
      pinMode(shocksensor, INPUT);
      radio.begin();
      radio.openWritingPipe(addresses[1]); // 00002
      radio.openReadingPipe(1, addresses[0]); // 00001
      radio.setPALevel(RF24_PA_MIN);
    }

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

void loop() {
      if(Serial.available() > 0) {
        serialData = Serial.read();

delay(5);
      radio.stopListening();
     
      traitementData();

delay(5);
      radio.startListening();
      while (!radio.available());
      radio.read(&dataRecue, sizeof(dataRecue));
      Serial.println(dataRecue);

} // fin if serial available
    } // fin de la boucle

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

void traitementData() {
  if (serialData == 'Z') { // mode Vitesse
    digitalWrite(LED, HIGH);
    reponse = 15;
    while (1){
        if (digitalRead(shocksensor)){
        digitalWrite(LED, LOW);
        Serial.println("TOUCHE"); //pour contrôle
        radio.write( &reponse, sizeof(reponse) );
        Serial.println(reponse); // pour contrôle
        break;
            }
        } // fin du while
  } // fin du mode Vitesse
 
  else if (serialData == 'V') { // mode vérification
    reponse = 12;
    radio.write( &reponse, sizeof(reponse) );
    Serial.println("START = OK");
  }
}

Edit : I am pretty sure my problem lies with the if(Serial.available >0) at the beginning. I think the code is stuck in this "if" and does not loop back at the beginning because of it.
All I need is the Serial to remain open and register new commands when they are sent.

Edit 2 : or maybe it comes from the Serial.available, or Serial.read... Closing the Serial monitor would maybe "empty" the stuff in memory and would help clean things up. But I tried Serial.flush() without fixing my problem.

This line

    while (!radio.available());

waits forever for something to be available. If nothing is available, you code is stuck here forever. If you close/re-open the serial monitor, that resets the board so that is why it works again. You can test this theory out by putting a Serial.println() before and after it and see what shows up on the serial monitor

Oh wow. blh64, thank you for pointing that out, I was definitely looking in the wrong place.

Now I can see where to look and try to figure out how I am going to solve my next issue...

My python code (that I am much more comfortable with) is written in a way that it just sends a command to the arduino master, waits for all replies to be received (counts them, handles them), and then moves to something else, for example sending a new command to the arduino master.

I guess that in order to be able to break from the while(!radio.available()), I will have to move part of my python code to the arduino. I will need the arduino to handle the replies, to know when it can break out of this while loop.

I will also try another approach, using a flag like newData=True/False to see if I can make it work that way.

Thanks for your help blh64 !

Well I feel really stupid ::slight_smile:

I believe I have solved my problem, by applying common sense, blh64 advice to look into the while(!radio.available()) thing and just thinking again about what tasks I want my code to perform.

I went back to robin2's tutorials on nrf24, especially the two way communication and... well, I feel stupid. I also tried to apply his advice on "breaking down the tasks" to make the code easier to understand and write and it seems like it worked.

I will certainly need to perform some more tests but for now, my master can send a command to a slave (haven't tried multiple slaves yet but their code being really simplistic, I should not have too much trouble with them), listen to the answer whenever it comes, send it through serial, and receive new commands from serial, without being stuck in an endless loop.

Here is the code I'm currently at on the master :

    /*
    * MASTER
    */
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

#define LED 5
#define shocksensor 4
    
    RF24 radio(9, 10); // CE, CSN
    const byte addresses[][6] = {"00001", "00002"};
   int dataRecue;
   int reponse;
   char serialData;
   bool newdatatosend = false ;

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

    void setup() {
      Serial.begin(9600);
      pinMode(LED, OUTPUT);
      pinMode(shocksensor, INPUT);
      radio.begin();
      radio.openWritingPipe(addresses[1]); // 00002
      radio.openReadingPipe(1, addresses[0]); // 00001
      radio.setPALevel(RF24_PA_MIN);
    }

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

    void loop() {

      getData();
      
      delay(5);
      radio.stopListening();
      
      traitementData();

      delay(5);
      radio.startListening();

      getanswers();

      sendresponsetopython();

    } // fin de la boucle

//=========================================================
void getData() {
    if (Serial.available() > 0) {
        serialData = Serial.read();
    }
}

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

void traitementData() {
  if (serialData == 'Z') { // mode Vitesse
    digitalWrite(LED, HIGH);
    reponse = 15;
    while (1){
        if (digitalRead(shocksensor)){
        digitalWrite(LED, LOW);
        Serial.println("TOUCHE"); //pour contrôle
        radio.write( &reponse, sizeof(reponse) );
        Serial.println(reponse); // pour contrôle
        break;
            }
        } // fin du while
  } // fin du mode Vitesse
  
  else if (serialData == 'V') { // mode vérification
    reponse = 12;
    radio.write( &reponse, sizeof(reponse) );
    Serial.println("START = OK");
  }
}

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

void getanswers() {
  //while (!radio.available());
  if (radio.available()) {
  radio.read(&dataRecue, sizeof(dataRecue));
  newdatatosend = true;
  }
}

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

void sendresponsetopython() {
  if (newdatatosend == true) {
  Serial.println(dataRecue);
  newdatatosend = false ;
  }
}

I have made some good progress but I am stuck with an issue that I don't know how to fix.

So, on my Master arduino, I have wired a bluetooth module (JDY-31 SPP) so I can connect to it wirelessly.

As soon as I connect to the arduino using BT, it triggers a "command" and lights up the LED. Activating the shock sensor to turn the LED off then returns several "answers" from the arduino through BT. Not just one : exactly 4. I could understand if there was one answer to be received (there was a command sent to the arduino and it replies something when the shock sensor is triggered)...

My questions are :

  • when connecting to an arduino through BT, what is sent exactly ?
  • How can I prevent the BT connexion to trigger anything on the arduino ?

I tried changing my "commands" set, from letters to numbers but the BT connexion always triggers an action on the arduino.
I tried using flush() and read() outside of the main loop but it did not have any effect.

I welcome any insight you guys might have on this!

    /*
    * MASTER
    */
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <SoftwareSerial.h>
// RX => 7 ; TX => 6  ; (TX, RX)
SoftwareSerial BTSerial(6,7);
#define LED 5
#define shocksensor 4
    
    RF24 radio(9, 10); // CE, CSN
    const byte addresses[][6] = {"00001", "00002"};
   int dataRecue;
   int reponse;
   char reponsepy = 'P';
   char reponsepyinf = 'Q';
   String sendreponse;
   int commande;
   char serialData;
   bool newdatatosend = false;
   long BTBAUD = 9600;

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

    void setup() {
      BTSerial.begin(BTBAUD);
      pinMode(LED, OUTPUT);
      pinMode(shocksensor, INPUT);
      radio.begin();
      radio.openWritingPipe(addresses[1]); // 00002
      radio.openReadingPipe(1, addresses[0]); // 00001
      radio.setPALevel(RF24_PA_MIN);
      
    }




//================================================================//
//==== Boucle principale ==========//
    void loop() {

      getAndTreatData();

      getanswers();

      sendresponsetopython();

    } // fin de la boucle

//==================================================================//
//==== Récupère les données de Python et réagit en fonction ==========//

void getAndTreatData() {
    if (BTSerial.available() > 0) {
        serialData = BTSerial.read();
        radio.stopListening();
        traitementData();
   }
}

//================================================================//
//==== Gestion du mode de jeu ==========//

void traitementData() {
   if (serialData == 'Z') { // mode Vitesse
     digitalWrite(LED, HIGH);
     sendreponse = (String)reponsepy;
     while (1){
       if (digitalRead(shocksensor)){
         digitalWrite(LED, LOW);
         radio.write( &reponse, sizeof(reponse) );
         BTSerial.println(sendreponse); // pour Python
         break;
        }
      } // fin du while
    } // fin du mode Vitesse
  

 else if (serialData == 'A') { // mode Attaque et Infini
    digitalWrite(LED, HIGH);
    sendreponse = (String)reponsepy;
    while (1){
        if (digitalRead(shocksensor)){
        digitalWrite(LED, LOW);
        BTSerial.println(sendreponse); // pour démarrage côté Python
        break;
            }
        } // fin du while
  } // fin du mode Attaque et Infini

   else if (serialData == 'E') { // Fin du mode Infini
    digitalWrite(LED, HIGH);
    sendreponse = (String)reponsepyinf;
    while (1){
        if (digitalRead(shocksensor)){
        digitalWrite(LED, LOW);
        BTSerial.println(sendreponse); // pour arrêt côté Python
        break;
            }
        } // fin du while
  } // fin du mode Attaque et Infini

} // fin traitementData

//================================================================//
//==== Récupère les réponses des slaves ==========//

void getanswers() {
  radio.startListening();
  if (radio.available() > 0) {
  radio.read(&dataRecue, sizeof(dataRecue));
  newdatatosend = true;
  }
}

//================================================================//
//==== Transmet les réponses des slaves à Python ==========//

void sendresponsetopython() {
  if (newdatatosend == true) {
  BTSerial.write(dataRecue);
  newdatatosend = false ;
  }
}

Unfortunately, flush() has nothing to do with your incoming data. That function just waits until your output buffer has been transmitted. If you want to empty the receive buffer, put this into your setup() after you initialize your bt.

  while(BTSerial.available() > 0) {
    BTSerial.read();
  }

@blh64 : thanks for your help. I tried your solution, added your code to my "void setup()" after the BTSerial.begin() but the results are the same. A command is still sent to the arduino upon BT connection I and still get 4 "answers" in the terminal when i trigger the shock sensor.

This is weird :frowning:

Also : another command seems to be sent to the arduino when it is disconnected from BT. My LED lights up again... and turns off when I trigger the shock sensor. I guess if there was a way of listening to the answers, I would still have these 4 answers received.

Edit: I did a few more tests and it looks like the issue is indeed that the arduino receives data when the BT module is successfully connected.
I changed the "commands" in my code and going from uppercase to lowercase seems to have fixed my issue.

I checked the datasheet for the bluetooth modules and, of course, there is no available information on the data it "writes" upon connection (if any).

I guess I will try to write a short arduino sketch to find out exactly what is going on during connection and disconnection of these BT modules.