如何在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
相关产品推荐
相关产品推荐

