AWS-IoT-Core

AWS IoTとラズパイでWEBからDCモーターを回してみた

はじめに

こんにちは、omkです。
ここ1ヶ月くらいずっと電子工作にハマっています。
当社で挑戦中のパタパタ企画(詳しくは→【STAR★FLAP 出発進行!】各駅停車「自動パタパタ展示」行き ~始発駅:企画編~ )でモーターの制御部分の協力要請を受けたことをきっかけに電子工作を始めてみました。
パタパタの制御の話はもうじきブログに公開されると思うのでもしご興味あればお待ち下さい。

そんなわけでそれとは関係なくラズパイに接続したDCモーターをAWS IoTとMQTTでインターネット経由で操作してみました。

作ってみた

図にするとこんな感じです。

CloudFrontとS3でホストした静的サイトからAPI GatewayのWebAPIを実行して、LambdaからMQTTトピックに操作命令をパブリッシュします。
ラズパイはPythonスクリプトでAWS IoTに接続してMQTTトピックをサブスクライブします。
そしてサブスクライブしたトピックから命令を受け取って処理を実行します。

WEBからの操作部分

想定している操作内容としては以下で考えています。

  • モーターを回転し続けさせる操作
  • 回っているモーターを停止させる操作

よって { 'action': 'start' }' で回り始めて { 'action': 'stop' } で停止するようにします。

LambdaはAPIのURIのクエリ文字を受けてこれらの命令をトピックにパブリッシュします。

import json
import boto3

iot = boto3.client('iot-data')
topic = 'omkMotor/subscribe'

def lambda_handler(event, context):
    # TODO implement
    queries = event['queryStringParameters'];
    print(queries)

    if 'action' not in queries:
        return {
            'statusCode': '400',
            "headers": {
                "Content-Type": "application/json",
                "Access-Control-Allow-Origin": '*'
            },
            'body': "Request's QueryStrings are wrong"
        }

    action = queries['action']

    payload = {
        "action": action
    }

    try:
        iot.publish(
            topic=topic,
            qos=0,
            payload=json.dumps(payload, ensure_ascii=False)
        )
        return {
            'statusCode': '200',
            "headers": {
                "Content-Type": "application/json",
                "Access-Control-Allow-Origin": '*'
            },
            'body': 'Succeeeded'
        }

    except Exception as e:
        print(e)
        return {
            'statusCode': '500',
            "headers": {
                "Content-Type": "application/json",
                "Access-Control-Allow-Origin": '*'
            },
            'body': e
        }

このAPIに対してGETリクエストを送信するようにHTMLを用意します。

<!DOCTYPE html>
<html>
<head>
  <title>omk Motor</title>
    <style>
      /* ボタンのスタイル */
      .button-container {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
      }

      .button {
        width: 200px;
        height: 200px;
        border-radius: 50%;
        color: #fff;
        font-size: 24px;
        display: flex;
        justify-content: center;
        align-items: center;
        cursor: pointer;
        outline: none;
        border: none;
        transition: all 0.2s;
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4); /* 影の追加 */
        margin: 0 10px; /* ボタンの間隔 */
      }

      /* STARTボタンのスタイル */
      #startButton {
        background-color: #B22222; /* 明るい臙脂色 */
      }

      /* STOPボタンのスタイル */
      #stopButton {
        background-color: #4169E1; /* 群青色 */
      }

      /* ボタンが押されたときのスタイル */
      .button:active {
        transform: scale(0.95);
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); /* クリック時の影の変更 */
      }
    </style>
</head>
<body>
  <div class="button-container">
    <button id="startButton" class="button">START</button>
    <button id="stopButton" class="button">STOP</button>
  </div>

  <script>
    // ボタン要素を取得
    var startButton = document.getElementById("startButton");
    var stopButton = document.getElementById("stopButton");

    // STARTボタンのクリックイベントのリスナーを設定
    startButton.addEventListener("click", function() {
      startButton.disabled = true; // ボタンを無効化
      fetch("https://{対象APIのFQDN}/?action=start")
        .then(response => {
          console.log("API request sent - start");
        })
        .catch(error => {
          console.error("API request error - start:", error);
        })
        .finally(() => {
          startButton.disabled = false; // ボタンを有効化
        });
    });

    // STOPボタンのクリックイベントのリスナーを設定
    stopButton.addEventListener("click", function() {
      stopButton.disabled = true; // ボタンを無効化
      fetch("https://{対象APIのFQDN}/?action=stop")
        .then(response => {
          console.log("API request sent - stop");
        })
        .catch(error => {
          console.error("API request error - stop:", error);
        })
        .finally(() => {
          stopButton.disabled = false; // ボタンを有効化
        });
    });
  </script>
</body>
</html>

こんなかんじのかわいいページが出来ました。

試しに実行してみてAWS IoTのテストクライアントでメッセージの確認を行います。

トピックにメッセージが到達しました。
これでWEBからトピックに対して命令を送信できるようになりました。

ラズパイのサブスクライブと命令の実行

次にラズパイの設定を進めていきます。
今回はMQTTライブラリには「paho」をモーターの制御ライブラリには「gpiozero」を利用します。

import ssl
import paho.mqtt.client as mqtt
import json
from gpiozero import Motor

# AWS IoTの接続情報
awshost = "{AWS IoTの接続先FQDN}"
awsport = 8883  # MQTT over TLSのデフォルトポート
keepalive = 1200

clientId = "omkMotor"
thingName = "omkMotor"

caPath = "./AmazonRootCA1.pem"
certPath = "./omkMotor.cert.pem"
keyPath = "./omkMotor.private.key"

topic = "omkMotor/subscribe"

# モーターの設定
motor = Motor(13, 12)

def on_connect(client, userdata, flags, rc):
    print("Connected with result code " + str(rc))
    client.subscribe(topic)

def on_message(client, userdata, msg):
    json_message = str(msg.payload,"utf-8")
    message = json.loads(json_message)
    print("action: " + message['action'])

    if "action" not in message.keys():
       return

    if message['action'] == "start":
       motor.forward(0.8)

    if message['action'] == "stop":
       motor.stop()

def main():
  client = mqtt.Client(clientId, protocol=mqtt.MQTTv311)

  # TLS接続の設定
  client.tls_set(caPath, certfile=certPath, keyfile=keyPath, cert_reqs=ssl.CERT_REQUIRED,tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None)

  client.on_connect = on_connect
  client.on_message = on_message

  # MQTTブローカーへの接続
  client.connect(awshost, awsport, keepalive)

  # メッセージの送受信を継続
  client.loop_forever()

if __name__ == "__main__":
    main()

MQTTクライアントでAWS IoTに対して接続して対象トピックをサブスクライブします。
サブスクライブしたトピックにメッセージが送信されてきた場合はメッセージの内容をもとに命令を実行します。

レイテンシーも気にならずいい感じにモーターを回すことができました。

おわりに

いい感じにWEBからモーターが回せるようになりました。
今回はモーターを回すか止めるかしか考えませんでしたが状態を管理・取得したりしてスマートにしていきたいですね。

以上、最後までお付き合いありがとうございました。

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA