C#でTelloを操作する。TelloLibを使って。

#はじめに
[Tello](https://www.amazon.co.jp/%E3%80%90%E5%9B%BD%E5%86%85%E6%AD%A3%E8%A6%8F%E5%93%81%E3%80%91-%E3%83%88%E3%82%A4%E3%83%89%E3%83%AD%E3%83%BC%E3%83%B3-Tello-Powered-DJI/dp/B07979Q4YS/ref=sr_1_5?s=photo&ie=UTF8&qid=1546922044&sr=1-5&keywords=tello)を買ったらプログラムで操作したくなることに異論の余地はないと思います。

この記事ではC#でTelloを操作する方法について書きます。

Telloを操作する方法といっても、TelloにUDP通信でコマンドをbyte配列で送るなんてことをゴリゴリ書くのはチョットムズカシイため、あくまでそういったところをマルっとやってくれるライブラリを使って、ということになります。

#TelloLib
強い人がTelloをそうさするためのC#のライブラリをGitHubで公開してくれています。
それが`TelloLib`です。
これを使うととても簡単にC#でTelloを操作することができます。

#入手方法
[こちら](https://github.com/Kragrathea/TelloLib)にTelloLibとサンプルを含むVisual Studioのソリューションが公開されているので、`git clone`するかzipファイルをダウンロードしてください。

簡単なコンソールアプリやAndroidアプリのサンプルプロジェクトがあるので、それを動かしたりしてもよいですが、今回用があるのはこの中の`TelloLib`プロジェクトです。

#TelloLibをコンパイル
まずはTelloLibをコンパイルして、後で自分のプロジェクトから参照できるようにdllを生成しましょう。
Visual Studioで`aTello.sln`ファイルを開きます。

ソリューションエクスプローラーの`TelloLib`プロジェクト上で右クリックして、メニューから`ビルド`を実行してください。

<img src="https://qiita-image-store.s3.amazonaws.com/0/245438/7dc74220-23ea-965d-f542-557d3549c62c.png" title="" alt="image.png" width="60%" height="" >

dllファイルの出力先は、以下のようなソリューション構成であれば、以下のフォルダ内に`TelloLib.dll`という名前であります。

```
...\TelloLib\bin\Debug
```

この`TelloLib.dll`をこれから作る自分のプロジェクトで参照することで、このライブラリを利用できるようになります。



#プロジェクトを作る
これからTelloを操作するアプリケーションを作っていきます。
今回はシンプルなコンソールアプリケーションから離陸と着陸だけを行うプログラムを作成します。

Visual Studioを起動し、`コンソールアプリ（.NET Core） Visual C#`プロジェクトを作成します。
プロジェクト名はここでは`MyTelloConsole`とします。
もちろん何でもよいです。

<img src="https://qiita-image-store.s3.amazonaws.com/0/245438/74d403cc-7e22-5444-8276-ce4667e27e1e.png" title="" alt="image.png" width="60%" height="" >

#参照を追加する
プロジェクトに先ほど生成した`TelloLib.dll`ファイルへの参照を追加します。
ソリューションエクスプローラーの`依存関係`を右クリックして`参照の追加`を選択します。
<img src="https://qiita-image-store.s3.amazonaws.com/0/245438/b0f4dd4f-b53f-b01d-c034-ce34f1ef511a.png" title="" alt="image.png" width="60%" height="" >

`参照するファイルの選択`ダイアログが開くので、先ほど生成した`TelloLib.dll`ファイルを選択して`追加`ボタンを押します。
<img src="https://qiita-image-store.s3.amazonaws.com/0/245438/07221ee8-137f-7746-04fd-ef3170662e48.png" title="" alt="image.png" width="60%" height="" >

これでライブラリを利用する準備が整いました。


#プログラムを書いていく
`Main`メソッドに直接書いていくことにします。
今回のプログラムの流れは以下のとおりです。

- イベントハンドラを設定
- Telloとの通信初期化
- ループ内でコマンド入力→送信


##イベントハンドラを設定
Telloを操作するために使用するクラスは`Tello`クラスです。
このクラスには以下の3つのイベントがあります。

- onConnection
- onUpdate
- onVideoData

このうち今回必要なのは`onConnection`イベントだけですが、これら3つのイベントは`TelloLib`内ではイベントハンドラが設定されていない場合（つまりnullだった場合）の対処がされていないので、イベントハンドラを設定せずに実行すると、これらのイベントが発火されたときにnull参照で例外が発生することになります。

それらの例外はcatchされてコンソールにエラーメッセージが表示されてアプリケーション自体は動き続けるのですが、エラーメッセージが延々と出力され続けるのは困ります。

これを解決するにはライブラリ利用側で各イベントにイベントハンドラを設定するか、TelloLib側を修正して、それぞれのイベントが発火される箇所でnull条件演算子`?.`を使ってnull参照を回避する、という方法が考えられます。こんな感じで。

```:null条件演算子を使ったTelloLibの修正
onUpdate(100)　→　onUpdate?.Invoke(100)
```

今回はすべてのイベントにイベントハンドラを設定することにします。
するとこんな感じになります。

```csharp
using System;
using TelloLib;

namespace MyTelloConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Tello.onConnection += Tello_onConnection;

            Tello.onUpdate += Tello_onUpdate;

            Tello.onVideoData += Tello_onVideoData;
        }


        private static void Tello_onConnection(TelloLib.Tello.ConnectionState newState)
        {
        }

        private static void Tello_onVideoData(byte[] data)
        {
        }

        private static void Tello_onUpdate(int cmdId)
        {
        }
    }
}
```

この中で今回必要なのはonConnectionイベントのイベントハンドラ（ここでは`Tello_onConnection`）だけです。
このイベントハンドラはTelloとの通信のステータスが変化したときに実行されます。
このイベントの重要な役割は、Telloとの通信が開始されたらTelloの初期設定を行うことです。

このイベントハンドラは変化後のステータスが引数(`newState`)として渡されて実行されるので、この`newState`が`Connected`になったらTelloの初期設定が行われるようにします。

```csharp
        private static void Tello_onConnection(TelloLib.Tello.ConnectionState newState)
        {
            if (newState == Tello.ConnectionState.Connected)
            {
                Tello.queryAttAngle();//???
                Tello.setMaxHeight(1);//最大高度？メートル？
            }
        }
```

このコードについてはサンプルプログラムの`TelloConsole`内の`Main`メソッドを参照してください。
`setMaxHeight`メソッドはわかります。可能最大高度を指定するのでしょう。メートル単位で。
しかし`queryAttAngle`メソッドがわかりません。わかりませんがきっと必要と思われるので、ここでは**おまじない**としておきます。

#Telloとの通信を開始
Telloとの通信は以下の1行で開始されます。
シンプルでよろしいですね。
これを`Main`メソッド内に追加します。

```csharp
            Tello.startConnecting();//接続開始
```

詳しいことはおいといて、この接続処理によって前述の通信ステータスが`Connected`になったときに`onConnection`イベントが発火されて、設定したイベントハンドラによってTelloの初期設定が行われることになります。

この時点で`Main`メソッドは以下のとおりです。

```csharp
        static void Main(string[] args)
        {
            Tello.onConnection += Tello_onConnection;

            Tello.onUpdate += Tello_onUpdate;

            Tello.onVideoData += Tello_onVideoData;

            Tello.startConnecting();//接続開始
        }
```

#コマンド送信
whileループでユーザーからのコマンド入力を受け付け、それに応じてTelloを操作するようにします。
基本的な形は以下のとおりです。

```csharp
            var inputStr = "";
            while (inputStr != "exit")
            {
                inputStr = Console.ReadLine().ToLower();

                //inputStrの内容に応じた処理を書く
            }
```

今回は以下の3つのコマンドを受け付けるようにします

- takeoff　離陸
- land　着陸
- exit　アプリケーションの終了（ループから抜ける）


離陸と着陸については`TelloLib`にそれぞれ`takeOff`メソッドと`land`メソッドが用意されていて、これらを呼び出すだけでよくなっています。

ユーザーからの入力に加えて、Telloとの通信ステータス、そして飛行中か否かのステータスを条件としてそれぞれのメソッドを実行するようにします。
加えて、`exit`と入力されたときにループから抜けてアプリケーションが終了するようにします。

```csharp
            var inputStr = "";
            while (inputStr != "exit")
            {
                inputStr = Console.ReadLine().ToLower();

                //離陸
                //Telloと接続中、かつ、飛行していない
                if (inputStr == "takeoff" && Tello.connected && !Tello.state.flying)
                {
                    Tello.takeOff();
                }

                //着陸
                //Telloと接続中、かつ、飛行中
                if (inputStr == "land" && Tello.connected && Tello.state.flying)
                {
                    Tello.land();
                }
            }

            //とりあえず着陸命令だしておく
            Tello.land();

```

以上で、離陸と着陸だけするプログラムが完成しました。
完成した`Main`メソッドは以下のようになります。

```csharp
        static void Main(string[] args)
        {
            Tello.onConnection += Tello_onConnection;

            Tello.onUpdate += Tello_onUpdate;

            //Tello.onVideoData += Tello_onVideoData;


            Tello.startConnecting();//接続開始

            var inputStr = "";
            while (inputStr != "exit")
            {
                inputStr = Console.ReadLine().ToLower();

                //離陸
                //Telloと接続中、かつ、飛行していない
                if (inputStr == "takeoff" && Tello.connected && !Tello.state.flying)
                {
                    Tello.takeOff();
                }

                //着陸
                //Telloと接続中、かつ、飛行中
                if (inputStr == "land" && Tello.connected && Tello.state.flying)
                {
                    Tello.land();
                }
            }

            //とりあえず着陸命令だしておく
            Tello.land();
            //これだと、命令が送られる前にアプリケーションが終了してしまうので、
            //ちょっと待つ
            Thread.Sleep(1000);
        }
```

では実際にTelloを操作してみましょう。

#やってみよう
##TelloとPCを接続
ここでいう接続とはTelloとPCをWifi接続するということです。

まず、Telloの電源を入れます。
しばらく待つと、LEDが黄色く速く点滅を繰り返すようになります。
Telloが起動しましたが、端末との接続がない状態です。

次にPC側です。
PCが拾っているWifiのアクセスポイントのリストに「TELLO-****」という名前のアクセスポイントが現れるので、それを選択して接続します。

「接続済み」となればOKです。

この時点ではTelloは相変わらずLEDが黄色く速く点滅していて何の変化もみられませんが大丈夫です。

##コンソールアプリケーションから操作
いよいよコンソールからTelloを操作してみましょう。
といっても離着陸だけですが。

実行してみてください。

コンソールが表示され、`Connected!`と表示されたら、`takeoff`と入力してエンターキーを勢いよく押しましょう。
離陸しましたか？
結構、おおー、って思っちゃいますよね。

続けて`land`と入力してこれまた勢いよくエンターキーを押しましょう。スターン！
着陸しましたか。



#おわりに
今回は離着陸のみの簡単なものでしたが、これじゃなんだか物足りませんよね。

次は前進、後退、左回転、右回転について書きたいと思います。