Go + ドローンで最高の世界へ飛び立つ その1 導入編

本記事は [オルトプラス Advent Calendar 2023](https://qiita.com/advent-calendar/2023/altplus2023) の12/08の記事です。

# はじめに
こんにちは！オルトプラスの中村です!
普段はサーバサイドがメインですが、フロントエンドやインフラ周りなども触りつつ、業務外では興味あるものがあったら突撃する雑食系です。
今回は趣味で始めたドローンで面白そうなことができそうなので、まずは導入編として書いてみます。
教育現場でもプログラミングの入り口としてドローンを使用することもあるそうです。
なので本記事ではそのような初学者あたりの方からIT関係ないワクワクしたいおじさんまで幅広く楽しんでいただけたらと思っています。

# 概要
Mac PCでDJI TelloのドローンをGoフレームワークGobotで操作していきます。
より応用した操作や映像周りに関しては次回の記事の[Go + ドローンで最高の世界へ飛び立つ その2 操作&映像編](https://qiita.com/ka_nakamu/items/823963327f49e3285892)で触れようと思います。
それぞれの環境等は、詳細の冒頭あたりで説明していきます。

# 詳細
## 環境
### PC
- OS: Mac (Ventura 13.3)
- エディタ: VSCode (Visual Studio Code)

化石なPCじゃなければ基本なんでも良いと思います。MacでもWindowsでも。

### Go
- バージョン: go1.21.4 darwin/arm64
  
  :::note warn
  できれば`1.16`以降のバージョンを推奨しておきます。
  パッケージ管理が1.10以前ではGOPATHモードのみだったのが、1.11からはモジュール対応モードが加わり、1.16から現在はこちらが主流となっています。
  本記事も現バージョン前提で記載しています。
  :::
  
  Homebrewでインストールする場合
  ```terminal:terminal
  $ brew install go
  ```

  [公式のインストーラー](https://go.dev/doc/install)からでもokです。
  インストールが終えたら正常に完了したかバージョンを確認してみてください。
  
  ```terminal:terminal
  $ go version
  go version go1.21.4 darwin/arm64
  ```
<br>

- フレームワーク: [Gobot](https://gobot.io/)
  Gobotは2023年現在、35のプラットフォームをサポートしたIoTフレームワークです。
  今回のドローン(DJI Tello)も[プラットフォーム一覧](https://gobot.io/documentation/platforms/)の中にありますね！
  別のドローンもあったり、他にも様々なロボティクスなモノに対応していることが確認できます。
  急速に発展してきているAIとのシナジーも高い分野なので今後も注目されるフレームワークかなと思います。

### ドローン
- 製品: [DJI Tello](https://store.dji.com/jp/product/tello?vid=38421)
- スペック: [公式](https://www.ryzerobotics.com/jp/tello/specs)
  購入したのはTelloの無印です。
  他にTello Eduというより教育向けのシリーズもあるのですが、現在は購入できないようです。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/d746c293-8778-82b4-f7fd-3937661701f1.png)

  DJIから販売されているドローンですが開発したのはRyze Tech社(Ryze Technology)になります。
  > Ryze Technologyは、2017年に中国の深センで設立されたテック系スタートアップカンパニーです。私たちのゴールは、ドローンフライトをより楽しくワクワクするものにすることです。遊び心こそが学びの最大の基盤だと、私たちは考えています。ドローンについて学ぶこと、それは、とても楽しくて面白いことなのです。私たちの初めてのドローン「Tello」は、まさにそういったコンセプトから生まれました。DJI製のフライトコントロール システムとIntelプロセッサーを搭載したTelloは、業界をリードする技術とエンターテイメント性を詰め込んだ、かんたん操作ドローンです。
<br>

- 規制
  ここを気にする方も多いかと思います。
  今回のTelloは重量が約80gのトイドローンです。
  2023年時点では以下のようになっています。
  > 「トイドローン」とは、一般的に「本体とバッテリーの重さの合計が100g未満の小型ドローン」のことです。
  100g以上のドローンは、「航空法」によって飛ばせる場所や方法が規制されていますが、100g未満のトイドローンには航空法が適用されません。

  100g未満だと小型無人機、100g以上では無人航空機という扱いだそうです。
  トイドローン(小型無人機)では以下が不要になります。
  - 特定飛行の許可申請
  - 機体登録
  - リモートIDの搭載

  リモートIDってなんなん？みたいな説明は省きますが、要はめんどくさいことはあまり考えず気軽に遊びやすいドローンって感じです。
  ただしトイドローンでも飛行禁止エリアはあるので屋外で飛ばす際は気をつけてください。
  
  参考: [【2023年版】ドローン規制の全体像｜全5大規制をわかりやすく解説](https://drone-navigator.com/drone-regulation)

<br>

- 開封
  (記事を書く際に開封しようと思っていたのでやっと開封できる…！)
  サイズ比較
  ![IMG_2443.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/088303f4-1c23-a4b1-07dc-357a1e3466f1.jpeg)
  箱の大きさは皆んな大好きリーダブルコードより少し大きい程度です。箱は薄め。
  同じタイミングでDJI MINI 4 PROも買ったのでサイズ感確認していきます。
  
  ![IMG_2445.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/7686d7c1-5bfa-b262-74d6-cbd21c859f1f.jpeg)
  Telloの本体ちっちゃい！お寿司くらいです🍣
  MINIは折り畳んだ状態ですが比較すると結構デカいですね。
  Telloを充電しながらコードの方を準備していきます。

## 操作していく
### 準備
- ディレクトリの作成
  この辺りは自由ですが、今回は自分の作業用ディレクトリにドローン用のディレクトリを作成するような想定でいこうと思います。
  作成したドローンディレクトリに移動しておきます。
  ```terminal:terminal
  [~/my_app]
  $ mkdir drone_tello_app && cd drone_tello_app

  [~/my_app/drone_tello_app]
  $ 
  ```
  このタイミングくらいでVSCodeで開こうと思います。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/daed0629-5470-fb81-014a-0d97d20dfaea.png)

<br>

- Goモジュールの初期化
  `go mod init　{project_name}`を実行します。
  ```terminal:terminal
  [~/my_app/drone_tello_app]
  $ go mod init drone_tello_app
  ```
  
  実行後、サイドバーのツリーにgo.modが作成されたらokです。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/44e0ab67-15d1-d14b-196e-db744a66b626.png)

  :::note info
  GoではGo Modulesによってパッケージを管理します。
  Node.jsならnpm、Pythonならpipで管理しているアレです。
  Go Modulesを利用する際には`go mod`コマンドを使用します。
  作成された`go.modファイル`にはモジュール名や他のモジュールとの依存関係が定義されています。
  外部パッケージを利用すると`go.sumファイル`も作成されます。(後で作成します)
  :::

<br>

- TELLO APPのインストール (スキップ可)
  App Storeにてアプリをインストールします。
　　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/498dc625-6561-aa4c-aaeb-1397bd9c216d.png)

  アプリを起動し、規約をチェックしたり権限周りを許可していくと接続方法の画面が表示されました。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/e8937749-7252-56cd-0696-10208b844263.png)

  方法に沿って本体の電源を入れた後にTelloに接続します。
  接続後、ガイドが表示され、ガイドを閉じるとドローンのカメラ映像とActivationのダイアログが表示されたのでそのままActivateしました。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/c0c4e6c4-2f10-53db-f760-6a482a31a596.png)
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/6af2661f-a9b4-8a06-3802-616e3dc428f1.png)
  カメラ映像の画質は荒めですが、オモチャとして考えれば十分すぎる気がします。
  設定の詳細にファームウェアのアップデートが出来たのでついでにしておきました。
  もうなんかアプリからのボタン操作でドローンが動きそうな気配しかしないですが、初めての離陸はGoに譲りたいため我慢します…！
  アプリの方は終了しておきます。これで準備は一通り完了したのかなと思います。

### Gobotによる操作
いよいよ飛ばしていきます！
[GobotのプラットフォームにあるDJI Telloページ](https://gobot.io/documentation/platforms/tello/)を確認しつつ試していきたいと思います。

:::note warn
上記ページにてHow to Installとして`go get -d -u gobot.io/x/gobot/...`を実行してねと書いてありますが、今回の記事では実行せず後で`go mod tidy`を実行したいと思います。
`go get`や`go install`コマンドについては触れません(たぶん)
:::

- サンプルコードの実施
  実行用のファイルとして`main.go`を作成します。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/fad70454-8b89-613b-3c8a-bdb9a25e373c.png)

  Telloのプラットフォームページにある「How to Use」のサンプルコードを参考に`main.go`のファイルに貼ります。
  fmtは今のところ不要なので削除。(インデントが多い…)
  ```go:main.go
    package main

    import (
            "time"
    
            "gobot.io/x/gobot"
            "gobot.io/x/gobot/platforms/dji/tello"
    )
    
    func main() {
            drone := tello.NewDriver("8888")
    
            work := func() {
                    drone.TakeOff()
    
                    gobot.After(5*time.Second, func() {
                            drone.Land()
                    })
            }
    
            robot := gobot.NewRobot("tello",
                    []gobot.Connection{},
                    []gobot.Device{drone},
                    work,
            )
    
            robot.Start()
    }
  ```
  main.goにコードを貼ったら保存して`go mod tidy`を実行してください。
  すると`go.sumファイル`が作成され、`go.modファイル`も内容が更新されたかと思います。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/c4dbb929-d845-a648-b7b8-ae1d3dd46a11.png)

  :::note info
  `go mod tidy`はソースコードでimportして使用しているものが`go.mod`や`go.sum`に依存関係が定義されていないものがあれば追加し、逆に`go.mod`や`go.sum`に定義しているがimportで使用していないものがあれば削除します。
  なので必要最低限の依存関係で整理整頓してくれるありがたいコマンドです。
  :::

  では、`main.go`を実行していきたいと思います。
  実行前に以下をチェックしてください。
  - [x] telloの電源が入っていること
  - [x] telloのWi-Fiに接続できていること
  - [x] telloを床など安定した所に置いていること
  
  もろもろokなら以下を実行します。
  ```terminal:terminal
  [~/my_app/drone_tello_app]
  $ go run main.go
  ```
  
  <blockquote class="twitter-tweet"><p lang="zxx" dir="ltr"><a href="https://t.co/Agmq4K1iu0">pic.twitter.com/Agmq4K1iu0</a></p>&mdash; ka-na (@nakamu_ka) <a href="https://twitter.com/nakamu_ka/status/1728791452304466168?ref_src=twsrc%5Etfw">November 26, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
  
  とんだァ!!!
  離陸と着陸のみですが地点がそこそこズレたので、なるべく周囲に何も無いところで実行したほうが良いかもです。
  `main.go`を実行してもドローンが反応しない場合は、TELLO APPのせいかもしれませんのでTELLOのアプリを終了してみてください。

<br>

- 処理内容の確認
  あっさりしたコードに見えますが何をしていたのでしょうか。
  main関数で何をしているか少しだけみていきます。
  `drone := tello.NewDriver("8888")`

  ```go:driver.go
    func NewDriver(port string) *Driver {
    	d := &Driver{name: gobot.DefaultName("Tello"),
    		reqAddr:   "192.168.10.1:8889",
    		respPort:  port,
    		videoPort: "11111",
    		Eventer:   gobot.NewEventer(),
    		doneCh:    make(chan struct{}, 1),
    	}
    
    	d.AddEvent(ConnectedEvent)
    	d.AddEvent(FlightDataEvent)
    	d.AddEvent(TakeoffEvent)
    	d.AddEvent(LandingEvent)
    	d.AddEvent(PalmLandingEvent)
    	d.AddEvent(BounceEvent)
    	d.AddEvent(FlipEvent)
    	d.AddEvent(TimeEvent)
    	d.AddEvent(LogEvent)
    	d.AddEvent(WifiDataEvent)
    	d.AddEvent(LightStrengthEvent)
    	d.AddEvent(SetExposureEvent)
    	d.AddEvent(VideoFrameEvent)
    	d.AddEvent(SetVideoEncoderRateEvent)
    
    	return d
    }
  ```
  `drone`の変数は短縮記法`:=`によってtelloパッケージのNewDriver関数を呼び、Driverポインタ型の値が代入されるとともに型も定めています。型推論的なやつです。
  引数に入れていたのはレスポンスのポートだったんですね。

  :::note info
  `:=`はセイウチ演算子とも呼ばれています。横向きですが目とキバに見えるからです。
  ちょっと気持ち悪い記法だと思えていたのがちょっと可愛く見えてきます。
  :::
  

  次が実質メインの処理で、ドローンに何をさせるかが書いてあります。
  ざっくり、離陸して着陸するって感じでしょうか。
  ```go:main.go
  work := func() {
          drone.TakeOff()

          gobot.After(5*time.Second, func() {
                  drone.Land()
          })
  }
  ```
  離陸させるTakeOffのメソッドはどんな感じなのか確認します。
  ```go:driver.go
    func (d *Driver) TakeOff() (err error) {
    	buf, _ := d.createPacket(takeoffCommand, 0x68, 0)
    	d.seq++
    	binary.Write(buf, binary.LittleEndian, d.seq)
    	binary.Write(buf, binary.LittleEndian, CalculateCRC16(buf.Bytes()))
    
    	_, err = d.cmdConn.Write(buf.Bytes())
    	return
    }
  ```
  このメソッドは`(d *Driver)`のポインタレシーバを持っています。
  なので`*Driver`のインスタンスに対して呼び出すことができます。
  今回だと最初に定義した`drone`のインスタンスに対して呼べるので`drone.TakeOff()`となっていたんですね。
  とりあえず`drone`のインスタンスにメソッドチェーンでドローンを動かすメソッドを書けば動く！ってのが分かりました。
  
  ```go:main.go
	robot := gobot.NewRobot("tello",
		[]gobot.Connection{},
		[]gobot.Device{drone},
		work,
	)

	robot.Start()
  ```
  ここでは`drone`と`work`を元にtelloのインスタンスである`robot`を定義し、そのインスタンスで`Start`メソッドを呼び出しています。上記でやっていた`TakeOff`と同じような感じですね。

<br>

- キャリブレーションしてみる
  離陸と着陸地点に差異があったので調整できるか調べたところ、以下とのことなのでやってみます。
  > 機体のフライトが安定しない（ふらつく、離陸時まっすぐ上昇しない、機体が流れる、離陸できない等）場合には、Telloアプリを利用しIMUキャリブレーション作業を行う事で改善可能な場合があります。

  [公式の動画一覧](https://www.ryzerobotics.com/tello/videos)の「Calibrating the IMU」にて動画でも方法が確認できます。
  尚、キャリブレーション作業はTELLO APPからのみ可能です。
  
  ![IMG_2473.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/61c8539b-a441-0720-1b12-0842dd090ef9.jpeg)
  ただプロペラ外しただけなのにイジってるぜ感高まります。
  
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/84c529dd-fdf1-ac45-18ce-bcd4413dc20d.png)
  アプリでガイドしてもらいながら簡単にキャリブレーションとやらが出来ました。

  :::note warn
  ベストを追求してキャリブレしまくってたら一時飛ばなくなって焦りました。
  結局ブレます！よほど狂った飛行しない限りはやらなくてokかもです。
  :::

<br>

- ドローンの他のアクションを試す
  今は離陸と着陸しかないので他のアクションも試していきます。
  他にどんなアクションがあるかは、`driver.goファイル`のメソッドを直接確認していくか、
  [telloのパッケージのドキュメント](https://pkg.go.dev/gobot.io/x/gobot/platforms/dji/tello#Driver.BackFlip)を確認すれば良いかと思います。

  私はとりあえずこのフリップアクションあたりが面白そうなので`FrontFlip`と`BackFlip`を試してみます…！
  ```go:driver.go
    // Flip tells drone to flip
    func (d *Driver) Flip(direction FlipType) (err error) {
    	buf, _ := d.createPacket(flipCommand, 0x70, 1)
    	d.seq++
    	binary.Write(buf, binary.LittleEndian, d.seq)
    	binary.Write(buf, binary.LittleEndian, byte(direction))
    	binary.Write(buf, binary.LittleEndian, CalculateCRC16(buf.Bytes()))
    
    	_, err = d.cmdConn.Write(buf.Bytes())
    	return
    }
    
    // FrontFlip tells the drone to perform a front flip.
    func (d *Driver) FrontFlip() (err error) {
    	return d.Flip(FlipFront)
    }
    
    // BackFlip tells the drone to perform a back flip.
    func (d *Driver) BackFlip() (err error) {
    	return d.Flip(FlipBack)
    }
    
    // RightFlip tells the drone to perform a flip to the right.
    func (d *Driver) RightFlip() (err error) {
    	return d.Flip(FlipRight)
    }
    
    // LeftFlip tells the drone to perform a flip to the left.
    func (d *Driver) LeftFlip() (err error) {
    	return d.Flip(FlipLeft)
    }
  ```

  関数型work変数の無名関数内を修正します。
  ```go:main.go
	work := func() {
		drone.TakeOff()

		gobot.After(5*time.Second, func() {
			drone.FrontFlip()
		})

		gobot.After(10*time.Second, func() {
			drone.BackFlip()
		})

		gobot.After(15*time.Second, func() {
			drone.Land()
		})
	}
  ```
  離陸から5秒後にフロントフリップ、10秒後にバックフリップ、15秒後に着陸としてみました。
  荒ぶりそうですがどうなるでしょうか。。

  また、あわせて様々なフライトデータが定義してあったので取得してみます。
  ```go:driver.go
    // FlightData packet returned by the Tello
    type FlightData struct {
    	BatteryLow               bool
    	BatteryLower             bool
    	BatteryPercentage        int8
    	BatteryState             bool
    	CameraState              int8
    	DownVisualState          bool
    	DroneBatteryLeft         int16
    	DroneFlyTimeLeft         int16
    	DroneHover               bool
    	EmOpen                   bool
    	Flying                   bool
    	OnGround                 bool
    	EastSpeed                int16
    	ElectricalMachineryState int16
    	FactoryMode              bool
    	FlyMode                  int8
    	FlyTime                  int16
    	FrontIn                  bool
    	FrontLSC                 bool
    	FrontOut                 bool
    	GravityState             bool
    	VerticalSpeed            int16
    	Height                   int16
    	ImuCalibrationState      int8
    	ImuState                 bool
    	LightStrength            int8
    	NorthSpeed               int16
    	OutageRecording          bool
    	PowerState               bool
    	PressureState            bool
    	SmartVideoExitMode       int16
    	TemperatureHigh          bool
    	ThrowFlyTimer            int8
    	WindState                bool
    }
  ```
  `FlightData`はイベントハンドラーで取得できるようです。
  `FlightDataEvent`は先ほどtelloパッケージの`NewDriver`の関数を確認した時に`d.AddEvent(FlightDataEvent)`とありましたね。
  今回は飛行高さ`Height`とバッテリー残量(%)`BatteryPercentage`を取得してターミナルに表示したいと思います。
  
  最終的にコードは以下になりました。
  ```go:main.go
    package main
    
    import (
    	"fmt"
    	"time"
    
    	"gobot.io/x/gobot"
    	"gobot.io/x/gobot/platforms/dji/tello"
    )
    
    func main() {
    	drone := tello.NewDriver("8888")
    	var flightData *tello.FlightData
    	var battery int8
    
    	work := func() {
    		fmt.Println("TakeOff")
    		drone.TakeOff()
    
    		drone.On(tello.FlightDataEvent, func(data interface{}) {
    			flightData = data.(*tello.FlightData)
    			battery = flightData.BatteryPercentage
    			fmt.Println("Height:", flightData.Height)
    		})
    
    		gobot.After(5*time.Second, func() {
    			fmt.Println("FrontFlip")
    			drone.FrontFlip()
    		})
    
    		gobot.After(10*time.Second, func() {
    			fmt.Println("BackFlip")
    			drone.BackFlip()
    		})
    
    		gobot.After(15*time.Second, func() {
    			fmt.Println("Land")
    			drone.Land()
    
    			fmt.Println("Battery:", battery)
    		})
    	}
    
    	robot := gobot.NewRobot("tello",
    		[]gobot.Connection{},
    		[]gobot.Device{drone},
    		work,
    	)
    
    	robot.Start()
    }
  ```
  
  <blockquote class="twitter-tweet"><p lang="zxx" dir="ltr"><a href="https://t.co/14yPckqL1q">pic.twitter.com/14yPckqL1q</a></p>&mdash; ka-na (@nakamu_ka) <a href="https://twitter.com/nakamu_ka/status/1728792059627045101?ref_src=twsrc%5Etfw">November 26, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

  まわったァ!!!
  Gobotのコード見る前は半フリップで逆さ飛行とか試したいと思っていましたがそれは無理そうですね。。
  
  フライトデータはこんな感じでした。(バックフリップ以降)
  ```terminal:terminal
    BackFlip
    Height: 9
    Height: 9
    Height: 9
    Height: 10
    Height: 11
    Height: 11
    Height: 10
    Height: 9
    Height: 8
    Height: 8
    Height: 8
    Height: 8
    Height: 8
    Height: 8
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 10
    Height: 10
    Height: 10
    Height: 10
    Height: 10
    Height: 10
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Land
    Battery: 67
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 9
    Height: 8
    Height: 8
    Height: 8
    Height: 7
    Height: 6
    Height: 6
    Height: 5
    Height: 4
    Height: 4
    Height: 3
    Height: 3
    Height: 2
    Height: 2
    Height: 2
    Height: 1
    Height: 1
    Height: 0
    Height: 0
    Height: 0
    Height: 0
  ```
  思っていたほどバックフリップ時のHeight値は変動しなかったです。
  こんな感じでアクション系のメソッドを色々試してみてください。

  いったん今回の記事ではここまでにしたいと思います！

# まとめ
今回は導入編ということで、
- [x] Goのインストールやパッケージ管理
- [x] Gobotフレームワークの活用
- [x] ドローン(Tello)について
- [x] プログラミングによる飛行体験

この辺りが分かったんじゃないかなと思います…！ワクワクしてもらえてたら幸いです。
次回はドローンの操作方法をより直感的にしてみたり、ドローンのカメラ側についても触れていけたらと思います。
それでは、最後までご覧いただきありがとうございました。

次回の記事はこちら
[Go + ドローンで最高の世界へ飛び立つ その2 操作&映像編](https://qiita.com/ka_nakamu/items/823963327f49e3285892)