diff --git a/usermods/multi_relay/multi_relay.cpp b/usermods/multi_relay/multi_relay.cpp index 4cbdb2fe39..a3a814ea21 100644 --- a/usermods/multi_relay/multi_relay.cpp +++ b/usermods/multi_relay/multi_relay.cpp @@ -35,8 +35,6 @@ #define MULTI_RELAY_INVERTS false #endif -#define WLED_DEBOUNCE_THRESHOLD 50 //only consider button input of at least 50ms as valid (debouncing) - #define ON true #define OFF false @@ -590,7 +588,7 @@ bool MultiRelay::handleButton(uint8_t b) { if (buttons[b].longPressed == buttons[b].pressedBefore) return handled; - if (now - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) + if (now - buttons[b].pressedTime > buttonDebounceMs) { //fire edge event only after debounce time without change for (int i=0; i 600) { //long press + if (now - buttons[b].pressedTime > buttonLongPressMs) { //long press //longPressAction(b); //not exposed //handled = false; //use if you want to pass to default behaviour buttons[b].longPressed = true; @@ -616,7 +614,7 @@ bool MultiRelay::handleButton(uint8_t b) { } else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released long dur = now - buttons[b].pressedTime; - if (dur < WLED_DEBOUNCE_THRESHOLD) { + if (dur < buttonDebounceMs) { buttons[b].pressedBefore = false; return handled; } //too short "press", debounce @@ -624,7 +622,7 @@ bool MultiRelay::handleButton(uint8_t b) { buttons[b].waitTime = 0; if (!buttons[b].longPressed) { //short press - // if this is second release within 350ms it is a double press (buttonWaitTime!=0) + // if this is second release within double press time it is a double press (buttonWaitTime!=0) if (doublePress) { //doublePressAction(b); //not exposed //handled = false; //use if you want to pass to default behaviour @@ -635,8 +633,8 @@ bool MultiRelay::handleButton(uint8_t b) { buttons[b].pressedBefore = false; buttons[b].longPressed = false; } - // if 350ms elapsed since last press/release it is a short press - if (buttons[b].waitTime && now - buttons[b].waitTime > 350 && !buttons[b].pressedBefore) { + // if double press time elapsed since last press/release it is a short press + if (buttons[b].waitTime && now - buttons[b].waitTime > buttonDoublePressMs && !buttons[b].pressedBefore) { buttons[b].waitTime = 0; //shortPressAction(b); //not exposed for (int i=0; i WLED_LONG_PRESS) { //long press + if (now - buttons[b].pressedTime > buttonLongPressMs) { //long press menu_ctrl.HandleButton(ButtonType::LONG, b); buttons[b].longPressed = true; return true; @@ -490,7 +483,7 @@ class PixelsDiceTrayUsermod : public Usermod { } else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released long dur = now - buttons[b].pressedTime; - if (dur < WLED_DEBOUNCE_THRESHOLD) { + if (dur < buttonDebounceMs) { buttons[b].pressedBefore = false; return true; } //too short "press", debounce @@ -499,7 +492,7 @@ class PixelsDiceTrayUsermod : public Usermod { buttons[b].waitTime = 0; if (!buttons[b].longPressed) { //short press - // if this is second release within 350ms it is a double press (buttonWaitTime!=0) + // if this is second release within double press time it is a double press (buttonWaitTime!=0) if (doublePress) { menu_ctrl.HandleButton(ButtonType::DOUBLE, b); } else { @@ -509,8 +502,8 @@ class PixelsDiceTrayUsermod : public Usermod { buttons[b].pressedBefore = false; buttons[b].longPressed = false; } - // if 350ms elapsed since last press/release it is a short press - if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && + // if double press time elapsed since last press/release it is a short press + if (buttons[b].waitTime && now - buttons[b].waitTime > buttonDoublePressMs && !buttons[b].pressedBefore) { buttons[b].waitTime = 0; menu_ctrl.HandleButton(ButtonType::SINGLE, b); diff --git a/wled00/button.cpp b/wled00/button.cpp index ef56cea7c6..17bf207b04 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -4,14 +4,13 @@ * 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 400 // 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 #define WLED_LONG_BRI_STEPS 16 // how much to increase/decrease the brightness with each long press repetition +static_assert(WLED_LONG_PRESS_MAX < WLED_LONG_AP, "long press must not reach the AP-mode/factory-reset hold time of button 0"); +static_assert(WLED_LONG_AP < WLED_LONG_FACTORY_RESET, "AP mode hold must be shorter than factory reset hold"); + static const char _mqtt_topic_button[] PROGMEM = "%s/button/%d"; // optimize flash usage // Runtime state private to this file - previously WLED_GLOBAL, a leftover from @@ -136,7 +135,7 @@ void handleSwitch(uint8_t b) if (buttons[b].longPressed == buttons[b].pressedBefore) return; - if (millis() - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) + if (millis() - buttons[b].pressedTime > buttonDebounceMs) { //fire edge event only after debounce threshold without change DEBUG_PRINTF_P(PSTR("Switch: Activating %u\n"), b); if (!buttons[b].pressedBefore) { // on -> off DEBUG_PRINTF_P(PSTR("Switch: On -> Off (%u)\n"), b); @@ -304,15 +303,17 @@ void handleButton() if (!buttons[b].pressedBefore) buttons[b].pressedTime = now; buttons[b].pressedBefore = true; - if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press - if (!buttons[b].longPressed) { + // first long press fires after buttonLongPressMs, repeats are timed from the last action + if (!buttons[b].longPressed) { + if (now - buttons[b].pressedTime > buttonLongPressMs) { //long press buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press longPressAction(b); - } else if (b) { //repeatable action (~5 times per s) on button > 0 - longPressAction(b); - buttons[b].pressedTime = now - WLED_LONG_REPEATED_ACTION; //200ms + if (b) buttons[b].pressedTime = now; // pause before first repeat; button 0 keeps press start for AP timing + buttons[b].longPressed = true; } - buttons[b].longPressed = true; + } else if (b && now - buttons[b].pressedTime > buttonRepeatDelayMs) { //repeatable action on button > 0 + longPressAction(b); + buttons[b].pressedTime = now - (buttonRepeatDelayMs - buttonRepeatIntervalMs); // next repeat after buttonRepeatIntervalMs (delay >= interval is enforced where settings are read) } } else if (buttons[b].pressedBefore) { //released @@ -320,11 +321,11 @@ void handleButton() // released after rising-edge short press action if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) { - if (dur > WLED_DEBOUNCE_THRESHOLD) buttons[b].pressedBefore = false; // debounce, blocks button for 50 ms once it has been released + if (dur > buttonDebounceMs) buttons[b].pressedBefore = false; // debounce, blocks button once it has been released continue; } - if (dur < WLED_DEBOUNCE_THRESHOLD) {buttons[b].pressedBefore = false; continue;} // too short "press", debounce + if (dur < buttonDebounceMs && !buttons[b].longPressed) {buttons[b].pressedBefore = false; continue;} // too short "press", debounce bool doublePress = buttons[b].waitTime; //did we have a short press before? buttons[b].waitTime = 0; @@ -339,7 +340,7 @@ void handleButton() //NOTE: this interferes with double click handling in usermods so usermod needs to implement full button handling if (b != 1 && !buttons[b].macroDoublePress) { //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) + } else { //double press if within double press window of current press and previous short press release (buttonWaitTime!=0) if (doublePress) { doublePressAction(b); } else { @@ -351,8 +352,8 @@ void handleButton() buttons[b].longPressed = false; } - //if 350ms elapsed since last short press release it is a short press - if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && !buttons[b].pressedBefore) { + //if double press window elapsed since last short press release it is a short press + if (buttons[b].waitTime && now - buttons[b].waitTime > buttonDoublePressMs && !buttons[b].pressedBefore) { buttons[b].waitTime = 0; shortPressAction(b); } diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index bfb6242883..d8af9b9b8a 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -451,6 +451,18 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } CJSON(buttonPublishMqtt, btn_obj["mqtt"]); + // CJSON keeps the previous value if the JSON value doesn't fit uint16_t (e.g. negative), so no wraparound before the clamps + CJSON(buttonDebounceMs, btn_obj[F("dbnc")]); + CJSON(buttonLongPressMs, btn_obj[F("lp")]); + CJSON(buttonDoublePressMs, btn_obj[F("dp")]); + CJSON(buttonRepeatDelayMs, btn_obj[F("brpd")]); + CJSON(buttonRepeatIntervalMs, btn_obj[F("brpi")]); + buttonDebounceMs = min(buttonDebounceMs, (uint16_t)WLED_DEBOUNCE_MAX); + buttonLongPressMs = constrain(buttonLongPressMs, WLED_LONG_PRESS_MIN, WLED_LONG_PRESS_MAX); + buttonDoublePressMs = constrain(buttonDoublePressMs, WLED_DOUBLE_PRESS_MIN, WLED_DOUBLE_PRESS_MAX); + buttonRepeatIntervalMs = constrain(buttonRepeatIntervalMs, WLED_LONG_REPEAT_INTERVAL_MIN, WLED_LONG_REPEAT_INTERVAL_MAX); + buttonRepeatDelayMs = constrain(buttonRepeatDelayMs, WLED_LONG_REPEAT_DELAY_MIN, WLED_LONG_REPEAT_DELAY_MAX); + buttonRepeatDelayMs = max(buttonRepeatDelayMs, buttonRepeatIntervalMs); // repeat delay must be at least the repeat interval #ifndef WLED_DISABLE_INFRARED int hw_ir_pin = hw["ir"]["pin"] | -2; // 4 @@ -1048,6 +1060,11 @@ void serializeConfig(JsonObject root) { hw_btn[F("tt")] = touchThreshold; hw_btn["mqtt"] = buttonPublishMqtt; + hw_btn[F("dbnc")] = buttonDebounceMs; + hw_btn[F("lp")] = buttonLongPressMs; + hw_btn[F("dp")] = buttonDoublePressMs; + hw_btn[F("brpd")] = buttonRepeatDelayMs; + hw_btn[F("brpi")] = buttonRepeatIntervalMs; JsonObject hw_ir = hw.createNestedObject("ir"); #ifndef WLED_DISABLE_INFRARED diff --git a/wled00/const.h b/wled00/const.h index 00a6b4d226..17799bb42d 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -651,6 +651,22 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define TOUCH_THRESHOLD 32 // limit to recognize a touch, higher value means more sensitive +#define WLED_DEBOUNCE_MAX 100 // settings limits for button timings (buttonDebounceMs, buttonLongPressMs, buttonDoublePressMs, buttonRepeatDelayMs, buttonRepeatIntervalMs) +#define WLED_LONG_PRESS_MIN 200 +#define WLED_LONG_PRESS_MAX 4000 // must stay below WLED_LONG_AP (button.cpp) +#define WLED_DOUBLE_PRESS_MIN 100 +#define WLED_DOUBLE_PRESS_MAX 1000 +static_assert(WLED_DEBOUNCE_MAX > 0, "debounce max must be positive"); +static_assert(WLED_LONG_PRESS_MIN < WLED_LONG_PRESS_MAX, "long press min must be below max"); +static_assert(WLED_DOUBLE_PRESS_MIN < WLED_DOUBLE_PRESS_MAX, "double press min must be below max"); +static_assert(WLED_LONG_PRESS_MIN > WLED_DEBOUNCE_MAX, "long press must be longer than the debounce time"); +#define WLED_LONG_REPEAT_DELAY_MIN 100 // buttonRepeatDelayMs must also be >= buttonRepeatIntervalMs (enforced where settings are read) +#define WLED_LONG_REPEAT_DELAY_MAX 4000 +#define WLED_LONG_REPEAT_INTERVAL_MIN 100 +#define WLED_LONG_REPEAT_INTERVAL_MAX 4000 +static_assert(WLED_LONG_REPEAT_DELAY_MIN < WLED_LONG_REPEAT_DELAY_MAX, "repeat delay min must be below max"); +static_assert(WLED_LONG_REPEAT_INTERVAL_MIN < WLED_LONG_REPEAT_INTERVAL_MAX, "repeat interval min must be below max"); +static_assert(WLED_LONG_REPEAT_INTERVAL_MAX <= WLED_LONG_REPEAT_DELAY_MAX, "raising the repeat delay to the repeat interval must stay within the delay range"); // Size of buffer for API JSON object (increase for more segments) #ifdef ESP8266 #define JSON_BUFFER_SIZE 10240 diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 03fae259c4..56b9e2f19c 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -39,6 +39,7 @@ function S() { getLoc(); if(localStorage.getItem('ASc')==='true') d.Sf.AS.checked=true; + if(localStorage.getItem('btnASc')==='true') gId('btnAS').checked=true; fetch(getURL('/json/info')) .then(r => r.ok ? r.json() : {}) .then(info => { hasPSRAM = Number(info.psrSz ?? 0) > 0 || Number(info.psram ?? 0) > 0; }) @@ -509,6 +510,11 @@ gId('psu').innerHTML = s; gId('psu2').innerHTML = s2; gId("json").style.display = d.Sf.IT.value==8 ? "" : "none"; + // advanced button settings only when enabled, or if invalid (a hidden invalid field would silently block saving) + // long-press repeat delay must be at least the repeat interval (blocks saving an invalid pair) + d.Sf.BRPD.setCustomValidity(parseInt(d.Sf.BRPD.value) < parseInt(d.Sf.BRPI.value) ? "Must not be shorter than the long-press repeat interval" : ""); + let btInv = [...gId("btnAdv").querySelectorAll("input")].some(e => !e.checkValidity()); + gId("btnAdv").style.display = (gId("btnAS").checked || btInv) ? "" : "none"; // show/hide FPS warning messages gId('fpsNone').style.display = (d.Sf.FR.value == 0) ? 'block':'none'; @@ -800,6 +806,11 @@ addBtn(i,v.pin[0],v.type); }); d.getElementsByName("TT")[0].value = b.tt; + if (b.dbnc !== undefined) d.getElementsByName("DB")[0].value = b.dbnc; + if (b.lp !== undefined) d.getElementsByName("LP")[0].value = b.lp; + if (b.dp !== undefined) d.getElementsByName("DP")[0].value = b.dp; + if (b.brpd !== undefined) d.getElementsByName("BRPD")[0].value = b.brpd; + if (b.brpi !== undefined) d.getElementsByName("BRPI")[0].value = b.brpi; } let ir = c.hw.ir; if (ir) { @@ -1088,12 +1099,20 @@

Hardware setup

Buttons

-
- Disable internal pull-up/down:
- Touch threshold:

+
+ Show Advanced Button Settings
+

IR Remote

diff --git a/wled00/set.cpp b/wled00/set.cpp index 5c459e5570..ccb5d471cb 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -332,6 +332,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) disablePullUp = (bool)request->hasArg(F("IP")); touchThreshold = request->arg(F("TT")).toInt(); + if (request->hasArg(F("DB"))) buttonDebounceMs = constrain(request->arg(F("DB")).toInt(), 0, WLED_DEBOUNCE_MAX); + if (request->hasArg(F("LP"))) buttonLongPressMs = constrain(request->arg(F("LP")).toInt(), WLED_LONG_PRESS_MIN, WLED_LONG_PRESS_MAX); + if (request->hasArg(F("DP"))) buttonDoublePressMs = constrain(request->arg(F("DP")).toInt(), WLED_DOUBLE_PRESS_MIN, WLED_DOUBLE_PRESS_MAX); + if (request->hasArg(F("BRPI"))) buttonRepeatIntervalMs = constrain(request->arg(F("BRPI")).toInt(), WLED_LONG_REPEAT_INTERVAL_MIN, WLED_LONG_REPEAT_INTERVAL_MAX); + if (request->hasArg(F("BRPD"))) buttonRepeatDelayMs = constrain(request->arg(F("BRPD")).toInt(), WLED_LONG_REPEAT_DELAY_MIN, WLED_LONG_REPEAT_DELAY_MAX); + buttonRepeatDelayMs = max(buttonRepeatDelayMs, buttonRepeatIntervalMs); // repeat delay must be at least the repeat interval for (int i = 0; i < WLED_MAX_BUTTONS; i++) { int offset = i < 10 ? '0' : 'A' - 10; char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10) diff --git a/wled00/wled.h b/wled00/wled.h index 5d2a6e75b1..26d4389220 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -647,6 +647,11 @@ WLED_GLOBAL std::vector