Да си напишем симплексен ретранслатор (папагал) на Bash

Да си напишем симплексен ретранслатор (папагал) на Bash

Преди около две седмици се захванах със задачата да си напиша простичка софтуерна имплементация на папагал (симплексен ретранслатор) на Bash за радиолюбителски нужди с поддръжка на DTMF-и и възможност за text-to-speech без да се ползва външен софтуер за последното.

Всъщност, „напиша“ сигурно звучи високопарно – може би правилната думичка е „сглобя“, тъй като 90% от функционалността на ретранслатора, която си струва, се върши от външни tool-ове и едно (две?) API с уеб интерфейс. Затова използвам и Bash за нещо подобно.

Оказа се не толкова трудно, колкото очаквах. По-голямото предизвикателство бе да се спра на архитектурата на папагала (пак незаслужено сложна думичка 🙂 ), или по-конкретно – как да си взаимодействат отделните му части.

Интерфейс с радиото

Идеята е да имаме класически интерфейс през звуковата карта на компютъра – изходът от speaker-а на радиото влиза на входа на микрофона на звуковата карта, а изходът на звуковата карта да влиза в микрофона на радиото.

Активираме предавателя на радиото през VOX (в моя случай) или с управление на GPIO пиновете на Raspberry Pi (по който начин за това си харесате от многобройните готови решения).

Като тестово радио, аз използвам Baofeng UV-5R. Радиото притежава Kenwood конектор, което го прави съвместимо с многобройни аксесоари, създадени за по-скъпи радиа, и едновременно с това осигурява много удобен интерфейс за вкарване и изкарване на аудио.

dualplug

Горният жак е 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 имате.

Кода, заедно с всичко необходимо за тест, може да свалите от тук.

Коментарите не са позволени.