├── 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)
--------------------------------------------------------------------------------