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

Kivy多线程实现串口读取与用户输入同步的响应异常问题

问题分析与修复方案

你的问题根源有三个核心点:

  • Clock调度错误:Clock.schedule_interval必须在Kivy的主线程(UI线程)中调用,你在子线程里启动这个调度,会导致事件循环被大量无效任务挤占,最终阻塞UI响应。
  • 串口资源竞争:按钮点击和标签更新线程同时直接操作串口对象ser,没有加锁同步,导致串口读写阻塞,操作堆积。
  • 程序终止方式错误:直接调用exit()不会正确清理Kivy的事件循环和串口资源,导致终止滞后。

修复步骤

  1. 重构标签更新逻辑:子线程只负责串口读写获取数据,通过主线程的Clock.schedule_once来更新UI,避免子线程操作UI相关的调度。
  2. 添加串口锁:用threading.Lock()保护所有串口读写操作,防止多线程竞争。
  3. 正确终止程序:用App.get_running_app().stop()替代exit(),确保资源正常释放。

修改后的完整代码

import threading
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.lang import Builder
from kivy.clock import Clock
import serial

# 全局串口锁,保护串口操作
ser_lock = threading.Lock()

Clock.max_iteration = 20

ser = serial.Serial()
ser.port = "/dev/ttyUSB0"
ser.baudrate = 9600
ser.bytesize = serial.EIGHTBITS
ser.parity = serial.PARITY_NONE
ser.stopbits = serial.STOPBITS_ONE
ser.timeout = 1
ser.xonxoff = False
ser.rtscts = False
ser.dsrdtr = False
ser.writeTimeout = 2

Builder.load_file('MyApp.kv')

class MyLayout(Widget):
    def Button1(self):
        with ser_lock:
            # 串口读写操作,根据实际协议调整指令
            ser.write(b"Button1_CMD\n")
            response = ser.readline().decode().strip()
            # 调度主线程更新UI
            Clock.schedule_once(lambda dt: self.update_label1(response), 0)
        print("Write Command: Button1")

    def Button2(self):
        with ser_lock:
            ser.write(b"Button2_CMD\n")
            response = ser.readline().decode().strip()
            Clock.schedule_once(lambda dt: self.update_label2(response), 0)
        print("Write Command: Button2")

    def Button3(self):
        with ser_lock:
            ser.write(b"Button3_CMD\n")
            response = ser.readline().decode().strip()
            Clock.schedule_once(lambda dt: self.update_label3(response), 0)
        print("Write Command: Button3")

    def exitButton(self):
        print("Program terminated")
        App.get_running_app().stop()

    # 示例标签更新方法,需根据你的kv文件调整控件ID
    def update_label1(self, text):
        self.ids.label1.text = text
        print(f"Update Data: label1 - {text}")

    def update_label2(self, text):
        self.ids.label2.text = text
        print(f"Update Data: label2 - {text}")

    def update_label3(self, text):
        self.ids.label3.text = text
        print(f"Update Data: label3 - {text}")

class MyApp(App):
    def build(self):
        return MyLayout()  

    def on_start(self):
        # 启动守护子线程,定时获取串口数据
        threading.Thread(target=self.poll_serial_data, daemon=True).start()
  
    def poll_serial_data(self):
        while True:
            with ser_lock:
                # 发送获取数据的指令,根据实际协议调整
                ser.write(b"GET_DATA_ALL\n")
                data1 = ser.readline().decode().strip()
                data2 = ser.readline().decode().strip()
                data3 = ser.readline().decode().strip()
            
            # 调度主线程更新UI
            Clock.schedule_once(lambda dt: self.root.update_label1(data1), 0)
            Clock.schedule_once(lambda dt: self.root.update_label2(data2), 0)
            Clock.schedule_once(lambda dt: self.root.update_label3(data3), 0)
            
            # 线程休眠2秒,避免占用过多CPU
            threading.Event().wait(2)

if __name__ == '__main__':
    try:
        ser.open()
    except Exception as e:
        print(f"error opening serial port: {e}") 
        exit()
    if ser.isOpen():
        try:
            ser.flushInput()
            ser.flushOutput()
        except Exception as e:    
            print(f"Error communication: {e}")
    else: 
        print("Error communicating")
    MyApp().run()
    # 程序结束后关闭串口
    if ser.isOpen():
        ser.close()

额外说明

  • 守护线程:设置daemon=True确保子线程在主线程退出时自动终止,避免残留线程占用资源。
  • UI更新安全:所有UI操作必须通过Clock.schedule_once在主线程执行,Kivy不允许子线程直接修改UI元素。
  • 串口锁的必要性:串口是独占资源,多线程同时读写会导致数据混乱或阻塞,锁能保证同一时间只有一个线程操作串口。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 17:45:29