EV3を動かしてみる (7) 〜 MQTTを使った通信 〜

ロボティクス入門ゼミ 教材 2017-12-27公開 (最新版2018-10-19)

MQTTというプロトコルを使ってEV3間あるいはEV3とパソコン間などの通信を行ってみる。

MQTTについて

MQTTは、ブローカー(仲介者)を介してメッセージ(データ)のやりとりをする軽量のプロトコル(通信規約)。 ブローカーはMQTTサーバとも呼ばれる。 また、メッセージを送信する側をパブリッシャー(出版者)、 受信する側をサブスクライバー(購読者)と呼ぶ。 パブリッシャーやサブスクライバーは単数でも複数でもかまわない。 パブリッシャーはトピック(話題)を指定してメッセージをブローカーに送る(直接サブスクライバーに送るのではない)。 そしてブローカーがそのトピックを購読しているサブスクライバーにメッセージを配信する。

例えば、あるEV3から他のEV3にメッセージを転送する場合、 それを仲介するためのブローカーが必要である。 ブローカーを別途用意したりパソコンをブローカーとして使用してもかまわないが、 以下の例のように、一方のEV3をブローカとして使用すると便利である。 つまり、そのEV3は他のEV3やパソコンからのメッセージを受信する場合はブローカー兼サブスクライバーとして機能し、 センサの値などを他のEV3やパソコンに送信する場合は、自分自身がパブリッシャーにもなる。

ev3devのサイトでMQTTを使ったサンプルが紹介されているので、それも参考にする。
http://www.ev3dev.org/docs/tutorials/sending-and-receiving-messages-with-mqtt/

準備と動作確認

以下、パソコン側のシェルは、EV3側と区別するために次にようなプロンプトで表記してある。

your_pc:~$ 

ところで、パソコンが利用できない、あるいは、WindowsやMacを使っているのでセットアップをするのが面倒、 というような場合には、別のEV3をパソコンとして利用してもかまわない。 もちろん通常のパソコンではなく Raspberry Pi を始めとしたシングルボードコンピュータでも十分作業できる。

EV3にMQTTサーバとクライアントをインストール

MQTTのサーバにmosquitto、クライントにmosquitto-clientsというオープンソースのパッケージを使用する。 stretch版のev3devには、mosquitto, mosquitto-clients がすでに収録されているので新たにインストールする必要はない (2018-10-19追記)。 また、pythonからもMQTTを使えるように paho-mqtt というライブラリをインストールしておく。 paho-mqttはev3devに収録されていないので、pip3を使ってインストールする。

robot@ev3dev:~$ sudo apt update
robot@ev3dev:~$ sudo apt install python3-pip
robot@ev3dev:~$ sudo pip3 install paho-mqtt

パソコンにMQTTクライアントをインストール

パソコン側からMQTTでEV3にメッセージを送ったり受け取ったりするために、 上記のmosquitto-clientsをパソコンにもインストールしておく。 DebianやUbuntuの場合は、次のように簡単にインストールできる (WindowsやMacOSで使用する場合は各自で調べること)。

your_pc:~$ sudo apt-get install mosquitto-clients

パソコン側でもpythonを使用する場合には、上記のEV3のセットアップ同様にして paho-mqtt をインストールしておく。

MQTTサーバの動作確認

まずEV3にログインして、mosquitto (MQTTサーバ) が動いているか確認する。

robot@ev3dev:~$ sudo netstat -apt

netstatはネットワーク接続の状態を表示するコマンドで、 次の出力のように、mosquittoが1883番ポートをListenしていればOK。

Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 localhost:domain        *:*                     LISTEN      395/connmand    
tcp        0      0 *:ssh                   *:*                     LISTEN      398/sshd        
tcp        0      0 *:1883                  *:*                     LISTEN      370/mosquitto   
tcp        0      0 ev3dev:ssh              192.168.2.23:49358      ESTABLISHED 635/sshd: robot [pr

(プログラム名を表示するためには管理者権限が必要なので sudo を使った)

mosquittoが見当たらない場合は、正しくインストールされていない可能性がある。

トピックを購読する

次に、EV3をサブスクライバーにして'test'というトピック(話題)を購読(サブスクライブ)する。 トピック名は適当につけてよいが、'ev3/outA/speed_sp' のように / で区切った階層構造のようなトピック名がよく使われる。

robot@ev3dev:~$ mosquitto_sub -h 192.168.2.30 -t 'test'

この例ように、ブローカのホスト名あるいはIPアドレスを-h に続けて指定する (今回は、EV3自身がブローカーなので自分のIPアドレスを指定する)。 また、購読するトピックは-tに続けて指定する(この例では'test')。

"command not found" のようなエラーが出る場合は、 mosquitto-clients が正しくインストールされていない可能性がある。

ところでこの例では、MQTTサーバを同一のEV3上で動かしているので、IPアドレスの代わりに localhost と指定してもよい。

robot@ev3dev:~$ mosquitto_sub -h localhost -t 'test'

これで、'test'というトピックを購読している状態になる。 新しいメッセージが出版されたら、その都度ここに表示される。

購読を停止する(サブスクライブを中断する)場合は、Controlキーを押しながら c を押して強制終了する (Ctrl-c)。

パソコンからEV3へメッセージを送信

ではパソコンから'Hello, EV3'というメッセージをブローカー(を経由してサブスクライバー)に送ってみる。 メッセージは -m に続けて記述する。

your_pc:~$ mosquitto_pub -h 192.168.2.30 -t 'test' -m 'Hello, EV3!'

EV3側で次のように表示されていればOK。

robot@ev3dev:~$ mosquitto_sub -h 192.168.2.30 -t 'test'
Hello, EV3!

さらに続けていくつかの文字列や数値をメッセージとして送ってみる。 上の矢印キーで以前入力したコマンドが表示されるので、メッセージだけを変更して入力すればよい。

your_pc:~$ mosquitto_pub -h 192.168.2.30 -t 'test' -m 'Hello, EV3!'
your_pc:~$ mosquitto_pub -h 192.168.2.30 -t 'test' -m 'abcde'
your_pc:~$ mosquitto_pub -h 192.168.2.30 -t 'test' -m '2018'

EV3側で次のように表示されていればOK。

robot@ev3dev:~$ mosquitto_sub -h 192.168.2.30 -t 'test'
Hello, EV3!
abcde
2018

サブスクラブを中断する時は、Controlキーを押しながら c を押す (Ctrl-c)。

【練習問題】 パソコンとEV3の役割を交代させて、上の確認をしなさい (パソコンをサブスクライバーに、EV3をパブリッシャーにする)。

pythonスクリプトで「購読」する

pythonでは、pahoというライブラリを使ってMQTTのメッセージを送受信できる。

簡単なスクリプトを作る

基本的には、ブローカーに接続した時に実行される on_connect() という関数と、 メッセージを受け取った時に実行される on_message() という関数を用意すればよい。 これらの関数はコールバック関数と呼ばれる。

パソコンからMQTTでモータを制御する前に、まず、パソコンが出版したメッセージを受け取って表示する、という簡単なスクリプトを作ってみる。

pahoの詳細は https://pypi.python.org/pypi/paho-mqtt/1.2 を参照のこと。

#!/usr/bin/env python3

import paho.mqtt.client as mqtt  # MQTTのクライアントライブラリをインポート
from ev3dev.ev3 import *
from time import sleep

ts = TouchSensor('in3')   # スクリプトの停止にタッチセンサを使う

def on_connect(client, userdata, flags, rc):     # ブローカへの接続時に実行されるコールバック関数の定義
    print("Connected with result code "+str(rc)) # 正しく接続できれば rc は 0
    client.subscribe("ev3/outA")                 # "ev3/outA"というトピックを購読

def on_message(client, userdata, message):       # メッセージの受信時に実行されるコールバック関数の定義
    print(message.topic + " " + str(message.payload))  # topicにトピック名、payloadにメッセージが入る

def main():
    client = mqtt.Client()                # MQTTのクライアントのインスタンスを生成
    client.on_connect = on_connect        # 上で定義した接続時のコールバック関数を渡す
    client.on_message = on_message        # 上で定義した受信時のコールバック関数を渡す

    client.connect("localhost", 1883, 60) # ブローカ(自分自身)の1883番ポートに接続 (Keep Aliveは60秒)
    client.loop_start()                   # メッセージ受信ループの開始

    while ts.value() == 0:    # タッチセンサが押されていない間、繰り返し
        sleep(0.01)

    client.loop_stop()        # 受信ループを停止
    client.disconnect()       # 接続を切断

if __name__ == '__main__':
    main()

上で使った on_connect()の引数は以下の通り。

client
クライアントのインスタンス
userdata
クライアントのプライベート・ユーザ・データ (今回は使わない)
flags
ブローカーからの応答フラグ
rc
接続結果

上で使ったon_message()の引数は以下の通り。

client
クライアントのインスタンス
userdata
クライアントのプライベート・ユーザ・データ (今回は使わない)
message
MQTTMessageのインスタンス

コールバック関数としては、上記の'on_connect'と'on_message'以外に、 'on_disconnect'、'on_subscribe'、'on_unsubscribe'、'on_publish' などが使える。

スクリプトの実行

上記のスクリプトを適当な名前(例えばmqtt-test.py)で保存して実行してみる。 実行後、しばらくして"Connected with result code 0"が表示されればOK。

robot@ev3dev:~$ ./mqtt-test.py
Connected with result code 0

この状態でパソコンからメッセージをパブリッシュしてみる (将来的にはduty_cycle_spの値をメッセージとして送る予定)。

your_pc:~$ mosquitto_pub -h 192.168.2.30 -t 'ev3/outA' -m '50'
your_pc:~$ mosquitto_pub -h 192.168.2.30 -t 'ev3/outA' -m '-30'

パソコン側でパブリッシュする度にEV3側でメッセージが表示されればOK。

robot@ev3dev:~$ ./mqtt-test.py
Connected with result code 0
ev3/outA b'50'
ev3/outA b'-30'

受信するメッセージが文字列ではなくて「バイト列」であることに注意する。

パソコン側でモータの速度を指定する

前のプログラムが正しく動いたら、パソコン側でモータの速度を指定できるようにプログラムを変更する。

#!/usr/bin/env python3

import paho.mqtt.client as mqtt
from ev3dev.ev3 import *
from time import sleep

ts = TouchSensor('in3')
m = LargeMotor('outA')   # 動作確認用のモータ
m.reset()                # モータをリセット

def on_connect(client, userdata, flags, rc):
    print("Connected with result code "+str(rc))
    client.subscribe("ev3/outA")
    m.run_direct()       # モータの属性変更が即時に反映されるモードで回転

def on_message(client, userdata, message):
    print(message.topic + " " + str(message.payload))
    m.duty_cycle_sp = int(message.payload)  # 整数型に変換してから代入

def main():
    client = mqtt.Client()
    client.on_connect = on_connect
    client.on_message = on_message

    client.connect("localhost", 1883, 60)
    client.loop_start()

    while ts.value() == 0:
        sleep(0.01)

    client.loop_stop()
    client.disconnect()
    mL.stop()  # モータを停止
    mR.stop()  #

if __name__ == '__main__':
    main()

スクリプトを実行して、パソコンからメッセージを送ってみる。 指定した duty_cycle_sp でモータが回転すればOK。

【練習問題】 上のプログラムを変更して、左右のモータのスピードを同時に指定できるようにしなさい。
(ヒント)指定したい左右のモータのduty_cycle_spを'30,-40'のようなメッセージで送る。 そして得られたメッセージを dc = message.payload.decode('utf-8').split(',') のようにして、バイト列から文字列へ、文字列をさらに','で分割してリストへ変換し、最後に int(dc[0]) や int(dc[1])のように整数型に変換すれば、一つのトピックを購読するだけでよい。

pythonスクリプトで「出版」する

これまでの例では、パソコンとEV3の間でメッセージのやり取りをしたが、 EV3とEV3の間の通信も全く同じと言って良い。

以下の例は、別のEV3のモータをアクセルのように使用して、 これまで使っきたEV3をコントロールする例である。 動く方(コントロールされる側)のEV3のスクリプトは以前のものをそのまま使う。

以下はコントローラ側のプログラム例。

#!/usr/bin/env python3

import paho.mqtt.client as mqtt  # pahoのライブラリをインポート
from ev3dev.ev3 import *
from time import sleep

ts = TouchSensor('in3')  # プログラム停止用のボタンとして使用
m = LargeMotor('outA')   # コントローラとして使うモータ
m.reset()

def main():
    duty_cycle_remote = 0    # 相手側モータのduty_cycle_spとして送信する値を格納するための変数

    client = mqtt.Client()                  # MQTTのクライアントを生成
    client.connect("192.168.2.30",1883, 60) # ブローカ(192.168.2.30)の1883番portに接続
                                            # (60秒以上接続がないと切断)

    while ts.value() == 0:            # タッチセンサが押されていない間だけ繰り返し
        pos = m.position              # 現在のモータのポジションを取得

        if pos > 90:                  # ポジションからduty_cycle_remoteの値を計算する
            duty_cycle_remote = 100   # 90度以上で100、-90度以下で-100 となるようにした
        elif pos < -90:
            duty_cycle_remote = -100
        else:
            duty_cycle_remote = pos*100/90   
                             
        client.publish("ev3/outA", duty_cycle_remote)  # "ev3/outA"というトピックでブローカに送る
        time.sleep(0.01)

    client.disconnect() # 切断
		   
if __name__ == '__main__':
    main()

【練習問題】 上の例を参考にして、コントローラ側に左右のモータを付け、それを使ってリモート側の左右のモータを独立に制御できるようにしなさい。