├── README.md ├── TelloSDK2.0UserGuide.pdf ├── Tello_flight_log_20201018_052457.db ├── fly_tello.py ├── jupyter-notebook ├── All_in_One.ipynb ├── Fly_Tello.ipynb ├── README.md └── Tello_Video.ipynb └── see_from_tello.py /README.md: -------------------------------------------------------------------------------- 1 | ## Python 3 version of [dji-sdk/Tello-Python](https://github.com/dji-sdk/Tello-Python) 2 | ### Summary 3 | * Written in Python 3 with minimum package dependency for video streaming and processing. (opencv-python and Flask) 4 | * Fly Tello and watch the video streaming from on-drone camera in a web browser. 5 | * Flight data is logged for later analysis. 6 | ### Usage 7 | * If you want keep your system python clean, try the virtual environment. 8 | ```shell 9 | sudo apt install python3-venv 10 | python3 -m venv testTello 11 | source testTello/bin/activate 12 | ``` 13 | 1. Connect your machine to the Tello via WIFI. (Don't worry about IP. Tello will assign one for your machine) 14 | 2. Run the code and fly Tello autonomously (it would take off, fly around and land by its own) 15 | ``` 16 | python3 fly_tello.py 17 | ``` 18 | * Install necessary libraries if you need image from Tello (opencv) and show it in your web browser (flask). 19 | ```shell 20 | pip install opencv-python Flask # That is it. No other dependent libraries 21 | ``` 22 | 3. Run the code and visit http://127.0.0.1:9999/stream.mjpg by using your browser (keep refresh the page if you have problem) 23 | ```shell 24 | python3 see_from_tello.py 25 | ``` 26 | ### Computer Vision 27 | * Tello will not setup a video streaming service for you to capture so you cannot use OpenCV directly. 28 | * If you worked with raspberry pi, you know its camera utility can set up a tcp server then you can use opencv to capture the h264 stream from it directly. 29 | * Tello is different. It does not setup a streaming server and you can not program it to do that. 30 | * You need to setup a video clip collector (a UDP service) then Tello will send video clips to your collector. 31 | * Tello is a client who sends out chunks of h264-encoded video stream to a udp server set up on your PC. It is up to you to collect those chunks and deal with it. 32 | * I don't know how to work with a locally buffered stream but I am familar with OpenCV. 33 | * In see_from_tello.py, I demonstrated a solution by setting up tcp and udp services to rebroadcast the buffered stream. 34 | * Tello sends the stream data my udp service then are fed to a tcp service. OpenCV can capture the tcp stream and do the real-time decoding frame by frame. 35 | * Tello --[h264-encoded chunks of frame]--> UDP server on my PC --[h264-encoded frame]--> TCP server on my PC --[h264-encoded stream]--> OpenCV --[image]--> Flask 36 | -------------------------------------------------------------------------------- /TelloSDK2.0UserGuide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xg590/Tello-Python/282cbcace21ddcc80a9034c4c8ae016eaf5dc898/TelloSDK2.0UserGuide.pdf -------------------------------------------------------------------------------- /Tello_flight_log_20201018_052457.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xg590/Tello-Python/282cbcace21ddcc80a9034c4c8ae016eaf5dc898/Tello_flight_log_20201018_052457.db -------------------------------------------------------------------------------- /fly_tello.py: -------------------------------------------------------------------------------- 1 | import time, queue, socket, sqlite3, datetime, threading 2 | 3 | class Tello: 4 | def __init__(self): 5 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 6 | self.socket.bind(('', 8889)) 7 | self.db_queue = queue.Queue() # cache flight data 8 | self.cmd_queue = queue.Queue() 9 | self.cmd_event = threading.Event() 10 | self.MAX_TIME_OUT = 15 # It must be longer than 10 sec, give time to "take off" command. 11 | self.MAX_RETRY = 2 12 | self.state = {} 13 | threading.Thread(target=self.flight_logger, kwargs={"debug":True}, daemon=True).start() 14 | threading.Thread(target=self.receiver , kwargs={"debug":True}, daemon=True).start() 15 | threading.Thread(target=self.sender , kwargs={"debug":True}, daemon=True).start() 16 | threading.Thread(target=self.update_state , daemon=True).start() 17 | 18 | def command(self, cmd): 19 | self.cmd_queue.put(cmd) 20 | 21 | def save_flight_data(self): 22 | self.db_queue.put('commit') 23 | 24 | def stop_flight_logger(self): 25 | self.db_queue.put('close') 26 | 27 | def flight_logger(self, debug=False): 28 | con = sqlite3.connect(f'Tello_flight_log_{datetime.datetime.fromtimestamp(time.time()).strftime("%Y%m%d_%H%M%S")}.db') 29 | cur = con.cursor() 30 | cur.execute('CREATE TABLE commands(timestamp REAL, command TEXT, who TEXT);') 31 | cur.execute('CREATE TABLE states(timestamp REAL, log TEXT );') 32 | if debug: print('Flight Data Recording Begins ~\n') 33 | while 1: 34 | operation = self.db_queue.get() 35 | if operation == 'commit': 36 | con.commit() 37 | if debug: print(f'Flight Data Saved @ {datetime.datetime.fromtimestamp(time.time()).strftime("%Y%m%d_%H%M%S")}~') 38 | elif operation == 'close': 39 | con.close() 40 | if debug: print(f'Flight Data Recording Ends @ {datetime.datetime.fromtimestamp(time.time()).strftime("%Y%m%d_%H%M%S")}~') 41 | break 42 | else: 43 | cur.execute(operation) 44 | 45 | def receiver(self, debug=False): 46 | while True: 47 | bytes_, address = self.socket.recvfrom(1024) 48 | if bytes_ == b'ok': 49 | self.cmd_event.set() # one command has been successfully executed. Begin new execution. 50 | else: 51 | if debug: print('[ Station ]:', bytes_) 52 | try: 53 | self.db_queue.put('INSERT INTO commands(timestamp, command, who) VALUES({}, "{}", "{}");'.format(time.time(), bytes_.decode(), "Tello")) 54 | except UnicodeDecodeError as e: 55 | if debug: print('Decoding Error that could be ignored~') 56 | 57 | def sender(self, debug=False): 58 | tello_address = ('192.168.10.1', 8889) 59 | self.cmd_event.set() # allow the first wait to proceed 60 | while True: 61 | self.cmd_event.wait() # block second queue.get() until an event is set from receiver or failure set 62 | self.cmd_event.clear() # block a timeout-enabled waiting 63 | cmd = self.cmd_queue.get() 64 | self.db_queue.put(f'INSERT INTO commands(timestamp, command, who) VALUES({time.time()}, "{cmd}", "Station");') 65 | self.socket.sendto(cmd.encode('utf-8'), tello_address) 66 | cmd_ok = False 67 | for i in range(self.MAX_RETRY): 68 | if self.cmd_event.wait(timeout=self.MAX_TIME_OUT): 69 | cmd_ok = True 70 | break 71 | else: 72 | if debug: print(f'Failed command: "{cmd}", Failure sequence: {i+1}.') 73 | self.socket.sendto(cmd.encode('utf-8'), tello_address) 74 | if cmd_ok: 75 | if debug: print(f'Success with "{cmd}".') 76 | else: 77 | self.cmd_event.set() # The failure set 78 | if debug: print(f'Stop retry: "{cmd}", Maximum re-tries: {self.MAX_RETRY}.') 79 | 80 | 81 | def update_state(self): 82 | UDP = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 83 | UDP.bind(('', 8890)) 84 | while True: 85 | bytes_, address = UDP.recvfrom(1024) 86 | str_ = bytes_.decode() 87 | self.db_queue.put('INSERT INTO states(timestamp, log) VALUES({},"{}");'.format(time.time(), str_)) 88 | state = str_.split(';') 89 | state.pop() 90 | self.state.update(dict([s.split(':') for s in state])) 91 | 92 | tello = Tello() 93 | tello.command('command') 94 | time.sleep(0.1) 95 | input("\nIt may take 10 seconds for Tello to take off. \nPress any key to continue~\n") 96 | input("Please press any key after the landing... \nNow press any key to take off~\n") 97 | tello.command('takeoff' ) 98 | tello.command('forward 50') 99 | tello.command('left 50' ) 100 | tello.command('back 50' ) 101 | tello.command('right 50' ) 102 | tello.command('land' ) 103 | input() 104 | print("Give Tello 3 seconds to save flight data") 105 | tello.save_flight_data() 106 | tello.stop_flight_logger() 107 | time.sleep(3) -------------------------------------------------------------------------------- /jupyter-notebook/All_in_One.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "ExecuteTime": { 8 | "end_time": "2020-10-23T05:24:15.630217Z", 9 | "start_time": "2020-10-23T05:24:15.605979Z" 10 | }, 11 | "code_folding": [ 12 | 2, 13 | 20, 14 | 38, 15 | 50, 16 | 77 17 | ] 18 | }, 19 | "outputs": [ 20 | { 21 | "name": "stdout", 22 | "output_type": "stream", 23 | "text": [ 24 | "Flight Data Recording Begins ~\n" 25 | ] 26 | } 27 | ], 28 | "source": [ 29 | "import time, queue, socket, sqlite3, datetime, threading \n", 30 | "\n", 31 | "class Tello:\n", 32 | " def __init__(self):\n", 33 | " self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) \n", 34 | " self.socket.bind(('', 8889)) \n", 35 | " self.db_queue = queue.Queue() # cache flight data\n", 36 | " self.cmd_queue = queue.Queue() \n", 37 | " self.cmd_event = threading.Event() \n", 38 | " self.MAX_TIME_OUT = 15 # It must be longer than 10 sec, give time to \"take off\" command. \n", 39 | " self.MAX_RETRY = 2\n", 40 | " self.state = {}\n", 41 | " threading.Thread(target=self.flight_logger, daemon=True).start() \n", 42 | " threading.Thread(target=self.receiver , daemon=True).start()\n", 43 | " threading.Thread(target=self.sender , daemon=True).start() \n", 44 | " threading.Thread(target=self.update_state , daemon=True).start() \n", 45 | " \n", 46 | " def command(self, cmd):\n", 47 | " self.cmd_queue.put(cmd) \n", 48 | " \n", 49 | " def flight_logger(self):\n", 50 | " con = sqlite3.connect(f'Tello_flight_log_{datetime.datetime.fromtimestamp(time.time()).strftime(\"%Y%m%d_%H%M%S\")}.db') \n", 51 | " cur = con.cursor() \n", 52 | " cur.execute('CREATE TABLE commands(timestamp REAL, command TEXT, who TEXT);')\n", 53 | " cur.execute('CREATE TABLE states(timestamp REAL, log TEXT );') \n", 54 | " print('Flight Data Recording Begins ~') \n", 55 | " while 1: \n", 56 | " operation = self.db_queue.get() \n", 57 | " if operation == 'commit': \n", 58 | " con.commit()\n", 59 | " print('Flight Data Saved ~') \n", 60 | " elif operation == 'close': \n", 61 | " con.close() \n", 62 | " print('Flight Data Recording Ends ~') \n", 63 | " break\n", 64 | " else: \n", 65 | " cur.execute(operation) \n", 66 | " \n", 67 | " def receiver(self): \n", 68 | " while True: \n", 69 | " bytes_, address = self.socket.recvfrom(1024) \n", 70 | " if bytes_ == b'ok': \n", 71 | " self.cmd_event.set() # one command has been successfully executed. Begin new execution. \n", 72 | " else:\n", 73 | " print('[ Station ]:', bytes_)\n", 74 | " try:\n", 75 | " self.db_queue.put('INSERT INTO commands(timestamp, command, who) VALUES({}, \"{}\", \"{}\");'.format(time.time(), bytes_.decode(), \"Tello\")) \n", 76 | " except UnicodeDecodeError as e:\n", 77 | " print('Decoding Error that could be ignored~')\n", 78 | " \n", 79 | " def sender(self, debug=True): \n", 80 | " tello_address = ('192.168.10.1', 8889)\n", 81 | " self.cmd_event.set() # allow the first wait to proceed \n", 82 | " while True:\n", 83 | " self.cmd_event.wait() # block second get until an event is set from receiver or failure set\n", 84 | " self.cmd_event.clear() # block a timeout-enabled waiting\n", 85 | " cmd = self.cmd_queue.get() \n", 86 | " self.db_queue.put(f'INSERT INTO commands(timestamp, command, who) VALUES({time.time()}, \"{cmd}\", \"Station\");') \n", 87 | " self.socket.sendto(cmd.encode('utf-8'), tello_address)\n", 88 | " cmd_ok = False\n", 89 | " for i in range(self.MAX_RETRY): \n", 90 | " if self.cmd_event.wait(timeout=self.MAX_TIME_OUT): \n", 91 | " cmd_ok = True\n", 92 | " break\n", 93 | " else:\n", 94 | " if debug: print(f'Failed command: \"{cmd}\", Failure sequence: {i+1}.')\n", 95 | " self.socket.sendto(cmd.encode('utf-8'), tello_address) \n", 96 | " if cmd_ok: \n", 97 | " print(f'Success with \"{cmd}\".') \n", 98 | " if cmd == 'land':\n", 99 | " self.db_queue.put('commit')\n", 100 | " self.db_queue.put('close') \n", 101 | " else:\n", 102 | " self.cmd_event.set() # The failure set\n", 103 | " if debug: print(f'Stop retry: \"{cmd}\", Maximum re-tries: {self.MAX_RETRY}.')\n", 104 | " \n", 105 | " \n", 106 | " def update_state(self):\n", 107 | " UDP = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) \n", 108 | " UDP.bind(('', 8890)) \n", 109 | " while True: \n", 110 | " bytes_, address = UDP.recvfrom(1024)\n", 111 | " str_ = bytes_.decode() \n", 112 | " self.db_queue.put('INSERT INTO states(timestamp, log) VALUES({},\"{}\");'.format(time.time(), str_)) \n", 113 | " state = str_.split(';')\n", 114 | " state.pop() \n", 115 | " self.state.update(dict([s.split(':') for s in state]))\n", 116 | " \n", 117 | "tello = Tello() " 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 2, 123 | "metadata": { 124 | "ExecuteTime": { 125 | "end_time": "2020-10-23T05:24:30.285951Z", 126 | "start_time": "2020-10-23T05:24:21.069196Z" 127 | }, 128 | "code_folding": [ 129 | 5 130 | ] 131 | }, 132 | "outputs": [ 133 | { 134 | "name": "stdout", 135 | "output_type": "stream", 136 | "text": [ 137 | "[ Station ]: b'\\xcc\\x18\\x01\\xb9\\x88V\\x00\\x8b\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00U\\x00\\x00&\\x10\\x00\\x06\\x00\\x00\\x00\\x00\\x00\\xa3\\xf0'\n", 138 | "Decoding Error that could be ignored~\n", 139 | "Success with \"command\".\n", 140 | "Success with \"streamon\".\n" 141 | ] 142 | } 143 | ], 144 | "source": [ 145 | "import socket\n", 146 | "import threading \n", 147 | "import cv2 \n", 148 | "import time\n", 149 | "\n", 150 | "class VIDEO: \n", 151 | " def __init__(self):\n", 152 | " tello.command('command')\n", 153 | " tello.command('streamon') \n", 154 | " time.sleep(3)\n", 155 | " self.void_frame = b'' \n", 156 | " self.h264_frame = self.void_frame\n", 157 | " self.jpeg_frame = self.void_frame\n", 158 | " self.frame_event = threading.Event() # tell transmitter that receiver has a new frame from tello ready \n", 159 | " self.stream_event = threading.Event() # tell opencv that transmitter has the stream ready. \n", 160 | " threading.Thread(target=self.video_receiver , daemon=True).start() \n", 161 | " threading.Thread(target=self.video_transmitter, daemon=True).start() \n", 162 | " time.sleep(3)\n", 163 | " threading.Thread(target=self.opencv , daemon=True).start() \n", 164 | " time.sleep(3)\n", 165 | " \n", 166 | " def video_receiver(self): # receive h264 stream from tello \n", 167 | " _receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # socket for receiving video stream (UDP) \n", 168 | " _receiver.bind(('', 11111)) # the udp port is fixed \n", 169 | " while True:\n", 170 | " frame = b'' \n", 171 | " while True:\n", 172 | " byte_, _ = _receiver.recvfrom(2048) \n", 173 | " frame += byte_\n", 174 | " if len(byte_) != 1460: # end of frame \n", 175 | " self.h264_frame = frame \n", 176 | " self.frame_event.set() # let the reading frame event happen\n", 177 | " self.frame_event.clear() # prevent it happen until next set\n", 178 | " break \n", 179 | " \n", 180 | " def video_transmitter(self): # feed h264 stream to opencv\n", 181 | " _transmitter = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # socket for transmitting stream (TCP) \n", 182 | " _transmitter.bind(('127.0.0.1', 12345)) # tcp port is up to us\n", 183 | " _transmitter.listen(0)\n", 184 | " while True: \n", 185 | " conn, address = _transmitter.accept() \n", 186 | " file_obj = conn.makefile('wb')\n", 187 | " stream_ready_flag = False \n", 188 | " while True: \n", 189 | " self.frame_event.wait() \n", 190 | " try:\n", 191 | " file_obj.write(self.h264_frame) \n", 192 | " except BrokenPipeError:\n", 193 | " print('[ Warning ] Tello returned nonsense!')\n", 194 | " print('[ Warning ] Please refresh stream after a while~\\n')\n", 195 | " break\n", 196 | " file_obj.flush() \n", 197 | "\n", 198 | " def opencv(self): \n", 199 | " while True:\n", 200 | " cap = cv2.VideoCapture(\"tcp://127.0.0.1:12345\") \n", 201 | " while(cap.isOpened()): \n", 202 | " ret, frame = cap.read() \n", 203 | " if not ret: \n", 204 | " print('[ Error ] Please check if your tello is off~')\n", 205 | " break \n", 206 | " ret, jpeg = cv2.imencode('.jpg', frame)\n", 207 | " self.jpeg_frame = jpeg.tobytes()\n", 208 | " cap.release()\n", 209 | " print('[ Warning ] OpenCV lost connection to transmitter!')\n", 210 | " print('[ Warning ] Try reconnection in 3 seconds~')\n", 211 | " time.sleep(3)\n", 212 | " \n", 213 | "\n", 214 | "video = VIDEO() " 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": 3, 220 | "metadata": { 221 | "ExecuteTime": { 222 | "end_time": "2020-10-23T05:24:32.714253Z", 223 | "start_time": "2020-10-23T05:24:32.545008Z" 224 | } 225 | }, 226 | "outputs": [ 227 | { 228 | "name": "stdout", 229 | "output_type": "stream", 230 | "text": [ 231 | "Click Here and see the video: http://127.0.0.1:9999/stream.mjpg\n", 232 | "\n", 233 | "\n", 234 | "\n", 235 | " * Serving Flask app \"__main__\" (lazy loading)\n", 236 | " * Environment: production\n", 237 | " WARNING: This is a development server. Do not use it in a production deployment.\n", 238 | " Use a production WSGI server instead.\n", 239 | " * Debug mode: off\n" 240 | ] 241 | }, 242 | { 243 | "name": "stderr", 244 | "output_type": "stream", 245 | "text": [ 246 | " * Running on http://127.0.0.1:9999/ (Press CTRL+C to quit)\n", 247 | "127.0.0.1 - - [23/Oct/2020 01:24:35] \"\u001b[37mGET /stream.mjpg HTTP/1.1\u001b[0m\" 200 -\n" 248 | ] 249 | } 250 | ], 251 | "source": [ 252 | "import flask \n", 253 | "\n", 254 | "app = flask.Flask(__name__)\n", 255 | "print('Click Here and see the video: http://127.0.0.1:9999/stream.mjpg\\n\\n\\n')\n", 256 | "fps=25\n", 257 | "interval = 1/fps\n", 258 | "@app.route(\"/stream.mjpg\") \n", 259 | "def mjpg1(): \n", 260 | " def generator(): \n", 261 | " while True: \n", 262 | " time.sleep(interval) # threading.condition is too shitty according to my test. no condition no lag. \n", 263 | " frame = video.jpeg_frame \n", 264 | " yield f'''--FRAME\\r\\nContent-Type: image/jpeg\\r\\nContent-Length: {len(frame)}\\r\\n\\r\\n'''.encode() \n", 265 | " yield frame\n", 266 | " r = flask.Response(response=generator(), status=200)\n", 267 | " r.headers.extend({'Age':0, 'Content-Type':'multipart/x-mixed-replace; boundary=FRAME',\n", 268 | " 'Pragma':'no-cache', 'Cache-Control':'no-cache, private',}) \n", 269 | " return r\n", 270 | "\n", 271 | "@app.route('/')\n", 272 | "def hello_world():\n", 273 | " return 'Hello, World!'\n", 274 | "\n", 275 | "def web():\n", 276 | " app.run('127.0.0.1', 9999)\n", 277 | "\n", 278 | "threading.Thread(target=web , daemon=True).start() " 279 | ] 280 | }, 281 | { 282 | "cell_type": "code", 283 | "execution_count": 6, 284 | "metadata": { 285 | "ExecuteTime": { 286 | "end_time": "2020-10-23T05:27:39.919443Z", 287 | "start_time": "2020-10-23T05:27:39.898375Z" 288 | } 289 | }, 290 | "outputs": [ 291 | { 292 | "name": "stdout", 293 | "output_type": "stream", 294 | "text": [ 295 | "It may take 10 seconds to take off.\n", 296 | "Success with \"takeoff\".\n", 297 | "Success with \"forward 50\".\n", 298 | "Success with \"cw 90\".\n", 299 | "Success with \"forward 50\".\n", 300 | "Success with \"cw 90\".\n", 301 | "Success with \"forward 50\".\n", 302 | "Success with \"cw 90\".\n", 303 | "Success with \"forward 50\".\n", 304 | "Success with \"cw 90\".\n", 305 | "Success with \"land\".\n", 306 | "[ Error ] Please check if your tello is off~\n", 307 | "[ Warning ] OpenCV lost connection to transmitter!\n", 308 | "[ Warning ] Try reconnection in 3 seconds~\n", 309 | "[ Warning ] OpenCV lost connection to transmitter!\n", 310 | "[ Warning ] Try reconnection in 3 seconds~\n", 311 | "[ Warning ] OpenCV lost connection to transmitter!\n", 312 | "[ Warning ] Try reconnection in 3 seconds~\n", 313 | "[ Warning ] OpenCV lost connection to transmitter!\n", 314 | "[ Warning ] Try reconnection in 3 seconds~\n", 315 | "[ Warning ] OpenCV lost connection to transmitter!\n", 316 | "[ Warning ] Try reconnection in 3 seconds~\n", 317 | "[ Warning ] OpenCV lost connection to transmitter!\n", 318 | "[ Warning ] Try reconnection in 3 seconds~\n" 319 | ] 320 | } 321 | ], 322 | "source": [ 323 | "print(\"It may take 10 seconds to take off.\") \n", 324 | "tello.command('takeoff') \n", 325 | "\n", 326 | "tello.command('forward 50') \n", 327 | "tello.command('cw 90') \n", 328 | "tello.command('forward 50') \n", 329 | "tello.command('cw 90') \n", 330 | "tello.command('forward 50') \n", 331 | "tello.command('cw 90') \n", 332 | "tello.command('forward 50') \n", 333 | "tello.command('cw 90') \n", 334 | "\n", 335 | "tello.command('land') " 336 | ] 337 | } 338 | ], 339 | "metadata": { 340 | "kernelspec": { 341 | "display_name": "Python 3", 342 | "language": "python", 343 | "name": "python3" 344 | }, 345 | "language_info": { 346 | "codemirror_mode": { 347 | "name": "ipython", 348 | "version": 3 349 | }, 350 | "file_extension": ".py", 351 | "mimetype": "text/x-python", 352 | "name": "python", 353 | "nbconvert_exporter": "python", 354 | "pygments_lexer": "ipython3", 355 | "version": "3.8.2" 356 | } 357 | }, 358 | "nbformat": 4, 359 | "nbformat_minor": 4 360 | } 361 | -------------------------------------------------------------------------------- /jupyter-notebook/Fly_Tello.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "ExecuteTime": { 8 | "end_time": "2020-10-18T09:24:57.811417Z", 9 | "start_time": "2020-10-18T09:24:57.777400Z" 10 | }, 11 | "code_folding": [ 12 | 17, 13 | 20 14 | ] 15 | }, 16 | "outputs": [ 17 | { 18 | "name": "stdout", 19 | "output_type": "stream", 20 | "text": [ 21 | "Flight Data Recording Begins ~\n" 22 | ] 23 | } 24 | ], 25 | "source": [ 26 | "import time, queue, socket, sqlite3, datetime, threading \n", 27 | "\n", 28 | "class Tello:\n", 29 | " def __init__(self):\n", 30 | " self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) \n", 31 | " self.socket.bind(('', 8889)) \n", 32 | " self.db_queue = queue.Queue() # cache flight data\n", 33 | " self.cmd_queue = queue.Queue() \n", 34 | " self.cmd_event = threading.Event() \n", 35 | " self.MAX_TIME_OUT = 15 # It must be longer than 10 sec, give time to \"take off\" command. \n", 36 | " self.MAX_RETRY = 2\n", 37 | " self.state = {}\n", 38 | " threading.Thread(target=self.flight_logger, daemon=True).start() \n", 39 | " threading.Thread(target=self.receiver , daemon=True).start()\n", 40 | " threading.Thread(target=self.sender , daemon=True).start() \n", 41 | " threading.Thread(target=self.update_state , daemon=True).start() \n", 42 | " \n", 43 | " def command(self, cmd):\n", 44 | " self.cmd_queue.put(cmd) \n", 45 | " \n", 46 | " def flight_logger(self):\n", 47 | " con = sqlite3.connect(f'Tello_flight_log_{datetime.datetime.fromtimestamp(time.time()).strftime(\"%Y%m%d_%H%M%S\")}.db') \n", 48 | " cur = con.cursor() \n", 49 | " cur.execute('CREATE TABLE commands(timestamp REAL, command TEXT, who TEXT);')\n", 50 | " cur.execute('CREATE TABLE states(timestamp REAL, log TEXT );') \n", 51 | " print('Flight Data Recording Begins ~') \n", 52 | " while 1: \n", 53 | " operation = self.db_queue.get() \n", 54 | " if operation == 'commit': \n", 55 | " con.commit()\n", 56 | " print('Flight Data Saved ~') \n", 57 | " elif operation == 'close': \n", 58 | " con.close() \n", 59 | " print('Flight Data Recording Ends ~') \n", 60 | " break\n", 61 | " else: \n", 62 | " cur.execute(operation) \n", 63 | " \n", 64 | " def receiver(self): \n", 65 | " while True: \n", 66 | " bytes_, address = self.socket.recvfrom(1024) \n", 67 | " if bytes_ == b'ok': \n", 68 | " self.cmd_event.set() # one command has been successfully executed. Begin new execution. \n", 69 | " else:\n", 70 | " print('[ Station ]:', bytes_)\n", 71 | " try:\n", 72 | " self.db_queue.put('INSERT INTO commands(timestamp, command, who) VALUES({}, \"{}\", \"{}\");'.format(time.time(), bytes_.decode(), \"Tello\")) \n", 73 | " except UnicodeDecodeError as e:\n", 74 | " print('Decoding Error that could be ignored~')\n", 75 | " \n", 76 | " def sender(self, debug=True): \n", 77 | " tello_address = ('192.168.10.1', 8889)\n", 78 | " self.cmd_event.set() # allow the first wait to proceed \n", 79 | " while True:\n", 80 | " self.cmd_event.wait() # block second get until an event is set from receiver or failure set\n", 81 | " self.cmd_event.clear() # block a timeout-enabled waiting\n", 82 | " cmd = self.cmd_queue.get() \n", 83 | " self.db_queue.put(f'INSERT INTO commands(timestamp, command, who) VALUES({time.time()}, \"{cmd}\", \"Station\");') \n", 84 | " self.socket.sendto(cmd.encode('utf-8'), tello_address)\n", 85 | " cmd_ok = False\n", 86 | " for i in range(self.MAX_RETRY): \n", 87 | " if self.cmd_event.wait(timeout=self.MAX_TIME_OUT): \n", 88 | " cmd_ok = True\n", 89 | " break\n", 90 | " else:\n", 91 | " if debug: print(f'Failed command: \"{cmd}\", Failure sequence: {i+1}.')\n", 92 | " self.socket.sendto(cmd.encode('utf-8'), tello_address) \n", 93 | " if cmd_ok: \n", 94 | " print(f'Success with \"{cmd}\".') \n", 95 | " if cmd == 'land':\n", 96 | " self.db_queue.put('commit')\n", 97 | " self.db_queue.put('close') \n", 98 | " else:\n", 99 | " self.cmd_event.set() # The failure set\n", 100 | " if debug: print(f'Stop retry: \"{cmd}\", Maximum re-tries: {self.MAX_RETRY}.')\n", 101 | " \n", 102 | " \n", 103 | " def update_state(self):\n", 104 | " UDP = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) \n", 105 | " UDP.bind(('', 8890)) \n", 106 | " while True: \n", 107 | " bytes_, address = UDP.recvfrom(1024)\n", 108 | " str_ = bytes_.decode() \n", 109 | " self.db_queue.put('INSERT INTO states(timestamp, log) VALUES({},\"{}\");'.format(time.time(), str_)) \n", 110 | " state = str_.split(';')\n", 111 | " state.pop() \n", 112 | " self.state.update(dict([s.split(':') for s in state]))\n", 113 | " \n", 114 | "tello = Tello() " 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 2, 120 | "metadata": { 121 | "ExecuteTime": { 122 | "end_time": "2020-10-18T09:24:57.852602Z", 123 | "start_time": "2020-10-18T09:24:57.815604Z" 124 | } 125 | }, 126 | "outputs": [], 127 | "source": [ 128 | "tello.command('command')" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 3, 134 | "metadata": { 135 | "ExecuteTime": { 136 | "end_time": "2020-10-18T09:24:57.902247Z", 137 | "start_time": "2020-10-18T09:24:57.859161Z" 138 | } 139 | }, 140 | "outputs": [ 141 | { 142 | "name": "stdout", 143 | "output_type": "stream", 144 | "text": [ 145 | "Success with \"command\".\n", 146 | "It may take 10 seconds to take off.\n" 147 | ] 148 | } 149 | ], 150 | "source": [ 151 | "print(\"It may take 10 seconds to take off.\") \n", 152 | "tello.command('takeoff') " 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 4, 158 | "metadata": { 159 | "ExecuteTime": { 160 | "end_time": "2020-10-18T09:24:57.927900Z", 161 | "start_time": "2020-10-18T09:24:57.913345Z" 162 | } 163 | }, 164 | "outputs": [], 165 | "source": [ 166 | "tello.command('forward 50')\n", 167 | "tello.command('left 50' )\n", 168 | "tello.command('back 50' )\n", 169 | "tello.command('right 50' ) " 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 5, 175 | "metadata": { 176 | "ExecuteTime": { 177 | "end_time": "2020-10-18T09:24:57.946727Z", 178 | "start_time": "2020-10-18T09:24:57.931286Z" 179 | } 180 | }, 181 | "outputs": [ 182 | { 183 | "name": "stdout", 184 | "output_type": "stream", 185 | "text": [ 186 | "Success with \"takeoff\".\n", 187 | "Success with \"forward 50\".\n", 188 | "Success with \"left 50\".\n", 189 | "Success with \"back 50\".\n", 190 | "Success with \"right 50\".\n", 191 | "Success with \"land\".\n", 192 | "Flight Data Saved ~\n", 193 | "Flight Data Recording Ends ~\n" 194 | ] 195 | } 196 | ], 197 | "source": [ 198 | "tello.command('land') " 199 | ] 200 | } 201 | ], 202 | "metadata": { 203 | "kernelspec": { 204 | "display_name": "Python 3", 205 | "language": "python", 206 | "name": "python3" 207 | }, 208 | "language_info": { 209 | "codemirror_mode": { 210 | "name": "ipython", 211 | "version": 3 212 | }, 213 | "file_extension": ".py", 214 | "mimetype": "text/x-python", 215 | "name": "python", 216 | "nbconvert_exporter": "python", 217 | "pygments_lexer": "ipython3", 218 | "version": "3.8.2" 219 | } 220 | }, 221 | "nbformat": 4, 222 | "nbformat_minor": 2 223 | } 224 | -------------------------------------------------------------------------------- /jupyter-notebook/README.md: -------------------------------------------------------------------------------- 1 | ### Fly_Tello.ipynb 2 | 1. Commands are executed one by another, i.e., the former one will block the latter one. 3 | 2. A command will be retried several time if it fails. 4 | 3. Drone state and command history are stored. A sample Tello_flight_log_20201018_052457.db is provided. 5 | ### Tello_Video.ipynb streams the video in a web browser. 6 | * It ONLY depends on opencv-python !!! 7 | * Use Flask to do web broadcasting. 8 | * h264decoder is required by dji-sdk/Tello-Python but it sucks. I tried the installation without success. 9 | * I came up a bizarre solution so opencv can decode the stream. 10 | * Video frame is in jpeg format (yeah, computer vision). 11 | -------------------------------------------------------------------------------- /jupyter-notebook/Tello_Video.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "ExecuteTime": { 8 | "end_time": "2020-10-18T11:13:25.834718Z", 9 | "start_time": "2020-10-18T11:13:19.509504Z" 10 | } 11 | }, 12 | "outputs": [], 13 | "source": [ 14 | "import socket\n", 15 | "import threading \n", 16 | "import cv2 \n", 17 | "import time\n", 18 | "\n", 19 | "class VIDEO: \n", 20 | " def __init__(self):\n", 21 | " ip, port='192.168.10.1', 8889\n", 22 | " socket.socket(socket.AF_INET, socket.SOCK_DGRAM).sendto(b'command', (ip, port)) \n", 23 | " socket.socket(socket.AF_INET, socket.SOCK_DGRAM).sendto(b'streamon', (ip, port)) \n", 24 | " self.void_frame = b'' \n", 25 | " self.h264_frame = self.void_frame\n", 26 | " self.jpeg_frame = self.void_frame\n", 27 | " self.frame_event = threading.Event() # tell transmitter that receiver has a new frame from tello ready \n", 28 | " self.stream_event = threading.Event() # tell opencv that transmitter has the stream ready. \n", 29 | " threading.Thread(target=self.video_receiver , daemon=True).start() \n", 30 | " threading.Thread(target=self.video_transmitter, daemon=True).start() \n", 31 | " time.sleep(3)\n", 32 | " threading.Thread(target=self.opencv , daemon=True).start() \n", 33 | " time.sleep(3)\n", 34 | " \n", 35 | " def video_receiver(self): # receive h264 stream from tello \n", 36 | " _receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # socket for receiving video stream (UDP) \n", 37 | " _receiver.bind(('', 11111)) # the udp port is fixed \n", 38 | " while True:\n", 39 | " frame = b'' \n", 40 | " while True:\n", 41 | " byte_, _ = _receiver.recvfrom(2048) \n", 42 | " frame += byte_\n", 43 | " if len(byte_) != 1460: # end of frame \n", 44 | " self.h264_frame = frame \n", 45 | " self.frame_event.set() # let the reading frame event happen\n", 46 | " self.frame_event.clear() # prevent it happen until next set\n", 47 | " break \n", 48 | " \n", 49 | " def video_transmitter(self): # feed h264 stream to opencv\n", 50 | " _transmitter = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # socket for transmitting stream (TCP) \n", 51 | " _transmitter.bind(('127.0.0.1', 12345)) # tcp port is up to us\n", 52 | " _transmitter.listen(0)\n", 53 | " while True: \n", 54 | " conn, address = _transmitter.accept() \n", 55 | " file_obj = conn.makefile('wb')\n", 56 | " stream_ready_flag = False \n", 57 | " while True: \n", 58 | " self.frame_event.wait() \n", 59 | " try:\n", 60 | " file_obj.write(self.h264_frame) \n", 61 | " except BrokenPipeError:\n", 62 | " print('[ Warning ] Tello returned nonsense!')\n", 63 | " print('[ Warning ] Please refresh stream after a while~\\n')\n", 64 | " break\n", 65 | " file_obj.flush() \n", 66 | "\n", 67 | " def opencv(self): \n", 68 | " while True:\n", 69 | " cap = cv2.VideoCapture(\"tcp://127.0.0.1:12345\") \n", 70 | " while(cap.isOpened()): \n", 71 | " ret, frame = cap.read() \n", 72 | " if not ret: \n", 73 | " print('[ Error ] Please check if your tello is off~')\n", 74 | " break \n", 75 | " ret, jpeg = cv2.imencode('.jpg', frame)\n", 76 | " self.jpeg_frame = jpeg.tobytes()\n", 77 | " cap.release()\n", 78 | " print('[ Warning ] OpenCV lost connection to transmitter!')\n", 79 | " print('[ Warning ] Try reconnection in 3 seconds~')\n", 80 | " time.sleep(3)\n", 81 | " \n", 82 | "\n", 83 | "video = VIDEO() " 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "metadata": { 90 | "ExecuteTime": { 91 | "start_time": "2020-10-18T11:13:19.492Z" 92 | } 93 | }, 94 | "outputs": [], 95 | "source": [ 96 | "import flask \n", 97 | "\n", 98 | "app = flask.Flask(__name__)\n", 99 | "print('Click Here and see the video: http://127.0.0.1:9999/stream.mjpg\\n\\n\\n')\n", 100 | "fps=25\n", 101 | "interval = 1/fps\n", 102 | "@app.route(\"/stream.mjpg\") \n", 103 | "def mjpg1(): \n", 104 | " def generator(): \n", 105 | " while True: \n", 106 | " time.sleep(interval) # threading.condition is too shitty according to my test. no condition no lag. \n", 107 | " frame = video.jpeg_frame \n", 108 | " yield f'''--FRAME\\r\\nContent-Type: image/jpeg\\r\\nContent-Length: {len(frame)}\\r\\n\\r\\n'''.encode() \n", 109 | " yield frame\n", 110 | " r = flask.Response(response=generator(), status=200)\n", 111 | " r.headers.extend({'Age':0, 'Content-Type':'multipart/x-mixed-replace; boundary=FRAME',\n", 112 | " 'Pragma':'no-cache', 'Cache-Control':'no-cache, private',}) \n", 113 | " return r\n", 114 | "\n", 115 | "@app.route('/')\n", 116 | "def hello_world():\n", 117 | " return 'Hello, World!'\n", 118 | "\n", 119 | "app.run('127.0.0.1', 9999)" 120 | ] 121 | } 122 | ], 123 | "metadata": { 124 | "kernelspec": { 125 | "display_name": "Python 3", 126 | "language": "python", 127 | "name": "python3" 128 | }, 129 | "language_info": { 130 | "codemirror_mode": { 131 | "name": "ipython", 132 | "version": 3 133 | }, 134 | "file_extension": ".py", 135 | "mimetype": "text/x-python", 136 | "name": "python", 137 | "nbconvert_exporter": "python", 138 | "pygments_lexer": "ipython3", 139 | "version": "3.8.2" 140 | } 141 | }, 142 | "nbformat": 4, 143 | "nbformat_minor": 4 144 | } 145 | -------------------------------------------------------------------------------- /see_from_tello.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import time, queue, socket, sqlite3, datetime, threading 4 | debug = True 5 | 6 | class Tello: 7 | def __init__(self): 8 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 9 | self.socket.bind(('', 8889)) 10 | self.db_queue = queue.Queue() # cache flight data 11 | self.cmd_queue = queue.Queue() 12 | self.cmd_event = threading.Event() 13 | self.MAX_TIME_OUT = 15 # It must be longer than 10 sec, give time to "take off" command. 14 | self.MAX_RETRY = 2 15 | self.state = {} 16 | threading.Thread(target=self.flight_logger, kwargs={"debug":debug}, daemon=True).start() 17 | threading.Thread(target=self.receiver , kwargs={"debug":debug}, daemon=True).start() 18 | threading.Thread(target=self.sender , kwargs={"debug":debug}, daemon=True).start() 19 | threading.Thread(target=self.update_state , daemon=True).start() 20 | 21 | def command(self, cmd): 22 | self.cmd_queue.put(cmd) 23 | 24 | def save_flight_data(self): 25 | self.db_queue.put('commit') 26 | 27 | def stop_flight_logger(self): 28 | self.db_queue.put('close') 29 | 30 | def flight_logger(self, debug=False): 31 | con = sqlite3.connect(f'Tello_flight_log_{datetime.datetime.fromtimestamp(time.time()).strftime("%Y%m%d_%H%M%S")}.db') 32 | cur = con.cursor() 33 | cur.execute('CREATE TABLE commands(timestamp REAL, command TEXT, who TEXT);') 34 | cur.execute('CREATE TABLE states(timestamp REAL, log TEXT );') 35 | if debug: print('Flight Data Recording Begins ~\n') 36 | while 1: 37 | operation = self.db_queue.get() 38 | if operation == 'commit': 39 | con.commit() 40 | if debug: print(f'Flight Data Saved @ {datetime.datetime.fromtimestamp(time.time()).strftime("%Y%m%d_%H%M%S")}~') 41 | elif operation == 'close': 42 | con.close() 43 | if debug: print(f'Flight Data Recording Ends @ {datetime.datetime.fromtimestamp(time.time()).strftime("%Y%m%d_%H%M%S")}~') 44 | break 45 | else: 46 | cur.execute(operation) 47 | 48 | def receiver(self, debug=False): 49 | while True: 50 | bytes_, address = self.socket.recvfrom(1024) 51 | if bytes_ == b'ok': 52 | self.cmd_event.set() # one command has been successfully executed. Begin new execution. 53 | else: 54 | if debug: print('[ Station ]:', bytes_) 55 | try: 56 | self.db_queue.put('INSERT INTO commands(timestamp, command, who) VALUES({}, "{}", "{}");'.format(time.time(), bytes_.decode(), "Tello")) 57 | except UnicodeDecodeError as e: 58 | if debug: print('Decoding Error that could be ignored~') 59 | 60 | def sender(self, debug=False): 61 | tello_address = ('192.168.10.1', 8889) 62 | self.cmd_event.set() # allow the first wait to proceed 63 | while True: 64 | self.cmd_event.wait() # block second queue.get() until an event is set from receiver or failure set 65 | self.cmd_event.clear() # block a timeout-enabled waiting 66 | cmd = self.cmd_queue.get() 67 | self.db_queue.put(f'INSERT INTO commands(timestamp, command, who) VALUES({time.time()}, "{cmd}", "Station");') 68 | self.socket.sendto(cmd.encode('utf-8'), tello_address) 69 | cmd_ok = False 70 | for i in range(self.MAX_RETRY): 71 | if self.cmd_event.wait(timeout=self.MAX_TIME_OUT): 72 | cmd_ok = True 73 | break 74 | else: 75 | if debug: print(f'Failed command: "{cmd}", Failure sequence: {i+1}.') 76 | self.socket.sendto(cmd.encode('utf-8'), tello_address) 77 | if cmd_ok: 78 | if debug: print(f'Success with "{cmd}".') 79 | else: 80 | self.cmd_event.set() # The failure set 81 | if debug: print(f'Stop retry: "{cmd}", Maximum re-tries: {self.MAX_RETRY}.') 82 | 83 | def update_state(self): 84 | UDP = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 85 | UDP.bind(('', 8890)) 86 | while True: 87 | bytes_, address = UDP.recvfrom(1024) 88 | str_ = bytes_.decode() 89 | self.db_queue.put('INSERT INTO states(timestamp, log) VALUES({},"{}");'.format(time.time(), str_)) 90 | state = str_.split(';') 91 | state.pop() 92 | self.state.update(dict([s.split(':') for s in state])) 93 | 94 | tello = Tello() 95 | 96 | import cv2 97 | 98 | class VIDEO: 99 | def __init__(self): 100 | tello.command('command') 101 | tello.command('streamon') 102 | self.void_frame = b'' 103 | self.h264_frame = self.void_frame 104 | self.jpeg_frame = self.void_frame 105 | self.frame_event = threading.Event() # tell transmitter that receiver has a new frame from tello ready 106 | threading.Thread(target=self.video_receiver , kwargs={"debug":debug}, daemon=True).start() 107 | time.sleep(3) 108 | threading.Thread(target=self.video_transmitter, kwargs={"debug":debug}, daemon=True).start() 109 | time.sleep(3) 110 | threading.Thread(target=self.opencv , kwargs={"debug":debug}, daemon=True).start() 111 | time.sleep(3) 112 | 113 | def video_receiver(self, debug=False): # receive h264 stream from tello 114 | _receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # socket for receiving video stream (UDP) 115 | _receiver.bind(('', 11111)) # the udp port is fixed 116 | while True: 117 | frame = b'' 118 | while True: 119 | byte_, _ = _receiver.recvfrom(2048) 120 | frame += byte_ 121 | if len(byte_) != 1460: # end of frame 122 | self.h264_frame = frame 123 | self.frame_event.set() # let the reading frame event happen 124 | self.frame_event.clear() # prevent it happen until next set 125 | break 126 | 127 | def video_transmitter(self, debug=False): # feed h264 stream to opencv 128 | _transmitter = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # socket for transmitting stream (TCP) 129 | _transmitter.bind(('127.0.0.1', 12345)) # tcp port is up to us 130 | _transmitter.listen(0) 131 | while True: 132 | conn, address = _transmitter.accept() 133 | file_obj = conn.makefile('wb') 134 | stream_ready_flag = False 135 | while True: 136 | self.frame_event.wait() 137 | try: 138 | file_obj.write(self.h264_frame) 139 | file_obj.flush() 140 | except BrokenPipeError: 141 | if debug: print('[ Warning ] Tello returned nonsense!') 142 | if debug: print('[ Warning ] Please refresh stream after a while~\n') 143 | break 144 | 145 | def opencv(self, debug=False): 146 | while True: 147 | cap = cv2.VideoCapture("tcp://127.0.0.1:12345") 148 | while(cap.isOpened()): 149 | ret, frame = cap.read() 150 | if not ret: 151 | if debug: print('[ Error ] Please check if your tello is off~') 152 | break 153 | ret, jpeg = cv2.imencode('.jpg', frame) 154 | self.jpeg_frame = jpeg.tobytes() 155 | cap.release() 156 | if debug: print('[ Warning ] OpenCV lost connection to transmitter!') 157 | if debug: print('[ Warning ] Try reconnection in 3 seconds~') 158 | time.sleep(3) 159 | 160 | video = VIDEO() 161 | 162 | import flask 163 | 164 | app = flask.Flask(__name__) 165 | fps=25 166 | interval = 1/fps 167 | @app.route("/stream.mjpg") 168 | def mjpg(): 169 | def generator(): 170 | while True: 171 | time.sleep(interval) # threading.condition is too shitty according to my test. no condition no lag. 172 | frame = video.jpeg_frame 173 | yield f'''--FRAME\r\nContent-Type: image/jpeg\r\nContent-Length: {len(frame)}\r\n\r\n'''.encode() 174 | yield frame 175 | r = flask.Response(response=generator(), status=200) 176 | r.headers.extend({'Age':0, 'Content-Type':'multipart/x-mixed-replace; boundary=FRAME', 177 | 'Pragma':'no-cache', 'Cache-Control':'no-cache, private',}) 178 | return r 179 | 180 | @app.route('/') 181 | def hello_world(): 182 | return 'Hello, World!' 183 | 184 | def web(): 185 | app.run('127.0.0.1', 9999) 186 | 187 | threading.Thread(target=web , daemon=True).start() 188 | time.sleep(5) 189 | tello.command('takeoff' ) 190 | 191 | tello.command('forward 50') 192 | tello.command('cw 90' ) 193 | tello.command('forward 50') 194 | tello.command('cw 90' ) 195 | tello.command('forward 50') 196 | tello.command('cw 90' ) 197 | tello.command('forward 50') 198 | tello.command('cw 90' ) 199 | 200 | tello.command('land' ) 201 | time.sleep(43) 202 | tello.save_flight_data() 203 | tello.stop_flight_logger() 204 | time.sleep(3) --------------------------------------------------------------------------------