A few weeks ago I was inspired to get a Bluetooth music player for use in the car on road trips. While any old Android device would have sufficed, I thought it would be more interesting to build something out of a $15 Raspberry Pi Zero 2 W. Since I'd only have to write the software, it couldn't be that hard, right? I even broke down and decided to code it in Python rather than C, which should make it even easier. I was wrong, and it was a royal pain, but I'm rather happy about the way it turned out.
The first difficulty was acquiring a Raspberry Pi. There's a worldwide shortage, with pretty much every supplier sold out with the next shipment expected months ahead. A few price-gouging suppliers were charging over $200 for those people with more money than patience. Finally one of the suppliers sent an email reporting that they had 1500 of them in stock. I made the mistake of waiting a couple hours to buy one. They were all gone. But eventually I got my hands on a Pi Zero.
Sporting a 1 Ghz processor, 512 MB of RAM, one mini USB port, a mini HDMI port, Wifi, and Bluetooth, the Raspberry Pi Zero 2 W is a pretty cool machine for $15. To think that our first computer, at 1 Mhz and 640 KB of RAM, cost about $7000 in today's money and weighed about 40 pounds... But it's not quite all it's hyped to be. I set up an SSH server on it and started some preliminary testing. My first (and second and third...) experience trying to connect it to something over Bluetooth was that of the whole system locking up for seconds at a time, dropping my SSH connection, and generally becoming unusable. When I finally got it connected to a Bluetooth audio player, the sound was terrible, with rapid stuttering several times per second. Of course, they didn't mention any of that in the documentation. Not a good experience. That was the start of a long chain of problems that I had to solve one by one.
Here are some of the problems I had to deal with, and their solutions.
Bluetooth & Wifi conflicts
After a lot of frustration and digging around on forums, I found an explanation for why the system seemed so unresponsive when I used Bluetooth. (Actually, I found a lot of explanations, but most of them turned out to be wrong.) The Bluetooth radio and the Wifi radio are packed close together and use the same frequency, causing massive interference with each other. In practice, you can't use them at the same time. The system wasn't freezing up; it's just that my connection to it became terrible. After plugging in a USB keyboard and HDMI monitor, and disabling the Wifi radio, I was finally able to type more than half a character per second!
The Linux Bluetooth tools kinda suck
Now that I could actually use the system, I was able to discover that the Bluetooth utilities kinda suck. Half of them are deprecated replacements for other deprecated tools, many of which no longer work, and the only tools that aren't deprecated are missing necessary features and are unsuitable for non-interactive use. (Even though some have non-interactive modes, they still ask questions on the console of the 'Do you accept this service (Y/N)?' sort.) So apparently I would have to write my own Bluetooth utilities using the bluez DBus interface, but unfortunately the python-dbus library is tied to X-Windows libraries and I was using a pure text-mode system. In any case, there was almost no documentation, and many of the interfaces that looked useful were already deprecated. What a pain. Eventually I just bit the bullet and wrote a program to use the interactive bluetoothctl utility, piping its output into my program and responding on another pipe as though I was typing answers to its questions on the console. Do I want to pair with this device? Yes I do. Is this the right PIN code? Uhh, yeah, sure it is. Since it wasn't designed for non-interactive use I had to do annoying things like strip out all the ANSI color codes and parse somewhat ambiguous text, but in the end I was able to programmatically control the Bluetooth radio.
A show-stopping problem with the audio player was that the Zero couldn't reliably steam audio, as far I could see. I could pair with an audio sink and play audio, but every second it would stutter. I trawled forums for hours and nearly everyone said this was because of the conflict with the Wifi radio, but I'd already disabled that. Right? Well, I used the rfkill tool to turn it off, but maybe that wasn't enough. So I hunted for other ways to really, definitely kill that Wifi radio. Blacklist the driver. Edit the startup config so the kernel won't even see that the device exists. No luck. It still stuttered. It was as if it just couldn't handle the load of decoding an .opus file and streaming it. I tried a simple .wav file and that stuttered too, so it wasn't the decoding. And anyway, the CPU didn't seem all that heavily loaded. It was maddening. Eventually I discovered that it's not capable of reliably scanning for Bluetooth devices and transmitting data at the same time. The trick was to disable scanning: scan, find the device you want, connect, and then stop scanning! And only start scanning again if you get disconnected. Finally, the audio came through nice and clear.
Getting a Hands-Free device profile
The next problem is that I wasn't trying to make a Bluetooth player for a Bluetooth speaker, but for the car, and the car seemed very picky. It said it was only compatible with phones that use the Hands-Free Link protocol. Well, what is that, and how am I going to emulate it? Documentation on it was practically nonexistent, as far as I could see, but I did discover some information saying that the Hands-Free Link stuff is a Bluetooth service that a device can advertise itself as supporting. So how could I make the Pi Zero pretend to be a phone with a Hands-Free Link service? The phone part wasn't too bad. I just took the Bluetooth Class ID that all my Android devices were reporting and stuck that in the bluetoothd config file. Now what about the Hands-Free stuff? All information pointed to the sdptool utility. Like most of the Bluetooth tools, it was already deprecated, and for good measure it was disabled too. I couldn't get it to work. Supposedly you have to run the Bluetooth daemon in a compatibility mode to make it work... except that as far as I could tell, it didn't. No matter what I did, I couldn't get the Zero to advertise itself as a hands-free device, as far as I could see. It was pretty frustrating, and I was set to lose even more hours to this problem when I discovered that the car isn't as picky as it claimed. While it did refuse to pair with the Zero, it eventually popped up a message saying that if the pairing wasn't working, try pushing this button. I pushed it and the car finally advertised itself as a regular Bluetooth audio sink and accepted the pair. Yay. Thanks, car.
The last main feature I was missing was support for multimedia buttons. It's much more convenient to be able to change tracks with the controls on the steering wheel than futzing around with the tiny buttons I have attached to the Raspberry PI. But how could I receive those signals? Of course they were coming in over Bluetooth somehow. I know there's an "Audio/Video Remote Control" Bluetooth service you can implement and advertise to say that you accept those signals, but how to implement it? It seemed I'd have to go back to using DBus. The bluez DBus service has an interface that seems like just the thing! It's the MediaControl1 interface, with methods like Next, Previous, Stop, Play, and so on. Just what I need! Too bad it's deprecated. Nonetheless I tried implementing it. It didn't work, but there's no real documentation on how to implement it anyway. I mean, I can implement it, but how do I make sure it actually gets called? Anyway, you're not supposed to use the MediaControl1 interface anymore. You're supposed to use the much more complicated MediaPlayer1 interface. But it didn't seem to work either. When I pushed the buttons, my interface methods didn't get called. But like I said, there's no good documentation. No real examples. Was I missing something? I spent hours going down that rathole until I discovered the secret. I didn't need DBus after all, because the Bluetooth daemon already converts all media button presses into key presses on a series of virtual keyboards! All I had to do was continually watch for new keyboards (with the right buttons) to get attached to the system and snoop on their key presses with the evdev interface! Finally, the last piece had fallen into place.
About this time, one of the buttons I had attached to the Raspberry Pi got really flaky and would only register one out of every three or so presses. Cheap Chinese crap... oh well. No easy fix for that, I guess. Good thing I can control it with media buttons now.
Finally I ran into the problem that discourages me on every project of this kind: the case. It's no good to just have a circuit board with wires coming off of it. You're sure to short the circuits, bend the pins, and otherwise ruin your hard work. But the case I bought for the Pi Zero 2 didn't fit! To be fair, it was for the original Pi Zero... They're supposed to have the same layout, but they seem to be subtly different. The first problem was the heat sinks I had attached to the Zero. They're probably not necessary, but I didn't want to remove them now. So I cut holes around them. Then the case got caught up on one of the chips, so I cut that part out too. But it was still getting caught up on tiny little resistors, and one side was only held together by a millimeter of plastic now. Any more cutting and it'd be in pieces. So I went back to the oversized throwaway case that came with it, which almost immediately broke when I'd tried using it earlier, and started laboriously putting holes in it with a hand file. After 20 minutes and thousands of tiny pieces of plastic dust flew everywhere, I had a case that worked, mostly. I mean, the circuit board for the screen and buttons is still sticking out the top, but all the important stuff is protected. Phew.
Finally all I had to do was finish writing the player software. I had a four-way joystick and two buttons in a layout like the original NES controller, and a tiny screen measuring 0.9" square, and I had to make a user interface that could conveniently browse and search through thousands of files. Thankfully, I was in my comfort zone now, software, and I think I did a pretty good job.
- A UI capable of efficiently managing thousands of tracks on a 0.9" square display with only a few input buttons
- Browsing the song library and playlist by title or artist, including searching by word or phrase
- Scanning for Bluetooth devices and pairing with them (including devices that use PIN codes)
- Processing multimedia buttons (e.g. Next Track, Pause, etc) from Bluetooth devices
- Shuffling and repeating the playlist
- Volume control
- Rewinding and seeking within a track
- Control of Wifi and Bluetooth radios (in case you want to connect from your computer)
- A system page showing IP address, CPU load & frequency, memory usage, CPU temperature, and system problems (undervoltage, overheating, ...)
- Low-power sleep modes with auto-sleep and auto-wake
To give an example, let's say you have thousands of songs in your library and want to find the song "The Legend of John Henry's Hammer", but you don't quite remember the title. You only remember it was something about a hammer. How can you find it quickly with only a four-way joystick and two buttons? Without a keyboard, the software has to do the work of indexing all the words, grouping common prefixes together, and guiding you towards the most likely selections. Something like this:
Anyway, the source code for the player is available on GitHub.