Introduction

I attend a lot of conferences on FPGA and Embedded Design, so far this year I have attended seven on both sides of the Atlantic. Along with good sessions in the demo areas, conference attendees like two things interesting demonstrations and cool SWAG.

I thought it would be a good idea to combine these two using the Ultra96v2 and the PYNQ framework to create a spinning prize wheel demonstration which can be used for giving away prizes.

Conceptually this will be very simple to implement requiring

Random number of iterations per spin

Modifiable Rules

Modifiable List of Prizes

While the target application has been the Ultra96v2 I also want this design to be capable of running on any PYNQ board including those without support for a desktop. Note this project was built on PYNQ V2.4

To implement this application we are going to use the Python and Pygame. if you are not familiar with it pygame is a number of Python libraries which support video game creation. Pygame has been around for about 18 years and seems to have a pretty active development community, many of who might be interested in the capabilities that PYNQ opens up to them.

Sharing a Desktop

The first thing to do is set up our development PC such that we can capture video streams created by our target as we develop for it. This way we can run the application either directly on the Ultra96 or over a SSH-X tunnel.

If we are using a windows based development machine then we will need to download and install a X-11 viewer. For this project I used Xming which allows me to capture windows created on the Ultra96v2 on my development machine.

I downloaded the application from https://sourceforge.net/projects/xming/

Once downloaded the installation is very simple, we just select the components required and define the version of PuTTY we are using. For this application I am using a normal PuTTY SSH Client.

Installing XMing

When we connect with PuTTY to our Ultra96v2 along with setting the IP address, username and password. We also want to configure the X11 settings under the SSH options, here we enable X11 forwarding and set the display location to be

localhost:0.0

Putty Configuration when using X11

Of course the application will be slower over a network than if we are using the desktop directly on the Ultra96.

Installing Pygame

The next step is to connect our Ultra96 to the internet using the WIFI and install the necessary packages. On the Ulra96v2 we can connect to WIFI using the WIFI notebook in Jupyter. We can connect the PYNQ jupyter installation by connecting the Ultra96v2 directly to our development machine using the Upstream MicroB connector.

Ultra96v2 Interfacing

Connecting over USB in this manner enables us to use the device as a Ethernet Gadget and work with the Jupyter environment.

Once the PYNQ image is created we can open the Jupyter notebooks by opening a browser and navigating to

192.168.3.1

This will open up the Jupyter notebook environment, under the common folder you will see a WIFI notebook open this with your WIFI settings to hand.

Jupyter Notebook Environment

Enter the SSID and Passphrase when prompted by the notebook.

WIFI Notebook

Once we are connected to the internet, we can then open a terminal window and download the packages which are necessary.

Connect to internet and get packages we require these include

sudo apt-get install python3-tk



sudo apt-get build-dep python-pygame



python3 -m pip install -U pygame

If you have not seen it before the apt-get build_dep is very powerful it will determine the dependencies required, and if they are missing download them to our Ultra96v2.

installing the Python3-TK

Once installed we are ready to begin writing our application.

Basic Functionality

The next step is to create the frame work using pygame, this means that we can draw the prize wheel outline and rotate it as required.

Pygame works on the princple of a screen and surfaces. We can then update surfaces are required with our drawings, graphics and text.

Once we have drawn this circle we can then rotate the wheel as required, to mimic the rotation.

The first thing we need to do is set up the screen size using the FRAME_X and FRAME_Y settings.

On to this frame I am going to create a new surface for the wheel and add this to the center of the frame. I control the size of the surface using the DIAL_Y and DIAL_X parameters.

I also set the radius of the circle that will be the wheel.

FRAME_X = 1000

FRAME_y = 1000

DIAL_X = 600

DIAL_Y = 600

RADIUS = 250

CX = DIAL_X/2

CY = DIAL_Y/2

SEGMENTS = 20

pygame.init()

display_surf = pygame.display.set_mode((FRAME_X, FRAME_y))

pygame.display.set_caption('Adiuvo Engineering & Training Ltd Spin Wheel')

background_colour = (255,255,255)

display_surf.fill(background_colour)

image_surf = pygame.Surface((DIAL_X, DIAL_Y))

image_surf.fill(background_colour)

w, h = image_surf.get_size()

pos = (FRAME_X/2, FRAME_y/2)

pygame.draw.line(display_surf, (0, 0, 0), (display_surf.get_width()/2,180),(display_surf.get_width()/2,200), 4)

To draw the wheel I used pygame.drawfx.pie fucntion this allows us to draw pie chart shapes. Although sadly they are not filled a solid colour, we will address how we do that later.

I want 20 segments in my wheel so each segement covers 18 degrees. As this is repetitive I can use a for loop to draw the circle.

step = 360 / SEGMENTS

accum = 0

x = 0

z = 0

y = 0

for x in range(SEGMENTS):

if x == 0:

pygame.gfxdraw.pie(image_surf, int(CX),int(CY), RADIUS, int(accum), int(accum+ste p),(255,255,255))

else:

pygame.gfxdraw.pie(image_surf, int(CX),int(CY), RADIUS, int(accum), int(accum+step),(255,255,255))

Once the circle is drawn we want to be able to rotate the circle. To do this we can use the pygame.transform.rotate function. when we do this we have to be careful we pivot the image around its center.

def blitRotate(surf, image, pos, originPos, angle):

w, h = image.get_size()

box = [pygame.math.Vector2(p) for p in [(0, 0), (w, 0), (w, -h), (0, -h)]]

box_rotate = [p.rotate(angle) for p in box]

min_box = (min(box_rotate, key=lambda p: p[0])[0], min(box_rotate, key=lambda p: p[1])[1])

max_box = (max(box_rotate, key=lambda p: p[0])[0], max(box_rotate, key=lambda p: p[1])[1])

pivot = pygame.math.Vector2(originPos[0], -originPos[1])

pivot_rotate = pivot.rotate(angle)

pivot_move = pivot_rotate - pivot

origin = (pos[0] - originPos[0] + min_box[0] - pivot_move[0], pos[1] - originPos[1] - max_box[1] + pivot_move[1])

rotated_image = pygame.transform.rotate(image, angle)

surf.blit(rotated_image, origin)

We also need to create button which when clicked rotates starts the rotation. In this application I am going to generate a random number when the button is clicked. This is the number of times the image will rotate, just like a real wheel the closer it gets to finishing its completion the slower the wheel will rotate.

The class below will enable me to create a button

class Button():

def __init__(self, txt, location, action, bg=GOLD, fg=BLACK, size=(200, 100), font_name="Segoe Print", font_size=50):

self.color = bg

self.bg = bg

self.fg = fg

self.size = size

self.font = pygame.font.SysFont(font_name, font_size)

self.txt = txt

self.txt_surf = self.font.render(self.txt, 1, self.fg)

self.txt_rect = self.txt_surf.get_rect(center=[s//2 for s in self.size])

self.surface = pygame.surface.Surface(size)

self.rect = self.surface.get_rect(center=location)

self.call_back_ = action

def draw(self):

self.mouseover()

self.surface.fill(self.bg)

self.surface.blit(self.txt_surf, self.txt_rect)

display_surf.blit(self.surface, self.rect)

def mouseover(self):

self.bg = self.color

pos = pygame.mouse.get_pos()

if self.rect.collidepoint(pos):

self.bg = GOLD # mouseover color

def call_back(self):

self.call_back_()

We can then use this button in the design, all we need to do now is write the function which defines the number of rotations

def spin():

iteration = randint(0, 255)

i=0

angle = 0

print("spin")

while i <= iteration:

if i == iteration:

pygame.draw.line(display_surf, (0, 0, 0), (display_surf.get_width()/2,180),(display_surf.get_width()/2,300), 4)

font = pygame.font.SysFont('Times New Roman', 50)

text = font.render("Winner!!!", True, GOLD)

textRect = text.get_rect()

textRect.center = (display_surf.get_width()/2,150)

display_surf.blit(text, textRect)

return

#print("complete")

elif i < int(iteration*0.85):

blitRotate(display_surf, image_surf, pos, (w/2, h/2), angle)

angle += 30

time.sleep(0.1)

pygame.display.flip()

i += 1

elif i < int(iteration*0.95):

blitRotate(display_surf, image_surf, pos, (w/2, h/2), angle)

angle += 20

time.sleep(0.3)

pygame.display.flip()

i += 1

else:

blitRotate(display_surf, image_surf, pos, (w/2, h/2), angle)

angle += 10

time.sleep(0.6)

pygame.display.flip()

i += 1

Putting all this together means the wheel rotated as below when the button was clicked.

Video of the first rotations

Completing the Application

Obviously the next thing that we want to do is color the elements of the wheel in. As there is no fill pie function we need to draw a number of lines next to each other to fill in the pie.

The coloring of the segments is important as they will relate to the prizes available. This application I want to have

One - Special Prize

Two - Premium Prizes

Three - Medium Prizes

Six - Easy Prizes

Eight - Very Easy Prizes

Each one of these prize groups will be colored a differently on the wheel. Of course, the Special Prize will be colored gold, the premium prizes will be purple, the medium prizes green while the Easy and Very Easy prizes will be blue and red respectively.

# Draw pie segment

if len(p) > 1:

#print("pie sgement %s " % (x))

if x == 1 or x == 3 or x== 7 or x == 9 or x == 11 or x == 13 or x == 17 or x == 19:

#]print("green")

pygame.draw.polygon(image_surf, (255, 0, 0), p)

elif x == 4 or x == 6 or x== 8 or x == 12 or x == 14 or x == 16:

pygame.draw.polygon(image_surf, (0, 0, 255), p)

elif x == 2 or x == 10 or x== 18:

pygame.draw.polygon(image_surf, (0, 255, 0), p)

elif x == 5 or x == 15:

pygame.draw.polygon(image_surf, (128, 0, 128), p)

#elif x == 4 or x == 14 or x== 19:

# pygame.draw.polygon(image_surf, (128, 0, 128), p)

elif x == 0:

pygame.draw.polygon(image_surf, (197, 179, 88), p)

Once these segments have been filled the next step is to identify to the user which prizes equate with which color. To enable flexibility I am going to use a separate text file which contains the prizes, this means anyone can change the prizes on offer.

Each of the prizes will be displayed and will be colored in the same as its associated segment. This makes identification of the winning segment and the associated prize a pretty simple method.

I also read in and display a rules text file this allows me to be able to display any special rules. For example the special prize only being available at set times and not throughout the day.

We can use the font rendering abilities of pygame to do this

font = pygame.font.SysFont('Times New Roman', 15)

text = font.render(str(rules[0][:-1]), True, (255, 0, 0))

textRect = text.get_rect()

textRect.center = (200,970)

display_surf.blit(text, textRect)

The final application looks like the below

Completed Prize Wheel Application

The complete code can be seen below

import pygame

import pygame.gfxdraw

import time

import math

from random import randint

from pygame.locals import *

WHITE = (255, 255, 255)

GREY = (200, 200, 200)

BLACK = (0, 0, 0)

GOLD = (197, 179, 88)

f = open("prizes.txt", "r")

prizes = f.readlines()

f.close()

f = open("Rules.txt", "r")

rules = f.readlines()

f.close()

class Button():

def __init__(self, txt, location, action, bg=GOLD, fg=BLACK, size=(200, 100), font_name="Segoe Print", font_size=50):

self.color = bg

self.bg = bg

self.fg = fg

self.size = size

self.font = pygame.font.SysFont(font_name, font_size)

self.txt = txt

self.txt_surf = self.font.render(self.txt, 1, self.fg)

self.txt_rect = self.txt_surf.get_rect(center=[s//2 for s in self.size])

self.surface = pygame.surface.Surface(size)

self.rect = self.surface.get_rect(center=location)

self.call_back_ = action

def draw(self):

self.mouseover()

self.surface.fill(self.bg)

self.surface.blit(self.txt_surf, self.txt_rect)

display_surf.blit(self.surface, self.rect)

def mouseover(self):

self.bg = self.color

pos = pygame.mouse.get_pos()

if self.rect.collidepoint(pos):

self.bg = GOLD # mouseover color

def call_back(self):

self.call_back_()

FRAME_X = 1000

FRAME_y = 1000

DIAL_X = 600

DIAL_Y = 600

RADIUS = 250

CX = DIAL_X/2

CY = DIAL_Y/2

SEGMENTS = 20

pygame.init()

display_surf = pygame.display.set_mode((FRAME_X, FRAME_y))

pygame.display.set_caption('Adiuvo Engineering & Training Ltd Spin Wheel')

background_colour = (255,255,255)

display_surf.fill(background_colour)

image_surf = pygame.Surface((DIAL_X, DIAL_Y))

image_surf.fill(background_colour)

w, h = image_surf.get_size()

pos = (FRAME_X/2, FRAME_y/2)

pygame.draw.line(display_surf, (0, 0, 0), (display_surf.get_width()/2,180),(display_surf.get_width()/2,200), 4)

font = pygame.font.SysFont('Times New Roman', 20)

text = font.render(str(prizes[0][:-1]), True, (197, 179, 88))

textRect = text.get_rect()

textRect.center = (100,30)

display_surf.blit(text, textRect)

font = pygame.font.SysFont('Times New Roman', 20)

text = font.render(str(prizes[1][:-1]), True, (128, 0, 128))

textRect = text.get_rect()

textRect.center = (400,30)

display_surf.blit(text, textRect)

font = pygame.font.SysFont('Times New Roman', 20)

text = font.render(str(prizes[2][:-1]), True, (0, 255, 0))

textRect = text.get_rect()

textRect.center = (100,60)

display_surf.blit(text, textRect)

font = pygame.font.SysFont('Times New Roman', 20)

text = font.render(str(prizes[3][:-1]), True, (0, 0, 255))

textRect = text.get_rect()

textRect.center = (400,60)

display_surf.blit(text, textRect)

font = pygame.font.SysFont('Times New Roman', 20)

text = font.render(str(prizes[4][:-1]), True, (255, 0, 0))

textRect = text.get_rect()

textRect.center = (700,60)

display_surf.blit(text, textRect)

font = pygame.font.SysFont('Times New Roman', 20)

text = font.render("Rules", True, (255, 0, 0))

textRect = text.get_rect()

textRect.center = (100,950)

display_surf.blit(text, textRect)

font = pygame.font.SysFont('Times New Roman', 15)

text = font.render(str(rules[0][:-1]), True, (255, 0, 0))

textRect = text.get_rect()

textRect.center = (200,970)

display_surf.blit(text, textRect)

step = 360 / SEGMENTS

accum = 0

x = 0

z = 0

y = 0

for x in range(SEGMENTS):

if x == 0:

pygame.gfxdraw.pie(image_surf, int(CX),int(CY), RADIUS, int(accum), int(accum+step),(255,255,255))

#text = font.render(str(x), False, (255, 0, 0))

#image_surf.blit(text, (10, 10))

else:

#newX = oldX + dist * cos(angle)

#newY = oldY + dist * sin(angle)

pygame.gfxdraw.pie(image_surf, int(CX),int(CY), RADIUS, int(accum), int(accum+step),(255,255,255))

#text = font.render(str(x), False, (255, 0, 0))

#image_surf.blit(text, (10, 10))

p = [(CX, CY)]

# Get points on arc

for n in range(int(accum),int(accum + step)):

z = CX + int(RADIUS*math.cos(n*math.pi/180))

y = CY + int(RADIUS*math.sin(n*math.pi/180))

p.append((z, y))

p.append((CX, CY))

# Draw pie segment

if len(p) > 1:

#print("pie sgement %s " % (x))

if x == 1 or x == 3 or x== 7 or x == 9 or x == 11 or x == 13 or x == 17 or x == 19:

#]print("green")

pygame.draw.polygon(image_surf, (255, 0, 0), p)

elif x == 4 or x == 6 or x== 8 or x == 12 or x == 14 or x == 16:

pygame.draw.polygon(image_surf, (0, 0, 255), p)

elif x == 2 or x == 10 or x== 18:

pygame.draw.polygon(image_surf, (0, 255, 0), p)

elif x == 5 or x == 15:

pygame.draw.polygon(image_surf, (128, 0, 128), p)

#elif x == 4 or x == 14 or x== 19:

# pygame.draw.polygon(image_surf, (128, 0, 128), p)

elif x == 0:

pygame.draw.polygon(image_surf, (197, 179, 88), p)

accum = accum + step

display_surf.blit(image_surf, (200,200))

pygame.display.flip()

start = time.time()

new = time.time()

def blitRotate(surf, image, pos, originPos, angle):

w, h = image.get_size()

box = [pygame.math.Vector2(p) for p in [(0, 0), (w, 0), (w, -h), (0, -h)]]

box_rotate = [p.rotate(angle) for p in box]

min_box = (min(box_rotate, key=lambda p: p[0])[0], min(box_rotate, key=lambda p: p[1])[1])

max_box = (max(box_rotate, key=lambda p: p[0])[0], max(box_rotate, key=lambda p: p[1])[1])

pivot = pygame.math.Vector2(originPos[0], -originPos[1])

pivot_rotate = pivot.rotate(angle)

pivot_move = pivot_rotate - pivot

origin = (pos[0] - originPos[0] + min_box[0] - pivot_move[0], pos[1] - originPos[1] - max_box[1] + pivot_move[1])

rotated_image = pygame.transform.rotate(image, angle)

surf.blit(rotated_image, origin)

def spin():

iteration = randint(0, 255)

i=0

angle = 0

print("spin")

while i <= iteration:

if i == iteration:

pygame.draw.line(display_surf, (0, 0, 0), (display_surf.get_width()/2,180),(display_surf.get_width()/2,300), 4)

font = pygame.font.SysFont('Times New Roman', 50)

text = font.render("Winner!!!", True, GOLD)

textRect = text.get_rect()

textRect.center = (display_surf.get_width()/2,150)

display_surf.blit(text, textRect)

return

#print("complete")

elif i < int(iteration*0.85):

blitRotate(display_surf, image_surf, pos, (w/2, h/2), angle)

angle += 30

time.sleep(0.1)

pygame.display.flip()

i += 1

elif i < int(iteration*0.95):

blitRotate(display_surf, image_surf, pos, (w/2, h/2), angle)

angle += 20

time.sleep(0.3)

pygame.display.flip()

i += 1

else:

blitRotate(display_surf, image_surf, pos, (w/2, h/2), angle)

angle += 10

time.sleep(0.6)

pygame.display.flip()

i += 1

def mousebuttondown():

pos = pygame.mouse.get_pos()

for button in buttons:

if button.rect.collidepoint(pos):

button.call_back()

button_01 = Button("SPIN!", (display_surf.get_width()/2, 950), spin)

buttons = [button_01]

while True:

for event in pygame.event.get():

if event.type == pygame.QUIT:

pygame.quit()

sys.exit()

elif event.type == pygame.MOUSEBUTTONDOWN:

mousebuttondown()

for button in buttons:

button.draw()

end = time.time()

if end - start > 300:

break

pygame.display.flip()

pygame.time.wait(40)

Conclusion

This is a pretty simple project that provides a nice introduction to how we can work with Python in a more traditional manner on a PYNQ board, creating simple applications using frameworks like pygame. It also provides a nice simple demo is a starting point for looking at other applications e.g. QT etc.

See previous projectshere.

Additional Information on Xilinx FPGA / SoC Development can be found weekly onMicroZed Chronicles.