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
こんな感じのダッシュボード表示です。
以上