NFC Player
The goal of this project was to use an NFC tag reader to play media from a selection of playlists. In this setting, an NFC tag maps N:1 to a playlist; there can be many NFC tags for a playlist, but we only select one playlist for each NFC tag.
This project can be used as a juke box replacement for home use, or as an audio book machine for kids.
In order not to reinvent the wheel too many times, this implementation uses MPD to manage music, playback, and playlists; the Python service implemented herein merely sends playback control commands to mpd.
Code
Installation Instructions
The NFC player consists of a hardware and software component. For my specific use case, I used the following hardware components, although they can easily be replaced with equivalents:
- System: Raspberry Pi Zero W
- NFC reader: mifare NFC MFRC 522
In terms of software, I went with the default Raspberry Pi OS, MPD and a self implemented amount of Python glue code.
The case was designed in OpenSCAD and is fairly basic; in fact, it has a number of shortcomings and turned out to be a point of failure when being accidentally dropped off the shelf repeatedly, so a redesign is advised.
Preparing the Raspberry Pi
The MFRC522 NFC reader will have to be attached to the Raspberry Pi connectors. You will typically want to solder the eyes of the MFRC522 to the eyes of the Raspberry Pi (or equivalent hardware). The following connections need to be made (follow along on pinout.xyz for details):
- SDA on the MFRC522 connects to pin 24 (SPI0 CE0)
- SCK on the MFRC522 connects to pin 23 (SPI0 SCLK)
- MOSI on the MFRC522 connects to pin 19 (SPI0 MOSI)
- MISO on the MFRC522 connects to pin 21 (SPI0 MISO)
- IRQ on the MFRC522 can optionally connect to pin 13 (GPIO 27), but is not used in this howto
- GND on the MFRC522 connects to pin 20 (Ground), or any other ground pin
- RST on the MFRC522 connects to pin 22 (GPIO 25)
- 3.3V on the MFRC522 connects to pin 17 (3v3 Power), or any other 3.3V pin
Installing the requirements
For the following description, we assume a readily installed Raspberry Pi OS to be present and booted.
First, we need to install all required software:
$ sudo apt update
$ sudo apt full-upgrade -y --purge
$ sudo apt install -y python3-musicpd mpd ncmpc python3-pip ipython3 pulsemixer pulseaudio pulseaudio-utils pulseaudio-module-zeroconf pulseaudio-module-bluetooth bluez subversion
$ sudo pip install --system mfrc522
You also need to enable SPI to be able to talk to the NFC reader. To do this, you need to edit your /boot/config.txt file and make sure the following lines are present under the unnamed default config section and not commented out:
dtparam=spi=on
dtoverlay=spi-bcm2708
To apply these changes, you will have to reboot your Raspberry Pi:
$ sudo reboot
This in itself should already be enough to connect to the NFC reader. If you want to put it to a test, you can use the ipython shell to check whether reading an NFC tag works as expected:
$ sudo apt install -y ipython3
$ ipython3
In [1]: from mfrc522 import SimpleMFRC522
In [2]: r = SimpleMFRC522()
In [3]: id, data = r.read()
In [4]: id
Out[4]: 628513193142
If you wired everything up correctly, the r.read() call above should complete as soon as you touch an NFC card to the reader, and you should see an ID in the Out[4] section. Note that bad wiring / soldering does not necessarily result in an error of some sort, it may just be that read() never finishes.
Setting up the software
In a next step, we will need to actually set up the software used to communicate between the NFC card reader and the music player daemon. Check out the repository and install the files into their proper places:
$ svn co https://svn.caoimhe.io/svn/pynfc-player/trunk pynfc-player
$ sudo install -o root -g root -m 0755 nfc_reader.py /usr/local/bin/nfc_reader.py
$ sudo install -o root -g root -m 0644 nfc_reader.service /lib/systemd/system/nfc_reader.service
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now nfc_reader.service
Setting up Bluetooth Audio
The gold standard of audio on Linux is, at this point, still PulseAudio. However, normally PulseAudio starts when an user logs into the system. Given that we are setting up an unattended Raspberry Pi in a corner somewhere, there is not usually going to be a logged-in user, so we need to set up system-wide pulseaudio.
To do this, we first need to make sure the following modules are loaded in /etc/pulse/system.pa:
### Automatically restore the volume of streams and devices
load-module module-device-restore
load-module module-stream-restore
load-module module-card-restore
### Should be after module-*-restore but before module-*-detect
load-module module-switch-on-port-available
.ifexists module-switch-on-connect.so
load-module module-switch-on-connect
.endif
### Automatically load driver modules depending on the hardware available
.ifexists module-udev-detect.so
load-module module-udev-detect
.else
### Use the static hardware detection module (for systems that lack udev/hal support)
load-module module-detect
.endif
### Automatically load driver modules for Bluetooth hardware
.ifexists module-bluetooth-policy.so
load-module module-bluetooth-policy
.endif
.ifexists module-bluetooth-discover.so
load-module module-bluetooth-discover autodetect_mtu=yes
.endif
### Load several protocols
.ifexists module-esound-protocol-unix.so
load-module module-esound-protocol-unix
.endif
load-module module-native-protocol-unix
.ifexists module-esound-protocol-tcp.so
load-module module-esound-protocol-tcp
.endif
.ifexists module-native-protocol-tcp.so
load-module module-native-protocol-tcp
.endif
.ifexists module-zeroconf-publish.so
load-module module-zeroconf-publish
.endif
### Automatically restore the default sink/source when changed by the user
### during runtime
### NOTE: This should be loaded as early as possible so that subsequent modules
### that look up the default sink/source get the right value
load-module module-default-device-restore
### Make sure we always have a sink around, even if it is a null sink.
load-module module-always-sink
### Automatically suspend sinks/sources that become idle for too long
load-module module-suspend-on-idle
### Enable positioned event sounds
load-module module-position-event-sounds
Then we need to ensure that pulseaudio has access to the relevant Bluetooth services by typing the following on the command line:
$ adduser pulse bluetooth
While we're here, we should give our very own pi user (or whatever user you are using to log into the player) all required access:
$ sudo adduser pi bluetooth
$ sudo adduser pi spi
$ sudo adduser pi pulse
$ sudo adduser pi pulse-access
We need to make sure now that the bluetooth group has all required permissions to be able to manage Bluetooth devices. To do this, make sure we have the following section configured in :
<policy group="bluetooth">
<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_interface="org.mpris.MediaPlayer2.Player"/>
</policy>
To apply this change, we need to restart dbus:
$ sudo systemctl restart dbus.service
At this point, there is still nothing though to tell the system to actually start an instance of pulseaudio, even though we would have access to one if it did. To achieve this, we need to set up the following service file in /etc/systemd/system/pulseaudio.service:
[Unit]
Description=PulseAudio system server
# DO NOT ADD ConditionUser=!root
[Service]
Type=notify
ExecStart=pulseaudio --daemonize=no --system --realtime --log-target=journal
Restart=on-failure
SystemCallArchitectures=native
SystemCallFilter=@system-service
CPUWeight=1000
IOWeight=1000
Nice=-19
[Install]
WantedBy=multi-user.target
Then, we need to actually fire up our new pulseaudio service:
$ sudo systemctl daemon-reload
$ sudo systemctl --system enable --now pulseaudio.service
This should give you a running pulseaudio server. Now we just need to instruct pulseaudio clients to use it. We do this by setting the following values in /etc/pulse/client.conf:
default-server = /var/run/pulse/native
autospawn = no
As a final step, we need to instruct mpd to use pulseaudio. To do this, we can edit the /etc/mpd.conf file, find the pulse section for audio_output, and make sure it has the following settings:
audio_output {
type "pulse"
name "PulseAudio Output"
}
Now we need to make sure that any commands we may be running on the player box do not cause mpd to stutter, so we need to add an override to /etc/systemd/system/mpd.service.d/override.conf. This can be done by running the following command:
$ sudo systemctl --system edit mpd.service
In this file, we will set the following overrides:
[Service]
Nice=-5
CPUWeight=250
IOWeight=250
Now we just need to make sure that mpd gets access to PulseAudio and that systemd gets notified about all the changes:
$ sudo adduser pi pulse-access
$ sudo systemctl daemon-reload
$ sudo systemctl --system restart mpd.service
We should now be able to pair the bluetooth receiver (speaker or headphone). To do this, run the following commands (with the correct bluetooth device ID for the receiver you are attempting to pair, of course):
$ bluetoothctl
[bluetooth]# power on
[bluetooth]# agent on
[bluetooth]# scan on
[bluetooth]# pair 00:11:22:AA:BB:CC
[bluetooth]# trust 00:11:22:AA:BB:CC
[bluetooth]# connect 00:11:22:AA:BB:CC
You should now be able to connect to mpd using any client and start playing music.
Usage
The user interface of the player itself is kept very simple to be accessible to kids:
- Placing an NFC tag on the reader will send a command to mpd to clear the playlist (stop playback), load a new playlist with the name specified on the NFC tag, and start playing back that playlist.
- Removing the tag from the range of the reader will pause playback.
- Placing the same NFC tag back on the reader will resume playback of the currently selected playlist from the current position.
- Placing a different NFC tag on the reader will start playing back another playlist; the current position in the currently playing playlist will be lost.
Adding content
In order to add a new figure to be played back by the player, you will need to first copy the music to the /var/lib/mpd/music directory. Connect to the mpd service on the device, e.g. by using ssh to log in and calling ncmpc to manage the music player daemon. Invoke a database update to get your new content recognized (Ctrl+U in ncmpc).
Then, add all the content you want to be played back to the play queue in the order you want it played back. Save the file to a playlist with an easy-to-remember name, in this example aeseaes. (Obviously, choose a playlist name that represents the content it contains, so that you will know what the playlist is about when you need to.)
Place the NFC tag to be associated with the content on the NFC card reader. Then invoke the following commands on the command line of the raspberry pi:
$ ipython3
In [1]: from mfrc522 import SimpleMFRC522
In [2]: w = SimpleMFRC522()
In [3]: w.write('aeseaes')
This will write the name of the playlist to the NFC tag so that the next time it is placed on the card reader, it will be picked up and the playlist named aeseaes will be loaded in mpd.
Handling non-writable NFC tags
Especially cheap NFC tags are not always writable, so you may not have the option to write a text to them. Affected tags will always return an empty string as the second return value of the read() method. In this case, a workaround exists in that the ID of the NFC tag can be used to identify a playlist to load.
You will need to determine the hexadecimal representation of the ID of the tag; one way to do that is to run the following:
$ ipython3
In [1]: from mfrc522 import SimpleMFRC522
In [2]: r = SimpleMFRC522()
In [3]: id, _ = r.read()
In [4]: '%X' % id
Out[4]: 88042D46E7
In this case, you can save your playlist as 8042D46E7 in mpd, and it will be correctly associated with the NFC tag in question.
Future Work
There are currently still some flaws with the way the jukebox is implemented:
- Some playlists should be played in shuffle mode, while others should not; it should be possible to embed a shuffle flag into the NFC tag.
- The alsa Bluetooth implementation (bluealsa) appears to be unmaintained and is unstable; it would be better to migrate to pulseaudio or pipewire.
- The bluetooth connectivity from the Raspberry Pi Zero W appears to be prone to disconnections, it is possible that a different board and/or a dedicated USB bluetooth dongle could improve the situation.
Some more features that could be implemented later:
- Volume control using buttons
- Support for an I²C display to show current playback information from mpd