A WiFi Radio With Four Buttons, Built for Parents in Their Eighties
Part 7 of a series on the tools I actually run.
My parents are in Germany, both past eighty, and the radio situation in their house had quietly degraded. The stations they’d listened to for decades were moving to DAB or to streaming, the old receiver picked up less every year, and the obvious modern answer is a smart speaker, which is the wrong answer twice over. First because they do not want to talk to furniture and should not have to install an app. And second because they’re German, and the idea of a microphone in the living room shipping everything said near it to a company in the United States is not a trade many Germans are willing to make. That instinct runs deep there, it predates the current moment by decades, and recent years have not exactly softened it. An Alexa was never going to happen in that house and I never suggested it.
What they wanted was a radio. Press a thing, sound happens.
So I built one. It’s a $14 USB soundbar with four holes drilled in it and an Orange Pi Zero 2W hiding inside. Press the green button, you get one station. Press the blue one, you get another. There’s no screen, no app, no account, no voice assistant, and nothing to update.
It’s been running for months and it’s the most satisfying thing I’ve made in years.
The enclosure is a cheap speaker
The case is a SOULION USB soundbar, about $14. USB powered, plug and play, physical rotary volume knob, and some RGB lighting modes I could take or leave.
I didn’t pick it for the audio, though it’s perfectly fine for spoken word and background music. I picked it because it’s a long plastic box with room inside, a real volume knob, and it costs less than most project enclosures that come with nothing in them at all.
Four holes drilled in the top face, four coloured arcade-style buttons pushed through, and the Pi sits inside. A strip off a label printer runs along the front under the buttons with the station names on it. From the outside it looks like a speaker that happens to have buttons, which is exactly the impression I wanted. Nobody has to be told it’s a computer.
I also took things away, which turned out to matter as much as what I added. The speaker ships with a Bluetooth mode on a switch, and I disconnected it. Not because it doesn’t work, but because it’s a second way for the device to behave, and a second way is a way to end up somewhere confusing with no idea how you got there. If the switch can’t be flipped, the radio only ever does one thing. The RGB lighting modes went too. A speaker that cycles through colours is a speaker someone is going to ask me about on the phone.
The rule I settled on: every control that isn’t a station button or the volume knob is a support call waiting to happen. There are exactly five things you can touch on this device, and four of them start a radio station.
One USB-C cable goes into the box and powers everything inside it. One cable to the wall, and that’s the whole installation. This mattered more than any technical decision I made, because “there’s a cable, plug it in” is an instruction that survives a transatlantic phone call.
Why an Orange Pi and not a Raspberry Pi
Two reasons went into the decision, and a third has only become more true since.
RAM. The Orange Pi Zero 2W comes in configurations well past what the Raspberry Pi Zero 2W offers, which is stuck at 512MB. It’s not that playing an audio stream needs much memory. It’s that having headroom means I can run Tailscale and a few other things alongside it without thinking hard, and that an apt upgrade over SSH from California doesn’t turn into an adventure.
The antenna. This is the one that actually decided it. The Orange Pi has a proper WiFi antenna, and the radio sits in a room with a wall between it and the router. The Raspberry Pi Zero 2W’s onboard antenna is fine in the same room as an access point and marginal through masonry. German houses have real walls. A radio that drops out is worse than no radio at all, because then I’m on the phone trying to debug WiFi with someone who has no reason to know what a router is.
And you can actually buy one. This wasn’t my reasoning at the time but it’s the first thing I’d say now. The Zero 2W has been out of stock nearly everywhere for months. List price is still $15, or €19.90 in Germany, and the boards simply aren’t there. What is there is third-party kits on Amazon going for something like €130, which is not a price, it’s an opportunity being taken. None of that premium reaches Raspberry Pi. If you can wait, the approved resellers do eventually restock and you can sign up for notifications. If you can’t wait, the Orange Pi Zero 2W is sitting in stock at a sane price with more RAM and a better antenna, and it runs Armbian perfectly well.
I have nothing against Raspberry Pi. The software ecosystem and the documentation are still the best in this space by a distance, and for a beginner that’s worth a lot. But the reflex to reach for a Pi for every small project made sense when they were cheap and available, and right now they’re neither.
How the buttons work
The Pi runs Armbian. The buttons are wired between a GPIO pin and a ground pin, which is the simplest possible arrangement: no resistors, no breadboard, just two wires per button.
The pin map I ended up with:
| Button | Header pin | gpiochip0 line | Ground |
|---|---|---|---|
| White | 11 | 226 | 9 |
| Yellow | 22 | 262 | 25 |
| Green | 12 | 257 | 14 |
| Blue | 29 | 256 | 30 |
Those line numbers are the part that will not transfer to your board. The physical header pin and the GPIO line number are different things, and the mapping is specific to this SoC. Find yours with gpioinfo before wiring anything, or you’ll spend an evening confused.
Watching them is one command, from libgpiod:
gpiomon --bias=pull-up --falling-edge gpiochip0 226 262 257 256
--bias=pull-up enables the internal pull-up resistor, so the line sits high until the button pulls it to ground. Without it the pins float and you get phantom presses, which is exactly what happened to me first and looked like a hardware fault for an embarrassing length of time. --falling-edge means only fire on press, not release.
A small script reads that stream and runs a shell script per button:
#!/bin/bash
CHIP="gpiochip0"
declare -A BUTTONS=(
[226]="/home/orangepi/radio/b1.sh" # White
[262]="/home/orangepi/radio/b2.sh" # Yellow
[257]="/home/orangepi/radio/b3.sh" # Green
[256]="/home/orangepi/radio/b4.sh" # Blue
)
gpiomon --bias=pull-up --falling-edge $CHIP "${!BUTTONS[@]}" | while read line_num event; do
line=$(echo "$line_num" | grep -oE '[0-9]+')
[ -n "${BUTTONS[$line]}" ] && bash "${BUTTONS[$line]}" &
done
And each button script is about as simple as it gets:
#!/bin/bash
pkill mpv
sleep 0.5
mpv --no-video --no-resume-playback --msg-level=all=error "https://stream.url/here" &
Same mpv flags as the codemusic alias, for the same reasons. The pkill first is important: without it, pressing two buttons gives you two stations playing simultaneously, which is a comedy routine the first time and an annoyance forever after.
Finding a station’s stream URL is the fiddly part. Most broadcasters publish them somewhere, and radio-browser.info has a large catalogue if they don’t. Test each one on your own machine before committing it to a button.
The whole thing runs as a systemd unit so it comes up on boot. That’s the only part that absolutely must not fail, because “it turned itself off and won’t come back” is the failure mode I’d have to fix by phone.
Debouncing, or: why the yellow button used to skip
Mechanical buttons don’t close cleanly. They bounce, and a single press can register as three or four falling edges in a few milliseconds. With pkill mpv at the top of each script, that reads as: start the station, kill it, start it, kill it.
The sleep 0.5 in the script covers most of it. If you want to do it properly, track the last trigger time per line and ignore anything within a couple of hundred milliseconds. I did the lazy version because the lazy version works.
Managing it from eight time zones away
The Pi is on my tailnet, which is the thing that makes this sustainable rather than a one-off gift I’d dread getting a phone call about.
From here I can SSH in, change a station, check whether the WiFi has been dropping, update packages. No port forwarding, nothing configured on their router, nothing my parents have to know exists.
One detail worth passing on: use mosh rather than plain SSH for this. A transatlantic link to a small board on domestic WiFi is not a stable thing, and mosh survives the connection dropping and your laptop sleeping in a way SSH simply doesn’t. It’s the difference between administering it and fighting it.
What I’d tell you if you’re building one
The interface is the whole product. Everything technical here is easy. The hard part was deciding there would be four buttons and no screen, and then going further and removing the Bluetooth switch and the light modes the speaker came with. Every feature I considered adding made it worse, and the two I took away made it better.
Make it recover by itself. systemd unit with Restart=always. Assume nobody at the other end will ever debug anything, and that you will find out something is wrong days later.
Label the buttons. I ran a strip off a label printer with the station names on it and stuck it along the front, under the buttons. This took two minutes and was probably the highest-value thing I did after choosing the antenna. Colour coding alone would have worked eventually, but “the green one is the one you want” is a thing I’d have had to say on the phone a few times. With the strip there, nobody had to learn anything. They just read it.
Spend the money on the antenna, not the audio. Nobody ever complained the sound wasn’t good enough. Connectivity is the thing that decides whether it gets used.
The best compliment it’s had is that nobody mentions it. It’s just the radio now.