👨‍💻

ラズパイにオムロンの環境センサーを繋いでCosmos DBにセンサーデータをストアする

2023/06/09に公開

はじめに

以前の記事にて温度センサーから取得したデータを IoT Hub と Stream Analytics を使って、Cosmos DB にストアする方法を紹介しましたが、今回は温度だけでなく湿度、照度、気圧、騒音、3軸加速度、eTVOC 等も取得可能なオムロンの環境センサー(2JCIE-BU)を新たに購入しましたので、このセンサーを使って様々なデータを取得して、Cosmos DB にストアしたいと思います。
また、昨年の11月頃に、IoT Hub から Cosmos DB へ直接データを送るカスタムエンドポイントがパブリックプレビューになったと発表がありましたので、今回はこちらの機能を使って Stream Analytics を使用せずに、IoT Hub から直接 Cosmos DB にデータを送信してみたいと思います。
https://azure.microsoft.com/en-us/updates/iot-hub-cosmos-db-custom-endpoint/

システム構成図

今回は以下のようなシステム構成を構築していきます。

※ラズパイはRaspberry Pi 4Bを使用しています。

  1. オムロンの環境センサーで計測した各センサーデータを取得
  2. 各種センサーデータを整形してIoT Hubに送信
  3. Cosmos DBのエンドポイントにセンサーデータを送信

前回と違うポイントはセンサーが温度センサーからオムロンの環境センサーに変わったことと、Stream Analytics を使用しなくなった部分です。

Raspberry Pi の準備

Raspberry Pi に オムロンの環境センサーを接続して、各種センサーデータを取得する準備を行います。

以下が、今回購入したオムロンの環境センサー(2JCIE-BU)です。
前回の温度センサーとは違って、GPIO ピンを使用せず USB 接続で使用できるので初心者にも優しいです。

まずは 2JCIE-BU を Raspberry Pi に接続します。

次に lsusb コマンドを使って USB 情報を表示します。
Omron Corp.の行を確認して、ベンダー IDとプロダクトIDを確認します。

$ lsusb
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 004: ID 0590:00d4 Omron Corp. 
Bus 001 Device 003: ID 04d9:4545 Holtek Semiconductor, Inc. Keyboard [Diatec Majestouch 2 Tenkeyless]
Bus 001 Device 002: ID 2109:3431 VIA Labs, Inc. Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
  • ベンダー ID:0590
  • プロダクト ID:00d4

次に以下のコマンドで FTDI のドライバをロードします。

sudo modprobe ftdi_sio

次に/sys/bus/usb-serial/drivers/ftdi_sio/new_idにベンダ ID とプロダクト ID を書き込むことで FTDI のカーネルと関連付けします。

sudo sh -c "echo 0590 00d4 > /sys/bus/usb-serial/drivers/ftdi_sio/new_id"

次に以下のコマンドで FTDI USB Serial Device converter が ttyUSB0 に割り当てられていることを確認します。

$ dmesg | grep FTDI
[ 1828.441370] usbserial: USB Serial support registered for FTDI USB Serial Device
[ 2174.630176] ftdi_sio 1-1.3:1.0: FTDI USB Serial Device converter detected
[ 2174.636523] usb 1-1.3: FTDI USB Serial Device converter now attached to ttyUSB0

次に Pythonのサンプルプログラムを実行して各センサーデータを取得できるか試してみます。

https://github.com/omron-devhub/2jciebu-usb-raspberrypi

git clone https://github.com/omron-devhub/2jciebu-usb-raspberrypi.git
cd 2jciebu-usb-raspberrypi/
python3 sample_2jciebu.py

Time measured:2023/06/01 05:29:27
Temperature:29.08
Relative humidity:48.68
Ambient light:359
Barometric pressure:1009.775
Sound noise:62.78
eTVOC:9
eCO2:463
Discomfort index:76.9
Heat stroke:24.62
Vibration information:2
SI value:3.8
PGA:51.0
Seismic intensity:3.566
Temperature flag:0
Relative humidity flag:0
Ambient light flag:0
Barometric pressure flag:0
Sound noise flag:0
eTVOC flag:0
eCO2 flag:0
Discomfort index flag:0
Heat stroke flag:0
SI value flag:0
PGA flag:0
Seismic intensity flag:0

部屋を暗くしたり、大きな音を立てたりすると Ambient light や、Sound noise の値が変化するので、正しく値が取得できていることがわかります。

Azure のリソースの準備

Cosmos DB のリソース作成

まずは Cosmos DB の作成から進めていきます。
リソースの作成から Cosmos DB を選択して作成ボタンを押下します。

次に API オプションの選択画面が表示されるので、「コア(SQL)-推奨」内の作成ボタンを押下します。

次に以下のように入力し、「レビュー + 作成」ボタンを押下してリソースを作成します。

  • リソースグループ:任意のリソースグループを選択
  • アカウント名:任意のcosmos DBの名称
  • 場所:任意のリージョンを選択
  • 容量モード:Serverless

リソースが作成できたら次にコンテナを作成します。
メニューからデータ エクスプローラーを開いて画面上部の「New Container」を押下します。
以下のように入力し OK ボタンを押下します。

  • Database id:任意の値
  • Container id:任意の値
  • Partition key:/partitionKey
    ※Partition key はデフォルトの /id が後のメッセージルーティングで設定できないので必ず変更する

IoT Hub のリソースの作成

次に IoT Hub のリソースの作成を進めていきます。

リソースの作成から IoT Hub を選択して作成ボタンを押下します。

次に以下のように入力し、確認および作成ボタンを押下してリソースを作成します。

  • リソースグループ:任意のリソースグループを選択
  • IoT Hub 名:任意のIoT Hub 名
  • 領域名:適切なリージョンを選択

リソースが作成できたら次にデバイスの作成を行います。
メニューからデバイスを開いて画面上部のデバイスの追加を押下します。
以下のように入力し、保存ボタンを押下します。

  • デバイス ID:任意の値

デバイスの作成が完了したら作成したデバイスを開いて、後ほど使用するのでプライマリ接続文字列を控えておきます。

次にメニューからメッセージルーティングを開き、画面上部の追加ボタンを押下します。
Endpoint では以下のように入力し、「Create + next」ボタンを押下します。

  • Endpoint type:Cosmos DB
  • Endpoint name:任意のエンドポイント名
  • Cosmos DB account:作成した Cosmos DB のリソースを選択
  • Database:Cosmos DB の指定のデータベースの IDを選択
  • Collection:Cosmos DB の指定のコンテナーを選択
  • Partition key name:partitionKey

次に Route では以下のように入力し、「Create + skip enrichments」ボタンを押下します。

  • Name:任意の Route 名

センサーのデータを Iot Hub に送信するプログラムの準備

前回のブログで使用した send_temperature.py と 2JCIE-BU の各センサーデータを取得するサンプルプログラムから一部のソースコードを利用して、以下のような Python のプログラムを作成します。
※import しているパッケージは適宜 pip3 コマンドでインストールしてください。

先に以下のような環境変数ファイルを作成して、前項で控えておいた Iot Hub のデバイスのプライマリ接続文字列を設定します。

.env
IOTHUB_DEVICE_CONNECTION_STRING=プライマリ接続文字列
send_omron_sensor.py
import os
from dotenv import load_dotenv
import serial
import time
from datetime import datetime
from zoneinfo import ZoneInfo
import asyncio
import sys
import json
from azure.iot.device.aio import IoTHubDeviceClient
from azure.iot.device import Message

# 環境変数ファイルの読み込み
load_dotenv('/home/maintenance/git/2jciebu-usb-raspberrypi/.env')
# IoT-Hubの接続文字列(環境変数より取得)
CONNECTION_STRING = os.getenv("IOTHUB_DEVICE_CONNECTION_STRING")

# LED表示ルール:ノーマルオフ
DISPLAY_RULE_NORMALLY_OFF = 0

# LED表示ルール:ノーマルオン
DISPLAY_RULE_NORMALLY_ON = 1

# 符号付き16ビット整数に変換
def s16(value):
    return -(value & 0x8000) | (value & 0x7fff)

# CRC-16の計算を行う
def calc_crc(buf, length):
    crc = 0xFFFF
    for i in range(length):
        crc = crc ^ buf[i]
        for i in range(8):
            carrayFlag = crc & 1
            crc = crc >> 1
            if (carrayFlag == 1):
                crc = crc ^ 0xA001
    crcH = crc >> 8
    crcL = crc & 0x00FF
    return (bytearray([crcL, crcH]))

# 測定した最新値を整形
async def latest_data_formating(data):
    sensor_data = {
        'time_measured': datetime.now(ZoneInfo("Asia/Tokyo")).strftime("%Y/%m/%d %H:%M:%S"),
        'temperature': s16(int(hex(data[9]) + '{:02x}'.format(data[8], 'x'), 16)) / 100,
        'relative_humidity': int(hex(data[11]) + '{:02x}'.format(data[10], 'x'), 16) / 100,
        'ambient_light': int(hex(data[13]) + '{:02x}'.format(data[12], 'x'), 16),
        'barometric_pressure': int(hex(data[17]) + '{:02x}'.format(data[16], 'x')+ '{:02x}'.format(data[15], 'x') + '{:02x}'.format(data[14], 'x'), 16) / 1000,
        'sound_noise': int(hex(data[19]) + '{:02x}'.format(data[18], 'x'), 16) / 100,
        'etvoc': int(hex(data[21]) + '{:02x}'.format(data[20], 'x'), 16),
        'eco2': int(hex(data[23]) + '{:02x}'.format(data[22], 'x'), 16),
        'discomfort_index': int(hex(data[25]) + '{:02x}'.format(data[24], 'x'), 16) / 100,
        'heat_stroke': s16(int(hex(data[27]) + '{:02x}'.format(data[26], 'x'), 16)) / 100,
        'vibration_information': int(hex(data[28]), 16),
        'si_value': int(hex(data[30]) + '{:02x}'.format(data[29], 'x'), 16) / 10,
        'pga': int(hex(data[32]) + '{:02x}'.format(data[31], 'x'), 16) / 10,
        'seismic_intensity': int(hex(data[34]) + '{:02x}'.format(data[33], 'x'), 16) / 1000,
        'temperature_flag': int(hex(data[36]) + '{:02x}'.format(data[35], 'x'), 16),
        'relative_humidity_flag': int(hex(data[38]) + '{:02x}'.format(data[37], 'x'), 16),
        'ambient_light_flag': int(hex(data[40]) + '{:02x}'.format(data[39], 'x'), 16),
        'barometric_pressure_flag': int(hex(data[42]) + '{:02x}'.format(data[41], 'x'), 16),
        'sound_noise_flag': int(hex(data[44]) + '{:02x}'.format(data[43], 'x'), 16),
        'etvoc_flag': int(hex(data[46]) + '{:02x}'.format(data[45], 'x'), 16),
        'eco2_flag': int(hex(data[48]) + '{:02x}'.format(data[47], 'x'), 16),
        'discomfort_index_flag': int(hex(data[50]) + '{:02x}'.format(data[49], 'x'), 16),
        'heat_stroke_flag': int(hex(data[52]) + '{:02x}'.format(data[51], 'x'), 16),
        'si_value_flag': int(hex(data[53]), 16),
        'pga_flag': int(hex(data[54]), 16),
        'seismic_intensity_flag': int(hex(data[55]), 16),
    }
    return sensor_data

async def main():
    # デバイス名と通信速度(ボーレート)、タイムアウトの有無を指定
    ser = serial.Serial("/dev/ttyUSB0", 115200, serial.EIGHTBITS, serial.PARITY_NONE)

    # 接続文字列を使用してデバイスクライアントのインスタンスを作成
    device_client = IoTHubDeviceClient.create_from_connection_string(CONNECTION_STRING)
    try:
        print("処理開始")
        # デバイスクライアントに接続
        await device_client.connect()
        # LEDをグリーンの色に点灯させる
        command = bytearray([0x52, 0x42, 0x0a, 0x00, 0x02, 0x11, 0x51, DISPLAY_RULE_NORMALLY_ON, 0x00, 0, 255, 0])
        command = command + calc_crc(command, len(command))
        ser.write(command)
        time.sleep(0.1)

        # 対象となるシリアルポートが開いている間繰り返し処理を行う
        while ser.isOpen():
            # 最新データ取得
            command = bytearray([0x52, 0x42, 0x05, 0x00, 0x01, 0x21, 0x50])
            command = command + calc_crc(command, len(command))
            tmp = ser.write(command)
            time.sleep(0.1)
            data = ser.read(ser.inWaiting())
            omron_data = await latest_data_formating(data)
            msg = Message(json.dumps(omron_data))
            msg.content_encoding = "utf-8"
            msg.content_type = "application/json"
            print(msg)
            await device_client.send_message(msg)
            print("センサーデータ送信")
            time.sleep(5)

    except KeyboardInterrupt:
        # デバイスクライアントを終了させる
        await device_client.shutdown()
        # LEDをOFFにする
        command = bytearray([0x52, 0x42, 0x0a, 0x00, 0x02, 0x11, 0x51, DISPLAY_RULE_NORMALLY_OFF, 0x00, 0, 0, 0])
        command = command + calc_crc(command, len(command))
        ser.write(command)
        time.sleep(1)
        # スクリプト終了
        sys.exit

if __name__ == '__main__':
    asyncio.run(main())

このプログラムのポイントは、 IoT Hubに送信するメッセージの content_encodingに utf-8 を指定し、content_type にapplication/json 形式を指定している部分です。
この指定をしていないと、送信したデータが Cosmos DB に base64 形式で書き込まれるため、人間が読み取ることができない値で記録されてしまいます。

準備ができたら以下のコマンドでこのプログラムを実行してみます。

python3 send_omron_sensor.py

このプログラムでは 5 秒毎に取得したセンサーデータを IoT Hub に送信し続けるので、何度か送信できていることが確認できたら途中でControl + cを入力してプログラムを終了します。

Cosmos DB のリソースからデータエクスプローラを開き、データが正しく格納されているか確認します。

取得した各センサーデータの値がbody内に格納されていることが確認できます。

以上で Raspberry Pi にオムロンの環境センサーを接続して、 Iot Hub から直接 Cosmos DB に送信する仕組みの構築は完了です。
次回以降は Cosmos DB にストアしたデータを使って環境データを視覚的に表示できるようにしてみたいと思います。

Discussion