Linux-десктоп своими руками: подключаем bluetooth-наушники

Вот казалось бы, что такое Bluetooth?
По сути — просто протокол связи, некая замена проводам, который в том числе позволяет передавать через себя разные данные.

Как мог бы он выглядеть с точки зрения классического UNIX-way: при подключении, скажем, наушников создавались бы некоторые устройства, ну например /dev/bt/pcm, и возможно /dev/bt/control, с помощью команд отправляемых в control можно было бы управлять свойствами pcm.

А какая-нибудь программа могла бы вместо /dev/snd/pcm отправлять звуковой поток в /dev/bt/pcm.
Ну, примерно так, как это происходит сегодня при подключении флешки (/dev/sdX) или usb-tty (/dev/ttyUSBX) — всё работает аналогично встроенным дискам или COM-портам, программам разницы нет.
Но это с точки зрения всяких динозавров.

Но Bluetooth появился не так давно, и разработкой софта для него занимались те, для кого UNIX-way — это что-то древнее и непонятно зачем, поэтому сделали по-современному, модно-стильно-молодежно.

Работой с bluetooth-у нас занимается специальный демон, bluetoothd. Что и как он там конкретно делает — простому смертному знать не положено, для простого смертного есть специальная книга заклинаний — bluetoothctl.
Нужно сказать ему правильные заклинания — например, «scan on» или там «connect XXXXXXX» — все остальное демон делает сам.

Но одного демона для такого важного дела мало — поэтому, например для наушников, нужен еще один демон, pulseaudio. Этот как раз занимается тем, куда отправлять звук — в родную «железку» или по bluetooth-каналу.

Для того, чтобы они могли общаться между собой — нужен еще один демон, D-Bus. Они отправляют друг другу сообщения через него, и таким образом обеспечивается взаимодействие.
Вообще, идея D-Bus, на самом деле — отличная: просто передавайте нужные сообщения и команды, и реагируйте на них — и всё будет хорошо. Магия!
Но есть нюанс…

Чем вообще магия отличается от науки?
Наука оперирует формулами и пониманием: камень падает потому что на него действует гравитация, перо летает по воздуху потому сопротивление воздуха падению пера противодействует силе гравитации, а движение воздушных потоков может изменить вектор движения, и всё это хоть и может быть сложно, но поддается пониманию и расчетам.

Магия же оперирует заклинаниями: нужно просто правильно произнести «Вингардиум левиосса!» (почему именно Вингардиум? хз, такое заклинание) и взмахнуть специальной волшебной палочкой.
А если не получается — значит, вы произнесли неправильно, или не так взмахнули, или палочка не волшебная, или место заколдованное, или в этом мире магия просто не работает.
Причина неизвестного характера — пробуйте еще.

Так и тут. Пока вы находитесь в магической среде, скажем, Gnome DE — всё более-менее работает, если не глючит.
Ну, может быть надо иногда «выйти и войти», плюнуть через левое плечо или постучать по дереву — должно работать, просто произнесите правильно заклинание.

Возьмем, к примеру, Убунту: сконнектили наушники, все прекрасно работает — убрали наушники.
Достали наушники снова — ии? И надо снова зайти в настройки блютуса, выбрать подключенные наушники, отключить подключенные наушники, включить их снова — оппа, «коннектед» — можно пользоваться дальше.
Почему? — Потому что «так здесь заведено!», таков ритуал.

Но стоит выйти за пределы этого мира…

Вот например, не проходит заклинание коннекта. Устройство обнаруживается -, но не хочет подключаться.
В процессе колдунства выясняется, что это не оно «не хочет», и даже не bluetoothd-демон вредничает, а проблема в том, что pulseaudio почему-то падает в обморок от вида наушников, и тогда блютуз-демон, видимо, из солидарности, говорит что подключение не удалось.

Если на pulseaudio побрызгать святой водой и поднять заново, но очень быстро, пока блютусный демон не передумал его пугать — то внезапно наушники подключаются и даже прекрасно работают. Магия!
(это не стёб — оно РЕАЛЬНО так себя ведет)

То есть, это не аппаратная проблема -, а проблема в межличностных взаимоотношениях демонов. Просто прекрасно.

Или вот например, D-Bus. В теории, согласно магическим книгам, через него демону Блютуса можно отправить команду «начни сканирование» — и он начнет, а о найденном сообщит через тот же D-Bus.

Но в реальности, команду-то он получит, и даже начнет ее выполнять -, но D-Bus тут же скажет «отставить!».
Почему? Потому что если вы, отдав команду, не стоите у него над душой и не требуете немедленного результата — значит, вам не очень-то и хотелось, недостаточно выражено намерение включить поиск.
Видимо так, потому что других, более логичных причин, почему D-Bus отправляет команду завершения сканирования — не видно.
Его кто просил, спрашивается?

Возможно, изучение исходного кода дало бы понимание логики разработчика -, но вот еще демонологией заниматься некогда.

Опять же, пресловутая «интеграция».
Как уже говорил — идея-то прекрасная, обмен сообщениями -, но почему, если понадобилось что-то поменять в конфигах и перезапустить D-Bus — тут же молча падает браузер? (не говоря про обмороки у pulseaudio — это само собой).
Причем тут браузер?!

Что, современные разработчики unix-софта никогда не слышали про какие-нибудь сокеты, про сеть, обрывы, реконнекты?
Почему какой-нибудь MQTT-сервер можно прекрасно перезапускать, и куча железок, обменивавшихся через него сигналами и командами, продолжит работать (если не совсем криворукие программисты их программировали), а остановка D-Bus приводит к крашу программ?
Может тогда заменить D-Bus на MQTT?
Опять же, модно-стильно-молодежно, IoT, беcшовное взаимодействие…

В общем, магия и колдовство.
Все «советы из интернета» по данной теме сводятся к повторению «попробуйте ещё такое заклинание — оно точно должно работать!» — поэтому поддержу эту традицию и вот мой набор работающих заклинаний:

Итак, допустим, ставим всё с нуля:

Для начала устанавливаем того самого Bluetooth-демона и его напарника для Pulseaudio:

apt install bluez pulseaudio-module-bluetooth

Теперь найти и обезвредить то, что заставляет падать в обморок Pulseaudio

vim /etc/pulse/default.pa


# закомментировать вот это
#load-module module-suspend-on-idle

vim /etc/pulse/client.conf

autospawn = yes daemon-binary = /usr/bin/pulseaudio

vim /etc/pulse/daemon.conf


exit-idle-time = -1

(что-то из этого лишнее, но магия требует больше заклинаний)
Перезапускаем pulseaudio:

pulseaudio --kill
pulseaudio --start

Теперь пытаемся что-то настроить:

bluetoothctl
show — должен показать список имеющихся работающих контроллеров.
Возможно, стоит попробовать включить их, если они выключены (Powered: no)
power on — включение
scan on — сканирование

Попробовать что-то подключить из найденного

connect XX: XX: XX: XX: XX: XX: XX

Всякие наушники-колонки должны просто подключиться, без спаривания и прочего.
Если подключение прошло успешно — можно из «запомнить»

trust XX: XX: XX: XX: XX: XX: XX

Тогда при повторном обнаружении в зоне видимости они сами автоматически должны подключиться.

А вот чем хороша магия — если заклинания сработали как надо — то это всё, можно пользоваться, дальше разбираться не обязательно.
Подключенное устройство должно появиться в pavucontrol:
если это колонка или наушники — то в Outputs, если с микрофоном — то еще и в Inputs.

Остается только сделать простейшую графическую обертку для этих команд (да, есть blueman, знаю).
По традиции — снова Perl:

apt install libgtk3-perl

#!/usr/bin/perl -w

use strict;
use warnings;
use IPC::Open2;
use Gtk3 -init;

# Создание окна GTK3
my $window = Gtk3::Window->new('toplevel');
$window->set_title("Bluetooth Devices");
$window->set_default_size(400, 300);

$window->signal_connect(destroy => sub {
    Gtk3->main_quit;
});

# Создаем главный контейнер GtkBox (вертикальный)
my $vbox = Gtk3::Box->new('vertical', 5);
$window->add($vbox);


# Список устройств
my $list_store = Gtk3::ListStore->new('Glib::String', 'Glib::String', 'Glib::String');
my $tree_view = Gtk3::TreeView->new($list_store);
$vbox->pack_start($tree_view, 1, 1, 0);

# Определение колонок для отображения
my $col1 = Gtk3::TreeViewColumn->new_with_attributes('Device Address', Gtk3::CellRendererText->new, text => 0);
my $col2 = Gtk3::TreeViewColumn->new_with_attributes('Name', Gtk3::CellRendererText->new, text => 1);
my $col3 = Gtk3::TreeViewColumn->new_with_attributes('Status', Gtk3::CellRendererText->new, text => 2);

$tree_view->append_column($col1);
$tree_view->append_column($col2);
$tree_view->append_column($col3);

# Создание кнопок для подключения/удаления
my $hbox = Gtk3::Box->new('horizontal', 5);
$vbox->pack_start($hbox, 0, 0, 5);

my $button_box = Gtk3::ButtonBox->new('horizontal');
$hbox->pack_start($button_box, 0, 0, 5);

my $btn_connect = Gtk3::Button->new_with_label("Connect");
$button_box->add($btn_connect);
my $btn_remove = Gtk3::Button->new_with_label("Remove");
$button_box->add($btn_remove);

my $btn_stop = Gtk3::Button->new_with_label("Stop");
$button_box->add($btn_stop);

$|=1;

my $red_button = Gtk3::CssProvider->new;
$red_button->load_from_data(<new;
$green_button->load_from_data(<add(3000, sub {
    if($askmode eq 'list'){
      print $control "devices\n";
    }
    elsif($askmode eq 'list_connected'){
      print $control "devices Connected\n";
    }
    return 1;
});

Glib::IO->add_watch(fileno($monitor), ['in'], sub {
  my ($fh, $a) = @_;
  my $line = <$monitor>;
  if ($line) {
    chomp $line;
    print "$line\n";

    if($askmode eq 'list' && $line =~ /^Device ([\w:]+) (.+)$/){
      my $address = $1;
      my $name = $2;
      if(! $devices{ $address }){
        $devices{ $address } = { name => $name };
        my $iter = $list_store->append;
        $devices{ $address }->{iter} = $iter;
        $list_store->set($iter, 0 => $address, 1 => $name);
      }
    }
    elsif($askmode eq 'connect'){
      if($line =~ /Connection successful/m){
        $askmode = 'connected';
        my $style_context = $btn_connect->get_style_context;
        $style_context->add_provider($green_button, -10);
    
        my $sel = $tree_view->get_selection;
        my ($model, $iter) = $sel->get_selected;
        $model->set($iter, 2 => "OK");
        my $device_address = $model->get($iter, 0);
        print $control "trust $device_address\n";

      }
      elsif($line =~ /Failed to connect/m){
        $askmode = 'failed';
        my $style_context = $btn_connect->get_style_context;
        $style_context->add_provider($red_button, -10); 

        my $sel = $tree_view->get_selection;
        my ($model, $iter) = $sel->get_selected;
        $model->set($iter, 2 => "Error");
      }
    }
  }
  return 1;
});

# Обработка нажатия кнопок

$btn_connect->signal_connect('clicked', sub {
    my $style_context = $btn_connect->get_style_context;
    $style_context->remove_provider($red_button);
    $style_context->remove_provider($green_button);
    my $sel = $tree_view->get_selection;
    if($sel){
      my ($model, $iter) = $sel->get_selected;
      my $device_address = $model->get($iter, 0);
      $askmode = 'connect';
      print $control "connect $device_address\n";
    }
});

$btn_remove->signal_connect('clicked', sub {
    my $sel = $tree_view->get_selection;
    if($sel){
      my ($model, $iter) = $sel->get_selected;
      my $device_address = $model->get($iter, 0);
      $askmode = 'disconnect';
      print $control "disconnect $device_address\n";
      print $control "remove $device_address\n";
      $list_store->remove($iter);
      delete $devices{ $device_address };
    }
});

$btn_stop->signal_connect('clicked',sub {
  if($scan){
    print $control "scan off\n" ;
    $askmode = '';
    $scan = 0;
    $btn_stop->set_label("Scan");
  }
  else{
    print $control "scan on\n" ;
    $askmode = 'list';
    $scan = 1;
    $btn_stop->set_label("Stop");
  }
});


$window->show_all;

Gtk3->main;

Просто создается окошко с кнопками, и через программу bluetoothctl управляем подключением.
Если запускать из консоли — еще и видны ответы bluetoothctl, что было удобно для отладки.

ea3f99c491935927ea8452b19fcb827d.png

По этой схеме прекрасно подключаются наушники нескольких видов, колонки, муз.центр — и всё само переподключается при необходимости.

© Habrahabr.ru