bluetoothctl で自動接続 (常時接続) をする方法

8 min read

はじめに

bluetoothctl を使って、Linux マシンにペアリングした Bluetooth デバイスを自動接続・常時接続する方法を紹介します。

なお、自動接続・常時接続の際に bluetoothctl コマンドを使用するという意味なので、Bluetooth デバイスのペアリング自体は GUI で行っても問題ありません。

忙しい人向け

$ git clone https://github.com/noraworld/bluetoothctl-autoconnector.git
$ cd bluetoothctl-autoconnector
$ ./setup.sh

モチベーション

我が家では、Raspberry Pi に Bluetooth アダプタをたくさん接続して、各々のアダプタに iPhone や MacBook などをペアリングして、iPhone や MacBook で再生した曲をミキシングして同時に聞けるようにしています。

iPhone や MacBook から流れる音を同時に聞けるようにするメリットはあまりないのですが、複数のデバイス間で Bluetooth ヘッドフォンのペアリングを切り替えるのがめんどくさいので、Raspberry Pi を Bluetooth 接続の中継機代わりに使っています。

ただ、Bluetooth が切断されてしまうと、自動で接続してくれません。たとえば外出する際に iPhone を外に持っていけば当然接続は切れますし、MacBook の場合はスリープモードに入ると接続が切れます。iPhone や MacBook 側から Raspberry Pi に Bluetooth 接続することはできますが、毎回手動で接続するのは面倒です。

そこで、一度登録した (ペアリングした) デバイスとの接続が切れていたら再接続する、というのを自動でやってくれたら良いなと思いました。

やり方

「接続が切れたら自動的に再接続する」といった機能は bluetoothctl にはないので、ペアリングしたすべてのデバイスに接続するスクリプトを作って、そのスクリプトを crontab で 1 分ごとに実行することで実現します。

スクリプト

以下は、ペアリングしたすべてのデバイスに接続するスクリプトです。

autoconnector.sh
#!/bin/bash

function paired_devices() {
  {
    printf "select $adapter\n\n"
    printf "paired-devices\n\n"
  } | bluetoothctl | grep "Device " | sed -r 's/^.*(([0-9A-F]{2}:){5}[0-9A-F]{2}).*$/\1/'
}

function is_connected() {
  {
    printf "select $adapter\n\n"
    printf "info $device\n\n"
  } | bluetoothctl | grep "Connected: " | sed -e 's/Connected: //' | sed -e 's/^[[:blank:]]*//'
}

bluetoothctl -- list | while read line
do
  adapter=`echo $line | sed -r 's/^.*(([0-9A-F]{2}:){5}[0-9A-F]{2}).*$/\1/'`

  paired_devices | while read device
  do
    if [[ $(is_connected) = "no" ]]; then
      {
        printf "select $adapter\n\n"
        printf "connect $device\n\n"
      } | bluetoothctl
    fi
  done
done

:warning: 上記スクリプトは、今後、改良する可能性があります。最新版をご覧になりたい方は こちら をご確認ください。

上記スクリプトを 3 つに分けて説明します。

(**main)

bluetoothctl -- list | while read line
do
  adapter=`echo $line | sed -r 's/^.*(([0-9A-F]{2}:){5}[0-9A-F]{2}).*$/\1/'`

  paired_devices | while read device
  do
    if [[ $(is_connected) = "no" ]]; then
      {
        printf "select $adapter\n\n"
        printf "connect $device\n\n"
      } | bluetoothctl
    fi
  done
done

bluetoothctl -- list コマンドで Bluetooth コントローラ (アダプタ) 一覧が表示されます。hciconfig コマンドで表示される Bluetooth コントローラ一覧と同じ BD アドレスのものが表示されるはずです。

複数の Bluetooth コントローラを接続している場合は複数表示されるので、ループを回してそれぞれのコントローラに対して処理します。

変数 adapter には Bluetooth コントローラの BD アドレスが入ります。

後述する paired_devices() 関数で、その Bluetooth コントローラにペアリングされているデバイスの BD アドレス一覧が取得できます。一つの Bluetooth コントローラに複数のデバイスがペアリングされている場合は複数取得するので、Bluetooth コントローラ同様にループを回してそれぞれのデバイスに対して処理します。

後述する is_connected() 関数は yes または no を返します。そのデバイスが接続されている場合は yes、切断されている場合は no を返します。返り値が no だった場合は切断されているので、Bluetooth コントローラに対してデバイスを接続するよう試みます。

paired_devices()

function paired_devices() {
  {
    printf "select $adapter\n\n"
    printf "paired-devices\n\n"
  } | bluetoothctl | grep "Device " | sed -r 's/^.*(([0-9A-F]{2}:){5}[0-9A-F]{2}).*$/\1/'
}

一つの Bluetooth コントローラにペアリングされている複数のデバイスの BD アドレスを返します。

bluetoothctl コマンド内で、select <BD_ADDR> (<BD_ADDR> は Bluetooth コントローラの BD アドレス) とすると、Bluetooth コントローラを指定できます。

Bluetooth コントローラが一つしかない場合はわざわざ select <BD_ADDR> とする必要はないのですが、複数の Bluetooth コントローラがある場合 (たとえば複数の Bluetooth アダプタを接続している場合) は、どのコントローラに対して Bluetooth の操作を行うのかを示す必要があります。

そして bluetoothctl コマンド内で paired-devices とすると、選択した Bluetooth コントローラにペアリングされているデバイス一覧が表示されます。複数ペアリングされている場合は複数表示されます。

今回ほしいのは BD アドレスだけなので grep や sed で BD アドレスのみを抽出しています。

is_connected()

function is_connected() {
  {
    printf "select $adapter\n\n"
    printf "info $device\n\n"
  } | bluetoothctl | grep "Connected: " | sed -e 's/Connected: //' | sed -e 's/^[[:blank:]]*//'
}

デバイスが接続されているかどうかを調べます。接続されていたら yes を返し、切断されていれば no を返します。

先ほどと同じように select <BD_ADDR> で Bluetooth コントローラを選択します。次に info <BD_ADDR> (この BD_ADDR は Bluetooth コントローラーではなくデバイスの BD アドレス) で、そのデバイスの接続情報などが表示されます。

Connected: yes という行があればそのデバイスは接続されており、Connected: no という行があればそのデバイスは切断されています。

yes または no の部分を grep や sed で抽出しています。

crontab への登録

前項のスクリプトを実行すると、ペアリングしているすべてのデバイスへ接続します。このスクリプトを crontab で 1 分ごとに実行するようにします。

cron.conf というファイルを作ります。

*/1 * * * * /path/to/autoconnector.sh

/path/to/autoconnector.sh には前項のスクリプトの絶対パスを指定します。crontab は、通常、ホームディレクトリから実行されるのでホームディレクトリからの相対パスでも良いです。

ファイルを保存したら、以下のコマンドを実行します。

:warning: すでに他の crontab のジョブが登録されている場合は消えてしまうので注意してください。

$ crontab cron.conf

これで 1 分ごとに前項のスクリプトを実行するようになりました。

動作確認

実際に MacBook で試してみると、スリープから復帰したあと、ちゃんと 1 分以内に Raspberry Pi に自動接続されるようになりました。

(optional) 音声の再生中は自動接続しないようにする (音切れ問題対策)

:information_source: 「モチベーション」の項で説明したように、Raspberry Pi に複数デバイスを接続して音声をミキシングして使う用途の場合は、次に紹介するスクリプトが役に立つかもしれません。

さて、自動接続はできるようになったのですが、少し問題が発生しました。Raspberry Pi にすでに接続されたデバイスで音声が再生されている間に、他のデバイスを接続しようとすると、再生中の音声が一時的に乱れてしまいます。プツプツする感じです。

たとえば、iPhone を Raspberry Pi に Bluetooth 接続している状態で、iPhone から音楽を流しているとします。その状態で、接続されていない MacBook に接続しようとすると、その間、iPhone から流れる音声が乱れます。

その接続要求で MacBook が接続されれば、それ以降は接続要求を行わないので音声が途切れることはありません。しかし、たとえば MacBook がスリープ状態で、Bluetooth 接続に応じない場合は、1 分ごとに接続要求をして失敗、を繰り返します。そうなると、iPhone から流れる音声が 1 分おきに乱れることになります。

なので、Raspberry Pi に Bluetooth 接続されているデバイスのうち、少なくとも一つが音声を再生している場合は自動接続を行わないようにすることでこの問題を解決します。接続されていないデバイスがあっても、すでに接続されているデバイスで音声が再生されていると、その間は自動接続されなくなってしまいますが、そこは妥協することにします。再生中の音楽がプツプツするほうが気になるので……。

これを実現するには先ほどのスクリプトに数行追加します。

  #!/bin/bash

  function paired_devices() {
    {
      printf "select $adapter\n\n"
      printf "paired-devices\n\n"
    } | bluetoothctl | grep "Device " | sed -r 's/^.*(([0-9A-F]{2}:){5}[0-9A-F]{2}).*$/\1/'
  }

  function is_connected() {
    {
      printf "select $adapter\n\n"
      printf "info $device\n\n"
    } | bluetoothctl | grep "Connected: " | sed -e 's/Connected: //' | sed -e 's/^[[:blank:]]*//'
  }
+
+ function is_playing() {
+   export PULSE_RUNTIME_PATH="/run/user/$(id -u)/pulse/"
+   pacmd list-sink-inputs | grep -c "state: RUNNING"
+ }
+
+ if [[ $(is_playing) -gt 0 ]]; then
+   echo -e "Error: Some devices now playing sounds" >&2
+   exit 2
+ fi

  bluetoothctl -- list | while read line
  do
    adapter=`echo $line | sed -r 's/^.*(([0-9A-F]{2}:){5}[0-9A-F]{2}).*$/\1/'`

    paired_devices | while read device
    do
      if [[ $(is_connected) = "no" ]]; then
        {
          printf "select $adapter\n\n"
          printf "connect $device\n\n"
        } | bluetoothctl
      fi
    done
  done

解説

is_playing() 関数と、is_playing() 関数の結果が 0 より大きかったら exit する処理を追加しました。

is_playing() 関数は 0 以上の整数を返します。そしてこの数値は音声を再生中の入力装置の数を表しています。0 であれば音声を再生中の入力装置がないということになるのでそのまま処理を続行します (自動接続します) が、そうでなければ音声を再生しているデバイスが少なくとも一つは存在することになるので exit して処理を中断します。

pacmd コマンドの代わりに pactl コマンドを使用しても良いです。つまり、pacmd list-sink-inputs | grep -c "state: RUNNING" の部分は、以下に置き換えても問題ありません。

(任意)
- pacmd list-sink-inputs | grep -c "state: RUNNING"
+ pactl list sink-inputs | grep -c -E "Sink Input #[0-9]{1,}"

いずれも音声を再生中の入力装置の情報を取得し、特徴的な文字列を grep してヒットした数を出力します。

export PULSE_RUNTIME_PATH="/run/user/$(id -u)/pulse/" という行がないと、crontab で実行した際に pacmd コマンド (pactl コマンド) が PulseAudio の情報を取得できず失敗します。crontab ではなくカレントシェルで実行する際は、この行がなくても動作してしまうのでハマるポイントです。

pacmd - Why doesn't it work from cron?

参考サイト