【電子工作】PCとラズパイ間のMQTT通信

これまでの実験。

wanko-sato.hatenablog.com
wanko-sato.hatenablog.com

これまではゲームパッドなりセンサデータなり、とにかくPCからラズパイにデータを飛ばすのにBluetoothを使ってきました。シリアル通信で非常に簡単ではあるのですが、以下の弱点を持ちます。

  • 近距離通信である
  • ペアリングしなければならない

家の中で実験する分にはこれで何ら問題ないのですが、例えば東京-大阪間での遠隔操作実験になってしまうとBluetoothではいかんともしがたくなってしまいます。そこで、遠距離通信可能な、MQTTサーバを経由した通信の実装を試みました。

MQTTとは?

IoT界隈の方々には当たり前の話と思うのですが、要するに「一回の通信あたり、少量のデータを省電力で行う」ための通信プロトコルです。また、1対1通信ではなく、1対多通信や双方向通信を可能にしているため、IoTの主要な構成である「少数の制御サーバ-大量のエッジコンピュータ」という通信ができるようになっています。さらに詳しくは下記のリンク先などをご覧ください。

amg-solution.jp

全体構成

f:id:wanko_sato:20220109170659p:plain

PCとラズパイの間に、MQTT Brokerと呼ばれる、MQTTによる通信を仲介するサーバを設置します。PCからBrokerにデータを"publish"することで、Brokerに対して"subscribe"で待機しているラズパイがそのデータを受け取ることができます。その逆も可能。
MQTTでは送受信するデータに「トピック」と呼ばれる属性を持たせることで、複数のデータが同時に入ってきても混乱しない仕組みを実現しています。これにより1対多の通信も問題なく行えるようになっています。

クラウドのMQTT Broker

自前PCをMQTT Brokerにすることもできるようなのですが、いろいろ面倒なのでクラウドサービスを利用するのが簡単です。
テスト用であれば、

mqtt.uko.jp

を使うのが一番簡単なように思います。今回の実装テストもこれを使っています。ほかにも、無料でテストできる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センサを使います。

回路

f:id:wanko_sato:20220108140248p:plain

前回使用したCdSセンサの回路をそのまま使います。前回と違うのは、ラズパイを立ち上げてsubscribe状態にしておくことです。

コード

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

stackoverflow.com

注意箇所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()の中に入っていますが、これはたぶん中でも外でも問題ないはずです。

まとめ

これで、これまでBluetoothで通信していたところをMQTT通信に変更できました。
今回は接続するデバイス(=センサ)をひとつだけにしましたが、topicを変えることで複数のセンサデータを送ることも十分できるはずです。毎回Bluetoothのペアリングをやり直していたわずらわしさからも解放されるでしょう。まぁ、mqtt.uko.jpを使うので、毎回パスワードを変えなければいけませんが、Bluetoothのペアリングに比べればずっと簡単です。

というわけで今回はいじょ。