3
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-27

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

概要

ミニ(ホビー)ドローンを使って小学生高学年から高等学校の児童生徒がプログラミングを学べる教材を開発しています。巻末の参考資料を参考に作成しています。ありがとうございます。

Tello Eduをpythonで動かそう!(SDK2対応)の改良版です。コマンドやインタフェースを変えています.

前バージョンからの主な変更点は以下の通りです.

Tell Eduとの間のコマンド通信ログ等 Tello Eduの状態がわかるインタフェースを作成

以下の画面に状態が表示されます.またここからドローンを実行できるようにしました.
image2.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]を利用するように設定しておく
スクリーンショット 2019-09-04 11.12.36.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本体のインストールが必要となる。

 pip install pyzbar

MacOSでzbarをインストールする方法

matplotlib

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

numpy

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

上記パッケージのインストールの際の注意点

  • バージョンに注意
  • Anacondaからインストールできない環境がありました.その場合は,PyCharmの設定にあるProjectInterpreterから設定できました.

ソースコード

ファイルの設置

PyCharmで作成したプロジェクト内に,以下のディレクトリの構成で各ファイルを配置してください。
最新のプログラムはこちらからダウンロードしてください.
Github 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対応ライブラリ(パッケージ)

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

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')
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)
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
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
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

メイン(プログラム)ファイル

 起動関数が定義されています。このファイルを実行してください。

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関数の中は,使えるコマンドなどを一通り試す目的でいれています。

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関数の内部で使える命令(コマンド)

コマンド                        引数                  説明 返値
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の実行ボタン(左側の緑の三角)で動かせます。緊急停止したい場合は停止ボタン(右側の赤い四角)を押します。
スクリーンショット 2020-01-27 12.02.55.png

プログラミング環境でのプログラミング方法

実行すると,以下の画面が表示されます.
image2.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
Tello3.py(SDK2.0サンプルプログラム)
Telloドローンでプログラミング!ーディープラーニングで物体認識編ー
Pythonの画像処理ライブラリpillowの使い方をわかりやすく解説!
Anacondaを使ってMac OSにOpenCVをインストールする
Pythonでバーコードを読み込む

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2