600円の温湿度計(LYWSD03MMC)とESP32でIoT
Xiaomiから発売されているMijiaブランドの温湿度計"LYWSD03MMC"は600円程度と安価にもかかわらずBLEに対応している。
この温湿度計のデータをESP32で収集して、IoTサービス Ambient
に記録した。
https://buy.mi.com/hk/item/3202200073
できること
温湿度の記録がブラウザからグラフ確認できるようになる。
あらかじめやっておくこと
温湿度計の購入
Aliexpress
ESP32モジュールの購入
ス
イッチサイエンス
秋
月電子
ESP32を常時稼働させるのでUSBアダプターと変換コネクタがあるとよい。
USBアダ
プター
変
換コネクタ
Ambientのアカウント作成
目次
・温湿度計にカスタムファームウェアを適用
・AmbientのチャネルIDとライトキーの確認
・ESP32プログラム
・Ambientの設定
温湿度計にカスタムファームウェアを適用
LYWSD03MMCのBLEは暗号化されているため、これを解除するためのカスタムファームウェアを適用する。
詳細はこちら→ https://github.com/pvvx/ATC_MiThermometer
1. 温湿度計の電源を入れる。
2. Chromeブラウザを使用して chrome://flags/#enable-experimental-web-platform-features
にアクセスする。
3. Experimental Web Platform features をEnableにしてブラウザを再起動する。
4. https://github.com/pvvx/ATC_MiThermometer
にアクセスする。
5. 「Fiemware」の項にある「LYWSD03MMC Custom Firmware Version
3.7」(.binファイル)を保存する。
6. https://pvvx.github.io/ATC_MiThermometer/TelinkMiFlasher.html
にアクセスする。
7. 「Connect」を選択。
8. しばらくすると「LYWSD03MMC」が表示されるので、これを選択して「ペア設定」を選択。
9. 「Do Activation」を選択。
10. Select Firmwareの「ファイルを選択」を選択。
11. 先ほど保存した「ATC_v37b.bin」を開く。
12. 「Start Flashing」を選択。
13. 数分後にファームウェアが適用され、Status:Disconnected となるので、もう一度「Connect」を選択。
14.「ATC_?????? -
ペア設定済み」が表示されるので、ATC_以降の6桁(ここでは97C5B1)をメモしておく。これは温湿度計のMACアドレス下6桁を表している。メモしたらキャンセル
を選択して終了。
AmbinetのチャネルIDとライトキーの確認
1. https://ambidata.io/ch/channels.html
にアクセスしてログインする。
2. 「チャネルを作る」を選択。
3.
チャネルが作成されたら、「チャネルID」と「ライトキー」をメモしておく。
これらはESP32のプログラム時に使用する。
ESP32プログラム
1. ESP32モジュールの開発環境セットアップ
http://trac.switch-science.com/wiki/esp32_setup
2. Ambientライブラリーのインストール
https://ambidata.io/docs/esp8266/#library_import
3. ArduinoIDEを開き、「ツール」→「Partition Scheme」→「Huge APP(3MB No OTA/1MB
SPIFFS)」を選択する。
4. ESP32モジュールのArduinoプログラムはこちら↓
LYWSD03MMC.ino (クリックでダウンロード)
5. プログラムの以下の箇所を修正する。
(修正例)
const char* ssid = "ssid";
const char* password = "password";
unsigned int channelId_01 =
48997; //ここには" "を付けない
const char* writeKey_01 =
"13fa17a451333efd";
uint8_t mac_01[6] =
{0xa4,0xc1,0x38,0x97,0xC5,0xB1}; //A4:C1:38:97:C5:B1
(ATC_97C5B1の場合)
uint8_t mac_02[6] =
{0xa4,0xc1,0x38,0xBB,0xBB,0xBB}; //A4:C1:38:BB:BB:BB
uint8_t mac_03[6] =
{0xa4,0xc1,0x38,0xCC,0xCC,0xCC}; //A4:C1:38:CC:CC:CC
uint8_t mac_04[6] =
{0xa4,0xc1,0x38,0xDD,0xDD,0xDD}; //A4:C1:38:DD:DD:DD
#include <HardwareSerial.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include <sstream>
#include <Ambient.h>
WiFiClient client;
Ambient ambient_01;
BLEScan* pBLEScan;
const char* ssid = "ここにWifiのSSIDを入力";
const char* password = "ここにWifiのパスワードを入力";
unsigned int channelId_01 = ここにAmbientのチャネルIDを入力;
const char* writeKey_01 = "ここにAmbinentのライトキーを入力";
int scanTime = 120; // seconds
uint8_t mac[6];
float temp;
float humidity;
float temp_cache_01;
float temp_cache_02;
float temp_cache_03;
float temp_cache_04;
uint8_t mac_01[6] = {0xa4,0xc1,0x38,0xAA,0xAA,0xAA}; //A4:C1:38:AA:AA:AA
uint8_t mac_02[6] = {0xa4,0xc1,0x38,0xBB,0xBB,0xBB}; //A4:C1:38:BB:BB:BB
uint8_t mac_03[6] = {0xa4,0xc1,0x38,0xCC,0xCC,0xCC}; //A4:C1:38:CC:CC:CC
uint8_t mac_04[6] = {0xa4,0xc1,0x38,0xDD,0xDD,0xDD}; //A4:C1:38:DD:DD:DD
void printBuffer(uint8_t* buf, int len) {
for (int i = 0; i < len; i++) {
Serial.printf("%02x", buf[i]);
}
Serial.print("\n");
}
void parse_value(uint8_t* buf, int len) {
int16_t x = buf[3];
if (buf[2] > 1)
x |= buf[4] << 8;
switch (buf[0]) {
case 0x0D:
if (buf[2] && len > 6) {
temp = x / 10.0;
x = buf[5] | (buf[6] << 8);
humidity = x / 10.0;
Serial.printf("Temp: %.1f°, Humidity: %.1f %%\n", temp, humidity);
}
break;
case 0x04: {
temp = x / 10.0;
Serial.printf("Temp: %.1f°\n", temp);
}
break;
case 0x06: {
humidity = x / 10.0;
Serial.printf("Humidity: %.1f%%\n", humidity);
}
break;
case 0x0A: {
Serial.printf("Battery: %d%%", x);
if (len > 5 && buf[4] == 2) {
uint16_t battery_mv = buf[5] | (buf[6] << 8);
Serial.printf(", %d mV", battery_mv);
}
Serial.printf("\n");
}
break;
default:
Serial.printf("Type: 0x%02x ", buf[0]);
printBuffer(buf, len);
break;
}
}
// Sensor No.1 - No.4 //
void ambient_01_set(void){
if(memcmp(mac, mac_01, 6) == 0){
Serial.printf("Sensor No.1 ... Temp: %.2f°, Humidity: %.1f%%\n", temp, humidity);
if (abs(temp - temp_cache_01) < 1){
ambient_01.set(1, temp);
ambient_01.set(2, humidity);
//Serial.println(" - ambient_01 set 1");
}
temp_cache_01 = temp;
}
else if(memcmp(mac, mac_02, 6 )== 0){
Serial.printf("Sensor No.2 ... Temp: %.2f°, Humidity: %.1f%%\n", temp, humidity);
if (abs(temp - temp_cache_02) < 1){
ambient_01.set(3, temp);
ambient_01.set(4, humidity);
//Serial.println(" - ambient_01 set 2");
}
temp_cache_02 = temp;
}
else if(memcmp(mac, mac_03, 6 )== 0){
Serial.printf("Sensor No.3 ... Temp: %.2f°, Humidity: %.1f%%\n", temp, humidity);
if (abs(temp - temp_cache_03) < 1){
ambient_01.set(5, temp);
ambient_01.set(6, humidity);
//Serial.println(" - ambient_01 set 2");
}
temp_cache_03 = temp;
}
else if(memcmp(mac, mac_04, 6 )== 0){
Serial.printf("Sensor No.4 ... Temp: %.2f°, Humidity: %.1f%%\n", temp, humidity);
if (abs(temp - temp_cache_04) < 1){
ambient_01.set(7, temp);
ambient_01.set(8, humidity);
//Serial.println(" - ambient_01 set 2");
}
temp_cache_04 = temp;
}
}
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
uint8_t* findServiceData(uint8_t* data, size_t length, uint8_t* foundBlockLength) {
uint8_t* rightBorder = data + length;
while (data < rightBorder) {
uint8_t blockLength = *data + 1;
//Serial.printf("blockLength: 0x%02x\n",blockLength);
if (blockLength < 5) {
data += blockLength;
continue;
}
uint8_t blockType = *(data + 1);
uint16_t serviceType = *(uint16_t*)(data + 2);
//Serial.printf("blockType: 0x%02x, 0x%04x\n", blockType, serviceType);
if (blockType == 0x16) { // https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/
// Serial.printf("blockType: 0x%02x, 0x%04x\n", blockType, serviceType);
/* 16-bit UUID for Members 0xFE95 Xiaomi Inc. https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf */
if (serviceType == 0xfe95 || serviceType == 0x181a) { // mi or custom service
//Serial.printf("blockLength: 0x%02x\n",blockLength);
//Serial.printf("blockType: 0x%02x, 0x%04x\n", blockType, serviceType);
*foundBlockLength = blockLength;
return data;
}
}
data += blockLength;
}
return nullptr;
}
void onResult(BLEAdvertisedDevice advertisedDevice) {
uint8_t* payload = advertisedDevice.getPayload();
size_t payloadLength = advertisedDevice.getPayloadLength();
uint8_t serviceDataLength = 0;
uint8_t* serviceData = findServiceData(payload, payloadLength, &serviceDataLength);
if (serviceData == nullptr || serviceDataLength < 15)
return;
uint16_t serviceType = *(uint16_t*)(serviceData + 2);
//Serial.printf("Found service '%04x' data len: %d, ", serviceType, serviceDataLength);
//printBuffer(serviceData, serviceDataLength);
if (serviceType == 0xfe95) {
if (serviceData[5] & 0x10) {
mac[5] = serviceData[9];
mac[4] = serviceData[10];
mac[3] = serviceData[11];
mac[2] = serviceData[12];
mac[1] = serviceData[13];
mac[0] = serviceData[14];
Serial.printf("MAC: "); printBuffer(mac, 6);
}
if ((serviceData[5] & 0x08) == 0) { // not encrypted
serviceDataLength -= 15;
payload = &serviceData[15];
while (serviceDataLength > 3) {
parse_value(payload, serviceDataLength);
serviceDataLength -= payload[2] + 3;
payload += payload[2] + 3;
}
Serial.printf("count: %d\n", serviceData[8]);
} else {
if (serviceDataLength > 19) { // aes-ccm bindkey
// https://github.com/ahpohl/xiaomi_lywsd03mmc
// https://github.com/Magalex2x14/LYWSD03MMC-info
Serial.printf("Crypted data[%d]! ", serviceDataLength - 15);
}
Serial.printf("count: %d\n", serviceData[8]);
}
} else { // serviceType == 0x181a
if(serviceDataLength > 18) { // custom format
mac[5] = serviceData[4];
mac[4] = serviceData[5];
mac[3] = serviceData[6];
mac[2] = serviceData[7];
mac[1] = serviceData[8];
mac[0] = serviceData[9];
//Serial.printf("MAC: ");
//printBuffer(mac, 6);
temp = *(int16_t*)(serviceData + 10) / 100.0;
humidity = *(uint16_t*)(serviceData + 12) / 100.0;
uint16_t vbat = *(uint16_t*)(serviceData + 14);
//Serial.printf("Temp: %.2f°, Humidity: %.2f%%, Vbatt: %d, Battery: %d%%, flg: 0x%02x, cout: %d\n", temp, humidity, vbat, serviceData[16], serviceData[18], serviceData[17]);
} else if(serviceDataLength == 17) { // format atc1441
Serial.printf("MAC: ");printBuffer(serviceData + 4, 6);
int16_t x = (serviceData[10]<<8) | serviceData[11];
temp = x / 10.0;
uint16_t vbat = x = (serviceData[14]<<8) | serviceData[15];
Serial.printf("Temp: %.1f°, Humidity: %d%%, Vbatt: %d, Battery: %d%%, cout: %d\n", temp, serviceData[12], vbat, serviceData[13], serviceData[16]);
}
}
ambient_01_set();
}
};
void setup() {
Serial.begin(115200);
ambient_01.begin(channelId_01, writeKey_01, &client);
Serial.println("Scanning...");
BLEDevice::init("");
pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks(), true);
pBLEScan->setInterval(625); // default 100
pBLEScan->setWindow(625); // default 100, less or equal setInterval value
pBLEScan->setActiveScan(true);
}
void loop() {
BLEScanResults foundDevices = pBLEScan->start(scanTime, false);
pBLEScan->stop();
pBLEScan->clearResults();
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
}
ambient_01.send();
Serial.println(" - ambient send");
WiFi.disconnect(true);
}
Ambientの設定
1. Ambeintにログインして、設定するチャネル名を選択する。
2. 「+」マークをクリック。グラフが表示される。
3.
「チャネル/データ設定」を選択する。
4. 赤枠の部分を設定して「設定する」を選択する。
5.
温湿度計とESP32がBLEで接続されており、さらにESP32がWifiに接続してAmbientにデータを送信していれば、約2分後からグラフ表示が始まる。
6.
ESP32モジュールはBLEとWifiの電波が届く位置を探して、コンセントに繋げたままにする。(基板むき出しが怖いので養生テープで保護した)
memo
LYWSD03MMC
https://yasurok2.wordpress.com/2020/10/18/custom-firmware-for-lywsd03mmc/
https://maky-ba.hatenablog.com/entry/2021/06/18/215016
https://twitter.com/tomozh/status/1441657460893241345
https://www.youtube.com/watch?v=NXKzFG61lNs
ESP32
http://marchan.e5.valueserver.jp/cabin/comp/jbox/arc212/doc21201.html
https://forum.arduino.cc/t/solved-reading-advertising-data-via-arduinoble-hack-for-atc_mithermometer/677540
https://forum.arduino.cc/t/esp32-xiaomi-lywsd03mmc/685428
https://decode.red/net/archives/746
https://www.denshi.club/cookbook/wireless/ble/ble-13-esp321.html
https://github.com/karolkalinski/esp32-snippets/blob/master/Mijia-LYWSD03MMC-Client/Mijia-LYWSD03MMC-Client.ino
https://bitbucket.org/dstacer/workspace/snippets/jBoazB/esp32-arduino-ide-ble-scanner-for-xiaomi
Ambient
https://ambidata.io/samples/m5stack/ble_gw/
CSS
https://web.hazu.jp/css-describe-code/
https://webtools.dounokouno.com/htmlescape/index.html