diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini
index 8e898f6b6..0d693b7b0 100644
--- a/platformio_override_sample.ini
+++ b/platformio_override_sample.ini
@@ -242,6 +242,9 @@ upload_speed = 921600
extra_scripts = ${common.extra_scripts}
build_flags = ${esp_defaults.build_flags}
+; NimBLE-Arduino uses arithmetic on void- and function-pointers, this is only a cosmetic warning suppression
+ -Wno-pointer-arith
+
-D BUFFER_LENGTH=128
-D MQTT_MAX_PACKET_SIZE=1200
-D uint32=uint32_t
@@ -251,7 +254,7 @@ build_flags = ${esp_defaults.build_flags}
-D sint32_t=int32_t
-D sint16_t=int16_t
-D memcpy_P=memcpy
- -D memcmp_P=memcmp
+ -D memcmp_P=memcmp
; -D USE_CONFIG_OVERRIDE
lib_extra_dirs =
diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h
index 6ac041770..c6bfdc57b 100644
--- a/tasmota/my_user_config.h
+++ b/tasmota/my_user_config.h
@@ -566,9 +566,16 @@
//#define USE_IBEACON // Add support for bluetooth LE passive scan of ibeacon devices (uses HM17 module)
//#define USE_GPS // Add support for GPS and NTP Server for becoming Stratus 1 Time Source (+3k1 code, +132 bytes RAM)
// #define USE_FLOG // Add support for GPS logging in OTA's Flash (Experimental) (+2k9 code, +8 bytes RAM)
-//#define USE_HM10 // Add support for HM-10 as a BLE-bridge for the LYWSD03 (+5k1 code)
+#ifdef ESP8266
+// #define USE_HM10 // Add support for HM-10 as a BLE-bridge (+9k3 code)
+#endif // ESP8266
//#define USE_HRXL // Add support for MaxBotix HRXL-MaxSonar ultrasonic range finders (+0k7)
+// -- built-in BLE of the ESP32 --------------------
+#ifdef ESP32
+// #define USE_MI_ESP32 // Add support for ESP32 as a BLE-bridge (+9k2 mem, +292k flash)
+#endif // ESP8266
+
// -- Power monitoring sensors --------------------
#define USE_ENERGY_MARGIN_DETECTION // Add support for Energy Margin detection (+1k6 code)
#define USE_ENERGY_POWER_LIMIT // Add additional support for Energy Power Limit detection (+1k2 code)
diff --git a/tasmota/xsns_62_MI_ESP32.ino b/tasmota/xsns_62_MI_ESP32.ino
new file mode 100644
index 000000000..4a5cf8069
--- /dev/null
+++ b/tasmota/xsns_62_MI_ESP32.ino
@@ -0,0 +1,1187 @@
+/*
+ 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.0 20200413 started - initial development by Christian Baars
+ forked - from arendst/tasmota - https://github.com/arendst/Tasmota
+
+*/
+#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;
+ } 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
+ };
+};
+
+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";
+
+#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
+ };
+
+enum MI32_TASK {
+ MI32_TASK_SCAN = 0,
+ MI32_TASK_CONN = 1,
+ MI32_TASK_TIME = 2,
+ MI32_TASK_BATT = 3,
+};
+
+/*********************************************************************************************\
+ * 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) return;
+ uint16_t uuid = advertisedDevice->getServiceDataUUID().getNative()->u16.value;
+ AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%x"),uuid);
+ uint8_t addr[6];
+ memcpy(addr,advertisedDevice->getAddress().getNative(),6);
+ MI32_ReverseMAC(addr);
+ if(uuid==0xfe95) {
+ MI32ParseResponse((char*)advertisedDevice->getServiceData().c_str(),advertisedDevice->getServiceData().length(), addr);
+ }
+ else if(uuid==0xfdcd) {
+ MI32parseCGD1Packet((char*)advertisedDevice->getServiceData().c_str(),advertisedDevice->getServiceData().length(), addr);
+ }
+ };
+};
+
+
+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; iconnect(NimBLEAddress(address), 0,false)) {
+ MI32.mode.willConnect = 0;
+ vTaskDelete( NULL );
+ }
+ }
+ else {
+ MI32Client = NimBLEDevice::getDisconnectedClient();
+ }
+ }
+ if(!MI32Client) {
+ if(NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) {
+ MI32.mode.willConnect = 0;
+ vTaskDelete( NULL );
+ }
+ MI32Client = NimBLEDevice::createClient();
+ MI32Client->setClientCallbacks(&MI32SensorCB , false);
+ MI32Client->setConnectionParams(12,12,0,51);
+ MI32Client->setConnectTimeout(10);
+ if (!MI32Client->connect(NimBLEAddress(address),0,false)) {
+ MI32.mode.willConnect = 0;
+ NimBLEDevice::deleteClient(MI32Client);
+ vTaskDelete( NULL );
+ }
+ }
+}
+
+
+void MI32StartScanTask(){
+ if (MI32.mode.connected) return;
+ MI32.mode.runningScan = 1;
+ xTaskCreatePinnedToCore(
+ MI32ScanTask, /* Function to implement the task */
+ "MI32ScanTask", /* Name of the task */
+ 4096, /* 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){
+ NimBLEScan* pScan = NimBLEDevice::getScan();
+ pScan->setAdvertisedDeviceCallbacks(&MI32ScanCallbacks);
+ pScan->setActiveScan(false);
+ pScan->start(5, MI32scanEndedCB); // hard coded duration
+ uint32_t timer = 0;
+ while (MI32.mode.runningScan){
+ if (timer>15){
+ vTaskDelete( NULL );
+ }
+ timer++;
+ vTaskDelay(1000);
+ }
+ 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 );
+ }
+ MI32ConnectActiveSensor();
+ MI32.mode.readingDone = 1;
+ switch(MIBLEsensors[MI32.state.sensor].type){
+ case LYWSD03MMC:
+ MI32.mode.readingDone = 0;
+ MI32connectLYWSD03();
+ break;
+ default:
+ break;
+ }
+ uint32_t timer = 0;
+ while (!MI32.mode.readingDone){
+ if (timer>150){
+ break;
+ }
+ timer++;
+ vTaskDelay(100);
+ }
+ MI32Client->disconnect();
+ NimBLEDevice::deleteClient(MI32Client);
+ vTaskDelay(500);
+ MI32.mode.connected = 0;
+ vTaskDelete( NULL );
+}
+
+void MI32connectLYWSD03(){
+ NimBLERemoteService* pSvc = nullptr;
+ NimBLERemoteCharacteristic* pChr = nullptr;
+ static BLEUUID serviceUUID("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6");
+ static BLEUUID charUUID("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6");
+ pSvc = MI32Client->getService(serviceUUID);
+ if(pSvc) {
+ pChr = pSvc->getCharacteristic(charUUID);
+ }
+ if(pChr->canNotify()) {
+ if(!pChr->registerForNotify(MI32notifyCB)) {
+ MI32.mode.willConnect = 0;
+ MI32Client->disconnect();
+ return;
+ }
+ }
+}
+
+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 );
+ }
+ MI32ConnectActiveSensor();
+
+ uint32_t timer = 0;
+ while (MI32.mode.connected == 0){
+ if (timer>1000){
+ break;
+ }
+ timer++;
+ vTaskDelay(10);
+ }
+
+ NimBLERemoteService* pSvc = nullptr;
+ NimBLERemoteCharacteristic* pChr = nullptr;
+ static BLEUUID serviceUUID("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6");
+ static BLEUUID charUUID("EBE0CCB7-7A0A-4B0C-8A1A-6FF2997DA3A6");
+ pSvc = MI32Client->getService(serviceUUID);
+ if(pSvc) {
+ pChr = pSvc->getCharacteristic(charUUID);
+ }
+ 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();
+ NimBLEDevice::deleteClient(MI32Client);
+ vTaskDelay(500);
+ MI32.mode.connected = 0;
+ vTaskDelete( NULL );
+}
+
+void MI32StartBatteryTask(){
+ if (MI32.mode.connected) return;
+ MI32.mode.willReadBatt = 1;
+ 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;
+ MI32ConnectActiveSensor();
+ uint32_t timer = 0;
+ while (MI32.mode.connected == 0){
+ if (timer>1000){
+ break;
+ }
+ timer++;
+ vTaskDelay(10);
+ }
+
+ switch(MIBLEsensors[MI32.state.sensor].type){
+ case FLORA:
+ MI32batteryFLORA();
+ break;
+ case LYWSD02:
+ MI32batteryLYWSD02();
+ break;
+ case CGD1:
+ MI32batteryCGD1();
+ break;
+ }
+ MI32Client->disconnect();
+ MI32.mode.willReadBatt = 0;
+ NimBLEDevice::deleteClient(MI32Client);
+ vTaskDelay(500);
+ MI32.mode.connected = 0;
+ vTaskDelete( NULL );
+}
+
+void MI32batteryFLORA(){
+ uint32_t timer = 0;
+ while (!MI32.mode.connected){
+ if (timer>1000){
+ break;
+ }
+ timer++;
+ vTaskDelay(10);
+ }
+ AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s connected for battery"),kMI32SlaveType[MIBLEsensors[MI32.state.sensor].type-1] );
+ NimBLERemoteService* pSvc = nullptr;
+ NimBLERemoteCharacteristic* pChr = nullptr;
+ static BLEUUID FLserviceUUID("00001204-0000-1000-8000-00805f9b34fb");
+ static BLEUUID FLcharUUID("00001a02-0000-1000-8000-00805f9b34fb");
+
+ pSvc = MI32Client->getService(FLserviceUUID);
+ if(pSvc) { /** make sure it's not null */
+ pChr = pSvc->getCharacteristic(FLcharUUID);
+ AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s: got Flora char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str());
+ }
+ else {
+ MI32.mode.readingDone = 1;
+ return;
+ }
+ if(pChr->canRead()) {
+ const char *buf = pChr->readValue().c_str();
+ MI32readBat((char*)buf);
+ }
+}
+
+void MI32batteryLYWSD02(){
+ uint32_t timer = 0;
+ while (!MI32.mode.connected){
+ if (timer>1000){
+ break;
+ }
+ timer++;
+ vTaskDelay(10);
+ }
+
+ NimBLERemoteService* pSvc = nullptr;
+ NimBLERemoteCharacteristic* pChr = nullptr;
+ static BLEUUID LY2serviceUUID("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6");
+ static BLEUUID LY2charUUID("EBE0CCC4-7A0A-4B0C-8A1A-6FF2997DA3A6");
+
+ pSvc = MI32Client->getService(LY2serviceUUID);
+ if(pSvc) {
+ pChr = pSvc->getCharacteristic(LY2charUUID);
+ AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s: got LYWSD02 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str());
+ }
+ else {
+ return;
+ }
+ if(pChr->canRead()) {
+ AddLog_P2(LOG_LEVEL_DEBUG, PSTR("LYWSD02 char"));
+ const char *buf = pChr->readValue().c_str();
+ MI32readBat((char*)buf);
+ }
+}
+
+void MI32batteryCGD1(){
+ uint32_t timer = 0;
+ while (!MI32.mode.connected){
+ if (timer>1000){
+ break;
+ }
+ timer++;
+ vTaskDelay(10);
+ }
+
+ NimBLERemoteService* pSvc = nullptr;
+ NimBLERemoteCharacteristic* pChr = nullptr;
+ static BLEUUID CGD1serviceUUID("180F");
+ static BLEUUID CGD1charUUID("2A19");
+
+ pSvc = MI32Client->getService(CGD1serviceUUID);
+ if(pSvc) {
+ pChr = pSvc->getCharacteristic(CGD1charUUID);
+ AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s: got CGD1 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str());
+ }
+ else {
+ return;
+ }
+ if(pChr->canRead()) {
+ const char *buf = pChr->readValue().c_str();
+ MI32readBat((char*)buf);
+ }
+}
+
+
+/*********************************************************************************************\
+ * 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);
+
+ DEBUG_SENSOR_LOG(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]);
+ DEBUG_SENSOR_LOG(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;
+ }
+ DEBUG_SENSOR_LOG(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"));
+ }
+ DEBUG_SENSOR_LOG(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"));
+ }
+ DEBUG_SENSOR_LOG(PSTR("Mode 6: U16: %u Hum"), _beacon.hum);
+ break;
+ case 0x07:
+ MIBLEsensors[_slot].lux=_beacon.lux & 0x00ffffff;
+ DEBUG_SENSOR_LOG(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"));
+ }
+ DEBUG_SENSOR_LOG(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"));
+ }
+ DEBUG_SENSOR_LOG(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"));
+ }
+ DEBUG_SENSOR_LOG(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"));
+ }
+ DEBUG_SENSOR_LOG(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;
+}
+
+/**
+ * @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;
+ 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.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"),D_CMND_MI32, MI32.state.sensor);
+ MI32.mode.canScan = 0;
+ if (MI32.mode.runningScan == 1 || MI32.mode.connected == 1) 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 (MI32.state.sensor==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_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}%uus/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];
+ sprintf_P(slave,"%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);
+ if (MIBLEsensors[i].type==FLORA){
+ if(!isnan(MIBLEsensors[i].temp)){ // this is the error code -> no temperature
+ 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 "\":%d"), MIBLEsensors[i].moisture);
+ }
+ if(!isnan(MIBLEsensors[i].fertility)){
+ ResponseAppend_P(PSTR(",\"Fertility\":%d"), 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 {
+ dtostrfd((MIBLEsensors[i].volt)/100.0f, Settings.flag2.temperature_resolution, slave); // reuse slave, borrow temperature resolution
+ ResponseAppend_P(PSTR(",\"" D_VOLTAGE "\":%s"), slave);
+ }
+ }
+ 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], MIBLEsensors[i].moisture);
+ }
+ if(!isnan(MIBLEsensors[i].fertility)){
+ WSContentSend_PD(HTTP_MI32_FLORA_DATA, kMI32SlaveType[MIBLEsensors[i].type-1], 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, 2, voltage); // borrow temperature resolution
+ 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
\ No newline at end of file