M5Stick-C + ENV HATで取得した情報をCayenneで視覚化する
最近入手したM5Stick-C(ESP32-PICO-D4を内蔵したマイコンモジュール)とM5Stick-Cにプラグインして使える環境センサーモジュールENV HATを使って温度・湿度・気圧データをセンサーから取得して、取得したデータを無料で使えるIoTサービスCayenneで視覚化するスケッチを作ってみました。
M5Stick-CはWi-Fi機能やGPIO、I2Cが使えるEPS32マイコンモジュールを内蔵したIoT向けの製品で日本国内でもスイッチサイエンスなどから購入できます。
M5Stick-CのWi-Fiモジュールはメーカーで技適も取得済みなので日本国内でも合法的に安心して使うことができます。
M5Stick-C本体には写真にもあるとおりマイコンモジュールESP32-PICO-D4、LCDコントローラーと表示モジュールST7735、マイクモジュールSPM1423、6軸加速度センサーMPU6886、バッテリーコントローラーAXP192、そして80mAh LiPoバッテリーと小さい中に色々と詰め込まれています。
これに加えて赤色のLEDが1つ、スイッチが2つ、GROVEポート1つ、プログラム書き込みと給電のためのUSB Type Cポートが1つ、8ピンのGPIOポートがあります。
8ピンのGPIOポートに接続する専用のENV HATには温度・湿度センサーDHT-12、3軸磁界センサーBMM150、気圧センサーBMP280の3つのセンサーが搭載され、I2C経由でこれを利用することができます。
M5Stick-Cやこれに接続したHATやセンサーモジュールを駆動するアプリケーションを書くにはArduino IDEやUIFlow(MicroPython)を利用することができますが、今回は個人的に取っ付きやすかったArduino IDEを使ってみました。
追加ボードマネージャーのインストール
Arduino IDEにデフォルトでインストールされているボードマネージャーにはM5Stick-Cの情報は存在しないので、Arduino IDEの「ファイル」->「環境設定」から「追加のボードマネージャのURL:」にURLhttps://dl.espressif.com/dl/package_esp32_index.json
を追加します。
「環境設定」でボードマネージャーのURLを追加したら、一旦Arduino IDEを終了させ再びArduino IDEを起動して変更を反映させます。
正しく追加ボードマネージャーの設定が反映されていればArduino IDEメニューの「ツール」->「ボード:〜」->「ボードマネージャ…」(メニューの一番上)を選んでボードマネージャを起動します。
「ボードマネージャ」で「esp32 by Espressif Systems」を探してこれをインストールします。
ボードマネージャで追加ボードマネージャーが読み込まれると「ツール」->「ボード:〜」->「ボードマネージャ…」メニューの中に「M5Stick-C」の選択肢が現われるのでこれを選択します。
追加ライブラリのインストール
M5Stick-Cのボードマネージャーをインストールすると同時にスケッチの追加ライブラリとしてM5StickCが利用可能となっているはずなのでArduino IDEのメニューから「スケッチ」->「ライブラリをインクルード」->「ライブラリを管理…」を選び「ライブラリマネージャ」を起動して「M5StickC by M5StickC」をインストールします。
今回のスケッチを作成するための叩き台としてM5Stack-Cのボードマネージャーと共にインストールされるExample中のENV.inoを使います。
ENV.inoではENV HATに搭載された各種センサーから読み取った値をM5Stick-CのLCDディスプレイへと表示するだけですが、これに各種情報をCayenneへと送信するロジックを追加します。
まずはENV HATのサンプルスケッチENV.inoが入っているフォルダを丸ごと作業ディレクトリへとコピーしてこれを修正していくことにします。
M5Stick-Cに接続したENV HATの温度・湿度センサー DHT-12や3軸磁界センサーBMM150の情報を読み取る為のライブラリはExsampleのフォルダに含まれていたのですが、何故か気圧センサーBMP280用の Adafruit_BMP280.h
だけはGithubから最新版をダウンロードしてインストールするようにスケッチのコメントに指示されていたのでこれを別途ダウンロードします。
$ git clone https://github.com/adafruit/Adafruit_BMP280_Library
Cloning into 'Adafruit_BMP280_Library'...
remote: Enumerating objects: 37, done.
remote: Counting objects: 100% (37/37), done.
remote: Compressing objects: 100% (34/34), done.
remote: Total 346 (delta 10), reused 12 (delta 3), pack-reused 309
Receiving objects: 100% (346/346), 661.37 KiB | 1.01 MiB/s, done.
Resolving deltas: 100% (158/158), done.
また、各種センサーから取得した情報をCayenneへとWi-Fiネットワーク経由で送信するためにCayenne-MQTT-ESPライブラリが必要になります。
一般的にArduinoでWi-Fiネットワークを利用する際にはArduinoに接続したWi-Fi HATやモジュールに対応したWiFiライブラリや暗号化通信の為のWiFiClientSecureライブラリのインクルードとWi-Fi APへの接続手順を別途スケッチへと実装する必要がありますが、M5Stick-Cの場合にはM5Stick-Cで使われているマイコンモジュールESP32-PICO-D4にWi-Fiモジュールが搭載されているのでCayenne-MQTT-ESPライブラリをインクルードするだけでWi-Fiを使っての通信を利用することができます。
またスケッチへのロジックの実装でもWi-Fi APへの接続や暗号化通信もCayenneサーバーへの接続と同時に行うことができるので簡単です。
Cayenne-MQTT-ESPライブラリもArduino IDEの標準リポジトリには存在していなかったのでGithubより直接 libraries
フォルダ以下にダウンロードしてインストールします。
$ git clone https://github.com/myDevicesIoT/Cayenne-MQTT-ESP.git
Cayenneへのアカウント登録とプロジェクトの作成
今回はM5Stick-CやENV HATから取得した情報を可視化するのに無料で利用できるIoTネットワークCayenneのサービスを利用するのでWebブラウザでCayenneのホームページへとアクセスしてアカウントの登録を行ないます。
あるいは既にアカウントを登録済みの場合には既存のアカウントを使ってCayenneへとサインインしてください。
新しいプロジェクトの作成
Raspberry PiやArduino向けには専用のインターフェイス(SDK)が用意されていて、Cayenne側でもそれを受け入れるための専用のテンプレートがありますが、M5Stick-C向けはないので、とりあえず汎用の「Bring Your Own Thing」を選んでプロジェクトを開始してください。
「Bring Your Own Thing」を選らんでプロジェクトを開始するとCayenneとの通信にはMQTT (Message Queue Telemetry Transport)プロトコル1が使われます。
プロジェクトが作成されると例題のようなデバイス(M5Stick-C)側に設定すべき接続情報、「MQTT USERNAME」、「MQTT PASSWORD」、「MQTT CLIENT ID」が生成されるのでこれをメモするかPCのクリップボードにコピーしてArduino IDEのスケッチ中の該当箇所にそれぞれ設定します。
スケッチへのデータ送信ロジックの追加
温度・湿度センサー DHT-12や気圧センサーBMP280、3軸磁界センサーBMM150から読み取った情報をCayenneへと送信する為のスケッチは以下のようになります。
/*
note: need add library Adafruit_BMP280 from library manage
Github: https://github.com/adafruit/Adafruit_BMP280_Library
*/
#include <M5StickC.h>
#include "DHT12.h"
#include <Wire.h>
#include "Adafruit_Sensor.h"
#include <Adafruit_BMP280.h>
#include "bmm150.h"
#include "bmm150_defs.h"
#include <CayenneMQTTESP32.h>
DHT12 dht12;
BMM150 bmm = BMM150();
bmm150_mag_data value_offset;
Adafruit_BMP280 bme;
const char *ssid = "YOUR_WIFI_AP_SSID_HERE";
const char *wifi_password = "YOUR_WIFI_AP_PASSWORD_HERE";
// Cayenne authentication info. This should be obtained from the Cayenne Dashboard.
char username[] = "YOUR_MQTT_USERNAME_HERE";
char password[] = "YOUR_MQTT_PASSWORD_HERE";
char clientID[] = "YOUR_MQTT_CLIENTID_HERE";
void calibrate(uint32_t timeout) {
int16_t value_x_min = 0;
int16_t value_x_max = 0;
int16_t value_y_min = 0;
int16_t value_y_max = 0;
int16_t value_z_min = 0;
int16_t value_z_max = 0;
uint32_t timeStart = 0;
bmm.read_mag_data();
value_x_min = bmm.raw_mag_data.raw_datax;
value_x_max = bmm.raw_mag_data.raw_datax;
value_y_min = bmm.raw_mag_data.raw_datay;
value_y_max = bmm.raw_mag_data.raw_datay;
value_z_min = bmm.raw_mag_data.raw_dataz;
value_z_max = bmm.raw_mag_data.raw_dataz;
delay(100);
timeStart = millis();
while((millis() - timeStart) < timeout) {
bmm.read_mag_data();
/* Update x-Axis max/min value */
if (value_x_min > bmm.raw_mag_data.raw_datax) {
value_x_min = bmm.raw_mag_data.raw_datax;
// Serial.print("Update value_x_min: ");
// Serial.println(value_x_min);
}
else if (value_x_max < bmm.raw_mag_data.raw_datax) {
value_x_max = bmm.raw_mag_data.raw_datax;
// Serial.print("update value_x_max: ");
// Serial.println(value_x_max);
}
/* Update y-Axis max/min value */
if (value_y_min > bmm.raw_mag_data.raw_datay) {
value_y_min = bmm.raw_mag_data.raw_datay;
// Serial.print("Update value_y_min: ");
// Serial.println(value_y_min);
}
else if (value_y_max < bmm.raw_mag_data.raw_datay) {
value_y_max = bmm.raw_mag_data.raw_datay;
// Serial.print("update value_y_max: ");
// Serial.println(value_y_max);
}
/* Update z-Axis max/min value */
if (value_z_min > bmm.raw_mag_data.raw_dataz) {
value_z_min = bmm.raw_mag_data.raw_dataz;
// Serial.print("Update value_z_min: ");
// Serial.println(value_z_min);
}
else if (value_z_max < bmm.raw_mag_data.raw_dataz) {
value_z_max = bmm.raw_mag_data.raw_dataz;
// Serial.print("update value_z_max: ");
// Serial.println(value_z_max);
}
Serial.print(".");
delay(1);
}
value_offset.x = value_x_min + (value_x_max - value_x_min)/2;
value_offset.y = value_y_min + (value_y_max - value_y_min)/2;
value_offset.z = value_z_min + (value_z_max - value_z_min)/2;
}
void setup() {
// put your setup code here, to run once:
M5.begin();
Wire.begin(0,26);
M5.Lcd.setRotation(3);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 0, 2);
M5.Lcd.println("ENV TEST");
pinMode(M5_BUTTON_HOME, INPUT);
if (bmm.initialize() == BMM150_E_ID_NOT_CONFORM) {
Serial.println("Chip ID can not read!");
while(1);
} else {
Serial.println("Initialize done!");
}
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BMP280 sensor, check wiring!");
while (1);
}
Cayenne.begin(username, password, clientID, ssid, wifi_password);
calibrate(10);
Serial.print("\n\rCalibrate done..");
}
uint8_t setup_flag = 1;
void loop() {
Cayenne.loop();
}
// Default function for sending sensor data at intervals to Cayenne.
// You can also use functions for specific channels, e.g CAYENNE_OUT(1) for sending channel 1 data.
CAYENNE_OUT_DEFAULT() {
// put your main code here, to run repeatedly:
float tmp = dht12.readTemperature();
float hum = dht12.readHumidity();
// Write data to Cayenne here. This example just sends the current uptime in milliseconds on virtual channel 0.
Cayenne.virtualWrite(0, hum, TYPE_RELATIVE_HUMIDITY, UNIT_PERCENT);
Cayenne.virtualWrite(1, tmp, TYPE_TEMPERATURE, UNIT_CELSIUS);
M5.Lcd.setCursor(0, 20, 2);
M5.Lcd.printf("Temp: %2.1f Humi: %2.0f%%", tmp, hum);
bmm150_mag_data value;
bmm.read_mag_data();
value.x = bmm.raw_mag_data.raw_datax - value_offset.x;
value.y = bmm.raw_mag_data.raw_datay - value_offset.y;
value.z = bmm.raw_mag_data.raw_dataz - value_offset.z;
float xyHeading = atan2(value.x, value.y);
float zxHeading = atan2(value.z, value.x);
float heading = xyHeading;
if (heading < 0)
heading += 2 * PI;
if (heading > 2 * PI)
heading -= 2 * PI;
float headingDegrees = heading * 180/M_PI;
float xyHeadingDegrees = xyHeading * 180 / M_PI;
float zxHeadingDegrees = zxHeading * 180 / M_PI;
/*
Serial.print("Heading: ");
Serial.println(headingDegrees);
Serial.print("xyHeadingDegrees: ");
Serial.println(xyHeadingDegrees);
Serial.print("zxHeadingDegrees: ");
Serial.println(zxHeadingDegrees);
*/
M5.Lcd.setCursor(0, 40, 2);
M5.Lcd.printf("headingDegrees: %2.1f", headingDegrees);
float pressure = bme.readPressure() / 100;
Cayenne.virtualWrite(2, pressure, TYPE_BAROMETRIC_PRESSURE, UNIT_HECTOPASCAL);
M5.Lcd.setCursor(0, 60, 2);
M5.Lcd.printf("pressure: %2.1f", pressure);
delay(100);
if (!setup_flag) {
setup_flag = 1;
if (bmm.initialize() == BMM150_E_ID_NOT_CONFORM) {
Serial.println("Chip ID can not read!");
while(1);
} else {
Serial.println("Initialize done!");
}
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BMP280 sensor, check wiring!");
while (1);
}
calibrate(10);
Serial.print("\n\rCalibrate done..");
}
if (digitalRead(M5_BUTTON_HOME) == LOW) {
setup_flag = 0;
while(digitalRead(M5_BUTTON_HOME) == LOW);
}
}
// Default function for processing actuator commands from the Cayenne Dashboard.
// You can also use functions for specific channels, e.g CAYENNE_IN(1) for channel 1 commands.
CAYENNE_IN_DEFAULT() {
CAYENNE_LOG("Channel %u, value %s", request.channel, getValue.asString());
//Process message here. If there is an error set an error message using getValue.setError(), e.g getValue.setError("Error message");
}
スケッチ21行目からの以下の2行はそれぞれM5Stick-Cを接続するWi-Fi APのSSIDとパスワードを設定します。
※Wi-Fi APは2.4GHz帯(802.11b/g/n)のアクセスポイントにしか接続できないので注意が必要です。
const char *ssid = "YOUR_WIFI_AP_SSID_HERE";
const char *wifi_password = "YOUR_WIFI_AP_PASSWORD_HERE";
スケッチ24行目からの以下の3行はそれぞれCayenneのダッシュボードに表示されている「MQTT USERNAME」、「MQTT PASSWORD」、「MQTT CLIENT ID」を設定します。
char username[] = "YOUR_MQTT_USERNAME_HERE";
char password[] = "YOUR_MQTT_PASSWORD_HERE";
char clientID[] = "YOUR_MQTT_CLIENTID_HERE";
スケッチのコンパイルと実行
スケッチのコンパイルが終わり、バイナリがM5Stick-Cへと転送され実行が始まるとM5Stick-CのLCDに温度、湿度、コンパスの方位情報、気圧などが表示され、同時に情報がCayenneのサーバーへと送信されます。
Cayenne MQTTサーバーとの接続が正常に開始されると以下の様に各チャンネルのセンサーの情報がダッシュボードに表示されるのでそれぞれのデバイスパネルの「+」をクリックして情報を受け入れます。
ダッシュボード上のデバイスパネルやウィジェットはマウスでドラッグして配置や大きさを調整することができます。
主なコードの変更点
M5Stick-Cのライブラリに収録されていたENV HATのサンプルコードからの主な変更点は以下の通りです。
#include <CayenneMQTTESP32.h>
Cayenne-MQTT-ESPライブラリをインクルード措定ます。
Cayenne MQTTサーバーとの通信やEPS32のWi-Fiモジュールを使ったWi-Fiアクセスポイントへの接続を行なうために必要です。
const char *ssid = "YOUR_WIFI_AP_SSID_HERE";
const char *wifi_password = "YOUR_WIFI_AP_PASSWORD_HERE";
// Cayenne authentication info. This should be obtained from the Cayenne Dashboard.
char username[] = "YOUR_MQTT_USERNAME_HERE";
char password[] = "YOUR_MQTT_PASSWORD_HERE";
char clientID[] = "YOUR_MQTT_CLIENTID_HERE";
Wi-Fi AP、Cayenne MQTTサーバーへの接続情報を追加しています。
Cayenne.begin(username, password, clientID, ssid, wifi_password);
20行目〜26行目で定義されているWi-Fi AP、Cayenne MQTTサーバーへの接続情報を使ってWi-Fi APへと接続し、Wi-Fi接続が確保されたならCayenne MQTTサーバーへと接続します。
Cayenne.loop();
オリジナルのサンプルコードではメインループの中で各種センサーからの情報の取得とLCDパネルへの値の表示を行なっていましたが、ここではCayenneライブラリのループマネージャーCayenne.loop();
を呼び出してCayenneのでフィルとのビジネスロジックが呼び出されるようにします。
元々のメインループの中にあったロジックはCayenneライブラリのループマネージャーから呼び出されるCAYENNE_OUT_DEFAULT()
(129行目から)に移動します。
Cayenne.virtualWrite(0, hum, TYPE_RELATIVE_HUMIDITY, UNIT_PERCENT);
Cayenne.virtualWrite(1, tmp, TYPE_TEMPERATURE, UNIT_CELSIUS);
Cayenne MQTTサーバーに温度・湿度の情報を送信します。
1番目のパラメータは情報を識別するためのチャンネルIDでCayenneのダッシュボードに表示するデバイスパネル毎に異なった番号を指定する必要があります。
2番目はCayenne MQTTサーバーに送信するセンサーからの値です。
3番目は情報の種類を指定します。
4番目は情報の単位を指定します。
情報の種類や単位はインストールしたCayenne-MQTT-ESPライブラリのヘッダーファイルlibraries/Cayenne-MQTT-ESP//src/CayenneUtils/CayenneTypes.h
で以下のような値が定義されていました。
Cayenneのダッシュボードではここで適切なパラメーターが指定されていればアイコンや情報表示の設定を適切に処理してくれるようです。
#define TYPE_BAROMETRIC_PRESSURE "bp" // Barometric pressure
#define TYPE_BATTERY "batt" // Battery
#define TYPE_LUMINOSITY "lum" // Luminosity
#define TYPE_PROXIMITY "prox" // Proximity
#define TYPE_RELATIVE_HUMIDITY "rel_hum" // Relative Humidity
#define TYPE_TEMPERATURE "temp" // Temperature
#define TYPE_VOLTAGE "voltage" // Voltage
#define TYPE_DIGITAL_SENSOR "digital_sensor" // Voltage
#define UNIT_UNDEFINED "null"
#define UNIT_PASCAL "pa" // Pascal
#define UNIT_HECTOPASCAL "hpa" // Hectopascal
#define UNIT_PERCENT "p" // % (0 to 100)
#define UNIT_RATIO "r" // Ratio
#define UNIT_VOLTS "v" // Volts
#define UNIT_LUX "lux" // Lux
#define UNIT_MILLIMETER "mm" // Millimeter
#define UNIT_CENTIMETER "cm" // Centimeter
#define UNIT_METER "m" // Meter
#define UNIT_DIGITAL "d" // Digital (0/1)
#define UNIT_FAHRENHEIT "f" // Fahrenheit
#define UNIT_CELSIUS "c" // Celsius
#define UNIT_KELVIN "k" // Kelvin
#define UNIT_MILLIVOLTS "mv" // Millivolts
float pressure = bme.readPressure() / 100;
Cayenne.virtualWrite(2, pressure, TYPE_BAROMETRIC_PRESSURE, UNIT_HECTOPASCAL);
172行目、オリジナルのサンプルコードではセンサーから取得した気圧(Pa=パスカル)をそのまま使っていましたが、より一般的だと思われるヘクトパスカルにするために値を100分の1しています。
173行目、気圧をCayenne MQTTサーバーに送信へと送信しています。
CAYENNE_IN_DEFAULT() {
CAYENNE_LOG("Channel %u, value %s", request.channel, getValue.asString());
//Process message here. If there is an error set an error message using getValue.setError(), e.g getValue.setError("Error message");
}
CAYENNE_IN_DEFAULT()
にはCayenneのダッシュボードに配置したウィジェットに対する操作が行われた際にCayenne MQTTサーバーからその情報が送信されてきます。
アプリケーションでは受け取ったチャンネルIDrequest.channel
や値getValue
を元にこれを適切に処理するようにします。
-
1990年代後半にIBMが考案して開発したIoT向けのプロトコル
https://www.ibm.com/developerworks/jp/iot/library/iot-mqtt-why-good-for-iot/index.html ↩