1

I am currently trying to program a robot arm which is controlled by a Raspberry Pi.

Everything works fine so far, except for one thing and I already googled and tried everything for many hours but can't find a working solution.

For the movement of the robot arm it is necessary to run all motors "simultaneously" with threads (works fine).

The problem I have is that I need to update a label which shows the current angle of an axis (motor) as soon as it finished its movement but other motors are still running (threads).

After a lot of research I thought I found the solution by using a queue and Tkinters after-method. But it still doesn't work as the labels text only gets updated after all threads terminated.

I wrote an example code where I want to get a label update for motor "one" which will finish its for-loop (100 iterations) before motor "two" (500 iterations). I expected the label to get updated as soon as motor one reached its target while motor two is still runing.

But although I used the after-method it still waits till motor two finished before updating the label.

Hope you can help me!

from tkinter import *
import threading
import time
from queue import * 


class StepperMotors:

    def __init__(self, root):
        self.root = root

        self.start_btn = Button(root, text="Start", command=lambda:self.start_movement())
        self.start_btn.config(width = 10)
        self.start_btn.grid(row=1,column=1)

        self.label_one = Label(root, text='')
        self.label_one.config(width = 10)
        self.label_one.grid(row=2, column=1)

        self.label_two = Label(root, text='')
        self.label_two.config(width = 10)
        self.label_two.grid(row=3, column=1)

    def start_movement(self):
        self.thread_queue = Queue()
        self.root.after(100, self.wait_for_finish) 

        thread_one = threading.Thread(target=self.motor_actuation, args=(1,100))
        thread_two = threading.Thread(target=self.motor_actuation, args=(2,500))

        thread_one.start()
        thread_two.start()

        thread_one.join()
        thread_two.join()

    def motor_actuation(self, motor, iterations):  
        for i in range(iterations):
            i = i+1  
            update_text = str(motor) + " " + str(i) + "\n"
            print(update_text)
            time.sleep(0.01)

        self.thread_queue.put(update_text)

    def wait_for_finish(self):
        try:      
            self.text = self.thread_queue.get()  
            self.label_one.config(text=self.text)  

        except self.thread_queue.empty():
            self.root.after(100, self.wait_for_finish)

if __name__ == "__main__":
    root = Tk()
    root.title("test")
    stepper = StepperMotors(root)
    root.mainloop()
timosmd
  • 159
  • 11
  • 1
    You should not call `.join()` as it will block the tkinter main loop. Also, the logic in your `wait_for_finish()` function will stop schedule itself when first motor finish. – acw1668 Feb 08 '20 at 12:58
  • Thanks! But the .join() method is very important for another part of the program (which is not shown here). Is there another way to use join AND update tkinter somehow during threading? – timosmd Feb 08 '20 at 13:40
  • @timosmd Your implementation of `def wait_for_finish` is erroneous, compare with [use threads to preventing main event loop from “freezing”](https://stackoverflow.com/a/16747734/7414759) – stovfl Feb 08 '20 at 13:51
  • Thanks, I will have a look at it! – timosmd Feb 08 '20 at 14:09
  • ***"another way to use join AND update tkinter ""***: No, it's contradict. Read [Tkinter understanding mainloop](https://stackoverflow.com/questions/29158220/tkinter-understanding-mainloop) – stovfl Feb 08 '20 at 14:29

1 Answers1

0

It’s better to use daemon thread which are non-blocking.

Also, It’s better to have a separation of concerns: a robot (or robot arm) can be a object which has its own life time: a daemon thread. Idem, you can define a "LabelUpdater" which read the state of a robot and update a label.

Let’s define a robot:

  • it is created on application initialisation and is run when the user click the "Start" button,
  • The robot moves and reports its angle in a app-level multithreading-queue,
class Robot(threading.Thread):

    def __init__(self, name: str, label_queue: queue.Queue, end_pos: int):
        super().__init__(name=name)
        self.daemon = True
        self.label_queue = label_queue
        self.end_pos = end_pos

    def run(self) -> None:
        for angle in range(self.end_pos):
            self.label_queue.put(angle)
            time.sleep(0.01)

Let’s define a LabelUpdater:

  • It is created on application initialisation and run forever (it can observe a robot even if it is not running).
  • It reads the robot queue (for instance each second to avoid blinking) and update the label
class LabelUpdater(threading.Thread):
    def __init__(self, name: str, label_queue: queue.Queue, root_app: tkinter.Tk, variable: tkinter.Variable):
        super().__init__(name=name)
        self.daemon = True
        self.label_queue = label_queue
        self.root_app = root_app
        self.variable = variable

    def run(self) -> None:
        # run forever
        while True:
            # wait a second please
            time.sleep(1)
            # consume all the queue and keep only the last message
            last_msg = None
            while True:
                try:
                    msg = self.label_queue.get(block=False)
                except queue.Empty:
                    break
                last_msg = msg
                self.label_queue.task_done()
            if last_msg:
                self.variable.set(last_msg)

Then, the main application should define:

  • 2 multithreading queues: one for each label,
  • 2 tkinter.StringVar variable which will be updated,
  • the robots and the label updaters,
  • the two updaters are started, and will run forever.
class StepperMotors:
    def __init__(self, root):
        self.root = root
        self.label_one_queue = queue.Queue()
        self.label_two_queue = queue.Queue()

        self.start_btn = tkinter.Button(root, text="Start", command=lambda: self.start_movement())
        self.start_btn.config(width=10)
        self.start_btn.grid(row=1, column=1)

        self.text_one = tkinter.StringVar()
        self.text_one.set("one")
        self.label_one = tkinter.Label(root, textvariable=self.text_one)
        self.label_one.config(width=10)
        self.label_one.grid(row=2, column=1)

        self.text_two = tkinter.StringVar()
        self.text_two.set("two")
        self.label_two = tkinter.Label(root, textvariable=self.text_two)
        self.label_two.config(width=10)
        self.label_two.grid(row=3, column=1)

        self.robot_one = Robot("robot_one", self.label_one_queue, 100)
        self.robot_two = Robot("robot_two", self.label_two_queue, 500)

        self.updater_one = LabelUpdater("updater_one", self.label_one_queue, self.root, self.text_one)
        self.updater_two = LabelUpdater("updater_two", self.label_two_queue, self.root, self.text_two)
        self.updater_one.start()
        self.updater_two.start()

    def start_movement(self):
        self.robot_one.start()
        self.robot_two.start()

Of course, you need a flag or something to check that each robot is not already running.

Laurent LAPORTE
  • 21,958
  • 6
  • 58
  • 103
  • ***"not necessary."***: I think so too, consider to remove it. Your whole `class LabelUpdater(` should be run in the `tkinter.mainloop`. – stovfl Feb 09 '20 at 12:16