Search code examples
pythonkivyros2

Kivy GUI not updating within the loop, updates when the loop ends


I want GUI to update within the loop but it updates only when the loop ends all the data within the screen and the screen itself doesn't change.

I tried threading and kivy.clock.Clock but nither of them worked.

My objective is to make this turtle move from GUI: - turtlesim window.

This is how my GUI home looks like: - Kivy GUI.

When I give turtle a target and hit 'Go' button from GUI home screen, the screen should change immediately and the changed screen should show the real time turtle coordinates. But instead what happens is that when the turtle reaches the given coordinates then the screen changes the the final coordinates is only displayed.

Changed screen and the last turtle coordinates: - changed screen

Final turtle position: - turtlesim window

This is my abit lengthy code: -

import rclpy
import threading
from geometry_msgs.msg import Twist
from turtlesim.msg import Pose
from math import atan2, pi
from matplotlib.pyplot import pause
from kivymd.app import MDApp
from kivy.lang import Builder
from kivy.core.window import Window
from kivymd.uix.datatables import MDDataTable
from kivy.metrics import dp
from kivy.config import Config
from kivy.clock import Clock

print('code started')                                                       # remove line when done
Config.set('graphics','resizable', False)
Window.size = (720, 480)


def isfloat(num):
    try:
        float(num)
        return True
    except ValueError:
        return False


class Gototarget(MDApp):
    print('App class started')                                              # remove line when done
    pose_x     = None
    pose_y     = None
    pose_theta = None


    def __init__(self, **kwargs):
        self.title = "node: '/gototarget'"
        super().__init__(**kwargs)

    
    def pose_listener(self, msg_sub_pose):
        self.pose_x     = msg_sub_pose.x
        self.pose_y     = msg_sub_pose.y
        self.pose_theta = msg_sub_pose.theta
        print('pose_listener ran')                                          # remove line when done


    def target_parameters(self, pose_x, pose_y, input_x, input_y):
        dist = pow((pow((input_x - pose_x),2) + pow((input_y - pose_y),2)),0.5)
        dirn = atan2((input_y - pose_y),(input_x - pose_x))
        print(f'''
        x coordinate  : {round(pose_x,3)}
        y coordinate  : {round(pose_y,3)}
        tar. distance : {round(dist,3)}''')
        return dist, dirn


    def turtle_state(self):
        print(f'''\nCurrent turtle state is: -
                x     : {self.pose_x}
                y     : {self.pose_y}
                theta : {self.pose_theta}
                ''')

    
    def user_input_call(self):
        print('user_input_call started')                                    # remove line when done
        while not self.root.ids.go.state == 'down':
            pause(0.1)
        inp_x = float(self.root.ids.x_coordinate.text)
        inp_y = float(self.root.ids.y_coordinate.text)
        print('user_input_call ended')                                      # remove line when done
        return inp_x, inp_y


    def build(self):
        print('build started')                                              # remove line when done

        rclpy.init()
        self.node             = rclpy.create_node('gototarget')
        self.pub_operator     = self.node.create_publisher(Twist, 'turtle1/cmd_vel', 10)
        self.sub_pose         = self.node.create_subscription(Pose, 'turtle1/pose', self.pose_listener, 10)
        self.msg_pub_operator = Twist()
        print('node created')                                               # remove line when done

        rclpy.spin_once(self.node)
        print('node spin first time')                                       # remove line when done

        self.input_x = float(self.pose_x)
        self.input_y = float(self.pose_y)
        self.spawn_x = self.input_x
        self.spawn_y = self.input_y

        # self.update_screen = Clock.create_trigger(self.root.ids.sm.current)
        self.update_data = Clock.create_trigger(self.dataupdate)

        self.root = Builder.load_string(
            '''
#:kivy 2.1.0
#:import NoTransition kivy.uix.screenmanager.NoTransition

MDScreen:
    MDToolbar:
        id: toolbar
        title: 'Command Turtle'
        pos_hint: {'top': 1}
        elevation: 15
        left_action_items: [['menu', lambda x: nav_drawer.set_state("open")]]
    MDNavigationLayout:
        id: nav_layout
        ScreenManager:
            id: sm
            transition: NoTransition()
            MDScreen:
                name: 'HomeScreen'
                MDLabel:
                    text: 'Enter target location for the turtle'
                    pos_hint: {"center_y": 0.8}
                    halign: 'center'
                MDTextField:
                    id: x_coordinate
                    hint_text: 'x coordinate'
                    pos_hint: {"center_x": 0.43, "center_y": 0.71}
                    size_hint: 0.12, None
                    helper_text_mode: 'persistent'
                    helper_text: ''
                    helper_text_color_normal: 1, 0, 0, 1
                    helper_text_color_focus: 1, 0, 0, 1
                    on_text:
                        self.helper_text = ''
                MDTextField:
                    id: y_coordinate
                    hint_text: 'y coordinate'
                    pos_hint: {"center_x": 0.57, "center_y": 0.71}
                    size_hint: 0.12, None
                    helper_text_mode: 'persistent'
                    helper_text: ''
                    helper_text_color_normal: 1, 0, 0, 1
                    helper_text_color_focus: 1, 0, 0, 1
                    on_text:
                        self.helper_text = ''
                MDRaisedButton:
                    id: go
                    text: 'Go'
                    pos_hint: {"center_x": 0.5, "center_y": 0.59}
                    elevation: 10
                    on_press:
                        app.textcheck()
                MDSeparator:
                    pos_hint: {"center_y": 0.5}
                MDLabel:
                    text: 'Current turtle state is: -'
                    pos_hint: {"center_x": 0.25, "center_y": 0.37}
                    halign: 'center'
                MDLabel:
                    text: 'x :'
                    size_hint_x: 0.25
                    pos_hint: {"center_x": 0.25, "center_y": 0.25}
                    halign: 'right'
                MDLabel:
                    text: 'y :'
                    size_hint_x: 0.25
                    pos_hint: {"center_x": 0.25, "center_y": 0.2}
                    halign: 'right'
                MDLabel:
                    size_hint_x: 0.25
                    text: 'theta :'
                    pos_hint: {"center_x": 0.25, "center_y": 0.15}
                    halign: 'right'
                MDLabel:
                    id : x
                    size_hint_x: 0.25
                    text: str(app.pose_x)
                    pos_hint: {"center_x": 0.55, "center_y": 0.25}
                    halign: 'left'
                MDLabel:
                    id : y
                    size_hint_x: 0.25
                    text: str(app.pose_y)
                    pos_hint: {"center_x": 0.55, "center_y": 0.2}
                    halign: 'left'
                MDLabel:
                    id : theta
                    size_hint_x: 0.25
                    text: str(app.pose_theta)
                    pos_hint: {"center_x": 0.55, "center_y": 0.15}
                    halign: 'left'
            MDScreen:
                name: 'TurtlePose'
                MDLabel:
                    text: 'Current turtle state: -'
                    size_hint_x: 0.3
                    pos_hint: {"center_x": 0.15, "center_y": 0.67}
                    halign: 'right'
                MDLabel:
                    id: turtle_state
                    text: 'Running'
                    size_hint_x: 0.25
                    pos_hint: {"center_x": 0.45, "center_y": 0.67}
                    halign: 'left'
                MDLabel:
                    text: 'x coordinate  :'
                    size_hint_x: 0.25
                    pos_hint: {"center_x": 0.25, "center_y": 0.55}
                    halign: 'right'
                MDLabel:
                    text: 'y coordinate  :'
                    size_hint_x: 0.25
                    pos_hint: {"center_x": 0.25, "center_y": 0.5}
                    halign: 'right'
                MDLabel:
                    size_hint_x: 0.25
                    text: 'target distance  :'
                    pos_hint: {"center_x": 0.25, "center_y": 0.45}
                    halign: 'right'
                MDLabel:
                    id: disp_x
                    size_hint_x: 0.30
                    text: ''
                    pos_hint: {"center_x": 0.58, "center_y": 0.55}
                    halign: 'left'
                MDLabel:
                    id: disp_y
                    size_hint_x: 0.30
                    text: ''
                    pos_hint: {"center_x": 0.58, "center_y": 0.5}
                    halign: 'left'
                MDLabel:
                    id: disp_dis
                    size_hint_x: 0.30
                    text: ''
                    pos_hint: {"center_x": 0.58, "center_y": 0.45}
                    halign: 'left'
                MDRaisedButton:
                    text: 'Enter another target'
                    pos_hint: {"center_x": 0.35, "center_y": 0.3}
                    elevation: 10
                    size_hint: None, None
                    width: root.width*0.2
                    on_press: sm.current = 'HomeScreen'
                MDRaisedButton:
                    text: 'Target History'
                    pos_hint: {"center_x": 0.65, "center_y": 0.3}
                    elevation: 10
                    size_hint: None, None
                    width: root.width*0.2
                    on_press: sm.current = 'TargetHistory'
            MDScreen:
                name: 'TargetHistory'
                id: 'history_screen'
                MDLabel:
                    text: 'Target history-coordinates are as follows: -'
                    size_hint_x: 0.5
                    pos_hint: {"center_x": 0.3, "center_y": 0.75}
                    halign: 'left'
                MDRaisedButton:
                    text: 'Enter another target'
                    pos_hint: {"center_x": 0.35, "center_y": 0.125}
                    elevation: 10
                    size_hint: None, None
                    width: root.width*0.2
                    on_press: sm.current = 'HomeScreen'
                MDRaisedButton:
                    text: 'Exit'
                    pos_hint: {"center_x": 0.65, "center_y": 0.125}
                    elevation: 10
                    size_hint: None, None
                    width: root.width*0.2
                    on_press: app.stop()
            MDScreen:
                name: 'AboutApp'
                MDLabel:
                    text: 'about'
        MDNavigationDrawer:
            id: nav_drawer
            orientation: 'vertical'
            padding: 10
            MDLabel:
                text: 'Command Turtle'
                size_hint_y: 0.1
            MDSeparator:
            MDSeparator:
            Widget:
                size_hint: None, 0.005
            MDSeparator:
            MDSeparator:
            ScrollView:
                padding: 10
                MDList:
                    padding: 10
                    OneLineListItem:
                        text: 'Target History'
                        on_press: sm.current = 'TargetHistory'; nav_drawer.set_state("close")
                    OneLineListItem:
                        text: 'About'
                        on_press: sm.current = 'AboutApp'; nav_drawer.set_state("close")
                    OneLineListItem:
                        text: 'Exit'
                        on_press: app.stop()
            '''
        )
        self.history_table = MDDataTable(
            column_data =[
                ("Target No.", dp(33)),
                ("x coordinate", dp(33)),
                ("y coordinate", dp(33)),
            ],
            row_data=[("@startup", str(self.spawn_x), str(self.spawn_y))],
            size_hint = (0.75, 0.5),
            pos_hint = {"center_x": 0.5, "center_y": 0.45},
            elevation = 10,
            anchor_x = "center"
        )
        self.root.ids.sm.get_screen('TargetHistory').add_widget(self.history_table)
        print('build ended')                                                # remove line when done


    def addtarget(self):
        sr_no = self.history_table.row_data[-1][0]
        if sr_no == "@startup":
            sr_no = 0
        else:
            sr_no = int(sr_no)
        self.history_table.row_data.insert(len(self.history_table.row_data), (str(sr_no + 1), str(self.root.ids.x_coordinate.text), str(self.root.ids.y_coordinate.text)))
        self.root.ids.x_coordinate.text = ''
        self.root.ids.y_coordinate.text = ''


    def textcheck(self):
        if not isfloat(self.root.ids.x_coordinate.text):
            self.root.ids.x_coordinate.helper_text = 'Invalid Input'
        elif not (float(self.root.ids.x_coordinate.text) <= 11.0) & (float(self.root.ids.x_coordinate.text) >= 0.0):
            self.root.ids.x_coordinate.helper_text = 'Input from 0 to 11'
        if not isfloat(self.root.ids.y_coordinate.text):
            self.root.ids.y_coordinate.helper_text = 'Invalid Input'
        elif not (float(self.root.ids.y_coordinate.text) <= 11.0) & (float(self.root.ids.y_coordinate.text) >= 0.0):
            self.root.ids.y_coordinate.helper_text = 'Input from 0 to 11'

        if (self.root.ids.x_coordinate.helper_text == '') & (self.root.ids.y_coordinate.helper_text == ''):
            inp_x = float(self.root.ids.x_coordinate.text)
            inp_y = float(self.root.ids.y_coordinate.text)
            if ((round(inp_x,1) == round(self.input_x,1)) & (round(inp_y,1) == round(self.input_y,1))):                         # Need Improvement
                print('\nW: turtle is already at this position. Please provide enough distance to move.\n')
                # popup needed to show this message
            else:
                print(f'x coordinate: {self.root.ids.x_coordinate.text} & y coordinate: {self.root.ids.y_coordinate.text}')
                self.input_x = inp_x
                self.input_y = inp_y
                # threading.Thread(target=self.posescreen).start()
                self.root.ids.sm.current = 'TurtlePose'
                # Clock.schedule_once(self.root.ids.sm.current, 0)
                # Clock.create_trigger(self.root.ids.sm.current)
                self.addtarget()
                self.runturtle()


    # def posescreen(self):
    #     self.root.ids.sm.current = 'TurtlePose'


    def dataupdate(self):
        self.root.ids.disp_x.text   = str(self.pose_x)
        self.root.ids.disp_y.text   = str(self.pose_y)
        self.root.ids.disp_dis.text = str(self.dist)
        self.root.ids.x.text        = str(self.pose_x)
        self.root.ids.y.text        = str(self.pose_y)
        self.root.ids.theta.text    = str(self.pose_theta)


    def runturtle(self):
        print('runturtle started')                                          # remove line when done
        pause(0.25)
        self.root.ids.turtle_state.text = 'Running'
        rclpy.spin_once(self.node)

        self.dist, dirn = self.target_parameters(self.pose_x, self.pose_y, self.input_x, self.input_y)

        # Clock.schedule_once(self.dataupdate, 0)
        # self.update_data()
        threading.Thread(target=self.dataupdate).start()

        while not round(self.dist,4) == 0.0009:
            print('turtle while loop started')                              # remove line when done
            rclpy.spin_once(self.node)
            rotation = dirn - self.pose_theta
            if abs(rotation) > pi:
                rotation = (rotation/abs(rotation))*(abs(rotation) - 2*pi)
            self.msg_pub_operator.angular.z = 1.5*rotation
            self.msg_pub_operator.linear.x  = 1*self.dist
            if abs(self.msg_pub_operator.linear.x)>1:
                self.msg_pub_operator.linear.x = ((self.msg_pub_operator.linear.x)/abs(self.msg_pub_operator.linear.x))
            self.dist, dirn = self.target_parameters(self.pose_x, self.pose_y, self.input_x, self.input_y)
            # Clock.schedule_once(self.dataupdate, 0)
            # self.update_data()
            threading.Thread(target=self.dataupdate).start()
            self.pub_operator.publish(self.msg_pub_operator)
            print('turtle while loop ended')                                # remove line when done

        self.msg_pub_operator.angular.z = 0.0
        self.msg_pub_operator.linear.x  = 0.0
        self.pub_operator.publish(self.msg_pub_operator)
        self.root.ids.turtle_state.text = 'Rest'
        print('runturtle ended')                                            # remove line when done

    print('App class ended')                                                # remove line when done


def main():

    print('main started')                                                   # remove line when done
    Gototarget().run()
    print('main ended')                                                     # remove line when done

print('code ended')                                                         # remove line when done

Solution

  • It looks like you are running a loop:

    while not round(self.dist,4) == 0.0009:
    

    on the main thread. As long as this loop is holding the main thread, no updates will be shown in your GUI. It is that loop that should be run in another thread, and the code that modifies the GUI display can be triggered from within that loop using Clock.schedule_once().