Pongo: wireless microbit Python pong

UPDATE: this project’s code is now on Github.

I love the wireless capabilities of Python on the BBC microbit and I’ve been using it with some success in my Year 8 classes.

I thought I’d have a go at writing a wireless Pong game in Python – it took me a lot longer than I expected for various reasons. I really wanted to have the same code running on both microbits, but I soon abandoned that as too complex. Much easier to have one microbit – Player A – controlling the game and deciding who gets a point and when. Player B is the ‘slave’ only sending its left and right paddle moves back to Player A and mirroring (literally) Player A’s screen.

I was keen to have each screen the same – rather than extending a long screen like a wired version I’ve seen. This is because I want each player to be able to be quite far apart, so seeing the other player’s screen isn’t necessary.

How to play

Flash Player A code (below) on to one Microbit using Mu, and Player B on to a separate microbit. You can optionally connect a headphone or buzzer to pins 0 and 1 on each microbit for some audio feedback joy.

Power up Player B first – it will wait for messages from Player A. Then power up Player A. The game starts straight away with the ball – the bright dot in the middle of the screen – moving in a random direction. Move your paddle left and right using A and B buttons. If you fail to hit the ball when it reaches your end the other player gets a point (points tallies are not shown on the screen) and the first player to 5 points wins. To play again you both need to press the reset button on the back of the microbits.

How it works

Player B is the easy one to explain. It runs a loop constantly polling for messages and keypresses. If you press button A to move left, or B to move right, it sends a message with your bat’s new position. It also listens for different kinds of messages from player A’s microbit. They all start with different code letters:
p + a number is the position of player A’s bat.
x and y messages give the current location of the ball, which is then inverted using a dictionary look-up table called ‘bat_map‘.
a and b messages give the respective scores or player A and B.

If a player reaches the winning score (5) it breaks out of the loop and plays a happy song (Nyan cat) if player B has won and a sad song (funeral march) is player A has won.

Player A is the master controller. It picks a random direction for the ball to start moving and bounces the ball if it hits any of the sides. If it hits the top or bottom and a player’s bat isn’t in the way, the other player gets a point. It has a crude timer using variables counter and delay – every time it reaches 1000 it moves the ball (I couldn’t work out how to get proper timers to work in Microbit Python – if indeed this is possible). If a player hits the ball with their bat it speeds up a bit.

It sends messages (as described above) to Player B with the ball position, score and player A bat position. The game ends in the same way as player B’s code described above, except you get the happy tune if player A wins and the sad one if player B wins.

How to mod

You can make the game faster by making the value of delay smaller. You can also make it last longer by increasing the value of winning_score in both sets of code.

A nice extension would be to add more sound (when you hit the ball for example) and to add levels with the game getting faster each time someone wins a game.

Let me know how you get on with it and if you have any other ideas for improvements – the physics of the ball bouncing is one area that I could do with help on!

Here’s the player A code:

# Pongo by @blogmywiki
# player A code - with points

import radio
import random
from microbit import *
from music import play, POWER_UP, JUMP_DOWN, NYAN, FUNERAL

a_bat = 2              # starting position of player A bat
b_bat = 2              # starting position of player B bat
bat_map = {0: 4, 1: 3, 2: 2, 3: 1, 4: 0}
ball_x = 2             # starting position of ball
ball_y = 2
directions = [1, -1]   # pick a random direction for ball at start
x_direction = random.choice(directions)
y_direction = random.choice(directions)
delay = 1000           # used as a crude timer
counter = 0            # used as a crude timer
a_points = 0
b_points = 0
winning_score = 5
game_over = False

def move_ball():
    global ball_x, ball_y, x_direction, y_direction, counter, a_bat, b_bat, a_points, b_points, delay
    display.set_pixel(ball_x, ball_y, 0)
    ball_x = ball_x + x_direction
    ball_y = ball_y + y_direction
    if ball_x < 0:							# bounce if hit left wall
        ball_x = 0
        x_direction = 1
    if ball_x > 4:							# bounce if hit right wall
        ball_x = 4
        x_direction = -1
    if ball_y == 0:
        if ball_x == b_bat:					# bounce if player B hit ball
            ball_y = 0
            y_direction = 1
            delay -= 50						# speed up after bat hits
        else:
            play(POWER_UP, wait=False)      # A gets point if B missed ball
            a_points += 1
            ball_y = 0
            y_direction = 1
            radio.send('a'+str(a_points))			# transmit points

    if ball_y == 4:							# bounce if player A hits ball
        if ball_x == a_bat:
            ball_y = 4
            y_direction = -1
            delay -= 50						# speed up after bat hits
        else:
            play(JUMP_DOWN, wait=False)     # player B gets point if A misses
            b_points += 1
            ball_y = 4
            y_direction = -1
            radio.send('b'+str(b_points))
    counter = 0
    radio.send('x'+str(ball_x))				# transmit ball position
    radio.send('y'+str(ball_y))

radio.on()    # like the roadrunner

while not game_over:
    counter += 1
    display.set_pixel(a_bat, 4, 6)        # draw bats
    display.set_pixel(b_bat, 0, 6)
    display.set_pixel(ball_x, ball_y, 9)  # draw ball
    if button_a.was_pressed():
        display.set_pixel(a_bat, 4, 0)
        a_bat = a_bat - 1
        if a_bat < 0:
            a_bat = 0
        radio.send('p'+str(a_bat))
    if button_b.was_pressed():
        display.set_pixel(a_bat, 4, 0)
        a_bat = a_bat + 1
        if a_bat > 4:
            a_bat = 4
        radio.send('p'+str(a_bat))
    incoming = radio.receive()
    if incoming:
        display.set_pixel(b_bat, 0, 0)
        b_bat = bat_map[int(incoming)]
    if counter == delay:
        move_ball()
    if a_points == winning_score or b_points == winning_score:
        game_over = True

if a_points > b_points:
    play(NYAN, wait=False)
    display.scroll('A wins!')
else:
    play(FUNERAL, wait=False)
    display.scroll('B wins!')

display.scroll('Press reset to play again')

Here’s the Player B code:

# Pongo by @blogmywiki
# player B code

import radio
from microbit import *
from music import play, POWER_UP, JUMP_DOWN, NYAN, FUNERAL

a_bat = 2              # starting position of player A bat
b_bat = 2              # starting position of player B bat
bat_map = {0: 4, 1: 3, 2: 2, 3: 1, 4: 0}
ball_x = 2             # starting position of ball
ball_y = 2
a_points = 0
b_points = 0
winning_score = 5
game_over = False
radio.on()     # like the roadrunner

def parse_message():
    global a_bat, incoming, bat_map, ball_x, ball_y, a_points, b_points
    msg_type = incoming[:1]    # find out what kind of message we have received
    msg = incoming[1:]         # strip initial letter from message
    if msg_type == 'p':
        display.set_pixel(a_bat, 0, 0)
        their_bat = int(msg)     # mirror their bat position
        a_bat = bat_map[their_bat]
    if msg_type == 'x':
        display.set_pixel(ball_x, ball_y, 0)
        ball_x = bat_map[int(msg)]
    if msg_type == 'y':
        display.set_pixel(ball_x, ball_y, 0)
        ball_y = bat_map[int(msg)]
    if msg_type == 'a':
        a_points = int(msg)
        play(JUMP_DOWN, wait=False)
    if msg_type == 'b':
        b_points = int(msg)
        play(POWER_UP, wait=False)

while not game_over:
    display.set_pixel(b_bat, 4, 6)
    display.set_pixel(a_bat, 0, 6)
    display.set_pixel(ball_x, ball_y, 9)  # draw ball
    if button_a.was_pressed():
        display.set_pixel(b_bat, 4, 0)
        b_bat = b_bat - 1
        if b_bat < 0:
            b_bat = 0
        radio.send(str(b_bat))
    if button_b.was_pressed():
        display.set_pixel(b_bat, 4, 0)
        b_bat = b_bat + 1
        if b_bat > 4:
            b_bat = 4
        radio.send(str(b_bat))
    incoming = radio.receive()
    if incoming:
        parse_message()
    if a_points == winning_score or b_points == winning_score:
        game_over = True

if a_points < b_points:
    play(NYAN, wait=False)
    display.scroll('B wins!')
else:
    play(FUNERAL, wait=False)
    display.scroll('A wins!')
Posted in computers, microbit | Tagged , | Leave a comment

Simple Microbit wireless data class activities

I’ve been very impressed with the radio module in microbit Python. As an extension to a year 8 microbit lesson I got two of my pupils to test out the awesome firefly project. There was a very magical look of amazement on one girl’s face as she realised she was pushing a button on her microbit and making another one on the other side of the room glow. I then got them to test out the range by walking out the classroom and down the corridor – we ran out of school before it stopped working!

I’ve already written a simple wireless Morse code project as an update of the wired one we used in class last year.

Here’s what I’m going to try next lesson… First I’ll get the pupils to load their microbits with some code to receive messages:

import radio
from microbit import *
radio.on()

while True:
    incoming = radio.receive()
    if incoming:
        display.scroll(incoming)
    sleep(100)

Then I will broadcast messages to them all from mine using this code (which they won’t see):

import radio
from microbit import *
radio.on()

while True:
    if button_a.was_pressed():
        display.show('A')
        radio.send('Who are you?')
    if button_b.was_pressed():
        display.show('B')
        radio.send('My name is Mr Booth')
    sleep(100)

Then, hopefully after I see my message scrolling across many of the microbit, I will ask what the problem is if we want to use them to communicate with each other. Hopefully this will elicit the response that we need some way of directing messages to individuals – using channels or some form of addressing (plus they have no way of sending a message yet!)

I will then hand out pieces of paper with numbers between 0 and 100 on them, making sure I have 2 copies of each number – preferably given to pupils sat far apart so they won’t know who they’re messaging. (I’ll tell them at this point that I’m monitoring all the messages and numbers even though I’m not to cut down on any nonsense!)

So a pupil with number 20 at the front of the class should now be able to message another pupil at the back of the class with the same number using code like this slotting in their given number as the channel number:

import radio
from microbit import *
radio.config(channel=20)
radio.on()

while True:
    if button_a.was_pressed():
        display.show('A')
        radio.send('Who are you?')
    if button_b.was_pressed():
        display.show('B')
        radio.send('My name is Sam')
    incoming = radio.receive()
    if incoming:
        display.scroll(incoming)
    sleep(100)

They could then develop the idea by seeing how close the channels need to be before they interfere with each other. Could they add code to change the channel somehow, perhaps using the shake gesture to select a random channel number in multiples of 10 shown on the display? They can message random people – in a class of 30 you’d get roughly 3 people on the same channel:

import radio
import random
from microbit import *
radio.config(channel=20)
radio.on()

while True:
    if accelerometer.was_gesture('shake'):
        new_channel = random.randrange(10,60,10)
        display.scroll(str(new_channel))
        radio.config(channel=new_channel)
    if button_a.was_pressed():
        display.show('A')
        radio.send('Who are you?')
    if button_b.was_pressed():
        display.show('B')
        radio.send('My name is Sam')
    incoming = radio.receive()
    if incoming:
        display.scroll(incoming)
    sleep(100)

A further extension (or for older groups) could focus on the group addressing features not explored in this simple project.

The plenary will be a discussion of how different text-based methods of communication work – SMS and internet-based services plus radio communication using walkie-talkies.

If you want to try this all you need are 2 microbits and the awesome Mu editor. Let me know if you try something like this or have ideas to develop this.

UPDATE – REFLECTION

I’ve now taught this lesson to a small class of Year 8 pupils and here’s what I discovered…

- Nicolas Tollervey’s firefly project worked really well in class. I had hoped to trigger the glowing under my control but kids being kids they fiddled and pressed the buttons so as they all flashed the code ALL the microbits began to glow. I got them to all press the reset button then turned the lights off, got them to hold them up and asked 1 girl to push button A. Then they all started glowing. Quite a wow factor. I got a lovely bit of knowledge about fireflies from 1 girl who told me that fireflies have a unique pattern to their lights, which leads nicely on to addressing and channels.

- The bit where I broadcast a message worked quite well. About half of the girls got the code typed in without errors and
could see my broadcast message. One questioned the point of it, but that moved us on to what was missing – any way for them to reply and addressing.

- Very few girls got the code to send their own message working. We ran out of time (we had about an hour for the whole lesson), plus there were too many syntax errors caused by typing errors. If they are to type the Python code themselves they need to have recently done Python work and/or a debugging lesson – these girls were coming to it cold after a break of several months. I possibly should have given them code to copy and paste.

- I had some more problems with our VDI and/or Mu seemingly forgetting where the microbits were but not knowing it didn’t know where they were. The symptom is that it says it’s flashing but nothing happens. Still not got to the bottom of this, but I was working with a pupil when this happened and she had not unplugged the device.

Conclusion: the firefly part of the lesson was the most successful! The rest of the lesson can work better with some tweaks outlined above. Will do this again next term!

Posted in education, ICT | Tagged , | Leave a comment

Scratch advent calendar projects

Here’s a Christmas-themed Scratch project that I am going to do with my Year 7s. We’re going to make advent calendars. It can be differentiated several ways:

First project is a simple calendar where you click on a numbered door to change its costume and reveal a surprise – there are loads of Christmassy pictures in the ‘holiday’ category on Scratch. Pupils can make their own from, ahem, scratch or use a supplied framework. They can shuffle the doors up, add sounds, animation, change the background – whatever they want.

The second project is a bit cleverer. This uses the ‘days since 2000′ block to work out when each day has happened and opens the door automatically. You could get your class to work out the numbers for this themselves, or give them a hint – either the number for 1st Dec (I think it’s 6179 days for 1st Dec 2016) or get them to print today’s day number and work it out from there.

Posted in Uncategorized | Tagged , | 2 Comments

Updated SenseHAT weather station

Still pondering how best to improve my simple Raspberry Pi SenseHAT weather station, but I thought I’d share some improvements.

The code now keeps track of highs and lows over the period it’s been logging and displays them on the chart along with the current readings for temperature, humidity and air pressure.

This FTPs the chart and data in a CSV file to a remote web server for display on a web page and it’s also designed to run headless at startup, so it doesn’t need a windowed environment running to plot the graph. It will also show on the SenseHAT display when it’s uploading a file and if there’s been an FTP problem.

The downside of this is, of course, the code is much longer…

#!/usr/bin/python

# v5 runs headless, info on LEDs, FTPs CSV file & only charts last 24 hrs
# v4 FTPs chart to remote web server
# v3 adds real timestamping and logging to a CSV file
import matplotlib
matplotlib.use('Agg')   # used as running headless without windows
import matplotlib.pyplot as plt
from time import sleep
from sense_hat import SenseHat
import time
from datetime import datetime
import os
import ftplib

def getCPUtemperature():
 res = os.popen('vcgencmd measure_temp').readline()
 return(res.replace("temp=","").replace("'C\n",""))

sense = SenseHat()

interval_min = 5 # measurement interval in minutes
hrs_shown = 24  # number of hours covered by chart
chart_span = int(hrs_shown*(60/interval_min)) # calculate number of readings to show

# uncomment these lines to write headers to CSV file
#fd = open('logging.csv','a')
#fd.write('time,pressure,temperature,humidity\n')
#fd.close()

while True:
    pressure_list = []
    temp_list = []
    humidity_list = []
    x = []
    a = 0
    daily_max_humidity = 0
    daily_max_temp = 0
    daily_max_pressure = 0
    daily_min_humidity = 2000
    daily_min_temp = 2000
    daily_min_pressure = 2000

    for a in range (chart_span):
        sense.clear()
        fd = open('/home/pi/Desktop/logging.csv','a')
        pressure = sense.get_pressure()-1000
        pressure_list.append(pressure)
    # attempt to calculate ambient temperature
    # based on dgaust in https://www.raspberrypi.org/forums/viewtopic.php?f=104&t=111457
        cpuTemp=int(float(getCPUtemperature()))
        ambient = sense.get_temperature_from_pressure()
        calctemp = ambient - ((cpuTemp - ambient)/ 1.5)
        temp_list.append(calctemp)

        humidity = sense.get_humidity()
        humidity_list.append(humidity)

        timestamp = str(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
        fd.write(timestamp+','+str(pressure)+','+str(calctemp)+','+str(humidity)+'\n')
        fd.close()
#        print(timestamp, pressure, calctemp, humidity)

        x.append(a)
        a = a + 1

        if humidity > daily_max_humidity:
            daily_max_humidity = humidity
        if calctemp > daily_max_temp:
            daily_max_temp = calctemp
        if pressure > daily_max_pressure:
            daily_max_pressure = pressure
        if humidity < daily_min_humidity:
            daily_min_humidity = humidity
        if calctemp < daily_min_temp:
            daily_min_temp = calctemp
        if pressure < daily_min_pressure:
            daily_min_pressure = pressure

        print('max humidity '+str(daily_max_humidity))
        print('max temp '+str(daily_max_temp))
        print('max pressure '+str(daily_max_pressure))
        print('min humidity '+str(daily_min_humidity))
        print('min temp '+str(daily_min_temp))
        print('min pressure '+str(daily_min_pressure))

        fig = plt.figure()
        plt.plot(x,humidity_list)
        plt.plot(x,temp_list,'r')
        plt.plot(x,pressure_list,'g')
        plt.title('@blogmywiki SenseHAT WX '+timestamp)
        plt.figtext(0, 0.9, "max "+str(round(daily_max_humidity,0))+"%",color='blue',fontsize=8)
        plt.figtext(0, 0.85, "min "+str(round(daily_min_humidity,0))+"%",color='blue',fontsize=8)
        plt.figtext(0, 0.8, "max "+str(round(daily_max_temp,0))+"C",color='red',fontsize=8)
        plt.figtext(0, 0.75, "min "+str(round(daily_min_temp,0))+"C",color='red',fontsize=8)
        plt.figtext(0, 0.7, "max "+str(round(daily_max_pressure,0)+1000)+"mb",color='green',fontsize=8)
        plt.figtext(0, 0.65, "min "+str(round(daily_min_pressure,0)+1000)+"mb",color='green',fontsize=8)
        plt.figtext(0, 0.04, "current humidity "+str(round(humidity,0))+"%",color='blue',fontsize=10)
        plt.figtext(0.25, 0.04, "temp "+str(round(calctemp,1))+"$^\circ$C",color='red',fontsize=10)
        plt.figtext(0.4, 0.04, "pressure "+str(round(pressure,1)+1000)+" mb",color='green',fontsize=10)
        fig.savefig('/home/pi/Desktop/wx_chart6.png')
        sense.show_message("Updating", text_colour=[0,80,0])

        try:
            session = ftplib.FTP('FTP-SERVER-ADDRESS','FTP-USERNAME','FTP-PASSWORD')
            file = open('/home/pi/Desktop/wx_chart6.png','rb')          # open chart file
            session.storbinary('STOR /SERVER_PATH/wx_chart.png', file)  # send the file
            file.close()                                                # close file
            file = open('/home/pi/Desktop/logging.csv','rb')            # open csv file
            session.storbinary('STOR /SERVER_PATH/logging.csv', file)   # send the file
            file.close()
            session.quit()
            sense.show_letter(".",text_colour=[0, 80, 0])   # green dot to show it's running & FTP'd ok
        except ftplib.all_errors as e:
            print(e)
            sense.show_message("FTP error", text_colour=[80,0,0])
            sense.show_letter(".",text_colour=[80, 0, 0])  # red dot to show it's running but not FTP'd
        sleep(interval_min*60)
Posted in Raspbian | Tagged , | 2 Comments

Simple Raspberry Pi weather station


Chart showing the weather in my lounge overnight! Can you spot when the central heating came on? Also note the correlation between temperature and humidity. Numbers refer to blocks of 5 minutes.

I previously blogged about some very simple Python you can use to do to live data logging and graphing of environmental factors like air pressure, humidity and (sort of) temperature using the Raspberry Pi computer and a SenseHAT module.

Well, the project’s evolved a bit over the last day or so and this 4th iteration is becoming more like a weather station. I know this is not an original idea, but I wanted to have a go at writing all the code myself as I want to build a weather station in my school and this would be a good ‘proof of concept’ project. Ideally I’d like the pupils to design as much as possible, this could be used to show them the kinds of things you can do with snap-together parts and relatively few lines of code.

This version still draws a live(ish) chart on the Pi’s screen but now…

  • It logs readings to a time-stamped CSV file – this means you can analyse the data later using a spreadsheet program or similar.
  • It saves a PNG image file of the chart which it then sends by FTP – for example, to update a chart on a web site.
  • The chart displays the current readings at the bottom and timestamps the title

Drawing charts from the saved CSV file using a spreadsheet is easy. This is using LibreOffice on the Pi itself but you could also use Microsoft Excel or Apple Numbers.

There’s much to do with this still. It keeps drawing the graph over bigger and bigger timescales, and I I guess it will eventually run out of memory as the list gets too long, so I need to sort that out. And it would be nice to also FTP the CSV file, include some daily high/low information, have proper time stamps on the chart, make use of the SenseHAT’s LED display/buttons in some way and make it run headless.

I also need to do some tests to see how accurate the temperature fudge might be – perhaps I should also log the SenseHAT’s temperature data as well as the estimated ambient temperature?

Here’s the code. It needs to run in Python 3, a connected SenseHAT and the matplotlib Python library installing.

# v4 FTPs chart to remote web server
# v3 this adds real timestamping and logging to a CSV file
import matplotlib.pyplot as plt
from time import sleep
from sense_hat import SenseHat
import time
from datetime import datetime
import os

import ftplib

def getCPUtemperature():
 res = os.popen('vcgencmd measure_temp').readline()
 return(res.replace("temp=","").replace("'C\n",""))

sense = SenseHat()
plt.ion()
pressure_list = []
temp_list = []
humidity_list = []
x = []
a = 0
# uncomment these lines to write headers to CSV file
#fd = open('logging.csv','a')
#fd.write('time,pressure,temperature,humidity\n')
#fd.close()

while True:
    fd = open('logging.csv','a')
    pressure = sense.get_pressure()-1000
    pressure_list.append(pressure)
# attempt to calculate ambient temperature
# based on dgaust in https://www.raspberrypi.org/forums/viewtopic.php?f=104&t=111457
    cpuTemp=int(float(getCPUtemperature()))
    ambient = sense.get_temperature_from_pressure()
    calctemp = ambient - ((cpuTemp - ambient)/ 1.5)
    temp_list.append(calctemp)

    humidity = sense.get_humidity()
    humidity_list.append(humidity)

    timestamp = str(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    fd.write(timestamp+','+str(pressure)+','+str(calctemp)+','+str(humidity)+'\n')
    fd.close()
    print(timestamp, pressure, calctemp, humidity)

    x.append(a)
    a = a + 1

    plt.clf()
    plt.plot(x,humidity_list)
    plt.plot(x,temp_list,'r')
    plt.plot(x,pressure_list,'g')
    plt.title('@blogmywiki SenseHAT WX '+timestamp)
    plt.figtext(0.2, 0.04, "humidity "+str(round(humidity,0))+"%",color='blue')
    plt.figtext(0.45, 0.04, "temp "+str(round(calctemp,1))+"$^\circ$C",color='red')
    plt.figtext(0.65, 0.04, "pressure "+str(round(pressure,1)+1000)+" mb",color='green')
    plt.savefig('wx_chart.png')
    plt.draw()

    try:
        session = ftplib.FTP('FTP-ADDRESS-HERE','FTP-USERNAME','FTP-PASSWORD')
        file = open('/home/pi/Desktop/wx_chart.png','rb')           # file to send
        session.storbinary('STOR WEB-SERVER-PATH-HERE/wx_chart.png', file)     # send the file
        file.close()                                                # close file and FTP
        session.quit()
    except ftplib.all_errors as e:
        print(e)

    sleep(300)

UPDATE – I’ve got a new version here that runs headless and shows max/min data.

Posted in Raspberry Pi, Raspbian | Tagged , , | Leave a comment