ESP32からXiaomiのBLE温湿度計の温湿度を取得する

ESP32からXiaomiのBLE温湿度計の温湿度を取得します。

# BLE温湿度計のプロトコル

以下のコードをESP32のArduinoに移植しました。

xiaomi-gap-parser

https://github.com/LynxyssCZ/node-xiaomi-gap-parser

こんな感じです。

```cpp
long parseMiija(const unsigned char *p_binary, int binary_length, unsigned char *p_mac, float *p_tmp, float *p_hum);

#define FACTORY_NEW		      0b0000000001
#define CONNECTING		      0b0000000010
#define IS_CENTRAL		      0b0000000100
#define IS_ENCRYPTED	      0b0000001000
#define MAC_INCLUDE		      0b0000010000
#define CAPABILITY_INCLUDE	0b0000100000
#define EVENT_INCLUDE		    0b0001000000
#define MANU_DATA_INCLUDE	  0b0010000000
#define MANU_TITLE_INCLUDE	0b0100000000
#define BINDING_CFM			    0b1000000000

unsigned short readUint16LE(const unsigned char *p_binary, int offset){
	return (unsigned short)((p_binary[offset + 1] << 8) | p_binary[offset]);
}

long parseMiija(const unsigned char *p_binary, int binary_length, unsigned char *p_mac, float *p_tmp, float *p_hum){
	unsigned short frameControl = readUint16LE(p_binary, 0);
	unsigned short productId = readUint16LE(p_binary, 2);
	unsigned char counter = p_binary[4];
	unsigned char capability;
	
  memset(p_mac, 0x00, 6);

	int offset = 5;
	
	if( frameControl & MAC_INCLUDE ){
		if( binary_length < offset + 6 )
			return -1;
    for( int i = 0 ; i < 6 ; i++ )
      p_mac[i] = p_binary[offset + 5 - i];
		offset += 6;
	}
	
	if( frameControl & CAPABILITY_INCLUDE ){
		if( binary_length < offset + 1 )
			return -1;
		capability = p_binary[offset];
		offset++;
	}
	
  if( !(frameControl & EVENT_INCLUDE) )
    return -1;
    
	if( frameControl & EVENT_INCLUDE ){
		if( binary_length < offset + 3 )
			return -1;
		unsigned short eventID = readUint16LE(p_binary, offset);
		if( eventID != 4109 )
			return -1;
		unsigned char length = p_binary[offset + 2];
		if( length < 4 || binary_length < offset + 3 + 4 )
			return -1;
		*p_tmp = readUint16LE(p_binary, offset + 3) / 10.0;
		*p_hum = readUint16LE(p_binary, offset + 3 + 2) / 10.0;	
	}
	
	return 0;
}
```

ここに、BLEアドバタイズで取得したサービスデータをぶち込みます。
ソースコード詳細は、次章で。

# 取得した温湿度をUDPで配信する

5分間隔で、BLEスキャンし、前章で実装した関数で温湿度を抽出し、JSON化した文字列をUDPで送信します。

```cpp:main.cpp
#include <Arduino.h>
#include <BLEDevice.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <Adafruit_NeoPixel.h>

#define PIXELS_PORT  2
#define NUM_OF_PIXELS 1
static Adafruit_NeoPixel pixels(NUM_OF_PIXELS, PIXELS_PORT, NEO_GRB + NEO_KHZ800);

static WiFiUDP udp;
long udp_sendText(const char *p_host, uint16_t port, const char *p_message);

#define UDP_CHANNEL_NAME  "【適当な名前】"
#define UDP_TOPIC_NAME    "【適当な名前】"
#define UDP_HOST "【UDP送信先ホスト名】"
#define UDP_PORT 【UDP送信先ポート番号】

long parseMiija(const unsigned char *p_binary, int binary_length, unsigned char *p_mac, float *p_tmp, float *p_hum);

//#define WIFI_SSID "【固定のWiFiアクセスポイントのSSID】" // WiFiアクセスポイントのSSID
//#define WIFI_PASSWORD "【固定のWiFIアクセスポイントのパスワード】" // WiFIアクセスポイントのパスワード
#define WIFI_SSID NULL // WiFiアクセスポイントのSSID
#define WIFI_PASSWORD NULL // WiFIアクセスポイントのパスワード
#define WIFI_TIMEOUT  10000
#define SERIAL_TIMEOUT1  10000
#define SERIAL_TIMEOUT2  20000
static long wifi_try_connect(bool infinit_loop);

#define BLESCAN_DURATION  60  // BLEスキャン時間(秒)
#define BLESCAN_INTARVAL  (5 * 60)  // BLEスキャン間隔(秒)

BLEScan* pBLEScan;
unsigned long start_tim = 0;

void scanCompleteCallback(BLEScanResults results){
  Serial.println("Ble Scan completed");
}

class BLEAdvertisedDevice_cb: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      std::string address = advertisedDevice.getAddress().toString();
      std::string name = advertisedDevice.getName(); 
      int rssi = advertisedDevice.getRSSI();

      if( !advertisedDevice.getServiceDataUUID().equals(BLEUUID((uint16_t)0xfe95)) )
        return;
      if( advertisedDevice.getName() != "MJ_HT_V1")
        return;

      Serial.print("BLE Device found -> Address: ");
      Serial.print(address.c_str());
      Serial.print(", Name: ");
      Serial.print(name.c_str());
      Serial.print(", RSSI: ");
      Serial.print(rssi);
      Serial.print(", ServiceDataUUID: ");
      Serial.print(advertisedDevice.getServiceDataUUID().toString().c_str());
      Serial.println("");

      if(advertisedDevice.haveServiceData()){
        int num = advertisedDevice.getServiceDataCount();
        for( int i = 0 ; i < num ; i++ ){
          int len = advertisedDevice.getServiceData(i).length();
          float tmp, hum;
          uint8_t mac[6];
          long ret = parseMiija((unsigned char*)advertisedDevice.getServiceData(i).c_str(), len, mac, &tmp, &hum );
          if( ret != 0 ){
            Serial.println("parse invalid");
            continue;
          }
          
          Serial.printf("mac=%02x%02x%02x%02x%02x%02x tmp=%f hum=%f\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], tmp, hum);

          char message[255];
          sprintf(message, "{\"topic\": \"%s\", \"channel\": \"%s\", \"data\": { \"temperature\": %f, \"humidity\": %f }}", 
                            UDP_TOPIC_NAME, UDP_CHANNEL_NAME, tmp, hum);
          ret = udp_sendText(UDP_HOST, UDP_PORT, message);
          if( ret != 0 ){
            Serial.println("UDP send ERROR");
            continue;
          }
        }
      }
    }
};

void setup() {
  Serial.begin(115200);

  pixels.begin();
  pixels.setPixelColor(0, 0x0000ff);
  pixels.show();

  long ret = wifi_try_connect(true);
  if( ret != 0 ){
    Serial.println("WiFi connect error");
    pixels.setPixelColor(0, 0xff0000);
    pixels.show();
    while(1);
  }

  pixels.setPixelColor(0, 0x00ff00);
  pixels.show();

  BLEDevice::init("");

  pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new BLEAdvertisedDevice_cb());
  pBLEScan->setActiveScan(true);

  Serial.println("Ble Scan start...");
  if( !pBLEScan->start(BLESCAN_DURATION, scanCompleteCallback, false) ){
    Serial.println("Ble scan error");
    pixels.setPixelColor(0, 0xff0000);
    pixels.show();
    while(1);
  }
}

void loop(){
  unsigned long now_tim = millis();
  if( (now_tim - start_tim) > (BLESCAN_INTARVAL * 1000) ){
    start_tim = now_tim;

    Serial.println("Ble Scan start...");
    pBLEScan->start(BLESCAN_DURATION, scanCompleteCallback, false);
  }

  delay(1);
}

static long wifi_connect(const char *ssid, const char *password, unsigned long timeout)
{
  Serial.println("");
  Serial.print("WiFi Connenting");

  if( ssid == NULL && password == NULL )
    WiFi.begin();
  else
    WiFi.begin(ssid, password);
  unsigned long past = 0;
  while (WiFi.status() != WL_CONNECTED){
    Serial.print(".");
    delay(500);
    past += 500;
    if( past > timeout ){
      Serial.println("\nCan't Connect");
      return -1;
    }
  }
  Serial.print("\nConnected : IP=");
  Serial.print(WiFi.localIP());
  Serial.print(" Mac=");
  Serial.println(WiFi.macAddress());

  return 0;
}

static long wifi_try_connect(bool infinit_loop)
{
  long ret = -1;
  do{
    ret = wifi_connect(WIFI_PASSWORD, WIFI_PASSWORD, WIFI_TIMEOUT);
    if( ret == 0 )
      return ret;

    Serial.print("\ninput SSID:");
    Serial.setTimeout(SERIAL_TIMEOUT1);
    char ssid[32 + 1] = {'\0'};
    ret = Serial.readBytesUntil('\r', ssid, sizeof(ssid) - 1);
    if( ret <= 0 )
      continue;

    delay(10);
    Serial.read();
    Serial.print("\ninput PASSWORD:");
    Serial.setTimeout(SERIAL_TIMEOUT2);
    char password[32 + 1] = {'\0'};
    ret = Serial.readBytesUntil('\r', password, sizeof(password) - 1);
    if( ret <= 0 )
      continue;

    delay(10);
    Serial.read();
    Serial.printf("\nSSID=%s PASSWORD=", ssid);
    for( int i = 0 ; i < strlen(password); i++ )
      Serial.print("*");
    Serial.println("");

    ret = wifi_connect(ssid, password, WIFI_TIMEOUT);
    if( ret == 0 )
      return ret;
  }while(infinit_loop);

  return ret;
}

long udp_sendText(const char *p_host, uint16_t port, const char *p_message)
{
  long ret;
  
  ret = udp.beginPacket(p_host, port);
  if( !ret )
    return -1;

  int len = strlen(p_message);
  ret = udp.write((const uint8_t*)p_message, len);
  udp.endPacket();

  if( ret != len )
    return -1;

  return 0;
}

#define FACTORY_NEW		      0b0000000001
#define CONNECTING		      0b0000000010
#define IS_CENTRAL		      0b0000000100
#define IS_ENCRYPTED	      0b0000001000
#define MAC_INCLUDE		      0b0000010000
#define CAPABILITY_INCLUDE	0b0000100000
#define EVENT_INCLUDE		    0b0001000000
#define MANU_DATA_INCLUDE	  0b0010000000
#define MANU_TITLE_INCLUDE	0b0100000000
#define BINDING_CFM			    0b1000000000

unsigned short readUint16LE(const unsigned char *p_binary, int offset){
	return (unsigned short)((p_binary[offset + 1] << 8) | p_binary[offset]);
}

long parseMiija(const unsigned char *p_binary, int binary_length, unsigned char *p_mac, float *p_tmp, float *p_hum){
	unsigned short frameControl = readUint16LE(p_binary, 0);
	unsigned short productId = readUint16LE(p_binary, 2);
	unsigned char counter = p_binary[4];
	unsigned char capability;
	
  memset(p_mac, 0x00, 6);

	int offset = 5;
	
	if( frameControl & MAC_INCLUDE ){
		if( binary_length < offset + 6 )
			return -1;
    for( int i = 0 ; i < 6 ; i++ )
      p_mac[i] = p_binary[offset + 5 - i];
		offset += 6;
	}
	
	if( frameControl & CAPABILITY_INCLUDE ){
		if( binary_length < offset + 1 )
			return -1;
		capability = p_binary[offset];
		offset++;
	}
	
  if( !(frameControl & EVENT_INCLUDE) )
    return -1;
    
	if( frameControl & EVENT_INCLUDE ){
		if( binary_length < offset + 3 )
			return -1;
		unsigned short eventID = readUint16LE(p_binary, offset);
		if( eventID != 4109 )
			return -1;
		unsigned char length = p_binary[offset + 2];
		if( length < 4 || binary_length < offset + 3 + 4 )
			return -1;
		*p_tmp = readUint16LE(p_binary, offset + 3) / 10.0;
		*p_hum = readUint16LE(p_binary, offset + 3 + 2) / 10.0;	
	}
	
	return 0;
}
```

ちなみに、ESP32は、M5Stamp C3にしてます。それ以外にする場合は、以下のboardを変えたり、PIXELS_PORT のあたりを変えたりしてください

```ini:platformeio.ini
; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:esp32-c3-devkitm-1]
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino
monitor_speed = 115200
upload_port = COM5
monitor_port = COM5
board_build.partitions = no_ota.csv
build_flags = -DCORE_DEBUG_LEVEL=0
lib_deps = adafruit/Adafruit NeoPixel@^1.10.5
```

# IoT Dashboard

上記の温湿度をダッシュボードとして表示するためのシステムを作成しました。

poruruba/IoTDashboard

https://github.com/poruruba/IoTDashboard

こんな感じのダッシュボード表示です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/729044d9-0d81-a1c0-8a92-1dc127f1ff9c.png)

以上