From 2dbd1ef97387fa20242a9f58ace80592ed8abca3 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Sat, 16 Jan 2021 15:39:33 +0100 Subject: [PATCH 001/186] Add support for 24/26/32/34 bit RFID Wiegand Add support for 24/26/32/34 bit RFID Wiegand interface (D0/D1) by Sigurd Leuther (#3647, #10565) --- CHANGELOG.md | 1 + RELEASENOTES.md | 1 + tasmota/language/af_AF.h | 2 + tasmota/language/bg_BG.h | 2 + tasmota/language/cs_CZ.h | 2 + tasmota/language/de_DE.h | 2 + tasmota/language/el_GR.h | 2 + tasmota/language/en_GB.h | 2 + tasmota/language/es_ES.h | 2 + tasmota/language/fr_FR.h | 2 + tasmota/language/he_HE.h | 2 + tasmota/language/hu_HU.h | 2 + tasmota/language/it_IT.h | 2 + tasmota/language/ko_KO.h | 2 + tasmota/language/nl_NL.h | 2 + tasmota/language/pl_PL.h | 2 + tasmota/language/pt_BR.h | 2 + tasmota/language/pt_PT.h | 2 + tasmota/language/ro_RO.h | 2 + tasmota/language/ru_RU.h | 2 + tasmota/language/sk_SK.h | 2 + tasmota/language/sv_SE.h | 2 + tasmota/language/tr_TR.h | 2 + tasmota/language/uk_UA.h | 2 + tasmota/language/vi_VN.h | 2 + tasmota/language/zh_CN.h | 2 + tasmota/language/zh_TW.h | 2 + tasmota/my_user_config.h | 3 +- tasmota/support_features.ino | 4 +- tasmota/tasmota_template.h | 6 + tasmota/xsns_82_wiegand.ino | 429 +++++++++++++++++++++++++++++++++++ tools/decode-status.py | 4 +- 32 files changed, 493 insertions(+), 5 deletions(-) create mode 100644 tasmota/xsns_82_wiegand.ino diff --git a/CHANGELOG.md b/CHANGELOG.md index 79e41be9e..743b0fcaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. - Support for up to 4 I2C SEESAW_SOIL Capacitance & Temperature sensors by Peter Franck (#10481) - ESP8266 Support for 2MB and up linker files with 1MB and up LittleFS - ESP32 support for TLS MQTT using BearSSL (same as ESP8266) +- Support for 24/26/32/34 bit RFID Wiegand interface (D0/D1) by Sigurd Leuther (#3647) ### Breaking Changed - ESP32 switch from default SPIFFS to default LittleFS file system loosing current (zigbee) files diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b3f8e38a0..136d679f3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -78,6 +78,7 @@ The attached binaries can also be downloaded from http://ota.tasmota.com/tasmota - Support for disabling 38kHz IR modulation using ``#define IR_SEND_USE_MODULATION false`` [#10301](https://github.com/arendst/Tasmota/issues/10301) - Support for SPI display driver for ST7789 TFT by Gerhard Mutz [#9037](https://github.com/arendst/Tasmota/issues/9037) - Support for time proportioned (``#define USE_TIMEPROP``) and optional PID (``#define USE_PID``) relay control [#10412](https://github.com/arendst/Tasmota/issues/10412) +- Support for 24/26/32/34 bit RFID Wiegand interface (D0/D1) by Sigurd Leuther [#3647](https://github.com/arendst/Tasmota/issues/3647) - Support rotary encoder on Shelly Dimmer [#10407](https://github.com/arendst/Tasmota/issues/10407#issuecomment-756240920) - Support character `#` to be replaced by `space`-character in command ``Publish`` topic [#10258](https://github.com/arendst/Tasmota/issues/10258) - Basic support for ESP32 Odroid Go 16MB binary tasmota32-odroidgo.bin [#8630](https://github.com/arendst/Tasmota/issues/8630) diff --git a/tasmota/language/af_AF.h b/tasmota/language/af_AF.h index f6fb4a662..b13396097 100644 --- a/tasmota/language/af_AF.h +++ b/tasmota/language/af_AF.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/bg_BG.h b/tasmota/language/bg_BG.h index f00b30d2c..b69e1b81c 100644 --- a/tasmota/language/bg_BG.h +++ b/tasmota/language/bg_BG.h @@ -781,6 +781,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/cs_CZ.h b/tasmota/language/cs_CZ.h index d529a4e16..6ed6b3b63 100644 --- a/tasmota/language/cs_CZ.h +++ b/tasmota/language/cs_CZ.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/de_DE.h b/tasmota/language/de_DE.h index 071bfaaa0..635318b54 100644 --- a/tasmota/language/de_DE.h +++ b/tasmota/language/de_DE.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/el_GR.h b/tasmota/language/el_GR.h index 9f1342b0b..718ee89d8 100644 --- a/tasmota/language/el_GR.h +++ b/tasmota/language/el_GR.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/en_GB.h b/tasmota/language/en_GB.h index 11acc25fb..9d0a73354 100644 --- a/tasmota/language/en_GB.h +++ b/tasmota/language/en_GB.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/es_ES.h b/tasmota/language/es_ES.h index bb9bdd4d0..b35de9d0b 100644 --- a/tasmota/language/es_ES.h +++ b/tasmota/language/es_ES.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/fr_FR.h b/tasmota/language/fr_FR.h index 709788ecc..5dfc7c5ba 100644 --- a/tasmota/language/fr_FR.h +++ b/tasmota/language/fr_FR.h @@ -778,6 +778,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "CarteSD CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/he_HE.h b/tasmota/language/he_HE.h index 1ed935184..b36412ac1 100644 --- a/tasmota/language/he_HE.h +++ b/tasmota/language/he_HE.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/hu_HU.h b/tasmota/language/hu_HU.h index b8d4d5347..62f2131ce 100644 --- a/tasmota/language/hu_HU.h +++ b/tasmota/language/hu_HU.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/it_IT.h b/tasmota/language/it_IT.h index 6791d768a..391f166a7 100644 --- a/tasmota/language/it_IT.h +++ b/tasmota/language/it_IT.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 - CS" #define D_SENSOR_SSD1331_DC "SSD1331 - DC" #define D_SENSOR_SDCARD_CS "Scheda SD - CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/ko_KO.h b/tasmota/language/ko_KO.h index a51333856..7593261df 100644 --- a/tasmota/language/ko_KO.h +++ b/tasmota/language/ko_KO.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/nl_NL.h b/tasmota/language/nl_NL.h index 185e3a0c0..0eea3bb75 100644 --- a/tasmota/language/nl_NL.h +++ b/tasmota/language/nl_NL.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/pl_PL.h b/tasmota/language/pl_PL.h index 9da13e702..2d614d037 100644 --- a/tasmota/language/pl_PL.h +++ b/tasmota/language/pl_PL.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/pt_BR.h b/tasmota/language/pt_BR.h index f48328e4c..ecb4a8afc 100644 --- a/tasmota/language/pt_BR.h +++ b/tasmota/language/pt_BR.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/pt_PT.h b/tasmota/language/pt_PT.h index f7893127c..6f7a11f01 100644 --- a/tasmota/language/pt_PT.h +++ b/tasmota/language/pt_PT.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/ro_RO.h b/tasmota/language/ro_RO.h index b4641fca8..270b251d7 100644 --- a/tasmota/language/ro_RO.h +++ b/tasmota/language/ro_RO.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/ru_RU.h b/tasmota/language/ru_RU.h index 9d19762a4..e37ed4e8f 100644 --- a/tasmota/language/ru_RU.h +++ b/tasmota/language/ru_RU.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "А" diff --git a/tasmota/language/sk_SK.h b/tasmota/language/sk_SK.h index 5c46b4b89..2eebf1331 100644 --- a/tasmota/language/sk_SK.h +++ b/tasmota/language/sk_SK.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/sv_SE.h b/tasmota/language/sv_SE.h index a6e6aa1f6..ed97b84e8 100644 --- a/tasmota/language/sv_SE.h +++ b/tasmota/language/sv_SE.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/tr_TR.h b/tasmota/language/tr_TR.h index 21ac79065..659dcf15b 100644 --- a/tasmota/language/tr_TR.h +++ b/tasmota/language/tr_TR.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/uk_UA.h b/tasmota/language/uk_UA.h index b32761e2e..a350cef9b 100644 --- a/tasmota/language/uk_UA.h +++ b/tasmota/language/uk_UA.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "А" diff --git a/tasmota/language/vi_VN.h b/tasmota/language/vi_VN.h index e5d7d6c35..86eb6eb2e 100644 --- a/tasmota/language/vi_VN.h +++ b/tasmota/language/vi_VN.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/zh_CN.h b/tasmota/language/zh_CN.h index 2041c1483..d0a29c3c1 100644 --- a/tasmota/language/zh_CN.h +++ b/tasmota/language/zh_CN.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "安" diff --git a/tasmota/language/zh_TW.h b/tasmota/language/zh_TW.h index 46814ce24..02d898d5a 100644 --- a/tasmota/language/zh_TW.h +++ b/tasmota/language/zh_TW.h @@ -782,6 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 CS" #define D_SENSOR_SSD1331_DC "SSD1331 DC" #define D_SENSOR_SDCARD_CS "SDCard CS" +#define D_SENSOR_WIEGAND_D0 "Wiegand D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand D1" // Units #define D_UNIT_AMPERE "安培" diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 12f47d073..f62d2a6b7 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -710,6 +710,7 @@ #define MAX31865_REF_RES 430 // Reference resistor (Usually 430Ω for a PT100, 4300Ω for a PT1000) #define MAX31865_PTD_BIAS 0 // To calibrate your not-so-good PTD //#define USE_LMT01 // Add support for TI LMT01 temperature sensor, count pulses on single GPIO (+0k5 code) +//#define USE_WIEGAND // Add support for 24/26/32/34 bit RFID Wiegand interface (D0/D1) (+1k7 code) // -- IR Remote features - all protocols from IRremoteESP8266 -------------------------- // IR Full Protocols mode is activated through platform.io only. @@ -767,8 +768,6 @@ #define USE_ZIGBEE_MAXTIME_SENSOR 60*60 // 1h #define USE_ZIGBEE_MAXTIME_LIGHT 60*60 // 1h - - // -- Other sensors/drivers ----------------------- //#define USE_TM1638 // Add support for TM1638 switches copying Switch1 .. Switch8 (+1k code) diff --git a/tasmota/support_features.ino b/tasmota/support_features.ino index 3a3597f2a..49c158653 100644 --- a/tasmota/support_features.ino +++ b/tasmota/support_features.ino @@ -704,7 +704,9 @@ void ResponseAppendFeatures(void) #ifdef USE_SEESAW_SOIL feature7 |= 0x02000000; // xsns_81_seesaw_soil.ino #endif -// feature7 |= 0x04000000; +#ifdef USE_WIEGAND + feature7 |= 0x04000000; // xsns_82_wiegand.ino +#endif // feature7 |= 0x08000000; // feature7 |= 0x10000000; diff --git a/tasmota/tasmota_template.h b/tasmota/tasmota_template.h index f2dbae6c4..40e90cccd 100644 --- a/tasmota/tasmota_template.h +++ b/tasmota/tasmota_template.h @@ -146,6 +146,7 @@ enum UserSelectablePins { GPIO_ROT1A_NP, GPIO_ROT1B_NP, // Rotary switch GPIO_ADC_PH, // Analog PH Sensor GPIO_BS814_CLK, GPIO_BS814_DAT, // Holtek BS814A2 touch ctrlr + GPIO_WIEGAND_D0, GPIO_WIEGAND_D1, // Wiegand Data lines GPIO_SENSOR_END }; enum ProgramSelectablePins { @@ -312,6 +313,7 @@ const char kSensorNames[] PROGMEM = D_SENSOR_ROTARY " A_n|" D_SENSOR_ROTARY " B_n|" D_SENSOR_ADC_PH "|" D_SENSOR_BS814_CLK "|" D_SENSOR_BS814_DAT "|" + D_SENSOR_WIEGAND_D0 "|" D_SENSOR_WIEGAND_D1 "|" ; const char kSensorNamesFixed[] PROGMEM = @@ -735,6 +737,10 @@ const uint16_t kGpioNiceList[] PROGMEM = { AGPIO(GPIO_MIEL_HVAC_TX), // Mitsubishi Electric HVAC TX pin AGPIO(GPIO_MIEL_HVAC_RX), // Mitsubishi Electric HVAC RX pin #endif +#ifdef USE_WIEGAND + AGPIO(GPIO_WIEGAND_D0), // Date line D0 of Wiegand devices + AGPIO(GPIO_WIEGAND_D1), // Date line D1 of Wiegand devices +#endif /*-------------------------------------------------------------------------------------------*\ * ESP32 specifics diff --git a/tasmota/xsns_82_wiegand.ino b/tasmota/xsns_82_wiegand.ino new file mode 100644 index 000000000..57ebfba2a --- /dev/null +++ b/tasmota/xsns_82_wiegand.ino @@ -0,0 +1,429 @@ +/* + xsns_82_wiegand.ino - Support for Wiegand Interface 125kHz NFC Tag Reader for Tasmota + + Copyright (C) 2021 Sigurd Leuther 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 . +*/ + +#ifdef USE_WIEGAND +/*********************************************************************************************\ + MQTT: + %prefix%/%topic%/SENSOR = {"Time":"2021-01-13T12:30:38","Wiegand":{"UID":"rfid tag"}} + + Domoticz: + The nvalue will be always 0 and the svalue will contain the tag UID as string. +\*********************************************************************************************/ +#warning **** Wiegand interface enabled **** + +#define XSNS_82 82 + +#define WIEGAND_BIT_TIMEOUT 25 //time to be wait after last bit detected. +// use only a randomly generate RFID for testing. using #define will save some space in the final code +// DEV_WIEGAND_TEST_MODE 1 : testing with random rfid without hardware connected, but GPIOs set correctly +// DEV_WIEGAND_TEST_MODE 2 : testing with hardware corretly connected. +// +#define DEV_WIEGAND_TEST_MODE 0 + +#ifdef DEV_WIEGAND_TEST_MODE + #if (DEV_WIEGAND_TEST_MODE==0) + #elif (DEV_WIEGAND_TEST_MODE==1) + #warning "Wiegand Interface compiled with 'DEV_WIEGAND_TEST_MODE' 1 (Random RFID)" + #elif (DEV_WIEGAND_TEST_MODE==2) + #warning "Wiegand Interface compiled with 'DEV_WIEGAND_TEST_MODE' 2 (Hardware connected)" + #else + #warning "Wiegand Interface compiled with unknown mode" + #endif +#endif + +class Wiegand { + public: + + Wiegand(void); + void Init(void); + void ScanForTag(void); +#ifdef USE_WEBSERVER + void Show(void); +#endif + + private: + + uint64_t HexStringToDec(uint64_t); + uint64_t CheckAndConvertRfid(uint64_t,uint16_t); + char translateEnterEscapeKeyPress(char); + uint8_t CalculateParities(uint64_t, int); + bool WiegandConversion (void); + static void handleD0Interrupt(void); + static void handleD1Interrupt(void); + + uint64_t rfid; + uint8_t tagSize; + + static volatile uint64_t rfidBuffer; + static volatile uint16_t bitCount; + static volatile uint32_t lastFoundTime; + static volatile uint8_t timeOut; + bool isInit = false; + +#if (DEV_WIEGAND_TEST_MODE)==1 + uint64_t GetRandomRfid(uint8_t); +#endif +}; + +Wiegand* oWiegand = new Wiegand(); +uint8_t scanDelay; + +volatile uint64_t Wiegand::rfidBuffer; +volatile uint16_t Wiegand::bitCount; +volatile uint32_t Wiegand::lastFoundTime; +volatile uint8_t Wiegand::timeOut; + +Wiegand::Wiegand() { + rfid = 0; + lastFoundTime = 0; + tagSize = 0; + rfidBuffer = 0; + bitCount = 0 ; + timeOut = 0; + isInit= false; +} + +#if (DEV_WIEGAND_TEST_MODE)==1 +uint64_t Wiegand::GetRandomRfid(uint8_t tag_size=34) { + //todo add support for 4 and 8 bit keyboard "tags" + uint64_t result = (uint32_t)HwRandom(); + uint8_t parities = 0; + bitCount = tag_size; + timeOut=millis() - WIEGAND_BIT_TIMEOUT; + result = result << 32; + result += HwRandom(); + + switch (tag_size){ + case 24: + result = (result & 0x7FFFFE) >>1; + break; + case 26: + result = (result & 0x1FFFFFE) >>1; + break; + case 32: + result = (result & 0x7FFFFFFE) >>1; + break; + case 34: + result = (result & 0x3FFFFFFFE) >>1; + break; + default: + break; + } + parities = CalculateParities(result, tag_size); + + result = (result << 1) | (parities & 0x01); //set LSB parity + if (parities & 0x80) { //MSB parity is 1 + switch (tag_size) { + case 24: + result |= 0x800000; + break; + case 26: + result |= 0x2000000; + break; + case 32: + result |= 0x80000000; + break; + case 34: + result |= 0x400000000; + break; + default: + break; + } + } + + return result; +} +#endif + +void ICACHE_RAM_ATTR Wiegand::handleD1Interrupt() { // receive a 1 bit. (D0=high & D1=low) + rfidBuffer = (rfidBuffer << 1) | 1; // leftshift + 1 bit + bitCount++; //increment the counter + lastFoundTime = millis(); // last time bit found +} + +void ICACHE_RAM_ATTR Wiegand::handleD0Interrupt() { // receive a 0 bit. (D0=low & D1=high) + rfidBuffer = rfidBuffer << 1; // leftshift the 0 bit is now at the end of rfidBuffer + bitCount++; //increment the counter + lastFoundTime = millis(); //last time bit found +} + +void Wiegand::Init() { + isInit = false; + if (PinUsed(GPIO_WIEGAND_D0) && PinUsed(GPIO_WIEGAND_D1)) { //only start, if the Wiegang pins are + #if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: Init()")); + #endif + pinMode(Pin(GPIO_WIEGAND_D0), INPUT_PULLUP); + pinMode(Pin(GPIO_WIEGAND_D1), INPUT_PULLUP); + attachInterrupt(Pin(GPIO_WIEGAND_D0), handleD0Interrupt, FALLING); + attachInterrupt(Pin(GPIO_WIEGAND_D1), handleD1Interrupt, FALLING); + isInit = true; // helps to run only if correctly setup + #if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: Testmode")); // for tests without reader attaiched + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: D0:%u"),Pin(GPIO_WIEGAND_D0)); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: D1:%u"),Pin(GPIO_WIEGAND_D1)); + #else + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: D0=%u, D1=%u"),Pin(GPIO_WIEGAND_D0), Pin(GPIO_WIEGAND_D1)); + #endif + } + #if (DEV_WIEGAND_TEST_MODE)>0 + else { + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: no GPIOs.")); + } + #endif +} + +uint64_t Wiegand::CheckAndConvertRfid(uint64_t rfidIn, uint16_t bitcount) { + uint8_t evenParityBit = 0; + uint8_t oddParityBit = (uint8_t) (rfidIn & 0x1); // last bit = odd parity + uint8_t calcParity = 0; + switch (bitcount) { + case 24: + evenParityBit = (rfidIn & 0x800000) ? 0x80 : 0; + rfidIn = (rfidIn & 0x7FFFFE) >>1; + break; + + case 26: + evenParityBit = (rfidIn & 0x2000000) ? 0x80 : 0; + rfidIn = (rfidIn & 0x1FFFFFE) >>1; + break; + + case 32: + evenParityBit = (rfidIn & 0x80000000) ? 0x80 : 0; + rfidIn = (rfidIn & 0x7FFFFFFE) >>1; + break; + + case 34: + evenParityBit = (rfidIn & 0x400000000) ? 0x80 : 0; + rfidIn = (rfidIn & 0x3FFFFFFFE) >>1; + break; + + default: + break; + } + calcParity = CalculateParities(rfidIn, bitCount); //ckeck result on http://www.ccdesignworks.com/wiegand_calc.htm with raw tag as input + if (calcParity != (evenParityBit | oddParityBit)) { // Paritybit is wrong + rfidIn=0; + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: %llu parity error"), rfidIn); + } + #if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: even (left) parity: %u "), (evenParityBit>>7)); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: even (calc) parity: %u "), (calcParity & 0x80)>>7); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: odd (right) parity: %u "), oddParityBit); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: odd (calc) parity: %u "), (calcParity & 0x01)); + #endif + return rfidIn; +} + +uint8_t Wiegand::CalculateParities(uint64_t tagWithoutParities, int tag_size=26) { + //tag_size is the size of the final tag including the 2 parity bits + //so length if the tagWithoutParities should be (tag_size-2) !! That will be not profed and + //lead to wrong results if the input value is larger! + //calculated start parity (even) will be returned as bit 8 + //calculated end parity (odd) will be returned as bit 1 + uint8_t retValue=0; + tag_size -= 2; + if (tag_size<=0) { return retValue; } //prohibit div zero exception and other wrong inputs + uint8_t parity=1; //check for odd parity on LSB + for (uint8_t i=0; i<(tag_size/2); i++) { + parity^=(tagWithoutParities & 1); + tagWithoutParities>>=1; + } + retValue |= parity; + + parity=0; //check for even parity on MSB + while (tagWithoutParities) { + parity^=(tagWithoutParities & 1); + tagWithoutParities>>=1; + } + retValue |= (parity<<7); + + return retValue; +} + +char Wiegand::translateEnterEscapeKeyPress(char oKeyPressed) { + switch(oKeyPressed) { + case 0x0b: // 11 or * key + return 0x0d; // 13 or ASCII ENTER + + case 0x0a: // 10 or # key + return 0x1b; // 27 or ASCII ESCAPE + + default: + return oKeyPressed; + } +} + +bool Wiegand::WiegandConversion () +{ + bool bRet = false; + unsigned long nowTick = millis(); + if ((nowTick - lastFoundTime) > WIEGAND_BIT_TIMEOUT) //last bit found is WIEGAND_BIT_TIMEOUT ms ago + { + #if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: raw tag: %llu "), rfidBuffer); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: bit count: %u "), bitCount); + #endif + if ((bitCount==4)||(bitCount==8)||(bitCount==24)||(bitCount==26)||(bitCount==32)||(bitCount==34)) { + if ((bitCount==24)||(bitCount==26)||(bitCount==32)||(bitCount==34)) { + // 24,26,32,34-bit Wiegand codes + rfid = CheckAndConvertRfid( rfidBuffer, bitCount); + tagSize=bitCount; + bitCount=0; + rfidBuffer=0; + bRet=true; + } + if (bitCount==4) { + // 4-bit Wiegand codes for keypads + rfid = (int)translateEnterEscapeKeyPress(rfidBuffer & 0x0000000F); + tagSize = bitCount; + bitCount = 0; + rfidBuffer = 0; + bRet=true; + } + if (bitCount==8){ + // 8-bit Wiegand codes for keypads with integrity + // 8-bit Wiegand keyboard data, high nibble is the "NOT" of low nibble + // eg if key 1 pressed, data=E1 in binary 11100001 , high nibble=1110 , low nibble = 0001 + char highNibble = (rfidBuffer & 0xf0) >>4; + char lowNibble = (rfidBuffer & 0x0f); + if (lowNibble == (~highNibble & 0x0f)) // check if low nibble matches the "NOT" of high nibble. + { + rfid = (int)translateEnterEscapeKeyPress(lowNibble); + bRet=true; + } + else { + lastFoundTime=nowTick; + bRet=false; + } + tagSize=bitCount; + bitCount=0; + rfidBuffer=0; + } + } + else { + // time reached but unknown bitCount, clear and start again + lastFoundTime=nowTick; + bitCount=0; + rfidBuffer=0; + bRet=false; + } + } + else{ + bRet=false; // watching time not finished + } + #if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: tag out: %llu "), rfid); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: tag size: %u"), tagSize); + #endif + return bRet; +} + +void Wiegand::ScanForTag() { + + if (!isInit) { return;} + #if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: ScanForTag().")); + #if (DEV_WIEGAND_TEST_MODE==1) + switch (millis() %4 ) { + case 0: + rfidBuffer = GetRandomRfid(24); + break; + case 1: + rfidBuffer = GetRandomRfid(26); + break; + case 2: + rfidBuffer = GetRandomRfid(32); + break; + case 3: + rfidBuffer = GetRandomRfid(34); + break; + default: + rfidBuffer = GetRandomRfid(34); + break; + } + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: raw generated: %lX"), rfidBuffer); // for tests without reader attaiched + #endif + #endif + if (bitCount > 0) { + uint64_t oldTag = rfid; + bool newKey = WiegandConversion(); + #if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: previous tag: %llu"), oldTag); + #endif + if(newKey && (oldTag != rfid)) { + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: new= %llu"), rfid); + } + else + { AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: prev= %llu"), rfid);} + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: bits= %u"), tagSize); + ResponseTime_P(PSTR(",\"Wiegand\":{\"UID\":\"%0llu\"}}"), rfid); + MqttPublishTeleSensor(); + } +} + +#ifdef USE_WEBSERVER +void Wiegand::Show(void) { + if (!isInit) { return; } + WSContentSend_PD(PSTR("{s}Wiegand UID{m}%llu {e}"), rfid); + #if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO,PSTR("WIE: Tag: %llu"), rfid); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: %u bits"), bitCount); + #endif +} +#endif // USE_WEBSERVER + + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xsns82(byte function) { + bool result = false; + + switch (function) { + case FUNC_INIT: + oWiegand->Init(); + scanDelay = 1; + break; + + case FUNC_EVERY_250_MSECOND: // some tags need more time, don't try shorter period + #if (DEV_WIEGAND_TEST_MODE)==1 + if (scanDelay>=4) // give a second because of the log entries to be send. + #else + if (scanDelay>=2) // only run every (delay * 250 ms) (every 250ms is too fast for some tags) + #endif + { + oWiegand->ScanForTag(); + scanDelay = 1; + } + else { + scanDelay++; + } + break; + + #ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: + oWiegand->Show(); + break; + #endif // USE_WEBSERVER + } + return result; +} +#endif // USE_WIEGAND \ No newline at end of file diff --git a/tools/decode-status.py b/tools/decode-status.py index 8d8bfc7ba..f6b6f923d 100755 --- a/tools/decode-status.py +++ b/tools/decode-status.py @@ -244,7 +244,7 @@ a_features = [[ "USE_SHELLY_DIMMER","USE_RC522","USE_FTC532","USE_DISPLAY_EPAPER_42", "USE_DISPLAY_ILI9488","USE_DISPLAY_SSD1351","USE_DISPLAY_RA8876","USE_DISPLAY_ST7789", "USE_DISPLAY_SSD1331","USE_UFILESYS","USE_TIMEPROP","USE_PID", - "USE_BS814A2","USE_SEESAW_SOIL","","", + "USE_BS814A2","USE_SEESAW_SOIL","USE_WIEGAND","", "","","","" ]] @@ -273,7 +273,7 @@ else: obj = json.load(fp) def StartDecode(): - print ("\n*** decode-status.py v20210111 by Theo Arends and Jacek Ziolkowski ***") + print ("\n*** decode-status.py v20210116 by Theo Arends and Jacek Ziolkowski ***") # print("Decoding\n{}".format(obj)) From e47065a66d3555132dab750b03fa236ccad8596f Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 16 Jan 2021 15:14:36 +0000 Subject: [PATCH 002/186] Put back SetOption40 support. --- tasmota/support_button.ino | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tasmota/support_button.ino b/tasmota/support_button.ino index 53fb218ba..473183665 100644 --- a/tasmota/support_button.ino +++ b/tasmota/support_button.ino @@ -270,6 +270,15 @@ void ButtonHandler(void) { ExecuteCommand(scmnd, SRC_BUTTON); } } + else + { + if (Settings.param[P_HOLD_IGNORE] > 0) { // SetOption40 (0) - Do not ignore button hold + if (Button.hold_timer[button_index] > loops_per_second * Settings.param[P_HOLD_IGNORE] / 10) { + Button.hold_timer[button_index] = 0; // Reset button hold counter to stay below hold trigger + Button.press_counter[button_index] = 0; // Discard button press to disable functionality + } + } + } } } } From a8235842384c56cbe2e2c3e27b19ba00914233eb Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Sat, 16 Jan 2021 16:49:37 +0100 Subject: [PATCH 003/186] ESP32 use 4K RSA for TLS --- platformio_tasmota32.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/platformio_tasmota32.ini b/platformio_tasmota32.ini index a41222bb4..717ca03a5 100644 --- a/platformio_tasmota32.ini +++ b/platformio_tasmota32.ini @@ -86,6 +86,8 @@ build_flags = ${esp_defaults.build_flags} -Dsint16_t=int16_t -Dmemcpy_P=memcpy -Dmemcmp_P=memcmp + ;for TLS we can afford compiling for 4K RSA keys + -DUSE_4K_RSA [core32] From a90eb18f1e90fe7e9ef1cb430e089d2041cec41d Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Sat, 16 Jan 2021 17:11:45 +0100 Subject: [PATCH 004/186] Fix Web UI that would not change TLS mode --- tasmota/xdrv_02_mqtt.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasmota/xdrv_02_mqtt.ino b/tasmota/xdrv_02_mqtt.ino index 9a0acba75..6324a584b 100644 --- a/tasmota/xdrv_02_mqtt.ino +++ b/tasmota/xdrv_02_mqtt.ino @@ -1315,7 +1315,7 @@ void MqttSaveSettings(void) WebGetArg("ml", tmp, sizeof(tmp)); Settings.mqtt_port = (!strlen(tmp)) ? MQTT_PORT : atoi(tmp); #ifdef USE_MQTT_TLS - Mqtt.mqtt_tls = Webserver->hasArg("b3"); // SetOption102 - Enable MQTT TLS + Settings.flag4.mqtt_tls = Webserver->hasArg("b3"); // SetOption102 - Enable MQTT TLS #endif WebGetArg("mc", tmp, sizeof(tmp)); SettingsUpdateText(SET_MQTT_CLIENT, (!strlen(tmp)) ? MQTT_CLIENT_ID : tmp); From f52f26f5663047695e64dd80ad4de2dcb751e48c Mon Sep 17 00:00:00 2001 From: s-hadinger <49731213+s-hadinger@users.noreply.github.com> Date: Sat, 16 Jan 2021 18:07:01 +0100 Subject: [PATCH 005/186] Added ``USE_MQTT_TLS_DROP_OLD_FINGERPRINT`` compile time option to drop old (less secure) TLS fingerprint (#10584) Co-authored-by: Stephan Hadinger --- CHANGELOG.md | 1 + tasmota/WiFiClientSecureLightBearSSL.cpp | 4 ++++ tasmota/my_user_config.h | 3 +++ 3 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 743b0fcaa..44b5e00c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. - ESP8266 Support for 2MB and up linker files with 1MB and up LittleFS - ESP32 support for TLS MQTT using BearSSL (same as ESP8266) - Support for 24/26/32/34 bit RFID Wiegand interface (D0/D1) by Sigurd Leuther (#3647) +- Added ``USE_MQTT_TLS_DROP_OLD_FINGERPRINT`` compile time option to drop old (less secure) TLS fingerprint ### Breaking Changed - ESP32 switch from default SPIFFS to default LittleFS file system loosing current (zigbee) files diff --git a/tasmota/WiFiClientSecureLightBearSSL.cpp b/tasmota/WiFiClientSecureLightBearSSL.cpp index 6d7ed37b4..60534c369 100755 --- a/tasmota/WiFiClientSecureLightBearSSL.cpp +++ b/tasmota/WiFiClientSecureLightBearSSL.cpp @@ -804,6 +804,7 @@ extern "C" { return 0; } +#ifndef USE_MQTT_TLS_DROP_OLD_FINGERPRINT // No match under new algorithm, do some basic checking on the key. // // RSA keys normally have an e value of 65537, which is three bytes long. @@ -838,6 +839,9 @@ extern "C" { pubkeyfingerprint_pubkey_fingerprint(xc, false); return 0; +#else // USE_TLS_OLD_FINGERPRINT_COMPAT + return 1; // no match, error +#endif // USE_TLS_OLD_FINGERPRINT_COMPAT } else { // Default (no validation at all) or no errors in prior checks = success. return 0; diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index f62d2a6b7..8f3fa5a89 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -401,6 +401,9 @@ // #define USE_MQTT_AWS_IOT // [Deprecated] Enable MQTT for AWS IoT - requires a private key (+11.9k code, +0.4k mem) // Note: you need to generate a private key + certificate per device and update 'tasmota/tasmota_aws_iot.cpp' // Full documentation here: https://github.com/arendst/Tasmota/wiki/AWS-IoT +// #define USE_MQTT_TLS_DROP_OLD_FINGERPRINT // If you use fingerprint (i.e. not CA) validation, the algorithm changed to a more secure one. + // Any valid fingerprint with the old algo will be automatically updated to the new algo. + // Enable this if you want to disable the old algo check, which should be more secure // for USE_4K_RSA (support for 4096 bits certificates, instead of 2048), you need to uncommend `-DUSE_4K_RSA` in `build_flags` from `platform.ini` or `platform_override.ini` // -- Telegram Protocol --------------------------- From ba12d8911ef1bef59aba98821d02e12184ebca5b Mon Sep 17 00:00:00 2001 From: sle Date: Sun, 17 Jan 2021 11:39:21 +0100 Subject: [PATCH 006/186] only in case of valid key do action. Issue#10585 and block very slow incoming signals --- tasmota/xsns_82_wiegand.ino | 135 +++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 64 deletions(-) diff --git a/tasmota/xsns_82_wiegand.ino b/tasmota/xsns_82_wiegand.ino index 57ebfba2a..59505a5fc 100644 --- a/tasmota/xsns_82_wiegand.ino +++ b/tasmota/xsns_82_wiegand.ino @@ -274,65 +274,72 @@ bool Wiegand::WiegandConversion () { bool bRet = false; unsigned long nowTick = millis(); - if ((nowTick - lastFoundTime) > WIEGAND_BIT_TIMEOUT) //last bit found is WIEGAND_BIT_TIMEOUT ms ago - { - #if (DEV_WIEGAND_TEST_MODE)>0 - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: raw tag: %llu "), rfidBuffer); - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: bit count: %u "), bitCount); - #endif - if ((bitCount==4)||(bitCount==8)||(bitCount==24)||(bitCount==26)||(bitCount==32)||(bitCount==34)) { - if ((bitCount==24)||(bitCount==26)||(bitCount==32)||(bitCount==34)) { - // 24,26,32,34-bit Wiegand codes - rfid = CheckAndConvertRfid( rfidBuffer, bitCount); - tagSize=bitCount; - bitCount=0; - rfidBuffer=0; - bRet=true; - } - if (bitCount==4) { - // 4-bit Wiegand codes for keypads - rfid = (int)translateEnterEscapeKeyPress(rfidBuffer & 0x0000000F); - tagSize = bitCount; - bitCount = 0; - rfidBuffer = 0; - bRet=true; - } - if (bitCount==8){ - // 8-bit Wiegand codes for keypads with integrity - // 8-bit Wiegand keyboard data, high nibble is the "NOT" of low nibble - // eg if key 1 pressed, data=E1 in binary 11100001 , high nibble=1110 , low nibble = 0001 - char highNibble = (rfidBuffer & 0xf0) >>4; - char lowNibble = (rfidBuffer & 0x0f); - if (lowNibble == (~highNibble & 0x0f)) // check if low nibble matches the "NOT" of high nibble. - { - rfid = (int)translateEnterEscapeKeyPress(lowNibble); - bRet=true; - } - else { - lastFoundTime=nowTick; - bRet=false; - } - tagSize=bitCount; - bitCount=0; - rfidBuffer=0; - } - } - else { - // time reached but unknown bitCount, clear and start again - lastFoundTime=nowTick; +//add a maximum wait time for new bits + unsigned long diffTicks = nowTick - lastFoundTime; + if ((diffTicks > WIEGAND_BIT_TIMEOUT) && (diffTicks >= 5000 )) { //max. 5 secs between 2 bits comming in + bitCount=0; + rfidBuffer=0; + lastFoundTime=nowTick; + return bRet; + } + if (diffTicks > WIEGAND_BIT_TIMEOUT) { //last bit found is WIEGAND_BIT_TIMEOUT ms ago + #if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: raw tag: %llu "), rfidBuffer); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: bit count: %u "), bitCount); + #endif + if ((bitCount==4)||(bitCount==8)||(bitCount==24)||(bitCount==26)||(bitCount==32)||(bitCount==34)) { + if ((bitCount==24)||(bitCount==26)||(bitCount==32)||(bitCount==34)) { + // 24,26,32,34-bit Wiegand codes + rfid = CheckAndConvertRfid( rfidBuffer, bitCount); + tagSize=bitCount; + bitCount=0; + rfidBuffer=0; + bRet=true; + } + if (bitCount==4) { + // 4-bit Wiegand codes for keypads + rfid = (int)translateEnterEscapeKeyPress(rfidBuffer & 0x0000000F); + tagSize = bitCount; + bitCount = 0; + rfidBuffer = 0; + bRet=true; + } + if (bitCount==8){ + // 8-bit Wiegand codes for keypads with integrity + // 8-bit Wiegand keyboard data, high nibble is the "NOT" of low nibble + // eg if key 1 pressed, data=E1 in binary 11100001 , high nibble=1110 , low nibble = 0001 + char highNibble = (rfidBuffer & 0xf0) >>4; + char lowNibble = (rfidBuffer & 0x0f); + if (lowNibble == (~highNibble & 0x0f)) // check if low nibble matches the "NOT" of high nibble. + { + rfid = (int)translateEnterEscapeKeyPress(lowNibble); + bRet=true; + } + else { + lastFoundTime=nowTick; + bRet=false; + } + tagSize=bitCount; bitCount=0; rfidBuffer=0; - bRet=false; } } - else{ - bRet=false; // watching time not finished + else { + // time reached but unknown bitCount, clear and start again + lastFoundTime=nowTick; + bitCount=0; + rfidBuffer=0; + bRet=false; } - #if (DEV_WIEGAND_TEST_MODE)>0 - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: tag out: %llu "), rfid); - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: tag size: %u"), tagSize); - #endif - return bRet; + } + else{ + bRet=false; // watching time not finished + } + #if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: tag out: %llu "), rfid); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: tag size: %u"), tagSize); + #endif + return bRet; } void Wiegand::ScanForTag() { @@ -363,19 +370,19 @@ void Wiegand::ScanForTag() { #endif if (bitCount > 0) { uint64_t oldTag = rfid; - bool newKey = WiegandConversion(); + bool validKey = WiegandConversion(); #if (DEV_WIEGAND_TEST_MODE)>0 AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: previous tag: %llu"), oldTag); #endif - if(newKey && (oldTag != rfid)) { - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: new= %llu"), rfid); - } - else - { AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: prev= %llu"), rfid);} - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: bits= %u"), tagSize); - ResponseTime_P(PSTR(",\"Wiegand\":{\"UID\":\"%0llu\"}}"), rfid); - MqttPublishTeleSensor(); - } + // only in case of valid key do action. Issue#10585 + if(validKey) { + if (oldTag != rfid) { AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: new= %llu"), rfid); } + else { AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: prev= %llu"), rfid); } + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: bits= %u"), tagSize); + ResponseTime_P(PSTR(",\"Wiegand\":{\"UID\":\"%0llu\"}}"), rfid); + MqttPublishTeleSensor(); + } + } } #ifdef USE_WEBSERVER From 8c5d4ea26d349ef7c4460a933299e2a6aa67500c Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Sun, 17 Jan 2021 12:30:20 +0100 Subject: [PATCH 007/186] fix esp32 download large files --- tasmota/xdrv_50_filesystem.ino | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tasmota/xdrv_50_filesystem.ino b/tasmota/xdrv_50_filesystem.ino index 0b7fc53d2..fb12e3b9d 100644 --- a/tasmota/xdrv_50_filesystem.ino +++ b/tasmota/xdrv_50_filesystem.ino @@ -402,6 +402,7 @@ void UFSDelete(void) { * Web support \*********************************************************************************************/ + #ifdef USE_WEBSERVER const char UFS_WEB_DIR[] PROGMEM = @@ -459,6 +460,8 @@ void UfsDirectory(void) { if (UfsDownloadFile(cp)) { // is directory strcpy(ufs_path, cp); + } else { + return; } } @@ -588,7 +591,9 @@ void UfsListDir(char *path, uint8_t depth) { } } - +#ifdef ESP32 +#define ESP32_DOWNLOAD_TASK +#endif // ESP32 uint8_t UfsDownloadFile(char *file) { File download_file; @@ -609,7 +614,7 @@ uint8_t UfsDownloadFile(char *file) { return 1; } -#ifdef ESP8266 +#ifndef ESP32_DOWNLOAD_TASK WiFiClient download_Client; uint32_t flen = download_file.size(); @@ -649,10 +654,10 @@ uint8_t UfsDownloadFile(char *file) { } download_file.close(); download_Client.stop(); -#endif // esp8266 +#endif // ESP32_DOWNLOAD_TASK -#ifdef ESP32 +#ifdef ESP32_DOWNLOAD_TASK download_file.close(); if (download_busy == true) { @@ -664,16 +669,16 @@ uint8_t UfsDownloadFile(char *file) { char *path = (char*)malloc(128); strcpy(path,file); xTaskCreatePinnedToCore(donload_task, "DT", 6000, (void*)path, 3, NULL, 1); -#endif // ESP32 +#endif // ESP32_DOWNLOAD_TASK return 0; } -#ifdef ESP32 +#ifdef ESP32_DOWNLOAD_TASK #ifndef DOWNLOAD_SIZE #define DOWNLOAD_SIZE 4096 -#endif +#endif // DOWNLOAD_SIZE void donload_task(void *path) { File download_file; WiFiClient download_Client; @@ -714,7 +719,7 @@ void donload_task(void *path) { download_busy = false; vTaskDelete( NULL ); } -#endif // ESP32 +#endif // ESP32_DOWNLOAD_TASK bool UfsUploadFileOpen(const char* upload_filename) { From 6cfc7d549ee27adcfb2baee8faf3e73144d0999e Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Sun, 17 Jan 2021 14:16:56 +0100 Subject: [PATCH 008/186] Update Italian language --- tasmota/language/it_IT.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tasmota/language/it_IT.h b/tasmota/language/it_IT.h index 391f166a7..fbf0d4baa 100644 --- a/tasmota/language/it_IT.h +++ b/tasmota/language/it_IT.h @@ -1,7 +1,7 @@ /* it-IT.h - localization for Italian - Italy for Tasmota - Copyright (C) 2021 Gennaro Tortone - some mods by Antonio Fragola - Updated by bovirus - rev. 09.01.2021 + Copyright (C) 2021 Gennaro Tortone - some mods by Antonio Fragola - Updated by bovirus - rev. 17.01.2021 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 @@ -782,8 +782,8 @@ #define D_SENSOR_SSD1331_CS "SSD1331 - CS" #define D_SENSOR_SSD1331_DC "SSD1331 - DC" #define D_SENSOR_SDCARD_CS "Scheda SD - CS" -#define D_SENSOR_WIEGAND_D0 "Wiegand D0" -#define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_WIEGAND_D0 "Wiegand - D0" +#define D_SENSOR_WIEGAND_D1 "Wiegand - D1" // Units #define D_UNIT_AMPERE "A" From 5f04cf2ec84e4584d4f9986da6ef337c593b91a3 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Sun, 17 Jan 2021 16:12:25 +0100 Subject: [PATCH 009/186] Zigbee support for Lidl Livarno Lux Remote Control Dimmer --- tasmota/xdrv_23_zigbee_5__constants.ino | 434 ++++++++++++------------ tasmota/xdrv_23_zigbee_6_commands.ino | 1 + 2 files changed, 219 insertions(+), 216 deletions(-) diff --git a/tasmota/xdrv_23_zigbee_5__constants.ino b/tasmota/xdrv_23_zigbee_5__constants.ino index 0a3926db7..7889e957a 100644 --- a/tasmota/xdrv_23_zigbee_5__constants.ino +++ b/tasmota/xdrv_23_zigbee_5__constants.ino @@ -73,7 +73,7 @@ def strings_to_pmem(arg): DO NOT EDIT */ -const char Z_strings[] PROGMEM = +const char Z_strings[] PROGMEM = "\x00" "00" "\x00" "00190200" "\x00" @@ -278,6 +278,7 @@ const char Z_strings[] PROGMEM = "LastMessageLQI" "\x00" "LastMessageRSSI" "\x00" "LastSetTime" "\x00" + "LidlPower" "\x00" "LocalTemperature" "\x00" "LocalTemperatureCalibration" "\x00" "LocalTime" "\x00" @@ -699,221 +700,222 @@ enum Z_offsets { Zo_LastMessageLQI = 3308, Zo_LastMessageRSSI = 3323, Zo_LastSetTime = 3339, - Zo_LocalTemperature = 3351, - Zo_LocalTemperatureCalibration = 3368, - Zo_LocalTime = 3396, - Zo_LocationAge = 3406, - Zo_LocationMethod = 3418, - Zo_LocationType = 3433, - Zo_LockState = 3446, - Zo_LockType = 3456, - Zo_LongPollInterval = 3465, - Zo_LongPollIntervalMin = 3482, - Zo_MainsFrequency = 3502, - Zo_MainsVoltage = 3517, - Zo_Manufacturer = 3530, - Zo_MaxTempExperienced = 3543, - Zo_MeterTypeID = 3562, - Zo_MinTempExperienced = 3574, - Zo_Mode = 3593, - Zo_Model = 3598, - Zo_ModelId = 3604, - Zo_MotorStepSize = 3612, - Zo_Movement = 3626, - Zo_MullerLightMode = 3635, - Zo_MultiApplicationType = 3651, - Zo_MultiDescription = 3672, - Zo_MultiInApplicationType = 3689, - Zo_MultiInDescription = 3712, - Zo_MultiInNumberOfStates = 3731, - Zo_MultiInOutOfService = 3753, - Zo_MultiInReliability = 3773, - Zo_MultiInStatusFlags = 3792, - Zo_MultiInValue = 3811, - Zo_MultiNumberOfStates = 3824, - Zo_MultiOutApplicationType = 3844, - Zo_MultiOutDescription = 3868, - Zo_MultiOutNumberOfStates = 3888, - Zo_MultiOutOfService = 3911, - Zo_MultiOutOutOfService = 3929, - Zo_MultiOutReliability = 3950, - Zo_MultiOutRelinquishDefault = 3970, - Zo_MultiOutStatusFlags = 3996, - Zo_MultiOutValue = 4016, - Zo_MultiReliability = 4030, - Zo_MultiRelinquishDefault = 4047, - Zo_MultiStatusFlags = 4070, - Zo_MultiValue = 4087, - Zo_MultipleScheduling = 4098, - Zo_NumberOfDevices = 4117, - Zo_NumberOfPrimaries = 4133, - Zo_NumberOfResets = 4151, - Zo_NumberofActuationsLift = 4166, - Zo_NumberofActuationsTilt = 4189, - Zo_Occupancy = 4212, - Zo_OccupancySensorType = 4222, - Zo_OccupiedCoolingSetpoint = 4242, - Zo_OccupiedHeatingSetpoint = 4266, - Zo_OnOffTransitionTime = 4290, - Zo_OpenPeriod = 4310, - Zo_OppleMode = 4321, - Zo_OutdoorTemperature = 4331, - Zo_OverTempTotalDwell = 4350, - Zo_PICoolingDemand = 4369, - Zo_PIHeatingDemand = 4385, - Zo_POD = 4401, - Zo_Panic = 4405, - Zo_PartNumber = 4411, - Zo_PersistentMemoryWrites = 4422, - Zo_PersonalAlarm = 4445, - Zo_PhysicalClosedLimit = 4459, - Zo_PhysicalClosedLimitLift = 4479, - Zo_PhysicalClosedLimitTilt = 4503, - Zo_Power = 4527, - Zo_Power2 = 4533, - Zo_Power3 = 4540, - Zo_Power4 = 4547, - Zo_PowerOffEffect = 4554, - Zo_PowerOnRecall = 4569, - Zo_PowerOnTimer = 4583, - Zo_PowerSource = 4596, - Zo_PowerThreshold = 4608, - Zo_Pressure = 4623, - Zo_PressureMaxMeasuredValue = 4632, - Zo_PressureMaxScaledValue = 4657, - Zo_PressureMinMeasuredValue = 4680, - Zo_PressureMinScaledValue = 4705, - Zo_PressureScale = 4728, - Zo_PressureScaledTolerance = 4742, - Zo_PressureScaledValue = 4766, - Zo_PressureTolerance = 4786, - Zo_Primary1Intensity = 4804, - Zo_Primary1X = 4822, - Zo_Primary1Y = 4832, - Zo_Primary2Intensity = 4842, - Zo_Primary2X = 4860, - Zo_Primary2Y = 4870, - Zo_Primary3Intensity = 4880, - Zo_Primary3X = 4898, - Zo_Primary3Y = 4908, - Zo_ProductCode = 4918, - Zo_ProductRevision = 4930, - Zo_ProductURL = 4946, - Zo_QualityMeasure = 4957, - Zo_RMSCurrent = 4972, - Zo_RMSVoltage = 4983, - Zo_ReactivePower = 4994, - Zo_RecallScene = 5008, - Zo_RemainingTime = 5020, - Zo_RemoteSensing = 5034, - Zo_RemoveAllGroups = 5048, - Zo_RemoveAllScenes = 5064, - Zo_RemoveGroup = 5080, - Zo_RemoveScene = 5092, - Zo_ResetAlarm = 5104, - Zo_ResetAllAlarms = 5115, - Zo_SWBuildID = 5130, - Zo_Sat = 5140, - Zo_SatMove = 5144, - Zo_SatStep = 5152, - Zo_SceneCount = 5160, - Zo_SceneValid = 5171, - Zo_ScheduleMode = 5182, - Zo_SeaPressure = 5195, - Zo_ShortPollInterval = 5207, - Zo_Shutter = 5225, - Zo_ShutterClose = 5233, - Zo_ShutterLift = 5246, - Zo_ShutterOpen = 5258, - Zo_ShutterStop = 5270, - Zo_ShutterTilt = 5282, - Zo_SoftwareRevision = 5294, - Zo_StackVersion = 5311, - Zo_StandardTime = 5324, - Zo_StartUpOnOff = 5337, - Zo_Status = 5350, - Zo_StoreScene = 5357, - Zo_SwitchType = 5368, - Zo_SystemMode = 5379, - Zo_TRVBoost = 5390, - Zo_TRVChildProtection = 5399, - Zo_TRVMirrorDisplay = 5418, - Zo_TRVMode = 5435, - Zo_TRVWindowOpen = 5443, - Zo_TempTarget = 5457, - Zo_Temperature = 5468, - Zo_TemperatureMaxMeasuredValue = 5480, - Zo_TemperatureMinMeasuredValue = 5508, - Zo_TemperatureTolerance = 5536, - Zo_TerncyDuration = 5557, - Zo_TerncyRotate = 5572, - Zo_ThSetpoint = 5585, - Zo_Time = 5596, - Zo_TimeEpoch = 5601, - Zo_TimeStatus = 5611, - Zo_TimeZone = 5622, - Zo_TotalProfileNum = 5631, - Zo_TuyaAutoLock = 5647, - Zo_TuyaAwayDays = 5660, - Zo_TuyaAwayTemp = 5673, - Zo_TuyaBattery = 5686, - Zo_TuyaBoostTime = 5698, - Zo_TuyaChildLock = 5712, - Zo_TuyaComfortTemp = 5726, - Zo_TuyaEcoTemp = 5742, - Zo_TuyaFanMode = 5754, - Zo_TuyaForceMode = 5766, - Zo_TuyaMaxTemp = 5780, - Zo_TuyaMinTemp = 5792, - Zo_TuyaPreset = 5804, - Zo_TuyaScheduleHolidays = 5815, - Zo_TuyaScheduleWorkdays = 5836, - Zo_TuyaTempTarget = 5857, - Zo_TuyaValveDetection = 5872, - Zo_TuyaValvePosition = 5891, - Zo_TuyaWeekSelect = 5909, - Zo_TuyaWindowDetection = 5924, - Zo_UnoccupiedCoolingSetpoint = 5944, - Zo_UnoccupiedHeatingSetpoint = 5970, - Zo_UtilityName = 5996, - Zo_ValidUntilTime = 6008, - Zo_ValvePosition = 6023, - Zo_VelocityLift = 6037, - Zo_ViewGroup = 6050, - Zo_ViewScene = 6060, - Zo_Water = 6070, - Zo_WhitePointX = 6076, - Zo_WhitePointY = 6088, - Zo_WindowCoveringType = 6100, - Zo_X = 6119, - Zo_Y = 6121, - Zo_ZCLVersion = 6123, - Zo_ZoneState = 6134, - Zo_ZoneStatus = 6144, - Zo_ZoneStatusChange = 6155, - Zo_ZoneType = 6172, - Zo_xx = 6181, - Zo_xx000A00 = 6184, - Zo_xx0A = 6193, - Zo_xx0A00 = 6198, - Zo_xx19 = 6205, - Zo_xx190A = 6210, - Zo_xx190A00 = 6217, - Zo_xxxx = 6226, - Zo_xxxx00 = 6231, - Zo_xxxx0A00 = 6238, - Zo_xxxxyy = 6247, - Zo_xxxxyyyy = 6254, - Zo_xxxxyyyy0A00 = 6263, - Zo_xxxxyyzz = 6276, - Zo_xxyy = 6285, - Zo_xxyy0A00 = 6290, - Zo_xxyyyy = 6299, - Zo_xxyyyy000000000000 = 6306, - Zo_xxyyyy0A0000000000 = 6325, - Zo_xxyyyyzz = 6344, - Zo_xxyyyyzzzz = 6353, - Zo_xxyyzzzz = 6364, + Zo_LidlPower = 3351, + Zo_LocalTemperature = 3361, + Zo_LocalTemperatureCalibration = 3378, + Zo_LocalTime = 3406, + Zo_LocationAge = 3416, + Zo_LocationMethod = 3428, + Zo_LocationType = 3443, + Zo_LockState = 3456, + Zo_LockType = 3466, + Zo_LongPollInterval = 3475, + Zo_LongPollIntervalMin = 3492, + Zo_MainsFrequency = 3512, + Zo_MainsVoltage = 3527, + Zo_Manufacturer = 3540, + Zo_MaxTempExperienced = 3553, + Zo_MeterTypeID = 3572, + Zo_MinTempExperienced = 3584, + Zo_Mode = 3603, + Zo_Model = 3608, + Zo_ModelId = 3614, + Zo_MotorStepSize = 3622, + Zo_Movement = 3636, + Zo_MullerLightMode = 3645, + Zo_MultiApplicationType = 3661, + Zo_MultiDescription = 3682, + Zo_MultiInApplicationType = 3699, + Zo_MultiInDescription = 3722, + Zo_MultiInNumberOfStates = 3741, + Zo_MultiInOutOfService = 3763, + Zo_MultiInReliability = 3783, + Zo_MultiInStatusFlags = 3802, + Zo_MultiInValue = 3821, + Zo_MultiNumberOfStates = 3834, + Zo_MultiOutApplicationType = 3854, + Zo_MultiOutDescription = 3878, + Zo_MultiOutNumberOfStates = 3898, + Zo_MultiOutOfService = 3921, + Zo_MultiOutOutOfService = 3939, + Zo_MultiOutReliability = 3960, + Zo_MultiOutRelinquishDefault = 3980, + Zo_MultiOutStatusFlags = 4006, + Zo_MultiOutValue = 4026, + Zo_MultiReliability = 4040, + Zo_MultiRelinquishDefault = 4057, + Zo_MultiStatusFlags = 4080, + Zo_MultiValue = 4097, + Zo_MultipleScheduling = 4108, + Zo_NumberOfDevices = 4127, + Zo_NumberOfPrimaries = 4143, + Zo_NumberOfResets = 4161, + Zo_NumberofActuationsLift = 4176, + Zo_NumberofActuationsTilt = 4199, + Zo_Occupancy = 4222, + Zo_OccupancySensorType = 4232, + Zo_OccupiedCoolingSetpoint = 4252, + Zo_OccupiedHeatingSetpoint = 4276, + Zo_OnOffTransitionTime = 4300, + Zo_OpenPeriod = 4320, + Zo_OppleMode = 4331, + Zo_OutdoorTemperature = 4341, + Zo_OverTempTotalDwell = 4360, + Zo_PICoolingDemand = 4379, + Zo_PIHeatingDemand = 4395, + Zo_POD = 4411, + Zo_Panic = 4415, + Zo_PartNumber = 4421, + Zo_PersistentMemoryWrites = 4432, + Zo_PersonalAlarm = 4455, + Zo_PhysicalClosedLimit = 4469, + Zo_PhysicalClosedLimitLift = 4489, + Zo_PhysicalClosedLimitTilt = 4513, + Zo_Power = 4537, + Zo_Power2 = 4543, + Zo_Power3 = 4550, + Zo_Power4 = 4557, + Zo_PowerOffEffect = 4564, + Zo_PowerOnRecall = 4579, + Zo_PowerOnTimer = 4593, + Zo_PowerSource = 4606, + Zo_PowerThreshold = 4618, + Zo_Pressure = 4633, + Zo_PressureMaxMeasuredValue = 4642, + Zo_PressureMaxScaledValue = 4667, + Zo_PressureMinMeasuredValue = 4690, + Zo_PressureMinScaledValue = 4715, + Zo_PressureScale = 4738, + Zo_PressureScaledTolerance = 4752, + Zo_PressureScaledValue = 4776, + Zo_PressureTolerance = 4796, + Zo_Primary1Intensity = 4814, + Zo_Primary1X = 4832, + Zo_Primary1Y = 4842, + Zo_Primary2Intensity = 4852, + Zo_Primary2X = 4870, + Zo_Primary2Y = 4880, + Zo_Primary3Intensity = 4890, + Zo_Primary3X = 4908, + Zo_Primary3Y = 4918, + Zo_ProductCode = 4928, + Zo_ProductRevision = 4940, + Zo_ProductURL = 4956, + Zo_QualityMeasure = 4967, + Zo_RMSCurrent = 4982, + Zo_RMSVoltage = 4993, + Zo_ReactivePower = 5004, + Zo_RecallScene = 5018, + Zo_RemainingTime = 5030, + Zo_RemoteSensing = 5044, + Zo_RemoveAllGroups = 5058, + Zo_RemoveAllScenes = 5074, + Zo_RemoveGroup = 5090, + Zo_RemoveScene = 5102, + Zo_ResetAlarm = 5114, + Zo_ResetAllAlarms = 5125, + Zo_SWBuildID = 5140, + Zo_Sat = 5150, + Zo_SatMove = 5154, + Zo_SatStep = 5162, + Zo_SceneCount = 5170, + Zo_SceneValid = 5181, + Zo_ScheduleMode = 5192, + Zo_SeaPressure = 5205, + Zo_ShortPollInterval = 5217, + Zo_Shutter = 5235, + Zo_ShutterClose = 5243, + Zo_ShutterLift = 5256, + Zo_ShutterOpen = 5268, + Zo_ShutterStop = 5280, + Zo_ShutterTilt = 5292, + Zo_SoftwareRevision = 5304, + Zo_StackVersion = 5321, + Zo_StandardTime = 5334, + Zo_StartUpOnOff = 5347, + Zo_Status = 5360, + Zo_StoreScene = 5367, + Zo_SwitchType = 5378, + Zo_SystemMode = 5389, + Zo_TRVBoost = 5400, + Zo_TRVChildProtection = 5409, + Zo_TRVMirrorDisplay = 5428, + Zo_TRVMode = 5445, + Zo_TRVWindowOpen = 5453, + Zo_TempTarget = 5467, + Zo_Temperature = 5478, + Zo_TemperatureMaxMeasuredValue = 5490, + Zo_TemperatureMinMeasuredValue = 5518, + Zo_TemperatureTolerance = 5546, + Zo_TerncyDuration = 5567, + Zo_TerncyRotate = 5582, + Zo_ThSetpoint = 5595, + Zo_Time = 5606, + Zo_TimeEpoch = 5611, + Zo_TimeStatus = 5621, + Zo_TimeZone = 5632, + Zo_TotalProfileNum = 5641, + Zo_TuyaAutoLock = 5657, + Zo_TuyaAwayDays = 5670, + Zo_TuyaAwayTemp = 5683, + Zo_TuyaBattery = 5696, + Zo_TuyaBoostTime = 5708, + Zo_TuyaChildLock = 5722, + Zo_TuyaComfortTemp = 5736, + Zo_TuyaEcoTemp = 5752, + Zo_TuyaFanMode = 5764, + Zo_TuyaForceMode = 5776, + Zo_TuyaMaxTemp = 5790, + Zo_TuyaMinTemp = 5802, + Zo_TuyaPreset = 5814, + Zo_TuyaScheduleHolidays = 5825, + Zo_TuyaScheduleWorkdays = 5846, + Zo_TuyaTempTarget = 5867, + Zo_TuyaValveDetection = 5882, + Zo_TuyaValvePosition = 5901, + Zo_TuyaWeekSelect = 5919, + Zo_TuyaWindowDetection = 5934, + Zo_UnoccupiedCoolingSetpoint = 5954, + Zo_UnoccupiedHeatingSetpoint = 5980, + Zo_UtilityName = 6006, + Zo_ValidUntilTime = 6018, + Zo_ValvePosition = 6033, + Zo_VelocityLift = 6047, + Zo_ViewGroup = 6060, + Zo_ViewScene = 6070, + Zo_Water = 6080, + Zo_WhitePointX = 6086, + Zo_WhitePointY = 6098, + Zo_WindowCoveringType = 6110, + Zo_X = 6129, + Zo_Y = 6131, + Zo_ZCLVersion = 6133, + Zo_ZoneState = 6144, + Zo_ZoneStatus = 6154, + Zo_ZoneStatusChange = 6165, + Zo_ZoneType = 6182, + Zo_xx = 6191, + Zo_xx000A00 = 6194, + Zo_xx0A = 6203, + Zo_xx0A00 = 6208, + Zo_xx19 = 6215, + Zo_xx190A = 6220, + Zo_xx190A00 = 6227, + Zo_xxxx = 6236, + Zo_xxxx00 = 6241, + Zo_xxxx0A00 = 6248, + Zo_xxxxyy = 6257, + Zo_xxxxyyyy = 6264, + Zo_xxxxyyyy0A00 = 6273, + Zo_xxxxyyzz = 6286, + Zo_xxyy = 6295, + Zo_xxyy0A00 = 6300, + Zo_xxyyyy = 6309, + Zo_xxyyyy000000000000 = 6316, + Zo_xxyyyy0A0000000000 = 6335, + Zo_xxyyyyzz = 6354, + Zo_xxyyyyzzzz = 6363, + Zo_xxyyzzzz = 6374, }; diff --git a/tasmota/xdrv_23_zigbee_6_commands.ino b/tasmota/xdrv_23_zigbee_6_commands.ino index 89e3a6e65..a8566705a 100644 --- a/tasmota/xdrv_23_zigbee_6_commands.ino +++ b/tasmota/xdrv_23_zigbee_6_commands.ino @@ -69,6 +69,7 @@ const Z_CommandConverter Z_Commands[] PROGMEM = { { Z_(PowerOffEffect), 0x0006, 0x40, 0x81, Z_(xxyy) }, // Power Off With Effect { Z_(PowerOnRecall), 0x0006, 0x41, 0x81, Z_() }, // Power On With Recall Global Scene { Z_(PowerOnTimer), 0x0006, 0x42, 0x81, Z_(xxyyyyzzzz) }, // Power On with Timed Off + { Z_(LidlPower), 0x0006, 0xFD, 0x01, Z_(xx) }, // Lidl specific encoding { Z_(Power), 0x0006, 0xFF, 0x01, Z_() }, // 0=Off, 1=On, 2=Toggle { Z_(Dimmer), 0x0008, 0x04, 0x01, Z_(xx0A00) }, // Move to Level with On/Off, xx=0..254 (255 is invalid) { Z_(DimmerUp), 0x0008, 0x06, 0x01, Z_(00190200) }, // Step up by 10%, 0.2 secs From 454892c971c81fac37c801e6ba958ec64cc4d003 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Sun, 17 Jan 2021 16:39:40 +0100 Subject: [PATCH 010/186] Zigbee report colors as RGB --- tasmota/xdrv_23_zigbee_5__constants.ino | 232 ++++++++++++------------ tasmota/xdrv_23_zigbee_5_converters.ino | 34 +++- 2 files changed, 150 insertions(+), 116 deletions(-) diff --git a/tasmota/xdrv_23_zigbee_5__constants.ino b/tasmota/xdrv_23_zigbee_5__constants.ino index 7889e957a..4176c7ee4 100644 --- a/tasmota/xdrv_23_zigbee_5__constants.ino +++ b/tasmota/xdrv_23_zigbee_5__constants.ino @@ -73,7 +73,7 @@ def strings_to_pmem(arg): DO NOT EDIT */ -const char Z_strings[] PROGMEM = +const char Z_strings[] PROGMEM = "\x00" "00" "\x00" "00190200" "\x00" @@ -380,6 +380,7 @@ const char Z_strings[] PROGMEM = "ProductRevision" "\x00" "ProductURL" "\x00" "QualityMeasure" "\x00" + "RGB" "\x00" "RMSCurrent" "\x00" "RMSVoltage" "\x00" "ReactivePower" "\x00" @@ -802,120 +803,121 @@ enum Z_offsets { Zo_ProductRevision = 4940, Zo_ProductURL = 4956, Zo_QualityMeasure = 4967, - Zo_RMSCurrent = 4982, - Zo_RMSVoltage = 4993, - Zo_ReactivePower = 5004, - Zo_RecallScene = 5018, - Zo_RemainingTime = 5030, - Zo_RemoteSensing = 5044, - Zo_RemoveAllGroups = 5058, - Zo_RemoveAllScenes = 5074, - Zo_RemoveGroup = 5090, - Zo_RemoveScene = 5102, - Zo_ResetAlarm = 5114, - Zo_ResetAllAlarms = 5125, - Zo_SWBuildID = 5140, - Zo_Sat = 5150, - Zo_SatMove = 5154, - Zo_SatStep = 5162, - Zo_SceneCount = 5170, - Zo_SceneValid = 5181, - Zo_ScheduleMode = 5192, - Zo_SeaPressure = 5205, - Zo_ShortPollInterval = 5217, - Zo_Shutter = 5235, - Zo_ShutterClose = 5243, - Zo_ShutterLift = 5256, - Zo_ShutterOpen = 5268, - Zo_ShutterStop = 5280, - Zo_ShutterTilt = 5292, - Zo_SoftwareRevision = 5304, - Zo_StackVersion = 5321, - Zo_StandardTime = 5334, - Zo_StartUpOnOff = 5347, - Zo_Status = 5360, - Zo_StoreScene = 5367, - Zo_SwitchType = 5378, - Zo_SystemMode = 5389, - Zo_TRVBoost = 5400, - Zo_TRVChildProtection = 5409, - Zo_TRVMirrorDisplay = 5428, - Zo_TRVMode = 5445, - Zo_TRVWindowOpen = 5453, - Zo_TempTarget = 5467, - Zo_Temperature = 5478, - Zo_TemperatureMaxMeasuredValue = 5490, - Zo_TemperatureMinMeasuredValue = 5518, - Zo_TemperatureTolerance = 5546, - Zo_TerncyDuration = 5567, - Zo_TerncyRotate = 5582, - Zo_ThSetpoint = 5595, - Zo_Time = 5606, - Zo_TimeEpoch = 5611, - Zo_TimeStatus = 5621, - Zo_TimeZone = 5632, - Zo_TotalProfileNum = 5641, - Zo_TuyaAutoLock = 5657, - Zo_TuyaAwayDays = 5670, - Zo_TuyaAwayTemp = 5683, - Zo_TuyaBattery = 5696, - Zo_TuyaBoostTime = 5708, - Zo_TuyaChildLock = 5722, - Zo_TuyaComfortTemp = 5736, - Zo_TuyaEcoTemp = 5752, - Zo_TuyaFanMode = 5764, - Zo_TuyaForceMode = 5776, - Zo_TuyaMaxTemp = 5790, - Zo_TuyaMinTemp = 5802, - Zo_TuyaPreset = 5814, - Zo_TuyaScheduleHolidays = 5825, - Zo_TuyaScheduleWorkdays = 5846, - Zo_TuyaTempTarget = 5867, - Zo_TuyaValveDetection = 5882, - Zo_TuyaValvePosition = 5901, - Zo_TuyaWeekSelect = 5919, - Zo_TuyaWindowDetection = 5934, - Zo_UnoccupiedCoolingSetpoint = 5954, - Zo_UnoccupiedHeatingSetpoint = 5980, - Zo_UtilityName = 6006, - Zo_ValidUntilTime = 6018, - Zo_ValvePosition = 6033, - Zo_VelocityLift = 6047, - Zo_ViewGroup = 6060, - Zo_ViewScene = 6070, - Zo_Water = 6080, - Zo_WhitePointX = 6086, - Zo_WhitePointY = 6098, - Zo_WindowCoveringType = 6110, - Zo_X = 6129, - Zo_Y = 6131, - Zo_ZCLVersion = 6133, - Zo_ZoneState = 6144, - Zo_ZoneStatus = 6154, - Zo_ZoneStatusChange = 6165, - Zo_ZoneType = 6182, - Zo_xx = 6191, - Zo_xx000A00 = 6194, - Zo_xx0A = 6203, - Zo_xx0A00 = 6208, - Zo_xx19 = 6215, - Zo_xx190A = 6220, - Zo_xx190A00 = 6227, - Zo_xxxx = 6236, - Zo_xxxx00 = 6241, - Zo_xxxx0A00 = 6248, - Zo_xxxxyy = 6257, - Zo_xxxxyyyy = 6264, - Zo_xxxxyyyy0A00 = 6273, - Zo_xxxxyyzz = 6286, - Zo_xxyy = 6295, - Zo_xxyy0A00 = 6300, - Zo_xxyyyy = 6309, - Zo_xxyyyy000000000000 = 6316, - Zo_xxyyyy0A0000000000 = 6335, - Zo_xxyyyyzz = 6354, - Zo_xxyyyyzzzz = 6363, - Zo_xxyyzzzz = 6374, + Zo_RGB = 4982, + Zo_RMSCurrent = 4986, + Zo_RMSVoltage = 4997, + Zo_ReactivePower = 5008, + Zo_RecallScene = 5022, + Zo_RemainingTime = 5034, + Zo_RemoteSensing = 5048, + Zo_RemoveAllGroups = 5062, + Zo_RemoveAllScenes = 5078, + Zo_RemoveGroup = 5094, + Zo_RemoveScene = 5106, + Zo_ResetAlarm = 5118, + Zo_ResetAllAlarms = 5129, + Zo_SWBuildID = 5144, + Zo_Sat = 5154, + Zo_SatMove = 5158, + Zo_SatStep = 5166, + Zo_SceneCount = 5174, + Zo_SceneValid = 5185, + Zo_ScheduleMode = 5196, + Zo_SeaPressure = 5209, + Zo_ShortPollInterval = 5221, + Zo_Shutter = 5239, + Zo_ShutterClose = 5247, + Zo_ShutterLift = 5260, + Zo_ShutterOpen = 5272, + Zo_ShutterStop = 5284, + Zo_ShutterTilt = 5296, + Zo_SoftwareRevision = 5308, + Zo_StackVersion = 5325, + Zo_StandardTime = 5338, + Zo_StartUpOnOff = 5351, + Zo_Status = 5364, + Zo_StoreScene = 5371, + Zo_SwitchType = 5382, + Zo_SystemMode = 5393, + Zo_TRVBoost = 5404, + Zo_TRVChildProtection = 5413, + Zo_TRVMirrorDisplay = 5432, + Zo_TRVMode = 5449, + Zo_TRVWindowOpen = 5457, + Zo_TempTarget = 5471, + Zo_Temperature = 5482, + Zo_TemperatureMaxMeasuredValue = 5494, + Zo_TemperatureMinMeasuredValue = 5522, + Zo_TemperatureTolerance = 5550, + Zo_TerncyDuration = 5571, + Zo_TerncyRotate = 5586, + Zo_ThSetpoint = 5599, + Zo_Time = 5610, + Zo_TimeEpoch = 5615, + Zo_TimeStatus = 5625, + Zo_TimeZone = 5636, + Zo_TotalProfileNum = 5645, + Zo_TuyaAutoLock = 5661, + Zo_TuyaAwayDays = 5674, + Zo_TuyaAwayTemp = 5687, + Zo_TuyaBattery = 5700, + Zo_TuyaBoostTime = 5712, + Zo_TuyaChildLock = 5726, + Zo_TuyaComfortTemp = 5740, + Zo_TuyaEcoTemp = 5756, + Zo_TuyaFanMode = 5768, + Zo_TuyaForceMode = 5780, + Zo_TuyaMaxTemp = 5794, + Zo_TuyaMinTemp = 5806, + Zo_TuyaPreset = 5818, + Zo_TuyaScheduleHolidays = 5829, + Zo_TuyaScheduleWorkdays = 5850, + Zo_TuyaTempTarget = 5871, + Zo_TuyaValveDetection = 5886, + Zo_TuyaValvePosition = 5905, + Zo_TuyaWeekSelect = 5923, + Zo_TuyaWindowDetection = 5938, + Zo_UnoccupiedCoolingSetpoint = 5958, + Zo_UnoccupiedHeatingSetpoint = 5984, + Zo_UtilityName = 6010, + Zo_ValidUntilTime = 6022, + Zo_ValvePosition = 6037, + Zo_VelocityLift = 6051, + Zo_ViewGroup = 6064, + Zo_ViewScene = 6074, + Zo_Water = 6084, + Zo_WhitePointX = 6090, + Zo_WhitePointY = 6102, + Zo_WindowCoveringType = 6114, + Zo_X = 6133, + Zo_Y = 6135, + Zo_ZCLVersion = 6137, + Zo_ZoneState = 6148, + Zo_ZoneStatus = 6158, + Zo_ZoneStatusChange = 6169, + Zo_ZoneType = 6186, + Zo_xx = 6195, + Zo_xx000A00 = 6198, + Zo_xx0A = 6207, + Zo_xx0A00 = 6212, + Zo_xx19 = 6219, + Zo_xx190A = 6224, + Zo_xx190A00 = 6231, + Zo_xxxx = 6240, + Zo_xxxx00 = 6245, + Zo_xxxx0A00 = 6252, + Zo_xxxxyy = 6261, + Zo_xxxxyyyy = 6268, + Zo_xxxxyyyy0A00 = 6277, + Zo_xxxxyyzz = 6290, + Zo_xxyy = 6299, + Zo_xxyy0A00 = 6304, + Zo_xxyyyy = 6313, + Zo_xxyyyy000000000000 = 6320, + Zo_xxyyyy0A0000000000 = 6339, + Zo_xxyyyyzz = 6358, + Zo_xxyyyyzzzz = 6367, + Zo_xxyyzzzz = 6378, }; diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index fe4d219c0..28f222c99 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -509,6 +509,7 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { Zuint16, Cx0300, 0x003A, Z_(ColorPointBX), Cm1, 0 }, { Zuint16, Cx0300, 0x003B, Z_(ColorPointBY), Cm1, 0 }, { Zuint8, Cx0300, 0x003C, Z_(ColorPointBIntensity), Cm1, 0 }, + { Zoctstr, Cx0300, 0xFFF0, Z_(RGB), Cm1, 0 }, // synthetic argument to show color as RGB (converted from HueSat or XY) // Illuminance Measurement cluster { Zuint16, Cx0400, 0x0000, Z_(Illuminance), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_PIR, illuminance) }, // Illuminance (in Lux) @@ -1332,7 +1333,38 @@ void ZCLFrame::computeSyntheticAttributes(Z_attribute_list& attr_list) { } } break; - case 0x04030000: // Pressure + case 0x03000000: // Hue + case 0x03000001: // Sat + case 0x03000003: // X + case 0x03000004: // Y + { // generate synthetic RGB + const Z_attribute * attr_rgb = attr_list.findAttribute(0x0300, 0xFFF0); + if (attr_rgb == nullptr) { // make sure we didn't already computed it + uint8_t r,g,b; + bool is_rgb = false; + const Z_attribute * attr_hue = attr_list.findAttribute(0x0300, 0x0000); + const Z_attribute * attr_sat = attr_list.findAttribute(0x0300, 0x0001); + const Z_attribute * attr_x = attr_list.findAttribute(0x0300, 0x0003); + const Z_attribute * attr_y = attr_list.findAttribute(0x0300, 0x0004); + if (attr_hue && attr_sat) { + uint8_t sat = changeUIntScale(attr_sat->getUInt(), 0, 254, 0, 255); + HsToRgb(attr_hue->getUInt(), sat, &r, &g, &b); + is_rgb = true; + } else if (attr_x && attr_y) { + XyToRgb(attr_x->getUInt() / 65535.0f, attr_y->getUInt() / 65535.0f, &r, &g, &b); + is_rgb = true; + } + if (is_rgb) { + SBuffer rgb(3); + rgb.add8(r); + rgb.add8(g); + rgb.add8(b); + attr_list.addAttribute(0x0300, 0xFFF0).setBuf(rgb, 0, 3); + } + } + } + break; + case 0x04030000: // SeaPressure { int16_t pressure = attr.getInt(); int16_t pressure_sealevel = (pressure / FastPrecisePow(1.0 - ((float)Settings.altitude / 44330.0f), 5.255f)) - 21.6f; From 47e23082cc5f5ee430d6e0b97966a008d2a6b8eb Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Sun, 17 Jan 2021 17:01:52 +0100 Subject: [PATCH 011/186] Update changelog --- CHANGELOG.md | 3 ++- RELEASENOTES.md | 2 ++ tasmota/support_button.ino | 18 ++++++++---------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b5e00c4..7cb4ac228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ All notable changes to this project will be documented in this file. - ESP8266 Support for 2MB and up linker files with 1MB and up LittleFS - ESP32 support for TLS MQTT using BearSSL (same as ESP8266) - Support for 24/26/32/34 bit RFID Wiegand interface (D0/D1) by Sigurd Leuther (#3647) -- Added ``USE_MQTT_TLS_DROP_OLD_FINGERPRINT`` compile time option to drop old (less secure) TLS fingerprint +- Compile time option ``USE_MQTT_TLS_DROP_OLD_FINGERPRINT`` to drop old (less secure) TLS fingerprint +- Command ``SetOption40 0..250`` to disable button functionality if activated for over 0.1 second re-introduced ### Breaking Changed - ESP32 switch from default SPIFFS to default LittleFS file system loosing current (zigbee) files diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 136d679f3..9c1e88528 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -61,6 +61,7 @@ The attached binaries can also be downloaded from http://ota.tasmota.com/tasmota - Command ``CTRange`` to specify the visible CT range the bulb is capable of [#10311](https://github.com/arendst/Tasmota/issues/10311) - Command ``RuleTimer0`` to access all RuleTimers at once [#10352](https://github.com/arendst/Tasmota/issues/10352) - Command ``VirtualCT`` to simulate or fine tune CT bulbs with 3,4,5 channels [#10311](https://github.com/arendst/Tasmota/issues/10311) +- Command ``SetOption40 0..250`` to disable button functionality if activated for over 0.1 second re-introduced - Command ``SetOption43 1..255`` to control Rotary step (#10407) - Command ``SetOption118 1`` to move ZbReceived from JSON message and into the subtopic replacing "SENSOR" default [#10353](https://github.com/arendst/Tasmota/issues/10353) - Command ``SetOption119 1`` to remove the device addr from json payload, can be used with zb_topic_fname where the addr is already known from the topic [#10355](https://github.com/arendst/Tasmota/issues/10355) @@ -83,6 +84,7 @@ The attached binaries can also be downloaded from http://ota.tasmota.com/tasmota - Support character `#` to be replaced by `space`-character in command ``Publish`` topic [#10258](https://github.com/arendst/Tasmota/issues/10258) - Basic support for ESP32 Odroid Go 16MB binary tasmota32-odroidgo.bin [#8630](https://github.com/arendst/Tasmota/issues/8630) - SPI display driver SSD1331 Color oled by Jeroen Vermeulen [#10376](https://github.com/arendst/Tasmota/issues/10376) +- Compile time option ``USE_MQTT_TLS_DROP_OLD_FINGERPRINT`` to drop old (less secure) TLS fingerprint ### Breaking Changed - ESP32 switch from default SPIFFS to default LittleFS file system loosing current (zigbee) files diff --git a/tasmota/support_button.ino b/tasmota/support_button.ino index 473183665..f1bd22c0f 100644 --- a/tasmota/support_button.ino +++ b/tasmota/support_button.ino @@ -263,22 +263,20 @@ void ButtonHandler(void) { SendKey(KEY_BUTTON, button_index +1, POWER_HOLD); // Execute Hold command via MQTT if ButtonTopic is set } } else { - if (!Settings.flag.button_restrict) { // SetOption1 - Control button multipress + if (Settings.flag.button_restrict) { // SetOption1 (0) - Control button multipress + if (Settings.param[P_HOLD_IGNORE] > 0) { // SetOption40 (0) - Do not ignore button hold + if (Button.hold_timer[button_index] > loops_per_second * Settings.param[P_HOLD_IGNORE] / 10) { + Button.hold_timer[button_index] = 0; // Reset button hold counter to stay below hold trigger + Button.press_counter[button_index] = 0; // Discard button press to disable functionality + } + } + } else { if ((Button.hold_timer[button_index] == loops_per_second * hold_time_extent * Settings.param[P_HOLD_TIME] / 10)) { // SetOption32 (40) - Button held for factor times longer Button.press_counter[button_index] = 0; snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_RESET " 1")); ExecuteCommand(scmnd, SRC_BUTTON); } } - else - { - if (Settings.param[P_HOLD_IGNORE] > 0) { // SetOption40 (0) - Do not ignore button hold - if (Button.hold_timer[button_index] > loops_per_second * Settings.param[P_HOLD_IGNORE] / 10) { - Button.hold_timer[button_index] = 0; // Reset button hold counter to stay below hold trigger - Button.press_counter[button_index] = 0; // Discard button press to disable functionality - } - } - } } } } From d402060e5616e7521195e978cfb5d12553be923f Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Sun, 17 Jan 2021 17:50:58 +0100 Subject: [PATCH 012/186] Clean up Wiegand --- tasmota/xsns_82_wiegand.ino | 276 ++++++++++++++++++------------------ 1 file changed, 135 insertions(+), 141 deletions(-) diff --git a/tasmota/xsns_82_wiegand.ino b/tasmota/xsns_82_wiegand.ino index 59505a5fc..ca707eaf7 100644 --- a/tasmota/xsns_82_wiegand.ino +++ b/tasmota/xsns_82_wiegand.ino @@ -30,21 +30,21 @@ #define XSNS_82 82 #define WIEGAND_BIT_TIMEOUT 25 //time to be wait after last bit detected. -// use only a randomly generate RFID for testing. using #define will save some space in the final code + +// Use only a randomly generate RFID for testing. using #define will save some space in the final code // DEV_WIEGAND_TEST_MODE 1 : testing with random rfid without hardware connected, but GPIOs set correctly // DEV_WIEGAND_TEST_MODE 2 : testing with hardware corretly connected. -// #define DEV_WIEGAND_TEST_MODE 0 #ifdef DEV_WIEGAND_TEST_MODE - #if (DEV_WIEGAND_TEST_MODE==0) - #elif (DEV_WIEGAND_TEST_MODE==1) - #warning "Wiegand Interface compiled with 'DEV_WIEGAND_TEST_MODE' 1 (Random RFID)" - #elif (DEV_WIEGAND_TEST_MODE==2) - #warning "Wiegand Interface compiled with 'DEV_WIEGAND_TEST_MODE' 2 (Hardware connected)" - #else - #warning "Wiegand Interface compiled with unknown mode" - #endif + #if (DEV_WIEGAND_TEST_MODE==0) + #elif (DEV_WIEGAND_TEST_MODE==1) + #warning "Wiegand Interface compiled with 'DEV_WIEGAND_TEST_MODE' 1 (Random RFID)" + #elif (DEV_WIEGAND_TEST_MODE==2) + #warning "Wiegand Interface compiled with 'DEV_WIEGAND_TEST_MODE' 2 (Hardware connected)" + #else + #warning "Wiegand Interface compiled with unknown mode" + #endif #endif class Wiegand { @@ -96,16 +96,16 @@ Wiegand::Wiegand() { rfidBuffer = 0; bitCount = 0 ; timeOut = 0; - isInit= false; + isInit = false; } #if (DEV_WIEGAND_TEST_MODE)==1 uint64_t Wiegand::GetRandomRfid(uint8_t tag_size=34) { - //todo add support for 4 and 8 bit keyboard "tags" + // Todo add support for 4 and 8 bit keyboard "tags" uint64_t result = (uint32_t)HwRandom(); uint8_t parities = 0; bitCount = tag_size; - timeOut=millis() - WIEGAND_BIT_TIMEOUT; + timeOut = millis() - WIEGAND_BIT_TIMEOUT; result = result << 32; result += HwRandom(); @@ -127,8 +127,8 @@ uint64_t Wiegand::GetRandomRfid(uint8_t tag_size=34) { } parities = CalculateParities(result, tag_size); - result = (result << 1) | (parities & 0x01); //set LSB parity - if (parities & 0x80) { //MSB parity is 1 + result = (result << 1) | (parities & 0x01); // Set LSB parity + if (parities & 0x80) { // MSB parity is 1 switch (tag_size) { case 24: result |= 0x800000; @@ -151,47 +151,47 @@ uint64_t Wiegand::GetRandomRfid(uint8_t tag_size=34) { } #endif -void ICACHE_RAM_ATTR Wiegand::handleD1Interrupt() { // receive a 1 bit. (D0=high & D1=low) - rfidBuffer = (rfidBuffer << 1) | 1; // leftshift + 1 bit - bitCount++; //increment the counter - lastFoundTime = millis(); // last time bit found +void ICACHE_RAM_ATTR Wiegand::handleD1Interrupt() { // Receive a 1 bit. (D0=high & D1=low) + rfidBuffer = (rfidBuffer << 1) | 1; // Leftshift + 1 bit + bitCount++; // Increment the counter + lastFoundTime = millis(); // Last time bit found } -void ICACHE_RAM_ATTR Wiegand::handleD0Interrupt() { // receive a 0 bit. (D0=low & D1=high) - rfidBuffer = rfidBuffer << 1; // leftshift the 0 bit is now at the end of rfidBuffer - bitCount++; //increment the counter - lastFoundTime = millis(); //last time bit found +void ICACHE_RAM_ATTR Wiegand::handleD0Interrupt() { // Receive a 0 bit. (D0=low & D1=high) + rfidBuffer = rfidBuffer << 1; // Leftshift the 0 bit is now at the end of rfidBuffer + bitCount++; // Increment the counter + lastFoundTime = millis(); // Last time bit found } void Wiegand::Init() { isInit = false; - if (PinUsed(GPIO_WIEGAND_D0) && PinUsed(GPIO_WIEGAND_D1)) { //only start, if the Wiegang pins are - #if (DEV_WIEGAND_TEST_MODE)>0 - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: Init()")); - #endif + if (PinUsed(GPIO_WIEGAND_D0) && PinUsed(GPIO_WIEGAND_D1)) { // Only start, if the Wiegang pins are +#if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: Init()")); +#endif pinMode(Pin(GPIO_WIEGAND_D0), INPUT_PULLUP); pinMode(Pin(GPIO_WIEGAND_D1), INPUT_PULLUP); - attachInterrupt(Pin(GPIO_WIEGAND_D0), handleD0Interrupt, FALLING); - attachInterrupt(Pin(GPIO_WIEGAND_D1), handleD1Interrupt, FALLING); - isInit = true; // helps to run only if correctly setup - #if (DEV_WIEGAND_TEST_MODE)>0 - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: Testmode")); // for tests without reader attaiched - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: D0:%u"),Pin(GPIO_WIEGAND_D0)); - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: D1:%u"),Pin(GPIO_WIEGAND_D1)); - #else + attachInterrupt(Pin(GPIO_WIEGAND_D0), handleD0Interrupt, FALLING); + attachInterrupt(Pin(GPIO_WIEGAND_D1), handleD1Interrupt, FALLING); + isInit = true; // Helps to run only if correctly setup +#if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: Testmode")); // For tests without reader attaiched + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: D0:%u"),Pin(GPIO_WIEGAND_D0)); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: D1:%u"),Pin(GPIO_WIEGAND_D1)); +#else AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: D0=%u, D1=%u"),Pin(GPIO_WIEGAND_D0), Pin(GPIO_WIEGAND_D1)); - #endif +#endif } - #if (DEV_WIEGAND_TEST_MODE)>0 +#if (DEV_WIEGAND_TEST_MODE)>0 else { AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: no GPIOs.")); } - #endif +#endif } uint64_t Wiegand::CheckAndConvertRfid(uint64_t rfidIn, uint16_t bitcount) { uint8_t evenParityBit = 0; - uint8_t oddParityBit = (uint8_t) (rfidIn & 0x1); // last bit = odd parity + uint8_t oddParityBit = (uint8_t) (rfidIn & 0x1); // Last bit = odd parity uint8_t calcParity = 0; switch (bitcount) { case 24: @@ -217,42 +217,42 @@ uint64_t Wiegand::CheckAndConvertRfid(uint64_t rfidIn, uint16_t bitcount) { default: break; } - calcParity = CalculateParities(rfidIn, bitCount); //ckeck result on http://www.ccdesignworks.com/wiegand_calc.htm with raw tag as input - if (calcParity != (evenParityBit | oddParityBit)) { // Paritybit is wrong + calcParity = CalculateParities(rfidIn, bitCount); // check result on http://www.ccdesignworks.com/wiegand_calc.htm with raw tag as input + if (calcParity != (evenParityBit | oddParityBit)) { // Paritybit is wrong rfidIn=0; AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: %llu parity error"), rfidIn); } - #if (DEV_WIEGAND_TEST_MODE)>0 - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: even (left) parity: %u "), (evenParityBit>>7)); - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: even (calc) parity: %u "), (calcParity & 0x80)>>7); - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: odd (right) parity: %u "), oddParityBit); - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: odd (calc) parity: %u "), (calcParity & 0x01)); - #endif +#if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: even (left) parity: %u "), (evenParityBit>>7)); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: even (calc) parity: %u "), (calcParity & 0x80)>>7); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: odd (right) parity: %u "), oddParityBit); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: odd (calc) parity: %u "), (calcParity & 0x01)); +#endif return rfidIn; } uint8_t Wiegand::CalculateParities(uint64_t tagWithoutParities, int tag_size=26) { - //tag_size is the size of the final tag including the 2 parity bits - //so length if the tagWithoutParities should be (tag_size-2) !! That will be not profed and - //lead to wrong results if the input value is larger! - //calculated start parity (even) will be returned as bit 8 - //calculated end parity (odd) will be returned as bit 1 + // tag_size is the size of the final tag including the 2 parity bits + // So length if the tagWithoutParities should be (tag_size-2) !! That will be not profed and + // lead to wrong results if the input value is larger! + // Calculated start parity (even) will be returned as bit 8 + // calculated end parity (odd) will be returned as bit 1 uint8_t retValue=0; tag_size -= 2; - if (tag_size<=0) { return retValue; } //prohibit div zero exception and other wrong inputs - uint8_t parity=1; //check for odd parity on LSB - for (uint8_t i=0; i<(tag_size/2); i++) { - parity^=(tagWithoutParities & 1); - tagWithoutParities>>=1; - } + if (tag_size <= 0) { return retValue; } // Prohibit div zero exception and other wrong inputs + uint8_t parity = 1; // Check for odd parity on LSB + for (uint8_t i = 0; i < (tag_size / 2); i++) { + parity ^= (tagWithoutParities & 1); + tagWithoutParities >>= 1; + } retValue |= parity; - parity=0; //check for even parity on MSB + parity = 0; // Check for even parity on MSB while (tagWithoutParities) { - parity^=(tagWithoutParities & 1); - tagWithoutParities>>=1; - } - retValue |= (parity<<7); + parity ^= (tagWithoutParities & 1); + tagWithoutParities >>= 1; + } + retValue |= (parity << 7); return retValue; } @@ -260,10 +260,10 @@ uint8_t Wiegand::CalculateParities(uint64_t tagWithoutParities, int tag_size=26) char Wiegand::translateEnterEscapeKeyPress(char oKeyPressed) { switch(oKeyPressed) { case 0x0b: // 11 or * key - return 0x0d; // 13 or ASCII ENTER + return 0x0d; // 13 or ASCII ENTER case 0x0a: // 10 or # key - return 0x1b; // 27 or ASCII ESCAPE + return 0x1b; // 27 or ASCII ESCAPE default: return oKeyPressed; @@ -274,19 +274,19 @@ bool Wiegand::WiegandConversion () { bool bRet = false; unsigned long nowTick = millis(); -//add a maximum wait time for new bits + // Add a maximum wait time for new bits unsigned long diffTicks = nowTick - lastFoundTime; - if ((diffTicks > WIEGAND_BIT_TIMEOUT) && (diffTicks >= 5000 )) { //max. 5 secs between 2 bits comming in - bitCount=0; - rfidBuffer=0; - lastFoundTime=nowTick; + if ((diffTicks > WIEGAND_BIT_TIMEOUT) && (diffTicks >= 5000 )) { // Max. 5 secs between 2 bits comming in + bitCount = 0; + rfidBuffer = 0; + lastFoundTime = nowTick; return bRet; - } - if (diffTicks > WIEGAND_BIT_TIMEOUT) { //last bit found is WIEGAND_BIT_TIMEOUT ms ago - #if (DEV_WIEGAND_TEST_MODE)>0 - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: raw tag: %llu "), rfidBuffer); - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: bit count: %u "), bitCount); - #endif + } + if (diffTicks > WIEGAND_BIT_TIMEOUT) { // Last bit found is WIEGAND_BIT_TIMEOUT ms ago +#if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: raw tag: %llu "), rfidBuffer); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: bit count: %u "), bitCount); +#endif if ((bitCount==4)||(bitCount==8)||(bitCount==24)||(bitCount==26)||(bitCount==32)||(bitCount==34)) { if ((bitCount==24)||(bitCount==26)||(bitCount==32)||(bitCount==34)) { // 24,26,32,34-bit Wiegand codes @@ -310,79 +310,74 @@ bool Wiegand::WiegandConversion () // eg if key 1 pressed, data=E1 in binary 11100001 , high nibble=1110 , low nibble = 0001 char highNibble = (rfidBuffer & 0xf0) >>4; char lowNibble = (rfidBuffer & 0x0f); - if (lowNibble == (~highNibble & 0x0f)) // check if low nibble matches the "NOT" of high nibble. - { + if (lowNibble == (~highNibble & 0x0f)) { // Check if low nibble matches the "NOT" of high nibble. rfid = (int)translateEnterEscapeKeyPress(lowNibble); - bRet=true; + bRet = true; + } else { + lastFoundTime = nowTick; + bRet = false; } - else { - lastFoundTime=nowTick; - bRet=false; - } - tagSize=bitCount; - bitCount=0; - rfidBuffer=0; + tagSize = bitCount; + bitCount = 0; + rfidBuffer = 0; } + } else { + // Time reached but unknown bitCount, clear and start again + lastFoundTime = nowTick; + bitCount = 0; + rfidBuffer = 0; + bRet = false; } - else { - // time reached but unknown bitCount, clear and start again - lastFoundTime=nowTick; - bitCount=0; - rfidBuffer=0; - bRet=false; - } + } else { + bRet = false; // watching time not finished } - else{ - bRet=false; // watching time not finished - } - #if (DEV_WIEGAND_TEST_MODE)>0 - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: tag out: %llu "), rfid); - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: tag size: %u"), tagSize); - #endif +#if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: tag out: %llu "), rfid); + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: tag size: %u"), tagSize); +#endif return bRet; } void Wiegand::ScanForTag() { - if (!isInit) { return;} - #if (DEV_WIEGAND_TEST_MODE)>0 - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: ScanForTag().")); - #if (DEV_WIEGAND_TEST_MODE==1) - switch (millis() %4 ) { - case 0: - rfidBuffer = GetRandomRfid(24); - break; - case 1: - rfidBuffer = GetRandomRfid(26); - break; - case 2: - rfidBuffer = GetRandomRfid(32); - break; - case 3: - rfidBuffer = GetRandomRfid(34); - break; - default: - rfidBuffer = GetRandomRfid(34); - break; - } - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: raw generated: %lX"), rfidBuffer); // for tests without reader attaiched - #endif - #endif +#if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: ScanForTag().")); +#if (DEV_WIEGAND_TEST_MODE==1) + switch (millis() %4 ) { + case 0: + rfidBuffer = GetRandomRfid(24); + break; + case 1: + rfidBuffer = GetRandomRfid(26); + break; + case 2: + rfidBuffer = GetRandomRfid(32); + break; + case 3: + rfidBuffer = GetRandomRfid(34); + break; + default: + rfidBuffer = GetRandomRfid(34); + break; + } + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: raw generated: %lX"), rfidBuffer); // for tests without reader attaiched +#endif +#endif if (bitCount > 0) { uint64_t oldTag = rfid; bool validKey = WiegandConversion(); - #if (DEV_WIEGAND_TEST_MODE)>0 - AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: previous tag: %llu"), oldTag); - #endif +#if (DEV_WIEGAND_TEST_MODE)>0 + AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: previous tag: %llu"), oldTag); +#endif // only in case of valid key do action. Issue#10585 if(validKey) { if (oldTag != rfid) { AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: new= %llu"), rfid); } else { AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: prev= %llu"), rfid); } AddLog_P(LOG_LEVEL_INFO, PSTR("WIE: bits= %u"), tagSize); ResponseTime_P(PSTR(",\"Wiegand\":{\"UID\":\"%0llu\"}}"), rfid); - MqttPublishTeleSensor(); - } - } + MqttPublishTeleSensor(); + } + } } #ifdef USE_WEBSERVER @@ -410,27 +405,26 @@ bool Xsns82(byte function) { scanDelay = 1; break; - case FUNC_EVERY_250_MSECOND: // some tags need more time, don't try shorter period - #if (DEV_WIEGAND_TEST_MODE)==1 - if (scanDelay>=4) // give a second because of the log entries to be send. - #else - if (scanDelay>=2) // only run every (delay * 250 ms) (every 250ms is too fast for some tags) - #endif + case FUNC_EVERY_250_MSECOND: // Some tags need more time, don't try shorter period +#if (DEV_WIEGAND_TEST_MODE)==1 + if (scanDelay >= 4) // Give a second because of the log entries to be send. +#else + if (scanDelay >= 2) // Only run every (delay * 250 ms) (every 250ms is too fast for some tags) +#endif { oWiegand->ScanForTag(); scanDelay = 1; - } - else { + } else { scanDelay++; } break; - - #ifdef USE_WEBSERVER +#ifdef USE_WEBSERVER case FUNC_WEB_SENSOR: oWiegand->Show(); break; - #endif // USE_WEBSERVER +#endif // USE_WEBSERVER } return result; } + #endif // USE_WIEGAND \ No newline at end of file From 06221f3ef1f64d086b6473bbf482d87fdfb4fe7d Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Sun, 17 Jan 2021 17:52:08 +0100 Subject: [PATCH 013/186] Zigbee add RGBb taking into account brightness --- tasmota/xdrv_23_zigbee_5__constants.ino | 230 ++++++++++++------------ tasmota/xdrv_23_zigbee_5_converters.ino | 19 ++ 2 files changed, 135 insertions(+), 114 deletions(-) diff --git a/tasmota/xdrv_23_zigbee_5__constants.ino b/tasmota/xdrv_23_zigbee_5__constants.ino index 4176c7ee4..fc8b95661 100644 --- a/tasmota/xdrv_23_zigbee_5__constants.ino +++ b/tasmota/xdrv_23_zigbee_5__constants.ino @@ -381,6 +381,7 @@ const char Z_strings[] PROGMEM = "ProductURL" "\x00" "QualityMeasure" "\x00" "RGB" "\x00" + "RGBb" "\x00" "RMSCurrent" "\x00" "RMSVoltage" "\x00" "ReactivePower" "\x00" @@ -804,120 +805,121 @@ enum Z_offsets { Zo_ProductURL = 4956, Zo_QualityMeasure = 4967, Zo_RGB = 4982, - Zo_RMSCurrent = 4986, - Zo_RMSVoltage = 4997, - Zo_ReactivePower = 5008, - Zo_RecallScene = 5022, - Zo_RemainingTime = 5034, - Zo_RemoteSensing = 5048, - Zo_RemoveAllGroups = 5062, - Zo_RemoveAllScenes = 5078, - Zo_RemoveGroup = 5094, - Zo_RemoveScene = 5106, - Zo_ResetAlarm = 5118, - Zo_ResetAllAlarms = 5129, - Zo_SWBuildID = 5144, - Zo_Sat = 5154, - Zo_SatMove = 5158, - Zo_SatStep = 5166, - Zo_SceneCount = 5174, - Zo_SceneValid = 5185, - Zo_ScheduleMode = 5196, - Zo_SeaPressure = 5209, - Zo_ShortPollInterval = 5221, - Zo_Shutter = 5239, - Zo_ShutterClose = 5247, - Zo_ShutterLift = 5260, - Zo_ShutterOpen = 5272, - Zo_ShutterStop = 5284, - Zo_ShutterTilt = 5296, - Zo_SoftwareRevision = 5308, - Zo_StackVersion = 5325, - Zo_StandardTime = 5338, - Zo_StartUpOnOff = 5351, - Zo_Status = 5364, - Zo_StoreScene = 5371, - Zo_SwitchType = 5382, - Zo_SystemMode = 5393, - Zo_TRVBoost = 5404, - Zo_TRVChildProtection = 5413, - Zo_TRVMirrorDisplay = 5432, - Zo_TRVMode = 5449, - Zo_TRVWindowOpen = 5457, - Zo_TempTarget = 5471, - Zo_Temperature = 5482, - Zo_TemperatureMaxMeasuredValue = 5494, - Zo_TemperatureMinMeasuredValue = 5522, - Zo_TemperatureTolerance = 5550, - Zo_TerncyDuration = 5571, - Zo_TerncyRotate = 5586, - Zo_ThSetpoint = 5599, - Zo_Time = 5610, - Zo_TimeEpoch = 5615, - Zo_TimeStatus = 5625, - Zo_TimeZone = 5636, - Zo_TotalProfileNum = 5645, - Zo_TuyaAutoLock = 5661, - Zo_TuyaAwayDays = 5674, - Zo_TuyaAwayTemp = 5687, - Zo_TuyaBattery = 5700, - Zo_TuyaBoostTime = 5712, - Zo_TuyaChildLock = 5726, - Zo_TuyaComfortTemp = 5740, - Zo_TuyaEcoTemp = 5756, - Zo_TuyaFanMode = 5768, - Zo_TuyaForceMode = 5780, - Zo_TuyaMaxTemp = 5794, - Zo_TuyaMinTemp = 5806, - Zo_TuyaPreset = 5818, - Zo_TuyaScheduleHolidays = 5829, - Zo_TuyaScheduleWorkdays = 5850, - Zo_TuyaTempTarget = 5871, - Zo_TuyaValveDetection = 5886, - Zo_TuyaValvePosition = 5905, - Zo_TuyaWeekSelect = 5923, - Zo_TuyaWindowDetection = 5938, - Zo_UnoccupiedCoolingSetpoint = 5958, - Zo_UnoccupiedHeatingSetpoint = 5984, - Zo_UtilityName = 6010, - Zo_ValidUntilTime = 6022, - Zo_ValvePosition = 6037, - Zo_VelocityLift = 6051, - Zo_ViewGroup = 6064, - Zo_ViewScene = 6074, - Zo_Water = 6084, - Zo_WhitePointX = 6090, - Zo_WhitePointY = 6102, - Zo_WindowCoveringType = 6114, - Zo_X = 6133, - Zo_Y = 6135, - Zo_ZCLVersion = 6137, - Zo_ZoneState = 6148, - Zo_ZoneStatus = 6158, - Zo_ZoneStatusChange = 6169, - Zo_ZoneType = 6186, - Zo_xx = 6195, - Zo_xx000A00 = 6198, - Zo_xx0A = 6207, - Zo_xx0A00 = 6212, - Zo_xx19 = 6219, - Zo_xx190A = 6224, - Zo_xx190A00 = 6231, - Zo_xxxx = 6240, - Zo_xxxx00 = 6245, - Zo_xxxx0A00 = 6252, - Zo_xxxxyy = 6261, - Zo_xxxxyyyy = 6268, - Zo_xxxxyyyy0A00 = 6277, - Zo_xxxxyyzz = 6290, - Zo_xxyy = 6299, - Zo_xxyy0A00 = 6304, - Zo_xxyyyy = 6313, - Zo_xxyyyy000000000000 = 6320, - Zo_xxyyyy0A0000000000 = 6339, - Zo_xxyyyyzz = 6358, - Zo_xxyyyyzzzz = 6367, - Zo_xxyyzzzz = 6378, + Zo_RGBb = 4986, + Zo_RMSCurrent = 4991, + Zo_RMSVoltage = 5002, + Zo_ReactivePower = 5013, + Zo_RecallScene = 5027, + Zo_RemainingTime = 5039, + Zo_RemoteSensing = 5053, + Zo_RemoveAllGroups = 5067, + Zo_RemoveAllScenes = 5083, + Zo_RemoveGroup = 5099, + Zo_RemoveScene = 5111, + Zo_ResetAlarm = 5123, + Zo_ResetAllAlarms = 5134, + Zo_SWBuildID = 5149, + Zo_Sat = 5159, + Zo_SatMove = 5163, + Zo_SatStep = 5171, + Zo_SceneCount = 5179, + Zo_SceneValid = 5190, + Zo_ScheduleMode = 5201, + Zo_SeaPressure = 5214, + Zo_ShortPollInterval = 5226, + Zo_Shutter = 5244, + Zo_ShutterClose = 5252, + Zo_ShutterLift = 5265, + Zo_ShutterOpen = 5277, + Zo_ShutterStop = 5289, + Zo_ShutterTilt = 5301, + Zo_SoftwareRevision = 5313, + Zo_StackVersion = 5330, + Zo_StandardTime = 5343, + Zo_StartUpOnOff = 5356, + Zo_Status = 5369, + Zo_StoreScene = 5376, + Zo_SwitchType = 5387, + Zo_SystemMode = 5398, + Zo_TRVBoost = 5409, + Zo_TRVChildProtection = 5418, + Zo_TRVMirrorDisplay = 5437, + Zo_TRVMode = 5454, + Zo_TRVWindowOpen = 5462, + Zo_TempTarget = 5476, + Zo_Temperature = 5487, + Zo_TemperatureMaxMeasuredValue = 5499, + Zo_TemperatureMinMeasuredValue = 5527, + Zo_TemperatureTolerance = 5555, + Zo_TerncyDuration = 5576, + Zo_TerncyRotate = 5591, + Zo_ThSetpoint = 5604, + Zo_Time = 5615, + Zo_TimeEpoch = 5620, + Zo_TimeStatus = 5630, + Zo_TimeZone = 5641, + Zo_TotalProfileNum = 5650, + Zo_TuyaAutoLock = 5666, + Zo_TuyaAwayDays = 5679, + Zo_TuyaAwayTemp = 5692, + Zo_TuyaBattery = 5705, + Zo_TuyaBoostTime = 5717, + Zo_TuyaChildLock = 5731, + Zo_TuyaComfortTemp = 5745, + Zo_TuyaEcoTemp = 5761, + Zo_TuyaFanMode = 5773, + Zo_TuyaForceMode = 5785, + Zo_TuyaMaxTemp = 5799, + Zo_TuyaMinTemp = 5811, + Zo_TuyaPreset = 5823, + Zo_TuyaScheduleHolidays = 5834, + Zo_TuyaScheduleWorkdays = 5855, + Zo_TuyaTempTarget = 5876, + Zo_TuyaValveDetection = 5891, + Zo_TuyaValvePosition = 5910, + Zo_TuyaWeekSelect = 5928, + Zo_TuyaWindowDetection = 5943, + Zo_UnoccupiedCoolingSetpoint = 5963, + Zo_UnoccupiedHeatingSetpoint = 5989, + Zo_UtilityName = 6015, + Zo_ValidUntilTime = 6027, + Zo_ValvePosition = 6042, + Zo_VelocityLift = 6056, + Zo_ViewGroup = 6069, + Zo_ViewScene = 6079, + Zo_Water = 6089, + Zo_WhitePointX = 6095, + Zo_WhitePointY = 6107, + Zo_WindowCoveringType = 6119, + Zo_X = 6138, + Zo_Y = 6140, + Zo_ZCLVersion = 6142, + Zo_ZoneState = 6153, + Zo_ZoneStatus = 6163, + Zo_ZoneStatusChange = 6174, + Zo_ZoneType = 6191, + Zo_xx = 6200, + Zo_xx000A00 = 6203, + Zo_xx0A = 6212, + Zo_xx0A00 = 6217, + Zo_xx19 = 6224, + Zo_xx190A = 6229, + Zo_xx190A00 = 6236, + Zo_xxxx = 6245, + Zo_xxxx00 = 6250, + Zo_xxxx0A00 = 6257, + Zo_xxxxyy = 6266, + Zo_xxxxyyyy = 6273, + Zo_xxxxyyyy0A00 = 6282, + Zo_xxxxyyzz = 6295, + Zo_xxyy = 6304, + Zo_xxyy0A00 = 6309, + Zo_xxyyyy = 6318, + Zo_xxyyyy000000000000 = 6325, + Zo_xxyyyy0A0000000000 = 6344, + Zo_xxyyyyzz = 6363, + Zo_xxyyyyzzzz = 6372, + Zo_xxyyzzzz = 6383, }; diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index 28f222c99..1f47204a0 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -510,6 +510,7 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { Zuint16, Cx0300, 0x003B, Z_(ColorPointBY), Cm1, 0 }, { Zuint8, Cx0300, 0x003C, Z_(ColorPointBIntensity), Cm1, 0 }, { Zoctstr, Cx0300, 0xFFF0, Z_(RGB), Cm1, 0 }, // synthetic argument to show color as RGB (converted from HueSat or XY) + { Zoctstr, Cx0300, 0xFFF1, Z_(RGBb), Cm1, 0 }, // synthetic argument to show color as RGB including last known brightness // Illuminance Measurement cluster { Zuint16, Cx0400, 0x0000, Z_(Illuminance), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_PIR, illuminance) }, // Illuminance (in Lux) @@ -1287,6 +1288,7 @@ void ZCLFrame::removeInvalidAttributes(Z_attribute_list& attr_list) { // Note: both function are now split to compute on extracted attributes // void ZCLFrame::computeSyntheticAttributes(Z_attribute_list& attr_list) { + const Z_Device & device = zigbee_devices.findShortAddr(_srcaddr); const char * model_c = zigbee_devices.getModelId(_srcaddr); // null if unknown String modelId((char*) model_c); // scan through attributes and apply specific converters @@ -1360,6 +1362,23 @@ void ZCLFrame::computeSyntheticAttributes(Z_attribute_list& attr_list) { rgb.add8(g); rgb.add8(b); attr_list.addAttribute(0x0300, 0xFFF0).setBuf(rgb, 0, 3); + + // do we know ZbData for this bulb + uint8_t brightness = 255; + if (device.valid()) { + const Z_Data_Light & light = device.data.find(_srcendpoint); + if (light.validDimmer()) { + // Dimmer has a valid value + brightness = changeUIntScale(light.getDimmer(), 0, 254, 0, 255); // range is 0..255 + } + } + r = changeUIntScale(r, 0, 255, 0, brightness); + g = changeUIntScale(g, 0, 255, 0, brightness); + b = changeUIntScale(b, 0, 255, 0, brightness); + rgb.set8(0, r); + rgb.set8(1, g); + rgb.set8(2, b); + attr_list.addAttribute(0x0300, 0xFFF1).setBuf(rgb, 0, 3); } } } From e089fae11a001b47712549ee3ab29a1381547ddb Mon Sep 17 00:00:00 2001 From: Simon Hailes Date: Sun, 17 Jan 2021 17:05:10 +0000 Subject: [PATCH 014/186] update NimBLE to 5dc72ab10d9f928442a25ef3bdcf8a31a7e16301 --- lib/libesp32/NimBLE-Arduino/CHANGELOG.md | 69 ++++ lib/libesp32/NimBLE-Arduino/README.md | 4 +- .../docs/Command_line_config.md | 93 +++++ .../NimBLE_Secure_Client.ino | 91 +++++ .../NimBLE_Secure_Server.ino | 37 ++ .../BLE_notify/BLE_notify.ino | 6 +- .../BLE_server/BLE_server.ino | 7 +- .../BLE_server_multiconnect.ino | 6 +- lib/libesp32/NimBLE-Arduino/src/FreeRTOS.cpp | 10 +- lib/libesp32/NimBLE-Arduino/src/FreeRTOS.h | 6 +- .../NimBLE-Arduino/src/NimBLE2904.cpp | 2 +- .../NimBLE-Arduino/src/NimBLEAdvertising.cpp | 289 ++++++++++++---- .../NimBLE-Arduino/src/NimBLEAdvertising.h | 11 +- .../src/NimBLECharacteristic.cpp | 3 +- .../NimBLE-Arduino/src/NimBLEClient.cpp | 318 ++++++++++++------ .../NimBLE-Arduino/src/NimBLEClient.h | 11 +- .../NimBLE-Arduino/src/NimBLEDevice.cpp | 94 ++++-- .../NimBLE-Arduino/src/NimBLEDevice.h | 2 + .../NimBLE-Arduino/src/NimBLEHIDDevice.cpp | 233 +++++++++++++ .../NimBLE-Arduino/src/NimBLEHIDDevice.h | 85 +++++ .../src/NimBLERemoteCharacteristic.cpp | 46 ++- .../src/NimBLERemoteDescriptor.cpp | 4 +- .../src/NimBLERemoteService.cpp | 34 +- .../NimBLE-Arduino/src/NimBLERemoteService.h | 1 + .../NimBLE-Arduino/src/NimBLEScan.cpp | 103 +++--- lib/libesp32/NimBLE-Arduino/src/NimBLEScan.h | 4 +- .../NimBLE-Arduino/src/NimBLEServer.cpp | 5 + .../NimBLE-Arduino/src/NimBLEServer.h | 9 + .../NimBLE-Arduino/src/NimBLEUUID.cpp | 31 ++ .../src/esp-hci/src/esp_nimble_hci.c | 97 ++++-- .../NimBLE-Arduino/src/esp_nimble_cfg.h | 4 + .../src/nimble/host/src/ble_eddystone.c | 2 +- .../src/nimble/host/src/ble_gap.c | 6 +- .../src/nimble/host/src/ble_hs_conn.c | 106 +++--- .../host/store/config/src/ble_store_nvs.c | 42 ++- lib/libesp32/NimBLE-Arduino/src/nimconfig.h | 44 ++- 36 files changed, 1537 insertions(+), 378 deletions(-) create mode 100644 lib/libesp32/NimBLE-Arduino/docs/Command_line_config.md create mode 100644 lib/libesp32/NimBLE-Arduino/examples/NimBLE_Secure_Client/NimBLE_Secure_Client.ino create mode 100644 lib/libesp32/NimBLE-Arduino/examples/NimBLE_Secure_Server/NimBLE_Secure_Server.ino create mode 100644 lib/libesp32/NimBLE-Arduino/src/NimBLEHIDDevice.cpp create mode 100644 lib/libesp32/NimBLE-Arduino/src/NimBLEHIDDevice.h diff --git a/lib/libesp32/NimBLE-Arduino/CHANGELOG.md b/lib/libesp32/NimBLE-Arduino/CHANGELOG.md index 8dfc5a141..128d3c93d 100644 --- a/lib/libesp32/NimBLE-Arduino/CHANGELOG.md +++ b/lib/libesp32/NimBLE-Arduino/CHANGELOG.md @@ -2,6 +2,75 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +### Added +- `NimBLEDevice::setOwnAddrType` added to enable the use of random and random-resolvable addresses, by asukiaaa + +- New examples for securing and authenticating client/server connections, by mblasee. + +- `NimBLEAdvertiseing::SetMinPreferred` and `NimBLEAdvertiseing::SetMinPreferred` re-added. + +- Conditional checks added for command line config options in `nimconfig.h` to support custom configuration in platformio. + +- `NimBLEClient::setValue` Now takes an extra bool parameter `response` to enable the use of write with response (default = false). + +- `NimBLEClient::getCharacteristic(uint16_t handle)` Enabling the use of the characteristic handle to be used to find +the NimBLERemoteCharacteristic object. + +- `NimBLEHIDDevice` class added by wakwak-koba. + +- `NimBLEServerCallbacks::onDisconnect` overloaded callback added to provide a ble_gap_conn_desc parameter for the application +to obtain information about the disconnected client. + +- Conditional checks in `nimconfig.h` for command line defined macros to support platformio config settings. + +### Changed +- `NimBLEAdvertising::start` now returns a bool value to indicate success/failure. + +- Some asserts were removed in `NimBLEAdvertising::start` and replaced with better return code handling and logging. + +- If a host reset event occurs, scanning and advertising will now only be restarted if their previous duration was indefinite. + +- `NimBLERemoteCharacteristic::subscribe` and `NimBLERemoteCharacteristic::registerForNotify` will now set the callback +regardless of the existance of the CCCD and return true unless the descriptor write operation failed. + +- Advertising tx power level is now sent in the advertisement packet instead of scan response. + +- `NimBLEScan` When the scan ends the scan stopped flag is now set before calling the scan complete callback (if used) +this allows the starting of a new scan from the callback function. + +### Fixed +- Sometimes `NimBLEClient::connect` would hang on the task block if no event arrived to unblock. +A time limit has been added to timeout appropriately. + +- When getting descriptors for a characterisic the end handle of the service was used as a proxy for the characteristic end +handle. This would be rejected by some devices and has been changed to use the next characteristic handle as the end when possible. + +- An exception could occur when deleting a client instance if a notification arrived while the attribute vectors were being +deleted. A flag has been added to prevent this. + +- An exception could occur after a host reset event when the host re-synced if the tasks that were stopped during the event did +not finish processing. A yield has been added after re-syncing to allow tasks to finish before proceeding. + +- Occasionally the controller would fail to send a disconnected event causing the client to indicate it is connected +and would be unable to reconnect. A timer has been added to reset the host/controller if it expires. + +- Occasionally the call to start scanning would get stuck in a loop on BLE_HS_EBUSY, this loop has been removed. + +- 16bit and 32bit UUID's in some cases were not discovered or compared correctly if the device +advertised them as 16/32bit but resolved them to 128bits. Both are now checked. + +- `FreeRTOS` compile errors resolved in latest Ardruino core and IDF v3.3. + +- Multiple instances of `time()` called inside critical sections caused sporadic crashes, these have been moved out of critical regions. + +- Advertisement type now correctly set when using non-connectable (advertiser only) mode. + +- Advertising payload length correction, now accounts for appearance. + +- (Arduino) Ensure controller mode is set to BLE Only. + ## [1.0.2] - 2020-09-13 ### Changed diff --git a/lib/libesp32/NimBLE-Arduino/README.md b/lib/libesp32/NimBLE-Arduino/README.md index 120b0c782..ea28b8811 100644 --- a/lib/libesp32/NimBLE-Arduino/README.md +++ b/lib/libesp32/NimBLE-Arduino/README.md @@ -68,9 +68,9 @@ such as increasing max connections, default is 3, absolute maximum connections i
# Development Status -This Library is tracking the esp-nimble repo, nimble-1.2.0-idf master branch, currently [@95bd864.](https://github.com/espressif/esp-nimble) +This Library is tracking the esp-nimble repo, nimble-1.2.0-idf master branch, currently [@f4ae049.](https://github.com/espressif/esp-nimble) -Also tracking the NimBLE related changes in ESP-IDF, master branch, currently [@2ef4890.](https://github.com/espressif/esp-idf/tree/master/components/bt/host/nimble) +Also tracking the NimBLE related changes in ESP-IDF, master branch, currently [@3caa969.](https://github.com/espressif/esp-idf/tree/master/components/bt/host/nimble)
# Acknowledgments diff --git a/lib/libesp32/NimBLE-Arduino/docs/Command_line_config.md b/lib/libesp32/NimBLE-Arduino/docs/Command_line_config.md new file mode 100644 index 000000000..813156f76 --- /dev/null +++ b/lib/libesp32/NimBLE-Arduino/docs/Command_line_config.md @@ -0,0 +1,93 @@ +# Arduino command line and platformio config options + +`CONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED` + + If defined, NimBLE Client functions will not be included. +- Reduces flash size by approx. 7kB. +
+ +`CONFIG_BT_NIMBLE_ROLE_OBSERVER_DISABLED` + +If defined, NimBLE Scan functions will not be included. +- Reduces flash size by approx. 26kB. +
+ +`CONFIG_BT_NIMBLE_ROLE_PERIPHERAL_DISABLED` + +If defined NimBLE Server functions will not be included. +- Reduces flash size by approx. 16kB. +
+ +`CONFIG_BT_NIMBLE_ROLE_BROADCASTER_DISABLED` + +If defined, NimBLE Advertising functions will not be included. +- Reduces flash size by approx. 5kB. +
+ +`CONFIG_BT_NIMBLE_DEBUG` + +If defined, enables debug log messages from the NimBLE host +- Uses approx. 32kB of flash memory. +
+ +`CONFIG_NIMBLE_CPP_ENABLE_RETURN_CODE_TEXT` + +If defined, NimBLE host return codes will be printed as text in debug log messages. +- Uses approx. 7kB of flash memory. +
+ +`CONFIG_NIMBLE_CPP_ENABLE_GAP_EVENT_CODE_TEXT` + +If defined, GAP event codes will be printed as text in debug log messages. +- Uses approx. 1kB of flash memory. +
+ +`CONFIG_NIMBLE_CPP_ENABLE_ADVERTISMENT_TYPE_TEXT` + +If defined, advertisment types will be printed as text while scanning in debug log messages. +- Uses approx. 250 bytes of flash memory. +
+ +`CONFIG_BT_NIMBLE_PINNED_TO_CORE` + +Sets the core the NimBLE host stack will run on +- Options: 0 or 1 +
+ +`CONFIG_BT_NIMBLE_TASK_STACK_SIZE` + +Set the task stack size for the NimBLE core. +- Default is 4096 +
+ + +`CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL` + +Sets the NimBLE stack to use external PSRAM will be loaded +- Must be defined with a value of 1; Default is CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_INTERNAL 1 +
+ +`CONFIG_BT_NIMBLE_MAX_CONNECTIONS` + +Sets the number of simultaneous connections (esp controller max is 9) +- Default value is 3 +
+ +`CONFIG_BT_NIMBLE_MAX_BONDS` + +Sets the number of devices allowed to store/bond with +- Default value is 3 +
+ +`CONFIG_BT_NIMBLE_MAX_CCCDS` + +Sets the maximum number of CCCD subscriptions to store +- Default value is 8 +
+ +`CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME` + +Set the default device name +- Default value is "nimble" +
+ diff --git a/lib/libesp32/NimBLE-Arduino/examples/NimBLE_Secure_Client/NimBLE_Secure_Client.ino b/lib/libesp32/NimBLE-Arduino/examples/NimBLE_Secure_Client/NimBLE_Secure_Client.ino new file mode 100644 index 000000000..6f9af4f74 --- /dev/null +++ b/lib/libesp32/NimBLE-Arduino/examples/NimBLE_Secure_Client/NimBLE_Secure_Client.ino @@ -0,0 +1,91 @@ +/** NimBLE_Secure_Client Demo: + * + * This example demonstrates the secure passkey protected conenction and communication between an esp32 server and an esp32 client. + * Please note that esp32 stores auth info in nvs memory. After a successful connection it is possible that a passkey change will be ineffective. + * To avoid this clear the memory of the esp32's between security testings. esptool.py is capable of this, example: esptool.py --port /dev/ttyUSB0 erase_flash. + * + * Created: on Jan 08 2021 + * Author: mblasee + */ + +#include + +class ClientCallbacks : public NimBLEClientCallbacks +{ + uint32_t onPassKeyRequest() + { + Serial.println("Client Passkey Request"); + /** return the passkey to send to the server */ + /** Change this to be different from NimBLE_Secure_Server if you want to test what happens on key mismatch */ + return 123456; + }; +}; +static ClientCallbacks clientCB; + +void setup() +{ + Serial.begin(115200); + Serial.println("Starting NimBLE Client"); + + NimBLEDevice::init(""); + NimBLEDevice::setPower(ESP_PWR_LVL_P9); + NimBLEDevice::setSecurityAuth(true, true, true); + NimBLEDevice::setSecurityIOCap(BLE_HS_IO_KEYBOARD_ONLY); + NimBLEScan *pScan = NimBLEDevice::getScan(); + NimBLEScanResults results = pScan->start(5); + + NimBLEUUID serviceUuid("ABCD"); + + for (int i = 0; i < results.getCount(); i++) + { + NimBLEAdvertisedDevice device = results.getDevice(i); + Serial.println(device.getName().c_str()); + + if (device.isAdvertisingService(serviceUuid)) + { + NimBLEClient *pClient = NimBLEDevice::createClient(); + pClient->setClientCallbacks(&clientCB, false); + + if (pClient->connect(&device)) + { + pClient->secureConnection(); + NimBLERemoteService *pService = pClient->getService(serviceUuid); + if (pService != nullptr) + { + NimBLERemoteCharacteristic *pNonSecureCharacteristic = pService->getCharacteristic("1234"); + + if (pNonSecureCharacteristic != nullptr) + { + // Testing to read a non secured characteristic, you should be able to read this even if you have mismatching passkeys. + std::string value = pNonSecureCharacteristic->readValue(); + // print or do whatever you need with the value + Serial.println(value.c_str()); + } + + NimBLERemoteCharacteristic *pSecureCharacteristic = pService->getCharacteristic("1235"); + + if (pSecureCharacteristic != nullptr) + { + // Testing to read a secured characteristic, you should be able to read this only if you have matching passkeys, otherwise you should + // get an error like this. E NimBLERemoteCharacteristic: "<< readValue rc=261" + // This means you are trying to do something without the proper permissions. + std::string value = pSecureCharacteristic->readValue(); + // print or do whatever you need with the value + Serial.println(value.c_str()); + } + } + } + else + { + // failed to connect + Serial.println("failed to connect"); + } + + NimBLEDevice::deleteClient(pClient); + } + } +} + +void loop() +{ +} diff --git a/lib/libesp32/NimBLE-Arduino/examples/NimBLE_Secure_Server/NimBLE_Secure_Server.ino b/lib/libesp32/NimBLE-Arduino/examples/NimBLE_Secure_Server/NimBLE_Secure_Server.ino new file mode 100644 index 000000000..f63cbc162 --- /dev/null +++ b/lib/libesp32/NimBLE-Arduino/examples/NimBLE_Secure_Server/NimBLE_Secure_Server.ino @@ -0,0 +1,37 @@ +/** NimBLE_Secure_Server Demo: + * + * This example demonstrates the secure passkey protected conenction and communication between an esp32 server and an esp32 client. + * Please note that esp32 stores auth info in nvs memory. After a successful connection it is possible that a passkey change will be ineffective. + * To avoid this clear the memory of the esp32's between security testings. esptool.py is capable of this, example: esptool.py --port /dev/ttyUSB0 erase_flash. + * + * Created: on Jan 08 2021 + * Author: mblasee + */ + +#include + +void setup() { + Serial.begin(115200); + Serial.println("Starting NimBLE Server"); + NimBLEDevice::init("NimBLE"); + NimBLEDevice::setPower(ESP_PWR_LVL_P9); + + NimBLEDevice::setSecurityAuth(true, true, true); + NimBLEDevice::setSecurityPasskey(123456); + NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); + NimBLEServer *pServer = NimBLEDevice::createServer(); + NimBLEService *pService = pServer->createService("ABCD"); + NimBLECharacteristic *pNonSecureCharacteristic = pService->createCharacteristic("1234", NIMBLE_PROPERTY::READ ); + NimBLECharacteristic *pSecureCharacteristic = pService->createCharacteristic("1235", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::READ_AUTHEN); + + pService->start(); + pNonSecureCharacteristic->setValue("Hello Non Secure BLE"); + pSecureCharacteristic->setValue("Hello Secure BLE"); + + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->addServiceUUID("ABCD"); + pAdvertising->start(); +} + +void loop() { +} diff --git a/lib/libesp32/NimBLE-Arduino/examples/Refactored_original_examples/BLE_notify/BLE_notify.ino b/lib/libesp32/NimBLE-Arduino/examples/Refactored_original_examples/BLE_notify/BLE_notify.ino index 83f129b83..cb0488819 100644 --- a/lib/libesp32/NimBLE-Arduino/examples/Refactored_original_examples/BLE_notify/BLE_notify.ino +++ b/lib/libesp32/NimBLE-Arduino/examples/Refactored_original_examples/BLE_notify/BLE_notify.ino @@ -116,9 +116,9 @@ void setup() { BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); pAdvertising->setScanResponse(false); - /**This method is removed as it was no longer useful and consumed advertising space - * pAdvertising->setMinPreferred(0x0); // set value to 0x00 to not advertise this parameter - */ + /** Note, this could be left out as that is the default value */ + pAdvertising->setMinPreferred(0x0); // set value to 0x00 to not advertise this parameter + BLEDevice::startAdvertising(); Serial.println("Waiting a client connection to notify..."); } diff --git a/lib/libesp32/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server/BLE_server.ino b/lib/libesp32/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server/BLE_server.ino index 652d77685..faa4d88ea 100644 --- a/lib/libesp32/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server/BLE_server.ino +++ b/lib/libesp32/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server/BLE_server.ino @@ -44,10 +44,9 @@ void setup() { BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); pAdvertising->setScanResponse(true); - /**These methods are removed as they are no longer useful and consumed advertising space - * pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue - * pAdvertising->setMinPreferred(0x12); - */ + pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue + pAdvertising->setMaxPreferred(0x12); + BLEDevice::startAdvertising(); Serial.println("Characteristic defined! Now you can read it in your phone!"); } diff --git a/lib/libesp32/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server_multiconnect/BLE_server_multiconnect.ino b/lib/libesp32/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server_multiconnect/BLE_server_multiconnect.ino index 2ec38c481..9ae3859c7 100644 --- a/lib/libesp32/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server_multiconnect/BLE_server_multiconnect.ino +++ b/lib/libesp32/NimBLE-Arduino/examples/Refactored_original_examples/BLE_server_multiconnect/BLE_server_multiconnect.ino @@ -120,9 +120,9 @@ void setup() { BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); pAdvertising->setScanResponse(false); - /**This method is removed it was no longer useful and consumed advertising space - * pAdvertising->setMinPreferred(0x0); // set value to 0x00 to not advertise this parameter - */ + /** Note, this could be left out as that is the default value */ + pAdvertising->setMinPreferred(0x0); // set value to 0x00 to not advertise this parameter + BLEDevice::startAdvertising(); Serial.println("Waiting a client connection to notify..."); } diff --git a/lib/libesp32/NimBLE-Arduino/src/FreeRTOS.cpp b/lib/libesp32/NimBLE-Arduino/src/FreeRTOS.cpp index 4ebab3958..1c398cbf3 100644 --- a/lib/libesp32/NimBLE-Arduino/src/FreeRTOS.cpp +++ b/lib/libesp32/NimBLE-Arduino/src/FreeRTOS.cpp @@ -264,10 +264,14 @@ void FreeRTOS::Semaphore::setName(std::string name) { * @param [in] length The amount of storage to allocate for the ring buffer. * @param [in] type The type of buffer. One of RINGBUF_TYPE_NOSPLIT, RINGBUF_TYPE_ALLOWSPLIT, RINGBUF_TYPE_BYTEBUF. */ -#if defined(ESP_IDF_VERSION) && !defined(ESP_IDF_VERSION_VAL) //Quick hack to detect if using IDF version that replaced ringbuf_type_t, ESP_IDF_VERSION_VAL is for IDF>4.0.0 -Ringbuffer::Ringbuffer(size_t length, RingbufferType_t type) { +#ifdef ESP_IDF_VERSION //Quick hack to detect if using IDF version that replaced ringbuf_type_t +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) + Ringbuffer::Ringbuffer(size_t length, RingbufferType_t type) { #else -Ringbuffer::Ringbuffer(size_t length, ringbuf_type_t type) { + Ringbuffer::Ringbuffer(size_t length, ringbuf_type_t type) { +#endif +#else + Ringbuffer::Ringbuffer(size_t length, ringbuf_type_t type) { #endif m_handle = ::xRingbufferCreate(length, type); } // Ringbuffer diff --git a/lib/libesp32/NimBLE-Arduino/src/FreeRTOS.h b/lib/libesp32/NimBLE-Arduino/src/FreeRTOS.h index f93f0b1a0..fa33921fe 100644 --- a/lib/libesp32/NimBLE-Arduino/src/FreeRTOS.h +++ b/lib/libesp32/NimBLE-Arduino/src/FreeRTOS.h @@ -68,8 +68,12 @@ public: */ class Ringbuffer { public: -#if defined(ESP_IDF_VERSION) && !defined(ESP_IDF_VERSION_VAL) //Quick hack to detect if using IDF version that replaced ringbuf_type_t, ESP_IDF_VERSION_VAL is for IDF>4.0.0 +#ifdef ESP_IDF_VERSION //Quick hack to detect if using IDF version that replaced ringbuf_type_t +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) Ringbuffer(size_t length, RingbufferType_t type = RINGBUF_TYPE_NOSPLIT); +#else + Ringbuffer(size_t length, ringbuf_type_t type = RINGBUF_TYPE_NOSPLIT); +#endif #else Ringbuffer(size_t length, ringbuf_type_t type = RINGBUF_TYPE_NOSPLIT); #endif diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLE2904.cpp b/lib/libesp32/NimBLE-Arduino/src/NimBLE2904.cpp index d85cd87e4..80318b5b8 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLE2904.cpp +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLE2904.cpp @@ -37,7 +37,7 @@ NimBLE2904::NimBLE2904(NimBLECharacteristic* pCharacterisitic) m_data.m_unit = 0; m_data.m_description = 0; setValue((uint8_t*) &m_data, sizeof(m_data)); -} // BLE2902 +} // BLE2904 /** diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEAdvertising.cpp b/lib/libesp32/NimBLE-Arduino/src/NimBLEAdvertising.cpp index 36bdbf9e9..a10480410 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLEAdvertising.cpp +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEAdvertising.cpp @@ -32,7 +32,7 @@ static const char* LOG_TAG = "NimBLEAdvertising"; /** * @brief Construct a default advertising object. */ -NimBLEAdvertising::NimBLEAdvertising() { +NimBLEAdvertising::NimBLEAdvertising() : m_slaveItvl() { memset(&m_advData, 0, sizeof m_advData); memset(&m_scanData, 0, sizeof m_scanData); memset(&m_advParams, 0, sizeof m_advParams); @@ -41,15 +41,20 @@ NimBLEAdvertising::NimBLEAdvertising() { m_advData.name = (uint8_t *)name; m_advData.name_len = strlen(name); m_advData.name_is_complete = 1; - m_scanData.tx_pwr_lvl_is_present = 1; - m_scanData.tx_pwr_lvl = NimBLEDevice::getPower(); + m_advData.tx_pwr_lvl_is_present = 1; + m_advData.tx_pwr_lvl = NimBLEDevice::getPower(); m_advData.flags = (BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP); m_advData.appearance = 0; m_advData.appearance_is_present = 0; m_advData.mfg_data_len = 0; m_advData.mfg_data = nullptr; + m_advData.slave_itvl_range = nullptr; +#if !defined(CONFIG_BT_NIMBLE_ROLE_PERIPHERAL) + m_advParams.conn_mode = BLE_GAP_CONN_MODE_NON; +#else m_advParams.conn_mode = BLE_GAP_CONN_MODE_UND; +#endif m_advParams.disc_mode = BLE_GAP_DISC_MODE_GEN; m_advParams.itvl_min = 0; m_advParams.itvl_max = 0; @@ -58,6 +63,8 @@ NimBLEAdvertising::NimBLEAdvertising() { m_customScanResponseData = false; m_scanResp = true; m_advDataSet = false; + // Set this to non-zero to prevent auto start if host reset before started by app. + m_duration = BLE_HS_FOREVER; } // NimBLEAdvertising @@ -86,7 +93,6 @@ void NimBLEAdvertising::addServiceUUID(const char* serviceUUID) { * @param [in] serviceUUID The UUID of the service to expose. */ void NimBLEAdvertising::removeServiceUUID(const NimBLEUUID &serviceUUID) { - //m_serviceUUIDs.erase(std::remove_if(m_serviceUUIDs.begin(), m_serviceUUIDs.end(),[serviceUUID](const NimBLEUUID &s) {return serviceUUID == s;}), m_serviceUUIDs.end()); for(auto it = m_serviceUUIDs.begin(); it != m_serviceUUIDs.end(); ++it) { if((*it) == serviceUUID) { m_serviceUUIDs.erase(it); @@ -112,11 +118,9 @@ void NimBLEAdvertising::setAppearance(uint16_t appearance) { /** * @brief Set the type of advertisment to use. * @param [in] adv_type: - * * BLE_HCI_ADV_TYPE_ADV_IND (0) - indirect advertising - * * BLE_HCI_ADV_TYPE_ADV_DIRECT_IND_HD (1) - direct advertisng - high duty cycle - * * BLE_HCI_ADV_TYPE_ADV_SCAN_IND (2) - indirect scan response - * * BLE_HCI_ADV_TYPE_ADV_NONCONN_IND (3) - indirect advertisng - not connectable - * * BLE_HCI_ADV_TYPE_ADV_DIRECT_IND_LD (4) - direct advertising - low duty cycle + * * BLE_GAP_CONN_MODE_NON (0) - not connectable advertising + * * BLE_GAP_CONN_MODE_DIR (1) - directed connectable advertising + * * BLE_GAP_CONN_MODE_UND (2) - undirected connectable advertising */ void NimBLEAdvertising::setAdvertisementType(uint8_t adv_type){ m_advParams.conn_mode = adv_type; @@ -141,6 +145,64 @@ void NimBLEAdvertising::setMaxInterval(uint16_t maxinterval) { } // setMaxInterval +/** + * @brief Set the advertised min connection interval preferred by this device. + * @param [in] mininterval the max interval value. Range = 0x0006 to 0x0C80. + * @details Values not within the range will cancel advertising of this data.\n + * Consumes 6 bytes of advertising space (combined with max interval). + */ +void NimBLEAdvertising::setMinPreferred(uint16_t mininterval) { + // invalid paramters, set the slave interval to null + if(mininterval < 0x0006 || mininterval > 0x0C80) { + m_advData.slave_itvl_range = nullptr; + return; + } + + if(m_advData.slave_itvl_range == nullptr) { + m_advData.slave_itvl_range = m_slaveItvl; + } + + m_slaveItvl[0] = mininterval; + m_slaveItvl[1] = mininterval >> 8; + + uint16_t maxinterval = *(uint16_t*)(m_advData.slave_itvl_range+2); + + // If mininterval is higher than the maxinterval make them the same + if(mininterval > maxinterval) { + m_slaveItvl[2] = m_slaveItvl[0]; + m_slaveItvl[3] = m_slaveItvl[1]; + } +} // setMinPreferred + + +/** + * @brief Set the advertised max connection interval preferred by this device. + * @param [in] maxinterval the max interval value. Range = 0x0006 to 0x0C80. + * @details Values not within the range will cancel advertising of this data.\n + * Consumes 6 bytes of advertising space (combined with min interval). + */ +void NimBLEAdvertising::setMaxPreferred(uint16_t maxinterval) { + // invalid paramters, set the slave interval to null + if(maxinterval < 0x0006 || maxinterval > 0x0C80) { + m_advData.slave_itvl_range = nullptr; + return; + } + if(m_advData.slave_itvl_range == nullptr) { + m_advData.slave_itvl_range = m_slaveItvl; + } + m_slaveItvl[2] = maxinterval; + m_slaveItvl[3] = maxinterval >> 8; + + uint16_t mininterval = *(uint16_t*)(m_advData.slave_itvl_range); + + // If mininterval is higher than the maxinterval make them the same + if(mininterval > maxinterval) { + m_slaveItvl[0] = m_slaveItvl[2]; + m_slaveItvl[1] = m_slaveItvl[3]; + } +} // setMaxPreferred + + /** * @brief Set if scan response is available. * @param [in] set true = scan response available. @@ -156,7 +218,8 @@ void NimBLEAdvertising::setScanResponse(bool set) { * @param [in] connectWhitelistOnly If true, only allow connections from those on the white list. */ void NimBLEAdvertising::setScanFilter(bool scanRequestWhitelistOnly, bool connectWhitelistOnly) { - NIMBLE_LOGD(LOG_TAG, ">> setScanFilter: scanRequestWhitelistOnly: %d, connectWhitelistOnly: %d", scanRequestWhitelistOnly, connectWhitelistOnly); + NIMBLE_LOGD(LOG_TAG, ">> setScanFilter: scanRequestWhitelistOnly: %d, connectWhitelistOnly: %d", + scanRequestWhitelistOnly, connectWhitelistOnly); if (!scanRequestWhitelistOnly && !connectWhitelistOnly) { m_advParams.filter_policy = BLE_HCI_ADV_FILT_NONE; NIMBLE_LOGD(LOG_TAG, "<< setScanFilter"); @@ -194,7 +257,8 @@ void NimBLEAdvertising::setAdvertisementData(NimBLEAdvertisementData& advertisem (uint8_t*)advertisementData.getPayload().data(), advertisementData.getPayload().length()); if (rc != 0) { - NIMBLE_LOGE(LOG_TAG, "ble_gap_adv_set_data: %d %s", rc, NimBLEUtils::returnCodeToString(rc)); + NIMBLE_LOGE(LOG_TAG, "ble_gap_adv_set_data: %d %s", + rc, NimBLEUtils::returnCodeToString(rc)); } m_customAdvData = true; // Set the flag that indicates we are using custom advertising data. NIMBLE_LOGD(LOG_TAG, "<< setAdvertisementData"); @@ -213,7 +277,8 @@ void NimBLEAdvertising::setScanResponseData(NimBLEAdvertisementData& advertiseme (uint8_t*)advertisementData.getPayload().data(), advertisementData.getPayload().length()); if (rc != 0) { - NIMBLE_LOGE(LOG_TAG, "ble_gap_adv_rsp_set_data: %d %s", rc, NimBLEUtils::returnCodeToString(rc)); + NIMBLE_LOGE(LOG_TAG, "ble_gap_adv_rsp_set_data: %d %s", + rc, NimBLEUtils::returnCodeToString(rc)); } m_customScanResponseData = true; // Set the flag that indicates we are using custom scan response data. NIMBLE_LOGD(LOG_TAG, "<< setScanResponseData"); @@ -225,13 +290,14 @@ void NimBLEAdvertising::setScanResponseData(NimBLEAdvertisementData& advertiseme * @param [in] duration The duration, in seconds, to advertise, 0 == advertise forever. * @param [in] advCompleteCB A pointer to a callback to be invoked when advertising ends. */ -void NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdvertising *pAdv)) { - NIMBLE_LOGD(LOG_TAG, ">> Advertising start: customAdvData: %d, customScanResponseData: %d", m_customAdvData, m_customScanResponseData); +bool NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdvertising *pAdv)) { + NIMBLE_LOGD(LOG_TAG, ">> Advertising start: customAdvData: %d, customScanResponseData: %d", + m_customAdvData, m_customScanResponseData); // If Host is not synced we cannot start advertising. if(!NimBLEDevice::m_synced) { NIMBLE_LOGE(LOG_TAG, "Host reset, wait for sync."); - return; + return false; } #if defined(CONFIG_BT_NIMBLE_ROLE_PERIPHERAL) @@ -240,17 +306,21 @@ void NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdv if(!pServer->m_gattsStarted){ pServer->start(); } else if(pServer->getConnectedCount() >= NIMBLE_MAX_CONNECTIONS) { - NIMBLE_LOGW(LOG_TAG, "Max connections reached - not advertising"); - return; + NIMBLE_LOGE(LOG_TAG, "Max connections reached - not advertising"); + return false; } } #endif // If already advertising just return if(ble_gap_adv_active()) { - return; + NIMBLE_LOGW(LOG_TAG, "Advertising already active"); + return false; } + // Save the duration incase of host reset so we can restart with the same params + m_duration = duration; + if(duration == 0){ duration = BLE_HS_FOREVER; } @@ -260,16 +330,31 @@ void NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdv m_advCompCB = advCompleteCB; + m_advParams.disc_mode = BLE_GAP_DISC_MODE_GEN; + m_advData.flags = (BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP); + if(m_advParams.conn_mode == BLE_GAP_CONN_MODE_NON) { + if(!m_scanResp) { + m_advParams.disc_mode = BLE_GAP_DISC_MODE_NON; + m_advData.flags = BLE_HS_ADV_F_BREDR_UNSUP; + } + } + int rc = 0; if (!m_customAdvData && !m_advDataSet) { //start with 3 bytes for the flags data - uint8_t payloadLen = 3; + uint8_t payloadLen = (2 + 1); + if(m_advData.appearance_is_present) + payloadLen += (2 + BLE_HS_ADV_APPEARANCE_LEN); + if(m_advData.tx_pwr_lvl_is_present) + payloadLen += (2 + 1); + if(m_advData.slave_itvl_range != nullptr) + payloadLen += (2 + 4); for(auto &it : m_serviceUUIDs) { if(it.getNative()->u.type == BLE_UUID_TYPE_16) { int add = (m_advData.num_uuids16 > 0) ? 2 : 4; - if((payloadLen + add) > 31){ + if((payloadLen + add) > BLE_HS_ADV_MAX_SZ){ m_advData.uuids16_is_complete = 0; continue; } @@ -278,7 +363,7 @@ void NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdv if(nullptr == (m_advData.uuids16 = (ble_uuid16_t*)realloc(m_advData.uuids16, (m_advData.num_uuids16 + 1) * sizeof(ble_uuid16_t)))) { - NIMBLE_LOGE(LOG_TAG, "Error, no mem"); + NIMBLE_LOGC(LOG_TAG, "Error, no mem"); abort(); } memcpy(&m_advData.uuids16[m_advData.num_uuids16].value, @@ -290,7 +375,7 @@ void NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdv } if(it.getNative()->u.type == BLE_UUID_TYPE_32) { int add = (m_advData.num_uuids32 > 0) ? 4 : 6; - if((payloadLen + add) > 31){ + if((payloadLen + add) > BLE_HS_ADV_MAX_SZ){ m_advData.uuids32_is_complete = 0; continue; } @@ -299,7 +384,7 @@ void NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdv if(nullptr == (m_advData.uuids32 = (ble_uuid32_t*)realloc(m_advData.uuids32, (m_advData.num_uuids32 + 1) * sizeof(ble_uuid32_t)))) { - NIMBLE_LOGE(LOG_TAG, "Error, no mem"); + NIMBLE_LOGC(LOG_TAG, "Error, no mem"); abort(); } memcpy(&m_advData.uuids32[m_advData.num_uuids32].value, @@ -311,7 +396,7 @@ void NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdv } if(it.getNative()->u.type == BLE_UUID_TYPE_128){ int add = (m_advData.num_uuids128 > 0) ? 16 : 18; - if((payloadLen + add) > 31){ + if((payloadLen + add) > BLE_HS_ADV_MAX_SZ){ m_advData.uuids128_is_complete = 0; continue; } @@ -320,7 +405,7 @@ void NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdv if(nullptr == (m_advData.uuids128 = (ble_uuid128_t*)realloc(m_advData.uuids128, (m_advData.num_uuids128 + 1) * sizeof(ble_uuid128_t)))) { - NIMBLE_LOGE(LOG_TAG, "Error, no mem"); + NIMBLE_LOGC(LOG_TAG, "Error, no mem"); abort(); } memcpy(&m_advData.uuids128[m_advData.num_uuids128].value, @@ -333,54 +418,74 @@ void NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdv } // check if there is room for the name, if not put it in scan data - if((payloadLen + m_advData.name_len) > 29) { + if((payloadLen + (2 + m_advData.name_len)) > BLE_HS_ADV_MAX_SZ) { if(m_scanResp){ m_scanData.name = m_advData.name; m_scanData.name_len = m_advData.name_len; - m_scanData.name_is_complete = m_advData.name_is_complete; + if(m_scanData.name_len > BLE_HS_ADV_MAX_SZ - 2) { + m_scanData.name_len = BLE_HS_ADV_MAX_SZ - 2; + m_scanData.name_is_complete = 0; + } else { + m_scanData.name_is_complete = 1; + } m_advData.name = nullptr; m_advData.name_len = 0; + m_advData.name_is_complete = 0; } else { + if(m_advData.tx_pwr_lvl_is_present) { + m_advData.tx_pwr_lvl = 0; + m_advData.tx_pwr_lvl_is_present = 0; + payloadLen -= (2 + 1); + } // if not using scan response just cut the name down // leaving 2 bytes for the data specifier. - m_advData.name_len = (29 - payloadLen); + if(m_advData.name_len > (BLE_HS_ADV_MAX_SZ - payloadLen - 2)) { + m_advData.name_len = (BLE_HS_ADV_MAX_SZ - payloadLen - 2); + m_advData.name_is_complete = 0; + } } - m_advData.name_is_complete = 0; - } - - if(m_advData.name_len > 0) { - payloadLen += (m_advData.name_len + 2); } if(m_scanResp) { - // name length + type byte + length byte + tx power type + length + data - if((m_scanData.name_len + 5) > 31) { - // prioritize name data over tx power - m_scanData.tx_pwr_lvl_is_present = 0; - m_scanData.tx_pwr_lvl = 0; - // limit name to 29 to leave room for the data specifiers - if(m_scanData.name_len > 29) { - m_scanData.name_len = 29; - m_scanData.name_is_complete = false; - } - } - rc = ble_gap_adv_rsp_set_fields(&m_scanData); - if (rc != 0) { - NIMBLE_LOGC(LOG_TAG, "error setting scan response data; rc=%d, %s", rc, NimBLEUtils::returnCodeToString(rc)); - abort(); + switch(rc) { + case 0: + break; + + case BLE_HS_EBUSY: + NIMBLE_LOGE(LOG_TAG, "Already advertising"); + break; + + case BLE_HS_EMSGSIZE: + NIMBLE_LOGE(LOG_TAG, "Scan data too long"); + break; + + default: + NIMBLE_LOGE(LOG_TAG, "Error setting scan response data; rc=%d, %s", + rc, NimBLEUtils::returnCodeToString(rc)); + break; } - // if not using scan response and there is room, - // put the tx power data into the advertisment - } else if (payloadLen < 29) { - m_advData.tx_pwr_lvl_is_present = 1; - m_advData.tx_pwr_lvl = NimBLEDevice::getPower(); } - rc = ble_gap_adv_set_fields(&m_advData); - if (rc != 0) { - NIMBLE_LOGC(LOG_TAG, "error setting advertisement data; rc=%d, %s", rc, NimBLEUtils::returnCodeToString(rc)); - abort(); + if(rc == 0) { + rc = ble_gap_adv_set_fields(&m_advData); + switch(rc) { + case 0: + break; + + case BLE_HS_EBUSY: + NIMBLE_LOGE(LOG_TAG, "Already advertising"); + break; + + case BLE_HS_EMSGSIZE: + NIMBLE_LOGE(LOG_TAG, "Advertisement data too long"); + break; + + default: + NIMBLE_LOGE(LOG_TAG, "Error setting advertisement data; rc=%d, %s", + rc, NimBLEUtils::returnCodeToString(rc)); + break; + } } if(m_advData.num_uuids128 > 0) { @@ -401,24 +506,54 @@ void NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdv m_advData.num_uuids16 = 0; } + if(rc !=0) { + return false; + } + m_advDataSet = true; } #if defined(CONFIG_BT_NIMBLE_ROLE_PERIPHERAL) - rc = ble_gap_adv_start(0, NULL, duration, + rc = ble_gap_adv_start(NimBLEDevice::m_own_addr_type, NULL, duration, &m_advParams, - (pServer != nullptr) ? NimBLEServer::handleGapEvent : NimBLEAdvertising::handleGapEvent, + (pServer != nullptr) ? NimBLEServer::handleGapEvent : + NimBLEAdvertising::handleGapEvent, (pServer != nullptr) ? (void*)pServer : (void*)this); #else - rc = ble_gap_adv_start(0, NULL, duration, + rc = ble_gap_adv_start(NimBLEDevice::m_own_addr_type, NULL, duration, &m_advParams, NimBLEAdvertising::handleGapEvent, this); #endif - if (rc != 0) { - NIMBLE_LOGC(LOG_TAG, "Error enabling advertising; rc=%d, %s", rc, NimBLEUtils::returnCodeToString(rc)); - abort(); + switch(rc) { + case 0: + break; + + case BLE_HS_EINVAL: + NIMBLE_LOGE(LOG_TAG, "Unable to advertise - Duration too long"); + break; + + case BLE_HS_EPREEMPTED: + NIMBLE_LOGE(LOG_TAG, "Unable to advertise - busy"); + break; + + case BLE_HS_ETIMEOUT_HCI: + case BLE_HS_EOS: + case BLE_HS_ECONTROLLER: + case BLE_HS_ENOTSYNCED: + NIMBLE_LOGE(LOG_TAG, "Unable to advertise - Host Reset"); + break; + + default: + NIMBLE_LOGE(LOG_TAG, "Error enabling advertising; rc=%d, %s", + rc, NimBLEUtils::returnCodeToString(rc)); + break; + } + + if(rc != 0) { + return false; } NIMBLE_LOGD(LOG_TAG, "<< Advertising start"); + return true; } // start @@ -427,9 +562,11 @@ void NimBLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(NimBLEAdv */ void NimBLEAdvertising::stop() { NIMBLE_LOGD(LOG_TAG, ">> stop"); + int rc = ble_gap_adv_stop(); if (rc != 0 && rc != BLE_HS_EALREADY) { - NIMBLE_LOGE(LOG_TAG, "ble_gap_adv_stop rc=%d %s", rc, NimBLEUtils::returnCodeToString(rc)); + NIMBLE_LOGE(LOG_TAG, "ble_gap_adv_stop rc=%d %s", + rc, NimBLEUtils::returnCodeToString(rc)); return; } @@ -460,8 +597,17 @@ bool NimBLEAdvertising::isAdvertising() { * Host reset seems to clear advertising data, * we need clear the flag so it reloads it. */ -void NimBLEAdvertising::onHostReset() { +void NimBLEAdvertising::onHostSync() { + NIMBLE_LOGD(LOG_TAG, "Host re-synced"); + m_advDataSet = false; + // If we were advertising forever, restart it now + if(m_duration == 0) { + start(m_duration, m_advCompCB); + } else { + // Otherwise we should tell the app that advertising stopped. + advCompleteCB(); + } } @@ -475,6 +621,19 @@ int NimBLEAdvertising::handleGapEvent(struct ble_gap_event *event, void *arg) { NimBLEAdvertising *pAdv = (NimBLEAdvertising*)arg; if(event->type == BLE_GAP_EVENT_ADV_COMPLETE) { + switch(event->adv_complete.reason) { + // Don't call the callback if host reset, we want to + // preserve the active flag until re-sync to restart advertising. + case BLE_HS_ETIMEOUT_HCI: + case BLE_HS_EOS: + case BLE_HS_ECONTROLLER: + case BLE_HS_ENOTSYNCED: + NIMBLE_LOGC(LOG_TAG, "host reset, rc=%d", event->adv_complete.reason); + NimBLEDevice::onReset(event->adv_complete.reason); + return 0; + default: + break; + } pAdv->advCompleteCB(); } return 0; diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEAdvertising.h b/lib/libesp32/NimBLE-Arduino/src/NimBLEAdvertising.h index 2fab71004..0d97ecbbc 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLEAdvertising.h +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEAdvertising.h @@ -77,7 +77,7 @@ public: void addServiceUUID(const NimBLEUUID &serviceUUID); void addServiceUUID(const char* serviceUUID); void removeServiceUUID(const NimBLEUUID &serviceUUID); - void start(uint32_t duration = 0, void (*advCompleteCB)(NimBLEAdvertising *pAdv) = nullptr); + bool start(uint32_t duration = 0, void (*advCompleteCB)(NimBLEAdvertising *pAdv) = nullptr); void stop(); void setAppearance(uint16_t appearance); void setAdvertisementType(uint8_t adv_type); @@ -87,13 +87,15 @@ public: void setScanFilter(bool scanRequestWhitelistOnly, bool connectWhitelistOnly); void setScanResponseData(NimBLEAdvertisementData& advertisementData); void setScanResponse(bool); + void setMinPreferred(uint16_t); + void setMaxPreferred(uint16_t); void advCompleteCB(); bool isAdvertising(); private: friend class NimBLEDevice; - void onHostReset(); + void onHostSync(); static int handleGapEvent(struct ble_gap_event *event, void *arg); ble_hs_adv_fields m_advData; @@ -104,8 +106,9 @@ private: bool m_customScanResponseData; bool m_scanResp; bool m_advDataSet; - void (*m_advCompCB)(NimBLEAdvertising *pAdv); - + void (*m_advCompCB)(NimBLEAdvertising *pAdv); + uint8_t m_slaveItvl[4]; + uint32_t m_duration; }; #endif // #if defined(CONFIG_BT_NIMBLE_ROLE_PERIPHERAL) diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLECharacteristic.cpp b/lib/libesp32/NimBLE-Arduino/src/NimBLECharacteristic.cpp index 6f7d3d7e0..e9a5a49c9 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLECharacteristic.cpp +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLECharacteristic.cpp @@ -473,9 +473,10 @@ void NimBLECharacteristic::setValue(const uint8_t* data, size_t length) { return; } + time_t t = time(nullptr); portENTER_CRITICAL(&m_valMux); m_value = std::string((char*)data, length); - m_timestamp = time(nullptr); + m_timestamp = t; portEXIT_CRITICAL(&m_valMux); NIMBLE_LOGD(LOG_TAG, "<< setValue"); diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEClient.cpp b/lib/libesp32/NimBLE-Arduino/src/NimBLEClient.cpp index 539a7a016..ddd3abecc 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLEClient.cpp +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEClient.cpp @@ -24,6 +24,9 @@ #include #include +#include "nimble/nimble_port.h" + + static const char* LOG_TAG = "NimBLEClient"; static NimBLEClientCallbacks defaultCallbacks; @@ -56,11 +59,10 @@ static NimBLEClientCallbacks defaultCallbacks; NimBLEClient::NimBLEClient(const NimBLEAddress &peerAddress) : m_peerAddress(peerAddress) { m_pClientCallbacks = &defaultCallbacks; m_conn_id = BLE_HS_CONN_HANDLE_NONE; - m_isConnected = false; - m_waitingToConnect = false; m_connectTimeout = 30000; m_deleteCallbacks = false; m_pTaskData = nullptr; + m_connEstablished = false; m_pConnParams.scan_itvl = 16; // Scan interval in 0.625ms units (NimBLE Default) m_pConnParams.scan_window = 16; // Scan window in 0.625ms units (NimBLE Default) @@ -70,6 +72,9 @@ NimBLEClient::NimBLEClient(const NimBLEAddress &peerAddress) : m_peerAddress(pee m_pConnParams.supervision_timeout = BLE_GAP_INITIAL_SUPERVISION_TIMEOUT; // timeout = 400*10ms = 4000ms m_pConnParams.min_ce_len = BLE_GAP_INITIAL_CONN_MIN_CE_LEN; // Minimum length of connection event in 0.625ms units m_pConnParams.max_ce_len = BLE_GAP_INITIAL_CONN_MAX_CE_LEN; // Maximum length of connection event in 0.625ms units + + ble_npl_callout_init(&m_dcTimer, nimble_port_get_dflt_eventq(), + NimBLEClient::dcTimerCb, this); } // NimBLEClient @@ -89,6 +94,19 @@ NimBLEClient::~NimBLEClient() { } // ~NimBLEClient +/** + * @brief If we have asked to disconnect and the event does not + * occur within the supervision timeout + added delay, this will + * be called to reset the host in the case of a stalled controller. + */ +void NimBLEClient::dcTimerCb(ble_npl_event *event) { + NimBLEClient *pClient = (NimBLEClient*)event->arg; + NIMBLE_LOGC(LOG_TAG, "Timed out disconnecting from %s - resetting host", + std::string(pClient->getPeerAddress()).c_str()); + ble_hs_sched_reset(BLE_HS_ECONTROLLER); +} + + /** * @brief Delete all service objects created by this client and clear the vector. */ @@ -164,12 +182,9 @@ bool NimBLEClient::connect(const NimBLEAddress &address, bool deleteAttibutes) { return false; } - if(ble_gap_conn_active()) { - NIMBLE_LOGE(LOG_TAG, "Connection in progress - must wait."); - return false; - } - - if(!NimBLEDevice::getScan()->stop()) { + if(isConnected() || m_pTaskData != nullptr) { + NIMBLE_LOGE(LOG_TAG, "Client busy, connected to %s, id=%d", + std::string(m_peerAddress).c_str(), getConnId()); return false; } @@ -180,54 +195,97 @@ bool NimBLEClient::connect(const NimBLEAddress &address, bool deleteAttibutes) { m_peerAddress = address; } - ble_addr_t peerAddrt; - memcpy(&peerAddrt.val, m_peerAddress.getNative(),6); - peerAddrt.type = m_peerAddress.getType(); + ble_addr_t peerAddr_t; + memcpy(&peerAddr_t.val, m_peerAddress.getNative(),6); + peerAddr_t.type = m_peerAddress.getType(); + ble_task_data_t taskData = {this, xTaskGetCurrentTaskHandle(), 0, nullptr}; - m_pTaskData = &taskData; - int rc = 0; /* Try to connect the the advertiser. Allow 30 seconds (30000 ms) for * timeout (default value of m_connectTimeout). * Loop on BLE_HS_EBUSY if the scan hasn't stopped yet. */ - do{ - rc = ble_gap_connect(BLE_OWN_ADDR_PUBLIC, &peerAddrt, m_connectTimeout, &m_pConnParams, - NimBLEClient::handleGapEvent, this); - if(rc == BLE_HS_EBUSY) { - vTaskDelay(1 / portTICK_PERIOD_MS); + do { + rc = ble_gap_connect(NimBLEDevice::m_own_addr_type, &peerAddr_t, + m_connectTimeout, &m_pConnParams, + NimBLEClient::handleGapEvent, this); + switch (rc) { + case 0: + m_pTaskData = &taskData; + break; + + case BLE_HS_EBUSY: + // Scan was still running, stop it and try again + if (!NimBLEDevice::getScan()->stop()) { + return false; + } + break; + + case BLE_HS_EDONE: + // A connection to this device already exists, do not connect twice. + NIMBLE_LOGE(LOG_TAG, "Already connected to device; addr=%s", + std::string(m_peerAddress).c_str()); + return false; + + case BLE_HS_EALREADY: + // Already attemting to connect to this device, cancel the previous + // attempt and report failure here so we don't get 2 connections. + NIMBLE_LOGE(LOG_TAG, "Already attempting to connect to %s - cancelling", + std::string(m_peerAddress).c_str()); + ble_gap_conn_cancel(); + return false; + + default: + NIMBLE_LOGE(LOG_TAG, "Failed to connect to %s, rc=%d; %s", + std::string(m_peerAddress).c_str(), + rc, NimBLEUtils::returnCodeToString(rc)); + return false; } - }while(rc == BLE_HS_EBUSY); - if (rc != 0 && rc != BLE_HS_EDONE) { - NIMBLE_LOGE(LOG_TAG, "Error: Failed to connect to device; " - "addr=%s, rc=%d; %s", - std::string(m_peerAddress).c_str(), - rc, NimBLEUtils::returnCodeToString(rc)); + } while (rc == BLE_HS_EBUSY); + + // Wait for the connect timeout time +1 second for the connection to complete + if(ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(m_connectTimeout + 1000)) == pdFALSE) { m_pTaskData = nullptr; - m_waitingToConnect = false; + // If a connection was made but no response from MTU exchange; disconnect + if(isConnected()) { + NIMBLE_LOGE(LOG_TAG, "Connect timeout - no response"); + disconnect(); + } else { + // workaround; if the controller doesn't cancel the connection + // at the timeout cancel it here. + NIMBLE_LOGE(LOG_TAG, "Connect timeout - cancelling"); + ble_gap_conn_cancel(); + } + return false; - } - m_waitingToConnect = true; - - // Wait for the connection to complete. - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - - if(taskData.rc != 0){ + } else if(taskData.rc != 0){ + NIMBLE_LOGE(LOG_TAG, "Connection failed; status=%d %s", + taskData.rc, + NimBLEUtils::returnCodeToString(taskData.rc)); + // If the failure was not a result of a disconnection + // make sure we disconnect now to avoid dangling connections + if(isConnected()) { + ble_gap_terminate(m_conn_id, BLE_ERR_REM_USER_CONN_TERM); + } return false; + } else { + NIMBLE_LOGI(LOG_TAG, "Connection established"); } if(deleteAttibutes) { deleteServices(); } + m_connEstablished = true; m_pClientCallbacks->onConnect(this); NIMBLE_LOGD(LOG_TAG, "<< connect()"); - return true; + // Check if still connected before returning + return isConnected(); } // connect @@ -267,13 +325,31 @@ bool NimBLEClient::secureConnection() { */ int NimBLEClient::disconnect(uint8_t reason) { NIMBLE_LOGD(LOG_TAG, ">> disconnect()"); + int rc = 0; - if(m_isConnected){ + if(isConnected()){ rc = ble_gap_terminate(m_conn_id, reason); - if(rc != 0){ - NIMBLE_LOGE(LOG_TAG, "ble_gap_terminate failed: rc=%d %s", rc, - NimBLEUtils::returnCodeToString(rc)); + if (rc == 0) { + ble_addr_t peerAddr_t; + memcpy(&peerAddr_t.val, m_peerAddress.getNative(),6); + peerAddr_t.type = m_peerAddress.getType(); + + // Set the disconnect timeout to the supervison timeout time + 1 second + // In case the event triggers shortly after the supervision timeout. + // We don't want to prematurely reset the host. + ble_gap_conn_desc desc; + if(ble_gap_conn_find_by_addr(&peerAddr_t, &desc) == 0){ + ble_npl_time_t ticks; + ble_npl_time_ms_to_ticks((desc.supervision_timeout + 100) * 10, &ticks); + ble_npl_callout_reset(&m_dcTimer, ticks); + NIMBLE_LOGD(LOG_TAG, "DC TIMEOUT = %dms", (desc.supervision_timeout + 100) * 10); + } + } else if (rc != BLE_HS_EALREADY) { + NIMBLE_LOGE(LOG_TAG, "ble_gap_terminate failed: rc=%d %s", + rc, NimBLEUtils::returnCodeToString(rc)); } + } else { + NIMBLE_LOGD(LOG_TAG, "Not connected to any peers"); } NIMBLE_LOGD(LOG_TAG, "<< disconnect()"); @@ -454,6 +530,16 @@ NimBLERemoteService* NimBLEClient::getService(const NimBLEUUID &uuid) { if(m_servicesVector.size() > prev_size) { return m_servicesVector.back(); } + + // If the request was successful but 16/32 bit service not found + // try again with the 128 bit uuid. + if(uuid.bitSize() == BLE_UUID_TYPE_16 || + uuid.bitSize() == BLE_UUID_TYPE_32) + { + NimBLEUUID uuid128(uuid); + uuid128.to128(); + return getService(uuid128); + } } NIMBLE_LOGD(LOG_TAG, "<< getService: not found"); @@ -510,7 +596,7 @@ bool NimBLEClient::retrieveServices(const NimBLEUUID *uuid_filter) { NIMBLE_LOGD(LOG_TAG, ">> retrieveServices"); - if(!m_isConnected){ + if(!isConnected()){ NIMBLE_LOGE(LOG_TAG, "Disconnected, could not retrieve services -aborting"); return false; } @@ -618,10 +704,11 @@ std::string NimBLEClient::getValue(const NimBLEUUID &serviceUUID, const NimBLEUU * @param [in] serviceUUID The service that owns the characteristic. * @param [in] characteristicUUID The characteristic whose value we wish to write. * @param [in] value The value to write to the characteristic. + * @param [in] response If true, uses write with response operation. * @returns true if successful otherwise false */ bool NimBLEClient::setValue(const NimBLEUUID &serviceUUID, const NimBLEUUID &characteristicUUID, - const std::string &value) + const std::string &value, bool response) { NIMBLE_LOGD(LOG_TAG, ">> setValue: serviceUUID: %s, characteristicUUID: %s", serviceUUID.toString().c_str(), characteristicUUID.toString().c_str()); @@ -632,7 +719,7 @@ bool NimBLEClient::setValue(const NimBLEUUID &serviceUUID, const NimBLEUUID &cha if(pService != nullptr) { NimBLERemoteCharacteristic* pChar = pService->getCharacteristic(characteristicUUID); if(pChar != nullptr) { - ret = pChar->writeValue(value); + ret = pChar->writeValue(value, response); } } @@ -641,6 +728,31 @@ bool NimBLEClient::setValue(const NimBLEUUID &serviceUUID, const NimBLEUUID &cha } // setValue +/** + * @brief Get the remote characteristic with the specified handle. + * @param [in] handle The handle of the desired characteristic. + * @returns The matching remote characteristic, nullptr otherwise. + */ +NimBLERemoteCharacteristic* NimBLEClient::getCharacteristic(const uint16_t handle) +{ + NimBLERemoteService *pService = nullptr; + for(auto it = m_servicesVector.begin(); it != m_servicesVector.end(); ++it) { + if ((*it)->getStartHandle() <= handle && handle <= (*it)->getEndHandle()) { + pService = *it; + break; + } + } + + if (pService != nullptr) { + for (auto it = pService->begin(); it != pService->end(); ++it) { + if ((*it)->getHandle() == handle) { + return *it; + } + } + } + + return nullptr; +} /** * @brief Get the current mtu of this connection. @@ -656,7 +768,8 @@ uint16_t NimBLEClient::getMTU() { * @param [in] event The event structure sent by the NimBLE stack. * @param [in] arg A pointer to the client instance that registered for this callback. */ - /*STATIC*/ int NimBLEClient::handleGapEvent(struct ble_gap_event *event, void *arg) { + /*STATIC*/ + int NimBLEClient::handleGapEvent(struct ble_gap_event *event, void *arg) { NimBLEClient* client = (NimBLEClient*)arg; int rc; @@ -665,61 +778,66 @@ uint16_t NimBLEClient::getMTU() { switch(event->type) { case BLE_GAP_EVENT_DISCONNECT: { - if(!client->m_isConnected) - return 0; - - if(client->m_conn_id != event->disconnect.conn.conn_handle) - return 0; - - client->m_isConnected = false; - client->m_waitingToConnect=false; - // Remove the device from ignore list so we will scan it again - NimBLEDevice::removeIgnored(client->m_peerAddress); - - NIMBLE_LOGI(LOG_TAG, "disconnect; reason=%d, %s", event->disconnect.reason, - NimBLEUtils::returnCodeToString(event->disconnect.reason)); - + rc = event->disconnect.reason; // If Host reset tell the device now before returning to prevent // any errors caused by calling host functions before resyncing. - switch(event->disconnect.reason) { - case BLE_HS_ETIMEOUT_HCI: - case BLE_HS_EOS: + switch(rc) { case BLE_HS_ECONTROLLER: + case BLE_HS_ETIMEOUT_HCI: case BLE_HS_ENOTSYNCED: - NIMBLE_LOGC(LOG_TAG, "Disconnect - host reset, rc=%d", event->disconnect.reason); - NimBLEDevice::onReset(event->disconnect.reason); + case BLE_HS_EOS: + NIMBLE_LOGC(LOG_TAG, "Disconnect - host reset, rc=%d", rc); + NimBLEDevice::onReset(rc); break; default: + // Check that the event is for this client. + if(client->m_conn_id != event->disconnect.conn.conn_handle) { + return 0; + } break; } - //client->m_conn_id = BLE_HS_CONN_HANDLE_NONE; + client->m_conn_id = BLE_HS_CONN_HANDLE_NONE; + + // Stop the disconnect timer since we are now disconnected. + ble_npl_callout_stop(&client->m_dcTimer); + + // Remove the device from ignore list so we will scan it again + NimBLEDevice::removeIgnored(client->m_peerAddress); + + // If we received a connected event but did not get established (no PDU) + // then a disconnect event will be sent but we should not send it to the + // app for processing. Instead we will ensure the task is released + // and report the error. + if(!client->m_connEstablished) + break; + + NIMBLE_LOGI(LOG_TAG, "disconnect; reason=%d, %s", + rc, NimBLEUtils::returnCodeToString(rc)); + + client->m_connEstablished = false; client->m_pClientCallbacks->onDisconnect(client); - rc = event->disconnect.reason; break; } // BLE_GAP_EVENT_DISCONNECT case BLE_GAP_EVENT_CONNECT: { - - if(!client->m_waitingToConnect) + // If we aren't waiting for this connection response + // we should drop the connection immediately. + if(client->isConnected() || client->m_pTaskData == nullptr) { + ble_gap_terminate(event->connect.conn_handle, BLE_ERR_REM_USER_CONN_TERM); return 0; + } - //if(client->m_conn_id != BLE_HS_CONN_HANDLE_NONE) - // return 0; - - client->m_waitingToConnect=false; - - if (event->connect.status == 0) { - client->m_isConnected = true; - - NIMBLE_LOGD(LOG_TAG, "Connection established"); + rc = event->connect.status; + if (rc == 0) { + NIMBLE_LOGI(LOG_TAG, "Connected event"); client->m_conn_id = event->connect.conn_handle; rc = ble_gattc_exchange_mtu(client->m_conn_id, NULL,NULL); if(rc != 0) { - NIMBLE_LOGE(LOG_TAG, "ble_gattc_exchange_mtu: rc=%d %s",rc, - NimBLEUtils::returnCodeToString(rc)); + NIMBLE_LOGE(LOG_TAG, "MTU exchange error; rc=%d %s", + rc, NimBLEUtils::returnCodeToString(rc)); break; } @@ -727,14 +845,10 @@ uint16_t NimBLEClient::getMTU() { // scanning since we are already connected to it NimBLEDevice::addIgnored(client->m_peerAddress); } else { - NIMBLE_LOGE(LOG_TAG, "Error: Connection failed; status=%d %s", - event->connect.status, - NimBLEUtils::returnCodeToString(event->connect.status)); - - client->m_isConnected = false; - rc = event->connect.status; + client->m_conn_id = BLE_HS_CONN_HANDLE_NONE; break; } + return 0; } // BLE_GAP_EVENT_CONNECT @@ -742,7 +856,14 @@ uint16_t NimBLEClient::getMTU() { if(client->m_conn_id != event->notify_rx.conn_handle) return 0; - NIMBLE_LOGD(LOG_TAG, "Notify Recieved for handle: %d",event->notify_rx.attr_handle); + // If a notification comes before this flag is set we might + // access a vector while it is being cleared in connect() + if(!client->m_connEstablished) { + return 0; + } + + NIMBLE_LOGD(LOG_TAG, "Notify Recieved for handle: %d", + event->notify_rx.attr_handle); for(auto &it: client->m_servicesVector) { // Dont waste cycles searching services without this handle in its range @@ -752,8 +873,8 @@ uint16_t NimBLEClient::getMTU() { auto cVector = &it->m_characteristicVector; NIMBLE_LOGD(LOG_TAG, "checking service %s for handle: %d", - it->getUUID().toString().c_str(), - event->notify_rx.attr_handle); + it->getUUID().toString().c_str(), + event->notify_rx.attr_handle); auto characteristic = cVector->cbegin(); for(; characteristic != cVector->cend(); ++characteristic) { @@ -762,16 +883,19 @@ uint16_t NimBLEClient::getMTU() { } if(characteristic != cVector->cend()) { - NIMBLE_LOGD(LOG_TAG, "Got Notification for characteristic %s", (*characteristic)->toString().c_str()); + NIMBLE_LOGD(LOG_TAG, "Got Notification for characteristic %s", + (*characteristic)->toString().c_str()); + time_t t = time(nullptr); portENTER_CRITICAL(&(*characteristic)->m_valMux); - (*characteristic)->m_value = std::string((char *)event->notify_rx.om->om_data, event->notify_rx.om->om_len); - (*characteristic)->m_timestamp = time(nullptr); + (*characteristic)->m_value = std::string((char *)event->notify_rx.om->om_data, + event->notify_rx.om->om_len); + (*characteristic)->m_timestamp = t; portEXIT_CRITICAL(&(*characteristic)->m_valMux); if ((*characteristic)->m_notifyCallback != nullptr) { NIMBLE_LOGD(LOG_TAG, "Invoking callback for notification on characteristic %s", - (*characteristic)->toString().c_str()); + (*characteristic)->toString().c_str()); (*characteristic)->m_notifyCallback(*characteristic, event->notify_rx.om->om_data, event->notify_rx.om->om_len, !event->notify_rx.indication); @@ -790,10 +914,10 @@ uint16_t NimBLEClient::getMTU() { } NIMBLE_LOGD(LOG_TAG, "Peer requesting to update connection parameters"); NIMBLE_LOGD(LOG_TAG, "MinInterval: %d, MaxInterval: %d, Latency: %d, Timeout: %d", - event->conn_update_req.peer_params->itvl_min, - event->conn_update_req.peer_params->itvl_max, - event->conn_update_req.peer_params->latency, - event->conn_update_req.peer_params->supervision_timeout); + event->conn_update_req.peer_params->itvl_min, + event->conn_update_req.peer_params->itvl_max, + event->conn_update_req.peer_params->latency, + event->conn_update_req.peer_params->supervision_timeout); rc = client->m_pClientCallbacks->onConnParamsUpdateRequest(client, event->conn_update_req.peer_params) ? 0 : BLE_ERR_CONN_PARMS; @@ -827,7 +951,9 @@ uint16_t NimBLEClient::getMTU() { return 0; } - if(event->enc_change.status == 0 || event->enc_change.status == (BLE_HS_ERR_HCI_BASE + BLE_ERR_PINKEY_MISSING)) { + if(event->enc_change.status == 0 || + event->enc_change.status == (BLE_HS_ERR_HCI_BASE + BLE_ERR_PINKEY_MISSING)) + { struct ble_gap_conn_desc desc; rc = ble_gap_conn_find(event->enc_change.conn_handle, &desc); assert(rc == 0); @@ -922,7 +1048,9 @@ uint16_t NimBLEClient::getMTU() { if(client->m_pTaskData != nullptr) { client->m_pTaskData->rc = rc; - xTaskNotifyGive(client->m_pTaskData->task); + if(client->m_pTaskData->task) { + xTaskNotifyGive(client->m_pTaskData->task); + } client->m_pTaskData = nullptr; } @@ -935,7 +1063,7 @@ uint16_t NimBLEClient::getMTU() { * @return True if we are connected and false if we are not connected. */ bool NimBLEClient::isConnected() { - return m_isConnected; + return m_conn_id != BLE_HS_CONN_HANDLE_NONE; } // isConnected diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEClient.h b/lib/libesp32/NimBLE-Arduino/src/NimBLEClient.h index ddeef3cc3..a4b847000 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLEClient.h +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEClient.h @@ -30,6 +30,7 @@ #include class NimBLERemoteService; +class NimBLERemoteCharacteristic; class NimBLEClientCallbacks; class NimBLEAdvertisedDevice; @@ -54,7 +55,8 @@ public: size_t deleteService(const NimBLEUUID &uuid); std::string getValue(const NimBLEUUID &serviceUUID, const NimBLEUUID &characteristicUUID); bool setValue(const NimBLEUUID &serviceUUID, const NimBLEUUID &characteristicUUID, - const std::string &value); + const std::string &value, bool response = false); + NimBLERemoteCharacteristic* getCharacteristic(const uint16_t handle); bool isConnected(); void setClientCallbacks(NimBLEClientCallbacks *pClientCallbacks, bool deleteCallbacks = true); @@ -82,16 +84,17 @@ private: const struct ble_gatt_error *error, const struct ble_gatt_svc *service, void *arg); + static void dcTimerCb(ble_npl_event *event); bool retrieveServices(const NimBLEUUID *uuid_filter = nullptr); NimBLEAddress m_peerAddress; uint16_t m_conn_id; - bool m_isConnected; - bool m_waitingToConnect; + bool m_connEstablished; bool m_deleteCallbacks; int32_t m_connectTimeout; NimBLEClientCallbacks* m_pClientCallbacks; - ble_task_data_t *m_pTaskData; + ble_task_data_t* m_pTaskData; + ble_npl_callout m_dcTimer; std::vector m_servicesVector; diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEDevice.cpp b/lib/libesp32/NimBLE-Arduino/src/NimBLEDevice.cpp index fb36e6e57..f34a522f7 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLEDevice.cpp +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEDevice.cpp @@ -25,6 +25,7 @@ #include "nimble/nimble_port.h" #include "nimble/nimble_port_freertos.h" #include "host/ble_hs.h" +#include "host/ble_hs_pvcy.h" #include "host/util/util.h" #include "services/gap/ble_svc_gap.h" #include "services/gatt/ble_svc_gatt.h" @@ -60,6 +61,7 @@ std::list NimBLEDevice::m_cList; #endif std::list NimBLEDevice::m_ignoreList; NimBLESecurityCallbacks* NimBLEDevice::m_securityCallbacks = nullptr; +uint8_t NimBLEDevice::m_own_addr_type = BLE_OWN_ADDR_PUBLIC; /** @@ -144,8 +146,8 @@ void NimBLEDevice::stopAdvertising() { #if defined(CONFIG_BT_NIMBLE_ROLE_CENTRAL) /* STATIC */ NimBLEClient* NimBLEDevice::createClient(NimBLEAddress peerAddress) { if(m_cList.size() >= NIMBLE_MAX_CONNECTIONS) { - NIMBLE_LOGW("Number of clients exceeds Max connections. Max=(%d)", - NIMBLE_MAX_CONNECTIONS); + NIMBLE_LOGW(LOG_TAG,"Number of clients exceeds Max connections. Cur=%d Max=%d", + m_cList.size(), NIMBLE_MAX_CONNECTIONS); } NimBLEClient* pClient = new NimBLEClient(peerAddress); @@ -165,26 +167,31 @@ void NimBLEDevice::stopAdvertising() { return false; } + // Set the connection established flag to false to stop notifications + // from accessing the attribute vectors while they are being deleted. + pClient->m_connEstablished = false; int rc =0; - if(pClient->m_isConnected) { + if(pClient->isConnected()) { rc = pClient->disconnect(); if (rc != 0 && rc != BLE_HS_EALREADY && rc != BLE_HS_ENOTCONN) { return false; } - while(pClient->m_isConnected) { - vTaskDelay(10); + while(pClient->isConnected()) { + taskYIELD(); } - } + // Since we set the flag to false the app will not get a callback + // in the disconnect event so we call it here for good measure. + pClient->m_pClientCallbacks->onDisconnect(pClient); - if(pClient->m_waitingToConnect) { + } else if(pClient->m_pTaskData != nullptr) { rc = ble_gap_conn_cancel(); if (rc != 0 && rc != BLE_HS_EALREADY) { return false; } - while(pClient->m_waitingToConnect) { - vTaskDelay(10); + while(pClient->m_pTaskData != nullptr) { + taskYIELD(); } } @@ -405,30 +412,16 @@ void NimBLEDevice::stopAdvertising() { m_synced = false; -#if defined(CONFIG_BT_NIMBLE_ROLE_OBSERVER) - if(m_pScan != nullptr) { - m_pScan->onHostReset(); - } -#endif - -/* Not needed - if(m_pServer != nullptr) { - m_pServer->onHostReset(); - } - - for(auto it = m_cList.cbegin(); it != m_cList.cend(); ++it) { - (*it)->onHostReset(); - } -*/ - -#if defined(CONFIG_BT_NIMBLE_ROLE_BROADCASTER) - if(m_bleAdvertising != nullptr) { - m_bleAdvertising->onHostReset(); - } -#endif - NIMBLE_LOGC(LOG_TAG, "Resetting state; reason=%d, %s", reason, NimBLEUtils::returnCodeToString(reason)); + +#if defined(CONFIG_BT_NIMBLE_ROLE_OBSERVER) + if(initialized) { + if(m_pScan != nullptr) { + m_pScan->onHostReset(); + } + } +#endif } // onReset @@ -448,20 +441,22 @@ void NimBLEDevice::stopAdvertising() { int rc = ble_hs_util_ensure_addr(0); assert(rc == 0); + // Yield for houskeeping before returning to operations. + // Occasionally triggers exception without. + taskYIELD(); + m_synced = true; if(initialized) { #if defined(CONFIG_BT_NIMBLE_ROLE_OBSERVER) if(m_pScan != nullptr) { - // Restart scanning with the last values sent, allow to clear results. - m_pScan->start(m_pScan->m_duration, m_pScan->m_scanCompleteCB); + m_pScan->onHostSync(); } #endif #if defined(CONFIG_BT_NIMBLE_ROLE_BROADCASTER) if(m_bleAdvertising != nullptr) { - // Restart advertisng, parameters should already be set. - m_bleAdvertising->start(); + m_bleAdvertising->onHostSync(); } #endif } @@ -705,6 +700,35 @@ void NimBLEDevice::setSecurityCallbacks(NimBLESecurityCallbacks* callbacks) { } // setSecurityCallbacks +/** + * @brief Set the own address type. + * @param [in] own_addr_type Own Bluetooth Device address type.\n + * The available bits are defined as: + * * 0x00: BLE_OWN_ADDR_PUBLIC + * * 0x01: BLE_OWN_ADDR_RANDOM + * * 0x02: BLE_OWN_ADDR_RPA_PUBLIC_DEFAULT + * * 0x03: BLE_OWN_ADDR_RPA_RANDOM_DEFAULT + * @param [in] useNRPA If true, and address type is random, uses a non-resolvable random address. + */ +void NimBLEDevice::setOwnAddrType(uint8_t own_addr_type, bool useNRPA) { + m_own_addr_type = own_addr_type; + switch (own_addr_type) { + case BLE_OWN_ADDR_PUBLIC: + ble_hs_pvcy_rpa_config(NIMBLE_HOST_DISABLE_PRIVACY); + break; + case BLE_OWN_ADDR_RANDOM: + setSecurityInitKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID); + ble_hs_pvcy_rpa_config(useNRPA ? NIMBLE_HOST_ENABLE_NRPA : NIMBLE_HOST_ENABLE_RPA); + break; + case BLE_OWN_ADDR_RPA_PUBLIC_DEFAULT: + case BLE_OWN_ADDR_RPA_RANDOM_DEFAULT: + setSecurityInitKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID); + ble_hs_pvcy_rpa_config(NIMBLE_HOST_ENABLE_RPA); + break; + } +} // setOwnAddrType + + /** * @brief Start the connection securing and authorization for this connection. * @param conn_id The connection id of the peer device. diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEDevice.h b/lib/libesp32/NimBLE-Arduino/src/NimBLEDevice.h index 252c52afd..2d586bb42 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLEDevice.h +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEDevice.h @@ -116,6 +116,7 @@ public: static void setSecurityPasskey(uint32_t pin); static uint32_t getSecurityPasskey(); static void setSecurityCallbacks(NimBLESecurityCallbacks* pCallbacks); + static void setOwnAddrType(uint8_t own_addr_type, bool useNRPA=false); static int startSecurity(uint16_t conn_id); static int setMTU(uint16_t mtu); static uint16_t getMTU(); @@ -182,6 +183,7 @@ private: static uint32_t m_passkey; static ble_gap_event_listener m_listener; static gap_event_handler m_customGapHandler; + static uint8_t m_own_addr_type; }; diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEHIDDevice.cpp b/lib/libesp32/NimBLE-Arduino/src/NimBLEHIDDevice.cpp new file mode 100644 index 000000000..39f07dede --- /dev/null +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEHIDDevice.cpp @@ -0,0 +1,233 @@ +/* + * NimBLEHIDDevice.cpp + * + * Created: on Oct 06 2020 + * Author wakwak-koba + * + * Originally: + * + * BLEHIDDevice.cpp + * + * Created on: Jan 03, 2018 + * Author: chegewara + */ +#include "sdkconfig.h" +#if defined(CONFIG_BT_ENABLED) + +#include "nimconfig.h" +#if defined(CONFIG_BT_NIMBLE_ROLE_PERIPHERAL) + +#include "NimBLEHIDDevice.h" +#include "NimBLE2904.h" + +NimBLEHIDDevice::NimBLEHIDDevice(NimBLEServer* server) { + /* + * Here we create mandatory services described in bluetooth specification + */ + m_deviceInfoService = server->createService(NimBLEUUID((uint16_t) 0x180a)); + m_hidService = server->createService(NimBLEUUID((uint16_t) 0x1812), 40); + m_batteryService = server->createService(NimBLEUUID((uint16_t) 0x180f)); + + /* + * Mandatory characteristic for device info service + */ + m_pnpCharacteristic = m_deviceInfoService->createCharacteristic((uint16_t) 0x2a50, NIMBLE_PROPERTY::READ); + + /* + * Mandatory characteristics for HID service + */ + m_hidInfoCharacteristic = m_hidService->createCharacteristic((uint16_t) 0x2a4a, NIMBLE_PROPERTY::READ); + m_reportMapCharacteristic = m_hidService->createCharacteristic((uint16_t) 0x2a4b, NIMBLE_PROPERTY::READ); + m_hidControlCharacteristic = m_hidService->createCharacteristic((uint16_t) 0x2a4c, NIMBLE_PROPERTY::WRITE_NR); + m_protocolModeCharacteristic = m_hidService->createCharacteristic((uint16_t) 0x2a4e, NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::READ); + + /* + * Mandatory battery level characteristic with notification and presence descriptor + */ + m_batteryLevelCharacteristic = m_batteryService->createCharacteristic((uint16_t) 0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); + NimBLE2904* batteryLevelDescriptor = (NimBLE2904*)m_batteryLevelCharacteristic->createDescriptor((uint16_t) 0x2904); + batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8); + batteryLevelDescriptor->setNamespace(1); + batteryLevelDescriptor->setUnit(0x27ad); + + /* + * This value is setup here because its default value in most usage cases, its very rare to use boot mode + * and we want to simplify library using as much as possible + */ + const uint8_t pMode[] = { 0x01 }; + protocolMode()->setValue((uint8_t*) pMode, 1); +} + +NimBLEHIDDevice::~NimBLEHIDDevice() { +} + +/* + * @brief + */ +void NimBLEHIDDevice::reportMap(uint8_t* map, uint16_t size) { + m_reportMapCharacteristic->setValue(map, size); +} + +/* + * @brief This function suppose to be called at the end, when we have created all characteristics we need to build HID service + */ +void NimBLEHIDDevice::startServices() { + m_deviceInfoService->start(); + m_hidService->start(); + m_batteryService->start(); +} + +/* + * @brief Create manufacturer characteristic (this characteristic is optional) + */ +NimBLECharacteristic* NimBLEHIDDevice::manufacturer() { + m_manufacturerCharacteristic = m_deviceInfoService->createCharacteristic((uint16_t) 0x2a29, NIMBLE_PROPERTY::READ); + return m_manufacturerCharacteristic; +} + +/* + * @brief Set manufacturer name + * @param [in] name manufacturer name + */ +void NimBLEHIDDevice::manufacturer(std::string name) { + m_manufacturerCharacteristic->setValue(name); +} + +/* + * @brief + */ +void NimBLEHIDDevice::pnp(uint8_t sig, uint16_t vid, uint16_t pid, uint16_t version) { + uint8_t pnp[] = { sig, (uint8_t) (vid >> 8), (uint8_t) vid, (uint8_t) (pid >> 8), (uint8_t) pid, (uint8_t) (version >> 8), (uint8_t) version }; + m_pnpCharacteristic->setValue(pnp, sizeof(pnp)); +} + +/* + * @brief + */ +void NimBLEHIDDevice::hidInfo(uint8_t country, uint8_t flags) { + uint8_t info[] = { 0x11, 0x1, country, flags }; + m_hidInfoCharacteristic->setValue(info, sizeof(info)); +} + +/* + * @brief Create input report characteristic that need to be saved as new characteristic object so can be further used + * @param [in] reportID input report ID, the same as in report map for input object related to created characteristic + * @return pointer to new input report characteristic + */ +NimBLECharacteristic* NimBLEHIDDevice::inputReport(uint8_t reportID) { + NimBLECharacteristic* inputReportCharacteristic = m_hidService->createCharacteristic((uint16_t) 0x2a4d, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); + NimBLEDescriptor* inputReportDescriptor = inputReportCharacteristic->createDescriptor((uint16_t) 0x2908); + + uint8_t desc1_val[] = { reportID, 0x01 }; + inputReportDescriptor->setValue((uint8_t*) desc1_val, 2); + + return inputReportCharacteristic; +} + +/* + * @brief Create output report characteristic that need to be saved as new characteristic object so can be further used + * @param [in] reportID Output report ID, the same as in report map for output object related to created characteristic + * @return Pointer to new output report characteristic + */ +NimBLECharacteristic* NimBLEHIDDevice::outputReport(uint8_t reportID) { + NimBLECharacteristic* outputReportCharacteristic = m_hidService->createCharacteristic((uint16_t) 0x2a4d, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC); + NimBLEDescriptor* outputReportDescriptor = outputReportCharacteristic->createDescriptor((uint16_t) 0x2908); + + uint8_t desc1_val[] = { reportID, 0x02 }; + outputReportDescriptor->setValue((uint8_t*) desc1_val, 2); + + return outputReportCharacteristic; +} + +/* + * @brief Create feature report characteristic that need to be saved as new characteristic object so can be further used + * @param [in] reportID Feature report ID, the same as in report map for feature object related to created characteristic + * @return Pointer to new feature report characteristic + */ +NimBLECharacteristic* NimBLEHIDDevice::featureReport(uint8_t reportID) { + NimBLECharacteristic* featureReportCharacteristic = m_hidService->createCharacteristic((uint16_t) 0x2a4d, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC); + NimBLEDescriptor* featureReportDescriptor = featureReportCharacteristic->createDescriptor((uint16_t) 0x2908); + + uint8_t desc1_val[] = { reportID, 0x03 }; + featureReportDescriptor->setValue((uint8_t*) desc1_val, 2); + + return featureReportCharacteristic; +} + +/* + * @brief + */ +NimBLECharacteristic* NimBLEHIDDevice::bootInput() { + return m_hidService->createCharacteristic((uint16_t) 0x2a22, NIMBLE_PROPERTY::NOTIFY); +} + +/* + * @brief + */ +NimBLECharacteristic* NimBLEHIDDevice::bootOutput() { + return m_hidService->createCharacteristic((uint16_t) 0x2a32, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR); +} + +/* + * @brief + */ +NimBLECharacteristic* NimBLEHIDDevice::hidControl() { + return m_hidControlCharacteristic; +} + +/* + * @brief + */ +NimBLECharacteristic* NimBLEHIDDevice::protocolMode() { + return m_protocolModeCharacteristic; +} + +void NimBLEHIDDevice::setBatteryLevel(uint8_t level) { + m_batteryLevelCharacteristic->setValue(&level, 1); +} +/* + * @brief Returns battery level characteristic + * @ return battery level characteristic + *//* +BLECharacteristic* BLEHIDDevice::batteryLevel() { + return m_batteryLevelCharacteristic; +} + + + +BLECharacteristic* BLEHIDDevice::reportMap() { + return m_reportMapCharacteristic; +} + +BLECharacteristic* BLEHIDDevice::pnp() { + return m_pnpCharacteristic; +} + + +BLECharacteristic* BLEHIDDevice::hidInfo() { + return m_hidInfoCharacteristic; +} +*/ +/* + * @brief + */ +NimBLEService* NimBLEHIDDevice::deviceInfo() { + return m_deviceInfoService; +} + +/* + * @brief + */ +NimBLEService* NimBLEHIDDevice::hidService() { + return m_hidService; +} + +/* + * @brief + */ +NimBLEService* NimBLEHIDDevice::batteryService() { + return m_batteryService; +} + +#endif // #if defined(CONFIG_BT_NIMBLE_ROLE_PERIPHERAL) +#endif // #if defined(CONFIG_BT_ENABLED) diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEHIDDevice.h b/lib/libesp32/NimBLE-Arduino/src/NimBLEHIDDevice.h new file mode 100644 index 000000000..3e7a6f759 --- /dev/null +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEHIDDevice.h @@ -0,0 +1,85 @@ +/* + * NimBLEHIDDevice.h + * + * Created: on Oct 06 2020 + * Author wakwak-koba + * + * Originally: + * + * BLEHIDDevice.h + * + * Created on: Jan 03, 2018 + * Author: chegewara + */ + +#ifndef _BLEHIDDEVICE_H_ +#define _BLEHIDDEVICE_H_ + +#include "sdkconfig.h" +#if defined(CONFIG_BT_ENABLED) + +#include "nimconfig.h" +#if defined(CONFIG_BT_NIMBLE_ROLE_BROADCASTER) + +#include "NimBLECharacteristic.h" +#include "NimBLEService.h" +#include "NimBLEDescriptor.h" +#include "HIDTypes.h" + +#define GENERIC_HID 0x03C0 +#define HID_KEYBOARD 0x03C1 +#define HID_MOUSE 0x03C2 +#define HID_JOYSTICK 0x03C3 +#define HID_GAMEPAD 0x03C4 +#define HID_TABLET 0x03C5 +#define HID_CARD_READER 0x03C6 +#define HID_DIGITAL_PEN 0x03C7 +#define HID_BARCODE 0x03C8 + +class NimBLEHIDDevice { +public: + NimBLEHIDDevice(NimBLEServer*); + virtual ~NimBLEHIDDevice(); + + void reportMap(uint8_t* map, uint16_t); + void startServices(); + + NimBLEService* deviceInfo(); + NimBLEService* hidService(); + NimBLEService* batteryService(); + + NimBLECharacteristic* manufacturer(); + void manufacturer(std::string name); + //NimBLECharacteristic* pnp(); + void pnp(uint8_t sig, uint16_t vid, uint16_t pid, uint16_t version); + //NimBLECharacteristic* hidInfo(); + void hidInfo(uint8_t country, uint8_t flags); + //NimBLECharacteristic* batteryLevel(); + void setBatteryLevel(uint8_t level); + + + //NimBLECharacteristic* reportMap(); + NimBLECharacteristic* hidControl(); + NimBLECharacteristic* inputReport(uint8_t reportID); + NimBLECharacteristic* outputReport(uint8_t reportID); + NimBLECharacteristic* featureReport(uint8_t reportID); + NimBLECharacteristic* protocolMode(); + NimBLECharacteristic* bootInput(); + NimBLECharacteristic* bootOutput(); + +private: + NimBLEService* m_deviceInfoService; //0x180a + NimBLEService* m_hidService; //0x1812 + NimBLEService* m_batteryService = 0; //0x180f + + NimBLECharacteristic* m_manufacturerCharacteristic; //0x2a29 + NimBLECharacteristic* m_pnpCharacteristic; //0x2a50 + NimBLECharacteristic* m_hidInfoCharacteristic; //0x2a4a + NimBLECharacteristic* m_reportMapCharacteristic; //0x2a4b + NimBLECharacteristic* m_hidControlCharacteristic; //0x2a4c + NimBLECharacteristic* m_protocolModeCharacteristic; //0x2a4e + NimBLECharacteristic* m_batteryLevelCharacteristic; //0x2a19 +}; +#endif // CONFIG_BT_NIMBLE_ROLE_BROADCASTER +#endif // CONFIG_BT_ENABLED +#endif /* _BLEHIDDEVICE_H_ */ diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteCharacteristic.cpp b/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteCharacteristic.cpp index 154206c73..8bc5090b1 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteCharacteristic.cpp +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteCharacteristic.cpp @@ -38,7 +38,7 @@ static const char* LOG_TAG = "NimBLERemoteCharacteristic"; NimBLERemoteCharacteristic::NimBLERemoteCharacteristic(NimBLERemoteService *pRemoteService, const struct ble_gatt_chr *chr) { - + NIMBLE_LOGD(LOG_TAG, ">> NimBLERemoteCharacteristic()"); switch (chr->uuid.u.type) { case BLE_UUID_TYPE_16: m_uuid = NimBLEUUID(chr->uuid.u16.value); @@ -50,7 +50,6 @@ static const char* LOG_TAG = "NimBLERemoteCharacteristic"; m_uuid = NimBLEUUID(const_cast(&chr->uuid.u128)); break; default: - m_uuid = nullptr; break; } @@ -61,6 +60,8 @@ static const char* LOG_TAG = "NimBLERemoteCharacteristic"; m_notifyCallback = nullptr; m_timestamp = 0; m_valMux = portMUX_INITIALIZER_UNLOCKED; + + NIMBLE_LOGD(LOG_TAG, "<< NimBLERemoteCharacteristic(): %s", m_uuid.toString().c_str()); } // NimBLERemoteCharacteristic @@ -208,15 +209,21 @@ int NimBLERemoteCharacteristic::descriptorDiscCB(uint16_t conn_handle, bool NimBLERemoteCharacteristic::retrieveDescriptors(const NimBLEUUID *uuid_filter) { NIMBLE_LOGD(LOG_TAG, ">> retrieveDescriptors() for characteristic: %s", getUUID().toString().c_str()); + uint16_t endHandle = getRemoteService()->getEndHandle(this); + if(m_handle >= endHandle) { + return false; + } + int rc = 0; ble_task_data_t taskData = {this, xTaskGetCurrentTaskHandle(), 0, nullptr}; desc_filter_t filter = {uuid_filter, &taskData}; rc = ble_gattc_disc_all_dscs(getRemoteService()->getClient()->getConnId(), m_handle, - getRemoteService()->getEndHandle(), + endHandle, NimBLERemoteCharacteristic::descriptorDiscCB, &filter); + if (rc != 0) { NIMBLE_LOGE(LOG_TAG, "ble_gattc_disc_all_chrs: rc=%d %s", rc, NimBLEUtils::returnCodeToString(rc)); return false; @@ -225,12 +232,13 @@ bool NimBLERemoteCharacteristic::retrieveDescriptors(const NimBLEUUID *uuid_filt ulTaskNotifyTake(pdTRUE, portMAX_DELAY); if(taskData.rc != 0) { + NIMBLE_LOGE(LOG_TAG, "ble_gattc_disc_all_chrs: startHandle:%d endHandle:%d taskData.rc=%d %s", m_handle, endHandle, taskData.rc, NimBLEUtils::returnCodeToString(0x0100+taskData.rc)); return false; } return true; NIMBLE_LOGD(LOG_TAG, "<< retrieveDescriptors(): Found %d descriptors.", m_descriptorVector.size()); -} // getDescriptors +} // retrieveDescriptors /** @@ -243,7 +251,7 @@ NimBLERemoteDescriptor* NimBLERemoteCharacteristic::getDescriptor(const NimBLEUU for(auto &it: m_descriptorVector) { if(it->getUUID() == uuid) { - NIMBLE_LOGD(LOG_TAG, "<< getDescriptor: found"); + NIMBLE_LOGD(LOG_TAG, "<< getDescriptor: found the descriptor with uuid: %s", uuid.toString().c_str()); return it; } } @@ -253,7 +261,18 @@ NimBLERemoteDescriptor* NimBLERemoteCharacteristic::getDescriptor(const NimBLEUU if(m_descriptorVector.size() > prev_size) { return m_descriptorVector.back(); } + + // If the request was successful but 16/32 bit descriptor not found + // try again with the 128 bit uuid. + if(uuid.bitSize() == BLE_UUID_TYPE_16 || + uuid.bitSize() == BLE_UUID_TYPE_32) + { + NimBLEUUID uuid128(uuid); + uuid128.to128(); + return getDescriptor(uuid128); + } } + NIMBLE_LOGD(LOG_TAG, "<< getDescriptor: Not found"); return nullptr; } // getDescriptor @@ -447,9 +466,10 @@ std::string NimBLERemoteCharacteristic::readValue(time_t *timestamp) { } } while(rc != 0 && retryCount--); + time_t t = time(nullptr); portENTER_CRITICAL(&m_valMux); m_value = value; - m_timestamp = time(nullptr); + m_timestamp = t; if(timestamp != nullptr) { *timestamp = m_timestamp; } @@ -506,19 +526,19 @@ int NimBLERemoteCharacteristic::onReadCB(uint16_t conn_handle, * @param [in] notifyCallback A callback to be invoked for a notification. * @param [in] response If write response required set this to true. * If NULL is provided then no callback is performed. - * @return true if successful. + * @return false if writing to the descriptor failed. */ bool NimBLERemoteCharacteristic::setNotify(uint16_t val, notify_callback notifyCallback, bool response) { NIMBLE_LOGD(LOG_TAG, ">> setNotify(): %s, %02x", toString().c_str(), val); + m_notifyCallback = notifyCallback; + NimBLERemoteDescriptor* desc = getDescriptor(NimBLEUUID((uint16_t)0x2902)); if(desc == nullptr) { - NIMBLE_LOGE(LOG_TAG, "<< setNotify(): Could not get descriptor"); - return false; + NIMBLE_LOGW(LOG_TAG, "<< setNotify(): Callback set, CCCD not found"); + return true; } - m_notifyCallback = notifyCallback; - NIMBLE_LOGD(LOG_TAG, "<< setNotify()"); return desc->writeValue((uint8_t *)&val, 2, response); @@ -531,7 +551,7 @@ bool NimBLERemoteCharacteristic::setNotify(uint16_t val, notify_callback notifyC * @param [in] notifyCallback A callback to be invoked for a notification. * @param [in] response If true, require a write response from the descriptor write operation. * If NULL is provided then no callback is performed. - * @return true if successful. + * @return false if writing to the descriptor failed. */ bool NimBLERemoteCharacteristic::subscribe(bool notifications, notify_callback notifyCallback, bool response) { if(notifications) { @@ -545,7 +565,7 @@ bool NimBLERemoteCharacteristic::subscribe(bool notifications, notify_callback n /** * @brief Unsubscribe for notifications or indications. * @param [in] response bool if true, require a write response from the descriptor write operation. - * @return true if successful. + * @return false if writing to the descriptor failed. */ bool NimBLERemoteCharacteristic::unsubscribe(bool response) { return setNotify(0x00, nullptr, response); diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteDescriptor.cpp b/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteDescriptor.cpp index 9281e7df7..fc0f06b67 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteDescriptor.cpp +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteDescriptor.cpp @@ -31,6 +31,7 @@ static const char* LOG_TAG = "NimBLERemoteDescriptor"; NimBLERemoteDescriptor::NimBLERemoteDescriptor(NimBLERemoteCharacteristic* pRemoteCharacteristic, const struct ble_gatt_dsc *dsc) { + NIMBLE_LOGD(LOG_TAG, ">> NimBLERemoteDescriptor()"); switch (dsc->uuid.u.type) { case BLE_UUID_TYPE_16: m_uuid = NimBLEUUID(dsc->uuid.u16.value); @@ -42,12 +43,13 @@ NimBLERemoteDescriptor::NimBLERemoteDescriptor(NimBLERemoteCharacteristic* pRemo m_uuid = NimBLEUUID(const_cast(&dsc->uuid.u128)); break; default: - m_uuid = nullptr; break; } m_handle = dsc->handle; m_pRemoteCharacteristic = pRemoteCharacteristic; + + NIMBLE_LOGD(LOG_TAG, "<< NimBLERemoteDescriptor(): %s", m_uuid.toString().c_str()); } diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteService.cpp b/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteService.cpp index 8901175dc..cb20e0b3f 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteService.cpp +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteService.cpp @@ -44,12 +44,11 @@ NimBLERemoteService::NimBLERemoteService(NimBLEClient* pClient, const struct ble m_uuid = NimBLEUUID(const_cast(&service->uuid.u128)); break; default: - m_uuid = nullptr; break; } m_startHandle = service->start_handle; m_endHandle = service->end_handle; - NIMBLE_LOGD(LOG_TAG, "<< NimBLERemoteService()"); + NIMBLE_LOGD(LOG_TAG, "<< NimBLERemoteService(): %s", m_uuid.toString().c_str()); } @@ -95,8 +94,11 @@ NimBLERemoteCharacteristic* NimBLERemoteService::getCharacteristic(const char* u * @return A pointer to the characteristic object, or nullptr if not found. */ NimBLERemoteCharacteristic* NimBLERemoteService::getCharacteristic(const NimBLEUUID &uuid) { + NIMBLE_LOGD(LOG_TAG, ">> getCharacteristic: uuid: %s", uuid.toString().c_str()); + for(auto &it: m_characteristicVector) { if(it->getUUID() == uuid) { + NIMBLE_LOGD(LOG_TAG, "<< getCharacteristic: found the characteristic with uuid: %s", uuid.toString().c_str()); return it; } } @@ -106,8 +108,19 @@ NimBLERemoteCharacteristic* NimBLERemoteService::getCharacteristic(const NimBLEU if(m_characteristicVector.size() > prev_size) { return m_characteristicVector.back(); } + + // If the request was successful but 16/32 bit characteristic not found + // try again with the 128 bit uuid. + if(uuid.bitSize() == BLE_UUID_TYPE_16 || + uuid.bitSize() == BLE_UUID_TYPE_32) + { + NimBLEUUID uuid128(uuid); + uuid128.to128(); + return getCharacteristic(uuid128); + } } + NIMBLE_LOGD(LOG_TAG, "<< getCharacteristic: not found"); return nullptr; } // getCharacteristic @@ -236,6 +249,23 @@ uint16_t NimBLERemoteService::getEndHandle() { return m_endHandle; } // getEndHandle +/** + * @brief Get the end handle of specified NimBLERemoteCharacteristic. + */ + +uint16_t NimBLERemoteService::getEndHandle(NimBLERemoteCharacteristic *pCharacteristic) { + uint16_t endHandle = m_endHandle; + + for(auto &it: m_characteristicVector) { + uint16_t defHandle = it->getDefHandle() - 1; + if(defHandle > pCharacteristic->getDefHandle() && endHandle > defHandle) { + endHandle = defHandle; + } + } + + return endHandle; +} // getEndHandle + /** * @brief Get the service start handle. diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteService.h b/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteService.h index 751c9effb..4920844e4 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteService.h +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLERemoteService.h @@ -70,6 +70,7 @@ private: uint16_t getStartHandle(); uint16_t getEndHandle(); + uint16_t getEndHandle(NimBLERemoteCharacteristic *pCharacteristic); void releaseSemaphores(); // Properties diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEScan.cpp b/lib/libesp32/NimBLE-Arduino/src/NimBLEScan.cpp index dc82de549..122ff3332 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLEScan.cpp +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEScan.cpp @@ -30,7 +30,6 @@ static const char* LOG_TAG = "NimBLEScan"; * @brief Scan constuctor. */ NimBLEScan::NimBLEScan() { - m_own_addr_type = 0; m_scan_params.filter_policy = BLE_HCI_SCAN_FILT_NO_WL; m_scan_params.passive = 1; // If set, don’t send scan requests to advertisers (i.e., don’t request additional advertising data). m_scan_params.itvl = 0; // This is defined as the time interval from when the Controller started its last LE scan until it begins the subsequent LE scan. (units=0.625 msec) @@ -38,9 +37,10 @@ NimBLEScan::NimBLEScan() { m_scan_params.limited = 0; // If set, only discover devices in limited discoverable mode. m_scan_params.filter_duplicates = 0; // If set, the controller ignores all but the first advertisement from each device. m_pAdvertisedDeviceCallbacks = nullptr; - m_stopped = true; + m_ignoreResults = false; m_wantDuplicates = false; m_pTaskData = nullptr; + m_duration = BLE_HS_FOREVER; // make sure this is non-zero in the event of a host reset } @@ -63,8 +63,8 @@ NimBLEScan::~NimBLEScan() { switch(event->type) { case BLE_GAP_EVENT_DISC: { - if(pScan->m_stopped) { - NIMBLE_LOGE(LOG_TAG, "Scan stop called, ignoring results."); + if(pScan->m_ignoreResults) { + NIMBLE_LOGE(LOG_TAG, "Scan op in progress - ignoring results"); return 0; } @@ -129,7 +129,6 @@ NimBLEScan::~NimBLEScan() { pScan->m_scanCompleteCB(pScan->m_scanResults); } - pScan->m_stopped = true; if(pScan->m_pTaskData != nullptr) { pScan->m_pTaskData->rc = event->disc_complete.reason; xTaskNotifyGive(pScan->m_pTaskData->task); @@ -238,7 +237,7 @@ void NimBLEScan::setWindow(uint16_t windowMSecs) { * @return true if scanning or scan starting. */ bool NimBLEScan::isScanning() { - return !m_stopped; + return ble_gap_disc_active(); } @@ -252,25 +251,6 @@ bool NimBLEScan::isScanning() { bool NimBLEScan::start(uint32_t duration, void (*scanCompleteCB)(NimBLEScanResults), bool is_continue) { NIMBLE_LOGD(LOG_TAG, ">> start(duration=%d)", duration); - // If Host is not synced we cannot start scanning. - if(!NimBLEDevice::m_synced) { - NIMBLE_LOGC(LOG_TAG, "Host reset, wait for sync."); - return false; - } - - if(ble_gap_conn_active()) { - NIMBLE_LOGE(LOG_TAG, "Connection in progress - must wait."); - return false; - } - - // If we are already scanning don't start again or we will get stuck on the semaphore. - if(!m_stopped || ble_gap_disc_active()) { // double check - can cause host reset. - NIMBLE_LOGE(LOG_TAG, "Scan already in progress"); - return false; - } - - m_stopped = false; - // Save the callback to be invoked when the scan completes. m_scanCompleteCB = scanCompleteCB; // Save the duration in the case that the host is reset so we can reuse it. @@ -281,32 +261,51 @@ bool NimBLEScan::start(uint32_t duration, void (*scanCompleteCB)(NimBLEScanResul duration = BLE_HS_FOREVER; } else{ - duration = duration*1000; // convert duration to milliseconds + // convert duration to milliseconds + duration = duration * 1000; } - // if we are connecting to devices that are advertising even after being connected, multiconnecting peripherals - // then we should not clear vector or we will connect the same device few times + // Set the flag to ignore the results while we are deleting the vector if(!is_continue) { - clearResults(); + m_ignoreResults = true; } - int rc = 0; - do{ - rc = ble_gap_disc(m_own_addr_type, duration, &m_scan_params, - NimBLEScan::handleGapEvent, this); - if(rc == BLE_HS_EBUSY) { - vTaskDelay(1 / portTICK_PERIOD_MS); - } - } while(rc == BLE_HS_EBUSY); + int rc = ble_gap_disc(NimBLEDevice::m_own_addr_type, duration, &m_scan_params, + NimBLEScan::handleGapEvent, this); - if (rc != 0 && rc != BLE_HS_EDONE) { - NIMBLE_LOGE(LOG_TAG, "Error initiating GAP discovery procedure; rc=%d, %s", - rc, NimBLEUtils::returnCodeToString(rc)); - m_stopped = true; + switch(rc) { + case 0: + if(!is_continue) { + clearResults(); + } + break; + + case BLE_HS_EALREADY: + break; + + case BLE_HS_EBUSY: + NIMBLE_LOGE(LOG_TAG, "Unable to scan - connection in progress."); + break; + + case BLE_HS_ETIMEOUT_HCI: + case BLE_HS_EOS: + case BLE_HS_ECONTROLLER: + case BLE_HS_ENOTSYNCED: + NIMBLE_LOGC(LOG_TAG, "Unable to scan - Host Reset"); + break; + + default: + NIMBLE_LOGE(LOG_TAG, "Error initiating GAP discovery procedure; rc=%d, %s", + rc, NimBLEUtils::returnCodeToString(rc)); + break; + } + + m_ignoreResults = false; + NIMBLE_LOGD(LOG_TAG, "<< start()"); + + if(rc != 0 && rc != BLE_HS_EALREADY) { return false; } - - NIMBLE_LOGD(LOG_TAG, "<< start()"); return true; } // start @@ -347,8 +346,6 @@ bool NimBLEScan::stop() { return false; } - m_stopped = true; - if (rc != BLE_HS_EALREADY && m_scanCompleteCB != nullptr) { m_scanCompleteCB(m_scanResults); } @@ -381,13 +378,25 @@ void NimBLEScan::erase(const NimBLEAddress &address) { /** - * @brief If the host reset the scan will have stopped so we should set the flag as stopped. + * @brief Called when host reset, we set a flag to stop scanning until synced. */ void NimBLEScan::onHostReset() { - m_stopped = true; + m_ignoreResults = true; } +/** + * @brief If the host reset and re-synced this is called. + * If the application was scanning indefinitely with a callback, restart it. + */ +void NimBLEScan::onHostSync() { + m_ignoreResults = false; + + if(m_duration == 0 && m_pAdvertisedDeviceCallbacks != nullptr) { + start(m_duration, m_scanCompleteCB); + } +} + /** * @brief Get the results of the scan. * @return NimBLEScanResults object. diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEScan.h b/lib/libesp32/NimBLE-Arduino/src/NimBLEScan.h index 822629025..9007a7dd8 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLEScan.h +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEScan.h @@ -83,12 +83,12 @@ private: ~NimBLEScan(); static int handleGapEvent(ble_gap_event* event, void* arg); void onHostReset(); + void onHostSync(); NimBLEAdvertisedDeviceCallbacks* m_pAdvertisedDeviceCallbacks = nullptr; void (*m_scanCompleteCB)(NimBLEScanResults scanResults); ble_gap_disc_params m_scan_params; - uint8_t m_own_addr_type; - bool m_stopped; + bool m_ignoreResults; bool m_wantDuplicates; NimBLEScanResults m_scanResults; uint32_t m_duration; diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEServer.cpp b/lib/libesp32/NimBLE-Arduino/src/NimBLEServer.cpp index 8c75192a9..655511aea 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLEServer.cpp +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEServer.cpp @@ -296,6 +296,7 @@ size_t NimBLEServer::getConnectedCount() { } server->m_pServerCallbacks->onDisconnect(server); + server->m_pServerCallbacks->onDisconnect(server, &event->disconnect.conn); if(server->m_advertiseOnDisconnect) { server->startAdvertising(); @@ -658,6 +659,10 @@ void NimBLEServerCallbacks::onDisconnect(NimBLEServer* pServer) { NIMBLE_LOGD("NimBLEServerCallbacks", "onDisconnect(): Default"); } // onDisconnect +void NimBLEServerCallbacks::onDisconnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) { + NIMBLE_LOGD("NimBLEServerCallbacks", "onDisconnect(): Default"); +} // onDisconnect + uint32_t NimBLEServerCallbacks::onPassKeyRequest(){ NIMBLE_LOGD("NimBLEServerCallbacks", "onPassKeyRequest: default: 123456"); return 123456; diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEServer.h b/lib/libesp32/NimBLE-Arduino/src/NimBLEServer.h index 1fa24b23c..bedf9cf58 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLEServer.h +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEServer.h @@ -114,6 +114,15 @@ public: */ virtual void onDisconnect(NimBLEServer* pServer); + /** + * @brief Handle a client disconnection. + * This is called when a client discconnects. + * @param [in] pServer A pointer to the %BLE server that received the client disconnection. + * @param [in] desc A pointer to the connection description structure containig information + * about the connection. + */ + virtual void onDisconnect(NimBLEServer* pServer, ble_gap_conn_desc* desc); + /** * @brief Called when a client requests a passkey for pairing. * @return The passkey to be sent to the client. diff --git a/lib/libesp32/NimBLE-Arduino/src/NimBLEUUID.cpp b/lib/libesp32/NimBLE-Arduino/src/NimBLEUUID.cpp index 1b00a3237..21ff27047 100644 --- a/lib/libesp32/NimBLE-Arduino/src/NimBLEUUID.cpp +++ b/lib/libesp32/NimBLE-Arduino/src/NimBLEUUID.cpp @@ -264,6 +264,37 @@ std::string NimBLEUUID::toString() const { */ bool NimBLEUUID::operator ==(const NimBLEUUID & rhs) const { if(m_valueSet && rhs.m_valueSet) { + NIMBLE_LOGD(LOG_TAG,"Comparing UUIDs; type %u to %u; UUID %s to %s", + m_uuid.u.type, rhs.m_uuid.u.type, + this->toString().c_str(), rhs.toString().c_str()); + + if(m_uuid.u.type != rhs.m_uuid.u.type) { + uint8_t uuidBase[16] = { + 0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + if(m_uuid.u.type == BLE_UUID_TYPE_128){ + if(rhs.m_uuid.u.type == BLE_UUID_TYPE_16){ + memcpy(uuidBase+12, &rhs.m_uuid.u16.value, 2); + } else if (rhs.m_uuid.u.type == BLE_UUID_TYPE_32){ + memcpy(uuidBase+12, &rhs.m_uuid.u32.value, 4); + } + return memcmp(m_uuid.u128.value,uuidBase,16) == 0; + + } else if(rhs.m_uuid.u.type == BLE_UUID_TYPE_128) { + if(m_uuid.u.type == BLE_UUID_TYPE_16){ + memcpy(uuidBase+12, &m_uuid.u16.value, 2); + } else if (m_uuid.u.type == BLE_UUID_TYPE_32){ + memcpy(uuidBase+12, &m_uuid.u32.value, 4); + } + return memcmp(rhs.m_uuid.u128.value,uuidBase,16) == 0; + + } else { + return false; + } + } + return ble_uuid_cmp(&m_uuid.u, &rhs.m_uuid.u) == 0; } diff --git a/lib/libesp32/NimBLE-Arduino/src/esp-hci/src/esp_nimble_hci.c b/lib/libesp32/NimBLE-Arduino/src/esp-hci/src/esp_nimble_hci.c index 0ba15e9c3..98c133fa5 100644 --- a/lib/libesp32/NimBLE-Arduino/src/esp-hci/src/esp_nimble_hci.c +++ b/lib/libesp32/NimBLE-Arduino/src/esp-hci/src/esp_nimble_hci.c @@ -19,6 +19,13 @@ * under the License. */ +/* + * This file has been modified by Ryan Powell, aka h2zero. + * The modifications are for the purpose of improving performance and support + * for Esprssif versions used by the ardruino-esp32 core that are less current + * than the esp-idf releases. + */ + #include #include "sysinit/sysinit.h" #include "nimble/hci_common.h" @@ -30,8 +37,10 @@ #include "esp_bt.h" #include "freertos/semphr.h" #include "esp_compiler.h" +/* IPC is used to improve performance when calls come from a processor not running the NimBLE stack */ +/* but does not exist for solo */ #ifndef CONFIG_FREERTOS_UNICORE -#include "esp_ipc.h" + #include "esp_ipc.h" #endif #define NIMBLE_VHCI_TIMEOUT_MS 2000 @@ -81,31 +90,40 @@ void ble_hci_trans_cfg_hs(ble_hci_trans_rx_cmd_fn *cmd_cb, ble_hci_rx_acl_hs_arg = acl_arg; } -void ble_hci_trans_hs_cmd_tx_on_core_0(void *arg) +/* Added; Called from the core NimBLE is running on, not used for unicore */ +#ifndef CONFIG_FREERTOS_UNICORE +void ble_hci_trans_hs_cmd_tx_on_core(void *arg) { - uint8_t *cmd = arg; - uint16_t len = BLE_HCI_CMD_HDR_LEN + cmd[3] + 1; - esp_vhci_host_send_packet(cmd, len); + // Ugly but necessary as the arduino core does not provide enough IPC stack for variables. + esp_vhci_host_send_packet((uint8_t*)arg, *((uint8_t*)arg + 3) + 1 + BLE_HCI_CMD_HDR_LEN); } +#endif +/* Modified to use ipc calls in arduino to correct performance issues */ int ble_hci_trans_hs_cmd_tx(uint8_t *cmd) { + uint16_t len; uint8_t rc = 0; assert(cmd != NULL); *cmd = BLE_HCI_UART_H4_CMD; + len = BLE_HCI_CMD_HDR_LEN + cmd[3] + 1; if (!esp_vhci_host_check_send_available()) { ESP_LOGD(TAG, "Controller not ready to receive packets"); } if (xSemaphoreTake(vhci_send_sem, NIMBLE_VHCI_TIMEOUT_MS / portTICK_PERIOD_MS) == pdTRUE) { - if (xPortGetCoreID() != 0) { +/* esp_ipc_call_blocking does not exist for solo */ #ifndef CONFIG_FREERTOS_UNICORE - esp_ipc_call_blocking(0, ble_hci_trans_hs_cmd_tx_on_core_0, cmd); -#endif + if (xPortGetCoreID() != CONFIG_BT_NIMBLE_PINNED_TO_CORE) { + esp_ipc_call_blocking(CONFIG_BT_NIMBLE_PINNED_TO_CORE, + ble_hci_trans_hs_cmd_tx_on_core, cmd); } else { - ble_hci_trans_hs_cmd_tx_on_core_0(cmd); + esp_vhci_host_send_packet(cmd, len); } +#else /* Unicore */ + esp_vhci_host_send_packet(cmd, len); +#endif } else { rc = BLE_HS_ETIMEOUT_HCI; } @@ -124,21 +142,21 @@ int ble_hci_trans_ll_evt_tx(uint8_t *hci_ev) return rc; } -void ble_hci_trans_hs_acl_tx_on_core_0(void *arg) +/* Added; Called from the core NimBLE is running on, not used for unicore */ +#ifndef CONFIG_FREERTOS_UNICORE +void ble_hci_trans_hs_acl_tx_on_core(void *arg) { - uint8_t data[MYNEWT_VAL(BLE_ACL_BUF_SIZE) + 1]; - struct os_mbuf *om = arg; - uint16_t len = 1 + OS_MBUF_PKTLEN(om); - - data[0] = BLE_HCI_UART_H4_ACL; - os_mbuf_copydata(om, 0, OS_MBUF_PKTLEN(om), &data[1]); - - esp_vhci_host_send_packet(data, len); + // Ugly but necessary as the arduino core does not provide enough IPC stack for variables. + esp_vhci_host_send_packet((uint8_t*)arg + 2, *(uint16_t*)arg); } +#endif +/* Modified to use ipc calls in arduino to correct performance issues */ int ble_hci_trans_hs_acl_tx(struct os_mbuf *om) { - uint8_t rc = 0; + uint16_t len = 0; + uint8_t data[MYNEWT_VAL(BLE_ACL_BUF_SIZE) + 3], rc = 0; + bool tx_using_nimble_core = 0; /* If this packet is zero length, just free it */ if (OS_MBUF_PKTLEN(om) == 0) { os_mbuf_free_chain(om); @@ -149,14 +167,36 @@ int ble_hci_trans_hs_acl_tx(struct os_mbuf *om) ESP_LOGD(TAG, "Controller not ready to receive packets"); } - if (xSemaphoreTake(vhci_send_sem, NIMBLE_VHCI_TIMEOUT_MS / portTICK_PERIOD_MS) == pdTRUE) { - if (xPortGetCoreID() != 0) { + len = 1 + OS_MBUF_PKTLEN(om); +/* Don't check core ID if unicore */ #ifndef CONFIG_FREERTOS_UNICORE - esp_ipc_call_blocking(0, ble_hci_trans_hs_acl_tx_on_core_0, om); + tx_using_nimble_core = xPortGetCoreID() != CONFIG_BT_NIMBLE_PINNED_TO_CORE; + if (tx_using_nimble_core) { + data[0] = len; + data[1] = (len >> 8); + data[2] = BLE_HCI_UART_H4_ACL; + os_mbuf_copydata(om, 0, OS_MBUF_PKTLEN(om), &data[3]); + } else { + data[0] = BLE_HCI_UART_H4_ACL; + os_mbuf_copydata(om, 0, OS_MBUF_PKTLEN(om), &data[1]); + } +#else /* Unicore */ + data[0] = BLE_HCI_UART_H4_ACL; + os_mbuf_copydata(om, 0, OS_MBUF_PKTLEN(om), &data[1]); #endif + + if (xSemaphoreTake(vhci_send_sem, NIMBLE_VHCI_TIMEOUT_MS / portTICK_PERIOD_MS) == pdTRUE) { +/* esp_ipc_call_blocking does not exist for solo */ +#ifndef CONFIG_FREERTOS_UNICORE + if (tx_using_nimble_core) { + esp_ipc_call_blocking(CONFIG_BT_NIMBLE_PINNED_TO_CORE, + ble_hci_trans_hs_acl_tx_on_core, data); } else { - ble_hci_trans_hs_acl_tx_on_core_0(om); + esp_vhci_host_send_packet(data, len); } +#else /* Unicore */ + esp_vhci_host_send_packet(data, len); +#endif } else { rc = BLE_HS_ETIMEOUT_HCI; } @@ -367,6 +407,13 @@ static int host_rcv_pkt(uint8_t *data, uint16_t len) totlen = BLE_HCI_EVENT_HDR_LEN + data[2]; assert(totlen <= UINT8_MAX + BLE_HCI_EVENT_HDR_LEN); + if (totlen > MYNEWT_VAL(BLE_HCI_EVT_BUF_SIZE)) { + ESP_LOGE(TAG, "Received HCI data length at host (%d) exceeds maximum configured HCI event buffer size (%d).", + totlen, MYNEWT_VAL(BLE_HCI_EVT_BUF_SIZE)); + ble_hs_sched_reset(BLE_HS_ECONTROLLER); + return 0; + } + if (data[1] == BLE_HCI_EVCODE_HW_ERROR) { assert(0); } @@ -476,6 +523,9 @@ esp_err_t esp_nimble_hci_and_controller_init(void) esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + /* Added to ensure BLE only mode */ + bt_cfg.mode = ESP_BT_MODE_BLE; + /* Added to set max connections from nimconfig */ bt_cfg.ble_max_conn = CONFIG_BT_NIMBLE_MAX_CONNECTIONS; if ((ret = esp_bt_controller_init(&bt_cfg)) != ESP_OK) { @@ -485,6 +535,7 @@ esp_err_t esp_nimble_hci_and_controller_init(void) if ((ret = esp_bt_controller_enable(ESP_BT_MODE_BLE)) != ESP_OK) { return ret; } + return esp_nimble_hci_init(); } diff --git a/lib/libesp32/NimBLE-Arduino/src/esp_nimble_cfg.h b/lib/libesp32/NimBLE-Arduino/src/esp_nimble_cfg.h index 384ec4a56..bafbeac8f 100644 --- a/lib/libesp32/NimBLE-Arduino/src/esp_nimble_cfg.h +++ b/lib/libesp32/NimBLE-Arduino/src/esp_nimble_cfg.h @@ -483,6 +483,10 @@ #define MYNEWT_VAL_BLE_L2CAP_COC_MAX_NUM CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM #endif +#ifndef MYNEWT_VAL_BLE_L2CAP_COC_MPS +#define MYNEWT_VAL_BLE_L2CAP_COC_MPS (MYNEWT_VAL_MSYS_1_BLOCK_SIZE - 8) +#endif + #ifndef MYNEWT_VAL_BLE_L2CAP_JOIN_RX_FRAGS #define MYNEWT_VAL_BLE_L2CAP_JOIN_RX_FRAGS (1) #endif diff --git a/lib/libesp32/NimBLE-Arduino/src/nimble/host/src/ble_eddystone.c b/lib/libesp32/NimBLE-Arduino/src/nimble/host/src/ble_eddystone.c index 7d80d134d..eccb3e988 100644 --- a/lib/libesp32/NimBLE-Arduino/src/nimble/host/src/ble_eddystone.c +++ b/lib/libesp32/NimBLE-Arduino/src/nimble/host/src/ble_eddystone.c @@ -76,7 +76,7 @@ ble_eddystone_set_adv_data_gen(struct ble_hs_adv_fields *adv_fields, if (adv_fields->num_uuids16 > BLE_EDDYSTONE_MAX_UUIDS16) { return BLE_HS_EINVAL; } - if (svc_data_len > BLE_EDDYSTONE_MAX_SVC_DATA_LEN) { + if (svc_data_len > (BLE_EDDYSTONE_MAX_SVC_DATA_LEN - BLE_EDDYSTONE_SVC_DATA_BASE_SZ)) { return BLE_HS_EINVAL; } if (adv_fields->num_uuids16 > 0 && !adv_fields->uuids16_is_complete) { diff --git a/lib/libesp32/NimBLE-Arduino/src/nimble/host/src/ble_gap.c b/lib/libesp32/NimBLE-Arduino/src/nimble/host/src/ble_gap.c index 09ae27047..d77ff6a87 100644 --- a/lib/libesp32/NimBLE-Arduino/src/nimble/host/src/ble_gap.c +++ b/lib/libesp32/NimBLE-Arduino/src/nimble/host/src/ble_gap.c @@ -1017,7 +1017,7 @@ ble_gap_master_failed(int status) #endif default: - BLE_HS_DBG_ASSERT(0); + //BLE_HS_DBG_ASSERT(0); break; } } @@ -1458,8 +1458,8 @@ ble_gap_rx_periodic_adv_rpt(struct hci_le_subev_periodic_adv_rpt *evt) { struct ble_hs_periodic_sync *psync; struct ble_gap_event event; - ble_gap_event_fn *cb; - void *cb_arg; + ble_gap_event_fn *cb = NULL; + void *cb_arg = NULL; ble_hs_lock(); psync = ble_hs_periodic_sync_find_by_handle(evt->sync_handle); diff --git a/lib/libesp32/NimBLE-Arduino/src/nimble/host/src/ble_hs_conn.c b/lib/libesp32/NimBLE-Arduino/src/nimble/host/src/ble_hs_conn.c index eb65e3288..b45128e23 100644 --- a/lib/libesp32/NimBLE-Arduino/src/nimble/host/src/ble_hs_conn.c +++ b/lib/libesp32/NimBLE-Arduino/src/nimble/host/src/ble_hs_conn.c @@ -470,29 +470,52 @@ ble_hs_conn_timer(void) int32_t time_diff; uint16_t conn_handle; - conn_handle = BLE_HS_CONN_HANDLE_NONE; - next_exp_in = BLE_HS_FOREVER; - now = ble_npl_time_get(); + for (;;) { + conn_handle = BLE_HS_CONN_HANDLE_NONE; + next_exp_in = BLE_HS_FOREVER; + now = ble_npl_time_get(); - ble_hs_lock(); + ble_hs_lock(); - /* This loop performs one of two tasks: - * 1. Determine if any connections need to be terminated due to timeout. - * If so, break out of the loop and terminate the connection. This - * function will need to be executed again. - * 2. Otherwise, determine when the next timeout will occur. - */ - SLIST_FOREACH(conn, &ble_hs_conns, bhc_next) { - if (!(conn->bhc_flags & BLE_HS_CONN_F_TERMINATING)) { + /* This loop performs one of two tasks: + * 1. Determine if any connections need to be terminated due to timeout. + * If so, break out of the loop and terminate the connection. This + * function will need to be executed again. + * 2. Otherwise, determine when the next timeout will occur. + */ + SLIST_FOREACH(conn, &ble_hs_conns, bhc_next) { + if (!(conn->bhc_flags & BLE_HS_CONN_F_TERMINATING)) { #if MYNEWT_VAL(BLE_L2CAP_RX_FRAG_TIMEOUT) != 0 - /* Check each connection's rx fragment timer. If too much time - * passes after a partial packet is received, the connection is - * terminated. - */ - if (conn->bhc_rx_chan != NULL) { - time_diff = conn->bhc_rx_timeout - now; + /* Check each connection's rx fragment timer. If too much time + * passes after a partial packet is received, the connection is + * terminated. + */ + if (conn->bhc_rx_chan != NULL) { + time_diff = conn->bhc_rx_timeout - now; + if (time_diff <= 0) { + /* ACL reassembly has timed out. Remember the connection + * handle so it can be terminated after the mutex is + * unlocked. + */ + conn_handle = conn->bhc_handle; + break; + } + + /* Determine if this connection is the soonest to time out. */ + if (time_diff < next_exp_in) { + next_exp_in = time_diff; + } + } +#endif + +#if BLE_HS_ATT_SVR_QUEUED_WRITE_TMO + /* Check each connection's rx queued write timer. If too much + * time passes after a prep write is received, the queue is + * cleared. + */ + time_diff = ble_att_svr_ticks_until_tmo(&conn->bhc_att_svr, now); if (time_diff <= 0) { /* ACL reassembly has timed out. Remember the connection * handle so it can be terminated after the mutex is @@ -506,45 +529,22 @@ ble_hs_conn_timer(void) if (time_diff < next_exp_in) { next_exp_in = time_diff; } - } #endif - -#if BLE_HS_ATT_SVR_QUEUED_WRITE_TMO - /* Check each connection's rx queued write timer. If too much - * time passes after a prep write is received, the queue is - * cleared. - */ - time_diff = ble_att_svr_ticks_until_tmo(&conn->bhc_att_svr, now); - if (time_diff <= 0) { - /* ACL reassembly has timed out. Remember the connection - * handle so it can be terminated after the mutex is - * unlocked. - */ - conn_handle = conn->bhc_handle; - break; } - - /* Determine if this connection is the soonest to time out. */ - if (time_diff < next_exp_in) { - next_exp_in = time_diff; - } -#endif } + + ble_hs_unlock(); + + /* If a connection has timed out, terminate it. We need to repeatedly + * call this function again to determine when the next timeout is. + */ + if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { + ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM); + continue; + } + + return next_exp_in; } - - ble_hs_unlock(); - - /* If a connection has timed out, terminate it. We need to recursively - * call this function again to determine when the next timeout is. This - * is a tail-recursive call, so it should be optimized to execute in the - * same stack frame. - */ - if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { - ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM); - return ble_hs_conn_timer(); - } - - return next_exp_in; } int diff --git a/lib/libesp32/NimBLE-Arduino/src/nimble/host/store/config/src/ble_store_nvs.c b/lib/libesp32/NimBLE-Arduino/src/nimble/host/store/config/src/ble_store_nvs.c index a6fea44c7..13dae8d2b 100644 --- a/lib/libesp32/NimBLE-Arduino/src/nimble/host/store/config/src/ble_store_nvs.c +++ b/lib/libesp32/NimBLE-Arduino/src/nimble/host/store/config/src/ble_store_nvs.c @@ -180,7 +180,9 @@ static int get_nvs_db_attribute(int obj_type, bool empty, void *value, int num_value) { union ble_store_value cur = {0}; +#if MYNEWT_VAL(BLE_HOST_BASED_PRIVACY) struct ble_hs_dev_records p_dev_rec = {0}; +#endif esp_err_t err; int i, count = 0, max_limit = 0; char key_string[NIMBLE_NVS_STR_NAME_MAX_LEN]; @@ -190,11 +192,15 @@ get_nvs_db_attribute(int obj_type, bool empty, void *value, int num_value) for (i = 1; i <= max_limit; i++) { get_nvs_key_string(obj_type, i, key_string); +#if MYNEWT_VAL(BLE_HOST_BASED_PRIVACY) if (obj_type != BLE_STORE_OBJ_TYPE_PEER_DEV_REC) { +#endif err = get_nvs_db_value(obj_type, key_string, &cur); +#if MYNEWT_VAL(BLE_HOST_BASED_PRIVACY) } else { err = get_nvs_peer_record(key_string, &p_dev_rec); } +#endif /* Check if the user is searching for empty index to write to */ if (err == ESP_ERR_NVS_NOT_FOUND) { if (empty) { @@ -206,10 +212,13 @@ get_nvs_db_attribute(int obj_type, bool empty, void *value, int num_value) /* If user has provided value, then the purpose is to find * non-matching entry from NVS */ if (value) { +#if MYNEWT_VAL(BLE_HOST_BASED_PRIVACY) if (obj_type == BLE_STORE_OBJ_TYPE_PEER_DEV_REC) { err = get_nvs_matching_index(&p_dev_rec, value, num_value, sizeof(struct ble_hs_dev_records)); - } else { + } else +#endif + { if (obj_type != BLE_STORE_OBJ_TYPE_CCCD) { err = get_nvs_matching_index(&cur.sec, value, num_value, sizeof(struct ble_store_value_sec)); @@ -376,7 +385,9 @@ populate_db_from_nvs(int obj_type, void *dst, int *db_num) { uint8_t *db_item = (uint8_t *)dst; union ble_store_value cur = {0}; +#if MYNEWT_VAL(BLE_HOST_BASED_PRIVACY) struct ble_hs_dev_records p_dev_rec = {0}; +#endif esp_err_t err; int i; @@ -385,8 +396,9 @@ populate_db_from_nvs(int obj_type, void *dst, int *db_num) for (i = 1; i <= get_nvs_max_obj_value(obj_type); i++) { get_nvs_key_string(obj_type, i, key_string); +#if MYNEWT_VAL(BLE_HOST_BASED_PRIVACY) if (obj_type != BLE_STORE_OBJ_TYPE_PEER_DEV_REC) { - +#endif err = get_nvs_db_value(obj_type, key_string, &cur); if (err == ESP_ERR_NVS_NOT_FOUND) { continue; @@ -394,6 +406,7 @@ populate_db_from_nvs(int obj_type, void *dst, int *db_num) ESP_LOGE(TAG, "NVS read operation failed !!"); return -1; } +#if MYNEWT_VAL(BLE_HOST_BASED_PRIVACY) } else { err = get_nvs_peer_record(key_string, &p_dev_rec); if (err == ESP_ERR_NVS_NOT_FOUND) { @@ -410,7 +423,9 @@ populate_db_from_nvs(int obj_type, void *dst, int *db_num) memcpy(db_item, &p_dev_rec, sizeof(struct ble_hs_dev_records)); db_item += sizeof(struct ble_hs_dev_records); (*db_num)++; - } else { + } else +#endif + { if (obj_type == BLE_STORE_OBJ_TYPE_CCCD) { ESP_LOGD(TAG, "CCCD in RAM is filled up from NVS index = %d", i); memcpy(db_item, &cur.cccd, sizeof(struct ble_store_value_cccd)); @@ -492,6 +507,11 @@ int ble_store_config_persist_cccds(void) union ble_store_value val; nvs_count = get_nvs_db_attribute(BLE_STORE_OBJ_TYPE_CCCD, 0, NULL, 0); + if (nvs_count == -1) { + ESP_LOGE(TAG, "NVS operation failed while persisting CCCD"); + return BLE_HS_ESTORE_FAIL; + } + if (nvs_count < ble_store_config_num_cccds) { /* NVS db count less than RAM count, write operation */ @@ -518,6 +538,11 @@ int ble_store_config_persist_peer_secs(void) union ble_store_value val; nvs_count = get_nvs_db_attribute(BLE_STORE_OBJ_TYPE_PEER_SEC, 0, NULL, 0); + if (nvs_count == -1) { + ESP_LOGE(TAG, "NVS operation failed while persisting peer sec"); + return BLE_HS_ESTORE_FAIL; + } + if (nvs_count < ble_store_config_num_peer_secs) { /* NVS db count less than RAM count, write operation */ @@ -544,6 +569,11 @@ int ble_store_config_persist_our_secs(void) union ble_store_value val; nvs_count = get_nvs_db_attribute(BLE_STORE_OBJ_TYPE_OUR_SEC, 0, NULL, 0); + if (nvs_count == -1) { + ESP_LOGE(TAG, "NVS operation failed while persisting our sec"); + return BLE_HS_ESTORE_FAIL; + } + if (nvs_count < ble_store_config_num_our_secs) { /* NVS db count less than RAM count, write operation */ @@ -573,7 +603,13 @@ int ble_store_persist_peer_records(void) struct ble_hs_dev_records *peer_dev_rec = ble_rpa_get_peer_dev_records(); nvs_count = get_nvs_db_attribute(BLE_STORE_OBJ_TYPE_PEER_DEV_REC, 0, NULL, 0); + if (nvs_count == -1) { + ESP_LOGE(TAG, "NVS operation failed while persisting peer_dev_rec"); + return BLE_HS_ESTORE_FAIL; + } + if (nvs_count < ble_store_num_peer_dev_rec) { + /* NVS db count less than RAM count, write operation */ ESP_LOGD(TAG, "Persisting peer dev record to NVS..."); peer_rec = peer_dev_rec[ble_store_num_peer_dev_rec - 1]; diff --git a/lib/libesp32/NimBLE-Arduino/src/nimconfig.h b/lib/libesp32/NimBLE-Arduino/src/nimconfig.h index 2f10fa2fc..d90921fa1 100644 --- a/lib/libesp32/NimBLE-Arduino/src/nimconfig.h +++ b/lib/libesp32/NimBLE-Arduino/src/nimconfig.h @@ -15,8 +15,13 @@ * This converts them to "CONFIG_BT_NIMBLE_" format used in the latest IDF. */ -/* Detect if using ESP-IDF or Arduino (Arduino won't have these defines in sdkconfig) */ -#if defined(CONFIG_BT_NIMBLE_TASK_STACK_SIZE) || defined(CONFIG_NIMBLE_TASK_STACK_SIZE) +/* Detect if using ESP-IDF or Arduino (Arduino won't have these defines in sdkconfig) + * + * Note: We do not use #ifdef CONFIG_BT_NIMBLE_ENABLED since we cannot enable NimBLE when using + * Arduino as a component and the esp-nimble-compnent, so we check if other config options are defined. + * We also need to use a config parameter that must be present and not likely defined in the command line. + */ +#if defined(CONFIG_BT_NIMBLE_GAP_DEVICE_NAME_MAX_LEN) || defined(CONFIG_NIMBLE_GAP_DEVICE_NAME_MAX_LEN) #if defined(CONFIG_NIMBLE_ENABLED) && !defined(CONFIG_BT_NIMBLE_ENABLED) #define CONFIG_BT_NIMBLE_ENABLED @@ -51,22 +56,30 @@ /** @brief Comment out if not using NimBLE Client functions \n * Reduces flash size by approx. 7kB. */ +#ifndef CONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED #define CONFIG_BT_NIMBLE_ROLE_CENTRAL +#endif /** @brief Comment out if not using NimBLE Scan functions \n * Reduces flash size by approx. 26kB. */ +#ifndef CONFIG_BT_NIMBLE_ROLE_OBSERVER_DISABLED #define CONFIG_BT_NIMBLE_ROLE_OBSERVER +#endif /** @brief Comment out if not using NimBLE Server functions \n * Reduces flash size by approx. 16kB. */ -// #define CONFIG_BT_NIMBLE_ROLE_PERIPHERAL +#ifndef CONFIG_BT_NIMBLE_ROLE_PERIPHERAL_DISABLED +#define CONFIG_BT_NIMBLE_ROLE_PERIPHERAL +#endif /** @brief Comment out if not using NimBLE Advertising functions \n * Reduces flash size by approx. 5kB. */ -// #define CONFIG_BT_NIMBLE_ROLE_BROADCASTER +#ifndef CONFIG_BT_NIMBLE_ROLE_BROADCASTER_DISABLED +#define CONFIG_BT_NIMBLE_ROLE_BROADCASTER +#endif /* Uncomment to see debug log messages from the NimBLE host * Uses approx. 32kB of flash memory. @@ -89,29 +102,46 @@ // #define CONFIG_NIMBLE_CPP_ENABLE_ADVERTISMENT_TYPE_TEXT /** @brief Sets the core NimBLE host runs on */ +#ifndef CONFIG_BT_NIMBLE_PINNED_TO_CORE #define CONFIG_BT_NIMBLE_PINNED_TO_CORE 0 +#endif /** @brief Sets the stack size for the NimBLE host task */ +#ifndef CONFIG_BT_NIMBLE_TASK_STACK_SIZE #define CONFIG_BT_NIMBLE_TASK_STACK_SIZE 4096 +#endif /** * @brief Sets the memory pool where NimBLE will be loaded * @details By default NimBLE is loaded in internal ram.\n * To use external PSRAM you must change this to `#define CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL 1` */ +#ifndef CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL #define CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_INTERNAL 1 +#endif /** @brief Sets the number of simultaneous connections (esp controller max is 9) */ +#ifndef CONFIG_BT_NIMBLE_MAX_CONNECTIONS #define CONFIG_BT_NIMBLE_MAX_CONNECTIONS 3 +#endif /** @brief Sets the number of devices allowed to store/bond with */ +#ifndef CONFIG_BT_NIMBLE_MAX_BONDS #define CONFIG_BT_NIMBLE_MAX_BONDS 3 +#endif /** @brief Sets the maximum number of CCCD subscriptions to store */ +#ifndef CONFIG_BT_NIMBLE_MAX_CCCDS #define CONFIG_BT_NIMBLE_MAX_CCCDS 8 +#endif + +/** @brief Default device name */ +#ifndef CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME +#define CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME "nimble" +#endif /** @brief Set if CCCD's and bond data should be stored in NVS */ -#define CONFIG_BT_NIMBLE_NVS_PERSIST 0 +#define CONFIG_BT_NIMBLE_NVS_PERSIST 1 /** @brief Allow legacy paring */ #define CONFIG_BT_NIMBLE_SM_LEGACY 1 @@ -119,9 +149,6 @@ /** @brief Allow BLE secure connections */ #define CONFIG_BT_NIMBLE_SM_SC 1 -/** @brief Default device name */ -#define CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME "nimble" - /** @brief Max device name length (bytes) */ #define CONFIG_BT_NIMBLE_GAP_DEVICE_NAME_MAX_LEN 31 @@ -154,7 +181,6 @@ */ #define CONFIG_BT_NIMBLE_MSYS1_BLOCK_COUNT 12 - /** @brief Random address refresh time in seconds */ #define CONFIG_BT_NIMBLE_RPA_TIMEOUT 900 From 37beaca1198da70b2dc31aab1d5383e5961773c7 Mon Sep 17 00:00:00 2001 From: Simon Hailes Date: Sun, 17 Jan 2021 17:08:54 +0000 Subject: [PATCH 015/186] Implements xdrv_52 - BLE_ESP32 Modifies xsns_52 - iBeacon, and xsns_62 - MI32 to use the new BLE driver --- tasmota/my_user_config.h | 3 + tasmota/support_command.ino | 11 + tasmota/xdrv_01_webserver.ino | 14 +- tasmota/xdrv_52_BLE_ESP32.ino | 3613 ++++++++++++++++++++++++ tasmota/xsns_52_ibeacon.ino | 17 +- tasmota/xsns_52_ibeacon_BLE_ESP32.ino | 952 +++++++ tasmota/xsns_62_MI_ESP32.ino | 53 +- tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino | 2742 ++++++++++++++++++ 8 files changed, 7376 insertions(+), 29 deletions(-) create mode 100644 tasmota/xdrv_52_BLE_ESP32.ino create mode 100644 tasmota/xsns_52_ibeacon_BLE_ESP32.ino create mode 100644 tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 8f3fa5a89..8cf0316f8 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -861,6 +861,9 @@ //#define USE_SPI // Add support for hardware SPI #define USE_MI_ESP32 // Add support for ESP32 as a BLE-bridge (+9k2 mem, +292k flash) +//#define USE_BLE_ESP32 // Add support for ESP32 as a BLE-bridge (+9k2? mem, +292k? flash) +//#define USE_IBEACON // Add support for bluetooth LE passive scan of ibeacon devices (uses HM17 module) +//#define USE_IBEACON_ESP32 //#define USE_WEBCAM // Add support for webcam #endif // ESP32 diff --git a/tasmota/support_command.ino b/tasmota/support_command.ino index 1c5e32474..32180783d 100644 --- a/tasmota/support_command.ino +++ b/tasmota/support_command.ino @@ -690,6 +690,12 @@ void CmndSleep(void) } +#ifdef USE_BLE_ESP32 + // declare the fn + int ExtStopBLE(); +#endif + + void CmndUpgrade(void) { // Check if the payload is numerically 1, and had no trailing chars. @@ -700,6 +706,11 @@ void CmndUpgrade(void) TasmotaGlobal.ota_state_flag = 3; char stemp1[TOPSZ]; Response_P(PSTR("{\"%s\":\"" D_JSON_VERSION " %s " D_JSON_FROM " %s\"}"), XdrvMailbox.command, TasmotaGlobal.version, GetOtaUrl(stemp1, sizeof(stemp1))); + +#ifdef USE_BLE_ESP32 + ExtStopBLE(); +#endif + } else { Response_P(PSTR("{\"%s\":\"" D_JSON_ONE_OR_GT "\"}"), XdrvMailbox.command, TasmotaGlobal.version); } diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index d68cf9cd0..f148a5a6e 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -2374,6 +2374,11 @@ void UploadServices(uint32_t start_service) { } } +#ifdef USE_BLE_ESP32 + // declare the fn + int ExtStopBLE(); +#endif + void HandleUploadLoop(void) { // Based on ESP8266HTTPUpdateServer.cpp uses ESP8266WebServer Parsing.cpp and Cores Updater.cpp (Update) static uint32_t upload_size; @@ -2409,6 +2414,11 @@ void HandleUploadLoop(void) { } SettingsSave(1); // Free flash for upload +#ifdef USE_BLE_ESP32 + ExtStopBLE(); +#endif + + AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPLOAD D_FILE " %s"), upload.filename.c_str()); if (UPL_SETTINGS == Web.upload_file_type) { @@ -2632,7 +2642,9 @@ void HandleUploadLoop(void) { Web.upload_error = 7; // Upload aborted if (UPL_TASMOTA == Web.upload_file_type) { Update.end(); } } - delay(0); + // do actually wait a little to allow ESP32 tasks to tick + // fixes task timeout in ESP32Solo1 style unicore code. + delay(10); OsWatchLoop(); // Scheduler(); // Feed OsWatch timer to prevent restart on long uploads } diff --git a/tasmota/xdrv_52_BLE_ESP32.ino b/tasmota/xdrv_52_BLE_ESP32.ino new file mode 100644 index 000000000..90739757d --- /dev/null +++ b/tasmota/xdrv_52_BLE_ESP32.ino @@ -0,0 +1,3613 @@ +/* + xdrv_52_BLE_ESP32.ino - BLE via ESP32 support for Tasmota + + Copyright (C) 2020 Christian Baars and Theo Arends and Simon Hailes + + 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 + -------------------------------------------------------------------------------------------- +*/ + +/* + xdrv_52: + This driver uses the ESP32 BLE functionality to hopefully provide enough + BLE functionality to implement specific drivers on top of it. + + As a generic driver, it can: + Be asked to + connect/write to a MAC/Service/Characteristic + connect/read from a MAC/Service/Characteristic + connect/write/awaitnotify from a MAC/Service/Characteristic/NotifyCharacteristic + connect/read/awaitnotify from a MAC/Service/Characteristic/NotifyCharacteristic + + Cmnds: + BLEOp0 - requests status of operations + BLEOp1 MAC - create an operation in preparation, and populate it's MAC address + BLEOp2 Service - add a serviceUUID to the operation in preparation + BLEOp3 Characteristic - add a CharacteristicUUID to the operation in preparation for read/write + BLEOp4 writedata - optional:add data to write to the operation in preparation - hex string + BLEOp5 - optional:signify that a read should be done + BLEOp6 NotifyCharacteristic - optional:add a NotifyCharacteristicUUID to the operation in preparation to wait for a notify + BLEOp9 - publish the 'operation in preparation' to MQTT. + BLEOp10 - add the 'operation in preparation' to the queue of operations to perform. + + Other drivers can add callbacks to receive advertisements + Other drivers can add 'operations' to be performed and receive callbacks from the operation's success or failure + +Example: +Write and request next notify: +backlog BLEOp1 001A22092EE0; BLEOp2 3e135142-654f-9090-134a-a6ff5bb77046; BLEOp3 3fa4585a-ce4a-3bad-db4b-b8df8179ea09; BLEOp4 03; BLEOp6 d0e8434d-cd29-0996-af41-6c90f4e0eb2a; +BLEOp10 -> +19:25:08 RSL: tele/tasmota_E89E98/SENSOR = {"BLEOperation":{"opid":"3,"state":"1,"MAC":"001A22092EE0","svc":"3e135142-654f-9090-134a-a6ff5bb77046","char":"3fa4585a-ce4a-3bad-db4b-b8df8179ea09","wrote":"03}} +19:25:08 queued 0 sent {"BLEOperation":{"opid":"3,"state":"1,"MAC":"001A22092EE0","svc":"3e135142-654f-9090-134a-a6ff5bb77046","char":"3fa4585a-ce4a-3bad-db4b-b8df8179ea09","wrote":"03}} +19:25:08 RSL: stat/tasmota_E89E98/RESULT = {"BLEOp":"Done"} +..... +19:25:11 RSL: tele/tasmota_E89E98/SENSOR = {"BLEOperation":{"opid":"3,"state":"11,"MAC":"001A22092EE0","svc":"3e135142-654f-9090-134a-a6ff5bb77046","char":"3fa4585a-ce4a-3bad-db4b-b8df8179ea09","wrote":"03","notify":"020109000428}} + +state: 1 -> starting, +7 -> read complete +8 -> write complete +11 -> notify complete +-ve + -> failure (see GEN_STATE_FAILED_XXXX constants below.) + + +The driver can also be used by other drivers, using the functions: + +void registerForAdvertismentCallbacks(char *loggingtag, ADVERTISMENT_CALLBACK* pFn); +void registerForOpCallbacks(char *loggingtag, OPCOMPLETE_CALLBACK* pFn); +bool extQueueOperation(generic_sensor_t** op); + +These allow other code to + receive advertisements + receive operation callbacks. + create and start an operation, and get a callback when done/failed. + +i.e. the Bluetooth of the ESP can be shared without conflict. + +*/ + + +// TEMPORARILY define ESP32 and USE_BLE_ESP32 so VSCODE shows highlighting.... +//#define VSCODE_DEV + +#ifdef VSCODE_DEV +#define ESP32 +#define USE_BLE_ESP32 +#endif + +#ifdef ESP32 // ESP32 only. Use define USE_HM10 for ESP8266 support + +#ifdef USE_BLE_ESP32 + +#define BLE_ESP32_ALIASES + +// uncomment for more diagnostic/information messages - + more flash use. +//#define BLE_ESP32_DEBUG + + + +#define XDRV_52 52 +#define USE_MI_DECRYPTION + +#include +#include +#include +#include +#ifdef USE_MI_DECRYPTION +#include +#endif //USE_MI_DECRYPTION + +#include +#include +#include "NimBLEEddystoneURL.h" +#include "NimBLEEddystoneTLM.h" +#include "NimBLEBeacon.h" + +// from ble_gap.c +extern "C" void ble_gap_conn_broken(uint16_t conn_handle, int reason); + +void installExamples(); +void sendExample(); + + +namespace BLE_ESP32 { + + +// generic sensor type used as during +// connect/read/wrtie/notify operations +// only one operation will happen at a time + +#pragma pack( push, 0 ) // aligned structures for speed. but be sepcific + +///////////////////////////////////////////////////// +// states for runTaskDoneOperation +#define GEN_STATE_IDLE 0 +#define GEN_STATE_START 1 +#define GEN_STATE_STARTED 2 + +#define GEN_STATE_READDONE 3 +#define GEN_STATE_WRITEDONE 4 +#define GEN_STATE_WAITNOTIFY 5 +#define GEN_STATE_WAITINDICATE 6 + +#define GEN_STATE_NOTIFIED 7 + + +// Errors are all base on 0x100 +#define GEN_STATE_FAILED -1 +#define GEN_STATE_FAILED_CANTNOTIFYORINDICATE -2 +#define GEN_STATE_FAILED_CANTREAD -3 +#define GEN_STATE_FAILED_CANTWRITE -4 +#define GEN_STATE_FAILED_NOSERVICE -5 +#define GEN_STATE_FAILED_NO_RW_CHAR -6 +#define GEN_STATE_FAILED_NONOTIFYCHAR -7 +#define GEN_STATE_FAILED_NOTIFYTIMEOUT -8 +#define GEN_STATE_FAILED_READ -9 +#define GEN_STATE_FAILED_WRITE -10 +#define GEN_STATE_FAILED_CONNECT -11 +#define GEN_STATE_FAILED_NOTIFY -12 +#define GEN_STATE_FAILED_INDICATE -13 +#define GEN_STATE_FAILED_NODEVICE -14 +#define GEN_STATE_FAILED_NOREADWRITE -15 +#define GEN_STATE_FAILED_CANCEL -16 +// +///////////////////////////////////////////////////// + +#define BLE_ESP32_MAXNAMELEN 32 +#define BLE_ESP32_MAXALIASLEN 20 + + +#define MAX_BLE_DATA_LEN 100 +struct generic_sensor_t { + int16_t state; + uint32_t opid; // incrementing id so we can find them + uint64_t notifytimer; + + // uint8_t cancel; + // uint8_t requestType; + NimBLEAddress addr; + NimBLEUUID serviceUUID; + NimBLEUUID characteristicUUID; + NimBLEUUID notificationCharacteristicUUID; + uint8_t dataToWrite[MAX_BLE_DATA_LEN]; + uint8_t writelen; + uint8_t dataRead[MAX_BLE_DATA_LEN]; + uint8_t readlen; + uint8_t readtruncated; + uint8_t dataNotify[MAX_BLE_DATA_LEN]; + uint8_t notifylen; + uint8_t notifytruncated; + + // NOTE!!!: this callback is called DIRECTLY from the operation task, so be careful about cross-thread access of data + // if is called after read, so that you can do a read/modify/write operation on a characteristic. + // i.e. modify dataToWrite and writelen according to what you see in readData and readlen. + // for a normal read, please use the OPCOMPLETE_CALLBACK 'completecallback' + // normally null + void *readmodifywritecallback; // READ_CALLBACK function, used by external drivers + + void *completecallback; // OPCOMPLETE_CALLBACK function, used by external drivers + void *context; // opaque context, used by external drivers, or can be set to a long for MQTT +}; + + +//////////////////////////////////////////////////////////////// +// structure for callbacks from other drivers from advertisements. +struct ble_advertisment_t { + BLEAdvertisedDevice *advertisedDevice; // the full NimBLE advertisment, in case people need MORE info. + uint32_t totalCount; + + uint8_t addr[6]; + uint8_t addrtype; + int8_t RSSI; + char name[BLE_ESP32_MAXNAMELEN+1]; +}; + +struct ble_alias_t { + uint8_t addr[6]; + char name[BLE_ESP32_MAXALIASLEN+1]; +}; + +/* +This is probabyl what you are looking for: +ble_gap_addr_t gap_addr; +gap_addr.addr_type = BLE_GAP_ADDR_TYPE_PUBLIC; //Public address 0x00 +gap_addr.addr_type = BLE_GAP_ADDR_TYPE_RANDOM_STATIC; //Random static address 0x01 +gap_addr.addr_type = BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE; //Random private resolvable address 0x02 +gap_addr.addr_type = BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE; //Random private non-resolvable address 0x03 +*/ + +#pragma pack( pop ) // byte-aligned structures to read the sensor data + +//////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////// +// External interface to this driver for use by others. +// +// callback types to be used by external drivers +// +// returns - +// 0 = let others see this, +// 1 = I processed this, no need to give it to the next callback +// 2 = I want this device erased from the scan +typedef int ADVERTISMENT_CALLBACK(BLE_ESP32::ble_advertisment_t *pStruct); +// returns - 0 = let others see this, 1 = I processed this, no need to give it to the next callback, or post on MQTT +typedef int OPCOMPLETE_CALLBACK(BLE_ESP32::generic_sensor_t *pStruct); + +// NOTE!!!: this callback is called DIRECTLY from the operation task, so be careful about cross-thread access of data +// if is called after read, so that you can do a read/modify/write operation on a characteristic. +typedef int READ_CALLBACK(BLE_ESP32::generic_sensor_t *pStruct); + +typedef int SCANCOMPLETE_CALLBACK(NimBLEScanResults results); + +// tag is just a name for logging +void registerForAdvertismentCallbacks(const char *tag, BLE_ESP32::ADVERTISMENT_CALLBACK* pFn); +void registerForOpCallbacks(const char *tag, BLE_ESP32::OPCOMPLETE_CALLBACK* pFn); +void registerForScanCallbacks(const char *tag, BLE_ESP32::SCANCOMPLETE_CALLBACK* pFn); + +//////////////////////////////////////////////////// +// BLE operations: these are currently 'new'ed and 'delete'ed. +// in the future, they may be allocated from some constant menory store to avoid fragmentation. +// so PLEASE don't create or destroy them yourselves. +// create a new BLE operation. +int newOperation(BLE_ESP32::generic_sensor_t** op); +// free a BLE operation - this should be done if you did not call extQueueOperation for some reason +int freeOperation(BLE_ESP32::generic_sensor_t** op); +// queue a BLE operation - it will happen some time in the future. +// Note: you do not need to free an operation once it have been queued. it will be freed by the driver. +int extQueueOperation(BLE_ESP32::generic_sensor_t** op); +const char * getStateString(int state); +/////////////////////////////////////////////////////////////////////// + +#define USE_NATIVE_LOGGING + + +// a temporay safe logging mechanism. This has a max of 40 chars, and a max of 15 slots per 50ms +//int SafeAddLog_P(uint32_t loglevel, PGM_P formatP, ...); + +static void BLEDiag(); +const char *getAlias(uint8_t *addr); +//void BLEAliasMqttList(); +void BLEAliasListResp(); +//////////////////////////////////////////////////////////////////////// +// utilities +// dump a binary to hex +char * dump(char *dest, int maxchars, const uint8_t *src, int len); + + + + +struct BLE_simple_device_t { + uint8_t mac[6]; + uint8_t addrtype; + char name[BLE_ESP32_MAXNAMELEN+1]; + int8_t RSSI; + uint64_t lastseen; // last seen us + uint16_t maxAge; // maximum observed age of this device +}; + + + +// this protects our queues, which can be accessed by multiple tasks +SemaphoreHandle_t BLEOperationsRecursiveMutex; +SemaphoreHandle_t BLEDevicesMutex; + + +// only run from main thread, because it deletes things that were newed there... +static void mainThreadOpCallbacks(); +static void mainThreadBLETimeouts(); + +int addOperation(std::deque *ops, BLE_ESP32::generic_sensor_t** op); +BLE_ESP32::generic_sensor_t* nextOperation(std::deque *ops); +std::string BLETriggerResponse(BLE_ESP32::generic_sensor_t *toSend); +static void BLEscanEndedCB(NimBLEScanResults results); +static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify); + +// this is called from the advert callback, be careful +void BLEPostAdvert(ble_advertisment_t *Advertisment); +static void BLEPostMQTTSeenDevices(int type); + +static void BLEShow(bool json); +static void BLEPostMQTT(bool json); +static void BLEStartOperationTask(); + +// these are only run from the run task +static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOperation, NimBLEClient **ppClient); +static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLEClient **ppClient); +int BLETaskStartScan(int time); + + +// these are run from main thread +static int StartBLE(void); +static int StopBLE(void); + +// called from advert callback +void setDetails(ble_advertisment_t *ad); + +#undef EXAMPLE_ADVERTISMENT_CALLBACK +#undef EXAMPLE_OPERATION_CALLBACK + +#ifdef EXAMPLE_ADVERTISMENT_CALLBACK +int myAdvertCallback(BLE_ESP32::ble_advertisment_t *pStruct); +#endif +#ifdef EXAMPLE_OPERATION_CALLBACK +int myOpCallback(BLE_ESP32::generic_sensor_t *pStruct); +int myOpCallback2(BLE_ESP32::generic_sensor_t *pStruct); +#endif + + +// single storage for advert callbacks.... +static ble_advertisment_t BLEAdvertisment; + + +////////////////////////////////////////////////// +// general variables for running the driver +TaskHandle_t TasmotaMainTask; + + +static int BLEMasterEnable = 0; +static int BLEInitState = 0; +static int BLERunningScan = 0; +static uint32_t BLEScanCount = 0; +static uint8_t BLEScanActiveMode = 0; +static uint32_t BLELoopCount = 0; +static uint32_t BLEOpCount = 0; + +static int BLEPublishDevices = 0; // causes MQTT publish of device list (each scan end) +static BLEScan* ble32Scan = nullptr; +bool BLERunning = false; +// time we last started a scan in uS using esp_timer_get_time(); +// used to setect a scan which did not call back? +uint64_t BLEScanStartedAt = 0; +uint64_t BLEScanToEndBefore = 0; +uint8_t BLEStopScan = 0; +uint8_t BLEOtaStallBLE = 0; +uint8_t BLEDebugMode = 0; +int BLEMaxTaskLoopTime = 120; // we expect the task to NOT take > 120s per loop!!! +uint64_t BLELastLoopTime = 0; +int BLEScanTimeS = 20; // scan duraiton in S +int BLEMaxTimeBetweenAdverts = 120; // we expect an advert at least this frequently, else restart BLE (in S) +uint64_t BLEScanLastAdvertismentAt = 0; +uint32_t lastopid = 0; // incrementing uinique opid +uint32_t BLEResets = 0; +// controls request of details about one device +uint8_t BLEDetailsRequest = 0; +uint8_t BLEDetailsMac[6]; +uint8_t BLEAliasListTrigger = 0; +// triggers send for ALL operations known about +uint8_t BLEPostMQTTTrigger = 0; +int BLEMaxAge = 60*10; // 10 minutes +int BLEAddressFilter = 3; + + +////////////////////////////////////////////////// + + +// operation being prepared through commands +BLE_ESP32::generic_sensor_t* prepOperation = nullptr; + +// operations which have been queued +std::deque queuedOperations; +// operations in progress (at the moment, only one) +std::deque currentOperations; +// operations which have completed or failed, ready to send to MQTT +std::deque completedOperations; + +// seen devices +#define MAX_BLE_DEVICES_LOGGED 80 +std::deque seenDevices; +std::deque freeDevices; + + + +// list of registered callbacks for advertisements +// register using void registerForAdvertismentCallbacks(const char *somename ADVERTISMENT_CALLBACK* pFN); +std::deque advertismentCallbacks; + +std::deque operationsCallbacks; + +std::deque scancompleteCallbacks; + + +#ifdef BLE_ESP32_ALIASES +std::deque aliases; +#endif + + +/*********************************************************************************************\ + * constants +\*********************************************************************************************/ + +#define D_CMND_BLE "BLE" + +const char kBLE_Commands[] PROGMEM = D_CMND_BLE "|" + "Period|Adv|Op|Mode|Details|Scan|Alias|Name|Debug|Devices|MaxAge|AddrFilter"; + +static void CmndBLEPeriod(void); +static void CmndBLEAdv(void); +static void CmndBLEOperation(void); +static void CmndBLEMode(void); +static void CmndBLEDetails(void); +static void CmndBLEScan(void); +static void CmndBLEAlias(void); +static void CmndBLEName(void); +static void CmndBLEDebug(void); +static void CmndBLEDevices(void); +static void CmndBLEMaxAge(void); +static void CmndBLEAddrFilter(void); + +void (*const BLE_Commands[])(void) PROGMEM = { + &BLE_ESP32::CmndBLEPeriod, + &BLE_ESP32::CmndBLEAdv, + &BLE_ESP32::CmndBLEOperation, + &BLE_ESP32::CmndBLEMode, + &BLE_ESP32::CmndBLEDetails, + &BLE_ESP32::CmndBLEScan, + &BLE_ESP32::CmndBLEAlias, + &BLE_ESP32::CmndBLEName, + &BLE_ESP32::CmndBLEDebug, + &BLE_ESP32::CmndBLEDevices, + &BLE_ESP32::CmndBLEMaxAge, + &BLE_ESP32::CmndBLEAddrFilter +}; + +const char *successStates[] PROGMEM = { + PSTR("IDLE"), // 0 + PSTR("START"), + PSTR("STARTED"), + PSTR("DONEREAD"), + PSTR("DONEWRITE"), + PSTR("WAITNOTIFY"), + PSTR("WAITINDICATE"), + PSTR("DONENOTIFIED") // 7 +}; + +const char *failStates[] PROGMEM = { + PSTR("IDLE"), //0 + PSTR("FAILED"), //-1 + PSTR("FAILCANTNOTIFYORINDICATE"), + PSTR("FAILCANTREAD"), + PSTR("FAILCANTWRITE"), + PSTR("FAILNOSERVICE"), + PSTR("FAILNORWCHAR"), //-6 + PSTR("FAILNONOTIFYCHAR"), + PSTR("FAILNOTIFYTIMEOUT"), + PSTR("FAILEREAD"), + PSTR("FAILWRITE"), + PSTR("FAILCONNECT"), + PSTR("FAILNOTIFY"), + PSTR("FAILINDICATE"), + PSTR("FAILNODEVICE"), + PSTR("FAILNOREADWRITE"), + PSTR("FAILCANCEL")// -16 +}; + +const char * getStateString(int state){ + if ((state >= 0) && (state < sizeof(successStates)/sizeof(*successStates))){ + return successStates[state]; + } + + state = -state; + if ((state >= 0) && (state < sizeof(failStates)/sizeof(*failStates))){ + return failStates[state]; + } + + return PSTR("STATEINVALID"); +} + +/*********************************************************************************************\ + * enumerations +\*********************************************************************************************/ + +enum BLE_Commands { // commands useable in console or rules + CMND_BLE_PERIOD, // set period like TELE-period in seconds between read-cycles + CMND_BLE_ADV, // change advertisment options at runtime + CMND_BLE_OP, // connect/read/write/notify operations + CMND_BLE_MODE, // change mode of ble - BLE_MODES + CMND_BLE_DETAILS, // get details for one device's adverts + CMND_BLE_SCAN // Scan control + }; + +enum { + BLEModeDisabled = 0, // BLE is disabled + BLEModeScanByCommand = 1, // BLE is activeated by commands only + BLEModeRegularScan = 2, // BLE is scanning all the time +} BLE_SCAN_MODES; + +// values of BLEAdvertMode +enum { + BLE_NO_ADV_SEND = 0, // driver is silent on MQTT regarding adverts + BLE_ADV_TELE = 1, // driver sends a summary at tele period + //BLE_ADV_ALL = 2, // driver sends every advert with full data to MQTT +} BLEADVERTMODE; + + +uint8_t BLEMode = BLEModeRegularScan; +//uint8_t BLEMode = BLEModeScanByCommand; +uint8_t BLETriggerScan = 0; +uint8_t BLEAdvertMode = BLE_ADV_TELE; +uint8_t BLEdeviceLimitReached = 0; + +uint8_t BLEStop = 0; +uint64_t BLEStopAt = 0; + +uint8_t BLERestartTasmota = 0; +uint8_t BLERestartNimBLE = 0; +const char *BLE_RESTART_TEAMOTA_REASON_UNKNOWN = PSTR("unknown"); +const char *BLE_RESTART_TEAMOTA_REASON_RESTARTING_BLE_TIMEOUT = PSTR("restarting BLE took > 5s"); +const char *BLE_RESTART_TEAMOTA_REASON_BLE_LOOP_STALLED = PSTR("BLE loop stalled > 120s"); +const char *BLE_RESTART_TEAMOTA_REASON_BLE_DISCONNECT_FAIL = PSTR("BLE disconnect taking > 60s"); +const char *BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_UNKNOWN; + +const char *BLE_RESTART_BLE_REASON_UNKNOWN = PSTR("unknown"); +const char *BLE_RESTART_BLE_REASON_ADVERT_BLE_TIMEOUT = PSTR("no adverts in 120s"); +const char *BLE_RESTART_BLE_REASON_CONN_LIMIT = PSTR("connect failed with connection limit reached"); +const char *BLE_RESTART_BLE_REASON_CONN_EXISTS = PSTR("connect failed with connection exists"); +const char *BLERestartBLEReason = nullptr; + + +/*********************************************************************************************\ + * log of all devices present +\*********************************************************************************************/ + +void initSeenDevices(){ + /* added dynamically below, but never removed. + for (int i = 0; i < MAX_BLE_DEVICES_LOGGED; i++){ + BLE_ESP32::BLE_simple_device_t* dev = new BLE_ESP32::BLE_simple_device_t; + freeDevices.push_back(dev); + } + */ + return; +} + +int addSeenDevice(const uint8_t *mac, uint8_t addrtype, const char *name, int8_t RSSI){ + int res = 0; + uint64_t now = esp_timer_get_time(); + TasAutoMutex localmutex(&BLEDevicesMutex, "BLEAdd"); + + int devicefound = 0; + // do we already know this device? + for (int i = 0; i < seenDevices.size(); i++){ + if (!memcmp(seenDevices[i]->mac, mac, 6)){ + seenDevices[i]->lastseen = now; + seenDevices[i]->addrtype = addrtype; + seenDevices[i]->RSSI = RSSI; + if ((!seenDevices[i]->name[0]) && name[0]){ + strncpy(seenDevices[i]->name, name, sizeof(seenDevices[i]->name)); + seenDevices[i]->name[sizeof(seenDevices[i]->name)-1] = 0; + } + devicefound = 1; + break; + } + } + if (!devicefound){ + // if no free slots, add one if we have not reached our limit + if (!freeDevices.size()){ + int total = seenDevices.size(); + if (total < MAX_BLE_DEVICES_LOGGED){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("new seendev slot %d"), total); +#endif + BLE_ESP32::BLE_simple_device_t* dev = new BLE_ESP32::BLE_simple_device_t; + freeDevices.push_back(dev); + } else { + // flag we hit the limit + BLEdeviceLimitReached ++; + if (BLEdeviceLimitReached >= 254){ + BLEdeviceLimitReached = 254; + } + } + } + + // get a new device from the free list + if (freeDevices.size()){ + BLE_ESP32::BLE_simple_device_t* dev = freeDevices[0]; + freeDevices.erase(freeDevices.begin()); + memcpy(dev->mac, mac, 6); + strncpy(dev->name, name, sizeof(dev->name)); + dev->name[sizeof(dev->name)-1] = 0; + dev->lastseen = now; + dev->addrtype = addrtype; + dev->RSSI = RSSI; + dev->maxAge = 1; + seenDevices.push_back(dev); + res = 2; // added + } + } else { + res = 1; // already there + } + return res; +} + +// remove devices from the seen list by age, and add them to the free list +// set ageS to 0 to delete all... +int deleteSeenDevices(int ageS = 0){ + int res = 0; + uint64_t now = esp_timer_get_time(); + now = now/1000L; + now = now/1000L; + uint32_t nowS = (uint32_t)now; + uint32_t mintime = nowS - ageS; + + { + TasAutoMutex localmutex(&BLEDevicesMutex, "BLEDel"); + + for (int i = seenDevices.size()-1; i >= 0; i--){ + BLE_ESP32::BLE_simple_device_t* dev = seenDevices[i]; + uint64_t lastseen = dev->lastseen/1000L; + lastseen = lastseen/1000L; + uint32_t lastseenS = (uint32_t) lastseen; + uint32_t del_at = lastseenS + ageS; + uint32_t devAge = nowS - lastseenS; + if (dev->maxAge < devAge){ + dev->maxAge = devAge; + } + + uint8_t filter = 0; + if (dev->addrtype > BLEAddressFilter){ + filter = 1; + } + + if ((del_at < nowS) || (ageS == 0) || filter){ +#ifdef BLE_ESP32_DEBUG + char addr[20]; + dump(addr, 20, dev->mac, 6); + const char *alias = getAlias(dev->mac); + if (!filter){ + AddLog_P(LOG_LEVEL_INFO,PSTR("delete device %s(%s) by age lastseen %u + maxage %u < now %u."), + addr, alias, lastseenS, ageS, nowS); + } else { + AddLog_P(LOG_LEVEL_INFO,PSTR("delete device %s(%s) by addrtype filter %d > %d."), + addr, alias, dev->addrtype, BLEAddressFilter); + } +#endif + seenDevices.erase(seenDevices.begin()+i); + freeDevices.push_back(dev); + res++; + } + } + } + if (res){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE deleted %d devices"), res); +#endif + } + return res; +} + +int deleteSeenDevice(uint8_t *mac){ + int res = 0; + TasAutoMutex localmutex(&BLEDevicesMutex, "BLEDel2"); + for (int i = 0; i < seenDevices.size(); i++){ + if (!memcmp(seenDevices[i]->mac, mac, 6)){ + BLE_ESP32::BLE_simple_device_t* dev = seenDevices[i]; + seenDevices.erase(seenDevices.begin()+i); + freeDevices.push_back(dev); + res = 1; + break; + } + } + return res; +} + + +void checkDeviceTimouts(){ + if (BLEMaxAge){ + deleteSeenDevices(BLEMaxAge); + } +} + + +/////////////////////////////////////////////////////// +// returns age of device or 0. if age IS0, returns 1s +uint32_t devicePresent(uint8_t *mac){ + int res = 0; + uint64_t now = esp_timer_get_time(); + now = now/1000L; + now = now/1000L; + uint32_t nowS = (uint32_t)now; + + TasAutoMutex localmutex(&BLEDevicesMutex, "BLEPRes"); + for (int i = 0; i < seenDevices.size(); i++){ + if (!memcmp(seenDevices[i]->mac, mac, 6)){ + uint64_t lastseen = seenDevices[i]->lastseen/1000L; + lastseen = lastseen/1000L; + uint32_t lastseenS = (uint32_t) lastseen; + uint32_t ageS = nowS-lastseenS; + if (!ageS) ageS++; + res = ageS; + break; + } + } + return res; +} + + +// the MAX we could expect. +#define MAX_DEV_JSON_NAME_LEN BLE_ESP32_MAXNAMELEN +#define MAX_DEV_JSON_RSSI_LEN 3 +#define MAX_DEV_JSON_INDEX_LEN 3 +#define MAX_DEV_JSON_ALIAS_LEN BLE_ESP32_MAXALIASLEN +// "001122334455":{"i":123,"n":"01234567890123456789","r":-77}\0 +#define MIN_REQUIRED_DEVJSON_LEN \ + (1+12+1 + 1 + 1 + \ + +4 + MAX_DEV_JSON_INDEX_LEN \ + +1 + 4 + MAX_DEV_JSON_NAME_LEN + 2 \ + +1 + 4 + MAX_DEV_JSON_RSSI_LEN + 2 \ + +1 + 4 + MAX_DEV_JSON_ALIAS_LEN + 2 \ + +1 +1 \ + ) +int getSeenDeviceToJson(int index, BLE_ESP32::BLE_simple_device_t* dev, char **dest, int *maxlen){ + char *p = *dest; + // add 20 to be sure + if (*maxlen < MIN_REQUIRED_DEVJSON_LEN+20){ + return 0; + } + // add mac as key + *((*dest)++) = '"'; + dump((*dest), 20, dev->mac, 6); + (*dest) += 12; + *((*dest)++) = '"'; + *((*dest)++) = ':'; + + // add a structure, so we COULD add more than name later + *((*dest)++) = '{'; + *((*dest)++) = '"'; + *((*dest)++) = 'i'; // index + *((*dest)++) = '"'; + *((*dest)++) = ':'; + sprintf((*dest), "%d", index); + (*dest) += strlen((*dest)); + + if (dev->name[0]){ + *((*dest)++) = ','; + *((*dest)++) = '"'; + *((*dest)++) = 'n'; + *((*dest)++) = '"'; + *((*dest)++) = ':'; + *((*dest)++) = '"'; + *(*dest) = 0; // must term, else it adds to the *end* of old data! + strncat((*dest), dev->name, MAX_DEV_JSON_NAME_LEN); + (*dest) += strlen((*dest)); + *((*dest)++) = '"'; + } + *((*dest)++) = ','; + *((*dest)++) = '"'; + *((*dest)++) = 'r'; + *((*dest)++) = '"'; + *((*dest)++) = ':'; + sprintf((*dest), "%d", dev->RSSI); + (*dest) += strlen((*dest)); + + const char *alias = getAlias(dev->mac); + if (alias && alias[0]){ + *((*dest)++) = ','; + *((*dest)++) = '"'; + *((*dest)++) = 'a'; + *((*dest)++) = '"'; + *((*dest)++) = ':'; + *((*dest)++) = '"'; + sprintf((*dest), "%s", alias); + (*dest) += strlen((*dest)); + *((*dest)++) = '"'; + } + + *((*dest)++) = '}'; + *maxlen -= (*dest - p); + return 1; +} + + +int nextSeenDev = 0; + +int getSeenDevicesToJson(char *dest, int maxlen){ + + if ((nextSeenDev == 0) || (nextSeenDev >= seenDevices.size())){ + nextSeenDev = 0; + } + + // deliberate test of SafeAddLog_P from main thread... + //AddLog_P(LOG_LEVEL_INFO,PSTR("getSeen %d"), seenDevices.size()); + + + int len; + if (!maxlen) return 0; + strcpy((dest), ",\"BLEDevices\":{"); + len = strlen(dest); + dest += len; + maxlen -= len; + + int added = 0; + TasAutoMutex localmutex(&BLEDevicesMutex, "BLEGet"); + + snprintf((dest), maxlen-5, "\"total\":%d", seenDevices.size()); + len = strlen(dest); + dest += len; + maxlen -= len; + added = 1; // trigger ',' + + for (; nextSeenDev < seenDevices.size(); nextSeenDev++){ + if (maxlen > MIN_REQUIRED_DEVJSON_LEN + 3){ + if (added){ + *(dest++) = ','; + maxlen--; + } + int res = getSeenDeviceToJson(nextSeenDev, seenDevices[nextSeenDev], &dest, &maxlen); + if (res) { + added++; + } else { + if (added){ + dest--; // reverse out comma it the string did not get added + maxlen++; + break; + } + } + } else { + break; + } + } + *(dest++) = '}'; + *(dest++) = '}'; + *(dest++) = 0; + int remains = (seenDevices.size() - nextSeenDev); + return remains; +} + + + + +/*********************************************************************************************\ + * Mutex protected logging - max 5 logs of 40 chars +\*********************************************************************************************/ + +/* +#ifdef BLE_ESP32_DEBUG + #define MAX_SAFELOG_LEN 40 + #define MAX_SAFELOG_COUNT 25 +#else + #define MAX_SAFELOG_LEN 20 + #define MAX_SAFELOG_COUNT 5 +#endif + +struct safelogdata { + int level; + char log_data[MAX_SAFELOG_LEN]; +}; + +std::deque freelogs; +std::deque filledlogs; +uint8_t filledlogsOverflows = 0; +SemaphoreHandle_t SafeLogMutex; + + +void initSafeLog(){ + TasmotaMainTask = xTaskGetCurrentTaskHandle(); + SafeLogMutex = xSemaphoreCreateMutex(); + + for (int i = 0; i < MAX_SAFELOG_COUNT; i++){ + BLE_ESP32::safelogdata* logdata = new BLE_ESP32::safelogdata; + freelogs.push_back(logdata); + } +} + +int SafeAddLog_P(uint32_t loglevel, PGM_P formatP, ...) { + TaskHandle_t thistask = xTaskGetCurrentTaskHandle(); + int added = 0; + + // if the log would not be output do nothing here. + if ((loglevel > Settings.weblog_level) && + (loglevel > TasmotaGlobal.seriallog_level) && + (loglevel > Settings.mqttlog_level) && + (loglevel > TasmotaGlobal.syslog_level)){ + return added; + } + + char BLE_temp_log_data[MAX_SAFELOG_LEN]; + // as these are'expensive', let's not bother unless they are lower than the serial log level +#ifndef USE_NATIVE_LOGGING + xSemaphoreTake(SafeLogMutex, portMAX_DELAY); +#endif + int maxlen = sizeof(BLE_temp_log_data)-3; + if (thistask == TasmotaMainTask){ + maxlen -= 13; // room for "-!MAINTHREAD!" + } + // assume this is thread safe - it may not be + va_list arg; + va_start(arg, formatP); + vsnprintf_P(BLE_temp_log_data, maxlen, formatP, arg); + va_end(arg); +#ifdef USE_NATIVE_LOGGING + AddLog_P(loglevel, PSTR("%s"), BLE_temp_log_data); + return 1; +#else + if (thistask == TasmotaMainTask){ + loglevel = LOG_LEVEL_ERROR; + snprintf(BLE_temp_log_data + strlen(BLE_temp_log_data), 13, "-!MAINTHREAD!"); + xSemaphoreGive(SafeLogMutex); // release mutex + AddLog_P(loglevel, PSTR("%s"), BLE_temp_log_data); + return 0; + } + + if (freelogs.size()){ + BLE_ESP32::safelogdata* logdata = (freelogs)[0]; + freelogs.pop_front(); + logdata->level = loglevel; + memcpy(logdata->log_data, BLE_temp_log_data, sizeof(logdata->log_data)); + filledlogs.push_back(logdata); + added = 1; + } else { + // can't log it? + filledlogsOverflows++; + } + xSemaphoreGive(SafeLogMutex); // release mutex + return added; +#endif +} + +BLE_ESP32::safelogdata* GetSafeLog() { + xSemaphoreTake(SafeLogMutex, portMAX_DELAY); + if (filledlogs.size()){ + BLE_ESP32::safelogdata* logdata = (filledlogs)[0]; + filledlogs.pop_front(); + xSemaphoreGive(SafeLogMutex); // release mutex + return logdata; + } + xSemaphoreGive(SafeLogMutex); // release mutex + return nullptr; +} + +void ReleaseSafeLog(BLE_ESP32::safelogdata* logdata){ + xSemaphoreTake(SafeLogMutex, portMAX_DELAY); + freelogs.push_back(logdata); + xSemaphoreGive(SafeLogMutex); // release mutex +} +*/ + +/*********************************************************************************************\ + * Helper functions +\*********************************************************************************************/ + +/** + * @brief Simple pair of functions to dump to a hex string. + * + */ +static const char h[] PROGMEM = "0123456789ABCDEF"; +void hex(char *dest, uint8_t v){ + *(dest++) = h[(v>>4)&0xf]; + *(dest++) = h[v&0xf]; + *(dest) = 0; +} + +// convert from binary to hex. +// add a '+' on the end if not enough room. +char * dump(char *dest, int maxchars, const uint8_t *src, int len){ + int lenmax = (maxchars-1)/2; + int actuallen = 0; + for (actuallen = 0; actuallen < lenmax && actuallen < len; actuallen++){ + if (actuallen < lenmax){ + hex(dest+actuallen*2, src[actuallen]); + } + } + if (actuallen != len){ + *(dest+(actuallen*2)) = '+'; + *(dest+(actuallen*2)+1) = 0; + } + return dest; +} + +// convert from a hex string to binary +int fromHex(uint8_t *dest, const char *src, int maxlen){ + int srclen = strlen(src)/2; + if (srclen > maxlen){ + return 0; + } + + for (int i = 0; i < srclen; i++){ + char t[3]; + if (!isalnum(src[i*2])){ + return 0; + } + if (!isalnum(src[i*2 + 1])){ + return 0; + } + + t[0] = src[i*2]; + t[1] = src[i*2 + 1]; + t[2] = 0; + + int byte = strtol(t, NULL, 16); + *dest++ = byte; + } + return srclen; +} + + +/** + * @brief Reverse an array of 6 bytes + * + * @param _mac a byte array of size 6 (typicalliy representing a MAC address) + */ +void 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)); +} + + + + +/*********************************************************************************************\ + * Advertisment details +\*********************************************************************************************/ + +//ble_advertisment_t BLEAdvertismentDetails; +#define MAX_ADVERT_DETAILS 200 +char BLEAdvertismentDetailsJson[MAX_ADVERT_DETAILS]; +uint8_t BLEAdvertismentDetailsJsonSet = 0; +uint8_t BLEAdvertismentDetailsJsonLost = 0; + + +void setDetails(ble_advertisment_t *ad){ + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLESetDet"); + if (BLEAdvertismentDetailsJsonSet){ + BLEAdvertismentDetailsJsonLost = 1; + return; + } + char *p = BLEAdvertismentDetailsJson; + int maxlen = sizeof(BLEAdvertismentDetailsJson); + // just in case someone tries to read whilst we are writing + BLEAdvertismentDetailsJson[sizeof(BLEAdvertismentDetailsJson)-1] = 0; + + *(p++) = '{'; + maxlen--; + strcpy(p, "\"details\":{"); + int len = strlen(p); + p += len; + maxlen -= len; + + strcpy(p, "\"mac\":\""); + len = strlen(p); + p += len; + maxlen -= len; + dump(p, 14, ad->addr, 6); + len = strlen(p); + p += len; + maxlen -= len; + *(p++) = '\"'; maxlen--; + + if (BLEAdvertismentDetailsJsonLost){ + BLEAdvertismentDetailsJsonLost = 0; + strcpy(p, ",\"lost\":true"); + len = strlen(p); + p += len; + maxlen -= len; + } + + BLEAdvertisedDevice *advertisedDevice = ad->advertisedDevice; + + uint8_t* payload = advertisedDevice->getPayload(); + size_t payloadlen = advertisedDevice->getPayloadLength(); + if (payloadlen && (maxlen > 30)){ // will truncate if not enough space + strcpy(p, ",\"p\":\""); + p += 6; + maxlen -= 6; + dump(p, maxlen-10, payload, payloadlen); + int len = strlen(p); + p += len; + maxlen -= len; + *(p++) = '\"'; maxlen--; + } + + int svcdataCount = advertisedDevice->getServiceDataCount(); + if (svcdataCount){ + for (int i = 0; i < svcdataCount; i++){ + NimBLEUUID UUID = advertisedDevice->getServiceDataUUID(i);//.getNative()->u16.value; + std::string ServiceData = advertisedDevice->getServiceData(i); + + size_t ServiceDataLength = ServiceData.length(); + const uint8_t *serviceData = (const uint8_t *)ServiceData.data(); + + //char svcuuidstr[20]; + std::string strUUID = UUID; + + int svclen = strUUID.length(); + svclen++; // , + svclen += 3; // "": + svclen += ServiceDataLength*2; + svclen += 3; // ""} + + if (maxlen -10 > svclen){ + *(p++) = ','; + *(p++) = '\"'; + strcpy(p, strUUID.c_str()); + p += strUUID.length(); + *(p++) = '\"'; + *(p++) = ':'; + *(p++) = '\"'; + dump(p, ServiceDataLength*2+2, (uint8_t*)serviceData, ServiceDataLength); + int len = strlen(p); + p += len; + *(p++) = '\"'; + maxlen -= len; + } + } + } + + *(p++) = '}'; maxlen--; + *(p++) = '}'; maxlen--; + *(p++) = 0; maxlen--; + + BLEAdvertismentDetailsJsonSet = 1; +} + + +// call from main thread only! +// post advertisment detail if available, then clear. +void postAdvertismentDetails(){ +// if (TasmotaGlobal.ota_state_flag) return; + + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEPostAdd"); + if (BLEAdvertismentDetailsJsonSet){ + strncpy(TasmotaGlobal.mqtt_data, BLEAdvertismentDetailsJson, sizeof(TasmotaGlobal.mqtt_data)); + TasmotaGlobal.mqtt_data[sizeof(TasmotaGlobal.mqtt_data)-1] = 0; + BLEAdvertismentDetailsJsonSet = 0; + // we got the data, give before MQTT call. + localmutex.give(); + // no retain - this is present devices, not historic + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), 0); + } else { + } +} + + + +/*********************************************************************************************\ + * Classes +\*********************************************************************************************/ + +// does not really take any action +class BLESensorCallback : public NimBLEClientCallbacks { + void onConnect(NimBLEClient* pClient) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("onConnect %s"), ((std::string)pClient->getPeerAddress()).c_str()); +#endif + } + void onDisconnect(NimBLEClient* pClient) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("onDisconnect %s"), ((std::string)pClient->getPeerAddress()).c_str()); +#endif + } + bool onConnParamsUpdateRequest(NimBLEClient* pClient, const ble_gap_upd_params* params) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("onConnParamsUpdateRequest %s"), ((std::string)pClient->getPeerAddress()).c_str()); +#endif + +// if(params->itvl_min < 24) { /** 1.25ms units */ +// return false; +// } else if(params->itvl_max > 300) { /** 1.25ms units */ +// return false; +// } else if(params->latency > 2) { /** Number of intervals allowed to skip */ +// return false; +// } else if(params->supervision_timeout > 6000) { /** 10ms units */ +// return false; +// } + +/* + 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 > 200) { /// 10ms units + return false; + } + + return true; +*/ + // just always reject thiers, and use ours. + return false; + + } +}; + +static BLESensorCallback clientCB; + + +class BLEAdvCallbacks: public NimBLEAdvertisedDeviceCallbacks { + void onResult(NimBLEAdvertisedDevice* advertisedDevice) { + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEAddCB"); + uint64_t now = esp_timer_get_time(); + BLEScanLastAdvertismentAt = now; // note the time of the last advertisment + + uint32_t totalCount = BLEAdvertisment.totalCount; + memset(&BLEAdvertisment, 0, sizeof(BLEAdvertisment)); + BLEAdvertisment.totalCount = totalCount+1; + + BLEAdvertisment.advertisedDevice = advertisedDevice; + + // keep sign - char seems unsigned + int8_t RSSI = (char)advertisedDevice->getRSSI(); + NimBLEAddress address = advertisedDevice->getAddress(); + + BLEAdvertisment.addrtype = address.getType(); + + memcpy(BLEAdvertisment.addr, address.getNative(), 6); + ReverseMAC(BLEAdvertisment.addr); + + BLEAdvertisment.RSSI = RSSI; + + char addrstr[20]; + dump(addrstr, 20, BLEAdvertisment.addr, 6); + + // this mjust survive the scope of the callbacks + std::string name = ""; + const char *namestr = name.c_str(); + if (advertisedDevice->haveName()){ + name = advertisedDevice->getName(); + namestr = name.c_str(); + strncpy(BLEAdvertisment.name, namestr, sizeof(BLEAdvertisment.name)-1); + BLEAdvertisment.name[sizeof(BLEAdvertisment.name)-1] = 0; + } + + + // log this device safely + if (BLEAdvertisment.addrtype <= BLEAddressFilter){ + addSeenDevice(BLEAdvertisment.addr, BLEAdvertisment.addrtype, BLEAdvertisment.name, BLEAdvertisment.RSSI); + } + + if (BLEDetailsRequest){ + switch (BLEDetailsRequest){ + case 1:{ // one advert for one device + BLEDetailsRequest = 0; // only one requested if 2, it's a request all + if (!memcmp(BLEDetailsMac, BLEAdvertisment.addr, 6)){ + setDetails(&BLEAdvertisment); + } + } break; + case 2:{ // all adverts for one device - may not get them all + if (!memcmp(BLEDetailsMac, BLEAdvertisment.addr, 6)){ + setDetails(&BLEAdvertisment); + } + } break; + case 3:{ // all adverts for ALL DEVICES - may not get them all + // ignore from here on if filtered on addrtype + if (BLEAdvertisment.addrtype > BLEAddressFilter){ + return; + } + setDetails(&BLEAdvertisment); + } break; + } + } + + // ignore from here on if filtered on addrtype + if (BLEAdvertisment.addrtype > BLEAddressFilter){ + return; + } + + // call anyone who asked about advertisements + for (int i = 0; i < advertismentCallbacks.size(); i++) { + try { + ADVERTISMENT_CALLBACK* pFN; + pFN = advertismentCallbacks[i]; + int res = pFN(&BLEAdvertisment); + + // if this callback wants to stop here, then do so. + if (1 == res) break; + + // if this callback wants to kill this device + if (2 == res) { + //BLEScan->erase(address); + } + } catch(const std::exception& e){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in advertismentCallbacks")); +#endif + } + } + + } +}; + + +static BLEAdvCallbacks BLEScanCallbacks; +static BLESensorCallback BLESensorCB; + +/*********************************************************************************************\ + * BLE callback functions +\*********************************************************************************************/ + +static void BLEscanEndedCB(NimBLEScanResults results){ + +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Scan ended")); +#endif + for (int i = 0; i < scancompleteCallbacks.size(); i++){ + try { + SCANCOMPLETE_CALLBACK *pFn = scancompleteCallbacks[i]; + int callbackres = pFn(results); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("scancompleteCallbacks %d %d"), i, callbackres); +#endif + } catch(const std::exception& e){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in operationsCallbacks")); +#endif + } + } + + BLERunningScan = 2; + BLEScanToEndBefore = 0L; + BLEScanCount++; +} + + +/////////////////////////////////////////////////////////////////////// +// !!!!!!!!!!@@@@@@@@@@@@@@@@ +// NOTE: this can callback BEFORE the write is completed. +// so we should not do any actions against the device if we can help it +// this COULD be the reason for the BLE stack hanging up.... +/////////////////////////////////////////////////////////////////////// +static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify){ + NimBLEClient *pRClient; + + if (!pRemoteCharacteristic){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("Notify: no remote char!!??")); +#endif + return; + } + + +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Notified length: %u"),length); +#endif + // find the operation this is associated with + NimBLERemoteService *pSvc = pRemoteCharacteristic->getRemoteService(); + + if (!pSvc){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Notify: no remote service found")); +#endif + return; + } + + pRClient = pSvc->getClient(); + if (!pRClient){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Notify: no remote client!!??")); +#endif + return; + } + NimBLEAddress devaddr = pRClient->getPeerAddress(); + + generic_sensor_t *thisop = nullptr; + { + // make sure we are not disturbed + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLENotif"); + + for (int i = 0; i < currentOperations.size(); i++){ + generic_sensor_t *op = currentOperations[i]; + if (!op){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Notify: null op in currentOperations!!??")); +#endif + } else { + if (devaddr == op->addr){ + thisop = op; + break; + } + } + } + } + + // we'll try without + //pRemoteCharacteristic->unsubscribe(); + + if (!thisop){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("no op for notify")); +#endif + return; + } + + for (int i = 0; i < length && i < sizeof(thisop->dataNotify); i++){ + thisop->dataNotify[i] = pData[i]; + } + thisop->notifylen = length; + if (length > sizeof(thisop->dataNotify)){ + thisop->notifytruncated = 1; + } else { + thisop->notifytruncated = 0; + } + // we will NOT change the state here... + // rely on thisop->notifylen as a flag notify is complete + //thisop->state = GEN_STATE_NOTIFIED; + + // this triggers our notify complete, either at the end of read/write, or next 1s cycle. + thisop->notifytimer = 0; + +} + + + + +/*********************************************************************************************\ + * functions for registering callbacks against the driver +\*********************************************************************************************/ + +void registerForAdvertismentCallbacks(const char *tag, BLE_ESP32::ADVERTISMENT_CALLBACK* pFn){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: registerForAdvertismentCallbacks %s:%x"), tag, (uint32_t) pFn); +#endif + advertismentCallbacks.push_back(pFn); +} + +void registerForOpCallbacks(const char *tag, BLE_ESP32::OPCOMPLETE_CALLBACK* pFn){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: registerForOpCallbacks %s:%x"), tag, (uint32_t) pFn); +#endif + operationsCallbacks.push_back(pFn); +} + +void registerForScanCallbacks(const char *tag, BLE_ESP32::SCANCOMPLETE_CALLBACK* pFn){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: registerForScnCallbacks %s:%x"), tag, (uint32_t) pFn); +#endif + scancompleteCallbacks.push_back(pFn); +} + + +/*********************************************************************************************\ + * init NimBLE +\*********************************************************************************************/ +static void BLEPreInit(void) { + BLEInitState = 0; + prepOperation = nullptr; +} + + +static void BLEInit(void) { + if (BLEMode == BLEModeDisabled) return; + + if (BLEInitState) { return; } + + if (TasmotaGlobal.global_state.wifi_down) { return; } + if (WiFi.getSleep() == false) { +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: WiFi modem not in sleep mode, BLE cannot start yet")); +#endif + if (0 == Settings.flag3.sleep_normal) { + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: About to restart to put WiFi modem in sleep mode")); + Settings.flag3.sleep_normal = 1; // SetOption60 - Enable normal sleep instead of dynamic sleep + TasmotaGlobal.restart_flag = 2; + } + return; + } + + + // this is only for testing, does nothin if examples are undefed + installExamples(); + //initSafeLog(); + initSeenDevices(); + + uint64_t now = esp_timer_get_time(); + BLEScanLastAdvertismentAt = now; // initialise the time of the last advertisment + BLELastLoopTime = now; + + BLEInitState = 1; + + // dont start of disabled + BLEMasterEnable = Settings.flag5.mi32_enable; + if (!BLEMasterEnable) return; + + + StartBLE(); + + return; +} + +/*********************************************************************************************\ + * Task section +\*********************************************************************************************/ + +static void BLEOperationTask(void *pvParameters); + +static void BLEStartOperationTask(){ + if (BLERunning == false){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Start operations"),D_CMND_BLE); +#endif + BLERunning = true; + + xTaskCreatePinnedToCore( + BLE_ESP32::BLEOperationTask, /* Function to implement the task */ + "BLEOperationTask", /* Name of the task */ + 4096, /* Stack size in bytes */ + NULL, /* Task input parameter */ + 0, /* Priority of the task */ + NULL, /* Task handle. */ +#ifdef CONFIG_FREERTOS_UNICORE + 0); /* Core where the task should run */ +#else + 1); /* Core where the task should run */ +#endif + } +} + + +static void BLETaskStopStartNimBLE(NimBLEClient **ppClient, bool start = true){ + + if (*ppClient){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLETask:Stopping NimBLE")); + + (*ppClient)->setClientCallbacks(nullptr, false); + + try { + if ((*ppClient)->isConnected()){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("disconnecting connected client")); +#endif + (*ppClient)->disconnect(); + } + NimBLEDevice::deleteClient((*ppClient)); + (*ppClient) = nullptr; +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("deleted client")); +#endif + } catch(const std::exception& e){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Stopping NimBLE:exception in delete client")); +#endif + } + + if (ble32Scan){ + ble32Scan->setAdvertisedDeviceCallbacks(nullptr,true); + ble32Scan->stop(); + ble32Scan = nullptr; + } + + // wait second + vTaskDelay(100/ portTICK_PERIOD_MS); + NimBLEDevice::deinit(true); + } + BLERunningScan = 0; + + if (start){ + AddLog_P(LOG_LEVEL_INFO,PSTR("BLETask:Starting NimBLE")); + NimBLEDevice::init("BLE_ESP32"); + + *ppClient = NimBLEDevice::createClient(); + (*ppClient)->setClientCallbacks(&clientCB, false); + /** Set initial connection parameters: These settings are 15ms interval, 0 latency, 120ms timout. + * These settings are safe for 3 clients to connect reliably, can go faster if you have less + * connections. Timeout should be a multiple of the interval, minimum is 100ms. + * Min interval: 12 * 1.25ms = 15, Max interval: 12 * 1.25ms = 15, 0 latency, 51 * 10ms = 510ms timeout + */ + (*ppClient)->setConnectionParams(12,12,0,51); + /** Set how long we are willing to wait for the connection to complete (seconds), default is 30. */ + (*ppClient)->setConnectTimeout(15); + } + + uint64_t now = esp_timer_get_time(); + + // don't restart because of these for a while + BLELastLoopTime = now; // initialise the time of the last advertisment + BLEScanLastAdvertismentAt = now; // initialise the time of the last advertisment + +} + +int BLETaskStartScan(int time){ + if (!ble32Scan) return -1; + if (BLEMode == BLEModeDisabled) return -4; + // don't scan whilst OTA in progress + if (BLEOtaStallBLE) return -5; + if (currentOperations.size()) return -3; + + if (BLERunningScan) { + // if we hit 2, wait one more time before starting + if (BLERunningScan == 2){ + // wait 100ms + vTaskDelay(100/ portTICK_PERIOD_MS); + BLERunningScan = 0; + } + return -2; + } + +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: Startscan")); +#endif + //vTaskDelay(500/ portTICK_PERIOD_MS); + ble32Scan->setActiveScan(BLEScanActiveMode ? 1: 0); + + + // seems we could get the callback within the start call.... + // so set these before starting + BLERunningScan = 1; + BLEScanStartedAt = esp_timer_get_time(); + if (BLETriggerScan){ + time = BLETriggerScan; + BLETriggerScan = 0; + } + ble32Scan->start(time, BLEscanEndedCB, (BLEScanActiveMode == 2)); // 20s scans, restarted when then finish + //vTaskDelay(500/ portTICK_PERIOD_MS); + return 0; +} + +// this runs one operation +// if the passed pointer is empty, it tries to get a next one. +static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOperation, NimBLEClient **ppClient){ + if (!pCurrentOperation) return; + + NimBLEClient *pClient = *ppClient; + if (!*pCurrentOperation) { + *pCurrentOperation = nextOperation(&queuedOperations); + if (*pCurrentOperation){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: new currentOperation")); +#endif + BLEOpCount++; + generic_sensor_t* temp = *pCurrentOperation; + //this will null it out, so save and restore. + addOperation(¤tOperations, pCurrentOperation); + *pCurrentOperation = temp; + } + } + if (!*pCurrentOperation) return; + + + + // if awaiting notification + if ((*pCurrentOperation)->notifytimer){ + // if it took too long, then disconnect + uint64_t now = esp_timer_get_time(); + uint64_t diff = now - (*pCurrentOperation)->notifytimer; + diff = diff/1000; + if (diff > 20000){ // 20s +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: notify timeout")); +#endif + (*pCurrentOperation)->state = GEN_STATE_FAILED_NOTIFYTIMEOUT; + (*pCurrentOperation)->notifytimer = 0; + } + // we can't process any further, because op will be at state readdone or writedone + return; + } + + + switch((*pCurrentOperation)->state){ + case GEN_STATE_WAITINDICATE: + case GEN_STATE_WAITNOTIFY: + //(*pCurrentOperation)->notifytimer == 0 at this point, so must be done + (*pCurrentOperation)->state = GEN_STATE_NOTIFIED; + // just stay here until this is removed by the main thread +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: notify operation complete")); +#endif + BLE_ESP32::BLETaskRunTaskDoneOperation(pCurrentOperation, ppClient); + pClient = *ppClient; + return; + break; + case GEN_STATE_READDONE: + case GEN_STATE_WRITEDONE: + case GEN_STATE_NOTIFIED: // - may have completed DURING our read/write to get here + // just stay here until this is removed by the main thread +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: operation complete")); +#endif + BLE_ESP32::BLETaskRunTaskDoneOperation(pCurrentOperation, ppClient); + pClient = *ppClient; + return; + break; + + case GEN_STATE_START: + // continue to start the process here. + break; + + default: + break; + } + + + if (!*pCurrentOperation) return; + + if ((*pCurrentOperation)->state <= GEN_STATE_FAILED){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLETask: op failed %d"), (*pCurrentOperation)->state); +#endif + BLE_ESP32::BLETaskRunTaskDoneOperation(pCurrentOperation, ppClient); + pClient = *ppClient; + return; + } + + if ((*pCurrentOperation)->state != GEN_STATE_START){ + return; + } + + if (pClient->isConnected()){ + // don't do anything if we are still connected +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: still connected")); +#endif + return; + } + + + // if we managed to run operations back to back with long connection timeouts, + // then we may NOT see advertisements. + // so to prevent triggering of the advert timeout restart mechanism, + // set the last advert time each time we start an operation + uint64_t now = esp_timer_get_time(); + BLEScanLastAdvertismentAt = now; // initialise the time of the last advertisment + + + generic_sensor_t* op = *pCurrentOperation; + + int newstate = GEN_STATE_STARTED; + op->state = GEN_STATE_STARTED; + +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: attempt connect %s"), ((std::string)op->addr).c_str()); +#endif + + if (!op->serviceUUID.bitSize()){ + op->state = GEN_STATE_FAILED_NOSERVICE; + return; + } + + if (pClient->connect(op->addr, true)) { + +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("connected %s -> getservice"), ((std::string)op->addr).c_str()); +#endif + NimBLERemoteService *pService = pClient->getService(op->serviceUUID); + int waitNotify = false; + int notifystate = 0; + op->notifytimer = 0L; + + if (pService != nullptr) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got service")); +#endif + // pre-set to fail if no operations requested + //newstate = GEN_STATE_FAILED_NOREADWRITE; + + /////////////////////////////////////////////////////////////////////// + // !!!!!!!!!!@@@@@@@@@@@@@@@@ + // NOTE: Notify callback can happen BEFORE the read/write is completed. + // this COULD be the reason for the BLE stack hanging up.... + /////////////////////////////////////////////////////////////////////// + + // if we have been asked to get a notification + if (op->notificationCharacteristicUUID.bitSize()) { + NimBLERemoteCharacteristic *pNCharacteristic = + pService->getCharacteristic(op->notificationCharacteristicUUID); + if (pNCharacteristic != nullptr) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got notify characteristic")); +#endif + op->notifylen = 0; + if(pNCharacteristic->canNotify()) { + if(pNCharacteristic->subscribe(true, BLE_ESP32::BLEGenNotifyCB)) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("subscribe for notify")); +#endif + uint64_t now = esp_timer_get_time(); + op->notifytimer = now; + // this will get changed to read or write, + // but here in case it's notify only (can that happen?) + notifystate = GEN_STATE_WAITNOTIFY; + waitNotify = true; + } else { +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("failed subscribe for notify")); +#endif + newstate = GEN_STATE_FAILED_NOTIFY; + } + } else { + if(pNCharacteristic->canIndicate()) { + if(pNCharacteristic->subscribe(false, BLE_ESP32::BLEGenNotifyCB)) { +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("subscribe for indicate")); +#endif + notifystate = GEN_STATE_WAITINDICATE; + uint64_t now = esp_timer_get_time(); + op->notifytimer = now; + waitNotify = true; + } else { +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("failed subscribe for indicate")); +#endif + newstate = GEN_STATE_FAILED_INDICATE; + } + } else { + newstate = GEN_STATE_FAILED_CANTNOTIFYORINDICATE; +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("characteristic can't notify")); +#endif + } + } + } else { + newstate = GEN_STATE_FAILED_NONOTIFYCHAR; +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("notify characteristic not found")); +#endif + } + + // force the 'error' of the notify coming in before the read/write for testing + //vTaskDelay(1000/ portTICK_PERIOD_MS); + } // no supplied notify char is ok + + // this will only happen if you ask for a notify char which is not there? + if (!(newstate <= GEN_STATE_FAILED)){ + if (op->characteristicUUID.bitSize()) { + // read or write characteristic - we always need this? + NimBLERemoteCharacteristic *pCharacteristic = nullptr; + + pCharacteristic = pService->getCharacteristic(op->characteristicUUID); + if (pCharacteristic != nullptr) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got read/write characteristic")); +#endif + newstate = GEN_STATE_FAILED_NOREADWRITE; // overwritten on failure + + if (op->readlen){ + if(pCharacteristic->canRead()) { + std::string value = pCharacteristic->readValue(); + op->readlen = value.length(); + memcpy(op->dataRead, value.data(), + (op->readlen > sizeof(op->dataRead))? + sizeof(op->dataRead): + op->readlen); + if (op->readlen > sizeof(op->dataRead)){ + op->readtruncated = 1; + } else { + op->readtruncated = 0; + } + if (op->readmodifywritecallback){ + READ_CALLBACK *pFn = (READ_CALLBACK *)op->readmodifywritecallback; +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("read characteristic with readmodifywritecallback")); +#endif + pFn(op); + } else { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("read characteristic")); +#endif + } + + // only change it to a 'finished' state if we really are + if (!waitNotify && !op->writelen) newstate = GEN_STATE_READDONE; + + } else { + newstate = GEN_STATE_FAILED_CANTREAD; + } + } + if (op->writelen){ + if(pCharacteristic->canWrite()) { + if (!pCharacteristic->writeValue(op->dataToWrite, op->writelen, true)){ + newstate = GEN_STATE_FAILED_WRITE; +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("characteristic write fail")); +#endif + } else { + if (!waitNotify) newstate = GEN_STATE_WRITEDONE; +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("write characteristic")); +#endif + } + } else { + newstate = GEN_STATE_FAILED_CANTWRITE; + } + } + // print or do whatever you need with the value + + } else { + newstate = GEN_STATE_FAILED_NO_RW_CHAR; +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("r/w characteristic not found")); +#endif + } + } + } + + + // disconnect if not waiting for notify, + if (!op->notifytimer){ + if (waitNotify){ + vTaskDelay(50/ portTICK_PERIOD_MS); + // must have completed during our read/write operation + newstate = GEN_STATE_NOTIFIED; + } + } else { + newstate = notifystate; + } + } else { + newstate = GEN_STATE_FAILED_NOSERVICE; + // failed to get a service +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("failed - svc not on device?")); +#endif + } + + } else { // connect itself failed + newstate = GEN_STATE_FAILED_CONNECT; +#ifdef NIMBLE_CLIENT_HAS_GETRESULT + int rc = pClient->getResult(); + + switch (rc){ + case (0x0200+BLE_ERR_CONN_LIMIT ): +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Hit connection limit? - restarting NimBLE")); +#endif + BLERestartNimBLE = 1; + BLERestartBLEReason = BLE_RESTART_BLE_REASON_CONN_LIMIT; + break; + case (0x0200+BLE_ERR_ACL_CONN_EXISTS): +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Connection exists? - restarting NimBLE")); +#endif + BLERestartNimBLE = 1; + BLERestartBLEReason = BLE_RESTART_BLE_REASON_CONN_EXISTS; + break; + } +#endif + + // failed to connect +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("failed to connect to device %d"), rc); +#endif + } + op->state = newstate; +} + + + +// this disconnects from a device if necessary, and then +// moves the operation from 'currentOperations' to 'completedOperations'. + +// for safety's sake, only call from the run task +static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLEClient **ppClient){ + try { + if ((*ppClient)->isConnected()){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("runTaskDoneOperation: disconnecting connected client")); +#endif + (*ppClient)->disconnect(); + // wait for 1/2 second after disconnect + int waits = 0; + do { + vTaskDelay(500/ portTICK_PERIOD_MS); + if (waits) { + //(*ppClient)->disconnect(); + // we will stall here forever!!! - as testing +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE wait discon%d"), waits); +#endif + vTaskDelay(500/ portTICK_PERIOD_MS); + } + waits++; + if (waits == 5){ + int conn_id = (*ppClient)->getConnId(); + ble_gap_conn_broken(conn_id, -1); +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE wait discon%d - kill connection"), waits); +#endif + } + if (waits == 60){ + AddLog_P(LOG_LEVEL_ERROR,PSTR(">60s waiting -> BLE Failed, restart Tasmota %d"), waits); + BLEStop = 1; + BLEStopAt = esp_timer_get_time(); + + BLERestartTasmota = 10; + BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_BLE_DISCONNECT_FAIL; + break; + } + } while ((*ppClient)->isConnected()); + } + } catch(const std::exception& e){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("runTaskDoneOperation: exception in disconnect")); +#endif + } + + + { + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEDoneOp"); + + // find this operation in currentOperations, and remove it. + for (int i = 0; i < currentOperations.size(); i++){ + if (currentOperations[i]->opid == (*op)->opid){ + currentOperations.erase(currentOperations.begin() + i); + break; + } + } + } + + + // by adding it to this list, this will cause it to be sent to MQTT +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("runTaskDoneOperation: add to completedOperations")); +#endif + addOperation(&completedOperations, op); + return; +} + + + + +// this IS as task. +// we MAY be able to run a few of these simultaneously, but this is not yet tested. +// and probably not required. But everything is there to do so.... +static void BLEOperationTask(void *pvParameters){ + + BLELoopCount = 0; + BLEOpCount = 0;; + + uint32_t timer = 0; + // operation which is currently in progress in THIS TASK + generic_sensor_t* currentOperation = nullptr; + + NimBLEClient *pClient = nullptr; + BLE_ESP32::BLETaskStopStartNimBLE(&pClient); + + for(;;){ + BLELastLoopTime = esp_timer_get_time(); + BLELoopCount++; + + BLE_ESP32::BLETaskRunCurrentOperation(¤tOperation, &pClient); + + // start a scan if possible + if ((BLEMode == BLEModeRegularScan) || (BLETriggerScan)){ + BLEScan* lastScan = ble32Scan; + ble32Scan = NimBLEDevice::getScan(); + if (lastScan != ble32Scan){ + //ble32Scan->setInterval(70); + //ble32Scan->setWindow(50); + ble32Scan->setInterval(0x40); + ble32Scan->setWindow(0x20); + ble32Scan->setAdvertisedDeviceCallbacks(&BLEScanCallbacks,true); + } + + BLE_ESP32::BLETaskStartScan(20); + } + + if (BLEStopScan){ + ble32Scan->stop(); + BLEStopScan = 0; + } + + // come around every 1/10s + vTaskDelay(100/ portTICK_PERIOD_MS); + + if (BLEStop == 1){ + break; + } + + if (BLERestartNimBLE){ + BLERestartNimBLE = 0; + BLERestartTasmota = 10; + BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_RESTARTING_BLE_TIMEOUT; + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLETask: Restart NimBLE - restart Tasmota in 10 if not complt")); + BLE_ESP32::BLETaskStopStartNimBLE(&pClient); + BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_UNKNOWN; + BLERestartTasmota = 0; + BLEResets ++; + } + } + + BLE_ESP32::BLETaskStopStartNimBLE(&pClient, false); + + // wait 1/10 second + vTaskDelay(100/ portTICK_PERIOD_MS); + +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLEOperationTask: Left task")); +#endif + deleteSeenDevices(); + + BLEStop = 2; + BLERunning = false; + vTaskDelete( NULL ); +} + + + + + +/***********************************************************************\ + * Regular Tasmota called functions + * +\***********************************************************************/ +void BLEEvery50mSecond(){ +/* if (BLEAliasListTrigger){ + BLEAliasListTrigger = 0; + BLEAliasMqttList(); + }*/ + postAdvertismentDetails(); +} + + + +/** + * @brief Main loop of the driver, "high level"-loop + * + */ + +static void BLEEverySecond(bool restart){ + + BLEDiag(); + + checkDeviceTimouts(); + + + if (Settings.flag5.mi32_enable != BLEMasterEnable){ + if (Settings.flag5.mi32_enable){ + if (StartBLE()){ + BLEMasterEnable = Settings.flag5.mi32_enable; + } + } else { + if (StopBLE()){ + BLEMasterEnable = Settings.flag5.mi32_enable; + } + } + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: MasterEnable->%d"), BLEMasterEnable); + } + + + // check for application callbacks here. + // this may remove complete items. + BLE_ESP32::mainThreadOpCallbacks(); + + // post any MQTT data if we completed anything in the last second + if (completedOperations.size()){ + BLE_ESP32::BLEPostMQTT(true); // send only completed + } + + // request send of ALL oeprations prepped, queued, in progress - + // in separate MQTT messages. + if (BLEPostMQTTTrigger){ + BLEPostMQTTTrigger = 0; + BLE_ESP32::BLEPostMQTT(false); // show all operations, not just completed + } + + if (BLEPublishDevices){ + BLEPostMQTTSeenDevices(BLEPublishDevices); + BLEPublishDevices = 0; + } + + // we have been asked to restart in this many seconds.... + if (BLERestartTasmota){ + BLERestartTasmota--; + // 2 seconds to go, post to BLE topic on MQTT our reason + if (BLERestartTasmota == 2){ + if (!BLERestartTasmotaReason) BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_UNKNOWN; + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("{\"reboot\":\"%s\"}"), BLERestartTasmotaReason); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE Failure! Restarting Tasmota in %d seconds because %s"), BLERestartTasmota, BLERestartTasmotaReason); + } + + if (!BLERestartTasmota){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE Failure! Restarting Tasmota because %s"), BLERestartTasmotaReason); + // just a normal restart + TasmotaGlobal.restart_flag = 1; + } + } + + if (BLERestartBLEReason){ // just use the ptr as the trigger to send MQTT + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("{\"blerestart\":\"%s\"}"), BLERestartBLEReason); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE Failure! Restarting BLE Stack because %s"), BLERestartBLEReason); + BLERestartBLEReason = nullptr; + } + + + BLE_ESP32::mainThreadBLETimeouts(); + if (!BLEMasterEnable){ + return; + } + +} + + + + + +/*********************************************************************************************\ + * Operations queue functions - all to do with read/write and notify for a device +\*********************************************************************************************/ + +// this retrievs the next operation from the passed list, and removes it from the list. +// or returns null if none. +generic_sensor_t* nextOperation(std::deque *ops){ + generic_sensor_t* op = nullptr; + if (ops->size()){ + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLENExtOp"); + op = (*ops)[0]; + ops->pop_front(); + } + return op; +} + +// this adds an operation to the end of passed list. +// it also sets the op pointer passed to null. +int addOperation(std::deque *ops, generic_sensor_t** op){ + int res = 0; + { + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEAddOp"); + if (ops->size() < 10){ + ops->push_back(*op); + *op = nullptr; + res = 1; + } + } + if (res){ + //AddLog_P(LOG_LEVEL_DEBUG,PSTR("added operation")); + } else { + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE op - no room")); + } + return res; +} + + +int newOperation(BLE_ESP32::generic_sensor_t** op){ + if (!op) { + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE op inv in newOperation")); + return 0; + } + + BLE_ESP32::generic_sensor_t *o = new BLE_ESP32::generic_sensor_t; + + // clear to zeros, but not the NimBLE classes + o->state = 0; + o->opid = 0; // incrementing id so we can find them + o->notifytimer = 0L; + //uint8_t writeRead[MAX_BLE_DATA_LEN]; + o->writelen = 0; + //uint8_t dataRead[MAX_BLE_DATA_LEN]; + o->readlen = 0; + o->readtruncated = 0; + //uint8_t dataNotify[MAX_BLE_DATA_LEN]; + o->notifylen = 0; + o->notifytruncated = 0; + o->readmodifywritecallback = nullptr; // READ_CALLBACK function, used by external drivers + o->completecallback = nullptr; // OPCOMPLETE_CALLBACK function, used by external drivers + o->context = nullptr; // opaque context, used by external drivers, or can be set to a long for MQTT + + (*op) = o; + return 1; +} + +int freeOperation(BLE_ESP32::generic_sensor_t** op){ + if (!op) { + return 0; + } + + delete (*op); + (*op) = nullptr; + return 1; +} + + +int extQueueOperation(BLE_ESP32::generic_sensor_t** op){ + if (!op || !(*op)) { + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: op invalid")); + return 0; + } + (*op)->state = GEN_STATE_START; // trigger request later + (*op)->opid = lastopid++; + + int res = addOperation(&queuedOperations, op); + if (!res){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("extQueueOperation: op added id %d failed"), (lastopid-1)); + } + return res; +} + + +/*********************************************************************************************\ + * BLE Name alisaes +\*********************************************************************************************/ +#ifdef BLE_ESP32_ALIASES +int addAlias( uint8_t *addr, char *name){ + if (!addr || !name){ + return 0; + } + + int count = aliases.size(); + // replace name for existing address + for (int i = 0; i < count; i++){ + if (!memcmp(aliases[i]->addr, addr, 6)){ + strncpy(aliases[i]->name, name, sizeof(aliases[i]->name)); + aliases[i]->name[sizeof(aliases[i]->name)-1] = 0; + return 2; + } + } + + // replace addr for existing name + for (int i = 0; i < count; i++){ + if (!strcmp(aliases[i]->name, name)){ + memcpy(aliases[i]->addr, addr, 6); + return 2; + } + } + + BLE_ESP32::ble_alias_t *alias = new BLE_ESP32::ble_alias_t; + memcpy(alias->addr, addr, 6); + strncpy(alias->name, name, sizeof(alias->name)); + alias->name[sizeof(alias->name)-1] = 0; + aliases.push_back(alias); + return 1; +} +#endif + +/** + * @brief Remove all colons from null terminated char array + * + * @param _string Typically representing a MAC-address like AA:BB:CC:DD:EE:FF + */ +void stripColon(char* _string){ + uint32_t _length = strlen(_string); + uint32_t _index = 0; + while (_index < _length) { + char c = _string[_index]; + if(c==':'){ + memmove(_string+_index,_string+_index+1,_length-_index); + } + _index++; + } + _string[_index] = 0; +} + + +////////////////////////////////////////////////// +// use this for address interpretaton from string +// it looks for aliases, and converts AABBCCDDEEFF and AA:BB:CC:DD:EE:FF +int getAddr(uint8_t *dest, char *src){ + if (!dest || !src){ + return 0; + } +#ifdef BLE_ESP32_ALIASES + for (int i = 0; i < aliases.size(); i++){ + if (!strcmp(aliases[i]->name, src)){ + memcpy(dest, aliases[i]->addr, 6); + return 2; //was an alias + } + } +#endif + + char tmp[12+5+1]; + if (strlen(src) == 12+5){ + strcpy(tmp, src); + stripColon(tmp); + src = tmp; + } + + int len = fromHex(dest, src, 6); + if (len == 6){ + return 1; + } + // not found + return 0; +} + +static const char *noAlias = PSTR(""); + +//////////////////////////////////////////// +// use to display the alias name if required +const char *getAlias(uint8_t *addr){ + if (!addr){ + return noAlias; + } +#ifdef BLE_ESP32_ALIASES + for (int i = 0; i < aliases.size(); i++){ + if (!memcmp(aliases[i]->addr, addr, 6)){ + return aliases[i]->name; //was an alias + } + } +#endif + return noAlias; +} + + +/*********************************************************************************************\ + * Highest level BLE task control functions +\*********************************************************************************************/ + +static int StartBLE(void) { + if (BLEStop != 1){ + BLE_ESP32::BLEStartOperationTask(); + return 1; + } + AddLog_P(LOG_LEVEL_ERROR,PSTR("StartBLE - wait as BLEStop==1")); + + return 0; +} + +static int StopBLE(void){ + if (BLERunning){ + if (BLEStop != 1){ + BLEStop = 1; + AddLog_P(LOG_LEVEL_INFO,PSTR("StopBLE - BLEStop->1")); + BLEStopAt = esp_timer_get_time(); + // give a little time for it to stop. + vTaskDelay(1000/ portTICK_PERIOD_MS); + return 1; + } + AddLog_P(LOG_LEVEL_ERROR,PSTR("StopBLE - wait as BLEStop==1")); + return 0; + } else { + AddLog_P(LOG_LEVEL_ERROR,PSTR("StopBLE - was not running")); + return 1; + } +} + + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +static void CmndBLEPeriod(void) { + //ResponseCmndNumber(BLE.period); + ResponseCmndDone(); +} + + +////////////////////////////////////////////////////////////// +// Determine what to do with advertismaents +// BLEAdv0 -> suppress MQTT about devices found +// BLEAdv1 -> send MQTT about devices found after each scan +void CmndBLEAdv(void){ + switch(XdrvMailbox.index){ + case 0: + BLEAdvertMode = BLE_ESP32::BLE_NO_ADV_SEND; + break; + case 1: + BLEAdvertMode = BLE_ESP32::BLE_ADV_TELE; + break; + /*case 2: + BLEAdvertMode = BLE_ADV_ALL; + break;*/ + case 3: + break; + } + + ResponseCmndNumber(BLEAdvertMode); +} + + +////////////////////////////////////////////////////////////// +// Determine what to do with advertismaents +// BLEAdv0 -> suppress MQTT about devices found +// BLEAdv1 -> send MQTT about devices found after each scan +void CmndBLEDebug(void){ + BLEDebugMode = XdrvMailbox.index; + ResponseCmndNumber(BLEDebugMode); +} + +void CmndBLEDevices(void){ + switch(XdrvMailbox.index){ + case 0:{ + // clear devices delete + deleteSeenDevices(); + } break; + case 1:{ + BLEPublishDevices = 2; // mqtt publish as 'STAT' + } break; + } + ResponseCmndDone(); +} + +void CmndBLEMaxAge(void){ + switch(XdrvMailbox.index){ + case 1:{ + if (XdrvMailbox.data_len > 0) { + BLEMaxAge = XdrvMailbox.payload; + } + } break; + } + ResponseCmndIdxNumber(BLEMaxAge); + if (BLEMaxAge) deleteSeenDevices(BLEMaxAge); +} + +void CmndBLEAddrFilter(void){ + switch(XdrvMailbox.index){ + case 1:{ + if (XdrvMailbox.data_len > 0) { + BLEAddressFilter = XdrvMailbox.payload; + } + } break; + } + ResponseCmndIdxNumber(BLEAddressFilter); +} + + +////////////////////////////////////////////////////////////// +// Scan options +// BLEScan0 -> do a scan now if BLEMode == BLEModeScanByCommand +// BLEScan0 -> do a scan now if BLEMode == BLEModeScanByCommand for timesec seconds +// BLEScan1 0 -> Scans are passive +// BLEScan1 1 -> Scans are active +// more options could be added... +void CmndBLEScan(void){ + switch(XdrvMailbox.index){ + case 0:{ + if (XdrvMailbox.data_len > 0) { + BLEScanActiveMode = XdrvMailbox.payload; + ResponseCmndNumber(BLEScanActiveMode); + } else { + ResponseCmndChar("Invalid"); + } + } break; + + case 1: // do a manual scan now + switch (BLEMode){ + case BLEModeScanByCommand: { + int time = 20; + if (XdrvMailbox.data_len > 0) { + time = XdrvMailbox.payload; + if (time < 2) time = 2; + if (time > 40) time = 40; + } + BLETriggerScan = time; + ResponseCmndNumber(time); // -ve for fail for a few reasons + } break; + case BLEModeDisabled: + ResponseCmndChar("BLEDisabled"); + break; + case BLEModeRegularScan: + ResponseCmndChar("BLEActive"); + break; + } + break; + default: + ResponseCmndChar("Invalid"); + break; + } +} + + +////////////////////////////////////////////////////////////// +// Determine what to do with advertismaents +// BLEMode0 -> kill BLE completely +// BLEMode1 -> start BLE, scan by command +// BLEMode2 -> start BLE, regular scan +void CmndBLEMode(void){ + int val = XdrvMailbox.index; + if (XdrvMailbox.data_len > 0) { + val = XdrvMailbox.payload; + } + + switch(val){ + case BLEModeDisabled:{ + if (BLEMode != BLEModeDisabled){ + BLEMode = BLEModeDisabled; + StopBLE(); + ResponseCmndChar("StoppingBLE"); + } else { + ResponseCmndChar("Disabled"); + } + } break; + case BLEModeScanByCommand:{ + uint64_t now = esp_timer_get_time(); + switch(BLEMode){ + // when changing from regular to by command, + // stop the scan next loop + case BLEModeRegularScan: { + BLEMode = BLEModeScanByCommand; + BLEStopScan = 1; + ResponseCmndChar("BLEStopScan"); + } break; + case BLEModeDisabled: { + BLEMode = BLEModeScanByCommand; + StartBLE(); + ResponseCmndChar("StartingBLE"); + } break; + case BLEModeScanByCommand:{ + ResponseCmndChar("BLERunning"); + } break; + } + BLEScanLastAdvertismentAt = now; // note the time of the last advertisment + } break; + case BLEModeRegularScan:{ + uint64_t now = esp_timer_get_time(); + switch(BLEMode){ + case BLEModeDisabled: { + BLEMode = BLEModeRegularScan; + StartBLE(); + ResponseCmndChar("StartingBLE"); + } break; + case BLEModeScanByCommand:{ + BLEMode = BLEModeRegularScan; + ResponseCmndChar("BLEEnableScan"); + } break; + case BLEModeRegularScan:{ + BLEMode = BLEModeRegularScan; + ResponseCmndChar("BLERunning"); + } break; + } + BLEScanLastAdvertismentAt = now; // note the time of the last advertisment + } break; + default: + ResponseCmndChar("InvalidIndex"); + break; + } +} + + +////////////////////////////////////////// +// get more drtails for a single MAC address +// BLEDetails0 -> don;t send me anything +// BLEDetails1 -> send me details for once +// BLEDetails2 -> send me details for every advert if possible +// example: BLEDetails1 001A22092C9A +// details look like: +// MQT: tele/tasmota_esp32/BLE = {"details":{"mac":"001A22092C9A","p":"0C0943432D52542D4D2D424C450CFF0000000000000000000000"}} +// and incliude mac, complete advert payload, plus optional ,"lost":true if an advert was not captured because MQTT we already +// had one waiting to be sent +void CmndBLEDetails(void){ + switch(XdrvMailbox.index){ + case 0: + BLEDetailsRequest = 0; + ResponseCmndNumber(BLEDetailsRequest); + break; + + case 1: + case 2:{ + BLEDetailsRequest = 0; + if (getAddr(BLEDetailsMac, XdrvMailbox.data)){ + BLEDetailsRequest = XdrvMailbox.index; + ResponseCmndIdxChar(XdrvMailbox.data); + } else { + ResponseCmndChar("InvalidMac"); + } + } break; + + case 3:{ + BLEDetailsRequest = XdrvMailbox.index; + ResponseCmndNumber(BLEDetailsRequest); + } break; + + default: + ResponseCmndChar("InvalidIndex"); + break; + } +} + + +void CmndBLEAlias(void){ +#ifdef BLE_ESP32_ALIASES + int op = XdrvMailbox.index; + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Alias %d %s"), op, XdrvMailbox.data); + + int res = -1; + switch(op){ + case 0: + case 1:{ + char *p = strtok(XdrvMailbox.data, " ,="); + bool trigger = false; + int added = 0; + + do { + if (!p || !(*p)){ + break; + } + + uint8_t addr[6]; + char *mac = p; + int len = fromHex(addr, p, sizeof(addr)); + if (len != 6){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("Alias invalid mac %s"), p); + ResponseCmndChar("invalidmac"); + return; + } + + p = strtok(nullptr, " ,="); + char *name = p; + if (!p || !(*p)){ + int i = 0; + for (i = 0; i < aliases.size(); i++){ + BLE_ESP32::ble_alias_t *alias = aliases[i]; + if (!memcmp(alias->addr, addr, 6)){ + aliases.erase(aliases.begin() + i); + BLEAliasListResp(); + return; + } + } + ResponseCmndChar("invalidmac"); + return; + } + + AddLog_P(LOG_LEVEL_ERROR,PSTR("Add Alias mac %s = name %s"), mac, p); + if (addAlias( addr, name )){ + added++; + } + p = strtok(nullptr, " ,="); + } while (p); + + if (added){ + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Added %d Aliases"), added); + BLEAliasListResp(); + } else { + BLEAliasListResp(); + } + return; + } break; + case 2:{ // clear + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLEAlias clearing %d"), aliases.size()); + for (int i = aliases.size()-1; i >= 0; i--){ + BLE_ESP32::ble_alias_t *alias = aliases[i]; + aliases.pop_back(); + delete alias; + } + BLEAliasListResp(); + return; + } break; + } + ResponseCmndChar("invalididx"); +#endif +} + + +// SET the BLE name for a device - +// uses s:1800 c:2a00 and writes name to DEVICE +void CmndBLEName(void) { + char *p = strtok(XdrvMailbox.data, " "); + + if (!p || !(*p)){ + ResponseCmndIdxChar(PSTR("invalid")); + return; + } + + uint8_t addrbin[6]; + int addrres = BLE_ESP32::getAddr(addrbin, p); + NimBLEAddress addr(addrbin); + + if (addrres){ + if (addrres == 2){ + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE addr used alias: %s"), p); + } + +//#ifdef EQ3_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE cmd addr: %s -> %s"), p, addr.toString().c_str()); +//#endif + } else { + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE addr invalid: %s"), p); + ResponseCmndIdxChar(PSTR("invalidaddr")); + return; + } + + BLE_ESP32::generic_sensor_t *op = nullptr; + // ALWAYS use this function to create a new one. + int res = BLE_ESP32::newOperation(&op); + if (!res){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("Can't get a newOperation")); + ResponseCmndChar(PSTR("FAIL")); + return; + } else { + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got a newOperation from BLE")); + } + + op->addr = addr; + op->serviceUUID = NimBLEUUID("1800"); + op->characteristicUUID = NimBLEUUID("2A00"); + + // get next part of cmd + char *name = strtok(nullptr, " "); + bool write = false; + if (name && *name){ + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("write name %s"), name); + op->writelen = strlen(name); + memcpy(op->dataToWrite, name, op->writelen); + write = true; + } else { + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("read name")); + op->readlen = 1; + } + + res = BLE_ESP32::extQueueOperation(&op); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("queue res %d"), res); + if (!res){ + // if it fails to add to the queue, do please delete it + BLE_ESP32::freeOperation(&op); + AddLog_P(LOG_LEVEL_ERROR,PSTR("Failed to queue new operation - deleted")); + ResponseCmndChar(PSTR("QUEUEFAIL")); + return; + } + + if (write){ + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE: will write name")); + } else { + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE: will read name")); + } + ResponseCmndDone(); + return; +} + + + +////////////////////////////////////////////////////////////////////////// +// Command to cause BLE read/write/notify operations to be run. +////////////////////////////////////////////////////////////////////////// + +// we expect BLEOp0 - poll state +// we expect BLEOp1 m:MAC s:svc +// we expect BLEOp2 trigger queue of op. return is opid + +// returns: Done|FailCreate|FailNoOp|FailQueue|InvalidIndex| + +// BLEop0/1/2 will cause an MQTT send of ops currently known. +// on op complete/op fail, a MQTT send is triggered of all known ops, and the completed/failed op removed. + +// example: +// BLEOp1 M:001A22092CDB s:3e135142-654f-9090-134a-a6ff5bb77046 c:3fa4585a-ce4a-3bad-db4b-b8df8179ea09 w:03 n:d0e8434d-cd29-0996-af41-6c90f4e0eb2a go +// requests write of 03, and request wait for notify. + +// You may queue up operations. they are currently processed serially. +void CmndBLEOperation(void){ + + int op = XdrvMailbox.index; + + //AddLog_P(LOG_LEVEL_INFO,PSTR("op %d"), op); + + int res = -1; + + // if in progress, only op 0 or 11 are allowed + switch(op) { + case 0: +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("preview")); +#endif + BLEPostMQTTTrigger = 1; + break; + case 1: { + if (prepOperation){ + BLE_ESP32::freeOperation(&prepOperation); + } + int opres = BLE_ESP32::newOperation(&prepOperation); + if (!opres){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not create new operation")); +#endif + ResponseCmndChar("FailCreate"); + return; + } + // expect m:MAC s:svc + // < > are optional + char *p = strtok(XdrvMailbox.data, " ,"); + bool trigger = false; + + while (p){ + switch(*p | 0x20){ + case 'm':{ + uint8_t addr[6]; + if (getAddr(addr, p+2)){ + prepOperation->addr = NimBLEAddress(addr); + } else { + prepOperation->addr = NimBLEAddress(); + } + } break; + case 's':{ + prepOperation->serviceUUID = NimBLEUUID(p+2); + } break; + case 'c': + prepOperation->characteristicUUID = NimBLEUUID(p+2); + //strncpy(prepOperation->characteristicStr, p+2, sizeof(prepOperation->characteristicStr)-1); + break; + case 'n': + prepOperation->notificationCharacteristicUUID = NimBLEUUID(p+2); + //strncpy(prepOperation->notificationCharacteristicStr, p+2, sizeof(prepOperation->notificationCharacteristicStr)-1); + break; + case 'w': + prepOperation->writelen = fromHex(prepOperation->dataToWrite, p+2, sizeof(prepOperation->dataToWrite)); + break; + case 'u': // 'unique' context for this request + prepOperation->context = (void *)atoi(p+2); + break; + case 'r': + prepOperation->readlen = 1; + break; + case 'g': + if ((*(p+1))|0x20 == 'o'){ + trigger = true; + } + break; + } + + p = strtok(nullptr, " ,"); + } + + if (trigger){ + int u = (int)prepOperation->context; + int opres = BLE_ESP32::extQueueOperation(&prepOperation); + if (!opres){ + // NOTE: prepOperation will NOT have been deleted. + // this means you could retry with another BLEOp10. + // it WOULD be deleted if you sent another BELOP1 +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not queue new operation")); +#endif + ResponseCmndChar("FailQueue"); + return; + } else { + // NOTE: prepOperation has been set to null if we queued sucessfully. +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Operations queued:%d"), queuedOperations.size()); +#endif + char temp[40]; + sprintf(temp, "{\"opid\":%d,\"u\":%d}", lastopid-1, u); + Response_P(S_JSON_COMMAND_XVALUE, XdrvMailbox.command, temp); + // don't do this here... overwrites response + //BLE_ESP32::BLEPostMQTT(false); + return; + } + } else { + ResponseCmndChar("Prepared"); + //BLE_ESP32::BLEPostMQTT(false); + return; + } + } break; + + case 2: { + if (!prepOperation) { + ResponseCmndChar("FailNoOp"); + return; + } + //prepOperation->requestType = atoi(XdrvMailbox.data); + int u = (int)prepOperation->context; + int opres = BLE_ESP32::extQueueOperation(&prepOperation); + if (!opres){ + // NOTE: prepOperation will NOT have been deleted. + // this means you could retry with another BLEOp10. + // it WOULD be deleted if you sent another BELOP1 +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not queue new operation")); +#endif + ResponseCmndChar("FailQueue"); + } else { + // NOTE: prepOperation has been set to null if we queued sucessfully. +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Operations queued:%d"), queuedOperations.size()); +#endif + char temp[40]; + sprintf(temp, "{\"opid\":%d,\"u\":%d}", lastopid-1, u); + Response_P(S_JSON_COMMAND_XVALUE, XdrvMailbox.command, temp); + } + return; + } break; + + default: + ResponseCmndChar("InvalidIndex"); + return; + } + + ResponseCmndDone(); + return; +} + + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ +static void BLEPostMQTTSeenDevices(int type) { + int remains = 0; + nextSeenDev = 0; + + memset(TasmotaGlobal.mqtt_data, 0, sizeof(TasmotaGlobal.mqtt_data)); + ResponseTime_P(PSTR("")); + int timelen = strlen(TasmotaGlobal.mqtt_data); + char *dest = TasmotaGlobal.mqtt_data + timelen; + int maxlen = (sizeof(TasmotaGlobal.mqtt_data)-20) - timelen; + +// if (!TasmotaGlobal.ota_state_flag){ + do { + remains = getSeenDevicesToJson(dest, maxlen); + // no retain - this is present devices, not historic + if (type == 1){ + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), 0); + } else { + MqttPublishPrefixTopic_P(STAT, PSTR("BLE"), 0); + } + } while (remains); +// } +} + +static void BLEPostMQTT(bool onlycompleted) { +// if (TasmotaGlobal.ota_state_flag) return; + + + if (prepOperation || completedOperations.size() || queuedOperations.size() || currentOperations.size()){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("some to show")); +#endif + if (prepOperation && !onlycompleted){ + std::string out = BLETriggerResponse(prepOperation); + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("prep sent %s"), out.c_str()); +#endif + } + + if (queuedOperations.size() && !onlycompleted){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("queued %d"), queuedOperations.size()); +#endif + for (int i = 0; i < queuedOperations.size(); i++){ + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEPost1"); + + generic_sensor_t *toSend = queuedOperations[i]; + if (!toSend) { + break; + } else { + std::string out = BLETriggerResponse(toSend); + localmutex.give(); + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("queued %d sent %s"), i, out.c_str()); +#endif + //break; + } + } + } + + if (currentOperations.size() && !onlycompleted){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("current %d"), currentOperations.size()); +#endif + for (int i = 0; i < currentOperations.size(); i++){ + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEPost2"); + generic_sensor_t *toSend = currentOperations[i]; + if (!toSend) { + break; + } else { + std::string out = BLETriggerResponse(toSend); + localmutex.give(); + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("curr %d sent %s"), i, out.c_str()); +#endif + //break; + } + } + } + + if (completedOperations.size()){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("completed %d"), completedOperations.size()); +#endif + do { + generic_sensor_t *toSend = nextOperation(&completedOperations); + if (!toSend) { + break; // break from while loop + } else { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE:completedOperation removed")); +#endif + std::string out = BLETriggerResponse(toSend); + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); + // we alreayd removed this from the queues, so now delete + delete toSend; + //break; + } + //break; + } while (1); + } + } else { + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("{\"BLEOperation\":{}}")); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); + } +} + +static void mainThreadBLETimeouts() { + uint64_t now = esp_timer_get_time(); + + if (!BLERunning){ + BLELastLoopTime = now; // initialise the time of the last advertisment + BLEScanLastAdvertismentAt = now; // initialise the time of the last advertisment + return; + } + + if (BLEStop == 1){ + if (BLEStopAt + 30L*1000L*1000L < now){ // if asked to stop > 30s ago... + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Stop Timeout - restart Tasmota")); + BLERestartTasmota = 2; + BLEStopAt = now; + } + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Awaiting BLEStop")); + return; + } + + // if no adverts for 120s, and BLE is running, retsart NimBLE. + // belt and braces.... + uint64_t adTimeout = ((uint64_t)BLEMaxTimeBetweenAdverts)*1000L*1000L; + if (BLEScanLastAdvertismentAt + adTimeout < now){ + BLEScanLastAdvertismentAt = now; // initialise the time of the last advertisment + BLERestartNimBLE = 1; + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: scan stall? no adverts > 120s, restart BLE")); + + BLERestartBLEReason = BLE_RESTART_BLE_REASON_ADVERT_BLE_TIMEOUT; + } + + // if stuck and have not done task for 120s, something is seriously wrong. + // restart Tasmota completely. (belt and braces) + uint64_t bleLoopTimeout = ((uint64_t)BLEMaxTaskLoopTime)*1000L*1000L; + if (BLELastLoopTime + bleLoopTimeout < now){ + BLELastLoopTime = now; // initialise the time of the last advertisment + BLERestartTasmota = 10; + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: BLETask stall > 120s, restart Tasmota in 10s")); + BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_BLE_LOOP_STALLED; + } +} + + +static void mainThreadOpCallbacks() { + if (completedOperations.size()){ + //AddLog_P(LOG_LEVEL_INFO,PSTR("completed %d"), completedOperations.size()); + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEMainCB"); + + // find this operation in currentOperations, and remove it. + // in reverse so we can erase them safely. + for (int i = completedOperations.size()-1; i >= 0 ; i--){ + generic_sensor_t *op = completedOperations[i]; + + bool callbackres = false; + + if (op->completecallback){ + try { + OPCOMPLETE_CALLBACK *pFn = (OPCOMPLETE_CALLBACK *)(op->completecallback); + callbackres = pFn(op); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("op->completecallback %d"), callbackres); +#endif + } catch(const std::exception& e){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in op->completecallback")); +#endif + } + } + + if (!callbackres){ + for (int i = 0; i < operationsCallbacks.size(); i++){ + try { + OPCOMPLETE_CALLBACK *pFn = operationsCallbacks[i]; + callbackres = pFn(op); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("operationsCallbacks %d %d"), i, callbackres); +#endif + if (callbackres){ + break; // this callback ate the op. + } + } catch(const std::exception& e){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in operationsCallbacks")); +#endif + } + } + } + + // if some callback told us not to send on MQTT, then remove from completed and delete the data + if (callbackres){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("callbackres true -> delete op")); +#endif + completedOperations.erase(completedOperations.begin() + i); + delete op; + } + } + } +} + + +static void BLEShow(bool json) +{ + if (json){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("show json %d"),json); +#endif + uint32_t totalCount = BLEAdvertisment.totalCount; + uint32_t deviceCount = seenDevices.size(); + + ResponseAppend_P(PSTR(",\"BLE\":{\"scans\":%u,\"adverts\":%u,\"devices\":%u,\"resets\":%u}"), BLEScanCount, totalCount, deviceCount, BLEResets); + } +#ifdef USE_WEBSERVER + else { + //WSContentSend_PD(HTTP_MI32, i+1,stemp,MIBLEsensors.size()); + } +#endif // USE_WEBSERVER + +} + +/*void BLEAliasMqttList(){ + ResponseTime_P(PSTR(",\"BLEAlias\":[")); + for (int i = 0; i < aliases.size(); i++){ + if (i){ + ResponseAppend_P(PSTR(",")); + } + char tmp[20]; + ToHex_P(aliases[i]->addr,6,tmp,20,0); + ResponseAppend_P(PSTR("{\"%s\":\"%s\"}"), tmp, aliases[i]->name); + } + ResponseAppend_P(PSTR("]}")); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); +}*/ + +void BLEAliasListResp(){ + Response_P(PSTR("{\"BLEAlias\":{")); + for (int i = 0; i < aliases.size(); i++){ + if (i){ + ResponseAppend_P(PSTR(",")); + } + char tmp[20]; + ToHex_P(aliases[i]->addr,6,tmp,20,0); + ResponseAppend_P(PSTR("\"%s\":\"%s\""), tmp, aliases[i]->name); + } + ResponseAppend_P(PSTR("}}")); +} + + +static void BLEDiag() +{ + uint32_t totalCount = BLEAdvertisment.totalCount; + uint32_t deviceCount = seenDevices.size(); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE:scans:%u,advertisements:%u,devices:%u,resets:%u,BLEStop:%d,BLERunning:%d,BLERunningScan:%d,BLELoopCount:%u,BLEOpCount:%u"), BLEScanCount, totalCount, deviceCount, BLEResets, BLEStop, BLERunning, BLERunningScan, BLELoopCount, BLEOpCount); +#endif +} + +/** + * @brief creates a JSON representing a single operation. + * + */ +std::string BLETriggerResponse(generic_sensor_t *toSend){ + char temp[100]; + if (!toSend) return ""; + std::string out = "{\"BLEOperation\":{\"opid\":\""; + sprintf(temp, "%d", toSend->opid); // note only 10 long! + out = out + temp; +/* out = out + "\",\"state\":\""; + sprintf(t, "%d", toSend->state); + out = out + t;*/ + out = out + "\",\"stat\":\""; + sprintf(temp, "%d", toSend->state); + out = out + temp; + out = out + "\",\"state\":\""; + out = out + getStateString(toSend->state); + + if (toSend->addr != NimBLEAddress()){ + out = out + "\",\"MAC\":\""; + uint8_t addrrev[6]; + memcpy(addrrev, toSend->addr.getNative(), 6); + ReverseMAC(addrrev); + dump(temp, 13, addrrev, 6); + out = out + temp; + } + if (toSend->context){ + out = out + "\",\"u\":\""; + sprintf(temp, "%d", (int32_t)toSend->context); + out = out + temp; + } + if (toSend->serviceUUID.bitSize()){ + out = out + "\",\"svc\":\""; + out = out + toSend->serviceUUID.toString(); + } + if (toSend->characteristicUUID.bitSize()){ + out = out + "\",\"char\":\""; + out = out + toSend->characteristicUUID.toString(); + } + if (toSend->notificationCharacteristicUUID.bitSize()){ + out = out + "\",\"notifychar\":\""; + out = out + toSend->notificationCharacteristicUUID.toString(); + } + out = out + "\""; + if (toSend->readlen){ + dump(temp, 99, toSend->dataRead, toSend->readlen); + if (toSend->readtruncated){ + strcat(temp, "+"); + } + out = out + ",\"read\":\""; + out = out + temp; + out = out + "\""; + } + if (toSend->writelen){ + dump(temp, 99, toSend->dataToWrite, toSend->writelen); + out = out + ",\"write\":\""; + out = out + temp; + out = out + "\""; + } + if (toSend->notifylen){ + dump(temp, 99, toSend->dataNotify, toSend->notifylen); + if (toSend->notifytruncated){ + strcat(temp, "+"); + } + out = out + ",\"notify\":\""; + out = out + temp; + out = out + "\""; + } + out = out + "}}"; + return out; +} + +#ifdef USE_WEBSERVER + +#define WEB_HANDLE_BLE "ble" +#define D_CONFIGURE_BLE "Configure BLE" +#define D_BLE_PARAMETERS "Bluetooth Settings" +#define D_MQTT_BLE_ENABLE "Enable Bluetooth" +#define D_MQTT_BLE_ACTIVESCAN "Enable Active Scan(*)" +#define D_BLE_DEVICES "Devices Seen" + +const char HTTP_BTN_MENU_BLE[] PROGMEM = + "

"; + +const char HTTP_FORM_BLE[] PROGMEM = + "
 " D_BLE_PARAMETERS " " + "
" + "

" + "

" + "

items marked (*) are not stored in config

"; + + +const char HTTP_BLE_DEV_STYLE[] PROGMEM = "th, td { padding-left:5px; }"; +const char HTTP_BLE_DEV_START[] PROGMEM = + "
 " D_BLE_DEVICES " " + ""; +const char HTTP_BLE_DEV[] PROGMEM = + ""; +const char HTTP_BLE_DEV_END[] PROGMEM = + "
"; + +void HandleBleConfiguration(void) +{ + +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG, PSTR("HandleBleConfiguration")); +#endif + + if (!HttpCheckPriviledgedAccess()) { +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG, PSTR("!HttpCheckPriviledgedAccess()")); +#endif + return; + } + +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_CONFIGURE_BLE)); +#endif + + char tmp[20]; + WebGetArg("en", tmp, sizeof(tmp)); + +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("arg en is %s"), tmp); +#endif + + if (Webserver->hasArg("save")) { +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE SETTINGS SAVE")); +#endif + Settings.flag5.mi32_enable = Webserver->hasArg("e0"); // + BLEScanActiveMode = (Webserver->hasArg("e1")?1:0); // + + SettingsSaveAll(); + HandleConfiguration(); + return; + } +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG, PSTR("!SAVE")); +#endif + char str[TOPSZ]; + + WSContentStart_P(PSTR(D_CONFIGURE_BLE)); + WSContentSendStyle_P(HTTP_BLE_DEV_STYLE); + //WSContentSendStyle(); + WSContentSend_P(HTTP_FORM_BLE, + (Settings.flag5.mi32_enable) ? " checked" : "", + (BLEScanActiveMode) ? " checked" : "" + ); + WSContentSend_P(HTTP_FORM_END); + + + { + //TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEConf"); + int number = seenDevices.size(); + if (number){ + WSContentSend_P(HTTP_BLE_DEV_START); + uint64_t now = esp_timer_get_time(); + now = now/1000L; + now = now/1000L; + uint32_t nowS = (uint32_t)now; + + for (int i = 0; i < number; i++){ + BLE_ESP32::BLE_simple_device_t* dev = seenDevices[i]; + char addr[20]; + dump(addr, 20, dev->mac, 6); + uint8_t addrtype = dev->addrtype; + const char *alias = getAlias(dev->mac); + uint64_t lastseen = dev->lastseen/1000L; + lastseen = lastseen/1000L; + uint32_t lastseenS = (uint32_t) lastseen; + uint32_t ageS = nowS-lastseenS; + + WSContentSend_P(HTTP_BLE_DEV, addr, addrtype, alias, dev->name, dev->RSSI, ageS, dev->maxAge); + } + WSContentSend_P(HTTP_BLE_DEV_END); + } + } + + WSContentSpaceButton(BUTTON_CONFIGURATION); + WSContentStop(); + +} +#endif + + +} // end namespace BLE_ESP32 + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +int ExtStopBLE(){ + AddLog_P(LOG_LEVEL_INFO, PSTR("Stopping BLE if active - upgrade starting?")); + BLE_ESP32::BLEMode = BLE_ESP32::BLEModeDisabled; + BLE_ESP32::StopBLE(); + return 0; +} + +bool Xdrv52(uint8_t function) +{ + //if (!Settings.flag5.mi32_enable) { return false; } // SetOption115 - Enable ESP32 BLE BLE + + bool result = false; + + if (FUNC_INIT == function){ + BLE_ESP32::BLEPreInit(); + } + + if (!BLE_ESP32::BLEInitState) { + if (function == FUNC_EVERY_250_MSECOND) { + BLE_ESP32::BLEInit(); + } + return result; + } + switch (function) { + case FUNC_EVERY_50_MSECOND: + BLE_ESP32::BLEEvery50mSecond(); + //############################# DEBUG + TasmotaGlobal.seriallog_timer = 0; + break; + case FUNC_EVERY_SECOND: + BLE_ESP32::BLEEverySecond(false); + break; + case FUNC_COMMAND: + result = DecodeCommand(BLE_ESP32::kBLE_Commands, BLE_ESP32::BLE_Commands); + break; + case FUNC_JSON_APPEND: + BLE_ESP32::BLEShow(1); + break; + + // next second, we will publish to our MQTT topic. + case FUNC_AFTER_TELEPERIOD: + BLE_ESP32::BLEPublishDevices = 1; // mqtt publish as 'TELE' + break; + +#ifdef USE_WEBSERVER + case FUNC_WEB_ADD_BUTTON: + WSContentSend_P(BLE_ESP32::HTTP_BTN_MENU_BLE); + break; + case FUNC_WEB_ADD_HANDLER: + WebServer_on(PSTR("/" WEB_HANDLE_BLE), BLE_ESP32::HandleBleConfiguration); + break; + + case FUNC_WEB_SENSOR: + BLE_ESP32::BLEShow(0); + break; +#endif // USE_WEBSERVER + } + return result; +} + + + +/*********************************************************************************************\ + * Example Advertisment callback +\*********************************************************************************************/ + +#ifdef EXAMPLE_ADVERTISMENT_CALLBACK + +// match ADVERTISMENT_CALLBACK +int myAdvertCallback(BLE_ESP32::ble_advertisment_t *pStruct) { + + // indicate others can also hear this + // to say 'I want this exclusively', return true. + return 0; + +} +#endif +/*********************************************************************************************\ + * End of Example Advertisment callback +\*********************************************************************************************/ + + +/*********************************************************************************************\ + * Example Operations callbacks +\*********************************************************************************************/ +#ifdef EXAMPLE_OPERATION_CALLBACK + +// this one is used to demonstrate processing ALL operations +int myOpCallback(BLE_ESP32::generic_sensor_t *pStruct){ + AddLog_P(LOG_LEVEL_INFO,PSTR("myOpCallback")); + return 0; // return true to block MQTT broadcast +} + +// this one is used to demonstrate processing of ONE specific operation +int myOpCallback2(BLE_ESP32::generic_sensor_t *pStruct){ + AddLog_P(LOG_LEVEL_INFO,PSTR("myOpCallback2")); + return 1; // return true to block MQTT broadcast +} +#endif +/*********************************************************************************************\ + * End of Example Operations callbacks +\*********************************************************************************************/ + +void installExamples(){ +#ifdef EXAMPLE_ADVERTISMENT_CALLBACK + BLE_ESP32::registerForAdvertismentCallbacks((const char *)"test myOpCallback", &myAdvertCallback); +#endif + +#ifdef EXAMPLE_OPERATION_CALLBACK + BLE_ESP32:registerForOpCallbacks((const char *)"test myOpCallback", &myOpCallback); +#endif +} + +void sendExample(){ +#ifdef EXAMPLE_OPERATION_CALLBACK + BLE_ESP32::generic_sensor_t *op = nullptr; + int res = BLE_ESP32::newOperation(&op); + if (!res){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not create new operation")); + return; + } + strncpy(op->MAC, "001A22092EE0", sizeof(op->MAC)); + strncpy(op->serviceStr, "3e135142-654f-9090-134a-a6ff5bb77046", sizeof(op->serviceStr)); + strncpy(op->characteristicStr, "3fa4585a-ce4a-3bad-db4b-b8df8179ea09", sizeof(op->characteristicStr)); + strncpy(op->notificationCharacteristicStr, "d0e8434d-cd29-0996-af41-6c90f4e0eb2a", sizeof(op->notificationCharacteristicStr)); + op->writelen = BLE_ESP32::fromHex(op->dataToWrite, (char *)"4040", sizeof(op->dataToWrite)); + + // this op will call us back on complete or failure. + op->completecallback = (void *)myOpCallback2; + res = BLE_ESP32::extQueueOperation(&op); + if (!res){ + // if it fails to add to the queue, do please delete it + BLE_ESP32::freeOperation(&op); + AddLog_P(LOG_LEVEL_ERROR,PSTR("Failed to queue new operation - deleted")); + return; + } + +#endif +} + + + +#endif +#endif // ESP32 + + diff --git a/tasmota/xsns_52_ibeacon.ino b/tasmota/xsns_52_ibeacon.ino index 723446700..8fb924d4e 100755 --- a/tasmota/xsns_52_ibeacon.ino +++ b/tasmota/xsns_52_ibeacon.ino @@ -17,6 +17,10 @@ along with this program. If not, see . */ +// for testing of BLE_ESP32, we remove this completely, and instead add the modified xsns_52_ibeacon_BLE_ESP32.ino +// in the future this may be more fine-grained, e.g. to allow hm17 for this, and BLE-ESP32 for other +#ifndef USE_BLE_ESP32 + #ifdef USE_IBEACON #define XSNS_52 52 @@ -279,13 +283,16 @@ void ESP32Init() { if (TasmotaGlobal.global_state.wifi_down) { return; } - TasmotaGlobal.wifi_stay_asleep = true; if (WiFi.getSleep() == false) { - AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Put WiFi modem in sleep mode"),"BLE"); - WiFi.setSleep(true); // Sleep + if (0 == Settings.flag3.sleep_normal) { + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: About to restart to put WiFi modem in sleep mode"),"BLE"); + Settings.flag3.sleep_normal = 1; // SetOption60 - Enable normal sleep instead of dynamic sleep + TasmotaGlobal.restart_flag = 2; + } + return; } - AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Initializing Bluetooth..."),"BLE"); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Initializing Blueetooth..."),"BLE"); if (!ESP32BLE.mode.init) { NimBLEDevice::init(""); @@ -1012,3 +1019,5 @@ bool Xsns52(byte function) } #endif // USE_IBEACON + +#endif \ No newline at end of file diff --git a/tasmota/xsns_52_ibeacon_BLE_ESP32.ino b/tasmota/xsns_52_ibeacon_BLE_ESP32.ino new file mode 100644 index 000000000..687bf05cf --- /dev/null +++ b/tasmota/xsns_52_ibeacon_BLE_ESP32.ino @@ -0,0 +1,952 @@ +/* + xsns_52_ibeacon.ino - Support for HM17 BLE Module + ibeacon reader on Tasmota + + Copyright (C) 2020 Gerhard Mutz 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 . +*/ + +// for testing of BLE_ESP32, we remove xsns_52_ibeacon.ino completely, and instead add this modified xsns_52_ibeacon_BLE_ESP32.ino +// in the future this may be more fine-grained, e.g. to allow hm17 for this, and BLE-ESP32 for other +#ifdef USE_BLE_ESP32 + +#ifdef USE_IBEACON_ESP32 + +#ifdef USE_IBEACON + +#define XSNS_52 52 + +// keyfob expires after N seconds +#define IB_TIMEOUT_INTERVAL 30 +// does a passive scan every N seconds +#define IB_UPDATE_TIME_INTERVAL 10 + +// should be in Settings +#if 1 + uint8_t ib_upd_interval,ib_tout_interval; + #define IB_UPDATE_TIME ib_upd_interval + #define IB_TIMEOUT_TIME ib_tout_interval +#else + #undef IB_UPDATE_TIME + #undef IB_TIMEOUT_TIME + #define IB_UPDATE_TIME Settings.ib_upd_interval + #define IB_TIMEOUT_TIME Settings.ib_tout_interval +#endif + +char ib_mac[14]; + + + struct { + union { + struct { + uint32_t init:1; + }; + uint32_t all = 0; + } mode; + } ESP32BLE; + + void *beaconmutex = nullptr; + + #define ENDIAN_CHANGE_U16(x) ((((x)&0xFF00) >> 8) + (((x)&0xFF) << 8)) + +#else + + #include + + #define TMSBSIZ52 512 + + #define HM17_BAUDRATE 9600 + + #define IBEACON_DEBUG + + // use this for Version 110 + #define HM17_V110 + + TasmotaSerial *IBEACON_Serial = nullptr; + + uint8_t hm17_found,hm17_cmd,hm17_flag; + + #ifdef IBEACON_DEBUG + uint8_t hm17_debug=0; + #endif + + // 78 is max serial response + #define HM17_BSIZ 128 + char hm17_sbuffer[HM17_BSIZ]; + uint8_t hm17_sindex,hm17_result,hm17_scanning,hm17_connecting; + uint32_t hm17_lastms; + + enum {HM17_TEST,HM17_ROLE,HM17_IMME,HM17_DISI,HM17_IBEA,HM17_SCAN,HM17_DISC,HM17_RESET,HM17_RENEW,HM17_CON}; + #define HM17_SUCESS 99 + +#endif + +struct IBEACON { + char FACID[8]; + char UID[32]; + char MAJOR[4]; + char MINOR[4]; + char PWR[2]; + char MAC[12]; + char RSSI[4]; +#ifdef USE_IBEACON_ESP32 + char NAME[16]; +#endif +}; + +#ifdef USE_IBEACON_ESP32 + #define MAX_IBEACONS 32 +#else + #define MAX_IBEACONS 16 +#endif + +struct IBEACON_UID { + char MAC[12]; + char RSSI[4]; + char UID[32]; + char MAJOR[4]; + char MINOR[4]; + uint8_t FLAGS; + uint8_t TIME; +#ifdef USE_IBEACON_ESP32 + uint8_t REPORTED; + uint8_t REPTIME; + char NAME[16]; +#endif +} ibeacons[MAX_IBEACONS]; + +#ifdef USE_IBEACON_ESP32 + +uint32_t ibeacon_add(struct IBEACON *ib); + +void ESP32BLE_ReverseStr(uint8_t _mac[], uint8_t len=6){ + uint8_t _reversedMAC[len]; + for (uint8_t i=0; i>4) & 0xF]; + pout[1] = hex[ pgm_read_byte(pin) & 0xF]; + } +} + +int advertismentCallback(BLE_ESP32::ble_advertisment_t *pStruct) +{ + struct IBEACON ib; + BLEAdvertisedDevice *advertisedDevice = pStruct->advertisedDevice; + + char sRSSI[6]; + itoa(pStruct->RSSI,sRSSI,10); + const uint8_t *MAC = pStruct->addr; + + int manufacturerDataLen = 0; + std::string data; + if (advertisedDevice->haveManufacturerData()){ + data = advertisedDevice->getManufacturerData(); + manufacturerDataLen = data.length(); + } + if (manufacturerDataLen){ + const uint8_t *manufacturerData = (const uint8_t *)data.data(); + DumpHex(manufacturerData, 2, ib.FACID); + if (manufacturerDataLen == 25 && + manufacturerData[0] == 0x4C && + manufacturerData[1] == 0x00) + { + BLEBeacon oBeacon = BLEBeacon(); + oBeacon.setData(std::string((char *)manufacturerData, manufacturerDataLen)); + uint8_t UUID[16]; + memcpy(UUID,oBeacon.getProximityUUID().getNative()->u128.value,16); + ESP32BLE_ReverseStr(UUID,16); + + uint16_t Major = ENDIAN_CHANGE_U16(oBeacon.getMajor()); + uint16_t Minor = ENDIAN_CHANGE_U16(oBeacon.getMinor()); + uint8_t PWR = oBeacon.getSignalPower(); + + DumpHex((const unsigned char*)&UUID,16,ib.UID); + DumpHex((const unsigned char*)&Major,2,ib.MAJOR); + DumpHex((const unsigned char*)&Minor,2,ib.MINOR); + DumpHex((const unsigned char*)&PWR,1,ib.PWR); + DumpHex((const unsigned char*)MAC,6,ib.MAC); + memcpy(ib.RSSI,sRSSI,4); + memset(ib.NAME,0x0,16); + + // if we added it + if (ibeacon_add(&ib) == 1){ + AddLog_P(LOG_LEVEL_DEBUG, PSTR("%s: MAC: %s Major: %d Minor: %d UUID: %s Power: %d RSSI: %d"), + "iBeacon", + advertisedDevice->getAddress().toString().c_str(), + Major, Minor, + oBeacon.getProximityUUID().toString().c_str(), + PWR, pStruct->RSSI); + } + return 0; + } + } + + // no manufacturer data, or not recognised. + // still have an RSSi.... + memset(ib.UID,'0',32); + memset(ib.MAJOR,'0',4); + memset(ib.MINOR,'0',4); + memset(ib.PWR,'0',2); + DumpHex((const unsigned char*)MAC,6,ib.MAC); + memcpy(ib.RSSI,sRSSI,4); + + if (advertisedDevice->haveName()) { + strncpy(ib.NAME,advertisedDevice->getName().c_str(),16); + } else { + memset(ib.NAME,0x0,16); + } + + ibeacon_add(&ib); + return 0; +} + +void ESP32Init() { + + if (!ESP32BLE.mode.init) { + ESP32BLE.mode.init = 1; + IB_UPDATE_TIME=IB_UPDATE_TIME_INTERVAL; + IB_TIMEOUT_TIME=IB_TIMEOUT_INTERVAL; + } +} + + +#endif + +void IBEACON_Init() { + + +#ifdef USE_IBEACON_ESP32 + BLE_ESP32::registerForAdvertismentCallbacks((const char *)"iBeacon", advertismentCallback); +#else + + hm17_found=0; + +// actually doesnt work reliably with software serial + if (PinUsed(GPIO_IBEACON_RX) && PinUsed(GPIO_IBEACON_TX)) { + IBEACON_Serial = new TasmotaSerial(Pin(GPIO_IBEACON_RX), Pin(GPIO_IBEACON_TX),1,0,TMSBSIZ52); + if (IBEACON_Serial->begin(HM17_BAUDRATE)) { + if (IBEACON_Serial->hardwareSerial()) { + ClaimSerial(); + } + hm17_sendcmd(HM17_TEST); + hm17_lastms=millis(); + // in case of using Settings this has to be moved + IB_UPDATE_TIME=IB_UPDATE_TIME_INTERVAL; + IB_TIMEOUT_TIME=IB_TIMEOUT_INTERVAL; + } + } + +#endif + +} + +#ifdef USE_IBEACON_ESP32 + +void esp32_every_second(void) { + for (uint32_t cnt=0; cnt < MAX_IBEACONS; cnt++) { + if (ibeacons[cnt].FLAGS) { + uint8_t mac[6]; + char tmp[13]; + memcpy(tmp, ibeacons[cnt].MAC, 12); + tmp[12] = 0; + BLE_ESP32::fromHex(mac, tmp, 6); + // use global device timeouts from BLE_ESP32. + + uint32_t ageS = BLE_ESP32::devicePresent(mac); + + // if device not present at all. + if (!ageS){ + //AddLog_P(LOG_LEVEL_INFO, PSTR("iBeacon no device %s %02x%02x%02x%02x%02x%02x"),tmp, mac[0],mac[1], mac[2],mac[3], mac[4],mac[5]); + ibeacons[cnt].FLAGS=0; + ibeacon_mqtt(ibeacons[cnt].MAC,"0000",ibeacons[cnt].UID,ibeacons[cnt].MAJOR,ibeacons[cnt].MINOR,ibeacons[cnt].NAME); + } else { + //AddLog_P(LOG_LEVEL_INFO, PSTR("iBeacon device %s %02x%02x%02x%02x%02x%02x"),tmp, mac[0],mac[1], mac[2],mac[3], mac[4],mac[5]); + } + //ibeacons[cnt].TIME++; + ibeacons[cnt].REPTIME++; // counter used to send mqtt for a dev regularly + } + } +} + +#else + +void hm17_every_second(void) { + if (!IBEACON_Serial) return; + + if (hm17_found) { + if (IB_UPDATE_TIME && (TasmotaGlobal.uptime%IB_UPDATE_TIME==0)) { + if (hm17_cmd!=99) { + if (hm17_flag&2) { + ib_sendbeep(); + } else { + if (!hm17_connecting) { + hm17_sendcmd(HM17_DISI); + } + } + } + } + for (uint32_t cnt=0;cntIB_TIMEOUT_TIME) { + ibeacons[cnt].FLAGS=0; + ibeacon_mqtt(ibeacons[cnt].MAC,"0000",ibeacons[cnt].UID,ibeacons[cnt].MAJOR,ibeacons[cnt].MINOR); + } + } + } + } else { + if (TasmotaGlobal.uptime%20==0) { + hm17_sendcmd(HM17_TEST); + } + } +} + +void hm17_sbclr(void) { + memset(hm17_sbuffer,0,HM17_BSIZ); + hm17_sindex=0; + //IBEACON_Serial->flush(); +} + +void hm17_sendcmd(uint8_t cmd) { + hm17_sbclr(); + hm17_cmd=cmd; +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("hm17cmd %d"),cmd); +#endif + switch (cmd) { + case HM17_TEST: + IBEACON_Serial->write("AT"); + break; + case HM17_ROLE: + IBEACON_Serial->write("AT+ROLE1"); + break; + case HM17_IMME: + IBEACON_Serial->write("AT+IMME1"); + break; + case HM17_DISI: + IBEACON_Serial->write("AT+DISI?"); + hm17_scanning=1; + break; + case HM17_IBEA: + IBEACON_Serial->write("AT+IBEA1"); + break; + case HM17_RESET: + IBEACON_Serial->write("AT+RESET"); + break; + case HM17_RENEW: + IBEACON_Serial->write("AT+RENEW"); + break; + case HM17_SCAN: + IBEACON_Serial->write("AT+SCAN5"); + break; + case HM17_DISC: + IBEACON_Serial->write("AT+DISC?"); + hm17_scanning=1; + break; + case HM17_CON: + IBEACON_Serial->write((const uint8_t*)"AT+CON",6); + IBEACON_Serial->write((const uint8_t*)ib_mac,12); + hm17_connecting=1; + break; + } +} + +#endif + +uint32_t ibeacon_add(struct IBEACON *ib) { +/* if (!strncmp(ib->MAJOR,"4B1C",4)) { + return 0; + } + */ + if (!strncmp(ib->RSSI,"0",1)) { + return 0; + } + + // don't bother protecting this. + //TasAutoMutex localmutex(&beaconmutex, "iBeacAdd"); + + // keyfob starts with ffff, ibeacon has valid facid + if (!strncmp(ib->MAC,"FFFF",4) || strncmp(ib->FACID,"00000000",8)) { + for (uint32_t cnt=0;cntUID,PSTR("00000000000000000000000000000000"),32)) { + if (!strncmp(ibeacons[cnt].MAC,ib->MAC,12)) { + // exists + memcpy(ibeacons[cnt].RSSI,ib->RSSI,4); + ibeacons[cnt].TIME=0; +#ifdef USE_IBEACON_ESP32 + if (ibeacons[cnt].REPTIME >= IB_UPDATE_TIME) { + ibeacons[cnt].REPTIME = 0; + ibeacons[cnt].REPORTED = 0; + } +#endif + return 2; + } + } else { + if (!strncmp(ibeacons[cnt].UID,ib->UID,32)) { + // exists + memcpy(ibeacons[cnt].RSSI,ib->RSSI,4); + ibeacons[cnt].TIME=0; +#ifdef USE_IBEACON_ESP32 + if (ibeacons[cnt].REPTIME >= IB_UPDATE_TIME) { + ibeacons[cnt].REPTIME = 0; + ibeacons[cnt].REPORTED = 0; + } +#endif + return 2; + } + } + } + } + for (uint32_t cnt=0;cntMAC,12); + memcpy(ibeacons[cnt].RSSI,ib->RSSI,4); + memcpy(ibeacons[cnt].UID,ib->UID,32); + memcpy(ibeacons[cnt].MAJOR,ib->MAJOR,4); + memcpy(ibeacons[cnt].MINOR,ib->MINOR,4); + ibeacons[cnt].FLAGS=1; + ibeacons[cnt].TIME=0; +#ifdef USE_IBEACON_ESP32 + memcpy(ibeacons[cnt].NAME,ib->NAME,16); + ibeacons[cnt].REPTIME = 0; + ibeacons[cnt].REPORTED = 0; +#endif + return 1; + } + } + } + return 0; +} + +#ifndef USE_IBEACON_ESP32 + +void hm17_decode(void) { + struct IBEACON ib; + switch (hm17_cmd) { + case HM17_TEST: + if (!strncmp(hm17_sbuffer,"OK",2)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("AT OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + hm17_found=1; + } + break; + case HM17_ROLE: + if (!strncmp(hm17_sbuffer,"OK+Set:1",8)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("ROLE OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + } + break; + case HM17_IMME: + if (!strncmp(hm17_sbuffer,"OK+Set:1",8)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("IMME OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + } + break; + case HM17_IBEA: + if (!strncmp(hm17_sbuffer,"OK+Set:1",8)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("IBEA OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + } + break; + case HM17_SCAN: + if (!strncmp(hm17_sbuffer,"OK+Set:5",8)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("SCAN OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + } + break; + case HM17_RESET: + if (!strncmp(hm17_sbuffer,"OK+RESET",8)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("RESET OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + } + break; + case HM17_RENEW: + if (!strncmp(hm17_sbuffer,"OK+RENEW",8)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("RENEW OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + } + break; + case HM17_CON: + if (!strncmp(hm17_sbuffer,"OK+CONNA",8)) { + hm17_sbclr(); +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("CONNA OK")); +#endif + hm17_connecting=2; + break; + } + if (!strncmp(hm17_sbuffer,"OK+CONNE",8)) { + hm17_sbclr(); +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("CONNE ERROR")); +#endif + break; + } + if (!strncmp(hm17_sbuffer,"OK+CONNF",8)) { + hm17_sbclr(); +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("CONNF ERROR")); +#endif + break; + } + if (hm17_connecting==2 && !strncmp(hm17_sbuffer,"OK+CONN",7)) { + hm17_sbclr(); +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("CONN OK")); +#endif + hm17_connecting=3; + hm17_sendcmd(HM17_TEST); + hm17_connecting=0; + break; + } + break; + + case HM17_DISI: + case HM17_DISC: + if (!strncmp(hm17_sbuffer,"OK+DISCS",8)) { + hm17_sbclr(); + hm17_result=1; +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("DISCS OK")); +#endif + break; + } + if (!strncmp(hm17_sbuffer,"OK+DISIS",8)) { + hm17_sbclr(); + hm17_result=1; +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("DISIS OK")); +#endif + break; + } + if (!strncmp(hm17_sbuffer,"OK+DISCE",8)) { + hm17_sbclr(); + hm17_result=HM17_SUCESS; +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("DISCE OK")); +#endif + hm17_scanning=0; + break; + } + if (!strncmp(hm17_sbuffer,"OK+NAME:",8)) { + if (hm17_sbuffer[hm17_sindex-1]=='\n') { + hm17_result=HM17_SUCESS; +#ifdef IBEACON_DEBUG + if (hm17_debug) { + AddLog_P(LOG_LEVEL_INFO, PSTR("NAME OK")); + AddLog_P(LOG_LEVEL_INFO, PSTR(">>%s"),&hm17_sbuffer[8]); + } +#endif + hm17_sbclr(); + } + break; + } + if (!strncmp(hm17_sbuffer,"OK+DIS0:",8)) { + if (hm17_cmd==HM17_DISI) { +#ifdef HM17_V110 + goto hm17_v110; +#endif + } else { + if (hm17_sindex==20) { + hm17_result=HM17_SUCESS; +#ifdef IBEACON_DEBUG + if (hm17_debug) { + AddLog_P(LOG_LEVEL_INFO, PSTR("DIS0 OK")); + AddLog_P(LOG_LEVEL_INFO, PSTR(">>%s"),&hm17_sbuffer[8]); + } +#endif + hm17_sbclr(); + } + } + break; + } + if (!strncmp(hm17_sbuffer,"OK+DISC:",8)) { +hm17_v110: + if (hm17_cmd==HM17_DISI) { + if (hm17_sindex==78) { +#ifdef IBEACON_DEBUG + if (hm17_debug) { + AddLog_P(LOG_LEVEL_INFO, PSTR("DISC: OK")); + //OK+DISC:4C 000C0E:003 A9144081A8 3B16849611 862EC1005: 0B1CE7485D :4DB4E940F C0E:-078 + AddLog_P(LOG_LEVEL_INFO, PSTR(">>%s"),&hm17_sbuffer[8]); + } +#endif + memcpy(ib.FACID,&hm17_sbuffer[8],8); + memcpy(ib.UID,&hm17_sbuffer[8+8+1],32); + memcpy(ib.MAJOR,&hm17_sbuffer[8+8+1+32+1],4); + memcpy(ib.MINOR,&hm17_sbuffer[8+8+1+32+1+4],4); + memcpy(ib.PWR,&hm17_sbuffer[8+8+1+32+1+4+4],2); + memcpy(ib.MAC,&hm17_sbuffer[8+8+1+32+1+4+4+2+1],12); + memcpy(ib.RSSI,&hm17_sbuffer[8+8+1+32+1+4+4+2+1+12+1],4); + + if (ibeacon_add(&ib)) { + ibeacon_mqtt(ib.MAC,ib.RSSI,ib.UID,ib.MAJOR,ib.MINOR); + } + hm17_sbclr(); + hm17_result=1; + } + } else { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR(">->%s"),&hm17_sbuffer[8]); +#endif + } + break; + } + } +} + +#endif + +void IBEACON_loop() { + +#ifdef USE_IBEACON_ESP32 + //TasAutoMutex localmutex(&beaconmutex, "iBeacLoop"); + for (uint32_t cnt=0;cntavailable()) { + hm17_lastms=millis(); + // shift in + if (hm17_sindexread(); + hm17_sindex++; + hm17_decode(); + } else { + hm17_sindex=0; + break; + } + } + + if (hm17_cmd==99) { + if (hm17_sindex>=HM17_BSIZ-2 || (hm17_sindex && (difftime>100))) { + AddLog_P(LOG_LEVEL_INFO, PSTR("%s"),hm17_sbuffer); + hm17_sbclr(); + } + } + +#endif + +} + +#ifdef USE_WEBSERVER +const char HTTP_IBEACON_HL[] PROGMEM = "{s}
{m}
{e}"; +const char HTTP_IBEACON_mac[] PROGMEM = + "{s}IBEACON-MAC : %s" " {m} RSSI : %s" "{e}"; +const char HTTP_IBEACON_uid[] PROGMEM = + "{s}IBEACON-UID : %s" " {m} RSSI : %s" "{e}"; +#ifdef USE_IBEACON_ESP32 +const char HTTP_IBEACON_name[] PROGMEM = + "{s}IBEACON-NAME : %s (%s)" " {m} RSSI : %s" "{e}"; +#endif +void IBEACON_Show(void) { + char mac[14]; + char rssi[6]; + char uid[34]; +#ifdef USE_IBEACON_ESP32 + char name[18]; + //TasAutoMutex localmutex(&beaconmutex, "iBeacShow"); +#endif + int total = 0; + + for (uint32_t cnt=0;cnt 0) { + char *cp=XdrvMailbox.data; + if (*cp=='u') { + cp++; + if (*cp) IB_UPDATE_TIME=atoi(cp); + Response_P(S_JSON_IBEACON, XSNS_52,"uintv",IB_UPDATE_TIME); + } else if (*cp=='t') { + cp++; + if (*cp) IB_TIMEOUT_TIME=atoi(cp); + Response_P(S_JSON_IBEACON, XSNS_52,"lintv",IB_TIMEOUT_TIME); + } else if (*cp=='c') { + for (uint32_t cnt=0;cnt='0' && *cp<='8') { + hm17_sendcmd(*cp&7); + Response_P(S_JSON_IBEACON, XSNS_52,"hm17cmd",*cp&7); + } else if (*cp=='s') { + cp++; + len--; + while (*cp==' ') { + len--; + cp++; + } + IBEACON_Serial->write((uint8_t*)cp,len); + hm17_cmd=99; + Response_P(S_JSON_IBEACON1, XSNS_52,"hm17cmd",cp); + } +#endif +#ifdef IBEACON_DEBUG + else if (*cp=='d') { + cp++; + hm17_debug=atoi(cp); + Response_P(S_JSON_IBEACON, XSNS_52,"debug",hm17_debug); + } +#endif + } else { + serviced=false; + } + return serviced; +} + +#define D_CMND_IBEACON "IBEACON" + +#ifndef USE_IBEACON_ESP32 +//"IBEACON_FFFF3D1B1E9D_RSSI", Data "99" causes TAG to beep +bool ibeacon_cmd(void) { + ib_mac[0]=0; + int16_t rssi=0; + const char S_JSON_IBEACON[] = "{\"" D_CMND_IBEACON "_%s_RSSI\":%d}"; + uint8_t cmd_len = strlen(D_CMND_IBEACON); + if (!strncasecmp_P(XdrvMailbox.topic, PSTR(D_CMND_IBEACON), cmd_len)) { + // IBEACON prefix + rssi = XdrvMailbox.payload; + if (rssi==99) { + memcpy(ib_mac,XdrvMailbox.topic+cmd_len+1,12); + ib_mac[12]=0; + if (hm17_scanning) { + // postpone sendbeep + hm17_flag|=2; + } else { + ib_sendbeep(); + } + } + Response_P(S_JSON_IBEACON,ib_mac,rssi); + return true; + } + return false; +} + +void ib_sendbeep(void) { + hm17_flag=0; + hm17_sendcmd(HM17_CON); +} + +#endif + +#ifdef USE_IBEACON_ESP32 +void ibeacon_mqtt(const char *mac,const char *rssi,const char *uid,const char *major,const char *minor, const char *name) { +#else +void ibeacon_mqtt(const char *mac,const char *rssi,const char *uid,const char *major,const char *minor) { +#endif + char s_mac[14]; + char s_uid[34]; + char s_major[6]; + char s_minor[6]; + char s_rssi[6]; +#ifdef USE_IBEACON_ESP32 + char *s_state; +#endif + char s_name[18]; + memcpy(s_mac,mac,12); + s_mac[12]=0; + memcpy(s_uid,uid,32); + s_uid[32]=0; + memcpy(s_major,major,4); + s_major[4]=0; + memcpy(s_minor,minor,4); + s_minor[4]=0; + memcpy(s_rssi,rssi,4); + s_rssi[4]=0; + int16_t n_rssi=atoi(s_rssi); +#ifdef USE_IBEACON_ESP32 + if (n_rssi) { + s_state=(char *)"ON"; + } else { + s_state=(char *)"OFF"; + } +#endif + // if uid == all zeros, take mac + if (!strncmp_P(s_uid,PSTR("00000000000000000000000000000000"),32)) { +#ifdef USE_IBEACON_ESP32 + if (name[0]) { + memcpy(s_name,name,16); + s_name[16]=0; + ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"MAC\":\"%s\",\"NAME\":\"%s\",\"RSSI\":%d,\"STATE\":\"%s\"}}"),s_mac,s_name,n_rssi,s_state); + } else { + ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"MAC\":\"%s\",\"RSSI\":%d,\"STATE\":\"%s\"}}"),s_mac,n_rssi,s_state); + } +#else + ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"MAC\":\"%s\",\"UID\":\"%s\",\"MAJOR\":\"%s\",\"MINOR\":\"%s\",\"RSSI\":%d}}"),s_mac,s_uid,s_major,s_minor,n_rssi); +#endif + } else { +#ifdef USE_IBEACON_ESP32 + ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"UID\":\"%s\",\"MAJOR\":\"%s\",\"MINOR\":\"%s\",\"MAC\":\"%s\",\"RSSI\":%d,\"STATE\":\"%s\"}}"),s_uid,s_major,s_minor,s_mac,n_rssi,s_state); +#else + ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"UID\":\"%s\",\"MAJOR\":\"%s\",\"MINOR\":\"%s\",\"MAC\":\"%s\",\"RSSI\":%d}}"),s_uid,s_major,s_minor,s_mac,n_rssi); +#endif + } + + MqttPublishTeleSensor(); +} + + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xsns52(byte function) +{ + bool result = false; + + switch (function) { + case FUNC_INIT: + IBEACON_Init(); + break; +#ifdef USE_IBEACON_ESP32 + case FUNC_EVERY_250_MSECOND: + if (!ESP32BLE.mode.init) { + ESP32Init(); + } + break; +#endif + case FUNC_LOOP: + IBEACON_loop(); + break; + case FUNC_EVERY_SECOND: +#ifdef USE_IBEACON_ESP32 + esp32_every_second(); +#else + hm17_every_second(); +#endif + break; + case FUNC_COMMAND_SENSOR: + if (XSNS_52 == XdrvMailbox.index) { + result = xsns52_cmd(); + } + break; +#ifndef USE_IBEACON_ESP32 + case FUNC_COMMAND: + result=ibeacon_cmd(); + break; +#endif +#ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: +#ifndef USE_IBEACON_ESP32 + if (hm17_found) IBEACON_Show(); +#else + IBEACON_Show(); +#endif + break; +#endif // USE_WEBSERVER + } + return result; +} + +#endif // USE_IBEACON + +#endif \ No newline at end of file diff --git a/tasmota/xsns_62_MI_ESP32.ino b/tasmota/xsns_62_MI_ESP32.ino index 689b5a72a..af24ec083 100644 --- a/tasmota/xsns_62_MI_ESP32.ino +++ b/tasmota/xsns_62_MI_ESP32.ino @@ -45,6 +45,7 @@ forked - from arendst/tasmota - https://github.com/arendst/Tasmota */ +#ifndef USE_BLE_ESP32 #ifdef ESP32 // ESP32 only. Use define USE_HM10 for ESP8266 support #ifdef USE_MI_ESP32 @@ -589,17 +590,17 @@ int MI32_decryptPacket(char *_buf, uint16_t _bufSize, uint32_t _type){ MI32_ReverseMAC(packet->MAC); uint8_t _bindkey[16] = {0x0}; bool foundNoKey = true; - AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Search key for MAC: %02x %02x %02x %02x %02x %02x"), packet->MAC[0], packet->MAC[1], packet->MAC[2], packet->MAC[3], packet->MAC[4], packet->MAC[5]); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: search key for MAC: %02x %02x %02x %02x %02x %02x"), packet->MAC[0], packet->MAC[1], packet->MAC[2], packet->MAC[3], packet->MAC[4], packet->MAC[5]); for(uint32_t i=0; iMAC,MIBLEbindKeys[i].MAC,sizeof(packet->MAC))==0){ memcpy(_bindkey,MIBLEbindKeys[i].key,sizeof(_bindkey)); - AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Decryption Key found")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: decryption Key found")); foundNoKey = false; break; } } if(foundNoKey){ - AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: No Key found !!")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: no Key found !!")); return -2; } @@ -619,7 +620,7 @@ int MI32_decryptPacket(char *_buf, uint16_t _bufSize, uint32_t _type){ ret = br_ccm_check_tag(&ctx, &tag); - AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Err:%i, Decrypted : %02x %02x %02x %02x %02x "), ret, packet->payload[1],packet->payload[2],packet->payload[3],packet->payload[4],packet->payload[5]); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: Err:%i, Decrypted : %02x %02x %02x %02x %02x "), ret, packet->payload[1],packet->payload[2],packet->payload[3],packet->payload[4],packet->payload[5]); return ret-1; } #endif // USE_MI_DECRYPTION @@ -660,7 +661,7 @@ uint32_t MIBLEgetSensorSlot(uint8_t (&_MAC)[6], uint16_t _type, uint8_t counter) bool _success = false; for (uint32_t i=0;i19) { - AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Scan buffer full")); + AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: Scan buffer full")); MI32.state.beaconScanCounter = 1; return; } for(auto _scanResult : MIBLEscanResult){ if(memcmp(addr,_scanResult.MAC,6)==0){ - // AddLog_P(LOG_LEVEL_INFO,PSTR("M32: known device")); + // AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: known device")); return; } } @@ -1582,12 +1586,12 @@ void MI32addBeacon(uint8_t index, char* data){ _new.time = 0; if(memcmp(_empty,_new.MAC,6) == 0){ _new.active = false; - AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Beacon%u deactivated"), index); + AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: beacon%u deactivated"), index); } else{ _new.active = true; MI32.mode.activeBeacon = 1; - AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Beacon added with MAC: %s"), _MAC); + AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: beacon added with MAC: %s"), _MAC); } } @@ -1842,7 +1846,7 @@ void CmndMi32Time(void) { if (XdrvMailbox.data_len > 0) { if (MIBLEsensors.size() > XdrvMailbox.payload) { if ((LYWSD02 == MIBLEsensors[XdrvMailbox.payload].type) || (MHOC303 == MIBLEsensors[XdrvMailbox.payload].type)) { - AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: Will set Time")); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("MI32: will set Time")); MI32.state.sensor = XdrvMailbox.payload; MI32.mode.canScan = 0; MI32.mode.canConnect = 0; @@ -1872,7 +1876,7 @@ void CmndMi32Unit(void) { if (XdrvMailbox.data_len > 0) { if (MIBLEsensors.size() > XdrvMailbox.payload) { if ((LYWSD02 == MIBLEsensors[XdrvMailbox.payload].type) || (MHOC303 == MIBLEsensors[XdrvMailbox.payload].type)) { - AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Will set Unit")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: will set Unit")); MI32.state.sensor = XdrvMailbox.payload; MI32.mode.canScan = 0; MI32.mode.canConnect = 0; @@ -1922,11 +1926,11 @@ void CmndMi32Block(void){ switch (XdrvMailbox.index) { case 0: MIBLEBlockList.clear(); - // AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Size of ilist: %u"), MIBLEBlockList.size()); - ResponseCmndIdxChar(PSTR("Block list cleared")); + // AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: size of ilist: %u"), MIBLEBlockList.size()); + ResponseCmndIdxChar(PSTR("block list cleared")); break; case 1: - ResponseCmndIdxChar(PSTR("Show block list")); + ResponseCmndIdxChar(PSTR("show block list")); break; } } @@ -1952,7 +1956,7 @@ void CmndMi32Block(void){ ResponseCmndIdxChar(XdrvMailbox.data); MI32removeMIBLEsensor(_MACasBytes.buf); } - // AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Size of ilist: %u"), MIBLEBlockList.size()); + // AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: size of ilist: %u"), MIBLEBlockList.size()); break; } } @@ -2319,3 +2323,4 @@ bool Xsns62(uint8_t function) } #endif // USE_MI_ESP32 #endif // ESP32 +#endif \ No newline at end of file diff --git a/tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino b/tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino new file mode 100644 index 000000000..832aae2e7 --- /dev/null +++ b/tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino @@ -0,0 +1,2742 @@ +/* + 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.1.9 20201226 changed - All change now. + ------- + 0.9.1.7 20201116 changed - small bugfixes, add BLOCK and OPTION command, send BLE scan via MQTT + ------- + 0.9.1.6 20201022 changed - Beacon support, RSSI at TELEPERIOD, refactoring + ------- + 0.9.1.5 20201021 changed - HASS related ('null', hold back discovery), number of found sensors for RULES + ------- + 0.9.1.4 20201020 changed - use BearSSL for decryption, revert to old TELEPERIOD-cycle as default + ------- + 0.9.1.3 20200926 changed - Improve HA discovery, make key+MAC case insensitive + ------- + 0.9.1.3 20200916 changed - add ATC (custom FW for LYWSD03MMC), API adaption for NimBLE-Arduino 1.0.2 + ------- + 0.9.1.2 20200802 changed - add MHO-C303 + ------- + 0.9.1.1 20200715 changed - add MHO-C401, refactoring + ------- + 0.9.1.0 20200712 changed - add lights and yeerc, add pure passive mode with decryption, + lots of refactoring + ------- + 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 + +*/ +//#define VSCODE_DEV + +/* +#ifdef VSCODE_DEV +#define ESP32 +#define USE_BLE_ESP32 +#define USE_MI_ESP32 +#endif +*/ +//#undef USE_MI_ESP32 + +// for testing of BLE_ESP32, we remove xsns_62_MI_ESP32.ino completely, and instead add this modified xsns_52_ibeacon_BLE_ESP32.ino +#ifdef USE_BLE_ESP32 + +#ifdef ESP32 // ESP32 only. Use define USE_HM10 for ESP8266 support + +#ifdef USE_MI_ESP32 + +#define XSNS_62 62 +#define USE_MI_DECRYPTION + +#include +#ifdef USE_MI_DECRYPTION +#include +#endif //USE_MI_DECRYPTION + +void MI32scanEndedCB(NimBLEScanResults results); +void MI32notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify); + +struct { + uint16_t perPage = 4; + uint8_t mqttCurrentSlot = 0; + uint32_t period; // set manually in addition to TELE-period, is set to TELE-period after start + int secondsCounter = 0; // counts up in MI32EverySecond to period + int secondsCounter2 = 0; // counts up in MI32EverySecond to period + union { + struct { + uint32_t init:1; + uint32_t shallClearResults:1; // BLE scan results + uint32_t shallShowStatusInfo:1; // react to amount of found sensors via RULES + uint32_t firstAutodiscoveryDone:1; + uint32_t shallTriggerTele:1; + uint32_t triggeredTele:1; + }; + uint32_t all = 0; + } mode; + + struct { + // the slot currently having it's battery read + // set to 0 to start a battery read cycle + uint8_t slot = 255; + uint8_t active = 0; + } batteryreader; + + struct { + // the slot currently having it's battery read + // set to 0 to start a battery read cycle + uint8_t slot = 255; + uint8_t active = 0; + } sensorreader; + + struct { + uint32_t allwaysAggregate:1; // always show all known values of one sensor in brdigemode + uint32_t noSummary:1; // no sensor values at TELE-period + uint32_t directBridgeMode:1; // send every received BLE-packet as a MQTT-message in real-time + uint32_t holdBackFirstAutodiscovery:1; // allows to trigger it later + uint32_t showRSSI:1; + uint32_t ignoreBogusBattery:1; + uint32_t minimalSummary:1; // DEPRECATED!! + } option; +} MI32; + +#pragma pack(1) // byte-aligned structures to read the sensor data + + struct { + int16_t temp; + uint8_t hum; + uint16_t volt; // LYWSD03 only + } LYWSD0x_HT; + struct { + uint8_t spare; + int16_t temp; + uint16_t hum; + } CGD1_HT; + struct { + int16_t temp; + uint8_t spare; + uint32_t lux; + uint8_t moist; + uint16_t fert; + } Flora_TLMF; // temperature, lux, moisture, fertility + + +//////////////////////////////////////////////////////////// +// from https://github.com/Magalex2x14/LYWSD03MMC-info +struct mi_beacon_frame_data_t{ + // data from byte 0 - e.g. 30 + uint8_t meshflag; //Byte 0: x....... + uint8_t dataflag; //Byte 0: .x...... + uint8_t compatibilityflag; //Byte 0: ..x..... - indicates compatibility data present + uint8_t MACFlag; //Byte 0: ...x.... + uint8_t isencrypted; //Byte 0: ....x... + uint8_t reserved; //Byte 0: .....xxx + + // data from byte 1 - e.g. 58 + uint8_t version; //Byte 0: xxxx.... + uint8_t authMode; //Byte 0: ....xx.. // e.g. 2 + uint8_t bindingvalidreq; //Byte 0: ......x. + uint8_t registeredflag; //Byte 0: .......x +}; + +struct mi_beacon_compatibility_data_t{ // e.g. 28/08 + uint8_t reserved; //Byte 0: xx...... + uint8_t IOcap; //Byte 0: ..x..... + uint8_t bondability; //Byte 0: ...xx... + uint8_t unused; //Byte 0: .....xxx + uint16_t IOCapability; // bytes 1-2, e.g. 01 00 -> 0001 +}; +struct mi_beacon_mac_data_t{ // e.g. 28/08 + uint8_t mac[6]; +}; +struct mi_beacon_payload_data_t{ // + uint8_t type; + uint8_t ten; + uint8_t size; + uint8_t data[16]; +}; + +struct mi_beacon_data_t { // + mi_beacon_frame_data_t framedata; + uint16_t devicetype; + uint8_t framecnt; + mi_beacon_mac_data_t macdata; + mi_beacon_compatibility_data_t compatibility; + uint8_t payloadpresent; + uint8_t needkey; // we need a (new) encryption key? + + mi_beacon_payload_data_t payload; +}; + +struct mi_beacon_data_payload_data_t { // + union { + struct{ //01 + uint16_t num; + uint8_t longPress; + } Btn; + + int16_t temp; //04 + uint16_t hum; //06 + uint32_t lux; //07 + uint8_t moist; //08 + uint16_t fert; //09 + uint8_t bat; //0a + struct{ //0d + int16_t temp; + uint16_t hum; + } HT; + uint32_t NMT; //17 + }; +}; + + + +/////////////////////////////////////////////////////////// + + + +union mi_bindKey_t{ + struct{ + uint8_t key[16]; + uint8_t MAC[6]; + }; + uint8_t buf[22]; +}; + +struct ATCPacket_t{ + //uint8_t size; // = 16? + //uint8_t uid; // = 0x16, 16-bit UUID + //uint16_t UUID; // = 0x181A, GATT Service 0x181A Environmental Sensing + uint8_t MAC[6]; // [0] - hi, .. [6] - lo digits + uint16_t temp; //sadly this is in wrong endianess + uint8_t hum; + uint8_t batPer; + uint16_t batMV; + uint8_t frameCnt; +}; + +// GATT Service 0x181A Environmental Sensing +// All data little-endian +struct PVVXPacket_t { + //uint8_t size; // = 19 + //uint8_t uid; // = 0x16, 16-bit UUID + //uint16_t UUID; // = 0x181A, GATT Service 0x181A Environmental Sensing + uint8_t MAC[6]; // [0] - lo, .. [6] - hi digits + int16_t temperature; // x 0.1 degree + uint16_t humidity; // x 0.01 % + uint16_t battery_mv; // mV + uint8_t battery_level; // 0..100 % + uint8_t counter; // measurement count + uint8_t flags; +}; + +#pragma pack(0) + +struct mi_sensor_t{ + uint8_t type; //MI_Flora = 1; MI_MI-HT_V1=2; MI_LYWSD02=3; MI_LYWSD03=4; MI_CGG1=5; MI_CGD1=6 + uint8_t needkey; // tells http to display needkey message with link + uint8_t lastCnt; //device generated counter of the packet + uint8_t shallSendMQTT; + uint8_t MAC[6]; + union { + struct { + uint32_t temp:1; + uint32_t hum:1; + uint32_t tempHum:1; //every hum sensor has temp too, easier to use Tasmota dew point functions + uint32_t lux:1; + uint32_t moist:1; + uint32_t fert:1; + uint32_t bat:1; + uint32_t NMT:1; + uint32_t PIR:1; + uint32_t Btn:1; + }; + uint32_t raw; + } feature; + union { + struct { + uint32_t temp:1; + uint32_t hum:1; + uint32_t tempHum:1; //can be combined from the sensor + uint32_t lux:1; + uint32_t moist:1; + uint32_t fert:1; + uint32_t bat:1; + uint32_t NMT:1; + uint32_t motion:1; + uint32_t noMotion:1; + uint32_t Btn:1; + uint32_t PairBtn:1; + }; + uint32_t raw; + } eventType; + + int RSSI; + uint8_t pairing; + uint32_t lastTime; + uint32_t lux; + float temp; //Flora, MJ_HT_V1, LYWSD0x, CGx + union { + struct { + uint8_t moisture; + uint16_t fertility; + char firmware[6]; // actually only for FLORA but hopefully we can add for more devices + }; // Flora + struct { + float hum; + }; // MJ_HT_V1, LYWSD0x + struct { + uint16_t events; //"alarms" since boot + uint32_t NMT; // no motion time in seconds for the MJYD2S + }; + uint16_t Btn; + }; + union { + uint8_t bat; // many values seem to be hard-coded garbage (LYWSD0x, GCD1) + }; +}; + +struct MAC_t { + uint8_t buf[6]; +}; + +std::vector MIBLEsensors; +std::vector MIBLEbindKeys; +std::vector MIBLEBlockList; + +void *slotmutex = nullptr; + +/*********************************************************************************************\ + * constants +\*********************************************************************************************/ + +#define D_CMND_MI32 "MI32" + +const char kMI32_Commands[] PROGMEM = D_CMND_MI32 "|" +#ifdef USE_MI_DECRYPTION + "Key|" + "Keys|" +#endif // USE_MI_DECRYPTION + "Period|Time|Page|Battery|Unit|Block|Option"; + +void (*const MI32_Commands[])(void) PROGMEM = { +#ifdef USE_MI_DECRYPTION + &CmndMi32Key, + &CmndMi32Keys, +#endif // USE_MI_DECRYPTION + &CmndMi32Period, &CmndMi32Time, &CmndMi32Page, &CmndMi32Battery, &CmndMi32Unit, &CmndMi32Block, &CmndMi32Option }; + + +#define MI_UNKOWN 1 +#define MI_FLORA 2 +#define MI_MJ_HT_V1 3 +#define MI_LYWSD02 4 +#define MI_LYWSD03MMC 5 +#define MI_CGG1 6 +#define MI_CGD1 7 +#define MI_NLIGHT 8 +#define MI_MJYD2S 9 +#define MI_YEERC 10 +#define MI_MHOC401 11 +#define MI_MHOC303 12 +#define MI_ATC 13 + +#define MI_MI32_TYPES 13 //count this manually + +const uint16_t kMI32DeviceID[MI_MI32_TYPES]={ + 0x0000, // Unkown + 0x0098, // Flora + 0x01aa, // MJ_HT_V1 + 0x045b, // LYWSD02 + 0x055b, // LYWSD03 + 0x0347, // CGG1 + 0x0576, // CGD1 + 0x03dd, // NLIGHT + 0x07f6, // MJYD2S + 0x0153, // yee-rc + 0x0387, // MHO-C401 + 0x06d3, // MHO-C303 + 0x0a1c // ATC -> this is a fake ID +}; + +const char kMI32DeviceType0[] PROGMEM = "Unknown"; +const char kMI32DeviceType1[] PROGMEM = "Flora"; +const char kMI32DeviceType2[] PROGMEM = "MJ_HT_V1"; +const char kMI32DeviceType3[] PROGMEM = "LYWSD02"; +const char kMI32DeviceType4[] PROGMEM = "LYWSD03"; +const char kMI32DeviceType5[] PROGMEM = "CGG1"; +const char kMI32DeviceType6[] PROGMEM = "CGD1"; +const char kMI32DeviceType7[] PROGMEM = "NLIGHT"; +const char kMI32DeviceType8[] PROGMEM = "MJYD2S"; +const char kMI32DeviceType9[] PROGMEM = "YEERC"; +const char kMI32DeviceType10[] PROGMEM ="MHOC401"; +const char kMI32DeviceType11[] PROGMEM ="MHOC303"; +const char kMI32DeviceType12[] PROGMEM ="ATC"; +const char * kMI32DeviceType[] PROGMEM = {kMI32DeviceType0,kMI32DeviceType1,kMI32DeviceType2,kMI32DeviceType3,kMI32DeviceType4,kMI32DeviceType5,kMI32DeviceType6,kMI32DeviceType7,kMI32DeviceType8,kMI32DeviceType9,kMI32DeviceType10,kMI32DeviceType11,kMI32DeviceType12}; + +typedef int BATREAD_FUNCTION(int slot); +typedef int UNITWRITE_FUNCTION(int slot, int unit); +typedef int TIMEWRITE_FUNCTION(int slot); + +int genericOpCompleteFn(BLE_ESP32::generic_sensor_t *pStruct); +int genericBatReadFn(int slot); +int genericUnitWriteFn(int slot, int unit); +int genericTimeWriteFn(int slot); +int MI32scanCompleteCallback(NimBLEScanResults results); + +const char LYWSD02_Svc[] PROGMEM = "EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6"; +const char LYWSD02_BattChar[] PROGMEM = "EBE0CCC4-7A0A-4B0C-8A1A-6FF2997DA3A6"; +const char LYWSD02_UnitChar[] PROGMEM = "EBE0CCBE-7A0A-4B0C-8A1A-6FF2997DA3A6"; +const char LYWSD02_TimeChar[] PROGMEM = "EBE0CCB7-7A0A-4B0C-8A1A-6FF2997DA3A6"; +const char LYWSD02_BattNotifyChar[] PROGMEM = "EBE0CCC1-7A0A-4B0C-8A1A-6FF2997DA3A6"; + +const char *LYWSD03_Svc = LYWSD02_Svc; +const char *LYWSD03_BattNotifyChar = LYWSD02_BattNotifyChar; + +const char *MHOC303_Svc = LYWSD02_Svc; +const char *MHOC303_UnitChar = LYWSD02_UnitChar; +const char *MHOC303_TimeChar = LYWSD02_TimeChar; + +const char *MHOC401_Svc = LYWSD02_Svc; +const char *MHOC401_BattNotifyChar = LYWSD02_BattNotifyChar; + +const char CGD1_Svc[] PROGMEM = "180F"; +const char CGD1_BattChar[] PROGMEM = "2A19"; + +const char FLORA_Svc[] PROGMEM = "00001204-0000-1000-8000-00805F9B34FB"; +const char FLORA_BattChar[] PROGMEM = "00001A02-0000-1000-8000-00805F9B34FB"; + + + +/*********************************************************************************************\ + * enumerations +\*********************************************************************************************/ + +// types of operation performed, included in context +enum MI32_MI_OP_TYPES { + OP_TIME_WRITE = 0, + OP_BATT_READ = 1, + OP_UNIT_WRITE = 2, + OP_UNIT_READ = 3, + OP_UNIT_TOGGLE = 4, + OP_READ_HT_LY = 5, +}; + + +enum MI32_MI_KEY_REQ { + KEY_REQUIREMENT_UNKNOWN = 0, // we don't know if a key is needed + KEY_NOT_REQUIRED = 1, // we got an unencrypted payload + KEY_REQUIRED_BUT_NOT_FOUND = 2, // we got an encrypted packet, but had not key + KEY_REQUIRED_AND_FOUND = 3, // we got an encrypted packet, and could decrypt + KEY_REQUIRED_AND_INVALID = 4, // we got an encrypted packet, and could not decrypt +}; + +/*********************************************************************************************\ + * Classes +\*********************************************************************************************/ + + +// fn type READ_CALLBACK +// NOTE!!!: this callback is called DIRECTLY from the operation task, so be careful about cross-thread access of data +// if is called after read, so that you can do a read/modify/write operation on a characteristic. +int toggleUnit(BLE_ESP32::generic_sensor_t *op){ + uint32_t context = (uint32_t) op->context; + int opType = context >> 24; + // we only need to op type + int devType = (context >> 16) & 0xff; + int slot = (context) & 0xff; + switch (opType){ + case OP_UNIT_TOGGLE:{ + uint8_t curUnit = 0; + if( op->dataRead[0] != 0 && op->dataRead[0] < 101 ){ + curUnit = op->dataRead[0]; + } + + curUnit = curUnit == 0x01?0xFF:0x01; // C/F + // copy in ALL of the data, because we don't know how long this is from the existing src code. + memcpy(op->dataToWrite, op->dataRead, op->readlen); + op->writelen = op->readlen; + op->dataToWrite[0] = curUnit; + } break; + case OP_UNIT_WRITE:{ + uint8_t curUnit = op->dataToWrite[0]; + // copy in ALL of the data, because we don't know how long this is from the existing src code. + memcpy(op->dataToWrite, op->dataRead, op->readlen); + op->writelen = op->readlen; + op->dataToWrite[0] = curUnit; + } break; + } + return 0; +} + +bool MI32Operation(int slot, int optype, const char *svc, const char *charactistic, const char *notifychar = nullptr, const uint8_t *data = nullptr, int datalen = 0, uint8_t *addr = nullptr ) { + if (!svc || !svc[0]){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("MI32Op: inv svc")); + return 0; + } + + BLE_ESP32::generic_sensor_t *op = nullptr; + + // ALWAYS use this function to create a new one. + int res = BLE_ESP32::newOperation(&op); + if (!res){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("Can't get a newOperation from BLE")); + return 0; + } else { + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got a newOperation from BLE")); + } + + if (slot >= 0){ + op->addr = NimBLEAddress(MIBLEsensors[slot].MAC); + } else { + if (!addr){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("no addr")); + BLE_ESP32::freeOperation(&op); + return 0; + } + op->addr = NimBLEAddress(addr); + } + + bool havechar = false; + op->serviceUUID = NimBLEUUID(svc); + + if (!op->serviceUUID.bitSize()){ + BLE_ESP32::freeOperation(&op); + AddLog_P(LOG_LEVEL_ERROR,PSTR("MI: Bad service string %s"), svc); + return 0; + } + + + if (charactistic && charactistic[0]){ + havechar = true; + op->characteristicUUID = NimBLEUUID(charactistic); + if (!op->characteristicUUID.bitSize()){ + BLE_ESP32::freeOperation(&op); + AddLog_P(LOG_LEVEL_ERROR,PSTR("MI: Bad characteristic string %s"), charactistic); + return 0; + } + } + if (notifychar && notifychar[0]){ + op->notificationCharacteristicUUID = NimBLEUUID(notifychar); + if (!op->notificationCharacteristicUUID.bitSize()){ + BLE_ESP32::freeOperation(&op); + AddLog_P(LOG_LEVEL_ERROR,PSTR("MI: Bad notifycharacteristic string %s"), notifychar); + return 0; + } + } + + if (data && datalen) { + op->writelen = datalen; + memcpy(op->dataToWrite, data, datalen); + } else { + if (!datalen && havechar){ + op->readlen = 1; // if we don't set readlen, then it won't read + } + } + + // the only times we intercept between read abnd write + if ((optype == OP_UNIT_WRITE) || (optype == OP_UNIT_TOGGLE)){ + op->readlen = 1; // if we don't set readlen, then it won't read + op->readmodifywritecallback = (void *)toggleUnit; + } + + // this op will call us back on complete or failure. + op->completecallback = (void *)genericOpCompleteFn; + uint32_t context = (optype << 24) | (MIBLEsensors[slot].type << 16) | slot; + op->context = (void *)context; + + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI s:%d op:%s"), slot, BLE_ESP32::BLETriggerResponse(op).c_str()); + + res = BLE_ESP32::extQueueOperation(&op); + if (!res){ + // if it fails to add to the queue, do please delete it + BLE_ESP32::freeOperation(&op); + AddLog_P(LOG_LEVEL_ERROR,PSTR("Failed to queue new operation - deleted")); + } + + return res; +} + + + +int genericBatReadFn(int slot){ + int res = 0; + + switch(MIBLEsensors[slot].type) { + // these use notify for battery read, and it comes in the temp packet + case MI_LYWSD03MMC: + res = MI32Operation(slot, OP_BATT_READ, LYWSD03_Svc, nullptr, LYWSD03_BattNotifyChar); + break; + case MI_MHOC401: + res = MI32Operation(slot, OP_BATT_READ, MHOC401_Svc, nullptr, MHOC401_BattNotifyChar); + break; + + // these read a characteristic + case MI_FLORA: + res = MI32Operation(slot, OP_BATT_READ, FLORA_Svc, FLORA_BattChar); + break; + case MI_LYWSD02: + res = MI32Operation(slot, OP_BATT_READ, LYWSD02_Svc, LYWSD02_BattChar); + break; + case MI_CGD1: + res = MI32Operation(slot, OP_BATT_READ, CGD1_Svc, CGD1_BattChar); + break; + +// this was for testing only - it does work, but no need to read as we get good bat in advert +// case MI_MJ_HT_V1: +// res = MI32Operation(slot, OP_BATT_READ, CGD1_Svc, CGD1_BattChar); +// break; + + default: + res = -10; // no need to read + break; + } + if (res > 0){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Req batt read slot %d type %d queued"), slot, MIBLEsensors[slot].type); + } else { + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Req batt read slot %d type %d non-queued res %d"), slot, MIBLEsensors[slot].type, res); + } + return res; +} + + + +int genericSensorReadFn(int slot, int force){ + int res = 0; + switch(MIBLEsensors[slot].type) { +/* seen notify timeout consistently with MI_LYWSD02, + so although the characteristic seems to exist, it does not work? + further dev required with sensor to hand. + case MI_LYWSD02: + // don't read if key present and we've decoded at least one advert + if (MIBLEsensors[slot].needkey == KEY_REQUIRED_AND_FOUND) return -2; + res = MI32Operation(slot, OP_READ_HT_LY, LYWSD02_Svc, nullptr, LYWSD02_BattNotifyChar); + break;*/ + case MI_LYWSD03MMC: + // don't read if key present and we've decoded at least one advert + if (MIBLEsensors[slot].needkey == KEY_REQUIRED_AND_FOUND && !force) return -2; + res = MI32Operation(slot, OP_READ_HT_LY, LYWSD03_Svc, nullptr, LYWSD03_BattNotifyChar); + break; + case MI_MHOC401: + // don't read if key present and we've decoded at least one advert + if (MIBLEsensors[slot].needkey == KEY_REQUIRED_AND_FOUND && !force) return -2; + res = MI32Operation(slot, OP_READ_HT_LY, MHOC401_Svc, nullptr, MHOC401_BattNotifyChar); + break; + + default: + res = -1; + break; + } + return res; +} + + +// called once per second +int readOneSensor(){ + if (MI32.sensorreader.active){ + AddLog_P(LOG_LEVEL_DEBUG,PSTR("readOneSensor - already active reading %d"), MI32.sensorreader.slot-1); + return 0; + } + + // loop if the sensor at the slot does not need to be read + // i.e. drop out of loop when we start a read, or hit the end + int res = -1; + do { + // MI32.sensorreader.slot is reset to zero to trigger a read sequence + if (MI32.sensorreader.slot >= MIBLEsensors.size()){ + //AddLog_P(LOG_LEVEL_DEBUG,PSTR("readOneSensor past end of slots - %d > %d"), MI32.sensorreader.slot, MIBLEsensors.size()); + return 0; + } + + res = genericSensorReadFn(MI32.sensorreader.slot, 0); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("genericSensorReadFn slot %d res %d"), MI32.sensorreader.slot, res); + + // if this sensor in this slot does not need to be read via notify, just move on top the next one + if (res < 0){ + MI32.sensorreader.slot++; + } else { + break; + } + } while (1); + + if (res == 0){ + // can't read at the moment (no operations available?) + AddLog_P(LOG_LEVEL_DEBUG,PSTR("readOneSensor no ops available slot %d res %d"), MI32.sensorreader.slot, res); + return 0; + } + + // setup next slot to read + MI32.sensorreader.slot++; + // and make it wait until the read/notify is complete + // this is cleared in the response callback. + MI32.sensorreader.active = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("readOneSensor reading for slot %d res %d"), MI32.sensorreader.slot-1, res); + + // started one + return 1; +} + + + +// called once per second +int readOneBat(){ + if (MI32.batteryreader.active){ + return 0; + } + + //MI32.batteryreader.slot is rest to zero to trigger a read... + if (MI32.batteryreader.slot >= MIBLEsensors.size()){ + return 0; + } + + int res = genericBatReadFn(MI32.batteryreader.slot); + + // if this sensor in this slot does not support battery read, just move on top the next one + if (res < 0){ + MI32.batteryreader.slot++; + if (MI32.batteryreader.slot >= MIBLEsensors.size()){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Batt loop complete at %d"), MI32.batteryreader.slot); + } + return 0; + } + + if (res == 0){ + // can't read at the moment (no operations available?) + return 0; + } + + // setup next slot to read + MI32.batteryreader.slot++; + // and make it wait until the read/notify is complete + // this is cleared in the response callback. + MI32.batteryreader.active = 1; + if (MI32.batteryreader.slot >= MIBLEsensors.size()){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Batt loop will complete at %d"), MI32.batteryreader.slot); + } + // started one + return 1; +} + + + +///////////////////////////////////////////////////// +// change the unit of measurement? +// call with unit == -1 to cause the unit to be toggled. +int genericUnitWriteFn(int slot, int unit){ + int res = 0; + int op = OP_UNIT_WRITE; + if (unit == -1){ + op = OP_UNIT_TOGGLE; + } + uint8_t writeData[1]; + writeData[0] = unit; + switch (MIBLEsensors[slot].type){ + case MI_LYWSD02: + res = MI32Operation(slot, op, LYWSD02_Svc, LYWSD02_UnitChar, nullptr, writeData, 1); + break; + case MI_MHOC303: // actually, EXACTLY the same as above, including the sevice and characteristic... + res = MI32Operation(slot, op, MHOC303_Svc, MHOC303_UnitChar, nullptr, writeData, 1); + break; + default: + res = -1; + break; + } + return res; +} + +///////////////////////////////////////////////////// +// read the unit of measurement. genericOpCompleteFn +int genericUnitReadFn(int slot){ + int res = 0; + switch (MIBLEsensors[slot].type){ + case MI_LYWSD02: + res = MI32Operation(slot, OP_UNIT_READ, LYWSD02_Svc, LYWSD02_UnitChar); + break; + case MI_MHOC303: // actually, EXACTLY the same as above, including the sevice and characteristic... + res = MI32Operation(slot, OP_UNIT_READ, MHOC303_Svc, MHOC303_UnitChar); + break; + default: + res = -1; + break; + } + return res; +} + + +///////////////////////////////////////////////////// +// write time to a device. genericOpCompleteFn +int genericTimeWriteFn(int slot){ + int res = 0; + switch (MIBLEsensors[slot].type){ + case MI_LYWSD02: { + union { + uint8_t buf[5]; + uint32_t time; + } _utc; + _utc.time = Rtc.utc_time; + _utc.buf[4] = Rtc.time_timezone / 60; + res = MI32Operation(slot, OP_TIME_WRITE, LYWSD02_Svc, LYWSD02_TimeChar, nullptr, _utc.buf, sizeof(_utc.buf)); + } break; + case MI_MHOC303: // actually, EXACTLY the same as above, including the sevice and characteristic... + union { + uint8_t buf[5]; + uint32_t time; + } _utc; + _utc.time = Rtc.utc_time; + _utc.buf[4] = Rtc.time_timezone / 60; + res = MI32Operation(slot, OP_TIME_WRITE, MHOC303_Svc, MHOC303_TimeChar, nullptr, _utc.buf, sizeof(_utc.buf)); + break; + default: + res = -1; + break; + } + return res; +} + + +int genericOpCompleteFn(BLE_ESP32::generic_sensor_t *op){ + uint32_t context = (uint32_t) op->context; + + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI op complete context %x"), context); + + int opType = context >> 24; + int devType = (context >> 16) & 0xff; + int slot = (context) & 0xff; + + char slotMAC[13]; + BLE_ESP32::dump(slotMAC, sizeof(slotMAC), MIBLEsensors[slot].MAC, 6) ; + uint8_t addrrev[6]; + memcpy(addrrev, MIBLEsensors[slot].MAC, 6); + //BLE_ESP32::ReverseMAC(addrrev); + NimBLEAddress addr(addrrev); + + bool fail = false; + if (op->addr != addr){ + // slot changed during operation? + AddLog_P(LOG_LEVEL_ERROR,PSTR("Slot mac changed during an operation")); + fail = true; + } + + if (op->state <= GEN_STATE_FAILED){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("operation failed %d for %s"), op->state, slotMAC); + fail = true; + } + + if (fail){ + switch(opType){ + case OP_BATT_READ:{ + // allow another... + MI32.batteryreader.active = 0; + } break; + case OP_READ_HT_LY: { + // allow another... + MI32.sensorreader.active = 0; + } break; + } + return 0; + } + + switch(opType){ + case OP_TIME_WRITE: + AddLog_P(LOG_LEVEL_DEBUG,PSTR("Time write for %s complete"), slotMAC); + return 0; // nothing to do + case OP_BATT_READ:{ + uint8_t *data = nullptr; + int len = 0; + if (op->notifylen){ + data = op->dataNotify; + len = op->notifylen; + // note: the only thingas that have battery in notify FOR THE MOMENT read it like this. + MI32notifyHT_LY(slot, (char*)op->dataNotify, op->notifylen); + } + if (op->readlen){ + data = op->dataRead; + len = op->readlen; + MIParseBatt(slot, data, len); + } + + // allow another... + MI32.batteryreader.active = 0; + AddLog_P(LOG_LEVEL_INFO,PSTR("batt read slot %d done state %x"), slot, op->state); + + } return 0; + + case OP_UNIT_WRITE: // nothing more to do? + AddLog_P(LOG_LEVEL_DEBUG,PSTR("Unit write for %s complete"), slotMAC); + return 0; + + case OP_UNIT_READ: { + uint8_t currUnit = op->dataRead[0]; + AddLog_P(LOG_LEVEL_DEBUG,PSTR("Unit read for %s complete %d"), slotMAC, currUnit); + } return 0; + + case OP_UNIT_TOGGLE: { + uint8_t currUnit = op->dataToWrite[0]; + AddLog_P(LOG_LEVEL_DEBUG,PSTR("Unit toggle for %s complete %d->%d; datasize was %d"), slotMAC, op->dataRead[0], op->dataToWrite[0], op->readlen); + } return 0; + + case OP_READ_HT_LY: { + // allow another... + MI32.sensorreader.active = 0; + MI32notifyHT_LY(slot, (char*)op->dataNotify, op->notifylen); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("HT_LY notify for %s complete"), slotMAC); + } return 0; + + default: + AddLog_P(LOG_LEVEL_ERROR,PSTR("OpType %d not recognised?"), opType); + return 0; + } + + return 0; +} + +int MI32advertismentCallback(BLE_ESP32::ble_advertisment_t *pStruct) +{ + // we will try not to use this... + BLEAdvertisedDevice *advertisedDevice = pStruct->advertisedDevice; + + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Advertised Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData(0).length()); + int RSSI = pStruct->RSSI; + const uint8_t *addr = pStruct->addr; + if(MI32isInBlockList(addr) == true) return 0; + + int svcdataCount = advertisedDevice->getServiceDataCount(); + + if (svcdataCount == 0) { + return 0; + } + + NimBLEUUID UUIDBig = advertisedDevice->getServiceDataUUID(0);//.getNative()->u16.value; + + const ble_uuid_any_t* native = UUIDBig.getNative(); + if (native->u.type != 16){ + //not interested in 128 bit; + return 0; + } + uint16_t UUID = native->u16.value; + + char temp[60]; + BLE_ESP32::dump(temp, 13, addr, 6); + + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("MI:%s svc[0] UUID (%x)"), temp, UUID); + std::string ServiceDataStr = advertisedDevice->getServiceData(0); + + uint32_t ServiceDataLength = ServiceDataStr.length(); + const uint8_t *ServiceData = (const uint8_t *)ServiceDataStr.data(); + BLE_ESP32::dump(temp, 60, ServiceData, ServiceDataLength); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("MI:%s"), temp); + + + if (UUID){ + // this will take and keep the mutex until the function is over + TasAutoMutex localmutex(&slotmutex, "Mi32AdCB2"); + switch(UUID){ + case 0xfe95: // std MI? + case 0xfdcd: // CGD1? + { + MI32ParseResponse(ServiceData, ServiceDataLength, addr, RSSI); + } break; + case 0x181a: { //ATC + MI32ParseATCPacket(ServiceData, ServiceDataLength, addr, RSSI); + } break; + + default:{ + } break; + } + } + return 0; +} + + +/*********************************************************************************************\ + * Helper functions +\*********************************************************************************************/ + +/** + * @brief Remove all colons from null terminated char array + * + * @param _string Typically representing a MAC-address like AA:BB:CC:DD:EE:FF + */ +void MI32stripColon(char* _string){ + uint32_t _length = strlen(_string); + uint32_t _index = 0; + while (_index < _length) { + char c = _string[_index]; + if(c==':'){ + memmove(_string+_index,_string+_index+1,_length-_index); + } + _index++; + } + _string[_index] = 0; +} + +/** + * @brief Convert string that repesents a hexadecimal number to a byte array + * + * @param _string input string in format: AABBCCDDEEFF or AA:BB:CC:DD:EE:FF, caseinsensitive + * @param _mac target byte array must match the correct size (i.e. AA:BB -> uint8_t bytes[2]) + */ + +void MI32HexStringToBytes(char* _string, uint8_t* _byteArray) { + MI32stripColon(_string); + UpperCase(_string,_string); + uint32_t index = 0; + uint32_t _end = strlen(_string); + memset(_byteArray,0,_end/2); + while (index < _end) { + char c = _string[index]; + uint8_t value = 0; + if(c >= '0' && c <= '9') + value = (c - '0'); + else if (c >= 'A' && c <= 'F') + value = (10 + (c - 'A')); + _byteArray[(index/2)] += value << (((index + 1) % 2) * 4); + index++; + } +} + +/** + * @brief Reverse an array of 6 bytes + * + * @param _mac a byte array of size 6 (typicalliy representing a MAC address) + */ +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)); +} + +#ifdef USE_MI_DECRYPTION +int MI32AddKey(char* payload, char* key = nullptr){ + mi_bindKey_t keyMAC; + + if (!key){ + MI32HexStringToBytes(payload,keyMAC.buf); + } else { + MI32HexStringToBytes(payload,keyMAC.MAC); + MI32HexStringToBytes(key,keyMAC.key); + } + + bool unknownKey = true; + for(uint32_t i=0; i 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: search key for MAC: %02x%02x%02x%02x%02x%02x"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + for(uint32_t i=0; i 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: decryption Key found")); + foundNoKey = false; + break; + } + } + if(foundNoKey){ + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: no Key found !!")); + return -2; // indicates needs key + } + + br_aes_small_ctrcbc_keys keyCtx; + br_aes_small_ctrcbc_init(&keyCtx, _bindkey, 16); + + br_ccm_context ctx; + br_ccm_init(&ctx, &keyCtx.vtable); + br_ccm_reset(&ctx, nonce, 12, 1, len, 4); + br_ccm_aad_inject(&ctx, authData, 1); + br_ccm_flip(&ctx); + + memcpy(payload, data, len); //we want to be sure about 4-byte alignement + br_ccm_run(&ctx, 0, payload, len); + memcpy(data, payload, len); //back to the packet + + + // crashed in here - why?, so give it more space to work with? + // returns 1 if matched, else 0 + int ret = br_ccm_check_tag(&ctx, &tag); + + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: Err:%i, Decrypted : %02x %02x %02x %02x %02x"), ret, payload[1],payload[2],payload[3],payload[4],payload[5]); + return ret-1; // -> -1=fail, 0=success +} + +#endif // USE_MI_DECRYPTION + + +// packet examples: +// MJ_HT_V1 +// 5020 AA01 41 3AF4DAA8654C 0A100109 +// 5020 AA01 43 3AF4DAA8654C 061002E901 +// 5020 AA01 48 3AF4DAA8654C 041002BF00 +// 5020 AA01 4A 3AF4DAA8654C 0D1004BF00E901 +// 7122 AA01 15 3AF4DAA8654C 0D 0200020D10 + +// LYWSD03 encrypted data: +// 5858 5B05 2F B3E30838C1A4 [69A9FBDF67] ,060000 0791C39A - 23bytes +// 23-9 = 14 +// -> nonce B3E30838C1A4|5B02|2F|060000 +// 23-6 = 17 +// -> tag 0791C39A +// datalen = 23 - 9 - 4 - 3 - 1 - 1 = 5 + +// CGD1 reconstructed from src: (svcdata on fdcd) +// xxyy FFEEDDCCBBAA MMMM TTTTHHHH|BB +// xxyy FFEEDDCCBBAA 0104 TTTTHHHH +// xxyy FFEEDDCCBBAA 0201 BB + + +int MIParsePacket(const uint8_t* slotmac, struct mi_beacon_data_t *parsed, const uint8_t *datain, int len){ + uint8_t data[32]; + memcpy(data, datain, len); + if (!parsed){ + return 0; + } + if (len < 5){ + return 0; + } + + int byteindex = 0; + + // 58 58 = 0x5858 = data|comp|mac|enc, v5|auth2 + // 30 58 = 0x5830 = comp|mac, v5|auth2 + // 30 50 = 0x5030 = comp|mac, v5|auth0 + // 48 59 = 0x5948 = data|enc, v5|auth2|registered + // 10 59 = 0x5910 = mac, v5|auth2|registered + // 71 22 = 0x2271 = data|comp|mac v2|bind + // 50 20 = 0x2050 = data|mac v2 - MJ_HT_V1 data + // 71 22 = 0x2271 = data|comp|mac|reserved1 v2|bind - MJ_HT_V1 pair + + // data from byte 0 - e.g. 30 + parsed->framedata.meshflag = (data[byteindex] & 0x80)>>7; //Byte 0: x....... + parsed->framedata.dataflag = (data[byteindex] & 0x40)>>6; //Byte 0: .x...... + parsed->framedata.compatibilityflag = (data[byteindex] & 0x20)>>5; //Byte 0: ..x..... - indicates compatibility data present + parsed->framedata.MACFlag = (data[byteindex] & 0x10)>>4; //Byte 0: ...x.... + parsed->framedata.isencrypted = (data[byteindex] & 0x08)>>3; //Byte 0: ....x... + parsed->framedata.reserved = (data[byteindex] & 0x03)>>6; //Byte 0: .....xxx + + // data from byte 1 - e.g. 58 + byteindex++; + parsed->framedata.version = (data[byteindex] & 0xf0)>>4; //Byte 0: xxxx.... + parsed->framedata.authMode = (data[byteindex] & 0x0C)>>6; //Byte 0: ....xx.. // e.g. 2 + parsed->framedata.bindingvalidreq = (data[byteindex] & 0x02)>>1; //Byte 0: ......x. + parsed->framedata.registeredflag = (data[byteindex] & 0x01); //Byte 0: .......x + + byteindex++; + + parsed->devicetype = *((uint16_t *)(data + byteindex)); + byteindex += 2; + parsed->framecnt = data[byteindex]; + //if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI frame %d"), parsed->framecnt); + byteindex++; + + + if (parsed->framedata.version <= 3){ + // e.g. MJ_HT_V1 + } + + if (parsed->framedata.MACFlag){ + if (len < byteindex + 6){ + return 0; + } + memcpy(parsed->macdata.mac, &data[byteindex], 6); + byteindex += 6; + } + + int decres = 1; + // everything after MAC is encrypted if specified? + if (parsed->framedata.isencrypted){ + if (len < byteindex + 3+4+1){ + return 0; + } + const uint8_t* mac = slotmac; + if (parsed->framedata.MACFlag){ + mac = parsed->macdata.mac; + } + uint8_t nonce[12]; + uint8_t *p = nonce; + memcpy(p, mac, 6); + p += 6; + memcpy(p, &parsed->devicetype, 2); + p += 2; + *(p++) = parsed->framecnt; + uint8_t *extCnt = data +(len-7); + memcpy(p, extCnt, 3); + p += 3; + uint32_t tag = *(uint32_t *)(data + (len-4)); + + // decrypt the data in place + decres = MIDecryptPayload(mac, nonce, tag, data + byteindex, len - byteindex - 7); + // no longer need the nonce data. + len -= 7; + } + + switch(decres){ + case 1: // decrypt not requested + break; + case 0: // suceeded + parsed->needkey = KEY_REQUIRED_AND_FOUND; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI payload decrypted")); + break; + case -1: // key failed to work + parsed->needkey = KEY_REQUIRED_AND_INVALID; + AddLog_P(LOG_LEVEL_ERROR,PSTR("MI payload decrypt failed")); + parsed->payloadpresent = 0; + return 0; + break; + case -2: // key not present + parsed->needkey = KEY_REQUIRED_BUT_NOT_FOUND; + AddLog_P(LOG_LEVEL_ERROR,PSTR("MI payload encrypted but no key")); + parsed->payloadpresent = 0; + return 0; + break; + } + + // if set, there could be 1 or 3 bytes here. + if (parsed->framedata.compatibilityflag) { + if (len < byteindex + 1){ + return 0; + } + // e.g. in pair: 7122 AA01 15 3AF4DAA8654C [0D] 0200020D10 -> bond|unused2 + parsed->compatibility.reserved = (data[byteindex] & 0xc0) >> 6; //Byte 0: xx...... + parsed->compatibility.IOcap = (data[byteindex] & 0x20) >> 5; //Byte 0: ..x..... + parsed->compatibility.bondability = (data[byteindex] & 0x18) >> 3; //Byte 0: ...xx... + parsed->compatibility.unused = (data[byteindex] & 0x07) >> 0; //Byte 0: .....xxx + byteindex ++; + + if (parsed->compatibility.IOcap) { + if (len < byteindex + 2){ + return 0; + } + parsed->compatibility.IOCapability = *((uint16_t *)(data + byteindex)); // bytes 1-2, e.g. 01 00 -> 0001 + byteindex += 2; + } + } + + // rest is payload + int rem = (len - byteindex); + if (rem > sizeof(parsed->payload)){ + rem = sizeof(parsed->payload); + return 0; + } + + if ((len - byteindex) == 0){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI no payload")); + parsed->payload.size = 0; + parsed->payloadpresent = 0; + return 0; + } + + // we have payload which did not need decrypt. + if (decres == 1){ + parsed->needkey = KEY_NOT_REQUIRED; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI payload unencrypted")); + } + + // already decrypted if required + parsed->payloadpresent = 1; + memcpy(&parsed->payload, (data + byteindex), (len - byteindex)); + if (parsed->payload.size != (len - byteindex) - 3){ + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI payload length mismatch")); + } + + return 1; +} + + + + +#ifdef USE_HOME_ASSISTANT +/** + * @brief For HASS only, changes last entry of JSON in mqtt_data to 'null' + */ + +void MI32nullifyEndOfMQTT_DATA(){ + char *p = TasmotaGlobal.mqtt_data + strlen(TasmotaGlobal.mqtt_data); + while(true){ + *p--; + if(p[0]==':'){ + p[1] = 0; + break; + } + } + ResponseAppend_P(PSTR("null")); +} +#endif // USE_HOME_ASSISTANT + +/*********************************************************************************************\ + * common functions +\*********************************************************************************************/ + + +/** + * @brief Return the slot number of a known sensor or return create new sensor slot + * + * @param _MAC 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(const uint8_t *mac, uint16_t _type, uint8_t counter){ + + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: will test ID-type: %x"),D_CMND_MI32, _type); + bool _success = false; + for (uint32_t i=0; i < MI_MI32_TYPES; i++){ // i < sizeof(kMI32DeviceID) gives compiler warning + if(_type == kMI32DeviceID[i]){ + _type = i+1; + _success = true; + break; + } + else { + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: ID-type is not: %x"),D_CMND_MI32,kMI32DeviceID[i]); + } + } + if(!_success) { + _type = 1; // unknown + } + + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: vector size %u"),D_CMND_MI32, MIBLEsensors.size()); + for(uint32_t i=0; i 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: slot: %u/%u - ign repeat"),D_CMND_MI32, i, MIBLEsensors.size()); + //return 0xff; // packet received before, stop here + } + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI frame %d, last %d"), counter, MIBLEsensors[i].lastCnt); + MIBLEsensors[i].lastCnt = counter; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: slot: %u/%u"),D_CMND_MI32, i, MIBLEsensors.size()); + + if (MIBLEsensors[i].type != _type){ + // this happens on incorrectly configured pvvx ATC firmware + AddLog_P(LOG_LEVEL_ERROR,PSTR("%s: slot: %u - device type 0x%04x(%s) -> 0x%04x(%s) - check device is only sending one type of advert."),D_CMND_MI32, i, + kMI32DeviceID[MIBLEsensors[i].type-1], kMI32DeviceType[MIBLEsensors[i].type-1], kMI32DeviceID[_type-1], kMI32DeviceType[_type-1]); + MIBLEsensors[i].type = _type; + } + + return i; + } + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: i: %x %x %x %x %x %x"),D_CMND_MI32, MIBLEsensors[i].MAC[5], MIBLEsensors[i].MAC[4],MIBLEsensors[i].MAC[3],MIBLEsensors[i].MAC[2],MIBLEsensors[i].MAC[1],MIBLEsensors[i].MAC[0]); + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: n: %x %x %x %x %x %x"),D_CMND_MI32, mac[5], mac[4], mac[3],mac[2],mac[1],mac[0]); + } + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: new sensor -> slot: %u"),D_CMND_MI32, MIBLEsensors.size()); + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: found new sensor"),D_CMND_MI32); + mi_sensor_t _newSensor; + memset(&_newSensor, 0 , sizeof(_newSensor)); + memcpy(_newSensor.MAC, mac, 6); + _newSensor.type = _type; + _newSensor.eventType.raw = 0; + _newSensor.feature.raw = 0; + _newSensor.temp = NAN; + _newSensor.needkey = KEY_REQUIREMENT_UNKNOWN; + _newSensor.bat = 0x00; + _newSensor.RSSI = 0xffff; + _newSensor.lux = 0x00ffffff; + + switch (_type) + { + case MI_FLORA: + _newSensor.moisture =0xff; + _newSensor.fertility =0xffff; + _newSensor.firmware[0]='\0'; + _newSensor.feature.temp=1; + _newSensor.feature.moist=1; + _newSensor.feature.fert=1; + _newSensor.feature.lux=1; + _newSensor.feature.bat=1; + break; + case MI_NLIGHT: + _newSensor.events=0x00; + _newSensor.feature.PIR=1; + _newSensor.feature.NMT=1; + break; + case MI_MJYD2S: + _newSensor.NMT=0; + _newSensor.events=0x00; + _newSensor.feature.PIR=1; + _newSensor.feature.NMT=1; + _newSensor.feature.lux=1; + _newSensor.feature.bat=1; + break; + case MI_YEERC: + _newSensor.feature.Btn=1; + break; + default: + _newSensor.hum=NAN; + _newSensor.feature.temp=1; + _newSensor.feature.hum=1; + _newSensor.feature.tempHum=1; + _newSensor.feature.bat=1; + break; + } + MIBLEsensors.push_back(_newSensor); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: new %s at slot: %u"),D_CMND_MI32, kMI32DeviceType[_type-1],MIBLEsensors.size()-1); + MI32.mode.shallShowStatusInfo = 1; + return MIBLEsensors.size()-1; +}; + +/** + * @brief trigger real-time message for PIR or RC + * + */ +void MI32triggerTele(void){ + MI32.mode.triggeredTele = 1; + MI32ShowTriggeredSensors(); + MI32.mode.triggeredTele = 0; +} + +/** + * @brief Is called after every finding of new BLE sensor + * + */ +void MI32StatusInfo() { + MI32.mode.shallShowStatusInfo = 0; + Response_P(PSTR("{\"%s\":{\"found\":%u}}"), D_CMND_MI32, MIBLEsensors.size()); + XdrvRulesProcess(); +} + +/*********************************************************************************************\ + * BLE callbacks section + * These are called from main thread only. +\*********************************************************************************************/ + + +int MI32scanCompleteCallback(NimBLEScanResults results){ + // we actually don't need to do anything here.... + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: scancomplete")); + return 0; +} + + +/*********************************************************************************************\ + * init BLE_32 +\*********************************************************************************************/ + + +void MI32Init(void) { + MIBLEsensors.reserve(10); + MIBLEbindKeys.reserve(10); + MI32.mode.init = false; + + //test section for options + MI32.option.allwaysAggregate = 1; + MI32.option.noSummary = 0; + MI32.option.minimalSummary = 0; + MI32.option.directBridgeMode = 0; + MI32.option.showRSSI = 1; + MI32.option.ignoreBogusBattery = 1; // from advertisements + MI32.option.holdBackFirstAutodiscovery = 1; + + BLE_ESP32::registerForAdvertismentCallbacks((const char *)"MI32", MI32advertismentCallback); + BLE_ESP32::registerForScanCallbacks((const char *)"MI32", MI32scanCompleteCallback); + // note: for operations, we will set individual callbacks in the operations we request + //void registerForOpCallbacks(const char *tag, BLE_ESP32::OPCOMPLETE_CALLBACK* pFn); + + AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: init: request callbacks")); + MI32.period = Settings.tele_period; + MI32.mode.init = 1; + return; +} + + +/*********************************************************************************************\ + * Task section +\*********************************************************************************************/ + + + + +int MIParseBatt(int slot, uint8_t *data, int len){ + int value = data[0]; + char slotMAC[13]; + BLE_ESP32::dump(slotMAC, sizeof(slotMAC), MIBLEsensors[slot].MAC, 6) ; + + if ((value != 0) && (value < 101)){ + MIBLEsensors[slot].bat = value; + if(MIBLEsensors[slot].type==MI_FLORA){ + if (len < 7){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("FLORA: not enough bytes read for firmware?")); + } else { + memcpy(MIBLEsensors[slot].firmware, data+2, 5); + MIBLEsensors[slot].firmware[5] = '\0'; + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: FLORA Firmware: %s"),D_CMND_MI32,MIBLEsensors[slot].firmware); + } + } + MIBLEsensors[slot].eventType.bat = 1; + MIBLEsensors[slot].shallSendMQTT = 1; + MI32.mode.shallTriggerTele = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Batt read for %s complete %d"), slotMAC, value); + } else { + AddLog_P(LOG_LEVEL_ERROR,PSTR("Batt read for %s complete but out of range 1-101 (%d)"), slotMAC, value); + } + + return 0; +} + +/*********************************************************************************************\ + * parse the response from advertisements +\*********************************************************************************************/ + + +void MI32ParseATCPacket(const uint8_t * _buf, uint32_t length, const uint8_t *addr, int RSSI){ + ATCPacket_t *_packet = (ATCPacket_t*)_buf; + PVVXPacket_t *ppv_packet = (PVVXPacket_t*)_buf; + + + if (length == 15){ // 19-1-1-2 + uint8_t addrrev[6]; + memcpy(addrrev, addr, 6); + MI32_ReverseMAC(addrrev); + if (!memcmp(addrrev, ppv_packet->MAC, 6)){ + //int16_t temperature; // x 0.1 degree + //uint16_t humidity; // x 0.01 % + //uint16_t battery_mv; // mV + //uint8_t battery_level; // 0..100 % + //uint8_t counter; // measurement count + //uint8_t flags; + + uint32_t _slot = MIBLEgetSensorSlot(addr, 0x0a1c, ppv_packet->counter); // This must be a hard-coded fake ID + if(_slot==0xff) return; + + if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s:pvvx at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); + MIBLEsensors[_slot].RSSI=RSSI; + MIBLEsensors[_slot].needkey=KEY_NOT_REQUIRED; + + MIBLEsensors[_slot].temp = (float)(ppv_packet->temperature)/100.0f; + MIBLEsensors[_slot].hum = (float)(ppv_packet->humidity)/100.0f; + MIBLEsensors[_slot].eventType.tempHum = 1; + MIBLEsensors[_slot].bat = ppv_packet->battery_level; + MIBLEsensors[_slot].eventType.bat = 1; + + if(MI32.option.directBridgeMode) { + MIBLEsensors[_slot].shallSendMQTT = 1; + MI32.mode.shallTriggerTele = 1; + } + } + return; + } else { + AddLog_P(LOG_LEVEL_ERROR, PSTR("PVVX packet mac mismatch - ignored?")); + return; + } + } + + + uint8_t addrrev[6]; + memcpy(addrrev, addr, 6); + //MI32_ReverseMAC(addrrev); + + // if packet tell a different address to origin, use the different address + if (memcmp(addrrev, _packet->MAC, 6)){ + MI32_ReverseMAC(_packet->MAC); + if (!memcmp(addrrev, _packet->MAC, 6)){ + AddLog_P(LOG_LEVEL_ERROR, PSTR("ATC packet with reversed MAC addr?")); + } else { + AddLog_P(LOG_LEVEL_ERROR, PSTR("ATC packet with MAC addr mismatch - is this mesh?")); + memcpy(addrrev, _packet->MAC, 6); + } + addr = addrrev; + } + + uint32_t _slot = MIBLEgetSensorSlot(addr, 0x0a1c, _packet->frameCnt); // This must be a hard-coded fake ID + + if(_slot==0xff) return; + + if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); + MIBLEsensors[_slot].RSSI=RSSI; + MIBLEsensors[_slot].needkey=KEY_NOT_REQUIRED; + + MIBLEsensors[_slot].temp = (float)(int16_t(__builtin_bswap16(_packet->temp)))/10.0f; + MIBLEsensors[_slot].hum = (float)_packet->hum; + MIBLEsensors[_slot].eventType.tempHum = 1; + MIBLEsensors[_slot].bat = _packet->batPer; + MIBLEsensors[_slot].eventType.bat = 1; + + if(MI32.option.directBridgeMode) { + MIBLEsensors[_slot].shallSendMQTT = 1; + MI32.mode.shallTriggerTele = 1; + } + } else { + + } +} + +//////////////////////////////////////////////////////////// +// this SHOULD parse any MI payload. +int MI32parseMiPayload(int _slot, struct mi_beacon_data_t *parsed){ + struct mi_beacon_data_payload_data_t *pld = + (struct mi_beacon_data_payload_data_t *) &parsed->payload.data; + int res = 1; + + if (!parsed->payloadpresent){ + return 0; + } + + char tmp[20]; + BLE_ESP32::dump(tmp, 20, (uint8_t*)&(parsed->payload), parsed->payload.size+3); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("MI%d payload %s"), _slot, tmp); + + switch(parsed->payload.type){ + case 0x01: // button press + MIBLEsensors[_slot].Btn = pld->Btn.num + (pld->Btn.longPress/2)*6; + MIBLEsensors[_slot].eventType.Btn = 1; + MI32.mode.shallTriggerTele = 1; + break; + case 0x02: + res = 0; + break; + case 0x03: {// motion? 1 byte + uint8_t motion = parsed->payload.data[0]; + res = 0; + }break; + case 0x04:{ + float _tempFloat=(float)(pld->temp)/10.0f; + if(_tempFloat<60){ + MIBLEsensors[_slot].temp=_tempFloat; + MIBLEsensors[_slot].eventType.temp = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 4: temp updated")); + } else { + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 4: temp ignored > 60 (%f)"), _tempFloat); + } + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 4: U16: %u Temp"), _beacon.temp ); + } break; + case 0x06: { + float _tempFloat=(float)(pld->hum)/10.0f; + if(_tempFloat<101){ + MIBLEsensors[_slot].hum=_tempFloat; + MIBLEsensors[_slot].eventType.hum = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 6: hum updated")); + } else { + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 6: hum ignored > 101 (%f)"), _tempFloat); + } + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 6: U16: %u Hum"), _beacon.hum); + } break; + case 0x07: + MIBLEsensors[_slot].lux=pld->lux & 0x00ffffff; + if(MIBLEsensors[_slot].type==MI_MJYD2S){ + MIBLEsensors[_slot].eventType.noMotion = 1; + } + MIBLEsensors[_slot].eventType.lux = 1; + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 7: U24: %u Lux"), _beacon.lux & 0x00ffffff); + break; + case 0x08: + MIBLEsensors[_slot].moisture=pld->moist; + MIBLEsensors[_slot].eventType.moist = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 8: moisture updated")); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 8: U8: %u Moisture"), _beacon.moist); + break; + case 0x09: // 'conductivity' + MIBLEsensors[_slot].fertility=pld->fert; + MIBLEsensors[_slot].eventType.fert = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 9: fertility updated")); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 9: U16: %u Fertility"), _beacon.fert); + break; + case 0x0a: + if(MI32.option.ignoreBogusBattery){ + if(MIBLEsensors[_slot].type==MI_LYWSD03MMC || MIBLEsensors[_slot].type==MI_MHOC401){ + res = 0; + break; + } + } + if(pld->bat<101){ + MIBLEsensors[_slot].bat = pld->bat; + MIBLEsensors[_slot].eventType.bat = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode a: bat updated")); + } else { + MIBLEsensors[_slot].bat = 100; + MIBLEsensors[_slot].eventType.bat = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode a: bat > 100 (%d)"), pld->bat); + } + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode a: U8: %u %%"), _beacon.bat); + break; + case 0x0d:{ + float _tempFloat=(float)(pld->HT.temp)/10.0f; + if(_tempFloat < 60){ + MIBLEsensors[_slot].temp = _tempFloat; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode d: temp updated")); + } else { + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode d: temp ignored > 60 (%f)"), _tempFloat); + } + _tempFloat=(float)(pld->HT.hum)/10.0f; + if(_tempFloat < 100){ + MIBLEsensors[_slot].hum = _tempFloat; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode d: hum updated")); + } else { + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode d: hum ignored > 100 (%f)"), _tempFloat); + } + MIBLEsensors[_slot].eventType.tempHum = 1; + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode d: U16: %x Temp U16: %x Hum"), _beacon.HT.temp, _beacon.HT.hum); + } break; + case 0x0f: + if (parsed->payload.ten != 0) break; + MIBLEsensors[_slot].eventType.motion = 1; + MIBLEsensors[_slot].lastTime = millis(); + MIBLEsensors[_slot].events++; + MIBLEsensors[_slot].lux = pld->lux; + MIBLEsensors[_slot].eventType.lux = 1; + MIBLEsensors[_slot].NMT = 0; + MI32.mode.shallTriggerTele = 1; + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("PIR: primary"),MIBLEsensors[_slot].lux ); + break; + case 0x10:{ // 'formaldehide' + const uint16_t f = uint16_t(parsed->payload.data[0]) | (uint16_t(parsed->payload.data[1]) << 8); + float formaldehyde = (float)f / 100.0f; + res = 0; + } break; + case 0x12:{ // 'active' + int active = parsed->payload.data[0]; + res = 0; + } break; + case 0x13:{ //mosquito tablet + int tablet = parsed->payload.data[0]; + res = 0; + } break; + case 0x17:{ + const uint32_t idle_time = + uint32_t(parsed->payload.data[0]) | (uint32_t(parsed->payload.data[1]) << 8) | (uint32_t(parsed->payload.data[2]) << 16) | (uint32_t(parsed->payload.data[2]) << 24); + float idlemins = (float)idle_time / 60.0f; + int has_motion = (idle_time) ? 0 : 0; + + MIBLEsensors[_slot].NMT = pld->NMT; + MIBLEsensors[_slot].eventType.NMT = 1; + MI32.mode.shallTriggerTele = 1; + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 17: NMT: %u seconds"), _beacon.NMT); + } break; + + default: { + AddLog_P(LOG_LEVEL_DEBUG,PSTR("Unknown MI pld")); + res = 0; + } break; + } + + if(res && MI32.option.directBridgeMode) { + MIBLEsensors[_slot].shallSendMQTT = 1; + MI32.mode.shallTriggerTele = 1; + } + + return res; +} + +//////////////////////////////////////////////////////////// +// this SHOULD parse any MI packet, including encrypted. +void MI32ParseResponse(const uint8_t *buf, uint16_t bufsize, const uint8_t* addr, int RSSI) { + struct mi_beacon_data_t parsed; + memset(&parsed, 0, sizeof(parsed)); + int res = MIParsePacket(addr, &parsed, buf, bufsize); + + uint8_t addrrev[6]; + memcpy(addrrev, addr, 6); + MI32_ReverseMAC(addrrev); + + if (memcmp(addrrev, parsed.macdata.mac, 6)){ + AddLog_P(LOG_LEVEL_ERROR, PSTR("MI packet with MAC addr mismatch - is this mesh?")); + memcpy(addrrev, parsed.macdata.mac, 6); + MI32_ReverseMAC(addrrev); + addr = addrrev; + } + + uint16_t _slot = MIBLEgetSensorSlot( addr, parsed.devicetype, parsed.framecnt ); + if(_slot==0xff) return; + if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ + if (parsed.needkey != KEY_REQUIREMENT_UNKNOWN){ + MIBLEsensors[_slot].needkey = parsed.needkey; + } + MIBLEsensors[_slot].RSSI=RSSI; + if (!res){ // - if the payload is not valid + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("MIParsePacket returned %d"), res); + return; + } else { + } + MI32parseMiPayload(_slot, &parsed); + } +} + +bool MI32isInBlockList(const uint8_t* MAC){ + bool isBlocked = false; + for(auto &_blockedMAC : MIBLEBlockList){ + if(memcmp(_blockedMAC.buf,MAC,6) == 0) isBlocked = true; + } + return isBlocked; +} + +void MI32removeMIBLEsensor(uint8_t* MAC){ + // this will take and keep the mutex until the function is over + TasAutoMutex localmutex(&slotmutex, "Mi32Rem"); + + MIBLEsensors.erase( std::remove_if( MIBLEsensors.begin() , MIBLEsensors.end(), [MAC]( mi_sensor_t _sensor )->bool + { return (memcmp(_sensor.MAC,MAC,6) == 0); } + ), end( MIBLEsensors ) ); +} +/***********************************************************************\ + * Read data from connections +\***********************************************************************/ + +void MI32notifyHT_LY(int slot, char *_buf, int len){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,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]); + // the value 0b00 is 28.16 C? + if(_buf[0] != 0 || _buf[1] != 0){ + memcpy(&LYWSD0x_HT,(void *)_buf,sizeof(LYWSD0x_HT)); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(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 = slot; + + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,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; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("LYWSD0x: hum updated")); + } + MIBLEsensors[_slot].eventType.tempHum = 1; + if (MIBLEsensors[_slot].type == MI_LYWSD03MMC || MIBLEsensors[_slot].type == MI_MHOC401){ + // ok, so CR2032 is 3.0v, but drops immediately to ~2.9. + // so we'll go with the 2.1 min, 2.95 max. + float minVolts = 2100.0; + //float maxVolts = 2950.0; + //float range = maxVolts - minVolts; + //float divisor = range/100; // = 8.5 + float percent = (((float)LYWSD0x_HT.volt) - minVolts)/ 8.5; //divisor; + if (percent > 100) percent = 100; + + MIBLEsensors[_slot].bat = (int) percent; + MIBLEsensors[_slot].eventType.bat = 1; + } + if(MI32.option.directBridgeMode) { + MIBLEsensors[_slot].shallSendMQTT = 1; + MI32.mode.shallTriggerTele = 1; + } + } +} + + +/** + * @brief Launch functions from Core 1 to make race conditions less likely + * + */ + +void MI32Every50mSecond(){ + + if(MI32.mode.shallTriggerTele){ + MI32.mode.shallTriggerTele = 0; + MI32triggerTele(); + } +} + +/** + * @brief Main loop of the driver, "high level"-loop + * + */ + +void MI32EverySecond(bool restart){ + +// AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("MI32: onesec")); + MI32TimeoutSensors(); + + MI32ShowSomeSensors(); + + // read a battery if + // MI32.batteryreader.slot < filled and !MI32.batteryreader.active + readOneBat(); + + + // read a sensor if + // MI32.sensorreader.slot < filled and !MI32.sensorreader.active + // for sensors which need to get data through notify... + readOneSensor(); + + if (MI32.secondsCounter >= MI32.period){ + // only if we finished the last read + if (MI32.sensorreader.slot >= MIBLEsensors.size()){ + AddLog_P(LOG_LEVEL_DEBUG,PSTR("kick off readOneSensor")); + // kick off notification sensor reading every period. + MI32.sensorreader.slot = 0; + MI32.secondsCounter = 0; + } + } + MI32.secondsCounter ++; + + if (MI32.secondsCounter2 >= MI32.period){ + if (MI32.mqttCurrentSlot >= MIBLEsensors.size()){ + AddLog_P(LOG_LEVEL_DEBUG,PSTR("kick off tele sending")); + MI32.mqttCurrentSlot = 0; + MI32.secondsCounter2 = 0; + } else { + AddLog_P(LOG_LEVEL_DEBUG,PSTR("hit tele time, restarted but not finished last - lost from slot %d")+MI32.mqttCurrentSlot); + MI32.mqttCurrentSlot = 0; + MI32.secondsCounter2 = 0; + } + } + MI32.secondsCounter2++; + + static uint32_t _counter = MI32.period - 15; + static uint32_t _nextSensorSlot = 0; + uint32_t _idx = 0; + + int numsensors = MIBLEsensors.size(); + for (uint32_t i = 0; i < numsensors; i++) { + if(MIBLEsensors[i].type==MI_NLIGHT || MIBLEsensors[i].type==MI_MJYD2S){ + MIBLEsensors[i].NMT++; + } + } + + if(MI32.mode.shallShowStatusInfo == 1){ + MI32StatusInfo(); + } +} + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +void CmndMi32Period(void) { + if (XdrvMailbox.data_len > 0) { + if (1 == XdrvMailbox.payload) { + MI32EverySecond(true); + } else { + MI32.period = XdrvMailbox.payload; + } + } + ResponseCmndNumber(MI32.period); +} + +int findSlot(char *addrOrAlias){ + uint8_t mac[6]; + int res = BLE_ESP32::getAddr(mac, addrOrAlias); + if (!res) return -1; + + for (int i = MIBLEsensors.size()-1; i >= 0 ; i--) { + if (!memcmp(MIBLEsensors[i].MAC, mac, 6)){ + return i; + } + } + return -1; +} + + +void CmndMi32Time(void) { + if (XdrvMailbox.data_len > 0) { + int slot = findSlot(XdrvMailbox.data); + if (slot < 0) { + slot = XdrvMailbox.payload; + } + if (MIBLEsensors.size() > slot) { + int res = genericTimeWriteFn(slot); + if (res > 0){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("MI32: will set Time")); + ResponseCmndNumber(slot); + return; + } + if (res < 0) { + AddLog_P(LOG_LEVEL_ERROR, PSTR("MI32: cannot set Time on sensor type")); + } + if (res == 0) { + AddLog_P(LOG_LEVEL_ERROR, PSTR("MI32: cannot set Time right now")); + } + } + } + ResponseCmndChar_P("fail"); +} + +void CmndMi32Page(void) { + if (XdrvMailbox.payload > 0) { + MI32.perPage = XdrvMailbox.payload; + } + ResponseCmndNumber(MI32.perPage); +} + +// read ALL battery values where we can? +void CmndMi32Battery(void) { + // trigger a read cycle + MI32.batteryreader.slot = 0; + ResponseCmndDone(); +} + + +void CmndMi32Unit(void) { + if (XdrvMailbox.data_len > 0) { + int slot = findSlot(XdrvMailbox.data); + if (slot < 0) { + slot = XdrvMailbox.payload; + } + + if (MIBLEsensors.size() > slot) { + // TOGGLE unit? + int res = genericUnitWriteFn(slot, -1); + if (res > 0){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("MI32: will toggle Unit")); + ResponseCmndNumber(slot); + return; + } + if (res < 0) { + AddLog_P(LOG_LEVEL_ERROR, PSTR("MI32: cannot toggle Unit on sensor type")); + } + if (res == 0) { + AddLog_P(LOG_LEVEL_ERROR, PSTR("MI32: cannot toggle Unit right now")); + } + } + } + ResponseCmndIdxChar(PSTR("Invalid")); +} + +#ifdef USE_MI_DECRYPTION +void CmndMi32Key(void) { + if (44 == XdrvMailbox.data_len) { // a KEY-MAC-string + MI32AddKey(XdrvMailbox.data, nullptr); + MI32KeyListResp(); + } else { + ResponseCmndIdxChar(PSTR("Invalid")); + } +} +#endif // USE_MI_DECRYPTION + +void MI32BlockListResp(){ + Response_P(PSTR("{\"MI32Block\":{")); + for (int i = 0; i < MIBLEBlockList.size(); i++){ + if (i){ + ResponseAppend_P(PSTR(",")); + } + char tmp[20]; + ToHex_P(MIBLEBlockList[i].buf,6,tmp,20,0); + ResponseAppend_P(PSTR("\"%s\":1"), tmp); + } + ResponseAppend_P(PSTR("}}")); +} + + +void CmndMi32Block(void){ + if (XdrvMailbox.data_len == 0) { + switch (XdrvMailbox.index) { + case 0: { + //TasAutoMutex localmutex(&slotmutex, "Mi32Block1"); + MIBLEBlockList.clear(); + } break; + default: + case 1: + break; + } + MI32BlockListResp(); + return; + } + + MAC_t _MACasBytes; + int res = BLE_ESP32::getAddr(_MACasBytes.buf, XdrvMailbox.data); + if (!res){ + ResponseCmndIdxChar(PSTR("Addr invalid")); + return; + } + + //MI32HexStringToBytes(XdrvMailbox.data,_MACasBytes.buf); + switch (XdrvMailbox.index) { + case 0: { + //TasAutoMutex localmutex(&slotmutex, "Mi32Block2"); + MIBLEBlockList.erase( std::remove_if( begin( MIBLEBlockList ), end( MIBLEBlockList ), [_MACasBytes]( MAC_t& _entry )->bool + { return (memcmp(_entry.buf,_MACasBytes.buf,6) == 0); } + ), end( MIBLEBlockList ) ); + } break; + case 1: { + //TasAutoMutex localmutex(&slotmutex, "Mi32Block3"); + bool _notYetInList = true; + for (auto &_entry : MIBLEBlockList) { + if (memcmp(_entry.buf,_MACasBytes.buf,6) == 0){ + _notYetInList = false; + } + } + if (_notYetInList) { + MIBLEBlockList.push_back(_MACasBytes); + MI32removeMIBLEsensor(_MACasBytes.buf); + } + // AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: size of ilist: %u"), MIBLEBlockList.size()); + } break; + } + MI32BlockListResp(); +} + +void CmndMi32Option(void){ + bool onOff = atoi(XdrvMailbox.data); + switch(XdrvMailbox.index) { + case 0: + MI32.option.allwaysAggregate = onOff; + break; + case 1: + MI32.option.noSummary = onOff; + break; + case 2: + MI32.option.directBridgeMode = onOff; + break; + case 4:{ + MI32.option.ignoreBogusBattery = onOff; + } break; + } + ResponseCmndDone(); +} + +void MI32KeyListResp(){ + Response_P(PSTR("{\"MIKeys\":{")); + for (int i = 0; i < MIBLEbindKeys.size(); i++){ + if (i){ + ResponseAppend_P(PSTR(",")); + } + char tmp[20]; + ToHex_P(MIBLEbindKeys[i].MAC,6,tmp,20,0); + char key[16*2+1]; + ToHex_P(MIBLEbindKeys[i].key,16,key,33,0); + + ResponseAppend_P(PSTR("\"%s\":\"%s\""), tmp, key); + } + ResponseAppend_P(PSTR("}}")); +} + + +void CmndMi32Keys(void){ +#ifdef BLE_ESP32_ALIASES + int op = XdrvMailbox.index; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("key %d %s"), op, XdrvMailbox.data); + + int res = -1; + switch(op){ + case 0: + case 1:{ + char *p = strtok(XdrvMailbox.data, " ,="); + bool trigger = false; + int added = 0; + + do { + if (!p || !(*p)){ + break; + } + + uint8_t addr[6]; + char *mac = p; + int addrres = BLE_ESP32::getAddr(addr, p); + if (!addrres){ + ResponseCmndChar("invalidmac"); + return; + } + + p = strtok(nullptr, " ,="); + char *key = p; + if (!p || !(*p)){ + int i = 0; + for (i = 0; i < MIBLEbindKeys.size(); i++){ + mi_bindKey_t *key = &MIBLEbindKeys[i]; + if (!memcmp(key->MAC, addr, 6)){ + MIBLEbindKeys.erase(MIBLEbindKeys.begin() + i); + MI32KeyListResp(); + return; + } + } + ResponseCmndChar("invalidmac"); + return; + } + + AddLog_P(LOG_LEVEL_ERROR,PSTR("Add key mac %s = key %s"), mac, key); + char tmp[20]; + // convert mac back to string + ToHex_P(addr,6,tmp,20,0); + if (MI32AddKey(tmp, key)){ + added++; + } + p = strtok(nullptr, " ,="); + } while (p); + + if (added){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Added %d Keys"), added); + MI32KeyListResp(); + } else { + MI32KeyListResp(); + } + return; + } break; + case 2:{ // clear + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32Keys clearing %d"), MIBLEbindKeys.size()); + for (int i = MIBLEbindKeys.size()-1; i >= 0; i--){ + MIBLEbindKeys.pop_back(); + } + MI32KeyListResp(); + return; + } break; + } + ResponseCmndChar("invalididx"); +#endif +} + + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +const char HTTP_MI32[] PROGMEM = "{s}MI ESP32 v0918{m}%u%s / %u{e}"; +const char HTTP_MI32_ALIAS[] PROGMEM = "{s}%s Alias {m}%s{e}"; +const char HTTP_MI32_MAC[] PROGMEM = "{s}%s %s{m}%s{e}"; +const char HTTP_RSSI[] PROGMEM = "{s}%s " D_RSSI "{m}%d dBm{e}"; +const char HTTP_BATTERY[] PROGMEM = "{s}%s" " Battery" "{m}%u %%{e}"; +const char HTTP_LASTBUTTON[] PROGMEM = "{s}%s Last Button{m}%u {e}"; +const char HTTP_EVENTS[] PROGMEM = "{s}%s Events{m}%u {e}"; +const char HTTP_NMT[] PROGMEM = "{s}%s No motion{m}> %u seconds{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}"; + +//const char HTTP_NEEDKEY[] PROGMEM = "{s}%s %s{m} {e}"; + +//const char HTTP_NEEDKEY[] PROGMEM = "{s}%s %s{m} {e}"; +const char HTTP_NEEDKEY[] PROGMEM = "{s}%s %s{m} {e}"; + + +const char HTTP_PAIRING[] PROGMEM = "{s}%s Pair Button Pressed{m} {e}"; + + +const char HTTP_KEY_ERROR[] PROGMEM = "Key error %s"; +const char HTTP_MAC_ERROR[] PROGMEM = "MAC error %s"; +const char HTTP_KEY_ADDED[] PROGMEM = "Cmnd: MI32Keys %s=%s"; +const char HTTP_MI_KEY_STYLE[] PROGMEM = ""; + + +#define D_MI32_KEY "MI32 Set Key" + +void HandleMI32Key(){ + AddLog_P(LOG_LEVEL_DEBUG, PSTR("HandleMI32Key hit")); + if (!HttpCheckPriviledgedAccess()) { + AddLog_P(LOG_LEVEL_DEBUG, PSTR("!HttpCheckPriviledgedAccess()")); + return; + } + WSContentStart_P(PSTR(D_MI32_KEY)); + WSContentSendStyle_P(HTTP_MI_KEY_STYLE); + + char key[64] = {0}; + WebGetArg("key", key, sizeof(key)); + + if (strlen(key) != 16*2){ + WSContentSend_P(HTTP_KEY_ERROR, key); + WSContentStop(); + return; + } + + char mac[13] = {0}; + WebGetArg("mac", mac, sizeof(mac)); + if (strlen(mac) != 12){ + WSContentSend_P(HTTP_MAC_ERROR, mac); + WSContentStop(); + return; + } + + WSContentSend_P(HTTP_KEY_ADDED, mac, key); + + strncat(key, mac, sizeof(key)); + MI32AddKey(key, nullptr); + +// WSContentSpaceButton(BUTTON_CONFIGURATION); + WSContentStop(); +} + + +void MI32TimeoutSensors(){ + // whatever, this function access all the arrays.... + // so block for as long as it takes. + + // PROBLEM: when we take this, it hangs the BLE loop. + // BUT, devicePresent uses the + // remove devices for which the adverts have timed out + for (int i = MIBLEsensors.size()-1; i >= 0 ; i--) { + //if (MIBLEsensors[i].MAC[2] || MIBLEsensors[i].MAC[3] || MIBLEsensors[i].MAC[4] || MIBLEsensors[i].MAC[5]){ + if (!BLE_ESP32::devicePresent(MIBLEsensors[i].MAC)){ + uint8_t *mac = MIBLEsensors[i].MAC; + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: dev no longer present MAC: %02x%02x%02x%02x%02x%02x"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + TasAutoMutex localmutex(&slotmutex, "Mi32Timeout"); + MIBLEsensors.erase(MIBLEsensors.begin() + i); + } + //} + } +} + + +// this assumes that we're adding to a ResponseTime_P +void MI32GetOneSensorJson(int slot){ + mi_sensor_t *p; + p = &MIBLEsensors[slot]; + + ResponseAppend_P(PSTR(",\"%s-%02x%02x%02x\":{"), + kMI32DeviceType[p->type-1], + p->MAC[3], p->MAC[4], p->MAC[5]); + + ResponseAppend_P(PSTR("\"MAC\":\"%02x%02x%02x%02x%02x%02x\""), + p->MAC[0], p->MAC[1], p->MAC[2], + p->MAC[3], p->MAC[4], p->MAC[5]); + + if((!MI32.mode.triggeredTele && !MI32.option.minimalSummary)||MI32.mode.triggeredTele){ + bool tempHumSended = false; + if(p->feature.tempHum){ + if(p->eventType.tempHum || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ + if (!isnan(p->hum) && !isnan(p->temp) +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { + ResponseAppend_P(PSTR(",")); + ResponseAppendTHD(p->temp, p->hum); + tempHumSended = true; + } + } + } + if(p->feature.temp && !tempHumSended){ + if(p->eventType.temp || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate) { + if (!isnan(p->temp) +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { + char temperature[FLOATSZ]; + dtostrfd(p->temp, Settings.flag2.temperature_resolution, temperature); + ResponseAppend_P(PSTR(",\"" D_JSON_TEMPERATURE "\":%s"), temperature); + } + } + } + if(p->feature.hum && !tempHumSended){ + if(p->eventType.hum || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate) { + if (!isnan(p->hum) +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { + char hum[FLOATSZ]; + dtostrfd(p->hum, Settings.flag2.humidity_resolution, hum); + ResponseAppend_P(PSTR(",\"" D_JSON_HUMIDITY "\":%s"), hum); + } + } + } + if (p->feature.lux){ + if(p->eventType.lux || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ + if (p->lux!=0x0ffffff +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { // this is the error code -> no lux + ResponseAppend_P(PSTR(",\"" D_JSON_ILLUMINANCE "\":%u"), p->lux); +#ifdef USE_HOME_ASSISTANT + if (p->lux==0x0ffffff) MI32nullifyEndOfMQTT_DATA(); +#endif //USE_HOME_ASSISTANT + } + } + } + if (p->feature.moist){ + if(p->eventType.moist || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ + if (p->moisture!=0xff +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { + ResponseAppend_P(PSTR(",\"" D_JSON_MOISTURE "\":%u"), p->moisture); +#ifdef USE_HOME_ASSISTANT + if (p->moisture==0xff) MI32nullifyEndOfMQTT_DATA(); +#endif //USE_HOME_ASSISTANT + } + } + } + if (p->feature.fert){ + if(p->eventType.fert || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ + if (p->fertility!=0xffff +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { + ResponseAppend_P(PSTR(",\"Fertility\":%u"), p->fertility); +#ifdef USE_HOME_ASSISTANT + if (p->fertility==0xffff) MI32nullifyEndOfMQTT_DATA(); +#endif //USE_HOME_ASSISTANT + } + } + } + if (p->feature.Btn){ + if(p->eventType.Btn +#ifdef USE_HOME_ASSISTANT + ||(hass_mode==2) +#endif //USE_HOME_ASSISTANT + ){ + ResponseAppend_P(PSTR(",\"Btn\":%u"),p->Btn); + } + } + if(p->eventType.PairBtn && p->pairing){ + ResponseAppend_P(PSTR(",\"Pair\":%u"),p->pairing); + } + } // minimal summary + + + if (p->feature.PIR){ + if(p->eventType.motion || !MI32.mode.triggeredTele){ + if(MI32.mode.triggeredTele) ResponseAppend_P(PSTR(",\"PIR\":1")); // only real-time + ResponseAppend_P(PSTR(",\"Events\":%u"),p->events); + } + else if(p->eventType.noMotion && MI32.mode.triggeredTele){ + ResponseAppend_P(PSTR(",\"PIR\":0")); + } + } + + if (p->type == MI_FLORA && !MI32.mode.triggeredTele) { + if (p->firmware[0] != '\0') { // this is the error code -> no firmware + ResponseAppend_P(PSTR(",\"Firmware\":\"%s\""), p->firmware); + } + } + + if (p->feature.NMT || !MI32.mode.triggeredTele){ + if(p->eventType.NMT){ + ResponseAppend_P(PSTR(",\"NMT\":%u"), p->NMT); + } + } + if (p->feature.bat){ + if(p->eventType.bat || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ + if (p->bat != 0x00 +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { // this is the error code -> no battery + ResponseAppend_P(PSTR(",\"Battery\":%u"), p->bat); +#ifdef USE_HOME_ASSISTANT + if (p->bat == 0x00) MI32nullifyEndOfMQTT_DATA(); +#endif //USE_HOME_ASSISTANT + } + } + } + if (MI32.option.showRSSI) ResponseAppend_P(PSTR(",\"RSSI\":%d"), p->RSSI); + + ResponseAppend_P(PSTR("}")); + p->eventType.raw = 0; + p->shallSendMQTT = 0; + +} + + +/////////////////////////////////////////////// +// starts a completely fresh MQTT message. +// sends up to 4 sensors +// triggered by setting MI32.mqttCurrentSlot = 0 +void MI32ShowSomeSensors(){ + // don't detect half-added ones here + int numsensors = MIBLEsensors.size(); + if (MI32.mqttCurrentSlot >= numsensors){ + // if we got to the end of the sensors, then don't send more + return; + } + +#ifdef USE_HOME_ASSISTANT + bool _noSummarySave = MI32.option.noSummary; + bool _minimalSummarySave = MI32.option.minimalSummary; + if(hass_mode==2){ + if(MI32.option.holdBackFirstAutodiscovery){ + if(!MI32.mode.firstAutodiscoveryDone){ + MI32.mode.firstAutodiscoveryDone = 1; + return; + } + } + MI32.option.noSummary = false; + MI32.option.minimalSummary = false; + } +#endif //USE_HOME_ASSISTANT + + ResponseTime_P(PSTR("")); + int cnt = 0; + for (; (MI32.mqttCurrentSlot < numsensors) && (cnt < 4); MI32.mqttCurrentSlot++, cnt++) { + MI32GetOneSensorJson(MI32.mqttCurrentSlot); + int mlen = strlen(TasmotaGlobal.mqtt_data); + + // if we ran out of room, leave here. + if (sizeof(TasmotaGlobal.mqtt_data) - mlen < 100){ + MI32.mqttCurrentSlot++; + break; + } + } + ResponseAppend_P(PSTR("}")); + MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); + //AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: show some %d %s"),D_CMND_MI32, MI32.mqttCurrentSlot, TasmotaGlobal.mqtt_data); + +#ifdef USE_RULES + RulesTeleperiod(); // Allow rule based HA messages +#endif // USE_RULES + +#ifdef USE_HOME_ASSISTANT + if(hass_mode==2){ + MI32.option.noSummary = _noSummarySave; + MI32.option.minimalSummary = _minimalSummarySave; + } +#endif //USE_HOME_ASSISTANT +} + +/////////////////////////////////////////////// +// starts a completely fresh MQTT message. +// sends up to 4 sensors pe5r msg +// sends only those which are raw and triggered. +// triggered by setting MI32.mode.triggeredTele = 1 +void MI32ShowTriggeredSensors(){ + if (!MI32.mode.triggeredTele) return; // none to show + MI32.mode.triggeredTele = 0; + + // don't detect half-added ones here + int numsensors = MIBLEsensors.size(); + + int sensor = 0; + + do { + ResponseTime_P(PSTR("")); + int cnt = 0; + for (; (sensor < numsensors) && (cnt < 4); sensor++) { + mi_sensor_t *p; + p = &MIBLEsensors[sensor]; + if(p->eventType.raw == 0) continue; + if(p->shallSendMQTT==0) continue; + + cnt++; + MI32GetOneSensorJson(sensor); + int mlen = strlen(TasmotaGlobal.mqtt_data); + + // if we ran out of room, leave here. + if (sizeof(TasmotaGlobal.mqtt_data) - mlen < 100){ + sensor++; + break; + } + } + if (cnt){ // if we got one, then publish + ResponseAppend_P(PSTR("}")); + MqttPublishPrefixTopic_P(STAT, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: triggered %d %s"),D_CMND_MI32, sensor, TasmotaGlobal.mqtt_data); + +#ifdef USE_RULES + RulesTeleperiod(); // Allow rule based HA messages +#endif // USE_RULES + + } else { // else don't and clear + ResponseClear(); + } + } while (sensor < numsensors); +} + + +void MI32Show(bool json) +{ + // don't detect half-added ones here + int numsensors = MIBLEsensors.size(); + + if (json) { + // TELE JSON messages now do nothing here, apart from set MI32.mqttCurrentSlot + // which will trigger send next second of up to 4 sensors, then the next four in the next second, etc. + //MI32.mqttCurrentSlot = 0; + +#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 > numsensors){ + j = numsensors; + } + char stemp[5] ={0}; + if (numsensors-(_page*MI32.perPage)>1 && MI32.perPage!=1) { + sprintf_P(stemp,"-%u",j); + } + if (numsensors==0) i=-1; // only for the GUI + + WSContentSend_PD(HTTP_MI32, i+1,stemp,numsensors); + for (i; itype-1]; + const char *alias = BLE_ESP32::getAlias(p->MAC); + if (alias && *alias){ + WSContentSend_PD(HTTP_MI32_ALIAS, typeName, alias); + } + char _MAC[18]; + ToHex_P(p->MAC,6,_MAC,18);//,':'); + WSContentSend_PD(HTTP_MI32_MAC, typeName, D_MAC_ADDRESS, _MAC); + WSContentSend_PD(HTTP_RSSI, typeName, p->RSSI); + + + // for some reason, display flora differently + switch(p->type){ + case MI_FLORA:{ + if (!isnan(p->temp)) { + char temperature[FLOATSZ]; + dtostrfd(p->temp, Settings.flag2.temperature_resolution, temperature); + WSContentSend_PD(HTTP_SNS_TEMP, typeName, temperature, TempUnit()); + } + if (p->moisture!=0xff) { + WSContentSend_PD(HTTP_SNS_MOISTURE, typeName, p->moisture); + } + if (p->fertility!=0xffff) { + WSContentSend_PD(HTTP_MI32_FLORA_DATA, typeName, p->fertility); + } + } break; + default:{ + if (!isnan(p->hum) && !isnan(p->temp)) { + WSContentSend_THD(typeName, p->temp, p->hum); + } + } + } + +#ifdef USE_MI_DECRYPTION + bool showkey = false; + char tmp[40]; + strcpy(tmp, PSTR("KeyRqd")); + switch(p->needkey) { + default:{ + snprintf(tmp, 39, PSTR("?%d?"), p->needkey ); + showkey = true; + } break; + case KEY_REQUIREMENT_UNKNOWN: { + strcpy(tmp, PSTR("WAIT")); + showkey = true; + } break; + case KEY_NOT_REQUIRED: { + strcpy(tmp, PSTR("NOTKEY")); + //showkey = true; + } break; + case KEY_REQUIRED_BUT_NOT_FOUND: { + strcpy(tmp, PSTR("NoKey")); + showkey = true; + } break; + case KEY_REQUIRED_AND_FOUND: { + strcpy(tmp, PSTR("KeyOk")); + showkey = true; + } break; + case KEY_REQUIRED_AND_INVALID: { + strcpy(tmp, PSTR("KeyInv")); + showkey = true; + } break; + } + + // adds the link to get the key. + // provides mac and callback address to receive the key, if we had a website which did this + // (future work) + if (showkey){ + BLE_ESP32::dump(_MAC, 13, p->MAC,6); + WSContentSend_PD(HTTP_NEEDKEY, typeName, _MAC, Webserver->client().localIP().toString().c_str(), tmp ); + } + + if (p->type==MI_NLIGHT || p->type==MI_MJYD2S) { +#else + if (p->type==MI_NLIGHT) { +#endif //USE_MI_DECRYPTION + WSContentSend_PD(HTTP_EVENTS, typeName, p->events); + if(p->NMT>0) WSContentSend_PD(HTTP_NMT, typeName, p->NMT); + } + + if (p->lux!=0x00ffffff) { // this is the error code -> no valid value + WSContentSend_PD(HTTP_SNS_ILLUMINANCE, typeName, p->lux); + } + if(p->bat!=0x00){ + WSContentSend_PD(HTTP_BATTERY, typeName, p->bat); + } + if (p->type==MI_YEERC){ + WSContentSend_PD(HTTP_LASTBUTTON, typeName, p->Btn); + } + if (p->pairing){ + WSContentSend_PD(HTTP_PAIRING, typeName); + } + } + _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 +\*********************************************************************************************/ +#define WEB_HANDLE_MI32 "mikey" + +bool Xsns62(uint8_t function) +{ +// if (!Settings.flag5.mi32_enable) { return false; } // SetOption115 - Enable ESP32 MI32 BLE +// return false; + + bool result = false; + + switch (function) { + case FUNC_INIT: + MI32Init(); + break; + case FUNC_EVERY_50_MSECOND: + MI32Every50mSecond(); + break; + case FUNC_EVERY_SECOND: + MI32EverySecond(false); + break; + case FUNC_COMMAND: + result = DecodeCommand(kMI32_Commands, MI32_Commands); + break; + case FUNC_JSON_APPEND: + // we are not in control of when this is called... + //MI32Show(1); + break; +#ifdef USE_WEBSERVER + case FUNC_WEB_ADD_HANDLER: + WebServer_on(PSTR("/" WEB_HANDLE_MI32), HandleMI32Key); + break; + case FUNC_WEB_SENSOR: + MI32Show(0); + break; +#endif // USE_WEBSERVER + } + return result; +} +#endif // USE_MI_ESP32 +#endif // ESP32 + +#endif \ No newline at end of file From e3ea65e8fada6c5ddb052f340855ee18eb897643 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Sun, 17 Jan 2021 18:51:22 +0100 Subject: [PATCH 016/186] Zigbee add RGB and RGBb to ZbInfo --- tasmota/xdrv_23_zigbee_2_devices.ino | 5 ++ tasmota/xdrv_23_zigbee_2a_devices_impl.ino | 4 + tasmota/xdrv_23_zigbee_5_converters.ino | 100 ++++++++++++++------- 3 files changed, 75 insertions(+), 34 deletions(-) diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino index d2515cd14..47247ddd5 100644 --- a/tasmota/xdrv_23_zigbee_2_devices.ino +++ b/tasmota/xdrv_23_zigbee_2_devices.ino @@ -236,6 +236,11 @@ public: inline void setX(uint16_t _x) { x = _x; } inline void setY(uint16_t _y) { y = _y; } + void toRGBAttributes(Z_attribute_list & attr_list) const ; + static void toRGBAttributesHSB(Z_attribute_list & attr_list, uint16_t hue, uint8_t sat, uint8_t brightness); + static void toRGBAttributesXYB(Z_attribute_list & attr_list, uint16_t x, uint16_t y, uint8_t brightness); + static void toRGBAttributesRGBb(Z_attribute_list & attr_list, uint8_t r, uint8_t g, uint8_t b, uint8_t brightness); + static const Z_Data_Type type = Z_Data_Type::Z_Light; // 12 bytes uint8_t colormode; // 0x00: Hue/Sat, 0x01: XY, 0x02: CT | 0xFF not set, default 0x01 diff --git a/tasmota/xdrv_23_zigbee_2a_devices_impl.ino b/tasmota/xdrv_23_zigbee_2a_devices_impl.ino index ec7d773e4..ffb5a72df 100644 --- a/tasmota/xdrv_23_zigbee_2a_devices_impl.ino +++ b/tasmota/xdrv_23_zigbee_2a_devices_impl.ino @@ -695,6 +695,9 @@ void Z_Device::jsonAddDataAttributes(Z_attribute_list & attr_list) const { // show internal data - mostly last known values for (auto & data_elt : data) { data_elt.toAttributes(attr_list); + if (data_elt.getType() == Z_Data_Type::Z_Light) { // since we don't have virtual methods, do an explicit test + ((Z_Data_Light&)data_elt).toRGBAttributes(attr_list); + } } } // Add "BatteryPercentage", "LastSeen", "LastSeenEpoch", "LinkQuality" @@ -728,6 +731,7 @@ void Z_Device::jsonLightState(Z_attribute_list & attr_list) const { if (light.validHue()) { attr_list.findOrCreateAttribute(PSTR("Hue")).setUInt(light.getHue()); } + light.toRGBAttributes(attr_list); } attr_list.addAttributePMEM(PSTR("Light")).setInt(light_mode); } diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index 1f47204a0..674f6fdd0 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -509,8 +509,6 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { Zuint16, Cx0300, 0x003A, Z_(ColorPointBX), Cm1, 0 }, { Zuint16, Cx0300, 0x003B, Z_(ColorPointBY), Cm1, 0 }, { Zuint8, Cx0300, 0x003C, Z_(ColorPointBIntensity), Cm1, 0 }, - { Zoctstr, Cx0300, 0xFFF0, Z_(RGB), Cm1, 0 }, // synthetic argument to show color as RGB (converted from HueSat or XY) - { Zoctstr, Cx0300, 0xFFF1, Z_(RGBb), Cm1, 0 }, // synthetic argument to show color as RGB including last known brightness // Illuminance Measurement cluster { Zuint16, Cx0400, 0x0000, Z_(Illuminance), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_PIR, illuminance) }, // Illuminance (in Lux) @@ -1340,45 +1338,27 @@ void ZCLFrame::computeSyntheticAttributes(Z_attribute_list& attr_list) { case 0x03000003: // X case 0x03000004: // Y { // generate synthetic RGB - const Z_attribute * attr_rgb = attr_list.findAttribute(0x0300, 0xFFF0); + const Z_attribute * attr_rgb = attr_list.findAttribute(PSTR("RGB")); if (attr_rgb == nullptr) { // make sure we didn't already computed it - uint8_t r,g,b; - bool is_rgb = false; + uint8_t brightness = 255; + if (device.valid()) { + const Z_Data_Light & light = device.data.find(_srcendpoint); + if ((&light != nullptr) && (light.validDimmer())) { + // Dimmer has a valid value + brightness = changeUIntScale(light.getDimmer(), 0, 254, 0, 255); // range is 0..255 + } + } + const Z_attribute * attr_hue = attr_list.findAttribute(0x0300, 0x0000); const Z_attribute * attr_sat = attr_list.findAttribute(0x0300, 0x0001); const Z_attribute * attr_x = attr_list.findAttribute(0x0300, 0x0003); const Z_attribute * attr_y = attr_list.findAttribute(0x0300, 0x0004); if (attr_hue && attr_sat) { uint8_t sat = changeUIntScale(attr_sat->getUInt(), 0, 254, 0, 255); - HsToRgb(attr_hue->getUInt(), sat, &r, &g, &b); - is_rgb = true; - } else if (attr_x && attr_y) { - XyToRgb(attr_x->getUInt() / 65535.0f, attr_y->getUInt() / 65535.0f, &r, &g, &b); - is_rgb = true; - } - if (is_rgb) { - SBuffer rgb(3); - rgb.add8(r); - rgb.add8(g); - rgb.add8(b); - attr_list.addAttribute(0x0300, 0xFFF0).setBuf(rgb, 0, 3); - - // do we know ZbData for this bulb - uint8_t brightness = 255; - if (device.valid()) { - const Z_Data_Light & light = device.data.find(_srcendpoint); - if (light.validDimmer()) { - // Dimmer has a valid value - brightness = changeUIntScale(light.getDimmer(), 0, 254, 0, 255); // range is 0..255 - } - } - r = changeUIntScale(r, 0, 255, 0, brightness); - g = changeUIntScale(g, 0, 255, 0, brightness); - b = changeUIntScale(b, 0, 255, 0, brightness); - rgb.set8(0, r); - rgb.set8(1, g); - rgb.set8(2, b); - attr_list.addAttribute(0x0300, 0xFFF1).setBuf(rgb, 0, 3); + uint16_t hue = changeUIntScale(attr_hue->getUInt(), 0, 254, 0, 360); + Z_Data_Light::toRGBAttributesHSB(attr_list, hue, sat, brightness); + } else if (attr_x && attr_y) { + Z_Data_Light::toRGBAttributesXYB(attr_list, attr_x->getUInt(), attr_y->getUInt(), brightness); } } } @@ -2232,6 +2212,58 @@ void Z_Data::toAttributes(Z_attribute_list & attr_list) const { } } +// Add both attributes RGB and RGBb based on the inputs +// r,g,b are expected to be 100% brightness +// brightness is expected 0..255 +void Z_Data_Light::toRGBAttributesRGBb(Z_attribute_list & attr_list, uint8_t r, uint8_t g, uint8_t b, uint8_t brightness) { + SBuffer rgb(3); + rgb.add8(r); + rgb.add8(g); + rgb.add8(b); + attr_list.addAttribute(PSTR("RGB"), true).setBuf(rgb, 0, 3); + // now blend with brightness + r = changeUIntScale(r, 0, 255, 0, brightness); + g = changeUIntScale(g, 0, 255, 0, brightness); + b = changeUIntScale(b, 0, 255, 0, brightness); + rgb.set8(0, r); + rgb.set8(1, g); + rgb.set8(2, b); + attr_list.addAttribute(PSTR("RGBb"), true).setBuf(rgb, 0, 3); +} + +// Convert from Hue/Sat + Brightness to RGB+RGBb +// sat: 0..255 +// hue: 0..359 +// brightness: 0..255 +void Z_Data_Light::toRGBAttributesHSB(Z_attribute_list & attr_list, uint16_t hue, uint8_t sat, uint8_t brightness) { + uint8_t r,g,b; + HsToRgb(hue, sat, &r, &g, &b); + Z_Data_Light::toRGBAttributesRGBb(attr_list, r, g, b, brightness); +} + +// Convert X/Y to RGB and RGBb +// X: 0..65535 +// Y: 0..65535 +// brightness: 0..255 +void Z_Data_Light::toRGBAttributesXYB(Z_attribute_list & attr_list, uint16_t x, uint16_t y, uint8_t brightness) { + uint8_t r,g,b; + XyToRgb(x / 65535.0f, y / 65535.0f, &r, &g, &b); + Z_Data_Light::toRGBAttributesRGBb(attr_list, r, g, b, brightness); +} + +void Z_Data_Light::toRGBAttributes(Z_attribute_list & attr_list) const { + uint8_t brightness = 255; + if (validDimmer()) { + brightness = changeUIntScale(getDimmer(), 0, 254, 0, 255); // range is 0..255 + } + if (validHue() && validSat()) { + uint8_t sat = changeUIntScale(getSat(), 0, 254, 0, 255); + Z_Data_Light::toRGBAttributesHSB(attr_list, getHue(), sat, brightness); + } else if (validX() && validY()) { + Z_Data_Light::toRGBAttributesXYB(attr_list, getX(), getY(), brightness); + } +} + // // Check if this device needs Battery reporting // This is useful for IKEA or Philips devices that tend to drain battery quickly when Battery reporting is set From bd6f6f3ae57d43c31b5a49be48e7147be53b9b31 Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Sun, 17 Jan 2021 18:52:16 +0100 Subject: [PATCH 017/186] force opaque mode --- tasmota/xdrv_13_display.ino | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tasmota/xdrv_13_display.ino b/tasmota/xdrv_13_display.ino index a6e81b711..2eb3c942d 100755 --- a/tasmota/xdrv_13_display.ino +++ b/tasmota/xdrv_13_display.ino @@ -41,7 +41,7 @@ VButton *buttons[MAXBUTTONS]; uint16_t fg_color = 1; uint16_t bg_color = 0; uint8_t color_type = COLOR_BW; -uint8_t auto_draw=1; +uint8_t auto_draw = 1; const uint8_t DISPLAY_MAX_DRIVERS = 16; // Max number of display drivers/models supported by xdsp_interface.ino const uint8_t DISPLAY_MAX_COLS = 64; // Max number of columns allowed with command DisplayCols @@ -1284,6 +1284,8 @@ void DisplayInitDriver(void) if (renderer) { renderer->setTextFont(Settings.display_font); renderer->setTextSize(Settings.display_size); + // force opaque mode + renderer->setDrawMode(0); } From 1c6b0d8a4d45c42adea095a854780f82681d3668 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Mon, 18 Jan 2021 11:33:13 +0100 Subject: [PATCH 018/186] Rebase fixes --- tasmota/xsns_52_ibeacon.ino | 13 ++++----- tasmota/xsns_62_MI_ESP32.ino | 53 +++++++++++++++++------------------- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/tasmota/xsns_52_ibeacon.ino b/tasmota/xsns_52_ibeacon.ino index 8fb924d4e..c31f64850 100755 --- a/tasmota/xsns_52_ibeacon.ino +++ b/tasmota/xsns_52_ibeacon.ino @@ -283,16 +283,13 @@ void ESP32Init() { if (TasmotaGlobal.global_state.wifi_down) { return; } + TasmotaGlobal.wifi_stay_asleep = true; if (WiFi.getSleep() == false) { - if (0 == Settings.flag3.sleep_normal) { - AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: About to restart to put WiFi modem in sleep mode"),"BLE"); - Settings.flag3.sleep_normal = 1; // SetOption60 - Enable normal sleep instead of dynamic sleep - TasmotaGlobal.restart_flag = 2; - } - return; + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Put WiFi modem in sleep mode"),"BLE"); + WiFi.setSleep(true); // Sleep } - AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Initializing Blueetooth..."),"BLE"); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Initializing Bluetooth..."),"BLE"); if (!ESP32BLE.mode.init) { NimBLEDevice::init(""); @@ -1020,4 +1017,4 @@ bool Xsns52(byte function) #endif // USE_IBEACON -#endif \ No newline at end of file +#endif // USE_BLE_ESP32 diff --git a/tasmota/xsns_62_MI_ESP32.ino b/tasmota/xsns_62_MI_ESP32.ino index af24ec083..1a9f75989 100644 --- a/tasmota/xsns_62_MI_ESP32.ino +++ b/tasmota/xsns_62_MI_ESP32.ino @@ -590,17 +590,17 @@ int MI32_decryptPacket(char *_buf, uint16_t _bufSize, uint32_t _type){ MI32_ReverseMAC(packet->MAC); uint8_t _bindkey[16] = {0x0}; bool foundNoKey = true; - AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: search key for MAC: %02x %02x %02x %02x %02x %02x"), packet->MAC[0], packet->MAC[1], packet->MAC[2], packet->MAC[3], packet->MAC[4], packet->MAC[5]); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Search key for MAC: %02x %02x %02x %02x %02x %02x"), packet->MAC[0], packet->MAC[1], packet->MAC[2], packet->MAC[3], packet->MAC[4], packet->MAC[5]); for(uint32_t i=0; iMAC,MIBLEbindKeys[i].MAC,sizeof(packet->MAC))==0){ memcpy(_bindkey,MIBLEbindKeys[i].key,sizeof(_bindkey)); - AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: decryption Key found")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Decryption Key found")); foundNoKey = false; break; } } if(foundNoKey){ - AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: no Key found !!")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: No Key found !!")); return -2; } @@ -620,7 +620,7 @@ int MI32_decryptPacket(char *_buf, uint16_t _bufSize, uint32_t _type){ ret = br_ccm_check_tag(&ctx, &tag); - AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: Err:%i, Decrypted : %02x %02x %02x %02x %02x "), ret, packet->payload[1],packet->payload[2],packet->payload[3],packet->payload[4],packet->payload[5]); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Err:%i, Decrypted : %02x %02x %02x %02x %02x "), ret, packet->payload[1],packet->payload[2],packet->payload[3],packet->payload[4],packet->payload[5]); return ret-1; } #endif // USE_MI_DECRYPTION @@ -661,7 +661,7 @@ uint32_t MIBLEgetSensorSlot(uint8_t (&_MAC)[6], uint16_t _type, uint8_t counter) bool _success = false; for (uint32_t i=0;i19) { - AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: Scan buffer full")); + AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Scan buffer full")); MI32.state.beaconScanCounter = 1; return; } for(auto _scanResult : MIBLEscanResult){ if(memcmp(addr,_scanResult.MAC,6)==0){ - // AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: known device")); + // AddLog_P(LOG_LEVEL_INFO,PSTR("M32: known device")); return; } } @@ -1586,12 +1583,12 @@ void MI32addBeacon(uint8_t index, char* data){ _new.time = 0; if(memcmp(_empty,_new.MAC,6) == 0){ _new.active = false; - AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: beacon%u deactivated"), index); + AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Beacon%u deactivated"), index); } else{ _new.active = true; MI32.mode.activeBeacon = 1; - AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: beacon added with MAC: %s"), _MAC); + AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Beacon added with MAC: %s"), _MAC); } } @@ -1846,7 +1843,7 @@ void CmndMi32Time(void) { if (XdrvMailbox.data_len > 0) { if (MIBLEsensors.size() > XdrvMailbox.payload) { if ((LYWSD02 == MIBLEsensors[XdrvMailbox.payload].type) || (MHOC303 == MIBLEsensors[XdrvMailbox.payload].type)) { - AddLog_P(LOG_LEVEL_DEBUG, PSTR("MI32: will set Time")); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: Will set Time")); MI32.state.sensor = XdrvMailbox.payload; MI32.mode.canScan = 0; MI32.mode.canConnect = 0; @@ -1876,7 +1873,7 @@ void CmndMi32Unit(void) { if (XdrvMailbox.data_len > 0) { if (MIBLEsensors.size() > XdrvMailbox.payload) { if ((LYWSD02 == MIBLEsensors[XdrvMailbox.payload].type) || (MHOC303 == MIBLEsensors[XdrvMailbox.payload].type)) { - AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: will set Unit")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Will set Unit")); MI32.state.sensor = XdrvMailbox.payload; MI32.mode.canScan = 0; MI32.mode.canConnect = 0; @@ -1926,11 +1923,11 @@ void CmndMi32Block(void){ switch (XdrvMailbox.index) { case 0: MIBLEBlockList.clear(); - // AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: size of ilist: %u"), MIBLEBlockList.size()); - ResponseCmndIdxChar(PSTR("block list cleared")); + // AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Size of ilist: %u"), MIBLEBlockList.size()); + ResponseCmndIdxChar(PSTR("Block list cleared")); break; case 1: - ResponseCmndIdxChar(PSTR("show block list")); + ResponseCmndIdxChar(PSTR("Show block list")); break; } } @@ -1956,7 +1953,7 @@ void CmndMi32Block(void){ ResponseCmndIdxChar(XdrvMailbox.data); MI32removeMIBLEsensor(_MACasBytes.buf); } - // AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: size of ilist: %u"), MIBLEBlockList.size()); + // AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Size of ilist: %u"), MIBLEBlockList.size()); break; } } @@ -2323,4 +2320,4 @@ bool Xsns62(uint8_t function) } #endif // USE_MI_ESP32 #endif // ESP32 -#endif \ No newline at end of file +#endif // USE_BLE_ESP32 \ No newline at end of file From d99ba6992c81677e8269dee0653f1e6f16cf6e6e Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Mon, 18 Jan 2021 11:51:24 +0100 Subject: [PATCH 019/186] Refactor BLE --- tasmota/support_command.ino | 11 - tasmota/support_tasmota.ino | 8 + tasmota/xdrv_01_webserver.ino | 18 +- tasmota/xdrv_52_BLE_ESP32.ino | 486 +++++++++++++++++----------------- 4 files changed, 256 insertions(+), 267 deletions(-) diff --git a/tasmota/support_command.ino b/tasmota/support_command.ino index 32180783d..1c5e32474 100644 --- a/tasmota/support_command.ino +++ b/tasmota/support_command.ino @@ -690,12 +690,6 @@ void CmndSleep(void) } -#ifdef USE_BLE_ESP32 - // declare the fn - int ExtStopBLE(); -#endif - - void CmndUpgrade(void) { // Check if the payload is numerically 1, and had no trailing chars. @@ -706,11 +700,6 @@ void CmndUpgrade(void) TasmotaGlobal.ota_state_flag = 3; char stemp1[TOPSZ]; Response_P(PSTR("{\"%s\":\"" D_JSON_VERSION " %s " D_JSON_FROM " %s\"}"), XdrvMailbox.command, TasmotaGlobal.version, GetOtaUrl(stemp1, sizeof(stemp1))); - -#ifdef USE_BLE_ESP32 - ExtStopBLE(); -#endif - } else { Response_P(PSTR("{\"%s\":\"" D_JSON_ONE_OR_GT "\"}"), XdrvMailbox.command, TasmotaGlobal.version); } diff --git a/tasmota/support_tasmota.ino b/tasmota/support_tasmota.ino index e4ae34177..64bc6e09d 100644 --- a/tasmota/support_tasmota.ino +++ b/tasmota/support_tasmota.ino @@ -941,6 +941,11 @@ void Every100mSeconds(void) * Every 0.25 second \*-------------------------------------------------------------------------------------------*/ +#ifdef USE_BLE_ESP32 + // declare the fn + int ExtStopBLE(); +#endif // USE_BLE_ESP32 + void Every250mSeconds(void) { // As the max amount of sleep = 250 mSec this loop should always be taken... @@ -1010,6 +1015,9 @@ void Every250mSeconds(void) SettingsSave(1); // Free flash for OTA update } if (TasmotaGlobal.ota_state_flag <= 0) { +#ifdef USE_BLE_ESP32 + ExtStopBLE(); +#endif // USE_BLE_ESP32 #ifdef USE_COUNTER CounterInterruptDisable(true); // Prevent OTA failures on 100Hz counter interrupts #endif // USE_COUNTER diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index f148a5a6e..42cbf1af4 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -2331,6 +2331,11 @@ void HandleUploadDone(void) { WSContentStop(); } +#ifdef USE_BLE_ESP32 + // declare the fn + int ExtStopBLE(); +#endif + void UploadServices(uint32_t start_service) { if (Web.upload_services_stopped != start_service) { return; } Web.upload_services_stopped = !start_service; @@ -2355,6 +2360,9 @@ void UploadServices(uint32_t start_service) { } else { // AddLog_P(LOG_LEVEL_DEBUG, PSTR("UPL: Services disabled")); +#ifdef USE_BLE_ESP32 + ExtStopBLE(); +#endif #ifdef USE_EMULATION UdpDisconnect(); #endif // USE_EMULATION @@ -2374,11 +2382,6 @@ void UploadServices(uint32_t start_service) { } } -#ifdef USE_BLE_ESP32 - // declare the fn - int ExtStopBLE(); -#endif - void HandleUploadLoop(void) { // Based on ESP8266HTTPUpdateServer.cpp uses ESP8266WebServer Parsing.cpp and Cores Updater.cpp (Update) static uint32_t upload_size; @@ -2414,11 +2417,6 @@ void HandleUploadLoop(void) { } SettingsSave(1); // Free flash for upload -#ifdef USE_BLE_ESP32 - ExtStopBLE(); -#endif - - AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPLOAD D_FILE " %s"), upload.filename.c_str()); if (UPL_SETTINGS == Web.upload_file_type) { diff --git a/tasmota/xdrv_52_BLE_ESP32.ino b/tasmota/xdrv_52_BLE_ESP32.ino index 90739757d..309934d3e 100644 --- a/tasmota/xdrv_52_BLE_ESP32.ino +++ b/tasmota/xdrv_52_BLE_ESP32.ino @@ -28,7 +28,7 @@ BLE functionality to implement specific drivers on top of it. As a generic driver, it can: - Be asked to + Be asked to connect/write to a MAC/Service/Characteristic connect/read from a MAC/Service/Characteristic connect/write/awaitnotify from a MAC/Service/Characteristic/NotifyCharacteristic @@ -51,16 +51,16 @@ Example: Write and request next notify: backlog BLEOp1 001A22092EE0; BLEOp2 3e135142-654f-9090-134a-a6ff5bb77046; BLEOp3 3fa4585a-ce4a-3bad-db4b-b8df8179ea09; BLEOp4 03; BLEOp6 d0e8434d-cd29-0996-af41-6c90f4e0eb2a; -BLEOp10 -> +BLEOp10 -> 19:25:08 RSL: tele/tasmota_E89E98/SENSOR = {"BLEOperation":{"opid":"3,"state":"1,"MAC":"001A22092EE0","svc":"3e135142-654f-9090-134a-a6ff5bb77046","char":"3fa4585a-ce4a-3bad-db4b-b8df8179ea09","wrote":"03}} 19:25:08 queued 0 sent {"BLEOperation":{"opid":"3,"state":"1,"MAC":"001A22092EE0","svc":"3e135142-654f-9090-134a-a6ff5bb77046","char":"3fa4585a-ce4a-3bad-db4b-b8df8179ea09","wrote":"03}} 19:25:08 RSL: stat/tasmota_E89E98/RESULT = {"BLEOp":"Done"} ..... 19:25:11 RSL: tele/tasmota_E89E98/SENSOR = {"BLEOperation":{"opid":"3,"state":"11,"MAC":"001A22092EE0","svc":"3e135142-654f-9090-134a-a6ff5bb77046","char":"3fa4585a-ce4a-3bad-db4b-b8df8179ea09","wrote":"03","notify":"020109000428}} -state: 1 -> starting, +state: 1 -> starting, 7 -> read complete -8 -> write complete +8 -> write complete 11 -> notify complete -ve + -> failure (see GEN_STATE_FAILED_XXXX constants below.) @@ -129,7 +129,7 @@ namespace BLE_ESP32 { // generic sensor type used as during // connect/read/wrtie/notify operations -// only one operation will happen at a time +// only one operation will happen at a time #pragma pack( push, 0 ) // aligned structures for speed. but be sepcific @@ -176,9 +176,9 @@ struct generic_sensor_t { int16_t state; uint32_t opid; // incrementing id so we can find them uint64_t notifytimer; - - // uint8_t cancel; - // uint8_t requestType; + + // uint8_t cancel; + // uint8_t requestType; NimBLEAddress addr; NimBLEUUID serviceUUID; NimBLEUUID characteristicUUID; @@ -208,7 +208,7 @@ struct generic_sensor_t { // structure for callbacks from other drivers from advertisements. struct ble_advertisment_t { BLEAdvertisedDevice *advertisedDevice; // the full NimBLE advertisment, in case people need MORE info. - uint32_t totalCount; + uint32_t totalCount; uint8_t addr[6]; uint8_t addrtype; @@ -239,8 +239,8 @@ gap_addr.addr_type = BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE; //Random p // // callback types to be used by external drivers // -// returns - -// 0 = let others see this, +// returns - +// 0 = let others see this, // 1 = I processed this, no need to give it to the next callback // 2 = I want this device erased from the scan typedef int ADVERTISMENT_CALLBACK(BLE_ESP32::ble_advertisment_t *pStruct); @@ -262,11 +262,11 @@ void registerForScanCallbacks(const char *tag, BLE_ESP32::SCANCOMPLETE_CALLBACK* // BLE operations: these are currently 'new'ed and 'delete'ed. // in the future, they may be allocated from some constant menory store to avoid fragmentation. // so PLEASE don't create or destroy them yourselves. -// create a new BLE operation. +// create a new BLE operation. int newOperation(BLE_ESP32::generic_sensor_t** op); // free a BLE operation - this should be done if you did not call extQueueOperation for some reason int freeOperation(BLE_ESP32::generic_sensor_t** op); -// queue a BLE operation - it will happen some time in the future. +// queue a BLE operation - it will happen some time in the future. // Note: you do not need to free an operation once it have been queued. it will be freed by the driver. int extQueueOperation(BLE_ESP32::generic_sensor_t** op); const char * getStateString(int state); @@ -301,7 +301,7 @@ struct BLE_simple_device_t { -// this protects our queues, which can be accessed by multiple tasks +// this protects our queues, which can be accessed by multiple tasks SemaphoreHandle_t BLEOperationsRecursiveMutex; SemaphoreHandle_t BLEDevicesMutex; @@ -355,7 +355,7 @@ static ble_advertisment_t BLEAdvertisment; ////////////////////////////////////////////////// // general variables for running the driver -TaskHandle_t TasmotaMainTask; +TaskHandle_t TasmotaMainTask; static int BLEMasterEnable = 0; @@ -371,7 +371,7 @@ static BLEScan* ble32Scan = nullptr; bool BLERunning = false; // time we last started a scan in uS using esp_timer_get_time(); // used to setect a scan which did not call back? -uint64_t BLEScanStartedAt = 0; +uint64_t BLEScanStartedAt = 0; uint64_t BLEScanToEndBefore = 0; uint8_t BLEStopScan = 0; uint8_t BLEOtaStallBLE = 0; @@ -450,11 +450,11 @@ static void CmndBLEMaxAge(void); static void CmndBLEAddrFilter(void); void (*const BLE_Commands[])(void) PROGMEM = { - &BLE_ESP32::CmndBLEPeriod, - &BLE_ESP32::CmndBLEAdv, - &BLE_ESP32::CmndBLEOperation, - &BLE_ESP32::CmndBLEMode, - &BLE_ESP32::CmndBLEDetails, + &BLE_ESP32::CmndBLEPeriod, + &BLE_ESP32::CmndBLEAdv, + &BLE_ESP32::CmndBLEOperation, + &BLE_ESP32::CmndBLEMode, + &BLE_ESP32::CmndBLEDetails, &BLE_ESP32::CmndBLEScan, &BLE_ESP32::CmndBLEAlias, &BLE_ESP32::CmndBLEName, @@ -506,7 +506,7 @@ const char * getStateString(int state){ } return PSTR("STATEINVALID"); -} +} /*********************************************************************************************\ * enumerations @@ -516,15 +516,15 @@ enum BLE_Commands { // commands useable in console or rules CMND_BLE_PERIOD, // set period like TELE-period in seconds between read-cycles CMND_BLE_ADV, // change advertisment options at runtime CMND_BLE_OP, // connect/read/write/notify operations - CMND_BLE_MODE, // change mode of ble - BLE_MODES - CMND_BLE_DETAILS, // get details for one device's adverts + CMND_BLE_MODE, // change mode of ble - BLE_MODES + CMND_BLE_DETAILS, // get details for one device's adverts CMND_BLE_SCAN // Scan control }; enum { BLEModeDisabled = 0, // BLE is disabled BLEModeScanByCommand = 1, // BLE is activeated by commands only - BLEModeRegularScan = 2, // BLE is scanning all the time + BLEModeRegularScan = 2, // BLE is scanning all the time } BLE_SCAN_MODES; // values of BLEAdvertMode @@ -582,7 +582,7 @@ int addSeenDevice(const uint8_t *mac, uint8_t addrtype, const char *name, int8_t // do we already know this device? for (int i = 0; i < seenDevices.size(); i++){ if (!memcmp(seenDevices[i]->mac, mac, 6)){ - seenDevices[i]->lastseen = now; + seenDevices[i]->lastseen = now; seenDevices[i]->addrtype = addrtype; seenDevices[i]->RSSI = RSSI; if ((!seenDevices[i]->name[0]) && name[0]){ @@ -598,7 +598,7 @@ int addSeenDevice(const uint8_t *mac, uint8_t addrtype, const char *name, int8_t if (!freeDevices.size()){ int total = seenDevices.size(); if (total < MAX_BLE_DEVICES_LOGGED){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("new seendev slot %d"), total); #endif BLE_ESP32::BLE_simple_device_t* dev = new BLE_ESP32::BLE_simple_device_t; @@ -619,7 +619,7 @@ int addSeenDevice(const uint8_t *mac, uint8_t addrtype, const char *name, int8_t memcpy(dev->mac, mac, 6); strncpy(dev->name, name, sizeof(dev->name)); dev->name[sizeof(dev->name)-1] = 0; - dev->lastseen = now; + dev->lastseen = now; dev->addrtype = addrtype; dev->RSSI = RSSI; dev->maxAge = 1; @@ -639,7 +639,7 @@ int deleteSeenDevices(int ageS = 0){ uint64_t now = esp_timer_get_time(); now = now/1000L; now = now/1000L; - uint32_t nowS = (uint32_t)now; + uint32_t nowS = (uint32_t)now; uint32_t mintime = nowS - ageS; { @@ -649,7 +649,7 @@ int deleteSeenDevices(int ageS = 0){ BLE_ESP32::BLE_simple_device_t* dev = seenDevices[i]; uint64_t lastseen = dev->lastseen/1000L; lastseen = lastseen/1000L; - uint32_t lastseenS = (uint32_t) lastseen; + uint32_t lastseenS = (uint32_t) lastseen; uint32_t del_at = lastseenS + ageS; uint32_t devAge = nowS - lastseenS; if (dev->maxAge < devAge){ @@ -667,10 +667,10 @@ int deleteSeenDevices(int ageS = 0){ dump(addr, 20, dev->mac, 6); const char *alias = getAlias(dev->mac); if (!filter){ - AddLog_P(LOG_LEVEL_INFO,PSTR("delete device %s(%s) by age lastseen %u + maxage %u < now %u."), + AddLog_P(LOG_LEVEL_INFO,PSTR("delete device %s(%s) by age lastseen %u + maxage %u < now %u."), addr, alias, lastseenS, ageS, nowS); } else { - AddLog_P(LOG_LEVEL_INFO,PSTR("delete device %s(%s) by addrtype filter %d > %d."), + AddLog_P(LOG_LEVEL_INFO,PSTR("delete device %s(%s) by addrtype filter %d > %d."), addr, alias, dev->addrtype, BLEAddressFilter); } #endif @@ -681,8 +681,8 @@ int deleteSeenDevices(int ageS = 0){ } } if (res){ -#ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_INFO,PSTR("BLE deleted %d devices"), res); +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE deleted %d devices"), res); #endif } return res; @@ -718,15 +718,15 @@ uint32_t devicePresent(uint8_t *mac){ uint64_t now = esp_timer_get_time(); now = now/1000L; now = now/1000L; - uint32_t nowS = (uint32_t)now; + uint32_t nowS = (uint32_t)now; TasAutoMutex localmutex(&BLEDevicesMutex, "BLEPRes"); for (int i = 0; i < seenDevices.size(); i++){ if (!memcmp(seenDevices[i]->mac, mac, 6)){ uint64_t lastseen = seenDevices[i]->lastseen/1000L; lastseen = lastseen/1000L; - uint32_t lastseenS = (uint32_t) lastseen; - uint32_t ageS = nowS-lastseenS; + uint32_t lastseenS = (uint32_t) lastseen; + uint32_t ageS = nowS-lastseenS; if (!ageS) ageS++; res = ageS; break; @@ -864,7 +864,7 @@ int getSeenDevicesToJson(char *dest, int maxlen){ *(dest++) = 0; int remains = (seenDevices.size() - nextSeenDev); return remains; -} +} @@ -874,7 +874,7 @@ int getSeenDevicesToJson(char *dest, int maxlen){ \*********************************************************************************************/ /* -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG #define MAX_SAFELOG_LEN 40 #define MAX_SAFELOG_COUNT 25 #else @@ -909,7 +909,7 @@ int SafeAddLog_P(uint32_t loglevel, PGM_P formatP, ...) { // if the log would not be output do nothing here. if ((loglevel > Settings.weblog_level) && - (loglevel > TasmotaGlobal.seriallog_level) && + (loglevel > TasmotaGlobal.seriallog_level) && (loglevel > Settings.mqttlog_level) && (loglevel > TasmotaGlobal.syslog_level)){ return added; @@ -942,7 +942,7 @@ int SafeAddLog_P(uint32_t loglevel, PGM_P formatP, ...) { } if (freelogs.size()){ - BLE_ESP32::safelogdata* logdata = (freelogs)[0]; + BLE_ESP32::safelogdata* logdata = (freelogs)[0]; freelogs.pop_front(); logdata->level = loglevel; memcpy(logdata->log_data, BLE_temp_log_data, sizeof(logdata->log_data)); @@ -960,7 +960,7 @@ int SafeAddLog_P(uint32_t loglevel, PGM_P formatP, ...) { BLE_ESP32::safelogdata* GetSafeLog() { xSemaphoreTake(SafeLogMutex, portMAX_DELAY); if (filledlogs.size()){ - BLE_ESP32::safelogdata* logdata = (filledlogs)[0]; + BLE_ESP32::safelogdata* logdata = (filledlogs)[0]; filledlogs.pop_front(); xSemaphoreGive(SafeLogMutex); // release mutex return logdata; @@ -1010,7 +1010,7 @@ char * dump(char *dest, int maxchars, const uint8_t *src, int len){ // convert from a hex string to binary int fromHex(uint8_t *dest, const char *src, int maxlen){ - int srclen = strlen(src)/2; + int srclen = strlen(src)/2; if (srclen > maxlen){ return 0; } @@ -1118,7 +1118,7 @@ void setDetails(ble_advertisment_t *ad){ for (int i = 0; i < svcdataCount; i++){ NimBLEUUID UUID = advertisedDevice->getServiceDataUUID(i);//.getNative()->u16.value; std::string ServiceData = advertisedDevice->getServiceData(i); - + size_t ServiceDataLength = ServiceData.length(); const uint8_t *serviceData = (const uint8_t *)ServiceData.data(); @@ -1144,14 +1144,14 @@ void setDetails(ble_advertisment_t *ad){ p += len; *(p++) = '\"'; maxlen -= len; - } + } } } *(p++) = '}'; maxlen--; *(p++) = '}'; maxlen--; *(p++) = 0; maxlen--; - + BLEAdvertismentDetailsJsonSet = 1; } @@ -1183,17 +1183,17 @@ void postAdvertismentDetails(){ // does not really take any action class BLESensorCallback : public NimBLEClientCallbacks { void onConnect(NimBLEClient* pClient) { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("onConnect %s"), ((std::string)pClient->getPeerAddress()).c_str()); #endif } void onDisconnect(NimBLEClient* pClient) { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("onDisconnect %s"), ((std::string)pClient->getPeerAddress()).c_str()); #endif } bool onConnParamsUpdateRequest(NimBLEClient* pClient, const ble_gap_upd_params* params) { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("onConnParamsUpdateRequest %s"), ((std::string)pClient->getPeerAddress()).c_str()); #endif @@ -1210,11 +1210,11 @@ class BLESensorCallback : public NimBLEClientCallbacks { /* if(params->itvl_min < 24) { // 1.25ms units return false; - } else if(params->itvl_max > 40) { // 1.25ms units + } else if(params->itvl_max > 40) { // 1.25ms units return false; - } else if(params->latency > 2) { // Number of intervals allowed to skip + } else if(params->latency > 2) { // Number of intervals allowed to skip return false; - } else if(params->supervision_timeout > 200) { /// 10ms units + } else if(params->supervision_timeout > 200) { /// 10ms units return false; } @@ -1245,11 +1245,11 @@ class BLEAdvCallbacks: public NimBLEAdvertisedDeviceCallbacks { int8_t RSSI = (char)advertisedDevice->getRSSI(); NimBLEAddress address = advertisedDevice->getAddress(); - BLEAdvertisment.addrtype = address.getType(); + BLEAdvertisment.addrtype = address.getType(); memcpy(BLEAdvertisment.addr, address.getNative(), 6); ReverseMAC(BLEAdvertisment.addr); - + BLEAdvertisment.RSSI = RSSI; char addrstr[20]; @@ -1285,7 +1285,7 @@ class BLEAdvCallbacks: public NimBLEAdvertisedDeviceCallbacks { } } break; case 3:{ // all adverts for ALL DEVICES - may not get them all - // ignore from here on if filtered on addrtype + // ignore from here on if filtered on addrtype if (BLEAdvertisment.addrtype > BLEAddressFilter){ return; } @@ -1314,7 +1314,7 @@ class BLEAdvCallbacks: public NimBLEAdvertisedDeviceCallbacks { //BLEScan->erase(address); } } catch(const std::exception& e){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in advertismentCallbacks")); #endif } @@ -1333,18 +1333,18 @@ static BLESensorCallback BLESensorCB; static void BLEscanEndedCB(NimBLEScanResults results){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Scan ended")); #endif for (int i = 0; i < scancompleteCallbacks.size(); i++){ try { SCANCOMPLETE_CALLBACK *pFn = scancompleteCallbacks[i]; int callbackres = pFn(results); -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("scancompleteCallbacks %d %d"), i, callbackres); #endif } catch(const std::exception& e){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in operationsCallbacks")); #endif } @@ -1366,21 +1366,21 @@ static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, ui NimBLEClient *pRClient; if (!pRemoteCharacteristic){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG,PSTR("Notify: no remote char!!??")); #endif return; } -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Notified length: %u"),length); #endif // find the operation this is associated with NimBLERemoteService *pSvc = pRemoteCharacteristic->getRemoteService(); if (!pSvc){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("Notify: no remote service found")); #endif return; @@ -1388,14 +1388,14 @@ static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, ui pRClient = pSvc->getClient(); if (!pRClient){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("Notify: no remote client!!??")); #endif return; } NimBLEAddress devaddr = pRClient->getPeerAddress(); - generic_sensor_t *thisop = nullptr; + generic_sensor_t *thisop = nullptr; { // make sure we are not disturbed TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLENotif"); @@ -1403,7 +1403,7 @@ static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, ui for (int i = 0; i < currentOperations.size(); i++){ generic_sensor_t *op = currentOperations[i]; if (!op){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("Notify: null op in currentOperations!!??")); #endif } else { @@ -1419,7 +1419,7 @@ static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, ui //pRemoteCharacteristic->unsubscribe(); if (!thisop){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG,PSTR("no op for notify")); #endif return; @@ -1451,23 +1451,23 @@ static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, ui \*********************************************************************************************/ void registerForAdvertismentCallbacks(const char *tag, BLE_ESP32::ADVERTISMENT_CALLBACK* pFn){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: registerForAdvertismentCallbacks %s:%x"), tag, (uint32_t) pFn); #endif advertismentCallbacks.push_back(pFn); } void registerForOpCallbacks(const char *tag, BLE_ESP32::OPCOMPLETE_CALLBACK* pFn){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: registerForOpCallbacks %s:%x"), tag, (uint32_t) pFn); #endif operationsCallbacks.push_back(pFn); } void registerForScanCallbacks(const char *tag, BLE_ESP32::SCANCOMPLETE_CALLBACK* pFn){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: registerForScnCallbacks %s:%x"), tag, (uint32_t) pFn); -#endif +#endif scancompleteCallbacks.push_back(pFn); } @@ -1487,18 +1487,12 @@ static void BLEInit(void) { if (BLEInitState) { return; } if (TasmotaGlobal.global_state.wifi_down) { return; } - if (WiFi.getSleep() == false) { -#ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: WiFi modem not in sleep mode, BLE cannot start yet")); -#endif - if (0 == Settings.flag3.sleep_normal) { - AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: About to restart to put WiFi modem in sleep mode")); - Settings.flag3.sleep_normal = 1; // SetOption60 - Enable normal sleep instead of dynamic sleep - TasmotaGlobal.restart_flag = 2; - } - return; - } + TasmotaGlobal.wifi_stay_asleep = true; + if (WiFi.getSleep() == false) { + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Put WiFi modem in sleep mode"),"BLE"); + WiFi.setSleep(true); // Sleep + } // this is only for testing, does nothin if examples are undefed installExamples(); @@ -1529,9 +1523,9 @@ static void BLEOperationTask(void *pvParameters); static void BLEStartOperationTask(){ if (BLERunning == false){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Start operations"),D_CMND_BLE); -#endif +#endif BLERunning = true; xTaskCreatePinnedToCore( @@ -1559,20 +1553,20 @@ static void BLETaskStopStartNimBLE(NimBLEClient **ppClient, bool start = true){ try { if ((*ppClient)->isConnected()){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_INFO,PSTR("disconnecting connected client")); -#endif +#endif (*ppClient)->disconnect(); } NimBLEDevice::deleteClient((*ppClient)); (*ppClient) = nullptr; -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_INFO,PSTR("deleted client")); -#endif +#endif } catch(const std::exception& e){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("Stopping NimBLE:exception in delete client")); -#endif +#endif } if (ble32Scan){ @@ -1593,10 +1587,10 @@ static void BLETaskStopStartNimBLE(NimBLEClient **ppClient, bool start = true){ *ppClient = NimBLEDevice::createClient(); (*ppClient)->setClientCallbacks(&clientCB, false); - /** Set initial connection parameters: These settings are 15ms interval, 0 latency, 120ms timout. - * These settings are safe for 3 clients to connect reliably, can go faster if you have less + /** Set initial connection parameters: These settings are 15ms interval, 0 latency, 120ms timout. + * These settings are safe for 3 clients to connect reliably, can go faster if you have less * connections. Timeout should be a multiple of the interval, minimum is 100ms. - * Min interval: 12 * 1.25ms = 15, Max interval: 12 * 1.25ms = 15, 0 latency, 51 * 10ms = 510ms timeout + * Min interval: 12 * 1.25ms = 15, Max interval: 12 * 1.25ms = 15, 0 latency, 51 * 10ms = 510ms timeout */ (*ppClient)->setConnectionParams(12,12,0,51); /** Set how long we are willing to wait for the connection to complete (seconds), default is 30. */ @@ -1621,16 +1615,16 @@ int BLETaskStartScan(int time){ if (BLERunningScan) { // if we hit 2, wait one more time before starting if (BLERunningScan == 2){ - // wait 100ms + // wait 100ms vTaskDelay(100/ portTICK_PERIOD_MS); BLERunningScan = 0; } return -2; } -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: Startscan")); -#endif +#endif //vTaskDelay(500/ portTICK_PERIOD_MS); ble32Scan->setActiveScan(BLEScanActiveMode ? 1: 0); @@ -1657,13 +1651,13 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe if (!*pCurrentOperation) { *pCurrentOperation = nextOperation(&queuedOperations); if (*pCurrentOperation){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: new currentOperation")); #endif BLEOpCount++; generic_sensor_t* temp = *pCurrentOperation; //this will null it out, so save and restore. - addOperation(¤tOperations, pCurrentOperation); + addOperation(¤tOperations, pCurrentOperation); *pCurrentOperation = temp; } } @@ -1678,9 +1672,9 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe uint64_t diff = now - (*pCurrentOperation)->notifytimer; diff = diff/1000; if (diff > 20000){ // 20s -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: notify timeout")); -#endif +#endif (*pCurrentOperation)->state = GEN_STATE_FAILED_NOTIFYTIMEOUT; (*pCurrentOperation)->notifytimer = 0; } @@ -1693,11 +1687,11 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe case GEN_STATE_WAITINDICATE: case GEN_STATE_WAITNOTIFY: //(*pCurrentOperation)->notifytimer == 0 at this point, so must be done - (*pCurrentOperation)->state = GEN_STATE_NOTIFIED; + (*pCurrentOperation)->state = GEN_STATE_NOTIFIED; // just stay here until this is removed by the main thread -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: notify operation complete")); -#endif +#endif BLE_ESP32::BLETaskRunTaskDoneOperation(pCurrentOperation, ppClient); pClient = *ppClient; return; @@ -1706,9 +1700,9 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe case GEN_STATE_WRITEDONE: case GEN_STATE_NOTIFIED: // - may have completed DURING our read/write to get here // just stay here until this is removed by the main thread -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: operation complete")); -#endif +#endif BLE_ESP32::BLETaskRunTaskDoneOperation(pCurrentOperation, ppClient); pClient = *ppClient; return; @@ -1726,9 +1720,9 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe if (!*pCurrentOperation) return; if ((*pCurrentOperation)->state <= GEN_STATE_FAILED){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("BLETask: op failed %d"), (*pCurrentOperation)->state); -#endif +#endif BLE_ESP32::BLETaskRunTaskDoneOperation(pCurrentOperation, ppClient); pClient = *ppClient; return; @@ -1740,16 +1734,16 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe if (pClient->isConnected()){ // don't do anything if we are still connected -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: still connected")); -#endif +#endif return; } // if we managed to run operations back to back with long connection timeouts, // then we may NOT see advertisements. - // so to prevent triggering of the advert timeout restart mechanism, + // so to prevent triggering of the advert timeout restart mechanism, // set the last advert time each time we start an operation uint64_t now = esp_timer_get_time(); BLEScanLastAdvertismentAt = now; // initialise the time of the last advertisment @@ -1760,9 +1754,9 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe int newstate = GEN_STATE_STARTED; op->state = GEN_STATE_STARTED; -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: attempt connect %s"), ((std::string)op->addr).c_str()); -#endif +#endif if (!op->serviceUUID.bitSize()){ op->state = GEN_STATE_FAILED_NOSERVICE; @@ -1771,16 +1765,16 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe if (pClient->connect(op->addr, true)) { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("connected %s -> getservice"), ((std::string)op->addr).c_str()); -#endif +#endif NimBLERemoteService *pService = pClient->getService(op->serviceUUID); int waitNotify = false; int notifystate = 0; op->notifytimer = 0L; if (pService != nullptr) { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got service")); #endif // pre-set to fail if no operations requested @@ -1794,16 +1788,16 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe // if we have been asked to get a notification if (op->notificationCharacteristicUUID.bitSize()) { - NimBLERemoteCharacteristic *pNCharacteristic = + NimBLERemoteCharacteristic *pNCharacteristic = pService->getCharacteristic(op->notificationCharacteristicUUID); if (pNCharacteristic != nullptr) { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got notify characteristic")); -#endif +#endif op->notifylen = 0; if(pNCharacteristic->canNotify()) { if(pNCharacteristic->subscribe(true, BLE_ESP32::BLEGenNotifyCB)) { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("subscribe for notify")); #endif uint64_t now = esp_timer_get_time(); @@ -1813,39 +1807,39 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe notifystate = GEN_STATE_WAITNOTIFY; waitNotify = true; } else { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("failed subscribe for notify")); -#endif +#endif newstate = GEN_STATE_FAILED_NOTIFY; } } else { if(pNCharacteristic->canIndicate()) { if(pNCharacteristic->subscribe(false, BLE_ESP32::BLEGenNotifyCB)) { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG,PSTR("subscribe for indicate")); -#endif +#endif notifystate = GEN_STATE_WAITINDICATE; uint64_t now = esp_timer_get_time(); op->notifytimer = now; waitNotify = true; } else { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("failed subscribe for indicate")); -#endif +#endif newstate = GEN_STATE_FAILED_INDICATE; } } else { newstate = GEN_STATE_FAILED_CANTNOTIFYORINDICATE; -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("characteristic can't notify")); -#endif +#endif } } } else { newstate = GEN_STATE_FAILED_NONOTIFYCHAR; -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("notify characteristic not found")); -#endif +#endif } // force the 'error' of the notify coming in before the read/write for testing @@ -1860,18 +1854,18 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe pCharacteristic = pService->getCharacteristic(op->characteristicUUID); if (pCharacteristic != nullptr) { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got read/write characteristic")); -#endif +#endif newstate = GEN_STATE_FAILED_NOREADWRITE; // overwritten on failure if (op->readlen){ if(pCharacteristic->canRead()) { std::string value = pCharacteristic->readValue(); op->readlen = value.length(); - memcpy(op->dataRead, value.data(), + memcpy(op->dataRead, value.data(), (op->readlen > sizeof(op->dataRead))? - sizeof(op->dataRead): + sizeof(op->dataRead): op->readlen); if (op->readlen > sizeof(op->dataRead)){ op->readtruncated = 1; @@ -1880,14 +1874,14 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe } if (op->readmodifywritecallback){ READ_CALLBACK *pFn = (READ_CALLBACK *)op->readmodifywritecallback; -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("read characteristic with readmodifywritecallback")); -#endif +#endif pFn(op); } else { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("read characteristic")); -#endif +#endif } // only change it to a 'finished' state if we really are @@ -1901,32 +1895,32 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe if(pCharacteristic->canWrite()) { if (!pCharacteristic->writeValue(op->dataToWrite, op->writelen, true)){ newstate = GEN_STATE_FAILED_WRITE; -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG,PSTR("characteristic write fail")); -#endif +#endif } else { if (!waitNotify) newstate = GEN_STATE_WRITEDONE; -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("write characteristic")); -#endif +#endif } } else { newstate = GEN_STATE_FAILED_CANTWRITE; } } // print or do whatever you need with the value - + } else { newstate = GEN_STATE_FAILED_NO_RW_CHAR; -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG,PSTR("r/w characteristic not found")); -#endif +#endif } } } - // disconnect if not waiting for notify, + // disconnect if not waiting for notify, if (!op->notifytimer){ if (waitNotify){ vTaskDelay(50/ portTICK_PERIOD_MS); @@ -1935,32 +1929,32 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe } } else { newstate = notifystate; - } + } } else { newstate = GEN_STATE_FAILED_NOSERVICE; // failed to get a service -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG,PSTR("failed - svc not on device?")); -#endif - } +#endif + } } else { // connect itself failed newstate = GEN_STATE_FAILED_CONNECT; -#ifdef NIMBLE_CLIENT_HAS_GETRESULT +#ifdef NIMBLE_CLIENT_HAS_GETRESULT int rc = pClient->getResult(); switch (rc){ case (0x0200+BLE_ERR_CONN_LIMIT ): -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("Hit connection limit? - restarting NimBLE")); -#endif +#endif BLERestartNimBLE = 1; BLERestartBLEReason = BLE_RESTART_BLE_REASON_CONN_LIMIT; break; case (0x0200+BLE_ERR_ACL_CONN_EXISTS): -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("Connection exists? - restarting NimBLE")); -#endif +#endif BLERestartNimBLE = 1; BLERestartBLEReason = BLE_RESTART_BLE_REASON_CONN_EXISTS; break; @@ -1968,10 +1962,10 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe #endif // failed to connect -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG,PSTR("failed to connect to device %d"), rc); -#endif - } +#endif + } op->state = newstate; } @@ -1984,9 +1978,9 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLEClient **ppClient){ try { if ((*ppClient)->isConnected()){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("runTaskDoneOperation: disconnecting connected client")); -#endif +#endif (*ppClient)->disconnect(); // wait for 1/2 second after disconnect int waits = 0; @@ -1994,19 +1988,19 @@ static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLE vTaskDelay(500/ portTICK_PERIOD_MS); if (waits) { //(*ppClient)->disconnect(); - // we will stall here forever!!! - as testing -#ifdef BLE_ESP32_DEBUG + // we will stall here forever!!! - as testing +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE wait discon%d"), waits); -#endif +#endif vTaskDelay(500/ portTICK_PERIOD_MS); } waits++; if (waits == 5){ int conn_id = (*ppClient)->getConnId(); ble_gap_conn_broken(conn_id, -1); -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE wait discon%d - kill connection"), waits); -#endif +#endif } if (waits == 60){ AddLog_P(LOG_LEVEL_ERROR,PSTR(">60s waiting -> BLE Failed, restart Tasmota %d"), waits); @@ -2020,9 +2014,9 @@ static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLE } while ((*ppClient)->isConnected()); } } catch(const std::exception& e){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("runTaskDoneOperation: exception in disconnect")); -#endif +#endif } @@ -2040,9 +2034,9 @@ static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLE // by adding it to this list, this will cause it to be sent to MQTT -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("runTaskDoneOperation: add to completedOperations")); -#endif +#endif addOperation(&completedOperations, op); return; } @@ -2115,9 +2109,9 @@ static void BLEOperationTask(void *pvParameters){ // wait 1/10 second vTaskDelay(100/ portTICK_PERIOD_MS); -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLEOperationTask: Left task")); -#endif +#endif deleteSeenDevices(); BLEStop = 2; @@ -2131,7 +2125,7 @@ static void BLEOperationTask(void *pvParameters){ /***********************************************************************\ * Regular Tasmota called functions - * + * \***********************************************************************/ void BLEEvery50mSecond(){ /* if (BLEAliasListTrigger){ @@ -2178,7 +2172,7 @@ static void BLEEverySecond(bool restart){ BLE_ESP32::BLEPostMQTT(true); // send only completed } - // request send of ALL oeprations prepped, queued, in progress - + // request send of ALL oeprations prepped, queued, in progress - // in separate MQTT messages. if (BLEPostMQTTTrigger){ BLEPostMQTTTrigger = 0; @@ -2237,7 +2231,7 @@ generic_sensor_t* nextOperation(std::deque *ops){ generic_sensor_t* op = nullptr; if (ops->size()){ TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLENExtOp"); - op = (*ops)[0]; + op = (*ops)[0]; ops->pop_front(); } return op; @@ -2253,7 +2247,7 @@ int addOperation(std::deque *ops, generic_sensor_t** op){ ops->push_back(*op); *op = nullptr; res = 1; - } + } } if (res){ //AddLog_P(LOG_LEVEL_DEBUG,PSTR("added operation")); @@ -2394,7 +2388,7 @@ int getAddr(uint8_t *dest, char *src){ if (strlen(src) == 12+5){ strcpy(tmp, src); stripColon(tmp); - src = tmp; + src = tmp; } int len = fromHex(dest, src, 6); @@ -2408,7 +2402,7 @@ int getAddr(uint8_t *dest, char *src){ static const char *noAlias = PSTR(""); //////////////////////////////////////////// -// use to display the alias name if required +// use to display the alias name if required const char *getAlias(uint8_t *addr){ if (!addr){ return noAlias; @@ -2454,7 +2448,7 @@ static int StopBLE(void){ AddLog_P(LOG_LEVEL_ERROR,PSTR("StopBLE - was not running")); return 1; } -} +} /*********************************************************************************************\ @@ -2605,7 +2599,7 @@ void CmndBLEMode(void){ case BLEModeScanByCommand:{ uint64_t now = esp_timer_get_time(); switch(BLEMode){ - // when changing from regular to by command, + // when changing from regular to by command, // stop the scan next loop case BLEModeRegularScan: { BLEMode = BLEModeScanByCommand; @@ -2655,10 +2649,10 @@ void CmndBLEMode(void){ // BLEDetails1 -> send me details for once // BLEDetails2 -> send me details for every advert if possible // example: BLEDetails1 001A22092C9A -// details look like: +// details look like: // MQT: tele/tasmota_esp32/BLE = {"details":{"mac":"001A22092C9A","p":"0C0943432D52542D4D2D424C450CFF0000000000000000000000"}} -// and incliude mac, complete advert payload, plus optional ,"lost":true if an advert was not captured because MQTT we already -// had one waiting to be sent +// and incliude mac, complete advert payload, plus optional ,"lost":true if an advert was not captured because MQTT we already +// had one waiting to be sent void CmndBLEDetails(void){ switch(XdrvMailbox.index){ case 0: @@ -2853,7 +2847,7 @@ void CmndBLEName(void) { // BLEop0/1/2 will cause an MQTT send of ops currently known. // on op complete/op fail, a MQTT send is triggered of all known ops, and the completed/failed op removed. -// example: +// example: // BLEOp1 M:001A22092CDB s:3e135142-654f-9090-134a-a6ff5bb77046 c:3fa4585a-ce4a-3bad-db4b-b8df8179ea09 w:03 n:d0e8434d-cd29-0996-af41-6c90f4e0eb2a go // requests write of 03, and request wait for notify. @@ -2869,9 +2863,9 @@ void CmndBLEOperation(void){ // if in progress, only op 0 or 11 are allowed switch(op) { case 0: -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("preview")); -#endif +#endif BLEPostMQTTTrigger = 1; break; case 1: { @@ -2880,9 +2874,9 @@ void CmndBLEOperation(void){ } int opres = BLE_ESP32::newOperation(&prepOperation); if (!opres){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not create new operation")); -#endif +#endif ResponseCmndChar("FailCreate"); return; } @@ -2935,19 +2929,19 @@ void CmndBLEOperation(void){ int u = (int)prepOperation->context; int opres = BLE_ESP32::extQueueOperation(&prepOperation); if (!opres){ - // NOTE: prepOperation will NOT have been deleted. + // NOTE: prepOperation will NOT have been deleted. // this means you could retry with another BLEOp10. // it WOULD be deleted if you sent another BELOP1 -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not queue new operation")); -#endif +#endif ResponseCmndChar("FailQueue"); return; } else { // NOTE: prepOperation has been set to null if we queued sucessfully. -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Operations queued:%d"), queuedOperations.size()); -#endif +#endif char temp[40]; sprintf(temp, "{\"opid\":%d,\"u\":%d}", lastopid-1, u); Response_P(S_JSON_COMMAND_XVALUE, XdrvMailbox.command, temp); @@ -2965,24 +2959,24 @@ void CmndBLEOperation(void){ case 2: { if (!prepOperation) { ResponseCmndChar("FailNoOp"); - return; + return; } //prepOperation->requestType = atoi(XdrvMailbox.data); int u = (int)prepOperation->context; int opres = BLE_ESP32::extQueueOperation(&prepOperation); if (!opres){ - // NOTE: prepOperation will NOT have been deleted. + // NOTE: prepOperation will NOT have been deleted. // this means you could retry with another BLEOp10. // it WOULD be deleted if you sent another BELOP1 -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not queue new operation")); -#endif +#endif ResponseCmndChar("FailQueue"); } else { // NOTE: prepOperation has been set to null if we queued sucessfully. -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Operations queued:%d"), queuedOperations.size()); -#endif +#endif char temp[40]; sprintf(temp, "{\"opid\":%d,\"u\":%d}", lastopid-1, u); Response_P(S_JSON_COMMAND_XVALUE, XdrvMailbox.command, temp); @@ -3031,22 +3025,22 @@ static void BLEPostMQTT(bool onlycompleted) { if (prepOperation || completedOperations.size() || queuedOperations.size() || currentOperations.size()){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("some to show")); #endif if (prepOperation && !onlycompleted){ std::string out = BLETriggerResponse(prepOperation); snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("prep sent %s"), out.c_str()); -#endif +#endif } if (queuedOperations.size() && !onlycompleted){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("queued %d"), queuedOperations.size()); -#endif +#endif for (int i = 0; i < queuedOperations.size(); i++){ TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEPost1"); @@ -3058,18 +3052,18 @@ static void BLEPostMQTT(bool onlycompleted) { localmutex.give(); snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("queued %d sent %s"), i, out.c_str()); -#endif +#endif //break; } } } if (currentOperations.size() && !onlycompleted){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("current %d"), currentOperations.size()); -#endif +#endif for (int i = 0; i < currentOperations.size(); i++){ TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEPost2"); generic_sensor_t *toSend = currentOperations[i]; @@ -3080,31 +3074,31 @@ static void BLEPostMQTT(bool onlycompleted) { localmutex.give(); snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("curr %d sent %s"), i, out.c_str()); -#endif +#endif //break; } } } if (completedOperations.size()){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("completed %d"), completedOperations.size()); -#endif +#endif do { generic_sensor_t *toSend = nextOperation(&completedOperations); if (!toSend) { break; // break from while loop } else { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE:completedOperation removed")); -#endif +#endif std::string out = BLETriggerResponse(toSend); snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); // we alreayd removed this from the queues, so now delete - delete toSend; + delete toSend; //break; } //break; @@ -3174,13 +3168,13 @@ static void mainThreadOpCallbacks() { try { OPCOMPLETE_CALLBACK *pFn = (OPCOMPLETE_CALLBACK *)(op->completecallback); callbackres = pFn(op); -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("op->completecallback %d"), callbackres); -#endif +#endif } catch(const std::exception& e){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in op->completecallback")); -#endif +#endif } } @@ -3189,25 +3183,25 @@ static void mainThreadOpCallbacks() { try { OPCOMPLETE_CALLBACK *pFn = operationsCallbacks[i]; callbackres = pFn(op); -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("operationsCallbacks %d %d"), i, callbackres); -#endif +#endif if (callbackres){ break; // this callback ate the op. } } catch(const std::exception& e){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in operationsCallbacks")); -#endif +#endif } } } // if some callback told us not to send on MQTT, then remove from completed and delete the data if (callbackres){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("callbackres true -> delete op")); -#endif +#endif completedOperations.erase(completedOperations.begin() + i); delete op; } @@ -3219,9 +3213,9 @@ static void mainThreadOpCallbacks() { static void BLEShow(bool json) { if (json){ -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("show json %d"),json); -#endif +#endif uint32_t totalCount = BLEAdvertisment.totalCount; uint32_t deviceCount = seenDevices.size(); @@ -3232,7 +3226,7 @@ static void BLEShow(bool json) //WSContentSend_PD(HTTP_MI32, i+1,stemp,MIBLEsensors.size()); } #endif // USE_WEBSERVER - + } /*void BLEAliasMqttList(){ @@ -3267,9 +3261,9 @@ static void BLEDiag() { uint32_t totalCount = BLEAdvertisment.totalCount; uint32_t deviceCount = seenDevices.size(); -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE:scans:%u,advertisements:%u,devices:%u,resets:%u,BLEStop:%d,BLERunning:%d,BLERunningScan:%d,BLELoopCount:%u,BLEOpCount:%u"), BLEScanCount, totalCount, deviceCount, BLEResets, BLEStop, BLERunning, BLERunningScan, BLELoopCount, BLEOpCount); -#endif +#endif } /** @@ -3290,7 +3284,7 @@ std::string BLETriggerResponse(generic_sensor_t *toSend){ out = out + temp; out = out + "\",\"state\":\""; out = out + getStateString(toSend->state); - + if (toSend->addr != NimBLEAddress()){ out = out + "\",\"MAC\":\""; uint8_t addrrev[6]; @@ -3377,40 +3371,40 @@ const char HTTP_BLE_DEV_END[] PROGMEM = void HandleBleConfiguration(void) { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG, PSTR("HandleBleConfiguration")); #endif - if (!HttpCheckPriviledgedAccess()) { -#ifdef BLE_ESP32_DEBUG + if (!HttpCheckPriviledgedAccess()) { +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG, PSTR("!HttpCheckPriviledgedAccess()")); -#endif - return; +#endif + return; } -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_CONFIGURE_BLE)); #endif char tmp[20]; WebGetArg("en", tmp, sizeof(tmp)); -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("arg en is %s"), tmp); #endif if (Webserver->hasArg("save")) { -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE SETTINGS SAVE")); -#endif - Settings.flag5.mi32_enable = Webserver->hasArg("e0"); // - BLEScanActiveMode = (Webserver->hasArg("e1")?1:0); // +#endif + Settings.flag5.mi32_enable = Webserver->hasArg("e0"); // + BLEScanActiveMode = (Webserver->hasArg("e1")?1:0); // SettingsSaveAll(); HandleConfiguration(); return; } -#ifdef BLE_ESP32_DEBUG +#ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG, PSTR("!SAVE")); #endif char str[TOPSZ]; @@ -3418,7 +3412,7 @@ void HandleBleConfiguration(void) WSContentStart_P(PSTR(D_CONFIGURE_BLE)); WSContentSendStyle_P(HTTP_BLE_DEV_STYLE); //WSContentSendStyle(); - WSContentSend_P(HTTP_FORM_BLE, + WSContentSend_P(HTTP_FORM_BLE, (Settings.flag5.mi32_enable) ? " checked" : "", (BLEScanActiveMode) ? " checked" : "" ); @@ -3433,7 +3427,7 @@ void HandleBleConfiguration(void) uint64_t now = esp_timer_get_time(); now = now/1000L; now = now/1000L; - uint32_t nowS = (uint32_t)now; + uint32_t nowS = (uint32_t)now; for (int i = 0; i < number; i++){ BLE_ESP32::BLE_simple_device_t* dev = seenDevices[i]; @@ -3443,8 +3437,8 @@ void HandleBleConfiguration(void) const char *alias = getAlias(dev->mac); uint64_t lastseen = dev->lastseen/1000L; lastseen = lastseen/1000L; - uint32_t lastseenS = (uint32_t) lastseen; - uint32_t ageS = nowS-lastseenS; + uint32_t lastseenS = (uint32_t) lastseen; + uint32_t ageS = nowS-lastseenS; WSContentSend_P(HTTP_BLE_DEV, addr, addrtype, alias, dev->name, dev->RSSI, ageS, dev->maxAge); } @@ -3508,7 +3502,7 @@ bool Xdrv52(uint8_t function) case FUNC_AFTER_TELEPERIOD: BLE_ESP32::BLEPublishDevices = 1; // mqtt publish as 'TELE' break; - + #ifdef USE_WEBSERVER case FUNC_WEB_ADD_BUTTON: WSContentSend_P(BLE_ESP32::HTTP_BTN_MENU_BLE); @@ -3607,7 +3601,7 @@ void sendExample(){ -#endif +#endif #endif // ESP32 From 96eac4a54723a86adaf876f4b475683384dac6c6 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Mon, 18 Jan 2021 16:10:17 +0100 Subject: [PATCH 020/186] Use solo1-release_v3.3-7e63061fa --- platformio_override_sample.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini index 4db6a41b3..deff8272a 100644 --- a/platformio_override_sample.ini +++ b/platformio_override_sample.ini @@ -165,7 +165,7 @@ lib_extra_dirs = ; *** EXPERIMENTAL Tasmota version for ESP32solo1 (used in some Xiaomi devices) [env:tasmota32solo1] extends = env:tasmota32 -platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/esp32-arduino-lib-builder/raw/framework-arduinoespressif32/framework-arduinoespressif32-release_v3.3-solo1-4b325f52e.tar.gz +platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/esp32-arduino-lib-builder/raw/framework-arduinoespressif32/framework-arduinoespressif32-solo1-release_v3.3-7e63061fa.tar.gz platformio/tool-mklittlefs @ ~1.203.200522 platformio/tool-esptoolpy @ ~1.30000.0 build_unflags = ${esp32_defaults.build_unflags} From ff08501b396c0e5ab5c9b51db4640bea08dd4ff2 Mon Sep 17 00:00:00 2001 From: polarduck-dev Date: Mon, 18 Jan 2021 15:32:58 +0000 Subject: [PATCH 021/186] fixed IPv6 address acquisition --- platformio.ini | 2 +- tasmota/my_user_config.h | 8 ++-- tasmota/settings.h | 2 +- tasmota/settings.ino | 8 ++-- tasmota/support.ino | 8 ++-- tasmota/support_command.ino | 14 +++---- tasmota/support_device_groups.ino | 15 ++----- tasmota/support_wifi.ino | 57 +++++++++------------------ tasmota/user_config_override_sample.h | 12 +++--- tasmota/xdrv_01_webserver.ino | 6 +-- tasmota/xdrv_10_scripter.ino | 9 +---- tasmota/xdrv_82_ethernet.ino | 6 +-- 12 files changed, 56 insertions(+), 91 deletions(-) diff --git a/platformio.ini b/platformio.ini index 12ccfcc90..6de5f5f55 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,7 +133,7 @@ build_flags = ${esp_defaults.build_flags} -DBEARSSL_SSL_BASIC ; NONOSDK22x_190703 = 2.2.2-dev(38a443e) -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 - -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH + -DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_HIGHER_BANDWIDTH ; VTABLES in Flash -DVTABLES_IN_FLASH ; remove the 4-bytes alignment for PSTR() diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 8cf0316f8..b66d04a31 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -65,10 +65,10 @@ #define BOOT_LOOP_OFFSET 1 // [SetOption36] Number of boot loops before starting restoring defaults (0 = disable, 1..200 = boot loops offset) // -- Wifi ---------------------------------------- -#define WIFI_IP_ADDRESS "0.0.0.0" // [IpAddress1] Set to 0.0.0.0 for using DHCP or enter a static IP address -#define WIFI_GATEWAY "192.168.1.1" // [IpAddress2] If not using DHCP set Gateway IP address -#define WIFI_SUBNETMASK "255.255.255.0" // [IpAddress3] If not using DHCP set Network mask -#define WIFI_DNS "192.168.1.1" // [IpAddress4] If not using DHCP set DNS IP address (might be equal to WIFI_GATEWAY) +#define WIFI_IPV4_ADDRESS "0.0.0.0" // [IpAddress1] Set to 0.0.0.0 for using DHCP or enter a static IP address +#define WIFI_IPV4_GATEWAY "192.168.1.1" // [IpAddress2] If not using DHCP set Gateway IP address +#define WIFI_IPV4_SUBNETMASK "255.255.255.0" // [IpAddress3] If not using DHCP set Network mask +#define WIFI_IPV4_DNS "192.168.1.1" // [IpAddress4] If not using DHCP set DNS IP address (might be equal to WIFI_GATEWAY) #define STA_SSID1 "" // [Ssid1] Wifi SSID #define STA_PASS1 "" // [Password1] Wifi password diff --git a/tasmota/settings.h b/tasmota/settings.h index 32d8b8871..5a5317673 100644 --- a/tasmota/settings.h +++ b/tasmota/settings.h @@ -497,7 +497,7 @@ struct { uint8_t ina219_mode; // 531 uint16_t pulse_timer[MAX_PULSETIMERS]; // 532 uint16_t button_debounce; // 542 - uint32_t ip_address[4]; // 544 + uint32_t ipv4_address[4]; // 544 unsigned long energy_kWhtotal; // 554 char ex_mqtt_fulltopic[100]; // 558 Free since 8.0.0.1 diff --git a/tasmota/settings.ino b/tasmota/settings.ino index b2d438df6..5421e7c8a 100644 --- a/tasmota/settings.ino +++ b/tasmota/settings.ino @@ -812,10 +812,10 @@ void SettingsDefaultSet2(void) flag3.use_wifi_rescan |= WIFI_SCAN_REGULARLY; Settings.wifi_output_power = 170; Settings.param[P_ARP_GRATUITOUS] = WIFI_ARP_INTERVAL; - ParseIp(&Settings.ip_address[0], WIFI_IP_ADDRESS); - ParseIp(&Settings.ip_address[1], WIFI_GATEWAY); - ParseIp(&Settings.ip_address[2], WIFI_SUBNETMASK); - ParseIp(&Settings.ip_address[3], WIFI_DNS); + ParseIPv4(&Settings.ipv4_address[0], WIFI_IPV4_ADDRESS); + ParseIPv4(&Settings.ipv4_address[1], WIFI_IPV4_GATEWAY); + ParseIPv4(&Settings.ipv4_address[2], WIFI_IPV4_SUBNETMASK); + ParseIPv4(&Settings.ipv4_address[3], WIFI_IPV4_DNS); Settings.sta_config = WIFI_CONFIG_TOOL; // Settings.sta_active = 0; SettingsUpdateText(SET_STASSID1, PSTR(STA_SSID1)); diff --git a/tasmota/support.ino b/tasmota/support.ino index 0d5ce5b94..730778d48 100644 --- a/tasmota/support.ino +++ b/tasmota/support.ino @@ -655,13 +655,11 @@ uint8_t Shortcut(void) bool ValidIpAddress(const char* str) { - const char* p = str; - - while (*p && ((*p == '.') || ((*p >= '0') && (*p <= '9')))) { p++; } - return (*p == '\0'); + IPAddress ip_address; + return ip_address.fromString(str); } -bool ParseIp(uint32_t* addr, const char* str) +bool ParseIPv4(uint32_t* addr, const char* str) { uint8_t *part = (uint8_t*)addr; uint8_t i; diff --git a/tasmota/support_command.ino b/tasmota/support_command.ino index 1c5e32474..98b198216 100644 --- a/tasmota/support_command.ino +++ b/tasmota/support_command.ino @@ -510,8 +510,8 @@ void CmndStatus(void) Response_P(PSTR("{\"" D_CMND_STATUS D_STATUS5_NETWORK "\":{\"" D_CMND_HOSTNAME "\":\"%s\",\"" D_CMND_IPADDRESS "\":\"%s\",\"" D_JSON_GATEWAY "\":\"%s\",\"" D_JSON_SUBNETMASK "\":\"%s\",\"" D_JSON_DNSSERVER "\":\"%s\",\"" D_JSON_MAC "\":\"%s\",\"" D_CMND_WEBSERVER "\":%d,\"" D_CMND_WIFICONFIG "\":%d,\"" D_CMND_WIFIPOWER "\":%s}}"), - NetworkHostname(), NetworkAddress().toString().c_str(), IPAddress(Settings.ip_address[1]).toString().c_str(), - IPAddress(Settings.ip_address[2]).toString().c_str(), IPAddress(Settings.ip_address[3]).toString().c_str(), NetworkMacAddress().c_str(), + NetworkHostname(), NetworkAddress().toString().c_str(), IPAddress(Settings.ipv4_address[1]).toString().c_str(), + IPAddress(Settings.ipv4_address[2]).toString().c_str(), IPAddress(Settings.ipv4_address[3]).toString().c_str(), NetworkMacAddress().c_str(), Settings.webserver, Settings.sta_config, WifiGetOutputPower().c_str()); MqttPublishPrefixTopic_P(STAT, PSTR(D_CMND_STATUS "5")); } @@ -1497,18 +1497,18 @@ void CmndIpAddress(void) snprintf_P(stemp1, sizeof(stemp1), PSTR(" %s"), NetworkAddress().toString().c_str()); ResponseClear(); for (uint32_t i = 0; i < 4; i++) { - ResponseAppend_P(PSTR("%c\"%s%d\":\"%s%s\""), (i) ? ',' : '{', XdrvMailbox.command, i +1, IPAddress(Settings.ip_address[i]).toString().c_str(), (0 == i) ? stemp1:""); + ResponseAppend_P(PSTR("%c\"%s%d\":\"%s%s\""), (i) ? ',' : '{', XdrvMailbox.command, i +1, IPAddress(Settings.ipv4_address[i]).toString().c_str(), (0 == i) ? stemp1:""); } ResponseJsonEnd(); } else { - uint32_t address; - if (ParseIp(&address, XdrvMailbox.data)) { - Settings.ip_address[XdrvMailbox.index -1] = address; + uint32_t ipv4_address; + if (ParseIPv4(&ipv4_address, XdrvMailbox.data)) { + Settings.ipv4_address[XdrvMailbox.index -1] = ipv4_address; // TasmotaGlobal.restart_flag = 2; } char stemp1[TOPSZ]; snprintf_P(stemp1, sizeof(stemp1), PSTR(" %s"), NetworkAddress().toString().c_str()); - Response_P(S_JSON_COMMAND_INDEX_SVALUE_SVALUE, XdrvMailbox.command, XdrvMailbox.index, IPAddress(Settings.ip_address[XdrvMailbox.index -1]).toString().c_str(), (1 == XdrvMailbox.index) ? stemp1:""); + Response_P(S_JSON_COMMAND_INDEX_SVALUE_SVALUE, XdrvMailbox.command, XdrvMailbox.index, IPAddress(Settings.ipv4_address[XdrvMailbox.index -1]).toString().c_str(), (1 == XdrvMailbox.index) ? stemp1:""); } } } diff --git a/tasmota/support_device_groups.ino b/tasmota/support_device_groups.ino index e5b00cba2..5ab856cd6 100644 --- a/tasmota/support_device_groups.ino +++ b/tasmota/support_device_groups.ino @@ -66,13 +66,6 @@ bool device_groups_up = false; bool building_status_message = false; bool ignore_dgr_sends = false; -char * IPAddressToString(const IPAddress& ip_address) -{ - static char buffer[16]; - sprintf_P(buffer, PSTR("%u.%u.%u.%u"), ip_address[0], ip_address[1], ip_address[2], ip_address[3]); - return buffer; -} - uint8_t * BeginDeviceGroupMessage(struct device_group * device_group, uint16_t flags, bool hold_sequence = false) { uint8_t * message_ptr = &device_group->message[device_group->message_header_length]; @@ -225,7 +218,7 @@ void SendReceiveDeviceGroupMessage(struct device_group * device_group, struct de flags |= *message_ptr++ << 8; // Initialize the log buffer. - log_length = sprintf(log_buffer, PSTR("DGR: %s %s message %s %s: seq=%u, flags=%u"), (received ? PSTR("Received") : PSTR("Sending")), device_group->group_name, (received ? PSTR("from") : PSTR("to")), (device_group_member ? IPAddressToString(device_group_member->ip_address) : received ? PSTR("local") : PSTR("network")), message_sequence, flags); + log_length = sprintf(log_buffer, PSTR("DGR: %s %s message %s %s: seq=%u, flags=%u"), (received ? PSTR("Received") : PSTR("Sending")), device_group->group_name, (received ? PSTR("from") : PSTR("to")), (device_group_member ? device_group_member->ip_address.toString().c_str() : received ? PSTR("local") : PSTR("network")), message_sequence, flags); log_ptr = log_buffer + log_length; log_remaining = sizeof(log_buffer) - log_length; @@ -777,7 +770,7 @@ void ProcessDeviceGroupMessage(uint8_t * message, int message_length) } device_group_member->ip_address = remote_ip; *flink = device_group_member; - AddLog_P(LOG_LEVEL_DEBUG, PSTR("DGR: Member %s added"), IPAddressToString(remote_ip)); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("DGR: Member %s added"), remote_ip.toString().c_str()); break; } else if (device_group_member->ip_address == remote_ip) { @@ -797,7 +790,7 @@ void DeviceGroupStatus(uint8_t device_group_index) struct device_group * device_group = &device_groups[device_group_index]; buffer[0] = buffer[1] = 0; for (struct device_group_member * device_group_member = device_group->device_group_members; device_group_member; device_group_member = device_group_member->flink) { - snprintf_P(buffer, sizeof(buffer), PSTR("%s,{\"IPAddress\":\"%s\",\"ResendCount\":%u,\"LastRcvdSeq\":%u,\"LastAckedSeq\":%u}"), buffer, IPAddressToString(device_group_member->ip_address), device_group_member->unicast_count, device_group_member->received_sequence, device_group_member->acked_sequence); + snprintf_P(buffer, sizeof(buffer), PSTR("%s,{\"IPAddress\":\"%s\",\"ResendCount\":%u,\"LastRcvdSeq\":%u,\"LastAckedSeq\":%u}"), buffer, device_group_member->ip_address.toString().c_str(), device_group_member->unicast_count, device_group_member->received_sequence, device_group_member->acked_sequence); member_count++; } Response_P(PSTR("{\"" D_CMND_DEVGROUPSTATUS "\":{\"Index\":%u,\"GroupName\":\"%s\",\"MessageSeq\":%u,\"MemberCount\":%d,\"Members\":[%s]}}"), device_group_index, device_group->group_name, device_group->outgoing_sequence, member_count, &buffer[1]); @@ -874,7 +867,7 @@ AddLog_P(LOG_LEVEL_DEBUG, PSTR("DGR: Checking next_check_time=%u, now=%u"), next if ((long)(now - device_group->member_timeout_time) >= 0) { *flink = device_group_member->flink; free(device_group_member); - AddLog_P(LOG_LEVEL_DEBUG, PSTR("DGR: Member %s removed"), IPAddressToString(device_group_member->ip_address)); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("DGR: Member %s removed"), device_group_member->ip_address.toString().c_str()); continue; } diff --git a/tasmota/support_wifi.ino b/tasmota/support_wifi.ino index 15f5e10c2..ca2180b77 100644 --- a/tasmota/support_wifi.ino +++ b/tasmota/support_wifi.ino @@ -203,8 +203,8 @@ void WifiBegin(uint8_t flag, uint8_t channel) if (!strlen(SettingsText(SET_STASSID1 + Settings.sta_active))) { Settings.sta_active ^= 1; // Skip empty SSID } - if (Settings.ip_address[0]) { - WiFi.config(Settings.ip_address[0], Settings.ip_address[1], Settings.ip_address[2], Settings.ip_address[3]); // Set static IP + if (Settings.ipv4_address[0]) { + WiFi.config(Settings.ipv4_address[0], Settings.ipv4_address[1], Settings.ipv4_address[2], Settings.ipv4_address[3]); // Set static IP } WiFi.hostname(TasmotaGlobal.hostname); @@ -228,9 +228,9 @@ void WifiBegin(uint8_t flag, uint8_t channel) AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_WIFI "Got IPv6 global address %s"), addr.toString().c_str()); break; // IPv6 is mandatory but stop after 15 seconds } - delay(500); // Loop until real IPv6 address is aquired or too many tries failed - cfgcnt++; } + delay(500); // Loop until real IPv6 address is aquired or too many tries failed + cfgcnt++; } #endif // LWIP_IPV6=1 } @@ -361,16 +361,6 @@ void WifiSetState(uint8_t state) } #if LWIP_IPV6 -bool WifiCheckIPv6(void) -{ - bool ipv6_global=false; - - for (auto a : addrList) { - if(!a.isLocal() && a.isV6()) ipv6_global=true; - } - return ipv6_global; -} - String WifiGetIPv6(void) { for (auto a : addrList) { @@ -378,35 +368,30 @@ String WifiGetIPv6(void) } return ""; } - -bool WifiCheckIPAddrStatus(void) // Return false for 169.254.x.x or fe80::/64 -{ - bool ip_global=false; - - for (auto a : addrList) { - if(!a.isLocal()) ip_global=true; - } - return ip_global; -} #endif // LWIP_IPV6=1 +// Check to see if we have any routable IP address +inline bool WifiCheck_hasIP(IPAddress const & ip_address) +{ +#ifdef LWIP2_IPV6 + return !a.isLocal(); +#else + return static_cast(ip_address) != 0; +#endif +} + void WifiCheckIp(void) { -#if LWIP_IPV6 - if(WifiCheckIPAddrStatus()) { - Wifi.status = WL_CONNECTED; -#else - if ((WL_CONNECTED == WiFi.status()) && (static_cast(WiFi.localIP()) != 0)) { -#endif // LWIP_IPV6=1 + if ((WL_CONNECTED == WiFi.status()) && WifiCheck_hasIP(WiFi.localIP())) { WifiSetState(1); Wifi.counter = WIFI_CHECK_SEC; Wifi.retry = Wifi.retry_init; if (Wifi.status != WL_CONNECTED) { AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_WIFI D_CONNECTED)); // AddLog_P(LOG_LEVEL_INFO, PSTR("Wifi: Set IP addresses")); - Settings.ip_address[1] = (uint32_t)WiFi.gatewayIP(); - Settings.ip_address[2] = (uint32_t)WiFi.subnetMask(); - Settings.ip_address[3] = (uint32_t)WiFi.dnsIP(); + Settings.ipv4_address[1] = (uint32_t)WiFi.gatewayIP(); + Settings.ipv4_address[2] = (uint32_t)WiFi.subnetMask(); + Settings.ipv4_address[3] = (uint32_t)WiFi.dnsIP(); // Save current AP parameters for quick reconnect Settings.wifi_channel = WiFi.channel(); @@ -521,11 +506,7 @@ void WifiCheck(uint8_t param) Wifi.counter = WIFI_CHECK_SEC; WifiCheckIp(); } -#if LWIP_IPV6 - if (WifiCheckIPAddrStatus()) { -#else - if ((WL_CONNECTED == WiFi.status()) && (static_cast(WiFi.localIP()) != 0) && !Wifi.config_type) { -#endif // LWIP_IPV6=1 + if ((WL_CONNECTED == WiFi.status()) && WifiCheck_hasIP(WiFi.localIP()) && !Wifi.config_type) { WifiSetState(1); if (Settings.flag3.use_wifi_rescan) { // SetOption57 - Scan wifi network every 44 minutes for configured AP's if (!(TasmotaGlobal.uptime % (60 * WIFI_RESCAN_MINUTES))) { diff --git a/tasmota/user_config_override_sample.h b/tasmota/user_config_override_sample.h index d33e5fc75..df151f9b2 100644 --- a/tasmota/user_config_override_sample.h +++ b/tasmota/user_config_override_sample.h @@ -68,18 +68,18 @@ Examples : // Ie: export PLATFORMIO_BUILD_FLAGS='-DUSE_CONFIG_OVERRIDE -DMY_IP="192.168.1.99" -DMY_GW="192.168.1.1" -DMY_DNS="192.168.1.1"' #ifdef MY_IP -#undef WIFI_IP_ADDRESS -#define WIFI_IP_ADDRESS MY_IP // Set to 0.0.0.0 for using DHCP or enter a static IP address +#undef WIFI_IPV4_ADDRESS +#define WIFI_IPV4_ADDRESS MY_IP // Set to 0.0.0.0 for using DHCP or enter a static IP address #endif #ifdef MY_GW -#undef WIFI_GATEWAY -#define WIFI_GATEWAY MY_GW // if not using DHCP set Gateway IP address +#undef WIFI_IPV4_GATEWAY +#define WIFI_IPV4_GATEWAY MY_GW // if not using DHCP set Gateway IP address #endif #ifdef MY_DNS -#undef WIFI_DNS -#define WIFI_DNS MY_DNS // If not using DHCP set DNS IP address (might be equal to WIFI_GATEWAY) +#undef WIFI_IPV4_DNS +#define WIFI_IPV4_DNS MY_DNS // If not using DHCP set DNS IP address (might be equal to WIFI_GATEWAY) #endif */ diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index 42cbf1af4..c7791653e 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -2110,9 +2110,9 @@ void HandleInformation(void) } } if (!TasmotaGlobal.global_state.network_down) { - WSContentSend_P(PSTR("}1" D_GATEWAY "}2%s"), IPAddress(Settings.ip_address[1]).toString().c_str()); - WSContentSend_P(PSTR("}1" D_SUBNET_MASK "}2%s"), IPAddress(Settings.ip_address[2]).toString().c_str()); - WSContentSend_P(PSTR("}1" D_DNS_SERVER "}2%s"), IPAddress(Settings.ip_address[3]).toString().c_str()); + WSContentSend_P(PSTR("}1" D_GATEWAY "}2%s"), IPAddress(Settings.ipv4_address[1]).toString().c_str()); + WSContentSend_P(PSTR("}1" D_SUBNET_MASK "}2%s"), IPAddress(Settings.ipv4_address[2]).toString().c_str()); + WSContentSend_P(PSTR("}1" D_DNS_SERVER "}2%s"), IPAddress(Settings.ipv4_address[3]).toString().c_str()); } if ((WiFi.getMode() >= WIFI_AP) && (static_cast(WiFi.softAPIP()) != 0)) { WSContentSend_P(PSTR("}1
}2
")); diff --git a/tasmota/xdrv_10_scripter.ino b/tasmota/xdrv_10_scripter.ino index cd07098f8..a04131ae5 100755 --- a/tasmota/xdrv_10_scripter.ino +++ b/tasmota/xdrv_10_scripter.ino @@ -477,13 +477,6 @@ bool event_handeled = false; IPAddress last_udp_ip; WiFiUDP Script_PortUdp; -#ifndef USE_DEVICE_GROUPS -char * IPAddressToString(const IPAddress& ip_address) { - static char ipbuffer[16]; - sprintf_P(ipbuffer, PSTR("%u.%u.%u.%u"), ip_address[0], ip_address[1], ip_address[2], ip_address[3]); - return ipbuffer; -} -#endif //USE_DEVICE_GROUPS #endif //USE_SCRIPT_GLOBVARS int16_t last_findex; @@ -2525,7 +2518,7 @@ chknext: } #ifdef USE_SCRIPT_GLOBVARS if (!strncmp(vname, "luip", 4)) { - if (sp) strlcpy(sp, IPAddressToString(last_udp_ip), glob_script_mem.max_ssize); + if (sp) strlcpy(sp, last_udp_ip.toString().c_str(), glob_script_mem.max_ssize); goto strexit; } #endif //USE_SCRIPT_GLOBVARS diff --git a/tasmota/xdrv_82_ethernet.ino b/tasmota/xdrv_82_ethernet.ino index 7d7ae3c59..d169b3e2e 100644 --- a/tasmota/xdrv_82_ethernet.ino +++ b/tasmota/xdrv_82_ethernet.ino @@ -95,9 +95,9 @@ void EthernetEvent(WiFiEvent_t event) { case SYSTEM_EVENT_ETH_GOT_IP: AddLog_P(LOG_LEVEL_DEBUG, PSTR("ETH: Mac %s, IPAddress %s, Hostname %s"), ETH.macAddress().c_str(), ETH.localIP().toString().c_str(), eth_hostname); - Settings.ip_address[1] = (uint32_t)ETH.gatewayIP(); - Settings.ip_address[2] = (uint32_t)ETH.subnetMask(); - Settings.ip_address[3] = (uint32_t)ETH.dnsIP(); + Settings.ipv4_address[1] = (uint32_t)ETH.gatewayIP(); + Settings.ipv4_address[2] = (uint32_t)ETH.subnetMask(); + Settings.ipv4_address[3] = (uint32_t)ETH.dnsIP(); TasmotaGlobal.global_state.eth_down = 0; break; case SYSTEM_EVENT_ETH_DISCONNECTED: From 57a4153a418d6daebc5d407f700caa611de9d05f Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Mon, 18 Jan 2021 18:31:19 +0100 Subject: [PATCH 022/186] Zigbee fix bad JSON for `ZbStatus0` --- tasmota/xdrv_23_zigbee_2a_devices_impl.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasmota/xdrv_23_zigbee_2a_devices_impl.ino b/tasmota/xdrv_23_zigbee_2a_devices_impl.ino index ffb5a72df..7d850ef5a 100644 --- a/tasmota/xdrv_23_zigbee_2a_devices_impl.ino +++ b/tasmota/xdrv_23_zigbee_2a_devices_impl.ino @@ -767,7 +767,7 @@ String Z_Devices::dumpCoordinator(void) const { attr_list.addAttributePMEM(PSTR("IEEEAddr")).setHex64(localIEEEAddr); attr_list.addAttributePMEM(PSTR("TotalDevices")).setUInt(zigbee_devices.devicesSize()); - return attr_list.toString(); + return attr_list.toString(true); } // If &device == nullptr, then dump all From 1b1b1ed83cdba93535b01449cacbb1100d94912d Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Mon, 18 Jan 2021 19:21:51 +0100 Subject: [PATCH 023/186] build gz only for ESP8266 --- pio-tools/gzip-firmware.py | 42 ++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/pio-tools/gzip-firmware.py b/pio-tools/gzip-firmware.py index e29c05576..bb1759123 100644 --- a/pio-tools/gzip-firmware.py +++ b/pio-tools/gzip-firmware.py @@ -3,26 +3,32 @@ import os import shutil import gzip -OUTPUT_DIR = "build_output{}".format(os.path.sep) +platform = env.PioPlatform() +board = env.BoardConfig() +mcu = board.get("build.mcu", "esp32") +# gzip only for ESP8266 +if env["PIOPLATFORM"] != "espressif32": -def bin_gzip(source, target, env): - variant = str(target[0]).split(os.path.sep)[2] - - # create string with location and file names based on variant - bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) - gzip_file = "{}firmware{}{}.bin.gz".format(OUTPUT_DIR, os.path.sep, variant) + OUTPUT_DIR = "build_output{}".format(os.path.sep) - # check if new target files exist and remove if necessary - if os.path.isfile(gzip_file): os.remove(gzip_file) + def bin_gzip(source, target, env): + variant = str(target[0]).split(os.path.sep)[2] - # write gzip firmware file - with open(bin_file,"rb") as fp: - with gzip.open(gzip_file, "wb", compresslevel = 9) as f: - shutil.copyfileobj(fp, f) - - ORG_FIRMWARE_SIZE = os.stat(bin_file).st_size - GZ_FIRMWARE_SIZE = os.stat(gzip_file).st_size + # create string with location and file names based on variant + bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) + gzip_file = "{}firmware{}{}.bin.gz".format(OUTPUT_DIR, os.path.sep, variant) - print("Compression reduced firmware size by {:.0f}% (was {} bytes, now {} bytes)".format((GZ_FIRMWARE_SIZE / ORG_FIRMWARE_SIZE) * 100, ORG_FIRMWARE_SIZE, GZ_FIRMWARE_SIZE)) + # check if new target files exist and remove if necessary + if os.path.isfile(gzip_file): os.remove(gzip_file) -env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_gzip]) + # write gzip firmware file + with open(bin_file,"rb") as fp: + with gzip.open(gzip_file, "wb", compresslevel = 9) as f: + shutil.copyfileobj(fp, f) + + ORG_FIRMWARE_SIZE = os.stat(bin_file).st_size + GZ_FIRMWARE_SIZE = os.stat(gzip_file).st_size + + print("Compression reduced firmware size by {:.0f}% (was {} bytes, now {} bytes)".format((GZ_FIRMWARE_SIZE / ORG_FIRMWARE_SIZE) * 100, ORG_FIRMWARE_SIZE, GZ_FIRMWARE_SIZE)) + + env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_gzip]) From 081f097b60b037e554d33a3a90e8b48460cccd3f Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Mon, 18 Jan 2021 20:39:14 +0100 Subject: [PATCH 024/186] Zigbee increase timeouts for EZSP --- tasmota/xdrv_23_zigbee_7_0_statemachine.ino | 64 ++++++++++----------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/tasmota/xdrv_23_zigbee_7_0_statemachine.ino b/tasmota/xdrv_23_zigbee_7_0_statemachine.ino index cc23c7a92..b2a01b6eb 100644 --- a/tasmota/xdrv_23_zigbee_7_0_statemachine.ino +++ b/tasmota/xdrv_23_zigbee_7_0_statemachine.ino @@ -789,38 +789,38 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { // configure EFR32 ZI_MQTT_STATE(ZIGBEE_STATUS_STARTING, kConfiguredCoord) - ZI_SEND(ZBS_SET_ADDR_TABLE) ZI_WAIT_RECV(500, ZBR_SET_OK) // Address table size - ZI_SEND(ZBS_SET_MCAST_TABLE) ZI_WAIT_RECV(500, ZBR_SET_OK) - ZI_SEND(ZBS_SET_STK_PROF) ZI_WAIT_RECV(500, ZBR_SET_OK) - ZI_SEND(ZBS_SET_SEC_LEVEL) ZI_WAIT_RECV(500, ZBR_SET_OK) - ZI_SEND(ZBS_SET_MAX_DEVICES) ZI_WAIT_RECV(500, ZBR_SET_OK) - ZI_SEND(ZBS_SET_INDIRECT_TMO) ZI_WAIT_RECV(500, ZBR_SET_OK) - ZI_SEND(ZBS_SET_TC_CACHE) ZI_WAIT_RECV(500, ZBR_SET_OK) - ZI_SEND(ZBS_SET_ROUTE_TBL) ZI_WAIT_RECV(500, ZBR_SET_OK) - ZI_SEND(ZBS_SET_KEY_TBL) ZI_WAIT_RECV(500, ZBR_SET_OK) - ZI_SEND(ZBS_SET_PANID_CNFLCT) ZI_WAIT_RECV(500, ZBR_SET_OK) - ZI_SEND(ZBS_SET_ZDO_REQ) ZI_WAIT_RECV(500, ZBR_SET_OK) - ZI_SEND(ZBS_SET_NETWORKS) ZI_WAIT_RECV(500, ZBR_SET_OK) - ZI_SEND(ZBS_SET_PACKET_BUF) ZI_WAIT_RECV(500, ZBR_SET_OK2) + ZI_SEND(ZBS_SET_ADDR_TABLE) ZI_WAIT_RECV(2500, ZBR_SET_OK) // Address table size + ZI_SEND(ZBS_SET_MCAST_TABLE) ZI_WAIT_RECV(2500, ZBR_SET_OK) + ZI_SEND(ZBS_SET_STK_PROF) ZI_WAIT_RECV(2500, ZBR_SET_OK) + ZI_SEND(ZBS_SET_SEC_LEVEL) ZI_WAIT_RECV(2500, ZBR_SET_OK) + ZI_SEND(ZBS_SET_MAX_DEVICES) ZI_WAIT_RECV(2500, ZBR_SET_OK) + ZI_SEND(ZBS_SET_INDIRECT_TMO) ZI_WAIT_RECV(2500, ZBR_SET_OK) + ZI_SEND(ZBS_SET_TC_CACHE) ZI_WAIT_RECV(2500, ZBR_SET_OK) + ZI_SEND(ZBS_SET_ROUTE_TBL) ZI_WAIT_RECV(2500, ZBR_SET_OK) + ZI_SEND(ZBS_SET_KEY_TBL) ZI_WAIT_RECV(2500, ZBR_SET_OK) + ZI_SEND(ZBS_SET_PANID_CNFLCT) ZI_WAIT_RECV(2500, ZBR_SET_OK) + ZI_SEND(ZBS_SET_ZDO_REQ) ZI_WAIT_RECV(2500, ZBR_SET_OK) + ZI_SEND(ZBS_SET_NETWORKS) ZI_WAIT_RECV(2500, ZBR_SET_OK) + ZI_SEND(ZBS_SET_PACKET_BUF) ZI_WAIT_RECV(2500, ZBR_SET_OK2) // read configuration // TODO - not sure it's useful - //ZI_SEND(ZBS_GET_APS_UNI) ZI_WAIT_RECV_FUNC(500, ZBR_GET_OK, &EZ_ReadAPSUnicastMessage) + //ZI_SEND(ZBS_GET_APS_UNI) ZI_WAIT_RECV_FUNC(2500, ZBR_GET_OK, &EZ_ReadAPSUnicastMessage) // add endpoint 0x01 and 0x0B - ZI_SEND(ZBS_ADD_ENDPOINT1) ZI_WAIT_RECV(500, ZBR_ADD_ENDPOINT) - ZI_SEND(ZBS_ADD_ENDPOINTB) ZI_WAIT_RECV(500, ZBR_ADD_ENDPOINT) + ZI_SEND(ZBS_ADD_ENDPOINT1) ZI_WAIT_RECV(2500, ZBR_ADD_ENDPOINT) + ZI_SEND(ZBS_ADD_ENDPOINTB) ZI_WAIT_RECV(2500, ZBR_ADD_ENDPOINT) // set Concentrator - ZI_SEND(ZBS_SET_CONCENTRATOR) ZI_WAIT_RECV(500, ZBR_SET_CONCENTRATOR) + ZI_SEND(ZBS_SET_CONCENTRATOR) ZI_WAIT_RECV(2500, ZBR_SET_CONCENTRATOR) // setInitialSecurityState - ZI_SEND(ZBS_SET_POLICY_00) ZI_WAIT_RECV(500, ZBR_SET_POLICY_XX) - ZI_SEND(ZBS_SET_POLICY_02) ZI_WAIT_RECV(500, ZBR_SET_POLICY_XX) - ZI_SEND(ZBS_SET_POLICY_03) ZI_WAIT_RECV(500, ZBR_SET_POLICY_XX) - // ZI_SEND(ZBS_SET_POLICY_04) ZI_WAIT_RECV(500, ZBR_SET_POLICY_XX) - ZI_SEND(ZBS_SET_POLICY_05) ZI_WAIT_RECV(500, ZBR_SET_POLICY_XX) - ZI_SEND(ZBS_SET_POLICY_06) ZI_WAIT_RECV(500, ZBR_SET_POLICY_XX) + ZI_SEND(ZBS_SET_POLICY_00) ZI_WAIT_RECV(2500, ZBR_SET_POLICY_XX) + ZI_SEND(ZBS_SET_POLICY_02) ZI_WAIT_RECV(2500, ZBR_SET_POLICY_XX) + ZI_SEND(ZBS_SET_POLICY_03) ZI_WAIT_RECV(2500, ZBR_SET_POLICY_XX) + // ZI_SEND(ZBS_SET_POLICY_04) ZI_WAIT_RECV(2500, ZBR_SET_POLICY_XX) + ZI_SEND(ZBS_SET_POLICY_05) ZI_WAIT_RECV(2500, ZBR_SET_POLICY_XX) + ZI_SEND(ZBS_SET_POLICY_06) ZI_WAIT_RECV(2500, ZBR_SET_POLICY_XX) // Decide whether we try 'networkInit()' to restore configuration, or create a new network ZI_CALL(&EZ_GotoIfResetConfig, ZIGBEE_LABEL_CONFIGURE_EZSP) // goto ZIGBEE_LABEL_CONFIGURE_EZSP if reset_config is set @@ -830,12 +830,12 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { // Try networkInit to restore settings, and check if network comes up ZI_ON_TIMEOUT_GOTO(ZIGBEE_LABEL_BAD_CONFIG) // ZI_ON_ERROR_GOTO(ZIGBEE_LABEL_BAD_CONFIG) - ZI_SEND(ZBS_NETWORK_INIT) ZI_WAIT_RECV(500, ZBR_NETWORK_INIT) + ZI_SEND(ZBS_NETWORK_INIT) ZI_WAIT_RECV(2500, ZBR_NETWORK_INIT) ZI_WAIT_RECV(1500, ZBR_NETWORK_UP) // wait for network to start // check if configuration is ok - ZI_SEND(ZBS_GET_KEY_NWK) ZI_WAIT_RECV_FUNC(500, ZBR_GET_KEY_NWK, &EZ_CheckKeyNWK) - ZI_SEND(ZBS_GET_EUI64) ZI_WAIT_RECV_FUNC(500, ZBR_GET_EUI64, &EZ_GetEUI64) - ZI_SEND(ZBS_GET_NETW_PARM) ZI_WAIT_RECV_FUNC(500, ZBR_CHECK_NETW_PARM, &EZ_NetworkParameters) + ZI_SEND(ZBS_GET_KEY_NWK) ZI_WAIT_RECV_FUNC(2500, ZBR_GET_KEY_NWK, &EZ_CheckKeyNWK) + ZI_SEND(ZBS_GET_EUI64) ZI_WAIT_RECV_FUNC(2500, ZBR_GET_EUI64, &EZ_GetEUI64) + ZI_SEND(ZBS_GET_NETW_PARM) ZI_WAIT_RECV_FUNC(2500, ZBR_CHECK_NETW_PARM, &EZ_NetworkParameters) // all ok, proceed to next step ZI_GOTO(ZIGBEE_LABEL_NETWORK_CONFIGURED) @@ -851,9 +851,9 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { ZI_ON_TIMEOUT_GOTO(ZIGBEE_LABEL_ABORT) ZI_ON_ERROR_GOTO(ZIGBEE_LABEL_ABORT) // set encryption keys - ZI_SEND(ZBS_SET_SECURITY) ZI_WAIT_RECV(500, ZBR_SET_SECURITY) + ZI_SEND(ZBS_SET_SECURITY) ZI_WAIT_RECV(2500, ZBR_SET_SECURITY) // formNetwork - ZI_SEND(ZBS_FORM_NETWORK) ZI_WAIT_RECV(500, ZBR_FORM_NETWORK) + ZI_SEND(ZBS_FORM_NETWORK) ZI_WAIT_RECV(2500, ZBR_FORM_NETWORK) ZI_WAIT_RECV(5000, ZBR_NETWORK_UP) // wait for network to start ZI_LABEL(ZIGBEE_LABEL_NETWORK_CONFIGURED) @@ -861,11 +861,11 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { ZI_ON_TIMEOUT_GOTO(ZIGBEE_LABEL_ABORT) ZI_ON_ERROR_GOTO(ZIGBEE_LABEL_ABORT) // Query device information - ZI_SEND(ZBS_GET_EUI64) ZI_WAIT_RECV_FUNC(500, ZBR_GET_EUI64, &EZ_GetEUI64) - ZI_SEND(ZBS_GET_NODEID) ZI_WAIT_RECV_FUNC(500, ZBR_GET_NODEID, &EZ_GetNodeId) + ZI_SEND(ZBS_GET_EUI64) ZI_WAIT_RECV_FUNC(2500, ZBR_GET_EUI64, &EZ_GetEUI64) + ZI_SEND(ZBS_GET_NODEID) ZI_WAIT_RECV_FUNC(2500, ZBR_GET_NODEID, &EZ_GetNodeId) // auto-register multicast group 0x0000 ZI_LOG(LOG_LEVEL_INFO, kZigbeeGroup0) - ZI_SEND(ZBS_SET_MCAST_ENTRY) ZI_WAIT_RECV(500, ZBR_SET_MCAST_ENTRY) + ZI_SEND(ZBS_SET_MCAST_ENTRY) ZI_WAIT_RECV(2500, ZBR_SET_MCAST_ENTRY) // ZI_LABEL(ZIGBEE_LABEL_READY) ZI_MQTT_STATE(ZIGBEE_STATUS_OK, kStarted) From cdc9d8dfc9082e39ef06ad33e0921c4913782db0 Mon Sep 17 00:00:00 2001 From: s-hadinger <49731213+s-hadinger@users.noreply.github.com> Date: Mon, 18 Jan 2021 21:28:12 +0100 Subject: [PATCH 025/186] Zigbee send ack to command (#10624) * Zigbee send ack to command * Fix cluster Co-authored-by: Stephan Hadinger --- tasmota/xdrv_23_zigbee_5_converters.ino | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index 674f6fdd0..4af809b5c 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -1673,6 +1673,27 @@ void ZCLFrame::parseClusterSpecificCommand(Z_attribute_list& attr_list) { } } } + // Send Default Response to acknowledge the attribute reporting + if (0 == _frame_control.b.disable_def_resp) { + // the device expects a default response + SBuffer buf(2); + buf.add8(_cmd_id); + buf.add8(0x00); // Status = OK + + ZigbeeZCLSend_Raw(ZigbeeZCLSendMessage({ + _srcaddr, + 0x0000, + _cluster_id, + _srcendpoint, + ZCL_DEFAULT_RESPONSE, + _manuf_code, + false /* not cluster specific */, + false /* noresponse */, + true /* direct no retry */, + _transact_seq, /* zcl transaction id */ + buf.getBuffer(), buf.len() + })); + } } // ====================================================================== From 2f139d0e47bf7e353fdf47d10b38c101cd8a1558 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Mon, 18 Jan 2021 21:48:04 +0100 Subject: [PATCH 026/186] More PROGMEM --- .../jsmn-shadinger-1.0/src/JsonGenerator.h | 4 +- tasmota/settings.ino | 38 ++--- tasmota/support.ino | 13 +- tasmota/support_command.ino | 6 +- tasmota/support_network.ino | 2 +- tasmota/support_rtc.ino | 18 ++- tasmota/support_tasmota.ino | 12 +- tasmota/support_wifi.ino | 4 +- tasmota/tasmota.ino | 4 +- tasmota/xdrv_01_webserver.ino | 145 +++++++++--------- tasmota/xdrv_02_mqtt.ino | 50 +++--- tasmota/xdrv_04_light.ino | 2 +- tasmota/xdrv_07_domoticz.ino | 4 +- tasmota/xdrv_09_timers.ino | 6 +- tasmota/xdrv_10_rules.ino | 22 +-- tasmota/xdrv_12_home_assistant.ino | 6 +- tasmota/xdrv_16_tuyamcu.ino | 2 +- tasmota/xdrv_20_hue.ino | 56 +++---- tasmota/xdrv_23_zigbee_1z_libs.ino | 6 +- tasmota/xdrv_23_zigbee_2a_devices_impl.ino | 2 +- tasmota/xdrv_23_zigbee_3_hue.ino | 16 +- tasmota/xdrv_23_zigbee_A_impl.ino | 2 +- tasmota/xdrv_38_ping.ino | 2 +- tasmota/xdrv_40_telegram.ino | 4 +- tasmota/xdrv_50_filesystem.ino | 28 ++-- tasmota/xsns_34_hx711.ino | 2 +- 26 files changed, 234 insertions(+), 222 deletions(-) diff --git a/lib/default/jsmn-shadinger-1.0/src/JsonGenerator.h b/lib/default/jsmn-shadinger-1.0/src/JsonGenerator.h index 2f27b846b..1d6c0f969 100644 --- a/lib/default/jsmn-shadinger-1.0/src/JsonGenerator.h +++ b/lib/default/jsmn-shadinger-1.0/src/JsonGenerator.h @@ -32,7 +32,7 @@ extern String EscapeJSONString(const char *str); class JsonGeneratorArray { public: - JsonGeneratorArray(): val("[]") {} // start with empty array + JsonGeneratorArray(): val(F("[]")) {} // start with empty array void add(uint32_t uval32); void add(int32_t uval32); @@ -53,7 +53,7 @@ protected: class JsonGeneratorObject { public: - JsonGeneratorObject(): val("{}") {} // start with empty object + JsonGeneratorObject(): val(F("{}")) {} // start with empty object void add(const char* key, uint32_t uval32); void add(const char* key, int32_t uval32); diff --git a/tasmota/settings.ino b/tasmota/settings.ino index b2d438df6..0ebdac4c7 100644 --- a/tasmota/settings.ino +++ b/tasmota/settings.ino @@ -812,10 +812,10 @@ void SettingsDefaultSet2(void) flag3.use_wifi_rescan |= WIFI_SCAN_REGULARLY; Settings.wifi_output_power = 170; Settings.param[P_ARP_GRATUITOUS] = WIFI_ARP_INTERVAL; - ParseIp(&Settings.ip_address[0], WIFI_IP_ADDRESS); - ParseIp(&Settings.ip_address[1], WIFI_GATEWAY); - ParseIp(&Settings.ip_address[2], WIFI_SUBNETMASK); - ParseIp(&Settings.ip_address[3], WIFI_DNS); + ParseIp(&Settings.ip_address[0], PSTR(WIFI_IP_ADDRESS)); + ParseIp(&Settings.ip_address[1], PSTR(WIFI_GATEWAY)); + ParseIp(&Settings.ip_address[2], PSTR(WIFI_SUBNETMASK)); + ParseIp(&Settings.ip_address[3], PSTR(WIFI_DNS)); Settings.sta_config = WIFI_CONFIG_TOOL; // Settings.sta_active = 0; SettingsUpdateText(SET_STASSID1, PSTR(STA_SSID1)); @@ -865,22 +865,22 @@ void SettingsDefaultSet2(void) flag3.grouptopic_mode |= MQTT_GROUPTOPIC_FORMAT; SettingsUpdateText(SET_MQTT_HOST, MQTT_HOST); Settings.mqtt_port = MQTT_PORT; - SettingsUpdateText(SET_MQTT_CLIENT, MQTT_CLIENT_ID); - SettingsUpdateText(SET_MQTT_USER, MQTT_USER); - SettingsUpdateText(SET_MQTT_PWD, MQTT_PASS); - SettingsUpdateText(SET_MQTT_TOPIC, MQTT_TOPIC); - SettingsUpdateText(SET_MQTT_BUTTON_TOPIC, MQTT_BUTTON_TOPIC); - SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, MQTT_SWITCH_TOPIC); - SettingsUpdateText(SET_MQTT_GRP_TOPIC, MQTT_GRPTOPIC); - SettingsUpdateText(SET_MQTT_FULLTOPIC, MQTT_FULLTOPIC); + SettingsUpdateText(SET_MQTT_CLIENT, PSTR(MQTT_CLIENT_ID)); + SettingsUpdateText(SET_MQTT_USER, PSTR(MQTT_USER)); + SettingsUpdateText(SET_MQTT_PWD, PSTR(MQTT_PASS)); + SettingsUpdateText(SET_MQTT_TOPIC, PSTR(MQTT_TOPIC)); + SettingsUpdateText(SET_MQTT_BUTTON_TOPIC, PSTR(MQTT_BUTTON_TOPIC)); + SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, PSTR(MQTT_SWITCH_TOPIC)); + SettingsUpdateText(SET_MQTT_GRP_TOPIC, PSTR(MQTT_GRPTOPIC)); + SettingsUpdateText(SET_MQTT_FULLTOPIC, PSTR(MQTT_FULLTOPIC)); Settings.mqtt_retry = MQTT_RETRY_SECS; - SettingsUpdateText(SET_MQTTPREFIX1, SUB_PREFIX); - SettingsUpdateText(SET_MQTTPREFIX2, PUB_PREFIX); - SettingsUpdateText(SET_MQTTPREFIX3, PUB_PREFIX2); - SettingsUpdateText(SET_STATE_TXT1, MQTT_STATUS_OFF); - SettingsUpdateText(SET_STATE_TXT2, MQTT_STATUS_ON); - SettingsUpdateText(SET_STATE_TXT3, MQTT_CMND_TOGGLE); - SettingsUpdateText(SET_STATE_TXT4, MQTT_CMND_HOLD); + SettingsUpdateText(SET_MQTTPREFIX1, PSTR(SUB_PREFIX)); + SettingsUpdateText(SET_MQTTPREFIX2, PSTR(PUB_PREFIX)); + SettingsUpdateText(SET_MQTTPREFIX3, PSTR(PUB_PREFIX2)); + SettingsUpdateText(SET_STATE_TXT1, PSTR(MQTT_STATUS_OFF)); + SettingsUpdateText(SET_STATE_TXT2, PSTR(MQTT_STATUS_ON)); + SettingsUpdateText(SET_STATE_TXT3, PSTR(MQTT_CMND_TOGGLE)); + SettingsUpdateText(SET_STATE_TXT4, PSTR(MQTT_CMND_HOLD)); memcpy_P(Settings.mqtt_fingerprint[0], default_fingerprint1, sizeof(default_fingerprint1)); memcpy_P(Settings.mqtt_fingerprint[1], default_fingerprint2, sizeof(default_fingerprint2)); Settings.tele_period = TELE_PERIOD; diff --git a/tasmota/support.ino b/tasmota/support.ino index 0d5ce5b94..dace03c58 100644 --- a/tasmota/support.ino +++ b/tasmota/support.ino @@ -384,7 +384,7 @@ char* Uint64toHex(uint64_t value, char *str, uint16_t bits) char* dtostrfd(double number, unsigned char prec, char *s) { if ((isnan(number)) || (isinf(number))) { // Fix for JSON output (https://stackoverflow.com/questions/1423081/json-left-out-infinity-and-nan-json-status-in-ecmascript) - strcpy(s, "null"); + strcpy_P(s, PSTR("null")); return s; } else { return dtostrf(number, 1, prec, s); @@ -661,10 +661,13 @@ bool ValidIpAddress(const char* str) return (*p == '\0'); } -bool ParseIp(uint32_t* addr, const char* str) +bool ParseIp(uint32_t* addr, const char* str_p) { uint8_t *part = (uint8_t*)addr; uint8_t i; + char str_r[strlen_P(str_p)+1]; + char * str = &str_r[0]; + strcpy_P(str, str_p); *addr = 0; for (i = 0; i < 4; i++) { @@ -861,7 +864,7 @@ float ConvertPressureForSeaLevel(float pressure) String PressureUnit(void) { - return (Settings.flag.pressure_conversion) ? String(D_UNIT_MILLIMETER_MERCURY) : String(D_UNIT_PRESSURE); + return (Settings.flag.pressure_conversion) ? String(F(D_UNIT_MILLIMETER_MERCURY)) : String(F(D_UNIT_PRESSURE)); } float ConvertSpeed(float s) @@ -1024,11 +1027,11 @@ String GetSerialConfig(void) { // b00000x00 - 1 or 2 stop bits // b000xx000 - None, Even or Odd parity - const char kParity[] = "NEOI"; + const static char kParity[] PROGMEM = "NEOI"; char config[4]; config[0] = '5' + (Settings.serial_config & 0x3); - config[1] = kParity[(Settings.serial_config >> 3) & 0x3]; + config[1] = pgm_read_byte(&kParity[(Settings.serial_config >> 3) & 0x3]); config[2] = '1' + ((Settings.serial_config >> 2) & 0x1); config[3] = '\0'; return String(config); diff --git a/tasmota/support_command.ino b/tasmota/support_command.ino index 1c5e32474..145d86ef2 100644 --- a/tasmota/support_command.ino +++ b/tasmota/support_command.ino @@ -110,7 +110,7 @@ void ResponseCmndStateText(uint32_t value) void ResponseCmndDone(void) { - ResponseCmndChar(D_JSON_DONE); + ResponseCmndChar(PSTR(D_JSON_DONE)); } void ResponseCmndIdxChar(const char* value) @@ -352,7 +352,7 @@ void CmndBacklog(void) #else TasmotaGlobal.backlog_pointer = TasmotaGlobal.backlog_index; #endif - ResponseCmndChar(blflag ? D_JSON_EMPTY : D_JSON_ABORTED); + ResponseCmndChar(blflag ? PSTR(D_JSON_EMPTY) : PSTR(D_JSON_ABORTED)); } } @@ -1282,7 +1282,7 @@ void CmndTemplate(void) if (Settings.module != USER_MODULE) { ModuleDefault(Settings.module); } - SettingsUpdateText(SET_TEMPLATE_NAME, "Merged"); + SettingsUpdateText(SET_TEMPLATE_NAME, PSTR("Merged")); uint32_t j = 0; for (uint32_t i = 0; i < ARRAY_SIZE(Settings.user_template.gp.io); i++) { if (6 == i) { j = 9; } diff --git a/tasmota/support_network.ino b/tasmota/support_network.ino index b2bb73bf0..e2b24d782 100644 --- a/tasmota/support_network.ino +++ b/tasmota/support_network.ino @@ -38,7 +38,7 @@ void StartMdns(void) { // mdns_delayed_start = Settings.param[P_MDNS_DELAYED_START]; MDNS.end(); // close existing or MDNS.begin will fail Mdns.begun = (uint8_t)MDNS.begin(TasmotaGlobal.hostname); - AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_MDNS "%s"), (Mdns.begun) ? D_INITIALIZED : D_FAILED); + AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_MDNS "%s"), (Mdns.begun) ? PSTR(D_INITIALIZED) : PSTR(D_FAILED)); // } } } diff --git a/tasmota/support_rtc.ino b/tasmota/support_rtc.ino index 08f7cfc3d..2f2403e7a 100644 --- a/tasmota/support_rtc.ino +++ b/tasmota/support_rtc.ino @@ -33,8 +33,8 @@ const uint32_t MINS_PER_HOUR = 60UL; Ticker TickerRtc; -static const uint8_t kDaysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; // API starts months from 1, this array starts from 0 -static const char kMonthNamesEnglish[] = "JanFebMarAprMayJunJulAugSepOctNovDec"; +static const uint8_t kDaysInMonth[] PROGMEM = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; // API starts months from 1, this array starts from 0 +static const char kMonthNamesEnglish[] PROGMEM = "JanFebMarAprMayJunJulAugSepOctNovDec"; struct RTC { uint32_t utc_time = 0; @@ -88,7 +88,9 @@ String GetBuildDateAndTime(void) // "2017-03-07T11:08:02" - ISO8601:2004 char bdt[21]; char *p; - char mdate[] = __DATE__; // "Mar 7 2017" + static const char mdate_P[] PROGMEM = __DATE__; // "Mar 7 2017" + char mdate[strlen_P(mdate_P)+1]; // copy on stack first + strcpy_P(mdate, mdate_P); char *smonth = mdate; int day = 0; int year = 0; @@ -107,8 +109,10 @@ String GetBuildDateAndTime(void) year = atoi(str); } } - int month = (strstr(kMonthNamesEnglish, smonth) -kMonthNamesEnglish) /3 +1; - snprintf_P(bdt, sizeof(bdt), PSTR("%d" D_YEAR_MONTH_SEPARATOR "%02d" D_MONTH_DAY_SEPARATOR "%02d" D_DATE_TIME_SEPARATOR "%s"), year, month, day, __TIME__); + char MonthNamesEnglish[sizeof(kMonthNamesEnglish)]; + strcpy_P(MonthNamesEnglish, kMonthNamesEnglish); + int month = (strstr(MonthNamesEnglish, smonth) -MonthNamesEnglish) /3 +1; + snprintf_P(bdt, sizeof(bdt), PSTR("%d" D_YEAR_MONTH_SEPARATOR "%02d" D_MONTH_DAY_SEPARATOR "%02d" D_DATE_TIME_SEPARATOR "%s"), year, month, day, PSTR(__TIME__)); return String(bdt); // 2017-03-07T11:08:02 } @@ -290,7 +294,7 @@ void BreakTime(uint32_t time_input, TIME_T &tm) month_length = 28; } } else { - month_length = kDaysInMonth[month]; + month_length = pgm_read_byte(&kDaysInMonth[month]); } if (time >= month_length) { @@ -326,7 +330,7 @@ uint32_t MakeTime(TIME_T &tm) if ((2 == i) && LEAP_YEAR(tm.year)) { seconds += SECS_PER_DAY * 29; } else { - seconds += SECS_PER_DAY * kDaysInMonth[i-1]; // monthDay array starts from 0 + seconds += SECS_PER_DAY * pgm_read_byte(&kDaysInMonth[i-1]); // monthDay array starts from 0 } } seconds+= (tm.day_of_month - 1) * SECS_PER_DAY; diff --git a/tasmota/support_tasmota.ino b/tasmota/support_tasmota.ino index 64bc6e09d..9cac3fa37 100644 --- a/tasmota/support_tasmota.ino +++ b/tasmota/support_tasmota.ino @@ -20,10 +20,12 @@ const char kSleepMode[] PROGMEM = "Dynamic|Normal"; const char kPrefixes[] PROGMEM = D_CMND "|" D_STAT "|" D_TELE; -char* Format(char* output, const char* input, int size) +char* Format(char* output, const char* input_p, int size) { char *token; uint32_t digits = 0; + char input[strlen_P(input_p)+1]; // copy from PMEM to RAM + strcpy_P(input, input_p); if (strchr(input, '%') != nullptr) { strlcpy(output, input, size); @@ -102,7 +104,7 @@ char* GetTopic_P(char *stopic, uint32_t prefix, char *topic, const char* subtopi fulltopic += TasmotaGlobal.mqtt_client; fulltopic += F("_fb"); // cmnd/_fb } else { - fulltopic += topic; // cmnd/ + fulltopic += (const __FlashStringHelper *)topic; // cmnd/ } } else { fulltopic = SettingsText(SET_MQTT_FULLTOPIC); @@ -118,7 +120,7 @@ char* GetTopic_P(char *stopic, uint32_t prefix, char *topic, const char* subtopi } fulltopic.replace(FPSTR(MQTT_TOKEN_PREFIX), SettingsText(SET_MQTTPREFIX1 + prefix)); - fulltopic.replace(FPSTR(MQTT_TOKEN_TOPIC), topic); + fulltopic.replace(FPSTR(MQTT_TOKEN_TOPIC), (const __FlashStringHelper *)topic); fulltopic.replace(F("%hostname%"), TasmotaGlobal.hostname); String token_id = WiFi.macAddress(); token_id.replace(":", ""); @@ -486,7 +488,7 @@ bool SendKey(uint32_t key, uint32_t device, uint32_t state) #endif // USE_DOMOTICZ result = !Settings.flag3.button_switch_force_local; // SetOption61 - Force local operation when button/switch topic is set } else { - Response_P(PSTR("{\"%s%d\":{\"State\":%d}}"), (key) ? "Switch" : "Button", device, state); + Response_P(PSTR("{\"%s%d\":{\"State\":%d}}"), (key) ? PSTR("Switch") : PSTR("Button"), device, state); result = XdrvRulesProcess(); } #ifdef USE_PWM_DIMMER @@ -742,7 +744,7 @@ void TempHumDewShow(bool json, bool pass_on, const char *types, float f_temperat String GetSwitchText(uint32_t i) { String switch_text = SettingsText(SET_SWITCH_TXT1 + i); if ('\0' == switch_text[0]) { - switch_text = D_JSON_SWITCH + String(i +1); + switch_text = F(D_JSON_SWITCH) + String(i +1); } return switch_text; } diff --git a/tasmota/support_wifi.ino b/tasmota/support_wifi.ino index 15f5e10c2..68208b941 100644 --- a/tasmota/support_wifi.ino +++ b/tasmota/support_wifi.ino @@ -176,7 +176,7 @@ void WiFiSetSleepMode(void) void WifiBegin(uint8_t flag, uint8_t channel) { - const char kWifiPhyMode[] = " bgnl"; + const static char kWifiPhyMode[] PROGMEM = " bgnl"; #ifdef USE_EMULATION UdpDisconnect(); @@ -218,7 +218,7 @@ void WifiBegin(uint8_t flag, uint8_t channel) WiFi.begin(SettingsText(SET_STASSID1 + Settings.sta_active), SettingsText(SET_STAPWD1 + Settings.sta_active)); } AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_WIFI D_CONNECTING_TO_AP "%d %s%s " D_IN_MODE " 11%c " D_AS " %s..."), - Settings.sta_active +1, SettingsText(SET_STASSID1 + Settings.sta_active), stemp, kWifiPhyMode[WiFi.getPhyMode() & 0x3], TasmotaGlobal.hostname); + Settings.sta_active +1, SettingsText(SET_STASSID1 + Settings.sta_active), stemp, pgm_read_byte(&kWifiPhyMode[WiFi.getPhyMode() & 0x3]), TasmotaGlobal.hostname); #if LWIP_IPV6 for (bool configured = false; !configured;) { diff --git a/tasmota/tasmota.ino b/tasmota/tasmota.ino index 2407e61b1..18995cd47 100644 --- a/tasmota/tasmota.ino +++ b/tasmota/tasmota.ino @@ -302,7 +302,7 @@ void setup(void) { snprintf_P(TasmotaGlobal.version, sizeof(TasmotaGlobal.version), PSTR("%s.%d"), TasmotaGlobal.version, VERSION & 0xff); } // Thehackbox inserts "release" or "commit number" before compiling using sed -i -e 's/PSTR("(%s)")/PSTR("(85cff52-%s)")/g' tasmota.ino - snprintf_P(TasmotaGlobal.image_name, sizeof(TasmotaGlobal.image_name), PSTR("(%s)"), CODE_IMAGE_STR); // Results in (85cff52-tasmota) or (release-tasmota) + snprintf_P(TasmotaGlobal.image_name, sizeof(TasmotaGlobal.image_name), PSTR("(%s)"), PSTR(CODE_IMAGE_STR)); // Results in (85cff52-tasmota) or (release-tasmota) Format(TasmotaGlobal.mqtt_client, SettingsText(SET_MQTT_CLIENT), sizeof(TasmotaGlobal.mqtt_client)); Format(TasmotaGlobal.mqtt_topic, SettingsText(SET_MQTT_TOPIC), sizeof(TasmotaGlobal.mqtt_topic)); @@ -321,7 +321,7 @@ void setup(void) { SetPowerOnState(); AddLog_P(LOG_LEVEL_INFO, PSTR(D_PROJECT " %s %s " D_VERSION " %s%s-" ARDUINO_CORE_RELEASE "(%s)"), - PROJECT, SettingsText(SET_DEVICENAME), TasmotaGlobal.version, TasmotaGlobal.image_name, GetBuildDateAndTime().c_str()); + PSTR(PROJECT), SettingsText(SET_DEVICENAME), TasmotaGlobal.version, TasmotaGlobal.image_name, GetBuildDateAndTime().c_str()); #ifdef FIRMWARE_MINIMAL AddLog_P(LOG_LEVEL_INFO, PSTR(D_WARNING_MINIMAL_VERSION)); #endif // FIRMWARE_MINIMAL diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index 42cbf1af4..b43bc573f 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -376,9 +376,10 @@ struct WEB { } Web; // Helper function to avoid code duplication (saves 4k Flash) +// arg can be in PROGMEM static void WebGetArg(const char* arg, char* out, size_t max) { - String s = Webserver->arg(arg); + String s = Webserver->arg((const __FlashStringHelper *)arg); strlcpy(out, s.c_str(), max); // out[max-1] = '\0'; // Ensure terminating NUL } @@ -460,7 +461,7 @@ void StartWebserver(int type, IPAddress ipweb) WebServer_on(uri, line.handler, pgm_read_byte(&line.method)); } Webserver->onNotFound(HandleNotFound); - Webserver->on("/u2", HTTP_POST, HandleUploadDone, HandleUploadLoop); // this call requires 2 functions so we keep a direct call + Webserver->on(F("/u2"), HTTP_POST, HandleUploadDone, HandleUploadLoop); // this call requires 2 functions so we keep a direct call #ifndef FIRMWARE_MINIMAL XdrvCall(FUNC_WEB_ADD_HANDLER); XsnsCall(FUNC_WEB_ADD_HANDLER); @@ -691,7 +692,7 @@ void WSContentStart_P(const char* title, bool auth) WSContentBegin(200, CT_HTML); if (title != nullptr) { - WSContentSend_P(HTTP_HEADER1, D_HTML_LANGUAGE, SettingsText(SET_DEVICENAME), title); + WSContentSend_P(HTTP_HEADER1, PSTR(D_HTML_LANGUAGE), SettingsText(SET_DEVICENAME), title); } } @@ -767,7 +768,7 @@ void WSContentButton(uint32_t title_index) WSContentSend_P(PSTR("

"), GetTextIndexed(action, sizeof(action), title_index, kButtonAction), GetTextIndexed(confirm, sizeof(confirm), title_index, kButtonConfirm), - (!title_index) ? "rst" : "non", + (!title_index) ? PSTR("rst") : PSTR("non"), GetTextIndexed(title, sizeof(title), title_index, kButtonTitle)); } else { WSContentSend_P(PSTR("

"), @@ -866,8 +867,8 @@ void HandleWifiLogin(void) void WebSliderColdWarm(void) { WSContentSend_P(HTTP_MSG_SLIDER_GRADIENT, // Cold Warm - "a", // a - Unique HTML id - "#eff", "#f81", // 6500k in RGB (White) to 2500k in RGB (Warm Yellow) + PSTR("a"), // a - Unique HTML id + PSTR("#eff"), PSTR("#f81"), // 6500k in RGB (White) to 2500k in RGB (Warm Yellow) 1, // sl1 153, 500, // Range color temperature LightGetColorTemp(), @@ -879,17 +880,17 @@ void HandleRoot(void) { if (CaptivePortal()) { return; } // If captive portal redirect instead of displaying the page. - if (Webserver->hasArg("rst")) { + if (Webserver->hasArg(F("rst"))) { WebRestart(0); return; } if (WifiIsInManagerMode()) { #ifndef FIRMWARE_MINIMAL - if (strlen(SettingsText(SET_WEBPWD)) && !(Webserver->hasArg("USER1")) && !(Webserver->hasArg("PASS1")) && HTTP_MANAGER_RESET_ONLY != Web.state) { + if (strlen(SettingsText(SET_WEBPWD)) && !(Webserver->hasArg(F("USER1"))) && !(Webserver->hasArg(F("PASS1"))) && HTTP_MANAGER_RESET_ONLY != Web.state) { HandleWifiLogin(); } else { - if (!strlen(SettingsText(SET_WEBPWD)) || (((Webserver->arg("USER1") == WEB_USERNAME ) && (Webserver->arg("PASS1") == SettingsText(SET_WEBPWD) )) || HTTP_MANAGER_RESET_ONLY == Web.state)) { + if (!strlen(SettingsText(SET_WEBPWD)) || (((Webserver->arg(F("USER1")) == WEB_USERNAME ) && (Webserver->arg(F("PASS1")) == SettingsText(SET_WEBPWD) )) || HTTP_MANAGER_RESET_ONLY == Web.state)) { HandleWifiConfiguration(); } else { // wrong user and pass @@ -936,8 +937,8 @@ void HandleRoot(void) LightGetHSB(&hue, &sat, nullptr); WSContentSend_P(HTTP_MSG_SLIDER_GRADIENT, // Hue - "b", // b - Unique HTML id - "#800", PSTR("#f00 5%,#ff0 20%,#0f0 35%,#0ff 50%,#00f 65%,#f0f 80%,#f00 95%,#800"), // Hue colors + PSTR("b"), // b - Unique HTML id + PSTR("#800"), PSTR("#f00 5%,#ff0 20%,#0f0 35%,#0ff 50%,#00f 65%,#f0f 80%,#f00 95%,#800"), // Hue colors 2, // sl2 - Unique range HTML id - Used as source for Saturation end color 1, 359, // Range valid Hue hue, @@ -951,7 +952,7 @@ void HandleRoot(void) snprintf_P(stemp, sizeof(stemp), PSTR("#%02X%02X%02X"), red, green, blue); // Saturation end color WSContentSend_P(HTTP_MSG_SLIDER_GRADIENT, // Saturation - "s", // s - Unique HTML id related to eb('s').style.background='linear-gradient(to right,rgb('+sl+'%%,'+sl+'%%,'+sl+'%%),hsl('+eb('sl2').value+',100%%,50%%))'; + PSTR("s"), // s - Unique HTML id related to eb('s').style.background='linear-gradient(to right,rgb('+sl+'%%,'+sl+'%%,'+sl+'%%),hsl('+eb('sl2').value+',100%%,50%%))'; scolor, stemp, // Brightness to max current color 3, // sl3 - Unique range HTML id - Not used 0, 100, // Range 0 to 100% @@ -960,8 +961,8 @@ void HandleRoot(void) } WSContentSend_P(HTTP_MSG_SLIDER_GRADIENT, // Brightness - Black to White - "c", // c - Unique HTML id - "#000", "#fff", // Black to White + PSTR("c"), // c - Unique HTML id + PSTR("#000"), PSTR("#fff"), // Black to White 4, // sl4 - Unique range HTML id - Used as source for Saturation begin color Settings.flag3.slider_dimmer_stay_on, 100, // Range 0/1 to 100% (SetOption77 - Do not power off if slider moved to far left) Settings.light_dimmer, @@ -972,8 +973,8 @@ void HandleRoot(void) WebSliderColdWarm(); } WSContentSend_P(HTTP_MSG_SLIDER_GRADIENT, // White brightness - Black to White - "f", // f - Unique HTML id - "#000", "#fff", // Black to White + PSTR("f"), // f - Unique HTML id + PSTR("#000"), PSTR("#fff"), // Black to White 5, // sl5 - Unique range HTML id - Not used Settings.flag3.slider_dimmer_stay_on, 100, // Range 0/1 to 100% (SetOption77 - Do not power off if slider moved to far left) LightGetDimmer(2), @@ -987,7 +988,7 @@ void HandleRoot(void) WSContentSend_P(HTTP_MSG_SLIDER_GRADIENT, // Channel brightness - Black to White stemp, // e1 to e5 - Unique HTML id - "#000", "#fff", // Black to White + PSTR("#000"), PSTR("#fff"), // Black to White i+1, // sl1 to sl5 - Unique range HTML id - Not used 1, 100, // Range 1 to 100% changeUIntScale(Settings.light_color[i], 0, 255, 0, 100), @@ -1008,7 +1009,7 @@ void HandleRoot(void) #ifdef USE_SONOFF_IFAN if (IsModuleIfan()) { WSContentSend_P(HTTP_DEVICE_CONTROL, 36, 1, - (strlen(SettingsText(SET_BUTTON1))) ? SettingsText(SET_BUTTON1) : D_BUTTON_TOGGLE, + (strlen(SettingsText(SET_BUTTON1))) ? SettingsText(SET_BUTTON1) : PSTR(D_BUTTON_TOGGLE), ""); for (uint32_t i = 0; i < MaxFanspeed(); i++) { snprintf_P(stemp, sizeof(stemp), PSTR("%d"), i); @@ -1031,7 +1032,7 @@ void HandleRoot(void) #endif // USE_SHUTTER snprintf_P(stemp, sizeof(stemp), PSTR(" %d"), idx); WSContentSend_P(HTTP_DEVICE_CONTROL, 100 / TasmotaGlobal.devices_present, idx, - (set_button) ? SettingsText(SET_BUTTON1 + idx -1) : (TasmotaGlobal.devices_present < 5) ? D_BUTTON_TOGGLE : "", + (set_button) ? SettingsText(SET_BUTTON1 + idx -1) : (TasmotaGlobal.devices_present < 5) ? PSTR(D_BUTTON_TOGGLE) : "", (set_button) ? "" : (TasmotaGlobal.devices_present > 1) ? stemp : ""); } #ifdef USE_SONOFF_IFAN @@ -1107,7 +1108,7 @@ bool HandleRootStatusRefresh(void) char svalue[32]; // Command and number parameter char webindex[5]; // WebGetArg name - WebGetArg("o", tmp, sizeof(tmp)); // 1 - 16 Device number for button Toggle or Fanspeed + WebGetArg(PSTR("o"), tmp, sizeof(tmp)); // 1 - 16 Device number for button Toggle or Fanspeed if (strlen(tmp)) { ShowWebSource(SRC_WEBGUI); uint32_t device = atoi(tmp); @@ -1153,12 +1154,12 @@ bool HandleRootStatusRefresh(void) #endif // USE_TUYA_MCU } #ifdef USE_LIGHT - WebGetArg("d0", tmp, sizeof(tmp)); // 0 - 100 Dimmer value + WebGetArg(PSTR("d0"), tmp, sizeof(tmp)); // 0 - 100 Dimmer value if (strlen(tmp)) { snprintf_P(svalue, sizeof(svalue), PSTR(D_CMND_DIMMER " %s"), tmp); ExecuteWebCommand(svalue, SRC_WEBGUI); } - WebGetArg("w0", tmp, sizeof(tmp)); // 0 - 100 White value + WebGetArg(PSTR("w0"), tmp, sizeof(tmp)); // 0 - 100 White value if (strlen(tmp)) { snprintf_P(svalue, sizeof(svalue), PSTR(D_CMND_WHITE " %s"), tmp); ExecuteWebCommand(svalue, SRC_WEBGUI); @@ -1173,17 +1174,17 @@ bool HandleRootStatusRefresh(void) ExecuteWebCommand(svalue, SRC_WEBGUI); } } - WebGetArg("t0", tmp, sizeof(tmp)); // 153 - 500 Color temperature + WebGetArg(PSTR("t0"), tmp, sizeof(tmp)); // 153 - 500 Color temperature if (strlen(tmp)) { snprintf_P(svalue, sizeof(svalue), PSTR(D_CMND_COLORTEMPERATURE " %s"), tmp); ExecuteWebCommand(svalue, SRC_WEBGUI); } - WebGetArg("h0", tmp, sizeof(tmp)); // 0 - 359 Hue value + WebGetArg(PSTR("h0"), tmp, sizeof(tmp)); // 0 - 359 Hue value if (strlen(tmp)) { snprintf_P(svalue, sizeof(svalue), PSTR(D_CMND_HSBCOLOR "1 %s"), tmp); ExecuteWebCommand(svalue, SRC_WEBGUI); } - WebGetArg("n0", tmp, sizeof(tmp)); // 0 - 99 Saturation value + WebGetArg(PSTR("n0"), tmp, sizeof(tmp)); // 0 - 99 Saturation value if (strlen(tmp)) { snprintf_P(svalue, sizeof(svalue), PSTR(D_CMND_HSBCOLOR "2 %s"), tmp); ExecuteWebCommand(svalue, SRC_WEBGUI); @@ -1200,19 +1201,19 @@ bool HandleRootStatusRefresh(void) } #endif // USE_SHUTTER #ifdef USE_SONOFF_RF - WebGetArg("k", tmp, sizeof(tmp)); // 1 - 16 Pre defined RF keys + WebGetArg(PSTR("k"), tmp, sizeof(tmp)); // 1 - 16 Pre defined RF keys if (strlen(tmp)) { snprintf_P(svalue, sizeof(svalue), PSTR(D_CMND_RFKEY "%s"), tmp); ExecuteWebCommand(svalue, SRC_WEBGUI); } #endif // USE_SONOFF_RF #ifdef USE_ZIGBEE - WebGetArg("zbj", tmp, sizeof(tmp)); + WebGetArg(PSTR("zbj"), tmp, sizeof(tmp)); if (strlen(tmp)) { snprintf_P(svalue, sizeof(svalue), PSTR("ZbPermitJoin")); ExecuteWebCommand(svalue, SRC_WEBGUI); } - WebGetArg("zbr", tmp, sizeof(tmp)); + WebGetArg(PSTR("zbr"), tmp, sizeof(tmp)); if (strlen(tmp)) { snprintf_P(svalue, sizeof(svalue), PSTR("ZbMap")); ExecuteWebCommand(svalue, SRC_WEBGUI); @@ -1315,7 +1316,7 @@ void WSContentSendNiceLists(uint32_t option) { char stemp[30]; // Template number and Sensor name for (uint32_t i = 0; i < ARRAY_SIZE(kGpioNiceList); i++) { // GPIO: }2'0'>None (0)}3}2'17'>Button1 (17)}3... if (option && (1 == i)) { - WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, AGPIO(GPIO_USER), D_SENSOR_USER); // }2'255'>User}3 + WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, AGPIO(GPIO_USER), PSTR(D_SENSOR_USER)); // }2'255'>User}3 } uint32_t ridx = pgm_read_word(kGpioNiceList + i) & 0xFFE0; uint32_t midx = BGPIO(ridx); @@ -1356,7 +1357,7 @@ void WSContentSendAdcNiceList(uint32_t option) { WSContentSend_P(PSTR("os=\"")); for (uint32_t i = 0; i < ARRAY_SIZE(kAdcNiceList); i++) { // GPIO: }2'0'>None}3}2'17'>Analog}3... if (option && (1 == i)) { - WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, AGPIO(GPIO_USER), D_SENSOR_USER); // }2'15'>User}3 + WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, AGPIO(GPIO_USER), PSTR(D_SENSOR_USER)); // }2'15'>User}3 } uint32_t ridx = pgm_read_word(kAdcNiceList + i) & 0xFFE0; uint32_t midx = BGPIO(ridx); @@ -1372,7 +1373,7 @@ void HandleTemplateConfiguration(void) { if (!HttpCheckPriviledgedAccess()) { return; } - if (Webserver->hasArg("save")) { + if (Webserver->hasArg(F("save"))) { TemplateSaveSettings(); WebRestart(1); return; @@ -1380,7 +1381,7 @@ void HandleTemplateConfiguration(void) char stemp[30]; // Template number and Sensor name - WebGetArg("t", stemp, sizeof(stemp)); // 0 - 69 Template number + WebGetArg(PSTR("t"), stemp, sizeof(stemp)); // 0 - 69 Template number if (strlen(stemp)) { uint32_t module = atoi(stemp); uint32_t module_save = Settings.module; @@ -1474,7 +1475,7 @@ void TemplateSaveSettings(void) char tmp[TOPSZ]; // WebGetArg NAME and GPIO/BASE/FLAG byte value char svalue[300]; // Template command string - WebGetArg("s1", tmp, sizeof(tmp)); // NAME + WebGetArg(PSTR("s1"), tmp, sizeof(tmp)); // NAME snprintf_P(svalue, sizeof(svalue), PSTR(D_CMND_TEMPLATE " {\"" D_JSON_NAME "\":\"%s\",\"" D_JSON_GPIO "\":["), tmp); uint32_t j = 0; @@ -1492,7 +1493,7 @@ void TemplateSaveSettings(void) uint32_t state = Webserver->hasArg(webindex) << i; // FLAG flag += state; } - WebGetArg("g99", tmp, sizeof(tmp)); // BASE + WebGetArg(PSTR("g99"), tmp, sizeof(tmp)); // BASE uint32_t base = atoi(tmp) +1; snprintf_P(svalue, sizeof(svalue), PSTR("%s],\"" D_JSON_FLAG "\":%d,\"" D_JSON_BASE "\":%d}"), svalue, flag, base); @@ -1505,7 +1506,7 @@ void HandleModuleConfiguration(void) { if (!HttpCheckPriviledgedAccess()) { return; } - if (Webserver->hasArg("save")) { + if (Webserver->hasArg(F("save"))) { ModuleSaveSettings(); WebRestart(1); return; @@ -1572,7 +1573,7 @@ void ModuleSaveSettings(void) { char tmp[8]; // WebGetArg numbers only - WebGetArg("g99", tmp, sizeof(tmp)); + WebGetArg(PSTR("g99"), tmp, sizeof(tmp)); uint32_t new_module = (!strlen(tmp)) ? MODULE : atoi(tmp); Settings.last_module = Settings.module; Settings.module = new_module; @@ -1624,7 +1625,7 @@ void HandleWifiConfiguration(void) AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_CONFIGURE_WIFI)); - if (Webserver->hasArg("save") && HTTP_MANAGER_RESET_ONLY != Web.state) { + if (Webserver->hasArg(F("save")) && HTTP_MANAGER_RESET_ONLY != Web.state) { WifiSaveSettings(); WebRestart(2); return; @@ -1639,7 +1640,7 @@ void HandleWifiConfiguration(void) #endif // USE_ENHANCED_GUI_WIFI_SCAN if (HTTP_MANAGER_RESET_ONLY != Web.state) { - if (Webserver->hasArg("scan")) { + if (Webserver->hasArg(F("scan"))) { #ifdef USE_EMULATION UdpDisconnect(); #endif // USE_EMULATION @@ -1776,20 +1777,20 @@ void WifiSaveSettings(void) { char tmp[TOPSZ]; // Max length is currently 150 - WebGetArg("h", tmp, sizeof(tmp)); + WebGetArg(PSTR("h"), tmp, sizeof(tmp)); SettingsUpdateText(SET_HOSTNAME, (!strlen(tmp)) ? WIFI_HOSTNAME : tmp); if (strchr(SettingsText(SET_HOSTNAME), '%') != nullptr) { SettingsUpdateText(SET_HOSTNAME, WIFI_HOSTNAME); } - WebGetArg("c", tmp, sizeof(tmp)); + WebGetArg(PSTR("c"), tmp, sizeof(tmp)); SettingsUpdateText(SET_CORS, (!strlen(tmp)) ? CORS_DOMAIN : tmp); - WebGetArg("s1", tmp, sizeof(tmp)); + WebGetArg(PSTR("s1"), tmp, sizeof(tmp)); SettingsUpdateText(SET_STASSID1, (!strlen(tmp)) ? STA_SSID1 : tmp); - WebGetArg("s2", tmp, sizeof(tmp)); + WebGetArg(PSTR("s2"), tmp, sizeof(tmp)); SettingsUpdateText(SET_STASSID2, (!strlen(tmp)) ? STA_SSID2 : tmp); - WebGetArg("p1", tmp, sizeof(tmp)); + WebGetArg(PSTR("p1"), tmp, sizeof(tmp)); SettingsUpdateText(SET_STAPWD1, (!strlen(tmp)) ? "" : (strlen(tmp) < 5) ? SettingsText(SET_STAPWD1) : tmp); - WebGetArg("p2", tmp, sizeof(tmp)); + WebGetArg(PSTR("p2"), tmp, sizeof(tmp)); SettingsUpdateText(SET_STAPWD2, (!strlen(tmp)) ? "" : (strlen(tmp) < 5) ? SettingsText(SET_STAPWD2) : tmp); AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_WIFI D_CMND_HOSTNAME " %s, " D_CMND_SSID "1 %s, " D_CMND_SSID "2 %s, " D_CMND_CORS " %s"), SettingsText(SET_HOSTNAME), SettingsText(SET_STASSID1), SettingsText(SET_STASSID2), SettingsText(SET_CORS)); @@ -1839,19 +1840,19 @@ void LoggingSaveSettings(void) { char tmp[TOPSZ]; // Max length is currently 33 - WebGetArg("l0", tmp, sizeof(tmp)); + WebGetArg(PSTR("l0"), tmp, sizeof(tmp)); SetSeriallog((!strlen(tmp)) ? SERIAL_LOG_LEVEL : atoi(tmp)); - WebGetArg("l1", tmp, sizeof(tmp)); + WebGetArg(PSTR("l1"), tmp, sizeof(tmp)); Settings.weblog_level = (!strlen(tmp)) ? WEB_LOG_LEVEL : atoi(tmp); - WebGetArg("l2", tmp, sizeof(tmp)); + WebGetArg(PSTR("l2"), tmp, sizeof(tmp)); Settings.mqttlog_level = (!strlen(tmp)) ? MQTT_LOG_LEVEL : atoi(tmp); - WebGetArg("l3", tmp, sizeof(tmp)); + WebGetArg(PSTR("l3"), tmp, sizeof(tmp)); SetSyslog((!strlen(tmp)) ? SYS_LOG_LEVEL : atoi(tmp)); - WebGetArg("lh", tmp, sizeof(tmp)); + WebGetArg(PSTR("lh"), tmp, sizeof(tmp)); SettingsUpdateText(SET_SYSLOG_HOST, (!strlen(tmp)) ? SYS_LOG_HOST : tmp); - WebGetArg("lp", tmp, sizeof(tmp)); + WebGetArg(PSTR("lp"), tmp, sizeof(tmp)); Settings.syslog_port = (!strlen(tmp)) ? SYS_LOG_PORT : atoi(tmp); - WebGetArg("lt", tmp, sizeof(tmp)); + WebGetArg(PSTR("lt"), tmp, sizeof(tmp)); Settings.tele_period = (!strlen(tmp)) ? TELE_PERIOD : atoi(tmp); if ((Settings.tele_period > 0) && (Settings.tele_period < 10)) { Settings.tele_period = 10; // Do not allow periods < 10 seconds @@ -1868,7 +1869,7 @@ void HandleOtherConfiguration(void) AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_CONFIGURE_OTHER)); - if (Webserver->hasArg("save")) { + if (Webserver->hasArg(F("save"))) { OtherSaveSettings(); WebRestart(1); return; @@ -1932,15 +1933,15 @@ void OtherSaveSettings(void) char friendlyname[TOPSZ]; char message[MAX_LOGSZ]; - WebGetArg("dn", tmp, sizeof(tmp)); + WebGetArg(PSTR("dn"), tmp, sizeof(tmp)); SettingsUpdateText(SET_DEVICENAME, (!strlen(tmp)) ? "" : (!strcmp(tmp,"1")) ? SettingsText(SET_FRIENDLYNAME1) : tmp); - WebGetArg("wp", tmp, sizeof(tmp)); + WebGetArg(PSTR("wp"), tmp, sizeof(tmp)); SettingsUpdateText(SET_WEBPWD, (!strlen(tmp)) ? "" : (strchr(tmp,'*')) ? SettingsText(SET_WEBPWD) : tmp); - Settings.flag.mqtt_enabled = Webserver->hasArg("b1"); // SetOption3 - Enable MQTT + Settings.flag.mqtt_enabled = Webserver->hasArg(F("b1")); // SetOption3 - Enable MQTT #ifdef USE_EMULATION UdpDisconnect(); #if defined(USE_EMULATION_WEMO) || defined(USE_EMULATION_HUE) - WebGetArg("b2", tmp, sizeof(tmp)); + WebGetArg(PSTR("b2"), tmp, sizeof(tmp)); Settings.flag2.emulation = (!strlen(tmp)) ? 0 : atoi(tmp); #endif // USE_EMULATION_WEMO || USE_EMULATION_HUE #endif // USE_EMULATION @@ -1951,14 +1952,14 @@ void OtherSaveSettings(void) snprintf_P(webindex, sizeof(webindex), PSTR("a%d"), i); WebGetArg(webindex, tmp, sizeof(tmp)); snprintf_P(friendlyname, sizeof(friendlyname), PSTR(FRIENDLY_NAME"%d"), i +1); - SettingsUpdateText(SET_FRIENDLYNAME1 +i, (!strlen(tmp)) ? (i) ? friendlyname : FRIENDLY_NAME : tmp); + SettingsUpdateText(SET_FRIENDLYNAME1 +i, (!strlen(tmp)) ? (i) ? friendlyname : PSTR(FRIENDLY_NAME) : tmp); snprintf_P(message, sizeof(message), PSTR("%s%s %s"), message, (i) ? "," : "", SettingsText(SET_FRIENDLYNAME1 +i)); } AddLogData(LOG_LEVEL_INFO, message); - WebGetArg("t1", tmp, sizeof(tmp)); + WebGetArg(PSTR("t1"), tmp, sizeof(tmp)); if (strlen(tmp)) { // {"NAME":"12345678901234","GPIO":[255,255,255,255,255,255,255,255,255,255,255,255,255],"FLAG":255,"BASE":255} - snprintf_P(message, sizeof(message), PSTR(D_CMND_BACKLOG " " D_CMND_TEMPLATE " %s%s"), tmp, (Webserver->hasArg("t2")) ? "; " D_CMND_MODULE " 0" : ""); + snprintf_P(message, sizeof(message), PSTR(D_CMND_BACKLOG " " D_CMND_TEMPLATE " %s%s"), tmp, (Webserver->hasArg(F("t2"))) ? PSTR("; " D_CMND_MODULE " 0") : ""); ExecuteWebCommand(message, SRC_WEBGUI); } } @@ -2034,7 +2035,7 @@ void HandleRestoreConfiguration(void) WSContentStart_P(PSTR(D_RESTORE_CONFIGURATION)); WSContentSendStyle(); WSContentSend_P(HTTP_FORM_RST); - WSContentSend_P(HTTP_FORM_RST_UPG, D_RESTORE); + WSContentSend_P(HTTP_FORM_RST_UPG, PSTR(D_RESTORE)); if (WifiIsInManagerMode()) { WSContentSpaceButton(BUTTON_MAIN); } else { @@ -2249,7 +2250,7 @@ void HandleUpgradeFirmware(void) { WSContentStart_P(PSTR(D_FIRMWARE_UPGRADE)); WSContentSendStyle(); WSContentSend_P(HTTP_FORM_UPG, SettingsText(SET_OTAURL)); - WSContentSend_P(HTTP_FORM_RST_UPG, D_UPGRADE); + WSContentSend_P(HTTP_FORM_RST_UPG, PSTR(D_UPGRADE)); WSContentSpaceButton(BUTTON_MAIN); WSContentStop(); @@ -2265,7 +2266,7 @@ void HandleUpgradeFirmwareStart(void) { WifiConfigCounter(); char otaurl[TOPSZ]; - WebGetArg("o", otaurl, sizeof(otaurl)); + WebGetArg(PSTR("o"), otaurl, sizeof(otaurl)); if (strlen(otaurl)) { snprintf_P(command, sizeof(command), PSTR(D_CMND_OTAURL " %s"), otaurl); ExecuteWebCommand(command, SRC_WEBGUI); @@ -2668,9 +2669,9 @@ void HandleHttpCommand(void) if (!WebAuthenticate()) { // Prefer authorization via HTTP header (Basic auth), if it fails, use legacy method via GET parameters char tmp1[33]; - WebGetArg("user", tmp1, sizeof(tmp1)); + WebGetArg(PSTR("user"), tmp1, sizeof(tmp1)); char tmp2[strlen(SettingsText(SET_WEBPWD)) + 1]; - WebGetArg("password", tmp2, sizeof(tmp2)); + WebGetArg(PSTR("password"), tmp2, sizeof(tmp2)); if (!(!strcmp(tmp1, WEB_USERNAME) && !strcmp(tmp2, SettingsText(SET_WEBPWD)))) { WSContentBegin(401, CT_JSON); @@ -2681,7 +2682,7 @@ void HandleHttpCommand(void) } WSContentBegin(200, CT_JSON); - String svalue = Webserver->arg("cmnd"); + String svalue = Webserver->arg(F("cmnd")); if (svalue.length() && (svalue.length() < MQTT_MAX_PACKET_SIZE)) { uint32_t curridx = TasmotaGlobal.log_buffer_pointer; TasmotaGlobal.templog_level = LOG_LEVEL_INFO; @@ -2717,7 +2718,7 @@ void HandleConsole(void) { if (!HttpCheckPriviledgedAccess()) { return; } - if (Webserver->hasArg("c2")) { // Console refresh requested + if (Webserver->hasArg(F("c2"))) { // Console refresh requested HandleConsoleRefresh(); return; } @@ -2734,14 +2735,14 @@ void HandleConsole(void) void HandleConsoleRefresh(void) { - String svalue = Webserver->arg("c1"); + String svalue = Webserver->arg(F("c1")); if (svalue.length() && (svalue.length() < MQTT_MAX_PACKET_SIZE)) { AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_COMMAND "%s"), svalue.c_str()); ExecuteWebCommand((char*)svalue.c_str(), SRC_WEBCONSOLE); } char stmp[8]; - WebGetArg("c2", stmp, sizeof(stmp)); + WebGetArg(PSTR("c2"), stmp, sizeof(stmp)); uint32_t index = 0; // Initial start, dump all if (strlen(stmp)) { index = atoi(stmp); } @@ -2758,7 +2759,7 @@ void HandleConsoleRefresh(void) if (len > sizeof(TasmotaGlobal.mqtt_data) -2) { len = sizeof(TasmotaGlobal.mqtt_data); } char stemp[len +1]; strlcpy(stemp, line, len); - WSContentSend_P(PSTR("%s%s"), (cflg) ? "\n" : "", stemp); + WSContentSend_P(PSTR("%s%s"), (cflg) ? PSTR("\n") : "", stemp); cflg = true; } WSContentSend_P(PSTR("}1")); @@ -2776,14 +2777,14 @@ void HandleNotFound(void) #ifdef USE_EMULATION #ifdef USE_EMULATION_HUE String path = Webserver->uri(); - if ((EMUL_HUE == Settings.flag2.emulation) && (path.startsWith("/api"))) { + if ((EMUL_HUE == Settings.flag2.emulation) && (path.startsWith(F("/api")))) { HandleHueApi(&path); } else #endif // USE_EMULATION_HUE #endif // USE_EMULATION { WSContentBegin(404, CT_PLAIN); - WSContentSend_P(PSTR(D_FILE_NOT_FOUND "\n\nURI: %s\nMethod: %s\nArguments: %d\n"), Webserver->uri().c_str(), (Webserver->method() == HTTP_GET) ? "GET" : "POST", Webserver->args()); + WSContentSend_P(PSTR(D_FILE_NOT_FOUND "\n\nURI: %s\nMethod: %s\nArguments: %d\n"), Webserver->uri().c_str(), (Webserver->method() == HTTP_GET) ? PSTR("GET") : PSTR("POST"), Webserver->args()); for (uint32_t i = 0; i < Webserver->args(); i++) { WSContentSend_P(PSTR(" %s: %s\n"), Webserver->argName(i).c_str(), Webserver->arg(i).c_str()); } @@ -2978,7 +2979,7 @@ void CmndWebServer(void) } if (Settings.webserver) { Response_P(PSTR("{\"" D_CMND_WEBSERVER "\":\"" D_JSON_ACTIVE_FOR " %s " D_JSON_ON_DEVICE " %s " D_JSON_WITH_IP_ADDRESS " %s\"}"), - (2 == Settings.webserver) ? D_ADMIN : D_USER, NetworkHostname(), NetworkAddress().toString().c_str()); + (2 == Settings.webserver) ? PSTR(D_ADMIN) : PSTR(D_USER), NetworkHostname(), NetworkAddress().toString().c_str()); } else { ResponseCmndStateText(0); } diff --git a/tasmota/xdrv_02_mqtt.ino b/tasmota/xdrv_02_mqtt.ino index 6324a584b..bf4a9e6f9 100644 --- a/tasmota/xdrv_02_mqtt.ino +++ b/tasmota/xdrv_02_mqtt.ino @@ -156,7 +156,7 @@ void MqttInit(void) { // Detect AWS IoT and set default parameters String host = String(SettingsText(SET_MQTT_HOST)); - if (host.indexOf(".iot.") && host.endsWith(".amazonaws.com")) { // look for ".iot." and ".amazonaws.com" in the domain name + if (host.indexOf(F(".iot.")) && host.endsWith(F(".amazonaws.com"))) { // look for ".iot." and ".amazonaws.com" in the domain name Settings.flag4.mqtt_no_retain = true; } @@ -527,10 +527,10 @@ void MqttConnected(void) { if (Settings.webserver) { #if LWIP_IPV6 Response_P(PSTR("{\"" D_JSON_WEBSERVER_MODE "\":\"%s\",\"" D_CMND_HOSTNAME "\":\"%s\",\"" D_CMND_IPADDRESS "\":\"%s\",\"IPv6Address\":\"%s\"}"), - (2 == Settings.webserver) ? D_ADMIN : D_USER, NetworkHostname(), NetworkAddress().toString().c_str(), WifiGetIPv6().c_str()); + (2 == Settings.webserver) ? PSTR(D_ADMIN) : PSTR(D_USER), NetworkHostname(), NetworkAddress().toString().c_str(), WifiGetIPv6().c_str()); #else Response_P(PSTR("{\"" D_JSON_WEBSERVER_MODE "\":\"%s\",\"" D_CMND_HOSTNAME "\":\"%s\",\"" D_CMND_IPADDRESS "\":\"%s\"}"), - (2 == Settings.webserver) ? D_ADMIN : D_USER, NetworkHostname(), NetworkAddress().toString().c_str()); + (2 == Settings.webserver) ? PSTR(D_ADMIN) : PSTR(D_USER), NetworkHostname(), NetworkAddress().toString().c_str()); #endif // LWIP_IPV6 = 1 MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_INFO "2")); } @@ -787,7 +787,7 @@ void CmndMqttFingerprint(void) { void CmndMqttUser(void) { if (XdrvMailbox.data_len > 0) { - SettingsUpdateText(SET_MQTT_USER, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? MQTT_USER : XdrvMailbox.data); + SettingsUpdateText(SET_MQTT_USER, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? PSTR(MQTT_USER) : XdrvMailbox.data); TasmotaGlobal.restart_flag = 2; } ResponseCmndChar(SettingsText(SET_MQTT_USER)); @@ -795,7 +795,7 @@ void CmndMqttUser(void) { void CmndMqttPassword(void) { if (XdrvMailbox.data_len > 0) { - SettingsUpdateText(SET_MQTT_PWD, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? MQTT_PASS : XdrvMailbox.data); + SettingsUpdateText(SET_MQTT_PWD, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? PSTR(MQTT_PASS) : XdrvMailbox.data); ResponseCmndChar(SettingsText(SET_MQTT_PWD)); TasmotaGlobal.restart_flag = 2; } else { @@ -852,7 +852,7 @@ void CmndStateText(void) { void CmndMqttClient(void) { if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0)) { - SettingsUpdateText(SET_MQTT_CLIENT, (SC_DEFAULT == Shortcut()) ? MQTT_CLIENT_ID : XdrvMailbox.data); + SettingsUpdateText(SET_MQTT_CLIENT, (SC_DEFAULT == Shortcut()) ? PSTR(MQTT_CLIENT_ID) : XdrvMailbox.data); TasmotaGlobal.restart_flag = 2; } ResponseCmndChar(SettingsText(SET_MQTT_CLIENT)); @@ -882,7 +882,7 @@ void CmndPrefix(void) { if (XdrvMailbox.data_len > 0) { MakeValidMqtt(0, XdrvMailbox.data); SettingsUpdateText(SET_MQTTPREFIX1 + XdrvMailbox.index -1, - (SC_DEFAULT == Shortcut()) ? (1==XdrvMailbox.index) ? SUB_PREFIX : (2==XdrvMailbox.index) ? PUB_PREFIX : PUB_PREFIX2 : XdrvMailbox.data); + (SC_DEFAULT == Shortcut()) ? (1==XdrvMailbox.index) ? PSTR(SUB_PREFIX) : (2==XdrvMailbox.index) ? PSTR(PUB_PREFIX) : PSTR(PUB_PREFIX2) : XdrvMailbox.data); TasmotaGlobal.restart_flag = 2; } ResponseCmndIdxChar(SettingsText(SET_MQTTPREFIX1 + XdrvMailbox.index -1)); @@ -918,7 +918,7 @@ void CmndGroupTopic(void) { uint32_t settings_text_index = (1 == XdrvMailbox.index) ? SET_MQTT_GRP_TOPIC : SET_MQTT_GRP_TOPIC2 + XdrvMailbox.index - 2; MakeValidMqtt(0, XdrvMailbox.data); if (!strcmp(XdrvMailbox.data, TasmotaGlobal.mqtt_client)) { SetShortcutDefault(); } - SettingsUpdateText(settings_text_index, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? MQTT_GRPTOPIC : XdrvMailbox.data); + SettingsUpdateText(settings_text_index, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? PSTR(MQTT_GRPTOPIC) : XdrvMailbox.data); // Eliminate duplicates, have at least one and fill from index 1 char stemp[MAX_GROUP_TOPICS][TOPSZ]; @@ -940,7 +940,7 @@ void CmndGroupTopic(void) { } } if (0 == read_index) { - SettingsUpdateText(SET_MQTT_GRP_TOPIC, MQTT_GRPTOPIC); + SettingsUpdateText(SET_MQTT_GRP_TOPIC, PSTR(MQTT_GRPTOPIC)); } else { uint32_t write_index = 0; uint32_t real_index = SET_MQTT_GRP_TOPIC; @@ -998,7 +998,7 @@ void CmndSwitchTopic(void) { switch (Shortcut()) { case SC_CLEAR: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, ""); break; case SC_DEFAULT: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, TasmotaGlobal.mqtt_topic); break; - case SC_USER: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, MQTT_SWITCH_TOPIC); break; + case SC_USER: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, PSTR(MQTT_SWITCH_TOPIC)); break; default: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, XdrvMailbox.data); } } @@ -1266,7 +1266,7 @@ void HandleMqttConfiguration(void) AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_CONFIGURE_MQTT)); - if (Webserver->hasArg("save")) { + if (Webserver->hasArg(F("save"))) { MqttSaveSettings(); WebRestart(1); return; @@ -1282,11 +1282,11 @@ void HandleMqttConfiguration(void) #ifdef USE_MQTT_TLS Mqtt.mqtt_tls ? PSTR(" checked") : "", // SetOption102 - Enable MQTT TLS #endif // USE_MQTT_TLS - Format(str, MQTT_CLIENT_ID, sizeof(str)), MQTT_CLIENT_ID, SettingsText(SET_MQTT_CLIENT)); + Format(str, PSTR(MQTT_CLIENT_ID), sizeof(str)), PSTR(MQTT_CLIENT_ID), SettingsText(SET_MQTT_CLIENT)); WSContentSend_P(HTTP_FORM_MQTT2, (!strlen(SettingsText(SET_MQTT_USER))) ? "0" : SettingsText(SET_MQTT_USER), - Format(str, MQTT_TOPIC, sizeof(str)), MQTT_TOPIC, SettingsText(SET_MQTT_TOPIC), - MQTT_FULLTOPIC, MQTT_FULLTOPIC, SettingsText(SET_MQTT_FULLTOPIC)); + Format(str, PSTR(MQTT_TOPIC), sizeof(str)), PSTR(MQTT_TOPIC), SettingsText(SET_MQTT_TOPIC), + PSTR(MQTT_FULLTOPIC), PSTR(MQTT_FULLTOPIC), SettingsText(SET_MQTT_FULLTOPIC)); WSContentSend_P(HTTP_FORM_END); WSContentSpaceButton(BUTTON_CONFIGURATION); WSContentStop(); @@ -1298,10 +1298,10 @@ void MqttSaveSettings(void) char stemp[TOPSZ]; char stemp2[TOPSZ]; - WebGetArg("mt", tmp, sizeof(tmp)); + WebGetArg(PSTR("mt"), tmp, sizeof(tmp)); strlcpy(stemp, (!strlen(tmp)) ? MQTT_TOPIC : tmp, sizeof(stemp)); MakeValidMqtt(0, stemp); - WebGetArg("mf", tmp, sizeof(tmp)); + WebGetArg(PSTR("mf"), tmp, sizeof(tmp)); strlcpy(stemp2, (!strlen(tmp)) ? MQTT_FULLTOPIC : tmp, sizeof(stemp2)); MakeValidMqtt(1, stemp2); if ((strcmp(stemp, SettingsText(SET_MQTT_TOPIC))) || (strcmp(stemp2, SettingsText(SET_MQTT_FULLTOPIC)))) { @@ -1310,18 +1310,18 @@ void MqttSaveSettings(void) } SettingsUpdateText(SET_MQTT_TOPIC, stemp); SettingsUpdateText(SET_MQTT_FULLTOPIC, stemp2); - WebGetArg("mh", tmp, sizeof(tmp)); - SettingsUpdateText(SET_MQTT_HOST, (!strlen(tmp)) ? MQTT_HOST : (!strcmp(tmp,"0")) ? "" : tmp); - WebGetArg("ml", tmp, sizeof(tmp)); + WebGetArg(PSTR("mh"), tmp, sizeof(tmp)); + SettingsUpdateText(SET_MQTT_HOST, (!strlen(tmp)) ? PSTR(MQTT_HOST) : (!strcmp(tmp,"0")) ? "" : tmp); + WebGetArg(PSTR("ml"), tmp, sizeof(tmp)); Settings.mqtt_port = (!strlen(tmp)) ? MQTT_PORT : atoi(tmp); #ifdef USE_MQTT_TLS - Settings.flag4.mqtt_tls = Webserver->hasArg("b3"); // SetOption102 - Enable MQTT TLS + Settings.flag4.mqtt_tls = Webserver->hasArg(F("b3")); // SetOption102 - Enable MQTT TLS #endif - WebGetArg("mc", tmp, sizeof(tmp)); - SettingsUpdateText(SET_MQTT_CLIENT, (!strlen(tmp)) ? MQTT_CLIENT_ID : tmp); - WebGetArg("mu", tmp, sizeof(tmp)); - SettingsUpdateText(SET_MQTT_USER, (!strlen(tmp)) ? MQTT_USER : (!strcmp(tmp,"0")) ? "" : tmp); - WebGetArg("mp", tmp, sizeof(tmp)); + WebGetArg(PSTR("mc"), tmp, sizeof(tmp)); + SettingsUpdateText(SET_MQTT_CLIENT, (!strlen(tmp)) ? PSTR(MQTT_CLIENT_ID) : tmp); + WebGetArg(PSTR("mu"), tmp, sizeof(tmp)); + SettingsUpdateText(SET_MQTT_USER, (!strlen(tmp)) ? PSTR(MQTT_USER) : (!strcmp(tmp,"0")) ? "" : tmp); + WebGetArg(PSTR("mp"), tmp, sizeof(tmp)); SettingsUpdateText(SET_MQTT_PWD, (!strlen(tmp)) ? "" : (!strcmp(tmp, D_ASTERISK_PWD)) ? SettingsText(SET_MQTT_PWD) : tmp); AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT D_CMND_MQTTHOST " %s, " D_CMND_MQTTPORT " %d, " D_CMND_MQTTCLIENT " %s, " D_CMND_MQTTUSER " %s, " D_CMND_TOPIC " %s, " D_CMND_FULLTOPIC " %s"), SettingsText(SET_MQTT_HOST), Settings.mqtt_port, SettingsText(SET_MQTT_CLIENT), SettingsText(SET_MQTT_USER), SettingsText(SET_MQTT_TOPIC), SettingsText(SET_MQTT_FULLTOPIC)); diff --git a/tasmota/xdrv_04_light.ino b/tasmota/xdrv_04_light.ino index 90d94ffb7..7e0907dd1 100644 --- a/tasmota/xdrv_04_light.ino +++ b/tasmota/xdrv_04_light.ino @@ -2625,7 +2625,7 @@ void CmndWakeup(void) Light.wakeup_active = 3; Settings.light_scheme = LS_WAKEUP; LightPowerOn(); - ResponseCmndChar(D_JSON_STARTED); + ResponseCmndChar(PSTR(D_JSON_STARTED)); } void CmndColorTemperature(void) diff --git a/tasmota/xdrv_07_domoticz.ino b/tasmota/xdrv_07_domoticz.ino index 82b769963..b982a58b1 100644 --- a/tasmota/xdrv_07_domoticz.ino +++ b/tasmota/xdrv_07_domoticz.ino @@ -546,7 +546,7 @@ void HandleDomoticzConfiguration(void) { AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_CONFIGURE_DOMOTICZ)); - if (Webserver->hasArg("save")) { + if (Webserver->hasArg(F("save"))) { DomoticzSaveSettings(); WebRestart(1); return; @@ -605,7 +605,7 @@ void DomoticzSaveSettings(void) { Settings.domoticz_sensor_idx[i] = (!strlen(tmp)) ? 0 : atoi(tmp); snprintf_P(ssensor_indices, sizeof(ssensor_indices), PSTR("%s%s%d"), ssensor_indices, (strlen(ssensor_indices)) ? "," : "", Settings.domoticz_sensor_idx[i]); } - WebGetArg("ut", tmp, sizeof(tmp)); + WebGetArg(PSTR("ut"), tmp, sizeof(tmp)); Settings.domoticz_update_timer = (!strlen(tmp)) ? DOMOTICZ_UPDATE_TIMER : atoi(tmp); AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_DOMOTICZ D_CMND_IDX " %d,%d,%d,%d, " D_CMND_KEYIDX " %d,%d,%d,%d, " D_CMND_SWITCHIDX " %d,%d,%d,%d, " D_CMND_SENSORIDX " %s, " D_CMND_UPDATETIMER " %d"), diff --git a/tasmota/xdrv_09_timers.ino b/tasmota/xdrv_09_timers.ino index 0f6292e13..a33204906 100644 --- a/tasmota/xdrv_09_timers.ino +++ b/tasmota/xdrv_09_timers.ino @@ -841,7 +841,7 @@ void HandleTimerConfiguration(void) AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_CONFIGURE_TIMER)); - if (Webserver->hasArg("save")) { + if (Webserver->hasArg(F("save"))) { TimerSaveSettings(); HandleConfiguration(); return; @@ -883,8 +883,8 @@ void TimerSaveSettings(void) char message[32 + (MAX_TIMERS *11)]; // MQT: Timers 0,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000 Timer timer; - Settings.flag3.timers_enable = Webserver->hasArg("e0"); // CMND_TIMERS - WebGetArg("t0", tmp, sizeof(tmp)); + Settings.flag3.timers_enable = Webserver->hasArg(F("e0")); // CMND_TIMERS + WebGetArg(PSTR("t0"), tmp, sizeof(tmp)); char *p = tmp; snprintf_P(message, sizeof(message), PSTR(D_LOG_MQTT D_CMND_TIMERS " %d"), Settings.flag3.timers_enable); // CMND_TIMERS for (uint32_t i = 0; i < MAX_TIMERS; i++) { diff --git a/tasmota/xdrv_10_rules.ino b/tasmota/xdrv_10_rules.ino index 637a3b963..31b8e9f46 100644 --- a/tasmota/xdrv_10_rules.ino +++ b/tasmota/xdrv_10_rules.ino @@ -416,7 +416,7 @@ bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule, bool stop_all // Step1: Analyse rule String rule_expr = rule; // "TELE-INA219#CURRENT>0.100" if (Rules.teleperiod) { - int ppos = rule_expr.indexOf("TELE-"); // "TELE-INA219#CURRENT>0.100" or "INA219#CURRENT>0.100" + int ppos = rule_expr.indexOf(F("TELE-")); // "TELE-INA219#CURRENT>0.100" or "INA219#CURRENT>0.100" if (ppos == -1) { return false; } // No pre-amble in rule rule_expr = rule.substring(5); // "INA219#CURRENT>0.100" or "SYSTEM#BOOT" } @@ -494,7 +494,7 @@ bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule, bool stop_all // Step2: Search rule_name int pos; int rule_name_idx = 0; - if ((pos = rule_name.indexOf("[")) > 0) { // "SUBTYPE1#CURRENT[1]" + if ((pos = rule_name.indexOf(F("["))) > 0) { // "SUBTYPE1#CURRENT[1]" rule_name_idx = rule_name.substring(pos +1).toInt(); if ((rule_name_idx < 1) || (rule_name_idx > 6)) { // Allow indexes 1 to 6 rule_name_idx = 1; @@ -515,7 +515,7 @@ bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule, bool stop_all } String subtype; uint32_t i = 0; - while ((pos = rule_name.indexOf("#")) > 0) { // "SUBTYPE1#SUBTYPE2#CURRENT" + while ((pos = rule_name.indexOf(F("#"))) > 0) { // "SUBTYPE1#SUBTYPE2#CURRENT" subtype = rule_name.substring(0, pos); obj = obj[subtype.c_str()].getObject(); if (!obj) { return false; } // not found @@ -686,14 +686,14 @@ bool RuleSetProcess(uint8_t rule_set, String &event_saved) String rule = rules; rule.toUpperCase(); // "ON INA219#CURRENT>0.100 DO BACKLOG DIMMER 10;COLOR 100000 ENDON" - if (!rule.startsWith("ON ")) { return serviced; } // Bad syntax - Nothing to start on + if (!rule.startsWith(F("ON "))) { return serviced; } // Bad syntax - Nothing to start on - int pevt = rule.indexOf(" DO "); + int pevt = rule.indexOf(F(" DO ")); if (pevt == -1) { return serviced; } // Bad syntax - Nothing to do String event_trigger = rule.substring(3, pevt); // "INA219#CURRENT>0.100" - plen = rule.indexOf(" ENDON"); - plen2 = rule.indexOf(" BREAK"); + plen = rule.indexOf(F(" ENDON")); + plen2 = rule.indexOf(F(" BREAK")); if ((plen == -1) && (plen2 == -1)) { return serviced; } // Bad syntax - No ENDON neither BREAK if (plen == -1) { plen = 9999; } @@ -717,10 +717,10 @@ bool RuleSetProcess(uint8_t rule_set, String &event_saved) // if (!ucommand.startsWith("BACKLOG")) { commands = "backlog " + commands; } // Always use Backlog to prevent power race exception // Use Backlog with event to prevent rule event loop exception unless IF is used which uses an implicit backlog - if ((ucommand.indexOf("IF ") == -1) && - (ucommand.indexOf("EVENT ") != -1) && - (ucommand.indexOf("BACKLOG ") == -1)) { - commands = "backlog " + commands; + if ((ucommand.indexOf(F("IF ")) == -1) && + (ucommand.indexOf(F("EVENT ")) != -1) && + (ucommand.indexOf(F("BACKLOG ")) == -1)) { + commands = F("backlog ") + commands; } RulesVarReplace(commands, F("%VALUE%"), Rules.event_value); diff --git a/tasmota/xdrv_12_home_assistant.ino b/tasmota/xdrv_12_home_assistant.ino index bf4664886..0120615e0 100644 --- a/tasmota/xdrv_12_home_assistant.ino +++ b/tasmota/xdrv_12_home_assistant.ino @@ -294,7 +294,7 @@ void NewHAssDiscovery(void) for (uint32_t i = 0; i < MAX_FRIENDLYNAMES; i++) { char fname[TOPSZ]; snprintf_P(fname, sizeof(fname), PSTR("\"%s\""), EscapeJSONString(SettingsText(SET_FRIENDLYNAME1 +i)).c_str()); - snprintf_P(stemp2, sizeof(stemp2), PSTR("%s%s%s"), stemp2, (i > 0 ? "," : ""), (i < maxfn) ? fname : "null"); + snprintf_P(stemp2, sizeof(stemp2), PSTR("%s%s%s"), stemp2, (i > 0 ? "," : ""), (i < maxfn) ? fname : PSTR("null")); } stemp3[0] = '\0'; @@ -304,7 +304,7 @@ void NewHAssDiscovery(void) char sname[TOPSZ]; snprintf_P(sname, sizeof(sname), PSTR("\"%s\""), GetSwitchText(i).c_str()); snprintf_P(stemp3, sizeof(stemp3), PSTR("%s%s%d"), stemp3, (i > 0 ? "," : ""), (PinUsed(GPIO_SWT1, i) & Settings.flag5.mqtt_switches) ? Settings.switchmode[i] : -1); - snprintf_P(stemp4, sizeof(stemp4), PSTR("%s%s%s"), stemp4, (i > 0 ? "," : ""), (PinUsed(GPIO_SWT1, i) & Settings.flag5.mqtt_switches) ? sname : "null"); + snprintf_P(stemp4, sizeof(stemp4), PSTR("%s%s%s"), stemp4, (i > 0 ? "," : ""), (PinUsed(GPIO_SWT1, i) & Settings.flag5.mqtt_switches) ? sname : PSTR("null")); } stemp5[0] = '\0'; @@ -338,7 +338,7 @@ void NewHAssDiscovery(void) if (!Settings.flag.hass_discovery) { // HassDiscoveryRelays(relays) Response_P(HASS_DISCOVER_DEVICE, WiFi.localIP().toString().c_str(), SettingsText(SET_DEVICENAME), stemp2, TasmotaGlobal.hostname, unique_id, ModuleName().c_str(), TuyaMod, iFanMod, GetStateText(0), GetStateText(1), GetStateText(2), GetStateText(3), - TasmotaGlobal.version, TasmotaGlobal.mqtt_topic, SettingsText(SET_MQTT_FULLTOPIC), SUB_PREFIX, PUB_PREFIX, PUB_PREFIX2, Hass.RelLst, stemp3, stemp4, + TasmotaGlobal.version, TasmotaGlobal.mqtt_topic, SettingsText(SET_MQTT_FULLTOPIC), PSTR(SUB_PREFIX), PSTR(PUB_PREFIX), PSTR(PUB_PREFIX2), Hass.RelLst, stemp3, stemp4, stemp5, Settings.flag.mqtt_response, Settings.flag.button_swap, Settings.flag.button_single, Settings.flag.decimal_text, Settings.flag.not_power_linked, Settings.flag.hass_light, Settings.flag3.pwm_multi_channels, Settings.flag3.mqtt_buttons, Settings.flag4.alexa_ct_range, Settings.flag5.mqtt_switches, Settings.flag5.fade_fixed_duration, light_controller.isCTRGBLinked(), Light.subtype, stemp6); diff --git a/tasmota/xdrv_16_tuyamcu.ino b/tasmota/xdrv_16_tuyamcu.ino index a291d2e11..7535d29c5 100644 --- a/tasmota/xdrv_16_tuyamcu.ino +++ b/tasmota/xdrv_16_tuyamcu.ino @@ -1295,7 +1295,7 @@ void TuyaSensorsShow(bool json) GetTextIndexed(sname, sizeof(sname), (sensor-71), kTuyaSensors); ResponseAppend_P(PSTR("\"%s\":%s"), sname, - (Tuya.SensorsValid[sensor-71] ? dtostrfd(Tuya.Sensors[sensor-71], res, tempval) : "null")); + (Tuya.SensorsValid[sensor-71] ? dtostrfd(Tuya.Sensors[sensor-71], res, tempval) : PSTR("null"))); added = true; } #ifdef USE_WEBSERVER diff --git a/tasmota/xdrv_20_hue.ino b/tasmota/xdrv_20_hue.ino index 4da625847..013e22a98 100644 --- a/tasmota/xdrv_20_hue.ino +++ b/tasmota/xdrv_20_hue.ino @@ -429,9 +429,9 @@ void HandleUpnpSetupHue(void) { AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_HUE_BRIDGE_SETUP)); String description_xml = Decompress(HUE_DESCRIPTION_XML_COMPRESSED,HUE_DESCRIPTION_XML_SIZE); - description_xml.replace("{x1", WiFi.localIP().toString()); - description_xml.replace("{x2", HueUuid()); - description_xml.replace("{x3", HueSerialnumber()); + description_xml.replace(F("{x1"), WiFi.localIP().toString()); + description_xml.replace(F("{x2"), HueUuid()); + description_xml.replace(F("{x3"), HueSerialnumber()); WSSend(200, CT_XML, description_xml); } @@ -439,19 +439,19 @@ void HueNotImplemented(String *path) { AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_HTTP D_HUE_API_NOT_IMPLEMENTED " (%s)"), path->c_str()); - WSSend(200, CT_JSON, "{}"); + WSSend(200, CT_JSON, PSTR("{}")); } void HueConfigResponse(String *response) { *response += Decompress(HueConfigResponse_JSON, HueConfigResponse_JSON_SIZE); - response->replace("{ma", WiFi.macAddress()); - response->replace("{ip", WiFi.localIP().toString()); - response->replace("{ms", WiFi.subnetMask().toString()); - response->replace("{gw", WiFi.gatewayIP().toString()); - response->replace("{br", HueBridgeId()); - response->replace("{dt", GetDateAndTime(DT_UTC)); - response->replace("{id", GetHueUserId()); + response->replace(F("{ma"), WiFi.macAddress()); + response->replace(F("{ip"), WiFi.localIP().toString()); + response->replace(F("{ms"), WiFi.subnetMask().toString()); + response->replace(F("{gw"), WiFi.gatewayIP().toString()); + response->replace(F("{br"), HueBridgeId()); + response->replace(F("{dt"), GetDateAndTime(DT_UTC)); + response->replace(F("{id"), GetHueUserId()); } void HueConfig(String *path) @@ -534,13 +534,13 @@ void HueLightStatus1(uint8_t device, String *response) char * buf = (char*) malloc(buf_size); // temp buffer for strings, avoid stack UnishoxStrings msg(HUE_LIGHTS); - snprintf_P(buf, buf_size, PSTR("{\"on\":%s,"), (TasmotaGlobal.power & (1 << (device-1))) ? "true" : "false"); + snprintf_P(buf, buf_size, PSTR("{\"on\":%s,"), (TasmotaGlobal.power & (1 << (device-1))) ? PSTR("true") : PSTR("false")); // Brightness for all devices with PWM if ((1 == echo_gen) || (LST_SINGLE <= local_light_subtype)) { // force dimmer for 1st gen Echo snprintf_P(buf, buf_size, PSTR("%s\"bri\":%d,"), buf, bri); } if (LST_COLDWARM <= local_light_subtype) { - snprintf_P(buf, buf_size, PSTR("%s\"colormode\":\"%s\","), buf, g_gotct ? "ct" : "hs"); + snprintf_P(buf, buf_size, PSTR("%s\"colormode\":\"%s\","), buf, g_gotct ? PSTR("ct") : PSTR("hs")); } if (LST_RGB <= local_light_subtype) { // colors if (prev_x_str[0] && prev_y_str[0]) { @@ -680,7 +680,7 @@ void HueGlobalConfig(String *path) { #endif // USE_ZIGBEE response += F("},\"groups\":{},\"schedules\":{},\"config\":"); HueConfigResponse(&response); - response += "}"; + response += F("}"); WSSend(200, CT_JSON, response); } @@ -700,7 +700,7 @@ void CheckHue(String * response, bool &appending) { for (uint32_t i = 1; i <= maxhue; i++) { if (HueActive(i)) { if (appending) { *response += ","; } - *response += "\""; + *response += F("\""); *response += EncodeLightId(i); *response += F("\":{\"state\":"); HueLightStatus1(i, response); @@ -735,7 +735,7 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { on = hue_on.getBool(); snprintf_P(buf, buf_size, msg[HUE_RESP_ON], - device_id, on ? "true" : "false"); + device_id, on ? PSTR("true") : PSTR("false")); #ifdef USE_SHUTTER if (ShutterState(device)) { @@ -778,7 +778,7 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { if (resp) { response += ","; } snprintf_P(buf, buf_size, msg[HUE_RESP_NUM], - device_id, "bri", bri); + device_id, PSTR("bri"), bri); response += buf; if (LST_SINGLE <= Light.subtype) { // extend bri value if set to max @@ -824,7 +824,7 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { if (resp) { response += ","; } snprintf_P(buf, buf_size, msg[HUE_RESP_NUM], - device_id, "hue", hue); + device_id, PSTR("hue"), hue); response += buf; if (LST_RGB <= Light.subtype) { // change range from 0..65535 to 0..360 @@ -843,7 +843,7 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { if (resp) { response += ","; } snprintf_P(buf, buf_size, msg[HUE_RESP_NUM], - device_id, "sat", sat); + device_id, PSTR("sat"), sat); response += buf; if (LST_RGB <= Light.subtype) { // extend sat value if set to max @@ -862,7 +862,7 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { if (resp) { response += ","; } snprintf_P(buf, buf_size, msg[HUE_RESP_NUM], - device_id, "ct", ct); + device_id, PSTR("ct"), ct); response += buf; if ((LST_COLDWARM == Light.subtype) || (LST_RGBW <= Light.subtype)) { g_gotct = true; @@ -923,7 +923,7 @@ void HueLights(String *path) path->remove(0,path->indexOf(F("/lights"))); // Remove until /lights if (path->endsWith(F("/lights"))) { // Got /lights - response = "{"; + response = F("{"); bool appending = false; #ifdef USE_LIGHT CheckHue(&response, appending); @@ -934,7 +934,7 @@ void HueLights(String *path) #ifdef USE_SCRIPT_HUE Script_Check_Hue(&response); #endif - response += "}"; + response += F("}"); } else if (path->endsWith(F("/state"))) { // Got ID/state path->remove(0,8); // Remove /lights/ @@ -992,7 +992,7 @@ void HueLights(String *path) #endif // USE_LIGHT } else { - response = "{}"; + response = F("{}"); code = 406; } exit: @@ -1005,24 +1005,24 @@ void HueGroups(String *path) /* * http://tasmota/api/username/groups?1={"name":"Woonkamer","lights":[],"type":"Room","class":"Living room"}) */ - String response = "{}"; + String response(F("{}")); uint8_t maxhue = (TasmotaGlobal.devices_present > MAX_HUE_DEVICES) ? MAX_HUE_DEVICES : TasmotaGlobal.devices_present; //AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_HTTP D_HUE " HueGroups (%s)"), path->c_str()); - if (path->endsWith("/0")) { + if (path->endsWith(F("/0"))) { UnishoxStrings msg(HUE_LIGHTS); response = msg[HUE_GROUP0_STATUS_JSON]; String lights = F("\"1\""); for (uint32_t i = 2; i <= maxhue; i++) { - lights += ",\""; + lights += F(",\""); lights += EncodeLightId(i); - lights += "\""; + lights += F("\""); } #ifdef USE_ZIGBEE ZigbeeHueGroups(&response); #endif // USE_ZIGBEE - response.replace("{l1", lights); + response.replace(F("{l1"), lights); #ifdef USE_LIGHT HueLightStatus1(1, &response); #endif // USE_LIGHT diff --git a/tasmota/xdrv_23_zigbee_1z_libs.ino b/tasmota/xdrv_23_zigbee_1z_libs.ino index 4e018a21e..a228798e2 100644 --- a/tasmota/xdrv_23_zigbee_1z_libs.ino +++ b/tasmota/xdrv_23_zigbee_1z_libs.ino @@ -579,7 +579,7 @@ String Z_attribute::toString(bool prefix_comma) const { // value part switch (type) { case Za_type::Za_none: - res += "null"; + res += F("null"); break; case Za_type::Za_bool: res += val.uval32 ? F("true") : F("false"); @@ -638,7 +638,9 @@ String Z_attribute::toString(bool prefix_comma) const { if (val.arrval) { res += val.arrval->toString(); } else { - res += "[]"; + // res += '['; + // res += ']'; + res += F("[]"); } break; } diff --git a/tasmota/xdrv_23_zigbee_2a_devices_impl.ino b/tasmota/xdrv_23_zigbee_2a_devices_impl.ino index 7d850ef5a..55a98a748 100644 --- a/tasmota/xdrv_23_zigbee_2a_devices_impl.ino +++ b/tasmota/xdrv_23_zigbee_2a_devices_impl.ino @@ -556,7 +556,7 @@ void Z_Device::jsonPublishAttrList(const char * json_prefix, const Z_attribute_l if (Settings.flag5.zb_received_as_subtopic) GetTopic_P(stopic, TELE, subtopic, json_prefix); else - GetTopic_P(stopic, TELE, subtopic, D_RSLT_SENSOR); + GetTopic_P(stopic, TELE, subtopic, PSTR(D_RSLT_SENSOR)); MqttPublish(stopic, Settings.flag.mqtt_sensor_retain); } else { MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); diff --git a/tasmota/xdrv_23_zigbee_3_hue.ino b/tasmota/xdrv_23_zigbee_3_hue.ino index a77c8b67f..d71a8c3a9 100644 --- a/tasmota/xdrv_23_zigbee_3_hue.ino +++ b/tasmota/xdrv_23_zigbee_3_hue.ino @@ -62,13 +62,13 @@ void HueLightStatus1Zigbee(uint16_t shortaddr, uint8_t local_light_subtype, Stri const size_t buf_size = 256; char * buf = (char*) malloc(buf_size); // temp buffer for strings, avoid stack - snprintf_P(buf, buf_size, PSTR("{\"on\":%s,"), power ? "true" : "false"); + snprintf_P(buf, buf_size, PSTR("{\"on\":%s,"), power ? PSTR("true") : PSTR("false")); // Brightness for all devices with PWM if ((1 == echo_gen) || (LST_SINGLE <= local_light_subtype)) { // force dimmer for 1st gen Echo snprintf_P(buf, buf_size, PSTR("%s\"bri\":%d,"), buf, bri); } if (LST_COLDWARM <= local_light_subtype) { - snprintf_P(buf, buf_size, PSTR("%s\"colormode\":\"%s\","), buf, (0 == colormode) ? "hs" : (1 == colormode) ? "xy" : "ct"); + snprintf_P(buf, buf_size, PSTR("%s\"colormode\":\"%s\","), buf, (0 == colormode) ? PSTR("hs") : (1 == colormode) ? PSTR("xy") : PSTR("ct")); } if (LST_RGB <= local_light_subtype) { // colors if (prev_x_str[0] && prev_y_str[0]) { @@ -83,7 +83,7 @@ void HueLightStatus1Zigbee(uint16_t shortaddr, uint8_t local_light_subtype, Stri if (LST_COLDWARM == local_light_subtype || LST_RGBW <= local_light_subtype) { // white temp snprintf_P(buf, buf_size, PSTR("%s\"ct\":%d,"), buf, ct > 0 ? ct : 284); } - snprintf_P(buf, buf_size, HUE_LIGHTS_STATUS_JSON1_SUFFIX_ZIGBEE, buf, reachable ? "true" : "false"); + snprintf_P(buf, buf_size, HUE_LIGHTS_STATUS_JSON1_SUFFIX_ZIGBEE, buf, reachable ? PSTR("true") : PSTR("false")); *response += buf; free(buf); @@ -233,7 +233,7 @@ void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) { on = hue_on.getBool(); snprintf_P(buf, buf_size, msg[HUE_RESP_ON], - device_id, on ? "true" : "false"); + device_id, on ? PSTR("true") : PSTR("false")); if (on) { ZigbeeHuePower(shortaddr, 0x01); @@ -252,7 +252,7 @@ void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) { if (resp) { response += ","; } snprintf_P(buf, buf_size, msg[HUE_RESP_NUM], - device_id, "bri", bri); + device_id, PSTR("bri"), bri); response += buf; if (LST_SINGLE <= bulbtype) { // extend bri value if set to max @@ -293,7 +293,7 @@ void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) { if (resp) { response += ","; } snprintf_P(buf, buf_size, msg[HUE_RESP_NUM], - device_id, "hue", hue); + device_id, PSTR("hue"), hue); response += buf; if (LST_RGB <= bulbtype) { // change range from 0..65535 to 0..360 @@ -311,7 +311,7 @@ void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) { if (resp) { response += ","; } snprintf_P(buf, buf_size, msg[HUE_RESP_NUM], - device_id, "sat", sat); + device_id, PSTR("sat"), sat); response += buf; if (LST_RGB <= bulbtype) { // extend sat value if set to max @@ -332,7 +332,7 @@ void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) { if (resp) { response += ","; } snprintf_P(buf, buf_size, msg[HUE_RESP_NUM], - device_id, "ct", ct); + device_id, PSTR("ct"), ct); response += buf; if ((LST_COLDWARM == bulbtype) || (LST_RGBW <= bulbtype)) { ZigbeeHueCT(shortaddr, ct); diff --git a/tasmota/xdrv_23_zigbee_A_impl.ino b/tasmota/xdrv_23_zigbee_A_impl.ino index ec9a454ac..673f1ff8d 100644 --- a/tasmota/xdrv_23_zigbee_A_impl.ino +++ b/tasmota/xdrv_23_zigbee_A_impl.ino @@ -2092,7 +2092,7 @@ void ZigbeeMapRefresh(void) { if ((!zigbee.init_phase) && (!zigbee.mapping_in_progress)) { ZigbeeMapAllDevices(); } - Webserver->sendHeader("Location","/zbm"); // Add a header to respond with a new location for the browser to go to the home page again + Webserver->sendHeader(F("Location"),F("/zbm")); // Add a header to respond with a new location for the browser to go to the home page again Webserver->send(302); } diff --git a/tasmota/xdrv_38_ping.ino b/tasmota/xdrv_38_ping.ino index 3f87a54bb..0974697fe 100644 --- a/tasmota/xdrv_38_ping.ino +++ b/tasmota/xdrv_38_ping.ino @@ -303,7 +303,7 @@ void PingResponsePoll(void) { ",\"AvgTime\":%d" "}}}"), ping->hostname.c_str(), - success ? "true" : "false", + success ? PSTR("true") : PSTR("false"), ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, ip >> 24, success, ping->timeout_count, diff --git a/tasmota/xdrv_40_telegram.ino b/tasmota/xdrv_40_telegram.ino index ec230e792..0ceefa3a7 100644 --- a/tasmota/xdrv_40_telegram.ino +++ b/tasmota/xdrv_40_telegram.ino @@ -434,14 +434,14 @@ void CmndTmChatId(void) { void CmndTmSend(void) { if (!Telegram.send_enable || !strlen(SettingsText(SET_TELEGRAM_CHATID))) { - ResponseCmndChar(D_JSON_FAILED); + ResponseCmndChar(PSTR(D_JSON_FAILED)); return; } if (XdrvMailbox.data_len > 0) { String message = XdrvMailbox.data; String chat_id = SettingsText(SET_TELEGRAM_CHATID); if (!TelegramSendMessage(chat_id.toInt(), message)) { - ResponseCmndChar(D_JSON_FAILED); + ResponseCmndChar(PSTR(D_JSON_FAILED)); return; } } diff --git a/tasmota/xdrv_50_filesystem.ino b/tasmota/xdrv_50_filesystem.ino index fb12e3b9d..dfca83cef 100644 --- a/tasmota/xdrv_50_filesystem.ino +++ b/tasmota/xdrv_50_filesystem.ino @@ -391,7 +391,7 @@ void UFSDelete(void) { result = (ufs_type && ufsp->remove(XdrvMailbox.data)); } if (!result) { - ResponseCmndChar(D_JSON_FAILED); + ResponseCmndChar(PSTR(D_JSON_FAILED)); } else { ResponseCmndDone(); } @@ -454,8 +454,8 @@ void UfsDirectory(void) { strcpy(ufs_path, "/"); - if (Webserver->hasArg("download")) { - String stmp = Webserver->arg("download"); + if (Webserver->hasArg(F("download"))) { + String stmp = Webserver->arg(F("download")); char *cp = (char*)stmp.c_str(); if (UfsDownloadFile(cp)) { // is directory @@ -465,8 +465,8 @@ void UfsDirectory(void) { } } - if (Webserver->hasArg("dir")) { - String stmp = Webserver->arg("dir"); + if (Webserver->hasArg(F("dir"))) { + String stmp = Webserver->arg(F("dir")); ufs_dir = atoi(stmp.c_str()); if (ufs_dir == 1) { dfsp = ufsp; @@ -477,8 +477,8 @@ void UfsDirectory(void) { } } - if (Webserver->hasArg("delete")) { - String stmp = Webserver->arg("delete"); + if (Webserver->hasArg(F("delete"))) { + String stmp = Webserver->arg(F("delete")); char *cp = (char*)stmp.c_str(); dfsp->remove(cp); } @@ -498,7 +498,7 @@ void UfsDirectory(void) { } WSContentSend_P(UFS_FORM_FILE_UPGc2); - WSContentSend_P(UFS_FORM_FILE_UPG, D_SCRIPT_UPLOAD); + WSContentSend_P(UFS_FORM_FILE_UPG, PSTR(D_SCRIPT_UPLOAD)); WSContentSend_P(UFS_FORM_SDC_DIRa); if (ufs_type) { @@ -516,7 +516,7 @@ void UfsListDir(char *path, uint8_t depth) { char name[32]; char npath[128]; char format[12]; - sprintf(format, "%%-%ds", 24 - depth); + sprintf(format, PSTR("%%-%ds"), 24 - depth); File dir = dfsp->open(path, UFS_FILE_READ); if (dir) { @@ -533,7 +533,7 @@ void UfsListDir(char *path, uint8_t depth) { break; } } - WSContentSend_P(UFS_FORM_SDC_DIRd, npath, path, ".."); + WSContentSend_P(UFS_FORM_SDC_DIRd, npath, path, PSTR("..")); } char *ep; while (true) { @@ -764,13 +764,13 @@ bool Xdrv50(uint8_t function) { #ifdef USE_WEBSERVER case FUNC_WEB_ADD_MANAGEMENT_BUTTON: if (ufs_type) { - WSContentSend_PD(UFS_WEB_DIR, D_MANAGE_FILE_SYSTEM); + WSContentSend_PD(UFS_WEB_DIR, PSTR(D_MANAGE_FILE_SYSTEM)); } break; case FUNC_WEB_ADD_HANDLER: - Webserver->on("/ufsd", UfsDirectory); - Webserver->on("/ufsu", HTTP_GET, UfsDirectory); - Webserver->on("/ufsu", HTTP_POST,[](){Webserver->sendHeader("Location","/ufsu");Webserver->send(303);}, HandleUploadLoop); + Webserver->on(F("/ufsd"), UfsDirectory); + Webserver->on(F("/ufsu"), HTTP_GET, UfsDirectory); + Webserver->on(F("/ufsu"), HTTP_POST,[](){Webserver->sendHeader(F("Location"),F("/ufsu"));Webserver->send(303);}, HandleUploadLoop); break; #endif // USE_WEBSERVER } diff --git a/tasmota/xsns_34_hx711.ino b/tasmota/xsns_34_hx711.ino index 4f22d7cca..7dc9da3d7 100644 --- a/tasmota/xsns_34_hx711.ino +++ b/tasmota/xsns_34_hx711.ino @@ -241,7 +241,7 @@ bool HxCommand(void) break; case 7: // WeightSave Settings.energy_frequency_calibration = Hx.weight; - Response_P(S_JSON_SENSOR_INDEX_SVALUE, XSNS_34, D_JSON_DONE); + Response_P(S_JSON_SENSOR_INDEX_SVALUE, XSNS_34, PSTR(D_JSON_DONE)); break; case 8: // Json on weight change if (strchr(XdrvMailbox.data, ',') != nullptr) { From b55fdcef75ca397ed25ee3fe0e6b638f951628c5 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Mon, 18 Jan 2021 22:32:59 +0100 Subject: [PATCH 027/186] Fix compilation ESP32 --- tasmota/xdrv_10_rules.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasmota/xdrv_10_rules.ino b/tasmota/xdrv_10_rules.ino index 31b8e9f46..4a6d3c9c1 100644 --- a/tasmota/xdrv_10_rules.ino +++ b/tasmota/xdrv_10_rules.ino @@ -720,7 +720,7 @@ bool RuleSetProcess(uint8_t rule_set, String &event_saved) if ((ucommand.indexOf(F("IF ")) == -1) && (ucommand.indexOf(F("EVENT ")) != -1) && (ucommand.indexOf(F("BACKLOG ")) == -1)) { - commands = F("backlog ") + commands; + commands = String(F("backlog ")) + commands; } RulesVarReplace(commands, F("%VALUE%"), Rules.event_value); From 3265236fe1943a08fef3b2f36db193dfa94652be Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Mon, 18 Jan 2021 22:37:36 +0100 Subject: [PATCH 028/186] Fix compilation ESP32 --- tasmota/support_tasmota.ino | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tasmota/support_tasmota.ino b/tasmota/support_tasmota.ino index 9cac3fa37..40d5836ad 100644 --- a/tasmota/support_tasmota.ino +++ b/tasmota/support_tasmota.ino @@ -744,7 +744,8 @@ void TempHumDewShow(bool json, bool pass_on, const char *types, float f_temperat String GetSwitchText(uint32_t i) { String switch_text = SettingsText(SET_SWITCH_TXT1 + i); if ('\0' == switch_text[0]) { - switch_text = F(D_JSON_SWITCH) + String(i +1); + switch_text = F(D_JSON_SWITCH); + switch_text += String(i+1); } return switch_text; } From 61bf455747f08c496c5130b6b312820518dc66ae Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Tue, 19 Jan 2021 08:50:52 +0100 Subject: [PATCH 029/186] gz not build anymore for ESP32 --- .github/workflows/Tasmota_build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/Tasmota_build.yml b/.github/workflows/Tasmota_build.yml index 9e8f61219..6cf253537 100644 --- a/.github/workflows/Tasmota_build.yml +++ b/.github/workflows/Tasmota_build.yml @@ -1603,8 +1603,6 @@ jobs: [ ! -f ./mv_firmware/tasmota32-knx.* ] || mv ./mv_firmware/tasmota32-knx.* ./firmware/tasmota32/ [ ! -f ./mv_firmware/tasmota32* ] || mv ./mv_firmware/tasmota32* ./firmware/tasmota32/languages/ [ ! -f ./mv_firmware/* ] || mv ./mv_firmware/* ./firmware/tasmota/languages/ - rm ./firmware/tasmota32/*.gz - rm ./firmware/tasmota32/languages/*.gz [ ! -f ./tools/Esptool/ESP32/*.* ] || mv ./tools/Esptool/ESP32/*.* ./firmware/tasmota32/ESP32_needed_files/ [ ! -f ./tools/Esptool/Odroid_go/*.* ] || mv ./tools/Esptool/Odroid_go/*.* ./firmware/tasmota32/Odroid_go_needed_files/ [ ! -f ./FIRMWARE.md ] || mv -f ./FIRMWARE.md ./README.md From 86f4e8ab74f874b24609097cd66686d512430380 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Tue, 19 Jan 2021 08:51:48 +0100 Subject: [PATCH 030/186] no gz build for ESP32 --- .github/workflows/Tasmota_build_master.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/Tasmota_build_master.yml b/.github/workflows/Tasmota_build_master.yml index 8c00d25e3..b2b68c477 100644 --- a/.github/workflows/Tasmota_build_master.yml +++ b/.github/workflows/Tasmota_build_master.yml @@ -1603,8 +1603,6 @@ jobs: [ ! -f ./mv_firmware/tasmota32-knx.* ] || mv ./mv_firmware/tasmota32-knx.* ./firmware/tasmota32/ [ ! -f ./mv_firmware/tasmota32* ] || mv ./mv_firmware/tasmota32* ./firmware/tasmota32/languages/ [ ! -f ./mv_firmware/* ] || mv ./mv_firmware/* ./firmware/tasmota/languages/ - rm ./firmware/tasmota32/*.gz - rm ./firmware/tasmota32/languages/*.gz [ ! -f ./tools/Esptool/ESP32/*.* ] || mv ./tools/Esptool/ESP32/*.* ./firmware/tasmota32/ESP32_needed_files/ [ ! -f ./tools/Esptool/Odroid_go/*.* ] || mv ./tools/Esptool/Odroid_go/*.* ./firmware/tasmota32/Odroid_go_needed_files/ [ ! -f ./FIRMWARE.md ] || mv -f ./RELEASENOTES.md ./README.md From ec74dc3a51f46dedda7a0c6cafd97c1c82e35390 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Tue, 19 Jan 2021 11:54:49 +0100 Subject: [PATCH 031/186] Make zbbridge transfer more verbose Make zbbridge transfer more verbose (#10413) --- tasmota/xdrv_23_zigbee_9a_upload.ino | 90 ++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 26 deletions(-) diff --git a/tasmota/xdrv_23_zigbee_9a_upload.ino b/tasmota/xdrv_23_zigbee_9a_upload.ino index b946848a3..d8157c127 100644 --- a/tasmota/xdrv_23_zigbee_9a_upload.ino +++ b/tasmota/xdrv_23_zigbee_9a_upload.ino @@ -81,6 +81,13 @@ char ZigbeeUploadFlashRead(void) { ZbUpload.byte_counter++; if (ZbUpload.byte_counter > ZbUpload.ota_size) { + +// static bool padding = true; +// if (padding) { +// AddLog_P(LOG_LEVEL_DEBUG, PSTR("XMD: Start padding from %d"), ZbUpload.byte_counter); +// padding = false; +// } + // When the source device reaches the last XModem data block, it should be padded to 128 bytes // of data using SUB (ASCII 0x1A) characters. data = XM_SUB; @@ -107,8 +114,8 @@ struct XMODEM { uint32_t delay = 0; uint32_t flush_delay = 0xFFFFFFFF; uint32_t filepos = 0; + uint32_t packet_no = 1; int crcBuf = 0; - uint8_t packetNo = 1; uint8_t checksumBuf = 0; bool oldChecksum; } XModem; @@ -142,6 +149,11 @@ char XModemWaitACK(void) if (i > 200) { return -1; } } in_char = ZigbeeSerial->read(); + +// if (in_char != XM_ACK) { +// AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("XMD: Rcvd3 0x%02X"), in_char); +// } + if (XM_CAN == in_char) { return XM_CAN; } } while ((in_char != XM_NAK) && (in_char != XM_ACK) && (in_char != 'C')); return in_char; @@ -162,10 +174,12 @@ bool XModemSendPacket(uint32_t packet_no) { XModem.checksumBuf = 0x00; XModem.crcBuf = 0x00; + uint8_t packet_num = packet_no; + // Try to send packet, so header first ZigbeeSerial->write(XM_SOH); - ZigbeeSerial->write(packet_no); - ZigbeeSerial->write(~packet_no); + ZigbeeSerial->write(packet_num); + ZigbeeSerial->write(~packet_num); for (uint32_t i = 0; i < XMODEM_PACKET_SIZE; i++) { in_char = ZigbeeUploadFlashRead(); XModemOutputByte(in_char); @@ -220,6 +234,13 @@ bool ZigbeeUploadBootloaderPrompt(void) { yield(); char bootloader_byte = ZigbeeSerial->read(); + // [cr][lf] + // Gecko Bootloader v1.A.3 or Gecko Bootloader v1.9.1.04[cr][lf] + // 1. upload gbl[cr][lf] + // 2. run[cr][lf] + // 3. ebl info[cr][lf] + // BL > + if (((uint8_t)bootloader_byte >=0) && (buf_len < sizeof(serial_buffer) -2)) { serial_buffer[buf_len++] = bootloader_byte; } @@ -332,17 +353,18 @@ bool ZigbeeUploadXmodem(void) { } } else { // After the bootloader receives a carriage return from the target device, it displays a menu - // Gecko Bootloader v1.A.3 - // 1. upload gbl - // 2. run - // 3. ebl info + // [cr][lf] + // Gecko Bootloader v1.A.3 or Gecko Bootloader v1.9.1.04[cr][lf] + // 1. upload gbl[cr][lf] + // 2. run[cr][lf] + // 3. ebl info[cr][lf] // BL > if (ZigbeeUploadBootloaderPrompt()) { AddLog_P(LOG_LEVEL_DEBUG, PSTR("XMD: Init sync")); ZigbeeSerial->flush(); ZigbeeSerial->write('1'); // upload ebl if (TasmotaGlobal.sleep > 0) { - TasmotaGlobal.sleep = 1; // Speed up loop used for xmodem upload + TasmotaGlobal.sleep = 1; // Speed up loop used for xmodem upload } XModem.timeout = millis() + (XMODEM_SYNC_TIMEOUT * 1000); ZbUpload.ota_step = ZBU_SYNC; @@ -358,11 +380,17 @@ bool ZigbeeUploadXmodem(void) { } // Wait for either C or NACK as a sync packet. Determines protocol details, checksum algorithm. if (ZigbeeSerial->available()) { + // [cr][lf] + // begin upload[cr][lf] + // C char xmodem_sync = ZigbeeSerial->read(); + +// AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("XMD: Rcvd2 0x%02X"), xmodem_sync); + if (('C' == xmodem_sync) || (XM_NAK == xmodem_sync)) { // Determine which checksum algorithm to use XModem.oldChecksum = (xmodem_sync == XM_NAK); - XModem.packetNo = 1; + XModem.packet_no = 1; ZbUpload.byte_counter = 0; ZbUpload.ota_step = ZBU_UPLOAD; AddLog_P(LOG_LEVEL_DEBUG, PSTR("XMD: Init packet send")); @@ -372,12 +400,15 @@ bool ZigbeeUploadXmodem(void) { } case ZBU_UPLOAD: { // *** Handle file upload using XModem - upload if (ZigbeeUploadAvailable()) { - if (!XModemSendPacket(XModem.packetNo)) { - AddLog_P(LOG_LEVEL_DEBUG, PSTR("XMD: Packet send failed")); + if (ZbUpload.byte_counter && !(ZbUpload.byte_counter % 10240)) { // Show progress every 10kB + AddLog_P(LOG_LEVEL_DEBUG, PSTR("XMD: Progress %d kB"), ZbUpload.byte_counter / 1024); + } + if (!XModemSendPacket(XModem.packet_no)) { + AddLog_P(LOG_LEVEL_DEBUG, PSTR("XMD: Packet %d send failed"), XModem.packet_no); ZbUpload.ota_step = ZBU_ERROR; return true; } - XModem.packetNo++; + XModem.packet_no++; } else { // Once the last block is ACKed by the target, the transfer should be finalized by an // EOT (ASCII 0x04) packet from the source. Once this packet is confirmed via XModem ACK @@ -385,6 +416,7 @@ bool ZigbeeUploadXmodem(void) { ZigbeeSerial->write(XM_EOT); XModem.timeout = millis() + (30 * 1000); // Allow 30 seconds to receive EOT ACK ZbUpload.ota_step = ZBU_EOT; + AddLog_P(LOG_LEVEL_DEBUG, PSTR("XMD: Transferred %d bytes"), ZbUpload.ota_size); } break; } @@ -401,7 +433,12 @@ bool ZigbeeUploadXmodem(void) { } if (ZigbeeSerial->available()) { char xmodem_ack = XModemWaitACK(); - if (XM_ACK == xmodem_ack) { + if (XM_CAN == xmodem_ack) { + AddLog_P(LOG_LEVEL_DEBUG, PSTR("XMD: Transfer invalid")); + ZbUpload.ota_step = ZBU_ERROR; + return true; + } + else if (XM_ACK == xmodem_ack) { AddLog_P(LOG_LEVEL_DEBUG, PSTR("XMD: " D_SUCCESSFUL)); XModem.timeout = millis() + (30 * 1000); // Allow 30 seconds to receive EBL prompt ZbUpload.byte_counter = 0; @@ -418,28 +455,33 @@ bool ZigbeeUploadXmodem(void) { } else { // After an image successfully uploads, the XModem transaction completes and the bootloader displays // ‘Serial upload complete’ before redisplaying the menu - // Serial upload complete - // Gecko Bootloader v1.A.3 - // 1. upload gbl - // 2. run - // 3. ebl info + // + // [cr][lf] + // Serial upload complete[cr][lf] + // [cr][lf] + // Gecko Bootloader v1.A.3 or Gecko Bootloader v1.9.1.04[cr][lf] + // 1. upload gbl[cr][lf] + // 2. run[cr][lf] + // 3. ebl info[cr][lf] // BL > if (ZigbeeUploadBootloaderPrompt()) { ZbUpload.state = ZBU_COMPLETE; ZbUpload.ota_step = ZBU_DONE; + AddLog_P(LOG_LEVEL_DEBUG, PSTR("XMD: " D_RESTARTING)); } } break; } case ZBU_ERROR: ZbUpload.state = ZBU_ERROR; + AddLog_P(LOG_LEVEL_DEBUG, PSTR("XMD: " D_FAILED)); case ZBU_DONE: { // *** Clean up and restart to disable bootloader and use new firmware - AddLog_P(LOG_LEVEL_DEBUG, PSTR("XMD: " D_RESTARTING)); ZigbeeUploadSetBootloader(1); // Disable bootloader and reset MCU - should happen at restart if (1 == TasmotaGlobal.sleep) { TasmotaGlobal.sleep = Settings.sleep; // Restore loop sleep } // TasmotaGlobal.restart_flag = 2; // Restart to disable bootloader and use new firmware + if (ZbUpload.buffer) { free(ZbUpload.buffer); } ZbUpload.ota_step = ZBU_FINISH; // Never return to zero without a restart to get a sane Zigbee environment break; } @@ -472,10 +514,6 @@ void ZigbeeUploadStep1Done(uint32_t data, size_t size) { ZbUpload.state = ZBU_UPLOAD; // Signal upload done and ready for delayed upload to MCU EFR32 } -bool ZigbeeUploadFinish(void) { - return (ZBU_FINISH == ZbUpload.ota_step); -} - #define WEB_HANDLE_ZIGBEE_XFER "zx" const char HTTP_SCRIPT_XFER_STATE[] PROGMEM = @@ -500,12 +538,12 @@ void HandleZigbeeXfer(void) { if (!HttpCheckPriviledgedAccess()) { return; } if (Webserver->hasArg("z")) { // Status refresh requested - WSContentBegin(200, CT_PLAIN); - WSContentSend_P(PSTR("%d"), ZbUpload.state); - WSContentEnd(); if (ZBU_ERROR == ZbUpload.state) { Web.upload_error = 7; // Upload aborted (xmodem transfer failed) } + WSContentBegin(200, CT_PLAIN); + WSContentSend_P(PSTR("%d"), ZbUpload.state); + WSContentEnd(); return; } From 865b25dff9dd4230b1d69fe3d6567c8e10a98952 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Tue, 19 Jan 2021 12:23:01 +0100 Subject: [PATCH 032/186] Remove disable messages --- tasmota/xdrv_01_webserver.ino | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index b43bc573f..33d4ebce8 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -2148,8 +2148,6 @@ void HandleInformation(void) #ifdef USE_EMULATION WSContentSend_P(PSTR("}1" D_EMULATION "}2%s"), GetTextIndexed(stopic, sizeof(stopic), Settings.flag2.emulation, kEmulationOptions)); -#else - WSContentSend_P(PSTR("}1" D_EMULATION "}2" D_DISABLED)); #endif // USE_EMULATION #ifdef USE_DISCOVERY @@ -2161,8 +2159,6 @@ void HandleInformation(void) WSContentSend_P(PSTR("}1" D_MDNS_ADVERTISE "}2" D_DISABLED)); #endif // WEBSERVER_ADVERTISE } -#else - WSContentSend_P(PSTR("}1" D_MDNS_DISCOVERY "}2" D_DISABLED)); #endif // USE_DISCOVERY WSContentSend_P(PSTR("}1}2 ")); // Empty line From 9a11d3613bc87c1ba9be7ed56ee3615f794348dc Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Tue, 19 Jan 2021 12:26:52 +0100 Subject: [PATCH 033/186] Remove disable messages --- tasmota/xdrv_01_webserver.ino | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index 33d4ebce8..26a720a2d 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -2127,7 +2127,7 @@ void HandleInformation(void) WSContentSend_P(PSTR("}1" D_MQTT_PORT "}2%d"), Settings.mqtt_port); #ifdef USE_MQTT_TLS WSContentSend_P(PSTR("}1" D_MQTT_TLS_ENABLE "}2%s"), Settings.flag4.mqtt_tls ? PSTR(D_ENABLED) : PSTR(D_DISABLED)); -#endif // USE_MQTT_TLS +#endif // USE_MQTT_TLS WSContentSend_P(PSTR("}1" D_MQTT_USER "}2%s"), SettingsText(SET_MQTT_USER)); WSContentSend_P(PSTR("}1" D_MQTT_CLIENT "}2%s"), TasmotaGlobal.mqtt_client); WSContentSend_P(PSTR("}1" D_MQTT_TOPIC "}2%s"), SettingsText(SET_MQTT_TOPIC)); @@ -2144,12 +2144,13 @@ void HandleInformation(void) } else { WSContentSend_P(PSTR("}1" D_MQTT "}2" D_DISABLED)); } - WSContentSend_P(PSTR("}1}2 ")); // Empty line +#if defined(USE_EMULATION) || defined(USE_DISCOVERY) + WSContentSend_P(PSTR("}1}2 ")); // Empty line +#endif // USE_EMULATION or USE_DISCOVERY #ifdef USE_EMULATION WSContentSend_P(PSTR("}1" D_EMULATION "}2%s"), GetTextIndexed(stopic, sizeof(stopic), Settings.flag2.emulation, kEmulationOptions)); -#endif // USE_EMULATION - +#endif // USE_EMULATION #ifdef USE_DISCOVERY WSContentSend_P(PSTR("}1" D_MDNS_DISCOVERY "}2%s"), (Settings.flag3.mdns_enabled) ? D_ENABLED : D_DISABLED); // SetOption55 - Control mDNS service if (Settings.flag3.mdns_enabled) { // SetOption55 - Control mDNS service @@ -2157,9 +2158,9 @@ void HandleInformation(void) WSContentSend_P(PSTR("}1" D_MDNS_ADVERTISE "}2" D_WEB_SERVER)); #else WSContentSend_P(PSTR("}1" D_MDNS_ADVERTISE "}2" D_DISABLED)); -#endif // WEBSERVER_ADVERTISE +#endif // WEBSERVER_ADVERTISE } -#endif // USE_DISCOVERY +#endif // USE_DISCOVERY WSContentSend_P(PSTR("}1}2 ")); // Empty line WSContentSend_P(PSTR("}1" D_ESP_CHIP_ID "}2%d"), ESP_getChipId()); From 6fe37d148a8b5a8603855e9bb555f5ba300b5095 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Tue, 19 Jan 2021 12:34:36 +0100 Subject: [PATCH 034/186] use esp32-1.0.5-rc6.zip for ESP32 --- platformio_tasmota32.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio_tasmota32.ini b/platformio_tasmota32.ini index 717ca03a5..7053ac3af 100644 --- a/platformio_tasmota32.ini +++ b/platformio_tasmota32.ini @@ -92,7 +92,7 @@ build_flags = ${esp_defaults.build_flags} [core32] platform = espressif32 @ 2.1.0 -platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/arduino-esp32/releases/download/1.0.5-rc4/esp32-1.0.5-rc4.zip +platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/arduino-esp32/releases/download/1.0.5-rc6/esp32-1.0.5-rc6.zip platformio/tool-mklittlefs @ ~1.203.200522 build_unflags = ${esp32_defaults.build_unflags} build_flags = ${esp32_defaults.build_flags} From cba145136e58906b87e98c2510549ba6c3eb6ee1 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Tue, 19 Jan 2021 12:35:33 +0100 Subject: [PATCH 035/186] Update PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e542181b9..0de14bbf8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,7 +7,7 @@ - [ ] Only relevant files were touched - [ ] Only one feature/fix was added per PR and the code change compiles without warnings - [ ] The code change is tested and works on Tasmota core ESP8266 V.2.7.4.9 - - [ ] The code change is tested and works on Tasmota core ESP32 V.1.0.5-rc4 + - [ ] The code change is tested and works on Tasmota core ESP32 V.1.0.5-rc6 - [ ] I accept the [CLA](https://github.com/arendst/Tasmota/blob/development/CONTRIBUTING.md#contributor-license-agreement-cla). _NOTE: The code change must pass CI tests. **Your PR cannot be merged unless tests pass**_ From f607152aa17cd5aa2e472b35239edc160b6474da Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Tue, 19 Jan 2021 13:37:53 +0100 Subject: [PATCH 036/186] Zigbee add EZSP 6.7.8 as Release Candidate firmware --- .../{ => archive}/ncp-uart-sw_6.5.5_115200.ota | Bin .../ncp-uart-sw_6.7.8_115200.ota | Bin 0 -> 190756 bytes tools/fw_SonoffZigbeeBridge_ezsp/readme.txt | 7 +++++-- 3 files changed, 5 insertions(+), 2 deletions(-) rename tools/fw_SonoffZigbeeBridge_ezsp/{ => archive}/ncp-uart-sw_6.5.5_115200.ota (100%) create mode 100644 tools/fw_SonoffZigbeeBridge_ezsp/ncp-uart-sw_6.7.8_115200.ota diff --git a/tools/fw_SonoffZigbeeBridge_ezsp/ncp-uart-sw_6.5.5_115200.ota b/tools/fw_SonoffZigbeeBridge_ezsp/archive/ncp-uart-sw_6.5.5_115200.ota similarity index 100% rename from tools/fw_SonoffZigbeeBridge_ezsp/ncp-uart-sw_6.5.5_115200.ota rename to tools/fw_SonoffZigbeeBridge_ezsp/archive/ncp-uart-sw_6.5.5_115200.ota diff --git a/tools/fw_SonoffZigbeeBridge_ezsp/ncp-uart-sw_6.7.8_115200.ota b/tools/fw_SonoffZigbeeBridge_ezsp/ncp-uart-sw_6.7.8_115200.ota new file mode 100644 index 0000000000000000000000000000000000000000..9bc81b544d7c55f82768b8088df51bed51e41052 GIT binary patch literal 190756 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%0AUWU5O5bd-u5 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%0A-Q>YP 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 sW6n1`oUq{i|Fdm+Z@U|3w^Spz8MKhX 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 literal 0 HcmV?d00001 diff --git a/tools/fw_SonoffZigbeeBridge_ezsp/readme.txt b/tools/fw_SonoffZigbeeBridge_ezsp/readme.txt index da6716cdd..f8a4b0bb2 100644 --- a/tools/fw_SonoffZigbeeBridge_ezsp/readme.txt +++ b/tools/fw_SonoffZigbeeBridge_ezsp/readme.txt @@ -3,8 +3,7 @@ ## EmberZNet NCP UART EZSP firmware signed for Sonoff ZBBridge - `ncp-uart-sw_6.7.6_115200.ota` - recommended stable version for EZSP v6, EZSP v7, and EZSP v8 compatible hosts. -- `ncp-uart-sw-6.8.0.1_115200.ota` - latest cutting-edge version. largely untested and only for experimentation with EZSP v8 compatible hosts. -- `ncp-uart-sw_6.5.5_115200.ota` - legacy version for EZSP v4, EZSP v5, EZSP v6, or EZSP v7 compatible hosts. +- `ncp-uart-sw_6.7.8_115200.ota` - release candidate, supposedly fixing IKEA battery drain issue. ## EmberZNet and EZSP Protocol Versions @@ -13,3 +12,7 @@ Silicon Labs do not currently have a consolidated list of changes by EmberZNet S The largest change was between EZSP v4 (first added in EmberZNet 4.7.2 SDK) and EZSP v5 that was added in EmberZNet 5.9.0 SDK which requires the Legacy Frame ID 0xFF. The change from EZSP v5 to EZSP v6 was done in EmberZNet 6.0.0 SDK. The change from EZSP v6 to EZSP v7 was in EmberZNet 6.4.0 SDK. EmberZNet 6.7.0 SDK added EZSP v8 (and Secure EZSP Protocol Version 2). Perhaps more important to know is that EZSP v5, v6 and v7 (EmberZNet 6.6.x.x) use the same framing format, but EmberZNet 6.7.x.x/EZSP v8 introduced new framing format and expanded command id field from 8 bits to 16 bits. + +## Archived Versions + +- `ncp-uart-sw_6.5.5_115200.ota` - legacy version for EZSP v4, EZSP v5, EZSP v6, or EZSP v7 compatible hosts. \ No newline at end of file From c5984875d65ae3755aeff95d577ebc75aff0bc04 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Tue, 19 Jan 2021 13:57:30 +0100 Subject: [PATCH 037/186] Remove upload watchdog --- tasmota/xdrv_01_webserver.ino | 3 --- 1 file changed, 3 deletions(-) diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index 26a720a2d..34d3947f6 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -2341,7 +2341,6 @@ void UploadServices(uint32_t start_service) { if (start_service) { // AddLog_P(LOG_LEVEL_DEBUG, PSTR("UPL: Services enabled")); - TasmotaGlobal.restart_flag = 0; /* MqttRetryCounter(0); */ @@ -2376,7 +2375,6 @@ void UploadServices(uint32_t start_service) { MqttDisconnect(); } */ - TasmotaGlobal.restart_flag = 120; // Set restart watchdog after 2 minutes } } @@ -2429,7 +2427,6 @@ void HandleUploadLoop(void) { Web.upload_error = 2; return; } - TasmotaGlobal.restart_flag = 0; } #endif // USE_UFILESYS } From f5f6c6e5a1675587d551c07e3eceb1011241ee61 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Tue, 19 Jan 2021 15:26:02 +0100 Subject: [PATCH 038/186] Enable new BLE driver for odroid --- tasmota/tasmota_configurations_ESP32.h | 1 + 1 file changed, 1 insertion(+) diff --git a/tasmota/tasmota_configurations_ESP32.h b/tasmota/tasmota_configurations_ESP32.h index 0a507dc2b..bb23c6663 100644 --- a/tasmota/tasmota_configurations_ESP32.h +++ b/tasmota/tasmota_configurations_ESP32.h @@ -57,6 +57,7 @@ #define USE_SPI #define USE_DISPLAY // Add SPI Display Support (+2k code) #define USE_DISPLAY_ILI9341 // [DisplayModel 4] Enable ILI9341 Tft 480x320 display (+19k code) +#define USE_BLE_ESP32 // Enable new BLE driver #define USE_MI_ESP32 // (ESP32 only) Add support for ESP32 as a BLE-bridge (+9k2 mem, +292k flash) #endif // FIRMWARE_ODROID_GO From c16fb465fb37392e114b181dba40ed1ef7b22a1f Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Tue, 19 Jan 2021 16:23:16 +0100 Subject: [PATCH 039/186] Add correct log info --- tasmota/xdrv_52_BLE_ESP32.ino | 234 +++++++++--------- tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino | 314 ++++++++++++------------- 2 files changed, 274 insertions(+), 274 deletions(-) diff --git a/tasmota/xdrv_52_BLE_ESP32.ino b/tasmota/xdrv_52_BLE_ESP32.ino index 309934d3e..cfd176a8d 100644 --- a/tasmota/xdrv_52_BLE_ESP32.ino +++ b/tasmota/xdrv_52_BLE_ESP32.ino @@ -599,7 +599,7 @@ int addSeenDevice(const uint8_t *mac, uint8_t addrtype, const char *name, int8_t int total = seenDevices.size(); if (total < MAX_BLE_DEVICES_LOGGED){ #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("new seendev slot %d"), total); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: New seendev slot %d"), total); #endif BLE_ESP32::BLE_simple_device_t* dev = new BLE_ESP32::BLE_simple_device_t; freeDevices.push_back(dev); @@ -667,10 +667,10 @@ int deleteSeenDevices(int ageS = 0){ dump(addr, 20, dev->mac, 6); const char *alias = getAlias(dev->mac); if (!filter){ - AddLog_P(LOG_LEVEL_INFO,PSTR("delete device %s(%s) by age lastseen %u + maxage %u < now %u."), + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: Delete device %s(%s) by age lastseen %u + maxage %u < now %u."), addr, alias, lastseenS, ageS, nowS); } else { - AddLog_P(LOG_LEVEL_INFO,PSTR("delete device %s(%s) by addrtype filter %d > %d."), + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: Delete device %s(%s) by addrtype filter %d > %d."), addr, alias, dev->addrtype, BLEAddressFilter); } #endif @@ -682,7 +682,7 @@ int deleteSeenDevices(int ageS = 0){ } if (res){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_INFO,PSTR("BLE deleted %d devices"), res); + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: Deleted %d devices"), res); #endif } return res; @@ -820,7 +820,7 @@ int getSeenDevicesToJson(char *dest, int maxlen){ } // deliberate test of SafeAddLog_P from main thread... - //AddLog_P(LOG_LEVEL_INFO,PSTR("getSeen %d"), seenDevices.size()); + //AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: getSeen %d"), seenDevices.size()); int len; @@ -1184,17 +1184,17 @@ void postAdvertismentDetails(){ class BLESensorCallback : public NimBLEClientCallbacks { void onConnect(NimBLEClient* pClient) { #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("onConnect %s"), ((std::string)pClient->getPeerAddress()).c_str()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: onConnect %s"), ((std::string)pClient->getPeerAddress()).c_str()); #endif } void onDisconnect(NimBLEClient* pClient) { #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("onDisconnect %s"), ((std::string)pClient->getPeerAddress()).c_str()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: onDisconnect %s"), ((std::string)pClient->getPeerAddress()).c_str()); #endif } bool onConnParamsUpdateRequest(NimBLEClient* pClient, const ble_gap_upd_params* params) { #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("onConnParamsUpdateRequest %s"), ((std::string)pClient->getPeerAddress()).c_str()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: onConnParamsUpdateRequest %s"), ((std::string)pClient->getPeerAddress()).c_str()); #endif // if(params->itvl_min < 24) { /** 1.25ms units */ @@ -1315,7 +1315,7 @@ class BLEAdvCallbacks: public NimBLEAdvertisedDeviceCallbacks { } } catch(const std::exception& e){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in advertismentCallbacks")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: exception in advertismentCallbacks")); #endif } } @@ -1334,18 +1334,18 @@ static BLESensorCallback BLESensorCB; static void BLEscanEndedCB(NimBLEScanResults results){ #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Scan ended")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: Scan ended")); #endif for (int i = 0; i < scancompleteCallbacks.size(); i++){ try { SCANCOMPLETE_CALLBACK *pFn = scancompleteCallbacks[i]; int callbackres = pFn(results); #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("scancompleteCallbacks %d %d"), i, callbackres); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: scancompleteCallbacks %d %d"), i, callbackres); #endif } catch(const std::exception& e){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in operationsCallbacks")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: exception in operationsCallbacks")); #endif } } @@ -1367,21 +1367,21 @@ static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, ui if (!pRemoteCharacteristic){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG,PSTR("Notify: no remote char!!??")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: Notify: no remote char!!??")); #endif return; } #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Notified length: %u"),length); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: Notified length: %u"),length); #endif // find the operation this is associated with NimBLERemoteService *pSvc = pRemoteCharacteristic->getRemoteService(); if (!pSvc){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("Notify: no remote service found")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Notify: no remote service found")); #endif return; } @@ -1389,7 +1389,7 @@ static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, ui pRClient = pSvc->getClient(); if (!pRClient){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("Notify: no remote client!!??")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Notify: no remote client!!??")); #endif return; } @@ -1404,7 +1404,7 @@ static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, ui generic_sensor_t *op = currentOperations[i]; if (!op){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("Notify: null op in currentOperations!!??")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Notify: null op in currentOperations!!??")); #endif } else { if (devaddr == op->addr){ @@ -1420,7 +1420,7 @@ static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, ui if (!thisop){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG,PSTR("no op for notify")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: no op for notify")); #endif return; } @@ -1524,7 +1524,7 @@ static void BLEOperationTask(void *pvParameters); static void BLEStartOperationTask(){ if (BLERunning == false){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Start operations"),D_CMND_BLE); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: %s: Start operations"),D_CMND_BLE); #endif BLERunning = true; @@ -1547,25 +1547,25 @@ static void BLEStartOperationTask(){ static void BLETaskStopStartNimBLE(NimBLEClient **ppClient, bool start = true){ if (*ppClient){ - AddLog_P(LOG_LEVEL_ERROR,PSTR("BLETask:Stopping NimBLE")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Task:Stopping NimBLE")); (*ppClient)->setClientCallbacks(nullptr, false); try { if ((*ppClient)->isConnected()){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_INFO,PSTR("disconnecting connected client")); + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: disconnecting connected client")); #endif (*ppClient)->disconnect(); } NimBLEDevice::deleteClient((*ppClient)); (*ppClient) = nullptr; #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_INFO,PSTR("deleted client")); + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: deleted client")); #endif } catch(const std::exception& e){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("Stopping NimBLE:exception in delete client")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Stopping NimBLE:exception in delete client")); #endif } @@ -1582,7 +1582,7 @@ static void BLETaskStopStartNimBLE(NimBLEClient **ppClient, bool start = true){ BLERunningScan = 0; if (start){ - AddLog_P(LOG_LEVEL_INFO,PSTR("BLETask:Starting NimBLE")); + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: BLETask:Starting NimBLE")); NimBLEDevice::init("BLE_ESP32"); *ppClient = NimBLEDevice::createClient(); @@ -1623,7 +1623,7 @@ int BLETaskStartScan(int time){ } #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: Startscan")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: BLETask: Startscan")); #endif //vTaskDelay(500/ portTICK_PERIOD_MS); ble32Scan->setActiveScan(BLEScanActiveMode ? 1: 0); @@ -1652,7 +1652,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe *pCurrentOperation = nextOperation(&queuedOperations); if (*pCurrentOperation){ #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: new currentOperation")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: BLETask: new currentOperation")); #endif BLEOpCount++; generic_sensor_t* temp = *pCurrentOperation; @@ -1673,7 +1673,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe diff = diff/1000; if (diff > 20000){ // 20s #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: notify timeout")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: BLETask: notify timeout")); #endif (*pCurrentOperation)->state = GEN_STATE_FAILED_NOTIFYTIMEOUT; (*pCurrentOperation)->notifytimer = 0; @@ -1690,7 +1690,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe (*pCurrentOperation)->state = GEN_STATE_NOTIFIED; // just stay here until this is removed by the main thread #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: notify operation complete")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: BLETask: notify operation complete")); #endif BLE_ESP32::BLETaskRunTaskDoneOperation(pCurrentOperation, ppClient); pClient = *ppClient; @@ -1701,7 +1701,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe case GEN_STATE_NOTIFIED: // - may have completed DURING our read/write to get here // just stay here until this is removed by the main thread #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: operation complete")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: BLETask: operation complete")); #endif BLE_ESP32::BLETaskRunTaskDoneOperation(pCurrentOperation, ppClient); pClient = *ppClient; @@ -1721,7 +1721,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe if ((*pCurrentOperation)->state <= GEN_STATE_FAILED){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("BLETask: op failed %d"), (*pCurrentOperation)->state); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: BLETask: op failed %d"), (*pCurrentOperation)->state); #endif BLE_ESP32::BLETaskRunTaskDoneOperation(pCurrentOperation, ppClient); pClient = *ppClient; @@ -1735,7 +1735,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe if (pClient->isConnected()){ // don't do anything if we are still connected #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: still connected")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: BLETask: still connected")); #endif return; } @@ -1755,7 +1755,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe op->state = GEN_STATE_STARTED; #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: attempt connect %s"), ((std::string)op->addr).c_str()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: BLETask: attempt connect %s"), ((std::string)op->addr).c_str()); #endif if (!op->serviceUUID.bitSize()){ @@ -1766,7 +1766,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe if (pClient->connect(op->addr, true)) { #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("connected %s -> getservice"), ((std::string)op->addr).c_str()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: connected %s -> getservice"), ((std::string)op->addr).c_str()); #endif NimBLERemoteService *pService = pClient->getService(op->serviceUUID); int waitNotify = false; @@ -1775,7 +1775,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe if (pService != nullptr) { #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got service")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: got service")); #endif // pre-set to fail if no operations requested //newstate = GEN_STATE_FAILED_NOREADWRITE; @@ -1792,13 +1792,13 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe pService->getCharacteristic(op->notificationCharacteristicUUID); if (pNCharacteristic != nullptr) { #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got notify characteristic")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: got notify characteristic")); #endif op->notifylen = 0; if(pNCharacteristic->canNotify()) { if(pNCharacteristic->subscribe(true, BLE_ESP32::BLEGenNotifyCB)) { #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("subscribe for notify")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: subscribe for notify")); #endif uint64_t now = esp_timer_get_time(); op->notifytimer = now; @@ -1808,7 +1808,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe waitNotify = true; } else { #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("failed subscribe for notify")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: failed subscribe for notify")); #endif newstate = GEN_STATE_FAILED_NOTIFY; } @@ -1816,7 +1816,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe if(pNCharacteristic->canIndicate()) { if(pNCharacteristic->subscribe(false, BLE_ESP32::BLEGenNotifyCB)) { #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG,PSTR("subscribe for indicate")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: subscribe for indicate")); #endif notifystate = GEN_STATE_WAITINDICATE; uint64_t now = esp_timer_get_time(); @@ -1824,21 +1824,21 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe waitNotify = true; } else { #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("failed subscribe for indicate")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: failed subscribe for indicate")); #endif newstate = GEN_STATE_FAILED_INDICATE; } } else { newstate = GEN_STATE_FAILED_CANTNOTIFYORINDICATE; #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("characteristic can't notify")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: characteristic can't notify")); #endif } } } else { newstate = GEN_STATE_FAILED_NONOTIFYCHAR; #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("notify characteristic not found")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: notify characteristic not found")); #endif } @@ -1855,7 +1855,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe pCharacteristic = pService->getCharacteristic(op->characteristicUUID); if (pCharacteristic != nullptr) { #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got read/write characteristic")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: got read/write characteristic")); #endif newstate = GEN_STATE_FAILED_NOREADWRITE; // overwritten on failure @@ -1875,12 +1875,12 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe if (op->readmodifywritecallback){ READ_CALLBACK *pFn = (READ_CALLBACK *)op->readmodifywritecallback; #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("read characteristic with readmodifywritecallback")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: read characteristic with readmodifywritecallback")); #endif pFn(op); } else { #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("read characteristic")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: read characteristic")); #endif } @@ -1896,12 +1896,12 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe if (!pCharacteristic->writeValue(op->dataToWrite, op->writelen, true)){ newstate = GEN_STATE_FAILED_WRITE; #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG,PSTR("characteristic write fail")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: characteristic write fail")); #endif } else { if (!waitNotify) newstate = GEN_STATE_WRITEDONE; #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("write characteristic")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: write characteristic")); #endif } } else { @@ -1913,7 +1913,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe } else { newstate = GEN_STATE_FAILED_NO_RW_CHAR; #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG,PSTR("r/w characteristic not found")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: r/w characteristic not found")); #endif } } @@ -1934,7 +1934,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe newstate = GEN_STATE_FAILED_NOSERVICE; // failed to get a service #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG,PSTR("failed - svc not on device?")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: failed - svc not on device?")); #endif } @@ -1946,14 +1946,14 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe switch (rc){ case (0x0200+BLE_ERR_CONN_LIMIT ): #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("Hit connection limit? - restarting NimBLE")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Hit connection limit? - restarting NimBLE")); #endif BLERestartNimBLE = 1; BLERestartBLEReason = BLE_RESTART_BLE_REASON_CONN_LIMIT; break; case (0x0200+BLE_ERR_ACL_CONN_EXISTS): #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("Connection exists? - restarting NimBLE")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Connection exists? - restarting NimBLE")); #endif BLERestartNimBLE = 1; BLERestartBLEReason = BLE_RESTART_BLE_REASON_CONN_EXISTS; @@ -1963,7 +1963,7 @@ static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOpe // failed to connect #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG,PSTR("failed to connect to device %d"), rc); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: failed to connect to device %d"), rc); #endif } op->state = newstate; @@ -1979,7 +1979,7 @@ static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLE try { if ((*ppClient)->isConnected()){ #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("runTaskDoneOperation: disconnecting connected client")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: runTaskDoneOperation: disconnecting connected client")); #endif (*ppClient)->disconnect(); // wait for 1/2 second after disconnect @@ -1990,7 +1990,7 @@ static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLE //(*ppClient)->disconnect(); // we will stall here forever!!! - as testing #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE wait discon%d"), waits); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: wait discon%d"), waits); #endif vTaskDelay(500/ portTICK_PERIOD_MS); } @@ -1999,11 +1999,11 @@ static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLE int conn_id = (*ppClient)->getConnId(); ble_gap_conn_broken(conn_id, -1); #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE wait discon%d - kill connection"), waits); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: wait discon%d - kill connection"), waits); #endif } if (waits == 60){ - AddLog_P(LOG_LEVEL_ERROR,PSTR(">60s waiting -> BLE Failed, restart Tasmota %d"), waits); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: >60s waiting -> BLE Failed, restart Tasmota %d"), waits); BLEStop = 1; BLEStopAt = esp_timer_get_time(); @@ -2015,7 +2015,7 @@ static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLE } } catch(const std::exception& e){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("runTaskDoneOperation: exception in disconnect")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: runTaskDoneOperation: exception in disconnect")); #endif } @@ -2035,7 +2035,7 @@ static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLE // by adding it to this list, this will cause it to be sent to MQTT #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("runTaskDoneOperation: add to completedOperations")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: runTaskDoneOperation: add to completedOperations")); #endif addOperation(&completedOperations, op); return; @@ -2096,7 +2096,7 @@ static void BLEOperationTask(void *pvParameters){ BLERestartNimBLE = 0; BLERestartTasmota = 10; BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_RESTARTING_BLE_TIMEOUT; - AddLog_P(LOG_LEVEL_ERROR,PSTR("BLETask: Restart NimBLE - restart Tasmota in 10 if not complt")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: BLETask: Restart NimBLE - restart Tasmota in 10 if not complt")); BLE_ESP32::BLETaskStopStartNimBLE(&pClient); BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_UNKNOWN; BLERestartTasmota = 0; @@ -2110,7 +2110,7 @@ static void BLEOperationTask(void *pvParameters){ vTaskDelay(100/ portTICK_PERIOD_MS); #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLEOperationTask: Left task")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: BLEOperationTask: Left task")); #endif deleteSeenDevices(); @@ -2192,11 +2192,11 @@ static void BLEEverySecond(bool restart){ if (!BLERestartTasmotaReason) BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_UNKNOWN; snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("{\"reboot\":\"%s\"}"), BLERestartTasmotaReason); MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); - AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE Failure! Restarting Tasmota in %d seconds because %s"), BLERestartTasmota, BLERestartTasmotaReason); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Failure! Restarting Tasmota in %d seconds because %s"), BLERestartTasmota, BLERestartTasmotaReason); } if (!BLERestartTasmota){ - AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE Failure! Restarting Tasmota because %s"), BLERestartTasmotaReason); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Failure! Restarting Tasmota because %s"), BLERestartTasmotaReason); // just a normal restart TasmotaGlobal.restart_flag = 1; } @@ -2205,7 +2205,7 @@ static void BLEEverySecond(bool restart){ if (BLERestartBLEReason){ // just use the ptr as the trigger to send MQTT snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("{\"blerestart\":\"%s\"}"), BLERestartBLEReason); MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); - AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE Failure! Restarting BLE Stack because %s"), BLERestartBLEReason); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Failure! Restarting BLE Stack because %s"), BLERestartBLEReason); BLERestartBLEReason = nullptr; } @@ -2250,9 +2250,9 @@ int addOperation(std::deque *ops, generic_sensor_t** op){ } } if (res){ - //AddLog_P(LOG_LEVEL_DEBUG,PSTR("added operation")); + //AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: added operation")); } else { - AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE op - no room")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: op - no room")); } return res; } @@ -2260,7 +2260,7 @@ int addOperation(std::deque *ops, generic_sensor_t** op){ int newOperation(BLE_ESP32::generic_sensor_t** op){ if (!op) { - AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE op inv in newOperation")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: op inv in newOperation")); return 0; } @@ -2307,7 +2307,7 @@ int extQueueOperation(BLE_ESP32::generic_sensor_t** op){ int res = addOperation(&queuedOperations, op); if (!res){ - AddLog_P(LOG_LEVEL_ERROR,PSTR("extQueueOperation: op added id %d failed"), (lastopid-1)); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: extQueueOperation: op added id %d failed"), (lastopid-1)); } return res; } @@ -2427,7 +2427,7 @@ static int StartBLE(void) { BLE_ESP32::BLEStartOperationTask(); return 1; } - AddLog_P(LOG_LEVEL_ERROR,PSTR("StartBLE - wait as BLEStop==1")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: StartBLE - wait as BLEStop==1")); return 0; } @@ -2436,16 +2436,16 @@ static int StopBLE(void){ if (BLERunning){ if (BLEStop != 1){ BLEStop = 1; - AddLog_P(LOG_LEVEL_INFO,PSTR("StopBLE - BLEStop->1")); + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: StopBLE - BLEStop->1")); BLEStopAt = esp_timer_get_time(); // give a little time for it to stop. vTaskDelay(1000/ portTICK_PERIOD_MS); return 1; } - AddLog_P(LOG_LEVEL_ERROR,PSTR("StopBLE - wait as BLEStop==1")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: StopBLE - wait as BLEStop==1")); return 0; } else { - AddLog_P(LOG_LEVEL_ERROR,PSTR("StopBLE - was not running")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: StopBLE - was not running")); return 1; } } @@ -2686,7 +2686,7 @@ void CmndBLEDetails(void){ void CmndBLEAlias(void){ #ifdef BLE_ESP32_ALIASES int op = XdrvMailbox.index; - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Alias %d %s"), op, XdrvMailbox.data); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: Alias %d %s"), op, XdrvMailbox.data); int res = -1; switch(op){ @@ -2705,7 +2705,7 @@ void CmndBLEAlias(void){ char *mac = p; int len = fromHex(addr, p, sizeof(addr)); if (len != 6){ - AddLog_P(LOG_LEVEL_ERROR,PSTR("Alias invalid mac %s"), p); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Alias invalid mac %s"), p); ResponseCmndChar("invalidmac"); return; } @@ -2726,7 +2726,7 @@ void CmndBLEAlias(void){ return; } - AddLog_P(LOG_LEVEL_ERROR,PSTR("Add Alias mac %s = name %s"), mac, p); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Add Alias mac %s = name %s"), mac, p); if (addAlias( addr, name )){ added++; } @@ -2734,7 +2734,7 @@ void CmndBLEAlias(void){ } while (p); if (added){ - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Added %d Aliases"), added); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: Added %d Aliases"), added); BLEAliasListResp(); } else { BLEAliasListResp(); @@ -2742,7 +2742,7 @@ void CmndBLEAlias(void){ return; } break; case 2:{ // clear - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLEAlias clearing %d"), aliases.size()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: Alias clearing %d"), aliases.size()); for (int i = aliases.size()-1; i >= 0; i--){ BLE_ESP32::ble_alias_t *alias = aliases[i]; aliases.pop_back(); @@ -2773,14 +2773,14 @@ void CmndBLEName(void) { if (addrres){ if (addrres == 2){ - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE addr used alias: %s"), p); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: addr used alias: %s"), p); } //#ifdef EQ3_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE cmd addr: %s -> %s"), p, addr.toString().c_str()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: cmd addr: %s -> %s"), p, addr.toString().c_str()); //#endif } else { - AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE addr invalid: %s"), p); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: addr invalid: %s"), p); ResponseCmndIdxChar(PSTR("invalidaddr")); return; } @@ -2789,11 +2789,11 @@ void CmndBLEName(void) { // ALWAYS use this function to create a new one. int res = BLE_ESP32::newOperation(&op); if (!res){ - AddLog_P(LOG_LEVEL_ERROR,PSTR("Can't get a newOperation")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Can't get a newOperation")); ResponseCmndChar(PSTR("FAIL")); return; } else { - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got a newOperation from BLE")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: got a newOperation from BLE")); } op->addr = addr; @@ -2804,21 +2804,21 @@ void CmndBLEName(void) { char *name = strtok(nullptr, " "); bool write = false; if (name && *name){ - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("write name %s"), name); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: write name %s"), name); op->writelen = strlen(name); memcpy(op->dataToWrite, name, op->writelen); write = true; } else { - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("read name")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: read name")); op->readlen = 1; } res = BLE_ESP32::extQueueOperation(&op); - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("queue res %d"), res); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: queue res %d"), res); if (!res){ // if it fails to add to the queue, do please delete it BLE_ESP32::freeOperation(&op); - AddLog_P(LOG_LEVEL_ERROR,PSTR("Failed to queue new operation - deleted")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Failed to queue new operation - deleted")); ResponseCmndChar(PSTR("QUEUEFAIL")); return; } @@ -2856,7 +2856,7 @@ void CmndBLEOperation(void){ int op = XdrvMailbox.index; - //AddLog_P(LOG_LEVEL_INFO,PSTR("op %d"), op); + //AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: op %d"), op); int res = -1; @@ -2864,7 +2864,7 @@ void CmndBLEOperation(void){ switch(op) { case 0: #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("preview")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: preview")); #endif BLEPostMQTTTrigger = 1; break; @@ -2875,7 +2875,7 @@ void CmndBLEOperation(void){ int opres = BLE_ESP32::newOperation(&prepOperation); if (!opres){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not create new operation")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Could not create new operation")); #endif ResponseCmndChar("FailCreate"); return; @@ -2933,14 +2933,14 @@ void CmndBLEOperation(void){ // this means you could retry with another BLEOp10. // it WOULD be deleted if you sent another BELOP1 #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not queue new operation")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Could not queue new operation")); #endif ResponseCmndChar("FailQueue"); return; } else { // NOTE: prepOperation has been set to null if we queued sucessfully. #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Operations queued:%d"), queuedOperations.size()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: Operations queued:%d"), queuedOperations.size()); #endif char temp[40]; sprintf(temp, "{\"opid\":%d,\"u\":%d}", lastopid-1, u); @@ -2969,13 +2969,13 @@ void CmndBLEOperation(void){ // this means you could retry with another BLEOp10. // it WOULD be deleted if you sent another BELOP1 #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not queue new operation")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Could not queue new operation")); #endif ResponseCmndChar("FailQueue"); } else { // NOTE: prepOperation has been set to null if we queued sucessfully. #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Operations queued:%d"), queuedOperations.size()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: Operations queued:%d"), queuedOperations.size()); #endif char temp[40]; sprintf(temp, "{\"opid\":%d,\"u\":%d}", lastopid-1, u); @@ -3026,20 +3026,20 @@ static void BLEPostMQTT(bool onlycompleted) { if (prepOperation || completedOperations.size() || queuedOperations.size() || currentOperations.size()){ #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("some to show")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: some to show")); #endif if (prepOperation && !onlycompleted){ std::string out = BLETriggerResponse(prepOperation); snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("prep sent %s"), out.c_str()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: prep sent %s"), out.c_str()); #endif } if (queuedOperations.size() && !onlycompleted){ #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("queued %d"), queuedOperations.size()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: queued %d"), queuedOperations.size()); #endif for (int i = 0; i < queuedOperations.size(); i++){ TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEPost1"); @@ -3053,7 +3053,7 @@ static void BLEPostMQTT(bool onlycompleted) { snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("queued %d sent %s"), i, out.c_str()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: queued %d sent %s"), i, out.c_str()); #endif //break; } @@ -3062,7 +3062,7 @@ static void BLEPostMQTT(bool onlycompleted) { if (currentOperations.size() && !onlycompleted){ #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("current %d"), currentOperations.size()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: current %d"), currentOperations.size()); #endif for (int i = 0; i < currentOperations.size(); i++){ TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEPost2"); @@ -3075,7 +3075,7 @@ static void BLEPostMQTT(bool onlycompleted) { snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("curr %d sent %s"), i, out.c_str()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: curr %d sent %s"), i, out.c_str()); #endif //break; } @@ -3084,7 +3084,7 @@ static void BLEPostMQTT(bool onlycompleted) { if (completedOperations.size()){ #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("completed %d"), completedOperations.size()); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: completed %d"), completedOperations.size()); #endif do { generic_sensor_t *toSend = nextOperation(&completedOperations); @@ -3092,7 +3092,7 @@ static void BLEPostMQTT(bool onlycompleted) { break; // break from while loop } else { #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE:completedOperation removed")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: completedOperation removed")); #endif std::string out = BLETriggerResponse(toSend); snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); @@ -3154,7 +3154,7 @@ static void mainThreadBLETimeouts() { static void mainThreadOpCallbacks() { if (completedOperations.size()){ - //AddLog_P(LOG_LEVEL_INFO,PSTR("completed %d"), completedOperations.size()); + //AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: completed %d"), completedOperations.size()); TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEMainCB"); // find this operation in currentOperations, and remove it. @@ -3169,11 +3169,11 @@ static void mainThreadOpCallbacks() { OPCOMPLETE_CALLBACK *pFn = (OPCOMPLETE_CALLBACK *)(op->completecallback); callbackres = pFn(op); #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("op->completecallback %d"), callbackres); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: op->completecallback %d"), callbackres); #endif } catch(const std::exception& e){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in op->completecallback")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: exception in op->completecallback")); #endif } } @@ -3184,14 +3184,14 @@ static void mainThreadOpCallbacks() { OPCOMPLETE_CALLBACK *pFn = operationsCallbacks[i]; callbackres = pFn(op); #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("operationsCallbacks %d %d"), i, callbackres); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: operationsCallbacks %d %d"), i, callbackres); #endif if (callbackres){ break; // this callback ate the op. } } catch(const std::exception& e){ #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in operationsCallbacks")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: exception in operationsCallbacks")); #endif } } @@ -3200,7 +3200,7 @@ static void mainThreadOpCallbacks() { // if some callback told us not to send on MQTT, then remove from completed and delete the data if (callbackres){ #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("callbackres true -> delete op")); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: callbackres true -> delete op")); #endif completedOperations.erase(completedOperations.begin() + i); delete op; @@ -3214,7 +3214,7 @@ static void BLEShow(bool json) { if (json){ #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("show json %d"),json); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: show json %d"),json); #endif uint32_t totalCount = BLEAdvertisment.totalCount; uint32_t deviceCount = seenDevices.size(); @@ -3262,7 +3262,7 @@ static void BLEDiag() uint32_t totalCount = BLEAdvertisment.totalCount; uint32_t deviceCount = seenDevices.size(); #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE:scans:%u,advertisements:%u,devices:%u,resets:%u,BLEStop:%d,BLERunning:%d,BLERunningScan:%d,BLELoopCount:%u,BLEOpCount:%u"), BLEScanCount, totalCount, deviceCount, BLEResets, BLEStop, BLERunning, BLERunningScan, BLELoopCount, BLEOpCount); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: scans:%u,advertisements:%u,devices:%u,resets:%u,BLEStop:%d,BLERunning:%d,BLERunningScan:%d,BLELoopCount:%u,BLEOpCount:%u"), BLEScanCount, totalCount, deviceCount, BLEResets, BLEStop, BLERunning, BLERunningScan, BLELoopCount, BLEOpCount); #endif } @@ -3372,12 +3372,12 @@ void HandleBleConfiguration(void) { #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG, PSTR("HandleBleConfiguration")); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE: HandleBleConfiguration")); #endif if (!HttpCheckPriviledgedAccess()) { #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG, PSTR("!HttpCheckPriviledgedAccess()")); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE: !HttpCheckPriviledgedAccess()")); #endif return; } @@ -3390,12 +3390,12 @@ void HandleBleConfiguration(void) WebGetArg("en", tmp, sizeof(tmp)); #ifdef BLE_ESP32_DEBUG - if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("arg en is %s"), tmp); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE: arg en is %s"), tmp); #endif if (Webserver->hasArg("save")) { #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE SETTINGS SAVE")); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE: SETTINGS SAVE")); #endif Settings.flag5.mi32_enable = Webserver->hasArg("e0"); // BLEScanActiveMode = (Webserver->hasArg("e1")?1:0); // @@ -3405,7 +3405,7 @@ void HandleBleConfiguration(void) return; } #ifdef BLE_ESP32_DEBUG - AddLog_P(LOG_LEVEL_DEBUG, PSTR("!SAVE")); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE: !SAVE")); #endif char str[TOPSZ]; @@ -3460,7 +3460,7 @@ void HandleBleConfiguration(void) \*********************************************************************************************/ int ExtStopBLE(){ - AddLog_P(LOG_LEVEL_INFO, PSTR("Stopping BLE if active - upgrade starting?")); + AddLog_P(LOG_LEVEL_INFO, PSTR("BLE: Stopping if active")); BLE_ESP32::BLEMode = BLE_ESP32::BLEModeDisabled; BLE_ESP32::StopBLE(); return 0; @@ -3548,13 +3548,13 @@ int myAdvertCallback(BLE_ESP32::ble_advertisment_t *pStruct) { // this one is used to demonstrate processing ALL operations int myOpCallback(BLE_ESP32::generic_sensor_t *pStruct){ - AddLog_P(LOG_LEVEL_INFO,PSTR("myOpCallback")); + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: myOpCallback")); return 0; // return true to block MQTT broadcast } // this one is used to demonstrate processing of ONE specific operation int myOpCallback2(BLE_ESP32::generic_sensor_t *pStruct){ - AddLog_P(LOG_LEVEL_INFO,PSTR("myOpCallback2")); + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: myOpCallback2")); return 1; // return true to block MQTT broadcast } #endif @@ -3577,7 +3577,7 @@ void sendExample(){ BLE_ESP32::generic_sensor_t *op = nullptr; int res = BLE_ESP32::newOperation(&op); if (!res){ - AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not create new operation")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Could not create new operation")); return; } strncpy(op->MAC, "001A22092EE0", sizeof(op->MAC)); @@ -3592,7 +3592,7 @@ void sendExample(){ if (!res){ // if it fails to add to the queue, do please delete it BLE_ESP32::freeOperation(&op); - AddLog_P(LOG_LEVEL_ERROR,PSTR("Failed to queue new operation - deleted")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Failed to queue new operation - deleted")); return; } diff --git a/tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino b/tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino index 832aae2e7..c6a3e3111 100644 --- a/tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino +++ b/tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino @@ -97,15 +97,15 @@ struct { struct { // the slot currently having it's battery read // set to 0 to start a battery read cycle - uint8_t slot = 255; - uint8_t active = 0; + uint8_t slot = 255; + uint8_t active = 0; } batteryreader; struct { // the slot currently having it's battery read // set to 0 to start a battery read cycle - uint8_t slot = 255; - uint8_t active = 0; + uint8_t slot = 255; + uint8_t active = 0; } sensorreader; struct { @@ -246,7 +246,7 @@ struct PVVXPacket_t { uint16_t battery_mv; // mV uint8_t battery_level; // 0..100 % uint8_t counter; // measurement count - uint8_t flags; + uint8_t flags; }; #pragma pack(0) @@ -362,7 +362,7 @@ void (*const MI32_Commands[])(void) PROGMEM = { #define MI_MI32_TYPES 13 //count this manually -const uint16_t kMI32DeviceID[MI_MI32_TYPES]={ +const uint16_t kMI32DeviceID[MI_MI32_TYPES]={ 0x0000, // Unkown 0x0098, // Flora 0x01aa, // MJ_HT_V1 @@ -419,8 +419,8 @@ const char *MHOC303_TimeChar = LYWSD02_TimeChar; const char *MHOC401_Svc = LYWSD02_Svc; const char *MHOC401_BattNotifyChar = LYWSD02_BattNotifyChar; -const char CGD1_Svc[] PROGMEM = "180F"; -const char CGD1_BattChar[] PROGMEM = "2A19"; +const char CGD1_Svc[] PROGMEM = "180F"; +const char CGD1_BattChar[] PROGMEM = "2A19"; const char FLORA_Svc[] PROGMEM = "00001204-0000-1000-8000-00805F9B34FB"; const char FLORA_BattChar[] PROGMEM = "00001A02-0000-1000-8000-00805F9B34FB"; @@ -443,7 +443,7 @@ enum MI32_MI_OP_TYPES { enum MI32_MI_KEY_REQ { - KEY_REQUIREMENT_UNKNOWN = 0, // we don't know if a key is needed + KEY_REQUIREMENT_UNKNOWN = 0, // we don't know if a key is needed KEY_NOT_REQUIRED = 1, // we got an unencrypted payload KEY_REQUIRED_BUT_NOT_FOUND = 2, // we got an encrypted packet, but had not key KEY_REQUIRED_AND_FOUND = 3, // we got an encrypted packet, and could decrypt @@ -490,7 +490,7 @@ int toggleUnit(BLE_ESP32::generic_sensor_t *op){ bool MI32Operation(int slot, int optype, const char *svc, const char *charactistic, const char *notifychar = nullptr, const uint8_t *data = nullptr, int datalen = 0, uint8_t *addr = nullptr ) { if (!svc || !svc[0]){ - AddLog_P(LOG_LEVEL_ERROR,PSTR("MI32Op: inv svc")); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: Op inv svc")); return 0; } @@ -499,17 +499,17 @@ bool MI32Operation(int slot, int optype, const char *svc, const char *charactist // ALWAYS use this function to create a new one. int res = BLE_ESP32::newOperation(&op); if (!res){ - AddLog_P(LOG_LEVEL_ERROR,PSTR("Can't get a newOperation from BLE")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("M32: Can't get a newOperation")); return 0; } else { - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got a newOperation from BLE")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: Got a newOperation")); } if (slot >= 0){ op->addr = NimBLEAddress(MIBLEsensors[slot].MAC); } else { if (!addr){ - AddLog_P(LOG_LEVEL_ERROR,PSTR("no addr")); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: No addr")); BLE_ESP32::freeOperation(&op); return 0; } @@ -521,7 +521,7 @@ bool MI32Operation(int slot, int optype, const char *svc, const char *charactist if (!op->serviceUUID.bitSize()){ BLE_ESP32::freeOperation(&op); - AddLog_P(LOG_LEVEL_ERROR,PSTR("MI: Bad service string %s"), svc); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: MI Bad service string %s"), svc); return 0; } @@ -531,7 +531,7 @@ bool MI32Operation(int slot, int optype, const char *svc, const char *charactist op->characteristicUUID = NimBLEUUID(charactistic); if (!op->characteristicUUID.bitSize()){ BLE_ESP32::freeOperation(&op); - AddLog_P(LOG_LEVEL_ERROR,PSTR("MI: Bad characteristic string %s"), charactistic); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: MI Bad characteristic string %s"), charactistic); return 0; } } @@ -539,10 +539,10 @@ bool MI32Operation(int slot, int optype, const char *svc, const char *charactist op->notificationCharacteristicUUID = NimBLEUUID(notifychar); if (!op->notificationCharacteristicUUID.bitSize()){ BLE_ESP32::freeOperation(&op); - AddLog_P(LOG_LEVEL_ERROR,PSTR("MI: Bad notifycharacteristic string %s"), notifychar); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: MI Bad notifycharacteristic string %s"), notifychar); return 0; } - } + } if (data && datalen) { op->writelen = datalen; @@ -564,13 +564,13 @@ bool MI32Operation(int slot, int optype, const char *svc, const char *charactist uint32_t context = (optype << 24) | (MIBLEsensors[slot].type << 16) | slot; op->context = (void *)context; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI s:%d op:%s"), slot, BLE_ESP32::BLETriggerResponse(op).c_str()); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: MI s:%d op:%s"), slot, BLE_ESP32::BLETriggerResponse(op).c_str()); res = BLE_ESP32::extQueueOperation(&op); if (!res){ // if it fails to add to the queue, do please delete it BLE_ESP32::freeOperation(&op); - AddLog_P(LOG_LEVEL_ERROR,PSTR("Failed to queue new operation - deleted")); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: Failed to queue new operation - deleted")); } return res; @@ -591,10 +591,10 @@ int genericBatReadFn(int slot){ break; // these read a characteristic - case MI_FLORA: + case MI_FLORA: res = MI32Operation(slot, OP_BATT_READ, FLORA_Svc, FLORA_BattChar); break; - case MI_LYWSD02: + case MI_LYWSD02: res = MI32Operation(slot, OP_BATT_READ, LYWSD02_Svc, LYWSD02_BattChar); break; case MI_CGD1: @@ -611,9 +611,9 @@ int genericBatReadFn(int slot){ break; } if (res > 0){ - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Req batt read slot %d type %d queued"), slot, MIBLEsensors[slot].type); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO, PSTR("M32: Req batt read slot %d type %d queued"), slot, MIBLEsensors[slot].type); } else { - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Req batt read slot %d type %d non-queued res %d"), slot, MIBLEsensors[slot].type, res); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO, PSTR("M32: Req batt read slot %d type %d non-queued res %d"), slot, MIBLEsensors[slot].type, res); } return res; } @@ -627,17 +627,17 @@ int genericSensorReadFn(int slot, int force){ so although the characteristic seems to exist, it does not work? further dev required with sensor to hand. case MI_LYWSD02: - // don't read if key present and we've decoded at least one advert + // don't read if key present and we've decoded at least one advert if (MIBLEsensors[slot].needkey == KEY_REQUIRED_AND_FOUND) return -2; res = MI32Operation(slot, OP_READ_HT_LY, LYWSD02_Svc, nullptr, LYWSD02_BattNotifyChar); break;*/ case MI_LYWSD03MMC: - // don't read if key present and we've decoded at least one advert + // don't read if key present and we've decoded at least one advert if (MIBLEsensors[slot].needkey == KEY_REQUIRED_AND_FOUND && !force) return -2; res = MI32Operation(slot, OP_READ_HT_LY, LYWSD03_Svc, nullptr, LYWSD03_BattNotifyChar); break; case MI_MHOC401: - // don't read if key present and we've decoded at least one advert + // don't read if key present and we've decoded at least one advert if (MIBLEsensors[slot].needkey == KEY_REQUIRED_AND_FOUND && !force) return -2; res = MI32Operation(slot, OP_READ_HT_LY, MHOC401_Svc, nullptr, MHOC401_BattNotifyChar); break; @@ -653,22 +653,22 @@ int genericSensorReadFn(int slot, int force){ // called once per second int readOneSensor(){ if (MI32.sensorreader.active){ - AddLog_P(LOG_LEVEL_DEBUG,PSTR("readOneSensor - already active reading %d"), MI32.sensorreader.slot-1); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: readOneSensor - already active reading %d"), MI32.sensorreader.slot-1); return 0; } // loop if the sensor at the slot does not need to be read // i.e. drop out of loop when we start a read, or hit the end - int res = -1; + int res = -1; do { // MI32.sensorreader.slot is reset to zero to trigger a read sequence if (MI32.sensorreader.slot >= MIBLEsensors.size()){ - //AddLog_P(LOG_LEVEL_DEBUG,PSTR("readOneSensor past end of slots - %d > %d"), MI32.sensorreader.slot, MIBLEsensors.size()); + //AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE: readOneSensor past end of slots - %d > %d"), MI32.sensorreader.slot, MIBLEsensors.size()); return 0; } res = genericSensorReadFn(MI32.sensorreader.slot, 0); - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("genericSensorReadFn slot %d res %d"), MI32.sensorreader.slot, res); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: genericSensorReadFn slot %d res %d"), MI32.sensorreader.slot, res); // if this sensor in this slot does not need to be read via notify, just move on top the next one if (res < 0){ @@ -680,7 +680,7 @@ int readOneSensor(){ if (res == 0){ // can't read at the moment (no operations available?) - AddLog_P(LOG_LEVEL_DEBUG,PSTR("readOneSensor no ops available slot %d res %d"), MI32.sensorreader.slot, res); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: readOneSensor no ops available slot %d res %d"), MI32.sensorreader.slot, res); return 0; } @@ -689,7 +689,7 @@ int readOneSensor(){ // and make it wait until the read/notify is complete // this is cleared in the response callback. MI32.sensorreader.active = 1; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("readOneSensor reading for slot %d res %d"), MI32.sensorreader.slot-1, res); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: readOneSensor reading for slot %d res %d"), MI32.sensorreader.slot-1, res); // started one return 1; @@ -714,7 +714,7 @@ int readOneBat(){ if (res < 0){ MI32.batteryreader.slot++; if (MI32.batteryreader.slot >= MIBLEsensors.size()){ - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Batt loop complete at %d"), MI32.batteryreader.slot); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO, PSTR("M32: Batt loop complete at %d"), MI32.batteryreader.slot); } return 0; } @@ -730,7 +730,7 @@ int readOneBat(){ // this is cleared in the response callback. MI32.batteryreader.active = 1; if (MI32.batteryreader.slot >= MIBLEsensors.size()){ - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Batt loop will complete at %d"), MI32.batteryreader.slot); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO, PSTR("M32: Batt loop will complete at %d"), MI32.batteryreader.slot); } // started one return 1; @@ -816,7 +816,7 @@ int genericTimeWriteFn(int slot){ int genericOpCompleteFn(BLE_ESP32::generic_sensor_t *op){ uint32_t context = (uint32_t) op->context; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI op complete context %x"), context); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: MI op complete context %x"), context); int opType = context >> 24; int devType = (context >> 16) & 0xff; @@ -832,12 +832,12 @@ int genericOpCompleteFn(BLE_ESP32::generic_sensor_t *op){ bool fail = false; if (op->addr != addr){ // slot changed during operation? - AddLog_P(LOG_LEVEL_ERROR,PSTR("Slot mac changed during an operation")); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: Slot mac changed during an operation")); fail = true; } if (op->state <= GEN_STATE_FAILED){ - AddLog_P(LOG_LEVEL_ERROR,PSTR("operation failed %d for %s"), op->state, slotMAC); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: Operation failed %d for %s"), op->state, slotMAC); fail = true; } @@ -857,7 +857,7 @@ int genericOpCompleteFn(BLE_ESP32::generic_sensor_t *op){ switch(opType){ case OP_TIME_WRITE: - AddLog_P(LOG_LEVEL_DEBUG,PSTR("Time write for %s complete"), slotMAC); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: Time write for %s complete"), slotMAC); return 0; // nothing to do case OP_BATT_READ:{ uint8_t *data = nullptr; @@ -876,33 +876,33 @@ int genericOpCompleteFn(BLE_ESP32::generic_sensor_t *op){ // allow another... MI32.batteryreader.active = 0; - AddLog_P(LOG_LEVEL_INFO,PSTR("batt read slot %d done state %x"), slot, op->state); + AddLog_P(LOG_LEVEL_INFO, PSTR("M32: Batt read slot %d done state %x"), slot, op->state); } return 0; case OP_UNIT_WRITE: // nothing more to do? - AddLog_P(LOG_LEVEL_DEBUG,PSTR("Unit write for %s complete"), slotMAC); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: Unit write for %s complete"), slotMAC); return 0; case OP_UNIT_READ: { - uint8_t currUnit = op->dataRead[0]; - AddLog_P(LOG_LEVEL_DEBUG,PSTR("Unit read for %s complete %d"), slotMAC, currUnit); + uint8_t currUnit = op->dataRead[0]; + AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: Unit read for %s complete %d"), slotMAC, currUnit); } return 0; case OP_UNIT_TOGGLE: { - uint8_t currUnit = op->dataToWrite[0]; - AddLog_P(LOG_LEVEL_DEBUG,PSTR("Unit toggle for %s complete %d->%d; datasize was %d"), slotMAC, op->dataRead[0], op->dataToWrite[0], op->readlen); + uint8_t currUnit = op->dataToWrite[0]; + AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: Unit toggle for %s complete %d->%d; datasize was %d"), slotMAC, op->dataRead[0], op->dataToWrite[0], op->readlen); } return 0; case OP_READ_HT_LY: { // allow another... MI32.sensorreader.active = 0; MI32notifyHT_LY(slot, (char*)op->dataNotify, op->notifylen); - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("HT_LY notify for %s complete"), slotMAC); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: HT_LY notify for %s complete"), slotMAC); } return 0; default: - AddLog_P(LOG_LEVEL_ERROR,PSTR("OpType %d not recognised?"), opType); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: OpType %d not recognised?"), opType); return 0; } @@ -914,7 +914,7 @@ int MI32advertismentCallback(BLE_ESP32::ble_advertisment_t *pStruct) // we will try not to use this... BLEAdvertisedDevice *advertisedDevice = pStruct->advertisedDevice; - // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Advertised Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData(0).length()); + // AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: Advertised Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData(0).length()); int RSSI = pStruct->RSSI; const uint8_t *addr = pStruct->addr; if(MI32isInBlockList(addr) == true) return 0; @@ -937,14 +937,14 @@ int MI32advertismentCallback(BLE_ESP32::ble_advertisment_t *pStruct) char temp[60]; BLE_ESP32::dump(temp, 13, addr, 6); - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("MI:%s svc[0] UUID (%x)"), temp, UUID); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("M32: MI:%s svc[0] UUID (%x)"), temp, UUID); std::string ServiceDataStr = advertisedDevice->getServiceData(0); - + uint32_t ServiceDataLength = ServiceDataStr.length(); const uint8_t *ServiceData = (const uint8_t *)ServiceDataStr.data(); BLE_ESP32::dump(temp, 60, ServiceData, ServiceDataLength); - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("MI:%s"), temp); - + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("M32: MI:%s"), temp); + if (UUID){ // this will take and keep the mutex until the function is over @@ -1041,14 +1041,14 @@ int MI32AddKey(char* payload, char* key = nullptr){ bool unknownKey = true; for(uint32_t i=0; i 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: search key for MAC: %02x%02x%02x%02x%02x%02x"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: Search key for MAC: %02x%02x%02x%02x%02x%02x"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); for(uint32_t i=0; i 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: decryption Key found")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Decryption Key found")); foundNoKey = false; break; } } if(foundNoKey){ - AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: no Key found !!")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: No Key found")); return -2; // indicates needs key } @@ -1098,7 +1098,7 @@ int MIDecryptPayload(const uint8_t *macin, const uint8_t *nonce, uint32_t tag, u // returns 1 if matched, else 0 int ret = br_ccm_check_tag(&ctx, &tag); - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: Err:%i, Decrypted : %02x %02x %02x %02x %02x"), ret, payload[1],payload[2],payload[3],payload[4],payload[5]); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Err:%i, Decrypted : %02x %02x %02x %02x %02x"), ret, payload[1],payload[2],payload[3],payload[4],payload[5]); return ret-1; // -> -1=fail, 0=success } @@ -1168,7 +1168,7 @@ int MIParsePacket(const uint8_t* slotmac, struct mi_beacon_data_t *parsed, const parsed->devicetype = *((uint16_t *)(data + byteindex)); byteindex += 2; parsed->framecnt = data[byteindex]; - //if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI frame %d"), parsed->framecnt); + //if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: MI frame %d"), parsed->framecnt); byteindex++; @@ -1184,7 +1184,7 @@ int MIParsePacket(const uint8_t* slotmac, struct mi_beacon_data_t *parsed, const byteindex += 6; } - int decres = 1; + int decres = 1; // everything after MAC is encrypted if specified? if (parsed->framedata.isencrypted){ if (len < byteindex + 3+4+1){ @@ -1217,17 +1217,17 @@ int MIParsePacket(const uint8_t* slotmac, struct mi_beacon_data_t *parsed, const break; case 0: // suceeded parsed->needkey = KEY_REQUIRED_AND_FOUND; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI payload decrypted")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Payload decrypted")); break; case -1: // key failed to work parsed->needkey = KEY_REQUIRED_AND_INVALID; - AddLog_P(LOG_LEVEL_ERROR,PSTR("MI payload decrypt failed")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("M32: Payload decrypt failed")); parsed->payloadpresent = 0; return 0; break; case -2: // key not present parsed->needkey = KEY_REQUIRED_BUT_NOT_FOUND; - AddLog_P(LOG_LEVEL_ERROR,PSTR("MI payload encrypted but no key")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("M32: Payload encrypted but no key")); parsed->payloadpresent = 0; return 0; break; @@ -1262,7 +1262,7 @@ int MIParsePacket(const uint8_t* slotmac, struct mi_beacon_data_t *parsed, const } if ((len - byteindex) == 0){ - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI no payload")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: No payload")); parsed->payload.size = 0; parsed->payloadpresent = 0; return 0; @@ -1271,14 +1271,14 @@ int MIParsePacket(const uint8_t* slotmac, struct mi_beacon_data_t *parsed, const // we have payload which did not need decrypt. if (decres == 1){ parsed->needkey = KEY_NOT_REQUIRED; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI payload unencrypted")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Payload unencrypted")); } // already decrypted if required parsed->payloadpresent = 1; memcpy(&parsed->payload, (data + byteindex), (len - byteindex)); if (parsed->payload.size != (len - byteindex) - 3){ - AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI payload length mismatch")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Payload length mismatch")); } return 1; @@ -1319,7 +1319,7 @@ void MI32nullifyEndOfMQTT_DATA(){ */ uint32_t MIBLEgetSensorSlot(const uint8_t *mac, uint16_t _type, uint8_t counter){ - //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: will test ID-type: %x"),D_CMND_MI32, _type); + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: will test ID-type: %x"),D_CMND_MI32, _type); bool _success = false; for (uint32_t i=0; i < MI_MI32_TYPES; i++){ // i < sizeof(kMI32DeviceID) gives compiler warning if(_type == kMI32DeviceID[i]){ @@ -1328,40 +1328,40 @@ uint32_t MIBLEgetSensorSlot(const uint8_t *mac, uint16_t _type, uint8_t counter) break; } else { - //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: ID-type is not: %x"),D_CMND_MI32,kMI32DeviceID[i]); + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: ID-type is not: %x"),D_CMND_MI32,kMI32DeviceID[i]); } } if(!_success) { _type = 1; // unknown } - //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: vector size %u"),D_CMND_MI32, MIBLEsensors.size()); + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: vector size %u"),D_CMND_MI32, MIBLEsensors.size()); for(uint32_t i=0; i 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: slot: %u/%u - ign repeat"),D_CMND_MI32, i, MIBLEsensors.size()); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: slot: %u/%u - ign repeat"),D_CMND_MI32, i, MIBLEsensors.size()); //return 0xff; // packet received before, stop here } - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI frame %d, last %d"), counter, MIBLEsensors[i].lastCnt); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Frame %d, last %d"), counter, MIBLEsensors[i].lastCnt); MIBLEsensors[i].lastCnt = counter; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: slot: %u/%u"),D_CMND_MI32, i, MIBLEsensors.size()); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: slot: %u/%u"),D_CMND_MI32, i, MIBLEsensors.size()); if (MIBLEsensors[i].type != _type){ // this happens on incorrectly configured pvvx ATC firmware - AddLog_P(LOG_LEVEL_ERROR,PSTR("%s: slot: %u - device type 0x%04x(%s) -> 0x%04x(%s) - check device is only sending one type of advert."),D_CMND_MI32, i, + AddLog_P(LOG_LEVEL_ERROR,PSTR("M32: %s: slot: %u - device type 0x%04x(%s) -> 0x%04x(%s) - check device is only sending one type of advert."),D_CMND_MI32, i, kMI32DeviceID[MIBLEsensors[i].type-1], kMI32DeviceType[MIBLEsensors[i].type-1], kMI32DeviceID[_type-1], kMI32DeviceType[_type-1]); MIBLEsensors[i].type = _type; } return i; } - //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: i: %x %x %x %x %x %x"),D_CMND_MI32, MIBLEsensors[i].MAC[5], MIBLEsensors[i].MAC[4],MIBLEsensors[i].MAC[3],MIBLEsensors[i].MAC[2],MIBLEsensors[i].MAC[1],MIBLEsensors[i].MAC[0]); - //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: n: %x %x %x %x %x %x"),D_CMND_MI32, mac[5], mac[4], mac[3],mac[2],mac[1],mac[0]); + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: i: %x %x %x %x %x %x"),D_CMND_MI32, MIBLEsensors[i].MAC[5], MIBLEsensors[i].MAC[4],MIBLEsensors[i].MAC[3],MIBLEsensors[i].MAC[2],MIBLEsensors[i].MAC[1],MIBLEsensors[i].MAC[0]); + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: n: %x %x %x %x %x %x"),D_CMND_MI32, mac[5], mac[4], mac[3],mac[2],mac[1],mac[0]); } - //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: new sensor -> slot: %u"),D_CMND_MI32, MIBLEsensors.size()); - //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: found new sensor"),D_CMND_MI32); + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: new sensor -> slot: %u"),D_CMND_MI32, MIBLEsensors.size()); + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: found new sensor"),D_CMND_MI32); mi_sensor_t _newSensor; memset(&_newSensor, 0 , sizeof(_newSensor)); memcpy(_newSensor.MAC, mac, 6); @@ -1411,7 +1411,7 @@ uint32_t MIBLEgetSensorSlot(const uint8_t *mac, uint16_t _type, uint8_t counter) break; } MIBLEsensors.push_back(_newSensor); - AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: new %s at slot: %u"),D_CMND_MI32, kMI32DeviceType[_type-1],MIBLEsensors.size()-1); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: %s: new %s at slot: %u"),D_CMND_MI32, kMI32DeviceType[_type-1],MIBLEsensors.size()-1); MI32.mode.shallShowStatusInfo = 1; return MIBLEsensors.size()-1; }; @@ -1444,7 +1444,7 @@ void MI32StatusInfo() { int MI32scanCompleteCallback(NimBLEScanResults results){ // we actually don't need to do anything here.... - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: scancomplete")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Scan complete")); return 0; } @@ -1473,7 +1473,7 @@ void MI32Init(void) { // note: for operations, we will set individual callbacks in the operations we request //void registerForOpCallbacks(const char *tag, BLE_ESP32::OPCOMPLETE_CALLBACK* pFn); - AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: init: request callbacks")); + AddLog_P(LOG_LEVEL_INFO,PSTR("M32: init: request callbacks")); MI32.period = Settings.tele_period; MI32.mode.init = 1; return; @@ -1496,19 +1496,19 @@ int MIParseBatt(int slot, uint8_t *data, int len){ MIBLEsensors[slot].bat = value; if(MIBLEsensors[slot].type==MI_FLORA){ if (len < 7){ - AddLog_P(LOG_LEVEL_ERROR,PSTR("FLORA: not enough bytes read for firmware?")); + AddLog_P(LOG_LEVEL_ERROR,PSTR("M32: FLORA: not enough bytes read for firmware?")); } else { memcpy(MIBLEsensors[slot].firmware, data+2, 5); MIBLEsensors[slot].firmware[5] = '\0'; - AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: FLORA Firmware: %s"),D_CMND_MI32,MIBLEsensors[slot].firmware); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: %s: FLORA Firmware: %s"),D_CMND_MI32,MIBLEsensors[slot].firmware); } } MIBLEsensors[slot].eventType.bat = 1; MIBLEsensors[slot].shallSendMQTT = 1; MI32.mode.shallTriggerTele = 1; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Batt read for %s complete %d"), slotMAC, value); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Batt read for %s complete %d"), slotMAC, value); } else { - AddLog_P(LOG_LEVEL_ERROR,PSTR("Batt read for %s complete but out of range 1-101 (%d)"), slotMAC, value); + AddLog_P(LOG_LEVEL_ERROR,PSTR("M32: Batt read for %s complete but out of range 1-101 (%d)"), slotMAC, value); } return 0; @@ -1534,13 +1534,13 @@ void MI32ParseATCPacket(const uint8_t * _buf, uint32_t length, const uint8_t *ad //uint16_t battery_mv; // mV //uint8_t battery_level; // 0..100 % //uint8_t counter; // measurement count - //uint8_t flags; + //uint8_t flags; uint32_t _slot = MIBLEgetSensorSlot(addr, 0x0a1c, ppv_packet->counter); // This must be a hard-coded fake ID if(_slot==0xff) return; if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s:pvvx at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: %s:pvvx at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); MIBLEsensors[_slot].RSSI=RSSI; MIBLEsensors[_slot].needkey=KEY_NOT_REQUIRED; @@ -1557,7 +1557,7 @@ void MI32ParseATCPacket(const uint8_t * _buf, uint32_t length, const uint8_t *ad } return; } else { - AddLog_P(LOG_LEVEL_ERROR, PSTR("PVVX packet mac mismatch - ignored?")); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: PVVX packet mac mismatch - ignored?")); return; } } @@ -1571,9 +1571,9 @@ void MI32ParseATCPacket(const uint8_t * _buf, uint32_t length, const uint8_t *ad if (memcmp(addrrev, _packet->MAC, 6)){ MI32_ReverseMAC(_packet->MAC); if (!memcmp(addrrev, _packet->MAC, 6)){ - AddLog_P(LOG_LEVEL_ERROR, PSTR("ATC packet with reversed MAC addr?")); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: ATC packet with reversed MAC addr?")); } else { - AddLog_P(LOG_LEVEL_ERROR, PSTR("ATC packet with MAC addr mismatch - is this mesh?")); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: ATC packet with MAC addr mismatch - is this mesh?")); memcpy(addrrev, _packet->MAC, 6); } addr = addrrev; @@ -1584,7 +1584,7 @@ void MI32ParseATCPacket(const uint8_t * _buf, uint32_t length, const uint8_t *ad if(_slot==0xff) return; if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: %s at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); MIBLEsensors[_slot].RSSI=RSSI; MIBLEsensors[_slot].needkey=KEY_NOT_REQUIRED; @@ -1606,17 +1606,17 @@ void MI32ParseATCPacket(const uint8_t * _buf, uint32_t length, const uint8_t *ad //////////////////////////////////////////////////////////// // this SHOULD parse any MI payload. int MI32parseMiPayload(int _slot, struct mi_beacon_data_t *parsed){ - struct mi_beacon_data_payload_data_t *pld = + struct mi_beacon_data_payload_data_t *pld = (struct mi_beacon_data_payload_data_t *) &parsed->payload.data; int res = 1; - + if (!parsed->payloadpresent){ return 0; } char tmp[20]; BLE_ESP32::dump(tmp, 20, (uint8_t*)&(parsed->payload), parsed->payload.size+3); - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("MI%d payload %s"), _slot, tmp); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: MI%d payload %s"), _slot, tmp); switch(parsed->payload.type){ case 0x01: // button press @@ -1624,7 +1624,7 @@ int MI32parseMiPayload(int _slot, struct mi_beacon_data_t *parsed){ MIBLEsensors[_slot].eventType.Btn = 1; MI32.mode.shallTriggerTele = 1; break; - case 0x02: + case 0x02: res = 0; break; case 0x03: {// motion? 1 byte @@ -1636,22 +1636,22 @@ int MI32parseMiPayload(int _slot, struct mi_beacon_data_t *parsed){ if(_tempFloat<60){ MIBLEsensors[_slot].temp=_tempFloat; MIBLEsensors[_slot].eventType.temp = 1; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 4: temp updated")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode 4: temp updated")); } else { - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 4: temp ignored > 60 (%f)"), _tempFloat); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode 4: temp ignored > 60 (%f)"), _tempFloat); } - // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 4: U16: %u Temp"), _beacon.temp ); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Mode 4: U16: %u Temp"), _beacon.temp ); } break; case 0x06: { float _tempFloat=(float)(pld->hum)/10.0f; if(_tempFloat<101){ MIBLEsensors[_slot].hum=_tempFloat; MIBLEsensors[_slot].eventType.hum = 1; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 6: hum updated")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode 6: hum updated")); } else { - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 6: hum ignored > 101 (%f)"), _tempFloat); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode 6: hum ignored > 101 (%f)"), _tempFloat); } - // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 6: U16: %u Hum"), _beacon.hum); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Mode 6: U16: %u Hum"), _beacon.hum); } break; case 0x07: MIBLEsensors[_slot].lux=pld->lux & 0x00ffffff; @@ -1659,19 +1659,19 @@ int MI32parseMiPayload(int _slot, struct mi_beacon_data_t *parsed){ MIBLEsensors[_slot].eventType.noMotion = 1; } MIBLEsensors[_slot].eventType.lux = 1; - // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 7: U24: %u Lux"), _beacon.lux & 0x00ffffff); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Mode 7: U24: %u Lux"), _beacon.lux & 0x00ffffff); break; case 0x08: MIBLEsensors[_slot].moisture=pld->moist; MIBLEsensors[_slot].eventType.moist = 1; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 8: moisture updated")); - // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 8: U8: %u Moisture"), _beacon.moist); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode 8: moisture updated")); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Mode 8: U8: %u Moisture"), _beacon.moist); break; case 0x09: // 'conductivity' MIBLEsensors[_slot].fertility=pld->fert; MIBLEsensors[_slot].eventType.fert = 1; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 9: fertility updated")); - // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 9: U16: %u Fertility"), _beacon.fert); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode 9: fertility updated")); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Mode 9: U16: %u Fertility"), _beacon.fert); break; case 0x0a: if(MI32.option.ignoreBogusBattery){ @@ -1683,31 +1683,31 @@ int MI32parseMiPayload(int _slot, struct mi_beacon_data_t *parsed){ if(pld->bat<101){ MIBLEsensors[_slot].bat = pld->bat; MIBLEsensors[_slot].eventType.bat = 1; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode a: bat updated")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode a: bat updated")); } else { MIBLEsensors[_slot].bat = 100; MIBLEsensors[_slot].eventType.bat = 1; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode a: bat > 100 (%d)"), pld->bat); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode a: bat > 100 (%d)"), pld->bat); } - // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode a: U8: %u %%"), _beacon.bat); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Mode a: U8: %u %%"), _beacon.bat); break; case 0x0d:{ float _tempFloat=(float)(pld->HT.temp)/10.0f; if(_tempFloat < 60){ MIBLEsensors[_slot].temp = _tempFloat; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode d: temp updated")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode d: temp updated")); } else { - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode d: temp ignored > 60 (%f)"), _tempFloat); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode d: temp ignored > 60 (%f)"), _tempFloat); } _tempFloat=(float)(pld->HT.hum)/10.0f; if(_tempFloat < 100){ MIBLEsensors[_slot].hum = _tempFloat; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode d: hum updated")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode d: hum updated")); } else { - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode d: hum ignored > 100 (%f)"), _tempFloat); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode d: hum ignored > 100 (%f)"), _tempFloat); } MIBLEsensors[_slot].eventType.tempHum = 1; - // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode d: U16: %x Temp U16: %x Hum"), _beacon.HT.temp, _beacon.HT.hum); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Mode d: U16: %x Temp U16: %x Hum"), _beacon.HT.temp, _beacon.HT.hum); } break; case 0x0f: if (parsed->payload.ten != 0) break; @@ -1718,7 +1718,7 @@ int MI32parseMiPayload(int _slot, struct mi_beacon_data_t *parsed){ MIBLEsensors[_slot].eventType.lux = 1; MIBLEsensors[_slot].NMT = 0; MI32.mode.shallTriggerTele = 1; - // AddLog_P(LOG_LEVEL_DEBUG,PSTR("PIR: primary"),MIBLEsensors[_slot].lux ); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: PIR: primary"),MIBLEsensors[_slot].lux ); break; case 0x10:{ // 'formaldehide' const uint16_t f = uint16_t(parsed->payload.data[0]) | (uint16_t(parsed->payload.data[1]) << 8); @@ -1742,11 +1742,11 @@ int MI32parseMiPayload(int _slot, struct mi_beacon_data_t *parsed){ MIBLEsensors[_slot].NMT = pld->NMT; MIBLEsensors[_slot].eventType.NMT = 1; MI32.mode.shallTriggerTele = 1; - // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 17: NMT: %u seconds"), _beacon.NMT); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Mode 17: NMT: %u seconds"), _beacon.NMT); } break; default: { - AddLog_P(LOG_LEVEL_DEBUG,PSTR("Unknown MI pld")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Unknown MI pld")); res = 0; } break; } @@ -1771,7 +1771,7 @@ void MI32ParseResponse(const uint8_t *buf, uint16_t bufsize, const uint8_t* addr MI32_ReverseMAC(addrrev); if (memcmp(addrrev, parsed.macdata.mac, 6)){ - AddLog_P(LOG_LEVEL_ERROR, PSTR("MI packet with MAC addr mismatch - is this mesh?")); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: MI packet with MAC addr mismatch - is this mesh?")); memcpy(addrrev, parsed.macdata.mac, 6); MI32_ReverseMAC(addrrev); addr = addrrev; @@ -1781,11 +1781,11 @@ void MI32ParseResponse(const uint8_t *buf, uint16_t bufsize, const uint8_t* addr if(_slot==0xff) return; if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ if (parsed.needkey != KEY_REQUIREMENT_UNKNOWN){ - MIBLEsensors[_slot].needkey = parsed.needkey; + MIBLEsensors[_slot].needkey = parsed.needkey; } MIBLEsensors[_slot].RSSI=RSSI; if (!res){ // - if the payload is not valid - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("MIParsePacket returned %d"), res); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: MIParsePacket returned %d"), res); return; } else { } @@ -1806,7 +1806,7 @@ void MI32removeMIBLEsensor(uint8_t* MAC){ TasAutoMutex localmutex(&slotmutex, "Mi32Rem"); MIBLEsensors.erase( std::remove_if( MIBLEsensors.begin() , MIBLEsensors.end(), [MAC]( mi_sensor_t _sensor )->bool - { return (memcmp(_sensor.MAC,MAC,6) == 0); } + { return (memcmp(_sensor.MAC,MAC,6) == 0); } ), end( MIBLEsensors ) ); } /***********************************************************************\ @@ -1814,14 +1814,14 @@ void MI32removeMIBLEsensor(uint8_t* MAC){ \***********************************************************************/ void MI32notifyHT_LY(int slot, char *_buf, int len){ - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,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 (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %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]); // the value 0b00 is 28.16 C? if(_buf[0] != 0 || _buf[1] != 0){ memcpy(&LYWSD0x_HT,(void *)_buf,sizeof(LYWSD0x_HT)); - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("%s: T * 100: %u, H: %u, V: %u"),D_CMND_MI32,LYWSD0x_HT.temp,LYWSD0x_HT.hum, LYWSD0x_HT.volt); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: %s: T * 100: %u, H: %u, V: %u"),D_CMND_MI32,LYWSD0x_HT.temp,LYWSD0x_HT.hum, LYWSD0x_HT.volt); uint32_t _slot = slot; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("MIBLE: Sensor slot: %u"), _slot); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: MIBLE: Sensor slot: %u"), _slot); static float _tempFloat; _tempFloat=(float)(LYWSD0x_HT.temp)/100.0f; if(_tempFloat<60){ @@ -1831,7 +1831,7 @@ void MI32notifyHT_LY(int slot, char *_buf, int len){ _tempFloat=(float)LYWSD0x_HT.hum; if(_tempFloat<100){ MIBLEsensors[_slot].hum = _tempFloat; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("LYWSD0x: hum updated")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("M32: LYWSD0x: hum updated")); } MIBLEsensors[_slot].eventType.tempHum = 1; if (MIBLEsensors[_slot].type == MI_LYWSD03MMC || MIBLEsensors[_slot].type == MI_MHOC401){ @@ -1875,17 +1875,17 @@ void MI32Every50mSecond(){ void MI32EverySecond(bool restart){ -// AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("MI32: onesec")); +// AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("M32: onesec")); MI32TimeoutSensors(); MI32ShowSomeSensors(); - // read a battery if + // read a battery if // MI32.batteryreader.slot < filled and !MI32.batteryreader.active readOneBat(); - // read a sensor if + // read a sensor if // MI32.sensorreader.slot < filled and !MI32.sensorreader.active // for sensors which need to get data through notify... readOneSensor(); @@ -1893,7 +1893,7 @@ void MI32EverySecond(bool restart){ if (MI32.secondsCounter >= MI32.period){ // only if we finished the last read if (MI32.sensorreader.slot >= MIBLEsensors.size()){ - AddLog_P(LOG_LEVEL_DEBUG,PSTR("kick off readOneSensor")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Kick off readOneSensor")); // kick off notification sensor reading every period. MI32.sensorreader.slot = 0; MI32.secondsCounter = 0; @@ -1903,11 +1903,11 @@ void MI32EverySecond(bool restart){ if (MI32.secondsCounter2 >= MI32.period){ if (MI32.mqttCurrentSlot >= MIBLEsensors.size()){ - AddLog_P(LOG_LEVEL_DEBUG,PSTR("kick off tele sending")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Kick off tele sending")); MI32.mqttCurrentSlot = 0; MI32.secondsCounter2 = 0; } else { - AddLog_P(LOG_LEVEL_DEBUG,PSTR("hit tele time, restarted but not finished last - lost from slot %d")+MI32.mqttCurrentSlot); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Hit tele time, restarted but not finished last - lost from slot %d")+MI32.mqttCurrentSlot); MI32.mqttCurrentSlot = 0; MI32.secondsCounter2 = 0; } @@ -1968,15 +1968,15 @@ void CmndMi32Time(void) { if (MIBLEsensors.size() > slot) { int res = genericTimeWriteFn(slot); if (res > 0){ - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("MI32: will set Time")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: will set Time")); ResponseCmndNumber(slot); return; } if (res < 0) { - AddLog_P(LOG_LEVEL_ERROR, PSTR("MI32: cannot set Time on sensor type")); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: cannot set Time on sensor type")); } if (res == 0) { - AddLog_P(LOG_LEVEL_ERROR, PSTR("MI32: cannot set Time right now")); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: cannot set Time right now")); } } } @@ -2009,15 +2009,15 @@ void CmndMi32Unit(void) { // TOGGLE unit? int res = genericUnitWriteFn(slot, -1); if (res > 0){ - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("MI32: will toggle Unit")); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: will toggle Unit")); ResponseCmndNumber(slot); return; } if (res < 0) { - AddLog_P(LOG_LEVEL_ERROR, PSTR("MI32: cannot toggle Unit on sensor type")); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: cannot toggle Unit on sensor type")); } if (res == 0) { - AddLog_P(LOG_LEVEL_ERROR, PSTR("MI32: cannot toggle Unit right now")); + AddLog_P(LOG_LEVEL_ERROR, PSTR("M32: cannot toggle Unit right now")); } } } @@ -2058,7 +2058,7 @@ void CmndMi32Block(void){ } break; default: case 1: - break; + break; } MI32BlockListResp(); return; @@ -2076,7 +2076,7 @@ void CmndMi32Block(void){ case 0: { //TasAutoMutex localmutex(&slotmutex, "Mi32Block2"); MIBLEBlockList.erase( std::remove_if( begin( MIBLEBlockList ), end( MIBLEBlockList ), [_MACasBytes]( MAC_t& _entry )->bool - { return (memcmp(_entry.buf,_MACasBytes.buf,6) == 0); } + { return (memcmp(_entry.buf,_MACasBytes.buf,6) == 0); } ), end( MIBLEBlockList ) ); } break; case 1: { @@ -2091,8 +2091,8 @@ void CmndMi32Block(void){ MIBLEBlockList.push_back(_MACasBytes); MI32removeMIBLEsensor(_MACasBytes.buf); } - // AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: size of ilist: %u"), MIBLEBlockList.size()); - } break; + // AddLog_P(LOG_LEVEL_INFO,PSTR("M32: size of ilist: %u"), MIBLEBlockList.size()); + } break; } MI32BlockListResp(); } @@ -2126,7 +2126,7 @@ void MI32KeyListResp(){ ToHex_P(MIBLEbindKeys[i].MAC,6,tmp,20,0); char key[16*2+1]; ToHex_P(MIBLEbindKeys[i].key,16,key,33,0); - + ResponseAppend_P(PSTR("\"%s\":\"%s\""), tmp, key); } ResponseAppend_P(PSTR("}}")); @@ -2136,7 +2136,7 @@ void MI32KeyListResp(){ void CmndMi32Keys(void){ #ifdef BLE_ESP32_ALIASES int op = XdrvMailbox.index; - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("key %d %s"), op, XdrvMailbox.data); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Key %d %s"), op, XdrvMailbox.data); int res = -1; switch(op){ @@ -2175,7 +2175,7 @@ void CmndMi32Keys(void){ return; } - AddLog_P(LOG_LEVEL_ERROR,PSTR("Add key mac %s = key %s"), mac, key); + AddLog_P(LOG_LEVEL_ERROR,PSTR("M32: Add key mac %s = key %s"), mac, key); char tmp[20]; // convert mac back to string ToHex_P(addr,6,tmp,20,0); @@ -2186,7 +2186,7 @@ void CmndMi32Keys(void){ } while (p); if (added){ - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Added %d Keys"), added); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Added %d Keys"), added); MI32KeyListResp(); } else { MI32KeyListResp(); @@ -2194,7 +2194,7 @@ void CmndMi32Keys(void){ return; } break; case 2:{ // clear - if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32Keys clearing %d"), MIBLEbindKeys.size()); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Keys clearing %d"), MIBLEbindKeys.size()); for (int i = MIBLEbindKeys.size()-1; i >= 0; i--){ MIBLEbindKeys.pop_back(); } @@ -2232,7 +2232,7 @@ const char HTTP_MI32_HL[] PROGMEM = "{s}
{m}
{e}"; const char HTTP_NEEDKEY[] PROGMEM = "{s}%s %s{m} {e}"; - + const char HTTP_PAIRING[] PROGMEM = "{s}%s Pair Button Pressed{m} {e}"; @@ -2246,10 +2246,10 @@ const char HTTP_MI_KEY_STYLE[] PROGMEM = ""; #define D_MI32_KEY "MI32 Set Key" void HandleMI32Key(){ - AddLog_P(LOG_LEVEL_DEBUG, PSTR("HandleMI32Key hit")); - if (!HttpCheckPriviledgedAccess()) { - AddLog_P(LOG_LEVEL_DEBUG, PSTR("!HttpCheckPriviledgedAccess()")); - return; + AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: HandleMI32Key hit")); + if (!HttpCheckPriviledgedAccess()) { + AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: !HttpCheckPriviledgedAccess()")); + return; } WSContentStart_P(PSTR(D_MI32_KEY)); WSContentSendStyle_P(HTTP_MI_KEY_STYLE); @@ -2286,13 +2286,13 @@ void MI32TimeoutSensors(){ // so block for as long as it takes. // PROBLEM: when we take this, it hangs the BLE loop. - // BUT, devicePresent uses the + // BUT, devicePresent uses the // remove devices for which the adverts have timed out for (int i = MIBLEsensors.size()-1; i >= 0 ; i--) { //if (MIBLEsensors[i].MAC[2] || MIBLEsensors[i].MAC[3] || MIBLEsensors[i].MAC[4] || MIBLEsensors[i].MAC[5]){ if (!BLE_ESP32::devicePresent(MIBLEsensors[i].MAC)){ uint8_t *mac = MIBLEsensors[i].MAC; - AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: dev no longer present MAC: %02x%02x%02x%02x%02x%02x"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Dev no longer present MAC: %02x%02x%02x%02x%02x%02x"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); TasAutoMutex localmutex(&slotmutex, "Mi32Timeout"); MIBLEsensors.erase(MIBLEsensors.begin() + i); } @@ -2313,7 +2313,7 @@ void MI32GetOneSensorJson(int slot){ ResponseAppend_P(PSTR("\"MAC\":\"%02x%02x%02x%02x%02x%02x\""), p->MAC[0], p->MAC[1], p->MAC[2], p->MAC[3], p->MAC[4], p->MAC[5]); - + if((!MI32.mode.triggeredTele && !MI32.option.minimalSummary)||MI32.mode.triggeredTele){ bool tempHumSended = false; if(p->feature.tempHum){ @@ -2497,7 +2497,7 @@ void MI32ShowSomeSensors(){ } ResponseAppend_P(PSTR("}")); MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); - //AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: show some %d %s"),D_CMND_MI32, MI32.mqttCurrentSlot, TasmotaGlobal.mqtt_data); + //AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: %s: show some %d %s"),D_CMND_MI32, MI32.mqttCurrentSlot, TasmotaGlobal.mqtt_data); #ifdef USE_RULES RulesTeleperiod(); // Allow rule based HA messages @@ -2547,7 +2547,7 @@ void MI32ShowTriggeredSensors(){ if (cnt){ // if we got one, then publish ResponseAppend_P(PSTR("}")); MqttPublishPrefixTopic_P(STAT, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); - AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: triggered %d %s"),D_CMND_MI32, sensor, TasmotaGlobal.mqtt_data); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: %s: triggered %d %s"),D_CMND_MI32, sensor, TasmotaGlobal.mqtt_data); #ifdef USE_RULES RulesTeleperiod(); // Allow rule based HA messages @@ -2565,7 +2565,7 @@ void MI32Show(bool json) // don't detect half-added ones here int numsensors = MIBLEsensors.size(); - if (json) { + if (json) { // TELE JSON messages now do nothing here, apart from set MI32.mqttCurrentSlot // which will trigger send next second of up to 4 sensors, then the next four in the next second, etc. //MI32.mqttCurrentSlot = 0; @@ -2576,7 +2576,7 @@ void MI32Show(bool json) static uint16_t _counter = 0; int32_t i = _page * MI32.perPage; uint32_t j = i + MI32.perPage; - + if (j+1 > numsensors){ j = numsensors; } From e14f464a236654c52557d3b0be1e4fb3270462c0 Mon Sep 17 00:00:00 2001 From: polarduck-dev <77441368+polarduck-dev@users.noreply.github.com> Date: Tue, 19 Jan 2021 16:33:06 +0000 Subject: [PATCH 040/186] Removed IPV4 from the existing macros so the change is externally minimal --- platformio.ini | 2 +- tasmota/my_user_config.h | 8 ++++---- tasmota/settings.ino | 8 ++++---- tasmota/user_config_override_sample.h | 12 ++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/platformio.ini b/platformio.ini index 6de5f5f55..12ccfcc90 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,7 +133,7 @@ build_flags = ${esp_defaults.build_flags} -DBEARSSL_SSL_BASIC ; NONOSDK22x_190703 = 2.2.2-dev(38a443e) -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 - -DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_HIGHER_BANDWIDTH + -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH ; VTABLES in Flash -DVTABLES_IN_FLASH ; remove the 4-bytes alignment for PSTR() diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index b66d04a31..8cf0316f8 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -65,10 +65,10 @@ #define BOOT_LOOP_OFFSET 1 // [SetOption36] Number of boot loops before starting restoring defaults (0 = disable, 1..200 = boot loops offset) // -- Wifi ---------------------------------------- -#define WIFI_IPV4_ADDRESS "0.0.0.0" // [IpAddress1] Set to 0.0.0.0 for using DHCP or enter a static IP address -#define WIFI_IPV4_GATEWAY "192.168.1.1" // [IpAddress2] If not using DHCP set Gateway IP address -#define WIFI_IPV4_SUBNETMASK "255.255.255.0" // [IpAddress3] If not using DHCP set Network mask -#define WIFI_IPV4_DNS "192.168.1.1" // [IpAddress4] If not using DHCP set DNS IP address (might be equal to WIFI_GATEWAY) +#define WIFI_IP_ADDRESS "0.0.0.0" // [IpAddress1] Set to 0.0.0.0 for using DHCP or enter a static IP address +#define WIFI_GATEWAY "192.168.1.1" // [IpAddress2] If not using DHCP set Gateway IP address +#define WIFI_SUBNETMASK "255.255.255.0" // [IpAddress3] If not using DHCP set Network mask +#define WIFI_DNS "192.168.1.1" // [IpAddress4] If not using DHCP set DNS IP address (might be equal to WIFI_GATEWAY) #define STA_SSID1 "" // [Ssid1] Wifi SSID #define STA_PASS1 "" // [Password1] Wifi password diff --git a/tasmota/settings.ino b/tasmota/settings.ino index 5421e7c8a..60c303452 100644 --- a/tasmota/settings.ino +++ b/tasmota/settings.ino @@ -812,10 +812,10 @@ void SettingsDefaultSet2(void) flag3.use_wifi_rescan |= WIFI_SCAN_REGULARLY; Settings.wifi_output_power = 170; Settings.param[P_ARP_GRATUITOUS] = WIFI_ARP_INTERVAL; - ParseIPv4(&Settings.ipv4_address[0], WIFI_IPV4_ADDRESS); - ParseIPv4(&Settings.ipv4_address[1], WIFI_IPV4_GATEWAY); - ParseIPv4(&Settings.ipv4_address[2], WIFI_IPV4_SUBNETMASK); - ParseIPv4(&Settings.ipv4_address[3], WIFI_IPV4_DNS); + ParseIPv4(&Settings.ipv4_address[0], WIFI_IP_ADDRESS); + ParseIPv4(&Settings.ipv4_address[1], WIFI_GATEWAY); + ParseIPv4(&Settings.ipv4_address[2], WIFI_SUBNETMASK); + ParseIPv4(&Settings.ipv4_address[3], WIFI_DNS); Settings.sta_config = WIFI_CONFIG_TOOL; // Settings.sta_active = 0; SettingsUpdateText(SET_STASSID1, PSTR(STA_SSID1)); diff --git a/tasmota/user_config_override_sample.h b/tasmota/user_config_override_sample.h index df151f9b2..cf5f27a25 100644 --- a/tasmota/user_config_override_sample.h +++ b/tasmota/user_config_override_sample.h @@ -68,18 +68,18 @@ Examples : // Ie: export PLATFORMIO_BUILD_FLAGS='-DUSE_CONFIG_OVERRIDE -DMY_IP="192.168.1.99" -DMY_GW="192.168.1.1" -DMY_DNS="192.168.1.1"' #ifdef MY_IP -#undef WIFI_IPV4_ADDRESS -#define WIFI_IPV4_ADDRESS MY_IP // Set to 0.0.0.0 for using DHCP or enter a static IP address +#undef WIFI_IP_ADDRESS +#define WIFI_IP_ADDRESS MY_IP // Set to 0.0.0.0 for using DHCP or enter a static IP address #endif #ifdef MY_GW -#undef WIFI_IPV4_GATEWAY -#define WIFI_IPV4_GATEWAY MY_GW // if not using DHCP set Gateway IP address +#undef WIFI_GATEWAY +#define WIFI_GATEWAY MY_GW // if not using DHCP set Gateway IP address #endif #ifdef MY_DNS -#undef WIFI_IPV4_DNS -#define WIFI_IPV4_DNS MY_DNS // If not using DHCP set DNS IP address (might be equal to WIFI_GATEWAY) +#undef WIFI_DNS +#define WIFI_DNS MY_DNS // If not using DHCP set DNS IP address (might be equal to WIFI_GATEWAY) #endif */ From a13c85c45eafcaaf7602d98af06efd7dbb557bf8 Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Wed, 20 Jan 2021 08:42:44 +0100 Subject: [PATCH 041/186] refactor rtsp and webcam --- lib/libesp32/rtsp/CRtspSession.cpp | 30 +-- lib/libesp32/rtsp/CRtspSession.h | 7 + lib/libesp32/rtsp/CStreamer.cpp | 2 +- lib/libesp32/rtsp/CStreamer.h | 1 + lib/libesp32/rtsp/LICENSE | 7 + lib/libesp32/rtsp/README.md | 93 ++++++++ lib/libesp32/rtsp/library.properties | 9 + tasmota/xdrv_81_webcam.ino | 304 +++++++++++++++------------ 8 files changed, 302 insertions(+), 151 deletions(-) create mode 100755 lib/libesp32/rtsp/LICENSE create mode 100755 lib/libesp32/rtsp/README.md create mode 100755 lib/libesp32/rtsp/library.properties diff --git a/lib/libesp32/rtsp/CRtspSession.cpp b/lib/libesp32/rtsp/CRtspSession.cpp index 9d5718091..a14610038 100755 --- a/lib/libesp32/rtsp/CRtspSession.cpp +++ b/lib/libesp32/rtsp/CRtspSession.cpp @@ -34,8 +34,8 @@ void CRtspSession::Init() bool CRtspSession::ParseRtspRequest(char const * aRequest, unsigned aRequestSize) { - char CmdName[RTSP_PARAM_STRING_MAX]; - static char CurRequest[RTSP_BUFFER_SIZE]; // Note: we assume single threaded, this large buf we keep off of the tiny stack + // char CmdName[RTSP_PARAM_STRING_MAX]; + //char CurRequest[RTSP_BUFFER_SIZE]; // Note: we assume single threaded, this large buf we keep off of the tiny stack unsigned CurRequestSize; Init(); @@ -45,7 +45,7 @@ bool CRtspSession::ParseRtspRequest(char const * aRequest, unsigned aRequestSize // check whether the request contains information about the RTP/RTCP UDP client ports (SETUP command) char * ClientPortPtr; char * TmpPtr; - static char CP[1024]; + char CP[128]; //static char CP[1024]; char * pCP; ClientPortPtr = strstr(CurRequest,"client_port"); @@ -230,7 +230,7 @@ RTSP_CMD_TYPES CRtspSession::Handle_RtspRequest(char const * aRequest, unsigned void CRtspSession::Handle_RtspOPTION() { - static char Response[1024]; // Note: we assume single threaded, this large buf we keep off of the tiny stack + //static char Response[1024]; // Note: we assume single threaded, this large buf we keep off of the tiny stack snprintf(Response,sizeof(Response), "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" @@ -241,9 +241,9 @@ void CRtspSession::Handle_RtspOPTION() void CRtspSession::Handle_RtspDESCRIBE() { - static char Response[1024]; // Note: we assume single threaded, this large buf we keep off of the tiny stack - static char SDPBuf[1024]; - static char URLBuf[1024]; + //static char Response[1024]; // Note: we assume single threaded, this large buf we keep off of the tiny stack + char SDPBuf[128]; //static char SDPBuf[1024]; + char URLBuf[128]; //static char URLBuf[1024]; // check whether we know a stream with the URL which is requested m_StreamID = -1; // invalid URL @@ -261,7 +261,7 @@ void CRtspSession::Handle_RtspDESCRIBE() }; // simulate DESCRIBE server response - static char OBuf[256]; + // static char OBuf[256]; char * ColonPtr; strcpy(OBuf,m_URLHostPort); ColonPtr = strstr(OBuf,":"); @@ -305,8 +305,8 @@ void CRtspSession::Handle_RtspDESCRIBE() void CRtspSession::Handle_RtspSETUP() { - static char Response[1024]; - static char Transport[255]; + //static char Response[1024]; + //static char Transport[255]; // init RTP streamer transport type (UDP or TCP) and ports for UDP transport m_Streamer->InitTransport(m_ClientRTPPort,m_ClientRTCPPort,m_TcpTransport); @@ -336,7 +336,7 @@ void CRtspSession::Handle_RtspSETUP() void CRtspSession::Handle_RtspPLAY() { - static char Response[1024]; + //static char Response[1024]; // simulate SETUP server response snprintf(Response,sizeof(Response), @@ -354,10 +354,10 @@ void CRtspSession::Handle_RtspPLAY() char const * CRtspSession::DateHeader() { - static char buf[200]; + //static char buf[200]; time_t tt = time(NULL); - strftime(buf, sizeof buf, "Date: %a, %b %d %Y %H:%M:%S GMT", gmtime(&tt)); - return buf; + strftime(session_buf, sizeof(session_buf), "Date: %a, %b %d %Y %H:%M:%S GMT", gmtime(&tt)); + return session_buf; } int CRtspSession::GetStreamID() @@ -375,7 +375,7 @@ bool CRtspSession::handleRequests(uint32_t readTimeoutMs) if(m_stopped) return false; // Already closed down - static char RecvBuf[RTSP_BUFFER_SIZE]; // Note: we assume single threaded, this large buf we keep off of the tiny stack + //char RecvBuf[RTSP_BUFFER_SIZE]; // Note: we assume single threaded, this large buf we keep off of the tiny stack memset(RecvBuf,0x00,sizeof(RecvBuf)); int res = socketread(m_RtspClient,RecvBuf,sizeof(RecvBuf), readTimeoutMs); diff --git a/lib/libesp32/rtsp/CRtspSession.h b/lib/libesp32/rtsp/CRtspSession.h index 298bd6a15..79700bd60 100755 --- a/lib/libesp32/rtsp/CRtspSession.h +++ b/lib/libesp32/rtsp/CRtspSession.h @@ -70,4 +70,11 @@ private: char m_CSeq[RTSP_PARAM_STRING_MAX]; // RTSP command sequence number char m_URLHostPort[MAX_HOSTNAME_LEN]; // host:port part of the URL unsigned m_ContentLength; // SDP string size + char CurRequest[RTSP_BUFFER_SIZE]; + char RecvBuf[RTSP_BUFFER_SIZE]; + char session_buf[128]; + char CmdName[RTSP_PARAM_STRING_MAX]; + char Transport[255]; + char Response[1024]; + char OBuf[256]; }; diff --git a/lib/libesp32/rtsp/CStreamer.cpp b/lib/libesp32/rtsp/CStreamer.cpp index 26322c0fa..af4519ec7 100755 --- a/lib/libesp32/rtsp/CStreamer.cpp +++ b/lib/libesp32/rtsp/CStreamer.cpp @@ -48,7 +48,7 @@ int CStreamer::SendRtpPacket(unsigned const char * jpeg, int jpegLen, int fragme bool includeQuantTbl = quant0tbl && quant1tbl && fragmentOffset == 0; uint8_t q = includeQuantTbl ? 128 : 0x5e; - static char RtpBuf[2048]; // Note: we assume single threaded, this large buf we keep off of the tiny stack + //static char RtpBuf[2048]; // Note: we assume single threaded, this large buf we keep off of the tiny stack int RtpPacketSize = fragmentLen + KRtpHeaderSize + KJpegHeaderSize + (includeQuantTbl ? (4 + 64 * 2) : 0); memset(RtpBuf,0x00,sizeof(RtpBuf)); diff --git a/lib/libesp32/rtsp/CStreamer.h b/lib/libesp32/rtsp/CStreamer.h index 78d112b5b..58a9983e2 100755 --- a/lib/libesp32/rtsp/CStreamer.h +++ b/lib/libesp32/rtsp/CStreamer.h @@ -39,6 +39,7 @@ private: u_short m_width; // image data info u_short m_height; + char RtpBuf[2048]; }; diff --git a/lib/libesp32/rtsp/LICENSE b/lib/libesp32/rtsp/LICENSE new file mode 100755 index 000000000..136bbd239 --- /dev/null +++ b/lib/libesp32/rtsp/LICENSE @@ -0,0 +1,7 @@ +Copyright 2018 S. Kevin Hester-Chow, kevinh@geeksville.com (MIT License) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/libesp32/rtsp/README.md b/lib/libesp32/rtsp/README.md new file mode 100755 index 000000000..37d6aec65 --- /dev/null +++ b/lib/libesp32/rtsp/README.md @@ -0,0 +1,93 @@ +# Micro-RTSP + +This is a small library which can be used to serve up RTSP streams from +resource constrained MCUs. It lets you trivially make a $10 open source +RTSP video stream camera. + +# Usage + +This library works for ESP32/arduino targets but also for most any posixish platform. + +## Example arduino/ESP32 usage + +This library will work standalone, but it is _super_ easy to use if your app is platform.io based. +Just "pio lib install Micro-RTSP" to pull the latest version from their library server. If you want to use the OV2640 +camera support you'll need to be targeting the espressif32 platform in your project. + +See the [example platform.io app](/examples). It should build and run on virtually any of the $10 +ESP32-CAM boards (such as M5CAM). The relevant bit of the code is included below. In short: +1. Listen for a TCP connection on the RTSP port with accept() +2. When a connection comes in, create a CRtspSession and OV2640Streamer camera streamer objects. +3. While the connection remains, call session->handleRequests(0) to handle any incoming client requests. +4. Every 100ms or so call session->broadcastCurrentFrame() to send new frames to any clients. + +``` +void loop() +{ + uint32_t msecPerFrame = 100; + static uint32_t lastimage = millis(); + + // If we have an active client connection, just service that until gone + // (FIXME - support multiple simultaneous clients) + if(session) { + session->handleRequests(0); // we don't use a timeout here, + // instead we send only if we have new enough frames + + uint32_t now = millis(); + if(now > lastimage + msecPerFrame || now < lastimage) { // handle clock rollover + session->broadcastCurrentFrame(now); + lastimage = now; + + // check if we are overrunning our max frame rate + now = millis(); + if(now > lastimage + msecPerFrame) + printf("warning exceeding max frame rate of %d ms\n", now - lastimage); + } + + if(session->m_stopped) { + delete session; + delete streamer; + session = NULL; + streamer = NULL; + } + } + else { + client = rtspServer.accept(); + + if(client) { + //streamer = new SimStreamer(&client, true); // our streamer for UDP/TCP based RTP transport + streamer = new OV2640Streamer(&client, cam); // our streamer for UDP/TCP based RTP transport + + session = new CRtspSession(&client, streamer); // our threads RTSP session and state + } + } +} +``` +## Example posix/linux usage + +There is a small standalone example [here](/test/RTSPTestServer.cpp). You can build it by following [these](/test/README.md) directions. The usage of the two key classes (CRtspSession and SimStreamer) are very similar to to the ESP32 usage. + +## Supporting new camera devices + +Supporting new camera devices is quite simple. See OV2640Streamer for an example and implement streamImage() +by reading a frame from your camera. + +# Structure and design notes + +# Issues and sending pull requests + +Please report issues and send pull requests. I'll happily reply. ;-) + +# Credits + +The server code was initially based on a great 2013 [tutorial](https://www.medialan.de/usecase0001.html) by Medialan. + +# License + +Copyright 2018 S. Kevin Hester-Chow, kevinh@geeksville.com (MIT License) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/libesp32/rtsp/library.properties b/lib/libesp32/rtsp/library.properties new file mode 100755 index 000000000..03b5dc224 --- /dev/null +++ b/lib/libesp32/rtsp/library.properties @@ -0,0 +1,9 @@ +name=Micro-RTSP +version=0.1.6 +author=Kevin Hester +maintainer=Kevin Hester +sentence=Mikro RTSP server for mikros +paragraph=A small/efficient RTSP server for ESP32 and other micros +category=Data Storage +url=https://github.com/geeksville/Micro-RTSP.git +architectures=* diff --git a/tasmota/xdrv_81_webcam.ino b/tasmota/xdrv_81_webcam.ino index 179655dd4..f3d44710e 100755 --- a/tasmota/xdrv_81_webcam.ino +++ b/tasmota/xdrv_81_webcam.ino @@ -44,6 +44,8 @@ * WcSaturation = Set picture Saturation -2 ... +2 * WcBrightness = Set picture Brightness -2 ... +2 * WcContrast = Set picture Contrast -2 ... +2 + * WcInit = Init Camera Interface + * WcRtsp = Control RTSP Server, 0=disable, 1=enable (forces restart) (if defined ENABLE_RTSPSERVER) * * Only boards with PSRAM should be used. To enable PSRAM board should be se set to esp32cam in common32 of platform_override.ini * board = esp32cam @@ -54,6 +56,7 @@ * not tolerate any capictive load * flash led = gpio 4 * red led = gpio 33 + * optional rtsp url: rtsp://xxx.xxx.xxx.xxx:8554/mjpeg/1 */ /*********************************************************************************************/ @@ -69,11 +72,8 @@ bool HttpCheckPriviledgedAccess(bool); extern ESP8266WebServer *Webserver; -ESP8266WebServer *CamServer; #define BOUNDARY "e8b8c539-047d-4777-a985-fbba6edff11e" -WiFiClient client; - // CAMERA_MODEL_AI_THINKER default template pins #define PWDN_GPIO_NUM 32 @@ -94,29 +94,50 @@ WiFiClient client; #define HREF_GPIO_NUM 23 #define PCLK_GPIO_NUM 22 -struct { - uint8_t up; - uint16_t width; - uint16_t height; - uint8_t stream_active; -#ifdef USE_FACE_DETECT - uint8_t faces; - uint16_t face_detect_time; +#ifndef MAX_PICSTORE +#define MAX_PICSTORE 4 #endif -} Wc; +struct PICSTORE { + uint8_t *buff; + uint32_t len; +}; #ifdef ENABLE_RTSPSERVER #include #include #include #include -WiFiServer rtspServer(8554); -CStreamer *rtsp_streamer; -CRtspSession *rtsp_session; -WiFiClient rtsp_client; -uint8_t rtsp_start; -OV2640 cam; -#endif +#ifndef RTSP_FRAME_TIME +#define RTSP_FRAME_TIME 100 +#endif // RTSP_FRAME_TIME +#endif // ENABLE_RTSPSERVER + +struct { + uint8_t up; + uint16_t width; + uint16_t height; + uint8_t stream_active; + WiFiClient client; + ESP8266WebServer *CamServer; + struct PICSTORE picstore[MAX_PICSTORE]; +#ifdef USE_FACE_DETECT + uint8_t faces; + uint16_t face_detect_time; + uint32_t face_ltime; + mtmn_config_t mtmn_config = {0}; +#endif // USE_FACE_DETECT +#ifdef ENABLE_RTSPSERVER + WiFiServer *rtspp; + CStreamer *rtsp_streamer; + CRtspSession *rtsp_session; + WiFiClient rtsp_client; + uint8_t rtsp_start; + OV2640 cam; + uint32_t rtsp_lastframe_time; +#endif // ENABLE_RTSPSERVER +} Wc; + + /*********************************************************************************************/ @@ -336,18 +357,21 @@ uint32_t WcGetHeight(void) { /*********************************************************************************************/ +struct WC_Motion { uint16_t motion_detect; uint32_t motion_ltime; uint32_t motion_trigger; uint32_t motion_brightness; uint8_t *last_motion_buffer; +} wc_motion; + uint32_t WcSetMotionDetect(int32_t value) { - if (value >= 0) { motion_detect = value; } + if (value >= 0) { wc_motion.motion_detect = value; } if (-1 == value) { - return motion_trigger; + return wc_motion.motion_trigger; } else { - return motion_brightness; + return wc_motion.motion_brightness; } } @@ -356,22 +380,22 @@ void WcDetectMotion(void) { camera_fb_t *wc_fb; uint8_t *out_buf = 0; - if ((millis()-motion_ltime) > motion_detect) { - motion_ltime = millis(); + if ((millis()-wc_motion.motion_ltime) > wc_motion.motion_detect) { + wc_motion.motion_ltime = millis(); wc_fb = esp_camera_fb_get(); if (!wc_fb) { return; } - if (!last_motion_buffer) { - last_motion_buffer=(uint8_t *)heap_caps_malloc((wc_fb->width*wc_fb->height)+4, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!wc_motion.last_motion_buffer) { + wc_motion.last_motion_buffer = (uint8_t *)heap_caps_malloc((wc_fb->width*wc_fb->height) + 4, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); } - if (last_motion_buffer) { + if (wc_motion.last_motion_buffer) { if (PIXFORMAT_JPEG == wc_fb->format) { out_buf = (uint8_t *)heap_caps_malloc((wc_fb->width*wc_fb->height*3)+4, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (out_buf) { fmt2rgb888(wc_fb->buf, wc_fb->len, wc_fb->format, out_buf); uint32_t x, y; uint8_t *pxi = out_buf; - uint8_t *pxr = last_motion_buffer; + uint8_t *pxr = wc_motion.last_motion_buffer; // convert to bw uint64_t accu = 0; uint64_t bright = 0; @@ -386,8 +410,8 @@ void WcDetectMotion(void) { bright += gray; } } - motion_trigger = accu / ((wc_fb->height * wc_fb->width) / 100); - motion_brightness = bright / ((wc_fb->height * wc_fb->width) / 100); + wc_motion.motion_trigger = accu / ((wc_fb->height * wc_fb->width) / 100); + wc_motion.motion_brightness = bright / ((wc_fb->height * wc_fb->width) / 100); free(out_buf); } } @@ -400,22 +424,20 @@ void WcDetectMotion(void) { #ifdef USE_FACE_DETECT -static mtmn_config_t mtmn_config = {0}; - void fd_init(void) { - mtmn_config.type = FAST; - mtmn_config.min_face = 80; - mtmn_config.pyramid = 0.707; - mtmn_config.pyramid_times = 4; - mtmn_config.p_threshold.score = 0.6; - mtmn_config.p_threshold.nms = 0.7; - mtmn_config.p_threshold.candidate_number = 20; - mtmn_config.r_threshold.score = 0.7; - mtmn_config.r_threshold.nms = 0.7; - mtmn_config.r_threshold.candidate_number = 10; - mtmn_config.o_threshold.score = 0.7; - mtmn_config.o_threshold.nms = 0.7; - mtmn_config.o_threshold.candidate_number = 1; + Wc.mtmn_config.type = FAST; + Wc.mtmn_config.min_face = 80; + Wc.mtmn_config.pyramid = 0.707; + Wc.mtmn_config.pyramid_times = 4; + Wc.mtmn_config.p_threshold.score = 0.6; + Wc.mtmn_config.p_threshold.nms = 0.7; + Wc.mtmn_config.p_threshold.candidate_number = 20; + Wc.mtmn_config.r_threshold.score = 0.7; + Wc.mtmn_config.r_threshold.nms = 0.7; + Wc.mtmn_config.r_threshold.candidate_number = 10; + Wc.mtmn_config.o_threshold.score = 0.7; + Wc.mtmn_config.o_threshold.nms = 0.7; + Wc.mtmn_config.o_threshold.candidate_number = 1; } #define FACE_COLOR_WHITE 0x00FFFFFF @@ -473,8 +495,6 @@ uint32_t WcSetFaceDetect(int32_t value) { return Wc.faces; } -uint32_t face_ltime; - uint32_t WcDetectFace(void); uint32_t WcDetectFace(void) { @@ -486,8 +506,8 @@ uint32_t WcDetectFace(void) { int face_id = 0; camera_fb_t *fb; - if ((millis() - face_ltime) > Wc.face_detect_time) { - face_ltime = millis(); + if ((millis() - Wc.face_ltime) > Wc.face_detect_time) { + Wc.face_ltime = millis(); fb = esp_camera_fb_get(); if (!fb) { return ESP_FAIL; } @@ -511,7 +531,7 @@ uint32_t WcDetectFace(void) { return ESP_FAIL; } - box_array_t *net_boxes = face_detect(image_matrix, &mtmn_config); + box_array_t *net_boxes = face_detect(image_matrix, &Wc.mtmn_config); if (net_boxes){ detected = true; Wc.faces = net_boxes->len; @@ -536,15 +556,6 @@ uint32_t WcDetectFace(void) { /*********************************************************************************************/ -#ifndef MAX_PICSTORE -#define MAX_PICSTORE 4 -#endif -struct PICSTORE { - uint8_t *buff; - uint32_t len; -}; - -struct PICSTORE picstore[MAX_PICSTORE]; #ifdef COPYFRAME struct PICSTORE tmp_picstore; @@ -552,8 +563,8 @@ struct PICSTORE tmp_picstore; uint32_t WcGetPicstore(int32_t num, uint8_t **buff) { if (num<0) { return MAX_PICSTORE; } - *buff = picstore[num].buff; - return picstore[num].len; + *buff = Wc.picstore[num].buff; + return Wc.picstore[num].len; } uint32_t WcGetFrame(int32_t bnum) { @@ -566,8 +577,8 @@ uint32_t WcGetFrame(int32_t bnum) { if (bnum < -MAX_PICSTORE) { bnum=-1; } bnum = -bnum; bnum--; - if (picstore[bnum].buff) { free(picstore[bnum].buff); } - picstore[bnum].len = 0; + if (Wc.picstore[bnum].buff) { free(Wc.picstore[bnum].buff); } + Wc.picstore[bnum].len = 0; return 0; } @@ -608,18 +619,18 @@ uint32_t WcGetFrame(int32_t bnum) { pcopy: if ((bnum < 1) || (bnum > MAX_PICSTORE)) { bnum = 1; } bnum--; - if (picstore[bnum].buff) { free(picstore[bnum].buff); } - picstore[bnum].buff = (uint8_t *)heap_caps_malloc(_jpg_buf_len+4, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); - if (picstore[bnum].buff) { - memcpy(picstore[bnum].buff, _jpg_buf, _jpg_buf_len); - picstore[bnum].len = _jpg_buf_len; + if (Wc.picstore[bnum].buff) { free(Wc.picstore[bnum].buff); } + Wc.picstore[bnum].buff = (uint8_t *)heap_caps_malloc(_jpg_buf_len+4, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (Wc.picstore[bnum].buff) { + memcpy(Wc.picstore[bnum].buff, _jpg_buf, _jpg_buf_len); + Wc.picstore[bnum].len = _jpg_buf_len; } else { AddLog_P(LOG_LEVEL_DEBUG, PSTR("CAM: Can't allocate picstore")); - picstore[bnum].len = 0; + Wc.picstore[bnum].len = 0; } if (wc_fb) { esp_camera_fb_return(wc_fb); } if (jpeg_converted) { free(_jpg_buf); } - if (!picstore[bnum].buff) { return 0; } + if (!Wc.picstore[bnum].buff) { return 0; } return _jpg_buf_len; } @@ -657,11 +668,11 @@ void HandleImage(void) { if (wc_fb) { esp_camera_fb_return(wc_fb); } } else { bnum--; - if (!picstore[bnum].len) { + if (!Wc.picstore[bnum].len) { AddLog_P(LOG_LEVEL_DEBUG, PSTR("CAM: No image #: %d"), bnum); return; } - client.write((char *)picstore[bnum].buff, picstore[bnum].len); + client.write((char *)Wc.picstore[bnum].buff, Wc.picstore[bnum].len); } client.stop(); @@ -674,7 +685,7 @@ void HandleImageBasic(void) { AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_HTTP "Capture image")); if (Settings.webcam_config.stream) { - if (!CamServer) { + if (!Wc.CamServer) { WcStreamControl(); } } @@ -717,7 +728,7 @@ void HandleWebcamMjpeg(void) { // if (!Wc.stream_active) { // always restart stream Wc.stream_active = 1; - client = CamServer->client(); + Wc.client = Wc.CamServer->client(); AddLog_P(LOG_LEVEL_DEBUG, PSTR("CAM: Create client")); // } } @@ -731,15 +742,15 @@ void HandleWebcamMjpegTask(void) { uint32_t tlen; bool jpeg_converted = false; - if (!client.connected()) { + if (!Wc.client.connected()) { AddLog_P(LOG_LEVEL_DEBUG, PSTR("CAM: Client fail")); Wc.stream_active = 0; } if (1 == Wc.stream_active) { - client.flush(); - client.setTimeout(3); + Wc.client.flush(); + Wc.client.setTimeout(3); AddLog_P(LOG_LEVEL_DEBUG, PSTR("CAM: Start stream")); - client.print("HTTP/1.1 200 OK\r\n" + Wc.client.print("HTTP/1.1 200 OK\r\n" "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" "\r\n"); Wc.stream_active = 2; @@ -764,17 +775,17 @@ void HandleWebcamMjpegTask(void) { _jpg_buf = wc_fb->buf; } - client.printf("Content-Type: image/jpeg\r\n" + Wc.client.printf("Content-Type: image/jpeg\r\n" "Content-Length: %d\r\n" "\r\n", static_cast(_jpg_buf_len)); - tlen = client.write(_jpg_buf, _jpg_buf_len); + tlen = Wc.client.write(_jpg_buf, _jpg_buf_len); /* if (tlen!=_jpg_buf_len) { esp_camera_fb_return(wc_fb); Wc.stream_active=0; AddLog_P(LOG_LEVEL_DEBUG, PSTR("CAM: Send fail")); }*/ - client.print("\r\n--" BOUNDARY "\r\n"); + Wc.client.print("\r\n--" BOUNDARY "\r\n"); #ifdef COPYFRAME if (tmp_picstore.buff) { free(tmp_picstore.buff); } @@ -793,15 +804,15 @@ void HandleWebcamMjpegTask(void) { } if (0 == Wc.stream_active) { AddLog_P(LOG_LEVEL_DEBUG, PSTR("CAM: Stream exit")); - client.flush(); - client.stop(); + Wc.client.flush(); + Wc.client.stop(); } } void HandleWebcamRoot(void) { //CamServer->redirect("http://" + String(ip) + ":81/cam.mjpeg"); - CamServer->sendHeader("Location", WiFi.localIP().toString() + ":81/cam.mjpeg"); - CamServer->send(302, "", ""); + Wc.CamServer->sendHeader("Location", WiFi.localIP().toString() + ":81/cam.mjpeg"); + Wc.CamServer->send(302, "", ""); AddLog_P(LOG_LEVEL_DEBUG, PSTR("CAM: Root called")); } @@ -813,20 +824,20 @@ uint32_t WcSetStreamserver(uint32_t flag) { Wc.stream_active = 0; if (flag) { - if (!CamServer) { - CamServer = new ESP8266WebServer(81); - CamServer->on("/", HandleWebcamRoot); - CamServer->on("/cam.mjpeg", HandleWebcamMjpeg); - CamServer->on("/cam.jpg", HandleWebcamMjpeg); - CamServer->on("/stream", HandleWebcamMjpeg); + if (!Wc.CamServer) { + Wc.CamServer = new ESP8266WebServer(81); + Wc.CamServer->on("/", HandleWebcamRoot); + Wc.CamServer->on("/cam.mjpeg", HandleWebcamMjpeg); + Wc.CamServer->on("/cam.jpg", HandleWebcamMjpeg); + Wc.CamServer->on("/stream", HandleWebcamMjpeg); AddLog_P(LOG_LEVEL_DEBUG, PSTR("CAM: Stream init")); - CamServer->begin(); + Wc.CamServer->begin(); } } else { - if (CamServer) { - CamServer->stop(); - delete CamServer; - CamServer = NULL; + if (Wc.CamServer) { + Wc.CamServer->stop(); + delete Wc.CamServer; + Wc.CamServer = NULL; AddLog_P(LOG_LEVEL_DEBUG, PSTR("CAM: Stream exit")); } } @@ -839,61 +850,58 @@ void WcStreamControl() { } /*********************************************************************************************/ -#ifdef ENABLE_RTSPSERVER -static uint32_t rtsp_lastframe_time; -#ifndef RTSP_FRAME_TIME -#define RTSP_FRAME_TIME 100 -#endif -#endif + void WcLoop(void) { - if (CamServer) { - CamServer->handleClient(); + if (Wc.CamServer) { + Wc.CamServer->handleClient(); if (Wc.stream_active) { HandleWebcamMjpegTask(); } } - if (motion_detect) { WcDetectMotion(); } + if (wc_motion.motion_detect) { WcDetectMotion(); } #ifdef USE_FACE_DETECT if (Wc.face_detect_time) { WcDetectFace(); } #endif #ifdef ENABLE_RTSPSERVER + if (Settings.webcam_config.rtsp && !TasmotaGlobal.global_state.wifi_down && Wc.up) { + if (!Wc.rtsp_start) { + Wc.rtspp = new WiFiServer(8554); + Wc.rtspp->begin(); + Wc.rtsp_start = 1; + AddLog_P(LOG_LEVEL_INFO, PSTR("CAM: RTSP init")); + Wc.rtsp_lastframe_time = millis(); + } - if (!rtsp_start && !TasmotaGlobal.global_state.wifi_down && Wc.up) { - rtspServer.begin(); - rtsp_start = 1; - AddLog_P(LOG_LEVEL_INFO, PSTR("CAM: RTSP init")); - rtsp_lastframe_time = millis(); - } - - // If we have an active client connection, just service that until gone - if (rtsp_session) { - rtsp_session->handleRequests(0); // we don't use a timeout here, + // If we have an active client connection, just service that until gone + if (Wc.rtsp_session) { + Wc.rtsp_session->handleRequests(0); // we don't use a timeout here, // instead we send only if we have new enough frames uint32_t now = millis(); - if ((now-rtsp_lastframe_time) > RTSP_FRAME_TIME) { - rtsp_session->broadcastCurrentFrame(now); - rtsp_lastframe_time = now; + if ((now-Wc.rtsp_lastframe_time) > RTSP_FRAME_TIME) { + Wc.rtsp_session->broadcastCurrentFrame(now); + Wc.rtsp_lastframe_time = now; // AddLog_P(LOG_LEVEL_INFO, PSTR("CAM: RTSP session frame")); } - if (rtsp_session->m_stopped) { - delete rtsp_session; - delete rtsp_streamer; - rtsp_session = NULL; - rtsp_streamer = NULL; + if (Wc.rtsp_session->m_stopped) { + delete Wc.rtsp_session; + delete Wc.rtsp_streamer; + Wc.rtsp_session = NULL; + Wc.rtsp_streamer = NULL; AddLog_P(LOG_LEVEL_INFO, PSTR("CAM: RTSP stopped")); } - } - else { - rtsp_client = rtspServer.accept(); - if (rtsp_client) { - rtsp_streamer = new OV2640Streamer(&rtsp_client, cam); // our streamer for UDP/TCP based RTP transport - rtsp_session = new CRtspSession(&rtsp_client, rtsp_streamer); // our threads RTSP session and state + } + else { + Wc.rtsp_client = Wc.rtspp->accept(); + if (Wc.rtsp_client) { + Wc.rtsp_streamer = new OV2640Streamer(&Wc.rtsp_client, Wc.cam); // our streamer for UDP/TCP based RTP transport + Wc.rtsp_session = new CRtspSession(&Wc.rtsp_client, Wc.rtsp_streamer); // our threads RTSP session and state AddLog_P(LOG_LEVEL_INFO, PSTR("CAM: RTSP stream created")); } + } } -#endif +#endif // ENABLE_RTSPSERVER } void WcPicSetup(void) { @@ -904,12 +912,12 @@ void WcPicSetup(void) { void WcShowStream(void) { if (Settings.webcam_config.stream) { -// if (!CamServer || !Wc.up) { - if (!CamServer) { +// if (!Wc.CamServer || !Wc.up) { + if (!Wc.CamServer) { WcStreamControl(); delay(50); // Give the webcam webserver some time to prepare the stream } - if (CamServer && Wc.up) { + if (Wc.CamServer && Wc.up) { WSContentSend_P(PSTR("

Webcam stream

"), WiFi.localIP().toString().c_str()); } @@ -941,24 +949,39 @@ void WcInit(void) { #define D_CMND_WC_BRIGHTNESS "Brightness" #define D_CMND_WC_CONTRAST "Contrast" #define D_CMND_WC_INIT "Init" +#define D_CMND_RTSP "Rtsp" const char kWCCommands[] PROGMEM = D_PRFX_WEBCAM "|" // Prefix "|" D_CMND_WC_STREAM "|" D_CMND_WC_RESOLUTION "|" D_CMND_WC_MIRROR "|" D_CMND_WC_FLIP "|" D_CMND_WC_SATURATION "|" D_CMND_WC_BRIGHTNESS "|" D_CMND_WC_CONTRAST "|" D_CMND_WC_INIT +#ifdef ENABLE_RTSPSERVER + "|" D_CMND_RTSP +#endif // ENABLE_RTSPSERVER ; void (* const WCCommand[])(void) PROGMEM = { &CmndWebcam, &CmndWebcamStream, &CmndWebcamResolution, &CmndWebcamMirror, &CmndWebcamFlip, &CmndWebcamSaturation, &CmndWebcamBrightness, &CmndWebcamContrast, &CmndWebcamInit +#ifdef ENABLE_RTSPSERVER + , &CmndWebRtsp +#endif // ENABLE_RTSPSERVER }; void CmndWebcam(void) { Response_P(PSTR("{\"" D_PRFX_WEBCAM "\":{\"" D_CMND_WC_STREAM "\":%d,\"" D_CMND_WC_RESOLUTION "\":%d,\"" D_CMND_WC_MIRROR "\":%d,\"" D_CMND_WC_FLIP "\":%d,\"" - D_CMND_WC_SATURATION "\":%d,\"" D_CMND_WC_BRIGHTNESS "\":%d,\"" D_CMND_WC_CONTRAST "\":%d}}"), + D_CMND_WC_SATURATION "\":%d,\"" D_CMND_WC_BRIGHTNESS "\":%d,\"" D_CMND_WC_CONTRAST "\":%d" +#ifdef ENABLE_RTSPSERVER + ",\"" D_CMND_RTSP "\":%d" +#endif // ENABLE_RTSPSERVER + "}}"), Settings.webcam_config.stream, Settings.webcam_config.resolution, Settings.webcam_config.mirror, Settings.webcam_config.flip, - Settings.webcam_config.saturation -2, Settings.webcam_config.brightness -2, Settings.webcam_config.contrast -2); + Settings.webcam_config.saturation -2, Settings.webcam_config.brightness -2, Settings.webcam_config.contrast -2 +#ifdef ENABLE_RTSPSERVER + , Settings.webcam_config.rtsp +#endif // ENABLE_RTSPSERVER + ); } void CmndWebcamStream(void) { @@ -1022,6 +1045,17 @@ void CmndWebcamInit(void) { ResponseCmndDone(); } +#ifdef ENABLE_RTSPSERVER +void CmndWebRtsp(void) { + if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) { + Settings.webcam_config.rtsp = XdrvMailbox.payload; + TasmotaGlobal.restart_flag = 2; + } + ResponseCmndStateText(Settings.webcam_config.rtsp); +} +#endif // ENABLE_RTSPSERVER + + /*********************************************************************************************\ * Interface \*********************************************************************************************/ From 62649f238e076a0507658ff9218483675481edb7 Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Wed, 20 Jan 2021 08:47:43 +0100 Subject: [PATCH 042/186] refactor core2, i2s --- tasmota/xdrv_42_i2s_audio.ino | 56 +++++++++++++++++++++++++---------- tasmota/xdrv_84_core2.ino | 41 ------------------------- 2 files changed, 40 insertions(+), 57 deletions(-) diff --git a/tasmota/xdrv_42_i2s_audio.ino b/tasmota/xdrv_42_i2s_audio.ino index 0567e3591..a239d9fe8 100644 --- a/tasmota/xdrv_42_i2s_audio.ino +++ b/tasmota/xdrv_42_i2s_audio.ino @@ -94,7 +94,7 @@ uint32_t retryms = 0; #ifdef SAY_TIME AudioGeneratorTalkie *talkie = nullptr; -#endif +#endif // SAY_TIME //! MAX98357A + INMP441 DOUBLE I2S BOARD #ifdef ESP8266 @@ -111,22 +111,22 @@ AudioGeneratorTalkie *talkie = nullptr; #ifndef DAC_IIS_BCK #undef DAC_IIS_BCK #define DAC_IIS_BCK 26 -#endif +#endif // DAC_IIS_BCK #ifndef DAC_IIS_WS #undef DAC_IIS_WS #define DAC_IIS_WS 25 -#endif +#endif // DAC_IIS_WS #ifndef DAC_IIS_DOUT #undef DAC_IIS_DOUT #define DAC_IIS_DOUT 33 -#endif +#endif // DAC_IIS_DOUT #ifndef DAC_IIS_DIN #undef DAC_IIS_DIN #define DAC_IIS_DIN 34 -#endif +#endif // DAC_IIS_DIN #endif // ESP32 @@ -229,7 +229,7 @@ void sayTime(int hour, int minutes, AudioGeneratorTalkie *talkie) { out->stop(); AUDIO_PWR_OFF } -#endif +#endif // SAY_TIME // should be in settings uint8_t is2_volume; @@ -382,7 +382,7 @@ uint32_t i2s_record(char *path, uint32_t secs) { //AddLog_P(LOG_LEVEL_INFO, PSTR("rectime: %d ms"), millis()-stime); SpeakerMic(MODE_SPK); // save to path - SaveWav(mic_path, mic_buff, mic_size); + SaveWav(path, mic_buff, mic_size); free(mic_buff); return 0; } @@ -393,7 +393,8 @@ static const uint8_t wavHTemplate[] PROGMEM = { // Hardcoded simple WAV header w 0x64, 0x61, 0x74, 0x61, 0xff, 0xff, 0xff, 0xff }; bool SaveWav(char *path, uint8_t *buff, uint32_t size) { - File fwp = ufsp->open(path, FILE_WRITE); + File fwp = ufsp->open(path, "w"); + if (!fwp) return false; uint8_t wavHeader[sizeof(wavHTemplate)]; memcpy_P(wavHeader, wavHTemplate, sizeof(wavHTemplate)); @@ -446,7 +447,7 @@ void mp3_task(void *arg) { } } } -#endif +#endif // ESP32 #ifdef USE_WEBRADIO void MDCallback(void *cbData, const char *type, bool isUnicode, const char *str) { @@ -541,6 +542,23 @@ void Cmd_WebRadio(void) { } +#ifdef USE_M5STACK_CORE2 +void Cmd_MicRec(void) { + if (XdrvMailbox.data_len > 0) { + uint16 time = 10; + char *cp = strchr(XdrvMailbox.data, ':'); + if (cp) { + time = atoi(cp + 1); + *cp = 0; + } + if (time<10) time = 10; + if (time>30) time = 30; + i2s_record(XdrvMailbox.data, time); + ResponseCmndChar(XdrvMailbox.data); + } +} +#endif // USE_M5STACK_CORE2 + const char HTTP_WEBRADIO[] PROGMEM = "{s}" "I2S_WR-Title" "{m}%s{e}"; @@ -626,8 +644,11 @@ const char kI2SAudio_Commands[] PROGMEM = "I2S|" "|Play" #ifdef USE_WEBRADIO "|WR" -#endif -#endif +#endif // USE_WEBRADIO +#ifdef USE_M5STACK_CORE2 + "|REC" +#endif // USE_M5STACK_CORE2 +#endif // ESP32 ; void (* const I2SAudio_Command[])(void) PROGMEM = { @@ -636,8 +657,11 @@ void (* const I2SAudio_Command[])(void) PROGMEM = { ,&Cmd_Play #ifdef USE_WEBRADIO ,&Cmd_WebRadio -#endif -#endif +#endif // USE_WEBRADIO +#ifdef USE_M5STACK_CORE2 + ,&Cmd_MicRec +#endif // USE_M5STACK_CORE2 +#endif // ESP32 }; @@ -669,7 +693,7 @@ void Cmd_Say(void) { void Cmd_Time(void) { #ifdef SAY_TIME sayTime(RtcTime.hour, RtcTime.minute, talkie); -#endif +#endif // SAY_TIME ResponseCmndDone(); } @@ -692,8 +716,8 @@ bool Xdrv42(uint8_t function) { case FUNC_WEB_SENSOR: I2S_WR_Show(); break; -#endif -#endif +#endif // USE_WEBRADIO +#endif // USE_WEBSERVER } return result; } diff --git a/tasmota/xdrv_84_core2.ino b/tasmota/xdrv_84_core2.ino index 22b80a1b7..86a576596 100644 --- a/tasmota/xdrv_84_core2.ino +++ b/tasmota/xdrv_84_core2.ino @@ -49,7 +49,6 @@ struct CORE2_globs { uint8_t wakeup_hour; uint8_t wakeup_minute; uint8_t shutdowndelay; - bool timesynced; } core2_globs; struct CORE2_ADC { @@ -262,44 +261,6 @@ uint16_t voltage = 2200; } -/* -void SetRtc(void) { - RTC_TimeTypeDef RTCtime; - RTCtime.Hours = RtcTime.hour; - RTCtime.Minutes = RtcTime.minute; - RTCtime.Seconds = RtcTime.second; - core2_globs.Rtc.SetTime(&RTCtime); - - RTC_DateTypeDef RTCdate; - RTCdate.WeekDay = RtcTime.day_of_week; - RTCdate.Month = RtcTime.month; - RTCdate.Date = RtcTime.day_of_month; - RTCdate.Year = RtcTime.year; - core2_globs.Rtc.SetDate(&RTCdate); -} -*/ - - -// needed for sd card time -void Sync_RTOS_TIME(void) { - - if (Rtc.local_time < START_VALID_TIME || core2_globs.timesynced) return; - - core2_globs.timesynced = 1; -// Set freertos time for sd card - - struct timeval tv; - //tv.tv_sec = Rtc.utc_time; - tv.tv_sec = Rtc.local_time; - tv.tv_usec = 0; - - //struct timezone tz; - //tz.tz_minuteswest = 0; - //tz.tz_dsttime = 0; - //settimeofday(&tv, &tz); - - settimeofday(&tv, NULL); -} void GetRtc(void) { RTC_TimeTypeDef RTCtime; @@ -372,8 +333,6 @@ void CORE2_EverySecond(void) { CORE2_DoShutdown(); } } - - Sync_RTOS_TIME(); } } From f22de759b57997af241a3f60c179b205ff94f98c Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Wed, 20 Jan 2021 08:48:29 +0100 Subject: [PATCH 043/186] add core2 --- platformio_tasmota_env32.ini | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/platformio_tasmota_env32.ini b/platformio_tasmota_env32.ini index 108792fce..610770505 100644 --- a/platformio_tasmota_env32.ini +++ b/platformio_tasmota_env32.ini @@ -44,6 +44,14 @@ board_build.partitions = esp32_partition_app1984k_spiffs12M.csv build_flags = ${common32.build_flags} -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DFIRMWARE_ODROID_GO lib_extra_dirs = lib/libesp32, lib/lib_basic, lib/lib_i2c, lib/lib_rf, lib/lib_div, lib/lib_ssl, lib/lib_display +[env:tasmota32-core2] +extends = env:tasmota32 +board = esp32dev +board_build.f_cpu = 240000000L +build_flags = ${common32.build_flags} -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue +lib_extra_dirs = ${common32.lib_extra_dirs} +board_build.partitions = esp32_partition_app1984k_spiffs12M.csv + [env:tasmota32-minimal] extends = env:tasmota32 build_flags = ${common32.build_flags} -DFIRMWARE_MINIMAL From 9ce6b53691a7b4791ce50cf8e2c9cb5bba1686b7 Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Wed, 20 Jan 2021 08:49:10 +0100 Subject: [PATCH 044/186] small fixes --- tasmota/xdrv_10_scripter.ino | 37 ++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/tasmota/xdrv_10_scripter.ino b/tasmota/xdrv_10_scripter.ino index cd07098f8..aa3a705d4 100755 --- a/tasmota/xdrv_10_scripter.ino +++ b/tasmota/xdrv_10_scripter.ino @@ -1983,6 +1983,9 @@ chknext: case 11: fvar = Energy.daily; break; + case 12: + fvar = (float)Settings.energy_kWhyesterday/100000.0; + break; default: fvar = 99999; @@ -2790,6 +2793,12 @@ chknext: fvar = GetStack(); goto exit; } +#ifdef ESP32 + if (!strncmp(vname, "stkwm", 5)) { + fvar = uxTaskGetStackHighWaterMark(NULL); + goto exit; + } +#endif // ESP32 if (!strncmp(vname, "slen", 4)) { fvar = strlen(glob_script_mem.script_ram); goto exit; @@ -3061,6 +3070,30 @@ chknext: #endif //USE_TOUCH_BUTTONS #endif //USE_DISPLAY +#if 1 + if (!strncmp(vname, "test(", 5)) { + lp = GetNumericArgument(lp + 5, OPER_EQU, &fvar, 0); + uint32_t cycles; + uint64_t accu=0; + char sbuffer[32]; + // PSTR performance test + // this is best case since everything will be in cache + // PSTR at least 3 times slower here, will be much slower if cache missed + for (uint16 cnt=0; cnt<1000; cnt++) { + cycles=ESP.getCycleCount(); + if (fvar>0) { + snprintf_P(sbuffer, sizeof(sbuffer), PSTR("1234")); + } else { + snprintf(sbuffer, sizeof(sbuffer), "1234"); + } + accu += ESP.getCycleCount()-cycles; + } + lp++; + len = 0; + fvar = accu / 1000; + goto exit; + } +#endif break; case 'u': if (!strncmp(vname, "uptime", 6)) { @@ -4552,7 +4585,7 @@ int16_t Run_script_sub(const char *type, int8_t tlen, JsonParserObject *jo) { ctype += tlen; char nxttok = '('; char *argptr = ctype+tlen; - + lp += tlen; do { if (*ctype==nxttok && *lp==nxttok) { @@ -7181,7 +7214,7 @@ bool RulesProcessEvent(char *json_event) { #ifdef USE_SCRIPT_TASK #ifndef STASK_STACK -#define STASK_STACK 8192 +#define STASK_STACK 8192-2048 #endif #if 1 From 238fce3d6216b68694e5d14d8bc4e2ddf9c1970e Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Wed, 20 Jan 2021 08:50:32 +0100 Subject: [PATCH 045/186] add rtsp flag to webcam config --- tasmota/settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasmota/settings.h b/tasmota/settings.h index 32d8b8871..0f27f2c35 100644 --- a/tasmota/settings.h +++ b/tasmota/settings.h @@ -226,7 +226,7 @@ typedef union { // Restricted by MISRA-C Rule 18.4 bu uint32_t stream : 1; uint32_t mirror : 1; uint32_t flip : 1; - uint32_t spare3 : 1; + uint32_t rtsp : 1; uint32_t spare4 : 1; uint32_t spare5 : 1; uint32_t spare6 : 1; From e120e5f1224bf49a8e593953cb3e327ac0053eca Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Wed, 20 Jan 2021 10:18:19 +0100 Subject: [PATCH 046/186] Fix DHT12 negative temps Fix DHT12 negative temps (#10632) --- tasmota/xsns_58_dht12.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasmota/xsns_58_dht12.ino b/tasmota/xsns_58_dht12.ino index 4bab87bb3..966794ce5 100644 --- a/tasmota/xsns_58_dht12.ino +++ b/tasmota/xsns_58_dht12.ino @@ -57,7 +57,7 @@ bool Dht12Read(void) uint8_t checksum = Wire.read(); Dht12.humidity = ConvertHumidity( (float) humidity + (float) humidityTenth/(float) 10.0 ); - Dht12.temperature = ConvertTemp( (float) temp + (float) tempTenth/(float) 10.0 ); + Dht12.temperature = ConvertTemp( ((float)temp + (float)(tempTenth & 0x7F) / (float) 10.0) * (tempTenth & 0x80) ? -1.0 : 1.0 ); if (isnan(Dht12.temperature) || isnan(Dht12.humidity)) { return false; } From be74a768fbabeaef66a02dee795298971544611b Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Wed, 20 Jan 2021 10:44:10 +0100 Subject: [PATCH 047/186] Update support_wifi.ino Correct inifinite loop as timeout would never trigger --- tasmota/support_wifi.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tasmota/support_wifi.ino b/tasmota/support_wifi.ino index c56b93e49..4c37b074a 100644 --- a/tasmota/support_wifi.ino +++ b/tasmota/support_wifi.ino @@ -228,9 +228,9 @@ void WifiBegin(uint8_t flag, uint8_t channel) AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_WIFI "Got IPv6 global address %s"), addr.toString().c_str()); break; // IPv6 is mandatory but stop after 15 seconds } + delay(500); // Loop until real IPv6 address is aquired or too many tries failed + cfgcnt++; } - delay(500); // Loop until real IPv6 address is aquired or too many tries failed - cfgcnt++; } #endif // LWIP_IPV6=1 } From 35bc095e00487fbaca41a854b0d2d1127758bcf9 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Wed, 20 Jan 2021 11:20:56 +0100 Subject: [PATCH 048/186] Add support for SM2135 current selection Add support for SM2135 current selection using GPIO ``SM2135 DAT`` index (#10634) --- CHANGELOG.md | 1 + RELEASENOTES.md | 1 + tasmota/tasmota_template.h | 2 +- tasmota/xlgt_04_sm2135.ino | 26 ++++++++++++++++++-------- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cb4ac228..bbef14ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Support for 24/26/32/34 bit RFID Wiegand interface (D0/D1) by Sigurd Leuther (#3647) - Compile time option ``USE_MQTT_TLS_DROP_OLD_FINGERPRINT`` to drop old (less secure) TLS fingerprint - Command ``SetOption40 0..250`` to disable button functionality if activated for over 0.1 second re-introduced +- Support for SM2135 current selection using GPIO ``SM2135 DAT`` index (#10634) ### Breaking Changed - ESP32 switch from default SPIFFS to default LittleFS file system loosing current (zigbee) files diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9c1e88528..feee3d709 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -80,6 +80,7 @@ The attached binaries can also be downloaded from http://ota.tasmota.com/tasmota - Support for SPI display driver for ST7789 TFT by Gerhard Mutz [#9037](https://github.com/arendst/Tasmota/issues/9037) - Support for time proportioned (``#define USE_TIMEPROP``) and optional PID (``#define USE_PID``) relay control [#10412](https://github.com/arendst/Tasmota/issues/10412) - Support for 24/26/32/34 bit RFID Wiegand interface (D0/D1) by Sigurd Leuther [#3647](https://github.com/arendst/Tasmota/issues/3647) +- Support for SM2135 current selection using GPIO ``SM2135 DAT`` index [#10634](https://github.com/arendst/Tasmota/issues/10634) - Support rotary encoder on Shelly Dimmer [#10407](https://github.com/arendst/Tasmota/issues/10407#issuecomment-756240920) - Support character `#` to be replaced by `space`-character in command ``Publish`` topic [#10258](https://github.com/arendst/Tasmota/issues/10258) - Basic support for ESP32 Odroid Go 16MB binary tasmota32-odroidgo.bin [#8630](https://github.com/arendst/Tasmota/issues/8630) diff --git a/tasmota/tasmota_template.h b/tasmota/tasmota_template.h index 40e90cccd..937cc76ab 100644 --- a/tasmota/tasmota_template.h +++ b/tasmota/tasmota_template.h @@ -323,7 +323,7 @@ const char kSensorNamesFixed[] PROGMEM = #define MAX_A4988_MSS 3 #define MAX_WEBCAM_DATA 8 #define MAX_WEBCAM_HSD 3 -#define MAX_SM2135_DAT 4 +#define MAX_SM2135_DAT 6 const uint16_t kGpioNiceList[] PROGMEM = { GPIO_NONE, // Not used diff --git a/tasmota/xlgt_04_sm2135.ino b/tasmota/xlgt_04_sm2135.ino index c5cf52195..8affdc528 100644 --- a/tasmota/xlgt_04_sm2135.ino +++ b/tasmota/xlgt_04_sm2135.ino @@ -58,7 +58,7 @@ #define SM2135_55MA 0x09 #define SM2135_60MA 0x0A -enum Sm2135Color { SM2135_WCGRB, SM2135_WCBGR, SM2135_WCGRBHI, SM2135_WCBGRHI }; +enum Sm2135Color { SM2135_WCGRB, SM2135_WCBGR, SM2135_WCGRBHI, SM2135_WCBGRHI, SM2135_WCGRB15W, SM2135_WCBGR15W }; struct SM2135 { uint8_t clk = 0; @@ -139,7 +139,7 @@ bool Sm2135SetChannels(void) { uint8_t data[6]; uint32_t light_type = 3; // RGB and CW - if (Sm2135.model < 2) { + if (Sm2135.model < 2) { // Only allow one of two options due to power supply if ((0 == cur_col[0]) && (0 == cur_col[1]) && (0 == cur_col[2])) { light_type = 1; // CW only } else { @@ -182,25 +182,35 @@ void Sm2135ModuleSelected(void) Sm2135.clk = Pin(GPIO_SM2135_CLK); Sm2135.data = Pin(GPIO_SM2135_DAT, GPIO_ANY); - Sm2135.model = GetPin(Sm2135.data) - AGPIO(GPIO_SM2135_DAT); // 0 .. 3 + // See #define MAX_SM2135_DAT 6 in tasmota_template.h + Sm2135.model = GetPin(Sm2135.data) - AGPIO(GPIO_SM2135_DAT); // 0 .. 5 + + // Legacy support of model selection if (PinUsed(GPIO_SWT1)) { Sm2135.model = SM2135_WCBGR; pinMode(Pin(GPIO_SWT1), INPUT); // Discard GPIO_SWT functionality SetPin(Pin(GPIO_SWT1), AGPIO(GPIO_NONE)); } -// RGB current CW current + // SM2135 Dat 1/2 + // RGB current CW current Sm2135.current = (SM2135_20MA << 4) | SM2135_15MA; // See https://github.com/arendst/Tasmota/issues/6495#issuecomment-549121683 - if (Sm2135.model > SM2135_WCBGR) { - Sm2135.current = (SM2135_20MA << 4) | SM2135_30MA; + switch (Sm2135.model) { + case SM2135_WCGRBHI: // SM2135 Dat 3 + case SM2135_WCBGRHI: // SM2135 Dat 4 + Sm2135.current = (SM2135_20MA << 4) | SM2135_30MA; + break; + case SM2135_WCGRB15W: // SM2135 Dat 5 + case SM2135_WCBGR15W: // SM2135 Dat 6 + Sm2135.current = (SM2135_45MA << 4) | SM2135_60MA; + break; } Sm2135Init(); TasmotaGlobal.light_type = LT_RGBWC; TasmotaGlobal.light_driver = XLGT_04; - AddLog_P(LOG_LEVEL_DEBUG, PSTR("LGT: SM2135 (%s-%s current) Found"), - (SM2135_WCBGR == (Sm2135.model &1)) ? PSTR("BGR") : PSTR("GRB"), (Sm2135.model > SM2135_WCBGR) ? PSTR("High") : PSTR("Low")); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("LGT: SM2135 %s Found"), (SM2135_WCBGR == (Sm2135.model &1)) ? PSTR("BGR") : PSTR("GRB")); } } From f08266a38451a4124e66401533613083a2405b20 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Wed, 20 Jan 2021 11:40:21 +0100 Subject: [PATCH 049/186] add tasmota32-core2 --- platformio_tasmota32.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/platformio_tasmota32.ini b/platformio_tasmota32.ini index 7053ac3af..40c3b38a1 100644 --- a/platformio_tasmota32.ini +++ b/platformio_tasmota32.ini @@ -9,6 +9,7 @@ default_envs = ${build_envs.default_envs} ; tasmota32 ; tasmota32-webcam ; tasmota32-odroidgo +; tasmota32-core2 ; tasmota32-minimal ; tasmota32-lite ; tasmota32-knx From 431a7cf18221eb1b5c54e17a70e931557d73f492 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Wed, 20 Jan 2021 11:48:17 +0100 Subject: [PATCH 050/186] add support for core2 --- .github/workflows/CI_github_ESP32.yml | 20 +++++++++++++++++++ .github/workflows/Tasmota_build.yml | 23 ++++++++++++++++++++++ .github/workflows/Tasmota_build_master.yml | 23 ++++++++++++++++++++++ platformio_override_sample.ini | 1 + 4 files changed, 67 insertions(+) diff --git a/.github/workflows/CI_github_ESP32.yml b/.github/workflows/CI_github_ESP32.yml index ecf8bf561..fbf8beab0 100644 --- a/.github/workflows/CI_github_ESP32.yml +++ b/.github/workflows/CI_github_ESP32.yml @@ -64,6 +64,26 @@ jobs: name: firmware path: ./build_output/firmware + tasmota32-core2: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U platformio + platformio upgrade --dev + platformio update + - name: Run PlatformIO + run: | + platformio run -e tasmota32-core2 + - uses: actions/upload-artifact@v2 + with: + name: firmware + path: ./build_output/firmware + tasmota32-minimal: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/Tasmota_build.yml b/.github/workflows/Tasmota_build.yml index 6cf253537..ccee1adf0 100644 --- a/.github/workflows/Tasmota_build.yml +++ b/.github/workflows/Tasmota_build.yml @@ -898,6 +898,29 @@ jobs: path: ./build_output/firmware + tasmota32-core2: + needs: tasmota_pull + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U platformio + platformio upgrade --dev + platformio update + - name: Run PlatformIO + run: | + platformio run -e tasmota32-core2 + - uses: actions/upload-artifact@v2 + with: + name: firmware + path: ./build_output/firmware + + tasmota32-knx: needs: tasmota_pull runs-on: ubuntu-latest diff --git a/.github/workflows/Tasmota_build_master.yml b/.github/workflows/Tasmota_build_master.yml index b2b68c477..966337686 100644 --- a/.github/workflows/Tasmota_build_master.yml +++ b/.github/workflows/Tasmota_build_master.yml @@ -898,6 +898,29 @@ jobs: path: ./build_output/firmware + tasmota32-core2: + needs: tasmota_pull + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U platformio + platformio upgrade --dev + platformio update + - name: Run PlatformIO + run: | + platformio run -e tasmota32-core2 + - uses: actions/upload-artifact@v2 + with: + name: firmware + path: ./build_output/firmware + + tasmota32-knx: needs: tasmota_pull runs-on: ubuntu-latest diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini index deff8272a..5bdf1a7f2 100644 --- a/platformio_override_sample.ini +++ b/platformio_override_sample.ini @@ -37,6 +37,7 @@ default_envs = ; tasmota32-ircustom ; tasmota32solo1 ; tasmota32-odroidgo +; tasmota32-core2 [common] From 80b6eb73ed85b0e2d1cbb02ec494eb2c5e07ef12 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Wed, 20 Jan 2021 12:54:09 +0100 Subject: [PATCH 051/186] mv core2 in firmware folder --- .github/workflows/Tasmota_build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/Tasmota_build.yml b/.github/workflows/Tasmota_build.yml index ccee1adf0..40a018b58 100644 --- a/.github/workflows/Tasmota_build.yml +++ b/.github/workflows/Tasmota_build.yml @@ -1623,6 +1623,7 @@ jobs: [ ! -f ./mv_firmware/tasmota32-display.* ] || mv ./mv_firmware/tasmota32-display.* ./firmware/tasmota32/ [ ! -f ./mv_firmware/tasmota32-web*.* ] || mv ./mv_firmware/tasmota32-web*.* ./firmware/tasmota32/ [ ! -f ./mv_firmware/tasmota32-odroidgo.* ] || mv ./mv_firmware/tasmota32-odroidgo.* ./firmware/tasmota32/ + [ ! -f ./mv_firmware/tasmota32-core2.* ] || mv ./mv_firmware/tasmota32-core2.* ./firmware/tasmota32/ [ ! -f ./mv_firmware/tasmota32-knx.* ] || mv ./mv_firmware/tasmota32-knx.* ./firmware/tasmota32/ [ ! -f ./mv_firmware/tasmota32* ] || mv ./mv_firmware/tasmota32* ./firmware/tasmota32/languages/ [ ! -f ./mv_firmware/* ] || mv ./mv_firmware/* ./firmware/tasmota/languages/ From 149cbc58e7ec2920cafe254d9adf501fd06ec9ef Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Wed, 20 Jan 2021 12:56:20 +0100 Subject: [PATCH 052/186] mv core2 in firmware folder --- .github/workflows/Tasmota_build_master.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/Tasmota_build_master.yml b/.github/workflows/Tasmota_build_master.yml index 966337686..62dde9232 100644 --- a/.github/workflows/Tasmota_build_master.yml +++ b/.github/workflows/Tasmota_build_master.yml @@ -1623,6 +1623,7 @@ jobs: [ ! -f ./mv_firmware/tasmota32-display.* ] || mv ./mv_firmware/tasmota32-display.* ./firmware/tasmota32/ [ ! -f ./mv_firmware/tasmota32-web*.* ] || mv ./mv_firmware/tasmota32-web*.* ./firmware/tasmota32/ [ ! -f ./mv_firmware/tasmota32-odroidgo.* ] || mv ./mv_firmware/tasmota32-odroidgo.* ./firmware/tasmota32/ + [ ! -f ./mv_firmware/tasmota32-core2.* ] || mv ./mv_firmware/tasmota32-core2.* ./firmware/tasmota32/ [ ! -f ./mv_firmware/tasmota32-knx.* ] || mv ./mv_firmware/tasmota32-knx.* ./firmware/tasmota32/ [ ! -f ./mv_firmware/tasmota32* ] || mv ./mv_firmware/tasmota32* ./firmware/tasmota32/languages/ [ ! -f ./mv_firmware/* ] || mv ./mv_firmware/* ./firmware/tasmota/languages/ From d4c22821701c9c037400d709c0c75a98c9e96438 Mon Sep 17 00:00:00 2001 From: Norbert Richter Date: Wed, 20 Jan 2021 13:56:57 +0100 Subject: [PATCH 053/186] Add Sugar Valley NeoPool Controller --- .../TasmotaModbus-1.2.0/src/TasmotaModbus.cpp | 2 +- .../TasmotaModbus-1.2.0/src/TasmotaModbus.h | 2 + tasmota/language/af_AF.h | 3 + tasmota/language/bg_BG.h | 3 + tasmota/language/cs_CZ.h | 3 + tasmota/language/de_DE.h | 3 + tasmota/language/el_GR.h | 3 + tasmota/language/en_GB.h | 3 + tasmota/language/es_ES.h | 3 + tasmota/language/fr_FR.h | 3 + tasmota/language/he_HE.h | 3 + tasmota/language/hu_HU.h | 3 + tasmota/language/ko_KO.h | 3 + tasmota/language/nl_NL.h | 3 + tasmota/language/pl_PL.h | 3 + tasmota/language/pt_BR.h | 3 + tasmota/language/pt_PT.h | 3 + tasmota/language/ro_RO.h | 3 + tasmota/language/ru_RU.h | 3 + tasmota/language/sk_SK.h | 3 + tasmota/language/sv_SE.h | 3 + tasmota/language/tr_TR.h | 3 + tasmota/language/uk_UA.h | 3 + tasmota/language/vi_VN.h | 3 + tasmota/language/zh_CN.h | 3 + tasmota/language/zh_TW.h | 3 + tasmota/my_user_config.h | 3 + tasmota/tasmota_template.h | 6 + tasmota/xsns_83_neopool.ino | 1403 +++++++++++++++++ 29 files changed, 1487 insertions(+), 1 deletion(-) create mode 100644 tasmota/xsns_83_neopool.ino diff --git a/lib/lib_basic/TasmotaModbus-1.2.0/src/TasmotaModbus.cpp b/lib/lib_basic/TasmotaModbus-1.2.0/src/TasmotaModbus.cpp index b1a8c48f8..725c8a467 100644 --- a/lib/lib_basic/TasmotaModbus-1.2.0/src/TasmotaModbus.cpp +++ b/lib/lib_basic/TasmotaModbus-1.2.0/src/TasmotaModbus.cpp @@ -24,7 +24,7 @@ TasmotaModbus::TasmotaModbus(int receive_pin, int transmit_pin) : TasmotaSerial( mb_address = 0; } -uint16_t CalculateCRC(uint8_t *frame, uint8_t num) +uint16_t TasmotaModbus::CalculateCRC(uint8_t *frame, uint8_t num) { uint16_t crc = 0xFFFF; diff --git a/lib/lib_basic/TasmotaModbus-1.2.0/src/TasmotaModbus.h b/lib/lib_basic/TasmotaModbus-1.2.0/src/TasmotaModbus.h index 22d4e12f3..d6c0c2883 100644 --- a/lib/lib_basic/TasmotaModbus-1.2.0/src/TasmotaModbus.h +++ b/lib/lib_basic/TasmotaModbus-1.2.0/src/TasmotaModbus.h @@ -32,6 +32,8 @@ class TasmotaModbus : public TasmotaSerial { int Begin(long speed = TM_MODBUS_BAUDRATE, int stop_bits = 1); + uint16_t CalculateCRC(uint8_t *frame, uint8_t num); + void Send(uint8_t device_address, uint8_t function_code, uint16_t start_address, uint16_t register_count); bool ReceiveReady(); diff --git a/tasmota/language/af_AF.h b/tasmota/language/af_AF.h index b13396097..48ea31b08 100644 --- a/tasmota/language/af_AF.h +++ b/tasmota/language/af_AF.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/bg_BG.h b/tasmota/language/bg_BG.h index b69e1b81c..50a221034 100644 --- a/tasmota/language/bg_BG.h +++ b/tasmota/language/bg_BG.h @@ -783,6 +783,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/cs_CZ.h b/tasmota/language/cs_CZ.h index 6ed6b3b63..8925ed5fc 100644 --- a/tasmota/language/cs_CZ.h +++ b/tasmota/language/cs_CZ.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/de_DE.h b/tasmota/language/de_DE.h index 635318b54..2228193fb 100644 --- a/tasmota/language/de_DE.h +++ b/tasmota/language/de_DE.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/el_GR.h b/tasmota/language/el_GR.h index 718ee89d8..3d0152d8a 100644 --- a/tasmota/language/el_GR.h +++ b/tasmota/language/el_GR.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/en_GB.h b/tasmota/language/en_GB.h index 9d0a73354..fa36d2fe2 100644 --- a/tasmota/language/en_GB.h +++ b/tasmota/language/en_GB.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/es_ES.h b/tasmota/language/es_ES.h index b35de9d0b..0f576cc9d 100644 --- a/tasmota/language/es_ES.h +++ b/tasmota/language/es_ES.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/fr_FR.h b/tasmota/language/fr_FR.h index 5dfc7c5ba..846ffdca0 100644 --- a/tasmota/language/fr_FR.h +++ b/tasmota/language/fr_FR.h @@ -780,6 +780,9 @@ #define D_SENSOR_SDCARD_CS "CarteSD CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/he_HE.h b/tasmota/language/he_HE.h index b36412ac1..800e71ec9 100644 --- a/tasmota/language/he_HE.h +++ b/tasmota/language/he_HE.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/hu_HU.h b/tasmota/language/hu_HU.h index 62f2131ce..e831e0cb1 100644 --- a/tasmota/language/hu_HU.h +++ b/tasmota/language/hu_HU.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/ko_KO.h b/tasmota/language/ko_KO.h index 7593261df..58b01ee18 100644 --- a/tasmota/language/ko_KO.h +++ b/tasmota/language/ko_KO.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/nl_NL.h b/tasmota/language/nl_NL.h index 0eea3bb75..587495f34 100644 --- a/tasmota/language/nl_NL.h +++ b/tasmota/language/nl_NL.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/pl_PL.h b/tasmota/language/pl_PL.h index 2d614d037..25687f769 100644 --- a/tasmota/language/pl_PL.h +++ b/tasmota/language/pl_PL.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/pt_BR.h b/tasmota/language/pt_BR.h index ecb4a8afc..800c7fd16 100644 --- a/tasmota/language/pt_BR.h +++ b/tasmota/language/pt_BR.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/pt_PT.h b/tasmota/language/pt_PT.h index 6f7a11f01..94f67d3d4 100644 --- a/tasmota/language/pt_PT.h +++ b/tasmota/language/pt_PT.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/ro_RO.h b/tasmota/language/ro_RO.h index 270b251d7..f3d72415c 100644 --- a/tasmota/language/ro_RO.h +++ b/tasmota/language/ro_RO.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/ru_RU.h b/tasmota/language/ru_RU.h index e37ed4e8f..d58300f8a 100644 --- a/tasmota/language/ru_RU.h +++ b/tasmota/language/ru_RU.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "А" diff --git a/tasmota/language/sk_SK.h b/tasmota/language/sk_SK.h index 2eebf1331..895042b6b 100644 --- a/tasmota/language/sk_SK.h +++ b/tasmota/language/sk_SK.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/sv_SE.h b/tasmota/language/sv_SE.h index ed97b84e8..32957d309 100644 --- a/tasmota/language/sv_SE.h +++ b/tasmota/language/sv_SE.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/tr_TR.h b/tasmota/language/tr_TR.h index 659dcf15b..02c0939b9 100644 --- a/tasmota/language/tr_TR.h +++ b/tasmota/language/tr_TR.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/uk_UA.h b/tasmota/language/uk_UA.h index a350cef9b..75f2e2d4b 100644 --- a/tasmota/language/uk_UA.h +++ b/tasmota/language/uk_UA.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "А" diff --git a/tasmota/language/vi_VN.h b/tasmota/language/vi_VN.h index 86eb6eb2e..d0abc1775 100644 --- a/tasmota/language/vi_VN.h +++ b/tasmota/language/vi_VN.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/zh_CN.h b/tasmota/language/zh_CN.h index d0a29c3c1..88f4463b3 100644 --- a/tasmota/language/zh_CN.h +++ b/tasmota/language/zh_CN.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "安" diff --git a/tasmota/language/zh_TW.h b/tasmota/language/zh_TW.h index 02d898d5a..7457285ba 100644 --- a/tasmota/language/zh_TW.h +++ b/tasmota/language/zh_TW.h @@ -784,6 +784,9 @@ #define D_SENSOR_SDCARD_CS "SDCard CS" #define D_SENSOR_WIEGAND_D0 "Wiegand D0" #define D_SENSOR_WIEGAND_D1 "Wiegand D1" +#define D_SENSOR_NEOPOOL_TX "NeoPool Tx" +#define D_SENSOR_NEOPOOL_RX "NeoPool Rx" + // Units #define D_UNIT_AMPERE "安培" diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 8cf0316f8..cd1d62cd7 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -796,6 +796,9 @@ //#define USE_PROMETHEUS // Add support for https://prometheus.io/ metrics exporting over HTTP /metrics endpoint +//#define USE_NEOPOOL // Add support for Sugar Valley NeoPool Controller - also known under brands Hidrolife, Aquascenic, Oxilife, Bionet, Hidroniser, UVScenic, Station, Brilix, Bayrol and Hay (+6k flash, +60 mem) +// #define NEOPOOL_MODBUS_ADDRESS 1 // Any modbus address + // -- Thermostat control ---------------------------- //#define USE_THERMOSTAT // Add support for Thermostat #define THERMOSTAT_CONTROLLER_OUTPUTS 1 // Number of outputs to be controlled independently diff --git a/tasmota/tasmota_template.h b/tasmota/tasmota_template.h index 937cc76ab..eff968de1 100644 --- a/tasmota/tasmota_template.h +++ b/tasmota/tasmota_template.h @@ -147,6 +147,7 @@ enum UserSelectablePins { GPIO_ADC_PH, // Analog PH Sensor GPIO_BS814_CLK, GPIO_BS814_DAT, // Holtek BS814A2 touch ctrlr GPIO_WIEGAND_D0, GPIO_WIEGAND_D1, // Wiegand Data lines + GPIO_NEOPOOL_TX, GPIO_NEOPOOL_RX, // Sugar Valley RS485 interface GPIO_SENSOR_END }; enum ProgramSelectablePins { @@ -314,6 +315,7 @@ const char kSensorNames[] PROGMEM = D_SENSOR_ADC_PH "|" D_SENSOR_BS814_CLK "|" D_SENSOR_BS814_DAT "|" D_SENSOR_WIEGAND_D0 "|" D_SENSOR_WIEGAND_D1 "|" + D_SENSOR_NEOPOOL_TX "|" D_SENSOR_NEOPOOL_RX "|" ; const char kSensorNamesFixed[] PROGMEM = @@ -741,6 +743,10 @@ const uint16_t kGpioNiceList[] PROGMEM = { AGPIO(GPIO_WIEGAND_D0), // Date line D0 of Wiegand devices AGPIO(GPIO_WIEGAND_D1), // Date line D1 of Wiegand devices #endif +#ifdef USE_NEOPOOL + AGPIO(GPIO_NEOPOOL_TX), // Sugar Valley RS485 Interface + AGPIO(GPIO_NEOPOOL_RX), // Sugar Valley RS485 Interface +#endif /*-------------------------------------------------------------------------------------------*\ * ESP32 specifics diff --git a/tasmota/xsns_83_neopool.ino b/tasmota/xsns_83_neopool.ino new file mode 100644 index 000000000..87a5f8e8a --- /dev/null +++ b/tasmota/xsns_83_neopool.ino @@ -0,0 +1,1403 @@ +/* + xsns_83_neopool.ino - Sugar Valley NeoPool Control System Modbus support for Tasmota + + Copyright (C) 2021 Norbert Richter + + 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 . +*/ + +#ifdef USE_NEOPOOL +/*********************************************************************************************\ + * Sugar Valley NeoPool electronic pool control and water treatment system, also known as brand + * Hidrolife (yellow case) + * Aquascenic (blue case) + * Oxilife (green case) + * Bionet (light blue case) + * Hidroniser (red case) + * UVScenic (lilca case) + * Station (orange case) + * Brilix + * Bayrol + * Hay + * + * Sugar Valley RS485 connector inside (Display/Wifi/External) + * pins (from top to bottom): + * + * RS485 MODBUS + * ___ + * 1 |* |- +12V + * 2 |* |- + * 3 |* |- Modbus A+ + * 4 |* |- Modbus B- + * 5 |*__|- Modbus GND + * + * RS485 Parameter: 19200 Baud / 1 Stopbit / Parity None + * + * Hardware serial will be selected if GPIO1 = [NeoPool Rx] and GPIO3 = [NeoPool Tx] +\*********************************************************************************************/ + +#define XSNS_83 83 + +#ifndef NEOPOOL_MODBUS_SPEED +#define NEOPOOL_MODBUS_SPEED 19200 +#endif + +#ifndef NEOPOOL_MODBUS_ADDRESS +#define NEOPOOL_MODBUS_ADDRESS 1 // Modbus address +#endif + + +#define NEOPOOL_READ_REGISTER 0x03 // Function code used to read register: Read Holding Registers +#define NEOPOOL_WRITE_REGISTER 0x10 // Function code used to write register: Write Multiple Registers +#define NEOPOOL_READ_TIMEOUT 25 // read data timeout in ms + +//#define NEOPOOL_OPTIMIZE_READINGS // Optimize modbus readings by considering of MBF_NOTIFICATION register - Note: Does not work on all systems! + +/*********************************************************************************************\ + * Sugar Valley Modbus Register (* register are currently used) + * (see https://downloads.vodnici.net/uploads/wpforo/attachments/69/171-Modbus-registers.pdf) +\*********************************************************************************************/ +enum NeoPoolRegister { + // addr Unit Description + // ------ ----- ------------------------------------------------------------ + // MODBUS page (0x0000 - 0x002E - unknown - for internal use only) + + // MEASURE page (0x01xx) + MBF_ION_CURRENT=0x0100, // 0x0100* Current measured in the ionization system + MBF_HIDRO_CURRENT, // 0x0101* Intensity level currently measured in the hydrolysissystem + MBF_MEASURE_PH, // 0x0102* ph Level measured in hundredths (700=7.00) + MBF_MEASURE_RX, // 0x0103* ppm Redox level in hundredths of ppm (100=1.00 ppm) + MBF_MEASURE_CL, // 0x0104* ppm Level measured in hundredths of chlorine ppm (100=1.00 ppm) + MBF_MEASURE_CONDUCTIVITY, // 0x0105 % Level of conductivity measured in the water. + MBF_MEASURE_TEMPERATURE, // 0x0106* °C Water temperature sensor (100=10.0°C) + MBF_PH_STATUS, // 0x0107* mask Status of the module control pH + MBF_RX_STATUS, // 0x0108* mask Status of the Rx-module + MBF_CL_STATUS, // 0x0109* mask Status of the Chlorine-module + MBF_CD_STATUS, // 0x010A mask Status of the Conductivity-module + MBF_ION_STATUS=0x010C, // 0x010C* mask Status of the Ionization-module + MBF_HIDRO_STATUS, // 0x010D* mask Status of the Hydrolysis-module + MBF_RELAY_STATE, // 0x010E* mask Status of each configurable relays + MBF_HIDRO_SWITCH_VALUE, // 0x010F INTERNAL - contains the opening of the hydrolysis PWM. + MBF_NOTIFICATION, // 0x0110* mask Reports whether a page of properties has changed since the last time it was consulted. + MBF_HIDRO_VOLTAGE, // 0x0111 Reports on the stress applied to the hydrolysis cell. This register, together with that of MBF_HIDRO_CURRENT allows extrapolating the salinity of the water. + + // GLOBAL page (0x02xx) + MBF_SAVE_TO_EEPROM=0x02F0, // 0x02F0 A write operation to this register starts a EEPROM storage operation immediately. During the EEPROM storage procedure, the system may be unresponsive to MODBUS requests. The operation will last always less than 1 second. + + // FACTORY page (0x03xx) + MBF_PAR_VERSION=0x0300, // 0x0300* Software version of the PowerBox (unused) + MBF_PAR_MODEL, // 0x0301* mask System model options + MBF_PAR_SERNUM, // 0x0302 Serial number of the PowerBox (unused) + MBF_PAR_ION_NOM, // 0x0303 Ionization maximum production level (DO NOT WRITE!) + MBF_PAR_HIDRO_NOM=0x0306, // 0x0306 Hydrolysis maximum production level. (DO NOT WRITE!) If the hydrolysis is set to work in percent mode, this value will be 100. If the hydrolysis module is set to work in g/h production, this module will contain the maximum amount of production in g/h units. (DO NOT WRITE!) + MBF_PAR_SAL_AMPS=0x030A, // 0x030A Current command in regulation for which we are going to measure voltage + MBF_PAR_SAL_CELLK, // 0x030B Specifies the relationship between the resistance obtained in the measurement process and its equivalence in g / l (grams per liter) + MBF_PAR_SAL_TCOMP, // 0x030C Specifies the deviation in temperature from the conductivity. + MBF_PAR_HIDRO_MAX_VOLTAGE=0x0322, // 0x0322 Allows setting the maximum voltage value that can be reached with the hydrolysis current regulation. The value is specified in tenths of a volt. The default value of this register when the EEPROM is cleared is 80, which is equivalent to a maximum cell operating voltage of 8 volts. + MBF_PAR_HIDRO_FLOW_SIGNAL, // 0x0323 Allows to select the operation of the flow detection signal associated with the operation of the hydrolysis (see MBV_PAR_HIDRO_FLOW_SIGNAL*). The default value for this register is 0 (standard detection). + MBF_PAR_HIDRO_MAX_PWM_STEP_UP, // 0x0324 This register sets the PWM ramp up of the hydrolysis in pulses per duty cycle. This register makes it possible to adjust the rate at which the power delivered to the cell increases, allowing a gradual rise in power so that the operation of the switching source of the equipment is not saturated. Default 150 + MBF_PAR_HIDRO_MAX_PWM_STEP_DOWN, // 0x0325 This register sets the PWM down ramp of the hydrolysis in pulses per duty cycle. This register allows adjusting the rate at which the power delivered to the cell decreases, allowing a gradual drop in power so that the switched source of the equipment is not disconnected due to lack of consumption. This gradual fall must be in accordance with the type of cell used, since said cell stores charge once the current stimulus has ceased. Default 20 + + // INSTALLER page (0x04xx) + MBF_PAR_ION_POL0=0x0400, // 0x0400 Time in min the team must spend working on positive polarization in copper-silver ionization. + MBF_PAR_ION_POL1, // 0x0401 Time in min the team must spend working on positive polarization in copper-silver ionization. + MBF_PAR_ION_POL2, // 0x0402 Time in min the team must spend working on positive polarization in copper-silver ionization. + MBF_PAR_HIDRO_ION_CAUDAL, // 0x0403 mask Bitmask register regulates the external control mode of ionization, hydrolysis and pumps. + MBF_PAR_HIDRO_MODE, // 0x0404 Regulates the external control mode of hydrolysis from the modules of measure. 0: no control, 1: standard control (on / off), 2: with timed pump + MBF_PAR_HIDRO_POL0, // 0x0405 Time must spend working on positive polarization in hydrolysis / electrolysis. Time is stored in minutes. + MBF_PAR_HIDRO_POL1, // 0x0406 Time must spend working on positive polarization in hydrolysis / electrolysis. Time is stored in minutes. + MBF_PAR_HIDRO_POL2, // 0x0407 Time must spend working on positive polarization in hydrolysis / electrolysis. Time is stored in minutes. + MBF_PAR_TIME_LOW, // 0x0408* System timestamp (32 bit unixtime) - low word + MBF_PAR_TIME_HIGH, // 0x0409* System timestamp (32 bit unixtime) - high word + MBF_PAR_PH_ACID_RELAY_GPIO, // 0x040A* Relay number assigned to the acid pump function (only with pH module). + MBF_PAR_PH_BASE_RELAY_GPIO, // 0x040B* Relay number assigned to the base pump function (only with pH module). + MBF_PAR_RX_RELAY_GPIO, // 0x040C* Relay number assigned to the Redox level regulation function. If the value is 0, there is no relay assigned, and therefore there is no pump function (ON / OFF should not be displayed) + MBF_PAR_CL_RELAY_GPIO, // 0x040D* Relay number assigned to the chlorine pump function (only with free chlorine measuring modules). + MBF_PAR_CD_RELAY_GPIO, // 0x040E* Relay number assigned to the conductivity (brine) pump function (only with conductivity measurement modules). + MBF_PAR_TEMPERATURE_ACTIVE, // 0x040F* Indicates whether the equipment has a temperature measurement or not. + MBF_PAR_LIGHTING_GPIO, // 0x0410* Relay number assigned to the lighting function. 0: inactive. + MBF_PAR_FILT_MODE, // 0x0411* Filtration mode (see MBV_PAR_FILT_*) + MBF_PAR_FILT_GPIO, // 0x0412* Relay selected to perform the filtering function (by default it is relay 2). When this value is at zero, there is no relay assigned and therefore it is understood that the equipment does not control the filtration. In this case, the filter option does not appear in the user menu. + MBF_PAR_FILT_MANUAL_STATE, // 0x0413 Filtration status in manual mode (on = 1; off = 0) + MBF_PAR_HEATING_MODE, // 0x0414 Heating mode. 0: the equipment is not heated. 1: the equipment is heating. + MBF_PAR_HEATING_GPIO, // 0x0415 Relay selected to perform the heating function (by default it is relay 7). When this value is at zero, there is no relay assigned and therefore it is understood that the equipment does not control the heating. In this case, the filter modes associated with heating will not be displayed. + MBF_PAR_HEATING_TEMP, // 0x0416 Heating setpoint temperature + MBF_PAR_CLIMA_ONOFF, // 0x0417 Activation of the air conditioning mode (0 inactive; 1 active. + MBF_PAR_SMART_TEMP_HIGH, // 0x0418 Superior temperature of the Smart mode + MBF_PAR_SMART_TEMP_LOW, // 0x0419 Lower temperature of the Smart mode + MBF_PAR_SMART_ANTI_FREEZE, // 0x041A Antifreeze mode activated (1) or not (0). This adjustment is only available in the Smart filtration mode. + MBF_PAR_SMART_INTERVAL_REDUCTION, // 0x041B This register is read-only and reports to the outside what percentage (0 to 100%) is being applied to the nominal filtration time. 100% means that the total programmed time is being filtered. + MBF_PAR_INTELLIGENT_TEMP, // 0x041C Setpoint temperature for smart mode + MBF_PAR_INTELLIGENT_FILT_MIN_TIME, // 0x041D Minimum filtration time in minutes + MBF_PAR_INTELLIGENT_BONUS_TIME, // 0x041E Bonus time for the current set of intervals + MBF_PAR_INTELLIGENT_TT_NEXT_INTERVAL,//0x041F Time to next filtration interval. When it reaches 0 an interval is started and the number of seconds is reloaded for the next interval (2x3600) + MBF_PAR_INTELLIGENT_INTERVALS, // 0x0420 Number of started intervals. When it reaches 12 it is reset to 0 and the bonus time is reloaded with the value of MBF_PAR_INTELLIGENT_FILT_MIN_TIME + MBF_PAR_FILTRATION_STATE, // 0x0421 Filtration function state: 0 is off and 1 is on. The filtration state is regulated according to the MBF_PAR_FILT_MANUAL_STATE register if the filtration mode held in register MBF_PAR_FILT_MODE is set to FILT_MODE_MANUAL (0). + MBF_PAR_HEATING_DELAY_TIME, // 0x0422 // Timer in seconds that counts up when the heating is to be enabled. Once this counter reaches 60 seconds, the heating is then enabled. This counter is for internal use only. + MBF_PAR_FILTERING_TIME_LOW, // 0x0423 32-bit value: + MBF_PAR_FILTERING_TIME_HIGH, // 0x0424 Internal timer for the intelligent filtering mode. It counts the filtering time done during a given day. This register is only for internal use and should not be modified by the user. + MBF_PAR_INTELLIGENT_INTERVAL_TIME_LOW,// 0x0425 32-bit value: + MBF_PAR_INTELLIGENT_INTERVAL_TIME_HIGH,// 0x0426 Internal timer that counts the filtration interval assigned to the the intelligent mode. This register is only for internal use and should not be modified by the user. + MBF_PAR_UV_MODE, // 0x0427 UV mode active or not - see MBV_PAR_UV_MODE*. To enable UV support for a given device, add the mask MBMSK_MODEL_UV to the MBF_PAR_MODEL register. + MBF_PAR_UV_HIDE_WARN, // 0x0428 mask Suppression for warning messages in the UV mode. + MBF_PAR_UV_RELAY_GPIO, // 0x0429 Relay number assigned to the UV function. + MBF_PAR_PH_PUMP_REP_TIME_ON, // 0x042A mask Time that the pH pump will be turn on in the repetitive mode (see MBMSK_PH_PUMP_*). Contains a special time format, see desc for MBMSK_PH_PUMP_TIME. + MBF_PAR_PH_PUMP_REP_TIME_OFF, // 0x042B Time that the pH pump will be turn off in the repetitive mode. Contains a special time format, see desc for MBMSK_PH_PUMP_TIME, has no upper configuration bit 0x8000 + MBF_PAR_HIDRO_COVER_ENABLE, // 0x042C Options for the hydrolysis/electrolysis module (see MBMSK_HIDRO_*) + MBF_PAR_HIDRO_COVER_REDUCTION, // 0x042D Configured levels for the cover reduction and the hydrolysis shutdown temperature options: LSB=Percentage for the cover reduction, MSB=Temperature level for the hydrolysis shutdown (see MBMSK_HIDRO_*) + MBF_PAR_PUMP_RELAY_TIME_OFF, // 0x042E Time level in minutes or seconds that the dosing pump must remain off when the temporized pump mode is selected. This time level register applies to all pumps except pH. Contains a special time format, see desc for MBMSK_PH_PUMP_TIME, has no upper configuration bit 0x8000 + MBF_PAR_PUMP_RELAY_TIME_ON, // 0x042F Time level in minutes or seconds that the dosing pump must remain on when the temporized pump mode is selected. This time level register applies to all pumps except pH. Contains a special time format, see desc for MBMSK_PH_PUMP_TIME, has no upper configuration bit 0x8000 + MBF_PAR_RELAY_PH, // 0x0430 Determine what pH regulation configuration the equipment has (see MBV_PAR_RELAY_PH_*) + MBF_PAR_RELAY_MAX_TIME, // 0x0431 Maximum amount of time, in seconds, that a dosing pump can operate before rising an alarm signal. The behavior of the system when the dosing time is exceeded is regulated by the type of action stored in the MBF_PAR_RELAY_MODE register. + MBF_PAR_RELAY_MODE, // 0x0432 Behavior of the system when the dosing time is exceeded (see MBMSK_PAR_RELAY_MODE_* and MBV_PAR_RELAY_MODE_*) + MBF_PAR_RELAY_ACTIVATION_DELAY, // 0x0433 Delay time in seconds for the pH pump when the measured pH value is outside the allowable pH setpoints. The system internally adds an extra time of 10 seconds to the value stored here. The pump starts the dosing operation once the condition of pH out of valid interval is maintained during the time specified in this register. + MBF_PAR_TIMER_BLOCK_BASE, // 0x0434 This block of 180 registers holds the configuration of the system timers. The system has a set of 12 fully configurable timers, each one assigned to a specific function, described below: + MBF_PAR_TIMER_BLOCK_FILT_INT1=0x0434,//0x0434 Filtration interval 1 (15 register - see PAR_TIMER_BLOCK_OFF* for desc) + MBF_PAR_TIMER_BLOCK_FILT_INT2=0x0443,//0x0443 Filtration interval 2 (15 register - see PAR_TIMER_BLOCK_OFF* for desc) + MBF_PAR_TIMER_BLOCK_FILT_INT3=0x0452,//0x0452 Filtration interval 3 (15 register - see PAR_TIMER_BLOCK_OFF* for desc) + MBF_PAR_TIMER_BLOCK_AUX1_INT2=0x0461,//0x0461 Auxiliary relay 1 - 2. interval (15 register - see PAR_TIMER_BLOCK_OFF* for desc) + MBF_PAR_TIMER_BLOCK_LIGHT_INT=0x0470,//0x0470 Lighting interval (15 register - see PAR_TIMER_BLOCK_OFF* for desc) + MBF_PAR_TIMER_BLOCK_AUX2_INT2=0x047F,//0x047F Auxiliary relay 2 - 2. interval (15 register - see PAR_TIMER_BLOCK_OFF* for desc) + MBF_PAR_TIMER_BLOCK_AUX3_INT2=0x048E,//0x048E Auxiliary relay 3 - 2. interval (15 register - see PAR_TIMER_BLOCK_OFF* for desc) + MBF_PAR_TIMER_BLOCK_AUX4_INT2=0x049D,//0x049D Auxiliary relay 4 - 2. interval (15 register - see PAR_TIMER_BLOCK_OFF* for desc) + MBF_PAR_TIMER_BLOCK_AUX1_INT1=0x04AC,//0x04AC Auxiliary relay 1 - 1. interval (15 register - see PAR_TIMER_BLOCK_OFF* for desc) + MBF_PAR_TIMER_BLOCK_AUX2_INT1=0x04BB,//0x04BB Auxiliary relay 2 - 1. interval (15 register - see PAR_TIMER_BLOCK_OFF* for desc) + MBF_PAR_TIMER_BLOCK_AUX3_INT1=0x04CA,//0x04CA Auxiliary relay 3 - 1. interval (15 register - see PAR_TIMER_BLOCK_OFF* for desc) + MBF_PAR_TIMER_BLOCK_AUX4_INT1=0x04D9,//0x04D9 Auxiliary relay 4 - 1. interval (15 register - see PAR_TIMER_BLOCK_OFF* for desc) + MBF_PAR_FILTVALVE_ENABLE=0x04E8, // 0x04E8 Filter cleaning functionality mode (0=off, 1=Besgo) + MBF_PAR_FILTVALVE_MODE, // 0x04E9 Filter cleaning valve timing mode, possible modes: MBV_PAR_CTIMER_ENABLED, MBV_PAR_CTIMER_ALWAYS_ON, MBV_PAR_CTIMER_ALWAYS_OFF + MBF_PAR_FILTVALVE_GPIO, // 0x04EA Relay associated with the filter cleaning function. default AUX2 (value 5) + MBF_PAR_FILTVALVE_START_LOW, // 0x04EB start timestamp of filter cleaning (32-bit) + MBF_PAR_FILTVALVE_START_HIGH, // 0x04EC " + MBF_PAR_FILTVALVE_PERIOD_MINUTES, // 0x04ED Period in minutes between cleaning actions. For example, if a value of 60 is stored in this registry, a cleanup action will occur every hour. + MBF_PAR_FILTVALVE_INTERVAL, // 0x04EE Cleaning action duration in seconds. + MBF_PAR_FILTVALVE_REMAINING, // 0x04EF Time remaining for the current cleaning action in seconds. If this register is 0, it means that there is no cleaning function running. When a cleanup function is started, the contents of the MBF_PAR_FILTVALVE_INTERVAL register are copied to this register, then decremented once per second. The display uses this log to track the progress of the cleaning function. + MBF_ACTION_COPY_TO_RTC, // 0x04F0 A write (any value) forces the writing of the RTC time registers MBF_PAR_TIME_LOW (0x0408) and MBF_PAR_TIME_HIGH (0x0409) into the RTC internal microcontroller clock management registers. + + // USER page (0x05xx) To make the modification of this register persistent, execute the EEPROM storage procedure described in global register MBF_SAVE_TO_EEPROM. + MBF_PAR_ION=0x0500, // 0x0500 Ionization target production level. The value adjusted in this register must not exceed the value set in the MBF_PAR_ION_NOM factory register. + MBF_PAR_ION_PR, // 0x0501 Amount of time in minutes that the ionization must be activated each time that the filtration starts. + MBF_PAR_HIDRO, // 0x0502 Hydrolisis target production level. When the hydrolysis production is to be set in percent values, this value will contain the percent of production. If the hydrolysis module is set to work in g/h production, this module will contain the desired amount of production in g/h units. The value adjusted in this register must not exceed the value set in the MBF_PAR_HIDRO_NOM factory register. + MBF_PAR_PH1=0x0504, // 0x0504 Higher limit of the pH regulation system. The value set in this register is multiplied by 100. This means that if we want to set a value of 7.5, the numerical content that we must write in this register is 750. This register must be always higher than MBF_PAR_PH2. + MBF_PAR_PH2, // 0x0505 Lower limit of the pH regulation system. The value set in this register is multiplied by 100. This means that if we want to set a value of 7.0, the numerical content that we must write in this register is 700. This register must be always lower than MBF_PAR_PH1. + MBF_PAR_RX1=0x0508, // 0x0508 Set point for the redox regulation system. This value must be in the range of 0 to 1000. + MBF_PAR_CL1=0x050A, // 0x050A Set point for the chlorine regulation system. The value stored in this register is multiplied by 100. This mean that if we want to set a value of 1.5 ppm, we will have to write a numerical value of 150. This value stored in this register must be in the range of 0 to 1000. + MBF_PAR_FUNCTION_DEPENDENCY=0x051B, // 0x051B mask Specification for the dependency of different functions, such as heating, from external events like FL1 (see MBMSK_FCTDEP_HEATING/MBMSK_DEPENDENCY_*) + + // MISC page (0x06xx) + MBF_PAR_UICFG_MACHINE=0x0600, // 0x0600* Machine type (see MBV_PAR_MACH_* and kNeoPoolMachineNames[]) + MBF_PAR_UICFG_LANGUAGE, // 0x0601 Selected language (see MBV_PAR_LANG_*) + MBF_PAR_UICFG_BACKLIGHT, // 0x0602 Display backlight (see MBV_PAR_BACKLIGHT_*) + MBF_PAR_UICFG_SOUND, // 0x0603 mask Audible alerts (see MBMSK_PAR_SOUND_*) + MBF_PAR_UICFG_PASSWORD, // 0x0604 System password encoded in BCD + MBF_PAR_UICFG_VISUAL_OPTIONS, // 0x0605 mask Stores the different display options for the user interface menus (bitmask). Some bits allow you to hide options that are normally visible (bits 0 to 3) while other bits allow you to show options that are normally hidden (bits 9 to 15) + MBF_PAR_UICFG_VISUAL_OPTIONS_EXT, // 0x0606 mask This register stores additional display options for the user interface menus, see MBMSK_VOE_* + MBF_PAR_UICFG_MACH_VISUAL_STYLE, // 0x0607 mask This register is an expansion of register 0x0600 and 0x0605. The lower part of the register (8 bits LSB) is used to store the type of color selected when in register 0x600 has been specified that the machine is of type "generic". Colors and styles correspond to those listed in record 0x600 MBF_PAR_UICFG_MACHINE. The upper part (8-bit MSB) contains extra bits MBMSK_VS_FORCE_UNITS_GRH, MBMSK_VS_FORCE_UNITS_PERCENTAGE and MBMSK_ELECTROLISIS + MBF_PAR_UICFG_MACH_NAME_BOLD_0, // 0x0608 This set of 4 registers stores an ASCIIZ string of up to 8 characters that is used to specify the bold part of the title to be displayed at startup if the specified machine type is generic. Note: only lowercase letters (a-z) can be used. + MBF_PAR_UICFG_MACH_NAME_BOLD_1, // 0x0609 + MBF_PAR_UICFG_MACH_NAME_BOLD_2, // 0x060A + MBF_PAR_UICFG_MACH_NAME_BOLD_3, // 0x060B + MBF_PAR_UICFG_MACH_NAME_LIGHT_0, // 0x060C This set of 4 registers stores an ASCIIZ string of up to 8 characters that is used to specify the normal intensity part of the title that will be displayed at startup if the specified machine type is generic. Note: Only lowercase letters (a-z) can be used. + MBF_PAR_UICFG_MACH_NAME_LIGHT_1, // 0x060D + MBF_PAR_UICFG_MACH_NAME_LIGHT_2, // 0x060E + MBF_PAR_UICFG_MACH_NAME_LIGHT_3, // 0x060F +}; + +// Sugar Valley register constants and bit masks +enum NeoPoolConstAndBitMask { + // MBF_PH_STATUS + MBMSK_PH_STATUS_ALARM = 0x000F, // PH alarm. The possible alarm values are depending on the regulation model: + // Valid alarm values for pH regulation with acid and base: + MBV_PH_ACID_BASE_ALARM0 = 0, // no alarm + MBV_PH_ACID_BASE_ALARM1 = 1, // pH too high; the pH value is 0.8 points higher than the setpoint value set in PH1 + MBV_PH_ACID_BASE_ALARM2 = 2, // pH too low: the pH value is 0.8 points lower than the set point value set in PH2 + MBV_PH_ACID_BASE_ALARM3 = 3, // pH pump (acid or base, it does not matter) has exceeded the working time set by the MBF_PAR_RELAY_PH_MAX_TIME parameter and has stopped. + MBV_PH_ACID_BASE_ALARM4 = 4, // pH higher than the set point indicated in PH1 + MBV_PH_ACID_BASE_ALARM5 = 5, // pH lower than the set point indicated in PH2 + // Valid alarm values for pH regulation with acid only: + MBV_PH_ACID_ALARM0 = 0, // no alarm + MBV_PH_ACID_ALARM1 = 1, // pH too high; the pH value is 0.8 points higher than the setpoint value set in PH1 + MBV_PH_ACID_ALARM2 = 2, // pH too low: the pH value is 0.8 points lower than the setpoint value set in PH1 + MBV_PH_ACID_ALARM3 = 3, // pH pump acid has exceeded the working time set by the MBF_PAR_RELAY_PH_MAX_TIME parameter and has stopped. + MBV_PH_ACID_ALARM4 = 4, // pH higher than the setpoint indicated in PH1 by 0.1 + MBV_PH_ACID_ALARM5 = 5, // pH lower than the set point indicated in PH1 by 0.3 + // Valid alarm values for pH regulation with base only: + MBV_PH_BASE_ALARM0 = 0, // no alarm + MBV_PH_BASE_ALARM1 = 1, // pH too high; the pH value is 0.8 points higher than the set point value set in PH2 + MBV_PH_BASE_ALARM2 = 2, // pH too low: the pH value is 0.8 points lower than the set point value set in PH2 + MBV_PH_BASE_ALARM3 = 3, // pH pump has exceeded the working time set by the MBF_PAR_RELAY_PH_MAX_TIME parameter and has stopped. + MBV_PH_BASE_ALARM4 = 4, // pH higher than the set point indicated in PH2 by 0.1 + MBV_PH_BASE_ALARM5 = 5, // pH lower than the set point indicated in PH2 by 0.3 + + MBMSK_PH_STATUS_CTRL_BY_FL = 0x0400, // Control status of the pH module by flow detection (if enabled by MBF_PAR_HIDRO_ION_CAUDAL) + MBMSK_PH_STATUS_ACID_PUMP_ACTIVE = 0x0800, // Acid pH pump relay on (pump on) + MBMSK_PH_STATUS_BASE_PUMP_ACTIVE = 0x1000, // Base pH Pump Relay On (Pump On) + MBMSK_PH_STATUS_CTRL_ACTIVE = 0x2000, // Active pH control module and controlling pumps + MBMSK_PH_STATUS_MEASURE_ACTIVE = 0x4000, // Active pH measurement module and making measurements. If this bit is at 1, the pH bar should be displayed. + MBMSK_PH_STATUS_MODULE_PRESENT = 0x8000, // Detected pH measurement module + + // MBF_RX_STATUS + MBMSK_RX_STATUS_RX_PUMP_ACTIVE = 0x1000, // Redox pump relay on (pump activated) + MBMSK_RX_STATUS_CTRL_ACTIVE = 0x2000, // Active Redox control module and controlling pump + MBMSK_RX_STATUS_MEASURE_ACTIVE = 0x4000, // Active Redox measurement module and performing measurements. If this bit is at 1, the Redox bar should be displayed on the screen. + MBMSK_RX_STATUS_MODULE_PRESENT = 0x8000, // Redox measurement module detected in the system + + // MBF_CL_STATUS + MBMSK_CL_STATUS_CHLORINE_FLOW = 0x0008, // Chlorine Probe Flow Sensor. This sensor is built into the probe itself and serves to detect whether there is water passing through the chlorine measurement probe. In case the sensor is at 0, the chlorine measurement will not be valid. + MBMSK_CL_STATUS_CL_PUMP_ACTIVE = 0x1000, // Chlorine pump relay on (pump on) + MBMSK_CL_STATUS_CTRL_ACTIVE = 0x2000, // Active chlorine control module and controlling pump + MBMSK_CL_STATUS_MEASURE_ACTIVE = 0x4000, // Active chlorine measurement module and taking measurements. If this bit is 1, the chlorine bar should be displayed on the screen. + MBMSK_CL_STATUS_MODULE_PRESENT = 0x8000, // Chlorine measurement module detected in the system + + // MBF_CD_STATUS + MBMSK_CD_STATUS_RX_PUMP_ACTIVE = 0x1000, // Conductivity pump relay on (pump active) + MBMSK_CD_STATUS_CTRL_ACTIVE = 0x2000, // Active conductivity control module and controlling pump + MBMSK_CD_STATUS_MEASURE_ACTIVE = 0x4000, // Active conductivity measurement module and making measurements. If this bit is 1, the conditionality bar should be displayed on the screen. + MBMSK_CD_STATUS_MODULE_PRESENT = 0x8000, // Conductivity measurement module detected in the system + + // MBF_ION_STATUS + MBMSK_ION_STATUS_ON_TARGET = 0x0001, // On Target - the system has reached the set point. + MBMSK_ION_STATUS_LOW = 0x0002, // Low - Ionization cannot reach the set point. + MBMSK_ION_STATUS_RESERVED = 0x0004, + MBMSK_ION_STATUS_PROGTIME_EXCEEDED = 0x0008, // Pr off - The programmed ionization time has been exceeded + MBMSK_ION_STATUS_POLOFF = 0x1000, // Ion Pol off - Ionization in dead time + MBMSK_ION_STATUS_POL1 = 0x2000, // Ion Pol 1 - Ionization working in polarization 1 + MBMSK_ION_STATUS_POL2 = 0x4000, // Ion Pol 2 - Ionization working in polarization 2 + + // MBF_HIDRO_STATUS + MBMSK_HIDRO_STATUS_ON_TARGET = 0x0001, // On Target - the system has reached the set point. + MBMSK_HIDRO_STATUS_LOW = 0x0002, // Low - Hydrolysis cannot reach the set point. + MBMSK_HIDRO_STATUS_RESERVED = 0x0004, + MBMSK_HIDRO_STATUS_FL1 = 0x0008, // Flow - Hydrolysis cell flow indicator (FL1) + MBMSK_HIDRO_STATUS_COVER = 0x0010, // Cover - Cover input activated + MBMSK_HIDRO_STATUS_MODULE_ACTIVE = 0x0020, // Active - Active Module hydrolysis (hidroEnable) + MBMSK_HIDRO_STATUS_CTRL_ACTIVE = 0x0040, // Control - Hydrolysis module working with regulation (hydroControlEnable) + MBMSK_HIDRO_STATUS_REDOX_ENABLED = 0x0080, // Redox enable - Activation of hydrolysis by the redox module + MBMSK_HIDRO_STATUS_SHOCK_ENABLED = 0x0100, // Hydro shock enabled - Chlorine shock mode enabled + MBMSK_HIDRO_STATUS_FL2 = 0x0200, // FL2 - Chlorine probe flow indicator, if present + MBMSK_HIDRO_STATUS_ENABLED_BY_CHLORINE = 0x0400, // Cl enable - Activation of hydrolysis by the chlorine module + MBMSK_HIDRO_STATUS_POLOFF = 0x1000, // Ion Pol off - Ionization in dead time + MBMSK_HIDRO_STATUS_POL1 = 0x2000, // Ion Pol 1 - Ionization working in polarization 1 + MBMSK_HIDRO_STATUS_POL2 = 0x4000, // Ion Pol 2 - Ionization working in polarization 2 + + // MBF_RELAY_STATE + MBMSK_RELAY_STATE1 = 0x0001, // Relay 1 state (1 on; 0 off) (normally assigned to ph) + MBMSK_RELAY_STATE2 = 0x0002, // Relay 2 state (1 on; 0 off) (normally assigned to filtering) + MBMSK_RELAY_STATE3 = 0x0004, // Relay 3 status (1 on; 0 off) (normally assigned to lighting) + MBMSK_RELAY_STATE4 = 0x0008, // Relay 4 status (1 on; 0 off) + MBMSK_RELAY_STATE5 = 0x0010, // Relay 5 status (1 on; 0 off) + MBMSK_RELAY_STATE6 = 0x0020, // Relay 6 status (1 on; 0 off) + MBMSK_RELAY_STATE7 = 0x0040, // Relay 7 status (1 on; 0 off) + MBMSK_RELAY_FILTSPEED_LOW = 0x0100, // Filtration low speed + MBMSK_RELAY_FILTSPEED_MID = 0x0200, // Filtration mid speed + MBMSK_RELAY_FILTSPEED_HIGH = 0x0400, // Filtration high speed + + // MBF_NOTIFICATION + MBMSK_NOTIF_MODBUS_CHANGED = 0x0001, + MBMSK_NOTIF_GLOBAL_CHANGED = 0x0002, + MBMSK_NOTIF_FACTORY_CHANGED = 0x0004, + MBMSK_NOTIF_INSTALLER_CHANGED = 0x0008, + MBMSK_NOTIF_USER_CHANGED = 0x0010, + MBMSK_NOTIF_MISC_CHANGED = 0x0020, + + // MBF_PAR_MODEL + MBMSK_MODEL_ION = 0x0001, // The equipment includes ionization control + MBMSK_MODEL_HIDRO = 0x0002, // The equipment includes hydrolysis or electrolysis + MBMSK_MODEL_UV = 0x0004, // The equipment includes disinfection control by ultraviolet lamp + MBMSK_MODEL_SALINITY = 0x0008, // The equipment includes measurement of salinity (Fanless equipment only) + + // MBF_PAR_HIDRO_FLOW_SIGNAL + MBV_PAR_HIDRO_FLOW_SIGNAL_STD = 0, // Standard detection based on conduction between an auxiliary electrode and either of the two electrodes of the cell. + MBV_PAR_HIDRO_FLOW_SIGNAL_ALWAYS_ON = 1, // Always connected. This value allows forcing the generation of the hydrolysis current even if no flow is detected in the sensor. + MBV_PAR_HIDRO_FLOW_SIGNAL_PADDLE = 2, // Detection based on the paddle switch, associated with the FL1 input + MBV_PAR_HIDRO_FLOW_SIGNAL_PADDLE_AND_STD= 3, // Detection based on the paddle switch, associated with the FL1 input, and the standard detector. The system will understand that there is flow when both elements detect flow. If either one opens, the hydrolysis will stop. + MBV_PAR_HIDRO_FLOW_SIGNAL_PADDLE_OR_STD = 4, // Detection based on the paddle switch, associated with the FL1 input, or the standard detector. The system will understand that there is flow when either of the two elements detects flow. Hydrolysis will stop only if both detectors detect no flow. + + // MBF_PAR_HIDRO_ION_CAUDAL + MBMSK_HIDRO_ION_CAUDAL_FL1_CTRL = 0x0001, // If the FL1 signal is detected to be inactive, the actuation of the different elements of the system is disabled. + MBMSK_HIDRO_ION_CAUDAL_FL2_CTRL = 0x0002, // If the FL2 signal is detected to be inactive, the actuation of the different elements of the system is disabled. + MBMSK_HIDRO_ION_CAUDAL_FULL_CL_HIDRO_CTRL=0x0004, // If there is a chlorine module installed and it is detected that its flow sensor is inactive, the action of the different elements of the system is disabled. + MBMSK_HIDRO_ION_CAUDAL_SLAVE = 0x0008, // The value of the slave input is taken and if it is inactive, the action of the different elements of the system is disabled. + MBMSK_HIDRO_ION_CAUDAL_PADDLE_SWITCH = 0x0010, + MBMSK_HIDRO_ION_CAUDAL_PADDLE_SWITCH_INV= 0x0020, + MBMSK_HIDRO_ION_CAUDAL_INVERSION = 0x0080, // This bit determines if active means open or closed for the input electrical signals, and allows to reverse the operation for example to implement a paddle switch that closes when there is no flow. + + // MBF_PAR_FILT_MODE + MBV_PAR_FILT_MANUAL = 0, // This mode allows to turn the filtration (and all other systems that depend on it) on and off manually. + MBV_PAR_FILT_AUTO = 1, // This mode allows filtering to be turned on and off according to the settings of the TIMER1, TIMER2 and TIMER3 timers. + MBV_PAR_FILT_HEADING = 2, // This mode is similar to the AUTO mode, but includes setting the temperature for the heating function. This mode is activated only if the MBF_PAR_HEATING_MODE register is at 1 and there is a heating relay assigned. + MBV_PAR_FILT_SMART = 3, // This filtration mode adjusts the pump operating times depending on the temperature. This mode is activated only if the MBF_PAR_TEMPERATURE_ACTIVE register is at 1. + MBV_PAR_FILT_INTELLIGENT = 4, // This mode performs an intelligent filtration process in combination with the heating function. This mode is activated only if the MBF_PAR_HEATING_MODE register is at 1 and there is a heating relay assigned. + MBV_PAR_FILT_BACKWASH = 13, // This filter mode is started when the backwash operation is activated. + + // MBF_PAR_UV_MODE + MBV_PAR_UV_MODE0 = 0, // UV is switched off and it will not turn on when filtration starts + MBV_PAR_UV_MODE1 = 1, // UV is switched on and it will turn on when filtration starts. Time counter for the UV lamp will be incremented. + + // MBF_PAR_UV_HIDE_WARN + MBMSK_UV_HIDE_WARN_CLEAN = 0x0001, + MBMSK_UV_HIDE_WARN_REPLACE = 0x0002, + + // MBF_PAR_PH_PUMP_REP_TIME_ON + MBMSK_PH_PUMP_TIME = 0x7FFF, // Time level for the pump: The time level has a special coding format. It can cover periods of 1 to 180 seconds with 1 second granularity and from 3 to 999 minutes with 1 minute granularity. f the value is set to 30 for example, a 30 second time will be considered. If we have the value 200, we will have an on time of (200-180+3) = 23 minutes. + MBMSK_PH_PUMP_REPETITIVE = 0x8000, // pH pump in repetitive mode (1) + + // MBF_PAR_HIDRO_COVER_ENABLE + MBMSK_HIDRO_COVER_ENABLE = 0x0001, // If enabled, the hydrolysis/electrolysis production will be reduced by a given percentage specified in the lower half of the MBF_PAR_HIDRO_COVER_REDUCTION register when the cover input is detected. + MBMSK_HIDRO_TEMPERATURE_SHUTDOWN_ENABLE = 0x0002, // If enabled, the hydrolysis/electrolysis production will stop when the temperature falls below a given temperature threshold specified in higher part of the MBF_PAR_HIDRO_COVER_REDUCTION register. + // MBF_PAR_HIDRO_COVER_REDUCTION + MBMSK_HIDRO_COVER_REDUCTION = 0x00FF, // Percentage for the cover reduction + MBMSK_HIDRO_SHUTDOWN_TEMPERATURE = 0xFF00, // Temperature level for the hydrolysis shutdown + + // MBF_PAR_RELAY_PH + MBV_PAR_RELAY_PH_ACID_AND_BASE = 0, // The equipment works with an acid and base pump + MBV_PAR_RELAY_PH_ACID_ONLY = 1, // The equipment works with acid pump only + MBV_PAR_RELAY_PH_BASE_ONLY = 2, // The equipment works with base pump only + + //MBF_PAR_RELAY_MODE + MBMSK_PAR_RELAY_MODE_PH = 0x0003, // Behavior for the pH module (MBV_PAR_RELAY_MODE_*) + MBMSK_PAR_RELAY_MODE_RX = 0x000C, // Behavior for the Redox module (MBV_PAR_RELAY_MODE_*) + MBMSK_PAR_RELAY_MODE_CL = 0x0030, // Behavior for the Chlorine module (MBV_PAR_RELAY_MODE_*) + MBMSK_PAR_RELAY_MODE_CD = 0x00C0, // Behavior for the Conductivity module (MBV_PAR_RELAY_MODE_*) + MBMSK_PAR_RELAY_MODE_TURBIDITY = 0x0300, // Behavior for the Turbidity module (MBV_PAR_RELAY_MODE_*) + MBV_PAR_RELAY_MODE_IGNORE = 0, // The system simply ignores the alarm and dosing continues. + MBV_PAR_RELAY_MODE_SHOW_ONLY = 1, // The system only shows the alarm on screen, but the dosing continues. + MBV_PAR_RELAY_MODE_SHOW_AND_STOP = 2, // The system shows the alarm on screen and stops the dosing pump + + // MBF_PAR_FUNCTION_DEPENDENCY + MBMSK_FCTDEP_HEATING = 0x0007, // Heating function dependency: + MBMSK_DEPENDENCY_FL1_PADDLE = 0x0001, + MBMSK_DEPENDENCY_FL2 = 0x0002, + MBMSK_DEPENDENCY_SLAVE = 0x0004, + + // MBF_PAR_UICFG_MACHINE + MBV_PAR_MACH_NONE = 0, // No machine assigned + MBV_PAR_MACH_HIDROLIFE = 1, // Hidrolife (yellow) + MBV_PAR_MACH_AQUASCENIC = 2, // Aquascenic (blue) + MBV_PAR_MACH_OXILIFE = 3, // Oxilife (green) + MBV_PAR_MACH_BIONET = 4, // Bionet (light blue) + MBV_PAR_MACH_HIDRONISER = 5, // Hidroniser (red) + MBV_PAR_MACH_UVSCENIC = 6, // UVScenic (lilac) + MBV_PAR_MACH_STATION = 7, // Station (orange) + MBV_PAR_MACH_BRILIX = 8, // Brilix + MBV_PAR_MACH_GENERIC = 9, // Generic + MBV_PAR_MACH_BAYROL = 10, // Bayrol + MBV_PAR_MACH_HAY = 11, // Hay + + // MBF_PAR_UICFG_LANGUAGE + MBV_PAR_LANG_SPANISH = 0, + MBV_PAR_LANG_ENGLISH = 1, + MBV_PAR_LANG_FRENCH = 2, + MBV_PAR_LANG_GERMAN = 3, + MBV_PAR_LANG_ITALIAN = 4, + MBV_PAR_LANG_TURKISH = 5, + MBV_PAR_LANG_CZECH = 6, + MBV_PAR_LANG_PORTUGUESE = 7, + MBV_PAR_LANG_DUTCH = 8, + MBV_PAR_LANG_POLISH = 9, + MBV_PAR_LANG_HUNGARIAN = 10, + MBV_PAR_LANG_RUSSIAN = 11, + + // MBF_PAR_UICFG_BACKLIGHT + MBV_PAR_BACKLIGHT_15SEC = 0, // Backlight off after 15 sec + MBV_PAR_BACKLIGHT_30SEC = 1, // Backlight off after 30 sec + MBV_PAR_BACKLIGHT_60SEC = 2, // Backlight off after 60 sec + MBV_PAR_BACKLIGHT_5MIN = 3, // Backlight off after 5 min + MBV_PAR_BACKLIGHT_ON = 4, // Backlight never turns off + + // MBF_PAR_TIMER_BLOCK_BASE + MBV_TIMER_OFFMB_TIMER_ENABLE = 0, // Enables the timer function in the selected working mode, see MBF_PAR_CTIMER_* + MBV_TIMER_OFFMB_TIMER_ON = 1, // Timer start (32-bit timestamp, LSB first) + MBV_TIMER_OFFMB_TIMER_OFF = 3, // Timer stop (32-bit timestamp, LSB first) - not used + MBV_TIMER_OFFMB_TIMER_PERIOD = 5, // Time in seconds between starting points (32-bit, LSB first), e.g. 86400 means daily + MBV_TIMER_OFFMB_TIMER_INTERVAL = 7, // Time in seconds that the timer has to run when started (32-bit, LSB first) + MBV_TIMER_OFFMB_TIMER_COUNTDOWN = 9, // Time remaining in seconds for the countdown mode (32-bit, LSB first) + MBV_TIMER_OFFMB_TIMER_FUNCTION = 11, // Function assigned to this timer, see + MBV_TIMER_OFFMB_TIMER_WORK_TIME = 13, // Number of seconds that the timer has been operating + // MBV_TIMER_OFFMB_TIMER_ENABLE working modes: + MBV_PAR_CTIMER_DISABLE = 0, // Timer disabled + MBV_PAR_CTIMER_ENABLED = 1, // Timer enabled and independent + MBV_PAR_CTIMER_ENABLED_LINKED = 2, // Timer enabled and linked to relay from timer 0 + MBV_PAR_CTIMER_ALWAYS_ON = 3, // Relay assigned to this timer always on + MBV_PAR_CTIMER_ALWAYS_OFF = 4, // Relay assigned to this timer always off + MBV_PAR_CTIMER_COUNTDOWN = 5, // Timer in countdown mode + // MBV_TIMER_OFFMB_TIMER_FUNCTION codes: + MBV_PAR_CTIMER_FCT_FILTRATION = 0x0001, // Filtration function of the system + MBV_PAR_CTIMER_FCT_LIGHTING = 0x0002, // Lighting function of the system + MBV_PAR_CTIMER_FCT_HEATING = 0x0004, // Heating function of the system + MBV_PAR_CTIMER_FCT_AUXREL1 = 0x0100, // Auxiliary function assigned to relay 1 + MBV_PAR_CTIMER_FCT_AUXREL2 = 0x0200, // Auxiliary function assigned to relay 2 + MBV_PAR_CTIMER_FCT_AUXREL3 = 0x0400, // Auxiliary function assigned to relay 3 + MBV_PAR_CTIMER_FCT_AUXREL4 = 0x0800, // Auxiliary function assigned to relay 4 + MBV_PAR_CTIMER_FCT_AUXREL5 = 0x1000, // Auxiliary function assigned to relay 5 + MBV_PAR_CTIMER_FCT_AUXREL6 = 0x2000, // Auxiliary function assigned to relay 6 + MBV_PAR_CTIMER_FCT_AUXREL7 = 0x4000, // Auxiliary function assigned to relay 7 + + // MBF_PAR_UICFG_SOUND + MBMSK_PAR_SOUND_CLICK = 0x0001, // Click sounds every time a key is pressed + MBMSK_PAR_SOUND_POPUP = 0x0002, // Sound plays each time a pop-up message appears + MBMSK_PAR_SOUND_ALERTS = 0x0004, // An alarm sounds when there is an alert on the equipment (AL3) + MBMSK_PAR_SOUND_FILTRATION = 0x0008, // Audible warning every time the filtration is started + + // MBF_PAR_UICFG_VISUAL_OPTIONS + MBMSK_HIDE_TEMPERATURE = 0x0001, // Hide temperature measurement from main menu + MBMSK_HIDE_FILTRATION = 0x0002, // Hide filter status from main menu + MBMSK_HIDE_LIGHTING = 0x0004, // Hide lighting status from main menu + MBMSK_HIDE_AUX_RELAYS = 0x0008, // Hide auxiliary relay status from main menu. + MBMSK_VO_HIDE_EXTRA_REGS = 0x0010, // Hide the option to adjust additional registers in the installer menu + MBMSK_VO_HIDE_RELAY_CONFIG = 0x0020, // Hide the relay configuration option in the installer menu. + MBMSK_VO_SLOW_FILTER_HIDRO_LEVEL = 0x0040, // This option enables the slow hydrolysis level filtering option when the pH module is installed. This is especially important when the acid / base dosing is done very close to the hydrolysis probe. + MBMSK_VO_HIDE_SALINITY_MAIN_WINDOW = 0x0080, // Hides the salinity measurement from main screen. + MBMSK_VO_SHOW_SPECIAL_REGS = 0x0100, // Displays the special register set configuration menu in the installer menu. + MBMSK_SHOW_HID_SHUTDOWN_BY_TEMPERATURE = 0x0200, // Displays the option to turn off hydrolysis by temperature. + MBMSK_SHOW_CELL_SELECTION = 0x0400, // Enables access to the cell selection menu from the service menu option of the configuration menu. + MBMSK_SHOW_PUMP_TYPE = 0x0800, // Displays the option for selecting the type of filtration pump (normal, three speeds, etc.). + MBMSK_SHOW_QUICK_MENU = 0x1000, // Displays the quick access menu instead of the conventional menu, when the SET key is pressed from the main display screen. Filtration (normal, three speeds, etc). + MBMSK_SHOW_OXI_MAIN_DATA_SCREEN = 0x2000, // Displays main screen shown with a particular style called OXI + MBMSK_SHOW_INSTALLER_MENU = 0x4000, // Shows access to the installer menu in the main menu without the need for a password. + MBMSK_SHOW_FACTORY_MENU = 0x8000, // Shows access to the factory menu in the main menu without the need for a password. + + // MBF_PAR_UICFG_VISUAL_OPTIONS_EXT + MBMSK_VOE_SHOW_PNEUMATIC_VALVE = 0x0001, // Shows the pneumatic valve + MBMSK_VOE_HIDE_AUX_REL_DEPENDENCY = 0x0002, // Hides the auxiliary relay dependency + MBMSK_VOE_SHOW_BESGO_NAME = 0x0004, // Show “Besgo” instead of “Pneumatic” for the pneumatic valve titles. + + // MBF_PAR_UICFG_MACH_VISUAL_STYLE + MBMSK_VS_FORCE_UNITS_GRH = 0x2000, // Display the hydrolysis/electrolysis in units of grams per hour (gr/h). + MBMSK_VS_FORCE_UNITS_PERCENTAGE = 0x4000, // Display the hydrolysis/electrolysis in percentage units (%). + MBMSK_ELECTROLISIS = 0x8000, // Display the word electrolysis instead of hydrolysis in generic mode. +}; + +#include +TasmotaModbus *NeoPoolModbus; + +bool neopool_active = false; +volatile bool neopool_poll = true; + +uint8_t neopool_read_state = 0; +uint8_t neopool_send_retry = 0; +uint8_t neopool_failed = 0; +uint8_t neopool_failed_count = 0; +#ifdef NEOPOOL_OPTIMIZE_READINGS +bool neopool_first_read = true; +#endif // NEOPOOL_OPTIMIZE_READINGS +bool neopool_error = true; + +// Modbus register set to read +struct NEOPOOL_REG { + const uint16_t addr; + const uint16_t cnt; + uint16_t *data; +} NeoPoolReg[] = { + {MBF_ION_CURRENT, MBF_NOTIFICATION - MBF_ION_CURRENT + 1, nullptr}, + {MBF_PAR_VERSION, MBF_PAR_MODEL - MBF_PAR_VERSION + 1, nullptr}, + {MBF_PAR_TIME_LOW, MBF_PAR_FILT_GPIO - MBF_PAR_TIME_LOW + 1, nullptr}, + {MBF_PAR_UICFG_MACHINE, MBF_PAR_UICFG_MACHINE - MBF_PAR_UICFG_MACHINE + 1, nullptr} +}; + +// NeoPool modbus function errors +#define NEOPOOL_OK 0 +#define NEOPOOL_ERROR_RW_DATA 1 +#define NEOPOOL_ERROR_TIMEOUT 2 +#define NEOPOOL_ERROR_OUT_OF_MEM 3 +#define NEOPOOL_ERROR_DEADLOCK 4 + + +#define D_NEOPOOL_NAME "NeoPool" + +// Machine names +#define D_NEOPOOL_MACH_NONE D_NEOPOOL_NAME +#define D_NEOPOOL_MACH_HIDROLIFE "Hidrolife (yellow)" +#define D_NEOPOOL_MACH_AQUASCENIC "Aquascenic (blue)" +#define D_NEOPOOL_MACH_OXILIFE "Oxilife (green)" +#define D_NEOPOOL_MACH_BIONET "Bionet (light blue)" +#define D_NEOPOOL_MACH_HIDRONISER "Hidroniser (red)" +#define D_NEOPOOL_MACH_UVSCENIC "UVScenic (lilac)" +#define D_NEOPOOL_MACH_STATION "Station (orange)" +#define D_NEOPOOL_MACH_BRILIX "Brilix" +#define D_NEOPOOL_MACH_GENERIC "Generic" +#define D_NEOPOOL_MACH_BAYROL "Bayrol" +#define D_NEOPOOL_MACH_HAY "Hay" + +const char kNeoPoolMachineNames[] PROGMEM = + D_NEOPOOL_MACH_NONE "|" + D_NEOPOOL_MACH_HIDROLIFE "|" + D_NEOPOOL_MACH_AQUASCENIC "|" + D_NEOPOOL_MACH_OXILIFE "|" + D_NEOPOOL_MACH_BIONET "|" + D_NEOPOOL_MACH_HIDRONISER "|" + D_NEOPOOL_MACH_UVSCENIC "|" + D_NEOPOOL_MACH_STATION "|" + D_NEOPOOL_MACH_BRILIX "|" + D_NEOPOOL_MACH_GENERIC "|" + D_NEOPOOL_MACH_BAYROL "|" + D_NEOPOOL_MACH_HAY + ; + +// Filtration modes +#define D_NEOPOOL_FILTRATION_MANUAL "Manual" +#define D_NEOPOOL_FILTRATION_AUTO "Auto" +#define D_NEOPOOL_FILTRATION_HEATING "Heating" +#define D_NEOPOOL_FILTRATION_SMART "Smart" +#define D_NEOPOOL_FILTRATION_INTELLIGENT "Intelligent" +#define D_NEOPOOL_FILTRATION_BACKWASH "Backwash" +const char kNeoPoolFiltrationMode[] PROGMEM = + D_NEOPOOL_FILTRATION_MANUAL "|" + D_NEOPOOL_FILTRATION_AUTO "|" + D_NEOPOOL_FILTRATION_HEATING "|" + D_NEOPOOL_FILTRATION_SMART "|" + D_NEOPOOL_FILTRATION_INTELLIGENT "|" + D_NEOPOOL_FILTRATION_BACKWASH + ; + +// Filtration speed level +#define D_NEOPOOL_FILTRATION_NONE "" +#define D_NEOPOOL_FILTRATION_SLOW "slow" +#define D_NEOPOOL_FILTRATION_MEDIUM "medium" +#define D_NEOPOOL_FILTRATION_FAST "fast" +const char kNeoPoolFiltrationSpeed[] PROGMEM = + D_NEOPOOL_FILTRATION_NONE "|" + D_NEOPOOL_FILTRATION_SLOW "|" + D_NEOPOOL_FILTRATION_MEDIUM "|" + D_NEOPOOL_FILTRATION_FAST + ; + +// Sensor & relais names +#define D_NEOPOOL_TYPE "Type" +#define D_NEOPOOL_REDOX "Redox" +#define D_NEOPOOL_CHLORINE "Chlorine" +#define D_NEOPOOL_CONDUCTIVITY "Conductivity" +#define D_NEOPOOL_IONIZATION "Ionization" +#define D_NEOPOOL_HYDROLYSIS "Hydrolysis" +#define D_NEOPOOL_RELAY "Relay" +#define D_NEOPOOL_RELAY_FILTRATION "Filtration" +#define D_NEOPOOL_RELAY_LIGHT "Light" +#define D_NEOPOOL_RELAY_PH_ACID "Acid pump" +#define D_NEOPOOL_RELAY_PH_BASE "Base pump" +#define D_NEOPOOL_RELAY_RX "Redox level" +#define D_NEOPOOL_RELAY_CL "Chlorine pump" +#define D_NEOPOOL_RELAY_CD "Brine pump" +#define D_NEOPOOL_TIME "Time" +#define D_NEOPOOL_FILT_MODE "Filtration" + +// Sensor status +#define D_NEOPOOL_POLARIZATION "Pol" +#define D_NEOPOOL_PR_OFF "PrOff" +#define D_NEOPOOL_SETPOINT_OK "Ok" +#define D_NEOPOOL_COVER "Cover" +#define D_NEOPOOL_SHOCK "Shock" +#define D_NEOPOOL_ALARM "! " +#define D_NEOPOOL_LOW "Low" +#define D_NEOPOOL_FLOW1 "FL1" +#define D_NEOPOOL_FLOW2 "FL2" + +#define D_NEOPOOL_PH_HIGH "too high" +#define D_NEOPOOL_PH_LOW "too low" +#define D_NEOPOOL_PUMP_TIME_EXCEEDED "pump time exceeded" +const char kNeoPoolpHAlarms[] PROGMEM = + D_NEOPOOL_SETPOINT_OK "|" + D_NEOPOOL_PH_HIGH "|" + D_NEOPOOL_PH_LOW "|" + D_NEOPOOL_PUMP_TIME_EXCEEDED + ; + +#define D_STR_BIT "Bit" + +const char HTTP_SNS_NEOPOOL_PH[] PROGMEM = "{s}%s " D_PH "{m}%s " "%s%s" "{e}"; +const char HTTP_SNS_NEOPOOL_TIME[] PROGMEM = "{s}%s " D_NEOPOOL_TIME "{m}%s" "{e}"; +const char HTTP_SNS_NEOPOOL_PPM_REDOX[] PROGMEM = "{s}%s " D_NEOPOOL_REDOX "{m}%s " D_UNIT_PARTS_PER_MILLION "{e}"; +const char HTTP_SNS_NEOPOOL_PPM_CHLORINE[] PROGMEM = "{s}%s " D_NEOPOOL_CHLORINE "{m}%s " D_UNIT_PARTS_PER_MILLION "{e}"; +const char HTTP_SNS_NEOPOOL_CONDUCTIVITY[] PROGMEM = "{s}%s " D_NEOPOOL_CONDUCTIVITY "{m}%s " D_UNIT_PERCENT "{e}"; +const char HTTP_SNS_NEOPOOL_IONIZATION[] PROGMEM = "{s}%s " D_NEOPOOL_IONIZATION "{m}%s " "%s%s" "{e}"; +const char HTTP_SNS_NEOPOOL_HYDROLYSIS[] PROGMEM = "{s}%s " D_NEOPOOL_HYDROLYSIS "{m}%s " "%s%s" "{e}"; +const char HTTP_SNS_NEOPOOL_FILT_MODE[] PROGMEM = "{s}%s " D_NEOPOOL_FILT_MODE "{m}%s" "{e}"; +const char HTTP_SNS_NEOPOOL_RELAY[] PROGMEM = "{s}%s " D_NEOPOOL_RELAY " %d %s" "{m}%s" "{e}"; + + +void NeoPool250ms(void) // Every 250 mSec +{ + if (!neopool_poll) { + return; + }; + + bool data_ready = NeoPoolModbus->ReceiveReady(); + + if (data_ready && nullptr != NeoPoolReg[neopool_read_state].data) { + uint8_t *buffer = (uint8_t *)malloc(5+(NeoPoolReg[neopool_read_state].cnt)*2); + + if( nullptr != buffer) { + uint8_t error = NeoPoolModbus->ReceiveBuffer(buffer, NeoPoolReg[neopool_read_state].cnt); // cnt x 16bit register + + if (0 == error) { + neopool_failed_count = 0; + neopool_error = false; + for(uint32_t i=0; i> 8)-1))) { +#endif // NEOPOOL_OPTIMIZE_READINGS +#ifdef DEBUG_TASMOTA_SENSOR + AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("NEO: modbus send(%d, %d, 0x%04X, %d)"), NEOPOOL_MODBUS_ADDRESS, NEOPOOL_READ_REGISTER, NeoPoolReg[neopool_read_state].addr, NeoPoolReg[neopool_read_state].cnt); +#endif // DEBUG_TASMOTA_SENSOR + NeoPoolModbus->Send(NEOPOOL_MODBUS_ADDRESS, NEOPOOL_READ_REGISTER, NeoPoolReg[neopool_read_state].addr, NeoPoolReg[neopool_read_state].cnt); +#ifdef NEOPOOL_OPTIMIZE_READINGS + } + else { + // search next addr block having notification + while( (NeoPoolReg[neopool_read_state].addr & 0x0F00) != 0x100 || (NeoPoolGetData(MBF_NOTIFICATION) & (1 << (NeoPoolReg[neopool_read_state].addr >> 8)-1)) ) { +#ifdef DEBUG_TASMOTA_SENSOR + AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("NEO: notify 0x%04X - addr block 0x%04X ignored"), NeoPoolGetData(MBF_NOTIFICATION), NeoPoolReg[neopool_read_state].addr); +#endif // DEBUG_TASMOTA_SENSOR + ++neopool_read_state %= ARRAY_SIZE(NeoPoolReg); + // neopool_read_state++; + // if (ARRAY_SIZE(NeoPoolReg) <= neopool_read_state) { + // neopool_read_state = 0; + // } + } + } +#endif // NEOPOOL_OPTIMIZE_READINGS + } + else { + if (1 == neopool_send_retry) { + neopool_failed_count++; + if (neopool_failed_count > 2) { + neopool_failed_count = 0; + neopool_error = true; + NeoPoolInitData(); + } + } + neopool_send_retry--; + } + } +} + +/*********************************************************************************************/ + +void NeoPoolInit(void) +{ + if (NeoPoolInitData()) { + if (PinUsed(GPIO_NEOPOOL_RX) && PinUsed(GPIO_NEOPOOL_TX)) { + NeoPoolModbus = new TasmotaModbus(Pin(GPIO_NEOPOOL_RX), Pin(GPIO_NEOPOOL_TX)); + uint8_t result = NeoPoolModbus->Begin(NEOPOOL_MODBUS_SPEED); + if (result) { + if (2 == result) { + ClaimSerial(); + } +#ifdef NEOPOOL_OPTIMIZE_READINGS + neopool_first_read = true; +#endif // NEOPOOL_OPTIMIZE_READINGS + neopool_active = true; + } + else { + neopool_active = false; + } + } + } +} + +bool NeoPoolInitData(void) +{ + bool res = false; + + neopool_error = true; + for(uint32_t i=0; iSend(NEOPOOL_MODBUS_ADDRESS, NEOPOOL_READ_REGISTER, addr, cnt); + timeoutMS = millis() + cnt * NEOPOOL_READ_TIMEOUT; // Max delay before we timeout + while (!(data_ready = NeoPoolModbus->ReceiveReady()) && millis() < timeoutMS) { delay(1); } + if (data_ready) { + uint8_t *buffer = (uint8_t*)malloc(5+cnt*2); + if (buffer != nullptr) { + uint8_t error = NeoPoolModbus->ReceiveBuffer(buffer, cnt); + if (error) { +#ifdef DEBUG_TASMOTA_SENSOR + AddLog_P(LOG_LEVEL_DEBUG, PSTR("NEO: addr 0x%04X read data error %d"), addr, error); +#endif // DEBUG_TASMOTA_SENSOR + neopool_poll = true; + return NEOPOOL_ERROR_RW_DATA; + } + for(uint32_t i=0; i> 8); // addr MSB + frame[3] = (uint8_t)(addr); // addr LSB + frame[4] = (uint8_t)(cnt >> 8); // register quantity MSB + frame[5] = (uint8_t)(cnt); // register quantity LSB + frame[6] = (uint8_t)(cnt*2); // byte count + for(uint32_t i=0; i> 8); // data MSB + frame[8+i*2] = (uint8_t)(data[i]); // data LSB + } + uint16_t crc = NeoPoolModbus->CalculateCRC(frame, numbytes); + frame[numbytes] = (uint8_t)(crc); + frame[numbytes+1] = (uint8_t)(crc >> 8); + + NeoPoolModbus->flush(); + NeoPoolModbus->write(frame, numbytes+2); + free(frame); + + timeoutMS = millis() + 1 * NEOPOOL_READ_TIMEOUT; // Max delay before we timeout + while (!(data_ready = NeoPoolModbus->ReceiveReady()) && millis() < timeoutMS) { delay(1); } + if (data_ready) { + uint8_t buffer[9]; + uint8_t error = NeoPoolModbus->ReceiveBuffer(buffer, 1); + if (0!=error && 9!=error) { // ReceiveBuffer can't handle 0x10 code result +#ifdef DEBUG_TASMOTA_SENSOR + AddLog_P(LOG_LEVEL_DEBUG, PSTR("NEO: addr 0x%04X write data response error %d"), addr, error); +#endif // DEBUG_TASMOTA_SENSOR + neopool_poll = true; + return NEOPOOL_ERROR_RW_DATA; + } + if (9==error) { + // clear buffer before we leave + while (NeoPoolModbus->available()) { + NeoPoolModbus->read(); + } + } + neopool_poll = true; + // delay(2); + return NEOPOOL_OK; + } +#ifdef DEBUG_TASMOTA_SENSOR + AddLog_P(LOG_LEVEL_DEBUG, PSTR("NEO: addr 0x%04X write data response timeout"), addr); +#endif // DEBUG_TASMOTA_SENSOR + neopool_poll = true; + return NEOPOOL_ERROR_TIMEOUT; +} + +uint16_t NeoPoolGetData(uint16_t addr) +{ + for(uint32_t i=0; i= NeoPoolReg[i].addr && addr < NeoPoolReg[i].addr+NeoPoolReg[i].cnt) { + return NeoPoolReg[i].data[addr - NeoPoolReg[i].addr]; + } + } + return 0; +} + +uint32_t NeoPoolGetSpeedIndex(uint16_t speedvalue) +{ + if (speedvalue>=4) { + return 3; + } + if (speedvalue>=2) { + return 2; + } + if (speedvalue>=1) { + return 1; + } + return 0; +} + +void NeoPoolShow(bool json) +{ + char parameter[FLOATSZ]; + char *neopool_type; + char stemp[128]; + char smachine[32]; + + if (neopool_error) { + return; + } + + neopool_type = GetTextIndexed(smachine, sizeof(smachine), NeoPoolGetData(MBF_PAR_UICFG_MACHINE), kNeoPoolMachineNames); + + if (json) { + char delimiter[] = {'\0','\0'}; + + ResponseAppend_P(PSTR(",\"" D_NEOPOOL_NAME "\":{")); + +#ifndef NEOPOOL_OPTIMIZE_READINGS + // Time + ResponseAppend_P(PSTR("%s\"" D_NEOPOOL_TIME "\":\"%s\""), delimiter, + GetDT((uint32_t)NeoPoolGetData(MBF_PAR_TIME_LOW) + ((uint32_t)NeoPoolGetData(MBF_PAR_TIME_HIGH) << 16)).c_str()); + *delimiter = ','; +#endif // NEOPOOL_OPTIMIZE_READINGS + + // TODO: Add alarm infos + + // Type + ResponseAppend_P(PSTR("%s\"" D_NEOPOOL_TYPE "\":\"%s\""), delimiter, neopool_type); + *delimiter = ','; + + // Temperature + if (NeoPoolGetData(MBF_PAR_TEMPERATURE_ACTIVE)) { + dtostrfd(Settings.flag.temperature_conversion ? + (float)NeoPoolGetData(MBF_MEASURE_TEMPERATURE)/10 * 1.8 + 32 : + (float)NeoPoolGetData(MBF_MEASURE_TEMPERATURE)/10, Settings.flag2.temperature_resolution, parameter); + ResponseAppend_P(PSTR("%s\"" D_TEMPERATURE "\":%s"), delimiter, parameter); + *delimiter = ','; + } + + // pH + if (NeoPoolGetData(MBF_PH_STATUS) & MBMSK_PH_STATUS_MEASURE_ACTIVE) { + dtostrfd((float)NeoPoolGetData(MBF_MEASURE_PH)/100, 2, parameter); + ResponseAppend_P(PSTR("%s\"" D_PH """\":%s"), delimiter, parameter); + *delimiter = ','; + } + + // Redox + if (NeoPoolGetData(MBF_RX_STATUS) & MBMSK_RX_STATUS_MEASURE_ACTIVE) { + dtostrfd((float)NeoPoolGetData(MBF_MEASURE_RX)/100, 2, parameter); + ResponseAppend_P(PSTR("%s\"" D_NEOPOOL_REDOX "\":%s"), delimiter, parameter); + *delimiter = ','; + } + + // Chlorine + if (NeoPoolGetData(MBF_CL_STATUS) & MBMSK_CL_STATUS_MEASURE_ACTIVE) { + dtostrfd((float)NeoPoolGetData(MBF_MEASURE_CL)/100, 2, parameter); + ResponseAppend_P(PSTR("%s\"" D_NEOPOOL_CHLORINE "\":%s"), delimiter, parameter); + } + + // Conductivity + if (NeoPoolGetData(MBF_CD_STATUS) & MBMSK_CD_STATUS_MEASURE_ACTIVE) { + ResponseAppend_P(PSTR("%s\"" D_NEOPOOL_CONDUCTIVITY "\":%d"), delimiter, NeoPoolGetData(MBF_MEASURE_CONDUCTIVITY)); + } + + // Ionization + if (NeoPoolGetData(MBF_PAR_MODEL) & MBMSK_MODEL_ION) { + dtostrfd((float)NeoPoolGetData(MBF_ION_CURRENT), 1, parameter); + ResponseAppend_P(PSTR("%s\"" D_NEOPOOL_IONIZATION "\":%s"), delimiter, parameter); + } + + // Hydrolysis + if ((NeoPoolGetData(MBF_PAR_MODEL) & MBMSK_MODEL_HIDRO) && (NeoPoolGetData(MBF_HIDRO_STATUS) & MBMSK_HIDRO_STATUS_MODULE_ACTIVE)) { + dtostrfd((float)NeoPoolGetData(MBF_HIDRO_CURRENT), 1, parameter); + ResponseAppend_P(PSTR("%s\"" D_NEOPOOL_HYDROLYSIS "\":%s"), delimiter, parameter); + } + + // Filtration + ResponseAppend_P(PSTR("%s\"" D_NEOPOOL_RELAY_FILTRATION "\":%d"), delimiter, (NeoPoolGetData(MBF_RELAY_STATE) >>1) & 0x02); + ResponseAppend_P(PSTR("%s\"" D_NEOPOOL_RELAY_FILTRATION D_JSON_SPEED "\":%d"), delimiter, + (NeoPoolGetData(MBF_RELAY_STATE) >> 8) & 0x7 + ); + + // Light + ResponseAppend_P(PSTR("%s\"" D_NEOPOOL_RELAY_LIGHT "\":%d"), delimiter, (NeoPoolGetData(MBF_RELAY_STATE) >>2) & 0x01); + + // Relays + ResponseAppend_P(PSTR("%s\"" D_NEOPOOL_RELAY "\":%d"), delimiter, NeoPoolGetData(MBF_RELAY_STATE)); + + ResponseJsonEnd(); + +#ifdef USE_WEBSERVER + } + else { +#ifndef NEOPOOL_OPTIMIZE_READINGS + // Time + char dt[20]; + TIME_T tmpTime; + BreakTime((uint32_t)NeoPoolGetData(MBF_PAR_TIME_LOW) + ((uint32_t)NeoPoolGetData(MBF_PAR_TIME_HIGH) << 16), tmpTime); + snprintf_P(dt, sizeof(dt), PSTR("%04d-%02d-%02d %02d:%02d"), + tmpTime.year +1970, tmpTime.month, tmpTime.day_of_month, tmpTime.hour, tmpTime.minute); + WSContentSend_PD(HTTP_SNS_NEOPOOL_TIME, neopool_type, dt); +#endif // NEOPOOL_OPTIMIZE_READINGS + + // Temperature + if (NeoPoolGetData(MBF_PAR_TEMPERATURE_ACTIVE)) { + dtostrfd(Settings.flag.temperature_conversion?(float)NeoPoolGetData(MBF_MEASURE_TEMPERATURE)/10 * 1.8 + 32:(float)NeoPoolGetData(MBF_MEASURE_TEMPERATURE)/10, Settings.flag2.temperature_resolution, parameter); + WSContentSend_PD(HTTP_SNS_TEMP, neopool_type, parameter, TempUnit()); + } + + // pH + if (NeoPoolGetData(MBF_PH_STATUS) & MBMSK_PH_STATUS_MEASURE_ACTIVE) { + dtostrfd((float)NeoPoolGetData(MBF_MEASURE_PH)/100, 2, parameter); + *stemp = 0; + if ((NeoPoolGetData(MBF_PH_STATUS) & MBMSK_PH_STATUS_ALARM) >=1 && (NeoPoolGetData(MBF_PH_STATUS) & MBMSK_PH_STATUS_ALARM) <= 3) { + GetTextIndexed(stemp, sizeof(stemp), NeoPoolGetData(MBF_PH_STATUS) & MBMSK_PH_STATUS_ALARM, kNeoPoolpHAlarms); + } + WSContentSend_PD(HTTP_SNS_NEOPOOL_PH, neopool_type, parameter, *stemp ? PSTR(" " D_NEOPOOL_ALARM) : PSTR(""), stemp); + } + + // Redox + if (NeoPoolGetData(MBF_RX_STATUS) & MBMSK_RX_STATUS_MEASURE_ACTIVE) { + dtostrfd((float)NeoPoolGetData(MBF_MEASURE_RX)/100, 2, parameter); + WSContentSend_PD(HTTP_SNS_NEOPOOL_PPM_REDOX, neopool_type, parameter); + } + + // Chlorine + if (NeoPoolGetData(MBF_CL_STATUS) & MBMSK_CL_STATUS_MEASURE_ACTIVE) { + dtostrfd((float)NeoPoolGetData(MBF_MEASURE_CL)/100, 2, parameter); + WSContentSend_PD(HTTP_SNS_NEOPOOL_PPM_CHLORINE, neopool_type, parameter); + } + + // Conductivity + if (NeoPoolGetData(MBF_CD_STATUS) & MBMSK_CD_STATUS_MEASURE_ACTIVE) { + dtostrfd((float)NeoPoolGetData(MBF_MEASURE_CONDUCTIVITY), 0, parameter); + WSContentSend_PD(HTTP_SNS_NEOPOOL_PPM_CHLORINE, neopool_type, parameter); + } + + // Ionization + if (NeoPoolGetData(MBF_PAR_MODEL) & MBMSK_MODEL_ION) { + dtostrfd((float)NeoPoolGetData(MBF_ION_CURRENT), 1, parameter); + char spol[32]; + sprintf_P(spol,PSTR(" " D_NEOPOOL_POLARIZATION "%d"),NeoPoolGetData(MBF_ION_STATUS)>>13); + sprintf_P(stemp, PSTR("%s%s%s"), + NeoPoolGetData(MBF_ION_STATUS)>>13?spol:PSTR(""), + NeoPoolGetData(MBF_ION_STATUS) & MBMSK_ION_STATUS_ON_TARGET ? PSTR(" " D_NEOPOOL_SETPOINT_OK) : PSTR(""), + NeoPoolGetData(MBF_ION_STATUS) & MBMSK_ION_STATUS_PROGTIME_EXCEEDED ? PSTR(" " D_NEOPOOL_PR_OFF) : PSTR("") + ); + WSContentSend_PD(HTTP_SNS_NEOPOOL_IONIZATION, neopool_type, parameter, NeoPoolGetData(MBF_ION_STATUS)>>13, NeoPoolGetData(MBF_ION_STATUS)&0x0002?" Low":""); + } + + // Hydrolysis +#ifdef DEBUG_TASMOTA_SENSOR + //AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("NEO: MBF_PAR_MODEL 0x%04X MBF_HIDRO_STATUS 0x%04X"), NeoPoolGetData(MBF_PAR_MODEL), NeoPoolGetData(MBF_HIDRO_STATUS)); +#endif // DEBUG_TASMOTA_SENSOR + if ((NeoPoolGetData(MBF_PAR_MODEL) & MBMSK_MODEL_HIDRO) && (NeoPoolGetData(MBF_HIDRO_STATUS) & MBMSK_HIDRO_STATUS_MODULE_ACTIVE)) { + dtostrfd((float)NeoPoolGetData(MBF_HIDRO_CURRENT), 1, parameter); + char spol[32]; + sprintf_P(spol,PSTR(" " D_NEOPOOL_POLARIZATION "%d"),NeoPoolGetData(MBF_HIDRO_STATUS)>>13); + sprintf_P(stemp, PSTR("%s%s%s%s%s%s%s%s"), + NeoPoolGetData(MBF_HIDRO_STATUS)>>13?spol:PSTR(""), + NeoPoolGetData(MBF_HIDRO_STATUS) & MBMSK_HIDRO_STATUS_ON_TARGET ? PSTR(" " D_NEOPOOL_SETPOINT_OK) : PSTR(""), + NeoPoolGetData(MBF_HIDRO_STATUS) & MBMSK_HIDRO_STATUS_COVER ? PSTR(" " D_NEOPOOL_COVER) : PSTR(""), + NeoPoolGetData(MBF_HIDRO_STATUS) & MBMSK_HIDRO_STATUS_SHOCK_ENABLED ? PSTR(" " D_NEOPOOL_SHOCK) : PSTR(""), + (NeoPoolGetData(MBF_HIDRO_STATUS) ^(MBMSK_HIDRO_STATUS_FL1|MBMSK_HIDRO_STATUS_FL2)) & (MBMSK_HIDRO_STATUS_LOW|MBMSK_HIDRO_STATUS_FL1|MBMSK_HIDRO_STATUS_FL2) ? PSTR(" " D_NEOPOOL_ALARM) : PSTR(""), + NeoPoolGetData(MBF_HIDRO_STATUS) & MBMSK_HIDRO_STATUS_LOW ? PSTR(" " D_NEOPOOL_LOW) : PSTR(""), + !(NeoPoolGetData(MBF_HIDRO_STATUS) & MBMSK_HIDRO_STATUS_FL1) ? PSTR(" " D_NEOPOOL_FLOW1) : PSTR(""), + !(NeoPoolGetData(MBF_HIDRO_STATUS) & MBMSK_HIDRO_STATUS_FL2) ? PSTR(" " D_NEOPOOL_FLOW2) : PSTR("") + ); + WSContentSend_PD(HTTP_SNS_NEOPOOL_HYDROLYSIS, neopool_type, parameter, *stemp ? PSTR("/") : PSTR(""), stemp); + } + GetTextIndexed(stemp, sizeof(stemp), NeoPoolGetData(MBF_PAR_FILT_MODE) < MBV_PAR_FILT_INTELLIGENT ? NeoPoolGetData(MBF_PAR_FILT_MODE) : 5, kNeoPoolFiltrationMode); + WSContentSend_PD(HTTP_SNS_NEOPOOL_FILT_MODE, neopool_type, stemp); + + // Relays + for(uint32_t i=0; i<8; i++) { + char sdesc[24]; + memset(sdesc, 0, ARRAY_SIZE(sdesc)); + memset(stemp, 0, ARRAY_SIZE(stemp)); + if (0 != NeoPoolGetData(MBF_PAR_PH_ACID_RELAY_GPIO) && i == NeoPoolGetData(MBF_PAR_PH_ACID_RELAY_GPIO)-1) { + strncpy_P(sdesc, PSTR(D_NEOPOOL_RELAY_PH_ACID), sizeof(sdesc)); + } + else if (0 != NeoPoolGetData(MBF_PAR_PH_BASE_RELAY_GPIO) && i == NeoPoolGetData(MBF_PAR_PH_BASE_RELAY_GPIO)-1) { + strncpy_P(sdesc, PSTR(D_NEOPOOL_RELAY_PH_BASE), sizeof(sdesc)); + } + else if (0 != NeoPoolGetData(MBF_PAR_RX_RELAY_GPIO) && i == NeoPoolGetData(MBF_PAR_RX_RELAY_GPIO)-1) { + strncpy_P(sdesc, PSTR(D_NEOPOOL_RELAY_RX), sizeof(sdesc)); + } + else if (0 != NeoPoolGetData(MBF_PAR_CL_RELAY_GPIO) && i == NeoPoolGetData(MBF_PAR_CL_RELAY_GPIO)-1) { + strncpy_P(sdesc, PSTR(D_NEOPOOL_RELAY_CL), sizeof(sdesc)); + } + else if (0 != NeoPoolGetData(MBF_PAR_CD_RELAY_GPIO) && i == NeoPoolGetData(MBF_PAR_CD_RELAY_GPIO)-1) { + strncpy_P(sdesc, PSTR(D_NEOPOOL_RELAY_CD), sizeof(sdesc)); + } + else if (0 != NeoPoolGetData(MBF_PAR_FILT_GPIO) && i == NeoPoolGetData(MBF_PAR_FILT_GPIO)-1) { + char smotorspeed[32]; + // Filtration + strncpy_P(sdesc, PSTR(D_NEOPOOL_RELAY_FILTRATION), sizeof(sdesc)); + GetTextIndexed(smotorspeed, sizeof(smotorspeed), NeoPoolGetSpeedIndex((NeoPoolGetData(MBF_RELAY_STATE) >> 8) & 0x7), kNeoPoolFiltrationSpeed); + sprintf_P(stemp, PSTR("%s%s%s%s"), ((NeoPoolGetData(MBF_RELAY_STATE) & (1< () + * read 16-bit register (cnt=1..30, cnt=1 if omitted) + * + * Sensor83 2 () + * read/write register bit (bit=0..15, data=0|1) + * read if param is omitted otherwise set to new + * + * Sensor83 3 (...) + * write 16-bit register (data=0..65535, max 8 times) + * + * Sensor83 4 () + * get/set manual filtration (state=0|1) + * get filtration state if param is omitted otherwise set new state + * + * Sensor83 5 () + * get/set filtration mode (mode=0..4|13) + * get mode if param is omitted otherwise set new mode + * + * Sensor83 6 (