8. Python, Manual Control
The code below will use Python to fly and control your Tello drone with your keyboard. There are three main Python files.
tello.py
defines theTello
object to abstract and represent the Tello droneui.py
is defines theUser Interface
to control your Tello droneapp.py
is the application program, or thedriver
, that will be executed as an entrypoint to start sending commands
8.1. Tello
The Tello
class is used to abstract the drone.
1import socket
2import threading
3import time
4
5class Tello(object):
6 """
7 Wrapper class to interact with the Tello drone.
8 """
9
10 def __init__(self, local_ip, local_port, imperial=False,
11 command_timeout=.3,
12 tello_ip='192.168.10.1',
13 tello_port=8889):
14 """
15 Binds to the local IP/port and puts the Tello into command mode.
16
17 :param local_ip: Local IP address to bind.
18 :param local_port: Local port to bind.
19 :param imperial: If True, speed is MPH and distance is feet.
20 If False, speed is KPH and distance is meters.
21 :param command_timeout: Number of seconds to wait for a response to a command.
22 :param tello_ip: Tello IP.
23 :param tello_port: Tello port.
24 """
25 self.abort_flag = False
26 self.command_timeout = command_timeout
27 self.imperial = imperial
28 self.response = None
29 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
30 self.tello_address = (tello_ip, tello_port)
31 self.last_height = 0
32 self.socket.bind((local_ip, local_port))
33
34 # thread for receiving cmd ack
35 self.receive_thread = threading.Thread(target=self._receive_thread)
36 self.receive_thread.daemon = True
37 self.receive_thread.start()
38
39 self.socket.sendto(b'command', self.tello_address)
40 print ('sent: command')
41
42 def __del__(self):
43 """
44 Closes the local socket.
45
46 :return: None.
47 """
48 self.socket.close()
49
50 def _receive_thread(self):
51 """
52 Listen to responses from the Tello.
53
54 Runs as a thread, sets self.response to whatever the Tello last returned.
55
56 :return: None.
57 """
58 while True:
59 try:
60 self.response, _ = self.socket.recvfrom(3000)
61 except socket.error as exc:
62 print(f'Caught exception socket.error : {exc}')
63
64 def send_command(self, command):
65 """
66 Send a command to the Tello and wait for a response.
67
68 :param command: Command to send.
69 :return: Response from Tello.
70 """
71 print(f'>> send cmd: {command}')
72 self.abort_flag = False
73 timer = threading.Timer(self.command_timeout, self.set_abort_flag)
74
75 self.socket.sendto(command.encode('utf-8'), self.tello_address)
76
77 timer.start()
78 while self.response is None:
79 if self.abort_flag is True:
80 break
81 timer.cancel()
82
83 if self.response is None:
84 response = 'none_response'
85 else:
86 response = self.response.decode('utf-8')
87
88 self.response = None
89
90 return response
91
92 def set_abort_flag(self):
93 """
94 Sets self.abort_flag to True.
95
96 Used by the timer in Tello.send_command() to indicate to that a response
97 timeout has occurred.
98
99 :return: None.
100 """
101 self.abort_flag = True
102
103 def takeoff(self):
104 """
105 Initiates take-off.
106
107 :return: Response from Tello, 'OK' or 'FALSE'.
108 """
109 return self.send_command('takeoff')
110
111 def set_speed(self, speed):
112 """
113 Sets speed.
114
115 This method expects KPH or MPH. The Tello API expects speeds from
116 1 to 100 centimeters/second.
117
118 Metric: .1 to 3.6 KPH
119 Imperial: .1 to 2.2 MPH
120
121 :param speed: Speed.
122 :return: Response from Tello, 'OK' or 'FALSE'.
123 """
124 speed = float(speed)
125
126 if self.imperial is True:
127 speed = int(round(speed * 44.704))
128 else:
129 speed = int(round(speed * 27.7778))
130
131 return self.send_command(f'speed {speed}')
132
133 def rotate_cw(self, degrees):
134 """
135 Rotates clockwise.
136
137 :param degrees: Degrees to rotate, 1 to 360.
138 :return:Response from Tello, 'OK' or 'FALSE'.
139 """
140 return self.send_command(f'cw {degrees}')
141
142 def rotate_ccw(self, degrees):
143 """
144 Rotates counter-clockwise.
145
146 :param degrees: Degrees to rotate, 1 to 360.
147 :return: Response from Tello, 'OK' or 'FALSE'.
148 """
149 return self.send_command(f'ccw {degrees}')
150
151 def flip(self, direction):
152 """
153 Flips.
154
155 :param direction: Direction to flip, 'l', 'r', 'f', 'b'.
156 :return: Response from Tello, 'OK' or 'FALSE'.
157 """
158 return self.send_command(f'flip {direction}')
159
160 def get_response(self):
161 """
162 Returns response of tello.
163
164 :return: Response of tello.
165 """
166 response = self.response
167 return response
168
169 def get_height(self):
170 """
171 Returns height(dm) of tello.
172
173 :return: Height(dm) of tello.
174 """
175 height = self.send_command('height?')
176 height = str(height)
177 height = filter(str.isdigit, height)
178 try:
179 height = int(height)
180 self.last_height = height
181 except:
182 height = self.last_height
183 pass
184 return height
185
186 def get_battery(self):
187 """
188 Returns percent battery life remaining.
189
190 :return: Percent battery life remaining.
191 """
192 battery = self.send_command('battery?')
193
194 try:
195 battery = int(battery)
196 except:
197 pass
198
199 return battery
200
201 def get_flight_time(self):
202 """
203 Returns the number of seconds elapsed during flight.
204
205 :return: Seconds elapsed during flight.
206 """
207 flight_time = self.send_command('time?')
208
209 try:
210 flight_time = int(flight_time)
211 except:
212 pass
213
214 return flight_time
215
216 def get_speed(self):
217 """
218 Returns the current speed.
219
220 :return: Current speed in KPH or MPH.
221 """
222 speed = self.send_command('speed?')
223
224 try:
225 speed = float(speed)
226
227 if self.imperial is True:
228 speed = round((speed / 44.704), 1)
229 else:
230 speed = round((speed / 27.7778), 1)
231 except:
232 pass
233
234 return speed
235
236 def land(self):
237 """
238 Initiates landing.
239
240 :return: Response from Tello, 'OK' or 'FALSE'.
241 """
242 return self.send_command('land')
243
244 def move(self, direction, distance):
245 """
246 Moves in a direction for a distance.
247
248 This method expects meters or feet. The Tello API expects distances
249 from 20 to 500 centimeters.
250
251 Metric: .02 to 5 meters
252 Imperial: .7 to 16.4 feet
253
254 :param direction: Direction to move, 'forward', 'back', 'right' or 'left'.
255 :param distance: Distance to move.
256 :return: Response from Tello, 'OK' or 'FALSE'.
257 """
258 distance = float(distance)
259
260 if self.imperial is True:
261 distance = int(round(distance * 30.48))
262 else:
263 distance = int(round(distance * 100))
264
265 return self.send_command(f'{direction} {distance}')
266
267 def move_backward(self, distance):
268 """
269 Moves backward for a distance.
270
271 See comments for Tello.move().
272
273 :param distance: Distance to move.
274 :return: Response from Tello, 'OK' or 'FALSE'.
275 """
276 return self.move('back', distance)
277
278 def move_down(self, distance):
279 """
280 Moves down for a distance.
281
282 See comments for Tello.move().
283
284 :param distance: Distance to move.
285 :return: Response from Tello, 'OK' or 'FALSE'.
286 """
287 return self.move('down', distance)
288
289 def move_forward(self, distance):
290 """
291 Moves forward for a distance.
292
293 See comments for Tello.move().
294
295 :param distance: Distance to move.
296 :return: Response from Tello, 'OK' or 'FALSE'.
297 """
298 return self.move('forward', distance)
299
300 def move_left(self, distance):
301 """
302 Moves left for a distance.
303
304 See comments for Tello.move().
305
306 :param distance: Distance to move.
307 :return: Response from Tello, 'OK' or 'FALSE'.
308 """
309 return self.move('left', distance)
310
311 def move_right(self, distance):
312 """
313 Moves right for a distance.
314
315 See comments for Tello.move().
316
317 :param distance: Distance to move.
318 :return: Response from Tello, 'OK' or 'FALSE'.
319 """
320 return self.move('right', distance)
321
322 def move_up(self, distance):
323 """
324 Moves up for a distance.
325
326 See comments for Tello.move().
327
328 :param distance: Distance to move.
329 :return: Response from Tello, 'OK' or 'FALSE'.
330 """
331 return self.move('up', distance)
8.2. User Interface
The TelloUI
class defines the User Interface (UI). The UI listens for the following keys pressed.
↑
go forward↓
go backward←
go left→
go rightw
go upa
turn counter-clockwises
turn clockwised
go downl
flip leftr
flip rightf
flip frontb
flip back
1import tkinter as tki
2from tkinter import Toplevel, Scale
3import threading
4import datetime
5import os
6import time
7import platform
8
9class TelloUI(object):
10 """
11 Wrapper class to enable the GUI.
12 """
13 def __init__(self, tello):
14 """
15 Initializes all the element of the GUI, supported by Tkinter
16
17 :param tello: class interacts with the Tello drone.
18 """
19 self.tello = tello # videostream device
20 self.thread = None # thread of the Tkinter mainloop
21 self.stopEvent = None
22
23 # control variables
24 self.distance = 0.1 # default distance for 'move' cmd
25 self.degree = 30 # default degree for 'cw' or 'ccw' cmd
26
27 # if the flag is TRUE,the auto-takeoff thread will stop waiting
28 # for the response from tello
29 self.quit_waiting_flag = False
30
31 # initialize the root window and image panel
32 self.root = tki.Tk()
33 self.panel = None
34
35 # create buttons
36 self.btn_landing = tki.Button(
37 self.root, text='Open Command Panel', relief='raised', command=self.openCmdWindow)
38 self.btn_landing.pack(side='bottom', fill='both',
39 expand='yes', padx=10, pady=5)
40
41 # start a thread that constantly pools the video sensor for
42 # the most recently read frame
43 self.stopEvent = threading.Event()
44
45 # set a callback to handle when the window is closed
46 self.root.wm_title('TELLO Controller')
47 self.root.wm_protocol('WM_DELETE_WINDOW', self.on_close)
48
49 # the sending_command will send command to tello every 5 seconds
50 self.sending_command_thread = threading.Thread(target = self._sendingCommand)
51
52 def _sendingCommand(self):
53 """
54 Starts a while loop that sends 'command' to tello every 5 second.
55
56 :return: None
57 """
58
59 while True:
60 self.tello.send_command('command')
61 time.sleep(5)
62
63 def _setQuitWaitingFlag(self):
64 """
65 Set the variable as TRUE; it will stop computer waiting for response from tello.
66
67 :return: None
68 """
69 self.quit_waiting_flag = True
70
71 def openCmdWindow(self):
72 """
73 Open the cmd window and initial all the button and text.
74
75 :return: None
76 """
77 panel = Toplevel(self.root)
78 panel.wm_title('Command Panel')
79
80 # create text input entry
81 text0 = tki.Label(panel,
82 text='This Controller map keyboard inputs to Tello control commands\n'
83 'Adjust the trackbar to reset distance and degree parameter',
84 font='Helvetica 10 bold'
85 )
86 text0.pack(side='top')
87
88 text1 = tki.Label(panel, text=
89 'W - Move Tello Up\t\t\tArrow Up - Move Tello Forward\n'
90 'S - Move Tello Down\t\t\tArrow Down - Move Tello Backward\n'
91 'A - Rotate Tello Counter-Clockwise\tArrow Left - Move Tello Left\n'
92 'D - Rotate Tello Clockwise\t\tArrow Right - Move Tello Right',
93 justify='left')
94 text1.pack(side='top')
95
96 self.btn_landing = tki.Button(
97 panel, text='Land', relief='raised', command=self.telloLanding)
98 self.btn_landing.pack(side='bottom', fill='both',
99 expand='yes', padx=10, pady=5)
100
101 self.btn_takeoff = tki.Button(
102 panel, text='Takeoff', relief='raised', command=self.telloTakeOff)
103 self.btn_takeoff.pack(side='bottom', fill='both',
104 expand='yes', padx=10, pady=5)
105
106 # binding arrow keys to drone control
107 self.tmp_f = tki.Frame(panel, width=100, height=2)
108 self.tmp_f.bind('<KeyPress-w>', self.on_keypress_w)
109 self.tmp_f.bind('<KeyPress-s>', self.on_keypress_s)
110 self.tmp_f.bind('<KeyPress-a>', self.on_keypress_a)
111 self.tmp_f.bind('<KeyPress-d>', self.on_keypress_d)
112 self.tmp_f.bind('<KeyPress-Up>', self.on_keypress_up)
113 self.tmp_f.bind('<KeyPress-Down>', self.on_keypress_down)
114 self.tmp_f.bind('<KeyPress-Left>', self.on_keypress_left)
115 self.tmp_f.bind('<KeyPress-Right>', self.on_keypress_right)
116 self.tmp_f.pack(side='bottom')
117 self.tmp_f.focus_set()
118
119 self.btn_landing = tki.Button(
120 panel, text='Flip', relief='raised', command=self.openFlipWindow)
121 self.btn_landing.pack(side='bottom', fill='both',
122 expand='yes', padx=10, pady=5)
123
124 self.distance_bar = Scale(panel, from_=0.02, to=5, tickinterval=0.01,
125 digits=3, label='Distance(m)',
126 resolution=0.01)
127 self.distance_bar.set(0.2)
128 self.distance_bar.pack(side='left')
129
130 self.btn_distance = tki.Button(panel, text='Reset Distance', relief='raised',
131 command=self.updateDistancebar,
132 )
133 self.btn_distance.pack(side='left', fill='both',
134 expand='yes', padx=10, pady=5)
135
136 self.degree_bar = Scale(panel, from_=1, to=360, tickinterval=10, label='Degree')
137 self.degree_bar.set(30)
138 self.degree_bar.pack(side='right')
139
140 self.btn_distance = tki.Button(panel, text='Reset Degree', relief='raised',
141 command=self.updateDegreebar)
142 self.btn_distance.pack(side='right', fill='both',
143 expand='yes', padx=10, pady=5)
144
145 def openFlipWindow(self):
146 """
147 Open the flip window and initial all the button and text.
148
149 :return: None
150 """
151 panel = Toplevel(self.root)
152 panel.wm_title('Gesture Recognition')
153
154 self.btn_flipl = tki.Button(
155 panel, text='Flip Left', relief='raised', command=self.telloFlip_l)
156 self.btn_flipl.pack(side='bottom', fill='both',
157 expand='yes', padx=10, pady=5)
158
159 self.btn_flipr = tki.Button(
160 panel, text='Flip Right', relief='raised', command=self.telloFlip_r)
161 self.btn_flipr.pack(side='bottom', fill='both',
162 expand='yes', padx=10, pady=5)
163
164 self.btn_flipf = tki.Button(
165 panel, text='Flip Forward', relief='raised', command=self.telloFlip_f)
166 self.btn_flipf.pack(side='bottom', fill='both',
167 expand='yes', padx=10, pady=5)
168
169 self.btn_flipb = tki.Button(
170 panel, text='Flip Backward', relief='raised', command=self.telloFlip_b)
171 self.btn_flipb.pack(side='bottom', fill='both',
172 expand='yes', padx=10, pady=5)
173
174 def telloTakeOff(self):
175 return self.tello.takeoff()
176
177 def telloLanding(self):
178 return self.tello.land()
179
180 def telloFlip_l(self):
181 return self.tello.flip('l')
182
183 def telloFlip_r(self):
184 return self.tello.flip('r')
185
186 def telloFlip_f(self):
187 return self.tello.flip('f')
188
189 def telloFlip_b(self):
190 return self.tello.flip('b')
191
192 def telloCW(self, degree):
193 return self.tello.rotate_cw(degree)
194
195 def telloCCW(self, degree):
196 return self.tello.rotate_ccw(degree)
197
198 def telloMoveForward(self, distance):
199 return self.tello.move_forward(distance)
200
201 def telloMoveBackward(self, distance):
202 return self.tello.move_backward(distance)
203
204 def telloMoveLeft(self, distance):
205 return self.tello.move_left(distance)
206
207 def telloMoveRight(self, distance):
208 return self.tello.move_right(distance)
209
210 def telloUp(self, dist):
211 return self.tello.move_up(dist)
212
213 def telloDown(self, dist):
214 return self.tello.move_down(dist)
215
216 def updateDistancebar(self):
217 self.distance = self.distance_bar.get()
218 print(f'reset distance to {self.distance:.1f}')
219
220 def updateDegreebar(self):
221 self.degree = self.degree_bar.get()
222 print(f'reset distance to {self.degree}')
223
224 def on_keypress_w(self, event):
225 print(f'up {self.distance} m')
226 self.telloUp(self.distance)
227
228 def on_keypress_s(self, event):
229 print(f'down {self.distance} m')
230 self.telloDown(self.distance)
231
232 def on_keypress_a(self, event):
233 print(f'ccw {self.degree} degree')
234 self.tello.rotate_ccw(self.degree)
235
236 def on_keypress_d(self, event):
237 print(f'cw {self.degree} m')
238 self.tello.rotate_cw(self.degree)
239
240 def on_keypress_up(self, event):
241 print(f'forward {self.distance} m')
242 self.telloMoveForward(self.distance)
243
244 def on_keypress_down(self, event):
245 print(f'backward {self.distance} m')
246 self.telloMoveBackward(self.distance)
247
248 def on_keypress_left(self, event):
249 print(f'left {self.distance} m')
250 self.telloMoveLeft(self.distance)
251
252 def on_keypress_right(self, event):
253 print(f'right {self.distance} m')
254 self.telloMoveRight(self.distance)
255
256 def on_close(self):
257 """
258 Sets the stop event, cleanup the camera, and allow the rest of
259 the quit process to continue.
260 :return: None
261 """
262 print('[INFO] closing...')
263 self.stopEvent.set()
264 del self.tello
265 self.root.quit()
266
8.3. Application
The app.py
file is the application program entry point.
1import tello
2from ui import TelloUI
3
4
5def main():
6 drone = tello.Tello('', 8889)
7 vplayer = TelloUI(drone)
8 vplayer.root.mainloop()
9
10
11if __name__ == '__main__':
12 main()
8.4. Running the application
To run the program, type in the following from a terminal.
python app.py