Add second rotary support

Add second rotary support default used for CW light control when first rotary is used for RGB light control (#8864)
This commit is contained in:
Theo Arends 2020-07-13 15:10:23 +02:00
parent 96d37ec535
commit 84ee6393a5
6 changed files with 147 additions and 244 deletions

View File

@ -34,207 +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; }
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; }
if (0 != button_index) { 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);
}
}
@ -245,63 +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
LightState(0);
MqttPublishPrefixTopic_P(RESULT_OR_STAT, PSTR(D_CMND_DIMMER));
XdrvRulesProcess();
}
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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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),

View File

@ -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();
}