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.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
import socket
import threading
import time

class Tello(object):
    """
    Wrapper class to interact with the Tello drone.
    """

    def __init__(self, local_ip, local_port, imperial=False, 
                 command_timeout=.3, 
                 tello_ip='192.168.10.1',
                 tello_port=8889):
        """
        Binds to the local IP/port and puts the Tello into command mode.

        :param local_ip: Local IP address to bind.
        :param local_port: Local port to bind.
        :param imperial: If True, speed is MPH and distance is feet. 
                         If False, speed is KPH and distance is meters.
        :param command_timeout: Number of seconds to wait for a response to a command.
        :param tello_ip: Tello IP.
        :param tello_port: Tello port.
        """
        self.abort_flag = False
        self.command_timeout = command_timeout
        self.imperial = imperial
        self.response = None  
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.tello_address = (tello_ip, tello_port)
        self.last_height = 0
        self.socket.bind((local_ip, local_port))

        # thread for receiving cmd ack
        self.receive_thread = threading.Thread(target=self._receive_thread)
        self.receive_thread.daemon = True
        self.receive_thread.start()

        self.socket.sendto(b'command', self.tello_address)
        print ('sent: command')

    def __del__(self):
        """
        Closes the local socket.

        :return: None.
        """
        self.socket.close()

    def _receive_thread(self):
        """
        Listen to responses from the Tello.

        Runs as a thread, sets self.response to whatever the Tello last returned.

        :return: None.
        """
        while True:
            try:
                self.response, _ = self.socket.recvfrom(3000)
            except socket.error as exc:
                print(f'Caught exception socket.error : {exc}')

    def send_command(self, command):
        """
        Send a command to the Tello and wait for a response.

        :param command: Command to send.
        :return: Response from Tello.
        """
        print(f'>> send cmd: {command}')
        self.abort_flag = False
        timer = threading.Timer(self.command_timeout, self.set_abort_flag)

        self.socket.sendto(command.encode('utf-8'), self.tello_address)

        timer.start()
        while self.response is None:
            if self.abort_flag is True:
                break
        timer.cancel()
        
        if self.response is None:
            response = 'none_response'
        else:
            response = self.response.decode('utf-8')

        self.response = None

        return response
    
    def set_abort_flag(self):
        """
        Sets self.abort_flag to True.

        Used by the timer in Tello.send_command() to indicate to that a response        
        timeout has occurred.

        :return: None.
        """
        self.abort_flag = True

    def takeoff(self):
        """
        Initiates take-off.

        :return: Response from Tello, 'OK' or 'FALSE'.
        """
        return self.send_command('takeoff')

    def set_speed(self, speed):
        """
        Sets speed.

        This method expects KPH or MPH. The Tello API expects speeds from
        1 to 100 centimeters/second.

        Metric: .1 to 3.6 KPH
        Imperial: .1 to 2.2 MPH

        :param speed: Speed.
        :return: Response from Tello, 'OK' or 'FALSE'.
        """
        speed = float(speed)

        if self.imperial is True:
            speed = int(round(speed * 44.704))
        else:
            speed = int(round(speed * 27.7778))

        return self.send_command(f'speed {speed}')

    def rotate_cw(self, degrees):
        """
        Rotates clockwise.

        :param degrees: Degrees to rotate, 1 to 360.
        :return:Response from Tello, 'OK' or 'FALSE'.
        """
        return self.send_command(f'cw {degrees}')

    def rotate_ccw(self, degrees):
        """
        Rotates counter-clockwise.

        :param degrees: Degrees to rotate, 1 to 360.
        :return: Response from Tello, 'OK' or 'FALSE'.
        """
        return self.send_command(f'ccw {degrees}')

    def flip(self, direction):
        """
        Flips.

        :param direction: Direction to flip, 'l', 'r', 'f', 'b'.
        :return: Response from Tello, 'OK' or 'FALSE'.
        """
        return self.send_command(f'flip {direction}')

    def get_response(self):
        """
        Returns response of tello.

        :return: Response of tello.
        """
        response = self.response
        return response

    def get_height(self):
        """
        Returns height(dm) of tello.

        :return: Height(dm) of tello.
        """
        height = self.send_command('height?')
        height = str(height)
        height = filter(str.isdigit, height)
        try:
            height = int(height)
            self.last_height = height
        except:
            height = self.last_height
            pass
        return height

    def get_battery(self):
        """
        Returns percent battery life remaining.

        :return: Percent battery life remaining.
        """
        battery = self.send_command('battery?')

        try:
            battery = int(battery)
        except:
            pass

        return battery

    def get_flight_time(self):
        """
        Returns the number of seconds elapsed during flight.

        :return: Seconds elapsed during flight.
        """
        flight_time = self.send_command('time?')

        try:
            flight_time = int(flight_time)
        except:
            pass

        return flight_time

    def get_speed(self):
        """
        Returns the current speed.

        :return: Current speed in KPH or MPH.
        """
        speed = self.send_command('speed?')

        try:
            speed = float(speed)

            if self.imperial is True:
                speed = round((speed / 44.704), 1)
            else:
                speed = round((speed / 27.7778), 1)
        except:
            pass

        return speed

    def land(self):
        """
        Initiates landing.

        :return: Response from Tello, 'OK' or 'FALSE'.
        """
        return self.send_command('land')

    def move(self, direction, distance):
        """
        Moves in a direction for a distance.

        This method expects meters or feet. The Tello API expects distances
        from 20 to 500 centimeters.

        Metric: .02 to 5 meters
        Imperial: .7 to 16.4 feet

        :param direction: Direction to move, 'forward', 'back', 'right' or 'left'.
        :param distance: Distance to move.
        :return: Response from Tello, 'OK' or 'FALSE'.
        """
        distance = float(distance)

        if self.imperial is True:
            distance = int(round(distance * 30.48))
        else:
            distance = int(round(distance * 100))

        return self.send_command(f'{direction} {distance}')

    def move_backward(self, distance):
        """
        Moves backward for a distance.

        See comments for Tello.move().

        :param distance: Distance to move.
        :return: Response from Tello, 'OK' or 'FALSE'.
        """
        return self.move('back', distance)

    def move_down(self, distance):
        """
        Moves down for a distance.

        See comments for Tello.move().

        :param distance: Distance to move.
        :return: Response from Tello, 'OK' or 'FALSE'.
        """
        return self.move('down', distance)

    def move_forward(self, distance):
        """
        Moves forward for a distance.

        See comments for Tello.move().

        :param distance: Distance to move.
        :return: Response from Tello, 'OK' or 'FALSE'.
        """
        return self.move('forward', distance)

    def move_left(self, distance):
        """
        Moves left for a distance.

        See comments for Tello.move().

        :param distance: Distance to move.
        :return: Response from Tello, 'OK' or 'FALSE'.
        """
        return self.move('left', distance)

    def move_right(self, distance):
        """
        Moves right for a distance.

        See comments for Tello.move().

        :param distance: Distance to move.
        :return: Response from Tello, 'OK' or 'FALSE'.
        """
        return self.move('right', distance)

    def move_up(self, distance):
        """
        Moves up for a distance.

        See comments for Tello.move().

        :param distance: Distance to move.
        :return: Response from Tello, 'OK' or 'FALSE'.
        """
        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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
import tkinter as tki
from tkinter import Toplevel, Scale
import threading
import datetime
import os
import time
import platform

class TelloUI(object):
    """
    Wrapper class to enable the GUI.
    """
    def __init__(self, tello):
        """
        Initializes all the element of the GUI, supported by Tkinter

        :param tello: class interacts with the Tello drone.
        """
        self.tello = tello # videostream device
        self.thread = None # thread of the Tkinter mainloop
        self.stopEvent = None  
        
        # control variables
        self.distance = 0.1  # default distance for 'move' cmd
        self.degree = 30  # default degree for 'cw' or 'ccw' cmd

        # if the flag is TRUE,the auto-takeoff thread will stop waiting
        # for the response from tello
        self.quit_waiting_flag = False
        
        # initialize the root window and image panel
        self.root = tki.Tk()
        self.panel = None

        # create buttons
        self.btn_landing = tki.Button(
            self.root, text='Open Command Panel', relief='raised', command=self.openCmdWindow)
        self.btn_landing.pack(side='bottom', fill='both',
                              expand='yes', padx=10, pady=5)
        
        # start a thread that constantly pools the video sensor for
        # the most recently read frame
        self.stopEvent = threading.Event()
        
        # set a callback to handle when the window is closed
        self.root.wm_title('TELLO Controller')
        self.root.wm_protocol('WM_DELETE_WINDOW', self.on_close)

        # the sending_command will send command to tello every 5 seconds
        self.sending_command_thread = threading.Thread(target = self._sendingCommand)
            
    def _sendingCommand(self):
        """
        Starts a while loop that sends 'command' to tello every 5 second.

        :return: None
        """    

        while True:
            self.tello.send_command('command')        
            time.sleep(5)

    def _setQuitWaitingFlag(self):  
        """
        Set the variable as TRUE; it will stop computer waiting for response from tello.

        :return: None
        """       
        self.quit_waiting_flag = True        
   
    def openCmdWindow(self):
        """
        Open the cmd window and initial all the button and text.

        :return: None
        """        
        panel = Toplevel(self.root)
        panel.wm_title('Command Panel')

        # create text input entry
        text0 = tki.Label(panel,
                          text='This Controller map keyboard inputs to Tello control commands\n'
                               'Adjust the trackbar to reset distance and degree parameter',
                          font='Helvetica 10 bold'
                          )
        text0.pack(side='top')

        text1 = tki.Label(panel, text=
                          'W - Move Tello Up\t\t\tArrow Up - Move Tello Forward\n'
                          'S - Move Tello Down\t\t\tArrow Down - Move Tello Backward\n'
                          'A - Rotate Tello Counter-Clockwise\tArrow Left - Move Tello Left\n'
                          'D - Rotate Tello Clockwise\t\tArrow Right - Move Tello Right',
                          justify='left')
        text1.pack(side='top')

        self.btn_landing = tki.Button(
            panel, text='Land', relief='raised', command=self.telloLanding)
        self.btn_landing.pack(side='bottom', fill='both',
                              expand='yes', padx=10, pady=5)

        self.btn_takeoff = tki.Button(
            panel, text='Takeoff', relief='raised', command=self.telloTakeOff)
        self.btn_takeoff.pack(side='bottom', fill='both',
                              expand='yes', padx=10, pady=5)

        # binding arrow keys to drone control
        self.tmp_f = tki.Frame(panel, width=100, height=2)
        self.tmp_f.bind('<KeyPress-w>', self.on_keypress_w)
        self.tmp_f.bind('<KeyPress-s>', self.on_keypress_s)
        self.tmp_f.bind('<KeyPress-a>', self.on_keypress_a)
        self.tmp_f.bind('<KeyPress-d>', self.on_keypress_d)
        self.tmp_f.bind('<KeyPress-Up>', self.on_keypress_up)
        self.tmp_f.bind('<KeyPress-Down>', self.on_keypress_down)
        self.tmp_f.bind('<KeyPress-Left>', self.on_keypress_left)
        self.tmp_f.bind('<KeyPress-Right>', self.on_keypress_right)
        self.tmp_f.pack(side='bottom')
        self.tmp_f.focus_set()

        self.btn_landing = tki.Button(
            panel, text='Flip', relief='raised', command=self.openFlipWindow)
        self.btn_landing.pack(side='bottom', fill='both',
                              expand='yes', padx=10, pady=5)

        self.distance_bar = Scale(panel, from_=0.02, to=5, tickinterval=0.01, 
                                  digits=3, label='Distance(m)',
                                  resolution=0.01)
        self.distance_bar.set(0.2)
        self.distance_bar.pack(side='left')

        self.btn_distance = tki.Button(panel, text='Reset Distance', relief='raised',
                                       command=self.updateDistancebar,
                                       )
        self.btn_distance.pack(side='left', fill='both',
                               expand='yes', padx=10, pady=5)

        self.degree_bar = Scale(panel, from_=1, to=360, tickinterval=10, label='Degree')
        self.degree_bar.set(30)
        self.degree_bar.pack(side='right')

        self.btn_distance = tki.Button(panel, text='Reset Degree', relief='raised', 
                                       command=self.updateDegreebar)
        self.btn_distance.pack(side='right', fill='both',
                               expand='yes', padx=10, pady=5)

    def openFlipWindow(self):
        """
        Open the flip window and initial all the button and text.

        :return: None
        """
        panel = Toplevel(self.root)
        panel.wm_title('Gesture Recognition')

        self.btn_flipl = tki.Button(
            panel, text='Flip Left', relief='raised', command=self.telloFlip_l)
        self.btn_flipl.pack(side='bottom', fill='both',
                            expand='yes', padx=10, pady=5)

        self.btn_flipr = tki.Button(
            panel, text='Flip Right', relief='raised', command=self.telloFlip_r)
        self.btn_flipr.pack(side='bottom', fill='both',
                            expand='yes', padx=10, pady=5)

        self.btn_flipf = tki.Button(
            panel, text='Flip Forward', relief='raised', command=self.telloFlip_f)
        self.btn_flipf.pack(side='bottom', fill='both',
                            expand='yes', padx=10, pady=5)

        self.btn_flipb = tki.Button(
            panel, text='Flip Backward', relief='raised', command=self.telloFlip_b)
        self.btn_flipb.pack(side='bottom', fill='both',
                            expand='yes', padx=10, pady=5)

    def telloTakeOff(self):
        return self.tello.takeoff()                

    def telloLanding(self):
        return self.tello.land()

    def telloFlip_l(self):
        return self.tello.flip('l')

    def telloFlip_r(self):
        return self.tello.flip('r')

    def telloFlip_f(self):
        return self.tello.flip('f')

    def telloFlip_b(self):
        return self.tello.flip('b')

    def telloCW(self, degree):
        return self.tello.rotate_cw(degree)

    def telloCCW(self, degree):
        return self.tello.rotate_ccw(degree)

    def telloMoveForward(self, distance):
        return self.tello.move_forward(distance)

    def telloMoveBackward(self, distance):
        return self.tello.move_backward(distance)

    def telloMoveLeft(self, distance):
        return self.tello.move_left(distance)

    def telloMoveRight(self, distance):
        return self.tello.move_right(distance)

    def telloUp(self, dist):
        return self.tello.move_up(dist)

    def telloDown(self, dist):
        return self.tello.move_down(dist)

    def updateDistancebar(self):
        self.distance = self.distance_bar.get()
        print(f'reset distance to {self.distance:.1f}')

    def updateDegreebar(self):
        self.degree = self.degree_bar.get()
        print(f'reset distance to {self.degree}')

    def on_keypress_w(self, event):
        print(f'up {self.distance} m')
        self.telloUp(self.distance)

    def on_keypress_s(self, event):
        print(f'down {self.distance} m')
        self.telloDown(self.distance)

    def on_keypress_a(self, event):
        print(f'ccw {self.degree} degree')
        self.tello.rotate_ccw(self.degree)

    def on_keypress_d(self, event):
        print(f'cw {self.degree} m')
        self.tello.rotate_cw(self.degree)

    def on_keypress_up(self, event):
        print(f'forward {self.distance} m')
        self.telloMoveForward(self.distance)

    def on_keypress_down(self, event):
        print(f'backward {self.distance} m')
        self.telloMoveBackward(self.distance)

    def on_keypress_left(self, event):
        print(f'left {self.distance} m')
        self.telloMoveLeft(self.distance)

    def on_keypress_right(self, event):
        print(f'right {self.distance} m')
        self.telloMoveRight(self.distance)

    def on_close(self):
        """
        Sets the stop event, cleanup the camera, and allow the rest of
        the quit process to continue.
        :return: None
        """
        print('[INFO] closing...')
        self.stopEvent.set()
        del self.tello
        self.root.quit()

Code

8.3. Application

The app.py file is the application program entry point.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import tello
from ui import TelloUI


def main():
    drone = tello.Tello('', 8889)  
    vplayer = TelloUI(drone)
    vplayer.root.mainloop() 


if __name__ == '__main__':
    main()

Code

8.4. Running the application

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

python app.py