はじめに
この記事は下記記事の続きとなっていますので先にこちらをご覧ください。
本記事では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のマネジメントコンソールからモノを作成します。
-
「モノの名前」を入力し、Device Shadowに「名前のないシャドウ(クラシック)」を選択し「次へ」
今回は「iot-env-data-collector」という名前にしました。
-
証明書とキーをダウンロードではすべてダウンロードして「完了」
注釈にあるようにキーファイルはこのタイミングでしかダウンロードできません
忘れずにすべてダウンロードしておきましょう
AWS IoT Device SDK for Pythonのインストール
ラズパイをAWS IoTCoreに接続するために必要なSDKをインストールします。
-
必要なライブラリのインストール
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
-
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. $
-
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への接続テストを実施します。
-
クローンした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
-
各種設定の問題がなければメッセージを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) $
クローンしたフォルダの削除
SDK自体は仮想環境にインストールしているので上記の接続テストに問題がなければクローンしたフォルダは不要になります。
そのためクローンしたフォルダは削除してしまいます。
$ rm -rf aws-iot-device-sdk-python-v2
IoT Coreに接続してセンサーデータを送信する
次のコードでIoT Coreに接続し名前のないシャドウにセンサーデータを60秒おきに送信します。
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にラズパイから送信したデータを確認することができます。
※Device Shadowの不要なキーは値にnull
を設定し更新することで消すことができます
PM2でPythonスクリプトを永続化する
部屋の環境をモニタリングするという性質上、作成したPythonスクリプトに予期せぬエラーが発生した場合やラズパイ自体を再起動した場合でもデータ送信し続けて欲しいです。
PM2というプロセス管理アプリケーションを使用することで上記を実現することができるので、これを利用してPythonスクリプトを永続化します。
-
Node.jsのインストール
PM2はNode.jsのアプリケーションであるため、Node.jsをインストールします。$ sudo apt install nodejs npm ~省略~ この操作後に追加で 162 MB のディスク容量が消費されます。 続行しますか? [Y/n] Y ~省略~ $
-
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 │ └────┴────────────────────┴──────────┴──────────┴──────────┘ $
-
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秒ごとにシャドウの更新ができていることが確認できると思います。
-
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)"
-
動作確認
ラズパイを再起動し、マネコン上で再起動後もシャドウの更新が行われていることが確認できれば完了です。$ sudo reboot
さいごに
センサーから取得したデータをAWS IoT Coreに送信することができるようになりました。
これでIoTデバイスと名乗れるようにはなりましたがデバイスシャドウで確認できるのは直近のデータのみとなっており、過去のデータも見れるようしたいところです。
次の記事ではAWS IoT Coreに送信されたデータを時系列データとして蓄えることができるようにしていきます。