2014-09-01

Raspberry Pi Pygame UI basics

Stage 1 setup

  1. Follow Adafruit instructions for PiTFT
  2. Get the tutorial code: git clone https://github.com/jerbly/tutorials.git

Test the setup

pi@raspberrypi ~ $ sudo python
Python 2.7.3 (default, Mar 18 2014, 05:13:23)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pygame
>>> import os
>>> os.putenv('SDL_FBDEV', '/dev/fb1')
>>> pygame.init()
(6, 0)
>>> lcd = pygame.display.set_mode((320, 240))
>>> lcd.fill((255,0,0))
<rect(0, 0, 320, 240)>
>>> pygame.display.update()
>>> pygame.mouse.set_visible(False)
1
>>> lcd.fill((0,0,0))
<rect(0, 0, 320, 240)>
>>> pygame.display.update()
You can also run this test (with a one second sleep) from the pygamelcd project: sudo python test1.py


From GPIO to screen

So, we can paint colours on the screen - let's do this from GPIs!

We'll use the four tactile buttons along the bottom of the screen to draw the GPIO number and a coloured background. From left to right the buttons correspond to GPIO #23, #22, #27, and #18.

(Note: If you have a revision 1 board then #27 is #21 - you'll just have to change the code a little)








import pygame
import os
from time import sleep
import RPi.GPIO as GPIO

#Note #21 changed to #27 for rev2 Pi
button_map = {23:(255,0,0), 22:(0,255,0), 27:(0,0,255), 18:(0,0,0)}

#Setup the GPIOs as inputs with Pull Ups since the buttons are connected to GND
GPIO.setmode(GPIO.BCM)
for k in button_map.keys():
    GPIO.setup(k, GPIO.IN, pull_up_down=GPIO.PUD_UP)

#Colours
WHITE = (255,255,255)

os.putenv('SDL_FBDEV', '/dev/fb1')
pygame.init()
pygame.mouse.set_visible(False)
lcd = pygame.display.set_mode((320, 240))
lcd.fill((0,0,0))
pygame.display.update()

font_big = pygame.font.Font(None, 100)

while True:
    # Scan the buttons
    for (k,v) in button_map.items():
        if GPIO.input(k) == False:
            lcd.fill(v)
            text_surface = font_big.render('%d'%k, True, WHITE)
            rect = text_surface.get_rect(center=(160,120))
            lcd.blit(text_surface, rect)
            pygame.display.update()
    sleep(0.1)

You can also run this from the pygamelcd project: sudo python test2.py


From screen to GPIO

The PiTFT from Adafruit is a touchscreen. So let's see how we get input from the screen. We'll use this to light some LEDs on the breadboard.

With the PiTFT installed and the 4 tactile buttons there aren't many GPIs left on the model B Raspberry Pi. So wire up #17 and #4. The software renders 4 labels on the screen and then looks for mouse events in the four quarters:









import pygame
from pygame.locals import *
import os
from time import sleep
import RPi.GPIO as GPIO

#Setup the GPIOs as outputs - only 4 and 17 are available
GPIO.setmode(GPIO.BCM)
GPIO.setup(4, GPIO.OUT)
GPIO.setup(17, GPIO.OUT)

#Colours
WHITE = (255,255,255)

os.putenv('SDL_FBDEV', '/dev/fb1')
os.putenv('SDL_MOUSEDRV', 'TSLIB')
os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')

pygame.init()
pygame.mouse.set_visible(False)
lcd = pygame.display.set_mode((320, 240))
lcd.fill((0,0,0))
pygame.display.update()

font_big = pygame.font.Font(None, 50)

touch_buttons = {'17 on':(80,60), '4 on':(240,60), '17 off':(80,180), '4 off':(240,180)}

for k,v in touch_buttons.items():
    text_surface = font_big.render('%s'%k, True, WHITE)
    rect = text_surface.get_rect(center=v)
    lcd.blit(text_surface, rect)

pygame.display.update()

while True:
    # Scan touchscreen events
    for event in pygame.event.get():
        if(event.type is MOUSEBUTTONDOWN):
            pos = pygame.mouse.get_pos()
            print pos
        elif(event.type is MOUSEBUTTONUP):
            pos = pygame.mouse.get_pos()
            print pos
            #Find which quarter of the screen we're in
            x,y = pos
            if y < 120:
                if x < 160:
                    GPIO.output(17, False)
                else:
                    GPIO.output(4, False)
            else:
                if x < 160:
                    GPIO.output(17, True)
                else:
                    GPIO.output(4, True)
    sleep(0.1)


Stage 2 setup

We're now going to improve the UI by introducing a widget framework PygameUI

1. Update your version of distribute: sudo easy_install -U distribute
2. Install PygameUI: sudo pip install pygameui

PygameUI GPIOs



This example controls GPIO #17 and #4 as above but now we're using the new framework.

The widget rendering and touchscreen events are handled by PygameUI. The PiTft class defines the buttons to draw on screen and the click event to be fired when a button is pressed.










import pygame
import os
import pygameui as ui
import logging
import RPi.GPIO as GPIO

#Setup the GPIOs as outputs - only 4 and 17 are available
GPIO.setmode(GPIO.BCM)
GPIO.setup(4, GPIO.OUT)
GPIO.setup(17, GPIO.OUT)

log_format = '%(asctime)-6s: %(name)s - %(levelname)s - %(message)s'
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter(log_format))
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logger.addHandler(console_handler)

os.putenv('SDL_FBDEV', '/dev/fb1')
os.putenv('SDL_MOUSEDRV', 'TSLIB')
os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')

MARGIN = 20

class PiTft(ui.Scene):
    def __init__(self):
        ui.Scene.__init__(self)

        self.on17_button = ui.Button(ui.Rect(MARGIN, MARGIN, 130, 90), '17 on')
        self.on17_button.on_clicked.connect(self.gpi_button)
        self.add_child(self.on17_button)

        self.on4_button = ui.Button(ui.Rect(170, MARGIN, 130, 90), '4 on')
        self.on4_button.on_clicked.connect(self.gpi_button)
        self.add_child(self.on4_button)

        self.off17_button = ui.Button(ui.Rect(MARGIN, 130, 130, 90), '17 off')
        self.off17_button.on_clicked.connect(self.gpi_button)
        self.add_child(self.off17_button)

        self.off4_button = ui.Button(ui.Rect(170, 130, 130, 90), '4 off')
        self.off4_button.on_clicked.connect(self.gpi_button)
        self.add_child(self.off4_button)

    def gpi_button(self, btn, mbtn):
        logger.info(btn.text)
        
        if btn.text == '17 on':
            GPIO.output(17, False)
        elif btn.text == '4 on':
            GPIO.output(4, False)
        elif btn.text == '17 off':
            GPIO.output(17, True)
        elif btn.text == '4 off':
            GPIO.output(4, True)

ui.init('Raspberry Pi UI', (320, 240))
pygame.mouse.set_visible(False)
ui.scene.push(PiTft())
ui.run()


Analog input



This next example uses a 10K potentiometer to provide a varying voltage. For analog to digital I normally use an MCP3008 over SPI. Unfortunately the downside to the Pi TFT touchscreen is that both SPI channels on the Pi are in use. So I've switched to an I2C ADC from Adafruit: ADS1115 16-Bit ADC - 4 Channel with Programmable Gain Amplifier.
Get the Adafruit Python library: git clone https://github.com/adafruit/Adafruit-Raspberry-Pi-Python-Code.git

If you need to enable i2c follow this guide: Configuring I2C






A few important notes about this code.

  • A thread is used to constantly read the potentiometer. If you take the reading in-line in the scene update method then you'll slow down the screen refresh rate.
  • The PotReader class is given a reference to the PiTft class in order to pass data
  • The ui.Scene class (PiTft) is instantiated after the call to ui.init - if you do this the other way around it will fail.
  • A signal handler is used to trap ctrl+c and terminate the PotReader thread before calling sys.exit - otherwise the program will not close.

import sys
sys.path.append('/home/pi/Adafruit-Raspberry-Pi-Python-Code/Adafruit_ADS1x15')

import pygame
import os
import pygameui as ui
import logging
import RPi.GPIO as GPIO
import signal
from Adafruit_ADS1x15 import ADS1x15
import threading
import time

ADS1015 = 0x00  # 12-bit ADC
ADS1115 = 0x01  # 16-bit ADC

# Select the gain
# gain = 6144  # +/- 6.144V
gain = 4096  # +/- 4.096V
# gain = 2048  # +/- 2.048V
# gain = 1024  # +/- 1.024V
# gain = 512   # +/- 0.512V
# gain = 256   # +/- 0.256V

# Select the sample rate
sps = 8    # 8 samples per second
# sps = 16   # 16 samples per second
# sps = 32   # 32 samples per second
# sps = 64   # 64 samples per second
# sps = 128  # 128 samples per second
# sps = 250  # 250 samples per second
# sps = 475  # 475 samples per second
# sps = 860  # 860 samples per second

# Initialise the ADC using the default mode (use default I2C address)
# Set this to ADS1015 or ADS1115 depending on the ADC you are using!
adc = ADS1x15(ic=ADS1115)

#Setup the GPIOs as outputs - only 4 and 17 are available
GPIO.setmode(GPIO.BCM)
GPIO.setup(4, GPIO.OUT)
GPIO.setup(17, GPIO.OUT)

log_format = '%(asctime)-6s: %(name)s - %(levelname)s - %(message)s'
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter(log_format))
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logger.addHandler(console_handler)

os.putenv('SDL_FBDEV', '/dev/fb1')
os.putenv('SDL_MOUSEDRV', 'TSLIB')
os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')

MARGIN = 20

class PotReader():
    def __init__(self, pitft):
        self.pitft = pitft
        self.terminated = False
        
    def terminate(self):
        self.terminated = True
        
    def __call__(self):
        while not self.terminated:
            # Read channel 0 in single-ended mode using the settings above
            volts = adc.readADCSingleEnded(0, gain, sps) / 1000
            self.pitft.set_volts_label(volts)
            self.pitft.set_progress(volts / 3.3)

class PiTft(ui.Scene):
    def __init__(self):
        ui.Scene.__init__(self)

        self.on17_button = ui.Button(ui.Rect(MARGIN, MARGIN, 130, 60), '17 on')
        self.on17_button.on_clicked.connect(self.gpi_button)
        self.add_child(self.on17_button)

        self.on4_button = ui.Button(ui.Rect(170, MARGIN, 130, 60), '4 on')
        self.on4_button.on_clicked.connect(self.gpi_button)
        self.add_child(self.on4_button)

        self.off17_button = ui.Button(ui.Rect(MARGIN, 100, 130, 60), '17 off')
        self.off17_button.on_clicked.connect(self.gpi_button)
        self.add_child(self.off17_button)

        self.off4_button = ui.Button(ui.Rect(170, 100, 130, 60), '4 off')
        self.off4_button.on_clicked.connect(self.gpi_button)
        self.add_child(self.off4_button)

        self.progress_view = ui.ProgressView(ui.Rect(MARGIN, 200, 280, 40))
        self.add_child(self.progress_view)

        self.volts_value = ui.Label(ui.Rect(135, 170, 50, 30), '')
        self.add_child(self.volts_value)

    def gpi_button(self, btn, mbtn):
        logger.info(btn.text)
        
        if btn.text == '17 on':
            GPIO.output(17, False)
        elif btn.text == '4 on':
            GPIO.output(4, False)
        elif btn.text == '17 off':
            GPIO.output(17, True)
        elif btn.text == '4 off':
            GPIO.output(4, True)

    def set_progress(self, percent):
        self.progress_view.progress = percent
        
    def set_volts_label(self, volts):
        self.volts_value.text = '%.2f' % volts

    def update(self, dt):
        ui.Scene.update(self, dt)


ui.init('Raspberry Pi UI', (320, 240))
pygame.mouse.set_visible(False)

pitft = PiTft()

# Start the thread running the callable
potreader = PotReader(pitft)
threading.Thread(target=potreader).start()

def signal_handler(signal, frame):
    print 'You pressed Ctrl+C!'
    potreader.terminate()
    sys.exit(0)
        
signal.signal(signal.SIGINT, signal_handler)

ui.scene.push(pitft)
ui.run()