Wireless Quiz Buzzer System with nRF24L01 Arduino

0 11330 Medium

A complete wire quiz buzzer system for 4 players using nRF24L01 radios with included TP4056 based battery charger and sound effects

 

Wireless Quiz Buzzer System with nRF24L01 Arduino

Things used in this project

 

Hardware components

HARDWARE LIST
1 DFRobot DFPlayer - A Mini MP3 Player
5 nRF24 Module (Generic)
1 Arduino Nano R3
1 TP4056 Based Charge/Boost Converter
5 18650 Battery Compartment
5 FT232RL USB to TTL
1 Red 10cm (4 inch) Arcade Button
1 Yellow 10cm (4 inch) Arcade Button
1 Green 10cm (4 inch) Arcade Button
1 Blue 10cm (4 inch) Arcade Button
1 5V LED Lamps
1 M3 10mm Countersunk Screws
1 M3 Metal Threadded Screw Inserts
1 Assorted 5mm LEDs
1 Plastic Chrome LED Panel Mount Holder
1 SparkFun Arduino Pro Mini 328 - 5V/16MHz

Software apps and online services

 

Arduino IDE

Hand tools and fabrication machines

 

3D Printer (generic)

Story

 

This is a four player Quiz buzzer system. The controller is designed with Ready & Reset buttons and will let you know who answered first, There's also the option to pass the question to the remaining players that haven't answered yet!

This makes use of the nRF24L01 wireless devices which use 2.4GHz to transmit and receive.

I have also included an integrated lithium charge module based on the TP4056 chip to charge an internal 18640 battery.

The four buttons each contain an Arduino Pro MINI.

 

 

 

The buttons are wired up like this:

The main controller uses an Arduino Nano, although it could use a mini instead. This controller also connects to a DFPlayerMini board to provide sound effects every time a button is pressed.

 

 

 

The controller is wired up like this:

The source code can be downloaded from https://robsmithdev.co.uk/youtube/wirelessbuzzer.zip

For more information, and full build, see the YouTube Video:

 

3D models for Printing:

Thingiverse: https://www.thingiverse.com/thing:5396155Tinkercad: https://www.tinkercad.com/things/k7JUDiFv1wr?sharecode=BaPiGkdqKju71d5xkFrCgnxJhQCo4WVAP1QIDcCe1sk

 

 

 

Parts List (Affiliate links)

nRF24L01+: https://amzn.to/3GxN7Hz (x10), https://amzn.to/3LLdtqn (x5)nRF24L01+PA+LNA (Longer Range): 

 

https://amzn.to/3lKRe9inRF24L01+ 8-Pin Adapter: https://amzn.to/3z2A4MiTP4056 Based Charge/Boost Converter: https://amzn.to/3sYHXyx or https://amzn.to/3sWtLGt18650 Battery Compartment: 

 

https://amzn.to/3GgvOdIArduino Pro Mini (16Mhz, 5V): https://amzn.to/3x8pnFFFT232RL USB to TTL Converter (for programming the Arduino): https://amzn.to/3wLZRpfOfficial Arduino Nano (Recommended): 

 

https://amzn.to/3FVaEBXArduino Nano (CH340 Version): https://amzn.to/3L3FLfvDFPlayer Mini: 

 

https://amzn.to/38t9dOxRed 10cm (4 inch) Arcade Button: https://amzn.to/39QotoZYellow 10cm (4 inch) Arcade Button: https://amzn.to/38ueTI2Green 10cm (4 inch) Arcade Button: https://amzn.to/3PgEMf6Blue 10cm (4 inch) 

 

Arcade Button: https://amzn.to/3sx4jaaSmaller Arcade Buttons (6cm) Alternative: https://amzn.to/3L8IT9Y5V LED 

 

Lamps: https://www.arcadeworlduk.com/products/awuk-5v-10mm-wedge-base-led.html (not a sponsor)M3 

 

10mm Countersunk Screws: https://amzn.to/3MejVqMM3 Metal Threadded Screw Inserts: 

 

https://amzn.to/3rgwhqBRed PLA Plastic Filament: https://amzn.to/3yz7Bh2Yellow PLA Plastic Filament: 

 

https://amzn.to/3swjccUGreen PLA Plastic Filament: https://amzn.to/37Ez45KBlue PLA Plastic Filament: 

 

https://amzn.to/3yvmdxSSpade Connectors: https://amzn.to/3w7GpUQAssorted 5mm LEDs: 

 

https://amzn.to/3LMT3gDPlastic Chrome LED Panel Mount Holder: https://amzn.to/39QnrtfAssorted Resistors: 

 

https://amzn.to/3MRX6thWire Stripping Tool: https://amzn.to/321om6g

Custom parts and enclosures

3D Printed Boxes

Schematics

Schematics for Buttons

projectImage

Schematics for Controller

projectImage

Code

 

Controller Source Code for Arduino

C/C++

CODE
////////////////////////////////////////////////////////////////
// Wireless Quiz Buzzer System                                //
// Copyright (C) RobSmithDev 2022                             //
// GPL3 Licence                                               //
////////////////////////////////////////////////////////////////
// Video: https://youtu.be/b3iqji1DUG0
// https://robsmithdev.co.uk
// https://youtube.com/c/robsmithdev

#include <RF24.h>
#include <DFRobotDFPlayerMini.h>
#include <SoftwareSerial.h>


//   2 -> White LED to GND  (Status)
//   3 -> RED LED to GND    (Button Status)
//   4 -> YELLOW LED to GND (Button Status)
//   5 -> GREEN LED to GND  (Button Status)
//   6 -> BLUE LED to GND   (Button Status)
//   7 -> RESET btn to GND
//   8 -> READY btn to GND
//   9 -> CE  (nRF24)
//  10 -> CSN (nRF24)
//  11 -> MO  (nRF24)
//  12 -> MI  (nRF24)
//  13 -> SCK (nRF24)
//  A0 -> 1K Reststor -> RX on DFPlayerMini
//  A1 -> TX on DFPlayerMini


RF24 radio(9, 10); // CE, CSN

#define LED_STATUS      2     // Status LED
#define BTN_RESET       7
#define BTN_READY       8

#define DFMINI_TX       A0   // connect to pin 2 on the DFPlayer via a 1K resistor
#define DFMINI_RX       A1   // connect to pin 3 on the DFPlayer
SoftwareSerial softwareSerial(DFMINI_RX, DFMINI_TX);

// Player
// Tip: If you have any problems with the DFPlayerMini, power it from the Arduino's 3.3v pin rather than 5v.
DFRobotDFPlayerMini player;

// LED pins
unsigned char BTN_LEDS[4] = {3, 4, 5, 6};

// LED status type
enum LedStatus : unsigned char { lsOff = 0, lsOn = 1, lsFlashing = 2 };

// Status we want to share with the buttons
LedStatus ledStatus[4]  = {lsOff, lsOff, lsOff, lsOff};
bool buttonEnabled[4]   = {false, false, false, false};
bool buttonConnected[4] = {false, false, false, false};
bool hasAnswered[4]     = {false, false, false, false};
unsigned long lastContact[4] = {0, 0, 0, 0};

// Last loop time
unsigned long lastLoopTime = 0;

// System status
bool isReady = false;

// Is audio playing?
bool isPlaying = false;
bool dfPlayerReady = false;

// searches the radio spectrum for a quiet channel
bool findEmptyChannel() {
  Serial.write("Scanning for empty channel...\n");
  char buffer[10];

  // Scan all channels looking for a quiet one.  We skip every 10
  for (int channel = 125; channel > 0; channel -= 10) {
    radio.setChannel(channel);
    delay(20);

    unsigned int inUse = 0;
    unsigned long testStart = millis();
    // Check for 400 ms per channel
    while (millis() - testStart < 400) {
      digitalWrite(LED_STATUS, millis() % 500 > 400);
      if ((radio.testCarrier()) || (radio.testRPD())) inUse++;
      delay(1);
    }

    // Low usage?
    if (inUse < 10) {
      itoa(channel, buffer, 10);
      Serial.write("Channel ");
      Serial.write(buffer);
      Serial.write(" selected\n");
      return true;
    }
  }
  return false;
}

// Sends a new ACK payload to the transmitter
void setupACKPayload() {
  // Update the ACK for the next payload
  unsigned char payload[4];
  for (unsigned char button=0; button<4; button++)
      payload[button] = (buttonEnabled[button] ? 128 : 0) | ledStatus[button];
  radio.writeAckPayload(1, &payload, 4);
}

// Check for messages from the buttons
void checkRadioMessageReceived() {
  // Check if data is available
  if (radio.available()) {
    unsigned char buffer;
    radio.read(&buffer, 1);

    // Grab the button number from the data
    unsigned char buttonNumber = buffer & 0x7F; // Get the button number
    if ((buttonNumber >= 1) && (buttonNumber <= 4)) {
      buttonNumber--;

      // Update the last contact time for this button
      lastContact[buttonNumber] = lastLoopTime;

      // And that it's connected
      buttonConnected[buttonNumber] = true;

      // If the button was pressed, was enabled, hasn't answered and the system is ready for button presses
      if ((buffer & 128) && (buttonEnabled[buttonNumber]) && (!hasAnswered[buttonNumber]) && (isReady)) {
        // No longer ready
        isReady = false;

        if (dfPlayerReady) {
          player.play(buttonNumber + 1);
          isPlaying = true;
        }

        // Signal the button was pressed
        hasAnswered[buttonNumber] = true;

        // Change button status
        for (unsigned char btn = 0; btn < 4; btn++)
          ledStatus[btn] = (btn == buttonNumber) ? lsOn : lsOff;

        // Turn off the ready light
        digitalWrite(LED_STATUS, LOW);
      }
    }

    setupACKPayload();
  }
}

// Setup the controller
void setup() {
  // put your setup code here, to run once:
  Serial.begin(57600);
  while (!Serial) {};

  // small delay to allow the DFPlayerMini to boot
  delay(1000);

  // For the DFPlayerMini
  softwareSerial.begin(9600);
  if (player.begin(softwareSerial)) {
    player.volume(30);
    dfPlayerReady = true;
  }

  // Setup the radio device
  radio.begin();
  radio.setPALevel(RF24_PA_LOW);
  radio.enableDynamicPayloads();
  radio.enableAckPayload();
  radio.setDataRate(RF24_250KBPS);
  radio.setRetries(4, 8);
  radio.maskIRQ(false, false, false);  // not using the IRQs

  // Setup our I/O
  pinMode(LED_STATUS, OUTPUT);
  pinMode(BTN_RESET, INPUT_PULLUP);
  pinMode(BTN_READY, INPUT_PULLUP);


  if (!radio.isChipConnected()) {
    Serial.write("RF24 device not detected.\n");
  } else {
    Serial.write("RF24 detected.\n");

    // Trun off the LED
    digitalWrite(LED_STATUS, LOW);

    // Now setup the pipes for the four buttons
    char pipe[6] = "0QBTN";
    radio.openWritingPipe((uint8_t*)pipe);
    pipe[0] = '1';
    radio.openReadingPipe(1, (uint8_t*)pipe);
    for (char channel = 0; channel < 4; channel++) {
      pinMode(BTN_LEDS[channel], OUTPUT);
      digitalWrite(BTN_LEDS[channel], LOW);
    }

    // Start listening for messages
    radio.startListening();

    // Find an empty channel to run on
    while (!findEmptyChannel()) {};

    // Start listening for messages
    radio.startListening();
    
    // Ready
    digitalWrite(LED_STATUS, LOW);

    setupACKPayload();
  }
}

// Main loop
void loop() {
  lastLoopTime = millis();


  if (digitalRead(BTN_RESET) == LOW) {                 // Reset button pressed?
    // Turn all buttons off
    for (unsigned char button = 0; button < 4; button++) {
      ledStatus[button] = lsOff;
      buttonEnabled[button] = false;
      hasAnswered[button] = false;
      if (isPlaying) {
        player.stop();
        isPlaying = false;
      }
    }
    isReady = false;
    digitalWrite(LED_STATUS, LOW);
  } else if (digitalRead(BTN_READY) == LOW) {                // Ready button pressed
    // Make the buttons flash that havent answered yet
    for (unsigned char button = 0; button < 4; button++) {
      buttonEnabled[button] = !hasAnswered[button];
      ledStatus[button] = hasAnswered[button] ? lsOff : lsFlashing;
    }
    isReady = true;
    if (isPlaying) {
      player.stop();
      isPlaying = false;
    }
    digitalWrite(LED_STATUS, HIGH);
  }

  // Update our LEDs and monitor for ones that are out of contact
  for (unsigned char button = 0; button < 4; button++) {
    // If the button is connected
    if (buttonConnected[button]) {
      // If its been 1 second since we heard from it
      if (lastLoopTime - lastContact[button] > 1000) {
        // Disconnect it
        buttonConnected[button] = false;
        digitalWrite(BTN_LEDS[button], LOW);
      } else {
        // Set the LED to match the state we have it in
        digitalWrite(BTN_LEDS[button], (ledStatus[button] == lsOn) || ((ledStatus[button] == lsFlashing) && (lastLoopTime & 255) > 128));
      }
    } else {
      // For disconnected ones we just give a short 'blip' once per few second
      digitalWrite(BTN_LEDS[button], (lastLoopTime & 2047) > 2000);
    }
  }

  // Check for messages on the 'network'
  checkRadioMessageReceived();
}

Buttons Code for Arduino

C/C++

CODE
////////////////////////////////////////////////////////////////
// Wireless Quiz Buzzer System                                //
// Copyright (C) RobSmithDev 2022                             //
// GPL3 Licence                                               //
////////////////////////////////////////////////////////////////
// Video: https://youtu.be/b3iqji1DUG0
// https://robsmithdev.co.uk
// https://youtube.com/c/robsmithdev


#include <RF24.h>
#include <EEPROM.h>

//   4 -> Button to GND 
//   5 -> LED to GND (Button)
//   9 -> CE  (nRF24)
//  10 -> CSN (nRF24)
//  11 -> MO  (nRF24)
//  12 -> MI  (nRF24)
//  13 -> SCK (nRF24)
RF24 radio(9, 10); // CE, CSN

#define PIN_BUTTON   4
#define PIN_LED      5

// LED status options
enum LedStatus : unsigned char { lsOff = 0, lsOn = 1, lsFlashing = 2 };

// Last loop start time
unsigned long lastLoopTime = 0;

// If this is in contact with the controller
bool isConnected = false;
// Last time we sent some status
unsigned long lastStatusSend = 0;
// When the button was pressed down
unsigned long buttonDownTime = 0;
// If the button is enabled
bool buttonEnabled = false;
// Status of the LED
LedStatus ledStatus = lsOff;

// Which button number we are
unsigned char buttonNumber = EEPROM.read(0);

// Main setup function
void setup() {
  // put your setup code here, to run once:
  pinMode(PIN_BUTTON, INPUT_PULLUP);
  pinMode(PIN_LED, OUTPUT);

  // put your setup code here, to run once:
  Serial.begin(57600);
  while (!Serial) {};

  while ((buttonNumber<1) || (buttonNumber>4)) {
    // A dirty PWM for dim brightness
    digitalWrite(PIN_LED, HIGH);
    delay(1);
    digitalWrite(PIN_LED, LOW);
    delay(10);
    if (Serial.available()) {
      char id = Serial.read();
      if ((id >= '1') && (id<='4')) {
        buttonNumber = id - '0';
        EEPROM.write(0, buttonNumber);
      }
    }
  }

  // Setup the radio device
  if (!radio.begin()) {
    Serial.write("RF24 device failed to begin\n");
  }
  radio.setPALevel(RF24_PA_LOW);     // Max power
  radio.enableDynamicPayloads();
  radio.enableAckPayload();
  radio.setDataRate(RF24_250KBPS);
  radio.setRetries(2, 2);
  radio.maskIRQ(false, false, false);  // not using the IRQs

   if (!radio.isChipConnected()) {
     Serial.write("RF24 device not detected.\n");
   } else {  
     Serial.write("RF24 device found\n");
   }

   // Configure the i/o
   char pipe[6] = "1QBTN";
   radio.openWritingPipe((uint8_t*)pipe);
   pipe[0] = '0';
   radio.openReadingPipe(1, (uint8_t*)pipe);
   radio.stopListening();
}

// Search for the button controller channel
bool findButtonController() {
   Serial.write("Searching for controller...\n");

   for (int a = 125; a > 0; a-=10) {
      radio.setChannel(a);
      delay(15);
      // Send a single byte for status
      if (sendButtonStatus(false)) {
        Serial.write("Quiz Controller found on channel ");
        char buffer[10];
        itoa(a,buffer,10);
        Serial.write(buffer);
        Serial.write("\n");
        return true;          
      }
      digitalWrite(PIN_LED, (millis() & 2047) > 2000);
   }

   // Add a 1.5 second pause before trying again (but still flash the LED)
   unsigned long m = millis();
   while (millis() - m < 1500) {
     digitalWrite(PIN_LED, (millis() & 2047) > 2000);
     delay(15);
   }
   
   return false;
}

// Attempt to send the sttaus of the button and receive what we shoudl be doing
bool sendButtonStatus(bool isDown) {
  unsigned char message = buttonNumber;
  if (isDown) message |= 128;

  for (unsigned char retries=0; retries<4; retries++) {  
    // This delay is used incase transmit fails.  We will assume it fails because of data collision with another button.
    // This is inspired by https://www.geeksforgeeks.org/back-off-algorithm-csmacd/
    unsigned int randomDelayAmount = random(1,2+((retries*retries)*2));
    if (radio.write(&message, 1)) {
      if (radio.available()) {
       if (radio.getDynamicPayloadSize() == 4) {
          unsigned char tmp[4];
          radio.read(&tmp, 4);
  
          buttonEnabled = (tmp[buttonNumber-1] & 128) != 0;
          ledStatus = (LedStatus)(tmp[buttonNumber-1] & 3);
          Serial.write("Write OK, ACK Payload\n");
  
          return true;          
        } else {
          // Remove redundant data
          int total = radio.getDynamicPayloadSize();
          unsigned char tmp;
          while (total-- > 0) radio.read(&tmp, 1);
          Serial.write("Write OK, ACK wrong size\n");
          delay(randomDelayAmount);
        }
      } else {
          // This shouldn't really happen, but can sometimes if the controller is busy
          Serial.write("Write OK, no ACK\n");
          return true;
      }
    } else {
       delay(randomDelayAmount);
    }
  }
  
  Serial.write("Write Failed\n");
  return false;
}

// Main loop
void loop() {
  lastLoopTime = millis();
    
  if (radio.isChipConnected()) {

    // If connectin ACK timeout or not connected
    if ((lastLoopTime - lastStatusSend > 1000) || (!isConnected)) {
        // A short blip meaning its powered up, but not working
        while (!findButtonController()) {};
        digitalWrite(PIN_LED, LOW);    
        isConnected = true;
        lastStatusSend = lastLoopTime;
    }  
  
    // If the button was pressed down (and its been 300ms since last check)
    if ((digitalRead(PIN_BUTTON) == LOW) && (lastLoopTime - buttonDownTime>300) && (buttonEnabled)) {
      // This ensures we get a random number sequence unique to this player.  The random number is used to prevent packet collision
      randomSeed(lastLoopTime);       
      // Send the DOWN state
      if (sendButtonStatus(true)) {
        buttonDownTime = lastLoopTime;
        lastStatusSend = lastLoopTime;
      }
    }
  
    // If its been 150ms since last TX send status
    if (lastLoopTime-lastStatusSend > 150) {    
      if (sendButtonStatus(false)) {
        lastStatusSend = lastLoopTime;
      } else delay(10); 
    }

     digitalWrite(PIN_LED, (ledStatus == lsOn) || ((ledStatus == lsFlashing) && ((lastLoopTime & 255)>128)));
  } else {
     // Error flash sequence
     digitalWrite(PIN_LED, (lastLoopTime & 1023) < 100);
  }

  // Slow the main loop down
  delay(1);
}

The article was first published in hackster, June 7, 2022

cr: https://www.hackster.io/RobSmithDev/wireless-quiz-buzzer-system-with-nrf24l01-arduino-29b1ce

author: RobSmithDev

 

License
All Rights
Reserved
licensBg
0