Радио-кнопки для игры "Угадай мелодию".

Нет ответов
Jaeger
Jaeger аватар
Offline
Зарегистрирован: 23.03.2018

      В игру давно играют как в семейном кругу так и на вечеринках с друзьями. Напомню смысл игры: ведущий включает фрагмент мелодии, игроки нажимают кнопки. Кто угадал и быстрее нажал свою кнопку тому + N-количество очков. Если не правильно угадал - минус. Возможны вариации правил игры в зависимости от фантазии ведущего. Преимущество беспроводных кнопок для игроков очевидно - они не привязаны к определенному месту и позволяют располагаться вокруг ведущего в радиусе 10 и более метров. Предлагаемый вариант беспроводных кнопок состоит из трех кнопок для игроков и одной для ведущего и предназначен для домашнего использования. Кнопки построены на самых дешевых ардуинах Pro Mini ATmega168/328/8MHz/3,3V и трансиверах nRF24L01+.

Структурная схема системы.

 

     Кнопка ведущего представляет собой HID - устройство, подключается к USB порту компьтера и позволяет эмулировать команды мм-клавиатуры для управления плеером. Данное устройство передает плееру только две команды - PLAY и STOP. Также к кнопке ведущего подключена лампа-сигнализатор, которая включатся соответствующим цветом при нажатии кнопки игрока. В режиме ожидания кнопки игроков заблокированы и ожидают команды от кнопки ведущего. Ведущий выбирает на компе файл мелодии и нажимает свою кнопку. "Кнопка посылает" кнопкам игроков сигнал разблокировки  и запускает плеер с выбранной мелодией. Длительность проигрывания мелодии ограничена в данном варианте 10-ю секундами, по истечении которых плеер останавливается и вновь блокируются кнопки игроков. Если при проигрывании мелодии будет нажата кнопка одного из игроков - плеер останавливается, блокируются кнопки игроков, зажигается лампа-сигнализатор на 5 сек соответствующим цветом.

Принципиальные схемы и скетчи.

Схема кнопки ведущего.

Кнопка ведущего запитана от шины 5V USB порта. При выборе ардуины для этой кнопки следует ориентироваться на варианты клонов Pro-Mini 168/328 с "большим" стабилизатором и кварцем на борту.

 

Дело в том, что от внутреннего стабилизатора 3,3V запитывается не только чип мк ардуины, но и периферия в лице трансивера nRF24L01+, что требует от стабилизатора повышенной мощности. Что касается кварца, то его нужно поменять на 12МГц. Это требования USB-драйвера для ардуины. Для кнопок игроков стабилизаторы ардуин не учавствуют в питании, поэтому там могут быть любые модификации плат 8Мгц/3,3V.

Трансиверы nRF24L01+ могут быть обычными или мини. Следует учесть, что у платы-мини вывод N1 это VCC(3,3V).

В качестве сигнализатора нажатой кнопки игрока используется "пятачек" CJMCU-2812-7, с 7-ю адресными светодиодами WS2812B, помещенный под колпак сгоревшей сетевой лед-лампы.

Можно использовать огрызок адресной ленты или 7 отдельных  WS2812B. При использовании длинной ленты, ее следует запитать от отдельного источника 5V и скорректировать скетч. Можно использовать обычные RGB светодиоды после минимальной доработки схемы и скетча.

Для заливки скетча в ардуины потребуется USB/UART конвертер. Рекомендую FTDI232 на чипе FT232RL, кроме USB/UART конвертера он может работать как SPI bit-bang программатор. SPI программатор потребуется для заливки загрузчика в ардуину после замены кварца с 8 на 12МГц или при заливке готового HEX файла. Для поддержки Arduino IDE плат с "нестандартной" частотой необходимо в IDE установить дополнение MiniCore Подробности установки дополнений и заливки загрузчика можно найти в интернете.

Скетч кнопки ведущего:

/***************************LEADER (ВЕДУЩИЙ)***********************************

*  Плата: ATmega168
 *  Clock: 12MHz external 
 *  Variant: 168/168A
 *  LTO enabled
 *  Bootloader: Yes (UART0) 


 * VER 11.12.20
******************************************************************************/   

//#define DEBUG     //Раскомментировать для отладки

#include <TrinketHidCombo.h>  // see tutorial at http://learn.adafruit.com/trinket-usb-volume-knob
#include <Adafruit_NeoPixel.h>
#include <RF24.h>             // Подключаем библиотеку  для работы с nRF24L01+

#ifdef DEBUG
#include <printf.h>   // для отладки
#endif

#define GAMER1      1     // 1 геймер красный
#define GAMER2      2     // 2 геймер зеленый   
#define GAMER3      3     // 3 геймер синий

// CE and CSN are configurable for nRF24L01+
#define CE_PIN      9
#define CSN_PIN     10

#define LED_PIN     5     // пин ленты
#define LED_COUNT   7     // кол-во светодиодов
#define LIGHT_TIME  5000  // Время свечения ленты (msec)

#define PIN_BUTTON  3     // Пин кнопки
#define LED         6     // Пин светодиода

//Время распознавания = FLASH_COUNT 20 * FREQ 500 = 10 000 msec (10 sec)
#define FLASH_COUNT 20
#define FREQ        500   // LED blinking frequency (msec)

#define OFF         100   // Константа для блокировки геймеров
#define ON          101   // Для разблокировки геймеров

Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);

RF24 radio(CE_PIN, CSN_PIN);  // Создаём объект radio   для работы с библиотекой RF24, указывая номера выводов nRF24L01+ (CE, CSN)

uint8_t       data_tx         = ON;         // Принятые данные
uint8_t       data_rx;                      // Данные для передачи
uint8_t       pipe;                         // Создаём переменную     для хранения номера трубы, по которой пришли данные
bool          ledState        = LOW;       //Status LED
unsigned long previousMillis  = 0;
unsigned long previousMillis2 = 0;

uint8_t       flashCounter    = 0; 
bool          flash           = false;
bool          led_show        = false;
byte          addresses[][6]  = {"1Node","2Node","3Node","4Node"};
static char   sw_was_pressed  = 0;

//***************************************************************************

void setup()
{
//****************************Для отладки ***********************************
#ifdef DEBUG
  Serial.begin(57600);
  printf_begin();
  Serial.println("BASE UNIT");
#endif
//***************************************************************************
 
  delay(1000);
  pinMode(PIN_BUTTON, INPUT_PULLUP);
  pinMode(LED, OUTPUT);
  digitalWrite(LED, HIGH);
  
  strip.begin();           // INITIALIZE NeoPixel strip object (REQUIRED)
  strip.setBrightness(150);    // яркость (0-255)
  
  for (byte i = 1; i < LED_COUNT; i++) {  //зажечь ленту белым на 2 с
              strip.setPixelColor(i, strip.Color(127, 127, 127));
            } 
  strip.show(); // выводим изменения на ленту 
  
  delay(2000);
  
  strip.clear();   // очищает буфер
  strip.show(); // выводим изменения на ленту
  
  // Setup and configure rf radio
  radio.begin();        // Инициируем работу nRF24L01+
  radio.setAutoAck(1); // Ensure autoACK is enabled
  radio.setRetries(0,15); // Max delay between retries & number of retries
  radio.setChannel(5);                                       // Указываем канал приёма данных (от 0 до 127), 5 - значит приём данных осуществляется на частоте 2,405 ГГц (на одном канале может быть только 1 приёмник и до 6 передатчиков)
  radio.setDataRate     (RF24_1MBPS);                        // Указываем скорость передачи данных (RF24_250KBPS, RF24_1MBPS, RF24_2MBPS), RF24_1MBPS - 1Мбит/сек
  radio.setPALevel      (RF24_PA_HIGH);                      // Указываем мощность передатчика (RF24_PA_MIN=-18dBm, RF24_PA_LOW=-12dBm, RF24_PA_HIGH=-6dBm, RF24_PA_MAX=0dBm)
  
  radio.openWritingPipe(addresses[0]);
  radio.openReadingPipe(GAMER1,addresses[GAMER1]);
  radio.openReadingPipe(GAMER2,addresses[GAMER2]);
  radio.openReadingPipe(GAMER3,addresses[GAMER3]);
  radio.startListening  ();                                  // Включаем приемник, начинаем прослушивать открытые трубы

#ifdef DEBUG
  radio.printDetails();       // Дамп конфигурации RF модуля в монитор для отладки.
#endif
  
  //USB d- (белый) PORT D4 задан в usbconfig.h
  //USB d+ (зелёный) PORT D2 задан в usbconfig.h
  TrinketHidCombo.begin(); // start the USB device engine and enumerate
  
  }
//****************************************************************************************
void loop()
{
        if(radio.available(&pipe)){            // Если в буфере имеются принятые данные, то получаем номер трубы, по которой они пришли, по ссылке на переменную pipe
        radio.read(&data_rx, sizeof(data_rx)); // Читаем данные в массив data и указываем сколько байт читать

#ifdef DEBUG        
        Serial.println(pipe);
        Serial.println(data_rx);
#endif        
        TrinketHidCombo.pressMultimediaKey(MMKEY_STOP); // Если данные пришли от геймеров, то тормозим плеер
        
        data_tx = OFF;          // Готовим код для блокировки геймеров
        radio.stopListening();  // First, stop listening so we can talk.
        radio.write( &data_tx, 1);  // Блокируем геймеров
        radio.startListening(); // Now, continue listening
        
        flash = false;          // Тормоз мигания LED
        
        if(pipe == GAMER1){            // Если кнопку нажал красный геймер
            for (byte i = 1; i < LED_COUNT; i++) {  // Зажигаем ленту красным
              strip.setPixelColor(i, strip.Color(255, 0, 0));  // красный
            }
            }

          if(pipe == GAMER2){          // Если кнопку нажал зеленый геймер
            for (byte i = 1; i < LED_COUNT; i++) {
              strip.setPixelColor(i, strip.Color(0, 255, 0));  // зеленый
            }
            }

          if(pipe == GAMER3){          // Если кнопку нажал синий геймер
          for (byte i = 1; i < LED_COUNT; i++) {
            strip.setPixelColor(i, strip.Color(0, 0, 255));  // синий
            }
            }
          
          strip.show();
          pipe = 0;
          previousMillis2 = millis();
          led_show  = true;
          flashCounter = 0;
          ledState = LOW;
          digitalWrite(LED, HIGH);
        }
                  
      Delay_Show();    //Зажигаем ленту на 5с     
          
    //----------------- Обработка кнопки PLAY/PAUSE------------------
  if (digitalRead(PIN_BUTTON) == LOW)
  {
    if (sw_was_pressed == 0) // only on initial press, so the keystroke is not repeated while the button is held down
    {
      TrinketHidCombo.pressMultimediaKey(MMKEY_PLAYPAUSE);//Запустить плеер с выбранным файлом
      flash = true; //!flash;
      data_tx = ON; // Данные для разблокировки геймеров
      previousMillis = millis();
      radio.stopListening(); // First, stop listening so we can talk.
      radio.write( &data_tx, 1);// Разблокировать геймеров
      radio.startListening(); // Now, continue listening
      
      delay(4); // debounce delay
    }
    sw_was_pressed = 1;
  }
  else
  {
    if (sw_was_pressed != 0) {
      delay(4); // debounce delay
    }
    sw_was_pressed = 0;
  }

  TrinketHidCombo.poll(); // check if USB needs anything done
  //----------------------------------------------------------------
  //Non-blocking blinking
  if ((millis() - previousMillis >= FREQ)&& flash == true) {
    
    previousMillis = millis();  // save the last time you blinked the LED
    ledState = !ledState;       //if the LED is off turn it on and vice-versa:
    digitalWrite(LED, ledState);// set the LED with the ledState of the variable:
    
    if (flashCounter++ >= FLASH_COUNT) {
        TrinketHidCombo.pressMultimediaKey(MMKEY_STOP);// Остановить плеер
        data_tx = OFF;
        
        radio.stopListening();    // First, stop listening so we can talk.
        radio.write( &data_tx, 1);// Блокировать геймеров, если время на распознавание вышло
        radio.startListening();   // Now, continue listening
        
        flashCounter = 0;
        flash = false;

        ledState = LOW;
        digitalWrite(LED, HIGH);
      }
        
  }
  
}

 void Delay_Show (void) {
    if (((millis() - previousMillis2) >= LIGHT_TIME) && led_show == true) {
        previousMillis2 = millis();  // save the last time you blinked the LED
        strip.clear();   // очищает буфер
        strip.show(); // выводим изменения на ленту
        led_show  = false;  
  }
   }

 

Схема кнопки игроков.

Для каждой кнопки игрока заливается свой скетч. В скетче нужно раскомментировать #define GAMER  для соответствующей кнопки, это нужно чтобы у каждой кнопки был свой адрес трубы для передачи, необходимо для распознавания нажатой кнопки у ведущего.

Скетч кнопки игрока:


//**********************GAMER (ИГРОК)*********************************

/*  Плата: Arduino Pro or Pro mini
 *  Процессор: ATmega168 (3,3V 8MHz) 
 *  
 *  VER 11.12.20
 */

//#define DEBUG     //Раскомментировать для отладки

#include <RF24.h>

#ifdef DEBUG
#include <printf.h>   // для отладки
#endif


//Раскомментировать для соответствующего геймера
//#define GAMER     1     // 1 геймер красный
#define GAMER 2     // 2 геймер зеленый   
//#define GAMER 3     // 3 геймер синий

// CE and CSN are configurable
#define CE_PIN    9
#define CSN_PIN   10

#define BUTTON    5  // пин кнопки

#define LED1      2  // пины светодиодов
#define LED2      3
#define LED3      4

#define OFF       100
#define ON        101

#define BRIGHNESS 100 // яркость LED в режиме ожидания

RF24 radio(CE_PIN, CSN_PIN);

const int ARRAY_SIZE = 3;
int ledPin[] = {LED1, LED2, LED3};  // Массив LED - пинов
int ledDelay = 100;       // Скорость бегущего огня (msec)
int currentLed = 0;
unsigned long changeTime;
bool run_light = false;   // Флаг разрешения бегущего огня

static char   sw_was_pressed = 0;
uint8_t data_tx = 111;            // Данные передатчика
uint8_t data_rx;                  // Данные приемника
bool    button_available = false; // Флаг доступа к кнопке
byte addresses[][6] = {"1Node","2Node","3Node","4Node"}; //"2Node" - красный,"3Node" - зеленый, "4Node" - синий геймер

void setup() {
//****************************Для отладки ***********************************
#ifdef DEBUG                
  Serial.begin(57600);
  printf_begin();
  Serial.println("BASE UNIT");
#endif
//***************************************************************************
  
  pinMode(BUTTON, INPUT_PULLUP);  // Пин для кнопки на ввод с подтяжкой
  
  for (int i = 0; i < ARRAY_SIZE; i++) { // Конфигурируем пины для LED на выход
    pinMode(ledPin[i], OUTPUT);
    }
  delay(2000); 
  analogWrite(ledPin[1], BRIGHNESS);  //режим ожидания LED
  
  // Setup and configure rf radio
  radio.begin(); // Start up the radio
  radio.setAutoAck(1); // Ensure autoACK is enabled
  radio.setRetries(0,15); // Max delay between retries & number of retries
  radio.setChannel(5);                                       // Указываем канал приёма данных (от 0 до 127), 5 - значит приём данных осуществляется на частоте 2,405 ГГц (на одном канале может быть только 1 приёмник и до 6 передатчиков)
  radio.setDataRate     (RF24_1MBPS);                        // Указываем скорость передачи данных (RF24_250KBPS, RF24_1MBPS, RF24_2MBPS), RF24_1MBPS - 1Мбит/сек
  radio.setPALevel      (RF24_PA_HIGH);//(RF24_PA_MIN);                      // Указываем мощность передатчика (RF24_PA_MIN=-18dBm, RF24_PA_LOW=-12dBm, RF24_PA_HIGH=-6dBm, RF24_PA_MAX=0dBm)

  radio.openWritingPipe(addresses[GAMER]);
  radio.openReadingPipe(1,addresses[0]);

//****************************Для отладки ***********************************
#ifdef DEBUG
  radio.printDetails();       // Дамп конфигурации RF модуля в монитор для отладки.
#endif
//***************************************************************************
  radio.startListening(); // Start listening

}

void loop(void){
  
  if(radio.available()){    // Если в буфере имеются принятые данные
  radio.read(&data_rx, 1);  // Читаем данные в массив data и указываем сколько байт читать
    if (data_rx == ON) {    // Если приняли данные по разблокировке геймера

//****************************Для отладки ***********************************
#ifdef DEBUG        
        //Serial.println(pipe);
        Serial.println(data_rx);
#endif  
//***************************************************************************
      
      button_available = true; // Разблокировать кнопку
      run_light = true;        // Запустить бегущий огонь 
    }
    if (data_rx == OFF) {      // Если приняли данные по блокировке геймера
      button_available = false;// Заблокировать кнопку
      run_light = false;        // Остановить бегущий огонь
      LED_off();                // Включить дежурный LED, остальные погасить
    }
  }      
  
 if  (button_available) { // Если кнопка разблокирована
   //---------------- Обработка нажатия кнопки active-low-----------------
  if (digitalRead(BUTTON) == LOW)
  {
    if (sw_was_pressed == 0) // only on initial press, so the keystroke is not repeated while the button is held down
    {
      radio.stopListening(); // First, stop listening so we can talk.
      radio.write( &data_tx, 1); // Передать ведущему, что кнопку нажали
      radio.startListening(); // Now, continue listening
      
      delay(5); // debounce delay
    }
    sw_was_pressed = 1;
  }
  else
  {
    if (sw_was_pressed != 0) {
      delay(5); // debounce delay
    }
    sw_was_pressed = 0;
    
  }
  //------------------------------------------------------------------------
 }
 Running_Light(); // Бегущий огонь
}

  //-------------------------Running Light--------------------------
  void Running_Light (void) {
    if (((millis() - changeTime) > ledDelay) && run_light == true) {
      digitalWrite(ledPin[currentLed-1], LOW); //выключаем предыдущий LED
      digitalWrite(ledPin[currentLed], HIGH); // включаем текущий LED
      if (currentLed++ == ARRAY_SIZE) 
          currentLed = 0;
      changeTime = millis();
      }
  }
  //--------------------------LED off--------------------------------
  void LED_off (void) {
    for (int i = 0; i < ARRAY_SIZE; i++) {
      digitalWrite(ledPin[i], LOW);
    }
    analogWrite(LED2, BRIGHNESS);  //включить режим ожидания LED2
  }

По мотивам темы:

Материалы проекта:

Лажевенький видосик: