====== Аудиоплеер, управляемый 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).