Bluetooth Speakers/Headphones
OpenWrt is a Linux distribution which is geared towards networking on routers. However, some of the travel routers which can run OpenWrt, are ideal portable music servers which can be connected to Bluetooth headphones and speakers which can play and control music from an OpenWrt Router. The HooToo TM03 is one of the travel routers which is an ideal portable music player.
- It fits is the palm of your hand
- It has a USB A port which you can use to connect a USB Bluetooth dongle
- It has a MicroSD slot which allows for continuous operation with an overlay file system.
- It contains a 3000mAh battery for easy portable operation.
This has been tested with OpenWrt 18.06.4 running on a HooToo TM03 and RAVPower RP-WD02 with a MicroSD card overlay file systems with a Plugable USB Bluetooth dongle. The microSD card also contains a 128MB swap partition.
Notes
- OpenWrt 18.06.1 Bluetooth does not work correctly
- Do not use OpenWrt 19.xx.xx with the HooToo TM03 or RAVPower RP-WD02 since the kernel has been compiled to no longer support overlay file systems with these devices.
- Although you can connect multiple Bluetooth devices simultaneously, the HooToo TM03 or RAVPower RP-WD02 do not have enough horsepower to drive Bluetooth speakers/headphones and tethering simultaneously.
Required OpenWrt packages:
Your OpenWrt router configured with an overlay file system.
- kmod-input-uinput
- bluez-daemon
- bluez-utils
- dbus
- dbus-utils
- pulseaudio-daemon-avahi
- pulseaudio-profiles
- pulseaudio-tools
- mpd-full
- mpc
- madplay
- inotifywait
- gawk
- grep
- kmod-6lowpan
- kmod-bluetooth_6lowpan
- wget
This should result in the installation of other dependencies including the USB packages.
Notes
- Since there is no OpenWrt bluezalsa package, we will use pulseaudio.
- Since the
mpd-mini
package does not supportpipe
or pulseaudio, we are forced to install thempd-full
package which does not support pulseaudio either, but does supportpipe
which we can use with the pulsaudiopacat
utility.
Modify Default Configuration files
/etc/bluetooth/main.conf
In /etc/bluetooth/main.conf
,
change the last line to
AutoEnable=true
/etc/pulse/system.pa
Modify /etc/pulse/system.pa
to look as below.
#!/usr/bin/pulseaudio -nF # # This file is part of PulseAudio. load-module module-stream-restore load-module module-device-restore load-module module-card-restore load-module module-bluez5-discover load-module module-bluetooth-policy load-module module-switch-on-port-available load-module module-rescue-streams load-module module-always-sink load-module module-suspend-on-idle load-module module-native-protocol-unix auth-group=pulse-access
/etc/dbus-1/system.d/bluetooth.conf
In /etc/dbus-1/system.d/bluetooth.conf
, add
<allow send_type="method_call"/> <allow send_type="method_return"/>
to the root
policy block. The root
policy block should now look like
<policy user="root"> <allow own="org.bluez"/> <allow send_destination="org.bluez"/> <allow send_interface="org.bluez.Agent1"/> <allow send_interface="org.bluez.MediaEndpoint1"/> <allow send_interface="org.bluez.MediaPlayer1"/> <allow send_interface="org.bluez.Profile1"/> <allow send_interface="org.bluez.GattCharacteristic1"/> <allow send_interface="org.bluez.GattDescriptor1"/> <allow send_interface="org.bluez.LEAdvertisement1"/> <allow send_interface="org.freedesktop.DBus.ObjectManager"/> <allow send_interface="org.freedesktop.DBus.Properties"/> <allow send_type="method_call"/> <allow send_type="method_return"/> </policy>
/etc/dbus-1/system.d/pulseaudio-system.conf
Modify /etc/dbus-1/system.d/pulseaudio-system.conf
to look as below
<?xml version="1.0"?><!--*-nxml-*--> <!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> <!-- This file is part of PulseAudio. PulseAudio is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. PulseAudio is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. --> <busconfig> <!-- System-wide PulseAudio runs as 'pulse' user. This fragment is not necessary for user PulseAudio instances. --> <policy user="pulse"> <allow own="org.pulseaudio.Server"/> <allow send_type="method_call"/> <allow send_type="method_return"/> </policy> </busconfig>
/etc/passwd
Make sure that /etc/passwd
has the following entry.
pulse:x:51:51:pulse:/var/run/pulse:/bin/false
/etc/group
Make sure that /etc/group
has the following entries.
pulse:x:51:pulse:root pulse-rt:x:53: pulse-access:x:54:root
/etc/init.d/pulseaudio
Modify /etc/init.d/pulseaudio
to look as below.
#!/bin/sh /etc/rc.common # Copyright (C) 2011 OpenWrt.org START=99 STOP=65 USE_PROCD=1 PROG=/usr/bin/pulseaudio start_service() { [ -d /var/run/pulse ] || { mkdir -m 0755 -p /var/run/pulse chmod 0750 /var/run/pulse chown pulse:pulse /var/run/pulse } [ -d /var/lib/pulse ] || { mkdir -m 0755 -p /var/lib/pulse chmod 0750 /var/lib/pulse chown pulse:pulse /var/lib/pulse } procd_open_instance procd_set_param command $PROG --system --disallow-exit --disable-shm --exit-idle-time=-1 --realtime=false procd_close_instance }
Pair, Trust and Connect Bluetooth Speaker/Headphones
While logged into your OpenWrt device, type in the command bluetoothctl
.
Make sure that your Bluetooth speaker/headphones is in pairing mode and then issue the command scan on
.
Wait for your speaker/headphones Bluetooth MAC address to appear and then type pair XX:XX:XX:XX:XX:XX
where XX represents the Bluetooth MAC address of your speaker/headphones. For example, for my VERSE headphones, I type pair 1C:A0:D3:D2:AE:75
and accept any pins.
When your speaker/headphones has paired successfully, type connect XX:XX:XX:XX:XX:XX
where XX represents the Bluetooth MAC address of your speaker/headphones. For example, for my VERSE Headphones, I type connect 1C:A0:D3:D2:AE:75
If you have connected to your speaker/headphones successfully, you should see the bluetoothctl
prompt change from [bluetooth]#
to the name of your connected device. For example, for my VERSE headphones, the bluetoothctl
prompt changes to [VERSE]#
.
If your speaker/headphones has connected successfully, type trust XX:XX:XX:XX:XX:XX
where XX represents the Bluetooth MAC address of your speaker/headphones. For example, for my VERSE Headphones, I type trust 1C:A0:D3:D2:AE:75
. Trusting your Bluetooth speaker/headphones will allow them to auto connect in the future.
Testing and Debugging the Bluetooth Connection
If your OpenWrt router has Internet access and you live in a country that does not block Google, issue the following command while logged into your OpenWrt router.
wget --quiet -O - -U "stream-mp3/mpg123/0.59r" 'http://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&q=Joy%20to%20the%20world&tl=en' | madplay -Q -o wave:- - | paplay
If you do not hear Joy to the world on your connected Bluetooth speaker/headphones, try rebooting your OpenWrt router and running bluetoothd
and pulseaudio
manually in debug mode and look for any error messages.
If you are still having problems, connect your Bluetooth dongle to a Debian machine and follow the steps in the previous section to pair. connect and trust your Bluetooth speaker/headphones. If you can connect successfully, copy everything in your Debian /var/lib/bluetooth
directory to your OpenWrt /etc/bluetooth/keys
directory. Reconnect your Bluetooth dongle to your OpenWrt router and reboot. Your Bluetooth speaker/headphones should now connect automatically to your OpenWrt router.
If your have your Bluetooth speaker/headphone paired with multiple devices, auto-connection may not always occur. Log into your OpenWrt router and issue the command
bluetoothctl connect XX:XX:XX:XX:XX:XX
where XX:XX:XX:XX:XX:XX is the MAC address of your Bluetooth speaker/headphones.
MPD Configuration
We will be using mpd to play our music through our Bluetooth headphones/speakers.
- Place all your music in your OpenWrt router
/data/share/mp3
directory - Place your playlists in your
/data/share/mp3/playlists
directory
Sample playlist file which contains music files.
Moby.m3u
/data/share/mp3/moby/everything_is_wrong/Moby - Everything Is Wrong - 13 - When It's Cold I'd Like To Die.flac /data/share/mp3/moby/i_like_to_score/01_novio.mp3 /data/share/mp3/moby/play/03 Radio 4.mp3 /data/share/mp3/moby/play/04 Toss The Feathers.mp3
Sample playlist file which contains Radio streams.
Radio.m3u
https://kexp-mp3-128.streamguys1.com/kexp128.mp3 https://onair.wfuv.org/onair-hi http://listen.noagendastream.com/noagenda http://icecast-ruvr.cdnvideo.ru/rian.voiceusa http://stream.wqxr.org/wqxr
- Modify your
/etc/mpd.conf
file to look like the below.
music_directory "/data/share/mp3" playlist_directory "/data/share/mp3/playlists" db_file "/data/mpd/tag_cache" pid_file "/data/mpd/pid" state_file "/data/mpd/state" sticker_file "/data/mpd/sticker.sql" audio_output { type "pipe" name "mypipe" command "/usr/bin/pacat --rate=44100 --format=s16le --channels=2 2>/dev/null" format "44100:16:2" }
Note that you must manually create any directories that do not exist.
- Reboot your OpenWrt router and then issue the command
mpc update
Once the update has completed, you can now create and load playlists and play music using mpc
Controlling Music Playback with your Headphones/Speaker
If your headphones/speaker supports the Bluetooth AVRCP profile, you can use the Play/Pause/Next/Previous buttons on your speaker/headphones to control music playback. In Linux, these buttons can be read just like any other keyboard event.
Set up cross-compiling environment
Follow the instructions here to set up a cross-compiling environment. Be sure to pull the environment that matches the OpenWrt version on your router.
My cross-compile environmental variables which need to be set before I cross-compile are as follows
export STAGING_DIR=/home/user/openwrt/staging_dir export TOOLCHAIN_DIR=$STAGING_DIR/toolchain-mipsel_24kc_gcc-7.3.0_musl export LDCFLAGS=$TOOLCHAIN_DIR/usr/lib export LD_LIBRARY_PATH=$TOOLCHAIN_DIR/usr/lib export PATH=$TOOLCHAIN_DIR/bin:$PATH
Compile buttonlogger.c
Anywhere on your cross-compile computer, create a file buttonlogger.c
with the following contents.
#include <fcntl.h> #include <linux/input.h> #include <unistd.h> #include <signal.h> #include <stdlib.h> #include <stdio.h> int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "Need /dev/input/eventx arguement\n"); return 1; } int device = open(argv[1], O_RDONLY); struct input_event ev; read(device,&ev, sizeof(ev)); if(ev.type == 1 && ev.value == 1)buttonlogger /dev/input/event0{ /* printf("Key: %i State: %i\n",ev.code,ev.value); */ switch(ev.code) { case KEY_PLAYCD : printf("PLAY\n"); break; case KEY_PAUSECD : printf("PAUSE\n"); break; case KEY_NEXTSONG : printf("NEXT\n"); break; case KEY_PREVIOUSSONG : printf("PREVIOUS\n"); break; } } else if(ev.value == 300 && ev.code == 0) printf("START\n"); }
Now compile buttonlogger.c
with your cross C compiler. For my system, the command is
mipsel-openwrt-linux-gcc -o buttonlogger buttonlogger.c
This will create an executable called buttonlogger
which you can copy to your OpenWrt router $PATH
.
Test buttonlogger
Press your Bluetooth speaker/headphones Play button while connected to your OpenWrt router. Then issue the following command.
buttonlogger /dev/input/event0
while logged into your OpenWrt router.
Press your Bluetooth speaker/headphones Play button again. butoonlogger
should return PLAY.
Your can re-issue the command
buttonlogger /dev/input/event0
and test all your other Bluetooth speaker/headphone buttons
Notes
/dev/input/event0
is dynamically created when your Bluetooth AVRCP profile speaker/headphones are connected to your OpenWrt router. For some speaker/headphones, you need to press the Play button before/dev/input/event0
is created.- The NEXT and PREVIOUS buttons will only work while audio is playing through your Bluetooth speaker/headphones.
- Similarly, the PAUSE button may only work while music is playing.
- Some Bluetooth speakers/headphones only output PLAY and never PAUSE. For these Bluetooth headphones/speakers, your controlling script will need to determine if music is currently playing so it knows whether to issue an
mpc play
ormpc pause
command.
Music Playback Controlling Script
I execute the following script on my OpenWrt router which allows me to use my Bluetooth speaker/headphones playback buttons to control music track playback.
button-monitor.sh
#!/bin/bash # Bluetooth speaker/headphone button monitor urlencode() { # urlencode <string> old_lc_collate=$LC_COLLATE LC_COLLATE=C local length="${#1}" for (( i = 0; i < length; i++ )); do local c="${1:$i:1}" case $c in [a-zA-Z0-9.~_-]) printf '%s' "$c" ;; *) printf '%%%02X' "'$c" ;; esac done LC_COLLATE=$old_lc_collate } google_tts() { # google_tts <string> wget --quiet -O - -U "stream-mp3/mpg123/0.59r" \ 'http://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&q='$(urlencode "$@")'&tl=en' | madplay -Q -o wave:- - } # Select Text To Speech Engine TTS_ENGINE="google_tts" while true do # If result is empty, bluetooth device is not connected. Wait for new # input device and try again while [ ! -e /dev/input/event0 ] do if [ ! -d /dev/input ] then inotifywait -qqe create /dev else inotifywait -qqe create /dev/input fi done # Parse the output of the Bluetooth speaker/headphone buttonlogger # program and output voice and control mpd accordingly. BUTTON=$(/usr/local/bin/buttonlogger /dev/input/event0) case $BUTTON in PLAY) paplay /usr/local/share/voices/play-track.wav mpc -q play ;; START) paplay /usr/local/share/voices/play-track.wav mpc -q play ;; PAUSE) CURRENT_SONG="$(mpc current | sed -e 's|["'\'']||g')" mpc -q pause paplay /usr/local/share/voices/pause-track.wav if [ $(grep -c wlan0-1 /proc/net/wireless) -eq 1 ] then $TTS_ENGINE "$CURRENT_SONG" | paplay fi ;; NEXT) mpc -q pause paplay /usr/local/share/voices/next-track.wav mpc -q next ;; PREVIOUS) mpc -q pause paplay /usr/local/share/voices/previous-track.wav mpc -q prev ;; esac done
Notes
- The above script requires the
bash
shell. - Pre-recorded
.wav
files are in my OpenWrt router/usr/local/share/voices/
directory. The script plays the appropriate voice file when a Bluetooth speaker/headphone button is pressed. - When my OpenWrt has Internet access (is in AP+STA mode) and the speaker/headphones Pause button is pressed, the script uses the
mpc current
command to retrieve the current track artist and song title, urlencode this information, send the text to Google for text to speech conversion and then play the result on my Bluetooth speaker/headphones.
Controlling MPD via a Web Browser
Rather than ssh into your OpenWrt router and control mpd via mpc, we will control the OpenWrt mpd daemon via an HTML 5 compliant Web browser. Note that the latest versions of Firefox and Safari are HTML 5 compliant. ympd is extremely lightweight and fast since it is written in C, uses web-sockets and HTML 5 putting all the CPU load on the client Web browser. ympd even includes an extremely lightweight web server in its executable. With ympd, you will be able to use your Smart Phone, Tablet or PC web browser to search for music, create playlists, start/stop/next/previous track and seek to different times within a track while your Smart Phone is connected to your OpenWrt AP. Neither your Smart Phone/Tablet/PC or your OpenWrt router needs to have Internet Access for this to work. I assume your already set up a successful cross-compiler environment as is described in the previous section.
Build libmpdclient
Download and extract the source code for libmpdclient which can be found here. Install both ninja and meson on your Linux cross-compiler computer. For Debian based Linux, the commands are
sudo apt install ninja-build sudo apt install meson
In the build/cross
sub-directory create a file called openwrt.txt
. My openwrt.txt
file is as follows
[binaries] c = '/home/user/openwrt/staging_dir/toolchain-mipsel_24kc_gcc-7.3.0_musl/bin/mipsel-openwrt-linux-gcc' cpp = '/home/user/openwrt/staging_dir/toolchain-mipsel_24kc_gcc-7.3.0_musl/bin/mipsel-openwrt-linux-g++' ar = '/home/user/openwrt/staging_dir/toolchain-mipsel_24kc_gcc-7.3.0_musl/bin/mipsel-openwrt-linux-ar' strip = '/home/user/openwrt/staging_dir/toolchain-mipsel_24kc_gcc-7.3.0_musl/bin/mipsel-openwrt-linux-musl-strip' pkgconfig = '/home/user/openwrt/Programs/libmpdclient-2.19/build/openwrt/pkg-config.sh' [properties] root = '/home/user/openwrt/staging_dir/toolchain-mipsel_24kc_gcc-7.3.0_musl' [host_machine] system = 'linux' cpu_family = 'mips' cpu = 'mips' endian = 'big'
Modify the above for your environment as per here. Then issue the following commands.
meson . output --cross-file build/cross/openwrt.txt ninja -C output ninja -C output install
Verify that the following files were installed correctly into the cross compiling environment.
ls /home/user/openwrt/staging_dir/toolchain-mipsel_24kc_gcc-7.3.0_musl/usr/include/mpd async.h entity.h neighbor.h protocol.h socket.h audio_format.h error.h output.h queue.h song.h capabilities.h fingerprint.h pair.h recv.h stats.h client.h idle.h parser.h replay_gain.h status.h compiler.h list.h partition.h response.h sticker.h connection.h message.h password.h search.h tag.h database.h mixer.h player.h send.h version.h directory.h mount.h playlist.h settings.h version.h.in
ls -l /home/openwrt/staging_dir/toolchain-mipsel_24kc_gcc-7.3.0_musl/usr/lib lrwxrwxrwx 1 user user 20 Oct 16 20:28 libmpdclient.so -> libmpdclient.so.2.19 lrwxrwxrwx 1 user user 20 Oct 16 20:28 libmpdclient.so.2 -> libmpdclient.so.2.19 -rwxrwxr-x 1 user user 391700 Oct 16 20:27 libmpdclient.so.2.19
Note that your build environment cross-compiling paths may differ.
Build ympd
Download and extract the source code for ympd which can be found here. Install cmake on your Linux cross-compiler computer. For Debian based Linux, the command is
sudo apt install cmake
Create a cmake
cross-compiling definition file as per the instructions here. My cmake
cross-compiling definition file, which I named ympd.cross
is below.
set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR mips) set(CMAKE_SYSROOT /home/user/openwrt/staging_dir/toolchain-mipsel_24kc_gcc-7.3.0_musl) set(tools /home/user/openwrt/staging_dir/toolchain-mipsel_24kc_gcc-7.3.0_musl) set(CMAKE_C_COMPILER ${tools}/bin/mipsel-openwrt-linux-gcc) set(CMAKE_CXX_COMPILER ${tools}/bin/mipsel-openwrt-linux-g++) set(include_directories ${tools}/usr/include)
Note that your build environment cross-compiling paths may differ.
Execute the following commands in the ympd
diectory.
cmake -DCMAKE_TOOLCHAIN_FILE=/home/user/openwrt/ympd/ympd.cross -DWITH_SSL=no make
Copy ympd
to somewhere in your OpenWrt router $PATH
.
Automatic Start
Start up everything automatically at power up by adding button-monitor.sh
and ympd
to your OpenWrt router /etc/rc.local
file. My /etc/rc.local
file contents are as follows.
# Put your custom commands here that should be executed once # the system init finished. By default this file does nothing. /usr/local/bin/ympd -w 80 & /usr/local/bin/button-monitor.sh & exit 0
When I power up my OpenWrt router, my Bluetooth speaker/headphones automatically connects and I can start playing music my simply pressing my Bluetooth speaker's/headphones' play button. If I want to change the playlist, I simply connect my Smart Phone or tablet to my OpenWrt AP and browse to LuCI.
Since I don't use LUCI and want to free up port 80 for ympd
, I have disabled uhttpd
by issuing the following command.
/etc/init.d/uhttpd disable
TODO
Since OpenWrt does not include the ofono package, we cannot get audio from the Bluetooth speaker/headphones microphone. If OpenWrt did include the ofono and the Darkice package, we could take audio from the the Bluetooth speaker/headphones microphone, send it to an Icecast server and listen or capture it as an audio stream.