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:

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):

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:

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 more features that could be implemented later: