ミニドローン Parrot MAMBO を Node.js で飛ばす（Windows 編）

# 目的
Node.js (JavaScript) でドローン（Parrot のミニドローン）を飛ばします。
既に [Mac による例](https://liginc.co.jp/187633)と [Raspberry Pi による例](http://qiita.com/AkihiroTakamura/items/25ba516f8ec624e66ee7)を拝見していたので、Windows でもやってみました。Windows では他の環境に比べて少しトリッキーでしたので、自分のメモの意味も含め、まとめておきます。

# 環境・機材

## ハードウェア
* Windows の パソコン（Lenovo ThinkPad E440; Windows 10 Home, Ver.1703）
* [Parrot MAMBO](https://www.parrot.com/us/minidrones/parrot-Mambo)（[Amazon で
 14,453 円で入手](http://amzn.asia/bkL2VLa)）
* [iBUFFALO Bluetooth アダプタ BSBT4D09BK](http://buffalo.jp/product/peripheral/wireless-adapter/bsbt4d09bk/)（[Amazon で 936 円で入手](http://amzn.asia/1k1zmmB)）
    * 上記 PC 内蔵の BT では動作確認できませんでした
    * このアダプタ（BSBT4D09BK）は「CSR8510 A10」というチップが使われており、下記 node-rolling-spider が依存している [node-bluetooth-hci-socket](https://github.com/sandeepmistry/node-bluetooth-hci-socket) モジュールに適合しています

## ソフトウェア
* [Node.js v6.10.2](https://nodejs.org/ja/)
* [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) モジュール
* [node-rolling-spider](https://github.com/voodootikigod/node-rolling-spider) モジュール
* [Zadig v2.3](http://zadig.akeo.ie/)


## 環境構築

### Node.js のインストール
* 通常どおりインストーラでインストール
    * 私は C:\nodejs にインストールしました

### windows-build-tools のインストール
* 各種の Node モジュールを Windows ネイティブでコンパイルするための環境一式で、以下を一括でインストール・各種設定できます（[こちらをご参考](https://github.com/felixrieseberg/windows-build-tools)）。なお、既に VisualStudio 、C++ Build Tools、Python がインストールされている環境でも「conflict-free」だそうです。
    * Visual C++ Build Tools 2015
    * Python 2.7.11
        * C:\Users\\(ユーザ名)\\.windows-build-tools\python27 にインストールされる

* コマンドプロンプト（cmd.exe）を「管理者として実行」して、以下のコマンドを実行

```
npm install --global windows-build-tools
```
* インストールが終わるまで気長に待ちます

### node-rolling-spider のインストール

* Parrot のミニドローンと Bluetooth でやりとりできるモジュールです
* これからアプリケーションを作るフォルダを適当に作成
    * __例　C:\Users\\(ユーザ名)\drone__
* コマンドプロンプトでそのフォルダに移動
* 以下のコマンドを実行

```
npm install rolling-spider
```

* 黄色い文字で「WARN」がいくつか出てくると思いますが、たいてい大丈夫です
* 赤い文字で「ERROR」の場合はネイティブコンパイル等に失敗しているので、windows-build-tools をインストールし直したり、エラーメッセージをもとに試行錯誤してください

### Bluetooth アダプタの設定
* Bluetooth アダプタをパソコンの USB ポートにさします
    * Windows 10 の場合、普通に使う場合はドライバ不要（自動インストール）だそうです（させば、デバイスマネージャーで Bluetooth デバイスとして認識されます）が…
* 上記 node-rolling-spider が依存している [node-bluetooth-hci-socket](https://github.com/sandeepmistry/node-bluetooth-hci-socket) というモジュールを通じて使う場合は、「WinUSB」というドライバに書き換える必要があります（[こちらに記載](https://github.com/sandeepmistry/node-bluetooth-hci-socket#windows)）。
* Zadig というドライバ書き換えツールを[こちらからダウンロード](http://zadig.akeo.ie/)します
* zadig-2.3.exe をダブルクリックで実行します
* Options メニュー -> List All Devices を選択するとデバイスの一覧が出ます

![zadig.PNG](https://qiita-image-store.s3.amazonaws.com/0/157570/e34787e9-3283-98ec-7fa4-7b6fc21f557a.png)

* デバイスの一覧から Bluetooth アダプタ（今回の場合は「CSR8510 A10」）を選ぶと、左側に現在のドライバが出ます（私の場合は以前に東芝のスタックを入れていたので tosrfusb と表示されています）
* これを「WinUSB」に書き換えるため、「Replace Driver」をクリックします

![zadig2.PNG](https://qiita-image-store.s3.amazonaws.com/0/157570/4f599bc6-0743-0e2a-6eb7-e639e19b1295.png)

* 「The driver was installed successfully.」と出れば成功です。

![zadig3.PNG](https://qiita-image-store.s3.amazonaws.com/0/157570/0415c982-1f7c-48b7-3963-f5db56843000.png)

* するとこれまで「デバイスマネージャー」で「Bluetooth」として認識されていたアダプタが、「ユニバーサル シリアル バス デバイス」として認識されるようになります。こうなれば OK です。

※ Bluetooth アダプタのドライバを元に戻す方法は後述します


# ようやく飛ばします

既に標準のスマホ／タブレットアプリを使って当該ドローンの操作はひととおり理解していることを前提とします。

以下のコードは rolling-spider モジュール内の eg フォルダにある「[keyboard.js](https://github.com/voodootikigod/node-rolling-spider/blob/master/eg/keyboard.js)」をベースにして、[モジュールのサイト](https://github.com/voodootikigod/node-rolling-spider)を参考に書き換えつつ、コメントを加えています。間違いがあればコメントいただけましたら幸いです。

## コーディング

以下のコードをテキストエディタで入力し、さきほど rolling-spider をインストールしたフォルダ（例　__C:\\(ユーザ名)\drone__）内に「__app.js__」として保存します。

```js:app.js
'use strict';                             // 厳格モードにする

// モジュールの読み込み
const Drone = require('rolling-spider');  // rolling-spider モジュールを使う
const keypress = require('keypress');     // キーボード操作を取得する keypress モジュールを使う（rolling-spider と同時にインストールされる）

// 変数の設定
let ACTIVE = true;                        // ドローンがアクティブ状態か否か
const STEPS = 2;                          // 一度のキー操作で命令を出す回数（動かすステップ数、0-100）

// rolling-spider のインスタンスを作る
const d = new Drone();
// ドローンの初期設定
d.connect( () => {                        // BLE でドローンに接続し、接続できたらコールバック
  d.setup( () => {                        // ドローンを初期設定してサービスや特徴を取得、その後コールバック
    d.flatTrim();                         // トリムをリセット
    d.startPing();                        // 不明
    d.flatTrim();                         // なぜ二回呼ぶのかは不明
    ACTIVE = true;                        // ドローンを ACTIVE 状態とする
    console.log(d.name, 'is ready!');     // 準備OKなことをコンソール出力
  });
});

// キー操作でイベントを発生させる
keypress(process.stdin);                  // 標準入力に keypress イベントを発生させる
process.stdin.setRawMode(true);           // raw mode（修飾を伴わない）で標準入力を受け付ける
process.stdin.resume();                   // keypress イベントをリッスン

// キー操作後に少しのあいだ入力を受け付けないようにする関数
function cooldown() {
  ACTIVE = false;       // いったん ACTIVE 状態でなくしておいて
  setTimeout( () => {   // 一定時間後に
    ACTIVE = true;      // ACTIVE に戻す
  }, STEPS * 12);       // この例では 24 ms
}

// キーボードからの入力による操作
process.stdin.on('keypress', (ch, key) => {   // keypress イベントが発生したら
  if (ACTIVE && key) {                        // ドローンが ACTIVE で key があれば
    // 緊急停止
    if (key.name === 'm') {             // m キー
      d.emergency();                      // 緊急停止（モーターを即時停止）
      setTimeout( () => {                 // 3秒後にプログラム終了
        process.exit();
      }, 3000);

    // 離陸（t; take-off）
    } else if (key.name === 't') {      // t キー
      console.log('takeoff');             // コンソール出力
      d.takeOff();                        // 離陸

    // w/s/a/d キー（前後左右の移動）
    } else if (key.name === 'w') {      // w キー
      d.forward({ steps: STEPS });        // 前進（前ピッチ）
      cooldown();                         // 少しの間キー操作を受け付けない
    } else if (key.name === 's') {      // s キー
      d.backward({ steps: STEPS });       // 後退（後ピッチ）
      cooldown();                         // 少しの間キー操作を受け付けない
    } else if (key.name === 'a') {      // a キー
      d.tiltLeft({ steps: STEPS });       // 左水平移動（左ロール）
      cooldown();                         // 少しの間キー操作を受け付けない
    } else if (key.name === 'd') {      // d キー
      d.tiltRight({ steps: STEPS });      // 右水平移動（右ロール）
      cooldown();                         // 少しの間キー操作を受け付けない

    // カーソルキー（上下と左右スピン）
    } else if (key.name === 'left') {   // 左カーソルキー
      d.turnLeft({ steps: STEPS });       // 左旋回（左スピン（ヨー））
      cooldown();                         // 少しの間キー操作を受け付けない
    } else if (key.name === 'right') {  // 右カーソルキー
      d.turnRight({ steps: STEPS });      // 右旋回（右スピン（ヨー））
      cooldown();                         // 少しの間キー操作を受け付けない
    } else if (key.name === 'up') {     // 上カーソルキー
      d.up({ steps: STEPS * 2.5 });       // 上昇
      cooldown();                         // 少しの間キー操作を受け付けない
    } else if (key.name === 'down') {   // 下カーソルキー
      d.down({ steps: STEPS * 2.5 });     // 下降
      cooldown();                         // 少しの間キー操作を受け付けない

    // i/j/k/l キー（アクロバット）
    } else if (key.name === 'i') {      // i キー
      d.frontFlip({ steps: STEPS });      // 前に宙返り
      cooldown();                         // 少しの間キー操作を受け付けない
    } else if (key.name === 'k') {      // k キー
      d.backFlip({ steps: STEPS });       // 後ろに宙返り
      cooldown();                         // 少しの間キー操作を受け付けない
    } else if (key.name === 'j') {      // j キー
      d.leftFlip({ steps: STEPS });       // 左に宙返り
      cooldown();                         // 少しの間キー操作を受け付けない
    } else if (key.name === 'l') {      // l キー
      d.rightFlip({ steps: STEPS });      // 右に宙返り
      cooldown();                         // 少しの間キー操作を受け付けない

    // 着陸（q; quit）
    } else if (key.name === 'q') {      // q キー
      console.log('landing...');          // コンソール出力
      d.land();                           // 着陸
    }
  }

  // プログラム終了（ctrl + c）
  if (key && key.ctrl && key.name === 'c') {  // ctrl + c なら
    process.stdin.pause();                    // 標準入力を一時停止
    process.exit();                           // プログラム終了
  }
});
```

## 動作確認

* ミニドローンにバッテリーを装着し、電源を ON にします
    * Parrot MAMBO の目が緑色に点滅します（ペアリングモード？）
* コマンドプロンプトで、上記の app.js があるディレクトリ（例　__C:\\(ユーザ名)\drone__）に移動し、以下を実行します

```
node app.js
```

コンソールに以下のように表示され、MAMBO の目が緑色の「点灯」になったら準備OKです（xxxxxxは個体の番号）。

```
Mambo_xxxxxx is ready!
```

### 操縦方法（キーアサイン）


|キー|動作|メソッド|
|:--:|:--|:--|
|m|緊急停止（即座にプロペラ停止）|emergency()|
|t|離陸|takeOff()|
|w|前進（前ピッチ）|forward()|
|s|後退（後ピッチ）|backward()|
|a|左水平移動（左ロール）|tiltLeft()|
|d|右水平移動（右ロール）|tiltRight()|
|↑ カーソル|上昇|up()|
|↓ カーソル|下降|down()|
|← カーソル|左旋回（左スピン（ヨー））|turnLeft()|
|→ カーソル|右旋回（右スピン（ヨー））|turnRight()|
|i|前方宙返り|frontFlip()|
|k|後方宙返り|backFlip()|
|j|左に宙返り|leftFlip()|
|l|右に宙返り|rightFlip()|
|q|着陸|land()|
|ctrl + c|プログラム終了|-|


# まとめ

[node-rolling-spider](https://github.com/voodootikigod/node-rolling-spider) モジュールを使って Windows でミニドローンを飛ばすポイントは以下です。

* [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) でモジュールをネイティブ・コンパイルできる環境を整えておく
* [node-bluetooth-hci-socket モジュールに対応した Bluetooth アダプタ](https://github.com/sandeepmistry/node-bluetooth-hci-socket#windows)を使う
* Bluetooth アダプタのドライバを WinUSB に書き換える（[Zadig](http://zadig.akeo.ie/) というツールを使う）

これらのうち1点目は今回の内容に限ったことではありませんが、これまで手動では設定しており、他のモジュールはうまくビルドされていたのに、今回つまずきました。windows-build-tools を再インストールしたらうまくいきました。手動設定で、なにか足りていなかったようです。

2点目と3点目については、node-rolling-spider の依存モジュールの readme までちゃんと読まずに試していて、つまずきました。また、一般的な Bluetooth アダプタのように「スタック」を使わずに（スタックをバイパスして）通信するというのが当初は理解できず、手間取りました。

このあたり、事前に Raspberry Pi で試した時は何も考えなくてもすんなり動いて、Windows でぜんっぜん動かなくて、心が折れかけでした。業務の都合上（？）、Windows マシンだけで済むと楽なので、少し手間取りはしましたが、動いてよかったです。


# 付録

## Bluetooth アダプタのドライバの戻し方

* デバイスマネージャーで WinUSB ドライバで書き換えていた Bluetooth アダプタを右クリックし、「ドライバーの更新」を選択

![zadig4.PNG](https://qiita-image-store.s3.amazonaws.com/0/157570/a22b1799-0c6e-0b5e-1005-9374d440da46.png)


* 「コンピューターを参照してドライバーソフトウェアを検索」を選択
* 「コンピューター上の利用可能なドライバーの一覧から選択します」を選択
* 「Generic Bluetooth Radio」（当初に認識されていたデバイス名）を選択し、「次へ」でドライバをインストール
* デバイスマネージャーで当該のアダプタが「Bluetooth」として認識されていれば OK です
* 念のため zadig-2.3.exe を実行し、「List All Device」して、当該アダプタを選択して、もとのドライバになっていることを確認します