diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 038f20687..fdafb0535 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,7 +21,7 @@ While fallback or downgrading is common practice it was never supported due to S ## Supported Core versions -This release will be supported from ESP8266/Arduino library Core version **2.7.1** due to reported security and stability issues on previous Core version. This will also support gzipped binaries. +This release will be supported from ESP8266/Arduino library Core version **2.7.2** due to reported security and stability issues on previous Core version. This will also support gzipped binaries. Although it might still compile on previous Core versions all support will be removed in the near future. @@ -35,7 +35,7 @@ For initial configuration this release supports Webserver based **WifiManager** ## Provided Binary Downloads -The following binary downloads have been compiled with ESP8266/Arduino library core version **2.7.1**. +The following binary downloads have been compiled with ESP8266/Arduino library core version **2.7.2**. - **tasmota.bin** = The Tasmota version with most drivers. **RECOMMENDED RELEASE BINARY** - **tasmota-BG.bin** to **tasmota-TW.bin** = The Tasmota version in different languages. @@ -52,7 +52,7 @@ The following binary downloads have been compiled with ESP8266/Arduino library c ## Changelog -### Version 8.4.0 George +### Version 8.3.1.6 - Change IRremoteESP8266 library updated to v2.7.7 - Change Adafruit_SGP30 library from v1.0.3 to v1.2.0 (#8519) @@ -94,4 +94,4 @@ The following binary downloads have been compiled with ESP8266/Arduino library c - Add compile time interlock parameters (#8759) - Add compile time user template (#8766) - Add rotary encoder support for light dimmer and optional color temperature if button1 still pressed (#8670) -- Add support for switches/relays using an AC detection circuitry e.g. MOES MS-104B / BlitzWolf SS5 / etc. (#8606) +- Add support for switches/relays using an AC detection circuitry e.g. MOES MS-104B or BlitzWolf SS5 (#8606) diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index 0d98612e1..a459c2aac 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -1,9 +1,3 @@ -## Released - -### 8.4.0 20200716 - -- Release George - ## Unreleased (development) ### 8.3.1.6 20200617 @@ -17,6 +11,7 @@ - Add compile time interlock parameters (#8759) - Add compile time user template (#8766) - Add rotary encoder support for light dimmer and optional color temperature if button1 still pressed (#8670) +- Add support for switches/relays using an AC detection circuitry e.g. MOES MS-104B or BlitzWolf SS5 (#8606) - Fix exception or watchdog on rule re-entry (#8757) - Change ESP32 USER GPIO template representation decreasing template message size - Change define USE_TASMOTA_SLAVE into USE_TASMOTA_CLIENT @@ -67,6 +62,8 @@ - Add support for VEML6075 UVA/UVB/UVINDEX Sensor by device111 (#8432) - Add support for VEML7700 Ambient light intensity Sensor by device111 (#8432) +## Released + ### 8.3.1 20200518 - Release Fred @@ -83,6 +80,8 @@ - Change Quick Power Cycle detection from 4 to 7 power interrupts (#4066) - Fix default state of ``SetOption73 0`` for button decoupling and send multi-press and hold MQTT messages +## Released + ### 8.3.0 20200514 - Release Fred @@ -167,6 +166,8 @@ - Add support for unreachable (unplugged) Zigbee devices in Philips Hue emulation and Alexa - Add support for 64x48 SSD1306 OLED (#6740) +## Released + ### 8.2.0 20200321 - Release Elliot diff --git a/tasmota/support_button.ino b/tasmota/support_button.ino index 2d7c6a415..e6e6e27ea 100644 --- a/tasmota/support_button.ino +++ b/tasmota/support_button.ino @@ -303,7 +303,7 @@ void ButtonHandler(void) } } #ifdef ROTARY_V1 - if (!((0 == button_index) && RotaryButtonPressed())) { + if (!RotaryButtonPressed(button_index)) { #endif if (!Settings.flag3.mqtt_buttons && single_press && SendKey(KEY_BUTTON, button_index + Button.press_counter[button_index], POWER_TOGGLE)) { // Execute Toggle command via MQTT if ButtonTopic is set // Success diff --git a/tasmota/support_rotary.ino b/tasmota/support_rotary.ino index cfe549a5f..d58ebd560 100644 --- a/tasmota/support_rotary.ino +++ b/tasmota/support_rotary.ino @@ -34,212 +34,98 @@ \*********************************************************************************************/ #ifndef ROTARY_MAX_STEPS -#define ROTARY_MAX_STEPS 10 // Rotary step boundary +#define ROTARY_MAX_STEPS 10 // Rotary step boundary #endif -//#define ROTARY_OPTION1 // Up to 4 interrupts and pulses per step -//#define ROTARY_OPTION2 // Up to 4 interrupts but 1 pulse per step -#define ROTARY_OPTION3 // 1 interrupt and pulse per step - -#ifdef ROTARY_OPTION1 -// up to 4 pulses per step -const uint8_t rotary_dimmer_increment = 100 / (ROTARY_MAX_STEPS * 3); // Dimmer 1..100 = 100 -const uint8_t rotary_ct_increment = 350 / (ROTARY_MAX_STEPS * 3); // Ct 153..500 = 347 -const uint8_t rotary_color_increment = 360 / (ROTARY_MAX_STEPS * 3); // Hue 0..359 = 360 -#endif // ROTARY_OPTION1 - -#ifdef ROTARY_OPTION2 // 1 pulse per step -const uint8_t rotary_dimmer_increment = 100 / ROTARY_MAX_STEPS; // Dimmer 1..100 = 100 -const uint8_t rotary_ct_increment = 350 / ROTARY_MAX_STEPS; // Ct 153..500 = 347 -const uint8_t rotary_color_increment = 360 / ROTARY_MAX_STEPS; // Hue 0..359 = 360 -#endif // ROTARY_OPTION2 +const uint8_t rotary_dimmer_increment = 100 / ROTARY_MAX_STEPS; // Dimmer 1..100 = 100 +const uint8_t rotary_ct_increment = 350 / ROTARY_MAX_STEPS; // Ct 153..500 = 347 +const uint8_t rotary_color_increment = 360 / ROTARY_MAX_STEPS; // Hue 0..359 = 360 -#ifdef ROTARY_OPTION3 -// 1 pulse per step -const uint8_t rotary_dimmer_increment = 100 / ROTARY_MAX_STEPS; // Dimmer 1..100 = 100 -const uint8_t rotary_ct_increment = 350 / ROTARY_MAX_STEPS; // Ct 153..500 = 347 -const uint8_t rotary_color_increment = 360 / ROTARY_MAX_STEPS; // Hue 0..359 = 360 -#endif // ROTARY_OPTION3 - -const uint8_t ROTARY_TIMEOUT = 10; // 10 * RotaryHandler() call which is usually 10 * 0.05 seconds +const uint8_t ROTARY_TIMEOUT = 10; // 10 * RotaryHandler() call which is usually 10 * 0.05 seconds struct ROTARY { -#ifdef ROTARY_OPTION1 - uint8_t state = 0; -#endif // ROTARY_OPTION1 -#ifdef ROTARY_OPTION2 - uint16_t store; - uint8_t prev_next_code; -#endif // ROTARY_OPTION2 -#ifdef ROTARY_OPTION3 + bool present = false; +} Rotary; + +struct ENCODER { uint32_t debounce = 0; -#endif // ROTARY_OPTION3 int8_t abs_position1 = 0; int8_t abs_position2 = 0; - int8_t direction = 0; // Control consistent direction - uint8_t present = 0; + int8_t direction = 0; // Control consistent direction + int8_t pin = -1; uint8_t position = 128; uint8_t last_position = 128; - uint8_t timeout = 0; // Disallow direction change within 0.5 second + uint8_t timeout = 0; // Disallow direction change within 0.5 second bool changed = false; bool busy = false; -} Rotary; +} Encoder[MAX_ROTARIES]; /********************************************************************************************/ -void update_rotary(void) ICACHE_RAM_ATTR; -void update_rotary(void) { - if (Rotary.busy) { return; } - bool powered_on = (power); -#ifdef USE_LIGHT - if (!Settings.flag4.rotary_uses_rules) { // SetOption98 - Use rules instead of light control - powered_on = (LightPowerIRAM()); - } -#endif // USE_LIGHT - if (!powered_on) { return; } +void ICACHE_RAM_ATTR RotaryIsr(uint32_t index) { + if (Encoder[index].busy) { return; } -#ifdef ROTARY_OPTION1 - // https://github.com/PaulStoffregen/Encoder/blob/master/Encoder.h -/* - uint8_t p1val = digitalRead(Pin(GPIO_ROT1A)); - uint8_t p2val = digitalRead(Pin(GPIO_ROT1B)); - uint8_t state = Rotary.state & 3; - if (p1val) { state |= 4; } - if (p2val) { state |= 8; } - Rotary.state = (state >> 2); - switch (state) { - case 1: case 7: case 8: case 14: - Rotary.position++; - return; - case 2: case 4: case 11: case 13: - Rotary.position--; - return; - case 3: case 12: - Rotary.position += 2; - return; - case 6: case 9: - Rotary.position -= 2; - return; - } -*/ - uint8_t p1val = digitalRead(Pin(GPIO_ROT1A)); - uint8_t p2val = digitalRead(Pin(GPIO_ROT1B)); - uint8_t state = Rotary.state & 3; - if (p1val) { state |= 4; } - if (p2val) { state |= 8; } - Rotary.state = (state >> 2); - int direction = 0; - int multiply = 1; - switch (state) { - case 3: case 12: - multiply = 2; - case 1: case 7: case 8: case 14: - direction = 1; - break; - case 6: case 9: - multiply = 2; - case 2: case 4: case 11: case 13: - direction = -1; - break; - } - if ((0 == Rotary.direction) || (direction == Rotary.direction)) { - Rotary.position += (direction * multiply); - Rotary.direction = direction; - } -#endif // ROTARY_OPTION1 - -#ifdef ROTARY_OPTION2 - // https://github.com/FrankBoesing/EncoderBounce/blob/master/EncoderBounce.h -/* - const uint16_t rot_enc = 0b0110100110010110; - - uint8_t p1val = digitalRead(Pin(GPIO_ROT1B)); - uint8_t p2val = digitalRead(Pin(GPIO_ROT1A)); - uint8_t t = Rotary.prev_next_code; - t <<= 2; - if (p1val) { t |= 0x02; } - if (p2val) { t |= 0x01; } - t &= 0x0f; - Rotary.prev_next_code = t; - - // If valid then store as 16 bit data. - if (rot_enc & (1 << t)) { - Rotary.store = (Rotary.store << 4) | Rotary.prev_next_code; - if (Rotary.store == 0xd42b) { Rotary.position++; } - else if (Rotary.store == 0xe817) { Rotary.position--; } - else if ((Rotary.store & 0xff) == 0x2b) { Rotary.position--; } - else if ((Rotary.store & 0xff) == 0x17) { Rotary.position++; } - } -*/ - const uint16_t rot_enc = 0b0110100110010110; - - uint8_t p1val = digitalRead(Pin(GPIO_ROT1B)); - uint8_t p2val = digitalRead(Pin(GPIO_ROT1A)); - uint8_t t = Rotary.prev_next_code; - t <<= 2; - if (p1val) { t |= 0x02; } - if (p2val) { t |= 0x01; } - t &= 0x0f; - Rotary.prev_next_code = t; - - // If valid then store as 16 bit data. - if (rot_enc & (1 << t)) { - Rotary.store = (Rotary.store << 4) | Rotary.prev_next_code; - int direction = 0; - if (Rotary.store == 0xd42b) { direction = 1; } - else if (Rotary.store == 0xe817) { direction = -1; } - else if ((Rotary.store & 0xff) == 0x2b) { direction = -1; } - else if ((Rotary.store & 0xff) == 0x17) { direction = 1; } - if ((0 == Rotary.direction) || (direction == Rotary.direction)) { - Rotary.position += direction; - Rotary.direction = direction; - } - } -#endif // ROTARY_OPTION2 - -#ifdef ROTARY_OPTION3 - // Theo Arends uint32_t time = micros(); - if (Rotary.debounce < time) { - int direction = (digitalRead(Pin(GPIO_ROT1B))) ? 1 : -1; - if ((0 == Rotary.direction) || (direction == Rotary.direction)) { - Rotary.position += direction; - Rotary.direction = direction; + if (Encoder[index].debounce < time) { + int direction = (digitalRead(Encoder[index].pin)) ? -1 : 1; + if ((0 == Encoder[index].direction) || (direction == Encoder[index].direction)) { + Encoder[index].position += direction; + Encoder[index].direction = direction; } - Rotary.debounce = time +20; // Experimental debounce + Encoder[index].debounce = time +50; // Experimental debounce in microseconds } -#endif // ROTARY_OPTION3 } -bool RotaryButtonPressed(void) { +void ICACHE_RAM_ATTR RotaryIsr1(void) { + RotaryIsr(0); +} + +void ICACHE_RAM_ATTR RotaryIsr2(void) { + RotaryIsr(1); +} + +bool RotaryButtonPressed(uint32_t button_index) { if (!Rotary.present) { return false; } - bool powered_on = (power); + for (uint32_t index = 0; index < MAX_ROTARIES; index++) { + if (-1 == Encoder[index].pin) { continue; } + if (index != button_index) { continue; } + + bool powered_on = (power); #ifdef USE_LIGHT - if (!Settings.flag4.rotary_uses_rules) { // SetOption98 - Use rules instead of light control - powered_on = LightPower(); - } + if (!Settings.flag4.rotary_uses_rules) { // SetOption98 - Use rules instead of light control + powered_on = LightPower(); + } #endif // USE_LIGHT - if (Rotary.changed && powered_on) { - Rotary.changed = false; // Color (temp) changed, no need to turn of the light - return true; + if (Encoder[index].changed && powered_on) { + Encoder[index].changed = false; // Color (temp) changed, no need to turn of the light + return true; + } + return false; } return false; } void RotaryInit(void) { - Rotary.present = 0; - if (PinUsed(GPIO_ROT1A) && PinUsed(GPIO_ROT1B)) { - Rotary.present++; - pinMode(Pin(GPIO_ROT1A), INPUT_PULLUP); - pinMode(Pin(GPIO_ROT1B), INPUT_PULLUP); -#ifdef ROTARY_OPTION3 - attachInterrupt(Pin(GPIO_ROT1A), update_rotary, RISING); -#else - attachInterrupt(Pin(GPIO_ROT1A), update_rotary, CHANGE); - attachInterrupt(Pin(GPIO_ROT1B), update_rotary, CHANGE); -#endif + Rotary.present = false; + for (uint32_t index = 0; index < MAX_ROTARIES; index++) { +#ifdef ESP8266 + uint32_t idx = index *2; +#else // ESP32 + uint32_t idx = index; +#endif // ESP8266 or ESP32 + if (PinUsed(GPIO_ROT1A, idx) && PinUsed(GPIO_ROT1B, idx)) { + Encoder[index].pin = Pin(GPIO_ROT1B, idx); + pinMode(Encoder[index].pin, INPUT_PULLUP); + pinMode(Pin(GPIO_ROT1A, idx), INPUT_PULLUP); + if (0 == index) { + attachInterrupt(Pin(GPIO_ROT1A, idx), RotaryIsr1, FALLING); + } else { + attachInterrupt(Pin(GPIO_ROT1A, idx), RotaryIsr2, FALLING); + } + } + Rotary.present |= (Encoder[index].pin > -1); } } @@ -250,56 +136,80 @@ void RotaryInit(void) { void RotaryHandler(void) { if (!Rotary.present) { return; } - if (Rotary.timeout) { - Rotary.timeout--; - if (!Rotary.timeout) { - Rotary.direction = 0; + for (uint32_t index = 0; index < MAX_ROTARIES; index++) { + if (-1 == Encoder[index].pin) { continue; } + + if (Encoder[index].timeout) { + Encoder[index].timeout--; + if (!Encoder[index].timeout) { +#ifdef USE_LIGHT + if (!Settings.flag4.rotary_uses_rules) { // SetOption98 - Use rules instead of light control + ResponseLightState(0); + MqttPublishPrefixTopic_P(RESULT_OR_STAT, PSTR(D_CMND_STATE)); + XdrvRulesProcess(); + } +#endif // USE_LIGHT + Encoder[index].direction = 0; + } } - } - if (Rotary.last_position == Rotary.position) { return; } - Rotary.busy = true; + if (Encoder[index].last_position == Encoder[index].position) { continue; } + Encoder[index].busy = true; - Rotary.timeout = ROTARY_TIMEOUT; // Prevent fast direction changes within 0.5 second + Encoder[index].timeout = ROTARY_TIMEOUT; // Prevent fast direction changes within 0.5 second - int rotary_position = Rotary.position - Rotary.last_position; + int rotary_position = Encoder[index].position - Encoder[index].last_position; - if (Settings.save_data && (save_data_counter < 2)) { - save_data_counter = 2; // Postpone flash writes while rotary is turned - } + if (Settings.save_data && (save_data_counter < 2)) { + save_data_counter = 2; // Postpone flash writes while rotary is turned + } - bool button_pressed = (Button.hold_timer[0]); // Button1 is pressed: set color temperature - if (button_pressed) { Rotary.changed = true; } -// AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ROT: Button1 %d, Position %d"), button_pressed, rotary_position); + bool button_pressed = (Button.hold_timer[index]); // Button is pressed: set color temperature + if (button_pressed) { Encoder[index].changed = true; } +// AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ROT: Button1 %d, Position %d"), button_pressed, rotary_position); #ifdef USE_LIGHT - if (!Settings.flag4.rotary_uses_rules) { // SetOption98 - Use rules instead of light control - if (button_pressed) { - if (!LightColorTempOffset(rotary_position * rotary_ct_increment)) { - LightColorOffset(rotary_position * rotary_color_increment); + if (!Settings.flag4.rotary_uses_rules) { // SetOption98 - Use rules instead of light control + bool second_rotary = (Encoder[1].pin > -1); + if (0 == index) { // Rotary1 + if (button_pressed) { + if (second_rotary) { // Color RGB + LightColorOffset(rotary_position * rotary_color_increment); + } else { // Color Temperature or Color RGB + if (!LightColorTempOffset(rotary_position * rotary_ct_increment)) { + LightColorOffset(rotary_position * rotary_color_increment); + } + } + } else { // Dimmer RGBCW or RGB only if second rotary + LightDimmerOffset(second_rotary ? 1 : 0, rotary_position * rotary_dimmer_increment); + } + } else { // Rotary2 + if (button_pressed) { // Color Temperature + LightColorTempOffset(rotary_position * rotary_ct_increment); + } else { // Dimmer CW + LightDimmerOffset(2, rotary_position * rotary_dimmer_increment); + } } } else { - LightDimmerOffset(rotary_position * rotary_dimmer_increment); - } - } else { #endif // USE_LIGHT - if (button_pressed) { - Rotary.abs_position2 += rotary_position; - if (Rotary.abs_position2 < 0) { Rotary.abs_position2 = 0; } - if (Rotary.abs_position2 > ROTARY_MAX_STEPS) { Rotary.abs_position2 = ROTARY_MAX_STEPS; } - } else { - Rotary.abs_position1 += rotary_position; - if (Rotary.abs_position1 < 0) { Rotary.abs_position1 = 0; } - if (Rotary.abs_position1 > ROTARY_MAX_STEPS) { Rotary.abs_position1 = ROTARY_MAX_STEPS; } - } - Response_P(PSTR("{\"Rotary1\":{\"Pos1\":%d,\"Pos2\":%d}}"), Rotary.abs_position1, Rotary.abs_position2); - XdrvRulesProcess(); + if (button_pressed) { + Encoder[index].abs_position2 += rotary_position; + if (Encoder[index].abs_position2 < 0) { Encoder[index].abs_position2 = 0; } + if (Encoder[index].abs_position2 > ROTARY_MAX_STEPS) { Encoder[index].abs_position2 = ROTARY_MAX_STEPS; } + } else { + Encoder[index].abs_position1 += rotary_position; + if (Encoder[index].abs_position1 < 0) { Encoder[index].abs_position1 = 0; } + if (Encoder[index].abs_position1 > ROTARY_MAX_STEPS) { Encoder[index].abs_position1 = ROTARY_MAX_STEPS; } + } + Response_P(PSTR("{\"Rotary%d\":{\"Pos1\":%d,\"Pos2\":%d}}"), index +1, Encoder[index].abs_position1, Encoder[index].abs_position2); + XdrvRulesProcess(); #ifdef USE_LIGHT - } + } #endif // USE_LIGHT - Rotary.last_position = 128; - Rotary.position = 128; - Rotary.busy = false; + Encoder[index].last_position = 128; + Encoder[index].position = 128; + Encoder[index].busy = false; + } } #endif // ROTARY_V1 diff --git a/tasmota/support_tasmota.ino b/tasmota/support_tasmota.ino index 68edd6a7a..70d7f66af 100644 --- a/tasmota/support_tasmota.ino +++ b/tasmota/support_tasmota.ino @@ -663,7 +663,7 @@ void MqttShowState(void) for (uint32_t i = 1; i <= devices_present; i++) { #ifdef USE_LIGHT if ((LightDevice()) && (i >= LightDevice())) { - if (i == LightDevice()) { LightState(1); } // call it only once + if (i == LightDevice()) { ResponseLightState(1); } // call it only once } else { #endif ResponseAppend_P(PSTR(",\"%s\":\"%s\""), GetPowerDevice(stemp1, i, sizeof(stemp1), Settings.flag.device_index_enable), // SetOption26 - Switch between POWER or POWER1 diff --git a/tasmota/tasmota.h b/tasmota/tasmota.h index 4eceddb5c..21a7b5b00 100644 --- a/tasmota/tasmota.h +++ b/tasmota/tasmota.h @@ -84,6 +84,7 @@ const uint8_t MAX_GROUP_TOPICS = 4; // Max number of Group Topics const uint8_t MAX_DEV_GROUP_NAMES = 4; // Max number of Device Group names const uint8_t MAX_HUE_DEVICES = 15; // Max number of Philips Hue device per emulation +const uint8_t MAX_ROTARIES = 2; // Max number of Rotary Encoders const char MQTT_TOKEN_PREFIX[] PROGMEM = "%prefix%"; // To be substituted by mqtt_prefix[x] const char MQTT_TOKEN_TOPIC[] PROGMEM = "%topic%"; // To be substituted by mqtt_topic, mqtt_grptopic, mqtt_buttontopic, mqtt_switchtopic diff --git a/tasmota/tasmota_template.h b/tasmota/tasmota_template.h index bd1c009de..d287d7e01 100644 --- a/tasmota/tasmota_template.h +++ b/tasmota/tasmota_template.h @@ -373,6 +373,12 @@ const uint8_t kGpioNiceList[] PROGMEM = { GPIO_SWT7_NP, GPIO_SWT8, GPIO_SWT8_NP, +#ifdef ROTARY_V1 + GPIO_ROT1A, // Rotary switch1 A Pin + GPIO_ROT1B, // Rotary switch1 B Pin + GPIO_ROT2A, // Rotary switch2 A Pin + GPIO_ROT2B, // Rotary switch2 B Pin +#endif GPIO_REL1, // Relays GPIO_REL1_INV, GPIO_REL2, @@ -665,12 +671,6 @@ const uint8_t kGpioNiceList[] PROGMEM = { GPIO_MAX31855CLK, // MAX31855 Serial interface GPIO_MAX31855DO, // MAX31855 Serial interface #endif -#ifdef ROTARY_V1 - GPIO_ROT1A, // Rotary switch1 A Pin - GPIO_ROT1B, // Rotary switch1 B Pin - GPIO_ROT2A, // Rotary switch2 A Pin - GPIO_ROT2B, // Rotary switch2 B Pin -#endif #ifdef USE_HRE GPIO_HRE_CLOCK, GPIO_HRE_DATA, diff --git a/tasmota/tasmota_template_ESP32.h b/tasmota/tasmota_template_ESP32.h index e5b30a5b1..72c72e48b 100644 --- a/tasmota/tasmota_template_ESP32.h +++ b/tasmota/tasmota_template_ESP32.h @@ -88,7 +88,10 @@ enum UserSelectablePins { GPIO_CSE7766_TX, GPIO_CSE7766_RX, // CSE7766 Serial interface (S31 and Pow R2) GPIO_ARIRFRCV, GPIO_ARIRFSEL, // Arilux RF Receive input GPIO_TXD, GPIO_RXD, // Serial interface - GPIO_ROT1A, GPIO_ROT1B, GPIO_ROT2A, GPIO_ROT2B, // Rotary switch + GPIO_ROT1A, GPIO_ROT1B, // Rotary switch + + GPIO_SPARE1, GPIO_SPARE2, // Spare GPIOs + GPIO_HRE_CLOCK, GPIO_HRE_DATA, // HR-E Water Meter GPIO_ADE7953_IRQ, // ADE7953 IRQ GPIO_SOLAXX1_TX, GPIO_SOLAXX1_RX, // Solax Inverter Serial interface @@ -188,7 +191,10 @@ const char kSensorNames[] PROGMEM = D_SENSOR_CSE7766_TX "|" D_SENSOR_CSE7766_RX "|" D_SENSOR_ARIRFRCV "|" D_SENSOR_ARIRFSEL "|" D_SENSOR_TXD "|" D_SENSOR_RXD "|" - D_SENSOR_ROTARY "_1a|" D_SENSOR_ROTARY "_1b|" D_SENSOR_ROTARY "_2a|" D_SENSOR_ROTARY "_2b|" + D_SENSOR_ROTARY "_a|" D_SENSOR_ROTARY "_b|" + + "Spare1|Spare2|" + D_SENSOR_HRE_CLOCK "|" D_SENSOR_HRE_DATA "|" D_SENSOR_ADE7953_IRQ "|" D_SENSOR_SOLAXX1_TX "|" D_SENSOR_SOLAXX1_RX "|" @@ -245,6 +251,10 @@ const uint16_t kGpioNiceList[] PROGMEM = { AGPIO(GPIO_KEY1_TC) + MAX_KEYS, // Touch button AGPIO(GPIO_SWT1) + MAX_SWITCHES, // User connected external switches AGPIO(GPIO_SWT1_NP) + MAX_SWITCHES, +#ifdef ROTARY_V1 + AGPIO(GPIO_ROT1A) + MAX_ROTARIES, // Rotary A Pin + AGPIO(GPIO_ROT1B) + MAX_ROTARIES, // Rotary B Pin +#endif AGPIO(GPIO_REL1) + MAX_RELAYS, // Relays AGPIO(GPIO_REL1_INV) + MAX_RELAYS, AGPIO(GPIO_LED1) + MAX_LEDS, // Leds @@ -508,12 +518,6 @@ const uint16_t kGpioNiceList[] PROGMEM = { AGPIO(GPIO_MAX31855CLK), // MAX31855 Serial interface AGPIO(GPIO_MAX31855DO), // MAX31855 Serial interface #endif -#ifdef ROTARY_V1 - AGPIO(GPIO_ROT1A), // Rotary switch1 A Pin - AGPIO(GPIO_ROT1B), // Rotary switch1 B Pin - AGPIO(GPIO_ROT2A), // Rotary switch2 A Pin - AGPIO(GPIO_ROT2B), // Rotary switch2 B Pin -#endif #ifdef USE_HRE AGPIO(GPIO_HRE_CLOCK), AGPIO(GPIO_HRE_DATA), diff --git a/tasmota/xdrv_04_light.ino b/tasmota/xdrv_04_light.ino index 86293c2b2..d284e71fe 100644 --- a/tasmota/xdrv_04_light.ino +++ b/tasmota/xdrv_04_light.ino @@ -314,16 +314,6 @@ power_t LightPower(void) return Light.power; // Make external } -// IRAM variant for rotary -#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 // Fix core 2.5.x ISR not in IRAM Exception -power_t LightPowerIRAM(void) ICACHE_RAM_ATTR; -#endif // ARDUINO_ESP8266_RELEASE_2_3_0 - -power_t LightPowerIRAM(void) -{ - return Light.power; // Make external -} - uint8_t LightDevice(void) { return Light.device; // Make external @@ -1568,7 +1558,7 @@ void LightPowerOn(void) } } -void LightState(uint8_t append) +void ResponseLightState(uint8_t append) { char scolor[LIGHT_COLOR_SIZE]; char scommand[33]; @@ -1718,7 +1708,7 @@ void LightPreparePower(power_t channels = 0xFFFFFFFF) { // 1 = only RGB, 2 = AddLog_P2(LOG_LEVEL_DEBUG, "LightPreparePower End power=%d Light.power=%d", power, Light.power); #endif Light.power = power >> (Light.device - 1); // reset next state, works also with unlinked RGB/CT - LightState(0); + ResponseLightState(0); } #ifdef USE_LIGHT_PALETTE @@ -1886,7 +1876,7 @@ void LightAnimate(void) MqttPublishPrefixTopic_P(TELE, PSTR(D_CMND_WAKEUP)); */ Response_P(PSTR("{\"" D_CMND_WAKEUP "\":\"" D_JSON_DONE "\"")); - LightState(1); + ResponseLightState(1); ResponseJsonEnd(); MqttPublishPrefixTopic_P(RESULT_OR_STAT, PSTR(D_CMND_WAKEUP)); XdrvRulesProcess(); @@ -2689,7 +2679,7 @@ void CmndHsbColor(void) light_controller.changeHSB(HSB[0], HSB[1], HSB[2]); LightPreparePower(1); } else { - LightState(0); + ResponseLightState(0); } } } @@ -2774,12 +2764,12 @@ void CmndColorTemperature(void) } } -void LightDimmerOffset(int32_t offset) { - int32_t dimmer = light_state.getDimmer() + offset; - if (dimmer < 1) { dimmer = 1; } +void LightDimmerOffset(uint32_t index, int32_t offset) { + int32_t dimmer = light_state.getDimmer(index) + offset; + if (dimmer < 1) { dimmer = Settings.flag3.slider_dimmer_stay_on; } // SetOption77 - Do not power off if slider moved to far left if (dimmer > 100) { dimmer = 100; } - XdrvMailbox.index = 0; + XdrvMailbox.index = index; XdrvMailbox.payload = dimmer; CmndDimmer(); } diff --git a/tasmota/xsns_05_ds18x20.ino b/tasmota/xsns_05_ds18x20.ino index a762014c7..361847932 100644 --- a/tasmota/xsns_05_ds18x20.ino +++ b/tasmota/xsns_05_ds18x20.ino @@ -375,12 +375,19 @@ bool Ds18x20Read(uint8_t sensor) if (OneWireCrc8(data)) { switch(ds18x20_sensor[index].address[0]) { case DS18S20_CHIPID: { +/* if (data[1] > 0x80) { data[0] = (~data[0]) +1; sign = -1; // App-Note fix possible sign error } float temp9 = (float)(data[0] >> 1) * sign; ds18x20_sensor[index].temperature = ConvertTemp((temp9 - 0.25) + ((16.0 - data[6]) / 16.0)); + + Replaced by below based on issue #8777 +*/ + int16_t tempS = (((data[1] << 8) | (data[0] & 0xFE)) << 3) | ((0x10 - data[6]) & 0x0F); + ds18x20_sensor[index].temperature = ConvertTemp(tempS * 0.0625 - 0.250); + ds18x20_sensor[index].valid = SENSOR_MAX_MISS; return true; } diff --git a/tasmota/xsns_61_MI_NRF24.ino b/tasmota/xsns_61_MI_NRF24.ino index c24bd07da..44456a6a7 100644 --- a/tasmota/xsns_61_MI_NRF24.ino +++ b/tasmota/xsns_61_MI_NRF24.ino @@ -83,8 +83,9 @@ #define NLIGHT 7 #define MJYD2S 8 #define YEERC 9 +#define MHOC401 10 -#define MI_TYPES 9 //count this manually +#define MI_TYPES 10 //count this manually #define D_CMND_NRF "NRF" @@ -119,7 +120,8 @@ const uint16_t kMINRFDeviceID[MI_TYPES]={ 0x0098, // Flora 0x0576, // CGD1 0x03dd, // NLIGHT 0x07f6, // MJYD2S - 0x0153 // yee-rc + 0x0153, // yee-rc + 0x0387 // MHO-C401 }; const char kMINRFDeviceType1[] PROGMEM = "Flora"; @@ -131,7 +133,8 @@ const char kMINRFDeviceType6[] PROGMEM = "CGD1"; const char kMINRFDeviceType7[] PROGMEM = "NLIGHT"; const char kMINRFDeviceType8[] PROGMEM = "MJYD2S"; const char kMINRFDeviceType9[] PROGMEM = "YEERC"; -const char * kMINRFDeviceType[] PROGMEM = {kMINRFDeviceType1,kMINRFDeviceType2,kMINRFDeviceType3,kMINRFDeviceType4,kMINRFDeviceType5,kMINRFDeviceType6,kMINRFDeviceType7,kMINRFDeviceType8,kMINRFDeviceType9}; +const char kMINRFDeviceType10[] PROGMEM = "MHOC401"; +const char * kMINRFDeviceType[] PROGMEM = {kMINRFDeviceType1,kMINRFDeviceType2,kMINRFDeviceType3,kMINRFDeviceType4,kMINRFDeviceType5,kMINRFDeviceType6,kMINRFDeviceType7,kMINRFDeviceType8,kMINRFDeviceType9,kMINRFDeviceType10}; // PDU's or different channels 37-39 const uint32_t kMINRFFloPDU[3] = {0x3eaa857d,0xef3b8730,0x71da7b46}; @@ -411,7 +414,7 @@ bool MINRFreceivePacket(void) case 1: case 3: MINRFwhiten((uint8_t *)&MINRF.buffer, sizeof(MINRF.buffer), kMINRFlsfrList_A[MINRF.currentChan]); // "flora" mode, "LYWSD02" mode break; - case 2: case 4: case 5: case 6: + case 2: case 4: case 5: case 6: case MHOC401: MINRFwhiten((uint8_t *)&MINRF.buffer, sizeof(MINRF.buffer), kMINRFlsfrList_B[MINRF.currentChan]); // "MJ_HT_V1" mode, LYWSD03" mode, "CGG1" mode, "CGD1" mode break; case 9: @@ -919,7 +922,7 @@ void MINRFchangePacketModeTo(uint8_t _mode) { case 3: // special LYWSD02 packet NRF24radio.openReadingPipe(0,kMINRFL2PDU[_nextchannel]);// 95 fe 70 20 -> LYWSD02 break; - case 4: // special LYWSD03 packet + case 4: case MHOC401: // special LYWSD03 packet, MHOC401 has the same NRF24radio.openReadingPipe(0,kMINRFL3PDU[_nextchannel]);// 95 fe 58 58 -> LYWSD03 (= encrypted data message) break; case 5: // special CGG1 packet @@ -1068,7 +1071,7 @@ void MINRFhandleMiBeaconPacket(void){ memcpy((uint8_t*)&MINRF.buffer.miBeacon.type,MINRFtempBuf, 32-9); // shift by one byte for the MJ_HT_V1 and CGG1 break; #ifdef USE_MI_DECRYPTION - case LYWSD03: + case LYWSD03: case MHOC401: decryptRet = MINRFdecryptPacket((char*)&MINRF.buffer); //start with PID if(decryptRet==1) _sensorVec->showedUp=255; // if decryption worked, this must be a valid sensor break; @@ -1324,7 +1327,7 @@ void MINRF_EVERY_50_MSECOND() { // Every 50mseconds } else MINRFhandleScan(); break; - case FLORA: case MJ_HT_V1: case LYWSD02: case CGG1: case LYWSD03: case YEERC: + case FLORA: case MJ_HT_V1: case LYWSD02: case CGG1: case LYWSD03: case YEERC: case MHOC401: MINRFhandleMiBeaconPacket(); break; case CGD1: diff --git a/tasmota/xsns_62_MI_ESP32.ino b/tasmota/xsns_62_MI_ESP32.ino index 9fa6a71d8..6ec2d6f83 100644 --- a/tasmota/xsns_62_MI_ESP32.ino +++ b/tasmota/xsns_62_MI_ESP32.ino @@ -20,7 +20,11 @@ -------------------------------------------------------------------------------------------- Version yyyymmdd Action Description -------------------------------------------------------------------------------------------- + 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 @@ -30,9 +34,13 @@ #ifdef USE_MI_ESP32 #define XSNS_62 62 +#define USE_MI_DECRYPTION #include #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); @@ -56,6 +64,10 @@ struct { uint32_t willReadBatt:1; uint32_t shallSetUnit:1; uint32_t willSetUnit:1; + uint32_t triggeredTele:1; + uint32_t shallClearResults:1; + uint32_t directMQTT:1; // TODO: direct bridging of every single sensor message + } mode; struct { uint8_t sensor; // points to to the number 0...255 @@ -87,7 +99,7 @@ struct mi_beacon_t{ uint16_t frame; uint16_t productID; uint8_t counter; - uint8_t Mac[6]; + uint8_t MAC[6]; uint8_t spare; uint8_t type; uint8_t ten; @@ -103,12 +115,18 @@ struct mi_beacon_t{ uint32_t lux; //07 uint8_t moist; //08 uint16_t fert; //09 + uint32_t NMT; //17 + struct{ //01 + uint16_t num; + uint8_t longPress; + }Btn; }; + uint8_t padding[12]; }; struct cg_packet_t { uint16_t frameID; - uint8_t serial[6]; + uint8_t MAC[6]; uint16_t mode; union { struct { @@ -119,34 +137,58 @@ struct cg_packet_t { }; }; +struct encPacket_t{ + // the packet is longer, but this part is enough to decrypt + uint16_t PID; + uint8_t frameCnt; + uint8_t MAC[6]; + uint8_t payload[16]; // only a pointer to the address, size is variable +}; + +union mi_bindKey_t{ + struct{ + uint8_t key[16]; + uint8_t MAC[6]; + }; + uint8_t buf[22]; +}; + #pragma pack(0) struct mi_sensor_t{ uint8_t type; //Flora = 1; MI-HT_V1=2; LYWSD02=3; LYWSD03=4; CGG1=5; CGD1=6 - uint8_t serial[6]; - uint8_t showedUp; + uint8_t lastCnt; //device generated counter of the packet + uint8_t shallSendMQTT; + uint8_t MAC[6]; + // uint8_t showedUp; + uint32_t lastTime; + uint32_t lux; float temp; //Flora, MJ_HT_V1, LYWSD0x, CGx union { struct { float moisture; float fertility; - uint32_t lux; + 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 + uint8_t eventType; //internal type of actual event for the MJYD2S -> 1: PIR, 2: No PIR, 3: NMT + }; + uint16_t Btn; }; - union - { - uint8_t bat; // many values seem to be hard-coded garbage (LYWSD0x, GCD1) - uint16_t volt; // LYWSD03MMC + union { + uint8_t bat; // many values seem to be hard-coded garbage (LYWSD0x, GCD1) }; - char firmware[6]; // actually only for FLORA but hopefully we can add for more devices }; std::vector MIBLEsensors; -BLEScan* MI32Scan; -BLEScanResults MI32foundDevices; +std::vector MIBLEbindKeys; + +static BLEScan* MI32Scan; /*********************************************************************************************\ * constants @@ -156,7 +198,7 @@ BLEScanResults MI32foundDevices; const char S_JSON_MI32_COMMAND_NVALUE[] PROGMEM = "{\"" D_CMND_MI32 "%s\":%d}"; const char S_JSON_MI32_COMMAND[] PROGMEM = "{\"" D_CMND_MI32 "%s%s\"}"; -const char kMI32_Commands[] PROGMEM = "Period|Time|Page|Battery|Unit"; +const char kMI32_Commands[] PROGMEM = "Period|Time|Page|Battery|Unit|Key"; #define FLORA 1 #define MJ_HT_V1 2 @@ -164,22 +206,31 @@ const char kMI32_Commands[] PROGMEM = "Period|Time|Page|Battery|Unit #define LYWSD03MMC 4 #define CGG1 5 #define CGD1 6 +#define NLIGHT 7 +#define MJYD2S 8 +#define YEERC 9 -const uint16_t kMI32SlaveID[6]={ 0x0098, // Flora +const uint16_t kMI32DeviceID[9]={ 0x0098, // Flora 0x01aa, // MJ_HT_V1 0x045b, // LYWSD02 0x055b, // LYWSD03 0x0347, // CGG1 - 0x0576 // CGD1 + 0x0576, // CGD1 + 0x03dd, // NLIGHT + 0x07f6, // MJYD2S + 0x0153 // yee-rc }; -const char kMI32SlaveType1[] PROGMEM = "Flora"; -const char kMI32SlaveType2[] PROGMEM = "MJ_HT_V1"; -const char kMI32SlaveType3[] PROGMEM = "LYWSD02"; -const char kMI32SlaveType4[] PROGMEM = "LYWSD03"; -const char kMI32SlaveType5[] PROGMEM = "CGG1"; -const char kMI32SlaveType6[] PROGMEM = "CGD1"; -const char * kMI32SlaveType[] PROGMEM = {kMI32SlaveType1,kMI32SlaveType2,kMI32SlaveType3,kMI32SlaveType4,kMI32SlaveType5,kMI32SlaveType6}; +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 * kMI32DeviceType[] PROGMEM = {kMI32DeviceType1,kMI32DeviceType2,kMI32DeviceType3,kMI32DeviceType4,kMI32DeviceType5,kMI32DeviceType6,kMI32DeviceType7,kMI32DeviceType8,kMI32DeviceType9}; /*********************************************************************************************\ * enumerations @@ -190,7 +241,8 @@ enum MI32_Commands { // commands useable in console or rules CMND_MI32_TIME, // set LYWSD02-Time from ESP8266-time CMND_MI32_PAGE, // sensor entries per web page, which will be shown alternated CMND_MI32_BATTERY, // read all battery levels - CMND_MI32_UNIT // toggles the displayed unit between C/F (LYWSD02) + CMND_MI32_UNIT, // toggles the displayed unit between C/F (LYWSD02) + CMND_MI32_KEY // add bind key to a mac for packet decryption }; enum MI32_TASK { @@ -207,13 +259,13 @@ enum MI32_TASK { class MI32SensorCallback : public NimBLEClientCallbacks { void onConnect(NimBLEClient* pclient) { - AddLog_P2(LOG_LEVEL_DEBUG,PSTR("connected %s"), kMI32SlaveType[(MIBLEsensors[MI32.state.sensor].type)-1]); + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("connected %s"), kMI32DeviceType[(MIBLEsensors[MI32.state.sensor].type)-1]); MI32.mode.willConnect = 0; MI32.mode.connected = 1; } void onDisconnect(NimBLEClient* pclient) { MI32.mode.connected = 0; - AddLog_P2(LOG_LEVEL_DEBUG,PSTR("disconnected %s"), kMI32SlaveType[(MIBLEsensors[MI32.state.sensor].type)-1]); + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("disconnected %s"), kMI32DeviceType[(MIBLEsensors[MI32.state.sensor].type)-1]); } bool onConnParamsUpdateRequest(NimBLEClient* MI32Client, const ble_gap_upd_params* params) { if(params->itvl_min < 24) { /** 1.25ms units */ @@ -234,6 +286,7 @@ class MI32AdvCallbacks: public NimBLEAdvertisedDeviceCallbacks { // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Advertised Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData().length()); if (advertisedDevice->getServiceData().length() == 0) { // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("No Xiaomi Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData().length()); + MI32Scan->erase(advertisedDevice->getAddress()); return; } uint16_t uuid = advertisedDevice->getServiceDataUUID().getNative()->u16.value; @@ -243,13 +296,12 @@ class MI32AdvCallbacks: public NimBLEAdvertisedDeviceCallbacks { MI32_ReverseMAC(addr); if(uuid==0xfe95) { MI32ParseResponse((char*)advertisedDevice->getServiceData().data(),advertisedDevice->getServiceData().length(), addr); - MI32Scan->erase(advertisedDevice->getAddress()); } else if(uuid==0xfdcd) { MI32parseCGD1Packet((char*)advertisedDevice->getServiceData().data(),advertisedDevice->getServiceData().length(), addr); - MI32Scan->erase(advertisedDevice->getAddress()); } else { + MI32Scan->erase(advertisedDevice->getAddress()); // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("No Xiaomi Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData().length()); } }; @@ -293,6 +345,125 @@ void MI32_ReverseMAC(uint8_t _mac[]){ memcpy(_mac,_reversedMAC, sizeof(_reversedMAC)); } +#ifdef USE_MI_DECRYPTION +void MI32AddKey(char* payload){ + mi_bindKey_t keyMAC; + memset(keyMAC.buf,0,sizeof(keyMAC)); + MI32KeyMACStringToBytes(payload,keyMAC.buf); + bool unknownKey = true; + for(uint32_t i=0; i= '0' && c <= '9') + value = (c - '0'); + else if (c >= 'A' && c <= 'F') + value = (10 + (c - 'A')); + _keyMAC[(index/2)] += value << (((index + 1) % 2) * 4); + index++; + } + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MI32: %s to:"),_string); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MI32: key-array: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X"),_keyMAC[0],_keyMAC[1],_keyMAC[2],_keyMAC[3],_keyMAC[4],_keyMAC[5],_keyMAC[6],_keyMAC[7],_keyMAC[8],_keyMAC[9],_keyMAC[10],_keyMAC[11],_string,_keyMAC[12],_keyMAC[13],_keyMAC[14],_keyMAC[15]); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MI32: MAC-array: %02X%02X%02X%02X%02X%02X"),_keyMAC[16],_keyMAC[17],_keyMAC[18],_keyMAC[19],_keyMAC[20],_keyMAC[21]); +} + +/** + * @brief Decrypts payload in place + * + * @param _buf - pointer to the buffer at position of PID + * @param _bufSize - buffersize (last position is last byte of TAG) + * @param _type - sensor type + * @return int - error code, 0 for success + */ +int MI32_decryptPacket(char *_buf, uint16_t _bufSize, uint32_t _type){ + encPacket_t *packet = (encPacket_t*)_buf; + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("to decrypt: %02x %02x %02x %02x %02x %02x %02x %02x"),(uint8_t)_buf[0],(uint8_t)_buf[1],(uint8_t)_buf[2],(uint8_t)_buf[3],(uint8_t)_buf[4],(uint8_t)_buf[5],(uint8_t)_buf[6],(uint8_t)_buf[7]); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR(" : %02x %02x %02x %02x %02x %02x %02x %02x"),(uint8_t)_buf[8],(uint8_t)_buf[9],(uint8_t)_buf[10],(uint8_t)_buf[11],(uint8_t)_buf[12],(uint8_t)_buf[13],(uint8_t)_buf[14],(uint8_t)_buf[15]); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR(" : %02x %02x %02x %02x %02x %02x %02x %02x"),(uint8_t)_buf[16],(uint8_t)_buf[17],(uint8_t)_buf[18],(uint8_t)_buf[19],(uint8_t)_buf[20],(uint8_t)_buf[21],(uint8_t)_buf[22],(uint8_t)_buf[23]); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("as packet: 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_P2(LOG_LEVEL_DEBUG,PSTR("Decrypt Size of Buffer: %u"), _bufSize); + int ret = 0; + unsigned char output[10] = {0}; + uint8_t nonce[12]; + uint8_t tag[4 ]; + const unsigned char authData[1] = {0x11}; + + // nonce: device MAC, device type, frame cnt, ext. cnt + for (uint32_t i = 0; i<6; i++){ + nonce[i] = packet->MAC[i]; + } + memcpy((uint8_t*)&nonce+6,(uint8_t*)&packet->PID,2); + nonce[8] = packet->frameCnt; + memcpy((uint8_t*)&nonce+9,(char*)&_buf[_bufSize-9],3); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("nonceCnt1 and 2: %02x %02x %02x"),nonce[9],nonce[10],nonce[11]); + memcpy((uint8_t*)&tag,(char*)&_buf[_bufSize-6],4); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("tag: %02x %02x %02x %02x"),tag[0],tag[1],tag[2],tag[3]); + + MI32_ReverseMAC(packet->MAC); + uint8_t _bindkey[16] = {0x0}; + for(uint32_t i=0; iMAC,MIBLEbindKeys[i].MAC,sizeof(packet->MAC))==0){ + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("have key")); + memcpy(_bindkey,MIBLEbindKeys[i].key,sizeof(_bindkey)); + } + else{ + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mac in packet: %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_P2(LOG_LEVEL_DEBUG,PSTR("Mac in vector: %02x %02x %02x %02x %02x %02x"), MIBLEbindKeys[i].MAC[0], MIBLEbindKeys[i].MAC[1], MIBLEbindKeys[i].MAC[2], MIBLEbindKeys[i].MAC[3], MIBLEbindKeys[i].MAC[4], MIBLEbindKeys[i].MAC[5]); + } + } + + // init + mbedtls_ccm_context ctx; + mbedtls_ccm_init(&ctx); + // set bind key + ret = mbedtls_ccm_setkey(&ctx, + MBEDTLS_CIPHER_ID_AES, + _bindkey, + 16 * 8 //bits + ); + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("set key: %i, MAC: %02x %02x %02x %02x %02x %02x"),ret, packet->MAC[0], packet->MAC[1], packet->MAC[2], packet->MAC[3], packet->MAC[4], packet->MAC[5]); + +/*int mbedtls_ccm_auth_decrypt( mbedtls_ccm_context *ctx, size_t length, + const unsigned char *iv, size_t iv_len, + const unsigned char *add, size_t add_len, + const unsigned char *input, unsigned char *output, + const unsigned char *tag, size_t tag_len ) +*/ + ret = mbedtls_ccm_auth_decrypt(&ctx,_bufSize-18, + (const unsigned char*)&nonce, sizeof(nonce), + authData, sizeof(authData), + (const unsigned char*)packet->payload, output, + tag,sizeof(tag)); + + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Decrypted %i: %02x %02x %02x %02x %02x %02x %02x"), ret, output[0],output[1],output[2],output[3],output[4],output[5],output[6]); + // put decrypted data in place + memcpy((uint8_t*)(packet->payload)+1,output,_bufSize-18); + // clean up + mbedtls_ccm_free(&ctx); + return ret; +} +#endif // USE_MI_DECRYPTION + /*********************************************************************************************\ * common functions \*********************************************************************************************/ @@ -301,63 +472,82 @@ void MI32_ReverseMAC(uint8_t _mac[]){ /** * @brief Return the slot number of a known sensor or return create new sensor slot * - * @param _serial BLE address of the sensor + * @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(uint8_t (&_serial)[6], uint16_t _type){ +uint32_t MIBLEgetSensorSlot(uint8_t (&_MAC)[6], uint16_t _type, uint8_t counter){ DEBUG_SENSOR_LOG(PSTR("%s: will test ID-type: %x"),D_CMND_MI32, _type); bool _success = false; - for (uint32_t i=0;i<6;i++){ // i < sizeof(kMI32SlaveID) gives compiler warning - if(_type == kMI32SlaveID[i]){ + for (uint32_t i=0;i<9;i++){ // i < sizeof(kMI32DeviceID) gives compiler warning + if(_type == kMI32DeviceID[i]){ DEBUG_SENSOR_LOG(PSTR("MI32: ID is type %u"), i); _type = i+1; _success = true; } else { - DEBUG_SENSOR_LOG(PSTR("%s: ID-type is not: %x"),D_CMND_MI32,kMI32SlaveID[i]); + DEBUG_SENSOR_LOG(PSTR("%s: ID-type is not: %x"),D_CMND_MI32,kMI32DeviceID[i]); } } if(!_success) return 0xff; DEBUG_SENSOR_LOG(PSTR("%s: vector size %u"),D_CMND_MI32, MIBLEsensors.size()); for(uint32_t i=0; isetClientCallbacks(&MI32SensorCB , false); MI32Client->setConnectionParams(12,12,0,48); MI32Client->setConnectTimeout(30); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: did create new client"),D_CMND_MI32); } + vTaskDelay(300/ portTICK_PERIOD_MS); if (!MI32Client->connect(_address,false)) { MI32.mode.willConnect = 0; - NimBLEDevice::deleteClient(MI32Client); - DEBUG_SENSOR_LOG(PSTR("%s: did not connect client"),D_CMND_MI32); + // NimBLEDevice::deleteClient(MI32Client); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: did not connect client"),D_CMND_MI32); return false; } - DEBUG_SENSOR_LOG(PSTR("%s: did create new client"),D_CMND_MI32); return true; // } } - void MI32StartScanTask(){ if (MI32.mode.connected) return; MI32.mode.runningScan = 1; - // Wifi.counter = Wifi.counter + 3; xTaskCreatePinnedToCore( MI32ScanTask, /* Function to implement the task */ "MI32ScanTask", /* Name of the task */ - 8192, /* Stack size in words */ + 4096, /* Stack size in words */ NULL, /* Task input parameter */ 0, /* Priority of the task */ NULL, /* Task handle. */ @@ -470,26 +658,33 @@ void MI32StartScanTask(){ void MI32ScanTask(void *pvParameters){ if (MI32Scan == nullptr) MI32Scan = NimBLEDevice::getScan(); DEBUG_SENSOR_LOG(PSTR("%s: Scan Cache Length: %u"),D_CMND_MI32, MI32Scan->getResults().getCount()); - MI32Scan->setAdvertisedDeviceCallbacks(&MI32ScanCallbacks); + MI32Scan->setInterval(70); + MI32Scan->setWindow(50); + MI32Scan->setAdvertisedDeviceCallbacks(&MI32ScanCallbacks,true); MI32Scan->setActiveScan(false); - MI32Scan->start(5, MI32scanEndedCB, true); // hard coded duration + MI32Scan->start(0, MI32scanEndedCB, true); // never stop scanning, will pause automaically while connecting + uint32_t timer = 0; - while (MI32.mode.runningScan){ - if (timer>15){ - vTaskDelete( NULL ); + for(;;){ + if(MI32.mode.shallClearResults){ + MI32Scan->clearResults(); + MI32.mode.shallClearResults=0; } - timer++; - vTaskDelay(1000/ portTICK_PERIOD_MS); + vTaskDelay(10000/ portTICK_PERIOD_MS); } vTaskDelete( NULL ); } void MI32StartSensorTask(){ MI32.mode.willConnect = 1; + if (MIBLEsensors[MI32.state.sensor].type != LYWSD03MMC) { + MI32.mode.willConnect = 0; + return; + } xTaskCreatePinnedToCore( MI32SensorTask, /* Function to implement the task */ "MI32SensorTask", /* Name of the task */ - 8192, /* Stack size in words */ + 4096, /* Stack size in words */ NULL, /* Task input parameter */ 15, /* Priority of the task */ NULL, /* Task handle. */ @@ -499,16 +694,12 @@ void MI32StartSensorTask(){ } void MI32SensorTask(void *pvParameters){ - if (MIBLEsensors[MI32.state.sensor].type != LYWSD03MMC) { - MI32.mode.willConnect = 0; - vTaskDelete( NULL ); - } if (MI32ConnectActiveSensor()){ uint32_t timer = 0; while (MI32.mode.connected == 0){ if (timer>1000){ MI32Client->disconnect(); - NimBLEDevice::deleteClient(MI32Client); + // NimBLEDevice::deleteClient(MI32Client); MI32.mode.willConnect = 0; vTaskDelay(100/ portTICK_PERIOD_MS); vTaskDelete( NULL ); @@ -537,7 +728,7 @@ void MI32SensorTask(void *pvParameters){ MI32Client->disconnect(); DEBUG_SENSOR_LOG(PSTR("%s: requested disconnect"),D_CMND_MI32); } - vTaskDelay(500/ portTICK_PERIOD_MS); + MI32.mode.connected = 0; vTaskDelete( NULL ); } @@ -566,7 +757,7 @@ void MI32StartTimeTask(){ xTaskCreatePinnedToCore( MI32TimeTask, /* Function to implement the task */ "MI32TimeTask", /* Name of the task */ - 8912, /* Stack size in words */ + 4096, /* Stack size in words */ NULL, /* Task input parameter */ 15, /* Priority of the task */ NULL, /* Task handle. */ @@ -621,8 +812,9 @@ void MI32TimeTask(void *pvParameters){ } MI32Client->disconnect(); } - vTaskDelay(500/ portTICK_PERIOD_MS); + MI32.mode.connected = 0; + MI32.mode.canScan = 1; vTaskDelete( NULL ); } @@ -631,7 +823,7 @@ void MI32StartUnitTask(){ xTaskCreatePinnedToCore( MI32UnitTask, /* Function to implement the task */ "MI32UnitTask", /* Name of the task */ - 8912, /* Stack size in words */ + 4096, /* Stack size in words */ NULL, /* Task input parameter */ 15, /* Priority of the task */ NULL, /* Task handle. */ @@ -658,8 +850,8 @@ void MI32UnitTask(void *pvParameters){ NimBLERemoteService* pSvc = nullptr; NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID serviceUUID("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6"); - static BLEUUID charUUID("EBE0CCBE-7A0A-4B0C-8A1A-6FF2997DA3A6"); + static BLEUUID serviceUUID(0xEBE0CCB0,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); + static BLEUUID charUUID(0xEBE0CCBE,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); pSvc = MI32Client->getService(serviceUUID); if(pSvc) { pChr = pSvc->getCharacteristic(charUUID); @@ -687,8 +879,9 @@ void MI32UnitTask(void *pvParameters){ } MI32Client->disconnect(); } - vTaskDelay(500/ portTICK_PERIOD_MS); + MI32.mode.connected = 0; + MI32.mode.canScan = 1; vTaskDelete( NULL ); } @@ -697,10 +890,18 @@ void MI32StartBatteryTask(){ MI32.mode.willReadBatt = 1; MI32.mode.willConnect = 1; MI32.mode.canScan = 0; + + switch (MIBLEsensors[MI32.state.sensor].type){ + case LYWSD03MMC: case MJ_HT_V1: case CGG1: case NLIGHT: case MJYD2S: case YEERC: + MI32.mode.willConnect = 0; + MI32.mode.willReadBatt = 0; + return; + } + xTaskCreatePinnedToCore( MI32BatteryTask, /* Function to implement the task */ "MI32BatteryTask", /* Name of the task */ - 8192, /* Stack size in words */ + 4096, /* Stack size in words */ NULL, /* Task input parameter */ 15, /* Priority of the task */ NULL, /* Task handle. */ @@ -709,15 +910,6 @@ void MI32StartBatteryTask(){ void MI32BatteryTask(void *pvParameters){ // all reported battery values are probably crap, but we allow the reading on demand - switch (MIBLEsensors[MI32.state.sensor].type){ - case LYWSD03MMC: case MJ_HT_V1: case CGG1: - MI32.mode.willConnect = 0; - MI32.mode.willReadBatt = 0; - vTaskDelete( NULL ); - break; - default: - break; - } MI32.mode.connected = 0; if(MI32ConnectActiveSensor()){ @@ -731,26 +923,18 @@ void MI32BatteryTask(void *pvParameters){ } switch(MIBLEsensors[MI32.state.sensor].type){ - case FLORA: - MI32batteryFLORA(); - break; - case LYWSD02: - MI32batteryLYWSD02(); - break; - case CGD1: - MI32batteryCGD1(); + case FLORA: case LYWSD02: case CGD1: + MI32batteryRead(MIBLEsensors[MI32.state.sensor].type); break; } MI32Client->disconnect(); } MI32.mode.willReadBatt = 0; - // Wifi.counter = 0; // Now check it - vTaskDelay(500/ portTICK_PERIOD_MS); MI32.mode.connected = 0; vTaskDelete( NULL ); } -void MI32batteryFLORA(){ +void MI32batteryRead(uint32_t _type){ uint32_t timer = 0; while (!MI32.mode.connected){ if (timer>1000){ @@ -759,79 +943,45 @@ void MI32batteryFLORA(){ timer++; vTaskDelay(10/ portTICK_PERIOD_MS); } - DEBUG_SENSOR_LOG(PSTR("%s connected for battery"),kMI32SlaveType[MIBLEsensors[MI32.state.sensor].type-1] ); + DEBUG_SENSOR_LOG(PSTR("%s connected for battery"),kMI32DeviceType[MIBLEsensors[MI32.state.sensor].type-1] ); NimBLERemoteService* pSvc = nullptr; NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID FLserviceUUID(0x00001204,0x0000,0x1000,0x800000805f9b34fb); - static BLEUUID FLcharUUID(0x00001a02,0x0000,0x1000,0x800000805f9b34fb); - pSvc = MI32Client->getService(FLserviceUUID); - if(pSvc) { - pChr = pSvc->getCharacteristic(FLcharUUID); - } - if (pChr){ - DEBUG_SENSOR_LOG(PSTR("%s: got Flora char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); - if(pChr->canRead()) { - const char *buf = pChr->readValue().c_str(); - MI32readBat((char*)buf); - //we also can read the firmware from response no extra request needed - MI32readFirmwareFLORA((char*)buf); - } - } - MI32.mode.readingDone = 1; -} - -void MI32batteryLYWSD02(){ - uint32_t timer = 0; - while (!MI32.mode.connected){ - if (timer>1000){ - break; + switch(_type){ + case FLORA: + { + static BLEUUID _serviceUUID(0x00001204,0x0000,0x1000,0x800000805f9b34fb); + static BLEUUID _charUUID(0x00001a02,0x0000,0x1000,0x800000805f9b34fb); + pSvc = MI32Client->getService(_serviceUUID); + if(pSvc) { + pChr = pSvc->getCharacteristic(_charUUID); } - timer++; - vTaskDelay(10/ portTICK_PERIOD_MS); } - - NimBLERemoteService* pSvc = nullptr; - NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID LY2serviceUUID(0xEBE0CCB0,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); - static BLEUUID LY2charUUID(0xEBE0CCC4,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); - - pSvc = MI32Client->getService(LY2serviceUUID); - if(pSvc) { - pChr = pSvc->getCharacteristic(LY2charUUID); - } - if (pChr){ - DEBUG_SENSOR_LOG( PSTR("%s: got LYWSD02 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); - if(pChr->canRead()) { - DEBUG_SENSOR_LOG(PSTR("LYWSD02 char")); - const char *buf = pChr->readValue().c_str(); - MI32readBat((char*)buf); - } - } - MI32.mode.readingDone = 1; -} - -void MI32batteryCGD1(){ - uint32_t timer = 0; - while (!MI32.mode.connected){ - if (timer>1000){ - break; + break; + case LYWSD02: + { + static BLEUUID _serviceUUID(0xEBE0CCB0,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); + static BLEUUID _charUUID(0xEBE0CCC4,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); + pSvc = MI32Client->getService(_serviceUUID); + if(pSvc) { + pChr = pSvc->getCharacteristic(_charUUID); } - timer++; - vTaskDelay(10/ portTICK_PERIOD_MS); } - - NimBLERemoteService* pSvc = nullptr; - NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID CGD1serviceUUID((uint16_t)0x180F); - static BLEUUID CGD1charUUID((uint16_t)0x2A19); - - pSvc = MI32Client->getService(CGD1serviceUUID); - if(pSvc) { - pChr = pSvc->getCharacteristic(CGD1charUUID); + break; + case CGD1: + { + static BLEUUID _serviceUUID((uint16_t)0x180F); + static BLEUUID _charUUID((uint16_t)0x2A19); + pSvc = MI32Client->getService(_serviceUUID); + if(pSvc) { + pChr = pSvc->getCharacteristic(_charUUID); + } + } + break; } + if (pChr){ - DEBUG_SENSOR_LOG(PSTR("%s: got CGD1 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); + DEBUG_SENSOR_LOG(PSTR("%s: got %s char %s"),D_CMND_MI32, kMI32DeviceType[MIBLEsensors[MI32.state.sensor].type-1], pChr->getUUID().toString().c_str()); if(pChr->canRead()) { const char *buf = pChr->readValue().c_str(); MI32readBat((char*)buf); @@ -840,87 +990,166 @@ void MI32batteryCGD1(){ MI32.mode.readingDone = 1; } - /*********************************************************************************************\ * parse the response from advertisements \*********************************************************************************************/ -void MI32parseMiBeacon(char * _buf, uint32_t _slot){ +void MI32parseMiBeacon(char * _buf, uint32_t _slot, uint16_t _bufSize){ float _tempFloat; mi_beacon_t _beacon; - if (MIBLEsensors[_slot].type==MJ_HT_V1 || MIBLEsensors[_slot].type==CGG1){ - memcpy((uint8_t*)&_beacon+1,(uint8_t*)_buf, sizeof(_beacon)); // shift by one byte for the MJ_HT_V1 - memcpy((uint8_t*)&_beacon.Mac,(uint8_t*)&_beacon.Mac+1,6); // but shift back the MAC + + if (MIBLEsensors[_slot].type==MJ_HT_V1 || MIBLEsensors[_slot].type==CGG1 || MIBLEsensors[_slot].type==YEERC){ + memcpy((uint8_t*)&_beacon+1,(uint8_t*)_buf, sizeof(_beacon)-1); // shift by one byte for the MJ_HT_V1 DANGER!!! + memcpy((uint8_t*)&_beacon.MAC,(uint8_t*)&_beacon.MAC+1,6); // but shift back the MAC + _beacon.counter = _buf[4]; // restore the counter } else{ - memcpy((void*)&_beacon,(void*)_buf, sizeof(_beacon)); + memcpy((char *)&_beacon, _buf, _bufSize); } - MI32_ReverseMAC(_beacon.Mac); - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MiBeacon type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[0],(uint8_t)_buf[1],(uint8_t)_buf[2],(uint8_t)_buf[3],(uint8_t)_buf[4],(uint8_t)_buf[5],(uint8_t)_buf[6],(uint8_t)_buf[7]); - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR(" type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[8],(uint8_t)_buf[9],(uint8_t)_buf[10],(uint8_t)_buf[11],(uint8_t)_buf[12],(uint8_t)_buf[13],(uint8_t)_buf[14],(uint8_t)_buf[15]); + MIBLEsensors[_slot].lastCnt = _beacon.counter; +#ifdef USE_MI_DECRYPTION + switch(MIBLEsensors[_slot].type){ + case LYWSD03MMC: + if (_beacon.frame == 0x5858){ + int decryptRet = MI32_decryptPacket((char*)&_beacon.productID,_bufSize, LYWSD03MMC); //start with PID + } + break; + case MJYD2S: + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MJYD2S: %x"),_beacon.frame); + if (_beacon.frame == 0x5948){ // Now let's build/recreate a special MiBeacon + memmove((uint8_t*)&_beacon.MAC+6,(uint8_t*)&_beacon.MAC, _bufSize); // shift payload by the size of the MAC = 6 bytes + memcpy((uint8_t*)&_beacon.MAC,MIBLEsensors[_slot].MAC,6); // now insert the real MAC from our internal vector + _bufSize+=6; // the packet has grown + MI32_ReverseMAC(_beacon.MAC); // payload MAC is always reversed + } + if (_beacon.frame != 0x5910){ + int decryptRet = MI32_decryptPacket((char*)&_beacon.productID,_bufSize,MJYD2S); //start with PID + } + else{ + if(millis()-MIBLEsensors[_slot].lastTime>120000){ + MIBLEsensors[_slot].eventType = 1; + MIBLEsensors[_slot].events++; + MIBLEsensors[_slot].shallSendMQTT = 1; + MIBLEsensors[_slot].lastTime = millis(); + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MI32: MJYD2S secondary PIR")); + MIBLEsensors[_slot].NMT = 0; + MI32triggerTele(); + } + } + break; + } +#endif //USE_MI_DECRYPTION - if(MIBLEsensors[_slot].type==4 || MIBLEsensors[_slot].type==6){ +if (MIBLEsensors[_slot].type==NLIGHT){ + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MiBeacon type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[0],(uint8_t)_buf[1],(uint8_t)_buf[2],(uint8_t)_buf[3],(uint8_t)_buf[4],(uint8_t)_buf[5],(uint8_t)_buf[6],(uint8_t)_buf[7]); + AddLog_P2(LOG_LEVEL_DEBUG,PSTR(" type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[8],(uint8_t)_buf[9],(uint8_t)_buf[10],(uint8_t)_buf[11],(uint8_t)_buf[12],(uint8_t)_buf[13],(uint8_t)_buf[14],(uint8_t)_buf[15]); +} + + if(MIBLEsensors[_slot].type==6){ DEBUG_SENSOR_LOG(PSTR("LYWSD03 and CGD1 no support for MiBeacon, type %u"),MIBLEsensors[_slot].type); return; } - AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s at slot %u"), kMI32SlaveType[MIBLEsensors[_slot].type-1],_slot); + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); switch(_beacon.type){ + case 0x01: + MIBLEsensors[_slot].Btn=_beacon.Btn.num + (_beacon.Btn.longPress/2)*6; + MIBLEsensors[_slot].shallSendMQTT = 1; + MI32triggerTele(); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 1: U16: %u Button"), MIBLEsensors[_slot].Btn ); + break; case 0x04: - _tempFloat=(float)(_beacon.temp)/10.0f; - if(_tempFloat<60){ - MIBLEsensors[_slot].temp=_tempFloat; - DEBUG_SENSOR_LOG(PSTR("Mode 4: temp updated")); - } - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 4: U16: %u Temp"), _beacon.temp ); + _tempFloat=(float)(_beacon.temp)/10.0f; + if(_tempFloat<60){ + MIBLEsensors[_slot].temp=_tempFloat; + DEBUG_SENSOR_LOG(PSTR("Mode 4: temp updated")); + } + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 4: U16: %u Temp"), _beacon.temp ); break; case 0x06: - _tempFloat=(float)(_beacon.hum)/10.0f; - if(_tempFloat<101){ - MIBLEsensors[_slot].hum=_tempFloat; - DEBUG_SENSOR_LOG(PSTR("Mode 6: hum updated")); - } - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 6: U16: %u Hum"), _beacon.hum); + _tempFloat=(float)(_beacon.hum)/10.0f; + if(_tempFloat<101){ + MIBLEsensors[_slot].hum=_tempFloat; + DEBUG_SENSOR_LOG(PSTR("Mode 6: hum updated")); + } + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 6: U16: %u Hum"), _beacon.hum); break; case 0x07: - MIBLEsensors[_slot].lux=_beacon.lux & 0x00ffffff; - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 7: U24: %u Lux"), _beacon.lux & 0x00ffffff); + MIBLEsensors[_slot].lux=_beacon.lux & 0x00ffffff; + if(MIBLEsensors[_slot].type==MJYD2S){ + MIBLEsensors[_slot].eventType = 2; //No PIR + MIBLEsensors[_slot].shallSendMQTT = 1; + MIBLEsensors[_slot].lastTime = millis(); + MI32triggerTele(); + } + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 7: U24: %u Lux"), _beacon.lux & 0x00ffffff); break; case 0x08: - _tempFloat =(float)_beacon.moist; - if(_tempFloat<100){ - MIBLEsensors[_slot].moisture=_tempFloat; - DEBUG_SENSOR_LOG(PSTR("Mode 8: moisture updated")); - } - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 8: U8: %u Moisture"), _beacon.moist); + _tempFloat =(float)_beacon.moist; + if(_tempFloat<100){ + MIBLEsensors[_slot].moisture=_tempFloat; + DEBUG_SENSOR_LOG(PSTR("Mode 8: moisture updated")); + } + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 8: U8: %u Moisture"), _beacon.moist); break; case 0x09: - _tempFloat=(float)(_beacon.fert); - if(_tempFloat<65535){ // ??? - MIBLEsensors[_slot].fertility=_tempFloat; - DEBUG_SENSOR_LOG(PSTR("Mode 9: fertility updated")); - } - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 9: U16: %u Fertility"), _beacon.fert); + _tempFloat=(float)(_beacon.fert); + if(_tempFloat<65535){ // ??? + MIBLEsensors[_slot].fertility=_tempFloat; + DEBUG_SENSOR_LOG(PSTR("Mode 9: fertility updated")); + } + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 9: U16: %u Fertility"), _beacon.fert); break; case 0x0a: - if(_beacon.bat<101){ - MIBLEsensors[_slot].bat = _beacon.bat; - DEBUG_SENSOR_LOG(PSTR("Mode a: bat updated")); - } - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode a: U8: %u %%"), _beacon.bat); + if(_beacon.bat<101){ + MIBLEsensors[_slot].bat = _beacon.bat; + DEBUG_SENSOR_LOG(PSTR("Mode a: bat updated")); + } + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode a: U8: %u %%"), _beacon.bat); break; case 0x0d: - _tempFloat=(float)(_beacon.HT.temp)/10.0f; - if(_tempFloat<60){ - MIBLEsensors[_slot].temp = _tempFloat; - DEBUG_SENSOR_LOG(PSTR("Mode d: temp updated")); - } - _tempFloat=(float)(_beacon.HT.hum)/10.0f; - if(_tempFloat<100){ - MIBLEsensors[_slot].hum = _tempFloat; - DEBUG_SENSOR_LOG(PSTR("Mode d: hum updated")); - } - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode d: U16: %x Temp U16: %x Hum"), _beacon.HT.temp, _beacon.HT.hum); + _tempFloat=(float)(_beacon.HT.temp)/10.0f; + if(_tempFloat<60){ + MIBLEsensors[_slot].temp = _tempFloat; + DEBUG_SENSOR_LOG(PSTR("Mode d: temp updated")); + } + _tempFloat=(float)(_beacon.HT.hum)/10.0f; + if(_tempFloat<100){ + MIBLEsensors[_slot].hum = _tempFloat; + DEBUG_SENSOR_LOG(PSTR("Mode d: hum updated")); + } + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode d: U16: %x Temp U16: %x Hum"), _beacon.HT.temp, _beacon.HT.hum); + break; +#ifdef USE_MI_DECRYPTION + case 0x0f: + if (_beacon.ten!=0) break; + MIBLEsensors[_slot].eventType = 1; //PIR + MIBLEsensors[_slot].shallSendMQTT = 1; + MIBLEsensors[_slot].lastTime = millis(); + MIBLEsensors[_slot].events++; + MIBLEsensors[_slot].lux = _beacon.lux; + MIBLEsensors[_slot].NMT = 0; + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("PIR: primary"),MIBLEsensors[_slot].lux ); + MI32triggerTele(); + break; + case 0x17: + MIBLEsensors[_slot].NMT = _beacon.NMT; + MIBLEsensors[_slot].eventType = 3; // NMT + MIBLEsensors[_slot].shallSendMQTT = 1; + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 17: NMT: %u seconds"), _beacon.NMT); + MI32triggerTele(); + break; +#endif //USE_MI_DECRYPTION + default: + if (MIBLEsensors[_slot].type==NLIGHT){ + MIBLEsensors[_slot].eventType = 1; //PIR + MIBLEsensors[_slot].shallSendMQTT = 1; + MIBLEsensors[_slot].events++; + MIBLEsensors[_slot].NMT = 0; + MIBLEsensors[_slot].lastTime = millis(); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("PIR: primary"),MIBLEsensors[_slot].lux ); + MI32triggerTele(); + } break; } } @@ -928,8 +1157,8 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ void MI32parseCGD1Packet(char * _buf, uint32_t length, uint8_t addr[6]){ // no MiBeacon uint8_t _addr[6]; memcpy(_addr,addr,6); - uint32_t _slot = MIBLEgetSensorSlot(_addr, 0x0576); // This must be hard-coded, no object-id in Cleargrass-packet - DEBUG_SENSOR_LOG(PSTR("MI32: Sensor slot: %u"), _slot); + uint32_t _slot = MIBLEgetSensorSlot(_addr, 0x0576, 0); // This must be hard-coded, no object-id in Cleargrass-packet, we have no packet counter too + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); if(_slot==0xff) return; cg_packet_t _packet; memcpy((char*)&_packet,_buf,sizeof(_packet)); @@ -960,16 +1189,15 @@ void MI32parseCGD1Packet(char * _buf, uint32_t length, uint8_t addr[6]){ // no M } void MI32ParseResponse(char *buf, uint16_t bufsize, uint8_t addr[6]) { - if(bufsize<10) { + if(bufsize<9) { //9 is from the NLIGHT return; } - char * _pos = buf; - uint16_t _type= _pos[3]*256 + _pos[2]; + uint16_t _type= buf[3]*256 + buf[2]; // AddLog_P2(LOG_LEVEL_INFO, PSTR("%02x %02x %02x %02x"),(uint8_t)buf[0], (uint8_t)buf[1],(uint8_t)buf[2],(uint8_t)buf[3]); uint8_t _addr[6]; memcpy(_addr,addr,6); - uint16_t _slot = MIBLEgetSensorSlot(_addr, _type); - if(_slot!=0xff) MI32parseMiBeacon(_pos,_slot); + uint16_t _slot = MIBLEgetSensorSlot(_addr, _type, buf[4]); + if(_slot!=0xff) MI32parseMiBeacon(buf,_slot,bufsize); } /***********************************************************************\ @@ -988,7 +1216,7 @@ void MI32readHT_LY(char *_buf){ _tempFloat=(float)(LYWSD0x_HT.temp)/100.0f; if(_tempFloat<60){ MIBLEsensors[_slot].temp=_tempFloat; - MIBLEsensors[_slot].showedUp=255; // this sensor is real + // MIBLEsensors[_slot].showedUp=255; // this sensor is real } _tempFloat=(float)LYWSD0x_HT.hum; if(_tempFloat<100){ @@ -996,7 +1224,7 @@ void MI32readHT_LY(char *_buf){ DEBUG_SENSOR_LOG(PSTR("LYWSD0x: hum updated")); } if (MIBLEsensors[_slot].type == LYWSD03MMC){ - MIBLEsensors[_slot].volt = LYWSD0x_HT.volt; + MIBLEsensors[_slot].bat = ((float)LYWSD0x_HT.volt-2100.0f)/12.0f; } } } @@ -1009,29 +1237,17 @@ bool MI32readBat(char *_buf){ DEBUG_SENSOR_LOG(PSTR("MIBLE: Sensor slot: %u"), _slot); if(_buf[0]<101){ MIBLEsensors[_slot].bat=_buf[0]; + if(MIBLEsensors[_slot].type==FLORA){ + memcpy(MIBLEsensors[_slot].firmware, _buf+2, 5); + MIBLEsensors[_slot].firmware[5] = '\0'; + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: Firmware: %s"),D_CMND_MI32,MIBLEsensors[_slot].firmware); + } return true; } } return false; } -bool MI32readFirmwareFLORA(char *_buf){ - DEBUG_SENSOR_LOG(PSTR("%s: raw data: %x%x%x%x%x%x%x"),D_CMND_MI32,_buf[0],_buf[1],_buf[2],_buf[3],_buf[4],_buf[5],_buf[6]); - if(_buf[0] != 0){ - char _firmware[5]; // FLORA send 5 byte for firmware version - strncpy(_firmware, _buf+2, 5); - AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: Firmware: %s"),D_CMND_MI32,_firmware); - - uint32_t _slot = MI32.state.sensor; - DEBUG_SENSOR_LOG(PSTR("MIBLE: Sensor slot: %u"), _slot); - - memcpy(MIBLEsensors[_slot].firmware, _firmware, 5); - MIBLEsensors[_slot].firmware[5] = '\0'; - return true; - } - return false; -} - /** * @brief Main loop of the driver, "high level"-loop * @@ -1041,6 +1257,12 @@ void MI32EverySecond(bool restart){ static uint32_t _counter = MI32.period - 15; static uint32_t _nextSensorSlot = 0; + for (uint32_t i = 0; i < MIBLEsensors.size(); i++) { + if(MIBLEsensors[i].type==NLIGHT || MIBLEsensors[i].type==MJYD2S){ + MIBLEsensors[i].NMT++; + } + } + if(restart){ _counter = 0; MI32.mode.canScan = 0; @@ -1084,22 +1306,25 @@ void MI32EverySecond(bool restart){ } if(_counter==0) { + MI32.state.sensor = _nextSensorSlot; AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: active sensor now: %u of %u"),D_CMND_MI32, MI32.state.sensor, MIBLEsensors.size()-1); MI32.mode.canScan = 0; - if (MI32.mode.runningScan|| MI32.mode.connected || MI32.mode.willConnect) return; + // if (MI32.mode.runningScan|| MI32.mode.connected || MI32.mode.willConnect) return; + if (MI32.mode.connected || MI32.mode.willConnect) return; _nextSensorSlot++; MI32.mode.canConnect = 1; if(MI32.mode.connected == 0) { - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("will connect to %s"),kMI32SlaveType[MIBLEsensors[MI32.state.sensor].type-1] ); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("will connect to %s"),kMI32DeviceType[MIBLEsensors[MI32.state.sensor].type-1] ); if (MI32.mode.shallReadBatt) { MI32StartTask(MI32_TASK_BATT); - } + } +#ifndef USE_MI_DECRYPTION // turn off connections, because we only listen to advertisements else{ MI32StartTask(MI32_TASK_CONN); - } - + } +#endif //USE_MI_DECRYPTION } if (_nextSensorSlot>(MIBLEsensors.size()-1)) { _nextSensorSlot= 0; @@ -1191,6 +1416,14 @@ bool MI32Cmd(void) { XdrvMailbox.payload = MI32.period; Response_P(S_JSON_MI32_COMMAND, command, ""); break; +#ifdef USE_MI_DECRYPTION + case CMND_MI32_KEY: + if (XdrvMailbox.data_len==44){ // a KEY-MAC-string + MI32AddKey(XdrvMailbox.data); + Response_P(S_JSON_MI32_COMMAND, command, XdrvMailbox.data); + } + break; +#endif //USE_MI_DECRYPTION default: // else for Unknown command serviced = false; @@ -1211,23 +1444,36 @@ const char HTTP_MI32[] PROGMEM = "{s}MI ESP32 {m}%u%s / %u{e}"; const char HTTP_MI32_SERIAL[] PROGMEM = "{s}%s %s{m}%02x:%02x:%02x:%02x:%02x:%02x%{e}"; const char HTTP_BATTERY[] PROGMEM = "{s}%s" " Battery" "{m}%u %%{e}"; const char HTTP_VOLTAGE[] PROGMEM = "{s}%s " D_VOLTAGE "{m}%s V{e}"; +const char HTTP_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}"; void MI32Show(bool json) { - if (json) { + if(!MI32.mode.triggeredTele){ + MI32.mode.shallClearResults=1; + } for (uint32_t i = 0; i < MIBLEsensors.size(); i++) { + switch(MIBLEsensors[i].type){ + case YEERC: + if(MIBLEsensors[i].shallSendMQTT==0) continue; + break; + default: + if(MI32.mode.triggeredTele) continue; + break; + } /* char slave[33]; snprintf_P(slave, sizeof(slave), PSTR("%s-%02x%02x%02x"), - kMI32SlaveType[MIBLEsensors[i].type-1],MIBLEsensors[i].serial[3],MIBLEsensors[i].serial[4],MIBLEsensors[i].serial[5]); + kMI32DeviceType[MIBLEsensors[i].type-1],MIBLEsensors[i].serial[3],MIBLEsensors[i].serial[4],MIBLEsensors[i].serial[5]); ResponseAppend_P(PSTR(",\"%s\":{"), slave); */ ResponseAppend_P(PSTR(",\"%s-%02x%02x%02x\":{"), - kMI32SlaveType[MIBLEsensors[i].type-1], - MIBLEsensors[i].serial[3], MIBLEsensors[i].serial[4], MIBLEsensors[i].serial[5]); + kMI32DeviceType[MIBLEsensors[i].type-1], + MIBLEsensors[i].MAC[3], MIBLEsensors[i].MAC[4], MIBLEsensors[i].MAC[5]); if (MIBLEsensors[i].type == FLORA) { if (!isnan(MIBLEsensors[i].temp)) { @@ -1247,25 +1493,35 @@ void MI32Show(bool json) if (!isnan(MIBLEsensors[i].fertility)) { ResponseAppend_P(PSTR(",\"Fertility\":%f"), MIBLEsensors[i].fertility); } + ResponseAppend_P(PSTR(",\"Firmware\":\"%s\""), MIBLEsensors[i].firmware); } if (MIBLEsensors[i].type > FLORA){ if (!isnan(MIBLEsensors[i].hum) && !isnan(MIBLEsensors[i].temp)) { ResponseAppendTHD(MIBLEsensors[i].temp, MIBLEsensors[i].hum); } } +#ifdef USE_MI_DECRYPTION + if (MIBLEsensors[i].type == MJYD2S){ + ResponseAppend_P(PSTR("\"Events\":%u"),MIBLEsensors[i].events); + if(MIBLEsensors[i].shallSendMQTT && MIBLEsensors[i].eventType<3) ResponseAppend_P(PSTR(",\"PIR\":%u"), 2 - MIBLEsensors[i].eventType); + if(MIBLEsensors[i].eventType==3) ResponseAppend_P(PSTR(",\"NMT\":%u"), MIBLEsensors[i].NMT); + MIBLEsensors[i].eventType=0; + if(MIBLEsensors[i].lux!=0x0ffffff) ResponseAppend_P(PSTR(",\"" D_JSON_ILLUMINANCE "\":%u"), MIBLEsensors[i].lux); + } +#endif //USE_MI_DECRYPTION + if (MIBLEsensors[i].type == NLIGHT){ + ResponseAppend_P(PSTR("\"Events\":%u"),MIBLEsensors[i].events); + if(MIBLEsensors[i].shallSendMQTT) ResponseAppend_P(PSTR(",\"PIR\":1")); + } + if (MIBLEsensors[i].type == YEERC){ + if(MIBLEsensors[i].shallSendMQTT) ResponseAppend_P(PSTR("\"Btn\":%u"),MIBLEsensors[i].Btn); + } if (MIBLEsensors[i].bat != 0x00) { // this is the error code -> no battery - if (MIBLEsensors[i].type != LYWSD03MMC) { - ResponseAppend_P(PSTR(",\"Battery\":%u"), MIBLEsensors[i].bat); - } else { - char voltage[FLOATSZ]; - dtostrfd((MIBLEsensors[i].volt)/1000.0f, Settings.flag2.voltage_resolution, voltage); - ResponseAppend_P(PSTR(",\"" D_VOLTAGE "\":%s"), voltage); - } - if (MIBLEsensors[i].type == FLORA) { //actually we can only read FLORA - ResponseAppend_P(PSTR(",\"Firmware\":\"%s\""), MIBLEsensors[i].firmware); - } + ResponseAppend_P(PSTR(",\"Battery\":%u"), MIBLEsensors[i].bat); } ResponseAppend_P(PSTR("}")); + MIBLEsensors[i].shallSendMQTT = 0; + MI32.mode.triggeredTele = 0; } #ifdef USE_WEBSERVER } else { @@ -1285,36 +1541,41 @@ void MI32Show(bool json) WSContentSend_PD(HTTP_MI32, i+1,stemp,MIBLEsensors.size()); for (i; i no valid value - WSContentSend_PD(HTTP_SNS_ILLUMINANCE, kMI32SlaveType[MIBLEsensors[i].type-1], MIBLEsensors[i].lux); + WSContentSend_PD(HTTP_SNS_TEMP, kMI32DeviceType[MIBLEsensors[i].type-1], temperature, TempUnit()); } if (!isnan(MIBLEsensors[i].moisture)) { - WSContentSend_PD(HTTP_SNS_MOISTURE, kMI32SlaveType[MIBLEsensors[i].type-1], int(MIBLEsensors[i].moisture)); + WSContentSend_PD(HTTP_SNS_MOISTURE, kMI32DeviceType[MIBLEsensors[i].type-1], int(MIBLEsensors[i].moisture)); } if (!isnan(MIBLEsensors[i].fertility)) { - WSContentSend_PD(HTTP_MI32_FLORA_DATA, kMI32SlaveType[MIBLEsensors[i].type-1], int(MIBLEsensors[i].fertility)); + WSContentSend_PD(HTTP_MI32_FLORA_DATA, kMI32DeviceType[MIBLEsensors[i].type-1], int(MIBLEsensors[i].fertility)); } } if (MIBLEsensors[i].type>FLORA) { // everything "above" Flora if (!isnan(MIBLEsensors[i].hum) && !isnan(MIBLEsensors[i].temp)) { - WSContentSend_THD(kMI32SlaveType[MIBLEsensors[i].type-1], MIBLEsensors[i].temp, MIBLEsensors[i].hum); + WSContentSend_THD(kMI32DeviceType[MIBLEsensors[i].type-1], MIBLEsensors[i].temp, MIBLEsensors[i].hum); } } +#ifdef USE_MI_DECRYPTION + if (MIBLEsensors[i].type==NLIGHT || MIBLEsensors[i].type==MJYD2S) { +#else + if (MIBLEsensors[i].type==NLIGHT) { +#endif //USE_MI_DECRYPTION + WSContentSend_PD(HTTP_EVENTS, kMI32DeviceType[MIBLEsensors[i].type-1], MIBLEsensors[i].events); + if(MIBLEsensors[i].NMT>0) WSContentSend_PD(HTTP_NMT, kMI32DeviceType[MIBLEsensors[i].type-1], MIBLEsensors[i].NMT); + } + if (MIBLEsensors[i].lux!=0x00ffffff) { // this is the error code -> no valid value + WSContentSend_PD(HTTP_SNS_ILLUMINANCE, kMI32DeviceType[MIBLEsensors[i].type-1], MIBLEsensors[i].lux); + } if(MIBLEsensors[i].bat!=0x00){ - if (MIBLEsensors[i].type != LYWSD03MMC) { - WSContentSend_PD(HTTP_BATTERY, kMI32SlaveType[MIBLEsensors[i].type-1], MIBLEsensors[i].bat); - } else { - char voltage[FLOATSZ]; - dtostrfd((MIBLEsensors[i].volt)/1000.0f, Settings.flag2.voltage_resolution, voltage); - WSContentSend_PD(HTTP_VOLTAGE, kMI32SlaveType[MIBLEsensors[i].type-1], voltage); - } + WSContentSend_PD(HTTP_BATTERY, kMI32DeviceType[MIBLEsensors[i].type-1], MIBLEsensors[i].bat); + } + if (MIBLEsensors[i].type==YEERC){ + WSContentSend_PD(HTTP_LASTBUTTON, kMI32DeviceType[MIBLEsensors[i].type-1], MIBLEsensors[i].Btn); } } _counter++; @@ -1360,4 +1621,4 @@ bool Xsns62(uint8_t function) return result; } #endif // USE_MI_ESP32 -#endif // ESP32 \ No newline at end of file +#endif // ESP32