Monday, 11 September 2023

Getting a bluetooth capable amplifier working with a bluetooth USB dongle on Linux

A while back I bought one of those $10 tiny class D amplifiers that can drive my bookshelf speakers with up to 30W (depending on power supply voltage, between 12V and 24V). An old laptop supply supplies this. It can be fed from a 3.5 audio socket or from bluetooth. Works like a charm. May not be audiophile but good enough for me.

I've been feeding it from an older mobile phone that mounts a SMB share from my workhorse computer. But as I would like to automate it so that I can use music to wake me up, I bought a bluetooth USB dongle from one of many AliExpress sellers. It contains a Realtek chipset that I checked is supported in Linux and supports BT 5.3 profile.

How to set the dongle up? Plugging it in elicited messages in the syslog, as dmesg showed. I used the KDE desktop widget (probably bluedevil) to connect to it and command it to pair and connect to amplifier. A prerequisite is that bluetoothd should be running. I discoved that the configuration can also be done from the CLI using bluez-tools. But once discovered and paired, nothing more needs to be done on this front as the setup is static.

How to send audio to it? I thought there might be a PCM device under /dev like for sound cards. But no. It seems that you need a service like pulseaudio to drive it. So I got stuck into pulseaudio documentation. Along the way I discovered that my distro prefers pipewire. So I installed the pipewire packages and this also obsoleted the corresponding pulseaudio packages. One of the packages pipewire-pulseaudio supplies compatible functionality.

Pipewire is a process that should keep running to handle all this. How to fire it off? It turns out that it's preferable to run it with user permissions, using the user instance of systemd, e.g.

systemctl --user start pipewire.service

How to make sure it runs when the user logs in? You activate the systemd units like this:

systemctl --user enable pipewire.service pipewire.socket

Then start both the service and the socket the first time.

How to send audio to pipewire? I struggled with this under pulseaudio, it wouldn't show up in players like vlc or amarok. But once I switched to pipewire, the bluetooth audio sink was just there and I could control the volume and make it the default audio playback.

How to specify this device to mplayer so that I can play from a cron job? I just had to specify it to the -ao option.

mplayer -ao pulse audio.mp3

Notice that mplayer sees pipewire the same as pulseaudio. I can put this in a profile in .mplayer/config named after the amplifier ID:

[xinyi]
ao=pulse

then I can specify:

mplayer -profile xinyi audio.mp3

The advantage is that I can specify other settings in the profile.

Wednesday, 6 September 2023

Pay attention to the size of ipsets for nftables

I was preparing an ipset for blocking access by geographical region using firewalld. Firewalld can work with either the traditional iptables or the newer nftables.

I created the ipset file using this Makefile stanza:

%.load: ipset_%.txt
        -firewall-cmd --permanent --delete-ipset=$(@:.load=)
        firewall-cmd --permanent --new-ipset=$(@:.load=) --type=hash:net
        firewall-cmd --permanent --ipset=$(@:.load=) --add-entries-from-file=$<
        systemctl reload firewalld

Here % is replaced by the two letter country code for the region I want to allow.

In the public zone I have a rich rule which inverts the test. This is the result of the firewall-cmd I issued:

  <rule family="ipv4">
    <source ipset="ok" invert="True"/>
    <service name="theservice"/>
    <log/>
    <drop/>
  </rule>

Later I discovered that that the countries not in ok weren't blocked. Troubleshooting found these symptoms:

  • firewall-cmd --state returned failed, despite systemctl stating that the service had started
  • There was a message about python-nftables failing in systemd logs
  • nft list showed nothing in the chains or rulesets

Searching on these symptoms got hits but none of the solutions worked. The only thing I learnt is that firewalld calls the python3-nftables routines to manipulate the nftables. I thought it might be the top line in the Python script which read #!/usr/bin/python which invokes python2 on my system, but adding 3 made no difference presumably because firewalld calls it with /usr/bin/python3 <name of script>. Someone suggested this python3 module was broken in a particular distro release because it didn't include a needed flag so he went back to iptables as the backend. I didn't want to do this. Also the report was for a previous distro release so over a year old.

Playing around with ipset which deals directly with the kernel to create the sets manually revealed that the default maxelem of 65336 was not sufficient for my set. After some experimentation I figured that I had to increase this parameter at creation. This boiled down to including this final option on the firewall-cmd line:

firewall-cmd --permanent --new-ipset=$(@:.load=) --type=hash:net --option=maxelem=262144

Now restarting firewalld took a bit longer due to having to digest lots of entries, and there was no more failure of python-nftables. Inspecting the ruleset using nft worked and showed that the nftables rule was in place.

It would have been easier if I had understood the relationship of all the components of this setup better, but things are obvious in hindsight.