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

如何在KivyMD窗口中实时显示ROS2发布者发送的消息

ROS2 rclpy.spin 与 KivyMD 事件循环互斥问题解决方案

问题根因

rclpy.spin()和MDApp.run()都是阻塞式主循环接口,同一线程同一时间只能运行其中一个:

  • rclpy.spin(node)启动后会持续占用线程处理ROS2通信回调,节点关闭前不会返回
  • MDApp.run()启动后会持续占用线程处理UI渲染、交互事件,窗口关闭前不会返回
    顺序执行两个调用时,前一个会一直阻塞线程,后一个完全没有执行机会,就会出现要么UI不更新、要么收不到ROS消息的现象。

方案1:Kivy时钟调度ROS2非阻塞自旋(推荐,无多线程风险)

不需要额外开线程,直接用Kivy的Clock定时调用非阻塞的rclpy.spin_once(),每次只处理当前积压的ROS2事件,处理完立刻返回控制权给Kivy主循环,把ROS2的事件处理嵌到UI循环的间隙执行。所有逻辑都在主线程运行,不存在跨线程操作UI的安全问题。

修正后的Python代码如下:

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

from kivy.lang import Builder
from kivymd.app import MDApp
from kivy.clock import Clock

# 全局缓存接收到的消息文本
textOutput = ""

class MainApp(MDApp):
    def on_start(self):
        # 每秒刷新一次UI显示
        Clock.schedule_interval(self.update_text, 1)
        # 每100ms处理一轮ROS2待执行事件,间隔可按需调整
        Clock.schedule_interval(self.ros_spin, 0.1)

    def build(self):
        self.theme_cls.theme_style = "Dark"
        self.theme_cls.primary_palette = "BlueGray"
        # 初始化ROS2节点
        rclpy.init()
        self.minimal_subscriber = MinimalSubscriber()
        return Builder.load_file('/home/cobot/dev_ws/src/py_pubsub/py_pubsub/ros_gui.kv')

    def update_text(self, dt):
        global textOutput
        self.root.ids.textOutputDisplay.text = textOutput

    def ros_spin(self, dt):
        # 非阻塞自旋,处理完当前队列的回调立刻返回
        rclpy.spin_once(self.minimal_subscriber, timeout_sec=0)

    def on_stop(self):
        # 窗口关闭时清理ROS2资源
        self.minimal_subscriber.destroy_node()
        rclpy.shutdown()


class MinimalSubscriber(Node):
    def __init__(self):
        super().__init__('minimal_subscriber')
        self.subscription = self.create_subscription(
            String,
            'topic',
            self.listener_callback,
            10)
        self.subscription  # 避免未使用变量警告

    def listener_callback(self, msg):
        global textOutput
        textOutput = msg.data
        self.get_logger().info('I heard: "%s"' % msg.data)


if __name__ == '__main__':
    MainApp().run()

原有.kv界面文件不需要任何修改,可直接使用。


方案2:子线程运行ROS2阻塞自旋

如果习惯用原有阻塞式spin的写法,可以把ROS2的spin逻辑放到独立守护线程运行,主线程专门跑Kivy UI循环。注意ROS2的订阅回调是在子线程执行,禁止直接在回调里修改UI控件属性,所有UI更新必须通过Kivy的Clock调度到主线程执行,否则会随机出现UI崩溃问题。

核心启动逻辑修改如下:

import threading
# 其余导入、类定义和方案1一致

def ros_spin_thread(node):
    rclpy.spin(node)

if __name__ == '__main__':
    rclpy.init()
    minimal_subscriber = MinimalSubscriber()
    # 启动守护线程跑ROS2自旋,主线程退出时线程自动终止
    spin_thread = threading.Thread(
        target=ros_spin_thread, 
        args=(minimal_subscriber,), 
        daemon=True
    )
    spin_thread.start()
    # 主线程启动Kivy UI
    MainApp().run()
    # UI退出后清理ROS2资源
    minimal_subscriber.destroy_node()
    rclpy.shutdown()

注意事项

  • 优先选择方案1,稳定性更高,调试成本更低。ROS自旋的调度间隔建议设置为10ms~100ms,间隔过小会占用过多CPU资源,间隔过大会增加消息接收延迟。
  • 必须在App退出回调中执行ROS2节点销毁和shutdown操作,避免进程退出后节点残留占用ROS2域名空间。
  • 如果后续要新增更多ROS2订阅、服务端逻辑,方案1不需要额外调整,所有回调都会正常被spin_once触发。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 14:24:20