Alternative Architecture DOJO

オルターブースのクラウドネイティブ特化型ブログです。

M5Stackで環境センサーを作ってAzure IoT Centralで可視化する

こんにちは。食事の時に娘が妻のマネをして僕の早食いを注意するので「パパは早く食べてるんじゃなくて一口が大きいだけだよ」と適当な言い訳をしていたら、遊んでばかりで食事が進まない娘を「もっと早く食べなさい!」と叱ったときに「私まだ口が小さいから早く食べられない!」と言い返されて、ちょっと感心した木村です。

最近めっきり寒くなってきましたが、部屋を閉め切って仕事をしないで皆さんちゃんと換気されてますでしょうか?私もつい忘れていて気がつくと酷く眠くなっているなんてこともあり、これはちゃんと換気するタイミングを見える化しないと駄目だなと思い立ちました。

ここはIoT番長としては既製品を使わず自作しないとだよね・・・ということで、M5Stackを使って環境センサーを作って、Azure IoT Centralで可視化してみました。

使用するデバイス

今回は、以下のようなデバイスを使うことにしました。

  • M5Stack Basic 3G 拡張ボード セット
    M5StackはESP32というマイコンとTFTカラーディスプレイ、豊富なインターフェースを備えたデバイスです。(株)ソラコムさんが3Gネットワークにつながる拡張ボードをセットで販売されており、こちらを以前購入していたのでこれを使います。なお、今回は3G拡張ボードは使用していませんので、M5Stack Basicだけで同じ事が実現可能です。
  • SCD40+BME680センサーボード
    株式会社シードプラスさんが開発された、温湿度センサーSCD40と総合環境センサーBME680を積んでGrove端子経由(I2C)でつながるボードです。この環境センサーの開発を始めた2021年11月末時点では発売前なのですが、SORACOM UGでお世話になっているシードプラスの前嶋さんのご厚意で先行販売して頂きました。

センサーデータの取得と表示

まずはセンサーデータの取得と表示を行います。こちらはシードプラスさんから提供頂いたサンプルコードをそのまま利用しました。オリジナルではシリアルに出力されていたメッセージ表示をM5StackのLCDに出すようにします。

ソースコードは最終バージョンを最後に掲載しますが、表示部分はこんな感じになります。

void loop()
{
  if(mySensor.readMeasurement() && !bme680.read_sensor_data())
  {
    int color = WHITE;
    float temperature = mySensor.getTemperature();
    float humidity = mySensor.getHumidity();
    uint16_t co2 = mySensor.getCO2();
    float temperature2 = bme680.sensor_result_value.temperature;
    float humidity2 = bme680.sensor_result_value.humidity;
    float pressure = bme680.sensor_result_value.pressure / 100.0;
    float gas = bme680.sensor_result_value.gas / 1000.0;    

    if(co2 > 3500){
      color = PURPLE;
    } else if (co2 > 2500){
      color = RED;
    } else if (co2 > 1500){
      color = YELLOW;
    } else if (co2 > 1000){
      color = GREEN;
    }

    M5.Lcd.clear(color);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.printf("TMP: %.2f / %.2f c\n", temperature,temperature2);
    M5.Lcd.printf("HUM: %.2f / %.2f %%\n", humidity, humidity2);
    M5.Lcd.printf("CO2: %d ppm\n", co2);
    M5.Lcd.printf("GAS:%.2f Kohms\n",gas);
    M5.Lcd.printf("PRS:%.2f hPa\n\n",pressure);
 
  }
  delay(SENSOR_LOOP_DELAY);
}

SCD40(ソースコード中では変数mySensor)で取得したCO2濃度に応じて、画面の背景色も変えてみました。基準値についてはI-O Dataさんの高精度 CO2センサー「UD-CO2S」の値を参考にさせて頂きました。

www.iodata.jp

SCD40から取得できる温度、湿度、CO2濃度、bme680から取得できる温度、湿度、気圧、ガス濃度の計5つのデータを表示しています。写真では結構CO2濃度高いですね、これは換気しないといけません(笑)。

M5StackのLCDに表示する

IoT Centralの準備

次に、IoT Centralのアプリケーションを準備します。IoT Centralでのアプリケーション作成の手順や画面イメージは、昨年の私のブログを参考にしてください。

aadojo.alterbooth.com

先ほどの5つの情報を含んだデバイステンプレートを作成します。

デバイステンプレート

そして、上記ブログの手順通りにデバイスID、IDスコープ、主キーを取得し、dps-keygenコマンドを作って接続文字列を取得しておきましょう。

IoT Centralに接続するコードを書く

次に、IoT Centralに接続するためのコードを書きます。

https://docs.microsoft.com/ja-jp/samples/azure-samples/esp32-iot-devkit-get-started/sample/docs.microsoft.com

こちらのドキュメントに従ってAzure IoT Device WorkbenchをVisual Studio Codeにインストールします。

F1キーまたはCtrl + Shift + Pでコマンドパレットを開き、「Azure IoT Device Workbench: Open Examples」を実行します。

Azure IoT Device Workbench: Open Examples

「Select a board」と出るので、「Generic ESP32 boards」を選びます。

Generic ESP32 boards

表示されたサンプルの中から、「ESP32 Azure IoT Central」の「Open Sample」を開くとgithubのコードを自動的にcloneして、ワークスペースとして開いた状態にしてくれます。

Example

こちらのコードは、esp32-azure-board kitで動かすことを想定しています。そのため、このままコンパイルしてもM5Stackでは動きません。ここからM5Stackには接続していないセンサー類の初期化やLEDを点灯させるといったコードを外していき、シリアルに出力しているメッセージをLCDに出すようにしていきます。あと、初期化や接続に失敗したら諦めて止まるようにしています。

そして、最初に書いたコードとマージするとこんな形になります。比較しやすいよう、できるだけオリジナルのコードをそのまま使うようにしています(そのため不要な処理も多少残ってます)ので比較してみてください。

#include <WiFi.h>
#include <M5Stack.h>
#include "SparkFun_SCD4x_Arduino_Library.h"
#include "seeed_bme680.h"
#include "AzureIotHub.h"

#define TELEMETRY_INTERVAL 10000
#define SENSOR_LOOP_DELAY 5000

SCD4x mySensor;
#define IIC_ADDR  uint8_t(0x76)
Seeed_BME680 bme680(IIC_ADDR);

// Please input the SSID and password of WiFi
const char *ssid = "WiFiのSSID";
const char *password = "WiFiのパスワード";

static const char *connectionString = "HostName=xxxxxxxxxxxxxxxxxxx.azure-devices.net;DeviceId=xxxx;SharedAccessKey=xxxxxxxxxxxxxxxxxxxxxxxxxx";

IOTHUB_CLIENT_LL_HANDLE iotHubClientHandle = NULL;
static int trackingId = 0;
static char propText[1024];
static char msgText[1024];

typedef struct EVENT_MESSAGE_INSTANCE_TAG
{
  IOTHUB_MESSAGE_HANDLE messageHandle;
  size_t messageTrackingId; // For tracking the messages within the user callback.
} EVENT_MESSAGE_INSTANCE_TAG;

static bool hasIoTHub = false;
static bool hasWifi = false;
static uint64_t send_interval_ms;

static void sendConfirmationCallback(IOTHUB_CLIENT_CONFIRMATION_RESULT result, void *userContextCallback);

static bool initIotHubClient(void)
{
  M5.Lcd.println(F("initIotHubClient Start!"));

  if ((iotHubClientHandle = IoTHubClient_LL_CreateFromConnectionString(connectionString, MQTT_Protocol)) == NULL)
  {
    M5.Lcd.println(F("ERROR: iotHubClientHandle is NULL!"));
    return false;
  }

  IoTHubClient_LL_SetRetryPolicy(iotHubClientHandle, IOTHUB_CLIENT_RETRY_EXPONENTIAL_BACKOFF, 1200);
  bool traceOn = true;
  IoTHubClient_LL_SetOption(iotHubClientHandle, "logtrace", &traceOn);

  M5.Lcd.println(F("initIotHubClient End!"));

  return true;
}

static void closeIotHubClient()
{
  if (iotHubClientHandle != NULL)
  {
    IoTHubClient_LL_Destroy(iotHubClientHandle);
    platform_deinit();
    iotHubClientHandle = NULL;
  }
  M5.Lcd.println(F("closeIotHubClient!"));
}

static void sendConfirmationCallback(IOTHUB_CLIENT_CONFIRMATION_RESULT result, void *userContextCallback)
{
  EVENT_MESSAGE_INSTANCE_TAG *eventInstance = (EVENT_MESSAGE_INSTANCE_TAG *)userContextCallback;
  size_t id = eventInstance->messageTrackingId;

  M5.Lcd.print(F("Confirmation received for message tracking id = "));
  M5.Lcd.print(id);
  M5.Lcd.print(F(" with result = "));
  M5.Lcd.println(ENUM_TO_STRING(IOTHUB_CLIENT_CONFIRMATION_RESULT, result));

  IoTHubMessage_Destroy(eventInstance->messageHandle);
  free(eventInstance);
}

static void sendTelemetry(const char *payload)
{
  EVENT_MESSAGE_INSTANCE_TAG *thisMessage = (EVENT_MESSAGE_INSTANCE_TAG *)malloc(sizeof(EVENT_MESSAGE_INSTANCE_TAG));
  thisMessage->messageHandle = IoTHubMessage_CreateFromByteArray((const unsigned char *)payload, strlen(payload));

  if (thisMessage->messageHandle == NULL)
  {
    M5.Lcd.println(F("ERROR: iotHubMessageHandle is NULL!"));
    free(thisMessage);
    return;
  }

  thisMessage->messageTrackingId = trackingId++;

  MAP_HANDLE propMap = IoTHubMessage_Properties(thisMessage->messageHandle);

  (void)sprintf_s(propText, sizeof(propText), "PropMsg_%zu", trackingId);
  if (Map_AddOrUpdate(propMap, "PropName", propText) != MAP_OK)
  {
    M5.Lcd.println(F("ERROR: Map_AddOrUpdate Failed!"));
  }

  // send message to the Azure Iot hub
  if (IoTHubClient_LL_SendEventAsync(iotHubClientHandle,
                                     thisMessage->messageHandle, sendConfirmationCallback, thisMessage) != IOTHUB_CLIENT_OK)
  {
    M5.Lcd.println(F("ERROR: IoTHubClient_LL_SendEventAsync..........FAILED!"));
    return;
  }

  IoTHubClient_LL_DoWork(iotHubClientHandle);
  M5.Lcd.println(F("IoTHubClient sendTelemetry completed!"));
}

void setup()
{
  M5.begin();
  M5.Power.begin();
  M5.Lcd.println(F("Initializing..."));
  M5.Lcd.println(F("ESP32 Device"));
  M5.Lcd.println(F("Initializing..."));
  M5.Lcd.println(F(" > WiFi"));
  M5.Lcd.println(F("Starting connecting WiFi."));

  WiFi.mode(WIFI_AP);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    M5.Lcd.print(".");
    hasWifi = false;
  }
  hasWifi = true;

  M5.Lcd.println(F("WiFi connected"));
  M5.Lcd.println(F("IP address: "));
  M5.Lcd.println(WiFi.localIP());

  M5.Lcd.println(F("justify system clock by ntp.."));
  configTime(9 * 60 * 60, 0, "ntp.jst.mfeed.ad.jp", "ntp.nict.jp", "time.google.com");

  M5.Lcd.println(F(" > IoT Hub"));
  if (!initIotHubClient())
  {
    hasIoTHub = false;
    M5.Lcd.println(F("Initializing IoT hub failed."));
    while(1);
  }
  hasIoTHub = true;

  M5.Lcd.println(F(" > Initialize sensor device"));

  if (mySensor.begin() == false)
  {
    M5.Lcd.println(F("Sensor not detected. Please check wiring. Freezing..."));
    while (1);
  }

  M5.Lcd.print(F("Initializing BME680 sensor\n"));
  while (!bme680.init()) {
    M5.Lcd.print(F("bme680 init failed ! can't find device!\n"));
    delay(5000);
  }

  M5.Lcd.println(F("Start sending events."));
  send_interval_ms = millis();
  M5.Lcd.setTextColor(BLACK);
  M5.Lcd.setTextSize(2);
}

void loop()
{
  if(mySensor.readMeasurement() && !bme680.read_sensor_data())
  {
    int color = WHITE;
    float temperature = mySensor.getTemperature();
    float humidity = mySensor.getHumidity();
    uint16_t co2 = mySensor.getCO2();
    float temperature2 = bme680.sensor_result_value.temperature;
    float humidity2 = bme680.sensor_result_value.humidity;
    float pressure = bme680.sensor_result_value.pressure / 100.0;
    float gas = bme680.sensor_result_value.gas / 1000.0;    

    if(co2 > 3500){
      color = PURPLE;
    } else if (co2 > 2500){
      color = RED;
    } else if (co2 > 1500){
      color = YELLOW;
    } else if (co2 > 1000){
      color = GREEN;
    }

    M5.Lcd.clear(color);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.printf("TMP: %.2f / %.2f c\n", temperature,temperature2);
    M5.Lcd.printf("HUM: %.2f / %.2f %%\n", humidity, humidity2);
    M5.Lcd.printf("CO2: %d ppm\n", co2);
    M5.Lcd.printf("GAS:%.2f Kohms\n",gas);
    M5.Lcd.printf("PRS:%.2f hPa\n\n",pressure);
 
    if (hasWifi && hasIoTHub)
    {
      if ((int)(millis() - send_interval_ms) >= TELEMETRY_INTERVAL)
      {
        sprintf_s(msgText, sizeof(msgText),
                  "{\"Temperature\":%.2f,\"Humidity\":%.2f,\"CO2\":%d,\"Temperature2\":%.2f,\"Humidity2\":%.2f,\"Gas\":%.2f,\"Pressure\":%.2f}",
                  temperature, humidity, co2, temperature2, humidity2, gas, pressure);
        sendTelemetry(msgText);
        send_interval_ms = millis();
      }
    }
  }
  delay(SENSOR_LOOP_DELAY);
}

LCDの表示を5秒に1回更新し、10秒に1回IoT Centralにデータ送信する感じです。

動かしてみた

では早速動かして、IoT Centralのダッシュボードを確認してみましょう。各データを直近30分について、1分ごとの平均値を折れ線グラフで表示してみました。2つのセンサーから出力される温湿度については1つのところに入れています。

IoT Centralでの表示

無事出ていますね!あとはCO2の値に応じてアラートを入れて上げるといい感じになりそうです。

まとめ

今回はESP32のようなマイコンをIoT Centralに接続する例として、M5Stack Basicで環境センサーを作成してみました。

M5StackやArduino、WioLTEなど、ESP32を搭載してネットワークにつながるデバイスは安価かつ容易に入手でき、接続できるセンサー類も豊富に販売されています。開発ツールも無償で手に入りますし、センサーの接続は多くの場合は半田ごても不要なのでちょっとしたデータの計測や可視化にはもってこいです。

ESP32 + センサーで計測したデータの蓄積、可視化や監視をAzureで行う際の参考になれば幸いです。

www.alterbooth.com

www.alterbooth.com

cloudpointer.tech