9. Swarm Programming

The Tello Edu drone may be programmed in swarm mode; meaning, you may code behavior into multiple drones at once. The Tello Edu uses the Tello 2.0 SDK. The commands for version 2.0 are similar to 1.0, but there are many more commands. Here’s a table listing the major commands.

Tello 2.0 Commands

Command

Description

Response

command

Enter command mode

ok|error

takeoff

Auto takeoff

ok|error

land

Auto landing

ok|error

streamon

Enable video stream

ok|error

streamoff

Disable video stream

ok|error

emergency

Stop motors immediately

ok|error

stop

Hovers in the air

ok|error

up xx

Fly upward [20, 500] cm

ok|error

down xx

Fly downward [20, 500] cm

ok|error

left xx

Fly left [20, 500] cm

ok|error

right xx

Fly right [20, 500] cm

ok|error

forward xx

Fly forward [20, 500] cm

ok|error

back xx

Fly backward [20, 500] cm

ok|error

cw xx

Rotate clockwise [1, 360] degrees

ok|error

ccw xx

Rotate counter-clockwise [1, 360] degrees

ok|error

flip x

Flip [l, r, f, b]

ok|error

speed x

Set speed to [10, 100] cm/s

ok|error

go x y z speed

Fly to x, y, z at speed

ok|error

x y z may be in the range [-500, 500]

speed is in the range [10, 100] cm/s

curve x1 y1 z1 x2 y2 z2 speed

Fly at a curve between the two given coordinates at speed

ok|error

x1 y1 z1 may be in the range [-500, 500]

x2 y2 z2 may be in the range [-500, 500]

speed is in the range [10, 60] cm/s

go x y z speed mid

Fly to x, y, z of mission pad at speed

ok|error

x y z may be in the range [-500, 500]

speed is in the range [10, 100] cm/s

mid is in the domain [m1, m2, m3, m4, m5, m6, m7, m8]

curve x1 y1 z1 x2 y2 z2 speed mid

Fly at a curve between the two given coordinates of mission pad at speed

ok|error

x1 y1 z1 may be in the range [-500, 500]

x2 y2 z2 may be in the range [-500, 500]

speed is in the range [10, 60] cm/s

mid is in the domain [m1, m2, m3, m4, m5, m6, m7, m8]

jump x y z speed yaw mid1 mid2

Fly to x, y, z of mission pad 1 and recognize coordinates 0, 0, z of mission pad 2 and rotate to yaw value

ok|error

x y z may be in the range [-500, 500]

speed is in the range [10, 100] cm/s

mid is in the domain [m1, m2, m3, m4, m5, m6, m7, m8]

wifi ssid pass

Set WiFi password

ok|error

mon

Enable mission pad detection

ok|error

moff

Disable mission pad detection

ok|error

mdirection x

Enable mission pad detection

ok|error

x = 0, downward detection only

x = 1, forward detection only

x = 2, downward and forward detection

ap ssid pass

Set Tello to station mode and connect to new access point

ok|error

speed?

Get current speed

[10, 100]

battery?

Get current battery percentage

[0, 100]

time?

Get current flight time

xx

wifi?

Get WiFi SNR

xx

sdk?

Get the SDK version

xx

sn?

Get the serial number

xx

9.1. Dependencies

To start using Python to manipulate the Tello Swarm, make sure you install the following packages netifaces and netaddr.

1
conda install -y -c conda-forge netifaces netaddr

Make sure you are using Python 3.7 or higher. The original code requires Python 2.7, but we have re-written the code for Python 3.7 and heavily refactored it to be easier to maintain and read.

9.2. Set Tello modes

Each Tello Edu can exist in AP Mode or Station Mode.

  • AP Mode or Access Point Mode is when the Tello becomes a client to a router.

  • Station Mode is when the Tello acts like a router.

Only when a Tello Edu is set to AP Mode will you be able to use Python to do swarm programming. The script set-ap-mode.py will help you set the Tello Edu to AP Mode. To reset the Tello Edu back to Station Mode, turn on the drone and then hold the power button for 5 seconds. Below is an example usage of the script; you will need to provide the SSID and password of the router to the program. Additionally, make sure your router supports the 2.4 GHz bandwidth, as the drone will not connect to the 5.0 GHz bandwidth.

1
python set-ap-mode.py -s [SSID] -p [PASSWORD]

The code for set-ap-mode.py is listed below.

 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
import socket
import argparse
import sys

def get_socket():
    """
    Gets a socket.
    :return: Socket.
    """
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.bind(('', 8889))

    return s

def set_ap(ssid, password, address):
    """
    A Function to set tello in Access Point (AP) mode.

    :param ssid: The SSID of the network (e.g. name of the Wi-Fi).
    :param password: The password of the network.
    :param address: Tello IP.
    :return: None.
    """
    s = get_socket()

    cmd = 'command'
    print(f'sending cmd {cmd}')
    s.sendto(cmd.encode('utf-8'), address)

    response, ip = s.recvfrom(100)
    print(f'from {ip}: {response}')

    cmd = f'ap {ssid} {password}'
    print(f'sending cmd {cmd}')
    s.sendto(cmd.encode('utf-8'), address)

    response, ip = s.recvfrom(100)
    print(f'from {ip}: {response}')

def parse_args(args):
    """
    Parses arguments.
    :param args: Arguments.
    :return: Arguments.
    """
    parser = argparse.ArgumentParser('set-ap-mode.py', 
                epilog='One-Off Coder http://www.oneoffcoder.com')

    parser.add_argument('-s', '--ssid', help='SSID', required=True)
    parser.add_argument('-p', '--pwd', help='password', required=True)
    parser.add_argument('--ip', help='Tello IP', default='192.168.10.1', required=False)
    parser.add_argument('--port', help='Tello port', default=8889, type=int, required=False)
    parser.add_argument('--version', action='version', version='%(prog)s v0.0.1')

    return parser.parse_args(args)

if __name__ == '__main__':
    args = parse_args(sys.argv[1:])
    ssid = args.ssid
    pwd = args.pwd
    tello_address = (args.ip, args.port)
    
    set_ap(ssid, pwd, tello_address)

9.3. Python programming

There are 6 main Python classes created to manipulate the drones, and they are listed below.

Python Classes

ID

Class Name

Purpose

1

Stats

Collect statistics

2

SubnetInfo

Stores subnet information

3

Tello

Models a Tello EDU (or drone)

4

TelloManager

Manages connections to drones

5

SwarmUtil

Utility class to help swarm programming

6

Swarm

Models a swarm of drones

All these classes are brought together by a single program planned-flight.py, which is the entry point where your pre-defined commands are sent to the swarm. The planned-flight.py program is very simple and looks like the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import sys
import argparse
from swarm import *

def parse_args(args):
    """
    Parses arguments.
    :param args: Arguments.
    :return: Arguments.
    """
    parser = argparse.ArgumentParser('planned-flight.py', 
                    epilog='One-Off Coder http://www.oneoffcoder.com')

    parser.add_argument('-f', '--file', help='Command text file', required=True)
    parser.add_argument('--version', action='version', version='%(prog)s v0.0.1')

    return parser.parse_args(args)

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

    swarm = Swarm(fpath)
    swarm.start()

As you can see, planned-flight.py takes in a file path as input. The file pointed to by the file path is simply a text file of the commands supported by the SDK. An example of the command file is as follows.

1
2
3
4
5
6
7
scan 1
battery_check 20
correct_ip
1=0TQZGANED0021X
1>takeoff
sync 1
1>land

You may then execute the program as follows.

1
python planned-flight.py -f cmds-01.txt

Here’s the Stats class.

 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
class Stats(object):
    """
    Statistics
    """

    def __init__(self, command, id):
        """
        Ctor.
        :param command: Command.
        :param id: ID.
        """
        self.command = command
        self.response = None
        self.id = id

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

    def add_response(self, response, ip):
        """
        Adds a response.
        :param response: Response.
        :param ip: IP address.
        :return: None.
        """
        if self.response == None:
            self.response = response
            self.end_time = datetime.now()
            self.duration = self.get_duration()
            self.drone_ip = ip

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

    def print_stats(self):
        """
        Prints statistics.
        :return: None.
        """
        print(self.get_stats())

    def got_response(self):
        """
        Checks if response was received.
        :return: A boolean indicating if response was received.
        """
        return False if self.response is None else True

    def get_stats(self):
        """
        Gets the statistics.
        :return: Statistics.
        """
        return {
            'id': self.id,
            'command': self.command,
            'response': self.response,
            'start_time': self.start_time,
            'end_time': self.end_time,
            'duration': self.duration
        }

    def get_stats_delimited(self):
        stats = self.get_stats()
        keys = ['id', 'command', 'response', 'start_time', 'end_time', 'duration']
        vals = [f'{k}={stats[k]}' for k in keys]
        vals = ', '.join(vals)
        return vals

    def __repr__(self):
        return self.get_stats_delimited()

Here’s the SubnetInfo class.

 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
class SubnetInfo(object):
    """
    Subnet information.
    """

    def __init__(self, ip, network, netmask):
        """
        Ctor.
        :param ip: IP.
        :param network: Network.
        :param netmask: Netmask.
        """
        self.ip = ip
        self.network = network
        self.netmask = netmask

    def __repr__(self):
        return f'{self.network} | {self.netmask} | {self.ip}'

    def get_ips(self):
        """
        Gets all the possible IP addresses in the subnet.
        :return: List of IPs.
        """
        def get_quad(ip):
            """
            Gets the third quad.
            :param ip: IP.
            :return: Third quad.
            """
            quads = str(ip).split('.')
            quad = quads[3]
            return quad
        
        def is_valid(ip):
            """
            Checks if IP is valid.
            :return: A boolean indicating if IP is valid.
            """
            quad = get_quad(ip)
            result = False if quad == '0' or quad == '255' else True

            if result:
                if str(ip) == self.ip:
                    result = False
            
            return result

        ip_network = IPNetwork(f'{self.network}/{self.netmask}')

        return [str(ip) for ip in ip_network if is_valid(ip)]

    @staticmethod
    def flatten(infos):
        return list(itertools.chain.from_iterable(infos))

Here’s the Tello class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Tello(object):
    """
    A wrapper class to interact with Tello.
    Communication with Tello is handled by TelloManager.
    """
    def __init__(self, tello_ip, tello_manager):
        """
        Ctor.
        :param tello_ip: Tello IP.
        :param tello_manager: Tello Manager.
        """
        self.tello_ip = tello_ip
        self.tello_manager = tello_manager

    def send_command(self, command):
        """
        Sends a command.
        :param command: Command.
        :return: None.
        """
        return self.tello_manager.send_command(command, self.tello_ip)

    def __repr__(self):
        return f'TELLO@{self.tello_ip}'

Here’s the TelloManager class.

  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
class TelloManager(object):
    """
    Tello Manager.
    """

    def __init__(self):
        """
        Ctor.
        """
        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_list = []
        self.tello_list = []
        self.log = defaultdict(list)

        self.COMMAND_TIME_OUT = 20.0

        self.last_response_index = {}
        self.str_cmd_index = {}

    def find_avaliable_tello(self, num):
        """
        Find Tellos.
        :param num: Number of Tellos to search.
        :return: None
        """
        possible_ips = self.get_possible_ips()

        print(f'[SEARCHING], Searching for {num} from {len(possible_ips)} possible IP addresses')

        iters = 0

        while len(self.tello_ip_list) < num:
            print(f'[SEARCHING], Trying to find Tellos, number of tries = {iters + 1}')

            # delete already found Tello
            for tello_ip in self.tello_ip_list:
                if tello_ip in possible_ips:
                    possible_ips.remove(tello_ip)

            # skip server itself
            for ip in possible_ips:
                cmd_id = len(self.log[ip])
                self.log[ip].append(Stats('command', cmd_id))

                # print(f'{iters}: sending command to {ip}:8889')

                try:
                    self.socket.sendto(b'command', (ip, 8889))
                except:
                    print(f'{iters}: ERROR: {ip}:8889')
                    pass

            iters = iters + 1
            time.sleep(5)

        # filter out non-tello addresses in log
        temp = defaultdict(list)
        for ip in self.tello_ip_list:
            temp[ip] = self.log[ip]
        self.log = temp

    def get_possible_ips(self):
        """
        Gets all the possible IP addresses for subnets that the computer is a part of.
        :return: List of IP addresses.
        """
        infos = self.get_subnets()
        ips = SubnetInfo.flatten([info.get_ips() for info in infos])
        ips = list(filter(lambda ip: ip.startswith('192.168.3.'), ips))
        return ips

    def get_subnets(self):
        """
        Gets all subnet information.

        :return: List of subnet information.
        """
        infos = []

        for iface in netifaces.interfaces():
            addrs = netifaces.ifaddresses(iface)

            if socket.AF_INET not in addrs:
                continue

            # Get ipv4 stuff
            ipinfo = addrs[socket.AF_INET][0]
            address, netmask = ipinfo['addr'], ipinfo['netmask']

            # limit range of search. This will work for router subnets
            if netmask != '255.255.255.0':
                continue

            # Create ip object and get
            cidr = netaddr.IPNetwork(f'{address}/{netmask}')
            network = cidr.network

            info = SubnetInfo(address, network, netmask)
            infos.append(info)

        return infos

    def get_tello_list(self):
        return self.tello_list

    def send_command(self, command, ip):
        """
        Sends 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: Command.
        :param ip: Tello IP.
        :return: Response.
        """
        #global cmd
        command_sof_1 = ord(command[0])
        command_sof_2 = ord(command[1])

        if command_sof_1 == 0x52 and command_sof_2 == 0x65:
            multi_cmd_send_flag = True
        else :
            multi_cmd_send_flag = False

        if multi_cmd_send_flag == True:      
            self.str_cmd_index[ip] = self.str_cmd_index[ip] + 1
            for num in range(1,5):                
                str_cmd_index_h = self.str_cmd_index[ip] / 128 + 1
                str_cmd_index_l = self.str_cmd_index[ip] % 128
                if str_cmd_index_l == 0:
                    str_cmd_index_l = str_cmd_index_l + 2
                cmd_sof = [0x52, 0x65, str_cmd_index_h, str_cmd_index_l, 0x01, num + 1, 0x20]
                cmd_sof_str = str(bytearray(cmd_sof))
                cmd = cmd_sof_str + command[3:]
                self.socket.sendto(cmd.encode('utf-8'), (ip, 8889))

            print(f'[MULTI_COMMAND], IP={ip}, COMMAND={command[3:]}')
            real_command = command[3:]
        else:
            self.socket.sendto(command.encode('utf-8'), (ip, 8889))
            print(f'[SINGLE_COMMAND] IP={ip}, COMMAND={command}')
            real_command = command
        
        self.log[ip].append(Stats(real_command, len(self.log[ip])))
        start = time.time()

        while not self.log[ip][-1].got_response():
            now = time.time()
            diff = now - start
            if diff > self.COMMAND_TIME_OUT:
                print(f'[NO_RESPONSE] Max timeout exceeded for command: {real_command}')
                return    

    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:
                response, ip = self.socket.recvfrom(1024)
                response = response.decode('utf-8')
                self.response = response
                
                ip = ''.join(str(ip[0]))                
                
                if self.response.upper() == 'OK' and ip not in self.tello_ip_list:
                    self.tello_ip_list.append(ip)
                    self.last_response_index[ip] = 100
                    self.tello_list.append(Tello(ip, self))
                    self.str_cmd_index[ip] = 1
                
                response_sof_part1 = ord(self.response[0])               
                response_sof_part2 = ord(self.response[1])

                if response_sof_part1 == 0x52 and response_sof_part2 == 0x65:
                    response_index = ord(self.response[3])
                    
                    if response_index != self.last_response_index[ip]:
                        print(f'[MULTI_RESPONSE], IP={ip}, RESPONSE={self.response[7:]}')
                        self.log[ip][-1].add_response(self.response[7:], ip)
                    self.last_response_index[ip] = response_index
                else:
                    # print(f'[SINGLE_RESPONSE], IP={ip}, RESPONSE={self.response}')
                    self.log[ip][-1].add_response(self.response, ip)
                         
            except socket.error as exc:
                # swallow exception
                # print "[Exception_Error]Caught exception socket.error : %s\n" % exc
                pass

    def get_log(self):
        """
        Get all logs.
        :return: Dictionary of logs.
        """
        return self.log

    def get_last_logs(self):
        """
        Gets the last logs.
        :return: List of last logs.
        """
        return [log[-1] for log in self.log.values()]

Here’s the SwarmUtil class.

  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
class SwarmUtil(object):
    """
    Swarm utility class.
    """

    @staticmethod
    def create_execution_pools(num):
        """
        Creates execution pools.

        :param num: Number of execution pools to create.
        :return: List of Queues.
        """
        return [queue.Queue() for x in range(num)]


    @staticmethod
    def drone_handler(tello, queue):
        """
        Drone handler.

        :param tello: Tello.
        :param queue: Queue.
        :return: None.
        """
        while True:
            while queue.empty():
                pass
            command = queue.get()
            tello.send_command(command)


    @staticmethod
    def all_queue_empty(pools):
        """
        Checks if all queues are empty.

        :param pools: List of Queues.
        :return: Boolean indicating if all queues are empty.
        """
        for queue in pools:
            if not queue.empty():
                return False
        return True


    @staticmethod
    def all_got_response(manager):
        """
        Checks if all responses are received.

        :param manager: TelloManager.
        :return: A boolean indicating if all responses are received.
        """
        for log in manager.get_last_logs():
            if not log.got_response():
                return False
        return True


    @staticmethod
    def create_dir(dpath):
        """
        Creates a directory if it does not exists.

        :param dpath: Directory path.
        :return: None.
        """
        if not os.path.exists(dpath):
            with suppress(Exception):
                os.makedirs(dpath)

    @staticmethod
    def save_log(manager):
        """
        Saves the logs into a file in the ./log directory.

        :param manager: TelloManager.
        :return: None.
        """
        dpath = './log'
        SwarmUtil.create_dir(dpath)

        start_time = str(time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime(time.time())))
        fpath = f'{dpath}/{start_time}.txt'

        with open(fpath, 'w') as out:
            log = manager.get_log()
            for cnt, stats in enumerate(log.values()):
                out.write(f'------\nDrone: {cnt + 1}\n')

                s = [stat.get_stats_delimited() for stat in stats]
                s = '\n'.join(s)

                out.write(f'{s}\n')
        
        print(f'[LOG] Saved log files to {fpath}')


    @staticmethod
    def check_timeout(start_time, end_time, timeout):
        """
        Checks if the duration between the end and start times
        is larger than the specified timeout.

        :param start_time: Start time.
        :param end_time: End time.
        :param timeout: Timeout threshold.
        :return: A boolean indicating if the duration is larger than the specified timeout threshold.
        """
        diff = end_time - start_time
        time.sleep(0.1)
        return diff > timeout

Here’s the Swarm class.

  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
class Swarm(object):
    """
    Tello Edu swarm.
    """

    def __init__(self, fpath):
        """
        Ctor.

        :param fpath: Path to command text file.
        """
        self.fpath = fpath
        self.commands = self._get_commands(fpath)
        self.manager = TelloManager()
        self.tellos = []
        self.pools = []
        self.sn2ip = {
            '0TQZGANED0021X': '192.168.3.101',
            '0TQZGANED0020C': '192.168.3.103',
            '0TQZGANED0023H': '192.168.3.104'
        }
        self.id2sn = {
            0: '0TQZGANED0021X',
            1: '0TQZGANED0020C',
            2: '0TQZGANED0023H'
        }
        self.ip2id = {
            '192.168.3.101': 0,
            '192.168.3.103': 1,
            '192.168.3.104': 2
        }

    def start(self):
        """
        Main loop. Starts the swarm.

        :return: None.
        """
        def is_invalid_command(command):
            if command is None:
                return True
            c = command.strip()
            if len(c) == 0:
                return True
            if c == '':
                return True
            if c == '\n':
                return True
            return False
        
        try:
            for command in self.commands:
                if is_invalid_command(command):
                    continue

                command = command.rstrip()

                if '//' in command:
                    self._handle_comments(command)
                elif 'scan' in command:
                    self._handle_scan(command)
                elif '>' in command:
                    self._handle_gte(command)
                elif 'battery_check' in command:
                    self._handle_battery_check(command)
                elif 'delay' in command:
                    self._handle_delay(command)
                elif 'correct_ip' in command:
                    self._handle_correct_ip(command)
                elif '=' in command:
                    self._handle_eq(command)
                elif 'sync' in command:
                    self._handle_sync(command)
            
            self._wait_for_all()
        except KeyboardInterrupt as ki:
            self._handle_keyboard_interrupt()
        except Exception as e:
            self._handle_exception(e)
            traceback.print_exc()
        finally:
            SwarmUtil.save_log(self.manager)

    def _wait_for_all(self):
        """
        Waits for all queues to be empty and for all responses
        to be received.

        :return: None.
        """
        while not SwarmUtil.all_queue_empty(self.pools):
            time.sleep(0.5)
        
        time.sleep(1)

        while not SwarmUtil.all_got_response(self.manager):
            time.sleep(0.5)

    def _get_commands(self, fpath):
        """
        Gets the commands.

        :param fpath: Command file path.
        :return: List of commands.
        """
        with open(fpath, 'r') as f:
            return f.readlines()

    def _handle_comments(self, command):
        """
        Handles comments.

        :param command: Command.
        :return: None.
        """
        print(f'[COMMENT] {command}')

    def _handle_scan(self, command):
        """
        Handles scan.

        :param command: Command.
        :return: None.
        """
        n_tellos = int(command.partition('scan')[2])

        self.manager.find_avaliable_tello(n_tellos)
        self.tellos = self.manager.get_tello_list()
        self.pools = SwarmUtil.create_execution_pools(n_tellos)

        for x, (tello, pool) in enumerate(zip(self.tellos, self.pools)):
            self.ip2id[tello.tello_ip] = x

            t = Thread(target=SwarmUtil.drone_handler, args=(tello, pool))
            t.daemon = True
            t.start()

            print(f'[SCAN] IP = {tello.tello_ip}, ID = {x}')

    def _handle_gte(self, command):
        """
        Handles gte or >.

        :param command: Command.
        :return: None.
        """
        id_list = []
        id = command.partition('>')[0]

        if id == '*':
            id_list = [t for t in range(len(self.tellos))]
        else:
            id_list.append(int(id)-1) 
        
        action = str(command.partition('>')[2])

        for tello_id in id_list:
            sn = self.id2sn[tello_id]
            ip = self.sn2ip[sn]
            id = self.ip2id[ip]

            self.pools[id].put(action)
            print(f'[ACTION] SN = {sn}, IP = {ip}, ID = {id}, ACTION = {action}')

    def _handle_battery_check(self, command):
        """
        Handles battery check. Raises exception if any drone has
        battery life lower than specified threshold in the command.

        :param command: Command.
        :return: None.
        """
        threshold = int(command.partition('battery_check')[2])
        for queue in self.pools:
            queue.put('battery?')

        self._wait_for_all()

        is_low = False

        for log in self.manager.get_last_logs():
            battery = int(log.response)
            drone_ip = log.drone_ip

            print(f'[BATTERY] IP = {drone_ip}, LIFE = {battery}%')

            if battery < threshold:
                is_low = True
        
        if is_low:
            raise Exception('Battery check failed!')
        else:
            print('[BATTERY] Passed battery check')

    def _handle_delay(self, command):
        """
        Handles delay.

        :param command: Command.
        :return: None.
        """
        delay_time = float(command.partition('delay')[2])
        print (f'[DELAY] Start Delay for {delay_time} second')
        time.sleep(delay_time)  

    def _handle_correct_ip(self, command):
        """
        Handles correction of IPs.

        :param command: Command.
        :return: None.
        """
        for queue in self.pools:
            queue.put('sn?') 

        self._wait_for_all()
        
        for log in self.manager.get_last_logs():
            sn = str(log.response)
            tello_ip = str(log.drone_ip)
            self.sn2ip[sn] = tello_ip

            print(f'[CORRECT_IP] SN = {sn}, IP = {tello_ip}')

    def _handle_eq(self, command):
        """
        Handles assignments of IDs to serial numbers.

        :param command: Command.
        :return: None.
        """
        id = int(command.partition('=')[0])
        sn = command.partition('=')[2]
        ip = self.sn2ip[sn]

        self.id2sn[id-1] = sn
        
        print(f'[IP_SN_ID] IP = {ip}, SN = {sn}, ID = {id}')

    def _handle_sync(self, command):
        """
        Handles synchronization.

        :param command: Command.
        :return: None.
        """
        timeout = float(command.partition('sync')[2])
        print(f'[SYNC] Sync for {timeout} seconds')

        time.sleep(1)

        try:
            start = time.time()
            
            while not SwarmUtil.all_queue_empty(self.pools):
                now = time.time()
                if SwarmUtil.check_timeout(start, now, timeout):
                    raise RuntimeError('Sync failed since all queues were not empty!')

            print('[SYNC] All queues empty and all commands sent')
           
            while not SwarmUtil.all_got_response(self.manager):
                now = time.time()
                if SwarmUtil.check_timeout(start, now, timeout):
                    raise RuntimeError('Sync failed since all responses were not received!')
            
            print('[SYNC] All response received')
        except RuntimeError:
            print('[SYNC] Failed to sync; timeout exceeded')

    def _handle_keyboard_interrupt(self):
        """
        Handles keyboard interrupt.

        :param command: Command.
        :return: None.
        """
        print('[QUIT_ALL], KeyboardInterrupt. Sending land to all drones')

9.4. Download

The files to program your Tello swarm may be downloaded. Note that you will have to modify the command files for your own drones (e.g. serial numbers).