You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

树莓派GPIO回调事件中终止while循环问题求助

问题分析与修复方案

嘿,刚看完你的代码,发现两个核心问题导致你没法终止OLED程序,咱们一步步来解决:

问题1:局部变量搞砸了状态控制

你在oledfunc里写了var = var,但这个var是函数内部的局部变量——也就是说,你在offbutton回调里调用oledfunc(1),其实是新开了一个oledfunc的实例,完全碰不到之前启动的那个循环里的var值。原来的循环还在拿着最初的var=2一直跑,自然不会停下来。

问题2:主线程被阻塞,GPIO回调根本跑不起来

当你按下启动按钮,button_callback调用oledfunc(2),这个函数里的while循环会直接占满主线程。而Python的GPIO事件回调是在主线程里处理的,主线程被OLED循环卡死了,就算你按下停止按钮,offbutton回调也没机会执行,程序自然没反应。


修复方案:用线程分离任务+全局状态控制

我们需要把OLED的循环放到单独的线程里,让主线程能专心监听GPIO事件;同时用一个全局变量(或者线程安全的事件)来统一控制启停。

第一步:重构主脚本,用线程管理OLED

把主脚本改成这样,核心是用线程跑OLED,主线程留着处理GPIO:

from oledscriptwithfunc import start_oled_loop, stop_oled_loop
from reset import rstfunc
import RPi.GPIO as GPIO
import threading

# 全局状态变量:控制OLED是否运行
oled_running = False
oled_thread = None

def button_callback(channel):
    global oled_running, oled_thread
    if not oled_running:
        print("Initializing..")
        oled_running = True
        # 新开线程运行OLED循环,不阻塞主线程
        oled_thread = threading.Thread(target=start_oled_loop)
        oled_thread.start()

def offbutton(channel):
    global oled_running
    if oled_running:
        print("Ending program..")
        oled_running = False
        # 等待OLED线程完全退出
        if oled_thread is not None:
            oled_thread.join()
        rstfunc() # 重置OLED屏幕

GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(10, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(9, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
# 加上防抖,避免开关抖动触发多次
GPIO.add_event_detect(10, GPIO.RISING, callback=button_callback, bouncetime=300)
GPIO.add_event_detect(9, GPIO.RISING, callback=offbutton, bouncetime=300)

message = input("Press enter to quit\n")
# 退出前确保OLED线程停止
if oled_running:
    oled_running = False
    oled_thread.join()
GPIO.cleanup()

第二步:重构oledscriptwithfunc.py,用全局状态控制循环

把原来的oledfunc改成依赖全局状态的循环,而且OLED初始化只做一次(原来每次调用都初始化太浪费资源):

import digitalio
import busio
import board
from PIL import Image, ImageDraw, ImageFont
import requests
import adafruit_ssd1306
import datetime
import time

# 全局状态变量,和主脚本共享
oled_running = False

# 只初始化一次OLED设备,避免重复初始化
i2c = busio.I2C(board.SCL, board.SDA)
oled = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c, addr=0x3c)
oled.fill(0)
oled.show()
image = Image.new("1", (128, 32))
draw = ImageDraw.Draw(image)
font = ImageFont.load_default()

def start_oled_loop():
    global oled_running
    timer1 = time.perf_counter()
    # 循环判断全局状态
    while oled_running:
        timer2 = time.perf_counter()
        # 每30分钟更新一次天气数据
        if int(timer2 - timer1) < 1 or int(timer2 - timer1) % 1800 == 0:
            # 这里可以加个异常处理,避免网络挂了程序崩掉
            try:
                response = requests.get("https://api.openweathermap.org/data/2.5/weather?lat=xxxx&lon=xxxxx&lang=de&units=metric&appid=xxxxxxxxxxxxxxxxxx")
                response.raise_for_status()
                data = response.json()
                status = data['weather'][0]['description']
                temp = data['main']['temp']
                temptext = f"{temp}°C"
                timer1 = time.perf_counter() # 重置计时器
            except Exception as e:
                print(f"获取天气失败: {e}")
                time.sleep(60) # 失败了等1分钟再试
                continue
        
        # 显示时间10秒,期间也要检查是否要停止
        t_end = time.time() + 10
        while time.time() < t_end and oled_running:
            now = datetime.datetime.now()
            date_str = now.strftime("%d-%m-%Y")
            time_str = now.strftime("%M:%S")
            hour = int(now.strftime("%H")) + 2
            text = f"{date_str} {hour}:{time_str}"
            draw.rectangle((0, 0, 128, 32), outline=255, fill=255)
            draw.rectangle((6, 6, 122, 26), outline=0, fill=0)
            font_width, font_height = font.getsize(text)
            draw.text((oled.width//2 - font_width//2, oled.height//2 - font_height//2), text, font=font, fill=255)
            oled.image(image)
            oled.show()
            time.sleep(0.1) # 加个小延迟,降低CPU占用
        
        if not oled_running:
            break
        
        # 显示温度5秒
        draw.rectangle((0, 0, 128, 32), outline=0, fill=0)
        font_width, font_height = font.getsize(temptext)
        draw.text((oled.width//2 - font_width//2, oled.height//2 - font_height//2), temptext, font=font, fill=255)
        oled.image(image)
        oled.show()
        time.sleep(5)
        
        if not oled_running:
            break
        
        # 显示天气状态5秒
        draw.rectangle((0, 0, 128, 32), outline=0, fill=0)
        font_width, font_height = font.getsize(status)
        draw.text((oled.width//2 - font_width//2, oled.height//2 - font_height//2), status, font=font, fill=255)
        oled.image(image)
        oled.show()
        time.sleep(5)
    
    # 停止后清空OLED屏幕
    oled.fill(0)
    oled.show()

def stop_oled_loop():
    global oled_running
    oled_running = False

关键修改点说明

  1. 线程分离:OLED循环在单独线程运行,主线程可以正常响应GPIO事件,不会被阻塞。
  2. 全局状态变量:oled_running在主脚本和OLED模块之间共享,修改它就能直接控制循环启停。
  3. 循环内状态检查:在每个子循环(显示时间、温度、状态)里都检查oled_running,确保按下停止按钮后能立刻退出,不用等当前子循环结束。
  4. 防抖处理:给GPIO事件加了bouncetime=300,避免开关抖动导致多次触发。
  5. 异常处理:天气API请求加了异常捕获,避免网络问题导致程序崩溃。

额外优化建议

  • 更推荐用threading.Event代替全局变量,线程安全更高:比如定义stop_event = threading.Event(),用stop_event.wait(0.1)代替time.sleep(0.1),用stop_event.set()触发停止,这样不会有全局变量的竞态问题。
  • 可以把天气API的参数(lat、lon、appid)放到配置文件里,方便修改。

内容的提问来源于stack exchange,提问作者aperturelabs

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 15:32:29