【SwitchBot温湿度計モニター】の製作~ソースコード付き~
はじめに:
【おすすめ!】SwitchBot温湿度計で【ミニ百葉箱】を作ってみた。
で屋外設置した温湿度計はとても良い仕事をしています。
同じ温湿度計を屋内に置いているため、すぐに温湿度を見比べることができます。
スマホでグラフ化できます。
ひとつの温湿度計についての情報はこれで分かります。
ですが、このグラフを重ねてみたいと思ったら、やっぱり自分で何とかする必要があります。
また、自動潅水機SALZシリーズで、温湿度を頼りに処理を変えたいと思うときがあります。例えば、温度計が35度を超えているときは、直射日光が当たっている可能性があります。その時に、水やりをするとお湯になった水を植物に掛けてしまうのでできれば避けたいところです。
このように、温湿度をモニタリングできると何かと便利なため、勉強して見ることにしました。勉強の過程は後回しにして、まずは今回できたものを発表します。
できたもの:
SwichBot温湿度計モニタです。
M5Stackで周辺のSwichBot温湿度計をモニタリングし、温度と湿度読み出し、その結果をAmbientへ送ります。
Ambientは1つのチャンネルに8つまでのデータが送れますので、最大4台までの情報が取得できます。
温度計だけ使用する場合であれば、少しプログラムを変えれば、最大8台まで接続できます。
M5Stackにプログラムを書き込み、電源を入れておくだけで機能します。とてもお手軽です。
モニタはテスト用のため、動作しているかわかる程度の情報を表示しています。
Ambientの表示。楽しくなって、どんどん表示してしまいます。
ソースコード:
//MySwitchBotMeterTestM5Stack bbd
//@yankeeさん
//SwitchBot 温湿度計のBLEデータをM5Stackで読み取って画面に表示する
////////https://qiita.com/yankee/items/f1e1fd47a1a3e83501e4
//のコードを元に作成しました。
#include <stdlib.h>
#include <M5Stack.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include "Ambient.h"
#define SECONDS(s) ((s) * 1000)
#define MINUTES(m) SECONDS(m * 60)
typedef struct {
// Byte:0 Enc Type Dev Type
uint8_t deviceType : 7;
uint8_t reserved1 : 1;
// Byte:1 Status
uint8_t groupA : 1;
uint8_t groupB : 1;
uint8_t groupC : 1;
uint8_t groupD : 1;
uint8_t reserved2 : 4;
// Byte:2 Update UTC Flag Battery
uint8_t remainingBattery : 7;
uint8_t reserved3 : 1;
// Byte:3
uint8_t decimalOfTheTemperature : 4;
uint8_t humidityAlertStatus : 2;
uint8_t temperatureAlertStatus : 2;
// Byte:4
uint8_t integerOfTheTemperature : 7;
uint8_t posiNegaTemperatureFlag : 1;
// Byte:5
uint8_t humidityValue : 7;
uint8_t temperatureScale : 1;
} SwitchBotServiceData;
static BLEUUID SwitchBotServiceUUID("cba20d00-224d-11e6-9fb8-0002a5d5c51b");
BLEScan *pBLEScan;
#define NADRESSES 8
int nAddresses = 0;
std::string addresses[NADRESSES];
WiFiClient client;
// Wi-Fi
const char * ssid = "ssid";
const char * password = "password";
Ambient ambient;
unsigned int channelId = 0000; // AmbientのチャネルID
const char* writeKey = "xxxx"; // ライトキー
void setup()
{
M5.begin(true, false, true, true);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextFont(4);
M5.Lcd.printf("MySwitchBotMeterTest\n");
delay(1000);
BLEDevice::init("M5Stack");
pBLEScan = BLEDevice::getScan(); //create new scan
pBLEScan->setInterval(1000);
pBLEScan->setWindow(1000);
pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
WiFi.begin(ssid, password); // Wi-Fi APに接続
while (WiFi.status() != WL_CONNECTED) { // Wi-Fi AP接続待ち
Serial.print(".");
delay(100);
}
// チャネルIDとライトキーを指定してAmbientの初期化
ambient.begin(channelId, writeKey, &client);
delay(1000);
}
void loop()
{
static int skipCount = 0;
delay(500);
M5.update();
// Serial.println("Scan start!");
BLEScanResults foundDevices = pBLEScan->start(1, false);
uint32_t dev_count = foundDevices.getCount(); // 受信したデバイス数を取得
for (int i = 0; i < dev_count; i++) {
BLEAdvertisedDevice device = foundDevices.getDevice(i);
BLEAddress address = device.getAddress();
char buf[256];
// sprintf(buf, "device(%d/%d):", i + 1, dev_count);
// Serial.print(buf);
// Serial.println(device.toString().c_str());
if (device.haveServiceUUID() && device.getServiceUUID().equals(SwitchBotServiceUUID)) {
SwitchBotServiceData sd;
memcpy(&sd, device.getServiceData().data(), device.getServiceData().length());
std::string tAddress = address.toString();
// 見つけたアドレス毎に番号を振り、その番号でAmbientへデータを書き込む。
bool fFound = false;
int pos = 0;
int loopCount = (nAddresses < NADRESSES) ? nAddresses : NADRESSES;
for (int i = 0; i < loopCount; i++) {
if (tAddress == addresses[i]) {
fFound = true;
pos = i;
continue;
}
}
if ((nAddresses < NADRESSES) && !fFound) {
pos = nAddresses;
addresses[nAddresses++] = tAddress;
}
float temperature = ((sd.posiNegaTemperatureFlag) ? 1.0 : -1.0) *
(sd.decimalOfTheTemperature * 0.1 +
sd.integerOfTheTemperature);
float humidity = (float)sd.humidityValue;
sprintf(buf, "temperature:%.1f", temperature);
Serial.println(buf);
M5.Lcd.printf(buf);
sprintf(buf, "humidity:%.1f", humidity);
Serial.println(buf);
M5.Lcd.printf(buf);
ambient.set(2 * pos + 1, temperature);
ambient.set(2 * pos + 2, humidity);
Serial.println("SwitchBotServiceData:");
sprintf(buf, " deviceType:%02x", sd.deviceType);
Serial.println(buf);
sprintf(buf, " reserved1:%02x", sd.reserved1);
Serial.println(buf);
sprintf(buf, " group A:%02x B:%02x C:%02x D:%02x", sd.groupA, sd.groupB, sd.groupC, sd.groupD);
Serial.println(buf);
sprintf(buf, " reserved2:%02x", sd.reserved2);
Serial.println(buf);
sprintf(buf, " remainingBattery:%02x(%d)", sd.remainingBattery, sd.remainingBattery);
Serial.println(buf);
sprintf(buf, " reserved3:%02x", sd.reserved3);
Serial.println(buf);
sprintf(buf, " decimalOfTheTemperature:%02x(%d)", sd.decimalOfTheTemperature, sd.decimalOfTheTemperature);
Serial.println(buf);
sprintf(buf, " humidityAlertStatus:%02x", sd.humidityAlertStatus);
Serial.println(buf);
sprintf(buf, " temperatureAlertStatus:%02x", sd.temperatureAlertStatus);
Serial.println(buf);
sprintf(buf, " integerOfTheTemperature:%02x(%d)", sd.integerOfTheTemperature, sd.integerOfTheTemperature);
Serial.println(buf);
sprintf(buf, " posiNegaTemperatureFlag:%02x", sd.posiNegaTemperatureFlag);
Serial.println(buf);
sprintf(buf, " humidityValue:%02x(%d)", sd.humidityValue, sd.humidityValue);
Serial.println(buf);
sprintf(buf, " temperatureScale:%02x", sd.temperatureScale);
Serial.println(buf);
}
}
pBLEScan->clearResults(); // delete results fromBLEScan buffer to release memory
if (--skipCount < 0) {
skipCount = 20;
ambient.send();
}
delay(MINUTES(1));
}
ここまでの道のり:
というわけで、ここまでたどり着くまでに私のたどった道筋をお話しいたします。
まずはネットで検索し、片っ端から情報収集します。
一番いいのは、目的のプログラムが既に先人によって公開されていること。どこかに、そんな都合の良いものはないか、探しさまよいます。
@yankeeさん
SwitchBot 温湿度計のBLEデータをM5Stackで読み取って画面に表示する
これはかなり勉強になりました。
実際にサンプルのプログラムを動かして見ました。
M5Stackでカレンダーや時間が表示できることも初めて知りました。
そして、SwitchBot温湿度計からのデータも読めています。
ソースを読んでみると、MACアドレスを直接指定しています。
そのMACアドレスを手に入れるため、スマホのアプリ「BLE Scanner」を動かし、SwitchBot温湿度計を覗いてみます。
今までの感覚であれば、この辺りまで情報を当たれば大体の全体像がつかめて来るのですが、Bluetoothに関しては、M5Stackのサンプルコードで行われていることと、スマホのアプリ「BLE Scanner」で得られる情報とをうまくつなぎ合わせることができません。
Bluetoothの概念なり基本的な構造なりを知らない限りは手に負えないと思いました。
まずは、ヨドバシで気になる本を手に取ってみました。
Bluetooth無線でワイヤレスI/O
トランジスタ技術編集部
CQ出版社 刊
おおよその俯瞰はできましたが、まだ今一つ理解できていません。
ただ、よくわかったことは
Bluetoothはどんどん形を変えてきている。
今知りたいのはBLEだ。
ということです。
なんとなく、
Bluetooth≒BLE
とか
とりあえずBluetoothを勉強すればいいか
では私の能力では手に負えないことがよくわかりました。
そこで、もっと的を絞った
Bluetooth Low Energyをはじめよう
Kevin Townsend、Carles Cufi;、Akiba、Robert Davidson 著
水原 文 訳
オライリー・ジャパン 刊
を手に取って、読み進めることにしました。
ここまで来て、ようやく私の頭の中にBLE用語が定着し始めました。
ブロードキャスト、GATT、UUID
など。
ようやくサンプルコードが読めるようになってきました。
まずは、ブロードキャストのデータを読み取ることから始めました。
できれば、MACアドレスをハードコーディングしないで、付近にあるSwitchbotの情報を拾い集め、定期的にAmbientへデータを送ってやることを目指しました。
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
(略)
BLEScanResults foundDevices = pBLEScan->start(1, false);
uint32_t dev_count = foundDevices.getCount();
for (int i = 0; i < dev_count; i++) {
// 受信したデータに対して
BLEAdvertisedDevice device = foundDevices.getDevice(i);
BLEAddress address = device.getAddress();
uint16_t appearance = device.getAppearance();
std::string manufacturerData = device.getManufacturerData();
std::string sName = device.getName();
int rssi = device.getRSSI();
std::string sd = device.getServiceData();
Serial.print("dv:");
Serial.println(device.toString().c_str());
Serial.print("ad:");
Serial.println(address.toString().c_str());
Serial.print("ap:");
Serial.println(appearance);
Serial.print("md:");
Serial.println(manufacturerData.c_str());
Serial.print("nm:");
Serial.println(sName.c_str());
Serial.print("rs:");
Serial.println(rssi);
Serial.print("sd:");
Serial.println(sd.c_str());
(略)
dv:Name: , Address: XX:XX:XX:XX:XX:XX, manufacturer data: 5900c861bab4eed7
ad:XX:XX:XX:XX:XX:XX
ap:0
md:Y
nm:
rs:-63
sd:
Scan done!
※アドレスはXX:XX:XX:XX:XX:XXにしています。
BLEAdvertisedDevice.hを調べて、役に立ちそうなデータを表示してみましたが、まだ不十分です。「BLE Scanner」で読めたデータぐらいは読みたいと思います。
そもそも、「BLE Scanner」で表示されている値が何の値かもあまり理解できていません。こちらからも攻めてみる必要があります。
LEN:2
TYPE:0x01
VALUE:0x06
LEN:9
TYPE:0xFF
VALUE:0x5900D00D0CF5863F
LEN:17
TYPE:0x07
VALUE:0x1BC5D5A50200B89FE6114D22000DA2CB
LEN:9
TYPE:0x16
VALUE:0x000D541064069F3F
参考にしたサンプルコードから、最後の値0x000D541064069F3Fに温度と湿度が含まれていることを知っています。
ただもう少し他の値の意味も理解したいと思いました。
ここでもう一度、Bluetooth Low Energyをはじめよう に戻ります。なんとか、これらの数値の意味を読み解こうと思いました。
ここで、本で紹介されていたアプリに出会います。
nRF Connect
このアプリによって、値の意味が少しずつわかってきます。
ここでようやく、アプリから得られるデータと最終的に欲しいデータの関連性が見えてきました。あともう少し、データの裏付けが欲しいです。
Meter BLE open API
SwitchBotのドキュメントにたどり着きました。今までの勉強の成果があり、このドキュメントを見れば、何をどうすれば良いかわかるまでに成長していました。ここまでくると、いつもは読めない英語のドキュメントもするする頭に入ってきます。
今やりたいことは、周囲にあるSwitchBot温湿度計の発するブロードキャストメッセージを認識して、Ambientサービスへ値を送り込むことです。
ServiceUUIDがあるか調べて「cba20002-224d-11e6-9fb8-0002a5d5c51b」であれば、SwitchBotシリーズであることがわかり、さらに、ServiceDataの中にあるDeviceTypeが「0x54」であれば、SwitchBot温湿度計であることが確定します。
ここまでの情報をもとにプログラムを書き直しました。
そうすると、もう一山越えなければならないことがわかりました。
ServiceDataを取得します。文字列で表すと「541064069F3F」ですが、この中の最初の1バイト目に「0x54」を見つけるような作業を行います。
厳密に言うと、バイト単位ではなくビット単位で情報が埋め込まれています。これらをうまくばらしてやる必要があります。
私のプログラミングスキルは20年前で止まっています。C++を使うことはできるけど、クラスの定義はできればしたくないレベルです。
目の前のサンプルコードではString型やstd::string型が踊っています。
この中でエレガントなコードは書けない。。。
一晩考えた末、昔の常套手段で望むことにしました。
ドキュメントを読んで、仕様書通りの構造体を作り、memcpy関数で丸ごと引き受ける方法です。
昔は当たり前に使っていたビットフィールドはもはや昔のやり方になってしまったのでしょうか。
typedef struct {
// Byte:0 Enc Type Dev Type
uint8_t deviceType : 7;
uint8_t reserved1 : 1;
// Byte:1 Status
uint8_t groupA : 1;
uint8_t groupB : 1;
uint8_t groupC : 1;
uint8_t groupD : 1;
uint8_t reserved2 : 4;
// Byte:2 Update UTC Flag Battery
uint8_t remainingBattery : 7;
uint8_t reserved3 : 1;
// Byte:3
uint8_t decimalOfTheTemperature : 4;
uint8_t humidityAlertStatus : 2;
uint8_t temperatureAlertStatus : 2;
// Byte:4
uint8_t integerOfTheTemperature : 7;
uint8_t posiNegaTemperatureFlag : 1;
// Byte:5
uint8_t humidityValue : 7;
uint8_t temperatureScale : 1;
} SwitchBotServiceData;
構造体の宣言部
SwitchBotServiceData sd;
memcpy(&sd, device.getServiceData().data(), device.getServiceData().length());
ServiceDataを構造体に流し込む様子。ここが一番泥臭い所です。
ここさえ越えれば、あとは普通の構造体のメンバとして参照できます。
ビットシフトを一切使わず、読み込めました!
エンディアンやバイトアライメントもそんなに気にすることなくできて嬉しいです。
float temperature = ((sd.posiNegaTemperatureFlag) ? 1.0 : -1.0) *
(sd.decimalOfTheTemperature * 0.1 +
sd.integerOfTheTemperature);
float humidity = (float)sd.humidityValue;
ようやくゴールが見えてきました。
あとは、得られた情報を整理して、データをAmbientへ送り込むのみです。
Ambientでは1つのチャンネルに8つのデータを送ることができます。
温度だけであれば8台、温湿度であれば4台のSwichBot温湿度計をまとめることができます。
ここで最後の山が現れます。
今まで、Ambientへは
ambient.set(1, temperature);
ambient.set(2, humidity);
ambient.send();
というようにしていました。
Ambient側では、1番目のデータは温度、2番目のデータは湿度というように認識します。
2台のSwitchBot温湿度計の温湿度を下記のように割り当てたとします。
ambient.set(1, temperature1);
ambient.set(2, humidity1);
ambient.set(3, temperature2);
ambient.set(4, humidity2);
ambient.send();
となるようにしたいのですが、BLEから得られる情報を元に
SwitchBot温湿度計1の温度:1
SwitchBot温湿度計1の湿度:2
SwitchBot温湿度計2の温度:3
SwitchBot温湿度計2の湿度:4
と割り付けるにはどうしたらよいでしょうか?
まずは、SwitchBot温湿度計1、SwitchBot温湿度計2を認識する必要があると思います。実際に自分の周囲にいくつSwitchBot温湿度計があるかは分かりません。
いつ追加され、いついなくなるかわからない状態を想定する必要があります。つまり、
1.BLEスキャンループを回しながら
2.SwitchBot温湿度計であると断定できた場合
3.アドレスを取得し、配列に格納されたアドレスを比較します。
4.もし、アドレスが一致した場合、インデックスを取得します。
5.一致しない場合は配列にアドレスを追加します。
こんなプログラムを昔はreallocを使いながら丁寧にメモリを使い管理していましたが、趣味のプログラムの世界。メモリ効率はそれほど重要ではなく、「できる」ことが大事です。あっさり配列で実装することにいたします。
ただ、アドレスと配列の関係は再起動の度にリセットされ、再起動後の発見順で割り当てられてしまいます。この点に関しては、実稼働時の課題としておきます。
温度と湿度を手に入れるだけのために、これほどまでの仕掛けがあったとは驚きです。また、BLEから出発して、C++のライブラリまで大変多くの学びの機会を得ることができました。
最後までお読みいただき誠にありがとうございました。
#SwitchBot #温度計 #モニタリング #IoT #BLE #M5Stack #Ambient