Да си напишем симплексен ретранслатор (папагал) на Bash
Преди около две седмици се захванах със задачата да си напиша простичка софтуерна имплементация на папагал (симплексен ретранслатор) на Bash за радиолюбителски нужди с поддръжка на DTMF-и и възможност за text-to-speech без да се ползва външен софтуер за последното.
Всъщност, „напиша“ сигурно звучи високопарно – може би правилната думичка е „сглобя“, тъй като 90% от функционалността на ретранслатора, която си струва, се върши от външни tool-ове и едно (две?) API с уеб интерфейс. Затова използвам и Bash за нещо подобно.
Оказа се не толкова трудно, колкото очаквах. По-голямото предизвикателство бе да се спра на архитектурата на папагала (пак незаслужено сложна думичка 🙂 ), или по-конкретно – как да си взаимодействат отделните му части.
Интерфейс с радиото
Идеята е да имаме класически интерфейс през звуковата карта на компютъра – изходът от speaker-а на радиото влиза на входа на микрофона на звуковата карта, а изходът на звуковата карта да влиза в микрофона на радиото.
Активираме предавателя на радиото през VOX (в моя случай) или с управление на GPIO пиновете на Raspberry Pi (по който начин за това си харесате от многобройните готови решения).
Като тестово радио, аз използвам Baofeng UV-5R. Радиото притежава Kenwood конектор, което го прави съвместимо с многобройни аксесоари, създадени за по-скъпи радиа, и едновременно с това осигурява много удобен интерфейс за вкарване и изкарване на аудио.

Горният жак е 3.5 мм и е за микрофона – така влиза аудио в радиото. Долният е 2.5 мм и е изходът от speaker-а.
Ако човек погледне по-горната схема обаче, сигурно ще заподозре или направо ще му стане ясно, че не може просто да си купи два кабела и да започне да изпраща и получава аудио от радиото. Защо? Масата на двата жака се използва като ключ за активиране на PTT-то на радиото. Когато включите и двата кабела в звуковата карта, масите се съединяват, което активира PTT-то за постоянно.
За решаване на проблема има два варианта, избирате си – или режете масата на един от двата кабела (да кажем на микрофона), или си купувате нещо подобно, което галванично развързва връзката и масата повече не е проблем, пък и идва с кабелче 3.5 мм към 3.5 мм. 🙂
Хардуерът в проекта свършва в този момент!
Архитектурата
Преди да продължа, държа да отбележа, че използваме GNU/Linux (в случая, Debian-базирано дистро) и Bash за shell.
Целият папагал се сведе от един Bash скрипт и 3 основни външни инструмента, които вършат почти всичката работа:
1. arecord – взимаме аудио от микрофонния вход на звуковата ни карта със sample rate 22k (заради DTMF-ите), и pipe-ваме на
2. sox, конфигуриран със silence филтър, който се държи като squelch, а с pseudo-effect-а restart правим нов файл в дадена директория при всяко „затваряне на squelch-а“. Тук pipe-ът свършва, а нататък поема
3. inotifywait, изходът от който се обработва в рамките на while цикъл, като tool-ът ни съобщава за всеки нов файл, който се появява в директорията, за да можем да вземем името му и да работим с него.
4. Името на новосъздадения файл, който по всяка вероятност съдържа нашата последно записана активност през папагала, се подава на multimon-ng, като му се указва да се опита да разпознае дали аудиото съдържа DTMF-и, без значение дали са смесени с глас или нещо друго или пък къде точно като време в записа се намират.
5. Ако намерим DTMF, изпълняваме съответната функция на която отговаря (виж по-долу), ако не – повтори съобщението.
Софтуерът
Първо, пускаме споменатия pipeline в точки 1 и 2 по-горе:
arecord -f S16_LE -c 1 -r 22k -t raw | sox --norm=-0 -t raw -r 22k -e signed -b 16 -c 1 -V1 - "./rec/.wav" silence 1 0.5 1% 1 0.5 1% : newfile : restart
Обяснението по-горе мисля, че е достатъчно, за да не се налага тук да обяснявам какво правим. 🙂
Продължаваме със стартиране на inotifywait, който слуша за новосъздадени или преместени в поддиректорията (на текущата директория) /rec файлове:
inotifywait -m ./rec -q -e create -e moved_to |
while read dir action file; do
Тук, преди да подадем името на файла, който съдържа записа на това, задействало папагала, трябва да решим един проблем. Когато се стартира с newfile псевдо ефекта, sox създава празен wav файл в директорията ни, който се попълва с аудиото чак при неговото рестартиране заради restart псевдо ефекта. Пускайки sox в началото на скрипта (преди inotifywait) означава, че inotifywait винаги ще ни уведомява за новия празен wav файл, който sox е създал, а не за този, съдържащ нашата последна връзка. Тъй като sox кръщава wav файловете с пореден номер – 001, 002, и т.н., това лесно може да се реши:
f=${file%.wav}
printf -v f %03d "$(( 10#$f -1 ))"
Просто премахвам разширението на файла и вадя единица от останалото число. 🙂 Вече в променливата $f имаме името на файла, съдържащ нашето последно QSO през папагала, и можем да го подадем на multimon-ng в търсене на DTMF-и:
multimon-ng -a DTMF -q -m -t wav ./rec/$f.wav | grep -oiP '(?<=DTMF: )[[:digit:]]+' | head -n 1
Инициализирам multimon-ng в тих режим (-q) и без предупреждения от SoX (-m). Последният ключ е много важен, защото често sox се оплаква за различни неща, например твърде силно аудио.
По-нататък, с регулярен израз извличам само числата от output-а на sox, който е във вида:
DTMF: 1
DTMF: 5
DTMF: 9
Тъй като в момента реагирам само на един DTMF, взимам единствено първия с head. Не чета и * или #, но ако искате да ги взимате, просто добавете [#*] в regex-а.
Оттук насетне, просто присвояваме резултата от multimon-ng в променлива и с една if-elseif-else структура реагираме на различните стойности, имплементирайки най-различни функционалности при получен DTMF. 🙂
if [ ! -z "$num" ]; then
if [ "$num" -eq "2" ]; then
#кажи колко е часа
speak_time "$f"
elif [ "$num" -eq "1" ]; then
#повтори последното съобщение
(...)
Предполагам се схваща идеята.
Text-to-speech
Както си и поставих за цел, правя това без външен софтуер, с… API-то на Google Translate:
function speak ()
{
wget -q -U Mozilla -O - "http://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&q=$1&tl=en" | play -t mp3 -
(...)
$1 е подаденият на функцията параметър, ако някой не е запознат с Bash. Google ще ни върне mp3 файл с всичко на английски, което поискаме да бъде „казано“. Нямам спомен откъде точно бях набарал API-то, имам го bookmark-нато от години, ама сигурно ще е резултат от някоя вечер ровене из stackoverflow за нещо. 🙂
С play е ясно какво правим – просто пускаме mp3 файла.
Опашка / roger beep
Можем да си направим опашка тривиално със sox, който може да генерира тонове:
function roger ()
{
if [ ! -z "$1" ] &&
[ "$1" -eq "0" ]; then
play -n synth 0.1 sin 800
play -n synth 0.1 sin 650
else
play -n synth 0.1 sin 650
play -n synth 0.1 sin 800
fi
}
При подаване на параметър 0 на функцията, се чува опашка, която може да пускаме след края на повтореното съобщение. При викане без параметър или с който и да е друг, пускаме „отварящ“ roger beep, който индикира, че папагалът успешно е записал съобщението и предстои да го повтори.
Съответно кодът с който повтаряме записаната активност, АКО нямаме налични DTMF-и в нея (т.е. това е частта else на първоначалната ни if-elseif-else конструкция, с която обработваме изхода от multimon-ng), е простичък:
roger
play ./rec/$f.wav
roger "0"
Други интересни нещица
Една от функциите ми казва колко е температурата в момента и вероятността за валежи. Взимам тези данни от sinoptik.bg, поради наличието на лимити в безплатните API-та за времето. За целта използвам писания на Go HTML parser pup, който е изключително бърз и функционален:
temp=$(curl -s 'https://www.sinoptik.bg/dobrich-bulgaria-100726418' | ./bin/pup 'span[class="wfCurrentTemp"]' | tr -dc '0-9','-')
rain=$(curl -s 'https://www.sinoptik.bg/dobrich-bulgaria-100726418/hourly' | ./bin/pup ':contains("%")' | grep -Eo '[0-9]{1,2}' | head -n 1)
Папагалът/скриптът може да прави и още някои неща, като например да се идентифицира през интервал от време, да се изключва / включва с DTMF, да се ограничи броя връзки, които да се пазят (примерно ако директорията в която записваме връзките е на RAM файлова система) и да се архивират при спиране на скрипта всички връзки, направени от пускането му, но тези неща не са толкова интересни и ще ги видите в кода.
Накрая искам да уточня, че идеята тук не е да се дублира SvxLink – трудно ще го стигнем с един Bash скрипт. 😀 Просто имах нужда от нещо съвършено просто, което има ограничен набор външни зависимости и използва инструменти с много приложения, налични във всички дистрибуции.
И споменавайки зависимости – на Debian-базирано дистро, имате нужда от следните пакети, които използва папагалът: sox, libsox-fmt-mp3, inotify-tools . Предполага се, че alsa имате.
Кода, заедно с всичко необходимо за тест, може да свалите от тук.