7. Python, Auto Flight Path

The code below will use Python to fly your Tello drone on a pre-determined flight path. There are four main Python files.

  • stats.py defines the Stats object to store statistics on command request and response

  • tello.py defines the Tello object to abstract and represent the Tello drone

  • command.txt stores the commands that will be sent to the Tello drone

  • app.py is the application program, or the driver, that will be executed as an entrypoint to start sending commands

7.1. Stats

The Stats class is used to log command requests and responses.

 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
from datetime import datetime

class Stats(object):
    def __init__(self, command, id):
        """
        Constructor.
        :param command: The command sent.
        :param id: The identifier.
        """
        self.command = command
        self.response = None
        self.id = id

        self.start_time = datetime.now()
        self.end_time = None
        self.duration = None

    def add_response(self, response):
        """
        Adds the response.
        :param response: Response.
        :return: None.
        """
        self.response = response
        self.end_time = datetime.now()
        self.duration = self.get_duration()

    def get_duration(self):
        """
        Gets the duration.
        :return: Duration.
        """
        diff = self.end_time - self.start_time
        return diff.total_seconds()

    def print_stats(self):
        """
        Prints the statistics.
        :return: None.
        """
        print(f'\nid {self.id}')
        print(f'command: {self.command}')
        print(f'response: {self.response}')
        print(f'start_time: {self.start_time}')
        print(f'end_time: {self.end_time}')
        print(f'duration: {self.duration}')

    def got_response(self):
        """
        Returns a boolean if a response was received.
        :return: Boolean.
        """
        
        if self.response is None:
            return False
        else:
            return True

    def return_stats(self):
        """
        Returns the statistics.
        :return: Statistics.
        """
        str = ''
        str +=  f'\nid: {self.id}\n'
        str += f'command: {self.command}\n'
        str += f'response: {self.response}\n'
        str += f'start_time: {self.start_time}\n'
        str += f'end_time: {self.end_time}\n'
        str += f'duration: {self.duration}\n'
        return str

Code

7.2. 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
import socket
import threading
import time
from stats import Stats

class Tello(object):
    def __init__(self):
        """
        Constructor.
        """
        self.local_ip = ''
        self.local_port = 8889
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.socket.bind((self.local_ip, self.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.tello_ip = '192.168.10.1'
        self.tello_port = 8889
        self.tello_address = (self.tello_ip, self.tello_port)
        self.log = []

        self.MAX_TIME_OUT = 15.0

    def send_command(self, command):
        """
        Send a command to the ip address. Will be blocked until
        the last command receives an 'OK'.
        If the command fails (either b/c time out or error),
        will try to resend the command
        :param command: (str) the command to send
        :param ip: (str) the ip of Tello
        :return: The latest command response
        """
        self.log.append(Stats(command, len(self.log)))

        self.socket.sendto(command.encode('utf-8'), self.tello_address)
        print(f'sending command: {command} to {self.tello_ip}')

        start = time.time()
        while not self.log[-1].got_response():
            now = time.time()
            diff = now - start
            if diff > self.MAX_TIME_OUT:
                print(f'Max timeout exceeded... command {command}')
                return
        print(f'Done!!! sent command: {command} to {self.tello_ip}')

    def _receive_thread(self):
        """
        Listen to responses from the Tello.
        Runs as a thread, sets self.response to whatever the Tello last returned.
        """
        while True:
            try:
                self.response, ip = self.socket.recvfrom(1024)
                print(f'from {ip}: {self.response}')

                self.log[-1].add_response(self.response)
            except Exception as exc:
                print(f'Caught exception socket.error : {exc}')

    def on_close(self):
        """
        On close.
        :returns: None.
        """
        pass

    def get_log(self):
        """
        Gets the logs.
        :returns: Logs.
        """
        return self.log

Code

7.3. Commands

The command.txt file stores the sequence of string commands that we will send to the drone. Note how we have to put a delay in between commands? This delay gives time for the drone and your program/computer to send, receive and process the data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
command
takeoff
delay 5
flip f
delay 2
flip b
delay 2
flip b
delay 2
flip f
delay 3
land

Code

7.4. Application

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

 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
from tello import Tello
import sys
from datetime import datetime
import time
import argparse


def parse_args(args):
    """
    Parses arguments.
    :param args: Arguments.
    :return: Parsed arguments.
    """
    parser = argparse.ArgumentParser('Tello Flight Commander', 
        epilog='One-Off Coder https://www.oneoffcoder.com')

    parser.add_argument('-f', '--file', help='command file', required=True)
    return parser.parse_args(args)


def start(file_name):
    """
    Starts sending commands to Tello.
    :param file_name: File name where commands are located.
    :return: None.
    """
    start_time = str(datetime.now())

    with open(file_name, 'r') as f:
        commands = f.readlines()

    tello = Tello()
    for command in commands:
        if command != '' and command != '\n':
            command = command.rstrip()

            if command.find('delay') != -1:
                sec = float(command.partition('delay')[2])
                print(f'delay {sec}')
                time.sleep(sec)
                pass
            else:
                tello.send_command(command)

    with open(f'log/{start_time}.txt', 'w') as out:
        log = tello.get_log()

        for stat in log:
            stat.print_stats()
            s = stat.return_stats()
            out.write(s)


if __name__ == '__main__':
    args = parse_args(sys.argv[1:])
    file_name = args.file
    start(file_name)

Code

7.5. Running the application

To run the program, type in the following from a terminal. The program will output the logs to the directory log/.

python app.py -f command.txt