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 DesktopThe 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.
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
Of course the application will be slower over a network than if we are using the desktop directly on the Ultra96.
Installing PygameThe 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.
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.
Enter the SSID and Passphrase when prompted by the 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.
Once installed we are ready to begin writing our application.
Basic FunctionalityThe 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.
Completing the ApplicationObviously 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
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)
ConclusionThis 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.
Comments