Thomas shares makes

2021-10-05

Snapcast Bluetooth speaker on Debian Sid

photo of the bluetooth speaker and dongle

I run two audio players on a Raspberry Pi, each one drives a set of speakers. To maximize the range of my Bluetooth speaker, I bought a Realtek RTL8761B-based USB Bluetooth dongle with a large antenna.

To get the Bluetooth dongle to work with the latest kernel drivers and bluez-alsa, I had to abandon Raspbian Linux and use the latest Debian Linux instead.

Linux Installation

I flashed a pre-installed Debian 10 image from https://raspi.debian.net/tested-images/ I downloaded the xz-compressed image of the 2021.8.23 build of '11 (Bullseye) Release' that was 'tested with raspberry pi 2B'.

apt install sudo vim screen
adduser pi
/sbin/adduser username sudo
hostnamectl set-hostname living

Next up was to follow the instructions to upgrade to Debian unstable (Sid) : https://dnuka.github.io/upgrading-debian-to-unstable.html

vi /etc/apt/sources.list

apt update
apt-get dist-upgrade
apt autoremove
reboot

Install the audio related tools:

sudo apt install snapclient
sudo apt install bluez bluez-alsa-utils alsa-utils

And the dependencies used by the bash script:

sudo apt install mosquitto-clients
sudo apt install expect

Follow the instructions of https://linuxreviews.org/Realtek_RTL8761B to install the Bluetooth chipset firmware:

su
cd /lib/firmware/
mkdir rtl_bt
cd rtl_bt
wget https://raw.githubusercontent.com/Realtek-OpenSource/android_hardware_realtek/rtk1395/bt/rtkbt/Firmware/BT/rtl8761b_config -O rtl8761b_config.bin
wget https://raw.githubusercontent.com/Realtek-OpenSource/android_hardware_realtek/rtk1395/bt/rtkbt/Firmware/BT/rtl8761b_fw -O rtl8761b_fw.bin
reboot

Bluetooth configuration

Pair the Bluetooth speaker to the Pi:

sudo bluetoothctl
scan
pair <MAC>
connect <MAC>

Add to /etc/asound.conf (and change the device address to your speaker's):

root@debian:~# cat /etc/asound.conf
#pcm.!default "bluealsa"
#ctl.!default "bluealsa"
defaults.bluealsa.service "org.bluealsa"
defaults.bluealsa.device "aa:bb:cc:dd:ee:ff"
defaults.bluealsa.profile "a2dp"
defaults.bluealsa.delay 0

Configure the snapclient arguments to use the right device. With the latest version of bluealsa the mixer is exposed and can be controlled both over the network (via snapclient) or via the physical buttons on the speaker.

root@debian:~# cat /etc/default/snapclient-bluetooth
SNAPCLIENT_OPTS="-h 1.2.3.4 --hostID keuken --mixer hardware:\"JBL Charge 4 - A2DP\" -s bluealsa --latency 0"

Set up a systemd service for it. We won't have it start on startup, but rather have a shell script activate it when needed.

root@debian:~# cat /etc/systemd/system/snapclient-bluetooth.service
[Unit]
Description=Snapcast client
After=network.target sound.target
Wants=avahi-daemon.service

[Service]
EnvironmentFile=-/etc/default/snapclient-bluetooth
Type=simple
ExecStart=/usr/bin/snapclient $SNAPCLIENT_OPTS
Restart=always

[Install]
WantedBy=multi-user.target

bluetoothspeaker2mqtt installation

Install my scripts from https://github.com/thouters/bluetoothspeaker2mqtt - where up to date instructions are available.

#!/bin/bash
# put into  $HOME/.config/mosquitto_pub onto (separate lines, no spaces at the ends!)
# -h hostname
# -u mqtt
# -P mqttpassword
# apt install expect mosquitto-clients # unbuffer, mosquitto_*
set -e
MAC=$(sed -n '/device/s/[^"]*"\([^"]*\)"/\1/p' /etc/asound.conf)
: "${TOPIC_STATE:=bluetoothspeaker2mqtt/myspeaker/state}"
: "${TOPIC_SET:=bluetoothspeaker2mqtt/myspeaker/set}"
: "${SNAPCLIENT_SERVICE:=snapclient-bluetooth}"
: "${STARTSTOP_SNAPCLIENT:=yes}"
: "${BUTTONWATCHER_SERVICE:=bluetoothspeaker-buttonwatcher}"
: "${STARTSTOP_BUTTONWATCHER:=yes}"
: "${POST_CONNECT_SETTLE_TIME:=1}"

function bluetooth_event_listener()
{
  MATCH=0
  ( while true; do sleep 1; done) | unbuffer bluetoothctl | while read -r line
  do
        # A little state machine: scan the output for the device MAC
        echo "<recv> $line"
        if echo "$line" | grep -qE "Device ([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})"
        then
                if echo "$line" | grep -q "Device ${MAC}"
                then
                        echo "Info about the device detected"
                        MATCH=1
                else
                        MATCH=0
                fi
        fi
        if [[ "$MATCH" = 1 ]]
        then
                if echo "$line" |grep -q "Connected: yes"
                then
                        echo Connected!
                        mosquitto_pub -t "${TOPIC_STATE}" -m on
                        sleep "$POST_CONNECT_SETTLE_TIME"
                        [[ "$STARTSTOP_SNAPCLIENT" = yes ]] && systemctl start "${SNAPCLIENT_SERVICE}"
                        [[ "$STARTSTOP_BUTTONWATCHER" = yes ]] && systemctl start "${BUTTONWATCHER_SERVICE}"
                fi
                if echo "$line" |grep -q "Connected: no"
                then
                        echo Disconnected!
                        mosquitto_pub -t "${TOPIC_STATE}" -m off
                        [[ "$STARTSTOP_SNAPCLIENT" = yes ]] && systemctl stop "${SNAPCLIENT_SERVICE}"
                        [[ "$STARTSTOP_BUTTONWATCHER" = yes ]] && systemctl stop "${BUTTONWATCHER_SERVICE}"
                fi
        fi
  done
}

function bluetoothctl_cmd()
{
  echo "<send> $1"
  printf "%s\n\n" "$1" | bluetoothctl
}


function mqtt_listener()
{
mosquitto_sub -t "${TOPIC_SET}" -v |while read -r message
do
    topic=$(echo "$message"|cut -d' ' -f 1)
    payload=$(echo "$message"|cut -d' ' -f 2)
    echo "<mqtt recv> topic ${topic}/${payload}"
    case $topic in
      "${TOPIC_SET}")
        case $payload in
        on)
            bluetoothctl_cmd "connect $MAC"
        ;;
        off)
            bluetoothctl_cmd "disconnect $MAC"
        ;;
        *)
            echo "invalid command $payload"
        ;;
        esac

        ;;
      *)
        echo "Not implemented $topic"
        ;;
    esac
done
}
bluetooth_event_listener &
mqtt_listener &
wait

Use the provided systemd service files:

[Unit]
Description=expose bluetoothspeaker on mqtt
Requires=bluealsa.service
After=bluealsa.service

[Service]
Type=simple
EnvironmentFile=-/etc/bluetoothspeaker2mqtt.conf
User=pi
ExecStart=/home/pi/bluetoothspeaker2mqtt.sh
[Install]
WantedBy=bluetooth.target

Home assistant integration

This switch configuration can be used to trigger bluetooth connect or disconnect:

- platform: mqtt
  unique_id: bluetooth_speaker_link
  name: "Bluetooth link"
  state_topic: "box/state"
  command_topic: "box/set"
  payload_on: "on"
  payload_off: "off"
  state_on: "on"
  state_off: "off"
  optimistic: false
  qos: 0
  retain: false
  icon: mdi:bluetooth-audio

And this binary_sensor can be used to view the state of the connection, and trigger automations based on it:

- platform: mqtt
  name: "box"
  state_topic: "box/status"
  payload_on: "on"
  payload_off: "off"

Running a second snapclient on the line out

I use the default snapclient initscripts to run a snapclient to a wired set of speakers.

Unfortunately I hit a bug that breaks the line out of the Pi 2: https://github.com/raspberrypi/linux/issues/4108 I worked around this by switching to a USB soundcard dongle that I already had.

root@debian:~# cat /etc/default/snapclient
SNAPCLIENT_OPTS="-h 1.2.3.4 --hostID living --mixer hardware:Speaker -s plughw:CARD=Device,DEV=0"