commit
afb3b38039
@ -0,0 +1,2 @@
|
|||||||
|
from .tello import Tello, TelloException, BackgroundFrameRead
|
||||||
|
from .swarm import TelloSwarm
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
This file is based on a StackOverflow post by @301_Moved_Permanently.
|
||||||
|
See https://stackoverflow.com/a/50622643
|
||||||
|
|
||||||
|
The code was adapted to be able to wrap all methods of a class by simply
|
||||||
|
adding the decorator to the class itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import typing
|
||||||
|
from contextlib import suppress
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
|
def _is_unparameterized_special_typing(type_hint):
|
||||||
|
# Check for typing.Any, typing.Union, typing.ClassVar (without parameters)
|
||||||
|
if hasattr(typing, "_SpecialForm"):
|
||||||
|
return isinstance(type_hint, typing._SpecialForm)
|
||||||
|
elif hasattr(type_hint, "__origin__"):
|
||||||
|
return type_hint.__origin__ is None
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def enforce_types(target):
|
||||||
|
"""Class decorator adding type checks to all member functions
|
||||||
|
"""
|
||||||
|
def check_types(spec, *args, **kwargs):
|
||||||
|
parameters = dict(zip(spec.args, args))
|
||||||
|
parameters.update(kwargs)
|
||||||
|
for name, value in parameters.items():
|
||||||
|
with suppress(KeyError): # Assume un-annotated parameters can be any type
|
||||||
|
type_hint = spec.annotations[name]
|
||||||
|
if _is_unparameterized_special_typing(type_hint):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if hasattr(type_hint, "__origin__") and type_hint.__origin__ is not None:
|
||||||
|
actual_type = type_hint.__origin__
|
||||||
|
elif hasattr(type_hint, "__args__") and type_hint.__args__ is not None:
|
||||||
|
actual_type = type_hint.__args__
|
||||||
|
else:
|
||||||
|
actual_type = type_hint
|
||||||
|
|
||||||
|
if not isinstance(value, actual_type):
|
||||||
|
raise TypeError("Unexpected type for '{}' (expected {} but found {})"
|
||||||
|
.format(name, type_hint, type(value)))
|
||||||
|
|
||||||
|
def decorate(func):
|
||||||
|
spec = inspect.getfullargspec(func)
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
check_types(spec, *args, **kwargs)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
if inspect.isclass(target):
|
||||||
|
members = inspect.getmembers(target, predicate=inspect.isfunction)
|
||||||
|
for name, func in members:
|
||||||
|
setattr(target, name, decorate(func))
|
||||||
|
|
||||||
|
return target
|
||||||
|
else:
|
||||||
|
return decorate(target)
|
@ -0,0 +1,159 @@
|
|||||||
|
"""Library for controlling multiple DJI Ryze Tello drones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from threading import Thread, Barrier
|
||||||
|
from queue import Queue
|
||||||
|
from typing import List, Callable
|
||||||
|
|
||||||
|
from .tello import Tello, TelloException
|
||||||
|
from .enforce_types import enforce_types
|
||||||
|
|
||||||
|
|
||||||
|
@enforce_types
|
||||||
|
class TelloSwarm:
|
||||||
|
"""Swarm library for controlling multiple Tellos simultaneously
|
||||||
|
"""
|
||||||
|
|
||||||
|
tellos: List[Tello]
|
||||||
|
barrier: Barrier
|
||||||
|
funcBarier: Barrier
|
||||||
|
funcQueues: List[Queue]
|
||||||
|
threads: List[Thread]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromFile(path: str):
|
||||||
|
"""Create TelloSwarm from file. The file should contain one IP address per line.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
path: path to the file
|
||||||
|
"""
|
||||||
|
with open(path, 'r') as fd:
|
||||||
|
ips = fd.readlines()
|
||||||
|
|
||||||
|
return TelloSwarm.fromIps(ips)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromIps(ips: list):
|
||||||
|
"""Create TelloSwarm from a list of IP addresses.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
ips: list of IP Addresses
|
||||||
|
"""
|
||||||
|
if not ips:
|
||||||
|
raise TelloException("No ips provided")
|
||||||
|
|
||||||
|
tellos = []
|
||||||
|
for ip in ips:
|
||||||
|
tellos.append(Tello(ip.strip()))
|
||||||
|
|
||||||
|
return TelloSwarm(tellos)
|
||||||
|
|
||||||
|
def __init__(self, tellos: List[Tello]):
|
||||||
|
"""Initialize a TelloSwarm instance
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
tellos: list of [Tello][tello] instances
|
||||||
|
"""
|
||||||
|
self.tellos = tellos
|
||||||
|
self.barrier = Barrier(len(tellos))
|
||||||
|
self.funcBarrier = Barrier(len(tellos) + 1)
|
||||||
|
self.funcQueues = [Queue() for tello in tellos]
|
||||||
|
|
||||||
|
def worker(i):
|
||||||
|
queue = self.funcQueues[i]
|
||||||
|
tello = self.tellos[i]
|
||||||
|
|
||||||
|
while True:
|
||||||
|
func = queue.get()
|
||||||
|
self.funcBarrier.wait()
|
||||||
|
func(i, tello)
|
||||||
|
self.funcBarrier.wait()
|
||||||
|
|
||||||
|
self.threads = []
|
||||||
|
for i, _ in enumerate(tellos):
|
||||||
|
thread = Thread(target=worker, daemon=True, args=(i,))
|
||||||
|
thread.start()
|
||||||
|
self.threads.append(thread)
|
||||||
|
|
||||||
|
def sequential(self, func: Callable[[int, Tello], None]):
|
||||||
|
"""Call `func` for each tello sequentially. The function retrieves
|
||||||
|
two arguments: The index `i` of the current drone and `tello` the
|
||||||
|
current [Tello][tello] instance.
|
||||||
|
|
||||||
|
```python
|
||||||
|
swarm.parallel(lambda i, tello: tello.land())
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
for i, tello in enumerate(self.tellos):
|
||||||
|
func(i, tello)
|
||||||
|
|
||||||
|
def parallel(self, func: Callable[[int, Tello], None]):
|
||||||
|
"""Call `func` for each tello in parallel. The function retrieves
|
||||||
|
two arguments: The index `i` of the current drone and `tello` the
|
||||||
|
current [Tello][tello] instance.
|
||||||
|
|
||||||
|
You can use `swarm.sync()` for syncing between threads.
|
||||||
|
|
||||||
|
```python
|
||||||
|
swarm.parallel(lambda i, tello: tello.move_up(50 + i * 10))
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
for queue in self.funcQueues:
|
||||||
|
queue.put(func)
|
||||||
|
|
||||||
|
self.funcBarrier.wait()
|
||||||
|
self.funcBarrier.wait()
|
||||||
|
|
||||||
|
def sync(self, timeout: float = None):
|
||||||
|
"""Sync parallel tello threads. The code continues when all threads
|
||||||
|
have called `swarm.sync`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def doStuff(i, tello):
|
||||||
|
tello.move_up(50 + i * 10)
|
||||||
|
swarm.sync()
|
||||||
|
|
||||||
|
if i == 2:
|
||||||
|
tello.flip_back()
|
||||||
|
# make all other drones wait for one to complete its flip
|
||||||
|
swarm.sync()
|
||||||
|
|
||||||
|
swarm.parallel(doStuff)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
return self.barrier.wait(timeout)
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
"""Call a standard tello function in parallel on all tellos.
|
||||||
|
|
||||||
|
```python
|
||||||
|
swarm.command()
|
||||||
|
swarm.takeoff()
|
||||||
|
swarm.move_up(50)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
def callAll(*args, **kwargs):
|
||||||
|
self.parallel(lambda i, tello: getattr(tello, attr)(*args, **kwargs))
|
||||||
|
|
||||||
|
return callAll
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""Iterate over all drones in the swarm.
|
||||||
|
|
||||||
|
```python
|
||||||
|
for tello in swarm:
|
||||||
|
print(tello.get_battery())
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
return iter(self.tellos)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""Return the amount of tellos in the swarm
|
||||||
|
|
||||||
|
```python
|
||||||
|
print("Tello count: {}".format(len(swarm)))
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
return len(self.tellos)
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue