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.
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 |
Fly upward [20, 500] cm |
ok|error |
down |
Fly downward [20, 500] cm |
ok|error |
left |
Fly left [20, 500] cm |
ok|error |
right |
Fly right [20, 500] cm |
ok|error |
forward |
Fly forward [20, 500] cm |
ok|error |
back |
Fly backward [20, 500] cm |
ok|error |
cw |
Rotate clockwise [1, 360] degrees |
ok|error |
ccw |
Rotate counter-clockwise [1, 360] degrees |
ok|error |
flip |
Flip [l, r, f, b] |
ok|error |
speed |
Set speed to [10, 100] cm/s |
ok|error |
go |
Fly to x, y, z at speed |
ok|error |
|
||
|
||
curve |
Fly at a curve between the two given coordinates at speed |
ok|error |
|
||
|
||
|
||
go |
Fly to x, y, z of mission pad at speed |
ok|error |
|
||
|
||
|
||
curve |
Fly at a curve between the two given coordinates of mission pad at speed |
ok|error |
|
||
|
||
|
||
|
||
jump |
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 |
|
||
|
||
|
||
wifi |
Set WiFi password |
ok|error |
mon |
Enable mission pad detection |
ok|error |
moff |
Disable mission pad detection |
ok|error |
mdirection |
Enable mission pad detection |
ok|error |
|
||
|
||
|
||
ap |
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 |
|
wifi? |
Get WiFi SNR |
|
sdk? |
Get the SDK version |
|
sn? |
Get the serial number |
|
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
orAccess 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.
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).