GPIO для чайников (часть 6)
-
Создаём простейшее интернет-радио.
Для начала составим себе техническое задание, чтобы чётко представлять, что мы хотим получить. Нам нужна программа, которая бы проигрывала аудиопотоки интернет-радиостанций. Программа должна автоматически запускаться при включении RPI, программа должна предоставлять возможность переключаться между радиостанциями, управление программой должно осуществляться без участия клавиатуры и мыши. Управление громкостью предусматривать не будем, т.к. с этим могут отлично справиться подключённые к RPI колонки, или наушники с регулятором громкости.
Для начала составим себе техническое задание, чтобы чётко представлять, что мы хотим получить.
Нам нужна программа, которая бы проигрывала аудиопотоки интернет-радиостанций. Программа должна автоматически запускаться при включении 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 <имя, или адрес проигрываемого файла>. Например, чтобы проиграть первый аудиопоток из нашего списка, можно набрать в терминале
omxplayer 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 // Библиотека для управления GPIO // Следующие 3 библиотеки необходимы для организации работы параллельного процесса #include #include #include #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.
Основная часть работы сделана. Осталось добавить нашу программу в автозапуск, чтобы она автоматически стартовала после загрузки системы. Сделать это нетрудно.
Создаём файл autostart.sh в директории /etc/init.d/
sudo nano /etc/init.d/autostart.sh
- В этом файле прописываем скрипт запуска нашего интернет-радио:
sudo /home/pi/Myprog/radio exit 0
обратите внимание на то, что нужно прописать полный путь до запускаемого файла. В моём случае файл radio находится в папке /Myprog/.
- Сохраняем файл и закрываем редактор nano. Теперь дадим права на исполнение нашему скрипту:
sudo chmod 755 /etc/init.d/autostart.sh
- Ну и добавим наш скрипт в автозапуск:
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-ом.
ПС. Я допускаю, что с точки зрения продвинутого программиста моя программа далека от идеала, и всё можно сделать более грамотно и возможно проще. Но я не профессиональный программист и мои знания, и опыт в программировании крайне малы. Но раз программа работает, и делает то, что от неё требовалось, значит можно считать, что задача выполнена.
-
-
Можно сделать более подробное описание кода для очень чайников
-
в частности хотелось бы подключить и присвоить каждой радиостанции свой светодиод
-
а также добавить кнопки для регулировки громкости
-
Подробнее- это уже получится целая книга, на которую у меня нет ни времени, ни опыта. Тем более, что все эти книги уже написаны другими. В инете всё в открытом дуступе есть. Так что извините, дальше развивайте свой уровень самостоятельно. В этом нет ничего сложного, просто нужно задачи решать постепенно с увеличением сложности. Любую сложную задачу можно разбить на множество простых, и решать их по отдельности.
По поводу добавить кнопочек. Для начала напишите простую программу, которая будет при каждом нажатии на кнопку зажигать очередной диод и гасить предыдущий. Как только она у вас получится, просто допишите свой код к коду плеера. Кроме получения готового продукта, вы ещё сможете получить громадное удовольствие от того, что справились со всем самостоятельно.
Ну а светодиодами можно и не ограничиваться. Можно подключить дисплей от старого сломанного мобильника и выводить на него название радиостанции и другую информацию. -
Она действительно далека от идеала
Например хардкодать список радиостанций это совсем не true way.
Форкаться необязательно нужно просто использовать неблокирующе функции.
Ну кроме того такую программу я бы написал на bash гораздо проще.
А в качестве плеера гораздо лучше подойдет mpd. Он специально заточен на управление извне и прочие фенечки.
http://mpd.wikia.com/wiki/What_MPD_Is_and_Is_Not -
Коллега, подскажите пожалуйста на какой версии сборки Вы реализовали этот проект. У меня сборка 2012-10-28-wheezy-raspbian и команда omxplayer не выполняется, только выдает сообщение: have a nice day ;).
Конкретно перепробовал разные адреса непосредственно взятые из Radio Tray, т.е. заведомо работающие и никак у меня не получается укротить omxplayer. -
Спасибо владельцам сайта за новый движок. Конечно пока не привычно и не обошлось без косяков. Кто захочет повторить приведенную в этом блоге программу для интернет-радио будьте внимательны к синтаксису. В данном случае закралась ошибка, которой, как мне кажется небыло на старом движке, а именно в строках содержащих оператор #include вместо уловых скобок <> появились апострофы '', т.е. если написано #include 'bcm2835.h', то следует читать #include <bcm2835.h>, иначе компилятор выдаст ошибку.
Теперь касательно темы блога.
У меня все получилось повторить, но пока до автозагрузки.
Спасибо автору.
К сожалению наша переписка после 31.01.13 пропала, поэтому повторюсь, т.к. решение вопроса по применению omxplayer, на мой взгляд не лежит на поверхности и может быть интересным другим пользователям, интересующимся проблемой воспроизведения аудио и видео.
Итак, для того, чтобы заставить omxplayer воспроизводить аудио необходимо в настройках raspi-config в параметре Memory_split указать значение 32, а для воспроизведения видео соответственно 64. -
Про угловые скобки знаю. Не было времени исправлять, хотелось побыстрее обновлённую версию выложить.
-
Не беда.
Все наладится. -
У меня не получилась автозагрузка как предлагает автор. Почему? Разбираться не стал, оставил на потом. Предполагаю, что предложенный автором способ заключается в вызове программы до запуска графической оболочки LXPanel. Я пошел иным путем, а именно включил функцию вызова программы в меню LXPanel и заставил ее выполняться после полной загрузки RPI, тем самым она работает в фоновом режиме и не мешает работе других приложений. Правда кроме звука визуально ничего не говорит о ее присутствии. Т.е. можно применять как чистое радио без клавы, мыши и пр. или полноценно как фоновую среду. На кнопочку отзывается из под любого приложения ну а удалить можно так как предлагает автор из LxTerminala Killall radio. На кнопку q у меня почему-то. хотя думаю понятно почему, не срабатывает, да это и к лучшему, не будет мешать другим приложениям.
Коротко:- В /usr/bin создал скрипт или исполняемый файл, не знаю как корректно выразиться, radio c содержимым
#! /bin/sh cd /home/pi/ sudo ./radio
- В /usr/share/applications/ , /home/pi/desktop , /home/pi/config/autostart создал файл radio.desktop c содержимым:
[Desktop Entry] Name=internet-radio Encoding=UTF-8 Comment=Amateur radio weak signal communications Exec=/usr/bin/radio TryExec=radio Terminal=true Type=Application Categories=AudioVideo;Audio;Player;GTK; Icon=radiotray
Может в какой из указанных папок и не нужно было вставлять его (radio.desktop). но у меня так получилось,
Теперь в главном меню в пункте Sound&Video появился пункт internet-radio и его при желании можно запускать непосредственно из главного меню. Категорию и иконку я взял от Radiotray, т.к. это ближе к теме, но можно указать любую другую или создать собственную указав в Categories=, хотя если честно, то я не пробовал. Икону также взял готовую от radiotray, свою создавать не пробовал.
Для того, чтобы программа не загружалась автоматически необходимо удалить radio.desktop из папки /home/pi/config/autostart.
Все. -
Надеюсь последний на сегодня мой комментарий.
При работе этой программы получаем 100% загрузку процессора и если пользуем ее в фоновом режиме, то это не очень хорошо, поэтому предлагаю лобовое решение по снижению загрузки проца до 15-20% без заметной потери функциональности. Просто несколько увеличилось время нажатия кнопки и все. Дискомфорта у мня такое решение не вызвало. Итак модифицируем одну строку:
while(bcm2835_gpio_lev(PIN_IN)){delay(1000);} // Просто ждём, пока не будет нажата кнопка управления
т.е. добавлен delay(1000);
Все. -
Несовсем верный вывод. Это в Виндусе, если показывается 100% загрузка процессора, значит компьютер задохнулся от возложенных на него задач и находится на грани зависания. В Линуксе на вашем RPi индикатор загрузки процессора показывает не его реальную загрузку, а скорее процент загрузки свободного времени. Если вы запустите ещё какое-либо приложение, оно спокойно запустится и будет работать. Просто на опрос кнопки процессор будет выделять меньше своего свободного времени. Задачи же имеющие более высокий приоритет, чем наша программа, сами прекрасно отберут себе ресурсов столько, сколько им нужно. Опрос состояния кнопки не является ресурсоёмкой задачей, поэтому и не представляет для процессора сколь-либо заметной нагрузки. Но раз у процессора куча свободного времени и делать ему больше нечего, он с удовольствием отдаёт всё своё свободное время процессу опроса кнопочки. Разумеется, если мы запусти несколько десятков тысяч процессов опроса кнопки, то они уже реально смогут загрузить процессор.
И ещё, если вы продолжаете использовать RPi как десктоп, то в репозитариях есть масса уже готовых интрнет-приёмников с графической оболочкой и тысячами радиостанций в плей-листе. Так что в вашем случае лучше выбрать одну из таких программ. (просто вбейте в поиск репозитария слово radio и выбирайте любую). -
Спасибо за разъяснение работы индикатора загрузки. Я не заядлый любитель линукс, но чисто визуально мне показалось, что при индикации полной загрузки процессора другие приложения прихрамывают, но это видимо отдельная тема.
Что касается второго вопроса о графической оболочке, то Вы видимо меня не совсем поняли. Я предложил иной способ загрузки прогаммы интернет радио в отличии от Вашего, т.к. Ваша методика у меня не сработала. Т.е. при описанном мною способе программа так же как и Вас загружается при включении RPI без клавы, мыши и пр.. Включил и получил радио, управляемое кнопкой. Ну так у меня получилось и не надо меня ругать. Это работает. Просто я имел в виду, что при таком способе сохраняется возможность полноценно применять графическую оболочки, при условии, что это необходимо. Еще раз повторюсь, что окна программы не видно. но кнопка работает. Может изложил сумбурно, но все это работает именно так как описано в Ваших техусловиях. -
Еще раз про загрузку программ из графической оболочки.
Как я понял есть программы, которые необходимо запускать именно при работаещей графике. Я столкнулся с этим при экспериментах с радиолюбительской программой WSPR. Все попытки загрузить эту прогу без графической оболочки у меня провалились. Это видимо от незнания и непонимания, но тем не менее результат был достигнут и прога стала загружаться автономно, т.е. при включении питания RPI. И я не вижу ничего зазорного при таком подходе. Может кому и пригодится. -
Ну дак а я вас и не ругаю. Способ, который описал я, является стандартным способом автозапуска приложений в Линуксе. Это не моё изобретение. Почему он у вас не сработал, я ничего сказать не могу, т.к. тоже в Линуксе чайник и сам с ним разбираюсь потихоньку.
-
Х-сервер (который запускается при старте графической оболочки LXDE, либо другой) требуют программы, которые для отображения графики используют его API. Грубо говоря, если интерфейс этих программ выглядит в виде типичного окна со стандартными кнопочками, то разумеется, просто в терминале такая программа не заработает. Есть программы, которые с графикой работают напрямую. Им LXDE тоже ненужен. В основном это либо игрушки, либо всякие демки с красивыми визуальными эффектами. А есть программы с псевдографическим интерфейсом, это когда графика рисуется в терминале печатными символами. Им тоже LXDE без надобности.
-
ОК!
Будем работать над ошибками.
Мой респект Вам! -
Куда уж подробнее. И так всё разжёвано
-
Здравствуйте!
А не могли бы рассмотреть работу RPI и датчика температуры DS18B20?