PythonによるTello操作（基本、及びクラウドからのMQTTによる操作まで）

# Telloとは


Ryze TechとDJIが共同開発した12,800円の小型トイドローンで、APIが公開されているのので誰でも簡単にプログラムによって操作することができます。

https://www.ryzerobotics.com/jp/tello
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132165/7c6bb381-0bbb-336f-4456-dea26c9f98a5.png)


APIには下記二種類があります。

1. 公開されている標準コマンド。WiFiで経由でTelloのアドレスの8887/udpにテキストコマンドを送信するだけ。機能制限があります。
2. 通称バイナリーコマンド。公開されていないが、世界中のTelloマニアによって仕様が明らかになったもので、基本的にはなんでもできます。

今日は大人しく１の標準コマンドを利用しましょう。真面目に書けばもっとありますが、操作に必要なのはざっくりとこんな感じです。詳細は[こちら](https://terra-1-g.djicdn.com/2d4dce68897a46b19fc717f3576b7c6a/Tello%20%E7%BC%96%E7%A8%8B%E7%9B%B8%E5%85%B3/For%20Tello/Tello%20SDK%20Documentation%20EN_1.3_1122.pdf)へ

| コマンド |仕様 |
|:--|:--|
| command | コマンドモードへ  |
| takeoff | 離陸 |
| land | 着陸 |
| up xx | xx cm上昇(20 < xx < 500) |
| down xx | xx cm下降(20 < xx < 500) |
| left xx | 左へxx cm移動(20 < xx < 500) |
| right xx | 右へxx cm移動(20 < xx < 500) |
| forward xx | 前へxx cm移動(20 < xx < 500) |
| back xx | 後ろへxx cm移動(20 < xx < 500) |
| cw xx | 時計回りにxx°回転(1 < xx < 3600) |
| ccw xx | 反時計回りにxx°回転(1 < xx < 3600) |
| flip x | 宙帰り<br>l (左方向)<br/>r (右方向)<br/>f (前方)<br/>b (後方) |

# 入門編：離着陸
単純に8889/udpに必要なコマンドを投げるだけです。

```python
#!/usr/bin/env python

import socket
import time

tello_ip = '192.168.10.1'
tello_port = 8889

#udpソケット作成
socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
tello_address = (tello_ip , tello_port)

#コマンドモードを使うため'command'というテキストを投げる
socket.sendto('command'.encode('utf-8'),tello_address)

#5秒まつ
time.sleep(5)

#離陸のため'takeoff'というテキストを投げる
socket.sendto('takeoff'.encode('utf-8'),tello_address)
#5秒まつ
time.sleep(5)
#着陸のため'land'というテキストを投げる
socket.sendto('land'.encode('utf-8'),tello_address)
```
はい簡単。入門編終了。

#応用編：クラウドのIoT PlatformからMQTTで操作

##概要図
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132165/a4c09e43-00f6-78bd-6abf-c50496be8b0e.png)
##
PCがドローンのコントローラーとなり、MQTTクライアントとしてMQTT Brokerへ接続します。MQTT BrokerをSaaSとして提供しているクラウドも色々ありますが、ここでは手元にあるアリババクラウドを利用します。アリババクラウドのIoT platformへMQTT接続するときのパラメータの準備は[「ESP32 〜 Alibaba Cloud IoT Platform をMQTT接続(Arduino Core版)」](https://qiita.com/makotaka/items/388fa1ee0eb1f0237012)で紹介しています。

図中PCと書いてありますが、ラズパイでもOK。ただ、TelloがWiFiのAPとして動く仕様で、ネットワークインターフェースのうちの一つはTelloとしか接続できないので、クラウドに接続するためにもう一つネットワークインターフェースが必要です。私はiPhoneのUSBテザリングを使いました。Telloと接続するネットワーク（デフォルトでは192.168.10.0/24）以外は全てiPhoneのusbテザリングを通す必要があります。OSXでの設定では、Internet経由でクラウドへ接続するインターフェイスの優先順位をTelloと接続するWiFiより上にする必要があります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132165/0975efb6-b895-2613-e30d-36660aa3ec64.png)

##コード
```python
#!/usr/bin/env python

import socket
import time
import paho.mqtt.client as mqtt

TELLO_IP = '192.168.10.1'
TELLO_PORT = 8889

# 以下はMQTT Brokerの仕様に応じて書き換えてね。
BROKER = 'MQTT BrokerのIPもしくはFQDN'
CLIENTID = 'クライアントID'
USER = 'ユーザーID'
PASS = 'パスワード'
TOPIC = '購読するトピック'

# subscribeしたtopicでメッセージを受けたらそのままTelloへ送る
def on_message(client, socket, msg):
    print(str(msg.payload))
    socket.sendto(str(msg.payload).encode('utf-8'),tello_address)

def on_connect(client, userdata, flags, rc):
    print("Connected with result code "+str(rc))

if __name__ == '__main__':
  tello_address = (TELLO_IP , TELLO_PORT)
  socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  socket.sendto('command'.encode('utf-8'),tello_address)

  client = mqtt.Client(client_id=CLIENTID,userdata=socket, protocol=mqtt.MQTTv311)
  client.username_pw_set(USER, password=PASS)
  client.on_message = on_message
  client.on_connect = on_connect
  client.connect(BROKER, port=1883, keepalive=30)
  client.subscribe(TOPIC)
  client.loop_forever()
```

##動作確認
ここれはAlibaba Cloudのコンソールから'takeoff'というコマンドをpusblishして離陸させる例です。細かい画面遷移は省略します。'land'をpublishすれば着陸します。簡単ですね。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/132165/0ae31c02-4023-ed7e-f39d-13914bf9ba2b.png)

#まとめ
##所感
10分ぐらいでバッテリー切れるので充電かバッテリー交換必要。人が近くにいなければならないので超低消費電力待機モードとか、ワイヤレス充電機能とかがないとそもそもクラウドから遠隔操作する意味が^^;
PCやラズパイまでなくてもESP32を二つ(WiFi環境がTello用とインターネット接続用の二つ必要なため)使ってArduino Coreでも動かせるのではないでしょか。
手元のクラウドを使いましたが、MQTT Brokerとしてmosquitto、Publisherとしてmosquito_pubを使ったテストもできるかと。

##最後に
これは私の趣味の活動です。所属する団体の考え方は全く反映していません。