====== Аудиоплеер, управляемый RFID-карточками ====== {{ :projects:becha-overview.jpg?nolink& |}} * Платформы: Arduino Leonardo, Raspberry Pi B * Языки программирования: [[wp>Wiring_(development_platform) | Wiring (C++)]], [[wpru>Ruby]] * Проект на [[wpru>GitHub]]: https://github.com/amperka-projects/becha-rfid ===== Что это такое? ===== {{ :projects:how-it-work.becha.png?nolink& |}} В этой статье мы расскажем о том как собрать управляемый при помощи [[wpru>Rfid|RFID]]-меток аудиоплеер. Устройство состоит из двух частей: - **Пульт**, который управляет воспроизведением музыки. Чтобы включить определённую мелодию, достаточно всего лишь поднести к нему нужную RFID-метку. В нашем случае это карточки от Московского метрополитена. - **Аудиоплеер** воспроизводит выбранную мелодию. ===== Что для этого необходимо? ===== {{ :projects:components.png?nolink& |}} Для изготовления пульта нам понадобятся: -[[amp>product/arduino-leonardo?utm_source=proj&utm_campaign=becha&utm_medium=wiki|Arduino Leonardo]] -[[amp>product/arduino-wireless-shield?utm_source=proj&utm_campaign=becha&utm_medium=wiki|Wireless Shield]] -[[amp>product/wifi-bee-v2?utm_source=proj&utm_campaign=becha&utm_medium=wiki|Wi-Fi Bee v2.0]] -[[amp>product/rfid-module-13-56?utm_source=proj&utm_campaign=becha&utm_medium=wiki|RFID-сканер 13,56 МГц]] -[[amp>product/breadboard-mini?utm_source=proj&utm_campaign=becha&utm_medium=wiki|Breadboard Mini]] -[[amp>product/wire-mm?utm_source=proj&utm_campaign=becha&utm_medium=wiki|Соединительные провода «папа-папа»]] -[[amp>product/krona-battery?utm_source=proj&utm_campaign=becha&utm_medium=wiki|Батарейка «Крона»]] -[[amp>product/krona-21mm-cable?utm_source=proj&utm_campaign=becha&utm_medium=wiki|Кабель питания от батарейки «Крона»]] -Пара карточек от Московского метрополитена Для изготовления аудиоплеера нам понадобятся: -[[amp>product/raspberry-pi-3-model-b?utm_source=proj&utm_campaign=becha&utm_medium=wiki|Raspberry Pi 3 Model B]] -[[amp>product/raspberry-pi-wolfson-audio-card?utm_source=proj&utm_campaign=becha&utm_medium=wiki|Wolfson Audio Card]] -[[amp>product/usb-power-plug?utm_source=proj&utm_campaign=becha&utm_medium=wiki|Импульсный блок питания с USB-разъёмом (5 В, 1000 мА)]] -[[amp>product/usb-cable-micro?utm_source=proj&utm_campaign=becha&utm_medium=wiki|Кабель USB (1,5 м, A — Micro USB)]] -SD карта объемом ≥4,5 ГБ -Акустические колонки и кабель для них. ===== Сборка пульта ===== - При помощи [[articles:arduino-ide-install|Arduino IDE]] запрограммируйте Arduino Leonardo скетчем [[projects:becha_rfid?&#пульт_скетч_для_arduino|rfid_wifi.ino]]. - Установите Wireless Shield на Arduino Leonardo. {{ :projects:rfid-audio:assembly:rfid-assembling0.png?nolink& |}} - Установите Wi-Fi Bee v2.0 на Wireless Shield. Обратите внимание: переключатель на Wireless Shield должен стоять в положении «MICRO». {{ :projects:rfid-audio:assembly:rfid-assembling1.png?nolink& |}} - Установите RFID-сканер на Breadboard Mini. Поместите получившуюся конструкцию на Wireless Shield. {{ :projects:rfid-audio:assembly:rfid-assembling2.png?nolink& |}} - Соедините 11-й пин Arduino с ножкой RX RFID-сканера. {{ :projects:rfid-audio:assembly:rfid-assembling3.png?nolink& |}} - Соедините 10-й пин Arduino с ножкой TX RFID-сканера. {{ :projects:rfid-audio:assembly:rfid-assembling4.png?nolink& |}} - Соедините контакты питания RFID-сканера с контактами питания Arduino (красный провод — 5 V, чёрный — GND). {{ :projects:rfid-audio:assembly:rfid-assembling5.png?nolink& |}} - Поместите конструкцию в корпус пульта и подсоедините питание. {{ :projects:rfid-audio:assembly:rfid-assembling6.png?nolink& |}} ===== Сборка аудиоплеера ===== {{ :projects:rpib_wolfson.png?400 |}} - Залейте образ [[http://downloads.element14.com/wolfson/wolfson_3.10_master.zip?COM=WolfsonAudioCard | Raspbian]] от Wolfson на SD-карту. - Установите аудиокарту Wolfson на Raspberry Pi. - Вставьте SD-карту в Raspberry Pi. - Подключите питание и сетевой кабель. - Залогиньтесь по [[wpru>SSH]] на Raspberry Pi (по умолчанию логин — ''pi'', пароль — ''raspberry'')\\ Обратите внимание: для подключения к Raspberry Pi по сети вам потребуется узнать[[wpru>IP-адрес]], назначенный Raspberry Pi вашим маршрутизатором. Для этого можно воспользоваться Web-интерфейсом маршрутизатора или специализированными программами, такими как [[http://www.advanced-ip-scanner.com/ru/|Advanced IP scanner]]. - Установите [[wpru>Ruby]], выполнив в [[wpru>Командная_строка|командной строке]] команду:\\ sudo apt-get install ruby - Клонируйте репозиторий кода:\\ git clone https://github.com/amperka-projects/becha-rfid cd ./becha-rfid - Для установки сервера выполните команды:\\ cd ./server ./prepare_wolfson sudo gem install bechad После установки сервера мы можем выполнять три простых действия: привязать композицию к карточке (добавление трека в playlist), запустить и остановить сервер. - Для добавления трека в playlist:\\ bechad record - Для запуска сервера:\\ bechad start - Для остановки сервера:\\ bechad stop ===== Как это работает? ===== ==== Протокол связи аудиоплеера с пультом ==== Протокола связи с пультом как такового нет. Пульт опрашивает RFID-сканер каждые 300 мс. При обнаружении карты производится проверка: не та ли это карта, которая была опрошена 300 мс назад? Чтобы даже при медленном поднесении карты к пульту не происходило более одной посылки команды, пульт выжидает 5 секунд до момента, когда старая карта снова может быть принята. Как только карта будет обнаружена, её идентификационный номер будет считан и отправлен по Wi-Fi на IP-адрес сервера. Сервер ожидает [[wpru>UDP|UDP]]-пакеты размером до 32 байт на [[wpru>Порт_(компьютерные_сети)|порте]] 2014. Пакеты бо́льшей длины обрезаются до 32-х байтов. Принятые пакеты сервер воспринимает как ID метки, один пакет — одна метка. Есть выделенный ID для остановки сервера - «quit» (посылается вызовом команды ''bechad stop''). ==== Сервер ==== Аудиосервер работает на Raspberry Pi B. Сервер представляет собой консольное Ruby-приложение, работающее в режиме [[wpru>Демон_(программа)|демона]]. Вызывая приложение с нужными аргументами можно запустить/остановить демона или составить плейлист. Последний представляет собой массив пар «уникальный код карты» → «путь к композиции». Сервер открывает слушающий [[wpru>Сокет_(программный_интерфейс)|UDP-сокет]] и ждёт пакеты. При этом он пишет в log-файл (по умолчанию — ''./becha.log'') сообщения о получении меток, старте, останове; во второй log-файл (по умолчанию — ''./becha.error.log'') сервер пишет ошибки исполнения. Пути к log-файлу и файлу ошибок задаются опциями ''-e'' и ''-E'' соответственно. Приём метки, соответствующей уже проигрываемой композиции, приводит к воспроизведению композиции с начала. Приём неизвестной метки приводит к остановке проигрывания. Таким образом, незапрограммированную метку можно использовать в качестве метки останова. Список композиций хранится в [[wpru>Yaml|YAML-формате]]. Для внесения новой записи в список композиций, необходимо выполнить команду ''record''. При выполнении этой команды сервер не переходит в режим демона. Вместо этого он ожидает метку, которую привязывает к пути аудиокомпозиции, переданному в качестве аргумента команде ''record''. Для успешного выполнения команды ''record'' сервер должен быть остановлен. Чтобы очистить playlist, просто удалите файл, в котором он хранится. Для старта сервера нужно выполнить команду ''start'', для останова — ''stop''. ===== Исходные коды ===== ==== Пульт. Скетч для Arduino ==== /* We use Arduino Leonardo */ //#define WIFI_DEBUG #include #include // Настройки Wi-Fi // Вместо «MySSID» необходимо указать имя вашей беспроводной сети #define SSID "MySSID" // Вместо «MyWifiPassword» необходимо указать пароль от вашей беспроводной сети #define KEY "MyWifiPassword" // WIFLY_AUTH_OPEN / WIFLY_AUTH_WPA1 / WIFLY_AUTH_WPA1_2 / WIFLY_AUTH_WPA2_PSK #define AUTH WIFLY_AUTH_WPA1_2 // Вместо 192.168.10.159 необходимо указать IP-адрес, // который получила Raspberry PI от вашего маршрутизатора #define UDP_HOST_IP "192.168.10.159" #define UDP_REMOTE_PORT 2014 #define UDP_LOCAL_PORT 2014 WiFly wifly(Serial1); SoftwareSerial rfidReader(10, 11); const char getReadID[] = { 0xAA , 0x00, 0x03, 0x25, 0x26, 0x00, 0x00, 0xBB }; const char nothing[] = { 0xAA , 0x00, 0x02, 0x01, 0x83, 0x80, 0xBB }; // #define GET_READ_ID_LEN (sizeof(getReadID) / sizeof(getReadID[0])) #define NOTHING_LEN (sizeof(nothing) / sizeof(nothing[0])) #define RFID_DATA_LEN 13 #define THROTTLE_DELAY 300 #define REPEAT_DELAY 5000 char incomingData[RFID_DATA_LEN]; char sendData[RFID_DATA_LEN]; unsigned long timePoint = 0; unsigned long timeClearPoint = 0; #define LED_PIN 13 void setup() { Serial1.begin(9600); rfidReader.begin(9600); Serial.begin(9600); #ifdef WIFI_DEBUG while (!Serial) ; #endif setupWiFly(); pinMode(LED_PIN, OUTPUT); } void loop() { while (Serial1.available()) Serial1.read(); //clear buffer // send an UDP packet periodically if (millis() - timePoint > THROTTLE_DELAY) { readRfidData(); bool dataDiffers = memcmp(sendData, incomingData, RFID_DATA_LEN); bool hasData = memcmp(incomingData, nothing, NOTHING_LEN); if (dataDiffers && hasData) { memcpy(sendData, incomingData, RFID_DATA_LEN); for (int i = 0; i < RFID_DATA_LEN; ++i) Serial1.print(sendData[i], HEX); Serial1.print("\r\n"); } timePoint = millis(); digitalWrite(LED_PIN, !digitalRead(LED_PIN)); } if (millis() - timeClearPoint > REPEAT_DELAY) { // clear stored data after to repeat send memset(sendData, 0, RFID_DATA_LEN); timeClearPoint = millis(); } } void readRfidData() { for (int counter = 0; counter < GET_READ_ID_LEN; ++counter) rfidReader.write(getReadID[counter]); while (!rfidReader.available()) ; delay(10); // wait for RX buffer fill up int i = 0; while (rfidReader.available() && i < RFID_DATA_LEN) incomingData[i++] = rfidReader.read(); while (rfidReader.available()) rfidReader.read(); //clear buffer } void setupWiFly() { Serial.println("--------- WIFLY UDP --------"); wifly.reset(); while (1) { Serial.println("Try to join " SSID ); if (wifly.join(SSID, KEY, AUTH)) { Serial.println("Succeed to join " SSID); wifly.clear(); break; } else { Serial.println("Failed to join " SSID); Serial.println("Wait 1 second and try again..."); delay(1000); } } setupUDP(UDP_HOST_IP, UDP_REMOTE_PORT, UDP_REMOTE_PORT); delay(1000); wifly.clear(); } void setupUDP(const char *host_ip, uint16_t remote_port, uint16_t local_port) { char cmd[32]; wifly.sendCommand("set w j 1\r", "AOK"); // enable auto join wifly.sendCommand("set i p 1\r", "AOK"); snprintf(cmd, sizeof(cmd), "set i h %s\r", host_ip); wifly.sendCommand(cmd, "AOK"); snprintf(cmd, sizeof(cmd), "set i r %d\r", remote_port); wifly.sendCommand(cmd, "AOK"); snprintf(cmd, sizeof(cmd), "set i l %d\r", local_port); wifly.sendCommand(cmd, "AOK"); wifly.sendCommand("save\r"); wifly.sendCommand("reboot\r"); } ==== Аудиоплеер. Скрипт для Raspberry Pi B ==== #!/usr/bin/env ruby require 'pathname' require 'optparse' require 'optparse/time' require 'ostruct' require 'socket' require 'yaml' class Pathname def extname= (extension) @path = @path[0..@path.length-extname.length-1] if extension.length > 0 @path << '.' if extension[0] != '.' @path << extension.to_s end end def chext (extension) res = self.clone res.extname = extension res end end APP_VERSION = 'BECHA-server v1.0.4' ARGV_COUNT = 1 DEFAULT_PLAYLIST_PATH = './becha.playlist.conf' DEFAULT_LOG_PATH = './becha.log' DEFAULT_ERRLOG_PATH = './becha.error.log' DEFAULT_PORT = 2014 def parse_options(args) options = OpenStruct.new opt_parser = OptionParser.new do |opts| opts.banner = "Usage: bechad [options] cmd [args]\n\n" + "Avalaible commands:\n" + " start\t\t- Power ON the tape recorder\n" + " stop\t\t- Power OFF the tape recorder\n" + " record track-path\t\t- Record track into the tape recorder" options.verbose = false options.playlist_path = Pathname.new(DEFAULT_PLAYLIST_PATH).expand_path options.log_path = Pathname.new(DEFAULT_LOG_PATH).expand_path options.errlog_path = Pathname.new(DEFAULT_ERRLOG_PATH).expand_path options.port = DEFAULT_PORT opts.separator "" opts.separator "Common options:" opts.on("-l", "--playlist path", String, "Define playlist path (default - " + DEFAULT_PLAYLIST_PATH.to_s + ")") do |path| options.playlist_path = Pathname.new path end opts.on("-p", "--port num", Integer, "Define UDP port number (default - " + DEFAULT_PORT.to_s + ")") do |port| options.port = port end opts.on("-e", "--log path", String, "Define log file (default - " + DEFAULT_LOG_PATH.to_s + ")") do |path| options.log_path = Pathname.new path end opts.on("-E", "--errlog path", String, "Define log file for errors (default - " + DEFAULT_ERRLOG_PATH.to_s + ")") do |path| options.errlog_path = Pathname.new path end opts.on("--verbose", "Print extra information") do options.verbose = true end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end opts.on_tail("--version", "Show version") do puts APP_VERSION exit end end opt_parser.parse!(args) [options, opt_parser] end res = parse_options(ARGV) options = res[0] opt_parser = res[1] if ARGV.count < ARGV_COUNT p opt_parser exit 1 end argv = ARGV.reverse cmd = argv.pop case cmd when 'start' playlist_path = Pathname.new(options.playlist_path.expand_path) playlist = YAML::load(IO.read playlist_path.expand_path) raise 'Config file is corrupted' unless playlist.class == Hash puts "Starting daemon..." Process.daemon STDOUT.reopen options.log_path.expand_path, 'a' STDERR.reopen options.errlog_path.expand_path, 'a' $stdout = STDOUT $stderr = STDERR STDOUT.sync = true STDERR.sync = true sock = UDPSocket.new sock.bind '', options.port puts "[#{Time.now}] Server started!" loop do packet = sock.recvfrom 32 if packet[0] == "quit" puts "[#{Time.now}] Exiting..." Process.kill "KILL", $mplayer_pid unless $mplayer_pid.nil? exit 0 end puts "[#{Time.now}] Received data: #{packet[0].to_s}" if playlist.has_key? packet[0].to_s puts "[#{Time.now}] Start playing: #{playlist[packet[0].to_s]}" begin Process.kill "KILL", $mplayer_pid unless $mplayer_pid.nil? $mplayer_pid = fork do `mplayer "#{playlist[packet[0].to_s]}"` end rescue => error puts 'Error playing' p error p "#{playlist[packet[0].to_s]}" $mplayer_pid = nil end else puts "[#{Time.now}] Item not found" begin Process.kill "KILL", $mplayer_pid unless $mplayer_pid.nil? $mplayer_pid = nil rescue $mplayer_pid = nil end end end loop { } when 'stop' sock = UDPSocket.new sock.send 'quit', 0, 'localhost', options.port when 'record' playlist_path = Pathname.new options.playlist_path.expand_path begin playlist = YAML::load IO.read playlist_path.expand_path rescue playlist = {} end raise 'Config file is corrupted' unless playlist.class == Hash track_path = argv.pop p opt_parser if track_path.nil? track_path = Pathname.new(track_path).expand_path sock = UDPSocket.new sock.bind '', options.port loop do packet = sock.recvfrom 32 if packet[0].length == 0 puts "Received error packet, retrying..." else playlist[packet[0]] = track_path IO.write playlist_path.expand_path, YAML::dump(playlist) if playlist == YAML::load(IO.read playlist_path.expand_path) puts "Recording successful: #{packet[0]} -> #{track_path}" break else puts "Recording error!" p playlist p '---' p YAML::load(IO.read playlist_path.expand_path) end end end else p 'Undefined command' p opt_parser exit 1 end exit 0 ===== Демонстрация работы устройства ===== {{youtube>JBCiB9PqbLA?t=4m2s}} ===== Что ещё можно сделать? ===== - Добавить к функционалу сервера веб-интерфейс, чтобы запись композиций, управление процессом проигрывания и другие действия можно было выполнять удалённо. - Добавить функцию «пауза». Привязать к этой функции отдельную метку, которой можно будет приостанавливать и возобновлять проигрывание. - Научить сервер доставать композиции из плейлиста аккаунта соцсети в интернете (например, из vk.com).