Tello Eduを用いたPythonプログラミング教育支援環境（SDK2対応，改訂版）

# Tello EduのSDK2を使ったプログラミング教育を行うための支援環境開発を行いました（ベータ版）

### 概要
ミニ（ホビー）ドローンを使って小学生高学年から高等学校の児童生徒がプログラミングを学べる教材を開発しています。巻末の参考資料を参考に作成しています。ありがとうございます。
※
[Tello Eduをpythonで動かそう！（SDK2対応）](https://qiita.com/yoomori/items/e847166433d44ab53c29)の改良版です。コマンドやインタフェースを変えています．

前バージョンからの主な変更点は以下の通りです．
#### Tell Eduとの間のコマンド通信ログ等　Tello Eduの状態がわかるインタフェースを作成
以下の画面に状態が表示されます．またここからドローンを実行できるようにしました．
<img width="1440" alt="image2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135880/0a32d8a5-29d4-0c9d-1a05-2b095298f4f1.png">
注意事項：画面の解像度が　1440 x 900　以上ないと画面が崩れるかもしれません．MacBooKAirなどでこのサイズの画面の場合は，Dockを自動的に表示／非表示にする設定をしておいてください．

#### 外部エディタの利用
外部エディタとして自分のすきなものを使えるようにしました．

### 実行環境

 - python3.7.6　（注：3.8系にすると以下のパッケージが対応でいていないものもある）
 - PyCharm 2019.3.2 (Community Edition) 
 - opencv 3.4.2
 - pillow 7.0.0
 - pyzbar 0.1.8
 - matplotlib 3.1.1
 - numpy 1.17.4

### 注意点
- pycharmの仮想環境の設定は，condaとする。

### インストール
#### python
anacondaを用いてインストールする。

#### PyCharm 2019.3.2 (Community Edition)
プロジェクトを作る際に，仮想環境として[CONDA]を利用するように設定しておく
<img width="480" alt="スクリーンショット 2019-09-04 11.12.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135880/c737ae57-4685-50d0-c737-f3f4777ef822.png">

#### opencv
PyCharmで作成したプロジェクトの仮想環境が[anaconda]に出来ているので，その環境（Environments)を選択肢して，そこにPackageを追加する。

追加する場合は，Not Installedを選択して，opencvで検索を行う。
次の3つのパッケージをインストールする。 libopencv 3.4.2，opencv 3.4.2，　py-opencv 3.4.2をインストールする。

#### pillow 
opencvと同様に，pillowを選択してインストール

#### pyzbar
pyzbarは，anacondaのライブラリとしてはない，また，anaconda cloudにおいても該当するものインストールする。ない場合は，PyCharmで該当するプロジェクト開き，コンソールから以下のコマンドを入れる。
ただし，MacOSの場合は，zbar本体のインストールが必要となる。

```python:
 pip install pyzbar
```

[MacOSでzbarをインストールする方法](http://macappstore.org/zbar/)

#### matplotlib
opencvと同様に，matplotlibを選択してインストール

#### numpy
これも同じくnumpyを選択してインストール

#### 上記パッケージのインストールの際の注意点
- バージョンに注意
- Anacondaからインストールできない環境がありました．その場合は，PyCharmの設定にあるProjectInterpreterから設定できました．

### ソースコード
#### ファイルの設置
PyCharmで作成したプロジェクト内に，以下のディレクトリの構成で各ファイルを配置してください。
最新のプログラムはこちらからダウンロードしてください．
[Github yoomori/PPETelloEdu](https://github.com/yoomori/PPETelloEdu)

- Tello Edu SDK2対応ライブラリ（パッケージ）[telloedu]
    - tello edu SDK2 コマンドモジュール[command.py]
    - tello edu SDK2 状態取得モジュール[status.py]
    - tello edu SDK2 映像系モジュール[streaming.py]
    - tello edu ドローンプログラム実行モジュール[tellostart.py]
    - tello edu 共通関数モジュール[tellolib.py]
- メイン（プログラム）ファイル[tellomain.py]
- 演習用ファイル[ugoki.py]

その他に，

- QRコードおよび前面カメラで撮影した写真データが保存される[img]ディレクトリ，
- Tello Eduから送られてくる動画を保存する[mpg]ディレクトリ，
- Tello Eduから送られてくるStatusデータをCSVファイルとして保存する[data]ディレクトリ

も同一のディレクトリ内に作成しておいてください。

```
　.
　├── data/
　├── img/
　├── mpg/
　├── telloedu/
　│   ├── __init__.py
　│   ├── command.py
　│   ├── status.py
　│   ├── streaming.py
　│   ├── tellostart.py
　│   └── tellolib.py
　├── tellomain.py
　└── ugoki.py
```



こんな感じです。

#### Tello Edu SDK2対応ライブラリ（パッケージ）

この部分まだバグがあるかもしれません。すべてのバグを取り切れているとは言えないと思っています。


```python:command.py
#
# Tello Edu (SDK2.0 対応) Python3 Command Library
#

import socket
import time

from telloedu.tellolib import *
from telloedu.status import *

import telloedu.streaming as streaming

# Create a UDP socket
host = ''
port = 9000
tello_ip = '192.168.10.1'
tello_port = 8889
locaddr = (host, port)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
tello_address = (tello_ip, tello_port)
sock.bind(locaddr)


# command
def emergency():
	cmd = 'emergency'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: ', cmd, ' to ', tello_address)
	try:
		sock.sendto(cmd, tello_address)

	except socket.error:
		print('\n....ERROR: ', cmd, ' ....\n')
	streaming.video_recording = 0
	# streaming.main_thread = False
	# streaming.set_mainthread(False)


def command():
	cmd = 'command'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: ', cmd, ' to ', tello_address)
	try:
		sock.sendto(cmd, tello_address)
		while streaming.main_thread:
			data, server = sock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			if res == 'ok' or res == 'error':
				print('recv: ', cmd, ' ', res)
				break
	except socket.error:
		print('\n....ERROR: ', cmd, ' ....\n')
		sock.close()


def takeoff():
	cmd = 'takeoff'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: ', cmd, ' to ', tello_address)
	try:
		sock.sendto(cmd, tello_address)
		while streaming.main_thread:
			data, server = sock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			if res == 'ok' or res == 'error':
				print('recv: ', cmd, ' ', res)
				break
	except socket.error:
		print('\n....ERROR: ', cmd, ' ....\n')
		sock.close()


def land():
	cmd = 'land'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: ', cmd, ' to ', tello_address)
	try:
		sock.sendto(cmd, tello_address)
		while streaming.main_thread:
			data, server = sock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			if res == 'ok' or res == 'error':
				print('recv: ', cmd, ' ', res)
				break
	except socket.error:
		print('\n....ERROR: ', cmd, ' ....\n')
		sock.close()


def stop():
	cmd = 'stop'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: ', cmd, ' to ', tello_address)
	try:
		sock.sendto(cmd, tello_address)
		while streaming.main_thread:
			data, server = sock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			if res == 'ok' or res == 'error':
				print('recv: ', cmd, ' ', res)
				break
	except socket.error:
		print('\n....ERROR: ', cmd, ' ....\n')
		sock.close()


def up(x):
	if type(x) is int:
		if (20 <= x) and (x <= 200):
			cmd = 'up ' + str(x)
			cmd = cmd.encode(encoding = "utf-8")
			print('Send: ', cmd, ' to ', tello_address)
			try:
				sock.sendto(cmd, tello_address)
				while streaming.main_thread:
					data, server = sock.recvfrom(1518)
					res = data.decode(encoding = "utf-8")
					if res == 'ok' or res == 'error':
						print('recv: ', cmd, ' ', res)
						break
			except socket.error:
				print('\n....ERROR: ', cmd, ' ....\n')
				sock.close()
		else:
			print('\n...引数の値が10...200の間でない\n')
	else:
		print('\n...引数の値が整数型でない\n')


def down(x):
	if type(x) is int:
		if 20 <= x <= 200:
			cmd = 'down ' + str(x)
			cmd = cmd.encode(encoding = "utf-8")
			print('Send: ', cmd, ' to ', tello_address)
			try:
				sock.sendto(cmd, tello_address)
				while streaming.main_thread:
					data, server = sock.recvfrom(1518)
					res = data.decode(encoding = "utf-8")
					if res == 'ok' or res == 'error':
						print('recv: ', cmd, ' ', res)
						break
			except socket.error:
				print('\n....ERROR: ', cmd, ' ....\n')
				sock.close()
		else:
			print('\n...引数の値が10...200の間でない\n')
	else:
		print('\n...引数の値が整数型でない\n')


def left(x):
	if type(x) is int:
		if 20 <= x <= 200:
			cmd = 'left ' + str(x)
			cmd = cmd.encode(encoding = "utf-8")
			print('Send: ', cmd, ' to ', tello_address)
			try:
				sock.sendto(cmd, tello_address)
				while streaming.main_thread:
					data, server = sock.recvfrom(1518)
					res = data.decode(encoding = "utf-8")
					if res == 'ok' or res == 'error':
						print('recv: ', cmd, ' ', res)
						break
			except socket.error:
				print('\n....ERROR: ', cmd, ' ....\n')
				sock.close()
		else:
			print('\n...引数の値が10...200の間でない\n')
	else:
		print('\n...引数の値が整数型でない\n')


def right(x):
	if type(x) is int:
		if 20 <= x <= 200:
			cmd = 'right ' + str(x)
			cmd = cmd.encode(encoding = "utf-8")
			print('Send: ', cmd, ' to ', tello_address)
			try:
				sock.sendto(cmd, tello_address)
				while streaming.main_thread:
					data, server = sock.recvfrom(1518)
					res = data.decode(encoding = "utf-8")
					if res == 'ok' or res == 'error':
						print('recv: ', cmd, ' ', res)
						break
			except socket.error:
				print('\n....ERROR: ', cmd, ' ....\n')
				sock.close()
		else:
			print('\n...引数の値が10...200の間でない\n')
	else:
		print('\n...引数の値が整数型でない\n')


def forward(x):
	if type(x) is int:
		if 20 <= x <= 200:
			cmd = 'forward ' + str(x)
			cmd = cmd.encode(encoding = "utf-8")
			print('Send: ', cmd, ' to ', tello_address)
			try:
				sock.sendto(cmd, tello_address)
				while streaming.main_thread:
					data, server = sock.recvfrom(1518)
					res = data.decode(encoding = "utf-8")
					if res == 'ok' or res == 'error':
						print('recv: ', cmd, ' ', res)
						break
			except socket.error:
				print('\n....ERROR: ', cmd, ' ....\n')
				sock.close()
		else:
			print('\n...引数の値が10...200の間でない\n')
	else:
		print('\n...引数の値が整数型でない\n')


def back(x):
	if type(x) is int:
		if 20 <= x <= 200:
			cmd = 'back ' + str(x)
			cmd = cmd.encode(encoding = "utf-8")
			print('Send: ', cmd, ' to ', tello_address)
			try:
				sock.sendto(cmd, tello_address)
				while streaming.main_thread:
					data, server = sock.recvfrom(1518)
					res = data.decode(encoding = "utf-8")
					if res == 'ok' or res == 'error':
						print('recv: ', cmd, ' ', res)
						break
			except socket.error:
				print('\n....ERROR: ', cmd, ' ....\n')
				sock.close()
		else:
			print('\n...引数の値が10...200の間でない\n')
	else:
		print('\n...引数の値が整数型でない\n')


def cw(x):
	if type(x) is int:
		if 1 <= x <= 360:
			cmd = 'cw ' + str(x)
			cmd = cmd.encode(encoding = "utf-8")
			print('Send: ', cmd, ' to ', tello_address)
			try:
				sock.sendto(cmd, tello_address)
				while streaming.main_thread:
					data, server = sock.recvfrom(1518)
					res = data.decode(encoding = "utf-8")
					if res == 'ok' or res == 'error':
						print('recv: ', cmd, ' ', res)
						break
			except socket.error:
				print('\n....ERROR: ', cmd, ' ....\n')
				sock.close()
		else:
			print('\n...引数の値が1...360の間でない\n')
	else:
		print('\n...引数の値が整数型でない\n')


def ccw(x):
	if type(x) is int:
		if 1 <= x <= 360:
			cmd = 'ccw ' + str(x)
			cmd = cmd.encode(encoding = "utf-8")
			print('Send: ', cmd, ' to ', tello_address)
			try:
				sock.sendto(cmd, tello_address)
				while streaming.main_thread:
					data, server = sock.recvfrom(1518)
					res = data.decode(encoding = "utf-8")
					if res == 'ok' or res == 'error':
						print('recv: ', cmd, ' ', res)
						break
			except socket.error:
				print('\n....ERROR: ', cmd, ' ....\n')
				sock.close()
		else:
			print('\n...引数の値が1...360の間でない\n')
	else:
		print('\n...引数の値が整数型でない\n')


def set_speed(x):
	if type(x) is int:
		if 10 <= x <= 100:
			cmd = 'speed ' + str(x)
			cmd = cmd.encode(encoding = "utf-8")
			print('Send: ', cmd, ' to ', tello_address)
			try:
				sock.sendto(cmd, tello_address)
				while streaming.main_thread:
					data, server = sock.recvfrom(1518)
					res = data.decode(encoding = "utf-8")
					if res == 'ok' or res == 'error':
						print('recv: ', cmd, ' ', res)
						break
			except socket.error:
				print('\n . . .\n')
				sock.close()
		else:
			print('\n...引数の値が10...100の間でない\n')
	else:
		print('\n...引数の値が整数型でない\n')


def streamon():
	cmd = 'streamon'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: ', cmd, ' to ', tello_address)
	try:
		sock.sendto(cmd, tello_address)
		while streaming.main_thread:
			data, server = sock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			if res == 'ok' or res == 'error':
				streaming.video_recording = 1
				print('recv: ', cmd, ' ', res)
				break
		time.sleep(5)
	except socket.error:
		print('\n....ERROR: ', cmd, ' ....\n')
		streaming.video_recording = 0
		sock.close()


def streamoff():
	cmd = 'streamoff'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: ', cmd, ' to ', tello_address)
	try:
		# time.sleep(5)
		sock.sendto(cmd, tello_address)
		while streaming.main_thread:
			data, server = sock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			if res == 'ok' or res == 'error':
				streaming.video_recording = 0
				print('recv: ', cmd, ' ', res)
				break
	except socket.error:
		print('\n....ERROR: ', cmd, ' ....\n')
		streaming.video_recording = 0
		sock.close()


def get_qrcode():
	cmd = 'streamon'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: get_qrcode(', cmd, ') to ', tello_address)
	try:
		sock.sendto(cmd, tello_address)
		while streaming.main_thread:
			data, server = sock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			if res == 'ok' or res == 'error':
				streaming.video_recording = 10
				print('recv: get_qrcode(', cmd, ') ', res)
				break
	except socket.error:
		print('\n....ERROR:  QR Code ....\n')
		streaming.video_recording = 0
		sock.close()

	time.sleep(5)
	cmd = 'streamoff'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: get_qrcode(', cmd, ') to ', tello_address)
	try:
		sock.sendto(cmd, tello_address)
		while streaming.main_thread:
			data, server = sock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			if res == 'ok' or res == 'error':
				streaming.video_recording = 0
				print('recv: get_qrcode(', cmd, ') ', res)
				break
	except socket.error:
		print('\n....ERROR:  QR Code ....\n')
		streaming.video_recording = 0
		sock.close()
	code = streaming.qr_code
	streaming.qr_code = ""
	return code

def take_picture():
	cmd = 'streamon'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: take a pictur(', cmd, ') to ', tello_address)
	try:
		sock.sendto(cmd, tello_address)
		while streaming.main_thread:
			data, server = sock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			if res == 'ok' or res == 'error':
				streaming.video_recording = 20
				print('recv: take a photograph(', cmd, ') ', res)
				break
	except socket.error:
		print('\n....ERROR:  take a photograph ....\n')
		streaming.video_recording = 0
		sock.close()

	time.sleep(5)
	cmd = 'streamoff'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: take a photograph(', cmd, ') to ', tello_address)
	try:
		sock.sendto(cmd, tello_address)
		while streaming.main_thread:
			data, server = sock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			if res == 'ok' or res == 'error':
				streaming.video_recording = 0
				print('recv: take a photograph(', cmd, ') ', res)
				break
	except socket.error:
		print('\n....ERROR:  take a photograph ....\n')
		streaming.video_recording = 0
		sock.close()

def get_speed():
	cmd = 'speed?'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: ', cmd, ' to ', tello_address)
	try:
		sock.sendto(cmd, tello_address)
		while streaming.main_thread:
			data, server = sock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			if is_float(res):
				res = float(res)
				print('recv: ', cmd, ' ', res, ' cm/s')
				return res
	except socket.error:
		print('\n .......get_speed.........\n')
		sock.close()


def get_battery():
	cmd = 'battery?'
	cmd = cmd.encode(encoding = "utf-8")
	print('Send: ', cmd, ' to ', tello_address)
	try:
		sock.sendto(cmd, tello_address)
		while streaming.main_thread:
			data, server = sock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			if is_int(res):
				res = int(res)
				print('recv: ', cmd, ' ', res, '%')
				return res
	except socket.error:
		print('\n .......get_battery.........\n')
		sock.close()


def end():
	print('Script: end1')
	streaming.main_thread = False
	streaming.set_mainthread(False)
	print('Script: end2')
	time.sleep(2)
	print('Script: end3')
	sock.close()
	print('Script: end4')
```

```python:status.py
#
# Tello Edu (SDK2.0 対応) Python3 status Library
#

import csv
import datetime
import re
import socket

import telloedu.streaming as streaming

Tello_TOF = 0
Tello_Height = 0
Tello_Bat = 0
Tello_Pitch = 0
Tello_Roll = 0
Tello_yaw = 0
Tello_Temph = 0


def tello_status_thread():
	global Tello_Temph
	global Tello_TOF
	global Tello_Height
	global Tello_Bat
	global Tello_Pitch
	global Tello_Roll
	global Tello_yaw
	path = './data/'
	date_fmt = '%Y-%m-%d_%H%M%S'
	file_name = '%stellostatus-%s.csv' % (path, datetime.datetime.now().strftime(date_fmt))
	csvhead = ["mid", "x", "y", "z", "xxx", "pitch", "roll", "yaw", "vgx", "vgy", "vgz", "templ", "temph", "tof",
	           "h", "bat", "baro", "time", "agx", "agy", "sgz"]
	tshost = '0.0.0.0'
	tsport = 8890
	tslocaddr = (tshost, tsport)
	tssock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
	tssock.bind(tslocaddr)

	while streaming.main_thread:
		try:
			data, server = tssock.recvfrom(1518)
			res = data.decode(encoding = "utf-8")
			res2 = re.sub('[a-z:\n\r]', '', res)
			res3 = res2.split(';')
			Tello_Temph = res3[12]
			Tello_TOF = res3[13]
			Tello_Height = res3[14]
			Tello_Bat = res3[15]
			Tello_Pitch = res3[5]
			Tello_Roll = res3[6]
			Tello_yaw = res3[7]
			try:
				with open(file_name, 'x') as f:
					writer = csv.writer(f)
					writer.writerow(csvhead)
					writer.writerow(res3)
			except FileExistsError:
				with open(file_name, 'a') as f:
					writer = csv.writer(f)
					writer.writerow(res3)
		except socket.error:
			print('\nTello Status Exit . . .\n')
			tssock.close()
			break
		except KeyboardInterrupt:
			print('\nTello Status Exit . . .\n')
			tssock.close()
			break
	tssock.close()

def get_temph():
	global Tello_Temph
	return(float(Tello_Temph))

def get_tof():
	global Tello_TOF
	return (float(Tello_TOF))


def get_height():
	global Tello_Height
	return (float(Tello_Height))


def get_bat():
	global Tello_Bat
	return (float(Tello_Bat))


def get_pitch():
	global Tello_Pitch
	return (Tello_Pitch)


def get_roll():
	global Tello_Roll
	return (Tello_Roll)


def get_yaw():
	global Tello_yaw
	return (Tello_yaw)
```

```python:streaming.py
#
# Tello Edu (SDK2.0 対応) Python3 Streaming Library
#

import datetime
import cv2
from PIL import Image
from pyzbar.pyzbar import decode

video_recording = 0
qr_code = ""

# Video Recording Thread
def video_recording_thread():
	global video_recording
	while main_thread:
		try:
			if video_recording == 1:
				video_recording_start()
			if video_recording == 10:
				analyze_qrcode()
			if video_recording == 20:
				take_photo()
			if video_recording == 999:
				break
		except KeyboardInterrupt:
			print('\nExit . . .\n')
			break


def video_recording_start():
	global video_recording
	vr_udp_ip = '0.0.0.0'
	vr_udp_port = 11111
	path = './mpg/'
	date_fmt = '%Y-%m-%d_%H%M%S'
	file_name = '%stello-video-%s.m4v' % (path, datetime.datetime.now().strftime(date_fmt))
	udp_video_address = 'udp://@' + vr_udp_ip + ':' + str(vr_udp_port)

	cap = cv2.VideoCapture(udp_video_address)
	frame_rate = cap.get(cv2.CAP_PROP_FPS)  # 40   フレームレート
	size = (640, 480)  # 動画の画面サイズ
	fmt = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
	writer = cv2.VideoWriter(file_name, fmt, frame_rate, size)

	if not cap.isOpened():
		cap.open(udp_video_address)

	while video_recording == 1:
		try:
			ret, frame = cap.read()
			frame = cv2.resize(frame, size)
			writer.write(frame)
		except cv2.error:
			print('\nExit (cv2.error). . .\n')
			writer.release()
			cap.release()
			break
	#video_recording = 0
	writer.release()
	cap.release()


def analyze_qrcode():
	global video_recording
	global qr_code
	qr_code = ""

	if video_recording <= 9:
		return 'analyze qrcode: error'

	vr_udp_ip = '0.0.0.0'
	vr_udp_port = 11111
	path = './img/'
	date_fmt = '%Y-%m-%d_%H%M%S'
	file_name = '%sqrcode-%s.png' % (path, datetime.datetime.now().strftime(date_fmt))
	cap = None
	udp_video_address = 'udp://@' + vr_udp_ip + ':' + str(vr_udp_port)

	if cap is None:
		cap = cv2.VideoCapture(udp_video_address)

	if not cap.isOpened():
		cap.open(udp_video_address)

	ret, frame = cap.read()
	cv2.imwrite(file_name, frame)
	qr = decode(Image.open(file_name))
	if len(qr) != 0:
		qr_code = qr[0][0].decode('utf-8', 'ignore')
		print('QR Code: %s' % qr_code)
	else:
		qr_code = ""
		print('No QR Code !!!!!!')
	video_recording = 0
	cap.release()

def take_photo():
	global video_recording

	if video_recording <= 19:
		return 'take a photo: error'

	vr_udp_ip = '0.0.0.0'
	vr_udp_port = 11111
	path = './img/'
	date_fmt = '%Y-%m-%d_%H%M%S'
	file_name = '%sphoto-%s.png' % (path, datetime.datetime.now().strftime(date_fmt))
	cap = None
	udp_video_address = 'udp://@' + vr_udp_ip + ':' + str(vr_udp_port)

	if cap is None:
		cap = cv2.VideoCapture(udp_video_address)

	if not cap.isOpened():
		cap.open(udp_video_address)

	ret, frame = cap.read()
	cv2.imwrite(file_name, frame)

	video_recording = 0
	cap.release()

main_thread = True

def set_mainthread(x):
	global main_thread
	main_thread = x
```

```python:telolib.py
#
# Tello Edu (SDK2.0 対応) Python3 Common Library
#

# type check function
def is_int(s):
	try:
		int(s)
		return True
	except ValueError:
		return False


def is_float(s):
	try:
		float(s)
		return True
	except ValueError:
		return False
```

``` python:tellostart.py
import ugoki
from telloedu.command import *
import telloedu.streaming as streaming



def set_drone_flg(x):
	global drone_flg
	drone_flg = x

def get_drone_flg():
	return(drone_flg)

def start_thread():
	while streaming.main_thread:
		try:
			if get_drone_flg():
				command()
				ugoki.gotello()
				#end()
				print("End")
				set_drone_flg(False)
		except KeyboardInterrupt:
			emergency()
			print('\n..... Keyboard Interrupt: emergency!! ......\n')
		except TypeError:
			emergency()
			print('\n..... Type Error: emergency!! ......\n')

drone_flg = False
```

### メイン（プログラム）ファイル
　起動関数が定義されています。このファイルを実行してください。

``` python:tellomain.py
from concurrent import futures
import sys
import tkinter
import tkinter.scrolledtext
import os, os.path
import datetime
import glob
from PIL import Image, ImageTk

from matplotlib.backends.backend_tkagg import (
	FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.figure import Figure
import matplotlib.animation as animation

import numpy as np
import random

import importlib

from telloedu.status import *
from telloedu.streaming import *
from telloedu.tellostart import *
from telloedu.command import *

import ugoki

def _quit():
	end()
	#root.quit()
	root.destroy()

def init_tof():
	global x_tof, y_tof
	x_tof = np.arange(-50, 0, 0.5)
	y_tof = np.zeros(100)
	line_tof.set_ydata(y_tof)
	line_tof.set_xdata(x_tof)
	return line_tof,

def animate_tof(n):
	global t_tof,x_tof,y_tof
	if get_drone_flg():
		y_tof = np.append(y_tof, get_tof())
		y_tof = np.delete(y_tof, 0)
		t_tof = t_tof + 1
		x_tof = np.append(x_tof, t_tof)
		x_tof = np.delete(x_tof, 0)
	plt_tof.set_ylim(0, 200)
	plt_tof.set_xlim(min(x_tof), max(x_tof))
	line_tof.set_ydata(y_tof)
	line_tof.set_xdata(x_tof)
	return line_tof,

def init_h():
	global x_h, y_h
	x_h = np.arange(-50, 0, 0.5)
	y_h = np.zeros(100)
	line_h.set_ydata(y_h)
	line_h.set_xdata(x_h)
	return line_h,

def animate_h(n):
	global t_h,x_h,y_h
	if get_drone_flg():
		y_h = np.append(y_h, get_height())
		y_h = np.delete(y_h, 0)
		t_h = t_h + 1
		x_h = np.append(x_h, t_h)
		x_h = np.delete(x_h, 0)
	plt_h.set_ylim(0, 200)
	plt_h.set_xlim(min(x_h), max(x_h))
	line_h.set_ydata(y_h)
	line_h.set_xdata(x_h)
	return line_h,

def init_temp():
	global x_temp, y_temp
	x_temp = np.arange(-50, 0, 0.5)
	y_temp = np.zeros(100)
	line_h.set_ydata(y_temp)
	line_h.set_xdata(x_temp)
	return line_temp,

def animate_temp(n):
	global t_temp,x_temp,y_temp
	if get_drone_flg():
		y_temp = np.append(y_temp, get_temph())
		y_temp = np.delete(y_temp, 0)
		t_temp = t_temp + 1
		x_temp = np.append(x_temp, t_temp)
		x_temp = np.delete(x_temp, 0)
	plt_temp.set_ylim(0, 100)
	plt_temp.set_xlim(min(x_temp), max(x_temp))
	line_temp.set_ydata(y_temp)
	line_temp.set_xdata(x_temp)
	return line_temp,

def init_bat():
	global x_bat, y_bat
	x_bat = np.arange(-50, 0, 0.5)
	y_bat = np.zeros(100)
	line_bat.set_ydata(y_bat)
	line_bat.set_xdata(x_bat)
	return line_bat,

def animate_bat(n):
	global t_bat,x_bat,y_bat
	if get_drone_flg():
		y_bat = np.append(y_bat, get_bat())
		y_bat = np.delete(y_bat, 0)
		t_bat = t_bat + 1
		x_bat = np.append(x_bat, t_bat)
		x_bat = np.delete(x_bat, 0)
	plt_bat.set_ylim(0, 100)
	plt_bat.set_xlim(min(x_bat), max(x_bat))
	line_bat.set_ydata(y_bat)
	line_bat.set_xdata(x_bat)
	return line_bat,

def _godrone():
	global drone_flg
	textbox1.delete('1.0','end')
	importlib.reload(ugoki)
	set_drone_flg(True)

def _droneEmergency():
	emergency()

def _thumbnail():
	qr_image_path = './img/qrcode-*.png'
	qr_files = glob.glob(qr_image_path)
	#print(qr_files)
	qr_files.sort(key = os.path.getmtime, reverse = True)
	#print(qr_files)
	if qr_files:
		qr_image = Image.open(qr_files[0])
		qr_image = qr_image.resize((184, 144))
		m1_image = ImageTk.PhotoImage(qr_image)
		dt = datetime.datetime.fromtimestamp(os.stat(qr_files[0]).st_mtime)
		qr_image_label = tkinter.Label(canvas2, image = m1_image, text = 'QR Code 撮影日\n'+ dt.strftime(
			'%Y年%m月%d日 %H:%M:%S'), compound='top')
		qr_image_label.grid(column = 1, row = 2)
		qr_image_label.image = m1_image
	pic_image_path = './img/photo-*.png'
	pic_files = glob.glob(pic_image_path)
	#print(pic_files)
	pic_files.sort(key = os.path.getmtime, reverse = True)
	#print(pic_files)
	if pic_files:
		pic_image = Image.open(pic_files[0])
		pic_image = pic_image.resize((184, 144))
		m2_image = ImageTk.PhotoImage(pic_image)
		dt = datetime.datetime.fromtimestamp(os.stat(pic_files[0]).st_mtime)
		pic_image_label = tkinter.Label(canvas2, image = m2_image, text = 'Photo 撮影日\n'+ dt.strftime(
			'%Y年%m月%d日 %H:%M:%S'), compound='top')
		pic_image_label.grid(column = 2, row = 2)
		pic_image_label.image = m2_image

def redirector(inputStr):
	textbox1.insert(tkinter.INSERT, inputStr)


if __name__ == "__main__":
	root = tkinter.Tk()
	root.wm_title("ドローン・プログラミング支援システム")

	frame1 = tkinter.Frame(root, width = 1200, height = 400, borderwidth = 4)
	frame1.pack(padx = 5, pady = 5)

	fig = Figure(figsize = (10, 7))
	fig.suptitle('Drone flight status')
	canvas = FigureCanvasTkAgg(fig, master = frame1)  # A tk.DrawingArea.

	# ToF
	t_tof = 1
	x_tof = np.arange(-50, 0, 0.5)
	y_tof = np.zeros(100)
	plt_tof = fig.add_subplot(221)
	plt_tof.set_title('ToF')
	plt_tof.set_xlabel("time[s]")
	plt_tof.set_ylabel("Height[cm]")
	line_tof, = plt_tof.plot(x_tof, y_tof)
	ani_tof = animation.FuncAnimation(fig, animate_tof, init_func = init_tof, interval = 500, blit = False)
	# Height
	t_h = 1
	x_h = np.arange(-50, 0, 0.5)
	y_h = np.zeros(100)
	plt_h = fig.add_subplot(222)
	plt_h.set_title('Height')
	plt_h.set_xlabel("time[s]")
	plt_h.set_ylabel("Height[cm]")
	line_h, = plt_h.plot(x_h, y_h)
	ani_h = animation.FuncAnimation(fig, animate_h, init_func = init_h, interval = 500, blit = False)
	# Temperature
	t_temp = 1
	x_temp = np.arange(-50, 0, 0.5)
	y_temp = np.zeros(100)
	plt_temp = fig.add_subplot(223)
	plt_temp.set_title('Temperature')
	plt_temp.set_xlabel("time[s]")
	plt_temp.set_ylabel("Temperature[c]")
	line_temp, = plt_temp.plot(x_temp, y_temp)
	ani_temp = animation.FuncAnimation(fig, animate_temp, init_func = init_temp, interval = 500, blit = False)
	# battery
	t_bat = 1
	x_bat = np.arange(-50, 0, 0.5)
	y_bat = np.zeros(100)
	plt_bat = fig.add_subplot(224)
	plt_bat.set_title('Battery')
	plt_bat.set_xlabel("time[s]")
	plt_bat.set_ylabel("battery[%]")
	line_bat, = plt_bat.plot(x_bat, y_bat)
	ani_bat = animation.FuncAnimation(fig, animate_bat, init_func = init_bat, interval = 500, blit = False)

	fig.subplots_adjust(wspace = 0.5, hspace = 0.5)
	toolbar = NavigationToolbar2Tk(canvas, root)
	canvas.get_tk_widget().pack(side = 'left')

	frame2 = tkinter.Frame(frame1, borderwidth = 4)
	frame2.pack(padx = 5, pady = 20)

	m1_text_label = tkinter.Label(frame2, text = '送信コマンド・ログ',font = ("",18))
	m1_text_label.pack(side = 'top', fill = 'both')

	#textbox1 = tkinter.Text(frame2)
	textbox1 = tkinter.scrolledtext.ScrolledText(frame2)
	textbox1.configure(bd=1, highlightbackground='gray')
	textbox1.pack(side = 'top', fill = 'both', padx=10)
	sys.stdout.write = redirector

	m2_text_label = tkinter.Label(frame2, text = '\n\n\n撮影した写真データ', font = ("",18))
	m2_text_label.pack(side = 'top', fill = 'both', padx = 10)
	canvas2 = tkinter.Canvas(frame2, width = 400)
	canvas2.pack(side = 'top', fill = 'both')
	_thumbnail()

	frame = tkinter.Frame(root, width = 60, height = 40, borderwidth = 4, bg = 'gray')
	frame.pack(padx = 5, pady = 5)
	button1 = tkinter.Button(master = frame, text = "プログラム実行", command = _godrone, width = 20, fg = '#0000ff')
	button1.pack(fill = 'x', padx = 30, side = 'left')

	button2 = tkinter.Button(master = frame, text = "撮影した写真表示", command = _thumbnail, width = 20)
	button2.pack(fill = 'x', padx = 30, side = 'left')

	button3 = tkinter.Button(master = frame, text = "終了", command = _quit, width = 20)
	button3.pack(fill = 'x', padx = 30, side = 'left')

	button4 = tkinter.Button(master = frame, text = "ドローン緊急停止", command = _droneEmergency, width = 20, fg = '#ff0000')
	button4.pack(fill = 'x', padx = 30, side = 'left')

	with futures.ThreadPoolExecutor(max_workers = 10) as executor:
		executor.submit(tello_status_thread)
		executor.submit(video_recording_thread)
		executor.submit(start_thread)
		set_drone_flg(False)
		tkinter.mainloop()

```
### 演習用ファイル
　演習では，このgotello関数の内部を課題に応じて変更します。以下のプログラムはサンプルです。
ただし，「# ここから以下を直します」と「# これから下は直さないでください」の間以外はさわらないこと。
以下のgotello関数の中は，使えるコマンドなどを一通り試す目的でいれています。

```python:ugoki.py
from telloedu.command import *
import time

def gotello():
	# ここから以下が修正可能です
	takeoff()
	up(50)
	forward(200)
	take_picture()
	ccw(180)
	take_picture()
	forward(200)
	land()
	# これから以下は修正できません
```

この部分をエディタで変更し保存すれば，以下のプログラミング環境を実行した際に何度でも【プログラムの実行】ボタンを押して実行できます．

#### gotello関数の内部で使える命令（コマンド）


|コマンド&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;| 引数&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | 説明 | 返値 |
|:--|:--|:--|:--|
|command()|なし|Tello EduのモードをSDK2に設定最初にこのコマンドを送信する必要がある|なし|
|takeoff()|なし|離陸（約1m上昇する）|なし|
|land()|なし|着陸|なし|
|stop()|なし|ホバリング（次のコマンドを50秒以内に送信）|なし|
|up(x)|x: 20cm〜200cm|上昇|なし|
|down(x)|x: 20cm〜200cm|下降|なし|
|left(x)|x: 20cm〜200cm|左側に動く|なし|
|right(x)|x: 20cm〜200cm|右側に動く|なし|
|forward(x)|x: 20cm〜200cm|前進|なし|
|back(x)|x: 20cm〜200cm|後進|なし|
|cw(x)|x:1度〜360度|時計回りに回転(Clockwise Rotation)|なし|
|ccw(x)|x:1度〜360度|反時計回りに回転(Counter Clockwise Rotation)|なし|
|end()|なし|すべてのコマンドを終了（一番最後に送信する必要がある）|なし|
|streamon()|なし|ビデオ録画開始　※録画中にget_qrcode()を呼び出さないこと|なし|
|streamoff()|なし|ビデオ録画終了|なし|
|take_picture()|なし|写真撮影|なし|
|set_speed(x)|x:10〜100|ドローンの動くスピードを指定|なし|
|get_qrcode()|なし|QRコードを撮影して解析　※解析中にstreamon()を呼び出さないこと|解析結果（文字列）|
|get_speed()|なし|ドローンに設定されてるスピード情報を得ることができる|スピード（数値：浮動小数点）
|get_battery() get_bat()|なし|ドローンのバッテリー残量を得ることができる|残量（0〜100）|
|get_temph()|なし|ドローンの機体温度を得ることができる|温度（整数）|
|get_tof()|なし|ドローンの高さ(ToF)を得ることができる|高さ（cm）|
|get_height()|なし|ドローンの高さを得ることができる|高さ（cm）|
|get_pitch()|なし|ドローンのピッチを得ることができる|数値|
|get_roll()|なし|ドローンのロールを得ることができる|数値|
|get_yaw()|なし|ドローンのヨーを得ることができる|数値|
|emergency()|なし|緊急停止（4つのモータを停止させる|なし|

### プログラミング環境の起動方法
PuCharmの実行ボタン（左側の緑の三角）で動かせます。緊急停止したい場合は停止ボタン（右側の赤い四角）を押します。
<img width="239" alt="スクリーンショット 2020-01-27 12.02.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135880/66e36c63-e9a3-5591-1761-d9af990dd3f7.png">

### プログラミング環境でのプログラミング方法
実行すると，以下の画面が表示されます．
<img width="1440" alt="image2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135880/0a32d8a5-29d4-0c9d-1a05-2b095298f4f1.png">
画面の下部には，実行ボタンがあります．
【プログラム実行】ボタンで，gotello.pyに書いた命令が実行されます．
【撮影した写真表示】ボタンで，get_qrcodeおよびtake_picture関数で撮影した最新の写真が表示されます．
【終了】ボタンで，システム全体を終了しますが，バグでうまく終了できない場合があります．その場合は，PyCharmの停止ボタンを使って終了させます．
【ドローン緊急停止】ボタンで，ドローンのモーターを止めます．緊急時に押してください．

画面の上部左と上部中にあるグラフは，ToFおよびHeightのデータから作られています．ToFは床面からの距離，Heightは離陸地点の高さからの距離になります．
画面下部左と下部中にあるグラフは，機体温度とバッテリ残量です．
画面上部右は，ドローンとPCとの間の通信ログです．ここに，読み込んだQRコードの文字列も表示されます，
画面下部右は，【撮影した写真表示】ボタンを押すと表示されます．

ミニドローンのプログラミングを行う場合は，演習用ファイルの【ugoki.py】を編集し，保存すれば，この環境の【プログラム実行】ボタンを押すことでミニドローンを動かすことができます．全体を再度起動する必要はありません．

演習用ファイルの【ugoki.py】を編集は，PyCharmなどpythonのプログラミングに対応した環境をお勧めします．PyCharmは，関数などの入力の際に補完機能があるのでお勧めの一つです．

### 終了時の注意事項
映像を記録などした場合，ファイルへの書き込みなどが終わるなど処理が完了するまで，プログラムが完全に停止しません。すぐに，停止ボタンを押すとファイルへの書き込みが失敗する可能性があります。
完全に終わるまでお待ちください。映像を録画している場合は，約1分ほど待ってください。録画をしていない場合は，10秒ほどまってください。

緊急停止の場合，1回の停止ボタン操作で終わらない場合があります。その場合は，もう一度押してください。

### 参考資料等
#### 参考にさせて頂いた以下のサイトおよび管理者に感謝いたします。
[Tello SDK2.0](https://dl-cdn.ryzerobotics.com/downloads/Tello/Tello%20SDK%202.0%20User%20Guide.pdf)
[Tello3.py（SDK2.0サンプルプログラム）](https://dl-cdn.ryzerobotics.com/downloads/Tello/Tello%20SDK%202.0%20User%20Guide.pdf)
[Telloドローンでプログラミング！ーディープラーニングで物体認識編ー](https://mitomoyo.com/entry/2018/06/22/010142)
[Pythonの画像処理ライブラリpillowの使い方をわかりやすく解説！](https://www.sejuku.net/blog/62599#pillow)
[Anacondaを使ってMac OSにOpenCVをインストールする](https://qiita.com/reima21/items/ec0aa731139f600c5f46)
[Pythonでバーコードを読み込む](https://qiita.com/orengepy/items/1d405762813846765ec1)