LoginSignup
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-08-15

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

BLE温湿度計のプロトコル

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

xiaomi-gap-parser

こんな感じです。

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で送信します。

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 のあたりを変えたりしてください

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

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

image.png

以上

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0