python-3.xkivymdros2

How Can I Display a Message From a ROS2 Publisher In a KivyMD Window?


What I'm trying to do is essentially take example code to set up and run a Subscriber and Publisher using ROS2 (found Here) and set up the Subscriber python script to use KivyMD to display the Message that it receives from the Publisher python script by updating a simple MDLabel text every second with a variable that the Subscriber callback updates (Note: I currently don't have the code trying to do this yet, as my issue doesn't pertain to it at the moment).

I have no idea what the 'best practice' for going about this would be, since I looked but couldn't find anybody who had done this, aside from some YouTube videos of someone doing it with ROS, but I need to use ROS2, and his tutorial doesn't help with that. So I'm pretty much just winging it.

The problem that I'm finding when trying to do this is that they both work, per se, but only one can work at a time, it seems? If in my script, at the end, I run main() (ROS2 code) before MainApp() (KivyMD code), then when I run both the Subscriber and Publisher files in separate terminals, the ROS2 functionality works fine. The Publisher's message reaches the Subscriber. However, the KivyMD window that pops up doesn't populate with the message.

The reverse is true as well, in that if I switch the position of main() and MainApp() and run the KivyMD code before the ROS2 code, then the KivyMD window shows up and populates with the placeholder text (found in .kv file), but the Subscriber doesn't hear the Publisher.

The issue might be obvious but I just can't see it, and I might have been working on this problem for too long to realize it. Can anyone help out?

Here's the python script for the Subscriber Node:

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

global textOutput

textOutput = ""

class MainApp(MDApp):
    def on_start(self):
        Clock.schedule_interval(self.update_text, 1)

    def build(self):
        self.theme_cls.theme_style = "Dark"
        self.theme_cls.primary_palette = "BlueGray"

        return Builder.load_file('/home/cobot/dev_ws/src/py_pubsub/py_pubsub/ros_gui.kv')

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


class MinimalSubscriber(Node):

    def __init__(self):
        super().__init__('minimal_subscriber')
        self.subscription = self.create_subscription(
            String,
            'topic',
            self.listener_callback,
            10)
        self.subscription  # prevent unused variable warning

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


def main(args=None):
    rclpy.init(args=args)

    minimal_subscriber = MinimalSubscriber()
    rclpy.spin(minimal_subscriber)

    # Destroy the node explicitly
    # (optional - otherwise it will be done automatically
    # when the garbage collector destroys the node object)
    # minimal_subscriber.destroy_node()
    rclpy.shutdown()



if __name__ == '__main__':
    
    main()

    MainApp().run()

And here's the code for the .kv file I'm using for KivyMD:

MDScreen:

    MDBoxLayout:
        padding: dp(4), dp(4)
        spacing: dp(4)
        MDLabel:
            id: textOutputDisplay
            text: 'Output Text'
            font_style: 'H1'
            halign: 'center'

Solution

  • I recently had a similar problem as you, but I was using Tkinter instead of Kivy.

    It seems like when you call the main() function, the program loops inside the rclpy.spin() function, constantly listening for new messages. Thus, your Kivy interface doesn't get updated.

    The other way around, when you exchange the place of the main() call and the MainApp().run() call. In this case, the Kivy interface starts first and listens for user input or output, hence the thread is blocked and the ROS2-functionality doesn't start.


    What might help is running both calls parallel in separate threads.
    The easiest way would be to import the threading package:

    import threading

    Then, you can start a new thread for the ROS2-function and start the Kivy-interface afterwards in the main thread as usual:

    process_thread = threading.Thread(target=main)
    process_thread.start()
    MainApp().run()
    

    It is important here to set target=main, NOT target=main(). Otherwise, you would run the main function immediately intead of passing it to threading.Thread.

    Hope, I could help!