返回

树莓派GPIO绑定Tkinter按钮:告别键盘交互

Linux

树莓派GPIO事件绑定Tkinter按钮:告别键盘,拥抱硬件交互

老是依赖键盘来触发Tkinter按钮事件?这次咱来点不一样的!把树莓派的GPIO输入变化跟Tkinter按钮事件直接挂钩,让你的GUI程序直接响应物理世界的信号,岂不美哉?这篇就讲怎么在不使用线程的情况下搞定这个事。

遇到的麻烦事

from tkinter import *
from tkinter import ttk
import keyboard

root = Tk()

def p():
  print('hello')

BUT_Quitter = ttk.Button ( root , text = "Quitter" , command = root.destroy )
BUT_Quitter.pack ( )
BUT_display = ttk.Button ( root , text = "Hello" , command = p )
BUT_display.pack ( )

def key_press(event):
    key = event.char
    if key=='q':
     BUT_display.invoke()

root.bind('<Key>', key_press)

root.mainloop ( )

上面这段代码,通过键盘事件(keyboard库)来触发BUT_display按钮的点击事件。问题来了:我想要的是GPIO引脚电平变化时,也能实现类似的效果,直接让按钮"自己动",怎么办?

为什么不能直接套用?

root.bind('<Key>', key_press) 这种方式,是Tkinter自带的事件绑定机制,它直接监听的是来自操作系统的键盘事件。GPIO的电平变化,可不是操作系统层面的键盘事件,它们是硬件层级的信号。所以,我们需要另辟蹊径。

解决思路和实操

几种可行的办法,各有优劣,咱一一拆解:

1. 简单粗暴轮询:after + RPi.GPIO

原理:

Tkinter的after方法,可以安排一个函数在一段时间后执行。我们利用这个机制,周期性地检查GPIO引脚的状态。如果状态变化了,就调用按钮的invoke()方法。

代码示例:

import tkinter as tk
from tkinter import ttk
import RPi.GPIO as GPIO

# 树莓派引脚设置
GPIO_PIN = 17  # 假设你要用GPIO 17
GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # 根据实际情况,可能需要上拉或下拉电阻

root = tk.Tk()

def p():
    print('hello')

BUT_Quitter = ttk.Button(root, text="Quitter", command=root.destroy)
BUT_Quitter.pack()
BUT_display = ttk.Button(root, text="Hello", command=p)
BUT_display.pack()

last_gpio_state = GPIO.input(GPIO_PIN)

def check_gpio():
    global last_gpio_state
    current_gpio_state = GPIO.input(GPIO_PIN)
    if current_gpio_state != last_gpio_state:
        last_gpio_state = current_gpio_state
        if current_gpio_state == GPIO.LOW:  # 假设低电平触发
            BUT_display.invoke()
    root.after(100, check_gpio)  # 每100毫秒检查一次

root.after(100, check_gpio)  # 启动检查
root.mainloop()
GPIO.cleanup()  # 退出时清理GPIO

代码解释:

  • GPIO.setup(GPIO_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP): 配置GPIO引脚为输入模式。pull_up_down 参数设置内部上拉电阻(或者GPIO.PUD_DOWN设置下拉),具体用哪个取决于你的电路设计。如果不接外部上拉/下拉电阻,而且GPIO引脚悬空,输入状态会不稳定,容易误触发。
  • last_gpio_state: 保存上一次读取的GPIO状态。
  • check_gpio():核心函数,检查GPIO状态变化,并调用按钮。
  • root.after(100, check_gpio):安排 check_gpio 函数每100毫秒执行一次。

注意事项:

  • 轮询频率(after的第一个参数)要根据实际情况调整。太高了会增加CPU负担,太低了反应会迟钝。
  • 代码里假设是低电平触发(current_gpio_state == GPIO.LOW)。根据实际电路连接情况,你可能需要改成高电平触发。

2. 中断!add_event_detect + after_idle

原理:

RPi.GPIO 库提供了一个非常强大的功能:add_event_detect。它可以监听GPIO引脚的电平变化(上升沿、下降沿或双边沿),并在变化发生时调用一个回调函数。 但是,直接在GPIO的回调函数里操作Tkinter是不安全的,因为回调函数是在一个单独的线程中运行的. 这时候要搭配 Tkinter 的 after_idle 使用。

代码示例:

import tkinter as tk
from tkinter import ttk
import RPi.GPIO as GPIO

# 树莓派引脚设置
GPIO_PIN = 17
GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

root = tk.Tk()

def p():
  print('hello')

BUT_Quitter = ttk.Button ( root , text = "Quitter" , command = root.destroy )
BUT_Quitter.pack ( )
BUT_display = ttk.Button ( root , text = "Hello" , command = p )
BUT_display.pack ( )

def gpio_callback(channel):
    root.after_idle(BUT_display.invoke)

GPIO.add_event_detect(GPIO_PIN, GPIO.FALLING, callback=gpio_callback, bouncetime=200)  # 检测下降沿

root.mainloop()
GPIO.cleanup()

代码解释:

  • GPIO.add_event_detect(GPIO_PIN, GPIO.FALLING, callback=gpio_callback, bouncetime=200): 设置GPIO引脚的事件检测。
    • GPIO.FALLING: 监听下降沿(从高电平到低电平)。根据需要,可以改成GPIO.RISING(上升沿)或GPIO.BOTH(双边沿)。
    • callback=gpio_callback: 指定事件发生时调用的函数。
    • bouncetime=200: 防抖动时间,单位是毫秒。避免因为按键抖动导致的多次触发。
  • root.after_idle(BUT_display.invoke): 保证了BUT_display.invoke()在主线程安全执行。
  • 务必保证GPIO设置为了正确的输入模式和上拉/下拉.

注意事项:

  • bouncetime 的设置要根据实际按键或传感器的特性来调整。

3. 高级玩法: asyncio (进阶)

原理:
asyncio 提供的事件循环和异步特性来简化对GPIO 事件和Tkinter循环的统一管理, 比单纯after 更加灵活高效。

代码示例:

import tkinter as tk
from tkinter import ttk
import RPi.GPIO as GPIO
import asyncio

async def main():
    # 树莓派引脚设置
    GPIO_PIN = 17
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(GPIO_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    
    root = tk.Tk()

    def p():
        print('hello')

    BUT_Quitter = ttk.Button(root, text="Quitter", command=root.destroy)
    BUT_Quitter.pack()
    BUT_display = ttk.Button(root, text="Hello", command=p)
    BUT_display.pack()


    async def wait_for_gpio_event(loop):
        future = loop.create_future()
        def gpio_callback(channel):
           loop.call_soon_threadsafe(future.set_result, None)

        GPIO.add_event_detect(GPIO_PIN, GPIO.FALLING, callback=gpio_callback, bouncetime=200)  # 检测下降沿
        await future #暂停直到set_result()被调用

    while True:
       try:
            await wait_for_gpio_event(asyncio.get_running_loop())
            BUT_display.invoke()

       except RuntimeError as e:
           if "Event loop is closed"  not in str(e):
               raise
           break
    root.destroy()
    GPIO.cleanup()


async def tk_main_loop(root): #使tk循环在asyncio loop 中运行
     while True:
        root.update() #更新所有事件, 如按键等等.
        await asyncio.sleep(0.01) #减少资源消耗

#把两个协程放在一起启动运行
loop = asyncio.get_event_loop()

loop.run_until_complete(asyncio.gather(
    main(),
    tk_main_loop(tk.Tk())  # 初始化tk, 传给协程
))

代码解释:

  • async def wait_for_gpio_event(loop):定义了等待GPIO 事件的异步函数。
  • tk_main_loop(root):异步运行 Tkinter。
  • 通过asyncio.gather()main()tk_main_loop() 在同一事件循环启动.

注意事项:

  • 代码较复杂。
  • 掌握 asyncio 的基本使用。

总结一下

这三种方案都能搞定GPIO和Tkinter按钮的联动. 各有利弊, 实际选择主要考虑以下几点:
1. 代码复杂度
2. 对外部的反应速度
3. CPU占用情况
一般项目使用方案二足矣。