1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ラズパイで部屋の環境(温度湿度気圧CO2)をモニタリングするIoTデバイスを作る② ~AWS IoT Coreへのデータ送信~

Last updated at Posted at 2024-11-19

はじめに

この記事は下記記事の続きとなっていますので先にこちらをご覧ください。

本記事ではIoTデバイスたらしめるためにAWS IoT Coreに接続してデータを送信できるようにします。

AWS IoT Coreでモノを作成する

AWS IoT Coreにラズパイを接続するためにはIoT Coreで事前にモノ(Thing)を作成する必要があります。
※AWSアカウントの作成方法等については触れませんので他の記事をご覧ください。

ポリシーの作成

モノをIoT Coreに接続する際、IoT Coreポリシーによって接続を制限することができます。
今回はゆるゆるですが次のようなポリシーを割り当てました。

ポリシー
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:Connect",
        "iot:Subscribe",
        "iot:Publish",
        "iot:Receive"
      ],
      "Resource": "*"
    }
  ]
}

モノの作成

AWS IoTのマネジメントコンソールからモノを作成します。

  1. 「AWS IoT > 管理 > モノ」を開き「モノを作成」を選択
    006.png

  2. 「1つのモノを作成」を選択し「次へ」
    007.png

  3. 「モノの名前」を入力し、Device Shadowに「名前のないシャドウ(クラシック)」を選択し「次へ」
    今回は「iot-env-data-collector」という名前にしました。
    008.png

  4. 「新しい証明書を自動生成(推奨)」を選択肢「次へ」
    009.png

  5. 証明書に作成しておいたポリシー「iot-thing-policy」をアタッチし「次へ」
    010.png

  6. 証明書とキーをダウンロードではすべてダウンロードして「完了」

    注釈にあるようにキーファイルはこのタイミングでしかダウンロードできません
    忘れずにすべてダウンロードしておきましょう

    011_mask.png

  7. 作成したモノの詳細が確認できれば、作成完了です
    012_mask.png

AWS IoT Device SDK for Pythonのインストール

ラズパイをAWS IoTCoreに接続するために必要なSDKをインストールします。

  1. 必要なライブラリのインストール
    SDKに必要なライブラリをインストールします。

    $ sudo apt install cmake
    ~省略~
    この操作後に追加で 37.2 MB のディスク容量が消費されます。
    続行しますか? [Y/n] Y
    ~省略~
    $
    $ sudo apt install libssl-dev
    ~省略~
    この操作後に追加で 13.1 MB のディスク容量が消費されます。
    続行しますか? [Y/n] Y
    ~省略~
    $
    

    Gitは既に入っていました。

    $ git --version
    git version 2.39.5
    $
    

    未インストールの場合はインストールが必要です。

    $ sudo apt install git
    
  2. SDKリポジトリのクローン
    AWS IoT Device SDK for Pythonのリポジトリをクローンします。

    $ git clone https://github.com/aws/aws-iot-device-sdk-python-v2.git
    Cloning into 'aws-iot-device-sdk-python-v2'...
    ~省略~
    Resolving deltas: 100% (1755/1755), done.
    $
    
  3. SDKのインストール
    仮想環境にAWS IoT Device SDK for Pythonをインストールします。

    $ source  bin/activate
    (EnvDataCollector) $
    (EnvDataCollector) $ pip install ./aws-iot-device-sdk-python-v2
    ~省略~
    Successfully installed awscrt-0.21.1 awsiotsdk-1.0.0.dev0
    (EnvDataCollector) $
    

AWS IoT Coreへの接続のテスト

ラズパイがAWS IoT Coreに接続でき、メッセージをPubSubできるかをテストします。

証明書の転送

モノの作成時にダウンロードした証明書をzip化してラズパイに転送します。
※WindowsPCからラズパイにSCPでcerts.zipファイルを転送しました。

>tar -tf certs.zip
certs/AmazonRootCA1.pem
certs/AmazonRootCA3.pem
certs/certificate.pem.crt
certs/private.pem.key
certs/public.pem.key

>
>scp certs.zip <ユーザ名>@<ラズパイのIPアドレス>:/home/<ユーザ名>/Program/EnvDataCollector
<ユーザ名>@<ラズパイのIPアドレス>'s password:
certs.zip                                                                             100% 4600   641.7KB/s   00:00

>

ラズパイ側ではcerts.zipを解凍しておきます。

$ ls -l | grep certs
-rw-r--r--  1 <ユーザ名> <ユーザグループ名> 4600 11月 17 16:49 certs.zip
$
$ unzip certs.zip
Archive:  certs.zip
  inflating: certs/AmazonRootCA1.pem
  inflating: certs/AmazonRootCA3.pem
  inflating: certs/certificate.pem.crt
  inflating: certs/private.pem.key
  inflating: certs/public.pem.key
$
$ ls -l | grep certs
drwxr-xr-x  2 <ユーザ名> <ユーザグループ名> 4096 11月 17 16:50 certs
-rw-r--r--  1 <ユーザ名> <ユーザグループ名> 4600 11月 17 16:49 certs.zip
$

接続テスト

サンプルプログラムを実行し、AWS IoT Coreへの接続テストを実施します。

  1. 「AWS IoT > MQTT テストクライアント」を開き「エンドポイント」のFQDNを確認します020_mask.png

  2. クローンしたSDKに同梱されているサンプルプログラムを実行します
    オプション「--endpoint」には前項で確認したエンドポイントのFQDNを入れます。

    $ source  bin/activate
    (EnvDataCollector) $ python aws-iot-device-sdk-python-v2/samples/pubsub.py --client_id iot-env-data-collector --topic topic_1 --ca_file certs/AmazonRootCA1.pem --cert certs/certificate.pem.crt --key certs/private.pem.key --endpoint aXXXXXXXXXXXX8-ats.iot.ap-northeast-1.amazonaws.com
    
  3. 各種設定の問題がなければメッセージをPubSubできます

    Connecting to aXXXXXXXXXXXX8-ats.iot.ap-northeast-1.amazonaws.com with client ID 'iot-env-data-collector'...
    Connection Successful with return code: 0 session present: True
    Connected!
    Subscribing to topic 'topic_1'...
    Subscribed with 1
    Sending 10 message(s)
    Publishing message to topic 'topic_1': Hello World!  [1]
    Received message from topic 'topic_1': b'"Hello World!  [1]"'
    Publishing message to topic 'topic_1': Hello World!  [2]
    Received message from topic 'topic_1': b'"Hello World!  [2]"'
    Publishing message to topic 'topic_1': Hello World!  [3]
    Received message from topic 'topic_1': b'"Hello World!  [3]"'
    Publishing message to topic 'topic_1': Hello World!  [4]
    Received message from topic 'topic_1': b'"Hello World!  [4]"'
    Publishing message to topic 'topic_1': Hello World!  [5]
    Received message from topic 'topic_1': b'"Hello World!  [5]"'
    Publishing message to topic 'topic_1': Hello World!  [6]
    Received message from topic 'topic_1': b'"Hello World!  [6]"'
    Publishing message to topic 'topic_1': Hello World!  [7]
    Received message from topic 'topic_1': b'"Hello World!  [7]"'
    Publishing message to topic 'topic_1': Hello World!  [8]
    Received message from topic 'topic_1': b'"Hello World!  [8]"'
    Publishing message to topic 'topic_1': Hello World!  [9]
    Received message from topic 'topic_1': b'"Hello World!  [9]"'
    Publishing message to topic 'topic_1': Hello World!  [10]
    Received message from topic 'topic_1': b'"Hello World!  [10]"'
    10 message(s) received.
    Disconnecting...
    Connection closed
    Disconnected!
    (EnvDataCollector) $
    

    またモノの「アクティビティ」タブで接続やサブスクライブ、切断済みなどのアクティビティを確認することができます。
    021_mask.png

クローンしたフォルダの削除

SDK自体は仮想環境にインストールしているので上記の接続テストに問題がなければクローンしたフォルダは不要になります。
そのためクローンしたフォルダは削除してしまいます。

$ rm -rf aws-iot-device-sdk-python-v2

IoT Coreに接続してセンサーデータを送信する

次のコードでIoT Coreに接続し名前のないシャドウにセンサーデータを60秒おきに送信します。

main.py
import os
import sys
import time
from typing import Literal
from concurrent.futures import Future
from logging import Logger, getLogger, StreamHandler, DEBUG, INFO, WARNING

import mh_z19
from cgsensor import BME280
from awscrt.mqtt import QoS, Connection
from awsiot import mqtt_connection_builder
from awsiot.iotshadow import ShadowState, IotShadowClient, UpdateShadowRequest, UpdateShadowResponse

class EnvDataCollector:
    LOGGING_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = os.environ.get("LOGGING_LEVEL", "WARNING")

    ENDPOINT: str = "aXXXXXXXXXXXX8-ats.iot.ap-northeast-1.amazonaws.com"
    CLIENT_ID: str = "iot-env-data-collector"
    KEEP_ALIVE_SECS: int = 90
    CERT_FILEPATH: str = "certs/certificate.pem.crt"
    PRI_KEY_FILEPATH: str = "certs/private.pem.key"
    CA_FILEPATH: str = "certs/AmazonRootCA1.pem"

    def __init__(self) -> None:
        self._create_logger()
        self._bme280: BME280 = BME280()
        self._mqtt_connection: Connection = mqtt_connection_builder.mtls_from_path(
            endpoint=self.ENDPOINT,
            cert_filepath=self.CERT_FILEPATH,
            pri_key_filepath=self.PRI_KEY_FILEPATH,
            ca_filepath=self.CA_FILEPATH,
            client_id=self.CLIENT_ID,
            keep_alive_secs=self.KEEP_ALIVE_SECS
        )
        self._shadow_client: IotShadowClient = IotShadowClient(self._mqtt_connection)

    def _create_logger(self) -> None:
        self._logger: Logger = getLogger(self.__class__.__name__)
        self._logger.setLevel(self.LOGGING_LEVEL)

        stdout_handler: StreamHandler = StreamHandler(stream=sys.stdout)
        stdout_handler.setLevel(DEBUG)
        stdout_handler.addFilter(lambda record: record.levelno <= INFO)

        stderr_handler: StreamHandler = StreamHandler(stream=sys.stderr)
        stderr_handler.setLevel(WARNING)

        self._logger.addHandler(stdout_handler)
        self._logger.addHandler(stderr_handler)

    def _connect_iot_core(self) -> bool:
        connect_future: Future[Connection] = self._mqtt_connection.connect()
        try:
            connect_future.result()
        except Exception as e:
            self._logger.error(f"Cannot connect iot core, reason:\n{e}")
            return False
        return True

    def _get_data_from_mh_z19c(self) -> int:
        return mh_z19.read()["co2"]

    def _get_data_from_bme280(self) -> tuple[float, float, float]:
        self._bme280.forced()
        return self._bme280.temperature, self._bme280.humidity, self._bme280.pressure

    def _report_env_data(self) -> bool:
        temperature: float
        humidity: float
        pressure: float
        try:
            temperature, humidity, pressure = self._get_data_from_bme280()
        except Exception as e:
            self._logger.error(f"Cannot get data from BME280, reason:\n{e}")
            return False

        try:
            concentration: int = self._get_data_from_mh_z19c()
        except Exception as e:
            self._logger.error(f"Cannot get data from MH-Z19C, reason:\n{e}")
            return False

        self._logger.debug(f"{temperature=}")
        self._logger.debug(f"{humidity=}")
        self._logger.debug(f"{pressure=}")
        self._logger.debug(f"{concentration=}")

        update_request: UpdateShadowRequest = UpdateShadowRequest(
            thing_name=self.CLIENT_ID,
            state=ShadowState(
                reported={
                    "connected": True,
                    "temperature": temperature,
                    "humidity": humidity,
                    "pressure": pressure,
                    "concentration": concentration
                }
            )
        )

        update_future: Future[UpdateShadowResponse] = self._shadow_client.publish_update_shadow(
            request=update_request,
            qos=QoS.AT_LEAST_ONCE
        )
        try:
            update_future.result()
        except Exception as e:
            self._logger.error(f"Cannot report device shadow, reason:\n{e}")
            return False
        return True

    def execute(self) -> None:
        if not self._connect_iot_core():
            self._logger.critical("Cannot connect iot core")
            return

        while True:
            if not self._report_env_data():
                self._logger.critical("Cannot report env data")
                return
            time.sleep(60)

if __name__ == "__main__":
    EnvDataCollector().execute()

環境変数「LOGGING_LEVEL」にDEBUGを与え実行すると60秒ごとに各種センサーデータが表示されます。

$ sudo LOGGING_LEVEL=DEBUG bin/python main.py
temperature=24.9
humidity=40.9
pressure=1021.5
concentration=1527
temperature=24.9
humidity=41.0
pressure=1021.4
concentration=1551

正常に接続とデータ送信ができれば、マネコン上でDevice Shadowのreportedにラズパイから送信したデータを確認することができます。
001.png

※Device Shadowの不要なキーは値にnullを設定し更新することで消すことができます
002.png

PM2でPythonスクリプトを永続化する

部屋の環境をモニタリングするという性質上、作成したPythonスクリプトに予期せぬエラーが発生した場合やラズパイ自体を再起動した場合でもデータ送信し続けて欲しいです。
PM2というプロセス管理アプリケーションを使用することで上記を実現することができるので、これを利用してPythonスクリプトを永続化します。

  1. Node.jsのインストール
    PM2はNode.jsのアプリケーションであるため、Node.jsをインストールします。

    $ sudo apt install nodejs npm
    ~省略~
    この操作後に追加で 162 MB のディスク容量が消費されます。
    続行しますか? [Y/n] Y
    ~省略~
    $
    
  2. PM2のインストール
    PM2をインストールします。

    $ sudo npm install -g pm2
    
    added 138 packages in 30s
    
    13 packages are looking for funding
      run `npm fund` for details
    $ sudo pm2 --version
    ~省略~
    5.4.3
    $
    

    PM2は実行しているプログラムの標準出力と標準エラーをログファイルに保存します。
    長期間実行するため、ログローテートできるようにpm2-logrotateを合わせてインストールしておきます。

    $ sudo pm2 install pm2-logrotate
    ~省略~
    Module
    ┌────┬────────────────────┬──────────┬──────────┬──────────┐
    │ id │ name               │ status   │ cpu      │ mem      │
    ├────┼────────────────────┼──────────┼──────────┼──────────┤
    │ 0  │ pm2-logrotate      │ online   │ 0%       │ 33.4mb   │
    └────┴────────────────────┴──────────┴──────────┴──────────┘
    $
    
  3. PM2でPythonスクリプトを永続化
    pm2 startコマンドでPythonスクリプトをPM2に登録し永続化します。

    $ sudo pm2 start main.py --name EnvDataCollector --interpreter bin/python
    [PM2] Starting /home/<ユーザ名>/Program/EnvDataCollector/main.py in fork_mode (1 instance)
    [PM2] Done.
    ┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
    │ id │ name               │ mode     │ ↺    │ status    │ cpu      │ memory   │
    ├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤
    │ 1  │ EnvDataCollector   │ fork     │ 0    │ online    │ 0%       │ 6.5mb    │
    └────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
    Module
    ┌────┬────────────────────┬──────────┬──────────┬──────────┐
    │ id │ name               │ status   │ cpu      │ mem      │
    ├────┼────────────────────┼──────────┼──────────┼──────────┤
    │ 0  │ pm2-logrotate      │ online   │ 0%       │ 63.3mb   │
    └────┴────────────────────┴──────────┴──────────┴──────────┘
    $
    

    上記は一時的な設定のためpm2 saveコマンドで設定を保存します。

    $ sudo pm2 save
    [PM2] Saving current process list...
    [PM2] Successfully saved in /root/.pm2/dump.pm2
    $
    

    Pythonスクリプト自体の永続化は完了したため、マネコン上で60秒ごとにシャドウの更新ができていることが確認できると思います。
    003.png

  4. PM2のsystemdへの登録
    Pythonスクリプト自体をPM2で永続化することはできましたが、ラズパイ再起動後もPM2が動作できるようpm2 startupコマンドでsystemdへ登録します。

     $ sudo pm2 startup
    [PM2] Init System found: systemd
    Platform systemd
    ~省略~
    [PM2] [-] Executing: systemctl enable pm2-root...
    Created symlink /etc/systemd/system/multi-user.target.wants/pm2-root.service → /etc/systemd/system/pm2-root.service.
    ~省略~
    

    systemctlでサービスを起動します。

    $ sudo systemctl start pm2-root
    $ sudo systemctl status pm2-root
    ● pm2-root.service - PM2 process manager
         Loaded: loaded (/etc/systemd/system/pm2-root.service; enabled; preset: enabled)
         Active: active (running) since Mon 2024-11-18 23:59:34 JST; 3s ago
           Docs: https://pm2.keymetrics.io/
        Process: 10078 ExecStart=/usr/local/lib/node_modules/pm2/bin/pm2 resurrect (code=exited, status=0/SUCCESS)
       Main PID: 9459 (PM2 v5.4.3: God)
          Tasks: 0 (limit: 755)
            CPU: 2.406s
         CGroup: /system.slice/pm2-root.service
                 ‣ 9459 "PM2 v5.4.3: God Daemon (/root/.pm2)"
    
  5. 動作確認
    ラズパイを再起動し、マネコン上で再起動後もシャドウの更新が行われていることが確認できれば完了です。

    $ sudo reboot
    

    004.png

さいごに

センサーから取得したデータをAWS IoT Coreに送信することができるようになりました。
これでIoTデバイスと名乗れるようにはなりましたがデバイスシャドウで確認できるのは直近のデータのみとなっており、過去のデータも見れるようしたいところです。

次の記事ではAWS IoT Coreに送信されたデータを時系列データとして蓄えることができるようにしていきます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?