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

Att*_*ila 5 python pygame raspberry-pi

TL; DR

我摆弄树莓派2的Raspberry Pi 2和2.8“ TFT触摸屏。树莓派还连接到HDMI监视器。
我的问题是我的Python3 pygame脚本无法使用TFT屏幕,但总是而是显示在我的HDMI屏幕上。

一些背景

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

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

sudo fbi -T 2 -d /dev/fb1 -noverbose -a my_picture.jpg
Run Code Online (Sandbox Code Playgroud)

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

while true; do sudo cat /dev/urandom > /dev/fb1; sleep .01; done
Run Code Online (Sandbox Code Playgroud)

=>这样可以成功在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()
Run Code Online (Sandbox Code Playgroud)

上面的脚本运行如下:

sudo fbi -T 2 -d /dev/fb1 -noverbose -a my_picture.jpg
Run Code Online (Sandbox Code Playgroud)

我尝试了不同的drivers(fbcon,directfb,svgalib ...),但没有成功。

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

Att*_*ila 8

我已经为此折腾了太多时间,但至少我找到了我称之为体面的解决方法,如果不是解决方案的话。

TL; 博士

我一直使用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()
Run Code Online (Sandbox Code Playgroud)

更多细节

快速回顾一下我想要实现的目标:我的目标是在具有以下约束的 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 替代方案,因为我挖得越深,它就越复杂,而且看起来不太吸引人(太旧了,感觉就像死灵浏览一样)。顺便说一句,任何有关此的新帮助或建议将不胜感激。

触摸屏输入呢?

在传统环境中工作的所有高级解决方案都嵌入了显示器。我已经尝试过 pygame 事件、pymouse 和其他一些在我的情况下不起作用的事件,因为我故意摆脱了显示的概念。这就是为什么我不得不回到通用和低级解决方案,互联网将我的介绍给evdev,有关更多详细信息,请参阅上面的注释代码。

对上述任何评论将不胜感激,这是我使用 Raspbian、Python 和 TFT 屏幕的第一步,我想我很可能在此过程中错过了一些非常明显的东西。

  • 绝妙的想法,但其他人需要注意一件事:这段代码*实际上初始化了 pygame 显示*,因为如果没有初始化显示,`Surface.convert()` 调用将失败。如果是这种情况 - 您可能想通过“Surface((x, y), height=16)”设置位深度并跳过“.convert()”调用。这样,您甚至可以通过 SSH 运行代码,而无需运行任何 X 服务器。 (2认同)