Python pygame无法在Raspberry Pi + TFT屏幕上输出到/ dev / fb1

时间:2019-02-20 02:50:40

标签: python raspberry-pi pygame

TL; DR

我对Raspberry Pi 2和连接到Pi的GPIO的2.8“ TFT触摸屏感到不满意。Pi还连接到HDMI监视器。
我的问题是我的Python3 pygame 脚本无法使用TFT屏幕,而是始终显示在我的HDMI屏幕上。

一些背景

我已经安装了最新的香草Raspbian即用型发行版,并按照TFT屏幕的安装步骤进行,一切正常:TFT可以显示控制台和X,而不会出现问题。触摸屏已校准,可以正确移动光标。我还可以看到一个新的 framebuffer 设备为/dev/fb1

我尝试了以下方法来测试此新设备:

sudo fbi -T 2 -d /dev/fb1 -noverbose -a my_picture.jpg

=>这样可以成功在TFT屏幕上显示图片

while true; do sudo cat /dev/urandom > /dev/fb1; sleep .01; done

=>这样可以在TFT屏幕上成功显示静电

但是,当我运行此Python3 / pygame脚本时,结果始终显示在HDMI屏幕上,而不显示在TFT屏幕上:

#!/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()

上面的脚本运行如下:

pi@raspberrypi:~/Documents/Python_HW_GUI $ ./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

我尝试了不同的driver(fbcon,directfb,svgalib ...)。

任何帮助或想法都将不胜感激,我已经阅读了许多文档,手册和示例,并且用尽了线索:/此外,看来很多人已经成功地将Python3 / pygame引入了通过/dev/fb1输出到他们的TFT屏幕。

1 个答案:

答案 0 :(得分:2)

我已经花了很多个小时来研究这个问题,但是至少我发现了所谓的不错的解决方法,即使不是解决方法。

TL; DR

我一直使用 pygame 来构建图形/ GUI,并切换到 evdev 来处理TFT触摸事件。在下一节中将说明使用evdev而不是pygame的内置输入管理(或pymouse或任何其他高级工具)的原因。

简而言之,该程序使用pygame在内存中构建一些图形(RAM,而不是图形),并将构建的图形作为字节直接推入TFT屏幕帧缓冲区。这会绕过任何驱动程序,因此实际上与可通过帧缓冲区访问的任何屏幕兼容,但是,它也绕过了任何可能会成为好的驱动程序的潜在优化。

以下是使魔术发生的代码示例:

#!/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()

更多详细信息

快速回顾一下我想要实现的目标:我的目标是在具有以下限制的情况下将内容显示在TFT显示器上:

  1. 能够在没有干扰的情况下在HDMI显示器上显示其他内容(例如,HDMI上的X,TFT上图形应用程序的输出);
  2. 能够利用TFT显示器的触摸功能来实现图形应用;
  3. 确保以上几点不会干扰HDMI显示器上的鼠标指针;
  4. 利用Python和Pygame轻松构建我喜欢的图形/ GUI;
  5. 保持不足以达到我的帧速率,例如10 FPS。

为什么不按照许多论坛和adafruit TFT手册中的指示使用pygame / SDL1.2.x?

首先,它根本不起作用。我试过了 libsdl 的庞大版本及其依赖项,它们都始终失败。我尝试强制将某些 libsdl 版本降级,与 pygame 版本相同,只是试图恢复发布TFT屏幕时的软件版本(〜2014) 。然后我也尝试切换到C并直接​​处理SDL2原语。

此外,SDL1.2越来越老,我认为在旧代码之上构建新代码是不好的做法。也就是说,我仍在使用pygame-1.9.4 ...

那么为什么不使用SDL2?好了,他们已经停止(或将要停止)支持帧缓冲区。我还没有尝试过使用它们代替帧缓冲区EGL,因为随着我进一步挖掘和it did not look too engaging(它太旧了,感觉像是坏了浏览),它变得越来越复杂。顺便说一句,BTW将为您提供任何新的帮助或建议。

那触摸屏输入呢?

在常规情况下可用的所有高级解决方案都嵌入了显示器。我尝试了pygame事件,pymouse和其他一些在我的情况下不起作用的方法,因为我摆脱了故意显示的概念。这就是为什么我必须回到通用的低级解决方案的原因,互联网将我介绍给了 evdev ,有关更多详细信息,请参见上面的注释代码。

对上述内容的任何评论将不胜感激,这是我使用Raspbian,Python和TFT屏幕的第一步,我认为我很可能一路上错过了一些非常明显的东西。