本記事は オルトプラス Advent Calendar 2023 の12/16の記事です。
はじめに
こんにちは!オルトプラスの中村です!
本記事はGo + ドローンで最高の世界へ飛び立つ その1 導入編の続編となります。
プログラミング対応のドローンに興味ある方や、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
前回までのおさらい
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()
}
- telloの電源が入っていること
- telloのWi-Fiに接続できていること
- 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のREADMEにHow to Useがあるので確認します。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というファイルを作成し、そこにコードを貼って実行してみました。
想定通りの挙動に見えます。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を確認します。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
の定義を確認します。keyboard.goconst ( 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で列挙型に近い機能を実現するために利用されています。
例えば、const ( apple = iota // apple == 0 orange // orange == 1 lemon // lemon == 2 )
となります。orange以降はiotaを省略してもインクリメントされます。
なので今回比較していたkeyboard.A
は、iota + 96
の次の値なので97
だということが分かります。
他のキーもこのような感じで定義されていました。
ということは同じような条件式で「A」なら左に移動、「W」なら前に移動、...のように後はコードを書いていけば良さそうですね!
-
ドローン操作に取り込んでいく
main.go
にてキーボードで操作できるよう修正していきます。
いったん離陸と着陸操作、後はフライトデータの高さとバッテリー残量の出力部分を置き換えてみました。
またkey.goファイル
はもう必要なくなったので削除しました。main.gopackage 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
を含めることくらいです。
これでテストフライトしてみます!!terminalBattery: 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.gopackage 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
を定義して必要に応じて値を変更できるようにしました。
ただ本来はキーを押している間は動き続けて、キーを離したら止まるような挙動にできたほうが操作感は良いと思います…!これで動かしてみます。
— ka-na (@nakamu_ka) December 12, 2023
わや!!
手元のキーボードを写しながらの動画がベストでしたが、ゴミ屋敷を見せるわけにはいかず断念しました。
Telloをお持ちの方は、これを参考にキー配置等変えたりして試してみてください。操作がうまくいかない場合
もし、部屋等でTelloのキーボード操作がうまくいかなかったり反応が悪いような状況の場合は照度を意識してみてください。
Telloにはビジョンポジショニングシステムという赤外線モジュールが本体裏にあり、これによって機体のホバリングを安定させているのですが、照度が100ルクス未満だとこのシステムが正常に動作しないようです。
なので部屋の照明は全灯(明るさMAX)をオススメします。
他にもこのシステムが作動しなくなるおそれがある状況というのが公式のTelloユーザーマニュアルに記載してありますので確認してみてください。
映像編
映像編は駆け足でいきます…!
映像表示
-
MPlayerでの表示
Telloのカメラ映像をMPlayerで表示するためインストールします。terminal$ brew install mplayer
映像もまずはサンプルくらいのものを別ファイルで準備しました。
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() }
こちらを実行していきたいと思います。
— ka-na (@nakamu_ka) December 13, 2023
良い感じにMPlayerで映像が表示されてます!
こちらを現在のmain.go
に取り込んでいきます。main.gopackage 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() }
実行していきます。
— ka-na (@nakamu_ka) December 13, 2023
違うんです。スマホでPC内で表示しているドローン映像&実際のドローン自体を撮影するのと、キーボード操作を行うのを同時にやる難易度が高すぎるんです!!
映像表示についてはいったん良いのではないでしょうか!!
まとめ
今回は操作&映像編ということで、
- ドローンそれぞれのアクションをキーボードの入力から行えるようにする
- Telloのカメラを利用して映像周りの機能に触れる
この辺りを行ってみました。次回はGoCVを利用し、より映像を活用して面白いことができたらと考えています。
が、次回の記事の公開時期は未定です…!
それでは、最後までご覧いただきありがとうございました。