├── demo_status.py ├── LICENSE ├── demo_single.py ├── demo_all_functions.py ├── demo_search.py ├── emergency_stop.py ├── tello.py ├── README.md ├── comms_manager.py └── fly_tello.py /demo_status.py: -------------------------------------------------------------------------------- 1 | from fly_tello import FlyTello 2 | my_tellos = list() 3 | 4 | 5 | # 6 | # SIMPLE EXAMPLE - MOST BASIC FLIGHT TO SHOW STATUS MESSAGES 7 | # 8 | # SETUP: Any number of Tellos 9 | # 10 | 11 | 12 | # 13 | # MAIN FLIGHT CONTROL LOGIC 14 | # 15 | 16 | # Define the Tello's we're using, in the order we want them numbered 17 | my_tellos.append('0TQDFC6EDBBX03') # 1-Yellow 18 | my_tellos.append('0TQDFC6EDB4398') # 2-Blue 19 | # my_tellos.append('0TQDFC6EDBH8M8') # 3-Green 20 | # my_tellos.append('0TQDFC7EDB4874') # 4-Red 21 | 22 | # Control the flight 23 | with FlyTello(my_tellos, get_status=True) as fly: 24 | fly.print_status(sync=True) 25 | fly.takeoff() 26 | fly.print_status(sync=True) 27 | fly.land() 28 | fly.print_status(sync=True) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dave Walker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo_single.py: -------------------------------------------------------------------------------- 1 | from fly_tello import FlyTello 2 | my_tellos = list() 3 | 4 | 5 | # 6 | # SIMPLE EXAMPLE - SINGLE TELLO WITH MISSION PAD 7 | # 8 | # SETUP: Tello on Mission Pad, facing in direction of the pad - goes max 50cm left and 100cm forward - takes ~45sec 9 | # 10 | 11 | # 12 | # MAIN FLIGHT CONTROL LOGIC 13 | # 14 | 15 | # Define the Tello's we're using, in the order we want them numbered 16 | my_tellos.append('0TQDFC6EDBBX03') # 1-Yellow 17 | # my_tellos.append('0TQDFC6EDB4398') # 2-Blue 18 | # my_tellos.append('0TQDFC6EDBH8M8') # 3-Green 19 | # my_tellos.append('0TQDFC7EDB4874') # 4-Red 20 | 21 | # Control the flight 22 | with FlyTello(my_tellos) as fly: 23 | fly.takeoff() 24 | fly.forward(dist=50) 25 | fly.back(dist=50) 26 | fly.reorient(height=100, pad='m-2') 27 | fly.left(dist=50) 28 | fly.flip(direction='right') 29 | fly.reorient(height=100, pad='m-2') 30 | fly.curve(x1=50, y1=30, z1=0, x2=100, y2=30, z2=-20, speed=60) 31 | fly.curve(x1=-50, y1=-30, z1=0, x2=-100, y2=-30, z2=20, speed=60) 32 | fly.reorient(height=100, pad='m-2') 33 | fly.rotate_cw(angle=360, tello=1) 34 | fly.straight_from_pad(x=30, y=0, z=75, speed=100, pad='m-2') 35 | fly.flip(direction='back') 36 | fly.reorient(height=50, pad='m-2') 37 | fly.land() 38 | -------------------------------------------------------------------------------- /demo_all_functions.py: -------------------------------------------------------------------------------- 1 | from fly_tello import FlyTello 2 | my_tellos = list() 3 | 4 | 5 | # 6 | # SIMPLE EXAMPLE - TWO TELLOs FLYING IN SYNC, DEMO'ING ALL KEY TELLO CAPABILITIES 7 | # 8 | # SETUP: Tello both facing away from controller, first Tello on the left, approx 0.5-1m apart 9 | # 10 | 11 | 12 | # 13 | # MAIN FLIGHT CONTROL LOGIC 14 | # 15 | 16 | # Define the Tello's we're using, in the order we want them numbered 17 | my_tellos.append('0TQDFC6EDBBX03') # 1-Yellow 18 | my_tellos.append('0TQDFC6EDB4398') # 2-Blue 19 | # my_tellos.append('0TQDFC6EDBH8M8') # 3-Green 20 | # my_tellos.append('0TQDFC7EDB4874') # 4-Red 21 | 22 | # Control the flight 23 | with FlyTello(my_tellos) as fly: 24 | fly.takeoff() 25 | fly.forward(dist=50) 26 | fly.back(dist=50) 27 | fly.reorient(height=100, pad='m-2') 28 | with fly.sync_these(): 29 | fly.left(dist=50, tello=1) 30 | fly.right(dist=50, tello=2) 31 | with fly.sync_these(): 32 | fly.flip(direction='right', tello=1) 33 | fly.flip(direction='left', tello=2) 34 | fly.reorient(height=100, pad='m-2') 35 | fly.straight(x=75, y=75, z=0, speed=100) 36 | fly.curve(x1=-55, y1=-20, z1=0, x2=-75, y2=-75, z2=0, speed=60) 37 | with fly.sync_these(): 38 | fly.rotate_cw(angle=360, tello=1) 39 | fly.rotate_ccw(angle=360, tello=2) 40 | fly.straight_from_pad(x=50, y=0, z=75, speed=100, pad='m-2') 41 | fly.flip(direction='back') 42 | fly.reorient(height=50, pad='m-2') 43 | fly.land() 44 | -------------------------------------------------------------------------------- /demo_search.py: -------------------------------------------------------------------------------- 1 | from fly_tello import FlyTello 2 | my_tellos = list() 3 | 4 | 5 | # 6 | # SIMPLE EXAMPLE - TWO TELLOs INDEPENDENTLY SEARCHING FOR MISSION PADS, IN AN OUTWARD SPIRAL PATTERN. 7 | # 8 | # PHYSICAL SETUP: Two Tellos back to back, ~30-50cm apart, with mission pads scattered roughly around. 9 | # 10 | 11 | 12 | # 13 | # INDIVIDUAL BEHAVIOURAL FUNCTIONS 14 | # 15 | 16 | def threaded_search_test(tello, pad_id): 17 | """ This function defines Tello behaviour for the search itself, when each Tello is searching independently. """ 18 | found = fly.search_spiral(dist=50, spirals=2, height=100, speed=100, pad=pad_id, tello=tello) 19 | if found: 20 | print('[Search]Tello %d Found the Mission Pad!' % tello) 21 | # Hover at low-level directly over the pad, to make an accurate landing 22 | fly.reorient(height=40, pad=pad_id, tello=tello, sync=False) 23 | fly.land(tello=tello) 24 | fly.flight_complete(tello=tello) 25 | 26 | 27 | # 28 | # MAIN FLIGHT CONTROL LOGIC 29 | # 30 | 31 | # Define the Tello's we're using, in the order we want them numbered 32 | my_tellos.append('0TQDFC6EDBBX03') # 1-Yellow 33 | my_tellos.append('0TQDFC6EDB4398') # 2-Blue 34 | # my_tellos.append('0TQDFC6EDBH8M8') # 3-Green 35 | # my_tellos.append('0TQDFC7EDB4874') # 4-Red 36 | 37 | # Control the flight 38 | with FlyTello(my_tellos) as fly: 39 | fly.pad_detection_on() 40 | fly.set_pad_detection(direction='downward') 41 | fly.takeoff() 42 | with fly.individual_behaviours(): 43 | # Tellos will each fly independently, as defined in the function above. If they find their own mission pad they 44 | # will land and ignore any later commands. Otherwise, they will continue after the with statement ends. 45 | fly.run_individual(threaded_search_test, tello_num=1, pad_id='m1') 46 | fly.run_individual(threaded_search_test, tello_num=2, pad_id='m2') 47 | fly.land() 48 | -------------------------------------------------------------------------------- /emergency_stop.py: -------------------------------------------------------------------------------- 1 | # 2 | # Quick and basic code for running a standalone "emergency stop" in case of any problems 3 | # 4 | # 5 | # 6 | 7 | import socket 8 | import netaddr 9 | import netifaces 10 | import time 11 | 12 | 13 | # 14 | # FUNCTION DEFINITIONS 15 | # 16 | 17 | def _get_subnets(): 18 | """ Get the local subnet and server IP address """ 19 | subnets = [] 20 | addr_list = [] 21 | ifaces = netifaces.interfaces() 22 | for this_iface in ifaces: 23 | addrs = netifaces.ifaddresses(this_iface) 24 | 25 | if socket.AF_INET not in addrs: 26 | continue 27 | 28 | # Get IPv4 info 29 | ip_info = addrs[socket.AF_INET][0] 30 | address = ip_info['addr'] 31 | netmask = ip_info['netmask'] 32 | 33 | # Avoid searching when on very large subnets 34 | if netmask != '255.255.255.0': 35 | continue 36 | 37 | # Create IP object and get the network details 38 | # Note CIDR is a networking term, describing the IP/subnet address format 39 | cidr = netaddr.IPNetwork('%s/%s' % (address, netmask)) 40 | network = cidr.network 41 | subnets.append((network, netmask)) 42 | addr_list.append(address) 43 | return subnets, addr_list 44 | 45 | 46 | def send_command(command, possible_addr, control_socket, control_port): 47 | # Send the command to each Tello on each possible_addr 48 | for ip in possible_addr: 49 | try: 50 | print('Sending %s command to drone at %s' % (command, ip)) 51 | control_socket.sendto(command.encode(), (ip, control_port)) 52 | except OSError as oserror: 53 | print(oserror) 54 | print('ERROR! Socket failed - terminating!') 55 | exit() 56 | time.sleep(0.01) 57 | 58 | 59 | def initialise(first_ip, last_ip, control_port, possible_addr): 60 | 61 | # Create socket for communication with Tello 62 | control_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # socket for sending cmd 63 | control_socket.bind(('', control_port)) 64 | 65 | # Get network addresses to search 66 | subnets, address = _get_subnets() 67 | 68 | # Create a list of possible IP addresses to search 69 | for subnet, netmask in subnets: 70 | for ip in netaddr.IPNetwork('%s/%s' % (subnet, netmask)): 71 | if not (first_ip <= int(str(ip).split('.')[3]) <= last_ip): 72 | continue 73 | # Don't add the server's address to the list 74 | if str(ip) in address: 75 | continue 76 | possible_addr.append(str(ip)) 77 | 78 | return possible_addr, control_socket 79 | 80 | 81 | # 82 | # MAIN SCRIPT 83 | # 84 | 85 | print('Emergency Stop Application Started!') 86 | first_ip = 51 87 | last_ip = 54 88 | control_port = 8889 89 | control_socket = None 90 | possible_addr = [] 91 | 92 | while True: 93 | # Do nothing until we've got a command 94 | command = input('Emergency Stop? ' ' or L = Auto-Land | S = Stop | E = Emergency Cut-Out | Q = Quit: ') 95 | 96 | # If not already initalised, initialise the network connection via a UDP socket 97 | if not control_socket: 98 | possible_addr, control_socket = initialise(first_ip, last_ip, control_port, possible_addr) 99 | print('Connection initialised!') 100 | 101 | if command.upper() == ' ': 102 | send_command('land', possible_addr, control_socket, control_port) 103 | elif command.upper() == 'L': 104 | send_command('land', possible_addr, control_socket, control_port) 105 | elif command.upper() == 'S': 106 | send_command('stop', possible_addr, control_socket, control_port) 107 | elif command.upper() == 'E': 108 | send_command('emergency', possible_addr, control_socket, control_port) 109 | elif command.upper() == 'Q': 110 | print('Q(uit) command received - exiting!') 111 | exit() 112 | else: 113 | print('Invalid command - enter space (to land), L(and), S(top), E(mergency), or Q(uit)') 114 | 115 | time.sleep(0.1) 116 | -------------------------------------------------------------------------------- /tello.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class Tello: 5 | """ Holds details about each individual Tello """ 6 | 7 | # 8 | # CLASS INIT 9 | # 10 | 11 | def __init__(self, ip): 12 | """ Keep track of the IP, SN and Num of each Tello, plus its command_queue and log """ 13 | self.ip = ip 14 | self.sn = None 15 | self.num = 0 16 | self.max_cmd_id = 0 17 | self.command_queue = [] 18 | self.log = [] 19 | self.flight_complete = False 20 | self.status = {} 21 | 22 | # 23 | # COMMAND_QUEUE AND LOG MANAGEMENT 24 | # 25 | 26 | def add_to_command_queue(self, command, command_type, on_error): 27 | """ Queues commands, which will be sent via the command_handler thread as soon as the Tello is ready. 28 | 29 | Each command in the queue is given a cmd_id, an increasing index, which is then carried over to the log - 30 | this allows commands and their responses to be tracked and tested reliable. 31 | Will not allow any new commands to be added to the queue once marked as flight_complete! 32 | :param command: The actual command from Tello SDK, e.g. 'battery?', 'forward 50', etc... 33 | :param command_type: Either 'Control', 'Set' or 'Read' - corresponding to the Tello SDK documentation. 34 | :param on_error: An alternative Tello SDK string to be sent if command returns an error. 35 | :return: The cmd_id for this new entry in the queue, to allow calling functions to track the response. 36 | """ 37 | if not self.flight_complete: 38 | self.max_cmd_id += 1 39 | self.command_queue.append(TelloCommand(self.max_cmd_id, command, command_type, on_error)) 40 | return self.max_cmd_id 41 | else: 42 | return -1 43 | 44 | def add_to_log(self, cmd_id, command, command_type, on_error): 45 | """ Logs commands; usually having just been taken out of the command_queue. 46 | 47 | :param cmd_id: The cmd_id that was previously assigned in the command_queue. 48 | :param command: The actual command from Tello SDK, e.g. 'battery?', 'forward 50', etc... 49 | :param command_type: Either 'Control', 'Set' or 'Read' - corresponding to the Tello SDK documentation. 50 | :param on_error: An alternative Tello SDK string to be sent if command returns an error, or None. 51 | :return: The new log entry (as a TelloCommand instance) 52 | """ 53 | new_log_entry = TelloCommand(cmd_id, command, command_type, on_error) 54 | self.log.append(new_log_entry) 55 | return new_log_entry 56 | 57 | def log_entry(self, cmd_id=None, timeout=10): 58 | """ Return the log entry for specified cmd_id, or latest if cmd_id is None. 59 | 60 | :param cmd_id: The cmd_id of the log entry we're looking for, or None for the last entry. 61 | :param timeout: Max seconds to wait if log entry doesn't exist yet, i.e. command hasn't yet been sent. 62 | :return: Returns the corresponding log entry, which will be a TelloCommand instance. 63 | """ 64 | return self._get_log_entry(cmd_id, timeout) 65 | 66 | # 67 | # PENDING RESPONSE 68 | # 69 | 70 | def wait_until_idle(self): 71 | """ Blocking method, will only return once command_queue is empty and the last response is received. """ 72 | # TODO: Add timeout to this method? 73 | while self.command_queue: 74 | time.sleep(0.05) 75 | while self.log[-1].response is None: 76 | time.sleep(0.05) 77 | 78 | def log_wait_response(self, cmd_id=None, timeout=10): 79 | """ Blocking method, will return log entry once it's been sent and response is received. 80 | 81 | :param cmd_id: The cmd_id of the log entry we're looking for, or None for the last entry. 82 | :param timeout: Max seconds to wait for response. 83 | :return: Returns the corresponding log entry, which will be a TelloCommand instance. 84 | """ 85 | log_entry = self._get_log_entry(cmd_id, timeout) 86 | while log_entry.response is None: 87 | # Sleep briefly whilst waiting for response, to prevent excessive CPU usage 88 | time.sleep(0.01) 89 | return log_entry 90 | 91 | # 92 | # PRIVATE HELPER METHODS 93 | # 94 | 95 | def _get_log_entry(self, cmd_id, timeout): 96 | """ Returns a log entry (TelloCommand object), either matching the cmd_id or else the latest log entry. 97 | 98 | This is a blocking function, that will wait max timeout secs for the log to become available, i.e. for the 99 | command to be sent! 100 | 101 | :param cmd_id: Either the cmd_id of the entry we want, or None, which will return the most recent log entry. 102 | :param timeout: Max seconds to wait for response, before raising an error. 103 | :return: Returns the corresponding log entry, which will be a TelloCommand object. 104 | """ 105 | if cmd_id is None: 106 | return self.log[-1] 107 | else: 108 | timeout_start = time.time() 109 | while time.time() - timeout_start < timeout: 110 | for log in self.log: 111 | if log.cmd_id == cmd_id: 112 | return log 113 | # Sleep briefly at the end of each loop, to prevent excessive CPU usage 114 | time.sleep(0.01) 115 | raise RuntimeError('Tello log entry not found!!') 116 | 117 | 118 | class TelloCommand: 119 | """ Simple class holding data associated with individual commands - used for both command_queue and log. """ 120 | 121 | def __init__(self, cmd_id, command, command_type, on_error): 122 | """ Create a new instance, with key fields populated at the start. response and success are updated later. 123 | 124 | :param cmd_id: An integer to uniquely identify this command. 125 | :param command: The actual command from Tello SDK, e.g. 'battery?', 'forward 50', etc... 126 | :param command_type: Either 'Control', 'Set' or 'Read' - corresponding to the Tello SDK documentation. 127 | :param on_error: An alternative Tello SDK string to be sent if command returns an error, or None. 128 | """ 129 | self.cmd_id = cmd_id 130 | self.command = command 131 | self.command_type = command_type 132 | self.response = None 133 | self.success = None 134 | self.on_error = on_error 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tello Edu: Swarm & Search 2 | - 3 | 4 | **The Tello Edu** 5 | 6 | After coming across the Tello Edu in Jan 2019, it seemed to offer something very new to the small drone market. Being designed for education, it had a few key features: 7 | * It had a published, open API allowing it to be easily flown with some very simple code. 8 | * Many of them can join the same WiFi Network, enabling multiple Tellos to fly in a **swarm**. 9 | * It has built-in image recognition for a set of 'Mission Pads' which are supplied with it, enabling improved positioning accuracy and **search** challenges to be performed. 10 | 11 | Note that the Tello Edu is different from the original Tello - the two key differences are noted in the following section. 12 | 13 | **Background & Motivation** 14 | 15 | This project is a replacement library for the Tello Edu, based on the official Python SDK here: https://github.com/TelloSDK/Multi-Tello-Formation 16 | 17 | The motivations for creating this new project, rather than simply using or updating the official `Multi-Tello-Formation` project were: 18 | * Python 3 support 19 | * Full support for all Tello Edu functionality 20 | * This project should work equally well with both original **Tello**, and **Tello Edu**, with a couple of limitations: 21 | * The original Tello lacks the option to connect it to a WiFi Access Point, and so is limited to a single Tello. 22 | * The original Tello lacks support for Mission Pads, so any methods using those will only work with a Tello Edu. 23 | * More advanced direct Python flight controls, rather than parsing commands from a text file 24 | * Enabling conditional flight controls, for example using the result of one command to determine the next 25 | * Removing race conditions which could occur when a second command was issued before the prior command was fully processed *(This was the biggest driver for starting a completely new project!)* 26 | * Implementing easy shortcut methods for both the Tello SDK and some aggregate behaviours, e.g. following search patterns 27 | * Making both synchronised and totally independent flight behaviours more intuitive and clear to programme 28 | 29 | There are some recommendations at the end of this README about improvements I'd suggest for further development. I don't plan to continue those developments myself, but welcome others to fork this project to do so. 30 | 31 | **Configuration / Setup** 32 | 33 | Only two non-standard Python libraries are required - ```netifaces``` and ```netaddr```. These are available for Windows, Mac and Linux. Otherwise this project is self-contained. 34 | 35 | Out of the box, each Tello and Tello Edu is configured with its own WiFi network, to which you connect in order to control them. However, the Tello Edu can also be made to connect to any other WiFi Network - a pre-requisite for any swarm behaviour. Once configured, the Tello Edu will always connect to this WiFi Network, until it is reset (by turning on then holding power button for 5-10secs). 36 | 37 | To make a Tello Edu connect to a WiFi Network, connect to the Tello (initially by connecting to its own WiFi network) then run the following: 38 | ``` 39 | from fly_tello import FlyTello 40 | with FlyTello(['XXX']) as fly: 41 | fly.set_ap_wifi(ssid='MY_SSID', password='MY_PASSWORD') 42 | ``` 43 | The above code initialises FlyTello, and then sets the SSID and Password you supply. You can get the Serial Number for your Tello from a tiny sticker inside the battery compartment, but by using `'XXX'` here FlyTello will print the Serial Number to the Console anyway. You should usually provide the Serial Number when initialising FlyTello, but it's not essential here because there's only one. 44 | 45 | **Project Structure** 46 | 47 | There are three key files in the project: 48 | * `fly_tello.py` - The `FlyTello` class is intended to be the only one that a typical user needs to use. It contains functions enabling all core behaviours of one or more Tellos, including some complex behaviour such as searching for Mission Pads. This should always be the starting point. 49 | * `comms_manager.py` - The `CommsManager` class performs all of the core functions that communicate with the Tellos, sending and receiving commands and status messages, and ensuring they are acted on appropriately. If you want to develop new non-standard behaviours, you'll probably need some of these functions. 50 | * `tello.py` - The `Tello` class stores key parameters for each Tello, enabling the rest of the functionality. The `TelloCommand` class provides the structure for both queued commands, and logs of commands which have already been sent. 51 | 52 | **FlyTello** 53 | 54 | Using `FlyTello` provides the easiest route to flying one or more Tellos. A simple demonstration would require the following code: 55 | ``` 56 | from fly_tello import FlyTello # Import FlyTello 57 | 58 | my_tellos = list() 59 | my_tellos.append('0TQDFCAABBCCDD') # Replace with your Tello Serial Number 60 | my_tellos.append('0TQDFCAABBCCEE') # Replace with your Tello Serial Number 61 | 62 | with FlyTello(my_tellos) as fly: # Use FlyTello as a Context Manager to ensure safe landing in case of any errors 63 | fly.takeoff() # Single command for all Tellos to take-off 64 | fly.forward(50) # Single command for all Tellos to fly forward by 50cm 65 | with fly.sync_these(): # Keep the following commands in-sync, even with different commands for each Tello 66 | fly.left(30, tello=1) # Tell just Tello1 to fly left 67 | fly.right(30, tello=2) # At the same time, Tello2 will fly right 68 | fly.flip(direction='forward') # Flips are easy to perform via the Tello SDK 69 | fly.land() # Finally, land 70 | ``` 71 | 72 | It is suggested to browse through `fly_tello.py` for full details of the available methods which you can use - all are fully commented and explained in the code. A few worth mentioning however include: 73 | * Every function listed in the Tello SDK v2.0 (available to download from https://www.ryzerobotics.com/tello-edu/downloads) is implemented as a method within FlyTello; though some have been renamed for clarity. 74 | * `reorient()` - a simplified method which causes the Tello to centre itself over the selected (or any nearby) Mission Pad. This is really helpful for long-running flights to ensure the Tellos remain exactly in the right positions. 75 | * `search_spiral()` - brings together multiple Tello SDK commands to effectively perform a search for a Mission Pad, via one very simple Python command. It will stop over the top of the Mission Pad if it finds it, otherwise returns to its starting position. 76 | * `search_pattern()` - like search_spiral, but you can specify any pattern you like for the search via a simple list of coordinates. 77 | * `sync_these()` - when used as a Context Manager (as a `with` block), this ensures all Tellos are in sync before any functions within the block are executed. 78 | 79 | `FlyTello` also provides a simple method of programming individual behaviours, which allow each Tello to behave and follow its own independent set of instructions completely independently from any other Tello. For full details read the comments in `fly_tello.py`, but key extracts from an example of this are also shown below: 80 | ``` 81 | # independent() is used to package up the FlyTello commands for the independent phase of the flight 82 | def independent(tello, pad): 83 | found = fly.search_spiral(dist=50, spirals=2, height=100, speed=100, pad=pad, tello=tello) 84 | if found: 85 | print('[Search]Tello %d Found the Mission Pad!' % tello) 86 | fly.land(tello=tello) 87 | 88 | with FlyTello(my_tellos) as fly: 89 | with fly.individual_behaviours(): 90 | # individual_behaviours() is a Context Manager to ensure separate threads are setup and managed for each Tello's 91 | # own behaviour, as defined in the independent() function above. 92 | # run_individual() actually initiates the behaviour for a single Tello - in this case both searching, but each 93 | # is searching for a different Mission Pad ('m1' vs 'm2'). 94 | fly.run_individual(independent, tello_num=1, pad_id='m1') 95 | fly.run_individual(independent, tello_num=2, pad_id='m2') 96 | ``` 97 | 98 | **Demos** 99 | 100 | Two demo videos are provided on YouTube, showing the capabilities of Tello Edu with this library. 101 | * Tello Edu Capabilities Demo (`demo_all_functions.py`) - https://youtu.be/F3rSW5VKsW8 102 | * Simple Searching Demo (`demo_search.py`) - https://youtu.be/pj2fJe7cPTE 103 | 104 | **Limitations** 105 | 106 | There are some limitations of what can be done with this project and the Tello Edu: 107 | * No Video Stream. The Tello is capable of sending its video stream, but only when connected directly to the in-build WiFi of a single Tello. The video is not accessible when the Tellos are connected to a separate WiFi network, as required for swarming behaviour. There is a workaround, which is to have multiple WiFi dongles connected to a single computer, one per Tello, but that hasn't been a focus for me. 108 | * Limited Status Messages. The Tello does broadcast a regular (multiple times per second) status message, however this seems to be of limited value as many of the values do not seem to correspond with the Tello's behaviour, and others are rather erratic. This requires further investigation to determine which are useful. 109 | 110 | **Recommendations** 111 | 112 | The project as it is currently is enough to fly one or more Tello Edu drones via a simple yet sophisticated set of controls. Expanding its capabilities is easy, with layers of modules which expose increasingly more detailed / low-level functionality. I'd suggest adding or changing: 113 | * Position Tracking. By tracking the relative position of each Tello from when it launches, this will enable behaviours such as "return to start", and will e.g. allow Mission Pad locations to be shared with other Tellos in the swarm - a pre-requisite for collaborative swarm behaviour. Clearly accuracy will decrease over time, but could be regularly restored using the `reorient()` method described above. 114 | * Better Error Checking. Some error checking is already implemented, but it's incomplete. Getting the arc radius correct for a curve is sometimes difficult, and this project could be more helpful in identifying the errors and suggesting valid alternative values. 115 | * Implement `on_error` alternative commands for Flips and Curves, which can easily fail due to e.g. battery low or incorrect curve radius values. This will ensure Tello is less likely to end up in an unexpected location. 116 | * Command Stream & Logging. Currently all commands either sent or received are printed to the Python Console. These would be better saved in a detailed log file, so that only key information is presented to the user in the Console. 117 | 118 | -------------------------------------------------------------------------------- /comms_manager.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import netifaces 3 | import netaddr 4 | import threading 5 | import time 6 | from tello import Tello 7 | 8 | 9 | class CommsManager: 10 | 11 | # 12 | # CLASS INIT & SETUP 13 | # 14 | 15 | def __init__(self): 16 | """ Open sockets ready for communicating with one or more Tellos. 17 | 18 | Also initiate the threads for receiving control messages and status from Tello. 19 | Also create the placeholder list for Tello objects. 20 | """ 21 | 22 | self.terminate_comms = False 23 | 24 | # Socket for primary bi-directional communication with Tello 25 | self.control_port = 8889 26 | self.control_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # socket for sending cmd 27 | self.control_socket.bind(('', self.control_port)) 28 | 29 | # Socket for receiving status messages from Tello - not activated here 30 | self.status_port = 8890 31 | self.status_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 32 | self.status_thread = None 33 | 34 | # Thread for receiving messages from Tello 35 | self.receive_thread = threading.Thread(target=self._receive_thread) 36 | self.receive_thread.daemon = True 37 | self.receive_thread.start() 38 | 39 | # Reference to all active Tellos 40 | self.tellos = [] 41 | 42 | def init_tellos(self, sn_list, get_status=False, first_ip=1, last_ip=254): 43 | """ Search the network until found the specified number of Tellos, then get each Tello ready for use. 44 | 45 | This must be run once; generally the first thing after initiating CommsManager. 46 | The 'command' message is sent to every IP on the network, with the response_handler thread managing the 47 | responses to create Tello objects in self.tellos. 48 | A command_handler is then created for each, which manages the command_queue for each. 49 | Finally, each Tello is queried for its serial number, which is stored in the Tello object with its number. 50 | 51 | :param sn_list: List of serial numbers, in order we want to number the Tellos. 52 | :param get_status: True to listen for and record the status messages from the Tellos. 53 | :param first_ip: If known, we can specify a smaller range of IP addresses to speed up the search. 54 | :param last_ip: If known, we can specify a smaller range of IP addresses to speed up the search. 55 | """ 56 | 57 | # Get network addresses to search 58 | subnets, address = self._get_subnets() 59 | possible_addr = [] 60 | 61 | # Create a list of possible IP addresses to search 62 | for subnet, netmask in subnets: 63 | for ip in netaddr.IPNetwork('%s/%s' % (subnet, netmask)): 64 | if not (first_ip <= int(str(ip).split('.')[3]) <= last_ip): 65 | continue 66 | # Don't add the server's address to the list 67 | if str(ip) in address: 68 | continue 69 | possible_addr.append(str(ip)) 70 | 71 | # Continue looking until we've found them all 72 | num = len(sn_list) 73 | while len(self.tellos) < num: 74 | print('[Tello Search]Looking for %d Tello(s)' % (num - len(self.tellos))) 75 | 76 | # Remove any found Tellos from the list to search 77 | for tello_ip in [tello.ip for tello in self.tellos]: 78 | if tello_ip in possible_addr: 79 | possible_addr.remove(tello_ip) 80 | 81 | # Try contacting Tello via each possible_addr 82 | for ip in possible_addr: 83 | self.control_socket.sendto('command'.encode(), (ip, self.control_port)) 84 | 85 | # Responses to the command above will be picked up in receive_thread. Here we check regularly to see if 86 | # they've all been found, so we can break out quickly. But after several failed attempts, go around the 87 | # whole loop again and retry contacting. 88 | for _ in range(0, 10): 89 | time.sleep(0.5) 90 | if len(self.tellos) >= num: 91 | break 92 | 93 | # Once we have all Tellos, startup a command_handler for each. These manage the command queues for each Tello. 94 | for tello in self.tellos: 95 | command_handler_thread = threading.Thread(target=self._command_handler, args=(tello,)) 96 | command_handler_thread.daemon = True 97 | command_handler_thread.start() 98 | 99 | # Start the status_handler, if needed. This receives and constantly updates the status of each Tello. 100 | if get_status: 101 | self.status_socket.bind(('', self.status_port)) 102 | self.status_thread = threading.Thread(target=self._status_thread) 103 | self.status_thread.daemon = True 104 | self.status_thread.start() 105 | 106 | # Query each Tello to get its serial number - saving the cmd_id so we can match-up responses when they arrive 107 | tello_cmd_id = [] 108 | for tello in self.tellos: 109 | # Save the tello together with the returned cmd_id, so we can match the responses with the right Tello below 110 | tello_cmd_id.append((tello, tello.add_to_command_queue('sn?', 'Read', None))) 111 | 112 | # Assign the sn to each Tello, as responses become available. 113 | # Note that log_wait_response will block until the response is received. 114 | for tello, cmd_id in tello_cmd_id: 115 | tello.sn = tello.log_wait_response(cmd_id).response 116 | # Once we know the SN, look it up in the supplied sn_list and assign the correct tello_num 117 | for index, sn in enumerate(sn_list, 1): 118 | if tello.sn == sn: 119 | tello.num = index 120 | 121 | # Sort the list of Tellos by their num 122 | self.tellos.sort(key=lambda tello: tello.num) 123 | 124 | # 125 | # PUBLIC METHODS 126 | # 127 | 128 | def queue_command(self, command, command_type, tello_num, on_error=None): 129 | """ Add a new command to the Tello's (either one Tello or all) command queue - returning the cmd_id. 130 | 131 | Note that if a Tello is marked as flight_completed, it will return -1 as its cmd_id. These are not 132 | added to the list returned here, so can effectively be ignored by calling functions. 133 | 134 | :param command: The Tello SDK string (e.g. 'forward 50' or 'battery?') to send to the Tello(s). 135 | :param command_type: Either 'Control', 'Set' or 'Read' - corresponding to the Tello SDK documentation. 136 | :param tello_num: Either 'All' or a Tello number (1,2,...) 137 | :param on_error: A different Tello SDK string to be sent if command returns an error. 138 | :return: A list of tuples in the form [(tello_num, cmd_id),...]. 139 | """ 140 | # Determine which Tellos to use, and add the command to the appropriate Tello's queue. 141 | cmd_ids = [] 142 | if tello_num == 'All': 143 | for tello in self.tellos: 144 | # If command is for all tellos, send to each and save the cmd_id in a list 145 | cmd_id = tello.add_to_command_queue(command, command_type, on_error) 146 | if cmd_id != -1: 147 | cmd_ids.append((tello.num, cmd_id)) 148 | else: 149 | tello = self.get_tello(num=tello_num) 150 | cmd_id = tello.add_to_command_queue(command, command_type, on_error) 151 | if cmd_id != -1: 152 | cmd_ids.append((tello.num, cmd_id)) 153 | return cmd_ids 154 | 155 | def wait_sync(self): 156 | """ Used to pause the main thread whilst all Tellos catch up, to bring all Tellos into sync. 157 | 158 | Simply checks with each Tello object that each individually has fully processed its queue and responses. 159 | The wait_until_idle command is a blocking function, so won't return until ready. 160 | """ 161 | for tello in self.tellos: 162 | tello.wait_until_idle() 163 | 164 | def get_tello(self, num): 165 | """ Shortcut function to return a specific Tello instance, based on its number. 166 | 167 | :param num: Tello number, as an integer (e.g. 1,2,...) 168 | :return: Tello object 169 | """ 170 | for tello in self.tellos: 171 | if tello.num == num: 172 | return tello 173 | raise RuntimeError('Tello not found!') 174 | 175 | def close_connections(self): 176 | """ Close all comms - to tidy up before exiting """ 177 | self.terminate_comms = True 178 | self.control_socket.close() 179 | self.status_socket.close() 180 | 181 | # 182 | # PRIVATE HELPER METHODS 183 | # 184 | 185 | @staticmethod 186 | def _get_subnets(): 187 | """ Get the local subnet and server IP address """ 188 | subnets = [] 189 | addr_list = [] 190 | ifaces = netifaces.interfaces() 191 | for this_iface in ifaces: 192 | addrs = netifaces.ifaddresses(this_iface) 193 | 194 | if socket.AF_INET not in addrs: 195 | continue 196 | 197 | # Get IPv4 info 198 | ip_info = addrs[socket.AF_INET][0] 199 | address = ip_info['addr'] 200 | netmask = ip_info['netmask'] 201 | 202 | # Avoid searching when on very large subnets 203 | if netmask != '255.255.255.0': 204 | continue 205 | 206 | # Create IP object and get the network details 207 | # Note CIDR is a networking term, describing the IP/subnet address format 208 | cidr = netaddr.IPNetwork('%s/%s' % (address, netmask)) 209 | network = cidr.network 210 | subnets.append((network, netmask)) 211 | addr_list.append(address) 212 | return subnets, addr_list 213 | 214 | def _get_tello(self, ip): 215 | """ Private function to return the Tello object with the matching IP address. 216 | 217 | :param ip: IP address of the requested Tello object, as a string e.g. '123.45.678.90' 218 | :return: Tello object 219 | """ 220 | for tello in self.tellos: 221 | if tello.ip == ip: 222 | return tello 223 | raise RuntimeError('Tello not found!') 224 | 225 | def _send_command(self, tello, cmd_id, command, command_type, on_error, timeout=10): 226 | """ Actually send a command to the Tello at specified IP address, recording details in the Tello's log. 227 | 228 | :param tello: The Tello object for which we're sending the command 229 | :param cmd_id: Corresponds to the id first given when in the Tello's queue, to be transferred to its log. 230 | :param command: The actual command from Tello SDK, e.g. 'battery?', 'forward 50', etc... 231 | :param command_type: Either 'Control', 'Set' or 'Read' - corresponding to the Tello SDK documentation. 232 | :param on_error: A different Tello SDK string to be sent if command returns an error. 233 | """ 234 | 235 | # Add the command to the Tello's log first 236 | log_entry = tello.add_to_log(cmd_id, command, command_type, on_error) 237 | 238 | # Then send the command 239 | self.control_socket.sendto(command.encode(), (tello.ip, self.control_port)) 240 | print('[Command %s]Sent cmd: %s' % (tello.ip, command)) 241 | 242 | # Wait until a response has been received, and handle timeout 243 | time_sent = time.time() 244 | while log_entry.response is None: 245 | now = time.time() 246 | if now - time_sent > timeout: 247 | print('[Command %s]Failed to send: %s' % (tello.ip, command)) 248 | log_entry.success = False 249 | log_entry.response = '' 250 | if log_entry.on_error is not None: 251 | tello.add_to_command_queue(log_entry.on_error, log_entry.command_type, None) 252 | print('[Command %s]Queuing alternative cmd: %s' % (tello.ip, log_entry.on_error)) 253 | return 254 | # Sleep briefly at the end of each loop, to prevent excessive CPU usage 255 | time.sleep(0.01) 256 | 257 | # 258 | # THREADS 259 | # 260 | 261 | def _command_handler(self, tello): 262 | """ Run Command Handler as a separate thread for each Tello, to manage the queue of commands. 263 | 264 | This runs as a separate thread so that applications can instantly add commands to multiple queues 265 | simultaneously, and then each of these threads (one per Tello) can all actually send the command 266 | together. The send_command function called from here is a blocking function, which doesn't return 267 | until the response has been received or the command exceeds its timeout. 268 | 269 | :param tello: The Tello object with which the command_handler should be associated. 270 | """ 271 | while True: 272 | # If nothing in the queue, just keep looping 273 | while not tello.command_queue: 274 | time.sleep(0.01) 275 | # Pop command off the Tello's queue, then send the command. 276 | # Note as part of send_command the same details will be added back into Tello's log. 277 | command = tello.command_queue.pop(0) 278 | self._send_command(tello, command.cmd_id, command.command, command.command_type, command.on_error) 279 | 280 | def _receive_thread(self): 281 | """ Listen continually to responses from the Tello - should run in its own thread. 282 | 283 | This method includes capturing and saving each Tello the first time it responds. 284 | If it is a known Tello, the response will be matched against the Tello's log, always recording the response 285 | against the last log entry as commands sent to each Tello are strictly sequential. 286 | Responses are also tested for success or failure, and if relevant an alternative command may be sent 287 | immediately on error. 288 | """ 289 | 290 | while not self.terminate_comms: 291 | try: 292 | # Get responses from all Tellos - this line blocks until a message is received - and reformat values 293 | response, ip = self.control_socket.recvfrom(1024) 294 | response = response.decode().strip() 295 | ip = str(ip[0]) 296 | 297 | # Capture Tellos when they respond for the first time 298 | if response.lower() == 'ok' and ip not in [tello.ip for tello in self.tellos]: 299 | print('[Tello Search]Found Tello on IP %s' % ip) 300 | self.tellos.append(Tello(ip)) 301 | continue 302 | 303 | # Get the current log entry for this Tello 304 | tello = self._get_tello(ip) 305 | log_entry = tello.log_entry() 306 | 307 | # Determine if the response was ok / error (or reading a value) 308 | send_on_error = False 309 | if log_entry.command_type in ['Control', 'Set']: 310 | if response == 'ok': 311 | log_entry.success = True 312 | else: 313 | log_entry.success = False 314 | if log_entry.on_error is not None: 315 | # If this command wasn't successful, and there's an on_error entry, flag to send it later. 316 | send_on_error = True 317 | elif log_entry.command_type == 'Read': 318 | # Assume Read commands are always successful... not aware they can return anything else!? 319 | log_entry.success = True 320 | else: 321 | print('[Response %s]Invalid command_type: %s' % (ip, log_entry.command_type)) 322 | # Save .response *after* .success, as elsewhere we use .response as a check to move on - avoids race 323 | # conditions across the other running threads, which might otherwise try to use .success before saved. 324 | log_entry.response = response 325 | print('[Response %s]Received: %s' % (ip, response)) 326 | # If required, queue the alternative command - assume same command type as the original. 327 | if send_on_error: 328 | tello.add_to_command_queue(log_entry.on_error, log_entry.command_type, None) 329 | print('[Command %s]Queuing alternative cmd: %s' % (ip, log_entry.on_error)) 330 | 331 | except socket.error as exc: 332 | if not self.terminate_comms: 333 | # Report socket errors, but only if we've not told it to terminate_comms. 334 | print('[Socket Error]Exception socket.error : %s' % exc) 335 | 336 | def _status_thread(self): 337 | """ Listen continually to status from the Tellos - should run in its own thread. 338 | 339 | Listens for status messages from each Tello, and saves them in the Tello object as they arrive. 340 | """ 341 | 342 | while not self.terminate_comms: 343 | try: 344 | response, ip = self.status_socket.recvfrom(1024) 345 | response = response.decode() 346 | if response == 'ok': 347 | continue 348 | ip = ''.join(str(ip[0])) 349 | tello = self._get_tello(ip) 350 | tello.status.clear() 351 | status_parts = response.split(';') 352 | for status_part in status_parts: 353 | key_value = status_part.split(':') 354 | if len(key_value) == 2: 355 | tello.status[key_value[0]] = key_value[1] 356 | 357 | except socket.error as exc: 358 | if not self.terminate_comms: 359 | # Report socket errors, but only if we've not told it to terminate_comms. 360 | print('[Socket Error]Exception socket.error : %s' % exc) 361 | -------------------------------------------------------------------------------- /fly_tello.py: -------------------------------------------------------------------------------- 1 | import time 2 | import threading 3 | from typing import Union, Optional 4 | from contextlib import contextmanager 5 | from comms_manager import CommsManager 6 | 7 | 8 | class FlyTello: 9 | """ Abstract class providing a simpler, user-friendly interface to CommsManager and Tello classes. 10 | 11 | FlyTello is dependent on CommsManager, which itself uses Tello and TelloCommand. 12 | 13 | FlyTello is intended to be used as a Context Manager, i.e. to be initialised using a "with" statement, e.g.: 14 | with FlyTello([sn1, sn2]) as fly: 15 | fly.takeoff() 16 | """ 17 | 18 | # 19 | # CLASS INITIALISATION AND CONTEXT HANDLER 20 | # 21 | 22 | def __init__(self, tello_sn_list: list, get_status=False, first_ip: int=1, last_ip: int=254): 23 | """ Initiate FlyTello, starting up CommsManager, finding and initialising our Tellos, and reporting battery. 24 | 25 | :param tello_sn_list: List of serial numbers, in the order we want to number the Tellos. 26 | :param first_ip: Optionally, we can specify a smaller range of IP addresses to speed up the search. 27 | :param last_ip: Optionally, we can specify a smaller range of IP addresses to speed up the search. 28 | """ 29 | self.tello_mgr = CommsManager() 30 | self.tello_mgr.init_tellos(sn_list=tello_sn_list, get_status=get_status, first_ip=first_ip, last_ip=last_ip) 31 | self.tello_mgr.queue_command('battery?', 'Read', 'All') 32 | self.individual_behaviour_threads = [] 33 | self.in_sync_these = False 34 | 35 | def __enter__(self): 36 | """ (ContextManager) Called when FlyTello is initiated using a with statement. """ 37 | return self 38 | 39 | def __exit__(self, exc_type, exc_val, exc_tb): 40 | """ (ContextManager) Tidies up when FlyTello leaves the scope of its with statement. """ 41 | if exc_type is not None: 42 | # If leaving after an Exception, ensure all Tellos have landed... 43 | self.tello_mgr.queue_command('land', 'Control', 'All') 44 | print('[Exception Occurred]All Tellos Landing...') 45 | else: 46 | pass 47 | # In all cases, wait until all commands have been sent and responses received before closing comms and exiting. 48 | self.tello_mgr.wait_sync() 49 | self.tello_mgr.close_connections() 50 | 51 | # 52 | # TELLO SDK V2.0 COMMANDS: CONTROL 53 | # 54 | # Control commands perform validation of input parameters. tello_num can be an individual (1,2,...) or 'All'. 55 | # If sync is True, will wait until all Tellos are ready before executing command. 56 | # Note sync is ignored (i.e. False) if tello_num is 'All', or if is called within a sync_these 'with' block. 57 | # 58 | 59 | def takeoff(self, tello: Union[int, str]='All', sync: bool=True) -> None: 60 | """ Auto takeoff, ascends to ~50cm above the floor. """ 61 | self._command('takeoff', 'Control', tello, sync) 62 | 63 | def land(self, tello: Union[int, str]='All', sync: bool=True) -> None: 64 | """ Auto landing """ 65 | self._command('land', 'Control', tello, sync) 66 | 67 | def stop(self, tello: Union[int, str]='All') -> None: 68 | """ Stop Tello wherever it is, even if mid-manoeuvre. """ 69 | self._command('stop', 'Control', tello, sync=False) 70 | 71 | def emergency(self, tello: Union[int, str]='All') -> None: 72 | """ Immediately kill power to the Tello's motors. """ 73 | self._command('emergency', 'Control', tello, sync=False) 74 | 75 | def up(self, dist: int, tello: Union[int, str]='All', sync: bool=True) -> None: 76 | """ Move up by dist (in cm) """ 77 | self._command_with_value('up', 'Control', dist, 20, 500, 'cm', tello, sync) 78 | 79 | def down(self, dist: int, tello: Union[int, str]='All', sync: bool=True) -> None: 80 | """ Move down by dist (in cm) """ 81 | self._command_with_value('down', 'Control', dist, 20, 500, 'cm', tello, sync) 82 | 83 | def left(self, dist: int, tello: Union[int, str]='All', sync: bool=True) -> None: 84 | """ Move left by dist (in cm) """ 85 | self._command_with_value('left', 'Control', dist, 20, 500, 'cm', tello, sync) 86 | 87 | def right(self, dist: int, tello: Union[int, str]='All', sync: bool=True) -> None: 88 | """ Move right by dist (in cm) """ 89 | self._command_with_value('right', 'Control', dist, 20, 500, 'cm', tello, sync) 90 | 91 | def forward(self, dist: int, tello: Union[int, str]='All', sync: bool=True) -> None: 92 | """ Move forward by dist (in cm) """ 93 | self._command_with_value('forward', 'Control', dist, 20, 500, 'cm', tello, sync) 94 | 95 | def back(self, dist: int, tello: Union[int, str]='All', sync: bool=True) -> None: 96 | """ Move back by dist (in cm) """ 97 | self._command_with_value('back', 'Control', dist, 20, 500, 'cm', tello, sync) 98 | 99 | def rotate_cw(self, angle: int, tello: Union[int, str]='All', sync: bool=True) -> None: 100 | """ Rotate clockwise (turn right) by angle (in degrees) """ 101 | self._command_with_value('cw', 'Control', angle, 1, 360, 'degrees', tello, sync) 102 | 103 | def rotate_ccw(self, angle: int, tello: Union[int, str]='All', sync: bool=True) -> None: 104 | """ Rotate anti-clockwise (turn left) by angle (in degrees) """ 105 | self._command_with_value('ccw', 'Control', angle, 1, 360, 'degrees', tello, sync) 106 | 107 | def flip(self, direction: str, tello: Union[int, str]='All', sync: bool=True) -> None: 108 | """ Perform a flip in the specified direction (left/right/forward/back) - will jump ~30cm in that direction. 109 | 110 | Note that Tello is unable to flip if battery is less than 50%! 111 | """ 112 | # TODO: Add an on_error command, which moves the Tello in the direction of the flip should the flip fail, e.g. 113 | # TODO: if the battery is low. Will ensure Tello is still in the expected position afterwards. 114 | # Convert left/right/forward/back direction inputs into the single letters (l/r/f/b) used by the Tello SDK. 115 | dir_dict = {'left': 'l', 'right': 'r', 'forward': 'f', 'back': 'b'} 116 | self._command_with_options('flip', 'Control', dir_dict[direction], ['l', 'r', 'f', 'b'], tello, sync) 117 | 118 | def straight(self, x: int, y: int, z: int, speed: int, tello: Union[int, str]='All', sync: bool=True) -> None: 119 | """ Fly straight to the coordinates specified, relative to the current position. 120 | 121 | :param x: x offset (+ forward, - back) in cm 122 | :param y: y offset (+ left, - right) in cm 123 | :param z: z offset (+ up, - down) in cm 124 | :param speed: Speed (in range 10-100cm/s) 125 | :param tello: The number of an individual Tello (1,2,...), or 'All'. 126 | :param sync: If True, will wait until all Tellos are ready before executing the command. 127 | """ 128 | self._control_multi(command='go', 129 | val_params=[(x, -500, 500, 'x'), 130 | (y, -500, 500, 'y'), 131 | (z, -500, 500, 'z'), 132 | (speed, 10, 100, 'speed')], 133 | opt_params=[], tello_num=tello, sync=sync) 134 | 135 | def curve(self, x1: int, y1: int, z1: int, x2: int, y2: int, z2: int, speed: int, 136 | tello: Union[int, str]='All', sync: bool=True) -> None: 137 | """ Fly a curve from current position, passing through mid point on way to end point (relative to current pos). 138 | 139 | The curve will be defined as an arc which passes through the three points (current, mid and end). The arc 140 | must have a radius between 50-1000cm (0.5-10m), otherwise the Tello will not move. Note that validation 141 | does *not* check the curve radius. 142 | 143 | :param x1: x offset of mid point of the curve (+ forward, - back) in cm 144 | :param y1: y offset of mid point of the curve (+ left, - right) in cm 145 | :param z1: z offset of mid point of the curve (+ up, - down) in cm 146 | :param x2: x offset of end point of the curve (+ forward, - back) in cm 147 | :param y2: y offset of end point of the curve (+ left, - right) in cm 148 | :param z2: z offset of end point of the curve (+ up, - down) in cm 149 | :param speed: Speed (in range 10-60cm/s) *** Note lower max speed of 60cm/s in curves *** 150 | :param tello: The number of an individual Tello (1,2,...), or 'All'. 151 | :param sync: If True, will wait until all Tellos are ready before executing the command. 152 | """ 153 | # TODO: Add an on_error command, which still moves the Tello to its destination should the curve fail, e.g. 154 | # TODO: if the curve radius is invalid. Will ensure Tello is still in the expected position afterwards. 155 | self._control_multi(command='curve', 156 | val_params=[(x1, -500, 500, 'x1'), 157 | (y1, -500, 500, 'y1'), 158 | (z1, -500, 500, 'z1'), 159 | (x2, -500, 500, 'x2'), 160 | (y2, -500, 500, 'y2'), 161 | (z2, -500, 500, 'z2'), 162 | (speed, 10, 60, 'speed')], 163 | opt_params=[], tello_num=tello, sync=sync) 164 | 165 | def straight_from_pad(self, x: int, y: int, z: int, speed: int, pad: str, 166 | tello: Union[int, str]='All', sync: bool=True) -> None: 167 | """ Fly straight to the coordinates specified, relative to the orientation of the mission pad. 168 | 169 | If the mission pad cannot be found, the Tello will not move, except to go to the height (z) above the pad. 170 | The Tello will always move to a position relative to the pad itself; not relative to the Tello's current 171 | position. This means that even if a Tello is slightly offset from the pad, it will always fly to the 172 | same location relative to the pad, i.e. helps to realign the Tello's location from that reference point. 173 | 174 | :param x: x offset from pad (+ forward, - back) in cm 175 | :param y: y offset from pad (+ left, - right) in cm 176 | :param z: z offset from pad (+ up, - down) in cm 177 | :param speed: Speed (in range 10-100cm/s) 178 | :param pad: ID of the mission pad to search for, e.g. 'm1'-'m8', 'm-1' (random pad), or 'm-2' (nearest pad). 179 | :param tello: The number of an individual Tello (1,2,...), or 'All'. 180 | :param sync: If True, will wait until all Tellos are ready before executing the command. 181 | """ 182 | self._control_multi(command='go', 183 | val_params=[(x, -500, 500, 'x'), 184 | (y, -500, 500, 'y'), 185 | (z, -500, 500, 'z'), 186 | (speed, 10, 100, 'speed')], 187 | opt_params=[(pad, ['m1', 'm2', 'm3', 'm4', 'm5', 188 | 'm6', 'm7', 'm8', 'm-1', 'm-2'], 'mid')], 189 | tello_num=tello, sync=sync) 190 | 191 | def curve_from_pad(self, x1: int, y1: int, z1: int, x2: int, y2: int, z2: int, speed: int, pad: str, 192 | tello: Union[int, str]='All', sync: bool=True) -> None: 193 | """ Fly a curve from current position, passing through mid point on way to end point (relative to mission pad). 194 | 195 | If the mission pad cannot be found, the Tello will not move, except to go to the height (z) above the pad. 196 | The curve will be defined as an arc which passes through three points - directly above pad, mid, and end. 197 | The arc must have a radius between 50-1000cm (0.5-10m), otherwise the Tello will not move. Because the 198 | position is relative to the pad, rather than the Tello itself, the curve radius can change depending on how 199 | near to the pad the Tello starts. Note that validation does *not* check the curve radius. 200 | 201 | :param x1: x offset from pad of mid point of the curve (+ forward, - back) in cm 202 | :param y1: y offset from pad of mid point of the curve (+ left, - right) in cm 203 | :param z1: z offset from pad of mid point of the curve (+ up, - down) in cm 204 | :param x2: x offset from pad of end point of the curve (+ forward, - back) in cm 205 | :param y2: y offset from pad of end point of the curve (+ left, - right) in cm 206 | :param z2: z offset from pad of end point of the curve (+ up, - down) in cm 207 | :param speed: Speed (in range 10-60cm/s) *** Note lower max speed of 60cm/s in curves *** 208 | :param pad: ID of the mission pad to search for, e.g. 'm1'-'m8', 'm-1' (random pad), or 'm-2' (nearest pad). 209 | :param tello: The number of an individual Tello (1,2,...), or 'All'. 210 | :param sync: If True, will wait until all Tellos are ready before executing the command. 211 | """ 212 | # TODO: Add an on_error command, which still moves the Tello to its destination should the curve fail, e.g. 213 | # TODO: if the curve radius is invalid. Will ensure Tello is still in the expected position afterwards. 214 | self._control_multi(command='curve', 215 | val_params=[(x1, -500, 500, 'x1'), 216 | (y1, -500, 500, 'y1'), 217 | (z1, -500, 500, 'z1'), 218 | (x2, -500, 500, 'x2'), 219 | (y2, -500, 500, 'y2'), 220 | (z2, -500, 500, 'z2'), 221 | (speed, 10, 60, 'speed')], 222 | opt_params=[(pad, ['m1', 'm2', 'm3', 'm4', 'm5', 223 | 'm6', 'm7', 'm8', 'm-1', 'm-2'], 'mid')], 224 | tello_num=tello, sync=sync) 225 | 226 | def jump_between_pads(self, x: int, y: int, z: int, speed: int, yaw: int, pad1: str, pad2: str, 227 | tello: Union[int, str]='All', sync: bool=True) -> None: 228 | """ Fly straight from pad1 to the coordinates specified (relative to pad1), then find pad2 at the end point. 229 | 230 | If the first mission pad cannot be found, the Tello will not move, except to go to the height (z) above the 231 | first pad. If the second mission pad cannot be found, the Tello will have moved to the point relative to 232 | pad1, but will return an error. 233 | 234 | :param x: x offset from pad1 (+ forward, - back) in cm 235 | :param y: y offset from pad1 (+ left, - right) in cm 236 | :param z: z offset from pad1 (+ up, - down) in cm 237 | :param speed: Speed (in range 10-100cm/s) 238 | :param yaw: Angle to rotate to, relative to the mission pad's orientation (direction that rocket points) 239 | :param pad1: ID of the mission pad at start, e.g. 'm1'-'m8', 'm-1' (random pad), or 'm-2' (nearest pad). 240 | :param pad2: ID of the mission pad at end, e.g. 'm1'-'m8', 'm-1' (random pad), or 'm-2' (nearest pad). 241 | :param tello: The number of an individual Tello (1,2,...), or 'All'. 242 | :param sync: If True, will wait until all Tellos are ready before executing the command. 243 | """ 244 | self._control_multi(command='jump', 245 | val_params=[(x, -500, 500, 'x'), 246 | (y, -500, 500, 'y'), 247 | (z, -500, 500, 'z'), 248 | (speed, 10, 100, 'speed'), 249 | (yaw, 0, 360, 'yaw')], 250 | opt_params=[(pad1, ['m1', 'm2', 'm3', 'm4', 'm5', 251 | 'm6', 'm7', 'm8', 'm-1', 'm-2'], 'mid1'), 252 | (pad2, ['m1', 'm2', 'm3', 'm4', 'm5', 253 | 'm6', 'm7', 'm8', 'm-1', 'm-2'], 'mid2')], 254 | tello_num=tello, sync=sync) 255 | 256 | # 257 | # TELLO SDK V2.0 COMMANDS: SET 258 | # 259 | 260 | def set_speed(self, speed: int, tello: Union[int, str]='All', sync: bool=False) -> None: 261 | """ Set 'normal' max speed for the Tello, for e.g. 'forward', 'back', etc commands. """ 262 | self._command_with_value('speed', 'Set', speed, 10, 100, 'cm/s', tello, sync) 263 | 264 | def set_rc(self, left_right: int, forward_back: int, up_down: int, yaw: int, 265 | tello: Union[int, str]='All', sync: bool=False) -> None: 266 | """ Simulate remote controller commands, with range of -100 to +100 on each axis. """ 267 | self._control_multi(command='rc', 268 | val_params=[(left_right, -100, 100, 'left_right'), 269 | (forward_back, -100, 100, 'forward_back'), 270 | (up_down, -100, 100, 'up_down'), 271 | (yaw, -100, 100, 'yaw')], 272 | opt_params=[], tello_num=tello, sync=sync) 273 | 274 | def set_own_wifi(self, ssid: str, password: str, tello: int, sync: bool=False) -> None: 275 | """ Set the Tello's own WiFi built-in hotspot to use the specified name (ssid) and password. """ 276 | self._command('wifi %s %s' % (ssid, password), 'Set', tello, sync) 277 | 278 | def pad_detection_on(self, tello: Union[int, str]='All', sync: bool=False) -> None: 279 | """ Turn on mission pad detection - must be set before setting direction or using pads. """ 280 | self._command('mon', 'Set', tello, sync) 281 | 282 | def pad_detection_off(self, tello: Union[int, str]='All', sync: bool=False) -> None: 283 | """ Turn off mission pad detection - commands using mid will not work if this is off. """ 284 | self._command('moff', 'Set', tello, sync) 285 | 286 | def set_pad_detection(self, direction: str, tello: Union[int, str]='All', sync: bool=False) -> None: 287 | """ Set the direction of mission pad detection. Must be done before mission pads are used. 288 | 289 | :param direction: Either 'downward', 'forward', or 'both'. 290 | :param tello: The number of an individual Tello (1,2,...), or 'All'. 291 | :param sync: If True, will wait until all Tellos are ready before executing the command. 292 | """ 293 | # Convert descriptions (downward/forward/both) into 0/1/2 required by Tello SDK. 294 | dir_dict = {'downward': 0, 'forward': 1, 'both': 2} 295 | self._command_with_options('mdirection', 'Set', dir_dict[direction], [0, 1, 2], tello, sync) 296 | 297 | def set_ap_wifi(self, ssid: str, password: str, tello: Union[int, str]='All', sync: bool=False) -> None: 298 | """ Tell the Tello to connect to an existing WiFi network using the supplied ssid and password. """ 299 | self._command('ap %s %s' % (ssid, password), 'Set', tello, sync) 300 | 301 | # 302 | # TELLO SDK V2.0 COMMANDS: READ 303 | # 304 | # Note arguments are common: tello can be an individual or 'All'; sync=True will wait until all are ready. 305 | # 306 | 307 | def get_speed(self, tello: Union[str, int]='All', sync: bool=False) -> None: 308 | """ Reads the speed setting of the Tello(s), in range 10-100. Reflects max speed, not actual current speed. """ 309 | self._command('speed?', 'Read', tello, sync) 310 | 311 | def get_battery(self, tello: Union[str, int]='All', sync: bool=False) -> None: 312 | """ Read the battery level of the Tello(s) """ 313 | self._command('battery?', 'Read', tello, sync) 314 | 315 | def get_time(self, tello: Union[str, int]='All', sync: bool=False) -> None: 316 | """ Should get current flight time of the Tello(s) """ 317 | self._command('time?', 'Read', tello, sync) 318 | 319 | def get_wifi(self, tello: Union[str, int]='All', sync: bool=False) -> None: 320 | """ Should get WiFi signal-to-noise ratio (SNR) - doesn't appear very reliable """ 321 | self._command('wifi?', 'Read', tello, sync) 322 | 323 | def get_sdk(self, tello: Union[str, int]='All', sync: bool=False) -> None: 324 | """ Read the SDK version of the Tello(s) """ 325 | self._command('sdk?', 'Read', tello, sync) 326 | 327 | def get_sn(self, tello: Union[str, int]='All', sync: bool=False) -> None: 328 | """ Read the Serial Number of the Tello(s) """ 329 | self._command('sn?', 'Read', tello, sync) 330 | 331 | # 332 | # TELLO SDK V2.0 EXTENDED & COMPOSITE COMMANDS 333 | # 334 | 335 | def reorient(self, height: int, pad: str, tello: Union[str, int]='All', sync: bool=False) -> None: 336 | """ Shortcut method to re-centre the Tello on the specified pad, helping maintain accurate positioning. 337 | 338 | Whilst the Tello has fairly good positioning stability by default, they can drift after flying for some 339 | time, or performing several manoeuvres. Using reorient gets back to a known position over a mission pad. 340 | 341 | :param height: Height above pad to fly to. 342 | :param pad: ID of the mission pad to reorient over, e.g. 'm1'-'m8', 'm-1', or 'm-2'. 343 | :param tello: The number of an individual Tello (1,2,...), or 'All'. 344 | :param sync: If True, will wait until all Tellos are ready before executing the command. 345 | """ 346 | self._control_multi(command='go', 347 | val_params=[(0, -500, 500, 'x'), 348 | (0, -500, 500, 'y'), 349 | (height, -500, 500, 'z'), 350 | (100, 10, 100, 'speed')], 351 | opt_params=[(pad, ['m1', 'm2', 'm3', 'm4', 'm5', 352 | 'm6', 'm7', 'm8', 'm-1', 'm-2'], 'mid')], 353 | tello_num=tello, 354 | sync=sync) 355 | 356 | def search_spiral(self, dist: int, spirals: int, height: int, speed: int, pad: str, tello: int) -> bool: 357 | """ Shortcut method to perform a spiral search around the starting point, returning True when found. 358 | 359 | Search follows a square pattern around, enlarging after each complete revolution. If pad is not found 360 | by the end of the last spiral, Tello will move back to its starting point and this method returns False. 361 | 362 | :param dist: Distance (in cm) from centre point to extend the spiral each time. 363 | :param spirals: Number of spirals to complete, moving out by 'dist' each time. Currently max 3. 364 | :param height: Height (cm) above ground at which to fly when searching. Detection range is 30-120cm. 365 | :param speed: Flight speed, in range 10-100cm/s. 366 | :param pad: ID of the mission pad to search for, e.g. 'm1'-'m8', 'm-1', or 'm-2'. 367 | :param tello: Number of an individual Tello, i.e. 1,2,.... Doesn't support 'All'. 368 | :return: Returns True when mission pad is found, and Tello is hovering directly above it. Otherwise False. 369 | """ 370 | pattern = [] 371 | if spirals >= 1: 372 | pattern.extend([(1, 1), 373 | (0, -2), 374 | (-2, 0), 375 | (0, 2)]) 376 | 377 | if spirals == 1: 378 | # Return to starting location 379 | pattern.extend([(1, -1)]) 380 | elif spirals >= 2: 381 | pattern.extend([(1, 1), 382 | (2, 0), 383 | (0, -2), 384 | (0, -2), 385 | (-2, 0), 386 | (-2, 0), 387 | (0, 2), 388 | (0, 2)]) 389 | 390 | if spirals == 2: 391 | # Return to starting location 392 | pattern.extend([(2, -2)]) 393 | elif spirals >= 3: 394 | pattern.extend([(1, 1), 395 | (2, 0), 396 | (2, 0), 397 | (0, -2), 398 | (0, -2), 399 | (0, -2), 400 | (-2, 0), 401 | (-2, 0), 402 | (-2, 0), 403 | (0, 2), 404 | (0, 2), 405 | (0, 2)]) 406 | 407 | if spirals >= 3: 408 | # Return to starting location 409 | pattern.extend([(3, -3)]) 410 | 411 | return self.search_pattern(pattern, dist, height, speed, pad, tello) 412 | 413 | def search_pattern(self, pattern: list, dist: int, height: int, speed: int, pad: str, tello: int) -> bool: 414 | """ Perform a search for a mission pad by following the supplied pattern, returning True when found. 415 | 416 | Pattern is usually clearest to define using relative integers, e.g. (0, 2), (-1, -1), etc. pattern_dist 417 | is therefore provided which is applied as a multiplier to all pattern values. If not needed then set to 1. 418 | 419 | :param pattern: A list of (x, y) tuples, defining the movement for each step of the search. 420 | :param dist: Multiplier for pattern values - if pattern has correct distances, set this to 1. 421 | :param height: Height (cm) above ground at which to fly when searching. Detection range is 30-120cm. 422 | :param speed: Flight speed, in range 10-100cm/s. 423 | :param pad: ID of the mission pad to search for, e.g. 'm1'-'m8', 'm-1', or 'm-2'. 424 | :param tello: Number of an individual Tello, i.e. 1,2,.... Doesn't support 'All'. 425 | :return: Returns True when mission pad is found, and Tello is hovering directly above it. Otherwise False. 426 | """ 427 | for x in range(0, len(pattern)): 428 | # Try to centre over the nearest mission pad 429 | cmd_ids = self.tello_mgr.queue_command('go 0 0 %d %d %s' % (height, speed, pad), 430 | 'Control', tello) 431 | for cmd_id in cmd_ids: 432 | cmd_log = self.tello_mgr.get_tello(cmd_id[0]).log_wait_response(cmd_id[1]) 433 | if cmd_log.success: 434 | return True 435 | else: 436 | # If not found i.e. Tello unable to orient itself over the Mission Pad, move to next position... 437 | self.tello_mgr.queue_command('go %d %d %d %d' % (pattern[x][0] * dist, 438 | pattern[x][1] * dist, 0, speed), 439 | 'Control', tello) 440 | return False 441 | 442 | # 443 | # MULTI-THREADING CONTROL FOR INDIVIDUAL BEHAVIOURS 444 | # 445 | 446 | @contextmanager 447 | def individual_behaviours(self): 448 | """ Context Manager, within which each Tello can have individual behaviours running in their own threads. 449 | 450 | By using this context manager, the individual threads will be monitored and the main thread will be blocked 451 | until all individual behaviours have completed. This allows individual behaviours to happen at some points 452 | in the flight control logic, but for Tellos to re-sync once they've completed their individual behaviour. 453 | """ 454 | # Clear list used to keep track of threads 455 | self.individual_behaviour_threads.clear() 456 | # Yield to allow threads to be created, inside the with statement 457 | yield 458 | # Block at the end of the with statement until all threads have completed 459 | for thread in self.individual_behaviour_threads: 460 | thread.join() 461 | 462 | def run_individual(self, behaviour, **kwargs): 463 | """ Start individual behaviour in its own thread, passing on keyword arguments to the behaviour function. 464 | 465 | Keeps main flight logic clear and simple, hiding threading capability within here. Should be run within 466 | the individual_behaviours() Context Manager to ensure threads are managed appropriately. 467 | 468 | :param behaviour: A (usually) custom-written function, to perform specific behaviour. 469 | :param kwargs: Any keyword arguments, i.e. arg_name1=value1, arg_name2=value2, etc, for the above function. 470 | """ 471 | thread = threading.Thread(target=behaviour, kwargs=kwargs) 472 | thread.start() 473 | self.individual_behaviour_threads.append(thread) 474 | 475 | # 476 | # SYNC AND TIMING METHODS 477 | # 478 | 479 | def wait_sync(self) -> None: 480 | """ Block execution until all Tellos are ready, i.e. no queued commands or pending responses. """ 481 | self.tello_mgr.wait_sync() 482 | 483 | @contextmanager 484 | def sync_these(self) -> None: 485 | """ Synchronise the commands within the "with" block, when this is used as a Context Manager. 486 | 487 | Provides a clearer way to layout code which will ensure all Tellos are ready before the code within this 488 | block will execute. Equivalent to calling wait_sync() prior to the same commands. 489 | 490 | sync_these() is intended to be used as a Context Manager, i.e. to initialise using a "with" statement, e.g.: 491 | with fly.sync_these(): 492 | fly.left(50, 1) 493 | fly.right(50, 2) 494 | Note that any sync=True setting on commands inside the block will be ignored! 495 | """ 496 | self.tello_mgr.wait_sync() 497 | self.in_sync_these = True 498 | yield 499 | self.in_sync_these = False 500 | 501 | @staticmethod 502 | def pause(secs: float) -> None: 503 | """ Pause for specified number of seconds, then continue. 504 | 505 | :param secs: Number of seconds to pause by. Can be integer or floating point i.e. 1, 0.1, etc 506 | """ 507 | time.sleep(secs) 508 | 509 | def flight_complete(self, tello: int) -> None: 510 | """ Mark the Tello's flight as complete - will ignore any subsequent control commands. 511 | 512 | :param tello: Tello Number - must be a single Tello, referenced by its number. Cannot be 'All'. 513 | """ 514 | self.tello_mgr.get_tello(tello).flight_complete = True 515 | 516 | # 517 | # STATUS MESSAGE PROCESSING 518 | # 519 | 520 | def print_status(self, tello: Union[int, str]='All', sync: bool=False) -> None: 521 | """ Print the entire Status Message to the Python Console, for the specified Tello(s). """ 522 | if sync and not self.in_sync_these: 523 | self.tello_mgr.wait_sync() 524 | if tello == 'All': 525 | for tello in self.tello_mgr.tellos: 526 | print('Tello %d Status: %s' % (tello.num, tello.status)) 527 | else: 528 | tello = self.tello_mgr.get_tello(num=tello) 529 | print('Tello %d Status: %s' % (tello.num, tello.status)) 530 | 531 | def get_status(self, key: str, tello: int, sync: bool=False) -> Optional[str]: 532 | """ Return the value of a specific key from an individual Tello """ 533 | if sync and not self.in_sync_these: 534 | self.tello_mgr.wait_sync() 535 | tello = self.tello_mgr.get_tello(num=tello) 536 | if key in tello.status: 537 | return tello.status[key] 538 | return None 539 | 540 | # 541 | # PRIVATE SHORTCUT METHODS 542 | # 543 | 544 | def _command(self, command, command_type, tello_num, sync): 545 | if sync and tello_num == 'All' and not self.in_sync_these: 546 | # TODO: Review whether tello_num=='All' should preclude wait_sync - might want to keep it! 547 | self.tello_mgr.wait_sync() 548 | self.tello_mgr.queue_command(command, command_type, tello_num) 549 | 550 | def _command_with_value(self, command, command_type, value, val_min, val_max, units, tello_num, sync): 551 | if sync and tello_num == 'All' and not self.in_sync_these: 552 | self.tello_mgr.wait_sync() 553 | if val_min <= value <= val_max: 554 | self.tello_mgr.queue_command('%s %d' % (command, value), command_type, tello_num) 555 | else: 556 | print('[FlyTello Error]%s %d - value must be %d-%d%s.' % (command, value, val_min, val_max, units)) 557 | 558 | def _command_with_options(self, command, command_type, option, validate_options, tello_num, sync): 559 | # TODO: Allow an on_error value to be passed through to queue_command 560 | if sync and tello_num == 'All' and not self.in_sync_these: 561 | self.tello_mgr.wait_sync() 562 | if option in validate_options: 563 | self.tello_mgr.queue_command('%s %s' % (command, option), command_type, tello_num) 564 | else: 565 | print('[FlyTello Error]%s %s - value must be in list %s.' % (command, option, validate_options)) 566 | 567 | def _control_multi(self, command: str, val_params: list, opt_params: list, tello_num: Union[int, str], sync: bool): 568 | """ Shortcut method to validate and send commands to Tello(s). 569 | 570 | Can have value parameters, option parameters, or both. These will always be applied in the order supplied, 571 | so must exactly match what is expected (as defined in the Tello SDK). Validation is not necessarily 572 | comprehensive, i.e. currently doesn't check for curve radius, or where x, y and z are all < 20. 573 | 574 | :param command: Base command in text format, from the Tello SDK. 575 | :param val_params: List of tuples, in the form: [(value, validate_min, validate_max, label), (...), ...] 576 | :param opt_params: List of tuples, in the form: [(value, validate_list, label), (...), ...] 577 | :param tello_num: Can be an individual Tello num (1,2,...), or 'All'. 578 | :param sync: Only valid if tello_num is 'All' - waits until all Tellos ready before sending the command. 579 | :return: Returns list of cmd_ids, from queue_command() - or nothing 580 | """ 581 | # TODO: Allow an on_error value to be passed through to queue_command 582 | if sync and tello_num == 'All' and not self.in_sync_these: 583 | self.tello_mgr.wait_sync() 584 | 585 | command_parameters = '' 586 | 587 | for val_param in val_params: 588 | if val_param[1] <= val_param[0] <= val_param[2]: 589 | command_parameters = '%s %d' % (command_parameters, val_param[0]) 590 | else: 591 | print('[FlyTello Error]%s - %s parameter out-of-range.' % (command, val_param[3])) 592 | return 593 | 594 | for opt_param in opt_params: 595 | if opt_param[0] in opt_param[1]: 596 | command_parameters = '%s %s' % (command_parameters, opt_param[0]) 597 | else: 598 | print('[FlyTello Error]%s - %s parameter not valid.' % (command, opt_param[2])) 599 | return 600 | 601 | self.tello_mgr.queue_command('%s%s' % (command, command_parameters), 'Control', tello_num) 602 | --------------------------------------------------------------------------------