【電子工作】PCとラズパイ間のMQTT通信
これまでの実験。
wanko-sato.hatenablog.com
wanko-sato.hatenablog.com
これまではゲームパッドなりセンサデータなり、とにかくPCからラズパイにデータを飛ばすのにBluetoothを使ってきました。シリアル通信で非常に簡単ではあるのですが、以下の弱点を持ちます。
- 近距離通信である
- ペアリングしなければならない
家の中で実験する分にはこれで何ら問題ないのですが、例えば東京-大阪間での遠隔操作実験になってしまうとBluetoothではいかんともしがたくなってしまいます。そこで、遠距離通信可能な、MQTTサーバを経由した通信の実装を試みました。
MQTTとは?
IoT界隈の方々には当たり前の話と思うのですが、要するに「一回の通信あたり、少量のデータを省電力で行う」ための通信プロトコルです。また、1対1通信ではなく、1対多通信や双方向通信を可能にしているため、IoTの主要な構成である「少数の制御サーバ-大量のエッジコンピュータ」という通信ができるようになっています。さらに詳しくは下記のリンク先などをご覧ください。
全体構成
PCとラズパイの間に、MQTT Brokerと呼ばれる、MQTTによる通信を仲介するサーバを設置します。PCからBrokerにデータを"publish"することで、Brokerに対して"subscribe"で待機しているラズパイがそのデータを受け取ることができます。その逆も可能。
MQTTでは送受信するデータに「トピック」と呼ばれる属性を持たせることで、複数のデータが同時に入ってきても混乱しない仕組みを実現しています。これにより1対多の通信も問題なく行えるようになっています。
クラウドのMQTT Broker
自前PCをMQTT Brokerにすることもできるようなのですが、いろいろ面倒なのでクラウドサービスを利用するのが簡単です。
テスト用であれば、
を使うのが一番簡単なように思います。今回の実装テストもこれを使っています。ほかにも、無料でテストできるMQTT Brokerはあるようですので、調べてみてください。
なお、お金がかかっても良い、ということでしたら、HerokuのCloudMQTTが各所でおすすめされています。
independence-sys.net
こちらの記事では無料との記載になっていますが、現在Herokuにアドオンで入れられるCloudMQTTは無料版がなくなっており、最安で5$のプランから担っています。月額500円程度ですし、実験に使う分には払っても良い費用かな?とも思いますし、サービス展開するならば有料版を使ってちゃんとセキュリティやらなんやらを確保したうえでやるのが良いと思います。
実装テスト
ここから実装テストをしていくわけですが、コードは以下のサイトからほぼ丸パクリさせていただいています。悪しからず。
qiita.com
また、テストで使用したMQTT Brokerはmqtt.uko.jpです。こちらは毎日パスワードが変わる仕様ですので、コード中のパスワードはその日のものを使うようにしてください。
なお、MQTTクライアントのmosquittoも試したのですが、Window10からの通信がいまいちうまくいかず、ここでは使っていません。
準備
paho-mqttのインストール
PC、ラズパイの両方に、pythonのmqtt通信用ライブラリであるpaho-mqttをインストールします。
Anaconda環境の場合は
conda install -c conda-forge paho-mqtt
pipの場合は
pip install paho-mqtt
でOKです。
実装テスト1 通信テスト
まずはpahoでPCからpublishしたmessageがラズパイ側でsubscribeできるかどうかのチェックです。
こちらのリンク先のコードを使用してください。publishのコードをpub.py、subscribeのコードをsub.pyなどとして、pub.pyをPCに、sub.pyをラズパイに保存します。あとはそれぞれ実行すればOKなんですが、今回はMQTT Brokerにmqtt.uko.jpを使うので、接続用のパラメータをいくつか変更する必要があります。
# for MQTT communication via mqtt.uko.jp broker = 'mqtt.uko.jp' port = 1883 topic = "YOUR_TOPIC_NAME" n = random.randint(0, 100) client_id = 'python-mqtt-{:d}'.format(n) username = 'icecream' password = 'xxxxxxxx' # Password is changed everyday.
broker, topic, passwordの3か所を変更してください。brokerは記載した通り、topicはお好きなものを、passwordはその日のパスワードを入力します。
これでうまく通信できていれば問題ありません。
実装テスト2 センサデータを送る
では、次にArduinoで取得したセンサデータをMQTTでうまく送れるかを確認します。前回つかったCdSセンサを使います。
コード
Arduinoに接続したCdSセンサデータをPCからpublishし、ラズパイでsubscribeします。PC、ラズパイのいずれでもpygameを使って取得した値を表示させるようにしています。
まずはPCのpublish側から。topic, passwordを適宜変更するのを忘れずに。
import pygame # for serial communnication import serial # mqtt communication from paho.mqtt import client as mqtt_client import random # serial port open print('COM port open') # Arduion側のポート設定 # ポート名はコンパネのデバイスとプリンターで確認できる s_a = serial.Serial('COM5', 9600, timeout=10) # for MQTT communication via mqtt.uko.jp broker = 'mqtt.uko.jp' port = 1883 topic = "YOUR_TOPIC_NAME" client_id = f'python-mqtt-{random.randint(0, 100)}' username = 'icecream' password = 'xxxxxxxx' # Password is changed everyday. # pygame window setting BLACK = (0,0,0) WHITE = (255, 255, 255) WIDTH = 320 HEIGHT = 240 # mqtt connect def connect_mqtt(): def on_connect(client, userdata, flags, rc): if rc == 0: print("Connected to MQTT Broker!") else: print("Failed to connect, return code %d\n", rc) client = mqtt_client.Client(client_id) client.username_pw_set(username, password) client.on_connect = on_connect client.connect(broker, port) return client # mqtt publish def publish(client, msg): msg = f"messages: {msg}" result = client.publish(topic, msg) status = result[0] if status == 0: print(f"Send `{msg}` to topic `{topic}`") else: print(f"Failed to send message to topic `{topic}`") # connect to mqtt server client = connect_mqtt() ''' arduino側は特に実行せず、python側のコードのみ実行すれば良い その際、シリアルモニタは切っておっこと。シリアルモニタがポートを占有してしまうため ただし、IDEを立上げてコンパイルしておかないといけない ''' def main(): # variables for data showing pygame.init() surface = pygame.display.set_mode((WIDTH, HEIGHT)) myfont = pygame.font.Font(None, 30) myclock = pygame.time.Clock() endflag = 0 # loop until showwindow close while endflag == 0: # ウィンドウが閉じられるまでwhileループを実行する for event in pygame.event.get(): if event.type == pygame.QUIT: endflag = 1 m = s_a.readline().decode('utf-8').replace('\r\n','') # まれにArduino側の送信がミスることがあるようなので # ValueErrorが発生したときは一瞬0を代入することにする try: m = float(m) except ValueError: m = float(0) # CdSセンサデータをMQTT Brokerにpublishする publish(client, m) # pygameで現在の値を表示 surface.fill(WHITE) bitmaptext = myfont.render('resist:{}'.format(m), True, BLACK) surface.blit(bitmaptext, (50, 75)) pygame.display.update() myclock.tick(60) # whileループから抜けたらシリアル通信をcloseする s_a.close() pygame.quit() if __name__ == '__main__': main()
次にラズパイのsubscribe側。
import pygame # mqtt communication from paho.mqtt import client as mqtt_client import random # for MQTT communication via mqtt.uko.jp broker = 'mqtt.uko.jp' port = 1883 topic = "YOUR_TOPIC_NAME" n = random.randint(0, 100) client_id = 'python-mqtt-{:d}'.format(n) username = 'icecream' password = 'xxxxxxxx' # Password is changed everyday. current_m = '' flg = 0 BLACK = (0,0,0) WHITE = (255, 255, 255) WIDTH = 320 HEIGHT = 240 def connect_mqtt() -> mqtt_client: def on_connect(client, userdata, flags, rc): if rc == 0: print("Connected to MQTT Broker!") else: print("Failed to connect, return code %d\n", rc) client = mqtt_client.Client(client_id) client.username_pw_set(username, password) client.on_connect = on_connect client.connect(broker, port) return client def subscribe(client: mqtt_client): def on_message(client, userdata, msg): global current_m current_m = msg.payload.decode() client.subscribe(topic) client.on_message = on_message def main(): client = connect_mqtt() pygame.init() surface = pygame.display.set_mode((WIDTH, HEIGHT)) myfont = pygame.font.Font(None, 30) myclock = pygame.time.Clock() while flg == 0: for event in pygame.event.get(): if event.type == pygame.QUIT: end == 1 client.loop_start() subscribe(client) m = current_m.split() if len(m) < 1: m = 0 else: m = m[1] print(m) client.loop_stop() surface.fill(WHITE) bitmaptext_0 = myfont.render('resist:{}'.format(float(m)), True, BLACK) surface.blit(bitmaptext_0, (50, 75)) pygame.display.update() pygame.quit() if __name__ == '__main__': main()
注意箇所1
いろいろ調べてみたところ、subscribe()の中のon_massageはどうやらそのままだと戻り値が返らないようで、current_mを事前に定義し、globalとして値を返しています。
def subscribe(client: mqtt_client): def on_message(client, userdata, msg): global current_m current_m = msg.payload.decode() client.subscribe(topic) client.on_message = on_message
注意箇所2
また、subscribe()を実行するにはloop.start()~loop_stop()で囲んであげないとうまく動かないようです。
client.loop_start() subscribe(client) m = current_m.split() if len(m) < 1: m = 0 else: m = m[1] print(m) client.loop_stop()
注意箇所3
client = connect_mqtt()がmain()の中に入っていますが、これはたぶん中でも外でも問題ないはずです。