Raspberry Pi & Node-REDからモーターを制御する

本記事は[Node-RED Advent Calendar 2019](https://qiita.com/advent-calendar/2019/node-red) 19日目の記事です。

#概要

ラピッドプロトタイピング用モーター[KeiganMotor](https://keigan-motor.com/)をNode-REDから操作する方法を紹介します。

[KeiganMotor](https://keigan-motor.com/)とは「ラピッドプロトタイピング」（迅速な試作）用に開発された、無線モジュールやセンサーが全て一体化され、位置制御・トルク制御等がコマンドで制御できる「モーターモジュール®」です。  

本記事では以下の内容を紹介します。

+ RaspberryPi上のNode-RED用からKeiganMotorを制御できるノードのインストール方法。
+ 回転、停止、距離を指定して往復運動を行うフローの作成。
+ ReSpeakerとKeiganMotorを使用した例。

サンプルのフロー、ソースコードは記事の最後尾に掲載しています。

##作業環境
+ Raspberry Pi 3 B+
+ Node-RED v1.0.2
+ Node.js v10.16.3

##node-red-contrib-keigan-motor-sequencerのインストール
「パレットの管理」＞「ノードを追加」から
「[node-red-contrib-keigan-motor-sequencer](https://flows.nodered.org/node/node-red-contrib-keigan-motor-sequencer)」で検索してインストールします
![search_palette.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103952/002471ec-a091-c00e-87f2-a0c6e3b540bd.png)


インストールに成功すると、左側のノードに「Keigan Motor」カテゴリ内に2つのノードが出てきます。

<b>インストール直後はUSBを認識しない場合があります。その場合はraspberry Piを再起動して下さい。</b>

![p_node.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103952/fd372a5a-855b-6dac-aec9-6a1ce0562ed2.png)

<dl>
    <dt>USB Selector</dt>
    <dd>USB接続したKeiganMotorを検出し接続するノード。接続したモーターを出力する。</dd>
    </dl>
<dl>
    <dt>Sequencer</dt>
    <dd>モーターを操作するコマンドが設定でき、入力から送られてきたモーターに対して設定したコマンド操作を行う。「USB Selector」の後方に繋いで使用する。</dd>
</dl>

##モーターの準備
KeiganMotorの給電ポート(USB Type-C)をバッテリーに、通信用ポート(USB MicroB)をRaspberry Piに接続します。
![con.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103952/ea48ddce-cca8-1900-8324-9e2a77557c0f.jpeg)

モバイルバッテリーの場合、過充電防止の為、モーターの負荷が高くなると、給電を停止する物が多いです。
負荷の高い動作や、車輪に使う場合は、3A出力の物やUSB充電器等の使用をお勧めします。

##取りあえず回してみる
フローに「inject」「USB Selector」「Sequencer」ノードを以下のように配置し、Sequencerノードの設定パネルで以下のようにします

![1-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103952/5979fe7b-b40a-d760-0fd7-8d6a625f74aa.png)

<dl>
<dt>USB Selectorの入力オプション</dt>
<dd>
<ul>
<li>USB Selectorノードの入力msg.payloadに"scan"（injectノードに"scan"）を設定する事で、Raspiに接続されているUSBモーターをNode-REDに認識させます。</li>
<li>モーターが認識されている場合は「USB Selector」に検出したモーター名が表示されます。一度認識すれば、Node-REDが再起動するまで有効です。</li>
<li>フロー上のUSB Selector設定パネルのスキャンでも認識しますが、Node-REDを起動させて自動実行する場合には、必要になります。</li>
</ul></dd>
</dl>

#####Sequencerノード
Sequencerノードのモーターコマンドは設定パネルからGUIで選択できます。
<dl>
<dt>{"cmd":"cmdEnable"}</dt><dd>モーターの動作を許可する（安全装置。一度設定すれば、再起動するまで有効）</dd>
</dl>
![cmdEnable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103952/4722549d-24a2-67dd-5569-146219aa8524.png)


<dl>
<dt>{"cmd":"cmdRun_rpm","arg":[10]}</dt><dd>速度設定 10rpmで回転  </dd>
</dl>
![cmdRun_rpm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103952/df564784-b276-f5a4-ba1d-8a92522b4852.png)

続いてモーターの停止フローも作成します
![stopflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103952/9e51dfdc-c214-c187-e58b-0fc01ea251af.png)

<dl>
    <dt>{"cmd":"cmdStop"}</dt><dd>モーターの動作を止める（その場で停止・磁力がかかり固まる）</dd>
    <dt>{"cmd":"cmdDisable"}</dt><dd>モーターへの通電を止める（その場で停止・空転する）</dd>
</dl>

#####完成した物はこちら(YouTube)

「回転開始」「停止」で動作を確認（モーターが認識されていない場合は先に「scan」を押す）

[![YouTube](http://img.youtube.com/vi/OwpJjfh40LY/0.jpg)](http://www.youtube.com/watch?v=OwpJjfh40LY)

##往復運動をさせる

一定の距離の往復を繰り返してみます。  
以下のようにUSB Selector、Sequencerノードを繋ぎます。

![loopflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103952/c559470b-d335-87c1-c6a5-76f852f0c091.png)

   <dl>
    <dt>cmdMoveByDistanceSync（移動先座標,移動速度,タイムアウト）</dt>
    <dd>移動先の座標に移動を完了したら、次のノードを実行します。   
従って、2つの「cmdMoveByDistanceSync」ノードに移動先の上限・下限を指定してループで繋ぐと、モーターが往復運動をおこないます。   
単位はradianで以下の例では、左1回転(6.28)、右1回転(-6.28)の合計2回転分を往復します。</dd>
    </dl>
![syncp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103952/b824defe-48bc-cb52-4122-3d80aa3100c0.png)

注意点として<b>「エラー時にメッセージの伝播を停止する」</b>のチェックは必ず入れて下さい。
Sequencerノードは入力があった場合、直ちにコマンドを実行して、その結果を出力します。
上記の場合、ノードがループして接続されている為、失敗した場合にも失敗の結果が出力され、無限ループに陥ります。

#####完成した物はこちら(YouTube)
[![YouTube](http://img.youtube.com/vi/IuvPbYBBI2c/0.jpg)](http://www.youtube.com/watch?v=IuvPbYBBI2c)

##応用 （音の方向に追従）
これだけだと面白くないので、ReSpeaker Mic Array v2.0と組み合わせて、音の方向を向くようにしてみました。
![moveflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103952/97e6e896-6010-becd-d81a-fc38530e888c.png)
[ReSpeaker](https://www.switch-science.com/catalog/3851/)は複数のマイクを組み合わせて、音声を正確にキャプチャーする事が可能なデバイスです。ReSpeakerには音声の方向を出力するAPIがあるので、今回はそれを使用してモーターの座標にマップしました。

ReSpeakerの音声方向の取得には以下の記事を参考にpythonで方向のみを出力するスクリプトを作成し、execノードで値を取得しています。
[https://tech.motoki-watanabe.net/entry/2018/09/02/034951](https://tech.motoki-watanabe.net/entry/2018/09/02/034951)

音声方向の取得部分のソースコードを一式アップしますので、ReSpeaker Mic Array v2.0をお持ちの方は試してみて下さい。
https://cdn-awscloudfrontorigin.keigan-motor.com/sample/usb_4_mic_array.zip

+ /home/pi/usb_4_mic_array にアップロード
+ ```$sudo pip3 install pyusb ``` でpythonの依存ライブラリのインストール


#####完成した物はこちら(YouTube)
[![YouTube](http://img.youtube.com/vi/845f83v4RFk/0.jpg)](http://www.youtube.com/watch?v=845f83v4RFk)

##フロー
今回作成したフローです

```json:回転・往復運動.json
[{"id":"461d5a5f.bdbf04","type":"tab","label":"デモ","disabled":false,"info":""},{"id":"a85f5d59.ee50c","type":"km-motor-usb-selector","z":"461d5a5f.bdbf04","name":"","selectMotorName":"","selectMotorNameself":false,"isInitializScan":true,"isMotorKeepalive":true,"x":580,"y":220,"wires":[["437b9cc6.b48824"]]},{"id":"437b9cc6.b48824","type":"km-motor-sequencer","z":"461d5a5f.bdbf04","name":"","cmdJson":"{\"cmd\":\"cmdEnable\"}","isErrStop":true,"x":800,"y":220,"wires":[["f8aed4cf.501a98"]]},{"id":"f8aed4cf.501a98","type":"km-motor-sequencer","z":"461d5a5f.bdbf04","name":"","cmdJson":"{\"cmd\":\"cmdRun_rpm\",\"arg\":[10]}","isErrStop":true,"x":1080,"y":220,"wires":[[]]},{"id":"bd03af87.d6137","type":"inject","z":"461d5a5f.bdbf04","name":"","topic":"","payload":"scan","payloadType":"str","repeat":"","crontab":"","once":true,"onceDelay":"1","x":350,"y":100,"wires":[["a3b1d26b.7cf2f"]]},{"id":"a3b1d26b.7cf2f","type":"km-motor-usb-selector","z":"461d5a5f.bdbf04","name":"","selectMotorName":"","selectMotorNameself":false,"isInitializScan":true,"isMotorKeepalive":true,"x":520,"y":100,"wires":[[]]},{"id":"bf3d74f.3985088","type":"inject","z":"461d5a5f.bdbf04","name":"回転開始","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":360,"y":220,"wires":[["a85f5d59.ee50c"]]},{"id":"4bf19bff.ed8f24","type":"comment","z":"461d5a5f.bdbf04","name":"Node-REDの初回起動等でモーターが認識していない状態の場合は実行する必要がある","info":"","x":920,"y":100,"wires":[]},{"id":"6a2c141f.27400c","type":"inject","z":"461d5a5f.bdbf04","name":"停止","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":350,"y":300,"wires":[["8b98e729.14d018"]]},{"id":"8b98e729.14d018","type":"km-motor-usb-selector","z":"461d5a5f.bdbf04","name":"","selectMotorName":"","selectMotorNameself":false,"isInitializScan":true,"isMotorKeepalive":true,"x":580,"y":300,"wires":[["ea2c8c37.a4752"]]},{"id":"ea2c8c37.a4752","type":"km-motor-sequencer","z":"461d5a5f.bdbf04","name":"","cmdJson":"{\"cmd\":\"cmdStop\"}","isErrStop":true,"x":790,"y":300,"wires":[[]]},{"id":"65bb9f42.762f5","type":"inject","z":"461d5a5f.bdbf04","name":"動作を無効","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":360,"y":380,"wires":[["7a734311.09939c"]]},{"id":"7a734311.09939c","type":"km-motor-usb-selector","z":"461d5a5f.bdbf04","name":"","selectMotorName":"","selectMotorNameself":false,"isInitializScan":true,"isMotorKeepalive":true,"x":580,"y":380,"wires":[["683bd3fb.82971c"]]},{"id":"683bd3fb.82971c","type":"km-motor-sequencer","z":"461d5a5f.bdbf04","name":"","cmdJson":"{\"cmd\":\"cmdDisable\"}","isErrStop":true,"x":800,"y":380,"wires":[[]]},{"id":"8c219e06.26f5c","type":"km-motor-usb-selector","z":"461d5a5f.bdbf04","name":"","selectMotorName":"","selectMotorNameself":false,"isInitializScan":true,"isMotorKeepalive":true,"x":580,"y":540,"wires":[["f78175ad.17a8b8"]]},{"id":"4d336553.d6205c","type":"km-motor-sequencer","z":"461d5a5f.bdbf04","name":"","cmdJson":"{\"cmd\":\"cmdMoveByDistanceSync\",\"arg\":[6.28,6.28,null]}","isErrStop":true,"x":550,"y":640,"wires":[["ca0ae616.66f0c8"]]},{"id":"ca0ae616.66f0c8","type":"km-motor-sequencer","z":"461d5a5f.bdbf04","name":"","cmdJson":"{\"cmd\":\"cmdMoveByDistanceSync\",\"arg\":[-6.28,6.28,null]}","isErrStop":true,"x":1020,"y":640,"wires":[["4d336553.d6205c"]]},{"id":"e0cff2d0.0b945","type":"inject","z":"461d5a5f.bdbf04","name":"回転開始","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":360,"y":540,"wires":[["8c219e06.26f5c"]]},{"id":"f78175ad.17a8b8","type":"km-motor-sequencer","z":"461d5a5f.bdbf04","name":"","cmdJson":"{\"cmd\":\"cmdEnable\"}","isErrStop":true,"x":840,"y":540,"wires":[["4d336553.d6205c"]]}]
```

```json:音の方向に追従.json
[{"id":"5355c38e.29b0fc","type":"tab","label":"音声追従","disabled":false,"info":""},{"id":"935c2204.3a753","type":"km-motor-usb-selector","z":"5355c38e.29b0fc","name":"","selectMotorName":"","selectMotorNameself":false,"isInitializScan":true,"isMotorKeepalive":false,"x":320,"y":100,"wires":[["cb9290cf.9b10e","7039f831.af4a88"]]},{"id":"1f863b5f.ee7785","type":"inject","z":"5355c38e.29b0fc","name":"開始","topic":"","payload":"scan","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":"2","x":130,"y":100,"wires":[["935c2204.3a753","790d341a.700ccc"]]},{"id":"9b1dc88.90e2a38","type":"exec","z":"5355c38e.29b0fc","command":"sudo python3 -u /home/pi/usb_4_mic_array/main.py","addpay":false,"append":"","useSpawn":"true","timer":"","oldrc":false,"name":"","x":700,"y":400,"wires":[["b981447d.08d408"],[],[]]},{"id":"166a8b93.b9b714","type":"inject","z":"5355c38e.29b0fc","name":"モーターの停止","topic":"","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":160,"y":700,"wires":[["e13e8686.b77c28"]]},{"id":"e13e8686.b77c28","type":"km-motor-usb-selector","z":"5355c38e.29b0fc","name":"","selectMotorName":"","selectMotorNameself":false,"isInitializScan":false,"isMotorKeepalive":false,"x":380,"y":700,"wires":[["dec50a53.443498"]]},{"id":"dec50a53.443498","type":"km-motor-sequencer","z":"5355c38e.29b0fc","name":"","cmdJson":"{\"cmd\":\"cmdDisable\"}","isErrStop":false,"x":600,"y":700,"wires":[[]]},{"id":"cb9290cf.9b10e","type":"km-motor-sequencer","z":"5355c38e.29b0fc","name":"","cmdJson":"{\"cmd\":\"cmdPresetPosition\",\"arg\":[0]}","isErrStop":false,"x":630,"y":220,"wires":[["985be5bd.f26bb8"]]},{"id":"985be5bd.f26bb8","type":"km-motor-sequencer","z":"5355c38e.29b0fc","name":"","cmdJson":"{\"cmd\":\"cmdSpeed_rpm\",\"arg\":[30]}","isErrStop":false,"x":970,"y":220,"wires":[["7a973b74.12a404"]]},{"id":"ebe0c4d2.4a3948","type":"km-motor-sequencer","z":"5355c38e.29b0fc","name":"","cmdJson":"{\"cmd\":\"cmdMoveToPosition\",\"arg\":\"msg.payload\"}","isErrStop":false,"x":940,"y":560,"wires":[[]]},{"id":"b981447d.08d408","type":"function","z":"5355c38e.29b0fc","name":"msg.payload *= (Math.PI/180);","func":"msg.payload =(msg.payload* (Math.PI/180));\n\nreturn msg;","outputs":1,"noerr":0,"x":390,"y":560,"wires":[["62e0ac5c.450bc4"]]},{"id":"7a973b74.12a404","type":"km-motor-sequencer","z":"5355c38e.29b0fc","name":"","cmdJson":"{\"cmd\":\"cmdEnable\"}","isErrStop":false,"x":1260,"y":220,"wires":[[]]},{"id":"62e0ac5c.450bc4","type":"km-motor-usb-selector","z":"5355c38e.29b0fc","name":"","selectMotorName":"","selectMotorNameself":false,"isInitializScan":true,"isMotorKeepalive":false,"x":620,"y":560,"wires":[["ebe0c4d2.4a3948"]]},{"id":"885b9856.d8b128","type":"status","z":"5355c38e.29b0fc","name":"","scope":["9b1dc88.90e2a38"],"x":1020,"y":400,"wires":[["790d341a.700ccc"]]},{"id":"790d341a.700ccc","type":"function","z":"5355c38e.29b0fc","name":"main.pyの二重起動防止","func":"if(msg.status&&msg.status.text.indexOf(\"pid\")===0){\n    context.set(\"pid\", msg.status.text);\n    return null;\n}else if(!context.get(\"pid\")){\n   return msg; \n}else{\n     return null; \n}\n","outputs":1,"noerr":0,"x":370,"y":400,"wires":[["9b1dc88.90e2a38"]]},{"id":"2c7dac0.0887854","type":"comment","z":"5355c38e.29b0fc","name":"ReSpeaker Mic Array v2.0からマイク座標を取得","info":"","x":440,"y":340,"wires":[]},{"id":"446889f1.b532c8","type":"comment","z":"5355c38e.29b0fc","name":"マイク座標をモーターの座標へ変換してその位置へ移動させる","info":"","x":480,"y":500,"wires":[]},{"id":"9d6b4af2.fcfd88","type":"comment","z":"5355c38e.29b0fc","name":"モーターの位置と速度を設定","info":"","x":600,"y":180,"wires":[]},{"id":"bd91ad02.d8dd5","type":"comment","z":"5355c38e.29b0fc","name":"現在の座標 (デバッグ用)","info":"","x":580,"y":60,"wires":[]},{"id":"7039f831.af4a88","type":"km-motor-sequencer","z":"5355c38e.29b0fc","name":"","cmdJson":"{\"cmd\":\"onMotorMeasurement\"}","isErrStop":false,"x":620,"y":100,"wires":[["2dcc41bd.ded7fe"]]},{"id":"2dcc41bd.ded7fe","type":"debug","z":"5355c38e.29b0fc","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":870,"y":100,"wires":[]}]
```

##参考
+ [node-red-contrib-keigan-motor-sequencer](https://flows.nodered.org/node/node-red-contrib-keigan-motor-sequencer)
+ [ReSpeaker Mic Array v2.0](https://www.switch-science.com/catalog/3851/)
+ https://tech.motoki-watanabe.net/entry/2018/09/02/034951](https://tech.motoki-watanabe.net/entry/2018/09/02/034951