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)