I'm currently working on a Tkinter GUI for my DJI Tello, and I'm trying to make it so that when I command the drone to takeoff/land, the streamed video on the GUI does not freeze. I'm not too familiar with multithreading, but I looked the issue up and it seems like I'm not the only one encountering this. So I used what I found regarding threading and starting threads, and ended up with this line (more or less):
forward_button = Button(root, text="Takeoff/Land", font=("Verdana", 18), bg="#95dff3", command=threading.Thread(target=lambda: takeoff_land(flydo)).start)
Now when I press the button, the drone takes off and the video no longer freezes. However, when I click it again, the code throws an error:
RuntimeError: threads can only be started once
But I want my button to be able to have the drone take off when it's landed, and then land when it's flying. Is there a way I can do that?
Here is what I have so far (in the takeoff_land() function, I set up some testing code in place of the actual commands. Basically, I want it to be able to start printing 0, 1, 2... afterwards even if it's already printing.) Most of it is just GUI stuff, but I didn't want to omit anything that would break the code.
import cv2
import threading
from djitellopy import tello
from tkinter import *
from PIL import Image, ImageTk
import time
def takeoff_land(flydo):
'''Flydo takes off if not flying, lands if flying.'''
global flying
if flying:
for i in range(10):
print(i)
time.sleep(1)
# flydo.land()
flying = False
else:
for i in range(10):
print(i)
time.sleep(1)
# flydo.takeoff()
flying = True
def run_app(HEIGHT=800, WIDTH=800):
root = Tk()
flydo = tello.Tello()
flydo.connect()
flydo.streamon()
global flying
flying = False # To toggle between takeoff and landing for button
canvas = Canvas(root, height=HEIGHT, width=WIDTH)
# For background image
bg_dir = "C:\\Users\\charl\\Desktop\\flydo\\Tacit.jpg"
img = Image.open(bg_dir).resize((WIDTH, HEIGHT))
bg_label = Label(root)
bg_label.img = ImageTk.PhotoImage(img)
bg_label["image"] = bg_label.img
bg_label.place(x=0, y=0, relwidth=1, relheight=1)
# Display current battery
battery = Label(text=f"Battery: {int(flydo.get_battery())}%", font=("Verdana", 18), bg="#95dff3")
bat_width = 200
bat_height = 50
battery.config(width=bat_width, height=bat_height)
battery.place(x=(WIDTH - bat_width - 0.1*HEIGHT + bat_height), rely=0.9, relwidth=bat_width/WIDTH, relheight=bat_height/HEIGHT)
# Takeoff/Land button
forward_button = Button(root, text="Takeoff/Land", font=("Verdana", 18), bg="#95dff3", command=threading.Thread(target=lambda: takeoff_land(flydo)).start)
if threading.Thread(target=lambda: takeoff_land(flydo)).is_alive():
threading.Thread(target=lambda: takeoff_land(flydo)).join() # This doesn't kill the thread the way I want it to...
fb_width = 200
fb_height = 100
forward_button.config(width=fb_width, height=fb_height)
forward_button.place(x=(WIDTH/2 - fb_width/2), rely=0.61, relwidth=fb_width/WIDTH, relheight=fb_height/HEIGHT)
cap_label = Label(root)
cap_label.pack()
def video_stream():
h = 480
w = 720
frame = flydo.get_frame_read().frame
frame = cv2.resize(frame, (w, h))
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
img = Image.fromarray(cv2image)
imgtk = ImageTk.PhotoImage(image=img)
cap_label.place(x=WIDTH/2 - w/2, y=0)
cap_label.imgtk = imgtk
cap_label.configure(image=imgtk)
cap_label.after(5, video_stream)
video_stream()
canvas.pack()
root.mainloop()
if __name__ == "__main__":
HEIGHT = 800
WIDTH = 800
run_app(HEIGHT, WIDTH)
Ok so I actually managed to figure out the solution this morning based on this article:
https://bhaveshsingh0124.medium.com/multi-threading-on-python-tkinter-button-f0d9f759ad3e
Essentially, what I had to do was thread the Tello commands WITHIN the function that I'm calling with the button, rather than that function itself. Since the drone can only land OR take off, it can create a new thread each time one of those two commands is called. Here is the fixed code:
import cv2
import threading
from djitellopy import tello
from tkinter import *
from PIL import Image, ImageTk # You have to import this last or else Image.open throws an error
import time
def dummy_tello_fn():
for i in range(3):
print(i)
time.sleep(1)
def takeoff_land(flydo):
'''Flydo takes off if not flying, lands if flying.'''
global flying
if flying:
# threading.Thread(target=lambda: dummy_tello_fn()).start()
threading.Thread(target=lambda: flydo.land()).start()
flying = False
else:
# threading.Thread(target=lambda: dummy_tello_fn()).start()
threading.Thread(target=lambda: flydo.takeoff()).start()
flying = True
def run_app(HEIGHT=800, WIDTH=800):
root = Tk()
flydo = tello.Tello()
flydo.connect()
flydo.streamon()
global flying
flying = False # To toggle between takeoff and landing for button
canvas = Canvas(root, height=HEIGHT, width=WIDTH)
# For background image
bg_dir = "C:\\Users\\charl\\Desktop\\flydo\\Tacit.jpg"
img = Image.open(bg_dir).resize((WIDTH, HEIGHT))
bg_label = Label(root)
bg_label.img = ImageTk.PhotoImage(img)
bg_label["image"] = bg_label.img
bg_label.place(x=0, y=0, relwidth=1, relheight=1)
# Display current battery
battery = Label(text=f"Battery: {int(flydo.get_battery())}%", font=("Verdana", 18), bg="#95dff3")
bat_width = 200
bat_height = 50
battery.config(width=bat_width, height=bat_height)
battery.place(x=(WIDTH - bat_width - 0.1*HEIGHT + bat_height), rely=0.9, relwidth=bat_width/WIDTH, relheight=bat_height/HEIGHT)
# Takeoff/Land button
forward_button = Button(root, text="Takeoff/Land", font=("Verdana", 18), bg="#95dff3", command=lambda: takeoff_land(flydo))
fb_width = 200
fb_height = 100
forward_button.config(width=fb_width, height=fb_height)
forward_button.place(x=(WIDTH/2 - fb_width/2), rely=0.61, relwidth=fb_width/WIDTH, relheight=fb_height/HEIGHT)
cap_label = Label(root)
cap_label.pack()
def video_stream():
h = 480
w = 720
frame = flydo.get_frame_read().frame
frame = cv2.resize(frame, (w, h))
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
img = Image.fromarray(cv2image)
imgtk = ImageTk.PhotoImage(image=img)
cap_label.place(x=WIDTH/2 - w/2, y=0)
cap_label.imgtk = imgtk
cap_label.configure(image=imgtk)
cap_label.after(5, video_stream)
video_stream()
canvas.pack()
root.mainloop()
if __name__ == "__main__":
HEIGHT = 800
WIDTH = 800
run_app(HEIGHT, WIDTH)