diff --git a/usermods/multi_relay/readme.md b/usermods/multi_relay/readme.md index a5f47bf5..ebf2056a 100644 --- a/usermods/multi_relay/readme.md +++ b/usermods/multi_relay/readme.md @@ -16,6 +16,12 @@ Examples 1. 4 relays at all, relay 2 will be toggled: `http://[device-ip]/relays?toggle=0,1,0,0` 2. 3 relays at all, relay 1&3 will be switched on: `http://[device-ip]/relays?switch=1,0,1` +## JSON API +You can switch relay state using the following JSON object transmitted to: `http://[device-ip]/json` + +Switch relay 0 on: `{"MultiRelay":{"relay":0,"on":true}}` +Switch relay4 3 & 4 off: `{"MultiRelay":[{"relay":2,"on":false},{"relay":3,"on":false}]}` + ## MQTT API wled/deviceMAC/relay/0/command on|off|toggle diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index c3d55d66..9a92d4e2 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -6,6 +6,8 @@ #define MULTI_RELAY_MAX_RELAYS 4 #endif +#define WLED_DEBOUNCE_THRESHOLD 50 //only consider button input of at least 50ms as valid (debouncing) + #define ON true #define OFF false @@ -23,6 +25,7 @@ typedef struct relay_t { bool state; bool external; uint16_t delay; + int8_t button; } Relay; @@ -35,7 +38,7 @@ class MultiRelay : public Usermod { // switch timer start time uint32_t _switchTimerStart = 0; // old brightness - bool _oldBrightness = 0; + bool _oldMode; // usermod enabled bool enabled = false; // needs to be configured (no default config) @@ -49,6 +52,7 @@ class MultiRelay : public Usermod { static const char _delay_str[]; static const char _activeHigh[]; static const char _external[]; + static const char _button[]; void publishMqtt(const char* state, int relay) { @@ -170,6 +174,7 @@ class MultiRelay : public Usermod { _relay[i].active = false; _relay[i].state = false; _relay[i].external = false; + _relay[i].button = -1; } } /** @@ -261,11 +266,12 @@ class MultiRelay : public Usermod { if (!pinManager.allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) { _relay[i].pin = -1; // allocation failed } else { - switchRelay(i, _relay[i].state = (bool)bri); + if (!_relay[i].external) _relay[i].state = offMode; + switchRelay(i, _relay[i].state); _relay[i].active = false; } } - _oldBrightness = (bool)bri; + _oldMode = offMode; initDone = true; } @@ -281,24 +287,110 @@ class MultiRelay : public Usermod { * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() { + yield(); if (!enabled || strip.isUpdating()) return; static unsigned long lastUpdate = 0; - if (millis() - lastUpdate < 200) return; // update only 5 times/s + if (millis() - lastUpdate < 100) return; // update only 10 times/s lastUpdate = millis(); //set relay when LEDs turn on - if (_oldBrightness != (bool)bri) { - _oldBrightness = (bool)bri; + if (_oldMode != offMode) { + _oldMode = offMode; _switchTimerStart = millis(); for (uint8_t i=0; i=0) _relay[i].active = true; + if (_relay[i].pin>=0 && !_relay[i].external) _relay[i].active = true; } } handleOffTimer(); } + /** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + * Replicating button.cpp + */ + bool handleButton(uint8_t b) { + yield(); + if (buttonType[b] == BTN_TYPE_NONE || buttonType[b] == BTN_TYPE_RESERVED || buttonType[b] == BTN_TYPE_PIR_SENSOR || buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { + return false; + } + + bool handled = false; + for (uint8_t i=0; i WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) + for (uint8_t i=0; i=0 && _relay[i].button == b) { + switchRelay(i, buttonPressedBefore[b]); + buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state + } + } + } + return handled; + } + + //momentary button logic + if (isButtonPressed(b)) { //pressed + + if (!buttonPressedBefore[b]) buttonPressedTime[b] = now; + buttonPressedBefore[b] = true; + + if (now - buttonPressedTime[b] > 600) { //long press + buttonLongPressed[b] = true; + } + + } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released + + long dur = now - buttonPressedTime[b]; + if (dur < WLED_DEBOUNCE_THRESHOLD) { + buttonPressedBefore[b] = false; + return handled; + } //too short "press", debounce + bool doublePress = buttonWaitTime[b]; //did we have short press before? + buttonWaitTime[b] = 0; + + if (!buttonLongPressed[b]) { //short press + // if this is second release within 350ms it is a double press (buttonWaitTime!=0) + if (doublePress) { + //doublePressAction(b); + } else { + buttonWaitTime[b] = now; + } + } + buttonPressedBefore[b] = false; + buttonLongPressed[b] = false; + } + // if 450ms elapsed since last press/release it is a short press + if (buttonWaitTime[b] && now - buttonWaitTime[b] > 350 && !buttonPressedBefore[b]) { + buttonWaitTime[b] = 0; + for (uint8_t i=0; i=0 && _relay[i].button == b) { + toggleRelay(i); + } + } + } + return handled; + } + /** * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. */ @@ -310,6 +402,26 @@ class MultiRelay : public Usermod { JsonArray infoArr = user.createNestedArray(F("Number of relays")); //name infoArr.add(String(getActiveRelayCount())); + + String uiDomString; + for (uint8_t i=0; i"); + uiDomString += F("Relay "); + uiDomString += i; + uiDomString += F(" "); + JsonArray infoArr = user.createNestedArray(uiDomString); // timer value + + infoArr.add(_relay[i].state ? "on" : "off"); + } } } @@ -317,15 +429,46 @@ class MultiRelay : public Usermod { * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - //void addToJsonState(JsonObject &root) { - //} + void addToJsonState(JsonObject &root) { + if (!initDone || !enabled) return; // prevent crash on boot applyPreset() + JsonObject multiRelay = root[FPSTR(_name)]; + if (multiRelay.isNull()) { + multiRelay = root.createNestedObject(FPSTR(_name)); + } + #if MULTI_RELAY_MAX_RELAYS > 1 + JsonArray rel_arr = multiRelay.createNestedArray(F("relays")); + for (uint8_t i=0; i() && usermod[FPSTR(_relay_str)].is() && usermod[FPSTR(_relay_str)].as()>=0) { + switchRelay(usermod[FPSTR(_relay_str)].as(), usermod["on"].as()); + } + } else if (root[FPSTR(_name)].is()) { + JsonArray relays = root[FPSTR(_name)].as(); + for (JsonVariant r : relays) { + if (r["on"].is() && r[FPSTR(_relay_str)].is() && r[FPSTR(_relay_str)].as()>=0) { + switchRelay(r[FPSTR(_relay_str)].as(), r["on"].as()); + } + } + } + } /** * provide the changeable values @@ -341,6 +484,7 @@ class MultiRelay : public Usermod { relay[FPSTR(_activeHigh)] = _relay[i].mode; relay[FPSTR(_delay_str)] = _relay[i].delay; relay[FPSTR(_external)] = _relay[i].external; + relay[FPSTR(_button)] = _relay[i].button; } DEBUG_PRINTLN(F("MultiRelay config saved.")); } @@ -370,6 +514,7 @@ class MultiRelay : public Usermod { _relay[i].mode = top[parName][FPSTR(_activeHigh)] | _relay[i].mode; _relay[i].external = top[parName][FPSTR(_external)] | _relay[i].external; _relay[i].delay = top[parName][FPSTR(_delay_str)] | _relay[i].delay; + _relay[i].button = top[parName][FPSTR(_button)] | _relay[i].button; // begin backwards compatibility (beta) remove when 0.13 is released parName += '-'; _relay[i].pin = top[parName+"pin"] | _relay[i].pin; @@ -394,7 +539,7 @@ class MultiRelay : public Usermod { for (uint8_t i=0; i=0 && pinManager.allocatePin(_relay[i].pin, true, PinOwner::UM_MultiRelay)) { if (!_relay[i].external) { - switchRelay(i, _relay[i].state = (bool)bri); + switchRelay(i, offMode); } } else { _relay[i].pin = -1; @@ -404,7 +549,7 @@ class MultiRelay : public Usermod { DEBUG_PRINTLN(F(" config (re)loaded.")); } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[F("relay-0")]["pin"].isNull(); + return !top[F("relay-0")][FPSTR(_button)].isNull(); } /** @@ -424,3 +569,4 @@ const char MultiRelay::_relay_str[] PROGMEM = "relay"; const char MultiRelay::_delay_str[] PROGMEM = "delay-s"; const char MultiRelay::_activeHigh[] PROGMEM = "active-high"; const char MultiRelay::_external[] PROGMEM = "external"; +const char MultiRelay::_button[] PROGMEM = "button"; diff --git a/wled00/button.cpp b/wled00/button.cpp index ba6a0c97..39c1e4f7 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -5,15 +5,20 @@ */ #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 6000 //how long the button needs to be held to activate WLED-AP static const char _mqtt_topic_button[] PROGMEM = "%s/button/%d"; // optimize flash usage void shortPressAction(uint8_t b) { - if (!macroButton[b]) - { - toggleOnOff(); - colorUpdated(CALL_MODE_BUTTON); + if (!macroButton[b]) { + switch (b) { + case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; + default: ++effectCurrent %= strip.getModeCount(); colorUpdated(CALL_MODE_BUTTON); break; + } } else { applyPreset(macroButton[b], CALL_MODE_BUTTON); } @@ -26,6 +31,44 @@ void shortPressAction(uint8_t b) } } +void longPressAction(uint8_t b) +{ + if (!macroLongPress[b]) { + switch (b) { + case 0: _setRandomColor(false,true); break; + default: bri += 8; colorUpdated(CALL_MODE_BUTTON); buttonPressedTime[b] = millis(); break; // repeatable action + } + } else { + applyPreset(macroLongPress[b], CALL_MODE_BUTTON); + } + + // 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"); + } +} + +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 + default: ++effectPalette %= strip.getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; + } + } else { + applyPreset(macroDoublePress[b], CALL_MODE_BUTTON); + } + + // 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"); + } +} + bool isButtonPressed(uint8_t i) { if (btnPin[i]<0) return false; @@ -175,6 +218,8 @@ void handleButton() if (btnPin[b]<0 || buttonType[b] == BTN_TYPE_NONE) continue; #endif + if (usermods.handleButton(b)) continue; // did usermod handle buttons + if ((buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) && millis() - lastRead > 250) { // button is not a button but a potentiometer if (b+1 == WLED_MAX_BUTTONS) lastRead = millis(); handleAnalog(b); continue; @@ -186,61 +231,46 @@ void handleButton() } //momentary button logic - if (isButtonPressed(b)) //pressed - { + if (isButtonPressed(b)) { //pressed + if (!buttonPressedBefore[b]) buttonPressedTime[b] = millis(); buttonPressedBefore[b] = true; - if (millis() - buttonPressedTime[b] > 600) //long press - { - if (!buttonLongPressed[b]) - { - if (macroLongPress[b]) {applyPreset(macroLongPress[b], CALL_MODE_BUTTON);} - else _setRandomColor(false,true); - - // 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"); - } - - buttonLongPressed[b] = true; + if (millis() - 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] = millis() - WLED_LONG_REPEATED_ACTION; //300ms } + buttonLongPressed[b] = true; } - } - else if (!isButtonPressed(b) && buttonPressedBefore[b]) //released - { + + } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released + long dur = millis() - buttonPressedTime[b]; if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} //too short "press", debounce - bool doublePress = buttonWaitTime[b]; + bool doublePress = buttonWaitTime[b]; //did we have a short press before? buttonWaitTime[b] = 0; - if (dur > 6000 && b==0) //long press on button 0 - { + if (b == 0 && dur > WLED_LONG_AP) { //long press on button 0 (when released) WLED::instance().initAP(true); - } - else if (!buttonLongPressed[b]) { //short press - if (macroDoublePress[b]) - { + } else if (!buttonLongPressed[b]) { //short press + if (b == 0 && !macroDoublePress[b]) { //don't wait for double press on button 0 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) { - applyPreset(macroDoublePress[b], CALL_MODE_BUTTON); - - // 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"); - } - } else buttonWaitTime[b] = millis(); - } else shortPressAction(b); + doublePressAction(b); + } else { + buttonWaitTime[b] = millis(); + } + } } buttonPressedBefore[b] = false; buttonLongPressed[b] = false; } - if (buttonWaitTime[b] && millis() - buttonWaitTime[b] > 450 && !buttonPressedBefore[b]) - { + //if 350ms elapsed since last short press release it is a short press + if (buttonWaitTime[b] && millis() - buttonWaitTime[b] > WLED_DOUBLE_PRESS && !buttonPressedBefore[b]) { buttonWaitTime[b] = 0; shortPressAction(b); } diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 61284b55..180d5935 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -208,6 +208,7 @@ class Usermod { public: virtual void loop() {} virtual void handleOverlayDraw() {} + virtual bool handleButton(uint8_t b) { return false; } virtual void setup() {} virtual void connected() {} virtual void addToJsonState(JsonObject& obj) {} @@ -228,7 +229,7 @@ class UsermodManager { public: void loop(); void handleOverlayDraw(); - + bool handleButton(uint8_t b); void setup(); void connected(); void addToJsonState(JsonObject& obj); diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index a9aac784..caaf100d 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -6,6 +6,13 @@ //Usermod Manager internals void UsermodManager::loop() { for (byte i = 0; i < numMods; i++) ums[i]->loop(); } void UsermodManager::handleOverlayDraw() { for (byte i = 0; i < numMods; i++) ums[i]->handleOverlayDraw(); } +bool UsermodManager::handleButton(uint8_t b) { + bool overrideIO = false; + for (byte i = 0; i < numMods; i++) { + if (ums[i]->handleButton(b)) overrideIO = true; + } + return overrideIO; +} void UsermodManager::setup() { for (byte i = 0; i < numMods; i++) ums[i]->setup(); } void UsermodManager::connected() { for (byte i = 0; i < numMods; i++) ums[i]->connected(); }