Радио-кнопки для игры "Угадай мелодию".
- Войдите на сайт для отправки комментариев
В игру давно играют как в семейном кругу так и на вечеринках с друзьями. Напомню смысл игры: ведущий включает фрагмент мелодии, игроки нажимают кнопки. Кто угадал и быстрее нажал свою кнопку тому + 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 }