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

Tkinter整合Asyncio与Telemetrix-AIO连接ESP32报错求助

使用Telemetrix AIO ESP32结合Tkinter+Asyncio时连接失败的问题

环境信息

  • Windows 11
  • Python 3.11.5

背景

我用Python Telemetrix库控制ESP32,做了一个带Tkinter界面的应用,想实现运行时连接/断开ESP32的功能。官方WiFi闪烁示例可以正常运行,能成功连接并控制LED闪烁。

我写了一个纯Asyncio(未用threading)的示例代码,用自定义事件循环处理Tk窗口更新和用户操作触发的异步方法,这种方式在websockets库中运行正常,但连接ESP32时出错。

示例代码

import asyncio
import tkinter as tk
from telemetrix_aio_esp32 import telemetrix_aio_esp32

DIGITAL_PIN = 2

class App:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("TelemetrixAioEsp32 Blink Example")

        self.board = None

        self.ip_address_frame = tk.Frame(self.root)
        self.ip_address_frame.pack()

        self.ip_address_label = tk.Label(self.ip_address_frame, text="IP Address:")
        self.ip_address_label.pack(side="left")

        self.ip_address_entry = tk.Entry(self.ip_address_frame)
        self.ip_address_entry.pack(side="left", padx=5)
        self.ip_address_entry.insert("end",'192.168.1.196')  

        self.button_frame = tk.Frame(self.root)
        self.button_frame.pack()

        self.connect_button = tk.Button(self.button_frame, text="Connect", command=self.start_connect)
        self.connect_button.pack(side="left", padx=5)

        self.start_button = tk.Button(self.button_frame, text="Start Blink", command=self.start_blink)
        self.start_button.pack(side="left")
        self.start_button['state'] = tk.DISABLED

        self.request_blink = False
        self.request_connect = False

        self.event_loop = asyncio.get_event_loop()
        asyncio.set_event_loop(self.event_loop)

        self.showing = True

        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

    async def connect(self):
        if self.board is None:
            ip_address = self.ip_address_entry.get()
            self.board = telemetrix_aio_esp32.TelemetrixAioEsp32(transport_address=ip_address, loop=self.event_loop, autostart=False)
            await self.board.start_aio()

            self.connect_button["text"] = "Disconnect"
            self.start_button['state'] = tk.NORMAL
        else:

            await self.board.shutdown()

            self.board = None
            self.connect_button["text"] = "Connect"
            self.start_button['state'] = tk.DISABLED

    async def show(self):
        while self.showing:
            self.root.update()

            if self.request_blink:
                self.request_blink = False
                await self.blink(DIGITAL_PIN)
            if self.request_connect:
                self.request_connect = False
                await self.connect()

        if self.board is not None:
            await self.connect() 

    async def blink(self, pin):
        try:
            await self.board.set_pin_mode_digital_output(pin)

            for x in range(4):
                print('ON')
                await self.board.digital_write(pin, 0)
                await asyncio.sleep(1)
                print('OFF')
                await self.board.digital_write(pin, 1)
                await asyncio.sleep(1)

        except Exception as e:
            print(f"Error during blink: {e}")

    def start_blink(self):
        self.request_blink = True

    def start_connect(self):
        self.request_connect = True

    async def run_tk(self):
        try:
            await self.show();
        finally:
            self.event_loop.close()

    def run(self):
        asyncio.run(self.run_tk())

    def on_closing(self):
        self.showing = False
        self.root.destroy()

if __name__ == "__main__":
    app = App()
    app.run()

错误信息

PS C:\Users\lokno\Desktop\Scratch\Code> python .\blink_wifi_app.py
TelemetrixAioEsp32 Version: 1.3
Copyright (c) 2022 Alan Yorinks All rights reserved.

Successfully connected to: 192.168.1.196:31336
Traceback (most recent call last):
  File "C:\Users\lokno\Desktop\Scratch\Code\blink_wifi_app.py", line 110, in <module>
    app.run()
  File "C:\Users\lokno\Desktop\Scratch\Code\blink_wifi_app.py", line 102, in run
    asyncio.run(self.run_tk())
  File "C:\Users\lokno\scoop\apps\python\current\Lib\asyncio\runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "C:\Users\lokno\scoop\apps\python\current\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\lokno\scoop\apps\python\current\Lib\asyncio\base_events.py", line 653, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "C:\Users\lokno\Desktop\Scratch\Code\blink_wifi_app.py", line 97, in run_tk
    await self.show();
    ^^^^^^^^^^^^^^^^^
  File "C:\Users\lokno\Desktop\Scratch\Code\blink_wifi_app.py", line 69, in show
    await self.connect()
  File "C:\Users\lokno\Desktop\Scratch\Code\blink_wifi_app.py", line 48, in connect
    await self.board.start_aio()
  File "C:\Users\lokno\scoop\apps\python\current\Lib\site-packages\telemetrix_aio_esp32\telemetrix_aio_esp32.py", line 282, in start_aio
    raise RuntimeError('Could not retrieve server firmware version')
RuntimeError: Could not retrieve server firmware version

问题分析

我认为问题出在Asyncio和Telemetrix库的结合使用上。我传入了自定义事件循环并设置autostart=False,官方库中该参数的逻辑是:

if autostart:
    self.loop.run_until_complete(self.start_aio())

现在卡在了如何正确调用start_aio()上,需要调试建议。


解决方案

问题根源

start_aio()内部需要等待ESP32返回固件版本信息,但你的Tkinter更新逻辑(self.root.update())是同步阻塞调用,会抢占事件循环的执行时间,导致Telemetrix无法及时接收ESP32的响应,最终超时抛出错误。

修复步骤

  1. 替换同步的Tk更新方式:用asyncio.create_task让Tk的事件循环和Asyncio事件循环协同工作,避免用self.root.update()死循环阻塞。
  2. 调整事件循环的管理:避免手动创建/设置事件循环,让asyncio.run()自动管理,同时用asyncio.to_thread运行Tk的主循环。
  3. 用Asyncio原生事件标记请求:替代布尔变量,更符合异步编程的通知机制。

修复后的代码

import asyncio
import tkinter as tk
from telemetrix_aio_esp32 import telemetrix_aio_esp32

DIGITAL_PIN = 2

class App:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("TelemetrixAioEsp32 Blink Example")

        self.board = None
        self.request_blink = asyncio.Event()
        self.request_connect = asyncio.Event()

        # UI布局
        ip_address_frame = tk.Frame(self.root)
        ip_address_frame.pack()
        tk.Label(ip_address_frame, text="IP Address:").pack(side="left")
        self.ip_address_entry = tk.Entry(ip_address_frame)
        self.ip_address_entry.pack(side="left", padx=5)
        self.ip_address_entry.insert("end", '192.168.1.196')

        button_frame = tk.Frame(self.root)
        button_frame.pack()
        self.connect_button = tk.Button(button_frame, text="Connect", command=self.trigger_connect)
        self.connect_button.pack(side="left", padx=5)
        self.start_button = tk.Button(button_frame, text="Start Blink", command=self.trigger_blink, state=tk.DISABLED)
        self.start_button.pack(side="left")

        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
        self.running = True

    def trigger_connect(self):
        self.request_connect.set()

    def trigger_blink(self):
        self.request_blink.set()

    async def connect(self):
        if self.board is None:
            ip_address = self.ip_address_entry.get()
            # 不手动指定loop,让库自动使用当前事件循环
            self.board = telemetrix_aio_esp32.TelemetrixAioEsp32(transport_address=ip_address, autostart=False)
            await self.board.start_aio()
            self.connect_button["text"] = "Disconnect"
            self.start_button['state'] = tk.NORMAL
        else:
            await self.board.shutdown()
            self.board = None
            self.connect_button["text"] = "Connect"
            self.start_button['state'] = tk.DISABLED

    async def blink(self, pin):
        if not self.board:
            return
        try:
            await self.board.set_pin_mode_digital_output(pin)
            for _ in range(4):
                print('ON')
                await self.board.digital_write(pin, 0)
                await asyncio.sleep(1)
                print('OFF')
                await self.board.digital_write(pin, 1)
                await asyncio.sleep(1)
        except Exception as e:
            print(f"Error during blink: {e}")

    async def handle_events(self):
        while self.running:
            await asyncio.sleep(0.01)  # 主动让出事件循环时间
            if self.request_connect.is_set():
                self.request_connect.clear()
                await self.connect()
            if self.request_blink.is_set():
                self.request_blink.clear()
                await self.blink(DIGITAL_PIN)

    async def run_tk(self):
        # 用线程运行Tk主循环,避免阻塞Asyncio事件循环
        await asyncio.to_thread(self.root.mainloop)

    async def run(self):
        # 同时运行Tk主循环和事件处理任务
        task_tk = asyncio.create_task(self.run_tk())
        task_events = asyncio.create_task(self.handle_events())
        await asyncio.gather(task_tk, task_events)

    def on_closing(self):
        self.running = False
        self.root.destroy()

if __name__ == "__main__":
    app = App()
    asyncio.run(app.run())

关键改动说明

  • 用asyncio.Event替代布尔变量标记用户请求,更符合Asyncio的异步通知机制,避免竞态条件。
  • 将Tk的mainloop放在单独线程中运行,通过asyncio.to_thread让它和Asyncio事件循环并行,彻底解决阻塞问题。
  • 移除手动创建/设置事件循环的代码,让asyncio.run()自动管理当前事件循环,Telemetrix库会自动绑定这个循环。
  • 事件处理逻辑中加入await asyncio.sleep(0.01),主动让出事件循环时间,确保Telemetrix的网络通信任务能及时执行,接收ESP32的响应。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 01:35:05