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.

1conda 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.

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

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

 1import socket
 2import argparse
 3import sys
 4
 5def get_socket():
 6    """
 7    Gets a socket.
 8    :return: Socket.
 9    """
10    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
11    s.bind(('', 8889))
12
13    return s
14
15def set_ap(ssid, password, address):
16    """
17    A Function to set tello in Access Point (AP) mode.
18
19    :param ssid: The SSID of the network (e.g. name of the Wi-Fi).
20    :param password: The password of the network.
21    :param address: Tello IP.
22    :return: None.
23    """
24    s = get_socket()
25
26    cmd = 'command'
27    print(f'sending cmd {cmd}')
28    s.sendto(cmd.encode('utf-8'), address)
29
30    response, ip = s.recvfrom(100)
31    print(f'from {ip}: {response}')
32
33    cmd = f'ap {ssid} {password}'
34    print(f'sending cmd {cmd}')
35    s.sendto(cmd.encode('utf-8'), address)
36
37    response, ip = s.recvfrom(100)
38    print(f'from {ip}: {response}')
39
40def parse_args(args):
41    """
42    Parses arguments.
43    :param args: Arguments.
44    :return: Arguments.
45    """
46    parser = argparse.ArgumentParser('set-ap-mode.py', 
47                epilog='One-Off Coder http://www.oneoffcoder.com')
48
49    parser.add_argument('-s', '--ssid', help='SSID', required=True)
50    parser.add_argument('-p', '--pwd', help='password', required=True)
51    parser.add_argument('--ip', help='Tello IP', default='192.168.10.1', required=False)
52    parser.add_argument('--port', help='Tello port', default=8889, type=int, required=False)
53    parser.add_argument('--version', action='version', version='%(prog)s v0.0.1')
54
55    return parser.parse_args(args)
56
57if __name__ == '__main__':
58    args = parse_args(sys.argv[1:])
59    ssid = args.ssid
60    pwd = args.pwd
61    tello_address = (args.ip, args.port)
62    
63    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.

 1import sys
 2import argparse
 3from swarm import *
 4
 5def parse_args(args):
 6    """
 7    Parses arguments.
 8    :param args: Arguments.
 9    :return: Arguments.
10    """
11    parser = argparse.ArgumentParser('planned-flight.py', 
12                    epilog='One-Off Coder http://www.oneoffcoder.com')
13
14    parser.add_argument('-f', '--file', help='Command text file', required=True)
15    parser.add_argument('--version', action='version', version='%(prog)s v0.0.1')
16
17    return parser.parse_args(args)
18
19if __name__ == '__main__':
20    args = parse_args(sys.argv[1:])
21    fpath = args.file
22
23    swarm = Swarm(fpath)
24    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.

1scan 1
2battery_check 20
3correct_ip
41=0TQZGANED0021X
51>takeoff
6sync 1
71>land

You may then execute the program as follows.

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

Here’s the Stats class.

 1class Stats(object):
 2    """
 3    Statistics
 4    """
 5
 6    def __init__(self, command, id):
 7        """
 8        Ctor.
 9        :param command: Command.
10        :param id: ID.
11        """
12        self.command = command
13        self.response = None
14        self.id = id
15
16        self.start_time = datetime.now()
17        self.end_time = None
18        self.duration = None
19        self.drone_ip = None
20
21    def add_response(self, response, ip):
22        """
23        Adds a response.
24        :param response: Response.
25        :param ip: IP address.
26        :return: None.
27        """
28        if self.response == None:
29            self.response = response
30            self.end_time = datetime.now()
31            self.duration = self.get_duration()
32            self.drone_ip = ip
33
34    def get_duration(self):
35        """
36        Gets the duration.
37        :return: Duration (seconds).
38        """
39        diff = self.end_time - self.start_time
40        return diff.total_seconds()
41
42    def print_stats(self):
43        """
44        Prints statistics.
45        :return: None.
46        """
47        print(self.get_stats())
48
49    def got_response(self):
50        """
51        Checks if response was received.
52        :return: A boolean indicating if response was received.
53        """
54        return False if self.response is None else True
55
56    def get_stats(self):
57        """
58        Gets the statistics.
59        :return: Statistics.
60        """
61        return {
62            'id': self.id,
63            'command': self.command,
64            'response': self.response,
65            'start_time': self.start_time,
66            'end_time': self.end_time,
67            'duration': self.duration
68        }
69
70    def get_stats_delimited(self):
71        stats = self.get_stats()
72        keys = ['id', 'command', 'response', 'start_time', 'end_time', 'duration']
73        vals = [f'{k}={stats[k]}' for k in keys]
74        vals = ', '.join(vals)
75        return vals
76
77    def __repr__(self):
78        return self.get_stats_delimited()

Here’s the SubnetInfo class.

 1class SubnetInfo(object):
 2    """
 3    Subnet information.
 4    """
 5
 6    def __init__(self, ip, network, netmask):
 7        """
 8        Ctor.
 9        :param ip: IP.
10        :param network: Network.
11        :param netmask: Netmask.
12        """
13        self.ip = ip
14        self.network = network
15        self.netmask = netmask
16
17    def __repr__(self):
18        return f'{self.network} | {self.netmask} | {self.ip}'
19
20    def get_ips(self):
21        """
22        Gets all the possible IP addresses in the subnet.
23        :return: List of IPs.
24        """
25        def get_quad(ip):
26            """
27            Gets the third quad.
28            :param ip: IP.
29            :return: Third quad.
30            """
31            quads = str(ip).split('.')
32            quad = quads[3]
33            return quad
34        
35        def is_valid(ip):
36            """
37            Checks if IP is valid.
38            :return: A boolean indicating if IP is valid.
39            """
40            quad = get_quad(ip)
41            result = False if quad == '0' or quad == '255' else True
42
43            if result:
44                if str(ip) == self.ip:
45                    result = False
46            
47            return result
48
49        ip_network = IPNetwork(f'{self.network}/{self.netmask}')
50
51        return [str(ip) for ip in ip_network if is_valid(ip)]
52
53    @staticmethod
54    def flatten(infos):
55        return list(itertools.chain.from_iterable(infos))

Here’s the Tello class.

 1class Tello(object):
 2    """
 3    A wrapper class to interact with Tello.
 4    Communication with Tello is handled by TelloManager.
 5    """
 6    def __init__(self, tello_ip, tello_manager):
 7        """
 8        Ctor.
 9        :param tello_ip: Tello IP.
10        :param tello_manager: Tello Manager.
11        """
12        self.tello_ip = tello_ip
13        self.tello_manager = tello_manager
14
15    def send_command(self, command):
16        """
17        Sends a command.
18        :param command: Command.
19        :return: None.
20        """
21        return self.tello_manager.send_command(command, self.tello_ip)
22
23    def __repr__(self):
24        return f'TELLO@{self.tello_ip}'

Here’s the TelloManager class.

  1class TelloManager(object):
  2    """
  3    Tello Manager.
  4    """
  5
  6    def __init__(self):
  7        """
  8        Ctor.
  9        """
 10        self.local_ip = ''
 11        self.local_port = 8889
 12        self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 13        self.socket.bind((self.local_ip, self.local_port))
 14
 15        # thread for receiving cmd ack
 16        self.receive_thread = threading.Thread(target=self._receive_thread)
 17        self.receive_thread.daemon = True
 18        self.receive_thread.start()
 19
 20        self.tello_ip_list = []
 21        self.tello_list = []
 22        self.log = defaultdict(list)
 23
 24        self.COMMAND_TIME_OUT = 20.0
 25
 26        self.last_response_index = {}
 27        self.str_cmd_index = {}
 28
 29    def find_avaliable_tello(self, num):
 30        """
 31        Find Tellos.
 32        :param num: Number of Tellos to search.
 33        :return: None
 34        """
 35        possible_ips = self.get_possible_ips()
 36
 37        print(f'[SEARCHING], Searching for {num} from {len(possible_ips)} possible IP addresses')
 38
 39        iters = 0
 40
 41        while len(self.tello_ip_list) < num:
 42            print(f'[SEARCHING], Trying to find Tellos, number of tries = {iters + 1}')
 43
 44            # delete already found Tello
 45            for tello_ip in self.tello_ip_list:
 46                if tello_ip in possible_ips:
 47                    possible_ips.remove(tello_ip)
 48
 49            # skip server itself
 50            for ip in possible_ips:
 51                cmd_id = len(self.log[ip])
 52                self.log[ip].append(Stats('command', cmd_id))
 53
 54                # print(f'{iters}: sending command to {ip}:8889')
 55
 56                try:
 57                    self.socket.sendto(b'command', (ip, 8889))
 58                except:
 59                    print(f'{iters}: ERROR: {ip}:8889')
 60                    pass
 61
 62            iters = iters + 1
 63            time.sleep(5)
 64
 65        # filter out non-tello addresses in log
 66        temp = defaultdict(list)
 67        for ip in self.tello_ip_list:
 68            temp[ip] = self.log[ip]
 69        self.log = temp
 70
 71    def get_possible_ips(self):
 72        """
 73        Gets all the possible IP addresses for subnets that the computer is a part of.
 74        :return: List of IP addresses.
 75        """
 76        infos = self.get_subnets()
 77        ips = SubnetInfo.flatten([info.get_ips() for info in infos])
 78        ips = list(filter(lambda ip: ip.startswith('192.168.3.'), ips))
 79        return ips
 80
 81    def get_subnets(self):
 82        """
 83        Gets all subnet information.
 84
 85        :return: List of subnet information.
 86        """
 87        infos = []
 88
 89        for iface in netifaces.interfaces():
 90            addrs = netifaces.ifaddresses(iface)
 91
 92            if socket.AF_INET not in addrs:
 93                continue
 94
 95            # Get ipv4 stuff
 96            ipinfo = addrs[socket.AF_INET][0]
 97            address, netmask = ipinfo['addr'], ipinfo['netmask']
 98
 99            # limit range of search. This will work for router subnets
100            if netmask != '255.255.255.0':
101                continue
102
103            # Create ip object and get
104            cidr = netaddr.IPNetwork(f'{address}/{netmask}')
105            network = cidr.network
106
107            info = SubnetInfo(address, network, netmask)
108            infos.append(info)
109
110        return infos
111
112    def get_tello_list(self):
113        return self.tello_list
114
115    def send_command(self, command, ip):
116        """
117        Sends a command to the IP address. Will be blocked until the last command receives an 'OK'.
118        If the command fails (either b/c time out or error),  will try to resend the command.
119
120        :param command: Command.
121        :param ip: Tello IP.
122        :return: Response.
123        """
124        #global cmd
125        command_sof_1 = ord(command[0])
126        command_sof_2 = ord(command[1])
127
128        if command_sof_1 == 0x52 and command_sof_2 == 0x65:
129            multi_cmd_send_flag = True
130        else :
131            multi_cmd_send_flag = False
132
133        if multi_cmd_send_flag == True:      
134            self.str_cmd_index[ip] = self.str_cmd_index[ip] + 1
135            for num in range(1,5):                
136                str_cmd_index_h = self.str_cmd_index[ip] / 128 + 1
137                str_cmd_index_l = self.str_cmd_index[ip] % 128
138                if str_cmd_index_l == 0:
139                    str_cmd_index_l = str_cmd_index_l + 2
140                cmd_sof = [0x52, 0x65, str_cmd_index_h, str_cmd_index_l, 0x01, num + 1, 0x20]
141                cmd_sof_str = str(bytearray(cmd_sof))
142                cmd = cmd_sof_str + command[3:]
143                self.socket.sendto(cmd.encode('utf-8'), (ip, 8889))
144
145            print(f'[MULTI_COMMAND], IP={ip}, COMMAND={command[3:]}')
146            real_command = command[3:]
147        else:
148            self.socket.sendto(command.encode('utf-8'), (ip, 8889))
149            print(f'[SINGLE_COMMAND] IP={ip}, COMMAND={command}')
150            real_command = command
151        
152        self.log[ip].append(Stats(real_command, len(self.log[ip])))
153        start = time.time()
154
155        while not self.log[ip][-1].got_response():
156            now = time.time()
157            diff = now - start
158            if diff > self.COMMAND_TIME_OUT:
159                print(f'[NO_RESPONSE] Max timeout exceeded for command: {real_command}')
160                return    
161
162    def _receive_thread(self):
163        """
164        Listen to responses from the Tello.
165        Runs as a thread, sets self.response to whatever the Tello last returned.
166
167        :return: None.
168        """
169        while True:
170            try:
171                response, ip = self.socket.recvfrom(1024)
172                response = response.decode('utf-8')
173                self.response = response
174                
175                ip = ''.join(str(ip[0]))                
176                
177                if self.response.upper() == 'OK' and ip not in self.tello_ip_list:
178                    self.tello_ip_list.append(ip)
179                    self.last_response_index[ip] = 100
180                    self.tello_list.append(Tello(ip, self))
181                    self.str_cmd_index[ip] = 1
182                
183                response_sof_part1 = ord(self.response[0])               
184                response_sof_part2 = ord(self.response[1])
185
186                if response_sof_part1 == 0x52 and response_sof_part2 == 0x65:
187                    response_index = ord(self.response[3])
188                    
189                    if response_index != self.last_response_index[ip]:
190                        print(f'[MULTI_RESPONSE], IP={ip}, RESPONSE={self.response[7:]}')
191                        self.log[ip][-1].add_response(self.response[7:], ip)
192                    self.last_response_index[ip] = response_index
193                else:
194                    # print(f'[SINGLE_RESPONSE], IP={ip}, RESPONSE={self.response}')
195                    self.log[ip][-1].add_response(self.response, ip)
196                         
197            except socket.error as exc:
198                # swallow exception
199                # print "[Exception_Error]Caught exception socket.error : %s\n" % exc
200                pass
201
202    def get_log(self):
203        """
204        Get all logs.
205        :return: Dictionary of logs.
206        """
207        return self.log
208
209    def get_last_logs(self):
210        """
211        Gets the last logs.
212        :return: List of last logs.
213        """
214        return [log[-1] for log in self.log.values()]

Here’s the SwarmUtil class.

  1class SwarmUtil(object):
  2    """
  3    Swarm utility class.
  4    """
  5
  6    @staticmethod
  7    def create_execution_pools(num):
  8        """
  9        Creates execution pools.
 10
 11        :param num: Number of execution pools to create.
 12        :return: List of Queues.
 13        """
 14        return [queue.Queue() for x in range(num)]
 15
 16
 17    @staticmethod
 18    def drone_handler(tello, queue):
 19        """
 20        Drone handler.
 21
 22        :param tello: Tello.
 23        :param queue: Queue.
 24        :return: None.
 25        """
 26        while True:
 27            while queue.empty():
 28                pass
 29            command = queue.get()
 30            tello.send_command(command)
 31
 32
 33    @staticmethod
 34    def all_queue_empty(pools):
 35        """
 36        Checks if all queues are empty.
 37
 38        :param pools: List of Queues.
 39        :return: Boolean indicating if all queues are empty.
 40        """
 41        for queue in pools:
 42            if not queue.empty():
 43                return False
 44        return True
 45
 46
 47    @staticmethod
 48    def all_got_response(manager):
 49        """
 50        Checks if all responses are received.
 51
 52        :param manager: TelloManager.
 53        :return: A boolean indicating if all responses are received.
 54        """
 55        for log in manager.get_last_logs():
 56            if not log.got_response():
 57                return False
 58        return True
 59
 60
 61    @staticmethod
 62    def create_dir(dpath):
 63        """
 64        Creates a directory if it does not exists.
 65
 66        :param dpath: Directory path.
 67        :return: None.
 68        """
 69        if not os.path.exists(dpath):
 70            with suppress(Exception):
 71                os.makedirs(dpath)
 72
 73    @staticmethod
 74    def save_log(manager):
 75        """
 76        Saves the logs into a file in the ./log directory.
 77
 78        :param manager: TelloManager.
 79        :return: None.
 80        """
 81        dpath = './log'
 82        SwarmUtil.create_dir(dpath)
 83
 84        start_time = str(time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime(time.time())))
 85        fpath = f'{dpath}/{start_time}.txt'
 86
 87        with open(fpath, 'w') as out:
 88            log = manager.get_log()
 89            for cnt, stats in enumerate(log.values()):
 90                out.write(f'------\nDrone: {cnt + 1}\n')
 91
 92                s = [stat.get_stats_delimited() for stat in stats]
 93                s = '\n'.join(s)
 94
 95                out.write(f'{s}\n')
 96        
 97        print(f'[LOG] Saved log files to {fpath}')
 98
 99
100    @staticmethod
101    def check_timeout(start_time, end_time, timeout):
102        """
103        Checks if the duration between the end and start times
104        is larger than the specified timeout.
105
106        :param start_time: Start time.
107        :param end_time: End time.
108        :param timeout: Timeout threshold.
109        :return: A boolean indicating if the duration is larger than the specified timeout threshold.
110        """
111        diff = end_time - start_time
112        time.sleep(0.1)
113        return diff > timeout

Here’s the Swarm class.

  1class Swarm(object):
  2    """
  3    Tello Edu swarm.
  4    """
  5
  6    def __init__(self, fpath):
  7        """
  8        Ctor.
  9
 10        :param fpath: Path to command text file.
 11        """
 12        self.fpath = fpath
 13        self.commands = self._get_commands(fpath)
 14        self.manager = TelloManager()
 15        self.tellos = []
 16        self.pools = []
 17        self.sn2ip = {
 18            '0TQZGANED0021X': '192.168.3.101',
 19            '0TQZGANED0020C': '192.168.3.103',
 20            '0TQZGANED0023H': '192.168.3.104'
 21        }
 22        self.id2sn = {
 23            0: '0TQZGANED0021X',
 24            1: '0TQZGANED0020C',
 25            2: '0TQZGANED0023H'
 26        }
 27        self.ip2id = {
 28            '192.168.3.101': 0,
 29            '192.168.3.103': 1,
 30            '192.168.3.104': 2
 31        }
 32
 33    def start(self):
 34        """
 35        Main loop. Starts the swarm.
 36
 37        :return: None.
 38        """
 39        def is_invalid_command(command):
 40            if command is None:
 41                return True
 42            c = command.strip()
 43            if len(c) == 0:
 44                return True
 45            if c == '':
 46                return True
 47            if c == '\n':
 48                return True
 49            return False
 50        
 51        try:
 52            for command in self.commands:
 53                if is_invalid_command(command):
 54                    continue
 55
 56                command = command.rstrip()
 57
 58                if '//' in command:
 59                    self._handle_comments(command)
 60                elif 'scan' in command:
 61                    self._handle_scan(command)
 62                elif '>' in command:
 63                    self._handle_gte(command)
 64                elif 'battery_check' in command:
 65                    self._handle_battery_check(command)
 66                elif 'delay' in command:
 67                    self._handle_delay(command)
 68                elif 'correct_ip' in command:
 69                    self._handle_correct_ip(command)
 70                elif '=' in command:
 71                    self._handle_eq(command)
 72                elif 'sync' in command:
 73                    self._handle_sync(command)
 74            
 75            self._wait_for_all()
 76        except KeyboardInterrupt as ki:
 77            self._handle_keyboard_interrupt()
 78        except Exception as e:
 79            self._handle_exception(e)
 80            traceback.print_exc()
 81        finally:
 82            SwarmUtil.save_log(self.manager)
 83
 84    def _wait_for_all(self):
 85        """
 86        Waits for all queues to be empty and for all responses
 87        to be received.
 88
 89        :return: None.
 90        """
 91        while not SwarmUtil.all_queue_empty(self.pools):
 92            time.sleep(0.5)
 93        
 94        time.sleep(1)
 95
 96        while not SwarmUtil.all_got_response(self.manager):
 97            time.sleep(0.5)
 98
 99    def _get_commands(self, fpath):
100        """
101        Gets the commands.
102
103        :param fpath: Command file path.
104        :return: List of commands.
105        """
106        with open(fpath, 'r') as f:
107            return f.readlines()
108
109    def _handle_comments(self, command):
110        """
111        Handles comments.
112
113        :param command: Command.
114        :return: None.
115        """
116        print(f'[COMMENT] {command}')
117
118    def _handle_scan(self, command):
119        """
120        Handles scan.
121
122        :param command: Command.
123        :return: None.
124        """
125        n_tellos = int(command.partition('scan')[2])
126
127        self.manager.find_avaliable_tello(n_tellos)
128        self.tellos = self.manager.get_tello_list()
129        self.pools = SwarmUtil.create_execution_pools(n_tellos)
130
131        for x, (tello, pool) in enumerate(zip(self.tellos, self.pools)):
132            self.ip2id[tello.tello_ip] = x
133
134            t = Thread(target=SwarmUtil.drone_handler, args=(tello, pool))
135            t.daemon = True
136            t.start()
137
138            print(f'[SCAN] IP = {tello.tello_ip}, ID = {x}')
139
140    def _handle_gte(self, command):
141        """
142        Handles gte or >.
143
144        :param command: Command.
145        :return: None.
146        """
147        id_list = []
148        id = command.partition('>')[0]
149
150        if id == '*':
151            id_list = [t for t in range(len(self.tellos))]
152        else:
153            id_list.append(int(id)-1) 
154        
155        action = str(command.partition('>')[2])
156
157        for tello_id in id_list:
158            sn = self.id2sn[tello_id]
159            ip = self.sn2ip[sn]
160            id = self.ip2id[ip]
161
162            self.pools[id].put(action)
163            print(f'[ACTION] SN = {sn}, IP = {ip}, ID = {id}, ACTION = {action}')
164
165    def _handle_battery_check(self, command):
166        """
167        Handles battery check. Raises exception if any drone has
168        battery life lower than specified threshold in the command.
169
170        :param command: Command.
171        :return: None.
172        """
173        threshold = int(command.partition('battery_check')[2])
174        for queue in self.pools:
175            queue.put('battery?')
176
177        self._wait_for_all()
178
179        is_low = False
180
181        for log in self.manager.get_last_logs():
182            battery = int(log.response)
183            drone_ip = log.drone_ip
184
185            print(f'[BATTERY] IP = {drone_ip}, LIFE = {battery}%')
186
187            if battery < threshold:
188                is_low = True
189        
190        if is_low:
191            raise Exception('Battery check failed!')
192        else:
193            print('[BATTERY] Passed battery check')
194
195    def _handle_delay(self, command):
196        """
197        Handles delay.
198
199        :param command: Command.
200        :return: None.
201        """
202        delay_time = float(command.partition('delay')[2])
203        print (f'[DELAY] Start Delay for {delay_time} second')
204        time.sleep(delay_time)  
205
206    def _handle_correct_ip(self, command):
207        """
208        Handles correction of IPs.
209
210        :param command: Command.
211        :return: None.
212        """
213        for queue in self.pools:
214            queue.put('sn?') 
215
216        self._wait_for_all()
217        
218        for log in self.manager.get_last_logs():
219            sn = str(log.response)
220            tello_ip = str(log.drone_ip)
221            self.sn2ip[sn] = tello_ip
222
223            print(f'[CORRECT_IP] SN = {sn}, IP = {tello_ip}')
224
225    def _handle_eq(self, command):
226        """
227        Handles assignments of IDs to serial numbers.
228
229        :param command: Command.
230        :return: None.
231        """
232        id = int(command.partition('=')[0])
233        sn = command.partition('=')[2]
234        ip = self.sn2ip[sn]
235
236        self.id2sn[id-1] = sn
237        
238        print(f'[IP_SN_ID] IP = {ip}, SN = {sn}, ID = {id}')
239
240    def _handle_sync(self, command):
241        """
242        Handles synchronization.
243
244        :param command: Command.
245        :return: None.
246        """
247        timeout = float(command.partition('sync')[2])
248        print(f'[SYNC] Sync for {timeout} seconds')
249
250        time.sleep(1)
251
252        try:
253            start = time.time()
254            
255            while not SwarmUtil.all_queue_empty(self.pools):
256                now = time.time()
257                if SwarmUtil.check_timeout(start, now, timeout):
258                    raise RuntimeError('Sync failed since all queues were not empty!')
259
260            print('[SYNC] All queues empty and all commands sent')
261           
262            while not SwarmUtil.all_got_response(self.manager):
263                now = time.time()
264                if SwarmUtil.check_timeout(start, now, timeout):
265                    raise RuntimeError('Sync failed since all responses were not received!')
266            
267            print('[SYNC] All response received')
268        except RuntimeError:
269            print('[SYNC] Failed to sync; timeout exceeded')
270
271    def _handle_keyboard_interrupt(self):
272        """
273        Handles keyboard interrupt.
274
275        :param command: Command.
276        :return: None.
277        """
278        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).