Go + ドローンで最高の世界へ飛び立つ その2 操作&映像編

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

# はじめに
こんにちは！オルトプラスの中村です!
本記事は[Go + ドローンで最高の世界へ飛び立つ その1 導入編](https://qiita.com/ka_nakamu/private/8feb27cecd63bdc93124)の続編となります。
プログラミング対応のドローンに興味ある方や、Goに興味あって何かしら手を動かす目的が欲しかった方や、IT関係ないワクワクしたいおじさんみたいな方は是非その1からご覧いただけたら嬉しいです。

# 概要
Mac PCでDJI TelloのドローンをGoフレームワークGobotで操作していきます。
前回は導入からドローンの一連のアクションを命令形式で記述し、それを実行して飛ばしました。
今回は大きく分けて以下の2つをやっていきたいと思います。
- ドローンそれぞれのアクションをキーボードの入力から行えるようにする
- Telloのカメラを利用して映像周りの機能に触れる

# 詳細
## 環境
- OS: Mac (Ventura 13.3)
- エディタ: VSCode (Visual Studio Code)
- ドローン: DJI Tello 無印(ボディ上面が白色だと無印です)
- Go: go1.21.4 darwin/arm64
- Gobot: v1.16.0

## 前回までのおさらい

```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()
}
```
- [x] telloの電源が入っていること
- [x] telloのWi-Fiに接続できていること
- [x] telloを床など安定した所に置いていること

上記を確認し`go run main.go`を実行すれば、`drone`インスタンスに対して`work`に記載したアクションのメソッドを、特定の時間後に関数呼び出しをスケジュールする`After`を用いて行っていました。
以上！！

## 操作編
### キーボード操作
- キーボードパッケージの導入
    `tello`のパッケージを使用するために`gobot.io/x/gobot/platforms/dji/tello`をimportしていたのと同じで、
    キーボードのパッケージを使用したいので`gobot.io/x/gobot/platforms/keyboard`をimportします。
    [Gobotのgithubのkeyboard](https://github.com/hybridgroup/gobot/tree/master/platforms/keyboard)のREADMEにHow to Useがあるので確認します。
    ```go
    package main
    
    import (
        "fmt"
    
        "gobot.io/x/gobot"
        "gobot.io/x/gobot/platforms/keyboard"
    )
    
    func main() {
        keys := keyboard.NewDriver()
    
        work := func() {
            keys.On(keyboard.Key, func(data interface{}) {
                key := data.(keyboard.KeyEvent)
    
                if key.Key == keyboard.A {
                    fmt.Println("A pressed!")
                } else {
                    fmt.Println("keyboard event!", key, key.Char)
                }
            })
        }
    
        robot := gobot.NewRobot("keyboardbot",
            []gobot.Connection{},
            []gobot.Device{keys},
            work,
        )
    
        robot.Start()
    }
    ```
    これまでのコードと似たような構成になっていませんか？分かりやすい…。
    見た感じ「Aかそれ以外か」みたいなイケメンが言いそうなことをしています。

    試しにkey.goというファイルを作成し、そこにコードを貼って実行してみました。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619988/5672268d-28ab-9c35-7b8d-2d6527972d1c.png)
    想定通りの挙動に見えます。A以外の出力結果が気になります。
    > `fmt.Println("keyboard event!", key, key.Char)`
    ↓ 出力結果
    `keyboard event! {[115 0 0] 115 s} s`
    
    `key.Char`は入力したキーです。「`s`」が出力されています。
    `{[115 0 0] 115 s}`は`key`インスタンス(`KeyEvent`)の構造体です。
    KeyEventを確認します。
    ```go:keyboard.go
    // KeyEvent contains data about a keyboard event
    type KeyEvent struct {
    	Bytes bytes
    	Key   int
    	Char  string
    }
    ```
    「s」の場合だと115がkey.Keyで、この値で`key.Key == keyboard.A`と比較した条件になっていたんですね。
    `keyboard.A`の定義を確認します。
    ```go:keyboard.go
    const (
    	Tilde = iota + 96
    	A
    	B
    	C
    	D
    	E
    	F
    	G
    	H
    	I
    	J
    	K
    	L
    	M
    	N
    	O
    	P
    	Q
    	R
    	S
    	T
    	U
    	V
    	W
    	X
    	Y
    	Z
    )
    ```
    別の言語からきた方だとまずGoにクラスや継承が無い事に気付くと思います。
    また合体演算子が無い事に気付き、
    そしてひっそりと列挙型のenumが無いことに気付きます。
    `iota`(イオタ)はGoで列挙型に近い機能を実現するために利用されています。
    例えば、
    ```go
    const (
        apple = iota // apple == 0
        orange       // orange == 1
        lemon        // lemon == 2
    )
    ```
    となります。orange以降はiotaを省略してもインクリメントされます。
    なので今回比較していた`keyboard.A`は、`iota + 96`の次の値なので`97`だということが分かります。
    他のキーもこのような感じで定義されていました。
    ということは同じような条件式で「A」なら左に移動、「W」なら前に移動、...のように後はコードを書いていけば良さそうですね！

<br>

- ドローン操作に取り込んでいく
    `main.go`にてキーボードで操作できるよう修正していきます。
    いったん離陸と着陸操作、後はフライトデータの高さとバッテリー残量の出力部分を置き換えてみました。
    また`key.goファイル`はもう必要なくなったので削除しました。
    ```go:main.go
    package main
    
    import (
    	"fmt"
    
    	"gobot.io/x/gobot"
    	"gobot.io/x/gobot/platforms/dji/tello"
    	"gobot.io/x/gobot/platforms/keyboard"
    )
    
    func main() {
    	drone := tello.NewDriver("8888")
    	keys := keyboard.NewDriver()
    	var height int16
    	var battery int8
    
    	work := func() {
    		drone.On(tello.FlightDataEvent, func(data interface{}) {
    			flightData := data.(*tello.FlightData)
    			height = flightData.Height
    			battery = flightData.BatteryPercentage
    		})
    
    		keys.On(keyboard.Key, func(data interface{}) {
    			key := data.(keyboard.KeyEvent)
    
    			switch key.Key {
    			case keyboard.Spacebar:
    				fmt.Println("TakeOff")
    				drone.TakeOff()
    			case keyboard.Escape:
    				fmt.Println("Land")
    				drone.Land()
    			case keyboard.H:
    				fmt.Println("Height:", height)
    			case keyboard.B:
    				fmt.Println("Battery:", battery)
    			}
    		})
    	}
    
    	robot := gobot.NewRobot("tello",
    		[]gobot.Connection{},
    		[]gobot.Device{drone, keys},
    		work,
    	)
    
    	robot.Start()
    }
    ```
    `[]gobot.Device{drone, keys},`
    注意点としては`Device`に`keys`を含めることくらいです。
    これでテストフライトしてみます！！
    
    ```terminal:terminal
    Battery: 83
    Height: 0
    TakeOff
    Height: 0
    Height: 0
    Height: 1
    Height: 4
    Height: 6
    Height: 7
    Height: 9
    Height: 9
    Height: 10
    Battery: 82
    Height: 10
    Land
    ```
    出力の通り、フライトデータを確認しつつTakeOffからLandまで実行できました！
    問題なさそうに思えるので、ドローンのアクションをどんどん追加していきます。
    追加した結果、以下のようになりました。
    ```go.main.go
    package main
    
    import (
    	"fmt"
    
    	"gobot.io/x/gobot"
    	"gobot.io/x/gobot/platforms/dji/tello"
    	"gobot.io/x/gobot/platforms/keyboard"
    )
    
    // 機体の移動と旋回に使用するパラメータ
    type KeyboardParams struct {
    	move     int
    	rotation int
    }
    
    func NewKeyboardParams(move int, rotation int) *KeyboardParams {
    	return &KeyboardParams{
    		move:     move,
    		rotation: rotation,
    	}
    }
    
    func main() {
    	drone := tello.NewDriver("8888")
    	keys := keyboard.NewDriver()
    	kp := NewKeyboardParams(3, 20)
    	var height int16
    	var battery int8
    
    	work := func() {
    		drone.On(tello.FlightDataEvent, func(data interface{}) {
    			flightData := data.(*tello.FlightData)
    			height = flightData.Height
    			battery = flightData.BatteryPercentage
    		})
    
    		keys.On(keyboard.Key, func(data interface{}) {
    			key := data.(keyboard.KeyEvent)
    
    			switch key.Key {
    			// フライトデータ
    			case keyboard.H:
    				fmt.Println("Height:", height)
    			case keyboard.B:
    				fmt.Println("Battery:", battery)
    
    			// 離陸・着陸
    			case keyboard.Spacebar:
    				fmt.Println("TakeOff")
    				drone.TakeOff()
    			case keyboard.Escape:
    				fmt.Println("Land")
    				drone.Land()
    
    			// 上昇・下降
    			case keyboard.ArrowUp:
    				fmt.Println("Up")
    				drone.Up(kp.move)
    			case keyboard.ArrowDown:
    				fmt.Println("Down")
    				drone.Down(kp.move)
    
    			// 進行
    			case keyboard.W:
    				fmt.Println("Forward")
    				drone.Forward(kp.move)
    			case keyboard.A:
    				fmt.Println("Left")
    				drone.Left(kp.move)
    			case keyboard.S:
    				fmt.Println("Backward")
    				drone.Backward(kp.move)
    			case keyboard.D:
    				fmt.Println("Right")
    				drone.Right(kp.move)
    			case keyboard.F:
    				fmt.Println("Hover")
    				drone.Hover()
    
    			// 旋回
    			case keyboard.Q:
    				fmt.Println("Clockwise")
    				drone.Clockwise(kp.rotation)
    			case keyboard.E:
    				fmt.Println("CounterClockwise")
    				drone.CounterClockwise(kp.rotation)
    			case keyboard.R:
    				fmt.Println("CeaseRotation")
    				drone.CeaseRotation()
    
    			// 特殊
    			case keyboard.I:
    				fmt.Println("FrontFlip")
    				drone.FrontFlip()
    			case keyboard.J:
    				fmt.Println("LeftFlip")
    				drone.LeftFlip()
    			case keyboard.K:
    				fmt.Println("BackFlip")
    				drone.BackFlip()
    			case keyboard.L:
    				fmt.Println("RightFlip")
    				drone.RightFlip()
    			case keyboard.U:
    				fmt.Println("Bounce")
    				drone.Bounce()
    
    			// 進行値セット (1〜100)
    			case keyboard.One:
    				kp.move = 1
    				fmt.Println("move:", kp.move)
    			case keyboard.Two:
    				kp.move = 2
    				fmt.Println("move:", kp.move)
    			case keyboard.Three:
    				kp.move = 3
    				fmt.Println("move:", kp.move)
    			case keyboard.Four:
    				kp.move = 4
    				fmt.Println("move:", kp.move)
    			case keyboard.Five:
    				kp.move = 5
    				fmt.Println("move:", kp.move)
    			case keyboard.Six:
    				kp.move = 6
    				fmt.Println("move:", kp.move)
    			case keyboard.Seven:
    				kp.move = 7
    				fmt.Println("move:", kp.move)
    			case keyboard.Eight:
    				kp.move = 8
    				fmt.Println("move:", kp.move)
    			case keyboard.Nine:
    				kp.move = 9
    				fmt.Println("move:", kp.move)
    			case keyboard.Zero:
    				kp.move *= 10
    				if kp.move > 100 {
    					kp.move = 100
    				}
    				fmt.Println("move:", kp.move)
    
    			// 旋回値セット (10〜100)
    			case keyboard.C:
    				kp.rotation += 10
    				if kp.rotation > 100 {
    					kp.rotation = 10
    				}
    				fmt.Println("rotation:", kp.rotation)
    			case keyboard.X:
    				kp.rotation -= 10
    				if kp.rotation < 10 {
    					kp.rotation = 100
    				}
    				fmt.Println("rotation:", kp.rotation)
    
    			// 設定パラメータ確認
    			case keyboard.P:
    				fmt.Println("move:", kp.move, "rotation:", kp.rotation)
    			}
    		})
    	}
    
    	robot := gobot.NewRobot("tello",
    		[]gobot.Connection{},
    		[]gobot.Device{drone, keys},
    		work,
    	)
    
    	robot.Start()
    }
    ```
    移動や旋回のアクションには`1〜100`の値が求められていたので、`move`と`rotation`を定義して必要に応じて値を変更できるようにしました。
    ただ本来はキーを押している間は動き続けて、キーを離したら止まるような挙動にできたほうが操作感は良いと思います…！
    
    これで動かしてみます。
    <blockquote class="twitter-tweet" data-media-max-width="560"><p lang="zxx" dir="ltr"><a href="https://t.co/B9qIHgvltI">pic.twitter.com/B9qIHgvltI</a></p>&mdash; ka-na (@nakamu_ka) <a href="https://twitter.com/nakamu_ka/status/1734551502914773494?ref_src=twsrc%5Etfw">December 12, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
    
    わや！！
    手元のキーボードを写しながらの動画がベストでしたが、ゴミ屋敷を見せるわけにはいかず断念しました。
    Telloをお持ちの方は、これを参考にキー配置等変えたりして試してみてください。
    
    :::note warn
    操作がうまくいかない場合
    もし、部屋等でTelloのキーボード操作がうまくいかなかったり反応が悪いような状況の場合は照度を意識してみてください。
    Telloにはビジョンポジショニングシステムという赤外線モジュールが本体裏にあり、これによって機体のホバリングを安定させているのですが、照度が100ルクス未満だとこのシステムが正常に動作しないようです。
    なので部屋の照明は全灯(明るさMAX)をオススメします。
    他にもこのシステムが作動しなくなるおそれがある状況というのが公式のTelloユーザーマニュアルに記載してありますので確認してみてください。
    

## 映像編
映像編は駆け足でいきます…！
### 映像表示
- MPlayerでの表示
    Telloのカメラ映像をMPlayerで表示するためインストールします。
    ```terminal:terminal
    $ brew install mplayer
    ```

    映像もまずはサンプルくらいのものを別ファイルで準備しました。
    ```go
    package main
    
    import (
    	"fmt"
    	"os/exec"
    	"time"
    
    	"gobot.io/x/gobot"
    	"gobot.io/x/gobot/platforms/dji/tello"
    )
    
    func main() {
    	drone := tello.NewDriver("8890")
    
    	mplayer := exec.Command("mplayer", "-fps", "60", "-")
    	mplayerIn, _ := mplayer.StdinPipe()
    	if err := mplayer.Start(); err != nil {
    		fmt.Println(err)
    		return
    	}
    
    	work := func() {
    		drone.On(tello.ConnectedEvent, func(data interface{}) {
    			fmt.Println("Connected")
    			drone.StartVideo()
    			drone.SetVideoEncoderRate(tello.VideoBitRateAuto)
    			gobot.Every(100*time.Millisecond, func() {
    				drone.StartVideo()
    			})
    		})
    
    		drone.On(tello.VideoFrameEvent, func(data interface{}) {
    			pkt := data.([]byte)
    			if _, err := mplayerIn.Write(pkt); err != nil {
    				fmt.Println(err)
    			}
    		})
    	}
    
    	robot := gobot.NewRobot("tello",
    		[]gobot.Connection{},
    		[]gobot.Device{drone},
    		work,
    	)
    
    	robot.Start()
    }
    ```
    こちらを実行していきたいと思います。
    <blockquote class="twitter-tweet" data-media-max-width="560"><p lang="zxx" dir="ltr"><a href="https://t.co/VIEetcxRC8">pic.twitter.com/VIEetcxRC8</a></p>&mdash; ka-na (@nakamu_ka) <a href="https://twitter.com/nakamu_ka/status/1734902223736635838?ref_src=twsrc%5Etfw">December 13, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

    良い感じにMPlayerで映像が表示されてます！
    こちらを現在の`main.go`に取り込んでいきます。
    ```go:main.go
    package main
    
    import (
    	"fmt"
    	"os/exec"
    	"time"
    
    	"gobot.io/x/gobot"
    	"gobot.io/x/gobot/platforms/dji/tello"
    	"gobot.io/x/gobot/platforms/keyboard"
    )
    
    // 機体の移動と旋回に使用するパラメータ
    type KeyboardParams struct {
    	move     int
    	rotation int
    }
    
    func NewKeyboardParams(move int, rotation int) *KeyboardParams {
    	return &KeyboardParams{
    		move:     move,
    		rotation: rotation,
    	}
    }
    
    func main() {
    	drone := tello.NewDriver("8888")
    	keys := keyboard.NewDriver()
    	kp := NewKeyboardParams(3, 20)
    	var height int16
    	var battery int8
    
    	mplayer := exec.Command("mplayer", "-fps", "60", "-")
    	mplayerIn, _ := mplayer.StdinPipe()
    	if err := mplayer.Start(); err != nil {
    		fmt.Println(err)
    		return
    	}
    
    	work := func() {
    		drone.On(tello.FlightDataEvent, func(data interface{}) {
    			flightData := data.(*tello.FlightData)
    			height = flightData.Height
    			battery = flightData.BatteryPercentage
    		})
    
    		keys.On(keyboard.Key, func(data interface{}) {
    			key := data.(keyboard.KeyEvent)
    
    			switch key.Key {
    			// フライトデータ
    			case keyboard.H:
    				fmt.Println("Height:", height)
    			case keyboard.B:
    				fmt.Println("Battery:", battery)
    
    			// 離陸・着陸
    			case keyboard.Spacebar:
    				fmt.Println("TakeOff")
    				drone.TakeOff()
    			case keyboard.Escape:
    				fmt.Println("Land")
    				drone.Land()
    
    			// 上昇・下降
    			case keyboard.ArrowUp:
    				fmt.Println("Up")
    				drone.Up(kp.move)
    			case keyboard.ArrowDown:
    				fmt.Println("Down")
    				drone.Down(kp.move)
    
    			// 進行
    			case keyboard.W:
    				fmt.Println("Forward")
    				drone.Forward(kp.move)
    			case keyboard.A:
    				fmt.Println("Left")
    				drone.Left(kp.move)
    			case keyboard.S:
    				fmt.Println("Backward")
    				drone.Backward(kp.move)
    			case keyboard.D:
    				fmt.Println("Right")
    				drone.Right(kp.move)
    			case keyboard.F:
    				fmt.Println("Hover")
    				drone.Hover()
    
    			// 旋回
    			case keyboard.Q:
    				fmt.Println("Clockwise")
    				drone.Clockwise(kp.rotation)
    			case keyboard.E:
    				fmt.Println("CounterClockwise")
    				drone.CounterClockwise(kp.rotation)
    			case keyboard.R:
    				fmt.Println("CeaseRotation")
    				drone.CeaseRotation()
    
    			// 特殊
    			case keyboard.I:
    				fmt.Println("FrontFlip")
    				drone.FrontFlip()
    			case keyboard.J:
    				fmt.Println("LeftFlip")
    				drone.LeftFlip()
    			case keyboard.K:
    				fmt.Println("BackFlip")
    				drone.BackFlip()
    			case keyboard.L:
    				fmt.Println("RightFlip")
    				drone.RightFlip()
    			case keyboard.U:
    				fmt.Println("Bounce")
    				drone.Bounce()
    
    			// 進行値セット (1〜100)
    			case keyboard.One:
    				kp.move = 1
    				fmt.Println("move:", kp.move)
    			case keyboard.Two:
    				kp.move = 2
    				fmt.Println("move:", kp.move)
    			case keyboard.Three:
    				kp.move = 3
    				fmt.Println("move:", kp.move)
    			case keyboard.Four:
    				kp.move = 4
    				fmt.Println("move:", kp.move)
    			case keyboard.Five:
    				kp.move = 5
    				fmt.Println("move:", kp.move)
    			case keyboard.Six:
    				kp.move = 6
    				fmt.Println("move:", kp.move)
    			case keyboard.Seven:
    				kp.move = 7
    				fmt.Println("move:", kp.move)
    			case keyboard.Eight:
    				kp.move = 8
    				fmt.Println("move:", kp.move)
    			case keyboard.Nine:
    				kp.move = 9
    				fmt.Println("move:", kp.move)
    			case keyboard.Zero:
    				kp.move *= 10
    				if kp.move > 100 {
    					kp.move = 100
    				}
    				fmt.Println("move:", kp.move)
    
    			// 旋回値セット (10〜100)
    			case keyboard.C:
    				kp.rotation += 10
    				if kp.rotation > 100 {
    					kp.rotation = 10
    				}
    				fmt.Println("rotation:", kp.rotation)
    			case keyboard.X:
    				kp.rotation -= 10
    				if kp.rotation < 10 {
    					kp.rotation = 100
    				}
    				fmt.Println("rotation:", kp.rotation)
    
    			// 設定パラメータ確認
    			case keyboard.P:
    				fmt.Println("move:", kp.move, "rotation:", kp.rotation)
    			}
    		})
    
    		drone.On(tello.ConnectedEvent, func(data interface{}) {
    			fmt.Println("Connected")
    			drone.StartVideo()
    			drone.SetVideoEncoderRate(tello.VideoBitRateAuto)
    			gobot.Every(100*time.Millisecond, func() {
    				drone.StartVideo()
    			})
    		})
    
    		drone.On(tello.VideoFrameEvent, func(data interface{}) {
    			pkt := data.([]byte)
    			if _, err := mplayerIn.Write(pkt); err != nil {
    				fmt.Println(err)
    			}
    		})
    	}
    
    	robot := gobot.NewRobot("tello",
    		[]gobot.Connection{},
    		[]gobot.Device{drone, keys},
    		work,
    	)
    
    	robot.Start()
    }
    ```

    実行していきます。
    <blockquote class="twitter-tweet" data-media-max-width="560"><p lang="zxx" dir="ltr"><a href="https://t.co/SWSL5EhWfz">pic.twitter.com/SWSL5EhWfz</a></p>&mdash; ka-na (@nakamu_ka) <a href="https://twitter.com/nakamu_ka/status/1734922774572109973?ref_src=twsrc%5Etfw">December 13, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

    違うんです。スマホでPC内で表示しているドローン映像&実際のドローン自体を撮影するのと、キーボード操作を行うのを同時にやる難易度が高すぎるんです！！
    映像表示についてはいったん良いのではないでしょうか！！
    
<br>

- GoCV(OpenCV)
    <a href="https://gocv.io/"><img title="Home :: GoCV - Golang Computer Vision Using OpenCV 4" src="https://capture.heartrails.com/400x300/cool/1702485055139?https://gocv.io/" alt="https://gocv.io/" width="400" height="300" /></a>

    さあここから映像系の華であるGoCVを利用して顔認識や自動追尾などを盛り込んでいきます！
    と言いたいところなのですが、今回はここまでとします。

# まとめ
今回は操作&映像編ということで、
- [x] ドローンそれぞれのアクションをキーボードの入力から行えるようにする
- [x] Telloのカメラを利用して映像周りの機能に触れる

この辺りを行ってみました。次回はGoCVを利用し、より映像を活用して面白いことができたらと考えています。
が、次回の記事の公開時期は未定です…！

それでは、最後までご覧いただきありがとうございました。