diff --git a/CHANGELOG.md b/CHANGELOG.md index 641568de..594fa97e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ ## WLED changelog +#### Build 2306020 + +- Support for segment sets (PR #3171) +- Reduce sound simulation modes to 2 to facilitiate segment sets +- Trigger button immediately on press if all configured presets are the same (PR #3226) +- Changes for allowing Alexa to change light color to White when auto-calculating from RGB (PR #3211) + +#### Build 2305280 +- DDP protocol update (#3193) +- added PCF8574 I2C port expander support for Multi relay usermod +- MQTT multipacket (fragmented) message fix +- added option to retain MQTT brightness and color messages +- new ethernet board: @srg74 Ethernet Shield +- new 2D effects: Soap (#3184) & Octopus & Waving cell (credit @St3P40 https://github.com/80Stepko08) +- various fixes and enhancements + +#### Build 2305090 +- new ethernet board: @Wladi ABC! WLED Eth +- Battery usermod voltage calculation (#3116) +- custom palette editor (#3164) +- improvements in Dancing Shadows and Tartan effects +- UCS389x support +- switched to NeoPixelBus 2.7.5 (replaced NeoPixelBrightnessBus with NeoPixelBusLg) +- SPI bus clock selection (for LEDs) (#3173) +- DMX mode preset fix (#3134) +- iOS fix for scroll (#3182) +- Wordclock "Norddeutsch" fix (#3161) +- various fixes and enhancements + #### Build 2304090 - updated Arduino ESP8266 core to 4.1.0 (newer compiler) - updated NeoPixelBus to 2.7.3 (with support for UCS890x chipset) diff --git a/requirements.txt b/requirements.txt index da65486c..bc536ed0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,9 +21,7 @@ click==8.1.3 # platformio # uvicorn colorama==0.4.6 - # via - # click - # platformio + # via platformio h11==0.14.0 # via # uvicorn @@ -42,7 +40,7 @@ pyelftools==0.29 # via platformio pyserial==3.5 # via platformio -requests==2.28.2 +requests==2.31.0 # via platformio semantic-version==2.10.0 # via platformio @@ -52,8 +50,6 @@ starlette==0.23.1 # via platformio tabulate==0.9.0 # via platformio -typing-extensions==4.5.0 - # via starlette urllib3==1.26.15 # via requests uvicorn==0.20.0 diff --git a/usermods/EleksTube_IPS/TFTs.h b/usermods/EleksTube_IPS/TFTs.h index e614704f..030ec23a 100644 --- a/usermods/EleksTube_IPS/TFTs.h +++ b/usermods/EleksTube_IPS/TFTs.h @@ -133,13 +133,13 @@ private: return false; } - read32(bmpFS); // filesize in bytes - read32(bmpFS); // reserved + (void) read32(bmpFS); // filesize in bytes + (void) read32(bmpFS); // reserved seekOffset = read32(bmpFS); // start of bitmap headerSize = read32(bmpFS); // header size w = read32(bmpFS); // width h = read32(bmpFS); // height - read16(bmpFS); // color planes (must be 1) + (void) read16(bmpFS); // color planes (must be 1) bitDepth = read16(bmpFS); if (read32(bmpFS) != 0 || (bitDepth != 24 && bitDepth != 1 && bitDepth != 4 && bitDepth != 8)) { @@ -151,9 +151,9 @@ private: uint32_t palette[256]; if (bitDepth <= 8) // 1,4,8 bit bitmap: read color palette { - read32(bmpFS); read32(bmpFS); read32(bmpFS); // size, w resolution, h resolution + (void) read32(bmpFS); (void) read32(bmpFS); (void) read32(bmpFS); // size, w resolution, h resolution paletteSize = read32(bmpFS); - if (paletteSize == 0) paletteSize = bitDepth * bitDepth; //if 0, size is 2^bitDepth + if (paletteSize == 0) paletteSize = 1 << bitDepth; //if 0, size is 2^bitDepth bmpFS.seek(14 + headerSize); // start of color palette for (uint16_t i = 0; i < paletteSize; i++) { palette[i] = read32(bmpFS); @@ -198,7 +198,7 @@ private: } b = c; g = c >> 8; r = c >> 16; } - if (dimming != 255) { // only dimm when needed + if (dimming != 255) { // only dim when needed r *= dimming; g *= dimming; b *= dimming; r = r >> 8; g = g >> 8; b = b >> 8; } diff --git a/usermods/multi_relay/readme.md b/usermods/multi_relay/readme.md index 2906b860..71a54070 100644 --- a/usermods/multi_relay/readme.md +++ b/usermods/multi_relay/readme.md @@ -1,6 +1,9 @@ # Multi Relay This usermod-v2 modification allows the connection of multiple relays, each with individual delay and on/off mode. +Usermod supports PCF8574 I2C port expander to reduce GPIO use. +PCF8574 supports 8 outputs and each output corresponds to a relay in WLED (relay 0 = port 0, etc). I you are using more than 8 relays with multiple PCF8574 make sure their addresses are set conscutively (e.g. 0x20 and 0x21). You can set address of first expander in settings. +(**NOTE:** Will require Wire library and global I2C pins defined.) ## HTTP API All responses are returned in JSON format. @@ -81,13 +84,15 @@ void registerUsermods() Usermod can be configured via the Usermods settings page. * `enabled` - enable/disable usermod +* `use-PCF8574` - use PCF8574 port expander instead of GPIO pins +* `first-PCF8574` - I2C address of first expander (WARNING: enter *decimal* value) +* `broadcast`- time in seconds between MQTT relay-state broadcasts +* `HA-discovery`- enable Home Assistant auto discovery * `pin` - ESP GPIO pin the relay is connected to (can be configured at compile time `-D MULTI_RELAY_PINS=xx,xx,...`) * `delay-s` - delay in seconds after on/off command is received * `active-high` - assign high/low activation of relay (can be used to reverse relay states) * `external` - if enabled, WLED does not control relay, it can only be triggered by an external command (MQTT, HTTP, JSON or button) * `button` - button (from LED Settings) that controls this relay -* `broadcast`- time in seconds between MQTT relay-state broadcasts -* `HA-discovery`- enable Home Assistant auto discovery If there is no MultiRelay section, just save current configuration and re-open Usermods settings page. @@ -100,3 +105,6 @@ Have fun - @blazoncek 2021-11 * Added information about dynamic configuration options * Added button support. + +2023-05 +* Added support for PCF8574 I2C port expander (multiple) \ No newline at end of file diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index 749b9c75..b8cabebb 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -4,6 +4,11 @@ #ifndef MULTI_RELAY_MAX_RELAYS #define MULTI_RELAY_MAX_RELAYS 4 +#else + #if MULTI_RELAY_MAX_RELAYS>16 + #undef MULTI_RELAY_MAX_RELAYS + #define MULTI_RELAY_MAX_RELAYS 16 + #endif #endif #ifndef MULTI_RELAY_PINS @@ -15,21 +20,29 @@ #define ON true #define OFF false +#ifndef PCF8574_ADDRESS + #define PCF8574_ADDRESS 0x20 // some may start at 0x38 +#endif + /* * This usermod handles multiple relay outputs. * These outputs complement built-in relay output in a way that the activation can be delayed. * They can also activate/deactivate in reverse logic independently. + * + * Written and maintained by @blazoncek */ typedef struct relay_t { int8_t pin; - bool active; - bool mode; - bool state; - bool external; - uint16_t delay; - int8_t button; + struct { // reduces memory footprint + bool active : 1; // is the relay waiting to be switched + bool mode : 1; // does On mean 1 or 0 (inverted output) + bool state : 1; // 1 relay is On, 0 relay is Off + bool external : 1; // is the relay externally controlled + int8_t button : 4; // which button triggers relay + }; + uint16_t delay; // amount of ms to wait after it is activated } Relay; @@ -48,7 +61,8 @@ class MultiRelay : public Usermod { bool enabled = false; // needs to be configured (no default config) // status of initialisation bool initDone = false; - + bool usePcf8574 = false; + uint8_t addrPcf8574 = PCF8574_ADDRESS; bool HAautodiscovery = false; uint16_t periodicBroadcastSec = 60; @@ -64,136 +78,28 @@ class MultiRelay : public Usermod { static const char _button[]; static const char _broadcast[]; static const char _HAautodiscovery[]; + static const char _pcf8574[]; + static const char _pcfAddress[]; - void publishMqtt(int relay) { + void handleOffTimer(); + void InitHtmlAPIHandle(); + int getValue(String data, char separator, int index); + uint8_t getActiveRelayCount(); + + byte IOexpanderWrite(byte address, byte _data); + byte IOexpanderRead(int address); + + void publishMqtt(int relay); #ifndef WLED_DISABLE_MQTT - //Check if MQTT Connected, otherwise it will crash the 8266 - if (WLED_MQTT_CONNECTED){ - char subuf[64]; - sprintf_P(subuf, PSTR("%s/relay/%d"), mqttDeviceTopic, relay); - mqtt->publish(subuf, 0, false, _relay[relay].state ? "on" : "off"); - } + void publishHomeAssistantAutodiscovery(); #endif - } - - /** - * switch off the strip if the delay has elapsed - */ - void handleOffTimer() { - unsigned long now = millis(); - bool activeRelays = false; - for (int i=0; i 0 && now - _switchTimerStart > (_relay[i].delay*1000)) { - if (!_relay[i].external) toggleRelay(i); - _relay[i].active = false; - } else if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) { - if (_relay[i].pin>=0) publishMqtt(i); - } - activeRelays = activeRelays || _relay[i].active; - } - if (!activeRelays) _switchTimerStart = 0; - if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) lastBroadcast = now; - } - - /** - * HTTP API handler - * borrowed from: - * https://github.com/gsieben/WLED/blob/master/usermods/GeoGab-Relays/usermod_GeoGab.h - */ - #define GEOGABVERSION "0.1.3" - void InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer - DEBUG_PRINTLN(F("Relays: Initialize HTML API")); - - server.on("/relays", HTTP_GET, [this](AsyncWebServerRequest *request) { - DEBUG_PRINTLN("Relays: HTML API"); - String janswer; - String error = ""; - //int params = request->params(); - janswer = F("{\"NoOfRelays\":"); - janswer += String(MULTI_RELAY_MAX_RELAYS) + ","; - - if (getActiveRelayCount()) { - // Commands - if(request->hasParam("switch")) { - /**** Switch ****/ - AsyncWebParameter* p = request->getParam("switch"); - // Get Values - for (int i=0; ivalue(), ',', i); - if (value==-1) { - error = F("There must be as many arguments as relays"); - } else { - // Switch - if (_relay[i].external) switchRelay(i, (bool)value); - } - } - } else if(request->hasParam("toggle")) { - /**** Toggle ****/ - AsyncWebParameter* p = request->getParam("toggle"); - // Get Values - for (int i=0;ivalue(), ',', i); - if (value==-1) { - error = F("There must be as many arguments as relays"); - } else { - // Toggle - if (value && _relay[i].external) toggleRelay(i); - } - } - } else { - error = F("No valid command found"); - } - } else { - error = F("No active relays"); - } - - // Status response - char sbuf[16]; - for (int i=0; isend(200, "application/json", janswer); - }); - } - - int getValue(String data, char separator, int index) { - int found = 0; - int strIndex[] = {0, -1}; - int maxIndex = data.length()-1; - - for(int i=0; i<=maxIndex && found<=index; i++){ - if(data.charAt(i)==separator || i==maxIndex){ - found++; - strIndex[0] = strIndex[1]+1; - strIndex[1] = (i == maxIndex) ? i+1 : i; - } - } - return found>index ? data.substring(strIndex[0], strIndex[1]).toInt() : -1; - } public: /** * constructor */ - MultiRelay() { - const int8_t defPins[] = {MULTI_RELAY_PINS}; - for (size_t i=0; i=MULTI_RELAY_MAX_RELAYS || _relay[relay].pin<0) return; - _relay[relay].state = mode; - pinMode(_relay[relay].pin, OUTPUT); - digitalWrite(_relay[relay].pin, mode ? !_relay[relay].mode : _relay[relay].mode); - publishMqtt(relay); - } + void switchRelay(uint8_t relay, bool mode); /** * toggle relay @@ -226,359 +133,58 @@ class MultiRelay : public Usermod { switchRelay(relay, !_relay[relay].state); } - uint8_t getActiveRelayCount() { - uint8_t count = 0; - for (int i=0; i=0) count++; - return count; - } - - //Functions called by WLED - -#ifndef WLED_DISABLE_MQTT - /** - * handling of MQTT message - * topic only contains stripped topic (part after /wled/MAC) - * topic should look like: /relay/X/command; where X is relay number, 0 based - */ - bool onMqttMessage(char* topic, char* payload) { - if (strlen(topic) > 8 && strncmp_P(topic, PSTR("/relay/"), 7) == 0 && strncmp_P(topic+8, PSTR("/command"), 8) == 0) { - uint8_t relay = strtoul(topic+7, NULL, 10); - if (relaysubscribe(subuf, 0); - if (HAautodiscovery) publishHomeAssistantAutodiscovery(); - for (int i=0; i= 0 && _relay[i].external) { - StaticJsonDocument<1024> json; - sprintf_P(buf, PSTR("%s Switch %d"), serverDescription, i); //max length: 33 + 8 + 3 = 44 - json[F("name")] = buf; - - sprintf_P(buf, PSTR("%s/relay/%d"), mqttDeviceTopic, i); //max length: 33 + 7 + 3 = 43 - json["~"] = buf; - strcat_P(buf, PSTR("/command")); - mqtt->subscribe(buf, 0); - - json[F("stat_t")] = "~"; - json[F("cmd_t")] = F("~/command"); - json[F("pl_off")] = "off"; - json[F("pl_on")] = "on"; - json[F("uniq_id")] = uid; - - strcpy(buf, mqttDeviceTopic); //max length: 33 + 7 = 40 - strcat_P(buf, PSTR("/status")); - json[F("avty_t")] = buf; - json[F("pl_avail")] = F("online"); - json[F("pl_not_avail")] = F("offline"); - //TODO: dev - payload_size = serializeJson(json, json_str); - } else { - //Unpublish disabled or internal relays - json_str[0] = 0; - payload_size = 0; - } - sprintf_P(buf, PSTR("homeassistant/switch/%s/config"), uid); - mqtt->publish(buf, 0, true, json_str, payload_size); - } - } -#endif - /** * setup() is called once at boot. WiFi is not yet connected at this point. * You can use it to initialize variables, sensors or similar. */ - void setup() { - // pins retrieved from cfg.json (readFromConfig()) prior to running setup() - for (int i=0; i=0 && !_relay[i].external) _relay[i].active = true; - } - } - - handleOffTimer(); - } +#ifndef WLED_DISABLE_MQTT + bool onMqttMessage(char* topic, char* payload); + void onMqttConnect(bool sessionPresent); +#endif /** * 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 (!enabled - || 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 handleButton(uint8_t b); - bool handled = false; - for (int i=0; i WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) - for (int i=0; i 600) { //long press - //longPressAction(b); //not exposed - //handled = false; //use if you want to pass to default behaviour - 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); //not exposed - //handled = false; //use if you want to pass to default behaviour - } else { - buttonWaitTime[b] = now; - } - } - buttonPressedBefore[b] = false; - buttonLongPressed[b] = false; - } - // if 350ms elapsed since last press/release it is a short press - if (buttonWaitTime[b] && now - buttonWaitTime[b] > 350 && !buttonPressedBefore[b]) { - buttonWaitTime[b] = 0; - //shortPressAction(b); //not exposed - for (int i=0; i"); - uiDomString += F(""); - infoArr.add(uiDomString); - } - } - } + void addToJsonInfo(JsonObject &root); /** * 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) { - 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 (int i=0; i() && usermod[FPSTR(_relay_str)].as()>=0) { - int rly = usermod[FPSTR(_relay_str)].as(); - if (usermod["on"].is()) { - switchRelay(rly, usermod["on"].as()); - } else if (usermod["on"].is() && usermod["on"].as()[0] == 't') { - toggleRelay(rly); - } - } - } else if (root[FPSTR(_name)].is()) { - JsonArray relays = root[FPSTR(_name)].as(); - for (JsonVariant r : relays) { - if (r[FPSTR(_relay_str)].is() && r[FPSTR(_relay_str)].as()>=0) { - int rly = r[FPSTR(_relay_str)].as(); - if (r["on"].is()) { - switchRelay(rly, r["on"].as()); - } else if (r["on"].is() && r["on"].as()[0] == 't') { - toggleRelay(rly); - } - } - } - } - } + void readFromJsonState(JsonObject &root); /** * provide the changeable values */ - void addToConfig(JsonObject &root) { - JsonObject top = root.createNestedObject(FPSTR(_name)); + void addToConfig(JsonObject &root); - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_broadcast)] = periodicBroadcastSec; - for (int i=0; i=0) { - pinManager.deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); - } - // allocate new pins - for (int i=0; i=0 && pinManager.allocatePin(_relay[i].pin, true, PinOwner::UM_MultiRelay)) { - if (!_relay[i].external) { - _relay[i].state = !offMode; - switchRelay(i, _relay[i].state); - _oldMode = offMode; - } - } else { - _relay[i].pin = -1; - } - _relay[i].active = false; - } - DEBUG_PRINTLN(F(" config (re)loaded.")); - } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_HAautodiscovery)].isNull(); - } - - /** - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() - { - return USERMOD_ID_MULTI_RELAY; - } + bool readFromConfig(JsonObject &root); }; + +// class implementetion + +void MultiRelay::publishMqtt(int relay) { +#ifndef WLED_DISABLE_MQTT + //Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED){ + char subuf[64]; + sprintf_P(subuf, PSTR("%s/relay/%d"), mqttDeviceTopic, relay); + mqtt->publish(subuf, 0, false, _relay[relay].state ? "on" : "off"); + } +#endif +} + +/** + * switch off the strip if the delay has elapsed + */ +void MultiRelay::handleOffTimer() { + unsigned long now = millis(); + bool activeRelays = false; + for (int i=0; i 0 && now - _switchTimerStart > (_relay[i].delay*1000)) { + if (!_relay[i].external) switchRelay(i, !offMode); + _relay[i].active = false; + } else if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) { + if (_relay[i].pin>=0) publishMqtt(i); + } + activeRelays = activeRelays || _relay[i].active; + } + if (!activeRelays) _switchTimerStart = 0; + if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) lastBroadcast = now; +} + +/** + * HTTP API handler + * borrowed from: + * https://github.com/gsieben/WLED/blob/master/usermods/GeoGab-Relays/usermod_GeoGab.h + */ +#define GEOGABVERSION "0.1.3" +void MultiRelay::InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer + DEBUG_PRINTLN(F("Relays: Initialize HTML API")); + + server.on("/relays", HTTP_GET, [this](AsyncWebServerRequest *request) { + DEBUG_PRINTLN("Relays: HTML API"); + String janswer; + String error = ""; + //int params = request->params(); + janswer = F("{\"NoOfRelays\":"); + janswer += String(MULTI_RELAY_MAX_RELAYS) + ","; + + if (getActiveRelayCount()) { + // Commands + if(request->hasParam("switch")) { + /**** Switch ****/ + AsyncWebParameter* p = request->getParam("switch"); + // Get Values + for (int i=0; ivalue(), ',', i); + if (value==-1) { + error = F("There must be as many arguments as relays"); + } else { + // Switch + if (_relay[i].external) switchRelay(i, (bool)value); + } + } + } else if(request->hasParam("toggle")) { + /**** Toggle ****/ + AsyncWebParameter* p = request->getParam("toggle"); + // Get Values + for (int i=0;ivalue(), ',', i); + if (value==-1) { + error = F("There must be as many arguments as relays"); + } else { + // Toggle + if (value && _relay[i].external) toggleRelay(i); + } + } + } else { + error = F("No valid command found"); + } + } else { + error = F("No active relays"); + } + + // Status response + char sbuf[16]; + for (int i=0; isend(200, "application/json", janswer); + }); +} + +int MultiRelay::getValue(String data, char separator, int index) { + int found = 0; + int strIndex[] = {0, -1}; + int maxIndex = data.length()-1; + + for(int i=0; i<=maxIndex && found<=index; i++){ + if(data.charAt(i)==separator || i==maxIndex){ + found++; + strIndex[0] = strIndex[1]+1; + strIndex[1] = (i == maxIndex) ? i+1 : i; + } + } + return found>index ? data.substring(strIndex[0], strIndex[1]).toInt() : -1; +} + +//Write a byte to the IO expander +byte MultiRelay::IOexpanderWrite(byte address, byte _data ) { + Wire.beginTransmission(addrPcf8574 + address); + Wire.write(_data); + return Wire.endTransmission(); +} + +//Read a byte from the IO expander +byte MultiRelay::IOexpanderRead(int address) { + byte _data = 0; + Wire.requestFrom(addrPcf8574 + address, 1); + if (Wire.available()) { + _data = Wire.read(); + } + return _data; +} + + +// public methods + +MultiRelay::MultiRelay() { + const int8_t defPins[] = {MULTI_RELAY_PINS}; + for (size_t i=0; i=MULTI_RELAY_MAX_RELAYS || (_relay[relay].pin<0 && !usePcf8574)) return; + _relay[relay].state = mode; + if (usePcf8574) { + byte expander = relay/8; + uint16_t state = 0; + for (int i=0; i>(8*expander)); + DEBUG_PRINT(F("PCF8574 Writing to ")); DEBUG_PRINT(addrPcf8574 + expander); DEBUG_PRINT(F(" with data ")); DEBUG_PRINTLN(state>>(8*expander)); + } else { + pinMode(_relay[relay].pin, OUTPUT); + digitalWrite(_relay[relay].pin, mode ? !_relay[relay].mode : _relay[relay].mode); + } + publishMqtt(relay); +} + +uint8_t MultiRelay::getActiveRelayCount() { + uint8_t count = 0; + if (usePcf8574) return MULTI_RELAY_MAX_RELAYS; // we don't know how many there are + for (int i=0; i=0) count++; + return count; +} + + +//Functions called by WLED + +#ifndef WLED_DISABLE_MQTT +/** + * handling of MQTT message + * topic only contains stripped topic (part after /wled/MAC) + * topic should look like: /relay/X/command; where X is relay number, 0 based + */ +bool MultiRelay::onMqttMessage(char* topic, char* payload) { + if (strlen(topic) > 8 && strncmp_P(topic, PSTR("/relay/"), 7) == 0 && strncmp_P(topic+8, PSTR("/command"), 8) == 0) { + uint8_t relay = strtoul(topic+7, NULL, 10); + if (relaysubscribe(subuf, 0); + if (HAautodiscovery) publishHomeAssistantAutodiscovery(); + for (int i=0; i= 0 && _relay[i].external) { + StaticJsonDocument<1024> json; + sprintf_P(buf, PSTR("%s Switch %d"), serverDescription, i); //max length: 33 + 8 + 3 = 44 + json[F("name")] = buf; + + sprintf_P(buf, PSTR("%s/relay/%d"), mqttDeviceTopic, i); //max length: 33 + 7 + 3 = 43 + json["~"] = buf; + strcat_P(buf, PSTR("/command")); + mqtt->subscribe(buf, 0); + + json[F("stat_t")] = "~"; + json[F("cmd_t")] = F("~/command"); + json[F("pl_off")] = "off"; + json[F("pl_on")] = "on"; + json[F("uniq_id")] = uid; + + strcpy(buf, mqttDeviceTopic); //max length: 33 + 7 = 40 + strcat_P(buf, PSTR("/status")); + json[F("avty_t")] = buf; + json[F("pl_avail")] = F("online"); + json[F("pl_not_avail")] = F("offline"); + //TODO: dev + payload_size = serializeJson(json, json_str); + } else { + //Unpublish disabled or internal relays + json_str[0] = 0; + payload_size = 0; + } + sprintf_P(buf, PSTR("homeassistant/switch/%s/config"), uid); + mqtt->publish(buf, 0, true, json_str, payload_size); + } +} +#endif + +/** + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ +void MultiRelay::setup() { + // pins retrieved from cfg.json (readFromConfig()) prior to running setup() + // if we want PCF8574 expander I2C pins need to be valid + if (i2c_sda == i2c_scl && i2c_sda == -1) usePcf8574 = false; + + if (usePcf8574) { + uint16_t state = 0; + for (int i=0; i>(8*expander)); // init expander (set all outputs) + delay(1); + } + DEBUG_PRINTLN(F("PCF8574(s) inited.")); + } else { + for (int i=0; i=0 || usePcf8574) && !_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 MultiRelay::handleButton(uint8_t b) { + yield(); + if (!enabled + || 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 (int i=0; i WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) + for (int i=0; i 600) { //long press + //longPressAction(b); //not exposed + //handled = false; //use if you want to pass to default behaviour + 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); //not exposed + //handled = false; //use if you want to pass to default behaviour + } else { + buttonWaitTime[b] = now; + } + } + buttonPressedBefore[b] = false; + buttonLongPressed[b] = false; + } + // if 350ms elapsed since last press/release it is a short press + if (buttonWaitTime[b] && now - buttonWaitTime[b] > 350 && !buttonPressedBefore[b]) { + buttonWaitTime[b] = 0; + //shortPressAction(b); //not exposed + for (int i=0; i"); + uiDomString += F(""); + infoArr.add(uiDomString); + } + } +} + +/** + * 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 MultiRelay::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 (int i=0; i() && usermod[FPSTR(_relay_str)].as()>=0) { + int rly = usermod[FPSTR(_relay_str)].as(); + if (usermod["on"].is()) { + switchRelay(rly, usermod["on"].as()); + } else if (usermod["on"].is() && usermod["on"].as()[0] == 't') { + toggleRelay(rly); + } + } + } else if (root[FPSTR(_name)].is()) { + JsonArray relays = root[FPSTR(_name)].as(); + for (JsonVariant r : relays) { + if (r[FPSTR(_relay_str)].is() && r[FPSTR(_relay_str)].as()>=0) { + int rly = r[FPSTR(_relay_str)].as(); + if (r["on"].is()) { + switchRelay(rly, r["on"].as()); + } else if (r["on"].is() && r["on"].as()[0] == 't') { + toggleRelay(rly); + } + } + } + } +} + +/** + * provide the changeable values + */ +void MultiRelay::addToConfig(JsonObject &root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_pcf8574)] = usePcf8574; + top[FPSTR(_pcfAddress)] = addrPcf8574; + top[FPSTR(_broadcast)] = periodicBroadcastSec; + top[FPSTR(_HAautodiscovery)] = HAautodiscovery; + for (int i=0; i(not hex!)','address');")); + oappend(SET_F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); + oappend(SET_F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); +} + +/** + * restore the changeable values + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ +bool MultiRelay::readFromConfig(JsonObject &root) { + int8_t oldPin[MULTI_RELAY_MAX_RELAYS]; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + //bool configComplete = !top.isNull(); + //configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + enabled = top[FPSTR(_enabled)] | enabled; + usePcf8574 = top[FPSTR(_pcf8574)] | usePcf8574; + addrPcf8574 = top[FPSTR(_pcfAddress)] | addrPcf8574; + // if I2C is not globally initialised just ignore + if (i2c_sda == i2c_scl && i2c_sda == -1) usePcf8574 = false; + periodicBroadcastSec = top[FPSTR(_broadcast)] | periodicBroadcastSec; + periodicBroadcastSec = min(900,max(0,(int)periodicBroadcastSec)); + HAautodiscovery = top[FPSTR(_HAautodiscovery)] | HAautodiscovery; + + for (int i=0; i=0) { + pinManager.deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); + } + // allocate new pins + for (int i=0; i=0 && pinManager.allocatePin(_relay[i].pin, true, PinOwner::UM_MultiRelay)) { + if (!_relay[i].external) { + _relay[i].state = !offMode; + switchRelay(i, _relay[i].state); + _oldMode = offMode; + } + } else { + _relay[i].pin = -1; + } + _relay[i].active = false; + } + DEBUG_PRINTLN(F(" config (re)loaded.")); + } + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_pcf8574)].isNull(); +} + // strings to reduce flash memory usage (used more than twice) -const char MultiRelay::_name[] PROGMEM = "MultiRelay"; -const char MultiRelay::_enabled[] PROGMEM = "enabled"; -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"; -const char MultiRelay::_broadcast[] PROGMEM = "broadcast-sec"; +const char MultiRelay::_name[] PROGMEM = "MultiRelay"; +const char MultiRelay::_enabled[] PROGMEM = "enabled"; +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"; +const char MultiRelay::_broadcast[] PROGMEM = "broadcast-sec"; const char MultiRelay::_HAautodiscovery[] PROGMEM = "HA-autodiscovery"; +const char MultiRelay::_pcf8574[] PROGMEM = "use-PCF8574"; +const char MultiRelay::_pcfAddress[] PROGMEM = "first-PCF8574"; diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 7b61f489..948ce2ef 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2805,7 +2805,7 @@ uint16_t mode_bouncing_balls(void) { // number of balls based on intensity setting to max of 7 (cycles colors) // non-chosen color is a random color uint16_t numBalls = (SEGMENT.intensity * (maxNumBalls - 1)) / 255 + 1; // minimum 1 ball - constexpr float gravity = -9.81; // standard value of gravity + const float gravity = -9.81f; // standard value of gravity const bool hasCol2 = SEGCOLOR(2); const unsigned long time = millis(); @@ -4177,11 +4177,9 @@ static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# By Stefan Seegel */ uint16_t mode_washing_machine(void) { - float speed = tristate_square8(strip.now >> 7, 90, 15); - float quot = 32.0f - ((float)SEGMENT.speed / 16.0f); - speed /= quot; + int speed = tristate_square8(strip.now >> 7, 90, 15); - SEGENV.step += (speed * 128.0f); + SEGENV.step += (speed * 2048) / (512 - SEGMENT.speed); for (int i = 0; i < SEGLEN; i++) { uint8_t col = sin8(((SEGMENT.intensity / 25 + 1) * 255 * i / SEGLEN) + (SEGENV.step >> 7)); @@ -4595,7 +4593,7 @@ uint16_t mode_2DBlackHole(void) { // By: Stepko https://editor.soulma } SEGMENT.fadeToBlackBy(16 + (SEGMENT.speed>>3)); // create fading trails - float t = (float)(millis())/128; // timebase + unsigned long t = millis()/128; // timebase // outer stars for (size_t i = 0; i < 8; i++) { x = beatsin8(SEGMENT.custom1>>3, 0, cols - 1, 0, ((i % 2) ? 128 : 0) + t * i); diff --git a/wled00/FX.h b/wled00/FX.h index f70f989f..f1d6f303 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -360,7 +360,8 @@ typedef struct Segment { bool mirror_y : 1; // 8 : mirrored Y (2D) bool transpose : 1; // 9 : transposed (2D, swapped X & Y) uint8_t map1D2D : 3; // 10-12 : mapping for 1D effect on 2D (0-use as strip, 1-expand vertically, 2-circular/arc, 3-rectangular/corner, ...) - uint8_t soundSim : 3; // 13-15 : 0-7 sound simulation types + uint8_t soundSim : 1; // 13 : 0-1 sound simulation types ("soft" & "hard" or "on"/"off") + uint8_t set : 2; // 14-15 : 0-3 UI segment sets/groups }; }; uint8_t grouping, spacing; @@ -533,7 +534,7 @@ typedef struct Segment { void allocLeds(); //WLEDMM - void set(uint16_t i1, uint16_t i2, uint8_t grp=1, uint8_t spc=0, uint16_t ofs=UINT16_MAX, uint16_t i1Y=0, uint16_t i2Y=1); + void setUp(uint16_t i1, uint16_t i2, uint8_t grp=1, uint8_t spc=0, uint16_t ofs=UINT16_MAX, uint16_t i1Y=0, uint16_t i2Y=1); bool setColor(uint8_t slot, uint32_t c); //returns true if changed void setCCT(uint16_t k); void setOpacity(uint8_t o); @@ -804,6 +805,7 @@ class WS2812FX { // 96 bytes getActiveSegmentsNum(void), getFirstSelectedSegId(void), getLastActiveSegmentId(void), + getActiveSegsLightCapabilities(bool selectedOnly = false), setPixelSegment(uint8_t n); inline uint8_t getBrightness(void) { return _brightness; } diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 2bba4088..2733ee62 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -185,12 +185,12 @@ bool Segment::allocateData(size_t len) { if (data && _dataLen == len) return true; //already allocated deallocateData(); if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) return false; //not enough memory - // if possible use SPI RAM on ESP32 - #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) - if (psramFound()) - data = (byte*) ps_malloc(len); - else - #endif + // do not use SPI RAM on ESP32 since it is slow + //#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) + //if (psramFound()) + // data = (byte*) ps_malloc(len); + //else + //#endif data = (byte*) malloc(len); if (!data) return false; //allocation failed Segment::addUsedSegmentData(len); @@ -437,7 +437,7 @@ void Segment::handleTransition() { } } -void Segment::set(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y) { +void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y) { //return if neither bounds nor grouping have changed bool boundsUnchanged = (start == i1 && stop == i2); #ifndef WLED_DISABLE_2D @@ -541,7 +541,7 @@ void Segment::setMode(uint8_t fx, bool loadDefaults) { sOpt = extractModeDefaults(fx, "o3"); check3 = (sOpt >= 0) ? (bool)sOpt : false; //WLEDMM: return to old setting if not explicitly set sOpt = extractModeDefaults(fx, "m12"); if (sOpt >= 0) {if (oldMap==-1) oldMap = map1D2D; map1D2D = constrain(sOpt, 0, 7);} else {if (oldMap!=-1) map1D2D = oldMap; oldMap = -1;} - sOpt = extractModeDefaults(fx, "si"); if (sOpt >= 0) {if (oldSim==-1) oldSim = soundSim; soundSim = constrain(sOpt, 0, 7);} else {if (oldSim!=-1) soundSim = oldSim; oldSim = -1;} + sOpt = extractModeDefaults(fx, "si"); if (sOpt >= 0) {if (oldSim==-1) oldSim = soundSim; soundSim = constrain(sOpt, 0, 1);} else {if (oldSim!=-1) soundSim = oldSim; oldSim = -1;} sOpt = extractModeDefaults(fx, "rev"); if (sOpt >= 0) {if (oldReverse==-1) oldReverse = reverse; reverse = (bool)sOpt;} else {if (oldReverse!=-1) reverse = oldReverse==1; oldReverse = -1;} sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) {if (oldMirror==-1) oldMirror = mirror; mirror = (bool)sOpt;} else {if (oldMirror!=-1) mirror = oldMirror==1; oldMirror = -1;} // NOTE: setting this option is a risky business sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) {if (oldReverse_y==-1) oldReverse_y = reverse_y; reverse_y = (bool)sOpt;} else {if (oldReverse_y!=-1) reverse_y = oldReverse_y==1; oldReverse_y = -1;} @@ -1073,7 +1073,7 @@ uint8_t Segment::differs(Segment& b) const { if (startY != b.startY) d |= SEG_DIFFERS_BOUNDS; if (stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; - //bit pattern: (msb first) sound:3, mapping:3, transposed, mirrorY, reverseY, [transitional, reset,] paused, mirrored, on, reverse, [selected] + //bit pattern: (msb first) set:2, sound:1, mapping:3, transposed, mirrorY, reverseY, [transitional, reset,] paused, mirrored, on, reverse, [selected] if ((options & 0b1111111110011110U) != (b.options & 0b1111111110011110U)) d |= SEG_DIFFERS_OPT; if ((options & 0x0001U) != (b.options & 0x0001U)) d |= SEG_DIFFERS_SEL; for (uint8_t i = 0; i < NUM_COLORS; i++) if (colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; @@ -1796,6 +1796,14 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) { } } +uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) { + uint8_t totalLC = 0; + for (segment &seg : _segments) { + if (seg.isActive() && (!selectedOnly || seg.isSelected())) totalLC |= seg.getLightCapabilities(); + } + return totalLC; +} + uint8_t WS2812FX::getFirstSelectedSegId(void) { size_t i = 0; @@ -1893,7 +1901,7 @@ Segment& WS2812FX::getSegment(uint8_t id) { void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping, uint8_t spacing, uint16_t offset, uint16_t startY, uint16_t stopY) { if (n >= _segments.size()) return; - _segments[n].set(i1, i2, grouping, spacing, offset, startY, stopY); + _segments[n].setUp(i1, i2, grouping, spacing, offset, startY, stopY); } void WS2812FX::restartRuntime() { diff --git a/wled00/alexa.cpp b/wled00/alexa.cpp index c122402a..179a522c 100644 --- a/wled00/alexa.cpp +++ b/wled00/alexa.cpp @@ -101,20 +101,27 @@ void onAlexaChange(EspalexaDevice* dev) { byte rgbw[4]; uint16_t ct = dev->getCt(); - if (!ct) return; - uint16_t k = 1000000 / ct; //mireds to kelvin - - if (strip.hasCCTBus()) { - strip.setCCT(k); - rgbw[0]= 0; rgbw[1]= 0; rgbw[2]= 0; rgbw[3]= 255; - } else if (strip.hasWhiteChannel()) { + if (!ct) return; + uint16_t k = 1000000 / ct; //mireds to kelvin + + if (strip.hasCCTBus()) { + bool hasManualWhite = strip.getActiveSegsLightCapabilities(true) & SEG_CAPABILITY_W; + + strip.setCCT(k); + if (hasManualWhite) { + rgbw[0] = 0; rgbw[1] = 0; rgbw[2] = 0; rgbw[3] = 255; + } else { + rgbw[0] = 255; rgbw[1] = 255; rgbw[2] = 255; rgbw[3] = 0; + dev->setValue(255); + } + } else if (strip.hasWhiteChannel()) { switch (ct) { //these values empirically look good on RGBW case 199: rgbw[0]=255; rgbw[1]=255; rgbw[2]=255; rgbw[3]=255; break; case 234: rgbw[0]=127; rgbw[1]=127; rgbw[2]=127; rgbw[3]=255; break; case 284: rgbw[0]= 0; rgbw[1]= 0; rgbw[2]= 0; rgbw[3]=255; break; case 350: rgbw[0]=130; rgbw[1]= 90; rgbw[2]= 0; rgbw[3]=255; break; case 383: rgbw[0]=255; rgbw[1]=153; rgbw[2]= 0; rgbw[3]=255; break; - default : colorKtoRGB(k, rgbw); + default : colorKtoRGB(k, rgbw); } } else { colorKtoRGB(k, rgbw); diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index f4a9af29..6819c935 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -247,7 +247,11 @@ #define B_SS_LPO_3 NeoPixelBusLg //WS2801 +#ifdef WLED_USE_ETHERNET +#define B_HS_WS1_3 NeoPixelBusLg>, NeoGammaNullMethod> +#else #define B_HS_WS1_3 NeoPixelBusLg +#endif #define B_SS_WS1_3 NeoPixelBusLg //P9813 diff --git a/wled00/button.cpp b/wled00/button.cpp index d51040a8..d45274a6 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -227,9 +227,8 @@ void handleButton() static unsigned long lastRun = 0UL; unsigned long now = millis(); - //if (strip.isUpdating()) return; // don't interfere with strip updates. Our button will still be there in 1ms (next cycle) - if (strip.isUpdating() && (millis() - lastRun < 400)) return; // be niced, but avoid button starvation - lastRun = millis(); + 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; for (uint8_t b=0; b ANALOG_BTN_READ_CYCLE) { handleAnalog(b); lastRead = now; @@ -248,14 +247,23 @@ void handleButton() continue; } - //button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0) + // 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; } - //momentary button logic - if (isButtonPressed(b)) { //pressed + // 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; buttonPressedBefore[b] = true; @@ -270,9 +278,15 @@ void handleButton() } } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released - long dur = now - buttonPressedTime[b]; - if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} //too short "press", debounce + + // 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? buttonWaitTime[b] = 0; diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 827e0210..b570feec 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -480,6 +480,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { getStringFromJson(mqttDeviceTopic, if_mqtt[F("topics")][F("device")], 33); // "wled/test" getStringFromJson(mqttGroupTopic, if_mqtt[F("topics")][F("group")], 33); // "" + CJSON(retainMqttMsg, if_mqtt[F("rtn")]); #endif #ifndef WLED_DISABLE_HUESYNC @@ -956,6 +957,7 @@ void serializeConfig() { if_mqtt[F("user")] = mqttUser; if_mqtt[F("pskl")] = strlen(mqttPass); if_mqtt[F("cid")] = mqttClientID; + if_mqtt[F("rtn")] = retainMqttMsg; JsonObject if_mqtt_topics = if_mqtt.createNestedObject(F("topics")); if_mqtt_topics[F("device")] = mqttDeviceTopic; diff --git a/wled00/const.h b/wled00/const.h index b337ac2a..c7ba011f 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -12,6 +12,7 @@ #define DEFAULT_AP_SSID "WLED-AP" #define DEFAULT_AP_PASS "wled1234" #define DEFAULT_OTA_PASS "wledota" +#define DEFAULT_MDNS_NAME "x" //increase if you need more #ifndef WLED_MAX_USERMODS diff --git a/wled00/data/index.css b/wled00/data/index.css index 6e68cd84..08654e1c 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -174,37 +174,62 @@ button { } .slider-icon { - /*transform: translate(3px,3px);*/ position: absolute; left: 8px; bottom: 5px; } -.e-icon { - transform: translateY(3px); -} - .sel-icon { transform: translateX(3px); } -.e-icon, .sel-icon, .slider-icon { +.e-icon, .g-icon, .sel-icon, .slider-icon { cursor: pointer; color: var(--c-d); } +.g-icon { + font-style: normal; + position: absolute; + top: 8px; + right: 8px; +} + +/* pop-up container */ +.pop { + position: absolute; + display: inline-block; + top: 0; + right: 0; +} + +/* pop-up content (segment sets) */ +.pop-c { + position: absolute; + background-color: var(--c-2); + border: 1px solid var(--c-8); + border-radius: 20px; + z-index: 1; + top: 3px; + right: 35px; + padding: 3px 8px 1px; + font-size: 24px; + line-height: 24px; +} +.pop-c span { + padding: 2px 6px; +} + .search-icon { position: absolute; top: 8px; left: 12px; - /*pointer-events: none;*/ width: 24px; height: 24px; } .clear-icon { position: absolute; - display: none; top: 8px; right: 9px; cursor: pointer; @@ -232,14 +257,12 @@ button { #liveview { height: 4px; - display: none; width: 100%; border: 0px; } #liveview2D { height: 90%; - display: none; width: 90%; border: 0px; position: absolute; @@ -482,13 +505,6 @@ button { opacity: 1; } -#pql, .edit-icon { - display: none; -} - -.hide { - display: none !important; -} .fade { visibility: hidden; /* hide it */ opacity: 0; /* make it transparent */ @@ -567,7 +583,6 @@ button { position: fixed; top: calc(var(--th) + 5px); left: 1px; - display: none; cursor: pointer; } @@ -674,7 +689,9 @@ img { #wbal .sliderdisplay { background: linear-gradient(90deg, #ff8f1f 0%, #fff 50%, #cbdbff); } /* wrapper divs hidden by default */ -#rgbwrap, #swrap, #hwrap, #kwrap, #wwrap, #wbal, #qcs-w, #hexw { +#liveview, #liveview2D, #roverstar, #pql +#rgbwrap, #swrap, #hwrap, #kwrap, #wwrap, #wbal, #qcs-w, #hexw, +.clear-icon, .edit-icon, .ptxt { display: none; } @@ -783,8 +800,8 @@ input[type=range]::-moz-range-thumb { -webkit-transform:translate3d(0,0,0); backface-visibility: hidden; transform:translate3d(0,0,0); - overflow: clip; - text-overflow: clip; + overflow: hidden; + text-overflow: ellipsis; border: 1px solid var(--c-3); background-color: var(--c-3); } @@ -815,6 +832,7 @@ input[type=range]::-moz-range-thumb { .btn-xs, .btn-pl-del, .btn-pl-add { width: 42px !important; height: 42px !important; + text-overflow: clip; } .btn-xs { margin: 2px 0 0 0; @@ -991,8 +1009,7 @@ textarea { } .ptxt { - margin: -1px 4px 8px !important; - display: none; + margin: -1px 4px 8px !important; } .stxt { @@ -1002,7 +1019,7 @@ textarea { .segname, .pname { white-space: nowrap; text-align: center; - overflow: clip; + overflow: hidden; text-overflow: ellipsis; line-height: 24px; padding: 8px 24px; @@ -1060,11 +1077,9 @@ textarea { #csl .xxs { border: 2px solid var(--c-d) !important; - /*box-shadow: 0 0 0 2px var(--c-d);*/ } #csl .xxs-w { border-width: 5px !important; - /*box-shadow: 0 0 0 5px var(--c-d);*/ } .qcs, #namelabel { /* text shadow for name to be legible on grey backround */ @@ -1078,7 +1093,6 @@ textarea { .pwr { color: var(--c-6); - transform: translate(1px, 1px); cursor: pointer; } @@ -1093,18 +1107,13 @@ textarea { } .frz { - left: 32px; + left: 10px; position: absolute; - top: -3px; + top: 8px; cursor: pointer; - padding: 8px; z-index: 1; } -.expanded .frz { - display: none; -} - /* radiobuttons and checkmarks */ .check, .radio { display: block; @@ -1246,7 +1255,6 @@ TD .checkmark, TD .radiomark { .lbl-s { display: inline-block; - /* margin: 10px 4px 0 0; */ margin-top: 6px; font-size: 13px; width: 48%; @@ -1296,10 +1304,6 @@ TD .checkmark, TD .radiomark { background-color: var(--c-3); } -/*.selected .radiomark { - border: 1px solid var(--c-3); -}*/ - /* selected list item */ .lstI.selected { top: 0; @@ -1417,7 +1421,7 @@ TD .checkmark, TD .radiomark { .expanded { display: inline-block !important; } -.expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide { +.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .frz, .expanded .g-icon { display: none !important; } @@ -1553,11 +1557,7 @@ TD .checkmark, TD .radiomark { left: 12px; } .segname { - padding: 8px 16px; - max-width: 140px; - } - .segname, .pname { - max-width: 134px; + max-width: calc(100% - 110px); } .segt TD { padding: 0 !important; diff --git a/wled00/data/index.htm b/wled00/data/index.htm index b3093816..e0c9ca54 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -74,7 +74,7 @@

Brightness

-
+
diff --git a/wled00/data/index.js b/wled00/data/index.js index c25ac8a4..6b5600e6 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -737,6 +737,14 @@ function populateSegments(s) ledmapFileNames.push((inst.n?inst.n:"default") + ".json"); //WLEDMM + // segment set icon color + let cG = "var(--c-b)"; + switch (inst.set) { + case 1: cG = "var(--c-r)"; break; + case 2: cG = "var(--c-g)"; break; + case 3: cG = "var(--c-l)"; break; + } + let segp = `
`+ ``+ `
`+ @@ -748,97 +756,99 @@ function populateSegments(s) let stoX = inst.stop; let staY = inst.startY; let stoY = inst.stopY; + let isMSeg = isM && staXReverse ${isM?'':'direction'}`; let miXck = ``; let rvYck = "", miYck =""; - if (isM && staXReverse`; miYck = ``; } // WLEDMM: jMap - let map2D = `
Expand 1D FX
-
-
`; - let sndSim = `
Sound sim
-
-
`; + let map2D = `
Expand 1D FX
`+ + `
`+ + `
`; + let sndSim = `
Sound sim
`+ + `
`+ + `
`; //WLEDMM ARTIFX let fxName = eJson.find((o)=>{return o.id==selectedFx}).name; let cusEff = `
`; - cn += `
- - &#x${inst.frz ? (li.live && li.liveseg==i?'e410':'e0e8') : 'e325'}; -
- ${inst.n ? inst.n : "Segment "+i} - -
- - ${cfg.comp.segpwr?segp:''} -
- - - - - - - - - - - ${isM&&staX'+ - ''+ - ''+ - ''+ - ''+ - '':''} - - - - - - - - - - -
${isM&&staX - ${isM&&staX - ${isM&&staX -
${isM&&staX'+rvXck:''}
Start Y'+(cfg.comp.seglen?'Height':'Stop Y')+'
'+miYck+'
'+rvYck+'
GroupingSpacing
-
- ${!(isM&&staX1&&stoX-staX>1?map2D:''} - ${s.AudioReactive && s.AudioReactive.on ? "" : sndSim} - ${s.ARTIFX && s.ARTIFX.on && fxName.includes("ARTI-FX") ? cusEff : ""} - -
- - -
-
- ${cfg.comp.segpwr?'':segp} -
`; + cn += `
`+ + ``+ + `
`+ + `&#x${inst.frz ? (li.live && li.liveseg==i?'e410':'e0e8') : 'e325'};`+ + (inst.n ? inst.n : "Segment "+i) + + `
`+ + `ɸ${String.fromCharCode(inst.set+"A".charCodeAt(0))};`+ + `
`+ + `
`+ + ``+ + `
`+ + ``+ + (cfg.comp.segpwr ? segp : '') + + `
`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + (isMSeg ? ''+ + ''+ + ''+ + ''+ + ''+ + '' : '') + + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `
${isMSeg?'Start X':'Start LED'}${isMSeg?(cfg.comp.seglen?"Width":"Stop X"):(cfg.comp.seglen?"LED count":"Stop LED")}${isMSeg?'':'Offset'}
${isMSeg?miXck+'
'+rvXck:''}
Start Y'+(cfg.comp.seglen?'Height':'Stop Y')+'
'+miYck+'
'+rvYck+'
GroupingSpacing
`+ + `
`+ + (!isMSeg ? rvXck : '') + + (isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') + + (s.AudioReactive && s.AudioReactive.on ? "" : sndSim) + + (s.ARTIFX && s.ARTIFX.on && fxName.includes("ARTI-FX") ? cusEff : "") + + ``+ + `
`+ + ``+ + ``+ + `
`+ + `
`+ + (cfg.comp.segpwr ? '' : segp) + + `
`; } gId('segcont').innerHTML = cn; @@ -855,7 +865,7 @@ function populateSegments(s) gId(`segd${lSeg}`).classList.add("hide"); gId(`segp0`).classList.add("hide"); } - if (!isM && !noNewSegs && (cfg.comp.seglen?parseInt(gId(`seg${lSeg}s`).value):0)+parseInt(gId(`seg${lSeg}e`).value) 1) ? "block":"none"; // rsbtn parent if (Array.isArray(li.maps) && li.maps.length>0) { //WLEDMM >0 instead of 1 to show also first ledmap. Attention: WLED AC has isM check, in MM Matrices are supported so do not check on isM @@ -1010,8 +1020,8 @@ function genPalPrevCss(id) function generateListItemHtml(listName, id, name, clickAction, extraHtml = '', effectPar = '') { - return `
`+ - `