Для начала составим себе техническое задание, чтобы чётко представлять, что мы хотим получить.
Нам нужна программа, которая бы проигрывала аудиопотоки интернет-радиостанций. Программа должна автоматически запускаться при включении RPI, программа должна предоставлять возможность переключаться между радиостанциями, управление программой должно осуществляться без участия клавиатуры и мыши. Управление громкостью предусматривать не будем, т.к. с этим могут отлично справиться подключённые к RPI колонки, или наушники с регулятором громкости.
Теперь набросаем общий алгоритм работы данной программы, т.е. последовательность действий:
- Старт программы (инициализация переменных, присваиваем счётчику проигрываемой радиостанции 1);
- Начинаем проигрывать радиостанцию, на которую указывает счётчик;
- Проверяем, нажата ли кнопка (если кнопка не нажата, то переходим к пункту 3);
- Если кнопка нажата, то увеличиваем счётчик радиостанций на 1, завершаем проигрывание текущей радиостанции и переходим к пункту 2.
Всё. Как видно из алгоритма, наша программа будет работать бесконечно. Выход из неё не предусмотрен. Но, т.к. в данном случае предполагается, что мы RPI будем использовать исключительно как интрнет-радиоприёмник, то в принципе выход и не нужен. Включать мы его будем включением в розетку источника питания, соответственно и выключать будем выключением БП из розетки. В прочем, если захочется, нам ничего не стоит сделать это в будущем.
Теперь обдумаем детали пройдясь по алгоритму.
По 1 пункту вопросов нет. Там всё ясно. В этом пункте нам, как минимум, нужно будет объявить и инициализировать переменную счётчика проигрываемой радиостанции и составить список этих самых радиостанций, т.е. плей-лист. Где взять адреса потоков интернет-радиостанций? Ну я просто вбил запрос в Гугль, и из него взял первые попавшиеся 4 ссылки свободного доступа. Вот они:
http://listen2.myradio24.com:9000/8304
http://89.208.99.16:8088/zvezda_128
http://imgradio.pro/MegaRadio
http://imgradio.pro/RusHit
Разумеется, вы можете взять любое количество, любых ссылок на любы интернет-радиостанции, которые нравятся лично вам.
По 2 пункту. Главный вопрос- как, или чем проигрывать радиостанции. Ответ на этот вопрос тоже простой- нужно либо написать программу- проигрыватель, либо взять уже готовую. Я, например, в программировании чайник. И написать аудиоплеер с нуля, для меня задача крайне трудновыполнимая. Поэтому я в решении данного вопроса выберу второй вариант. В стандартную комплектацию ОС Raspbian входит прекрасный консольный медиаплеер- omxplayer. Он является многофункциональной и законченной программой. Думаю, для решения нашей задачи трудно найти что либо более подходящее, чем omxplayer. Формат запуска этого медиплеера такой: omxplayer <имя, или адрес проигрываемого файла>. Например, чтобы проиграть первый аудиопоток из нашего списка, можно набрать в терминале oxplayer http://listen2.myradio24.com:9000/8304
Через пару секунд плеер подключится к данному потоку и в наушниках, вы услышите музыку. Чтобы прекратить воспроизведение и выйти из программы, в том же терминале нужно нажать клавишу q. Опций у этого плеера довольно много. С ними вы можете ознакомиться из хелпа к самому плееру. Но для нашего случая нам достаточно знать как этот плеер можно запустить и как его можно выключить. Мы это знаем, а значит с пунктом 2 мы тоже разобрались.
По пункту 3.Этот пункт для нас не составляет никаких проблем. Кнопку мы подключать научились. Нужно лишь выбрать порт, к которому мы её подключим. Выберем порт, который уже имеет подтяжку к +3,3в, а именно Р1-05. Ну и Р1-06 GND.
По пункту 4. Тоже никаких вопросов. Этот пункт решается программно без проблем.
Вроде бы можно приступать к реализации. Но тут скрываются ещё две проблемки, невидимые на первый взгляд, но очень существенные. Во-первых, вспомните нашу программу из второй части цикла статей. Мы там писали программу мигающего светодиода и завершали программу по нажатию кнопки. Там наш светодиод мигал 1 раз в секунду, потом происходил опрос состояния кнопки. И чтобы гарантированно выйти из программы, нам приходилось держать кнопку нажатой не менее 1 секунды, чтобы дождаться окончания цикла мигания светодиода. Иначе мы рисковали тем, что программа не заметит нажатие кнопки и не отреагирует на него. В нашем случае эта проблема встаёт наиболее остро. Ведь все команды в программе выполняются последовательно, и если мы запустим omxplayer на воспроизведение, то очередь до опроса состояния кнопки дойдёт лишь после завершения работы запущенного плеера, а значит никогда! Т.к. плеер наш должен выключиться только после нажатия на эту самую кнопку. Значит, нам нужно найти какой-то механизм, позволяющий опрашивать состояние кнопки даже во время запущенной другой программы. Во-вторых, нам нужно как-то из нашей программы запустить другую независимую программу (собственно сам omxplayer), да ещё и найти возможность управлять им, подменяя нажатие клавиш на клавиатуре нажатием нашей кнопки.
Для решения первой проблемы стоит обратить внимание на то, что Linux- многозадачная ОС. Мы видим, что при запущенном LXDE по нему прекрасно бегает курсор мышки, можем запустить браузер, текстовый редактор, треминал… И всё это вместе прекрасно сосуществует и работает. Нам нет необходимости закрывать LXDE, чтобы передвинуть курсор мышки на новое место. Значит, нам в программе нужно сделать тоже самое. Мы должны организовать работу плеера и опрос клавиатуры в разных задачах, но работающих одновременно. В Linux существует множество способов организации многозадачности. Для нашего случая, я посчитал, что наиболее оптимальным решением будет организация двух параллельных процессов. Вопрос этот достаточно обширен и не очевиден для лёгкого понимания. Поэтому, чтобы разобраться с ним, надо обратиться к литературе. Наберите в гугле «Linux процессы» и изучите найденные там статьи. Если же попытаться объяснить работу процессов в двух словах, то происходит это примерно так. Когда мы запускаем какую-то программу, то она запускается в отдельном процессе, который организует операционная система. Далее, в нашей программе мы можем создать несколько копий этого процесса. Основной процесс называется родительский, а созданные в программе другие процессы (порождённые) называются дочерними. Дочерние процессы подчиняются родительскому и наследуют его свойства. Так же из родительского процесса мы можем управлять дочерними (командовать), дочерние же процессы могут только просить родительский процесс о чём либо, либо рассказывать ему о своём состоянии. После создания родительского процесса мы можем запустить в нём любую другую программу. Причём программы в родительском и дочерних процессах будут выполняться одновременно, пользуясь многозадачностью, предоставляемую Linux-ом. Вот и мы поступим так же. Создадим параллельный процесс. В родительском процессе будем следить за состоянием кнопки, а в дочернем процессе запустим медиаплеер. Тепрь наша программа в целом сможет одновременно проигрывать поток и следить за нажатием кнопки, а так же отдавать команды этому медиаплееру.
Для решения второй проблемы мы используем функцию execlp(). Эта функция принадлежит целому семейству похожих функций. Все они позволяют запускать из своего приложения другие, но требуют различных параметров. Подробности использования данной функции можно легко найти через Гугл. А чтобы запущенный медиаплеер воспринимал нажатие нашей кнопки, как команду с клавиатуры, мы будем запускать его не в обычном терминале, а в псевдотерминале. Псевдотерминал, это такой терминал, который вроде бы есть, но на самом деле его нет, поэтому мы легко сможем ему скормить всё, что угодно, выдавая это за команды с клавиатуры. При чём об этом будет беспокоится сам Linux, а не мы. Для запуска процесса в псевдотерминале существует функция forkpty(), а для записи «всего, чего угодно» в этот псевдотерминал мы используем функцию write().
И так, чтобы полностью осознать то, как это всё вместе будет работать, вам нужно самостоятельно через гугл найти описание процессов в Linux, работу функций forkpty(), execlp() и попытаться разобраться.
Теперь вроде всё готово для написания нашей программы. У меня получилась такая:
// Упражнение к 5-й части статей "GPIO для чайников."
// Интернет-радио для Raspberry Pi
//
// Переключение между станциями осуществляется
// кратковременным нажатием на кнопку.
// Кнопку управления подключить к Р1-05 и Р1-06
//
// Компиляция:
// gcc -o radio radio.c -lutil -lrt -lbcm2835
//
// Запуск: sudo ./radio
// или автозапуск при помощи скрипта.
#include <bcm2835.h> // Библиотека для управления GPIO
// Следующие 3 библиотеки необходимы для организации работы параллельного процесса
#include <stdlib.h>
#include <pty.h>
#include <unistd.h>
#define PIN_IN RPI_GPIO_P1_05 // Определяем порт GPIO, к которому будет подключена кнопка
#define STATIONS_MAX_NUM 4 // Количество станций в нашем плейлисте
// Это наш маленький типа плэйлист
// Между кавычек вставляем URL потока вещания станции
char *station1={"http://listen2.myradio24.com:9000/8304", NULL};
char *station2={"http://89.208.99.16:8088/zvezda_128", NULL};
char *station3={"http://imgradio.pro/MegaRadio", NULL};
char *station4={"http://imgradio.pro/RusHit", NULL};
// Функция запускает omxplayer в псевдотерминале
// и заставляет проигрывать заданный поток
// Параметр функции **omx_arg содержит текстовую строку с URL потока
// Это дочерний процесс
void do_child (char **omx_arg)
{
if (execlp("omxplayer", "omxplayer", *omx_arg) < 0)
{
perror("exec omxplayer");
exit(1);
}
}
// В основном процессе мы будем опрашивать состояние кнопки
// и обрабатывать её нажатие, передавая команды
// omxplayer-у, работающему в дочернем процессе.
// Входными параметрами функции являются:
// fd- дескриптор файл;
// p- ID организованного процесса;
// n- порядковый номер проигрываемой станции.
// На выходе функция возвращает порядковый номер следующей станции.
int do_parent (int fd, pid_t p, int n)
{
while(bcm2835_gpio_lev(PIN_IN)){} // Просто ждём, пока не будет нажата кнопка управления
bcm2835_delay(500); // Эта задержка в 500мс нужна для фиксации нажатия кнопки
char r;
write(fd, "q", 1); // Здесь мы посылаем omxplayer-у эквивалент нажатия на клавишу "q" клавиатуры
// что заставляет плеер завершить работу
while (read(fd, &r, 1) > 0) { write(1, &r, 1); } // Всё, что ниже, нужно для корректного завершения процесса
waitpid(p, 0, 0);
close(fd); // Всё, плеер выключен, процесс завершён. Можно начинать всё заново с новой станцией
++n; // Увеличиваем порядковый номер проигрываемой станции на 1
if (n>STATIONS_MAX_NUM) n=1; // Проверяем, если мы проиграли все станции в плей-листе, то устанавливаем номер снова на 1
return(n); // Выходим из функции и возвращаем номер очередной станции
}
// Функция проигрывания очередной станции
// Входной параметр n- порядковый номер проигрываемой станции.
// Если добавили в плей-лист ещё станций, то и здесь нужно увеличить
// количество строк case
void play_station(int n)
{
switch (n)
{
case 1: do_child(station1);
case 2: do_child(station2);
case 3: do_child(station3);
case 4: do_child(station4);
}
}
// Основная программа
int main()
{
int fd; // файловый дескриптор
int n=1; // порядковый номер станции
if (!bcm2835_init()) // Инициализация GPIO
return 1; //Завершение программы, если инициализация не удалась
bcm2835_gpio_fsel(PIN_IN, BCM2835_GPIO_FSEL_INPT);
while(n) // Бесконечный цикл. Выход не предусмотрен.
{
pid_t p = forkpty(&fd, 0, 0, 0); // Запускаем параллельный процесс в псевдотерминале
switch (p)
{
case 0: play_station(n); // Если всё получилось, запускаем плеер в созданном процессе
case -1: perror("forkpty"); // Иначе аварийное завершение.
exit(EXIT_FAILURE);
default: break;
}
n=do_parent(fd, p, n); // А в основном процессе начинаем следить за кнопкой.
}
}
Ну вот, как то так!
Теперь эту программку нужно сохранить под именем radio.c, скомпилировать и можно запускать. Команда для компиляции написана в шапке программы. Перед запуском не забудьте подключить кнопку к пинам Р1-05 и Р1-06. Если нет ошибок, то после запуска программы вы должны услышать музыку, а каждое нажатие на кнопку должно переключать медиаплеер на следующую станцию. Когда наиграетесь, завершить выполнение программы можно нажатием на Ctrl-c. А чтобы наверняка прибить все запущенные процессы, можно ввести команду sudo killall radio.
Основная часть работы сделана. Осталось добавить нашу программу в автозапуск, чтобы она автоматически стартовала после загрузки системы. Сделать это нетрудно.
1. Создаём файл autostart.sh в директории /etc/init.d/
sudo nano /etc/init.d/autostart.sh
2. В этом файле прописываем скрипт запуска нашего интернет-радио:
sudo /home/pi/Myprog/radio
exit 0
Обратите внимание на то, что нужно прописать полный путь до запускаемого файла. В моём случае файл radio находится в папке /Myprog/.
3. Сохраняем файл и закрываем редактор nano. Теперь дадим права на исполнение нашему скрипту:
sudo chmod 755 /etc/init.d/autostart.sh
4. Ну и добавим наш скрипт в автозапуск:
sudo update-rc.d autostart.sh default
Если всё сделано верно, то RPI ответит на это сообщением:
update-rc.d: using dependency based boot sequencing
Это всё. Теперь можно перегрузить RPI. После перезагрузки мы должны услышать трансляцию аудиопотока первой по списку интернет-радиостанции. Если всё так, то выключаем RPI и выдёргиваем из него все провода (если у вас нет Wi-Fi адаптера, то Ethernet кабель придётся оставить). Прячем наш RPI в изящную коробочку, выводим на панель нашу кнопочку, подключаем колонки и ставим на холодильник готовое устройство, наслаждаемся проделанной работой под музычку, играемую нашим RPI-интернет-радио. Остаётся теперь заказать ещё один RPI, чтобы можно было и дальше проводить различные эксперименты и создавать различные устройства.
Если же вы не планируете пожизненно использовать данный RPI под радио, то когда наиграетесь, можно убрать наш скрипт из автозагрузки командой:
sudo update-rc.d autostart.sh remove
После этого можно вообще удалить наш скрипт autostart.sh.
На этом закончим. Желающие могут легко модифицировать программу под свои нужды. Например, можно добавить ещё пару кнопок и привязать к ним управление громкостью, или выключение. Для этого достаточно последовательно опрашивать порты на которые подключены кнопки и при нажатии на них передавать функцией write() символы, или коды клавиш управления omxplayer-ом.
ПС. Я допускаю, что с точки зрения продвинутого программиста моя программа далека от идеала, и всё можно сделать более грамотно и возможно проще. Но я не профессиональный программист и мои знания, и опыт в программировании крайне малы. Но раз программа работает, и делает то, что от неё требовалось, значит можно считать, что задача выполнена.