Desoya
Posts: 4
Joined: Wed Feb 20, 2019 10:26 am

Issue using Python3/pygame to output to /dev/fb1 targetting a 2.8" TFT screen

Wed Feb 20, 2019 10:46 am

Hello, I've posted this on StackOverflow until I realised this would rather fit this community, here we go:

TL;DR
I am fiddling with a Raspberry Pi 2 and a 2.8" TFT touch screen attached to the Pi's GPIO. The Pi is also connected to a HDMI monitor.
My issue is that my Python3 pygame script is not able to use the TFT screen, but always displays on my HDMI screen instead.

Some background
I've installed the latest vanilla Raspbian ready-to-use distro and followed the TFT screen installation steps, everything works well: the TFT can display the console and X without issue. The touchscreen is calibrated and moves the cursor correctly. I can also see a new framebuffer device as /dev/fb1.

I've tried the following to test this new device:

Code: Select all

sudo fbi -T 2 -d /dev/fb1 -noverbose -a my_picture.jpg
=> This successfully displays the pic on the TFT screen

Code: Select all

while true; do sudo cat /dev/urandom > /dev/fb1; sleep .01; done
=> This successfully displays continuous statics on the TFT screen

However, when I run this Python3/pygame script, the result appears in the HDMI screen consistently and not on the TFT screen:

Code: Select all

#!/usr/bin/python3

import os, pygame, time

def setSDLVariables():
    print("Setting SDL variables...")
    os.environ["SDL_FBDEV"] = "/dev/fb1"
    os.environ["SDL_VIDEODRIVER"] = driver
    print("...done") 

def printSDLVariables():
    print("Checking current env variables...")
    print("SDL_VIDEODRIVER = {0}".format(os.getenv("SDL_VIDEODRIVER")))
    print("SDL_FBDEV = {0}".format(os.getenv("SDL_FBDEV")))

def runHW5():
    print("Running HW5...")
    try:
        pygame.init()
    except pygame.error:
        print("Driver '{0}' failed!".format(driver))
    size = (pygame.display.Info().current_w, pygame.display.Info().current_h)
    print("Detected screen size: {0}".format(size))
    lcd = pygame.display.set_mode(size)
    lcd.fill((10,50,100))
    pygame.display.update()
    time.sleep(sleepTime)
    print("...done")

driver = 'fbcon'
sleepTime= 0.1

printSDLVariables()
setSDLVariables()
printSDLVariables()
runHW5()
The script above runs as follow:
[email protected]:~/Documents/Python_HW_GUI $ sudo ./hw5-ThorPy-fb1.py
Checking current env variables...
SDL_VIDEODRIVER = None
SDL_FBDEV = None
Setting SDL variables...
...done
Checking current env variables...
SDL_VIDEODRIVER = fbcon
SDL_FBDEV = /dev/fb1
Running HW5...
Detected screen size: (1920, 1080)
...done
I have tried different drivers (fbcon, directfb, svgalib...) without success.

Any help or idea would be greatly appreciated, I've been through a lot of doc, manuals and samples and just ran out of leads :? Furthermore, it appears that a lot of people have succeeded in getting Python3/pygame to output to their TFT screen via /dev/fb1.

Andyroo

Re: Issue using Python3/pygame to output to /dev/fb1 targetting a 2.8" TFT screen

Wed Feb 20, 2019 2:31 pm

Have you seen https://www.raspberrypi.org/forums/view ... 2&t=234065 - here the SUDO command is the issue BUT the output is going to the smaller screen.

It would be interesting to see what your result is without the HDMI monitor being connected - currently the Pi only supports one monitor and a testing thread for multiple frame buffers is https://www.raspberrypi.org/forums/view ... ?p=1422702

Desoya
Posts: 4
Joined: Wed Feb 20, 2019 10:26 am

Re: Issue using Python3/pygame to output to /dev/fb1 targetting a 2.8" TFT screen

Wed Feb 20, 2019 3:03 pm

Thanks for your feedback @Andyroo
Andyroo wrote: Have you seen https://www.raspberrypi.org/forums/view ... 2&t=234065 - here the SUDO command is the issue BUT the output is going to the smaller screen.
I've tried both with and without sudo, making sure the device /dev/fb1 is accessible by my user (pi):

Code: Select all

sudo chmod a+rw /dev/sb1
Using /dev/fb1 does not trigger any error.
Andyroo wrote: It would be interesting to see what your result is without the HDMI monitor being connected - currently the Pi only supports one monitor and a testing thread for multiple frame buffers is https://www.raspberrypi.org/forums/view ... ?p=1422702
I'm a bit puzzled by this. When playing with the screen "manually" (see my initial post with the bit: cat /dev/urandom > /dev/fb1), the TFT screen updates live, along with my HDMI screen, so I know for a fact that both devices can be fed at the same time.

My issue here is that Python/pygame/SDL does not seem to get ahold of /dev/fb1, whereas I can do it directly by piping data into it.

Cheers

Desoya
Posts: 4
Joined: Wed Feb 20, 2019 10:26 am

Re: Issue using Python3/pygame to output to /dev/fb1 targetting a 2.8" TFT screen

Mon Mar 04, 2019 4:58 pm

I have been fiddling around that for far too many hours now, but at least I have found what I'd call a decent workaround, if not a solution. Here is a copy paste of my last post on stack overflow:

TL;DR
I've kept using pygame for building my graphics/GUI, and switched to evdev for handling the TFT touch events. The reason for using evdev rather than pygame's built-in input management (or pymouse, or any other high level stuff) is explained in the next section.

In a nutshell, this program builds some graphics in memory (RAM, not graphic) using pygame, and pushes the built graphics as bytes into the TFT screen framebuffer directly. This bypasses any driver so it is virtually compatible with any screen accessible through a framebuffer, however it also bypasses any potential optimizations coming along what would be a good driver.

Here is a code sample that makes the magic happen:

Code: Select all

#!/usr/bin/python3

##
# Prerequisites:
# A Touchscreen properly installed on your system:
# - a device to output to it, e.g. /dev/fb1
# - a device to get input from it, e.g. /dev/input/touchscreen
##

import pygame, time, evdev, select, math

# Very important: the exact pixel size of the TFT screen must be known so we can build graphics at this exact format
surfaceSize = (320, 240)

# Note that we don't instantiate any display!
pygame.init()

# The pygame surface we are going to draw onto. 
# /!\ It must be the exact same size of the target display /!\
lcd = pygame.Surface(surfaceSize)

# This is the important bit
def refresh():
    # We open the TFT screen's framebuffer as a binary file. Note that we will write bytes into it, hence the "wb" operator
    f = open("/dev/fb1","wb")
    # According to the TFT screen specs, it supports only 16bits pixels depth
    # Pygame surfaces use 24bits pixels depth by default, but the surface itself provides a very handy method to convert it.
    # once converted, we write the full byte buffer of the pygame surface into the TFT screen framebuffer like we would in a plain file:
    f.write(lcd.convert(16,0).get_buffer())
    # We can then close our access to the framebuffer
    f.close()
    time.sleep(0.1)

# Now we've got a function that can get the bytes from a pygame surface to the TFT framebuffer, 
# we can use the usual pygame primitives to draw on our surface before calling the refresh function.

# Here we just blink the screen background in a few colors with the "Hello World!" text
pygame.font.init()
defaultFont = pygame.font.SysFont(None,30)

lcd.fill((255,0,0))
lcd.blit(defaultFont.render("Hello World!", False, (0, 0, 0)),(0, 0))
refresh()

lcd.fill((0, 255, 0))
lcd.blit(defaultFont.render("Hello World!", False, (0, 0, 0)),(0, 0))
refresh()

lcd.fill((0,0,255))
lcd.blit(defaultFont.render("Hello World!", False, (0, 0, 0)),(0, 0))
refresh()

lcd.fill((128, 128, 128))
lcd.blit(defaultFont.render("Hello World!", False, (0, 0, 0)),(0, 0))
refresh()

##
# Everything that follows is for handling the touchscreen touch events via evdev
##

# Used to map touch event from the screen hardware to the pygame surface pixels. 
# (Those values have been found empirically, but I'm working on a simple interactive calibration tool
tftOrig = (3750, 180)
tftEnd = (150, 3750)
tftDelta = (tftEnd [0] - tftOrig [0], tftEnd [1] - tftOrig [1])
tftAbsDelta = (abs(tftEnd [0] - tftOrig [0]), abs(tftEnd [1] - tftOrig [1]))

# We use evdev to read events from our touchscreen
# (The device must exist and be properly installed for this to work)
touch = evdev.InputDevice('/dev/input/touchscreen')

# We make sure the events from the touchscreen will be handled only by this program
# (so the mouse pointer won't move on X when we touch the TFT screen)
touch.grab()
# Prints some info on how evdev sees our input device
print(touch)
# Even more info for curious people
#print(touch.capabilities())

# Here we convert the evdev "hardware" touch coordinates into pygame surface pixel coordinates
def getPixelsFromCoordinates(coords):
    # TODO check divide by 0!
    if tftDelta [0] < 0:
        x = float(tftAbsDelta [0] - coords [0] + tftEnd [0]) / float(tftAbsDelta [0]) * float(surfaceSize [0])
    else:    
        x = float(coords [0] - tftOrig [0]) / float(tftAbsDelta [0]) * float(surfaceSize [0])
    if tftDelta [1] < 0:
        y = float(tftAbsDelta [1] - coords [1] + tftEnd [1]) / float(tftAbsDelta [1]) * float(surfaceSize [1])
    else:        
        y = float(coords [1] - tftOrig [1]) / float(tftAbsDelta [1]) * float(surfaceSize [1])
    return (int(x), int(y))

# Was useful to see what pieces I would need from the evdev events
def printEvent(event):
    print(evdev.categorize(event))
    print("Value: {0}".format(event.value))
    print("Type: {0}".format(event.type))
    print("Code: {0}".format(event.code))

# This loop allows us to write red dots on the screen where we touch it 
while True:
    # TODO get the right ecodes instead of int
    r,w,x = select.select([touch], [], [])
    for event in touch.read():
        if event.type == evdev.ecodes.EV_ABS:
            if event.code == 1:
                X = event.value
            elif event.code == 0:
                Y = event.value
        elif event.type == evdev.ecodes.EV_KEY:
            if event.code == 330 and event.value == 1:
                printEvent(event)
                p = getPixelsFromCoordinates((X, Y))
                print("TFT: {0}:{1} | Pixels: {2}:{3}".format(X, Y, p [0], p [1]))
                pygame.draw.circle(lcd, (255, 0, 0), p , 2, 2)
                refresh()

exit()
More details
A quick recap on what I wanted to achieve: my goal is to display content onto a TFT display with the following constraints:
  • Be able to display another content on the HDMI display without interference (e.g. X on HDMI, the output of a graphical app on the TFT);
  • be able to use the touch capability of the TFT display for the benefit of the graphical app;
  • make sure the point above would not interfere with the mouse pointer on the HDMI display;
  • leverage Python and Pygame to keep it very easy to build whatever graphics/GUI I'd fancy;
  • keep a less-than-decent-but-sufficient-for-me framerate, e.g. 10 FPS.
Why not using pygame/SDL1.2.x as instructed in many forums and the adafruit TFT manual?
First, it doesn't work, at all. I have tried a gazillion versions of libsdl and its dependencies and they all failed consistently. I've tried forcing some libsdl versions downgrades, same with pygame version, just to try to get back to what the software was when my TFT screen was released (~2014). Then I aslo tried switching to C and handle SDL2 primitives directly.

Furthermore, SDL1.2 is getting old and I believe it is bad practice to build new code on top of old one. That said, I am still using pygame-1.9.4...

So why not SDL2? Well, they have stopped (or are about to stop) supporting framebuffers. I have not tried their alternative to framebuffers, EGL, as it got more complex the further I digged and it did not look too engaging (so old it felt like necro-browsing). Any fresh help or advice on that would be greatly appreciated BTW.

What about the touchscreen inputs?
All the high level solutions that work in a conventional context are embedding a display. I've tried pygame events, pymouse and a couple others that would not work in my case as I got rid of the notion of display on purpose. That's why I had to go back to a generic and low level solution, and the internet introduced my to evdev, see the commented code above for more details.

Any comment on the above would be greatly appreciated, these are my first step with Raspbian, Python and TFT screens, I reckon I most probably have missed some pretty obvious stuff along the way.

tcornall
Posts: 2
Joined: Sat Jun 29, 2019 7:00 am

Re: Issue using Python3/pygame to output to /dev/fb1 targetting a 2.8" TFT screen

Sat Jun 29, 2019 7:06 am

Brilliant Desoya!
I had arrived at the evdev solution for the touch but previous to installing Buster, pygame was still playing nice and drawing to the lcd.
Then I wanted Buster with HDMI and LCD at the same time but although urandom-> fb1 worked, pygame wouldn't, same as you found.
But your idea to copy the surface to /dev/fb1 works nicely for me, so thanks again!
Terry

wolfer649
Posts: 1
Joined: Sun Jul 21, 2019 10:38 pm

Re: Issue using Python3/pygame to output to /dev/fb1 targetting a 2.8" TFT screen

Sun Jul 21, 2019 11:10 pm

This is a fantastic solution!
Just what I was looking for for my project's latest 2.8" PiTFT display issues - to be implemented.
https://github.com/wolfer649/WGOT
(If you're really a newbie like me, you may find some other "tricks" in that code for dealing with PiTFT buttons, pygame and matplotlib.)

You really should post this to Adafruit's web site, since SDL isn't working on the PiTFT so well anymore.

P.S. The display part worked for me with Raspbian Stretch on a Raspberry Pi 3 B with an Adafruit PiTFT 2.8" capacitive (320x240) touch screen.
(Not so much with the "red dot" touch screen results - resolution TBD.)
Thanks for all of your research and for sharing the code!

jdonald
Posts: 413
Joined: Fri Nov 03, 2017 4:36 pm

Re: Issue using Python3/pygame to output to /dev/fb1 targetting a 2.8" TFT screen

Wed Jul 24, 2019 8:27 pm

I find this brilliant, yet equally unsatisfying. If there are tutorials and videos where SDL_VIDEODRIVER = fbcon SDL_FBDEV = /dev/fb1 just works, there must be some explanation.

Initial thoughts:
* Running apt source libsdl1.2debian gets something quite different compared to the SDL-1.2.15.tar.gz sourceball on libsdl.org. For example the patched source contains an entire video mode for dispmanx. Which source were you building from?
* Touch events: I recently learned that the Debian maintainers broke touch events with SDL 1.2.15/Pygame starting in Raspbian Jessie. To fix that you need to sudo apt install libts-dev and rebuild. reference: internet archive
* DirectFB is something else entirely, but it's also not included in the Debian/Raspbian libsdl1.2 binary. Did you sudo apt install libdirectfb-dev before compiling from source?
* When you say you tested by going back to what the software when the TFT was released in 2014, do you literally mean you ran Raspbian Wheezy on a Pi 1 and avoided any apt-get upgrade? If so, that makes the above angles less hopeful and the answer could lie somewhere else.

Return to “Graphics, sound and multimedia”