ProfFalken ProfFalken - 6 months ago 32
Linux Question

Non blocking serial readline when using PyGame

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?

Answer

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.