WLED/wled00/button.cpp

391 lines
14 KiB
C++
Raw Normal View History

#include "wled.h"
2020-03-25 10:14:23 +01:00
/*
* Physical IO
*/
#define WLED_DEBOUNCE_THRESHOLD 50 // only consider button input of at least 50ms as valid (debouncing)
#define WLED_LONG_PRESS 600 // long press if button is released after held for at least 600ms
#define WLED_DOUBLE_PRESS 350 // double press if another press within 350ms after a short press
#define WLED_LONG_REPEATED_ACTION 300 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0
#define WLED_LONG_AP 5000 // how long button 0 needs to be held to activate WLED-AP
#define WLED_LONG_FACTORY_RESET 10000 // how long button 0 needs to be held to trigger a factory reset
2021-04-12 00:45:33 +02:00
static const char _mqtt_topic_button[] PROGMEM = "%s/button/%d"; // optimize flash usage
2021-05-20 19:54:07 +02:00
void shortPressAction(uint8_t b)
{
if (!macroButton[b]) {
switch (b) {
case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break;
case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break;
}
} else {
applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
}
2023-01-12 20:35:34 +01:00
#ifndef WLED_DISABLE_MQTT
// publish MQTT message
2021-07-01 20:51:52 +02:00
if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
char subuf[64];
sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
mqtt->publish(subuf, 0, false, "short");
}
2023-01-12 20:35:34 +01:00
#endif
}
void longPressAction(uint8_t b)
{
if (!macroLongPress[b]) {
switch (b) {
case 0: setRandomColor(col); colorUpdated(CALL_MODE_BUTTON); break;
case 1: bri += 8; stateUpdated(CALL_MODE_BUTTON); buttonPressedTime[b] = millis(); break; // repeatable action
}
} else {
applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
}
2023-01-12 20:35:34 +01:00
#ifndef WLED_DISABLE_MQTT
// publish MQTT message
if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
char subuf[64];
sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
mqtt->publish(subuf, 0, false, "long");
}
2023-01-12 20:35:34 +01:00
#endif
}
void doublePressAction(uint8_t b)
{
if (!macroDoublePress[b]) {
switch (b) {
//case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set
case 1: ++effectPalette %= strip.getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break;
}
} else {
applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET);
}
2023-01-12 20:35:34 +01:00
#ifndef WLED_DISABLE_MQTT
// publish MQTT message
if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
char subuf[64];
sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
mqtt->publish(subuf, 0, false, "double");
}
2023-01-12 20:35:34 +01:00
#endif
}
2021-05-20 19:54:07 +02:00
bool isButtonPressed(uint8_t i)
{
2021-05-20 19:54:07 +02:00
if (btnPin[i]<0) return false;
2022-02-09 19:59:17 +01:00
uint8_t pin = btnPin[i];
2021-05-20 19:54:07 +02:00
switch (buttonType[i]) {
case BTN_TYPE_NONE:
case BTN_TYPE_RESERVED:
break;
case BTN_TYPE_PUSH:
case BTN_TYPE_SWITCH:
2022-02-09 19:59:17 +01:00
if (digitalRead(pin) == LOW) return true;
2021-05-20 19:54:07 +02:00
break;
case BTN_TYPE_PUSH_ACT_HIGH:
case BTN_TYPE_PIR_SENSOR:
2022-02-09 19:59:17 +01:00
if (digitalRead(pin) == HIGH) return true;
2021-05-20 19:54:07 +02:00
break;
case BTN_TYPE_TOUCH:
#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3)
2022-02-09 19:59:17 +01:00
if (touchRead(pin) <= touchThreshold) return true;
2021-05-20 19:54:07 +02:00
#endif
break;
}
return false;
}
2021-05-20 19:54:07 +02:00
void handleSwitch(uint8_t b)
2021-04-12 00:45:33 +02:00
{
// isButtonPressed() handles inverted/noninverted logic
2021-05-20 19:54:07 +02:00
if (buttonPressedBefore[b] != isButtonPressed(b)) {
buttonPressedTime[b] = millis();
buttonPressedBefore[b] = !buttonPressedBefore[b];
2021-04-12 00:45:33 +02:00
}
2021-05-20 19:54:07 +02:00
if (buttonLongPressed[b] == buttonPressedBefore[b]) return;
2023-01-06 09:24:29 +01:00
2021-05-20 19:54:07 +02:00
if (millis() - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
if (!buttonPressedBefore[b]) { // on -> off
if (macroButton[b]) applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
2021-04-12 00:45:33 +02:00
else { //turn on
if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
2023-01-06 09:24:29 +01:00
}
} else { // off -> on
if (macroLongPress[b]) applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
2021-04-12 00:45:33 +02:00
else { //turn off
if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
2023-01-06 09:24:29 +01:00
}
2021-04-12 00:45:33 +02:00
}
2023-01-12 20:35:34 +01:00
#ifndef WLED_DISABLE_MQTT
// publish MQTT message
2021-07-01 20:51:52 +02:00
if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
char subuf[64];
if (buttonType[b] == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
mqtt->publish(subuf, 0, false, !buttonPressedBefore[b] ? "off" : "on");
}
2023-01-12 20:35:34 +01:00
#endif
2021-05-20 19:54:07 +02:00
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
2021-04-12 00:45:33 +02:00
}
}
2022-06-20 16:04:43 +02:00
#define ANALOG_BTN_READ_CYCLE 250 // min time between two analog reading cycles
2023-01-06 09:24:29 +01:00
#define STRIP_WAIT_TIME 6 // max wait time in case of strip.isUpdating()
2022-06-20 22:00:23 +02:00
#define POT_SMOOTHING 0.25f // smoothing factor for raw potentiometer readings
2022-06-20 16:04:43 +02:00
#define POT_SENSITIVITY 4 // changes below this amount are noise (POT scratching, or ADC noise)
void handleAnalog(uint8_t b)
{
2022-06-22 09:58:21 +02:00
static uint8_t oldRead[WLED_MAX_BUTTONS] = {0};
static float filteredReading[WLED_MAX_BUTTONS] = {0.0f};
2022-06-20 16:04:43 +02:00
uint16_t rawReading; // raw value from analogRead, scaled to 12bit
#ifdef ESP8266
2022-06-20 16:04:43 +02:00
rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit
#else
2022-06-20 16:04:43 +02:00
rawReading = analogRead(btnPin[b]); // collect at full 12bit resolution
#endif
2022-06-20 16:04:43 +02:00
yield(); // keep WiFi task running - analog read may take several millis on ESP8266
2022-06-22 09:58:21 +02:00
filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255]
2022-06-20 16:04:43 +02:00
uint16_t aRead = max(min(int(filteredReading[b]), 255), 0); // squash into 8bit
if(aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
if(aRead >= 255-POT_SENSITIVITY) aRead = 255;
if (buttonType[b] == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
2021-05-28 14:14:50 +02:00
// remove noise & reduce frequency of UI updates
2022-06-20 16:04:43 +02:00
if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading
// Unomment the next lines if you still see flickering related to potentiometer
// This waits until strip finishes updating (why: strip was not updating at the start of handleButton() but may have started during analogRead()?)
//unsigned long wait_started = millis();
//while(strip.isUpdating() && (millis() - wait_started < STRIP_WAIT_TIME)) {
// delay(1);
//}
2023-01-06 09:24:29 +01:00
//if (strip.isUpdating()) return; // give up
2021-05-25 23:59:43 +02:00
oldRead[b] = aRead;
// if no macro for "short press" and "long press" is defined use brightness control
if (!macroButton[b] && !macroLongPress[b]) {
// if "double press" macro defines which option to change
2021-05-25 23:59:43 +02:00
if (macroDoublePress[b] >= 250) {
// global brightness
2021-05-25 23:59:43 +02:00
if (aRead == 0) {
briLast = bri;
bri = 0;
} else{
2021-05-28 14:14:50 +02:00
bri = aRead;
}
} else if (macroDoublePress[b] == 249) {
// effect speed
2021-05-28 14:14:50 +02:00
effectSpeed = aRead;
} else if (macroDoublePress[b] == 248) {
// effect intensity
2021-05-28 14:14:50 +02:00
effectIntensity = aRead;
} else if (macroDoublePress[b] == 247) {
// selected palette
2021-05-28 14:14:50 +02:00
effectPalette = map(aRead, 0, 252, 0, strip.getPaletteCount()-1);
2022-06-20 16:04:43 +02:00
effectPalette = constrain(effectPalette, 0, strip.getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result
2021-05-30 02:03:32 +02:00
} else if (macroDoublePress[b] == 200) {
// primary color, hue, full saturation
colorHStoRGB(aRead*256,255,col);
} else {
// otherwise use "double press" for segment selection
Segment& seg = strip.getSegment(macroDoublePress[b]);
if (aRead == 0) {
seg.setOption(SEG_OPTION_ON, false); // off (use transition)
} else {
seg.setOpacity(aRead);
seg.setOption(SEG_OPTION_ON, true); // on (use transition)
}
2021-05-25 23:59:43 +02:00
// this will notify clients of update (websockets,mqtt,etc)
updateInterfaces(CALL_MODE_BUTTON);
}
} else {
//TODO:
// we can either trigger a preset depending on the level (between short and long entries)
// or use it for RGBW direct control
}
colorUpdated(CALL_MODE_BUTTON);
}
void handleButton()
{
2021-05-25 23:59:43 +02:00
static unsigned long lastRead = 0UL;
static unsigned long lastRun = 0UL;
unsigned long now = millis();
2021-05-25 23:59:43 +02:00
if (strip.isUpdating() && (now - lastRun < 400)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips)
lastRun = now;
2022-06-20 16:04:43 +02:00
2021-05-20 19:54:07 +02:00
for (uint8_t b=0; b<WLED_MAX_BUTTONS; b++) {
2021-05-28 17:45:14 +02:00
#ifdef ESP8266
if ((btnPin[b]<0 && !(buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)) || buttonType[b] == BTN_TYPE_NONE) continue;
2021-05-28 17:45:14 +02:00
#else
2021-05-25 23:59:43 +02:00
if (btnPin[b]<0 || buttonType[b] == BTN_TYPE_NONE) continue;
2021-05-28 17:45:14 +02:00
#endif
2021-04-12 00:45:33 +02:00
if (usermods.handleButton(b)) continue; // did usermod handle buttons
if (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
if (now - lastRead > ANALOG_BTN_READ_CYCLE) {
handleAnalog(b);
lastRead = now;
}
continue;
}
2021-04-12 00:45:33 +02:00
// button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
if (buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_PIR_SENSOR) {
handleSwitch(b);
continue;
2021-05-20 19:54:07 +02:00
}
2019-10-18 12:19:52 +02:00
// momentary button logic
if (isButtonPressed(b)) { // pressed
// if all macros are the same, fire action immediately on rising edge
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
if (!buttonPressedBefore[b])
shortPressAction(b);
buttonPressedBefore[b] = true;
buttonPressedTime[b] = now; // continually update (for debouncing to work in release handler)
return;
}
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
2021-05-20 19:54:07 +02:00
buttonPressedBefore[b] = true;
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
if (!buttonLongPressed[b]) longPressAction(b);
else if (b) { //repeatable action (~3 times per s) on button > 0
longPressAction(b);
buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //333ms
2021-05-20 19:54:07 +02:00
}
buttonLongPressed[b] = true;
2019-10-18 12:19:52 +02:00
}
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
long dur = now - buttonPressedTime[b];
// released after rising-edge short press action
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
if (dur > WLED_DEBOUNCE_THRESHOLD) buttonPressedBefore[b] = false; // debounce, blocks button for 50 ms once it has been released
return;
}
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} // too short "press", debounce
bool doublePress = buttonWaitTime[b]; //did we have a short press before?
2021-05-20 19:54:07 +02:00
buttonWaitTime[b] = 0;
if (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released)
if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds
WLED_FS.format();
#ifdef WLED_ADD_EEPROM_SUPPORT
clearEEPROM();
#endif
doReboot = true;
} else {
WLED::instance().initAP(true);
}
} else if (!buttonLongPressed[b]) { //short press
//NOTE: this interferes with double click handling in usermods so usermod needs to implement full button handling
if (b != 1 && !macroDoublePress[b]) { //don't wait for double press on buttons without a default action if no double press macro set
shortPressAction(b);
} else { //double press if less than 350 ms between current press and previous short press release (buttonWaitTime!=0)
if (doublePress) {
doublePressAction(b);
} else {
buttonWaitTime[b] = now;
}
}
2021-05-20 19:54:07 +02:00
}
buttonPressedBefore[b] = false;
buttonLongPressed[b] = false;
}
//if 350ms elapsed since last short press release it is a short press
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && !buttonPressedBefore[b]) {
2021-05-20 19:54:07 +02:00
buttonWaitTime[b] = 0;
shortPressAction(b);
}
}
}
// If enabled, RMT idle level is set to HIGH when off
// to prevent leakage current when using an N-channel MOSFET to toggle LED power
#ifdef ESP32_DATA_IDLE_HIGH
void esp32RMTInvertIdle()
{
bool idle_out;
for (uint8_t u = 0; u < busses.getNumBusses(); u++)
{
if (u > 7) return; // only 8 RMT channels, TODO: ESP32 variants have less RMT channels
Bus *bus = busses.getBus(u);
if (!bus || bus->getLength()==0 || !IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType())) continue;
//assumes that bus number to rmt channel mapping stays 1:1
rmt_channel_t ch = static_cast<rmt_channel_t>(u);
rmt_idle_level_t lvl;
rmt_get_idle_level(ch, &idle_out, &lvl);
if (lvl == RMT_IDLE_LEVEL_HIGH) lvl = RMT_IDLE_LEVEL_LOW;
else if (lvl == RMT_IDLE_LEVEL_LOW) lvl = RMT_IDLE_LEVEL_HIGH;
else continue;
rmt_set_idle_level(ch, idle_out, lvl);
}
}
#endif
void handleIO()
{
handleButton();
2023-01-06 09:24:29 +01:00
//set relay when LEDs turn on
if (strip.getBrightness())
{
lastOnTime = millis();
if (offMode)
{
#ifdef ESP32_DATA_IDLE_HIGH
esp32RMTInvertIdle();
#endif
2021-01-17 00:20:31 +01:00
if (rlyPin>=0) {
pinMode(rlyPin, OUTPUT);
digitalWrite(rlyPin, rlyMde);
}
offMode = false;
}
} else if (millis() - lastOnTime > 600)
{
2021-01-17 00:20:31 +01:00
if (!offMode) {
#ifdef ESP8266
2021-05-20 19:54:07 +02:00
// turn off built-in LED if strip is turned off
// this will break digital bus so will need to be reinitialised on On
PinOwner ledPinOwner = pinManager.getPinOwner(LED_BUILTIN);
2022-02-04 13:28:00 +01:00
if (!strip.isOffRefreshRequired() && (ledPinOwner == PinOwner::None || ledPinOwner == PinOwner::BusDigital)) {
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
}
#endif
#ifdef ESP32_DATA_IDLE_HIGH
esp32RMTInvertIdle();
#endif
2021-01-17 00:20:31 +01:00
if (rlyPin>=0) {
pinMode(rlyPin, OUTPUT);
digitalWrite(rlyPin, !rlyMde);
}
}
offMode = true;
}
2022-02-09 19:59:17 +01:00
}