/*
xsns_62_MI_ESP32.ino - MI-BLE-sensors via ESP32 support for Tasmota
Copyright (C) 2020 Christian Baars and Theo Arends
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
--------------------------------------------------------------------------------------------
Version yyyymmdd Action Description
--------------------------------------------------------------------------------------------
0.9.0.1 20200706 changed - adapt to new NimBLE-API, tweak scan process
0.9.0.0 20200413 started - initial development by Christian Baars
forked - from arendst/tasmota - https://github.com/arendst/Tasmota
*/
#ifdef ESP32 // ESP32 only. Use define USE_HM10 for ESP8266 support
#ifdef USE_MI_ESP32
#define XSNS_62 62
#include
#include
void MI32scanEndedCB(NimBLEScanResults results);
void MI32notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify);
struct {
uint16_t perPage = 4;
uint32_t period; // set manually in addition to TELE-period, is set to TELE-period after start
struct {
uint32_t init:1;
uint32_t connected:1;
uint32_t autoScan:1;
uint32_t canScan:1;
uint32_t runningScan:1;
uint32_t canConnect:1;
uint32_t willConnect:1;
uint32_t readingDone:1;
uint32_t shallSetTime:1;
uint32_t willSetTime:1;
uint32_t shallReadBatt:1;
uint32_t willReadBatt:1;
uint32_t shallSetUnit:1;
uint32_t willSetUnit:1;
} mode;
struct {
uint8_t sensor; // points to to the number 0...255
} state;
} MI32;
#pragma pack(1) // byte-aligned structures to read the sensor data
struct {
uint16_t temp;
uint8_t hum;
uint16_t volt; // LYWSD03 only
} LYWSD0x_HT;
struct {
uint8_t spare;
uint16_t temp;
uint16_t hum;
} CGD1_HT;
struct {
uint16_t temp;
uint8_t spare;
uint32_t lux;
uint8_t moist;
uint16_t fert;
} Flora_TLMF; // temperature, lux, moisture, fertility
struct mi_beacon_t{
uint16_t frame;
uint16_t productID;
uint8_t counter;
uint8_t Mac[6];
uint8_t spare;
uint8_t type;
uint8_t ten;
uint8_t size;
union {
struct{ //0d
uint16_t temp;
uint16_t hum;
}HT;
uint8_t bat; //0a
uint16_t temp; //04
uint16_t hum; //06
uint32_t lux; //07
uint8_t moist; //08
uint16_t fert; //09
};
};
struct cg_packet_t {
uint16_t frameID;
uint8_t serial[6];
uint16_t mode;
union {
struct {
int16_t temp; // -9 - 59 °C
uint16_t hum;
};
uint8_t bat;
};
};
#pragma pack(0)
struct mi_sensor_t{
uint8_t type; //Flora = 1; MI-HT_V1=2; LYWSD02=3; LYWSD03=4; CGG1=5; CGD1=6
uint8_t serial[6];
uint8_t showedUp;
float temp; //Flora, MJ_HT_V1, LYWSD0x, CGx
union {
struct {
float moisture;
float fertility;
uint32_t lux;
}; // Flora
struct {
float hum;
}; // MJ_HT_V1, LYWSD0x
};
union
{
uint8_t bat; // many values seem to be hard-coded garbage (LYWSD0x, GCD1)
uint16_t volt; // LYWSD03MMC
};
char firmware[6]; // actually only for FLORA but hopefully we can add for more devices
};
std::vector MIBLEsensors;
BLEScan* MI32Scan;
BLEScanResults MI32foundDevices;
/*********************************************************************************************\
* constants
\*********************************************************************************************/
#define D_CMND_MI32 "MI32"
const char S_JSON_MI32_COMMAND_NVALUE[] PROGMEM = "{\"" D_CMND_MI32 "%s\":%d}";
const char S_JSON_MI32_COMMAND[] PROGMEM = "{\"" D_CMND_MI32 "%s%s\"}";
const char kMI32_Commands[] PROGMEM = "Period|Time|Page|Battery|Unit";
#define FLORA 1
#define MJ_HT_V1 2
#define LYWSD02 3
#define LYWSD03MMC 4
#define CGG1 5
#define CGD1 6
const uint16_t kMI32SlaveID[6]={ 0x0098, // Flora
0x01aa, // MJ_HT_V1
0x045b, // LYWSD02
0x055b, // LYWSD03
0x0347, // CGG1
0x0576 // CGD1
};
const char kMI32SlaveType1[] PROGMEM = "Flora";
const char kMI32SlaveType2[] PROGMEM = "MJ_HT_V1";
const char kMI32SlaveType3[] PROGMEM = "LYWSD02";
const char kMI32SlaveType4[] PROGMEM = "LYWSD03";
const char kMI32SlaveType5[] PROGMEM = "CGG1";
const char kMI32SlaveType6[] PROGMEM = "CGD1";
const char * kMI32SlaveType[] PROGMEM = {kMI32SlaveType1,kMI32SlaveType2,kMI32SlaveType3,kMI32SlaveType4,kMI32SlaveType5,kMI32SlaveType6};
/*********************************************************************************************\
* enumerations
\*********************************************************************************************/
enum MI32_Commands { // commands useable in console or rules
CMND_MI32_PERIOD, // set period like TELE-period in seconds between read-cycles
CMND_MI32_TIME, // set LYWSD02-Time from ESP8266-time
CMND_MI32_PAGE, // sensor entries per web page, which will be shown alternated
CMND_MI32_BATTERY, // read all battery levels
CMND_MI32_UNIT // toggles the displayed unit between C/F (LYWSD02)
};
enum MI32_TASK {
MI32_TASK_SCAN = 0,
MI32_TASK_CONN = 1,
MI32_TASK_TIME = 2,
MI32_TASK_BATT = 3,
MI32_TASK_UNIT = 4,
};
/*********************************************************************************************\
* Classes
\*********************************************************************************************/
class MI32SensorCallback : public NimBLEClientCallbacks {
void onConnect(NimBLEClient* pclient) {
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("connected %s"), kMI32SlaveType[(MIBLEsensors[MI32.state.sensor].type)-1]);
MI32.mode.willConnect = 0;
MI32.mode.connected = 1;
}
void onDisconnect(NimBLEClient* pclient) {
MI32.mode.connected = 0;
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("disconnected %s"), kMI32SlaveType[(MIBLEsensors[MI32.state.sensor].type)-1]);
}
bool onConnParamsUpdateRequest(NimBLEClient* MI32Client, const ble_gap_upd_params* params) {
if(params->itvl_min < 24) { /** 1.25ms units */
return false;
} else if(params->itvl_max > 40) { /** 1.25ms units */
return false;
} else if(params->latency > 2) { /** Number of intervals allowed to skip */
return false;
} else if(params->supervision_timeout > 100) { /** 10ms units */
return false;
}
return true;
}
};
class MI32AdvCallbacks: public NimBLEAdvertisedDeviceCallbacks {
void onResult(NimBLEAdvertisedDevice* advertisedDevice) {
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Advertised Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData().length());
if (advertisedDevice->getServiceData().length() == 0) {
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("No Xiaomi Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData().length());
return;
}
uint16_t uuid = advertisedDevice->getServiceDataUUID().getNative()->u16.value;
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("UUID: %x"),uuid);
uint8_t addr[6];
memcpy(addr,advertisedDevice->getAddress().getNative(),6);
MI32_ReverseMAC(addr);
if(uuid==0xfe95) {
MI32ParseResponse((char*)advertisedDevice->getServiceData().data(),advertisedDevice->getServiceData().length(), addr);
MI32Scan->erase(advertisedDevice->getAddress());
}
else if(uuid==0xfdcd) {
MI32parseCGD1Packet((char*)advertisedDevice->getServiceData().data(),advertisedDevice->getServiceData().length(), addr);
MI32Scan->erase(advertisedDevice->getAddress());
}
else {
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("No Xiaomi Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData().length());
}
};
};
static MI32AdvCallbacks MI32ScanCallbacks;
static MI32SensorCallback MI32SensorCB;
static NimBLEClient* MI32Client;
/*********************************************************************************************\
* BLE callback functions
\*********************************************************************************************/
void MI32scanEndedCB(NimBLEScanResults results){
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Scan ended"));
MI32.mode.runningScan = 0;
}
void MI32notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify){
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Notified length: %u"),length);
switch(MIBLEsensors[MI32.state.sensor].type){
case LYWSD03MMC: case LYWSD02:
MI32readHT_LY((char*)pData);
MI32.mode.readingDone = 1;
break;
default:
MI32.mode.readingDone = 1;
break;
}
}
/*********************************************************************************************\
* Helper functions
\*********************************************************************************************/
void MI32_ReverseMAC(uint8_t _mac[]){
uint8_t _reversedMAC[6];
for (uint8_t i=0; i<6; i++){
_reversedMAC[5-i] = _mac[i];
}
memcpy(_mac,_reversedMAC, sizeof(_reversedMAC));
}
/*********************************************************************************************\
* common functions
\*********************************************************************************************/
/**
* @brief Return the slot number of a known sensor or return create new sensor slot
*
* @param _serial BLE address of the sensor
* @param _type Type number of the sensor
* @return uint32_t Known or new slot in the sensors-vector
*/
uint32_t MIBLEgetSensorSlot(uint8_t (&_serial)[6], uint16_t _type){
DEBUG_SENSOR_LOG(PSTR("%s: will test ID-type: %x"),D_CMND_MI32, _type);
bool _success = false;
for (uint32_t i=0;i<6;i++){ // i < sizeof(kMI32SlaveID) gives compiler warning
if(_type == kMI32SlaveID[i]){
DEBUG_SENSOR_LOG(PSTR("MI32: ID is type %u"), i);
_type = i+1;
_success = true;
}
else {
DEBUG_SENSOR_LOG(PSTR("%s: ID-type is not: %x"),D_CMND_MI32,kMI32SlaveID[i]);
}
}
if(!_success) return 0xff;
DEBUG_SENSOR_LOG(PSTR("%s: vector size %u"),D_CMND_MI32, MIBLEsensors.size());
for(uint32_t i=0; i= NIMBLE_MAX_CONNECTIONS) {
MI32.mode.willConnect = 0;
DEBUG_SENSOR_LOG(PSTR("%s: max connection already reached"),D_CMND_MI32);
return false;
}
if(!MI32Client) {
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: will create client"),D_CMND_MI32);
MI32Client = NimBLEDevice::createClient();
MI32Client->setClientCallbacks(&MI32SensorCB , false);
MI32Client->setConnectionParams(12,12,0,48);
MI32Client->setConnectTimeout(30);
}
if (!MI32Client->connect(_address,false)) {
MI32.mode.willConnect = 0;
NimBLEDevice::deleteClient(MI32Client);
DEBUG_SENSOR_LOG(PSTR("%s: did not connect client"),D_CMND_MI32);
return false;
}
DEBUG_SENSOR_LOG(PSTR("%s: did create new client"),D_CMND_MI32);
return true;
// }
}
void MI32StartScanTask(){
if (MI32.mode.connected) return;
MI32.mode.runningScan = 1;
// Wifi.counter = Wifi.counter + 3;
xTaskCreatePinnedToCore(
MI32ScanTask, /* Function to implement the task */
"MI32ScanTask", /* Name of the task */
8192, /* Stack size in words */
NULL, /* Task input parameter */
0, /* Priority of the task */
NULL, /* Task handle. */
0); /* Core where the task should run */
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: Start scanning"),D_CMND_MI32);
}
void MI32ScanTask(void *pvParameters){
if (MI32Scan == nullptr) MI32Scan = NimBLEDevice::getScan();
DEBUG_SENSOR_LOG(PSTR("%s: Scan Cache Length: %u"),D_CMND_MI32, MI32Scan->getResults().getCount());
MI32Scan->setAdvertisedDeviceCallbacks(&MI32ScanCallbacks);
MI32Scan->setActiveScan(false);
MI32Scan->start(5, MI32scanEndedCB, true); // hard coded duration
uint32_t timer = 0;
while (MI32.mode.runningScan){
if (timer>15){
vTaskDelete( NULL );
}
timer++;
vTaskDelay(1000/ portTICK_PERIOD_MS);
}
vTaskDelete( NULL );
}
void MI32StartSensorTask(){
MI32.mode.willConnect = 1;
xTaskCreatePinnedToCore(
MI32SensorTask, /* Function to implement the task */
"MI32SensorTask", /* Name of the task */
8192, /* Stack size in words */
NULL, /* Task input parameter */
15, /* Priority of the task */
NULL, /* Task handle. */
0); /* Core where the task should run */
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: Start sensor connections"),D_CMND_MI32);
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: with sensor: %u"),D_CMND_MI32, MI32.state.sensor);
}
void MI32SensorTask(void *pvParameters){
if (MIBLEsensors[MI32.state.sensor].type != LYWSD03MMC) {
MI32.mode.willConnect = 0;
vTaskDelete( NULL );
}
if (MI32ConnectActiveSensor()){
uint32_t timer = 0;
while (MI32.mode.connected == 0){
if (timer>1000){
MI32Client->disconnect();
NimBLEDevice::deleteClient(MI32Client);
MI32.mode.willConnect = 0;
vTaskDelay(100/ portTICK_PERIOD_MS);
vTaskDelete( NULL );
}
timer++;
vTaskDelay(10/ portTICK_PERIOD_MS);
}
timer = 150;
switch(MIBLEsensors[MI32.state.sensor].type){
case LYWSD03MMC:
MI32.mode.readingDone = 0;
if(MI32connectLYWSD03forNotification()) timer=0;
break;
default:
break;
}
while (!MI32.mode.readingDone){
if (timer>150){
break;
}
timer++;
vTaskDelay(100/ portTICK_PERIOD_MS);
}
MI32Client->disconnect();
DEBUG_SENSOR_LOG(PSTR("%s: requested disconnect"),D_CMND_MI32);
}
vTaskDelay(500/ portTICK_PERIOD_MS);
MI32.mode.connected = 0;
vTaskDelete( NULL );
}
bool MI32connectLYWSD03forNotification(){
NimBLERemoteService* pSvc = nullptr;
NimBLERemoteCharacteristic* pChr = nullptr;
static BLEUUID serviceUUID(0xebe0ccb0,0x7a0a,0x4b0c,0x8a1a6ff2997da3a6);
static BLEUUID charUUID(0xebe0ccc1,0x7a0a,0x4b0c,0x8a1a6ff2997da3a6);
pSvc = MI32Client->getService(serviceUUID);
if(pSvc) {
pChr = pSvc->getCharacteristic(charUUID);
}
if (pChr){
if(pChr->canNotify()) {
if(pChr->subscribe(true,false,MI32notifyCB)) {
return true;
}
}
}
return false;
}
void MI32StartTimeTask(){
MI32.mode.willConnect = 1;
xTaskCreatePinnedToCore(
MI32TimeTask, /* Function to implement the task */
"MI32TimeTask", /* Name of the task */
8912, /* Stack size in words */
NULL, /* Task input parameter */
15, /* Priority of the task */
NULL, /* Task handle. */
0); /* Core where the task should run */
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: Start time set"),D_CMND_MI32);
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: with sensor: %u"),D_CMND_MI32, MI32.state.sensor);
}
void MI32TimeTask(void *pvParameters){
if (MIBLEsensors[MI32.state.sensor].type != LYWSD02) {
MI32.mode.shallSetTime = 0;
vTaskDelete( NULL );
}
if(MI32ConnectActiveSensor()){
uint32_t timer = 0;
while (MI32.mode.connected == 0){
if (timer>1000){
break;
}
timer++;
vTaskDelay(10/ portTICK_PERIOD_MS);
}
NimBLERemoteService* pSvc = nullptr;
NimBLERemoteCharacteristic* pChr = nullptr;
static BLEUUID serviceUUID(0xEBE0CCB0,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6);
static BLEUUID charUUID(0xEBE0CCB7,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6);
pSvc = MI32Client->getService(serviceUUID);
if(pSvc) {
pChr = pSvc->getCharacteristic(charUUID);
}
if (pChr){
if(pChr->canWrite()) {
union {
uint8_t buf[5];
uint32_t time;
} _utc;
_utc.time = Rtc.utc_time;
_utc.buf[4] = Rtc.time_timezone / 60;
if(!pChr->writeValue(_utc.buf,sizeof(_utc.buf),true)) { // true is important !
MI32.mode.willConnect = 0;
MI32Client->disconnect();
}
else {
MI32.mode.shallSetTime = 0;
MI32.mode.willSetTime = 0;
}
}
}
MI32Client->disconnect();
}
vTaskDelay(500/ portTICK_PERIOD_MS);
MI32.mode.connected = 0;
vTaskDelete( NULL );
}
void MI32StartUnitTask(){
MI32.mode.willConnect = 1;
xTaskCreatePinnedToCore(
MI32UnitTask, /* Function to implement the task */
"MI32UnitTask", /* Name of the task */
8912, /* Stack size in words */
NULL, /* Task input parameter */
15, /* Priority of the task */
NULL, /* Task handle. */
0); /* Core where the task should run */
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: Start unit set"),D_CMND_MI32);
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: with sensor: %u"),D_CMND_MI32, MI32.state.sensor);
}
void MI32UnitTask(void *pvParameters){
if (MIBLEsensors[MI32.state.sensor].type != LYWSD02) {
MI32.mode.shallSetUnit = 0;
vTaskDelete( NULL );
}
if(MI32ConnectActiveSensor()){
uint32_t timer = 0;
while (MI32.mode.connected == 0){
if (timer>1000){
break;
}
timer++;
vTaskDelay(10/ portTICK_PERIOD_MS);
}
NimBLERemoteService* pSvc = nullptr;
NimBLERemoteCharacteristic* pChr = nullptr;
static BLEUUID serviceUUID("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6");
static BLEUUID charUUID("EBE0CCBE-7A0A-4B0C-8A1A-6FF2997DA3A6");
pSvc = MI32Client->getService(serviceUUID);
if(pSvc) {
pChr = pSvc->getCharacteristic(charUUID);
}
if(pChr->canRead()){
uint8_t curUnit;
const char *buf = pChr->readValue().c_str();
if( buf[0] != 0 && buf[0]<101 ){
curUnit = buf[0];
}
if(pChr->canWrite()) {
curUnit = curUnit == 0x01?0xFF:0x01; // C/F
if(!pChr->writeValue(&curUnit,sizeof(curUnit),true)) { // true is important !
MI32.mode.willConnect = 0;
MI32Client->disconnect();
}
else {
MI32.mode.shallSetUnit = 0;
MI32.mode.willSetUnit = 0;
}
}
}
MI32Client->disconnect();
}
vTaskDelay(500/ portTICK_PERIOD_MS);
MI32.mode.connected = 0;
vTaskDelete( NULL );
}
void MI32StartBatteryTask(){
if (MI32.mode.connected) return;
MI32.mode.willReadBatt = 1;
MI32.mode.willConnect = 1;
MI32.mode.canScan = 0;
xTaskCreatePinnedToCore(
MI32BatteryTask, /* Function to implement the task */
"MI32BatteryTask", /* Name of the task */
8192, /* Stack size in words */
NULL, /* Task input parameter */
15, /* Priority of the task */
NULL, /* Task handle. */
0); /* Core where the task should run */
}
void MI32BatteryTask(void *pvParameters){
// all reported battery values are probably crap, but we allow the reading on demand
switch (MIBLEsensors[MI32.state.sensor].type){
case LYWSD03MMC: case MJ_HT_V1: case CGG1:
MI32.mode.willConnect = 0;
MI32.mode.willReadBatt = 0;
vTaskDelete( NULL );
break;
default:
break;
}
MI32.mode.connected = 0;
if(MI32ConnectActiveSensor()){
uint32_t timer = 0;
while (MI32.mode.connected == 0){
if (timer>1000){
break;
}
timer++;
vTaskDelay(30/ portTICK_PERIOD_MS);
}
switch(MIBLEsensors[MI32.state.sensor].type){
case FLORA:
MI32batteryFLORA();
break;
case LYWSD02:
MI32batteryLYWSD02();
break;
case CGD1:
MI32batteryCGD1();
break;
}
MI32Client->disconnect();
}
MI32.mode.willReadBatt = 0;
// Wifi.counter = 0; // Now check it
vTaskDelay(500/ portTICK_PERIOD_MS);
MI32.mode.connected = 0;
vTaskDelete( NULL );
}
void MI32batteryFLORA(){
uint32_t timer = 0;
while (!MI32.mode.connected){
if (timer>1000){
break;
}
timer++;
vTaskDelay(10/ portTICK_PERIOD_MS);
}
DEBUG_SENSOR_LOG(PSTR("%s connected for battery"),kMI32SlaveType[MIBLEsensors[MI32.state.sensor].type-1] );
NimBLERemoteService* pSvc = nullptr;
NimBLERemoteCharacteristic* pChr = nullptr;
static BLEUUID FLserviceUUID(0x00001204,0x0000,0x1000,0x800000805f9b34fb);
static BLEUUID FLcharUUID(0x00001a02,0x0000,0x1000,0x800000805f9b34fb);
pSvc = MI32Client->getService(FLserviceUUID);
if(pSvc) {
pChr = pSvc->getCharacteristic(FLcharUUID);
}
if (pChr){
DEBUG_SENSOR_LOG(PSTR("%s: got Flora char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str());
if(pChr->canRead()) {
const char *buf = pChr->readValue().c_str();
MI32readBat((char*)buf);
//we also can read the firmware from response no extra request needed
MI32readFirmwareFLORA((char*)buf);
}
}
MI32.mode.readingDone = 1;
}
void MI32batteryLYWSD02(){
uint32_t timer = 0;
while (!MI32.mode.connected){
if (timer>1000){
break;
}
timer++;
vTaskDelay(10/ portTICK_PERIOD_MS);
}
NimBLERemoteService* pSvc = nullptr;
NimBLERemoteCharacteristic* pChr = nullptr;
static BLEUUID LY2serviceUUID(0xEBE0CCB0,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6);
static BLEUUID LY2charUUID(0xEBE0CCC4,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6);
pSvc = MI32Client->getService(LY2serviceUUID);
if(pSvc) {
pChr = pSvc->getCharacteristic(LY2charUUID);
}
if (pChr){
DEBUG_SENSOR_LOG( PSTR("%s: got LYWSD02 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str());
if(pChr->canRead()) {
DEBUG_SENSOR_LOG(PSTR("LYWSD02 char"));
const char *buf = pChr->readValue().c_str();
MI32readBat((char*)buf);
}
}
MI32.mode.readingDone = 1;
}
void MI32batteryCGD1(){
uint32_t timer = 0;
while (!MI32.mode.connected){
if (timer>1000){
break;
}
timer++;
vTaskDelay(10/ portTICK_PERIOD_MS);
}
NimBLERemoteService* pSvc = nullptr;
NimBLERemoteCharacteristic* pChr = nullptr;
static BLEUUID CGD1serviceUUID((uint16_t)0x180F);
static BLEUUID CGD1charUUID((uint16_t)0x2A19);
pSvc = MI32Client->getService(CGD1serviceUUID);
if(pSvc) {
pChr = pSvc->getCharacteristic(CGD1charUUID);
}
if (pChr){
DEBUG_SENSOR_LOG(PSTR("%s: got CGD1 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str());
if(pChr->canRead()) {
const char *buf = pChr->readValue().c_str();
MI32readBat((char*)buf);
}
}
MI32.mode.readingDone = 1;
}
/*********************************************************************************************\
* parse the response from advertisements
\*********************************************************************************************/
void MI32parseMiBeacon(char * _buf, uint32_t _slot){
float _tempFloat;
mi_beacon_t _beacon;
if (MIBLEsensors[_slot].type==MJ_HT_V1 || MIBLEsensors[_slot].type==CGG1){
memcpy((uint8_t*)&_beacon+1,(uint8_t*)_buf, sizeof(_beacon)); // shift by one byte for the MJ_HT_V1
memcpy((uint8_t*)&_beacon.Mac,(uint8_t*)&_beacon.Mac+1,6); // but shift back the MAC
}
else{
memcpy((void*)&_beacon,(void*)_buf, sizeof(_beacon));
}
MI32_ReverseMAC(_beacon.Mac);
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MiBeacon type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[0],(uint8_t)_buf[1],(uint8_t)_buf[2],(uint8_t)_buf[3],(uint8_t)_buf[4],(uint8_t)_buf[5],(uint8_t)_buf[6],(uint8_t)_buf[7]);
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR(" type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[8],(uint8_t)_buf[9],(uint8_t)_buf[10],(uint8_t)_buf[11],(uint8_t)_buf[12],(uint8_t)_buf[13],(uint8_t)_buf[14],(uint8_t)_buf[15]);
if(MIBLEsensors[_slot].type==4 || MIBLEsensors[_slot].type==6){
DEBUG_SENSOR_LOG(PSTR("LYWSD03 and CGD1 no support for MiBeacon, type %u"),MIBLEsensors[_slot].type);
return;
}
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s at slot %u"), kMI32SlaveType[MIBLEsensors[_slot].type-1],_slot);
switch(_beacon.type){
case 0x04:
_tempFloat=(float)(_beacon.temp)/10.0f;
if(_tempFloat<60){
MIBLEsensors[_slot].temp=_tempFloat;
DEBUG_SENSOR_LOG(PSTR("Mode 4: temp updated"));
}
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 4: U16: %u Temp"), _beacon.temp );
break;
case 0x06:
_tempFloat=(float)(_beacon.hum)/10.0f;
if(_tempFloat<101){
MIBLEsensors[_slot].hum=_tempFloat;
DEBUG_SENSOR_LOG(PSTR("Mode 6: hum updated"));
}
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 6: U16: %u Hum"), _beacon.hum);
break;
case 0x07:
MIBLEsensors[_slot].lux=_beacon.lux & 0x00ffffff;
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 7: U24: %u Lux"), _beacon.lux & 0x00ffffff);
break;
case 0x08:
_tempFloat =(float)_beacon.moist;
if(_tempFloat<100){
MIBLEsensors[_slot].moisture=_tempFloat;
DEBUG_SENSOR_LOG(PSTR("Mode 8: moisture updated"));
}
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 8: U8: %u Moisture"), _beacon.moist);
break;
case 0x09:
_tempFloat=(float)(_beacon.fert);
if(_tempFloat<65535){ // ???
MIBLEsensors[_slot].fertility=_tempFloat;
DEBUG_SENSOR_LOG(PSTR("Mode 9: fertility updated"));
}
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 9: U16: %u Fertility"), _beacon.fert);
break;
case 0x0a:
if(_beacon.bat<101){
MIBLEsensors[_slot].bat = _beacon.bat;
DEBUG_SENSOR_LOG(PSTR("Mode a: bat updated"));
}
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode a: U8: %u %%"), _beacon.bat);
break;
case 0x0d:
_tempFloat=(float)(_beacon.HT.temp)/10.0f;
if(_tempFloat<60){
MIBLEsensors[_slot].temp = _tempFloat;
DEBUG_SENSOR_LOG(PSTR("Mode d: temp updated"));
}
_tempFloat=(float)(_beacon.HT.hum)/10.0f;
if(_tempFloat<100){
MIBLEsensors[_slot].hum = _tempFloat;
DEBUG_SENSOR_LOG(PSTR("Mode d: hum updated"));
}
// AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode d: U16: %x Temp U16: %x Hum"), _beacon.HT.temp, _beacon.HT.hum);
break;
}
}
void MI32parseCGD1Packet(char * _buf, uint32_t length, uint8_t addr[6]){ // no MiBeacon
uint8_t _addr[6];
memcpy(_addr,addr,6);
uint32_t _slot = MIBLEgetSensorSlot(_addr, 0x0576); // This must be hard-coded, no object-id in Cleargrass-packet
DEBUG_SENSOR_LOG(PSTR("MI32: Sensor slot: %u"), _slot);
if(_slot==0xff) return;
cg_packet_t _packet;
memcpy((char*)&_packet,_buf,sizeof(_packet));
switch (_packet.mode){
case 0x0401:
float _tempFloat;
_tempFloat=(float)(_packet.temp)/10.0f;
if(_tempFloat<60){
MIBLEsensors.at(_slot).temp = _tempFloat;
DEBUG_SENSOR_LOG(PSTR("CGD1: temp updated"));
}
_tempFloat=(float)(_packet.hum)/10.0f;
if(_tempFloat<100){
MIBLEsensors.at(_slot).hum = _tempFloat;
DEBUG_SENSOR_LOG(PSTR("CGD1: hum updated"));
}
DEBUG_SENSOR_LOG(PSTR("CGD1: U16: %x Temp U16: %x Hum"), _packet.temp, _packet.hum);
break;
case 0x0102:
if(_packet.bat<101){
MIBLEsensors.at(_slot).bat = _packet.bat;
DEBUG_SENSOR_LOG(PSTR("Mode a: bat updated"));
}
break;
default:
DEBUG_SENSOR_LOG(PSTR("MI32: unexpected CGD1-packet"));
}
}
void MI32ParseResponse(char *buf, uint16_t bufsize, uint8_t addr[6]) {
if(bufsize<10) {
return;
}
char * _pos = buf;
uint16_t _type= _pos[3]*256 + _pos[2];
// AddLog_P2(LOG_LEVEL_INFO, PSTR("%02x %02x %02x %02x"),(uint8_t)buf[0], (uint8_t)buf[1],(uint8_t)buf[2],(uint8_t)buf[3]);
uint8_t _addr[6];
memcpy(_addr,addr,6);
uint16_t _slot = MIBLEgetSensorSlot(_addr, _type);
if(_slot!=0xff) MI32parseMiBeacon(_pos,_slot);
}
/***********************************************************************\
* Read data from connections
\***********************************************************************/
void MI32readHT_LY(char *_buf){
DEBUG_SENSOR_LOG(PSTR("%s: raw data: %x%x%x%x%x%x%x"),D_CMND_MI32,_buf[0],_buf[1],_buf[2],_buf[3],_buf[4],_buf[5],_buf[6]);
if(_buf[0] != 0 && _buf[1] != 0){
memcpy(&LYWSD0x_HT,(void *)_buf,sizeof(LYWSD0x_HT));
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s: T * 100: %u, H: %u, V: %u"),D_CMND_MI32,LYWSD0x_HT.temp,LYWSD0x_HT.hum, LYWSD0x_HT.volt);
uint32_t _slot = MI32.state.sensor;
DEBUG_SENSOR_LOG(PSTR("MIBLE: Sensor slot: %u"), _slot);
static float _tempFloat;
_tempFloat=(float)(LYWSD0x_HT.temp)/100.0f;
if(_tempFloat<60){
MIBLEsensors[_slot].temp=_tempFloat;
MIBLEsensors[_slot].showedUp=255; // this sensor is real
}
_tempFloat=(float)LYWSD0x_HT.hum;
if(_tempFloat<100){
MIBLEsensors[_slot].hum = _tempFloat;
DEBUG_SENSOR_LOG(PSTR("LYWSD0x: hum updated"));
}
if (MIBLEsensors[_slot].type == LYWSD03MMC){
MIBLEsensors[_slot].volt = LYWSD0x_HT.volt;
}
}
}
bool MI32readBat(char *_buf){
DEBUG_SENSOR_LOG(PSTR("%s: raw data: %x%x%x%x%x%x%x"),D_CMND_MI32,_buf[0],_buf[1],_buf[2],_buf[3],_buf[4],_buf[5],_buf[6]);
if(_buf[0] != 0){
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: Battery: %u"),D_CMND_MI32,_buf[0]);
uint32_t _slot = MI32.state.sensor;
DEBUG_SENSOR_LOG(PSTR("MIBLE: Sensor slot: %u"), _slot);
if(_buf[0]<101){
MIBLEsensors[_slot].bat=_buf[0];
return true;
}
}
return false;
}
bool MI32readFirmwareFLORA(char *_buf){
DEBUG_SENSOR_LOG(PSTR("%s: raw data: %x%x%x%x%x%x%x"),D_CMND_MI32,_buf[0],_buf[1],_buf[2],_buf[3],_buf[4],_buf[5],_buf[6]);
if(_buf[0] != 0){
char _firmware[5]; // FLORA send 5 byte for firmware version
strncpy(_firmware, _buf+2, 5);
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: Firmware: %s"),D_CMND_MI32,_firmware);
uint32_t _slot = MI32.state.sensor;
DEBUG_SENSOR_LOG(PSTR("MIBLE: Sensor slot: %u"), _slot);
memcpy(MIBLEsensors[_slot].firmware, _firmware, 5);
MIBLEsensors[_slot].firmware[5] = '\0';
return true;
}
return false;
}
/**
* @brief Main loop of the driver, "high level"-loop
*
*/
void MI32EverySecond(bool restart){
static uint32_t _counter = MI32.period - 15;
static uint32_t _nextSensorSlot = 0;
if(restart){
_counter = 0;
MI32.mode.canScan = 0;
MI32.mode.canConnect = 1;
MI32.mode.willReadBatt = 0;
MI32.mode.willConnect = 0;
return;
}
if (MI32.mode.shallSetTime) {
MI32.mode.canScan = 0;
MI32.mode.canConnect = 0;
if (MI32.mode.willSetTime == 0){
MI32.mode.willSetTime = 1;
MI32StartTask(MI32_TASK_TIME);
}
}
if (MI32.mode.shallSetUnit) {
MI32.mode.canScan = 0;
MI32.mode.canConnect = 0;
if (MI32.mode.willSetUnit == 0){
MI32.mode.willSetUnit = 1;
MI32StartTask(MI32_TASK_UNIT);
}
}
if (MI32.mode.willReadBatt) return;
if (_counter>MI32.period) {
_counter = 0;
MI32.mode.canScan = 0;
MI32.mode.canConnect = 1;
}
if(MI32.mode.connected == 1 || MI32.mode.willConnect == 1) return;
if(MIBLEsensors.size()==0) {
if (MI32.mode.runningScan == 0 && MI32.mode.canScan == 1) MI32StartTask(MI32_TASK_SCAN);
return;
}
if(_counter==0) {
MI32.state.sensor = _nextSensorSlot;
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: active sensor now: %u of %u"),D_CMND_MI32, MI32.state.sensor, MIBLEsensors.size()-1);
MI32.mode.canScan = 0;
if (MI32.mode.runningScan|| MI32.mode.connected || MI32.mode.willConnect) return;
_nextSensorSlot++;
MI32.mode.canConnect = 1;
if(MI32.mode.connected == 0) {
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("will connect to %s"),kMI32SlaveType[MIBLEsensors[MI32.state.sensor].type-1] );
if (MI32.mode.shallReadBatt) {
MI32StartTask(MI32_TASK_BATT);
}
else{
MI32StartTask(MI32_TASK_CONN);
}
}
if (_nextSensorSlot>(MIBLEsensors.size()-1)) {
_nextSensorSlot= 0;
_counter++;
if (MI32.mode.shallReadBatt){
MI32.mode.shallReadBatt = 0;
}
MI32.mode.canConnect = 0;
MI32.mode.canScan = 1;
}
}
else _counter++;
if (MI32.state.sensor>MIBLEsensors.size()-1) {
_nextSensorSlot = 0;
MI32.mode.canScan = 1;
}
MI32StartTask(MI32_TASK_SCAN);
}
/*********************************************************************************************\
* Commands
\*********************************************************************************************/
bool MI32Cmd(void) {
char command[CMDSZ];
bool serviced = true;
uint8_t disp_len = strlen(D_CMND_MI32);
if (!strncasecmp_P(XdrvMailbox.topic, PSTR(D_CMND_MI32), disp_len)) { // prefix
uint32_t command_code = GetCommandCode(command, sizeof(command), XdrvMailbox.topic + disp_len, kMI32_Commands);
switch (command_code) {
case CMND_MI32_PERIOD:
if (XdrvMailbox.data_len > 0) {
if (XdrvMailbox.payload==1) {
MI32EverySecond(true);
XdrvMailbox.payload = MI32.period;
}
else {
MI32.period = XdrvMailbox.payload;
}
}
else {
XdrvMailbox.payload = MI32.period;
}
Response_P(S_JSON_MI32_COMMAND_NVALUE, command, XdrvMailbox.payload);
break;
case CMND_MI32_TIME:
if (XdrvMailbox.data_len > 0) {
if(MIBLEsensors.size()>XdrvMailbox.payload){
if(MIBLEsensors[XdrvMailbox.payload].type == LYWSD02){
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: will set Time"),D_CMND_MI32);
MI32.state.sensor = XdrvMailbox.payload;
MI32.mode.canScan = 0;
MI32.mode.canConnect = 0;
MI32.mode.shallSetTime = 1;
MI32.mode.willSetTime = 0;
}
}
}
Response_P(S_JSON_MI32_COMMAND_NVALUE, command, XdrvMailbox.payload);
break;
case CMND_MI32_UNIT:
if (XdrvMailbox.data_len > 0) {
if(MIBLEsensors.size()>XdrvMailbox.payload){
if(MIBLEsensors[XdrvMailbox.payload].type == LYWSD02){
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: will set Unit"),D_CMND_MI32);
MI32.state.sensor = XdrvMailbox.payload;
MI32.mode.canScan = 0;
MI32.mode.canConnect = 0;
MI32.mode.shallSetUnit = 1;
MI32.mode.willSetUnit = 0;
}
}
}
Response_P(S_JSON_MI32_COMMAND_NVALUE, command, XdrvMailbox.payload);
break;
case CMND_MI32_PAGE:
if (XdrvMailbox.data_len > 0) {
if (XdrvMailbox.payload == 0) XdrvMailbox.payload = MI32.perPage; // ignore 0
MI32.perPage = XdrvMailbox.payload;
}
else XdrvMailbox.payload = MI32.perPage;
Response_P(S_JSON_MI32_COMMAND_NVALUE, command, XdrvMailbox.payload);
break;
case CMND_MI32_BATTERY:
MI32EverySecond(true);
MI32.mode.shallReadBatt = 1;
MI32.mode.canConnect = 1;
XdrvMailbox.payload = MI32.period;
Response_P(S_JSON_MI32_COMMAND, command, "");
break;
default:
// else for Unknown command
serviced = false;
break;
}
} else {
return false;
}
return serviced;
}
/*********************************************************************************************\
* Presentation
\*********************************************************************************************/
const char HTTP_MI32[] PROGMEM = "{s}MI ESP32 {m}%u%s / %u{e}";
const char HTTP_MI32_SERIAL[] PROGMEM = "{s}%s %s{m}%02x:%02x:%02x:%02x:%02x:%02x%{e}";
const char HTTP_BATTERY[] PROGMEM = "{s}%s" " Battery" "{m}%u %%{e}";
const char HTTP_VOLTAGE[] PROGMEM = "{s}%s " D_VOLTAGE "{m}%s V{e}";
const char HTTP_MI32_FLORA_DATA[] PROGMEM = "{s}%s" " Fertility" "{m}%u us/cm{e}";
const char HTTP_MI32_HL[] PROGMEM = "{s}
{m}
{e}";
void MI32Show(bool json)
{
if (json) {
for (uint32_t i = 0; i < MIBLEsensors.size(); i++) {
/*
char slave[33];
snprintf_P(slave, sizeof(slave), PSTR("%s-%02x%02x%02x"),
kMI32SlaveType[MIBLEsensors[i].type-1],MIBLEsensors[i].serial[3],MIBLEsensors[i].serial[4],MIBLEsensors[i].serial[5]);
ResponseAppend_P(PSTR(",\"%s\":{"), slave);
*/
ResponseAppend_P(PSTR(",\"%s-%02x%02x%02x\":{"),
kMI32SlaveType[MIBLEsensors[i].type-1],
MIBLEsensors[i].serial[3], MIBLEsensors[i].serial[4], MIBLEsensors[i].serial[5]);
if (MIBLEsensors[i].type == FLORA) {
if (!isnan(MIBLEsensors[i].temp)) {
char temperature[FLOATSZ]; // all sensors have temperature
dtostrfd(MIBLEsensors[i].temp, Settings.flag2.temperature_resolution, temperature);
ResponseAppend_P(PSTR("\"" D_JSON_TEMPERATURE "\":%s"), temperature);
} else {
ResponseAppend_P(PSTR("}"));
continue;
}
if (MIBLEsensors[i].lux!=0x0ffffff) { // this is the error code -> no lux
ResponseAppend_P(PSTR(",\"" D_JSON_ILLUMINANCE "\":%u"), MIBLEsensors[i].lux);
}
if (!isnan(MIBLEsensors[i].moisture)) {
ResponseAppend_P(PSTR(",\"" D_JSON_MOISTURE "\":%f"), MIBLEsensors[i].moisture);
}
if (!isnan(MIBLEsensors[i].fertility)) {
ResponseAppend_P(PSTR(",\"Fertility\":%f"), MIBLEsensors[i].fertility);
}
}
if (MIBLEsensors[i].type > FLORA){
if (!isnan(MIBLEsensors[i].hum) && !isnan(MIBLEsensors[i].temp)) {
ResponseAppendTHD(MIBLEsensors[i].temp, MIBLEsensors[i].hum);
}
}
if (MIBLEsensors[i].bat != 0x00) { // this is the error code -> no battery
if (MIBLEsensors[i].type != LYWSD03MMC) {
ResponseAppend_P(PSTR(",\"Battery\":%u"), MIBLEsensors[i].bat);
} else {
char voltage[FLOATSZ];
dtostrfd((MIBLEsensors[i].volt)/1000.0f, Settings.flag2.voltage_resolution, voltage);
ResponseAppend_P(PSTR(",\"" D_VOLTAGE "\":%s"), voltage);
}
if (MIBLEsensors[i].type == FLORA) { //actually we can only read FLORA
ResponseAppend_P(PSTR(",\"Firmware\":\"%s\""), MIBLEsensors[i].firmware);
}
}
ResponseAppend_P(PSTR("}"));
}
#ifdef USE_WEBSERVER
} else {
static uint16_t _page = 0;
static uint16_t _counter = 0;
int32_t i = _page * MI32.perPage;
uint32_t j = i + MI32.perPage;
if (j+1>MIBLEsensors.size()){
j = MIBLEsensors.size();
}
char stemp[5] ={0};
if (MIBLEsensors.size()-(_page*MI32.perPage)>1 && MI32.perPage!=1) {
sprintf_P(stemp,"-%u",j);
}
if (MIBLEsensors.size()==0) i=-1; // only for the GUI
WSContentSend_PD(HTTP_MI32, i+1,stemp,MIBLEsensors.size());
for (i; i no valid value
WSContentSend_PD(HTTP_SNS_ILLUMINANCE, kMI32SlaveType[MIBLEsensors[i].type-1], MIBLEsensors[i].lux);
}
if (!isnan(MIBLEsensors[i].moisture)) {
WSContentSend_PD(HTTP_SNS_MOISTURE, kMI32SlaveType[MIBLEsensors[i].type-1], int(MIBLEsensors[i].moisture));
}
if (!isnan(MIBLEsensors[i].fertility)) {
WSContentSend_PD(HTTP_MI32_FLORA_DATA, kMI32SlaveType[MIBLEsensors[i].type-1], int(MIBLEsensors[i].fertility));
}
}
if (MIBLEsensors[i].type>FLORA) { // everything "above" Flora
if (!isnan(MIBLEsensors[i].hum) && !isnan(MIBLEsensors[i].temp)) {
WSContentSend_THD(kMI32SlaveType[MIBLEsensors[i].type-1], MIBLEsensors[i].temp, MIBLEsensors[i].hum);
}
}
if(MIBLEsensors[i].bat!=0x00){
if (MIBLEsensors[i].type != LYWSD03MMC) {
WSContentSend_PD(HTTP_BATTERY, kMI32SlaveType[MIBLEsensors[i].type-1], MIBLEsensors[i].bat);
} else {
char voltage[FLOATSZ];
dtostrfd((MIBLEsensors[i].volt)/1000.0f, Settings.flag2.voltage_resolution, voltage);
WSContentSend_PD(HTTP_VOLTAGE, kMI32SlaveType[MIBLEsensors[i].type-1], voltage);
}
}
}
_counter++;
if(_counter>3) {
_page++;
_counter=0;
}
if (MIBLEsensors.size()%MI32.perPage==0 && _page==MIBLEsensors.size()/MI32.perPage) { _page = 0; }
if (_page>MIBLEsensors.size()/MI32.perPage) { _page = 0; }
#endif // USE_WEBSERVER
}
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
bool Xsns62(uint8_t function)
{
bool result = false;
if (FUNC_INIT == function){
MI32Init();
}
if (MI32.mode.init) {
switch (function) {
case FUNC_EVERY_SECOND:
MI32EverySecond(false);
break;
case FUNC_COMMAND:
result = MI32Cmd();
break;
case FUNC_JSON_APPEND:
MI32Show(1);
break;
#ifdef USE_WEBSERVER
case FUNC_WEB_SENSOR:
MI32Show(0);
break;
#endif // USE_WEBSERVER
}
}
return result;
}
#endif // USE_MI_ESP32
#endif // ESP32