Non blocking serial readline when using PyGame - python

I'm working on a heart rate monitor written in PyGame using an Arduino as an input, the idea being that the game allows you to try and control your heart rate through relaxation exercises regardless of what's on screen.
I want to be able to run things like video/mouse/keybord button capture etc. in the game itself, whilst displaying the heart rate in the top left hand corner and updating it from the arduino when it changes.
The arduino reads a heart rate monitor and then publishes a JSON string formatted as follows:
{'heart_rate': 65,'state': 'running'}
"State" can be one of 'intialising','running','failed' or 'stopped'.
Whilst I'm more than familiar with Python, I've taken code from http://www.akeric.com/blog/?p=1237 to get me started with PyGame as I've not ventured here much before.
The problem I have is that when I try and read from the serial port, it locks up the game.
I've read around threading and I think I've implemented it properly, however the following code still blocks:
"""
default.py
www.akeric.com - 2010-09-07
Default, base, initial setup for a pygame program.
In this case, black background, white circle follows mouse position, and
framerate is shown in the title-bar.
"""
#-------------------------------------------------------------------------------
# Imports & Inits
import sys
import serial
import json
import threading
import pygame
from pygame.locals import *
pygame.init()
ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=0)
#-------------------------------------------------------------------------------
# Constants
VERSION = '1.0'
WIDTH = 800
HEIGHT = 600
FRAMERATE = 60
CURRENT_MESSAGE = {'heart_rate': 0}
#-------------------------------------------------------------------------------
# Screen Setup
screen = pygame.display.set_mode((WIDTH, HEIGHT))
bgCol = Color('black')
clock = pygame.time.Clock()
moduleName = __file__.split('\\')[-1]
#-------------------------------------------------------------------------------
# Define helper functions, classes, etc...
def text_objects(text, font):
textSurface = font.render(text, True, (255,255,255))
return textSurface, textSurface.get_rect()
def message_display(text,x_pos=(WIDTH/2),y_pos=(HEIGHT/2)):
largeText = pygame.font.Font('freesansbold.ttf',25)
TextSurf, TextRect = text_objects(text, largeText)
TextRect.center = (x_pos,y_pos)
screen.blit(TextSurf, TextRect)
def spam():
pos = pygame.mouse.get_pos()
pygame.draw.circle(screen, Color('white'), pos, 32)
def handle_data(data):
CURRENT_MESSAGE = json.loads(data)
message_display("HR: %s" % CURRENT_MESSAGE['heart_rate'],50,20)
def read_from_port(ser):
while True:
reading = ser.readline().decode()
if len(reading) > 0:
print "Data Recieved"
handle_data(reading)
#-------------------------------------------------------------------------------
# Main Program
def main():
print "Running Python version: %s"%sys.version
print "Running PyGame version: %s"%pygame.ver
print "Running %s version: %s"%(moduleName, VERSION)
looping = True
# Main Loop-----------------------
while looping:
# Maintain our framerate, set caption, clear background:
clock.tick(FRAMERATE)
pygame.display.set_caption("%s - FPS: %.2f" %(moduleName,clock.get_fps()) )
screen.fill(bgCol)
spam()
# Update our display:---------
pygame.display.flip()
#-------------------------------------------------------------------------------
# Execution from shell\icon:
if __name__ == "__main__":
# Make running from IDE work better:
thread = threading.Thread(target=read_from_port, args=(ser,))
thread.start()
sys.exit(main())
Can anyone help me understand where I'm going wrong here?

I know very little about Python but I would test to see if there are any characters waiting to be read and just read that number instead of using readline. I think you want to use in_waiting and read(size).
Readline will block until a carriage return is received.
So, I think something like:
def read_from_port(ser):
while True:
if in_waiting > 0:
reading = read(in_waiting)
print "Data Recieved"
handle_data(reading)
Now you will need to concatenate and parse the string at the delimiter to make sure the entire string has been received.

I fixed it! :)
#!/usr/bin/env python
import pygame
from threading import Thread
import serial
import json
from pygame.color import Color
from pygame.locals import *
CURHR = 0
ser = serial.Serial('/dev/pts/3', 9600, timeout=0)
def worker():
global CURHR
while True:
msg = ser.readline()
if len(msg) > 0:
print "Message Received: %s" % msg
current_message = json.loads(msg)
if current_message["state"] == "running":
CURHR=current_message['heart_rate']
t = Thread(target=worker)
t.daemon = True
t.start()
pygame.init()
#-------------------------------------------------------------------------------
# Constants
VERSION = '1.0'
WIDTH = 800
HEIGHT = 600
FRAMERATE = 60
#-------------------------------------------------------------------------------
# Define helper functions, classes, etc...
def text_objects(text, font):
textSurface = font.render(text, True, (255,255,255))
return textSurface, textSurface.get_rect()
def message_display(text,x_pos=(WIDTH/2),y_pos=(HEIGHT/2)):
largeText = pygame.font.Font('freesansbold.ttf',25)
TextSurf, TextRect = text_objects(text, largeText)
TextRect.center = (x_pos,y_pos)
screen.blit(TextSurf, TextRect)
def spam():
pos = pygame.mouse.get_pos()
pygame.draw.circle(screen, Color('white'), pos, 32)
screen = pygame.display.set_mode((WIDTH,HEIGHT))
clock = pygame.time.Clock()
font = pygame.font.SysFont("consolas", 25, True)
count = 0
pygame.display.set_caption("Test")
done = False
while not done:
screen.fill(Color('black'))
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT: # If user clicked close
done = True
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
done = True
spam()
hr_string = "HR: %s" % CURHR
hr_text = font.render(hr_string, True, Color('red'))
screen.blit(hr_text, [25,10])
clock.tick(20)
pygame.display.flip()
The trick is to setup a Global and then get the thread to set the value of that global.
The global is then rendered when the screen is "blitted" (is that a thing?!) and the value is updated in realtime without affecting the movement of the cursor.

Related

Whenever i try run my python code with pygame the window opens and then stops responding after a second or any input

heres all the code i have currently, it is just a start of a pacman game i am creating all that should be shown is a tab with press space to play
ive looked at previous responses to similar questions and i have what i should need to make it work but it still doesn't... i hope someone can help, thanks.
import os
import sys
import pygame
Start_window_font = 'arial black'
pygame.init()
vector=pygame.math.Vector2
class RPMGame:
def __init__(self):
self.screen = pygame.display.set_mode((450, 600))
self.clock = pygame.time.Clock()
self.running = True
self.state = 'startwindow'
self.load()
self.node_width = 450//25
self.node_height = 600//30
def run(self):
while self.running:
if self.state == 'startwindow':
self.startwindow_events()
self.startwindow_update()
self.startwindow_draw()
if self.state == 'gaming':
self.gaming_events()
self.gaming_update()
self.gaming_draw()
#fps
self.clock.tick(60)
pygame.quit()
sys.exit()
def draw_text(self, words, screen, position, size, colour, font_name, centered = False):
font = pygame.font.SysFont(font_name, size)
text = font.render(words, False, colour)
text_size = text.get_size()
#centering the starting text##
if centered:
position[0] = position[0]-text_size[0]//2
position[1] = position[1]-text_size[1]//2
screen.blit(text, position)
def load(self):
self.gameboard = pygame.image.load('pacmanmaze.png')
self.gameboard = pygame.transform.scale(self.gameboard, (450, 600))
def draw_maze(self):
for x in range(450//self.node_width):
pygame.draw.line(self.screen, 107,107,107, (x*self.node_height, 0), (x*self.node_width, 600))
def startwindow_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
###key used to start game##
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
self.state= 'gaming'
def startwindow_update(self):
pass
def startwindow_draw(self):
####colour of background##
self.screen.fill(0,0,0)
##start game text (text, position, text size, colour, centering)##
self.draw_text('CLICK TO START GAME', self.screen, (225,300), 16,(255,0,0),
Start_window_font, centered= True)
self.draw_text('HIGH SCORE', self.screen, [5,0] ,(225,300), 16,(255,255,255),
Start_window_font)
pygame.display.update()
def gaming_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
def gaming_update(self):
pass
def gaming_draw(self):
self.screen.blit(self.gameboard, (0,0))
self.draw_maze()
pygame.display.update()
game = RPMGame()
game.run()
**edit
i realised that i forgot to change the variable for one of my states which was needed for the stop part
You have a number of minor issues. When the app started and then immediately stopped, did you not notice that you're getting syntax and runtime errors on the console?
In this line:
self.screen.fill(0,0,0)
fill accepts one parameter, which is an RGB tuple:
self.screen.fill((0,0,0))
You have the same problem in draw_maze:
pygame.draw.line(self.screen, 107,107,107, (x*self.node_height, 0), (x*self.node_width, 600))
... should be ...
pygame.draw.line(self.screen, (107,107,107), (x*self.node_height, 0), (x*self.node_width, 600))
Then, in this call:
self.draw_text('HIGH SCORE', self.screen, [5,0] ,(225,300), 16,(255,255,255),
I don't know what the [5,0] was trying to do, but draw_text isn't expecting that parameter. Just remove it.
Then, in this code:
if centered:
position[0] = position[0]-text_size[0]//2
position[1] = position[1]-text_size[1]//2
screen.blit(text, position)
you pass in position as a tuple. You can't modify a tuple. You need to build a new tuple, or just change to a list:
pos = list(position)
if centered:
pos[0] = pos[0]-text_size[0]//2
pos[1] = pos[1]-text_size[1]//2
screen.blit(text, pos)
With that, your app at least starts for me.

SGC GUI and Pygame Widget implementation

Hi I am trying to code a simple application with Pygame. I have made various searches and found that best way to get an user input is to use a 3rd Party GUI.
I have found Simple Game Code for this aim. Below, you can find my base code, it looks for the images inside same path of script and replaces them in order at screen.
But I have no experience with this kind of applications. I am trying to understand from the documentation of SGC: https://github.com/codetricity/sgc/blob/master/example/test.py
It is not an easy task for me. I could develop this far, my code is running. But I couldn't understand the button implementation part.
Can you help me implement a "Scale Widget" at beginning to get user input between a range of integers. Also, a "Button Widget" to pass starting screen and begin my main code I will share with you.
Thanks for your time
import glob
import time
import numpy as np
import timeit
import pygame
import sgc
from sgc.locals import *
start = timeit.default_timer()
maxnote = 10
maxduration = 10
pygame.init()
white = (255, 255, 255)
path = r'C:\Path'
mylistname = [f for f in sorted(glob.glob("*.png"))]
mylistpath = [f for f in sorted(glob.glob(path + "/*.png"))]
for i in range(len(mylistname)):
mylistname[i] = mylistname[i].replace(".png", "")
mylistname[i] = mylistname[i].replace("h", ".")
mylistname[i] = float(mylistname[i])
imgname = []
for i in range(len(mylistname)):
imgname.append(str("img" + str(mylistname[i])))
imglist = []
for i in range(len(mylistpath)):
name = str(imgname[i])
name = pygame.image.load(mylistpath[i])
imglist.append(name)
current_image = 0
display_surface = pygame.display.set_mode((400, 400))
while (timeit.default_timer() - start < maxduration) | (current_image < maxnote):
#for imj in range(len(imglist)+1):
print(str(current_image) + "s")
if current_image < len(imglist):
print(str(current_image) + "0")
while True:
print(str(current_image) + "p")
display_surface.fill(white)
display_rect = display_surface.get_rect()
image_rect = imglist[current_image].get_rect()
image_rect.center = display_rect.center
display_surface.blit(imglist[current_image],image_rect)
pygame.display.update()
pygame.display.flip()
time.sleep(5)
current_image = current_image + 1
print(str(current_image) + "n")
break
else:
font = pygame.font.Font('freesansbold.ttf', 32)
text = font.render('GeeksForGeeks', True, (0, 255, 0), (0, 0, 128))
textRect = text.get_rect()
textRect.center = display_rect.center
display_surface.blit(text, textRect)
pygame.display.update()
pygame.display.flip()
time.sleep(5)
pygame.display.quit()
print("the end")
Using your code I added SGC button which is displayed on images and it display text in console when it is clicked.
I had two problems:
SGC is old and works only with Python 2. For Python 3 it would need relative imports. But later it may need other changes.
time.sleep() was blocking loop which checks key/mouse events, updates widgets, runs function when button is clicked, etc. sleep makes this problem with all GUI frameworks (tkinter, PyQt, wxpython, etc.) and in PyGame loop which has to run all time to check and update widgets and other elements. I use clock to check if it is time to change image. This way loop can works all time and it can update Button when mouse move on button and click it.
Tested on Python 2.7, Linux Mint 19.2
import glob
import pygame
import time
import sgc
from sgc.locals import *
# --- constants --- (UPPER_CASE)
WHITE = (255, 255, 255)
MAXNOTE = 10
MAXDURATION = 10
PATH = r'C:\Path'
# --- functions --- (lower_case_names)
def on_click_button():
print('on_click_button')
# --- main ---
filenames = sorted(glob.glob(PATH + "/*.png"))
print('len:', len(filenames))
names = []
images = []
for item in filenames:
names.append("img" + item.replace(".png", "").replace("h", "."))
images.append(pygame.image.load(item))
current_image = 0
# ---
pygame.init()
display_surface = sgc.surface.Screen((400, 400))
#display_surface = pygame.display.set_mode((400, 400))
display_rect = display_surface.get_rect()
font = pygame.font.Font('freesansbold.ttf', 32)
# add button
btn = sgc.Button(label="Clicky", pos=(10, 10))#, label_font=font)
btn.add(0)
# assign function to button
btn.on_click = on_click_button
# ---
clock = pygame.time.Clock()
current_time = pygame.time.get_ticks()
end_time = current_time + MAXDURATION*1000
end_slide = current_time
running = True
while running and ((current_time < end_time) or (current_image < MAXNOTE)):
ticks = clock.tick(30)
for event in pygame.event.get():
# send events to SGC so it can check if button was clicke
sgc.event(event)
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
current_time = pygame.time.get_ticks()
if (end_slide <= current_time) and (current_image < len(images)):
image = images[current_image]
image_rect = image.get_rect()
image_rect.center = display_rect.center
end_slide = current_time + 2000 # 2000ms (2s)
current_image += 1
display_surface.fill(WHITE)
display_surface.blit(image, image_rect)
# draw all widgets
sgc.update(ticks)
pygame.display.flip() # doesn't need pygame.display.update() because both do the same
# ---
display_surface.fill(WHITE)
text = font.render('GeeksForGeeks', True, (0, 255, 0), (0, 0, 128))
text_rect = text.get_rect()
text_rect.center = display_rect.center
display_surface.blit(text, text_rect)
#pygame.display.update() # no need it
pygame.display.flip()
time.sleep(5)
# --- end ---
pygame.display.quit()
print("the end")

How to remove sprites out of group, outside of the class: Pygame

#Importing Modules
import pygame as pg
import sys
import random
#All pygame stuff under here
pg.init()
#Font definitions
backFont = pg.font.SysFont("monospace",40)
titleFont = pg.font.SysFont("garamond", 100)
cipherFont = pg.font.SysFont("garamond", 50)
buttonFont = pg.font.SysFont("garamond", 25)
bigFont = pg.font.SysFont("garamond",100)
Font = pg.font.SysFont(None,32)
inputFont = pg.font.SysFont('consola', 35)
errorFont = pg.font.SysFont('tahoma',20)
diagramFont = pg.font.SysFont('courier new',25)
#Colour definitions
BackGray = pg.Color('gray60')
screenGray = pg.Color('gray80')
buttonGray2 = pg.Color('gray50')
textColour = pg.Color('navy')
#Screen size set
screen = pg.display.set_mode((400, 400))
clock = pg.time.Clock()
class Button(pg.sprite.Sprite):
def __init__(self, text, x, y, width, height, colour, enabled):
super().__init__()
self.image = pg.Surface((width, height))
self.image.fill(colour)
self.rect = self.image.get_rect()
txt = buttonFont.render(text, True, textColour)
txtRect = txt.get_rect(center = self.rect.center)
self.image.blit(txt, txtRect)
self.rect.topleft = x, y
self.enabled = enabled
def isPressed(self, event):
if self.enabled == True:
if event.type == pg.MOUSEBUTTONDOWN:
if self.rect.collidepoint(event.pos):
return True
return False
def Function():
background = pg.Surface(screen.get_size())
background.fill(screenGray)
Button1 = Button('Encrypt',100,100,125,50,buttonGray2,True)
Button2 = Button('Decrypt',100,200,125,50,buttonGray2,True)
buttonsGroup = pg.sprite.Group(Button1,Button2)
ACTIONPRINT = False
Active1 = False
while True:
for event in pg.event.get():
if event.type == pg.QUIT:
pg.quit()
sys.exit()
elif Button1.isPressed(event):
print("1")
Active1 = True
elif Button2.isPressed(event):
print("2")
if Active1 == True:
ACTIONPRINT = True
buttonsGroup = pg.sprite.Sprite.remove(Button2)
screen.blit(background,(0,0))
buttonsGroup.draw(screen)
pg.display.flip()
clock.tick(60)
Function()
Above is the code for the class of Buttons, and a simple function that runs two buttons. What I'd like to do is remove one of the buttons, when one is pressed, which are set as sprites. When the button is removed from the group, I believe it should disappear from the screen, after one is pressed.
The above code at the minute returns an error saying that there is an AttributeError: 'NoneType' object has no attribute 'draw'. However, in another program, when it did work, it said that in the Sprite.remove method, the parameters must be a sequence and not a button - what does this mean?
I have looked online, and all the removing sprites examples are inside a class. Does that mean that the only way for this to work is by changing the class?
Or can sprites still be removed from outside the class, and if so how is it done?
Any other methods are welcome to
Thanks in advance!
sprite.Group.remove doesn't return anything, it removes the sprite from the group that calls it, so instead of:
buttonsGroup = pg.sprite.Sprite.remove(Button2)
Try:
buttonsGroup.remove(Button2)

How to update the display of Pygame only when there is fresh incoming data from another Python script?

I have a Python script that receives data through Bluetooth at not so regular intervals. Specifically it receives the coordinates of an image through Bluetooth. Using those coordinates I want to update the display of an image in a separate Pygame script. I don't want the Pygame display to close and open again. The Pygame script should run continuously and it should update the display of the image only when it receives new data.
Here is my Bluetooth script:
import bluetooth
server_sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
port = 1
server_sock.bind(("", port))
server_sock.listen(1)
client_sock, address = server_sock.accept()
print "Accepted connection from ", address
while True:
data = client_sock.recv(1024)
print "received [%s]" % data
Any guidelines on how to achieve this behaviour would be appreciated!
Thank you
import pygame
from pygame.locals import *
def main():
# Initialise screen
pygame.init()
screen = pygame.display.set_mode((150, 50))
pygame.display.set_caption('Basic Pygame program')
# Fill background
background = pygame.Surface(screen.get_size())
background = background.convert()
background.fill((250, 250, 250))
# Display some text
font = pygame.font.Font(None, 36)
text = font.render("Hello There", 1, (10, 10, 10))
textpos = text.get_rect()
textpos.centerx = background.get_rect().centerx
background.blit(text, textpos)
# Blit everything to the screen
screen.blit(background, (0, 0))
pygame.display.flip()
# Event loop
while 1:
for event in pygame.event.get():
if event.type == QUIT:
return
screen.blit(background, (0, 0))
data = client_sock.recv(1024)
#do something with the data
pygame.display.flip()
if __name__ == '__main__': main()
I just got that from the example. You should be able to do whatever you want inside the game loop.

new text rendered over older text in pygame

I wrote an application in pygame to display some text. The text consist of a counter which is updated every second or so. I am using raspberry pi for this application. So when I use xserver then everything is displayed correctly but if I use sdl_videodriver fbcon for display then static text is displayed correctly but the counter(text) whose value changes is not displayed correctly. The new value of counter is displayed over the older value and thus after few seconds it becomes unreadable. Following is my code
class pyscope :
def __init__(self):
disp_no = os.getenv("DISPLAY")
if disp_no:
print "I'm running under X display = {0}".format(disp_no)
drivers = ['fbcon', 'directfb', 'svgalib']
found = False
for driver in drivers:
if not os.getenv('SDL_VIDEODRIVER'):
os.putenv('SDL_VIDEODRIVER', driver)
try:
pygame.display.init()
except pygame.error:
print 'Driver: {0} failed.'.format(driver)
continue
found = True
break
if not found:
raise Exception('No suitable video driver found!')
size = [1920,1080]
self.screen = pygame.display.set_mode(size,pygame.FULLSCREEN)
self.screen.fill((0,0,0))
pygame.font.init()
pygame.display.update()
def __del__(self):
"Destructor to make sure pygame shuts down, etc."
def test(self):
pygame.display.set_caption("Test")
done=False
clock=pygame.time.Clock()
font = pygame.font.SysFont("consolas", 34, True)
frame_rate = 20
count = 0
while done==False:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done=True
high_score = 2270
plan = 2100
count = count + 1
font = pygame.font.SysFont("consolas", 200, True)
if count >100:
count = 12
output_string = "ACTUAL %s" %count
text = font.render(output_string,True,red)
pygame.display.flip()
self.screen.blit(text, [250,420])
output1 = "random %.2f" %(float(count)/100*100)
text = font.render(output1,True,red)
self.screen.blit(text, [250,540])
pygame.display.flip()
clock.tick(20)
pygame.display.flip()
scope = pyscope()
scope.test()
time.sleep(10)
Thus my question how can I avoid new text being rendered over older text while using sdl_videodriver?
It is not enough to update() the screen, you should also "clear" it with a color. Before any blitting/drawing, do:
self.screen.fill((0,0,0))
You currently only do this once, when initializing the application. This should be done on each frame, if you want to keep a fresh, new screen on each frame.
It sounds like pygame is not clearing the area of the text counter.
Since I don't have access to an raspberry pi I would suggest you to
make sure you clear/update the area where the counter is rendered.
Before blitting out the text, you could clear the screen area with pygame.draw.rect
pygame.draw.rect(self.screen,(0,0,0),text.get_rect())
Should work OK, as long as your background is a solid color.

Categories