5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RaspberryPiの死活監視システムを Azure IoT Central で構築した

Last updated at Posted at 2021-10-09

はじめに

自宅で運用しているRaspberryPiの死活監視システムを構築したいと思い、GUIを含めたシステムを簡単に構築が出来そうなAzure IoT Centralを使用してみようと思います。

#####やりたいこと

  • リソース情報をダッシュボードに表示
  • CPU・メモリ使用率 等のリソース増加の通知
  • デバイスのシステムダウン(ラズパイからの通信無し) の通知

環境

  • Raspberry Pi 4 Model B
  • Python : 3.7.3
  • azure-iot-device : 2.7.1 <pipパッケージ>
  • sysstat : 11.5.2 <debパッケージ>
インストール
$ pip3 install azure-iot-device
$ sudo apt install sysstat

システム構成

ラズパイからCPU・メモリ 等のデバイスリソース情報をAzure IoT Centralに送信して、ダッシュボードにリアルタイムでリソース情報を表示します。また、リソース情報が一定条件を満たすとメールを送信します。

RaspiSystemMonitor.drawio.png

ダッシュボード

ラズパイから送信された、CPU使用率、メモリ使用率、ディスク使用率を数値とグラフで表示しています。変動が多いCPUとメモリのみをグラフ表示しています。
数値を表示しているパネルの色は、数値によって変更できるようになっていて、「0~70は青、70~90は黄、90~100は赤」みたいな感じで、ぱっと見で状態が分かるようになっています。
image.png

通知メール

「CPU使用率:90%以上」or「メモリ使用率:85%」の状態を検知すると以下のメールが送られ、デバイスのリソース使用率増加をすぐに知ることが出来ます。
image.png

ラズパイの設計

送信リソース情報

デバイスのリソース情報をjson形式に整形してAzure IoT Centralに送信します。
※ コア毎のCPU使用率を送信していますが、今回は「All」のみを使用してます。

{
    "CpuUsage": {
        "All": 25.63,
        "Core0": 58.16,
        "Core1": 16.32,
        "Core2": 11.34,
        "Core3": 16.67
    },
    "MemoryUsage": 51.70,
    "DiskUsage": 92,
    "MsgId": 7
}

Pythonプログラム

Azure IoT Centralの「SCOPE_ID」,「DEVICE_ID」,「DEVICE_KEY」が環境変数に設定されていることが前提条件のプログラムとなっています。main.pyでは通信コネクション及び、取得したリソース情報の定期送信を行っています。

main.py
import os
import sys
import json
import asyncio
from systemMoniter import SystemMoniter
from deviceProvisioningService import Device
from azure.iot.device.aio import IoTHubDeviceClient

def message_received_handler(message):
    # ログ出力のみ
    print("the data in the message received was ")
    print(message.data)
    print("custom properties are")
    print(message.custom_properties)

async def main():
    scopeID = None
    deviceId = None
    key = None

    # 環境変数の取得
    scopeID = os.getenv('SCOPE_ID')
    deviceId = os.getenv('DEVICE_ID')
    key = os.getenv('DEVICE_KEY')
    if scopeID is None or deviceId is None or key is None:
        sys.exit(1)

    # Azureとの通信に必要な「connection string」の取得
    dps = Device(scopeID, deviceId, key)
    conn_str = await dps.connection_string

    # コネクション確立
    device_client = IoTHubDeviceClient.create_from_connection_string(conn_str)
    await device_client.connect()

    # 受信ハンドラの設定、Azureからコマンドを受信するとこの関数が呼び出される
    device_client.on_message_received = message_received_handler

    systemMoniter = SystemMoniter()
    while True:
        try:
            # リソース情報取得
            telemetry = await systemMoniter.getSystemStatus()
            if telemetry is not None:
                # ログ出力、json形式に変換し送信
                print(telemetry)
                data = json.dumps(telemetry)
                await device_client.send_message(data)
                await asyncio.sleep(10)

        except:
            print("Unexpected error:", sys.exc_info()[0])

    # コネクション切断
    await device_client.disconnect()

if __name__ == "__main__":
    asyncio.run(main())
    # If using Python 3.6 or below, use the following code instead of asyncio.run(main()):
    # loop = asyncio.get_event_loop()
    # loop.run_until_complete(main())
    # loop.close()
`deviceProvisioningService.py`では通信に必要な「connection string」の生成を行います。**Azure IoT Central**は内部的には**Azure IoT Hub**を使用しているようで、**Device Provisioning Service**からデバイスに紐づけられたhub名を取得して「connection string」を生成する必要があります。
deviceProvisioningService.py
import asyncio
from azure.iot.device.aio import ProvisioningDeviceClient

class Device():
    def __init__(self, scope, device_id, key):
        self.scope = scope
        self.device_id = device_id
        self.key = key

    async def __register_device(self):
        provisioning_device_client = ProvisioningDeviceClient.create_from_symmetric_key(
            provisioning_host='global.azure-devices-provisioning.net',
            registration_id=self.device_id,
            id_scope=self.scope,
            symmetric_key=self.key,
        )

        return await provisioning_device_client.register()

    @property
    async def connection_string(self):
        results = await asyncio.gather(self.__register_device())
        registration_result = results[0]

        # build the connection string
        conn_str = 'HostName=' + registration_result.registration_state.assigned_hub + \
            ';DeviceId=' + self.device_id + \
            ';SharedAccessKey=' + self.key

        return conn_str
`systemMoniter.py`ではラズパイのリソース情報の取得を行います。LinuxコマンドをPythonから取得し、後からjson型に変換しやすいように`dict`型にデータをまとめています。
systemMoniter.py
import subprocess

class SystemMoniter():
    def __init__(self) -> None:
        self.msgId = 0

    async def getSystemStatus(self) -> dict:
        telemetry = {}
        self.msgId += 1

        telemetry.update(self._cpuUpuUsage())
        telemetry.update(self._memoryUsage())
        telemetry.update(self._diskUsage())
        telemetry["MsgId"] = self.msgId

        return telemetry

    def _cpuUsage(self) -> dict:
        ret = {}
        try:
            usages = {}
            cmd = R"LC_ALL=C sar -P ALL 1 1 | grep Average"
            cpuUsage = subprocess.check_output(cmd, shell=True).decode('utf-8')
            list = cpuUsage.splitlines()[1:]
            for num in range(0, len(list)):
                value = list[num].split()
                if(num == 0): property = "All"
                else:         property = R"Core" + str(num-1)
                # cpuUsage = %user + %nice + %system + %iowait + %steal
                usages[property] = float(value[2])+float(value[3])+float(value[4])+float(value[5])+float(value[6])
            ret["CpuUsage"] = usages
        except:
            print("Failed get the cpu usages.")
            ret = {}
        return ret

    def _memoryUsage(self) -> dict:
        ret = {}
        try:
            cmd = R"free -k | grep Mem:"
            cpuUsage = subprocess.check_output(cmd, shell=True).decode('utf-8')
            list = cpuUsage.split()
            ret["MemoryUsage"] = (float(list[2])*100)/float(list[1])
        except:
            print("Failed get the memory usage.")
            ret = {}
        return ret

    def _diskUsage(self) -> dict:
        ret = {}
        try:
            cmd = R"df | grep /dev/root | awk '{print $5}' | sed 's/%//'"
            diskUsage = subprocess.check_output(cmd, shell=True).decode('utf-8')
            ret["DiskUsage"] = float(diskUsage)
        except:
            print("Failed get the disk usage.")
            ret = {}
        return ret

Azure IoT Central の設計

デバイステンプレート

ラズパイから送信するjsonデータに合わせてパラメータを設定します。
image.png

規則(アクション)

以下のように条件を設定してメールを送るようにしています。
パラメータの数値によって条件を付けることが出来るが、「受信できなかった」等の条件付けは無く、デバイスからデータが送信されていることが前提の条件付けになっている。その為、やりたかった「デバイスのシステムダウン(ラズパイからの通信無し) の通知」は現状できなさそう。
image.png

最後に

今回はAzure IoT Centralを使用して死活監視システムを構築しました。想像していたより簡単に作ることが出来たため、かなり驚いています。これぐらいのシステムなら構想から構築まで1~2日あれば実現できると思います。
気になる点としては、ダッシュボードや規則(アクション)についてはカスタマイズ性がないため、複雑なGUIや規則は設計できませんでした。複雑なことがしたいなら自前で作る必要があります。PoCレベルでさっと作る分ならかなり使えるサービスだと感じました。
やりたかった「デバイスのシステムダウン(ラズパイからの通信無し) の通知」が出来なかったのは残念でしたが、今後のアップデートに期待したいと思います。

参考

以下の情報を参考にさせていただきました。
Raspberry-Pi-Python-Environment-Monitor-with-the-Pimoroni-Enviro-Air-Quality-PMS5003-Sensor

5
8
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
5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?