Аудиоплеер, управляемый RFID-карточками
- Платформы: Arduino Leonardo, Raspberry Pi B
- Языки программирования: Wiring (C++), Ruby
Что это такое?
В этой статье мы расскажем о том как собрать управляемый при помощи RFID-меток аудиоплеер. Устройство состоит из двух частей:
- Пульт, который управляет воспроизведением музыки. Чтобы включить определённую мелодию, достаточно всего лишь поднести к нему нужную RFID-метку. В нашем случае это карточки от Московского метрополитена.
- Аудиоплеер воспроизводит выбранную мелодию.
Что для этого необходимо?
Для изготовления пульта нам понадобятся:
- Пара карточек от Московского метрополитена
Для изготовления аудиоплеера нам понадобятся:
- SD карта объемом ≥4,5 ГБ
- Акустические колонки и кабель для них.
Сборка пульта
- При помощи Arduino IDE запрограммируйте Arduino Leonardo скетчем rfid_wifi.ino.
- Установите Wireless Shield на Arduino Leonardo.
- Установите Wi-Fi Bee v2.0 на Wireless Shield. Обратите внимание: переключатель на Wireless Shield должен стоять в положении «MICRO».
- Установите RFID-сканер на Breadboard Mini. Поместите получившуюся конструкцию на Wireless Shield.
- Соедините 11-й пин Arduino с ножкой RX RFID-сканера.
- Соедините 10-й пин Arduino с ножкой TX RFID-сканера.
- Соедините контакты питания RFID-сканера с контактами питания Arduino (красный провод — 5 V, чёрный — GND).
- Поместите конструкцию в корпус пульта и подсоедините питание.
Сборка аудиоплеера
- Залейте образ Raspbian от Wolfson на SD-карту.
- Установите аудиокарту Wolfson на Raspberry Pi.
- Вставьте SD-карту в Raspberry Pi.
- Подключите питание и сетевой кабель.
- Залогиньтесь по SSH на Raspberry Pi (по умолчанию логин —
pi
, пароль —raspberry
)
Обратите внимание: для подключения к Raspberry Pi по сети вам потребуется узнатьIP-адрес, назначенный Raspberry Pi вашим маршрутизатором. Для этого можно воспользоваться Web-интерфейсом маршрутизатора или специализированными программами, такими как Advanced IP scanner.
- Клонируйте репозиторий кода:
git clone https://github.com/amperka-projects/becha-rfid cd ./becha-rfid
- Для установки сервера выполните команды:
cd ./server ./prepare_wolfson sudo gem install bechad
После установки сервера мы можем выполнять три простых действия: привязать композицию к карточке (добавление трека в playlist), запустить и остановить сервер.
- Для добавления трека в playlist:
bechad record <path-to-track>
- Для запуска сервера:
bechad start
- Для остановки сервера:
bechad stop
Как это работает?
Протокол связи аудиоплеера с пультом
Протокола связи с пультом как такового нет. Пульт опрашивает RFID-сканер каждые 300 мс. При обнаружении карты производится проверка: не та ли это карта, которая была опрошена 300 мс назад? Чтобы даже при медленном поднесении карты к пульту не происходило более одной посылки команды, пульт выжидает 5 секунд до момента, когда старая карта снова может быть принята. Как только карта будет обнаружена, её идентификационный номер будет считан и отправлен по Wi-Fi на IP-адрес сервера.
Сервер ожидает UDP-пакеты размером до 32 байт на порте 2014. Пакеты бо́льшей длины обрезаются до 32-х байтов. Принятые пакеты сервер воспринимает как ID метки, один пакет — одна метка. Есть выделенный ID для остановки сервера - «quit» (посылается вызовом команды bechad stop
).
Сервер
Аудиосервер работает на Raspberry Pi B. Сервер представляет собой консольное Ruby-приложение, работающее в режиме демона. Вызывая приложение с нужными аргументами можно запустить/остановить демона или составить плейлист. Последний представляет собой массив пар «уникальный код карты» → «путь к композиции».
Сервер открывает слушающий UDP-сокет и ждёт пакеты. При этом он пишет в log-файл (по умолчанию — ./becha.log
) сообщения о получении меток, старте, останове; во второй log-файл (по умолчанию — ./becha.error.log
) сервер пишет ошибки исполнения. Пути к log-файлу и файлу ошибок задаются опциями -e
и -E
соответственно.
Приём метки, соответствующей уже проигрываемой композиции, приводит к воспроизведению композиции с начала. Приём неизвестной метки приводит к остановке проигрывания. Таким образом, незапрограммированную метку можно использовать в качестве метки останова.
Список композиций хранится в YAML-формате. Для внесения новой записи в список композиций, необходимо выполнить команду record
. При выполнении этой команды сервер не переходит в режим демона. Вместо этого он ожидает метку, которую привязывает к пути аудиокомпозиции, переданному в качестве аргумента команде record
. Для успешного выполнения команды record
сервер должен быть остановлен. Чтобы очистить playlist, просто удалите файл, в котором он хранится.
Для старта сервера нужно выполнить команду start
, для останова — stop
.
Исходные коды
Пульт. Скетч для Arduino
- rfid_wifi.ino
/* We use Arduino Leonardo */ //#define WIFI_DEBUG #include <SoftwareSerial.h> #include <WiFly.h> // Настройки 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
- bechad
#!/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
Демонстрация работы устройства
Что ещё можно сделать?
- Добавить к функционалу сервера веб-интерфейс, чтобы запись композиций, управление процессом проигрывания и другие действия можно было выполнять удалённо.
- Добавить функцию «пауза». Привязать к этой функции отдельную метку, которой можно будет приостанавливать и возобновлять проигрывание.
- Научить сервер доставать композиции из плейлиста аккаунта соцсети в интернете (например, из vk.com).