Node.js
ifttt
drone
Firebase
GoogleHome

Google Homeに話しかけてドローンを音声操作してみる

はじめに

image.jpg

Telloという12,800円ほどのコスパやばい小型ドローンをGWに買いました!
ドローンと言うと航空法による規制で中々自由に飛ばせないんじゃないの?というイメージがありましたが、実は航空法の対象となるのは200g以上のドローンなのです。
Telloは80gなので航空法の対象外となり、飛ばす場所の持ち主が禁止していなければ外でも飛ばすことが可能です!

そして公式SDKによりプログラムで操作可能です。
※Scratchやスマホアプリでビジュアルプログラミングも可能です。

これはもう音声操作するっきゃないですね。
ということでやってみました。

Telloプログラミング

まずはTello単体のプログラミングを試してみます。
こちら(PDF)がTelloのSDK仕様となりますが、Tello自体が192.168.10.1のWi-Fi親機となっており、PCをTelloのWi-Fiにぶら下げることで接続できます。
あとはSDKにあるコマンドをUDPで投げるだけです。

Node.jsでプログラミング

Pythonのサンプルが公開されていますが、今回はNode.jsで制御してみましょう。

ちなみに私の開発環境は以下の通りです。

  • OS: Windows10
  • Node: 8.11.1
  • npm: 5.6.0

※OSはMacでもラズパイ(Linux)でもなんでもいいです

UDPパッケージのインストール

適当にディレクトリ作成し、UDP操作用にdgramというパッケージをインストールします。

mkdir tello
cd tello
npm i dgram

Tello制御プログラム

dgramを使ってUDPでコマンドを投げます。
離陸や移動といった各動作を行ってる間waitするミリ秒も一緒に記述します。
index.jsというファイルを作って以下をコピペってみましょう。

index.js
"use strict"

const dgram = require("dgram")
const sock = dgram.createSocket("udp4")

// Telloの固定情報
const tello = {
    ip: "192.168.10.1",
    port: 8889
}

// Telloにコマンド送信する関数
const send = async (buf, ms = 0) => {
    console.log(buf)
    const command = new Buffer(buf)
    sock.send(command, 0, command.length, tello.port, tello.ip)
    await wait(ms)
}
const wait = ms => new Promise(res => setTimeout(res, ms))

// Telloに操作したいコマンドを記述
const main = async () => {
    await send("command", 100)
    await send("takeoff", 4000)
    await send("land")
    process.exit()
}
main()

プログラム実行

それではプログラムを実行してみます。
プログラム実行時はPCをWi-FiでTelloにつないでください。
念の為ping 192.168.10.1を打ってTelloと疎通できてることを確認しておきましょう。
問題なく疎通できたら以下のコマンドを実行します。

node index.js

Telloが離陸して着陸したら成功です。
SDKに従って、await send("left 50", 2000)(移動)やawait send("cw 90", 2000)(旋回)やawait send("flip l", 2000)(フリップ)やら色々試してみましょう。

こんな感じでプログラムから操作できます。

Google Homeに話しかけて音声操作してみる

続いてTelloをGoogle Homeから操作してみましょう。
流れは以下の図のようになります。

image.png

Google Homeに話しかけた内容を取得するためにIFTTTを使います。
そしてIFTTTで受け取った情報をローカルのPCに受け渡すために、FirebaseのRealtime Databaseへ書き込みます。
で、Firebaseの状態をローカルPC上のNode.jsで監視し、書き込みがあったらTelloに対し対応するコマンドを送信する、という流れです。

では以下の順序で構築していきます。

  1. Firebase Realtime Databaseの作成
  2. IFTTTアプレットの作成
  3. Node.jsプログラムの作成
  4. 動かしてみる

Firebase Realtime Databaseの作成

まずはFirebaseにてRealtime Databaseを作成します。
手順を箇条書きで簡単に書いていきます。

  • Firebaseコンソールにて「プロジェクトを追加」
  • 「プロジェクト名」を入力し、「国 / 地域」で日本を選択し「プロジェクトを作成」
  • 遷移先の画面左のメニューより、「DEVELOP」 > 「Database」と選択
  • Realtime Databaseにて「スタートガイド」
  • 「テストモードで開始」を選択し「有効にする」
  • 以下のようにDatabaseを作成
プロジェクトID
 └ googlehome
   └ word: ""

なおプロジェクトIDのちほどNode.jsでも使うのでメモしておいてください。

IFTTTアプレットの作成

IFTTTにてTello音声操作用アプレットを作成します。

  • IFTTTにログイン
  • 画面上部の「My Applets」 > 画面右上の「New Applet」
  • 「this」をクリック
  • テキストボックスに「ass」と入力し、絞られたサービス結果から「Google Assistant」を選択
    • このとき初めて選択した場合は接続画面になるので「connect」ボタンを押し、接続するアカウントを選択してください
  • 「Say a phrase with a text ingredient」を選択
  • 以下のよう入力して「Create trigger」
項目名 入力値
What do you want to say? ドローン $
What do you want the Assistant to say in response? ドローンを操作します
Language Japanese

image.png

  • 「that」をクリック
  • テキストボックスに「web」と入力し、絞られたサービス結果から「Webhooks」を選択
    • ここも初めて選択した場合は接続画面になるので「connect」ボタンを押してください
  • 「Make a web request」を選択

  • 以下のよう入力して「Create action」

項目名 入力値 備考
URL https://xxxxxxxx.firebaseio.com/googlehome/word.json xxxxxxxxはFirebaseのプロジェクトID
Method PUT
Content Type application/json
Body "drone {{TextField}}" ダブルクォーテーションで括る!

image.png

  • 最後に「Finish」ボタン

Node.jsプログラムの作成

以下のソースをindex.jsにコピペしてください。
その後ソース中のconst firebaseProjectId = "xxxxxxxx"xxxxxxxxを先ほど作成したFirebaseのプロジェクトIDに書き換えてください。

index.js
"use strict"

// firebase
const firebase = require("firebase")
const firebaseProjectId = "xxxxxxxx"
const firebasePath = "/googlehome/word"
firebase.initializeApp({databaseURL: `https://${firebaseProjectId}.firebaseio.com`})

// udp
const dgram = require("dgram")
const sock = dgram.createSocket("udp4")

// Tello
const tello = {
    address: "192.168.10.1",
    port: 8889
}

// Telloへcommand送信
const sendTello = buf => {
    const command = new Buffer(buf)
    setTimeout(() => sock.send(command, 0, command.length, tello.port, tello.address), 500)
}



// database更新時
const db = firebase.database()
db.ref(firebasePath).on("value", snapshot => {
    // 値取得
    let value = snapshot.val()
    if (!value) return
    console.log(value)

    // option word index
    let index = 1
    // 助詞を除外
    value = value.replace(/ ([がのをにへとでや]|より|から)/g, "")

    // コマンド定義
    const commandJson = {

        // drone
        "drone": () => {
            // 距離情報取得
            const distanceWord = / \d+ (CM|cm|センチ)/
            const distanceMatch = value.match(distanceWord)
            const distance = distanceMatch ? distanceMatch[0].match(/\d+/)[0] : 0
            value = value.replace(distanceWord, "")

            // 角度情報取得
            const angleWord = / \d+ (°|度|ドル)/
            const angleMatch = value.match(angleWord)
            const angle = angleMatch ? angleMatch[0].match(/\d+/)[0] : 0
            value = value.replace(angleWord, "")

            // スペースで区切られた単語を修正
            value = value.replace(" 旋 回", "旋回")
            value = value.replace("旋 回", "旋回")
            value = value.replace(" フリップ", "フリップ")
            value = value.replace(" グリップ", "フリップ")

            sendTello("command")
            const option = {
                "離陸": "takeoff",
                "着陸": "land",
                "上昇": `up ${distance}`,
                "上": `up ${distance}`,
                "下降": `down ${distance}`,
                "下": `down ${distance}`,
                "右": `right ${distance}`,
                "左": `left ${distance}`,
                "前": `forward ${distance}`,
                "前進": `forward ${distance}`,
                "バック": `back ${distance}`,
                "後ろ": `back ${distance}`,
                "後退": `back ${distance}`,
                "右旋回": `cw ${angle}`,
                "左旋回": `ccw ${angle}`,
                "右フリップ": "flip r",
                "左フリップ": "flip l",
                "前フリップ": "flip f",
                "バックフリップ": "flip b",
            }[value.split(" ")[index]]
            return typeof option === "string" ? () => sendTello(option) : false
        },

    }[value.split(" ")[0]]

    // コマンド取得
    if (!commandJson) return
    const command = commandJson()

    // コマンド実行
    if (!command) return
    command()

    // firebase clear
    db.ref(firebasePath).set("")
})

動かしてみる

以上で一通り準備ができたので動かしてみましょう。
このときPCのネットワークについてひとつ注意があり、インターネット接続用の口と、Tello接続用の口とで2つ必要になります。
Tello側はWi-Fiでの接続となるので、USB Wi-Fi子機をTello用に挿すのがてっとり早いです。

そしてpingコマンドなりでインターネット、Tello両方につながっていることを確認したらNode.jsプログラムを実行します。

node index.js

あとはGoogle Homeに話しかけるだけです。

「OK グーグル、ドローン離陸」
「OK グーグル、ドローン前に50センチ」
「OK グーグル、ドローン90度右旋回」
「OK グーグル、ドローン左フリップ」

こんな感じでドローンを音声操作できます。

おわりに

ドローンを音声操作ってやっぱロマンがありますよね。
ある程度音声操作した後はPCそっ閉じして普通にリモコンで遊んでますが。。。

というのも現状の公式SDKだとドローンの操縦しかできず、映像が取得できないんですよね。
できれば映像取得してあんなことやこんなことしてみたいじゃないですか。
実は非公式のGoのbot用ライブラリがあるようです。
言語がGoとなり私はまずGoの勉強からとなるので、こちらもいずれ挑戦してみたいなと思います。