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 the Tello object to abstract and represent the Tello drone

  • ui.py is defines the User Interface to control your Tello drone

  • app.py is the application program, or the driver, 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)

Code

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 right

  • w go up

  • a turn counter-clockwise

  • s turn clockwise

  • d go down

  • l flip left

  • r flip right

  • f flip front

  • b 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

Code

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()

Code

8.4. Running the application

To run the program, type in the following from a terminal.

python app.py