Using a Raspberry Pi with Android phones for media streaming with UPnP / DLNA

Edit, 2013-07-21: Please note there is now a completely updated (and simpler) blog post on this subject.

This summarises and completes my previous posts on configuring a Raspberry Pi to play music streamed from another computer in the house, using Android phones as remote controls.

There is quite a long discussion below and also an area on the Raspberry Pi forum for discussing this solution.

What we are aiming for

A music system with your music stored on a server and played through your Hi-Fi via the Raspberry Pi, all controlled by multiple, synchronised Android phones/tablets. In addition you can play music from the server on your phone either in the house or anywhere else. This is similar to using Apple’s AirPlay system but uses free software and open standards.

With any luck your initial outlay will only be about £45. The end result will be similar to the systems from Sonos and Squeezebox costing loadsamoney. Further additional devices to play music through another Hi-Fi or TV would also be £45.

What you need

  • A Raspberry Pi
    • I got mine with a case from ModMyPi for £35
  • A wireless USB dongle (or wired ethernet connection near your Hi-Fi)
  • A micro-USB power supply (most phones use these)
  • An SD Card (2GB minimum) – perhaps an old one from a camera?
  • An SD Card reader/writer – often built in to a computer
  • Your music available on a linux server (e.g. CDs ripped to MP3 files) – this could be replaced with e.g. a NAS device or a Windows machine
  • An amplifier with a 3.5mm auxiliary input and speakers (perhaps a TV sound bar?)
  • At least one Android device
  • Some sort of router / wireless network to connect the media server to the Raspberry Pi
  • You might need a USB keyboard and a TV/monitor to do the initial set-up (hopefully you have these lying around)

Note: these instructions assume you have your music on a linux server, but it’s also possible to get all the necessary software on one Raspberry Pi and just point it at a NAS (or, indeed, plug a USB drive in directly). See comments for some more suggestions.

Instructions

Install Debian Wheezy on the Pi

The Raspberry Pi comes with no operating system (OS). The first thing to do is to download one and put it onto the SD card. These instructions assume you are using the Debian Wheezy image (I have checked these instructions work with 2012-12-16-wheezy-raspbian.zip) from the Raspberry Pi site. There are brief instructions there and detailed instructions at elinux.org.

Once you have the OS on the SD card, put it in the Raspberry Pi, plug in your keyboard and TV (via the HDMI or composite video sockets), plug in the micro-USB power supply and turn it on.

Getting the Pi on the wireless network

You can of course skip this if you’re just going to plug it in with an ethernet cable.

Different wireless dongles use different drivers and different home networks have different security settings. It’s therefore pretty tricky to describe how to do this in a generic way but I can give you some pointers.

The lsusb command shows you what USB devices are plugged in. The 4th one on the list below is the Wi-Fi dongle.

$ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 0424:9512 Standard Microsystems Corp.
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp.
Bus 001 Device 004: ID 148f:5370 Ralink Technology, Corp. RT5370 Wireless Adapter

I didn’t have to edit the /etc/network/interfaces file. It was already how I needed it, namely:

auto lo

iface lo inet loopback
iface eth0 inet dhcp

allow-hotplug wlan0
iface wlan0 inet manual
wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
iface default inet dhcp

Amongst other things, this says that the wireless dongle can be plugged in and out when you like, it is configured with DHCP (so it gets an IP address and hostname automatically from your router) and that the password and other security settings for your Wi-Fi can be found in the /etc/wpa_supplicant/wpa_supplicant.conf file.

By typing the command “sudo vi /etc/wpa_supplicant/wpa_supplicant.conf” you can edit the file (replace “vi” with “nano” if you like your text editors to help you out a bit). For me I needed to add these lines:

network={
  ssid="your_ssid_goes_here"
  scan_ssid=1
  proto=RSN
  key_mgmt=WPA-PSK
  pairwise=TKIP
  psk="this is where your secret passphrase goes"
}

Your configuration depends on what Wi-Fi security settings you have on your router: the above work for “WPA2 personal” with “TKIP+AES” for the WPA algorithm with a WPA shared key. There’s more info to be found by typing “man wpa_supplicant.conf”.

At this point, typing “ifconfig” will show you the configuration of “eth0″ (the ethernet interface with the cable in), “lo” (a dummy interface) and “wlan0″: the new wireless interface.

wlan0     Link encap:Ethernet  HWaddr 00:01:02:03:04:05
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

At this point you should probably also configure your router to provide the Pi with an IP address via DHCP (it may do this automatically). I use DD-WRT on my router which lets me assign it a static IP address and a hostname as well. Again, it’s a bit beyond the scope of this post to explain all that but you may need the Wi-Fi dongle’s MAC address which is called the “HWaddr” in the ifconfig output.

If you then do a “sudo ifdown wlan0″ and a “sudo ifup wlan0″ to take the interface down and bring it up again then with any luck, on executing “ifconfig” again you’ll see an additional line showing that your router has assigned an IP address to the Wi-Fi dongle:

wlan0     Link encap:Ethernet  HWaddr 00:01:02:03:04:05
          inet addr:192.168.1.99  Bcast:192.168.1.255  Mask:255.255.255.0
          etc, etc... (addresses changed to protect the innocent)

Installing software on the Pi

Now the Raspberry Pi is on the wireless network, you can disconnect it from the TV and keyboard and put it next to the Hi-Fi. Connect it to the auxiliary input of the Hi-Fi with a stereo audio cable (3.5mm jack like headphones use) and power it up again.

Log in to the Pi from another computer using SSH (username “pi”, password “raspberry”). I use PuTTY from my Windows laptop which does the job nicely. You can then type in comfort at your laptop rather than sitting on the floor by the TV. Incidentally, with PuTTY you paste by clicking with the right mouse button (and copy just by highlighting with the left). If you’re on a Windows machine therefore you can copy and paste from this blog post using Ctrl-C to copy and right-click in PuTTY to paste.

The first thing to complete the set-up of the Pu and update all the packages already installed on the Pi to the latest versions:

$ sudo raspi-config
$ sudo apt-get update
$ sudo apt-get upgrade

The first command runs a config utility that lets you choose your country, keyboard, etc and also lets you choose to stop the Pi booting straight into a desktop which we don’t want. The next gets the latest list of packages and the third command updates existing packages to the latest versions found on that list. This will take a little while.

We then need to install gmrender-resurrect which is a UPnP “renderer”. That is, it plays (renders) music files when told to via the UPnP protocol. It is not provided as a pre-built package so we also have to compile it and manually install all the prerequisites.

# install all the packages we'll need

$ sudo apt-get install git automake libglib2.0-dev gstreamer0.10-alsa gstreamer0.10-tools libgstreamer0.10-dev libupnp-dev libxml2-dev gstreamer0.10-ffmpeg gstreamer0.10-plugins-base gstreamer0.10-plugins-good gstreamer0.10-fluendo-mp3 gstreamer0.10-pulseaudio pulseaudio

# Now get the latest gmrender-ressurect using git

$ cd
$ git clone https://github.com/hzeller/gmrender-resurrect.git
$ cd gmrender-resurrect

# build and install gmrender

$ ./autogen.sh
$ ./configure LIBS=-lm
$ make
$ sudo make install

gmrender uses GStreamer to play sound and GStreamer uses ALSA. For some reason, the sound quality through GStreamer at this point is really bad. To get it sounding good we need to direct the sound through the PulseAudio system (which then uses ALSA). To make GStreamer use PulseAudio rather than directly using ALSA you need to do the following configuration:

$ gconftool-2 -t string --set /system/gstreamer/0.10/default/audiosink pulsesink
$ gconftool-2 -t string --set /system/gstreamer/0.10/default/audiosrc pulsesrc

Note that these commands are executed as the “pi” user and only apply to the “pi” user. If you later run gmrender (or GStreamer directly) as another user (such as root) then the sound quality will be bad again as PulseAudio won’t be in the stack.

To get PulseAudio to start on boot and to allow the “pi” user to access it, two more changes are required:

  1. Edit /etc/default/pulseaudio and change the first uncommented line to “PULSEAUDIO_SYSTEM_START=1″. This is not recommended but it works in this case.
  2. Execute the command “sudo adduser pi pulse-access” to add the pi user to the pulse-access group so that it is permitted to use PulseAudio.

You’ll also need to turn the volume up on the Pi. To do this, run “alsamixer” and press the up arrow cursor key until the volume display gets to the top (if this isn’t doing anything then it may be muted: press “M”). Then press the Esc key to exit. To then save this configuration for the next time the Pi boots, type “sudo alsactl store” which updates the /var/lib/alsa/asound.state file.

2013-01-91 Edit: I’ve written much more about configuring ALSA on the Raspberry Pi here.

To make gmrender start when the Pi is turned on, create the file /etc/init.d/gmediarenderer (as root) with the following contents (e.g. using vi or nano):

#!/bin/sh

### BEGIN INIT INFO
# Provides: gmediarender
# Required-Start: $remote_fs $syslog $all
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start GMediaRender at boot time
# Description: Start GMediaRender at boot time.
### END INIT INFO

USER=root
HOME=/root
export USER HOME
case "$1" in
start)
echo "Starting GMediaRender"
start-stop-daemon -x /usr/local/bin/gmediarender -c pi:audio -S -- -f "Raspberry" -d
;;
stop)
echo "Stopping GMediaRender"
start-stop-daemon -x /usr/local/bin/gmediarender -K
;;
*)
echo "Usage: /etc/init.d/gmediarender {start|stop}"
exit 1
;;
esac
exit 0

Then do:

$ sudo chmod 755 /etc/init.d/gmediarenderer
$ sudo update-rc.d gmediarenderer defaults

Reboot (either “sudo shutdown -r now” or just turn it off and on again) and you should have a working system.

Installing software on the media server

I have all my CDs ripped as FLAC files along with some MP3s I’ve bought on a small linux box (an Acer Revo Aspire 3700 bought a couple of years ago in the pre-Raspberry era) with 2 USB drives plugged into it. It currently runs some version or other of Ubuntu. There’s no reason that you couldn’t use another Raspberry Pi for this with a USB disc drive attached or even the same Raspberry Pi, perhaps pointed at a NAS box for the media storage.

We need to install:

  • minidlna: this serves up the music files using the DLNA / UPnP protocol;
  • BubbleUPnP server: this provides the OpenHome protocol on top of all the UPnP devices so that all your controllers (phones, tablets) share the same playlist and “playing now” view.

For minidlna you can just use apt-get:

$ sudo apt-get install minidlna

Point minidlna at your music files by editing the /etc/minidlna.conf file, e.g.:

# set this to the directory you want scanned.
media_dir=A,/mnt/usbdisc1/music/albums

# set this if you want to customize the name that shows up on your clients
friendly_name=My DLNA Server

# set this if you would like to specify the directory where you want MiniDLNA to store its database and album art cache
db_dir=/var/cache/minidlna

I also had to “sudo chown minidlna.minidlna /var/cache/minidlna” to get it to work, but YMMV.

The BubbleUPnP server is also easy to install, just following the instructions on the web site, which for me (on Ubuntu) was just:

$ sudo add-apt-repository ppa:bubbleguuum/bubbleupnpserver
$ sudo apt-get update
$ sudo apt-get install bubbleupnpserver
$ sudo start bubbleupnp

To install BubbleUPnP on a Pi, follow the “Other Java Platforms” instructions on the BubbleUPnP server web page (e.g. download the zip file etc).

Then go to the address “http://your.media.server:58050″ and you will see the BubbleUPnP server admin interface. The important page for us is the “Media Renderers” tab where you should find the Raspberry Pi’s gmrender service listed as “Raspberry”. Select it and tick the “Create an OpenHome renderer” box on the right and that’s that sorted.

Installing software on Android devices

If you have an Android phone then go to Google Play and install BubbleUPnP. It is a UPnP renderer and control point and also works with the OpenHome protocol. In other words, it will play music (and videos and pictures) from the media server and can also tell the Pi to play music. As it uses the OpenHome system, playlists and so on are shared between all Android devices connected to the system. The app has some limitations (not too restrictive) and you can pay £3 to have it unrestricted which is well worth it whether you need to or not if you ask me.

If you don’t have an Android phone, just go and buy a second-hand one for £50, don’t bother sticking a SIM card in it and just use it as a remote control!

Once installed on the phone, run BubbleUPnP and go to the “Devices” page. You should see under “Renderers” the “Local renderer” (your phone), “Raspberry” and “Raspberry (OpenHome)”. “Raspberry” will work as the renderer but choosing that bypasses the OpenHome goodness already described. So, choose “Raspberry (OpenHome)” and under the “Libraries” select your media server. You will also see “Local Media Server” there (your phone’s files) and perhaps some other computers on your network.

Finally, go to the “Library” page, choose some music and play it: it should come out of the Hi-Fi!

Final thoughts

In this post I have concentrated on the particular steps I have taken to get a working system from my starting point. There are many possible variations on this. For instance, you could have a workable system without a dedicated media server but just using your home PC instead: BubbleUPnP Server runs on Windows as well (if you need it at all), and you could use MediaMonkey for instance on the same machine to be the UPnP server. Or you could do without the media server entirely and just stream music from your phone to the Raspberry Pi.

Finally, there is a lot more to the BubbleUPnP client and server that I couldn’t cover here and which isn’t to do with the Raspberry Pi. The client for instance can play music from Google Music. You can also set up the server so that you can access it from outside your house and by some clever configuration make it so that if you go to a friend’s house who has a DLNA renderer (lots of TVs do these days) then you can use your phone to tell your friend’s TV to play music from your server!

Please see the Raspberry Pi forum for more discussion as well as the comments below.

2013-01-08 Edit: added instructions for getting PulseAudio to start on boot and altered gmediarenderer init script to make it start after PulseAudio.

2013-01-09 Edit: added instructions to save ALSA volume between reboots.

2013-01-10 Edit: clarified how to install BubbleUPnP Server on a Raspberry Pi.

2013-01-14 Edit: added link to Raspberry Pi forum.

2013-01-19 Edit: added better ALSA instructions and linked to my other post.

Making gmediarender start on boot

Now gmediarender is compiled and gstreamer works on the Raspberry Pi we need to get it all to start on boot.

Fairly standard stuff here (copied from Malte’s post in Chris’ blog). Create a file /etc/init.d/gmediarenderer with the following contents:

#!/bin/sh

### BEGIN INIT INFO
# Provides: gmediarender
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start GMediaRender at boot time
# Description: Start GMediaRender at boot time.
### END INIT INFO

USER=root
HOME=/root
export USER HOME
case "$1" in
start)
echo "Starting GMediaRender"
start-stop-daemon -x /usr/local/bin/gmediarender -c pi:audio -S -- -f 'Raspberry' -d
;;
stop)
echo "Stopping GMediaRender"
start-stop-daemon -x /usr/local/bin/gmediarender -K
;;
*)
echo "Usage: /etc/init.d/gmediarender {start|stop}"
exit 1
;;
esac
exit 0

Then do:

$ sudo chmod 755 /etc/init.d/gmediarenderer
$ sudo update-rc.d gmediarenderer defaults

to make the script executable and add in all the symbolic links from the /etc/rc.x directories.

You can then do “sudo service gmediarenderer start” to run gmediarenderer as a daemon there and then or reboot and it will start automatically. Interestingly, I tried it without the “-c pi:audio” so that the daemon ran as root and the sound quality went bad again. Fine running as the “pi” user though.

In summary, we now have:

  1. minidlna running on the media server (a nettop linux box behind the sofa);
  2. gmedia-resurrected running on the Raspberry Pi to play the music through the mini Hi-Fi;
  3. bubbleupnp on multiple Android phones to control the music choice.

This is a pretty good solution but has some problems caused by the playlist being held on the controller (one of the phones). So if one of us sets an album playing and then goes out, the Pi doesn’t receive the instruction to play the next song. If you look at a second controller then you cannot see what is playing (it may think something else is), but on the plus side, the volume controls work on all controllers and the play/pause does as well.

These problems are known of course, and the BubbleUPnP author has another piece of software called BubbleUPnPServer which is an OpenHome renderer. As the site says:

An OpenHome Renderer has the following advantages over a regular UPnP AV renderer:

  • each OpenHome renderer has its own playlist
  • no need to leave the Control Point running for playlist track advance to happen
  • several OpenHome Control Points (BubbleUPnP, Linn Kinsky) can be connected to the same renderer simultaneously, showing the same playlist and playback state
  • create a playing playlist on a device and pick it up later on another device

It looks as though the BubbleUPnPServer basically adds another, persistent and stateful, layer to the system so that an OpenHome controller (such as BubbleUPnP) tells BubbleUPnPServer what to play and it then tells the existing (plain) UPnP renderer what to do. Hopefully I can get this working on the same linux box as minidlna is running on.

Getting gstreamer to work on a Raspberry Pi

Having now got gmrender to compile and run on the Raspberry Pi and use gstreamer as a backend I need to fix the audio quality issues. A bit more googling revealed a post from someone doing much the same as me. One of Chris Baume’s commenters also had problems with the audio qut.ality and suggested that directing the audio via PulseAudio fixed it.

It’s been a long time since I got into the detail of linux audio systems and it all seems to have changed. There’s a great article on how linux audio works which helps explain things a lot.

The following annotated shell transcript shows how I installed PulseAudio and configured gstreamer to use it. I may post have now posted a complete cleaned up installation guide later but personally I find seeing how other people have made mistakes and worked out how to do things instructive as well.

$ sudo apt-get install gstreamer0.10-pulseaudio

# Need to configure gstreamer to use pulseaudio
# Looking here: https://wiki.archlinux.org/index.php/PulseAudio

$ gconftool-2 -t string --set /system/gstreamer/0.10/default/audiosink pulsesink
$ gconftool-2 -t string --set /system/gstreamer/0.10/default/audiosrc pulsesrc

# and another just in case:

$ gconftool-2 -t string --set /system/gstreamer/0.10/default/musicaudiosink pulsesink

# check the keys are there:

$ gconftool-2 -a /system/gstreamer/0.10/default
 musicaudiosink_description = Default
 audiosrc = pulsesrc
 audiosrc_description = Default
 chataudiosink_description = Default
 musicaudiosink = pulsesink
 audiosink_description = Default
 visualization = goom
 videosrc = v4l2src
 audiosink = pulsesink
 chataudiosink = autoaudiosink
 videosink = autovideosink

# Still not working
# Then I realise I don't have pulseaudio itself installed...

$ sudo apt-get install pulseaudio

After doing all of that the sound was just the same. I later realised that I probably didn’t actually have PulseAudio running and would need to start it with the command “pulseaudio –start” but next time I turned the Pi on to try it, PulseAudio was running and the sound from gmedia-resurrected, via gstreamer, to PulseAudio, ALSA and finally to the hardware sounded just great! How strange that adding another layer to the sound stack makes it better, not worse…

One problem I’ve just noticed is that when you skip forwards a track in an album for instance then there are a few pops and clicks.

Next task is to get the gmediarender process to start on boot.

European public holiday leaderboard

I work in a lot of European projects.  This means that we often have weekly teleconferences across several countries which are often scuppered by a partner or partners being on holiday because of a national holiday. In the UK, all our public holidays are put on Mondays (with some Fridays) so that you don’t get the country shutting down in the middle of the week. In other countries the public holidays (e.g. saints’ days) just land on the same date every year with two results:

  1. Sometimes they land on a weekend so most people don’t actually get a holiday.
  2. When they land on a Tuesday or a Thursday lots of people take off the Monday or Friday as well to make a really long weekend and you lose almost half a week.

There is often the feeling expressed that we in the UK don’t get as many holidays as the rest of Europe, but is it true? Euroalert have published an iCalendar format file of all the public holidays for the whole of the European Union. This is very handy as you can import it into Outlook (for instance) and see when a partner is going to be on holiday. As it is in iCalendar format you can also parse it yourself easily and do some investigation:

#!/usr/bin/env python2.7

# This code is written by Stephen C Phillips.
# It is in the public domain, so you can do what you like with it
# but a link to http://scphillips.com would be nice.

from datetime import date as ddate

class Holiday:
    def __init__(self, date, country):
        d = ddate(int(date[:4]),
                 int(date[4:6]),
                 int(date[6:])
                 )
        self.date = d
        self.country = country

    def __str__(self):
        return self.country + ': ' + str(self.date)

    def _get_day(self):
        # Monday is 0, Sunday is 6
        return self.date.weekday()

    day = property(_get_day)

#http://euroalert.net/dl/docs/open-data/euroalert-Public-Holidays-EU-2012.ics
cal_file = file('c:/tmp/euroalert-Public-Holidays-EU-2012.ics')

holidays = []

ignore_weekends = 1

for line in cal_file:
    line = line[:-1]
    if line.startswith('DTSTART'):
        date = line[-8:]
    if line.startswith('SUMMARY'):
        country = line[8:]
        if '/' in country:
            country = country[:country.index('/')]
        hol = Holiday(date, country)
        if ignore_weekends and hol.day >= 5:
            pass
        else:
            holidays.append(hol)

holidays_by_country = {}
for h in holidays:
    holidays_by_country.setdefault(h.country, [])
    holidays_by_country[h.country].append(h)

countries = holidays_by_country.keys()

countries_by_holidays = {}
for c, h in holidays_by_country.items():
    num = len(h)
    countries_by_holidays.setdefault(num, [])
    countries_by_holidays[num].append(c)

print "*** LEADERBOARD ***"
for n in sorted(countries_by_holidays.keys(), reverse=True):
    print n, ':', countries_by_holidays[n]
print "*******************"

def get_days(countries):
    "Return the number of times not all partners will be on a call for each day of the week."
    holidays_by_date = {}
    for c in countries:
        for h in holidays_by_country1:
            holidays_by_date.setdefault(h.date, [])
            holidays_by_date[h.date].append(h)

    dates = holidays_by_date.keys()
    days = [0,0,0,0,0,0,0]
    for date in dates:
        day = date.weekday()
        #days[day] += len(holidays_by_date[date])  # this would tell you how many partners would be missing in total
        days[day] += 1  # this is how many times not all partners are present
    return days

project_countries = ['Austria', 'Belgium', 'France', 'Greece', 'Spain', 'Sweden', 'Wales and England']
days = get_days(project_countries)
print
print project_countries
print days

That script expects you’ve downloaded the iCalendar file yourself. It parses the file and outputs some info:

*** LEADERBOARD ***
17 : ['Belgium']
14 : ['Hungary']
12 : ['Slovenia']
11 : ['Northern Ireland', 'Austria', 'Cyprus']
10 : ['Malta', 'Slovakia', 'Ireland', 'Scotland']
9 : ['Italy', 'Czech Republic', 'Lithuania', 'Wales and England', 'Poland', 'Greece']
8 : ['France', 'Germany', 'Denmark', 'Spain', 'Finland', 'Sweden', 'Latvia', 'Luxembourg', 'Bulgaria', 'Portugal']
7 : ['Romania', 'Estonia']
6 : ['Netherlands']
*******************

['Austria', 'Belgium', 'France', 'Greece', 'Spain', 'Sweden', 'Wales and England']
[11, 4, 3, 6, 9, 0, 0]

So this is telling us that in 2012, Belgium had 17 days off (and this is weekdays off as the script has been set to ignore the weekends) whereas Wales & England had 9 (Scotland 10) and the poor Dutch only got 6. It also tells us that for the particular set of partners listed, Monday is a terrible day to hold a weekly teleconference (inevitable given the UK being on the list) and Wednesday is good with only 3 days when all partners wouldn’t be there.

Running the script and not ignoring the weekends gives this leaderboard:

*** LEADERBOARD ***
24 : ['Belgium']
17 : ['Hungary']
15 : ['Lithuania', 'Latvia', 'Cyprus']
14 : ['Malta', 'Slovakia', 'Bulgaria']
13 : ['Poland', 'Sweden', 'Portugal', 'Austria']
12 : ['Czech Republic', 'Slovenia', 'Finland', 'Estonia', 'Greece']
11 : ['France', 'Denmark', 'Northern Ireland', 'Romania']
10 : ['Italy', 'Ireland', 'Scotland', 'Luxembourg']
9 : ['Wales and England', 'Germany', 'Spain', 'Netherlands']
*******************

So here, Wales & England trail in last place with 9 days off and Belgium is still unassailable at the top with 24 public holidays. However, as we’ve seen, in other countries many of these “holidays” fall at the weekends so it’s not (quite) as bad as all that. The UK also has a high minimum statutory holiday allowance of 28 days paid annual leave per year. Your employer can choose whether to count the public holidays as part of this allowance though.

If someone can point me at the data for 2013 then that would be great!

A UPnP renderer for the Raspberry Pi

Edit, 2013-07-21: See my complete instructions for setting up the Raspberry Pi here.

Edit, 2013-01-05: See my complete instructions for setting up the Raspberry Pi here.

I want to use a Raspberry Pi to play music through the micro Hi-Fi system in my kitchen/dining room. Up to now I have had a San Francisco Android phone velcroed to the kitchen wall running the Subsonic client and accessing the FLAC files on my media server via the excellent Subsonic server software (which transcodes them on the fly to MP3). This has been a good solution for a couple of years now but one problem is that the headphone output of the San Francisco is not very powerful so we have to turn the (small) amp up to max to get a decent volume and sometimes you want it higher.

I just tested a Raspberry Pi’s audio output and it is more powerful than the phone’s so it would make a good replacement. A couple of years ago when I set up the phone as a media player,  DLNA for sharing/streaming media was not very widespread, but now it’s become quite popular so I’d like to try using that protocol with the Pi. DLNA defines various types of devices, such as “server”, “renderer” and “controller” and they then communicate via Universal Plug and Play (UPnP). I want my media server to be a DLNA server and the Pi to be a “renderer” and all our mobile phones to be the “controllers”.

On the media server I’ve installed minidlna via apt-get and pointed it at my music folder. The server runs as a “minidlna” user and for some reason I had to “chown minidlna.minidlna” the /var/cache/minidlna folder to get it to work but it seems fine now.

To control the system I’ve installed bubbleupnp on my phone and this means I can stream music from the media server to my phone. What I want to do though is use the phone as a UPnP controller and have it instruct the Pi to play (or “render”) the music through the Hi-Fi’s speakers.

Enter gmrender…

There seems to be a shortage of headless UPnP renderers for linux: I just want a server process that sits there and accepts instructions to play music and plays it – nothing more, nothing graphical. XBMC can be a UPnP renderer I believe but that’s way too heavyweight for what I want to do (even though the Raspbmc project makes it easyish). Google found me gmrender but it’s long abandoned. However, a post on Google+ from Henner Zeller sent me on to the gmrender-resurrect project on GitHub where Henner has taken the old code and added some more features.

However, getting it to install on the Raspberry Pi (with standard Debian wheezy OS) was not completely straightforward…

$ git clone https://github.com/hzeller/gmrender-resurrect.git
$ cd gmrender-resurrect

# looking at what you get, there is an autogen script so I
# need some more tools...

$ sudo apt-get install automake

# this installs autoconf autotools-dev m4
(load of output snipped...)

$ ./autogen.sh
$ ./configure
$ make
make all-recursive
make[1]: Entering directory `/home/pi/gmrender-resurrect'
Making all in src
make[2]: Entering directory `/home/pi/gmrender-resurrect/src'
gcc -DHAVE_CONFIG_H -I. -I.. -DPKG_DATADIR=\"/usr/local/share/gmediarender\" -Wall -Wpointer-arith -Wcast-align -Wmissing-prototypes -Wmissing-declarations -Wwrite-strings -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
main.c:34:18: fatal error: glib.h: No such file or directory
compilation terminated.
make[2]: *** [main.o] Error 1
make[2]: Leaving directory `/home/pi/gmrender-resurrect/src'
make[1]: *** [all-recursive] Error 1
make[1]: Leaving directory `/home/pi/gmrender-resurrect'
make: *** [all] Error 2

# so I am missing glib.h, where do I get that?

$ sudo apt-get install apt-file
$ sudo apt-file update
$ apt-file search /glib.h
libghc-glib-doc: /usr/lib/ghc-doc/haddock/glib-0.12.2/glib.haddock
libgirepository1.0-doc: /usr/share/gtk-doc/html/gi/glib.html
libglib2.0-dev: /usr/include/glib-2.0/glib.h
libglib2.0-doc: /usr/share/doc/libglib2.0-doc/glib/glib.html

# that gives me the answer

$ sudo apt-get install libglib2.0-dev
$ rm config.status config.log
$ ./configure
$ make

(output snipped)
# fails with:

gcc -Wall -Wpointer-arith -Wcast-align -Wmissing-prototypes -Wmissing-declarations -Wwrite-strings -o gmediarender main.o upnp.o upnp_control.o upnp_connmgr.o upnp_transport.o upnp_device.o upnp_renderer.o webserver.o output.o output_dummy.o xmldoc.o xmlescape.o -pthread -lgthread-2.0 -lrt -lglib-2.0
upnp_control.o: In function `set_volume_db':
upnp_control.c:(.text+0xc4c): undefined reference to `exp'
upnp_control.o: In function `set_volume':
upnp_control.c:(.text+0xd6c): undefined reference to `exp'
upnp_control.o: In function `upnp_control_init':
upnp_control.c:(.text+0x100c): undefined reference to `log'
collect2: ld returned 1 exit status
make[2]: *** [gmediarender] Error 1

# when it links the code together it can't find the basic maths
# functions, so need to tell it to use the maths lib (why?!)...

$ ./configure LIBS=-lm
$ make
$ sudo make install
$ gmediarenderer --list-outputs
Supported output modules:
  dummy Dummy output module (default)

# so I have installed it but it's no use because
# there is no gstreamer output to play the music

$ sudo apt-get install gstreamer0.10-alsa
$ sudo apt-get install gstreamer0.10-tools
$ sudo apt-get install libgstreamer0.10-dev

# more examination of the config.log reveals that it also
# didn't find the upnp and libxml2 libraries
# why does it just blindly continue?!

$ sudo apt-get install libupnp-dev
$ sudo apt-get install libxml2-dev

$ rm confifg.status config.log
$ ./configure LIBS=-lm
checking for GLIB... yes
checking for GST... yes
checking for LIBUPNP... yes
checking for LIBXML... yes
$ make clean
$ make
$ sudo make install
$ gmediarender --list-outputs
Supported output modules:
  gst   GStreamer multimedia framework (default)
  dummy Dummy output module
$ gmediarender -f Raspberry
Using output module: gst (GStreamer multimedia framework)
Registering support for 'audio/x-raw-int'
Registering support for 'audio/x-iec958'
gmediarender: output_gstreamer.c:482: output_gstreamer_init: Assertion `player_ != ((void *)0)' failed.
Aborted

# what's going on?  More bits of gstreamer needed?

$ sudo apt-get install gstreamer0.10-ffmpeg
$ gmediarender -f Raspberry
Using output module: gst (GStreamer multimedia framework)
...followed by 100 or so media types it can now render...
gmediarender: output_gstreamer.c:482: output_gstreamer_init: Assertion `player_ != ((void *)0)' failed.
Aborted

# more needed?

$ sudo apt-get install gstreamer0.10-plugins-base
$ sudo apt-get install gstreamer0.10-plugins-good
$ gmediarender -f Raspberry

#...and it runs!

At this point, the Pi appears in Bubbleupnp’s “devices” list as a “renderer”. If I select it and then choose a song from the media server and press play all sorts of log messages appear on the Pi’s console. Unfortunately I tried playing a FLAC file and the sound quality is terrible! Playing an MP3 file doesn’t work – it says I have a missing plugin so I had a look (type “sudo apt-get install gstreamer” and press tab) and saw another one that looked to do with MP3s:

$ sudo apt-get install gstreamer0.10-fluendo-mp3

Now I can play an MP3 file and it also sounds terrible…

I copied an MP3 across onto the Pi from the media server (using scp) and tried playing it with mpg123 and it sounds fine. So what is the problem with the gstreamer rendering?

$ gst-launch-0.10 playbin uri=file:///tmp/song.mp3

This command produces the same rubbish output as via gmrender (as expected), so the problem is in gstreamer. I’ll post this and do some more investigation…

Complete on{X} script to control the Where Clock

After a lot of head scratching I’ve finished the on{X} script to control the Where Clock: making it turn  around when I enter or leave places. Here it is:

// add your locations to this list, use as many decimal places as possible, these are fake  :-) 
var locations = [
    {name: "home", lat: 50.9, lon: -1.3, rad: 1},
    {name: "pilates", lat: 51.9, lon: -1.3, rad: 1},
    {name: "pub", lat: 52.9, lon: -1.3, rad: 1}
    ];
var hostname = "your.hostname.here";
var port = "1234";  // your port here

function move(place) {
    console.log('Entered region: ' + place);
    device.ajax(
    {
      url: "http://" + hostname + ":" + port + "/move?l=" + place,
      type: 'GET',
      headers: {}
    },
    function onSuccess(body, textStatus, response) {
        var msg = device.notifications.createNotification('Clock is set to: ' + place);
        msg.show();
        console.log('Success: ' + textStatus);
    },
    function onError(textStatus, response) {
        var msg = device.notifications.createNotification('Failed to set clock to ' + place + '\n' + textStatus);
        msg.show();
        console.log('Error: ' + textStatus);
    });
}

function build_callback(place) {
    console.log("Building callback for " + place);
    return function() {
        move(place);
    };
}

for (i = 0; i < locations.length; i++) {
    var location = locations[i];
    var region = device.regions.createRegion({
        latitude: location.lat,
        longitude: location.lon,
        name: location.name,
        radius: location.rad
    });
    region.on('enter', build_callback(region.name));
    region.on('exit', build_callback("travelling"));
    device.regions.startMonitoring(region);
}
console.log("Script executed");

// test code follows
/*
var regions = device.regions.getAll();
console.log(regions.length);
for (i = 0; i < regions.length; i++) {
    r = regions[i];
    console.log(r.name + ":" + r.latitude + ':' + r.longitude + ':' + r.radius);
}
var locationSignal = { latitude: 50.919675, longitude: -1.377101 };
regions[0].emit('enter', locationSignal);
regions[0].emit('exit', locationSignal);
regions[1].emit('enter', locationSignal);
regions[1].emit('exit', locationSignal);
regions[2].emit('enter', locationSignal);
regions[2].emit('exit', locationSignal);
*/

The hardest part was the callback (line 45). I’m not that good at Javascript and I’m never sure if things are being passed by value or reference and when they are evaluated. You have to provide a function that will be called when a region is entered or exited. The API for regions shows a callback inline with a “signal” parameter passed to it. The tricky thing is, how to do that in a loop? I want to loop through all the locations defined at the top of the script and set all the entry and exit callbacks for them.

You can’t just do

region.on('enter', move(region.name))

because the “move(region.name)” gets evaluated (called) as the callback is defined so it sends instructions to move to all the places at the beginning (and probably not later). So we need some delay to the evaluation.

You also can’t just do

region.on('enter', function () { move(region.name); });

because all those anonymous callback functions get bound to the same instance of “region”: Javascript (unnecessarily) reuses the object each time it goes round the loop. When “region.name” is evaluated it ends up being whatever the last place you defined was. In this example I would always be at the pub…

(To be fair, the surprisingly good Javascript editor in the on{X} web page does warn you against creating functions in loops, presumably for this very reason.)

The solution is in the code above: set the callback to be the result of the “build_callback” function which returns a function that calls the “move” function with the place already evaluated and fixed.

You might have noticed that in the code above, the regions are defined to have a 1m radius.  I originally set them to be 10m which seems to make more sense. However, although this code seems to be correct now it doesn’t actually work very well.  I went to the shops today and it was only once I was about 3 miles away from home that the phone sent the “travelling” signal (because it realised I was outside the home region) and then it did send the “home” signal at some point when I got back but I missed it.

So I think it’s all technically working now but that on{X} just isn’t up to the job.  I might try with tasker instead but I never got on with tasker’s UI.

Using on{X} to control the Where Clock

I want to be able to send GET requests to the web server on my Raspberry Pi from my phone so that I can have the hand of the Where Clock turn to point at where I am or what I’m doing.

Of course, I can fire up a web browser on my phone and type in the URL of the Pi’s web server but that’s a little inconvenient: it needs to be automatic, driven by my location or other events. That’s where an interesting project from Microsoft comes in: on{X}. You go to that site and sign in with Facebook, download an app to your Android phone and again associate it with Facebook and then, on the web site, you can write little scripts using JavaScript which get sent to your phone and run there.

The API for on{X} is designed so that you can perform actions when certain events happen (hence the name of the system: on{X}) and it’s very easy to use. Here’s my first attempt to get the Where Clock to move, it fires when my phone’s screen is unlocked:

device.screen.on("unlock", function(){
    console.log('Screen unlocked');
    device.ajax(
    {
        url: 'http://raspberry1.scphillips.com:8080/move?a=180',
        type: 'GET',
        headers: {}
    },
    function onSuccess(body, textStatus, response) {
        var msg = device.notifications.createNotification('Clock is set: ' + textStatus);
        msg.show();
        console.log('Success: ' + textStatus);
    },
    function onError(textStatus, response) {
        var msg = device.notifications.createNotification('Error: ' + textStatus);
        msg.show();
        console.log('Error: ' + textStatus);
    });
});

The notification messages pop up in the notification bar on the phone and the console messages go to a log that can be viewed in the on{X} app or on the on{X} website. All the script does is when the screen is unlocked it calls the Raspberry Pi’s web server (using the “ajax” method) and then depending on the response it receives reports success or the error. The move command sent to the server just sends it to 180 degrees so doing it a second time appears to have no effect on the clock.

It is surely just a small step from this to something that moves the hand based on location and anything else I can think of.

Moving the Where Clock from a web browser

Now I’ve got a web server and I’ve got code to control the stepper motor, let’s put them together and control the stepper motor from a web browser!  Here’s the new code: clockServer.py

#!/usr/bin/env python

# This code is written by Stephen C Phillips.
# It is in the public domain, so you can do what you like with it
# but a link to http://scphillips.com would be nice.

import socket
import re
import RPi.GPIO as GPIO
from move import Motor

# Set up stepper-motor:
GPIO.setmode(GPIO.BOARD)
motor = Motor([18,22,24,26])
motor.rpm = 5

# Standard socket stuff:
host = ''  # do we need socket.gethostname() ?
port = 8080
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((host, port))
sock.listen(1)  # don't queue up any requests

# Loop forever, listening for requests:
while True:
    print "Waiting..."
    csock, caddr = sock.accept()
    print "Connection from: " + `caddr`
    req = csock.recv(1024)  # get the request, 1kB max
    req = req.split("\n")[0]
    print "Request: " + req
    # Look in the first line of the request for a move command
    # A move command should be e.g. 'http://server/move?a=90'
    match = re.match('GET /move\?a=(\d+)\sHTTP/1', req)
    if match:
        angle = int(match.group(1))
        print "Angle: " + `angle`
        csock.sendall("HTTP/1.0 200 OK\r\n")
        print "Moving motor..."
        motor.move_to(angle)
    else:
        # If there was no recognised command then return a 404 (page not found)
        print "Returning 404"
        csock.sendall("HTTP/1.0 404 Not Found\r\n")
    csock.close()
    print "--------"

This is pretty much the same as the little web server from the last post, but this time instead of returning a web page when we’re asked to move the motor we just return a 200 code and actually move the motor (lines 36-40). This is very nearly all in place now – I just need to get the phone to call the web server at appropriate moments.

Till then, here’s a little video:

A simple Python webserver

Now I can control the stepper-motor from Python I need to be able to tell the Raspberry Pi to move the motor from my phone so that, for instance, when I get to work it can tell the motor to move the indicator hand to point to “Work”.

To do this, the easiest way seem to me to be using a web server on the Pi.  I want to make it so that if you GET the URL http://<raspberry.server>:8080/move?a=90 then it will move the hand to 90 degrees. This normally requires a web server and a CGI script but it seemed overkill to install Apache or even something lighter-weight such as lighttpd or nginx for such a simple task so I’ve written my own: much more interesting!

#!/usr/bin/env python

# This code is written by Stephen C Phillips.
# It is in the public domain, so you can do what you like with it
# but a link to http://scphillips.com would be nice.

import socket
import re

# Standard socket stuff:
host = ''  # do we need socket.gethostname() ?
port = 8080
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((host, port))
sock.listen(1)  # don't queue up any requests

# Loop forever, listening for requests:
while True:
    csock, caddr = sock.accept()
    print "Connection from: " + `caddr`
    req = csock.recv(1024)  # get the request, 1kB max
    print req
    # Look in the first line of the request for a move command
    # A move command should be e.g. 'http://server/move?a=90'
    match = re.match('GET /move\?a=(\d+)\sHTTP/1', req)
    if match:
        angle = match.group(1)
        print "ANGLE: " + angle + "\n"
        csock.sendall("""HTTP/1.0 200 OK
Content-Type: text/html

<html>
<head>
<title>Success</title>
</head>
<body>
Boo!
</body>
</html>
""")
    else:
        # If there was no recognised command then return a 404 (page not found)
        print "Returning 404"
        csock.sendall("HTTP/1.0 404 Not Found\r\n")
    csock.close()

This is about as simple as a web server gets. It sets up a server socket to listen on using port 8080 and doesn’t queue up any requests (it’s not going to get many…).  When a connection is made from the client (e.g. a web browser) the “sock.accept()” line (19) creates a client socket to communicate with the client on.  We then just read at most 1kB of data from the client, which is the HTTP headers saying what the client wants and using a regular expression (line 25) we see if it is a “move” request and pull out the angle.  If it was a move request then we print the angle to the console and return a simple web page by using “csock.sendall” on line 29.  Otherwise we return a 404 error (line 45).  Finally we close the client socket and loop round to the top again to wait for another connection.

If I run this script (e.g. “python server.py”) and go to the URL “http://<server>:8080/move?a=20″ then I see this on the console of the Pi:

Connection from: ('192.168.1.129', 54340)
GET /move?a=20 HTTP/1.1
Host: raspberry1.scphillips.com:8080
Connection: keep-alive
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.97 Safari/537.11
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
DNT: 1
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-GB,en;q=0.8,en-US;q=0.6
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

ANGLE: 20

Connection from: ('192.168.1.129', 54344)
GET /favicon.ico HTTP/1.1
Host: raspberry1.scphillips.com:8080
Connection: keep-alive
Accept: */*
DNT: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.97 Safari/537.11
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-GB,en;q=0.8,en-US;q=0.6
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

Returning 404

So we first get a connection from IP address 192.168.1.129 (my address on my home network) asking for “/move?a=20″ using HTTP version 1.1.  It then says that it wants that “page” from “Host: raspberry1.scphillips.com:8080″.  This is in case my web browser is talking to a proxy – the proxy needs to know where to get the page from (however, in this case there is no proxy).  There are then a load of other headers that describe what the client is and how it would like information sent to it (language, character set, etc).

The web server ignores all but the first line of the headers, spots that it’s a move request and prints out the angle to the console.  Immediately we then get a second request from the web browser asking for the “favicon.ico” – this just happens automatically.  The favicon is the little icon you see in the address bar of most websites.  Web browsers request them a lot. Our little web server just ignores this request and sends a 404 (page not found) so the web browser is happy.

On the web browser we get a page that says “Boo!”.

Next, this needs linking up with the Motor class from the last post.

A Python class to move the stepper motor

To properly control the stepper motor from the Raspberry Pi we need a class to represent it. This is one of the most direct ways of understanding object oriented programming (OOP): from the class you make an “object” and the object represents and controls an object in the real world (the stepper motor).

The class lets us remember (and control) all the information we need:

  • the pins the motor is connected on;
  • the speed of the motor;
  • the angle the motor is currently pointing to.

The motor seems to have 4096 positions (steps) it can point to.  These don’t correspond exactly to degrees. The class takes an angle we want the motor to point to, converts that into a step position and works out how many steps to turn it by and in what direction to get there.

Actually, it doesn’t move to the step position that is closest to the angle, it chooses step positions that are divisible by 8 because the control sequence has 8 steps in it and I didn’t want to bother starting or stopping in the middle of the sequence.

#!/usr/bin/env python

# This code is written by Stephen C Phillips.
# It is in the public domain, so you can do what you like with it
# but a link to http://scphillips.com would be nice.

# It works on the Raspberry Pi computer with the standard Debian Wheezy OS and
# the 28BJY-48 stepper motor with ULN2003 control board.

from time import sleep
import RPi.GPIO as GPIO

class Motor(object):
    def __init__(self, pins):
        self.P1 = pins[0]
        self.P2 = pins[1]
        self.P3 = pins[2]
        self.P4 = pins[3]
        self.deg_per_step = 5.625 / 64
        self.steps_per_rev = int(360 / self.deg_per_step)  # 4096
        self.step_angle = 0  # Assume the way it is pointing is zero degrees
        for p in pins:
            GPIO.setup(p, GPIO.OUT)
            GPIO.output(p, 0)

    def _set_rpm(self, rpm):
        """Set the turn speed in RPM."""
        self._rpm = rpm
        # T is the amount of time to stop between signals
        self._T = (60.0 / rpm) / self.steps_per_rev

    # This means you can set "rpm" as if it is an attribute and
    # behind the scenes it sets the _T attribute
    rpm = property(lambda self: self._rpm, _set_rpm)

    def move_to(self, angle):
        """Take the shortest route to a particular angle (degrees)."""
        # Make sure there is a 1:1 mapping between angle and stepper angle
        target_step_angle = 8 * (int(angle / self.deg_per_step) / 8)
        steps = target_step_angle - self.step_angle
        steps = (steps % self.steps_per_rev)
        if steps > self.steps_per_rev / 2:
            steps -= self.steps_per_rev
            print "moving " + `steps` + " steps"
            self._move_acw(-steps / 8)
        else:
            print "moving " + `steps` + " steps"
            self._move_cw(steps / 8)
        self.step_angle = target_step_angle

    def _move_acw(self, big_steps):
        GPIO.output(self.P1, 0)
        GPIO.output(self.P2, 0)
        GPIO.output(self.P3, 0)
        GPIO.output(self.P4, 0)
        for i in range(big_steps):
            GPIO.output(self.P1, 0)
            sleep(self._T)
            GPIO.output(self.P3, 1)
            sleep(self._T)
            GPIO.output(self.P4, 0)
            sleep(self._T)
            GPIO.output(self.P2, 1)
            sleep(self._T)
            GPIO.output(self.P3, 0)
            sleep(self._T)
            GPIO.output(self.P1, 1)
            sleep(self._T)
            GPIO.output(self.P2, 0)
            sleep(self._T)
            GPIO.output(self.P4, 1)
            sleep(self._T)

    def _move_cw(self, big_steps):
        GPIO.output(self.P1, 0)
        GPIO.output(self.P2, 0)
        GPIO.output(self.P3, 0)
        GPIO.output(self.P4, 0)
        for i in range(big_steps):
            GPIO.output(self.P3, 0)
            sleep(self._T)
            GPIO.output(self.P1, 1)
            sleep(self._T)
            GPIO.output(self.P4, 0)
            sleep(self._T)
            GPIO.output(self.P2, 1)
            sleep(self._T)
            GPIO.output(self.P1, 0)
            sleep(self._T)
            GPIO.output(self.P3, 1)
            sleep(self._T)
            GPIO.output(self.P2, 0)
            sleep(self._T)
            GPIO.output(self.P4, 1)
            sleep(self._T)

if __name__ == "__main__":
    GPIO.setmode(GPIO.BOARD)
    m = Motor([18,22,24,26])
    m.rpm = 5
    print "Pause in seconds: " + `m._T`
    m.move_to(90)
    sleep(1)
    m.move_to(0)
    sleep(1)
    m.move_to(-90)
    sleep(1)
    m.move_to(-180)
    sleep(1)
    m.move_to(0)
    GPIO.cleanup()

If you save it as “move.py” you can run it using “sudo python move.py”. It will then move the motor round by 90 degrees, pause for 1 second, move it back again and so on according to the instructions at the end of the program. Alternatively, you can import the file and use the class from another program which is what I’ll be doing soon…