AWS IoT Device SDKを使用して、Raspberry PiをAWS IoT Coreに接続する

AWS IoT Device SDKを使用して、Raspberry PiをAWS IoT Coreに接続する

以下のページで、AWSIoT Device SDKを使用して、AWS IoT Coreに接続するデモが紹介されています。

あわせて読みたい
チュートリアル: AWS IoT Device SDK を使用してデバイスを AWS IoT Core に接続する - AWS IoT Core AWS IoT Device SDK を使用して、デバイスを AWS IoT Core に接続する方法について説明します。

今回はRaspberry Pi 5を使用して、実際にこのデモを体験します。

構築する環境

Detail of connecting your Raspberry Pi to AWS IoT Core using the AWS IoT Device SDK.

基本的な構成は以下のページと同様です。

あわせて読みたい
Systems Managerに登録したRaspberry Piに、AWS IoT Device Clientをインストールする 【Systems Managerに登録したRaspberry Piに、AWS IoT Device Clientをインストールする】 本ページでは、AWS IoT Device Clientを取り上げます。 The AWS IoT Device C...

変更点はRaspberry Piに対して実行するSSMドキュメントの内容です。
具体的には、以下の2つの処理を実行するように修正します。

  • AWS IoT Device SDKインストール
  • 同SDKのデモ用プログラムの準備

なお今回はPythonバージョンのSDKを使用します。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

GitHub
awstut-fa/157 at main · awstut-an-r/awstut-fa Contribute to awstut-an-r/awstut-fa development by creating an account on GitHub.

テンプレートファイルのポイント解説

本構成は先述のページと概ね同様ですから、変更点を中心に取り上げます。

IoTポリシー

Resources:
  Policy:
    Type: AWS::IoT::Policy
    Properties:
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - iot:Publish
              - iot:Receive
              - iot:Subscribe
              - iot:Connect
            Resource: "*"
      PolicyName: !Sub "${Prefix}-policy"
Code language: YAML (yaml)

以下のページで指定されている通りに設定します。

あわせて読みたい
AWS IoT リソースの作成 - AWS IoT Core このチュートリアルでは、デバイスがメッセージに接続 AWS IoT Core して交換するために必要な AWS IoT リソースを作成します。

SSMドキュメント

Resources:
  RunShellScriptAssociation:
    Type: AWS::SSM::Association
    Properties:
      AssociationName: !Sub "${Prefix}-shellscript-association"
      Name: AWS-RunShellScript
      OutputLocation:
        S3Location:
          OutputS3BucketName: !Ref BucketName
          OutputS3KeyPrefix: shellscript-association-log
      Parameters:
        commands:
          - "sudo apt-get -y install cmake libssl-dev"
          
          - !Sub 'su - ${UserName} -c "git clone https://github.com/aws/aws-cli.git"'
          - !Sub 'su - ${UserName} -c "cd aws-cli && git checkout v2 && sudo pip3 install --break-system-packages -r requirements.txt"'
          - !Sub 'su - ${UserName} -c "cd aws-cli && git checkout v2 && sudo pip3 install --break-system-packages ."'
          
          - !Sub 'su - ${UserName} -c "cd ~ && python3 -m pip install awsiotsdk --break-system-packages"'
          - !Sub 'su - ${UserName} -c "git clone https://github.com/aws/aws-iot-device-sdk-python-v2.git"'
          
          - !Sub 'su - ${UserName} -c "mkdir ~/certs"'
          - !Sub 'su - ${UserName} -c "curl -o ~/certs/AmazonRootCA1.pem https://www.amazontrust.com/repository/AmazonRootCA1.pem"'
          - !Sub 'su - ${UserName} -c "sudo aws s3 cp s3://${BucketName}/${Thing}/${CertificateName} ~/certs/"'
          - !Sub 'su - ${UserName} -c "sudo aws s3 cp s3://${BucketName}/${Thing}/${PrivateKeyName} ~/certs/"'
          - !Sub 'su - ${UserName} -c "sudo aws s3 cp s3://${BucketName}/${Thing}/${PublicKeyName} ~/certs/"'
          
          - !Sub "row=`sed -n '/print(\"Received message/=' /home/${UserName}/aws-iot-device-sdk-python-v2/samples/pubsub.py`"
          - !Sub |
            cat << EOF | sed ''$row'r /dev/stdin' /home/${UserName}/aws-iot-device-sdk-python-v2/samples/pubsub.py > /home/${UserName}/aws-iot-device-sdk-python-v2/samples/pubsub2.py
                # insert start
                topic_parsed = False
                if "/" in topic:
                    parsed_topic = topic.split("/")
                    if len(parsed_topic) == 3:
                        # this topic has the correct format
                        if (parsed_topic[0] == 'device') and (parsed_topic[2] == 'details'):
                            # this is a topic we care about, so check the 2nd element
                            if (parsed_topic[1] == 'temp'):
                                print("Received temperature request: {}".format(payload))
                                topic_parsed = True
                            if (parsed_topic[1] == 'light'):
                                print("Received light request: {}".format(payload))
                                topic_parsed = True
                if not topic_parsed:
                    print("Unrecognized message topic.")
                # insert end
            EOF
          - !Sub 'su - ${UserName} -c "sudo chown ${UserName}:${UserName} ~/aws-iot-device-sdk-python-v2/samples/pubsub2.py"'
          
          - !Sub 'su - ${UserName} -c "cp ~/aws-iot-device-sdk-python-v2/samples/pubsub.py ~/aws-iot-device-sdk-python-v2/samples/pubsub3.py"'
          - !Sub |
            su - ${UserName} -c "sed -i 's/message = \"{} \[{}\]\".format(message_string, publish_count)/message = \"{}\".format(message_string)/' ~/aws-iot-device-sdk-python-v2/samples/pubsub3.py"
          - !Sub |
            su - ${UserName} -c "sed -i 's/message_json = json.dumps(message)/message_json = json.dumps(json.loads(message))/' ~/aws-iot-device-sdk-python-v2/samples/pubsub3.py"
      Targets:
        - Key: InstanceIds
          Values:
            - !Ref InstanceId
      WaitForSuccessTimeoutSeconds: !Ref WaitForSuccessTimeoutSeconds
Code language: YAML (yaml)

SSMドキュメントAWS-RunShellScriptを実行して、Raspberry Piにてコマンドを実行します。
以下の2点がポイントです。

1点目はAWS IoT Device SDKのインストールです。
インストール手順は以下のページを参考にしました。

あわせて読みたい
Raspberry Pi または他のデバイスを接続する - AWS IoT Core このセクションでは、 で使用する Raspberry Pi を設定します AWS IoT。接続したい別のデバイスがある場合、Raspberry Pi の手順には、これらの指示をデバイスに合わせて適...

なおPythonやGitは、Raspberry Piにデフォルトでインストールされているものを使用します。

2点目はSDKのデモ用プログラムの準備です。
SDKをインストールすると、pubsub.pyというサンプルプログラムが使用できます。
デモを進める過程で、このファイルをコピーし、一部修正したファイル(pubsub2.py, pubsub3.py)を使用します。
本来、この処理は手作業で実施することになっていますが、自動的に同処理を実行するようにSSMドキュメントを設定します。

なお本処理を実装する上で、以下のページを参考にしました。

テックブログ | クロジカテックブ...
sedコマンドの条件式に変数を使う場合に注意すること | クロジカ sedを使用して、次のコマンドを実行しようと指定していた際にエラーの原因が分からず何十分かくらい悩んでいました
grep Tips *
Rubyやsedで複数行の文章を任意の箇所に挿入する - grep Tips * お題 複数行の文章を任意の箇所に挿入するワンライナーを書く ファイル中の任意の箇所、例えば複数行にわたる正規表現にマッチした箇所の下に、複数行の
ゲンゾウ用ポストイット
`sed` で検索文字列を含む行の行番号だけを出力する方法 | ゲンゾウ用ポストイット はじめに 検索文字列を含む行の 「行番号」 だけを出力させたいときの方法について。 sed を使って実現できます。 検証環境 $ uname -moi x86_64 MacBook...

環境構築

Raspberry PiをSSMに登録する

1つ目のCloudFormationスタックを作成します。

AWS CLIを使用して、CloudFormationスタックを作成します。
上記のテンプレートファイルを任意のS3バケットに設置した上で、以下のコマンドを実行します。

$ aws cloudformation create-stack \
--stack-name fa-157-01 \
--template-url https://[bucket-name].s3.[region].amazonaws.com/fa-157/fa-157-01.yaml \
--capabilities CAPABILITY_IAM
Code language: Bash (bash)

スタック作成の詳細については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 CloudFormationにおけるネストされたスタックを検証します。 CloudFormationでは、スタックをネストす...

このスタックによって、主に以下のリソースが作成されます。

  • クライアント証明書や鍵を配置するS3バケット
  • SSMハイブリッドアクティベーション
  • アクティベーション用のIAMロール

スタックの作成が完了すると、以下の通りハイブリッドアクティベーション用のコードとIDが取得できます。

Detail of SSM 01.

Raspberry PiにSSM Agentをインストールする

Raspberry Piで以下のコマンドを実行して、SSM Agentをインストールします。

sudo apt-get -y update
sudo apt-get -y upgrade
sudo apt-get -y install libc6:armhf
mkdir /tmp/ssm
curl https://amazon-ssm-ap-northeast-1.s3.ap-northeast-1.amazonaws.com/latest/debian_arm/ssm-setup-cli -o /tmp/ssm/ssm-setup-cli
sudo chmod +x /tmp/ssm/ssm-setup-cli
sudo /tmp/ssm/ssm-setup-cli -register -activation-code "[activation-code]" -activation-id "[activation-id]" -region "ap-northeast-1"
Code language: Bash (bash)

最後のコマンドに、先ほど確認したアクティベーション用のIDとコードを埋め込みます。

手順の詳細については以下のページをご確認ください。

あわせて読みたい
Raspberry Pi 5をセットアップ後、Systems Managerに登録して、AWSからコマンドを実行する 【Raspberry Pi 5をセットアップ後、Systems Managerに登録して、AWSからコマンドを実行する】 AWS Systems ManagerはEC2インスタンスだけでなく、ハイブリッドおよびマ...

SSM Fleet Managerを確認すると、確かにRaspberry PiにSSMエージェントがインストールされて、管理対象であることがわかります。

Detail of SSM 02.

SSMドキュメントを使用して、Raspberry PiをAWS IoTに登録後、AWS IoT Device SDKをセットアップする

2つ目のCloudFormationスタックを作成します。

先ほどと同様に、AWS CLIを使用してスタックを作成します。

$ aws cloudformation create-stack \
--stack-name fa-157-02 \
--template-url https://[bucket-name].s3.[region].amazonaws.com/fa-157/fa-157-02.yaml \
--capabilities CAPABILITY_IAM
Code language: Bash (bash)

このスタックによって、主に以下のリソースが作成されます。

  • IoT Thing(モノ)
  • IoTポリシー
  • クライアント証明書と鍵
  • クライアント証明書をモノおよびIoTポリシーにアタッチ

加えてSSMドキュメントによって、Raspberry Piにクライアント証明書や鍵がダウンロードされて、Raspberry Piをモノとして扱うことができるようになります。
そしてAWS IoT Device SDKを使用するために必要な手続きが実行されます。

マネージメントコンソールからAWS IoTに関係するリソースを確認します。

Detail of AWS IoT 01.
Detail of AWS IoT 02.
Detail of AWS IoT 03.

モノ、証明書、ポリシーが正常に作成されています。
特にポリシーは先ほど確認したCloudFormationテンプレート通りであることがわかります。

動作確認

AWS IoT Device SDKのインストール状況確認

準備が整いましたので、Raspberry Piにアクセスします。
アクセスはローカルマシンからSSHします。

まず証明書と鍵の有無を確認します。

awstut@raspberrypi:~ $ ls -l ~/certs
total 16
-rw-r--r-- 1 awstut awstut 1188 Apr 30 14:09 AmazonRootCA1.pem
-rw-r--r-- 1 root   root    root   1220 Apr 30 14:02 device.pem.crt
-rw-r--r-- 1 root   root    root   1679 Apr 30 14:02 private.pem.key
-rw-r--r-- 1 root    root    451 Apr 30 14:02 public.pem.key
Code language: Bash (bash)

ルート証明書に加えて、S3バケットから証明書および鍵がダウンロードされています。
つまりこのRaspberry PiはAWS IoTデバイスとして動作できます。

AWS IoT Device SDKのインストール状況を確認します。

awstut@raspberrypi:~ $ ls -ld ~/aws*
drwxr-xr-x 18 awstut awstut 4096 Apr 30 14:04 /home/awstut/aws-cli
drwxr-xr-x 14 awstut awstut 4096 Apr 30 14:09 /home/awstut/aws-iot-device-sdk-python-v2
Code language: Bash (bash)

AWS CLIに加えて、AWS IoT Device SDKがインストールされています。

サンプルプログラムの状況を確認します。

awstut@raspberrypi:~ $ ls -l ~/aws-iot-device-sdk-python-v2/samples/pubsub*
-rw-r--r-- 1 awstut awstut 7244 Apr 30 14:10 /home/awstut/aws-iot-device-sdk-python-v2/samples/pubsub2.py
-rw-r--r-- 1 awstut awstut 6479 Apr 30 14:10 /home/awstut/aws-iot-device-sdk-python-v2/samples/pubsub3.py
-rw-r--r-- 1 awstut awstut 3204 Apr 30 14:09 /home/awstut/aws-iot-device-sdk-python-v2/samples/pubsub.md
-rw-r--r-- 1 awstut awstut 6487 Apr 30 14:09 /home/awstut/aws-iot-device-sdk-python-v2/samples/pubsub.py
Code language: Bash (bash)

ベースとなるpubsub.pyをベースとして、pubsub2.pyおよびpubsub3.pyが作成されています。
これらのファイルの作成は、SSMドキュメントで定義しています。

AWS IoT Device SDKのデモ

以下のページに従いデモを行います。

あわせて読みたい
チュートリアル: AWS IoT Device SDK を使用してデバイスを AWS IoT Core に接続する - AWS IoT Core AWS IoT Device SDK を使用して、デバイスを AWS IoT Core に接続する方法について説明します。

デモ1:ワイルドカードトピックフィルターを使用してサブスクライブする

このデモの目的は、ワイルドカードトピックフィルターを使用してメッセージをサブスクライブすることです。

まずRaspberry Pi側でpubsub.pyを実行します。

awstut@raspberrypi:~ $ python3 ~/aws-iot-device-sdk-python-v2/samples/pubsub.py \
--message "" \
--count 2 \
--topic device/+/details \
--ca_file ~/certs/AmazonRootCA1.pem \
--cert ~/certs/device.pem.crt \
--key ~/certs/private.pem.key \
--endpoint [endpoint-name]
Connecting to a2oxckhng7gmur-ats.iot.ap-northeast-1.amazonaws.com with client ID 'test-cd3f51c3-7a0a-43ad-b5af-98f62ee53dfa'...
Connection Successful with return code: 0 session present: False
Connected!
Subscribing to topic 'device/+/details'...
Subscribed with 1
Waiting for all messages to be received...
Code language: Bash (bash)

endpoint引数に渡す値はAWSアカウントごとにユニークです。
適切なエンドポイント名をしてください。

プログラムが実行されて、待機状態となりました。

続いてマネージドコンソールのMQTT test clientページで、トピックフィルターに「device/+/details」を指定して登録します。

Detail of AWS IoT 04.

そして2つのトピックに対して、メッセージを発行します。

Detail of AWS IoT 05.

正常にメッセージを発行できました。

改めてRaspberry Piを確認します。

Waiting for all messages to be received...
Received message from topic 'device/temp/details': b'{ "desiredTemp": 20, "currentTemp": 15 }'
Received message from topic 'device/light/details': b'{ "desiredLight": 100, "currentLight": 50 }'
2 message(s) received.
Disconnecting...
Connection closed
Disconnected!
Code language: Bash (bash)

確かに2つのメッセージを受け取ることができました。

このようにAWS IoT Device SDKを使用して、メッセージを受信することができます。

デモ2:コールバック関数のカスタマイズ

このデモの目的は、メッセージ受信時に実行するコールバック関数をカスタマイズし、トピックに応じて実行する処理を分けることです。

以下がカスタマイズしたコールバック関数です。

# Callback when the subscribed topic receives a message
def on_message_received(topic, payload, dup, qos, retain, **kwargs):
    print("Received message from topic '{}': {}".format(topic, payload))
    # insert start
    topic_parsed = False
    if "/" in topic:
        parsed_topic = topic.split("/")
        if len(parsed_topic) == 3:
            # this topic has the correct format
            if (parsed_topic[0] == 'device') and (parsed_topic[2] == 'details'):
                # this is a topic we care about, so check the 2nd element
                if (parsed_topic[1] == 'temp'):
                    print("Received temperature request: {}".format(payload))
                    topic_parsed = True
                if (parsed_topic[1] == 'light'):
                    print("Received light request: {}".format(payload))
                    topic_parsed = True
    if not topic_parsed:
        print("Unrecognized message topic.")
    # insert end
    global received_count
    received_count += 1
    if received_count == cmdData.input_count:
        received_all_event.set()
Code language: Python (python)

トピック名をスラッシュで分割し、中央の文字列に応じて、実行するアクションを変化させます。
今までは統一的に文字列を返していましたが、返す文字列の一部を変化させます。

まずRaspberry Pi側でpubsub2.pyを実行します。

awstut@raspberrypi:~ $ python3 ~/aws-iot-device-sdk-python-v2/samples/pubsub2.py \
--message "" \
--count 2 \
--topic device/+/details \
--ca_file ~/certs/AmazonRootCA1.pem \
--cert ~/certs/device.pem.crt \
--key ~/certs/private.pem.key \
--endpoint [endpoint-name]
Connecting to a2oxckhng7gmur-ats.iot.ap-northeast-1.amazonaws.com with client ID 'test-21b600e1-018d-42eb-81c8-d156e55dd729'...
Connection Successful with return code: 0 session present: False
Connected!
Subscribing to topic 'device/+/details'...
Subscribed with 1
Waiting for all messages to be received...
Code language: Bash (bash)

プログラムが実行されて、待機状態となりました。

続いてマネージドコンソールのMQTT test clientページで、2つのメッセージを発行します。

Detail of AWS IoT 06.

正常にメッセージを発行できました。

改めてRaspberry Piを確認します。

Waiting for all messages to be received...
Received message from topic 'device/temp/details': b'{ "desiredTemp": 20, "currentTemp": 15 }'
Received temperature request: b'{ "desiredTemp": 20, "currentTemp": 15 }'
Received message from topic 'device/light/details': b'{ "desiredLight": 100, "currentLight": 50 }'
Received light request: b'{ "desiredLight": 100, "currentLight": 50 }'
2 message(s) received.
Disconnecting...
Connection closed
Disconnected!
Code language: Bash (bash)

先ほどと同様に、2つのメッセージを受け取ることができました。
受信した際のメッセージに注目すると、トピック名に応じて、出力される文字列が変化していることがわかります。

このようにAWS IoT Device SDKでは、コールバック変数をカスタマイズすることで、サブスクリプションごとにアクションを定義することができます。

デモ3:メッセージ発行

このデモの目的は、AWS IoT Device SDKを使用して、メッセージを発行することです。

デモの前に、このデモのために用意したpubsub3.pyを確認します。
pubsub.pyとの違いは以下の通りです。

awstut@raspberrypi:~ $ diff -c ~/aws-iot-device-sdk-python-v2/samples/pubsub.py ~/aws-iot-device-sdk-python-v2/samples/pubsub3.py
*** /home/awstut/aws-iot-device-sdk-python-v2/samples/pubsub.py	2024-04-30 14:09:43.040952447 +0900
--- /home/awstut/aws-iot-device-sdk-python-v2/samples/pubsub3.py	2024-04-30 14:10:05.797367406 +0900
***************
*** 132,140 ****
  
          publish_count = 1
          while (publish_count <= message_count) or (message_count == 0):
!              message = "{} [{}]".format(message_string, publish_count)
              print("Publishing message to topic '{}': {}".format(message_topic, message))
!              message_json = json.dumps(message)
              mqtt_connection.publish(
                  topic=message_topic,
                  payload=message_json,
--- 132,140 ----
  
          publish_count = 1
          while (publish_count <= message_count) or (message_count == 0):
!              message = "{}".format(message_string)
              print("Publishing message to topic '{}': {}".format(message_topic, message))
!              message_json = json.dumps(json.loads(message))
              mqtt_connection.publish(
                  topic=message_topic,
                  payload=message_json,
Code language: Bash (bash)

見ての通り、2箇所を修正しています。

1箇所目はメッセージ数に関する修正です。
デフォルトでは、発行したメッセージ数を、メッセージと併記していましたが、これを削除します。

2箇所目はメッセージのフォーマットの変更です。
デフォルトでは、文字列のままjson.dumpsを実行していましたが、その前にjson.loads関数を実行します。

準備が整いました。
まずマネージドコンソールのMQTT test clientページで、サブスクリプション設定を行います。

Detail of AWS IoT 07.

「device/data」をサブスクライブします。

続いてRaspberry Pi側でpubsub3.pyを実行します。

awstut@raspberrypi:~ $ python3 ~/aws-iot-device-sdk-python-v2/samples/pubsub3.py \
--message '{"timestamp":1601048303,"sensorId":28,"sensorData":[{"sensorName":"Wind speed","sensorValue":34.2211224}]}' \
--count 2 \
--topic device/data \
--ca_file ~/certs/AmazonRootCA1.pem \
--cert ~/certs/device.pem.crt \
--key ~/certs/private.pem.key \
--endpoint [endpoint-name]
Connecting to a2oxckhng7gmur-ats.iot.ap-northeast-1.amazonaws.com with client ID 'test-4522c1a4-f306-4910-9331-e87728ccd222'...
Connection Successful with return code: 0 session present: False
Connected!
Subscribing to topic 'device/data'...
Subscribed with 1
Sending 2 message(s)
Publishing message to topic 'device/data': {"timestamp":1601048303,"sensorId":28,"sensorData":[{"sensorName":"Wind speed","sensorValue":34.2211224}]}
Received message from topic 'device/data': b'{"timestamp": 1601048303, "sensorId": 28, "sensorData": [{"sensorName": "Wind speed", "sensorValue": 34.2211224}]}'
Publishing message to topic 'device/data': {"timestamp":1601048303,"sensorId":28,"sensorData":[{"sensorName":"Wind speed","sensorValue":34.2211224}]}
Received message from topic 'device/data': b'{"timestamp": 1601048303, "sensorId": 28, "sensorData": [{"sensorName": "Wind speed", "sensorValue": 34.2211224}]}'
2 message(s) received.
Disconnecting...
Connection closed
Disconnected!
Code language: Bash (bash)

2つメッセージを発行後、プログラムが終了しました。

最後にMQTT test clientページをもう一度確認します。

Detail of AWS IoT 08.

確かに発行したメッセージが表示されました。

メッセージのペイロードがJSON形式である点も注目です。

比較のためにpubsub.pyでメッセージを発行します。

Detail of AWS IoT 09.

JSONフォーマットではなく、文字列としてメッセージが送信されました。

このようにAWS IoT Device SDKを使用すれば、メッセージを発行することが可能です。
またメッセージ発行ロジックを適切に実装することによって、JSONフォーマットでメッセージを発行することもできます。

まとめ

Raspberry Pi 5を使用して、AWS IoT Device SDKのデモを体験しました。