/* support_rotary.ino - rotary switch support for Tasmota Copyright (C) 2021 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 ROTARY_V1 /*********************************************************************************************\ * Rotary support * * Supports full range in 10 steps of the Rotary Encoder: * - Light Dimmer * - Light Color for RGB lights when Button1 pressed * - Light Color Temperature for CW lights when Button1 pressed * * _______ _______ * GPIO_ROT1A ______| |_______| |______ GPIO_ROT1A * negative <-- _______ _______ __ --> positive * GPIO_ROT1B __| |_______| |_______| GPIO_ROT1B * \*********************************************************************************************/ #ifndef ROTARY_MAX_STEPS #define ROTARY_MAX_STEPS 10 // Rotary step boundary #endif #ifndef ROTARY_START_DIM #define ROTARY_START_DIM 1 // Minimal dimmer value after power on with SetOption113 1 #endif #ifndef ROTARY_TIMEOUT #define ROTARY_TIMEOUT 2 // 2 * RotaryHandler() call which is usually 2 * 0.05 seconds #endif #ifndef ROTARY_DEBOUNCE #define ROTARY_DEBOUNCE 10 // Debounce time in milliseconds #endif const uint8_t rotary_offset = 128; #ifdef ESP8266 const int8_t rotary_state_pos[16] = { 0, 1, -1, 2, -1, 0, -2, 1, 1, -2, 0, -1, 2, -1, 1, 0 }; #endif // ESP8266 struct ROTARY { uint8_t no_pullup_mask_a = 0; // Rotary A pull-up bitmask flags uint8_t no_pullup_mask_b = 0; // Rotary B pull-up bitmask flags uint8_t dimmer_increment; uint8_t ct_increment; uint8_t color_increment; uint8_t model; bool present; } Rotary; struct tEncoder { volatile uint32_t debounce = 0; volatile uint8_t state = 0; volatile uint8_t position; volatile int8_t direction = 0; // Control consistent direction volatile int8_t pina; volatile int8_t pinb; uint8_t timeout = 0; // Disallow direction change within 0.5 second int8_t abs_position[2] = { 0 }; bool changed = false; }; tEncoder Encoder[MAX_ROTARIES]; /********************************************************************************************/ void RotaryAPullupFlag(uint32 switch_bit) { bitSet(Rotary.no_pullup_mask_a, switch_bit); } void RotaryBPullupFlag(uint32 switch_bit) { bitSet(Rotary.no_pullup_mask_b, switch_bit); } bool RotaryButtonPressed(uint32_t button_index) { if (!Rotary.present) { return false; } for (uint32_t index = 0; index < MAX_ROTARIES; index++) { if (-1 == Encoder[index].pinb) { continue; } if (index != button_index) { continue; } bool powered_on = (TasmotaGlobal.power); #ifdef USE_LIGHT if (!Settings->flag4.rotary_uses_rules) { // SetOption98 - Use rules instead of light control powered_on = LightPower(); } #endif // USE_LIGHT 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; } static void IRAM_ATTR RotaryIsrArgMiDesk(void *arg) { tEncoder* encoder = static_cast(arg); // https://github.com/PaulStoffregen/Encoder/blob/master/Encoder.h uint32_t state = encoder->state & 3; if (digitalRead(encoder->pina)) { state |= 4; } if (digitalRead(encoder->pinb)) { state |= 8; } #ifdef ESP32 // This fails intermittendly with panic (Cache disabled but cached memory region accessed) if not in DRAM // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/memory-types.html const static DRAM_ATTR int8_t rotary_state_pos[16] = { 0, 1, -1, 2, -1, 0, -2, 1, 1, -2, 0, -1, 2, -1, 1, 0 }; #endif // ESP32 encoder->position += rotary_state_pos[state]; encoder->state = (state >> 2); } static void IRAM_ATTR RotaryIsrArg(void *arg) { tEncoder* encoder = static_cast(arg); // Theo Arends uint32_t time = millis(); if ((encoder->debounce < time) || (encoder->debounce > time + ROTARY_DEBOUNCE)) { int direction = (digitalRead(encoder->pinb)) ? -1 : 1; if ((0 == encoder->direction) || (direction == encoder->direction)) { encoder->position += direction; encoder->direction = direction; } encoder->debounce = time + ROTARY_DEBOUNCE; // Experimental debounce } } void RotaryInitMaxSteps(void) { if (0 == Settings->param[P_ROTARY_MAX_STEP]) { Settings->param[P_ROTARY_MAX_STEP] = ROTARY_MAX_STEPS; // SetOption43 } uint8_t max_steps = Settings->param[P_ROTARY_MAX_STEP]; if (!Rotary.model) { max_steps *= 3; } Rotary.dimmer_increment = 100 / min((uint8_t)100, max_steps); // Dimmer 1..100 = 100 Rotary.ct_increment = 350 / min((uint8_t)350, max_steps); // Ct 153..500 = 347 Rotary.color_increment = 360 / min((uint8_t)360, max_steps); // Hue 0..359 = 360 } void RotaryInit(void) { Rotary.present = false; Rotary.model = !TasmotaGlobal.gpio_optiona.rotary_mi_desk; // Option_A5 #ifdef ESP8266 if (MI_DESK_LAMP == TasmotaGlobal.module_type) { Rotary.model = 0; } #endif // ESP8266 AddLog(LOG_LEVEL_DEBUG, PSTR("ROT: Mode %d"), Rotary.model); RotaryInitMaxSteps(); for (uint32_t index = 0; index < MAX_ROTARIES; index++) { Encoder[index].pinb = -1; if (PinUsed(GPIO_ROT1A, index) && PinUsed(GPIO_ROT1B, index)) { Encoder[index].position = rotary_offset; Encoder[index].pina = Pin(GPIO_ROT1A, index); Encoder[index].pinb = Pin(GPIO_ROT1B, index); pinMode(Encoder[index].pina, bitRead(Rotary.no_pullup_mask_a, index) ? INPUT : INPUT_PULLUP); pinMode(Encoder[index].pinb, bitRead(Rotary.no_pullup_mask_b, index) ? INPUT : INPUT_PULLUP); if (0 == Rotary.model) { attachInterruptArg(Encoder[index].pina, RotaryIsrArgMiDesk, &Encoder[index], CHANGE); attachInterruptArg(Encoder[index].pinb, RotaryIsrArgMiDesk, &Encoder[index], CHANGE); } else { attachInterruptArg(Encoder[index].pina, RotaryIsrArg, &Encoder[index], FALLING); } } Rotary.present |= (Encoder[index].pinb >= 0); } } /*********************************************************************************************\ * Rotary handler \*********************************************************************************************/ void RotaryHandler(void) { if (!Rotary.present) { return; } for (uint32_t index = 0; index < MAX_ROTARIES; index++) { if (-1 == Encoder[index].pinb) { 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); MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, PSTR(D_CMND_STATE)); } #endif // USE_LIGHT Encoder[index].direction = 0; } } if (rotary_offset == Encoder[index].position) { continue; } Encoder[index].timeout = ROTARY_TIMEOUT; // Prevent fast direction changes within 0.5 second noInterrupts(); int rotary_position = Encoder[index].position - rotary_offset; Encoder[index].position = rotary_offset; interrupts(); if (Settings->save_data && (TasmotaGlobal.save_data_counter < 2)) { TasmotaGlobal.save_data_counter = 3; // Postpone flash writes while rotary is turned } bool button_pressed = (Button.hold_timer[index]); // Button is pressed: set color temperature if (button_pressed) { Encoder[index].changed = true; } // AddLog(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 bool second_rotary = (Encoder[1].pinb >= 0); 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 uint32_t dimmer_index = second_rotary ? 1 : 0; if (!Settings->flag4.rotary_poweron_dimlow || TasmotaGlobal.power) { // SetOption113 - On rotary dial after power off set dimmer low LightDimmerOffset(dimmer_index, rotary_position * Rotary.dimmer_increment); } else { if (rotary_position > 0) { // Only power on if rotary increase LightDimmerOffset(dimmer_index, -LightGetDimmer(dimmer_index) + ROTARY_START_DIM); } } } } else { // Rotary2 if (button_pressed) { // Color Temperature LightColorTempOffset(rotary_position * Rotary.ct_increment); } else { // Dimmer CW LightDimmerOffset(2, rotary_position * Rotary.dimmer_increment); } } } else { #endif // USE_LIGHT Encoder[index].abs_position[button_pressed] += rotary_position; if (Encoder[index].abs_position[button_pressed] < 0) { Encoder[index].abs_position[button_pressed] = 0; } if (Encoder[index].abs_position[button_pressed] > Settings->param[P_ROTARY_MAX_STEP]) { // SetOption43 - Rotary steps Encoder[index].abs_position[button_pressed] = Settings->param[P_ROTARY_MAX_STEP]; // SetOption43 - Rotary steps } Response_P(PSTR("{\"Rotary%d\":{\"Pos1\":%d,\"Pos2\":%d}}"), index +1, Encoder[index].abs_position[0], Encoder[index].abs_position[1]); XdrvRulesProcess(0); #ifdef USE_LIGHT } #endif // USE_LIGHT } } #endif // ROTARY_V1